@fantastic.dev/better-stripe 0.15.0 → 0.17.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 (127) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/MIGRATION.md +6 -0
  3. package/README.md +15 -1
  4. package/dist/client/connect/payoutSnapshot.d.ts +2 -0
  5. package/dist/client/connect/payoutSnapshot.d.ts.map +1 -1
  6. package/dist/client/connect/payoutSnapshot.js +2 -0
  7. package/dist/client/connect/payoutSnapshot.js.map +1 -1
  8. package/dist/client/connect/refunds.d.ts +12 -0
  9. package/dist/client/connect/refunds.d.ts.map +1 -1
  10. package/dist/client/connect/refunds.js +8 -0
  11. package/dist/client/connect/refunds.js.map +1 -1
  12. package/dist/client/index.d.ts +8 -10
  13. package/dist/client/index.d.ts.map +1 -1
  14. package/dist/client/index.js +6 -3
  15. package/dist/client/index.js.map +1 -1
  16. package/dist/client/readContracts.d.ts +50 -0
  17. package/dist/client/readContracts.d.ts.map +1 -0
  18. package/dist/client/readContracts.js +24 -0
  19. package/dist/client/readContracts.js.map +1 -0
  20. package/dist/client/types/documents.d.ts +2 -0
  21. package/dist/client/types/documents.d.ts.map +1 -1
  22. package/dist/client/webhooks/processors.js +2 -0
  23. package/dist/client/webhooks/processors.js.map +1 -1
  24. package/dist/component/_generated/component.d.ts +213 -0
  25. package/dist/component/_generated/component.d.ts.map +1 -1
  26. package/dist/component/connect/mutations.d.ts +2 -0
  27. package/dist/component/connect/mutations.d.ts.map +1 -1
  28. package/dist/component/connect/mutations.js +8 -8
  29. package/dist/component/connect/mutations.js.map +1 -1
  30. package/dist/component/connect/queries.d.ts +102 -2
  31. package/dist/component/connect/queries.d.ts.map +1 -1
  32. package/dist/component/connect/queries.js +88 -45
  33. package/dist/component/connect/queries.js.map +1 -1
  34. package/dist/component/connect/schema.d.ts +14 -2
  35. package/dist/component/connect/schema.d.ts.map +1 -1
  36. package/dist/component/connect/schema.js +19 -1
  37. package/dist/component/connect/schema.js.map +1 -1
  38. package/dist/component/connect/validators.d.ts +13 -2
  39. package/dist/component/connect/validators.d.ts.map +1 -1
  40. package/dist/component/connect/validators.js +4 -0
  41. package/dist/component/connect/validators.js.map +1 -1
  42. package/dist/component/earnings/legacyReads.d.ts +51 -0
  43. package/dist/component/earnings/legacyReads.d.ts.map +1 -0
  44. package/dist/component/earnings/legacyReads.js +43 -0
  45. package/dist/component/earnings/legacyReads.js.map +1 -0
  46. package/dist/component/earnings/mutations.d.ts +9 -0
  47. package/dist/component/earnings/mutations.d.ts.map +1 -0
  48. package/dist/component/earnings/mutations.js +23 -0
  49. package/dist/component/earnings/mutations.js.map +1 -0
  50. package/dist/component/earnings/reads.d.ts +14 -0
  51. package/dist/component/earnings/reads.d.ts.map +1 -0
  52. package/dist/component/earnings/reads.js +28 -0
  53. package/dist/component/earnings/reads.js.map +1 -0
  54. package/dist/component/earnings/schema.d.ts +20 -0
  55. package/dist/component/earnings/schema.d.ts.map +1 -0
  56. package/dist/component/earnings/schema.js +12 -0
  57. package/dist/component/earnings/schema.js.map +1 -0
  58. package/dist/component/earnings/triggers.d.ts +1294 -0
  59. package/dist/component/earnings/triggers.d.ts.map +1 -0
  60. package/dist/component/earnings/triggers.js +68 -0
  61. package/dist/component/earnings/triggers.js.map +1 -0
  62. package/dist/component/products/queries.d.ts +47 -0
  63. package/dist/component/products/queries.d.ts.map +1 -1
  64. package/dist/component/products/queries.js +59 -0
  65. package/dist/component/products/queries.js.map +1 -1
  66. package/dist/component/refundHistory/queries.d.ts +28 -0
  67. package/dist/component/refundHistory/queries.d.ts.map +1 -0
  68. package/dist/component/refundHistory/queries.js +59 -0
  69. package/dist/component/refundHistory/queries.js.map +1 -0
  70. package/dist/component/schema.d.ts +33 -2
  71. package/dist/component/schema.js +2 -0
  72. package/dist/component/schema.js.map +1 -1
  73. package/dist/react/components/EarningsSummary.d.ts +3 -1
  74. package/dist/react/components/EarningsSummary.d.ts.map +1 -1
  75. package/dist/react/components/EarningsSummary.js +3 -3
  76. package/dist/react/components/EarningsSummary.js.map +1 -1
  77. package/dist/react/hooks/useCurrencyEarnings.d.ts +31 -0
  78. package/dist/react/hooks/useCurrencyEarnings.d.ts.map +1 -0
  79. package/dist/react/hooks/useCurrencyEarnings.js +30 -0
  80. package/dist/react/hooks/useCurrencyEarnings.js.map +1 -0
  81. package/dist/react/index.d.ts +3 -0
  82. package/dist/react/index.d.ts.map +1 -1
  83. package/dist/react/index.js +3 -0
  84. package/dist/react/index.js.map +1 -1
  85. package/dist/react/workflows/Costs.d.ts +1 -0
  86. package/dist/react/workflows/Costs.d.ts.map +1 -1
  87. package/dist/react/workflows/Costs.js +7 -3
  88. package/dist/react/workflows/Costs.js.map +1 -1
  89. package/docs/react-workflows.md +40 -0
  90. package/docs/read-contracts.md +136 -0
  91. package/package.json +3 -2
  92. package/src/client/connect/payoutSnapshot.test.ts +27 -0
  93. package/src/client/connect/payoutSnapshot.ts +4 -0
  94. package/src/client/connect/refunds.ts +28 -0
  95. package/src/client/index.ts +10 -11
  96. package/src/client/readContracts.ts +90 -0
  97. package/src/client/readPagination.test.ts +62 -0
  98. package/src/client/refund-history.test.ts +67 -0
  99. package/src/client/types/documents.ts +2 -0
  100. package/src/client/webhooks/payout-canceled.integration.test.ts +25 -0
  101. package/src/client/webhooks/processors.ts +2 -0
  102. package/src/component/_generated/api.ts +10 -0
  103. package/src/component/_generated/component.ts +257 -0
  104. package/src/component/connect/mutations.ts +8 -8
  105. package/src/component/connect/queries.ts +110 -48
  106. package/src/component/connect/schema.ts +19 -1
  107. package/src/component/connect/validators.ts +4 -0
  108. package/src/component/currency-migration.test.ts +119 -0
  109. package/src/component/currency-totals.test.ts +95 -0
  110. package/src/component/earnings/legacyReads.ts +59 -0
  111. package/src/component/earnings/mutations.ts +21 -0
  112. package/src/component/earnings/reads.ts +41 -0
  113. package/src/component/earnings/schema.ts +11 -0
  114. package/src/component/earnings/triggers.ts +91 -0
  115. package/src/component/products/queries.ts +74 -0
  116. package/src/component/public.test.ts +1 -1
  117. package/src/component/read-pagination.test.ts +193 -0
  118. package/src/component/refund-history.test.ts +198 -0
  119. package/src/component/refundHistory/queries.ts +99 -0
  120. package/src/component/schema.ts +2 -0
  121. package/src/react/components/EarningsSummary.tsx +5 -2
  122. package/src/react/hooks/useCurrencyEarnings.test.ts +59 -0
  123. package/src/react/hooks/useCurrencyEarnings.ts +73 -0
  124. package/src/react/index.ts +12 -0
  125. package/src/react/read-contracts.render.test.tsx +20 -0
  126. package/src/react/workflows/Costs.test.tsx +21 -0
  127. package/src/react/workflows/Costs.tsx +9 -3
