@volter/twin-stripe 3.0.0 → 3.0.1

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 (253) hide show
  1. package/README.md +51 -21
  2. package/dist/generated/pack-facts.json +3 -3
  3. package/dist/journeys/coverage.json +596 -0
  4. package/dist/journeys/customer-life.json +6108 -0
  5. package/dist/journeys/customer-life.outline.md +371 -0
  6. package/dist/journeys/decisions.json +677 -0
  7. package/dist/journeys/demand.json +706 -0
  8. package/dist/journeys/score.json +853 -0
  9. package/dist/journeys/unreachable.json +102 -0
  10. package/dist/journeys/vendor-examples.json +8010 -0
  11. package/dist/spec/SOURCE.md +5 -1
  12. package/dist/spec/doc-examples.json +445 -0
  13. package/dist/spec/sources.json +135 -207
  14. package/dist/src/cli.d.ts +0 -1
  15. package/dist/src/cli.js +0 -2
  16. package/dist/src/engine/after-payment.d.ts +0 -1
  17. package/dist/src/engine/after-payment.js +0 -1
  18. package/dist/src/engine/common.d.ts +5 -5
  19. package/dist/src/engine/common.js +13 -11
  20. package/dist/src/engine/connect.d.ts +0 -21
  21. package/dist/src/engine/connect.js +1 -82
  22. package/dist/src/engine/customers.d.ts +0 -8
  23. package/dist/src/engine/customers.js +0 -22
  24. package/dist/src/engine/display.d.ts +0 -3
  25. package/dist/src/engine/display.js +0 -3
  26. package/dist/src/engine/files.d.ts +0 -7
  27. package/dist/src/engine/files.js +0 -10
  28. package/dist/src/engine/invoices.d.ts +6 -9
  29. package/dist/src/engine/invoices.js +56 -29
  30. package/dist/src/engine/issuing.d.ts +6 -8
  31. package/dist/src/engine/issuing.js +19 -10
  32. package/dist/src/engine/ledger.d.ts +0 -1
  33. package/dist/src/engine/ledger.js +0 -1
  34. package/dist/src/engine/oauth.js +1 -2
  35. package/dist/src/engine/refunds.js +2 -27
  36. package/dist/src/engine/stripe.d.ts +2 -7
  37. package/dist/src/engine/stripe.js +34 -83
  38. package/dist/src/engine/subscriptions.d.ts +0 -1
  39. package/dist/src/engine/subscriptions.js +0 -1
  40. package/dist/src/engine/version.d.ts +0 -2
  41. package/dist/src/engine/version.js +17 -6
  42. package/dist/src/fetch.js +2 -2
  43. package/dist/src/generated/surface.gen.json +1 -1
  44. package/dist/src/generated/ui.gen.json +1 -1
  45. package/dist/src/index.js +5 -3
  46. package/dist/src/manifest.js +348 -220
  47. package/dist/src/screens/billing-portal.js +5 -5
  48. package/dist/src/screens/{connect-oauth.d.ts → checkout-js.d.ts} +0 -1
  49. package/dist/src/screens/checkout-js.js +84 -0
  50. package/dist/src/screens/connect-onboarding.js +3 -3
  51. package/dist/src/screens/dashboard.d.ts +1 -1
  52. package/dist/src/screens/dashboard.js +52 -53
  53. package/dist/src/screens/stripe-js.js +53 -93
  54. package/dist/src/semantics/account.d.ts +2 -0
  55. package/dist/src/semantics/account.js +8 -0
  56. package/dist/src/semantics/accounts.js +3 -1
  57. package/dist/src/semantics/around.js +3 -6
  58. package/dist/src/semantics/billing_portal.d.ts +0 -1
  59. package/dist/src/semantics/billing_portal.js +1 -7
  60. package/dist/src/semantics/checkout.d.ts +1 -1
  61. package/dist/src/semantics/checkout.js +18 -15
  62. package/dist/src/semantics/clock.js +1 -7
  63. package/dist/src/semantics/customers.d.ts +1 -2
  64. package/dist/src/semantics/customers.js +32 -38
  65. package/dist/src/semantics/entitlements.d.ts +0 -2
  66. package/dist/src/semantics/entitlements.js +4 -50
  67. package/dist/src/semantics/events.d.ts +2 -3
  68. package/dist/src/semantics/events.js +6 -11
  69. package/dist/src/semantics/index.js +16 -38
  70. package/dist/src/semantics/invoices.d.ts +3 -8
  71. package/dist/src/semantics/invoices.js +134 -127
  72. package/dist/src/semantics/issuing.d.ts +6 -0
  73. package/dist/src/semantics/issuing.js +77 -0
  74. package/dist/src/semantics/payment_intents.d.ts +0 -1
  75. package/dist/src/semantics/payment_intents.js +5 -13
  76. package/dist/src/semantics/payment_methods.d.ts +0 -2
  77. package/dist/src/semantics/payment_methods.js +2 -26
  78. package/dist/src/semantics/payouts.d.ts +1 -0
  79. package/dist/src/semantics/payouts.js +8 -1
  80. package/dist/src/semantics/products.d.ts +1 -3
  81. package/dist/src/semantics/products.js +3 -14
  82. package/dist/src/semantics/refunds.d.ts +1 -2
  83. package/dist/src/semantics/refunds.js +1 -5
  84. package/dist/src/semantics/shared.d.ts +64 -336
  85. package/dist/src/semantics/shared.js +326 -1336
  86. package/dist/src/semantics/states.d.ts +20 -89
  87. package/dist/src/semantics/states.js +116 -334
  88. package/dist/src/semantics/subscriptions.d.ts +1 -2
  89. package/dist/src/semantics/subscriptions.js +23 -145
  90. package/dist/src/semantics/test_helpers.d.ts +2 -5
  91. package/dist/src/semantics/test_helpers.js +121 -43
  92. package/dist/src/semantics/webhook_endpoints.d.ts +2 -0
  93. package/dist/src/semantics/webhook_endpoints.js +26 -0
  94. package/generated/pack-facts.json +3 -3
  95. package/journeys/coverage.json +596 -0
  96. package/journeys/customer-life.json +6108 -0
  97. package/journeys/customer-life.outline.md +371 -0
  98. package/journeys/decisions.json +677 -0
  99. package/journeys/demand.json +706 -0
  100. package/journeys/score.json +853 -0
  101. package/journeys/unreachable.json +102 -0
  102. package/journeys/vendor-examples.json +8010 -0
  103. package/package.json +4 -3
  104. package/spec/SOURCE.md +5 -1
  105. package/spec/doc-examples.json +445 -0
  106. package/spec/sources.json +135 -207
  107. package/src/cli.ts +0 -3
  108. package/src/engine/after-payment.ts +0 -2
  109. package/src/engine/balance.ts +0 -1
  110. package/src/engine/checkout.ts +0 -1
  111. package/src/engine/common.ts +19 -17
  112. package/src/engine/connect.ts +1 -79
  113. package/src/engine/customers.ts +0 -25
  114. package/src/engine/display.ts +2 -3
  115. package/src/engine/files.ts +0 -13
  116. package/src/engine/invoices.ts +64 -33
  117. package/src/engine/issuing.ts +16 -11
  118. package/src/engine/ledger.ts +0 -2
  119. package/src/engine/oauth.ts +1 -2
  120. package/src/engine/onboarding.ts +0 -1
  121. package/src/engine/params.ts +0 -1
  122. package/src/engine/payment-intents.ts +0 -1
  123. package/src/engine/payment-methods.ts +0 -1
  124. package/src/engine/refunds.ts +2 -28
  125. package/src/engine/renewals.ts +0 -1
  126. package/src/engine/stripe.ts +35 -83
  127. package/src/engine/subscriptions.ts +0 -3
  128. package/src/engine/transfers.ts +0 -1
  129. package/src/engine/version.ts +15 -7
  130. package/src/engine/webhooks.ts +0 -2
  131. package/src/fetch.ts +2 -2
  132. package/src/generated/surface.gen.json +1 -1
  133. package/src/generated/ui.gen.json +1 -1
  134. package/src/index.ts +6 -4
  135. package/src/manifest.ts +348 -220
  136. package/src/screens/billing-portal.tsx +5 -7
  137. package/src/screens/checkout-js.ts +80 -0
  138. package/src/screens/checkout.tsx +0 -2
  139. package/src/screens/connect-onboarding.tsx +3 -5
  140. package/src/screens/dashboard.tsx +53 -73
  141. package/src/screens/stripe-js.ts +57 -125
  142. package/src/semantics/account.ts +10 -0
  143. package/src/semantics/account_links.ts +0 -1
  144. package/src/semantics/accounts.ts +8 -7
  145. package/src/semantics/apps.ts +1 -2
  146. package/src/semantics/around.ts +3 -6
  147. package/src/semantics/balance.ts +0 -1
  148. package/src/semantics/balance_transactions.ts +1 -3
  149. package/src/semantics/billing_portal.ts +1 -7
  150. package/src/semantics/charges.ts +1 -6
  151. package/src/semantics/checkout.ts +16 -12
  152. package/src/semantics/clock.ts +1 -8
  153. package/src/semantics/coupons.ts +0 -1
  154. package/src/semantics/customers.ts +32 -35
  155. package/src/semantics/doors.ts +2 -3
  156. package/src/semantics/entitlements.ts +1 -45
  157. package/src/semantics/events.ts +7 -14
  158. package/src/semantics/index.ts +16 -38
  159. package/src/semantics/invoice_payments.ts +1 -3
  160. package/src/semantics/invoices.ts +124 -119
  161. package/src/semantics/issuing.ts +74 -0
  162. package/src/semantics/payment_intents.ts +10 -17
  163. package/src/semantics/payment_methods.ts +2 -25
  164. package/src/semantics/payouts.ts +9 -3
  165. package/src/semantics/prices.ts +0 -1
  166. package/src/semantics/products.ts +4 -14
  167. package/src/semantics/promotion_codes.ts +0 -1
  168. package/src/semantics/radar.ts +1 -3
  169. package/src/semantics/refunds.ts +2 -11
  170. package/src/semantics/shared.ts +350 -1382
  171. package/src/semantics/states.ts +109 -330
  172. package/src/semantics/subscriptions.ts +25 -145
  173. package/src/semantics/test_helpers.ts +109 -43
  174. package/src/semantics/transfers.ts +1 -3
  175. package/src/semantics/webhook_endpoints.ts +27 -0
  176. package/src/server.ts +0 -1
  177. package/dist/src/engine/billing.d.ts +0 -5
  178. package/dist/src/engine/billing.js +0 -6
  179. package/dist/src/engine/charges.d.ts +0 -6
  180. package/dist/src/engine/charges.js +0 -13
  181. package/dist/src/engine/connect-settings.d.ts +0 -16
  182. package/dist/src/engine/connect-settings.js +0 -26
  183. package/dist/src/engine/coupons.d.ts +0 -0
  184. package/dist/src/engine/coupons.js +0 -5
  185. package/dist/src/engine/credit-notes.d.ts +0 -1
  186. package/dist/src/engine/credit-notes.js +0 -5
  187. package/dist/src/engine/disputes.d.ts +0 -0
  188. package/dist/src/engine/disputes.js +0 -9
  189. package/dist/src/engine/entitlements.d.ts +0 -1
  190. package/dist/src/engine/entitlements.js +0 -4
  191. package/dist/src/engine/ephemeral-keys.d.ts +0 -0
  192. package/dist/src/engine/ephemeral-keys.js +0 -4
  193. package/dist/src/engine/front.d.ts +0 -5
  194. package/dist/src/engine/front.js +0 -25
  195. package/dist/src/engine/payment-links.d.ts +0 -1
  196. package/dist/src/engine/payment-links.js +0 -5
  197. package/dist/src/engine/plans.d.ts +0 -6
  198. package/dist/src/engine/plans.js +0 -26
  199. package/dist/src/engine/platform.d.ts +0 -39
  200. package/dist/src/engine/platform.js +0 -34
  201. package/dist/src/engine/products.d.ts +0 -0
  202. package/dist/src/engine/products.js +0 -4
  203. package/dist/src/engine/setup-intents.d.ts +0 -1
  204. package/dist/src/engine/setup-intents.js +0 -4
  205. package/dist/src/engine/subscription-schedules.d.ts +0 -3
  206. package/dist/src/engine/subscription-schedules.js +0 -1
  207. package/dist/src/engine/tax.d.ts +0 -7
  208. package/dist/src/engine/tax.js +0 -28
  209. package/dist/src/engine/terminal.d.ts +0 -4
  210. package/dist/src/engine/terminal.js +0 -20
  211. package/dist/src/engine/test-clocks.d.ts +0 -5
  212. package/dist/src/engine/test-clocks.js +0 -9
  213. package/dist/src/engine/tokens.d.ts +0 -7
  214. package/dist/src/engine/tokens.js +0 -20
  215. package/dist/src/engine/treasury.d.ts +0 -23
  216. package/dist/src/engine/treasury.js +0 -45
  217. package/dist/src/screens/connect-oauth.js +0 -333
  218. package/dist/src/semantics/disputes.d.ts +0 -2
  219. package/dist/src/semantics/disputes.js +0 -5
  220. package/dist/src/semantics/invoiceitems.d.ts +0 -3
  221. package/dist/src/semantics/invoiceitems.js +0 -59
  222. package/dist/src/semantics/setup_intents.d.ts +0 -3
  223. package/dist/src/semantics/setup_intents.js +0 -41
  224. package/dist/src/semantics/subscription_items.d.ts +0 -5
  225. package/dist/src/semantics/subscription_items.js +0 -55
  226. package/dist/src/semantics/subscription_schedules.d.ts +0 -4
  227. package/dist/src/semantics/subscription_schedules.js +0 -60
  228. package/src/engine/billing.ts +0 -16
  229. package/src/engine/charges.ts +0 -19
  230. package/src/engine/connect-settings.ts +0 -23
  231. package/src/engine/coupons.ts +0 -5
  232. package/src/engine/credit-notes.ts +0 -6
  233. package/src/engine/disputes.ts +0 -9
  234. package/src/engine/entitlements.ts +0 -5
  235. package/src/engine/ephemeral-keys.ts +0 -3
  236. package/src/engine/front.ts +0 -31
  237. package/src/engine/payment-links.ts +0 -6
  238. package/src/engine/plans.ts +0 -42
  239. package/src/engine/platform.ts +0 -47
  240. package/src/engine/products.ts +0 -3
  241. package/src/engine/setup-intents.ts +0 -5
  242. package/src/engine/subscription-schedules.ts +0 -10
  243. package/src/engine/tax.ts +0 -39
  244. package/src/engine/terminal.ts +0 -23
  245. package/src/engine/test-clocks.ts +0 -11
  246. package/src/engine/tokens.ts +0 -22
  247. package/src/engine/treasury.ts +0 -47
  248. package/src/screens/connect-oauth.tsx +0 -376
  249. package/src/semantics/disputes.ts +0 -12
  250. package/src/semantics/invoiceitems.ts +0 -59
  251. package/src/semantics/setup_intents.ts +0 -45
  252. package/src/semantics/subscription_items.ts +0 -54
  253. package/src/semantics/subscription_schedules.ts +0 -66
