@volter/twin-stripe 0.1.1 → 2.0.0

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 (219) hide show
  1. package/README.md +64 -27
  2. package/client/dashboard-api.ts +286 -0
  3. package/client/stripe-mirror.css +272 -159
  4. package/client/stripe-mirror.tsx +1384 -541
  5. package/dist/client/dashboard-api.d.ts +107 -0
  6. package/dist/client/dashboard-api.js +238 -0
  7. package/dist/client/dashboard-api.ts +286 -0
  8. package/dist/client/stripe-mirror.bundle.js +236 -0
  9. package/dist/client/stripe-mirror.css +275 -0
  10. package/dist/client/stripe-mirror.d.ts +134 -0
  11. package/dist/client/stripe-mirror.js +823 -0
  12. package/dist/client/stripe-mirror.tsx +1534 -0
  13. package/dist/src/cli.d.ts +2 -0
  14. package/dist/src/cli.js +39 -0
  15. package/dist/src/generated/events.gen.json +1 -0
  16. package/dist/src/generated/surface.gen.json +1 -0
  17. package/dist/src/generated/ui.gen.json +1 -0
  18. package/dist/src/index.d.ts +14 -0
  19. package/dist/src/index.js +73 -0
  20. package/dist/src/manifest.d.ts +2 -0
  21. package/dist/src/manifest.js +1065 -0
  22. package/dist/src/screens/checkout.d.ts +31 -0
  23. package/dist/src/screens/checkout.js +241 -0
  24. package/dist/src/screens/consent-skin.d.ts +4 -0
  25. package/dist/src/screens/consent-skin.js +18 -0
  26. package/dist/src/screens/financial-connections.d.ts +5 -0
  27. package/dist/src/screens/financial-connections.js +90 -0
  28. package/dist/src/screens/identity.d.ts +5 -0
  29. package/dist/src/screens/identity.js +86 -0
  30. package/dist/src/screens/industries.d.ts +1 -0
  31. package/dist/src/screens/industries.js +267 -0
  32. package/dist/src/screens/onboarding.d.ts +13 -0
  33. package/dist/src/screens/onboarding.js +225 -0
  34. package/dist/src/screens/portal.d.ts +5 -0
  35. package/dist/src/screens/portal.js +214 -0
  36. package/dist/src/screens/public-details.d.ts +5 -0
  37. package/dist/src/screens/public-details.js +90 -0
  38. package/dist/src/semantics/after-payment.d.ts +22 -0
  39. package/dist/src/semantics/after-payment.js +93 -0
  40. package/dist/src/semantics/apps-secrets.d.ts +2 -0
  41. package/dist/src/semantics/apps-secrets.js +54 -0
  42. package/dist/src/semantics/balance.d.ts +11 -0
  43. package/dist/src/semantics/balance.js +195 -0
  44. package/dist/src/semantics/billing.d.ts +2 -0
  45. package/dist/src/semantics/billing.js +220 -0
  46. package/dist/src/semantics/charges.d.ts +28 -0
  47. package/dist/src/semantics/charges.js +201 -0
  48. package/dist/src/semantics/checkout.d.ts +15 -0
  49. package/dist/src/semantics/checkout.js +303 -0
  50. package/dist/src/semantics/connect.d.ts +5 -0
  51. package/dist/src/semantics/connect.js +476 -0
  52. package/dist/src/semantics/coupons.d.ts +6 -0
  53. package/dist/src/semantics/coupons.js +92 -0
  54. package/dist/src/semantics/credit-notes.d.ts +2 -0
  55. package/dist/src/semantics/credit-notes.js +172 -0
  56. package/dist/src/semantics/customers.d.ts +6 -0
  57. package/dist/src/semantics/customers.js +429 -0
  58. package/dist/src/semantics/disputes.d.ts +2 -0
  59. package/dist/src/semantics/disputes.js +51 -0
  60. package/dist/src/semantics/entitlements.d.ts +2 -0
  61. package/dist/src/semantics/entitlements.js +95 -0
  62. package/dist/src/semantics/ephemeral-keys.d.ts +2 -0
  63. package/dist/src/semantics/ephemeral-keys.js +34 -0
  64. package/dist/src/semantics/files.d.ts +2 -0
  65. package/dist/src/semantics/files.js +125 -0
  66. package/dist/src/semantics/invoices.d.ts +18 -0
  67. package/dist/src/semantics/invoices.js +541 -0
  68. package/dist/src/semantics/issuing.d.ts +13 -0
  69. package/dist/src/semantics/issuing.js +570 -0
  70. package/dist/src/semantics/ledger.d.ts +54 -0
  71. package/dist/src/semantics/ledger.js +181 -0
  72. package/dist/src/semantics/payment-intents.d.ts +18 -0
  73. package/dist/src/semantics/payment-intents.js +404 -0
  74. package/dist/src/semantics/payment-links.d.ts +2 -0
  75. package/dist/src/semantics/payment-links.js +133 -0
  76. package/dist/src/semantics/payment-methods.d.ts +20 -0
  77. package/dist/src/semantics/payment-methods.js +138 -0
  78. package/dist/src/semantics/plans.d.ts +5 -0
  79. package/dist/src/semantics/plans.js +121 -0
  80. package/dist/src/semantics/platform.d.ts +9 -0
  81. package/dist/src/semantics/platform.js +206 -0
  82. package/dist/src/semantics/products.d.ts +2 -0
  83. package/dist/src/semantics/products.js +140 -0
  84. package/dist/src/semantics/radar.d.ts +2 -0
  85. package/dist/src/semantics/radar.js +83 -0
  86. package/dist/src/semantics/refunds.d.ts +9 -0
  87. package/dist/src/semantics/refunds.js +195 -0
  88. package/dist/src/semantics/renewals.d.ts +47 -0
  89. package/dist/src/semantics/renewals.js +251 -0
  90. package/dist/src/semantics/setup-intents.d.ts +2 -0
  91. package/dist/src/semantics/setup-intents.js +84 -0
  92. package/dist/src/semantics/shared.d.ts +78 -0
  93. package/dist/src/semantics/shared.js +192 -0
  94. package/dist/src/semantics/subscription-schedules.d.ts +2 -0
  95. package/dist/src/semantics/subscription-schedules.js +119 -0
  96. package/dist/src/semantics/subscriptions.d.ts +11 -0
  97. package/dist/src/semantics/subscriptions.js +605 -0
  98. package/dist/src/semantics/tax.d.ts +2 -0
  99. package/dist/src/semantics/tax.js +197 -0
  100. package/dist/src/semantics/terminal.d.ts +5 -0
  101. package/dist/src/semantics/terminal.js +182 -0
  102. package/dist/src/semantics/test-clocks.d.ts +6 -0
  103. package/dist/src/semantics/test-clocks.js +73 -0
  104. package/dist/src/semantics/tokens.d.ts +4 -0
  105. package/dist/src/semantics/tokens.js +44 -0
  106. package/dist/src/semantics/transfers.d.ts +2 -0
  107. package/dist/src/semantics/transfers.js +154 -0
  108. package/dist/src/semantics/treasury.d.ts +2 -0
  109. package/dist/src/semantics/treasury.js +377 -0
  110. package/dist/src/semantics/webhook-endpoints.d.ts +3 -0
  111. package/dist/src/semantics/webhook-endpoints.js +85 -0
  112. package/dist/src/stripe-budget.d.ts +55 -0
  113. package/dist/src/stripe-budget.js +155 -0
  114. package/dist/src/stripe-capabilities.d.ts +3 -0
  115. package/dist/src/stripe-capabilities.js +5052 -0
  116. package/dist/src/stripe-conformance.d.ts +41 -0
  117. package/dist/src/stripe-conformance.js +96 -0
  118. package/dist/src/stripe-connector.d.ts +161 -0
  119. package/dist/src/stripe-connector.js +414 -0
  120. package/dist/src/stripe-emit.d.ts +2 -0
  121. package/dist/src/stripe-emit.js +145 -0
  122. package/dist/src/stripe-events.d.ts +93 -0
  123. package/dist/src/stripe-events.js +388 -0
  124. package/dist/src/stripe-js.d.ts +4 -0
  125. package/dist/src/stripe-js.js +70 -0
  126. package/dist/src/stripe-mirror-ui.d.ts +15 -0
  127. package/dist/src/stripe-mirror-ui.js +87 -0
  128. package/dist/src/stripe-params.d.ts +3 -0
  129. package/dist/src/stripe-params.js +43 -0
  130. package/dist/src/stripe-perform-harness.d.ts +9 -0
  131. package/dist/src/stripe-perform-harness.js +26 -0
  132. package/dist/src/stripe-server.d.ts +33 -0
  133. package/dist/src/stripe-server.js +326 -0
  134. package/dist/src/stripe-shared.d.ts +106 -0
  135. package/dist/src/stripe-shared.js +273 -0
  136. package/dist/src/stripe-twin.d.ts +155 -0
  137. package/dist/src/stripe-twin.js +1226 -0
  138. package/dist/src/stripe-ui-conformance.d.ts +5 -0
  139. package/dist/src/stripe-ui-conformance.js +79 -0
  140. package/dist/src/stripe-ui-structure.d.ts +3 -0
  141. package/dist/src/stripe-ui-structure.js +168 -0
  142. package/dist/src/stripe-version.d.ts +10 -0
  143. package/dist/src/stripe-version.js +285 -0
  144. package/dist/test-fixtures/stripe-known-deviations.json +105 -0
  145. package/dist/test-fixtures/stripe-openapi-operations.SOURCE.md +14 -0
  146. package/dist/test-fixtures/stripe-openapi-operations.json +4717 -0
  147. package/dist/test-fixtures/stripe-schemas.SOURCE.md +35 -0
  148. package/dist/test-fixtures/stripe-schemas.json +3740 -0
  149. package/package.json +18 -10
  150. package/src/cli.ts +7 -7
  151. package/src/generated/events.gen.json +1 -0
  152. package/src/generated/surface.gen.json +1 -0
  153. package/src/generated/ui.gen.json +1 -0
  154. package/src/index.ts +31 -9
  155. package/src/manifest.ts +1097 -0
  156. package/src/screens/checkout.tsx +252 -0
  157. package/src/screens/consent-skin.ts +20 -0
  158. package/src/screens/financial-connections.tsx +101 -0
  159. package/src/screens/identity.tsx +96 -0
  160. package/src/screens/industries.ts +267 -0
  161. package/src/screens/onboarding.tsx +243 -0
  162. package/src/screens/portal.tsx +218 -0
  163. package/src/screens/public-details.tsx +105 -0
  164. package/src/semantics/after-payment.ts +113 -0
  165. package/src/semantics/apps-secrets.ts +58 -0
  166. package/src/semantics/balance.ts +209 -0
  167. package/src/semantics/billing.ts +216 -0
  168. package/src/semantics/charges.ts +211 -0
  169. package/src/semantics/checkout.ts +297 -0
  170. package/src/semantics/connect.ts +471 -0
  171. package/src/semantics/coupons.ts +97 -0
  172. package/src/semantics/credit-notes.ts +168 -0
  173. package/src/semantics/customers.ts +432 -0
  174. package/src/semantics/disputes.ts +62 -0
  175. package/src/semantics/entitlements.ts +94 -0
  176. package/src/semantics/ephemeral-keys.ts +34 -0
  177. package/src/semantics/files.ts +143 -0
  178. package/src/semantics/invoices.ts +541 -0
  179. package/src/semantics/issuing.ts +585 -0
  180. package/src/semantics/ledger.ts +216 -0
  181. package/src/semantics/payment-intents.ts +420 -0
  182. package/src/semantics/payment-links.ts +148 -0
  183. package/src/semantics/payment-methods.ts +143 -0
  184. package/src/semantics/plans.ts +131 -0
  185. package/src/semantics/platform.ts +220 -0
  186. package/src/semantics/products.ts +154 -0
  187. package/src/semantics/radar.ts +85 -0
  188. package/src/semantics/refunds.ts +218 -0
  189. package/src/semantics/renewals.ts +274 -0
  190. package/src/semantics/setup-intents.ts +87 -0
  191. package/src/semantics/shared.ts +215 -0
  192. package/src/semantics/subscription-schedules.ts +129 -0
  193. package/src/semantics/subscriptions.ts +610 -0
  194. package/src/semantics/tax.ts +220 -0
  195. package/src/semantics/terminal.ts +195 -0
  196. package/src/semantics/test-clocks.ts +77 -0
  197. package/src/semantics/tokens.ts +52 -0
  198. package/src/semantics/transfers.ts +174 -0
  199. package/src/semantics/treasury.ts +383 -0
  200. package/src/semantics/webhook-endpoints.ts +87 -0
  201. package/src/stripe-budget.ts +4 -4
  202. package/src/stripe-capabilities.ts +1456 -222
  203. package/src/stripe-conformance.ts +6 -5
  204. package/src/stripe-connector.ts +68 -40
  205. package/src/stripe-emit.ts +14 -7
  206. package/src/stripe-events.ts +94 -36
  207. package/src/stripe-js.ts +70 -0
  208. package/src/stripe-mirror-ui.ts +28 -298
  209. package/src/stripe-params.ts +44 -0
  210. package/src/stripe-perform-harness.ts +29 -0
  211. package/src/stripe-server.ts +263 -38
  212. package/src/stripe-shared.ts +294 -0
  213. package/src/stripe-twin.ts +429 -5325
  214. package/src/stripe-ui-conformance.ts +70 -107
  215. package/src/stripe-ui-structure.ts +124 -348
  216. package/src/stripe-version.ts +278 -0
  217. package/test-fixtures/stripe-known-deviations.json +2 -7
  218. package/test-fixtures/stripe-openapi-operations.json +1188 -2855
  219. package/src/stripe-form.ts +0 -35
