@owlmeans/server-payment 0.1.18-rc.1 → 0.1.18-rc.11

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.
Files changed (148) hide show
  1. package/README.md +109 -25
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/server-payment/SKILL.md +263 -48
  4. package/build/actions/index.d.ts +1 -0
  5. package/build/actions/index.d.ts.map +1 -1
  6. package/build/actions/index.js +1 -0
  7. package/build/actions/index.js.map +1 -1
  8. package/build/actions/resync-subscriptions.d.ts +3 -0
  9. package/build/actions/resync-subscriptions.d.ts.map +1 -0
  10. package/build/actions/resync-subscriptions.js +7 -0
  11. package/build/actions/resync-subscriptions.js.map +1 -0
  12. package/build/actions/resync.d.ts +1 -0
  13. package/build/actions/resync.d.ts.map +1 -1
  14. package/build/actions/resync.js +7 -3
  15. package/build/actions/resync.js.map +1 -1
  16. package/build/actions/webhook.d.ts.map +1 -1
  17. package/build/actions/webhook.js +6 -3
  18. package/build/actions/webhook.js.map +1 -1
  19. package/build/config.d.ts +32 -5
  20. package/build/config.d.ts.map +1 -1
  21. package/build/config.js +102 -4
  22. package/build/config.js.map +1 -1
  23. package/build/consts.d.ts +38 -3
  24. package/build/consts.d.ts.map +1 -1
  25. package/build/consts.js +57 -5
  26. package/build/consts.js.map +1 -1
  27. package/build/entitlement.d.ts +10 -0
  28. package/build/entitlement.d.ts.map +1 -0
  29. package/build/entitlement.js +69 -0
  30. package/build/entitlement.js.map +1 -0
  31. package/build/entrypoints.d.ts +1 -1
  32. package/build/entrypoints.d.ts.map +1 -1
  33. package/build/entrypoints.js +2 -1
  34. package/build/entrypoints.js.map +1 -1
  35. package/build/gate.d.ts +32 -4
  36. package/build/gate.d.ts.map +1 -1
  37. package/build/gate.js +59 -19
  38. package/build/gate.js.map +1 -1
  39. package/build/index.d.ts +14 -3
  40. package/build/index.d.ts.map +1 -1
  41. package/build/index.js +12 -3
  42. package/build/index.js.map +1 -1
  43. package/build/limit.d.ts +13 -0
  44. package/build/limit.d.ts.map +1 -0
  45. package/build/limit.js +47 -0
  46. package/build/limit.js.map +1 -0
  47. package/build/model.d.ts +5 -1
  48. package/build/model.d.ts.map +1 -1
  49. package/build/model.js +84 -17
  50. package/build/model.js.map +1 -1
  51. package/build/observer.d.ts +5 -0
  52. package/build/observer.d.ts.map +1 -1
  53. package/build/observer.js +25 -12
  54. package/build/observer.js.map +1 -1
  55. package/build/plan.d.ts +22 -0
  56. package/build/plan.d.ts.map +1 -0
  57. package/build/plan.js +72 -0
  58. package/build/plan.js.map +1 -0
  59. package/build/plugins/events.d.ts +43 -10
  60. package/build/plugins/events.d.ts.map +1 -1
  61. package/build/plugins/events.js +477 -138
  62. package/build/plugins/events.js.map +1 -1
  63. package/build/plugins/portal.d.ts +37 -0
  64. package/build/plugins/portal.d.ts.map +1 -0
  65. package/build/plugins/portal.js +251 -0
  66. package/build/plugins/portal.js.map +1 -0
  67. package/build/plugins/refunds.d.ts +33 -0
  68. package/build/plugins/refunds.d.ts.map +1 -0
  69. package/build/plugins/refunds.js +80 -0
  70. package/build/plugins/refunds.js.map +1 -0
  71. package/build/plugins/stripe.d.ts +15 -3
  72. package/build/plugins/stripe.d.ts.map +1 -1
  73. package/build/plugins/stripe.js +96 -77
  74. package/build/plugins/stripe.js.map +1 -1
  75. package/build/plugins/webhook-manager.d.ts +46 -0
  76. package/build/plugins/webhook-manager.d.ts.map +1 -0
  77. package/build/plugins/webhook-manager.js +231 -0
  78. package/build/plugins/webhook-manager.js.map +1 -0
  79. package/build/reconcile.d.ts +15 -0
  80. package/build/reconcile.d.ts.map +1 -0
  81. package/build/reconcile.js +88 -0
  82. package/build/reconcile.js.map +1 -0
  83. package/build/resource.d.ts +5 -1
  84. package/build/resource.d.ts.map +1 -1
  85. package/build/resource.js +40 -6
  86. package/build/resource.js.map +1 -1
  87. package/build/service.d.ts +27 -3
  88. package/build/service.d.ts.map +1 -1
  89. package/build/service.js +117 -19
  90. package/build/service.js.map +1 -1
  91. package/build/subscription.d.ts +42 -0
  92. package/build/subscription.d.ts.map +1 -0
  93. package/build/subscription.js +175 -0
  94. package/build/subscription.js.map +1 -0
  95. package/build/sync.d.ts +16 -1
  96. package/build/sync.d.ts.map +1 -1
  97. package/build/sync.js +28 -10
  98. package/build/sync.js.map +1 -1
  99. package/build/types.d.ts +363 -36
  100. package/build/types.d.ts.map +1 -1
  101. package/build/usage.d.ts +63 -0
  102. package/build/usage.d.ts.map +1 -0
  103. package/build/usage.js +363 -0
  104. package/build/usage.js.map +1 -0
  105. package/build/utils.d.ts +29 -1
  106. package/build/utils.d.ts.map +1 -1
  107. package/build/utils.js +41 -7
  108. package/build/utils.js.map +1 -1
  109. package/package.json +16 -13
  110. package/src/actions/index.ts +1 -0
  111. package/src/actions/resync-subscriptions.ts +10 -0
  112. package/src/actions/resync.ts +6 -3
  113. package/src/actions/webhook.ts +5 -3
  114. package/src/config.ts +117 -7
  115. package/src/consts.ts +69 -6
  116. package/src/entitlement.ts +84 -0
  117. package/src/entrypoints.ts +2 -1
  118. package/src/gate.ts +88 -21
  119. package/src/index.ts +20 -3
  120. package/src/limit.ts +57 -0
  121. package/src/model.ts +94 -18
  122. package/src/observer.ts +31 -11
  123. package/src/plan.ts +89 -0
  124. package/src/plugins/events.ts +542 -123
  125. package/src/plugins/portal.ts +292 -0
  126. package/src/plugins/refunds.ts +108 -0
  127. package/src/plugins/stripe.ts +99 -75
  128. package/src/plugins/webhook-manager.ts +270 -0
  129. package/src/reconcile.ts +103 -0
  130. package/src/resource.ts +69 -7
  131. package/src/service.ts +146 -18
  132. package/src/subscription.ts +224 -0
  133. package/src/sync.ts +34 -9
  134. package/src/types.ts +426 -32
  135. package/src/usage.ts +453 -0
  136. package/src/utils.ts +74 -10
  137. package/tests/checkout.spec.ts +65 -83
  138. package/tests/context.ts +91 -0
  139. package/tests/entitlement.spec.ts +103 -0
  140. package/tests/events.spec.ts +343 -0
  141. package/tests/fake-stripe.ts +672 -0
  142. package/tests/gate.spec.ts +62 -72
  143. package/tests/limit-gate.spec.ts +68 -0
  144. package/tests/portal.spec.ts +183 -0
  145. package/tests/protocol.spec.ts +23 -6
  146. package/tests/usage.integration.spec.ts +101 -0
  147. package/tests/usage.spec.ts +171 -0
  148. package/tests/webhook-manager.spec.ts +152 -0