@@ -46,6 +46,8 @@ export type EarningsSummaryProps = {
46
46
  net?: string;
47
47
  paidOut?: string;
48
48
  };
49
+ /** Override formatting for observed currencies outside the charge formatter. */
50
+ formatAmount?: (amount: number, currency: string, locale?: string) => string;
49
51
  locale?: string;
50
52
  className?: string;
51
53
  children?: (props: EarningsSummaryRenderProps) => ReactNode;
@@ -70,6 +72,7 @@ export function EarningsSummary({
70
72
  noPayoutsLabel = "No payouts yet",
71
73
  labels,
72
74
  locale,
75
+ formatAmount = formatPrice,
73
76
  className,
74
77
  children,
75
78
  }: EarningsSummaryProps) {
@@ -93,12 +96,12 @@ export function EarningsSummary({
93
96
  ? latestPayoutLabel
94
97
  .replace(
95
98
  "{amount}",
96
- formatPrice(latestPayout.amount, latestPayout.currency, locale),
99
+ formatAmount(latestPayout.amount, latestPayout.currency, locale),
97
100
  )
98
101
  .replace("{status}", latestPayout.status)
99
102
  : noPayoutsLabel;
100
103
 
101
- const money = (amount: number) => formatPrice(amount, currency, locale);
104
+ const money = (amount: number) => formatAmount(amount, currency, locale);
102
105
  const renderProps: EarningsSummaryRenderProps = {
103
106
  grossLabel: money(gross),
104
107
  reversedLabel: money(reversed),
@@ -0,0 +1,59 @@
1
+ import { expect, it } from "vitest";
2
+ import { createUseCurrencyEarnings } from "./useCurrencyEarnings.js";
3
+ it("joins exact server totals by currency without summing preview rows", () => {
4
+ const earnings = {
5
+ totals: [
6
+ {
7
+ currency: "jpy",
8
+ gross: 1000,
9
+ reversed: 100,
10
+ net: 900,
11
+ transferCount: 99,
12
+ },
13
+ ],
14
+ transfers: [],
15
+ };
16
+ const payouts = {
17
+ totals: [{ currency: "usd", paidOut: 50, payoutCount: 20 }],
18
+ payouts: [],
19
+ };
20
+ const useEarnings = createUseCurrencyEarnings(
21
+ (ref) => (ref === "earnings" ? earnings : payouts),
22
+ "earnings",
23
+ "payouts",
24
+ );
25
+ expect(useEarnings("acct_a").totals).toEqual([
26
+ {
27
+ currency: "jpy",
28
+ gross: 1000,
29
+ reversed: 100,
30
+ net: 900,
31
+ transferCount: 99,
32
+ paidOut: 0,
33
+ payoutCount: 0,
34
+ },
35
+ {
36
+ currency: "usd",
37
+ gross: 0,
38
+ reversed: 0,
39
+ net: 0,
40
+ transferCount: 0,
41
+ paidOut: 50,
42
+ payoutCount: 20,
43
+ },
44
+ ]);
45
+ });
46
+ it("skips unresolved accounts and distinguishes loading from empty totals", () => {
47
+ const args: unknown[] = [];
48
+ const hook = createUseCurrencyEarnings(
49
+ (_ref, value) => {
50
+ args.push(value);
51
+ return undefined;
52
+ },
53
+ "earnings",
54
+ "payouts",
55
+ );
56
+ expect(hook(undefined)).toMatchObject({ totals: [], isLoading: false });
57
+ expect(args).toEqual(["skip", "skip"]);
58
+ expect(hook("acct_a").isLoading).toBe(true);
59
+ });
@@ -0,0 +1,73 @@
1
+ "use client";
2
+ import type { EarningsTransfer } from "./useEarnings.js";
3
+ import type { StripeComponentPayout } from "../types.js";
4
+ export type CurrencyEarningsTotal = {
5
+ currency: string;
6
+ gross: number;
7
+ reversed: number;
8
+ net: number;
9
+ transferCount: number;
10
+ };
11
+ export type CurrencyPayoutTotal = {
12
+ currency: string;
13
+ paidOut: number;
14
+ payoutCount: number;
15
+ };
16
+ export type AccountCurrencyEarnings = {
17
+ totals: CurrencyEarningsTotal[];
18
+ transfers: EarningsTransfer[];
19
+ };
20
+ export type AccountCurrencyPayouts = {
21
+ totals: CurrencyPayoutTotal[];
22
+ payouts: StripeComponentPayout[];
23
+ };
24
+ export type UseCurrencyEarningsResult = {
25
+ totals: (CurrencyEarningsTotal & CurrencyPayoutTotal)[];
26
+ transfers: EarningsTransfer[];
27
+ payouts: StripeComponentPayout[];
28
+ isLoading: boolean;
29
+ };
30
+ /** Bind authorized host queries to the maintained component currency summaries. */
31
+ export function createUseCurrencyEarnings(
32
+ useQuery: (ref: any, args: Record<string, unknown> | "skip") => any,
33
+ earningsRef: any,
34
+ payoutsRef: any,
35
+ ) {
36
+ return function useCurrencyEarnings(
37
+ accountId: string | undefined,
38
+ ): UseCurrencyEarningsResult {
39
+ const earnings = useQuery(
40
+ earningsRef,
41
+ accountId ? { destinationAccountId: accountId } : "skip",
42
+ ) as AccountCurrencyEarnings | undefined;
43
+ const payouts = useQuery(payoutsRef, accountId ? { accountId } : "skip") as
44
+ AccountCurrencyPayouts | undefined;
45
+ const byCurrency = new Map<
46
+ string,
47
+ CurrencyEarningsTotal & CurrencyPayoutTotal
48
+ >();
49
+ for (const row of earnings?.totals ?? [])
50
+ byCurrency.set(row.currency, { ...row, paidOut: 0, payoutCount: 0 });
51
+ for (const row of payouts?.totals ?? [])
52
+ byCurrency.set(row.currency, {
53
+ ...(byCurrency.get(row.currency) ?? {
54
+ currency: row.currency,
55
+ gross: 0,
56
+ reversed: 0,
57
+ net: 0,
58
+ transferCount: 0,
59
+ }),
60
+ ...row,
61
+ });
62
+ return {
63
+ totals: [...byCurrency.values()].sort((a, b) =>
64
+ a.currency.localeCompare(b.currency),
65
+ ),
66
+ transfers: earnings?.transfers ?? [],
67
+ payouts: payouts?.payouts ?? [],
68
+ isLoading:
69
+ accountId !== undefined &&
70
+ (earnings === undefined || payouts === undefined),
71
+ };
72
+ };
73
+ }
@@ -293,3 +293,15 @@ export {
293
293
 
294
294
  export { SaveCard } from "./workflows/SaveCard.js";
295
295
  export type { SaveCardProps, SaveCardFormState } from "./workflows/SaveCard.js";
296
+
297
+ export { createUseCurrencyEarnings } from "./hooks/useCurrencyEarnings.js";
298
+ export type {
299
+ CurrencyEarningsTotal,
300
+ CurrencyPayoutTotal,
301
+ AccountCurrencyEarnings,
302
+ AccountCurrencyPayouts,
303
+ UseCurrencyEarningsResult,
304
+ } from "./hooks/useCurrencyEarnings.js";
305
+
306
+ // Pins end cursors for the component paginator, including reactive page splits.
307
+ export { usePaginatedQuery as useComponentPaginatedQuery } from "convex-helpers/react";
@@ -0,0 +1,20 @@
1
+ import { createElement } from "react";
2
+ import { renderToStaticMarkup } from "react-dom/server";
3
+ import { expect, it } from "vitest";
4
+ import { EarningsSummary } from "./components/EarningsSummary.js";
5
+ it("lets currency-aware earnings consumers format three-decimal observed amounts", () => {
6
+ const html = renderToStaticMarkup(
7
+ createElement(EarningsSummary, {
8
+ gross: 1234,
9
+ reversed: 100,
10
+ net: 1134,
11
+ paidOut: 1000,
12
+ currency: "kwd",
13
+ payouts: [],
14
+ formatAmount: (amount: number, currency: string) =>
15
+ `${currency.toUpperCase()} ${(amount / 1000).toFixed(3)}`,
16
+ }),
17
+ );
18
+ expect(html).toContain("KWD 1.234");
19
+ expect(html).toContain("KWD 1.134");
20
+ });
@@ -39,3 +39,24 @@ it("renders chargeback economics and explicitly handles missing charge links", (
39
39
  expect(screen.getByText("$80.00")).toBeTruthy();
40
40
  expect(screen.getByText("No transfer rows found.")).toBeTruthy();
41
41
  });
42
+ it("distinguishes reserved recovery from money already collected", () => {
43
+ render(
44
+ <CostRecovery
45
+ rows={[
46
+ {
47
+ paymentIntentId: "pi_reserved",
48
+ amountDue: 1000,
49
+ collectedAmount: 400,
50
+ invoicedAmount: 999,
51
+ reservedAmount: 300,
52
+ outstanding: 300,
53
+ currency: "usd",
54
+ },
55
+ ]}
56
+ />,
57
+ );
58
+ expect(screen.getByText("Collected")).toBeTruthy();
59
+ expect(screen.getByText("Reserved")).toBeTruthy();
60
+ expect(screen.queryByText("$9.99")).toBeNull();
61
+ expect(screen.getAllByText("$3.00")).toHaveLength(2);
62
+ });
@@ -10,24 +10,30 @@ export type CostRecoveryRow = {
10
10
  amountDue: number;
11
11
  collectedAmount: number;
12
12
  invoicedAmount: number;
13
+ reservedAmount?: number;
13
14
  outstanding: number;
14
15
  currency: string;
15
16
  };
16
17
  export function CostRecovery(props: ResourceCollectionProps<CostRecoveryRow>) {
18
+ const reserved = props.rows?.some((row) => row.reservedAmount !== undefined);
17
19
  const columns: WorkflowColumn<CostRecoveryRow>[] = [
18
20
  { id: "payment", label: "Payment", cell: (row) => row.paymentIntentId },
19
21
  ...(
20
22
  [
21
23
  ["amountDue", "Total cost"],
22
- ["collectedAmount", "Deducted from earnings"],
23
- ["invoicedAmount", "Added to invoice"],
24
+ ["collectedAmount", reserved ? "Collected" : "Deducted from earnings"],
25
+ [
26
+ reserved ? "reservedAmount" : "invoicedAmount",
27
+ reserved ? "Reserved" : "Added to invoice",
28
+ ],
24
29
  ["outstanding", "Outstanding"],
25
30
  ] as const
26
31
  ).map(([field, label]) => ({
27
32
  id: field,
28
33
  label,
29
34
  align: "end" as const,
30
- cell: (row: CostRecoveryRow) => formatPrice(row[field], row.currency),
35
+ cell: (row: CostRecoveryRow) =>
36
+ formatPrice(row[field] ?? row.invoicedAmount, row.currency),
31
37
  })),
32
38
  ];
33
39
  return (