@fantastic.dev/better-stripe 0.28.3 → 0.28.5

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.
Files changed (101) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +5 -1
  3. package/dist/client/connect/payoutAdoptionTestHelpers.d.ts +102 -4
  4. package/dist/client/connect/payoutAdoptionTestHelpers.d.ts.map +1 -1
  5. package/dist/client/connect/payoutExecutionTestHelpers.d.ts +51 -2
  6. package/dist/client/connect/payoutExecutionTestHelpers.d.ts.map +1 -1
  7. package/dist/client/connect/payoutExpiry.d.ts +42 -0
  8. package/dist/client/connect/payoutExpiry.d.ts.map +1 -0
  9. package/dist/client/connect/payoutExpiry.js +93 -0
  10. package/dist/client/connect/payoutExpiry.js.map +1 -0
  11. package/dist/client/connect/payoutSyncTestHelpers.d.ts +41 -2
  12. package/dist/client/connect/payoutSyncTestHelpers.d.ts.map +1 -1
  13. package/dist/client/connect/payoutWorker.d.ts +4 -1
  14. package/dist/client/connect/payoutWorker.d.ts.map +1 -1
  15. package/dist/client/connect/payoutWorker.js +35 -1
  16. package/dist/client/connect/payoutWorker.js.map +1 -1
  17. package/dist/client/payoutPolicy.d.ts +20 -2
  18. package/dist/client/payoutPolicy.d.ts.map +1 -1
  19. package/dist/client/payoutPolicy.js +5 -25
  20. package/dist/client/payoutPolicy.js.map +1 -1
  21. package/dist/component/_generated/component.d.ts +120 -0
  22. package/dist/component/_generated/component.d.ts.map +1 -1
  23. package/dist/component/core/resetSafety.d.ts.map +1 -1
  24. package/dist/component/core/resetSafety.js +7 -1
  25. package/dist/component/core/resetSafety.js.map +1 -1
  26. package/dist/component/earnings/triggers.d.ts +26 -4
  27. package/dist/component/earnings/triggers.d.ts.map +1 -1
  28. package/dist/component/payoutPolicy/expiry.d.ts +78 -0
  29. package/dist/component/payoutPolicy/expiry.d.ts.map +1 -0
  30. package/dist/component/payoutPolicy/expiry.js +57 -0
  31. package/dist/component/payoutPolicy/expiry.js.map +1 -0
  32. package/dist/component/payoutPolicy/helpers.d.ts +10 -0
  33. package/dist/component/payoutPolicy/helpers.d.ts.map +1 -1
  34. package/dist/component/payoutPolicy/mutations.d.ts +77 -0
  35. package/dist/component/payoutPolicy/mutations.d.ts.map +1 -1
  36. package/dist/component/payoutPolicy/mutations.js +36 -3
  37. package/dist/component/payoutPolicy/mutations.js.map +1 -1
  38. package/dist/component/payoutPolicy/queries.d.ts +20 -0
  39. package/dist/component/payoutPolicy/queries.d.ts.map +1 -1
  40. package/dist/component/payoutPolicy/schema.d.ts +139 -2
  41. package/dist/component/payoutPolicy/schema.d.ts.map +1 -1
  42. package/dist/component/payoutPolicy/schema.js +12 -0
  43. package/dist/component/payoutPolicy/schema.js.map +1 -1
  44. package/dist/component/payoutRelease/helpers.d.ts +10 -0
  45. package/dist/component/payoutRelease/helpers.d.ts.map +1 -1
  46. package/dist/component/payoutWorker/helpers.d.ts +1 -0
  47. package/dist/component/payoutWorker/helpers.d.ts.map +1 -1
  48. package/dist/component/payoutWorker/mutations.d.ts +2 -0
  49. package/dist/component/payoutWorker/mutations.d.ts.map +1 -1
  50. package/dist/component/payoutWorker/mutations.js +3 -0
  51. package/dist/component/payoutWorker/mutations.js.map +1 -1
  52. package/dist/component/payoutWorker/queries.d.ts +22 -0
  53. package/dist/component/payoutWorker/queries.d.ts.map +1 -1
  54. package/dist/component/payoutWorker/schema.d.ts +18 -2
  55. package/dist/component/payoutWorker/schema.d.ts.map +1 -1
  56. package/dist/component/payoutWorker/schema.js +7 -0
  57. package/dist/component/payoutWorker/schema.js.map +1 -1
  58. package/dist/component/schema.d.ts +41 -2
  59. package/dist/react/workflows/PayoutAttemptStatus.d.ts +14 -0
  60. package/dist/react/workflows/PayoutAttemptStatus.d.ts.map +1 -0
  61. package/dist/react/workflows/PayoutAttemptStatus.js +13 -0
  62. package/dist/react/workflows/PayoutAttemptStatus.js.map +1 -0
  63. package/dist/react/workflows/PayoutProgress.d.ts.map +1 -1
  64. package/dist/react/workflows/PayoutProgress.js +3 -2
  65. package/dist/react/workflows/PayoutProgress.js.map +1 -1
  66. package/dist/react/workflows/index.d.ts +2 -0
  67. package/dist/react/workflows/index.d.ts.map +1 -1
  68. package/dist/react/workflows/index.js +1 -0
  69. package/dist/react/workflows/index.js.map +1 -1
  70. package/dist/react/workflows/payout-attempt-state.d.ts +9 -0
  71. package/dist/react/workflows/payout-attempt-state.d.ts.map +1 -0
  72. package/dist/react/workflows/payout-attempt-state.js +47 -0
  73. package/dist/react/workflows/payout-attempt-state.js.map +1 -0
  74. package/dist/react/workflows/payout-progress-state.d.ts +16 -2
  75. package/dist/react/workflows/payout-progress-state.d.ts.map +1 -1
  76. package/dist/react/workflows/payout-progress-state.js +37 -2
  77. package/dist/react/workflows/payout-progress-state.js.map +1 -1
  78. package/docs/react-workflows.md +30 -0
  79. package/docs/settlement.md +47 -3
  80. package/package.json +1 -1
  81. package/src/client/connect/payoutDeadline.test.ts +70 -0
  82. package/src/client/connect/payoutExpiry.ts +132 -0
  83. package/src/client/connect/payoutWorker.test.ts +57 -1
  84. package/src/client/connect/payoutWorker.ts +46 -2
  85. package/src/client/payoutPolicy.test.ts +196 -0
  86. package/src/client/payoutPolicy.ts +11 -23
  87. package/src/component/_generated/api.ts +2 -0
  88. package/src/component/_generated/component.ts +147 -0
  89. package/src/component/core/resetSafety.ts +49 -33
  90. package/src/component/payout-reset-safety.test.ts +28 -0
  91. package/src/component/payoutPolicy/expiry.ts +80 -0
  92. package/src/component/payoutPolicy/mutations.ts +47 -2
  93. package/src/component/payoutPolicy/schema.ts +17 -0
  94. package/src/component/payoutWorker/mutations.ts +3 -0
  95. package/src/component/payoutWorker/schema.ts +15 -0
  96. package/src/react/workflows/PayoutAttemptStatus.tsx +44 -0
  97. package/src/react/workflows/PayoutProgress.test.tsx +90 -0
  98. package/src/react/workflows/PayoutProgress.tsx +31 -1
  99. package/src/react/workflows/index.ts +6 -0
  100. package/src/react/workflows/payout-attempt-state.ts +70 -0
  101. package/src/react/workflows/payout-progress-state.ts +62 -2
