@stacksjs/defaults 0.75.82 → 0.75.83

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -230,11 +230,12 @@ straight-line distance by a detour factor and returns `null` for a stationary
230
230
  courier rather than `Infinity`.
231
231
 
232
232
  ## Integration with Payments
233
- Commerce works with `@stacksjs/payments` for Stripe integration:
233
+ Commerce takes payments through `@stacksjs/payments`, on whichever provider `config.payment.driver` names:
234
234
  ```typescript
235
235
  import { Payment } from '@stacksjs/payments'
236
- await Payment.charge(customer, order.totalAmount, paymentMethodId)
236
+ await Payment.charge(customer, { amount: order.totalAmount, currency: 'usd' }, paymentMethodId, { reference: `order-${order.id}` })
237
237
  ```
238
+ The provider's webhook reaches orders through `POST /webhooks/payments` (see `stacks-payments`).
238
239
 
239
240
  ## Dashboard Routes
240
241
  All commerce models have dashboard views at `/dashboard/commerce/*`.
@@ -23,7 +23,7 @@ Full Stripe integration via the Payment facade. Uses Stripe API version `2026-01
23
23
  - The Stripe driver delegates to the existing billable modules, so idempotency keys and `stripe_id` handling are unchanged.
24
24
  - 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()`.
25
25
  - 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.
26
- - Everything below this section is Stripe's own API and returns Stripe objects.
26
+ - The Payment facade's neutral functions go through this driver; its Stripe-only functions and everything Stripe-level below return Stripe objects.
27
27
 
28
28
  ## Key Paths
29
29
  - Core package: `storage/framework/core/payments/src/`
@@ -44,87 +44,91 @@ Full Stripe integration via the Payment facade. Uses Stripe API version `2026-01
44
44
 
45
45
  ## Payment Facade
46
46
 
47
- The `Payment` object aggregates all payment operations. Every method is also available as a standalone export.
47
+ 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`):
48
+
49
+ - **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.
50
+ - **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.
51
+
52
+ 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.
48
53
 
49
54
  ### Charges
50
55
 
51
56
  ```typescript
52
57
  import { Payment } from '@stacksjs/payments'
53
58
 
54
- // Create and confirm a charge (creates PaymentIntent with confirm: true)
55
- const intent = await Payment.charge(user, 2999, 'pm_xxx', { currency: 'usd' })
59
+ // Charge a stored payment method, server side
60
+ const result = await Payment.charge(user, { amount: 2999, currency: 'usd' }, 'pm_xxx', { reference: 'order-1' })
56
61
 
57
- // Create PaymentIntent without confirming (for client-side confirmation)
58
- const intent = await Payment.createPayment(user, 2999, { currency: 'usd' })
62
+ // A payment the browser completes: result.clientConfirmation is a Stripe client secret or an Adyen session
63
+ const pending = await Payment.createPayment(user, { amount: 2999, currency: 'usd' })
59
64
 
60
- // Refund -- partial
61
- const refund = await Payment.refund('pi_xxx', 1000)
62
-
63
- // Refund -- full (omit amount)
64
- const refund = await Payment.refund('pi_xxx')
65
+ // Refund -- partial, or full when the amount is left out
66
+ const partial = await Payment.refund('pi_xxx', { amount: { amount: 1000, currency: 'usd' } })
67
+ const full = await Payment.refund('pi_xxx')
65
68
  ```
66
69
 
67
- `charge()` sets `confirmation_method: 'automatic'`, `confirm: true`, attaches the payment method, and delegates to `createPayment()`. If the user has a `stripe_id`, it is set as the customer on the PaymentIntent. Default currency is `'usd'`.
68
-
69
- The `manageCharge` module also exposes `findPayment(id)` which retrieves a PaymentIntent by ID, returning `null` on failure.
70
+ `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).
70
71
 
71
72
  ### Checkout Sessions
72
73
 
73
74
  ```typescript
