diff --git a/.agents/skills/cashier-stripe-development/SKILL.md b/.agents/skills/cashier-stripe-development/SKILL.md new file mode 100644 index 00000000..355e8a92 --- /dev/null +++ b/.agents/skills/cashier-stripe-development/SKILL.md @@ -0,0 +1,108 @@ +--- +name: cashier-stripe-development +description: "Handles Laravel Cashier Stripe integration including subscriptions, webhooks, Stripe Checkout, invoices, charges, refunds, trials, coupons, metered billing, and payment failure handling. Triggered when a user mentions Cashier, Billable, IncompletePayment, stripe_id, newSubscription, Stripe subscriptions, or billing. Also applies when setting up webhooks, handling SCA/3DS payment failures, testing with Stripe test cards, or troubleshooting incomplete subscriptions, CSRF webhook errors, or migration publish issues." +license: MIT +metadata: + author: laravel +--- + +# Cashier Stripe Development + +## When to Apply + +Activate this skill when: + +- Installing or configuring Laravel Cashier Stripe +- Setting up subscriptions, trials, quantities, or plan swapping +- Handling webhooks or SCA/3DS payment failures +- Working with Stripe Checkout, invoices, or charges +- Testing billing scenarios with Stripe test cards or tokens + +## Documentation + +Use `search-docs` for detailed Cashier patterns and documentation covering subscriptions, webhooks, Stripe Checkout, invoices, payment methods, and testing. + +For deeper guidance on specific topics, read the relevant reference file before implementing: + +- `references/subscriptions.md` covers subscription creation, status checks, swapping, trials, quantities, and multiple products +- `references/webhooks.md` covers webhook setup, custom handlers, CSRF exclusion, and local development with the Stripe CLI +- `references/testing.md` covers Stripe test cards, payment method tokens, and feature test patterns + +## Basic Usage + +### Installation + +```bash +php artisan vendor:publish --tag="cashier-migrations" +php artisan migrate +php artisan vendor:publish --tag="cashier-config" +``` + +### Environment Variables + +``` +STRIPE_KEY=pk_test_... +STRIPE_SECRET=sk_test_... +STRIPE_WEBHOOK_SECRET=whsec_... +CASHIER_CURRENCY=usd +CASHIER_CURRENCY_LOCALE=en_US +``` + +### Billable Model + + +```php +use Laravel\Cashier\Billable; + +class User extends Authenticatable +{ + use Billable; +} +``` + +For a non-User model, register it in a service provider: + + +```php +// In AppServiceProvider::boot() +Cashier::useCustomerModel(Team::class); +``` + +### Creating a Subscription + + +```php +use Laravel\Cashier\Exceptions\IncompletePayment; + +try { + $user->newSubscription('default', 'price_xxxx')->create($paymentMethodId); +} catch (IncompletePayment $e) { + return redirect()->route('cashier.payment', [$e->payment->id, 'redirect' => route('home')]); +} +``` + +Always wrap subscription creation in a try/catch for `IncompletePayment`. When a card requires 3DS authentication, Cashier throws this exception. The `cashier.payment` route is auto-registered and handles the confirmation flow. + +## Verification + +1. Run migrations and confirm `stripe_id`, `pm_type`, `pm_last_four`, and `trial_ends_at` columns exist on the billable model table +2. Test the webhook endpoint with `stripe listen --forward-to localhost/stripe/webhook` if you use the default path, or swap `stripe` for your configured `CASHIER_PATH` +3. Confirm `$user->subscribed('default')` returns the expected value for active and incomplete subscriptions + +## Common Pitfalls + +- The migration publish tag is `cashier-migrations`, not `cashier`. Running `migrate` before publishing results in missing columns and tables. +- `CASHIER_CURRENCY` must be set explicitly. It defaults to USD, which silently breaks non-US apps. +- The Stripe CLI generates its own webhook signing secret. It is different from the Dashboard endpoint secret. Using the wrong one causes signature verification failures. +- The webhook route must be excluded from CSRF verification using your configured `cashier.path`. If you change `CASHIER_PATH` from `stripe` to `billing`, exclude `billing/*`, not `stripe/*`. +- `canceled()` returns true as soon as `cancel()` is called, but the user still has access during the grace period. Use `ended()` to confirm access is fully revoked. +- `subscribed()` returns true during the grace period even though the subscription is canceled. +- `subscribed()` returns false for `incomplete` and `past_due` subscriptions by default. +- Prices cannot be swapped and quantity cannot be updated while a subscription has an incomplete payment. +- When extending `WebhookController`, call `Cashier::ignoreRoutes()` in a service provider and re-register both `cashier.payment` and `cashier.webhook` under the configured `cashier.path`. +- Use `Cashier::useCustomerModel()` in a service provider to set a custom billable model. There is no `CASHIER_MODEL` env var. +- `trial_ends_at` is a local database column synced via webhooks. It will be stale if webhooks are not configured in production. +- In MySQL, the `stripe_id` column must use `utf8_bin` collation to avoid case-sensitivity issues. +- `noProrate()` has no effect when combined with `swapAndInvoice()`. That method always prorates. +- Methods like `withPromotionCode()` require the Stripe API ID such as `promo_xxxx`, not the customer-facing code. Use `findPromotionCode()` to resolve a code to its ID. +- Always use `search-docs` for the latest Cashier documentation rather than relying on this skill alone. \ No newline at end of file diff --git a/.agents/skills/cashier-stripe-development/references/subscriptions.md b/.agents/skills/cashier-stripe-development/references/subscriptions.md new file mode 100644 index 00000000..ad7113ea --- /dev/null +++ b/.agents/skills/cashier-stripe-development/references/subscriptions.md @@ -0,0 +1,108 @@ +# Subscriptions Reference + +Use `search-docs` for authoritative documentation on subscriptions. + +## Status Checks + +| Method | Returns true when | +|---|---| +| `$user->subscribed('default')` | Active or on grace period | +| `->onTrial()` | Trial period active | +| `->onGracePeriod()` | Canceled, period not yet ended | +| `->canceled()` | `ends_at` is set, may still have access | +| `->ended()` | Canceled and grace period expired | +| `->incomplete()` | Awaiting SCA/3DS confirmation | +| `->pastDue()` | Payment overdue | +| `->recurring()` | Active and not on trial | + +Check by product or price: + +```php +$user->subscribedToProduct('prod_premium', 'default'); +$user->subscribedToPrice('price_monthly', 'default'); +``` + +## Swapping Plans + +```php +$user->subscription('default')->swap('price_new'); +$user->subscription('default')->noProrate()->swap('price_new'); +$user->subscription('default')->swapAndInvoice('price_new'); +$user->subscription('default')->skipTrial()->swap('price_new'); +``` + +## Quantity + +```php +$user->subscription('default')->incrementQuantity(); +$user->subscription('default')->decrementQuantity(); +$user->subscription('default')->updateQuantity(10); +$user->subscription('default')->noProrate()->updateQuantity(10); +``` + +## Trials + +```php +$user->newSubscription('default', 'price_xxxx') + ->trialDays(14) + ->create($paymentMethodId); + +$subscription->extendTrial(now()->addDays(7)); +``` + +## Multiple Products on One Subscription + +```php +$user->newSubscription('default', ['price_monthly', 'price_chat']) + ->quantity(5, 'price_chat') + ->create($paymentMethod); + +$user->subscription('default')->addPrice('price_chat'); +$user->subscription('default')->removePrice('price_chat'); +$user->subscription('default')->swap(['price_pro', 'price_chat']); +``` + +## Multiple Subscriptions + +```php +$user->newSubscription('swimming', 'price_swimming_monthly')->create($pm); +$user->newSubscription('gym', 'price_gym_monthly')->create($pm); + +$user->subscription('swimming')->swap('price_swimming_yearly'); +$user->subscription('gym')->cancel(); +``` + +## Cancellation and Resumption + +```php +$user->subscription('default')->cancel(); // At end of billing period +$user->subscription('default')->cancelNow(); // Immediately +$user->subscription('default')->resume(); // During grace period only +``` + +## Incomplete Payment Handling + +```php +if ($user->hasIncompletePayment('default')) { + $paymentId = $user->subscription('default')->latestPayment()->id; + return redirect()->route('cashier.payment', $paymentId); +} +``` + +Opt out of default deactivation behavior: + +```php +Cashier::keepPastDueSubscriptionsActive(); +Cashier::keepIncompleteSubscriptionsActive(); +``` + +## Metered / Usage-Based Billing + +```php +$user->newSubscription('default') + ->meteredPrice('price_metered') + ->create($paymentMethodId); + +$user->reportMeterEvent('emails-sent'); +$user->reportMeterEvent('emails-sent', quantity: 15); +``` \ No newline at end of file diff --git a/.agents/skills/cashier-stripe-development/references/testing.md b/.agents/skills/cashier-stripe-development/references/testing.md new file mode 100644 index 00000000..97e91f91 --- /dev/null +++ b/.agents/skills/cashier-stripe-development/references/testing.md @@ -0,0 +1,52 @@ +# Testing Reference + +Use `search-docs` for authoritative documentation on testing Cashier integrations. + +## Test Cards and Tokens + +Use card numbers for browser-based flows (Stripe.js / Checkout). Use `pm_card_*` tokens directly in feature tests that call the Stripe API. + +| Card Number | Token | Behavior | +|---|---|---| +| `4242 4242 4242 4242` | `pm_card_visa` | Succeeds immediately | +| `4000 0025 0000 3155` | `pm_card_threeDSecure2Required` | Requires SCA/3DS | +| `4000 0027 6000 3184` | `pm_card_authenticationRequired` | Requires authentication | +| `4000 0000 0000 9995` | `pm_card_chargeDeclinedInsufficientFunds` | Declined, insufficient funds | +| `4000 0000 0000 0002` | `pm_card_chargeDeclined` | Declined | + +Use expiry `12/34`, any CVC, any ZIP for card number inputs. + +## Feature Test Example + +Feature tests that hit the real Stripe test API use `pm_card_*` tokens: + +```php +public function test_user_can_subscribe(): void +{ + $user = User::factory()->create(); + + $user->newSubscription('default', 'price_xxxx') + ->create('pm_card_visa'); + + $this->assertTrue($user->subscribed('default')); +} + +public function test_incomplete_payment_is_handled(): void +{ + $user = User::factory()->create(); + + try { + $user->newSubscription('default', 'price_xxxx') + ->create('pm_card_threeDSecure2Required'); + } catch (\Laravel\Cashier\Exceptions\IncompletePayment $e) { + $this->assertTrue($user->subscription('default')->incomplete()); + } +} +``` + +## Setup Notes + +- Use Stripe test mode keys (`sk_test_...`, `pk_test_...`) in your test environment +- Cashier does not ship a global `fake()` helper. Tests hit the real Stripe test API by default. +- Refer to `tests/Feature/` in the Cashier package itself for integration test patterns covering subscription creation, payment methods, and webhook handling +- Use `search-docs` for current guidance on mocking Stripe HTTP calls or using Stripe's test clock feature for time-sensitive scenarios \ No newline at end of file diff --git a/.agents/skills/cashier-stripe-development/references/webhooks.md b/.agents/skills/cashier-stripe-development/references/webhooks.md new file mode 100644 index 00000000..19d2f708 --- /dev/null +++ b/.agents/skills/cashier-stripe-development/references/webhooks.md @@ -0,0 +1,132 @@ +# Webhooks Reference + +Use `search-docs` for authoritative documentation on webhooks. + +## Auto-Registered Routes + +Cashier registers two routes automatically under the `cashier.path` prefix (`config('cashier.path')`, default `stripe`): + +- `POST /{cashier.path}/webhook` named `cashier.webhook` +- `GET /{cashier.path}/payment/{id}` named `cashier.payment` + +With the default config these are `/stripe/webhook` and `/stripe/payment/{id}`. If you set `CASHIER_PATH=billing`, they become `/billing/webhook` and `/billing/payment/{id}`. + +## CSRF Exclusion + +Use the same path prefix you configured for Cashier here. If `CASHIER_PATH=billing`, exclude `billing/*` instead of `stripe/*`. + +**Laravel 11+ (`bootstrap/app.php`, default path example):** + +```php +->withMiddleware(function (Middleware $middleware) { + $middleware->validateCsrfTokens(except: ['stripe/*']); +}) +``` + +**Laravel 10 (`app/Http/Middleware/VerifyCsrfToken.php`, default path example):** + +```php +protected $except = [ + 'stripe/*', +]; +``` + +## Local Development with Stripe CLI + +If you changed `cashier.path`, forward Stripe CLI events to that URL instead of `/stripe/webhook`. + +```bash +stripe login +stripe listen --forward-to your-app.test/stripe/webhook +stripe trigger invoice.payment_succeeded +``` + +The CLI outputs a `whsec_...` signing secret specific to that session. Set it as `STRIPE_WEBHOOK_SECRET` locally. It is not the same as the Dashboard endpoint secret. + +## Registering Events in the Stripe Dashboard + +Use the Artisan command to create the endpoint automatically with all required events: + +```bash +php artisan cashier:webhook +``` + +Cashier's `cashier:webhook` command registers these events by default: + +- `customer.subscription.created` +- `customer.subscription.updated` +- `customer.subscription.deleted` +- `customer.updated` / `customer.deleted` +- `invoice.payment_action_required` +- `invoice.payment_succeeded` +- `payment_method.automatically_updated` + +Cashier's `WebhookController` has built-in handlers for all of the above except `invoice.payment_succeeded`. For renewal hooks, prefer `WebhookReceived` / `WebhookHandled` listeners unless you intentionally add your own controller method. + +## Custom Handlers: Extending WebhookController + +Method name pattern: `handle` + StudlyCase of event type with dots replaced by underscores. + +`customer.subscription.created` becomes `handleCustomerSubscriptionCreated`. + +```php +use Laravel\Cashier\Http\Controllers\WebhookController as CashierController; + +class StripeWebhookController extends CashierController +{ + public function handleCustomerSubscriptionCreated(array $payload) + { + $response = parent::handleCustomerSubscriptionCreated($payload); + + // your logic after Cashier syncs the subscription + + return $response; + } +} +``` + +If you add a method for an event Cashier does not handle internally, such as `invoice.payment_succeeded`, do not call `parent::handle...()` unless the base controller actually defines that method. + +In a service provider, disable auto-registration and re-register both Cashier routes so the incomplete-payment flow and `cashier:webhook` command keep working: + +```php +Cashier::ignoreRoutes(); +``` + +```php +// routes/web.php +use App\Http\Controllers\StripeWebhookController; +use Illuminate\Support\Facades\Route; +use Laravel\Cashier\Http\Controllers\PaymentController; + +Route::prefix(config('cashier.path')) + ->name('cashier.') + ->group(function () { + Route::get('payment/{id}', [PaymentController::class, 'show'])->name('payment'); + Route::post('webhook', [StripeWebhookController::class, 'handleWebhook'])->name('webhook'); + }); +``` + +Keep the `cashier.webhook` route name unless you plan to pass `--url` explicitly to `php artisan cashier:webhook`. + +## Custom Handlers: Listening to Events + +The simpler option when you do not need to replace Cashier's internal logic, or when you want to react to events such as `invoice.payment_succeeded` that Cashier does not process itself: + +```php +use Laravel\Cashier\Events\WebhookReceived; +use Laravel\Cashier\Events\WebhookHandled; + +// WebhookReceived fires for every event before Cashier processes it +// WebhookHandled fires after Cashier processes it + +Event::listen(WebhookReceived::class, function (WebhookReceived $event) { + if ($event->payload['type'] === 'invoice.payment_succeeded') { + // handle renewal + } +}); +``` + +## Signature Verification + +`VerifyWebhookSignature` middleware is applied automatically when `cashier.webhook.secret` is set. No extra wiring is needed. \ No newline at end of file diff --git a/.agents/skills/developing-with-fortify/SKILL.md b/.agents/skills/fortify-development/SKILL.md similarity index 99% rename from .agents/skills/developing-with-fortify/SKILL.md rename to .agents/skills/fortify-development/SKILL.md index a09178e3..bd05dd6d 100644 --- a/.agents/skills/developing-with-fortify/SKILL.md +++ b/.agents/skills/fortify-development/SKILL.md @@ -1,5 +1,5 @@ --- -name: developing-with-fortify +name: fortify-development description: Laravel Fortify headless authentication backend development. Activate when implementing authentication features including login, registration, password reset, email verification, two-factor authentication (2FA/TOTP), profile updates, headless auth, authentication scaffolding, or auth guards in Laravel applications. --- diff --git a/.agents/skills/inertia-react-development/SKILL.md b/.agents/skills/inertia-react-development/SKILL.md index 5fe2712d..bb01fd9c 100644 --- a/.agents/skills/inertia-react-development/SKILL.md +++ b/.agents/skills/inertia-react-development/SKILL.md @@ -15,7 +15,7 @@ Activate this skill when: - Creating or modifying React page components for Inertia - Working with forms in React (using `