@fantastic.dev/better-stripe 0.23.2 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/CHANGELOG.md +20 -2
  2. package/README.md +40 -5
  3. package/dist/client/affiliateRecovery/disputes.d.ts +18 -0
  4. package/dist/client/affiliateRecovery/disputes.d.ts.map +1 -0
  5. package/dist/client/affiliateRecovery/disputes.js +33 -0
  6. package/dist/client/affiliateRecovery/disputes.js.map +1 -0
  7. package/dist/client/affiliateRecovery/engine.d.ts +29 -0
  8. package/dist/client/affiliateRecovery/engine.d.ts.map +1 -0
  9. package/dist/client/affiliateRecovery/engine.js +116 -0
  10. package/dist/client/affiliateRecovery/engine.js.map +1 -0
  11. package/dist/client/affiliateRecovery/payoutGate.d.ts +24 -0
  12. package/dist/client/affiliateRecovery/payoutGate.d.ts.map +1 -0
  13. package/dist/client/affiliateRecovery/payoutGate.js +30 -0
  14. package/dist/client/affiliateRecovery/payoutGate.js.map +1 -0
  15. package/dist/client/affiliateRecovery/provider.d.ts +14 -0
  16. package/dist/client/affiliateRecovery/provider.d.ts.map +1 -0
  17. package/dist/client/affiliateRecovery/provider.js +96 -0
  18. package/dist/client/affiliateRecovery/provider.js.map +1 -0
  19. package/dist/client/affiliateRecovery/review.d.ts +28 -0
  20. package/dist/client/affiliateRecovery/review.d.ts.map +1 -0
  21. package/dist/client/affiliateRecovery/review.js +86 -0
  22. package/dist/client/affiliateRecovery/review.js.map +1 -0
  23. package/dist/client/affiliateRecovery/validators.d.ts +402 -0
  24. package/dist/client/affiliateRecovery/validators.d.ts.map +1 -0
  25. package/dist/client/affiliateRecovery/validators.js +27 -0
  26. package/dist/client/affiliateRecovery/validators.js.map +1 -0
  27. package/dist/client/affiliateRecovery.d.ts +131 -0
  28. package/dist/client/affiliateRecovery.d.ts.map +1 -0
  29. package/dist/client/affiliateRecovery.js +69 -0
  30. package/dist/client/affiliateRecovery.js.map +1 -0
  31. package/dist/client/connect/commissionReceipts.d.ts +4 -0
  32. package/dist/client/connect/commissionReceipts.d.ts.map +1 -0
  33. package/dist/client/connect/commissionReceipts.js +17 -0
  34. package/dist/client/connect/commissionReceipts.js.map +1 -0
  35. package/dist/client/connect/payoutAdoptionTestHelpers.d.ts +494 -2
  36. package/dist/client/connect/payoutAdoptionTestHelpers.d.ts.map +1 -1
  37. package/dist/client/connect/payoutSyncTestHelpers.d.ts +247 -1
  38. package/dist/client/connect/payoutSyncTestHelpers.d.ts.map +1 -1
  39. package/dist/client/connect/payouts.d.ts +3 -1
  40. package/dist/client/connect/payouts.d.ts.map +1 -1
  41. package/dist/client/connect/payouts.js +45 -12
  42. package/dist/client/connect/payouts.js.map +1 -1
  43. package/dist/client/connect/refundPrincipal.d.ts +4 -0
  44. package/dist/client/connect/refundPrincipal.d.ts.map +1 -0
  45. package/dist/client/connect/refundPrincipal.js +21 -0
  46. package/dist/client/connect/refundPrincipal.js.map +1 -0
  47. package/dist/client/connect/reversalExecution.d.ts +47 -0
  48. package/dist/client/connect/reversalExecution.d.ts.map +1 -0
  49. package/dist/client/connect/reversalExecution.js +84 -0
  50. package/dist/client/connect/reversalExecution.js.map +1 -0
  51. package/dist/client/connect/transfers.d.ts +8 -4
  52. package/dist/client/connect/transfers.d.ts.map +1 -1
  53. package/dist/client/connect/transfers.js +52 -57
  54. package/dist/client/connect/transfers.js.map +1 -1
  55. package/dist/client/index.d.ts +3 -0
  56. package/dist/client/index.d.ts.map +1 -1
  57. package/dist/client/index.js +6 -1
  58. package/dist/client/index.js.map +1 -1
  59. package/dist/client/settlement/execution.d.ts.map +1 -1
  60. package/dist/client/settlement/execution.js +30 -3
  61. package/dist/client/settlement/execution.js.map +1 -1
  62. package/dist/client/settlement/reconciliation.d.ts.map +1 -1
  63. package/dist/client/settlement/reconciliation.js +16 -2
  64. package/dist/client/settlement/reconciliation.js.map +1 -1
  65. package/dist/client/webhooks/disputeRecovery.d.ts +4 -0
  66. package/dist/client/webhooks/disputeRecovery.d.ts.map +1 -0
  67. package/dist/client/webhooks/disputeRecovery.js +40 -0
  68. package/dist/client/webhooks/disputeRecovery.js.map +1 -0
  69. package/dist/client/webhooks/processors.d.ts.map +1 -1
  70. package/dist/client/webhooks/processors.js +14 -29
  71. package/dist/client/webhooks/processors.js.map +1 -1
  72. package/dist/component/_generated/component.d.ts +491 -0
  73. package/dist/component/_generated/component.d.ts.map +1 -1
  74. package/dist/component/affiliateEarnings/mutations.d.ts +22 -0
  75. package/dist/component/affiliateEarnings/mutations.d.ts.map +1 -0
  76. package/dist/component/affiliateEarnings/mutations.js +160 -0
  77. package/dist/component/affiliateEarnings/mutations.js.map +1 -0
  78. package/dist/component/affiliatePayouts/mutations.d.ts +25 -0
  79. package/dist/component/affiliatePayouts/mutations.d.ts.map +1 -0
  80. package/dist/component/affiliatePayouts/mutations.js +104 -0
  81. package/dist/component/affiliatePayouts/mutations.js.map +1 -0
  82. package/dist/component/affiliatePayouts/queries.d.ts +23 -0
  83. package/dist/component/affiliatePayouts/queries.d.ts.map +1 -0
  84. package/dist/component/affiliatePayouts/queries.js +23 -0
  85. package/dist/component/affiliatePayouts/queries.js.map +1 -0
  86. package/dist/component/affiliatePayouts/recipientGuard.d.ts +12 -0
  87. package/dist/component/affiliatePayouts/recipientGuard.d.ts.map +1 -0
  88. package/dist/component/affiliatePayouts/recipientGuard.js +26 -0
  89. package/dist/component/affiliatePayouts/recipientGuard.js.map +1 -0
  90. package/dist/component/affiliateRecovery/accounting.d.ts +28 -0
  91. package/dist/component/affiliateRecovery/accounting.d.ts.map +1 -0
  92. package/dist/component/affiliateRecovery/accounting.js +71 -0
  93. package/dist/component/affiliateRecovery/accounting.js.map +1 -0
  94. package/dist/component/affiliateRecovery/mutations.d.ts +65 -0
  95. package/dist/component/affiliateRecovery/mutations.d.ts.map +1 -0
  96. package/dist/component/affiliateRecovery/mutations.js +250 -0
  97. package/dist/component/affiliateRecovery/mutations.js.map +1 -0
  98. package/dist/component/affiliateRecovery/queries.d.ts +305 -0
  99. package/dist/component/affiliateRecovery/queries.d.ts.map +1 -0
  100. package/dist/component/affiliateRecovery/queries.js +113 -0
  101. package/dist/component/affiliateRecovery/queries.js.map +1 -0
  102. package/dist/component/affiliateRecovery/schema.d.ts +171 -0
  103. package/dist/component/affiliateRecovery/schema.d.ts.map +1 -0
  104. package/dist/component/affiliateRecovery/schema.js +71 -0
  105. package/dist/component/affiliateRecovery/schema.js.map +1 -0
  106. package/dist/component/connect/mutations.d.ts +36 -23
  107. package/dist/component/connect/mutations.d.ts.map +1 -1
  108. package/dist/component/connect/mutations.js +7 -124
  109. package/dist/component/connect/mutations.js.map +1 -1
  110. package/dist/component/connect/queries.d.ts +10 -0
  111. package/dist/component/connect/queries.d.ts.map +1 -1
  112. package/dist/component/connect/queries.js +3 -40
  113. package/dist/component/connect/queries.js.map +1 -1
  114. package/dist/component/connect/reversalClaims.d.ts +169 -0
  115. package/dist/component/connect/reversalClaims.d.ts.map +1 -0
  116. package/dist/component/connect/reversalClaims.js +176 -0
  117. package/dist/component/connect/reversalClaims.js.map +1 -0
  118. package/dist/component/connect/reversalDiagnostics.d.ts +59 -0
  119. package/dist/component/connect/reversalDiagnostics.d.ts.map +1 -0
  120. package/dist/component/connect/reversalDiagnostics.js +50 -0
  121. package/dist/component/connect/reversalDiagnostics.js.map +1 -0
  122. package/dist/component/connect/reversalReceipts.d.ts +27 -0
  123. package/dist/component/connect/reversalReceipts.d.ts.map +1 -0
  124. package/dist/component/connect/reversalReceipts.js +85 -0
  125. package/dist/component/connect/reversalReceipts.js.map +1 -0
  126. package/dist/component/connect/schema.d.ts +31 -1
  127. package/dist/component/connect/schema.d.ts.map +1 -1
  128. package/dist/component/connect/schema.js +3 -1
  129. package/dist/component/connect/schema.js.map +1 -1
  130. package/dist/component/connect/validators.d.ts +49 -1
  131. package/dist/component/connect/validators.d.ts.map +1 -1
  132. package/dist/component/connect/validators.js +2 -0
  133. package/dist/component/connect/validators.js.map +1 -1
  134. package/dist/component/earnings/triggers.d.ts +410 -2
  135. package/dist/component/earnings/triggers.d.ts.map +1 -1
  136. package/dist/component/payoutRelease/mutations.d.ts.map +1 -1
  137. package/dist/component/payoutRelease/mutations.js +6 -0
  138. package/dist/component/payoutRelease/mutations.js.map +1 -1
  139. package/dist/component/payoutRelease/queries.d.ts.map +1 -1
  140. package/dist/component/payoutRelease/queries.js +3 -1
  141. package/dist/component/payoutRelease/queries.js.map +1 -1
  142. package/dist/component/schema.d.ts +247 -1
  143. package/dist/component/schema.js +9 -0
  144. package/dist/component/schema.js.map +1 -1
  145. package/dist/component/settlement/clawbackSchema.d.ts +161 -0
  146. package/dist/component/settlement/clawbackSchema.d.ts.map +1 -0
  147. package/dist/component/settlement/clawbackSchema.js +73 -0
  148. package/dist/component/settlement/clawbackSchema.js.map +1 -0
  149. package/dist/component/settlement/clawbacks.d.ts +55 -0
  150. package/dist/component/settlement/clawbacks.d.ts.map +1 -0
  151. package/dist/component/settlement/clawbacks.js +146 -0
  152. package/dist/component/settlement/clawbacks.js.map +1 -0
  153. package/dist/component/settlement/mutations.d.ts +16 -0
  154. package/dist/component/settlement/mutations.d.ts.map +1 -1
  155. package/dist/component/settlement/mutations.js +48 -0
  156. package/dist/component/settlement/mutations.js.map +1 -1
  157. package/dist/component/settlement/queries.d.ts +53 -0
  158. package/dist/component/settlement/queries.d.ts.map +1 -1
  159. package/dist/component/settlement/queries.js +26 -0
  160. package/dist/component/settlement/queries.js.map +1 -1
  161. package/dist/react/workflows/AffiliateRecovery.d.ts +46 -0
  162. package/dist/react/workflows/AffiliateRecovery.d.ts.map +1 -0
  163. package/dist/react/workflows/AffiliateRecovery.js +84 -0
  164. package/dist/react/workflows/AffiliateRecovery.js.map +1 -0
  165. package/dist/react/workflows/index.d.ts +2 -0
  166. package/dist/react/workflows/index.d.ts.map +1 -1
  167. package/dist/react/workflows/index.js +1 -0
  168. package/dist/react/workflows/index.js.map +1 -1
  169. package/dist/testing/affiliate-recovery.d.ts +10 -0
  170. package/dist/testing/affiliate-recovery.d.ts.map +1 -0
  171. package/dist/testing/affiliate-recovery.js +126 -0
  172. package/dist/testing/affiliate-recovery.js.map +1 -0
  173. package/dist/testing/index.d.ts +2 -0
  174. package/dist/testing/index.d.ts.map +1 -1
  175. package/dist/testing/index.js +1 -0
  176. package/dist/testing/index.js.map +1 -1
  177. package/docs/affiliate-clawbacks.md +306 -0
  178. package/docs/settlement.md +17 -0
  179. package/package.json +2 -1
  180. package/src/client/affiliateRecovery/disputes.ts +62 -0
  181. package/src/client/affiliateRecovery/engine.ts +196 -0
  182. package/src/client/affiliateRecovery/payoutGate.ts +65 -0
  183. package/src/client/affiliateRecovery/provider.ts +127 -0
  184. package/src/client/affiliateRecovery/review.ts +117 -0
  185. package/src/client/affiliateRecovery/validators.ts +38 -0
  186. package/src/client/affiliateRecovery.ts +173 -0
  187. package/src/client/connect/commissionReceipts.test.ts +157 -0
  188. package/src/client/connect/commissionReceipts.ts +23 -0
  189. package/src/client/connect/payouts.ts +75 -15
  190. package/src/client/connect/refundPrincipal.test.ts +46 -0
  191. package/src/client/connect/refundPrincipal.ts +26 -0
  192. package/src/client/connect/reversalExecution.ts +130 -0
  193. package/src/client/connect/transfers.test.ts +3 -3
  194. package/src/client/connect/transfers.ts +93 -92
  195. package/src/client/index.ts +6 -7
  196. package/src/client/settlement/execution.test.ts +5 -0
  197. package/src/client/settlement/execution.ts +50 -3
  198. package/src/client/settlement/reconciliation.ts +20 -2
  199. package/src/client/webhooks/disputeRecovery.ts +74 -0
  200. package/src/client/webhooks/processors.test.ts +36 -53
  201. package/src/client/webhooks/processors.ts +18 -31
  202. package/src/component/_generated/api.ts +24 -0
  203. package/src/component/_generated/component.ts +614 -1
  204. package/src/component/affiliate-clawbacks.test.ts +370 -0
  205. package/src/component/affiliate-component-pagination.test.ts +83 -0
  206. package/src/component/affiliate-direct-payout.test.ts +160 -0
  207. package/src/component/affiliate-recovery-lifecycle.test.ts +587 -0
  208. package/src/component/affiliateEarnings/mutations.ts +196 -0
  209. package/src/component/affiliatePayouts/mutations.ts +121 -0
  210. package/src/component/affiliatePayouts/queries.ts +28 -0
  211. package/src/component/affiliatePayouts/recipientGuard.ts +42 -0
  212. package/src/component/affiliateRecovery/accounting.ts +111 -0
  213. package/src/component/affiliateRecovery/mutations.ts +275 -0
  214. package/src/component/affiliateRecovery/queries.ts +138 -0
  215. package/src/component/affiliateRecovery/schema.ts +86 -0
  216. package/src/component/connect/mutations.ts +9 -160
  217. package/src/component/connect/queries.ts +4 -47
  218. package/src/component/connect/reversalClaims.ts +232 -0
  219. package/src/component/connect/reversalDiagnostics.ts +61 -0
  220. package/src/component/connect/reversalReceipts.ts +147 -0
  221. package/src/component/connect/schema.ts +3 -3
  222. package/src/component/connect/validators.ts +2 -0
  223. package/src/component/payout-recovery-integration.test.ts +172 -0
  224. package/src/component/payoutRelease/mutations.ts +9 -0
  225. package/src/component/payoutRelease/queries.ts +3 -1
  226. package/src/component/schema.ts +18 -0
  227. package/src/component/settlement/clawbackSchema.ts +78 -0
  228. package/src/component/settlement/clawbacks.ts +233 -0
  229. package/src/component/settlement/mutations.ts +54 -0
  230. package/src/component/settlement/queries.ts +35 -0
  231. package/src/component/summary-arithmetic.test.ts +9 -4
  232. package/src/react/workflows/AffiliateRecovery.test.tsx +106 -0
  233. package/src/react/workflows/AffiliateRecovery.tsx +273 -0
  234. package/src/react/workflows/index.ts +9 -0
  235. package/src/testing/affiliate-recovery.ts +137 -0
  236. package/src/testing/index.ts +2 -0
