*/ 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'], ], ] ); } }