@volter/twin-stripe 3.0.0 → 3.0.2

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 +75 -21
  2. package/dist/journeys/coverage.json +596 -0
  3. package/dist/journeys/customer-life.json +6108 -0
  4. package/dist/journeys/customer-life.outline.md +371 -0
  5. package/dist/journeys/decisions.json +677 -0
  6. package/dist/journeys/demand.json +706 -0
  7. package/dist/journeys/score.json +853 -0
  8. package/dist/journeys/unreachable.json +102 -0
  9. package/dist/journeys/vendor-examples.json +8010 -0
  10. package/dist/spec/SOURCE.md +5 -1
  11. package/dist/spec/doc-examples.json +445 -0
  12. package/dist/spec/sources.json +135 -207
  13. package/dist/src/cli.d.ts +0 -1
  14. package/dist/src/cli.js +0 -2
  15. package/dist/src/engine/after-payment.d.ts +0 -1
  16. package/dist/src/engine/after-payment.js +0 -1
  17. package/dist/src/engine/common.d.ts +5 -5
  18. package/dist/src/engine/common.js +13 -11
  19. package/dist/src/engine/connect.d.ts +0 -21
  20. package/dist/src/engine/connect.js +1 -82
  21. package/dist/src/engine/customers.d.ts +0 -8
  22. package/dist/src/engine/customers.js +0 -22
  23. package/dist/src/engine/display.d.ts +0 -3
  24. package/dist/src/engine/display.js +0 -3
  25. package/dist/src/engine/files.d.ts +0 -7
  26. package/dist/src/engine/files.js +0 -10
  27. package/dist/src/engine/invoices.d.ts +6 -9
  28. package/dist/src/engine/invoices.js +56 -29
  29. package/dist/src/engine/issuing.d.ts +6 -8
  30. package/dist/src/engine/issuing.js +19 -10
  31. package/dist/src/engine/ledger.d.ts +0 -1
  32. package/dist/src/engine/ledger.js +0 -1
  33. package/dist/src/engine/oauth.js +1 -2
  34. package/dist/src/engine/refunds.js +2 -27
  35. package/dist/src/engine/stripe.d.ts +2 -7
  36. package/dist/src/engine/stripe.js +34 -83
  37. package/dist/src/engine/subscriptions.d.ts +0 -1
  38. package/dist/src/engine/subscriptions.js +0 -1
  39. package/dist/src/engine/version.d.ts +0 -2
  40. package/dist/src/engine/version.js +17 -6
  41. package/dist/src/fetch.js +2 -2
  42. package/dist/src/generated/surface.gen.json +1 -1
  43. package/dist/src/generated/ui.gen.json +1 -1
  44. package/dist/src/index.js +5 -3
  45. package/dist/src/manifest.js +348 -220
  46. package/dist/src/screens/billing-portal.js +5 -5
  47. package/dist/src/screens/{connect-oauth.d.ts → checkout-js.d.ts} +0 -1
  48. package/dist/src/screens/checkout-js.js +84 -0
  49. package/dist/src/screens/connect-onboarding.js +3 -3
  50. package/dist/src/screens/dashboard.d.ts +1 -1
  51. package/dist/src/screens/dashboard.js +56 -53
  52. package/dist/src/screens/stripe-js.js +53 -93
  53. package/dist/src/semantics/account.d.ts +2 -0
  54. package/dist/src/semantics/account.js +8 -0
  55. package/dist/src/semantics/accounts.js +3 -1
  56. package/dist/src/semantics/around.js +3 -6
  57. package/dist/src/semantics/billing_portal.d.ts +0 -1
  58. package/dist/src/semantics/billing_portal.js +1 -7
  59. package/dist/src/semantics/checkout.d.ts +1 -1
  60. package/dist/src/semantics/checkout.js +18 -15
  61. package/dist/src/semantics/clock.js +1 -7
  62. package/dist/src/semantics/customers.d.ts +1 -2
  63. package/dist/src/semantics/customers.js +32 -38
  64. package/dist/src/semantics/entitlements.d.ts +0 -2
  65. package/dist/src/semantics/entitlements.js +4 -50
  66. package/dist/src/semantics/events.d.ts +2 -3
  67. package/dist/src/semantics/events.js +6 -11
  68. package/dist/src/semantics/index.js +16 -38
  69. package/dist/src/semantics/invoices.d.ts +3 -8
  70. package/dist/src/semantics/invoices.js +134 -127
  71. package/dist/src/semantics/issuing.d.ts +6 -0
  72. package/dist/src/semantics/issuing.js +77 -0
  73. package/dist/src/semantics/payment_intents.d.ts +0 -1
  74. package/dist/src/semantics/payment_intents.js +5 -13
  75. package/dist/src/semantics/payment_methods.d.ts +0 -2
  76. package/dist/src/semantics/payment_methods.js +2 -26
  77. package/dist/src/semantics/payouts.d.ts +1 -0
  78. package/dist/src/semantics/payouts.js +8 -1
  79. package/dist/src/semantics/products.d.ts +1 -3
  80. package/dist/src/semantics/products.js +3 -14
  81. package/dist/src/semantics/refunds.d.ts +1 -2
  82. package/dist/src/semantics/refunds.js +1 -5
  83. package/dist/src/semantics/shared.d.ts +64 -336
  84. package/dist/src/semantics/shared.js +326 -1336
  85. package/dist/src/semantics/states.d.ts +20 -89
  86. package/dist/src/semantics/states.js +116 -334
  87. package/dist/src/semantics/subscriptions.d.ts +1 -2
  88. package/dist/src/semantics/subscriptions.js +23 -145
  89. package/dist/src/semantics/test_helpers.d.ts +2 -5
  90. package/dist/src/semantics/test_helpers.js +121 -43
  91. package/dist/src/semantics/webhook_endpoints.d.ts +2 -0
  92. package/dist/src/semantics/webhook_endpoints.js +26 -0
  93. package/generated/pack-facts.json +3 -3
  94. package/journeys/coverage.json +596 -0
  95. package/journeys/customer-life.json +6108 -0
  96. package/journeys/customer-life.outline.md +371 -0
  97. package/journeys/decisions.json +677 -0
  98. package/journeys/demand.json +706 -0
  99. package/journeys/score.json +853 -0
  100. package/journeys/unreachable.json +102 -0
  101. package/journeys/vendor-examples.json +8010 -0
  102. package/package.json +11 -10
  103. package/spec/SOURCE.md +5 -1
  104. package/spec/doc-examples.json +445 -0
  105. package/spec/sources.json +135 -207
  106. package/src/cli.ts +0 -3
  107. package/src/engine/after-payment.ts +0 -2
  108. package/src/engine/balance.ts +0 -1
  109. package/src/engine/checkout.ts +0 -1
  110. package/src/engine/common.ts +19 -17
  111. package/src/engine/connect.ts +1 -79
  112. package/src/engine/customers.ts +0 -25
  113. package/src/engine/display.ts +2 -3
  114. package/src/engine/files.ts +0 -13
  115. package/src/engine/invoices.ts +64 -33
  116. package/src/engine/issuing.ts +16 -11
  117. package/src/engine/ledger.ts +0 -2
  118. package/src/engine/oauth.ts +1 -2
  119. package/src/engine/onboarding.ts +0 -1
  120. package/src/engine/params.ts +0 -1
  121. package/src/engine/payment-intents.ts +0 -1
  122. package/src/engine/payment-methods.ts +0 -1
  123. package/src/engine/refunds.ts +2 -28
  124. package/src/engine/renewals.ts +0 -1
  125. package/src/engine/stripe.ts +35 -83
  126. package/src/engine/subscriptions.ts +0 -3
  127. package/src/engine/transfers.ts +0 -1
  128. package/src/engine/version.ts +15 -7
  129. package/src/engine/webhooks.ts +0 -2
  130. package/src/fetch.ts +2 -2
  131. package/src/generated/surface.gen.json +1 -1
  132. package/src/generated/ui.gen.json +1 -1
  133. package/src/index.ts +6 -4
  134. package/src/manifest.ts +348 -220
  135. package/src/screens/billing-portal.tsx +5 -7
  136. package/src/screens/checkout-js.ts +80 -0
  137. package/src/screens/checkout.tsx +0 -2
  138. package/src/screens/connect-onboarding.tsx +3 -5
  139. package/src/screens/dashboard.tsx +55 -73
  140. package/src/screens/stripe-js.ts +57 -125
  141. package/src/semantics/account.ts +10 -0
  142. package/src/semantics/account_links.ts +0 -1
  143. package/src/semantics/accounts.ts +8 -7
  144. package/src/semantics/apps.ts +1 -2
  145. package/src/semantics/around.ts +3 -6
  146. package/src/semantics/balance.ts +0 -1
  147. package/src/semantics/balance_transactions.ts +1 -3
  148. package/src/semantics/billing_portal.ts +1 -7
  149. package/src/semantics/charges.ts +1 -6
  150. package/src/semantics/checkout.ts +16 -12
  151. package/src/semantics/clock.ts +1 -8
  152. package/src/semantics/coupons.ts +0 -1
  153. package/src/semantics/customers.ts +32 -35
  154. package/src/semantics/doors.ts +2 -3
  155. package/src/semantics/entitlements.ts +1 -45
  156. package/src/semantics/events.ts +7 -14
  157. package/src/semantics/index.ts +16 -38
  158. package/src/semantics/invoice_payments.ts +1 -3
  159. package/src/semantics/invoices.ts +124 -119
  160. package/src/semantics/issuing.ts +74 -0
  161. package/src/semantics/payment_intents.ts +10 -17
  162. package/src/semantics/payment_methods.ts +2 -25
  163. package/src/semantics/payouts.ts +9 -3
  164. package/src/semantics/prices.ts +0 -1
  165. package/src/semantics/products.ts +4 -14
  166. package/src/semantics/promotion_codes.ts +0 -1
  167. package/src/semantics/radar.ts +1 -3
  168. package/src/semantics/refunds.ts +2 -11
  169. package/src/semantics/shared.ts +350 -1382
  170. package/src/semantics/states.ts +109 -330
  171. package/src/semantics/subscriptions.ts +25 -145
  172. package/src/semantics/test_helpers.ts +109 -43
  173. package/src/semantics/transfers.ts +1 -3
  174. package/src/semantics/webhook_endpoints.ts +27 -0
  175. package/src/server.ts +0 -1
  176. package/dist/generated/pack-facts.json +0 -94
  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
