@fantastic.dev/better-stripe 0.23.2 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -2
- package/README.md +46 -5
- package/dist/client/affiliateRecovery/disputes.d.ts +18 -0
- package/dist/client/affiliateRecovery/disputes.d.ts.map +1 -0
- package/dist/client/affiliateRecovery/disputes.js +33 -0
- package/dist/client/affiliateRecovery/disputes.js.map +1 -0
- package/dist/client/affiliateRecovery/engine.d.ts +29 -0
- package/dist/client/affiliateRecovery/engine.d.ts.map +1 -0
- package/dist/client/affiliateRecovery/engine.js +116 -0
- package/dist/client/affiliateRecovery/engine.js.map +1 -0
- package/dist/client/affiliateRecovery/payoutGate.d.ts +24 -0
- package/dist/client/affiliateRecovery/payoutGate.d.ts.map +1 -0
- package/dist/client/affiliateRecovery/payoutGate.js +30 -0
- package/dist/client/affiliateRecovery/payoutGate.js.map +1 -0
- package/dist/client/affiliateRecovery/provider.d.ts +14 -0
- package/dist/client/affiliateRecovery/provider.d.ts.map +1 -0
- package/dist/client/affiliateRecovery/provider.js +96 -0
- package/dist/client/affiliateRecovery/provider.js.map +1 -0
- package/dist/client/affiliateRecovery/review.d.ts +28 -0
- package/dist/client/affiliateRecovery/review.d.ts.map +1 -0
- package/dist/client/affiliateRecovery/review.js +86 -0
- package/dist/client/affiliateRecovery/review.js.map +1 -0
- package/dist/client/affiliateRecovery/validators.d.ts +402 -0
- package/dist/client/affiliateRecovery/validators.d.ts.map +1 -0
- package/dist/client/affiliateRecovery/validators.js +27 -0
- package/dist/client/affiliateRecovery/validators.js.map +1 -0
- package/dist/client/affiliateRecovery.d.ts +131 -0
- package/dist/client/affiliateRecovery.d.ts.map +1 -0
- package/dist/client/affiliateRecovery.js +69 -0
- package/dist/client/affiliateRecovery.js.map +1 -0
- package/dist/client/billing/checkout.d.ts +3 -43
- package/dist/client/billing/checkout.d.ts.map +1 -1
- package/dist/client/billing/checkout.js +5 -260
- package/dist/client/billing/checkout.js.map +1 -1
- package/dist/client/billing/checkoutRequest.d.ts +129 -0
- package/dist/client/billing/checkoutRequest.d.ts.map +1 -0
- package/dist/client/billing/checkoutRequest.js +263 -0
- package/dist/client/billing/checkoutRequest.js.map +1 -0
- package/dist/client/billing/preparedCheckout.d.ts +22 -0
- package/dist/client/billing/preparedCheckout.d.ts.map +1 -0
- package/dist/client/billing/preparedCheckout.js +86 -0
- package/dist/client/billing/preparedCheckout.js.map +1 -0
- package/dist/client/checkoutRequests.d.ts +80 -0
- package/dist/client/checkoutRequests.d.ts.map +1 -0
- package/dist/client/checkoutRequests.js +28 -0
- package/dist/client/checkoutRequests.js.map +1 -0
- package/dist/client/connect/commissionReceipts.d.ts +4 -0
- package/dist/client/connect/commissionReceipts.d.ts.map +1 -0
- package/dist/client/connect/commissionReceipts.js +17 -0
- package/dist/client/connect/commissionReceipts.js.map +1 -0
- package/dist/client/connect/payoutAdoptionTestHelpers.d.ts +958 -2
- package/dist/client/connect/payoutAdoptionTestHelpers.d.ts.map +1 -1
- package/dist/client/connect/payoutCollection.d.ts +21 -0
- package/dist/client/connect/payoutCollection.d.ts.map +1 -0
- package/dist/client/connect/payoutCollection.js +47 -0
- package/dist/client/connect/payoutCollection.js.map +1 -0
- package/dist/client/connect/payoutExecutionTestHelpers.d.ts +1805 -0
- package/dist/client/connect/payoutExecutionTestHelpers.d.ts.map +1 -0
- package/dist/client/connect/payoutExecutionTestHelpers.js +194 -0
- package/dist/client/connect/payoutExecutionTestHelpers.js.map +1 -0
- package/dist/client/connect/payoutSyncTestHelpers.d.ts +479 -1
- package/dist/client/connect/payoutSyncTestHelpers.d.ts.map +1 -1
- package/dist/client/connect/payoutWorker.d.ts +48 -0
- package/dist/client/connect/payoutWorker.d.ts.map +1 -0
- package/dist/client/connect/payoutWorker.js +78 -0
- package/dist/client/connect/payoutWorker.js.map +1 -0
- package/dist/client/connect/payouts.d.ts +3 -1
- package/dist/client/connect/payouts.d.ts.map +1 -1
- package/dist/client/connect/payouts.js +45 -12
- package/dist/client/connect/payouts.js.map +1 -1
- package/dist/client/connect/refundPrincipal.d.ts +4 -0
- package/dist/client/connect/refundPrincipal.d.ts.map +1 -0
- package/dist/client/connect/refundPrincipal.js +21 -0
- package/dist/client/connect/refundPrincipal.js.map +1 -0
- package/dist/client/connect/reversalExecution.d.ts +47 -0
- package/dist/client/connect/reversalExecution.d.ts.map +1 -0
- package/dist/client/connect/reversalExecution.js +84 -0
- package/dist/client/connect/reversalExecution.js.map +1 -0
- package/dist/client/connect/transfers.d.ts +8 -4
- package/dist/client/connect/transfers.d.ts.map +1 -1
- package/dist/client/connect/transfers.js +52 -57
- package/dist/client/connect/transfers.js.map +1 -1
- package/dist/client/index.d.ts +7 -11
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +10 -10
- package/dist/client/index.js.map +1 -1
- package/dist/client/payoutPolicy.d.ts +67 -0
- package/dist/client/payoutPolicy.d.ts.map +1 -1
- package/dist/client/payoutPolicy.js +10 -0
- package/dist/client/payoutPolicy.js.map +1 -1
- package/dist/client/readContracts.d.ts +11 -1
- package/dist/client/readContracts.d.ts.map +1 -1
- package/dist/client/readContracts.js +10 -0
- package/dist/client/readContracts.js.map +1 -1
- package/dist/client/settlement/execution.d.ts.map +1 -1
- package/dist/client/settlement/execution.js +30 -3
- package/dist/client/settlement/execution.js.map +1 -1
- package/dist/client/settlement/reconciliation.d.ts.map +1 -1
- package/dist/client/settlement/reconciliation.js +16 -2
- package/dist/client/settlement/reconciliation.js.map +1 -1
- package/dist/client/webhooks/disputeRecovery.d.ts +4 -0
- package/dist/client/webhooks/disputeRecovery.d.ts.map +1 -0
- package/dist/client/webhooks/disputeRecovery.js +40 -0
- package/dist/client/webhooks/disputeRecovery.js.map +1 -0
- package/dist/client/webhooks/processors.d.ts.map +1 -1
- package/dist/client/webhooks/processors.js +14 -29
- package/dist/client/webhooks/processors.js.map +1 -1
- package/dist/component/_generated/component.d.ts +784 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/affiliateEarnings/mutations.d.ts +22 -0
- package/dist/component/affiliateEarnings/mutations.d.ts.map +1 -0
- package/dist/component/affiliateEarnings/mutations.js +160 -0
- package/dist/component/affiliateEarnings/mutations.js.map +1 -0
- package/dist/component/affiliatePayouts/mutations.d.ts +25 -0
- package/dist/component/affiliatePayouts/mutations.d.ts.map +1 -0
- package/dist/component/affiliatePayouts/mutations.js +104 -0
- package/dist/component/affiliatePayouts/mutations.js.map +1 -0
- package/dist/component/affiliatePayouts/queries.d.ts +23 -0
- package/dist/component/affiliatePayouts/queries.d.ts.map +1 -0
- package/dist/component/affiliatePayouts/queries.js +23 -0
- package/dist/component/affiliatePayouts/queries.js.map +1 -0
- package/dist/component/affiliatePayouts/recipientGuard.d.ts +12 -0
- package/dist/component/affiliatePayouts/recipientGuard.d.ts.map +1 -0
- package/dist/component/affiliatePayouts/recipientGuard.js +26 -0
- package/dist/component/affiliatePayouts/recipientGuard.js.map +1 -0
- package/dist/component/affiliateRecovery/accounting.d.ts +28 -0
- package/dist/component/affiliateRecovery/accounting.d.ts.map +1 -0
- package/dist/component/affiliateRecovery/accounting.js +71 -0
- package/dist/component/affiliateRecovery/accounting.js.map +1 -0
- package/dist/component/affiliateRecovery/mutations.d.ts +65 -0
- package/dist/component/affiliateRecovery/mutations.d.ts.map +1 -0
- package/dist/component/affiliateRecovery/mutations.js +250 -0
- package/dist/component/affiliateRecovery/mutations.js.map +1 -0
- package/dist/component/affiliateRecovery/queries.d.ts +305 -0
- package/dist/component/affiliateRecovery/queries.d.ts.map +1 -0
- package/dist/component/affiliateRecovery/queries.js +113 -0
- package/dist/component/affiliateRecovery/queries.js.map +1 -0
- package/dist/component/affiliateRecovery/schema.d.ts +171 -0
- package/dist/component/affiliateRecovery/schema.d.ts.map +1 -0
- package/dist/component/affiliateRecovery/schema.js +71 -0
- package/dist/component/affiliateRecovery/schema.js.map +1 -0
- package/dist/component/connect/mutations.d.ts +36 -23
- package/dist/component/connect/mutations.d.ts.map +1 -1
- package/dist/component/connect/mutations.js +7 -124
- package/dist/component/connect/mutations.js.map +1 -1
- package/dist/component/connect/queries.d.ts +10 -0
- package/dist/component/connect/queries.d.ts.map +1 -1
- package/dist/component/connect/queries.js +3 -40
- package/dist/component/connect/queries.js.map +1 -1
- package/dist/component/connect/reversalClaims.d.ts +169 -0
- package/dist/component/connect/reversalClaims.d.ts.map +1 -0
- package/dist/component/connect/reversalClaims.js +176 -0
- package/dist/component/connect/reversalClaims.js.map +1 -0
- package/dist/component/connect/reversalDiagnostics.d.ts +59 -0
- package/dist/component/connect/reversalDiagnostics.d.ts.map +1 -0
- package/dist/component/connect/reversalDiagnostics.js +50 -0
- package/dist/component/connect/reversalDiagnostics.js.map +1 -0
- package/dist/component/connect/reversalReceipts.d.ts +27 -0
- package/dist/component/connect/reversalReceipts.d.ts.map +1 -0
- package/dist/component/connect/reversalReceipts.js +85 -0
- package/dist/component/connect/reversalReceipts.js.map +1 -0
- package/dist/component/connect/schema.d.ts +31 -1
- package/dist/component/connect/schema.d.ts.map +1 -1
- package/dist/component/connect/schema.js +3 -1
- package/dist/component/connect/schema.js.map +1 -1
- package/dist/component/connect/validators.d.ts +49 -1
- package/dist/component/connect/validators.d.ts.map +1 -1
- package/dist/component/connect/validators.js +2 -0
- package/dist/component/connect/validators.js.map +1 -1
- package/dist/component/core/mutations.d.ts.map +1 -1
- package/dist/component/core/mutations.js +2 -0
- package/dist/component/core/mutations.js.map +1 -1
- package/dist/component/core/resetSafety.d.ts +4 -0
- package/dist/component/core/resetSafety.d.ts.map +1 -0
- package/dist/component/core/resetSafety.js +54 -0
- package/dist/component/core/resetSafety.js.map +1 -0
- package/dist/component/earnings/triggers.d.ts +828 -2
- package/dist/component/earnings/triggers.d.ts.map +1 -1
- package/dist/component/earnings/triggers.js +2 -0
- package/dist/component/earnings/triggers.js.map +1 -1
- package/dist/component/payoutPolicy/schema.d.ts +2 -0
- package/dist/component/payoutPolicy/schema.d.ts.map +1 -1
- package/dist/component/payoutPolicy/schema.js +3 -1
- package/dist/component/payoutPolicy/schema.js.map +1 -1
- package/dist/component/payoutRelease/mutations.d.ts.map +1 -1
- package/dist/component/payoutRelease/mutations.js +7 -1
- package/dist/component/payoutRelease/mutations.js.map +1 -1
- package/dist/component/payoutRelease/queries.d.ts.map +1 -1
- package/dist/component/payoutRelease/queries.js +3 -1
- package/dist/component/payoutRelease/queries.js.map +1 -1
- package/dist/component/payoutRelease/schema.d.ts +1 -0
- package/dist/component/payoutRelease/schema.d.ts.map +1 -1
- package/dist/component/payoutRelease/schema.js +1 -0
- package/dist/component/payoutRelease/schema.js.map +1 -1
- package/dist/component/payoutSnapshots/localState.d.ts +89 -0
- package/dist/component/payoutSnapshots/localState.d.ts.map +1 -0
- package/dist/component/payoutSnapshots/localState.js +186 -0
- package/dist/component/payoutSnapshots/localState.js.map +1 -0
- package/dist/component/payoutSnapshots/mutations.d.ts +108 -0
- package/dist/component/payoutSnapshots/mutations.d.ts.map +1 -0
- package/dist/component/payoutSnapshots/mutations.js +263 -0
- package/dist/component/payoutSnapshots/mutations.js.map +1 -0
- package/dist/component/payoutSnapshots/revisions.d.ts +12 -0
- package/dist/component/payoutSnapshots/revisions.d.ts.map +1 -0
- package/dist/component/payoutSnapshots/revisions.js +173 -0
- package/dist/component/payoutSnapshots/revisions.js.map +1 -0
- package/dist/component/payoutSnapshots/schema.d.ts +285 -0
- package/dist/component/payoutSnapshots/schema.d.ts.map +1 -0
- package/dist/component/payoutSnapshots/schema.js +108 -0
- package/dist/component/payoutSnapshots/schema.js.map +1 -0
- package/dist/component/payoutSnapshots/scope.d.ts +7 -0
- package/dist/component/payoutSnapshots/scope.d.ts.map +1 -0
- package/dist/component/payoutSnapshots/scope.js +8 -0
- package/dist/component/payoutSnapshots/scope.js.map +1 -0
- package/dist/component/payoutWorker/helpers.d.ts +30 -0
- package/dist/component/payoutWorker/helpers.d.ts.map +1 -0
- package/dist/component/payoutWorker/helpers.js +21 -0
- package/dist/component/payoutWorker/helpers.js.map +1 -0
- package/dist/component/payoutWorker/mutations.d.ts +68 -0
- package/dist/component/payoutWorker/mutations.d.ts.map +1 -0
- package/dist/component/payoutWorker/mutations.js +161 -0
- package/dist/component/payoutWorker/mutations.js.map +1 -0
- package/dist/component/payoutWorker/queries.d.ts +123 -0
- package/dist/component/payoutWorker/queries.d.ts.map +1 -0
- package/dist/component/payoutWorker/queries.js +61 -0
- package/dist/component/payoutWorker/queries.js.map +1 -0
- package/dist/component/payoutWorker/schema.d.ts +91 -0
- package/dist/component/payoutWorker/schema.d.ts.map +1 -0
- package/dist/component/payoutWorker/schema.js +27 -0
- package/dist/component/payoutWorker/schema.js.map +1 -0
- package/dist/component/schema.d.ts +479 -1
- package/dist/component/schema.js +19 -0
- package/dist/component/schema.js.map +1 -1
- package/dist/component/settlement/clawbackSchema.d.ts +161 -0
- package/dist/component/settlement/clawbackSchema.d.ts.map +1 -0
- package/dist/component/settlement/clawbackSchema.js +73 -0
- package/dist/component/settlement/clawbackSchema.js.map +1 -0
- package/dist/component/settlement/clawbacks.d.ts +55 -0
- package/dist/component/settlement/clawbacks.d.ts.map +1 -0
- package/dist/component/settlement/clawbacks.js +146 -0
- package/dist/component/settlement/clawbacks.js.map +1 -0
- package/dist/component/settlement/mutations.d.ts +16 -0
- package/dist/component/settlement/mutations.d.ts.map +1 -1
- package/dist/component/settlement/mutations.js +48 -0
- package/dist/component/settlement/mutations.js.map +1 -1
- package/dist/component/settlement/queries.d.ts +53 -0
- package/dist/component/settlement/queries.d.ts.map +1 -1
- package/dist/component/settlement/queries.js +26 -0
- package/dist/component/settlement/queries.js.map +1 -1
- package/dist/react/workflows/AffiliateRecovery.d.ts +46 -0
- package/dist/react/workflows/AffiliateRecovery.d.ts.map +1 -0
- package/dist/react/workflows/AffiliateRecovery.js +84 -0
- package/dist/react/workflows/AffiliateRecovery.js.map +1 -0
- package/dist/react/workflows/index.d.ts +2 -0
- package/dist/react/workflows/index.d.ts.map +1 -1
- package/dist/react/workflows/index.js +1 -0
- package/dist/react/workflows/index.js.map +1 -1
- package/dist/testing/affiliate-recovery.d.ts +10 -0
- package/dist/testing/affiliate-recovery.d.ts.map +1 -0
- package/dist/testing/affiliate-recovery.js +126 -0
- package/dist/testing/affiliate-recovery.js.map +1 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +1 -0
- package/dist/testing/index.js.map +1 -1
- package/docs/affiliate-clawbacks.md +306 -0
- package/docs/read-contracts.md +48 -0
- package/docs/settlement.md +145 -1
- package/package.json +2 -1
- package/src/client/affiliateRecovery/disputes.ts +62 -0
- package/src/client/affiliateRecovery/engine.ts +196 -0
- package/src/client/affiliateRecovery/payoutGate.ts +65 -0
- package/src/client/affiliateRecovery/provider.ts +127 -0
- package/src/client/affiliateRecovery/review.ts +117 -0
- package/src/client/affiliateRecovery/validators.ts +38 -0
- package/src/client/affiliateRecovery.ts +173 -0
- package/src/client/billing/checkout.ts +11 -359
- package/src/client/billing/checkoutRequest.ts +362 -0
- package/src/client/billing/prepared-checkout.test.ts +430 -0
- package/src/client/billing/preparedCheckout.ts +143 -0
- package/src/client/checkoutRequests.ts +40 -0
- package/src/client/connect/commissionReceipts.test.ts +157 -0
- package/src/client/connect/commissionReceipts.ts +23 -0
- package/src/client/connect/payoutCollection.test.ts +74 -0
- package/src/client/connect/payoutCollection.ts +82 -0
- package/src/client/connect/payoutExecution.test.ts +1 -211
- package/src/client/connect/payoutExecutionTestHelpers.ts +213 -0
- package/src/client/connect/payoutWorker.test.ts +145 -0
- package/src/client/connect/payoutWorker.ts +144 -0
- package/src/client/connect/payouts.ts +75 -15
- package/src/client/connect/refundPrincipal.test.ts +46 -0
- package/src/client/connect/refundPrincipal.ts +26 -0
- package/src/client/connect/reversalExecution.ts +130 -0
- package/src/client/connect/transfers.test.ts +3 -3
- package/src/client/connect/transfers.ts +93 -92
- package/src/client/index.ts +16 -28
- package/src/client/payoutPolicy.ts +26 -1
- package/src/client/readContracts.ts +23 -0
- package/src/client/settlement/execution.test.ts +5 -0
- package/src/client/settlement/execution.ts +50 -3
- package/src/client/settlement/reconciliation.ts +20 -2
- package/src/client/webhooks/disputeRecovery.ts +74 -0
- package/src/client/webhooks/processors.test.ts +36 -53
- package/src/client/webhooks/processors.ts +18 -31
- package/src/component/_generated/api.ts +40 -0
- package/src/component/_generated/component.ts +975 -1
- package/src/component/affiliate-clawbacks.test.ts +370 -0
- package/src/component/affiliate-component-pagination.test.ts +83 -0
- package/src/component/affiliate-direct-payout.test.ts +160 -0
- package/src/component/affiliate-recovery-lifecycle.test.ts +587 -0
- package/src/component/affiliateEarnings/mutations.ts +196 -0
- package/src/component/affiliatePayouts/mutations.ts +121 -0
- package/src/component/affiliatePayouts/queries.ts +28 -0
- package/src/component/affiliatePayouts/recipientGuard.ts +42 -0
- package/src/component/affiliateRecovery/accounting.ts +111 -0
- package/src/component/affiliateRecovery/mutations.ts +275 -0
- package/src/component/affiliateRecovery/queries.ts +138 -0
- package/src/component/affiliateRecovery/schema.ts +86 -0
- package/src/component/connect/mutations.ts +9 -160
- package/src/component/connect/queries.ts +4 -47
- package/src/component/connect/reversalClaims.ts +232 -0
- package/src/component/connect/reversalDiagnostics.ts +61 -0
- package/src/component/connect/reversalReceipts.ts +147 -0
- package/src/component/connect/schema.ts +3 -3
- package/src/component/connect/validators.ts +2 -0
- package/src/component/core/mutations.ts +2 -0
- package/src/component/core/resetSafety.ts +67 -0
- package/src/component/earnings/triggers.ts +2 -0
- package/src/component/payout-recovery-integration.test.ts +172 -0
- package/src/component/payout-reset-safety.test.ts +148 -0
- package/src/component/payout-revisions.test.ts +125 -0
- package/src/component/payout-snapshots.test.ts +395 -0
- package/src/component/payout-worker.test.ts +165 -0
- package/src/component/payoutPolicy/schema.ts +3 -1
- package/src/component/payoutRelease/mutations.ts +10 -1
- package/src/component/payoutRelease/queries.ts +3 -1
- package/src/component/payoutRelease/schema.ts +1 -0
- package/src/component/payoutSnapshots/localState.ts +238 -0
- package/src/component/payoutSnapshots/mutations.ts +275 -0
- package/src/component/payoutSnapshots/revisions.ts +247 -0
- package/src/component/payoutSnapshots/schema.ts +125 -0
- package/src/component/payoutSnapshots/scope.ts +7 -0
- package/src/component/payoutWorker/helpers.ts +30 -0
- package/src/component/payoutWorker/mutations.ts +181 -0
- package/src/component/payoutWorker/queries.ts +64 -0
- package/src/component/payoutWorker/schema.ts +26 -0
- package/src/component/public.test.ts +2 -2
- package/src/component/schema.ts +36 -0
- package/src/component/settlement/clawbackSchema.ts +78 -0
- package/src/component/settlement/clawbacks.ts +233 -0
- package/src/component/settlement/mutations.ts +54 -0
- package/src/component/settlement/queries.ts +35 -0
- package/src/component/summary-arithmetic.test.ts +9 -4
- package/src/react/workflows/AffiliateRecovery.test.tsx +106 -0
- package/src/react/workflows/AffiliateRecovery.tsx +273 -0
- package/src/react/workflows/index.ts +9 -0
- package/src/testing/affiliate-recovery.ts +137 -0
- package/src/testing/index.ts +2 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import { expect, it, vi } from "vitest";
|
|
2
|
+
import type Stripe from "stripe";
|
|
3
|
+
import type { Component, RunCtx } from "../helpers.js";
|
|
4
|
+
import type { CreateCheckoutSessionOptions } from "./checkoutOptions.js";
|
|
5
|
+
|
|
6
|
+
async function contract() {
|
|
7
|
+
const path = "./preparedCheckout.js";
|
|
8
|
+
const mod = await import(path).catch(() => ({}));
|
|
9
|
+
expect(
|
|
10
|
+
mod.prepareCheckoutSession,
|
|
11
|
+
"package-owned preparation is missing",
|
|
12
|
+
).toBeTypeOf("function");
|
|
13
|
+
expect(
|
|
14
|
+
mod.executePreparedCheckoutSession,
|
|
15
|
+
"package-owned prepared execution is missing",
|
|
16
|
+
).toBeTypeOf("function");
|
|
17
|
+
return mod;
|
|
18
|
+
}
|
|
19
|
+
function fixture() {
|
|
20
|
+
const create = vi.fn().mockResolvedValue({
|
|
21
|
+
id: "cs_one",
|
|
22
|
+
status: "open",
|
|
23
|
+
client_secret: "secret_one",
|
|
24
|
+
url: null,
|
|
25
|
+
});
|
|
26
|
+
const provider = {
|
|
27
|
+
checkout: { sessions: { create } },
|
|
28
|
+
prices: { retrieve: vi.fn().mockResolvedValue({ id: "price_one" }) },
|
|
29
|
+
} as unknown as Stripe;
|
|
30
|
+
const runMutation = vi.fn().mockResolvedValue(null);
|
|
31
|
+
const runQuery = vi.fn().mockResolvedValue(null);
|
|
32
|
+
const ctx = { runMutation, runQuery } as unknown as RunCtx;
|
|
33
|
+
const component = {
|
|
34
|
+
billing: {
|
|
35
|
+
mutations: {
|
|
36
|
+
upsertCheckoutSession: {
|
|
37
|
+
[Symbol.for("toReferencePath")]:
|
|
38
|
+
"betterStripe/billing/mutations/upsertCheckoutSession",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
} as unknown as Component;
|
|
43
|
+
const options: CreateCheckoutSessionOptions = {
|
|
44
|
+
userId: "buyer_one",
|
|
45
|
+
stripePriceId: "price_one",
|
|
46
|
+
mode: "subscription",
|
|
47
|
+
accountId: "acct_buyer",
|
|
48
|
+
returnUrl: "https://example.com/return",
|
|
49
|
+
metadata: { intentId: "intent_one", episodeId: "episode_one" },
|
|
50
|
+
trialDays: 0,
|
|
51
|
+
sessionOverrides: {
|
|
52
|
+
allowed_payment_method_types: ["card"],
|
|
53
|
+
subscription_data: {
|
|
54
|
+
metadata: {
|
|
55
|
+
userId: "buyer_one",
|
|
56
|
+
intentId: "intent_one",
|
|
57
|
+
episodeId: "episode_one",
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
return { create, provider, runMutation, runQuery, ctx, component, options };
|
|
63
|
+
}
|
|
64
|
+
it("prepares the exact serializable provider request without a provider or component write", async () => {
|
|
65
|
+
const api = await contract();
|
|
66
|
+
const s = fixture();
|
|
67
|
+
const prepared = await api.prepareCheckoutSession(
|
|
68
|
+
s.provider,
|
|
69
|
+
s.component,
|
|
70
|
+
s.ctx,
|
|
71
|
+
s.options,
|
|
72
|
+
);
|
|
73
|
+
expect(prepared.request).toMatchObject({
|
|
74
|
+
mode: "subscription",
|
|
75
|
+
customer_account: "acct_buyer",
|
|
76
|
+
line_items: [{ price: "price_one", quantity: 1 }],
|
|
77
|
+
metadata: {
|
|
78
|
+
userId: "buyer_one",
|
|
79
|
+
intentId: "intent_one",
|
|
80
|
+
episodeId: "episode_one",
|
|
81
|
+
},
|
|
82
|
+
subscription_data: {
|
|
83
|
+
metadata: {
|
|
84
|
+
userId: "buyer_one",
|
|
85
|
+
intentId: "intent_one",
|
|
86
|
+
episodeId: "episode_one",
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
expect(prepared.request.subscription_data.trial_period_days).toBeUndefined();
|
|
91
|
+
expect(JSON.parse(JSON.stringify(prepared))).toEqual(prepared);
|
|
92
|
+
expect(s.create).not.toHaveBeenCalled();
|
|
93
|
+
expect(s.runMutation).not.toHaveBeenCalled();
|
|
94
|
+
});
|
|
95
|
+
it("executes a persisted request unchanged with explicit idempotency and correlated component metadata", async () => {
|
|
96
|
+
const api = await contract();
|
|
97
|
+
const s = fixture();
|
|
98
|
+
const prepared = JSON.parse(
|
|
99
|
+
JSON.stringify(
|
|
100
|
+
await api.prepareCheckoutSession(
|
|
101
|
+
s.provider,
|
|
102
|
+
s.component,
|
|
103
|
+
s.ctx,
|
|
104
|
+
s.options,
|
|
105
|
+
),
|
|
106
|
+
),
|
|
107
|
+
);
|
|
108
|
+
s.options.metadata = { intentId: "changed" };
|
|
109
|
+
s.options.accountId = "acct_changed";
|
|
110
|
+
s.options.trialDays = 30;
|
|
111
|
+
expect(
|
|
112
|
+
await api.executePreparedCheckoutSession(s.provider, s.component, s.ctx, {
|
|
113
|
+
prepared,
|
|
114
|
+
idempotencyKey: "intent-one-checkout",
|
|
115
|
+
}),
|
|
116
|
+
).toEqual({
|
|
117
|
+
stripeSessionId: "cs_one",
|
|
118
|
+
clientSecret: "secret_one",
|
|
119
|
+
url: undefined,
|
|
120
|
+
});
|
|
121
|
+
expect(s.create).toHaveBeenCalledWith(prepared.request, {
|
|
122
|
+
idempotencyKey: "intent-one-checkout",
|
|
123
|
+
});
|
|
124
|
+
expect(s.runMutation.mock.calls[0][1]).toMatchObject({
|
|
125
|
+
userId: "buyer_one",
|
|
126
|
+
accountId: "acct_buyer",
|
|
127
|
+
priceId: "price_one",
|
|
128
|
+
metadata: { intentId: "intent_one", episodeId: "episode_one" },
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it.each([
|
|
133
|
+
{ metadata: { userId: "foreign_buyer" } },
|
|
134
|
+
{ customer_account: "acct_foreign" },
|
|
135
|
+
{ line_items: [{ price: "price_foreign", quantity: 1 }] },
|
|
136
|
+
])(
|
|
137
|
+
"rejects provider overrides that change owned correlation %j before writes",
|
|
138
|
+
async (patch) => {
|
|
139
|
+
const api = await contract();
|
|
140
|
+
const s = fixture();
|
|
141
|
+
s.options.sessionOverrides = { ...s.options.sessionOverrides, ...patch };
|
|
142
|
+
await expect(
|
|
143
|
+
api.prepareCheckoutSession(s.provider, s.component, s.ctx, s.options),
|
|
144
|
+
).rejects.toThrow("Prepared Checkout correlation does not match.");
|
|
145
|
+
expect(s.create).not.toHaveBeenCalled();
|
|
146
|
+
expect(s.runMutation).not.toHaveBeenCalled();
|
|
147
|
+
},
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
it("exposes preparation and execution on the actual BetterStripe client", async () => {
|
|
151
|
+
const { BetterStripe } = await import("../index.js");
|
|
152
|
+
const s = fixture();
|
|
153
|
+
const client = new BetterStripe(s.component as never, {
|
|
154
|
+
STRIPE_SECRET_KEY: "sk_test_fake",
|
|
155
|
+
});
|
|
156
|
+
expect(client.checkoutRequests?.prepare).toBeTypeOf("function");
|
|
157
|
+
expect(client.checkoutRequests?.execute).toBeTypeOf("function");
|
|
158
|
+
});
|
|
159
|
+
it.each([undefined, "", "bad\nkey"])(
|
|
160
|
+
"requires an explicit valid prepared execution key %j",
|
|
161
|
+
async (key) => {
|
|
162
|
+
const api = await contract();
|
|
163
|
+
const s = fixture();
|
|
164
|
+
const prepared = await api.prepareCheckoutSession(
|
|
165
|
+
s.provider,
|
|
166
|
+
s.component,
|
|
167
|
+
s.ctx,
|
|
168
|
+
s.options,
|
|
169
|
+
);
|
|
170
|
+
await expect(
|
|
171
|
+
api.executePreparedCheckoutSession(s.provider, s.component, s.ctx, {
|
|
172
|
+
prepared,
|
|
173
|
+
idempotencyKey: key,
|
|
174
|
+
}),
|
|
175
|
+
).rejects.toThrow();
|
|
176
|
+
expect(s.create).not.toHaveBeenCalled();
|
|
177
|
+
},
|
|
178
|
+
);
|
|
179
|
+
it("does not execute changed prepared parameters or component correlation", async () => {
|
|
180
|
+
const api = await contract();
|
|
181
|
+
const s = fixture();
|
|
182
|
+
const prepared = await api.prepareCheckoutSession(
|
|
183
|
+
s.provider,
|
|
184
|
+
s.component,
|
|
185
|
+
s.ctx,
|
|
186
|
+
s.options,
|
|
187
|
+
);
|
|
188
|
+
for (const changed of [
|
|
189
|
+
{
|
|
190
|
+
...prepared,
|
|
191
|
+
request: { ...prepared.request, return_url: "https://foreign.example" },
|
|
192
|
+
},
|
|
193
|
+
{ ...prepared, row: { ...prepared.row, userId: "foreign" } },
|
|
194
|
+
]) {
|
|
195
|
+
await expect(
|
|
196
|
+
api.executePreparedCheckoutSession(s.provider, s.component, s.ctx, {
|
|
197
|
+
prepared: changed,
|
|
198
|
+
idempotencyKey: "same-intent",
|
|
199
|
+
}),
|
|
200
|
+
).rejects.toThrow();
|
|
201
|
+
}
|
|
202
|
+
expect(s.create).not.toHaveBeenCalled();
|
|
203
|
+
});
|
|
204
|
+
it("retries the exact provider request after a lost component response", async () => {
|
|
205
|
+
const api = await contract();
|
|
206
|
+
const s = fixture();
|
|
207
|
+
const prepared = await api.prepareCheckoutSession(
|
|
208
|
+
s.provider,
|
|
209
|
+
s.component,
|
|
210
|
+
s.ctx,
|
|
211
|
+
s.options,
|
|
212
|
+
);
|
|
213
|
+
s.runMutation.mockRejectedValueOnce(new Error("Lost component response"));
|
|
214
|
+
const args = { prepared, idempotencyKey: "same-intent" };
|
|
215
|
+
await expect(
|
|
216
|
+
api.executePreparedCheckoutSession(s.provider, s.component, s.ctx, args),
|
|
217
|
+
).rejects.toThrow("Lost component response");
|
|
218
|
+
await api.executePreparedCheckoutSession(
|
|
219
|
+
s.provider,
|
|
220
|
+
s.component,
|
|
221
|
+
s.ctx,
|
|
222
|
+
args,
|
|
223
|
+
);
|
|
224
|
+
expect(s.create.mock.calls).toEqual([
|
|
225
|
+
[prepared.request, { idempotencyKey: "same-intent" }],
|
|
226
|
+
[prepared.request, { idempotencyKey: "same-intent" }],
|
|
227
|
+
]);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("freezes fee and descriptor reads before execution even when configuration changes", async () => {
|
|
231
|
+
const api = await contract();
|
|
232
|
+
const s = fixture();
|
|
233
|
+
s.runQuery.mockImplementation(async (ref: Record<symbol, string>) =>
|
|
234
|
+
ref[Symbol.for("toReferencePath")].endsWith(
|
|
235
|
+
"core/queries/getAccountByStripeId",
|
|
236
|
+
)
|
|
237
|
+
? { statementDescriptor: "ORIGINAL" }
|
|
238
|
+
: null,
|
|
239
|
+
);
|
|
240
|
+
const opts = {
|
|
241
|
+
...s.options,
|
|
242
|
+
destinationAccountId: "acct_seller",
|
|
243
|
+
feeConfig: { percent: 10 },
|
|
244
|
+
defaultStatementDescriptorSuffix: "DEFAULT",
|
|
245
|
+
sessionOverrides: { allowed_payment_method_types: ["card"] },
|
|
246
|
+
};
|
|
247
|
+
const prepared = await api.prepareCheckoutSession(
|
|
248
|
+
s.provider,
|
|
249
|
+
s.component,
|
|
250
|
+
s.ctx,
|
|
251
|
+
opts,
|
|
252
|
+
);
|
|
253
|
+
expect(prepared.request.subscription_data).toMatchObject({
|
|
254
|
+
application_fee_percent: 10,
|
|
255
|
+
transfer_data: { destination: "acct_seller" },
|
|
256
|
+
metadata: { bsStatementDescriptor: "ORIGINAL" },
|
|
257
|
+
});
|
|
258
|
+
s.runQuery.mockClear();
|
|
259
|
+
s.runQuery.mockResolvedValue({ statementDescriptor: "CHANGED" });
|
|
260
|
+
opts.feeConfig.percent = 40;
|
|
261
|
+
opts.defaultStatementDescriptorSuffix = "CHANGED";
|
|
262
|
+
await api.executePreparedCheckoutSession(s.provider, s.component, s.ctx, {
|
|
263
|
+
prepared,
|
|
264
|
+
idempotencyKey: "frozen-defaults",
|
|
265
|
+
});
|
|
266
|
+
expect(s.create).toHaveBeenCalledWith(prepared.request, {
|
|
267
|
+
idempotencyKey: "frozen-defaults",
|
|
268
|
+
});
|
|
269
|
+
expect(
|
|
270
|
+
s.runQuery.mock.calls.some(([ref]) =>
|
|
271
|
+
ref[Symbol.for("toReferencePath")].endsWith(
|
|
272
|
+
"core/queries/getAccountByStripeId",
|
|
273
|
+
),
|
|
274
|
+
),
|
|
275
|
+
).toBe(false);
|
|
276
|
+
});
|
|
277
|
+
it("rejects nested reserved split instructions injected by overrides", async () => {
|
|
278
|
+
const api = await contract();
|
|
279
|
+
const s = fixture();
|
|
280
|
+
s.options.sessionOverrides = {
|
|
281
|
+
subscription_data: {
|
|
282
|
+
metadata: {
|
|
283
|
+
userId: "buyer_one",
|
|
284
|
+
intentId: "intent_one",
|
|
285
|
+
episodeId: "episode_one",
|
|
286
|
+
bsChargeType: "separate",
|
|
287
|
+
bsSplit: "forged",
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
await expect(
|
|
292
|
+
api.prepareCheckoutSession(s.provider, s.component, s.ctx, s.options),
|
|
293
|
+
).rejects.toThrow("Prepared Checkout routing metadata does not match.");
|
|
294
|
+
expect(s.create).not.toHaveBeenCalled();
|
|
295
|
+
});
|
|
296
|
+
it("rejects executing a different key from an explicitly prepared key", async () => {
|
|
297
|
+
const api = await contract();
|
|
298
|
+
const s = fixture();
|
|
299
|
+
s.options.idempotencyKey = "original-key";
|
|
300
|
+
const prepared = await api.prepareCheckoutSession(
|
|
301
|
+
s.provider,
|
|
302
|
+
s.component,
|
|
303
|
+
s.ctx,
|
|
304
|
+
s.options,
|
|
305
|
+
);
|
|
306
|
+
await expect(
|
|
307
|
+
api.executePreparedCheckoutSession(s.provider, s.component, s.ctx, {
|
|
308
|
+
prepared,
|
|
309
|
+
idempotencyKey: "changed-key",
|
|
310
|
+
}),
|
|
311
|
+
).rejects.toThrow("Prepared Checkout idempotency key does not match.");
|
|
312
|
+
expect(s.create).not.toHaveBeenCalled();
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("runs the public client contract using resolved fee defaults and the actual provider boundary", async () => {
|
|
316
|
+
const { BetterStripe } = await import("../index.js");
|
|
317
|
+
const s = fixture();
|
|
318
|
+
const client = new BetterStripe(s.component as never, {
|
|
319
|
+
STRIPE_SECRET_KEY: "sk_test_fake",
|
|
320
|
+
platformFee: { percent: 10 },
|
|
321
|
+
statementDescriptorSuffix: "DOJO",
|
|
322
|
+
});
|
|
323
|
+
vi.spyOn(
|
|
324
|
+
client as unknown as { stripe: () => Stripe },
|
|
325
|
+
"stripe",
|
|
326
|
+
).mockReturnValue(s.provider);
|
|
327
|
+
const prepared = await client.checkoutRequests.prepare(s.ctx, {
|
|
328
|
+
...s.options,
|
|
329
|
+
destinationAccountId: "acct_seller",
|
|
330
|
+
sessionOverrides: { allowed_payment_method_types: ["card"] },
|
|
331
|
+
});
|
|
332
|
+
expect(prepared.request.subscription_data).toMatchObject({
|
|
333
|
+
application_fee_percent: 10,
|
|
334
|
+
metadata: { bsStatementDescriptor: "DOJO" },
|
|
335
|
+
});
|
|
336
|
+
await client.checkoutRequests.execute(s.ctx, {
|
|
337
|
+
prepared,
|
|
338
|
+
idempotencyKey: "public-client-intent",
|
|
339
|
+
});
|
|
340
|
+
expect(s.create).toHaveBeenCalledWith(prepared.request, {
|
|
341
|
+
idempotencyKey: "public-client-intent",
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it.each(["subscription", "payment"] as const)(
|
|
346
|
+
"preserves generated split routing through prepared %s execution",
|
|
347
|
+
async (mode) => {
|
|
348
|
+
const api = await contract();
|
|
349
|
+
const s = fixture();
|
|
350
|
+
const opts = {
|
|
351
|
+
...s.options,
|
|
352
|
+
mode,
|
|
353
|
+
sessionOverrides: { allowed_payment_method_types: ["card"] },
|
|
354
|
+
split: [
|
|
355
|
+
{
|
|
356
|
+
destinationAccountId: "acct_seller",
|
|
357
|
+
role: "store" as const,
|
|
358
|
+
percent: 80,
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
destinationAccountId: "acct_affiliate",
|
|
362
|
+
role: "affiliate" as const,
|
|
363
|
+
percent: 10,
|
|
364
|
+
},
|
|
365
|
+
],
|
|
366
|
+
feeConfig: { percent: 5 },
|
|
367
|
+
};
|
|
368
|
+
const prepared = await api.prepareCheckoutSession(
|
|
369
|
+
s.provider,
|
|
370
|
+
s.component,
|
|
371
|
+
s.ctx,
|
|
372
|
+
opts,
|
|
373
|
+
);
|
|
374
|
+
const nested =
|
|
375
|
+
mode === "subscription"
|
|
376
|
+
? prepared.request.subscription_data
|
|
377
|
+
: prepared.request.payment_intent_data;
|
|
378
|
+
expect(nested.metadata).toMatchObject({
|
|
379
|
+
bsChargeType: "separate",
|
|
380
|
+
bsSplit: JSON.stringify(prepared.options.split),
|
|
381
|
+
bsFeeConfig: JSON.stringify(prepared.options.feeConfig),
|
|
382
|
+
});
|
|
383
|
+
expect(nested.transfer_data).toBeUndefined();
|
|
384
|
+
await api.executePreparedCheckoutSession(s.provider, s.component, s.ctx, {
|
|
385
|
+
prepared,
|
|
386
|
+
idempotencyKey: "prepared-split",
|
|
387
|
+
});
|
|
388
|
+
expect(s.create).toHaveBeenCalledWith(prepared.request, {
|
|
389
|
+
idempotencyKey: "prepared-split",
|
|
390
|
+
});
|
|
391
|
+
expect(s.runMutation.mock.calls[0][1]).toMatchObject({
|
|
392
|
+
chargeType: "separate",
|
|
393
|
+
splitRecipients: opts.split,
|
|
394
|
+
});
|
|
395
|
+
},
|
|
396
|
+
);
|
|
397
|
+
it("retains the platform-price provider guard during prepared execution", async () => {
|
|
398
|
+
const api = await contract();
|
|
399
|
+
const s = fixture();
|
|
400
|
+
const opts = {
|
|
401
|
+
...s.options,
|
|
402
|
+
destinationAccountId: "acct_seller",
|
|
403
|
+
sessionOverrides: { allowed_payment_method_types: ["card"] },
|
|
404
|
+
};
|
|
405
|
+
const prepared = await api.prepareCheckoutSession(
|
|
406
|
+
s.provider,
|
|
407
|
+
s.component,
|
|
408
|
+
s.ctx,
|
|
409
|
+
opts,
|
|
410
|
+
);
|
|
411
|
+
s.runQuery.mockImplementation(async (ref: Record<symbol, string>) => {
|
|
412
|
+
const path = ref[Symbol.for("toReferencePath")];
|
|
413
|
+
if (path.endsWith("products/queries/getPriceByStripeId"))
|
|
414
|
+
return { stripeProductId: "prod_one" };
|
|
415
|
+
if (path.endsWith("products/queries/getProductByStripeId"))
|
|
416
|
+
return { accountId: "acct_seller" };
|
|
417
|
+
return null;
|
|
418
|
+
});
|
|
419
|
+
vi.mocked(s.provider.prices.retrieve).mockRejectedValueOnce({
|
|
420
|
+
code: "resource_missing",
|
|
421
|
+
});
|
|
422
|
+
await expect(
|
|
423
|
+
api.executePreparedCheckoutSession(s.provider, s.component, s.ctx, {
|
|
424
|
+
prepared,
|
|
425
|
+
idempotencyKey: "prepared-price-guard",
|
|
426
|
+
}),
|
|
427
|
+
).rejects.toThrow("unavailable on the platform");
|
|
428
|
+
expect(s.create).not.toHaveBeenCalled();
|
|
429
|
+
expect(s.runMutation).not.toHaveBeenCalled();
|
|
430
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type Stripe from "stripe";
|
|
2
|
+
import type { Component, RunCtx } from "../helpers.js";
|
|
3
|
+
import { runMutationOrThrow } from "../helpers.js";
|
|
4
|
+
import { componentRef } from "../webhooks/helpers.js";
|
|
5
|
+
import { idempotencyOptions } from "../core/idempotency.js";
|
|
6
|
+
import { renderCheckoutSession } from "./checkoutRequest.js";
|
|
7
|
+
|
|
8
|
+
export type PreparedCheckoutSession = Awaited<
|
|
9
|
+
ReturnType<typeof renderCheckoutSession>
|
|
10
|
+
>;
|
|
11
|
+
function canonical(value: unknown): string {
|
|
12
|
+
return JSON.stringify(value, (_key, item: unknown) => {
|
|
13
|
+
if (typeof item === "number" && !Number.isFinite(item))
|
|
14
|
+
throw new Error("Checkout request must be serializable.");
|
|
15
|
+
if (["function", "symbol", "bigint"].includes(typeof item))
|
|
16
|
+
throw new Error("Checkout request must be serializable.");
|
|
17
|
+
if (item && typeof item === "object" && !Array.isArray(item))
|
|
18
|
+
return Object.fromEntries(
|
|
19
|
+
Object.entries(item).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),
|
|
20
|
+
);
|
|
21
|
+
return item;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function snapshot<T>(value: T): T {
|
|
25
|
+
return JSON.parse(canonical(value)) as T;
|
|
26
|
+
}
|
|
27
|
+
function validateCorrelation(prepared: PreparedCheckoutSession) {
|
|
28
|
+
const { request, row, options } = prepared;
|
|
29
|
+
const nested =
|
|
30
|
+
request.mode === "subscription"
|
|
31
|
+
? request.subscription_data?.metadata
|
|
32
|
+
: request.mode === "payment"
|
|
33
|
+
? request.payment_intent_data?.metadata
|
|
34
|
+
: undefined;
|
|
35
|
+
if (
|
|
36
|
+
request.mode !== options.mode ||
|
|
37
|
+
request.customer_account !== options.accountId ||
|
|
38
|
+
request.customer_email !==
|
|
39
|
+
(options.accountId ? undefined : options.customerEmail) ||
|
|
40
|
+
request.line_items?.length !== 1 ||
|
|
41
|
+
request.line_items[0]?.price !== options.stripePriceId ||
|
|
42
|
+
canonical(request.metadata) !== canonical(row.metadata) ||
|
|
43
|
+
(request.mode !== "setup" &&
|
|
44
|
+
Object.entries(row.metadata).some(
|
|
45
|
+
([key, value]) => nested?.[key] !== value,
|
|
46
|
+
))
|
|
47
|
+
)
|
|
48
|
+
throw new Error("Prepared Checkout correlation does not match.");
|
|
49
|
+
const generated =
|
|
50
|
+
request.mode === "subscription"
|
|
51
|
+
? prepared.routingMetadata.subscription
|
|
52
|
+
: prepared.routingMetadata.payment;
|
|
53
|
+
const reserved = new Set(
|
|
54
|
+
[...Object.keys(nested ?? {}), ...Object.keys(generated ?? {})].filter(
|
|
55
|
+
(key) => key.startsWith("bs"),
|
|
56
|
+
),
|
|
57
|
+
);
|
|
58
|
+
if ([...reserved].some((key) => nested?.[key] !== generated?.[key]))
|
|
59
|
+
throw new Error("Prepared Checkout routing metadata does not match.");
|
|
60
|
+
}
|
|
61
|
+
/** Read-only preparation. Persist the whole snapshot under host-owned immutable authorization. */
|
|
62
|
+
export async function prepareCheckoutSession(
|
|
63
|
+
stripe: Stripe,
|
|
64
|
+
component: Component,
|
|
65
|
+
ctx: RunCtx,
|
|
66
|
+
opts: Parameters<typeof renderCheckoutSession>[3],
|
|
67
|
+
): Promise<PreparedCheckoutSession> {
|
|
68
|
+
const prepared = snapshot(
|
|
69
|
+
await renderCheckoutSession(stripe, component, ctx, snapshot(opts)),
|
|
70
|
+
);
|
|
71
|
+
validateCorrelation(prepared);
|
|
72
|
+
return prepared;
|
|
73
|
+
}
|
|
74
|
+
/** Execute only a trusted server snapshot. Ownership and durable request binding belong to the host. */
|
|
75
|
+
export async function executePreparedCheckoutSession(
|
|
76
|
+
stripe: Stripe,
|
|
77
|
+
component: Component,
|
|
78
|
+
ctx: RunCtx,
|
|
79
|
+
args: { prepared: PreparedCheckoutSession; idempotencyKey: string },
|
|
80
|
+
) {
|
|
81
|
+
const requestOptions = idempotencyOptions(args.idempotencyKey);
|
|
82
|
+
if (!requestOptions)
|
|
83
|
+
throw new Error("Prepared Checkout requires an explicit idempotency key.");
|
|
84
|
+
const prepared = snapshot(args.prepared);
|
|
85
|
+
if (prepared.version !== 1)
|
|
86
|
+
throw new Error("Unsupported prepared Checkout version.");
|
|
87
|
+
if (
|
|
88
|
+
prepared.options.idempotencyKey !== undefined &&
|
|
89
|
+
prepared.options.idempotencyKey !== args.idempotencyKey
|
|
90
|
+
)
|
|
91
|
+
throw new Error("Prepared Checkout idempotency key does not match.");
|
|
92
|
+
validateCorrelation(prepared);
|
|
93
|
+
const expected = snapshot(
|
|
94
|
+
await renderCheckoutSession(
|
|
95
|
+
stripe,
|
|
96
|
+
component,
|
|
97
|
+
ctx,
|
|
98
|
+
prepared.options,
|
|
99
|
+
prepared.descriptor,
|
|
100
|
+
),
|
|
101
|
+
);
|
|
102
|
+
if (canonical(prepared) !== canonical(expected))
|
|
103
|
+
throw new Error(
|
|
104
|
+
"Prepared Checkout request does not match its bound context.",
|
|
105
|
+
);
|
|
106
|
+
return submitPreparedCheckout(
|
|
107
|
+
stripe,
|
|
108
|
+
component,
|
|
109
|
+
ctx,
|
|
110
|
+
prepared,
|
|
111
|
+
args.idempotencyKey,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
/** Shared legacy submission. Its input has already passed the maintained renderer's safeguards. */
|
|
115
|
+
export async function submitPreparedCheckout(
|
|
116
|
+
stripe: Stripe,
|
|
117
|
+
component: Component,
|
|
118
|
+
ctx: RunCtx,
|
|
119
|
+
prepared: PreparedCheckoutSession,
|
|
120
|
+
key?: string,
|
|
121
|
+
) {
|
|
122
|
+
const options = idempotencyOptions(key);
|
|
123
|
+
const session = options
|
|
124
|
+
? await stripe.checkout.sessions.create(prepared.request, options)
|
|
125
|
+
: await stripe.checkout.sessions.create(prepared.request);
|
|
126
|
+
await runMutationOrThrow(
|
|
127
|
+
ctx,
|
|
128
|
+
componentRef(component, "billing/mutations/upsertCheckoutSession"),
|
|
129
|
+
{
|
|
130
|
+
...prepared.row,
|
|
131
|
+
stripeSessionId: session.id,
|
|
132
|
+
mode: prepared.request.mode,
|
|
133
|
+
status: (session.status ?? "open") as "open" | "complete" | "expired",
|
|
134
|
+
clientSecret: session.client_secret ?? undefined,
|
|
135
|
+
url: session.url ?? undefined,
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
return {
|
|
139
|
+
stripeSessionId: session.id,
|
|
140
|
+
clientSecret: session.client_secret ?? undefined,
|
|
141
|
+
url: session.url ?? undefined,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type Stripe from "stripe";
|
|
2
|
+
import type { Component, RunCtx } from "./helpers.js";
|
|
3
|
+
import type { FeeOverride, PlatformFeeConfig } from "./types.js";
|
|
4
|
+
import type { CreateCheckoutSessionOptions } from "./billing/checkoutOptions.js";
|
|
5
|
+
import {
|
|
6
|
+
prepareCheckoutSession,
|
|
7
|
+
executePreparedCheckoutSession,
|
|
8
|
+
} from "./billing/preparedCheckout.js";
|
|
9
|
+
|
|
10
|
+
/** Server-only request preparation and execution, without a browser-owned money API. */
|
|
11
|
+
export class CheckoutRequestClient {
|
|
12
|
+
constructor(
|
|
13
|
+
private component: Component,
|
|
14
|
+
private provider: () => Stripe,
|
|
15
|
+
private resolveFee: (fee?: FeeOverride) => PlatformFeeConfig | undefined,
|
|
16
|
+
private descriptor?: string,
|
|
17
|
+
) {}
|
|
18
|
+
prepare(ctx: RunCtx, opts: CreateCheckoutSessionOptions) {
|
|
19
|
+
const { fee, ...rest } = opts;
|
|
20
|
+
return prepareCheckoutSession(this.provider(), this.component, ctx, {
|
|
21
|
+
...rest,
|
|
22
|
+
feeConfig:
|
|
23
|
+
opts.destinationAccountId || opts.split?.length
|
|
24
|
+
? this.resolveFee(fee)
|
|
25
|
+
: undefined,
|
|
26
|
+
defaultStatementDescriptorSuffix: this.descriptor,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
execute(
|
|
30
|
+
ctx: RunCtx,
|
|
31
|
+
args: Parameters<typeof executePreparedCheckoutSession>[3],
|
|
32
|
+
) {
|
|
33
|
+
return executePreparedCheckoutSession(
|
|
34
|
+
this.provider(),
|
|
35
|
+
this.component,
|
|
36
|
+
ctx,
|
|
37
|
+
args,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|