From 1135915797eb6940aa4ba2ee8655833736e735ef Mon Sep 17 00:00:00 2001 From: isaacclad Date: Sat, 6 Jun 2026 17:02:44 +0000 Subject: [PATCH] Fix 500 when exiting hosting panel for existing accounts. The account overview queried a local mailboxes table that the extracted hosting app does not have; guard those lookups and skip email-addon upsells when mailboxes are managed on Ladill Email. Co-authored-by: Cursor --- .../Hosting/HostingProductController.php | 7 ++-- app/Jobs/DeactivateMailboxJob.php | 2 +- app/Models/HostingAccount.php | 32 +++++++++++++++++-- .../Hosting/HostingMailboxService.php | 25 ++++++++++----- .../Hosting/UpsellRecommendationService.php | 17 ++++++---- 5 files changed, 62 insertions(+), 21 deletions(-) diff --git a/app/Http/Controllers/Hosting/HostingProductController.php b/app/Http/Controllers/Hosting/HostingProductController.php index 76d1eac..b8a92a3 100644 --- a/app/Http/Controllers/Hosting/HostingProductController.php +++ b/app/Http/Controllers/Hosting/HostingProductController.php @@ -260,10 +260,11 @@ class HostingProductController extends Controller $upsells ??= app(UpsellRecommendationService::class); $renewals ??= app(HostingRenewalCheckoutService::class); - $account->load(['product', 'user', 'node', 'sites', 'mailboxes']); + $account->load(['product', 'user', 'node', 'sites']); $maxDomains = $account->resource_limits['max_domains'] ?? $account->product?->max_domains ?? 1; $freeEmailAllowance = $account->freeEmailAllowance(); - $mailboxCount = $account->mailboxes->count(); + $mailboxes = $account->loadedMailboxes(); + $mailboxCount = $mailboxes->count(); $extraMailboxCount = $freeEmailAllowance === null ? 0 : max($mailboxCount - $freeEmailAllowance, 0); return view('hosting.account', [ @@ -276,7 +277,7 @@ class HostingProductController extends Controller 'mailbox_count' => $mailboxCount, 'free_allowance' => $freeEmailAllowance, 'extra_count' => $extraMailboxCount, - 'paid_count' => $account->mailboxes->where('is_paid', true)->count(), + 'paid_count' => $mailboxes->where('is_paid', true)->count(), 'extra_total' => round($extraMailboxCount * 5, 2), ], 'upsellRecommendations' => $upsells->recommendationsForAccount($account), diff --git a/app/Jobs/DeactivateMailboxJob.php b/app/Jobs/DeactivateMailboxJob.php index 48184c6..2693f40 100644 --- a/app/Jobs/DeactivateMailboxJob.php +++ b/app/Jobs/DeactivateMailboxJob.php @@ -52,7 +52,7 @@ class DeactivateMailboxJob implements ShouldQueue $mailbox->delete(); if ($hostingAccountId) { - $account = HostingAccount::query()->with(['product', 'mailboxes'])->find($hostingAccountId); + $account = HostingAccount::query()->with(['product'])->find($hostingAccountId); if ($account) { $hostingMailboxes->syncAddonInvoice($account); } diff --git a/app/Models/HostingAccount.php b/app/Models/HostingAccount.php index 23d3d6d..782e0a8 100644 --- a/app/Models/HostingAccount.php +++ b/app/Models/HostingAccount.php @@ -3,6 +3,7 @@ namespace App\Models; use Carbon\CarbonInterface; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Model; @@ -10,6 +11,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Support\Facades\Schema; class HostingAccount extends Model { @@ -159,6 +161,28 @@ class HostingAccount extends Model return $this->hasMany(Mailbox::class, 'hosting_account_id'); } + /** The extracted hosting app does not store mailboxes locally — they live on Ladill Email. */ + public static function tracksLocalMailboxes(): bool + { + static $hasTable = null; + + return $hasTable ??= Schema::hasTable('mailboxes'); + } + + /** @return Collection */ + public function loadedMailboxes(): Collection + { + if (! static::tracksLocalMailboxes()) { + return new Collection; + } + + if (! $this->relationLoaded('mailboxes')) { + $this->load('mailboxes'); + } + + return $this->mailboxes; + } + public function billingInvoices(): HasMany { return $this->hasMany(HostingBillingInvoice::class, 'hosting_account_id'); @@ -268,9 +292,11 @@ class HostingAccount extends Model public function paidMailboxCount(): int { - return $this->relationLoaded('mailboxes') - ? $this->mailboxes->where('is_paid', true)->count() - : $this->mailboxes()->where('is_paid', true)->count(); + if (! static::tracksLocalMailboxes()) { + return 0; + } + + return $this->loadedMailboxes()->where('is_paid', true)->count(); } public function renew(?int $durationMonths = null, array $metadata = []): void diff --git a/app/Services/Hosting/HostingMailboxService.php b/app/Services/Hosting/HostingMailboxService.php index d7388c0..7363e4e 100644 --- a/app/Services/Hosting/HostingMailboxService.php +++ b/app/Services/Hosting/HostingMailboxService.php @@ -26,11 +26,12 @@ class HostingMailboxService */ public function usageSummary(HostingAccount $account): array { - $account->loadMissing(['product', 'mailboxes']); + $account->loadMissing(['product']); + $mailboxes = $account->loadedMailboxes(); $freeAllowance = $account->freeEmailAllowance(); - $mailboxCount = $account->mailboxes->count(); - $paidCount = $account->mailboxes->where('is_paid', true)->count(); + $mailboxCount = $mailboxes->count(); + $paidCount = $mailboxes->where('is_paid', true)->count(); $freeUsed = $freeAllowance === null ? $mailboxCount : min($mailboxCount, $freeAllowance); $extraCount = max($mailboxCount - ($freeAllowance ?? $mailboxCount), 0); @@ -53,7 +54,15 @@ class HostingMailboxService public function createMailbox(HostingAccount $account, array $input): array { - $account->loadMissing(['product', 'mailboxes', 'sites']); + $account->loadMissing(['product', 'sites']); + + if (! HostingAccount::tracksLocalMailboxes()) { + throw ValidationException::withMessages([ + 'local_part' => 'Mailbox creation is managed in Ladill Email.', + ]); + } + + $account->loadMissing(['mailboxes']); $domain = Domain::query() ->whereKey((int) ($input['domain_id'] ?? 0)) @@ -113,11 +122,11 @@ class HostingMailboxService ProvisionMailboxJob::dispatch($mailbox->id, encrypt((string) $input['password'])); - $invoice = $this->syncAddonInvoice($account->fresh(['mailboxes'])); + $invoice = $this->syncAddonInvoice($account->fresh()); return [ 'mailbox' => $mailbox->fresh(['domain']), - 'usage' => $this->usageSummary($account->fresh(['product', 'mailboxes'])), + 'usage' => $this->usageSummary($account->fresh(['product'])), 'invoice' => $invoice, ]; } @@ -133,12 +142,12 @@ class HostingMailboxService $mailbox->update(['status' => 'deleting']); DeactivateMailboxJob::dispatch($mailbox->id); - return $this->syncAddonInvoice($account->fresh(['mailboxes'])); + return $this->syncAddonInvoice($account->fresh()); } public function syncAddonInvoice(HostingAccount $account): ?HostingBillingInvoice { - $account->loadMissing(['product', 'mailboxes']); + $account->loadMissing(['product']); $summary = $this->usageSummary($account); if ($summary['extra_count'] === 0) { HostingBillingInvoice::query() diff --git a/app/Services/Hosting/UpsellRecommendationService.php b/app/Services/Hosting/UpsellRecommendationService.php index 82a146d..8595be0 100644 --- a/app/Services/Hosting/UpsellRecommendationService.php +++ b/app/Services/Hosting/UpsellRecommendationService.php @@ -20,11 +20,16 @@ class UpsellRecommendationService */ public function recommendationsForUser(User $user): Collection { - $accounts = HostingAccount::query() + $accountsQuery = HostingAccount::query() ->where('user_id', $user->id) ->where('status', HostingAccount::STATUS_ACTIVE) - ->with(['product', 'mailboxes:id,hosting_account_id,is_paid']) - ->get(); + ->with(['product']); + + if (HostingAccount::tracksLocalMailboxes()) { + $accountsQuery->with(['mailboxes:id,hosting_account_id,is_paid']); + } + + $accounts = $accountsQuery->get(); return $accounts ->flatMap(fn (HostingAccount $account): array => $this->recommendationsForAccount($account)) @@ -37,7 +42,7 @@ class UpsellRecommendationService */ public function recommendationsForAccount(HostingAccount $account): array { - $account->loadMissing(['product', 'mailboxes']); + $account->loadMissing(['product']); $recommendations = []; $upgradeProduct = $this->nextUpgradeProduct($account); @@ -75,8 +80,8 @@ class UpsellRecommendationService } $freeAllowance = $account->freeEmailAllowance(); - $mailboxCount = $account->mailboxes->count(); - if ($freeAllowance !== null && $mailboxCount > $freeAllowance) { + $mailboxCount = $account->loadedMailboxes()->count(); + if (HostingAccount::tracksLocalMailboxes() && $freeAllowance !== null && $mailboxCount > $freeAllowance) { $extraMailboxes = $mailboxCount - $freeAllowance; $addonAmount = self::EMAIL_ADDON_QUANTITY * self::EMAIL_ADDON_UNIT_PRICE;