@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
@@ -1,17 +1,18 @@
1
1
  // Stripe's subscriptions operations (docs/contributing/architecture.md, "What an author writes"): each handler over the
2
2
  // handler contract's context, computing with the engine (../engine/).
3
3
  import type { HandlerContext } from '@volter/world-core';
4
- import { deletedDiscount, type Row } from '../engine/common.ts';
5
- import { itemQuantity, keptAfter } from '../engine/renewals.ts';
6
- import { addBillingInterval, applyCouponDiscount, asBool, buildDiscount, buildSubscriptionItemsList, declineFor, mintClientSecret, nowUnix, priceAmount, resolveSubscriptionBillingInterval, resolveTrial, subscriptionCouponParam, subscriptionHasPrice, subscriptionItemEntries } from '../engine/stripe.ts';
4
+ import { type Row } from '../engine/common.ts';
5
+ import { keptAfter } from '../engine/renewals.ts';
6
+ import { asBool, nowUnix, subscriptionCouponParam, subscriptionHasPrice, subscriptionItemEntries } from '../engine/stripe.ts';
7
7
  import { pauseCollection, SUB } from '../engine/subscriptions.ts';
8
- import { advanceBilling, applyItemChanges, applyTrialEnd, at, clockNow, collectSubscriptionInvoice, created, draftSubscriptionInvoice, expanded, fail, finalizedFields, finder, firstPaymentRefused, itemChangesRefused, list, mintCharge, newest, redeemCoupon, replacedDiscounts, search, spendOnceCoupon, storeItems, subMissing, trialEndBlocked, trialEndRefused, where } from './shared.ts';
9
-
8
+ import { resourceView, advanceBilling, applyItemChanges, applyTrialEnd, at, clockNow, collectSubscriptionInvoice, expanded, itemChangesRefused, list, newest, redeemCoupon, replacedDiscounts, subMissing, trialEndBlocked, trialEndRefused, where } from './shared.ts';
9
+ import { deletedDiscount } from '../engine/common.ts';
10
+ import { fail } from './shared.ts';
10
11
  // canceling keeps the subscription, now canceled, stamped with when it was canceled and ended
11
12
  // (https://docs.stripe.com/api/subscriptions/object#subscription_object-canceled_at, #subscription_object-ended_at)
12
13
  export async function DeleteSubscriptionsSubscriptionExposedId(ctx: HandlerContext): Promise<Response> {
13
14
  const id = at(ctx, 'subscription_exposed_id');
14
- const sub = ctx.get(SUB, id);
15
+ const sub = resourceView(ctx, SUB, id);
15
16
  if (!sub) return subMissing(ctx, id);
16
17
  const refused = ctx.legal(SUB, 'status', 'DeleteSubscriptionsSubscriptionExposedId', sub.status, undefined, id);
17
18
  if (refused) return ctx.refuse(refused);
@@ -21,7 +22,7 @@ export async function DeleteSubscriptionsSubscriptionExposedId(ctx: HandlerConte
21
22
  // `"reason": "cancellation_requested"` (docs.stripe.com/api/subscriptions/cancel)
22
23
  const given = ctx.params.cancellation_details && typeof ctx.params.cancellation_details === 'object' ? (ctx.params.cancellation_details as Row) : {};
23
24
  const cancellation_details = { comment: given.comment ?? null, feedback: given.feedback ?? null, reason: 'cancellation_requested' };
24
- return ctx.reply(await ctx.write(SUB, id, { status: 'canceled', canceled_at: now, ended_at: now, cancellation_details }, 'subscription.cancel'));
25
+ return ctx.reply(ctx.expand(SUB, await ctx.write(SUB, id, { status: 'canceled', canceled_at: now, ended_at: now, cancellation_details }, 'subscription.cancel')));
25
26
  }
26
27
 
27
28
  export async function GetSubscriptions(ctx: HandlerContext): Promise<Response> {
@@ -33,153 +34,18 @@ export async function GetSubscriptions(ctx: HandlerContext): Promise<Response> {
33
34
  }));
34
35
  }
35
36
 
36
- export async function GetSubscriptionsSearch(ctx: HandlerContext): Promise<Response> {
37
- await advanceBilling(ctx);
38
- return search(ctx, SUB);
39
- }
40
-
41
37
  export async function GetSubscriptionsSubscriptionExposedId(ctx: HandlerContext): Promise<Response> {
42
38
  const id = at(ctx, 'subscription_exposed_id');
43
39
  await advanceBilling(ctx, (sub) => sub.id === id);
44
- const sub = ctx.get(SUB, id);
40
+ const sub = resourceView(ctx, SUB, id);
45
41
  return sub ? ctx.reply(expanded(ctx, SUB, sub)) : subMissing(ctx, id);
46
42
  }
47
43
 
