@stacksjs/defaults 0.75.20 → 0.75.22

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.
@@ -292,6 +292,9 @@ if (!result.isErr)
292
292
  console.log(formatSetupReport(result.value).join('\n'))
293
293
  ```
294
294
 
295
+ It also creates the coupons in `saas.coupons` with their promotion codes, and
296
+ the customer portal configuration in `saas.portal` (see Customer Portal).
297
+
295
298
  This iterates `saas.plans` and reconciles each one against the live account: a
296
299
  product is matched by name and reused, and each pricing option is matched by its
297
300
  `lookup_key` (from the `key` field). Prices are immutable in Stripe, so a changed
@@ -303,6 +306,36 @@ Re-running is safe and converges. It does not use `products.search` on purpose:
303
306
  that index is eventually consistent, so a second run inside the lag window would
304
307
  find nothing and create a duplicate.
305
308
 
309
+ ### Customer Portal
310
+
311
+ The Stripe-hosted page where a customer cancels or changes a subscription,
312
+ updates their card and downloads invoices.
313
+
314
+ ```typescript
315
+ // Customer id, or a user with a stripe_id. Single-use and short-lived:
316
+ // create it when the customer clicks, then redirect to session.url.
317
+ const session = await Payment.billingPortal('cus_xxx', { returnUrl: 'https://app.example/account' })
318
+ ```
319
+
320
+ Declare what the portal allows in `config/saas.ts`, and `buddy stripe:setup`
321
+ keeps one configuration in the account to match it:
322
+
323
+ ```typescript
324
+ portal: {
325
+ headline: 'Manage your plan',
326
+ returnUrl: 'https://app.example/account', // used when a session names none
327
+ cancel: 'at_period_end', // or 'immediately', or false
328
+ paymentMethodUpdate: true, // default true
329
+ invoiceHistory: true, // default true
330
+ emailUpdate: true, // default true
331
+ }
332
+ ```
333
+
334
+ The configuration is tagged `metadata: { managed_by: 'stacks', stacks_app: <APP_NAME> }`
335
+ and found by that tag, so several apps can share one Stripe account without
336
+ editing each other's portal. `billingPortal()` uses it, or the account default
337
+ when the app declares no portal.
338
+
306
339
  ### Utility Functions
307
340
 
308
341
  ```typescript
@@ -413,13 +446,31 @@ Plans use `productName`, `description`, `metadata`, and a `pricing` array where
413
446
  ],
414
447
  webhook: { endpoint: 'your-webhook-endpoint', secret: 'your-webhook-secret' },
415
448
  currencies: ['usd'],
416
- coupons: [],
449
+ coupons: [
450
+ {
451
+ id: 'six_months_free', // the Stripe coupon id; how a re-run finds it
452
+ name: '6 months free',
453
+ percentOff: 100, // or amountOff (minor units) + currency
454
+ duration: 'repeating', // 'once' | 'repeating' | 'forever'
455
+ durationInMonths: 6,
456
+ appliesTo: ['Stacks Hobby'], // plan productNames; omit for every product
457
+ codes: ['SIXFREE'], // what customers type: Stripe promotion codes
458
+ },
459
+ ],
417
460
  products: [
418
461
  { name: 'Stacks Hobby', description: '...', images: ['image-url'] },
419
462
  ],
420
463
  } satisfies SaasConfig
421
464
  ```
422
465
 
466
+ Coupons are immutable in Stripe. One that exists with a different discount is
467
+ reported as a conflict and left alone (deleting it would void codes already
468
+ handed out), and `stripe:setup` exits non-zero so CI notices. Change a
469
+ discount by giving it a new `id`. Stripe restricts a coupon by product, not
470
+ price, so a plan that must not get a discount needs a product of its own - or
471
+ apply the promotion code yourself at checkout (`discounts: [{ promotion_code }]`)
472
+ for the plans it is meant for.
473
+
423
474
  ## Database Tables Used
424
475
  - `subscriptions` -- columns: `user_id`, `type`, `unit_price`, `provider_id`, `provider_status`, `provider_price_id`, `quantity`, `trial_ends_at`, `ends_at`, `provider_type`, `last_used_at`
425
476
  - `payment_methods` -- columns: `id`, `type`, `last_four`, `brand`, `exp_year`, `exp_month`, `user_id`, `provider_id`, `is_default`
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.75.20",
5
+ "version": "0.75.22",
6
6
  "description": "A modern Stacks development environment.",
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.20",
5
+ "version": "0.75.22",
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.27",
58
- "@stacksjs/mobile": "^0.75.20",
58
+ "@stacksjs/mobile": "^0.75.22",
59
59
  "@stacksjs/sanitizer": "^0.2.113",
60
60
  "ts-qr-codes": "^0.1.8"
61
61
  }