@@ -3,10 +3,9 @@
3
3
  // sessions. A new connected account is not yet active: charges and payouts are off and its
4
4
  // requirements are outstanding, as on Stripe before onboarding. Account retrieve and delete and
5
5
  // the person list are the derived core's.
6
- import { accountRequirements, accountSettings, asBool, PLATFORM_ACCOUNT_ID } from './stripe.ts';
6
+ import { accountRequirements, accountSettings, PLATFORM_ACCOUNT_ID } from './stripe.ts';
7
7
  import { currentlyDue, kindOf } from './onboarding.ts';
8
8
  import { externalList, type Row } from './common.ts';
9
-
10
9
  // the platform's own account: stored once it is written, otherwise Stripe's default. It "controls itself" (controller
11
10
  // type `account`, docs.stripe.com/api/accounts/object), is fully onboarded with nothing due, and takes card payments
12
11
  // and transfers. Where the documentation stops and the twin decides: it was created as the World began (2026-01-01),
@@ -64,8 +63,6 @@ export const at_path = (a: Row, path: string): unknown => path.split('.').reduce
64
63
  export const CARD_COMPANY_NOW = ['business_profile.mcc', 'company.address.line1', 'company.address.city', 'company.address.state', 'company.address.postal_code', 'company.owners_provided', 'owners.first_name', 'owners.last_name', 'owners.email', 'representative.first_name', 'representative.last_name', 'representative.email', 'representative.address.line1', 'representative.address.city', 'representative.address.state', 'representative.address.postal_code', 'representative.relationship.title', 'representative.ssn_last_4'];
