Show linked domains on cards and fix Email leftovers.
Deploy Ladill Hosting / deploy (push) Successful in 24s
Deploy Ladill Hosting / deploy (push) Successful in 24s
Display hosted_sites on account cards, scope Afia and Settings to hosting, add hosting_settings for notifications, and remove mailbox UI from the hosting layout and account pages. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,29 +3,22 @@
|
||||
namespace App\Http\Controllers\Hosting;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\EmailSetting;
|
||||
use App\Models\User;
|
||||
use App\Models\HostingSetting;
|
||||
use App\Services\Billing\BillingClient;
|
||||
use App\Services\Identity\IdentityClient;
|
||||
use App\Services\Mailbox\MailboxClient;
|
||||
use App\Services\Payment\WalletPaymentService;
|
||||
use App\Support\MailboxPricing;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* Account — Wallet, Billing, Settings. Money is read from the one platform
|
||||
* UserWallet via the Billing API (tagged 'email'); funding is platform-level, so
|
||||
* "Add funds" deep-links to the account portal. Settings are email-local prefs.
|
||||
* Account — Wallet, Billing, Settings for Ladill Hosting.
|
||||
* Money is read from the one platform UserWallet via the Billing API.
|
||||
*/
|
||||
class AccountController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private WalletPaymentService $wallet,
|
||||
private BillingClient $billing,
|
||||
private MailboxClient $mailboxes,
|
||||
private IdentityClient $identity,
|
||||
) {}
|
||||
|
||||
private function topupUrl(): string
|
||||
@@ -33,7 +26,6 @@ class AccountController extends Controller
|
||||
return 'https://'.config('app.account_domain').'/wallet';
|
||||
}
|
||||
|
||||
/** Wallet: balance + email spend summary + add-funds link. */
|
||||
public function wallet(): View
|
||||
{
|
||||
$account = ladill_account();
|
||||
@@ -47,34 +39,15 @@ class AccountController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
/** Billing: wallet snapshot + recurring paid-mailbox subscriptions. */
|
||||
public function billing(): View
|
||||
{
|
||||
$account = ladill_account();
|
||||
[$balanceMinor, $ledger] = $this->billingSnapshot($account?->public_id);
|
||||
|
||||
$paidMailboxes = [];
|
||||
$monthlyTotalMinor = 0;
|
||||
try {
|
||||
foreach ($this->mailboxes->forUser((string) $account?->public_id) as $m) {
|
||||
if (! ($m['is_paid'] ?? false)) {
|
||||
continue;
|
||||
}
|
||||
$m['price_minor'] = MailboxPricing::priceMinorFor((int) ($m['quota_mb'] ?? 0));
|
||||
$m['quota_label'] = MailboxPricing::label((int) ($m['quota_mb'] ?? 0));
|
||||
$monthlyTotalMinor += $m['price_minor'];
|
||||
$paidMailboxes[] = $m;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
|
||||
return view('hosting.account.billing', [
|
||||
'balanceMinor' => $balanceMinor,
|
||||
'spentMinor' => (int) ($ledger['spent_minor'] ?? 0),
|
||||
'creditedMinor' => (int) ($ledger['credited_minor'] ?? 0),
|
||||
'paidMailboxes' => $paidMailboxes,
|
||||
'monthlyTotalMinor' => $monthlyTotalMinor,
|
||||
'topupUrl' => $this->topupUrl(),
|
||||
]);
|
||||
}
|
||||
@@ -82,169 +55,26 @@ class AccountController extends Controller
|
||||
public function settings(): View
|
||||
{
|
||||
$account = ladill_account();
|
||||
$settings = EmailSetting::firstOrNew(['user_id' => $account?->id]);
|
||||
['linkStatus' => $linkStatus, 'mailboxOptions' => $mailboxOptions, 'showMailboxLinkUi' => $showMailboxLinkUi] = $this->mailboxLinkContext($account);
|
||||
$settings = HostingSetting::firstOrNew(['user_id' => $account?->id]);
|
||||
|
||||
return view('hosting.account.settings', [
|
||||
'account' => $account,
|
||||
'settings' => $settings,
|
||||
'linkStatus' => $linkStatus,
|
||||
'mailboxOptions' => $mailboxOptions,
|
||||
'showMailboxLinkUi' => $showMailboxLinkUi,
|
||||
]);
|
||||
}
|
||||
|
||||
public function linkMailbox(Request $request): RedirectResponse
|
||||
{
|
||||
$account = ladill_account();
|
||||
|
||||
$data = $request->validate([
|
||||
'mailbox_address' => ['required', 'email', 'max:255'],
|
||||
]);
|
||||
|
||||
try {
|
||||
$result = $this->identity->linkMailbox(
|
||||
(string) $account->public_id,
|
||||
$data['mailbox_address'],
|
||||
);
|
||||
} catch (\Illuminate\Http\Client\RequestException $e) {
|
||||
$message = (string) $e->response?->json('message', '');
|
||||
if ($message !== '') {
|
||||
return redirect()->route('account.settings')->with('error', $message);
|
||||
}
|
||||
|
||||
report($e);
|
||||
|
||||
return redirect()->route('account.settings')->with('error', 'Could not link mailbox. Try again in a moment, or contact support if this keeps happening.');
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
|
||||
return redirect()->route('account.settings')->with('error', 'Could not link mailbox. Try again in a moment, or contact support if this keeps happening.');
|
||||
}
|
||||
|
||||
$request->session()->forget('mailbox_link_banner_dismissed');
|
||||
|
||||
$connectUrl = (string) ($result['webmail_connect_url'] ?? '');
|
||||
if ($connectUrl === '') {
|
||||
$connectUrl = rtrim((string) config('services.ladill_webmail.url', 'https://mail.ladill.com'), '/').'/sso/connect';
|
||||
}
|
||||
|
||||
return redirect()->away($connectUrl);
|
||||
}
|
||||
|
||||
public function unlinkMailbox(Request $request): RedirectResponse
|
||||
{
|
||||
$account = ladill_account();
|
||||
|
||||
try {
|
||||
$this->identity->unlinkMailbox((string) $account->public_id);
|
||||
} catch (\Illuminate\Http\Client\RequestException $e) {
|
||||
$message = (string) $e->response?->json('message', '');
|
||||
if ($message !== '') {
|
||||
return redirect()->route('account.settings')->with('error', $message);
|
||||
}
|
||||
|
||||
report($e);
|
||||
|
||||
return redirect()->route('account.settings')->with('error', 'Could not unlink mailbox. Try again in a moment.');
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
|
||||
return redirect()->route('account.settings')->with('error', 'Could not unlink mailbox. Try again in a moment.');
|
||||
}
|
||||
|
||||
$request->session()->forget('mailbox_link_banner_dismissed');
|
||||
|
||||
return redirect()->route('account.settings')->with('success', 'Mailbox unlinked from your Ladill account.');
|
||||
}
|
||||
|
||||
/** @return array{linkStatus: array<string, mixed>, mailboxOptions: array<int, array<string, mixed>>, showMailboxLinkUi: bool} */
|
||||
private function mailboxLinkContext(?User $account): array
|
||||
{
|
||||
$linkStatus = ['show_reminder' => false];
|
||||
$mailboxOptions = [];
|
||||
|
||||
if (! $account?->public_id) {
|
||||
return [
|
||||
'linkStatus' => $linkStatus,
|
||||
'mailboxOptions' => $mailboxOptions,
|
||||
'showMailboxLinkUi' => false,
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
$mailboxOptions = $this->mailboxes->forUser((string) $account->public_id);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
|
||||
try {
|
||||
$linkStatus = $this->identity->mailboxLinkStatus((string) $account->public_id);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
$linkStatus = $this->localMailboxLinkStatus($account, $mailboxOptions);
|
||||
}
|
||||
|
||||
$showMailboxLinkUi = (bool) ($linkStatus['linked_mailbox'] ?? null)
|
||||
|| ($linkStatus['show_reminder'] ?? false)
|
||||
|| $this->localNeedsMailboxLink($account, $mailboxOptions, $linkStatus);
|
||||
|
||||
return [
|
||||
'linkStatus' => $linkStatus,
|
||||
'mailboxOptions' => $mailboxOptions,
|
||||
'showMailboxLinkUi' => $showMailboxLinkUi,
|
||||
];
|
||||
}
|
||||
|
||||
/** @param array<int, array<string, mixed>> $mailboxOptions */
|
||||
private function localNeedsMailboxLink(User $account, array $mailboxOptions, array $linkStatus): bool
|
||||
{
|
||||
if ($linkStatus['linked_mailbox'] ?? null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$email = strtolower(trim($account->email ?? ''));
|
||||
if ($email === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($mailboxOptions as $mailbox) {
|
||||
if (strtolower((string) ($mailbox['address'] ?? '')) === $email) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return count($mailboxOptions) > 0;
|
||||
}
|
||||
|
||||
/** @param array<int, array<string, mixed>> $mailboxOptions */
|
||||
/** @return array<string, mixed> */
|
||||
private function localMailboxLinkStatus(User $account, array $mailboxOptions): array
|
||||
{
|
||||
$needsLink = $this->localNeedsMailboxLink($account, $mailboxOptions, []);
|
||||
|
||||
return [
|
||||
'show_reminder' => $needsLink,
|
||||
'stage' => count($mailboxOptions) > 0 ? 'needs_link' : 'needs_mailbox',
|
||||
'linked_mailbox' => null,
|
||||
'account_email' => $account->email,
|
||||
];
|
||||
}
|
||||
|
||||
public function updateSettings(Request $request): RedirectResponse
|
||||
{
|
||||
$account = ladill_account();
|
||||
|
||||
$data = $request->validate([
|
||||
'default_quota_mb' => ['nullable', 'integer', 'min:256', 'max:153600'],
|
||||
'notify_email' => ['nullable', 'email', 'max:255'],
|
||||
'product_updates' => ['nullable', 'boolean'],
|
||||
]);
|
||||
|
||||
EmailSetting::updateOrCreate(
|
||||
HostingSetting::updateOrCreate(
|
||||
['user_id' => $account->id],
|
||||
[
|
||||
'default_quota_mb' => $data['default_quota_mb'] ?? null,
|
||||
'notify_email' => $data['notify_email'] ?? null,
|
||||
'product_updates' => $request->boolean('product_updates'),
|
||||
],
|
||||
@@ -265,7 +95,7 @@ class AccountController extends Controller
|
||||
try {
|
||||
return [
|
||||
$this->wallet->balanceMinor(ladill_account()),
|
||||
$this->billing->serviceLedger($publicId, (string) config('billing.service', 'email')),
|
||||
$this->billing->serviceLedger($publicId, (string) config('billing.service', 'hosting')),
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
|
||||
@@ -3,17 +3,17 @@
|
||||
namespace App\Http\Controllers\Hosting;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\CustomerHostingOrder;
|
||||
use App\Models\HostedSite;
|
||||
use App\Models\HostingAccount;
|
||||
use App\Models\HostingAccountMember;
|
||||
use App\Models\HostingProduct;
|
||||
use App\Services\Afia\AfiaService;
|
||||
use App\Services\Billing\BillingClient;
|
||||
use App\Services\EmailDomain\EmailDomainClient;
|
||||
use App\Services\Mailbox\MailboxClient;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* Ladill Email — Afia chat endpoint. Grounds the assistant in the user's live
|
||||
* mailboxes/domains so answers are specific.
|
||||
*/
|
||||
/** Afia chat for Ladill Hosting — grounded in the user's live hosting accounts. */
|
||||
class AfiaController extends Controller
|
||||
{
|
||||
public function chat(Request $request, AfiaService $afia): JsonResponse
|
||||
@@ -40,35 +40,60 @@ class AfiaController extends Controller
|
||||
return response()->json(['reply' => $reply]);
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> Live email context for grounding. */
|
||||
/** @return array<string,mixed> */
|
||||
private function context(): array
|
||||
{
|
||||
$account = ladill_account();
|
||||
$user = auth()->user();
|
||||
|
||||
if (! $account) {
|
||||
if (! $user) {
|
||||
return ['signed_in' => 'no'];
|
||||
}
|
||||
|
||||
$pid = (string) $account->public_id;
|
||||
$ctx = ['signed_in' => 'yes'];
|
||||
$sharedTypes = [
|
||||
HostingProduct::TYPE_SINGLE_DOMAIN,
|
||||
HostingProduct::TYPE_MULTI_DOMAIN,
|
||||
HostingProduct::TYPE_WORDPRESS,
|
||||
];
|
||||
|
||||
try {
|
||||
$mailboxes = app(MailboxClient::class)->forUser($pid);
|
||||
$ctx['mailboxes_total'] = count($mailboxes);
|
||||
$ctx['mailboxes_paid'] = count(array_filter($mailboxes, fn ($m) => (bool) ($m['is_paid'] ?? false)));
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
$sharedAccountIds = HostingAccountMember::query()
|
||||
->where('user_id', $user->id)
|
||||
->pluck('hosting_account_id');
|
||||
|
||||
try {
|
||||
$domains = app(EmailDomainClient::class)->forUser($pid);
|
||||
$ctx['domains_total'] = count($domains);
|
||||
$ctx['domains_verified'] = count(array_filter($domains, fn ($d) => (bool) ($d['active'] ?? $d['verified'] ?? false)));
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
$accounts = HostingAccount::query()
|
||||
->where(function ($query) use ($user, $sharedAccountIds) {
|
||||
$query->where('user_id', $user->id);
|
||||
if ($sharedAccountIds->isNotEmpty()) {
|
||||
$query->orWhereIn('id', $sharedAccountIds);
|
||||
}
|
||||
})
|
||||
->whereHas('product', fn ($q) => $q->whereIn('type', $sharedTypes))
|
||||
->with('sites')
|
||||
->get();
|
||||
|
||||
try {
|
||||
$ctx['wallet_balance_ghs'] = number_format(app(BillingClient::class)->balanceMinor($pid) / 100, 2);
|
||||
} catch (\Throwable) {
|
||||
$ctx = [
|
||||
'signed_in' => 'yes',
|
||||
'hosting_accounts_total' => $accounts->count(),
|
||||
'hosting_accounts_active' => $accounts->where('status', HostingAccount::STATUS_ACTIVE)->count(),
|
||||
'linked_domains' => HostedSite::query()
|
||||
->whereIn('hosting_account_id', $accounts->pluck('id'))
|
||||
->count(),
|
||||
'pending_orders' => CustomerHostingOrder::query()
|
||||
->forUser($user->id)
|
||||
->whereIn('status', [
|
||||
CustomerHostingOrder::STATUS_PENDING_PAYMENT,
|
||||
CustomerHostingOrder::STATUS_PENDING_APPROVAL,
|
||||
CustomerHostingOrder::STATUS_APPROVED,
|
||||
CustomerHostingOrder::STATUS_PROVISIONING,
|
||||
])
|
||||
->count(),
|
||||
];
|
||||
|
||||
$account = ladill_account();
|
||||
if ($account?->public_id) {
|
||||
try {
|
||||
$ctx['wallet_balance_ghs'] = number_format(app(BillingClient::class)->balanceMinor((string) $account->public_id) / 100, 2);
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
}
|
||||
|
||||
return $ctx;
|
||||
|
||||
@@ -76,7 +76,7 @@ class HostingProductController extends Controller
|
||||
}
|
||||
})
|
||||
->whereHas('product', fn ($q) => $q->ofType($type))
|
||||
->with('product')
|
||||
->with(['product', 'sites'])
|
||||
->latest()
|
||||
->get();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user