@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,147 @@
|
|
|
1
|
+
import { updateObligation } from "../affiliateRecovery/accounting.js";
|
|
2
|
+
import { v } from "convex/values";
|
|
3
|
+
import type { Doc } from "../_generated/dataModel.js";
|
|
4
|
+
import type { MutationCtx } from "../_generated/server.js";
|
|
5
|
+
import { deriveReversalState } from "../lib/reversals.js";
|
|
6
|
+
|
|
7
|
+
async function recordCommissionReceipt(
|
|
8
|
+
ctx: MutationCtx,
|
|
9
|
+
transfer: Doc<"transfers">,
|
|
10
|
+
reversedAmount: number,
|
|
11
|
+
receipt: { stripeReversalId: string; operationId: string; amount: number },
|
|
12
|
+
) {
|
|
13
|
+
const op = await ctx.db
|
|
14
|
+
.query("transferReversalOps")
|
|
15
|
+
.withIndex("by_operation_id", (q) =>
|
|
16
|
+
q.eq("operationId", receipt.operationId),
|
|
17
|
+
)
|
|
18
|
+
.unique();
|
|
19
|
+
const slice = op?.slices.find(
|
|
20
|
+
(s) => s.stripeTransferId === transfer.stripeTransferId,
|
|
21
|
+
);
|
|
22
|
+
if (
|
|
23
|
+
transfer.role !== "affiliate" ||
|
|
24
|
+
!op?.commissionExposure ||
|
|
25
|
+
!slice ||
|
|
26
|
+
receipt.amount !== slice.to - slice.from ||
|
|
27
|
+
reversedAmount !== slice.to ||
|
|
28
|
+
!receipt.stripeReversalId
|
|
29
|
+
)
|
|
30
|
+
throw new Error(
|
|
31
|
+
"Commission reversal receipt does not match its reserved operation.",
|
|
32
|
+
);
|
|
33
|
+
const priorOperationReceipt = await ctx.db
|
|
34
|
+
.query("affiliateReversalReceipts")
|
|
35
|
+
.withIndex("by_transfer_operation", (q) =>
|
|
36
|
+
q
|
|
37
|
+
.eq("stripeTransferId", transfer.stripeTransferId)
|
|
38
|
+
.eq("operationId", receipt.operationId),
|
|
39
|
+
)
|
|
40
|
+
.unique();
|
|
41
|
+
if (
|
|
42
|
+
priorOperationReceipt &&
|
|
43
|
+
priorOperationReceipt.stripeReversalId !== receipt.stripeReversalId
|
|
44
|
+
)
|
|
45
|
+
throw new Error(
|
|
46
|
+
"Commission operation already has a different reversal receipt.",
|
|
47
|
+
);
|
|
48
|
+
const existing = await ctx.db
|
|
49
|
+
.query("affiliateReversalReceipts")
|
|
50
|
+
.withIndex("by_receipt", (q) =>
|
|
51
|
+
q.eq("stripeReversalId", receipt.stripeReversalId),
|
|
52
|
+
)
|
|
53
|
+
.unique();
|
|
54
|
+
if (existing) {
|
|
55
|
+
if (
|
|
56
|
+
existing.stripeTransferId !== transfer.stripeTransferId ||
|
|
57
|
+
existing.operationId !== receipt.operationId ||
|
|
58
|
+
existing.amount !== receipt.amount
|
|
59
|
+
)
|
|
60
|
+
throw new Error("Commission reversal receipt identity cannot change.");
|
|
61
|
+
} else {
|
|
62
|
+
await ctx.db.insert("affiliateReversalReceipts", {
|
|
63
|
+
...receipt,
|
|
64
|
+
stripeTransferId: transfer.stripeTransferId,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const recordTransferReversalDefinition = {
|
|
70
|
+
args: {
|
|
71
|
+
stripeTransferId: v.string(),
|
|
72
|
+
reversedAmount: v.number(),
|
|
73
|
+
receipt: v.optional(
|
|
74
|
+
v.object({
|
|
75
|
+
stripeReversalId: v.string(),
|
|
76
|
+
operationId: v.string(),
|
|
77
|
+
amount: v.number(),
|
|
78
|
+
}),
|
|
79
|
+
),
|
|
80
|
+
},
|
|
81
|
+
returns: v.null(),
|
|
82
|
+
handler: async (
|
|
83
|
+
ctx: MutationCtx,
|
|
84
|
+
args: {
|
|
85
|
+
stripeTransferId: string;
|
|
86
|
+
reversedAmount: number;
|
|
87
|
+
receipt?: {
|
|
88
|
+
stripeReversalId: string;
|
|
89
|
+
operationId: string;
|
|
90
|
+
amount: number;
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
) => {
|
|
94
|
+
const transfer = await ctx.db
|
|
95
|
+
.query("transfers")
|
|
96
|
+
.withIndex("by_stripe_transfer_id", (q) =>
|
|
97
|
+
q.eq("stripeTransferId", args.stripeTransferId),
|
|
98
|
+
)
|
|
99
|
+
.first();
|
|
100
|
+
|
|
101
|
+
if (!transfer) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`Cannot record reversal: no transfer found for ${args.stripeTransferId}`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// `reversedAmount` is a cumulative total (matches Stripe's
|
|
108
|
+
// `Transfer.amount_reversed`). Keep it monotonic so a stale/out-of-order
|
|
109
|
+
// event can't shrink it or undo a full reversal.
|
|
110
|
+
const reversedAmount = Math.max(
|
|
111
|
+
transfer.reversedAmount ?? 0,
|
|
112
|
+
args.reversedAmount,
|
|
113
|
+
);
|
|
114
|
+
const derived = deriveReversalState(
|
|
115
|
+
transfer.amount,
|
|
116
|
+
reversedAmount,
|
|
117
|
+
transfer.status,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
if (
|
|
121
|
+
!Number.isSafeInteger(reversedAmount) ||
|
|
122
|
+
reversedAmount < 0 ||
|
|
123
|
+
reversedAmount > transfer.amount
|
|
124
|
+
)
|
|
125
|
+
throw new Error("Invalid confirmed transfer reversal amount.");
|
|
126
|
+
if (args.receipt)
|
|
127
|
+
await recordCommissionReceipt(
|
|
128
|
+
ctx,
|
|
129
|
+
transfer,
|
|
130
|
+
args.reversedAmount,
|
|
131
|
+
args.receipt,
|
|
132
|
+
);
|
|
133
|
+
await ctx.db.patch("transfers", transfer._id, derived);
|
|
134
|
+
const obligation = await ctx.db
|
|
135
|
+
.query("affiliateObligations")
|
|
136
|
+
.withIndex("by_transfer", (q) =>
|
|
137
|
+
q.eq("stripeTransferId", args.stripeTransferId),
|
|
138
|
+
)
|
|
139
|
+
.unique();
|
|
140
|
+
if (obligation)
|
|
141
|
+
await updateObligation(ctx, obligation, {
|
|
142
|
+
recoveredAmount: reversedAmount,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return null;
|
|
146
|
+
},
|
|
147
|
+
};
|
|
@@ -90,9 +90,9 @@ export const transfersTable = defineTable({
|
|
|
90
90
|
* delivery replays byte-identical Stripe requests instead of recomputing
|
|
91
91
|
* amounts from mutable ledger state.
|
|
92
92
|
*/
|
|
93
|
-
export const transferReversalOpsTable = defineTable(
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
export const transferReversalOpsTable = defineTable(transferReversalOpFields)
|
|
94
|
+
.index("by_operation_id", ["operationId"])
|
|
95
|
+
.index("by_source_charge", ["sourceChargeId"]);
|
|
96
96
|
|
|
97
97
|
/**
|
|
98
98
|
* Disputes (chargebacks). Same indexing strategy as refunds.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { commissionExposureValidator } from "../settlement/clawbackSchema.js";
|
|
1
2
|
import { type Infer, v } from "convex/values";
|
|
2
3
|
|
|
3
4
|
import { feeRoutingFields, splitRecipientRoleValidator } from "../lib/fees";
|
|
@@ -305,6 +306,7 @@ export const transferReversalOpFields = {
|
|
|
305
306
|
operationId: v.string(),
|
|
306
307
|
sourceChargeId: v.string(),
|
|
307
308
|
slices: v.array(reversalSliceValidator),
|
|
309
|
+
commissionExposure: v.optional(commissionExposureValidator),
|
|
308
310
|
};
|
|
309
311
|
|
|
310
312
|
/** Full `transferReversalOps` document, including system fields. */
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// @vitest-environment edge-runtime
|
|
2
|
+
import { convexTest } from "convex-test";
|
|
3
|
+
import { makeFunctionReference } from "convex/server";
|
|
4
|
+
import { expect, it } from "vitest";
|
|
5
|
+
import schema from "./schema.js";
|
|
6
|
+
import { changeBalance } from "./affiliateRecovery/accounting.js";
|
|
7
|
+
import { transferFingerprint } from "./payoutRelease/helpers.js";
|
|
8
|
+
const modules = import.meta.glob("./**/*.*s");
|
|
9
|
+
const m = (name: string) =>
|
|
10
|
+
makeFunctionReference<"mutation">(`payoutRelease/mutations:${name}`);
|
|
11
|
+
const pm = (name: string) =>
|
|
12
|
+
makeFunctionReference<"mutation">(`payoutPolicy/mutations:${name}`);
|
|
13
|
+
const scope = { accountId: "acct_recipient", ownerKey: "user_1" };
|
|
14
|
+
const transfer = (
|
|
15
|
+
stripeTransferId: string,
|
|
16
|
+
amount: number,
|
|
17
|
+
role = "store",
|
|
18
|
+
) => ({
|
|
19
|
+
stripeTransferId,
|
|
20
|
+
destinationAccountId: scope.accountId,
|
|
21
|
+
amount,
|
|
22
|
+
currency: "usd",
|
|
23
|
+
status: "paid",
|
|
24
|
+
role,
|
|
25
|
+
});
|
|
26
|
+
async function setup() {
|
|
27
|
+
const t = convexTest(schema, modules);
|
|
28
|
+
await t.mutation(pm("prepare"), {
|
|
29
|
+
...scope,
|
|
30
|
+
operationKey: "enable",
|
|
31
|
+
expectedRevision: -1,
|
|
32
|
+
mode: "custom",
|
|
33
|
+
thresholds: [{ currency: "usd", amount: 1000 }],
|
|
34
|
+
country: "US",
|
|
35
|
+
observedSchedule: { interval: "daily" },
|
|
36
|
+
});
|
|
37
|
+
await t.mutation(pm("confirm"), {
|
|
38
|
+
...scope,
|
|
39
|
+
operationKey: "enable",
|
|
40
|
+
observedSchedule: { interval: "manual" },
|
|
41
|
+
});
|
|
42
|
+
for (const row of [
|
|
43
|
+
transfer("tr_creator", 600),
|
|
44
|
+
transfer("tr_affiliate", 400, "affiliate"),
|
|
45
|
+
])
|
|
46
|
+
await t.mutation(
|
|
47
|
+
makeFunctionReference<"mutation">("connect/mutations:upsertTransfer"),
|
|
48
|
+
row,
|
|
49
|
+
);
|
|
50
|
+
const rows = await t.run(async (ctx) => ctx.db.query("transfers").take(10));
|
|
51
|
+
const args = {
|
|
52
|
+
...scope,
|
|
53
|
+
operationKey: "release-1",
|
|
54
|
+
expectedRevision: 0,
|
|
55
|
+
currency: "usd",
|
|
56
|
+
providerAvailable: 1000,
|
|
57
|
+
independentLiabilities: 0,
|
|
58
|
+
observations: rows.map((row) => ({
|
|
59
|
+
stripeTransferId: row.stripeTransferId,
|
|
60
|
+
matureNet: row.amount,
|
|
61
|
+
pendingCredits: 0,
|
|
62
|
+
currency: row.currency,
|
|
63
|
+
ledgerFingerprint: transferFingerprint(row),
|
|
64
|
+
observedAt: Date.now(),
|
|
65
|
+
transactionIds: [`txn_${row.stripeTransferId}`],
|
|
66
|
+
})),
|
|
67
|
+
};
|
|
68
|
+
return { t, args };
|
|
69
|
+
}
|
|
70
|
+
it("blocks combined payouts on affiliate debt in the payout currency", async () => {
|
|
71
|
+
const { t, args } = await setup();
|
|
72
|
+
await t.run((ctx) =>
|
|
73
|
+
changeBalance(ctx, scope.accountId, "usd", { outstandingDebt: 100 }),
|
|
74
|
+
);
|
|
75
|
+
await expect(t.mutation(m("claim"), args)).rejects.toThrow(/recovery/);
|
|
76
|
+
expect(
|
|
77
|
+
await t.run((ctx) => ctx.db.query("payoutReleaseClaims").collect()),
|
|
78
|
+
).toEqual([]);
|
|
79
|
+
expect(
|
|
80
|
+
await t.run((ctx) => ctx.db.query("payoutSourceAllocations").collect()),
|
|
81
|
+
).toEqual([]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("keeps independent currency debts out of a verified payout", async () => {
|
|
85
|
+
const { t, args } = await setup();
|
|
86
|
+
await t.run((ctx) =>
|
|
87
|
+
changeBalance(ctx, scope.accountId, "eur", { outstandingDebt: 100 }),
|
|
88
|
+
);
|
|
89
|
+
expect(await t.mutation(m("claim"), args)).toMatchObject({
|
|
90
|
+
status: "reserved",
|
|
91
|
+
amount: 1000,
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("shares one reservation between direct and combined payout paths", async () => {
|
|
96
|
+
const { t, args } = await setup();
|
|
97
|
+
await t.run((ctx) => changeBalance(ctx, scope.accountId, "usd", {}));
|
|
98
|
+
const direct = () =>
|
|
99
|
+
t.mutation(
|
|
100
|
+
makeFunctionReference<"mutation">(
|
|
101
|
+
"affiliatePayouts/mutations:reservePayout",
|
|
102
|
+
),
|
|
103
|
+
{
|
|
104
|
+
affiliateAccountId: scope.accountId,
|
|
105
|
+
currency: "usd",
|
|
106
|
+
amount: 1000,
|
|
107
|
+
minimumAmount: 1,
|
|
108
|
+
operationKey: "direct",
|
|
109
|
+
now: Date.now(),
|
|
110
|
+
},
|
|
111
|
+
);
|
|
112
|
+
const outcomes = await Promise.allSettled([
|
|
113
|
+
t.mutation(m("claim"), args),
|
|
114
|
+
direct(),
|
|
115
|
+
]);
|
|
116
|
+
expect(
|
|
117
|
+
outcomes.filter((result) => result.status === "fulfilled"),
|
|
118
|
+
).toHaveLength(1);
|
|
119
|
+
expect(
|
|
120
|
+
outcomes.filter((result) => result.status === "rejected"),
|
|
121
|
+
).toHaveLength(1);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("rechecks new debt immediately before the provider attempt and permits safe cancellation", async () => {
|
|
125
|
+
const { t, args } = await setup();
|
|
126
|
+
await t.mutation(m("claim"), args);
|
|
127
|
+
await t.run((ctx) =>
|
|
128
|
+
changeBalance(ctx, scope.accountId, "usd", { outstandingDebt: 100 }),
|
|
129
|
+
);
|
|
130
|
+
const identity = { ...scope, operationKey: args.operationKey };
|
|
131
|
+
await expect(t.mutation(m("beginAttempt"), identity)).rejects.toThrow(
|
|
132
|
+
/recovery/,
|
|
133
|
+
);
|
|
134
|
+
await t.mutation(m("cancelUnattempted"), identity);
|
|
135
|
+
expect(
|
|
136
|
+
await t.run((ctx) => ctx.db.query("affiliateRecoveryBalances").first()),
|
|
137
|
+
).toMatchObject({
|
|
138
|
+
outstandingDebt: 100,
|
|
139
|
+
pendingPayoutAmount: 0,
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("holds unknown payout attempts until a verified receipt, without erasing later debt", async () => {
|
|
144
|
+
const { t, args } = await setup();
|
|
145
|
+
await t.mutation(m("claim"), args);
|
|
146
|
+
const identity = { ...scope, operationKey: args.operationKey };
|
|
147
|
+
await t.mutation(m("beginAttempt"), identity);
|
|
148
|
+
await t.run((ctx) =>
|
|
149
|
+
changeBalance(ctx, scope.accountId, "usd", { outstandingDebt: 100 }),
|
|
150
|
+
);
|
|
151
|
+
const before = await t.run((ctx) =>
|
|
152
|
+
ctx.db.query("affiliateRecoveryBalances").first(),
|
|
153
|
+
);
|
|
154
|
+
expect(before?.activePayoutKey).toBe(
|
|
155
|
+
`bs:recipient-payout:${args.operationKey}`,
|
|
156
|
+
);
|
|
157
|
+
await expect(t.mutation(m("cancelUnattempted"), identity)).rejects.toThrow(
|
|
158
|
+
/attempt/,
|
|
159
|
+
);
|
|
160
|
+
await t.mutation(m("recordOutcome"), {
|
|
161
|
+
...identity,
|
|
162
|
+
stripePayoutId: "po_verified",
|
|
163
|
+
amount: 1000,
|
|
164
|
+
currency: "usd",
|
|
165
|
+
providerStatus: "pending",
|
|
166
|
+
});
|
|
167
|
+
const after = await t.run((ctx) =>
|
|
168
|
+
ctx.db.query("affiliateRecoveryBalances").first(),
|
|
169
|
+
);
|
|
170
|
+
expect(after?.activePayoutKey).toBeUndefined();
|
|
171
|
+
expect(after?.outstandingDebt).toBe(100);
|
|
172
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// @vitest-environment edge-runtime
|
|
2
|
+
import { convexTest } from "convex-test";
|
|
3
|
+
import { makeFunctionReference } from "convex/server";
|
|
4
|
+
import { expect, it, vi } from "vitest";
|
|
5
|
+
import schema from "./schema.js";
|
|
6
|
+
const modules = import.meta.glob("./**/*.*s");
|
|
7
|
+
const scope = { stripePayoutId: "po_one", accountId: "acct_owner" };
|
|
8
|
+
const payout = {
|
|
9
|
+
...scope,
|
|
10
|
+
amount: 500,
|
|
11
|
+
currency: "jpy",
|
|
12
|
+
status: "paid" as const,
|
|
13
|
+
arrivalDate: "1970-01-01T00:03:20.000Z",
|
|
14
|
+
method: "standard",
|
|
15
|
+
metadata: {},
|
|
16
|
+
};
|
|
17
|
+
const mutation = (name: string) =>
|
|
18
|
+
makeFunctionReference<"mutation">(`payoutSync/mutations:${name}`);
|
|
19
|
+
async function setup() {
|
|
20
|
+
const t = convexTest(schema, modules);
|
|
21
|
+
const generation = await t.mutation(mutation("begin"), scope);
|
|
22
|
+
return { t, args: { ...scope, generation, payout } };
|
|
23
|
+
}
|
|
24
|
+
it("replays a committed generation without changing its acceptance time or financial row", async () => {
|
|
25
|
+
vi.useFakeTimers();
|
|
26
|
+
try {
|
|
27
|
+
vi.setSystemTime(1000);
|
|
28
|
+
const { t, args } = await setup();
|
|
29
|
+
const first = await t.mutation(mutation("finish"), args);
|
|
30
|
+
const oldRow = await t.run((ctx) => ctx.db.query("payouts").unique());
|
|
31
|
+
vi.setSystemTime(2000);
|
|
32
|
+
expect(await t.mutation(mutation("finish"), args)).toMatchObject({
|
|
33
|
+
...first,
|
|
34
|
+
changed: false,
|
|
35
|
+
});
|
|
36
|
+
expect(
|
|
37
|
+
await t.run((ctx) => ctx.db.query("payoutSyncClaims").unique()),
|
|
38
|
+
).toMatchObject({ acceptedAt: 1000 });
|
|
39
|
+
expect(await t.run((ctx) => ctx.db.query("payouts").unique())).toEqual(
|
|
40
|
+
oldRow,
|
|
41
|
+
);
|
|
42
|
+
} finally {
|
|
43
|
+
vi.useRealTimers();
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
it("rejects replacing the accepted snapshot under the same generation", async () => {
|
|
47
|
+
const { t, args } = await setup();
|
|
48
|
+
await t.mutation(mutation("finish"), args);
|
|
49
|
+
await expect(
|
|
50
|
+
t.mutation(mutation("finish"), {
|
|
51
|
+
...args,
|
|
52
|
+
payout: { ...payout, status: "pending" },
|
|
53
|
+
}),
|
|
54
|
+
).rejects.toThrow("An accepted payout observation cannot be replaced.");
|
|
55
|
+
expect(await t.run((ctx) => ctx.db.query("payouts").unique())).toMatchObject({
|
|
56
|
+
status: "paid",
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
it("ignores stale completion and stale unknown markers before any financial write", async () => {
|
|
60
|
+
const { t, args } = await setup();
|
|
61
|
+
const generation = await t.mutation(mutation("begin"), scope);
|
|
62
|
+
expect(await t.mutation(mutation("finish"), args)).toMatchObject({
|
|
63
|
+
status: "superseded",
|
|
64
|
+
});
|
|
65
|
+
await t.mutation(mutation("markUnknown"), {
|
|
66
|
+
...scope,
|
|
67
|
+
generation: args.generation,
|
|
68
|
+
});
|
|
69
|
+
expect(
|
|
70
|
+
await t.run((ctx) => ctx.db.query("payoutSyncClaims").unique()),
|
|
71
|
+
).toMatchObject({ status: "pending", generation });
|
|
72
|
+
expect(await t.run((ctx) => ctx.db.query("payouts").collect())).toEqual([]);
|
|
73
|
+
await t.mutation(mutation("finish"), { ...args, generation });
|
|
74
|
+
await t.mutation(mutation("markUnknown"), { ...scope, generation });
|
|
75
|
+
expect(
|
|
76
|
+
await t.run((ctx) => ctx.db.query("payoutSyncClaims").unique()),
|
|
77
|
+
).toMatchObject({ status: "accepted" });
|
|
78
|
+
});
|
|
79
|
+
it.each([{ stripePayoutId: "po_foreign" }, { accountId: "acct_foreign" }])(
|
|
80
|
+
"rejects snapshot identity mismatch %j atomically",
|
|
81
|
+
async (patch) => {
|
|
82
|
+
const { t, args } = await setup();
|
|
83
|
+
await expect(
|
|
84
|
+
t.mutation(mutation("finish"), {
|
|
85
|
+
...args,
|
|
86
|
+
payout: { ...payout, ...patch },
|
|
87
|
+
}),
|
|
88
|
+
).rejects.toThrow(
|
|
89
|
+
"Payout synchronization snapshot identity does not match.",
|
|
90
|
+
);
|
|
91
|
+
expect(await t.run((ctx) => ctx.db.query("payouts").collect())).toEqual([]);
|
|
92
|
+
expect(
|
|
93
|
+
await t.run((ctx) => ctx.db.query("currencyTotals").collect()),
|
|
94
|
+
).toEqual([]);
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
it("rejects a foreign claim lookup and generation overflow without changing the claim", async () => {
|
|
98
|
+
const { t } = await setup();
|
|
99
|
+
await expect(
|
|
100
|
+
t.query(makeFunctionReference<"query">("payoutSync/queries:get"), {
|
|
101
|
+
...scope,
|
|
102
|
+
accountId: "acct_foreign",
|
|
103
|
+
}),
|
|
104
|
+
).rejects.toThrow("Payout synchronization account does not match.");
|
|
105
|
+
await t.run(async (ctx) => {
|
|
106
|
+
const row = await ctx.db.query("payoutSyncClaims").unique();
|
|
107
|
+
await ctx.db.patch("payoutSyncClaims", row!._id, {
|
|
108
|
+
generation: Number.MAX_SAFE_INTEGER,
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
await expect(t.mutation(mutation("begin"), scope)).rejects.toThrow(
|
|
112
|
+
"Payout synchronization generation exhausted.",
|
|
113
|
+
);
|
|
114
|
+
expect(
|
|
115
|
+
await t.run((ctx) => ctx.db.query("payoutSyncClaims").unique()),
|
|
116
|
+
).toMatchObject({ generation: Number.MAX_SAFE_INTEGER });
|
|
117
|
+
});
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
reserveRecoveryGuard,
|
|
3
|
+
releaseRecoveryGuard,
|
|
4
|
+
} from "../affiliatePayouts/recipientGuard.js";
|
|
1
5
|
import {
|
|
2
6
|
historicalBatchArgs,
|
|
3
7
|
adoptHistoricalBatch as importHistoricalBatch,
|
|
@@ -110,6 +114,7 @@ export const claim = mutation({
|
|
|
110
114
|
sources,
|
|
111
115
|
});
|
|
112
116
|
if (plan.status === "held") return plan;
|
|
117
|
+
await reserveRecoveryGuard(ctx, args, plan.amount);
|
|
113
118
|
for (const allocation of plan.allocations) {
|
|
114
119
|
const prior = await sourceById(ctx, allocation.stripeTransferId);
|
|
115
120
|
if (prior)
|
|
@@ -155,6 +160,7 @@ export const beginAttempt = mutation({
|
|
|
155
160
|
if (feeRevision !== (claim.feeRevision ?? 0))
|
|
156
161
|
throw new Error("Payout fee revision changed after reservation.");
|
|
157
162
|
await checkSnapshot(ctx, claim.accountId, claim.observations, true);
|
|
163
|
+
await reserveRecoveryGuard(ctx, claim, claim.amount);
|
|
158
164
|
await ctx.db.patch("payoutReleaseClaims", claim._id, {
|
|
159
165
|
attemptedAt: Date.now(),
|
|
160
166
|
});
|
|
@@ -174,6 +180,7 @@ export const cancelUnattempted = mutation({
|
|
|
174
180
|
throw new Error("An attempted payout claim cannot be canceled.");
|
|
175
181
|
if (claim.status === "canceled") return claim;
|
|
176
182
|
await moveAllocations(ctx, claim, "release");
|
|
183
|
+
await releaseRecoveryGuard(ctx, claim);
|
|
177
184
|
await ctx.db.patch("payoutReleaseClaims", claim._id, {
|
|
178
185
|
status: "canceled",
|
|
179
186
|
});
|
|
@@ -257,6 +264,7 @@ export const recordOutcome = mutation({
|
|
|
257
264
|
if (claim.status === "paid" && args.providerStatus !== "paid" && !returned)
|
|
258
265
|
return claim;
|
|
259
266
|
await recordFees(ctx, claim, args, returned);
|
|
267
|
+
await releaseRecoveryGuard(ctx, claim);
|
|
260
268
|
if (returned) {
|
|
261
269
|
await moveAllocations(
|
|
262
270
|
ctx,
|
|
@@ -329,6 +337,7 @@ export const recordRejection = mutation({
|
|
|
329
337
|
if (adoptedRejection && adoptedRejection._id !== claim._id)
|
|
330
338
|
throw new Error("Payout rejection receipt is already allocated.");
|
|
331
339
|
await moveAllocations(ctx, claim, "release");
|
|
340
|
+
await releaseRecoveryGuard(ctx, claim);
|
|
332
341
|
await ctx.db.patch("payoutReleaseClaims", claim._id, {
|
|
333
342
|
status: "rejected",
|
|
334
343
|
rejection: { requestId: args.requestId, code: args.code },
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { paginator } from "convex-helpers/server/pagination";
|
|
2
|
+
import schema from "../schema.js";
|
|
1
3
|
import { transferDocValidator } from "../connect/validators.js";
|
|
2
4
|
import { readFees } from "./fees.js";
|
|
3
5
|
import { paginationOptsValidator } from "convex/server";
|
|
@@ -43,7 +45,7 @@ export const pending = query({
|
|
|
43
45
|
args.paginationOpts.numItems > 100
|
|
44
46
|
)
|
|
45
47
|
throw new Error("Invalid payout claim page.");
|
|
46
|
-
const result = await ctx.db
|
|
48
|
+
const result = await paginator(ctx.db, schema)
|
|
47
49
|
.query("payoutReleaseClaims")
|
|
48
50
|
.withIndex("by_account_status", (q) =>
|
|
49
51
|
q.eq("accountId", args.accountId).eq("status", "reserved"),
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { Infer } from "convex/values";
|
|
2
|
+
import type { MutationCtx, QueryCtx } from "../_generated/server.js";
|
|
3
|
+
import type { Doc } from "../_generated/dataModel.js";
|
|
4
|
+
import { generateIdempotencyKey } from "../lib/idempotency.js";
|
|
5
|
+
import type { snapshot } from "./validators.js";
|
|
6
|
+
export function validateIdentity(args: {
|
|
7
|
+
stripePayoutId: string;
|
|
8
|
+
accountId: string;
|
|
9
|
+
}) {
|
|
10
|
+
if (
|
|
11
|
+
!/^po_[A-Za-z0-9_]+$/.test(args.stripePayoutId) ||
|
|
12
|
+
args.stripePayoutId.length > 255 ||
|
|
13
|
+
(args.accountId !== "platform" &&
|
|
14
|
+
!/^acct_[A-Za-z0-9_]+$/.test(args.accountId)) ||
|
|
15
|
+
args.accountId.length > 255
|
|
16
|
+
)
|
|
17
|
+
throw new Error("Invalid payout synchronization identity.");
|
|
18
|
+
}
|
|
19
|
+
export async function claimByPayout(
|
|
20
|
+
ctx: QueryCtx | MutationCtx,
|
|
21
|
+
args: { stripePayoutId: string; accountId: string },
|
|
22
|
+
) {
|
|
23
|
+
validateIdentity(args);
|
|
24
|
+
const claim = await ctx.db
|
|
25
|
+
.query("payoutSyncClaims")
|
|
26
|
+
.withIndex("by_stripe_payout_id", (q) =>
|
|
27
|
+
q.eq("stripePayoutId", args.stripePayoutId),
|
|
28
|
+
)
|
|
29
|
+
.unique();
|
|
30
|
+
if (claim && claim.accountId !== args.accountId)
|
|
31
|
+
throw new Error("Payout synchronization account does not match.");
|
|
32
|
+
return claim;
|
|
33
|
+
}
|
|
34
|
+
export async function payoutById(
|
|
35
|
+
ctx: QueryCtx | MutationCtx,
|
|
36
|
+
args: { stripePayoutId: string; accountId: string },
|
|
37
|
+
) {
|
|
38
|
+
const payout = await ctx.db
|
|
39
|
+
.query("payouts")
|
|
40
|
+
.withIndex("by_stripe_payout_id", (q) =>
|
|
41
|
+
q.eq("stripePayoutId", args.stripePayoutId),
|
|
42
|
+
)
|
|
43
|
+
.unique();
|
|
44
|
+
if (payout && payout.accountId !== args.accountId)
|
|
45
|
+
throw new Error("Existing payout account does not match.");
|
|
46
|
+
return payout;
|
|
47
|
+
}
|
|
48
|
+
export function validateSnapshot(p: Infer<typeof snapshot>) {
|
|
49
|
+
validateIdentity(p);
|
|
50
|
+
if (
|
|
51
|
+
!Number.isSafeInteger(p.amount) ||
|
|
52
|
+
p.amount <= 0 ||
|
|
53
|
+
!/^[a-z]{3}$/.test(p.currency) ||
|
|
54
|
+
(p.method !== undefined && !["standard", "instant"].includes(p.method))
|
|
55
|
+
)
|
|
56
|
+
throw new Error("Invalid payout synchronization money or method.");
|
|
57
|
+
if (
|
|
58
|
+
p.arrivalDate !== undefined &&
|
|
59
|
+
(!Number.isFinite(Date.parse(p.arrivalDate)) ||
|
|
60
|
+
new Date(p.arrivalDate).toISOString() !== p.arrivalDate)
|
|
61
|
+
)
|
|
62
|
+
throw new Error("Invalid payout arrival date.");
|
|
63
|
+
if (
|
|
64
|
+
(p.failureCode !== undefined && p.failureCode.length > 255) ||
|
|
65
|
+
(p.failureMessage !== undefined && p.failureMessage.length > 4096)
|
|
66
|
+
)
|
|
67
|
+
throw new Error("Invalid payout failure detail.");
|
|
68
|
+
}
|
|
69
|
+
export async function fingerprint(p: Infer<typeof snapshot> | Doc<"payouts">) {
|
|
70
|
+
return generateIdempotencyKey("payout-sync", {
|
|
71
|
+
snapshot: [
|
|
72
|
+
p.stripePayoutId,
|
|
73
|
+
p.accountId,
|
|
74
|
+
p.amount,
|
|
75
|
+
p.currency,
|
|
76
|
+
p.status,
|
|
77
|
+
p.arrivalDate,
|
|
78
|
+
p.method,
|
|
79
|
+
p.failureCode,
|
|
80
|
+
p.failureMessage,
|
|
81
|
+
Object.entries(p.metadata ?? {}).sort(([a], [b]) =>
|
|
82
|
+
a < b ? -1 : a > b ? 1 : 0,
|
|
83
|
+
),
|
|
84
|
+
],
|
|
85
|
+
});
|
|
86
|
+
}
|