74
- // One-time payment checkout
75
- const session = await Payment.checkout(user, [
76
- { price: 'price_xxx', quantity: 1 }
77
- ], { success_url: '/success', cancel_url: '/cancel' })
78
-
79
- // Subscription checkout
80
- const subSession = await Payment.subscriptionCheckout(user, 'price_xxx', {
81
- success_url: '/success', cancel_url: '/cancel'
75
+ const session = await Payment.checkout(user, {
76
+ mode: 'payment',
77
+ lines: [{ price: 'price_xxx', quantity: 1 }],
78
+ successUrl: 'https://app.example.com/success',
79
+ cancelUrl: 'https://app.example.com/cancel',
80
+ })
81
+
82
+ // One price in subscription mode
83
+ const upgrade = await Payment.subscriptionCheckout(user, 'price_xxx', {
84
+ successUrl: 'https://app.example.com/welcome',
85
+ cancelUrl: 'https://app.example.com/pricing',
86
+ metadata: { user_id: String(user.id) },
82
87
  })
83
88
  ```
84
89
 
85
- Both methods require the user to have a `stripe_id` (throws if missing). `checkout()` sets `mode: 'payment'`, `subscriptionCheckout()` sets `mode: 'subscription'`.
90
+ 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.
86
91
 
87
92
  ### Subscriptions
88
93
 
89
94
  ```typescript
90
- // Create -- uses lookup_key to resolve the Stripe Price
95
+ // Subscribe to a price (a Stripe lookup key); `type` names it locally, `default` unless given
91
96
  const sub = await Payment.subscribe(user, 'premium-monthly')
92
97
 
93
- // Cancel at period end (prorate: true)
94
- const cancelled = await Payment.cancelSubscription('sub_xxx')
98
+ // Cancel now
99
+ await Payment.cancelSubscription('sub_xxx')
95
100
 
96
- // Cancel immediately (invoice_now: true, prorate: false)
97
- const cancelled = await Payment.cancelSubscription('sub_xxx', true)
101
+ // Stop renewing: entitled until the period ends, recorded in the row's ends_at
102
+ await Payment.cancelSubscription('sub_xxx', { atPeriodEnd: true })
98
103
 
99
- // Check active subscription
104
+ // The provider's subscriptions, every status
105
+ const all = await Payment.subscriptions(user)
106
+
107
+ // Entitlement, from the local subscriptions table
100
108
  const hasActive = await Payment.hasActiveSubscription(user, 'default')
101
109
 
102
- // Change plan -- swaps the subscription item's price
110
+ // Stripe only: swap the subscription item's price
103
111
  const changed = await Payment.changeSubscription(user, 'enterprise-monthly')
104
112
  ```
105
113
 
106
- `subscribe()` calls `managePrice.retrieveByLookupKey(lookupKey)` to find the price, then creates the subscription with `payment_behavior: 'allow_incomplete'` and `expand: ['latest_invoice.payment_intent']`. It stores the subscription in the `subscriptions` database table.
114
+ `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.
107
115
 
108
116
  `isValid()` returns `true` if the subscription status is `'active'` or `'trialing'`. `isIncomplete()` checks for `'incomplete'` status.
109
117
 
110
- `cancel()` calls `stripe.subscriptions.cancel()` and updates `provider_status` to `'canceled'` in the database.
111
-
112
- `update()` retrieves the active subscription via `user.activeSubscription()`, finds the new price by lookup key, updates the subscription item, and stores the updated price in the database.
118
+ `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)`.
113
119
 
114
120
  ### Customers
115
121
 
116
122
  ```typescript
117
- // Get existing or create new Stripe customer
118
- const customer = await Payment.getOrCreateCustomer(user, { name: 'John' })
123
+ // The provider's customer, created on first use: { id, email, name }
124
+ const customer = await Payment.getOrCreateCustomer(user)
119
125
 
120
- // Update customer details in Stripe
126
+ // Stripe only
121
127
  const updated = await Payment.updateCustomer(user, { name: 'Jane' })
122
-
123
- // Delete from Stripe and clear stripe_id on user model
124
128
  const deleted = await Payment.deleteCustomer(user)
125
129
  ```
126
130
 
127
- `createOrGetStripeUser()` 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 })`.
131
+ `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)`.
128
132
 
129
133
  Additional methods on `manageCustomer`:
130
134
  - `stripeId(user)` -- returns `user.stripe_id`
@@ -137,19 +141,20 @@ Additional methods on `manageCustomer`:
137
141
  ### Payment Methods
138
142
 
139
143
  ```typescript