48
- export async function PostSubscriptions(ctx: HandlerContext): Promise<Response> {
49
- const params = ctx.params;
50
- const customer = typeof params.customer === 'string' ? params.customer : '';
51
- if (!customer) return fail(ctx, 'Missing required param: customer.', 400, 'parameter_missing');
52
- if (!ctx.get('customer', customer)) return fail(ctx, `No such customer: '${customer}'`, 404, 'resource_missing');
53
- const now = clockNow(ctx, customer);
54
- const badTrial = trialEndRefused(ctx, params.trial_end, now);
55
- if (badTrial) return badTrial;
56
- // a coupon (or discounts[0][coupon]) must exist; it becomes the subscription's discount
57
- const couponId = subscriptionCouponParam(params);
58
- let discounts: Row[] = [];
59
- if (couponId) {
60
- const coupon = ctx.get('coupon', couponId);
61
- if (!coupon) return fail(ctx, `No such coupon: '${couponId}'`, 400, 'resource_missing');
62
- discounts = [buildDiscount(coupon, customer, now)];
63
- }
64
- // a trial is the current period; otherwise one billing interval of the first item's price. The
65
- // period sits top-level: this twin's default API version predates basil moving it onto items.
66
- const trial = resolveTrial(params, now);
67
- const itemEntries = subscriptionItemEntries(params.items);
68
- const { interval, interval_count } = resolveSubscriptionBillingInterval(itemEntries, finder(ctx));
69
- const periodStart = trial ? trial.start : now;
70
- const periodEnd = trial ? trial.end : addBillingInterval(now, interval, interval_count);
71
- const subId = typeof params.id === 'string' && params.id ? params.id : ctx.mint(SUB);
72
- const itemsList = buildSubscriptionItemsList(itemEntries, subId, now, finder(ctx));
73
- const itemsData = itemsList.data as Row[];
74
- const currency = typeof params.currency === 'string' && params.currency ? params.currency : 'usd';
75
-
76
- // Stripe opens a subscription's first invoice at once (a trialing one has nothing due yet) and, by
77
- // payment_behavior (docs.stripe.com/api/subscriptions/create#create_subscription-payment_behavior):
78
- // allow_incomplete (the default) charges the default payment method now, and without one, or on a decline,
79
- // leaves the subscription incomplete with the invoice open; error_if_incomplete refuses the create instead;
80
- // default_incomplete never charges, leaving a PaymentIntent the frontend confirms.
81
- const behavior = typeof params.payment_behavior === 'string' ? params.payment_behavior : 'allow_incomplete';
82
- const needsFirstInvoice = !trial;
83
- const holder = ctx.get('customer', customer) ?? {};
84
- const payWith = behavior === 'default_incomplete' ? undefined
85
- : (typeof params.default_payment_method === 'string' ? params.default_payment_method : undefined) ?? ((holder.invoice_settings as Row | undefined)?.default_payment_method as string | undefined) ?? undefined;
86
- const declined = payWith ? declineFor(finder(ctx), { payment_method: payWith }) : undefined;
87
- let subtotal = 0;
88
- for (const it of itemsData) {
89
- const price = it.price && typeof it.price === 'object' ? (it.price as Row) : undefined;
90
- subtotal += priceAmount(price, itemQuantity(it));
91
- }
92
- const coupon = discounts[0]?.coupon as Row | undefined;
93
- const discountAmount = applyCouponDiscount(subtotal, coupon);
94
- const invoiceTotal = Math.max(0, subtotal - discountAmount);
95
- // a $0 first invoice is paid at once and needs no PaymentIntent
96
- const isFree = invoiceTotal <= 0;
97
- const pays = needsFirstInvoice && !isFree && !!payWith && !declined;
98
- if (needsFirstInvoice && !isFree && !pays && behavior === 'error_if_incomplete') return firstPaymentRefused(ctx, declined);
99
- // minted first so the subscription names its latest_invoice in its own create (a trial's $0 invoice too)
100
- const invoiceId = needsFirstInvoice ? ctx.mint('invoice') : null;
101
- const trialInvoiceId = needsFirstInvoice ? null : ctx.mint('invoice');
102
- const piId = needsFirstInvoice && !isFree ? ctx.mint('payment_intent') : null;
103
-
104
- const { coupon: _c, discounts: _ds, trial_period_days: _tpd, trial_end: _te, items: _items, id: _id, ...rest } = params;
105
- const sub = await created(ctx, SUB, { ...rest, id: subId }, {
106
- status: trial ? 'trialing' : needsFirstInvoice && !isFree && !pays ? 'incomplete' : 'active',
107
- livemode: false, currency, collection_method: 'charge_automatically',
108
- // a trial's end is the billing anchor: "The `billing_cycle_anchor` will be updated to the `trial_end` value"
109
- // (docs.stripe.com/api/subscriptions/create#create_subscription-trial_end)
110
- cancel_at_period_end: false, start_date: now, billing_cycle_anchor: trial ? trial.end : now, metadata: {},
111
- discounts: discounts.map((d) => ({ ...d, subscription: subId })), billing_schedules: [],
112
- ...(trial ? { trial_start: trial.start, trial_end: trial.end } : { trial_start: null, trial_end: null }),
113
- current_period_start: periodStart, current_period_end: periodEnd,
114
- items: itemsList,
115
- latest_invoice: invoiceId ?? trialInvoiceId,
116
- automatic_tax: { enabled: false, liability: null },
117
- billing_mode: { type: 'classic' },
118
- invoice_settings: { issuer: { type: 'self' } },
119
- });
120
- await storeItems(ctx, subId, itemsData);
121
- if (discounts.length) await redeemCoupon(ctx, couponId);
122
- // a customer billed for the first time takes the subscription's currency
123
- if (!holder.currency) await ctx.write('customer', customer, { currency }, 'customer.update');
124
- if (!invoiceId) {
125
- // a trial's subscription still opens an invoice at once: "An immediate invoice is still created, but the amount is 0"
126
- // (docs.stripe.com/billing/subscriptions/trials/free-trials); finalizing it spends a once coupon (renewals.ts)
127
- const trialInvoice = await draftSubscriptionInvoice(ctx, ctx.get(SUB, subId) ?? sub, { start: periodStart, end: periodEnd, reason: 'subscription_create', trial: true, ...(trialInvoiceId ? { id: trialInvoiceId } : {}) });
128
- await collectSubscriptionInvoice(ctx, trialInvoice, now, 'api');
129
- return ctx.reply(expanded(ctx, SUB, ctx.get(SUB, subId) ?? sub));
130
- }
131
- // the default payment method pays the first invoice now: its charge (with its ledger entry) comes first
132
- const chargeId = pays && piId ? await mintCharge(ctx, piId, { currency, customer, payment_method: payWith, invoice: invoiceId }, invoiceTotal) : null;
133
-
134
- const invoiceLines = itemsData.map((it, i) => {
135
- const price = it.price && typeof it.price === 'object' ? (it.price as Row) : undefined;
136
- const quantity = itemQuantity(it);
137
- return {
138
- id: `il_twin_${invoiceId}_${i + 1}`, object: 'line_item', type: 'subscription',
139
- amount: priceAmount(price, quantity), currency, quantity, proration: false,
140
- price: it.price ?? null, subscription: subId, invoice_item: null, subscription_item: it.id ?? null,
141
- period: { start: periodStart, end: periodEnd }, description: null,
142
- };
143
- });
144
- await created(ctx, 'invoice', {
145
- id: invoiceId,
146
- ...finalizedFields(ctx, { id: invoiceId, customer }, now),
147
- status: isFree || pays ? 'paid' : 'open', livemode: false, currency, collection_method: 'charge_automatically',
148
- auto_advance: !(isFree || pays), attempt_count: payWith && !isFree ? 1 : 0, attempted: !isFree && behavior !== 'default_incomplete',
149
- customer, subscription: subId, billing_reason: 'subscription_create', charge: chargeId,
150
- amount_due: invoiceTotal, amount_paid: isFree || pays ? invoiceTotal : 0, amount_remaining: isFree || pays ? 0 : invoiceTotal,
151
- amount_overpaid: 0, amount_paid_off_stripe: 0, amount_shipping: 0,
152
- subtotal, total: invoiceTotal, starting_balance: 0,
153
- post_payment_credit_notes_amount: 0, pre_payment_credit_notes_amount: 0,
154
- period_start: periodStart, period_end: periodEnd,
155
- default_tax_rates: [], discounts: discounts[0] ? [discounts[0]] : [],
156
- lines: { object: 'list', data: invoiceLines, has_more: false, total_count: invoiceLines.length, url: `/v1/invoices/${invoiceId}/lines` },
157
- automatic_tax: { enabled: false, liability: null, status: null },
158
- status_transitions: { finalized_at: now, marked_uncollectible_at: null, paid_at: isFree || pays ? now : null, voided_at: null },
159
- issuer: { type: 'self' },
160
- payment_settings: { default_mandate: null, payment_method_options: null, payment_method_types: null },
161
- payment_intent: piId,
162
- }, {});
163
- if (piId) {
164
- // paid by the default payment method, or waiting on one (declined, absent, or collected client-side)
165
- await created(ctx, 'payment_intent', { id: piId, amount: invoiceTotal, currency, customer, invoice: invoiceId, ...(payWith ? { payment_method: pays ? payWith : null } : {}) }, {
166
- status: pays ? 'succeeded' : 'requires_payment_method', client_secret: mintClientSecret(piId, await ctx.secret(`client-secret:${piId}`)), livemode: false,
167
- capture_method: 'automatic', amount_capturable: 0, amount_received: pays ? invoiceTotal : 0, next_action: null,
168
- latest_charge: chargeId,
169
- last_payment_error: declined ? { type: 'card_error', code: declined.code, decline_code: declined.decline_code ?? null, message: declined.message, payment_method: { id: payWith } } : null,
170
- automatic_payment_methods: null, payment_method_types: ['card', 'link'], payment_method_options: {},
171
- });
172
- }
173
- // the first invoice is finalized: the `once` coupon it took is spent (renewals.ts spendOnceCoupon)
174
- await spendOnceCoupon(ctx, ctx.get('invoice', invoiceId) ?? {});
175
- return ctx.reply(expanded(ctx, SUB, ctx.get(SUB, subId) ?? sub));
176
- }
177
-
178
44
  export async function PostSubscriptionsSubscriptionExposedId(ctx: HandlerContext): Promise<Response> {
179
45
  const id = at(ctx, 'subscription_exposed_id');
180
46
  // a period that has already ended renews (or a trial that has, ends) before the update applies (renewals.ts)
181
47
  await advanceBilling(ctx, (s) => s.id === id);
182
- const sub = ctx.get(SUB, id);
48
+ const sub = resourceView(ctx, SUB, id);
183
49
  if (!sub) return subMissing(ctx, id);
184
50
  const params = ctx.params;
185
51
  const now = clockNow(ctx, sub.customer);
@@ -223,5 +89,19 @@ export async function PostSubscriptionsSubscriptionExposedId(ctx: HandlerContext
223
89
  fields.status = pays ? 'active' : 'past_due';
224
90
  if (fields.discounts !== undefined) fields.discounts = keptAfter(fields.discounts as Row[], ctx.get('invoice', trial.invoice) ?? {});
225
91
  }
226
- return ctx.reply(await ctx.write(SUB, id, fields, op));
92
+ return ctx.reply(ctx.expand(SUB, await ctx.write(SUB, id, fields, op)));
93
+ }
94
+
95
+
96
+
97
+ export async function DeleteSubscriptionsSubscriptionExposedIdDiscount(ctx: HandlerContext): Promise<Response> {
98
+ const id = at(ctx, 'subscription_exposed_id');
99
+ const sub = resourceView(ctx, SUB, id);
100
+ if (!sub) return subMissing(ctx, id);
101
+ // source: spec:DeleteSubscriptionsSubscriptionExposedIdDiscount "Removes the currently applied discount on a subscription."
102
+ const current = ((sub.discounts as unknown[] | undefined) ?? [])[0] ?? sub.discount;
103
+ // Where the documentation stops and the twin decides: a subscription with no discount answers its absence as a missing resource.
104
+ if (!current) return fail(ctx, `No active discount for subscription: '${id}'`, 404, 'resource_missing');
105
+ await ctx.write(SUB, id, { discounts: [] }, 'subscription.update');
106
+ return ctx.reply(deletedDiscount(typeof current === 'string' ? { id: current } : { ...(current as Row), subscription: id }, nowUnix(ctx.occurredAt)));
227
107
  }
