Files
website/website/app/Http/Controllers/Admin/InvoiceController.php
Claude Dev 9a410dc3c8 Fix impersonation routing, invoice numbers, dashboard caching, indexes, and Stripe property name
- Add impersonation stop route on account subdomain so impersonated users can exit (#11)
- Replace non-concurrency-safe invoice number generation (count+1, rand) with date+random string
- Wrap admin dashboard stats queries in Cache::remember with 5-minute TTL
- Add database indexes on invoices.status, orders.status, audit_logs.action, audit_logs.created_at
- Fix last_four to last4 matching Stripe's actual card property name

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 18:53:37 -04:00

249 lines
8.1 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\StoreInvoiceRequest;
use App\Http\Requests\Admin\UpdateInvoiceRequest;
use App\Models\AuditLog;
use App\Models\Invoice;
use App\Models\User;
use App\Notifications\InvoiceNotification;
use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Inertia\Inertia;
use Inertia\Response;
class InvoiceController extends Controller
{
public function index(Request $request): Response
{
$query = Invoice::query()
->with('user:id,name,email');
// Search by invoice number or customer name
if ($search = $request->input('search')) {
$query->where(function ($q) use ($search): void {
$q->where('number', 'like', "%{$search}%")
->orWhereHas('user', function ($uq) use ($search): void {
$uq->where('name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%");
});
});
}
// Filter by status
if ($status = $request->input('status')) {
$query->where('status', $status);
}
$invoices = $query->latest()->paginate(15)->withQueryString();
return Inertia::render('Admin/Invoices/Index', [
'invoices' => $invoices,
'filters' => [
'search' => $request->input('search', ''),
'status' => $request->input('status', ''),
],
]);
}
public function create(): Response
{
$customers = User::query()
->select('id', 'name', 'email')
->orderBy('name')
->get();
return Inertia::render('Admin/Invoices/Create', [
'customers' => $customers,
]);
}
public function store(StoreInvoiceRequest $request): RedirectResponse
{
$validated = $request->validated();
$invoice = DB::transaction(function () use ($validated, $request): Invoice {
// Calculate total from line items
$total = collect($validated['items'])->sum(function (array $item): float {
return (float) $item['unit_price'] * (int) $item['quantity'];
});
// Generate unique invoice number
$number = 'INV-'.now()->format('Ymd').'-'.strtoupper(Str::random(6));
$status = ($validated['send_immediately'] ?? false) ? 'pending' : 'draft';
$invoice = Invoice::create([
'user_id' => $validated['customer_id'],
'gateway' => 'manual',
'number' => $number,
'total' => $total,
'tax' => 0,
'currency' => 'USD',
'status' => $status,
'due_date' => $validated['due_date'],
'notes' => $validated['notes'] ?? null,
]);
// Create line items
foreach ($validated['items'] as $item) {
$invoice->items()->create([
'description' => $item['description'],
'quantity' => $item['quantity'],
'amount' => $item['unit_price'],
]);
}
AuditLog::create([
'user_id' => $validated['customer_id'],
'admin_id' => $request->user()?->id,
'action' => 'create_invoice',
'resource_type' => 'invoice',
'resource_id' => $invoice->id,
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent(),
]);
return $invoice;
});
// Send email if requested
if ($validated['send_immediately'] ?? false) {
$invoice->load('user');
$invoice->user?->notify(new InvoiceNotification($invoice));
}
return redirect()->route('invoices.show', $invoice)
->with('success', "Invoice {$invoice->number} has been created.");
}
public function show(Invoice $invoice): Response
{
$invoice->load([
'user:id,name,email,status',
'items',
'paymentTransactions',
]);
return Inertia::render('Admin/Invoices/Show', [
'invoice' => $invoice,
]);
}
public function edit(Invoice $invoice): Response|RedirectResponse
{
if (! in_array($invoice->status, ['draft', 'pending'])) {
return redirect()->route('invoices.show', $invoice)
->with('error', 'Only draft or pending invoices can be edited.');
}
$invoice->load(['user:id,name,email', 'items']);
return Inertia::render('Admin/Invoices/Edit', [
'invoice' => $invoice,
]);
}
public function update(UpdateInvoiceRequest $request, Invoice $invoice): RedirectResponse
{
if (! in_array($invoice->status, ['draft', 'pending'])) {
return redirect()->route('invoices.show', $invoice)
->with('error', 'Only draft or pending invoices can be edited.');
}
$validated = $request->validated();
DB::transaction(function () use ($invoice, $validated, $request): void {
// Delete existing items and recreate
$invoice->items()->delete();
$total = 0.0;
foreach ($validated['items'] as $item) {
$lineTotal = (float) $item['unit_price'] * (int) $item['quantity'];
$total += $lineTotal;
$invoice->items()->create([
'description' => $item['description'],
'quantity' => $item['quantity'],
'amount' => $item['unit_price'],
]);
}
$invoice->update([
'total' => $total,
'due_date' => $validated['due_date'],
'notes' => $validated['notes'] ?? null,
]);
AuditLog::create([
'user_id' => $invoice->user_id,
'admin_id' => $request->user()?->id,
'action' => 'update_invoice',
'resource_type' => 'invoice',
'resource_id' => $invoice->id,
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent(),
]);
});
return redirect()->route('invoices.show', $invoice)
->with('success', "Invoice {$invoice->number} has been updated.");
}
public function resend(Invoice $invoice): RedirectResponse
{
$invoice->load('user');
if (! $invoice->user) {
return redirect()->back()->with('error', 'Cannot resend: no customer associated with this invoice.');
}
$invoice->user->notify(new InvoiceNotification($invoice));
AuditLog::create([
'user_id' => $invoice->user_id,
'admin_id' => auth()->id(),
'action' => 'resend_invoice',
'resource_type' => 'invoice',
'resource_id' => $invoice->id,
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
]);
return redirect()->back()->with('success', "Invoice {$invoice->number} email has been queued for delivery.");
}
public function download(Invoice $invoice): \Symfony\Component\HttpFoundation\Response
{
$invoice->load(['user', 'items']);
$pdf = Pdf::loadView('pdf.invoice', ['invoice' => $invoice]);
return $pdf->download("invoice-{$invoice->number}.pdf");
}
public function void(Invoice $invoice): RedirectResponse
{
$invoice->update(['status' => 'void']);
AuditLog::create([
'user_id' => $invoice->user_id,
'admin_id' => auth()->id(),
'action' => 'void_invoice',
'resource_type' => 'invoice',
'resource_id' => $invoice->id,
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
]);
return redirect()->back()->with('success', "Invoice {$invoice->number} has been voided.");
}
}