public_id; } /** Storage tiers + the default (free) plan. Pricing is purely per-tier. */ private function quota(): array { return [ 'tiers' => MailboxPricing::tiers(), 'default_mb' => MailboxPricing::defaultQuotaMb(), 'currency' => config('email.currency', 'GHS'), ]; } public function index(Request $request): View { $mailboxes = []; $error = null; try { $mailboxes = $this->mailboxes->forUser($this->pid()); } catch (\Throwable $e) { report($e); $error = 'Could not load your mailboxes.'; } $q = trim((string) $request->query('q', '')); if ($q !== '') { $needle = mb_strtolower($q); $mailboxes = array_values(array_filter($mailboxes, fn ($m) => str_contains( mb_strtolower(($m['address'] ?? '').' '.($m['display_name'] ?? '')), $needle ))); } return view('email.mailboxes.index', ['mailboxes' => $mailboxes, 'error' => $error, 'q' => $q]); } public function create(): View { $domains = []; try { $domains = $this->domains->verified($this->pid()); } catch (\Throwable $e) { report($e); } return view('email.mailboxes.create', [ 'domains' => $domains, 'quota' => $this->quota(), 'walletBalanceMinor' => $this->wallet->balanceMinor(ladill_account()), 'topupUrl' => 'https://'.config('app.account_domain').'/wallet', ]); } public function store(Request $request): RedirectResponse { $data = $request->validate([ 'email_domain_id' => ['required', 'integer'], 'local_part' => ['required', 'string', 'max:64', 'regex:/^[a-z0-9._%+-]+$/i'], 'display_name' => ['required', 'string', 'max:160'], 'password' => ['required', 'string', 'min:8', 'max:190', 'confirmed'], 'quota_mb' => ['required', 'integer', function ($attr, $value, $fail) { if (! MailboxPricing::isValidQuota((int) $value)) { $fail('Choose a valid storage plan.'); } }], ]); $user = ladill_account(); $quotaMb = (int) $data['quota_mb']; $price = MailboxPricing::priceMinorFor($quotaMb); // 1 GB = 0 (free forever) $isPaid = $price > 0; $reference = 'mbx_'.Str::lower(Str::random(20)); // Paid tiers are wallet-funded (debit first); insufficient → top up. if ($isPaid) { if (! $this->wallet->canAfford($user, $price)) { return back()->withInput() ->with('error', 'Your wallet balance is too low for this storage plan. Please top up and try again.') ->with('topup', true); } if (! $this->wallet->charge($user, $price, $reference, 'mailbox_create', 'Mailbox '.strtolower($data['local_part']))) { return back()->withInput()->with('error', 'Could not charge your wallet. Please try again.')->with('topup', true); } } try { $mailbox = $this->mailboxes->create( $this->pid(), (int) $data['email_domain_id'], strtolower(trim($data['local_part'])), $data['display_name'], $data['password'], $quotaMb, $isPaid, $isPaid ? $reference : null, ); return redirect()->route('email.mailboxes.show', $mailbox['id']) ->with('success', 'Mailbox '.$mailbox['address'].' is being set up.'); } catch (\Throwable $e) { // Provisioning failed after a charge — refund so the customer isn't out of pocket. if ($isPaid) { $this->wallet->refund($user, $price, $reference, 'Mailbox creation failed'); } $msg = $e instanceof \Illuminate\Http\Client\RequestException ? (string) ($e->response?->json('message') ?? 'Could not create the mailbox.') : 'Could not create the mailbox.'; report($e); return back()->withInput()->with('error', $msg); } } public function show(int $id): View|RedirectResponse { try { $mailbox = $this->mailboxes->show($this->pid(), $id); } catch (\Throwable $e) { return redirect()->route('email.mailboxes.index')->with('error', 'Mailbox not found.'); } return view('email.mailboxes.show', ['mailbox' => $mailbox]); } /** Upgrade form — pick a larger storage plan for an existing mailbox. */ public function upgrade(int $id): View|RedirectResponse { try { $mailbox = $this->mailboxes->show($this->pid(), $id); } catch (\Throwable $e) { return redirect()->route('email.mailboxes.index')->with('error', 'Mailbox not found.'); } $currentMb = (int) ($mailbox['quota_mb'] ?? 0); // Only offer plans larger than the current one. $tiers = array_values(array_filter(MailboxPricing::tiers(), fn ($t) => $t['mb'] > $currentMb)); if (empty($tiers)) { return redirect()->route('email.mailboxes.show', $id)->with('error', 'This mailbox is already on the largest plan.'); } return view('email.mailboxes.upgrade', [ 'mailbox' => $mailbox, 'currentMb' => $currentMb, 'tiers' => $tiers, 'defaultMb' => $tiers[0]['mb'], 'currency' => config('email.currency', 'GHS'), 'walletBalanceMinor' => $this->wallet->balanceMinor(ladill_account()), 'topupUrl' => 'https://'.config('app.account_domain').'/wallet', ]); } /** Apply the upgrade — charge the new tier, then change the quota. */ public function upgradeStore(Request $request, int $id): RedirectResponse { $data = $request->validate([ 'quota_mb' => ['required', 'integer', function ($attr, $value, $fail) { if (! MailboxPricing::isValidQuota((int) $value)) { $fail('Choose a valid storage plan.'); } }], ]); $user = ladill_account(); $newMb = (int) $data['quota_mb']; try { $mailbox = $this->mailboxes->show($this->pid(), $id); } catch (\Throwable $e) { return redirect()->route('email.mailboxes.index')->with('error', 'Mailbox not found.'); } if ($newMb <= (int) ($mailbox['quota_mb'] ?? 0)) { return back()->withInput()->with('error', 'Choose a larger plan than your current one.'); } $price = MailboxPricing::priceMinorFor($newMb); $isPaid = $price > 0; $reference = 'mbxup_'.Str::lower(Str::random(20)); if ($isPaid) { if (! $this->wallet->canAfford($user, $price)) { return back()->withInput() ->with('error', 'Your wallet balance is too low for this plan. Please top up and try again.') ->with('topup', true); } if (! $this->wallet->charge($user, $price, $reference, 'mailbox_upgrade', 'Upgrade '.($mailbox['address'] ?? 'mailbox').' to '.MailboxPricing::label($newMb))) { return back()->withInput()->with('error', 'Could not charge your wallet. Please try again.')->with('topup', true); } } try { $this->mailboxes->updateQuota($this->pid(), $id, $newMb, $isPaid, $isPaid ? $reference : null); } catch (\Throwable $e) { if ($isPaid) { $this->wallet->refund($user, $price, $reference, 'Mailbox upgrade failed'); } report($e); return back()->withInput()->with('error', 'Could not upgrade the mailbox.'); } return redirect()->route('email.mailboxes.show', $id) ->with('success', 'Upgraded to '.MailboxPricing::label($newMb).'.'); } public function resetPassword(Request $request, int $id): RedirectResponse { $data = $request->validate(['password' => ['required', 'string', 'min:8', 'max:190', 'confirmed']]); try { $this->mailboxes->resetPassword($this->pid(), $id, $data['password']); } catch (\Throwable $e) { return back()->with('error', 'Could not update the password.'); } return redirect()->route('email.mailboxes.show', $id)->with('success', 'Password updated.'); } public function destroy(int $id): RedirectResponse { try { $this->mailboxes->delete($this->pid(), $id); } catch (\Throwable $e) { return back()->with('error', 'Could not delete the mailbox.'); } return redirect()->route('email.mailboxes.index')->with('success', 'Mailbox deleted.'); } }