@fantastic.dev/better-stripe 0.22.0 → 0.23.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 +6 -0
- package/README.md +5 -3
- package/dist/client/connect/payoutActivationProof.d.ts +16 -0
- package/dist/client/connect/payoutActivationProof.d.ts.map +1 -0
- package/dist/client/connect/payoutActivationProof.js +72 -0
- package/dist/client/connect/payoutActivationProof.js.map +1 -0
- package/dist/client/connect/payoutAdoption.d.ts +35 -0
- package/dist/client/connect/payoutAdoption.d.ts.map +1 -0
- package/dist/client/connect/payoutAdoption.js +230 -0
- package/dist/client/connect/payoutAdoption.js.map +1 -0
- package/dist/client/connect/payoutAdoptionAge.d.ts +19 -0
- package/dist/client/connect/payoutAdoptionAge.d.ts.map +1 -0
- package/dist/client/connect/payoutAdoptionAge.js +56 -0
- package/dist/client/connect/payoutAdoptionAge.js.map +1 -0
- package/dist/client/connect/payoutAdoptionAllocation.d.ts +24 -0
- package/dist/client/connect/payoutAdoptionAllocation.d.ts.map +1 -0
- package/dist/client/connect/payoutAdoptionAllocation.js +78 -0
- package/dist/client/connect/payoutAdoptionAllocation.js.map +1 -0
- package/dist/client/connect/payoutReconciliation.d.ts.map +1 -1
- package/dist/client/connect/payoutReconciliation.js +4 -2
- package/dist/client/connect/payoutReconciliation.js.map +1 -1
- package/dist/client/payoutPolicy.d.ts +7 -1
- package/dist/client/payoutPolicy.d.ts.map +1 -1
- package/dist/client/payoutPolicy.js +78 -7
- package/dist/client/payoutPolicy.js.map +1 -1
- package/dist/component/_generated/component.d.ts +196 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/earnings/triggers.d.ts +38 -4
- package/dist/component/earnings/triggers.d.ts.map +1 -1
- package/dist/component/payoutPolicy/adoption.d.ts +9 -0
- package/dist/component/payoutPolicy/adoption.d.ts.map +1 -0
- package/dist/component/payoutPolicy/adoption.js +45 -0
- package/dist/component/payoutPolicy/adoption.js.map +1 -0
- package/dist/component/payoutPolicy/mutations.d.ts +96 -0
- package/dist/component/payoutPolicy/mutations.d.ts.map +1 -1
- package/dist/component/payoutPolicy/mutations.js +38 -5
- package/dist/component/payoutPolicy/mutations.js.map +1 -1
- package/dist/component/payoutPolicy/queries.d.ts +16 -0
- package/dist/component/payoutPolicy/queries.d.ts.map +1 -1
- package/dist/component/payoutPolicy/schema.d.ts +299 -2
- package/dist/component/payoutPolicy/schema.d.ts.map +1 -1
- package/dist/component/payoutPolicy/schema.js +7 -0
- package/dist/component/payoutPolicy/schema.js.map +1 -1
- package/dist/component/payoutRelease/adoption.d.ts +269 -0
- package/dist/component/payoutRelease/adoption.d.ts.map +1 -0
- package/dist/component/payoutRelease/adoption.js +197 -0
- package/dist/component/payoutRelease/adoption.js.map +1 -0
- package/dist/component/payoutRelease/helpers.d.ts +3 -0
- package/dist/component/payoutRelease/helpers.d.ts.map +1 -1
- package/dist/component/payoutRelease/helpers.js +20 -0
- package/dist/component/payoutRelease/helpers.js.map +1 -1
- package/dist/component/payoutRelease/mutations.d.ts +81 -0
- 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/observation.d.ts +18 -0
- package/dist/component/payoutRelease/observation.d.ts.map +1 -0
- package/dist/component/payoutRelease/observation.js +11 -0
- package/dist/component/payoutRelease/observation.js.map +1 -0
- package/dist/component/payoutRelease/queries.d.ts +3 -0
- package/dist/component/payoutRelease/queries.d.ts.map +1 -1
- package/dist/component/payoutRelease/schema.d.ts +8 -19
- package/dist/component/payoutRelease/schema.d.ts.map +1 -1
- package/dist/component/payoutRelease/schema.js +3 -9
- package/dist/component/payoutRelease/schema.js.map +1 -1
- package/dist/component/schema.d.ts +73 -2
- package/docs/settlement.md +121 -40
- package/package.json +1 -1
- package/src/client/connect/payoutActivationProof.ts +101 -0
- package/src/client/connect/payoutAdoption.test.ts +421 -0
- package/src/client/connect/payoutAdoption.ts +330 -0
- package/src/client/connect/payoutAdoptionAge.test.ts +89 -0
- package/src/client/connect/payoutAdoptionAge.ts +74 -0
- package/src/client/connect/payoutAdoptionAllocation.test.ts +132 -0
- package/src/client/connect/payoutAdoptionAllocation.ts +106 -0
- package/src/client/connect/payoutReconciliation.test.ts +16 -0
- package/src/client/connect/payoutReconciliation.ts +6 -2
- package/src/client/payoutPolicy.test.ts +16 -0
- package/src/client/payoutPolicy.ts +107 -5
- package/src/component/_generated/api.ts +6 -0
- package/src/component/_generated/component.ts +192 -1
- package/src/component/payout-adoption.test.ts +272 -0
- package/src/component/payout-policy-adoption.test.ts +131 -0
- package/src/component/payoutPolicy/adoption.ts +76 -0
- package/src/component/payoutPolicy/mutations.ts +46 -3
- package/src/component/payoutPolicy/schema.ts +9 -0
- package/src/component/payoutRelease/adoption.ts +262 -0
- package/src/component/payoutRelease/helpers.ts +21 -0
- package/src/component/payoutRelease/mutations.ts +10 -0
- package/src/component/payoutRelease/observation.ts +10 -0
- package/src/component/payoutRelease/schema.ts +3 -9
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Infer } from "convex/values";
|
|
2
|
+
import type { MutationCtx } from "../_generated/server.js";
|
|
3
|
+
import type { adoptionProof } from "./schema.js";
|
|
4
|
+
import {
|
|
5
|
+
add,
|
|
6
|
+
checkSnapshot,
|
|
7
|
+
payoutClaimFingerprint,
|
|
8
|
+
sourceById,
|
|
9
|
+
} from "../payoutRelease/helpers.js";
|
|
10
|
+
import { readFees } from "../payoutRelease/fees.js";
|
|
11
|
+
/** Package-authenticated provider proof, rechecked against all local financial state under OCC. */
|
|
12
|
+
export async function validateAdoption(
|
|
13
|
+
ctx: MutationCtx,
|
|
14
|
+
scope: { accountId: string; ownerKey: string },
|
|
15
|
+
proof: Infer<typeof adoptionProof>,
|
|
16
|
+
) {
|
|
17
|
+
await checkSnapshot(ctx, scope.accountId, proof.observations, true);
|
|
18
|
+
const rows = await ctx.db
|
|
19
|
+
.query("payoutReleaseClaims")
|
|
20
|
+
.withIndex("by_account_status", (q) => q.eq("accountId", scope.accountId))
|
|
21
|
+
.take(101);
|
|
22
|
+
const expected = new Map(
|
|
23
|
+
proof.claims.map((c) => [c.operationKey, c.fingerprint]),
|
|
24
|
+
);
|
|
25
|
+
if (
|
|
26
|
+
rows.length > 100 ||
|
|
27
|
+
expected.size !== proof.claims.length ||
|
|
28
|
+
rows.length !== expected.size ||
|
|
29
|
+
rows.some(
|
|
30
|
+
(c) =>
|
|
31
|
+
c.ownerKey !== scope.ownerKey ||
|
|
32
|
+
c.status === "reserved" ||
|
|
33
|
+
c.status === "submitted" ||
|
|
34
|
+
expected.get(c.operationKey) !== payoutClaimFingerprint(c),
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
throw new Error(
|
|
38
|
+
"Adoption payout history snapshot changed or is unresolved.",
|
|
39
|
+
);
|
|
40
|
+
const owed = new Map<string, number>();
|
|
41
|
+
for (const o of proof.observations) {
|
|
42
|
+
const source = await sourceById(ctx, o.stripeTransferId);
|
|
43
|
+
if (
|
|
44
|
+
source &&
|
|
45
|
+
(source.accountId !== scope.accountId ||
|
|
46
|
+
source.ownerKey !== scope.ownerKey ||
|
|
47
|
+
source.currency !== o.currency ||
|
|
48
|
+
source.reservedAmount !== 0)
|
|
49
|
+
)
|
|
50
|
+
throw new Error(
|
|
51
|
+
"Adoption source allocation identity or reservations changed.",
|
|
52
|
+
);
|
|
53
|
+
owed.set(
|
|
54
|
+
o.currency,
|
|
55
|
+
add(
|
|
56
|
+
owed.get(o.currency) ?? 0,
|
|
57
|
+
add(add(o.matureNet, o.pendingCredits), -(source?.paidAmount ?? 0)),
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
let hasHeld = false;
|
|
62
|
+
for (const [currency, amount] of owed) {
|
|
63
|
+
const fees = await readFees(ctx, { ...scope, currency });
|
|
64
|
+
if (add(amount, -(fees?.amount ?? 0)) > 0) hasHeld = true;
|
|
65
|
+
}
|
|
66
|
+
if (
|
|
67
|
+
proof.oldestHeldAt !== undefined &&
|
|
68
|
+
(!Number.isSafeInteger(proof.oldestHeldAt) ||
|
|
69
|
+
proof.oldestHeldAt < 0 ||
|
|
70
|
+
proof.oldestHeldAt > Date.now())
|
|
71
|
+
)
|
|
72
|
+
throw new Error("Invalid adoption holding timestamp.");
|
|
73
|
+
if (hasHeld && proof.oldestHeldAt === undefined)
|
|
74
|
+
throw new Error("Adoption holding age is required for unpaid funds.");
|
|
75
|
+
return proof.oldestHeldAt ?? Date.now();
|
|
76
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { validateAdoption } from "./adoption.js";
|
|
1
2
|
import { v } from "convex/values";
|
|
2
3
|
import { mutation } from "../_generated/server.js";
|
|
3
4
|
import {
|
|
5
|
+
adoptionProof,
|
|
4
6
|
prepareFields,
|
|
5
7
|
operationDoc,
|
|
6
8
|
scopeFields,
|
|
@@ -49,6 +51,7 @@ export const prepare = mutation({
|
|
|
49
51
|
old.mode !== args.mode ||
|
|
50
52
|
old.country !== args.country ||
|
|
51
53
|
old.observedAt !== args.observedAt ||
|
|
54
|
+
Boolean(old.adoption) !== Boolean(args.adoption) ||
|
|
52
55
|
!same(old.thresholds, args.thresholds) ||
|
|
53
56
|
!same(old.observedSchedule, args.observedSchedule)
|
|
54
57
|
)
|
|
@@ -84,6 +87,17 @@ export const prepare = mutation({
|
|
|
84
87
|
args.observedSchedule.interval === "manual"
|
|
85
88
|
)
|
|
86
89
|
throw new Error("An existing manual schedule requires migration.");
|
|
90
|
+
let adoptedDeadline: number | undefined;
|
|
91
|
+
if (args.adoption) {
|
|
92
|
+
if (args.mode !== "custom" || policy?.mode !== "stripe_managed")
|
|
93
|
+
throw new Error(
|
|
94
|
+
"Adoption requires an owned automatic policy before custom activation.",
|
|
95
|
+
);
|
|
96
|
+
const anchor = await validateAdoption(ctx, args, args.adoption);
|
|
97
|
+
adoptedDeadline = anchor + days * 86400000;
|
|
98
|
+
if (adoptedDeadline <= Date.now())
|
|
99
|
+
throw new Error("Adoption holding deadline has already expired.");
|
|
100
|
+
}
|
|
87
101
|
const previousSchedule =
|
|
88
102
|
policy?.mode === "custom"
|
|
89
103
|
? policy.previousSchedule!
|
|
@@ -111,6 +125,7 @@ export const prepare = mutation({
|
|
|
111
125
|
country: args.country,
|
|
112
126
|
previousSchedule,
|
|
113
127
|
holdDeadline:
|
|
128
|
+
adoptedDeadline ??
|
|
114
129
|
policy?.holdDeadline ??
|
|
115
130
|
(args.mode === "custom"
|
|
116
131
|
? (args.observedAt ?? Date.now()) + days * 86400000
|
|
@@ -139,6 +154,10 @@ export const confirm = mutation({
|
|
|
139
154
|
if (!same(canonicalSchedule(args.observedSchedule), op.targetSchedule))
|
|
140
155
|
throw new Error("Provider payout schedule was not confirmed.");
|
|
141
156
|
if (op.status === "complete") return op;
|
|
157
|
+
if (op.adoption && !op.attempted)
|
|
158
|
+
throw new Error(
|
|
159
|
+
"Funded adoption requires a verified provider attempt before confirmation.",
|
|
160
|
+
);
|
|
142
161
|
if (op.status === "canceled")
|
|
143
162
|
throw new Error("Payout policy operation was canceled.");
|
|
144
163
|
if (
|
|
@@ -162,7 +181,11 @@ export const confirm = mutation({
|
|
|
162
181
|
},
|
|
163
182
|
});
|
|
164
183
|
export const beginAttempt = mutation({
|
|
165
|
-
args: {
|
|
184
|
+
args: {
|
|
185
|
+
...scopeFields,
|
|
186
|
+
operationKey: v.string(),
|
|
187
|
+
adoption: v.optional(adoptionProof),
|
|
188
|
+
},
|
|
166
189
|
returns: operationDoc,
|
|
167
190
|
handler: async (ctx, args) => {
|
|
168
191
|
const policy = await readPolicy(ctx, args);
|
|
@@ -179,8 +202,28 @@ export const beginAttempt = mutation({
|
|
|
179
202
|
policy.status !== "changing"
|
|
180
203
|
)
|
|
181
204
|
throw new Error("Payout policy operation is not pending.");
|
|
182
|
-
if (!op.attempted)
|
|
183
|
-
|
|
205
|
+
if (!op.attempted) {
|
|
206
|
+
if (Boolean(op.adoption) !== Boolean(args.adoption))
|
|
207
|
+
throw new Error(
|
|
208
|
+
"A fresh adoption proof is required for this operation.",
|
|
209
|
+
);
|
|
210
|
+
if (args.adoption) {
|
|
211
|
+
const anchor = await validateAdoption(ctx, args, args.adoption);
|
|
212
|
+
const deadline = Math.min(
|
|
213
|
+
policy.holdDeadline!,
|
|
214
|
+
anchor + holdingDays(op.country) * 86400000,
|
|
215
|
+
);
|
|
216
|
+
if (deadline <= Date.now())
|
|
217
|
+
throw new Error("Adoption holding deadline has already expired.");
|
|
218
|
+
await ctx.db.patch("payoutPolicies", policy._id, {
|
|
219
|
+
holdDeadline: deadline,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
await ctx.db.patch("payoutPolicyOperations", op._id, {
|
|
223
|
+
attempted: true,
|
|
224
|
+
...(args.adoption ? { adoption: args.adoption } : {}),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
184
227
|
return (await ctx.db.get("payoutPolicyOperations", op._id))!;
|
|
185
228
|
},
|
|
186
229
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { observation } from "../payoutRelease/observation.js";
|
|
1
2
|
import { defineTable } from "convex/server";
|
|
2
3
|
import { v } from "convex/values";
|
|
3
4
|
export const scopeFields = { accountId: v.string(), ownerKey: v.string() };
|
|
@@ -15,7 +16,15 @@ export const mode = v.union(v.literal("stripe_managed"), v.literal("custom"));
|
|
|
15
16
|
export const thresholds = v.array(
|
|
16
17
|
v.object({ currency: v.string(), amount: v.number() }),
|
|
17
18
|
);
|
|
19
|
+
export const adoptionProof = v.object({
|
|
20
|
+
observations: v.array(observation),
|
|
21
|
+
claims: v.array(
|
|
22
|
+
v.object({ operationKey: v.string(), fingerprint: v.string() }),
|
|
23
|
+
),
|
|
24
|
+
oldestHeldAt: v.optional(v.number()),
|
|
25
|
+
});
|
|
18
26
|
export const prepareFields = {
|
|
27
|
+
adoption: v.optional(adoptionProof),
|
|
19
28
|
...scopeFields,
|
|
20
29
|
operationKey: v.string(),
|
|
21
30
|
expectedRevision: v.number(),
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { v, type Infer } from "convex/values";
|
|
2
|
+
import type { MutationCtx } from "../_generated/server.js";
|
|
3
|
+
import { scopeFields } from "../payoutPolicy/schema.js";
|
|
4
|
+
import { readPolicy } from "../payoutPolicy/helpers.js";
|
|
5
|
+
import { observation, allocation } from "./schema.js";
|
|
6
|
+
import {
|
|
7
|
+
add,
|
|
8
|
+
checkSnapshot,
|
|
9
|
+
sourceById,
|
|
10
|
+
payoutClaimFingerprint,
|
|
11
|
+
} from "./helpers.js";
|
|
12
|
+
import { recordFees } from "./fees.js";
|
|
13
|
+
export const historicalArgs = {
|
|
14
|
+
...scopeFields,
|
|
15
|
+
expectedRevision: v.number(),
|
|
16
|
+
payout: v.object({
|
|
17
|
+
stripePayoutId: v.string(),
|
|
18
|
+
amount: v.number(),
|
|
19
|
+
currency: v.string(),
|
|
20
|
+
created: v.number(),
|
|
21
|
+
destinationId: v.string(),
|
|
22
|
+
status: v.union(v.literal("paid"), v.literal("returned")),
|
|
23
|
+
debitTransactionId: v.string(),
|
|
24
|
+
debitFee: v.number(),
|
|
25
|
+
returnTransactionId: v.optional(v.string()),
|
|
26
|
+
returnFee: v.optional(v.number()),
|
|
27
|
+
}),
|
|
28
|
+
allocations: v.array(allocation),
|
|
29
|
+
observations: v.array(observation),
|
|
30
|
+
};
|
|
31
|
+
type Args = {
|
|
32
|
+
[K in keyof typeof historicalArgs]: Infer<(typeof historicalArgs)[K]>;
|
|
33
|
+
};
|
|
34
|
+
/** Trusted package observations only. Provider history and historical maturity are verified upstream. */
|
|
35
|
+
export async function adoptHistoricalPayout(ctx: MutationCtx, args: Args) {
|
|
36
|
+
const policy = await readPolicy(ctx, args);
|
|
37
|
+
if (!policy || policy.revision !== args.expectedRevision)
|
|
38
|
+
throw new Error("Historical payout policy revision changed.");
|
|
39
|
+
if (policy.mode !== "stripe_managed" || policy.status !== "ready")
|
|
40
|
+
throw new Error(
|
|
41
|
+
"Historical adoption requires an inactive automatic policy.",
|
|
42
|
+
);
|
|
43
|
+
const p = args.payout;
|
|
44
|
+
if (
|
|
45
|
+
!/^po_\w+$/.test(p.stripePayoutId) ||
|
|
46
|
+
!/^ba_\w+$/.test(p.destinationId) ||
|
|
47
|
+
!/^[a-z]{3}$/.test(p.currency) ||
|
|
48
|
+
!Number.isSafeInteger(p.amount) ||
|
|
49
|
+
p.amount <= 0 ||
|
|
50
|
+
!Number.isSafeInteger(p.created) ||
|
|
51
|
+
p.created < 0 ||
|
|
52
|
+
p.created * 1000 > Date.now() ||
|
|
53
|
+
!Number.isSafeInteger(p.created * 1000) ||
|
|
54
|
+
!/^txn_\w+$/.test(p.debitTransactionId) ||
|
|
55
|
+
!Number.isSafeInteger(p.debitFee) ||
|
|
56
|
+
p.debitFee < 0 ||
|
|
57
|
+
(p.status === "returned"
|
|
58
|
+
? !p.returnTransactionId ||
|
|
59
|
+
!/^txn_\w+$/.test(p.returnTransactionId) ||
|
|
60
|
+
!Number.isSafeInteger(p.returnFee)
|
|
61
|
+
: p.returnTransactionId !== undefined || p.returnFee !== undefined)
|
|
62
|
+
)
|
|
63
|
+
throw new Error("Invalid historical payout receipt.");
|
|
64
|
+
const rows = await checkSnapshot(
|
|
65
|
+
ctx,
|
|
66
|
+
args.accountId,
|
|
67
|
+
args.observations,
|
|
68
|
+
true,
|
|
69
|
+
);
|
|
70
|
+
const byObservation = new Map(
|
|
71
|
+
args.observations.map((o) => [o.stripeTransferId, o]),
|
|
72
|
+
);
|
|
73
|
+
const seen = new Set<string>();
|
|
74
|
+
let total = 0;
|
|
75
|
+
const allocations = args.allocations
|
|
76
|
+
.map((a) => {
|
|
77
|
+
if (
|
|
78
|
+
!Number.isSafeInteger(a.amount) ||
|
|
79
|
+
a.amount <= 0 ||
|
|
80
|
+
seen.has(a.stripeTransferId) ||
|
|
81
|
+
!rows.has(a.stripeTransferId) ||
|
|
82
|
+
byObservation.get(a.stripeTransferId)?.currency !== p.currency
|
|
83
|
+
)
|
|
84
|
+
throw new Error("Invalid historical source allocation.");
|
|
85
|
+
seen.add(a.stripeTransferId);
|
|
86
|
+
total = add(total, a.amount);
|
|
87
|
+
const role = rows.get(a.stripeTransferId)!.role;
|
|
88
|
+
return {
|
|
89
|
+
stripeTransferId: a.stripeTransferId,
|
|
90
|
+
amount: a.amount,
|
|
91
|
+
...(role ? { role } : {}),
|
|
92
|
+
};
|
|
93
|
+
})
|
|
94
|
+
.sort((a, b) => a.stripeTransferId.localeCompare(b.stripeTransferId));
|
|
95
|
+
if (total !== p.amount)
|
|
96
|
+
throw new Error(
|
|
97
|
+
"Historical source allocations do not equal payout principal.",
|
|
98
|
+
);
|
|
99
|
+
const operationKey = `bs:historical:${p.stripePayoutId}`;
|
|
100
|
+
const old = await ctx.db
|
|
101
|
+
.query("payoutReleaseClaims")
|
|
102
|
+
.withIndex("by_provider_payout", (q) =>
|
|
103
|
+
q.eq("stripePayoutId", p.stripePayoutId),
|
|
104
|
+
)
|
|
105
|
+
.unique();
|
|
106
|
+
if (old) {
|
|
107
|
+
if (
|
|
108
|
+
!old.historical ||
|
|
109
|
+
old.accountId !== args.accountId ||
|
|
110
|
+
old.ownerKey !== args.ownerKey ||
|
|
111
|
+
old.operationKey !== operationKey ||
|
|
112
|
+
old.currency !== p.currency ||
|
|
113
|
+
old.amount !== p.amount ||
|
|
114
|
+
old.destinationId !== p.destinationId ||
|
|
115
|
+
old.attemptedAt !== p.created * 1000 ||
|
|
116
|
+
old.status !== p.status ||
|
|
117
|
+
old.debitTransactionId !== p.debitTransactionId ||
|
|
118
|
+
old.debitFee !== p.debitFee ||
|
|
119
|
+
old.returnTransactionId !== p.returnTransactionId ||
|
|
120
|
+
old.providerFee !== add(p.debitFee, p.returnFee ?? 0) ||
|
|
121
|
+
JSON.stringify(
|
|
122
|
+
old.allocations.map((a) => [a.stripeTransferId, a.amount, a.role]),
|
|
123
|
+
) !==
|
|
124
|
+
JSON.stringify(
|
|
125
|
+
allocations.map((a) => [a.stripeTransferId, a.amount, a.role]),
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
throw new Error(
|
|
129
|
+
"Historical payout identity or source allocation changed; reconcile existing outcomes.",
|
|
130
|
+
);
|
|
131
|
+
return old;
|
|
132
|
+
}
|
|
133
|
+
for (const status of ["reserved", "submitted"] as const) {
|
|
134
|
+
const unresolved = await ctx.db
|
|
135
|
+
.query("payoutReleaseClaims")
|
|
136
|
+
.withIndex("by_account_status", (q) =>
|
|
137
|
+
q.eq("accountId", args.accountId).eq("status", status),
|
|
138
|
+
)
|
|
139
|
+
.first();
|
|
140
|
+
if (unresolved)
|
|
141
|
+
throw new Error(
|
|
142
|
+
"Resolve existing payout attempts before historical adoption.",
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
const history = await ctx.db
|
|
146
|
+
.query("payoutReleaseClaims")
|
|
147
|
+
.withIndex("by_account_status", (q) => q.eq("accountId", args.accountId))
|
|
148
|
+
.take(100);
|
|
149
|
+
if (history.length >= 100)
|
|
150
|
+
throw new Error("Historical payout snapshot limit exceeded.");
|
|
151
|
+
for (const a of allocations) {
|
|
152
|
+
const source = await sourceById(ctx, a.stripeTransferId);
|
|
153
|
+
if (
|
|
154
|
+
source &&
|
|
155
|
+
(source.accountId !== args.accountId ||
|
|
156
|
+
source.ownerKey !== args.ownerKey ||
|
|
157
|
+
source.currency !== p.currency ||
|
|
158
|
+
source.reservedAmount !== 0)
|
|
159
|
+
)
|
|
160
|
+
throw new Error(
|
|
161
|
+
"Historical source allocation identity or reservations changed.",
|
|
162
|
+
);
|
|
163
|
+
const paid = p.status === "paid" ? a.amount : 0;
|
|
164
|
+
if (source)
|
|
165
|
+
await ctx.db.patch("payoutSourceAllocations", source._id, {
|
|
166
|
+
paidAmount: add(source.paidAmount, paid),
|
|
167
|
+
});
|
|
168
|
+
else
|
|
169
|
+
await ctx.db.insert("payoutSourceAllocations", {
|
|
170
|
+
accountId: args.accountId,
|
|
171
|
+
ownerKey: args.ownerKey,
|
|
172
|
+
stripeTransferId: a.stripeTransferId,
|
|
173
|
+
currency: p.currency,
|
|
174
|
+
paidAmount: paid,
|
|
175
|
+
reservedAmount: 0,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
const id = await ctx.db.insert("payoutReleaseClaims", {
|
|
179
|
+
accountId: args.accountId,
|
|
180
|
+
ownerKey: args.ownerKey,
|
|
181
|
+
operationKey,
|
|
182
|
+
expectedRevision: args.expectedRevision,
|
|
183
|
+
historical: true,
|
|
184
|
+
currency: p.currency,
|
|
185
|
+
amount: p.amount,
|
|
186
|
+
destinationId: p.destinationId,
|
|
187
|
+
allocations,
|
|
188
|
+
observations: args.observations,
|
|
189
|
+
status: p.status,
|
|
190
|
+
attemptedAt: p.created * 1000,
|
|
191
|
+
stripePayoutId: p.stripePayoutId,
|
|
192
|
+
...(p.returnTransactionId
|
|
193
|
+
? { returnTransactionId: p.returnTransactionId }
|
|
194
|
+
: {}),
|
|
195
|
+
});
|
|
196
|
+
const claim = (await ctx.db.get("payoutReleaseClaims", id))!;
|
|
197
|
+
await recordFees(
|
|
198
|
+
ctx,
|
|
199
|
+
claim,
|
|
200
|
+
{
|
|
201
|
+
debitTransactionId: p.debitTransactionId,
|
|
202
|
+
debitFee: p.debitFee,
|
|
203
|
+
...(p.returnFee !== undefined ? { returnFee: p.returnFee } : {}),
|
|
204
|
+
},
|
|
205
|
+
p.status === "returned",
|
|
206
|
+
);
|
|
207
|
+
return (await ctx.db.get("payoutReleaseClaims", id))!;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export const historicalBatchArgs = {
|
|
211
|
+
...scopeFields,
|
|
212
|
+
expectedRevision: v.number(),
|
|
213
|
+
observations: v.array(observation),
|
|
214
|
+
expectedClaims: v.array(
|
|
215
|
+
v.object({ operationKey: v.string(), fingerprint: v.string() }),
|
|
216
|
+
),
|
|
217
|
+
payouts: v.array(
|
|
218
|
+
v.object({
|
|
219
|
+
payout: historicalArgs.payout,
|
|
220
|
+
allocations: historicalArgs.allocations,
|
|
221
|
+
}),
|
|
222
|
+
),
|
|
223
|
+
};
|
|
224
|
+
type BatchArgs = {
|
|
225
|
+
[K in keyof typeof historicalBatchArgs]: Infer<
|
|
226
|
+
(typeof historicalBatchArgs)[K]
|
|
227
|
+
>;
|
|
228
|
+
};
|
|
229
|
+
export async function adoptHistoricalBatch(ctx: MutationCtx, args: BatchArgs) {
|
|
230
|
+
if (!args.payouts.length || args.payouts.length > 100)
|
|
231
|
+
throw new Error("Invalid historical payout batch size.");
|
|
232
|
+
const rows = await ctx.db
|
|
233
|
+
.query("payoutReleaseClaims")
|
|
234
|
+
.withIndex("by_account_status", (q) => q.eq("accountId", args.accountId))
|
|
235
|
+
.take(101);
|
|
236
|
+
const expected = new Map(
|
|
237
|
+
args.expectedClaims.map((c) => [c.operationKey, c.fingerprint]),
|
|
238
|
+
);
|
|
239
|
+
if (
|
|
240
|
+
rows.length > 100 ||
|
|
241
|
+
expected.size !== args.expectedClaims.length ||
|
|
242
|
+
rows.length !== expected.size ||
|
|
243
|
+
rows.some(
|
|
244
|
+
(c) =>
|
|
245
|
+
c.ownerKey !== args.ownerKey ||
|
|
246
|
+
expected.get(c.operationKey) !== payoutClaimFingerprint(c),
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
throw new Error("Historical payout history snapshot changed.");
|
|
250
|
+
const claims = [];
|
|
251
|
+
for (const p of args.payouts)
|
|
252
|
+
claims.push(
|
|
253
|
+
await adoptHistoricalPayout(ctx, {
|
|
254
|
+
accountId: args.accountId,
|
|
255
|
+
ownerKey: args.ownerKey,
|
|
256
|
+
expectedRevision: args.expectedRevision,
|
|
257
|
+
observations: args.observations,
|
|
258
|
+
...p,
|
|
259
|
+
}),
|
|
260
|
+
);
|
|
261
|
+
return { claims };
|
|
262
|
+
}
|
|
@@ -166,3 +166,24 @@ export async function moveAllocations(
|
|
|
166
166
|
});
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
|
+
|
|
170
|
+
/** Stable financial identity for historical adoption and activation snapshots. */
|
|
171
|
+
export function payoutClaimFingerprint(c: Doc<"payoutReleaseClaims">) {
|
|
172
|
+
return JSON.stringify([
|
|
173
|
+
c.accountId,
|
|
174
|
+
c.ownerKey,
|
|
175
|
+
c.operationKey,
|
|
176
|
+
c.historical ?? false,
|
|
177
|
+
c.currency,
|
|
178
|
+
c.amount,
|
|
179
|
+
c.status,
|
|
180
|
+
c.attemptedAt,
|
|
181
|
+
c.stripePayoutId,
|
|
182
|
+
c.destinationId,
|
|
183
|
+
c.debitTransactionId,
|
|
184
|
+
c.debitFee,
|
|
185
|
+
c.providerFee,
|
|
186
|
+
c.returnTransactionId,
|
|
187
|
+
c.allocations.map((a) => [a.stripeTransferId, a.amount, a.role]),
|
|
188
|
+
]);
|
|
189
|
+
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
historicalBatchArgs,
|
|
3
|
+
adoptHistoricalBatch as importHistoricalBatch,
|
|
4
|
+
} from "./adoption.js";
|
|
1
5
|
import { readFees, recordFees } from "./fees.js";
|
|
2
6
|
import { v } from "convex/values";
|
|
3
7
|
import { mutation } from "../_generated/server.js";
|
|
@@ -332,3 +336,9 @@ export const recordRejection = mutation({
|
|
|
332
336
|
return (await ctx.db.get("payoutReleaseClaims", claim._id))!;
|
|
333
337
|
},
|
|
334
338
|
});
|
|
339
|
+
|
|
340
|
+
export const adoptHistoricalBatch = mutation({
|
|
341
|
+
args: historicalBatchArgs,
|
|
342
|
+
returns: v.object({ claims: v.array(claimDoc) }),
|
|
343
|
+
handler: importHistoricalBatch,
|
|
344
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
export const observation = v.object({
|
|
3
|
+
stripeTransferId: v.string(),
|
|
4
|
+
currency: v.string(),
|
|
5
|
+
matureNet: v.number(),
|
|
6
|
+
pendingCredits: v.number(),
|
|
7
|
+
ledgerFingerprint: v.string(),
|
|
8
|
+
observedAt: v.number(),
|
|
9
|
+
transactionIds: v.array(v.string()),
|
|
10
|
+
});
|
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
import { defineTable } from "convex/server";
|
|
2
2
|
import { v } from "convex/values";
|
|
3
3
|
import { scopeFields } from "../payoutPolicy/schema.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
currency: v.string(),
|
|
7
|
-
matureNet: v.number(),
|
|
8
|
-
pendingCredits: v.number(),
|
|
9
|
-
ledgerFingerprint: v.string(),
|
|
10
|
-
observedAt: v.number(),
|
|
11
|
-
transactionIds: v.array(v.string()),
|
|
12
|
-
});
|
|
4
|
+
import { observation } from "./observation.js";
|
|
5
|
+
export { observation } from "./observation.js";
|
|
13
6
|
export const allocation = v.object({
|
|
14
7
|
stripeTransferId: v.string(),
|
|
15
8
|
amount: v.number(),
|
|
@@ -19,6 +12,7 @@ export const allocation = v.object({
|
|
|
19
12
|
});
|
|
20
13
|
export const claimFields = {
|
|
21
14
|
...scopeFields,
|
|
15
|
+
historical: v.optional(v.boolean()),
|
|
22
16
|
operationKey: v.string(),
|
|
23
17
|
expectedRevision: v.number(),
|
|
24
18
|
currency: v.string(),
|