package/README.md CHANGED
@@ -1,42 +1,126 @@
1
1
  # @owlmeans/server-payment
2
2
 
3
- Public server-side payment primitives for OwlMeans applications. The package registers payment
4
- resources, the entitlement gate, protocol-bound webhook/resync entrypoints, and a Stripe gateway
5
- supporting amount-priced consumables, quantity-priced consumables, and subscriptions.
3
+ Server-side payments for OwlMeans applications: a Stripe gateway embedded in the backend
4
+ (amount and quantity checkout, subscriptions, the customer portal), a subscription store that
5
+ understands the whole Stripe lifecycle, a usage ledger for counted plan limits, the entitlement
6
+ service that resolves what an organization entity may do, and the two gate services that refuse a
7
+ request before its handler runs. The contracts — plans, limits, promos, the entitlement view, the
8
+ refusal errors — live in `@owlmeans/payment`.
6
9
 
7
- Applications declare products and plans during configuration, mount `paymentGateEntrypoints`, and
8
- register completion callbacks on the payment observer. Amount policies are validated at declaration
9
- time. Stripe Checkout is always authoritative: completion is idempotent by session id and an
10
- amount checkout is fulfilled only after its paid currency and subtotal match its signed-in metadata.
10
+ ## Declare the catalogue
11
11
 
12
12
  ```typescript
13
- import { CheckoutPricingMode, PlanDuration, ProductType } from '@owlmeans/payment'
13
+ import { CheckoutPricingMode, LimitKind, LimitWindow, PlanDuration, ProductType } from '@owlmeans/payment'
14
14
  import {
15
- appendPaymentGatewayService, declarePaymentPlan, declarePaymentProduct, observer,
15
+ declarePaymentPlan, declarePaymentProduct, portalBranding, stripeSecrets,
16
16
  } from '@owlmeans/server-payment'
17
17
 
18
- declarePaymentProduct(cfg, {
19
- sku: 'app-credits', type: ProductType.Consumable, services: ['app'], gateways: ['stripe'],
20
- name: 'Credits',
18
+ stripeSecrets(cfg, { api: '/secrets/stripe-key' }) // `webhook` is an optional override
19
+ portalBranding(cfg, { returnUrl: 'https://app.example.com/billing', headline: 'Example' })
20
+
21
+ declarePaymentProduct(cfg, { sku: 'app-plans', type: ProductType.Service, services: ['app'], name: 'Plans' })
22
+ declarePaymentPlan(cfg, {
23
+ productSku: 'app-plans', sku: 'free', duration: PlanDuration.Monthly, rank: 0, free: true, price: 0,
24
+ capabilities: [{ scope: 'feature', permissions: { basic: true } }],
25
+ limits: { seats: { kind: LimitKind.Occupancy, limit: 1 }, exports: { kind: LimitKind.Window, window: LimitWindow.Month, limit: 3 } },
21
26
  })
22
27
  declarePaymentPlan(cfg, {
23
- productSku: 'app-credits', sku: 'app-credit-unit', duration: PlanDuration.Consumable,
24
- price: 0.02, pricingMode: CheckoutPricingMode.Amount,
25
- amountPolicy: {
26
- currency: 'usd', minimumMinor: 500, maximumMinor: 50_000, defaultMinor: 1_000,
27
- presetsMinor: [1_000, 2_000, 5_000, 10_000], fixedMinor: 0, rateBps: 200,
28
- },
28
+ productSku: 'app-plans', sku: 'pro-monthly', duration: PlanDuration.Monthly, rank: 10, price: 20,
29
+ recurring: { interval: 'month' },
30
+ capabilities: [{ scope: 'feature', permissions: { basic: true, whitelabel: true } }],
31
+ limits: { seats: { kind: LimitKind.Occupancy, limit: 5 }, exports: { kind: LimitKind.Window, window: LimitWindow.Day, limit: 20 } },
29
32
  })
30
33
 
31
- appendPaymentGatewayService(context)
32
- observer(context).onTopUp(async completion => {
33
- if (completion.mode === 'amount') await credit(completion.entityId, completion.amountMinor)
34
+ declarePaymentProduct(cfg, { sku: 'app-credits', type: ProductType.Consumable, services: ['app'], name: 'Credits' })
35
+ declarePaymentPlan(cfg, {
36
+ productSku: 'app-credits', sku: 'app-credit-unit', duration: PlanDuration.Consumable, price: 0.02,
37
+ pricingMode: CheckoutPricingMode.Amount,
38
+ amountPolicy: { currency: 'usd', minimumMinor: 500, maximumMinor: 50_000, defaultMinor: 1_000,
39
+ presetsMinor: [1_000, 5_000], fixedMinor: 0, rateBps: 200 },
34
40
  })
35
41
  ```
36
42
 
