@fantastic.dev/better-stripe 0.28.3 → 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 +6 -0
- package/README.md +3 -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/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 +116 -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/helpers.d.ts +10 -0
- package/dist/component/payoutRelease/helpers.d.ts.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/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 +26 -0
- package/dist/react/workflows/payout-progress-state.js.map +1 -1
- package/docs/react-workflows.md +11 -0
- package/docs/settlement.md +31 -3
- package/package.json +1 -1
- package/src/client/connect/payoutDeadline.test.ts +70 -0
- 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 +2 -0
- package/src/component/_generated/component.ts +143 -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/react/workflows/PayoutProgress.test.tsx +42 -0
- package/src/react/workflows/PayoutProgress.tsx +25 -1
- package/src/react/workflows/payout-progress-state.ts +41 -1
|
@@ -1,39 +1,51 @@
|
|
|
1
1
|
import type { MutationCtx } from "../_generated/server.js";
|
|
2
2
|
/** A database reset cannot erase the only restoration or unresolved-payout record. */
|
|
3
3
|
export async function assertPayoutResetSafe(ctx: MutationCtx) {
|
|
4
|
-
const [
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
4
|
+
const [
|
|
5
|
+
bound,
|
|
6
|
+
custom,
|
|
7
|
+
changing,
|
|
8
|
+
reserved,
|
|
9
|
+
submitted,
|
|
10
|
+
pending,
|
|
11
|
+
transit,
|
|
12
|
+
deadline,
|
|
13
|
+
] = await Promise.all([
|
|
14
|
+
ctx.db
|
|
15
|
+
.query("payoutSnapshots")
|
|
16
|
+
.withIndex("by_read_only", (q) => q.eq("readOnly", false))
|
|
17
|
+
.first(),
|
|
18
|
+
ctx.db
|
|
19
|
+
.query("payoutPolicies")
|
|
20
|
+
.withIndex("by_mode_status", (q) => q.eq("mode", "custom"))
|
|
21
|
+
.first(),
|
|
22
|
+
ctx.db
|
|
23
|
+
.query("payoutPolicies")
|
|
24
|
+
.withIndex("by_mode_status", (q) =>
|
|
25
|
+
q.eq("mode", "stripe_managed").eq("status", "changing"),
|
|
26
|
+
)
|
|
27
|
+
.first(),
|
|
28
|
+
ctx.db
|
|
29
|
+
.query("payoutReleaseClaims")
|
|
30
|
+
.withIndex("by_status", (q) => q.eq("status", "reserved"))
|
|
31
|
+
.first(),
|
|
32
|
+
ctx.db
|
|
33
|
+
.query("payoutReleaseClaims")
|
|
34
|
+
.withIndex("by_status", (q) => q.eq("status", "submitted"))
|
|
35
|
+
.first(),
|
|
36
|
+
ctx.db
|
|
37
|
+
.query("payouts")
|
|
38
|
+
.withIndex("by_status", (q) => q.eq("status", "pending"))
|
|
39
|
+
.first(),
|
|
40
|
+
ctx.db
|
|
41
|
+
.query("payouts")
|
|
42
|
+
.withIndex("by_status", (q) => q.eq("status", "in_transit"))
|
|
43
|
+
.first(),
|
|
44
|
+
ctx.db
|
|
45
|
+
.query("payoutPolicies")
|
|
46
|
+
.withIndex("by_deadline", (q) => q.gt("holdDeadline", 0))
|
|
47
|
+
.first(),
|
|
48
|
+
]);
|
|
37
49
|
if (bound)
|
|
38
50
|
throw new Error(
|
|
39
51
|
"A financially bound payout collection must be resolved before reset.",
|
|
@@ -42,6 +54,10 @@ export async function assertPayoutResetSafe(ctx: MutationCtx) {
|
|
|
42
54
|
throw new Error(
|
|
43
55
|
"Restore and verify payout schedules before reset; restoration records cannot be erased.",
|
|
44
56
|
);
|
|
57
|
+
if (deadline)
|
|
58
|
+
throw new Error(
|
|
59
|
+
"Resolve holding expiry recovery before reset; its deadline and balance record cannot be erased.",
|
|
60
|
+
);
|
|
45
61
|
if (reserved || submitted)
|
|
46
62
|
throw new Error(
|
|
47
63
|
"Resolve payout claims before reset; unresolved reservations cannot be erased.",
|
|
@@ -146,3 +146,31 @@ it.each(["pending", "unknown"] as const)(
|
|
|
146
146
|
).toHaveLength(1);
|
|
147
147
|
},
|
|
148
148
|
);
|
|
149
|
+
|
|
150
|
+
it("retains an unresolved holding-expiry record after automatic scheduling was restored", async () => {
|
|
151
|
+
const t = convexTest(schema, modules);
|
|
152
|
+
await t.run((ctx) =>
|
|
153
|
+
ctx.db.insert("payoutPolicies", {
|
|
154
|
+
...scope,
|
|
155
|
+
revision: 1,
|
|
156
|
+
mode: "stripe_managed",
|
|
157
|
+
status: "ready",
|
|
158
|
+
operationKey: "restore",
|
|
159
|
+
country: "US",
|
|
160
|
+
thresholds: [],
|
|
161
|
+
holdDeadline: Date.now() - 1,
|
|
162
|
+
holdingExpiry: {
|
|
163
|
+
deadline: Date.now() - 1,
|
|
164
|
+
status: "balance_remaining",
|
|
165
|
+
balances: [{ currency: "usd", available: 999, pending: 0 }],
|
|
166
|
+
observedAt: Date.now(),
|
|
167
|
+
},
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
await expect(t.mutation(reset, {})).rejects.toThrow(
|
|
171
|
+
/holding expiry.*recovery/i,
|
|
172
|
+
);
|
|
173
|
+
expect(
|
|
174
|
+
await t.run((ctx) => ctx.db.query("payoutPolicies").first()),
|
|
175
|
+
).not.toBeNull();
|
|
176
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { MutationCtx } from "../_generated/server.js";
|
|
2
|
+
import { readPolicy } from "./helpers.js";
|
|
3
|
+
type Scope = { accountId: string; ownerKey: string };
|
|
4
|
+
export async function beginHoldingExpiry(ctx: MutationCtx, args: Scope) {
|
|
5
|
+
const p = await readPolicy(ctx, args);
|
|
6
|
+
if (
|
|
7
|
+
!p ||
|
|
8
|
+
p.mode !== "custom" ||
|
|
9
|
+
!p.holdDeadline ||
|
|
10
|
+
p.holdDeadline > Date.now()
|
|
11
|
+
)
|
|
12
|
+
throw new Error("A due custom holding deadline is required.");
|
|
13
|
+
if (p.holdingExpiry?.deadline === p.holdDeadline) return p;
|
|
14
|
+
if (p.holdingExpiry && p.holdingExpiry.status !== "no_held_balance")
|
|
15
|
+
throw new Error("Previous holding expiry recovery is unresolved.");
|
|
16
|
+
await ctx.db.patch("payoutPolicies", p._id, {
|
|
17
|
+
holdingExpiry: { deadline: p.holdDeadline, status: "unverified" },
|
|
18
|
+
});
|
|
19
|
+
return (await ctx.db.get("payoutPolicies", p._id))!;
|
|
20
|
+
}
|
|
21
|
+
export async function observeHoldingExpiry(
|
|
22
|
+
ctx: MutationCtx,
|
|
23
|
+
args: Scope & {
|
|
24
|
+
expectedRevision: number;
|
|
25
|
+
deadline: number;
|
|
26
|
+
observedAt: number;
|
|
27
|
+
balances: { currency: string; available: number; pending: number }[];
|
|
28
|
+
},
|
|
29
|
+
) {
|
|
30
|
+
const p = await readPolicy(ctx, args);
|
|
31
|
+
if (
|
|
32
|
+
!p ||
|
|
33
|
+
p.revision !== args.expectedRevision ||
|
|
34
|
+
p.mode !== "stripe_managed" ||
|
|
35
|
+
p.status !== "ready" ||
|
|
36
|
+
p.holdingExpiry?.deadline !== args.deadline ||
|
|
37
|
+
p.holdDeadline !== args.deadline
|
|
38
|
+
)
|
|
39
|
+
throw new Error("Holding expiry recovery changed.");
|
|
40
|
+
if (
|
|
41
|
+
!Number.isSafeInteger(args.observedAt) ||
|
|
42
|
+
args.observedAt > Date.now() ||
|
|
43
|
+
args.observedAt < Date.now() - 30000 ||
|
|
44
|
+
new Set(args.balances.map((b) => b.currency)).size !==
|
|
45
|
+
args.balances.length ||
|
|
46
|
+
args.balances.some(
|
|
47
|
+
(b) =>
|
|
48
|
+
!/^[a-z]{3}$/.test(b.currency) ||
|
|
49
|
+
!Number.isSafeInteger(b.available) ||
|
|
50
|
+
!Number.isSafeInteger(b.pending),
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
throw new Error("Invalid or stale holding expiry balance observation.");
|
|
54
|
+
const unresolved = await Promise.all(
|
|
55
|
+
(["reserved", "submitted"] as const).map((status) =>
|
|
56
|
+
ctx.db
|
|
57
|
+
.query("payoutReleaseClaims")
|
|
58
|
+
.withIndex("by_account_status", (q) =>
|
|
59
|
+
q.eq("accountId", args.accountId).eq("status", status),
|
|
60
|
+
)
|
|
61
|
+
.first(),
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
const status = unresolved.some(Boolean)
|
|
65
|
+
? ("claims_unresolved" as const)
|
|
66
|
+
: args.balances.some((b) => b.available !== 0 || b.pending !== 0)
|
|
67
|
+
? ("balance_remaining" as const)
|
|
68
|
+
: ("no_held_balance" as const);
|
|
69
|
+
await ctx.db.patch("payoutPolicies", p._id, {
|
|
70
|
+
holdingExpiry: {
|
|
71
|
+
deadline: args.deadline,
|
|
72
|
+
observedAt: args.observedAt,
|
|
73
|
+
balances: args.balances,
|
|
74
|
+
status,
|
|
75
|
+
},
|
|
76
|
+
// Retain due discovery until both provider balances and durable claims clear.
|
|
77
|
+
holdDeadline: status === "no_held_balance" ? undefined : args.deadline,
|
|
78
|
+
});
|
|
79
|
+
return (await ctx.db.get("payoutPolicies", p._id))!;
|
|
80
|
+
}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
beginHoldingExpiry as beginExpiry,
|
|
3
|
+
observeHoldingExpiry as observeExpiry,
|
|
4
|
+
} from "./expiry.js";
|
|
1
5
|
import { validateAdoption } from "./adoption.js";
|
|
2
6
|
import { v } from "convex/values";
|
|
3
7
|
import { mutation } from "../_generated/server.js";
|
|
4
8
|
import {
|
|
5
9
|
adoptionProof,
|
|
10
|
+
expiryBalance,
|
|
11
|
+
policyDoc,
|
|
6
12
|
prepareFields,
|
|
7
13
|
operationDoc,
|
|
8
14
|
scopeFields,
|
|
@@ -69,6 +75,14 @@ export const prepare = mutation({
|
|
|
69
75
|
)
|
|
70
76
|
throw new Error("Payout policy observation is stale or in the future.");
|
|
71
77
|
const policy = await readPolicy(ctx, args);
|
|
78
|
+
if (
|
|
79
|
+
args.mode === "custom" &&
|
|
80
|
+
policy?.holdingExpiry &&
|
|
81
|
+
policy.holdingExpiry.status !== "no_held_balance"
|
|
82
|
+
)
|
|
83
|
+
throw new Error(
|
|
84
|
+
"Holding expiry requires recovery before custom activation.",
|
|
85
|
+
);
|
|
72
86
|
if (policy?.status === "changing")
|
|
73
87
|
throw new Error("A payout policy operation is pending.");
|
|
74
88
|
if ((policy?.revision ?? -1) !== args.expectedRevision)
|
|
@@ -177,7 +191,14 @@ export const confirm = mutation({
|
|
|
177
191
|
thresholds: op.thresholds,
|
|
178
192
|
status: "ready",
|
|
179
193
|
...(op.mode === "stripe_managed"
|
|
180
|
-
? {
|
|
194
|
+
? {
|
|
195
|
+
previousSchedule: undefined,
|
|
196
|
+
holdDeadline:
|
|
197
|
+
policy.holdingExpiry &&
|
|
198
|
+
policy.holdingExpiry.status !== "no_held_balance"
|
|
199
|
+
? policy.holdDeadline
|
|
200
|
+
: undefined,
|
|
201
|
+
}
|
|
181
202
|
: {}),
|
|
182
203
|
});
|
|
183
204
|
return (await ctx.db.get("payoutPolicyOperations", op._id))!;
|
|
@@ -263,9 +284,33 @@ export const cancelUnattempted = mutation({
|
|
|
263
284
|
await ctx.db.patch("payoutPolicies", policy._id, {
|
|
264
285
|
status: "ready",
|
|
265
286
|
...(policy.mode === "stripe_managed"
|
|
266
|
-
? {
|
|
287
|
+
? {
|
|
288
|
+
previousSchedule: undefined,
|
|
289
|
+
holdDeadline:
|
|
290
|
+
policy.holdingExpiry &&
|
|
291
|
+
policy.holdingExpiry.status !== "no_held_balance"
|
|
292
|
+
? policy.holdDeadline
|
|
293
|
+
: undefined,
|
|
294
|
+
}
|
|
267
295
|
: {}),
|
|
268
296
|
});
|
|
269
297
|
return (await ctx.db.get("payoutPolicyOperations", op._id))!;
|
|
270
298
|
},
|
|
271
299
|
});
|
|
300
|
+
|
|
301
|
+
export const beginHoldingExpiry = mutation({
|
|
302
|
+
args: scopeFields,
|
|
303
|
+
returns: policyDoc,
|
|
304
|
+
handler: beginExpiry,
|
|
305
|
+
});
|
|
306
|
+
export const observeHoldingExpiry = mutation({
|
|
307
|
+
args: {
|
|
308
|
+
...scopeFields,
|
|
309
|
+
expectedRevision: v.number(),
|
|
310
|
+
deadline: v.number(),
|
|
311
|
+
observedAt: v.number(),
|
|
312
|
+
balances: v.array(expiryBalance),
|
|
313
|
+
},
|
|
314
|
+
returns: policyDoc,
|
|
315
|
+
handler: observeExpiry,
|
|
316
|
+
});
|
|
@@ -34,6 +34,22 @@ export const prepareFields = {
|
|
|
34
34
|
observedSchedule: schedule,
|
|
35
35
|
observedAt: v.optional(v.number()),
|
|
36
36
|
};
|
|
37
|
+
export const expiryBalance = v.object({
|
|
38
|
+
currency: v.string(),
|
|
39
|
+
available: v.number(),
|
|
40
|
+
pending: v.number(),
|
|
41
|
+
});
|
|
42
|
+
export const holdingExpiry = v.object({
|
|
43
|
+
deadline: v.number(),
|
|
44
|
+
status: v.union(
|
|
45
|
+
v.literal("unverified"),
|
|
46
|
+
v.literal("balance_remaining"),
|
|
47
|
+
v.literal("claims_unresolved"),
|
|
48
|
+
v.literal("no_held_balance"),
|
|
49
|
+
),
|
|
50
|
+
observedAt: v.optional(v.number()),
|
|
51
|
+
balances: v.optional(v.array(expiryBalance)),
|
|
52
|
+
});
|
|
37
53
|
export const policyFields = {
|
|
38
54
|
...scopeFields,
|
|
39
55
|
revision: v.number(),
|
|
@@ -44,6 +60,7 @@ export const policyFields = {
|
|
|
44
60
|
country: v.string(),
|
|
45
61
|
previousSchedule: v.optional(schedule),
|
|
46
62
|
holdDeadline: v.optional(v.number()),
|
|
63
|
+
holdingExpiry: v.optional(holdingExpiry),
|
|
47
64
|
};
|
|
48
65
|
export const operationFields = {
|
|
49
66
|
...prepareFields,
|
|
@@ -264,3 +264,45 @@ it("explains the deadline waiver without a minimum progress bar", () => {
|
|
|
264
264
|
expect(screen.queryByRole("progressbar")).toBeNull();
|
|
265
265
|
expect(screen.getByText("Combined payout minimum")).toBeTruthy();
|
|
266
266
|
});
|
|
267
|
+
|
|
268
|
+
it("shows expiry recovery after automatic schedule restoration without claiming disbursement", () => {
|
|
269
|
+
render(
|
|
270
|
+
<PayoutProgress
|
|
271
|
+
policy={{
|
|
272
|
+
...policy,
|
|
273
|
+
mode: "stripe_managed",
|
|
274
|
+
thresholds: [],
|
|
275
|
+
holdingExpiry: {
|
|
276
|
+
deadline: policy.holdDeadline,
|
|
277
|
+
status: "balance_remaining",
|
|
278
|
+
observedAt: 1800000000000,
|
|
279
|
+
balances: [{ currency: "usd", available: 999, pending: 25 }],
|
|
280
|
+
},
|
|
281
|
+
}}
|
|
282
|
+
currency="usd"
|
|
283
|
+
/>,
|
|
284
|
+
);
|
|
285
|
+
expect(screen.getByRole("status").textContent).toContain(
|
|
286
|
+
"does not confirm disbursement",
|
|
287
|
+
);
|
|
288
|
+
expect(screen.queryByText(/Stripe manages automatic payouts/)).toBeNull();
|
|
289
|
+
expect(screen.queryByRole("progressbar")).toBeNull();
|
|
290
|
+
expect(screen.getByText("$9.99")).toBeTruthy();
|
|
291
|
+
expect(screen.getByText("$0.25")).toBeTruthy();
|
|
292
|
+
});
|
|
293
|
+
it("keeps unavailable expiry balances unverified instead of showing fabricated zero", () => {
|
|
294
|
+
render(
|
|
295
|
+
<PayoutProgress
|
|
296
|
+
policy={{
|
|
297
|
+
...policy,
|
|
298
|
+
mode: "stripe_managed",
|
|
299
|
+
holdingExpiry: { deadline: policy.holdDeadline, status: "unverified" },
|
|
300
|
+
}}
|
|
301
|
+
currency="usd"
|
|
302
|
+
/>,
|
|
303
|
+
);
|
|
304
|
+
expect(screen.getByRole("status").textContent).toContain(
|
|
305
|
+
"verification is unavailable",
|
|
306
|
+
);
|
|
307
|
+
expect(screen.queryByText("$0.00")).toBeNull();
|
|
308
|
+
});
|
|
@@ -29,6 +29,25 @@ export function PayoutProgress(props: PayoutProgressProps) {
|
|
|
29
29
|
<h2>Automatic payout progress ({currency.toUpperCase()})</h2>
|
|
30
30
|
<p role="status">{state.message}</p>
|
|
31
31
|
<p>Stripe enforces its payout floor; a numeric amount is unavailable.</p>
|
|
32
|
+
{state.recovery?.balances && (
|
|
33
|
+
<div>
|
|
34
|
+
<h3>Stripe balances at last expiry check</h3>
|
|
35
|
+
<p>
|
|
36
|
+
Provider balances are not a payable earnings assessment. Currencies
|
|
37
|
+
remain separate.
|
|
38
|
+
</p>
|
|
39
|
+
<dl>
|
|
40
|
+
{state.recovery.balances.map((balance) => (
|
|
41
|
+
<div key={balance.currency}>
|
|
42
|
+
<dt>{balance.currency.toUpperCase()} available</dt>
|
|
43
|
+
<dd>{balance.availableLabel}</dd>
|
|
44
|
+
<dt>{balance.currency.toUpperCase()} pending</dt>
|
|
45
|
+
<dd>{balance.pendingLabel}</dd>
|
|
46
|
+
</div>
|
|
47
|
+
))}
|
|
48
|
+
</dl>
|
|
49
|
+
</div>
|
|
50
|
+
)}
|
|
32
51
|
{state.progress && (
|
|
33
52
|
<progress
|
|
34
53
|
aria-label="Combined payout minimum"
|
|
@@ -102,7 +121,12 @@ export function PayoutProgress(props: PayoutProgressProps) {
|
|
|
102
121
|
{state.holdDeadline !== null && (
|
|
103
122
|
<p>Custom holding deadline: {date(state.holdDeadline)}</p>
|
|
104
123
|
)}
|
|
105
|
-
{state.observedAt !== null &&
|
|
124
|
+
{state.observedAt !== null && (
|
|
125
|
+
<p>
|
|
126
|
+
{state.recovery ? "Last recovery check" : "Assessed"}:{" "}
|
|
127
|
+
{date(state.observedAt)}
|
|
128
|
+
</p>
|
|
129
|
+
)}
|
|
106
130
|
</section>
|
|
107
131
|
);
|
|
108
132
|
}
|
|
@@ -42,10 +42,23 @@ export type PayoutProgressState = {
|
|
|
42
42
|
nextAttemptAt: number | null;
|
|
43
43
|
holdDeadline: number | null;
|
|
44
44
|
observedAt: number | null;
|
|
45
|
+
recovery?: {
|
|
46
|
+
status: NonNullable<PayoutPolicy["holdingExpiry"]>["status"];
|
|
47
|
+
balances?: {
|
|
48
|
+
currency: string;
|
|
49
|
+
available: number;
|
|
50
|
+
pending: number;
|
|
51
|
+
availableLabel: string;
|
|
52
|
+
pendingLabel: string;
|
|
53
|
+
}[];
|
|
54
|
+
};
|
|
45
55
|
};
|
|
46
56
|
export type PayoutProgressProps = {
|
|
47
57
|
policy:
|
|
48
|
-
| Pick<
|
|
58
|
+
| Pick<
|
|
59
|
+
PayoutPolicy,
|
|
60
|
+
"mode" | "status" | "thresholds" | "holdDeadline" | "holdingExpiry"
|
|
61
|
+
>
|
|
49
62
|
| null
|
|
50
63
|
| undefined;
|
|
51
64
|
preview?: PayoutPreview | null;
|
|
@@ -104,6 +117,33 @@ export function payoutProgressState({
|
|
|
104
117
|
state.status = "changing";
|
|
105
118
|
state.message =
|
|
106
119
|
"Payout settings are being verified. Eligibility is unavailable until the change completes.";
|
|
120
|
+
} else if (
|
|
121
|
+
policy?.holdingExpiry &&
|
|
122
|
+
policy.holdingExpiry.status !== "no_held_balance"
|
|
123
|
+
) {
|
|
124
|
+
const expiry = policy.holdingExpiry;
|
|
125
|
+
state.status = "unavailable";
|
|
126
|
+
state.reason = "holding_expiry_unresolved";
|
|
127
|
+
state.holdDeadline = expiry.deadline;
|
|
128
|
+
state.observedAt = expiry.observedAt ?? null;
|
|
129
|
+
state.message =
|
|
130
|
+
"The holding deadline has passed. Schedule restoration does not confirm disbursement. " +
|
|
131
|
+
(expiry.status === "unverified"
|
|
132
|
+
? "Stripe balance verification is unavailable. "
|
|
133
|
+
: "Stripe balances or payout claims still require recovery. ") +
|
|
134
|
+
"Contact platform support; automatic recovery checks will retry.";
|
|
135
|
+
state.recovery = {
|
|
136
|
+
status: expiry.status,
|
|
137
|
+
...(expiry.balances
|
|
138
|
+
? {
|
|
139
|
+
balances: expiry.balances.map((b) => ({
|
|
140
|
+
...b,
|
|
141
|
+
availableLabel: formatPrice(b.available, b.currency, locale),
|
|
142
|
+
pendingLabel: formatPrice(b.pending, b.currency, locale),
|
|
143
|
+
})),
|
|
144
|
+
}
|
|
145
|
+
: {}),
|
|
146
|
+
};
|
|
107
147
|
} else if (!policy || policy.mode === "stripe_managed") {
|
|
108
148
|
state.status = "stripe_managed";
|
|
109
149
|
state.message =
|