Fix 500 when exiting hosting panel for existing accounts.
Deploy Ladill Hosting / deploy (push) Successful in 20s

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 <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-06-06 17:02:44 +00:00
co-authored by Cursor
parent 206a022e27
commit 1135915797
5 changed files with 62 additions and 21 deletions
@@ -260,10 +260,11 @@ class HostingProductController extends Controller
$upsells ??= app(UpsellRecommendationService::class); $upsells ??= app(UpsellRecommendationService::class);
$renewals ??= app(HostingRenewalCheckoutService::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; $maxDomains = $account->resource_limits['max_domains'] ?? $account->product?->max_domains ?? 1;
$freeEmailAllowance = $account->freeEmailAllowance(); $freeEmailAllowance = $account->freeEmailAllowance();
$mailboxCount = $account->mailboxes->count(); $mailboxes = $account->loadedMailboxes();
$mailboxCount = $mailboxes->count();
$extraMailboxCount = $freeEmailAllowance === null ? 0 : max($mailboxCount - $freeEmailAllowance, 0); $extraMailboxCount = $freeEmailAllowance === null ? 0 : max($mailboxCount - $freeEmailAllowance, 0);
return view('hosting.account', [ return view('hosting.account', [
@@ -276,7 +277,7 @@ class HostingProductController extends Controller
'mailbox_count' => $mailboxCount, 'mailbox_count' => $mailboxCount,
'free_allowance' => $freeEmailAllowance, 'free_allowance' => $freeEmailAllowance,
'extra_count' => $extraMailboxCount, '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), 'extra_total' => round($extraMailboxCount * 5, 2),
], ],
'upsellRecommendations' => $upsells->recommendationsForAccount($account), 'upsellRecommendations' => $upsells->recommendationsForAccount($account),
+1 -1
View File
@@ -52,7 +52,7 @@ class DeactivateMailboxJob implements ShouldQueue
$mailbox->delete(); $mailbox->delete();
if ($hostingAccountId) { if ($hostingAccountId) {
$account = HostingAccount::query()->with(['product', 'mailboxes'])->find($hostingAccountId); $account = HostingAccount::query()->with(['product'])->find($hostingAccountId);
if ($account) { if ($account) {
$hostingMailboxes->syncAddonInvoice($account); $hostingMailboxes->syncAddonInvoice($account);
} }
+29 -3
View File
@@ -3,6 +3,7 @@
namespace App\Models; namespace App\Models;
use Carbon\CarbonInterface; use Carbon\CarbonInterface;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Model; 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\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Schema;
class HostingAccount extends Model class HostingAccount extends Model
{ {
@@ -159,6 +161,28 @@ class HostingAccount extends Model
return $this->hasMany(Mailbox::class, 'hosting_account_id'); 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<int, Mailbox> */
public function loadedMailboxes(): Collection
{
if (! static::tracksLocalMailboxes()) {
return new Collection;
}
if (! $this->relationLoaded('mailboxes')) {
$this->load('mailboxes');
}
return $this->mailboxes;
}
public function billingInvoices(): HasMany public function billingInvoices(): HasMany
{ {
return $this->hasMany(HostingBillingInvoice::class, 'hosting_account_id'); return $this->hasMany(HostingBillingInvoice::class, 'hosting_account_id');
@@ -268,9 +292,11 @@ class HostingAccount extends Model
public function paidMailboxCount(): int public function paidMailboxCount(): int
{ {
return $this->relationLoaded('mailboxes') if (! static::tracksLocalMailboxes()) {
? $this->mailboxes->where('is_paid', true)->count() return 0;
: $this->mailboxes()->where('is_paid', true)->count(); }
return $this->loadedMailboxes()->where('is_paid', true)->count();
} }
public function renew(?int $durationMonths = null, array $metadata = []): void public function renew(?int $durationMonths = null, array $metadata = []): void
+17 -8
View File
@@ -26,11 +26,12 @@ class HostingMailboxService
*/ */
public function usageSummary(HostingAccount $account): array public function usageSummary(HostingAccount $account): array
{ {
$account->loadMissing(['product', 'mailboxes']); $account->loadMissing(['product']);
$mailboxes = $account->loadedMailboxes();
$freeAllowance = $account->freeEmailAllowance(); $freeAllowance = $account->freeEmailAllowance();
$mailboxCount = $account->mailboxes->count(); $mailboxCount = $mailboxes->count();
$paidCount = $account->mailboxes->where('is_paid', true)->count(); $paidCount = $mailboxes->where('is_paid', true)->count();
$freeUsed = $freeAllowance === null ? $mailboxCount : min($mailboxCount, $freeAllowance); $freeUsed = $freeAllowance === null ? $mailboxCount : min($mailboxCount, $freeAllowance);
$extraCount = max($mailboxCount - ($freeAllowance ?? $mailboxCount), 0); $extraCount = max($mailboxCount - ($freeAllowance ?? $mailboxCount), 0);
@@ -53,7 +54,15 @@ class HostingMailboxService
public function createMailbox(HostingAccount $account, array $input): array 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() $domain = Domain::query()
->whereKey((int) ($input['domain_id'] ?? 0)) ->whereKey((int) ($input['domain_id'] ?? 0))
@@ -113,11 +122,11 @@ class HostingMailboxService
ProvisionMailboxJob::dispatch($mailbox->id, encrypt((string) $input['password'])); ProvisionMailboxJob::dispatch($mailbox->id, encrypt((string) $input['password']));
$invoice = $this->syncAddonInvoice($account->fresh(['mailboxes'])); $invoice = $this->syncAddonInvoice($account->fresh());
return [ return [
'mailbox' => $mailbox->fresh(['domain']), 'mailbox' => $mailbox->fresh(['domain']),
'usage' => $this->usageSummary($account->fresh(['product', 'mailboxes'])), 'usage' => $this->usageSummary($account->fresh(['product'])),
'invoice' => $invoice, 'invoice' => $invoice,
]; ];
} }
@@ -133,12 +142,12 @@ class HostingMailboxService
$mailbox->update(['status' => 'deleting']); $mailbox->update(['status' => 'deleting']);
DeactivateMailboxJob::dispatch($mailbox->id); DeactivateMailboxJob::dispatch($mailbox->id);
return $this->syncAddonInvoice($account->fresh(['mailboxes'])); return $this->syncAddonInvoice($account->fresh());
} }
public function syncAddonInvoice(HostingAccount $account): ?HostingBillingInvoice public function syncAddonInvoice(HostingAccount $account): ?HostingBillingInvoice
{ {
$account->loadMissing(['product', 'mailboxes']); $account->loadMissing(['product']);
$summary = $this->usageSummary($account); $summary = $this->usageSummary($account);
if ($summary['extra_count'] === 0) { if ($summary['extra_count'] === 0) {
HostingBillingInvoice::query() HostingBillingInvoice::query()
@@ -20,11 +20,16 @@ class UpsellRecommendationService
*/ */
public function recommendationsForUser(User $user): Collection public function recommendationsForUser(User $user): Collection
{ {
$accounts = HostingAccount::query() $accountsQuery = HostingAccount::query()
->where('user_id', $user->id) ->where('user_id', $user->id)
->where('status', HostingAccount::STATUS_ACTIVE) ->where('status', HostingAccount::STATUS_ACTIVE)
->with(['product', 'mailboxes:id,hosting_account_id,is_paid']) ->with(['product']);
->get();
if (HostingAccount::tracksLocalMailboxes()) {
$accountsQuery->with(['mailboxes:id,hosting_account_id,is_paid']);
}
$accounts = $accountsQuery->get();
return $accounts return $accounts
->flatMap(fn (HostingAccount $account): array => $this->recommendationsForAccount($account)) ->flatMap(fn (HostingAccount $account): array => $this->recommendationsForAccount($account))
@@ -37,7 +42,7 @@ class UpsellRecommendationService
*/ */
public function recommendationsForAccount(HostingAccount $account): array public function recommendationsForAccount(HostingAccount $account): array
{ {
$account->loadMissing(['product', 'mailboxes']); $account->loadMissing(['product']);
$recommendations = []; $recommendations = [];
$upgradeProduct = $this->nextUpgradeProduct($account); $upgradeProduct = $this->nextUpgradeProduct($account);
@@ -75,8 +80,8 @@ class UpsellRecommendationService
} }
$freeAllowance = $account->freeEmailAllowance(); $freeAllowance = $account->freeEmailAllowance();
$mailboxCount = $account->mailboxes->count(); $mailboxCount = $account->loadedMailboxes()->count();
if ($freeAllowance !== null && $mailboxCount > $freeAllowance) { if (HostingAccount::tracksLocalMailboxes() && $freeAllowance !== null && $mailboxCount > $freeAllowance) {
$extraMailboxes = $mailboxCount - $freeAllowance; $extraMailboxes = $mailboxCount - $freeAllowance;
$addonAmount = self::EMAIL_ADDON_QUANTITY * self::EMAIL_ADDON_UNIT_PRICE; $addonAmount = self::EMAIL_ADDON_QUANTITY * self::EMAIL_ADDON_UNIT_PRICE;