@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
@@ -2,11 +2,6 @@
2
2
  // author writes": states), each move cited; the manifest takes each resource's whole.
3
3
  import type { StateField, Transition } from '@volter/world-core';
4
4
 
5
- const unexpected = (verb: string, allowed: string) => ({
6
- status: 400,
7
- code: 'payment_intent_unexpected_state',
8
- message: `This PaymentIntent could not be ${verb} because it has a status of {from}. ${allowed}`,
9
- });
10
5
  const OPEN = ['requires_payment_method', 'requires_confirmation', 'requires_action', 'requires_capture', 'processing'];
11
6
 
12
7
  /** A move no API call makes: Stripe's own (`vendor`), the clock's (`time`), or the person on a
@@ -29,18 +24,9 @@ const paymentIntentStatus: StateField = {
29
24
  transitions: [
30
25
  // created with its payment method, it "enters the `requires_confirmation` status and is ready to confirm" (the same page)
31
26
  { operation: 'PostPaymentIntents', from: ['requires_payment_method'], to: 'requires_confirmation', source: 'https://docs.stripe.com/payments/paymentintents/lifecycle' },
32
- // confirm lands on one of its outcomes, decided by the payment method and the capture method
33
- ...['requires_payment_method', 'requires_action', 'requires_capture', 'succeeded'].map((to) => ({
34
- operation: 'PostPaymentIntentsIntentConfirm',
35
- from: ['requires_payment_method', 'requires_confirmation', 'requires_action'],
36
- to,
37
- refusal: { status: 400, code: 'payment_intent_unexpected_state', message: 'You cannot confirm this PaymentIntent because it has a status of {from}.' },
38
- source: 'https://docs.stripe.com/api/payment_intents/confirm',
39
- })),
40
- // a bank debit confirmed (created with `confirm`, confirmed, or verified by micro-deposits) is submitted and
41
- // processes: "The PaymentIntent you create initially has a status of `processing`"; "When the bank account is
42
- // successfully verified, Stripe returns the PaymentIntent object with a status of `processing`"
43
- ...['PostPaymentIntents', 'PostPaymentIntentsIntentConfirm'].map((operation) => ({
27
+ // a bank debit confirmed (created with `confirm`, or confirmed) is submitted and processes: "The PaymentIntent you
28
+ // create initially has a status of `processing`"
29
+ ...['PostPaymentIntents',].map((operation) => ({
44
30
  operation, from: ['requires_payment_method', 'requires_confirmation', 'requires_action'], to: 'processing',
45
31
  source: 'https://docs.stripe.com/payments/ach-direct-debit/accept-a-payment?payment-ui=direct-api',
46
32
  })),
@@ -55,33 +41,10 @@ const paymentIntentStatus: StateField = {
55
41
  source: 'spec:PostPaymentIntents "Set to `true` to attempt to [confirm this PaymentIntent](https://docs.stripe.com/api/payment_intents/confirm) immediately."',
56
42
  })),
57
43
  {
58
- operation: 'PostPaymentIntentsIntentCapture',
59
- from: ['requires_capture'],
60
- to: 'succeeded',
61
- refusal: unexpected('captured', 'Only a PaymentIntent with one of the following statuses may be captured: requires_capture.'),
62
- source: 'https://docs.stripe.com/api/payment_intents/capture',
63
- },
64
- {
65
- operation: 'PostPaymentIntentsIntentIncrementAuthorization',
66
- from: ['requires_capture'],
67
- to: 'requires_capture',
68
- refusal: unexpected('incremented', 'Only a PaymentIntent with a status of requires_capture may be incremented.'),
69
- source: 'https://docs.stripe.com/api/payment_intents/increment_authorization',
70
- },
71
- {
72
- operation: 'PostPaymentIntentsIntentCancel',
73
- from: OPEN,
74
- to: 'canceled',
75
- refusal: unexpected('canceled', 'Only a PaymentIntent with one of the following statuses may be canceled: requires_payment_method, requires_capture, requires_confirmation, requires_action, processing.'),
44
+ operation: 'PostPaymentIntentsIntentCancel', from: OPEN, to: 'canceled',
45
+ refusal: { status: 400, code: 'payment_intent_unexpected_state', message: 'You cannot cancel this PaymentIntent because it has a status of {from}. Only a PaymentIntent with one of the following statuses may be canceled: requires_payment_method, requires_capture, requires_confirmation, requires_action, processing.' },
76
46
  source: 'https://docs.stripe.com/api/payment_intents/cancel',
77
47
  },
78
- {
79
- operation: 'PostPaymentIntentsIntentVerifyMicrodeposits',
80
- from: ['requires_action'],
81
- to: 'processing',
82
- refusal: { status: 400, code: 'payment_intent_unexpected_state', message: 'This PaymentIntent could not be verified because it has a status of {from}.' },
83
- source: 'https://docs.stripe.com/payments/ach-direct-debit/accept-a-payment?payment-ui=direct-api',
84
- },
85
48
  {
86
49
  // paying the invoice an intent collects for settles the intent
87
50
  operation: 'PostInvoicesInvoicePay',
@@ -89,51 +52,32 @@ const paymentIntentStatus: StateField = {
89
52
  to: 'succeeded',
90
53
  source: 'https://docs.stripe.com/api/invoices/pay',
91
54
  },
92
- ...['succeeded', 'requires_action'].map((to) => ({
93
- operation: 'PostPaymentIntentsIntentApplyCustomerBalance',
94
- from: OPEN,
95
- to,
96
- refusal: { status: 400, code: 'payment_intent_unexpected_state', message: 'This PaymentIntent could not be updated because it has a status of {from}.' },
97
- source: 'https://docs.stripe.com/api/payment_intents/apply_customer_balance',
98
- })),
99
- // a card presented to the Terminal reader processing an intent pays it (held, under manual capture)
100
- ...['succeeded', 'requires_capture'].map((to) => ({
101
- operation: 'PostTestHelpersTerminalReadersReaderPresentPaymentMethod', from: ['requires_payment_method', 'requires_confirmation'], to,
102
- source: 'https://docs.stripe.com/api/terminal/readers/present_payment_method',
103
- })),
104
55
  // the customer pays, in the billing portal, the invoice an intent collects for after its charge was declined
105
56
  by('external', ['requires_payment_method'], 'succeeded', 'https://docs.stripe.com/customer-management'),
106
57
  // Stripe also fails a bank debit back to needing a payment method, completes 3-D Secure on the bank's page and
107
58
  // cancels a hold left uncaptured; the twin's bank debits succeed unless their test account stays processing, it has no 3-D Secure page, and it keeps
108
- // holds until they are captured or canceled
59
+ // holds until they are canceled. Capture, increment_authorization, verify_microdeposits, apply_customer_balance and
60
+ // the Terminal reader's present_payment_method are the gap (no demand, life step or refresh sends them:
61
+ // ../manifest.ts unmodeled), so no move of theirs is declared.
109
62
  ],
110
63
  };
111
64
 
112
- /** A charge's `captured`: an authorization (capture=false) is captured once. */
65
+ /** A charge's `captured`: an authorization (capture_method=manual) is captured once, by the Charges API's capture or
66
+ * its PaymentIntent's; both are the gap here (no demand, life step or refresh sends them: ../manifest.ts unmodeled), so
67
+ * a held charge stays uncaptured until its intent is canceled, and no move is declared. */
113
68
  const chargeCaptured: StateField = {
114
69
  initial: true,
115
- transitions: [
116
- {
117
- operation: 'PostChargesChargeCapture',
118
- from: ['false'],
119
- to: 'true',
120
- refusal: { status: 400, code: 'charge_already_captured', message: 'Charge ch_xxx has already been captured.' },
121
- source: 'https://docs.stripe.com/api/charges/capture',
122
- },
123
- // a manual-capture intent's capture captures the charge its confirm authorized
124
- { operation: 'PostPaymentIntentsIntentCapture', from: ['false'], to: 'true', source: 'https://docs.stripe.com/api/payment_intents/capture' },
125
- ],
70
+ transitions: [],
126
71
  };
