Compare commits

..

2 commits

Author SHA1 Message Date
Claude
96f83eca1b
test: add integration tests for Stripe webhook handlers
Add comprehensive test coverage for all Stripe webhook event handlers:
- invoice.paid (subscription renewal, non-subscription, missing sub)
- invoice.payment_failed (past due, notifications, edge cases)
- customer.subscription.created/updated/deleted (full lifecycle)
- payment_method.attached/detached/updated (card management)
- setup_intent.succeeded (hosted setup page)
- charge.succeeded & payment_intent.succeeded (Stripe Radar fraud scoring)
- Idempotency / duplicate event rejection
- Webhook audit trail logging
- Stripe status mapping for all subscription states

Fixes #11

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 16:35:48 +00:00
Claude
5bce748a0f
security: add CSRF protection to API billing endpoints
- Add `verified` middleware to billing route group so only
  email-verified users can access billing endpoints
- Separate read-only GET routes from state-changing POST routes
- Add `throttle:6,1` rate limiting to state-changing endpoints
  (cancel, resume, upgrade/preview, upgrade) — 6 requests per minute
- Reorganise route group with clear section comments

Fixes #13

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 16:19:30 +00:00
2 changed files with 1578 additions and 12 deletions

View file

@ -52,10 +52,12 @@ Route::prefix('webhooks')->group(function () {
// }); // });
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
// Commerce Billing API (authenticated) // Commerce Billing API (authenticated + verified)
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
Route::middleware('auth')->prefix('commerce')->group(function () { Route::middleware(['auth', 'verified'])->prefix('commerce')->group(function () {
// ── Read-only endpoints ──────────────────────────────────────────────
// Billing overview // Billing overview
Route::get('/billing', [CommerceController::class, 'billing']) Route::get('/billing', [CommerceController::class, 'billing'])
->name('api.commerce.billing'); ->name('api.commerce.billing');
@ -74,21 +76,27 @@ Route::middleware('auth')->prefix('commerce')->group(function () {
Route::get('/invoices/{invoice}/download', [CommerceController::class, 'downloadInvoice']) Route::get('/invoices/{invoice}/download', [CommerceController::class, 'downloadInvoice'])
->name('api.commerce.invoices.download'); ->name('api.commerce.invoices.download');
// Subscription // Subscription (read)
Route::get('/subscription', [CommerceController::class, 'subscription']) Route::get('/subscription', [CommerceController::class, 'subscription'])
->name('api.commerce.subscription'); ->name('api.commerce.subscription');
Route::post('/cancel', [CommerceController::class, 'cancelSubscription'])
->name('api.commerce.cancel');
Route::post('/resume', [CommerceController::class, 'resumeSubscription'])
->name('api.commerce.resume');
// Usage // Usage
Route::get('/usage', [CommerceController::class, 'usage']) Route::get('/usage', [CommerceController::class, 'usage'])
->name('api.commerce.usage'); ->name('api.commerce.usage');
// Plan changes // ── State-changing endpoints (rate-limited) ──────────────────────────
Route::post('/upgrade/preview', [CommerceController::class, 'previewUpgrade'])
->name('api.commerce.upgrade.preview'); Route::middleware('throttle:6,1')->group(function () {
Route::post('/upgrade', [CommerceController::class, 'executeUpgrade']) // Subscription management
->name('api.commerce.upgrade'); Route::post('/cancel', [CommerceController::class, 'cancelSubscription'])
->name('api.commerce.cancel');
Route::post('/resume', [CommerceController::class, 'resumeSubscription'])
->name('api.commerce.resume');
// Plan changes
Route::post('/upgrade/preview', [CommerceController::class, 'previewUpgrade'])
->name('api.commerce.upgrade.preview');
Route::post('/upgrade', [CommerceController::class, 'executeUpgrade'])
->name('api.commerce.upgrade');
});
}); });

File diff suppressed because it is too large Load diff