@@ -4,50 +4,116 @@ import type { HandlerContext } from '@volter/world-core';
4
4
  import type { Row } from '../engine/common.ts';
5
5
  import { AUTH, CARD, cardId, CH, historyEntry, holderId, merchantData, TXN } from '../engine/issuing.ts';
6
6
  import { isValidApiVersion, nowUnix, spendingControlsViolation, TWIN_API_VERSION } from '../engine/stripe.ts';
7
- import { READER } from '../engine/terminal.ts';
8
- import { CLOCK } from '../engine/test-clocks.ts';
9
- import { FA, TX } from '../engine/treasury.ts';
10
7
  import { stripeEventMatches } from '../engine/webhooks.ts';
11
- import { answeredByEndpoint, approvedAtOnce, askForAuthorization, at, authMissing, balanceOf, capture, cashBalance, created, customerMissing, fail, holdForCapture, issuingRefusal, mintCharge, nothingToPresent, readerMissing, received, reconcile, review, withCard } from './shared.ts';
12
-
13
- /** A test clock deleted with what lives on it: "Deleting the simulation also deletes the test customers associated
14
- * with the clock and cancels their subscriptions" (docs.stripe.com/billing/testing/test-clocks/api-advanced-usage). */
15
- export async function DeleteTestHelpersTestClocksTestClock(ctx: HandlerContext): Promise<Response> {
16
- const id = at(ctx, 'test_clock');
17
- if (!ctx.get(CLOCK, id)) return fail(ctx, `No such test clock: '${id}'`, 404, 'resource_missing');
18
- const now = Number(nowUnix(ctx.occurredAt));
19
- for (const c of ctx.rows('customer').filter((c) => c.test_clock === id)) {
20
- for (const sub of ctx.rows('subscription').filter((s) => s.customer === c.id && s.status !== 'canceled')) {
21
- ctx.legal('subscription', 'status', 'DeleteTestHelpersTestClocksTestClock', String(sub.status), 'canceled', String(sub.id));
22
- await ctx.write('subscription', String(sub.id), { status: 'canceled', canceled_at: now, ended_at: now }, 'subscription.cancel');
23
- }
24
- await ctx.write('customer', String(c.id), { deleted: true }, 'customer.deleted');
25
- }
26
- await ctx.write(CLOCK, id, { deleted: true }, 'test_clock.deleted');
27
- return ctx.reply({ id, object: 'test_helpers.test_clock', deleted: true });
8
+ import { answeredByEndpoint, approvedAtOnce, askForAuthorization, at, authMissing, balanceOf, capture, created, fail, issuingRefusal, withCard } from './shared.ts';
9
+ export async function PostTestHelpersIssuingAuthorizations(ctx: HandlerContext): Promise<Response> {
10
+ const params = ctx.params;
11
+ const card = typeof params.card === 'string' ? params.card : '';
12
+ if (!card) return fail(ctx, 'Missing required param: card.', 400, 'parameter_missing');
13
+ const c = ctx.get(CARD, card);
14
+ if (!c) return fail(ctx, `No such card: '${card}'`, 400, 'resource_missing');
15
+ if (params.amount === undefined) return fail(ctx, 'Missing required param: amount.', 400, 'parameter_missing');
16
+ const amount = Math.trunc(Number(params.amount) || 0);
17
+ const currency = String(params.currency ?? c.currency ?? 'usd');
18
+ const createdSec = Number(nowUnix(ctx.occurredAt));
19
+ const cardholderId = holderId(c);
20
+ const merchant = merchantData(params.merchant_data);
21
+ const isAmountControllable = params.is_amount_controllable === true;
22
+ const authMethod = typeof params.authorization_method === 'string' ? params.authorization_method : 'online';
23
+ if (!['chip', 'contactless', 'keyed_in', 'online', 'swipe'].includes(authMethod)) return issuingRefusal(ctx, 'authorization_method');
24
+ const merchantAmount = params.merchant_amount !== undefined ? Math.trunc(Number(params.merchant_amount) || 0) : amount;
25
+ const merchantCurrency = typeof params.merchant_currency === 'string' ? params.merchant_currency : currency;
26
+ const authId = ctx.mint(AUTH);
27
+ const baseFields: Row = {
28
+ amount, amount_details: null, authorization_method: authMethod,
29
+ balance_transactions: [], transactions: [],
30
+ currency, fleet: null, fuel: null, livemode: false,
31
+ merchant_amount: merchantAmount, merchant_currency: merchantCurrency, merchant_data: merchant,
32
+ metadata: params.metadata && typeof params.metadata === 'object' ? params.metadata : {},
33
+ network_data: null,
34
+ verification_data: {
35
+ address_line1_check: 'not_provided', address_postal_code_check: 'not_provided',
36
+ authentication_exemption: null, cvc_check: 'match', expiry_check: 'match',
37
+ postal_code: null, three_d_secure: null,
38
+ },
39
+ wallet: null,
40
+ };
41
+ const subject = { id: authId, card, cardholder: cardholderId };
42
+ const entry = (o: { amount: number; approved: boolean; reason: string; reasonMessage: string | null }) => historyEntry({ ...o, currency, createdSec, merchantAmount, merchantCurrency });
43
+ const decline = async (reason: string, reasonMessage: string | null) =>
44
+ ctx.reply(withCard(ctx, await created(ctx, AUTH, subject, { ...baseFields, status: 'closed', approved: false, pending_request: null, request_history: [entry({ amount, approved: false, reason, reasonMessage })] })));
45
+ if (c.status === 'canceled') return decline('card_canceled', 'The card has been canceled.');
46
+ // source: https://docs.stripe.com/api/issuing/cards/create "Defaults to inactive"
47
+ // source: spec:/components/schemas/issuing_authorization_request/properties/reason/enum "card_inactive"
48
+ if (c.status !== 'active') return decline('card_inactive', 'The card is inactive.');
49
+ // what earlier approved authorizations on the card (or cardholder) already spent, for the limits
50
+ const prior = (key: 'card' | 'cardholder', value: string) =>
51
+ ctx.rows(AUTH).filter((a) => a[key] === value && a.approved === true)
52
+ .map((a) => ({ amount: Number(a.amount) || 0, created: Number(a.created) || 0, category: String((a.merchant_data as Row | undefined)?.category ?? '') }));
53
+ const controlsOpts = { amount, category: String(merchant.category ?? ''), country: merchant.country == null ? null : String(merchant.country), nowSec: createdSec };
54
+ const cardholderRow = cardholderId ? ctx.get(CH, cardholderId) : undefined;
55
+ const violation =
56
+ spendingControlsViolation(c.spending_controls, { ...controlsOpts, priorApproved: prior('card', card) }) ??
57
+ spendingControlsViolation(cardholderRow?.spending_controls, { ...controlsOpts, priorApproved: prior('cardholder', cardholderId) });
58
+ if (violation) return decline('spending_controls', violation);
59
+ // an authorization is paid from the Issuing balance, and declined when it cannot cover it
60
+ // (docs.stripe.com/issuing/funding/balance)
61
+ if ((balanceOf(ctx).issuing.get(currency) ?? 0) < amount) return decline('insufficient_funds', 'Your Issuing balance does not have enough funds for this authorization.');
62
+ const pendingRequest = {
63
+ amount, amount_details: null, currency, is_amount_controllable: isAmountControllable,
64
+ merchant_amount: merchantAmount, merchant_currency: merchantCurrency, network_risk_score: null,
65
+ };
66
+ const endpoint = ctx.rows('webhook_endpoint').find((w) => w.status === 'enabled' && stripeEventMatches(w.enabled_events, 'issuing_authorization.request'));
67
+ // no real-time enrollment: "If you don't have a real-time authorization webhook, we approve the authorization without
68
+ // sending the issuing_authorization.request" (docs.stripe.com/issuing/purchases/authorizations), its outcome
69
+ // card_active, "approved according to your default Issuing settings"
70
+ if (!endpoint) return approvedAtOnce(ctx, subject, baseFields, amount, entry({ amount, approved: true, reason: 'card_active', reasonMessage: null }));
71
+ // the request event's authorization carries pending_request, non-null only during the request
72
+ const requestObject = withCard(ctx, { object: 'issuing.authorization', id: authId, created: createdSec, ...baseFields, card, cardholder: cardholderId, status: 'pending', approved: false, pending_request: pendingRequest, request_history: [] });
73
+ // the request asked of the endpoint and the stored event are one event under one id
74
+ const pinned = ctx.call.request.headers.get('stripe-version');
75
+ const requestEvent = {
76
+ id: ctx.mint('event'), object: 'event', api_version: pinned && isValidApiVersion(pinned) ? pinned : TWIN_API_VERSION, created: createdSec,
77
+ data: { object: requestObject }, livemode: false, pending_webhooks: 1, request: { id: null, idempotency_key: null }, type: 'issuing_authorization.request',
78
+ };
79
+ await ctx.write('event', requestEvent.id, requestEvent, 'event.record');
80
+ const outcome = await askForAuthorization(ctx, String(endpoint.url), requestEvent, String(endpoint.secret ?? ''));
81
+ // a request no one answered (the World refused the delivery, it failed, or it timed out) is not a decision: the
82
+ // authorization stays pending with its request, for the deprecated approve or decline "within the timeout window of
83
+ // the real-time authorization flow" (the served spec), until lapseRealtimeRequests decides it when the window ends
84
+ if (outcome.kind === 'timeout') return ctx.reply(withCard(ctx, await created(ctx, AUTH, subject, { ...baseFields, status: 'pending', approved: false, request_history: [], pending_request: pendingRequest })));
85
+ return answeredByEndpoint(ctx, outcome, { subject, fields: baseFields, amount, controllable: isAmountControllable, entry, decline });
28
86
  }
29
-
30
- export async function PostTestHelpersTestClocks(ctx: HandlerContext): Promise<Response> {
31
- if (ctx.params.frozen_time === undefined) return fail(ctx, 'Missing required param: frozen_time.', 400, 'parameter_missing');
32
- const frozen = Math.trunc(Number(ctx.params.frozen_time) || 0);
33
- // "Simulations are automatically deleted 30 days after you create them" (docs.stripe.com/billing/testing/test-clocks/
34
- // api-advanced-usage): from its creation on the account's clock, not its frozen time
35
- return ctx.reply(await created(ctx, CLOCK, { name: ctx.params.name ?? null }, { frozen_time: frozen, status: 'ready', status_details: {}, livemode: false, deletes_after: Number(nowUnix(ctx.occurredAt)) + 30 * 24 * 3600 }));
36
- }
37
-
38
- export async function PostTestHelpersTestClocksTestClockAdvance(ctx: HandlerContext): Promise<Response> {
39
- const id = at(ctx, 'test_clock');
40
- const clock = ctx.get(CLOCK, id);
41
- if (!clock) return fail(ctx, `No such test clock: '${id}'`, 404, 'resource_missing');
42
- if (ctx.params.frozen_time === undefined) return fail(ctx, 'Missing required param: frozen_time.', 400, 'parameter_missing');
43
- const to = Math.trunc(Number(ctx.params.frozen_time) || 0);
44
- if (to <= Number(clock.frozen_time)) return fail(ctx, 'frozen_time must be after the current clock time.', 400, 'parameter_invalid');
45
- const refused = ctx.legal(CLOCK, 'status', 'PostTestHelpersTestClocksTestClockAdvance', clock.status, 'advancing', id);
87
+ export async function PostTestHelpersIssuingAuthorizationsAuthorizationCapture(ctx: HandlerContext): Promise<Response> {
88
+ const id = at(ctx, 'authorization');
89
+ const auth = ctx.get(AUTH, id);
90
+ if (!auth) return authMissing(ctx, id);
91
+ if (auth.approved !== true || auth.status !== 'pending') return issuingRefusal(ctx, 'not_capturable', auth);
92
+ const closeAuthorization = ctx.params.close_authorization !== false;
93
+ const refused = ctx.legal(AUTH, 'status', 'PostTestHelpersIssuingAuthorizationsAuthorizationCapture', auth.status, closeAuthorization ? 'closed' : 'pending');
46
94
  if (refused) return ctx.refuse(refused);
47
- // "After a successful API request or Dashboard operation, it takes a few seconds to advance to the specified time"
48
- // (docs.stripe.com/billing/testing/test-clocks/api-advanced-usage): the advance answers advancing toward its target,
49
- // and finishClockAdvances completes it ADVANCE_SECONDS of World time later; an advance asked meanwhile is refused
50
- // test_clock_not_ready (the machine's refusal).
51
- return ctx.reply(await ctx.write(CLOCK, id, { status: 'advancing', status_details: { advancing: { target_frozen_time: to } } }, 'test_clock.advancing'));
95
+ const txns = Array.isArray(auth.transactions) ? auth.transactions : [];
96
+ const capturedSoFar = txns.map((t) => ctx.get(TXN, String(t))).reduce((s, t) => s + Math.abs(Number(t?.amount) || 0), 0);
97
+ const remaining = (Number(auth.amount) || 0) - capturedSoFar;
98
+ const captureAmount = ctx.params.capture_amount !== undefined ? Math.trunc(Number(ctx.params.capture_amount) || 0) : remaining;
99
+ if (captureAmount <= 0 || captureAmount > remaining) return fail(ctx, 'Invalid capture_amount: must be a positive integer no greater than the uncaptured authorized amount.', 400, 'parameter_invalid_integer');
100
+ // the capture releases what the approval held and debits the transaction; a capture that closes the authorization
101
+ // releases ALL it still holds, the uncaptured rest included: the amount is held "until the authorization is either
102
+ // captured, voided, or expired without capture" (docs.stripe.com/issuing/purchases/authorizations), and a closed
103
+ // authorization can be captured no further (close_authorization "Defaults to true. Set to false to enable
104
+ // multi-capture flows", the served spec). A capture that keeps it open releases only what it captured.
105
+ const currency = typeof auth.currency === 'string' ? auth.currency : 'usd';
106
+ const released = closeAuthorization ? remaining : captureAmount;
107
+ const release = await created(ctx, 'balance_transaction', {}, {
108
+ amount: released, currency, fee: 0, net: released, type: 'issuing_authorization_release',
109
+ status: 'available', balance_type: 'issuing', reporting_category: 'issuing_authorization_release',
110
+ available_on: nowUnix(ctx.occurredAt), fee_details: [], source: id,
111
+ });
112
+ const captured = await capture(ctx, id, { ...auth, card: cardId(auth) }, captureAmount);
113
+ return ctx.reply(withCard(ctx, await ctx.write(AUTH, id, {
114
+ status: closeAuthorization ? 'closed' : 'pending',
115
+ transactions: [...txns, captured.txnId],
116
+ balance_transactions: [...(Array.isArray(auth.balance_transactions) ? auth.balance_transactions : []), release.id, captured.btId],
117
+ }, 'issuing_authorization.updated'),
118
+ ));
52
119
  }
53
-
@@ -4,8 +4,7 @@ import type { HandlerContext } from '@volter/world-core';
4
4
  import type { Row } from '../engine/common.ts';
5
5
  import { nowUnix, validateMoney } from '../engine/stripe.ts';
6
6
  import { emptyList } from '../engine/transfers.ts';
7
- import { at, created, fail, giveBack, list, newest, refusePayout, send, settledSourceRefused, settleTransfer, settleTransferReversal, transferMissing, transfersInactive } from './shared.ts';
8
-
7
+ import { created, fail, refusePayout, send, settledSourceRefused, settleTransfer, transfersInactive } from './shared.ts';
9
8
  export async function PostTransfers(ctx: HandlerContext): Promise<Response> {
10
9
  const bad = validateMoney(ctx.params);
11
10
  if (bad) return send(ctx, bad);
@@ -51,4 +50,3 @@ export async function PostTransfers(ctx: HandlerContext): Promise<Response> {
51
50
  }),
52
51
  );
53
52
  }