37
- Mount `paymentGateEntrypoints` in the server entrypoint list. `gateway(context).createLink(...)`
38
- accepts the stable organization `entityId` only after the application has resolved it at its
39
- authenticated boundary; public protocol bodies use `entitySlug`.
43
+ `declarePaymentPlan` validates each declaration before recording it; `assertPlanDeclarations` checks
44
+ the catalogue as a whole (one free plan per rank, no two paid plans of a product at one rank) and runs
45
+ when the gateway initializes. A limit key may use a different kind on different plans — each kind keeps
46
+ its own counter window, so a lifetime count still sticks to the entity when it moves to a monthly plan.
47
+
48
+ ## Wire the services
49
+
50
+ ```typescript
51
+ import {
52
+ appendPaymentGatewayService, entitlements, gateway, observer, paymentGateEntrypoints,
53
+ } from '@owlmeans/server-payment'
54
+
55
+ appendPaymentGatewayService(context) // the process that owns Stripe
56
+ appendPaymentGatewayService(context, { manage: false }) // a process that only reads entitlements
57
+ export const serverBindings = [...paymentGateEntrypoints]
58
+
59
+ observer(context).onTopUp(async completion => { /* credit, idempotent by completion.externalId */ })
60
+ observer(context).onSubscription(async event => { /* idempotent by event.eventKey */ })
61
+ observer(context).onRefund(async event => { /* claw back, idempotent by event.eventKey */ })
62
+ observer(context).onDispute(async event => { /* … */ })
63
+ observer(context).onPaymentFailed(async event => { /* notify */ })
64
+ ```
65
+
66
+ A managed gateway brings Stripe to the declared state at boot — products and prices, one
67
+ customer-portal configuration, one webhook endpoint at this deployment's public URL (its signing
68
+ secret stored in `payment-webhook`) — and each step is fingerprinted, so an unchanged deployment
69
+ makes no Stripe call.
70
+
71
+ Several deployments of one service may share a Stripe account (typically every test-mode
72
+ deployment), each with its own database and URL — and a deployment's identity is its webhook URL,
73
+ even an undeliverable local one.
74
+
75
+ - **Webhook endpoint.** A deployment owns exactly the endpoint at its own URL: it deletes an
76
+ endpoint only when its own `payment-webhook` rows name it — the endpoint of a URL it has moved away
77
+ from — and never one it merely finds at another URL, whatever its metadata says. The endpoint of a
78
+ retired deployment is removed by hand in the Stripe dashboard; one deleted from outside comes back
79
+ on the next forced `resync`.
80
+ - **Portal configuration.** Each deployment has its own, tagged
81
+ `{ owlmeans: 'payment', service, deployment: <webhook URL> }`. It updates the configuration its
82
+ `portal:<service>` fingerprint row names, unless that one is tagged for another deployment, and
83
+ without a row adopts only a configuration carrying exactly its own tag. Stripe cannot delete a
84
+ portal configuration, so one a deployment can no longer identify stays in the account and a new
85
+ one is created.
86
+
87
+ ## Use it
88
+
89
+ ```typescript
90
+ // Checkout and the portal (in-process; resolve the stable entityId at your authenticated boundary)
91
+ const url = await gateway(ctx).createLink(ctx, { productSku: 'app-plans', planSku: 'pro-monthly', entityId, service: 'app', successUrl })
92
+ const portal = await gateway(ctx).portalLink(ctx, entityId, { flow: PortalFlow.Change, planSku: 'pro-monthly', returnUrl })
93
+ await gateway(ctx).grantInternalPlan(ctx, entityId, 'free') // when the organization is created
94
+
95
+ // What the entity may do
96
+ const view = await entitlements(ctx).entitlements(entityId) // the EntitlementView a UI renders
97
+ await entitlements(ctx).hasCapability(entityId, 'feature:whitelabel')
98
+
99
+ // Spend a counted allowance — key it by the record it pays for
100
+ await entitlements(ctx).consume({ entityId, limitKey: 'exports', eventKey: `export:${reportId}`, ref: reportId })
101
+ await entitlements(ctx).release({ entityId, limitKey: 'exports', eventKey: `export:${reportId}` })
102
+
103
+ // Periodic repair
104
+ await reconcileAll(ctx, { freePlanSku: 'free' })
105
+ await entitlements(ctx).reconcileOccupancy(entityId, 'seats', liveSeatCount)
106
+ ```
107
+
108
+ Routes declare what they need; the gates refuse before the handler:
109
+
110
+ ```typescript
111
+ protocol(route(...), contract(...), { gate: { alias: ENTITLEMENT_GATE, params: ['feature:whitelabel'] } })
112
+ protocol(route(...), contract(...), { gate: { alias: LIMIT_GATE, params: [formatLimitParam('seats')] } })
113
+ ```
114
+
115
+ `CapabilityRequired` and `LimitExhausted` extend `AuthForbidden`, so an HTTP boundary answers 403.
116
+ The limit gate never consumes; the handler does.
117
+
118
+ ## Routes
119
+
120
+ `paymentGateEntrypoints` binds the immutable `paymentGate` tree: the public Stripe webhook
121
+ (`POST /payment-gate/webhook/:paygate`, verified by signature over the raw body), and two
122
+ Ed25519-guarded repairs — `resync` (products, portal, webhook endpoint) and `resyncSubscriptions`
123
+ (re-reads every live Stripe subscription, answering `{ scanned, updated }`).
40
124
 
41
125
  <!-- owlmeans:agent-guidance:start -->
42
126
  ## Agent guidance
@@ -46,7 +130,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
46
130
  your project's skill store (`.agents/skills/`):
47
131
 
48
132
  ```sh
49
- npx @owlmeans/agent-skills@^0.1.18-rc.16
133
+ npx @owlmeans/agent-skills@^0.1.18-rc.28
50
134
  ```
51
135
 
52
136
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/server-payment",
4
- "version": "0.1.18-rc.1",
5
- "generatedAt": "2026-09-12T12:44:58.938Z",
4
+ "version": "0.1.18-rc.11",
5
+ "generatedAt": "2026-09-16T17:32:31.941Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -7,74 +7,289 @@ user-invocable: false
7
7
 
8
8
  # @owlmeans/server-payment
9
9
 
10
- Public MIT package in the common monorepo. It embeds Stripe into an application backend; the
11
- consumer owns products, credit conversion and entitlement side effects. It persists customers,
12
- subscriptions/fulfilled sessions and sync fingerprints in Mongo.
10
+ **Install:** `bun add @owlmeans/server-payment@^0.1.18-rc.11`
11
+
12
+ Public MIT package. It embeds Stripe into an application backend and owns everything between
13
+ Stripe and an entity's entitlements: the subscription store, one-time fulfillments, the usage
14
+ ledger of counted limits, plan resolution, the two gate services and Stripe's own configuration
15
+ (products, prices, the portal, the webhook endpoint). The application owns its catalogue, what a
16
+ purchase is worth to it (credits, provisioning) and its side effects. Contracts — plans, limits,
17
+ promos, the entitlement view, refusals — are `@owlmeans/payment`; the model across packages is
18
+ the `entitlements` skill.
13
19
 
14
20
  ## Wiring
