@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
|
@@ -1,306 +1,31 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
const INV = 'invoice';
|
|
13
|
-
const invoiceMissing = (ctx: SemanticsContext, id: string): Response => fail(ctx, `No such invoice: '${id}'`, 404, 'resource_missing');
|
|
14
|
-
const linesOf = (inv: Row): Row[] => (inv.lines as { data?: Row[] } | undefined)?.data ?? [];
|
|
15
|
-
const sumLines = (lines: Row[]): number => lines.reduce((s, l) => s + (Number(l.amount) || 0), 0);
|
|
16
|
-
|
|
17
|
-
/** Write a draft's line list and the totals derived from it. */
|
|
18
|
-
async function writeLines(ctx: SemanticsContext, invoiceId: string, next: Row[]): Promise<Row> {
|
|
19
|
-
const subtotal = sumLines(next);
|
|
20
|
-
const lines = { object: 'list', data: next, has_more: false, total_count: next.length, url: `/v1/invoices/${invoiceId}/lines` };
|
|
21
|
-
return ctx.write(INV, invoiceId, { lines, subtotal, total: subtotal, amount_due: subtotal, amount_remaining: subtotal }, 'invoice.update');
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Paying an invoice with nothing to pay it: "If not set, defaults to the subscription's default payment method, if any,
|
|
25
|
-
* or to the default payment method in the customer's invoice settings" (docs.stripe.com/api/invoices/object,
|
|
26
|
-
* default_payment_method), and none of them is set. */
|
|
27
|
-
function nothingToPayWith(ctx: SemanticsContext): Response {
|
|
28
|
-
return fail(ctx, 'This customer has no attached payment source or default payment method. Please consider adding a default payment method.', 400, 'missing');
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** A subscription waiting on the invoice paid through the invoice's own pay (incomplete on its first, past_due on a
|
|
32
|
-
* failed renewal) is active again. */
|
|
33
|
-
async function activatedByPayment(ctx: SemanticsContext, subId: string): Promise<void> {
|
|
34
|
-
await ctx.write('subscription', subId, { status: 'active' }, 'subscription.update');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** A draft's line no invoice item made (a subscription's) stays as it is when an item changes. */
|
|
38
|
-
function unlinkedLine(line: Row): Row {
|
|
39
|
-
return line;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** A preview's proration for a change at proration_date. Where the documentation stops and the twin decides: half the
|
|
43
|
-
* period's charge credited as unused time and half charged as remaining time. */
|
|
44
|
-
function prorationLines(out: Row[], subtotal: number, currency: string, now: number, period: number): void {
|
|
45
|
-
const half = Math.round(subtotal / 2);
|
|
46
|
-
out.unshift({ id: 'il_twin_proration_unused', object: 'line_item', type: 'invoiceitem', amount: -half, currency, proration: true, period: { start: now, end: period }, description: 'Unused time (proration credit)' });
|
|
47
|
-
out.push({ id: 'il_twin_proration_remaining', object: 'line_item', type: 'invoiceitem', amount: half, currency, proration: true, period: { start: now, end: period }, description: 'Remaining time (proration charge)' });
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Re-derive a draft's lines from the items still on it: an item that changed or was deleted moves the invoice with it. */
|
|
51
|
-
async function resyncDraftLines(ctx: SemanticsContext, invoiceId: string): Promise<void> {
|
|
52
|
-
const inv = ctx.get(INV, invoiceId);
|
|
53
|
-
if (!inv || inv.status !== 'draft') return;
|
|
54
|
-
const next: Row[] = [];
|
|
55
|
-
for (const line of linesOf(inv)) {
|
|
56
|
-
const itemId = typeof line.invoice_item === 'string' ? line.invoice_item : '';
|
|
57
|
-
if (!itemId) next.push(unlinkedLine(line));
|
|
58
|
-
const item = itemId ? ctx.get('invoiceitem', itemId) : undefined;
|
|
59
|
-
if (item) next.push({ ...line, amount: Number(item.amount) || 0, description: (item.description as string) ?? null });
|
|
60
|
-
}
|
|
61
|
-
await writeLines(ctx, invoiceId, next);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// ── invoice items: a price-based item takes its amount from the price; one naming a draft invoice
|
|
65
|
-
// lands on its lines at once ──
|
|
66
|
-
|
|
67
|
-
/** An item priced by a Price: its unit amount times the quantity, in its currency (docs.stripe.com/api/invoiceitems/create#create_invoiceitem-pricing). */
|
|
68
|
-
function pricedItem(ctx: SemanticsContext, priceRef: string, qty: number): { amount: number; currency: string | undefined } | Response {
|
|
69
|
-
const price = ctx.get('price', priceRef);
|
|
70
|
-
if (!price) return fail(ctx, `No such price: '${priceRef}'`, 400, 'resource_missing');
|
|
71
|
-
return { amount: priceAmount(price, qty), currency: typeof price.currency === 'string' ? price.currency : undefined };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const createItem: Semantics = async (ctx) => {
|
|
75
|
-
const params = ctx.params;
|
|
76
|
-
const now = Number(ctx.now());
|
|
77
|
-
const qty = Number(params.quantity) || 1;
|
|
78
|
-
// the served version names the price in `pricing[price]` ("The pricing information for the invoice item", the served
|
|
79
|
-
// spec); an older version sends `price`. It is kept as the item's price, which the answer renders as `pricing`
|
|
80
|
-
// (stripe-version.ts)
|
|
81
|
-
const { pricing: pricingParam, ...stored } = params;
|
|
82
|
-
const given = (pricingParam as Row | undefined)?.price;
|
|
83
|
-
const priceRef = typeof given === 'string' && given ? given : typeof params.price === 'string' ? params.price : '';
|
|
84
|
-
const priced = priceRef ? pricedItem(ctx, priceRef, qty) : undefined;
|
|
85
|
-
if (priced instanceof Response) return priced;
|
|
86
|
-
const resolvedAmount = priced?.amount;
|
|
87
|
-
let resolvedCurrency = priced?.currency;
|
|
88
|
-
// only a draft invoice takes new items
|
|
89
|
-
const invoiceRef = typeof params.invoice === 'string' ? params.invoice : '';
|
|
90
|
-
let target: Row | undefined;
|
|
91
|
-
if (invoiceRef) {
|
|
92
|
-
target = ctx.get(INV, invoiceRef);
|
|
93
|
-
if (!target) return invoiceMissing(ctx, invoiceRef);
|
|
94
|
-
if (target.status !== 'draft') return fail(ctx, `This invoice cannot have invoice items added because it has status ${String(target.status)}.`, 400, 'invoice_not_editable');
|
|
95
|
-
if (resolvedCurrency === undefined && typeof target.currency === 'string') resolvedCurrency = target.currency;
|
|
96
|
-
}
|
|
97
|
-
// an invoice item is dated by `date`, not `created`; its currency is the price's, else the invoice's
|
|
98
|
-
const item = await created(ctx, 'invoiceitem', { ...stored, ...(priceRef ? { price: priceRef } : {}) }, {
|
|
99
|
-
livemode: false, discountable: true, proration: false, quantity: qty, quantity_decimal: String(qty),
|
|
100
|
-
period: { start: now, end: now }, currency: resolvedCurrency ?? 'usd',
|
|
101
|
-
...(resolvedAmount !== undefined ? { amount: resolvedAmount } : {}),
|
|
102
|
-
}, { timeField: 'date' });
|
|
103
|
-
if (target) {
|
|
104
|
-
await writeLines(ctx, invoiceRef, [...linesOf(target), {
|
|
105
|
-
id: `il_${String(item.id)}`, object: 'line_item', type: 'invoiceitem', amount: Number(item.amount) || 0, currency: String(target.currency ?? item.currency ?? 'usd'),
|
|
106
|
-
quantity: qty, proration: false, invoice_item: item.id, price: item.price ?? null,
|
|
107
|
-
period: item.period ?? { start: now, end: now }, description: (item.description as string) ?? null,
|
|
108
|
-
}]);
|
|
109
|
-
}
|
|
110
|
-
return ctx.reply(item);
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
const itemMissing = (ctx: SemanticsContext, id: string): Response => fail(ctx, `No such invoiceitem: '${id}'`, 404, 'resource_missing');
|
|
114
|
-
|
|
115
|
-
const retrieveItem: Semantics = async (ctx) => {
|
|
116
|
-
const it = ctx.get('invoiceitem', at(ctx, 'invoiceitem'));
|
|
117
|
-
return it ? ctx.reply(it) : itemMissing(ctx, at(ctx, 'invoiceitem'));
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
const updateItem: Semantics = async (ctx) => {
|
|
121
|
-
const id = at(ctx, 'invoiceitem');
|
|
122
|
-
const it = ctx.get('invoiceitem', id);
|
|
123
|
-
if (!it) return itemMissing(ctx, id);
|
|
124
|
-
const updated = await ctx.write('invoiceitem', id, ctx.params, 'invoiceitem.update');
|
|
125
|
-
if (typeof it.invoice === 'string') await resyncDraftLines(ctx, it.invoice);
|
|
126
|
-
return ctx.reply(updated);
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
// an item is deleted only while its invoice is still editable
|
|
130
|
-
const deleteItem: Semantics = async (ctx) => {
|
|
131
|
-
const id = at(ctx, 'invoiceitem');
|
|
132
|
-
const it = ctx.get('invoiceitem', id);
|
|
133
|
-
if (!it) return itemMissing(ctx, id);
|
|
134
|
-
const onInvoice = typeof it.invoice === 'string' ? ctx.get(INV, it.invoice) : undefined;
|
|
135
|
-
if (onInvoice && onInvoice.status !== 'draft') return fail(ctx, 'You cannot delete an invoice item that is attached to an invoice that is no longer editable.', 400, 'invoice_not_editable');
|
|
136
|
-
await ctx.write('invoiceitem', id, { deleted: true }, 'invoiceitem.delete');
|
|
137
|
-
if (typeof it.invoice === 'string') await resyncDraftLines(ctx, it.invoice);
|
|
138
|
-
return ctx.reply({ id, object: 'invoiceitem', deleted: true });
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
// ── invoices ──
|
|
142
|
-
|
|
143
|
-
/** A draft invoice's fields before anything is on it: what a new invoice and a preview start from. */
|
|
144
|
-
const draft = (now: number): Row => ({
|
|
145
|
-
status: 'draft', livemode: false, currency: 'usd', collection_method: 'charge_automatically',
|
|
146
|
-
auto_advance: false, attempt_count: 0, attempted: false,
|
|
147
|
-
amount_due: 0, amount_paid: 0, amount_remaining: 0, amount_overpaid: 0, amount_paid_off_stripe: 0, amount_shipping: 0,
|
|
148
|
-
subtotal: 0, total: 0, starting_balance: 0, post_payment_credit_notes_amount: 0, pre_payment_credit_notes_amount: 0,
|
|
149
|
-
period_start: now, period_end: now, default_tax_rates: [], discounts: [],
|
|
150
|
-
lines: { object: 'list', data: [], has_more: false, total_count: 0 },
|
|
151
|
-
automatic_tax: { enabled: false, liability: null, status: null },
|
|
152
|
-
status_transitions: { finalized_at: null, marked_uncollectible_at: null, paid_at: null, voided_at: null },
|
|
153
|
-
issuer: { type: 'self' },
|
|
154
|
-
payment_settings: { default_mandate: null, payment_method_options: null, payment_method_types: null },
|
|
155
|
-
// a draft has no PaymentIntent: finalize mints it
|
|
156
|
-
payment_intent: null,
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
// a new invoice pulls in the customer's pending items only when asked: pending_invoice_items_behavior "Defaults to
|
|
160
|
-
// `exclude` if the parameter is omitted" (docs.stripe.com/api/invoices/create)
|
|
161
|
-
const create: Semantics = async (ctx) => {
|
|
162
|
-
const params = ctx.params;
|
|
163
|
-
const now = Number(ctx.now());
|
|
164
|
-
// the lines list names where it can be read, "The URL where this list can be accessed" (served spec, invoice.lines.url)
|
|
165
|
-
const newId = ctx.mint(INV);
|
|
166
|
-
// a send_invoice invoice is due `days_until_due` days "from when the invoice is created" (the same page); the customer's
|
|
167
|
-
// email and name are its own "Until the invoice is finalized, this field will equal customer.email" (served spec,
|
|
168
|
-
// invoice.customer_email; .customer_name likewise)
|
|
169
|
-
const days = Number(params.days_until_due);
|
|
170
|
-
const due = params.collection_method === 'send_invoice' && params.due_date === undefined && Number.isFinite(days) && days > 0 ? { due_date: now + days * 86_400 } : {};
|
|
171
|
-
const holder = typeof params.customer === 'string' ? ctx.get('customer', params.customer) : undefined;
|
|
172
|
-
const snapshot = holder ? { customer_email: holder.email ?? null, customer_name: holder.name ?? null } : {};
|
|
173
|
-
const inv = await created(ctx, INV, { ...params, id: newId }, { ...draft(now), ...due, ...snapshot, lines: { object: 'list', data: [], has_more: false, total_count: 0, url: `/v1/invoices/${newId}/lines` } });
|
|
174
|
-
if (params.pending_invoice_items_behavior !== 'include') return ctx.reply(inv);
|
|
175
|
-
const invoiceId = String(inv.id);
|
|
176
|
-
const customerId = typeof inv.customer === 'string' ? inv.customer : '';
|
|
177
|
-
const pending = customerId ? newest(ctx, 'invoiceitem', 'date').filter((it) => it.customer === customerId && !it.invoice).reverse() : [];
|
|
178
|
-
for (const item of pending) await pullPendingItem(ctx, invoiceId, inv, item, now);
|
|
179
|
-
return ctx.reply(pending.length > 0 ? (ctx.get(INV, invoiceId) ?? inv) : inv);
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
/** A draft invoice another object makes (an accepted quote): its customer's, with an invoice item per line the
|
|
183
|
-
* object gives, and the object named as its parent. Answers the invoice's id. */
|
|
184
|
-
export async function draftInvoiceFor(ctx: SemanticsContext, customer: string, currency: string, parent: Row, lines: Array<{ price: unknown; quantity: number; amount: number; description: unknown }>): Promise<string> {
|
|
185
|
-
const now = Number(ctx.now());
|
|
186
|
-
const id = ctx.mint(INV);
|
|
187
|
-
const holder = ctx.get('customer', customer);
|
|
188
|
-
const inv = await created(ctx, INV, { id, customer, currency, parent }, { ...draft(now), currency, customer_email: holder?.email ?? null, customer_name: holder?.name ?? null, lines: { object: 'list', data: [], has_more: false, total_count: 0, url: `/v1/invoices/${id}/lines` } });
|
|
189
|
-
for (const l of lines) {
|
|
190
|
-
const price = typeof l.price === 'string' ? l.price : l.price && typeof l.price === 'object' ? String((l.price as Row).id) : null;
|
|
191
|
-
const item = await created(ctx, 'invoiceitem', { customer, currency, amount: l.amount, quantity: l.quantity, ...(price ? { price } : {}), description: l.description ?? null }, { date: now, livemode: false, metadata: {}, discountable: true, proration: false, period: { start: now, end: now } });
|
|
192
|
-
await pullPendingItem(ctx, id, inv, item, now);
|
|
193
|
-
}
|
|
194
|
-
return id;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/** A customer's pending invoice item, made before the invoice, pulled onto it (docs.stripe.com/api/invoices/create#create_invoice-pending_invoice_items_behavior). */
|
|
198
|
-
async function pullPendingItem(ctx: SemanticsContext, invoiceId: string, inv: Row, item: Row, now: number): Promise<void> {
|
|
199
|
-
const current = ctx.get(INV, invoiceId) ?? inv;
|
|
200
|
-
await writeLines(ctx, invoiceId, [...linesOf(current), {
|
|
201
|
-
id: `il_${String(item.id)}`, object: 'line_item', type: 'invoiceitem', amount: Number(item.amount) || 0, currency: String(current.currency ?? item.currency ?? 'usd'),
|
|
202
|
-
quantity: Number(item.quantity) || 1, proration: false, invoice_item: item.id, price: item.price ?? null,
|
|
203
|
-
period: item.period ?? { start: now, end: now }, description: (item.description as string) ?? null,
|
|
204
|
-
discountable: item.discountable ?? true, discounts: [], livemode: false, metadata: item.metadata ?? {}, tax_amounts: [], tax_rates: [],
|
|
205
|
-
}]);
|
|
206
|
-
await ctx.write('invoiceitem', String(item.id), { invoice: invoiceId }, 'invoiceitem.update');
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/** The invoice the path names, and the machine's refusal when this operation may not move it. */
|
|
210
|
-
function load(ctx: SemanticsContext, operationId: string): { inv: Row } | { answer: Response } {
|
|
1
|
+
// Stripe's invoices 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 { INV, transitions } from '../engine/invoices.ts';
|
|
6
|
+
import { asBool, cardError, declineFor, nowUnix } from '../engine/stripe.ts';
|
|
7
|
+
import { resourceView, activatedByPayment, at, expanded, finalizeForPayment, finder, invoiceMissing, invoicesLoad, mintCharge, nothingToPayWith, send } from './shared.ts';
|
|
8
|
+
import { draft, prorationLines, sumLines } from '../engine/invoices.ts';
|
|
9
|
+
import { addBillingInterval, applyCouponDiscount, priceAmount } from '../engine/stripe.ts';
|
|
10
|
+
import { created, fail, newest, pendingPreviewLines, pullPendingItem } from './shared.ts';
|
|
11
|
+
export async function GetInvoicesInvoice(ctx: HandlerContext): Promise<Response> {
|
|
211
12
|
const id = at(ctx, 'invoice');
|
|
212
13
|
const inv = ctx.get(INV, id);
|
|
213
|
-
|
|
214
|
-
const refused = ctx.legal(INV, 'status', operationId, inv.status, undefined, id);
|
|
215
|
-
return refused ? { answer: ctx.refuse(refused) } : { inv };
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/** The PaymentIntent a charge_automatically invoice with a balance gets when it is finalized; it
|
|
219
|
-
* waits for confirmation when the invoice already names a default payment method. */
|
|
220
|
-
async function openPaymentIntent(ctx: SemanticsContext, piId: string, inv: Row, invoiceId: string): Promise<void> {
|
|
221
|
-
const defaultPaymentMethod = typeof inv.default_payment_method === 'string' ? inv.default_payment_method : undefined;
|
|
222
|
-
await created(ctx, 'payment_intent', {
|
|
223
|
-
id: piId, amount: Number(inv.amount_due) || 0, currency: String(inv.currency ?? 'usd'), customer: inv.customer, invoice: invoiceId,
|
|
224
|
-
...(defaultPaymentMethod ? { payment_method: defaultPaymentMethod } : {}),
|
|
225
|
-
}, {
|
|
226
|
-
status: defaultPaymentMethod ? 'requires_confirmation' : 'requires_payment_method',
|
|
227
|
-
client_secret: mintClientSecret(piId), livemode: false,
|
|
228
|
-
capture_method: 'automatic', amount_capturable: 0, amount_received: 0, next_action: null,
|
|
229
|
-
automatic_payment_methods: null, payment_method_types: ['card', 'link'], payment_method_options: {},
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/** Whether leaving draft mints a PaymentIntent: only a charge_automatically invoice with a balance. */
|
|
234
|
-
// "Finalizing an invoice does the following: ... It creates an incomplete payment intent for the invoice"
|
|
235
|
-
// (docs.stripe.com/invoicing/integration/workflow-transitions), whichever its collection method
|
|
236
|
-
const collectsThroughIntent = (inv: Row): boolean => inv.status === 'draft' && (Number(inv.amount_due) || 0) > 0;
|
|
237
|
-
|
|
238
|
-
const transitions = (inv: Row): object => (inv.status_transitions as object) ?? {};
|
|
239
|
-
|
|
240
|
-
/** What finalizing assigns: the invoice's `number`, the customer's invoice_prefix and a sequence
|
|
241
|
-
* (https://docs.stripe.com/api/invoices/object#invoice_object-number), and for send_invoice a
|
|
242
|
-
* `due_date` days_until_due after finalization (#invoice_object-due_date). */
|
|
243
|
-
/** A customer's invoice_prefix, "The prefix for the customer used to generate unique invoice numbers"
|
|
244
|
-
* (docs.stripe.com/api/customers/object): the twin's, from the customer's id. */
|
|
245
|
-
export const invoicePrefix = (customerId: string): string => customerId.replace(/[^a-z0-9]/gi, '').slice(-8).toUpperCase() || 'TWIN';
|
|
246
|
-
|
|
247
|
-
export function finalizedFields(ctx: SemanticsContext, inv: Row, now: number): Row {
|
|
248
|
-
const customerId = typeof inv.customer === 'string' ? inv.customer : '';
|
|
249
|
-
const customer = customerId ? ctx.get('customer', customerId) : undefined;
|
|
250
|
-
const prefix = typeof customer?.invoice_prefix === 'string' && customer.invoice_prefix
|
|
251
|
-
? customer.invoice_prefix
|
|
252
|
-
: invoicePrefix(customerId);
|
|
253
|
-
const numbered = ctx.rows(INV).filter((i) => i.customer === customerId && typeof i.number === 'string' && i.id !== inv.id).length;
|
|
254
|
-
const fields: Row = { number: typeof inv.number === 'string' && inv.number ? inv.number : `${prefix}-${String(numbered + 1).padStart(4, '0')}` };
|
|
255
|
-
const days = Number(inv.days_until_due);
|
|
256
|
-
if (inv.collection_method === 'send_invoice' && inv.due_date == null && Number.isFinite(days) && days > 0) fields.due_date = now + days * 86400;
|
|
257
|
-
return fields;
|
|
14
|
+
return inv ? ctx.reply(expanded(ctx, INV, inv)) : invoiceMissing(ctx, id);
|
|
258
15
|
}
|
|
259
16
|
|
|
260
|
-
const finalize: Semantics = async (ctx) => {
|
|
261
|
-
const loaded = load(ctx, 'PostInvoicesInvoiceFinalize');
|
|
262
|
-
if ('answer' in loaded) return loaded.answer;
|
|
263
|
-
const inv = loaded.inv;
|
|
264
|
-
const id = String(inv.id);
|
|
265
|
-
const piId = collectsThroughIntent(inv) ? ctx.mint('payment_intent') : null;
|
|
266
|
-
const body = await ctx.write(INV, id, { ...finalizedFields(ctx, inv, Number(ctx.now())), status: 'open', status_transitions: { ...transitions(inv), finalized_at: ctx.now() }, payment_intent: piId }, 'invoice.finalize');
|
|
267
|
-
// the customer's next_invoice_sequence, "The suffix of the customer's next invoice number" (docs.stripe.com/api/customers/object)
|
|
268
|
-
const customerId = typeof inv.customer === 'string' ? inv.customer : '';
|
|
269
|
-
if (customerId && ctx.get('customer', customerId)) await ctx.write('customer', customerId, { next_invoice_sequence: ctx.rows(INV).filter((i) => i.customer === customerId && typeof i.number === 'string').length + 1 }, 'customer.invoice_sequence');
|
|
270
|
-
if (piId) await openPaymentIntent(ctx, piId, inv, id);
|
|
271
|
-
// a subscription's invoice finalized spends the once coupon it took (renewals.ts spendOnceCoupon)
|
|
272
|
-
await spendOnceCoupon(ctx, inv);
|
|
273
|
-
return ctx.reply(expanded(ctx, INV, body));
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
// sending a draft finalizes it; any other invoice keeps its status and records the send
|
|
277
|
-
const sendInvoice: Semantics = async (ctx) => {
|
|
278
|
-
const loaded = load(ctx, 'PostInvoicesInvoiceSend');
|
|
279
|
-
if ('answer' in loaded) return loaded.answer;
|
|
280
|
-
const inv = loaded.inv;
|
|
281
|
-
const id = String(inv.id);
|
|
282
|
-
const becameOpen = inv.status === 'draft';
|
|
283
|
-
const piId = collectsThroughIntent(inv) ? ctx.mint('payment_intent') : null;
|
|
284
|
-
const now = ctx.now();
|
|
285
|
-
const body = await ctx.write(INV, id, {
|
|
286
|
-
status: becameOpen ? 'open' : (inv.status as string), _sent: true, _sent_at: now,
|
|
287
|
-
status_transitions: { ...transitions(inv), ...(becameOpen ? { finalized_at: now } : {}) },
|
|
288
|
-
...(becameOpen ? { payment_intent: piId } : {}),
|
|
289
|
-
}, 'invoice.sent');
|
|
290
|
-
if (piId) await openPaymentIntent(ctx, piId, inv, id);
|
|
291
|
-
if (becameOpen) await spendOnceCoupon(ctx, inv);
|
|
292
|
-
return ctx.reply(expanded(ctx, INV, body));
|
|
293
|
-
};
|
|
294
|
-
|
|
295
17
|
// paying charges a payment method (the one passed, the invoice's default, or the customer's) unless
|
|
296
18
|
// the payment is recorded out of band; a declining card answers 402 and leaves the invoice open. A
|
|
297
19
|
// paid invoice settles the PaymentIntent finalize opened, and names the charge that took the money.
|
|
298
|
-
|
|
20
|
+
export async function PostInvoicesInvoicePay(ctx: HandlerContext): Promise<Response> {
|
|
299
21
|
const id = at(ctx, 'invoice');
|
|
300
|
-
const
|
|
301
|
-
if (!
|
|
302
|
-
const refused = ctx.legal(INV, 'status', 'PostInvoicesInvoicePay',
|
|
22
|
+
const found = ctx.get(INV, id);
|
|
23
|
+
if (!found) return invoiceMissing(ctx, id);
|
|
24
|
+
const refused = ctx.legal(INV, 'status', 'PostInvoicesInvoicePay', found.status, undefined, id);
|
|
303
25
|
if (refused) return ctx.refuse(refused);
|
|
26
|
+
// paying a draft finalizes it first, as its finalize does (its number, and the PaymentIntent it collects through)
|
|
27
|
+
// source: https://docs.stripe.com/invoicing/integration "To finalize a draft invoice, use the Dashboard, send it to the customer, or pay it."
|
|
28
|
+
const inv = found.status === 'draft' ? await finalizeForPayment(ctx, found, id) : found;
|
|
304
29
|
const params = ctx.params;
|
|
305
30
|
const paidOutOfBand = params.paid_out_of_band !== undefined && asBool(params.paid_out_of_band);
|
|
306
31
|
const payer = ctx.get('customer', String(inv.customer ?? ''));
|
|
@@ -308,7 +33,7 @@ const pay: Semantics = async (ctx) => {
|
|
|
308
33
|
if (!paidOutOfBand && !params.payment_method && !params.source && !inv.default_payment_method && !inv.default_source && !customerDefault) return nothingToPayWith(ctx);
|
|
309
34
|
// what it charges: the method passed, else the invoice's default, its subscription's, then its customer's
|
|
310
35
|
// (docs.stripe.com/api/invoices/pay#pay_invoice-payment_method)
|
|
311
|
-
const subscription = typeof inv.subscription === 'string' ? ctx
|
|
36
|
+
const subscription = typeof inv.subscription === 'string' ? resourceView(ctx, 'subscription', inv.subscription) : undefined;
|
|
312
37
|
const decline = declineFor(finder(ctx), params, { payment_method: inv.default_payment_method ?? subscription?.default_payment_method ?? customerDefault });
|
|
313
38
|
const intent = typeof inv.payment_intent === 'string' ? ctx.get('payment_intent', inv.payment_intent) : undefined;
|
|
314
39
|
if (decline) return send(ctx, cardError(decline, intent ? { payment_intent: intent } : {}));
|
|
@@ -316,10 +41,10 @@ const pay: Semantics = async (ctx) => {
|
|
|
316
41
|
let body = await ctx.write(INV, id, {
|
|
317
42
|
// paid outside Stripe, the invoice counts it as such: `amount_paid_off_stripe`, "Amount, in cents (or local
|
|
318
43
|
// equivalent), that was paid on the invoice outside of Stripe" (served spec, invoice), the only place the served
|
|
319
|
-
// version shows it now that basil renders no `paid_out_of_band` (
|
|
44
|
+
// version shows it now that basil renders no `paid_out_of_band` (../engine/version.ts)
|
|
320
45
|
status: 'paid', paid: true, paid_out_of_band: paidOutOfBand, amount_paid: amountPaid, amount_remaining: 0,
|
|
321
46
|
...(paidOutOfBand ? { amount_paid_off_stripe: amountPaid } : {}),
|
|
322
|
-
status_transitions: { ...transitions(inv), paid_at: ctx.
|
|
47
|
+
status_transitions: { ...transitions(inv), paid_at: nowUnix(ctx.occurredAt) },
|
|
323
48
|
}, 'invoice.pay');
|
|
324
49
|
if (!paidOutOfBand && typeof inv.payment_intent === 'string' && inv.payment_intent) {
|
|
325
50
|
const pi = ctx.get('payment_intent', inv.payment_intent);
|
|
@@ -331,105 +56,63 @@ const pay: Semantics = async (ctx) => {
|
|
|
331
56
|
}
|
|
332
57
|
// a subscription waiting on this invoice (incomplete on its first, past_due on a failed renewal) is active once it is paid
|
|
333
58
|
const subId = typeof inv.subscription === 'string' ? inv.subscription : undefined;
|
|
334
|
-
const sub = subId ? ctx
|
|
59
|
+
const sub = subId ? resourceView(ctx, 'subscription', subId) : undefined;
|
|
335
60
|
if (subId && sub && (sub.status === 'incomplete' || sub.status === 'past_due') && !ctx.legal('subscription', 'status', 'PostInvoicesInvoicePay', sub.status, 'active', subId)) await activatedByPayment(ctx, subId);
|
|
336
|
-
return ctx.reply(expanded(ctx, INV, body));
|
|
337
|
-
}
|
|
61
|
+
return ctx.reply(expanded(ctx, INV, ctx.get(INV, id) ?? body));
|
|
62
|
+
}
|
|
338
63
|
|
|
339
|
-
//
|
|
340
|
-
const deleteDraft: Semantics = async (ctx) => {
|
|
341
|
-
const id = at(ctx, 'invoice');
|
|
342
|
-
const inv = ctx.get(INV, id);
|
|
343
|
-
if (!inv) return invoiceMissing(ctx, id);
|
|
344
|
-
if (inv.status !== 'draft') return fail(ctx, 'You can only delete draft invoices.', 400, 'invoice_not_editable');
|
|
345
|
-
await ctx.write(INV, id, { deleted: true }, 'invoice.deleted');
|
|
346
|
-
return ctx.reply({ id, object: 'invoice', deleted: true });
|
|
347
|
-
};
|
|
64
|
+
// sending a draft finalizes it; any other invoice keeps its status and records the send
|
|
348
65
|
|
|
349
|
-
|
|
350
|
-
const loaded =
|
|
66
|
+
export async function PostInvoicesInvoiceVoid(ctx: HandlerContext): Promise<Response> {
|
|
67
|
+
const loaded = invoicesLoad(ctx, 'PostInvoicesInvoiceVoid');
|
|
351
68
|
if ('answer' in loaded) return loaded.answer;
|
|
352
69
|
const inv = loaded.inv;
|
|
353
|
-
const body = await ctx.write(INV, String(inv.id), { status: '
|
|
70
|
+
const body = await ctx.write(INV, String(inv.id), { status: 'void', status_transitions: { ...transitions(inv), voided_at: nowUnix(ctx.occurredAt) } }, 'invoice.void');
|
|
354
71
|
return ctx.reply(expanded(ctx, INV, body));
|
|
355
|
-
}
|
|
72
|
+
}
|
|
73
|
+
|
|
356
74
|
|
|
357
|
-
const voidInvoice: Semantics = async (ctx) => {
|
|
358
|
-
const loaded = load(ctx, 'PostInvoicesInvoiceVoid');
|
|
359
|
-
if ('answer' in loaded) return loaded.answer;
|
|
360
|
-
const inv = loaded.inv;
|
|
361
|
-
const body = await ctx.write(INV, String(inv.id), { status: 'void', status_transitions: { ...transitions(inv), voided_at: ctx.now() } }, 'invoice.void');
|
|
362
|
-
return ctx.reply(expanded(ctx, INV, body));
|
|
363
|
-
};
|
|
364
75
|
|
|
365
76
|
// the invoice's lines, a page at a time
|
|
366
|
-
const lines: Semantics = async (ctx) => {
|
|
367
|
-
const id = at(ctx, 'invoice');
|
|
368
|
-
const inv = ctx.get(INV, id);
|
|
369
|
-
if (!inv) return invoiceMissing(ctx, id);
|
|
370
|
-
const { page, hasMore } = paginate(linesOf(inv), ctx.params);
|
|
371
|
-
return ctx.reply({ object: 'list', url: path(ctx), has_more: hasMore, data: page.map((l) => ({ invoice: id, ...l })) });
|
|
372
|
-
};
|
|
373
77
|
|
|
374
|
-
//
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
next.push({ id: `il_twin_${id}_${cur.length + i + 1}`, object: 'line_item', amount: price ? priceAmount(price, quantity) : unit, currency, quantity, price: price ?? (priceRef || null), description: typeof e.description === 'string' ? e.description : null, proration: false });
|
|
396
|
-
}
|
|
397
|
-
} else if (kind === 'update') {
|
|
398
|
-
for (const e of entries) {
|
|
399
|
-
const lid = typeof e.id === 'string' ? e.id : '';
|
|
400
|
-
next = next.map((l) => (l.id === lid ? { ...l, ...(e.amount !== undefined ? { amount: Math.trunc(Number(e.amount) || 0) } : {}), ...(e.quantity !== undefined ? { quantity: Math.max(1, Math.trunc(Number(e.quantity) || 1)) } : {}), ...(e.description !== undefined ? { description: e.description } : {}) } : l));
|
|
401
|
-
}
|
|
402
|
-
} else {
|
|
403
|
-
const remove = new Set(entries.map((e) => (typeof e.id === 'string' ? e.id : '')).filter(Boolean));
|
|
404
|
-
next = next.filter((l) => !remove.has(String(l.id)));
|
|
405
|
-
}
|
|
406
|
-
return ctx.reply(await writeLines(ctx, id, next));
|
|
407
|
-
};
|
|
78
|
+
// a new invoice pulls in the customer's pending items only when asked: pending_invoice_items_behavior "Defaults to
|
|
79
|
+
// `exclude` if the parameter is omitted" (docs.stripe.com/api/invoices/create)
|
|
80
|
+
export async function PostInvoices(ctx: HandlerContext): Promise<Response> {
|
|
81
|
+
const params = ctx.params;
|
|
82
|
+
const now = Number(nowUnix(ctx.occurredAt));
|
|
83
|
+
// the lines list names where it can be read, "The URL where this list can be accessed" (served spec, invoice.lines.url)
|
|
84
|
+
const newId = ctx.mint(INV);
|
|
85
|
+
// a send_invoice invoice is due `days_until_due` days "from when the invoice is created" (the same page); the customer's
|
|
86
|
+
// email and name are its own "Until the invoice is finalized, this field will equal customer.email" (served spec,
|
|
87
|
+
// invoice.customer_email; .customer_name likewise)
|
|
88
|
+
const days = Number(params.days_until_due);
|
|
89
|
+
const due = params.collection_method === 'send_invoice' && params.due_date === undefined && Number.isFinite(days) && days > 0 ? { due_date: now + days * 86_400 } : {};
|
|
90
|
+
const holder = typeof params.customer === 'string' ? ctx.get('customer', params.customer) : undefined;
|
|
91
|
+
const snapshot = holder ? { customer_email: holder.email ?? null, customer_name: holder.name ?? null } : {};
|
|
92
|
+
const inv = await created(ctx, INV, { ...params, id: newId }, { ...draft(now), ...due, ...snapshot, lines: { object: 'list', data: [], has_more: false, total_count: 0, url: `/v1/invoices/${newId}/lines` } });
|
|
93
|
+
if (params.pending_invoice_items_behavior !== 'include') return ctx.reply(inv);
|
|
94
|
+
const invoiceId = String(inv.id);
|
|
95
|
+
const customerId = typeof inv.customer === 'string' ? inv.customer : '';
|
|
96
|
+
const pending = customerId ? newest(ctx, 'invoiceitem', 'date').filter((it) => it.customer === customerId && !it.invoice).reverse() : [];
|
|
97
|
+
for (const item of pending) await pullPendingItem(ctx, invoiceId, inv, item, now);
|
|
98
|
+
return ctx.reply(pending.length > 0 ? (ctx.get(INV, invoiceId) ?? inv) : inv);
|
|
408
99
|
}
|
|
409
100
|
|
|
410
|
-
const searchInvoices: Semantics = async (ctx) => search(ctx, INV);
|
|
411
101
|
|
|
412
|
-
const retrieve: Semantics = async (ctx) => {
|
|
413
|
-
const id = at(ctx, 'invoice');
|
|
414
|
-
const inv = ctx.get(INV, id);
|
|
415
|
-
return inv ? ctx.reply(expanded(ctx, INV, inv)) : invoiceMissing(ctx, id);
|
|
416
|
-
};
|
|
417
102
|
|
|
418
103
|
/**
|
|
419
104
|
* A preview of the next invoice for a customer or subscription (create_preview), computed and never
|
|
420
105
|
* stored: the subscription's items at their prices, the customer's pending items, the subscription's
|
|
421
|
-
* discount, and
|
|
422
|
-
* charge for the new one (split evenly: there is no clock here to measure the remaining fraction).
|
|
106
|
+
* discount, and an update's unused-time credit and remaining-time charge at the old and new prices.
|
|
423
107
|
*/
|
|
424
|
-
|
|
108
|
+
export async function PostInvoicesCreatePreview(ctx: HandlerContext): Promise<Response> {
|
|
425
109
|
const params = ctx.params;
|
|
426
|
-
const
|
|
427
|
-
const now = Number(ctx.now());
|
|
110
|
+
const now = Number(nowUnix(ctx.occurredAt));
|
|
428
111
|
let subId = typeof params.subscription === 'string' ? params.subscription : '';
|
|
429
112
|
let customer = typeof params.customer === 'string' ? params.customer : '';
|
|
430
113
|
let sub: Row | undefined;
|
|
431
114
|
if (subId) {
|
|
432
|
-
sub = ctx
|
|
115
|
+
sub = resourceView(ctx, 'subscription', subId);
|
|
433
116
|
if (!sub) return fail(ctx, `No such subscription: '${subId}'`, 404, 'resource_missing');
|
|
434
117
|
customer = customer || String(sub.customer ?? '');
|
|
435
118
|
} else if (customer) {
|
|
@@ -443,17 +126,48 @@ const createPreview: Semantics = async (ctx) => {
|
|
|
443
126
|
// the upcoming invoice bills the subscription's next period, its "subscription renewal charges"
|
|
444
127
|
// (docs.stripe.com/api/invoices/create_preview): from the current period's end, one billing interval long
|
|
445
128
|
const renewsAt = sub && Number(sub.current_period_end) > 0 ? Number(sub.current_period_end) : now;
|
|
446
|
-
|
|
129
|
+
let itemRows = sub ? ctx.rows('subscription_item').filter((r) => r.subscription === subId) : [];
|
|
130
|
+
const details = params.subscription_details as Row | undefined;
|
|
131
|
+
const previousItems = itemRows;
|
|
132
|
+
// source: https://docs.stripe.com/api/invoices/create_preview.md?query=subscription_details "The default value is"
|
|
133
|
+
const behavior = details?.proration_behavior ?? 'create_prorations';
|
|
134
|
+
// source: https://docs.stripe.com/billing/subscriptions/prorations "The prorated amount is calculated as soon as the API updates the subscription."
|
|
135
|
+
// source: https://docs.stripe.com/api/invoices/create_preview.md?query=subscription_details "forces the proration to be calculated as though the update was done at the specified time."
|
|
136
|
+
const prorationTime = details?.proration_date === undefined ? now : Number(details.proration_date);
|
|
137
|
+
const periodStart = Number(sub?.current_period_start ?? now);
|
|
138
|
+
const periodEnd = Number(sub?.current_period_end ?? now);
|
|
139
|
+
// source: https://docs.stripe.com/api/invoices/create_preview.md?query=subscription_details "The time given must be within the current subscription period"
|
|
140
|
+
// source: https://docs.stripe.com/api/invoices/create_preview.md?query=subscription_details "cannot be set to"
|
|
141
|
+
if (details?.proration_date !== undefined && (behavior === 'none' || !sub || !Array.isArray(details.items) || prorationTime < periodStart || prorationTime > periodEnd)) return fail(ctx, 'Invalid subscription_details[proration_date] for this subscription preview.', 400, 'parameter_invalid_integer');
|
|
142
|
+
// source: spec:/paths/~1v1~1invoices~1create_preview/post/requestBody/content/application~1x-www-form-urlencoded/schema/properties/subscription_details/description "The subscription creation or modification params to apply as a preview."
|
|
143
|
+
// Preview changes are local to this calculation: the subscription and its stored items are never changed.
|
|
144
|
+
if (Array.isArray(details?.items)) {
|
|
145
|
+
itemRows = itemRows.map(item => ({ ...item }));
|
|
146
|
+
for (const change of details.items as Row[]) {
|
|
147
|
+
const index = change.id ? itemRows.findIndex(item => item.id === change.id) : -1;
|
|
148
|
+
if (change.id && index < 0) return fail(ctx, `No such subscription_item: '${change.id}'`, 400, 'resource_missing');
|
|
149
|
+
if (asBool(change.deleted)) { if (index >= 0) itemRows.splice(index, 1); continue; }
|
|
150
|
+
const previous = index >= 0 ? itemRows[index]! : {};
|
|
151
|
+
const priceId = typeof change.price === 'string' ? change.price : typeof previous.price === 'string' ? previous.price : (previous.price as Row | undefined)?.id;
|
|
152
|
+
const price = typeof priceId === 'string' ? ctx.get('price', priceId) : undefined;
|
|
153
|
+
if (!price) return fail(ctx, `No such price: '${priceId ?? ''}'`, 400, 'resource_missing');
|
|
154
|
+
const quantity = change.quantity === undefined ? Number(previous.quantity ?? 1) : Number(change.quantity);
|
|
155
|
+
const previewItem = { ...previous, price, quantity };
|
|
156
|
+
if (index >= 0) itemRows[index] = previewItem; else itemRows.push(previewItem);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
447
159
|
const out: Row[] = [];
|
|
448
160
|
let subtotal = 0;
|
|
449
161
|
let period = now + 30 * 24 * 3600;
|
|
450
162
|
if (sub) {
|
|
451
|
-
for (const
|
|
163
|
+
for (const previewItem of itemRows) {
|
|
164
|
+
const priceId = typeof previewItem.price === 'string' ? previewItem.price : String((previewItem.price as Row | undefined)?.id ?? '');
|
|
165
|
+
const quantity = Number(previewItem.quantity) || 0;
|
|
452
166
|
const price = ctx.get('price', priceId);
|
|
453
167
|
const amount = priceAmount(price, quantity);
|
|
454
168
|
const recurring = (price?.recurring as Row | undefined) ?? {};
|
|
455
169
|
period = recurring.interval ? addBillingInterval(renewsAt, recurring.interval as Parameters<typeof addBillingInterval>[1], Number(recurring.interval_count) || 1) : period;
|
|
456
|
-
const item =
|
|
170
|
+
const item = previewItem;
|
|
457
171
|
subtotal += amount;
|
|
458
172
|
out.push({
|
|
459
173
|
id: `il_twin_${priceId}`, object: 'line_item', type: 'subscription', amount, currency,
|
|
@@ -461,17 +175,30 @@ const createPreview: Semantics = async (ctx) => {
|
|
|
461
175
|
period: { start: renewsAt, end: period }, description: `${quantity} × ${(price?.nickname as string) ?? priceId}`,
|
|
462
176
|
});
|
|
463
177
|
}
|
|
464
|
-
if (prorationDate !== undefined && subtotal > 0) prorationLines(out, subtotal, currency, now, period);
|
|
465
178
|
}
|
|
466
|
-
for (const ii of newest(ctx, 'invoiceitem', 'date')) {
|
|
467
|
-
if (ii.customer !== customer || ii.invoice) continue;
|
|
468
|
-
out.push({ id: `il_${String(ii.id)}`, object: 'line_item', type: 'invoiceitem', amount: Number(ii.amount) || 0, currency, quantity: Number(ii.quantity) || 1, proration: false, invoice_item: ii.id, period: ii.period ?? { start: now, end: now }, description: (ii.description as string) ?? null });
|
|
469
|
-
}
|
|
470
|
-
subtotal = sumLines(out);
|
|
471
179
|
const subDiscount = Array.isArray(sub?.discounts) && sub!.discounts.length ? (sub!.discounts as Row[])[0] : undefined;
|
|
472
180
|
const coupon = subDiscount && typeof subDiscount.coupon === 'object' ? (subDiscount.coupon as Row) : undefined;
|
|
473
|
-
|
|
474
|
-
|
|
181
|
+
// source: https://docs.stripe.com/api/invoices/create_preview.md?query=subscription_details "Disable creating prorations in this request."
|
|
182
|
+
// source: https://docs.stripe.com/billing/subscriptions/prorations "creates a non-proration debit, even when the debit covers a partial billing period."
|
|
183
|
+
if (sub && Array.isArray(details?.items) && behavior !== 'none' && sub.status !== 'trialing') {
|
|
184
|
+
const before = previousItems.map(item => {
|
|
185
|
+
const priceId = typeof item.price === 'string' ? item.price : String((item.price as Row).id);
|
|
186
|
+
const price = ctx.get('price', priceId);
|
|
187
|
+
return { id: `il_twin_${priceId}`, object: 'line_item', type: 'subscription', amount: priceAmount(price, Number(item.quantity ?? 1)), currency,
|
|
188
|
+
price: price ?? priceId, quantity: Number(item.quantity ?? 1), subscription: subId, subscription_item: item.id,
|
|
189
|
+
period: { start: item.current_period_start ?? periodStart, end: item.current_period_end ?? periodEnd } };
|
|
190
|
+
});
|
|
191
|
+
const after = out.map(line => ({ ...line, period: { start: periodStart, end: periodEnd } }));
|
|
192
|
+
const prorations = prorationLines(before, after, prorationTime, periodStart, periodEnd, coupon);
|
|
193
|
+
// source: https://docs.stripe.com/api/invoices/create_preview.md?query=subscription_details "Always invoice immediately for prorations."
|
|
194
|
+
if (behavior === 'always_invoice' && prorations.length) out.splice(0, out.length, ...prorations);
|
|
195
|
+
else out.unshift(...prorations);
|
|
196
|
+
}
|
|
197
|
+
out.push(...pendingPreviewLines(ctx, customer, currency, now));
|
|
198
|
+
subtotal = sumLines(out);
|
|
199
|
+
// source: https://docs.stripe.com/billing/subscriptions/prorations "no additional discounts are applied to the proration line items themselves"
|
|
200
|
+
const discountAmount = applyCouponDiscount(Math.max(0, sumLines(out.filter(line => !line.proration))), coupon);
|
|
201
|
+
const total = subtotal - discountAmount;
|
|
475
202
|
return ctx.reply({
|
|
476
203
|
...draft(now),
|
|
477
204
|
// "For preview invoices created using the create preview endpoint, this id will be prefixed with `upcoming_in`"
|
|
@@ -480,66 +207,10 @@ const createPreview: Semantics = async (ctx) => {
|
|
|
480
207
|
object: 'invoice', id: `upcoming_${ctx.mint(INV)}`, customer, subscription: subId || null, currency,
|
|
481
208
|
billing_reason: subId ? 'upcoming' : 'manual',
|
|
482
209
|
created: now, period_start: now, period_end: period,
|
|
483
|
-
subtotal, total, amount_due: total, amount_paid: 0, amount_remaining: total,
|
|
210
|
+
subtotal, total, amount_due: Math.max(0, total), amount_paid: 0, amount_remaining: Math.max(0, total),
|
|
484
211
|
starting_balance: 0, ending_balance: null,
|
|
485
212
|
discounts: discountAmount > 0 && subDiscount ? [subDiscount] : [],
|
|
486
213
|
total_discount_amounts: discountAmount > 0 ? [{ amount: discountAmount, discount: subDiscount?.id ?? null }] : [],
|
|
487
214
|
lines: { object: 'list', url: '/v1/invoices/create_preview/lines', has_more: false, total_count: out.length, data: out },
|
|
488
215
|
});
|
|
489
|
-
};
|
|
490
|
-
|
|
491
|
-
/** An invoice's payment as Stripe answers it (docs.stripe.com/api/invoice-payment/object): the PaymentIntent that
|
|
492
|
-
* collects the invoice, open until the invoice is paid. Where the documentation stops and the twin decides: an
|
|
493
|
-
* invoice has one payment, its default, and the payment's id is the invoice's with `inpay_`. */
|
|
494
|
-
function invoicePayment(inv: Row): Row | undefined {
|
|
495
|
-
if (typeof inv.payment_intent !== 'string') return undefined;
|
|
496
|
-
const paid = inv.status === 'paid';
|
|
497
|
-
const transitions = (inv.status_transitions as Row | undefined) ?? {};
|
|
498
|
-
return {
|
|
499
|
-
id: `inpay_${String(inv.id).replace(/^in_/, '')}`, object: 'invoice_payment', invoice: inv.id, is_default: true, livemode: false,
|
|
500
|
-
amount_requested: Number(inv.amount_due ?? 0), amount_paid: paid ? Number(inv.amount_paid ?? inv.amount_due ?? 0) : null, currency: inv.currency,
|
|
501
|
-
created: inv.created, payment: { type: 'payment_intent', payment_intent: inv.payment_intent },
|
|
502
|
-
status: paid ? 'paid' : inv.status === 'void' ? 'canceled' : 'open',
|
|
503
|
-
status_transitions: { paid_at: paid ? (transitions.paid_at ?? null) : null, canceled_at: inv.status === 'void' ? (transitions.voided_at ?? null) : null },
|
|
504
|
-
};
|
|
505
216
|
}
|
|
506
|
-
|
|
507
|
-
const invoicePayments: Semantics = async (ctx) => {
|
|
508
|
-
const p = ctx.params;
|
|
509
|
-
const wanted = (p.payment as Row | undefined)?.payment_intent;
|
|
510
|
-
const items = newest(ctx, INV)
|
|
511
|
-
.filter((inv) => typeof p.invoice !== 'string' || inv.id === p.invoice)
|
|
512
|
-
.map(invoicePayment).filter((x): x is Row => !!x)
|
|
513
|
-
.filter((x) => typeof wanted !== 'string' || (x.payment as Row).payment_intent === wanted)
|
|
514
|
-
.filter((x) => typeof p.status !== 'string' || x.status === p.status);
|
|
515
|
-
return list(ctx, 'invoice_payment', items);
|
|
516
|
-
};
|
|
517
|
-
|
|
518
|
-
const invoicePaymentOne: Semantics = async (ctx) => {
|
|
519
|
-
const id = at(ctx, 'invoice_payment');
|
|
520
|
-
const found = newest(ctx, INV).map(invoicePayment).find((x) => x?.id === id);
|
|
521
|
-
return found ? ctx.reply(found) : fail(ctx, `No such invoice payment: '${id}'`, 404, 'resource_missing');
|
|
522
|
-
};
|
|
523
|
-
|
|
524
|
-
export const invoices: Record<string, Semantics> = {
|
|
525
|
-
GetInvoicePayments: invoicePayments,
|
|
526
|
-
GetInvoicePaymentsInvoicePayment: invoicePaymentOne,
|
|
527
|
-
PostInvoiceitems: createItem,
|
|
528
|
-
GetInvoiceitemsInvoiceitem: retrieveItem,
|
|
529
|
-
PostInvoiceitemsInvoiceitem: updateItem,
|
|
530
|
-
DeleteInvoiceitemsInvoiceitem: deleteItem,
|
|
531
|
-
PostInvoices: create,
|
|
532
|
-
PostInvoicesCreatePreview: createPreview,
|
|
533
|
-
PostInvoicesInvoiceFinalize: finalize,
|
|
534
|
-
PostInvoicesInvoiceSend: sendInvoice,
|
|
535
|
-
PostInvoicesInvoicePay: pay,
|
|
536
|
-
PostInvoicesInvoiceMarkUncollectible: markUncollectible,
|
|
537
|
-
PostInvoicesInvoiceVoid: voidInvoice,
|
|
538
|
-
DeleteInvoicesInvoice: deleteDraft,
|
|
539
|
-
GetInvoicesInvoiceLines: lines,
|
|
540
|
-
PostInvoicesInvoiceAddLines: editLines('add'),
|
|
541
|
-
PostInvoicesInvoiceUpdateLines: editLines('update'),
|
|
542
|
-
PostInvoicesInvoiceRemoveLines: editLines('remove'),
|
|
543
|
-
GetInvoicesSearch: searchInvoices,
|
|
544
|
-
GetInvoicesInvoice: retrieve,
|
|
545
|
-
};
|