@@ -1,7 +1,4 @@
1
- // Stripe's own computation that its handlers share (docs/contributing/architecture.md, "Pack layout": the engine): money
2
- // and its declines, 3-D Secure and micro-deposits, the list page and the search grammar, a subscription's items, billing
3
- // intervals and discounts, Issuing's spending controls, a payment method's sub-object, a Connect account's settings and a
4
- // Checkout Session's line items. None of it reads or writes state: a caller hands it what it looked up (FindStored).
1
+ import { digest } from '@volter/world-core';
5
2
  import { ownFields, type TwinResource } from '@volter/world-core';
6
3
  import { SERVED_VERSION } from './version.ts';
7
4
 
@@ -170,8 +167,6 @@ export function threeDsNextAction(): Record<string, unknown> {
170
167
  // a `descriptor_code` (the test value `SM11AA`). We model that exact pair so the verify
171
168
  // can FAIL with the wrong amounts. The well-known token that triggers this flow:
172
169
  const MICRODEPOSIT_TOKENS = new Set(['pm_usBankAccount_requiresVerification', 'pm_us_bank_account']);
173
- const MICRODEPOSIT_AMOUNTS = [32, 45];
174
- const MICRODEPOSIT_DESCRIPTOR = 'SM11AA';
175
170
  export function requiresMicrodeposits(...paramSets: Array<Record<string, unknown>>): boolean {
176
171
  for (const params of paramSets) {
177
172
  const ref = resolveCardRef(params);
@@ -188,31 +183,6 @@ export function microdepositsNextAction(): Record<string, unknown> {
188
183
  },
189
184
  };
190
185
  }
191
- /** The two deposits' amounts as sent: the test values (docs.stripe.com/payments/ach-direct-debit/accept-a-payment). */
192
- function amountsMatch(params: Record<string, unknown>): boolean {
193
- const got = (params.amounts as unknown[]).map((a) => Math.trunc(Number(a) || 0));
194
- return got.length === 2 && got[0] === MICRODEPOSIT_AMOUNTS[0] && got[1] === MICRODEPOSIT_AMOUNTS[1];
195
- }
196
- /** The deposit's descriptor code as sent: the test value. */
197
- function descriptorMatches(params: Record<string, unknown>): boolean {
198
- return String(params.descriptor_code) === MICRODEPOSIT_DESCRIPTOR;
199
- }
200
- /** Stripe's refusal of deposits that do not match what was sent. */
201
- function depositMismatch(byAmounts: boolean): StripeResponse {
202
- return byAmounts
203
- ? err('The amounts provided do not match the amounts that were sent to the bank account.', 400, 'payment_intent_microdeposit_verification_amounts_mismatch')
204
- : err('The descriptor code provided does not match the one that was sent to the bank account.', 400, 'payment_intent_microdeposit_verification_descriptor_code_mismatch');
205
- }
206
- // Validate the submitted micro-deposit verification params against the modeled test values.
207
- // Returns a vendor 400 on missing/both/wrong inputs, or null when the verification matches.
208
- export function verifyMicrodeposits(params: Record<string, unknown>, _existing: Record<string, unknown>): StripeResponse | null {
209
- const hasAmounts = Array.isArray(params.amounts);
210
- const hasDescriptor = typeof params.descriptor_code === 'string' && params.descriptor_code !== '';
211
- if (hasAmounts === hasDescriptor) return err('You must pass exactly one of `amounts` and `descriptor_code`.', 400, 'parameter_missing');
212
- const matches = hasAmounts ? amountsMatch(params) : descriptorMatches(params);
213
- return matches ? null : depositMismatch(hasAmounts);
214
- }
215
-
216
186
  // Build the real Stripe card-error envelope (HTTP 402). `param` is 'card' for card
217
187
  // errors; charge/payment_intent ids are attached when known so SDK error objects carry
218
188
  // them (Stripe does this for confirm/charge failures).
@@ -364,7 +334,7 @@ export function asBool(v: unknown): boolean {
364
334
  // resource view. This is the vendor's OWN per-field semantics (A4: not a cross-vendor query
365
335
  // engine — it lives in the Stripe pack and speaks Stripe's language only). Unsupported
366
336
  // constructs cause the clause to not match (safe), never a fabricated hit.
367
- type SearchClause = { field: string; op: ':' | '>' | '<' | '>=' | '<='; value: string };
337
+ type SearchClause = { field: string; op: ':' | '~' | '>' | '<' | '>=' | '<='; value: string };
368
338
  // Tokenize on top-level AND/OR (we treat the whole query as either all-AND or all-OR; mixed
369
339
  // precedence is uncommon in practice and Stripe groups with parens we don't model — kept simple).
370
340
  function parseSearchQuery(query: string): { clauses: SearchClause[]; disjunction: boolean } | null {
@@ -373,12 +343,13 @@ function parseSearchQuery(query: string): { clauses: SearchClause[]; disjunction
373
343
  const disjunction = / OR /i.test(trimmed) && !/ AND /i.test(trimmed);
374
344
  const parts = trimmed.split(disjunction ? / OR /i : / AND /i);
375
345
  const clauses: SearchClause[] = [];
376
- const re = /^\s*([A-Za-z0-9_]+(?:\[[^\]]+\])?)\s*(>=|<=|>|<|:)\s*(.+?)\s*$/;
346
+ const re = /^\s*([A-Za-z0-9_]+(?:\[[^\]]+\])?)\s*(>=|<=|>|<|:|~)\s*(.+?)\s*$/;
377
347
  for (const part of parts) {
378
348
  const m = re.exec(part);
379
349
  if (!m) return null;
380
350
  let value = m[3]!.trim();
381
351
  if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) value = value.slice(1, -1);
352
+ if (m[2] === '~' && (!['email', 'name'].includes(m[1]!) || value.length < 3)) return null;
382
353
  clauses.push({ field: m[1]!, op: m[2] as SearchClause['op'], value });
383
354
  }
384
355
  return { clauses, disjunction };
@@ -396,22 +367,19 @@ function searchFieldValue(item: Record<string, unknown>, field: string): unknown
396
367
  }