@@ -103,15 +103,20 @@ it("rolls back subtract/reapply updates that would overflow", async () => {
103
103
  ).toMatchObject({ totals: [{ gross: max, transferCount: 2 }] });
104
104
  });
105
105
 
106
- it("rolls back a reversal whose cumulative summary would overflow", async () => {
106
+ it("rolls back a reversal when a legacy cumulative summary would overflow", async () => {
107
107
  const t = convexTest(schema, modules);
108
108
  await t.mutation(api.connect.mutations.upsertTransfer, {
109
109
  ...transfer("tr_a", 1),
110
- reversedAmount: max - 1,
110
+ reversedAmount: 0,
111
111
  });
112
112
  await t.mutation(api.connect.mutations.upsertTransfer, {
113
113
  ...transfer("tr_b", 1),
114
- reversedAmount: 1,
114
+ reversedAmount: 0,
115
+ });
116
+ // Imported legacy totals can be inconsistent even when the new receipt is valid.
117
+ await t.run(async (ctx) => {
118
+ const totals = (await ctx.db.query("currencyTotals").first())!;
119
+ await ctx.db.patch("currencyTotals", totals._id, { reversed: max });
115
120
  });
116
121
  const before = await t.query(
117
122
  api.connect.queries.getAccountEarningsByCurrency,
@@ -120,7 +125,7 @@ it("rolls back a reversal whose cumulative summary would overflow", async () =>
120
125
  await expect(
121
126
  t.mutation(api.connect.mutations.recordTransferReversal, {
122
127
  stripeTransferId: "tr_b",
123
- reversedAmount: 2,
128
+ reversedAmount: 1,
124
129
  }),
125
130
  ).rejects.toThrow(/safe integer/i);
126
131
  expect(
@@ -0,0 +1,106 @@
1
+ // @vitest-environment jsdom
2
+ import {
3
+ cleanup,
4
+ render,
5
+ screen,
6
+ fireEvent,
7
+ waitFor,
8
+ } from "@testing-library/react";
9
+ import { afterEach, expect, it, vi } from "vitest";
10
+ import {
11
+ AffiliateRecovery,
12
+ AffiliateRecoveryHistory,
13
+ } from "./AffiliateRecovery.js";
14
+ import { affiliateRecoveryScenarios } from "../../testing/affiliate-recovery.js";
15
+ import type { AffiliateRecoveryOperation } from "../../client/affiliateRecovery.js";
16
+ afterEach(cleanup);
17
+ it("shows exact recovered, debt and withheld amounts and labels simulated data separately", () => {
18
+ const fixture = affiliateRecoveryScenarios().find(
19
+ (r) => r.id === "paid-out",
20
+ )!;
21
+ expect(fixture.evidence).toBe("simulated");
22
+ render(<AffiliateRecovery snapshot={fixture.snapshot} currency="usd" />);
23
+ expect(screen.getAllByText("$10.01").length).toBeGreaterThan(1);
24
+ expect(screen.getByText("insufficient_balance")).toBeDefined();
25
+ expect(screen.getByText("Outstanding debt")).toBeDefined();
26
+ expect(screen.getByText("Withheld earnings")).toBeDefined();
27
+ });
28
+ it("exposes identical formatted metrics through the headless render API", () => {
29
+ const fixture = affiliateRecoveryScenarios().find((r) => r.id === "overlap")!;
30
+ let metrics: { key: string; formattedValue: string }[] = [];
31
+ render(
32
+ <AffiliateRecovery snapshot={fixture.snapshot} currency="usd">
33
+ {(state) => {
34
+ metrics = state.metrics;
35
+ return <p>{state.rows[0].sourceChargeId}</p>;
36
+ }}
37
+ </AffiliateRecovery>,
38
+ );
39
+ expect(metrics.find((r) => r.key === "recoveredAmount")!.formattedValue).toBe(
40
+ "$10.01",
41
+ );
42
+ expect(metrics.find((r) => r.key === "restoredAmount")!.formattedValue).toBe(
43
+ "$5.00",
44
+ );
45
+ expect(metrics.find((r) => r.key === "outstandingDebt")!.formattedValue).toBe(
46
+ "$0.00",
47
+ );
48
+ });
49
+ it("provides an operator reconcile action and visible provider errors without claiming success", async () => {
50
+ const fixture = affiliateRecoveryScenarios().find((r) => r.id === "unknown")!;
51
+ const retry = vi
52
+ .fn()
53
+ .mockRejectedValue(new Error("Provider outcome is unknown"));
54
+ render(
55
+ <AffiliateRecovery
56
+ snapshot={fixture.snapshot}
57
+ currency="usd"
58
+ onRetry={retry}
59
+ />,
60
+ );
61
+ fireEvent.click(screen.getByRole("button", { name: "Reconcile" }));
62
+ await waitFor(() =>
63
+ expect(screen.getByRole("alert").textContent).toBe(
64
+ "Provider outcome is unknown",
65
+ ),
66
+ );
67
+ expect(retry).toHaveBeenCalledWith(fixture.snapshot.obligations.page[0]._id);
68
+ });
69
+ it("shows authentication, pagination and unconfirmed receipt states", () => {
70
+ const { rerender } = render(
71
+ <AffiliateRecovery snapshot={null} currency="usd" />,
72
+ );
73
+ expect(
74
+ screen.getByText("Sign in to view your commission recovery."),
75
+ ).toBeDefined();
76
+ const fixture = affiliateRecoveryScenarios()[0];
77
+ rerender(
78
+ <AffiliateRecovery
79
+ snapshot={{
80
+ ...fixture.snapshot,
81
+ obligations: { ...fixture.snapshot.obligations, isDone: false },
82
+ }}
83
+ currency="usd"
84
+ />,
85
+ );
86
+ expect(screen.getByText(/More adjustments are available/)).toBeDefined();
87
+ rerender(
88
+ <AffiliateRecoveryHistory
89
+ currency="usd"
90
+ operations={[
91
+ {
92
+ _id: "fixture_op",
93
+ kind: "restoration",
94
+ amount: 501,
95
+ state: "unknown",
96
+ attempted: true,
97
+ createdAt: 0,
98
+ _creationTime: 0,
99
+ obligationId: "fixture_debt",
100
+ } as AffiliateRecoveryOperation,
101
+ ]}
102
+ />,
103
+ );
104
+ expect(screen.getByText("Unconfirmed")).toBeDefined();
105
+ expect(screen.getByText("unknown")).toBeDefined();
106
+ });
@@ -0,0 +1,273 @@
1
+ "use client";
2
+ import type { ReactNode } from "react";
3
+ import type {
4
+ AffiliateRecoverySnapshot,
5
+ AffiliateRecoveryObligation,
6
+ AffiliateRecoveryOperation,
7
+ } from "../../client/affiliateRecovery.js";
8
+ import { formatPrice } from "../lib/price-helpers.js";
9
+ import { useWorkflowAction } from "./useWorkflowAction.js";
10
+ export type RecoveryMetric = {
11
+ key: string;
12
+ label: string;
13
+ amount: number;
14
+ formattedValue: string;
15
+ };
16
+ export type RecoveryAdjustment = {
17
+ row: AffiliateRecoveryObligation;
18
+ source: string;
19
+ commission: string;
20
+ recovered: string;
21
+ debt: string;
22
+ status: string;
23
+ };
24
+ export type AffiliateRecoveryView = {
25
+ adjustments: RecoveryAdjustment[];
26
+ metrics: RecoveryMetric[];
27
+ rows: AffiliateRecoveryObligation[];
28
+ currency: string;
29
+ isLoading: boolean;
30
+ isComplete: boolean;
31
+ isPending: boolean;
32
+ error: string | null;
33
+ retry: (id: string) => Promise<unknown>;
34
+ };
35
+ export type AffiliateRecoveryProps = {
36
+ snapshot: AffiliateRecoverySnapshot | undefined | null;
37
+ currency: string;
38
+ showMetrics?: boolean;
39
+ locale?: string;
40
+ className?: string;
41
+ onRetry?: (obligationId: string) => Promise<unknown>;
42
+ onInspect?: (obligationId: string) => void;
43
+ sourceHref?: (row: AffiliateRecoveryObligation) => string | undefined;
44
+ children?: (state: AffiliateRecoveryView) => ReactNode;
45
+ };
46
+ /** Reusable, unstyled recovery state for earnings, referred sales and payouts. */
47
+ export function AffiliateRecovery({
48
+ snapshot,
49
+ showMetrics = true,
50
+ currency,
51
+ locale,
52
+ className,
53
+ onRetry,
54
+ onInspect,
55
+ sourceHref,
56
+ children,
57
+ }: AffiliateRecoveryProps) {
58
+ const action = useWorkflowAction(
59
+ async (id: string) => onRetry?.(id),
60
+ `affiliate-recovery:${currency}`,
61
+ );
62
+ const balance = snapshot?.balance;
63
+ if (
64
+ (balance && balance.currency !== currency) ||
65
+ snapshot?.obligations.page.some((row) => row.currency !== currency)
66
+ )
67
+ throw new Error("Affiliate recovery display cannot mix currencies.");
68
+ const definitions = [
69
+ ["originalCommission", "Original commission"],
70
+ ["recoveredAmount", "Reversed"],
71
+ ["outstandingDebt", "Outstanding debt"],
72
+ ["offsetAmount", "Recovered from future earnings"],
73
+ ["withheldAmount", "Withheld earnings"],
74
+ ["reservedRecovery", "Pending recovery"],
75
+ ["reservedRestoration", "Pending restoration"],
76
+ ["restoredAmount", "Restored"],
77
+ ["restorationDue", "Restoration owed"],
78
+ ["netPayable", "Net payable"],
79
+ ] as const;
80
+ const metrics = definitions.map(([key, label]) => {
81
+ const amount = balance?.[key] ?? 0;
82
+ return {
83
+ key,
84
+ label,
85
+ amount,
86
+ formattedValue: formatPrice(amount, currency, locale),
87
+ };
88
+ });
89
+ const adjustments = (snapshot?.obligations.page ?? []).map((row) => ({
90
+ row,
91
+ source: [
92
+ row.sourceChargeId,
93
+ row.storeAccountId,
94
+ row.sourceInvoiceId,
95
+ row.paymentId,
96
+ row.lastRefundId,
97
+ row.disputeId,
98
+ ]
99
+ .filter(Boolean)
100
+ .join(" · "),
101
+ commission: formatPrice(row.originalCommission, row.currency, locale),
102
+ recovered: formatPrice(
103
+ row.recoveredAmount + (row.offsetAmount ?? 0) - (row.restoredAmount ?? 0),
104
+ row.currency,
105
+ locale,
106
+ ),
107
+ debt: formatPrice(row.debtAmount ?? 0, row.currency, locale),
108
+ status: row.needsReconciliation
109
+ ? "Operator reconciliation required"
110
+ : (row.lastState ?? "Settled"),
111
+ }));
112
+ const state: AffiliateRecoveryView = {
113
+ adjustments,
114
+ metrics,
115
+ rows: snapshot?.obligations.page ?? [],
116
+ currency,
117
+ isLoading: snapshot === undefined,
118
+ isComplete: snapshot?.obligations.isDone ?? false,
119
+ isPending: action.isPending,
120
+ error: action.error,
121
+ retry: action.run,
122
+ };
123
+ if (children) return <>{children(state)}</>;
124
+ return (
125
+ <section className={className} data-better-stripe="affiliate-recovery">
126
+ <h2>Commission recovery ({currency.toUpperCase()})</h2>
127
+ {state.isLoading ? (
128
+ <p role="status">Loading commission recovery…</p>
129
+ ) : snapshot === null ? (
130
+ <p>Sign in to view your commission recovery.</p>
131
+ ) : (
132
+ <>
133
+ {showMetrics && (
134
+ <dl>
135
+ {metrics.map((metric) => (
136
+ <div key={metric.key}>
137
+ <dt>{metric.label}</dt>
138
+ <dd>{metric.formattedValue}</dd>
139
+ </div>
140
+ ))}
141
+ </dl>
142
+ )}
143
+ {state.error && <p role="alert">{state.error}</p>}
144
+ {!state.rows.length && <p>No commission adjustments yet.</p>}
145
+ <table>
146
+ <caption>Commission adjustments</caption>
147
+ <thead>
148
+ <tr>
149
+ <th scope="col">Original sale</th>
150
+ <th scope="col">Commission</th>
151
+ <th scope="col">Recovered</th>
152
+ <th scope="col">Debt</th>
153
+ <th scope="col">State</th>
154
+ {(onRetry || onInspect) && <th scope="col">Action</th>}
155
+ </tr>
156
+ </thead>
157
+ <tbody>
158
+ {state.rows.map((row) => (
159
+ <tr key={row._id}>
160
+ <td>
161
+ {sourceHref?.(row) ? (
162
+ <a href={sourceHref(row)}>{row.sourceChargeId}</a>
163
+ ) : (
164
+ row.sourceChargeId
165
+ )}
166
+ <br />
167
+ {[
168
+ row.storeAccountId,
169
+ row.sourceInvoiceId,
170
+ row.paymentId,
171
+ row.lastRefundId,
172
+ row.disputeId,
173
+ ]
174
+ .filter(Boolean)
175
+ .join(" · ")}
176
+ </td>
177
+ <td>
178
+ {formatPrice(row.originalCommission, row.currency, locale)}
179
+ </td>
180
+ <td>
181
+ {formatPrice(
182
+ row.recoveredAmount +
183
+ (row.offsetAmount ?? 0) -
184
+ (row.restoredAmount ?? 0),
185
+ row.currency,
186
+ locale,
187
+ )}
188
+ </td>
189
+ <td>
190
+ {formatPrice(row.debtAmount ?? 0, row.currency, locale)}
191
+ </td>
192
+ <td>
193
+ {row.needsReconciliation
194
+ ? "Operator reconciliation required"
195
+ : (row.lastState ?? "Settled")}
196
+ {row.lastError && <p>{row.lastError}</p>}
197
+ </td>
198
+ {(onRetry || onInspect) && (
199
+ <td>
200
+ {onInspect && (
201
+ <button
202
+ type="button"
203
+ onClick={() => onInspect(row._id)}
204
+ >
205
+ Inspect attempts
206
+ </button>
207
+ )}
208
+ {onRetry && (
209
+ <button
210
+ type="button"
211
+ disabled={state.isPending}
212
+ onClick={() => {
213
+ void state.retry(row._id);
214
+ }}
215
+ >
216
+ Reconcile
217
+ </button>
218
+ )}
219
+ </td>
220
+ )}
221
+ </tr>
222
+ ))}
223
+ </tbody>
224
+ </table>
225
+ {!state.isComplete && (
226
+ <p>
227
+ More adjustments are available. Totals include the complete
228
+ account ledger.
229
+ </p>
230
+ )}
231
+ </>
232
+ )}
233
+ </section>
234
+ );
235
+ }
236
+ export function AffiliateRecoveryHistory({
237
+ operations,
238
+ currency,
239
+ locale,
240
+ }: {
241
+ operations: AffiliateRecoveryOperation[];
242
+ currency: string;
243
+ locale?: string;
244
+ }) {
245
+ return (
246
+ <table data-better-stripe="affiliate-recovery-history">
247
+ <caption>Recovery receipts and attempts</caption>
248
+ <thead>
249
+ <tr>
250
+ <th scope="col">Operation</th>
251
+ <th scope="col">Amount</th>
252
+ <th scope="col">Outcome</th>
253
+ <th scope="col">Receipt</th>
254
+ </tr>
255
+ </thead>
256
+ <tbody>
257
+ {operations.map((op) => (
258
+ <tr key={op._id}>
259
+ <td>
260
+ {op.kind} · {op.sourceEventId ?? op._id}
261
+ </td>
262
+ <td>{formatPrice(op.amount, currency, locale)}</td>
263
+ <td>
264
+ {op.state}
265
+ {op.error && <p>{op.error}</p>}
266
+ </td>
267
+ <td>{op.receiptId ?? "Unconfirmed"}</td>
268
+ </tr>
269
+ ))}
270
+ </tbody>
271
+ </table>
272
+ );
273
+ }
@@ -265,6 +265,15 @@ export type {
265
265
  AffiliatePayoutEditorState,
266
266
  AffiliatePayoutUpdate,
267
267
  } from "./AffiliatePayoutEditor.js";
268
+ export {
269
+ AffiliateRecovery,
270
+ AffiliateRecoveryHistory,
271
+ } from "./AffiliateRecovery.js";
272
+ export type {
273
+ AffiliateRecoveryProps,
274
+ AffiliateRecoveryView,
275
+ RecoveryMetric,
276
+ } from "./AffiliateRecovery.js";
268
277
 
269
278
  export { EvidenceUpload } from "./EvidenceUpload.js";
270
279
  export type { EvidenceUploadProps } from "./EvidenceUpload.js";
@@ -0,0 +1,137 @@
1
+ import type { AffiliateRecoverySnapshot } from "../client/affiliateRecovery.js";
2
+ export type AffiliateRecoveryScenario = {
3
+ id: string;
4
+ label: string;
5
+ evidence: "simulated";
6
+ snapshot: AffiliateRecoverySnapshot;
7
+ };
8
+ /** Presentation fixtures only. These IDs are not Stripe or bank-payout receipts. */
9
+ export function affiliateRecoveryScenarios(): AffiliateRecoveryScenario[] {
10
+ const cases = [
11
+ {
12
+ id: "one-time-refund",
13
+ label: "One-time full refund",
14
+ finalized: 1001,
15
+ recovered: 1001,
16
+ },
17
+ {
18
+ id: "invoice-refund",
19
+ label: "Recurring invoice refund",
20
+ finalized: 1001,
21
+ recovered: 1001,
22
+ invoice: "fixture_invoice_renewal",
23
+ },
24
+ {
25
+ id: "partial-refund",
26
+ label: "Partial refund and rounding",
27
+ finalized: 501,
28
+ recovered: 501,
29
+ },
30
+ {
31
+ id: "open-dispute",
32
+ label: "Open monetary dispute",
33
+ finalized: 0,
34
+ provisional: 1001,
35
+ recovered: 1001,
36
+ },
37
+ {
38
+ id: "lost-dispute",
39
+ label: "Lost dispute finalized",
40
+ finalized: 1001,
41
+ recovered: 1001,
42
+ },
43
+ {
44
+ id: "won-dispute",
45
+ label: "Won dispute restored",
46
+ finalized: 0,
47
+ recovered: 1001,
48
+ restored: 1001,
49
+ },
50
+ {
51
+ id: "overlap",
52
+ label: "Refund retained after dispute win",
53
+ finalized: 501,
54
+ recovered: 1001,
55
+ restored: 500,
56
+ },
57
+ {
58
+ id: "paid-out",
59
+ label: "Insufficient balance after external payout",
60
+ finalized: 1001,
61
+ recovered: 0,
62
+ state: "insufficient_balance",
63
+ },
64
+ {
65
+ id: "future-offset",
66
+ label: "Debt recovered from future commission",
67
+ finalized: 1001,
68
+ recovered: 0,
69
+ offset: 1001,
70
+ },
71
+ {
72
+ id: "unknown",
73
+ label: "Provider timeout awaiting receipt",
74
+ finalized: 1001,
75
+ recovered: 0,
76
+ reserved: 1001,
77
+ state: "unknown",
78
+ },
79
+ ];
80
+ return cases.map((c) => {
81
+ const recoveredAmount = c.recovered,
82
+ offsetAmount = c.offset ?? 0,
83
+ restoredAmount = c.restored ?? 0;
84
+ const debt = Math.max(
85
+ 0,
86
+ c.finalized +
87
+ (c.provisional ?? 0) -
88
+ recoveredAmount -
89
+ offsetAmount +
90
+ restoredAmount,
91
+ );
92
+ const row = {
93
+ _id: `fixture_${c.id}`,
94
+ _creationTime: 0,
95
+ sourceChargeId: `fixture_charge_${c.id}`,
96
+ sourceInvoiceId: c.invoice,
97
+ paymentIntentId: "fixture_payment",
98
+ affiliateAccountId: "fixture_affiliate",
99
+ currency: "usd",
100
+ chargeAmount: 10000,
101
+ originalCommission: 1001,
102
+ refundedPrincipal: c.finalized ? 10000 : 0,
103
+ disputedPrincipal: c.provisional ? 10000 : 0,
104
+ lostPrincipal: 0,
105
+ finalizedAmount: c.finalized,
106
+ provisionalAmount: c.provisional ?? 0,
107
+ recoveredAmount,
108
+ offsetAmount,
109
+ restoredAmount,
110
+ reservedRecovery: c.reserved ?? 0,
111
+ debtAmount: debt,
112
+ creditAmount: 0,
113
+ needsReconciliation: false,
114
+ lastState: c.state ?? "succeeded",
115
+ } as AffiliateRecoverySnapshot["obligations"]["page"][number];
116
+ const snapshot = {
117
+ balance: {
118
+ _id: `fixture_balance_${c.id}`,
119
+ _creationTime: 0,
120
+ affiliateAccountId: "fixture_affiliate",
121
+ currency: "usd",
122
+ originalCommission: 1001,
123
+ recoveredAmount,
124
+ offsetAmount,
125
+ restoredAmount,
126
+ outstandingDebt: debt,
127
+ reservedRecovery: c.reserved ?? 0,
128
+ reservedRestoration: 0,
129
+ restorationDue: 0,
130
+ withheldAmount: offsetAmount,
131
+ netPayable: 0,
132
+ },
133
+ obligations: { page: [row], continueCursor: "", isDone: true },
134
+ } as AffiliateRecoverySnapshot;
135
+ return { id: c.id, label: c.label, evidence: "simulated", snapshot };
136
+ });
137
+ }
@@ -11,3 +11,5 @@ export {
11
11
  mockAccountUpdated,
12
12
  mockInvoicePaid,
13
13
  } from "./mock-webhooks.js";
14
+ export { affiliateRecoveryScenarios } from "./affiliate-recovery.js";
15
+ export type { AffiliateRecoveryScenario } from "./affiliate-recovery.js";