@fantastic.dev/better-stripe 0.6.0 → 0.7.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 +73 -45
- package/README.md +22 -14
- package/dist/client/webhooks/processors.js +9 -5
- package/dist/client/webhooks/processors.js.map +1 -1
- package/dist/component/_generated/component.d.ts +34 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/billing/queries.d.ts +36 -0
- package/dist/component/billing/queries.d.ts.map +1 -1
- package/dist/component/billing/queries.js +10 -0
- package/dist/component/billing/queries.js.map +1 -1
- package/dist/component/billing/schema.d.ts +1 -0
- package/dist/component/billing/schema.d.ts.map +1 -1
- package/dist/component/billing/schema.js +1 -0
- package/dist/component/billing/schema.js.map +1 -1
- package/dist/component/connect/mutations.d.ts.map +1 -1
- package/dist/component/connect/mutations.js +46 -8
- package/dist/component/connect/mutations.js.map +1 -1
- package/dist/component/connect/queries.d.ts +3 -4
- package/dist/component/connect/queries.d.ts.map +1 -1
- package/dist/component/connect/queries.js +19 -10
- package/dist/component/connect/queries.js.map +1 -1
- package/dist/component/schema.d.ts +1 -0
- package/dist/react/hooks/useSplitBreakdown.d.ts +4 -6
- package/dist/react/hooks/useSplitBreakdown.d.ts.map +1 -1
- package/dist/react/hooks/useSplitBreakdown.js +4 -6
- package/dist/react/hooks/useSplitBreakdown.js.map +1 -1
- package/package.json +1 -1
- package/src/client/webhooks/processors.test.ts +16 -0
- package/src/client/webhooks/processors.ts +8 -3
- package/src/component/_generated/component.ts +46 -0
- package/src/component/billing/queries.ts +12 -0
- package/src/component/billing/schema.ts +1 -0
- package/src/component/connect/mutations.ts +68 -17
- package/src/component/connect/queries.ts +18 -10
- package/src/component/reversal-races.test.ts +41 -1
- package/src/component/seller-case-ownership.test.ts +61 -0
- package/src/component/store-subscriptions.test.ts +32 -0
- package/src/component/transfers.test.ts +69 -0
- package/src/react/hooks/useSplitBreakdown.ts +4 -6
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { v } from "convex/values";
|
|
2
2
|
|
|
3
|
-
import type { Id } from "../_generated/dataModel";
|
|
4
|
-
import { mutation } from "../_generated/server";
|
|
3
|
+
import type { Doc, Id } from "../_generated/dataModel";
|
|
4
|
+
import { mutation, type MutationCtx } from "../_generated/server";
|
|
5
5
|
import { feeRoutingFields } from "../lib/fees";
|
|
6
6
|
import { computeReversalSlices } from "../lib/reversals";
|
|
7
7
|
import {
|
|
@@ -12,6 +12,20 @@ import {
|
|
|
12
12
|
transferFields,
|
|
13
13
|
} from "./validators";
|
|
14
14
|
|
|
15
|
+
async function paymentSeller(
|
|
16
|
+
ctx: MutationCtx,
|
|
17
|
+
stripePaymentIntentId?: string,
|
|
18
|
+
): Promise<string | undefined> {
|
|
19
|
+
if (!stripePaymentIntentId) return undefined;
|
|
20
|
+
const payment = await ctx.db
|
|
21
|
+
.query("payments")
|
|
22
|
+
.withIndex("by_stripe_payment_intent_id", (q) =>
|
|
23
|
+
q.eq("stripePaymentIntentId", stripePaymentIntentId),
|
|
24
|
+
)
|
|
25
|
+
.first();
|
|
26
|
+
return payment?.destinationAccountId;
|
|
27
|
+
}
|
|
28
|
+
|
|
15
29
|
// =============================================================================
|
|
16
30
|
// PAYMENT MUTATIONS
|
|
17
31
|
// =============================================================================
|
|
@@ -87,6 +101,26 @@ export const upsertPayment = mutation({
|
|
|
87
101
|
}
|
|
88
102
|
}
|
|
89
103
|
|
|
104
|
+
// A refund/dispute can arrive before the platform charge has its seller
|
|
105
|
+
// routing metadata. Reconcile those case owners once routing is known.
|
|
106
|
+
const sellerAccountId =
|
|
107
|
+
args.destinationAccountId ?? existing?.destinationAccountId;
|
|
108
|
+
if (sellerAccountId) {
|
|
109
|
+
for (const table of ["refunds", "disputes"] as const) {
|
|
110
|
+
const cases = await ctx.db
|
|
111
|
+
.query(table)
|
|
112
|
+
.withIndex("by_stripe_payment_intent_id", (q) =>
|
|
113
|
+
q.eq("stripePaymentIntentId", args.stripePaymentIntentId),
|
|
114
|
+
)
|
|
115
|
+
.take(100);
|
|
116
|
+
for (const record of cases)
|
|
117
|
+
if (!record.accountId)
|
|
118
|
+
await ctx.db.patch(table, record._id, {
|
|
119
|
+
accountId: sellerAccountId,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
90
124
|
return null;
|
|
91
125
|
},
|
|
92
126
|
});
|
|
@@ -140,10 +174,13 @@ export const upsertRefund = mutation({
|
|
|
140
174
|
)
|
|
141
175
|
.first();
|
|
142
176
|
|
|
177
|
+
const accountId =
|
|
178
|
+
args.accountId ?? (await paymentSeller(ctx, args.stripePaymentIntentId));
|
|
179
|
+
const record = { ...args, ...(accountId ? { accountId } : {}) };
|
|
143
180
|
if (existing) {
|
|
144
|
-
await ctx.db.patch("refunds", existing._id,
|
|
181
|
+
await ctx.db.patch("refunds", existing._id, record);
|
|
145
182
|
} else {
|
|
146
|
-
await ctx.db.insert("refunds",
|
|
183
|
+
await ctx.db.insert("refunds", record);
|
|
147
184
|
}
|
|
148
185
|
|
|
149
186
|
// Denormalize cumulative refund state onto the linked `payments` row so
|
|
@@ -278,10 +315,13 @@ export const upsertDispute = mutation({
|
|
|
278
315
|
)
|
|
279
316
|
.first();
|
|
280
317
|
|
|
318
|
+
const accountId =
|
|
319
|
+
args.accountId ?? (await paymentSeller(ctx, args.stripePaymentIntentId));
|
|
320
|
+
const record = { ...args, ...(accountId ? { accountId } : {}) };
|
|
281
321
|
if (existing) {
|
|
282
|
-
await ctx.db.patch("disputes", existing._id,
|
|
322
|
+
await ctx.db.patch("disputes", existing._id, record);
|
|
283
323
|
} else {
|
|
284
|
-
await ctx.db.insert("disputes",
|
|
324
|
+
await ctx.db.insert("disputes", record);
|
|
285
325
|
}
|
|
286
326
|
|
|
287
327
|
return null;
|
|
@@ -311,7 +351,8 @@ function deriveReversalState(
|
|
|
311
351
|
return {
|
|
312
352
|
reversedAmount: reversedAmount > 0 ? reversedAmount : undefined,
|
|
313
353
|
reversalStatus,
|
|
314
|
-
status:
|
|
354
|
+
status:
|
|
355
|
+
reversalStatus === "fully_reversed" ? ("reversed" as const) : baseStatus,
|
|
315
356
|
};
|
|
316
357
|
}
|
|
317
358
|
|
|
@@ -333,7 +374,11 @@ export const upsertTransfer = mutation({
|
|
|
333
374
|
existing.reversedAmount ?? 0,
|
|
334
375
|
args.reversedAmount ?? 0,
|
|
335
376
|
);
|
|
336
|
-
const derived = deriveReversalState(
|
|
377
|
+
const derived = deriveReversalState(
|
|
378
|
+
args.amount,
|
|
379
|
+
reversedAmount,
|
|
380
|
+
args.status,
|
|
381
|
+
);
|
|
337
382
|
await ctx.db.patch("transfers", existing._id, { ...args, ...derived });
|
|
338
383
|
} else {
|
|
339
384
|
const derived = deriveReversalState(
|
|
@@ -459,21 +504,27 @@ export const claimReversalSlices = mutation({
|
|
|
459
504
|
if (existingOp) {
|
|
460
505
|
const slices = [];
|
|
461
506
|
for (const s of existingOp.slices) {
|
|
462
|
-
slices.push({
|
|
507
|
+
slices.push({
|
|
508
|
+
...s,
|
|
509
|
+
confirmed: await confirmedFor(s.stripeTransferId),
|
|
510
|
+
});
|
|
463
511
|
}
|
|
464
512
|
return { replay: true, slices };
|
|
465
513
|
}
|
|
466
514
|
|
|
467
515
|
// Original split legs only — reinstatement payouts are audit rows and
|
|
468
516
|
// must never be re-reversed (mirrors listTransfersByCharge).
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
517
|
+
// Claim every original split leg, including those beyond the former
|
|
518
|
+
// 50-row preview. Read/write limits abort the transaction before a partial
|
|
519
|
+
// claim can commit; no Stripe call happens until this mutation succeeds.
|
|
520
|
+
const rows: Doc<"transfers">[] = [];
|
|
521
|
+
for await (const transfer of ctx.db
|
|
522
|
+
.query("transfers")
|
|
523
|
+
.withIndex("by_source_charge_id", (q) =>
|
|
524
|
+
q.eq("sourceChargeId", args.sourceChargeId),
|
|
525
|
+
)) {
|
|
526
|
+
if (!transfer.reinstatement) rows.push(transfer);
|
|
527
|
+
}
|
|
477
528
|
|
|
478
529
|
const byId = new Map(rows.map((r) => [r.stripeTransferId, r]));
|
|
479
530
|
const slices = computeReversalSlices(
|
|
@@ -293,24 +293,32 @@ export const getTransferByStripeId = query({
|
|
|
293
293
|
/**
|
|
294
294
|
* All transfers funded from one charge (the legs of a split).
|
|
295
295
|
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
* recipients.
|
|
296
|
+
* Returns every original split leg unless an explicit limit is provided.
|
|
297
|
+
* Reinstatement audit rows do not consume that limit. Database read/response
|
|
298
|
+
* limits fail the query rather than silently returning incomplete money data.
|
|
300
299
|
*/
|
|
301
300
|
export const listTransfersByCharge = query({
|
|
302
301
|
args: { sourceChargeId: v.string(), limit: v.optional(v.number()) },
|
|
303
302
|
returns: v.array(transferDocValidator),
|
|
304
303
|
handler: async (ctx, args) => {
|
|
305
|
-
|
|
304
|
+
if (
|
|
305
|
+
args.limit !== undefined &&
|
|
306
|
+
(!Number.isInteger(args.limit) || args.limit < 0)
|
|
307
|
+
) {
|
|
308
|
+
throw new Error("limit must be a non-negative integer");
|
|
309
|
+
}
|
|
310
|
+
const rows: Doc<"transfers">[] = [];
|
|
311
|
+
if (args.limit === 0) return rows;
|
|
312
|
+
for await (const transfer of ctx.db
|
|
306
313
|
.query("transfers")
|
|
307
314
|
.withIndex("by_source_charge_id", (q) =>
|
|
308
315
|
q.eq("sourceChargeId", args.sourceChargeId),
|
|
309
|
-
)
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
316
|
+
)) {
|
|
317
|
+
if (transfer.reinstatement) continue;
|
|
318
|
+
rows.push(transfer);
|
|
319
|
+
if (args.limit !== undefined && rows.length >= args.limit) break;
|
|
320
|
+
}
|
|
321
|
+
return rows;
|
|
314
322
|
},
|
|
315
323
|
});
|
|
316
324
|
|
|
@@ -296,7 +296,47 @@ describe("BTS-63 race 1 — refund path: concurrent DISTINCT refunds must not do
|
|
|
296
296
|
});
|
|
297
297
|
|
|
298
298
|
describe("BTS-63 race 2 — dispute path: retry after partial success must converge", () => {
|
|
299
|
-
|
|
299
|
+
it("claws back all 120 legs and converges after a failure beyond the former cap", async () => {
|
|
300
|
+
const t = convexTest(schema, modules);
|
|
301
|
+
const ctx = makeCtx(t);
|
|
302
|
+
const component = proxyComponent();
|
|
303
|
+
const stripe = makeRacingStripe();
|
|
304
|
+
for (let i = 0; i < 120; i++) {
|
|
305
|
+
await seedLeg(t, {
|
|
306
|
+
stripeTransferId: `tr_large_${i}`,
|
|
307
|
+
sourceChargeId: "ch_large",
|
|
308
|
+
amount: 100,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
const opts = {
|
|
312
|
+
sourceChargeId: "ch_large",
|
|
313
|
+
amount: 6000,
|
|
314
|
+
operationId: "dispute_large",
|
|
315
|
+
};
|
|
316
|
+
stripe.failNextCallFor("tr_large_75");
|
|
317
|
+
await expect(
|
|
318
|
+
reverseTransfers(asStripe(stripe), component, ctx, opts),
|
|
319
|
+
).rejects.toThrow(/network error/);
|
|
320
|
+
await reverseTransfers(asStripe(stripe), component, ctx, opts);
|
|
321
|
+
// A further redelivery must not move money or conflict with earlier keys.
|
|
322
|
+
await reverseTransfers(asStripe(stripe), component, ctx, opts);
|
|
323
|
+
const rows = await t.query(api.connect.queries.listTransfersByCharge, {
|
|
324
|
+
sourceChargeId: "ch_large",
|
|
325
|
+
});
|
|
326
|
+
expect(rows).toHaveLength(120);
|
|
327
|
+
expect(rows.reduce((sum, row) => sum + (row.reversedAmount ?? 0), 0)).toBe(
|
|
328
|
+
6000,
|
|
329
|
+
);
|
|
330
|
+
for (const row of rows) {
|
|
331
|
+
expect(row.reversedAmount).toBe(50);
|
|
332
|
+
expect(stripe.total(row.stripeTransferId)).toBe(50);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
const DISPUTE = {
|
|
336
|
+
sourceChargeId: "ch_dp",
|
|
337
|
+
amount: 4500,
|
|
338
|
+
operationId: "dp_1",
|
|
339
|
+
};
|
|
300
340
|
|
|
301
341
|
async function seedDisputeLegs(t: ReturnType<typeof convexTest>) {
|
|
302
342
|
await seedLeg(t, {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { convexTest } from "convex-test";
|
|
2
|
+
import { expect, it } from "vitest";
|
|
3
|
+
import { api } from "./_generated/api";
|
|
4
|
+
import schema from "./schema";
|
|
5
|
+
const modules = import.meta.glob("./**/*.*s");
|
|
6
|
+
const payment = {
|
|
7
|
+
stripePaymentIntentId: "pi_sale",
|
|
8
|
+
userId: "customer",
|
|
9
|
+
amount: 10000,
|
|
10
|
+
currency: "usd",
|
|
11
|
+
status: "succeeded" as const,
|
|
12
|
+
destinationAccountId: "acct_store",
|
|
13
|
+
chargeType: "separate" as const,
|
|
14
|
+
};
|
|
15
|
+
const refund = {
|
|
16
|
+
stripeRefundId: "re_sale",
|
|
17
|
+
stripePaymentIntentId: "pi_sale",
|
|
18
|
+
amount: 1000,
|
|
19
|
+
currency: "usd",
|
|
20
|
+
status: "succeeded" as const,
|
|
21
|
+
};
|
|
22
|
+
const dispute = {
|
|
23
|
+
stripeDisputeId: "dp_sale",
|
|
24
|
+
stripePaymentIntentId: "pi_sale",
|
|
25
|
+
amount: 10000,
|
|
26
|
+
currency: "usd",
|
|
27
|
+
status: "needs_response" as const,
|
|
28
|
+
reason: "product_not_received",
|
|
29
|
+
isChargeRefundable: false,
|
|
30
|
+
};
|
|
31
|
+
it("attributes refunds and disputes to the seller instead of the purchasing customer", async () => {
|
|
32
|
+
const t = convexTest(schema, modules);
|
|
33
|
+
await t.mutation(api.connect.mutations.upsertPayment, payment);
|
|
34
|
+
await t.mutation(api.connect.mutations.upsertRefund, refund);
|
|
35
|
+
await t.mutation(api.connect.mutations.upsertDispute, dispute);
|
|
36
|
+
expect(
|
|
37
|
+
await t.query(api.connect.queries.listRefunds, { accountId: "acct_store" }),
|
|
38
|
+
).toHaveLength(1);
|
|
39
|
+
expect(
|
|
40
|
+
await t.query(api.connect.queries.listDisputes, {
|
|
41
|
+
accountId: "acct_store",
|
|
42
|
+
}),
|
|
43
|
+
).toHaveLength(1);
|
|
44
|
+
expect(
|
|
45
|
+
await t.query(api.connect.queries.listRefunds, { accountId: "acct_other" }),
|
|
46
|
+
).toHaveLength(0);
|
|
47
|
+
});
|
|
48
|
+
it("reconciles cases delivered before the seller's settlement metadata", async () => {
|
|
49
|
+
const t = convexTest(schema, modules);
|
|
50
|
+
await t.mutation(api.connect.mutations.upsertRefund, refund);
|
|
51
|
+
await t.mutation(api.connect.mutations.upsertDispute, dispute);
|
|
52
|
+
await t.mutation(api.connect.mutations.upsertPayment, payment);
|
|
53
|
+
expect(
|
|
54
|
+
await t.query(api.connect.queries.listRefunds, { accountId: "acct_store" }),
|
|
55
|
+
).toHaveLength(1);
|
|
56
|
+
expect(
|
|
57
|
+
await t.query(api.connect.queries.listDisputes, {
|
|
58
|
+
accountId: "acct_store",
|
|
59
|
+
}),
|
|
60
|
+
).toHaveLength(1);
|
|
61
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { convexTest } from "convex-test";
|
|
2
|
+
import { expect, it } from "vitest";
|
|
3
|
+
import { api } from "./_generated/api";
|
|
4
|
+
import schema from "./schema";
|
|
5
|
+
const modules = import.meta.glob("./**/*.*s");
|
|
6
|
+
it("scopes seller subscriptions by catalog price for both destination and split sales", async () => {
|
|
7
|
+
const t = convexTest(schema, modules);
|
|
8
|
+
await t.run(async (ctx) => {
|
|
9
|
+
for (const [id, priceId] of [
|
|
10
|
+
["sub_direct", "price_seller"],
|
|
11
|
+
["sub_referred", "price_seller"],
|
|
12
|
+
["sub_other", "price_other"],
|
|
13
|
+
]) {
|
|
14
|
+
await ctx.db.insert("subscriptions", {
|
|
15
|
+
stripeSubscriptionId: id,
|
|
16
|
+
priceId,
|
|
17
|
+
userId: "customer",
|
|
18
|
+
status: "active",
|
|
19
|
+
cancelAtPeriodEnd: false,
|
|
20
|
+
isTrialing: false,
|
|
21
|
+
isCollectionPaused: false,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
const rows = await t.query(api.billing.queries.listSubscriptionsByPrice, {
|
|
26
|
+
priceId: "price_seller",
|
|
27
|
+
});
|
|
28
|
+
expect(rows.map((s) => s.stripeSubscriptionId).sort()).toEqual([
|
|
29
|
+
"sub_direct",
|
|
30
|
+
"sub_referred",
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
@@ -2,11 +2,80 @@
|
|
|
2
2
|
import { convexTest } from "convex-test";
|
|
3
3
|
import { describe, expect, it } from "vitest";
|
|
4
4
|
|
|
5
|
+
import { createUseSplitBreakdown } from "../react/hooks/useSplitBreakdown.js";
|
|
6
|
+
|
|
5
7
|
import { api } from "./_generated/api.js";
|
|
6
8
|
import schema from "./schema.js";
|
|
7
9
|
|
|
8
10
|
const modules = import.meta.glob("./**/*.*s");
|
|
9
11
|
|
|
12
|
+
describe("complete split-leg reads", () => {
|
|
13
|
+
it("returns every original leg beyond the former 50-row cap and filters audit rows before limiting", async () => {
|
|
14
|
+
const t = convexTest(schema, modules);
|
|
15
|
+
await t.mutation(api.connect.mutations.upsertTransfer, {
|
|
16
|
+
stripeTransferId: "tr_audit_first",
|
|
17
|
+
sourceChargeId: "ch_many",
|
|
18
|
+
destinationAccountId: "acct_store",
|
|
19
|
+
amount: 100,
|
|
20
|
+
currency: "usd",
|
|
21
|
+
status: "paid",
|
|
22
|
+
reinstatement: true,
|
|
23
|
+
});
|
|
24
|
+
for (let i = 0; i < 120; i++) {
|
|
25
|
+
await t.mutation(api.connect.mutations.upsertTransfer, {
|
|
26
|
+
stripeTransferId: `tr_many_${i}`,
|
|
27
|
+
sourceChargeId: "ch_many",
|
|
28
|
+
destinationAccountId: `acct_${i}`,
|
|
29
|
+
amount: 100,
|
|
30
|
+
currency: "usd",
|
|
31
|
+
status: "paid",
|
|
32
|
+
role: i % 2 === 0 ? "store" : "affiliate",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const rows = await t.query(api.connect.queries.listTransfersByCharge, {
|
|
36
|
+
sourceChargeId: "ch_many",
|
|
37
|
+
});
|
|
38
|
+
expect(rows).toHaveLength(120);
|
|
39
|
+
expect(rows.reduce((sum, row) => sum + row.amount, 0)).toBe(12000);
|
|
40
|
+
expect(rows.some((row) => row.reinstatement)).toBe(false);
|
|
41
|
+
const useSplitBreakdown = createUseSplitBreakdown(
|
|
42
|
+
() => rows,
|
|
43
|
+
"chargeQuery",
|
|
44
|
+
);
|
|
45
|
+
expect(
|
|
46
|
+
useSplitBreakdown({ sourceChargeId: "ch_many", saleAmount: 13000 })
|
|
47
|
+
.breakdown,
|
|
48
|
+
).toMatchObject({
|
|
49
|
+
store: 6000,
|
|
50
|
+
affiliate: 6000,
|
|
51
|
+
recipientsTotal: 12000,
|
|
52
|
+
platform: 1000,
|
|
53
|
+
});
|
|
54
|
+
const preview = await t.query(api.connect.queries.listTransfersByCharge, {
|
|
55
|
+
sourceChargeId: "ch_many",
|
|
56
|
+
limit: 50,
|
|
57
|
+
});
|
|
58
|
+
expect(preview).toHaveLength(50);
|
|
59
|
+
expect(preview[0].stripeTransferId).toBe("tr_many_0");
|
|
60
|
+
expect(
|
|
61
|
+
await t.query(api.connect.queries.listTransfersByCharge, {
|
|
62
|
+
sourceChargeId: "ch_many",
|
|
63
|
+
limit: 0,
|
|
64
|
+
}),
|
|
65
|
+
).toEqual([]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it.each([-1, 1.5])("rejects invalid explicit limit %s", async (limit) => {
|
|
69
|
+
const t = convexTest(schema, modules);
|
|
70
|
+
await expect(
|
|
71
|
+
t.query(api.connect.queries.listTransfersByCharge, {
|
|
72
|
+
sourceChargeId: "ch_many",
|
|
73
|
+
limit,
|
|
74
|
+
}),
|
|
75
|
+
).rejects.toThrow("non-negative integer");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
10
79
|
describe("transfers ledger (BTS-12)", () => {
|
|
11
80
|
it("upserts a transfer and reads it back by stripe id", async () => {
|
|
12
81
|
const t = convexTest(schema, modules);
|
|
@@ -42,7 +42,7 @@ export type UseSplitBreakdownResult = {
|
|
|
42
42
|
};
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
|
-
* Factory for a per-sale split-breakdown hook
|
|
45
|
+
* Factory for a per-sale split-breakdown hook: reads the sale's
|
|
46
46
|
* original split legs (`listTransfersByCharge` — reinstatement rows are
|
|
47
47
|
* excluded server-side) and sums them by role, with the platform's share
|
|
48
48
|
* derived from the sale amount when provided. Presentation math only — the
|
|
@@ -51,11 +51,9 @@ export type UseSplitBreakdownResult = {
|
|
|
51
51
|
* Pass `sourceChargeId: undefined` to skip the query. The `useQuery` binding
|
|
52
52
|
* must support Convex's `"skip"` sentinel.
|
|
53
53
|
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* than 50 recipients would undercount here; pass a higher `limit` on that query
|
|
58
|
-
* if you ever do that.
|
|
54
|
+
* Bind this to a query that returns every original leg for the charge. The
|
|
55
|
+
* component's `listTransfersByCharge` returns all legs by default; applying an
|
|
56
|
+
* explicit preview limit in the app's wrapper would understate these totals.
|
|
59
57
|
*/
|
|
60
58
|
export function createUseSplitBreakdown(
|
|
61
59
|
useQuery: (queryRef: any, args: Record<string, unknown> | "skip") => any,
|