From c627d83e594a4185cbaae12056536520a3e615a0 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Sat, 6 Jun 2026 16:41:27 +0000 Subject: [PATCH] Fix hosting 500s: rc_service_orders migration and account layout. Account pages were extending the email layout with undefined routes, and hosting type pages queried a missing rc_service_orders table. Co-authored-by: Cursor --- .../Hosting/HostingProductController.php | 5 +- ..._220000_create_rc_service_orders_table.php | 36 +++++++++ ...kout_fields_to_rc_service_orders_table.php | 74 +++++++++++++++++++ .../views/hosting/account/billing.blade.php | 18 ++--- .../hosting/account/developers.blade.php | 15 ++-- .../views/hosting/account/settings.blade.php | 38 ++++------ .../views/hosting/account/team.blade.php | 10 +-- .../views/hosting/account/wallet.blade.php | 12 ++- routes/web.php | 2 + 9 files changed, 150 insertions(+), 60 deletions(-) create mode 100644 database/migrations/2026_03_18_220000_create_rc_service_orders_table.php create mode 100644 database/migrations/2026_03_19_120000_add_checkout_fields_to_rc_service_orders_table.php diff --git a/app/Http/Controllers/Hosting/HostingProductController.php b/app/Http/Controllers/Hosting/HostingProductController.php index 1e8561d..606b3cc 100644 --- a/app/Http/Controllers/Hosting/HostingProductController.php +++ b/app/Http/Controllers/Hosting/HostingProductController.php @@ -19,6 +19,7 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Gate; +use Illuminate\Support\Facades\Schema; use Illuminate\Validation\ValidationException; use Illuminate\View\View; @@ -157,7 +158,7 @@ class HostingProductController extends Controller ->get(); } - if ($rcCategory) { + if ($rcCategory && Schema::hasTable('rc_service_orders')) { $rcServiceOrders = RcServiceOrder::query() ->where('user_id', $user->id) ->where('category', $rcCategory) @@ -202,7 +203,7 @@ class HostingProductController extends Controller $rcCategory = $this->getRcCategoryForType($type); $rcServiceOrders = collect(); - if ($rcCategory) { + if ($rcCategory && Schema::hasTable('rc_service_orders')) { $rcServiceOrders = RcServiceOrder::query() ->where('user_id', $user->id) ->where('category', $rcCategory) diff --git a/database/migrations/2026_03_18_220000_create_rc_service_orders_table.php b/database/migrations/2026_03_18_220000_create_rc_service_orders_table.php new file mode 100644 index 0000000..26f7af2 --- /dev/null +++ b/database/migrations/2026_03_18_220000_create_rc_service_orders_table.php @@ -0,0 +1,36 @@ +id(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->string('category'); + $table->string('domain_name'); + $table->string('rc_order_id')->nullable()->index(); + $table->string('rc_customer_id')->nullable(); + $table->string('plan_id')->nullable(); + $table->string('plan_name')->nullable(); + $table->string('status')->default('pending'); + $table->string('server_location')->nullable(); + $table->string('ip_address')->nullable(); + $table->string('access_url')->nullable(); + $table->string('access_username')->nullable(); + $table->timestamp('expires_at')->nullable(); + $table->timestamp('provisioned_at')->nullable(); + $table->json('meta')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('rc_service_orders'); + } +}; diff --git a/database/migrations/2026_03_19_120000_add_checkout_fields_to_rc_service_orders_table.php b/database/migrations/2026_03_19_120000_add_checkout_fields_to_rc_service_orders_table.php new file mode 100644 index 0000000..b486692 --- /dev/null +++ b/database/migrations/2026_03_19_120000_add_checkout_fields_to_rc_service_orders_table.php @@ -0,0 +1,74 @@ +string('order_type')->default('service')->after('category')->index(); + $table->string('payment_status')->default('unpaid')->after('status')->index(); + $table->string('fulfillment_status')->default('cart')->after('payment_status')->index(); + $table->string('payment_reference')->nullable()->after('fulfillment_status')->index(); + $table->unsignedInteger('amount_minor')->nullable()->after('payment_reference'); + $table->string('currency', 3)->default('GHS')->after('amount_minor'); + $table->unsignedSmallInteger('term_months')->nullable()->after('currency'); + $table->unsignedSmallInteger('term_years')->nullable()->after('term_months'); + $table->timestamp('paid_at')->nullable()->after('term_years'); + $table->timestamp('submitted_at')->nullable()->after('paid_at'); + $table->timestamp('last_synced_at')->nullable()->after('submitted_at'); + }); + + DB::table('rc_service_orders') + ->orderBy('id') + ->chunkById(200, function ($rows): void { + foreach ($rows as $row) { + $status = strtolower((string) ($row->status ?? 'pending')); + + DB::table('rc_service_orders') + ->where('id', (int) $row->id) + ->update([ + 'order_type' => in_array((string) ($row->category ?? ''), ['domain-registration', 'domain-transfer'], true) + ? ((string) $row->category === 'domain-transfer' ? 'domain_transfer' : 'domain_registration') + : 'service', + 'payment_status' => 'paid', + 'fulfillment_status' => match ($status) { + 'active', 'suspended', 'cancelled', 'expired' => 'fulfilled', + 'failed' => 'failed', + default => 'awaiting_vendor', + }, + 'currency' => 'GHS', + 'submitted_at' => $row->created_at, + 'last_synced_at' => $row->updated_at, + ]); + } + }); + } + + public function down(): void + { + Schema::table('rc_service_orders', function (Blueprint $table) { + $table->dropIndex(['order_type']); + $table->dropIndex(['payment_status']); + $table->dropIndex(['fulfillment_status']); + $table->dropIndex(['payment_reference']); + $table->dropColumn([ + 'order_type', + 'payment_status', + 'fulfillment_status', + 'payment_reference', + 'amount_minor', + 'currency', + 'term_months', + 'term_years', + 'paid_at', + 'submitted_at', + 'last_synced_at', + ]); + }); + } +}; diff --git a/resources/views/hosting/account/billing.blade.php b/resources/views/hosting/account/billing.blade.php index 2f7442a..c0bbd70 100644 --- a/resources/views/hosting/account/billing.blade.php +++ b/resources/views/hosting/account/billing.blade.php @@ -1,10 +1,8 @@ -@extends('layouts.email') -@section('title', 'Billing — Ladill Email') -@section('content') + @php $fmt = fn ($m) => 'GHS '.number_format($m / 100, 2); @endphp

