@fantastic.dev/better-stripe 0.8.1 → 0.10.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 +12 -0
- package/MIGRATION.md +12 -0
- package/README.md +46 -2
- package/dist/client/billing/checkout.d.ts +1 -0
- package/dist/client/billing/checkout.d.ts.map +1 -1
- package/dist/client/billing/checkout.js +5 -1
- package/dist/client/billing/checkout.js.map +1 -1
- package/dist/client/billing/providerReads.d.ts +18 -0
- package/dist/client/billing/providerReads.d.ts.map +1 -0
- package/dist/client/billing/providerReads.js +15 -0
- package/dist/client/billing/providerReads.js.map +1 -0
- package/dist/client/billing/subscriptions.d.ts +1 -1
- package/dist/client/billing/subscriptions.d.ts.map +1 -1
- package/dist/client/billing/subscriptions.js.map +1 -1
- package/dist/client/connect/payoutSnapshot.d.ts +28 -0
- package/dist/client/connect/payoutSnapshot.d.ts.map +1 -0
- package/dist/client/connect/payoutSnapshot.js +81 -0
- package/dist/client/connect/payoutSnapshot.js.map +1 -0
- package/dist/client/connect/transfers.d.ts.map +1 -1
- package/dist/client/connect/transfers.js +13 -32
- package/dist/client/connect/transfers.js.map +1 -1
- package/dist/client/core/accounts.d.ts +12 -23
- package/dist/client/core/accounts.d.ts.map +1 -1
- package/dist/client/core/accounts.js +5 -1
- package/dist/client/core/accounts.js.map +1 -1
- package/dist/client/core/idempotency.d.ts +5 -0
- package/dist/client/core/idempotency.d.ts.map +1 -0
- package/dist/client/core/idempotency.js +10 -0
- package/dist/client/core/idempotency.js.map +1 -0
- package/dist/client/index.d.ts +31 -25
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +29 -52
- package/dist/client/index.js.map +1 -1
- package/dist/client/settlement/allocation.d.ts +30 -0
- package/dist/client/settlement/allocation.d.ts.map +1 -0
- package/dist/client/settlement/allocation.js +71 -0
- package/dist/client/settlement/allocation.js.map +1 -0
- package/dist/client/settlement/commissions.d.ts +27 -0
- package/dist/client/settlement/commissions.d.ts.map +1 -0
- package/dist/client/settlement/commissions.js +69 -0
- package/dist/client/settlement/commissions.js.map +1 -0
- package/dist/client/settlement/execution.d.ts +27 -0
- package/dist/client/settlement/execution.d.ts.map +1 -0
- package/dist/client/settlement/execution.js +70 -0
- package/dist/client/settlement/execution.js.map +1 -0
- package/dist/client/settlement/reconciliation.d.ts +10 -0
- package/dist/client/settlement/reconciliation.d.ts.map +1 -0
- package/dist/client/settlement/reconciliation.js +73 -0
- package/dist/client/settlement/reconciliation.js.map +1 -0
- package/dist/client/settlement/settleSale.d.ts +21 -0
- package/dist/client/settlement/settleSale.d.ts.map +1 -0
- package/dist/client/settlement/settleSale.js +27 -0
- package/dist/client/settlement/settleSale.js.map +1 -0
- package/dist/client/settlement/verifiedSale.d.ts +25 -0
- package/dist/client/settlement/verifiedSale.d.ts.map +1 -0
- package/dist/client/settlement/verifiedSale.js +100 -0
- package/dist/client/settlement/verifiedSale.js.map +1 -0
- package/dist/client/webhooks/asyncHooks.d.ts +5 -0
- package/dist/client/webhooks/asyncHooks.d.ts.map +1 -0
- package/dist/client/webhooks/asyncHooks.js +20 -0
- package/dist/client/webhooks/asyncHooks.js.map +1 -0
- package/dist/component/_generated/component.d.ts +115 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/connect/queries.d.ts +30 -1
- package/dist/component/connect/queries.d.ts.map +1 -1
- package/dist/component/connect/queries.js +19 -2
- package/dist/component/connect/queries.js.map +1 -1
- package/dist/component/schema.d.ts +67 -0
- package/dist/component/schema.js +4 -0
- package/dist/component/schema.js.map +1 -1
- package/dist/component/settlement/mutations.d.ts +47 -0
- package/dist/component/settlement/mutations.d.ts.map +1 -0
- package/dist/component/settlement/mutations.js +104 -0
- package/dist/component/settlement/mutations.js.map +1 -0
- package/dist/component/settlement/queries.d.ts +29 -0
- package/dist/component/settlement/queries.d.ts.map +1 -0
- package/dist/component/settlement/queries.js +35 -0
- package/dist/component/settlement/queries.js.map +1 -0
- package/dist/component/settlement/schema.d.ts +99 -0
- package/dist/component/settlement/schema.d.ts.map +1 -0
- package/dist/component/settlement/schema.js +35 -0
- package/dist/component/settlement/schema.js.map +1 -0
- package/dist/react/workflows/AffiliatePayoutEditor.d.ts +41 -0
- package/dist/react/workflows/AffiliatePayoutEditor.d.ts.map +1 -0
- package/dist/react/workflows/AffiliatePayoutEditor.js +80 -0
- package/dist/react/workflows/AffiliatePayoutEditor.js.map +1 -0
- package/dist/react/workflows/AffiliateRateEditor.d.ts +26 -0
- package/dist/react/workflows/AffiliateRateEditor.d.ts.map +1 -0
- package/dist/react/workflows/AffiliateRateEditor.js +49 -0
- package/dist/react/workflows/AffiliateRateEditor.js.map +1 -0
- package/dist/react/workflows/Checkout.d.ts +3 -1
- package/dist/react/workflows/Checkout.d.ts.map +1 -1
- package/dist/react/workflows/Checkout.js +3 -2
- package/dist/react/workflows/Checkout.js.map +1 -1
- package/dist/react/workflows/CheckoutForms.d.ts +2 -0
- package/dist/react/workflows/CheckoutForms.d.ts.map +1 -1
- package/dist/react/workflows/CheckoutForms.js +22 -16
- package/dist/react/workflows/CheckoutForms.js.map +1 -1
- package/dist/react/workflows/IntegrationSwitch.js +2 -2
- package/dist/react/workflows/IntegrationSwitch.js.map +1 -1
- package/dist/react/workflows/Pagination.d.ts +32 -0
- package/dist/react/workflows/Pagination.d.ts.map +1 -0
- package/dist/react/workflows/Pagination.js +49 -0
- package/dist/react/workflows/Pagination.js.map +1 -0
- package/dist/react/workflows/Payouts.d.ts +3 -18
- package/dist/react/workflows/Payouts.d.ts.map +1 -1
- package/dist/react/workflows/Payouts.js +2 -2
- package/dist/react/workflows/Payouts.js.map +1 -1
- package/dist/react/workflows/PlanPricingEditor.d.ts +35 -0
- package/dist/react/workflows/PlanPricingEditor.d.ts.map +1 -0
- package/dist/react/workflows/PlanPricingEditor.js +64 -0
- package/dist/react/workflows/PlanPricingEditor.js.map +1 -0
- package/dist/react/workflows/index.d.ts +8 -0
- package/dist/react/workflows/index.d.ts.map +1 -1
- package/dist/react/workflows/index.js +4 -0
- package/dist/react/workflows/index.js.map +1 -1
- package/docs/react-workflows-audit.md +19 -0
- package/docs/react-workflows.md +94 -3
- package/docs/settlement.md +158 -0
- package/package.json +6 -4
- package/src/client/billing/checkout-idempotency.test.ts +82 -0
- package/src/client/billing/checkout.ts +6 -1
- package/src/client/billing/providerReads.test.ts +60 -0
- package/src/client/billing/providerReads.ts +30 -0
- package/src/client/billing/subscriptions.ts +1 -8
- package/src/client/connect/payoutSnapshot.test.ts +91 -0
- package/src/client/connect/payoutSnapshot.ts +130 -0
- package/src/client/connect/transfers.test.ts +15 -4
- package/src/client/connect/transfers.ts +13 -42
- package/src/client/core/account-idempotency.test.ts +78 -0
- package/src/client/core/accounts.ts +18 -25
- package/src/client/core/idempotency.test.ts +10 -0
- package/src/client/core/idempotency.ts +11 -0
- package/src/client/index.ts +84 -103
- package/src/client/settlement/allocation.test.ts +98 -0
- package/src/client/settlement/allocation.ts +110 -0
- package/src/client/settlement/commissions.test.ts +103 -0
- package/src/client/settlement/commissions.ts +125 -0
- package/src/client/settlement/execution.test.ts +186 -0
- package/src/client/settlement/execution.ts +134 -0
- package/src/client/settlement/reconciliation.ts +119 -0
- package/src/client/settlement/settleSale.ts +59 -0
- package/src/client/settlement/verifiedSale.test.ts +139 -0
- package/src/client/settlement/verifiedSale.ts +159 -0
- package/src/client/webhooks/asyncHooks.ts +38 -0
- package/src/client/webhooks/processors.test.ts +9 -9
- package/src/component/_generated/api.ts +4 -0
- package/src/component/_generated/component.ts +141 -0
- package/src/component/connect/queries.ts +26 -2
- package/src/component/earnings-totals.test.ts +13 -0
- package/src/component/schema.ts +8 -0
- package/src/component/settlement/mutations.ts +119 -0
- package/src/component/settlement/queries.ts +38 -0
- package/src/component/settlement/schema.ts +45 -0
- package/src/component/settlement-claims.test.ts +77 -0
- package/src/component/transfers.test.ts +48 -0
- package/src/react/components/EmbeddedDisputes.render.test.tsx +2 -2
- package/src/react/index.payment-element.test.ts +22 -37
- package/src/react/workflows/AffiliatePayoutEditor.test.tsx +90 -0
- package/src/react/workflows/AffiliatePayoutEditor.tsx +201 -0
- package/src/react/workflows/AffiliateRateEditor.test.tsx +36 -0
- package/src/react/workflows/AffiliateRateEditor.tsx +92 -0
- package/src/react/workflows/Checkout.test.tsx +30 -0
- package/src/react/workflows/Checkout.tsx +10 -3
- package/src/react/workflows/CheckoutForms.test.tsx +40 -0
- package/src/react/workflows/CheckoutForms.tsx +37 -16
- package/src/react/workflows/IntegrationSwitch.test.tsx +25 -0
- package/src/react/workflows/IntegrationSwitch.tsx +2 -2
- package/src/react/workflows/Pagination.test.tsx +53 -0
- package/src/react/workflows/Pagination.tsx +93 -0
- package/src/react/workflows/Payouts.test.tsx +33 -0
- package/src/react/workflows/Payouts.tsx +21 -15
- package/src/react/workflows/PlanPricingEditor.test.tsx +79 -0
- package/src/react/workflows/PlanPricingEditor.tsx +156 -0
- package/src/react/workflows/index.ts +22 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { mutation } from "../_generated/server.js";
|
|
3
|
+
import { allocationFields, commissionFields, termValidator } from "./schema.js";
|
|
4
|
+
const doc = v.object({
|
|
5
|
+
...allocationFields,
|
|
6
|
+
_id: v.id("settlementAllocations"),
|
|
7
|
+
_creationTime: v.number(),
|
|
8
|
+
});
|
|
9
|
+
export const reserveSettlementAllocation = mutation({
|
|
10
|
+
args: allocationFields,
|
|
11
|
+
returns: doc,
|
|
12
|
+
handler: async (ctx, args) => {
|
|
13
|
+
const existing = await ctx.db
|
|
14
|
+
.query("settlementAllocations")
|
|
15
|
+
.withIndex("by_source_charge", (q) =>
|
|
16
|
+
q.eq("sourceChargeId", args.sourceChargeId),
|
|
17
|
+
)
|
|
18
|
+
.first();
|
|
19
|
+
if (existing) {
|
|
20
|
+
if (
|
|
21
|
+
existing.currency !== args.currency ||
|
|
22
|
+
existing.paymentIntentId !== args.paymentIntentId ||
|
|
23
|
+
existing.sourceInvoiceId !== args.sourceInvoiceId
|
|
24
|
+
)
|
|
25
|
+
throw new Error("Settlement source identity cannot change.");
|
|
26
|
+
return existing;
|
|
27
|
+
}
|
|
28
|
+
const adjustment = args.adjustmentAmount ?? 0;
|
|
29
|
+
const amounts = [
|
|
30
|
+
args.gross,
|
|
31
|
+
args.processingFee,
|
|
32
|
+
args.platformNet,
|
|
33
|
+
...args.transfers.map((t) => t.amount),
|
|
34
|
+
];
|
|
35
|
+
const keys = args.transfers.map(
|
|
36
|
+
(t) => `${t.destinationAccountId}:${t.role}`,
|
|
37
|
+
);
|
|
38
|
+
if (
|
|
39
|
+
!Number.isSafeInteger(adjustment) ||
|
|
40
|
+
args.processingFee + args.platformNet + adjustment < 0 ||
|
|
41
|
+
amounts.some((n) => !Number.isSafeInteger(n) || n < 0) ||
|
|
42
|
+
args.gross <= 0 ||
|
|
43
|
+
!args.currency ||
|
|
44
|
+
!args.sourceChargeId ||
|
|
45
|
+
args.transfers.some((t) => !t.destinationAccountId) ||
|
|
46
|
+
new Set(keys).size !== keys.length ||
|
|
47
|
+
args.gross !==
|
|
48
|
+
args.processingFee +
|
|
49
|
+
args.platformNet +
|
|
50
|
+
adjustment +
|
|
51
|
+
args.transfers.reduce((sum, t) => sum + t.amount, 0)
|
|
52
|
+
)
|
|
53
|
+
throw new Error(
|
|
54
|
+
"Settlement allocation must reconcile in integer currency units with unique recipient legs.",
|
|
55
|
+
);
|
|
56
|
+
const id = await ctx.db.insert("settlementAllocations", args);
|
|
57
|
+
return (await ctx.db.get("settlementAllocations", id))!;
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
export const claimCommissionInvoice = mutation({
|
|
61
|
+
args: { ...commissionFields, invoiceId: v.string() },
|
|
62
|
+
returns: v.object({
|
|
63
|
+
eligible: v.boolean(),
|
|
64
|
+
term: termValidator,
|
|
65
|
+
firstPaidInvoiceId: v.string(),
|
|
66
|
+
claimKey: v.string(),
|
|
67
|
+
replay: v.boolean(),
|
|
68
|
+
}),
|
|
69
|
+
handler: async (ctx, args) => {
|
|
70
|
+
let stored = await ctx.db
|
|
71
|
+
.query("commissionTerms")
|
|
72
|
+
.withIndex("by_subscription", (q) =>
|
|
73
|
+
q.eq("subscriptionId", args.subscriptionId),
|
|
74
|
+
)
|
|
75
|
+
.first();
|
|
76
|
+
if (stored && stored.affiliateAccountId !== args.affiliateAccountId)
|
|
77
|
+
throw new Error("Subscription affiliate identity cannot change.");
|
|
78
|
+
if (!stored) {
|
|
79
|
+
const fields = {
|
|
80
|
+
subscriptionId: args.subscriptionId,
|
|
81
|
+
term: args.term,
|
|
82
|
+
affiliateAccountId: args.affiliateAccountId,
|
|
83
|
+
firstPaidInvoiceId: args.firstPaidInvoiceId,
|
|
84
|
+
};
|
|
85
|
+
const id = await ctx.db.insert("commissionTerms", fields);
|
|
86
|
+
stored = (await ctx.db.get("commissionTerms", id))!;
|
|
87
|
+
}
|
|
88
|
+
const existing = await ctx.db
|
|
89
|
+
.query("commissionDecisions")
|
|
90
|
+
.withIndex("by_subscription_invoice", (q) =>
|
|
91
|
+
q
|
|
92
|
+
.eq("subscriptionId", args.subscriptionId)
|
|
93
|
+
.eq("invoiceId", args.invoiceId),
|
|
94
|
+
)
|
|
95
|
+
.first();
|
|
96
|
+
const claimKey =
|
|
97
|
+
existing?.claimKey ??
|
|
98
|
+
`commission:${args.subscriptionId}:${args.invoiceId}`;
|
|
99
|
+
const eligible =
|
|
100
|
+
existing?.eligible ??
|
|
101
|
+
(!!stored.affiliateAccountId &&
|
|
102
|
+
(stored.term === "forever" ||
|
|
103
|
+
args.invoiceId === stored.firstPaidInvoiceId));
|
|
104
|
+
if (!existing)
|
|
105
|
+
await ctx.db.insert("commissionDecisions", {
|
|
106
|
+
subscriptionId: args.subscriptionId,
|
|
107
|
+
invoiceId: args.invoiceId,
|
|
108
|
+
eligible,
|
|
109
|
+
claimKey,
|
|
110
|
+
});
|
|
111
|
+
return {
|
|
112
|
+
eligible,
|
|
113
|
+
term: stored.term,
|
|
114
|
+
firstPaidInvoiceId: stored.firstPaidInvoiceId,
|
|
115
|
+
claimKey,
|
|
116
|
+
replay: !!existing,
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { query } from "../_generated/server.js";
|
|
3
|
+
import { allocationFields, commissionFields } from "./schema.js";
|
|
4
|
+
const doc = v.object({
|
|
5
|
+
...allocationFields,
|
|
6
|
+
_id: v.id("settlementAllocations"),
|
|
7
|
+
_creationTime: v.number(),
|
|
8
|
+
});
|
|
9
|
+
export const getSettlementAllocation = query({
|
|
10
|
+
args: { sourceChargeId: v.string() },
|
|
11
|
+
returns: v.union(doc, v.null()),
|
|
12
|
+
handler: (ctx, args) =>
|
|
13
|
+
ctx.db
|
|
14
|
+
.query("settlementAllocations")
|
|
15
|
+
.withIndex("by_source_charge", (q) =>
|
|
16
|
+
q.eq("sourceChargeId", args.sourceChargeId),
|
|
17
|
+
)
|
|
18
|
+
.first(),
|
|
19
|
+
});
|
|
20
|
+
export const getCommissionTerm = query({
|
|
21
|
+
args: { subscriptionId: v.string() },
|
|
22
|
+
returns: v.union(v.object(commissionFields), v.null()),
|
|
23
|
+
handler: async (ctx, args) => {
|
|
24
|
+
const row = await ctx.db
|
|
25
|
+
.query("commissionTerms")
|
|
26
|
+
.withIndex("by_subscription", (q) =>
|
|
27
|
+
q.eq("subscriptionId", args.subscriptionId),
|
|
28
|
+
)
|
|
29
|
+
.first();
|
|
30
|
+
if (!row) return null;
|
|
31
|
+
return {
|
|
32
|
+
subscriptionId: row.subscriptionId,
|
|
33
|
+
term: row.term,
|
|
34
|
+
affiliateAccountId: row.affiliateAccountId,
|
|
35
|
+
firstPaidInvoiceId: row.firstPaidInvoiceId,
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { defineTable } from "convex/server";
|
|
2
|
+
import { v } from "convex/values";
|
|
3
|
+
import { splitRecipientRoleValidator } from "../lib/fees.js";
|
|
4
|
+
export const termValidator = v.union(
|
|
5
|
+
v.literal("forever"),
|
|
6
|
+
v.literal("one_time"),
|
|
7
|
+
);
|
|
8
|
+
export const allocationFields = {
|
|
9
|
+
sourceChargeId: v.string(),
|
|
10
|
+
paymentIntentId: v.optional(v.string()),
|
|
11
|
+
sourceInvoiceId: v.optional(v.string()),
|
|
12
|
+
gross: v.number(),
|
|
13
|
+
currency: v.string(),
|
|
14
|
+
processingFee: v.number(),
|
|
15
|
+
platformNet: v.number(),
|
|
16
|
+
adjustmentAmount: v.optional(v.number()),
|
|
17
|
+
paymentId: v.optional(v.string()),
|
|
18
|
+
transfers: v.array(
|
|
19
|
+
v.object({
|
|
20
|
+
destinationAccountId: v.string(),
|
|
21
|
+
role: splitRecipientRoleValidator,
|
|
22
|
+
amount: v.number(),
|
|
23
|
+
}),
|
|
24
|
+
),
|
|
25
|
+
};
|
|
26
|
+
export const allocationsTable = defineTable(allocationFields).index(
|
|
27
|
+
"by_source_charge",
|
|
28
|
+
["sourceChargeId"],
|
|
29
|
+
);
|
|
30
|
+
export const commissionFields = {
|
|
31
|
+
subscriptionId: v.string(),
|
|
32
|
+
term: termValidator,
|
|
33
|
+
affiliateAccountId: v.optional(v.string()),
|
|
34
|
+
firstPaidInvoiceId: v.string(),
|
|
35
|
+
};
|
|
36
|
+
export const commissionTermsTable = defineTable(commissionFields).index(
|
|
37
|
+
"by_subscription",
|
|
38
|
+
["subscriptionId"],
|
|
39
|
+
);
|
|
40
|
+
export const commissionDecisionsTable = defineTable({
|
|
41
|
+
subscriptionId: v.string(),
|
|
42
|
+
invoiceId: v.string(),
|
|
43
|
+
eligible: v.boolean(),
|
|
44
|
+
claimKey: v.string(),
|
|
45
|
+
}).index("by_subscription_invoice", ["subscriptionId", "invoiceId"]);
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
it("keeps a claimed allocation immutable and isolated per source charge", async () => {
|
|
8
|
+
const t = convexTest(schema, modules);
|
|
9
|
+
const opts = {
|
|
10
|
+
sourceChargeId: "ch_1",
|
|
11
|
+
paymentIntentId: "pi_1",
|
|
12
|
+
gross: 100,
|
|
13
|
+
currency: "usd",
|
|
14
|
+
processingFee: 3,
|
|
15
|
+
platformNet: 7,
|
|
16
|
+
transfers: [
|
|
17
|
+
{ destinationAccountId: "acct_c", amount: 90, role: "store" as const },
|
|
18
|
+
],
|
|
19
|
+
};
|
|
20
|
+
const first = await t.mutation(
|
|
21
|
+
api.settlement.mutations.reserveSettlementAllocation,
|
|
22
|
+
opts,
|
|
23
|
+
);
|
|
24
|
+
const replay = await t.mutation(
|
|
25
|
+
api.settlement.mutations.reserveSettlementAllocation,
|
|
26
|
+
{
|
|
27
|
+
...opts,
|
|
28
|
+
transfers: [{ ...opts.transfers[0], amount: 80 }],
|
|
29
|
+
platformNet: 17,
|
|
30
|
+
},
|
|
31
|
+
);
|
|
32
|
+
expect(replay).toEqual(first);
|
|
33
|
+
await expect(
|
|
34
|
+
t.mutation(api.settlement.mutations.reserveSettlementAllocation, {
|
|
35
|
+
...opts,
|
|
36
|
+
sourceChargeId: "ch_invalid",
|
|
37
|
+
gross: 1,
|
|
38
|
+
}),
|
|
39
|
+
).rejects.toThrow();
|
|
40
|
+
});
|
|
41
|
+
it("persists the term, identity and earliest invoice despite replay and term changes", async () => {
|
|
42
|
+
const t = convexTest(schema, modules);
|
|
43
|
+
const opts = {
|
|
44
|
+
subscriptionId: "sub_1",
|
|
45
|
+
invoiceId: "in_renewal",
|
|
46
|
+
firstPaidInvoiceId: "in_first",
|
|
47
|
+
term: "one_time" as const,
|
|
48
|
+
affiliateAccountId: "acct_a",
|
|
49
|
+
};
|
|
50
|
+
expect(
|
|
51
|
+
await t.mutation(api.settlement.mutations.claimCommissionInvoice, opts),
|
|
52
|
+
).toMatchObject({
|
|
53
|
+
eligible: false,
|
|
54
|
+
firstPaidInvoiceId: "in_first",
|
|
55
|
+
term: "one_time",
|
|
56
|
+
replay: false,
|
|
57
|
+
});
|
|
58
|
+
expect(
|
|
59
|
+
await t.mutation(api.settlement.mutations.claimCommissionInvoice, {
|
|
60
|
+
...opts,
|
|
61
|
+
invoiceId: "in_first",
|
|
62
|
+
term: "forever",
|
|
63
|
+
}),
|
|
64
|
+
).toMatchObject({ eligible: true, term: "one_time" });
|
|
65
|
+
expect(
|
|
66
|
+
await t.mutation(api.settlement.mutations.claimCommissionInvoice, {
|
|
67
|
+
...opts,
|
|
68
|
+
invoiceId: "in_first",
|
|
69
|
+
}),
|
|
70
|
+
).toMatchObject({ eligible: true, replay: true });
|
|
71
|
+
await expect(
|
|
72
|
+
t.mutation(api.settlement.mutations.claimCommissionInvoice, {
|
|
73
|
+
...opts,
|
|
74
|
+
affiliateAccountId: "acct_other",
|
|
75
|
+
}),
|
|
76
|
+
).rejects.toThrow();
|
|
77
|
+
});
|
|
@@ -469,3 +469,51 @@ describe("claimReversalSlices (BTS-63)", () => {
|
|
|
469
469
|
expect(rival.slices).toEqual([]);
|
|
470
470
|
});
|
|
471
471
|
});
|
|
472
|
+
|
|
473
|
+
it("paginates an account's newest transfers beyond 50 without duplicates or other accounts", async () => {
|
|
474
|
+
const t = convexTest(schema, modules);
|
|
475
|
+
for (let i = 0; i < 57; i++) {
|
|
476
|
+
await t.mutation(api.connect.mutations.upsertTransfer, {
|
|
477
|
+
stripeTransferId: `tr_page_${i}`,
|
|
478
|
+
destinationAccountId: "acct_pages",
|
|
479
|
+
amount: 100,
|
|
480
|
+
currency: "usd",
|
|
481
|
+
status: "paid",
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
await t.mutation(api.connect.mutations.upsertTransfer, {
|
|
485
|
+
stripeTransferId: "tr_foreign",
|
|
486
|
+
destinationAccountId: "acct_other",
|
|
487
|
+
amount: 200,
|
|
488
|
+
currency: "usd",
|
|
489
|
+
status: "paid",
|
|
490
|
+
});
|
|
491
|
+
let cursor: string | null = null;
|
|
492
|
+
const ids: string[] = [];
|
|
493
|
+
const sizes: number[] = [];
|
|
494
|
+
for (;;) {
|
|
495
|
+
const result: {
|
|
496
|
+
page: { stripeTransferId: string }[];
|
|
497
|
+
isDone: boolean;
|
|
498
|
+
continueCursor: string;
|
|
499
|
+
} = await t.query(api.connect.queries.paginateTransfersByAccount, {
|
|
500
|
+
destinationAccountId: "acct_pages",
|
|
501
|
+
paginationOpts: { numItems: 10, cursor },
|
|
502
|
+
});
|
|
503
|
+
sizes.push(result.page.length);
|
|
504
|
+
ids.push(...result.page.map((row) => row.stripeTransferId));
|
|
505
|
+
if (result.isDone) break;
|
|
506
|
+
cursor = result.continueCursor;
|
|
507
|
+
}
|
|
508
|
+
expect(sizes).toEqual([10, 10, 10, 10, 10, 7]);
|
|
509
|
+
expect(ids).toEqual(
|
|
510
|
+
Array.from({ length: 57 }, (_, i) => `tr_page_${56 - i}`),
|
|
511
|
+
);
|
|
512
|
+
expect(new Set(ids).size).toBe(57);
|
|
513
|
+
const empty = await t.query(api.connect.queries.paginateTransfersByAccount, {
|
|
514
|
+
destinationAccountId: "acct_empty",
|
|
515
|
+
paginationOpts: { numItems: 10, cursor: null },
|
|
516
|
+
});
|
|
517
|
+
expect(empty.page).toEqual([]);
|
|
518
|
+
expect(empty.isDone).toBe(true);
|
|
519
|
+
});
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* tree rendering without throwing via react-dom/server, the disputes_list vs
|
|
11
11
|
* payment_disputes mount switch, and the headless overrides.
|
|
12
12
|
*/
|
|
13
|
+
import * as barrel from "../index.js";
|
|
13
14
|
import { createElement } from "react";
|
|
14
15
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
15
16
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
@@ -136,8 +137,7 @@ describe("EmbeddedDisputes", () => {
|
|
|
136
137
|
});
|
|
137
138
|
|
|
138
139
|
describe("react barrel exports", () => {
|
|
139
|
-
it("exports ConnectProvider and EmbeddedDisputes",
|
|
140
|
-
const barrel = await import("../index.js");
|
|
140
|
+
it("exports ConnectProvider and EmbeddedDisputes", () => {
|
|
141
141
|
expect(barrel.ConnectProvider).toBe(ConnectProvider);
|
|
142
142
|
expect(barrel.EmbeddedDisputes).toBe(EmbeddedDisputes);
|
|
143
143
|
});
|
|
@@ -1,41 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const elements = vi.hoisted(() => ({
|
|
4
|
+
checkout: Symbol("checkout-payment-element"),
|
|
5
|
+
paymentIntent: Symbol("elements-payment-element"),
|
|
6
|
+
}));
|
|
7
|
+
vi.mock("@stripe/react-stripe-js", async () => ({
|
|
8
|
+
...(await vi.importActual<typeof import("@stripe/react-stripe-js")>(
|
|
9
|
+
"@stripe/react-stripe-js",
|
|
10
|
+
)),
|
|
11
|
+
PaymentElement: elements.paymentIntent,
|
|
12
|
+
}));
|
|
13
|
+
vi.mock("@stripe/react-stripe-js/checkout", async () => ({
|
|
14
|
+
...(await vi.importActual<typeof import("@stripe/react-stripe-js/checkout")>(
|
|
15
|
+
"@stripe/react-stripe-js/checkout",
|
|
16
|
+
)),
|
|
17
|
+
PaymentElement: elements.checkout,
|
|
18
|
+
}));
|
|
19
|
+
import * as reactExports from "./index.js";
|
|
2
20
|
|
|
3
21
|
describe("better-stripe/react PaymentElement exports", () => {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
vi.doUnmock("@stripe/react-stripe-js/checkout");
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it("uses checkout PaymentElement as the default export and keeps the Elements variant aliased", async () => {
|
|
11
|
-
const checkoutPaymentElement = Symbol("checkout-payment-element");
|
|
12
|
-
const elementsPaymentElement = Symbol("elements-payment-element");
|
|
13
|
-
|
|
14
|
-
vi.doMock("@stripe/react-stripe-js", async () => {
|
|
15
|
-
const actual = await vi.importActual<
|
|
16
|
-
typeof import("@stripe/react-stripe-js")
|
|
17
|
-
>("@stripe/react-stripe-js");
|
|
18
|
-
|
|
19
|
-
return {
|
|
20
|
-
...actual,
|
|
21
|
-
PaymentElement: elementsPaymentElement,
|
|
22
|
-
};
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
vi.doMock("@stripe/react-stripe-js/checkout", async () => {
|
|
26
|
-
const actual = await vi.importActual<
|
|
27
|
-
typeof import("@stripe/react-stripe-js/checkout")
|
|
28
|
-
>("@stripe/react-stripe-js/checkout");
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
...actual,
|
|
32
|
-
PaymentElement: checkoutPaymentElement,
|
|
33
|
-
};
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const reactExports = await import("./index.js");
|
|
37
|
-
|
|
38
|
-
expect(reactExports.PaymentElement).toBe(checkoutPaymentElement);
|
|
39
|
-
expect(reactExports.ElementsPaymentElement).toBe(elementsPaymentElement);
|
|
22
|
+
it("uses checkout PaymentElement as the default export and keeps the Elements variant aliased", () => {
|
|
23
|
+
expect(reactExports.PaymentElement).toBe(elements.checkout);
|
|
24
|
+
expect(reactExports.ElementsPaymentElement).toBe(elements.paymentIntent);
|
|
40
25
|
});
|
|
41
26
|
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { cleanup, render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { afterEach, expect, it, vi } from "vitest";
|
|
5
|
+
import { AffiliatePayoutEditor } from "./AffiliatePayoutEditor.js";
|
|
6
|
+
afterEach(cleanup);
|
|
7
|
+
it("saves either a percentage or a currency amount and clears inheritance", async () => {
|
|
8
|
+
const save = vi.fn().mockResolvedValue(undefined);
|
|
9
|
+
render(
|
|
10
|
+
<AffiliatePayoutEditor
|
|
11
|
+
scopeKey="price"
|
|
12
|
+
affiliateBasisPoints={null}
|
|
13
|
+
effectiveBasisPoints={2000}
|
|
14
|
+
version={0}
|
|
15
|
+
currency="eur"
|
|
16
|
+
onSave={save}
|
|
17
|
+
/>,
|
|
18
|
+
);
|
|
19
|
+
const input = screen.getByLabelText("Affiliate payout");
|
|
20
|
+
await userEvent.type(input, "12.5");
|
|
21
|
+
await userEvent.click(screen.getByRole("button", { name: "Save" }));
|
|
22
|
+
expect(save).toHaveBeenLastCalledWith({
|
|
23
|
+
affiliateBasisPoints: 1250,
|
|
24
|
+
affiliateFixedAmount: null,
|
|
25
|
+
affiliateCommissionTerm: null,
|
|
26
|
+
expectedVersion: 0,
|
|
27
|
+
});
|
|
28
|
+
await userEvent.click(
|
|
29
|
+
screen.getByRole("button", { name: "Fixed amount in EUR" }),
|
|
30
|
+
);
|
|
31
|
+
await userEvent.click(screen.getByRole("button", { name: "Save" }));
|
|
32
|
+
expect(save).toHaveBeenLastCalledWith({
|
|
33
|
+
affiliateBasisPoints: null,
|
|
34
|
+
affiliateFixedAmount: 12.5,
|
|
35
|
+
affiliateCommissionTerm: null,
|
|
36
|
+
expectedVersion: 0,
|
|
37
|
+
});
|
|
38
|
+
await userEvent.click(
|
|
39
|
+
screen.getByRole("button", { name: "Clear affiliate payout" }),
|
|
40
|
+
);
|
|
41
|
+
expect(save).toHaveBeenLastCalledWith({
|
|
42
|
+
affiliateBasisPoints: null,
|
|
43
|
+
affiliateFixedAmount: null,
|
|
44
|
+
affiliateCommissionTerm: null,
|
|
45
|
+
expectedVersion: 0,
|
|
46
|
+
});
|
|
47
|
+
expect((input as HTMLInputElement).value).toBe("");
|
|
48
|
+
});
|
|
49
|
+
it("rejects excessive percentages and invalid decimals", async () => {
|
|
50
|
+
const save = vi.fn();
|
|
51
|
+
render(
|
|
52
|
+
<AffiliatePayoutEditor
|
|
53
|
+
scopeKey="product"
|
|
54
|
+
affiliateBasisPoints={null}
|
|
55
|
+
effectiveBasisPoints={2000}
|
|
56
|
+
version={0}
|
|
57
|
+
onSave={save}
|
|
58
|
+
/>,
|
|
59
|
+
);
|
|
60
|
+
await userEvent.type(screen.getByLabelText("Affiliate payout"), "101");
|
|
61
|
+
await userEvent.click(screen.getByRole("button", { name: "Save" }));
|
|
62
|
+
expect(screen.getByRole("alert").textContent).toMatch(/100/);
|
|
63
|
+
expect(save).not.toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
it("saves a one-time subscription commission and clears its term to inheritance", async () => {
|
|
66
|
+
const save = vi.fn().mockResolvedValue(undefined);
|
|
67
|
+
render(
|
|
68
|
+
<AffiliatePayoutEditor
|
|
69
|
+
scopeKey="term"
|
|
70
|
+
affiliateBasisPoints={1000}
|
|
71
|
+
effectiveBasisPoints={1000}
|
|
72
|
+
version={0}
|
|
73
|
+
onSave={save}
|
|
74
|
+
/>,
|
|
75
|
+
);
|
|
76
|
+
await userEvent.selectOptions(
|
|
77
|
+
screen.getByLabelText("Commission term"),
|
|
78
|
+
"one_time",
|
|
79
|
+
);
|
|
80
|
+
await userEvent.click(screen.getByRole("button", { name: "Save" }));
|
|
81
|
+
expect(save).toHaveBeenLastCalledWith(
|
|
82
|
+
expect.objectContaining({ affiliateCommissionTerm: "one_time" }),
|
|
83
|
+
);
|
|
84
|
+
await userEvent.click(
|
|
85
|
+
screen.getByRole("button", { name: "Clear affiliate payout" }),
|
|
86
|
+
);
|
|
87
|
+
expect(save).toHaveBeenLastCalledWith(
|
|
88
|
+
expect.objectContaining({ affiliateCommissionTerm: null }),
|
|
89
|
+
);
|
|
90
|
+
});
|