397
368
  /** A search's numeric comparison other than `>=` (docs.stripe.com/search#search-query-language). */
398
369
  function otherComparison(op: string, a: number, n: number): boolean {
399
- switch (op) {
400
- case '>': return a > n;
401
- case '<': return a < n;
402
- case '<=': return a <= n;
403
- }
404
- return false;
370
+ return op === '>' ? a > n : op === '<' ? a < n : a <= n;
405
371
  }
406
372
 
407
373
  function evalSearchClause(item: Record<string, unknown>, c: SearchClause): boolean {
408
374
  const actual = searchFieldValue(item, c.field);
375
+ // source: https://docs.stripe.com/search "Substring match operator (substrings must be a minimum of 3 characters)"
376
+ if (c.op === '~') return actual != null && String(actual).toLowerCase().includes(c.value.toLowerCase());
409
377
  if (c.op === ':') {
410
378
  if (c.value === 'null') return actual === null || actual === undefined;
411
379
  // numeric exact when both look numeric; else string-equality (case-sensitive, like Stripe tokens)
412
380
  if (typeof actual === 'number' && /^-?\d+(\.\d+)?$/.test(c.value)) return actual === Number(c.value);
413
- if (typeof actual === 'boolean') return String(actual) === c.value;
414
- return String(actual) === c.value;
381
+ if (typeof actual === 'boolean') return String(actual).toLowerCase() === c.value.toLowerCase();
382
+ return String(actual).toLowerCase() === c.value.toLowerCase();
415
383
  }
416
384
  const n = Number(c.value);
417
385
  const a = Number(actual);
@@ -470,23 +438,6 @@ export function buildDiscount(coupon: Record<string, unknown>, customer: string,
470
438
  };
471
439
  }
