@fantastic.dev/better-stripe 0.15.0 → 0.16.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.
- package/CHANGELOG.md +6 -0
- package/MIGRATION.md +6 -0
- package/README.md +7 -1
- package/dist/client/connect/payoutSnapshot.d.ts +2 -0
- package/dist/client/connect/payoutSnapshot.d.ts.map +1 -1
- package/dist/client/connect/payoutSnapshot.js +2 -0
- package/dist/client/connect/payoutSnapshot.js.map +1 -1
- package/dist/client/index.d.ts +3 -3
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +3 -3
- package/dist/client/index.js.map +1 -1
- package/dist/client/readContracts.d.ts +50 -0
- package/dist/client/readContracts.d.ts.map +1 -0
- package/dist/client/readContracts.js +24 -0
- package/dist/client/readContracts.js.map +1 -0
- package/dist/client/types/documents.d.ts +2 -0
- package/dist/client/types/documents.d.ts.map +1 -1
- package/dist/client/webhooks/processors.js +2 -0
- package/dist/client/webhooks/processors.js.map +1 -1
- package/dist/component/_generated/component.d.ts +177 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/connect/mutations.d.ts +2 -0
- package/dist/component/connect/mutations.d.ts.map +1 -1
- package/dist/component/connect/mutations.js +8 -8
- package/dist/component/connect/mutations.js.map +1 -1
- package/dist/component/connect/queries.d.ts +102 -2
- package/dist/component/connect/queries.d.ts.map +1 -1
- package/dist/component/connect/queries.js +88 -45
- package/dist/component/connect/queries.js.map +1 -1
- package/dist/component/connect/schema.d.ts +12 -2
- package/dist/component/connect/schema.d.ts.map +1 -1
- package/dist/component/connect/schema.js +10 -1
- package/dist/component/connect/schema.js.map +1 -1
- package/dist/component/connect/validators.d.ts +13 -2
- package/dist/component/connect/validators.d.ts.map +1 -1
- package/dist/component/connect/validators.js +4 -0
- package/dist/component/connect/validators.js.map +1 -1
- package/dist/component/earnings/legacyReads.d.ts +51 -0
- package/dist/component/earnings/legacyReads.d.ts.map +1 -0
- package/dist/component/earnings/legacyReads.js +43 -0
- package/dist/component/earnings/legacyReads.js.map +1 -0
- package/dist/component/earnings/mutations.d.ts +9 -0
- package/dist/component/earnings/mutations.d.ts.map +1 -0
- package/dist/component/earnings/mutations.js +23 -0
- package/dist/component/earnings/mutations.js.map +1 -0
- package/dist/component/earnings/reads.d.ts +14 -0
- package/dist/component/earnings/reads.d.ts.map +1 -0
- package/dist/component/earnings/reads.js +28 -0
- package/dist/component/earnings/reads.js.map +1 -0
- package/dist/component/earnings/schema.d.ts +20 -0
- package/dist/component/earnings/schema.d.ts.map +1 -0
- package/dist/component/earnings/schema.js +12 -0
- package/dist/component/earnings/schema.js.map +1 -0
- package/dist/component/earnings/triggers.d.ts +1290 -0
- package/dist/component/earnings/triggers.d.ts.map +1 -0
- package/dist/component/earnings/triggers.js +68 -0
- package/dist/component/earnings/triggers.js.map +1 -0
- package/dist/component/products/queries.d.ts +47 -0
- package/dist/component/products/queries.d.ts.map +1 -1
- package/dist/component/products/queries.js +59 -0
- package/dist/component/products/queries.js.map +1 -1
- package/dist/component/schema.d.ts +31 -2
- package/dist/component/schema.js +2 -0
- package/dist/component/schema.js.map +1 -1
- package/dist/react/components/EarningsSummary.d.ts +3 -1
- package/dist/react/components/EarningsSummary.d.ts.map +1 -1
- package/dist/react/components/EarningsSummary.js +3 -3
- package/dist/react/components/EarningsSummary.js.map +1 -1
- package/dist/react/hooks/useCurrencyEarnings.d.ts +31 -0
- package/dist/react/hooks/useCurrencyEarnings.d.ts.map +1 -0
- package/dist/react/hooks/useCurrencyEarnings.js +30 -0
- package/dist/react/hooks/useCurrencyEarnings.js.map +1 -0
- package/dist/react/index.d.ts +3 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +3 -0
- package/dist/react/index.js.map +1 -1
- package/docs/react-workflows.md +6 -0
- package/docs/read-contracts.md +136 -0
- package/package.json +3 -2
- package/src/client/connect/payoutSnapshot.test.ts +27 -0
- package/src/client/connect/payoutSnapshot.ts +4 -0
- package/src/client/index.ts +4 -4
- package/src/client/readContracts.ts +90 -0
- package/src/client/readPagination.test.ts +62 -0
- package/src/client/types/documents.ts +2 -0
- package/src/client/webhooks/payout-canceled.integration.test.ts +25 -0
- package/src/client/webhooks/processors.ts +2 -0
- package/src/component/_generated/api.ts +8 -0
- package/src/component/_generated/component.ts +201 -0
- package/src/component/connect/mutations.ts +8 -8
- package/src/component/connect/queries.ts +110 -48
- package/src/component/connect/schema.ts +10 -1
- package/src/component/connect/validators.ts +4 -0
- package/src/component/currency-migration.test.ts +119 -0
- package/src/component/currency-totals.test.ts +95 -0
- package/src/component/earnings/legacyReads.ts +59 -0
- package/src/component/earnings/mutations.ts +21 -0
- package/src/component/earnings/reads.ts +41 -0
- package/src/component/earnings/schema.ts +11 -0
- package/src/component/earnings/triggers.ts +91 -0
- package/src/component/products/queries.ts +74 -0
- package/src/component/public.test.ts +1 -1
- package/src/component/read-pagination.test.ts +193 -0
- package/src/component/schema.ts +2 -0
- package/src/react/components/EarningsSummary.tsx +5 -2
- package/src/react/hooks/useCurrencyEarnings.test.ts +59 -0
- package/src/react/hooks/useCurrencyEarnings.ts +73 -0
- package/src/react/index.ts +12 -0
- package/src/react/read-contracts.render.test.tsx +20 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// @vitest-environment edge-runtime
|
|
2
|
+
import { convexTest } from "convex-test";
|
|
3
|
+
import { expect, it } from "vitest";
|
|
4
|
+
import { api } from "./_generated/api.js";
|
|
5
|
+
import schema from "./schema.js";
|
|
6
|
+
const modules = import.meta.glob("./**/*.*s");
|
|
7
|
+
|
|
8
|
+
it("traverses account/status payout history beyond the cap without duplicates", async () => {
|
|
9
|
+
const t = convexTest(schema, modules);
|
|
10
|
+
for (let i = 0; i < 67; i++)
|
|
11
|
+
await t.mutation(api.connect.mutations.upsertPayout, {
|
|
12
|
+
stripePayoutId: `po_${i}`,
|
|
13
|
+
accountId: i < 57 ? "acct_a" : "acct_b",
|
|
14
|
+
status: "paid",
|
|
15
|
+
amount: i,
|
|
16
|
+
currency: "usd",
|
|
17
|
+
});
|
|
18
|
+
await t.mutation(api.connect.mutations.upsertPayout, {
|
|
19
|
+
stripePayoutId: "po_failed",
|
|
20
|
+
accountId: "acct_a",
|
|
21
|
+
status: "failed",
|
|
22
|
+
amount: 999,
|
|
23
|
+
currency: "usd",
|
|
24
|
+
});
|
|
25
|
+
const ids: string[] = [];
|
|
26
|
+
let cursor: string | null = null;
|
|
27
|
+
for (let i = 0; i < 10; i++) {
|
|
28
|
+
const result: {
|
|
29
|
+
page: { stripePayoutId: string }[];
|
|
30
|
+
isDone: boolean;
|
|
31
|
+
continueCursor: string;
|
|
32
|
+
} = await t.query(api.connect.queries.paginatePayouts, {
|
|
33
|
+
accountId: "acct_a",
|
|
34
|
+
status: "paid",
|
|
35
|
+
paginationOpts: { numItems: 10, cursor },
|
|
36
|
+
});
|
|
37
|
+
ids.push(...result.page.map((p) => p.stripePayoutId));
|
|
38
|
+
if (result.isDone) break;
|
|
39
|
+
cursor = result.continueCursor;
|
|
40
|
+
}
|
|
41
|
+
expect(ids).toEqual(Array.from({ length: 57 }, (_, i) => `po_${56 - i}`));
|
|
42
|
+
expect(new Set(ids).size).toBe(57);
|
|
43
|
+
expect(
|
|
44
|
+
await t.query(api.connect.queries.paginatePayouts, {
|
|
45
|
+
accountId: "empty",
|
|
46
|
+
paginationOpts: { numItems: 10, cursor: null },
|
|
47
|
+
}),
|
|
48
|
+
).toMatchObject({ page: [], isDone: true });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("traverses indexed product and price pages with account/product/active isolation", async () => {
|
|
52
|
+
const t = convexTest(schema, modules);
|
|
53
|
+
for (let i = 0; i < 67; i++) {
|
|
54
|
+
await t.mutation(api.products.mutations.upsertProduct, {
|
|
55
|
+
stripeProductId: `prod_${i}`,
|
|
56
|
+
accountId: i < 57 ? "acct_a" : "acct_b",
|
|
57
|
+
name: `Product ${i}`,
|
|
58
|
+
active: true,
|
|
59
|
+
});
|
|
60
|
+
await t.mutation(api.products.mutations.upsertPrice, {
|
|
61
|
+
stripePriceId: `price_${i}`,
|
|
62
|
+
productId: i < 57 ? "product_a" : "product_b",
|
|
63
|
+
stripeProductId: i < 57 ? "prod_a" : "prod_b",
|
|
64
|
+
unitAmount: i,
|
|
65
|
+
currency: "usd",
|
|
66
|
+
type: "one_time",
|
|
67
|
+
active: true,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
for (const [ref, filters, key, prefix] of [
|
|
71
|
+
[
|
|
72
|
+
api.products.queries.paginateProducts,
|
|
73
|
+
{ accountId: "acct_a", active: true },
|
|
74
|
+
"stripeProductId",
|
|
75
|
+
"prod_",
|
|
76
|
+
],
|
|
77
|
+
[
|
|
78
|
+
api.products.queries.paginatePrices,
|
|
79
|
+
{ productId: "product_a", active: true },
|
|
80
|
+
"stripePriceId",
|
|
81
|
+
"price_",
|
|
82
|
+
],
|
|
83
|
+
] as const) {
|
|
84
|
+
const ids: string[] = [];
|
|
85
|
+
let cursor: string | null = null;
|
|
86
|
+
for (let i = 0; i < 10; i++) {
|
|
87
|
+
const result: {
|
|
88
|
+
page: Record<string, unknown>[];
|
|
89
|
+
isDone: boolean;
|
|
90
|
+
continueCursor: string;
|
|
91
|
+
} = await t.query(ref, {
|
|
92
|
+
...filters,
|
|
93
|
+
paginationOpts: { numItems: 10, cursor },
|
|
94
|
+
});
|
|
95
|
+
ids.push(
|
|
96
|
+
...result.page.map((row: Record<string, unknown>) => String(row[key])),
|
|
97
|
+
);
|
|
98
|
+
if (result.isDone) break;
|
|
99
|
+
cursor = result.continueCursor;
|
|
100
|
+
}
|
|
101
|
+
expect(ids).toEqual(
|
|
102
|
+
Array.from({ length: 57 }, (_, i) => `${prefix}${56 - i}`),
|
|
103
|
+
);
|
|
104
|
+
expect(
|
|
105
|
+
await t.query(ref, {
|
|
106
|
+
...filters,
|
|
107
|
+
active: false,
|
|
108
|
+
paginationOpts: { numItems: 10, cursor: null },
|
|
109
|
+
}),
|
|
110
|
+
).toMatchObject({ page: [], isDone: true });
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("rejects invalid page bounds and preserves legacy capped readers", async () => {
|
|
115
|
+
const t = convexTest(schema, modules);
|
|
116
|
+
for (const numItems of [0, -1, 101, 1.5]) {
|
|
117
|
+
await expect(
|
|
118
|
+
t.query(api.products.queries.paginateProducts, {
|
|
119
|
+
paginationOpts: { numItems, cursor: null },
|
|
120
|
+
}),
|
|
121
|
+
).rejects.toThrow("Page size");
|
|
122
|
+
await expect(
|
|
123
|
+
t.query(api.products.queries.paginatePrices, {
|
|
124
|
+
paginationOpts: { numItems, cursor: null },
|
|
125
|
+
}),
|
|
126
|
+
).rejects.toThrow("Page size");
|
|
127
|
+
await expect(
|
|
128
|
+
t.query(api.connect.queries.paginatePayouts, {
|
|
129
|
+
accountId: "acct_a",
|
|
130
|
+
paginationOpts: { numItems, cursor: null },
|
|
131
|
+
}),
|
|
132
|
+
).rejects.toThrow("Page size");
|
|
133
|
+
}
|
|
134
|
+
expect(await t.query(api.products.queries.listProducts, {})).toEqual([]);
|
|
135
|
+
expect(await t.query(api.products.queries.listPrices, {})).toEqual([]);
|
|
136
|
+
expect(await t.query(api.connect.queries.listPayouts, {})).toEqual([]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("pins reactive product boundaries and splits a grown page before read limits", async () => {
|
|
140
|
+
const t = convexTest(schema, modules);
|
|
141
|
+
for (let i = 0; i < 20; i++)
|
|
142
|
+
await t.mutation(api.products.mutations.upsertProduct, {
|
|
143
|
+
stripeProductId: `boundary_${i}`,
|
|
144
|
+
accountId: "acct_boundary",
|
|
145
|
+
name: "Boundary",
|
|
146
|
+
active: true,
|
|
147
|
+
});
|
|
148
|
+
const first = await t.query(api.products.queries.paginateProducts, {
|
|
149
|
+
accountId: "acct_boundary",
|
|
150
|
+
paginationOpts: { numItems: 10, cursor: null },
|
|
151
|
+
});
|
|
152
|
+
const second = await t.query(api.products.queries.paginateProducts, {
|
|
153
|
+
accountId: "acct_boundary",
|
|
154
|
+
paginationOpts: { numItems: 10, cursor: first.continueCursor },
|
|
155
|
+
});
|
|
156
|
+
await t.mutation(api.products.mutations.upsertProduct, {
|
|
157
|
+
stripeProductId: "new_boundary",
|
|
158
|
+
accountId: "acct_boundary",
|
|
159
|
+
name: "New",
|
|
160
|
+
active: true,
|
|
161
|
+
});
|
|
162
|
+
const pinned = await t.query(api.products.queries.paginateProducts, {
|
|
163
|
+
accountId: "acct_boundary",
|
|
164
|
+
paginationOpts: {
|
|
165
|
+
numItems: 10,
|
|
166
|
+
cursor: null,
|
|
167
|
+
endCursor: first.continueCursor,
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
expect(pinned.page).toHaveLength(11);
|
|
171
|
+
expect(
|
|
172
|
+
new Set([...pinned.page, ...second.page].map((p) => p.stripeProductId))
|
|
173
|
+
.size,
|
|
174
|
+
).toBe(21);
|
|
175
|
+
for (let i = 0; i < 220; i++)
|
|
176
|
+
await t.mutation(api.products.mutations.upsertProduct, {
|
|
177
|
+
stripeProductId: `growth_${i}`,
|
|
178
|
+
accountId: "acct_boundary",
|
|
179
|
+
name: "Growth",
|
|
180
|
+
active: true,
|
|
181
|
+
});
|
|
182
|
+
const grown = await t.query(api.products.queries.paginateProducts, {
|
|
183
|
+
accountId: "acct_boundary",
|
|
184
|
+
paginationOpts: {
|
|
185
|
+
numItems: 10,
|
|
186
|
+
cursor: null,
|
|
187
|
+
endCursor: first.continueCursor,
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
expect(grown.page).toHaveLength(200);
|
|
191
|
+
expect(grown.pageStatus).toBe("SplitRequired");
|
|
192
|
+
expect(grown.splitCursor).toBeTruthy();
|
|
193
|
+
});
|
package/src/component/schema.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { currencyTotalsTable } from "./earnings/schema.js";
|
|
1
2
|
import {
|
|
2
3
|
claimsTable,
|
|
3
4
|
balancesTable,
|
|
@@ -30,6 +31,7 @@ import { pricesTable, productsTable } from "./products/schema";
|
|
|
30
31
|
import { webhookEventsTable } from "./webhooks/schema";
|
|
31
32
|
|
|
32
33
|
export default defineSchema({
|
|
34
|
+
currencyTotals: currencyTotalsTable,
|
|
33
35
|
recoveryClaims: claimsTable,
|
|
34
36
|
recoveryBalances: balancesTable,
|
|
35
37
|
recoveryGroups: groupsTable,
|
|
@@ -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
|
-
|
|
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) =>
|
|
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
|
+
}
|
package/src/react/index.ts
CHANGED
|
@@ -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
|
+
});
|