127
72
 
128
- /** A charge's `refunded`: true once refunds take its whole amount, false again when a refund that made it so is
129
- * canceled. A capture or a cancel never makes it so: "`refunded` will no longer be `true` for payment cancellation
73
+ /** A charge's `refunded`: true once refunds take its whole amount. A capture or a cancel never makes it so: "`refunded` will no longer be `true` for payment cancellation
130
74
  * flows", and a partial capture makes no Refund (docs.stripe.com/changelog/basil/2025-03-31/remove-refund-from-partial-
131
75
  * capture-and-payment-cancellation-flow). */
132
76
  const chargeRefunded: StateField = {
133
77
  initial: false,
134
78
  transitions: [
135
- ...['PostRefunds', 'PostChargesChargeRefunds', 'PostCreditNotes'].map((operation) => ({ operation, from: ['false'], to: 'true', source: 'https://docs.stripe.com/api/charges/object#charge_object-refunded' })),
136
- { operation: 'PostRefundsRefundCancel', from: ['true'], to: 'false', source: 'https://docs.stripe.com/api/refunds/cancel' },
79
+ { operation: 'PostRefunds', from: ['false'], to: 'true', source: 'https://docs.stripe.com/api/charges/object#charge_object-refunded' },
80
+ // (a charge's own refunds path, a credit note's refund and a refund's cancel are the gap here: ../manifest.ts unmodeled)
137
81
  // (an authorization left uncaptured is canceled after seven days, "Uncaptured PaymentIntents are cancelled a set
138
82
  // number of days (7 by default) after their creation", docs.stripe.com/api/payment_intents/capture; the twin keeps it)
139
83
  ],
@@ -155,63 +99,25 @@ const chargePaid: StateField = {
155
99
  transitions: [],
156
100
  };
157
101
 
158
- /** A refund's `status`: only one waiting on the customer (requires_action) may be canceled; the
159
- * rest is the network's. */
102
+ /** A refund's `status`: held pending for want of balance, then made; the rest is the network's. (Canceling one that
103
+ * waits on the customer is the gap here: ../manifest.ts unmodeled.) */
160
104
  const refundStatus: StateField = {
161
105
  initial: 'succeeded',
162
106
  transitions: [
163
- {
164
- operation: 'PostRefundsRefundCancel',
165
- from: ['requires_action'],
166
- to: 'canceled',
167
- refusal: { status: 400, code: 'refund_status_invalid', message: 'Refund {id} cannot be canceled because its status is {from}.' },
168
- source: 'https://docs.stripe.com/api/refunds/cancel',
169
- },
170
- // a card refund the available balance does not cover is held pending, and made once the balance covers it: "Stripe
171
- // holds the refund as pending for card transactions ... until your Stripe balance becomes sufficient"
172
- // (docs.stripe.com/refunds; semantics/refunds.ts)
173
- // (a paid invoice's credit note makes its refund as a refund call does: "Refunds: create a new refund (using
174
- // refund_amount)", docs.stripe.com/api/credit_notes/create)
175
- ...['PostRefunds', 'PostChargesChargeRefunds', 'PostCreditNotes'].map((operation) => ({ operation, from: ['succeeded'], to: 'pending', source: 'https://docs.stripe.com/refunds' })),
107
+ { operation: 'PostRefunds', from: ['succeeded'], to: 'pending', source: 'https://docs.stripe.com/refunds' },
176
108
  by('vendor', ['pending'], 'succeeded', 'https://docs.stripe.com/refunds'),
177
109
  // a bank-transfer payment's refund waits in requires_action for the customer's bank details, and is made so
178
110
  // (semantics/refunds.ts); (Stripe also fails some refunds; the twin's do not fail)
179
111
  ],
180
112
  };
181
113
 
182
- const SETUP_OPEN = ['requires_payment_method', 'requires_confirmation', 'requires_action'];
183
- const setupUnexpected = (verb: string) => ({ status: 400, code: 'setup_intent_unexpected_state', message: `You cannot ${verb} this SetupIntent because it has a status of {from}.` });
184
-
185
- /** A SetupIntent's `status`: confirm settles it (or leaves a bank account awaiting micro-deposits),
186
- * cancel ends an open one, and verification completes the micro-deposit wait. */
114
+ /** A SetupIntent's `status`: confirm settles it, or leaves a bank account awaiting micro-deposits. (Its cancel and its
115
+ * micro-deposit verification are the gap here: ../manifest.ts unmodeled.) */
187
116
  const setupIntentStatus: StateField = {
188
117
  // "After you create the SetupIntent, it has a status of `requires_payment_method` until you attach a payment method"
189
118
  // (docs.stripe.com/payments/paymentintents/lifecycle)
190
119
  initial: 'requires_payment_method',
191
120
  transitions: [
192
- // created with its payment method, it "enters the `requires_confirmation` status and is ready to confirm" (the same page)
193
- { operation: 'PostSetupIntents', from: ['requires_payment_method'], to: 'requires_confirmation', source: 'https://docs.stripe.com/payments/paymentintents/lifecycle' },
194
- ...['requires_action', 'succeeded'].map((to) => ({
195
- operation: 'PostSetupIntentsIntentConfirm',
196
- from: SETUP_OPEN,
197
- to,
198
- refusal: setupUnexpected('confirm'),
199
- source: 'https://docs.stripe.com/api/setup_intents/confirm',
200
- })),
201
- {
202
- operation: 'PostSetupIntentsIntentCancel',
203
- from: SETUP_OPEN,
204
- to: 'canceled',
205
- refusal: setupUnexpected('cancel'),
206
- source: 'https://docs.stripe.com/api/setup_intents/cancel',
207
- },
208
- {
209
- operation: 'PostSetupIntentsIntentVerifyMicrodeposits',
210
- from: ['requires_action'],
211
- to: 'succeeded',
212
- refusal: { status: 400, code: 'setup_intent_unexpected_state', message: 'This SetupIntent could not be verified because it has a status of {from}.' },
213
- source: 'https://docs.stripe.com/api/setup_intents/verify_microdeposits',
214
- },
215
121
  // (Stripe completes 3-D Secure on the bank's page and settles an asynchronous method from processing; the
216
122
  // twin has no such page and settles every method at once)
217
123
  ],
@@ -224,38 +130,20 @@ const invoiceRefusal = (message: string) => ({ status: 400, code: 'invoice_not_e
224
130
  const invoiceStatus: StateField = {
225
131
  initial: 'draft',
226
132
  transitions: [
133
+ { operation: 'PostInvoicesInvoicePay', from: ['draft'], to: 'open', source: 'https://docs.stripe.com/invoicing/integration' },
227
134
  {
228
- operation: 'PostInvoicesInvoiceFinalize',
229
- from: ['draft'],
230
- to: 'open',
231
- refusal: invoiceRefusal('This invoice cannot be finalized because it has status {from}.'),
232
- source: 'https://docs.stripe.com/api/invoices/finalize',
233
- },
234
- // docs.stripe.com/invoicing/integration: "To finalize a draft invoice, use the Dashboard, send it to the customer, or pay it."
235
- { operation: 'PostInvoicesInvoiceSend', from: ['draft'], to: 'open', source: 'https://docs.stripe.com/invoicing/integration' },
236
- { operation: 'PostInvoicesInvoiceSend', from: ['open', 'paid', 'uncollectible', 'void'], source: 'https://docs.stripe.com/api/invoices/send' },
237
- {
135
+ // paying a draft finalizes it on the way: "To finalize a draft invoice, use the Dashboard, send it to the customer, or
136
+ // pay it" (docs.stripe.com/invoicing/integration)
238
137
  operation: 'PostInvoicesInvoicePay',
239
- from: ['open', 'uncollectible'],
138
+ from: ['draft', 'open', 'uncollectible'],
240
139
  to: 'paid',
241
140
  refusal: invoiceRefusal('This invoice cannot be paid because it has status {from}.'),
242
141
  refusals: { paid: { status: 400, code: 'invoice_payment_intent_requires_action', message: 'This invoice has already been paid.' } },
243
142
  source: 'https://docs.stripe.com/api/invoices/pay',
244
143
  },
245
- {
246
- operation: 'PostInvoicesInvoiceMarkUncollectible',
247
- from: ['open'],
248
- to: 'uncollectible',
249
- refusal: invoiceRefusal('Only open invoices can be marked uncollectible (this one is {from}).'),
250
- source: 'https://docs.stripe.com/api/invoices/mark_uncollectible',
251
- },
144
+ // (marking one uncollectible is the gap here: ../manifest.ts unmodeled)
252
145
  // a guard: only a draft is deleted; a finalized invoice is voided instead
253
- {
254
- operation: 'DeleteInvoicesInvoice',
255
- from: ['draft'],
256
- refusal: invoiceRefusal('You can only delete draft invoices.'),
257
- source: 'https://docs.stripe.com/api/invoices/delete',
258
- },
146
+ // in any other state it is refused (a refusal with no `to` refuses where it stands)
259
147
  {
260
148
  // only a finalized invoice is voided; a draft is deleted
261
149
  operation: 'PostInvoicesInvoiceVoid',
@@ -264,15 +152,11 @@ const invoiceStatus: StateField = {
264
152
  refusal: invoiceRefusal('This invoice cannot be voided because it has status {from}.'),
265
153
  source: 'https://docs.stripe.com/api/invoices/void',
266
154
  },
267
- // confirming or capturing the PaymentIntent an invoice collects through pays the invoice
268
- ...['PostPaymentIntentsIntentConfirm', 'PostPaymentIntentsIntentCapture'].map((operation) => ({
269
- operation, from: ['draft', 'open', 'uncollectible'], to: 'paid', source: 'https://docs.stripe.com/billing/subscriptions/build-subscriptions',
270
- })),
155
+ // confirming the PaymentIntent an invoice collects through pays the invoice
271
156
  // as does its bank debit succeeding once submitted (../engine/payment-intents.ts settleBankDebits)
272
157
  by('vendor', ['draft', 'open', 'uncollectible'], 'paid', 'https://docs.stripe.com/billing/subscriptions/build-subscriptions'),
273
- // a bank transfer that arrives in the customer's cash balance pays an open invoice that takes transfers
274
- // (automatic reconciliation; in test mode the funding helper stands in for the transfer)
275
- { operation: 'PostTestHelpersCustomersCustomerFundCashBalance', from: ['open'], to: 'paid', source: 'https://docs.stripe.com/payments/customer-balance/reconciliation' },
158
+ // (a bank transfer reconciled from the customer's cash balance pays one at Stripe; funding a cash balance is the gap
159
+ // here: ../manifest.ts unmodeled)
276
160
  // a subscription's first invoice is paid on the Checkout page that started it
277
161
  by('external', ['draft'], 'paid', 'https://docs.stripe.com/payments/checkout/how-checkout-works'),
278
162
  // a subscription's renewal, drafted at its period's end, is finalized about an hour later and charged: paid, or
@@ -284,7 +168,6 @@ const invoiceStatus: StateField = {
284
168
  { operation: 'PostSubscriptionsSubscriptionExposedId', from: ['draft'], to: 'paid', source: 'https://docs.stripe.com/billing/subscriptions/upgrade-downgrade#immediate-payment' },
285
169
  // a trialing subscription's first invoice, for nothing, is paid as it is made ("An immediate invoice is still
286
170
  // created, but the amount is 0", docs.stripe.com/billing/subscriptions/trials/free-trials)
287
- { operation: 'PostSubscriptions', from: ['draft'], to: 'paid', source: 'https://docs.stripe.com/billing/subscriptions/trials/free-trials' },
288
171
  { operation: 'PostSubscriptionsSubscriptionExposedId', from: ['draft'], to: 'open', source: 'https://docs.stripe.com/billing/subscriptions/upgrade-downgrade#immediate-payment' },
289
172
  // the customer pays an open invoice in the billing portal
290
173
  by('external', ['open'], 'paid', 'https://docs.stripe.com/customer-management'),
@@ -301,16 +184,8 @@ const subscriptionStatus: StateField = {
301
184
  { operation: 'DeleteSubscriptionsSubscriptionExposedId', from: '*', to: 'canceled', source: 'https://docs.stripe.com/api/subscriptions/cancel' },
302
185
  // (a test clock's advance moves no subscription itself: when the clock reaches its target, a trial that ended there
303
186
  // ends by time's move below, on the next request; ../engine/test-clocks.ts finishClockAdvances)
304
- // "Deleting the simulation also deletes the test customers associated with the clock and cancels their
305
- // subscriptions" (docs.stripe.com/billing/testing/test-clocks/api-advanced-usage)
306
- { operation: 'DeleteTestHelpersTestClocksTestClock', from: '*', to: 'canceled', source: 'https://docs.stripe.com/billing/testing/test-clocks/api-advanced-usage' },
307
- // "Cancels a subscription schedule and its associated subscription immediately" (docs.stripe.com/api/subscription_schedules/cancel)
308
- { operation: 'PostSubscriptionSchedulesScheduleCancel', from: ['active', 'trialing', 'past_due', 'incomplete'], to: 'canceled', source: 'https://docs.stripe.com/api/subscription_schedules/cancel' },
309
- // confirming or capturing the first invoice's PaymentIntent, or paying the invoice itself, pays it and
310
- // activates the subscription
311
- ...['PostPaymentIntentsIntentConfirm', 'PostPaymentIntentsIntentCapture'].map((operation) => ({
312
- operation, from: ['incomplete'], to: 'active', source: 'https://docs.stripe.com/billing/subscriptions/build-subscriptions',
313
- })),
187
+ // (canceling a subscription schedule, which cancels its subscription, is the gap here: ../manifest.ts unmodeled)
188
+ // confirming the first invoice's PaymentIntent, or paying the invoice itself, pays it and activates the subscription
314
189
  // or when its bank debit succeeds once submitted (../engine/payment-intents.ts settleBankDebits)
315
190
  by('vendor', ['incomplete'], 'active', 'https://docs.stripe.com/billing/subscriptions/build-subscriptions'),
316
191
  // paying the invoice a subscription waits on (its first, or a renewal that failed) makes it active
@@ -351,7 +226,6 @@ const notOpen = (verb: string) => ({ status: 400, code: 'checkout_session_not_op
351
226
  const checkoutStatus: StateField = {
352
227
  initial: 'open',
353
228
  transitions: [
354
- { operation: 'PostCheckoutSessionsSessionExpire', from: ['open'], to: 'expired', refusal: notOpen('expire'), source: 'https://docs.stripe.com/api/checkout/sessions/expire' },
355
229
  { actor: 'external', from: ['open'], to: 'complete', refusal: notOpen('complete'), source: 'https://docs.stripe.com/api/checkout/sessions/object#checkout_session_object-status' },
356
230
  // (Stripe expires an open session at expires_at; the twin keeps it open until asked)
357
231
  ],
@@ -367,60 +241,39 @@ const checkoutPaymentStatus: StateField = {
367
241
  ],
368
242
  };
369
243
 
370
- /** A dispute's `status`: closing concedes it; the bank decides the rest, and an unanswered one is
371
- * lost when its evidence is due. */
244
+ /** A dispute's `status`: a disputed test card's charge opens one (needs_response, or warning_needs_response for an
245
+ * inquiry). Submitting evidence and closing (conceding) are the gap here (../manifest.ts unmodeled), so nothing puts a
246
+ * dispute under review for the bank to decide; the bank escalates an inquiry and loses one left unanswered at
247
+ * Stripe, and the twin decides none. */
372
248
  const disputeStatus: StateField = {
373
249
  initial: 'warning_needs_response',
374
- transitions: [
375
- { operation: 'PostDisputesDisputeClose', from: '*', to: 'lost', source: 'https://docs.stripe.com/api/disputes/close' },
376
- // submitting evidence puts a dispute (or an inquiry) under review; in test mode the evidence decides it
377
- { operation: 'PostDisputesDispute', from: ['needs_response'], to: 'under_review', source: 'https://docs.stripe.com/api/disputes/update#update_dispute-submit' },
378
- { operation: 'PostDisputesDispute', from: ['warning_needs_response'], to: 'warning_under_review', source: 'https://docs.stripe.com/api/disputes/update#update_dispute-submit' },
379
- by('vendor', ['under_review'], 'won', 'https://docs.stripe.com/testing#evidence'),
380
- by('vendor', ['under_review'], 'lost', 'https://docs.stripe.com/testing#evidence'),
381
- // (the bank decides a dispute under review, escalates an inquiry and loses one left unanswered; the twin
382
- // decides none)
383
- // Stripe also closes an inquiry the bank drops (warning_closed); no inquiry the twin holds is dropped
384
- ],
250
+ transitions: [],
385
251
  };
386
252
 
387
253
  /** A payout's `status`: cancel ends it; reversing cancels it and pays a reversal, never twice. */
388
254
  const payoutStatus: StateField = {
389
255
  initial: 'pending',
390
256
  transitions: [
391
- // only a pending payout can be canceled (docs.stripe.com/api/payouts/cancel); reversing a paid one makes a new
392
- // payout and leaves the original paid (semantics/balance.ts)
393
- { operation: 'PostPayoutsPayoutCancel', from: ['pending'], to: 'canceled', source: 'https://docs.stripe.com/api/payouts/cancel' },
257
+ // (canceling a pending payout and reversing a paid one are the gap here: ../manifest.ts unmodeled)
394
258
  // it arrives in the bank on its arrival date
395
259
  by('time', ['pending'], 'paid', 'https://docs.stripe.com/payouts#payout-statuses'),
396
260
  // (a bank may fail a payout at Stripe; the twin's arrive)
397
261
  ],
398
262
  };
399
263
 
400
- /** A credit note's `status`: issued, then voided once. */
264
+ /** A credit note's `status`: issued, then voided once at Stripe. Issuing and voiding one are the gap here
265
+ * (../manifest.ts unmodeled): a vendor-backed World reads credit notes back from its root (GetCreditNotes). */
401
266
  const creditNoteStatus: StateField = {
402
267
  initial: 'issued',
403
- transitions: [
404
- {
405
- operation: 'PostCreditNotesIdVoid',
406
- from: ['issued'],
407
- to: 'void',
408
- refusal: { status: 400, code: 'credit_note_already_voided', message: 'This credit note has already been voided.' },
409
- source: 'https://docs.stripe.com/api/credit_notes/void',
410
- },
411
- ],
268
+ transitions: [],
412
269
  };
413
270
 
414
- const scheduleEnded = (verb: string) => ({ status: 400, code: 'subscription_schedule_status_invalid', message: `This subscription schedule has status {from} and cannot be ${verb}.` });
415
-
416
- /** A subscription schedule's `status`: canceling or releasing ends it; an ended one refuses both. */
271
+ /** A subscription schedule's `status`: releasing ends it, and an ended one refuses it. (Canceling one is the gap here:
272
+ * ../manifest.ts unmodeled.) */
417
273
  const scheduleStatus: StateField = {
418
274
  initial: 'active',
419
275
  transitions: [
420
- // "A subscription schedule can only be canceled if its status is not_started or active"; "A schedule can only be
421
- // released if its status is not_started or active" (the cancel and release pages)
422
- { operation: 'PostSubscriptionSchedulesScheduleCancel', from: ['active', 'not_started'], to: 'canceled', refusal: scheduleEnded('canceled'), source: 'https://docs.stripe.com/api/subscription_schedules/cancel' },
423
- { operation: 'PostSubscriptionSchedulesScheduleRelease', from: ['active', 'not_started'], to: 'released', refusal: scheduleEnded('released'), source: 'https://docs.stripe.com/api/subscription_schedules/release' },
276
+ // "A schedule can only be released if its status is not_started or active" (the release page)
424
277
  // its phases run on the clock: it starts at its start date and ends with its last phase
425
278
  // (a schedule whose end_behavior is cancel completes at its end at Stripe; the twin's release)
426
279
  // (Stripe starts a schedule at its start date and ends it with its last phase; the twin's start at once
@@ -428,18 +281,12 @@ const scheduleStatus: StateField = {
428
281
  ],
429
282
  };
430
283
 
431
- const quoteRefusal = (verb: string) => ({ status: 400, code: 'quote_invalid_status', message: `This quote cannot be ${verb} because it has status {from}.` });
432
-
433
- /** A quote's `status`: a draft is finalized into open, an open one accepted, and either canceled. */
284
+ /** A quote's `status`: at Stripe a draft is finalized into open, an open one accepted, and either canceled. Quotes are
285
+ * the gap here (../manifest.ts unmodeled): a vendor-backed World reads them back from its root (GetQuotes), so no
286
+ * move is the twin's. */
434
287
  const quoteStatus: StateField = {
435
288
  initial: 'draft',
436
- transitions: [
437
- { operation: 'PostQuotesQuoteFinalize', from: ['draft'], to: 'open', refusal: quoteRefusal('finalized'), source: 'https://docs.stripe.com/api/quotes/finalize' },
438
- { operation: 'PostQuotesQuoteAccept', from: ['open'], to: 'accepted', refusal: quoteRefusal('accepted'), source: 'https://docs.stripe.com/api/quotes/accept' },
439
- { operation: 'PostQuotesQuoteCancel', from: ['draft', 'open'], to: 'canceled', refusal: quoteRefusal('canceled'), source: 'https://docs.stripe.com/api/quotes/cancel' },
440
- // an open quote not accepted by its expires_at is canceled
441
- // (Stripe cancels an open quote at its expires_at; the twin keeps it open)
442
- ],
289
+ transitions: [],
443
290
  };
444
291
 
445
292
  const setBy = (operation: string, values: string[], source: string): Transition[] => values.map((to) => ({ operation, from: '*' as const, to, source }));
@@ -450,7 +297,9 @@ const issuingAuthorizationStatus: StateField = {
450
297
  initial: 'pending',
451
298
  transitions: [
452
299
  // approving keeps it pending (approved) until the merchant captures; declining closes it
453
- { operation: 'PostIssuingAuthorizationsAuthorizationApprove', from: ['pending'], refusal: { status: 400, code: 'authorization_already_finalized', message: 'This authorization has already been finalized (status {from}).' }, source: 'https://docs.stripe.com/issuing/purchases/authorizations' },
300
+ { operation: 'PostIssuingAuthorizationsAuthorizationApprove', from: ['pending'], source: 'https://docs.stripe.com/issuing/purchases/authorizations' },
301
+ // in any other state it is refused (a refusal with no `to` refuses where it stands)
302
+ { operation: 'PostIssuingAuthorizationsAuthorizationApprove', from: '*', refusal: { status: 400, code: 'authorization_already_finalized', message: 'This authorization has already been finalized (status {from}).' }, source: 'https://docs.stripe.com/issuing/purchases/authorizations' },
454
303
  { operation: 'PostIssuingAuthorizationsAuthorizationDecline', from: ['pending'], to: 'closed', refusal: { status: 400, code: 'authorization_already_finalized', message: 'This authorization has already been finalized (status {from}).' }, source: 'https://docs.stripe.com/api/issuing/authorizations/decline' },
455
304
  { operation: 'PostTestHelpersIssuingAuthorizationsAuthorizationCapture', from: ['pending'], to: 'closed', source: 'https://docs.stripe.com/api/issuing/authorizations/test_mode_capture' },
456
305
  { operation: 'PostTestHelpersIssuingAuthorizationsAuthorizationCapture', from: ['pending'], source: 'https://docs.stripe.com/api/issuing/authorizations/test_mode_capture' },
@@ -463,35 +312,23 @@ const issuingAuthorizationStatus: StateField = {
463
312
  ],
464
313
  };
465
314
 
466
- /** A Treasury flow's `status` as the twin's own operations move it: only a processing flow cancels. */
467
- const flowCancel = (operation: string, code: string, name: string, network: Transition[]): StateField => ({
468
- initial: 'processing',
469
- transitions: [
470
- { operation, from: ['processing'], to: 'canceled', refusal: { status: 400, code, message: `This ${name} cannot be canceled because it is no longer processing.` }, source: 'https://docs.stripe.com/api/treasury' },
471
- ...network,
472
- ],
473
- });
474
- /** An outbound flow on the network: Stripe posts or fails it and a posted one may come back returned (in
475
- * test mode by test helpers the twin does not serve), so none of those moves is the twin's. */
476
- const outbound = (_doc: string): Transition[] => [];
315
+ /** A Treasury flow's `status`: at Stripe a processing flow cancels, the network posts or fails it, and a posted one may
316
+ * come back returned. Treasury's flows are the gap here (../manifest.ts unmodeled): a vendor-backed World reads them
317
+ * back from its root, so no move is the twin's. */
318
+ const flow: StateField = { initial: 'processing', transitions: [] };
477
319
 
478
320
  export const states = {
479
321
  'billing.meter': {
480
322
  notState: ['event_time_window'],
481
- state: { status: { initial: 'active', transitions: [
482
- { operation: 'PostBillingMetersIdDeactivate', from: '*', to: 'inactive', source: 'https://docs.stripe.com/api/billing/meter/deactivate' },
483
- { operation: 'PostBillingMetersIdReactivate', from: '*', to: 'active', source: 'https://docs.stripe.com/api/billing/meter/reactivate' },
484
- ] } },
323
+ // (deactivating and reactivating a meter are the gap here: ../manifest.ts unmodeled)
324
+ state: { status: { initial: 'active', transitions: [] } },
485
325
  },
486
326
  'billing.credit_grant': {
487
327
  notState: ['category'],
488
328
  },
489
329
  'billing.alert': {
490
- state: { status: { initial: 'active', transitions: [
491
- { operation: 'PostBillingAlertsIdActivate', from: '*', to: 'active', source: 'https://docs.stripe.com/api/billing/alert/activate' },
492
- { operation: 'PostBillingAlertsIdDeactivate', from: '*', to: 'inactive', source: 'https://docs.stripe.com/api/billing/alert/deactivate' },
493
- { operation: 'PostBillingAlertsIdArchive', from: '*', to: 'archived', source: 'https://docs.stripe.com/api/billing/alert/archive' },
494
- ] } },
330
+ // (activating, deactivating and archiving an alert are the gap here: ../manifest.ts unmodeled)
331
+ state: { status: { initial: 'active', transitions: [] } },
495
332
  },
496
333
  payment_intent: {
497
334
  state: { status: paymentIntentStatus },
@@ -554,17 +391,10 @@ export const states = {
554
391
  notState: ['billing_address_collection', 'customer_creation', 'locale', 'mode', 'origin_context', 'payment_method_collection', 'redirect_on_completion', 'submit_type', 'ui_mode'],
555
392
  },
556
393
  mandate: {
557
- state: {
558
- status: {
559
- initial: 'active',
560
- transitions: [
561
- // a single-use mandate is spent by its payment: inactive "The mandate was rejected, revoked, or previously
562
- // used, and may not be used to initiate future payments" (../engine/payment-intents.ts settleBankDebits)
563
- by('vendor', ['active'], 'inactive', 'https://docs.stripe.com/api/mandates/object'),
564
- // (Stripe also activates a pending mandate and deactivates one revoked; the twin's are active when made)
565
- ],
566
- },
567
- },
394
+ // Stripe activates a pending mandate, spends a single-use one with its payment and deactivates one revoked; the twin
395
+ // makes no mandate (micro-deposit verification, which accepts one, is the gap here: ../manifest.ts unmodeled), so no
396
+ // move is the twin's
397
+ state: { status: { initial: 'active', transitions: [] } },
568
398
  notState: ['type'],
569
399
  },
570
400
  dispute: {
@@ -602,7 +432,8 @@ export const states = {
602
432
  notState: ['end_behavior'],
603
433
  },
604
434
  'entitlements.feature': {
605
- state: { active: { initial: true, transitions: flag('PostEntitlementsFeaturesId', 'spec:PostEntitlementsFeaturesId "Update a feature’s metadata or permanently deactivate it."') } },
435
+ // (updating a feature, which "permanently" deactivates it, is the gap here: ../manifest.ts unmodeled)
436
+ state: { active: { initial: true, transitions: [] } },
606
437
  },
607
438
  'test_helpers.test_clock': {
608
439
  state: {
@@ -611,7 +442,6 @@ export const states = {
611
442
  transitions: [
612
443
  // "advancing: The clock has started to advance but hasn't reached the specified time"; "ready: The clock has
613
444
  // completed advancing to the specified time" (docs.stripe.com/billing/testing/test-clocks/api-advanced-usage)
614
- { operation: 'PostTestHelpersTestClocksTestClockAdvance', from: ['ready'], to: 'advancing', refusal: { status: 400, code: 'test_clock_not_ready', message: 'This test clock is still advancing.' }, source: 'https://docs.stripe.com/api/test_clocks/advance' },
615
445
  by('vendor', ['advancing'], 'ready', 'https://docs.stripe.com/billing/testing/test-clocks/api-advanced-usage'),
616
446
  // (Stripe may also end an advance in internal_failure; the twin's advances complete)
617
447
  ],
@@ -619,14 +449,8 @@ export const states = {
619
449
  },
620
450
  },
621
451
  webhook_endpoint: {
622
- state: {
623
- status: {
624
- initial: 'enabled',
625
- transitions: [
626
- ...['enabled', 'disabled'].map((to) => ({ operation: 'PostWebhookEndpointsWebhookEndpoint', from: '*' as const, to, source: 'https://docs.stripe.com/api/webhook_endpoints/update#update_webhook_endpoint-disabled' })),
627
- ],
628
- },
629
- },
452
+ // (disabling and enabling an endpoint by its update is the gap here: ../manifest.ts unmodeled)
453
+ state: { status: { initial: 'enabled', transitions: [] } },
630
454
  },
631
455
  account: {
632
456
  notState: ['business_type', 'type'],
@@ -639,10 +463,8 @@ export const states = {
639
463
  status: {
640
464
  initial: 'unrequested',
641
465
  transitions: [
642
- ...['pending', 'inactive'].map((to) => ({ operation: 'PostAccountsAccountCapabilitiesCapability', from: '*' as const, to, source: 'https://docs.stripe.com/api/capabilities/update' })),
466
+ // (requesting or unrequesting a capability by its update is the gap here: ../manifest.ts unmodeled)
643
467
  // Stripe reviews the account's requirements
644
- by('vendor', ['pending', 'inactive'], 'active', 'https://docs.stripe.com/connect/account-capabilities#capability-status'),
645
- by('vendor', ['active', 'pending'], 'inactive', 'https://docs.stripe.com/connect/account-capabilities#capability-status'),
646
468
  ],
647
469
  },
648
470
  },
@@ -653,25 +475,17 @@ export const states = {
653
475
  },
654
476
  application_fee: {
655
477
  state: { refunded: { initial: false, transitions: [
656
- { operation: 'PostApplicationFeesIdRefunds', from: ['false'], to: 'true', source: 'https://docs.stripe.com/api/fee_refunds/create' },
657
- // a refund of its charge with refund_application_fee refunds it in proportion
658
- ...['PostRefunds', 'PostChargesChargeRefunds'].map((operation) => ({ operation, from: ['false'], to: 'true', source: 'https://docs.stripe.com/api/refunds/create#create_refund-refund_application_fee' })),
478
+ { operation: 'PostRefunds', from: ['false'], to: 'true', source: 'https://docs.stripe.com/connect/destination-charges#issue-refunds' },
659
479
  ] } },
660
480
  },
661
481
  file: {
662
482
  notState: ['purpose'],
663
483
  },
664
484
  'identity.verification_session': {
665
- state: {
666
- status: {
667
- initial: 'requires_input',
668
- transitions: [
669
- by('external', ['requires_input'], 'processing', 'https://docs.stripe.com/identity/how-sessions-work'),
670
- by('vendor', ['processing'], 'verified', 'https://docs.stripe.com/identity/how-sessions-work'),
671
- by('vendor', ['processing'], 'requires_input', 'https://docs.stripe.com/identity/how-sessions-work'),
672
- ],
673
- },
674
- },
485
+ // the person verifies on Stripe's hosted page, and Stripe decides (docs.stripe.com/identity/how-sessions-work);
486
+ // sessions are the gap here (../manifest.ts unmodeled) and a vendor-backed World reads them back from its root,
487
+ // so no move is the twin's
488
+ state: { status: { initial: 'requires_input', transitions: [] } },
675
489
  notState: ['type'],
676
490
  },
677
491
  'identity.verification_report': {
@@ -681,11 +495,12 @@ export const states = {
681
495
  notState: ['locale'],
682
496
  },
683
497
  'billing_portal.configuration': {
684
- state: { active: { initial: true, transitions: flag('PostBillingPortalConfigurationsConfiguration', 'https://docs.stripe.com/api/customer_portal/configurations/update') } },
498
+ state: { active: { initial: true, transitions: [] } },
685
499
  notState: ['is_default'],
686
500
  },
687
501
  tax_rate: {
688
- state: { active: { initial: true, transitions: flag('PostTaxRatesTaxRate', 'https://docs.stripe.com/api/tax_rates/update') } },
502
+ // (archiving a rate by its update is the gap here: ../manifest.ts unmodeled)
503
+ state: { active: { initial: true, transitions: [] } },
689
504
  notState: ['jurisdiction_level', 'rate_type', 'state', 'tax_type'],
690
505
  },
691
506
  'tax.settings': {
@@ -702,7 +517,8 @@ export const states = {
702
517
  notState: ['type'],
703
518
  },
704
519
  payment_link: {
705
- state: { active: { initial: true, transitions: flag('PostPaymentLinksPaymentLink', 'https://docs.stripe.com/api/payment_links/payment_links/update') } },
520
+ // (switching a link off by its update is the gap here: ../manifest.ts unmodeled)
521
+ state: { active: { initial: true, transitions: [] } },
706
522
  notState: ['billing_address_collection', 'customer_creation', 'payment_method_collection', 'submit_type'],
707
523
  },
708
524
  quote: {
@@ -710,26 +526,22 @@ export const states = {
710
526
  notState: ['collection_method'],
711
527
  },
712
528
  review: {
713
- state: {
714
- open: {
715
- initial: true,
716
- transitions: [
717
- // (Stripe also closes one when the payment is refunded or disputed; the twin's close only on approval)
718
- { operation: 'PostReviewsReviewApprove', from: ['true'], to: 'false', refusal: { status: 400, code: 'review_already_closed', message: 'This review has already been closed.' }, source: 'https://docs.stripe.com/api/radar/reviews/approve' },
719
- ],
720
- },
721
- },
529
+ // an elevated-risk card's charge opens one (shared.ts openReview); approving it is the gap here (../manifest.ts
530
+ // unmodeled), and Stripe closes one when the payment is refunded or disputed, which the twin does not
531
+ state: { open: { initial: true, transitions: [] } },
722
532
  notState: ['closed_reason', 'opened_reason'],
723
533
  },
724
534
  'radar.value_list': {
725
535
  notState: ['item_type'],
726
536
  },
727
537
  'issuing.cardholder': {
728
- state: { status: { initial: 'active', transitions: setBy('PostIssuingCardholdersCardholder', ['active', 'inactive', 'blocked'], 'https://docs.stripe.com/api/issuing/cardholders/update') } },
538
+ // (a cardholder's status set by its update is the gap here: ../manifest.ts unmodeled)
539
+ state: { status: { initial: 'active', transitions: [] } },
729
540
  notState: ['type'],
730
541
  },
731
542
  'issuing.card': {
732
- state: { status: { initial: 'active', transitions: setBy('PostIssuingCardsCard', ['active', 'inactive', 'canceled'], 'https://docs.stripe.com/api/issuing/cards/update') } },
543
+ // a card is made inactive unless its create names a status: "Defaults to inactive" (docs.stripe.com/api/issuing/cards/create)
544
+ state: { status: { initial: 'inactive', transitions: setBy('PostIssuingCardsCard', ['active', 'inactive', 'canceled'], 'https://docs.stripe.com/api/issuing/cards/update') } },
733
545
  notState: ['cancellation_reason', 'replacement_reason', 'type'],
734
546
  },
735
547
  'issuing.authorization': {
@@ -740,34 +552,15 @@ export const states = {
740
552
  notState: ['type', 'wallet'],
741
553
  },
742
554
  'issuing.dispute': {
743
- state: {
744
- status: {
745
- initial: 'unsubmitted',
746
- transitions: [
747
- { operation: 'PostIssuingDisputesDisputeSubmit', from: ['unsubmitted'], to: 'submitted', refusal: { status: 400, code: 'dispute_unsubmitted_required', message: 'This dispute has already been submitted (status {from}).' }, source: 'spec:PostIssuingDisputesDisputeSubmit "Submits an Issuing <code>Dispute</code> to the card network."' },
748
- // (Stripe decides a submitted dispute won or lost and expires one never submitted; the twin decides none)
749
- ],
750
- },
751
- },
555
+ // (submitting one to the network is the gap here, ../manifest.ts unmodeled, and Stripe decides a submitted dispute
556
+ // won or lost and expires one never submitted: a vendor-backed World reads them back from its root)
557
+ state: { status: { initial: 'unsubmitted', transitions: [] } },
752
558
  notState: ['loss_reason'],
753
559
  },
754
- 'issuing.token': {
755
- state: { status: { initial: 'active', transitions: [...setBy('PostIssuingTokensToken', ['active', 'deleted', 'suspended'], 'https://docs.stripe.com/api/issuing/tokens/update')] } },
756
- notState: ['network', 'wallet_provider'],
757
- },
758
560
  'issuing.personalization_design': {
759
- state: {
760
- status: {
761
- // a new design waits on Stripe's review (docs.stripe.com/issuing/cards/physical/personalization-design)
762
- initial: 'review',
763
- transitions: [
764
- ...setBy('PostTestHelpersIssuingPersonalizationDesignsPersonalizationDesignActivate', ['active'], 'spec:PostTestHelpersIssuingPersonalizationDesignsPersonalizationDesignActivate "to <code>active</code>"'),
765
- ...setBy('PostTestHelpersIssuingPersonalizationDesignsPersonalizationDesignDeactivate', ['inactive'], 'spec:PostTestHelpersIssuingPersonalizationDesignsPersonalizationDesignDeactivate "to <code>inactive</code>"'),
766
- ...setBy('PostTestHelpersIssuingPersonalizationDesignsPersonalizationDesignReject', ['rejected'], 'spec:PostTestHelpersIssuingPersonalizationDesignsPersonalizationDesignReject "to <code>rejected</code>"'),
767
- // (outside test mode Stripe's review decides it; the twin is test mode, where these helpers do)
768
- ],
769
- },
770
- },
561
+ // a new design waits on Stripe's review (docs.stripe.com/issuing/cards/physical/personalization-design); designs and
562
+ // the test helpers that decide them are the gap here (../manifest.ts unmodeled), read back from a vendor-backed root
563
+ state: { status: { initial: 'review', transitions: [] } },
771
564
  },
772
565
  'terminal.reader': {
773
566
  notState: ['device_type'],
@@ -784,7 +577,7 @@ export const states = {
784
577
  // later: "USA (USD) ACH Debit Transfer 5 days" (docs.stripe.com/connect/top-ups; semantics/terminal.ts)
785
578
  initial: 'pending',
786
579
  transitions: [
787
- { operation: 'PostTopupsTopupCancel', from: ['pending'], to: 'canceled', refusal: { status: 400, code: 'topup_unexpected_state', message: 'This top-up cannot be canceled because it has a status of {from}.' }, source: 'https://docs.stripe.com/api/topups/cancel' },
580
+ // (canceling a pending top-up is the gap here: ../manifest.ts unmodeled)
788
581
  // to the Issuing balance alike: "Your top-ups can take up to 5 business days to become available"
789
582
  // (docs.stripe.com/issuing/funding/balance)
790
583
  by('vendor', ['pending'], 'succeeded', 'https://docs.stripe.com/connect/top-ups'),
@@ -799,17 +592,13 @@ export const states = {
799
592
  notState: ['is_default'],
800
593
  },
801
594
  'treasury.outbound_payment': {
802
- state: { status: flowCancel('PostTreasuryOutboundPaymentsIdCancel', 'outbound_payment_not_cancelable', 'OutboundPayment', outbound('https://docs.stripe.com/treasury/moving-money/financial-accounts/out-of/outbound-payments')) },
595
+ state: { status: flow },
803
596
  },
804
597
  'treasury.outbound_transfer': {
805
- state: { status: flowCancel('PostTreasuryOutboundTransfersOutboundTransferCancel', 'outbound_transfer_not_cancelable', 'OutboundTransfer', outbound('https://docs.stripe.com/treasury/moving-money/financial-accounts/out-of/outbound-transfers')) },
598
+ state: { status: flow },
806
599
  },
807
600
  'treasury.inbound_transfer': {
808
- // "An InboundTransfer is processing if it is created and pending. The status changes to succeeded once the funds have
809
- // been "confirmed""; in test mode the succeed helper confirms them (docs.stripe.com/api/treasury/inbound_transfers)
810
- state: { status: flowCancel('PostTreasuryInboundTransfersInboundTransferCancel', 'inbound_transfer_not_cancelable', 'InboundTransfer', [
811
- { operation: 'PostTestHelpersTreasuryInboundTransfersIdSucceed', from: ['processing'], to: 'succeeded', refusal: { status: 400, code: 'inbound_transfer_not_processing', message: 'This InboundTransfer cannot succeed because it is no longer processing.' }, source: 'https://docs.stripe.com/api/treasury/inbound_transfers' },
812
- ]) },
601
+ state: { status: flow },
813
602
  },
814
603
  'treasury.received_credit': {
815
604
  state: { status: { initial: 'succeeded', transitions: [] } },
@@ -821,15 +610,9 @@ export const states = {
821
610
  },
822
611
  'treasury.transaction': {
823
612
  notState: ['flow_type'],
824
- // void: "The Transaction never impacted the balance. For example, a Transaction would enter this state if an
825
- // OutboundPayment was initiated but then canceled" (docs.stripe.com/api/treasury/transactions/object); canceling an
826
- // outbound flow voids its open transaction (semantics/treasury.ts). (Stripe also posts an open transaction as its
827
- // flow settles; the twin's outbound flows stay processing)
828
- state: { status: { initial: 'open', transitions: [
829
- ...['PostTreasuryOutboundPaymentsIdCancel', 'PostTreasuryOutboundTransfersOutboundTransferCancel', 'PostTreasuryInboundTransfersInboundTransferCancel'].map((operation) => ({ operation, from: ['open'], to: 'void', source: 'https://docs.stripe.com/api/treasury/transactions/object' })),
830
- // an inbound transfer's transaction posts when it succeeds ("a transaction is created and posted")
831
- { operation: 'PostTestHelpersTreasuryInboundTransfersIdSucceed', from: ['open'], to: 'posted', source: 'https://docs.stripe.com/api/treasury/inbound_transfers' },
832
- ] } },
613
+ // a transaction posts as its flow settles and is voided when its flow is canceled, both Stripe's (the flows are the
614
+ // gap here: ../manifest.ts unmodeled)
615
+ state: { status: { initial: 'open', transitions: [] } },
833
616
  },
834
617
  'treasury.transaction_entry': {
835
618
  notState: ['flow_type', 'type'],
@@ -840,11 +623,9 @@ export const states = {
840
623
  // an order is created confirmed: the create page's example answers "status": "confirmed" with confirmed_at set
841
624
  // (docs.stripe.com/api/climate/order/create)
842
625
  initial: 'confirmed',
843
- transitions: [
844
- { operation: 'PostClimateOrdersOrderCancel', from: ['awaiting_funds', 'confirmed', 'open'], to: 'canceled', refusal: { status: 400, code: 'climate_order_not_cancelable', message: 'This order can no longer be canceled because it is {from}.' }, source: 'https://docs.stripe.com/api/climate/order/cancel' },
845
- // funded, confirmed by the supplier, then delivered
846
- // (Stripe funds, confirms and delivers an order over months; the twin's orders stay where they are made)
847
- ],
626
+ // (Stripe funds, confirms and delivers an order over months, and cancels one asked to; orders are the gap here,
627
+ // ../manifest.ts unmodeled, read back from a vendor-backed root)
628
+ transitions: [],
848
629
  },
849
630
  },
850
631
  notState: ['cancellation_reason'],
@@ -853,10 +634,9 @@ export const states = {
853
634
  state: {
854
635
  status: {
855
636
  initial: 'active',
856
- transitions: [
857
- { operation: 'PostFinancialConnectionsAccountsAccountDisconnect', from: '*', to: 'disconnected', source: 'https://docs.stripe.com/api/financial_connections/accounts/disconnect' },
858
- // (Stripe marks an account inactive when the bank link breaks, and active again; the twin's links hold)
859
- ],
637
+ // (disconnecting one is the gap here, ../manifest.ts unmodeled; Stripe marks an account inactive when the bank
638
+ // link breaks, and active again)
639
+ transitions: [],
860
640
  },
861
641
  },
862
642
  notState: ['category', 'subcategory'],
@@ -867,17 +647,16 @@ export const states = {
867
647
  },
868
648
  'reporting.report_run': {
869
649
  // "When first created, the object appears with status="pending""; "When the run completes, Stripe updates the
870
- // object, and it has a status of succeeded" (docs.stripe.com/reports/api; ../engine/platform.ts finishReportRuns).
871
- // (Stripe may also fail a run; the twin's runs succeed)
872
- state: { status: { initial: 'pending', transitions: [by('vendor', ['pending'], 'succeeded', 'https://docs.stripe.com/reports/api')] } },
650
+ // object, and it has a status of succeeded" (docs.stripe.com/reports/api). Report runs are the gap here
651
+ // (../manifest.ts unmodeled), read back from a vendor-backed root, so Stripe's move is not the twin's.
652
+ state: { status: { initial: 'pending', transitions: [] } },
873
653
  },
874
654
  product: {
875
- state: { active: { initial: true, transitions: flag('PostProductsId', 'https://docs.stripe.com/api/products/update#update_product-active') } },
655
+ state: { active: { initial: true, transitions: [] } },
876
656
  },
877
657
  price: {
878
658
  notState: ['billing_scheme', 'tax_behavior', 'tiers_mode', 'type'],
879
- // deleting a plan leaves its price for its subscribers, inactive: "Deleting plans means new subscribers can’t be added.
880
- // Existing subscribers aren’t affected." (docs.stripe.com/api/plans/delete; semantics/plans.ts)
881
- state: { active: { initial: true, transitions: [...flag('PostPricesPrice', 'https://docs.stripe.com/api/prices/update#update_price-active'), { operation: 'DeletePlansPlan', from: ['true'], to: 'false', source: 'https://docs.stripe.com/api/plans/delete' }] } },
659
+ // (deleting a plan, which leaves its price inactive, is the gap here: ../manifest.ts unmodeled)
660
+ state: { active: { initial: true, transitions: [] } },
882
661
  },
883
662
  } satisfies Record<string, { state?: Record<string, StateField>; notState?: string[] }>;