@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,7 +1,6 @@
1
1
  import type { HandlerContext } from '@volter/world-core';
2
2
  export declare function DeleteSubscriptionsSubscriptionExposedId(ctx: HandlerContext): Promise<Response>;
3
3
  export declare function GetSubscriptions(ctx: HandlerContext): Promise<Response>;
4
- export declare function GetSubscriptionsSearch(ctx: HandlerContext): Promise<Response>;
5
4
  export declare function GetSubscriptionsSubscriptionExposedId(ctx: HandlerContext): Promise<Response>;
6
- export declare function PostSubscriptions(ctx: HandlerContext): Promise<Response>;
7
5
  export declare function PostSubscriptionsSubscriptionExposedId(ctx: HandlerContext): Promise<Response>;
6
+ export declare function DeleteSubscriptionsSubscriptionExposedIdDiscount(ctx: HandlerContext): Promise<Response>;
@@ -1,12 +1,14 @@
1
- import { itemQuantity, keptAfter } from "../engine/renewals.js";
2
- import { addBillingInterval, applyCouponDiscount, asBool, buildDiscount, buildSubscriptionItemsList, declineFor, mintClientSecret, nowUnix, priceAmount, resolveSubscriptionBillingInterval, resolveTrial, subscriptionCouponParam, subscriptionHasPrice, subscriptionItemEntries } from "../engine/stripe.js";
1
+ import { keptAfter } from "../engine/renewals.js";
2
+ import { asBool, nowUnix, subscriptionCouponParam, subscriptionHasPrice, subscriptionItemEntries } from "../engine/stripe.js";
3
3
  import { pauseCollection, SUB } from "../engine/subscriptions.js";
4
- 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.js";
4
+ import { resourceView, advanceBilling, applyItemChanges, applyTrialEnd, at, clockNow, collectSubscriptionInvoice, expanded, itemChangesRefused, list, newest, redeemCoupon, replacedDiscounts, subMissing, trialEndBlocked, trialEndRefused, where } from "./shared.js";
5
+ import { deletedDiscount } from "../engine/common.js";
6
+ import { fail } from "./shared.js";
5
7
  // canceling keeps the subscription, now canceled, stamped with when it was canceled and ended
6
8
  // (https://docs.stripe.com/api/subscriptions/object#subscription_object-canceled_at, #subscription_object-ended_at)
7
9
  export async function DeleteSubscriptionsSubscriptionExposedId(ctx) {
8
10
  const id = at(ctx, 'subscription_exposed_id');
9
- const sub = ctx.get(SUB, id);
11
+ const sub = resourceView(ctx, SUB, id);
10
12
  if (!sub)
11
13
  return subMissing(ctx, id);
12
14
  const refused = ctx.legal(SUB, 'status', 'DeleteSubscriptionsSubscriptionExposedId', sub.status, undefined, id);
@@ -18,7 +20,7 @@ export async function DeleteSubscriptionsSubscriptionExposedId(ctx) {
18
20
  // `"reason": "cancellation_requested"` (docs.stripe.com/api/subscriptions/cancel)
19
21
  const given = ctx.params.cancellation_details && typeof ctx.params.cancellation_details === 'object' ? ctx.params.cancellation_details : {};
20
22
  const cancellation_details = { comment: given.comment ?? null, feedback: given.feedback ?? null, reason: 'cancellation_requested' };
21
- return ctx.reply(await ctx.write(SUB, id, { status: 'canceled', canceled_at: now, ended_at: now, cancellation_details }, 'subscription.cancel'));
23
+ return ctx.reply(ctx.expand(SUB, await ctx.write(SUB, id, { status: 'canceled', canceled_at: now, ended_at: now, cancellation_details }, 'subscription.cancel')));
22
24
  }
23
25
  export async function GetSubscriptions(ctx) {
24
26
  await advanceBilling(ctx);
@@ -28,154 +30,17 @@ export async function GetSubscriptions(ctx) {
28
30
  price: (s, v) => subscriptionHasPrice(s, String(v)),
29
31
  }));
30
32
  }
