Fix hosting 500s: rc_service_orders migration and account layout.
Deploy Ladill Hosting / deploy (push) Successful in 22s

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 <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-06-06 16:41:27 +00:00
co-authored by Cursor
parent 2df303048c
commit c627d83e59
9 changed files with 150 additions and 60 deletions
@@ -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)
@@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('rc_service_orders', function (Blueprint $table) {
$table->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');
}
};
@@ -0,0 +1,74 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('rc_service_orders', function (Blueprint $table) {
$table->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',
]);
});
}
};
@@ -1,10 +1,8 @@
@extends('layouts.email')
@section('title', 'Billing — Ladill Email')
@section('content')
<x-app-layout title="Billing — Ladill Hosting">
@php $fmt = fn ($m) => 'GHS '.number_format($m / 100, 2); @endphp
<div class="mx-auto max-w-3xl">
<h1 class="text-xl font-semibold tracking-tight text-slate-900">Billing</h1>
<p class="mt-0.5 text-sm text-slate-500">Your Ladill wallet funds mailboxes across every Ladill app.</p>
<p class="mt-0.5 text-sm text-slate-500">Your Ladill wallet funds hosting and other Ladill apps.</p>
<div class="mt-6 grid gap-4 sm:grid-cols-3">
<div class="rounded-2xl bg-gradient-to-br from-indigo-700 via-indigo-600 to-blue-500 p-5 text-white shadow-sm sm:col-span-1">
@@ -13,7 +11,7 @@
<a href="{{ $topupUrl }}" class="mt-3 inline-block rounded-lg bg-white/15 px-3.5 py-1.5 text-xs font-semibold text-white backdrop-blur transition hover:bg-white/25">Add funds</a>
</div>
<div class="rounded-2xl border border-slate-200 bg-white p-5">
<p class="text-xs font-medium uppercase tracking-wide text-slate-400">Spent on email</p>
<p class="text-xs font-medium uppercase tracking-wide text-slate-400">Spent on hosting</p>
<p class="mt-1.5 text-2xl font-semibold text-slate-900">{{ $fmt($spentMinor) }}</p>
</div>
<div class="rounded-2xl border border-slate-200 bg-white p-5">
@@ -24,11 +22,11 @@
<div class="mt-6 rounded-2xl border border-slate-200 bg-white">
<div class="flex items-center justify-between border-b border-slate-100 px-5 py-3.5">
<h2 class="text-sm font-semibold text-slate-900">Mailbox subscriptions</h2>
<span class="text-xs text-slate-400">priced by storage plan</span>
<h2 class="text-sm font-semibold text-slate-900">Hosting subscriptions</h2>
<span class="text-xs text-slate-400">billed from wallet</span>
</div>
@if(empty($paidMailboxes))
<p class="px-5 py-8 text-center text-sm text-slate-400">No paid mailboxes yet your free allowance covers you.</p>
<p class="px-5 py-8 text-center text-sm text-slate-400">No active hosting subscriptions yet.</p>
@else
<ul class="divide-y divide-slate-50">
@foreach($paidMailboxes as $m)
@@ -47,6 +45,6 @@
</div>
@endif
</div>
<p class="mt-4 text-xs text-slate-400">Mailboxes beyond your free allowance renew monthly from your wallet. Keep it funded to avoid interruption.</p>
<p class="mt-4 text-xs text-slate-400">Keep your wallet funded to avoid hosting interruption.</p>
</div>
@endsection
</x-app-layout>
@@ -1,9 +1,7 @@
@extends('layouts.email')
@section('title', 'Developers — Ladill Email')
@section('content')
<x-app-layout title="Developers — Ladill Hosting">
<div class="mx-auto max-w-3xl">
<h1 class="text-xl font-semibold tracking-tight text-slate-900">Developers</h1>
<p class="mt-0.5 text-sm text-slate-500">API tokens to manage your mailboxes programmatically.</p>
<p class="mt-0.5 text-sm text-slate-500">API tokens to manage your hosting programmatically.</p>
@if($newToken)
<div class="mt-6 rounded-2xl border border-emerald-200 bg-emerald-50 p-5">
@@ -19,7 +17,6 @@
</div>
@endif
{{-- Create --}}
<div class="mt-6 rounded-2xl border border-slate-200 bg-white p-5">
<h2 class="text-sm font-semibold text-slate-900">Create a token</h2>
<form method="POST" action="{{ route('account.developers.store') }}" class="mt-4 flex flex-col gap-3 sm:flex-row sm:items-end">
@@ -34,7 +31,6 @@
</form>
</div>
{{-- Tokens --}}
<div class="mt-6 rounded-2xl border border-slate-200 bg-white">
<div class="border-b border-slate-100 px-5 py-3.5"><h2 class="text-sm font-semibold text-slate-900">Your tokens</h2></div>
@forelse($tokens as $token)
@@ -56,15 +52,14 @@
@endforelse
</div>
{{-- Docs --}}
<div class="mt-6 rounded-2xl border border-slate-200 bg-slate-900 p-5 text-slate-200">
<h2 class="text-sm font-semibold text-white">Quick start</h2>
<p class="mt-1 text-xs text-slate-400">Authenticate with a Bearer token. Base URL:</p>
<code class="mt-2 block rounded-lg bg-black/40 px-3 py-2 font-mono text-[11px] text-emerald-300">{{ $apiBase }}</code>
<pre class="mt-3 overflow-x-auto rounded-lg bg-black/40 px-3 py-3 font-mono text-[11px] leading-relaxed text-slate-300"><code>curl {{ $apiBase }}/mailboxes \
<pre class="mt-3 overflow-x-auto rounded-lg bg-black/40 px-3 py-3 font-mono text-[11px] leading-relaxed text-slate-300"><code>curl {{ $apiBase }}/me \
-H "Authorization: Bearer &lt;your-token&gt;" \
-H "Accept: application/json"</code></pre>
<p class="mt-3 text-[11px] text-slate-400">Endpoints: <span class="font-mono text-slate-300">GET /me</span>, <span class="font-mono text-slate-300">GET /mailboxes</span>. More coming soon.</p>
<p class="mt-3 text-[11px] text-slate-400">Endpoints: <span class="font-mono text-slate-300">GET /me</span>. More coming soon.</p>
</div>
</div>
@endsection
</x-app-layout>
@@ -1,9 +1,8 @@
@extends('layouts.email')
@section('title', 'Settings — Ladill Email')
@section('content')
<x-app-layout title="Settings — Ladill Hosting">
@php $emailApp = 'https://email.'.config('app.platform_domain'); @endphp
<div class="mx-auto max-w-2xl">
<h1 class="text-xl font-semibold tracking-tight text-slate-900">Settings</h1>
<p class="mt-0.5 text-sm text-slate-500">Defaults, preferences, and account mailbox linking.</p>
<p class="mt-0.5 text-sm text-slate-500">Hosting preferences and account mailbox linking.</p>
@if($showMailboxLinkUi ?? (($linkStatus['linked_mailbox'] ?? null) || ($linkStatus['show_reminder'] ?? false)))
<div class="mt-6 rounded-2xl border border-indigo-200 bg-white p-5 shadow-sm">
@@ -31,13 +30,13 @@
</form>
@elseif(($linkStatus['stage'] ?? '') === 'needs_domain')
<p class="mt-3 text-sm text-amber-800">Add and verify an email domain first.</p>
<a href="{{ route('hosting.domains.index') }}" class="mt-3 inline-flex items-center gap-1 text-sm font-semibold text-indigo-700 hover:text-indigo-800">
Go to domains
<a href="{{ $emailApp }}/domains" class="mt-3 inline-flex items-center gap-1 text-sm font-semibold text-indigo-700 hover:text-indigo-800">
Go to Ladill Email
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3"/></svg>
</a>
@elseif(($linkStatus['stage'] ?? '') === 'needs_mailbox')
<p class="mt-3 text-sm text-amber-800">Create a mailbox on your verified domain first.</p>
<a href="{{ route('hosting.mailboxes.create') }}" class="mt-3 inline-flex items-center gap-1 text-sm font-semibold text-indigo-700 hover:text-indigo-800">
<a href="{{ $emailApp }}/mailboxes/create" class="mt-3 inline-flex items-center gap-1 text-sm font-semibold text-indigo-700 hover:text-indigo-800">
Create mailbox
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3"/></svg>
</a>
@@ -66,22 +65,11 @@
@csrf @method('PUT')
<div class="rounded-2xl border border-slate-200 bg-white p-5">
<h2 class="text-sm font-semibold text-slate-900">Mailbox defaults</h2>
<div class="mt-4 grid gap-3 sm:grid-cols-2">
<div>
<label class="block text-[11px] font-medium text-slate-500">Default mailbox quota</label>
<select name="default_quota_mb" class="mt-1 w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none">
@php $cur = (int) ($settings->default_quota_mb ?? config('hosting.default_quota_mb', 10240)); @endphp
@foreach([1024 => '1 GB', 5120 => '5 GB', 10240 => '10 GB', 25600 => '25 GB', 51200 => '50 GB'] as $mb => $label)
<option value="{{ $mb }}" @selected($cur === $mb)>{{ $label }}</option>
@endforeach
</select>
</div>
<div>
<label class="block text-[11px] font-medium text-slate-500">Notifications email</label>
<input type="email" name="notify_email" value="{{ old('notify_email', $settings->notify_email ?? $account->email) }}"
class="mt-1 w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none focus:ring-2 focus:ring-indigo-100">
</div>
<h2 class="text-sm font-semibold text-slate-900">Notifications</h2>
<div class="mt-4">
<label class="block text-[11px] font-medium text-slate-500">Notifications email</label>
<input type="email" name="notify_email" value="{{ old('notify_email', $settings->notify_email ?? $account->email) }}"
class="mt-1 w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none focus:ring-2 focus:ring-indigo-100">
</div>
</div>
@@ -89,7 +77,7 @@
<label class="flex items-center justify-between">
<span>
<span class="block text-sm font-medium text-slate-800">Product updates</span>
<span class="block text-xs text-slate-400">Occasional emails about new Ladill Email features.</span>
<span class="block text-xs text-slate-400">Occasional emails about new Ladill Hosting features.</span>
</span>
<input type="checkbox" name="product_updates" value="1" @checked($settings->product_updates ?? true) class="h-5 w-5 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500">
</label>
@@ -98,4 +86,4 @@
<button class="rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-indigo-700">Save settings</button>
</form>
</div>
@endsection
</x-app-layout>
@@ -1,9 +1,7 @@
@extends('layouts.email')
@section('title', 'Team — Ladill Email')
@section('content')
<x-app-layout title="Team — Ladill Hosting">
<div class="mx-auto max-w-3xl">
<h1 class="text-xl font-semibold tracking-tight text-slate-900">Team</h1>
<p class="mt-0.5 text-sm text-slate-500">Invite people to help manage this accounts mailboxes & domains.</p>
<p class="mt-0.5 text-sm text-slate-500">Invite people to help manage this accounts hosting.</p>
@if($canManage)
<div class="mt-6 rounded-2xl border border-slate-200 bg-white p-5">
@@ -25,7 +23,7 @@
</div>
<button class="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-700">Send invite</button>
</form>
<p class="mt-2 text-[11px] text-slate-400">Admins can manage mailboxes and the team. Members can manage mailboxes. Invitees join by signing in with that hosting.</p>
<p class="mt-2 text-[11px] text-slate-400">Admins can manage hosting and the team. Members can manage hosting. Invitees join by signing in with that email.</p>
</div>
@endif
@@ -79,4 +77,4 @@
</ul>
</div>
</div>
@endsection
</x-app-layout>
@@ -1,10 +1,8 @@
@extends('layouts.email')
@section('title', 'Wallet — Ladill Email')
@section('content')
<x-app-layout title="Wallet — Ladill Hosting">
@php $fmt = fn ($m) => 'GHS '.number_format($m / 100, 2); @endphp
<div class="mx-auto max-w-3xl">
<h1 class="text-xl font-semibold tracking-tight text-slate-900">Wallet</h1>
<p class="mt-0.5 text-sm text-slate-500">Your Ladill wallet funds mailboxes across every Ladill app.</p>
<p class="mt-0.5 text-sm text-slate-500">Your Ladill wallet funds hosting and other Ladill apps.</p>
<div class="mt-6 rounded-2xl bg-gradient-to-br from-indigo-700 via-indigo-600 to-blue-500 p-6 text-white shadow-sm">
<p class="text-xs font-medium uppercase tracking-wide text-indigo-100">Balance</p>
@@ -14,7 +12,7 @@
<div class="mt-4 grid gap-4 sm:grid-cols-2">
<div class="rounded-2xl border border-slate-200 bg-white p-5">
<p class="text-xs font-medium uppercase tracking-wide text-slate-400">Spent on email</p>
<p class="text-xs font-medium uppercase tracking-wide text-slate-400">Spent on hosting</p>
<p class="mt-1.5 text-2xl font-semibold text-slate-900">{{ $fmt($spentMinor) }}</p>
</div>
<div class="rounded-2xl border border-slate-200 bg-white p-5">
@@ -22,6 +20,6 @@
<p class="mt-1.5 text-2xl font-semibold text-slate-900">{{ $fmt($creditedMinor) }}</p>
</div>
</div>
<p class="mt-4 text-xs text-slate-400">Mailboxes beyond your free allowance are billed monthly from this wallet.</p>
<p class="mt-4 text-xs text-slate-400">Hosting renewals and upgrades are billed from this wallet.</p>
</div>
@endsection
</x-app-layout>
+2
View File
@@ -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');