@fantastic.dev/better-stripe 0.28.2 → 0.28.4
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 +12 -0
- package/README.md +5 -1
- package/dist/client/connect/payoutAdoptionTestHelpers.d.ts +96 -2
- package/dist/client/connect/payoutAdoptionTestHelpers.d.ts.map +1 -1
- package/dist/client/connect/payoutExecutionTestHelpers.d.ts +48 -1
- package/dist/client/connect/payoutExecutionTestHelpers.d.ts.map +1 -1
- package/dist/client/connect/payoutExecutionTestHelpers.js +1 -1
- package/dist/client/connect/payoutExecutionTestHelpers.js.map +1 -1
- package/dist/client/connect/payoutExpiry.d.ts +42 -0
- package/dist/client/connect/payoutExpiry.d.ts.map +1 -0
- package/dist/client/connect/payoutExpiry.js +93 -0
- package/dist/client/connect/payoutExpiry.js.map +1 -0
- package/dist/client/connect/payoutSyncTestHelpers.d.ts +38 -1
- package/dist/client/connect/payoutSyncTestHelpers.d.ts.map +1 -1
- package/dist/client/payoutPolicy.d.ts +18 -2
- package/dist/client/payoutPolicy.d.ts.map +1 -1
- package/dist/client/payoutPolicy.js +3 -25
- package/dist/client/payoutPolicy.js.map +1 -1
- package/dist/component/_generated/component.d.ts +118 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/core/resetSafety.d.ts.map +1 -1
- package/dist/component/core/resetSafety.js +7 -1
- package/dist/component/core/resetSafety.js.map +1 -1
- package/dist/component/earnings/triggers.d.ts +22 -2
- package/dist/component/earnings/triggers.d.ts.map +1 -1
- package/dist/component/payoutPolicy/expiry.d.ts +78 -0
- package/dist/component/payoutPolicy/expiry.d.ts.map +1 -0
- package/dist/component/payoutPolicy/expiry.js +57 -0
- package/dist/component/payoutPolicy/expiry.js.map +1 -0
- package/dist/component/payoutPolicy/helpers.d.ts +10 -0
- package/dist/component/payoutPolicy/helpers.d.ts.map +1 -1
- package/dist/component/payoutPolicy/mutations.d.ts +77 -0
- package/dist/component/payoutPolicy/mutations.d.ts.map +1 -1
- package/dist/component/payoutPolicy/mutations.js +36 -3
- package/dist/component/payoutPolicy/mutations.js.map +1 -1
- package/dist/component/payoutPolicy/queries.d.ts +20 -0
- package/dist/component/payoutPolicy/queries.d.ts.map +1 -1
- package/dist/component/payoutPolicy/schema.d.ts +139 -2
- package/dist/component/payoutPolicy/schema.d.ts.map +1 -1
- package/dist/component/payoutPolicy/schema.js +12 -0
- package/dist/component/payoutPolicy/schema.js.map +1 -1
- package/dist/component/payoutRelease/deadline.d.ts +10 -0
- package/dist/component/payoutRelease/deadline.d.ts.map +1 -0
- package/dist/component/payoutRelease/deadline.js +20 -0
- package/dist/component/payoutRelease/deadline.js.map +1 -0
- package/dist/component/payoutRelease/helpers.d.ts +10 -0
- package/dist/component/payoutRelease/helpers.d.ts.map +1 -1
- package/dist/component/payoutRelease/planning.d.ts +1 -0
- package/dist/component/payoutRelease/planning.d.ts.map +1 -1
- package/dist/component/payoutRelease/planning.js +5 -2
- package/dist/component/payoutRelease/planning.js.map +1 -1
- package/dist/component/payoutRelease/preview.d.ts +7 -1
- package/dist/component/payoutRelease/preview.d.ts.map +1 -1
- package/dist/component/payoutRelease/preview.js +5 -1
- package/dist/component/payoutRelease/preview.js.map +1 -1
- package/dist/component/payoutRelease/queries.d.ts +2 -0
- package/dist/component/payoutRelease/queries.d.ts.map +1 -1
- package/dist/component/payoutWorker/mutations.d.ts.map +1 -1
- package/dist/component/payoutWorker/mutations.js +12 -4
- package/dist/component/payoutWorker/mutations.js.map +1 -1
- package/dist/component/payoutWorker/queries.d.ts +20 -0
- package/dist/component/payoutWorker/queries.d.ts.map +1 -1
- package/dist/component/schema.d.ts +38 -1
- package/dist/react/workflows/PayoutProgress.d.ts.map +1 -1
- package/dist/react/workflows/PayoutProgress.js +2 -2
- package/dist/react/workflows/PayoutProgress.js.map +1 -1
- package/dist/react/workflows/PayoutSettings.d.ts.map +1 -1
- package/dist/react/workflows/PayoutSettings.js +1 -1
- package/dist/react/workflows/PayoutSettings.js.map +1 -1
- package/dist/react/workflows/payout-progress-state.d.ts +11 -1
- package/dist/react/workflows/payout-progress-state.d.ts.map +1 -1
- package/dist/react/workflows/payout-progress-state.js +42 -7
- package/dist/react/workflows/payout-progress-state.js.map +1 -1
- package/docs/react-workflows.md +14 -0
- package/docs/settlement.md +50 -0
- package/package.json +1 -1
- package/src/client/connect/payoutDeadline.test.ts +238 -0
- package/src/client/connect/payoutExecutionTestHelpers.ts +1 -1
- package/src/client/connect/payoutExpiry.ts +132 -0
- package/src/client/payoutPolicy.test.ts +196 -0
- package/src/client/payoutPolicy.ts +9 -23
- package/src/component/_generated/api.ts +4 -0
- package/src/component/_generated/component.ts +145 -0
- package/src/component/core/resetSafety.ts +49 -33
- package/src/component/payout-reset-safety.test.ts +28 -0
- package/src/component/payoutPolicy/expiry.ts +80 -0
- package/src/component/payoutPolicy/mutations.ts +47 -2
- package/src/component/payoutPolicy/schema.ts +17 -0
- package/src/component/payoutRelease/deadline.ts +24 -0
- package/src/component/payoutRelease/planning.ts +5 -2
- package/src/component/payoutRelease/preview.ts +5 -0
- package/src/component/payoutWorker/mutations.ts +20 -4
- package/src/react/workflows/PayoutProgress.test.tsx +65 -0
- package/src/react/workflows/PayoutProgress.tsx +25 -1
- package/src/react/workflows/PayoutSettings.tsx +4 -1
- package/src/react/workflows/payout-progress-state.ts +60 -8
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type Stripe from "stripe";
|
|
2
|
+
import type { Infer } from "convex/values";
|
|
3
|
+
import type { Component, RunCtx } from "../helpers.js";
|
|
4
|
+
import { runMutationOrThrow } from "../helpers.js";
|
|
5
|
+
import { componentRef } from "../webhooks/helpers.js";
|
|
6
|
+
import type { policyDoc } from "../../component/payoutPolicy/schema.js";
|
|
7
|
+
type Scope = { accountId: string; ownerKey: string };
|
|
8
|
+
type Policy = Infer<typeof policyDoc>;
|
|
9
|
+
function expiryBalances(balance: Stripe.Balance) {
|
|
10
|
+
const values = new Map<
|
|
11
|
+
string,
|
|
12
|
+
{ currency: string; available: number; pending: number }
|
|
13
|
+
>();
|
|
14
|
+
for (const type of ["available", "pending"] as const) {
|
|
15
|
+
const seen = new Set<string>();
|
|
16
|
+
for (const row of balance[type]) {
|
|
17
|
+
if (
|
|
18
|
+
!/^[a-z]{3}$/.test(row.currency) ||
|
|
19
|
+
!Number.isSafeInteger(row.amount) ||
|
|
20
|
+
seen.has(row.currency)
|
|
21
|
+
)
|
|
22
|
+
throw new Error("Invalid expiry balance.");
|
|
23
|
+
seen.add(row.currency);
|
|
24
|
+
const value = values.get(row.currency) ?? {
|
|
25
|
+
currency: row.currency,
|
|
26
|
+
available: 0,
|
|
27
|
+
pending: 0,
|
|
28
|
+
};
|
|
29
|
+
value[type] = row.amount;
|
|
30
|
+
values.set(row.currency, value);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return [...values.values()].sort((a, b) =>
|
|
34
|
+
a.currency.localeCompare(b.currency),
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
/** Restore controls, then separately verify remaining balances and durable claims. Never creates a payout. */
|
|
38
|
+
export async function expirePayoutPolicy<
|
|
39
|
+
R extends { status: string },
|
|
40
|
+
P extends { status: string },
|
|
41
|
+
>(
|
|
42
|
+
stripe: Stripe,
|
|
43
|
+
component: Component,
|
|
44
|
+
ctx: RunCtx,
|
|
45
|
+
scope: Scope,
|
|
46
|
+
client: {
|
|
47
|
+
get(ctx: RunCtx, scope: Scope): Promise<Policy | null>;
|
|
48
|
+
apply(ctx: RunCtx, args: Scope & { operationKey: string }): Promise<R>;
|
|
49
|
+
prepare(
|
|
50
|
+
ctx: RunCtx,
|
|
51
|
+
args: Scope & {
|
|
52
|
+
operationKey: string;
|
|
53
|
+
expectedRevision: number;
|
|
54
|
+
mode: "stripe_managed";
|
|
55
|
+
thresholds: [];
|
|
56
|
+
},
|
|
57
|
+
): Promise<P>;
|
|
58
|
+
},
|
|
59
|
+
) {
|
|
60
|
+
scope = { accountId: scope.accountId, ownerKey: scope.ownerKey };
|
|
61
|
+
let policy = await client.get(ctx, scope);
|
|
62
|
+
if (!policy?.holdDeadline || policy.holdDeadline > Date.now())
|
|
63
|
+
return { status: "not_due" as const };
|
|
64
|
+
if (policy.mode === "custom")
|
|
65
|
+
policy = (await runMutationOrThrow(
|
|
66
|
+
ctx,
|
|
67
|
+
componentRef(component, "payoutPolicy/mutations/beginHoldingExpiry"),
|
|
68
|
+
scope,
|
|
69
|
+
)) as Policy;
|
|
70
|
+
if (policy.status === "changing") {
|
|
71
|
+
const result = await client.apply(ctx, {
|
|
72
|
+
...scope,
|
|
73
|
+
operationKey: policy.operationKey,
|
|
74
|
+
});
|
|
75
|
+
if (result.status !== "ready") return result;
|
|
76
|
+
policy = await client.get(ctx, scope);
|
|
77
|
+
}
|
|
78
|
+
if (!policy) return { status: "not_due" as const };
|
|
79
|
+
if (policy.mode === "custom") {
|
|
80
|
+
const operationKey = `bs:payout-expiry:${scope.accountId}:${policy.revision}`;
|
|
81
|
+
const prepared = await client.prepare(ctx, {
|
|
82
|
+
...scope,
|
|
83
|
+
operationKey,
|
|
84
|
+
expectedRevision: policy.revision,
|
|
85
|
+
mode: "stripe_managed",
|
|
86
|
+
thresholds: [],
|
|
87
|
+
});
|
|
88
|
+
if (prepared.status !== "prepared") return prepared;
|
|
89
|
+
const applied = await client.apply(ctx, { ...scope, operationKey });
|
|
90
|
+
if (applied.status !== "ready") return applied;
|
|
91
|
+
policy = await client.get(ctx, scope);
|
|
92
|
+
}
|
|
93
|
+
if (
|
|
94
|
+
!policy?.holdingExpiry ||
|
|
95
|
+
policy.mode !== "stripe_managed" ||
|
|
96
|
+
policy.status !== "ready"
|
|
97
|
+
)
|
|
98
|
+
return { status: "not_due" as const };
|
|
99
|
+
const unresolved = {
|
|
100
|
+
status: "expiry_unresolved" as const,
|
|
101
|
+
holdingExpiry: policy.holdingExpiry,
|
|
102
|
+
};
|
|
103
|
+
let balances;
|
|
104
|
+
const observedAt = Date.now();
|
|
105
|
+
try {
|
|
106
|
+
balances = expiryBalances(
|
|
107
|
+
await stripe.balance.retrieve(undefined, {
|
|
108
|
+
stripeAccount: scope.accountId,
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
} catch {
|
|
112
|
+
return unresolved;
|
|
113
|
+
}
|
|
114
|
+
if (Date.now() - observedAt > 30000) return unresolved;
|
|
115
|
+
const observed = (await runMutationOrThrow(
|
|
116
|
+
ctx,
|
|
117
|
+
componentRef(component, "payoutPolicy/mutations/observeHoldingExpiry"),
|
|
118
|
+
{
|
|
119
|
+
...scope,
|
|
120
|
+
expectedRevision: policy.revision,
|
|
121
|
+
deadline: policy.holdingExpiry.deadline,
|
|
122
|
+
observedAt,
|
|
123
|
+
balances,
|
|
124
|
+
},
|
|
125
|
+
)) as Policy;
|
|
126
|
+
if (observed.holdingExpiry!.status !== "no_held_balance")
|
|
127
|
+
return {
|
|
128
|
+
status: "expiry_unresolved" as const,
|
|
129
|
+
holdingExpiry: observed.holdingExpiry!,
|
|
130
|
+
};
|
|
131
|
+
return client.apply(ctx, { ...scope, operationKey: policy.operationKey });
|
|
132
|
+
}
|
|
@@ -381,3 +381,199 @@ it("requires adoption for historical payouts even when today's balance is zero",
|
|
|
381
381
|
expect(provider.balanceSettings.update).not.toHaveBeenCalled();
|
|
382
382
|
expect(await client.get(ctx, scope)).toBeNull();
|
|
383
383
|
});
|
|
384
|
+
|
|
385
|
+
async function expiredRecipient() {
|
|
386
|
+
const s = await setup();
|
|
387
|
+
await s.client.prepare(s.ctx, options);
|
|
388
|
+
await s.client.apply(s.ctx, { ...scope, operationKey: "enable" });
|
|
389
|
+
const deadline = Date.now() - 1;
|
|
390
|
+
await s.t.run(async (ctx) => {
|
|
391
|
+
const p = (await ctx.db.query("payoutPolicies").first())!;
|
|
392
|
+
await ctx.db.patch(p._id, { holdDeadline: deadline });
|
|
393
|
+
});
|
|
394
|
+
return { ...s, deadline };
|
|
395
|
+
}
|
|
396
|
+
it("keeps funded expiry discoverable after restoring weekly controls until fresh balances clear", async () => {
|
|
397
|
+
const s = await expiredRecipient();
|
|
398
|
+
s.provider.balance.retrieve.mockResolvedValue({
|
|
399
|
+
available: [{ currency: "usd", amount: 999 }],
|
|
400
|
+
pending: [],
|
|
401
|
+
});
|
|
402
|
+
expect(await s.client.expire(s.ctx, scope)).toMatchObject({
|
|
403
|
+
status: "expiry_unresolved",
|
|
404
|
+
});
|
|
405
|
+
expect(await s.client.get(s.ctx, scope)).toMatchObject({
|
|
406
|
+
mode: "stripe_managed",
|
|
407
|
+
status: "ready",
|
|
408
|
+
holdDeadline: s.deadline,
|
|
409
|
+
holdingExpiry: {
|
|
410
|
+
deadline: s.deadline,
|
|
411
|
+
status: "balance_remaining",
|
|
412
|
+
balances: [{ currency: "usd", available: 999, pending: 0 }],
|
|
413
|
+
},
|
|
414
|
+
});
|
|
415
|
+
expect(
|
|
416
|
+
(await s.client.due(s.ctx, { cursor: null, numItems: 10 })).page,
|
|
417
|
+
).toHaveLength(1);
|
|
418
|
+
await s.client.expire(s.ctx, scope);
|
|
419
|
+
expect(s.provider.balanceSettings.update).toHaveBeenCalledTimes(2);
|
|
420
|
+
s.provider.balance.retrieve.mockResolvedValue({
|
|
421
|
+
available: [{ currency: "usd", amount: 0 }],
|
|
422
|
+
pending: [],
|
|
423
|
+
});
|
|
424
|
+
expect(await s.client.expire(s.ctx, scope)).toMatchObject({
|
|
425
|
+
status: "ready",
|
|
426
|
+
});
|
|
427
|
+
expect((await s.client.get(s.ctx, scope))!.holdDeadline).toBeUndefined();
|
|
428
|
+
expect((await s.client.get(s.ctx, scope))!.holdingExpiry).toMatchObject({
|
|
429
|
+
status: "no_held_balance",
|
|
430
|
+
deadline: s.deadline,
|
|
431
|
+
});
|
|
432
|
+
expect(
|
|
433
|
+
(await s.client.due(s.ctx, { cursor: null, numItems: 10 })).page,
|
|
434
|
+
).toHaveLength(0);
|
|
435
|
+
});
|
|
436
|
+
it("restores controls even if expiry balance verification is unavailable, without fabricating zero", async () => {
|
|
437
|
+
const s = await expiredRecipient();
|
|
438
|
+
s.provider.balance.retrieve.mockRejectedValue(
|
|
439
|
+
new Error("provider unavailable"),
|
|
440
|
+
);
|
|
441
|
+
expect(await s.client.expire(s.ctx, scope)).toMatchObject({
|
|
442
|
+
status: "expiry_unresolved",
|
|
443
|
+
});
|
|
444
|
+
const p = await s.client.get(s.ctx, scope);
|
|
445
|
+
expect(p).toMatchObject({
|
|
446
|
+
mode: "stripe_managed",
|
|
447
|
+
holdDeadline: s.deadline,
|
|
448
|
+
holdingExpiry: { status: "unverified" },
|
|
449
|
+
});
|
|
450
|
+
expect(p!.holdingExpiry!.balances).toBeUndefined();
|
|
451
|
+
expect(
|
|
452
|
+
(await s.provider.balanceSettings.retrieve()).payments.payouts.schedule
|
|
453
|
+
.interval,
|
|
454
|
+
).toBe("weekly");
|
|
455
|
+
});
|
|
456
|
+
it("does not clear an unknown payout claim at expiry even when Stripe balance is zero", async () => {
|
|
457
|
+
const s = await expiredRecipient();
|
|
458
|
+
const id = await s.t.run((ctx) =>
|
|
459
|
+
ctx.db.insert("payoutReleaseClaims", {
|
|
460
|
+
...scope,
|
|
461
|
+
operationKey: "unknown-payout",
|
|
462
|
+
expectedRevision: 0,
|
|
463
|
+
amount: 999,
|
|
464
|
+
currency: "usd",
|
|
465
|
+
allocations: [],
|
|
466
|
+
observations: [],
|
|
467
|
+
status: "reserved",
|
|
468
|
+
attemptedAt: Date.now(),
|
|
469
|
+
}),
|
|
470
|
+
);
|
|
471
|
+
expect(await s.client.expire(s.ctx, scope)).toMatchObject({
|
|
472
|
+
status: "expiry_unresolved",
|
|
473
|
+
});
|
|
474
|
+
expect((await s.client.get(s.ctx, scope))!.holdingExpiry).toMatchObject({
|
|
475
|
+
status: "claims_unresolved",
|
|
476
|
+
});
|
|
477
|
+
expect(await s.t.run((ctx) => ctx.db.get(id))).toMatchObject({
|
|
478
|
+
status: "reserved",
|
|
479
|
+
amount: 999,
|
|
480
|
+
});
|
|
481
|
+
await expect(
|
|
482
|
+
s.client.prepare(s.ctx, {
|
|
483
|
+
...options,
|
|
484
|
+
expectedRevision: 1,
|
|
485
|
+
operationKey: "reactivate",
|
|
486
|
+
}),
|
|
487
|
+
).rejects.toThrow(/expiry.*recovery/i);
|
|
488
|
+
});
|
|
489
|
+
it("does not net currencies or pending funds when checking expiry recovery", async () => {
|
|
490
|
+
const s = await expiredRecipient();
|
|
491
|
+
s.provider.balance.retrieve.mockResolvedValue({
|
|
492
|
+
available: [
|
|
493
|
+
{ currency: "usd", amount: 100 },
|
|
494
|
+
{ currency: "eur", amount: -100 },
|
|
495
|
+
],
|
|
496
|
+
pending: [{ currency: "usd", amount: 25 }],
|
|
497
|
+
});
|
|
498
|
+
expect(await s.client.expire(s.ctx, scope)).toMatchObject({
|
|
499
|
+
status: "expiry_unresolved",
|
|
500
|
+
});
|
|
501
|
+
expect((await s.client.get(s.ctx, scope))!.holdingExpiry!.balances).toEqual([
|
|
502
|
+
{ currency: "eur", available: -100, pending: 0 },
|
|
503
|
+
{ currency: "usd", available: 100, pending: 25 },
|
|
504
|
+
]);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it("does not label a delayed expiry balance response as a fresh observation", async () => {
|
|
508
|
+
vi.useFakeTimers();
|
|
509
|
+
try {
|
|
510
|
+
const s = await expiredRecipient();
|
|
511
|
+
s.provider.balance.retrieve.mockImplementation(async () => {
|
|
512
|
+
vi.advanceTimersByTime(31000);
|
|
513
|
+
return { available: [{ currency: "usd", amount: 0 }], pending: [] };
|
|
514
|
+
});
|
|
515
|
+
expect(await s.client.expire(s.ctx, scope)).toMatchObject({
|
|
516
|
+
status: "expiry_unresolved",
|
|
517
|
+
});
|
|
518
|
+
expect((await s.client.get(s.ctx, scope))!.holdingExpiry).toMatchObject({
|
|
519
|
+
status: "unverified",
|
|
520
|
+
});
|
|
521
|
+
} finally {
|
|
522
|
+
vi.useRealTimers();
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it("retries a lost recovery write without losing its original deadline or restoring twice", async () => {
|
|
527
|
+
const s = await expiredRecipient();
|
|
528
|
+
const write = s.ctx.runMutation!;
|
|
529
|
+
s.ctx.runMutation = (async (ref: Record<symbol, string>, args: never) => {
|
|
530
|
+
if (ref[symbol].endsWith("/observeHoldingExpiry"))
|
|
531
|
+
throw new Error("recovery write failed");
|
|
532
|
+
return write(ref as never, args);
|
|
533
|
+
}) as never;
|
|
534
|
+
await expect(s.client.expire(s.ctx, scope)).rejects.toThrow(
|
|
535
|
+
/recovery write failed/,
|
|
536
|
+
);
|
|
537
|
+
expect(await s.client.get(s.ctx, scope)).toMatchObject({
|
|
538
|
+
mode: "stripe_managed",
|
|
539
|
+
holdDeadline: s.deadline,
|
|
540
|
+
holdingExpiry: { status: "unverified" },
|
|
541
|
+
});
|
|
542
|
+
s.ctx.runMutation = write;
|
|
543
|
+
expect(await s.client.expire(s.ctx, scope)).toMatchObject({
|
|
544
|
+
status: "ready",
|
|
545
|
+
});
|
|
546
|
+
expect(s.provider.balanceSettings.update).toHaveBeenCalledTimes(2);
|
|
547
|
+
});
|
|
548
|
+
it("concurrent expiry retries preserve one restoration identity and unresolved funds", async () => {
|
|
549
|
+
const s = await expiredRecipient();
|
|
550
|
+
s.provider.balance.retrieve.mockResolvedValue({
|
|
551
|
+
available: [{ currency: "usd", amount: 999 }],
|
|
552
|
+
pending: [],
|
|
553
|
+
});
|
|
554
|
+
const results = await Promise.allSettled([
|
|
555
|
+
s.client.expire(s.ctx, scope),
|
|
556
|
+
s.client.expire(s.ctx, scope),
|
|
557
|
+
]);
|
|
558
|
+
expect(results.some((r) => r.status === "fulfilled")).toBe(true);
|
|
559
|
+
expect(await s.client.expire(s.ctx, scope)).toMatchObject({
|
|
560
|
+
status: "expiry_unresolved",
|
|
561
|
+
});
|
|
562
|
+
expect(await s.client.get(s.ctx, scope)).toMatchObject({
|
|
563
|
+
mode: "stripe_managed",
|
|
564
|
+
holdDeadline: s.deadline,
|
|
565
|
+
holdingExpiry: { status: "balance_remaining" },
|
|
566
|
+
});
|
|
567
|
+
const restoreKeys = s.provider.balanceSettings.update.mock.calls
|
|
568
|
+
.slice(1)
|
|
569
|
+
.map((c) => (c[1] as { idempotencyKey: string }).idempotencyKey);
|
|
570
|
+
expect(new Set(restoreKeys).size).toBe(1);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it("accepts a due policy document as scope without forwarding its extra fields to mutations", async () => {
|
|
574
|
+
const s = await expiredRecipient();
|
|
575
|
+
const policy = (await s.client.get(s.ctx, scope))!;
|
|
576
|
+
expect(await s.client.expire(s.ctx, policy)).toMatchObject({
|
|
577
|
+
status: "ready",
|
|
578
|
+
});
|
|
579
|
+
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { holdingExpiry as payoutHoldingExpiryValidator } from "../component/payoutPolicy/schema.js";
|
|
2
|
+
import { expirePayoutPolicy } from "./connect/payoutExpiry.js";
|
|
1
3
|
import {
|
|
2
4
|
previewRecipientPayout,
|
|
3
5
|
type PayoutPreviewOptions,
|
|
@@ -570,28 +572,12 @@ export class PayoutPolicyClient {
|
|
|
570
572
|
}
|
|
571
573
|
/** A host cron must page due() and call expire(); payout execution is a separate boundary. */
|
|
572
574
|
async expire(ctx: RunCtx, scope: PayoutPolicyScope) {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
});
|
|
581
|
-
if (result.status !== "ready") return result;
|
|
582
|
-
policy = await this.get(ctx, scope);
|
|
583
|
-
}
|
|
584
|
-
if (!policy || policy.mode !== "custom")
|
|
585
|
-
return { status: "not_due" as const };
|
|
586
|
-
const operationKey = `bs:payout-expiry:${scope.accountId}:${policy.revision}`;
|
|
587
|
-
const prepared = await this.prepare(ctx, {
|
|
588
|
-
...scope,
|
|
589
|
-
operationKey,
|
|
590
|
-
expectedRevision: policy.revision,
|
|
591
|
-
mode: "stripe_managed",
|
|
592
|
-
thresholds: [],
|
|
593
|
-
});
|
|
594
|
-
if (prepared.status !== "prepared") return prepared;
|
|
595
|
-
return this.apply(ctx, { ...scope, operationKey });
|
|
575
|
+
return expirePayoutPolicy(
|
|
576
|
+
this.provider(),
|
|
577
|
+
this.component,
|
|
578
|
+
ctx,
|
|
579
|
+
scope,
|
|
580
|
+
this,
|
|
581
|
+
);
|
|
596
582
|
}
|
|
597
583
|
}
|
|
@@ -47,12 +47,14 @@ import type * as lib_paymentSeller from "../lib/paymentSeller.js";
|
|
|
47
47
|
import type * as lib_reversals from "../lib/reversals.js";
|
|
48
48
|
import type * as lib_stripeFactory from "../lib/stripeFactory.js";
|
|
49
49
|
import type * as payoutPolicy_adoption from "../payoutPolicy/adoption.js";
|
|
50
|
+
import type * as payoutPolicy_expiry from "../payoutPolicy/expiry.js";
|
|
50
51
|
import type * as payoutPolicy_helpers from "../payoutPolicy/helpers.js";
|
|
51
52
|
import type * as payoutPolicy_mutations from "../payoutPolicy/mutations.js";
|
|
52
53
|
import type * as payoutPolicy_queries from "../payoutPolicy/queries.js";
|
|
53
54
|
import type * as payoutRelease_adoption from "../payoutRelease/adoption.js";
|
|
54
55
|
import type * as payoutRelease_allocation from "../payoutRelease/allocation.js";
|
|
55
56
|
import type * as payoutRelease_allocationTotals from "../payoutRelease/allocationTotals.js";
|
|
57
|
+
import type * as payoutRelease_deadline from "../payoutRelease/deadline.js";
|
|
56
58
|
import type * as payoutRelease_fees from "../payoutRelease/fees.js";
|
|
57
59
|
import type * as payoutRelease_helpers from "../payoutRelease/helpers.js";
|
|
58
60
|
import type * as payoutRelease_mutations from "../payoutRelease/mutations.js";
|
|
@@ -131,12 +133,14 @@ const fullApi: ApiFromModules<{
|
|
|
131
133
|
"lib/reversals": typeof lib_reversals;
|
|
132
134
|
"lib/stripeFactory": typeof lib_stripeFactory;
|
|
133
135
|
"payoutPolicy/adoption": typeof payoutPolicy_adoption;
|
|
136
|
+
"payoutPolicy/expiry": typeof payoutPolicy_expiry;
|
|
134
137
|
"payoutPolicy/helpers": typeof payoutPolicy_helpers;
|
|
135
138
|
"payoutPolicy/mutations": typeof payoutPolicy_mutations;
|
|
136
139
|
"payoutPolicy/queries": typeof payoutPolicy_queries;
|
|
137
140
|
"payoutRelease/adoption": typeof payoutRelease_adoption;
|
|
138
141
|
"payoutRelease/allocation": typeof payoutRelease_allocation;
|
|
139
142
|
"payoutRelease/allocationTotals": typeof payoutRelease_allocationTotals;
|
|
143
|
+
"payoutRelease/deadline": typeof payoutRelease_deadline;
|
|
140
144
|
"payoutRelease/fees": typeof payoutRelease_fees;
|
|
141
145
|
"payoutRelease/helpers": typeof payoutRelease_helpers;
|
|
142
146
|
"payoutRelease/mutations": typeof payoutRelease_mutations;
|
|
@@ -2790,6 +2790,44 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
2790
2790
|
},
|
|
2791
2791
|
Name
|
|
2792
2792
|
>;
|
|
2793
|
+
beginHoldingExpiry: FunctionReference<
|
|
2794
|
+
"mutation",
|
|
2795
|
+
"internal",
|
|
2796
|
+
{ accountId: string; ownerKey: string },
|
|
2797
|
+
{
|
|
2798
|
+
_creationTime: number;
|
|
2799
|
+
_id: string;
|
|
2800
|
+
accountId: string;
|
|
2801
|
+
country: string;
|
|
2802
|
+
holdDeadline?: number;
|
|
2803
|
+
holdingExpiry?: {
|
|
2804
|
+
balances?: Array<{
|
|
2805
|
+
available: number;
|
|
2806
|
+
currency: string;
|
|
2807
|
+
pending: number;
|
|
2808
|
+
}>;
|
|
2809
|
+
deadline: number;
|
|
2810
|
+
observedAt?: number;
|
|
2811
|
+
status:
|
|
2812
|
+
| "unverified"
|
|
2813
|
+
| "balance_remaining"
|
|
2814
|
+
| "claims_unresolved"
|
|
2815
|
+
| "no_held_balance";
|
|
2816
|
+
};
|
|
2817
|
+
mode: "stripe_managed" | "custom";
|
|
2818
|
+
operationKey: string;
|
|
2819
|
+
ownerKey: string;
|
|
2820
|
+
previousSchedule?: {
|
|
2821
|
+
interval: "daily" | "weekly" | "monthly" | "manual";
|
|
2822
|
+
monthlyDays?: Array<number>;
|
|
2823
|
+
weeklyDays?: Array<string>;
|
|
2824
|
+
};
|
|
2825
|
+
revision: number;
|
|
2826
|
+
status: "changing" | "ready";
|
|
2827
|
+
thresholds: Array<{ amount: number; currency: string }>;
|
|
2828
|
+
},
|
|
2829
|
+
Name
|
|
2830
|
+
>;
|
|
2793
2831
|
cancelUnattempted: FunctionReference<
|
|
2794
2832
|
"mutation",
|
|
2795
2833
|
"internal",
|
|
@@ -2906,6 +2944,55 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
2906
2944
|
},
|
|
2907
2945
|
Name
|
|
2908
2946
|
>;
|
|
2947
|
+
observeHoldingExpiry: FunctionReference<
|
|
2948
|
+
"mutation",
|
|
2949
|
+
"internal",
|
|
2950
|
+
{
|
|
2951
|
+
accountId: string;
|
|
2952
|
+
balances: Array<{
|
|
2953
|
+
available: number;
|
|
2954
|
+
currency: string;
|
|
2955
|
+
pending: number;
|
|
2956
|
+
}>;
|
|
2957
|
+
deadline: number;
|
|
2958
|
+
expectedRevision: number;
|
|
2959
|
+
observedAt: number;
|
|
2960
|
+
ownerKey: string;
|
|
2961
|
+
},
|
|
2962
|
+
{
|
|
2963
|
+
_creationTime: number;
|
|
2964
|
+
_id: string;
|
|
2965
|
+
accountId: string;
|
|
2966
|
+
country: string;
|
|
2967
|
+
holdDeadline?: number;
|
|
2968
|
+
holdingExpiry?: {
|
|
2969
|
+
balances?: Array<{
|
|
2970
|
+
available: number;
|
|
2971
|
+
currency: string;
|
|
2972
|
+
pending: number;
|
|
2973
|
+
}>;
|
|
2974
|
+
deadline: number;
|
|
2975
|
+
observedAt?: number;
|
|
2976
|
+
status:
|
|
2977
|
+
| "unverified"
|
|
2978
|
+
| "balance_remaining"
|
|
2979
|
+
| "claims_unresolved"
|
|
2980
|
+
| "no_held_balance";
|
|
2981
|
+
};
|
|
2982
|
+
mode: "stripe_managed" | "custom";
|
|
2983
|
+
operationKey: string;
|
|
2984
|
+
ownerKey: string;
|
|
2985
|
+
previousSchedule?: {
|
|
2986
|
+
interval: "daily" | "weekly" | "monthly" | "manual";
|
|
2987
|
+
monthlyDays?: Array<number>;
|
|
2988
|
+
weeklyDays?: Array<string>;
|
|
2989
|
+
};
|
|
2990
|
+
revision: number;
|
|
2991
|
+
status: "changing" | "ready";
|
|
2992
|
+
thresholds: Array<{ amount: number; currency: string }>;
|
|
2993
|
+
},
|
|
2994
|
+
Name
|
|
2995
|
+
>;
|
|
2909
2996
|
prepare: FunctionReference<
|
|
2910
2997
|
"mutation",
|
|
2911
2998
|
"internal",
|
|
@@ -3007,6 +3094,20 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
3007
3094
|
accountId: string;
|
|
3008
3095
|
country: string;
|
|
3009
3096
|
holdDeadline?: number;
|
|
3097
|
+
holdingExpiry?: {
|
|
3098
|
+
balances?: Array<{
|
|
3099
|
+
available: number;
|
|
3100
|
+
currency: string;
|
|
3101
|
+
pending: number;
|
|
3102
|
+
}>;
|
|
3103
|
+
deadline: number;
|
|
3104
|
+
observedAt?: number;
|
|
3105
|
+
status:
|
|
3106
|
+
| "unverified"
|
|
3107
|
+
| "balance_remaining"
|
|
3108
|
+
| "claims_unresolved"
|
|
3109
|
+
| "no_held_balance";
|
|
3110
|
+
};
|
|
3010
3111
|
mode: "stripe_managed" | "custom";
|
|
3011
3112
|
operationKey: string;
|
|
3012
3113
|
ownerKey: string;
|
|
@@ -3032,6 +3133,20 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
3032
3133
|
accountId: string;
|
|
3033
3134
|
country: string;
|
|
3034
3135
|
holdDeadline?: number;
|
|
3136
|
+
holdingExpiry?: {
|
|
3137
|
+
balances?: Array<{
|
|
3138
|
+
available: number;
|
|
3139
|
+
currency: string;
|
|
3140
|
+
pending: number;
|
|
3141
|
+
}>;
|
|
3142
|
+
deadline: number;
|
|
3143
|
+
observedAt?: number;
|
|
3144
|
+
status:
|
|
3145
|
+
| "unverified"
|
|
3146
|
+
| "balance_remaining"
|
|
3147
|
+
| "claims_unresolved"
|
|
3148
|
+
| "no_held_balance";
|
|
3149
|
+
};
|
|
3035
3150
|
mode: "stripe_managed" | "custom";
|
|
3036
3151
|
operationKey: string;
|
|
3037
3152
|
ownerKey: string;
|
|
@@ -3639,8 +3754,10 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
3639
3754
|
amount: number;
|
|
3640
3755
|
confirmedFees: number;
|
|
3641
3756
|
currency: string;
|
|
3757
|
+
holdDeadline?: number;
|
|
3642
3758
|
independentLiabilities: number;
|
|
3643
3759
|
matureUnpaid: number;
|
|
3760
|
+
minimumWaived?: boolean;
|
|
3644
3761
|
observedAt: number;
|
|
3645
3762
|
pendingCredits: number;
|
|
3646
3763
|
providerAvailable: number;
|
|
@@ -4061,6 +4178,20 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
4061
4178
|
accountId: string;
|
|
4062
4179
|
country: string;
|
|
4063
4180
|
holdDeadline?: number;
|
|
4181
|
+
holdingExpiry?: {
|
|
4182
|
+
balances?: Array<{
|
|
4183
|
+
available: number;
|
|
4184
|
+
currency: string;
|
|
4185
|
+
pending: number;
|
|
4186
|
+
}>;
|
|
4187
|
+
deadline: number;
|
|
4188
|
+
observedAt?: number;
|
|
4189
|
+
status:
|
|
4190
|
+
| "unverified"
|
|
4191
|
+
| "balance_remaining"
|
|
4192
|
+
| "claims_unresolved"
|
|
4193
|
+
| "no_held_balance";
|
|
4194
|
+
};
|
|
4064
4195
|
mode: "stripe_managed" | "custom";
|
|
4065
4196
|
operationKey: string;
|
|
4066
4197
|
ownerKey: string;
|
|
@@ -4122,6 +4253,20 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
4122
4253
|
accountId: string;
|
|
4123
4254
|
country: string;
|
|
4124
4255
|
holdDeadline?: number;
|
|
4256
|
+
holdingExpiry?: {
|
|
4257
|
+
balances?: Array<{
|
|
4258
|
+
available: number;
|
|
4259
|
+
currency: string;
|
|
4260
|
+
pending: number;
|
|
4261
|
+
}>;
|
|
4262
|
+
deadline: number;
|
|
4263
|
+
observedAt?: number;
|
|
4264
|
+
status:
|
|
4265
|
+
| "unverified"
|
|
4266
|
+
| "balance_remaining"
|
|
4267
|
+
| "claims_unresolved"
|
|
4268
|
+
| "no_held_balance";
|
|
4269
|
+
};
|
|
4125
4270
|
mode: "stripe_managed" | "custom";
|
|
4126
4271
|
operationKey: string;
|
|
4127
4272
|
ownerKey: string;
|