31
- export async function GetSubscriptionsSearch(ctx) {
32
- await advanceBilling(ctx);
33
- return search(ctx, SUB);
34
- }
35
33
  export async function GetSubscriptionsSubscriptionExposedId(ctx) {
36
34
  const id = at(ctx, 'subscription_exposed_id');
37
35
  await advanceBilling(ctx, (sub) => sub.id === id);
38
- const sub = ctx.get(SUB, id);
36
+ const sub = resourceView(ctx, SUB, id);
39
37
  return sub ? ctx.reply(expanded(ctx, SUB, sub)) : subMissing(ctx, id);
40
38
  }
41
- export async function PostSubscriptions(ctx) {
42
- const params = ctx.params;
43
- const customer = typeof params.customer === 'string' ? params.customer : '';
44
- if (!customer)
45
- return fail(ctx, 'Missing required param: customer.', 400, 'parameter_missing');
46
- if (!ctx.get('customer', customer))
47
- return fail(ctx, `No such customer: '${customer}'`, 404, 'resource_missing');
48
- const now = clockNow(ctx, customer);
49
- const badTrial = trialEndRefused(ctx, params.trial_end, now);
50
- if (badTrial)
51
- return badTrial;
52
- // a coupon (or discounts[0][coupon]) must exist; it becomes the subscription's discount
53
- const couponId = subscriptionCouponParam(params);
54
- let discounts = [];
55
- if (couponId) {
56
- const coupon = ctx.get('coupon', couponId);
57
- if (!coupon)
58
- return fail(ctx, `No such coupon: '${couponId}'`, 400, 'resource_missing');
59
- discounts = [buildDiscount(coupon, customer, now)];
60
- }
61
- // a trial is the current period; otherwise one billing interval of the first item's price. The
62
- // period sits top-level: this twin's default API version predates basil moving it onto items.
63
- const trial = resolveTrial(params, now);
64
- const itemEntries = subscriptionItemEntries(params.items);
65
- const { interval, interval_count } = resolveSubscriptionBillingInterval(itemEntries, finder(ctx));
66
- const periodStart = trial ? trial.start : now;
67
- const periodEnd = trial ? trial.end : addBillingInterval(now, interval, interval_count);
68
- const subId = typeof params.id === 'string' && params.id ? params.id : ctx.mint(SUB);
69
- const itemsList = buildSubscriptionItemsList(itemEntries, subId, now, finder(ctx));
70
- const itemsData = itemsList.data;
71
- const currency = typeof params.currency === 'string' && params.currency ? params.currency : 'usd';
72
- // Stripe opens a subscription's first invoice at once (a trialing one has nothing due yet) and, by
73
- // payment_behavior (docs.stripe.com/api/subscriptions/create#create_subscription-payment_behavior):
74
- // allow_incomplete (the default) charges the default payment method now, and without one, or on a decline,
75
- // leaves the subscription incomplete with the invoice open; error_if_incomplete refuses the create instead;
76
- // default_incomplete never charges, leaving a PaymentIntent the frontend confirms.
77
- const behavior = typeof params.payment_behavior === 'string' ? params.payment_behavior : 'allow_incomplete';
78
- const needsFirstInvoice = !trial;
79
- const holder = ctx.get('customer', customer) ?? {};
80
- const payWith = behavior === 'default_incomplete' ? undefined
81
- : (typeof params.default_payment_method === 'string' ? params.default_payment_method : undefined) ?? holder.invoice_settings?.default_payment_method ?? undefined;
82
- const declined = payWith ? declineFor(finder(ctx), { payment_method: payWith }) : undefined;
83
- let subtotal = 0;
84
- for (const it of itemsData) {
85
- const price = it.price && typeof it.price === 'object' ? it.price : undefined;
86
- subtotal += priceAmount(price, itemQuantity(it));
87
- }
88
- const coupon = discounts[0]?.coupon;
89
- const discountAmount = applyCouponDiscount(subtotal, coupon);
90
- const invoiceTotal = Math.max(0, subtotal - discountAmount);
91
- // a $0 first invoice is paid at once and needs no PaymentIntent
92
- const isFree = invoiceTotal <= 0;
93
- const pays = needsFirstInvoice && !isFree && !!payWith && !declined;
94
- if (needsFirstInvoice && !isFree && !pays && behavior === 'error_if_incomplete')
95
- return firstPaymentRefused(ctx, declined);
96
- // minted first so the subscription names its latest_invoice in its own create (a trial's $0 invoice too)
97
- const invoiceId = needsFirstInvoice ? ctx.mint('invoice') : null;
98
- const trialInvoiceId = needsFirstInvoice ? null : ctx.mint('invoice');
99
- const piId = needsFirstInvoice && !isFree ? ctx.mint('payment_intent') : null;
100
- const { coupon: _c, discounts: _ds, trial_period_days: _tpd, trial_end: _te, items: _items, id: _id, ...rest } = params;
101
- const sub = await created(ctx, SUB, { ...rest, id: subId }, {
102
- status: trial ? 'trialing' : needsFirstInvoice && !isFree && !pays ? 'incomplete' : 'active',
103
- livemode: false, currency, collection_method: 'charge_automatically',
104
- // a trial's end is the billing anchor: "The `billing_cycle_anchor` will be updated to the `trial_end` value"
105
- // (docs.stripe.com/api/subscriptions/create#create_subscription-trial_end)
106
- cancel_at_period_end: false, start_date: now, billing_cycle_anchor: trial ? trial.end : now, metadata: {},
107
- discounts: discounts.map((d) => ({ ...d, subscription: subId })), billing_schedules: [],
108
- ...(trial ? { trial_start: trial.start, trial_end: trial.end } : { trial_start: null, trial_end: null }),
109
- current_period_start: periodStart, current_period_end: periodEnd,
110
- items: itemsList,
111
- latest_invoice: invoiceId ?? trialInvoiceId,
112
- automatic_tax: { enabled: false, liability: null },
113
- billing_mode: { type: 'classic' },
114
- invoice_settings: { issuer: { type: 'self' } },
115
- });
116
- await storeItems(ctx, subId, itemsData);
117
- if (discounts.length)
118
- await redeemCoupon(ctx, couponId);
119
- // a customer billed for the first time takes the subscription's currency
120
- if (!holder.currency)
121
- await ctx.write('customer', customer, { currency }, 'customer.update');
122
- if (!invoiceId) {
123
- // a trial's subscription still opens an invoice at once: "An immediate invoice is still created, but the amount is 0"
124
- // (docs.stripe.com/billing/subscriptions/trials/free-trials); finalizing it spends a once coupon (renewals.ts)
125
- const trialInvoice = await draftSubscriptionInvoice(ctx, ctx.get(SUB, subId) ?? sub, { start: periodStart, end: periodEnd, reason: 'subscription_create', trial: true, ...(trialInvoiceId ? { id: trialInvoiceId } : {}) });
126
- await collectSubscriptionInvoice(ctx, trialInvoice, now, 'api');
127
- return ctx.reply(expanded(ctx, SUB, ctx.get(SUB, subId) ?? sub));
128
- }
129
- // the default payment method pays the first invoice now: its charge (with its ledger entry) comes first
130
- const chargeId = pays && piId ? await mintCharge(ctx, piId, { currency, customer, payment_method: payWith, invoice: invoiceId }, invoiceTotal) : null;
131
- const invoiceLines = itemsData.map((it, i) => {
132
- const price = it.price && typeof it.price === 'object' ? it.price : undefined;
133
- const quantity = itemQuantity(it);
134
- return {
135
- id: `il_twin_${invoiceId}_${i + 1}`, object: 'line_item', type: 'subscription',
136
- amount: priceAmount(price, quantity), currency, quantity, proration: false,
137
- price: it.price ?? null, subscription: subId, invoice_item: null, subscription_item: it.id ?? null,
138
- period: { start: periodStart, end: periodEnd }, description: null,
139
- };
140
- });
141
- await created(ctx, 'invoice', {
142
- id: invoiceId,
143
- ...finalizedFields(ctx, { id: invoiceId, customer }, now),
144
- status: isFree || pays ? 'paid' : 'open', livemode: false, currency, collection_method: 'charge_automatically',
145
- auto_advance: !(isFree || pays), attempt_count: payWith && !isFree ? 1 : 0, attempted: !isFree && behavior !== 'default_incomplete',
146
- customer, subscription: subId, billing_reason: 'subscription_create', charge: chargeId,
147
- amount_due: invoiceTotal, amount_paid: isFree || pays ? invoiceTotal : 0, amount_remaining: isFree || pays ? 0 : invoiceTotal,
148
- amount_overpaid: 0, amount_paid_off_stripe: 0, amount_shipping: 0,
149
- subtotal, total: invoiceTotal, starting_balance: 0,
150
- post_payment_credit_notes_amount: 0, pre_payment_credit_notes_amount: 0,
151
- period_start: periodStart, period_end: periodEnd,
152
- default_tax_rates: [], discounts: discounts[0] ? [discounts[0]] : [],
153
- lines: { object: 'list', data: invoiceLines, has_more: false, total_count: invoiceLines.length, url: `/v1/invoices/${invoiceId}/lines` },
154
- automatic_tax: { enabled: false, liability: null, status: null },
155
- status_transitions: { finalized_at: now, marked_uncollectible_at: null, paid_at: isFree || pays ? now : null, voided_at: null },
156
- issuer: { type: 'self' },
157
- payment_settings: { default_mandate: null, payment_method_options: null, payment_method_types: null },
158
- payment_intent: piId,
159
- }, {});
160
- if (piId) {
161
- // paid by the default payment method, or waiting on one (declined, absent, or collected client-side)
162
- await created(ctx, 'payment_intent', { id: piId, amount: invoiceTotal, currency, customer, invoice: invoiceId, ...(payWith ? { payment_method: pays ? payWith : null } : {}) }, {
163
- status: pays ? 'succeeded' : 'requires_payment_method', client_secret: mintClientSecret(piId, await ctx.secret(`client-secret:${piId}`)), livemode: false,
164
- capture_method: 'automatic', amount_capturable: 0, amount_received: pays ? invoiceTotal : 0, next_action: null,
165
- latest_charge: chargeId,
166
- last_payment_error: declined ? { type: 'card_error', code: declined.code, decline_code: declined.decline_code ?? null, message: declined.message, payment_method: { id: payWith } } : null,
167
- automatic_payment_methods: null, payment_method_types: ['card', 'link'], payment_method_options: {},
168
- });
169
- }
170
- // the first invoice is finalized: the `once` coupon it took is spent (renewals.ts spendOnceCoupon)
171
- await spendOnceCoupon(ctx, ctx.get('invoice', invoiceId) ?? {});
172
- return ctx.reply(expanded(ctx, SUB, ctx.get(SUB, subId) ?? sub));
173
- }
174
39
  export async function PostSubscriptionsSubscriptionExposedId(ctx) {
175
40
  const id = at(ctx, 'subscription_exposed_id');
176
41
  // a period that has already ended renews (or a trial that has, ends) before the update applies (renewals.ts)
177
42
  await advanceBilling(ctx, (s) => s.id === id);
178
- const sub = ctx.get(SUB, id);
43
+ const sub = resourceView(ctx, SUB, id);
179
44
  if (!sub)
180
45
  return subMissing(ctx, id);
181
46
  const params = ctx.params;
@@ -230,5 +95,18 @@ export async function PostSubscriptionsSubscriptionExposedId(ctx) {
230
95
  if (fields.discounts !== undefined)
231
96
  fields.discounts = keptAfter(fields.discounts, ctx.get('invoice', trial.invoice) ?? {});
232
97
  }
233
- return ctx.reply(await ctx.write(SUB, id, fields, op));
98
+ return ctx.reply(ctx.expand(SUB, await ctx.write(SUB, id, fields, op)));
99
+ }
100
+ export async function DeleteSubscriptionsSubscriptionExposedIdDiscount(ctx) {
101
+ const id = at(ctx, 'subscription_exposed_id');
102
+ const sub = resourceView(ctx, SUB, id);
103
+ if (!sub)
104
+ return subMissing(ctx, id);
105
+ // source: spec:DeleteSubscriptionsSubscriptionExposedIdDiscount "Removes the currently applied discount on a subscription."
106
+ const current = (sub.discounts ?? [])[0] ?? sub.discount;
107
+ // Where the documentation stops and the twin decides: a subscription with no discount answers its absence as a missing resource.
108
+ if (!current)
109
+ return fail(ctx, `No active discount for subscription: '${id}'`, 404, 'resource_missing');
110
+ await ctx.write(SUB, id, { discounts: [] }, 'subscription.update');
111
+ return ctx.reply(deletedDiscount(typeof current === 'string' ? { id: current } : { ...current, subscription: id }, nowUnix(ctx.occurredAt)));
234
112
  }
