@volter/twin-stripe 2.0.38 → 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 +112 -207
- package/dist/generated/pack-facts.json +94 -0
- 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 +18 -0
- package/dist/spec/doc-examples.json +445 -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 +306 -0
- package/dist/src/cli.d.ts +0 -1
- package/dist/src/cli.js +11 -31
- package/dist/src/engine/after-payment.d.ts +8 -0
- package/dist/src/engine/after-payment.js +27 -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/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 +34 -0
- package/dist/src/engine/connect.d.ts +45 -0
- package/dist/src/engine/connect.js +108 -0
- package/dist/src/engine/customers.d.ts +4 -0
- package/dist/src/engine/customers.js +8 -0
- package/dist/src/engine/display.d.ts +11 -0
- package/dist/src/engine/display.js +13 -0
- package/dist/src/engine/files.d.ts +11 -0
- package/dist/src/engine/files.js +23 -0
- package/dist/src/engine/invoices.d.ts +21 -0
- package/dist/src/engine/invoices.js +84 -0
- package/dist/src/engine/issuing.d.ts +33 -0
- package/dist/src/engine/issuing.js +54 -0
- package/dist/src/engine/ledger.d.ts +8 -0
- package/dist/src/engine/ledger.js +8 -0
- package/dist/src/engine/oauth.d.ts +4 -0
- package/dist/src/engine/oauth.js +24 -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-methods.d.ts +16 -0
- package/dist/src/engine/payment-methods.js +22 -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 +3 -0
- package/dist/src/engine/renewals.d.ts +36 -0
- package/dist/src/engine/renewals.js +65 -0
- package/dist/src/{stripe-twin.d.ts → engine/stripe.d.ts} +12 -45
- package/dist/src/{stripe-twin.js → engine/stripe.js} +53 -387
- package/dist/src/engine/subscriptions.d.ts +11 -0
- package/dist/src/engine/subscriptions.js +30 -0
- package/dist/src/engine/transfers.d.ts +2 -0
- package/dist/src/engine/transfers.js +1 -0
- package/dist/src/{stripe-version.d.ts → engine/version.d.ts} +0 -2
- package/dist/src/{stripe-version.js → engine/version.js} +21 -9
- 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 +10 -75
- package/dist/src/manifest.js +478 -876
- package/dist/src/screens/billing-portal.d.ts +3 -0
- package/dist/src/screens/{portal.js → billing-portal.js} +32 -58
- package/dist/src/screens/checkout-js.d.ts +2 -0
- package/dist/src/screens/checkout-js.js +84 -0
- package/dist/src/screens/checkout.d.ts +2 -31
- package/dist/src/screens/checkout.js +32 -121
- package/dist/src/screens/connect-onboarding.d.ts +3 -0
- package/dist/src/screens/{onboarding.js → connect-onboarding.js} +38 -104
- package/dist/src/screens/dashboard.d.ts +3 -0
- package/dist/src/screens/dashboard.js +116 -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 +95 -0
- package/dist/src/semantics/account.d.ts +2 -0
- package/dist/src/semantics/account.js +8 -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 +40 -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 +13 -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 +4 -0
- package/dist/src/semantics/billing_portal.js +36 -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 +21 -271
- package/dist/src/semantics/clock.d.ts +2 -0
- package/dist/src/semantics/clock.js +15 -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 +5 -6
- package/dist/src/semantics/customers.js +47 -413
- package/dist/src/semantics/doors.d.ts +23 -0
- package/dist/src/semantics/doors.js +73 -0
- package/dist/src/semantics/entitlements.d.ts +0 -2
- package/dist/src/semantics/entitlements.js +4 -95
- package/dist/src/semantics/events.d.ts +11 -0
- package/dist/src/semantics/events.js +31 -0
- package/dist/src/semantics/index.d.ts +2 -0
- package/dist/src/semantics/index.js +85 -0
- package/dist/src/semantics/invoice_payments.d.ts +2 -0
- package/dist/src/semantics/invoice_payments.js +12 -0
- package/dist/src/semantics/invoices.d.ts +11 -18
- package/dist/src/semantics/invoices.js +121 -440
- package/dist/src/semantics/issuing.d.ts +6 -13
- package/dist/src/semantics/issuing.js +58 -556
- package/dist/src/semantics/payment_intents.d.ts +3 -0
- package/dist/src/semantics/payment_intents.js +71 -0
- package/dist/src/semantics/payment_methods.d.ts +3 -0
- package/dist/src/semantics/payment_methods.js +15 -0
- package/dist/src/semantics/payouts.d.ts +3 -0
- package/dist/src/semantics/payouts.js +28 -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 +4 -2
- package/dist/src/semantics/products.js +12 -138
- 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 +2 -9
- package/dist/src/semantics/refunds.js +3 -193
- package/dist/src/semantics/shared.d.ts +558 -55
- package/dist/src/semantics/shared.js +2070 -114
- package/dist/src/semantics/states.d.ts +462 -0
- package/dist/src/semantics/states.js +634 -0
- package/dist/src/semantics/subscriptions.d.ts +6 -11
- package/dist/src/semantics/subscriptions.js +37 -530
- package/dist/src/semantics/test_helpers.d.ts +3 -0
- package/dist/src/semantics/test_helpers.js +125 -0
- package/dist/src/semantics/transfers.d.ts +2 -2
- package/dist/src/semantics/transfers.js +6 -101
- package/dist/src/semantics/webhook_endpoints.d.ts +2 -0
- package/dist/src/semantics/webhook_endpoints.js +26 -0
- package/dist/src/server.d.ts +7 -0
- package/dist/src/server.js +7 -0
- package/generated/pack-facts.json +94 -0
- 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 +11 -20
- package/spec/SOURCE.md +18 -0
- package/spec/doc-examples.json +445 -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 +306 -0
- package/src/cli.ts +11 -28
- package/src/engine/after-payment.ts +29 -0
- package/src/engine/apps-secrets.ts +4 -0
- package/src/engine/balance.ts +32 -0
- package/src/engine/checkout.ts +39 -0
- package/src/engine/common.ts +49 -0
- package/src/engine/connect.ts +106 -0
- package/src/engine/customers.ts +10 -0
- package/src/engine/display.ts +18 -0
- package/src/engine/files.ts +28 -0
- package/src/engine/invoices.ts +102 -0
- package/src/engine/issuing.ts +66 -0
- package/src/engine/ledger.ts +48 -0
- package/src/engine/oauth.ts +18 -0
- package/src/engine/onboarding.ts +53 -0
- package/src/{stripe-params.ts → engine/params.ts} +7 -56
- package/src/engine/payment-intents.ts +32 -0
- package/src/engine/payment-methods.ts +24 -0
- package/src/engine/radar.ts +6 -0
- package/src/engine/refunds.ts +3 -0
- package/src/engine/renewals.ts +72 -0
- package/src/{stripe-twin.ts → engine/stripe.ts} +57 -400
- package/src/engine/subscriptions.ts +37 -0
- package/src/engine/transfers.ts +22 -0
- package/src/{stripe-version.ts → engine/version.ts} +19 -10
- package/src/engine/webhook-endpoints.ts +14 -0
- package/src/engine/webhooks.ts +70 -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 +10 -116
- package/src/manifest.ts +480 -904
- package/src/screens/{portal.tsx → billing-portal.tsx} +35 -42
- package/src/screens/checkout-js.ts +80 -0
- package/src/screens/checkout.tsx +32 -96
- package/src/screens/{onboarding.tsx → connect-onboarding.tsx} +39 -85
- package/src/screens/dashboard.tsx +178 -0
- package/src/screens/shared.tsx +84 -0
- package/src/screens/stripe-js.ts +102 -0
- package/src/semantics/account.ts +10 -0
- package/src/semantics/account_links.ts +20 -0
- package/src/semantics/accounts.ts +44 -0
- package/src/semantics/{apps-secrets.ts → apps.ts} +17 -33
- package/src/semantics/around.ts +17 -0
- package/src/semantics/balance.ts +6 -208
- package/src/semantics/balance_transactions.ts +18 -0
- package/src/semantics/billing_portal.ts +45 -0
- package/src/semantics/charges.ts +8 -212
- package/src/semantics/checkout.ts +25 -276
- package/src/semantics/clock.ts +19 -0
- package/src/semantics/coupons.ts +5 -68
- package/src/semantics/customers.ts +48 -414
- package/src/semantics/doors.ts +79 -0
- package/src/semantics/entitlements.ts +3 -94
- package/src/semantics/events.ts +37 -0
- package/src/semantics/index.ts +89 -0
- package/src/semantics/invoice_payments.ts +16 -0
- package/src/semantics/invoices.ts +112 -441
- package/src/semantics/issuing.ts +57 -573
- package/src/semantics/payment_intents.ts +76 -0
- package/src/semantics/payment_methods.ts +20 -0
- package/src/semantics/payouts.ts +29 -0
- package/src/semantics/prices.ts +41 -0
- package/src/semantics/products.ts +12 -146
- package/src/semantics/promotion_codes.ts +38 -0
- package/src/semantics/radar.ts +8 -79
- package/src/semantics/refunds.ts +4 -216
- package/src/semantics/shared.ts +2089 -135
- package/src/semantics/states.ts +662 -0
- package/src/semantics/subscriptions.ts +39 -542
- package/src/semantics/test_helpers.ts +119 -0
- package/src/semantics/transfers.ts +10 -132
- package/src/semantics/webhook_endpoints.ts +27 -0
- package/src/server.ts +7 -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-oauth.d.ts +0 -27
- package/dist/src/screens/connect-oauth.js +0 -414
- package/dist/src/screens/connect-settings.d.ts +0 -22
- 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/disputes.d.ts +0 -2
- package/dist/src/semantics/disputes.js +0 -51
- 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/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/setup-intents.js +0 -84
- 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/test-clocks.js +0 -73
- 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-oauth.tsx +0 -400
- 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/disputes.ts +0 -62
- package/src/semantics/ephemeral-keys.ts +0 -34
- package/src/semantics/files.ts +0 -143
- 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/test-clocks.ts +0 -77
- 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/src/{screens → engine}/industries.ts +0 -0
- /package/src/{semantics → engine}/test-cards.ts +0 -0
package/src/semantics/issuing.ts
CHANGED
|
@@ -1,590 +1,74 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const CARD = 'issuing.card';
|
|
22
|
-
const AUTH = 'issuing.authorization';
|
|
23
|
-
const TXN = 'issuing.transaction';
|
|
24
|
-
const DSP = 'issuing.dispute';
|
|
25
|
-
const TOK = 'issuing.token';
|
|
26
|
-
const PD = 'issuing.personalization_design';
|
|
27
|
-
|
|
28
|
-
// ── cardholders ──
|
|
29
|
-
|
|
30
|
-
const createCardholder: Semantics = async (ctx) => {
|
|
31
|
-
const params = ctx.params;
|
|
32
|
-
if (params.name === undefined || params.name === '') return fail(ctx, 'Missing required param: name.', 400, 'parameter_missing');
|
|
33
|
-
const chType = typeof params.type === 'string' ? params.type : '';
|
|
34
|
-
if (chType !== 'individual' && chType !== 'company') return fail(ctx, "Invalid type: must be one of 'individual' or 'company'.", 400, 'parameter_invalid_string_enum');
|
|
35
|
-
const billing = params.billing && typeof params.billing === 'object' ? (params.billing as Row) : undefined;
|
|
36
|
-
if (!billing || !billing.address || typeof billing.address !== 'object') return fail(ctx, 'Missing required param: billing[address].', 400, 'parameter_missing');
|
|
37
|
-
const controls = normalizeSpendingControls(params.spending_controls, null);
|
|
38
|
-
if (controls.error) return send(ctx, controls.error);
|
|
39
|
-
return ctx.reply(
|
|
40
|
-
await created(ctx, CH, { ...params, spending_controls: controls.controls ?? emptySpendingControls() }, {
|
|
41
|
-
status: 'active', livemode: false, metadata: {}, phone_number: params.phone_number ?? null,
|
|
42
|
-
email: params.email ?? null, requirements: { disabled_reason: null, past_due: [] },
|
|
43
|
-
}),
|
|
44
|
-
);
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
/** A requested move of a state field the machine declares, legal or refused; a value the machine
|
|
48
|
-
* does not name is written as given, as the hand-written route did. */
|
|
49
|
-
function requestedMove(ctx: SemanticsContext, resource: string, operationId: string, current: unknown, values: string[]): Response | undefined {
|
|
50
|
-
const to = ctx.params.status;
|
|
51
|
-
if (typeof to !== 'string' || !values.includes(to)) return undefined;
|
|
52
|
-
const refused = ctx.legal(resource, 'status', operationId, current, to);
|
|
53
|
-
return refused ? ctx.refuse(refused) : undefined;
|
|
1
|
+
// Stripe's issuing 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 { CARD, CH, cardNumberOf, cvcOf } from '../engine/issuing.ts';
|
|
6
|
+
import { emptySpendingControls, normalizeSpendingControls, nowUnix } from '../engine/stripe.ts';
|
|
7
|
+
import { at, created, fail, issuingDecide, issuingRefusal, send } from './shared.ts';
|
|
8
|
+
|
|
9
|
+
export async function GetIssuingCardsCard(ctx: HandlerContext): Promise<Response> {
|
|
10
|
+
const id = at(ctx, 'card');
|
|
11
|
+
const card = ctx.get(CARD, id);
|
|
12
|
+
if (!card) return fail(ctx, `No such card: '${id}'`, 404, 'resource_missing');
|
|
13
|
+
const expand = Array.isArray(ctx.params.expand) ? ctx.params.expand.map(String) : [];
|
|
14
|
+
// source: spec:/components/schemas/issuing.card/properties/number/description "For security reasons, this is only available for virtual cards, and will be omitted unless you explicitly request it with"
|
|
15
|
+
const virtual = card.type === 'virtual';
|
|
16
|
+
const shown: Row = { ...card };
|
|
17
|
+
if (virtual && expand.includes('number')) shown.number = cardNumberOf(await ctx.secret(`issuing-card-number:${id}`));
|
|
18
|
+
// source: spec:/components/schemas/issuing.card/properties/cvc/description "The card's CVC. For security reasons, this is only available for virtual cards"
|
|
19
|
+
if (virtual && expand.includes('cvc')) shown.cvc = cvcOf(await ctx.secret(`issuing-card-cvc:${id}`));
|
|
20
|
+
return ctx.reply(ctx.expand(CARD, shown));
|
|
54
21
|
}
|
|
55
22
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (!ch) return fail(ctx, `No such cardholder: '${id}'`, 404, 'resource_missing');
|
|
60
|
-
const controls = normalizeSpendingControls(ctx.params.spending_controls, null);
|
|
61
|
-
if (controls.error) return send(ctx, controls.error);
|
|
62
|
-
const refused = requestedMove(ctx, CH, 'PostIssuingCardholdersCardholder', ch.status, ['active', 'inactive', 'blocked']);
|
|
63
|
-
if (refused) return refused;
|
|
64
|
-
const updated = await ctx.write(CH, id, controls.controls ? { ...ctx.params, spending_controls: controls.controls } : ctx.params, 'issuing_cardholder.updated');
|
|
65
|
-
// each of the cardholder's cards answers it as it now stands
|
|
66
|
-
for (const card of ctx.rowsRaw(CARD).filter((c) => holderId(c) === id)) await ctx.write(CARD, String(card.id), { cardholder: updated }, 'issuing_card.cardholder_synced');
|
|
67
|
-
return ctx.reply(updated);
|
|
68
|
-
};
|
|
23
|
+
export async function PostIssuingAuthorizationsAuthorizationApprove(ctx: HandlerContext): Promise<Response> {
|
|
24
|
+
return issuingDecide(true)(ctx);
|
|
25
|
+
}
|
|
69
26
|
|
|
70
|
-
|
|
27
|
+
export async function PostIssuingAuthorizationsAuthorizationDecline(ctx: HandlerContext): Promise<Response> {
|
|
28
|
+
return issuingDecide(false)(ctx);
|
|
29
|
+
}
|
|
71
30
|
|
|
72
|
-
|
|
31
|
+
export async function PostIssuingCards(ctx: HandlerContext): Promise<Response> {
|
|
73
32
|
const params = ctx.params;
|
|
74
|
-
|
|
75
|
-
if (!cardholder) return fail(ctx, 'Missing required param: cardholder.', 400, 'parameter_missing');
|
|
76
|
-
if (!ctx.get(CH, cardholder)) return fail(ctx, `No such cardholder: '${cardholder}'`, 400, 'resource_missing');
|
|
33
|
+
// source: spec:PostIssuingCards "The currency for the card."
|
|
77
34
|
if (params.currency === undefined || params.currency === '') return fail(ctx, 'Missing required param: currency.', 400, 'parameter_missing');
|
|
78
35
|
const cardType = typeof params.type === 'string' ? params.type : '';
|
|
36
|
+
if (!cardType) return fail(ctx, 'Missing required param: type.', 400, 'parameter_missing');
|
|
37
|
+
// source: spec:PostIssuingCards "The type of card to issue."
|
|
79
38
|
if (cardType !== 'virtual' && cardType !== 'physical') return fail(ctx, "Invalid type: must be one of 'virtual' or 'physical'.", 400, 'parameter_invalid_string_enum');
|
|
80
|
-
|
|
39
|
+
// the card answers its cardholder in full (the served spec's issuing.card.cardholder is the Cardholder object)
|
|
40
|
+
const holder = typeof params.cardholder === 'string' ? params.cardholder : '';
|
|
41
|
+
const cardholder = holder ? ctx.get(CH, holder) : undefined;
|
|
42
|
+
if (holder && !cardholder) return fail(ctx, `No such cardholder: '${holder}'`, 400, 'resource_missing');
|
|
81
43
|
const controls = normalizeSpendingControls(params.spending_controls, typeof params.currency === 'string' ? params.currency : null);
|
|
82
44
|
if (controls.error) return send(ctx, controls.error);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
type: (c, v) => c.type === v,
|
|
97
|
-
last4: (c, v) => c.last4 === v,
|
|
98
|
-
}));
|
|
45
|
+
const id = ctx.mint(CARD);
|
|
46
|
+
const number = cardNumberOf(await ctx.secret(`issuing-card-number:${id}`));
|
|
47
|
+
// Where the documentation stops and the twin decides: a card expires three years after it is issued, at the end of
|
|
48
|
+
// the month it was issued in.
|
|
49
|
+
const issued = new Date(Number(nowUnix(ctx.occurredAt)) * 1000);
|
|
50
|
+
return ctx.reply(ctx.expand(CARD, await created(ctx, CARD, { ...params, id, cardholder: holder || null, spending_controls: controls.controls ?? emptySpendingControls() }, {
|
|
51
|
+
// source: https://docs.stripe.com/api/issuing/cards/create "Whether authorizations can be approved on this card. May be blocked from activating cards depending on past-due Cardholder requirements. Defaults to inactive."
|
|
52
|
+
status: typeof params.status === 'string' ? params.status : 'inactive',
|
|
53
|
+
livemode: false, metadata: {}, brand: 'Visa', last4: number.slice(-4),
|
|
54
|
+
exp_month: issued.getUTCMonth() + 1, exp_year: issued.getUTCFullYear() + 3,
|
|
55
|
+
cancellation_reason: null, replacement_for: null, replacement_reason: null, shipping: null, wallets: null,
|
|
56
|
+
})));
|
|
57
|
+
}
|
|
99
58
|
|
|
100
|
-
|
|
59
|
+
// source: spec:PostIssuingCardsCard "Any parameters not provided will be left unchanged."
|
|
60
|
+
export async function PostIssuingCardsCard(ctx: HandlerContext): Promise<Response> {
|
|
101
61
|
const id = at(ctx, 'card');
|
|
102
62
|
const card = ctx.get(CARD, id);
|
|
103
63
|
if (!card) return fail(ctx, `No such card: '${id}'`, 404, 'resource_missing');
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return ctx.reply(await ctx.write(CARD, id, controls.controls ? { ...ctx.params, spending_controls: controls.controls } : ctx.params, 'issuing_card.updated'));
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
// ── authorizations ──
|
|
113
|
-
|
|
114
|
-
/** An authorization as every egress carries it: its card embedded in full. */
|
|
115
|
-
function withCard(ctx: SemanticsContext, a: Row): Row {
|
|
116
|
-
const out = { ...a };
|
|
117
|
-
if (typeof out.card === 'string') {
|
|
118
|
-
const card = ctx.get(CARD, out.card);
|
|
119
|
-
if (card) out.card = card;
|
|
120
|
-
}
|
|
121
|
-
return out;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const authMissing = (ctx: SemanticsContext, id: string): Response => fail(ctx, `No such authorization: '${id}'`, 404, 'resource_missing');
|
|
125
|
-
|
|
126
|
-
const listAuthorizations: Semantics = async (ctx) =>
|
|
127
|
-
list(ctx, AUTH, where(ctx, newest(ctx, AUTH), {
|
|
128
|
-
card: (a, v) => a.card === v,
|
|
129
|
-
cardholder: (a, v) => a.cardholder === v,
|
|
130
|
-
status: (a, v) => a.status === v,
|
|
131
|
-
}).map((a) => withCard(ctx, a)));
|
|
132
|
-
|
|
133
|
-
const retrieveAuthorization: Semantics = async (ctx) => {
|
|
134
|
-
const a = ctx.get(AUTH, at(ctx, 'authorization'));
|
|
135
|
-
return a ? ctx.reply(withCard(ctx, a)) : authMissing(ctx, at(ctx, 'authorization'));
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
const updateAuthorization: Semantics = async (ctx) => {
|
|
139
|
-
const id = at(ctx, 'authorization');
|
|
140
|
-
if (!ctx.get(AUTH, id)) return authMissing(ctx, id);
|
|
141
|
-
return ctx.reply(await ctx.write(AUTH, id, ctx.params, 'issuing_authorization.updated'));
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
/** One request_history entry; the authorization code is derived from the request time (Stripe's is not unique either). */
|
|
145
|
-
function historyEntry(o: { amount: number; currency: string; approved: boolean; reason: string; reasonMessage: string | null; createdSec: number; merchantAmount: number; merchantCurrency: string }): Row {
|
|
146
|
-
return {
|
|
147
|
-
amount: o.amount, amount_details: null, approved: o.approved,
|
|
148
|
-
authorization_code: o.approved ? `S${String(100000 + (o.createdSec % 900000))}` : null,
|
|
149
|
-
created: o.createdSec, currency: o.currency,
|
|
150
|
-
merchant_amount: o.merchantAmount, merchant_currency: o.merchantCurrency,
|
|
151
|
-
network_risk_score: null, reason: o.reason, reason_message: o.reasonMessage,
|
|
152
|
-
requested_at: o.createdSec,
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/** The test helper's default merchant, under any merchant_data the caller gives. */
|
|
157
|
-
function merchantData(param: unknown): Row {
|
|
158
|
-
const base: Row = {
|
|
159
|
-
category: 'general', category_code: '5399', city: 'San Francisco', country: 'US',
|
|
160
|
-
name: 'Twin Test Merchant', network_id: '1234567890', postal_code: '94103', state: 'CA',
|
|
161
|
-
tax_id: null, terminal_id: null, url: null,
|
|
162
|
-
};
|
|
163
|
-
if (param && typeof param === 'object' && !Array.isArray(param)) Object.assign(base, param);
|
|
164
|
-
return base;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/** A capture: an Issuing transaction (negative: it debits) and the issuing balance's debit. */
|
|
168
|
-
async function capture(ctx: SemanticsContext, authId: string, auth: Row, amountCents: number): Promise<{ txnId: string; btId: string }> {
|
|
169
|
-
const currency = typeof auth.currency === 'string' ? auth.currency : 'usd';
|
|
170
|
-
const bt = await created(ctx, 'balance_transaction', {}, {
|
|
171
|
-
amount: -amountCents, currency, fee: 0, net: -amountCents, type: 'issuing_transaction',
|
|
172
|
-
status: 'available', balance_type: 'issuing', reporting_category: 'issuing_transaction',
|
|
173
|
-
available_on: ctx.now(), fee_details: [],
|
|
174
|
-
});
|
|
175
|
-
const txn = await created(ctx, TXN, { authorization: authId, card: auth.card, cardholder: auth.cardholder }, {
|
|
176
|
-
type: 'capture', amount: -amountCents, currency,
|
|
177
|
-
merchant_amount: -amountCents, merchant_currency: typeof auth.merchant_currency === 'string' ? auth.merchant_currency : currency,
|
|
178
|
-
merchant_data: auth.merchant_data ?? merchantData(undefined),
|
|
179
|
-
livemode: false, metadata: {}, dispute: null, wallet: null, network_data: null,
|
|
180
|
-
amount_details: null, purchase_details: null, balance_transaction: bt.id,
|
|
181
|
-
});
|
|
182
|
-
return { txnId: String(txn.id), btId: String(bt.id) };
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/** The card's stored id (an embedded card answers with its id). */
|
|
186
|
-
const cardId = (auth: Row): unknown => (auth.card && typeof auth.card === 'object' ? (auth.card as Row).id : auth.card);
|
|
187
|
-
|
|
188
|
-
/** Issuing refusals of a request no story sends: an unknown card status or authorization method, and a capture of an
|
|
189
|
-
* authorization that is not approved and pending. */
|
|
190
|
-
function issuingRefusal(ctx: SemanticsContext, what: 'card_status' | 'authorization_method' | 'not_capturable', auth?: Row): Response {
|
|
191
|
-
switch (what) {
|
|
192
|
-
case 'card_status': return fail(ctx, "Invalid status: must be one of 'active', 'inactive', or 'canceled'.", 400, 'parameter_invalid_string_enum');
|
|
193
|
-
case 'authorization_method': return fail(ctx, "Invalid authorization_method: must be one of 'chip', 'contactless', 'keyed_in', 'online', or 'swipe'.", 400, 'parameter_invalid_string_enum');
|
|
194
|
-
case 'not_capturable': return fail(ctx, `This authorization cannot be captured because it is not an approved pending authorization (status: ${String(auth?.status)}, approved: ${auth?.approved === true}).`, 400);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/** A partial approval's refusal: "You can specify the amount you want to approve by setting the amount in the webhook
|
|
199
|
-
* response body or the approve call" only "When an authorization is partially authorized, the is_amount_controllable
|
|
200
|
-
* field on the authorization request is set to true" (docs.stripe.com/issuing/purchases/authorizations), and never a
|
|
201
|
-
* credit. Answers the refusal, or undefined when the amount may be approved. */
|
|
202
|
-
function partialApprovalRefused(ctx: SemanticsContext, auth: Row): Response | undefined {
|
|
203
|
-
if (Math.trunc(Number(ctx.params.amount) || 0) <= 0) return fail(ctx, 'Invalid integer: amount must be a positive integer.', 400, 'parameter_invalid_integer');
|
|
204
|
-
if ((auth.pending_request as Row | null | undefined)?.is_amount_controllable !== true) return fail(ctx, 'amount may only be provided when the authorization was presented with is_amount_controllable.', 400);
|
|
205
|
-
return undefined;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// the (deprecated but real) API decision on a pending authorization: it lands in request_history
|
|
209
|
-
// as the webhook's answer would, consumes the pending request, and an approval captures
|
|
210
|
-
function decide(approved: boolean): Semantics {
|
|
211
|
-
return async (ctx) => {
|
|
212
|
-
const id = at(ctx, 'authorization');
|
|
213
|
-
const auth = ctx.get(AUTH, id);
|
|
214
|
-
if (!auth) return authMissing(ctx, id);
|
|
215
|
-
const finalized = { status: 400, code: 'authorization_already_finalized', message: `This authorization has already been finalized (status ${String(auth.status)}).` };
|
|
216
|
-
// a webhook-approved authorization is decided, though still pending
|
|
217
|
-
if (auth.approved === true) return ctx.refuse(finalized);
|
|
218
|
-
const refused = ctx.legal(AUTH, 'status', approved ? 'PostIssuingAuthorizationsAuthorizationApprove' : 'PostIssuingAuthorizationsAuthorizationDecline', auth.status, undefined, id);
|
|
64
|
+
const { expand: _expand, ...fields } = ctx.params;
|
|
65
|
+
// source: spec:/paths/~1v1~1issuing~1cards~1{card}/post/requestBody/content/application~1x-www-form-urlencoded/schema/properties/status "Dictates whether authorizations can be approved on this card."
|
|
66
|
+
if (fields.status !== undefined && !['active', 'inactive', 'canceled'].includes(String(fields.status))) return issuingRefusal(ctx, 'card_status');
|
|
67
|
+
if (fields.status !== undefined) {
|
|
68
|
+
const refused = ctx.legal(CARD, 'status', 'PostIssuingCardsCard', card.status, String(fields.status), id);
|
|
219
69
|
if (refused) return ctx.refuse(refused);
|
|
220
|
-
// a partial approval only for an amount-controllable presentment, and never a credit
|
|
221
|
-
const badAmount = approved && ctx.params.amount !== undefined ? partialApprovalRefused(ctx, auth) : undefined;
|
|
222
|
-
if (badAmount) return badAmount;
|
|
223
|
-
const decidedAmount = approved && ctx.params.amount !== undefined ? Math.trunc(Number(ctx.params.amount) || 0) : Number(auth.amount) || 0;
|
|
224
|
-
const currency = typeof auth.currency === 'string' ? auth.currency : 'usd';
|
|
225
|
-
const history = Array.isArray(auth.request_history) ? (auth.request_history as Row[]) : [];
|
|
226
|
-
// an approval holds the funds and waits for the merchant's capture; a decline closes it
|
|
227
|
-
// (docs.stripe.com/issuing/purchases/authorizations, docs.stripe.com/issuing/funding/balance)
|
|
228
|
-
const hold = approved ? await holdFor(ctx, id, decidedAmount, currency) : null;
|
|
229
|
-
return ctx.reply(
|
|
230
|
-
await ctx.write(AUTH, id, {
|
|
231
|
-
...(approved ? {} : { status: 'closed' }), approved, pending_request: null,
|
|
232
|
-
...(approved && ctx.params.amount !== undefined ? { amount: decidedAmount } : {}),
|
|
233
|
-
request_history: [...history, historyEntry({
|
|
234
|
-
amount: decidedAmount, currency, approved, reason: approved ? 'webhook_approved' : 'webhook_declined', reasonMessage: null,
|
|
235
|
-
createdSec: Number(ctx.now()), merchantAmount: Number(auth.merchant_amount) || decidedAmount,
|
|
236
|
-
merchantCurrency: typeof auth.merchant_currency === 'string' ? auth.merchant_currency : currency,
|
|
237
|
-
})],
|
|
238
|
-
...(hold ? { balance_transactions: [...(Array.isArray(auth.balance_transactions) ? auth.balance_transactions : []), hold.id] } : {}),
|
|
239
|
-
}, 'issuing_authorization.updated'),
|
|
240
|
-
);
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/** The real-time window: "If Stripe doesn't receive your approve or decline response within 2 seconds, the
|
|
245
|
-
* Authorization is automatically approved or declined based on your timeout settings"
|
|
246
|
-
* (docs.stripe.com/issuing/controls/real-time-authorizations). */
|
|
247
|
-
const REALTIME_WINDOW_SECONDS = 2;
|
|
248
|
-
|
|
249
|
-
/** Time's decision on a real-time request no one answered, caught up to the World's clock: once its window has ended,
|
|
250
|
-
* a pending authorization whose issuing_authorization.request went unanswered, and that no approve or decline decided,
|
|
251
|
-
* is decided automatically, its request_history reason webhook_timeout ("webhook_timeout for all other failure modes",
|
|
252
|
-
* the same page, beside webhook_error for a response Stripe cannot process).
|
|
253
|
-
* Where the documentation stops and the twin decides: the timeout setting is the Dashboard's, which the API does not
|
|
254
|
-
* answer, and the twin's is to decline; the window is measured in World time from the authorization's created. */
|
|
255
|
-
export async function lapseRealtimeRequests(ctx: SemanticsContext): Promise<void> {
|
|
256
|
-
const now = Number(ctx.now());
|
|
257
|
-
const requested = new Set(ctx.rowsRaw('event').filter((e) => e._stripe_type === 'issuing_authorization.request').map((e) => String(((e.data as Row | undefined)?.object as Row | undefined)?.id ?? '')));
|
|
258
|
-
for (const a of ctx.rowsRaw(AUTH)) {
|
|
259
|
-
if (a.status !== 'pending' || a.approved === true || !a.pending_request || !requested.has(String(a.id))) continue;
|
|
260
|
-
const at = Number(a.created) + REALTIME_WINDOW_SECONDS;
|
|
261
|
-
if (now < at) continue;
|
|
262
|
-
const id = String(a.id);
|
|
263
|
-
const currency = typeof a.currency === 'string' ? a.currency : 'usd';
|
|
264
|
-
const amount = Number(a.amount) || 0;
|
|
265
|
-
ctx.legal(AUTH, 'status', ctx.call.operation.id, 'pending', 'closed', id, 'time');
|
|
266
|
-
await ctx.write(AUTH, id, {
|
|
267
|
-
status: 'closed', approved: false, pending_request: null,
|
|
268
|
-
request_history: [...(Array.isArray(a.request_history) ? (a.request_history as Row[]) : []), historyEntry({
|
|
269
|
-
amount, currency, approved: false, reason: 'webhook_timeout', reasonMessage: null, createdSec: at,
|
|
270
|
-
merchantAmount: Number(a.merchant_amount) || amount, merchantCurrency: typeof a.merchant_currency === 'string' ? a.merchant_currency : currency,
|
|
271
|
-
})],
|
|
272
|
-
}, 'issuing_authorization.updated');
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/** An approval's hold: "If you approve the authorization, we deduct the `amount` from your Issuing balance and hold it
|
|
277
|
-
* in reserve until the authorization is either captured, voided, or expired without capture"
|
|
278
|
-
* (docs.stripe.com/issuing/purchases/authorizations). */
|
|
279
|
-
async function holdFor(ctx: SemanticsContext, authId: string, amount: number, currency: string): Promise<Row> {
|
|
280
|
-
return created(ctx, 'balance_transaction', {}, {
|
|
281
|
-
amount: -amount, currency, fee: 0, net: -amount, type: 'issuing_authorization_hold',
|
|
282
|
-
status: 'available', balance_type: 'issuing', reporting_category: 'issuing_authorization_hold',
|
|
283
|
-
available_on: ctx.now(), fee_details: [], source: authId,
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/** An authorization approved as it is presented, its amount held: approved and pending until the merchant captures. */
|
|
288
|
-
async function approvedAtOnce(ctx: SemanticsContext, subject: Row, fields: Row, held: number, entry: Row): Promise<Response> {
|
|
289
|
-
const hold = await holdFor(ctx, String(subject.id), held, String(fields.currency ?? 'usd'));
|
|
290
|
-
return ctx.reply(await created(ctx, AUTH, subject, { ...fields, amount: held, status: 'pending', approved: true, pending_request: null, request_history: [entry], balance_transactions: [hold.id] }));
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/** An authorization as the platform's real-time answer makes it: approved (in part, only when the request was
|
|
294
|
-
* amount-controllable), declined, or declined for an answer Stripe cannot read (docs.stripe.com/issuing/controls/
|
|
295
|
-
* real-time-authorizations: the response's `approved` and `amount`; webhook_error "if we can't process your response"). */
|
|
296
|
-
async function answeredByEndpoint(ctx: SemanticsContext, outcome: { kind: 'approved'; amount?: number } | { kind: 'declined' } | { kind: 'error'; message: string }, o: {
|
|
297
|
-
subject: Row; fields: Row; amount: number; controllable: boolean; entry: (e: { amount: number; approved: boolean; reason: string; reasonMessage: string | null }) => Row;
|
|
298
|
-
decline: (reason: string, message: string | null) => Promise<Response>;
|
|
299
|
-
}): Promise<Response> {
|
|
300
|
-
if (outcome.kind === 'approved') {
|
|
301
|
-
const held = o.controllable && outcome.amount !== undefined ? outcome.amount : o.amount;
|
|
302
|
-
return approvedAtOnce(ctx, o.subject, o.fields, held, o.entry({ amount: held, approved: true, reason: 'webhook_approved', reasonMessage: null }));
|
|
303
70
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const present: Semantics = async (ctx) => {
|
|
308
|
-
const params = ctx.params;
|
|
309
|
-
const card = typeof params.card === 'string' ? params.card : '';
|
|
310
|
-
if (!card) return fail(ctx, 'Missing required param: card.', 400, 'parameter_missing');
|
|
311
|
-
const c = ctx.get(CARD, card);
|
|
312
|
-
if (!c) return fail(ctx, `No such card: '${card}'`, 400, 'resource_missing');
|
|
313
|
-
if (params.amount === undefined) return fail(ctx, 'Missing required param: amount.', 400, 'parameter_missing');
|
|
314
|
-
const amount = Math.trunc(Number(params.amount) || 0);
|
|
315
|
-
const currency = String(params.currency ?? c.currency ?? 'usd');
|
|
316
|
-
const createdSec = Number(ctx.now());
|
|
317
|
-
const cardholderId = holderId(c);
|
|
318
|
-
const merchant = merchantData(params.merchant_data);
|
|
319
|
-
const isAmountControllable = params.is_amount_controllable === true;
|
|
320
|
-
const authMethod = typeof params.authorization_method === 'string' ? params.authorization_method : 'online';
|
|
321
|
-
if (!['chip', 'contactless', 'keyed_in', 'online', 'swipe'].includes(authMethod)) return issuingRefusal(ctx, 'authorization_method');
|
|
322
|
-
const merchantAmount = params.merchant_amount !== undefined ? Math.trunc(Number(params.merchant_amount) || 0) : amount;
|
|
323
|
-
const merchantCurrency = typeof params.merchant_currency === 'string' ? params.merchant_currency : currency;
|
|
324
|
-
const authId = ctx.mint(AUTH);
|
|
325
|
-
const baseFields: Row = {
|
|
326
|
-
amount, amount_details: null, authorization_method: authMethod,
|
|
327
|
-
balance_transactions: [], transactions: [],
|
|
328
|
-
currency, fleet: null, fuel: null, livemode: false,
|
|
329
|
-
merchant_amount: merchantAmount, merchant_currency: merchantCurrency, merchant_data: merchant,
|
|
330
|
-
metadata: params.metadata && typeof params.metadata === 'object' ? params.metadata : {},
|
|
331
|
-
network_data: null,
|
|
332
|
-
verification_data: {
|
|
333
|
-
address_line1_check: 'not_provided', address_postal_code_check: 'not_provided',
|
|
334
|
-
authentication_exemption: null, cvc_check: 'match', expiry_check: 'match',
|
|
335
|
-
postal_code: null, three_d_secure: null,
|
|
336
|
-
},
|
|
337
|
-
wallet: null,
|
|
338
|
-
};
|
|
339
|
-
const subject = { id: authId, card, cardholder: cardholderId };
|
|
340
|
-
const entry = (o: { amount: number; approved: boolean; reason: string; reasonMessage: string | null }) => historyEntry({ ...o, currency, createdSec, merchantAmount, merchantCurrency });
|
|
341
|
-
const decline = async (reason: string, reasonMessage: string | null) =>
|
|
342
|
-
ctx.reply(await created(ctx, AUTH, subject, { ...baseFields, status: 'closed', approved: false, pending_request: null, request_history: [entry({ amount, approved: false, reason, reasonMessage })] }));
|
|
343
|
-
if (c.status === 'canceled') return decline('card_canceled', 'The card has been canceled.');
|
|
344
|
-
if (c.status !== 'active') return decline('card_inactive', 'The card is inactive.');
|
|
345
|
-
// what earlier approved authorizations on the card (or cardholder) already spent, for the limits
|
|
346
|
-
const prior = (key: 'card' | 'cardholder', value: string) =>
|
|
347
|
-
ctx.rows(AUTH).filter((a) => a[key] === value && a.approved === true)
|
|
348
|
-
.map((a) => ({ amount: Number(a.amount) || 0, created: Number(a.created) || 0, category: String((a.merchant_data as Row | undefined)?.category ?? '') }));
|
|
349
|
-
const controlsOpts = { amount, category: String(merchant.category ?? ''), country: merchant.country == null ? null : String(merchant.country), nowSec: createdSec };
|
|
350
|
-
const cardholderRow = cardholderId ? ctx.get(CH, cardholderId) : undefined;
|
|
351
|
-
const violation =
|
|
352
|
-
spendingControlsViolation(c.spending_controls, { ...controlsOpts, priorApproved: prior('card', card) }) ??
|
|
353
|
-
spendingControlsViolation(cardholderRow?.spending_controls, { ...controlsOpts, priorApproved: prior('cardholder', cardholderId) });
|
|
354
|
-
if (violation) return decline('spending_controls', violation);
|
|
355
|
-
// an authorization is paid from the Issuing balance, and declined when it cannot cover it
|
|
356
|
-
// (docs.stripe.com/issuing/funding/balance)
|
|
357
|
-
if ((balanceOf(ctx).issuing.get(currency) ?? 0) < amount) return decline('insufficient_funds', 'Your Issuing balance does not have enough funds for this authorization.');
|
|
358
|
-
const pendingRequest = {
|
|
359
|
-
amount, amount_details: null, currency, is_amount_controllable: isAmountControllable,
|
|
360
|
-
merchant_amount: merchantAmount, merchant_currency: merchantCurrency, network_risk_score: null,
|
|
361
|
-
};
|
|
362
|
-
const endpoint = ctx.rows('webhook_endpoint').find((w) => w.status === 'enabled' && stripeEventMatches(w.enabled_events, 'issuing_authorization.request'));
|
|
363
|
-
// no real-time enrollment: "If you don't have a real-time authorization webhook, we approve the authorization without
|
|
364
|
-
// sending the issuing_authorization.request" (docs.stripe.com/issuing/purchases/authorizations), its outcome
|
|
365
|
-
// card_active, "approved according to your default Issuing settings"
|
|
366
|
-
if (!endpoint) return approvedAtOnce(ctx, subject, baseFields, amount, entry({ amount, approved: true, reason: 'card_active', reasonMessage: null }));
|
|
367
|
-
// the request event's authorization carries pending_request, non-null only during the request
|
|
368
|
-
const requestObject = withCard(ctx, { object: 'issuing.authorization', id: authId, created: createdSec, ...baseFields, card, cardholder: cardholderId, status: 'pending', approved: false, pending_request: pendingRequest, request_history: [] });
|
|
369
|
-
// the delivered request and the stored event are one event under one id
|
|
370
|
-
const requestEvent: StripeEvent = { id: nextStripeEventId(ctx.root), object: 'event', type: 'issuing_authorization.request', created: createdSec, livemode: false, data: { object: requestObject } };
|
|
371
|
-
const version = ctx.call.request.headers.get('stripe-version') ?? undefined;
|
|
372
|
-
await persistStripeEvent('issuing_authorization.request', requestObject, ctx.root, ctx.occurredAt, version, undefined, requestEvent.id);
|
|
373
|
-
const secret = typeof endpoint.secret === 'string' && endpoint.secret ? endpoint.secret : STRIPE_WEBHOOK_FALLBACK_SECRET;
|
|
374
|
-
const outcome = await requestAuthorizationDecision(String(endpoint.url), requestEvent, secret);
|
|
375
|
-
// a request no one answered (the World refused the delivery, it failed, or it timed out) is not a decision: the
|
|
376
|
-
// authorization stays pending with its request, for the deprecated approve or decline "within the timeout window of
|
|
377
|
-
// the real-time authorization flow" (the served spec), until lapseRealtimeRequests decides it when the window ends
|
|
378
|
-
if (outcome.kind === 'timeout') return ctx.reply(await created(ctx, AUTH, subject, { ...baseFields, status: 'pending', approved: false, request_history: [], pending_request: pendingRequest }));
|
|
379
|
-
return answeredByEndpoint(ctx, outcome, { subject, fields: baseFields, amount, controllable: isAmountControllable, entry, decline });
|
|
380
|
-
};
|
|
381
|
-
|
|
382
|
-
// capturing an approved pending authorization, up to what it still holds; it closes unless told not to
|
|
383
|
-
const captureAuthorization: Semantics = async (ctx) => {
|
|
384
|
-
const id = at(ctx, 'authorization');
|
|
385
|
-
const auth = ctx.get(AUTH, id);
|
|
386
|
-
if (!auth) return authMissing(ctx, id);
|
|
387
|
-
if (auth.approved !== true || auth.status !== 'pending') return issuingRefusal(ctx, 'not_capturable', auth);
|
|
388
|
-
const closeAuthorization = ctx.params.close_authorization !== false;
|
|
389
|
-
const refused = ctx.legal(AUTH, 'status', 'PostTestHelpersIssuingAuthorizationsAuthorizationCapture', auth.status, closeAuthorization ? 'closed' : 'pending');
|
|
390
|
-
if (refused) return ctx.refuse(refused);
|
|
391
|
-
const txns = Array.isArray(auth.transactions) ? auth.transactions : [];
|
|
392
|
-
const capturedSoFar = txns.map((t) => ctx.get(TXN, String(t))).reduce((s, t) => s + Math.abs(Number(t?.amount) || 0), 0);
|
|
393
|
-
const remaining = (Number(auth.amount) || 0) - capturedSoFar;
|
|
394
|
-
const captureAmount = ctx.params.capture_amount !== undefined ? Math.trunc(Number(ctx.params.capture_amount) || 0) : remaining;
|
|
395
|
-
if (captureAmount <= 0 || captureAmount > remaining) return fail(ctx, 'Invalid capture_amount: must be a positive integer no greater than the uncaptured authorized amount.', 400, 'parameter_invalid_integer');
|
|
396
|
-
// the capture releases what the approval held and debits the transaction; a capture that closes the authorization
|
|
397
|
-
// releases ALL it still holds, the uncaptured rest included: the amount is held "until the authorization is either
|
|
398
|
-
// captured, voided, or expired without capture" (docs.stripe.com/issuing/purchases/authorizations), and a closed
|
|
399
|
-
// authorization can be captured no further (close_authorization "Defaults to true. Set to false to enable
|
|
400
|
-
// multi-capture flows", the served spec). A capture that keeps it open releases only what it captured.
|
|
401
|
-
const currency = typeof auth.currency === 'string' ? auth.currency : 'usd';
|
|
402
|
-
const released = closeAuthorization ? remaining : captureAmount;
|
|
403
|
-
const release = await created(ctx, 'balance_transaction', {}, {
|
|
404
|
-
amount: released, currency, fee: 0, net: released, type: 'issuing_authorization_release',
|
|
405
|
-
status: 'available', balance_type: 'issuing', reporting_category: 'issuing_authorization_release',
|
|
406
|
-
available_on: ctx.now(), fee_details: [], source: id,
|
|
407
|
-
});
|
|
408
|
-
const captured = await capture(ctx, id, { ...auth, card: cardId(auth) }, captureAmount);
|
|
409
|
-
return ctx.reply(
|
|
410
|
-
await ctx.write(AUTH, id, {
|
|
411
|
-
...(closeAuthorization ? { status: 'closed' } : {}),
|
|
412
|
-
transactions: [...txns, captured.txnId],
|
|
413
|
-
balance_transactions: [...(Array.isArray(auth.balance_transactions) ? auth.balance_transactions : []), release.id, captured.btId],
|
|
414
|
-
}, 'issuing_authorization.updated'),
|
|
415
|
-
);
|
|
416
|
-
};
|
|
417
|
-
|
|
418
|
-
// ── transactions ──
|
|
419
|
-
|
|
420
|
-
const updateTransaction: Semantics = async (ctx) => {
|
|
421
|
-
const id = at(ctx, 'transaction');
|
|
422
|
-
if (!ctx.get(TXN, id)) return fail(ctx, `No such transaction: '${id}'`, 404, 'resource_missing');
|
|
423
|
-
return ctx.reply(await ctx.write(TXN, id, ctx.params, 'issuing_transaction.updated'));
|
|
424
|
-
};
|
|
425
|
-
|
|
426
|
-
// a settlement with no authorization before it still debits the issuing balance
|
|
427
|
-
const forceCapture: Semantics = async (ctx) => {
|
|
428
|
-
const card = typeof ctx.params.card === 'string' ? ctx.params.card : '';
|
|
429
|
-
if (!card) return fail(ctx, 'Missing required param: card.', 400, 'parameter_missing');
|
|
430
|
-
const c = ctx.get(CARD, card);
|
|
431
|
-
if (!c) return fail(ctx, `No such card: '${card}'`, 400, 'resource_missing');
|
|
432
|
-
if (ctx.params.amount === undefined) return fail(ctx, 'Missing required param: amount.', 400, 'parameter_missing');
|
|
433
|
-
const amount = Math.trunc(Number(ctx.params.amount) || 0);
|
|
434
|
-
if (amount <= 0) return fail(ctx, 'Invalid integer: amount must be a positive integer.', 400, 'parameter_invalid_integer');
|
|
435
|
-
const currency = String(ctx.params.currency ?? c.currency ?? 'usd');
|
|
436
|
-
const bt = await created(ctx, 'balance_transaction', {}, {
|
|
437
|
-
amount: -amount, currency, fee: 0, net: -amount, type: 'issuing_transaction',
|
|
438
|
-
status: 'available', balance_type: 'issuing', reporting_category: 'issuing_transaction',
|
|
439
|
-
available_on: ctx.now(), fee_details: [],
|
|
440
|
-
});
|
|
441
|
-
return ctx.reply(
|
|
442
|
-
await created(ctx, TXN, { card, cardholder: holderId(c) }, {
|
|
443
|
-
type: 'capture', amount: -amount, currency,
|
|
444
|
-
merchant_amount: -amount, merchant_currency: typeof ctx.params.merchant_currency === 'string' ? ctx.params.merchant_currency : currency,
|
|
445
|
-
merchant_data: merchantData(ctx.params.merchant_data),
|
|
446
|
-
authorization: null, dispute: null, balance_transaction: bt.id,
|
|
447
|
-
livemode: false, metadata: {}, wallet: null, network_data: null, amount_details: null, purchase_details: null,
|
|
448
|
-
}),
|
|
449
|
-
);
|
|
450
|
-
};
|
|
451
|
-
|
|
452
|
-
// ── disputes: opened against a transaction, unsubmitted until submitted ──
|
|
453
|
-
|
|
454
|
-
const disputeMissing = (ctx: SemanticsContext, id: string): Response => fail(ctx, `No such dispute: '${id}'`, 404, 'resource_missing');
|
|
455
|
-
|
|
456
|
-
const openDispute: Semantics = async (ctx) => {
|
|
457
|
-
const txn = typeof ctx.params.transaction === 'string' ? ctx.params.transaction : '';
|
|
458
|
-
if (!txn) return fail(ctx, 'Missing required param: transaction.', 400, 'parameter_missing');
|
|
459
|
-
const t = ctx.get(TXN, txn);
|
|
460
|
-
if (!t) return fail(ctx, `No such transaction: '${txn}'`, 400, 'resource_missing');
|
|
461
|
-
const ev = ctx.params.evidence && typeof ctx.params.evidence === 'object' ? (ctx.params.evidence as Row) : undefined;
|
|
462
|
-
if (!ev || ev.reason === undefined || ev.reason === '') return fail(ctx, 'Missing required param: evidence[reason].', 400, 'parameter_missing');
|
|
463
|
-
const amount = ctx.params.amount !== undefined ? Math.trunc(Number(ctx.params.amount) || 0) : Math.abs(Number(t.amount) || 0);
|
|
464
|
-
return ctx.reply(await created(ctx, DSP, ctx.params, { status: 'unsubmitted', amount, currency: t.currency ?? 'usd', livemode: false, metadata: {}, balance_transactions: null }));
|
|
465
|
-
};
|
|
466
|
-
|
|
467
|
-
const submitDispute: Semantics = async (ctx) => {
|
|
468
|
-
const id = at(ctx, 'dispute');
|
|
469
|
-
const d = ctx.get(DSP, id);
|
|
470
|
-
if (!d) return disputeMissing(ctx, id);
|
|
471
|
-
const refused = ctx.legal(DSP, 'status', 'PostIssuingDisputesDisputeSubmit', d.status, undefined, id);
|
|
472
|
-
if (refused) return ctx.refuse(refused);
|
|
473
|
-
return ctx.reply(await ctx.write(DSP, id, { status: 'submitted' }, 'issuing_dispute.submitted'));
|
|
474
|
-
};
|
|
475
|
-
|
|
476
|
-
const updateDispute: Semantics = async (ctx) => {
|
|
477
|
-
const id = at(ctx, 'dispute');
|
|
478
|
-
if (!ctx.get(DSP, id)) return disputeMissing(ctx, id);
|
|
479
|
-
return ctx.reply(await ctx.write(DSP, id, ctx.params, 'issuing_dispute.updated'));
|
|
480
|
-
};
|
|
481
|
-
|
|
482
|
-
// ── network tokens: minted by the card network (the twin's mint is a test helper outside Stripe's
|
|
483
|
-
// surface); Stripe lets a token be set active, deleted or suspended ──
|
|
484
|
-
|
|
485
|
-
const updateToken: Semantics = async (ctx) => {
|
|
486
|
-
const id = at(ctx, 'token');
|
|
487
|
-
const t = ctx.get(TOK, id);
|
|
488
|
-
if (!t) return fail(ctx, `No such issuing token: '${id}'`, 404, 'resource_missing');
|
|
489
|
-
const status = typeof ctx.params.status === 'string' ? ctx.params.status : '';
|
|
490
|
-
if (!['active', 'deleted', 'suspended'].includes(status)) return fail(ctx, 'Invalid status: must be active, deleted, or suspended.', 400, 'parameter_invalid_string_enum');
|
|
491
|
-
const refused = ctx.legal(TOK, 'status', 'PostIssuingTokensToken', t.status, status);
|
|
492
|
-
if (refused) return ctx.refuse(refused);
|
|
493
|
-
return ctx.reply(await ctx.write(TOK, id, { status }, 'issuing_token.updated'));
|
|
494
|
-
};
|
|
495
|
-
|
|
496
|
-
// ── personalization designs: in review until Stripe's review (the test helpers in test mode) decides ──
|
|
497
|
-
|
|
498
|
-
const designMissing = (ctx: SemanticsContext, id: string): Response => fail(ctx, `No such personalization design: '${id}'`, 404, 'resource_missing');
|
|
499
|
-
|
|
500
|
-
/** Stripe's physical bundles are its catalog, not the account's: the twin serves the one its reference publishes
|
|
501
|
-
* (docs.stripe.com/api/issuing/physical_bundles: "US Visa Credit White", standard, active, a card logo required and
|
|
502
|
-
* carrier text optional). Where the documentation stops and the twin decides: the catalog is that one bundle, and its
|
|
503
|
-
* second_line (required by the served spec, absent from the example) is optional. */
|
|
504
|
-
const PHYSICAL_BUNDLES: Row[] = [
|
|
505
|
-
{ id: 'ics_NLuXJPDYSTjFON', object: 'issuing.physical_bundle', livemode: false, name: 'US Visa Credit White', features: { card_logo: 'required', carrier_text: 'optional', second_line: 'optional' }, status: 'active', type: 'standard' },
|
|
506
|
-
];
|
|
507
|
-
const listBundles: Semantics = async (ctx) =>
|
|
508
|
-
ctx.reply({ object: 'list', url: '/v1/issuing/physical_bundles', has_more: false, data: PHYSICAL_BUNDLES.filter((b) => (ctx.params.status === undefined || b.status === ctx.params.status) && (ctx.params.type === undefined || b.type === ctx.params.type)) });
|
|
509
|
-
const retrieveBundle: Semantics = async (ctx) => {
|
|
510
|
-
const b = PHYSICAL_BUNDLES.find((x) => x.id === at(ctx, 'physical_bundle'));
|
|
511
|
-
return b ? ctx.reply(b) : fail(ctx, `No such physical bundle: '${at(ctx, 'physical_bundle')}'`, 404, 'resource_missing');
|
|
512
|
-
};
|
|
513
|
-
|
|
514
|
-
const createDesign: Semantics = async (ctx) => {
|
|
515
|
-
const bundle = typeof ctx.params.physical_bundle === 'string' ? ctx.params.physical_bundle : '';
|
|
516
|
-
if (!bundle) return fail(ctx, 'Missing required param: physical_bundle.', 400, 'parameter_missing');
|
|
517
|
-
if (!PHYSICAL_BUNDLES.some((b) => b.id === bundle)) return fail(ctx, `No such physical bundle: '${bundle}'`, 400, 'resource_missing');
|
|
518
|
-
const logo = filePurposeRefused(ctx, 'card_logo', ['issuing_logo']);
|
|
519
|
-
if (logo) return logo;
|
|
520
|
-
return ctx.reply(
|
|
521
|
-
await created(ctx, PD, {}, {
|
|
522
|
-
livemode: false, status: 'review', physical_bundle: bundle,
|
|
523
|
-
name: ctx.params.name ?? null, lookup_key: ctx.params.lookup_key ?? null,
|
|
524
|
-
card_logo: (ctx.params.card_logo as string) ?? null, carrier_text: (ctx.params.carrier_text as object) ?? null,
|
|
525
|
-
preferences: { is_default: false, is_platform_default: null },
|
|
526
|
-
rejection_reasons: { card_logo: [], carrier_text: [] },
|
|
527
|
-
metadata: ctx.params.metadata && typeof ctx.params.metadata === 'object' ? ctx.params.metadata : {},
|
|
528
|
-
}),
|
|
529
|
-
);
|
|
530
|
-
};
|
|
531
|
-
|
|
532
|
-
const listDesigns: Semantics = async (ctx) =>
|
|
533
|
-
list(ctx, PD, where(ctx, newest(ctx, PD), {
|
|
534
|
-
status: (d, v) => d.status === v,
|
|
535
|
-
lookup_keys: (d, v) => (Array.isArray(v) ? v.map(String).includes(String(d.lookup_key)) : String(d.lookup_key) === String(v)),
|
|
536
|
-
}));
|
|
537
|
-
|
|
538
|
-
const updateDesign: Semantics = async (ctx) => {
|
|
539
|
-
const id = at(ctx, 'personalization_design');
|
|
540
|
-
if (!ctx.get(PD, id)) return designMissing(ctx, id);
|
|
541
|
-
const logo = filePurposeRefused(ctx, 'card_logo', ['issuing_logo']);
|
|
542
|
-
if (logo) return logo;
|
|
543
|
-
return ctx.reply(await ctx.write(PD, id, ctx.params, 'personalization_design.updated'));
|
|
544
|
-
};
|
|
545
|
-
|
|
546
|
-
function review(action: 'activate' | 'deactivate' | 'reject', operationId: string): Semantics {
|
|
547
|
-
return async (ctx) => {
|
|
548
|
-
const id = at(ctx, 'personalization_design');
|
|
549
|
-
const d = ctx.get(PD, id);
|
|
550
|
-
if (!d) return designMissing(ctx, id);
|
|
551
|
-
const refused = ctx.legal(PD, 'status', operationId, d.status, undefined, id);
|
|
552
|
-
if (refused) return ctx.refuse(refused);
|
|
553
|
-
const status = action === 'activate' ? 'active' : action === 'deactivate' ? 'inactive' : 'rejected';
|
|
554
|
-
// a rejection records its reasons: the reject helper's required rejection_reasons, "The reason(s) the
|
|
555
|
-
// personalization design was rejected" (the served spec), per card_logo and carrier_text
|
|
556
|
-
const given = ctx.params.rejection_reasons && typeof ctx.params.rejection_reasons === 'object' ? (ctx.params.rejection_reasons as Row) : undefined;
|
|
557
|
-
if (action === 'reject' && !given) return fail(ctx, 'Missing required param: rejection_reasons.', 400, 'parameter_missing');
|
|
558
|
-
const reasons = action === 'reject' ? { rejection_reasons: { card_logo: Array.isArray(given!.card_logo) ? given!.card_logo : [], carrier_text: Array.isArray(given!.carrier_text) ? given!.carrier_text : [] } } : {};
|
|
559
|
-
return ctx.reply(await ctx.write(PD, id, { status, ...reasons }, `personalization_design.${action}`));
|
|
560
|
-
};
|
|
71
|
+
const controls = normalizeSpendingControls(fields.spending_controls, String(card.currency), card.spending_controls as Row);
|
|
72
|
+
if (controls.error) return send(ctx, controls.error);
|
|
73
|
+
return ctx.reply(ctx.expand(CARD, await ctx.write(CARD, id, { ...fields, ...(controls.controls ? { spending_controls: controls.controls } : {}) }, 'issuing.card.update')));
|
|
561
74
|
}
|
|
562
|
-
|
|
563
|
-
export const issuing: Record<string, Semantics> = {
|
|
564
|
-
PostIssuingCardholders: createCardholder,
|
|
565
|
-
PostIssuingCardholdersCardholder: updateCardholder,
|
|
566
|
-
PostIssuingCards: createCard,
|
|
567
|
-
GetIssuingCards: listCards,
|
|
568
|
-
PostIssuingCardsCard: updateCard,
|
|
569
|
-
GetIssuingAuthorizations: listAuthorizations,
|
|
570
|
-
GetIssuingAuthorizationsAuthorization: retrieveAuthorization,
|
|
571
|
-
PostIssuingAuthorizationsAuthorization: updateAuthorization,
|
|
572
|
-
PostIssuingAuthorizationsAuthorizationApprove: decide(true),
|
|
573
|
-
PostIssuingAuthorizationsAuthorizationDecline: decide(false),
|
|
574
|
-
PostTestHelpersIssuingAuthorizations: present,
|
|
575
|
-
PostTestHelpersIssuingAuthorizationsAuthorizationCapture: captureAuthorization,
|
|
576
|
-
PostIssuingTransactionsTransaction: updateTransaction,
|
|
577
|
-
PostTestHelpersIssuingTransactionsCreateForceCapture: forceCapture,
|
|
578
|
-
PostIssuingDisputes: openDispute,
|
|
579
|
-
PostIssuingDisputesDisputeSubmit: submitDispute,
|
|
580
|
-
PostIssuingDisputesDispute: updateDispute,
|
|
581
|
-
PostIssuingTokensToken: updateToken,
|
|
582
|
-
GetIssuingPhysicalBundles: listBundles,
|
|
583
|
-
GetIssuingPhysicalBundlesPhysicalBundle: retrieveBundle,
|
|
584
|
-
PostIssuingPersonalizationDesigns: createDesign,
|
|
585
|
-
GetIssuingPersonalizationDesigns: listDesigns,
|
|
586
|
-
PostIssuingPersonalizationDesignsPersonalizationDesign: updateDesign,
|
|
587
|
-
PostTestHelpersIssuingPersonalizationDesignsPersonalizationDesignActivate: review('activate', 'PostTestHelpersIssuingPersonalizationDesignsPersonalizationDesignActivate'),
|
|
588
|
-
PostTestHelpersIssuingPersonalizationDesignsPersonalizationDesignDeactivate: review('deactivate', 'PostTestHelpersIssuingPersonalizationDesignsPersonalizationDesignDeactivate'),
|
|
589
|
-
PostTestHelpersIssuingPersonalizationDesignsPersonalizationDesignReject: review('reject', 'PostTestHelpersIssuingPersonalizationDesignsPersonalizationDesignReject'),
|
|
590
|
-
};
|