54
-
@@ -0,0 +1,27 @@
1
+ // Stripe's webhook_endpoints operations (docs/contributing/architecture.md, "What an author writes"): each handler over the
2
+ // handler contract's context, computing with the engine (../engine/).
3
+ import type { HandlerContext } from '@volter/world-core';
4
+ import { asBool } from '../engine/stripe.ts';
5
+ import { WE } from '../engine/webhook-endpoints.ts';
6
+ import { created, fail } from './shared.ts';
7
+ // an endpoint is enabled at once and signs each event with a secret of its own, answered by this create alone (the
8
+ // around answers no other read of it)
9
+ export async function PostWebhookEndpoints(ctx: HandlerContext): Promise<Response> {
10
+ const params = ctx.params;
11
+ // source: spec:PostWebhookEndpoints "The URL of the webhook endpoint."
12
+ if (typeof params.url !== 'string' || params.url === '') return fail(ctx, 'Missing required param: url.', 400, 'parameter_missing');
13
+ // source: spec:PostWebhookEndpoints "The list of events to enable for this endpoint."
14
+ if (params.enabled_events === undefined || params.enabled_events === '' || (Array.isArray(params.enabled_events) && !params.enabled_events.length)) return fail(ctx, 'Missing required param: enabled_events.', 400, 'parameter_missing');
15
+ const id = ctx.mint(WE);
16
+ // source: spec:/components/schemas/webhook_endpoint/properties/secret/description "The endpoint's secret, used to generate"
17
+ const secret = `whsec_${(await ctx.secret(`webhook-endpoint-secret:${id}`)).replace(/[^A-Za-z0-9]/g, '').slice(0, 32)}`;
18
+ return ctx.reply(await created(ctx, WE, { ...params, id }, {
19
+ url: params.url, enabled_events: Array.isArray(params.enabled_events) ? params.enabled_events.map(String) : [String(params.enabled_events)],
20
+ // source: spec:/components/schemas/webhook_endpoint/properties/status/description "The status of the webhook."
21
+ status: 'enabled', livemode: false, metadata: {}, application: null,
22
+ api_version: typeof params.api_version === 'string' ? params.api_version : null,
23
+ description: typeof params.description === 'string' ? params.description : null,
24
+ // source: spec:PostWebhookEndpoints "Whether this endpoint should receive events from connected accounts"
25
+ connect: asBool(params.connect), secret,
26
+ }));
27
+ }
package/src/server.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  // FETCH-FIRST (runtime contract R12b): a fixed file, the kernel's serve seam around the pack's fetch (./fetch.ts).
2
2
  import { serveHttp, type PackFetchOptions } from '@volter/world-core';
