From 06dbb1dffb20b657062156f7313818e3716764f0 Mon Sep 17 00:00:00 2001 From: boban Date: Sat, 16 May 2026 01:35:11 +0200 Subject: [PATCH] feat(billing): Cashier Billable on Workspace, Stripe webhook route - Add Laravel\Cashier\Billable trait to Workspace model - Register Workspace as Cashier customer model in AppServiceProvider - Publish cashier config with EUR currency and de_DE locale defaults - Add CSRF exclusion for stripe/* in bootstrap/app.php - Add StripeWebhookTest verifying endpoint exists and rejects bad signatures Co-Authored-By: Claude Sonnet 4.6 --- app/Domains/Workspace/Models/Workspace.php | 3 +- app/Providers/AppServiceProvider.php | 3 + bootstrap/app.php | 4 + config/cashier.php | 130 ++++++++++++++++++++ tests/Feature/Billing/StripeWebhookTest.php | 31 +++++ 5 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 config/cashier.php create mode 100644 tests/Feature/Billing/StripeWebhookTest.php diff --git a/app/Domains/Workspace/Models/Workspace.php b/app/Domains/Workspace/Models/Workspace.php index 06f3776..7b54a7e 100644 --- a/app/Domains/Workspace/Models/Workspace.php +++ b/app/Domains/Workspace/Models/Workspace.php @@ -7,10 +7,11 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Laravel\Cashier\Billable; class Workspace extends Model { - use HasFactory, SoftDeletes; + use HasFactory, SoftDeletes, Billable; protected $guarded = ['id']; diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 262b5cb..9177ac3 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -7,6 +7,7 @@ use App\Domains\Workspace\Models\Workspace; use App\Domains\Workspace\Policies\WorkspacePolicy; use Illuminate\Support\Facades\Gate; use Illuminate\Support\ServiceProvider; +use Laravel\Cashier\Cashier; class AppServiceProvider extends ServiceProvider { @@ -34,5 +35,7 @@ class AppServiceProvider extends ServiceProvider public function boot(): void { Gate::policy(Workspace::class, WorkspacePolicy::class); + + Cashier::useCustomerModel(Workspace::class); } } diff --git a/bootstrap/app.php b/bootstrap/app.php index 738a326..eda1a3a 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -15,6 +15,10 @@ return Application::configure(basePath: dirname(__DIR__)) $middleware->web(append: [ \App\Http\Middleware\LocaleFromUser::class, ]); + + $middleware->validateCsrfTokens(except: [ + 'stripe/*', + ]); }) ->withExceptions(function (Exceptions $exceptions): void { // diff --git a/config/cashier.php b/config/cashier.php new file mode 100644 index 0000000..c5b142a --- /dev/null +++ b/config/cashier.php @@ -0,0 +1,130 @@ + env('STRIPE_KEY'), + + 'secret' => env('STRIPE_SECRET'), + + /* + |-------------------------------------------------------------------------- + | Cashier Path + |-------------------------------------------------------------------------- + | + | This is the base URI path where Cashier's views, such as the payment + | verification screen, will be available from. You're free to tweak + | this path according to your preferences and application design. + | + */ + + 'path' => env('CASHIER_PATH', 'stripe'), + + /* + |-------------------------------------------------------------------------- + | Stripe Webhooks + |-------------------------------------------------------------------------- + | + | Your Stripe webhook secret is used to prevent unauthorized requests to + | your Stripe webhook handling controllers. The tolerance setting will + | check the drift between the current time and the signed request's. + | + */ + + 'webhook' => [ + 'secret' => env('STRIPE_WEBHOOK_SECRET'), + 'tolerance' => env('STRIPE_WEBHOOK_TOLERANCE', 300), + 'events' => WebhookCommand::DEFAULT_EVENTS, + ], + + /* + |-------------------------------------------------------------------------- + | Currency + |-------------------------------------------------------------------------- + | + | This is the default currency that will be used when generating charges + | from your application. Of course, you are welcome to use any of the + | various world currencies that are currently supported via Stripe. + | + */ + + 'currency' => env('CASHIER_CURRENCY', 'eur'), + + /* + |-------------------------------------------------------------------------- + | Currency Locale + |-------------------------------------------------------------------------- + | + | This is the default locale in which your money values are formatted in + | for display. To utilize other locales besides the default en locale + | verify you have the "intl" PHP extension installed on the system. + | + */ + + 'currency_locale' => env('CASHIER_CURRENCY_LOCALE', 'de_DE'), + + /* + |-------------------------------------------------------------------------- + | Payment Confirmation Notification + |-------------------------------------------------------------------------- + | + | If this setting is enabled, Cashier will automatically notify customers + | whose payments require additional verification. You should listen to + | Stripe's webhooks in order for this feature to function correctly. + | + */ + + 'payment_notification' => env('CASHIER_PAYMENT_NOTIFICATION'), + + /* + |-------------------------------------------------------------------------- + | Invoice Settings + |-------------------------------------------------------------------------- + | + | The following options determine how Cashier invoices are converted from + | HTML into PDFs. You're free to change the options based on the needs + | of your application or your preferences regarding invoice styling. + | + */ + + 'invoices' => [ + // Supported: DompdfInvoiceRenderer::class, LaravelPdfInvoiceRenderer::class + 'renderer' => env('CASHIER_INVOICE_RENDERER', DompdfInvoiceRenderer::class), + + 'options' => [ + // Supported: 'letter', 'legal', 'A4' + 'paper' => env('CASHIER_PAPER', 'letter'), + + 'remote_enabled' => env('CASHIER_REMOTE_ENABLED', false), + ], + ], + + /* + |-------------------------------------------------------------------------- + | Stripe Logger + |-------------------------------------------------------------------------- + | + | This setting defines which logging channel will be used by the Stripe + | library to write log messages. You are free to specify any of your + | logging channels listed inside the "logging" configuration file. + | + */ + + 'logger' => env('CASHIER_LOGGER'), + +]; diff --git a/tests/Feature/Billing/StripeWebhookTest.php b/tests/Feature/Billing/StripeWebhookTest.php new file mode 100644 index 0000000..27e3c55 --- /dev/null +++ b/tests/Feature/Billing/StripeWebhookTest.php @@ -0,0 +1,31 @@ + 'whsec_test_dummy_secret']); + + $workspace = \App\Domains\Workspace\Models\Workspace::factory()->create(); + + $payload = [ + 'type' => 'customer.subscription.created', + 'data' => [ + 'object' => [ + 'id' => 'sub_test123', + 'customer' => 'cus_test123', + 'status' => 'active', + 'items' => ['data' => [['price' => ['id' => 'price_pro']]]], + 'current_period_start' => now()->timestamp, + 'current_period_end' => now()->addMonth()->timestamp, + ] + ] + ]; + + $response = $this->postJson('/stripe/webhook', $payload, [ + 'Stripe-Signature' => 'invalid_signature', + ]); + + // 403 because signature verification is active and the signature is invalid. + // This confirms the endpoint exists, CSRF is excluded, and Cashier is wired up. + $response->assertStatus(403); +});