@volter/twin-stripe 2.0.37 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -207
- package/dist/generated/pack-facts.json +94 -0
- package/dist/spec/SOURCE.md +14 -0
- package/dist/spec/fixtures3.json.gz +0 -0
- package/dist/spec/openapi.json.gz +0 -0
- package/dist/spec/patches.json +34 -0
- package/dist/spec/sources.json +378 -0
- package/dist/src/cli.js +12 -30
- package/dist/src/engine/after-payment.d.ts +9 -0
- package/dist/src/engine/after-payment.js +28 -0
- package/dist/src/engine/apps-secrets.d.ts +1 -0
- package/dist/src/engine/apps-secrets.js +3 -0
- package/dist/src/engine/balance.d.ts +5 -0
- package/dist/src/engine/balance.js +15 -0
- package/dist/src/engine/billing.d.ts +5 -0
- package/dist/src/engine/billing.js +6 -0
- package/dist/src/engine/charges.d.ts +6 -0
- package/dist/src/engine/charges.js +13 -0
- package/dist/src/engine/checkout.d.ts +24 -0
- package/dist/src/engine/checkout.js +30 -0
- package/dist/src/engine/common.d.ts +19 -0
- package/dist/src/engine/common.js +32 -0
- package/dist/src/{screens → engine}/connect-settings.d.ts +1 -7
- package/dist/src/engine/connect-settings.js +26 -0
- package/dist/src/engine/connect.d.ts +66 -0
- package/dist/src/engine/connect.js +189 -0
- package/dist/src/engine/coupons.d.ts +0 -0
- package/dist/src/engine/coupons.js +5 -0
- package/dist/src/engine/credit-notes.d.ts +1 -0
- package/dist/src/engine/credit-notes.js +5 -0
- package/dist/src/engine/customers.d.ts +12 -0
- package/dist/src/engine/customers.js +30 -0
- package/dist/src/engine/display.d.ts +14 -0
- package/dist/src/engine/display.js +16 -0
- package/dist/src/engine/disputes.d.ts +0 -0
- package/dist/src/engine/disputes.js +9 -0
- package/dist/src/engine/entitlements.d.ts +1 -0
- package/dist/src/engine/entitlements.js +4 -0
- package/dist/src/engine/ephemeral-keys.d.ts +0 -0
- package/dist/src/engine/ephemeral-keys.js +4 -0
- package/dist/src/engine/files.d.ts +18 -0
- package/dist/src/engine/files.js +33 -0
- package/dist/src/engine/front.d.ts +5 -0
- package/dist/src/engine/front.js +25 -0
- package/dist/src/engine/invoices.d.ts +24 -0
- package/dist/src/engine/invoices.js +57 -0
- package/dist/src/engine/issuing.d.ts +35 -0
- package/dist/src/engine/issuing.js +45 -0
- package/dist/src/engine/ledger.d.ts +9 -0
- package/dist/src/engine/ledger.js +9 -0
- package/dist/src/engine/oauth.d.ts +4 -0
- package/dist/src/engine/oauth.js +25 -0
- package/dist/src/engine/onboarding.d.ts +13 -0
- package/dist/src/engine/onboarding.js +42 -0
- package/dist/src/{stripe-params.d.ts → engine/params.d.ts} +20 -3
- package/dist/src/{stripe-params.js → engine/params.js} +7 -60
- package/dist/src/engine/payment-intents.d.ts +10 -0
- package/dist/src/engine/payment-intents.js +27 -0
- package/dist/src/engine/payment-links.d.ts +1 -0
- package/dist/src/engine/payment-links.js +5 -0
- package/dist/src/engine/payment-methods.d.ts +16 -0
- package/dist/src/engine/payment-methods.js +22 -0
- package/dist/src/engine/plans.d.ts +6 -0
- package/dist/src/engine/plans.js +26 -0
- package/dist/src/engine/platform.d.ts +39 -0
- package/dist/src/engine/platform.js +34 -0
- package/dist/src/engine/products.d.ts +0 -0
- package/dist/src/engine/products.js +4 -0
- package/dist/src/engine/radar.d.ts +2 -0
- package/dist/src/engine/radar.js +5 -0
- package/dist/src/engine/refunds.d.ts +1 -0
- package/dist/src/engine/refunds.js +28 -0
- package/dist/src/engine/renewals.d.ts +36 -0
- package/dist/src/engine/renewals.js +65 -0
- package/dist/src/engine/setup-intents.d.ts +1 -0
- package/dist/src/engine/setup-intents.js +4 -0
- package/dist/src/{stripe-twin.d.ts → engine/stripe.d.ts} +11 -39
- package/dist/src/{stripe-twin.js → engine/stripe.js} +25 -310
- package/dist/src/engine/subscription-schedules.d.ts +3 -0
- package/dist/src/engine/subscription-schedules.js +1 -0
- package/dist/src/engine/subscriptions.d.ts +12 -0
- package/dist/src/engine/subscriptions.js +31 -0
- package/dist/src/engine/tax.d.ts +7 -0
- package/dist/src/engine/tax.js +28 -0
- package/dist/src/engine/terminal.d.ts +4 -0
- package/dist/src/engine/terminal.js +20 -0
- package/dist/src/engine/test-clocks.d.ts +5 -0
- package/dist/src/engine/test-clocks.js +9 -0
- package/dist/src/engine/tokens.d.ts +7 -0
- package/dist/src/engine/tokens.js +20 -0
- package/dist/src/engine/transfers.d.ts +2 -0
- package/dist/src/engine/transfers.js +1 -0
- package/dist/src/engine/treasury.d.ts +23 -0
- package/dist/src/engine/treasury.js +45 -0
- package/dist/src/{stripe-version.js → engine/version.js} +4 -3
- package/dist/src/engine/webhook-endpoints.d.ts +1 -0
- package/dist/src/engine/webhook-endpoints.js +12 -0
- package/dist/src/engine/webhooks.d.ts +31 -0
- package/dist/src/engine/webhooks.js +68 -0
- package/dist/src/fetch.d.ts +2 -0
- package/dist/src/fetch.js +26 -0
- package/dist/src/generated/surface.gen.json +1 -1
- package/dist/src/generated/ui.gen.json +1 -1
- package/dist/src/index.d.ts +5 -14
- package/dist/src/index.js +8 -75
- package/dist/src/manifest.js +232 -758
- package/dist/src/screens/billing-portal.d.ts +3 -0
- package/dist/src/screens/{portal.js → billing-portal.js} +28 -54
- package/dist/src/screens/checkout.d.ts +2 -31
- package/dist/src/screens/checkout.js +32 -121
- package/dist/src/screens/connect-oauth.d.ts +3 -27
- package/dist/src/screens/connect-oauth.js +51 -132
- package/dist/src/screens/connect-onboarding.d.ts +3 -0
- package/dist/src/screens/{onboarding.js → connect-onboarding.js} +36 -102
- package/dist/src/screens/dashboard.d.ts +3 -0
- package/dist/src/screens/dashboard.js +117 -0
- package/dist/src/screens/shared.d.ts +27 -0
- package/dist/src/screens/shared.js +87 -0
- package/dist/src/screens/stripe-js.d.ts +3 -0
- package/dist/src/screens/stripe-js.js +135 -0
- package/dist/src/semantics/account_links.d.ts +2 -0
- package/dist/src/semantics/account_links.js +22 -0
- package/dist/src/semantics/accounts.d.ts +4 -0
- package/dist/src/semantics/accounts.js +38 -0
- package/dist/src/semantics/apps.d.ts +4 -0
- package/dist/src/semantics/{apps-secrets.js → apps.js} +14 -26
- package/dist/src/semantics/around.d.ts +2 -0
- package/dist/src/semantics/around.js +16 -0
- package/dist/src/semantics/balance.d.ts +2 -11
- package/dist/src/semantics/balance.js +3 -194
- package/dist/src/semantics/balance_transactions.d.ts +2 -0
- package/dist/src/semantics/balance_transactions.js +15 -0
- package/dist/src/semantics/billing_portal.d.ts +5 -0
- package/dist/src/semantics/billing_portal.js +42 -0
- package/dist/src/semantics/charges.d.ts +3 -28
- package/dist/src/semantics/charges.js +5 -202
- package/dist/src/semantics/checkout.d.ts +3 -20
- package/dist/src/semantics/checkout.js +8 -261
- package/dist/src/semantics/clock.d.ts +2 -0
- package/dist/src/semantics/clock.js +21 -0
- package/dist/src/semantics/coupons.d.ts +2 -6
- package/dist/src/semantics/coupons.js +2 -60
- package/dist/src/semantics/customers.d.ts +6 -6
- package/dist/src/semantics/customers.js +27 -387
- package/dist/src/semantics/disputes.d.ts +2 -2
- package/dist/src/semantics/disputes.js +3 -49
- package/dist/src/semantics/doors.d.ts +23 -0
- package/dist/src/semantics/doors.js +73 -0
- package/dist/src/semantics/entitlements.d.ts +2 -2
- package/dist/src/semantics/entitlements.js +5 -50
- package/dist/src/semantics/events.d.ts +12 -0
- package/dist/src/semantics/events.js +36 -0
- package/dist/src/semantics/index.d.ts +2 -0
- package/dist/src/semantics/index.js +107 -0
- package/dist/src/semantics/invoice_payments.d.ts +2 -0
- package/dist/src/semantics/invoice_payments.js +12 -0
- package/dist/src/semantics/invoiceitems.d.ts +3 -0
- package/dist/src/semantics/invoiceitems.js +59 -0
- package/dist/src/semantics/invoices.d.ts +16 -18
- package/dist/src/semantics/invoices.js +118 -444
- package/dist/src/semantics/payment_intents.d.ts +4 -0
- package/dist/src/semantics/payment_intents.js +79 -0
- package/dist/src/semantics/payment_methods.d.ts +5 -0
- package/dist/src/semantics/payment_methods.js +39 -0
- package/dist/src/semantics/payouts.d.ts +2 -0
- package/dist/src/semantics/payouts.js +21 -0
- package/dist/src/semantics/prices.d.ts +4 -0
- package/dist/src/semantics/prices.js +37 -0
- package/dist/src/semantics/products.d.ts +6 -2
- package/dist/src/semantics/products.js +15 -130
- package/dist/src/semantics/promotion_codes.d.ts +3 -0
- package/dist/src/semantics/promotion_codes.js +35 -0
- package/dist/src/semantics/radar.d.ts +2 -2
- package/dist/src/semantics/radar.js +5 -74
- package/dist/src/semantics/refunds.d.ts +3 -9
- package/dist/src/semantics/refunds.js +6 -192
- package/dist/src/semantics/setup_intents.d.ts +3 -0
- package/dist/src/semantics/{setup-intents.js → setup_intents.js} +10 -53
- package/dist/src/semantics/shared.d.ts +822 -47
- package/dist/src/semantics/shared.js +3044 -78
- package/dist/src/semantics/states.d.ts +531 -0
- package/dist/src/semantics/states.js +852 -0
- package/dist/src/semantics/subscription_items.d.ts +5 -0
- package/dist/src/semantics/subscription_items.js +55 -0
- package/dist/src/semantics/subscription_schedules.d.ts +4 -0
- package/dist/src/semantics/subscription_schedules.js +60 -0
- package/dist/src/semantics/subscriptions.d.ts +7 -11
- package/dist/src/semantics/subscriptions.js +44 -415
- package/dist/src/semantics/test_helpers.d.ts +6 -0
- package/dist/src/semantics/{test-clocks.js → test_helpers.js} +14 -40
- package/dist/src/semantics/transfers.d.ts +2 -2
- package/dist/src/semantics/transfers.js +6 -101
- package/dist/src/server.d.ts +7 -0
- package/dist/src/server.js +7 -0
- package/generated/pack-facts.json +94 -0
- package/package.json +10 -20
- package/spec/SOURCE.md +14 -0
- package/spec/fixtures3.json.gz +0 -0
- package/spec/openapi.json.gz +0 -0
- package/spec/patches.json +34 -0
- package/spec/sources.json +378 -0
- package/src/cli.ts +12 -26
- package/src/engine/after-payment.ts +31 -0
- package/src/engine/apps-secrets.ts +4 -0
- package/src/engine/balance.ts +33 -0
- package/src/engine/billing.ts +16 -0
- package/src/engine/charges.ts +19 -0
- package/src/engine/checkout.ts +40 -0
- package/src/engine/common.ts +47 -0
- package/src/engine/connect-settings.ts +23 -0
- package/src/engine/connect.ts +184 -0
- package/src/engine/coupons.ts +5 -0
- package/src/engine/credit-notes.ts +6 -0
- package/src/engine/customers.ts +35 -0
- package/src/engine/display.ts +19 -0
- package/src/engine/disputes.ts +9 -0
- package/src/engine/entitlements.ts +5 -0
- package/src/engine/ephemeral-keys.ts +3 -0
- package/src/engine/files.ts +41 -0
- package/src/engine/front.ts +31 -0
- package/src/engine/invoices.ts +71 -0
- package/src/engine/issuing.ts +61 -0
- package/src/engine/ledger.ts +50 -0
- package/src/engine/oauth.ts +19 -0
- package/src/engine/onboarding.ts +54 -0
- package/src/{stripe-params.ts → engine/params.ts} +8 -56
- package/src/engine/payment-intents.ts +33 -0
- package/src/engine/payment-links.ts +6 -0
- package/src/engine/payment-methods.ts +25 -0
- package/src/engine/plans.ts +42 -0
- package/src/engine/platform.ts +47 -0
- package/src/engine/products.ts +3 -0
- package/src/engine/radar.ts +6 -0
- package/src/engine/refunds.ts +29 -0
- package/src/engine/renewals.ts +73 -0
- package/src/engine/setup-intents.ts +5 -0
- package/src/{stripe-twin.ts → engine/stripe.ts} +28 -323
- package/src/engine/subscription-schedules.ts +10 -0
- package/src/engine/subscriptions.ts +40 -0
- package/src/engine/tax.ts +39 -0
- package/src/engine/terminal.ts +23 -0
- package/src/engine/test-clocks.ts +11 -0
- package/src/engine/tokens.ts +22 -0
- package/src/engine/transfers.ts +23 -0
- package/src/engine/treasury.ts +47 -0
- package/src/{stripe-version.ts → engine/version.ts} +4 -3
- package/src/engine/webhook-endpoints.ts +14 -0
- package/src/engine/webhooks.ts +72 -0
- package/src/fetch.ts +27 -0
- package/src/generated/surface.gen.json +1 -1
- package/src/generated/ui.gen.json +1 -1
- package/src/index.ts +8 -116
- package/src/manifest.ts +234 -786
- package/src/screens/{portal.tsx → billing-portal.tsx} +32 -37
- package/src/screens/checkout.tsx +33 -95
- package/src/screens/connect-oauth.tsx +64 -88
- package/src/screens/{onboarding.tsx → connect-onboarding.tsx} +38 -82
- package/src/screens/dashboard.tsx +198 -0
- package/src/screens/shared.tsx +84 -0
- package/src/screens/stripe-js.ts +170 -0
- package/src/semantics/account_links.ts +21 -0
- package/src/semantics/accounts.ts +43 -0
- package/src/semantics/{apps-secrets.ts → apps.ts} +17 -32
- package/src/semantics/around.ts +20 -0
- package/src/semantics/balance.ts +6 -207
- package/src/semantics/balance_transactions.ts +20 -0
- package/src/semantics/billing_portal.ts +51 -0
- package/src/semantics/charges.ts +12 -211
- package/src/semantics/checkout.ts +13 -268
- package/src/semantics/clock.ts +26 -0
- package/src/semantics/coupons.ts +5 -67
- package/src/semantics/customers.ts +29 -392
- package/src/semantics/disputes.ts +9 -59
- package/src/semantics/doors.ts +80 -0
- package/src/semantics/entitlements.ts +9 -56
- package/src/semantics/events.ts +44 -0
- package/src/semantics/index.ts +111 -0
- package/src/semantics/invoice_payments.ts +18 -0
- package/src/semantics/invoiceitems.ts +59 -0
- package/src/semantics/invoices.ts +109 -443
- package/src/semantics/payment_intents.ts +83 -0
- package/src/semantics/payment_methods.ts +43 -0
- package/src/semantics/payouts.ts +23 -0
- package/src/semantics/prices.ts +42 -0
- package/src/semantics/products.ts +18 -142
- package/src/semantics/promotion_codes.ts +39 -0
- package/src/semantics/radar.ts +8 -77
- package/src/semantics/refunds.ts +9 -212
- package/src/semantics/setup_intents.ts +45 -0
- package/src/semantics/shared.ts +3092 -106
- package/src/semantics/states.ts +883 -0
- package/src/semantics/subscription_items.ts +54 -0
- package/src/semantics/subscription_schedules.ts +66 -0
- package/src/semantics/subscriptions.ts +49 -432
- package/src/semantics/{test-clocks.ts → test_helpers.ts} +23 -47
- package/src/semantics/transfers.ts +11 -131
- package/src/server.ts +8 -0
- package/client/dashboard-api.ts +0 -286
- package/client/stripe-mirror.css +0 -277
- package/client/stripe-mirror.tsx +0 -1559
- package/dist/client/dashboard-api.d.ts +0 -107
- package/dist/client/dashboard-api.js +0 -238
- package/dist/client/dashboard-api.ts +0 -286
- package/dist/client/stripe-mirror.bundle.js +0 -236
- package/dist/client/stripe-mirror.css +0 -277
- package/dist/client/stripe-mirror.d.ts +0 -134
- package/dist/client/stripe-mirror.js +0 -859
- package/dist/client/stripe-mirror.tsx +0 -1559
- package/dist/src/screens/connect-settings.js +0 -103
- package/dist/src/screens/consent-skin.d.ts +0 -4
- package/dist/src/screens/consent-skin.js +0 -18
- package/dist/src/screens/financial-connections.d.ts +0 -5
- package/dist/src/screens/financial-connections.js +0 -90
- package/dist/src/screens/identity.d.ts +0 -5
- package/dist/src/screens/identity.js +0 -86
- package/dist/src/screens/onboarding.d.ts +0 -13
- package/dist/src/screens/portal.d.ts +0 -5
- package/dist/src/screens/public-details.d.ts +0 -5
- package/dist/src/screens/public-details.js +0 -90
- package/dist/src/semantics/after-payment.d.ts +0 -22
- package/dist/src/semantics/after-payment.js +0 -99
- package/dist/src/semantics/apps-secrets.d.ts +0 -2
- package/dist/src/semantics/billing.d.ts +0 -2
- package/dist/src/semantics/billing.js +0 -220
- package/dist/src/semantics/connect.d.ts +0 -5
- package/dist/src/semantics/connect.js +0 -493
- package/dist/src/semantics/credit-notes.d.ts +0 -2
- package/dist/src/semantics/credit-notes.js +0 -172
- package/dist/src/semantics/ephemeral-keys.d.ts +0 -2
- package/dist/src/semantics/ephemeral-keys.js +0 -34
- package/dist/src/semantics/files.d.ts +0 -2
- package/dist/src/semantics/files.js +0 -125
- package/dist/src/semantics/issuing.d.ts +0 -13
- package/dist/src/semantics/issuing.js +0 -575
- package/dist/src/semantics/ledger.d.ts +0 -59
- package/dist/src/semantics/ledger.js +0 -200
- package/dist/src/semantics/payment-intents.d.ts +0 -18
- package/dist/src/semantics/payment-intents.js +0 -452
- package/dist/src/semantics/payment-links.d.ts +0 -2
- package/dist/src/semantics/payment-links.js +0 -133
- package/dist/src/semantics/payment-methods.d.ts +0 -20
- package/dist/src/semantics/payment-methods.js +0 -140
- package/dist/src/semantics/plans.d.ts +0 -5
- package/dist/src/semantics/plans.js +0 -121
- package/dist/src/semantics/platform.d.ts +0 -9
- package/dist/src/semantics/platform.js +0 -206
- package/dist/src/semantics/renewals.d.ts +0 -47
- package/dist/src/semantics/renewals.js +0 -281
- package/dist/src/semantics/setup-intents.d.ts +0 -2
- package/dist/src/semantics/subscription-schedules.d.ts +0 -2
- package/dist/src/semantics/subscription-schedules.js +0 -119
- package/dist/src/semantics/tax.d.ts +0 -2
- package/dist/src/semantics/tax.js +0 -197
- package/dist/src/semantics/terminal.d.ts +0 -5
- package/dist/src/semantics/terminal.js +0 -182
- package/dist/src/semantics/test-clocks.d.ts +0 -6
- package/dist/src/semantics/tokens.d.ts +0 -4
- package/dist/src/semantics/tokens.js +0 -44
- package/dist/src/semantics/treasury.d.ts +0 -2
- package/dist/src/semantics/treasury.js +0 -377
- package/dist/src/semantics/webhook-endpoints.d.ts +0 -3
- package/dist/src/semantics/webhook-endpoints.js +0 -85
- package/dist/src/stripe-budget.d.ts +0 -55
- package/dist/src/stripe-budget.js +0 -155
- package/dist/src/stripe-capabilities.d.ts +0 -3
- package/dist/src/stripe-capabilities.js +0 -5836
- package/dist/src/stripe-conformance.d.ts +0 -43
- package/dist/src/stripe-conformance.js +0 -105
- package/dist/src/stripe-connector.d.ts +0 -161
- package/dist/src/stripe-connector.js +0 -414
- package/dist/src/stripe-emit.d.ts +0 -2
- package/dist/src/stripe-emit.js +0 -145
- package/dist/src/stripe-events.d.ts +0 -93
- package/dist/src/stripe-events.js +0 -392
- package/dist/src/stripe-js.d.ts +0 -4
- package/dist/src/stripe-js.js +0 -70
- package/dist/src/stripe-mirror-ui.d.ts +0 -15
- package/dist/src/stripe-mirror-ui.js +0 -87
- package/dist/src/stripe-perform-harness.d.ts +0 -9
- package/dist/src/stripe-perform-harness.js +0 -26
- package/dist/src/stripe-server.d.ts +0 -33
- package/dist/src/stripe-server.js +0 -429
- package/dist/src/stripe-shared.d.ts +0 -109
- package/dist/src/stripe-shared.js +0 -276
- package/dist/src/stripe-ui-conformance.d.ts +0 -5
- package/dist/src/stripe-ui-conformance.js +0 -79
- package/dist/src/stripe-ui-structure.d.ts +0 -3
- package/dist/src/stripe-ui-structure.js +0 -168
- package/dist/test-fixtures/stripe-known-deviations.json +0 -110
- package/dist/test-fixtures/stripe-openapi-operations.SOURCE.md +0 -14
- package/dist/test-fixtures/stripe-openapi-operations.json +0 -4717
- package/dist/test-fixtures/stripe-schemas.SOURCE.md +0 -35
- package/dist/test-fixtures/stripe-schemas.json +0 -3813
- package/src/screens/connect-settings.tsx +0 -121
- package/src/screens/consent-skin.ts +0 -20
- package/src/screens/financial-connections.tsx +0 -101
- package/src/screens/identity.tsx +0 -96
- package/src/screens/public-details.tsx +0 -105
- package/src/semantics/after-payment.ts +0 -118
- package/src/semantics/billing.ts +0 -216
- package/src/semantics/connect.ts +0 -487
- package/src/semantics/credit-notes.ts +0 -168
- package/src/semantics/ephemeral-keys.ts +0 -34
- package/src/semantics/files.ts +0 -143
- package/src/semantics/issuing.ts +0 -590
- package/src/semantics/ledger.ts +0 -253
- package/src/semantics/payment-intents.ts +0 -463
- package/src/semantics/payment-links.ts +0 -148
- package/src/semantics/payment-methods.ts +0 -145
- package/src/semantics/plans.ts +0 -131
- package/src/semantics/platform.ts +0 -220
- package/src/semantics/renewals.ts +0 -279
- package/src/semantics/setup-intents.ts +0 -87
- package/src/semantics/subscription-schedules.ts +0 -129
- package/src/semantics/tax.ts +0 -220
- package/src/semantics/terminal.ts +0 -195
- package/src/semantics/tokens.ts +0 -52
- package/src/semantics/treasury.ts +0 -383
- package/src/semantics/webhook-endpoints.ts +0 -87
- package/src/stripe-budget.ts +0 -181
- package/src/stripe-capabilities.ts +0 -5651
- package/src/stripe-conformance.ts +0 -124
- package/src/stripe-connector.ts +0 -462
- package/src/stripe-emit.ts +0 -157
- package/src/stripe-events.ts +0 -434
- package/src/stripe-js.ts +0 -70
- package/src/stripe-mirror-ui.ts +0 -90
- package/src/stripe-perform-harness.ts +0 -29
- package/src/stripe-server.ts +0 -393
- package/src/stripe-shared.ts +0 -297
- package/src/stripe-ui-conformance.ts +0 -88
- package/src/stripe-ui-structure.ts +0 -182
- package/test-fixtures/stripe-known-deviations.json +0 -110
- package/test-fixtures/stripe-openapi-operations.json +0 -4717
- package/test-fixtures/stripe-schemas.json +0 -3813
- /package/dist/src/{screens → engine}/industries.d.ts +0 -0
- /package/dist/src/{screens → engine}/industries.js +0 -0
- /package/dist/src/{semantics → engine}/test-cards.d.ts +0 -0
- /package/dist/src/{semantics → engine}/test-cards.js +0 -0
- /package/dist/src/{stripe-version.d.ts → engine/version.d.ts} +0 -0
- /package/src/{screens → engine}/industries.ts +0 -0
- /package/src/{semantics → engine}/test-cards.ts +0 -0
|
@@ -1,34 +1,51 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
asBool,
|
|
10
|
-
buildDiscount,
|
|
11
|
-
buildSubscriptionItemsList,
|
|
12
|
-
cardError,
|
|
13
|
-
declineFor,
|
|
14
|
-
mintClientSecret,
|
|
15
|
-
priceAmount,
|
|
16
|
-
resolveSubscriptionBillingInterval,
|
|
17
|
-
resolveTrial,
|
|
18
|
-
subscriptionCouponParam,
|
|
19
|
-
subscriptionHasPrice,
|
|
20
|
-
subscriptionItemEntries,
|
|
21
|
-
subscriptionItemPrice,
|
|
22
|
-
} from '../stripe-twin.ts';
|
|
23
|
-
import { finalizedFields } from './invoices.ts';
|
|
24
|
-
import { mintCharge } from './payment-intents.ts';
|
|
25
|
-
import { advanceBilling, clockNow, collectSubscriptionInvoice, draftSubscriptionInvoice, itemQuantity, keptAfter, payerOf, spendOnceCoupon } from './renewals.ts';
|
|
26
|
-
import { at, created, deletedDiscount, expanded, fail, finder, list, newest, redeemCoupon, search, send, where, type Row } from './shared.ts';
|
|
1
|
+
// Stripe's subscriptions 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 { 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';
|
|
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';
|
|
27
9
|
|
|
28
|
-
|
|
29
|
-
|
|
10
|
+
// canceling keeps the subscription, now canceled, stamped with when it was canceled and ended
|
|
11
|
+
// (https://docs.stripe.com/api/subscriptions/object#subscription_object-canceled_at, #subscription_object-ended_at)
|
|
12
|
+
export async function DeleteSubscriptionsSubscriptionExposedId(ctx: HandlerContext): Promise<Response> {
|
|
13
|
+
const id = at(ctx, 'subscription_exposed_id');
|
|
14
|
+
const sub = ctx.get(SUB, id);
|
|
15
|
+
if (!sub) return subMissing(ctx, id);
|
|
16
|
+
const refused = ctx.legal(SUB, 'status', 'DeleteSubscriptionsSubscriptionExposedId', sub.status, undefined, id);
|
|
17
|
+
if (refused) return ctx.refuse(refused);
|
|
18
|
+
const now = nowUnix(ctx.occurredAt);
|
|
19
|
+
// why it was canceled: the request's `cancellation_details` ("Details about why this subscription was cancelled"), and
|
|
20
|
+
// the reason Stripe records for a cancel asked through the API, as the cancel page's example answers it:
|
|
21
|
+
// `"reason": "cancellation_requested"` (docs.stripe.com/api/subscriptions/cancel)
|
|
22
|
+
const given = ctx.params.cancellation_details && typeof ctx.params.cancellation_details === 'object' ? (ctx.params.cancellation_details as Row) : {};
|
|
23
|
+
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
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function GetSubscriptions(ctx: HandlerContext): Promise<Response> {
|
|
28
|
+
await advanceBilling(ctx);
|
|
29
|
+
return list(ctx, SUB, where(ctx, newest(ctx, SUB), {
|
|
30
|
+
customer: (s, v) => s.customer === v,
|
|
31
|
+
status: (s, v) => (v === 'all' ? true : s.status === v),
|
|
32
|
+
price: (s, v) => subscriptionHasPrice(s, String(v)),
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function GetSubscriptionsSearch(ctx: HandlerContext): Promise<Response> {
|
|
37
|
+
await advanceBilling(ctx);
|
|
38
|
+
return search(ctx, SUB);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function GetSubscriptionsSubscriptionExposedId(ctx: HandlerContext): Promise<Response> {
|
|
42
|
+
const id = at(ctx, 'subscription_exposed_id');
|
|
43
|
+
await advanceBilling(ctx, (sub) => sub.id === id);
|
|
44
|
+
const sub = ctx.get(SUB, id);
|
|
45
|
+
return sub ? ctx.reply(expanded(ctx, SUB, sub)) : subMissing(ctx, id);
|
|
46
|
+
}
|
|
30
47
|
|
|
31
|
-
|
|
48
|
+
export async function PostSubscriptions(ctx: HandlerContext): Promise<Response> {
|
|
32
49
|
const params = ctx.params;
|
|
33
50
|
const customer = typeof params.customer === 'string' ? params.customer : '';
|
|
34
51
|
if (!customer) return fail(ctx, 'Missing required param: customer.', 400, 'parameter_missing');
|
|
@@ -146,7 +163,7 @@ const create: Semantics = async (ctx) => {
|
|
|
146
163
|
if (piId) {
|
|
147
164
|
// paid by the default payment method, or waiting on one (declined, absent, or collected client-side)
|
|
148
165
|
await created(ctx, 'payment_intent', { id: piId, amount: invoiceTotal, currency, customer, invoice: invoiceId, ...(payWith ? { payment_method: pays ? payWith : null } : {}) }, {
|
|
149
|
-
status: pays ? 'succeeded' : 'requires_payment_method', client_secret: mintClientSecret(piId), livemode: false,
|
|
166
|
+
status: pays ? 'succeeded' : 'requires_payment_method', client_secret: mintClientSecret(piId, await ctx.secret(`client-secret:${piId}`)), livemode: false,
|
|
150
167
|
capture_method: 'automatic', amount_capturable: 0, amount_received: pays ? invoiceTotal : 0, next_action: null,
|
|
151
168
|
latest_charge: chargeId,
|
|
152
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,
|
|
@@ -156,194 +173,9 @@ const create: Semantics = async (ctx) => {
|
|
|
156
173
|
// the first invoice is finalized: the `once` coupon it took is spent (renewals.ts spendOnceCoupon)
|
|
157
174
|
await spendOnceCoupon(ctx, ctx.get('invoice', invoiceId) ?? {});
|
|
158
175
|
return ctx.reply(expanded(ctx, SUB, ctx.get(SUB, subId) ?? sub));
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
const listSubscriptions: Semantics = async (ctx) => {
|
|
162
|
-
await advanceBilling(ctx);
|
|
163
|
-
return list(ctx, SUB, where(ctx, newest(ctx, SUB), {
|
|
164
|
-
customer: (s, v) => s.customer === v,
|
|
165
|
-
status: (s, v) => (v === 'all' ? true : s.status === v),
|
|
166
|
-
price: (s, v) => subscriptionHasPrice(s, String(v)),
|
|
167
|
-
}));
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
const searchSubscriptions: Semantics = async (ctx) => {
|
|
171
|
-
await advanceBilling(ctx);
|
|
172
|
-
return search(ctx, SUB);
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
const retrieveSubscription: Semantics = async (ctx) => {
|
|
176
|
-
const id = at(ctx, 'subscription_exposed_id');
|
|
177
|
-
await advanceBilling(ctx, (sub) => sub.id === id);
|
|
178
|
-
const sub = ctx.get(SUB, id);
|
|
179
|
-
return sub ? ctx.reply(expanded(ctx, SUB, sub)) : subMissing(ctx, id);
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
// pause_collection pauses billing (the subscription stays active; the paused event fires) and
|
|
183
|
-
// clearing it resumes; cancel_at_period_end schedules or withdraws a cancel; a coupon replaces the
|
|
184
|
-
// discount and an empty one clears it
|
|
185
|
-
/** pause_collection pauses billing (the paused event fires) and clearing it resumes
|
|
186
|
-
* (docs.stripe.com/billing/subscriptions/pause-payment); answers the event, or nothing for a value it does not take. */
|
|
187
|
-
function pauseCollection(pc: unknown, fields: Row): string | undefined {
|
|
188
|
-
const isClear = pc === '' || pc === null || (typeof pc === 'object' && pc !== null && Object.keys(pc as object).length === 0);
|
|
189
|
-
if (isClear) {
|
|
190
|
-
fields.pause_collection = null;
|
|
191
|
-
return 'customer.subscription.resumed';
|
|
192
|
-
}
|
|
193
|
-
if (!pc || typeof pc !== 'object') return undefined;
|
|
194
|
-
const p = pc as Row;
|
|
195
|
-
fields.pause_collection = { behavior: typeof p.behavior === 'string' ? p.behavior : 'void', resumes_at: p.resumes_at !== undefined ? Number(p.resumes_at) : null };
|
|
196
|
-
return 'customer.subscription.paused';
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/** A coupon that replaces the subscription's discount, or an empty one that clears it (docs.stripe.com/api/subscriptions/update#update_subscription-discounts). */
|
|
200
|
-
function replacedDiscounts(ctx: SemanticsContext, couponId: string, sub: Row): Row[] | Response {
|
|
201
|
-
if (couponId === '') return [];
|
|
202
|
-
const coupon = ctx.get('coupon', couponId);
|
|
203
|
-
return coupon ? [buildDiscount(coupon, String(sub.customer ?? ''), Number(ctx.now()), { subscription: String(sub.id) })] : fail(ctx, `No such coupon: '${couponId}'`, 400, 'resource_missing');
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/** The longest trial Stripe takes: `trial_end` "Can be at most two years from `billing_cycle_anchor`"
|
|
207
|
-
* (docs.stripe.com/api/subscriptions/update#update_subscription-trial_end); "The trial period must be 730 days (2 years)
|
|
208
|
-
* or less" (docs.stripe.com/billing/subscriptions/trials/free-trials). */
|
|
209
|
-
const MAX_TRIAL = 730 * 86400;
|
|
210
|
-
|
|
211
|
-
/** A `trial_end` Stripe refuses: the parameter is `"now" | timestamp` (docs.stripe.com/api/subscriptions/update), and a
|
|
212
|
-
* timestamp that is not in the future answers "Invalid timestamp: must be an integer Unix timestamp in the future."
|
|
213
|
-
* (Stripe's own message, as its SDKs' users report it: github.com/stripe/stripe-dotnet/issues/805,
|
|
214
|
-
* github.com/stripe/stripe-ruby/issues/682). Where the documentation stops and the twin decides: a value that is no
|
|
215
|
-
* number at all, and one more than two years on, answer that same message. */
|
|
216
|
-
export function trialEndRefused(ctx: SemanticsContext, value: unknown, now: number): Response | undefined {
|
|
217
|
-
const t = typeof value === 'number' ? value : typeof value === 'string' && /^\d+$/.test(value.trim()) ? Number(value) : NaN;
|
|
218
|
-
return value === undefined || value === 'now' || (Number.isInteger(t) && t > now && t <= now + MAX_TRIAL) ? undefined : invalidTrialEnd(ctx);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function invalidTrialEnd(ctx: SemanticsContext): Response {
|
|
222
|
-
return ctx.refuse({ status: 400, param: 'trial_end', message: 'Invalid timestamp: must be an integer Unix timestamp in the future.' });
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/** An update's `trial_end` on the subscription, into `fields`.
|
|
226
|
-
*
|
|
227
|
-
* `now` on a trialing subscription ends its trial at once: "The special value `now` can be provided to end the customer's
|
|
228
|
-
* trial immediately" (docs.stripe.com/api/subscriptions/update#update_subscription-trial_end; the trials guide's "To end a
|
|
229
|
-
* trial early … setting the `trial_end` value to … **now** to end immediately", docs.stripe.com/billing/subscriptions/
|
|
230
|
-
* trials/free-trials). The update resets the billing date and charges at once: "Switching prices does not normally change
|
|
231
|
-
* the billing date or generate an immediate charge unless … A trial starts or ends. In these cases, we … immediately
|
|
232
|
-
* charge the customer using the new price, and reset the billing date" (docs.stripe.com/api/subscriptions/update), and
|
|
233
|
-
* "Stripe immediately attempts payment when a subscription's billing cycle anchor is reset … When billing is performed
|
|
234
|
-
* immediately, but the required payment fails, the subscription change request succeeds and the subscription transitions
|
|
235
|
-
* to `past_due`" (docs.stripe.com/billing/subscriptions/upgrade-downgrade#immediate-payment). So a new period starts now,
|
|
236
|
-
* its invoice is made (`invoice.created`) and charged to the subscription's default payment method, else the
|
|
237
|
-
* customer's (paid: `invoice.paid`, the subscription `active`; declined or none: the invoice open, `past_due`), and the
|
|
238
|
-
* subscription's own write sends `customer.subscription.updated`. `payment_behavior` (the same page): `error_if_incomplete`
|
|
239
|
-
* answers 402 and leaves the subscription as it was, `default_incomplete` leaves the invoice open without an attempt.
|
|
240
|
-
*
|
|
241
|
-
* A future timestamp moves the trial's end, and the period and billing anchor with it: "The `billing_cycle_anchor` will be
|
|
242
|
-
* updated to the `trial_end` value" (the same parameter); on a subscription out of its trial it starts a new one ("You can
|
|
243
|
-
* add a new trial on a non-trialing subscription by updating the subscription while specifying trial_end", and for
|
|
244
|
-
* classic billing mode "the `trial_start` field remains set to the start of the first trial", docs.stripe.com/billing/
|
|
245
|
-
* subscriptions/trials/free-trials).
|
|
246
|
-
*
|
|
247
|
-
* Where the documentation stops and the twin decides: the invoice's billing_reason is `subscription_update` ("A
|
|
248
|
-
* subscription was updated", docs.stripe.com/api/invoices/object#invoice_object-billing_reason); `now` on a subscription
|
|
249
|
-
* that is not trialing has no trial to end and changes nothing; the credit Stripe prorates for the unused time when a new
|
|
250
|
-
* trial starts mid-period is not made (as `proration_behavior=none` would have it); `pending_if_incomplete` answers as
|
|
251
|
-
* `allow_incomplete` (no pending update is modelled). Answers a refusal, or the invoice to collect once the subscription is written. */
|
|
252
|
-
async function applyTrialEnd(ctx: SemanticsContext, sub: Row, fields: Row, now: number): Promise<{ invoice: string; payer?: string; attempt: boolean } | undefined> {
|
|
253
|
-
const value = ctx.params.trial_end;
|
|
254
|
-
delete fields.trial_end;
|
|
255
|
-
const id = String(sub.id);
|
|
256
|
-
if (value !== 'now' || sub.status !== 'trialing') return value === 'now' ? undefined : movedTrial(sub, fields, Number(value), now);
|
|
257
|
-
// the period's invoice bills the items as this update leaves them, under its discount
|
|
258
|
-
const next: Row = { ...sub, ...fields, items: ctx.get(SUB, id)?.items ?? sub.items };
|
|
259
|
-
const { interval, interval_count } = resolveSubscriptionBillingInterval(((next.items as { data?: Row[] } | undefined)?.data) ?? [], finder(ctx));
|
|
260
|
-
const period = { start: now, end: addBillingInterval(now, interval, interval_count) };
|
|
261
|
-
const invoice = await draftSubscriptionInvoice(ctx, next, { ...period, reason: 'subscription_update' });
|
|
262
|
-
Object.assign(fields, { trial_end: now, billing_cycle_anchor: now, current_period_start: period.start, current_period_end: period.end, latest_invoice: invoice });
|
|
263
|
-
const payer = trialPayer(ctx, sub);
|
|
264
|
-
return { invoice, ...(payer ? { payer } : {}), attempt: ctx.params.payment_behavior !== 'default_incomplete' };
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/** A trial moved to end at `end`, or a new one to `end` on a subscription out of its trial (applyTrialEnd). */
|
|
268
|
-
function movedTrial(sub: Row, fields: Row, end: number, now: number): undefined {
|
|
269
|
-
Object.assign(fields, {
|
|
270
|
-
status: 'trialing', trial_end: end, trial_start: sub.trial_start ?? now, billing_cycle_anchor: end,
|
|
271
|
-
current_period_end: end, ...(sub.status !== 'trialing' ? { current_period_start: now } : {}),
|
|
272
|
-
});
|
|
273
|
-
return undefined;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/** What ending the trial charges: the update's own default payment method, else the subscription's, else its customer's. */
|
|
277
|
-
function trialPayer(ctx: SemanticsContext, sub: Row): string | undefined {
|
|
278
|
-
const given = ctx.params.default_payment_method;
|
|
279
|
-
return typeof given === 'string' && given ? given : payerOf(ctx, sub);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/** What an update's `trial_end` refuses before anything is written: a new trial the machine does not allow from the
|
|
283
|
-
* subscription's status, or, under `error_if_incomplete`, ending a trial that cannot be paid at once ("If payment fails,
|
|
284
|
-
* return an HTTP `402` status code and don't update the subscription", docs.stripe.com/api/subscriptions/update). */
|
|
285
|
-
function trialEndBlocked(ctx: SemanticsContext, sub: Row, discounts: Row[] | undefined): Response | undefined {
|
|
286
|
-
if (ctx.params.trial_end !== 'now') return newTrialRefused(ctx, sub);
|
|
287
|
-
if (sub.status !== 'trialing') return undefined;
|
|
288
|
-
const charge = trialEndCharge(ctx, sub, discounts);
|
|
289
|
-
if (ctx.params.payment_behavior === 'error_if_incomplete' && charge.due > 0 && !charge.pays) return firstPaymentRefused(ctx, charge.declined);
|
|
290
|
-
// the move ending the trial makes (paid or nothing due: active; else past_due) is the machine's, asked before
|
|
291
|
-
// anything is written
|
|
292
|
-
const refused = ctx.legal(SUB, 'status', ctx.call.operation.id, 'trialing', charge.due <= 0 || charge.pays ? 'active' : 'past_due', String(sub.id));
|
|
293
|
-
return refused ? ctx.refuse(refused) : undefined;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/** A price named by id, or given as an object. */
|
|
297
|
-
function priceRow(ctx: SemanticsContext, price: unknown): Row | undefined {
|
|
298
|
-
return typeof price === 'string' ? ctx.get('price', price) : price && typeof price === 'object' ? (price as Row) : undefined;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/** One period of the subscription's items as an update's `items` would leave them, before any discount: an entry naming
|
|
302
|
-
* an item changes its price and quantity (a new price resets the quantity to 1 unless one is given) or deletes it, and
|
|
303
|
-
* one naming none adds an item (applyItemChanges). */
|
|
304
|
-
function dueAfter(ctx: SemanticsContext, sub: Row, entries: Row[]): number {
|
|
305
|
-
const lines = new Map<string, { price: Row | undefined; quantity: number }>();
|
|
306
|
-
for (const it of ((sub.items as { data?: Row[] } | undefined)?.data) ?? []) lines.set(String(it.id), { price: priceRow(ctx, it.price), quantity: itemQuantity(it) });
|
|
307
|
-
entries.forEach((e, i) => entryAfter(ctx, lines, e, i));
|
|
308
|
-
return [...lines.values()].reduce((sum, l) => sum + priceAmount(l.price, l.quantity), 0);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/** One entry of an update's `items` on the lines it would leave (dueAfter). */
|
|
312
|
-
function entryAfter(ctx: SemanticsContext, lines: Map<string, { price: Row | undefined; quantity: number }>, e: Row, i: number): void {
|
|
313
|
-
// an inline price bills as the Price inlinePrice makes of it
|
|
314
|
-
const d = e.price_data && typeof e.price_data === 'object' ? (e.price_data as Row) : undefined;
|
|
315
|
-
const inline: Row | undefined = d ? (d.unit_amount_decimal !== undefined ? { unit_amount_decimal: String(d.unit_amount_decimal) } : { unit_amount: Math.trunc(Number(d.unit_amount) || 0) }) : undefined;
|
|
316
|
-
const price = typeof e.price === 'string' && e.price ? priceRow(ctx, e.price) : inline;
|
|
317
|
-
const named = typeof e.id === 'string' && e.id;
|
|
318
|
-
const key = named ? String(e.id) : `new_${i}`;
|
|
319
|
-
// only a named item is deleted; an entry naming none adds one, as applyItemChanges does
|
|
320
|
-
if (named && asBool(e.deleted)) { lines.delete(key); return; }
|
|
321
|
-
const was = lines.get(key);
|
|
322
|
-
const quantity = e.quantity !== undefined ? Math.max(0, Math.trunc(Number(e.quantity) || 0)) : price && was && price.id !== was.price?.id ? 1 : was?.quantity ?? 1;
|
|
323
|
-
lines.set(key, { price: price ?? was?.price, quantity });
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/** A new trial on a subscription whose status the machine does not let start one. */
|
|
327
|
-
function newTrialRefused(ctx: SemanticsContext, sub: Row): Response | undefined {
|
|
328
|
-
const refused = sub.status === 'trialing' ? undefined : ctx.legal(SUB, 'status', ctx.call.operation.id, sub.status, 'trialing', String(sub.id));
|
|
329
|
-
return refused ? ctx.refuse(refused) : undefined;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/** What ending the trial charges, judged before anything is written: the period of the items as this update leaves them
|
|
333
|
-
* (dueAfter) under the discount it leaves, and whether the payer pays it (an attempt made, a payer, no decline). */
|
|
334
|
-
function trialEndCharge(ctx: SemanticsContext, sub: Row, discounts: Row[] | undefined): { due: number; pays: boolean; declined?: ReturnType<typeof declineFor> } {
|
|
335
|
-
const subtotal = dueAfter(ctx, sub, subscriptionItemEntries(ctx.params.items));
|
|
336
|
-
// the discount the subscription carries after this update takes this invoice (renewals.ts draftSubscriptionInvoice)
|
|
337
|
-
const discount = (discounts ?? (sub.discounts as Row[] | undefined) ?? [])[0];
|
|
338
|
-
const coupon = discount && typeof discount === 'object' ? (discount.coupon as Row | undefined) : undefined;
|
|
339
|
-
const due = subtotal - (coupon ? applyCouponDiscount(subtotal, coupon) : 0);
|
|
340
|
-
const payer = trialPayer(ctx, sub);
|
|
341
|
-
const attempt = ctx.params.payment_behavior !== 'default_incomplete';
|
|
342
|
-
const declined = payer && attempt ? declineFor(finder(ctx), { payment_method: payer }) : undefined;
|
|
343
|
-
return { due, pays: attempt && !!payer && !declined, ...(declined ? { declined } : {}) };
|
|
344
176
|
}
|
|
345
177
|
|
|
346
|
-
|
|
178
|
+
export async function PostSubscriptionsSubscriptionExposedId(ctx: HandlerContext): Promise<Response> {
|
|
347
179
|
const id = at(ctx, 'subscription_exposed_id');
|
|
348
180
|
// a period that has already ended renews (or a trial that has, ends) before the update applies (renewals.ts)
|
|
349
181
|
await advanceBilling(ctx, (s) => s.id === id);
|
|
@@ -381,7 +213,7 @@ const update: Semantics = async (ctx) => {
|
|
|
381
213
|
// scheduled cancel follows a period this update moves
|
|
382
214
|
if ('cancel_at_period_end' in params) {
|
|
383
215
|
fields.cancel_at_period_end = asBool(params.cancel_at_period_end);
|
|
384
|
-
Object.assign(fields, fields.cancel_at_period_end ? { cancel_at: fields.current_period_end ?? sub.current_period_end ?? null, canceled_at: Number(ctx.
|
|
216
|
+
Object.assign(fields, fields.cancel_at_period_end ? { cancel_at: fields.current_period_end ?? sub.current_period_end ?? null, canceled_at: Number(nowUnix(ctx.occurredAt)) } : { cancel_at: null, canceled_at: null });
|
|
385
217
|
} else if (sub.cancel_at_period_end === true && fields.current_period_end !== undefined) fields.cancel_at = fields.current_period_end;
|
|
386
218
|
if (trial?.invoice) {
|
|
387
219
|
// the trial's end is charged at once: paid (or nothing due), the subscription is active; not, past_due
|
|
@@ -392,219 +224,4 @@ const update: Semantics = async (ctx) => {
|
|
|
392
224
|
if (fields.discounts !== undefined) fields.discounts = keptAfter(fields.discounts as Row[], ctx.get('invoice', trial.invoice) ?? {});
|
|
393
225
|
}
|
|
394
226
|
return ctx.reply(await ctx.write(SUB, id, fields, op));
|
|
395
|
-
};
|
|
396
|
-
|
|
397
|
-
/** A new item named with neither `price` nor `price_data`: "One of `price` or `price_data` is required"
|
|
398
|
-
* (docs.stripe.com/api/subscriptions/update). */
|
|
399
|
-
function noItemPrice(ctx: SemanticsContext): Response {
|
|
400
|
-
return fail(ctx, 'Missing required param: items[price].', 400, 'parameter_missing');
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
/** A subscription asked to fail when its first invoice cannot be paid at once (payment_behavior=error_if_incomplete):
|
|
404
|
-
* the card's decline, or no payment method to charge (docs.stripe.com/api/subscriptions/create#create_subscription-payment_behavior). */
|
|
405
|
-
function firstPaymentRefused(ctx: SemanticsContext, declined: Parameters<typeof cardError>[0] | null | undefined): Response {
|
|
406
|
-
return declined ? send(ctx, cardError(declined)) : fail(ctx, 'This customer has no attached payment source or default payment method.', 400, 'resource_missing');
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/** An update's `items`: "A list of up to 20 subscription items, each with an attached price"
|
|
410
|
-
* (docs.stripe.com/api/subscriptions/update): an entry naming an item's `id` changes that item (its price, quantity,
|
|
411
|
-
* metadata) or, with `deleted` ("A flag that, if set to `true`, will delete the specified item"), removes it; "If you
|
|
412
|
-
* omit `id`, the API adds a new subscription item rather than updating the existing one". Each takes `price` or
|
|
413
|
-
* `price_data`, "Data used to generate a new Price object inline. One of `price` or `price_data` is required", and
|
|
414
|
-
* "When changing a subscription item's price, `quantity` is set to 1 unless a `quantity` parameter is provided". An
|
|
415
|
-
* inline price is made as the manage-prices guide says: "By default, prices created with `price_data` are effectively
|
|
416
|
-
* archived (they're marked as `active=false`)" (docs.stripe.com/products-prices/manage-prices).
|
|
417
|
-
* Where the documentation stops and the twin decides: the update is all or nothing, every entry checked before any is
|
|
418
|
-
* written (Stripe answers one error for the request); the refusals are worded by the twin. */
|
|
419
|
-
async function applyItemChanges(ctx: SemanticsContext, subId: string, entries: Row[]): Promise<Response | undefined> {
|
|
420
|
-
// every entry is checked first: a refusal leaves the subscription as it was
|
|
421
|
-
const refused = itemChangesRefused(ctx, subId, entries);
|
|
422
|
-
if (refused) return refused;
|
|
423
|
-
return writeItemChanges(ctx, subId, entries);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
/** The refusal of an update's `items`, checked entry by entry before any is written (applyItemChanges). */
|
|
427
|
-
function itemChangesRefused(ctx: SemanticsContext, subId: string, entries: Row[]): Response | undefined {
|
|
428
|
-
for (const entry of entries) {
|
|
429
|
-
const priceRef = typeof entry.price === 'string' && entry.price ? entry.price : undefined;
|
|
430
|
-
if (priceRef && !ctx.get('price', priceRef)) return fail(ctx, `No such price: '${priceRef}'`, 400, 'resource_missing');
|
|
431
|
-
const inline = !priceRef && entry.price_data && typeof entry.price_data === 'object' ? (entry.price_data as Row) : undefined;
|
|
432
|
-
if (inline) {
|
|
433
|
-
const refused = refuseInlinePrice(ctx, inline);
|
|
434
|
-
if (refused) return refused;
|
|
435
|
-
}
|
|
436
|
-
if (typeof entry.id === 'string' && entry.id) {
|
|
437
|
-
const si = ctx.get('subscription_item', entry.id);
|
|
438
|
-
if (!si || si.subscription !== subId) return fail(ctx, `No such subscription_item: '${entry.id}'`, 400, 'resource_missing');
|
|
439
|
-
} else if (!priceRef && !inline) return noItemPrice(ctx);
|
|
440
|
-
}
|
|
441
|
-
return undefined;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
/** An update's checked `items`, written. */
|
|
445
|
-
async function writeItemChanges(ctx: SemanticsContext, subId: string, entries: Row[]): Promise<undefined> {
|
|
446
|
-
for (const entry of entries) {
|
|
447
|
-
const inline = typeof entry.price === 'string' && entry.price ? undefined : entry.price_data && typeof entry.price_data === 'object' ? (entry.price_data as Row) : undefined;
|
|
448
|
-
const priceRef = typeof entry.price === 'string' && entry.price ? entry.price : inline ? await inlinePrice(ctx, inline) : undefined;
|
|
449
|
-
// only a price that changes resets the quantity; one naming the item's own price leaves it
|
|
450
|
-
const was = typeof entry.id === 'string' && entry.id ? ctx.get('subscription_item', entry.id)?.price : undefined;
|
|
451
|
-
const wasId = typeof was === 'string' ? was : was && typeof was === 'object' ? String((was as Row).id ?? '') : undefined;
|
|
452
|
-
const quantity = entry.quantity !== undefined ? { quantity: Math.max(0, Math.trunc(Number(entry.quantity) || 0)) } : priceRef && priceRef !== wasId ? { quantity: 1 } : {};
|
|
453
|
-
const metadata = entry.metadata && typeof entry.metadata === 'object' ? { metadata: entry.metadata } : {};
|
|
454
|
-
if (typeof entry.id === 'string' && entry.id) {
|
|
455
|
-
if (asBool(entry.deleted)) { await ctx.write('subscription_item', entry.id, { deleted: true }, 'subscription_item.delete'); continue; }
|
|
456
|
-
await ctx.write('subscription_item', entry.id, { ...(priceRef ? subscriptionItemPrice(priceRef, finder(ctx)) : {}), ...quantity, ...metadata }, 'subscription_item.update');
|
|
457
|
-
} else {
|
|
458
|
-
await created(ctx, 'subscription_item', { subscription: subId, ...subscriptionItemPrice(priceRef, finder(ctx)), quantity: 1, ...quantity, ...metadata }, { livemode: false, metadata: {}, discounts: [], tax_rates: [], billing_thresholds: null });
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
await syncItems(ctx, subId);
|
|
462
|
-
return undefined;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
/** A subscription item's `price_data` Stripe would refuse: its required `currency`, `product` (an existing one) and
|
|
466
|
-
* `recurring[interval]`, and "Only one of `unit_amount` and `unit_amount_decimal` can be set". */
|
|
467
|
-
function refuseInlinePrice(ctx: SemanticsContext, d: Row): Response | undefined {
|
|
468
|
-
for (const name of ['currency', 'product']) if (typeof d[name] !== 'string' || !d[name]) return fail(ctx, `Missing required param: items[price_data][${name}].`, 400, 'parameter_missing');
|
|
469
|
-
if (!ctx.get('product', String(d.product))) return fail(ctx, `No such product: '${String(d.product)}'`, 400, 'resource_missing');
|
|
470
|
-
const interval = (d.recurring as Row | undefined)?.interval;
|
|
471
|
-
if (!['day', 'week', 'month', 'year'].includes(String(interval))) return fail(ctx, 'Missing required param: items[price_data][recurring][interval].', 400, 'parameter_missing');
|
|
472
|
-
if (d.unit_amount !== undefined && d.unit_amount_decimal !== undefined) return fail(ctx, 'Only one of `unit_amount` and `unit_amount_decimal` can be set.', 400, 'parameter_invalid');
|
|
473
|
-
return undefined;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
/** The inline Price a `price_data` generates: recurring, archived (active=false). Answers its id. */
|
|
477
|
-
async function inlinePrice(ctx: SemanticsContext, d: Row): Promise<string> {
|
|
478
|
-
const r = d.recurring as Row;
|
|
479
|
-
const decimal = d.unit_amount_decimal !== undefined ? String(d.unit_amount_decimal) : String(Math.trunc(Number(d.unit_amount) || 0));
|
|
480
|
-
const price = await created(ctx, 'price', {
|
|
481
|
-
product: d.product, currency: String(d.currency).toLowerCase(),
|
|
482
|
-
// a decimal amount that is not a whole number of the smallest unit has no integer unit_amount (as semantics/plans.ts)
|
|
483
|
-
unit_amount: d.unit_amount_decimal !== undefined ? (Number.isInteger(Number(d.unit_amount_decimal)) ? Number(d.unit_amount_decimal) : null) : Math.trunc(Number(d.unit_amount) || 0), unit_amount_decimal: decimal,
|
|
484
|
-
recurring: { interval: r.interval, interval_count: Math.max(1, Math.trunc(Number(r.interval_count ?? 1)) || 1), usage_type: 'licensed', trial_period_days: null, meter: null },
|
|
485
|
-
tax_behavior: typeof d.tax_behavior === 'string' ? d.tax_behavior : 'unspecified',
|
|
486
|
-
}, { active: false, livemode: false, billing_scheme: 'per_unit', type: 'recurring', metadata: {}, lookup_key: null, nickname: null });
|
|
487
|
-
return String(price.id);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
const deleteDiscount: Semantics = async (ctx) => {
|
|
491
|
-
const id = at(ctx, 'subscription_exposed_id');
|
|
492
|
-
const sub = ctx.get(SUB, id);
|
|
493
|
-
if (!sub) return subMissing(ctx, id);
|
|
494
|
-
const current = ((sub.discounts as unknown[] | undefined) ?? [])[0] ?? sub.discount;
|
|
495
|
-
if (!current) return fail(ctx, `No active discount for subscription: '${id}'`, 404, 'resource_missing');
|
|
496
|
-
await ctx.write(SUB, id, { discounts: [] }, 'subscription.update');
|
|
497
|
-
return ctx.reply(deletedDiscount(typeof current === 'string' ? { id: current } : (current as Row), ctx.now()));
|
|
498
|
-
};
|
|
499
|
-
|
|
500
|
-
// canceling keeps the subscription, now canceled, stamped with when it was canceled and ended
|
|
501
|
-
// (https://docs.stripe.com/api/subscriptions/object#subscription_object-canceled_at, #subscription_object-ended_at)
|
|
502
|
-
const cancel: Semantics = async (ctx) => {
|
|
503
|
-
const id = at(ctx, 'subscription_exposed_id');
|
|
504
|
-
const sub = ctx.get(SUB, id);
|
|
505
|
-
if (!sub) return subMissing(ctx, id);
|
|
506
|
-
const refused = ctx.legal(SUB, 'status', 'DeleteSubscriptionsSubscriptionExposedId', sub.status, undefined, id);
|
|
507
|
-
if (refused) return ctx.refuse(refused);
|
|
508
|
-
const now = ctx.now();
|
|
509
|
-
// why it was canceled: the request's `cancellation_details` ("Details about why this subscription was cancelled"), and
|
|
510
|
-
// the reason Stripe records for a cancel asked through the API, as the cancel page's example answers it:
|
|
511
|
-
// `"reason": "cancellation_requested"` (docs.stripe.com/api/subscriptions/cancel)
|
|
512
|
-
const given = ctx.params.cancellation_details && typeof ctx.params.cancellation_details === 'object' ? (ctx.params.cancellation_details as Row) : {};
|
|
513
|
-
const cancellation_details = { comment: given.comment ?? null, feedback: given.feedback ?? null, reason: 'cancellation_requested' };
|
|
514
|
-
return ctx.reply(await ctx.write(SUB, id, { status: 'canceled', canceled_at: now, ended_at: now, cancellation_details }, 'subscription.cancel'));
|
|
515
|
-
};
|
|
516
|
-
|
|
517
|
-
// ── items: each write keeps the parent subscription's items.data in step, rebuilt from the live items ──
|
|
518
|
-
|
|
519
|
-
/** A new subscription's items, each its own subscription_item as Stripe keeps it (/v1/subscription_items/{id}). */
|
|
520
|
-
export async function storeItems(ctx: SemanticsContext, subId: string, items: Row[]): Promise<void> {
|
|
521
|
-
for (const item of items) {
|
|
522
|
-
if (typeof item.id !== 'string' || ctx.get('subscription_item', item.id)) continue;
|
|
523
|
-
await ctx.write('subscription_item', item.id, { ...item, subscription: subId }, 'subscription_item.create');
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
async function syncItems(ctx: SemanticsContext, subId: string): Promise<void> {
|
|
528
|
-
if (!subId) return;
|
|
529
|
-
const items = ctx.rows('subscription_item').filter((r) => r.subscription === subId);
|
|
530
|
-
await ctx.write(SUB, subId, { items: { object: 'list', data: items, has_more: false, total_count: items.length, url: `/v1/subscription_items?subscription=${subId}` } }, 'subscription.items_synced');
|
|
531
227
|
}
|
|
532
|
-
|
|
533
|
-
/** A subscription item with its billing period: its subscription's (basil moved the period onto the items). */
|
|
534
|
-
const withPeriod = (ctx: SemanticsContext, si: Row): Row => {
|
|
535
|
-
const sub = ctx.get(SUB, String(si.subscription));
|
|
536
|
-
return { ...si, current_period_start: si.current_period_start ?? sub?.current_period_start ?? null, current_period_end: si.current_period_end ?? sub?.current_period_end ?? null };
|
|
537
|
-
};
|
|
538
|
-
|
|
539
|
-
const createItem: Semantics = async (ctx) => {
|
|
540
|
-
const params = ctx.params;
|
|
541
|
-
const subId = typeof params.subscription === 'string' ? params.subscription : '';
|
|
542
|
-
if (!subId) return fail(ctx, 'Missing required param: subscription.', 400, 'parameter_missing');
|
|
543
|
-
if (!ctx.get(SUB, subId)) return subMissing(ctx, subId);
|
|
544
|
-
const priceRef = typeof params.price === 'string' ? params.price : '';
|
|
545
|
-
if (!priceRef && !(params.price_data && typeof params.price_data === 'object')) return fail(ctx, 'Missing required param: price.', 400, 'parameter_missing');
|
|
546
|
-
if (priceRef && !ctx.get('price', priceRef)) return fail(ctx, `No such price: '${priceRef}'`, 400, 'resource_missing');
|
|
547
|
-
const quantity = params.quantity !== undefined ? Math.max(0, Math.trunc(Number(params.quantity) || 0)) : 1;
|
|
548
|
-
// "metadata: Set of key-value pairs that you can attach to an object" (docs.stripe.com/api/subscription_items/create)
|
|
549
|
-
const metadata = params.metadata && typeof params.metadata === 'object' ? { metadata: params.metadata } : {};
|
|
550
|
-
const body = await created(ctx, 'subscription_item', { subscription: subId, ...(priceRef ? subscriptionItemPrice(priceRef, finder(ctx)) : { price: null, plan: null }), quantity, ...metadata }, { livemode: false, metadata: {} });
|
|
551
|
-
await syncItems(ctx, subId);
|
|
552
|
-
return ctx.reply(withPeriod(ctx, body));
|
|
553
|
-
};
|
|
554
|
-
|
|
555
|
-
const listItems: Semantics = async (ctx) => {
|
|
556
|
-
const subId = typeof ctx.params.subscription === 'string' ? ctx.params.subscription : '';
|
|
557
|
-
if (!subId) return fail(ctx, 'Missing required param: subscription.', 400, 'parameter_missing');
|
|
558
|
-
if (!ctx.get(SUB, subId)) return subMissing(ctx, subId);
|
|
559
|
-
return list(ctx, 'subscription_item', newest(ctx, 'subscription_item').filter((si) => si.subscription === subId).map((si) => withPeriod(ctx, si)));
|
|
560
|
-
};
|
|
561
|
-
|
|
562
|
-
const ITEM_ATTRIBUTES = new Set(['billing_thresholds', 'discounts', 'metadata', 'price', 'quantity', 'tax_rates']);
|
|
563
|
-
|
|
564
|
-
const itemMissing = (ctx: SemanticsContext, id: string): Response => fail(ctx, `No such subscription_item: '${id}'`, 404, 'resource_missing');
|
|
565
|
-
|
|
566
|
-
const updateItem: Semantics = async (ctx) => {
|
|
567
|
-
const id = at(ctx, 'item');
|
|
568
|
-
const si = ctx.get('subscription_item', id);
|
|
569
|
-
if (!si) return itemMissing(ctx, id);
|
|
570
|
-
// only the item's own attributes are kept; the update's instructions (off_session, payment_behavior, price_data,
|
|
571
|
-
// proration_behavior, proration_date) are not attributes of the Subscription Item object
|
|
572
|
-
// (docs.stripe.com/api/subscription_items/object lists none of them)
|
|
573
|
-
const fields: Row = Object.fromEntries(Object.entries(ctx.params).filter(([k]) => ITEM_ATTRIBUTES.has(k)));
|
|
574
|
-
if (ctx.params.quantity !== undefined) fields.quantity = Math.max(0, Math.trunc(Number(ctx.params.quantity) || 0));
|
|
575
|
-
if (typeof ctx.params.price === 'string' && ctx.params.price && !ctx.get('price', ctx.params.price)) return fail(ctx, `No such price: '${ctx.params.price}'`, 400, 'resource_missing');
|
|
576
|
-
// the item carries its Price and plan, as the subscription's own items do (subscriptionItemPrice)
|
|
577
|
-
if (typeof ctx.params.price === 'string' && ctx.params.price) Object.assign(fields, subscriptionItemPrice(ctx.params.price, finder(ctx)));
|
|
578
|
-
const out = await ctx.write('subscription_item', id, fields, 'subscription_item.update');
|
|
579
|
-
await syncItems(ctx, String(si.subscription));
|
|
580
|
-
return ctx.reply(withPeriod(ctx, out));
|
|
581
|
-
};
|
|
582
|
-
|
|
583
|
-
const retrieveItem: Semantics = async (ctx) => {
|
|
584
|
-
const si = ctx.get('subscription_item', at(ctx, 'item'));
|
|
585
|
-
return si ? ctx.reply(withPeriod(ctx, si)) : itemMissing(ctx, at(ctx, 'item'));
|
|
586
|
-
};
|
|
587
|
-
|
|
588
|
-
const deleteItem: Semantics = async (ctx) => {
|
|
589
|
-
const id = at(ctx, 'item');
|
|
590
|
-
const si = ctx.get('subscription_item', id);
|
|
591
|
-
if (!si) return itemMissing(ctx, id);
|
|
592
|
-
await ctx.write('subscription_item', id, { deleted: true }, 'subscription_item.delete');
|
|
593
|
-
await syncItems(ctx, String(si.subscription));
|
|
594
|
-
return ctx.reply({ id, object: 'subscription_item', deleted: true });
|
|
595
|
-
};
|
|
596
|
-
|
|
597
|
-
export const subscriptions: Record<string, Semantics> = {
|
|
598
|
-
PostSubscriptions: create,
|
|
599
|
-
GetSubscriptions: listSubscriptions,
|
|
600
|
-
GetSubscriptionsSearch: searchSubscriptions,
|
|
601
|
-
GetSubscriptionsSubscriptionExposedId: retrieveSubscription,
|
|
602
|
-
PostSubscriptionsSubscriptionExposedId: update,
|
|
603
|
-
DeleteSubscriptionsSubscriptionExposedIdDiscount: deleteDiscount,
|
|
604
|
-
DeleteSubscriptionsSubscriptionExposedId: cancel,
|
|
605
|
-
PostSubscriptionItems: createItem,
|
|
606
|
-
GetSubscriptionItems: listItems,
|
|
607
|
-
PostSubscriptionItemsItem: updateItem,
|
|
608
|
-
GetSubscriptionItemsItem: retrieveItem,
|
|
609
|
-
DeleteSubscriptionItemsItem: deleteItem,
|
|
610
|
-
};
|
|
@@ -1,27 +1,21 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (ctx.params.frozen_time === undefined) return fail(ctx, 'Missing required param: frozen_time.', 400, 'parameter_missing');
|
|
13
|
-
const frozen = Math.trunc(Number(ctx.params.frozen_time) || 0);
|
|
14
|
-
// "Simulations are automatically deleted 30 days after you create them" (docs.stripe.com/billing/testing/test-clocks/
|
|
15
|
-
// api-advanced-usage): from its creation on the account's clock, not its frozen time
|
|
16
|
-
return ctx.reply(await created(ctx, CLOCK, { name: ctx.params.name ?? null }, { frozen_time: frozen, status: 'ready', status_details: {}, livemode: false, deletes_after: Number(ctx.now()) + 30 * 24 * 3600 }));
|
|
17
|
-
};
|
|
1
|
+
// Stripe's test_helpers 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 type { Row } from '../engine/common.ts';
|
|
5
|
+
import { AUTH, CARD, cardId, CH, historyEntry, holderId, merchantData, TXN } from '../engine/issuing.ts';
|
|
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
|
+
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';
|
|
18
12
|
|
|
19
13
|
/** A test clock deleted with what lives on it: "Deleting the simulation also deletes the test customers associated
|
|
20
14
|
* with the clock and cancels their subscriptions" (docs.stripe.com/billing/testing/test-clocks/api-advanced-usage). */
|
|
21
|
-
|
|
15
|
+
export async function DeleteTestHelpersTestClocksTestClock(ctx: HandlerContext): Promise<Response> {
|
|
22
16
|
const id = at(ctx, 'test_clock');
|
|
23
17
|
if (!ctx.get(CLOCK, id)) return fail(ctx, `No such test clock: '${id}'`, 404, 'resource_missing');
|
|
24
|
-
const now = Number(ctx.
|
|
18
|
+
const now = Number(nowUnix(ctx.occurredAt));
|
|
25
19
|
for (const c of ctx.rows('customer').filter((c) => c.test_clock === id)) {
|
|
26
20
|
for (const sub of ctx.rows('subscription').filter((s) => s.customer === c.id && s.status !== 'canceled')) {
|
|
27
21
|
ctx.legal('subscription', 'status', 'DeleteTestHelpersTestClocksTestClock', String(sub.status), 'canceled', String(sub.id));
|
|
@@ -31,9 +25,17 @@ const remove: Semantics = async (ctx) => {
|
|
|
31
25
|
}
|
|
32
26
|
await ctx.write(CLOCK, id, { deleted: true }, 'test_clock.deleted');
|
|
33
27
|
return ctx.reply({ id, object: 'test_helpers.test_clock', deleted: true });
|
|
34
|
-
}
|
|
28
|
+
}
|
|
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
|
+
}
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
export async function PostTestHelpersTestClocksTestClockAdvance(ctx: HandlerContext): Promise<Response> {
|
|
37
39
|
const id = at(ctx, 'test_clock');
|
|
38
40
|
const clock = ctx.get(CLOCK, id);
|
|
39
41
|
if (!clock) return fail(ctx, `No such test clock: '${id}'`, 404, 'resource_missing');
|
|
@@ -47,31 +49,5 @@ const advance: Semantics = async (ctx) => {
|
|
|
47
49
|
// and finishClockAdvances completes it ADVANCE_SECONDS of World time later; an advance asked meanwhile is refused
|
|
48
50
|
// test_clock_not_ready (the machine's refusal).
|
|
49
51
|
return ctx.reply(await ctx.write(CLOCK, id, { status: 'advancing', status_details: { advancing: { target_frozen_time: to } } }, 'test_clock.advancing'));
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
/** How long an advance takes: "it takes a few seconds to advance to the specified time"
|
|
53
|
-
* (docs.stripe.com/billing/testing/test-clocks/api-advanced-usage). Where the documentation stops and the twin decides:
|
|
54
|
-
* five seconds of World time from the advance. */
|
|
55
|
-
const ADVANCE_SECONDS = 5;
|
|
56
|
-
|
|
57
|
-
/** Each advancing clock whose advance has taken its time reaches its target, caught up before anything is answered:
|
|
58
|
-
* its frozen_time moves, it is ready, and its customers' trials and periods that ended by then end
|
|
59
|
-
* (./renewals.ts advanceBilling). The advance's own write is when it began (the kernel's updatedAt). */
|
|
60
|
-
export async function finishClockAdvances(ctx: SemanticsContext): Promise<void> {
|
|
61
|
-
const now = Number(ctx.now());
|
|
62
|
-
for (const clock of ctx.rowsRaw(CLOCK)) {
|
|
63
|
-
if (clock.status !== 'advancing') continue;
|
|
64
|
-
if (Date.parse(String(clock.updatedAt)) / 1000 + ADVANCE_SECONDS > now) continue;
|
|
65
|
-
const id = String(clock.id);
|
|
66
|
-
const target = Number((clock.status_details as { advancing?: { target_frozen_time?: number } } | undefined)?.advancing?.target_frozen_time);
|
|
67
|
-
ctx.legal(CLOCK, 'status', ctx.call.operation.id, 'advancing', 'ready', id, 'vendor');
|
|
68
|
-
await ctx.write(CLOCK, id, { frozen_time: target, status: 'ready', status_details: {} }, 'test_clock.ready');
|
|
69
|
-
await advanceBilling(ctx, (sub) => typeof sub.customer === 'string' && ctx.get('customer', sub.customer)?.test_clock === id);
|
|
70
|
-
}
|
|
71
52
|
}
|
|
72
53
|
|
|
73
|
-
export const testClocks: Record<string, Semantics> = {
|
|
74
|
-
PostTestHelpersTestClocks: create,
|
|
75
|
-
PostTestHelpersTestClocksTestClockAdvance: advance,
|
|
76
|
-
DeleteTestHelpersTestClocksTestClock: remove,
|
|
77
|
-
};
|