15
21
 
16
22
  ```typescript
17
- stripeSecrets(cfg, { api: '/secrets/stripe-key', webhook: '/secrets/stripe-webhook' })
18
- declarePaymentProduct(cfg, { sku: 'credits', type: ProductType.Consumable,
19
- services: ['app'], name: 'Credits', taxCode: 'txcd_10103000' })
20
- declarePaymentPlan(cfg, { productSku: 'credits', sku: 'credits-unit',
21
- duration: PlanDuration.Consumable, price: 0.02,
22
- pricingMode: CheckoutPricingMode.Amount, amountPolicy })
23
-
24
- appendPaymentGatewayService(context)
25
- export const appEntrypoints = [...paymentGateEntrypoints]
26
- observer(context).onTopUp(async completion => { /* append an idempotent ledger event */ })
23
+ stripeSecrets(cfg, { api: '/secrets/stripe-key' }) // webhook secret: optional override
24
+ portalBranding(cfg, { returnUrl: 'https://app.example.com/billing', headline: 'Example' })
25
+ declarePaymentProduct(cfg, { sku: 'app-plans', type: ProductType.Service, services: ['app'], name: 'Plans' })
26
+ declarePaymentPlan(cfg, { productSku: 'app-plans', sku: 'free', rank: 0, free: true, price: 0, … })
27
+ declarePaymentPlan(cfg, { productSku: 'app-plans', sku: 'pro-monthly', rank: 10, price: 20,
28
+ recurring: { interval: 'month' }, capabilities: […], limits: { seats: {…} } })
29
+
30
+ appendPaymentGatewayService(context) // the process that talks to Stripe
31
+ appendPaymentGatewayService(context, { manage: false }) // a worker that only reads entitlements
32
+ export const serverBindings = [...paymentGateEntrypoints]
33
+ observer(context).onSubscription(async event => { /* keyed by event.eventKey */ })
27
34
  ```
28
35
 