3
3
  import { createStripeFetch } from './fetch.ts';
4
-
5
4
  export async function createStripeTwinServer(options: PackFetchOptions & { port?: number } = {}): Promise<{ port: number; stop: () => void }> {
6
5
  const server = await serveHttp({ port: options.port ?? 0, idleTimeout: 60, fetch: createStripeFetch(options) });
7
6
  return { port: server.port ?? options.port ?? 0, stop: () => server.stop(true) };
@@ -1,94 +0,0 @@
1
- {
2
- "//": "GENERATED by scripts/pack-facts.ts — current pack descriptors and explicit world-runtime server backings; no prior-artifact entries are retained.",
3
- "packs": {
4
- "stripe": {
5
- "adoption": {
6
- "pypi": [
7
- "stripe"
8
- ],
9
- "sdks": [
10
- "stripe"
11
- ],
12
- "envStems": [
13
- "STRIPE",
14
- "NEXTPRIVATESTRIPE"
15
- ]
16
- },
17
- "hosts": [
18
- {
19
- "host": "api.stripe.com"
20
- },
21
- {
22
- "host": "checkout.stripe.com",
23
- "pathPattern": "^/c/pay/"
24
- },
25
- {
26
- "host": "billing.stripe.com",
27
- "pathPattern": "^/p/session/"
28
- },
29
- {
30
- "host": "connect.stripe.com",
31
- "pathPattern": "^/setup/|^/oauth/(authorize|token|deauthorize)/?$"
32
- },
33
- {
34
- "host": "dashboard.stripe.com",
35
- "pathPattern": "^/settings/public/?$|^/(test/)?settings/connect(/onboarding-options/oauth)?/?$"
36
- },
37
- {
38
- "host": "verify.stripe.com",
39
- "pathPattern": "^/start/"
40
- },
41
- {
42
- "host": "js.stripe.com",
43
- "pathPattern": "^/v3/financial-connections/|^/v3/?$|^/(v3|[a-z]+)/stripe\\.js$"
44
- }
45
- ],
46
- "archetype": "crud",
47
- "protocol": {
48
- "declared": "3",
49
- "major": 3,
50
- "standing": "current"
51
- },
52
- "transport": "rest",
53
- "resources": [
54
- "charge",
55
- "customer",
56
- "payment_intent",
57
- "setup_intent",
58
- "payment_method",
59
- "subscription",
60
- "price",
61
- "product",
62
- "invoice",
63
- "invoiceitem",
64
- "refund",
65
- "event"
66
- ],
67
- "credentialDoor": {
68
- "path": "/_twin/app-credentials",
69
- "body": {},
70
- "fill": {
71
- "STRIPE_SECRET_KEY": "secret_key",
72
- "STRIPE_API_KEY": "secret_key",
73
- "STRIPE_PRIVATE_KEY": "secret_key",
74
- "BILLING_STRIPE_API_KEY": "secret_key",
75
- "STRIPE_PUBLISHABLE_KEY": "publishable_key",
76
- "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY": "publishable_key",
77
- "NEXT_PUBLIC_STRIPE_PUBLIC_KEY": "publishable_key",
78
- "NEXT_PUBLIC_STRIPE_KEY": "publishable_key",
79
- "STRIPE_WEBHOOK_SECRET": "webhook_secret",
80
- "STRIPE_WEBHOOK_SIGNING_SECRET": "webhook_secret",
81
- "STRIPE_ENDPOINT_SECRET": "webhook_secret",
82
- "STRIPE_WEBHOOK_SECRET_KEY": "webhook_secret",
83
- "NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET": "webhook_secret",
84
- "BILLING_STRIPE_WEBHOOK_SECRET": "webhook_secret",
85
- "STRIPE_CONNECT_WEBHOOK_SECRET": "connect_webhook_secret",
86
- "STRIPE_WEBHOOK_SECRET_CONNECT": "connect_webhook_secret",
87
- "STRIPE_CONNECT_WEBHOOK_SIGNING_SECRET": "connect_webhook_secret"
88
- }
89
- },
90
- "serveExport": "createStripeTwinServer"
91
- }
92
- },
93
- "undeclared": []
94
- }
@@ -1,5 +0,0 @@
1
- import type { Row } from './common.js';
2
- export declare const METER = "billing.meter";
3
- export declare const objectOf: (v: unknown) => Row;
4
- export declare const GRANT = "billing.credit_grant";
5
- export declare const ALERT = "billing.alert";
@@ -1,6 +0,0 @@
1
- export const METER = 'billing.meter';
2
- export const objectOf = (v) => (v && typeof v === 'object' ? v : {});
3
- // ── credit grants: a customer's prepaid credit, paid or promotional, in a monetary amount ──
4
- export const GRANT = 'billing.credit_grant';
5
- // ── alerts: a usage threshold on a meter ──
6
- export const ALERT = 'billing.alert';
@@ -1,6 +0,0 @@
1
- import type { Row } from './common.js';
2
- /** The create's parameters as the charge keeps them: without `card`, the request's card details ("A token, like the ones
3
- * returned by Stripe.js", or a hash of the number, expiry and CVC). The Charge object has no `card` property (served
4
- * spec, charge); the card it was made with is its payment_method_details. Kept, it answered the card's full number and
5
- * CVC back on every read of the charge. */
6
- export declare function withoutCard(params: Row): Row;
@@ -1,13 +0,0 @@
1
- // A charge is written as the event Stripe sends for it (docs.stripe.com/api/events/types; Stripe has no
2
- // `charge.created`): charge.succeeded, "Occurs whenever a charge is successful", for one made or authorized
3
- // (capture=false: its status is succeeded, captured false), and charge.failed, "Occurs whenever a failed charge attempt
4
- // occurs", for a declined attempt. The twin makes no pending charge (charge.pending, "Occurs whenever a pending charge is
5
- // created"): its charges settle when made.
6
- /** The create's parameters as the charge keeps them: without `card`, the request's card details ("A token, like the ones
7
- * returned by Stripe.js", or a hash of the number, expiry and CVC). The Charge object has no `card` property (served
8
- * spec, charge); the card it was made with is its payment_method_details. Kept, it answered the card's full number and
9
- * CVC back on every read of the charge. */
10
- export function withoutCard(params) {
11
- const { card: _card, ...rest } = params;
12
- return rest;
13
- }
@@ -1,16 +0,0 @@
1
- /** The platform's test client_id: the World's one platform (acct_twin_self) is the application it names. */
2
- export declare const PLATFORM_CLIENT_ID = "ca_twin_self";
3
- export declare const SETTINGS_URL = "https://dashboard.stripe.com/settings/connect/onboarding-options/oauth";
4
- /** The page's addresses: the docs' own, its test-mode link, and Connect's settings root. */
5
- export declare const CONNECT_SETTINGS_PATH: RegExp;
6
- export declare const SETTINGS = "_connect_oauth_settings";
7
- export type ConnectOAuthSettings = {
8
- oauth_enabled: boolean;
9
- redirect_uris: string[];
10
- };
11
- /** The redirect URIs a form's field names, or the first one refused and why. */
12
- export declare function redirectUris(field: string): {
13
- uris: string[];
14
- } | {
15
- refused: string;
16
- };
@@ -1,26 +0,0 @@
1
- // The platform's Connect OAuth settings (its client_id, whether OAuth is on, its redirect URIs), which the Dashboard's
2
- // settings page saves (../screens/dashboard.tsx) and connect.stripe.com/oauth/authorize reads (../screens/connect-oauth.tsx).
3
- /** The platform's test client_id: the World's one platform (acct_twin_self) is the application it names. */
4
- export const PLATFORM_CLIENT_ID = 'ca_twin_self';
5
- export const SETTINGS_URL = 'https://dashboard.stripe.com/settings/connect/onboarding-options/oauth';
6
- /** The page's addresses: the docs' own, its test-mode link, and Connect's settings root. */
7
- export const CONNECT_SETTINGS_PATH = /^\/(test\/)?settings\/connect(\/onboarding-options\/oauth)?\/?$/;
8
- export const SETTINGS = '_connect_oauth_settings';
9
- /** The redirect URIs a form's field names, or the first one refused and why. */
10
- export function redirectUris(field) {
11
- const uris = [...new Set(field.split(/[\n,]/).map((s) => s.trim()).filter(Boolean))];
12
- for (const uri of uris) {
13
- let url;
14
- try {
15
- url = new URL(uri);
16
- }
17
- catch {
18
- return { refused: `${uri} is not a valid URL.` };
19
- }
20
- if (url.protocol !== 'https:' && url.protocol !== 'http:')
21
- return { refused: `${uri} must use http or https.` };
22
- if (uri.includes('#'))
23
- return { refused: `${uri} must not contain a fragment.` };
24
- }
25
- return { uris };
26
- }
File without changes
@@ -1,5 +0,0 @@
1
- "use strict";
2
- // Coupon and PromotionCode semantics. A coupon is exactly one of percent_off or amount_off (with
3
- // its currency) over a duration; a promotion code is a customer-facing code for an existing
4
- // coupon, active until switched off (the machine in ../manifest.ts). Coupon list, retrieve, update
5
- // and delete and promotion-code retrieve and update (`active`, the machine) are the derived core's.
@@ -1 +0,0 @@
1
- export declare const CN = "credit_note";