65
64
  export const CARD_COMPANY_LATER = ['company.phone', 'settings.payments.statement_descriptor', 'representative.dob.day', 'representative.dob.month', 'representative.dob.year', 'representative.phone'];
66
65
  export const CARD_INDIVIDUAL_NOW = ['business_profile.mcc', 'individual.address.line1', 'individual.address.city', 'individual.address.state', 'individual.address.postal_code', 'individual.email', 'individual.ssn_last_4'];
67
- /** A field only card_payments asks for (the transfers capability's requirements leave it out). */
68
- export const cardOnly = (field: string): boolean => [...CARD_COMPANY_NOW, ...CARD_COMPANY_LATER, ...CARD_INDIVIDUAL_NOW].includes(field);
69
66
  export const personHas = (p: Row, field: string): boolean => {
70
67
  if (field === 'ssn_last_4') return p.ssn_last_4_provided === true;
71
68
  const v = at_path(p, field);
@@ -107,78 +104,3 @@ export function keptKyc(params: Row): Row {
107
104
  }
108
105
  return out;
109
106
  }
110
-
111
- /** A person's details as Stripe keeps them: an SSN or ID number given is kept only as provided (the Person object
112
- * answers ssn_last_4_provided and id_number_provided, never the numbers; docs.stripe.com/api/persons/object). */
113
- export function keptPerson(params: Row, was: Row = {}): Row {
114
- const { ssn_last_4, id_number, ...rest } = params;
115
- const out: Row = { ...rest, ...(ssn_last_4 !== undefined && ssn_last_4 !== '' ? { ssn_last_4_provided: true } : {}), ...(id_number !== undefined && id_number !== '' ? { id_number_provided: true } : {}) };
116
- // a relationship, address or date of birth given in part is laid over what the person holds
117
- for (const k of ['relationship', 'address', 'dob'] as const) if (out[k] && typeof out[k] === 'object' && was[k] && typeof was[k] === 'object') out[k] = { ...(was[k] as Row), ...(out[k] as Row) };
118
- return out;
119
- }
120
- export const RELATIONSHIP: Row = { director: false, executive: false, owner: false, representative: false, percent_ownership: null, title: null };
121
-
122
- // ── capabilities: the account's capability map is the truth; requesting one leaves it pending ──
123
-
124
- /** A capability as its account stands: its status, and the account's requirements that capability asks for (transfers
125
- * leaves out what only card_payments asks for). Where the documentation stops and the twin decides: the account-wide
126
- * fields (the bank account, the terms) belong to every capability. */
127
- export const capabilityBody = (acct: Row, id: string, status: unknown, requested = true, requestedAt: unknown = null): Row => {
128
- const req = (acct.requirements as Row | undefined) ?? {};
129
- const mine = (list: unknown): string[] => (Array.isArray(list) ? (list as string[]) : []).filter((f) => id === 'card_payments' || !cardOnly(f));
130
- const due = mine(req.currently_due);
131
- return {
132
- id, object: 'capability', account: acct.id, status, requested, requested_at: requestedAt,
133
- requirements: { currently_due: due, eventually_due: mine(req.eventually_due), past_due: [], pending_verification: [], errors: [], alternatives: [], current_deadline: null, disabled_reason: due.length ? 'requirements.past_due' : null },
134
- };
135
- };
136
-
137
- // a session is identified by its client_secret: the row is kept for traceability, the answer has no id
138
- /** Every embedded component an account session answers (the served spec requires all of them), each enabled only when
139
- * the request enables it, with its features: what the request gives, else the spec's stated default, "The default
140
- * value for this feature is `true`" (external_account_collection); disable_stripe_user_authentication "the opposite of
141
- * the `external_account_collection` value"; edit_payout_schedule and standard_payouts "Defaults to `true` when
142
- * `controller.losses.payments` is set to `stripe` for the account, otherwise `false`"; capture_payments, dispute_management
143
- * and refund_management "true by default"; destination_on_behalf_of_charge_management "false by default";
144
- * smart_disputes_management "Defaults to the value of `dispute_management`". Where the documentation stops and the twin
145
- * decides: instant_payouts (described as `enabled` "when Stripe is responsible for negative account balances", typed
146
- * boolean) is true exactly then, and the Issuing and financial-account features, which state no default, are false. */
147
- export const SESSION_COMPONENTS: Record<string, string[]> = {
148
- account_management: ['external_account_collection', 'disable_stripe_user_authentication'],
149
- account_onboarding: ['external_account_collection', 'disable_stripe_user_authentication'],
150
- balance_report: [], documents: [], payout_details: [], payout_reconciliation_report: [], payouts_list: [], tax_registrations: [], tax_settings: [],
151
- balances: ['external_account_collection', 'disable_stripe_user_authentication', 'edit_payout_schedule', 'instant_payouts', 'standard_payouts'],
152
- payouts: ['external_account_collection', 'disable_stripe_user_authentication', 'edit_payout_schedule', 'instant_payouts', 'standard_payouts'],
153
- disputes_list: ['capture_payments', 'destination_on_behalf_of_charge_management', 'dispute_management', 'refund_management', 'smart_disputes_management'],
154
- payment_details: ['capture_payments', 'destination_on_behalf_of_charge_management', 'dispute_management', 'refund_management', 'smart_disputes_management'],
155
- payments: ['capture_payments', 'destination_on_behalf_of_charge_management', 'dispute_management', 'refund_management', 'smart_disputes_management'],
156
- payment_disputes: ['destination_on_behalf_of_charge_management', 'dispute_management', 'refund_management', 'smart_disputes_management'],
157
- financial_account: ['external_account_collection', 'disable_stripe_user_authentication', 'send_money', 'transfer_balance'],
158
- financial_account_transactions: ['card_spend_dispute_management'],
159
- instant_payouts_promotion: ['external_account_collection', 'disable_stripe_user_authentication', 'instant_payouts'],
160
- issuing_card: ['card_management', 'card_spend_dispute_management', 'cardholder_management', 'spend_control_management'],
161
- issuing_cards_list: ['card_management', 'card_spend_dispute_management', 'cardholder_management', 'disable_stripe_user_authentication', 'spend_control_management'],
162
- notification_banner: ['external_account_collection', 'disable_stripe_user_authentication'],
163
- payment_method_settings: ['disable_stripe_user_authentication'],
164
- };
165
- export function sessionComponents(account: Row, given: Row): Row {
166
- const stripeLosses = (((account.controller as Row | undefined)?.losses as Row | undefined)?.payments) === 'stripe';
167
- const out: Row = {};
168
- for (const [name, featureNames] of Object.entries(SESSION_COMPONENTS)) {
169
- const c = given[name] && typeof given[name] === 'object' ? (given[name] as Row) : {};
170
- const asked = c.features && typeof c.features === 'object' ? (c.features as Row) : {};
171
- const f: Row = {};
172
- const val = (k: string, fallback: boolean): boolean => (asked[k] !== undefined ? asBool(asked[k]) : fallback);
173
- for (const k of featureNames) {
174
- if (k === 'external_account_collection') f[k] = val(k, true);
175
- else if (k === 'disable_stripe_user_authentication') f[k] = val(k, !val('external_account_collection', true));
176
- else if (k === 'edit_payout_schedule' || k === 'standard_payouts' || k === 'instant_payouts') f[k] = val(k, stripeLosses);
177
- else if (k === 'capture_payments' || k === 'dispute_management' || k === 'refund_management') f[k] = val(k, true);
178
- else if (k === 'smart_disputes_management') f[k] = val(k, val('dispute_management', true));
179
- else f[k] = val(k, false);
180
- }
181
- out[name] = { enabled: c.enabled !== undefined ? asBool(c.enabled) : false, features: f };
182
- }
183
- return out;
184
- }
@@ -3,33 +3,8 @@
3
3
  // update and delete of a customer, retrieve of a Source and the flat tax-id list are the derived core's.
