@volter/twin-stripe 3.0.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/README.md +51 -21
  2. package/dist/generated/pack-facts.json +3 -3
  3. package/dist/journeys/coverage.json +596 -0
  4. package/dist/journeys/customer-life.json +6108 -0
  5. package/dist/journeys/customer-life.outline.md +371 -0
  6. package/dist/journeys/decisions.json +677 -0
  7. package/dist/journeys/demand.json +706 -0
  8. package/dist/journeys/score.json +853 -0
  9. package/dist/journeys/unreachable.json +102 -0
  10. package/dist/journeys/vendor-examples.json +8010 -0
  11. package/dist/spec/SOURCE.md +5 -1
  12. package/dist/spec/doc-examples.json +445 -0
  13. package/dist/spec/sources.json +135 -207
  14. package/dist/src/cli.d.ts +0 -1
  15. package/dist/src/cli.js +0 -2
  16. package/dist/src/engine/after-payment.d.ts +0 -1
  17. package/dist/src/engine/after-payment.js +0 -1
  18. package/dist/src/engine/common.d.ts +5 -5
  19. package/dist/src/engine/common.js +13 -11
  20. package/dist/src/engine/connect.d.ts +0 -21
  21. package/dist/src/engine/connect.js +1 -82
  22. package/dist/src/engine/customers.d.ts +0 -8
  23. package/dist/src/engine/customers.js +0 -22
  24. package/dist/src/engine/display.d.ts +0 -3
  25. package/dist/src/engine/display.js +0 -3
  26. package/dist/src/engine/files.d.ts +0 -7
  27. package/dist/src/engine/files.js +0 -10
  28. package/dist/src/engine/invoices.d.ts +6 -9
  29. package/dist/src/engine/invoices.js +56 -29
  30. package/dist/src/engine/issuing.d.ts +6 -8
  31. package/dist/src/engine/issuing.js +19 -10
  32. package/dist/src/engine/ledger.d.ts +0 -1
  33. package/dist/src/engine/ledger.js +0 -1
  34. package/dist/src/engine/oauth.js +1 -2
  35. package/dist/src/engine/refunds.js +2 -27
  36. package/dist/src/engine/stripe.d.ts +2 -7
  37. package/dist/src/engine/stripe.js +34 -83
  38. package/dist/src/engine/subscriptions.d.ts +0 -1
  39. package/dist/src/engine/subscriptions.js +0 -1
  40. package/dist/src/engine/version.d.ts +0 -2
  41. package/dist/src/engine/version.js +17 -6
  42. package/dist/src/fetch.js +2 -2
  43. package/dist/src/generated/surface.gen.json +1 -1
  44. package/dist/src/generated/ui.gen.json +1 -1
  45. package/dist/src/index.js +5 -3
  46. package/dist/src/manifest.js +348 -220
  47. package/dist/src/screens/billing-portal.js +5 -5
  48. package/dist/src/screens/{connect-oauth.d.ts → checkout-js.d.ts} +0 -1
  49. package/dist/src/screens/checkout-js.js +84 -0
  50. package/dist/src/screens/connect-onboarding.js +3 -3
  51. package/dist/src/screens/dashboard.d.ts +1 -1
  52. package/dist/src/screens/dashboard.js +52 -53
  53. package/dist/src/screens/stripe-js.js +53 -93
  54. package/dist/src/semantics/account.d.ts +2 -0
  55. package/dist/src/semantics/account.js +8 -0
  56. package/dist/src/semantics/accounts.js +3 -1
  57. package/dist/src/semantics/around.js +3 -6
  58. package/dist/src/semantics/billing_portal.d.ts +0 -1
  59. package/dist/src/semantics/billing_portal.js +1 -7
  60. package/dist/src/semantics/checkout.d.ts +1 -1
  61. package/dist/src/semantics/checkout.js +18 -15
  62. package/dist/src/semantics/clock.js +1 -7
  63. package/dist/src/semantics/customers.d.ts +1 -2
  64. package/dist/src/semantics/customers.js +32 -38
  65. package/dist/src/semantics/entitlements.d.ts +0 -2
  66. package/dist/src/semantics/entitlements.js +4 -50
  67. package/dist/src/semantics/events.d.ts +2 -3
  68. package/dist/src/semantics/events.js +6 -11
  69. package/dist/src/semantics/index.js +16 -38
  70. package/dist/src/semantics/invoices.d.ts +3 -8
  71. package/dist/src/semantics/invoices.js +134 -127
  72. package/dist/src/semantics/issuing.d.ts +6 -0
  73. package/dist/src/semantics/issuing.js +77 -0
  74. package/dist/src/semantics/payment_intents.d.ts +0 -1
  75. package/dist/src/semantics/payment_intents.js +5 -13
  76. package/dist/src/semantics/payment_methods.d.ts +0 -2
  77. package/dist/src/semantics/payment_methods.js +2 -26
  78. package/dist/src/semantics/payouts.d.ts +1 -0
  79. package/dist/src/semantics/payouts.js +8 -1
  80. package/dist/src/semantics/products.d.ts +1 -3
  81. package/dist/src/semantics/products.js +3 -14
  82. package/dist/src/semantics/refunds.d.ts +1 -2
  83. package/dist/src/semantics/refunds.js +1 -5
  84. package/dist/src/semantics/shared.d.ts +64 -336
  85. package/dist/src/semantics/shared.js +326 -1336
  86. package/dist/src/semantics/states.d.ts +20 -89
  87. package/dist/src/semantics/states.js +116 -334
  88. package/dist/src/semantics/subscriptions.d.ts +1 -2
  89. package/dist/src/semantics/subscriptions.js +23 -145
  90. package/dist/src/semantics/test_helpers.d.ts +2 -5
  91. package/dist/src/semantics/test_helpers.js +121 -43
  92. package/dist/src/semantics/webhook_endpoints.d.ts +2 -0
  93. package/dist/src/semantics/webhook_endpoints.js +26 -0
  94. package/generated/pack-facts.json +3 -3
  95. package/journeys/coverage.json +596 -0
  96. package/journeys/customer-life.json +6108 -0
  97. package/journeys/customer-life.outline.md +371 -0
  98. package/journeys/decisions.json +677 -0
  99. package/journeys/demand.json +706 -0
  100. package/journeys/score.json +853 -0
  101. package/journeys/unreachable.json +102 -0
  102. package/journeys/vendor-examples.json +8010 -0
  103. package/package.json +4 -3
  104. package/spec/SOURCE.md +5 -1
  105. package/spec/doc-examples.json +445 -0
  106. package/spec/sources.json +135 -207
  107. package/src/cli.ts +0 -3
  108. package/src/engine/after-payment.ts +0 -2
  109. package/src/engine/balance.ts +0 -1
  110. package/src/engine/checkout.ts +0 -1
  111. package/src/engine/common.ts +19 -17
  112. package/src/engine/connect.ts +1 -79
  113. package/src/engine/customers.ts +0 -25
  114. package/src/engine/display.ts +2 -3
  115. package/src/engine/files.ts +0 -13
  116. package/src/engine/invoices.ts +64 -33
  117. package/src/engine/issuing.ts +16 -11
  118. package/src/engine/ledger.ts +0 -2
  119. package/src/engine/oauth.ts +1 -2
  120. package/src/engine/onboarding.ts +0 -1
  121. package/src/engine/params.ts +0 -1
  122. package/src/engine/payment-intents.ts +0 -1
  123. package/src/engine/payment-methods.ts +0 -1
  124. package/src/engine/refunds.ts +2 -28
  125. package/src/engine/renewals.ts +0 -1
  126. package/src/engine/stripe.ts +35 -83
  127. package/src/engine/subscriptions.ts +0 -3
  128. package/src/engine/transfers.ts +0 -1
  129. package/src/engine/version.ts +15 -7
  130. package/src/engine/webhooks.ts +0 -2
  131. package/src/fetch.ts +2 -2
  132. package/src/generated/surface.gen.json +1 -1
  133. package/src/generated/ui.gen.json +1 -1
  134. package/src/index.ts +6 -4
  135. package/src/manifest.ts +348 -220
  136. package/src/screens/billing-portal.tsx +5 -7
  137. package/src/screens/checkout-js.ts +80 -0
  138. package/src/screens/checkout.tsx +0 -2
  139. package/src/screens/connect-onboarding.tsx +3 -5
  140. package/src/screens/dashboard.tsx +53 -73
  141. package/src/screens/stripe-js.ts +57 -125
  142. package/src/semantics/account.ts +10 -0
  143. package/src/semantics/account_links.ts +0 -1
  144. package/src/semantics/accounts.ts +8 -7
  145. package/src/semantics/apps.ts +1 -2
  146. package/src/semantics/around.ts +3 -6
  147. package/src/semantics/balance.ts +0 -1
  148. package/src/semantics/balance_transactions.ts +1 -3
  149. package/src/semantics/billing_portal.ts +1 -7
  150. package/src/semantics/charges.ts +1 -6
  151. package/src/semantics/checkout.ts +16 -12
  152. package/src/semantics/clock.ts +1 -8
  153. package/src/semantics/coupons.ts +0 -1
  154. package/src/semantics/customers.ts +32 -35
  155. package/src/semantics/doors.ts +2 -3
  156. package/src/semantics/entitlements.ts +1 -45
  157. package/src/semantics/events.ts +7 -14
  158. package/src/semantics/index.ts +16 -38
  159. package/src/semantics/invoice_payments.ts +1 -3
  160. package/src/semantics/invoices.ts +124 -119
  161. package/src/semantics/issuing.ts +74 -0
  162. package/src/semantics/payment_intents.ts +10 -17
  163. package/src/semantics/payment_methods.ts +2 -25
  164. package/src/semantics/payouts.ts +9 -3
  165. package/src/semantics/prices.ts +0 -1
  166. package/src/semantics/products.ts +4 -14
  167. package/src/semantics/promotion_codes.ts +0 -1
  168. package/src/semantics/radar.ts +1 -3
  169. package/src/semantics/refunds.ts +2 -11
  170. package/src/semantics/shared.ts +350 -1382
  171. package/src/semantics/states.ts +109 -330
  172. package/src/semantics/subscriptions.ts +25 -145
  173. package/src/semantics/test_helpers.ts +109 -43
  174. package/src/semantics/transfers.ts +1 -3
  175. package/src/semantics/webhook_endpoints.ts +27 -0
  176. package/src/server.ts +0 -1
  177. package/dist/src/engine/billing.d.ts +0 -5
  178. package/dist/src/engine/billing.js +0 -6
  179. package/dist/src/engine/charges.d.ts +0 -6
  180. package/dist/src/engine/charges.js +0 -13
  181. package/dist/src/engine/connect-settings.d.ts +0 -16
  182. package/dist/src/engine/connect-settings.js +0 -26
  183. package/dist/src/engine/coupons.d.ts +0 -0
  184. package/dist/src/engine/coupons.js +0 -5
  185. package/dist/src/engine/credit-notes.d.ts +0 -1
  186. package/dist/src/engine/credit-notes.js +0 -5
  187. package/dist/src/engine/disputes.d.ts +0 -0
  188. package/dist/src/engine/disputes.js +0 -9
  189. package/dist/src/engine/entitlements.d.ts +0 -1
  190. package/dist/src/engine/entitlements.js +0 -4
  191. package/dist/src/engine/ephemeral-keys.d.ts +0 -0
  192. package/dist/src/engine/ephemeral-keys.js +0 -4
  193. package/dist/src/engine/front.d.ts +0 -5
  194. package/dist/src/engine/front.js +0 -25
  195. package/dist/src/engine/payment-links.d.ts +0 -1
  196. package/dist/src/engine/payment-links.js +0 -5
  197. package/dist/src/engine/plans.d.ts +0 -6
  198. package/dist/src/engine/plans.js +0 -26
  199. package/dist/src/engine/platform.d.ts +0 -39
  200. package/dist/src/engine/platform.js +0 -34
  201. package/dist/src/engine/products.d.ts +0 -0
  202. package/dist/src/engine/products.js +0 -4
  203. package/dist/src/engine/setup-intents.d.ts +0 -1
  204. package/dist/src/engine/setup-intents.js +0 -4
  205. package/dist/src/engine/subscription-schedules.d.ts +0 -3
  206. package/dist/src/engine/subscription-schedules.js +0 -1
  207. package/dist/src/engine/tax.d.ts +0 -7
  208. package/dist/src/engine/tax.js +0 -28
  209. package/dist/src/engine/terminal.d.ts +0 -4
  210. package/dist/src/engine/terminal.js +0 -20
  211. package/dist/src/engine/test-clocks.d.ts +0 -5
  212. package/dist/src/engine/test-clocks.js +0 -9
  213. package/dist/src/engine/tokens.d.ts +0 -7
  214. package/dist/src/engine/tokens.js +0 -20
  215. package/dist/src/engine/treasury.d.ts +0 -23
  216. package/dist/src/engine/treasury.js +0 -45
  217. package/dist/src/screens/connect-oauth.js +0 -333
  218. package/dist/src/semantics/disputes.d.ts +0 -2
  219. package/dist/src/semantics/disputes.js +0 -5
  220. package/dist/src/semantics/invoiceitems.d.ts +0 -3
  221. package/dist/src/semantics/invoiceitems.js +0 -59
  222. package/dist/src/semantics/setup_intents.d.ts +0 -3
  223. package/dist/src/semantics/setup_intents.js +0 -41
  224. package/dist/src/semantics/subscription_items.d.ts +0 -5
  225. package/dist/src/semantics/subscription_items.js +0 -55
  226. package/dist/src/semantics/subscription_schedules.d.ts +0 -4
  227. package/dist/src/semantics/subscription_schedules.js +0 -60
  228. package/src/engine/billing.ts +0 -16
  229. package/src/engine/charges.ts +0 -19
  230. package/src/engine/connect-settings.ts +0 -23
  231. package/src/engine/coupons.ts +0 -5
  232. package/src/engine/credit-notes.ts +0 -6
  233. package/src/engine/disputes.ts +0 -9
  234. package/src/engine/entitlements.ts +0 -5
  235. package/src/engine/ephemeral-keys.ts +0 -3
  236. package/src/engine/front.ts +0 -31
  237. package/src/engine/payment-links.ts +0 -6
  238. package/src/engine/plans.ts +0 -42
  239. package/src/engine/platform.ts +0 -47
  240. package/src/engine/products.ts +0 -3
  241. package/src/engine/setup-intents.ts +0 -5
  242. package/src/engine/subscription-schedules.ts +0 -10
  243. package/src/engine/tax.ts +0 -39
  244. package/src/engine/terminal.ts +0 -23
  245. package/src/engine/test-clocks.ts +0 -11
  246. package/src/engine/tokens.ts +0 -22
  247. package/src/engine/treasury.ts +0 -47
  248. package/src/screens/connect-oauth.tsx +0 -376
  249. package/src/semantics/disputes.ts +0 -12
  250. package/src/semantics/invoiceitems.ts +0 -59
  251. package/src/semantics/setup_intents.ts +0 -45
  252. package/src/semantics/subscription_items.ts +0 -54
  253. package/src/semantics/subscription_schedules.ts +0 -66
