@fantastic.dev/better-stripe 0.17.0 → 0.18.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 +17 -0
- package/README.md +58 -9
- package/dist/client/connect/payouts.d.ts +3 -2
- package/dist/client/connect/payouts.d.ts.map +1 -1
- package/dist/client/connect/payouts.js.map +1 -1
- package/dist/client/index.d.ts +4 -8
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +5 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/payoutPolicy.d.ts +81 -0
- package/dist/client/payoutPolicy.d.ts.map +1 -0
- package/dist/client/payoutPolicy.js +262 -0
- package/dist/client/payoutPolicy.js.map +1 -0
- package/dist/component/_generated/component.d.ts +273 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/connect/queries.d.ts.map +1 -1
- package/dist/component/connect/queries.js +2 -1
- package/dist/component/connect/queries.js.map +1 -1
- package/dist/component/earnings/arithmetic.d.ts +3 -0
- package/dist/component/earnings/arithmetic.d.ts.map +1 -0
- package/dist/component/earnings/arithmetic.js +11 -0
- package/dist/component/earnings/arithmetic.js.map +1 -0
- package/dist/component/earnings/reads.d.ts.map +1 -1
- package/dist/component/earnings/reads.js +6 -0
- package/dist/component/earnings/reads.js.map +1 -1
- package/dist/component/earnings/triggers.d.ts +150 -0
- package/dist/component/earnings/triggers.d.ts.map +1 -1
- package/dist/component/earnings/triggers.js +17 -12
- package/dist/component/earnings/triggers.js.map +1 -1
- package/dist/component/payoutPolicy/helpers.d.ts +51 -0
- package/dist/component/payoutPolicy/helpers.d.ts.map +1 -0
- package/dist/component/payoutPolicy/helpers.js +68 -0
- package/dist/component/payoutPolicy/helpers.js.map +1 -0
- package/dist/component/payoutPolicy/mutations.d.ts +172 -0
- package/dist/component/payoutPolicy/mutations.d.ts.map +1 -0
- package/dist/component/payoutPolicy/mutations.js +191 -0
- package/dist/component/payoutPolicy/mutations.js.map +1 -0
- package/dist/component/payoutPolicy/queries.d.ts +97 -0
- package/dist/component/payoutPolicy/queries.d.ts.map +1 -0
- package/dist/component/payoutPolicy/queries.js +51 -0
- package/dist/component/payoutPolicy/queries.js.map +1 -0
- package/dist/component/payoutPolicy/schema.d.ts +396 -0
- package/dist/component/payoutPolicy/schema.d.ts.map +1 -0
- package/dist/component/payoutPolicy/schema.js +54 -0
- package/dist/component/payoutPolicy/schema.js.map +1 -0
- package/dist/component/schema.d.ts +131 -0
- package/dist/component/schema.js +3 -0
- package/dist/component/schema.js.map +1 -1
- package/docs/react-workflows.md +5 -3
- package/docs/read-contracts.md +13 -0
- package/docs/settlement.md +62 -0
- package/package.json +1 -1
- package/src/client/connect/payouts.ts +10 -8
- package/src/client/index.ts +7 -11
- package/src/client/payoutPolicy.test.ts +320 -0
- package/src/client/payoutPolicy.ts +409 -0
- package/src/component/_generated/api.ts +8 -0
- package/src/component/_generated/component.ts +280 -0
- package/src/component/connect/queries.ts +2 -1
- package/src/component/earnings/arithmetic.ts +10 -0
- package/src/component/earnings/reads.ts +6 -0
- package/src/component/earnings/triggers.ts +18 -13
- package/src/component/payout-policy.test.ts +248 -0
- package/src/component/payoutPolicy/helpers.ts +88 -0
- package/src/component/payoutPolicy/mutations.ts +225 -0
- package/src/component/payoutPolicy/queries.ts +56 -0
- package/src/component/payoutPolicy/schema.ts +67 -0
- package/src/component/schema.ts +3 -0
- package/src/component/summary-arithmetic.test.ts +246 -0
|
@@ -2194,6 +2194,286 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
2194
2194
|
>;
|
|
2195
2195
|
};
|
|
2196
2196
|
};
|
|
2197
|
+
payoutPolicy: {
|
|
2198
|
+
mutations: {
|
|
2199
|
+
beginAttempt: FunctionReference<
|
|
2200
|
+
"mutation",
|
|
2201
|
+
"internal",
|
|
2202
|
+
{ accountId: string; operationKey: string; ownerKey: string },
|
|
2203
|
+
{
|
|
2204
|
+
_creationTime: number;
|
|
2205
|
+
_id: string;
|
|
2206
|
+
accountId: string;
|
|
2207
|
+
attempted?: boolean;
|
|
2208
|
+
country: string;
|
|
2209
|
+
expectedRevision: number;
|
|
2210
|
+
mode: "stripe_managed" | "custom";
|
|
2211
|
+
observedAt?: number;
|
|
2212
|
+
observedSchedule: {
|
|
2213
|
+
interval: "daily" | "weekly" | "monthly" | "manual";
|
|
2214
|
+
monthlyDays?: Array<number>;
|
|
2215
|
+
weeklyDays?: Array<string>;
|
|
2216
|
+
};
|
|
2217
|
+
operationKey: string;
|
|
2218
|
+
ownerKey: string;
|
|
2219
|
+
previousSchedule: {
|
|
2220
|
+
interval: "daily" | "weekly" | "monthly" | "manual";
|
|
2221
|
+
monthlyDays?: Array<number>;
|
|
2222
|
+
weeklyDays?: Array<string>;
|
|
2223
|
+
};
|
|
2224
|
+
revision: number;
|
|
2225
|
+
status: "pending" | "complete" | "canceled";
|
|
2226
|
+
targetSchedule: {
|
|
2227
|
+
interval: "daily" | "weekly" | "monthly" | "manual";
|
|
2228
|
+
monthlyDays?: Array<number>;
|
|
2229
|
+
weeklyDays?: Array<string>;
|
|
2230
|
+
};
|
|
2231
|
+
thresholds: Array<{ amount: number; currency: string }>;
|
|
2232
|
+
},
|
|
2233
|
+
Name
|
|
2234
|
+
>;
|
|
2235
|
+
cancelUnattempted: FunctionReference<
|
|
2236
|
+
"mutation",
|
|
2237
|
+
"internal",
|
|
2238
|
+
{
|
|
2239
|
+
accountId: string;
|
|
2240
|
+
observedSchedule: {
|
|
2241
|
+
interval: "daily" | "weekly" | "monthly" | "manual";
|
|
2242
|
+
monthlyDays?: Array<number>;
|
|
2243
|
+
weeklyDays?: Array<string>;
|
|
2244
|
+
};
|
|
2245
|
+
operationKey: string;
|
|
2246
|
+
ownerKey: string;
|
|
2247
|
+
},
|
|
2248
|
+
{
|
|
2249
|
+
_creationTime: number;
|
|
2250
|
+
_id: string;
|
|
2251
|
+
accountId: string;
|
|
2252
|
+
attempted?: boolean;
|
|
2253
|
+
country: string;
|
|
2254
|
+
expectedRevision: number;
|
|
2255
|
+
mode: "stripe_managed" | "custom";
|
|
2256
|
+
observedAt?: number;
|
|
2257
|
+
observedSchedule: {
|
|
2258
|
+
interval: "daily" | "weekly" | "monthly" | "manual";
|
|
2259
|
+
monthlyDays?: Array<number>;
|
|
2260
|
+
weeklyDays?: Array<string>;
|
|
2261
|
+
};
|
|
2262
|
+
operationKey: string;
|
|
2263
|
+
ownerKey: string;
|
|
2264
|
+
previousSchedule: {
|
|
2265
|
+
interval: "daily" | "weekly" | "monthly" | "manual";
|
|
2266
|
+
monthlyDays?: Array<number>;
|
|
2267
|
+
weeklyDays?: Array<string>;
|
|
2268
|
+
};
|
|
2269
|
+
revision: number;
|
|
2270
|
+
status: "pending" | "complete" | "canceled";
|
|
2271
|
+
targetSchedule: {
|
|
2272
|
+
interval: "daily" | "weekly" | "monthly" | "manual";
|
|
2273
|
+
monthlyDays?: Array<number>;
|
|
2274
|
+
weeklyDays?: Array<string>;
|
|
2275
|
+
};
|
|
2276
|
+
thresholds: Array<{ amount: number; currency: string }>;
|
|
2277
|
+
},
|
|
2278
|
+
Name
|
|
2279
|
+
>;
|
|
2280
|
+
confirm: FunctionReference<
|
|
2281
|
+
"mutation",
|
|
2282
|
+
"internal",
|
|
2283
|
+
{
|
|
2284
|
+
accountId: string;
|
|
2285
|
+
observedSchedule: {
|
|
2286
|
+
interval: "daily" | "weekly" | "monthly" | "manual";
|
|
2287
|
+
monthlyDays?: Array<number>;
|
|
2288
|
+
weeklyDays?: Array<string>;
|
|
2289
|
+
};
|
|
2290
|
+
operationKey: string;
|
|
2291
|
+
ownerKey: string;
|
|
2292
|
+
},
|
|
2293
|
+
{
|
|
2294
|
+
_creationTime: number;
|
|
2295
|
+
_id: string;
|
|
2296
|
+
accountId: string;
|
|
2297
|
+
attempted?: boolean;
|
|
2298
|
+
country: string;
|
|
2299
|
+
expectedRevision: number;
|
|
2300
|
+
mode: "stripe_managed" | "custom";
|
|
2301
|
+
observedAt?: number;
|
|
2302
|
+
observedSchedule: {
|
|
2303
|
+
interval: "daily" | "weekly" | "monthly" | "manual";
|
|
2304
|
+
monthlyDays?: Array<number>;
|
|
2305
|
+
weeklyDays?: Array<string>;
|
|
2306
|
+
};
|
|
2307
|
+
operationKey: string;
|
|
2308
|
+
ownerKey: string;
|
|
2309
|
+
previousSchedule: {
|
|
2310
|
+
interval: "daily" | "weekly" | "monthly" | "manual";
|
|
2311
|
+
monthlyDays?: Array<number>;
|
|
2312
|
+
weeklyDays?: Array<string>;
|
|
2313
|
+
};
|
|
2314
|
+
revision: number;
|
|
2315
|
+
status: "pending" | "complete" | "canceled";
|
|
2316
|
+
targetSchedule: {
|
|
2317
|
+
interval: "daily" | "weekly" | "monthly" | "manual";
|
|
2318
|
+
monthlyDays?: Array<number>;
|
|
2319
|
+
weeklyDays?: Array<string>;
|
|
2320
|
+
};
|
|
2321
|
+
thresholds: Array<{ amount: number; currency: string }>;
|
|
2322
|
+
},
|
|
2323
|
+
Name
|
|
2324
|
+
>;
|
|
2325
|
+
prepare: FunctionReference<
|
|
2326
|
+
"mutation",
|
|
2327
|
+
"internal",
|
|
2328
|
+
{
|
|
2329
|
+
accountId: string;
|
|
2330
|
+
country: string;
|
|
2331
|
+
expectedRevision: number;
|
|
2332
|
+
mode: "stripe_managed" | "custom";
|
|
2333
|
+
observedAt?: number;
|
|
2334
|
+
observedSchedule: {
|
|
2335
|
+
interval: "daily" | "weekly" | "monthly" | "manual";
|
|
2336
|
+
monthlyDays?: Array<number>;
|
|
2337
|
+
weeklyDays?: Array<string>;
|
|
2338
|
+
};
|
|
2339
|
+
operationKey: string;
|
|
2340
|
+
ownerKey: string;
|
|
2341
|
+
thresholds: Array<{ amount: number; currency: string }>;
|
|
2342
|
+
},
|
|
2343
|
+
{
|
|
2344
|
+
_creationTime: number;
|
|
2345
|
+
_id: string;
|
|
2346
|
+
accountId: string;
|
|
2347
|
+
attempted?: boolean;
|
|
2348
|
+
country: string;
|
|
2349
|
+
expectedRevision: number;
|
|
2350
|
+
mode: "stripe_managed" | "custom";
|
|
2351
|
+
observedAt?: number;
|
|
2352
|
+
observedSchedule: {
|
|
2353
|
+
interval: "daily" | "weekly" | "monthly" | "manual";
|
|
2354
|
+
monthlyDays?: Array<number>;
|
|
2355
|
+
weeklyDays?: Array<string>;
|
|
2356
|
+
};
|
|
2357
|
+
operationKey: string;
|
|
2358
|
+
ownerKey: string;
|
|
2359
|
+
previousSchedule: {
|
|
2360
|
+
interval: "daily" | "weekly" | "monthly" | "manual";
|
|
2361
|
+
monthlyDays?: Array<number>;
|
|
2362
|
+
weeklyDays?: Array<string>;
|
|
2363
|
+
};
|
|
2364
|
+
revision: number;
|
|
2365
|
+
status: "pending" | "complete" | "canceled";
|
|
2366
|
+
targetSchedule: {
|
|
2367
|
+
interval: "daily" | "weekly" | "monthly" | "manual";
|
|
2368
|
+
monthlyDays?: Array<number>;
|
|
2369
|
+
weeklyDays?: Array<string>;
|
|
2370
|
+
};
|
|
2371
|
+
thresholds: Array<{ amount: number; currency: string }>;
|
|
2372
|
+
},
|
|
2373
|
+
Name
|
|
2374
|
+
>;
|
|
2375
|
+
};
|
|
2376
|
+
queries: {
|
|
2377
|
+
due: FunctionReference<
|
|
2378
|
+
"query",
|
|
2379
|
+
"internal",
|
|
2380
|
+
{
|
|
2381
|
+
now: number;
|
|
2382
|
+
paginationOpts: {
|
|
2383
|
+
cursor: string | null;
|
|
2384
|
+
endCursor?: string | null;
|
|
2385
|
+
id?: number;
|
|
2386
|
+
maximumBytesRead?: number;
|
|
2387
|
+
maximumRowsRead?: number;
|
|
2388
|
+
numItems: number;
|
|
2389
|
+
};
|
|
2390
|
+
},
|
|
2391
|
+
{
|
|
2392
|
+
continueCursor: string;
|
|
2393
|
+
isDone: boolean;
|
|
2394
|
+
page: Array<{
|
|
2395
|
+
_creationTime: number;
|
|
2396
|
+
_id: string;
|
|
2397
|
+
accountId: string;
|
|
2398
|
+
country: string;
|
|
2399
|
+
holdDeadline?: number;
|
|
2400
|
+
mode: "stripe_managed" | "custom";
|
|
2401
|
+
operationKey: string;
|
|
2402
|
+
ownerKey: string;
|
|
2403
|
+
previousSchedule?: {
|
|
2404
|
+
interval: "daily" | "weekly" | "monthly" | "manual";
|
|
2405
|
+
monthlyDays?: Array<number>;
|
|
2406
|
+
weeklyDays?: Array<string>;
|
|
2407
|
+
};
|
|
2408
|
+
revision: number;
|
|
2409
|
+
status: "changing" | "ready";
|
|
2410
|
+
thresholds: Array<{ amount: number; currency: string }>;
|
|
2411
|
+
}>;
|
|
2412
|
+
},
|
|
2413
|
+
Name
|
|
2414
|
+
>;
|
|
2415
|
+
get: FunctionReference<
|
|
2416
|
+
"query",
|
|
2417
|
+
"internal",
|
|
2418
|
+
{ accountId: string; ownerKey: string },
|
|
2419
|
+
{
|
|
2420
|
+
_creationTime: number;
|
|
2421
|
+
_id: string;
|
|
2422
|
+
accountId: string;
|
|
2423
|
+
country: string;
|
|
2424
|
+
holdDeadline?: number;
|
|
2425
|
+
mode: "stripe_managed" | "custom";
|
|
2426
|
+
operationKey: string;
|
|
2427
|
+
ownerKey: string;
|
|
2428
|
+
previousSchedule?: {
|
|
2429
|
+
interval: "daily" | "weekly" | "monthly" | "manual";
|
|
2430
|
+
monthlyDays?: Array<number>;
|
|
2431
|
+
weeklyDays?: Array<string>;
|
|
2432
|
+
};
|
|
2433
|
+
revision: number;
|
|
2434
|
+
status: "changing" | "ready";
|
|
2435
|
+
thresholds: Array<{ amount: number; currency: string }>;
|
|
2436
|
+
} | null,
|
|
2437
|
+
Name
|
|
2438
|
+
>;
|
|
2439
|
+
operation: FunctionReference<
|
|
2440
|
+
"query",
|
|
2441
|
+
"internal",
|
|
2442
|
+
{ accountId: string; operationKey: string; ownerKey: string },
|
|
2443
|
+
{
|
|
2444
|
+
_creationTime: number;
|
|
2445
|
+
_id: string;
|
|
2446
|
+
accountId: string;
|
|
2447
|
+
attempted?: boolean;
|
|
2448
|
+
country: string;
|
|
2449
|
+
expectedRevision: number;
|
|
2450
|
+
mode: "stripe_managed" | "custom";
|
|
2451
|
+
observedAt?: number;
|
|
2452
|
+
observedSchedule: {
|
|
2453
|
+
interval: "daily" | "weekly" | "monthly" | "manual";
|
|
2454
|
+
monthlyDays?: Array<number>;
|
|
2455
|
+
weeklyDays?: Array<string>;
|
|
2456
|
+
};
|
|
2457
|
+
operationKey: string;
|
|
2458
|
+
ownerKey: string;
|
|
2459
|
+
previousSchedule: {
|
|
2460
|
+
interval: "daily" | "weekly" | "monthly" | "manual";
|
|
2461
|
+
monthlyDays?: Array<number>;
|
|
2462
|
+
weeklyDays?: Array<string>;
|
|
2463
|
+
};
|
|
2464
|
+
revision: number;
|
|
2465
|
+
status: "pending" | "complete" | "canceled";
|
|
2466
|
+
targetSchedule: {
|
|
2467
|
+
interval: "daily" | "weekly" | "monthly" | "manual";
|
|
2468
|
+
monthlyDays?: Array<number>;
|
|
2469
|
+
weeklyDays?: Array<string>;
|
|
2470
|
+
};
|
|
2471
|
+
thresholds: Array<{ amount: number; currency: string }>;
|
|
2472
|
+
} | null,
|
|
2473
|
+
Name
|
|
2474
|
+
>;
|
|
2475
|
+
};
|
|
2476
|
+
};
|
|
2197
2477
|
products: {
|
|
2198
2478
|
mutations: {
|
|
2199
2479
|
upsertPrice: FunctionReference<
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
getAccountPayouts as getAccountPayoutsLegacy,
|
|
4
4
|
} from "../earnings/legacyReads.js";
|
|
5
5
|
import { currencyTotals, previewSize } from "../earnings/reads.js";
|
|
6
|
+
import { addSummaryAmounts } from "../earnings/arithmetic.js";
|
|
6
7
|
import { v } from "convex/values";
|
|
7
8
|
import { paginator } from "convex-helpers/server/pagination";
|
|
8
9
|
import schema from "../schema.js";
|
|
@@ -557,7 +558,7 @@ export const getAccountEarningsByCurrency = query({
|
|
|
557
558
|
currency: r.currency,
|
|
558
559
|
gross: r.gross,
|
|
559
560
|
reversed: r.reversed,
|
|
560
|
-
net: r.gross -
|
|
561
|
+
net: addSummaryAmounts(r.gross, -r.reversed),
|
|
561
562
|
transferCount: r.count,
|
|
562
563
|
})),
|
|
563
564
|
transfers,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Check exact integer arithmetic before converting back to the public Number representation. */
|
|
2
|
+
export function addSummaryAmounts(left: number, right: number): number {
|
|
3
|
+
if (!Number.isSafeInteger(left) || !Number.isSafeInteger(right))
|
|
4
|
+
throw new Error("Currency totals require safe integer amounts");
|
|
5
|
+
const result = BigInt(left) + BigInt(right);
|
|
6
|
+
const limit = BigInt(Number.MAX_SAFE_INTEGER);
|
|
7
|
+
if (result > limit || result < -limit)
|
|
8
|
+
throw new Error("Currency totals exceed supported safe integer bounds");
|
|
9
|
+
return Number(result);
|
|
10
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { QueryCtx } from "../_generated/server.js";
|
|
2
|
+
import { addSummaryAmounts } from "./arithmetic.js";
|
|
2
3
|
export function previewSize(value = 50) {
|
|
3
4
|
if (!Number.isInteger(value) || value < 0 || value > 100)
|
|
4
5
|
throw new Error("Preview size must be an integer from 0 to 100");
|
|
@@ -37,5 +38,10 @@ export async function currencyTotals(
|
|
|
37
38
|
.take(501);
|
|
38
39
|
if (rows.length > 500)
|
|
39
40
|
throw new Error("Account exceeds supported currency count");
|
|
41
|
+
for (const row of rows) {
|
|
42
|
+
for (const value of [row.gross, row.reversed, row.paidOut, row.count])
|
|
43
|
+
addSummaryAmounts(value, 0);
|
|
44
|
+
addSummaryAmounts(row.gross, -row.reversed);
|
|
45
|
+
}
|
|
40
46
|
return rows;
|
|
41
47
|
}
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
customMutation,
|
|
5
5
|
} from "convex-helpers/server/customFunctions";
|
|
6
6
|
import type { DataModel } from "../_generated/dataModel.js";
|
|
7
|
+
import { addSummaryAmounts } from "./arithmetic.js";
|
|
7
8
|
import {
|
|
8
9
|
mutation as rawMutation,
|
|
9
10
|
type MutationCtx,
|
|
@@ -21,7 +22,7 @@ async function adjust(
|
|
|
21
22
|
ctx: MutationCtx,
|
|
22
23
|
kind: "earnings" | "payouts",
|
|
23
24
|
value: Contribution,
|
|
24
|
-
sign:
|
|
25
|
+
sign: 1 | -1,
|
|
25
26
|
) {
|
|
26
27
|
const row = await ctx.db
|
|
27
28
|
.query("currencyTotals")
|
|
@@ -33,11 +34,12 @@ async function adjust(
|
|
|
33
34
|
)
|
|
34
35
|
.unique();
|
|
35
36
|
const totals = {
|
|
36
|
-
gross: (row?.gross ?? 0
|
|
37
|
-
reversed: (row?.reversed ?? 0
|
|
38
|
-
paidOut: (row?.paidOut ?? 0
|
|
39
|
-
count: (row?.count ?? 0
|
|
37
|
+
gross: addSummaryAmounts(row?.gross ?? 0, sign * value.gross),
|
|
38
|
+
reversed: addSummaryAmounts(row?.reversed ?? 0, sign * value.reversed),
|
|
39
|
+
paidOut: addSummaryAmounts(row?.paidOut ?? 0, sign * value.paidOut),
|
|
40
|
+
count: addSummaryAmounts(row?.count ?? 0, sign * value.count),
|
|
40
41
|
};
|
|
42
|
+
addSummaryAmounts(totals.gross, -totals.reversed);
|
|
41
43
|
if (row) {
|
|
42
44
|
if (totals.count === 0) await ctx.db.delete("currencyTotals", row._id);
|
|
43
45
|
else await ctx.db.patch("currencyTotals", row._id, totals);
|
|
@@ -72,14 +74,17 @@ triggers.register("transfers", async (ctx, change) => {
|
|
|
72
74
|
triggers.register("payouts", async (ctx, change) => {
|
|
73
75
|
const contribution = (
|
|
74
76
|
row: NonNullable<typeof change.newDoc>,
|
|
75
|
-
): Contribution =>
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
): Contribution => {
|
|
78
|
+
const amount = addSummaryAmounts(row.amount, 0);
|
|
79
|
+
return {
|
|
80
|
+
accountId: row.accountId,
|
|
81
|
+
currency: row.currency,
|
|
82
|
+
gross: 0,
|
|
83
|
+
reversed: 0,
|
|
84
|
+
paidOut: row.status === "paid" ? amount : 0,
|
|
85
|
+
count: 1,
|
|
86
|
+
};
|
|
87
|
+
};
|
|
83
88
|
if (change.oldDoc?.totalsRecorded)
|
|
84
89
|
await adjust(ctx, "payouts", contribution(change.oldDoc), -1);
|
|
85
90
|
if (change.newDoc) {
|
|
@@ -0,0 +1,248 @@
|
|
|
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
|
+
const modules = import.meta.glob("./**/*.*s");
|
|
7
|
+
const m = (name: string) =>
|
|
8
|
+
makeFunctionReference<"mutation">(`payoutPolicy/mutations:${name}`);
|
|
9
|
+
const q = (name: string) =>
|
|
10
|
+
makeFunctionReference<"query">(`payoutPolicy/queries:${name}`);
|
|
11
|
+
const scope = { accountId: "acct_recipient", ownerKey: "user_1" };
|
|
12
|
+
const initial = {
|
|
13
|
+
...scope,
|
|
14
|
+
operationKey: "enable-1",
|
|
15
|
+
expectedRevision: -1,
|
|
16
|
+
mode: "custom",
|
|
17
|
+
thresholds: [{ currency: "usd", amount: 1000 }],
|
|
18
|
+
country: "US",
|
|
19
|
+
observedSchedule: { interval: "weekly", weeklyDays: ["friday", "monday"] },
|
|
20
|
+
};
|
|
21
|
+
it("persists restoration and a deadline before provider mutation, then confirms exactly once", async () => {
|
|
22
|
+
const t = convexTest(schema, modules);
|
|
23
|
+
const op = await t.mutation(m("prepare"), initial);
|
|
24
|
+
expect(op.status).toBe("pending");
|
|
25
|
+
expect(op.targetSchedule).toEqual({ interval: "manual" });
|
|
26
|
+
expect(op.previousSchedule).toEqual(initial.observedSchedule);
|
|
27
|
+
let policy = await t.query(q("get"), scope);
|
|
28
|
+
expect(policy.status).toBe("changing");
|
|
29
|
+
expect(policy.revision).toBe(0);
|
|
30
|
+
expect(policy.holdDeadline).toBeGreaterThan(Date.now());
|
|
31
|
+
await t.mutation(m("confirm"), {
|
|
32
|
+
...scope,
|
|
33
|
+
operationKey: "enable-1",
|
|
34
|
+
observedSchedule: { interval: "manual" },
|
|
35
|
+
});
|
|
36
|
+
policy = await t.query(q("get"), scope);
|
|
37
|
+
expect(policy.status).toBe("ready");
|
|
38
|
+
expect(policy.mode).toBe("custom");
|
|
39
|
+
expect(
|
|
40
|
+
await t.mutation(m("confirm"), {
|
|
41
|
+
...scope,
|
|
42
|
+
operationKey: "enable-1",
|
|
43
|
+
observedSchedule: { interval: "manual" },
|
|
44
|
+
}),
|
|
45
|
+
).toEqual(
|
|
46
|
+
await t.query(q("operation"), { ...scope, operationKey: "enable-1" }),
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
it("preserves the original schedule and deadline across threshold changes and opt-out", async () => {
|
|
50
|
+
const t = convexTest(schema, modules);
|
|
51
|
+
await t.mutation(m("prepare"), initial);
|
|
52
|
+
await t.mutation(m("confirm"), {
|
|
53
|
+
...scope,
|
|
54
|
+
operationKey: "enable-1",
|
|
55
|
+
observedSchedule: { interval: "manual" },
|
|
56
|
+
});
|
|
57
|
+
const original = await t.query(q("get"), scope);
|
|
58
|
+
const change = {
|
|
59
|
+
...initial,
|
|
60
|
+
operationKey: "change",
|
|
61
|
+
expectedRevision: 0,
|
|
62
|
+
thresholds: [
|
|
63
|
+
{ currency: "eur", amount: 500 },
|
|
64
|
+
{ currency: "usd", amount: 2000 },
|
|
65
|
+
],
|
|
66
|
+
observedSchedule: { interval: "manual" },
|
|
67
|
+
};
|
|
68
|
+
await t.mutation(m("prepare"), change);
|
|
69
|
+
await t.mutation(m("confirm"), {
|
|
70
|
+
...scope,
|
|
71
|
+
operationKey: "change",
|
|
72
|
+
observedSchedule: { interval: "manual" },
|
|
73
|
+
});
|
|
74
|
+
const updated = await t.query(q("get"), scope);
|
|
75
|
+
expect(updated.previousSchedule).toEqual(original.previousSchedule);
|
|
76
|
+
expect(updated.holdDeadline).toBe(original.holdDeadline);
|
|
77
|
+
const off = await t.mutation(m("prepare"), {
|
|
78
|
+
...change,
|
|
79
|
+
operationKey: "off",
|
|
80
|
+
expectedRevision: 1,
|
|
81
|
+
mode: "stripe_managed",
|
|
82
|
+
thresholds: [],
|
|
83
|
+
});
|
|
84
|
+
expect(off.targetSchedule).toEqual(initial.observedSchedule);
|
|
85
|
+
await t.mutation(m("confirm"), {
|
|
86
|
+
...scope,
|
|
87
|
+
operationKey: "off",
|
|
88
|
+
observedSchedule: initial.observedSchedule,
|
|
89
|
+
});
|
|
90
|
+
expect(await t.query(q("get"), scope)).toMatchObject({
|
|
91
|
+
mode: "stripe_managed",
|
|
92
|
+
status: "ready",
|
|
93
|
+
thresholds: [],
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
it("rejects changed replays, ownership changes, stale revisions and competing operations", async () => {
|
|
97
|
+
const t = convexTest(schema, modules);
|
|
98
|
+
const op = await t.mutation(m("prepare"), initial);
|
|
99
|
+
expect(await t.mutation(m("prepare"), initial)).toEqual(op);
|
|
100
|
+
for (const changed of [
|
|
101
|
+
{ thresholds: [{ currency: "usd", amount: 900 }] },
|
|
102
|
+
{ ownerKey: "other" },
|
|
103
|
+
{ accountId: "acct_other" },
|
|
104
|
+
])
|
|
105
|
+
await expect(
|
|
106
|
+
t.mutation(m("prepare"), { ...initial, ...changed }),
|
|
107
|
+
).rejects.toThrow(/identity|operation/);
|
|
108
|
+
await expect(
|
|
109
|
+
t.mutation(m("prepare"), {
|
|
110
|
+
...initial,
|
|
111
|
+
operationKey: "other",
|
|
112
|
+
expectedRevision: 0,
|
|
113
|
+
}),
|
|
114
|
+
).rejects.toThrow(/pending/);
|
|
115
|
+
await t.mutation(m("confirm"), {
|
|
116
|
+
...scope,
|
|
117
|
+
operationKey: "enable-1",
|
|
118
|
+
observedSchedule: { interval: "manual" },
|
|
119
|
+
});
|
|
120
|
+
await expect(
|
|
121
|
+
t.mutation(m("prepare"), { ...initial, operationKey: "other" }),
|
|
122
|
+
).rejects.toThrow(/revision/);
|
|
123
|
+
await expect(
|
|
124
|
+
t.query(q("get"), { ...scope, ownerKey: "other" }),
|
|
125
|
+
).rejects.toThrow(/owner/);
|
|
126
|
+
});
|
|
127
|
+
it("cannot confirm a different provider schedule or hide external drift", async () => {
|
|
128
|
+
const t = convexTest(schema, modules);
|
|
129
|
+
await t.mutation(m("prepare"), initial);
|
|
130
|
+
await expect(
|
|
131
|
+
t.mutation(m("confirm"), {
|
|
132
|
+
...scope,
|
|
133
|
+
operationKey: "enable-1",
|
|
134
|
+
observedSchedule: { interval: "daily" },
|
|
135
|
+
}),
|
|
136
|
+
).rejects.toThrow(/schedule/);
|
|
137
|
+
await t.mutation(m("confirm"), {
|
|
138
|
+
...scope,
|
|
139
|
+
operationKey: "enable-1",
|
|
140
|
+
observedSchedule: { interval: "manual" },
|
|
141
|
+
});
|
|
142
|
+
await expect(
|
|
143
|
+
t.mutation(m("prepare"), {
|
|
144
|
+
...initial,
|
|
145
|
+
operationKey: "change",
|
|
146
|
+
expectedRevision: 0,
|
|
147
|
+
observedSchedule: { interval: "daily" },
|
|
148
|
+
}),
|
|
149
|
+
).rejects.toThrow(/drift/);
|
|
150
|
+
});
|
|
151
|
+
it.each([0, -1, 1.5, Number.MAX_SAFE_INTEGER + 1])(
|
|
152
|
+
"rejects invalid custom minor-unit thresholds (%s)",
|
|
153
|
+
async (amount) => {
|
|
154
|
+
const t = convexTest(schema, modules);
|
|
155
|
+
await expect(
|
|
156
|
+
t.mutation(m("prepare"), {
|
|
157
|
+
...initial,
|
|
158
|
+
thresholds: [{ currency: "usd", amount }],
|
|
159
|
+
}),
|
|
160
|
+
).rejects.toThrow(/threshold/);
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
it("uses country holding limits and never extends a hold by editing thresholds", async () => {
|
|
164
|
+
for (const [country, days] of [
|
|
165
|
+
["TH", 10],
|
|
166
|
+
["US", 730],
|
|
167
|
+
["GB", 90],
|
|
168
|
+
] as const) {
|
|
169
|
+
const t = convexTest(schema, modules);
|
|
170
|
+
const before = Date.now();
|
|
171
|
+
await t.mutation(m("prepare"), { ...initial, country });
|
|
172
|
+
const policy = await t.query(q("get"), scope);
|
|
173
|
+
expect(policy.holdDeadline).toBeGreaterThanOrEqual(
|
|
174
|
+
before + days * 86400000,
|
|
175
|
+
);
|
|
176
|
+
expect(policy.holdDeadline).toBeLessThan(
|
|
177
|
+
Date.now() + days * 86400000 + 1000,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
it("returns bounded due policies and retains expired changing operations for reconciliation", async () => {
|
|
182
|
+
const t = convexTest(schema, modules);
|
|
183
|
+
await t.mutation(m("prepare"), initial);
|
|
184
|
+
const p = await t.query(q("get"), scope);
|
|
185
|
+
const result = await t.query(q("due"), {
|
|
186
|
+
now: p.holdDeadline + 1,
|
|
187
|
+
paginationOpts: { numItems: 1, cursor: null },
|
|
188
|
+
});
|
|
189
|
+
expect(result.page).toHaveLength(1);
|
|
190
|
+
expect(result.page[0].status).toBe("changing");
|
|
191
|
+
});
|
|
192
|
+
it("accepts equivalent retries regardless of object property order", async () => {
|
|
193
|
+
const t = convexTest(schema, modules);
|
|
194
|
+
const op = await t.mutation(m("prepare"), initial);
|
|
195
|
+
expect(
|
|
196
|
+
await t.mutation(
|
|
197
|
+
m("prepare"),
|
|
198
|
+
Object.fromEntries(Object.entries(initial).reverse()),
|
|
199
|
+
),
|
|
200
|
+
).toEqual(op);
|
|
201
|
+
});
|
|
202
|
+
it("marks attempts durably and only cancels an unattempted provider change", async () => {
|
|
203
|
+
const t = convexTest(schema, modules);
|
|
204
|
+
await t.mutation(m("prepare"), initial);
|
|
205
|
+
const args = {
|
|
206
|
+
...scope,
|
|
207
|
+
operationKey: "enable-1",
|
|
208
|
+
observedSchedule: initial.observedSchedule,
|
|
209
|
+
};
|
|
210
|
+
expect((await t.mutation(m("cancelUnattempted"), args)).status).toBe(
|
|
211
|
+
"canceled",
|
|
212
|
+
);
|
|
213
|
+
expect(await t.query(q("get"), scope)).toMatchObject({
|
|
214
|
+
mode: "stripe_managed",
|
|
215
|
+
status: "ready",
|
|
216
|
+
});
|
|
217
|
+
await expect(
|
|
218
|
+
t.mutation(m("confirm"), {
|
|
219
|
+
...scope,
|
|
220
|
+
operationKey: "enable-1",
|
|
221
|
+
observedSchedule: { interval: "manual" },
|
|
222
|
+
}),
|
|
223
|
+
).rejects.toThrow(/canceled/);
|
|
224
|
+
await t.mutation(m("prepare"), {
|
|
225
|
+
...initial,
|
|
226
|
+
operationKey: "second",
|
|
227
|
+
expectedRevision: 0,
|
|
228
|
+
});
|
|
229
|
+
await t.mutation(m("beginAttempt"), { ...scope, operationKey: "second" });
|
|
230
|
+
await expect(
|
|
231
|
+
t.mutation(m("cancelUnattempted"), { ...args, operationKey: "second" }),
|
|
232
|
+
).rejects.toThrow(/attempt/);
|
|
233
|
+
});
|
|
234
|
+
it("anchors the hold deadline before provider reads and rejects future observations", async () => {
|
|
235
|
+
const t = convexTest(schema, modules);
|
|
236
|
+
const observedAt = Date.now() - 60000;
|
|
237
|
+
await t.mutation(m("prepare"), { ...initial, observedAt });
|
|
238
|
+
expect((await t.query(q("get"), scope)).holdDeadline).toBe(
|
|
239
|
+
observedAt + 730 * 86400000,
|
|
240
|
+
);
|
|
241
|
+
await expect(
|
|
242
|
+
t.mutation(m("prepare"), {
|
|
243
|
+
...initial,
|
|
244
|
+
operationKey: "future",
|
|
245
|
+
observedAt: Date.now() + 60000,
|
|
246
|
+
}),
|
|
247
|
+
).rejects.toThrow(/observation/);
|
|
248
|
+
});
|