@fantastic.dev/better-stripe 0.23.1 → 0.24.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 +26 -2
- package/README.md +40 -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/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 +536 -2
- package/dist/client/connect/payoutAdoptionTestHelpers.d.ts.map +1 -1
- package/dist/client/connect/payoutSync.d.ts +32 -0
- package/dist/client/connect/payoutSync.d.ts.map +1 -0
- package/dist/client/connect/payoutSync.js +48 -0
- package/dist/client/connect/payoutSync.js.map +1 -0
- package/dist/client/connect/payoutSyncTestHelpers.d.ts +1509 -0
- package/dist/client/connect/payoutSyncTestHelpers.d.ts.map +1 -0
- package/dist/client/connect/payoutSyncTestHelpers.js +62 -0
- package/dist/client/connect/payoutSyncTestHelpers.js.map +1 -0
- package/dist/client/connect/payoutSyncValidation.d.ts +16 -0
- package/dist/client/connect/payoutSyncValidation.d.ts.map +1 -0
- package/dist/client/connect/payoutSyncValidation.js +54 -0
- package/dist/client/connect/payoutSyncValidation.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 +29 -9
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +9 -6
- package/dist/client/index.js.map +1 -1
- package/dist/client/readContracts.d.ts +8 -0
- package/dist/client/readContracts.d.ts.map +1 -1
- package/dist/client/readContracts.js +7 -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 +560 -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 +11 -151
- package/dist/component/connect/mutations.js.map +1 -1
- package/dist/component/connect/payoutWrites.d.ts +21 -0
- package/dist/component/connect/payoutWrites.d.ts.map +1 -0
- package/dist/component/connect/payoutWrites.js +19 -0
- package/dist/component/connect/payoutWrites.js.map +1 -0
- 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/earnings/triggers.d.ts +454 -2
- package/dist/component/earnings/triggers.d.ts.map +1 -1
- package/dist/component/payoutRelease/mutations.d.ts.map +1 -1
- package/dist/component/payoutRelease/mutations.js +6 -0
- 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/payoutSync/helpers.d.ts +44 -0
- package/dist/component/payoutSync/helpers.d.ts.map +1 -0
- package/dist/component/payoutSync/helpers.js +60 -0
- package/dist/component/payoutSync/helpers.js.map +1 -0
- package/dist/component/payoutSync/mutations.d.ts +51 -0
- package/dist/component/payoutSync/mutations.d.ts.map +1 -0
- package/dist/component/payoutSync/mutations.js +88 -0
- package/dist/component/payoutSync/mutations.js.map +1 -0
- package/dist/component/payoutSync/queries.d.ts +16 -0
- package/dist/component/payoutSync/queries.d.ts.map +1 -0
- package/dist/component/payoutSync/queries.js +11 -0
- package/dist/component/payoutSync/queries.js.map +1 -0
- package/dist/component/payoutSync/schema.d.ts +46 -0
- package/dist/component/payoutSync/schema.d.ts.map +1 -0
- package/dist/component/payoutSync/schema.js +22 -0
- package/dist/component/payoutSync/schema.js.map +1 -0
- package/dist/component/payoutSync/validators.d.ts +137 -0
- package/dist/component/payoutSync/validators.d.ts.map +1 -0
- package/dist/component/payoutSync/validators.js +19 -0
- package/dist/component/payoutSync/validators.js.map +1 -0
- package/dist/component/schema.d.ts +268 -1
- package/dist/component/schema.js +11 -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 +40 -5
- package/docs/settlement.md +17 -0
- 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/connect/commissionReceipts.test.ts +157 -0
- package/src/client/connect/commissionReceipts.ts +23 -0
- package/src/client/connect/payoutSync.test.ts +313 -0
- package/src/client/connect/payoutSync.ts +93 -0
- package/src/client/connect/payoutSyncTestHelpers.ts +82 -0
- package/src/client/connect/payoutSyncValidation.ts +74 -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 +20 -18
- package/src/client/readContracts.ts +12 -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 +34 -0
- package/src/component/_generated/component.ts +696 -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 +13 -190
- package/src/component/connect/payoutWrites.ts +25 -0
- 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/payout-recovery-integration.test.ts +172 -0
- package/src/component/payout-sync.test.ts +117 -0
- package/src/component/payoutRelease/mutations.ts +9 -0
- package/src/component/payoutRelease/queries.ts +3 -1
- package/src/component/payoutSync/helpers.ts +86 -0
- package/src/component/payoutSync/mutations.ts +102 -0
- package/src/component/payoutSync/queries.ts +10 -0
- package/src/component/payoutSync/schema.ts +25 -0
- package/src/component/payoutSync/validators.ts +21 -0
- package/src/component/schema.ts +20 -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,173 @@
|
|
|
1
|
+
import {
|
|
2
|
+
reconcileAffiliatePayout,
|
|
3
|
+
completeAffiliatePayout,
|
|
4
|
+
} from "./affiliateRecovery/payoutGate.js";
|
|
5
|
+
import { verifiedLegacyReview } from "./affiliateRecovery/review.js";
|
|
6
|
+
import { runMutationOrThrow } from "./helpers.js";
|
|
7
|
+
import { createPayout } from "./connect/payouts.js";
|
|
8
|
+
import type Stripe from "stripe";
|
|
9
|
+
import type { PaginationOptions } from "convex/server";
|
|
10
|
+
import type { Component, RunCtx } from "./helpers.js";
|
|
11
|
+
import { componentRef } from "./webhooks/helpers.js";
|
|
12
|
+
import { reconcileObligation } from "./affiliateRecovery/engine.js";
|
|
13
|
+
import type {
|
|
14
|
+
RecoveryObligation,
|
|
15
|
+
RecoveryOperation,
|
|
16
|
+
} from "./affiliateRecovery/provider.js";
|
|
17
|
+
import type { Doc } from "../component/_generated/dataModel.js";
|
|
18
|
+
export type AffiliateRecoveryObligation = RecoveryObligation;
|
|
19
|
+
export type AffiliateRecoveryOperation = RecoveryOperation;
|
|
20
|
+
export type AffiliateRecoveryBalance = Doc<"affiliateRecoveryBalances">;
|
|
21
|
+
export type AffiliateRecoverySnapshot = {
|
|
22
|
+
balance: AffiliateRecoveryBalance | null;
|
|
23
|
+
obligations: {
|
|
24
|
+
page: RecoveryObligation[];
|
|
25
|
+
continueCursor: string;
|
|
26
|
+
isDone: boolean;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
/** Server API. Host must authorize account ownership and operator retries. */
|
|
30
|
+
export class AffiliateRecoveryClient {
|
|
31
|
+
constructor(
|
|
32
|
+
private readonly component: Component,
|
|
33
|
+
private readonly provider: () => Stripe,
|
|
34
|
+
) {}
|
|
35
|
+
async snapshot(
|
|
36
|
+
ctx: RunCtx,
|
|
37
|
+
opts: {
|
|
38
|
+
affiliateAccountId: string;
|
|
39
|
+
currency: string;
|
|
40
|
+
paginationOpts: PaginationOptions;
|
|
41
|
+
},
|
|
42
|
+
): Promise<AffiliateRecoverySnapshot> {
|
|
43
|
+
return ctx.runQuery(
|
|
44
|
+
componentRef(
|
|
45
|
+
this.component,
|
|
46
|
+
"affiliateRecovery/queries/recoverySnapshot",
|
|
47
|
+
),
|
|
48
|
+
opts,
|
|
49
|
+
) as Promise<AffiliateRecoverySnapshot>;
|
|
50
|
+
}
|
|
51
|
+
async obligation(
|
|
52
|
+
ctx: RunCtx,
|
|
53
|
+
opts: { obligationId: string },
|
|
54
|
+
): Promise<RecoveryObligation | null> {
|
|
55
|
+
return ctx.runQuery(
|
|
56
|
+
componentRef(this.component, "affiliateRecovery/queries/getObligation"),
|
|
57
|
+
opts,
|
|
58
|
+
) as Promise<RecoveryObligation | null>;
|
|
59
|
+
}
|
|
60
|
+
async history(
|
|
61
|
+
ctx: RunCtx,
|
|
62
|
+
opts: { obligationId: string; paginationOpts: PaginationOptions },
|
|
63
|
+
) {
|
|
64
|
+
return ctx.runQuery(
|
|
65
|
+
componentRef(
|
|
66
|
+
this.component,
|
|
67
|
+
"affiliateRecovery/queries/operationHistory",
|
|
68
|
+
),
|
|
69
|
+
opts,
|
|
70
|
+
) as Promise<{
|
|
71
|
+
page: RecoveryOperation[];
|
|
72
|
+
continueCursor: string;
|
|
73
|
+
isDone: boolean;
|
|
74
|
+
}>;
|
|
75
|
+
}
|
|
76
|
+
async reviewLegacy(
|
|
77
|
+
ctx: RunCtx,
|
|
78
|
+
opts: {
|
|
79
|
+
obligationId: string;
|
|
80
|
+
reversalIds: string[];
|
|
81
|
+
restorationTransferIds?: string[];
|
|
82
|
+
note: string;
|
|
83
|
+
},
|
|
84
|
+
) {
|
|
85
|
+
const row = await this.obligation(ctx, opts);
|
|
86
|
+
if (!row) throw new Error("Affiliate obligation not found.");
|
|
87
|
+
const review = await verifiedLegacyReview(this.provider(), row, opts);
|
|
88
|
+
await runMutationOrThrow(
|
|
89
|
+
ctx,
|
|
90
|
+
componentRef(this.component, "affiliateRecovery/mutations/reviewLegacy"),
|
|
91
|
+
review,
|
|
92
|
+
);
|
|
93
|
+
return this.obligation(ctx, opts);
|
|
94
|
+
}
|
|
95
|
+
async retry(ctx: RunCtx, opts: { obligationId: string }) {
|
|
96
|
+
return reconcileObligation(
|
|
97
|
+
this.provider(),
|
|
98
|
+
this.component,
|
|
99
|
+
ctx,
|
|
100
|
+
opts.obligationId,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
async reconcilePayouts(
|
|
104
|
+
ctx: RunCtx,
|
|
105
|
+
opts: { paginationOpts: PaginationOptions; state: "pending" | "unknown" },
|
|
106
|
+
) {
|
|
107
|
+
const page = (await ctx.runQuery(
|
|
108
|
+
componentRef(this.component, "affiliatePayouts/queries/pendingPayouts"),
|
|
109
|
+
opts,
|
|
110
|
+
)) as {
|
|
111
|
+
page: Doc<"affiliatePayoutReleases">[];
|
|
112
|
+
continueCursor: string;
|
|
113
|
+
isDone: boolean;
|
|
114
|
+
};
|
|
115
|
+
for (const row of page.page) {
|
|
116
|
+
try {
|
|
117
|
+
// Confirm money already moved even when new debt blocks another attempt.
|
|
118
|
+
const receipt = await reconcileAffiliatePayout(this.provider(), row);
|
|
119
|
+
if (receipt) {
|
|
120
|
+
await completeAffiliatePayout(
|
|
121
|
+
this.component,
|
|
122
|
+
ctx,
|
|
123
|
+
row.operationKey,
|
|
124
|
+
"submitted",
|
|
125
|
+
receipt,
|
|
126
|
+
);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
await createPayout(
|
|
130
|
+
this.provider(),
|
|
131
|
+
ctx,
|
|
132
|
+
{
|
|
133
|
+
stripeAccountId: row.affiliateAccountId,
|
|
134
|
+
currency: row.currency,
|
|
135
|
+
amount: row.amount,
|
|
136
|
+
minimumAmount: row.minimumAmount,
|
|
137
|
+
idempotencyKey: row.operationKey,
|
|
138
|
+
},
|
|
139
|
+
this.component,
|
|
140
|
+
);
|
|
141
|
+
} catch {
|
|
142
|
+
/* The payout gate retains the durable failure/unknown outcome. */
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return page;
|
|
146
|
+
}
|
|
147
|
+
/** Run from a host cron for both pending and unknown outcomes. */
|
|
148
|
+
async reconcilePending(
|
|
149
|
+
ctx: RunCtx,
|
|
150
|
+
opts: { paginationOpts: PaginationOptions; state: "pending" | "unknown" },
|
|
151
|
+
) {
|
|
152
|
+
const page = (await ctx.runQuery(
|
|
153
|
+
componentRef(
|
|
154
|
+
this.component,
|
|
155
|
+
"affiliateRecovery/queries/pendingOperations",
|
|
156
|
+
),
|
|
157
|
+
opts,
|
|
158
|
+
)) as {
|
|
159
|
+
page: RecoveryOperation[];
|
|
160
|
+
continueCursor: string;
|
|
161
|
+
isDone: boolean;
|
|
162
|
+
};
|
|
163
|
+
const results = [];
|
|
164
|
+
for (const op of page.page)
|
|
165
|
+
results.push(await this.retry(ctx, { obligationId: op.obligationId }));
|
|
166
|
+
return { ...page, results };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
export {
|
|
170
|
+
affiliateSnapshotValidator,
|
|
171
|
+
affiliateObligationValidator,
|
|
172
|
+
affiliateOperationValidator,
|
|
173
|
+
} from "./affiliateRecovery/validators.js";
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { reclaimReversalClaim } from "./transfers.js";
|
|
2
|
+
import type Stripe from "stripe";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { findCommissionReversal } from "./commissionReceipts.js";
|
|
5
|
+
import { executeClaimedSlices } from "./reversalExecution.js";
|
|
6
|
+
import type { Component, RunCtx } from "../helpers.js";
|
|
7
|
+
|
|
8
|
+
const component = new Proxy(
|
|
9
|
+
{},
|
|
10
|
+
{
|
|
11
|
+
get: (_t, p) =>
|
|
12
|
+
p === Symbol.for("toReferencePath") ? "betterStripe" : component,
|
|
13
|
+
},
|
|
14
|
+
) as Component;
|
|
15
|
+
const claim = {
|
|
16
|
+
replay: false,
|
|
17
|
+
slices: [
|
|
18
|
+
{
|
|
19
|
+
stripeTransferId: "tr_1",
|
|
20
|
+
from: 0,
|
|
21
|
+
to: 501,
|
|
22
|
+
confirmed: 0,
|
|
23
|
+
commissionTracked: true,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
const fixture = () => {
|
|
28
|
+
const receipts: {
|
|
29
|
+
id: string;
|
|
30
|
+
amount: number;
|
|
31
|
+
metadata: Record<string, string>;
|
|
32
|
+
}[] = [];
|
|
33
|
+
const stripe = {
|
|
34
|
+
transfers: {
|
|
35
|
+
createReversal: vi.fn(
|
|
36
|
+
async (
|
|
37
|
+
_id: string,
|
|
38
|
+
params: { amount: number; metadata: Record<string, string> },
|
|
39
|
+
) => {
|
|
40
|
+
const receipt = { id: "trr_1", ...params };
|
|
41
|
+
receipts.push(receipt);
|
|
42
|
+
return receipt;
|
|
43
|
+
},
|
|
44
|
+
),
|
|
45
|
+
listReversals: vi.fn(() =>
|
|
46
|
+
(async function* () {
|
|
47
|
+
yield* receipts;
|
|
48
|
+
})(),
|
|
49
|
+
),
|
|
50
|
+
retrieve: vi.fn(),
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
const ctx = { runMutation: vi.fn().mockResolvedValue(null) };
|
|
54
|
+
const run = (replay = false, confirmed = 0) =>
|
|
55
|
+
executeClaimedSlices(
|
|
56
|
+
stripe as unknown as Stripe,
|
|
57
|
+
component,
|
|
58
|
+
ctx as unknown as RunCtx,
|
|
59
|
+
"re_1",
|
|
60
|
+
{ ...claim, replay, slices: [{ ...claim.slices[0], confirmed }] },
|
|
61
|
+
);
|
|
62
|
+
return { stripe, ctx, receipts, run };
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
describe("commission reversal receipt reconciliation", () => {
|
|
66
|
+
it("recovers a provider success after the ledger confirmation crashed, even beyond the idempotency window", async () => {
|
|
67
|
+
const f = fixture();
|
|
68
|
+
f.ctx.runMutation.mockRejectedValueOnce(new Error("ledger offline"));
|
|
69
|
+
await expect(f.run()).rejects.toThrow("ledger offline");
|
|
70
|
+
expect(f.receipts).toHaveLength(1);
|
|
71
|
+
await f.run(true);
|
|
72
|
+
await f.run(true, 501);
|
|
73
|
+
expect(f.stripe.transfers.createReversal).toHaveBeenCalledTimes(1);
|
|
74
|
+
expect(f.ctx.runMutation).toHaveBeenLastCalledWith(expect.anything(), {
|
|
75
|
+
stripeTransferId: "tr_1",
|
|
76
|
+
reversedAmount: 501,
|
|
77
|
+
receipt: { stripeReversalId: "trr_1", operationId: "re_1", amount: 501 },
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("does not mistake another operation's aggregate reversal for this receipt", async () => {
|
|
82
|
+
const f = fixture();
|
|
83
|
+
f.receipts.push({
|
|
84
|
+
id: "trr_other",
|
|
85
|
+
amount: 501,
|
|
86
|
+
metadata: { bsReversalOperation: "re_other" },
|
|
87
|
+
});
|
|
88
|
+
await expect(f.run(true, 501)).rejects.toThrow(
|
|
89
|
+
"requires receipt reconciliation",
|
|
90
|
+
);
|
|
91
|
+
expect(f.stripe.transfers.retrieve).not.toHaveBeenCalled();
|
|
92
|
+
expect(f.stripe.transfers.createReversal).not.toHaveBeenCalled();
|
|
93
|
+
expect(f.ctx.runMutation).not.toHaveBeenCalled();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("retries a failed provider attempt with byte-identical parameters and operation identity", async () => {
|
|
97
|
+
const f = fixture();
|
|
98
|
+
f.stripe.transfers.createReversal.mockRejectedValueOnce(
|
|
99
|
+
new Error("network offline"),
|
|
100
|
+
);
|
|
101
|
+
await expect(f.run()).rejects.toThrow("network offline");
|
|
102
|
+
await f.run(true);
|
|
103
|
+
expect(f.stripe.transfers.createReversal.mock.calls[0]).toEqual(
|
|
104
|
+
f.stripe.transfers.createReversal.mock.calls[1],
|
|
105
|
+
);
|
|
106
|
+
expect(f.receipts).toHaveLength(1);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("rejects a mismatching receipt amount without confirming money", async () => {
|
|
110
|
+
const f = fixture();
|
|
111
|
+
f.receipts.push({
|
|
112
|
+
id: "trr_bad",
|
|
113
|
+
amount: 500,
|
|
114
|
+
metadata: { bsReversalOperation: "re_1" },
|
|
115
|
+
});
|
|
116
|
+
await expect(f.run(true)).rejects.toThrow("amount mismatch");
|
|
117
|
+
expect(f.ctx.runMutation).not.toHaveBeenCalled();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("fails explicitly when the bounded receipt search cannot establish success", async () => {
|
|
121
|
+
const f = fixture();
|
|
122
|
+
for (let i = 0; i < 1001; i++)
|
|
123
|
+
f.receipts.push({ id: `trr_${i}`, amount: 1, metadata: {} });
|
|
124
|
+
await expect(
|
|
125
|
+
findCommissionReversal(
|
|
126
|
+
f.stripe as unknown as Stripe,
|
|
127
|
+
"tr_1",
|
|
128
|
+
"re_1",
|
|
129
|
+
501,
|
|
130
|
+
),
|
|
131
|
+
).rejects.toThrow("lookup limit exceeded");
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("operator re-execution delegates tracked commissions to the durable recovery engine", async () => {
|
|
136
|
+
const f = fixture();
|
|
137
|
+
const runQuery = vi
|
|
138
|
+
.fn()
|
|
139
|
+
.mockResolvedValueOnce({
|
|
140
|
+
operationId: "re_1",
|
|
141
|
+
sourceChargeId: "ch_1",
|
|
142
|
+
_creationTime: 0,
|
|
143
|
+
slices: [{ stripeTransferId: "tr_1", from: 0, to: 501 }],
|
|
144
|
+
commissionExposure: { kind: "refund" },
|
|
145
|
+
})
|
|
146
|
+
.mockResolvedValueOnce({ role: "affiliate", reversedAmount: 0 });
|
|
147
|
+
await reclaimReversalClaim(
|
|
148
|
+
f.stripe as unknown as Stripe,
|
|
149
|
+
component,
|
|
150
|
+
{ ...f.ctx, runQuery } as unknown as RunCtx,
|
|
151
|
+
{ operationId: "re_1" },
|
|
152
|
+
);
|
|
153
|
+
expect(f.stripe.transfers.createReversal).not.toHaveBeenCalled();
|
|
154
|
+
expect(runQuery).toHaveBeenLastCalledWith(expect.anything(), {
|
|
155
|
+
sourceChargeId: "ch_1",
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type Stripe from "stripe";
|
|
2
|
+
|
|
3
|
+
/** Locate the exact receipt, never infer operation success from an aggregate. */
|
|
4
|
+
export async function findCommissionReversal(
|
|
5
|
+
stripe: Stripe,
|
|
6
|
+
stripeTransferId: string,
|
|
7
|
+
operationId: string,
|
|
8
|
+
amount: number,
|
|
9
|
+
) {
|
|
10
|
+
let count = 0;
|
|
11
|
+
for await (const receipt of stripe.transfers.listReversals(stripeTransferId, {
|
|
12
|
+
limit: 100,
|
|
13
|
+
})) {
|
|
14
|
+
if (++count > 1000)
|
|
15
|
+
throw new Error("Commission reversal receipt lookup limit exceeded.");
|
|
16
|
+
if (receipt.metadata?.bsReversalOperation === operationId) {
|
|
17
|
+
if (receipt.amount !== amount)
|
|
18
|
+
throw new Error("Commission reversal receipt amount mismatch.");
|
|
19
|
+
return receipt;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
// @vitest-environment edge-runtime
|
|
2
|
+
import { expect, it, vi } from "vitest";
|
|
3
|
+
import type Stripe from "stripe";
|
|
4
|
+
import { makeFunctionReference } from "convex/server";
|
|
5
|
+
import type { RunCtx } from "../helpers.js";
|
|
6
|
+
import {
|
|
7
|
+
component,
|
|
8
|
+
setup,
|
|
9
|
+
operation,
|
|
10
|
+
type Scope,
|
|
11
|
+
} from "./payoutSyncTestHelpers.js";
|
|
12
|
+
const symbol = Symbol.for("toReferencePath");
|
|
13
|
+
it("persists the current automatic payout under the connected scope without requiring mature funding", async () => {
|
|
14
|
+
const s = await setup();
|
|
15
|
+
expect(await s.run()).toMatchObject({ status: "accepted", changed: true });
|
|
16
|
+
expect(s.retrieve).toHaveBeenCalledWith(
|
|
17
|
+
"po_one",
|
|
18
|
+
{},
|
|
19
|
+
{ stripeAccount: "acct_owner" },
|
|
20
|
+
);
|
|
21
|
+
expect(await s.row()).toMatchObject({
|
|
22
|
+
stripePayoutId: "po_one",
|
|
23
|
+
accountId: "acct_owner",
|
|
24
|
+
amount: 500,
|
|
25
|
+
currency: "jpy",
|
|
26
|
+
status: "paid",
|
|
27
|
+
arrivalDate: "1970-01-01T00:03:20.000Z",
|
|
28
|
+
});
|
|
29
|
+
expect(await s.run()).toMatchObject({ status: "accepted", changed: false });
|
|
30
|
+
expect(
|
|
31
|
+
await s.t.run((ctx) => ctx.db.query("payouts").collect()),
|
|
32
|
+
).toHaveLength(1);
|
|
33
|
+
expect(
|
|
34
|
+
await s.t.run((ctx) => ctx.db.query("currencyTotals").unique()),
|
|
35
|
+
).toMatchObject({
|
|
36
|
+
accountId: "acct_owner",
|
|
37
|
+
currency: "jpy",
|
|
38
|
+
paidOut: 500,
|
|
39
|
+
count: 1,
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
it("allows a later authoritative paid-to-failed transition and clears obsolete failure details", async () => {
|
|
43
|
+
const s = await setup();
|
|
44
|
+
await s.run();
|
|
45
|
+
s.set({
|
|
46
|
+
status: "failed",
|
|
47
|
+
failure_code: "account_closed",
|
|
48
|
+
failure_message: "Bank account closed",
|
|
49
|
+
});
|
|
50
|
+
await s.run();
|
|
51
|
+
expect(await s.row()).toMatchObject({
|
|
52
|
+
status: "failed",
|
|
53
|
+
failureCode: "account_closed",
|
|
54
|
+
});
|
|
55
|
+
expect(
|
|
56
|
+
await s.t.run((ctx) => ctx.db.query("currencyTotals").unique()),
|
|
57
|
+
).toMatchObject({ paidOut: 0, count: 1 });
|
|
58
|
+
s.set({ status: "pending", failure_code: null, failure_message: null });
|
|
59
|
+
await s.run();
|
|
60
|
+
expect((await s.row())?.failureCode).toBeUndefined();
|
|
61
|
+
expect((await s.row())?.failureMessage).toBeUndefined();
|
|
62
|
+
});
|
|
63
|
+
it("keeps platform scope explicit and ignores provider metadata as account authority", async () => {
|
|
64
|
+
const s = await setup({ type: "platform" });
|
|
65
|
+
s.set({ automatic: false });
|
|
66
|
+
await s.run();
|
|
67
|
+
expect(s.retrieve).toHaveBeenCalledWith("po_one", {}, {});
|
|
68
|
+
expect(await s.row()).toMatchObject({
|
|
69
|
+
accountId: "platform",
|
|
70
|
+
currency: "jpy",
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
function deferred<T>() {
|
|
74
|
+
let resolve!: (value: T) => void;
|
|
75
|
+
const promise = new Promise<T>((done) => {
|
|
76
|
+
resolve = done;
|
|
77
|
+
});
|
|
78
|
+
return { promise, resolve };
|
|
79
|
+
}
|
|
80
|
+
it("supersedes a delayed old pending read after a newer paid observation commits", async () => {
|
|
81
|
+
const s = await setup();
|
|
82
|
+
const old = { ...s.read(), status: "pending" as const };
|
|
83
|
+
const response = deferred<Stripe.Payout>();
|
|
84
|
+
s.retrieve.mockImplementationOnce(() => response.promise);
|
|
85
|
+
const first = s.run();
|
|
86
|
+
await vi.waitFor(() => expect(s.retrieve).toHaveBeenCalledTimes(1));
|
|
87
|
+
expect(await s.run()).toMatchObject({ status: "accepted", generation: 2 });
|
|
88
|
+
response.resolve(old);
|
|
89
|
+
expect(await first).toMatchObject({ status: "superseded", generation: 1 });
|
|
90
|
+
expect(await s.row()).toMatchObject({ status: "paid" });
|
|
91
|
+
expect(
|
|
92
|
+
await s.t.run((ctx) => ctx.db.query("payoutSyncClaims").unique()),
|
|
93
|
+
).toMatchObject({ status: "accepted", generation: 2, acceptedGeneration: 2 });
|
|
94
|
+
});
|
|
95
|
+
it("a newer failed read holds its generation instead of accepting an older in-flight snapshot", async () => {
|
|
96
|
+
const s = await setup();
|
|
97
|
+
await s.run();
|
|
98
|
+
const response = deferred<Stripe.Payout>();
|
|
99
|
+
s.retrieve.mockImplementationOnce(() => response.promise);
|
|
100
|
+
const first = s.run();
|
|
101
|
+
await vi.waitFor(() => expect(s.retrieve).toHaveBeenCalledTimes(2));
|
|
102
|
+
s.retrieve.mockRejectedValueOnce(new Error("Provider unavailable"));
|
|
103
|
+
await expect(s.run()).rejects.toThrow("Provider unavailable");
|
|
104
|
+
response.resolve({ ...s.read(), status: "pending" });
|
|
105
|
+
expect(await first).toMatchObject({ status: "superseded" });
|
|
106
|
+
expect(await s.row()).toMatchObject({ status: "paid" });
|
|
107
|
+
expect(
|
|
108
|
+
await s.t.run((ctx) => ctx.db.query("payoutSyncClaims").unique()),
|
|
109
|
+
).toMatchObject({ status: "unknown", generation: 3, acceptedGeneration: 1 });
|
|
110
|
+
expect(await s.run()).toMatchObject({
|
|
111
|
+
status: "accepted",
|
|
112
|
+
generation: 4,
|
|
113
|
+
changed: false,
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
it.each(["begin", "finish"] as const)(
|
|
117
|
+
"recovers a lost %s response through a fresh durable generation",
|
|
118
|
+
async (stage) => {
|
|
119
|
+
const s = await setup();
|
|
120
|
+
let isLost = false;
|
|
121
|
+
const ctx = {
|
|
122
|
+
...s.ctx,
|
|
123
|
+
runMutation: async (ref: Record<symbol, string>, args: never) => {
|
|
124
|
+
const saved = await s.mutate(ref, args);
|
|
125
|
+
if (!isLost && ref[symbol].endsWith(`/payoutSync/mutations/${stage}`)) {
|
|
126
|
+
isLost = true;
|
|
127
|
+
throw new Error("Lost component response");
|
|
128
|
+
}
|
|
129
|
+
return saved;
|
|
130
|
+
},
|
|
131
|
+
} as unknown as RunCtx;
|
|
132
|
+
const sync = await operation();
|
|
133
|
+
await expect(
|
|
134
|
+
sync(s.provider, component, ctx, {
|
|
135
|
+
stripePayoutId: "po_one",
|
|
136
|
+
scope: s.scope,
|
|
137
|
+
}),
|
|
138
|
+
).rejects.toThrow("Lost component response");
|
|
139
|
+
expect(
|
|
140
|
+
await s.t.run((ctx) => ctx.db.query("payoutSyncClaims").unique()),
|
|
141
|
+
).toMatchObject({
|
|
142
|
+
generation: 1,
|
|
143
|
+
status: stage === "finish" ? "accepted" : "pending",
|
|
144
|
+
});
|
|
145
|
+
expect(await s.run()).toMatchObject({
|
|
146
|
+
status: "accepted",
|
|
147
|
+
generation: 2,
|
|
148
|
+
changed: stage === "begin",
|
|
149
|
+
});
|
|
150
|
+
expect(
|
|
151
|
+
await s.t.run((ctx) => ctx.db.query("payouts").collect()),
|
|
152
|
+
).toHaveLength(1);
|
|
153
|
+
expect(
|
|
154
|
+
await s.t.run((ctx) => ctx.db.query("currencyTotals").unique()),
|
|
155
|
+
).toMatchObject({ paidOut: 500, count: 1 });
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
it("rejects a foreign existing payout before requesting provider state", async () => {
|
|
159
|
+
const s = await setup();
|
|
160
|
+
await s.t.mutation(
|
|
161
|
+
makeFunctionReference<"mutation">("connect/mutations:upsertPayout"),
|
|
162
|
+
{
|
|
163
|
+
stripePayoutId: "po_one",
|
|
164
|
+
accountId: "acct_foreign",
|
|
165
|
+
amount: 500,
|
|
166
|
+
currency: "jpy",
|
|
167
|
+
status: "paid",
|
|
168
|
+
},
|
|
169
|
+
);
|
|
170
|
+
await expect(s.run()).rejects.toThrow(
|
|
171
|
+
"Existing payout account does not match.",
|
|
172
|
+
);
|
|
173
|
+
expect(s.retrieve).not.toHaveBeenCalled();
|
|
174
|
+
expect(await s.row()).toMatchObject({ accountId: "acct_foreign" });
|
|
175
|
+
});
|
|
176
|
+
it("rejects changing a payout's durable account even if no financial row was saved", async () => {
|
|
177
|
+
const s = await setup();
|
|
178
|
+
s.retrieve.mockRejectedValueOnce(new Error("Unavailable"));
|
|
179
|
+
await expect(s.run()).rejects.toThrow("Unavailable");
|
|
180
|
+
s.retrieve.mockClear();
|
|
181
|
+
const sync = await operation();
|
|
182
|
+
await expect(
|
|
183
|
+
sync(s.provider, component, s.ctx, {
|
|
184
|
+
stripePayoutId: "po_one",
|
|
185
|
+
scope: { type: "connected", accountId: "acct_foreign" },
|
|
186
|
+
}),
|
|
187
|
+
).rejects.toThrow("Payout synchronization account does not match.");
|
|
188
|
+
expect(s.retrieve).not.toHaveBeenCalled();
|
|
189
|
+
expect(await s.row()).toBeNull();
|
|
190
|
+
});
|
|
191
|
+
it.each([{ amount: 501 }, { currency: "kwd" }])(
|
|
192
|
+
"rejects changed original money %j without changing summaries",
|
|
193
|
+
async (patch) => {
|
|
194
|
+
const s = await setup();
|
|
195
|
+
await s.run();
|
|
196
|
+
s.set(patch);
|
|
197
|
+
await expect(s.run()).rejects.toThrow(
|
|
198
|
+
"Existing payout original money does not match.",
|
|
199
|
+
);
|
|
200
|
+
expect(await s.row()).toMatchObject({ amount: 500, currency: "jpy" });
|
|
201
|
+
expect(
|
|
202
|
+
await s.t.run((ctx) => ctx.db.query("currencyTotals").unique()),
|
|
203
|
+
).toMatchObject({ paidOut: 500, count: 1 });
|
|
204
|
+
},
|
|
205
|
+
);
|
|
206
|
+
it.each([0, -1, 0.5, Number.MAX_SAFE_INTEGER + 1, NaN])(
|
|
207
|
+
"rejects invalid provider amount %s without a financial record",
|
|
208
|
+
async (amount) => {
|
|
209
|
+
const s = await setup();
|
|
210
|
+
s.set({ amount });
|
|
211
|
+
await expect(s.run()).rejects.toThrow(
|
|
212
|
+
"Invalid payout synchronization money or method.",
|
|
213
|
+
);
|
|
214
|
+
expect(await s.row()).toBeNull();
|
|
215
|
+
expect(
|
|
216
|
+
await s.t.run((ctx) => ctx.db.query("payoutSyncClaims").unique()),
|
|
217
|
+
).toMatchObject({ status: "unknown" });
|
|
218
|
+
},
|
|
219
|
+
);
|
|
220
|
+
it("rejects an unavailable provider response and preserves retryability", async () => {
|
|
221
|
+
const s = await setup();
|
|
222
|
+
s.retrieve.mockResolvedValueOnce(null as unknown as Stripe.Payout);
|
|
223
|
+
await expect(s.run()).rejects.toThrow(
|
|
224
|
+
"Provider payout response is unavailable.",
|
|
225
|
+
);
|
|
226
|
+
expect(await s.row()).toBeNull();
|
|
227
|
+
expect(await s.run()).toMatchObject({ status: "accepted", generation: 2 });
|
|
228
|
+
});
|
|
229
|
+
it("rejects a foreign provider payout ID", async () => {
|
|
230
|
+
const s = await setup();
|
|
231
|
+
s.set({ id: "po_foreign" });
|
|
232
|
+
await expect(s.run()).rejects.toThrow(
|
|
233
|
+
"Provider payout identity does not match.",
|
|
234
|
+
);
|
|
235
|
+
expect(await s.row()).toBeNull();
|
|
236
|
+
});
|
|
237
|
+
it.each([NaN, -1, 8640000000001])(
|
|
238
|
+
"rejects invalid arrival seconds %s",
|
|
239
|
+
async (arrival_date) => {
|
|
240
|
+
const s = await setup();
|
|
241
|
+
s.set({ arrival_date });
|
|
242
|
+
await expect(s.run()).rejects.toThrow(
|
|
243
|
+
"Invalid provider payout arrival date.",
|
|
244
|
+
);
|
|
245
|
+
expect(await s.row()).toBeNull();
|
|
246
|
+
},
|
|
247
|
+
);
|
|
248
|
+
it.each(["jpy", "kwd"])(
|
|
249
|
+
"preserves exact safe original %s units at the maximum integer",
|
|
250
|
+
async (currency) => {
|
|
251
|
+
const s = await setup();
|
|
252
|
+
s.set({ currency, amount: Number.MAX_SAFE_INTEGER });
|
|
253
|
+
await s.run();
|
|
254
|
+
expect(await s.row()).toMatchObject({
|
|
255
|
+
currency,
|
|
256
|
+
amount: Number.MAX_SAFE_INTEGER,
|
|
257
|
+
});
|
|
258
|
+
expect(
|
|
259
|
+
await s.t.run((ctx) => ctx.db.query("currencyTotals").unique()),
|
|
260
|
+
).toMatchObject({ currency, paidOut: Number.MAX_SAFE_INTEGER });
|
|
261
|
+
},
|
|
262
|
+
);
|
|
263
|
+
it("does not reinterpret a connected account named platform as platform authority", async () => {
|
|
264
|
+
const s = await setup({ type: "connected", accountId: "platform" });
|
|
265
|
+
await expect(s.run()).rejects.toThrow(
|
|
266
|
+
"Payout synchronization scope must be explicit.",
|
|
267
|
+
);
|
|
268
|
+
expect(s.retrieve).not.toHaveBeenCalled();
|
|
269
|
+
expect(await s.row()).toBeNull();
|
|
270
|
+
});
|
|
271
|
+
it("deduplicates equivalent provider metadata order without a financial update", async () => {
|
|
272
|
+
const s = await setup();
|
|
273
|
+
s.set({ metadata: { b: "two", a: "one" } });
|
|
274
|
+
await s.run();
|
|
275
|
+
s.set({ metadata: { a: "one", b: "two" } });
|
|
276
|
+
expect(await s.run()).toMatchObject({ status: "accepted", changed: false });
|
|
277
|
+
});
|
|
278
|
+
it("publishes authoritative synchronization through the actual BetterStripe client", async () => {
|
|
279
|
+
const s = await setup();
|
|
280
|
+
const { BetterStripe } = await import("../index.js");
|
|
281
|
+
const spy = vi
|
|
282
|
+
.spyOn(
|
|
283
|
+
BetterStripe.prototype as unknown as { stripe: () => Stripe },
|
|
284
|
+
"stripe",
|
|
285
|
+
)
|
|
286
|
+
.mockReturnValue(s.provider);
|
|
287
|
+
try {
|
|
288
|
+
const client = new BetterStripe(component) as InstanceType<
|
|
289
|
+
typeof BetterStripe
|
|
290
|
+
> & {
|
|
291
|
+
syncPayoutFromStripe: (
|
|
292
|
+
ctx: RunCtx,
|
|
293
|
+
opts: { stripePayoutId: string; scope: Scope },
|
|
294
|
+
) => Promise<unknown>;
|
|
295
|
+
};
|
|
296
|
+
expect(
|
|
297
|
+
client.syncPayoutFromStripe,
|
|
298
|
+
"public authoritative payout method is missing",
|
|
299
|
+
).toBeTypeOf("function");
|
|
300
|
+
expect(
|
|
301
|
+
await client.syncPayoutFromStripe(s.ctx, {
|
|
302
|
+
stripePayoutId: "po_one",
|
|
303
|
+
scope: s.scope,
|
|
304
|
+
}),
|
|
305
|
+
).toMatchObject({ status: "accepted" });
|
|
306
|
+
expect(await s.row()).toMatchObject({
|
|
307
|
+
accountId: "acct_owner",
|
|
308
|
+
status: "paid",
|
|
309
|
+
});
|
|
310
|
+
} finally {
|
|
311
|
+
spy.mockRestore();
|
|
312
|
+
}
|
|
313
|
+
});
|