@@ -1,333 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Consent, CONSENT_CSS, flowPage } from '@volter/world-ui';
3
- import { PLATFORM_CLIENT_ID, SETTINGS_URL } from "../engine/connect-settings.js";
4
- import { OAUTH_CONNECTIONS, publicBusinessName } from "../engine/display.js";
5
- import { redactKey } from "../engine/oauth.js";
6
- import { accountSettings, nowUnix, PLATFORM_ACCOUNT_ID } from "../engine/stripe.js";
7
- import { formOf, notFound, seeOther, STRIPE_CONSENT_SKIN } from "./shared.js";
8
- import { connectOAuthSettings, created, decide } from "../semantics/shared.js";
9
- /** A code "expires in 5 minutes". */
10
- const CODE_TTL_SECONDS = 300;
11
- const CODES = '_oauth_code';
12
- /** One connection per connected account (the twin's one platform): its scope, tokens, and whether it was revoked. */
13
- const CONNECTIONS = OAUTH_CONNECTIONS;
14
- const SCOPES = ['read_write', 'read_only'];
15
- const json = (body, status = 200) => Response.json(body, { status, headers: { 'request-id': 'req_twin', 'cache-control': 'no-store' } });
16
- const oauthError = (error, description, status = 400, extra = {}) => json({ error, error_description: description, ...extra }, status);
17
- // ── authorize ──
18
- /** A query parameter, or undefined when absent (an empty value is absent too, as a browser form sends one). */
19
- const param = (q, k) => { const v = q.get(k); return v === null || v === '' ? undefined : v; };
20
- /** The authorize request checked as Stripe checks it, or the JSON error it answers. */
21
- function checked(ctx, q) {
22
- const state = param(q, 'state');
23
- const fail = (error, description) => oauthError(error, description, 400, state !== undefined ? { state } : {});
24
- const client = param(q, 'client_id');
25
- if (!client)
26
- return fail('invalid_request', 'No client_id provided.');
27
- if (client !== PLATFORM_CLIENT_ID)
28
- return fail('invalid_client', 'No application matches the supplied client identifier');
29
- const settings = connectOAuthSettings(ctx);
30
- if (!settings.oauth_enabled)
31
- return fail('invalid_client', `OAuth is not enabled for this application. Enable it in your Connect OAuth settings (${SETTINGS_URL}).`);
32
- const given = q.get('redirect_uri');
33
- let redirect;
34
- if (given !== null) {
35
- if (!settings.redirect_uris.includes(given))
36
- return fail('invalid_redirect_uri', `Invalid redirect URI '${given}'. Ensure this uri exactly matches one of the uris specified in your application settings`);
37
- redirect = given;
38
- }
39
- else {
40
- redirect = settings.redirect_uris[0];
41
- if (!redirect)
42
- return fail('invalid_redirect_uri', 'No redirect URI is configured in your application settings');
43
- }
44
- const type = q.get('response_type');
45
- if (type === null || type === '')
46
- return fail('invalid_request', 'Missing response_type parameter.');
47
- if (type !== 'code')
48
- return fail('unsupported_response_type', `Unsupported response_type parameter: ${type}. Currently the only supported response_type is code.`);
49
- const scope = param(q, 'scope') ?? 'read_only';
50
- if (!SCOPES.includes(scope))
51
- return fail('invalid_scope', `Invalid scope parameter provided: '${scope}'. Accepted scopes are 'read_write' or 'read_only'.`);
52
- return { redirect, scope: scope, state };
53
- }
54
- /** The redirect_uri with these parameters added, each percent-encoded (a space as %20, as Stripe's examples show). */
55
- function withParams(uri, params) {
56
- const [base, fragment] = uri.split('#', 2);
57
- const added = params.filter((p) => p[1] !== undefined).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&');
58
- return `${base}${base.includes('?') ? (base.endsWith('?') || base.endsWith('&') ? '' : '&') : '?'}${added}${fragment !== undefined ? `#${fragment}` : ''}`;
59
- }
60
- /** The prefill the request carries, invalid values silently dropped (docs.stripe.com/connect/oauth-reference). Only
61
- * what the new account's object shows the platform, or the page shows the person, is read: the person's birth date and
62
- * the business address are the account application's, which the twin does not model (the test-mode skip). */
63
- function prefill(q) {
64
- const v = (k) => param(q, `stripe_user[${k}]`)?.trim() || undefined;
65
- const out = {};
66
- const keep = (k, ok = () => true) => { const s = v(k); if (s !== undefined && ok(s))
67
- out[k] = s; };
68
- keep('email', (s) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s));
69
- keep('url', (s) => /^https?:\/\/\S+$/i.test(s));
70
- keep('country', (s) => /^[A-Z]{2}$/.test(s));
71
- keep('phone_number', (s) => /^\d{10}$/.test(s) && out.country !== undefined);
72
- keep('business_name');
73
- keep('business_type', (s) => ['sole_prop', 'corporation', 'non_profit', 'partnership', 'llc'].includes(s));
74
- keep('first_name');
75
- keep('last_name');
76
- keep('product_description');
77
- keep('currency', (s) => /^[a-z]{3}$/.test(s) && out.country !== undefined);
78
- return out;
79
- }
80
- // the default currency of an account in a country, where the prefill names none (docs.stripe.com/connect/currencies:
81
- // the account's default is its country's currency); a country outside this list is given usd
82
- const COUNTRY_CURRENCY = { US: 'usd', CA: 'cad', GB: 'gbp', AU: 'aud', NZ: 'nzd', JP: 'jpy', SG: 'sgd', HK: 'hkd', CH: 'chf', SE: 'sek', NO: 'nok', DK: 'dkk', PL: 'pln', MX: 'mxn', BR: 'brl', IN: 'inr', DE: 'eur', FR: 'eur', ES: 'eur', IT: 'eur', NL: 'eur', IE: 'eur', BE: 'eur', AT: 'eur', PT: 'eur', FI: 'eur' };
83
- const BUSINESS_TYPE = { sole_prop: 'individual', corporation: 'company', llc: 'company', partnership: 'company', non_profit: 'non_profit' };
84
- /** The Standard account the skipped form creates, from the prefill. */
85
- async function createAccount(ctx, p) {
86
- const country = p.country ?? 'US';
87
- const id = ctx.mint('account');
88
- const none = { alternatives: [], current_deadline: null, currently_due: [], disabled_reason: null, errors: [], eventually_due: [], past_due: [], pending_verification: [] };
89
- await created(ctx, 'account', { id }, {
90
- type: 'standard', country, default_currency: p.currency ?? COUNTRY_CURRENCY[country] ?? 'usd', email: p.email ?? null,
91
- business_type: p.business_type ? BUSINESS_TYPE[p.business_type] ?? null : null,
92
- business_profile: { name: p.business_name ?? null, url: p.url ?? null, product_description: p.product_description ?? null, support_phone: p.phone_number ?? null, mcc: null },
93
- charges_enabled: true, payouts_enabled: true, details_submitted: true,
94
- capabilities: { card_payments: 'active', transfers: 'active' },
95
- controller: { type: 'account' },
96
- requirements: none, future_requirements: none,
97
- settings: accountSettings(undefined, p.business_name ? { dashboard: { display_name: p.business_name, timezone: 'Etc/UTC' } } : undefined),
98
- external_accounts: { object: 'list', data: [], has_more: false, total_count: 0, url: `/v1/accounts/${id}/external_accounts` },
99
- tos_acceptance: { date: null, ip: null, user_agent: null }, metadata: {}, livemode: false,
100
- }, { operation: 'account.oauth_connect' });
101
- return id;
102
- }
103
- /** The platform's application as the account.application.* events carry it. */
104
- function application(ctx) {
105
- return { id: PLATFORM_CLIENT_ID, object: 'application', name: publicBusinessName(ctx.get('account', PLATFORM_ACCOUNT_ID)) };
106
- }
107
- /** Send one of the account.application.* events as the connected account's: the notice it is sent for (the kernel
108
- * sends it, ../semantics/events.ts renders it). */
109
- async function applicationEvent(ctx, type, account) {
110
- await ctx.write('_application_notice', `${account}:${type}:${ctx.occurredAt}`, { account, application: application(ctx) }, type);
111
- }
112
- function page(ctx, action, a, p) {
113
- const name = publicBusinessName(ctx.get('account', PLATFORM_ACCOUNT_ID));
114
- const who = [p.email, [p.first_name, p.last_name].filter(Boolean).join(' '), p.business_name].filter((s) => s).join(' · ');
115
- return flowPage({
116
- title: `Connect to ${name} – Stripe`,
117
- css: [CONSENT_CSS, STRIPE_CONSENT_SKIN],
118
- body: (_jsx(Consent, { heading: `Connect your Stripe account to ${name}`, app: { name }, request: "wants to connect to your Stripe account \u00B7 test mode", permissions: [
119
- a.scope === 'read_write'
120
- ? { title: 'Read and write access', detail: 'Create payments, customers and other data on your account, and read its data' }
121
- : { title: 'Read-only access', detail: 'Read your account’s data' },
122
- { title: 'A new test account', detail: who || 'No details prefilled' },
123
- ], action: action, fields: {}, deny: { name: 'decision', value: 'deny', label: 'Deny access' }, allow: { name: 'decision', value: 'skip', label: 'Skip this form' }, note: `Test mode: skipping the form creates a new test account and connects it. You will be returned to ${new URL(a.redirect).host}.` })),
124
- });
125
- }
126
- /** The person connects: a new Standard account, its connection's code, the authorized event, and the redirect. */
127
- async function connect(ctx, a, p) {
128
- const account = await createAccount(ctx, p);
129
- // the code's row id is minted and its value drawn from the World's secret; the row keeps the value's SHA-256 alone
130
- const id = ctx.mint(CODES);
131
- const code = `ac_${(await ctx.secret(`stripe-oauth-code:${id}`)).replace(/[^A-Za-z0-9]/g, '').slice(0, 24)}`;
132
- await ctx.record(CODES, { client_id: PLATFORM_CLIENT_ID, account, scope: a.scope, redirect_uri: a.redirect, expires_at: nowUnix(ctx.occurredAt) + CODE_TTL_SECONDS, used: false, livemode: false, sha256: ctx.crypto.sha256(code) }, id);
133
- await applicationEvent(ctx, 'account.application.authorized', account);
134
- return seeOther(withParams(a.redirect, [['scope', a.scope], ['code', code], ['state', a.state]]));
135
- }
136
- async function authorize(ctx) {
137
- const request = ctx.call.request;
138
- const url = new URL(request.url);
139
- const form = request.method === 'POST' ? await formOf(request.clone()) : {};
140
- const a = checked(ctx, url.searchParams);
141
- if (a instanceof Response)
142
- return a;
143
- const p = prefill(url.searchParams);
144
- if (request.method === 'GET')
145
- return page(ctx, `${url.pathname}${url.search}`, a, p);
146
- if (form.decision === 'deny')
147
- return seeOther(withParams(a.redirect, [['error', 'access_denied'], ['error_description', 'The user denied your request'], ['state', a.state]]));
148
- if (form.decision === 'skip')
149
- return connect(ctx, a, p);
150
- return page(ctx, `${url.pathname}${url.search}`, a, p);
151
- }
152
- // ── token and deauthorize ──
153
- /** The request's parameters: a form, as stripe-node and curl send them, or JSON. */
154
- async function bodyOf(request) {
155
- const text = await request.clone().text();
156
- if ((request.headers.get('content-type') ?? '').includes('json') || text.trim().startsWith('{')) {
157
- try {
158
- return Object.fromEntries(Object.entries(JSON.parse(text)).map(([k, v]) => [k, String(v)]));
159
- }
160
- catch {
161
- return {};
162
- }
163
- }
164
- return Object.fromEntries(new URLSearchParams(text));
165
- }
166
- /** The secret key a request carries: Bearer, Basic's user, or the client_secret parameter. */
167
- function secretKey(request, params) {
168
- const header = request.headers.get('authorization') ?? '';
169
- const bearer = /^bearer\s+(\S+)/i.exec(header)?.[1];
170
- if (bearer)
171
- return bearer;
172
- const basic = /^basic\s+(\S+)/i.exec(header)?.[1];
173
- if (basic) {
174
- try {
175
- const user = atob(basic).split(':')[0];
176
- if (user)
177
- return user;
178
- }
179
- catch { /* not base64 */ }
180
- }
181
- return params.client_secret || undefined;
182
- }
183
- /** The refusal of a request without a usable secret key, or undefined when it has one (answered with whether it is live). */
184
- function keyRefused(key) {
185
- if (!key)
186
- return oauthError('invalid_request', 'No API key provided. Provide your secret key in the Authorization header, using Bearer auth (e.g. \'Authorization: Bearer YOUR_SECRET_KEY\').', 401);
187
- if (!/^(sk|rk)_(test|live)_/.test(key))
188
- return oauthError('invalid_request', `Invalid API Key provided: ${redactKey(key)}. This endpoint requires a secret key.`, 401);
189
- // a connected account's access_token is that account's key, never the platform's ("Make this call using your secret API
190
- // key", the reference): it neither exchanges codes nor refreshes nor deauthorizes
191
- if (/^sk_test_oauth_/.test(key))
192
- return oauthError('invalid_request', `Invalid API Key provided: ${redactKey(key)}. This endpoint requires your platform's secret key.`, 401);
193
- return undefined;
194
- }
195
- const live = (key) => /^(sk|rk)_live_/.test(key);
196
- /** A new access token for a connection: its generation counted from the ones it had. */
197
- /** A connection's access token of one generation: its account, then a digest of the connection's secret and the
198
- * generation (the secret read before the atomic move that numbers the generation). */
199
- // p3: keyed by the connection's secret, drawn from ctx.secret
200
- const accessToken = (ctx, secret, account, n) => `sk_test_oauth_${account}_${ctx.crypto.digest('sha256', `${secret}:${n}`, 'base64url').replace(/[-_]/g, '').slice(0, 24)}`;
201
- /** Revoke an account's connection, once: the check and the write are one move, and only the move that revoked it sends
202
- * account.application.deauthorized. Answers whether this call revoked it. */
203
- async function revoke(ctx, account) {
204
- const revoked = await decide(ctx, (rows) => {
205
- const held = rows(CONNECTIONS).find((c) => c.id === account);
206
- if (!held || held.revoked === true)
207
- return { value: false };
208
- return { value: true, write: { resource: CONNECTIONS, id: account, fields: { revoked: true, access_token: null, revoked_at: nowUnix(ctx.occurredAt) }, operation: 'oauth_connection.revoke' } };
209
- });
210
- if (revoked)
211
- await applicationEvent(ctx, 'account.application.deauthorized', account);
212
- return revoked;
213
- }
214
- async function token(ctx) {
215
- const request = ctx.call.request;
216
- const params = await bodyOf(request);
217
- const key = secretKey(request, params);
218
- const refused = keyRefused(key);
219
- if (refused)
220
- return refused;
221
- const grant = params.grant_type;
222
- if (!grant)
223
- return oauthError('invalid_request', 'No grant type specified');
224
- if (grant !== 'authorization_code' && grant !== 'refresh_token')
225
- return oauthError('unsupported_grant_type', `Unsupported grant type: ${grant}. The only currently supported types are authorization_code and refresh_token.`);
226
- if (grant === 'authorization_code') {
227
- const code = params.code;
228
- if (!code)
229
- return oauthError('invalid_request', 'No authorization code provided');
230
- if (live(key))
231
- return oauthError('invalid_grant', `Authorization code provided does not belong to the livemode of the API key: ${code} is a test mode code, but a live mode key was used.`);
232
- const found = await decide(ctx, (rows) => {
233
- const row = rows(CODES).find((r) => r.sha256 === ctx.crypto.sha256(code));
234
- if (!row)
235
- return { value: { outcome: 'missing' } };
236
- // a second use is marked in the same move, so the exchange that spent it sees it after writing its connection
237
- if (row.used === true)
238
- return { value: { outcome: 'used', row }, write: { resource: CODES, id: String(row.id), fields: { reused: true }, operation: 'oauth_code.reuse' } };
239
- if (nowUnix(ctx.occurredAt) >= Number(row.expires_at))
240
- return { value: { outcome: 'expired', row } };
241
- return { value: { outcome: 'ok', row }, write: { resource: CODES, id: String(row.id), fields: { used: true, used_at: nowUnix(ctx.occurredAt) }, operation: 'oauth_code.redeem' } };
242
- });
243
- if (found.outcome === 'missing')
244
- return oauthError('invalid_grant', `Authorization code does not exist: ${code}`);
245
- const account = String(found.row.account);
246
- if (found.outcome === 'used') {
247
- // "Consuming an authorization code more than once revokes the account connection"
248
- await revoke(ctx, account);
249
- return oauthError('invalid_grant', 'This authorization code has already been used. All tokens issued with this code have been revoked.');
250
- }
251
- if (found.outcome === 'expired')
252
- return oauthError('invalid_grant', `Authorization code expired: ${code}`);
253
- const granted = String(found.row.scope);
254
- const refresh = `rt_${(await ctx.secret(`stripe-oauth-refresh:${String(found.row.id)}`)).replace(/[-_]/g, '').slice(0, 32)}`;
255
- const secret = await ctx.secret(`stripe-oauth-access:${account}`);
256
- const connection = { client_id: PLATFORM_CLIENT_ID, account, scope: granted, code: found.row.id, refresh_token: refresh, access_token: accessToken(ctx, secret, account, 1), access_scope: granted, generation: 1, publishable_key: `pk_test_oauth_${account}`, revoked: false, livemode: false };
257
- await ctx.record(CONNECTIONS, connection, account);
258
- // the code used again while this exchange was writing: that use found no connection to revoke, so this one does
259
- if (ctx.rowsRaw(CODES).some((r) => r.id === found.row.id && r.reused === true)) {
260
- await revoke(ctx, account);
261
- return oauthError('invalid_grant', 'This authorization code has already been used. All tokens issued with this code have been revoked.');
262
- }
263
- return json(tokenBody(connection, granted));
264
- }
265
- const refresh = params.refresh_token;
266
- if (!refresh)
267
- return oauthError('invalid_request', 'No refresh token provided');
268
- const held = ctx.rowsRaw(CONNECTIONS).find((c) => c.refresh_token === refresh && c.revoked !== true);
269
- if (!held)
270
- return oauthError('invalid_grant', `Refresh token does not exist: ${refresh}`);
271
- if (live(key))
272
- return oauthError('invalid_grant', 'The refresh token provided does not belong to the livemode of the API key.');
273
- const asked = params.scope || String(held.scope);
274
- if (!SCOPES.includes(asked))
275
- return oauthError('invalid_scope', `Invalid scope parameter provided: '${asked}'. Accepted scopes are 'read_write' or 'read_only'.`);
276
- if (asked === 'read_write' && held.scope !== 'read_write')
277
- return oauthError('invalid_scope', `The requested scope 'read_write' is greater than the scope of the refresh token ('${String(held.scope)}').`);
278
- const account = String(held.id);
279
- // "Any existing access token with the same scope and mode ... is revoked": the connection holds the one now issued; the
280
- // generation is read and written in one move, so two refreshes at once issue two tokens, and a refresh never lands
281
- // on a connection revoked meanwhile
282
- const secret = await ctx.secret(`stripe-oauth-access:${account}`);
283
- const renewed = await decide(ctx, (rows) => {
284
- const now = rows(CONNECTIONS).find((c) => c.id === account && c.revoked !== true);
285
- if (!now)
286
- return { value: undefined };
287
- const generation = Number(now.generation ?? 1) + 1;
288
- const fields = { access_token: accessToken(ctx, secret, account, generation), access_scope: asked, generation };
289
- return { value: { ...now, ...fields }, write: { resource: CONNECTIONS, id: account, fields, operation: 'oauth_connection.refresh' } };
290
- });
291
- if (!renewed)
292
- return oauthError('invalid_grant', `Refresh token does not exist: ${refresh}`);
293
- return json(tokenBody(renewed, asked));
294
- }
295
- /** The token endpoint's answer, as the reference lists it. */
296
- function tokenBody(c, scope) {
297
- return { access_token: c.access_token, livemode: false, refresh_token: c.refresh_token, scope, stripe_publishable_key: c.publishable_key, stripe_user_id: c.account ?? c.id, token_type: 'bearer' };
298
- }
299
- async function deauthorize(ctx) {
300
- const request = ctx.call.request;
301
- const params = await bodyOf(request);
302
- const key = secretKey(request, params);
303
- const refused = keyRefused(key);
304
- if (refused)
305
- return refused;
306
- const client = params.client_id;
307
- const account = params.stripe_user_id;
308
- if (!client)
309
- return oauthError('invalid_request', 'No client_id provided');
310
- if (!account)
311
- return oauthError('invalid_request', 'No stripe_user_id provided');
312
- if (client !== PLATFORM_CLIENT_ID)
313
- return oauthError('invalid_client', `No such application: '${client}'`);
314
- if (live(key))
315
- return oauthError('invalid_client', `The API key mode (live) does not match the mode of the client_id ${client} (test).`);
316
- // the check and the revocation are one move (revoke): of two deauthorizations at once, one revokes and the other finds
317
- // the account no longer connected
318
- if (!(await revoke(ctx, account)))
319
- return oauthError('invalid_client', `This application is not connected to stripe account ${account}, or that account does not exist.`);
320
- return json({ stripe_user_id: account });
321
- }
322
- /** connect.stripe.com's OAuth endpoints; Stripe's 404 for anything else there. */
323
- export async function screen(ctx) {
324
- const request = ctx.call.request;
325
- const path = new URL(request.url).pathname.replace(/\/+$/, '');
326
- if (path === '/oauth/authorize' && (request.method === 'GET' || request.method === 'POST'))
327
- return authorize(ctx);
328
- if (path === '/oauth/token' && request.method === 'POST')
329
- return token(ctx);
330
- if (path === '/oauth/deauthorize' && request.method === 'POST')
331
- return deauthorize(ctx);
332
- return notFound();
333
- }
@@ -1,2 +0,0 @@
1
- import type { HandlerContext } from '@volter/world-core';
2
- export declare function GetDisputesDispute(ctx: HandlerContext): Promise<Response>;
@@ -1,5 +0,0 @@
1
- import { at, disputesDisputeMissing } from "./shared.js";
2
- export async function GetDisputesDispute(ctx) {
3
- const d = ctx.get('dispute', at(ctx, 'dispute'));
4
- return d ? ctx.reply(d) : disputesDisputeMissing(ctx, at(ctx, 'dispute'));
5
- }
@@ -1,3 +0,0 @@
1
- import type { HandlerContext } from '@volter/world-core';
2
- export declare function DeleteInvoiceitemsInvoiceitem(ctx: HandlerContext): Promise<Response>;
3
- export declare function PostInvoiceitems(ctx: HandlerContext): Promise<Response>;
@@ -1,59 +0,0 @@
1
- import { INV, linesOf } from "../engine/invoices.js";
2
- import { nowUnix } from "../engine/stripe.js";
3
- import { at, created, fail, invoiceMissing, itemMissing, pricedItem, resyncDraftLines, writeLines } from "./shared.js";
4
- // an item is deleted only while its invoice is still editable
5
- export async function DeleteInvoiceitemsInvoiceitem(ctx) {
6
- const id = at(ctx, 'invoiceitem');
7
- const it = ctx.get('invoiceitem', id);
8
- if (!it)
9
- return itemMissing(ctx, id);
10
- const onInvoice = typeof it.invoice === 'string' ? ctx.get(INV, it.invoice) : undefined;
11
- if (onInvoice && onInvoice.status !== 'draft')
12
- return fail(ctx, 'You cannot delete an invoice item that is attached to an invoice that is no longer editable.', 400, 'invoice_not_editable');
13
- await ctx.write('invoiceitem', id, { deleted: true }, 'invoiceitem.delete');
14
- if (typeof it.invoice === 'string')
15
- await resyncDraftLines(ctx, it.invoice);
16
- return ctx.reply({ id, object: 'invoiceitem', deleted: true });
17
- }
18
- export async function PostInvoiceitems(ctx) {
19
- const params = ctx.params;
20
- const now = Number(nowUnix(ctx.occurredAt));
21
- const qty = Number(params.quantity) || 1;
22
- // the served version names the price in `pricing[price]` ("The pricing information for the invoice item", the served
23
- // spec); an older version sends `price`. It is kept as the item's price, which the answer renders as `pricing`
24
- // (../engine/version.ts)
25
- const { pricing: pricingParam, ...stored } = params;
26
- const given = pricingParam?.price;
27
- const priceRef = typeof given === 'string' && given ? given : typeof params.price === 'string' ? params.price : '';
28
- const priced = priceRef ? pricedItem(ctx, priceRef, qty) : undefined;
29
- if (priced instanceof Response)
30
- return priced;
31
- const resolvedAmount = priced?.amount;
32
- let resolvedCurrency = priced?.currency;
33
- // only a draft invoice takes new items
34
- const invoiceRef = typeof params.invoice === 'string' ? params.invoice : '';
35
- let target;
36
- if (invoiceRef) {
37
- target = ctx.get(INV, invoiceRef);
38
- if (!target)
39
- return invoiceMissing(ctx, invoiceRef);
40
- if (target.status !== 'draft')
41
- return fail(ctx, `This invoice cannot have invoice items added because it has status ${String(target.status)}.`, 400, 'invoice_not_editable');
42
- if (resolvedCurrency === undefined && typeof target.currency === 'string')
43
- resolvedCurrency = target.currency;
44
- }
45
- // an invoice item is dated by `date`, not `created`; its currency is the price's, else the invoice's
46
- const item = await created(ctx, 'invoiceitem', { ...stored, ...(priceRef ? { price: priceRef } : {}) }, {
47
- livemode: false, discountable: true, proration: false, quantity: qty, quantity_decimal: String(qty),
48
- period: { start: now, end: now }, currency: resolvedCurrency ?? 'usd',
49
- ...(resolvedAmount !== undefined ? { amount: resolvedAmount } : {}),
50
- }, { timeField: 'date' });
51
- if (target) {
52
- await writeLines(ctx, invoiceRef, [...linesOf(target), {
53
- id: `il_${String(item.id)}`, object: 'line_item', type: 'invoiceitem', amount: Number(item.amount) || 0, currency: String(target.currency ?? item.currency ?? 'usd'),
54
- quantity: qty, proration: false, invoice_item: item.id, price: item.price ?? null,
55
- period: item.period ?? { start: now, end: now }, description: item.description ?? null,
56
- }]);
57
- }
58
- return ctx.reply(item);
59
- }
@@ -1,3 +0,0 @@
1
- import type { HandlerContext } from '@volter/world-core';
2
- export declare function PostSetupIntents(ctx: HandlerContext): Promise<Response>;
3
- export declare function PostSetupIntentsIntentConfirm(ctx: HandlerContext): Promise<Response>;
@@ -1,41 +0,0 @@
1
- import { SI } from "../engine/setup-intents.js";
2
- import { asBool, microdepositsNextAction, mintClientSecret, requiresMicrodeposits } from "../engine/stripe.js";
3
- import { attachable, attachPaymentMethod, confirmOnly, created, load, mergeUpdate, methodFromData, microdepositsAsked } from "./shared.js";
4
- export async function PostSetupIntents(ctx) {
5
- // mandate_data and return_url each "can only be used with `confirm=true`" (the served spec; shared.ts confirmOnly)
6
- const unconfirmed = confirmOnly(ctx, ['mandate_data', 'return_url'], asBool(ctx.params.confirm));
7
- if (unconfirmed)
8
- return unconfirmed;
9
- // the id is minted first so the client secret can carry it: Stripe.js parses the id back out
10
- const id = typeof ctx.params.id === 'string' && ctx.params.id ? ctx.params.id : ctx.mint(SI);
11
- // it waits for a payment method until it has one, then for its confirmation (manifest.ts)
12
- const hasMethod = typeof ctx.params.payment_method === 'string' && ctx.params.payment_method !== '';
13
- if (hasMethod)
14
- ctx.legal(SI, 'status', 'PostSetupIntents', 'requires_payment_method', 'requires_confirmation', id);
15
- return ctx.reply(
16
- // allowed_payment_method_types in the served version names the methods (docs.stripe.com/api/setup_intents/create)
17
- await created(ctx, SI, { ...ctx.params, ...(Array.isArray(ctx.params.allowed_payment_method_types) ? { payment_method_types: ctx.params.allowed_payment_method_types.map(String) } : {}), id }, {
18
- status: hasMethod ? 'requires_confirmation' : 'requires_payment_method', usage: 'off_session', client_secret: mintClientSecret(id, await ctx.secret(`client-secret:${id}`)), payment_method_types: ['card'],
19
- next_action: null, latest_attempt: null, last_setup_error: null, livemode: false,
20
- }));
21
- }
22
- // a bank account that needs micro-deposit verification waits in requires_action; anything else succeeds. The confirm's
23
- // payment_method_data and mandate_data are instructions, not fields of the SetupIntent.
24
- export async function PostSetupIntentsIntentConfirm(ctx) {
25
- const loaded = load(ctx, 'PostSetupIntentsIntentConfirm');
26
- if ('answer' in loaded)
27
- return loaded.answer;
28
- const si = loaded.si;
29
- const id = String(si.id);
30
- const given = ctx.params;
31
- const { payment_method_data: data, mandate_data: _mandate, ...fields } = given;
32
- const made = await methodFromData(ctx, data);
33
- const pm = made ?? (typeof ctx.params.payment_method === 'string' ? ctx.params.payment_method : typeof si.payment_method === 'string' ? si.payment_method : undefined);
34
- if (si.status !== 'requires_action' && (requiresMicrodeposits(ctx.params, si) || microdepositsAsked(ctx, ctx.params, si, pm))) {
35
- return ctx.reply(await ctx.write(SI, id, mergeUpdate(ctx, SI, id, { ...fields, ...(pm ? { payment_method: pm } : {}), status: 'requires_action', next_action: microdepositsNextAction() }), 'setup_intent.requires_action'));
36
- }
37
- // a setup for a customer saves the method to them (docs.stripe.com/payments/save-and-reuse): its payment method is attached
38
- const customer = typeof si.customer === 'string' ? si.customer : undefined;
39
- const saved = pm && customer && attachable(ctx, pm) && ctx.get('payment_method', pm)?.customer !== customer ? String((await attachPaymentMethod(ctx, pm, customer)).id) : pm;
40
- return ctx.reply(await ctx.write(SI, id, mergeUpdate(ctx, SI, id, { ...fields, ...(saved ? { payment_method: saved } : {}), status: 'succeeded' }), 'setup_intent.confirm'));
41
- }
@@ -1,5 +0,0 @@
1
- import type { HandlerContext } from '@volter/world-core';
2
- export declare function DeleteSubscriptionItemsItem(ctx: HandlerContext): Promise<Response>;
3
- export declare function GetSubscriptionItemsItem(ctx: HandlerContext): Promise<Response>;
4
- export declare function PostSubscriptionItems(ctx: HandlerContext): Promise<Response>;
5
- export declare function PostSubscriptionItemsItem(ctx: HandlerContext): Promise<Response>;
@@ -1,55 +0,0 @@
1
- import { subscriptionItemPrice } from "../engine/stripe.js";
2
- import { ITEM_ATTRIBUTES, SUB } from "../engine/subscriptions.js";
3
- import { at, created, fail, finder, subMissing, subscriptionsItemMissing, syncItems, withPeriod } from "./shared.js";
4
- export async function DeleteSubscriptionItemsItem(ctx) {
5
- const id = at(ctx, 'item');
6
- const si = ctx.get('subscription_item', id);
7
- if (!si)
8
- return subscriptionsItemMissing(ctx, id);
9
- await ctx.write('subscription_item', id, { deleted: true }, 'subscription_item.delete');
10
- await syncItems(ctx, String(si.subscription));
11
- return ctx.reply({ id, object: 'subscription_item', deleted: true });
12
- }
13
- export async function GetSubscriptionItemsItem(ctx) {
14
- const si = ctx.get('subscription_item', at(ctx, 'item'));
15
- return si ? ctx.reply(withPeriod(ctx, si)) : subscriptionsItemMissing(ctx, at(ctx, 'item'));
16
- }
17
- export async function PostSubscriptionItems(ctx) {
18
- const params = ctx.params;
19
- const subId = typeof params.subscription === 'string' ? params.subscription : '';
20
- if (!subId)
21
- return fail(ctx, 'Missing required param: subscription.', 400, 'parameter_missing');
22
- if (!ctx.get(SUB, subId))
23
- return subMissing(ctx, subId);
24
- const priceRef = typeof params.price === 'string' ? params.price : '';
25
- if (!priceRef && !(params.price_data && typeof params.price_data === 'object'))
26
- return fail(ctx, 'Missing required param: price.', 400, 'parameter_missing');
27
- if (priceRef && !ctx.get('price', priceRef))
28
- return fail(ctx, `No such price: '${priceRef}'`, 400, 'resource_missing');
29
- const quantity = params.quantity !== undefined ? Math.max(0, Math.trunc(Number(params.quantity) || 0)) : 1;
30
- // "metadata: Set of key-value pairs that you can attach to an object" (docs.stripe.com/api/subscription_items/create)
31
- const metadata = params.metadata && typeof params.metadata === 'object' ? { metadata: params.metadata } : {};
32
- const body = await created(ctx, 'subscription_item', { subscription: subId, ...(priceRef ? subscriptionItemPrice(priceRef, finder(ctx)) : { price: null, plan: null }), quantity, ...metadata }, { livemode: false, metadata: {} });
33
- await syncItems(ctx, subId);
34
- return ctx.reply(withPeriod(ctx, body));
35
- }
36
- export async function PostSubscriptionItemsItem(ctx) {
37
- const id = at(ctx, 'item');
38
- const si = ctx.get('subscription_item', id);
39
- if (!si)
40
- return subscriptionsItemMissing(ctx, id);
41
- // only the item's own attributes are kept; the update's instructions (off_session, payment_behavior, price_data,
42
- // proration_behavior, proration_date) are not attributes of the Subscription Item object
43
- // (docs.stripe.com/api/subscription_items/object lists none of them)
44
- const fields = Object.fromEntries(Object.entries(ctx.params).filter(([k]) => ITEM_ATTRIBUTES.has(k)));
45
- if (ctx.params.quantity !== undefined)
46
- fields.quantity = Math.max(0, Math.trunc(Number(ctx.params.quantity) || 0));
47
- if (typeof ctx.params.price === 'string' && ctx.params.price && !ctx.get('price', ctx.params.price))
48
- return fail(ctx, `No such price: '${ctx.params.price}'`, 400, 'resource_missing');
49
- // the item carries its Price and plan, as the subscription's own items do (subscriptionItemPrice)
50
- if (typeof ctx.params.price === 'string' && ctx.params.price)
51
- Object.assign(fields, subscriptionItemPrice(ctx.params.price, finder(ctx)));
52
- const out = await ctx.write('subscription_item', id, fields, 'subscription_item.update');
53
- await syncItems(ctx, String(si.subscription));
54
- return ctx.reply(withPeriod(ctx, out));
55
- }
@@ -1,4 +0,0 @@
1
- import type { HandlerContext } from '@volter/world-core';
2
- export declare function GetSubscriptionSchedulesSchedule(ctx: HandlerContext): Promise<Response>;
3
- export declare function PostSubscriptionSchedules(ctx: HandlerContext): Promise<Response>;
4
- export declare function PostSubscriptionSchedulesScheduleRelease(ctx: HandlerContext): Promise<Response>;
@@ -1,60 +0,0 @@
1
- import { buildSubscriptionItemsList, nowUnix } from "../engine/stripe.js";
2
- import { SS } from "../engine/subscription-schedules.js";
3
- import { at, created, end, fail, finder, phasesOf, scheduleMissing, storeItems } from "./shared.js";
4
- export async function GetSubscriptionSchedulesSchedule(ctx) {
5
- const s = ctx.get(SS, at(ctx, 'schedule'));
6
- return s ? ctx.reply(s) : scheduleMissing(ctx, at(ctx, 'schedule'));
7
- }
8
- // a schedule starts from a customer or from an existing subscription
9
- export async function PostSubscriptionSchedules(ctx) {
10
- const params = ctx.params;
11
- const customer = typeof params.customer === 'string' ? params.customer : '';
12
- const fromSub = typeof params.from_subscription === 'string' ? params.from_subscription : '';
13
- if (!customer && !fromSub)
14
- return fail(ctx, 'Missing required param: customer (or from_subscription).', 400, 'parameter_missing');
15
- if (customer && !ctx.get('customer', customer))
16
- return fail(ctx, `No such customer: '${customer}'`, 404, 'resource_missing');
17
- if (fromSub && !ctx.get('subscription', fromSub))
18
- return fail(ctx, `No such subscription: '${fromSub}'`, 404, 'resource_missing');
19
- // start_date is an instruction ("now" or when), not a field of the schedule
20
- const start = params.start_date !== undefined && params.start_date !== 'now' ? Number(params.start_date) : Number(nowUnix(ctx.occurredAt));
21
- const phases = phasesOf(ctx, Array.isArray(params.phases) ? params.phases : [], start);
22
- const { phases: _p, start_date: _s, default_settings: givenDefaults, ...rest } = params;
23
- const defaults = givenDefaults && typeof givenDefaults === 'object' ? givenDefaults : {};
24
- const schedId = ctx.mint(SS);
25
- // a schedule for a customer whose first phase starts now starts its subscription at once, on that phase's items
26
- // (docs.stripe.com/billing/subscriptions/subscription-schedules#managing)
27
- let subscription = fromSub || null;
28
- if (!fromSub && phases.length) {
29
- const now = Number(nowUnix(ctx.occurredAt));
30
- const subId = ctx.mint('subscription');
31
- const items = buildSubscriptionItemsList(phases[0].items, subId, now, finder(ctx));
32
- await created(ctx, 'subscription', { customer, id: subId }, {
33
- status: 'active', livemode: false, currency: 'usd', collection_method: String(params.default_settings?.collection_method ?? 'charge_automatically'),
34
- cancel_at_period_end: false, start_date: now, billing_cycle_anchor: now, metadata: {}, discounts: [], billing_schedules: [],
35
- trial_start: null, trial_end: null, current_period_start: now, current_period_end: phases[0].end_date, items, schedule: schedId,
36
- automatic_tax: { enabled: false, liability: null }, billing_mode: { type: 'classic' }, invoice_settings: { issuer: { type: 'self' } },
37
- });
38
- await storeItems(ctx, subId, items.data ?? []);
39
- subscription = subId;
40
- }
41
- return ctx.reply(await created(ctx, SS, { ...rest, id: schedId }, {
42
- status: 'active', livemode: false, phases, current_phase: phases.length ? { start_date: phases[0].start_date, end_date: phases[0].end_date } : null,
43
- subscription, released_subscription: null, released_at: null,
44
- canceled_at: null, completed_at: null, end_behavior: params.end_behavior ?? 'release',
45
- renewal_interval: null, metadata: {}, billing_mode: { type: 'classic' },
46
- // "billing_cycle_anchor: Possible values are phase_start or automatic" and invoice_settings are required (the
47
- // served spec's default settings); the rest answer what the request set, else null
48
- default_settings: {
49
- application_fee_percent: defaults.application_fee_percent ?? null, automatic_tax: { enabled: false, liability: null, disabled_reason: null },
50
- billing_cycle_anchor: typeof defaults.billing_cycle_anchor === 'string' ? defaults.billing_cycle_anchor : 'automatic', billing_thresholds: null,
51
- collection_method: typeof defaults.collection_method === 'string' ? defaults.collection_method : 'charge_automatically', default_payment_method: defaults.default_payment_method ?? null,
52
- description: defaults.description ?? null, invoice_settings: { account_tax_ids: null, days_until_due: null, issuer: { type: 'self' } }, on_behalf_of: null, transfer_data: null,
53
- },
54
- // a schedule made from a subscription is its customer's
55
- customer: customer || String(ctx.get('subscription', fromSub)?.customer ?? ''),
56
- }));
57
- }
58
- export async function PostSubscriptionSchedulesScheduleRelease(ctx) {
59
- return (end('release'))(ctx);
60
- }
@@ -1,16 +0,0 @@
1
- // Billing semantics: meters (how usage events aggregate), the meter events that report usage and
2
- // the summaries that aggregate them, prepaid credit grants and the balance they leave, and usage
3
- // alerts. The machines in ../manifest.ts say how a meter and an alert move. Lists and retrieves
4
- // with no scoping of their own are the derived core's.
5
- import type { Row } from './common.ts';
6
-
7
- export const METER = 'billing.meter';
8
- export const objectOf = (v: unknown): Row => (v && typeof v === 'object' ? (v as Row) : {});
9
-
10
- // ── credit grants: a customer's prepaid credit, paid or promotional, in a monetary amount ──
11
-
12
- export const GRANT = 'billing.credit_grant';
13
-
14
- // ── alerts: a usage threshold on a meter ──
15
-
16
- export const ALERT = 'billing.alert';
@@ -1,19 +0,0 @@
1
- // Charge semantics. The machine in ../manifest.ts says a charge is captured once; these handlers
2
- // compute the money: a charge's `refunds` sub-list is derived on every read from the refunds that
3
- // point at it, never a stored snapshot, while the cumulative `amount_refunded` / `refunded` stay
4
- // stored (a partial capture moves them for a reason that is also a Refund row).
5
- import type { Row } from './common.ts';
6
-
7
- // A charge is written as the event Stripe sends for it (docs.stripe.com/api/events/types; Stripe has no
8
- // `charge.created`): charge.succeeded, "Occurs whenever a charge is successful", for one made or authorized
9
- // (capture=false: its status is succeeded, captured false), and charge.failed, "Occurs whenever a failed charge attempt
10
- // occurs", for a declined attempt. The twin makes no pending charge (charge.pending, "Occurs whenever a pending charge is
11
- // created"): its charges settle when made.
12
- /** The create's parameters as the charge keeps them: without `card`, the request's card details ("A token, like the ones
13
- * returned by Stripe.js", or a hash of the number, expiry and CVC). The Charge object has no `card` property (served
14
- * spec, charge); the card it was made with is its payment_method_details. Kept, it answered the card's full number and
15
- * CVC back on every read of the charge. */
16
- export function withoutCard(params: Row): Row {
17
- const { card: _card, ...rest } = params;
18
- return rest;
19
- }