Billing

-

Your Ladill wallet funds mailboxes across every Ladill app.

+

Your Ladill wallet funds hosting and other Ladill apps.

@@ -13,7 +11,7 @@ Add funds
-

Spent on email

+

Spent on hosting

{{ $fmt($spentMinor) }}

@@ -24,11 +22,11 @@
-

Mailbox subscriptions

- priced by storage plan +

Hosting subscriptions

+ billed from wallet
@if(empty($paidMailboxes)) -

No paid mailboxes yet — your free allowance covers you.

+

No active hosting subscriptions yet.

@else
    @foreach($paidMailboxes as $m) @@ -47,6 +45,6 @@
@endif
-

Mailboxes beyond your free allowance renew monthly from your wallet. Keep it funded to avoid interruption.

+

Keep your wallet funded to avoid hosting interruption.

-@endsection + diff --git a/resources/views/hosting/account/developers.blade.php b/resources/views/hosting/account/developers.blade.php index 28d1b4a..5afe1e2 100644 --- a/resources/views/hosting/account/developers.blade.php +++ b/resources/views/hosting/account/developers.blade.php @@ -1,9 +1,7 @@ -@extends('layouts.email') -@section('title', 'Developers — Ladill Email') -@section('content') +

Developers

-

API tokens to manage your mailboxes programmatically.

+

API tokens to manage your hosting programmatically.

@if($newToken)
@@ -19,7 +17,6 @@
@endif - {{-- Create --}}

Create a token

@@ -34,7 +31,6 @@
- {{-- Tokens --}}

Your tokens

@forelse($tokens as $token) @@ -56,15 +52,14 @@ @endforelse
- {{-- Docs --}}

Quick start

Authenticate with a Bearer token. Base URL:

{{ $apiBase }} -
curl {{ $apiBase }}/mailboxes \
+        
curl {{ $apiBase }}/me \
   -H "Authorization: Bearer <your-token>" \
   -H "Accept: application/json"
-

Endpoints: GET /me, GET /mailboxes. More coming soon.

+