@@ -0,0 +1,51 @@
1
+ import { asBool } from "../stripe-twin.js";
2
+ import { settleDisputeWon } from "./ledger.js";
3
+ import { at, fail } from "./shared.js";
4
+ const disputeMissing = (ctx, id) => fail(ctx, `No such dispute: '${id}'`, 404, 'resource_missing');
5
+ const retrieve = async (ctx) => {
6
+ const d = ctx.get('dispute', at(ctx, 'dispute'));
7
+ return d ? ctx.reply(d) : disputeMissing(ctx, at(ctx, 'dispute'));
8
+ };
9
+ const close = async (ctx) => {
10
+ const id = at(ctx, 'dispute');
11
+ const d = ctx.get('dispute', id);
12
+ if (!d)
13
+ return disputeMissing(ctx, id);
14
+ const refused = ctx.legal('dispute', 'status', 'PostDisputesDisputeClose', d.status, undefined, id);
15
+ if (refused)
16
+ return ctx.refuse(refused);
17
+ return ctx.reply(await ctx.write('dispute', id, { status: 'lost' }, 'dispute.close'));
18
+ };
19
+ /** Evidence staged without submitting it (submit=false), or on a dispute already under review or decided. */
20
+ async function keepEvidence(ctx, id, fields) {
21
+ return ctx.reply(await ctx.write('dispute', id, fields, 'dispute.update'));
22
+ }
23
+ const update = async (ctx) => {
24
+ const id = at(ctx, 'dispute');
25
+ const d = ctx.get('dispute', id);
26
+ if (!d)
27
+ return disputeMissing(ctx, id);
28
+ const params = ctx.params;
29
+ const given = params.evidence && typeof params.evidence === 'object' ? params.evidence : {};
30
+ const evidence = { ...(d.evidence ?? {}), ...given };
31
+ const fields = { evidence, ...(params.metadata && typeof params.metadata === 'object' ? { metadata: { ...(d.metadata ?? {}), ...params.metadata } } : {}) };
32
+ const submit = params.submit === undefined ? true : asBool(params.submit);
33
+ const review = d.status === 'warning_needs_response' ? 'warning_under_review' : 'under_review';
34
+ if (!submit || ctx.legal('dispute', 'status', 'PostDisputesDispute', d.status, review, id))
35
+ return keepEvidence(ctx, id, fields);
36
+ const details = d.evidence_details ?? {};
37
+ let body = await ctx.write('dispute', id, { ...fields, status: review, evidence_details: { ...details, has_evidence: true, submission_count: (Number(details.submission_count) || 0) + 1 } }, 'charge.dispute.updated');
38
+ const text = String(evidence.uncategorized_text ?? '');
39
+ const decided = review === 'under_review' ? (text.includes('winning_evidence') ? 'won' : text.includes('losing_evidence') ? 'lost' : undefined) : undefined;
40
+ if (decided && !ctx.legal('dispute', 'status', 'PostDisputesDispute', review, decided, id, 'vendor')) {
41
+ const bt = decided === 'won' ? await settleDisputeWon(ctx, id, Number(d.amount) || 0, String(d.currency ?? 'usd')) : undefined;
42
+ const kept = (ctx.get('dispute', id)?.balance_transactions ?? []);
43
+ body = await ctx.write('dispute', id, { status: decided, ...(bt ? { balance_transactions: [...kept, ctx.get('balance_transaction', bt) ?? bt] } : {}) }, 'charge.dispute.closed');
44
+ }
45
+ return ctx.reply(body);
46
+ };
47
+ export const disputes = {
48
+ GetDisputesDispute: retrieve,
49
+ PostDisputesDispute: update,
50
+ PostDisputesDisputeClose: close,
51
+ };
@@ -0,0 +1,2 @@
1
+ import type { Semantics } from '@volter/world-core';
2
+ export declare const entitlements: Record<string, Semantics>;
@@ -0,0 +1,95 @@
1
+ import { asBool, subscriptionItemPairs } from "../stripe-twin.js";
2
+ import { at, created, fail, list, newest, path, where, kept } from "./shared.js";
3
+ const FEAT = 'entitlements.feature';
4
+ /** A feature whose lookup_key an unarchived feature already has: "A unique key you provide as your own system identifier"
5
+ * (docs.stripe.com/api/entitlements/feature/create). */
6
+ function lookupKeyTaken(ctx, lookupKey) {
7
+ return fail(ctx, `A feature with lookup_key '${lookupKey}' already exists.`, 400, 'resource_already_exists');
8
+ }
9
+ const create = async (ctx) => {
10
+ const params = ctx.params;
11
+ if (typeof params.name !== 'string' || !params.name)
12
+ return fail(ctx, 'Missing required param: name.', 400, 'parameter_missing');
13
+ const lookupKey = typeof params.lookup_key === 'string' ? params.lookup_key : '';
14
+ if (!lookupKey)
15
+ return fail(ctx, 'Missing required param: lookup_key.', 400, 'parameter_missing');
16
+ if (ctx.rowsRaw(FEAT, { withDeleted: true }).some((f) => f.lookup_key === lookupKey && !f._archived))
17
+ return lookupKeyTaken(ctx, lookupKey);
18
+ return ctx.reply(await created(ctx, FEAT, {}, {
19
+ name: params.name, lookup_key: lookupKey, active: true, livemode: false,
20
+ metadata: params.metadata && typeof params.metadata === 'object' ? params.metadata : {},
21
+ }));
22
+ };
23
+ const listFeatures = async (ctx) => list(ctx, FEAT, where(ctx, newest(ctx, FEAT), {
24
+ lookup_key: (f, v) => f.lookup_key === v,
25
+ archived: (f, v) => asBool(kept(ctx, FEAT, f, '_archived')) === asBool(v),
26
+ }));
27
+ // name, active and metadata change
28
+ const update = async (ctx) => {
29
+ const id = at(ctx, 'id');
30
+ if (!ctx.get(FEAT, id))
31
+ return ctx.notFound(FEAT, id);
32
+ const patch = {};
33
+ if (ctx.params.name !== undefined)
34
+ patch.name = ctx.params.name;
35
+ if (ctx.params.active !== undefined) {
36
+ patch.active = asBool(ctx.params.active);
37
+ patch._archived = undefined;
38
+ }
39
+ if (ctx.params.metadata !== undefined)
40
+ patch.metadata = ctx.params.metadata;
41
+ return ctx.reply(await ctx.write(FEAT, id, patch, 'entitlements_feature.update'));
42
+ };
43
+ // a customer is entitled to every feature granted to a product its live subscriptions bill, as the product stood when
44
+ // the subscription's current period began: "When a customer purchases or subscribes to a product with an attached
45
+ // feature, Stripe creates and manages an Active Entitlement for that customer and feature"; "Existing subscriptions will
46
+ // create active entitlements for any product feature changes at the start of the next billing period"
47
+ // (docs.stripe.com/billing/entitlements). Where the documentation stops and the twin decides: a feature removed from a
48
+ // product is kept, the same way, until the next period begins.
49
+ const activeEntitlements = async (ctx) => {
50
+ const customer = typeof ctx.params.customer === 'string' ? ctx.params.customer : '';
51
+ if (!customer)
52
+ return fail(ctx, 'Missing required param: customer.', 400, 'parameter_missing');
53
+ if (!ctx.row('customer', customer, { withDeleted: true }))
54
+ return fail(ctx, `No such customer: '${customer}'`, 404, 'resource_missing');
55
+ // each product a live subscription bills, with the start of the period it is billed for
56
+ const productIds = new Map();
57
+ for (const sub of newest(ctx, 'subscription')) {
58
+ // a subscription entitles while it runs: not before its first payment (incomplete), nor once it has ended
59
+ // (docs.stripe.com/billing/entitlements; past_due keeps access during retries — that edge is the twin's reading)
60
+ if (sub.customer !== customer || !['active', 'trialing', 'past_due'].includes(String(sub.status)))
61
+ continue;
62
+ const periodStart = Number(sub.current_period_start ?? sub.start_date ?? sub.created ?? 0);
63
+ for (const it of subscriptionItemPairs(sub)) {
64
+ const price = ctx.get('price', it.price);
65
+ if (price && typeof price.product === 'string')
66
+ productIds.set(price.product, Math.max(productIds.get(price.product) ?? -Infinity, periodStart));
67
+ }
68
+ }
69
+ const seen = new Set();
70
+ const data = [];
71
+ for (const pf of ctx.rowsRaw('product_feature', { withDeleted: true })) {
72
+ const periodStart = productIds.get(String(pf._product));
73
+ if (periodStart === undefined)
74
+ continue;
75
+ // attached by the period's start, and not removed before it
76
+ if (Number(pf._created ?? 0) > periodStart)
77
+ continue;
78
+ if (pf.deleted && !(Number(pf._removed_at) > periodStart))
79
+ continue;
80
+ const featId = String(pf._feature ?? pf.entitlement_feature?.id ?? pf.entitlement_feature);
81
+ if (seen.has(featId))
82
+ continue;
83
+ seen.add(featId);
84
+ const feat = ctx.get(FEAT, featId);
85
+ if (feat)
86
+ data.push({ id: `ent_twin_${customer}_${featId}`, object: 'entitlements.active_entitlement', feature: featId, lookup_key: feat.lookup_key, livemode: false });
87
+ }
88
+ return ctx.reply({ object: 'list', url: path(ctx), has_more: false, data });
89
+ };
90
+ export const entitlements = {
91
+ PostEntitlementsFeatures: create,
92
+ GetEntitlementsFeatures: listFeatures,
93
+ PostEntitlementsFeaturesId: update,
94
+ GetEntitlementsActiveEntitlements: activeEntitlements,
95
+ };
@@ -0,0 +1,2 @@
1
+ import type { Semantics } from '@volter/world-core';
2
+ export declare const ephemeralKeys: Record<string, Semantics>;
@@ -0,0 +1,34 @@
1
+ import { at, fail } from "./shared.js";
2
+ const create = async (ctx) => {
3
+ const version = ctx.call.request.headers.get('stripe-version');
4
+ if (!version)
5
+ return fail(ctx, 'You must pass the `Stripe-Version` header (e.g. via the apiVersion option) when creating an ephemeral key.', 400, 'parameter_missing');
6
+ const params = ctx.params;
7
+ const scopes = [];
8
+ if (typeof params.customer === 'string' && params.customer)
9
+ scopes.push(['customer', params.customer]);
10
+ if (typeof params.issuing_card === 'string' && params.issuing_card)
11
+ scopes.push(['issuing.card', params.issuing_card]);
12
+ if (typeof params.verification_session === 'string' && params.verification_session)
13
+ scopes.push(['identity.verification_session', params.verification_session]);
14
+ if (scopes.length !== 1)
15
+ return fail(ctx, 'You must pass exactly one of `customer`, `issuing_card`, or `verification_session`.', 400, 'parameter_missing');
16
+ const [scope, scopedId] = scopes[0];
17
+ if (scope === 'customer' && !ctx.get('customer', scopedId))
18
+ return fail(ctx, `No such customer: '${scopedId}'`, 400, 'resource_missing');
19
+ const id = ctx.mint('ephemeral_key');
20
+ const now = Number(ctx.now());
21
+ const associated_objects = [{ type: scope, id: scopedId }];
22
+ await ctx.write('ephemeral_key', id, { object: 'ephemeral_key', created: now, expires: now + 3600, livemode: false, associated_objects }, 'ephemeral_key.create');
23
+ return ctx.reply({ id, object: 'ephemeral_key', created: now, expires: now + 3600, livemode: false, secret: `ek_test_twin_${id}`, associated_objects });
24
+ };
25
+ const expire = async (ctx) => {
26
+ const id = at(ctx, 'key');
27
+ if (!ctx.get('ephemeral_key', id))
28
+ return fail(ctx, `No such ephemeral key: '${id}'`, 404, 'resource_missing');
29
+ return ctx.reply(await ctx.write('ephemeral_key', id, { expires: ctx.now() }, 'ephemeral_key.delete'));
30
+ };
31
+ export const ephemeralKeys = {
32
+ PostEphemeralKeys: create,
33
+ DeleteEphemeralKeysKey: expire,
34
+ };
@@ -0,0 +1,2 @@
1
+ import type { Semantics } from '@volter/world-core';
2
+ export declare const files: Record<string, Semantics>;
@@ -0,0 +1,125 @@
1
+ import { asBool } from "../stripe-twin.js";
2
+ import { actingAccount } from "./ledger.js";
3
+ import { at, created, fail, filePurposeRefused, list, mergeUpdate, newest, where } from "./shared.js";
4
+ /** The type Stripe answers for an uploaded file: "The returned file type (for example, csv, pdf, jpg, or png)"
5
+ * (docs.stripe.com/api/files/object). Where the documentation stops and the twin decides: read from the name's
6
+ * extension (jpeg as jpg), and null for a name without one. */
7
+ function fileType(name) {
8
+ const ext = /\.([a-z0-9]+)$/i.exec(name)?.[1]?.toLowerCase();
9
+ return ext ? (ext === 'jpeg' ? 'jpg' : ext) : null;
10
+ }
11
+ // multipart on Stripe; the file is described by the part uploaded: its name, its size in bytes and its type
12
+ const createFile = async (ctx) => {
13
+ const purpose = typeof ctx.params.purpose === 'string' ? ctx.params.purpose : '';
14
+ if (!purpose)
15
+ return fail(ctx, 'Missing required param: purpose.', 400, 'parameter_missing');
16
+ const part = ctx.params.file && typeof ctx.params.file === 'object' ? ctx.params.file : undefined;
17
+ const filename = typeof ctx.params.file === 'string' ? ctx.params.file : typeof part?.name === 'string' ? part.name : 'twin_upload';
18
+ const made = await created(ctx, 'file', { purpose }, {
19
+ filename, size: typeof part?.size === 'number' ? part.size : 0,
20
+ type: fileType(filename), title: null, expires_at: null, url: null,
21
+ links: { object: 'list', data: [], has_more: false, total_count: 0, url: '' },
22
+ });
23
+ await syncLinks(ctx, String(made.id));
24
+ return ctx.reply(ctx.get('file', String(made.id)));
25
+ };
26
+ /** A file's links, kept in step with the links made to it, at the address Stripe lists them
27
+ * (/v1/file_links?file=…, the File object's example). */
28
+ async function syncLinks(ctx, fileId) {
29
+ const data = newest(ctx, 'file_link').filter((l) => l.file === fileId);
30
+ await ctx.write('file', fileId, { links: { object: 'list', data, has_more: false, url: `/v1/file_links?file=${fileId}` } }, 'file.links_synced');
31
+ }
32
+ /** The purposes a file link may be made to: "The file's `purpose` must be one of the following: …" (the served
33
+ * spec, POST /v1/file_links `file`). */
34
+ 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'];
35
+ // `file` is required; a link is kept, has its own public url, and is listed on its file
36
+ const createFileLink = async (ctx) => {
37
+ if (ctx.params.file === undefined || ctx.params.file === '')
38
+ return fail(ctx, 'Missing required param: file.', 400, 'parameter_missing');
39
+ const fileId = String(ctx.params.file);
40
+ const refused = filePurposeRefused(ctx, 'file', LINKABLE);
41
+ if (refused)
42
+ return refused;
43
+ const id = ctx.mint('file_link');
44
+ const link = await created(ctx, 'file_link', { ...ctx.params, id, file: fileId }, {
45
+ url: `https://files.twin.local/links/${id}`, expired: false, livemode: false, metadata: {},
46
+ expires_at: ctx.params.expires_at !== undefined ? Number(ctx.params.expires_at) : null,
47
+ });
48
+ await syncLinks(ctx, fileId);
49
+ return ctx.reply(link);
50
+ };
51
+ // the person verifies at the session's url, Stripe Identity's hosted page (../screens/identity.tsx)
52
+ const createVerificationSession = async (ctx) => {
53
+ const id = ctx.mint('identity.verification_session');
54
+ return ctx.reply(await created(ctx, 'identity.verification_session', { ...ctx.params, id }, {
55
+ status: 'requires_input', livemode: false, client_secret: `${id}_secret_twin`, url: `https://verify.stripe.com/start/${id}`,
56
+ last_error: null, last_verification_report: null, verified_outputs: null, return_url: ctx.params.return_url ?? null,
57
+ }));
58
+ };
59
+ // ── the customer portal ──
60
+ // the configuration defaults to the account's default one
61
+ const createPortalSession = async (ctx) => {
62
+ const customer = typeof ctx.params.customer === 'string' ? ctx.params.customer : '';
63
+ if (!customer)
64
+ return fail(ctx, 'Missing required param: customer.', 400, 'parameter_missing');
65
+ if (!ctx.row('customer', customer, { withDeleted: true }))
66
+ return fail(ctx, `No such customer: '${customer}'`, 404, 'resource_missing');
67
+ const configuration = typeof ctx.params.configuration === 'string' && ctx.params.configuration
68
+ ? ctx.params.configuration
69
+ : (ctx.rowsRaw('billing_portal.configuration', { withDeleted: true }).find((c) => c.is_default)?.id ?? 'bpc_twin_default');
70
+ const id = ctx.mint('billing_portal.session');
71
+ return ctx.reply(await created(ctx, 'billing_portal.session', {
72
+ id, customer, configuration, return_url: ctx.params.return_url ?? null, url: `https://billing.stripe.com/p/session/${id}`,
73
+ locale: ctx.params.locale ?? null, on_behalf_of: ctx.params.on_behalf_of ?? null, livemode: false,
74
+ // the account the session was made on (Stripe-Account), whose name the portal shows (screens/portal.tsx)
75
+ ...(actingAccount(ctx) ? { _brand_account: actingAccount(ctx) } : {}),
76
+ }, {}));
77
+ };
78
+ const PBC = 'billing_portal.configuration';
79
+ /** A portal configuration's features, each filled where the request leaves it: every feature is answered (the served
80
+ * spec requires all five), as the create page's example answers a request that sets only customer_update and
81
+ * invoice_history: payment_method_update disabled, subscription_cancel disabled at_period_end with proration_behavior
82
+ * none and its five default reasons, subscription_update disabled with no allowed updates and proration_behavior none
83
+ * (docs.stripe.com/api/customer_portal/configurations/create); trial_update_behavior "Defaults to a value of
84
+ * `end_trial` if you don't set it during creation" (the served spec). Where the documentation stops and the twin
85
+ * decides: a feature the request leaves out is disabled, and schedule_at_period_end has no conditions. */
86
+ const PORTAL_FEATURES = {
87
+ customer_update: { allowed_updates: [], enabled: false },
88
+ invoice_history: { enabled: false },
89
+ payment_method_update: { enabled: false, payment_method_configuration: null },
90
+ subscription_cancel: { cancellation_reason: { enabled: false, options: ['too_expensive', 'missing_features', 'switched_service', 'unused', 'other'] }, enabled: false, mode: 'at_period_end', proration_behavior: 'none' },
91
+ subscription_update: { default_allowed_updates: [], enabled: false, proration_behavior: 'none', schedule_at_period_end: { conditions: [] }, trial_update_behavior: 'end_trial' },
92
+ };
93
+ const filled = (base, given) => {
94
+ if (!given || typeof given !== 'object' || Array.isArray(given) || !base || typeof base !== 'object' || Array.isArray(base))
95
+ return given === undefined ? base : given;
96
+ const out = { ...base };
97
+ for (const [k, v] of Object.entries(given))
98
+ out[k] = filled(base[k], v);
99
+ return out;
100
+ };
101
+ const createPortalConfiguration = async (ctx) => ctx.reply(await created(ctx, PBC, { ...ctx.params, business_profile: filled({ headline: null, privacy_policy_url: null, terms_of_service_url: null }, ctx.params.business_profile), features: filled(PORTAL_FEATURES, ctx.params.features) }, {
102
+ active: true, is_default: false, livemode: false,
103
+ login_page: { enabled: false, url: null },
104
+ metadata: {}, default_return_url: ctx.params.default_return_url ?? null,
105
+ updated: ctx.now(),
106
+ }));
107
+ const listPortalConfigurations = async (ctx) => list(ctx, PBC, where(ctx, newest(ctx, PBC), {
108
+ active: (c, v) => asBool(c.active) === asBool(v),
109
+ is_default: (c, v) => asBool(c.is_default) === asBool(v),
110
+ }));
111
+ const updatePortalConfiguration = async (ctx) => {
112
+ const id = at(ctx, 'configuration');
113
+ if (!ctx.get(PBC, id))
114
+ return ctx.notFound(PBC, id);
115
+ return ctx.reply(await ctx.write(PBC, id, mergeUpdate(ctx, PBC, id, { ...ctx.params, updated: ctx.now() }), 'billing_portal.configuration.update'));
116
+ };
117
+ export const files = {
118
+ PostFiles: createFile,
119
+ PostFileLinks: createFileLink,
120
+ PostIdentityVerificationSessions: createVerificationSession,
121
+ PostBillingPortalSessions: createPortalSession,
122
+ PostBillingPortalConfigurations: createPortalConfiguration,
123
+ GetBillingPortalConfigurations: listPortalConfigurations,
124
+ PostBillingPortalConfigurationsConfiguration: updatePortalConfiguration,
125
+ };
@@ -0,0 +1,18 @@
1
+ import type { Semantics, SemanticsContext } from '@volter/world-core';
2
+ import { type Row } from './shared.js';
3
+ /** A draft invoice another object makes (an accepted quote): its customer's, with an invoice item per line the
4
+ * object gives, and the object named as its parent. Answers the invoice's id. */
5
+ export declare function draftInvoiceFor(ctx: SemanticsContext, customer: string, currency: string, parent: Row, lines: Array<{
6
+ price: unknown;
7
+ quantity: number;
8
+ amount: number;
9
+ description: unknown;
10
+ }>): Promise<string>;
11
+ /** What finalizing assigns: the invoice's `number`, the customer's invoice_prefix and a sequence
12
+ * (https://docs.stripe.com/api/invoices/object#invoice_object-number), and for send_invoice a
13
+ * `due_date` days_until_due after finalization (#invoice_object-due_date). */
14
+ /** A customer's invoice_prefix, "The prefix for the customer used to generate unique invoice numbers"
15
+ * (docs.stripe.com/api/customers/object): the twin's, from the customer's id. */
16
+ export declare const invoicePrefix: (customerId: string) => string;
17
+ export declare function finalizedFields(ctx: SemanticsContext, inv: Row, now: number): Row;
18
+ export declare const invoices: Record<string, Semantics>;