@@ -264,3 +264,93 @@ 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
+ });
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,7 +29,31 @@ 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>
38
+ {state.recovery?.balances && (
39
+ <div>
40
+ <h3>Stripe balances at last expiry check</h3>
41
+ <p>
42
+ Provider balances are not a payable earnings assessment. Currencies
43
+ remain separate.
44
+ </p>
45
+ <dl>
46
+ {state.recovery.balances.map((balance) => (
47
+ <div key={balance.currency}>
48
+ <dt>{balance.currency.toUpperCase()} available</dt>
49
+ <dd>{balance.availableLabel}</dd>
50
+ <dt>{balance.currency.toUpperCase()} pending</dt>
51
+ <dd>{balance.pendingLabel}</dd>
52
+ </div>
53
+ ))}
54
+ </dl>
55
+ </div>
56
+ )}
32
57
  {state.progress && (
33
58
  <progress
34
59
  aria-label="Combined payout minimum"
@@ -102,7 +127,12 @@ export function PayoutProgress(props: PayoutProgressProps) {
102
127
  {state.holdDeadline !== null && (
103
128
  <p>Custom holding deadline: {date(state.holdDeadline)}</p>
104
129
  )}
105
- {state.observedAt !== null && <p>Assessed: {date(state.observedAt)}</p>}
130
+ {state.observedAt !== null && (
131
+ <p>
132
+ {state.recovery ? "Last recovery check" : "Assessed"}:{" "}
133
+ {date(state.observedAt)}
134
+ </p>
135
+ )}
106
136
  </section>
107
137
  );
108
138
  }
@@ -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,13 +47,28 @@ export type PayoutProgressState = {
42
47
  nextAttemptAt: number | null;
43
48
  holdDeadline: number | null;
44
49
  observedAt: number | null;
50
+ lastAttempt?: PayoutAttemptState;
51
+ recovery?: {
52
+ status: NonNullable<PayoutPolicy["holdingExpiry"]>["status"];
53
+ balances?: {
54
+ currency: string;
55
+ available: number;
56
+ pending: number;
57
+ availableLabel: string;
58
+ pendingLabel: string;
59
+ }[];
60
+ };
45
61
  };
46
62
  export type PayoutProgressProps = {
47
63
  policy:
48
- | Pick<PayoutPolicy, "mode" | "status" | "thresholds" | "holdDeadline">
64
+ | Pick<
65
+ PayoutPolicy,
66
+ "mode" | "status" | "thresholds" | "holdDeadline" | "holdingExpiry"
67
+ >
49
68
  | null
50
69
  | undefined;
51
70
  preview?: PayoutPreview | null;
71
+ lastAttempt?: PayoutAttempt | null;
52
72
  currency: string;
53
73
  /** Application worker attempt time, never a bank arrival estimate. */
54
74
  nextAttemptAt?: number | null;
@@ -83,8 +103,14 @@ export function payoutProgressState({
83
103
  preview,
84
104
  currency,
85
105
  nextAttemptAt,
106
+ lastAttempt,
107
+ timeZone,
86
108
  locale = "en-US",
87
109
  }: PayoutProgressProps): PayoutProgressState {
110
+ if (lastAttempt && lastAttempt.currency !== currency)
111
+ throw new Error(
112
+ "Payout attempt currency does not match the selected currency.",
113
+ );
88
114
  const amount = (value: number) => formatPrice(value, currency, locale);
89
115
  const state: PayoutProgressState = {
90
116
  status: "loading",
@@ -98,18 +124,52 @@ export function payoutProgressState({
98
124
  holdDeadline:
99
125
  policy?.mode === "custom" ? (policy.holdDeadline ?? null) : null,
100
126
  observedAt: null,
127
+ lastAttempt:
128
+ policy === undefined
129
+ ? undefined
130
+ : payoutAttemptState(lastAttempt, locale, timeZone),
101
131
  };
102
132
  if (policy !== undefined) {
103
133
  if (policy?.status === "changing") {
104
134
  state.status = "changing";
105
135
  state.message =
106
136
  "Payout settings are being verified. Eligibility is unavailable until the change completes.";
137
+ } else if (
138
+ policy?.holdingExpiry &&
139
+ policy.holdingExpiry.status !== "no_held_balance"
140
+ ) {
141
+ const expiry = policy.holdingExpiry;
142
+ state.status = "unavailable";
143
+ state.reason = "holding_expiry_unresolved";
144
+ state.holdDeadline = expiry.deadline;
145
+ state.observedAt = expiry.observedAt ?? null;
146
+ state.message =
147
+ "The holding deadline has passed. Schedule restoration does not confirm disbursement. " +
148
+ (expiry.status === "unverified"
149
+ ? "Stripe balance verification is unavailable. "
150
+ : "Stripe balances or payout claims still require recovery. ") +
151
+ "Contact platform support; automatic recovery checks will retry.";
152
+ state.recovery = {
153
+ status: expiry.status,
154
+ ...(expiry.balances
155
+ ? {
156
+ balances: expiry.balances.map((b) => ({
157
+ ...b,
158
+ availableLabel: formatPrice(b.available, b.currency, locale),
159
+ pendingLabel: formatPrice(b.pending, b.currency, locale),
160
+ })),
161
+ }
162
+ : {}),
163
+ };
107
164
  } else if (!policy || policy.mode === "stripe_managed") {
108
165
  state.status = "stripe_managed";
109
166
  state.message =
110
167
  "Stripe manages automatic payouts. No additional application minimum is set.";
111
168
  } else if (preview !== undefined) {
112
- state.nextAttemptAt = nextAttemptAt ?? null;
169
+ state.nextAttemptAt =
170
+ state.lastAttempt?.status === "running"
171
+ ? null
172
+ : (nextAttemptAt ?? null);
113
173
  if (!preview || !("currency" in preview)) {
114
174
  state.status = "unavailable";
115
175
  state.reason = preview?.reason ?? "assessment_unavailable";