@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.
- package/README.md +51 -21
- package/dist/generated/pack-facts.json +3 -3
- package/dist/journeys/coverage.json +596 -0
- package/dist/journeys/customer-life.json +6108 -0
- package/dist/journeys/customer-life.outline.md +371 -0
- package/dist/journeys/decisions.json +677 -0
- package/dist/journeys/demand.json +706 -0
- package/dist/journeys/score.json +853 -0
- package/dist/journeys/unreachable.json +102 -0
- package/dist/journeys/vendor-examples.json +8010 -0
- package/dist/spec/SOURCE.md +5 -1
- package/dist/spec/doc-examples.json +445 -0
- package/dist/spec/sources.json +135 -207
- package/dist/src/cli.d.ts +0 -1
- package/dist/src/cli.js +0 -2
- package/dist/src/engine/after-payment.d.ts +0 -1
- package/dist/src/engine/after-payment.js +0 -1
- package/dist/src/engine/common.d.ts +5 -5
- package/dist/src/engine/common.js +13 -11
- package/dist/src/engine/connect.d.ts +0 -21
- package/dist/src/engine/connect.js +1 -82
- package/dist/src/engine/customers.d.ts +0 -8
- package/dist/src/engine/customers.js +0 -22
- package/dist/src/engine/display.d.ts +0 -3
- package/dist/src/engine/display.js +0 -3
- package/dist/src/engine/files.d.ts +0 -7
- package/dist/src/engine/files.js +0 -10
- package/dist/src/engine/invoices.d.ts +6 -9
- package/dist/src/engine/invoices.js +56 -29
- package/dist/src/engine/issuing.d.ts +6 -8
- package/dist/src/engine/issuing.js +19 -10
- package/dist/src/engine/ledger.d.ts +0 -1
- package/dist/src/engine/ledger.js +0 -1
- package/dist/src/engine/oauth.js +1 -2
- package/dist/src/engine/refunds.js +2 -27
- package/dist/src/engine/stripe.d.ts +2 -7
- package/dist/src/engine/stripe.js +34 -83
- package/dist/src/engine/subscriptions.d.ts +0 -1
- package/dist/src/engine/subscriptions.js +0 -1
- package/dist/src/engine/version.d.ts +0 -2
- package/dist/src/engine/version.js +17 -6
- package/dist/src/fetch.js +2 -2
- package/dist/src/generated/surface.gen.json +1 -1
- package/dist/src/generated/ui.gen.json +1 -1
- package/dist/src/index.js +5 -3
- package/dist/src/manifest.js +348 -220
- package/dist/src/screens/billing-portal.js +5 -5
- package/dist/src/screens/{connect-oauth.d.ts → checkout-js.d.ts} +0 -1
- package/dist/src/screens/checkout-js.js +84 -0
- package/dist/src/screens/connect-onboarding.js +3 -3
- package/dist/src/screens/dashboard.d.ts +1 -1
- package/dist/src/screens/dashboard.js +52 -53
- package/dist/src/screens/stripe-js.js +53 -93
- package/dist/src/semantics/account.d.ts +2 -0
- package/dist/src/semantics/account.js +8 -0
- package/dist/src/semantics/accounts.js +3 -1
- package/dist/src/semantics/around.js +3 -6
- package/dist/src/semantics/billing_portal.d.ts +0 -1
- package/dist/src/semantics/billing_portal.js +1 -7
- package/dist/src/semantics/checkout.d.ts +1 -1
- package/dist/src/semantics/checkout.js +18 -15
- package/dist/src/semantics/clock.js +1 -7
- package/dist/src/semantics/customers.d.ts +1 -2
- package/dist/src/semantics/customers.js +32 -38
- package/dist/src/semantics/entitlements.d.ts +0 -2
- package/dist/src/semantics/entitlements.js +4 -50
- package/dist/src/semantics/events.d.ts +2 -3
- package/dist/src/semantics/events.js +6 -11
- package/dist/src/semantics/index.js +16 -38
- package/dist/src/semantics/invoices.d.ts +3 -8
- package/dist/src/semantics/invoices.js +134 -127
- package/dist/src/semantics/issuing.d.ts +6 -0
- package/dist/src/semantics/issuing.js +77 -0
- package/dist/src/semantics/payment_intents.d.ts +0 -1
- package/dist/src/semantics/payment_intents.js +5 -13
- package/dist/src/semantics/payment_methods.d.ts +0 -2
- package/dist/src/semantics/payment_methods.js +2 -26
- package/dist/src/semantics/payouts.d.ts +1 -0
- package/dist/src/semantics/payouts.js +8 -1
- package/dist/src/semantics/products.d.ts +1 -3
- package/dist/src/semantics/products.js +3 -14
- package/dist/src/semantics/refunds.d.ts +1 -2
- package/dist/src/semantics/refunds.js +1 -5
- package/dist/src/semantics/shared.d.ts +64 -336
- package/dist/src/semantics/shared.js +326 -1336
- package/dist/src/semantics/states.d.ts +20 -89
- package/dist/src/semantics/states.js +116 -334
- package/dist/src/semantics/subscriptions.d.ts +1 -2
- package/dist/src/semantics/subscriptions.js +23 -145
- package/dist/src/semantics/test_helpers.d.ts +2 -5
- package/dist/src/semantics/test_helpers.js +121 -43
- package/dist/src/semantics/webhook_endpoints.d.ts +2 -0
- package/dist/src/semantics/webhook_endpoints.js +26 -0
- package/generated/pack-facts.json +3 -3
- package/journeys/coverage.json +596 -0
- package/journeys/customer-life.json +6108 -0
- package/journeys/customer-life.outline.md +371 -0
- package/journeys/decisions.json +677 -0
- package/journeys/demand.json +706 -0
- package/journeys/score.json +853 -0
- package/journeys/unreachable.json +102 -0
- package/journeys/vendor-examples.json +8010 -0
- package/package.json +4 -3
- package/spec/SOURCE.md +5 -1
- package/spec/doc-examples.json +445 -0
- package/spec/sources.json +135 -207
- package/src/cli.ts +0 -3
- package/src/engine/after-payment.ts +0 -2
- package/src/engine/balance.ts +0 -1
- package/src/engine/checkout.ts +0 -1
- package/src/engine/common.ts +19 -17
- package/src/engine/connect.ts +1 -79
- package/src/engine/customers.ts +0 -25
- package/src/engine/display.ts +2 -3
- package/src/engine/files.ts +0 -13
- package/src/engine/invoices.ts +64 -33
- package/src/engine/issuing.ts +16 -11
- package/src/engine/ledger.ts +0 -2
- package/src/engine/oauth.ts +1 -2
- package/src/engine/onboarding.ts +0 -1
- package/src/engine/params.ts +0 -1
- package/src/engine/payment-intents.ts +0 -1
- package/src/engine/payment-methods.ts +0 -1
- package/src/engine/refunds.ts +2 -28
- package/src/engine/renewals.ts +0 -1
- package/src/engine/stripe.ts +35 -83
- package/src/engine/subscriptions.ts +0 -3
- package/src/engine/transfers.ts +0 -1
- package/src/engine/version.ts +15 -7
- package/src/engine/webhooks.ts +0 -2
- package/src/fetch.ts +2 -2
- package/src/generated/surface.gen.json +1 -1
- package/src/generated/ui.gen.json +1 -1
- package/src/index.ts +6 -4
- package/src/manifest.ts +348 -220
- package/src/screens/billing-portal.tsx +5 -7
- package/src/screens/checkout-js.ts +80 -0
- package/src/screens/checkout.tsx +0 -2
- package/src/screens/connect-onboarding.tsx +3 -5
- package/src/screens/dashboard.tsx +53 -73
- package/src/screens/stripe-js.ts +57 -125
- package/src/semantics/account.ts +10 -0
- package/src/semantics/account_links.ts +0 -1
- package/src/semantics/accounts.ts +8 -7
- package/src/semantics/apps.ts +1 -2
- package/src/semantics/around.ts +3 -6
- package/src/semantics/balance.ts +0 -1
- package/src/semantics/balance_transactions.ts +1 -3
- package/src/semantics/billing_portal.ts +1 -7
- package/src/semantics/charges.ts +1 -6
- package/src/semantics/checkout.ts +16 -12
- package/src/semantics/clock.ts +1 -8
- package/src/semantics/coupons.ts +0 -1
- package/src/semantics/customers.ts +32 -35
- package/src/semantics/doors.ts +2 -3
- package/src/semantics/entitlements.ts +1 -45
- package/src/semantics/events.ts +7 -14
- package/src/semantics/index.ts +16 -38
- package/src/semantics/invoice_payments.ts +1 -3
- package/src/semantics/invoices.ts +124 -119
- package/src/semantics/issuing.ts +74 -0
- package/src/semantics/payment_intents.ts +10 -17
- package/src/semantics/payment_methods.ts +2 -25
- package/src/semantics/payouts.ts +9 -3
- package/src/semantics/prices.ts +0 -1
- package/src/semantics/products.ts +4 -14
- package/src/semantics/promotion_codes.ts +0 -1
- package/src/semantics/radar.ts +1 -3
- package/src/semantics/refunds.ts +2 -11
- package/src/semantics/shared.ts +350 -1382
- package/src/semantics/states.ts +109 -330
- package/src/semantics/subscriptions.ts +25 -145
- package/src/semantics/test_helpers.ts +109 -43
- package/src/semantics/transfers.ts +1 -3
- package/src/semantics/webhook_endpoints.ts +27 -0
- package/src/server.ts +0 -1
- package/dist/src/engine/billing.d.ts +0 -5
- package/dist/src/engine/billing.js +0 -6
- package/dist/src/engine/charges.d.ts +0 -6
- package/dist/src/engine/charges.js +0 -13
- package/dist/src/engine/connect-settings.d.ts +0 -16
- package/dist/src/engine/connect-settings.js +0 -26
- package/dist/src/engine/coupons.d.ts +0 -0
- package/dist/src/engine/coupons.js +0 -5
- package/dist/src/engine/credit-notes.d.ts +0 -1
- package/dist/src/engine/credit-notes.js +0 -5
- package/dist/src/engine/disputes.d.ts +0 -0
- package/dist/src/engine/disputes.js +0 -9
- package/dist/src/engine/entitlements.d.ts +0 -1
- package/dist/src/engine/entitlements.js +0 -4
- package/dist/src/engine/ephemeral-keys.d.ts +0 -0
- package/dist/src/engine/ephemeral-keys.js +0 -4
- package/dist/src/engine/front.d.ts +0 -5
- package/dist/src/engine/front.js +0 -25
- package/dist/src/engine/payment-links.d.ts +0 -1
- package/dist/src/engine/payment-links.js +0 -5
- package/dist/src/engine/plans.d.ts +0 -6
- package/dist/src/engine/plans.js +0 -26
- package/dist/src/engine/platform.d.ts +0 -39
- package/dist/src/engine/platform.js +0 -34
- package/dist/src/engine/products.d.ts +0 -0
- package/dist/src/engine/products.js +0 -4
- package/dist/src/engine/setup-intents.d.ts +0 -1
- package/dist/src/engine/setup-intents.js +0 -4
- package/dist/src/engine/subscription-schedules.d.ts +0 -3
- package/dist/src/engine/subscription-schedules.js +0 -1
- package/dist/src/engine/tax.d.ts +0 -7
- package/dist/src/engine/tax.js +0 -28
- package/dist/src/engine/terminal.d.ts +0 -4
- package/dist/src/engine/terminal.js +0 -20
- package/dist/src/engine/test-clocks.d.ts +0 -5
- package/dist/src/engine/test-clocks.js +0 -9
- package/dist/src/engine/tokens.d.ts +0 -7
- package/dist/src/engine/tokens.js +0 -20
- package/dist/src/engine/treasury.d.ts +0 -23
- package/dist/src/engine/treasury.js +0 -45
- package/dist/src/screens/connect-oauth.js +0 -333
- package/dist/src/semantics/disputes.d.ts +0 -2
- package/dist/src/semantics/disputes.js +0 -5
- package/dist/src/semantics/invoiceitems.d.ts +0 -3
- package/dist/src/semantics/invoiceitems.js +0 -59
- package/dist/src/semantics/setup_intents.d.ts +0 -3
- package/dist/src/semantics/setup_intents.js +0 -41
- package/dist/src/semantics/subscription_items.d.ts +0 -5
- package/dist/src/semantics/subscription_items.js +0 -55
- package/dist/src/semantics/subscription_schedules.d.ts +0 -4
- package/dist/src/semantics/subscription_schedules.js +0 -60
- package/src/engine/billing.ts +0 -16
- package/src/engine/charges.ts +0 -19
- package/src/engine/connect-settings.ts +0 -23
- package/src/engine/coupons.ts +0 -5
- package/src/engine/credit-notes.ts +0 -6
- package/src/engine/disputes.ts +0 -9
- package/src/engine/entitlements.ts +0 -5
- package/src/engine/ephemeral-keys.ts +0 -3
- package/src/engine/front.ts +0 -31
- package/src/engine/payment-links.ts +0 -6
- package/src/engine/plans.ts +0 -42
- package/src/engine/platform.ts +0 -47
- package/src/engine/products.ts +0 -3
- package/src/engine/setup-intents.ts +0 -5
- package/src/engine/subscription-schedules.ts +0 -10
- package/src/engine/tax.ts +0 -39
- package/src/engine/terminal.ts +0 -23
- package/src/engine/test-clocks.ts +0 -11
- package/src/engine/tokens.ts +0 -22
- package/src/engine/treasury.ts +0 -47
- package/src/screens/connect-oauth.tsx +0 -376
- package/src/semantics/disputes.ts +0 -12
- package/src/semantics/invoiceitems.ts +0 -59
- package/src/semantics/setup_intents.ts +0 -45
- package/src/semantics/subscription_items.ts +0 -54
- package/src/semantics/subscription_schedules.ts +0 -66
package/src/engine/stripe.ts
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
// and its declines, 3-D Secure and micro-deposits, the list page and the search grammar, a subscription's items, billing
|
|
3
|
-
// intervals and discounts, Issuing's spending controls, a payment method's sub-object, a Connect account's settings and a
|
|
4
|
-
// Checkout Session's line items. None of it reads or writes state: a caller hands it what it looked up (FindStored).
|
|
1
|
+
import { digest } from '@volter/world-core';
|
|
5
2
|
import { ownFields, type TwinResource } from '@volter/world-core';
|
|
6
3
|
import { SERVED_VERSION } from './version.ts';
|
|
7
4
|
|
|
@@ -170,8 +167,6 @@ export function threeDsNextAction(): Record<string, unknown> {
|
|
|
170
167
|
// a `descriptor_code` (the test value `SM11AA`). We model that exact pair so the verify
|
|
171
168
|
// can FAIL with the wrong amounts. The well-known token that triggers this flow:
|
|
172
169
|
const MICRODEPOSIT_TOKENS = new Set(['pm_usBankAccount_requiresVerification', 'pm_us_bank_account']);
|
|
173
|
-
const MICRODEPOSIT_AMOUNTS = [32, 45];
|
|
174
|
-
const MICRODEPOSIT_DESCRIPTOR = 'SM11AA';
|
|
175
170
|
export function requiresMicrodeposits(...paramSets: Array<Record<string, unknown>>): boolean {
|
|
176
171
|
for (const params of paramSets) {
|
|
177
172
|
const ref = resolveCardRef(params);
|
|
@@ -188,31 +183,6 @@ export function microdepositsNextAction(): Record<string, unknown> {
|
|
|
188
183
|
},
|
|
189
184
|
};
|
|
190
185
|
}
|
|
191
|
-
/** The two deposits' amounts as sent: the test values (docs.stripe.com/payments/ach-direct-debit/accept-a-payment). */
|
|
192
|
-
function amountsMatch(params: Record<string, unknown>): boolean {
|
|
193
|
-
const got = (params.amounts as unknown[]).map((a) => Math.trunc(Number(a) || 0));
|
|
194
|
-
return got.length === 2 && got[0] === MICRODEPOSIT_AMOUNTS[0] && got[1] === MICRODEPOSIT_AMOUNTS[1];
|
|
195
|
-
}
|
|
196
|
-
/** The deposit's descriptor code as sent: the test value. */
|
|
197
|
-
function descriptorMatches(params: Record<string, unknown>): boolean {
|
|
198
|
-
return String(params.descriptor_code) === MICRODEPOSIT_DESCRIPTOR;
|
|
199
|
-
}
|
|
200
|
-
/** Stripe's refusal of deposits that do not match what was sent. */
|
|
201
|
-
function depositMismatch(byAmounts: boolean): StripeResponse {
|
|
202
|
-
return byAmounts
|
|
203
|
-
? err('The amounts provided do not match the amounts that were sent to the bank account.', 400, 'payment_intent_microdeposit_verification_amounts_mismatch')
|
|
204
|
-
: err('The descriptor code provided does not match the one that was sent to the bank account.', 400, 'payment_intent_microdeposit_verification_descriptor_code_mismatch');
|
|
205
|
-
}
|
|
206
|
-
// Validate the submitted micro-deposit verification params against the modeled test values.
|
|
207
|
-
// Returns a vendor 400 on missing/both/wrong inputs, or null when the verification matches.
|
|
208
|
-
export function verifyMicrodeposits(params: Record<string, unknown>, _existing: Record<string, unknown>): StripeResponse | null {
|
|
209
|
-
const hasAmounts = Array.isArray(params.amounts);
|
|
210
|
-
const hasDescriptor = typeof params.descriptor_code === 'string' && params.descriptor_code !== '';
|
|
211
|
-
if (hasAmounts === hasDescriptor) return err('You must pass exactly one of `amounts` and `descriptor_code`.', 400, 'parameter_missing');
|
|
212
|
-
const matches = hasAmounts ? amountsMatch(params) : descriptorMatches(params);
|
|
213
|
-
return matches ? null : depositMismatch(hasAmounts);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
186
|
// Build the real Stripe card-error envelope (HTTP 402). `param` is 'card' for card
|
|
217
187
|
// errors; charge/payment_intent ids are attached when known so SDK error objects carry
|
|
218
188
|
// them (Stripe does this for confirm/charge failures).
|
|
@@ -364,7 +334,7 @@ export function asBool(v: unknown): boolean {
|
|
|
364
334
|
// resource view. This is the vendor's OWN per-field semantics (A4: not a cross-vendor query
|
|
365
335
|
// engine — it lives in the Stripe pack and speaks Stripe's language only). Unsupported
|
|
366
336
|
// constructs cause the clause to not match (safe), never a fabricated hit.
|
|
367
|
-
type SearchClause = { field: string; op: ':' | '>' | '<' | '>=' | '<='; value: string };
|
|
337
|
+
type SearchClause = { field: string; op: ':' | '~' | '>' | '<' | '>=' | '<='; value: string };
|
|
368
338
|
// Tokenize on top-level AND/OR (we treat the whole query as either all-AND or all-OR; mixed
|
|
369
339
|
// precedence is uncommon in practice and Stripe groups with parens we don't model — kept simple).
|
|
370
340
|
function parseSearchQuery(query: string): { clauses: SearchClause[]; disjunction: boolean } | null {
|
|
@@ -373,12 +343,13 @@ function parseSearchQuery(query: string): { clauses: SearchClause[]; disjunction
|
|
|
373
343
|
const disjunction = / OR /i.test(trimmed) && !/ AND /i.test(trimmed);
|
|
374
344
|
const parts = trimmed.split(disjunction ? / OR /i : / AND /i);
|
|
375
345
|
const clauses: SearchClause[] = [];
|
|
376
|
-
const re = /^\s*([A-Za-z0-9_]+(?:\[[^\]]+\])?)\s*(
|
|
346
|
+
const re = /^\s*([A-Za-z0-9_]+(?:\[[^\]]+\])?)\s*(>=|<=|>|<|:|~)\s*(.+?)\s*$/;
|
|
377
347
|
for (const part of parts) {
|
|
378
348
|
const m = re.exec(part);
|
|
379
349
|
if (!m) return null;
|
|
380
350
|
let value = m[3]!.trim();
|
|
381
351
|
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) value = value.slice(1, -1);
|
|
352
|
+
if (m[2] === '~' && (!['email', 'name'].includes(m[1]!) || value.length < 3)) return null;
|
|
382
353
|
clauses.push({ field: m[1]!, op: m[2] as SearchClause['op'], value });
|
|
383
354
|
}
|
|
384
355
|
return { clauses, disjunction };
|
|
@@ -396,22 +367,19 @@ function searchFieldValue(item: Record<string, unknown>, field: string): unknown
|
|
|
396
367
|
}
|
|
397
368
|
/** A search's numeric comparison other than `>=` (docs.stripe.com/search#search-query-language). */
|
|
398
369
|
function otherComparison(op: string, a: number, n: number): boolean {
|
|
399
|
-
|
|
400
|
-
case '>': return a > n;
|
|
401
|
-
case '<': return a < n;
|
|
402
|
-
case '<=': return a <= n;
|
|
403
|
-
}
|
|
404
|
-
return false;
|
|
370
|
+
return op === '>' ? a > n : op === '<' ? a < n : a <= n;
|
|
405
371
|
}
|
|
406
372
|
|
|
407
373
|
function evalSearchClause(item: Record<string, unknown>, c: SearchClause): boolean {
|
|
408
374
|
const actual = searchFieldValue(item, c.field);
|
|
375
|
+
// source: https://docs.stripe.com/search "Substring match operator (substrings must be a minimum of 3 characters)"
|
|
376
|
+
if (c.op === '~') return actual != null && String(actual).toLowerCase().includes(c.value.toLowerCase());
|
|
409
377
|
if (c.op === ':') {
|
|
410
378
|
if (c.value === 'null') return actual === null || actual === undefined;
|
|
411
379
|
// numeric exact when both look numeric; else string-equality (case-sensitive, like Stripe tokens)
|
|
412
380
|
if (typeof actual === 'number' && /^-?\d+(\.\d+)?$/.test(c.value)) return actual === Number(c.value);
|
|
413
|
-
if (typeof actual === 'boolean') return String(actual) === c.value;
|
|
414
|
-
return String(actual) === c.value;
|
|
381
|
+
if (typeof actual === 'boolean') return String(actual).toLowerCase() === c.value.toLowerCase();
|
|
382
|
+
return String(actual).toLowerCase() === c.value.toLowerCase();
|
|
415
383
|
}
|
|
416
384
|
const n = Number(c.value);
|
|
417
385
|
const a = Number(actual);
|
|
@@ -470,23 +438,6 @@ export function buildDiscount(coupon: Record<string, unknown>, customer: string,
|
|
|
470
438
|
};
|
|
471
439
|
}
|
|
472
440
|
|
|
473
|
-
// The (price id, quantity) pairs a subscription bills, normalized across the shapes the
|
|
474
|
-
// twin stores items in (the create form array items:[{price,quantity}], or the canonical
|
|
475
|
-
// items:{data:[{price}]} list). Used to compute upcoming-invoice lines.
|
|
476
|
-
export function subscriptionItemPairs(sub: Record<string, unknown>): Array<{ price: string; quantity: number }> {
|
|
477
|
-
const items = sub.items as { data?: unknown } | undefined;
|
|
478
|
-
const data = Array.isArray(items?.data) ? items!.data : Array.isArray(sub.items) ? (sub.items as unknown[]) : [];
|
|
479
|
-
const out: Array<{ price: string; quantity: number }> = [];
|
|
480
|
-
for (const it of data as Array<Record<string, unknown>>) {
|
|
481
|
-
const p = it?.price;
|
|
482
|
-
const id = typeof p === 'string' ? p : (p && typeof p === 'object' ? String((p as Record<string, unknown>).id ?? '') : '');
|
|
483
|
-
if (id) out.push({ price: id, quantity: Math.max(1, Math.trunc(Number(it.quantity) || 1)) });
|
|
484
|
-
}
|
|
485
|
-
// a bare `price` field (the connector's flattened shape) counts as one item.
|
|
486
|
-
if (out.length === 0 && typeof sub.price === 'string' && sub.price) out.push({ price: sub.price, quantity: 1 });
|
|
487
|
-
return out;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
441
|
// The raw item entries a subscription/checkout-derived-subscription create sent,
|
|
491
442
|
// normalized to an ordered array. The form reader already turns the bracket form
|
|
492
443
|
// items[0][price]=… into an array of objects (see lineItemEntries() above for the
|
|
@@ -548,18 +499,13 @@ export function addBillingInterval(atUnix: number, interval: BillingInterval, co
|
|
|
548
499
|
// response includes the actual items, mirroring what was requested.
|
|
549
500
|
/** What a subscription item carries of its price: the Price itself and its plan view (a subscription item answers both,
|
|
550
501
|
* docs.stripe.com/api/subscription_items/object), by the price's id or an already-resolved Price. */
|
|
502
|
+
// source: https://docs.stripe.com/api/plans/create "It replaces the Plans API and is backwards compatible to simplify your migration."
|
|
503
|
+
// source: spec:/components/schemas/plan "amount_decimal"
|
|
504
|
+
|
|
551
505
|
export function subscriptionItemPrice(p: unknown, find: FindStored): { price: Record<string, unknown> | string | null; plan: Record<string, unknown> | null } {
|
|
552
506
|
const priceId = typeof p === 'string' ? p : (p && typeof p === 'object' ? String((p as Record<string, unknown>).id ?? '') : '');
|
|
553
507
|
const price = priceId ? find('price', priceId) : undefined;
|
|
554
|
-
const
|
|
555
|
-
const plan = price ? {
|
|
556
|
-
id: price.id, object: 'plan', active: price.active ?? true, amount: price.unit_amount ?? null,
|
|
557
|
-
amount_decimal: price.unit_amount_decimal ?? null, billing_scheme: price.billing_scheme ?? 'per_unit',
|
|
558
|
-
currency: price.currency ?? 'usd', interval: recurring?.interval ?? null,
|
|
559
|
-
interval_count: recurring?.interval_count ?? 1, livemode: false,
|
|
560
|
-
metadata: price.metadata ?? {}, nickname: price.nickname ?? null,
|
|
561
|
-
product: price.product ?? null, tiers_mode: price.tiers_mode ?? null, usage_type: 'licensed',
|
|
562
|
-
} : null;
|
|
508
|
+
const plan = price ? planView(price) : null;
|
|
563
509
|
return { price: price ?? (priceId || null), plan };
|
|
564
510
|
}
|
|
565
511
|
|
|
@@ -612,20 +558,6 @@ export function resolveTrial(params: Record<string, unknown>, at: number): { sta
|
|
|
612
558
|
return undefined;
|
|
613
559
|
}
|
|
614
560
|
|
|
615
|
-
// ── ISSUING helpers: spending controls, real-time-auth shapes ─────────────────────────
|
|
616
|
-
// Authorization.card is the FULL Card object in every vendor egress (API responses and
|
|
617
|
-
// webhook payloads) — it is not expandable (stripe@22.3.0 Issuing.Authorization.card:
|
|
618
|
-
// Stripe.Issuing.Card). The twin stores the id internally and embeds at the boundary;
|
|
619
|
-
// cardholder stays an id string (expandable, unexpanded default). Caught live: the
|
|
620
|
-
// issuing-bridge reads event.data.object.card.id and refused every twin presentment.
|
|
621
|
-
export function embedIssuingAuthorizationCard(body: Record<string, unknown>, find: FindStored): Record<string, unknown> {
|
|
622
|
-
if (typeof body.card === 'string') {
|
|
623
|
-
const cardRow = find('issuing_card', body.card);
|
|
624
|
-
if (cardRow !== undefined) body.card = cardRow;
|
|
625
|
-
}
|
|
626
|
-
return body;
|
|
627
|
-
}
|
|
628
|
-
|
|
629
561
|
// The closed interval set for spending_controls[spending_limits][][interval]
|
|
630
562
|
// (stripe@22.3.0 Issuing/Cards.d.ts SpendingLimit.Interval — a documented closed enum).
|
|
631
563
|
const SPENDING_LIMIT_INTERVALS = new Set(['all_time', 'daily', 'monthly', 'per_authorization', 'weekly', 'yearly']);
|
|
@@ -656,10 +588,10 @@ function controlsRefused(what: 'dictionary' | 'categories' | 'countries' | 'limi
|
|
|
656
588
|
// Vendor rules enforced: interval must be in the closed enum; a spending limit amount is a
|
|
657
589
|
// positive integer; allowed_categories "Cannot be set with blocked_categories" (and the
|
|
658
590
|
// merchant-country pair likewise) — both restrictions verbatim from the SDK's field docs.
|
|
659
|
-
export function normalizeSpendingControls(raw: unknown, currency: string | null): { controls?: Record<string, unknown>; error?: StripeResponse } {
|
|
591
|
+
export function normalizeSpendingControls(raw: unknown, currency: string | null, previous?: Record<string, unknown>): { controls?: Record<string, unknown>; error?: StripeResponse } {
|
|
660
592
|
if (raw === undefined) return {};
|
|
661
593
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return controlsRefused('dictionary');
|
|
662
|
-
const sc = raw as Record<string, unknown
|
|
594
|
+
const sc = { ...previous, ...(raw as Record<string, unknown>) };
|
|
663
595
|
const strArr = (v: unknown): string[] | undefined => (Array.isArray(v) ? v.map(String) : undefined);
|
|
664
596
|
const allowedCats = strArr(sc.allowed_categories);
|
|
665
597
|
const blockedCats = strArr(sc.blocked_categories);
|
|
@@ -681,6 +613,9 @@ export function normalizeSpendingControls(raw: unknown, currency: string | null)
|
|
|
681
613
|
const arrOrNull = (v: string[] | undefined) => (v === undefined ? undefined : v.length ? v : null);
|
|
682
614
|
const controls: Record<string, unknown> = {
|
|
683
615
|
...emptySpendingControls(),
|
|
616
|
+
// source: spec:/paths/~1v1~1issuing~1cards~1{card}/post/requestBody/content/application~1x-www-form-urlencoded/schema/properties/spending_controls "Rules that control spending for this card."
|
|
617
|
+
// Keep every documented control, including allowed_card_presences and blocked_card_presences.
|
|
618
|
+
...sc,
|
|
684
619
|
spending_limits: limits,
|
|
685
620
|
...(limits.length ? { spending_limits_currency: typeof sc.spending_limits_currency === 'string' ? sc.spending_limits_currency : currency } : {}),
|
|
686
621
|
};
|
|
@@ -789,6 +724,9 @@ function cardSubObject(params: Params): Params {
|
|
|
789
724
|
brand: 'visa', last4: number ? number.slice(-4) : '4242',
|
|
790
725
|
exp_month: Number(c.exp_month) || 12, exp_year: Number(c.exp_year) || 2034,
|
|
791
726
|
funding: 'credit', country: 'US',
|
|
727
|
+
// source: spec:/components/schemas/payment_method_card/properties/fingerprint/description "Uniquely identifies this particular card number."
|
|
728
|
+
// The synthetic fingerprint is stable for a number; Stripe's opaque fingerprint algorithm is not published.
|
|
729
|
+
fingerprint: digest('sha256', number || '4242424242424242').slice(0, 16),
|
|
792
730
|
checks: { address_line1_check: null, address_postal_code_check: null, cvc_check: 'pass' },
|
|
793
731
|
networks: { available: ['visa'], preferred: null },
|
|
794
732
|
three_d_secure_usage: { supported: true }, wallet: null,
|
|
@@ -1014,3 +952,17 @@ export function chargeDefaults(chargeId: string, amount: number, captured: boole
|
|
|
1014
952
|
// collection path segment → resource type, for the generic list of a collection Stripe does not
|
|
1015
953
|
// list at that path (the twin's test clocks, which Stripe lists under /v1/test_helpers).
|
|
1016
954
|
|
|
955
|
+
|
|
956
|
+
export function planView(price: Record<string, unknown>): Record<string, unknown> {
|
|
957
|
+
const recurring = price.recurring as Record<string, unknown> | undefined;
|
|
958
|
+
return {
|
|
959
|
+
id: price.id, object: 'plan', active: price.active ?? true, amount: price.unit_amount ?? null,
|
|
960
|
+
amount_decimal: price.unit_amount_decimal ?? (price.unit_amount == null ? null : String(price.unit_amount)),
|
|
961
|
+
billing_scheme: price.billing_scheme ?? 'per_unit', created: price.created, currency: price.currency,
|
|
962
|
+
interval: recurring?.interval ?? null, interval_count: recurring?.interval_count ?? 1,
|
|
963
|
+
livemode: price.livemode ?? false, metadata: price.metadata ?? {}, nickname: price.nickname ?? null,
|
|
964
|
+
product: price.product ?? null, tiers_mode: price.tiers_mode ?? null,
|
|
965
|
+
meter: recurring?.meter ?? null, trial_period_days: recurring?.trial_period_days ?? null,
|
|
966
|
+
transform_usage: price.transform_quantity ?? null, usage_type: recurring?.usage_type ?? 'licensed',
|
|
967
|
+
};
|
|
968
|
+
}
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
// invoice's PaymentIntent; canceling it is the machine's move in ../manifest.ts. Retrieve of a
|
|
4
4
|
// subscription and of an item are the derived core's.
|
|
5
5
|
import type { Row } from './common.ts';
|
|
6
|
-
|
|
7
6
|
export const SUB = 'subscription';
|
|
8
7
|
|
|
9
8
|
// pause_collection pauses billing (the subscription stays active; the paused event fires) and
|
|
@@ -36,5 +35,3 @@ export function movedTrial(sub: Row, fields: Row, end: number, now: number): und
|
|
|
36
35
|
});
|
|
37
36
|
return undefined;
|
|
38
37
|
}
|
|
39
|
-
|
|
40
|
-
export const ITEM_ATTRIBUTES = new Set(['billing_thresholds', 'discounts', 'metadata', 'price', 'quantity', 'tax_rates']);
|
package/src/engine/transfers.ts
CHANGED
|
@@ -19,5 +19,4 @@
|
|
|
19
19
|
// captured; the refusals' wording is the twin's.
|
|
20
20
|
// Transfer list, retrieve and update and application-fee list and retrieve are the derived core's.
|
|
21
21
|
import type { Row } from './common.ts';
|
|
22
|
-
|
|
23
22
|
export const emptyList = (url: string): Row => ({ object: 'list', data: [], has_more: false, total_count: 0, url });
|
package/src/engine/version.ts
CHANGED
|
@@ -15,6 +15,7 @@ type Row = Record<string, unknown>;
|
|
|
15
15
|
/** The version the pack serves: its vendored spec's. */
|
|
16
16
|
export const SERVED_VERSION = String(surface.version);
|
|
17
17
|
const BASIL = '2025-03-31';
|
|
18
|
+
const CLOVER = '2025-09-30';
|
|
18
19
|
|
|
19
20
|
/** Whether a request is answered in the served version's shape. */
|
|
20
21
|
export const servesCurrent = (pinned?: string | null): boolean => !pinned || pinned >= BASIL;
|
|
@@ -23,6 +24,9 @@ export const servesCurrent = (pinned?: string | null): boolean => !pinned || pin
|
|
|
23
24
|
// flag, the payment behavior a subscription was created with) and links it follows internally.
|
|
24
25
|
const KEPT: Record<string, string[]> = {
|
|
25
26
|
account: ['livemode'],
|
|
27
|
+
// source: spec:PostWebhookEndpoints "Whether this endpoint should receive events from connected accounts"
|
|
28
|
+
// connect configures event delivery; it is not a WebhookEndpoint response field.
|
|
29
|
+
webhook_endpoint: ['connect'],
|
|
26
30
|
// a meter event is identified by its identifier; the twin's row id is its own
|
|
27
31
|
'billing.meter_event': ['id'],
|
|
28
32
|
// a card source's creation time is the twin's, for ordering
|
|
@@ -37,7 +41,7 @@ const KEPT: Record<string, string[]> = {
|
|
|
37
41
|
payment_intent: ['mandate', 'off_session'],
|
|
38
42
|
subscription: ['payment_behavior'],
|
|
39
43
|
subscription_item: ['livemode'],
|
|
40
|
-
subscription_schedule: ['from_subscription', 'renewal_interval'],
|
|
44
|
+
subscription_schedule: ['from_subscription', 'renewal_interval', 'proration_behavior'],
|
|
41
45
|
'tax.transaction': ['calculation'],
|
|
42
46
|
'terminal.reader': ['registration_code'],
|
|
43
47
|
topup: ['destination_balance'],
|
|
@@ -66,8 +70,7 @@ const BASIL_CHANGES: Record<string, (o: Row, parent?: Row) => Row> = {
|
|
|
66
70
|
// "confirmation_secret … Currently, this contains the client_secret of the PaymentIntent that Stripe creates during
|
|
67
71
|
// invoice finalization" (docs.stripe.com/api/invoices/object; includable, answered only when expanded: INCLUDABLE);
|
|
68
72
|
// the intent's client_secret is the one ./stripe.ts mintClientSecret gives it
|
|
69
|
-
const
|
|
70
|
-
const confirmation_secret = intent ? { type: 'payment_intent', client_secret: `${intent}_secret_twin` } : null;
|
|
73
|
+
const confirmation_secret = o.confirmation_secret ?? null;
|
|
71
74
|
return { ...omit(o, ['payment_intent', 'charge', 'paid', 'paid_out_of_band', 'subscription']), parent, confirmation_secret };
|
|
72
75
|
},
|
|
73
76
|
charge: (o) => omit(o, ['invoice', 'source']),
|
|
@@ -228,9 +231,6 @@ const INCLUDABLE: Record<string, string[]> = {
|
|
|
228
231
|
invoice: ['confirmation_secret'],
|
|
229
232
|
};
|
|
230
233
|
|
|
231
|
-
/** Every includable field name, as `expand` paths on a top-level object: what a check of an object's FULL answer expands. */
|
|
232
|
-
export const INCLUDABLE_FIELDS: string[] = [...new Set(Object.values(INCLUDABLE).flat())];
|
|
233
|
-
|
|
234
234
|
/** The `expand[]` paths a request names, in its query or its body (JSON or form). */
|
|
235
235
|
export async function expandOf(request: Request): Promise<string[]> {
|
|
236
236
|
const url = new URL(request.url);
|
|
@@ -274,9 +274,17 @@ export function render(value: unknown, pinned?: string | null, expand: string[]
|
|
|
274
274
|
// reader coerces a numeric-looking value to a number, which the answer gives back as the string it was sent as
|
|
275
275
|
// reads the same, and a key posted "" to remove it is not there (../semantics/shared.ts withMetadata)
|
|
276
276
|
if (o.metadata && typeof o.metadata === 'object' && !Array.isArray(o.metadata)) o.metadata = Object.fromEntries(Object.entries(o.metadata as Row).filter(([, v]) => v !== '').map(([k, v]) => [k, typeof v === 'number' || typeof v === 'boolean' ? String(v) : v]));
|
|
277
|
+
// source: spec:/components/schemas/checkout.session/properties/ui_mode "Defaults to `hosted_page`."
|
|
278
|
+
// Endive names hosted/custom Checkout hosted_page/elements; older SDK versions keep their own names.
|
|
279
|
+
if (kind === 'checkout.session' && (!pinned || pinned >= '2026-09-30')) {
|
|
280
|
+
if (o.ui_mode === 'hosted') o.ui_mode = 'hosted_page';
|
|
281
|
+
if (o.ui_mode === 'custom') o.ui_mode = 'elements';
|
|
282
|
+
}
|
|
277
283
|
const kept = KEPT[kind] ? omit(o, KEPT[kind]!) : o;
|
|
278
284
|
// an includable field a version's change adds (an invoice's confirmation_secret) is hidden as its stored ones are
|
|
279
|
-
|
|
285
|
+
// source: https://docs.stripe.com/changelog/clover/2025-09-30/polymorphic-coupon "Moves the top-level coupon field on Promotion Codes to promotion.coupon"
|
|
286
|
+
const changed = kind === 'promotion_code' && pinned && pinned < CLOVER ? omit(kept, ['promotion']) : BASIL_CHANGES[kind] ? BASIL_CHANGES[kind]!(kept) : kept;
|
|
287
|
+
return omit(withNulls(changed, kind), hidden);
|
|
280
288
|
};
|
|
281
289
|
return walk(value);
|
|
282
290
|
}
|
package/src/engine/webhooks.ts
CHANGED
|
@@ -3,9 +3,7 @@
|
|
|
3
3
|
// event type, Stripe's own event types (the spec's list, ../generated/events.gen.json), and the one message Stripe decides
|
|
4
4
|
// by its receiver's answer, the real-time authorization request.
|
|
5
5
|
import { hmac } from '@volter/world-core';
|
|
6
|
-
|
|
7
6
|
import vendorEvents from '../generated/events.gen.json' with { type: 'json' };
|
|
8
|
-
|
|
9
7
|
/** Every event type Stripe's spec enumerates: a write whose operation is one sends it, and `<resource>.create`,
|
|
10
8
|
* `.update` and `.delete` send `<resource>.created`, `.updated` and `.deleted` when Stripe has that type. */
|
|
11
9
|
export const STRIPE_EVENT_TYPES: ReadonlyArray<string> = (vendorEvents as { types: string[] }).types;
|
package/src/fetch.ts
CHANGED
|
@@ -8,8 +8,8 @@ import { clock } from './semantics/clock.ts';
|
|
|
8
8
|
import * as doors from './semantics/doors.ts';
|
|
9
9
|
import { stripeSemantics } from './semantics/index.ts';
|
|
10
10
|
import { screen as screen_billing_portal } from './screens/billing-portal.tsx';
|
|
11
|
+
import { screen as screen_checkout_js } from './screens/checkout-js.ts';
|
|
11
12
|
import { screen as screen_checkout } from './screens/checkout.tsx';
|
|
12
|
-
import { screen as screen_connect_oauth } from './screens/connect-oauth.tsx';
|
|
13
13
|
import { screen as screen_connect_onboarding } from './screens/connect-onboarding.tsx';
|
|
14
14
|
import { screen as screen_dashboard } from './screens/dashboard.tsx';
|
|
15
15
|
import { screen as screen_stripe_js } from './screens/stripe-js.ts';
|
|
@@ -22,6 +22,6 @@ export function createStripeFetch(options: PackFetchOptions = {}): DerivedFetch
|
|
|
22
22
|
doors,
|
|
23
23
|
around,
|
|
24
24
|
clock,
|
|
25
|
-
screens: { "billing-portal": screen_billing_portal, "checkout":
|
|
25
|
+
screens: { "billing-portal": screen_billing_portal, "checkout-js": screen_checkout_js, "checkout": screen_checkout, "connect-onboarding": screen_connect_onboarding, "dashboard": screen_dashboard, "stripe-js": screen_stripe_js },
|
|
26
26
|
}, options);
|
|
27
27
|
}
|