@fantastic.dev/better-stripe 0.23.2 → 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 +20 -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 +494 -2
- package/dist/client/connect/payoutAdoptionTestHelpers.d.ts.map +1 -1
- package/dist/client/connect/payoutSyncTestHelpers.d.ts +247 -1
- package/dist/client/connect/payoutSyncTestHelpers.d.ts.map +1 -1
- 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 +3 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +6 -1
- package/dist/client/index.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 +491 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/affiliateEarnings/mutations.d.ts +22 -0
- package/dist/component/affiliateEarnings/mutations.d.ts.map +1 -0
- package/dist/component/affiliateEarnings/mutations.js +160 -0
- package/dist/component/affiliateEarnings/mutations.js.map +1 -0
- package/dist/component/affiliatePayouts/mutations.d.ts +25 -0
- package/dist/component/affiliatePayouts/mutations.d.ts.map +1 -0
- package/dist/component/affiliatePayouts/mutations.js +104 -0
- package/dist/component/affiliatePayouts/mutations.js.map +1 -0
- package/dist/component/affiliatePayouts/queries.d.ts +23 -0
- package/dist/component/affiliatePayouts/queries.d.ts.map +1 -0
- package/dist/component/affiliatePayouts/queries.js +23 -0
- package/dist/component/affiliatePayouts/queries.js.map +1 -0
- package/dist/component/affiliatePayouts/recipientGuard.d.ts +12 -0
- package/dist/component/affiliatePayouts/recipientGuard.d.ts.map +1 -0
- package/dist/component/affiliatePayouts/recipientGuard.js +26 -0
- package/dist/component/affiliatePayouts/recipientGuard.js.map +1 -0
- package/dist/component/affiliateRecovery/accounting.d.ts +28 -0
- package/dist/component/affiliateRecovery/accounting.d.ts.map +1 -0
- package/dist/component/affiliateRecovery/accounting.js +71 -0
- package/dist/component/affiliateRecovery/accounting.js.map +1 -0
- package/dist/component/affiliateRecovery/mutations.d.ts +65 -0
- package/dist/component/affiliateRecovery/mutations.d.ts.map +1 -0
- package/dist/component/affiliateRecovery/mutations.js +250 -0
- package/dist/component/affiliateRecovery/mutations.js.map +1 -0
- package/dist/component/affiliateRecovery/queries.d.ts +305 -0
- package/dist/component/affiliateRecovery/queries.d.ts.map +1 -0
- package/dist/component/affiliateRecovery/queries.js +113 -0
- package/dist/component/affiliateRecovery/queries.js.map +1 -0
- package/dist/component/affiliateRecovery/schema.d.ts +171 -0
- package/dist/component/affiliateRecovery/schema.d.ts.map +1 -0
- package/dist/component/affiliateRecovery/schema.js +71 -0
- package/dist/component/affiliateRecovery/schema.js.map +1 -0
- package/dist/component/connect/mutations.d.ts +36 -23
- package/dist/component/connect/mutations.d.ts.map +1 -1
- package/dist/component/connect/mutations.js +7 -124
- package/dist/component/connect/mutations.js.map +1 -1
- package/dist/component/connect/queries.d.ts +10 -0
- package/dist/component/connect/queries.d.ts.map +1 -1
- package/dist/component/connect/queries.js +3 -40
- package/dist/component/connect/queries.js.map +1 -1
- package/dist/component/connect/reversalClaims.d.ts +169 -0
- package/dist/component/connect/reversalClaims.d.ts.map +1 -0
- package/dist/component/connect/reversalClaims.js +176 -0
- package/dist/component/connect/reversalClaims.js.map +1 -0
- package/dist/component/connect/reversalDiagnostics.d.ts +59 -0
- package/dist/component/connect/reversalDiagnostics.d.ts.map +1 -0
- package/dist/component/connect/reversalDiagnostics.js +50 -0
- package/dist/component/connect/reversalDiagnostics.js.map +1 -0
- package/dist/component/connect/reversalReceipts.d.ts +27 -0
- package/dist/component/connect/reversalReceipts.d.ts.map +1 -0
- package/dist/component/connect/reversalReceipts.js +85 -0
- package/dist/component/connect/reversalReceipts.js.map +1 -0
- package/dist/component/connect/schema.d.ts +31 -1
- package/dist/component/connect/schema.d.ts.map +1 -1
- package/dist/component/connect/schema.js +3 -1
- package/dist/component/connect/schema.js.map +1 -1
- package/dist/component/connect/validators.d.ts +49 -1
- package/dist/component/connect/validators.d.ts.map +1 -1
- package/dist/component/connect/validators.js +2 -0
- package/dist/component/connect/validators.js.map +1 -1
- package/dist/component/earnings/triggers.d.ts +410 -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/schema.d.ts +247 -1
- package/dist/component/schema.js +9 -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/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/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 +6 -7
- 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 +24 -0
- package/src/component/_generated/component.ts +614 -1
- package/src/component/affiliate-clawbacks.test.ts +370 -0
- package/src/component/affiliate-component-pagination.test.ts +83 -0
- package/src/component/affiliate-direct-payout.test.ts +160 -0
- package/src/component/affiliate-recovery-lifecycle.test.ts +587 -0
- package/src/component/affiliateEarnings/mutations.ts +196 -0
- package/src/component/affiliatePayouts/mutations.ts +121 -0
- package/src/component/affiliatePayouts/queries.ts +28 -0
- package/src/component/affiliatePayouts/recipientGuard.ts +42 -0
- package/src/component/affiliateRecovery/accounting.ts +111 -0
- package/src/component/affiliateRecovery/mutations.ts +275 -0
- package/src/component/affiliateRecovery/queries.ts +138 -0
- package/src/component/affiliateRecovery/schema.ts +86 -0
- package/src/component/connect/mutations.ts +9 -160
- package/src/component/connect/queries.ts +4 -47
- package/src/component/connect/reversalClaims.ts +232 -0
- package/src/component/connect/reversalDiagnostics.ts +61 -0
- package/src/component/connect/reversalReceipts.ts +147 -0
- package/src/component/connect/schema.ts +3 -3
- package/src/component/connect/validators.ts +2 -0
- package/src/component/payout-recovery-integration.test.ts +172 -0
- package/src/component/payoutRelease/mutations.ts +9 -0
- package/src/component/payoutRelease/queries.ts +3 -1
- package/src/component/schema.ts +18 -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,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
|
+
});
|
|
@@ -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"),
|
package/src/component/schema.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { claimsTable as payoutSyncClaimsTable } from "./payoutSync/schema.js";
|
|
2
|
+
import {
|
|
3
|
+
payoutReleasesTable as affiliatePayoutReleases,
|
|
4
|
+
operationsTable as affiliateOperations,
|
|
5
|
+
earningsTable as affiliateEarnings,
|
|
6
|
+
offsetsTable as affiliateOffsets,
|
|
7
|
+
balancesTable as affiliateBalances,
|
|
8
|
+
} from "./affiliateRecovery/schema.js";
|
|
9
|
+
import {
|
|
10
|
+
affiliateObligationsTable,
|
|
11
|
+
affiliateReversalReceiptsTable,
|
|
12
|
+
} from "./settlement/clawbackSchema.js";
|
|
2
13
|
import {
|
|
3
14
|
feeBalancesTable as payoutFeeBalancesTable,
|
|
4
15
|
claimsTable as payoutReleaseClaimsTable,
|
|
@@ -38,6 +49,13 @@ import { pricesTable, productsTable } from "./products/schema";
|
|
|
38
49
|
import { webhookEventsTable } from "./webhooks/schema";
|
|
39
50
|
|
|
40
51
|
export default defineSchema({
|
|
52
|
+
affiliateObligations: affiliateObligationsTable,
|
|
53
|
+
affiliateRecoveryOperations: affiliateOperations,
|
|
54
|
+
affiliatePayoutReleases,
|
|
55
|
+
affiliateEarnings,
|
|
56
|
+
affiliateOffsets,
|
|
57
|
+
affiliateRecoveryBalances: affiliateBalances,
|
|
58
|
+
affiliateReversalReceipts: affiliateReversalReceiptsTable,
|
|
41
59
|
payoutSyncClaims: payoutSyncClaimsTable,
|
|
42
60
|
payoutFeeBalances: payoutFeeBalancesTable,
|
|
43
61
|
payoutReleaseClaims: payoutReleaseClaimsTable,
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { defineTable } from "convex/server";
|
|
2
|
+
import { v } from "convex/values";
|
|
3
|
+
|
|
4
|
+
/** Principal snapshots on the original charge axis, never incremental debits. */
|
|
5
|
+
export const commissionExposureValidator = v.object({
|
|
6
|
+
kind: v.union(v.literal("refund"), v.literal("dispute")),
|
|
7
|
+
chargeAmount: v.number(),
|
|
8
|
+
principal: v.number(),
|
|
9
|
+
finalized: v.boolean(),
|
|
10
|
+
disputeState: v.optional(
|
|
11
|
+
v.union(v.literal("open"), v.literal("lost"), v.literal("won")),
|
|
12
|
+
),
|
|
13
|
+
disputeId: v.optional(v.string()),
|
|
14
|
+
refundedPrincipal: v.optional(v.number()),
|
|
15
|
+
refundId: v.optional(v.string()),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const obligationFields = {
|
|
19
|
+
stripeTransferId: v.optional(v.string()),
|
|
20
|
+
sourceChargeId: v.string(),
|
|
21
|
+
sourceInvoiceId: v.optional(v.string()),
|
|
22
|
+
paymentIntentId: v.optional(v.string()),
|
|
23
|
+
paymentId: v.optional(v.string()),
|
|
24
|
+
storeAccountId: v.optional(v.string()),
|
|
25
|
+
lastRefundId: v.optional(v.string()),
|
|
26
|
+
affiliateAccountId: v.string(),
|
|
27
|
+
currency: v.string(),
|
|
28
|
+
chargeAmount: v.number(),
|
|
29
|
+
originalCommission: v.number(),
|
|
30
|
+
refundedPrincipal: v.number(),
|
|
31
|
+
disputedPrincipal: v.number(),
|
|
32
|
+
lostPrincipal: v.number(),
|
|
33
|
+
finalizedAmount: v.number(),
|
|
34
|
+
provisionalAmount: v.number(),
|
|
35
|
+
/** Confirmed transfer reversal total; reservations are not receipts. */
|
|
36
|
+
recoveredAmount: v.number(),
|
|
37
|
+
/** Existing reversals may have no attributable refund/dispute receipt. */
|
|
38
|
+
needsReconciliation: v.boolean(),
|
|
39
|
+
accounted: v.optional(v.boolean()),
|
|
40
|
+
legacyReceiptIds: v.optional(v.array(v.string())),
|
|
41
|
+
legacyRestorationIds: v.optional(v.array(v.string())),
|
|
42
|
+
operatorNote: v.optional(v.string()),
|
|
43
|
+
earningId: v.optional(v.id("affiliateEarnings")),
|
|
44
|
+
releasedCommission: v.optional(v.number()),
|
|
45
|
+
offsetAmount: v.optional(v.number()),
|
|
46
|
+
restoredAmount: v.optional(v.number()),
|
|
47
|
+
reservedRecovery: v.optional(v.number()),
|
|
48
|
+
reservedRestoration: v.optional(v.number()),
|
|
49
|
+
debtAmount: v.optional(v.number()),
|
|
50
|
+
creditAmount: v.optional(v.number()),
|
|
51
|
+
debtStatus: v.optional(v.union(v.literal("open"), v.literal("settled"))),
|
|
52
|
+
disputeState: v.optional(
|
|
53
|
+
v.union(v.literal("open"), v.literal("lost"), v.literal("won")),
|
|
54
|
+
),
|
|
55
|
+
disputeId: v.optional(v.string()),
|
|
56
|
+
activeOperationId: v.optional(v.id("affiliateRecoveryOperations")),
|
|
57
|
+
lastState: v.optional(v.string()),
|
|
58
|
+
lastError: v.optional(v.string()),
|
|
59
|
+
operationSequence: v.optional(v.number()),
|
|
60
|
+
};
|
|
61
|
+
export const affiliateObligationsTable = defineTable(obligationFields)
|
|
62
|
+
.index("by_transfer", ["stripeTransferId"])
|
|
63
|
+
.index("by_charge", ["sourceChargeId"])
|
|
64
|
+
.index("by_affiliate_currency_debt", [
|
|
65
|
+
"affiliateAccountId",
|
|
66
|
+
"currency",
|
|
67
|
+
"debtStatus",
|
|
68
|
+
])
|
|
69
|
+
.index("by_affiliate_currency", ["affiliateAccountId", "currency"]);
|
|
70
|
+
export const affiliateReversalReceiptsTable = defineTable({
|
|
71
|
+
stripeReversalId: v.string(),
|
|
72
|
+
stripeTransferId: v.string(),
|
|
73
|
+
operationId: v.string(),
|
|
74
|
+
amount: v.number(),
|
|
75
|
+
})
|
|
76
|
+
.index("by_receipt", ["stripeReversalId"])
|
|
77
|
+
.index("by_transfer_operation", ["stripeTransferId", "operationId"])
|
|
78
|
+
.index("by_transfer", ["stripeTransferId"]);
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { updateObligation } from "../affiliateRecovery/accounting.js";
|
|
2
|
+
import type { Infer } from "convex/values";
|
|
3
|
+
import type { Doc } from "../_generated/dataModel.js";
|
|
4
|
+
import type { MutationCtx } from "../_generated/server.js";
|
|
5
|
+
import { commissionExposureValidator } from "./clawbackSchema.js";
|
|
6
|
+
|
|
7
|
+
export type CommissionExposure = Infer<typeof commissionExposureValidator>;
|
|
8
|
+
|
|
9
|
+
/** Exact round-half-up proportional commission in integer currency units. */
|
|
10
|
+
export function proportionalCommission(
|
|
11
|
+
commission: number,
|
|
12
|
+
principal: number,
|
|
13
|
+
charge: number,
|
|
14
|
+
): number {
|
|
15
|
+
for (const n of [commission, principal, charge]) {
|
|
16
|
+
if (!Number.isSafeInteger(n) || n < 0)
|
|
17
|
+
throw new Error(
|
|
18
|
+
"Commission recovery requires non-negative integer units.",
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
if (charge === 0)
|
|
22
|
+
throw new Error("Commission recovery requires a positive charge.");
|
|
23
|
+
const numerator = BigInt(commission) * BigInt(Math.min(principal, charge));
|
|
24
|
+
return Number((2n * numerator + BigInt(charge)) / (2n * BigInt(charge)));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Lazily migrate one immutable original affiliate leg, also used by backfill. */
|
|
28
|
+
export async function ensureCommissionObligation(
|
|
29
|
+
ctx: MutationCtx,
|
|
30
|
+
transfer: Doc<"transfers">,
|
|
31
|
+
chargeAmount: number,
|
|
32
|
+
) {
|
|
33
|
+
if (
|
|
34
|
+
transfer.role !== "affiliate" ||
|
|
35
|
+
transfer.reinstatement ||
|
|
36
|
+
!transfer.sourceChargeId
|
|
37
|
+
)
|
|
38
|
+
throw new Error(
|
|
39
|
+
"Commission obligation requires an original affiliate transfer.",
|
|
40
|
+
);
|
|
41
|
+
const existing = await ctx.db
|
|
42
|
+
.query("affiliateObligations")
|
|
43
|
+
.withIndex("by_transfer", (q) =>
|
|
44
|
+
q.eq("stripeTransferId", transfer.stripeTransferId),
|
|
45
|
+
)
|
|
46
|
+
.unique();
|
|
47
|
+
if (existing) {
|
|
48
|
+
if (
|
|
49
|
+
existing.chargeAmount !== chargeAmount ||
|
|
50
|
+
(existing.releasedCommission ?? existing.originalCommission) !==
|
|
51
|
+
transfer.amount ||
|
|
52
|
+
existing.affiliateAccountId !== transfer.destinationAccountId ||
|
|
53
|
+
existing.currency !== transfer.currency ||
|
|
54
|
+
existing.sourceChargeId !== transfer.sourceChargeId ||
|
|
55
|
+
(transfer.sourceInvoiceId !== undefined &&
|
|
56
|
+
existing.sourceInvoiceId !== transfer.sourceInvoiceId)
|
|
57
|
+
)
|
|
58
|
+
throw new Error("Original affiliate commission identity cannot change.");
|
|
59
|
+
return existing;
|
|
60
|
+
}
|
|
61
|
+
proportionalCommission(transfer.amount, 0, chargeAmount);
|
|
62
|
+
const allocation = await ctx.db
|
|
63
|
+
.query("settlementAllocations")
|
|
64
|
+
.withIndex("by_source_charge", (q) =>
|
|
65
|
+
q.eq("sourceChargeId", transfer.sourceChargeId!),
|
|
66
|
+
)
|
|
67
|
+
.unique();
|
|
68
|
+
const earning = await ctx.db
|
|
69
|
+
.query("affiliateEarnings")
|
|
70
|
+
.withIndex("by_charge_affiliate", (q) =>
|
|
71
|
+
q
|
|
72
|
+
.eq("sourceChargeId", transfer.sourceChargeId!)
|
|
73
|
+
.eq("affiliateAccountId", transfer.destinationAccountId),
|
|
74
|
+
)
|
|
75
|
+
.unique();
|
|
76
|
+
if (
|
|
77
|
+
allocation &&
|
|
78
|
+
(allocation.gross !== chargeAmount ||
|
|
79
|
+
allocation.currency !== transfer.currency ||
|
|
80
|
+
!allocation.transfers.some(
|
|
81
|
+
(leg) =>
|
|
82
|
+
leg.role === "affiliate" &&
|
|
83
|
+
leg.destinationAccountId === transfer.destinationAccountId &&
|
|
84
|
+
leg.amount === (earning?.originalCommission ?? transfer.amount),
|
|
85
|
+
))
|
|
86
|
+
)
|
|
87
|
+
throw new Error("Commission recovery conflicts with original settlement.");
|
|
88
|
+
const recoveredAmount = transfer.reversedAmount ?? 0;
|
|
89
|
+
if (
|
|
90
|
+
!Number.isSafeInteger(recoveredAmount) ||
|
|
91
|
+
recoveredAmount < 0 ||
|
|
92
|
+
recoveredAmount > transfer.amount
|
|
93
|
+
)
|
|
94
|
+
throw new Error("Invalid original affiliate reversal balance.");
|
|
95
|
+
const id = await ctx.db.insert("affiliateObligations", {
|
|
96
|
+
stripeTransferId: transfer.stripeTransferId,
|
|
97
|
+
sourceChargeId: transfer.sourceChargeId,
|
|
98
|
+
sourceInvoiceId: transfer.sourceInvoiceId ?? allocation?.sourceInvoiceId,
|
|
99
|
+
paymentIntentId: allocation?.paymentIntentId,
|
|
100
|
+
paymentId: transfer.paymentId ?? allocation?.paymentId,
|
|
101
|
+
storeAccountId: allocation?.transfers.find((leg) => leg.role === "store")
|
|
102
|
+
?.destinationAccountId,
|
|
103
|
+
affiliateAccountId: transfer.destinationAccountId,
|
|
104
|
+
currency: transfer.currency,
|
|
105
|
+
chargeAmount,
|
|
106
|
+
originalCommission: earning?.originalCommission ?? transfer.amount,
|
|
107
|
+
releasedCommission: transfer.amount,
|
|
108
|
+
earningId: earning?._id,
|
|
109
|
+
refundedPrincipal: 0,
|
|
110
|
+
disputedPrincipal: 0,
|
|
111
|
+
lostPrincipal: 0,
|
|
112
|
+
finalizedAmount: 0,
|
|
113
|
+
provisionalAmount: 0,
|
|
114
|
+
recoveredAmount,
|
|
115
|
+
needsReconciliation:
|
|
116
|
+
recoveredAmount > 0 || (transfer.reversalClaimedAmount ?? 0) > 0,
|
|
117
|
+
});
|
|
118
|
+
return (await ctx.db.get("affiliateObligations", id))!;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Refunds and disputes share the same original-principal exposure axis. */
|
|
122
|
+
export async function applyCommissionExposure(
|
|
123
|
+
ctx: MutationCtx,
|
|
124
|
+
transfer: Doc<"transfers">,
|
|
125
|
+
exposure: CommissionExposure,
|
|
126
|
+
) {
|
|
127
|
+
proportionalCommission(
|
|
128
|
+
transfer.amount,
|
|
129
|
+
exposure.principal,
|
|
130
|
+
exposure.chargeAmount,
|
|
131
|
+
);
|
|
132
|
+
if (exposure.kind === "refund" && !exposure.finalized)
|
|
133
|
+
throw new Error("Pending refunds cannot create commission obligations.");
|
|
134
|
+
const row = await ensureCommissionObligation(
|
|
135
|
+
ctx,
|
|
136
|
+
transfer,
|
|
137
|
+
exposure.chargeAmount,
|
|
138
|
+
);
|
|
139
|
+
return applyObligationExposure(ctx, row, exposure);
|
|
140
|
+
}
|
|
141
|
+
export async function applyObligationExposure(
|
|
142
|
+
ctx: MutationCtx,
|
|
143
|
+
row: Doc<"affiliateObligations">,
|
|
144
|
+
exposure: CommissionExposure,
|
|
145
|
+
) {
|
|
146
|
+
proportionalCommission(
|
|
147
|
+
row.originalCommission,
|
|
148
|
+
exposure.principal,
|
|
149
|
+
exposure.chargeAmount,
|
|
150
|
+
);
|
|
151
|
+
if (row.chargeAmount !== exposure.chargeAmount)
|
|
152
|
+
throw new Error("Original charge amount cannot change.");
|
|
153
|
+
const principal = Math.min(exposure.principal, exposure.chargeAmount);
|
|
154
|
+
const refundedPrincipal = Math.max(
|
|
155
|
+
row.refundedPrincipal,
|
|
156
|
+
exposure.refundedPrincipal ?? 0,
|
|
157
|
+
exposure.kind === "refund" ? principal : 0,
|
|
158
|
+
);
|
|
159
|
+
let disputedPrincipal = Math.max(
|
|
160
|
+
row.disputedPrincipal,
|
|
161
|
+
exposure.kind === "dispute" ? principal : 0,
|
|
162
|
+
);
|
|
163
|
+
let lostPrincipal = Math.max(
|
|
164
|
+
row.lostPrincipal,
|
|
165
|
+
exposure.kind === "dispute" && exposure.finalized ? principal : 0,
|
|
166
|
+
);
|
|
167
|
+
const incomingState =
|
|
168
|
+
exposure.disputeState ?? (exposure.finalized ? "lost" : "open");
|
|
169
|
+
const rank = { open: 0, lost: 1, won: 2 };
|
|
170
|
+
const disputeState =
|
|
171
|
+
exposure.kind === "dispute" &&
|
|
172
|
+
rank[incomingState] >= rank[row.disputeState ?? "open"]
|
|
173
|
+
? incomingState
|
|
174
|
+
: row.disputeState;
|
|
175
|
+
if (disputeState === "won") {
|
|
176
|
+
disputedPrincipal = 0;
|
|
177
|
+
lostPrincipal = 0;
|
|
178
|
+
}
|
|
179
|
+
if (exposure.kind === "dispute" && disputeState === "lost")
|
|
180
|
+
lostPrincipal = Math.max(lostPrincipal, disputedPrincipal);
|
|
181
|
+
const finalizedAmount = proportionalCommission(
|
|
182
|
+
row.originalCommission,
|
|
183
|
+
Math.max(refundedPrincipal, lostPrincipal),
|
|
184
|
+
row.chargeAmount,
|
|
185
|
+
);
|
|
186
|
+
const target = proportionalCommission(
|
|
187
|
+
row.originalCommission,
|
|
188
|
+
Math.max(refundedPrincipal, disputedPrincipal),
|
|
189
|
+
row.chargeAmount,
|
|
190
|
+
);
|
|
191
|
+
await updateObligation(ctx, row, {
|
|
192
|
+
disputeState,
|
|
193
|
+
lastRefundId: exposure.refundId ?? row.lastRefundId,
|
|
194
|
+
disputeId: exposure.disputeId ?? row.disputeId,
|
|
195
|
+
refundedPrincipal,
|
|
196
|
+
disputedPrincipal,
|
|
197
|
+
lostPrincipal,
|
|
198
|
+
finalizedAmount,
|
|
199
|
+
provisionalAmount: target - finalizedAmount,
|
|
200
|
+
});
|
|
201
|
+
return target;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Webhook upserts cannot rewrite a frozen commission or its confirmed balance. */
|
|
205
|
+
export async function syncCommissionTransfer(
|
|
206
|
+
ctx: MutationCtx,
|
|
207
|
+
transfer: Doc<"transfers">,
|
|
208
|
+
) {
|
|
209
|
+
const row = await ctx.db
|
|
210
|
+
.query("affiliateObligations")
|
|
211
|
+
.withIndex("by_transfer", (q) =>
|
|
212
|
+
q.eq("stripeTransferId", transfer.stripeTransferId),
|
|
213
|
+
)
|
|
214
|
+
.unique();
|
|
215
|
+
if (!row) return;
|
|
216
|
+
await ensureCommissionObligation(ctx, transfer, row.chargeAmount);
|
|
217
|
+
const recoveredAmount = Math.max(
|
|
218
|
+
row.recoveredAmount,
|
|
219
|
+
transfer.reversedAmount ?? 0,
|
|
220
|
+
);
|
|
221
|
+
if (
|
|
222
|
+
!Number.isSafeInteger(recoveredAmount) ||
|
|
223
|
+
recoveredAmount > row.originalCommission
|
|
224
|
+
)
|
|
225
|
+
throw new Error("Invalid confirmed affiliate reversal balance.");
|
|
226
|
+
if (recoveredAmount > row.recoveredAmount && row.activeOperationId) return;
|
|
227
|
+
await updateObligation(ctx, row, {
|
|
228
|
+
recoveredAmount,
|
|
229
|
+
...(recoveredAmount > row.recoveredAmount && row.accounted
|
|
230
|
+
? { needsReconciliation: true }
|
|
231
|
+
: {}),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { paginator } from "convex-helpers/server/pagination";
|
|
2
|
+
import schema from "../schema.js";
|
|
3
|
+
import { updateObligation } from "../affiliateRecovery/accounting.js";
|
|
4
|
+
import { paginationOptsValidator } from "convex/server";
|
|
5
|
+
import { ensureCommissionObligation } from "./clawbacks.js";
|
|
1
6
|
import { v } from "convex/values";
|
|
2
7
|
import { mutation } from "../_generated/server.js";
|
|
3
8
|
import { allocationFields, commissionFields, termValidator } from "./schema.js";
|
|
@@ -117,3 +122,52 @@ export const claimCommissionInvoice = mutation({
|
|
|
117
122
|
};
|
|
118
123
|
},
|
|
119
124
|
});
|
|
125
|
+
|
|
126
|
+
/** Explicit, resumable migration. Never fabricate legacy refund provenance. */
|
|
127
|
+
export const migrateAffiliateObligations = mutation({
|
|
128
|
+
args: { paginationOpts: paginationOptsValidator },
|
|
129
|
+
returns: v.object({
|
|
130
|
+
migrated: v.number(),
|
|
131
|
+
skipped: v.number(),
|
|
132
|
+
isDone: v.boolean(),
|
|
133
|
+
continueCursor: v.string(),
|
|
134
|
+
}),
|
|
135
|
+
handler: async (ctx, args) => {
|
|
136
|
+
const page = await paginator(ctx.db, schema)
|
|
137
|
+
.query("transfers")
|
|
138
|
+
.paginate({
|
|
139
|
+
...args.paginationOpts,
|
|
140
|
+
numItems: Math.min(100, args.paginationOpts.numItems),
|
|
141
|
+
});
|
|
142
|
+
let migrated = 0;
|
|
143
|
+
let skipped = 0;
|
|
144
|
+
for (const transfer of page.page) {
|
|
145
|
+
if (transfer.role !== "affiliate" || transfer.reinstatement) continue;
|
|
146
|
+
const allocation = transfer.sourceChargeId
|
|
147
|
+
? await ctx.db
|
|
148
|
+
.query("settlementAllocations")
|
|
149
|
+
.withIndex("by_source_charge", (q) =>
|
|
150
|
+
q.eq("sourceChargeId", transfer.sourceChargeId!),
|
|
151
|
+
)
|
|
152
|
+
.unique()
|
|
153
|
+
: null;
|
|
154
|
+
if (!allocation) {
|
|
155
|
+
skipped++;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const obligation = await ensureCommissionObligation(
|
|
159
|
+
ctx,
|
|
160
|
+
transfer,
|
|
161
|
+
allocation.gross,
|
|
162
|
+
);
|
|
163
|
+
await updateObligation(ctx, obligation, {});
|
|
164
|
+
migrated++;
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
migrated,
|
|
168
|
+
skipped,
|
|
169
|
+
isDone: page.isDone,
|
|
170
|
+
continueCursor: page.continueCursor,
|
|
171
|
+
};
|
|
172
|
+
},
|
|
173
|
+
});
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import { paginator } from "convex-helpers/server/pagination";
|
|
2
|
+
import schema from "../schema.js";
|
|
3
|
+
import {
|
|
4
|
+
paginationOptsValidator,
|
|
5
|
+
paginationResultValidator,
|
|
6
|
+
} from "convex/server";
|
|
7
|
+
import { obligationFields } from "./clawbackSchema.js";
|
|
1
8
|
import { v } from "convex/values";
|
|
2
9
|
import { query } from "../_generated/server.js";
|
|
3
10
|
import { allocationFields, commissionFields } from "./schema.js";
|
|
@@ -36,3 +43,31 @@ export const getCommissionTerm = query({
|
|
|
36
43
|
};
|
|
37
44
|
},
|
|
38
45
|
});
|
|
46
|
+
|
|
47
|
+
/** Trusted host API: authorize account ownership before forwarding arguments. */
|
|
48
|
+
export const listAffiliateObligations = query({
|
|
49
|
+
args: {
|
|
50
|
+
affiliateAccountId: v.string(),
|
|
51
|
+
currency: v.string(),
|
|
52
|
+
paginationOpts: paginationOptsValidator,
|
|
53
|
+
},
|
|
54
|
+
returns: paginationResultValidator(
|
|
55
|
+
v.object({
|
|
56
|
+
...obligationFields,
|
|
57
|
+
_id: v.id("affiliateObligations"),
|
|
58
|
+
_creationTime: v.number(),
|
|
59
|
+
}),
|
|
60
|
+
),
|
|
61
|
+
handler: (ctx, args) =>
|
|
62
|
+
paginator(ctx.db, schema)
|
|
63
|
+
.query("affiliateObligations")
|
|
64
|
+
.withIndex("by_affiliate_currency", (q) =>
|
|
65
|
+
q
|
|
66
|
+
.eq("affiliateAccountId", args.affiliateAccountId)
|
|
67
|
+
.eq("currency", args.currency),
|
|
68
|
+
)
|
|
69
|
+
.paginate({
|
|
70
|
+
...args.paginationOpts,
|
|
71
|
+
numItems: Math.min(100, args.paginationOpts.numItems),
|
|
72
|
+
}),
|
|
73
|
+
});
|