Skip to main content

stacks-payments

Use when implementing payment processing in Stacks - Stripe charges, subscriptions, checkout sessions, customer management, payment methods, invoices, coupons, promo codes, products, prices, webhooks, or the Payment facade. Covers @stacksjs/payments and config/payment.ts.

Quellinformationen

Repository
stacksjs/stacks
Letzte Quellaktivität
6. Oktober 2026 um 21:03
Erkannte Sprache von SKILL.md
Englisch
Sterne
631
Forks
20

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
stacks-payments
description
Use when implementing payment processing in Stacks - Stripe charges, subscriptions, checkout sessions, customer management, payment methods, invoices, coupons, promo codes, products, prices, webhooks, or the Payment facade. Covers @stacksjs/payments and config/payment.ts.
license
MIT
compatibility
Bun >= 1.3.0, TypeScript
allowed-tools
Read Edit Write Bash Grep Glob
# Stacks Payments Full Stripe integration via the Payment facade. Uses Stripe API version `2026-01-28.clover`. The Stripe SDK is initialized from `services.stripe.secretKey` (sourced from `config/payment.ts`). ## Payment Drivers (provider-neutral) `paymentDriver()` (also `Payment.driver()`) returns the driver `config.payment.driver` selects: `stripe` (default), `adyen`, `paddle` or `lemonsqueezy`, or one added with `registerPaymentDriver(name, factory)`. Source: `storage/framework/core/payments/src/driver/`. - Money is `{ amount, currency }` in minor units; `assertMoney` refuses floats before a request leaves. - Methods: `customer`, `charge(payer, money, paymentMethodId)`, `createPayment` (browser completes: `clientConfirmation` is a Stripe client secret or an Adyen session), `refund`, `checkout(payer, { mode, lines, successUrl, ... })`, `paymentMethods`, `removePaymentMethod`, `subscribe`, `cancelSubscription`, `subscriptions`, `verifyWebhook({ payload, headers })` -> `PaymentEvent[]`, `acknowledgeWebhook()`. - `checkout` copies `reference` and `metadata` onto the payment / subscription / setup intent it creates (Stripe: `payment_intent_data`, `subscription_data`, `setup_intent_data`), so their webhooks can be attributed. `allowPromotionCodes` and `automaticTax` are Stripe only; Adyen refuses them. - Unsupported operations throw `PaymentUnsupportedError` (driver + operation). Adyen has no catalog, subscriptions, invoices, coupons or portal; Paddle has no server-side charge, server-side subscribe, setup checkout, promotion-code or trial option on a checkout, or separate cancel URL. - Paddle (`driver: 'paddle'`, Billing API v1, merchant of record): customers found or created by email; every payment is a transaction completed in Paddle.js (`clientConfirmation: { provider: 'paddle', transactionId, successUrl? }`), so `charge` and `subscribe` are unsupported and subscriptions start at `checkout` in `subscription` mode with catalog prices; refunds are adjustments Paddle reviews (`pending`, settled by `adjustment.updated`; a refund's event id is the adjustment's, so a created-and-updated approval counts once); webhooks `Paddle-Signature` = HMAC-SHA256 hex of `<ts>:<raw body>` under the secret, 5 s tolerance. Paddle has no hosted page: `GET /payments/checkout` (mounted while the driver is `paddle`) is the default payment link page, reading `stacks_success_url` from the transaction for Paddle.js. Env: `PADDLE_API_KEY`, `PADDLE_ENVIRONMENT`, `PADDLE_CLIENT_TOKEN`, `PADDLE_WEBHOOK_SECRET`, `PADDLE_TAX_CATEGORY`. - Lemon Squeezy (`driver: 'lemonsqueezy'`, API v1, merchant of record): customers found or created by email in the store; `checkout` sells exactly one variant (a `price` line is a variant id; a line priced here sells as `LEMONSQUEEZY_VARIANT_ID` at a custom price in the store's currency); `charge`, `createPayment`, `subscribe` and payment methods are unsupported; refunds are immediate against the order; `cancelSubscription` needs `{ atPeriodEnd: true }`; webhooks `X-Signature` = HMAC-SHA256 hex of the raw body, event id = digest of the bytes, `order_refunded` carries `refundedTotal` (the order's running total), which commerce applies as a difference. Env: `LEMONSQUEEZY_API_KEY`, `LEMONSQUEEZY_STORE_ID`, `LEMONSQUEEZY_WEBHOOK_SECRET`, `LEMONSQUEEZY_VARIANT_ID`, `LEMONSQUEEZY_TEST_MODE`. - Adyen: Checkout API v72, `X-API-Key`; shopper reference `user-<id>` (no PII); full refunds go to `/reversals`; webhooks verified per item with HMAC-SHA256 over 8 colon-joined fields under the hex key, acknowledged with 202. - `PaymentEvent` carries `provider` and `id` (together, the retry key), `reference` (the payment; for a refund, the payment refunded), `amount` (for a refund, that one refund - Stripe's cumulative `amount_refunded` is turned into a delta from `previous_attributes`) and `reason`. Adyen's id is `eventCode:pspReference:success`, since Adyen may resend a pair with the outcome changed. - The Stripe driver delegates to the existing billable modules, so idempotency keys and `stripe_id` handling are unchanged. - Webhook route: the `payments` default route bundle (mounted with the `commerce` feature) serves `POST /webhooks/payments` through `orders.receivePaymentWebhook(request)`: 401 `missing-config` while the driver's secret is unset (`WebhookNotConfiguredError`), 400 `invalid-signature` (`WebhookSignatureError`), otherwise applies the events and answers with `acknowledgeWebhook()`. - Commerce: `orders.handleCommercePaymentEvent(event)` from `@stacksjs/commerce` applies `payment.succeeded` / `payment.failed` (reason into `payments.failure_reason`) / `refund.succeeded` (added to `refund_amount`; `refunded` + order REFUNDED only when it covers the payment, else `partiallyRefunded`) to the order, deduplicated in `payment_webhook_events` (the `PaymentWebhookEvent` model) in the same transaction. - The Payment facade's neutral functions go through this driver; its Stripe-only functions and everything Stripe-level below return Stripe objects. ## Key Paths - Core package: `storage/framework/core/payments/src/` - Payment facade: `storage/framework/core/payments/src/payment.ts` - Stripe driver: `storage/framework/core/payments/src/drivers/stripe.ts` - Billable modules: `storage/framework/core/payments/src/billable/` - Configuration: `config/payment.ts` - SaaS config: `config/saas.ts` - Default billing functions: `storage/framework/defaults/functions/billing/payments.ts` ## Package Exports - `Payment` (default) -- facade object with all payment methods - All individual functions (`charge`, `subscribe`, `checkout`, etc.) - All billable modules (`manageCharge`, `manageCustomer`, `manageSubscription`, etc.) - `stripe` -- raw Stripe SDK instance - `Stripe` -- re-exported Stripe types namespace - `stacksIdempotencyKey`, `freshIdempotencyKey` -- see Idempotency below ## Payment Facade The `Payment` object aggregates the payment operations; every function is also a standalone export. It has two halves (`storage/framework/core/payments/src/payment.ts`): - **Provider-neutral** - through `paymentDriver()`, so they work on Stripe and Adyen alike. Money is `{ amount, currency }` in minor units; results are the driver's shapes. - **Stripe only** - Stripe's parameters and types, and `PaymentUnsupportedError` (naming the operation) under any other driver: `changeSubscription`, `updateCustomer`, `deleteCustomer`, `addPaymentMethod`, `setDefaultPaymentMethod`, `createSetupIntent`, `getInvoices`, `createInvoice`, `payInvoice`, `createProduct`, `getPrice`, `listProducts`, `createCoupon`, `createPromoCode`, `validatePromoCode`, `billingPortal`. The check is `assertStripeDriver(operation)`, shared with the billable trait. Something Stripe-specific the neutral half cannot express (a Connect destination charge's `payment_intent_data.transfer_data`, `prorate`/`invoice_now` on a cancellation, customer create params) goes through the `manage*` modules or the raw `stripe` client. ### Charges ```typescript import { Payment } from '@stacksjs/payments' // Charge a stored payment method, server side const result = await Payment.charge(user, { amount: 2999, currency: 'usd' }, 'pm_xxx', { reference: 'order-1' }) // A payment the browser completes: result.clientConfirmation is a Stripe client secret or an Adyen session const pending = await Payment.createPayment(user, { amount: 2999, currency: 'usd' }) // Refund -- partial, or full when the amount is left out const partial = await Payment.refund('pi_xxx', { amount: { amount: 1000, currency: 'usd' } }) const full = await Payment.refund('pi_xxx') ``` `PaymentResult` has `id`, `status` (`succeeded`, `processing`, `requires_action`, `requires_payment_method`, `failed`, `canceled`), `amount`, `clientConfirmation`, `redirectUrl` and `raw` (the provider's object; strip it before a browser sees it). The `manageCharge` module is the Stripe-level API (`findPayment(id)` and the PaymentIntent params). ### Checkout Sessions ```typescript const session = await Payment.checkout(user, { mode: 'payment', lines: [{ price: 'price_xxx', quantity: 1 }], successUrl: 'https://app.example.com/success', cancelUrl: 'https://app.example.com/cancel', }) // One price in subscription mode const upgrade = await Payment.subscriptionCheckout(user, 'price_xxx', { successUrl: 'https://app.example.com/welcome', cancelUrl: 'https://app.example.com/pricing', metadata: { user_id: String(user.id) }, }) ``` Send the payer to `session.url`. `trialDays` starts a subscription checkout with a trial. `reference` and `metadata` are copied onto the payment or subscription the checkout creates. Return URLs must be on the app's origin. ### Subscriptions ```typescript // Subscribe to a price (a Stripe lookup key); `type` names it locally, `default` unless given const sub = await Payment.subscribe(user, 'premium-monthly') // Cancel now await Payment.cancelSubscription('sub_xxx') // Stop renewing: entitled until the period ends, recorded in the row's ends_at await Payment.cancelSubscription('sub_xxx', { atPeriodEnd: true }) // The provider's subscriptions, every status const all = await Payment.subscriptions(user) // Entitlement, from the local subscriptions table const hasActive = await Payment.hasActiveSubscription(user, 'default') // Stripe only: swap the subscription item's price const changed = await Payment.changeSubscription(user, 'enterprise-monthly') ``` `subscribe()` resolves the price by lookup key, creates the subscription with `payment_behavior: 'allow_incomplete'` and `expand: ['latest_invoice.confirmation_secret']` (an incomplete subscription's summary carries that as `clientConfirmation`), and stores it in `subscriptions`. Dates are stored as timestamps from the subscription item's `current_period_end`, where Stripe keeps it since API 2025-03-31.basil. `isValid()` returns `true` if the subscription status is `'active'` or `'trialing'`. `isIncomplete()` checks for `'incomplete'` status. `cancelSubscription(id, immediately)` is gone: its `false` also cancelled at once, only with a proration. For Stripe's `prorate` / `invoice_now`, call `manageSubscription.cancel(id, params)`. ### Customers ```typescript // The provider's customer, created on first use: { id, email, name } const customer = await Payment.getOrCreateCustomer(user) // Stripe only const updated = await Payment.updateCustomer(user, { name: 'Jane' }) const deleted = await Payment.deleteCustomer(user) ``` `createOrGetStripeUser()` (what the Stripe driver uses) checks `user.stripe_id` first. If the user has one, it retrieves the customer from Stripe. If the customer was deleted (404 or `deleted: true`), it creates a new one. On creation, it auto-fills `name` and `email` from the user model and calls `user.update({ stripe_id: customer.id })`. Pass Stripe customer create params through `manageCustomer.createOrGetStripeUser(user, params)`. Additional methods on `manageCustomer`: - `stripeId(user)` -- returns `user.stripe_id` - `hasStripeId(user)` -- boolean check - `createStripeCustomer(user, options)` -- throws if user already has a stripe_id - `createOrUpdateStripeUser(user, options)` -- creates or updates - `retrieveStripeUser(user)` -- returns customer or undefined - `syncStripeCustomerDetails(user, options)` -- updates Stripe with user's name, email, address, locales, metadata ### Payment Methods ```typescript // The provider's stored methods, default marked: { id, type, brand, last4, expMonth, expYear, isDefault } const methods = await Payment.paymentMethods(user) // Detach one, by the provider's id await Payment.removePaymentMethod(user, 'pm_xxx') // Stripe only const pm = await Payment.addPaymentMethod(user, 'pm_xxx') const customer = await Payment.setDefaultPaymentMethod(user, 'pm_xxx') const intent = await Payment.createSetupIntent(user, { payment_method_types: ['card'] }) ``` `removePaymentMethod()` used to take the local `payment_methods` row id as well; `managePaymentMethod.deletePaymentMethod(user, id)` still does. `addPaymentMethod()` accepts a string (Stripe PM ID) or Stripe.PaymentMethod object. It attaches the PM to the customer if not already attached, then stores it in the `payment_methods` table with `type`, `last_four`, `brand`, `exp_year`, `exp_month`, `user_id`, `provider_id`. `setUserDefaultPayment()` accepts a Stripe PM ID string, clears existing `is_default` flags, sets the new default in the database, and updates `invoice_settings.default_payment_method` on the Stripe customer. `setDefaultPaymentMethod()` accepts a database record ID number. Additional methods on `managePaymentMethod`: - `updatePaymentMethod(user, pmId, params)` -- updates PM in Stripe - `listPaymentMethods(user)` -- queries `payment_methods` table by `user_id` - `retrievePaymentMethod(user, pmId)` -- by database ID - `retrieveDefaultPaymentMethod(user)` -- finds where `is_default: true` ### Invoices ```typescript const invoices = await Payment.getInvoices(user) // lists with expanded payment_intent.payment_method const invoice = await Payment.createInvoice('cus_xxx', { description: 'Custom invoice' }) const paid = await Payment.payInvoice('inv_xxx') ``` `getInvoices()` requires the user to have a `stripe_id` and expands `data.payment_intent.payment_method`. ### Products & Prices ```typescript // Create a product with its price const { product, price } = await Payment.createProduct('Pro Plan', 2999, { currency: 'usd', interval: 'month', description: 'Pro features' }) // Get a price by Stripe lookup_key const price = await Payment.getPrice('premium-monthly') // List active products const products = await Payment.listProducts({ limit: 10 }) ``` `createProduct()` creates the Stripe product, then creates a price linked to it. If `interval` is provided, the price gets `recurring: { interval }`. Additional methods on `manageProduct`: - `create(params)`, `retrieve(productId)`, `update(productId, params)` - `archive(productId)` -- sets `active: false` - `search(query, params)` -- Stripe product search - `list(params)` -- defaults to `active: true` Additional methods on `managePriceExtended`: - `create(params)`, `retrieve(priceId)`, `update(priceId, params)` - `list(params)` -- defaults to `active: true` - `listByProduct(productId, params)` -- prices for a specific product - `search(query, params)` -- Stripe price search - `archive(priceId)` -- sets `active: false` ### Coupons & Promo Codes ```typescript const coupon = await Payment.createCoupon({
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen