@fantastic.dev/better-stripe 0.17.1 → 0.18.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 (50) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +7 -1
  3. package/dist/client/connect/payouts.d.ts +3 -2
  4. package/dist/client/connect/payouts.d.ts.map +1 -1
  5. package/dist/client/connect/payouts.js.map +1 -1
  6. package/dist/client/index.d.ts +4 -8
  7. package/dist/client/index.d.ts.map +1 -1
  8. package/dist/client/index.js +5 -0
  9. package/dist/client/index.js.map +1 -1
  10. package/dist/client/payoutPolicy.d.ts +81 -0
  11. package/dist/client/payoutPolicy.d.ts.map +1 -0
  12. package/dist/client/payoutPolicy.js +262 -0
  13. package/dist/client/payoutPolicy.js.map +1 -0
  14. package/dist/component/_generated/component.d.ts +273 -0
  15. package/dist/component/_generated/component.d.ts.map +1 -1
  16. package/dist/component/earnings/triggers.d.ts +150 -0
  17. package/dist/component/earnings/triggers.d.ts.map +1 -1
  18. package/dist/component/payoutPolicy/helpers.d.ts +51 -0
  19. package/dist/component/payoutPolicy/helpers.d.ts.map +1 -0
  20. package/dist/component/payoutPolicy/helpers.js +68 -0
  21. package/dist/component/payoutPolicy/helpers.js.map +1 -0
  22. package/dist/component/payoutPolicy/mutations.d.ts +172 -0
  23. package/dist/component/payoutPolicy/mutations.d.ts.map +1 -0
  24. package/dist/component/payoutPolicy/mutations.js +191 -0
  25. package/dist/component/payoutPolicy/mutations.js.map +1 -0
  26. package/dist/component/payoutPolicy/queries.d.ts +97 -0
  27. package/dist/component/payoutPolicy/queries.d.ts.map +1 -0
  28. package/dist/component/payoutPolicy/queries.js +51 -0
  29. package/dist/component/payoutPolicy/queries.js.map +1 -0
  30. package/dist/component/payoutPolicy/schema.d.ts +396 -0
  31. package/dist/component/payoutPolicy/schema.d.ts.map +1 -0
  32. package/dist/component/payoutPolicy/schema.js +54 -0
  33. package/dist/component/payoutPolicy/schema.js.map +1 -0
  34. package/dist/component/schema.d.ts +131 -0
  35. package/dist/component/schema.js +3 -0
  36. package/dist/component/schema.js.map +1 -1
  37. package/docs/settlement.md +62 -0
  38. package/package.json +1 -1
  39. package/src/client/connect/payouts.ts +10 -8
  40. package/src/client/index.ts +7 -11
  41. package/src/client/payoutPolicy.test.ts +320 -0
  42. package/src/client/payoutPolicy.ts +409 -0
  43. package/src/component/_generated/api.ts +6 -0
  44. package/src/component/_generated/component.ts +280 -0
  45. package/src/component/payout-policy.test.ts +248 -0
  46. package/src/component/payoutPolicy/helpers.ts +88 -0
  47. package/src/component/payoutPolicy/mutations.ts +225 -0
  48. package/src/component/payoutPolicy/queries.ts +56 -0
  49. package/src/component/payoutPolicy/schema.ts +67 -0
  50. package/src/component/schema.ts +3 -0
@@ -2194,6 +2194,286 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
2194
2194
  >;
2195
2195
  };
2196
2196
  };