472
440
 
473
- // The (price id, quantity) pairs a subscription bills, normalized across the shapes the
474
- // twin stores items in (the create form array items:[{price,quantity}], or the canonical
475
- // items:{data:[{price}]} list). Used to compute upcoming-invoice lines.
476
- export function subscriptionItemPairs(sub: Record<string, unknown>): Array<{ price: string; quantity: number }> {
477
- const items = sub.items as { data?: unknown } | undefined;
478
- const data = Array.isArray(items?.data) ? items!.data : Array.isArray(sub.items) ? (sub.items as unknown[]) : [];
479
- const out: Array<{ price: string; quantity: number }> = [];
480
- for (const it of data as Array<Record<string, unknown>>) {
481
- const p = it?.price;
482
- const id = typeof p === 'string' ? p : (p && typeof p === 'object' ? String((p as Record<string, unknown>).id ?? '') : '');
483
- if (id) out.push({ price: id, quantity: Math.max(1, Math.trunc(Number(it.quantity) || 1)) });
484
- }
485
- // a bare `price` field (the connector's flattened shape) counts as one item.
486
- if (out.length === 0 && typeof sub.price === 'string' && sub.price) out.push({ price: sub.price, quantity: 1 });
487
- return out;
488
- }
489
-
490
441
  // The raw item entries a subscription/checkout-derived-subscription create sent,