Endpoints: GET /me. More coming soon.

-@endsection +
diff --git a/resources/views/hosting/account/settings.blade.php b/resources/views/hosting/account/settings.blade.php index d539ec1..7616c8d 100644 --- a/resources/views/hosting/account/settings.blade.php +++ b/resources/views/hosting/account/settings.blade.php @@ -1,9 +1,8 @@ -@extends('layouts.email') -@section('title', 'Settings — Ladill Email') -@section('content') + +@php $emailApp = 'https://email.'.config('app.platform_domain'); @endphp

Settings

-

Defaults, preferences, and account mailbox linking.

+

Hosting preferences and account mailbox linking.

@if($showMailboxLinkUi ?? (($linkStatus['linked_mailbox'] ?? null) || ($linkStatus['show_reminder'] ?? false)))
@@ -31,13 +30,13 @@ @elseif(($linkStatus['stage'] ?? '') === 'needs_domain')

Add and verify an email domain first.

- - Go to domains + + Go to Ladill Email @elseif(($linkStatus['stage'] ?? '') === 'needs_mailbox')

Create a mailbox on your verified domain first.

- + Create mailbox @@ -66,22 +65,11 @@ @csrf @method('PUT')
-

Mailbox defaults

-
-
- - -
-
- - -
+

Notifications

+
+ +
@@ -89,7 +77,7 @@ @@ -98,4 +86,4 @@
-@endsection + diff --git a/resources/views/hosting/account/team.blade.php b/resources/views/hosting/account/team.blade.php index f9405e4..646671d 100644 --- a/resources/views/hosting/account/team.blade.php +++ b/resources/views/hosting/account/team.blade.php @@ -1,9 +1,7 @@ -@extends('layouts.email') -@section('title', 'Team — Ladill Email') -@section('content') +

Team

-

Invite people to help manage this account’s mailboxes & domains.

+

Invite people to help manage this account’s hosting.

@if($canManage)
@@ -25,7 +23,7 @@
-

Admins can manage mailboxes and the team. Members can manage mailboxes. Invitees join by signing in with that hosting.

+

Admins can manage hosting and the team. Members can manage hosting. Invitees join by signing in with that email.

@endif @@ -79,4 +77,4 @@
-@endsection +
diff --git a/resources/views/hosting/account/wallet.blade.php b/resources/views/hosting/account/wallet.blade.php index 59a772b..30ba58b 100644 --- a/resources/views/hosting/account/wallet.blade.php +++ b/resources/views/hosting/account/wallet.blade.php @@ -1,10 +1,8 @@ -@extends('layouts.email') -@section('title', 'Wallet — Ladill Email') -@section('content') + @php $fmt = fn ($m) => 'GHS '.number_format($m / 100, 2); @endphp

Wallet

-

Your Ladill wallet funds mailboxes across every Ladill app.

+

Your Ladill wallet funds hosting and other Ladill apps.

Balance

@@ -14,7 +12,7 @@
-

Spent on email

+

Spent on hosting

{{ $fmt($spentMinor) }}

@@ -22,6 +20,6 @@

{{ $fmt($creditedMinor) }}

-

Mailboxes beyond your free allowance are billed monthly from this wallet.

+

Hosting renewals and upgrades are billed from this wallet.

-@endsection + diff --git a/routes/web.php b/routes/web.php index d453b3d..ddc9ce1 100644 --- a/routes/web.php +++ b/routes/web.php @@ -116,6 +116,8 @@ Route::middleware(['auth'])->group(function () use ($serversApp) { Route::get('/account/billing', [AccountController::class, 'billing'])->name('account.billing'); Route::get('/account/settings', [AccountController::class, 'settings'])->name('account.settings'); Route::put('/account/settings', [AccountController::class, 'updateSettings'])->name('account.settings.update'); + Route::put('/account/settings/mailbox-link', [AccountController::class, 'linkMailbox'])->name('account.settings.mailbox-link'); + Route::delete('/account/settings/mailbox-link', [AccountController::class, 'unlinkMailbox'])->name('account.settings.mailbox-unlink'); Route::post('/mailbox-link-banner/dismiss', [MailboxLinkBannerController::class, 'dismiss'])->name('account.mailbox-link-banner.dismiss'); Route::get('/account/team', [TeamController::class, 'index'])->name('account.team');