4
4
  import { invoicePrefix } from './invoices.ts';
5
5
  import type { Row } from './common.ts';
6
-
7
6
  /** What a new customer answers before any invoice: its invoice_prefix and a next_invoice_sequence of 1, a zero balance,
8
7
  * no metadata (docs.stripe.com/api/customers/object). */
9
8
  export const newCustomer = (id: string): Row => ({ invoice_prefix: invoicePrefix(id), next_invoice_sequence: 1, balance: 0, metadata: {}, invoice_settings: {} });
10
9
 
11
10
  // ── tax ids: a customer's, keyed under it; `type` and `value` are required ──
12
-
13
- // An EU VAT number is its country's prefix and that country's format (the European Commission's VIES formats,
14
- // ec.europa.eu/taxation_customs/vies); Stripe refuses one that does not fit as tax_id_invalid
15
- // (docs.stripe.com/error-codes#tax-id-invalid). The message's wording is the twin's.
16
- export const EU_VAT: Record<string, RegExp> = {
17
- AT: /^U\d{8}$/, BE: /^[01]\d{9}$/, BG: /^\d{9,10}$/, CY: /^\d{8}[A-Z]$/, CZ: /^\d{8,10}$/, DE: /^\d{9}$/, DK: /^\d{8}$/,
18
- EE: /^\d{9}$/, EL: /^\d{9}$/, ES: /^[A-Z0-9]\d{7}[A-Z0-9]$/, FI: /^\d{8}$/, FR: /^[A-Z0-9]{2}\d{9}$/, HR: /^\d{11}$/,
19
- HU: /^\d{8}$/, IE: /^(\d{7}[A-W][A-I]?|\d[A-Z+*]\d{5}[A-W])$/, IT: /^\d{11}$/, LT: /^(\d{9}|\d{12})$/, LU: /^\d{8}$/,
20
- LV: /^\d{11}$/, MT: /^\d{8}$/, NL: /^\d{9}B\d{2}$/, PL: /^\d{10}$/, PT: /^\d{9}$/, RO: /^\d{2,10}$/, SE: /^\d{12}$/,
21
- SI: /^\d{8}$/, SK: /^\d{10}$/, XI: /^(\d{9}|\d{12}|GD\d{3}|HA\d{3})$/,
22
- };
23
- export const euVatCountry = (value: string): string | undefined => {
24
- const v = value.replace(/[\s.-]/g, '').toUpperCase();
25
- const prefix = v.slice(0, 2);
26
- const format = EU_VAT[prefix];
27
- return format && format.test(v.slice(2)) ? (prefix === 'EL' ? 'GR' : prefix) : undefined;
28
- };
29
-
30
- /** The tax ID types Stripe checks against a government register: "Stripe automatically validates all Australian
31
- * Business Numbers (ABNs)", "all European Value-Added-Tax (EU VAT) numbers" (VIES) and "all UK Value-Added-Tax (GB VAT)
32
- * numbers" (HMRC); a verification's status "is pending, verified, unverified, or unavailable", and "When automatic
33
- * validation is unavailable, you must manually verify the tax ID" (docs.stripe.com/billing/customer/tax-ids). Any other
34
- * type answers unavailable; one of these waits pending. */
35
- export const AUTO_VALIDATED = new Set(['au_abn', 'eu_vat', 'gb_vat']);
@@ -1,5 +1,7 @@
1
1
  // What Stripe's pages show of an account, and where its Connect OAuth connections are kept.
2
2
 
3
+ // What Stripe's pages show of an account, and where its Connect OAuth connections are kept.
4
+
3
5
  type Row = Record<string, any>;
4
6
 
5
7
  /** The name an account shows its customers (Checkout, the customer portal): its public business name,
@@ -10,9 +12,6 @@ type Row = Record<string, any>;
10
12
  * Dashboard to differentiate between accounts", the same object page), and with neither to "Twin Inc.", the name the
11
13
  * twin has always given the World's own account. */
12
14
  export const PLATFORM_DEFAULT_NAME = 'Twin Inc.';
13
- /** The bookkeeping type of a connected account's Connect OAuth connection (../screens/connect-oauth.tsx): one row per
14
- * account, `revoked: true` once deauthorized or its code reused. Read by the account list and the event path too. */
15
- export const OAUTH_CONNECTIONS = '_oauth_connection';
16
15
  export function publicBusinessName(account: Row | undefined): string {
17
16
  const named = (v: unknown): string | undefined => (typeof v === 'string' && v.trim() ? v : undefined);
18
17
  return named(account?.business_profile?.name) ?? named(account?.settings?.dashboard?.display_name) ?? PLATFORM_DEFAULT_NAME;
@@ -4,19 +4,6 @@
4
4
  // are the derived core's to list and read, as are file list and retrieve, session retrieve and
5
5
  // portal-configuration retrieve.
6
6
  import type { Row } from './common.ts';
7
-
8
- /** The type Stripe answers for an uploaded file: "The returned file type (for example, csv, pdf, jpg, or png)"
9
- * (docs.stripe.com/api/files/object). Where the documentation stops and the twin decides: read from the name's
10
- * extension (jpeg as jpg), and null for a name without one. */
11
- export function fileType(name: string): string | null {
12
- const ext = /\.([a-z0-9]+)$/i.exec(name)?.[1]?.toLowerCase();
13
- return ext ? (ext === 'jpeg' ? 'jpg' : ext) : null;
14
- }
15
-
16
- /** The purposes a file link may be made to: "The file's `purpose` must be one of the following: …" (the served
17
- * spec, POST /v1/file_links `file`). */
18
- export const LINKABLE = ['business_icon', 'business_logo', 'customer_signature', 'dispute_evidence', 'finance_report_run', 'financial_account_statement', 'identity_document_downloadable', 'issuing_regulatory_reporting', 'pci_document', 'selfie', 'sigma_scheduled_query', 'tax_document_user_upload', 'terminal_android_apk', 'terminal_reader_splashscreen'] as const;
19
-
20
7
  export const PBC = 'billing_portal.configuration';
21
8
 
22
9
  /** A portal configuration's features, each filled where the request leaves it: every feature is answered (the served
@@ -4,41 +4,9 @@
4
4
  // items, and mint the PaymentIntent a finalized invoice collects through. The invoice list and the
5
5
  // item list are the derived core's.
6
6
  import type { Row } from './common.ts';
7
+ import { applyCouponDiscount } from './stripe.ts';
7
8
 
8
9
  export const INV = 'invoice';
9
- export const linesOf = (inv: Row): Row[] => (inv.lines as { data?: Row[] } | undefined)?.data ?? [];
10
- export const sumLines = (lines: Row[]): number => lines.reduce((s, l) => s + (Number(l.amount) || 0), 0);
11
-
12
- /** A draft's line no invoice item made (a subscription's) stays as it is when an item changes. */
13
- export function unlinkedLine(line: Row): Row {
14
- return line;
15
- }
16
-
17
- /** A preview's proration for a change at proration_date. Where the documentation stops and the twin decides: half the
18
- * period's charge credited as unused time and half charged as remaining time. */
19
- export function prorationLines(out: Row[], subtotal: number, currency: string, now: number, period: number): void {
20
- const half = Math.round(subtotal / 2);
21
- out.unshift({ id: 'il_twin_proration_unused', object: 'line_item', type: 'invoiceitem', amount: -half, currency, proration: true, period: { start: now, end: period }, description: 'Unused time (proration credit)' });
22
- out.push({ id: 'il_twin_proration_remaining', object: 'line_item', type: 'invoiceitem', amount: half, currency, proration: true, period: { start: now, end: period }, description: 'Remaining time (proration charge)' });
23
- }
24
-
25
- // ── invoices ──
26
-
27
- /** A draft invoice's fields before anything is on it: what a new invoice and a preview start from. */
28
- export const draft = (now: number): Row => ({
29
- status: 'draft', livemode: false, currency: 'usd', collection_method: 'charge_automatically',
30
- auto_advance: false, attempt_count: 0, attempted: false,
31
- amount_due: 0, amount_paid: 0, amount_remaining: 0, amount_overpaid: 0, amount_paid_off_stripe: 0, amount_shipping: 0,
32
- subtotal: 0, total: 0, starting_balance: 0, post_payment_credit_notes_amount: 0, pre_payment_credit_notes_amount: 0,
33
- period_start: now, period_end: now, default_tax_rates: [], discounts: [],
34
- lines: { object: 'list', data: [], has_more: false, total_count: 0 },
35
- automatic_tax: { enabled: false, liability: null, status: null },
36
- status_transitions: { finalized_at: null, marked_uncollectible_at: null, paid_at: null, voided_at: null },
37
- issuer: { type: 'self' },
38
- payment_settings: { default_mandate: null, payment_method_options: null, payment_method_types: null },
39
- // a draft has no PaymentIntent: finalize mints it
40
- payment_intent: null,
41
- });
42
10
 
43
11
  /** Whether leaving draft mints a PaymentIntent: only a charge_automatically invoice with a balance. */
44
12
  // "Finalizing an invoice does the following: ... It creates an incomplete payment intent for the invoice"
@@ -69,3 +37,66 @@ export function invoicePayment(inv: Row): Row | undefined {
69
37
  status_transitions: { paid_at: paid ? (transitions.paid_at ?? null) : null, canceled_at: inv.status === 'void' ? (transitions.voided_at ?? null) : null },
70
38
  };
71
39
  }
