whereRaw('lower(customer_email) = ?', [strtolower($request->user()->email)]); } public function index(Request $request) { $licenses = $this->owned($request)->latest()->get(); $hasProfessional = $licenses->contains(fn (License $license) => $license->is_usable && $license->edition === 'Professional'); return view('portal.index', compact('licenses', 'hasProfessional')); } public function show(Request $request, License $license) { $this->authorise($request, $license); $license->load(['product', 'activations']); return view('portal.show', compact('license')); } public function upgrade(Request $request, License $license): RedirectResponse { $this->authorise($request, $license); abort_unless($license->payment_provider === 'paddle' && $license->external_subscription_id, 422); abort_unless($license->edition === 'Personal' && $license->is_usable, 422); $targetPriceID = $this->professionalPriceFor($license); $targetProduct = LicenseProduct::query() ->where('payment_provider', 'paddle') ->where('external_price_id', $targetPriceID) ->where('active', true) ->firstOrFail(); try { $subscription = $this->paddle->updateSubscriptionPrice($license->external_subscription_id, $targetPriceID); } catch (RuntimeException $exception) { return back()->withErrors(['licence' => $exception->getMessage()]); } // Apply the entitlement immediately. Paddle webhooks remain authoritative and // will reconcile subscription status/current-period details afterwards. $license->update([ 'license_product_id' => $targetProduct->id, 'edition' => $targetProduct->edition, 'activation_limit' => $targetProduct->activation_limit, 'max_major_version' => $targetProduct->max_major_version, 'external_price_id' => $targetPriceID, 'subscription_status' => (string) ($subscription['status'] ?? $license->subscription_status), 'revoked_at' => null, 'revoke_reason' => null, ]); return redirect()->route('portal.show', $license)->with('status', 'Upgraded to Professional. Your existing Paddle subscription was changed; no second subscription was created.'); } public function cancel(Request $request, License $license): RedirectResponse { $this->authorise($request, $license); abort_unless($license->payment_provider === 'paddle' && $license->external_subscription_id, 422); abort_unless(! in_array($license->subscription_status, ['canceled'], true), 422); try { $this->paddle->cancelSubscriptionImmediately($license->external_subscription_id); } catch (RuntimeException $exception) { return back()->withErrors(['licence' => $exception->getMessage()]); } $license->update([ 'subscription_status' => 'canceled', 'expires_at' => now(), 'revoked_at' => now(), 'revoke_reason' => 'Subscription canceled from licence portal', 'cancel_scheduled_at' => null, ]); return redirect()->route('portal.index')->with('status', 'Subscription canceled in Paddle and the licence is no longer active.'); } public function deactivate(Request $request, License $license, int $activation) { $this->authorise($request, $license); $item = $license->activations()->whereKey($activation)->firstOrFail(); if (! $item->deactivated_at) { $item->update(['deactivated_at' => now()]); } return back()->with('status', 'Mac deactivated. SonarDesk will detect this through the licensing API.'); } public function resend(Request $request, License $license) { $this->authorise($request, $license); $out = LicenseDeliveryOutbox::where('license_id', $license->id)->first(); if (! $out) { return back()->withErrors(['licence' => 'This licence was issued manually and its full key is not recoverable. Contact support.']); } $out->update([ 'recipient_email' => strtolower($request->user()->email), 'delivered_at' => null, ]); $this->delivery->deliverPending(1); return back()->with('status', 'Licence key sent to your verified email address.'); } private function authorise(Request $request, License $license): void { abort_unless(strtolower((string) $license->customer_email) === strtolower($request->user()->email), 404); } private function professionalPriceFor(License $license): string { $paddle = config('paddle'); $current = (string) $license->external_price_id; if ($current === (string) ($paddle['personal_annual_price_id'] ?? '')) { return (string) ($paddle['professional_annual_price_id'] ?? ''); } return (string) ($paddle['professional_monthly_price_id'] ?? ''); } }