491
442
  // normalized to an ordered array. The form reader already turns the bracket form
492
443
  // items[0][price]=… into an array of objects (see lineItemEntries() above for the
@@ -548,18 +499,13 @@ export function addBillingInterval(atUnix: number, interval: BillingInterval, co
548
499
  // response includes the actual items, mirroring what was requested.
549
500
  /** What a subscription item carries of its price: the Price itself and its plan view (a subscription item answers both,
550
501
  * docs.stripe.com/api/subscription_items/object), by the price's id or an already-resolved Price. */
502
+ // source: https://docs.stripe.com/api/plans/create "It replaces the Plans API and is backwards compatible to simplify your migration."
503
+ // source: spec:/components/schemas/plan "amount_decimal"
504
+
551
505
  export function subscriptionItemPrice(p: unknown, find: FindStored): { price: Record<string, unknown> | string | null; plan: Record<string, unknown> | null } {
552
506
  const priceId = typeof p === 'string' ? p : (p && typeof p === 'object' ? String((p as Record<string, unknown>).id ?? '') : '');
553
507
  const price = priceId ? find('price', priceId) : undefined;
554
- const recurring = price?.recurring as Record<string, unknown> | undefined;
555
- const plan = price ? {
556
- id: price.id, object: 'plan', active: price.active ?? true, amount: price.unit_amount ?? null,
557
- amount_decimal: price.unit_amount_decimal ?? null, billing_scheme: price.billing_scheme ?? 'per_unit',
558
- currency: price.currency ?? 'usd', interval: recurring?.interval ?? null,
559
- interval_count: recurring?.interval_count ?? 1, livemode: false,
560
- metadata: price.metadata ?? {}, nickname: price.nickname ?? null,
561
- product: price.product ?? null, tiers_mode: price.tiers_mode ?? null, usage_type: 'licensed',
562
- } : null;
508
+ const plan = price ? planView(price) : null;
563
509
  return { price: price ?? (priceId || null), plan };
564
510
  }
565
511
 
@@ -612,20 +558,6 @@ export function resolveTrial(params: Record<string, unknown>, at: number): { sta
612
558
  return undefined;
613
559
  }
614
560
 
615
- // ── ISSUING helpers: spending controls, real-time-auth shapes ─────────────────────────
616
- // Authorization.card is the FULL Card object in every vendor egress (API responses and
617
- // webhook payloads) — it is not expandable (stripe@22.3.0 Issuing.Authorization.card:
618
- // Stripe.Issuing.Card). The twin stores the id internally and embeds at the boundary;
619
- // cardholder stays an id string (expandable, unexpanded default). Caught live: the
620
- // issuing-bridge reads event.data.object.card.id and refused every twin presentment.
621
- export function embedIssuingAuthorizationCard(body: Record<string, unknown>, find: FindStored): Record<string, unknown> {
622
- if (typeof body.card === 'string') {
623
- const cardRow = find('issuing_card', body.card);
624
- if (cardRow !== undefined) body.card = cardRow;
625
- }
626
- return body;
627
- }
628
-
629
561
  // The closed interval set for spending_controls[spending_limits][][interval]
630
562
  // (stripe@22.3.0 Issuing/Cards.d.ts SpendingLimit.Interval — a documented closed enum).
631
563
  const SPENDING_LIMIT_INTERVALS = new Set(['all_time', 'daily', 'monthly', 'per_authorization', 'weekly', 'yearly']);
@@ -656,10 +588,10 @@ function controlsRefused(what: 'dictionary' | 'categories' | 'countries' | 'limi
656
588
  // Vendor rules enforced: interval must be in the closed enum; a spending limit amount is a
657
589
  // positive integer; allowed_categories "Cannot be set with blocked_categories" (and the
658
590
  // merchant-country pair likewise) — both restrictions verbatim from the SDK's field docs.
659
- export function normalizeSpendingControls(raw: unknown, currency: string | null): { controls?: Record<string, unknown>; error?: StripeResponse } {
591
+ export function normalizeSpendingControls(raw: unknown, currency: string | null, previous?: Record<string, unknown>): { controls?: Record<string, unknown>; error?: StripeResponse } {
660
592
  if (raw === undefined) return {};
661
593
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return controlsRefused('dictionary');
662
- const sc = raw as Record<string, unknown>;
594
+ const sc = { ...previous, ...(raw as Record<string, unknown>) };
663
595
  const strArr = (v: unknown): string[] | undefined => (Array.isArray(v) ? v.map(String) : undefined);
664
596
  const allowedCats = strArr(sc.allowed_categories);
665
597
  const blockedCats = strArr(sc.blocked_categories);
@@ -681,6 +613,9 @@ export function normalizeSpendingControls(raw: unknown, currency: string | null)
681
613
  const arrOrNull = (v: string[] | undefined) => (v === undefined ? undefined : v.length ? v : null);
682
614
  const controls: Record<string, unknown> = {
683
615
  ...emptySpendingControls(),
616
+ // source: spec:/paths/~1v1~1issuing~1cards~1{card}/post/requestBody/content/application~1x-www-form-urlencoded/schema/properties/spending_controls "Rules that control spending for this card."
617
+ // Keep every documented control, including allowed_card_presences and blocked_card_presences.
618
+ ...sc,
684
619
  spending_limits: limits,
685
620
  ...(limits.length ? { spending_limits_currency: typeof sc.spending_limits_currency === 'string' ? sc.spending_limits_currency : currency } : {}),
686
621
  };
@@ -789,6 +724,9 @@ function cardSubObject(params: Params): Params {
789
724
  brand: 'visa', last4: number ? number.slice(-4) : '4242',
790
725
  exp_month: Number(c.exp_month) || 12, exp_year: Number(c.exp_year) || 2034,
791
726
  funding: 'credit', country: 'US',
727
+ // source: spec:/components/schemas/payment_method_card/properties/fingerprint/description "Uniquely identifies this particular card number."
728
+ // The synthetic fingerprint is stable for a number; Stripe's opaque fingerprint algorithm is not published.
729
+ fingerprint: digest('sha256', number || '4242424242424242').slice(0, 16),
792
730
  checks: { address_line1_check: null, address_postal_code_check: null, cvc_check: 'pass' },
793
731
  networks: { available: ['visa'], preferred: null },
794
732
  three_d_secure_usage: { supported: true }, wallet: null,
@@ -1014,3 +952,17 @@ export function chargeDefaults(chargeId: string, amount: number, captured: boole
1014
952
  // collection path segment → resource type, for the generic list of a collection Stripe does not
1015
953
  // list at that path (the twin's test clocks, which Stripe lists under /v1/test_helpers).
1016
954
 
955
+
956
+ export function planView(price: Record<string, unknown>): Record<string, unknown> {
957
+ const recurring = price.recurring as Record<string, unknown> | undefined;
958
+ return {
959
+ id: price.id, object: 'plan', active: price.active ?? true, amount: price.unit_amount ?? null,
960
+ amount_decimal: price.unit_amount_decimal ?? (price.unit_amount == null ? null : String(price.unit_amount)),
961
+ billing_scheme: price.billing_scheme ?? 'per_unit', created: price.created, currency: price.currency,
962
+ interval: recurring?.interval ?? null, interval_count: recurring?.interval_count ?? 1,
963
+ livemode: price.livemode ?? false, metadata: price.metadata ?? {}, nickname: price.nickname ?? null,
964
+ product: price.product ?? null, tiers_mode: price.tiers_mode ?? null,
965
+ meter: recurring?.meter ?? null, trial_period_days: recurring?.trial_period_days ?? null,
966
+ transform_usage: price.transform_quantity ?? null, usage_type: recurring?.usage_type ?? 'licensed',
967
+ };
968
+ }
@@ -3,7 +3,6 @@
3
3
  // invoice's PaymentIntent; canceling it is the machine's move in ../manifest.ts. Retrieve of a
4
4
  // subscription and of an item are the derived core's.
5
5
  import type { Row } from './common.ts';
6
-
7
6
  export const SUB = 'subscription';
8
7
 
9
8
  // pause_collection pauses billing (the subscription stays active; the paused event fires) and
@@ -36,5 +35,3 @@ export function movedTrial(sub: Row, fields: Row, end: number, now: number): und
36
35
  });
37
36
  return undefined;
38
37
  }
39
-
40
- export const ITEM_ATTRIBUTES = new Set(['billing_thresholds', 'discounts', 'metadata', 'price', 'quantity', 'tax_rates']);
@@ -19,5 +19,4 @@
19
19
  // captured; the refusals' wording is the twin's.
20
20
  // Transfer list, retrieve and update and application-fee list and retrieve are the derived core's.
21
21
  import type { Row } from './common.ts';
22
-
23
22
  export const emptyList = (url: string): Row => ({ object: 'list', data: [], has_more: false, total_count: 0, url });
@@ -15,6 +15,7 @@ type Row = Record<string, unknown>;
15
15
  /** The version the pack serves: its vendored spec's. */
16
16
  export const SERVED_VERSION = String(surface.version);
17
17
  const BASIL = '2025-03-31';
18
+ const CLOVER = '2025-09-30';
18
19
 
19
20
  /** Whether a request is answered in the served version's shape. */
20
21
  export const servesCurrent = (pinned?: string | null): boolean => !pinned || pinned >= BASIL;
@@ -23,6 +24,9 @@ export const servesCurrent = (pinned?: string | null): boolean => !pinned || pin
23
24
  // flag, the payment behavior a subscription was created with) and links it follows internally.
24
25
  const KEPT: Record<string, string[]> = {
25
26
  account: ['livemode'],
27
+ // source: spec:PostWebhookEndpoints "Whether this endpoint should receive events from connected accounts"
28
+ // connect configures event delivery; it is not a WebhookEndpoint response field.
29
+ webhook_endpoint: ['connect'],
26
30
  // a meter event is identified by its identifier; the twin's row id is its own
27
31
  'billing.meter_event': ['id'],
28
32
  // a card source's creation time is the twin's, for ordering
@@ -37,7 +41,7 @@ const KEPT: Record<string, string[]> = {
37
41
  payment_intent: ['mandate', 'off_session'],
38
42
  subscription: ['payment_behavior'],
39
43
  subscription_item: ['livemode'],
40
- subscription_schedule: ['from_subscription', 'renewal_interval'],
44
+ subscription_schedule: ['from_subscription', 'renewal_interval', 'proration_behavior'],
41
45
  'tax.transaction': ['calculation'],
42
46
  'terminal.reader': ['registration_code'],
43
47
  topup: ['destination_balance'],
@@ -66,8 +70,7 @@ const BASIL_CHANGES: Record<string, (o: Row, parent?: Row) => Row> = {
66
70
  // "confirmation_secret … Currently, this contains the client_secret of the PaymentIntent that Stripe creates during
67
71
  // invoice finalization" (docs.stripe.com/api/invoices/object; includable, answered only when expanded: INCLUDABLE);
68
72
  // the intent's client_secret is the one ./stripe.ts mintClientSecret gives it
69
- const intent = typeof o.payment_intent === 'string' && o.payment_intent ? o.payment_intent : null;
70
- const confirmation_secret = intent ? { type: 'payment_intent', client_secret: `${intent}_secret_twin` } : null;
73
+ const confirmation_secret = o.confirmation_secret ?? null;
71
74
  return { ...omit(o, ['payment_intent', 'charge', 'paid', 'paid_out_of_band', 'subscription']), parent, confirmation_secret };
72
75
  },
73
76
  charge: (o) => omit(o, ['invoice', 'source']),
@@ -228,9 +231,6 @@ const INCLUDABLE: Record<string, string[]> = {
228
231
  invoice: ['confirmation_secret'],
229
232
  };
230
233
 
231
- /** Every includable field name, as `expand` paths on a top-level object: what a check of an object's FULL answer expands. */
232
- export const INCLUDABLE_FIELDS: string[] = [...new Set(Object.values(INCLUDABLE).flat())];
233
-
234
234
  /** The `expand[]` paths a request names, in its query or its body (JSON or form). */
235
235
  export async function expandOf(request: Request): Promise<string[]> {
236
236
  const url = new URL(request.url);
@@ -274,9 +274,17 @@ export function render(value: unknown, pinned?: string | null, expand: string[]
274
274
  // reader coerces a numeric-looking value to a number, which the answer gives back as the string it was sent as
275
275
  // reads the same, and a key posted "" to remove it is not there (../semantics/shared.ts withMetadata)
276
276
  if (o.metadata && typeof o.metadata === 'object' && !Array.isArray(o.metadata)) o.metadata = Object.fromEntries(Object.entries(o.metadata as Row).filter(([, v]) => v !== '').map(([k, v]) => [k, typeof v === 'number' || typeof v === 'boolean' ? String(v) : v]));
277
+ // source: spec:/components/schemas/checkout.session/properties/ui_mode "Defaults to `hosted_page`."
278
+ // Endive names hosted/custom Checkout hosted_page/elements; older SDK versions keep their own names.
279
+ if (kind === 'checkout.session' && (!pinned || pinned >= '2026-09-30')) {
280
+ if (o.ui_mode === 'hosted') o.ui_mode = 'hosted_page';
281
+ if (o.ui_mode === 'custom') o.ui_mode = 'elements';
282
+ }
277
283
  const kept = KEPT[kind] ? omit(o, KEPT[kind]!) : o;
278
284
  // an includable field a version's change adds (an invoice's confirmation_secret) is hidden as its stored ones are
279
- return omit(withNulls(BASIL_CHANGES[kind] ? BASIL_CHANGES[kind]!(kept) : kept, kind), hidden);
285
+ // source: https://docs.stripe.com/changelog/clover/2025-09-30/polymorphic-coupon "Moves the top-level coupon field on Promotion Codes to promotion.coupon"
286
+ const changed = kind === 'promotion_code' && pinned && pinned < CLOVER ? omit(kept, ['promotion']) : BASIL_CHANGES[kind] ? BASIL_CHANGES[kind]!(kept) : kept;
287
+ return omit(withNulls(changed, kind), hidden);
280
288
  };
281
289
  return walk(value);
282
290
  }
@@ -3,9 +3,7 @@
3
3
  // event type, Stripe's own event types (the spec's list, ../generated/events.gen.json), and the one message Stripe decides
4
4
  // by its receiver's answer, the real-time authorization request.
5
5
  import { hmac } from '@volter/world-core';
6
-
7
6
  import vendorEvents from '../generated/events.gen.json' with { type: 'json' };
8
-
9
7
  /** Every event type Stripe's spec enumerates: a write whose operation is one sends it, and `<resource>.create`,
10
8
  * `.update` and `.delete` send `<resource>.created`, `.updated` and `.deleted` when Stripe has that type. */
11
9
  export const STRIPE_EVENT_TYPES: ReadonlyArray<string> = (vendorEvents as { types: string[] }).types;
package/src/fetch.ts CHANGED
@@ -8,8 +8,8 @@ import { clock } from './semantics/clock.ts';
8
8
  import * as doors from './semantics/doors.ts';
9
9
  import { stripeSemantics } from './semantics/index.ts';
10
10
  import { screen as screen_billing_portal } from './screens/billing-portal.tsx';
11
+ import { screen as screen_checkout_js } from './screens/checkout-js.ts';
11
12
  import { screen as screen_checkout } from './screens/checkout.tsx';
12
- import { screen as screen_connect_oauth } from './screens/connect-oauth.tsx';
13
13
  import { screen as screen_connect_onboarding } from './screens/connect-onboarding.tsx';
14
14
  import { screen as screen_dashboard } from './screens/dashboard.tsx';
15
15
  import { screen as screen_stripe_js } from './screens/stripe-js.ts';
@@ -22,6 +22,6 @@ export function createStripeFetch(options: PackFetchOptions = {}): DerivedFetch
22
22
  doors,
23
23
  around,
24
24
  clock,
25
- screens: { "billing-portal": screen_billing_portal, "checkout": screen_checkout, "connect-oauth": screen_connect_oauth, "connect-onboarding": screen_connect_onboarding, "dashboard": screen_dashboard, "stripe-js": screen_stripe_js },
25
+ screens: { "billing-portal": screen_billing_portal, "checkout-js": screen_checkout_js, "checkout": screen_checkout, "connect-onboarding": screen_connect_onboarding, "dashboard": screen_dashboard, "stripe-js": screen_stripe_js },
26
26
  }, options);
27
27
  }