40
+
41
+
42
+ export const linesOf = (inv: Row): Row[] => (inv.lines as { data?: Row[] } | undefined)?.data ?? [];
43
+
44
+
45
+ export const sumLines = (lines: Row[]): number => lines.reduce((s, l) => s + (Number(l.amount) || 0), 0);
46
+
47
+
48
+
49
+ /** Credit the previous price and debit the replacement for the same remaining service time. */
50
+ // source: https://docs.stripe.com/billing/subscriptions/prorations "Unused time on original 10 USD plan (credit)"
51
+ // source: https://docs.stripe.com/billing/subscriptions/prorations "Remaining time on new 20 USD plan (debit)"
52
+ // source: https://docs.stripe.com/billing/subscriptions/prorations "The current billing period’s start and end times are used to calculate the cost of the subscription before and after the change."
53
+ // source: https://docs.stripe.com/billing/subscriptions/prorations "By default, Stripe calculates prorations down to the second."
54
+ export function prorationLines(before: Row[], after: Row[], time: number, start: number, end: number, coupon: Row | undefined): Row[] {
55
+ const out: Row[] = [];
56
+ const discounted = (line: Row, lines: Row[]): number => {
57
+ const total = sumLines(lines);
58
+ // source: https://docs.stripe.com/billing/subscriptions/prorations "When calculating proration credits or debits, Stripe uses the subscription’s discounted price, not the original price."
59
+ return Number(line.amount) - (total > 0 ? applyCouponDiscount(total, coupon) * Number(line.amount) / total : 0);
60
+ };
61
+ const priceId = (line: Row): unknown => typeof line.price === 'object' ? (line.price as Row).id : line.price;
62
+ const append = (line: Row, lines: Row[], credit: boolean): void => {
63
+ const billingPeriod = line.period as Row;
64
+ const periodStart = Number(billingPeriod.start ?? start), periodEnd = Number(billingPeriod.end ?? end);
65
+ const fraction = Math.max(0, Math.min(1, (periodEnd - time) / Math.max(1, periodEnd - periodStart)));
66
+ const amount = Math.round(discounted(line, lines) * fraction) * (credit ? -1 : 1);
67
+ if (!amount) return;
68
+ out.push({ ...line, id: `${line.id}_${credit ? 'unused' : 'remaining'}`, amount, proration: true, discountable: false,
69
+ period: { start: time, end: periodEnd }, description: `${credit ? 'Unused' : 'Remaining'} time on ${String((line.price as Row)?.nickname ?? priceId(line))}` });
70
+ };
71
+ for (const old of before) {
72
+ const replacement = after.find(line => line.subscription_item === old.subscription_item);
73
+ if (replacement && priceId(replacement) === priceId(old) && replacement.quantity === old.quantity) continue;
74
+ append(old, before, true);
75
+ }
76
+ for (const next of after) {
77
+ const previous = before.find(line => line.subscription_item && line.subscription_item === next.subscription_item);
78
+ if (previous && priceId(previous) === priceId(next) && previous.quantity === next.quantity) continue;
79
+ append(next, after, false);
80
+ }
81
+ return out;
82
+ }
83
+
84
+
85
+
86
+ // ── invoices ──
87
+
88
+ /** A draft invoice's fields before anything is on it: what a new invoice and a preview start from. */
89
+ export const draft = (now: number): Row => ({
90
+ status: 'draft', livemode: false, currency: 'usd', collection_method: 'charge_automatically',
91
+ auto_advance: false, attempt_count: 0, attempted: false,
92
+ amount_due: 0, amount_paid: 0, amount_remaining: 0, amount_overpaid: 0, amount_paid_off_stripe: 0, amount_shipping: 0,
93
+ subtotal: 0, total: 0, starting_balance: 0, post_payment_credit_notes_amount: 0, pre_payment_credit_notes_amount: 0,
94
+ period_start: now, period_end: now, default_tax_rates: [], discounts: [],
95
+ lines: { object: 'list', data: [], has_more: false, total_count: 0 },
96
+ automatic_tax: { enabled: false, liability: null, status: null },
97
+ status_transitions: { finalized_at: null, marked_uncollectible_at: null, paid_at: null, voided_at: null },
98
+ issuer: { type: 'self' },
99
+ payment_settings: { default_mandate: null, payment_method_options: null, payment_method_types: null },
100
+ // a draft has no PaymentIntent: finalize mints it
101
+ payment_intent: null,
102
+ });
@@ -9,7 +9,6 @@
9
9
  // (card, then cardholder), then the real-time issuing_authorization.request webhook delivered