@@ -1,6 +1,3 @@
1
1
  import type { HandlerContext } from '@volter/world-core';
2
- /** A test clock deleted with what lives on it: "Deleting the simulation also deletes the test customers associated
3
- * with the clock and cancels their subscriptions" (docs.stripe.com/billing/testing/test-clocks/api-advanced-usage). */
4
- export declare function DeleteTestHelpersTestClocksTestClock(ctx: HandlerContext): Promise<Response>;
5
- export declare function PostTestHelpersTestClocks(ctx: HandlerContext): Promise<Response>;
6
- export declare function PostTestHelpersTestClocksTestClockAdvance(ctx: HandlerContext): Promise<Response>;
2
+ export declare function PostTestHelpersIssuingAuthorizations(ctx: HandlerContext): Promise<Response>;
3
+ export declare function PostTestHelpersIssuingAuthorizationsAuthorizationCapture(ctx: HandlerContext): Promise<Response>;
@@ -1,47 +1,125 @@
1
- import { nowUnix } from "../engine/stripe.js";
2
- import { CLOCK } from "../engine/test-clocks.js";
3
- import { at, created, fail } from "./shared.js";
4
- /** A test clock deleted with what lives on it: "Deleting the simulation also deletes the test customers associated
5
- * with the clock and cancels their subscriptions" (docs.stripe.com/billing/testing/test-clocks/api-advanced-usage). */
6
- export async function DeleteTestHelpersTestClocksTestClock(ctx) {
7
- const id = at(ctx, 'test_clock');
8
- if (!ctx.get(CLOCK, id))
9
- return fail(ctx, `No such test clock: '${id}'`, 404, 'resource_missing');
10
- const now = Number(nowUnix(ctx.occurredAt));
11
- for (const c of ctx.rows('customer').filter((c) => c.test_clock === id)) {
12
- for (const sub of ctx.rows('subscription').filter((s) => s.customer === c.id && s.status !== 'canceled')) {
13
- ctx.legal('subscription', 'status', 'DeleteTestHelpersTestClocksTestClock', String(sub.status), 'canceled', String(sub.id));
14
- await ctx.write('subscription', String(sub.id), { status: 'canceled', canceled_at: now, ended_at: now }, 'subscription.cancel');
15
- }
16
- await ctx.write('customer', String(c.id), { deleted: true }, 'customer.deleted');
17
- }
18
- await ctx.write(CLOCK, id, { deleted: true }, 'test_clock.deleted');
19
- return ctx.reply({ id, object: 'test_helpers.test_clock', deleted: true });
1
+ import { AUTH, CARD, cardId, CH, historyEntry, holderId, merchantData, TXN } from "../engine/issuing.js";
2
+ import { isValidApiVersion, nowUnix, spendingControlsViolation, TWIN_API_VERSION } from "../engine/stripe.js";
3
+ import { stripeEventMatches } from "../engine/webhooks.js";
4
+ import { answeredByEndpoint, approvedAtOnce, askForAuthorization, at, authMissing, balanceOf, capture, created, fail, issuingRefusal, withCard } from "./shared.js";
5
+ export async function PostTestHelpersIssuingAuthorizations(ctx) {
6
+ const params = ctx.params;
7
+ const card = typeof params.card === 'string' ? params.card : '';
8
+ if (!card)
9
+ return fail(ctx, 'Missing required param: card.', 400, 'parameter_missing');
10
+ const c = ctx.get(CARD, card);
11
+ if (!c)
12
+ return fail(ctx, `No such card: '${card}'`, 400, 'resource_missing');
13
+ if (params.amount === undefined)
14
+ return fail(ctx, 'Missing required param: amount.', 400, 'parameter_missing');
15
+ const amount = Math.trunc(Number(params.amount) || 0);
16
+ const currency = String(params.currency ?? c.currency ?? 'usd');
17
+ const createdSec = Number(nowUnix(ctx.occurredAt));
18
+ const cardholderId = holderId(c);
19
+ const merchant = merchantData(params.merchant_data);
20
+ const isAmountControllable = params.is_amount_controllable === true;
21
+ const authMethod = typeof params.authorization_method === 'string' ? params.authorization_method : 'online';
22
+ if (!['chip', 'contactless', 'keyed_in', 'online', 'swipe'].includes(authMethod))
23
+ 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 = {
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) => historyEntry({ ...o, currency, createdSec, merchantAmount, merchantCurrency });
43
+ const decline = async (reason, reasonMessage) => 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 })] })));
44
+ if (c.status === 'canceled')
45
+ 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')
49
+ return decline('card_inactive', 'The card is inactive.');
50
+ // what earlier approved authorizations on the card (or cardholder) already spent, for the limits
51
+ const prior = (key, value) => 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?.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 = spendingControlsViolation(c.spending_controls, { ...controlsOpts, priorApproved: prior('card', card) }) ??
56
+ spendingControlsViolation(cardholderRow?.spending_controls, { ...controlsOpts, priorApproved: prior('cardholder', cardholderId) });
57
+ if (violation)
58
+ 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)
62
+ return decline('insufficient_funds', 'Your Issuing balance does not have enough funds for this authorization.');
63
+ const pendingRequest = {
64
+ amount, amount_details: null, currency, is_amount_controllable: isAmountControllable,
65
+ merchant_amount: merchantAmount, merchant_currency: merchantCurrency, network_risk_score: null,
66
+ };
67
+ const endpoint = ctx.rows('webhook_endpoint').find((w) => w.status === 'enabled' && stripeEventMatches(w.enabled_events, 'issuing_authorization.request'));
68
+ // no real-time enrollment: "If you don't have a real-time authorization webhook, we approve the authorization without
69
+ // sending the issuing_authorization.request" (docs.stripe.com/issuing/purchases/authorizations), its outcome
70
+ // card_active, "approved according to your default Issuing settings"
71
+ if (!endpoint)
72
+ return approvedAtOnce(ctx, subject, baseFields, amount, entry({ amount, approved: true, reason: 'card_active', reasonMessage: null }));
73
+ // the request event's authorization carries pending_request, non-null only during the request
74
+ const requestObject = withCard(ctx, { object: 'issuing.authorization', id: authId, created: createdSec, ...baseFields, card, cardholder: cardholderId, status: 'pending', approved: false, pending_request: pendingRequest, request_history: [] });
75
+ // the request asked of the endpoint and the stored event are one event under one id
76
+ const pinned = ctx.call.request.headers.get('stripe-version');
77
+ const requestEvent = {
78
+ id: ctx.mint('event'), object: 'event', api_version: pinned && isValidApiVersion(pinned) ? pinned : TWIN_API_VERSION, created: createdSec,
79
+ data: { object: requestObject }, livemode: false, pending_webhooks: 1, request: { id: null, idempotency_key: null }, type: 'issuing_authorization.request',
80
+ };
81
+ await ctx.write('event', requestEvent.id, requestEvent, 'event.record');
82
+ const outcome = await askForAuthorization(ctx, String(endpoint.url), requestEvent, String(endpoint.secret ?? ''));
83
+ // a request no one answered (the World refused the delivery, it failed, or it timed out) is not a decision: the
84
+ // authorization stays pending with its request, for the deprecated approve or decline "within the timeout window of
85
+ // the real-time authorization flow" (the served spec), until lapseRealtimeRequests decides it when the window ends
86
+ if (outcome.kind === 'timeout')
87
+ return ctx.reply(withCard(ctx, await created(ctx, AUTH, subject, { ...baseFields, status: 'pending', approved: false, request_history: [], pending_request: pendingRequest })));
88
+ return answeredByEndpoint(ctx, outcome, { subject, fields: baseFields, amount, controllable: isAmountControllable, entry, decline });
20
89
  }
