@fantastic.dev/better-stripe 0.19.0 → 0.20.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/transferReceipt.d.ts.map +1 -1
- package/dist/client/connect/transferReceipt.js +1 -14
- package/dist/client/connect/transferReceipt.js.map +1 -1
- package/dist/component/_generated/component.d.ts +297 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/earnings/triggers.d.ts +134 -0
- package/dist/component/earnings/triggers.d.ts.map +1 -1
- package/dist/component/payoutRelease/allocation.d.ts +48 -0
- package/dist/component/payoutRelease/allocation.d.ts.map +1 -0
- package/dist/component/payoutRelease/allocation.js +98 -0
- package/dist/component/payoutRelease/allocation.js.map +1 -0
- package/dist/component/payoutRelease/helpers.d.ts +106 -0
- package/dist/component/payoutRelease/helpers.d.ts.map +1 -0
- package/dist/component/payoutRelease/helpers.js +119 -0
- package/dist/component/payoutRelease/helpers.js.map +1 -0
- package/dist/component/payoutRelease/mutations.d.ts +218 -0
- package/dist/component/payoutRelease/mutations.d.ts.map +1 -0
- package/dist/component/payoutRelease/mutations.js +254 -0
- package/dist/component/payoutRelease/mutations.js.map +1 -0
- package/dist/component/payoutRelease/queries.d.ts +84 -0
- package/dist/component/payoutRelease/queries.d.ts.map +1 -0
- package/dist/component/payoutRelease/queries.js +51 -0
- package/dist/component/payoutRelease/queries.js.map +1 -0
- package/dist/component/payoutRelease/schema.d.ts +289 -0
- package/dist/component/payoutRelease/schema.d.ts.map +1 -0
- package/dist/component/payoutRelease/schema.js +56 -0
- package/dist/component/payoutRelease/schema.js.map +1 -0
- package/dist/component/schema.d.ts +110 -0
- package/dist/component/schema.js +3 -0
- package/dist/component/schema.js.map +1 -1
- package/docs/settlement.md +31 -0
- package/package.json +1 -1
- package/src/client/connect/payoutAllocation.test.ts +292 -0
- package/src/client/connect/transferReceipt.ts +1 -14
- package/src/component/_generated/api.ts +8 -0
- package/src/component/_generated/component.ts +372 -0
- package/src/component/payout-release.test.ts +588 -0
- package/src/component/payoutRelease/allocation.ts +147 -0
- package/src/component/payoutRelease/helpers.ts +168 -0
- package/src/component/payoutRelease/mutations.ts +309 -0
- package/src/component/payoutRelease/queries.ts +56 -0
- package/src/component/payoutRelease/schema.ts +69 -0
- package/src/component/schema.ts +6 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { planRecipientPayout } from "../../component/payoutRelease/allocation.js";
|
|
3
|
+
|
|
4
|
+
const source = (id: string, matureNet: number, extra = {}) => ({
|
|
5
|
+
stripeTransferId: id,
|
|
6
|
+
currency: "usd",
|
|
7
|
+
matureNet,
|
|
8
|
+
paidAmount: 0,
|
|
9
|
+
reservedAmount: 0,
|
|
10
|
+
pendingCredits: 0,
|
|
11
|
+
role: "store" as const,
|
|
12
|
+
...extra,
|
|
13
|
+
});
|
|
14
|
+
const options = (
|
|
15
|
+
sources = [
|
|
16
|
+
source("tr_creator", 600),
|
|
17
|
+
source("tr_affiliate", 400, { role: "affiliate" }),
|
|
18
|
+
],
|
|
19
|
+
) => ({
|
|
20
|
+
currency: "usd",
|
|
21
|
+
threshold: 1000,
|
|
22
|
+
providerAvailable: 1000,
|
|
23
|
+
independentLiabilities: 0,
|
|
24
|
+
sources,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("combined recipient payout allocation", () => {
|
|
28
|
+
it("qualifies creator and affiliate funds together at the exact threshold", () => {
|
|
29
|
+
expect(planRecipientPayout(options())).toEqual({
|
|
30
|
+
status: "qualified",
|
|
31
|
+
currency: "usd",
|
|
32
|
+
amount: 1000,
|
|
33
|
+
matureUnpaid: 1000,
|
|
34
|
+
pendingCredits: 0,
|
|
35
|
+
providerMinimum: null,
|
|
36
|
+
allocations: [
|
|
37
|
+
{ stripeTransferId: "tr_affiliate", amount: 400, role: "affiliate" },
|
|
38
|
+
{ stripeTransferId: "tr_creator", amount: 600, role: "store" },
|
|
39
|
+
],
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
it("keeps the entire combined balance owed below the threshold", () => {
|
|
43
|
+
expect(
|
|
44
|
+
planRecipientPayout(options([source("tr_creator", 999)])),
|
|
45
|
+
).toMatchObject({
|
|
46
|
+
status: "held",
|
|
47
|
+
reason: "below_threshold",
|
|
48
|
+
matureUnpaid: 999,
|
|
49
|
+
amount: 0,
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
it("releases the full eligible balance above the threshold", () => {
|
|
53
|
+
expect(
|
|
54
|
+
planRecipientPayout({
|
|
55
|
+
...options([source("tr_creator", 1200)]),
|
|
56
|
+
providerAvailable: 1500,
|
|
57
|
+
}),
|
|
58
|
+
).toMatchObject({
|
|
59
|
+
status: "qualified",
|
|
60
|
+
amount: 1200,
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
it("cannot use unrelated available money to qualify a pending source", () => {
|
|
64
|
+
expect(
|
|
65
|
+
planRecipientPayout({
|
|
66
|
+
...options([source("tr_pending", 0, { pendingCredits: 1500 })]),
|
|
67
|
+
providerAvailable: 10000,
|
|
68
|
+
}),
|
|
69
|
+
).toMatchObject({
|
|
70
|
+
status: "held",
|
|
71
|
+
reason: "below_threshold",
|
|
72
|
+
matureUnpaid: 0,
|
|
73
|
+
pendingCredits: 1500,
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
it("deducts previously paid and still reserved allocations exactly once", () => {
|
|
77
|
+
expect(
|
|
78
|
+
planRecipientPayout(
|
|
79
|
+
options([
|
|
80
|
+
source("tr_creator", 2500, { paidAmount: 800, reservedAmount: 700 }),
|
|
81
|
+
]),
|
|
82
|
+
),
|
|
83
|
+
).toMatchObject({
|
|
84
|
+
status: "qualified",
|
|
85
|
+
matureUnpaid: 1000,
|
|
86
|
+
amount: 1000,
|
|
87
|
+
allocations: [
|
|
88
|
+
{ stripeTransferId: "tr_creator", amount: 1000, role: "store" },
|
|
89
|
+
],
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
it("carries a refunded paid source as debt against future funds", () => {
|
|
93
|
+
expect(
|
|
94
|
+
planRecipientPayout(
|
|
95
|
+
options([
|
|
96
|
+
source("tr_refunded", 0, { paidAmount: 600 }),
|
|
97
|
+
source("tr_new", 1500, { role: "affiliate" }),
|
|
98
|
+
]),
|
|
99
|
+
),
|
|
100
|
+
).toMatchObject({
|
|
101
|
+
status: "held",
|
|
102
|
+
reason: "below_threshold",
|
|
103
|
+
matureUnpaid: 900,
|
|
104
|
+
});
|
|
105
|
+
expect(
|
|
106
|
+
planRecipientPayout(
|
|
107
|
+
options([
|
|
108
|
+
source("tr_refunded", 0, { paidAmount: 600 }),
|
|
109
|
+
source("tr_new", 1600, { role: "affiliate" }),
|
|
110
|
+
]),
|
|
111
|
+
),
|
|
112
|
+
).toMatchObject({
|
|
113
|
+
status: "qualified",
|
|
114
|
+
amount: 1000,
|
|
115
|
+
allocations: [
|
|
116
|
+
{ stripeTransferId: "tr_new", amount: 1000, role: "affiliate" },
|
|
117
|
+
],
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
it("retains negative fee adjustments without forfeiting unpaid funds", () => {
|
|
121
|
+
expect(
|
|
122
|
+
planRecipientPayout(
|
|
123
|
+
options([source("tr_fee", -30), source("tr_new", 1030)]),
|
|
124
|
+
),
|
|
125
|
+
).toMatchObject({
|
|
126
|
+
status: "qualified",
|
|
127
|
+
amount: 1000,
|
|
128
|
+
allocations: [
|
|
129
|
+
{ stripeTransferId: "tr_new", amount: 1000, role: "store" },
|
|
130
|
+
],
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
it("caps release by fresh provider availability after independent liabilities", () => {
|
|
134
|
+
const result = planRecipientPayout({
|
|
135
|
+
...options([source("tr_creator", 2000)]),
|
|
136
|
+
providerAvailable: 1500,
|
|
137
|
+
independentLiabilities: 200,
|
|
138
|
+
});
|
|
139
|
+
expect(result).toMatchObject({
|
|
140
|
+
status: "qualified",
|
|
141
|
+
amount: 1300,
|
|
142
|
+
matureUnpaid: 2000,
|
|
143
|
+
});
|
|
144
|
+
expect(
|
|
145
|
+
planRecipientPayout({
|
|
146
|
+
...options(),
|
|
147
|
+
providerAvailable: 1000,
|
|
148
|
+
independentLiabilities: 1,
|
|
149
|
+
}),
|
|
150
|
+
).toMatchObject({
|
|
151
|
+
status: "held",
|
|
152
|
+
reason: "insufficient_available",
|
|
153
|
+
matureUnpaid: 1000,
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
it("does not subtract reservations again from an already debited provider balance", () => {
|
|
157
|
+
expect(
|
|
158
|
+
planRecipientPayout({
|
|
159
|
+
...options([source("tr_creator", 2000, { reservedAmount: 1000 })]),
|
|
160
|
+
providerAvailable: 1000,
|
|
161
|
+
}),
|
|
162
|
+
).toMatchObject({ status: "qualified", amount: 1000 });
|
|
163
|
+
});
|
|
164
|
+
it("isolates currencies, including debts and pending credits", () => {
|
|
165
|
+
expect(
|
|
166
|
+
planRecipientPayout(
|
|
167
|
+
options([
|
|
168
|
+
source("tr_usd", 1000),
|
|
169
|
+
source("tr_eur", -5000, { currency: "eur", pendingCredits: 300 }),
|
|
170
|
+
]),
|
|
171
|
+
),
|
|
172
|
+
).toMatchObject({ status: "qualified", amount: 1000, pendingCredits: 0 });
|
|
173
|
+
});
|
|
174
|
+
it("uses stable source ordering regardless of observation order", () => {
|
|
175
|
+
const rows = [source("tr_z", 700), source("tr_a", 800)];
|
|
176
|
+
const args = { ...options(rows), providerAvailable: 1100 };
|
|
177
|
+
const first = planRecipientPayout(args);
|
|
178
|
+
expect(first).toEqual(
|
|
179
|
+
planRecipientPayout({ ...args, sources: [...rows].reverse() }),
|
|
180
|
+
);
|
|
181
|
+
expect(first).toMatchObject({
|
|
182
|
+
allocations: [
|
|
183
|
+
{ stripeTransferId: "tr_a", amount: 800, role: "store" },
|
|
184
|
+
{ stripeTransferId: "tr_z", amount: 300, role: "store" },
|
|
185
|
+
],
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
it("preserves signed debt even if there are no current credits", () => {
|
|
189
|
+
expect(
|
|
190
|
+
planRecipientPayout(
|
|
191
|
+
options([source("tr_debt", -50, { paidAmount: 200 })]),
|
|
192
|
+
),
|
|
193
|
+
).toMatchObject({
|
|
194
|
+
status: "held",
|
|
195
|
+
matureUnpaid: -250,
|
|
196
|
+
amount: 0,
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
it("does not invent a numeric provider minimum", () => {
|
|
200
|
+
expect(
|
|
201
|
+
planRecipientPayout({
|
|
202
|
+
...options([source("tr_small", 1)]),
|
|
203
|
+
threshold: 1,
|
|
204
|
+
}),
|
|
205
|
+
).toMatchObject({
|
|
206
|
+
status: "qualified",
|
|
207
|
+
amount: 1,
|
|
208
|
+
providerMinimum: null,
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
it("rejects duplicate sources rather than counting them twice", () => {
|
|
212
|
+
awaitInvalid(() =>
|
|
213
|
+
planRecipientPayout(
|
|
214
|
+
options([source("tr_same", 600), source("tr_same", 600)]),
|
|
215
|
+
),
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
it.each([0, -1, 0.5, Number.MAX_SAFE_INTEGER + 1, NaN, Infinity])(
|
|
219
|
+
"rejects an unsafe threshold %s",
|
|
220
|
+
(threshold) => {
|
|
221
|
+
awaitInvalid(() => planRecipientPayout({ ...options(), threshold }));
|
|
222
|
+
},
|
|
223
|
+
);
|
|
224
|
+
it.each(["USD", "us", "", "usd\n"])(
|
|
225
|
+
"rejects malformed currency %s",
|
|
226
|
+
(currency) => {
|
|
227
|
+
awaitInvalid(() => planRecipientPayout({ ...options(), currency }));
|
|
228
|
+
},
|
|
229
|
+
);
|
|
230
|
+
it.each(["paidAmount", "reservedAmount", "pendingCredits"] as const)(
|
|
231
|
+
"rejects negative source %s",
|
|
232
|
+
(field) => {
|
|
233
|
+
awaitInvalid(() =>
|
|
234
|
+
planRecipientPayout(options([source("tr_bad", 1000, { [field]: -1 })])),
|
|
235
|
+
);
|
|
236
|
+
},
|
|
237
|
+
);
|
|
238
|
+
it("checks every source, even an otherwise ignored currency", () => {
|
|
239
|
+
awaitInvalid(() =>
|
|
240
|
+
planRecipientPayout(
|
|
241
|
+
options([
|
|
242
|
+
source("tr_usd", 1000),
|
|
243
|
+
source("tr_bad", 1.2, { currency: "eur" }),
|
|
244
|
+
]),
|
|
245
|
+
),
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
it.each([NaN, Infinity, 0.1, Number.MAX_SAFE_INTEGER + 1])(
|
|
249
|
+
"rejects unsafe source money %s",
|
|
250
|
+
(matureNet) => {
|
|
251
|
+
awaitInvalid(() =>
|
|
252
|
+
planRecipientPayout(options([source("tr_bad", matureNet)])),
|
|
253
|
+
);
|
|
254
|
+
},
|
|
255
|
+
);
|
|
256
|
+
it("rejects arithmetic overflow rather than producing approximate money", () => {
|
|
257
|
+
awaitInvalid(() =>
|
|
258
|
+
planRecipientPayout(
|
|
259
|
+
options([source("tr_a", Number.MAX_SAFE_INTEGER), source("tr_b", 1)]),
|
|
260
|
+
),
|
|
261
|
+
);
|
|
262
|
+
awaitInvalid(() =>
|
|
263
|
+
planRecipientPayout(
|
|
264
|
+
options([source("tr_a", -Number.MAX_SAFE_INTEGER, { paidAmount: 1 })]),
|
|
265
|
+
),
|
|
266
|
+
);
|
|
267
|
+
awaitInvalid(() =>
|
|
268
|
+
planRecipientPayout({
|
|
269
|
+
...options(),
|
|
270
|
+
providerAvailable: -Number.MAX_SAFE_INTEGER,
|
|
271
|
+
independentLiabilities: 1,
|
|
272
|
+
}),
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
it("holds on a negative provider balance without erasing entitlement", () => {
|
|
276
|
+
expect(
|
|
277
|
+
planRecipientPayout({ ...options(), providerAvailable: -100 }),
|
|
278
|
+
).toMatchObject({
|
|
279
|
+
status: "held",
|
|
280
|
+
reason: "insufficient_available",
|
|
281
|
+
matureUnpaid: 1000,
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
it("rejects negative independent liabilities", () => {
|
|
285
|
+
awaitInvalid(() =>
|
|
286
|
+
planRecipientPayout({ ...options(), independentLiabilities: -1 }),
|
|
287
|
+
);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
function awaitInvalid(action: () => unknown) {
|
|
291
|
+
expect(action).toThrow(/Invalid|Unsafe|Duplicate/);
|
|
292
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { transferFingerprint as ledgerFingerprint } from "../../component/payoutRelease/helpers.js";
|
|
1
2
|
import type Stripe from "stripe";
|
|
2
3
|
import type { Infer } from "convex/values";
|
|
3
4
|
import type { transferDocValidator } from "../../component/connect/validators.js";
|
|
@@ -82,20 +83,6 @@ function fingerprint(t: Stripe.Transfer) {
|
|
|
82
83
|
t.metadata?.bsReinstateOf,
|
|
83
84
|
]);
|
|
84
85
|
}
|
|
85
|
-
function ledgerFingerprint(t: LedgerTransfer) {
|
|
86
|
-
return JSON.stringify([
|
|
87
|
-
t.stripeTransferId,
|
|
88
|
-
t.destinationAccountId,
|
|
89
|
-
t.sourceChargeId,
|
|
90
|
-
t.amount,
|
|
91
|
-
t.currency,
|
|
92
|
-
t.role,
|
|
93
|
-
t.status,
|
|
94
|
-
t.reversedAmount ?? 0,
|
|
95
|
-
t.reversalClaimedAmount ?? 0,
|
|
96
|
-
t.reinstatement ?? false,
|
|
97
|
-
]);
|
|
98
|
-
}
|
|
99
86
|
/** Recipient-scoped received credit. Previous payouts are deliberately not allocated here. */
|
|
100
87
|
export async function getTransferFundingReceipt(
|
|
101
88
|
stripe: Stripe,
|
|
@@ -37,6 +37,10 @@ import type * as lib_stripeFactory from "../lib/stripeFactory.js";
|
|
|
37
37
|
import type * as payoutPolicy_helpers from "../payoutPolicy/helpers.js";
|
|
38
38
|
import type * as payoutPolicy_mutations from "../payoutPolicy/mutations.js";
|
|
39
39
|
import type * as payoutPolicy_queries from "../payoutPolicy/queries.js";
|
|
40
|
+
import type * as payoutRelease_allocation from "../payoutRelease/allocation.js";
|
|
41
|
+
import type * as payoutRelease_helpers from "../payoutRelease/helpers.js";
|
|
42
|
+
import type * as payoutRelease_mutations from "../payoutRelease/mutations.js";
|
|
43
|
+
import type * as payoutRelease_queries from "../payoutRelease/queries.js";
|
|
40
44
|
import type * as products_mutations from "../products/mutations.js";
|
|
41
45
|
import type * as products_queries from "../products/queries.js";
|
|
42
46
|
import type * as products_validators from "../products/validators.js";
|
|
@@ -84,6 +88,10 @@ const fullApi: ApiFromModules<{
|
|
|
84
88
|
"payoutPolicy/helpers": typeof payoutPolicy_helpers;
|
|
85
89
|
"payoutPolicy/mutations": typeof payoutPolicy_mutations;
|
|
86
90
|
"payoutPolicy/queries": typeof payoutPolicy_queries;
|
|
91
|
+
"payoutRelease/allocation": typeof payoutRelease_allocation;
|
|
92
|
+
"payoutRelease/helpers": typeof payoutRelease_helpers;
|
|
93
|
+
"payoutRelease/mutations": typeof payoutRelease_mutations;
|
|
94
|
+
"payoutRelease/queries": typeof payoutRelease_queries;
|
|
87
95
|
"products/mutations": typeof products_mutations;
|
|
88
96
|
"products/queries": typeof products_queries;
|
|
89
97
|
"products/validators": typeof products_validators;
|