2197
+ payoutPolicy: {
2198
+ mutations: {
2199
+ beginAttempt: FunctionReference<
2200
+ "mutation",
2201
+ "internal",
2202
+ { accountId: string; operationKey: string; ownerKey: string },
2203
+ {
2204
+ _creationTime: number;
2205
+ _id: string;
2206
+ accountId: string;
2207
+ attempted?: boolean;
2208
+ country: string;
2209
+ expectedRevision: number;
2210
+ mode: "stripe_managed" | "custom";
2211
+ observedAt?: number;
2212
+ observedSchedule: {
2213
+ interval: "daily" | "weekly" | "monthly" | "manual";
2214
+ monthlyDays?: Array<number>;
2215
+ weeklyDays?: Array<string>;
2216
+ };
2217
+ operationKey: string;
2218
+ ownerKey: string;
2219
+ previousSchedule: {
2220
+ interval: "daily" | "weekly" | "monthly" | "manual";
2221
+ monthlyDays?: Array<number>;
2222
+ weeklyDays?: Array<string>;
2223
+ };
2224
+ revision: number;
2225
+ status: "pending" | "complete" | "canceled";
2226
+ targetSchedule: {
2227
+ interval: "daily" | "weekly" | "monthly" | "manual";
2228
+ monthlyDays?: Array<number>;
2229
+ weeklyDays?: Array<string>;
2230
+ };
2231
+ thresholds: Array<{ amount: number; currency: string }>;
2232
+ },
2233
+ Name
2234
+ >;
2235
+ cancelUnattempted: FunctionReference<
2236
+ "mutation",
2237
+ "internal",
2238
+ {
2239
+ accountId: string;
2240
+ observedSchedule: {
2241
+ interval: "daily" | "weekly" | "monthly" | "manual";
2242
+ monthlyDays?: Array<number>;
2243
+ weeklyDays?: Array<string>;
2244
+ };
2245
+ operationKey: string;
2246
+ ownerKey: string;
2247
+ },
2248
+ {
2249
+ _creationTime: number;
2250
+ _id: string;
2251
+ accountId: string;
2252
+ attempted?: boolean;
2253
+ country: string;
2254
+ expectedRevision: number;
2255
+ mode: "stripe_managed" | "custom";
2256
+ observedAt?: number;
2257
+ observedSchedule: {
2258
+ interval: "daily" | "weekly" | "monthly" | "manual";
2259
+ monthlyDays?: Array<number>;
2260
+ weeklyDays?: Array<string>;
2261
+ };
2262
+ operationKey: string;
2263
+ ownerKey: string;
2264
+ previousSchedule: {
2265
+ interval: "daily" | "weekly" | "monthly" | "manual";
2266
+ monthlyDays?: Array<number>;
2267
+ weeklyDays?: Array<string>;
2268
+ };
2269
+ revision: number;
2270
+ status: "pending" | "complete" | "canceled";
2271
+ targetSchedule: {
2272
+ interval: "daily" | "weekly" | "monthly" | "manual";
2273
+ monthlyDays?: Array<number>;
2274
+ weeklyDays?: Array<string>;
2275
+ };
2276
+ thresholds: Array<{ amount: number; currency: string }>;
2277
+ },
2278
+ Name
2279
+ >;
2280
+ confirm: FunctionReference<
2281
+ "mutation",
2282
+ "internal",
2283
+ {
2284
+ accountId: string;
2285
+ observedSchedule: {
2286
+ interval: "daily" | "weekly" | "monthly" | "manual";
2287
+ monthlyDays?: Array<number>;
2288
+ weeklyDays?: Array<string>;
2289
+ };
2290
+ operationKey: string;
2291
+ ownerKey: string;
2292
+ },
2293
+ {
2294
+ _creationTime: number;
2295
+ _id: string;
2296
+ accountId: string;
2297
+ attempted?: boolean;
2298
+ country: string;
2299
+ expectedRevision: number;
2300
+ mode: "stripe_managed" | "custom";
2301
+ observedAt?: number;
2302
+ observedSchedule: {
2303
+ interval: "daily" | "weekly" | "monthly" | "manual";
2304
+ monthlyDays?: Array<number>;
2305
+ weeklyDays?: Array<string>;
2306
+ };
2307
+ operationKey: string;
2308
+ ownerKey: string;
2309
+ previousSchedule: {
2310
+ interval: "daily" | "weekly" | "monthly" | "manual";
2311
+ monthlyDays?: Array<number>;
2312
+ weeklyDays?: Array<string>;
2313
+ };
2314
+ revision: number;
2315
+ status: "pending" | "complete" | "canceled";
2316
+ targetSchedule: {
2317
+ interval: "daily" | "weekly" | "monthly" | "manual";
2318
+ monthlyDays?: Array<number>;
2319
+ weeklyDays?: Array<string>;
2320
+ };
2321
+ thresholds: Array<{ amount: number; currency: string }>;
2322
+ },
2323
+ Name
2324
+ >;
2325
+ prepare: FunctionReference<
2326
+ "mutation",
2327
+ "internal",
2328
+ {
2329
+ accountId: string;
2330
+ country: string;
2331
+ expectedRevision: number;
2332
+ mode: "stripe_managed" | "custom";
2333
+ observedAt?: number;
2334
+ observedSchedule: {
2335
+ interval: "daily" | "weekly" | "monthly" | "manual";
2336
+ monthlyDays?: Array<number>;
2337
+ weeklyDays?: Array<string>;
2338
+ };
2339
+ operationKey: string;
2340
+ ownerKey: string;
2341
+ thresholds: Array<{ amount: number; currency: string }>;
2342
+ },
2343
+ {
2344
+ _creationTime: number;
2345
+ _id: string;
2346
+ accountId: string;
2347
+ attempted?: boolean;
2348
+ country: string;
2349
+ expectedRevision: number;
2350
+ mode: "stripe_managed" | "custom";
2351
+ observedAt?: number;
2352
+ observedSchedule: {
2353
+ interval: "daily" | "weekly" | "monthly" | "manual";
2354
+ monthlyDays?: Array<number>;
2355
+ weeklyDays?: Array<string>;
2356
+ };
2357
+ operationKey: string;
2358
+ ownerKey: string;
2359
+ previousSchedule: {
2360
+ interval: "daily" | "weekly" | "monthly" | "manual";
2361
+ monthlyDays?: Array<number>;
2362
+ weeklyDays?: Array<string>;
2363
+ };
2364
+ revision: number;
2365
+ status: "pending" | "complete" | "canceled";
2366
+ targetSchedule: {
2367
+ interval: "daily" | "weekly" | "monthly" | "manual";
2368
+ monthlyDays?: Array<number>;
2369
+ weeklyDays?: Array<string>;
2370
+ };
2371
+ thresholds: Array<{ amount: number; currency: string }>;
2372
+ },
2373
+ Name
2374
+ >;
2375
+ };
2376
+ queries: {
2377
+ due: FunctionReference<
2378
+ "query",
2379
+ "internal",
2380
+ {
2381
+ now: number;
2382
+ paginationOpts: {
2383
+ cursor: string | null;
2384
+ endCursor?: string | null;
2385
+ id?: number;
2386
+ maximumBytesRead?: number;
2387
+ maximumRowsRead?: number;
2388
+ numItems: number;
2389
+ };
2390
+ },
2391
+ {
2392
+ continueCursor: string;
2393
+ isDone: boolean;
2394
+ page: Array<{
2395
+ _creationTime: number;
2396
+ _id: string;
2397
+ accountId: string;
2398
+ country: string;
2399
+ holdDeadline?: number;
2400
+ mode: "stripe_managed" | "custom";
2401
+ operationKey: string;
2402
+ ownerKey: string;
2403
+ previousSchedule?: {
2404
+ interval: "daily" | "weekly" | "monthly" | "manual";
2405
+ monthlyDays?: Array<number>;
2406
+ weeklyDays?: Array<string>;
2407
+ };
2408
+ revision: number;
2409
+ status: "changing" | "ready";
2410
+ thresholds: Array<{ amount: number; currency: string }>;
2411
+ }>;
2412
+ },
2413
+ Name
2414
+ >;
2415
+ get: FunctionReference<
2416
+ "query",
2417
+ "internal",
2418
+ { accountId: string; ownerKey: string },
2419
+ {
2420
+ _creationTime: number;
2421
+ _id: string;
2422
+ accountId: string;
2423
+ country: string;
2424
+ holdDeadline?: number;
2425
+ mode: "stripe_managed" | "custom";
2426
+ operationKey: string;
2427
+ ownerKey: string;
2428
+ previousSchedule?: {
2429
+ interval: "daily" | "weekly" | "monthly" | "manual";
2430
+ monthlyDays?: Array<number>;
2431
+ weeklyDays?: Array<string>;
2432
+ };
2433
+ revision: number;
2434
+ status: "changing" | "ready";
2435
+ thresholds: Array<{ amount: number; currency: string }>;
2436
+ } | null,
2437
+ Name
2438
+ >;
2439
+ operation: FunctionReference<
2440
+ "query",
2441
+ "internal",
2442
+ { accountId: string; operationKey: string; ownerKey: string },
2443
+ {
2444
+ _creationTime: number;
2445
+ _id: string;
2446
+ accountId: string;
2447
+ attempted?: boolean;
2448
+ country: string;
2449
+ expectedRevision: number;
2450
+ mode: "stripe_managed" | "custom";
2451
+ observedAt?: number;
2452
+ observedSchedule: {
2453
+ interval: "daily" | "weekly" | "monthly" | "manual";
2454
+ monthlyDays?: Array<number>;
2455
+ weeklyDays?: Array<string>;
2456
+ };
2457
+ operationKey: string;
2458
+ ownerKey: string;
2459
+ previousSchedule: {
2460
+ interval: "daily" | "weekly" | "monthly" | "manual";
2461
+ monthlyDays?: Array<number>;
2462
+ weeklyDays?: Array<string>;
2463
+ };
2464
+ revision: number;
2465
+ status: "pending" | "complete" | "canceled";
2466
+ targetSchedule: {
2467
+ interval: "daily" | "weekly" | "monthly" | "manual";
2468
+ monthlyDays?: Array<number>;
2469
+ weeklyDays?: Array<string>;
2470
+ };
2471
+ thresholds: Array<{ amount: number; currency: string }>;
2472
+ } | null,
2473
+ Name
2474
+ >;
2475
+ };
2476
+ };
2197
2477
  products: {
2198
2478
  mutations: {
2199
2479
  upsertPrice: FunctionReference<
@@ -0,0 +1,248 @@
1
+ // @vitest-environment edge-runtime
2
+ import { convexTest } from "convex-test";
3
+ import { makeFunctionReference } from "convex/server";
4
+ import { expect, it } from "vitest";
5
+ import schema from "./schema.js";
6
+ const modules = import.meta.glob("./**/*.*s");
7
+ const m = (name: string) =>
8
+ makeFunctionReference<"mutation">(`payoutPolicy/mutations:${name}`);
9
+ const q = (name: string) =>
10
+ makeFunctionReference<"query">(`payoutPolicy/queries:${name}`);
11
+ const scope = { accountId: "acct_recipient", ownerKey: "user_1" };
12
+ const initial = {
13
+ ...scope,
14
+ operationKey: "enable-1",
15
+ expectedRevision: -1,
16
+ mode: "custom",
17
+ thresholds: [{ currency: "usd", amount: 1000 }],
18
+ country: "US",
19
+ observedSchedule: { interval: "weekly", weeklyDays: ["friday", "monday"] },
20
+ };
21
+ it("persists restoration and a deadline before provider mutation, then confirms exactly once", async () => {
22
+ const t = convexTest(schema, modules);
23
+ const op = await t.mutation(m("prepare"), initial);
24
+ expect(op.status).toBe("pending");
25
+ expect(op.targetSchedule).toEqual({ interval: "manual" });
26
+ expect(op.previousSchedule).toEqual(initial.observedSchedule);
27
+ let policy = await t.query(q("get"), scope);
28
+ expect(policy.status).toBe("changing");
29
+ expect(policy.revision).toBe(0);
30
+ expect(policy.holdDeadline).toBeGreaterThan(Date.now());
31
+ await t.mutation(m("confirm"), {
32
+ ...scope,
33
+ operationKey: "enable-1",
34
+ observedSchedule: { interval: "manual" },
35
+ });
36
+ policy = await t.query(q("get"), scope);
37
+ expect(policy.status).toBe("ready");
38
+ expect(policy.mode).toBe("custom");
39
+ expect(
40
+ await t.mutation(m("confirm"), {
41
+ ...scope,
42
+ operationKey: "enable-1",
43
+ observedSchedule: { interval: "manual" },
44
+ }),
45
+ ).toEqual(
46
+ await t.query(q("operation"), { ...scope, operationKey: "enable-1" }),
47
+ );
48
+ });
49
+ it("preserves the original schedule and deadline across threshold changes and opt-out", async () => {
50
+ const t = convexTest(schema, modules);
51
+ await t.mutation(m("prepare"), initial);
52
+ await t.mutation(m("confirm"), {
53
+ ...scope,
54
+ operationKey: "enable-1",
55
+ observedSchedule: { interval: "manual" },
56
+ });
57
+ const original = await t.query(q("get"), scope);
58
+ const change = {
59
+ ...initial,
60
+ operationKey: "change",
61
+ expectedRevision: 0,
62
+ thresholds: [
63
+ { currency: "eur", amount: 500 },
64
+ { currency: "usd", amount: 2000 },
65
+ ],
66
+ observedSchedule: { interval: "manual" },
67
+ };
68
+ await t.mutation(m("prepare"), change);
69
+ await t.mutation(m("confirm"), {
70
+ ...scope,
71
+ operationKey: "change",
72
+ observedSchedule: { interval: "manual" },
73
+ });
74
+ const updated = await t.query(q("get"), scope);
75
+ expect(updated.previousSchedule).toEqual(original.previousSchedule);
76
+ expect(updated.holdDeadline).toBe(original.holdDeadline);
77
+ const off = await t.mutation(m("prepare"), {
78
+ ...change,
79
+ operationKey: "off",
80
+ expectedRevision: 1,
81
+ mode: "stripe_managed",
82
+ thresholds: [],
83
+ });
84
+ expect(off.targetSchedule).toEqual(initial.observedSchedule);
85
+ await t.mutation(m("confirm"), {
86
+ ...scope,
87
+ operationKey: "off",
88
+ observedSchedule: initial.observedSchedule,
89
+ });
90
+ expect(await t.query(q("get"), scope)).toMatchObject({
91
+ mode: "stripe_managed",
92
+ status: "ready",
93
+ thresholds: [],
94
+ });
95
+ });
96
+ it("rejects changed replays, ownership changes, stale revisions and competing operations", async () => {
97
+ const t = convexTest(schema, modules);
98
+ const op = await t.mutation(m("prepare"), initial);
99
+ expect(await t.mutation(m("prepare"), initial)).toEqual(op);
100
+ for (const changed of [
101
+ { thresholds: [{ currency: "usd", amount: 900 }] },
102
+ { ownerKey: "other" },
103
+ { accountId: "acct_other" },
104
+ ])
105
+ await expect(
106
+ t.mutation(m("prepare"), { ...initial, ...changed }),
107
+ ).rejects.toThrow(/identity|operation/);
108
+ await expect(
109
+ t.mutation(m("prepare"), {
110
+ ...initial,
111
+ operationKey: "other",
112
+ expectedRevision: 0,
113
+ }),
114
+ ).rejects.toThrow(/pending/);
115
+ await t.mutation(m("confirm"), {
116
+ ...scope,
117
+ operationKey: "enable-1",
118
+ observedSchedule: { interval: "manual" },
119
+ });
120
+ await expect(
121
+ t.mutation(m("prepare"), { ...initial, operationKey: "other" }),
122
+ ).rejects.toThrow(/revision/);
123
+ await expect(
124
+ t.query(q("get"), { ...scope, ownerKey: "other" }),
125
+ ).rejects.toThrow(/owner/);
126
+ });
127
+ it("cannot confirm a different provider schedule or hide external drift", async () => {
128
+ const t = convexTest(schema, modules);
129
+ await t.mutation(m("prepare"), initial);
130
+ await expect(
131
+ t.mutation(m("confirm"), {
132
+ ...scope,
133
+ operationKey: "enable-1",
134
+ observedSchedule: { interval: "daily" },
135
+ }),
136
+ ).rejects.toThrow(/schedule/);
137
+ await t.mutation(m("confirm"), {
138
+ ...scope,
139
+ operationKey: "enable-1",
140
+ observedSchedule: { interval: "manual" },
141
+ });
142
+ await expect(
143
+ t.mutation(m("prepare"), {
144
+ ...initial,
145
+ operationKey: "change",
146
+ expectedRevision: 0,
147
+ observedSchedule: { interval: "daily" },
148
+ }),
149
+ ).rejects.toThrow(/drift/);
150
+ });
151
+ it.each([0, -1, 1.5, Number.MAX_SAFE_INTEGER + 1])(
152
+ "rejects invalid custom minor-unit thresholds (%s)",
153
+ async (amount) => {
154
+ const t = convexTest(schema, modules);
155
+ await expect(
156
+ t.mutation(m("prepare"), {
157
+ ...initial,
158
+ thresholds: [{ currency: "usd", amount }],
159
+ }),
160
+ ).rejects.toThrow(/threshold/);
161
+ },
162
+ );
163
+ it("uses country holding limits and never extends a hold by editing thresholds", async () => {
164
+ for (const [country, days] of [
165
+ ["TH", 10],
166
+ ["US", 730],
167
+ ["GB", 90],
168
+ ] as const) {
169
+ const t = convexTest(schema, modules);
170
+ const before = Date.now();
171
+ await t.mutation(m("prepare"), { ...initial, country });
172
+ const policy = await t.query(q("get"), scope);
173
+ expect(policy.holdDeadline).toBeGreaterThanOrEqual(
174
+ before + days * 86400000,
175
+ );
176
+ expect(policy.holdDeadline).toBeLessThan(
177
+ Date.now() + days * 86400000 + 1000,
178
+ );
179
+ }
180
+ });
181
+ it("returns bounded due policies and retains expired changing operations for reconciliation", async () => {
182
+ const t = convexTest(schema, modules);
183
+ await t.mutation(m("prepare"), initial);
184
+ const p = await t.query(q("get"), scope);
185
+ const result = await t.query(q("due"), {
186
+ now: p.holdDeadline + 1,
187
+ paginationOpts: { numItems: 1, cursor: null },
188
+ });
189
+ expect(result.page).toHaveLength(1);
190
+ expect(result.page[0].status).toBe("changing");
191
+ });
192
+ it("accepts equivalent retries regardless of object property order", async () => {
193
+ const t = convexTest(schema, modules);
194
+ const op = await t.mutation(m("prepare"), initial);
195
+ expect(
196
+ await t.mutation(
197
+ m("prepare"),
198
+ Object.fromEntries(Object.entries(initial).reverse()),
199
+ ),
200
+ ).toEqual(op);
201
+ });
202
+ it("marks attempts durably and only cancels an unattempted provider change", async () => {
203
+ const t = convexTest(schema, modules);
204
+ await t.mutation(m("prepare"), initial);
205
+ const args = {
206
+ ...scope,
207
+ operationKey: "enable-1",
208
+ observedSchedule: initial.observedSchedule,
209
+ };
210
+ expect((await t.mutation(m("cancelUnattempted"), args)).status).toBe(
211
+ "canceled",
212
+ );
213
+ expect(await t.query(q("get"), scope)).toMatchObject({
214
+ mode: "stripe_managed",
215
+ status: "ready",
216
+ });
217
+ await expect(
218
+ t.mutation(m("confirm"), {
219
+ ...scope,
220
+ operationKey: "enable-1",
221
+ observedSchedule: { interval: "manual" },
222
+ }),
223
+ ).rejects.toThrow(/canceled/);
224
+ await t.mutation(m("prepare"), {
225
+ ...initial,
226
+ operationKey: "second",
227
+ expectedRevision: 0,
228
+ });
229
+ await t.mutation(m("beginAttempt"), { ...scope, operationKey: "second" });
230
+ await expect(
231
+ t.mutation(m("cancelUnattempted"), { ...args, operationKey: "second" }),
232
+ ).rejects.toThrow(/attempt/);
233
+ });
234
+ it("anchors the hold deadline before provider reads and rejects future observations", async () => {
235
+ const t = convexTest(schema, modules);
236
+ const observedAt = Date.now() - 60000;
237
+ await t.mutation(m("prepare"), { ...initial, observedAt });
238
+ expect((await t.query(q("get"), scope)).holdDeadline).toBe(
239
+ observedAt + 730 * 86400000,
240
+ );
241
+ await expect(
242
+ t.mutation(m("prepare"), {
243
+ ...initial,
244
+ operationKey: "future",
245
+ observedAt: Date.now() + 60000,
246
+ }),
247
+ ).rejects.toThrow(/observation/);
248
+ });
@@ -0,0 +1,88 @@
1
+ import type { Infer } from "convex/values";
2
+ import type { QueryCtx, MutationCtx } from "../_generated/server.js";
3
+ import type { schedule, thresholds } from "./schema.js";
4
+ export function canonicalSchedule(value: Infer<typeof schedule>) {
5
+ const weekdays = [
6
+ "monday",
7
+ "tuesday",
8
+ "wednesday",
9
+ "thursday",
10
+ "friday",
11
+ "saturday",
12
+ "sunday",
13
+ ];
14
+ if (value.interval === "weekly") {
15
+ const days = value.weeklyDays;
16
+ if (
17
+ !days?.length ||
18
+ days.length > 7 ||
19
+ new Set(days).size !== days.length ||
20
+ days.some((day) => !weekdays.includes(day))
21
+ )
22
+ throw new Error("Invalid weekly payout schedule.");
23
+ return { interval: value.interval, weeklyDays: [...days].sort() };
24
+ }
25
+ if (value.interval === "monthly") {
26
+ const days = value.monthlyDays;
27
+ if (
28
+ !days?.length ||
29
+ days.length > 31 ||
30
+ new Set(days).size !== days.length ||
31
+ days.some((day) => !Number.isInteger(day) || day < 1 || day > 31)
32
+ )
33
+ throw new Error("Invalid monthly payout schedule.");
34
+ return {
35
+ interval: value.interval,
36
+ monthlyDays: [...days].sort((a, b) => a - b),
37
+ };
38
+ }
39
+ return { interval: value.interval };
40
+ }
41
+ export function canonicalThresholds(
42
+ values: Infer<typeof thresholds>,
43
+ custom: boolean,
44
+ ) {
45
+ if (
46
+ (custom && !values.length) ||
47
+ values.length > 50 ||
48
+ (!custom && values.length) ||
49
+ new Set(values.map((v) => v.currency)).size !== values.length ||
50
+ values.some(
51
+ (v) =>
52
+ !Number.isSafeInteger(v.amount) ||
53
+ v.amount <= 0 ||
54
+ !/^[a-z]{3}$/.test(v.currency),
55
+ )
56
+ )
57
+ throw new Error("Invalid custom payout thresholds.");
58
+ return [...values].sort((a, b) => a.currency.localeCompare(b.currency));
59
+ }
60
+ export function validateScope(scope: { accountId: string; ownerKey: string }) {
61
+ if (
62
+ !/^acct_\w+$/.test(scope.accountId) ||
63
+ scope.accountId.length > 80 ||
64
+ !scope.ownerKey.trim() ||
65
+ scope.ownerKey.length > 200
66
+ )
67
+ throw new Error("Invalid payout policy identity.");
68
+ }
69
+ export async function readPolicy(
70
+ ctx: QueryCtx | MutationCtx,
71
+ scope: { accountId: string; ownerKey: string },
72
+ ) {
73
+ validateScope(scope);
74
+ const value = await ctx.db
75
+ .query("payoutPolicies")
76
+ .withIndex("by_account", (q) => q.eq("accountId", scope.accountId))
77
+ .unique();
78
+ if (value && value.ownerKey !== scope.ownerKey)
79
+ throw new Error("Payout policy owner does not match.");
80
+ return value;
81
+ }
82
+ export const same = (a: unknown, b: unknown) =>
83
+ JSON.stringify(a) === JSON.stringify(b);
84
+ export function holdingDays(country: string) {
85
+ if (!/^[A-Z]{2}$/.test(country))
86
+ throw new Error("A verified account country is required.");
87
+ return country === "TH" ? 10 : country === "US" ? 730 : 90;
88
+ }