21
- export async function PostTestHelpersTestClocks(ctx) {
22
- if (ctx.params.frozen_time === undefined)
23
- return fail(ctx, 'Missing required param: frozen_time.', 400, 'parameter_missing');
24
- const frozen = Math.trunc(Number(ctx.params.frozen_time) || 0);
25
- // "Simulations are automatically deleted 30 days after you create them" (docs.stripe.com/billing/testing/test-clocks/
26
- // api-advanced-usage): from its creation on the account's clock, not its frozen time
27
- 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 }));
28
- }
29
- export async function PostTestHelpersTestClocksTestClockAdvance(ctx) {
30
- const id = at(ctx, 'test_clock');
31
- const clock = ctx.get(CLOCK, id);
32
- if (!clock)
33
- return fail(ctx, `No such test clock: '${id}'`, 404, 'resource_missing');
34
- if (ctx.params.frozen_time === undefined)
35
- return fail(ctx, 'Missing required param: frozen_time.', 400, 'parameter_missing');
36
- const to = Math.trunc(Number(ctx.params.frozen_time) || 0);
37
- if (to <= Number(clock.frozen_time))
38
- return fail(ctx, 'frozen_time must be after the current clock time.', 400, 'parameter_invalid');
39
- const refused = ctx.legal(CLOCK, 'status', 'PostTestHelpersTestClocksTestClockAdvance', clock.status, 'advancing', id);
90
+ export async function PostTestHelpersIssuingAuthorizationsAuthorizationCapture(ctx) {
91
+ const id = at(ctx, 'authorization');
92
+ const auth = ctx.get(AUTH, id);
93
+ if (!auth)
94
+ return authMissing(ctx, id);
95
+ if (auth.approved !== true || auth.status !== 'pending')
96
+ return issuingRefusal(ctx, 'not_capturable', auth);
97
+ const closeAuthorization = ctx.params.close_authorization !== false;
98
+ const refused = ctx.legal(AUTH, 'status', 'PostTestHelpersIssuingAuthorizationsAuthorizationCapture', auth.status, closeAuthorization ? 'closed' : 'pending');
40
99
  if (refused)
41
100
  return ctx.refuse(refused);
42
- // "After a successful API request or Dashboard operation, it takes a few seconds to advance to the specified time"
43
- // (docs.stripe.com/billing/testing/test-clocks/api-advanced-usage): the advance answers advancing toward its target,
44
- // and finishClockAdvances completes it ADVANCE_SECONDS of World time later; an advance asked meanwhile is refused
45
- // test_clock_not_ready (the machine's refusal).
46
- return ctx.reply(await ctx.write(CLOCK, id, { status: 'advancing', status_details: { advancing: { target_frozen_time: to } } }, 'test_clock.advancing'));
101
+ const txns = Array.isArray(auth.transactions) ? auth.transactions : [];
102
+ const capturedSoFar = txns.map((t) => ctx.get(TXN, String(t))).reduce((s, t) => s + Math.abs(Number(t?.amount) || 0), 0);
103
+ const remaining = (Number(auth.amount) || 0) - capturedSoFar;
104
+ const captureAmount = ctx.params.capture_amount !== undefined ? Math.trunc(Number(ctx.params.capture_amount) || 0) : remaining;
105
+ if (captureAmount <= 0 || captureAmount > remaining)
106
+ return fail(ctx, 'Invalid capture_amount: must be a positive integer no greater than the uncaptured authorized amount.', 400, 'parameter_invalid_integer');
107
+ // the capture releases what the approval held and debits the transaction; a capture that closes the authorization
108
+ // releases ALL it still holds, the uncaptured rest included: the amount is held "until the authorization is either
109
+ // captured, voided, or expired without capture" (docs.stripe.com/issuing/purchases/authorizations), and a closed
110
+ // authorization can be captured no further (close_authorization "Defaults to true. Set to false to enable
111
+ // multi-capture flows", the served spec). A capture that keeps it open releases only what it captured.
112
+ const currency = typeof auth.currency === 'string' ? auth.currency : 'usd';
113
+ const released = closeAuthorization ? remaining : captureAmount;
114
+ const release = await created(ctx, 'balance_transaction', {}, {
115
+ amount: released, currency, fee: 0, net: released, type: 'issuing_authorization_release',
116
+ status: 'available', balance_type: 'issuing', reporting_category: 'issuing_authorization_release',
117
+ available_on: nowUnix(ctx.occurredAt), fee_details: [], source: id,
118
+ });
119
+ const captured = await capture(ctx, id, { ...auth, card: cardId(auth) }, captureAmount);
120
+ return ctx.reply(withCard(ctx, await ctx.write(AUTH, id, {
121
+ status: closeAuthorization ? 'closed' : 'pending',
122
+ transactions: [...txns, captured.txnId],
123
+ balance_transactions: [...(Array.isArray(auth.balance_transactions) ? auth.balance_transactions : []), release.id, captured.btId],
124
+ }, 'issuing_authorization.updated')));
47
125
  }
@@ -0,0 +1,2 @@
1
+ import type { HandlerContext } from '@volter/world-core';
2
+ export declare function PostWebhookEndpoints(ctx: HandlerContext): Promise<Response>;
@@ -0,0 +1,26 @@
1
+ import { asBool } from "../engine/stripe.js";
2
+ import { WE } from "../engine/webhook-endpoints.js";
3
+ import { created, fail } from "./shared.js";
4
+ // an endpoint is enabled at once and signs each event with a secret of its own, answered by this create alone (the
5
+ // around answers no other read of it)
6
+ export async function PostWebhookEndpoints(ctx) {
7
+ const params = ctx.params;
8
+ // source: spec:PostWebhookEndpoints "The URL of the webhook endpoint."
9
+ if (typeof params.url !== 'string' || params.url === '')
10
+ return fail(ctx, 'Missing required param: url.', 400, 'parameter_missing');
11
+ // source: spec:PostWebhookEndpoints "The list of events to enable for this endpoint."
12
+ if (params.enabled_events === undefined || params.enabled_events === '' || (Array.isArray(params.enabled_events) && !params.enabled_events.length))
13
+ return fail(ctx, 'Missing required param: enabled_events.', 400, 'parameter_missing');
14
+ const id = ctx.mint(WE);
15
+ // source: spec:/components/schemas/webhook_endpoint/properties/secret/description "The endpoint's secret, used to generate"
16
+ const secret = `whsec_${(await ctx.secret(`webhook-endpoint-secret:${id}`)).replace(/[^A-Za-z0-9]/g, '').slice(0, 32)}`;
17
+ return ctx.reply(await created(ctx, WE, { ...params, id }, {
18
+ url: params.url, enabled_events: Array.isArray(params.enabled_events) ? params.enabled_events.map(String) : [String(params.enabled_events)],
19
+ // source: spec:/components/schemas/webhook_endpoint/properties/status/description "The status of the webhook."
20
+ status: 'enabled', livemode: false, metadata: {}, application: null,
21
+ api_version: typeof params.api_version === 'string' ? params.api_version : null,
22
+ description: typeof params.description === 'string' ? params.description : null,
23
+ // source: spec:PostWebhookEndpoints "Whether this endpoint should receive events from connected accounts"
24
+ connect: asBool(params.connect), secret,
25
+ }));
26
+ }
@@ -28,11 +28,11 @@
28
28
  },
29
29
  {
30
30
  "host": "connect.stripe.com",
31
- "pathPattern": "^/setup/|^/oauth/(authorize|token|deauthorize)/?$"
31
+ "pathPattern": "^/setup/"
32
32
  },
33
33
  {
34
34
  "host": "dashboard.stripe.com",
35
- "pathPattern": "^/settings/public/?$|^/(test/)?settings/connect(/onboarding-options/oauth)?/?$"
35
+ "pathPattern": "^/radar/lists(?:/[^/]+)?/?$|^/test/issuing/balance/?$|^/settings/public/?$"
36
36
  },
37
37
  {
38
38
  "host": "verify.stripe.com",
@@ -40,7 +40,7 @@
40
40
  },
41
41
  {
42
42
  "host": "js.stripe.com",
43
- "pathPattern": "^/v3/financial-connections/|^/v3/?$|^/(v3|[a-z]+)/stripe\\.js$"
43
+ "pathPattern": "^/v3/?$|^/(v3|[a-z]+)/stripe\\.js$"
44
44
  }
45
45
  ],
46
46
  "archetype": "crud",