Files
isaaccladandCursor 1135915797
Deploy Ladill Hosting / deploy (push) Successful in 20s
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 <cursoragent@cursor.com>
2026-06-06 17:02:44 +00:00

191 lines
7.2 KiB
PHP

<?php
namespace App\Services\Hosting;
use App\Jobs\DeactivateMailboxJob;
use App\Jobs\ProvisionMailboxJob;
use App\Services\Mail\MailboxUserProvisioner;
use App\Models\Domain;
use App\Models\HostingAccount;
use App\Models\HostingBillingInvoice;
use App\Models\Mailbox;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
class HostingMailboxService
{
public const EXTRA_MAILBOX_PRICE_CEDIS = 5;
public function __construct(
protected MailboxUserProvisioner $mailboxUsers,
) {}
/**
* @return array<string, mixed>
*/
public function usageSummary(HostingAccount $account): array
{
$account->loadMissing(['product']);
$mailboxes = $account->loadedMailboxes();
$freeAllowance = $account->freeEmailAllowance();
$mailboxCount = $mailboxes->count();
$paidCount = $mailboxes->where('is_paid', true)->count();
$freeUsed = $freeAllowance === null ? $mailboxCount : min($mailboxCount, $freeAllowance);
$extraCount = max($mailboxCount - ($freeAllowance ?? $mailboxCount), 0);
return [
'mailbox_count' => $mailboxCount,
'free_allowance' => $freeAllowance,
'free_used' => $freeUsed,
'paid_count' => $paidCount,
'extra_count' => $extraCount,
'extra_price' => self::EXTRA_MAILBOX_PRICE_CEDIS,
'extra_total' => round($extraCount * self::EXTRA_MAILBOX_PRICE_CEDIS, 2),
'breakdown' => sprintf(
'Extra Emails: %d x GHS %d = GHS %.2f',
$extraCount,
self::EXTRA_MAILBOX_PRICE_CEDIS,
round($extraCount * self::EXTRA_MAILBOX_PRICE_CEDIS, 2)
),
];
}
public function createMailbox(HostingAccount $account, array $input): array
{
$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))
->where('hosting_account_id', $account->id)
->first();
if (! $domain) {
throw ValidationException::withMessages([
'domain_id' => 'Select a domain linked to this hosting account.',
]);
}
$localPart = strtolower(trim((string) ($input['local_part'] ?? '')));
$address = "{$localPart}@{$domain->host}";
if (Mailbox::query()->where('address', $address)->exists()) {
throw ValidationException::withMessages([
'local_part' => 'This mailbox address already exists.',
]);
}
$summary = $this->usageSummary($account);
$freeAllowance = $summary['free_allowance'];
$isPaid = $freeAllowance !== null && $summary['mailbox_count'] >= $freeAllowance;
$mailbox = Mailbox::query()->create([
'user_id' => $account->user_id,
'website_id' => null,
'hosting_account_id' => $account->id,
'domain_id' => $domain->id,
'display_name' => (string) ($input['display_name'] ?? $localPart),
'local_part' => $localPart,
'address' => $address,
'password_hash' => Hash::make((string) $input['password']),
'quota_mb' => (int) ($input['quota_mb'] ?? Mailbox::defaultQuotaMb()),
'status' => 'provisioning',
'is_paid' => $isPaid,
'paid_at' => $isPaid ? now() : null,
'billing_type' => $isPaid ? 'subscription' : 'free',
'monthly_price' => $isPaid ? self::EXTRA_MAILBOX_PRICE_CEDIS : null,
'subscription_started_at' => $isPaid ? now() : null,
'subscription_expires_at' => $isPaid ? now()->addMonth() : null,
'subscription_status' => 'active',
'maildir_path' => sprintf('hosting/%d/%s/%s', $account->id, $domain->host, $localPart),
'credentials_issued_at' => now(),
'incoming_token' => Str::random(64),
'inbound_enabled' => true,
'outbound_enabled' => true,
'outbound_provider' => 'smtp',
'metadata' => [
'provisioned_by' => 'hosting_account',
'hosting_account_id' => $account->id,
],
]);
$this->mailboxUsers->provisionMailboxSilently($mailbox, (string) $input['password']);
ProvisionMailboxJob::dispatch($mailbox->id, encrypt((string) $input['password']));
$invoice = $this->syncAddonInvoice($account->fresh());
return [
'mailbox' => $mailbox->fresh(['domain']),
'usage' => $this->usageSummary($account->fresh(['product'])),
'invoice' => $invoice,
];
}
public function deleteMailbox(HostingAccount $account, Mailbox $mailbox): ?HostingBillingInvoice
{
if ((int) $mailbox->hosting_account_id !== (int) $account->id) {
throw ValidationException::withMessages([
'mailbox_id' => 'Mailbox does not belong to this hosting account.',
]);
}
$mailbox->update(['status' => 'deleting']);
DeactivateMailboxJob::dispatch($mailbox->id);
return $this->syncAddonInvoice($account->fresh());
}
public function syncAddonInvoice(HostingAccount $account): ?HostingBillingInvoice
{
$account->loadMissing(['product']);
$summary = $this->usageSummary($account);
if ($summary['extra_count'] === 0) {
HostingBillingInvoice::query()
->where('hosting_account_id', $account->id)
->where('invoice_type', HostingBillingInvoice::TYPE_EMAIL_ADDON)
->where('status', HostingBillingInvoice::STATUS_PENDING)
->delete();
return null;
}
return HostingBillingInvoice::query()->updateOrCreate(
[
'hosting_account_id' => $account->id,
'invoice_type' => HostingBillingInvoice::TYPE_EMAIL_ADDON,
'status' => HostingBillingInvoice::STATUS_PENDING,
],
[
'user_id' => $account->user_id,
'currency' => 'GHS',
'subtotal_amount' => $summary['extra_total'],
'credit_amount' => 0,
'total_amount' => $summary['extra_total'],
'description' => sprintf('Extra mailbox charges for %s', $account->primary_domain ?: $account->username),
'line_items' => [[
'code' => 'extra_emails',
'label' => 'Extra Emails',
'quantity' => $summary['extra_count'],
'unit_amount' => self::EXTRA_MAILBOX_PRICE_CEDIS,
'amount' => $summary['extra_total'],
'description' => $summary['breakdown'],
]],
'metadata' => [
'billing_period' => now()->format('Y-m'),
'extra_mailboxes' => $summary['extra_count'],
],
]
);
}
}