@fantastic.dev/better-stripe 0.28.4 → 0.28.6
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 +6 -2
- package/dist/client/connect/payoutAdoptionTestHelpers.d.ts.map +1 -1
- package/dist/client/connect/payoutExecutionTestHelpers.d.ts +3 -1
- package/dist/client/connect/payoutExecutionTestHelpers.d.ts.map +1 -1
- package/dist/client/connect/payoutPreview.d.ts.map +1 -1
- package/dist/client/connect/payoutPreview.js +1 -1
- package/dist/client/connect/payoutPreview.js.map +1 -1
- package/dist/client/connect/payoutQualification.d.ts.map +1 -1
- package/dist/client/connect/payoutQualification.js +5 -1
- package/dist/client/connect/payoutQualification.js.map +1 -1
- package/dist/client/connect/payoutSyncTestHelpers.d.ts +3 -1
- package/dist/client/connect/payoutSyncTestHelpers.d.ts.map +1 -1
- package/dist/client/connect/payoutWorker.d.ts +4 -1
- package/dist/client/connect/payoutWorker.d.ts.map +1 -1
- package/dist/client/connect/payoutWorker.js +35 -1
- package/dist/client/connect/payoutWorker.js.map +1 -1
- package/dist/client/payoutPolicy.d.ts +2 -0
- package/dist/client/payoutPolicy.d.ts.map +1 -1
- package/dist/client/payoutPolicy.js +2 -0
- package/dist/client/payoutPolicy.js.map +1 -1
- package/dist/component/_generated/component.d.ts +4 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/earnings/triggers.d.ts +4 -2
- package/dist/component/earnings/triggers.d.ts.map +1 -1
- package/dist/component/payoutRelease/helpers.d.ts +1 -1
- package/dist/component/payoutRelease/helpers.d.ts.map +1 -1
- package/dist/component/payoutRelease/helpers.js +2 -1
- package/dist/component/payoutRelease/helpers.js.map +1 -1
- package/dist/component/payoutRelease/mutations.d.ts.map +1 -1
- package/dist/component/payoutRelease/mutations.js +1 -1
- package/dist/component/payoutRelease/mutations.js.map +1 -1
- package/dist/component/payoutRelease/planning.d.ts.map +1 -1
- package/dist/component/payoutRelease/planning.js +1 -1
- package/dist/component/payoutRelease/planning.js.map +1 -1
- package/dist/component/payoutRelease/preview.d.ts.map +1 -1
- package/dist/component/payoutRelease/preview.js +3 -1
- package/dist/component/payoutRelease/preview.js.map +1 -1
- package/dist/component/payoutWorker/helpers.d.ts +1 -0
- package/dist/component/payoutWorker/helpers.d.ts.map +1 -1
- package/dist/component/payoutWorker/mutations.d.ts +2 -0
- package/dist/component/payoutWorker/mutations.d.ts.map +1 -1
- package/dist/component/payoutWorker/mutations.js +3 -0
- package/dist/component/payoutWorker/mutations.js.map +1 -1
- package/dist/component/payoutWorker/queries.d.ts +2 -0
- package/dist/component/payoutWorker/queries.d.ts.map +1 -1
- package/dist/component/payoutWorker/schema.d.ts +18 -2
- package/dist/component/payoutWorker/schema.d.ts.map +1 -1
- package/dist/component/payoutWorker/schema.js +7 -0
- package/dist/component/payoutWorker/schema.js.map +1 -1
- package/dist/component/schema.d.ts +3 -1
- package/dist/react/workflows/PayoutAttemptStatus.d.ts +14 -0
- package/dist/react/workflows/PayoutAttemptStatus.d.ts.map +1 -0
- package/dist/react/workflows/PayoutAttemptStatus.js +13 -0
- package/dist/react/workflows/PayoutAttemptStatus.js.map +1 -0
- package/dist/react/workflows/PayoutProgress.d.ts.map +1 -1
- package/dist/react/workflows/PayoutProgress.js +2 -1
- package/dist/react/workflows/PayoutProgress.js.map +1 -1
- package/dist/react/workflows/index.d.ts +2 -0
- package/dist/react/workflows/index.d.ts.map +1 -1
- package/dist/react/workflows/index.js +1 -0
- package/dist/react/workflows/index.js.map +1 -1
- package/dist/react/workflows/payout-attempt-state.d.ts +9 -0
- package/dist/react/workflows/payout-attempt-state.d.ts.map +1 -0
- package/dist/react/workflows/payout-attempt-state.js +47 -0
- package/dist/react/workflows/payout-attempt-state.js.map +1 -0
- package/dist/react/workflows/payout-progress-state.d.ts +5 -1
- package/dist/react/workflows/payout-progress-state.d.ts.map +1 -1
- package/dist/react/workflows/payout-progress-state.js +11 -2
- package/dist/react/workflows/payout-progress-state.js.map +1 -1
- package/docs/react-workflows.md +19 -0
- package/docs/settlement.md +25 -1
- package/package.json +1 -1
- package/src/client/connect/payoutPreview.ts +3 -1
- package/src/client/connect/payoutQualification.ts +7 -1
- package/src/client/connect/payoutQualificationFreshness.test.ts +192 -0
- package/src/client/connect/payoutWorker.test.ts +57 -1
- package/src/client/connect/payoutWorker.ts +46 -2
- package/src/client/payoutPolicy.ts +2 -0
- package/src/component/_generated/component.ts +4 -0
- package/src/component/payoutRelease/helpers.ts +2 -0
- package/src/component/payoutRelease/mutations.ts +7 -1
- package/src/component/payoutRelease/planning.ts +1 -0
- package/src/component/payoutRelease/preview.ts +3 -1
- package/src/component/payoutWorker/mutations.ts +3 -0
- package/src/component/payoutWorker/schema.ts +15 -0
- package/src/react/workflows/PayoutAttemptStatus.tsx +44 -0
- package/src/react/workflows/PayoutProgress.test.tsx +48 -0
- package/src/react/workflows/PayoutProgress.tsx +6 -0
- package/src/react/workflows/index.ts +6 -0
- package/src/react/workflows/payout-attempt-state.ts +70 -0
- package/src/react/workflows/payout-progress-state.ts +21 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { payoutAttemptStatus } from "./connect/payoutWorker.js";
|
|
2
|
+
export { attemptStatus as payoutAttemptValidator } from "../component/payoutWorker/schema.js";
|
|
1
3
|
export { holdingExpiry as payoutHoldingExpiryValidator } from "../component/payoutPolicy/schema.js";
|
|
2
4
|
import { expirePayoutPolicy } from "./connect/payoutExpiry.js";
|
|
3
5
|
import {
|
|
@@ -4112,6 +4112,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
4112
4112
|
hasOperation: boolean;
|
|
4113
4113
|
lastAttemptAt?: number;
|
|
4114
4114
|
lastReason?: string;
|
|
4115
|
+
lastResultAt?: number;
|
|
4115
4116
|
leaseUntil?: number;
|
|
4116
4117
|
nextAttemptAt: number;
|
|
4117
4118
|
operationKey?: string;
|
|
@@ -4146,6 +4147,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
4146
4147
|
hasOperation: boolean;
|
|
4147
4148
|
lastAttemptAt?: number;
|
|
4148
4149
|
lastReason?: string;
|
|
4150
|
+
lastResultAt?: number;
|
|
4149
4151
|
leaseUntil?: number;
|
|
4150
4152
|
nextAttemptAt: number;
|
|
4151
4153
|
operationKey?: string;
|
|
@@ -4223,6 +4225,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
4223
4225
|
hasOperation: boolean;
|
|
4224
4226
|
lastAttemptAt?: number;
|
|
4225
4227
|
lastReason?: string;
|
|
4228
|
+
lastResultAt?: number;
|
|
4226
4229
|
leaseUntil?: number;
|
|
4227
4230
|
nextAttemptAt: number;
|
|
4228
4231
|
operationKey?: string;
|
|
@@ -4310,6 +4313,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
4310
4313
|
hasOperation: boolean;
|
|
4311
4314
|
lastAttemptAt?: number;
|
|
4312
4315
|
lastReason?: string;
|
|
4316
|
+
lastResultAt?: number;
|
|
4313
4317
|
leaseUntil?: number;
|
|
4314
4318
|
nextAttemptAt: number;
|
|
4315
4319
|
operationKey?: string;
|
|
@@ -71,6 +71,7 @@ export async function checkSnapshot(
|
|
|
71
71
|
accountId: string,
|
|
72
72
|
observations: Infer<typeof observation>[],
|
|
73
73
|
fresh: boolean,
|
|
74
|
+
freshCurrency?: string,
|
|
74
75
|
) {
|
|
75
76
|
const rows = await ctx.db
|
|
76
77
|
.query("transfers")
|
|
@@ -106,6 +107,7 @@ export async function checkSnapshot(
|
|
|
106
107
|
throw new Error("Payout source snapshot changed or is not ready.");
|
|
107
108
|
if (
|
|
108
109
|
fresh &&
|
|
110
|
+
(freshCurrency === undefined || o.currency === freshCurrency) &&
|
|
109
111
|
(!Number.isSafeInteger(o.observedAt) ||
|
|
110
112
|
o.observedAt > Date.now() ||
|
|
111
113
|
o.observedAt < Date.now() - 30000)
|
|
@@ -124,7 +124,13 @@ export const beginAttempt = mutation({
|
|
|
124
124
|
const feeRevision = (await readFees(ctx, claim))?.revision ?? 0;
|
|
125
125
|
if (feeRevision !== (claim.feeRevision ?? 0))
|
|
126
126
|
throw new Error("Payout fee revision changed after reservation.");
|
|
127
|
-
await checkSnapshot(
|
|
127
|
+
await checkSnapshot(
|
|
128
|
+
ctx,
|
|
129
|
+
claim.accountId,
|
|
130
|
+
claim.observations,
|
|
131
|
+
true,
|
|
132
|
+
claim.currency,
|
|
133
|
+
);
|
|
128
134
|
await reserveRecoveryGuard(ctx, claim, claim.amount);
|
|
129
135
|
await ctx.db.patch("payoutReleaseClaims", claim._id, {
|
|
130
136
|
attemptedAt: Date.now(),
|
|
@@ -106,7 +106,9 @@ export async function previewPlan(
|
|
|
106
106
|
providerAvailable: args.providerAvailable,
|
|
107
107
|
observedAt: Math.min(
|
|
108
108
|
Date.now(),
|
|
109
|
-
...args.observations
|
|
109
|
+
...args.observations
|
|
110
|
+
.filter((o) => o.currency === args.currency)
|
|
111
|
+
.map((o) => o.observedAt),
|
|
110
112
|
),
|
|
111
113
|
...progress,
|
|
112
114
|
};
|
|
@@ -77,6 +77,7 @@ export const begin = mutation({
|
|
|
77
77
|
hasOperation: false,
|
|
78
78
|
leaseUntil: undefined,
|
|
79
79
|
lastReason: "policy_inactive",
|
|
80
|
+
lastResultAt: now,
|
|
80
81
|
nextAttemptAt: nextPayoutAttempt(policy, now, args.cadenceMs),
|
|
81
82
|
});
|
|
82
83
|
return { status: "held" as const, reason: "policy_inactive" };
|
|
@@ -113,6 +114,7 @@ export const begin = mutation({
|
|
|
113
114
|
hasOperation: false,
|
|
114
115
|
leaseUntil: undefined,
|
|
115
116
|
lastReason: "affiliate_recovery_pending",
|
|
117
|
+
lastResultAt: now,
|
|
116
118
|
nextAttemptAt,
|
|
117
119
|
});
|
|
118
120
|
return {
|
|
@@ -189,6 +191,7 @@ export const finish = mutation({
|
|
|
189
191
|
job.cadenceMs,
|
|
190
192
|
),
|
|
191
193
|
lastReason: args.reason,
|
|
194
|
+
lastResultAt: Date.now(),
|
|
192
195
|
completedGeneration: args.generation,
|
|
193
196
|
completedOperationKey: args.operationKey,
|
|
194
197
|
});
|
|
@@ -13,6 +13,7 @@ export const fields = {
|
|
|
13
13
|
hasOperation: v.boolean(),
|
|
14
14
|
lastReason: v.optional(v.string()),
|
|
15
15
|
lastAttemptAt: v.optional(v.number()),
|
|
16
|
+
lastResultAt: v.optional(v.number()),
|
|
16
17
|
completedGeneration: v.optional(v.number()),
|
|
17
18
|
completedOperationKey: v.optional(v.string()),
|
|
18
19
|
};
|
|
@@ -24,3 +25,17 @@ export const jobDoc = v.object({
|
|
|
24
25
|
export const jobsTable = defineTable(fields)
|
|
25
26
|
.index("by_account_currency", ["accountId", "currency"])
|
|
26
27
|
.index("by_operation_pending", ["hasOperation", "nextAttemptAt"]);
|
|
28
|
+
|
|
29
|
+
export const attemptStatus = v.object({
|
|
30
|
+
currency: v.string(),
|
|
31
|
+
status: v.union(
|
|
32
|
+
v.literal("running"),
|
|
33
|
+
v.literal("reconciling"),
|
|
34
|
+
v.literal("rejected"),
|
|
35
|
+
v.literal("failed"),
|
|
36
|
+
v.literal("paid"),
|
|
37
|
+
v.literal("held"),
|
|
38
|
+
),
|
|
39
|
+
reason: v.string(),
|
|
40
|
+
at: v.optional(v.number()),
|
|
41
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import type { PayoutAttempt } from "../../client/connect/payoutWorker.js";
|
|
4
|
+
import {
|
|
5
|
+
payoutAttemptState,
|
|
6
|
+
type PayoutAttemptState,
|
|
7
|
+
} from "./payout-attempt-state.js";
|
|
8
|
+
export type { PayoutAttemptState } from "./payout-attempt-state.js";
|
|
9
|
+
export type PayoutAttemptStatusProps = {
|
|
10
|
+
attempt?: PayoutAttempt | null;
|
|
11
|
+
locale?: string;
|
|
12
|
+
timeZone?: string;
|
|
13
|
+
className?: string;
|
|
14
|
+
children?: (state: PayoutAttemptState) => ReactNode;
|
|
15
|
+
};
|
|
16
|
+
/** Unstyled worker status. Result times are not bank arrival estimates. */
|
|
17
|
+
export function PayoutAttemptStatus({
|
|
18
|
+
attempt,
|
|
19
|
+
locale,
|
|
20
|
+
timeZone,
|
|
21
|
+
className,
|
|
22
|
+
children,
|
|
23
|
+
}: PayoutAttemptStatusProps) {
|
|
24
|
+
const state = payoutAttemptState(attempt, locale, timeZone);
|
|
25
|
+
if (!state) return null;
|
|
26
|
+
if (children) return <>{children(state)}</>;
|
|
27
|
+
return (
|
|
28
|
+
<aside
|
|
29
|
+
className={className}
|
|
30
|
+
data-better-stripe="payout-attempt"
|
|
31
|
+
data-tone={state.tone}
|
|
32
|
+
>
|
|
33
|
+
<h3>Latest automatic payout check</h3>
|
|
34
|
+
<p role="status" aria-label="Latest automatic payout attempt">
|
|
35
|
+
{state.message}
|
|
36
|
+
</p>
|
|
37
|
+
{state.formattedAt && (
|
|
38
|
+
<p>
|
|
39
|
+
{state.timeLabel}: {state.formattedAt}
|
|
40
|
+
</p>
|
|
41
|
+
)}
|
|
42
|
+
</aside>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -306,3 +306,51 @@ it("keeps unavailable expiry balances unverified instead of showing fabricated z
|
|
|
306
306
|
);
|
|
307
307
|
expect(screen.queryByText("$0.00")).toBeNull();
|
|
308
308
|
});
|
|
309
|
+
|
|
310
|
+
it("shows a previous Stripe rejection separately from informational eligibility", () => {
|
|
311
|
+
render(
|
|
312
|
+
<PayoutProgress
|
|
313
|
+
policy={policy}
|
|
314
|
+
preview={{
|
|
315
|
+
...preview,
|
|
316
|
+
status: "qualified",
|
|
317
|
+
reason: undefined,
|
|
318
|
+
amount: 999,
|
|
319
|
+
minimumWaived: true,
|
|
320
|
+
holdDeadline: policy.holdDeadline,
|
|
321
|
+
}}
|
|
322
|
+
currency="usd"
|
|
323
|
+
lastAttempt={{
|
|
324
|
+
currency: "usd",
|
|
325
|
+
status: "rejected",
|
|
326
|
+
reason: "balance_insufficient",
|
|
327
|
+
at: 1800000000000,
|
|
328
|
+
}}
|
|
329
|
+
nextAttemptAt={1800000300000}
|
|
330
|
+
timeZone="UTC"
|
|
331
|
+
/>,
|
|
332
|
+
);
|
|
333
|
+
expect(
|
|
334
|
+
screen.getByText(
|
|
335
|
+
/Stripe rejected the payout because the available balance changed/,
|
|
336
|
+
),
|
|
337
|
+
).toBeTruthy();
|
|
338
|
+
expect(screen.getByText(/Next automatic attempt/)).toBeTruthy();
|
|
339
|
+
});
|
|
340
|
+
it("does not display an old paid result or stale retry time while a new check is running", () => {
|
|
341
|
+
render(
|
|
342
|
+
<PayoutProgress
|
|
343
|
+
policy={policy}
|
|
344
|
+
preview={preview}
|
|
345
|
+
currency="usd"
|
|
346
|
+
lastAttempt={{
|
|
347
|
+
currency: "usd",
|
|
348
|
+
status: "running",
|
|
349
|
+
reason: "pending_claim",
|
|
350
|
+
}}
|
|
351
|
+
nextAttemptAt={1800000000000}
|
|
352
|
+
/>,
|
|
353
|
+
);
|
|
354
|
+
expect(screen.getByText(/automatic payout check is running/)).toBeTruthy();
|
|
355
|
+
expect(screen.queryByText(/Next automatic attempt/)).toBeNull();
|
|
356
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { PayoutAttemptStatus } from "./PayoutAttemptStatus.js";
|
|
2
3
|
import { useMemo } from "react";
|
|
3
4
|
import {
|
|
4
5
|
payoutProgressState,
|
|
@@ -28,6 +29,11 @@ export function PayoutProgress(props: PayoutProgressProps) {
|
|
|
28
29
|
<section className={className} data-better-stripe="payout-progress">
|
|
29
30
|
<h2>Automatic payout progress ({currency.toUpperCase()})</h2>
|
|
30
31
|
<p role="status">{state.message}</p>
|
|
32
|
+
<PayoutAttemptStatus
|
|
33
|
+
attempt={state.lastAttempt}
|
|
34
|
+
locale={locale}
|
|
35
|
+
timeZone={timeZone}
|
|
36
|
+
/>
|
|
31
37
|
<p>Stripe enforces its payout floor; a numeric amount is unavailable.</p>
|
|
32
38
|
{state.recovery?.balances && (
|
|
33
39
|
<div>
|
|
@@ -290,3 +290,9 @@ export type {
|
|
|
290
290
|
|
|
291
291
|
export { EvidenceUpload } from "./EvidenceUpload.js";
|
|
292
292
|
export type { EvidenceUploadProps } from "./EvidenceUpload.js";
|
|
293
|
+
|
|
294
|
+
export { PayoutAttemptStatus } from "./PayoutAttemptStatus.js";
|
|
295
|
+
export type {
|
|
296
|
+
PayoutAttemptStatusProps,
|
|
297
|
+
PayoutAttemptState,
|
|
298
|
+
} from "./PayoutAttemptStatus.js";
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { PayoutAttempt } from "../../client/connect/payoutWorker.js";
|
|
2
|
+
const messages: Record<string, string> = {
|
|
3
|
+
balance_insufficient:
|
|
4
|
+
"Stripe rejected the payout because the available balance changed. Remaining unpaid entitlement is preserved; any retry requires fresh balance verification.",
|
|
5
|
+
amount_too_small:
|
|
6
|
+
"Stripe rejected the payout below its supported minimum. A numeric provider floor is unavailable. Review payout settings promptly; holding limits still apply.",
|
|
7
|
+
worker_error:
|
|
8
|
+
"The automatic payout check could not complete. An uncertain outcome must be reconciled before another payout can be created.",
|
|
9
|
+
returned:
|
|
10
|
+
"The bank returned the payout. Returned funds will be assessed again when available.",
|
|
11
|
+
bank_destination_unavailable:
|
|
12
|
+
"Add or verify a supported payout bank account before the worker can retry.",
|
|
13
|
+
affiliate_recovery_pending:
|
|
14
|
+
"Commission recovery must settle before a new automatic payout can qualify.",
|
|
15
|
+
below_threshold:
|
|
16
|
+
"No payout was requested because the combined balance is below the application minimum.",
|
|
17
|
+
insufficient_available:
|
|
18
|
+
"No payout was requested because the supported available Stripe balance is insufficient.",
|
|
19
|
+
schedule_drift:
|
|
20
|
+
"Payout scheduling changed outside the application. Contact platform support to verify controls.",
|
|
21
|
+
snapshot_limit:
|
|
22
|
+
"History reconciliation is incomplete. No payout was requested from a partial balance.",
|
|
23
|
+
policy_inactive:
|
|
24
|
+
"The application-managed payout policy is inactive. No new application payout was requested.",
|
|
25
|
+
};
|
|
26
|
+
export type PayoutAttemptState = PayoutAttempt & {
|
|
27
|
+
message: string;
|
|
28
|
+
tone: "warning" | "neutral" | "success";
|
|
29
|
+
formattedAt?: string;
|
|
30
|
+
timeLabel?: string;
|
|
31
|
+
};
|
|
32
|
+
export function payoutAttemptState(
|
|
33
|
+
attempt: PayoutAttempt | null | undefined,
|
|
34
|
+
locale = "en-US",
|
|
35
|
+
timeZone?: string,
|
|
36
|
+
): PayoutAttemptState | undefined {
|
|
37
|
+
if (!attempt) return undefined;
|
|
38
|
+
const message =
|
|
39
|
+
attempt.status === "running"
|
|
40
|
+
? "An automatic payout check is running. Its outcome is not verified yet."
|
|
41
|
+
: attempt.status === "reconciling"
|
|
42
|
+
? "The payout outcome needs verification. The same operation will be reconciled before any new payout."
|
|
43
|
+
: attempt.status === "paid"
|
|
44
|
+
? "The latest application payout was verified as paid by Stripe."
|
|
45
|
+
: (messages[attempt.reason] ??
|
|
46
|
+
"The latest automatic payout check did not complete. Review account settings or contact platform support.");
|
|
47
|
+
return {
|
|
48
|
+
...attempt,
|
|
49
|
+
message,
|
|
50
|
+
tone:
|
|
51
|
+
attempt.status === "paid"
|
|
52
|
+
? "success"
|
|
53
|
+
: ["rejected", "failed", "reconciling"].includes(attempt.status)
|
|
54
|
+
? "warning"
|
|
55
|
+
: "neutral",
|
|
56
|
+
...(attempt.at === undefined
|
|
57
|
+
? {}
|
|
58
|
+
: {
|
|
59
|
+
formattedAt: new Intl.DateTimeFormat(locale, {
|
|
60
|
+
dateStyle: "medium",
|
|
61
|
+
timeStyle: "short",
|
|
62
|
+
timeZone,
|
|
63
|
+
}).format(attempt.at),
|
|
64
|
+
timeLabel:
|
|
65
|
+
attempt.status === "running"
|
|
66
|
+
? "Attempt started"
|
|
67
|
+
: "Result recorded",
|
|
68
|
+
}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
payoutAttemptState,
|
|
3
|
+
type PayoutAttemptState,
|
|
4
|
+
} from "./payout-attempt-state.js";
|
|
5
|
+
import type { PayoutAttempt } from "../../client/connect/payoutWorker.js";
|
|
1
6
|
import type { ReactNode } from "react";
|
|
2
7
|
import type { PayoutPolicy, PayoutPreview } from "../../client/payoutPolicy.js";
|
|
3
8
|
import { formatPrice } from "../lib/price-helpers.js";
|
|
@@ -42,6 +47,7 @@ export type PayoutProgressState = {
|
|
|
42
47
|
nextAttemptAt: number | null;
|
|
43
48
|
holdDeadline: number | null;
|
|
44
49
|
observedAt: number | null;
|
|
50
|
+
lastAttempt?: PayoutAttemptState;
|
|
45
51
|
recovery?: {
|
|
46
52
|
status: NonNullable<PayoutPolicy["holdingExpiry"]>["status"];
|
|
47
53
|
balances?: {
|
|
@@ -62,6 +68,7 @@ export type PayoutProgressProps = {
|
|
|
62
68
|
| null
|
|
63
69
|
| undefined;
|
|
64
70
|
preview?: PayoutPreview | null;
|
|
71
|
+
lastAttempt?: PayoutAttempt | null;
|
|
65
72
|
currency: string;
|
|
66
73
|
/** Application worker attempt time, never a bank arrival estimate. */
|
|
67
74
|
nextAttemptAt?: number | null;
|
|
@@ -96,8 +103,14 @@ export function payoutProgressState({
|
|
|
96
103
|
preview,
|
|
97
104
|
currency,
|
|
98
105
|
nextAttemptAt,
|
|
106
|
+
lastAttempt,
|
|
107
|
+
timeZone,
|
|
99
108
|
locale = "en-US",
|
|
100
109
|
}: PayoutProgressProps): PayoutProgressState {
|
|
110
|
+
if (lastAttempt && lastAttempt.currency !== currency)
|
|
111
|
+
throw new Error(
|
|
112
|
+
"Payout attempt currency does not match the selected currency.",
|
|
113
|
+
);
|
|
101
114
|
const amount = (value: number) => formatPrice(value, currency, locale);
|
|
102
115
|
const state: PayoutProgressState = {
|
|
103
116
|
status: "loading",
|
|
@@ -111,6 +124,10 @@ export function payoutProgressState({
|
|
|
111
124
|
holdDeadline:
|
|
112
125
|
policy?.mode === "custom" ? (policy.holdDeadline ?? null) : null,
|
|
113
126
|
observedAt: null,
|
|
127
|
+
lastAttempt:
|
|
128
|
+
policy === undefined
|
|
129
|
+
? undefined
|
|
130
|
+
: payoutAttemptState(lastAttempt, locale, timeZone),
|
|
114
131
|
};
|
|
115
132
|
if (policy !== undefined) {
|
|
116
133
|
if (policy?.status === "changing") {
|
|
@@ -149,7 +166,10 @@ export function payoutProgressState({
|
|
|
149
166
|
state.message =
|
|
150
167
|
"Stripe manages automatic payouts. No additional application minimum is set.";
|
|
151
168
|
} else if (preview !== undefined) {
|
|
152
|
-
state.nextAttemptAt =
|
|
169
|
+
state.nextAttemptAt =
|
|
170
|
+
state.lastAttempt?.status === "running"
|
|
171
|
+
? null
|
|
172
|
+
: (nextAttemptAt ?? null);
|
|
153
173
|
if (!preview || !("currency" in preview)) {
|
|
154
174
|
state.status = "unavailable";
|
|
155
175
|
state.reason = preview?.reason ?? "assessment_unavailable";
|