@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
|
@@ -1,14 +1,18 @@
|
|
|
1
|
+
import { syncCommissionTransfer } from "../settlement/clawbacks.js";
|
|
2
|
+
import { recordTransferReversalDefinition } from "./reversalReceipts.js";
|
|
3
|
+
import { claimReversalSlicesDefinition } from "./reversalClaims.js";
|
|
4
|
+
import { writePayout } from "./payoutWrites.js";
|
|
1
5
|
import { v } from "convex/values";
|
|
2
6
|
|
|
3
|
-
import type {
|
|
7
|
+
import type { Id } from "../_generated/dataModel";
|
|
4
8
|
import { mutation } from "../earnings/triggers.js";
|
|
5
9
|
import { feeRoutingFields } from "../lib/fees";
|
|
6
|
-
import {
|
|
10
|
+
import { deriveReversalState } from "../lib/reversals";
|
|
7
11
|
import { paymentSeller } from "../lib/paymentSeller";
|
|
8
12
|
import {
|
|
9
13
|
disputeFields,
|
|
10
14
|
paymentStatusValidator,
|
|
11
|
-
|
|
15
|
+
payoutFields,
|
|
12
16
|
refundFields,
|
|
13
17
|
transferFields,
|
|
14
18
|
} from "./validators";
|
|
@@ -113,37 +117,10 @@ export const upsertPayment = mutation({
|
|
|
113
117
|
// Payout mutations
|
|
114
118
|
|
|
115
119
|
export const upsertPayout = mutation({
|
|
116
|
-
args:
|
|
117
|
-
stripePayoutId: v.string(),
|
|
118
|
-
accountId: v.string(),
|
|
119
|
-
amount: v.number(),
|
|
120
|
-
currency: v.string(),
|
|
121
|
-
status: payoutStatusValidator,
|
|
122
|
-
arrivalDate: v.optional(v.string()),
|
|
123
|
-
method: v.optional(v.string()),
|
|
124
|
-
failureCode: v.optional(v.string()),
|
|
125
|
-
failureMessage: v.optional(v.string()),
|
|
126
|
-
metadata: v.optional(v.any()),
|
|
127
|
-
},
|
|
120
|
+
args: payoutFields,
|
|
128
121
|
returns: v.null(),
|
|
129
122
|
handler: async (ctx, args) => {
|
|
130
|
-
|
|
131
|
-
.query("payouts")
|
|
132
|
-
.withIndex("by_stripe_payout_id", (q) =>
|
|
133
|
-
q.eq("stripePayoutId", args.stripePayoutId),
|
|
134
|
-
)
|
|
135
|
-
.first();
|
|
136
|
-
|
|
137
|
-
if (existing) {
|
|
138
|
-
await ctx.db.patch("payouts", existing._id, {
|
|
139
|
-
...args,
|
|
140
|
-
failureCode: args.failureCode,
|
|
141
|
-
failureMessage: args.failureMessage,
|
|
142
|
-
});
|
|
143
|
-
} else {
|
|
144
|
-
await ctx.db.insert("payouts", args);
|
|
145
|
-
}
|
|
146
|
-
|
|
123
|
+
await writePayout(ctx, args);
|
|
147
124
|
return null;
|
|
148
125
|
},
|
|
149
126
|
});
|
|
@@ -338,6 +315,7 @@ export const upsertTransfer = mutation({
|
|
|
338
315
|
reversedAmount,
|
|
339
316
|
args.status,
|
|
340
317
|
);
|
|
318
|
+
await syncCommissionTransfer(ctx, { ...existing, ...args, ...derived });
|
|
341
319
|
await ctx.db.patch("transfers", existing._id, { ...args, ...derived });
|
|
342
320
|
} else {
|
|
343
321
|
const derived = deriveReversalState(
|
|
@@ -357,166 +335,11 @@ export const upsertTransfer = mutation({
|
|
|
357
335
|
* cumulative `reversedAmount` and a derived `reversalStatus`. `reversedAmount`
|
|
358
336
|
* is the new cumulative total (matches Stripe's `Transfer.amount_reversed`).
|
|
359
337
|
*/
|
|
360
|
-
export const recordTransferReversal = mutation(
|
|
361
|
-
|
|
362
|
-
stripeTransferId: v.string(),
|
|
363
|
-
reversedAmount: v.number(),
|
|
364
|
-
},
|
|
365
|
-
returns: v.null(),
|
|
366
|
-
handler: async (ctx, args) => {
|
|
367
|
-
const transfer = await ctx.db
|
|
368
|
-
.query("transfers")
|
|
369
|
-
.withIndex("by_stripe_transfer_id", (q) =>
|
|
370
|
-
q.eq("stripeTransferId", args.stripeTransferId),
|
|
371
|
-
)
|
|
372
|
-
.first();
|
|
373
|
-
|
|
374
|
-
if (!transfer) {
|
|
375
|
-
throw new Error(
|
|
376
|
-
`Cannot record reversal: no transfer found for ${args.stripeTransferId}`,
|
|
377
|
-
);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// `reversedAmount` is a cumulative total (matches Stripe's
|
|
381
|
-
// `Transfer.amount_reversed`). Keep it monotonic so a stale/out-of-order
|
|
382
|
-
// event can't shrink it or undo a full reversal.
|
|
383
|
-
const reversedAmount = Math.max(
|
|
384
|
-
transfer.reversedAmount ?? 0,
|
|
385
|
-
args.reversedAmount,
|
|
386
|
-
);
|
|
387
|
-
const derived = deriveReversalState(
|
|
388
|
-
transfer.amount,
|
|
389
|
-
reversedAmount,
|
|
390
|
-
transfer.status,
|
|
391
|
-
);
|
|
392
|
-
|
|
393
|
-
await ctx.db.patch("transfers", transfer._id, derived);
|
|
394
|
-
|
|
395
|
-
return null;
|
|
396
|
-
},
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
/** How `claimReversalSlices` sizes each leg's slice. */
|
|
400
|
-
const reversalModeValidator = v.union(
|
|
401
|
-
v.object({ kind: v.literal("full") }),
|
|
402
|
-
v.object({ kind: v.literal("percent"), percent: v.number() }),
|
|
403
|
-
v.object({ kind: v.literal("amount"), amount: v.number() }),
|
|
404
|
-
v.object({
|
|
405
|
-
kind: v.literal("fraction"),
|
|
406
|
-
chargeAmount: v.number(),
|
|
407
|
-
amountRefunded: v.number(),
|
|
408
|
-
}),
|
|
338
|
+
export const recordTransferReversal = mutation(
|
|
339
|
+
recordTransferReversalDefinition,
|
|
409
340
|
);
|
|
410
341
|
|
|
411
|
-
|
|
412
|
-
* Atomically claim reversal slices for one operation (a dispute or refund
|
|
413
|
-
* clawback) BEFORE any money moves (BTS-63). Runs in a single transaction, so
|
|
414
|
-
* concurrent operations serialize here instead of racing between the ledger
|
|
415
|
-
* read and the Stripe call:
|
|
416
|
-
*
|
|
417
|
-
* - First claim for an `operationId`: computes slices against each leg's
|
|
418
|
-
* claim frontier (`max(reversedAmount, reversalClaimedAmount)`), advances
|
|
419
|
-
* the frontier, and records the slices — atomically. A concurrent distinct
|
|
420
|
-
* operation claiming the same target gets an EMPTY claim, not a duplicate.
|
|
421
|
-
* - Any later call with the same `operationId` (webhook redelivery, retry
|
|
422
|
-
* after a partial failure) replays the recorded slices verbatim, so the
|
|
423
|
-
* caller re-sends byte-identical Stripe params under the same idempotency
|
|
424
|
-
* keys no matter how the ledger moved in between.
|
|
425
|
-
*
|
|
426
|
-
* Each returned slice carries the leg's current CONFIRMED `reversedAmount` so
|
|
427
|
-
* the executor can skip slices whose money already moved and was recorded.
|
|
428
|
-
*/
|
|
429
|
-
export const claimReversalSlices = mutation({
|
|
430
|
-
args: {
|
|
431
|
-
operationId: v.string(),
|
|
432
|
-
sourceChargeId: v.string(),
|
|
433
|
-
mode: reversalModeValidator,
|
|
434
|
-
},
|
|
435
|
-
returns: v.object({
|
|
436
|
-
replay: v.boolean(),
|
|
437
|
-
slices: v.array(
|
|
438
|
-
v.object({
|
|
439
|
-
stripeTransferId: v.string(),
|
|
440
|
-
from: v.number(),
|
|
441
|
-
to: v.number(),
|
|
442
|
-
confirmed: v.number(),
|
|
443
|
-
}),
|
|
444
|
-
),
|
|
445
|
-
}),
|
|
446
|
-
handler: async (ctx, args) => {
|
|
447
|
-
const confirmedFor = async (stripeTransferId: string) => {
|
|
448
|
-
const row = await ctx.db
|
|
449
|
-
.query("transfers")
|
|
450
|
-
.withIndex("by_stripe_transfer_id", (q) =>
|
|
451
|
-
q.eq("stripeTransferId", stripeTransferId),
|
|
452
|
-
)
|
|
453
|
-
.first();
|
|
454
|
-
return row?.reversedAmount ?? 0;
|
|
455
|
-
};
|
|
456
|
-
|
|
457
|
-
const existingOp = await ctx.db
|
|
458
|
-
.query("transferReversalOps")
|
|
459
|
-
.withIndex("by_operation_id", (q) =>
|
|
460
|
-
q.eq("operationId", args.operationId),
|
|
461
|
-
)
|
|
462
|
-
.first();
|
|
463
|
-
if (existingOp) {
|
|
464
|
-
const slices = [];
|
|
465
|
-
for (const s of existingOp.slices) {
|
|
466
|
-
slices.push({
|
|
467
|
-
...s,
|
|
468
|
-
confirmed: await confirmedFor(s.stripeTransferId),
|
|
469
|
-
});
|
|
470
|
-
}
|
|
471
|
-
return { replay: true, slices };
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// Original split legs only — reinstatement payouts are audit rows and
|
|
475
|
-
// must never be re-reversed (mirrors listTransfersByCharge).
|
|
476
|
-
// Claim every original split leg, including those beyond the former
|
|
477
|
-
// 50-row preview. Read/write limits abort the transaction before a partial
|
|
478
|
-
// claim can commit; no Stripe call happens until this mutation succeeds.
|
|
479
|
-
const rows: Doc<"transfers">[] = [];
|
|
480
|
-
for await (const transfer of ctx.db
|
|
481
|
-
.query("transfers")
|
|
482
|
-
.withIndex("by_source_charge_id", (q) =>
|
|
483
|
-
q.eq("sourceChargeId", args.sourceChargeId),
|
|
484
|
-
)) {
|
|
485
|
-
if (!transfer.reinstatement) rows.push(transfer);
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
const byId = new Map(rows.map((r) => [r.stripeTransferId, r]));
|
|
489
|
-
const slices = computeReversalSlices(
|
|
490
|
-
rows.map((t) => ({
|
|
491
|
-
stripeTransferId: t.stripeTransferId,
|
|
492
|
-
amount: t.amount,
|
|
493
|
-
frontier: Math.max(t.reversedAmount ?? 0, t.reversalClaimedAmount ?? 0),
|
|
494
|
-
})),
|
|
495
|
-
args.mode,
|
|
496
|
-
);
|
|
497
|
-
|
|
498
|
-
for (const s of slices) {
|
|
499
|
-
await ctx.db.patch("transfers", byId.get(s.stripeTransferId)!._id, {
|
|
500
|
-
reversalClaimedAmount: s.to,
|
|
501
|
-
});
|
|
502
|
-
}
|
|
503
|
-
// Record the operation even when it claims nothing, so a redelivery is a
|
|
504
|
-
// deterministic no-op instead of a recomputation against newer state.
|
|
505
|
-
await ctx.db.insert("transferReversalOps", {
|
|
506
|
-
operationId: args.operationId,
|
|
507
|
-
sourceChargeId: args.sourceChargeId,
|
|
508
|
-
slices,
|
|
509
|
-
});
|
|
510
|
-
|
|
511
|
-
return {
|
|
512
|
-
replay: false,
|
|
513
|
-
slices: slices.map((s) => ({
|
|
514
|
-
...s,
|
|
515
|
-
confirmed: byId.get(s.stripeTransferId)!.reversedAmount ?? 0,
|
|
516
|
-
})),
|
|
517
|
-
};
|
|
518
|
-
},
|
|
519
|
-
});
|
|
342
|
+
export const claimReversalSlices = mutation(claimReversalSlicesDefinition);
|
|
520
343
|
|
|
521
344
|
/**
|
|
522
345
|
* Release a permanently-dead reversal claim (BTS-74): rewind each of the
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { MutationCtx } from "../_generated/server.js";
|
|
2
|
+
import type { Doc } from "../_generated/dataModel.js";
|
|
3
|
+
type PayoutData = Omit<
|
|
4
|
+
Doc<"payouts">,
|
|
5
|
+
"_id" | "_creationTime" | "totalsRecorded"
|
|
6
|
+
>;
|
|
7
|
+
/** The caller's triggered DB maintains the existing exact currency summaries. */
|
|
8
|
+
export async function writePayout(ctx: MutationCtx, args: PayoutData) {
|
|
9
|
+
const existing = await ctx.db
|
|
10
|
+
.query("payouts")
|
|
11
|
+
.withIndex("by_stripe_payout_id", (q) =>
|
|
12
|
+
q.eq("stripePayoutId", args.stripePayoutId),
|
|
13
|
+
)
|
|
14
|
+
.first();
|
|
15
|
+
const id = existing?._id ?? (await ctx.db.insert("payouts", args));
|
|
16
|
+
if (existing)
|
|
17
|
+
await ctx.db.patch("payouts", id, {
|
|
18
|
+
...args,
|
|
19
|
+
failureCode: args.failureCode,
|
|
20
|
+
failureMessage: args.failureMessage,
|
|
21
|
+
});
|
|
22
|
+
const saved = await ctx.db.get("payouts", id);
|
|
23
|
+
if (!saved) throw new Error("Payout was not found after synchronization.");
|
|
24
|
+
return saved;
|
|
25
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { listWedgedReversalClaimsDefinition } from "./reversalDiagnostics.js";
|
|
1
2
|
import {
|
|
2
3
|
getAccountEarnings as getAccountEarningsLegacy,
|
|
3
4
|
getAccountPayouts as getAccountPayoutsLegacy,
|
|
@@ -24,7 +25,6 @@ import {
|
|
|
24
25
|
refundStatusValidator,
|
|
25
26
|
transferDocValidator,
|
|
26
27
|
transferReversalOpDocValidator,
|
|
27
|
-
wedgedReversalClaimValidator,
|
|
28
28
|
} from "./validators";
|
|
29
29
|
|
|
30
30
|
// PAYMENT QUERIES
|
|
@@ -394,52 +394,9 @@ export const getReversalOp = query({
|
|
|
394
394
|
* per-query document-read limit (16,384) — beyond which it throws (loud) rather
|
|
395
395
|
* than silently truncating, matching the BTS-64 aggregate convention.
|
|
396
396
|
*/
|
|
397
|
-
export const listWedgedReversalClaims = query(
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
handler: async (ctx) => {
|
|
401
|
-
// Collect the wedged legs first.
|
|
402
|
-
const wedged: Doc<"transfers">[] = [];
|
|
403
|
-
for await (const tr of ctx.db.query("transfers")) {
|
|
404
|
-
if ((tr.reversalClaimedAmount ?? 0) > (tr.reversedAmount ?? 0)) {
|
|
405
|
-
wedged.push(tr);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
if (wedged.length === 0) return [];
|
|
409
|
-
|
|
410
|
-
const wedgedIds = new Set(wedged.map((t) => t.stripeTransferId));
|
|
411
|
-
|
|
412
|
-
// Index the ops that hold an unexecuted slice on a wedged leg. A slice is
|
|
413
|
-
// unexecuted when its `to` exceeds the leg's confirmed `reversedAmount`.
|
|
414
|
-
const confirmedById = new Map(
|
|
415
|
-
wedged.map((t) => [t.stripeTransferId, t.reversedAmount ?? 0]),
|
|
416
|
-
);
|
|
417
|
-
const blockersByLeg = new Map<
|
|
418
|
-
string,
|
|
419
|
-
{ operationId: string; from: number; to: number }[]
|
|
420
|
-
>();
|
|
421
|
-
for await (const op of ctx.db.query("transferReversalOps")) {
|
|
422
|
-
for (const s of op.slices) {
|
|
423
|
-
if (!wedgedIds.has(s.stripeTransferId)) continue;
|
|
424
|
-
if (s.to <= (confirmedById.get(s.stripeTransferId) ?? 0)) continue;
|
|
425
|
-
const list = blockersByLeg.get(s.stripeTransferId) ?? [];
|
|
426
|
-
list.push({ operationId: op.operationId, from: s.from, to: s.to });
|
|
427
|
-
blockersByLeg.set(s.stripeTransferId, list);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
return wedged.map((t) => ({
|
|
432
|
-
stripeTransferId: t.stripeTransferId,
|
|
433
|
-
sourceChargeId: t.sourceChargeId,
|
|
434
|
-
amount: t.amount,
|
|
435
|
-
reversedAmount: t.reversedAmount ?? 0,
|
|
436
|
-
reversalClaimedAmount: t.reversalClaimedAmount ?? 0,
|
|
437
|
-
blockingOps: (blockersByLeg.get(t.stripeTransferId) ?? []).sort(
|
|
438
|
-
(a, b) => a.from - b.from,
|
|
439
|
-
),
|
|
440
|
-
}));
|
|
441
|
-
},
|
|
442
|
-
});
|
|
397
|
+
export const listWedgedReversalClaims = query(
|
|
398
|
+
listWedgedReversalClaimsDefinition,
|
|
399
|
+
);
|
|
443
400
|
|
|
444
401
|
// EARNINGS AGGREGATES (BTS-64)
|
|
445
402
|
//
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import type { MutationCtx } from "../_generated/server.js";
|
|
3
|
+
import { computeReversalSlices } from "../lib/reversals.js";
|
|
4
|
+
import {
|
|
5
|
+
applyCommissionExposure,
|
|
6
|
+
applyObligationExposure,
|
|
7
|
+
} from "../settlement/clawbacks.js";
|
|
8
|
+
import { commissionExposureValidator } from "../settlement/clawbackSchema.js";
|
|
9
|
+
import type { Infer } from "convex/values";
|
|
10
|
+
/** How `claimReversalSlices` sizes each leg's slice. */
|
|
11
|
+
const reversalModeValidator = v.union(
|
|
12
|
+
v.object({ kind: v.literal("full") }),
|
|
13
|
+
v.object({ kind: v.literal("percent"), percent: v.number() }),
|
|
14
|
+
v.object({ kind: v.literal("amount"), amount: v.number() }),
|
|
15
|
+
v.object({
|
|
16
|
+
kind: v.literal("fraction"),
|
|
17
|
+
chargeAmount: v.number(),
|
|
18
|
+
amountRefunded: v.number(),
|
|
19
|
+
}),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Atomically claim reversal slices for one operation (a dispute or refund
|
|
24
|
+
* clawback) BEFORE any money moves (BTS-63). Runs in a single transaction, so
|
|
25
|
+
* concurrent operations serialize here instead of racing between the ledger
|
|
26
|
+
* read and the Stripe call:
|
|
27
|
+
*
|
|
28
|
+
* - First claim for an `operationId`: computes slices against each leg's
|
|
29
|
+
* claim frontier (`max(reversedAmount, reversalClaimedAmount)`), advances
|
|
30
|
+
* the frontier, and records the slices — atomically. A concurrent distinct
|
|
31
|
+
* operation claiming the same target gets an EMPTY claim, not a duplicate.
|
|
32
|
+
* - Any later call with the same `operationId` (webhook redelivery, retry
|
|
33
|
+
* after a partial failure) replays the recorded slices verbatim, so the
|
|
34
|
+
* caller re-sends byte-identical Stripe params under the same idempotency
|
|
35
|
+
* keys no matter how the ledger moved in between.
|
|
36
|
+
*
|
|
37
|
+
* Each returned slice carries the leg's current CONFIRMED `reversedAmount` so
|
|
38
|
+
* the executor can skip slices whose money already moved and was recorded.
|
|
39
|
+
*/
|
|
40
|
+
export const claimReversalSlicesDefinition = {
|
|
41
|
+
args: {
|
|
42
|
+
operationId: v.string(),
|
|
43
|
+
sourceChargeId: v.string(),
|
|
44
|
+
mode: reversalModeValidator,
|
|
45
|
+
commissionExposure: v.optional(commissionExposureValidator),
|
|
46
|
+
},
|
|
47
|
+
returns: v.object({
|
|
48
|
+
replay: v.boolean(),
|
|
49
|
+
slices: v.array(
|
|
50
|
+
v.object({
|
|
51
|
+
stripeTransferId: v.string(),
|
|
52
|
+
from: v.number(),
|
|
53
|
+
to: v.number(),
|
|
54
|
+
confirmed: v.number(),
|
|
55
|
+
commissionTracked: v.optional(v.boolean()),
|
|
56
|
+
}),
|
|
57
|
+
),
|
|
58
|
+
}),
|
|
59
|
+
handler: async (
|
|
60
|
+
ctx: MutationCtx,
|
|
61
|
+
args: {
|
|
62
|
+
operationId: string;
|
|
63
|
+
sourceChargeId: string;
|
|
64
|
+
mode: Infer<typeof reversalModeValidator>;
|
|
65
|
+
commissionExposure?: Infer<typeof commissionExposureValidator>;
|
|
66
|
+
},
|
|
67
|
+
) => {
|
|
68
|
+
const confirmedFor = async (stripeTransferId: string) => {
|
|
69
|
+
const row = await ctx.db
|
|
70
|
+
.query("transfers")
|
|
71
|
+
.withIndex("by_stripe_transfer_id", (q) =>
|
|
72
|
+
q.eq("stripeTransferId", stripeTransferId),
|
|
73
|
+
)
|
|
74
|
+
.first();
|
|
75
|
+
return row?.reversedAmount ?? 0;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const isAffiliate = async (stripeTransferId: string) => {
|
|
79
|
+
const row = await ctx.db
|
|
80
|
+
.query("transfers")
|
|
81
|
+
.withIndex("by_stripe_transfer_id", (q) =>
|
|
82
|
+
q.eq("stripeTransferId", stripeTransferId),
|
|
83
|
+
)
|
|
84
|
+
.unique();
|
|
85
|
+
return row?.role === "affiliate";
|
|
86
|
+
};
|
|
87
|
+
// Fail atomically rather than commit a partial recovery beyond the bound.
|
|
88
|
+
const chargeRows = await ctx.db
|
|
89
|
+
.query("transfers")
|
|
90
|
+
.withIndex("by_source_charge_id", (q) =>
|
|
91
|
+
q.eq("sourceChargeId", args.sourceChargeId),
|
|
92
|
+
)
|
|
93
|
+
.take(1001);
|
|
94
|
+
if (chargeRows.length > 1000)
|
|
95
|
+
throw new Error("Charge transfer recovery limit exceeded.");
|
|
96
|
+
const rows = chargeRows.filter((t) => !t.reinstatement);
|
|
97
|
+
|
|
98
|
+
const existingOp = await ctx.db
|
|
99
|
+
.query("transferReversalOps")
|
|
100
|
+
.withIndex("by_operation_id", (q) =>
|
|
101
|
+
q.eq("operationId", args.operationId),
|
|
102
|
+
)
|
|
103
|
+
.first();
|
|
104
|
+
const exposure =
|
|
105
|
+
existingOp?.commissionExposure && args.commissionExposure
|
|
106
|
+
? {
|
|
107
|
+
...existingOp.commissionExposure,
|
|
108
|
+
finalized:
|
|
109
|
+
existingOp.commissionExposure.finalized ||
|
|
110
|
+
args.commissionExposure.finalized,
|
|
111
|
+
disputeState:
|
|
112
|
+
args.commissionExposure.disputeState ??
|
|
113
|
+
existingOp.commissionExposure.disputeState,
|
|
114
|
+
refundedPrincipal: args.commissionExposure.refundedPrincipal,
|
|
115
|
+
}
|
|
116
|
+
: existingOp
|
|
117
|
+
? undefined
|
|
118
|
+
: args.commissionExposure;
|
|
119
|
+
const targets = new Map<string, number>();
|
|
120
|
+
if (exposure) {
|
|
121
|
+
for (const transfer of rows)
|
|
122
|
+
if (transfer.role === "affiliate")
|
|
123
|
+
targets.set(
|
|
124
|
+
transfer.stripeTransferId,
|
|
125
|
+
await applyCommissionExposure(ctx, transfer, exposure),
|
|
126
|
+
);
|
|
127
|
+
const untransferred = await ctx.db
|
|
128
|
+
.query("affiliateObligations")
|
|
129
|
+
.withIndex("by_charge", (q) =>
|
|
130
|
+
q.eq("sourceChargeId", args.sourceChargeId),
|
|
131
|
+
)
|
|
132
|
+
.take(1001);
|
|
133
|
+
if (untransferred.length > 1000)
|
|
134
|
+
throw new Error("Affiliate obligation limit exceeded.");
|
|
135
|
+
for (const row of untransferred)
|
|
136
|
+
if (!row.stripeTransferId)
|
|
137
|
+
await applyObligationExposure(ctx, row, exposure);
|
|
138
|
+
}
|
|
139
|
+
if (existingOp) {
|
|
140
|
+
if (existingOp.sourceChargeId !== args.sourceChargeId)
|
|
141
|
+
throw new Error("Reversal operation source identity cannot change.");
|
|
142
|
+
if (existingOp.commissionExposure && args.commissionExposure) {
|
|
143
|
+
if (
|
|
144
|
+
existingOp.commissionExposure.kind !== args.commissionExposure.kind ||
|
|
145
|
+
existingOp.commissionExposure.chargeAmount !==
|
|
146
|
+
args.commissionExposure.chargeAmount
|
|
147
|
+
)
|
|
148
|
+
throw new Error("Commission operation identity cannot change.");
|
|
149
|
+
if (exposure)
|
|
150
|
+
await ctx.db.patch("transferReversalOps", existingOp._id, {
|
|
151
|
+
commissionExposure: exposure,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
const slices = [];
|
|
155
|
+
for (const s of existingOp.slices) {
|
|
156
|
+
slices.push({
|
|
157
|
+
...s,
|
|
158
|
+
confirmed: await confirmedFor(s.stripeTransferId),
|
|
159
|
+
...(existingOp.commissionExposure
|
|
160
|
+
? { commissionTracked: await isAffiliate(s.stripeTransferId) }
|
|
161
|
+
: {}),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return { replay: true, slices };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const byId = new Map(rows.map((r) => [r.stripeTransferId, r]));
|
|
168
|
+
let slices = computeReversalSlices(
|
|
169
|
+
rows.map((t) => ({
|
|
170
|
+
stripeTransferId: t.stripeTransferId,
|
|
171
|
+
amount: t.amount,
|
|
172
|
+
frontier: Math.max(t.reversedAmount ?? 0, t.reversalClaimedAmount ?? 0),
|
|
173
|
+
})),
|
|
174
|
+
args.mode,
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
if (args.commissionExposure) {
|
|
178
|
+
// Keep the shipped seller/other paths; affiliates converge on original
|
|
179
|
+
// commission exposure rather than a second incremental dispute debit.
|
|
180
|
+
const affiliates = rows.filter((t) => t.role === "affiliate");
|
|
181
|
+
slices = slices.filter(
|
|
182
|
+
(s) => byId.get(s.stripeTransferId)!.role !== "affiliate",
|
|
183
|
+
);
|
|
184
|
+
for (const transfer of affiliates) {
|
|
185
|
+
const target = Math.min(
|
|
186
|
+
transfer.amount,
|
|
187
|
+
targets.get(transfer.stripeTransferId) ?? 0,
|
|
188
|
+
);
|
|
189
|
+
const from = Math.max(
|
|
190
|
+
transfer.reversedAmount ?? 0,
|
|
191
|
+
transfer.reversalClaimedAmount ?? 0,
|
|
192
|
+
);
|
|
193
|
+
if (target > from)
|
|
194
|
+
slices.push({
|
|
195
|
+
stripeTransferId: transfer.stripeTransferId,
|
|
196
|
+
from,
|
|
197
|
+
to: target,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
for (const s of slices) {
|
|
203
|
+
await ctx.db.patch("transfers", byId.get(s.stripeTransferId)!._id, {
|
|
204
|
+
reversalClaimedAmount: s.to,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
// Record the operation even when it claims nothing, so a redelivery is a
|
|
208
|
+
// deterministic no-op instead of a recomputation against newer state.
|
|
209
|
+
await ctx.db.insert("transferReversalOps", {
|
|
210
|
+
operationId: args.operationId,
|
|
211
|
+
sourceChargeId: args.sourceChargeId,
|
|
212
|
+
slices,
|
|
213
|
+
...(args.commissionExposure
|
|
214
|
+
? { commissionExposure: args.commissionExposure }
|
|
215
|
+
: {}),
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
replay: false,
|
|
220
|
+
slices: slices.map((s) => ({
|
|
221
|
+
...s,
|
|
222
|
+
confirmed: byId.get(s.stripeTransferId)!.reversedAmount ?? 0,
|
|
223
|
+
...(args.commissionExposure
|
|
224
|
+
? {
|
|
225
|
+
commissionTracked:
|
|
226
|
+
byId.get(s.stripeTransferId)!.role === "affiliate",
|
|
227
|
+
}
|
|
228
|
+
: {}),
|
|
229
|
+
})),
|
|
230
|
+
};
|
|
231
|
+
},
|
|
232
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import type { QueryCtx } from "../_generated/server.js";
|
|
3
|
+
import type { Doc } from "../_generated/dataModel.js";
|
|
4
|
+
import { wedgedReversalClaimValidator } from "./validators.js";
|
|
5
|
+
|
|
6
|
+
export const listWedgedReversalClaimsDefinition = {
|
|
7
|
+
args: {},
|
|
8
|
+
returns: v.array(wedgedReversalClaimValidator),
|
|
9
|
+
handler: async (ctx: QueryCtx) => {
|
|
10
|
+
// Collect the wedged legs first.
|
|
11
|
+
const wedged: Doc<"transfers">[] = [];
|
|
12
|
+
for await (const tr of ctx.db.query("transfers")) {
|
|
13
|
+
const tracked =
|
|
14
|
+
tr.role === "affiliate"
|
|
15
|
+
? await ctx.db
|
|
16
|
+
.query("affiliateObligations")
|
|
17
|
+
.withIndex("by_transfer", (q) =>
|
|
18
|
+
q.eq("stripeTransferId", tr.stripeTransferId),
|
|
19
|
+
)
|
|
20
|
+
.unique()
|
|
21
|
+
: null;
|
|
22
|
+
if (tracked?.accounted) continue;
|
|
23
|
+
if ((tr.reversalClaimedAmount ?? 0) > (tr.reversedAmount ?? 0)) {
|
|
24
|
+
wedged.push(tr);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (wedged.length === 0) return [];
|
|
28
|
+
|
|
29
|
+
const wedgedIds = new Set(wedged.map((t) => t.stripeTransferId));
|
|
30
|
+
|
|
31
|
+
// Index the ops that hold an unexecuted slice on a wedged leg. A slice is
|
|
32
|
+
// unexecuted when its `to` exceeds the leg's confirmed `reversedAmount`.
|
|
33
|
+
const confirmedById = new Map(
|
|
34
|
+
wedged.map((t) => [t.stripeTransferId, t.reversedAmount ?? 0]),
|
|
35
|
+
);
|
|
36
|
+
const blockersByLeg = new Map<
|
|
37
|
+
string,
|
|
38
|
+
{ operationId: string; from: number; to: number }[]
|
|
39
|
+
>();
|
|
40
|
+
for await (const op of ctx.db.query("transferReversalOps")) {
|
|
41
|
+
for (const s of op.slices) {
|
|
42
|
+
if (!wedgedIds.has(s.stripeTransferId)) continue;
|
|
43
|
+
if (s.to <= (confirmedById.get(s.stripeTransferId) ?? 0)) continue;
|
|
44
|
+
const list = blockersByLeg.get(s.stripeTransferId) ?? [];
|
|
45
|
+
list.push({ operationId: op.operationId, from: s.from, to: s.to });
|
|
46
|
+
blockersByLeg.set(s.stripeTransferId, list);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return wedged.map((t) => ({
|
|
51
|
+
stripeTransferId: t.stripeTransferId,
|
|
52
|
+
sourceChargeId: t.sourceChargeId,
|
|
53
|
+
amount: t.amount,
|
|
54
|
+
reversedAmount: t.reversedAmount ?? 0,
|
|
55
|
+
reversalClaimedAmount: t.reversalClaimedAmount ?? 0,
|
|
56
|
+
blockingOps: (blockersByLeg.get(t.stripeTransferId) ?? []).sort(
|
|
57
|
+
(a, b) => a.from - b.from,
|
|
58
|
+
),
|
|
59
|
+
}));
|
|
60
|
+
},
|
|
61
|
+
};
|