10
10
  // synchronously to the first enrolled endpoint; with none it is approved (card_active).
11
11
  import type { Row } from './common.ts';
12
-
13
12
  export const CH = 'issuing.cardholder';
14
13
  /** The id of a card's cardholder: the card answers the cardholder in full (the served spec's issuing.card.cardholder is
15
14
  * the Cardholder object, not an id); its id is read from it for filtering and for the objects that name it by id. */
@@ -17,9 +16,6 @@ export const holderId = (c: Row): string => (typeof c.cardholder === 'string' ?
17
16
  export const CARD = 'issuing.card';
18
17
  export const AUTH = 'issuing.authorization';
19
18
  export const TXN = 'issuing.transaction';
20
- export const DSP = 'issuing.dispute';
21
- export const TOK = 'issuing.token';
22
- export const PD = 'issuing.personalization_design';
23
19
 
24
20
  /** One request_history entry; the authorization code is derived from the request time (Stripe's is not unique either). */
25
21
  export function historyEntry(o: { amount: number; currency: string; approved: boolean; reason: string; reasonMessage: string | null; createdSec: number; merchantAmount: number; merchantCurrency: string }): Row {
@@ -52,10 +48,19 @@ export const cardId = (auth: Row): unknown => (auth.card && typeof auth.card ===
52
48
  * (docs.stripe.com/issuing/controls/real-time-authorizations). */
53
49
  export const REALTIME_WINDOW_SECONDS = 2;
54
50
 
55
- /** Stripe's physical bundles are its catalog, not the account's: the twin serves the one its reference publishes
56
- * (docs.stripe.com/api/issuing/physical_bundles: "US Visa Credit White", standard, active, a card logo required and
57
- * carrier text optional). Where the documentation stops and the twin decides: the catalog is that one bundle, and its
58
- * second_line (required by the served spec, absent from the example) is optional. */
59
- export const PHYSICAL_BUNDLES: Row[] = [
60
- { id: 'ics_NLuXJPDYSTjFON', object: 'issuing.physical_bundle', livemode: false, name: 'US Visa Credit White', features: { card_logo: 'required', carrier_text: 'optional', second_line: 'optional' }, status: 'active', type: 'standard' },
61
- ];
51
+ /** A virtual card's number, drawn from the World: a 16-digit Visa number ("The brand of the card": the twin issues
52
+ * Visa) whose last digit is the Luhn check digit every card number carries. Where the documentation stops and the twin
53
+ * decides: the digits are the World's draw, Stripe's are the issuer's. */
54
+ export function cardNumberOf(drawn: string): string {
55
+ const body = `4${[...drawn].map((ch) => String(ch.charCodeAt(0) % 10)).join('').padEnd(14, '0').slice(0, 14)}`;
56
+ let sum = 0;
57
+ for (let i = 0; i < body.length; i++) {
58
+ let d = Number(body[body.length - 1 - i]);
59
+ if (i % 2 === 0) { d *= 2; if (d > 9) d -= 9; }
60
+ sum += d;
61
+ }
62
+ return `${body}${(10 - (sum % 10)) % 10}`;
63
+ }
64
+
65
+ /** A virtual card's CVC, drawn from the World: three digits. */
66
+ export const cvcOf = (drawn: string): string => [...drawn].map((ch) => String(ch.charCodeAt(0) % 10)).join('').padEnd(3, '0').slice(0, 3);
@@ -37,9 +37,7 @@
37
37
  // connected account to the platform.
38
38
  import type { Row } from './common.ts';
39
39
  import { BYPASS_PENDING_CARDS } from './test-cards.ts';
40
-
41
40
  export const BT = 'balance_transaction';
42
- export const DAY = 86_400;
43
41
  /** A ledger entry's mark: available at once, its balance.available not yet sent (settleDueEntries). */
44
42
  export const UNSENT = '_availableUnsent';
45
43
  export const BYPASS_NUMBERS = new Set(Object.values(BYPASS_PENDING_CARDS).map((c) => c.number));
@@ -1,5 +1,4 @@
1
- // A connected account's OAuth connection to the platform (../screens/connect-oauth.tsx makes it), as the API reads it:
2
- // whether it was revoked, which account an OAuth key acts as, and a key shown as Stripe's errors show one.
1
+ // Stripe API key parsing and error redaction, from the API authentication documentation.
3
2
 
4
3
  /** A key shown the way Stripe's errors show one: its prefix and its last four. */
5
4
  export const redactKey = (key: string): string => {
@@ -2,7 +2,6 @@
2
2
  // ../screens/connect-onboarding.tsx), as the Accounts API reports them (`requirements.currently_due`): the two US sets the twin
3
3
  // models, a company's and an individual's.
4
4
  import { INDUSTRIES as MCCS } from './industries.ts';
5
-
6
5
  type Row = Record<string, unknown>;
7
6
 
8
7
  export const OFFERED = new Set(MCCS.map(([mcc]) => mcc));
@@ -11,7 +11,6 @@
11
11
  // the values of the payment_method_types it still takes (PAYMENT_METHOD_TYPES below); nested parameters are not checked.
12
12
 
13
13
  import surface from '../generated/surface.gen.json' with { type: 'json' };
14
-
15
14
  type Param = { name: string; required?: boolean };
16
15
  type Op = { id: string; method: string; query?: Param[]; body?: Param[] };
17
16
  export const OPS = new Map((surface.operations as Op[]).map((o) => [o.id, o]));
@@ -2,7 +2,6 @@
2
2
  // what Stripe answers otherwise; these handlers compute what each move does. List, retrieve and
3
3
  // update are the derived core's; search is Stripe's query language over the intents.
4
4
  import type { Row } from './common.ts';
5
-
6
5
  export const PI = 'payment_intent';
7
6
  /** Bookkeeping: the intents whose caller named the methods (payment_method_types, allowed_payment_method_types) on
8
7
  * create. The twin's default list (['card']) stands for Stripe's choice and restricts nothing, so only a named list
@@ -3,7 +3,6 @@
3
3
  import { paymentMethodSubObject } from './stripe.ts';
4
4
  import { AFTER_SUCCESS_CARDS } from './after-payment.ts';
5
5
  import type { Row } from './common.ts';
6
-
7
6
  export const PM = 'payment_method';
8
7
  export const noBilling = { address: null, email: null, name: null, phone: null };
9
8
 
@@ -1,29 +1,3 @@
1
- // Refund semantics. A refund always names what it took money out of and lands there: a refund
2
- // naming nothing, an id nobody minted, or more than the charge has left is refused as Stripe
3
- // refuses it. The machine in ../manifest.ts says only a refund waiting on the customer
4
- // (requires_action) may be canceled. List and update are the derived core's.
5
- //
6
- // Refunding a destination charge can take back the connected account's share (reverse_transfer: a reversal of the
7
- // charge's transfer, in proportion to the refund) and the platform's (refund_application_fee: a refund of the
8
- // application fee, in proportion) (docs.stripe.com/api/refunds/create#create_refund-reverse_transfer,
9
- // docs.stripe.com/connect/destination-charges#issue-refunds). Where the documentation stops and the twin decides: the
10
- // twin's destination charge transfers the amount less the fee (the fee is kept back rather than collected from the
11
- // connected account), so a fee refund moves no money of its own; the reversal and the refund settle both shares.
12
- //
13
- // A payment made by bank transfer (from the customer's cash balance) is refunded to the customer's bank account: Stripe
14
- // emails the customer (instructions_email, else the customer's email) for their bank details and the refund waits in
15
- // requires_action, moving no money, until they answer; only then can it still be canceled. With origin=customer_balance
16
- // it goes back to the cash balance at once (docs.stripe.com/payments/customer-balance/refunding,
17
- // docs.stripe.com/api/refunds/cancel). Where the documentation stops and the twin decides: the customer never answers
18
- // the email in the World, and its link expires 30 days on.
19
- //
20
- // An uncaptured charge holds an authorization, not money. A PaymentIntent's is not refunded: "the charge attached to the
21
- // PaymentIntent remains uncaptured and can't be refunded directly. You must cancel the PaymentIntent"
22
- // (docs.stripe.com/refunds), which releases it (charges.ts cancelAuthorization). A charge made with capture=false is
23
- // released by its refund, as its authorization would be "automatically refunded if uncaptured" (spec/openapi.json.gz,
24
- // `capture_before`): refunded in full, no balance moved, and its capture refused afterwards. Where the documentation
25
- // stops and the twin decides: the refusal's code (payment_intent_unexpected_state) and wording, and that a refund of a
26
- // capture=false charge releases the whole authorization, so an `amount` short of it is refused.
27
-
28
- // the reasons a caller may give; `expired_uncaptured_charge` is Stripe's own
1
+ // The published refund reasons; expired_uncaptured_charge is Stripe's own, not a request reason.
2
+ // source: spec:/paths/~1v1~1refunds/post/requestBody/content/application~1x-www-form-urlencoded/schema/properties/reason "String indicating the reason for the refund."
29
3
  export const REFUND_REASONS = new Set(['duplicate', 'fraudulent', 'requested_by_customer']);
@@ -25,7 +25,6 @@
25
25
  // (docs.stripe.com/billing/subscriptions/cancel#cancel-at-end-of-cycle).
26
26
  import { priceAmount } from './stripe.ts';
27
27
  import type { Row } from './common.ts';
28
-
29
28
  export const SUB = 'subscription';
30
29
  export const INV = 'invoice';
31
30
  export const HOUR = 3600;