140
- // Add a payment method to the customer
141
- const pm = await Payment.addPaymentMethod(user, 'pm_xxx')
142
-
143
- // Set as default (by Stripe payment method ID string)
144
- const customer = await Payment.setDefaultPaymentMethod(user, 'pm_xxx')
144
+ // The provider's stored methods, default marked: { id, type, brand, last4, expMonth, expYear, isDefault }
145
+ const methods = await Payment.paymentMethods(user)
145
146
 
146
- // Remove a payment method (by database record ID number)
147
- const removed = await Payment.removePaymentMethod(user, paymentMethodDbId)
147
+ // Detach one, by the provider's id
148
+ await Payment.removePaymentMethod(user, 'pm_xxx')
148
149
 
149
- // Create a setup intent for collecting payment methods
150
+ // Stripe only
151
+ const pm = await Payment.addPaymentMethod(user, 'pm_xxx')
152
+ const customer = await Payment.setDefaultPaymentMethod(user, 'pm_xxx')
150
153
  const intent = await Payment.createSetupIntent(user, { payment_method_types: ['card'] })
151
154
  ```
152
155
 
156
+ `removePaymentMethod()` used to take the local `payment_methods` row id as well; `managePaymentMethod.deletePaymentMethod(user, id)` still does.
157
+
153
158
  `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`.
154
159
 
155
160
  `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.
@@ -549,9 +554,9 @@ The instance methods are exactly the keys of `createBillableMethods` in
549
554
  - Stripe API keys MUST be in `.env` as `STRIPE_SECRET_KEY` and `STRIPE_PUBLISHABLE_KEY` -- never hardcode them in config files
550
555
  - The Stripe client is created lazily, on first use: importing `@stacksjs/payments` without `STRIPE_SECRET_KEY` (or without the opt-in `stripe` package) is fine, and the first Stripe call throws naming what is missing
551
556
  - All amounts are in cents -- use `toCents()` and `toDollars()` for conversion
552
- - `charge()` creates AND confirms the PaymentIntent in one step
557
+ - `Payment.charge()` (Stripe driver) creates AND confirms the PaymentIntent in one step
553
558
  - `subscribe()` resolves the price via `lookup_key`, not a direct Stripe price ID
554
- - The facade's `Payment.removePaymentMethod()` takes a database record ID (number); the billable instance method `removePaymentMethod()` takes the provider's id (string)
559
+ - `Payment.removePaymentMethod()` and the billable instance method `removePaymentMethod()` both take the provider's id (string); `managePaymentMethod.deletePaymentMethod()` takes the local row id (number)
555
560
  - `setDefaultPaymentMethod` has two variants: one takes a Stripe PM ID string (`setUserDefaultPayment`), the other takes a database ID number
556
561
  - `getOrCreateCustomer()` handles deleted Stripe customers by recreating them
557
562
  - Subscription status checks query the local database, not Stripe directly
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.75.82",
5
+ "version": "0.75.83",
6
6
  "description": "Stacks, built into VS Code: stx templates, pickier linting and formatting, encrypted env files, the dev server preview and every buddy command. No other extensions required.",
7
7
  "license": "MIT",
8
8
  "funding": "https://github.com/sponsors/chrisbbreuer",
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/defaults",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.82",
5
+ "version": "0.75.83",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/stacksjs/stacks.git",
@@ -55,7 +55,7 @@
55
55
  "dependencies": {
56
56
  "@iconify-json/f7": "^1.2.2",
57
57
  "@iconify-json/hugeicons": "^1.2.35",
58
- "@stacksjs/mobile": "^0.75.82",
58
+ "@stacksjs/mobile": "^0.75.83",
59
59
  "@stacksjs/sanitizer": "^0.2.378",
60
60
  "@xterm/addon-fit": "^0.11.0",
61
61
  "@xterm/xterm": "^6.0.0",