29
- `declarePaymentPlan` validates amount and quantity policies immediately. An amount plan requires
30
- `amountPolicy`; a quantity/legacy plan may declare `quantityPolicy` or the legacy min/default/max
31
- fields. `gateway(ctx).createLink(...)` takes the stable internal `entityId`; an authenticated public
32
- handler must resolve that id from its request entity before calling the in-process service.
36
+ - `appendPaymentGatewayService` registers seven resources, the catalogue service
37
+ (`PAYMENT_SERVICE`), the completion observer, the gateway (`GATEWAY_SERVICE`), the capability
38
+ gate (`ENTITLEMENT_GATE`), the limit gate (`LIMIT_GATE`) and the entitlement service
39
+ (`ENTITLEMENT_SERVICE`), each only when not registered already.
40
+ - **`manage: false`** registers the same surface with no Stripe client: no bootstrap at init, and
41
+ `createLink`, `portalLink`, `resyncSubscription`, `resyncAll` and the webhook route throw
42
+ `PaygateError('unmanaged')`. `grantInternalPlan` and the whole entitlement service work, because
43
+ they are Mongo only.
44
+ - Gateway methods take the stable `entityId`. A public handler resolves it from its request
45
+ entity first; protocol bodies carry `entitySlug`.
46
+ - A value in `stripeSecrets` / `portalBranding` that starts with `/` is read from that file at
47
+ boot, and a missing file fails the boot — so leave `webhook` out unless the file exists.
48
+
49
+ ## Declaring plans
50
+
51
+ - **`rank`** orders a product's plans; a higher rank is an upgrade. A safe integer `>= 0`; absent
52
+ reads as `0`.
53
+ - **The free plan is a plan**: `free: true`, `price: 0`, no `gateways`. It is never synchronized to
54
+ Stripe and never checked out; an entity without an entitling subscription resolves to it.
55
+ - `gateways` names the paygates a plan is sold through; absent inherits the product's.
56
+ - `declarePaymentPlan` refuses before recording: a bad rank or a priced/gatewayed free plan
57
+ (`PlanRankConflict`), a capability set under the reserved `limit` scope, or a limit that is
58
+ malformed — a window limit without its window, a lifetime/occupancy limit with one, a ceiling that
59
+ is not a safe integer, a promo whose `until` is not a `Date` (`LimitMisdeclared('<key>:<reason>')`).
60
+ - `assertPlanDeclarations` runs when the gateway initializes and fails the boot on two free plans
61
+ at one rank, or two paid non-consumable plans of one product at one rank.
62
+ - A limit key may use a different kind on different plans (lifetime on the free plan, a monthly window
63
+ on a paid one). The counter window is derived from the kind, so each kind counts separately and a
64
+ lifetime count stays with the entity through upgrades, downgrades and cancellations.
65
+
66
+ ## Records
67
+
68
+ None declares an ObjectId reference: `entityId` is an organization key and every other id is Stripe's.
69
+
70
+ | Collection | One row per | Indexes |
71
+ |---|---|---|
72
+ | `payment-paygate-customer` | Stripe customer (`deletedAt` once deleted) | `{paygate, externalId}` unique · `{paygate, entityId}` · `{paygate, profileId}` |
73
+ | `payment-subscription` | subscription: `sub_…`, `free:<entityId>`, `internal:<planSku>:<entityId>` | `{paygate, externalId}` unique · `{entityId, status, rank:-1}` · `{entityId, planSku}` · `{paygate, customerId}` · `{paygate, itemId}` sparse · `{paygate, status, updatedAt}` |
74
+ | `payment-fulfillment` | one-time checkout session | `{paygate, externalId}` unique · `{paygate, paymentIntentId}` sparse · `{paygate, chargeId}` sparse · `{entityId, createdAt:-1}` |
75
+ | `payment-webhook` | managed webhook endpoint (`secret` is `secure: true`) | `{paygate, service, url}` unique |
76
+ | `payment-usage` | usage event — the ledger | `{entityId, limitKey, eventKey}` unique · `{entityId, limitKey, window}` · `{entityId, limitKey, ref}` sparse · `{entityId, createdAt:-1}` |
77
+ | `payment-usage-counter` | (entity, limit, window) projection | `{entityId, limitKey, window}` unique |
78
+ | `payment-fingerprint` | synchronized product (`<productSku>`) or portal (`portal:<service>`) | `{sku}` unique |
79
+
80
+ Every stored property is declared in the record schema: the resource writes a property its schema
81
+ does not know as a string, and the collection validator rejects it.
82
+
83
+ ## Checkout and fulfillment
84
+
85
+ - `Amount`: one inline `price_data` item for the synchronized product, quantity 1, tax-exclusive,
86
+ no promotion codes; the net `amountMinor` and the grossed-up `chargeAmountMinor` travel in the
87
+ session metadata. `Quantity`: the reusable price under the plan lookup key, adjustable quantity.
88
+ Subscription: `planSku` (else the product's first recurring plan), quantity 1.
89
+ - A plan the paygate does not sell — a free plan, another gateway's plan — is refused
90
+ (`ProductError`). Every session enables automatic tax, billing address and tax id collection.
91
+ - `checkout.session.completed` and `…async_payment_succeeded` fulfill only a `payment`-mode session
92
+ with `payment_status === 'paid'`; an amount session must match its metadata currency and subtotal.
93
+ - A pending `payment-fulfillment` row (with `paymentIntentId` and `invoiceId`) is written before
94
+ observers run; `fulfilledAt` is stamped after they succeed. An observer throw escapes so Stripe
95
+ redelivers, and the observer must stay idempotent by `externalId` — a crash after its side effect
96
+ and before the stamp redelivers the same session.
97
+
98
+ ## The subscription store
33
99
 
34
- ## Checkout modes
100
+ - **The effective plan** is the highest-ranked row in `ENTITLING_STATUSES` (catalogue rank; the
101
+ newest on a tie), else the declared free plan, else `PlanRequired`. An internal row with a past
102
+ `periodEnd` no longer entitles; a row naming a plan the catalogue lost is skipped (logged once).
103
+ - `mapStatus`: `active`→Active, `trialing`→Trial, `past_due`→PastDue (entitled, flagged),
104
+ `unpaid`/`paused`→Suspended, `incomplete`→Created, `incomplete_expired`→Ended, `canceled`→Canceled.
105
+ `pause_collection` on an active or trialing subscription is Suspended with `pausedAt`.
106
+ - **State is written before observers, and classified against what observers were last told.**
107
+ `propagated` holds the last propagated `{planSku, rank, status, cancelAtPeriodEnd, pausedAt,
108
+ renewedInvoiceId}`; it and `lastEventId` are stamped only after every observer succeeded. An
109
+ observer that throws therefore sees the same change again when Stripe retries, while an observer
110
+ that reads entitlements already sees the new plan.
111
+ - Classification, first match: `created` (the first propagated state that entitles — an
112
+ `incomplete` subscription reports nothing yet) · `canceled` · `paused` · `resumed` (pause cleared,
113
+ or suspended → entitling) · `upgraded`/`downgraded` by rank on a plan change (equal rank reads
114
+ `upgraded`) · `cancel-scheduled` · `cancel-undone` · `renewed` (a paid cycle invoice, once per
115
+ invoice) · `past-due` · `suspended` · `trial-ending` · otherwise nothing is reported.
116
+ - A repeated delivery (`lastEventId`) is ignored, and a webhook payload older than the stored state
117
+ (`event.created` before `syncedAt`) is not applied. State Stripe is asked for (invoice events,
118
+ resync) is always fresh.
119
+ - `grantInternalPlan(ctx, entityId, planSku, { force?, periodEnd? })` upserts an internal row,
120
+ keeps its `createdAt` (what promos are grandfathered against), and reports `created` once. A
121
+ plan that is not free needs `force`.
122
+ - `resyncSubscription(ctx, { entityId | subscriptionId })` retrieves and applies each subscription
123
+ exactly as a webhook would; one Stripe no longer has is canceled. `resyncAll` covers every
124
+ non-terminal Stripe row. Both answer how many rows changed.
35
125
 
36
- - `Amount`: validate `amountMinor`, compute `chargeAmountMinor`, create one inline
37
- `price_data` item for the synchronized Stripe Product, quantity 1, tax-exclusive, with no
38
- adjustable quantity and no promotion codes. No reusable Stripe Price is created; a superseded
39
- quantity Price under the plan lookup key is deactivated.
40
- - `Quantity`: load the reusable Stripe Price and create an adjustable item from the quantity
41
- policy. This remains supported so existing consumers and already-open Checkout sessions work.
42
- - Subscription: reusable recurring Stripe Price, quantity 1, with billing portal management.
126
+ ## The usage ledger
43
127
 
44
- Every session enables automatic tax, billing address and tax-id collection. Amount metadata carries
45
- pricing mode, net amount, adjusted pre-tax subtotal, currency, product, plan, service and owner.
128
+ `payment-usage` events are the source of truth; `payment-usage-counter` is the projection that
129
+ admission reads.
46
130
 
47
- ## Fulfillment
131
+ - **`consume({ entityId, limitKey, eventKey, amount?, ref?, reason? })`** — the counter is
132
+ incremented FIRST, by one conditional upsert that matches only while `used <= limit - amount`,
133
+ then the event is appended. No room ⇒ `LimitExhausted` with `used`, `limit`, `resetsAt`, and no
134
+ event. **Invariant: the counter may over-count, never over-admit.**
135
+ - **An event key is idempotent.** A key already written replays its outcome (`replayed: true`)
136
+ before admission is asked — at the ceiling too; a concurrent duplicate that loses the unique event
137
+ undoes its increment and replays the winner. A released key stays released.
138
+ - Key a consumption by the record it pays for (`eventKey: <purpose>:<recordId>`, `ref: recordId`).
139
+ `consumption(entityId, limitKey, eventKey)` answers whether that event still holds a unit;
140
+ `consumptionByRef(entityId, limitKey, ref)` answers the newest active event of a record — for a
141
+ unit acquired under a fresh key per cycle.
142
+ - **`release`** appends `release:<eventKey>` first, decrements only when that append was new (never
143
+ below zero), then stamps `releasedAt`. The default amount is the consumed amount.
144
+ - A lapsed promo makes the ceiling `0`; an undeclared key is `LimitUnknown`.
145
+ - **`reconcileCounters(entityId?)`** recomputes every counter from the ledger sum, deletes past
146
+ day/month counters with no events (never lifetime or occupancy), and creates the current window
147
+ counter of every window limit.
148
+ - **`reconcileOccupancy(entityId, key, actual)`** sets an occupancy counter to the live count and
149
+ keeps the ledger equal to it with one adjusting event per UTC day (`reconcile:<key>:<YYYY-MM-DD>`,
150
+ adjusted in place by a later run that day). `overSince` is set when over the limit and cleared
151
+ within it. It never stops anything — what happens to an entity over its limit is the
152
+ application's decision.
153
+ - `reconcileEntity(ctx, entityId, { freePlanSku?, resync? })` and `reconcileAll(ctx, { freePlanSku?,
154
+ resync?, entities? })` combine the optional Stripe resync, the free-plan backfill and the counter
155
+ repair; a failing entity is counted, never fatal.
48
156
 
49
- Handle both `checkout.session.completed` and `checkout.session.async_payment_succeeded`, but grant
50
- nothing until `payment_status === 'paid'`. Amount fulfillment compares Stripe currency and actual
51
- `amount_subtotal` with the server-created metadata. Completion is discriminated:
157
+ ## Entitlements and gates
52
158
 
53
- - `{ mode: 'amount', amountMinor, chargeAmountMinor, currency, ... }`
54
- - `{ mode: 'quantity', units, ... }`
159
+ - `entitlements(ctx)` → `effectivePlan`, `entitlements` (the `EntitlementView`, with
160
+ `plan.subscribedAt` = the subscription's `createdAt`), `hasCapability`, `limitState`, the ledger
161
+ operations above. It takes no context argument and never calls Stripe.
162
+ - **Capability gate** (`makeCapabilityGate(alias = ENTITLEMENT_GATE, { productSkus?, resolveEntity?,
163
+ requirePermission? })`, also `makeEntitlementGate`): authentication and an entity are required
164
+ (`AuthForbidden`); it passes when the view grants ANY parameter. The plan is the authority — a
165
+ token permission set to `false` denies, a token grant alone never allows, and `requirePermission`
166
+ (IAM `hasPermission`) is off by default because platform tokens carry no permissions. An
167
+ unreadable store refuses. Refusal: `CapabilityRequired(params)`.
168
+ - **Limit gate** (`makeLimitGate(alias = LIMIT_GATE, { resolveEntity? })`): passes when ANY
169
+ `limit:<key>[>=n]` has `remaining >= n`; malformed and undeclared parameters are skipped, a store
170
+ error refuses. Refusal: `LimitExhausted` for the first declared key. **It never consumes.**
171
+ - The two gates are two aliases because an entrypoint's gates are collected per gate service.
172
+ `entitlementsOf(ctx, entityId, productSkus?)` returns the capability sets in force.
55
173
 
56
- The session id is `externalId`. A pending fulfillment record is created before calling observers
57
- and gets `fulfilledAt` only after they succeed; observer failures escape so Stripe retries. Consumer
58
- callbacks must still use `externalId` as their own append-only event key, covering a crash after the
59
- side effect and before `fulfilledAt`. A session without pricing-mode metadata is a legacy quantity
60
- session and remains fulfillable.
174
+ ## Stripe self-management
61
175
 
62
- Subscription observers receive `{ isNew, status, capabilities, limits, ... }`. One-time bundles go
63
- behind `isNew` and use an idempotent external event key. Status updates and entitlement provisioning
64
- must be safe to repeat.
176
+ Runs in `initialize()` of a managed gateway, after the context is ready; each step is independent
177
+ and logged when it fails.
178
+
179
+ - **Products and prices** of plans sold through Stripe, fingerprinted per product. An amount plan
180
+ owns no reusable price.
181
+ - **The portal configuration** (`ensurePortalConfiguration`): customer update (email, address, tax
182
+ id), invoice history, payment method update, cancellation at period end without proration, and
183
+ price switching between every product's active recurring prices with prorations — both
184
+ subscription features off when nothing recurring is sold. `portalBranding` supplies the business
185
+ profile and default return URL; fingerprint `portal:<service>` holds its id, and its hash covers
186
+ the catalogue, the branding and the deployment key, so an unchanged declaration makes no call.
187
+ - **Each deployment owns its own portal configuration**, tagged
188
+ `{ owlmeans: 'payment', service, deployment: webhookUrlOf(ctx) }` (`STRIPE_DEPLOYMENT_KEY`) — the
189
+ webhook URL keys it even when undeliverable (local). The configuration the fingerprint row names
190
+ is retrieved and updated, unless its metadata tags it for another deployment or service, which is
191
+ never overwritten. Without a usable row, only an active configuration tagged with exactly this
192
+ service and deployment key is adopted (how a forced `resync`, which clears fingerprints, finds its
193
+ own again); a configuration carrying only the service label is not. Stripe cannot delete portal
194
+ configurations, so one a deployment can no longer identify stays behind and a new one is created.
195
+ - **`portalLink(ctx, entityId, { flow, planSku?, returnUrl })`**: a customer is required
196
+ (`PortalUnavailable('customer')`); `Manage` opens the home, `PaymentMethod` the payment form;
197
+ `Cancel`, `Update` and `Change` need an entitling Stripe subscription
198
+ (`PortalUnavailable('subscription')`), and `Change` confirms its stored item switching to
199
+ `planSku`'s price as exactly one item. Deep links return with `after_completion: redirect`.
200
+ `PortalUnavailable` declares 409, so a portal asked of an entity with nothing to manage answers
201
+ 409 Conflict, never 500.
202
+ - **The webhook endpoint** (`ensureWebhookEndpoint`): at `webhookUrlOf(ctx)` — this service's
203
+ public URL plus the webhook route — on the API version read back from the client
204
+ (`apiVersionOf`, the SDK default; never a literal), subscribed to `WEBHOOK_EVENTS`. A URL that is
205
+ not https on a public dotted host is skipped. An unchanged `{url, apiVersion, events}` hash makes
206
+ no call; `force` (the `resync` route) first verifies the stored endpoint exists and recreates one
207
+ deleted from outside; changed events update in place; a changed API version deletes and recreates
208
+ (the version is create-only); an endpoint already at this exact URL that no row names is replaced
209
+ (its secret is unknowable). The secret — returned only by create — is stored field-encrypted where
210
+ the database has a key.
211
+ - **A deployment is its webhook URL, and deletes only what it remembers.** Deployments of one
212
+ service may share a Stripe account, each with its own database and URL. A `payment-webhook` row of
213
+ the same paygate and service at another URL is a URL this deployment has left: after the current
214
+ endpoint is in place, the endpoint that row names is deleted (already gone is fine) and the row
215
+ removed. An endpoint at another URL that no row names belongs to another deployment and is never
216
+ deleted; the `{ owlmeans: 'payment', service }` metadata on created endpoints is an operator's
217
+ label, never deletion authority. A retired deployment's endpoint is removed by hand. The portal
218
+ configuration follows the same identity (above).
219
+ - **Signature verification** tries the configured `webhook` override, then the stored secret
220
+ (`stripeWebhookSecrets`); none configured is `WebhookSetupError('secret')`.
221
+ - **Do not bump the Stripe SDK major**: the pinned API version reads `current_period_*`,
222
+ `invoice.subscription` and `charge.invoice` at the top level, and they move in later versions.
223
+
224
+ ## Event dispatch
225
+
226
+ | Event | Persisted | Observer · event key |
227
+ |---|---|---|
228
+ | `customer.created` / `.updated` | customer upsert | — |
229
+ | `customer.deleted` | customer `deletedAt` | — |
230
+ | `checkout.session.completed` / `.async_payment_succeeded` | paid payment session ⇒ fulfillment, `fulfilledAt` after observers | `onTopUp` · `externalId` (session id) |
231
+ | `checkout.session.async_payment_failed` | fulfillment `failedAt` | `onPaymentFailed {kind:'checkout'}` · `payment-failed:<session>:0` |
232
+ | `checkout.session.expired` | unfulfilled fulfillment purged | — |
233
+ | `customer.subscription.created` / `.updated` / `.pending_update_applied` / `.pending_update_expired` / `.paused` / `.resumed` | subscription applied | `onSubscription` · classified |
234
+ | `customer.subscription.deleted` | applied as Canceled + `endedAt` | `canceled` |
235
+ | `customer.subscription.trial_will_end` | applied | `trial-ending` |
236
+ | `invoice.paid` | cycle invoice ⇒ subscription re-read, applied as a renewal; otherwise `latestInvoiceId` | `renewed` |
237
+ | `invoice.payment_failed` / `.payment_action_required` | subscription re-read and applied | classified (`past-due`), then `onPaymentFailed {kind:'invoice', attempt, nextAttemptAt, actionRequired}` · `payment-failed:<invoice>:<attempt>` |
238
+ | `invoice.upcoming` | nothing (enabled for the application's own use) | — |
239
+ | `invoice.marked_uncollectible` | subscription re-read and applied | classified (`suspended`) |
240
+ | `invoice.voided` | `latestInvoiceId` refreshed | — |
241
+ | `charge.refunded` (each refund of the charge), `refund.created` / `.updated` (succeeded only) | fulfillment `refundedMinor` / `refundedAt` | `onRefund` · `refund:<refund>` |
242
+ | `refund.failed` | — | — |
243
+ | `charge.dispute.created` / `.funds_withdrawn` / `.funds_reinstated` / `.closed` | target `disputedAt`, `disputeStatus` | `onDispute {phase}` · `dispute:<dispute>:<phase>` |
244
+
245
+ A refund or dispute resolves to its target by payment intent (fulfillment), by charge (stored
246
+ charge, the charge's payment intent, else its invoice), by invoice (the subscription whose latest
247
+ invoice it is, else the invoice's subscription). Nothing resolved ⇒ logged, no observer.
248
+
249
+ ## Observer API and idempotency keys
250
+
251
+ Callbacks run sequentially and are awaited; a throw escapes so Stripe redelivers. Every callback
252
+ must be idempotent by its key.
253
+
254
+ | Callback | Payload | Key |
255
+ |---|---|---|
256
+ | `onTopUp` | `TopUpCompletion` (`amount` / `quantity`) | `externalId` — the session id |
257
+ | `onSubscription` | `SubscriptionEvent {change, previous, current, active, eventKey, invoiceId?, externalEventId?}`; snapshots carry `rank`, `status`, period, `capabilities`, `limits` | `subscription:<id>:created:<createdAt ISO>` · `…:renewed:<invoice>` · `…:upgraded|downgraded:<planSku>` · `…:trial-ending:<trialEnd ISO>` · `…:<change>:<event id>` (`sync-<ISO>` from a resync) |
258
+ | `onRefund` | `RefundEvent {target, amountMinor, refundedTotalMinor, paidMinor?, partial, netAmountMinor?, chargeAmountMinor?, …}` | `refund:<refund>` |
259
+ | `onDispute` | `DisputeEvent {phase, status, amountMinor, …}` | `dispute:<dispute>:<phase>` |
260
+ | `onPaymentFailed` | `PaymentFailedEvent {kind, attempt?, nextAttemptAt?, actionRequired?, …}` | `payment-failed:<session|invoice>:<attempt>` |
261
+
262
+ A proportional claw-back of a top-up uses the net credited value against what was paid:
263
+ `netAmountMinor * refundedTotalMinor / paidMinor`.
65
264
 
66
265
  ## Protocols and security
67
266
 
68
- `paymentGate` is an immutable protocol tree. `paymentGateEntrypoints` binds its base, webhook, and
69
- resync declarations directly with `bind(protocol, handler)`; the webhook is public because Stripe
70
- signature verification requires the untouched raw body, while resync carries the ED25519 guard.
71
- Never put an application auth guard on the Stripe webhook.
267
+ `paymentGate` is an immutable protocol tree bound by `paymentGateEntrypoints` with
268
+ `bind(protocol, handler)`: `webhook` (`POST /payment-gate/webhook/:paygate`) is public because
269
+ Stripe signs the untouched raw body — never put an application guard on it; `resync` (products,
270
+ portal, webhook endpoint, fingerprints ignored) and `resyncSubscriptions` (`{ scanned, updated }`)
271
+ carry `GUARD_ED25519`, so another service of the deployment triggers them with its own key.
272
+
273
+ ## Testing
72
274
 
73
- `appendPaymentGatewayService` also registers `ENTITLEMENT_GATE`. Paid capabilities belong on shared
74
- route declarations through `entitled(...)`, not in handlers. The gate fails closed when the
75
- subscription store cannot be read.
275
+ Unit specs run a real server context — real catalogue, services and gates — over in-memory
276
+ resources and a fake Stripe that records every SDK call, so "no Stripe call" is an assertion. The
277
+ Mongo-gated spec proves admission under concurrency and the collection validators against a real
278
+ database.
76
279
 
77
280
  ## External docs
78
281
 
79
282
  - https://docs.stripe.com/api/checkout/sessions/create — Checkout accepts inline `price_data` with integer minor-unit `unit_amount`; automatic tax is enabled on the Session and amount items are tax-exclusive.
80
283
  - https://docs.stripe.com/checkout/fulfillment — Fulfillment must be idempotent, check payment state and support delayed-payment success events rather than trusting completion alone.
284
+ - https://docs.stripe.com/api/webhook_endpoints/create — the signing `secret` is returned only by create; update accepts `enabled_events`, `disabled`, `url`, `description`, `metadata`; `api_version` is create-only.
285
+ - https://docs.stripe.com/api/events/types — the event names `WEBHOOK_EVENTS` subscribes to.
286
+ - https://docs.stripe.com/api/subscriptions/object — statuses `incomplete|incomplete_expired|trialing|active|past_due|canceled|unpaid|paused`; `pause_collection` pauses collection without changing the status; `paused` only after a trial without a payment method; on `2025-02-24.acacia` (stripe-node 17) `current_period_start/end` and `invoice.subscription` are top-level and move in later versions.
287
+ - https://docs.stripe.com/customer-management/portal-deep-links and https://docs.stripe.com/api/customer_portal/sessions/create — `flow_data.type` ∈ `payment_method_update|subscription_cancel|subscription_update|subscription_update_confirm`; `subscription_update_confirm.items` holds exactly one `{ id: <subscription item id>, price, quantity }`; `after_completion` is `redirect|hosted_confirmation|portal_homepage`; the configuration must enable `subscription_update` (with `products[{product, prices[]}]`) and `subscription_cancel`.
288
+ - https://docs.stripe.com/api/customer_portal/configurations/create — `features.{customer_update, invoice_history, payment_method_update, subscription_cancel{enabled, mode, proration_behavior}, subscription_update{enabled, default_allowed_updates, products, proration_behavior}}`, `business_profile`, `default_return_url`, `metadata`; updatable by id, retrievable and listable (`active`, paginated), and never deletable — a configuration can only be deactivated.
289
+
290
+ ## Related
291
+
292
+ - [[entitlements]] — the model across packages
293
+ - [[payment]] — the contracts: grammars, window algebra, views, refusals
294
+ - [[web-payment]] — hooks and pieces over the entitlement view
295
+ - [[mongo-resource]] — raw collection access, duplicate-key detection, field locking
@@ -1,3 +1,4 @@
1
1
  export * from './webhook.js';
2
2
  export * from './resync.js';
3
+ export * from './resync-subscriptions.js';
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/actions/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/actions/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,2BAA2B,CAAA"}
@@ -1,3 +1,4 @@
1
1
  export * from './webhook.js';
2
2
  export * from './resync.js';
3
+ export * from './resync-subscriptions.js';
3
4
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/actions/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/actions/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,2BAA2B,CAAA"}
@@ -0,0 +1,3 @@
1
+ /** Re-read every live paygate subscription — the manually or periodically triggered repair. */
2
+ export declare const resyncSubscriptions: import("@owlmeans/server-entrypoint").BoundEntrypointHandler<import("@owlmeans/entrypoint").EntrypointProtocol<{}, import("../consts.js").ResyncSubscriptionsResult>>;
3
+ //# sourceMappingURL=resync-subscriptions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resync-subscriptions.d.ts","sourceRoot":"","sources":["../../src/actions/resync-subscriptions.ts"],"names":[],"mappings":"AAOA,+FAA+F;AAC/F,eAAO,MAAM,mBAAmB,uKACY,CAAA"}
@@ -0,0 +1,7 @@
1
+ import { handlers } from '@owlmeans/server-api';
2
+ import { paymentGate } from '../consts.js';
3
+ import { gateway } from '../utils.js';
4
+ const bind = handlers();
5
+ /** Re-read every live paygate subscription — the manually or periodically triggered repair. */
6
+ export const resyncSubscriptions = bind.request(paymentGate.resyncSubscriptions, async (_request, context) => await gateway(context).resyncAll(context));
7
+ //# sourceMappingURL=resync-subscriptions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resync-subscriptions.js","sourceRoot":"","sources":["../../src/actions/resync-subscriptions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAGrC,MAAM,IAAI,GAAG,QAAQ,EAAW,CAAA;AAEhC,+FAA+F;AAC/F,MAAM,CAAC,MAAM,mBAAmB,GAAG,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,mBAAmB,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,CAC3G,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAA"}
@@ -1,2 +1,3 @@
1
+ /** Forget every sync fingerprint and bring Stripe back to the declared catalogue, portal and webhook. */
1
2
  export declare const resync: import("@owlmeans/server-entrypoint").BoundEntrypointHandler<import("@owlmeans/entrypoint").EntrypointProtocol<{}, import("../consts.js").ResyncResult>>;
2
3
  //# sourceMappingURL=resync.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"resync.d.ts","sourceRoot":"","sources":["../../src/actions/resync.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,MAAM,0JAIjB,CAAA"}
1
+ {"version":3,"file":"resync.d.ts","sourceRoot":"","sources":["../../src/actions/resync.ts"],"names":[],"mappings":"AASA,yGAAyG;AACzG,eAAO,MAAM,MAAM,0JAKjB,CAAA"}
@@ -1,11 +1,15 @@
1
+ import { PaygateError } from '@owlmeans/payment';
1
2
  import { handlers } from '@owlmeans/server-api';
2
3
  import { paymentGate } from '../consts.js';
3
- import { fingerprints } from '../utils.js';
4
- import { initialize } from '../sync.js';
4
+ import { bootstrapStripe } from '../service.js';
5
+ import { fingerprints, gateway, stripeClient } from '../utils.js';
5
6
  const bind = handlers();
7
+ /** Forget every sync fingerprint and bring Stripe back to the declared catalogue, portal and webhook. */
6
8
  export const resync = bind.request(paymentGate.resync, async (_request, context) => {
9
+ if (!gateway(context).managed)
10
+ throw new PaygateError('unmanaged');
7
11
  await fingerprints(context).clear();
8
- await initialize(context);
12
+ await bootstrapStripe(context, await stripeClient(context), { force: true });
9
13
  return { ok: true };
10
14
  });
11
15
  //# sourceMappingURL=resync.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"resync.js","sourceRoot":"","sources":["../../src/actions/resync.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAGvC,MAAM,IAAI,GAAG,QAAQ,EAAW,CAAA;AAEhC,MAAM,CAAC,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE;IACjF,MAAM,YAAY,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,CAAA;IACnC,MAAM,UAAU,CAAC,OAAO,CAAC,CAAA;IACzB,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAA;AACrB,CAAC,CAAC,CAAA"}
1
+ {"version":3,"file":"resync.js","sourceRoot":"","sources":["../../src/actions/resync.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAC/C,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAGjE,MAAM,IAAI,GAAG,QAAQ,EAAW,CAAA;AAEhC,yGAAyG;AACzG,MAAM,CAAC,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE;IACjF,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,OAAO;QAAE,MAAM,IAAI,YAAY,CAAC,WAAW,CAAC,CAAA;IAClE,MAAM,YAAY,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,CAAA;IACnC,MAAM,eAAe,CAAC,OAAO,EAAE,MAAM,YAAY,CAAC,OAAO,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;IAC5E,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAA;AACrB,CAAC,CAAC,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../src/actions/webhook.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,OAAO;;cAGlB,CAAA"}
1
+ {"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../src/actions/webhook.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,OAAO;;cAIlB,CAAA"}