@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
|
@@ -2199,11 +2199,41 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
2199
2199
|
beginAttempt: FunctionReference<
|
|
2200
2200
|
"mutation",
|
|
2201
2201
|
"internal",
|
|
2202
|
-
{
|
|
2202
|
+
{
|
|
2203
|
+
accountId: string;
|
|
2204
|
+
adoption?: {
|
|
2205
|
+
claims: Array<{ fingerprint: string; operationKey: string }>;
|
|
2206
|
+
observations: Array<{
|
|
2207
|
+
currency: string;
|
|
2208
|
+
ledgerFingerprint: string;
|
|
2209
|
+
matureNet: number;
|
|
2210
|
+
observedAt: number;
|
|
2211
|
+
pendingCredits: number;
|
|
2212
|
+
stripeTransferId: string;
|
|
2213
|
+
transactionIds: Array<string>;
|
|
2214
|
+
}>;
|
|
2215
|
+
oldestHeldAt?: number;
|
|
2216
|
+
};
|
|
2217
|
+
operationKey: string;
|
|
2218
|
+
ownerKey: string;
|
|
2219
|
+
},
|
|
2203
2220
|
{
|
|
2204
2221
|
_creationTime: number;
|
|
2205
2222
|
_id: string;
|
|
2206
2223
|
accountId: string;
|
|
2224
|
+
adoption?: {
|
|
2225
|
+
claims: Array<{ fingerprint: string; operationKey: string }>;
|
|
2226
|
+
observations: Array<{
|
|
2227
|
+
currency: string;
|
|
2228
|
+
ledgerFingerprint: string;
|
|
2229
|
+
matureNet: number;
|
|
2230
|
+
observedAt: number;
|
|
2231
|
+
pendingCredits: number;
|
|
2232
|
+
stripeTransferId: string;
|
|
2233
|
+
transactionIds: Array<string>;
|
|
2234
|
+
}>;
|
|
2235
|
+
oldestHeldAt?: number;
|
|
2236
|
+
};
|
|
2207
2237
|
attempted?: boolean;
|
|
2208
2238
|
country: string;
|
|
2209
2239
|
expectedRevision: number;
|
|
@@ -2249,6 +2279,19 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
2249
2279
|
_creationTime: number;
|
|
2250
2280
|
_id: string;
|
|
2251
2281
|
accountId: string;
|
|
2282
|
+
adoption?: {
|
|
2283
|
+
claims: Array<{ fingerprint: string; operationKey: string }>;
|
|
2284
|
+
observations: Array<{
|
|
2285
|
+
currency: string;
|
|
2286
|
+
ledgerFingerprint: string;
|
|
2287
|
+
matureNet: number;
|
|
2288
|
+
observedAt: number;
|
|
2289
|
+
pendingCredits: number;
|
|
2290
|
+
stripeTransferId: string;
|
|
2291
|
+
transactionIds: Array<string>;
|
|
2292
|
+
}>;
|
|
2293
|
+
oldestHeldAt?: number;
|
|
2294
|
+
};
|
|
2252
2295
|
attempted?: boolean;
|
|
2253
2296
|
country: string;
|
|
2254
2297
|
expectedRevision: number;
|
|
@@ -2294,6 +2337,19 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
2294
2337
|
_creationTime: number;
|
|
2295
2338
|
_id: string;
|
|
2296
2339
|
accountId: string;
|
|
2340
|
+
adoption?: {
|
|
2341
|
+
claims: Array<{ fingerprint: string; operationKey: string }>;
|
|
2342
|
+
observations: Array<{
|
|
2343
|
+
currency: string;
|
|
2344
|
+
ledgerFingerprint: string;
|
|
2345
|
+
matureNet: number;
|
|
2346
|
+
observedAt: number;
|
|
2347
|
+
pendingCredits: number;
|
|
2348
|
+
stripeTransferId: string;
|
|
2349
|
+
transactionIds: Array<string>;
|
|
2350
|
+
}>;
|
|
2351
|
+
oldestHeldAt?: number;
|
|
2352
|
+
};
|
|
2297
2353
|
attempted?: boolean;
|
|
2298
2354
|
country: string;
|
|
2299
2355
|
expectedRevision: number;
|
|
@@ -2327,6 +2383,19 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
2327
2383
|
"internal",
|
|
2328
2384
|
{
|
|
2329
2385
|
accountId: string;
|
|
2386
|
+
adoption?: {
|
|
2387
|
+
claims: Array<{ fingerprint: string; operationKey: string }>;
|
|
2388
|
+
observations: Array<{
|
|
2389
|
+
currency: string;
|
|
2390
|
+
ledgerFingerprint: string;
|
|
2391
|
+
matureNet: number;
|
|
2392
|
+
observedAt: number;
|
|
2393
|
+
pendingCredits: number;
|
|
2394
|
+
stripeTransferId: string;
|
|
2395
|
+
transactionIds: Array<string>;
|
|
2396
|
+
}>;
|
|
2397
|
+
oldestHeldAt?: number;
|
|
2398
|
+
};
|
|
2330
2399
|
country: string;
|
|
2331
2400
|
expectedRevision: number;
|
|
2332
2401
|
mode: "stripe_managed" | "custom";
|
|
@@ -2344,6 +2413,19 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
2344
2413
|
_creationTime: number;
|
|
2345
2414
|
_id: string;
|
|
2346
2415
|
accountId: string;
|
|
2416
|
+
adoption?: {
|
|
2417
|
+
claims: Array<{ fingerprint: string; operationKey: string }>;
|
|
2418
|
+
observations: Array<{
|
|
2419
|
+
currency: string;
|
|
2420
|
+
ledgerFingerprint: string;
|
|
2421
|
+
matureNet: number;
|
|
2422
|
+
observedAt: number;
|
|
2423
|
+
pendingCredits: number;
|
|
2424
|
+
stripeTransferId: string;
|
|
2425
|
+
transactionIds: Array<string>;
|
|
2426
|
+
}>;
|
|
2427
|
+
oldestHeldAt?: number;
|
|
2428
|
+
};
|
|
2347
2429
|
attempted?: boolean;
|
|
2348
2430
|
country: string;
|
|
2349
2431
|
expectedRevision: number;
|
|
@@ -2444,6 +2526,19 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
2444
2526
|
_creationTime: number;
|
|
2445
2527
|
_id: string;
|
|
2446
2528
|
accountId: string;
|
|
2529
|
+
adoption?: {
|
|
2530
|
+
claims: Array<{ fingerprint: string; operationKey: string }>;
|
|
2531
|
+
observations: Array<{
|
|
2532
|
+
currency: string;
|
|
2533
|
+
ledgerFingerprint: string;
|
|
2534
|
+
matureNet: number;
|
|
2535
|
+
observedAt: number;
|
|
2536
|
+
pendingCredits: number;
|
|
2537
|
+
stripeTransferId: string;
|
|
2538
|
+
transactionIds: Array<string>;
|
|
2539
|
+
}>;
|
|
2540
|
+
oldestHeldAt?: number;
|
|
2541
|
+
};
|
|
2447
2542
|
attempted?: boolean;
|
|
2448
2543
|
country: string;
|
|
2449
2544
|
expectedRevision: number;
|
|
@@ -2476,6 +2571,94 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
2476
2571
|
};
|
|
2477
2572
|
payoutRelease: {
|
|
2478
2573
|
mutations: {
|
|
2574
|
+
adoptHistoricalBatch: FunctionReference<
|
|
2575
|
+
"mutation",
|
|
2576
|
+
"internal",
|
|
2577
|
+
{
|
|
2578
|
+
accountId: string;
|
|
2579
|
+
expectedClaims: Array<{
|
|
2580
|
+
fingerprint: string;
|
|
2581
|
+
operationKey: string;
|
|
2582
|
+
}>;
|
|
2583
|
+
expectedRevision: number;
|
|
2584
|
+
observations: Array<{
|
|
2585
|
+
currency: string;
|
|
2586
|
+
ledgerFingerprint: string;
|
|
2587
|
+
matureNet: number;
|
|
2588
|
+
observedAt: number;
|
|
2589
|
+
pendingCredits: number;
|
|
2590
|
+
stripeTransferId: string;
|
|
2591
|
+
transactionIds: Array<string>;
|
|
2592
|
+
}>;
|
|
2593
|
+
ownerKey: string;
|
|
2594
|
+
payouts: Array<{
|
|
2595
|
+
allocations: Array<{
|
|
2596
|
+
amount: number;
|
|
2597
|
+
role?: "store" | "affiliate" | "other";
|
|
2598
|
+
stripeTransferId: string;
|
|
2599
|
+
}>;
|
|
2600
|
+
payout: {
|
|
2601
|
+
amount: number;
|
|
2602
|
+
created: number;
|
|
2603
|
+
currency: string;
|
|
2604
|
+
debitFee: number;
|
|
2605
|
+
debitTransactionId: string;
|
|
2606
|
+
destinationId: string;
|
|
2607
|
+
returnFee?: number;
|
|
2608
|
+
returnTransactionId?: string;
|
|
2609
|
+
status: "paid" | "returned";
|
|
2610
|
+
stripePayoutId: string;
|
|
2611
|
+
};
|
|
2612
|
+
}>;
|
|
2613
|
+
},
|
|
2614
|
+
{
|
|
2615
|
+
claims: Array<{
|
|
2616
|
+
_creationTime: number;
|
|
2617
|
+
_id: string;
|
|
2618
|
+
accountId: string;
|
|
2619
|
+
allocations: Array<{
|
|
2620
|
+
amount: number;
|
|
2621
|
+
role?: "store" | "affiliate" | "other";
|
|
2622
|
+
stripeTransferId: string;
|
|
2623
|
+
}>;
|
|
2624
|
+
amount: number;
|
|
2625
|
+
attemptedAt?: number;
|
|
2626
|
+
currency: string;
|
|
2627
|
+
debitFee?: number;
|
|
2628
|
+
debitTransactionId?: string;
|
|
2629
|
+
destinationId?: string;
|
|
2630
|
+
expectedRevision: number;
|
|
2631
|
+
feeRevision?: number;
|
|
2632
|
+
historical?: boolean;
|
|
2633
|
+
observations: Array<{
|
|
2634
|
+
currency: string;
|
|
2635
|
+
ledgerFingerprint: string;
|
|
2636
|
+
matureNet: number;
|
|
2637
|
+
observedAt: number;
|
|
2638
|
+
pendingCredits: number;
|
|
2639
|
+
stripeTransferId: string;
|
|
2640
|
+
transactionIds: Array<string>;
|
|
2641
|
+
}>;
|
|
2642
|
+
operationKey: string;
|
|
2643
|
+
ownerKey: string;
|
|
2644
|
+
providerFee?: number;
|
|
2645
|
+
rejection?: {
|
|
2646
|
+
code: "balance_insufficient" | "amount_too_small";
|
|
2647
|
+
requestId: string;
|
|
2648
|
+
};
|
|
2649
|
+
returnTransactionId?: string;
|
|
2650
|
+
status:
|
|
2651
|
+
| "reserved"
|
|
2652
|
+
| "submitted"
|
|
2653
|
+
| "paid"
|
|
2654
|
+
| "returned"
|
|
2655
|
+
| "canceled"
|
|
2656
|
+
| "rejected";
|
|
2657
|
+
stripePayoutId?: string;
|
|
2658
|
+
}>;
|
|
2659
|
+
},
|
|
2660
|
+
Name
|
|
2661
|
+
>;
|
|
2479
2662
|
beginAttempt: FunctionReference<
|
|
2480
2663
|
"mutation",
|
|
2481
2664
|
"internal",
|
|
@@ -2497,6 +2680,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
2497
2680
|
destinationId?: string;
|
|
2498
2681
|
expectedRevision: number;
|
|
2499
2682
|
feeRevision?: number;
|
|
2683
|
+
historical?: boolean;
|
|
2500
2684
|
observations: Array<{
|
|
2501
2685
|
currency: string;
|
|
2502
2686
|
ledgerFingerprint: string;
|
|
@@ -2546,6 +2730,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
2546
2730
|
destinationId?: string;
|
|
2547
2731
|
expectedRevision: number;
|
|
2548
2732
|
feeRevision?: number;
|
|
2733
|
+
historical?: boolean;
|
|
2549
2734
|
observations: Array<{
|
|
2550
2735
|
currency: string;
|
|
2551
2736
|
ledgerFingerprint: string;
|
|
@@ -2613,6 +2798,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
2613
2798
|
destinationId?: string;
|
|
2614
2799
|
expectedRevision: number;
|
|
2615
2800
|
feeRevision?: number;
|
|
2801
|
+
historical?: boolean;
|
|
2616
2802
|
observations: Array<{
|
|
2617
2803
|
currency: string;
|
|
2618
2804
|
ledgerFingerprint: string;
|
|
@@ -2685,6 +2871,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
2685
2871
|
destinationId?: string;
|
|
2686
2872
|
expectedRevision: number;
|
|
2687
2873
|
feeRevision?: number;
|
|
2874
|
+
historical?: boolean;
|
|
2688
2875
|
observations: Array<{
|
|
2689
2876
|
currency: string;
|
|
2690
2877
|
ledgerFingerprint: string;
|
|
@@ -2740,6 +2927,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
2740
2927
|
destinationId?: string;
|
|
2741
2928
|
expectedRevision: number;
|
|
2742
2929
|
feeRevision?: number;
|
|
2930
|
+
historical?: boolean;
|
|
2743
2931
|
observations: Array<{
|
|
2744
2932
|
currency: string;
|
|
2745
2933
|
ledgerFingerprint: string;
|
|
@@ -2798,6 +2986,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
2798
2986
|
destinationId?: string;
|
|
2799
2987
|
expectedRevision: number;
|
|
2800
2988
|
feeRevision?: number;
|
|
2989
|
+
historical?: boolean;
|
|
2801
2990
|
observations: Array<{
|
|
2802
2991
|
currency: string;
|
|
2803
2992
|
ledgerFingerprint: string;
|
|
@@ -2861,6 +3050,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
2861
3050
|
destinationId?: string;
|
|
2862
3051
|
expectedRevision: number;
|
|
2863
3052
|
feeRevision?: number;
|
|
3053
|
+
historical?: boolean;
|
|
2864
3054
|
observations: Array<{
|
|
2865
3055
|
currency: string;
|
|
2866
3056
|
ledgerFingerprint: string;
|
|
@@ -2913,6 +3103,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
2913
3103
|
destinationId?: string;
|
|
2914
3104
|
expectedRevision: number;
|
|
2915
3105
|
feeRevision?: number;
|
|
3106
|
+
historical?: boolean;
|
|
2916
3107
|
observations: Array<{
|
|
2917
3108
|
currency: string;
|
|
2918
3109
|
ledgerFingerprint: string;
|
|
@@ -0,0 +1,272 @@
|
|
|
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 {
|
|
7
|
+
transferFingerprint,
|
|
8
|
+
payoutClaimFingerprint,
|
|
9
|
+
} from "./payoutRelease/helpers.js";
|
|
10
|
+
const modules = import.meta.glob("./**/*.*s");
|
|
11
|
+
const scope = { accountId: "acct_one", ownerKey: "owner" };
|
|
12
|
+
|
|
13
|
+
async function setup() {
|
|
14
|
+
const t = convexTest(schema, modules);
|
|
15
|
+
await t.run(async (ctx) => {
|
|
16
|
+
await ctx.db.insert("payoutPolicies", {
|
|
17
|
+
...scope,
|
|
18
|
+
mode: "stripe_managed",
|
|
19
|
+
status: "ready",
|
|
20
|
+
revision: 0,
|
|
21
|
+
operationKey: "default",
|
|
22
|
+
country: "US",
|
|
23
|
+
thresholds: [],
|
|
24
|
+
});
|
|
25
|
+
for (const [id, amount, role] of [
|
|
26
|
+
["tr_one", 600, "store"],
|
|
27
|
+
["tr_two", 400, "affiliate"],
|
|
28
|
+
] as const)
|
|
29
|
+
await ctx.db.insert("transfers", {
|
|
30
|
+
stripeTransferId: id,
|
|
31
|
+
amount,
|
|
32
|
+
currency: "usd",
|
|
33
|
+
destinationAccountId: scope.accountId,
|
|
34
|
+
status: "paid",
|
|
35
|
+
role,
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
const rows = await t.run((ctx) => ctx.db.query("transfers").collect());
|
|
39
|
+
const args = {
|
|
40
|
+
...scope,
|
|
41
|
+
expectedRevision: 0,
|
|
42
|
+
payout: {
|
|
43
|
+
stripePayoutId: "po_old",
|
|
44
|
+
amount: 1000,
|
|
45
|
+
currency: "usd",
|
|
46
|
+
created: Math.floor(Date.now() / 1000) - 100,
|
|
47
|
+
destinationId: "ba_one",
|
|
48
|
+
status: "paid",
|
|
49
|
+
debitTransactionId: "txn_debit",
|
|
50
|
+
debitFee: 30,
|
|
51
|
+
},
|
|
52
|
+
allocations: [
|
|
53
|
+
{ stripeTransferId: "tr_one", amount: 600 },
|
|
54
|
+
{ stripeTransferId: "tr_two", amount: 400 },
|
|
55
|
+
],
|
|
56
|
+
observations: rows.map((row) => ({
|
|
57
|
+
stripeTransferId: row.stripeTransferId,
|
|
58
|
+
currency: "usd",
|
|
59
|
+
matureNet: row.amount,
|
|
60
|
+
pendingCredits: 0,
|
|
61
|
+
ledgerFingerprint: transferFingerprint(row),
|
|
62
|
+
observedAt: Date.now(),
|
|
63
|
+
transactionIds: [`txn_${row.stripeTransferId}`],
|
|
64
|
+
})),
|
|
65
|
+
};
|
|
66
|
+
return { t, args };
|
|
67
|
+
}
|
|
68
|
+
async function importOne(
|
|
69
|
+
t: ReturnType<typeof convexTest>,
|
|
70
|
+
args: Awaited<ReturnType<typeof setup>>["args"] & {
|
|
71
|
+
payout: { returnTransactionId?: string; returnFee?: number };
|
|
72
|
+
},
|
|
73
|
+
) {
|
|
74
|
+
const rows = await t.run((ctx) =>
|
|
75
|
+
ctx.db.query("payoutReleaseClaims").collect(),
|
|
76
|
+
);
|
|
77
|
+
const result = await t.mutation(
|
|
78
|
+
makeFunctionReference<"mutation">(
|
|
79
|
+
"payoutRelease/mutations:adoptHistoricalBatch",
|
|
80
|
+
),
|
|
81
|
+
{
|
|
82
|
+
accountId: args.accountId,
|
|
83
|
+
ownerKey: args.ownerKey,
|
|
84
|
+
expectedRevision: args.expectedRevision,
|
|
85
|
+
observations: args.observations,
|
|
86
|
+
expectedClaims: rows.map((c) => ({
|
|
87
|
+
operationKey: c.operationKey,
|
|
88
|
+
fingerprint: payoutClaimFingerprint(c),
|
|
89
|
+
})),
|
|
90
|
+
payouts: [{ payout: args.payout, allocations: args.allocations }],
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
return result.claims[0];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
it("imports historical paid principal and actual fees once", async () => {
|
|
97
|
+
const { t, args } = await setup();
|
|
98
|
+
expect(await importOne(t, args)).toMatchObject({
|
|
99
|
+
historical: true,
|
|
100
|
+
status: "paid",
|
|
101
|
+
amount: 1000,
|
|
102
|
+
providerFee: 30,
|
|
103
|
+
destinationId: "ba_one",
|
|
104
|
+
});
|
|
105
|
+
await importOne(t, args);
|
|
106
|
+
expect(
|
|
107
|
+
await t.run((ctx) => ctx.db.query("payoutSourceAllocations").collect()),
|
|
108
|
+
).toMatchObject([
|
|
109
|
+
{ paidAmount: 600, reservedAmount: 0 },
|
|
110
|
+
{ paidAmount: 400, reservedAmount: 0 },
|
|
111
|
+
]);
|
|
112
|
+
expect(
|
|
113
|
+
await t.run((ctx) => ctx.db.query("payoutFeeBalances").collect()),
|
|
114
|
+
).toMatchObject([{ amount: 30 }]);
|
|
115
|
+
});
|
|
116
|
+
it("imports a verified returned payout without recording its principal as paid", async () => {
|
|
117
|
+
const { t, args } = await setup();
|
|
118
|
+
expect(
|
|
119
|
+
await importOne(t, {
|
|
120
|
+
...args,
|
|
121
|
+
payout: {
|
|
122
|
+
...args.payout,
|
|
123
|
+
status: "returned",
|
|
124
|
+
returnTransactionId: "txn_return",
|
|
125
|
+
returnFee: -10,
|
|
126
|
+
},
|
|
127
|
+
}),
|
|
128
|
+
).toMatchObject({ status: "returned", providerFee: 20 });
|
|
129
|
+
expect(
|
|
130
|
+
await t.run((ctx) => ctx.db.query("payoutSourceAllocations").collect()),
|
|
131
|
+
).toMatchObject([{ paidAmount: 0 }, { paidAmount: 0 }]);
|
|
132
|
+
});
|
|
133
|
+
it("rejects changing source allocations on replay", async () => {
|
|
134
|
+
const { t, args } = await setup();
|
|
135
|
+
await importOne(t, args);
|
|
136
|
+
await expect(
|
|
137
|
+
importOne(t, {
|
|
138
|
+
...args,
|
|
139
|
+
allocations: [
|
|
140
|
+
{ stripeTransferId: "tr_one", amount: 500 },
|
|
141
|
+
{ stripeTransferId: "tr_two", amount: 500 },
|
|
142
|
+
],
|
|
143
|
+
}),
|
|
144
|
+
).rejects.toThrow(/identity|allocation/i);
|
|
145
|
+
});
|
|
146
|
+
it.each([999, 1001, 0, -1, 1.5])(
|
|
147
|
+
"requires exact positive integer allocations: %s",
|
|
148
|
+
async (amount) => {
|
|
149
|
+
const { t, args } = await setup();
|
|
150
|
+
await expect(
|
|
151
|
+
importOne(t, {
|
|
152
|
+
...args,
|
|
153
|
+
allocations: [{ stripeTransferId: "tr_one", amount }],
|
|
154
|
+
}),
|
|
155
|
+
).rejects.toThrow();
|
|
156
|
+
expect(
|
|
157
|
+
await t.run((ctx) => ctx.db.query("payoutSourceAllocations").collect()),
|
|
158
|
+
).toEqual([]);
|
|
159
|
+
},
|
|
160
|
+
);
|
|
161
|
+
it("rejects another owner before any ledger writes", async () => {
|
|
162
|
+
const { t, args } = await setup();
|
|
163
|
+
await expect(importOne(t, { ...args, ownerKey: "other" })).rejects.toThrow(
|
|
164
|
+
/owner/i,
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
it("rejects an incomplete source snapshot", async () => {
|
|
168
|
+
const { t, args } = await setup();
|
|
169
|
+
await expect(
|
|
170
|
+
importOne(t, { ...args, observations: args.observations.slice(0, 1) }),
|
|
171
|
+
).rejects.toThrow(/snapshot/i);
|
|
172
|
+
});
|
|
173
|
+
it("requires inactive automatic policy at the observed revision", async () => {
|
|
174
|
+
const { t, args } = await setup();
|
|
175
|
+
await expect(importOne(t, { ...args, expectedRevision: 1 })).rejects.toThrow(
|
|
176
|
+
/revision/i,
|
|
177
|
+
);
|
|
178
|
+
await t.run(async (ctx) => {
|
|
179
|
+
const p = await ctx.db.query("payoutPolicies").first();
|
|
180
|
+
await ctx.db.patch("payoutPolicies", p!._id, { mode: "custom" });
|
|
181
|
+
});
|
|
182
|
+
await expect(importOne(t, args)).rejects.toThrow(/policy/i);
|
|
183
|
+
});
|
|
184
|
+
it("retains historical paid allocations even when a later refund leaves debt", async () => {
|
|
185
|
+
const { t, args } = await setup();
|
|
186
|
+
await t.run(async (ctx) => {
|
|
187
|
+
const row = await ctx.db.query("transfers").first();
|
|
188
|
+
await ctx.db.patch("transfers", row!._id, { reversedAmount: 500 });
|
|
189
|
+
});
|
|
190
|
+
const rows = await t.run((ctx) => ctx.db.query("transfers").collect());
|
|
191
|
+
const observations = args.observations.map((o) => ({
|
|
192
|
+
...o,
|
|
193
|
+
matureNet: o.stripeTransferId === "tr_one" ? 100 : o.matureNet,
|
|
194
|
+
ledgerFingerprint: transferFingerprint(
|
|
195
|
+
rows.find((r) => r.stripeTransferId === o.stripeTransferId)!,
|
|
196
|
+
),
|
|
197
|
+
}));
|
|
198
|
+
await importOne(t, { ...args, observations });
|
|
199
|
+
expect(
|
|
200
|
+
await t.run((ctx) => ctx.db.query("payoutSourceAllocations").collect()),
|
|
201
|
+
).toMatchObject([{ paidAmount: 600 }, { paidAmount: 400 }]);
|
|
202
|
+
});
|
|
203
|
+
it("imports a complete historical batch atomically and rejects stale competing mappings", async () => {
|
|
204
|
+
const { t, args } = await setup();
|
|
205
|
+
const batch = makeFunctionReference<"mutation">(
|
|
206
|
+
"payoutRelease/mutations:adoptHistoricalBatch",
|
|
207
|
+
);
|
|
208
|
+
const input = {
|
|
209
|
+
...scope,
|
|
210
|
+
expectedRevision: 0,
|
|
211
|
+
observations: args.observations,
|
|
212
|
+
expectedClaims: [],
|
|
213
|
+
payouts: [
|
|
214
|
+
{
|
|
215
|
+
payout: {
|
|
216
|
+
...args.payout,
|
|
217
|
+
stripePayoutId: "po_first",
|
|
218
|
+
amount: 600,
|
|
219
|
+
debitTransactionId: "txn_first",
|
|
220
|
+
},
|
|
221
|
+
allocations: [{ stripeTransferId: "tr_one", amount: 600 }],
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
payout: {
|
|
225
|
+
...args.payout,
|
|
226
|
+
stripePayoutId: "po_second",
|
|
227
|
+
amount: 400,
|
|
228
|
+
debitTransactionId: "txn_second",
|
|
229
|
+
},
|
|
230
|
+
allocations: [{ stripeTransferId: "tr_two", amount: 400 }],
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
};
|
|
234
|
+
expect(await t.mutation(batch, input)).toMatchObject({
|
|
235
|
+
claims: [{ amount: 600 }, { amount: 400 }],
|
|
236
|
+
});
|
|
237
|
+
await expect(t.mutation(batch, input)).rejects.toThrow(/snapshot|history/i);
|
|
238
|
+
expect(
|
|
239
|
+
await t.run((ctx) => ctx.db.query("payoutSourceAllocations").collect()),
|
|
240
|
+
).toMatchObject([{ paidAmount: 600 }, { paidAmount: 400 }]);
|
|
241
|
+
});
|
|
242
|
+
it("a bad later payout rolls back the entire historical batch", async () => {
|
|
243
|
+
const { t, args } = await setup();
|
|
244
|
+
const batch = makeFunctionReference<"mutation">(
|
|
245
|
+
"payoutRelease/mutations:adoptHistoricalBatch",
|
|
246
|
+
);
|
|
247
|
+
await expect(
|
|
248
|
+
t.mutation(batch, {
|
|
249
|
+
...scope,
|
|
250
|
+
expectedRevision: 0,
|
|
251
|
+
observations: args.observations,
|
|
252
|
+
expectedClaims: [],
|
|
253
|
+
payouts: [
|
|
254
|
+
{ payout: args.payout, allocations: args.allocations },
|
|
255
|
+
{
|
|
256
|
+
payout: {
|
|
257
|
+
...args.payout,
|
|
258
|
+
stripePayoutId: "po_bad",
|
|
259
|
+
debitTransactionId: "txn_bad",
|
|
260
|
+
},
|
|
261
|
+
allocations: [],
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
}),
|
|
265
|
+
).rejects.toThrow();
|
|
266
|
+
expect(
|
|
267
|
+
await t.run((ctx) => ctx.db.query("payoutReleaseClaims").collect()),
|
|
268
|
+
).toEqual([]);
|
|
269
|
+
expect(
|
|
270
|
+
await t.run((ctx) => ctx.db.query("payoutSourceAllocations").collect()),
|
|
271
|
+
).toEqual([]);
|
|
272
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
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 { transferFingerprint } from "./payoutRelease/helpers.js";
|
|
7
|
+
const modules = import.meta.glob("./**/*.*s");
|
|
8
|
+
const m = (name: string) =>
|
|
9
|
+
makeFunctionReference<"mutation">(`payoutPolicy/mutations:${name}`);
|
|
10
|
+
const scope = { accountId: "acct_one", ownerKey: "owner" };
|
|
11
|
+
async function setup(country = "US") {
|
|
12
|
+
const t = convexTest(schema, modules);
|
|
13
|
+
await t.mutation(m("prepare"), {
|
|
14
|
+
...scope,
|
|
15
|
+
operationKey: "default",
|
|
16
|
+
expectedRevision: -1,
|
|
17
|
+
mode: "stripe_managed",
|
|
18
|
+
thresholds: [],
|
|
19
|
+
country,
|
|
20
|
+
observedSchedule: { interval: "weekly", weeklyDays: ["monday"] },
|
|
21
|
+
});
|
|
22
|
+
await t.mutation(m("confirm"), {
|
|
23
|
+
...scope,
|
|
24
|
+
operationKey: "default",
|
|
25
|
+
observedSchedule: { interval: "weekly", weeklyDays: ["monday"] },
|
|
26
|
+
});
|
|
27
|
+
const row = await t.run(async (ctx) => {
|
|
28
|
+
const id = await ctx.db.insert("transfers", {
|
|
29
|
+
stripeTransferId: "tr_one",
|
|
30
|
+
destinationAccountId: scope.accountId,
|
|
31
|
+
amount: 1000,
|
|
32
|
+
currency: "usd",
|
|
33
|
+
status: "paid",
|
|
34
|
+
role: "store",
|
|
35
|
+
});
|
|
36
|
+
return (await ctx.db.get("transfers", id))!;
|
|
37
|
+
});
|
|
38
|
+
const proof = {
|
|
39
|
+
observations: [
|
|
40
|
+
{
|
|
41
|
+
stripeTransferId: row.stripeTransferId,
|
|
42
|
+
currency: "usd",
|
|
43
|
+
matureNet: 1000,
|
|
44
|
+
pendingCredits: 0,
|
|
45
|
+
ledgerFingerprint: transferFingerprint(row),
|
|
46
|
+
observedAt: Date.now(),
|
|
47
|
+
transactionIds: ["txn_one"],
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
claims: [],
|
|
51
|
+
oldestHeldAt: Date.now() - 20 * 86400000,
|
|
52
|
+
};
|
|
53
|
+
const args = {
|
|
54
|
+
...scope,
|
|
55
|
+
operationKey: "custom",
|
|
56
|
+
expectedRevision: 0,
|
|
57
|
+
mode: "custom",
|
|
58
|
+
thresholds: [{ currency: "usd", amount: 1000 }],
|
|
59
|
+
country,
|
|
60
|
+
observedSchedule: { interval: "weekly", weeklyDays: ["monday"] },
|
|
61
|
+
adoption: proof,
|
|
62
|
+
};
|
|
63
|
+
return { t, args, proof, row };
|
|
64
|
+
}
|
|
65
|
+
it("anchors funded activation to the oldest held money rather than activation time", async () => {
|
|
66
|
+
const { t, args, proof } = await setup();
|
|
67
|
+
await t.mutation(m("prepare"), args);
|
|
68
|
+
const p = await t.run((ctx) => ctx.db.query("payoutPolicies").first());
|
|
69
|
+
expect(p!.holdDeadline).toBe(proof.oldestHeldAt + 730 * 86400000);
|
|
70
|
+
});
|
|
71
|
+
it("rejects already-expired holding periods before changing policy", async () => {
|
|
72
|
+
const { t, args } = await setup("TH");
|
|
73
|
+
await expect(t.mutation(m("prepare"), args)).rejects.toThrow(
|
|
74
|
+
/holding|deadline/i,
|
|
75
|
+
);
|
|
76
|
+
expect(
|
|
77
|
+
await t.run((ctx) => ctx.db.query("payoutPolicies").first()),
|
|
78
|
+
).toMatchObject({ mode: "stripe_managed", status: "ready", revision: 0 });
|
|
79
|
+
});
|
|
80
|
+
it("requires fresh adoption proof before a delayed provider attempt", async () => {
|
|
81
|
+
const { t, args } = await setup();
|
|
82
|
+
await t.mutation(m("prepare"), args);
|
|
83
|
+
await expect(
|
|
84
|
+
t.mutation(m("beginAttempt"), { ...scope, operationKey: "custom" }),
|
|
85
|
+
).rejects.toThrow(/adoption/i);
|
|
86
|
+
});
|
|
87
|
+
it("rejects changed source state before marking the provider attempt", async () => {
|
|
88
|
+
const { t, args, proof, row } = await setup();
|
|
89
|
+
await t.mutation(m("prepare"), args);
|
|
90
|
+
await t.run((ctx) =>
|
|
91
|
+
ctx.db.patch("transfers", row._id, { reversedAmount: 10 }),
|
|
92
|
+
);
|
|
93
|
+
await expect(
|
|
94
|
+
t.mutation(m("beginAttempt"), {
|
|
95
|
+
...scope,
|
|
96
|
+
operationKey: "custom",
|
|
97
|
+
adoption: proof,
|
|
98
|
+
}),
|
|
99
|
+
).rejects.toThrow(/snapshot/i);
|
|
100
|
+
expect(
|
|
101
|
+
await t.run((ctx) =>
|
|
102
|
+
ctx.db
|
|
103
|
+
.query("payoutPolicyOperations")
|
|
104
|
+
.withIndex("by_operation", (q) => q.eq("operationKey", "custom"))
|
|
105
|
+
.unique(),
|
|
106
|
+
),
|
|
107
|
+
).not.toHaveProperty("attempted");
|
|
108
|
+
});
|
|
109
|
+
it("fresh checks cannot extend the stored holding deadline", async () => {
|
|
110
|
+
const { t, args, proof } = await setup();
|
|
111
|
+
await t.mutation(m("prepare"), args);
|
|
112
|
+
await t.mutation(m("beginAttempt"), {
|
|
113
|
+
...scope,
|
|
114
|
+
operationKey: "custom",
|
|
115
|
+
adoption: { ...proof, oldestHeldAt: Date.now() - 86400000 },
|
|
116
|
+
});
|
|
117
|
+
expect(
|
|
118
|
+
await t.run((ctx) => ctx.db.query("payoutPolicies").first()),
|
|
119
|
+
).toMatchObject({ holdDeadline: proof.oldestHeldAt + 730 * 86400000 });
|
|
120
|
+
});
|
|
121
|
+
it("cannot confirm funded adoption without the fresh checked attempt marker", async () => {
|
|
122
|
+
const { t, args } = await setup();
|
|
123
|
+
await t.mutation(m("prepare"), args);
|
|
124
|
+
await expect(
|
|
125
|
+
t.mutation(m("confirm"), {
|
|
126
|
+
...scope,
|
|
127
|
+
operationKey: "custom",
|
|
128
|
+
observedSchedule: { interval: "manual" },
|
|
129
|
+
}),
|
|
130
|
+
).rejects.toThrow(/attempt/i);
|
|
131
|
+
});
|