@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
@@ -0,0 +1,225 @@
1
+ import { v } from "convex/values";
2
+ import { mutation } from "../_generated/server.js";
3
+ import {
4
+ prepareFields,
5
+ operationDoc,
6
+ scopeFields,
7
+ schedule,
8
+ } from "./schema.js";
9
+ import {
10
+ canonicalSchedule,
11
+ canonicalThresholds,
12
+ readPolicy,
13
+ holdingDays,
14
+ same,
15
+ validateScope,
16
+ } from "./helpers.js";
17
+ export const prepare = mutation({
18
+ args: prepareFields,
19
+ returns: operationDoc,
20
+ handler: async (ctx, input) => {
21
+ validateScope(input);
22
+ if (
23
+ !input.operationKey.trim() ||
24
+ input.operationKey.length > 180 ||
25
+ /[\r\n]/.test(input.operationKey) ||
26
+ !Number.isSafeInteger(input.expectedRevision) ||
27
+ input.expectedRevision < -1 ||
28
+ input.expectedRevision >= Number.MAX_SAFE_INTEGER
29
+ )
30
+ throw new Error("Invalid payout policy operation identity or revision.");
31
+ const days = holdingDays(input.country);
32
+ const args = {
33
+ ...input,
34
+ observedSchedule: canonicalSchedule(input.observedSchedule),
35
+ thresholds: canonicalThresholds(
36
+ input.thresholds,
37
+ input.mode === "custom",
38
+ ),
39
+ };
40
+ const old = await ctx.db
41
+ .query("payoutPolicyOperations")
42
+ .withIndex("by_operation", (q) => q.eq("operationKey", args.operationKey))
43
+ .unique();
44
+ if (old) {
45
+ if (
46
+ old.accountId !== args.accountId ||
47
+ old.ownerKey !== args.ownerKey ||
48
+ old.expectedRevision !== args.expectedRevision ||
49
+ old.mode !== args.mode ||
50
+ old.country !== args.country ||
51
+ old.observedAt !== args.observedAt ||
52
+ !same(old.thresholds, args.thresholds) ||
53
+ !same(old.observedSchedule, args.observedSchedule)
54
+ )
55
+ throw new Error("Payout policy operation identity cannot be reused.");
56
+ return old;
57
+ }
58
+ if (
59
+ input.observedAt !== undefined &&
60
+ (!Number.isSafeInteger(input.observedAt) ||
61
+ input.observedAt > Date.now() ||
62
+ input.observedAt < Date.now() - 600000)
63
+ )
64
+ throw new Error("Payout policy observation is stale or in the future.");
65
+ const policy = await readPolicy(ctx, args);
66
+ if (policy?.status === "changing")
67
+ throw new Error("A payout policy operation is pending.");
68
+ if ((policy?.revision ?? -1) !== args.expectedRevision)
69
+ throw new Error("Payout policy revision changed.");
70
+ if (
71
+ policy?.mode === "custom" &&
72
+ args.observedSchedule.interval !== "manual"
73
+ )
74
+ throw new Error("Payout schedule drift requires reconciliation.");
75
+ if (
76
+ policy?.mode === "custom" &&
77
+ policy.country !== args.country &&
78
+ args.mode === "custom"
79
+ )
80
+ throw new Error("Payout policy country changed.");
81
+ if (
82
+ args.mode === "custom" &&
83
+ policy?.mode !== "custom" &&
84
+ args.observedSchedule.interval === "manual"
85
+ )
86
+ throw new Error("An existing manual schedule requires migration.");
87
+ const previousSchedule =
88
+ policy?.mode === "custom"
89
+ ? policy.previousSchedule!
90
+ : args.observedSchedule;
91
+ const targetSchedule =
92
+ args.mode === "custom"
93
+ ? { interval: "manual" as const }
94
+ : previousSchedule;
95
+ const revision = args.expectedRevision + 1;
96
+ const operationId = await ctx.db.insert("payoutPolicyOperations", {
97
+ ...args,
98
+ revision,
99
+ previousSchedule,
100
+ targetSchedule,
101
+ status: "pending",
102
+ });
103
+ const next = {
104
+ accountId: args.accountId,
105
+ ownerKey: args.ownerKey,
106
+ revision,
107
+ mode: policy?.mode ?? ("stripe_managed" as const),
108
+ thresholds: policy?.thresholds ?? [],
109
+ status: "changing" as const,
110
+ operationKey: args.operationKey,
111
+ country: args.country,
112
+ previousSchedule,
113
+ holdDeadline:
114
+ policy?.holdDeadline ??
115
+ (args.mode === "custom"
116
+ ? (args.observedAt ?? Date.now()) + days * 86400000
117
+ : undefined),
118
+ };
119
+ if (policy) await ctx.db.patch("payoutPolicies", policy._id, next);
120
+ else await ctx.db.insert("payoutPolicies", next);
121
+ return (await ctx.db.get("payoutPolicyOperations", operationId))!;
122
+ },
123
+ });
124
+ export const confirm = mutation({
125
+ args: {
126
+ ...scopeFields,
127
+ operationKey: v.string(),
128
+ observedSchedule: schedule,
129
+ },
130
+ returns: operationDoc,
131
+ handler: async (ctx, args) => {
132
+ const policy = await readPolicy(ctx, args);
133
+ const op = await ctx.db
134
+ .query("payoutPolicyOperations")
135
+ .withIndex("by_operation", (q) => q.eq("operationKey", args.operationKey))
136
+ .unique();
137
+ if (!op || op.accountId !== args.accountId || op.ownerKey !== args.ownerKey)
138
+ throw new Error("Payout policy operation identity does not match.");
139
+ if (!same(canonicalSchedule(args.observedSchedule), op.targetSchedule))
140
+ throw new Error("Provider payout schedule was not confirmed.");
141
+ if (op.status === "complete") return op;
142
+ if (op.status === "canceled")
143
+ throw new Error("Payout policy operation was canceled.");
144
+ if (
145
+ !policy ||
146
+ policy.operationKey !== op.operationKey ||
147
+ policy.revision !== op.revision
148
+ )
149
+ throw new Error("Payout policy revision changed.");
150
+ await ctx.db.patch("payoutPolicyOperations", op._id, {
151
+ status: "complete",
152
+ });
153
+ await ctx.db.patch("payoutPolicies", policy._id, {
154
+ mode: op.mode,
155
+ thresholds: op.thresholds,
156
+ status: "ready",
157
+ ...(op.mode === "stripe_managed"
158
+ ? { previousSchedule: undefined, holdDeadline: undefined }
159
+ : {}),
160
+ });
161
+ return (await ctx.db.get("payoutPolicyOperations", op._id))!;
162
+ },
163
+ });
164
+ export const beginAttempt = mutation({
165
+ args: { ...scopeFields, operationKey: v.string() },
166
+ returns: operationDoc,
167
+ handler: async (ctx, args) => {
168
+ const policy = await readPolicy(ctx, args);
169
+ const op = await ctx.db
170
+ .query("payoutPolicyOperations")
171
+ .withIndex("by_operation", (q) => q.eq("operationKey", args.operationKey))
172
+ .unique();
173
+ if (
174
+ !op ||
175
+ op.accountId !== args.accountId ||
176
+ op.ownerKey !== args.ownerKey ||
177
+ op.status !== "pending" ||
178
+ policy?.operationKey !== op.operationKey ||
179
+ policy.status !== "changing"
180
+ )
181
+ throw new Error("Payout policy operation is not pending.");
182
+ if (!op.attempted)
183
+ await ctx.db.patch("payoutPolicyOperations", op._id, { attempted: true });
184
+ return (await ctx.db.get("payoutPolicyOperations", op._id))!;
185
+ },
186
+ });
187
+ export const cancelUnattempted = mutation({
188
+ args: {
189
+ ...scopeFields,
190
+ operationKey: v.string(),
191
+ observedSchedule: schedule,
192
+ },
193
+ returns: operationDoc,
194
+ handler: async (ctx, args) => {
195
+ const policy = await readPolicy(ctx, args);
196
+ const op = await ctx.db
197
+ .query("payoutPolicyOperations")
198
+ .withIndex("by_operation", (q) => q.eq("operationKey", args.operationKey))
199
+ .unique();
200
+ if (
201
+ !op ||
202
+ op.accountId !== args.accountId ||
203
+ op.ownerKey !== args.ownerKey ||
204
+ op.attempted ||
205
+ op.status !== "pending" ||
206
+ policy?.operationKey !== op.operationKey ||
207
+ policy.status !== "changing"
208
+ )
209
+ throw new Error(
210
+ "An attempted payout policy operation cannot be canceled.",
211
+ );
212
+ if (!same(canonicalSchedule(args.observedSchedule), op.observedSchedule))
213
+ throw new Error("Provider schedule changed before cancellation.");
214
+ await ctx.db.patch("payoutPolicyOperations", op._id, {
215
+ status: "canceled",
216
+ });
217
+ await ctx.db.patch("payoutPolicies", policy._id, {
218
+ status: "ready",
219
+ ...(policy.mode === "stripe_managed"
220
+ ? { previousSchedule: undefined, holdDeadline: undefined }
221
+ : {}),
222
+ });
223
+ return (await ctx.db.get("payoutPolicyOperations", op._id))!;
224
+ },
225
+ });
@@ -0,0 +1,56 @@
1
+ import { paginationOptsValidator } from "convex/server";
2
+ import { v } from "convex/values";
3
+ import { query } from "../_generated/server.js";
4
+ import { scopeFields, policyDoc, operationDoc } from "./schema.js";
5
+ import { readPolicy, validateScope } from "./helpers.js";
6
+ export const get = query({
7
+ args: scopeFields,
8
+ returns: v.union(policyDoc, v.null()),
9
+ handler: readPolicy,
10
+ });
11
+ export const operation = query({
12
+ args: { ...scopeFields, operationKey: v.string() },
13
+ returns: v.union(operationDoc, v.null()),
14
+ handler: async (ctx, args) => {
15
+ validateScope(args);
16
+ const op = await ctx.db
17
+ .query("payoutPolicyOperations")
18
+ .withIndex("by_operation", (q) => q.eq("operationKey", args.operationKey))
19
+ .unique();
20
+ if (
21
+ op &&
22
+ (op.accountId !== args.accountId || op.ownerKey !== args.ownerKey)
23
+ )
24
+ throw new Error("Payout policy operation identity does not match.");
25
+ return op;
26
+ },
27
+ });
28
+ export const due = query({
29
+ args: { now: v.number(), paginationOpts: paginationOptsValidator },
30
+ returns: v.object({
31
+ page: v.array(policyDoc),
32
+ continueCursor: v.string(),
33
+ isDone: v.boolean(),
34
+ }),
35
+ handler: async (ctx, args) => {
36
+ if (
37
+ !Number.isSafeInteger(args.now) ||
38
+ args.now < 0 ||
39
+ !Number.isInteger(args.paginationOpts.numItems) ||
40
+ args.paginationOpts.numItems < 1 ||
41
+ args.paginationOpts.numItems > 100
42
+ )
43
+ throw new Error("Invalid payout policy due page.");
44
+ const page = await ctx.db
45
+ .query("payoutPolicies")
46
+ .withIndex("by_deadline", (q) =>
47
+ q.gt("holdDeadline", 0).lte("holdDeadline", args.now),
48
+ )
49
+ .paginate(args.paginationOpts);
50
+ return {
51
+ page: page.page,
52
+ continueCursor: page.continueCursor,
53
+ isDone: page.isDone,
54
+ };
55
+ },
56
+ });
@@ -0,0 +1,67 @@
1
+ import { defineTable } from "convex/server";
2
+ import { v } from "convex/values";
3
+ export const scopeFields = { accountId: v.string(), ownerKey: v.string() };
4
+ export const schedule = v.object({
5
+ interval: v.union(
6
+ v.literal("daily"),
7
+ v.literal("weekly"),
8
+ v.literal("monthly"),
9
+ v.literal("manual"),
10
+ ),
11
+ weeklyDays: v.optional(v.array(v.string())),
12
+ monthlyDays: v.optional(v.array(v.number())),
13
+ });
14
+ export const mode = v.union(v.literal("stripe_managed"), v.literal("custom"));
15
+ export const thresholds = v.array(
16
+ v.object({ currency: v.string(), amount: v.number() }),
17
+ );
18
+ export const prepareFields = {
19
+ ...scopeFields,
20
+ operationKey: v.string(),
21
+ expectedRevision: v.number(),
22
+ mode,
23
+ thresholds,
24
+ country: v.string(),
25
+ observedSchedule: schedule,
26
+ observedAt: v.optional(v.number()),
27
+ };
28
+ export const policyFields = {
29
+ ...scopeFields,
30
+ revision: v.number(),
31
+ mode,
32
+ thresholds,
33
+ status: v.union(v.literal("changing"), v.literal("ready")),
34
+ operationKey: v.string(),
35
+ country: v.string(),
36
+ previousSchedule: v.optional(schedule),
37
+ holdDeadline: v.optional(v.number()),
38
+ };
39
+ export const operationFields = {
40
+ ...prepareFields,
41
+ revision: v.number(),
42
+ previousSchedule: schedule,
43
+ targetSchedule: schedule,
44
+ status: v.union(
45
+ v.literal("pending"),
46
+ v.literal("complete"),
47
+ v.literal("canceled"),
48
+ ),
49
+ attempted: v.optional(v.boolean()),
50
+ };
51
+ export const policyDoc = v.object({
52
+ ...policyFields,
53
+ _id: v.id("payoutPolicies"),
54
+ _creationTime: v.number(),
55
+ });
56
+ export const operationDoc = v.object({
57
+ ...operationFields,
58
+ _id: v.id("payoutPolicyOperations"),
59
+ _creationTime: v.number(),
60
+ });
61
+ export const policiesTable = defineTable(policyFields)
62
+ .index("by_account", ["accountId"])
63
+ .index("by_deadline", ["holdDeadline"]);
64
+ export const operationsTable = defineTable(operationFields).index(
65
+ "by_operation",
66
+ ["operationKey"],
67
+ );
@@ -1,3 +1,4 @@
1
+ import { policiesTable, operationsTable } from "./payoutPolicy/schema.js";
1
2
  import { currencyTotalsTable } from "./earnings/schema.js";
2
3
  import {
3
4
  claimsTable,
@@ -31,6 +32,8 @@ import { pricesTable, productsTable } from "./products/schema";
31
32
  import { webhookEventsTable } from "./webhooks/schema";
32
33
 
33
34
  export default defineSchema({
35
+ payoutPolicies: policiesTable,
36
+ payoutPolicyOperations: operationsTable,
34
37
  currencyTotals: currencyTotalsTable,
35
38
  recoveryClaims: claimsTable,
36
39
  recoveryBalances: balancesTable,