Implement Phase 2: Billing & Subscriptions

Add complete billing system with Stripe and PayPal gateway support,
checkout flow with coupon validation, subscription management
(cancel/resume/swap), payment method management, invoice and
transaction history, webhook handlers, dunning/suspension system
with scheduled processing, and 29 new tests (53 total passing).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude Dev
2026-02-09 07:18:48 -05:00
parent 5988c6d064
commit b1e080d70c
40 changed files with 3018 additions and 1 deletions

View File

@@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Account;
use App\Http\Controllers\Controller;
use App\Models\Invoice;
use App\Services\Billing\BillingServiceFactory;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class BillingController extends Controller
{
public function __construct(
private BillingServiceFactory $billingFactory,
) {}
public function index(Request $request): Response
{
$user = $request->user();
$stripeService = $this->billingFactory->make('stripe');
return Inertia::render('Billing/Index', [
'paymentMethods' => $stripeService->getPaymentMethods($user),
'invoices' => $user->invoices()
->latest()
->take(20)
->get(),
'transactions' => $user->paymentTransactions()
->latest()
->take(20)
->get(),
'intent' => $user->hasStripeId() ? $user->createSetupIntent() : null,
'stripeKey' => config('cashier.key'),
]);
}
public function addPaymentMethod(Request $request): RedirectResponse
{
$request->validate([
'payment_method_id' => ['required', 'string'],
]);
$service = $this->billingFactory->make('stripe');
$success = $service->addPaymentMethod($request->user(), $request->input('payment_method_id'));
if (! $success) {
return back()->with('error', 'Failed to add payment method.');
}
return back()->with('success', 'Payment method added.');
}
public function removePaymentMethod(Request $request, string $paymentMethodId): RedirectResponse
{
$service = $this->billingFactory->make('stripe');
$service->removePaymentMethod($request->user(), $paymentMethodId);
return back()->with('success', 'Payment method removed.');
}
public function setDefaultPaymentMethod(Request $request): RedirectResponse
{
$request->validate([
'payment_method_id' => ['required', 'string'],
]);
$service = $this->billingFactory->make('stripe');
$service->setDefaultPaymentMethod($request->user(), $request->input('payment_method_id'));
return back()->with('success', 'Default payment method updated.');
}
public function invoices(Request $request): Response
{
$invoices = $request->user()
->invoices()
->latest()
->paginate(20);
return Inertia::render('Billing/Invoices', [
'invoices' => $invoices,
]);
}
public function downloadInvoice(Request $request, Invoice $invoice): \Symfony\Component\HttpFoundation\Response
{
if ($invoice->user_id !== $request->user()->id) {
abort(403);
}
if ($invoice->invoice_pdf) {
return redirect($invoice->invoice_pdf);
}
// Generate a basic invoice download as JSON for now
// PDF generation will be added with a dedicated package
return response()->json($invoice->load('items'));
}
public function transactions(Request $request): Response
{
$transactions = $request->user()
->paymentTransactions()
->latest()
->paginate(20);
return Inertia::render('Billing/Transactions', [
'transactions' => $transactions,
]);
}
public function setupIntent(Request $request): JsonResponse
{
$user = $request->user();
if (! $user->hasStripeId()) {
$user->createAsStripeCustomer();
}
return response()->json([
'client_secret' => $user->createSetupIntent()->client_secret,
]);
}
}