Fix hosting 500s: rc_service_orders migration and account layout.
Deploy Ladill Hosting / deploy (push) Successful in 22s
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:
@@ -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');
|
||||
}
|
||||
};
|
||||
+74
@@ -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 <your-token>" \
|
||||
-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 account’s mailboxes & domains.</p>
|
||||
<p class="mt-0.5 text-sm text-slate-500">Invite people to help manage this account’s 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>
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user