@fantastic.dev/better-stripe 0.20.1 → 0.22.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 (73) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +16 -5
  3. package/dist/client/connect/payoutExecution.d.ts +34 -0
  4. package/dist/client/connect/payoutExecution.d.ts.map +1 -0
  5. package/dist/client/connect/payoutExecution.js +189 -0
  6. package/dist/client/connect/payoutExecution.js.map +1 -0
  7. package/dist/client/connect/payoutHistory.d.ts +22 -0
  8. package/dist/client/connect/payoutHistory.d.ts.map +1 -0
  9. package/dist/client/connect/payoutHistory.js +122 -0
  10. package/dist/client/connect/payoutHistory.js.map +1 -0
  11. package/dist/client/connect/payoutReceipt.d.ts +53 -0
  12. package/dist/client/connect/payoutReceipt.d.ts.map +1 -0
  13. package/dist/client/connect/payoutReceipt.js +185 -0
  14. package/dist/client/connect/payoutReceipt.js.map +1 -0
  15. package/dist/client/connect/payoutReconciliation.d.ts +38 -0
  16. package/dist/client/connect/payoutReconciliation.d.ts.map +1 -0
  17. package/dist/client/connect/payoutReconciliation.js +112 -0
  18. package/dist/client/connect/payoutReconciliation.js.map +1 -0
  19. package/dist/client/payoutPolicy.d.ts +9 -0
  20. package/dist/client/payoutPolicy.d.ts.map +1 -1
  21. package/dist/client/payoutPolicy.js +12 -0
  22. package/dist/client/payoutPolicy.js.map +1 -1
  23. package/dist/component/_generated/component.d.ts +112 -0
  24. package/dist/component/_generated/component.d.ts.map +1 -1
  25. package/dist/component/earnings/triggers.d.ts +52 -2
  26. package/dist/component/earnings/triggers.d.ts.map +1 -1
  27. package/dist/component/payoutRelease/allocation.d.ts +1 -0
  28. package/dist/component/payoutRelease/allocation.d.ts.map +1 -1
  29. package/dist/component/payoutRelease/allocation.js +3 -0
  30. package/dist/component/payoutRelease/allocation.js.map +1 -1
  31. package/dist/component/payoutRelease/fees.d.ts +22 -0
  32. package/dist/component/payoutRelease/fees.d.ts.map +1 -0
  33. package/dist/component/payoutRelease/fees.js +73 -0
  34. package/dist/component/payoutRelease/fees.js.map +1 -0
  35. package/dist/component/payoutRelease/helpers.d.ts +5 -0
  36. package/dist/component/payoutRelease/helpers.d.ts.map +1 -1
  37. package/dist/component/payoutRelease/mutations.d.ts +29 -0
  38. package/dist/component/payoutRelease/mutations.d.ts.map +1 -1
  39. package/dist/component/payoutRelease/mutations.js +24 -1
  40. package/dist/component/payoutRelease/mutations.js.map +1 -1
  41. package/dist/component/payoutRelease/queries.d.ts +87 -0
  42. package/dist/component/payoutRelease/queries.d.ts.map +1 -1
  43. package/dist/component/payoutRelease/queries.js +39 -0
  44. package/dist/component/payoutRelease/queries.js.map +1 -1
  45. package/dist/component/payoutRelease/schema.d.ts +43 -2
  46. package/dist/component/payoutRelease/schema.d.ts.map +1 -1
  47. package/dist/component/payoutRelease/schema.js +12 -0
  48. package/dist/component/payoutRelease/schema.js.map +1 -1
  49. package/dist/component/schema.d.ts +27 -1
  50. package/dist/component/schema.js +2 -1
  51. package/dist/component/schema.js.map +1 -1
  52. package/docs/settlement.md +134 -3
  53. package/package.json +1 -1
  54. package/src/client/connect/payoutAllocation.test.ts +36 -3
  55. package/src/client/connect/payoutExecution.test.ts +504 -0
  56. package/src/client/connect/payoutExecution.ts +265 -0
  57. package/src/client/connect/payoutHistory.test.ts +145 -0
  58. package/src/client/connect/payoutHistory.ts +149 -0
  59. package/src/client/connect/payoutReceipt.test.ts +312 -0
  60. package/src/client/connect/payoutReceipt.ts +287 -0
  61. package/src/client/connect/payoutReconciliation.test.ts +398 -0
  62. package/src/client/connect/payoutReconciliation.ts +178 -0
  63. package/src/client/payoutPolicy.ts +30 -0
  64. package/src/component/_generated/api.ts +2 -0
  65. package/src/component/_generated/component.ts +118 -0
  66. package/src/component/payout-fees.test.ts +224 -0
  67. package/src/component/payout-release-snapshot.test.ts +57 -0
  68. package/src/component/payoutRelease/allocation.ts +6 -0
  69. package/src/component/payoutRelease/fees.ts +93 -0
  70. package/src/component/payoutRelease/mutations.ts +26 -1
  71. package/src/component/payoutRelease/queries.ts +46 -0
  72. package/src/component/payoutRelease/schema.ts +13 -0
  73. package/src/component/schema.ts +2 -0
@@ -1,3 +1,4 @@
1
+ import { readFees, recordFees } from "./fees.js";
1
2
  import { v } from "convex/values";
2
3
  import { mutation } from "../_generated/server.js";
3
4
  import { identity, observation, claimDoc } from "./schema.js";
@@ -30,6 +31,7 @@ export const claim = mutation({
30
31
  ...identity,
31
32
  expectedRevision: v.number(),
32
33
  currency: v.string(),
34
+ destinationId: v.optional(v.string()),
33
35
  providerAvailable: v.number(),
34
36
  independentLiabilities: v.number(),
35
37
  observations: v.array(observation),
@@ -37,11 +39,17 @@ export const claim = mutation({
37
39
  returns: v.union(claimDoc, held),
38
40
  handler: async (ctx, args) => {
39
41
  validateIdentity(args);
42
+ if (
43
+ args.destinationId !== undefined &&
44
+ !/^ba_\w+$/.test(args.destinationId)
45
+ )
46
+ throw new Error("Invalid payout bank destination.");
40
47
  const existing = await claimByKey(ctx, args);
41
48
  if (existing) {
42
49
  if (
43
50
  existing.currency !== args.currency ||
44
- existing.expectedRevision !== args.expectedRevision
51
+ existing.expectedRevision !== args.expectedRevision ||
52
+ existing.destinationId !== args.destinationId
45
53
  )
46
54
  throw new Error("Payout claim identity cannot be reused.");
47
55
  return existing;
@@ -88,11 +96,13 @@ export const claim = mutation({
88
96
  reservedAmount: prior?.reservedAmount ?? 0,
89
97
  });
90
98
  }
99
+ const fees = await readFees(ctx, args);
91
100
  const plan = planRecipientPayout({
92
101
  currency: args.currency,
93
102
  threshold: minimum.amount,
94
103
  providerAvailable: args.providerAvailable,
95
104
  independentLiabilities: args.independentLiabilities,
105
+ confirmedFees: fees?.amount ?? 0,
96
106
  sources,
97
107
  });
98
108
  if (plan.status === "held") return plan;
@@ -119,8 +129,10 @@ export const claim = mutation({
119
129
  expectedRevision: args.expectedRevision,
120
130
  currency: args.currency,
121
131
  amount: plan.amount,
132
+ ...(args.destinationId ? { destinationId: args.destinationId } : {}),
122
133
  allocations: plan.allocations,
123
134
  observations: args.observations,
135
+ feeRevision: fees?.revision ?? 0,
124
136
  status: "reserved",
125
137
  });
126
138
  return (await ctx.db.get("payoutReleaseClaims", id))!;
@@ -135,6 +147,9 @@ export const beginAttempt = mutation({
135
147
  throw new Error("Payout claim is not reserved.");
136
148
  if (claim.attemptedAt !== undefined) return claim;
137
149
  await readyPolicy(ctx, claim);
150
+ const feeRevision = (await readFees(ctx, claim))?.revision ?? 0;
151
+ if (feeRevision !== (claim.feeRevision ?? 0))
152
+ throw new Error("Payout fee revision changed after reservation.");
138
153
  await checkSnapshot(ctx, claim.accountId, claim.observations, true);
139
154
  await ctx.db.patch("payoutReleaseClaims", claim._id, {
140
155
  attemptedAt: Date.now(),
@@ -175,6 +190,9 @@ export const recordOutcome = mutation({
175
190
  v.literal("canceled"),
176
191
  ),
177
192
  returnTransactionId: v.optional(v.string()),
193
+ debitTransactionId: v.optional(v.string()),
194
+ debitFee: v.optional(v.number()),
195
+ returnFee: v.optional(v.number()),
178
196
  },
179
197
  returns: claimDoc,
180
198
  handler: async (ctx, args) => {
@@ -226,8 +244,15 @@ export const recordOutcome = mutation({
226
244
  if (claim.status === "returned") {
227
245
  if (returned && args.returnTransactionId !== claim.returnTransactionId)
228
246
  throw new Error("Payout return receipt identity changed.");
247
+ if (returned) {
248
+ await recordFees(ctx, claim, args, true);
249
+ return (await ctx.db.get("payoutReleaseClaims", claim._id))!;
250
+ }
229
251
  return claim;
230
252
  }
253
+ if (claim.status === "paid" && args.providerStatus !== "paid" && !returned)
254
+ return claim;
255
+ await recordFees(ctx, claim, args, returned);
231
256
  if (returned) {
232
257
  await moveAllocations(
233
258
  ctx,
@@ -1,3 +1,5 @@
1
+ import { transferDocValidator } from "../connect/validators.js";
2
+ import { readFees } from "./fees.js";
1
3
  import { paginationOptsValidator } from "convex/server";
2
4
  import { v } from "convex/values";
3
5
  import { query } from "../_generated/server.js";
@@ -54,3 +56,47 @@ export const pending = query({
54
56
  };
55
57
  },
56
58
  });
59
+
60
+ export const fees = query({
61
+ args: { accountId: v.string(), ownerKey: v.string(), currency: v.string() },
62
+ returns: v.object({ amount: v.number(), revision: v.number() }),
63
+ handler: async (ctx, args) => {
64
+ await readPolicy(ctx, args);
65
+ const balance = await readFees(ctx, args);
66
+ return { amount: balance?.amount ?? 0, revision: balance?.revision ?? 0 };
67
+ },
68
+ });
69
+
70
+ /** Complete bounded input for package qualification. Partial membership never qualifies. */
71
+ export const snapshot = query({
72
+ args: { accountId: v.string(), ownerKey: v.string() },
73
+ returns: v.union(
74
+ v.object({
75
+ status: v.literal("held"),
76
+ reason: v.literal("snapshot_limit"),
77
+ }),
78
+ v.object({
79
+ status: v.literal("ready"),
80
+ transfers: v.array(transferDocValidator),
81
+ claims: v.array(claimDoc),
82
+ }),
83
+ ),
84
+ handler: async (ctx, args) => {
85
+ await readPolicy(ctx, args);
86
+ const transfers = await ctx.db
87
+ .query("transfers")
88
+ .withIndex("by_destination_account_id", (q) =>
89
+ q.eq("destinationAccountId", args.accountId),
90
+ )
91
+ .take(101);
92
+ const claims = await ctx.db
93
+ .query("payoutReleaseClaims")
94
+ .withIndex("by_account_status", (q) => q.eq("accountId", args.accountId))
95
+ .take(101);
96
+ if (transfers.length > 100 || claims.length > 100)
97
+ return { status: "held" as const, reason: "snapshot_limit" as const };
98
+ if (claims.some((c) => c.ownerKey !== args.ownerKey))
99
+ throw new Error("Payout history owner does not match.");
100
+ return { status: "ready" as const, transfers, claims };
101
+ },
102
+ });
@@ -23,6 +23,7 @@ export const claimFields = {
23
23
  expectedRevision: v.number(),
24
24
  currency: v.string(),
25
25
  amount: v.number(),
26
+ destinationId: v.optional(v.string()),
26
27
  allocations: v.array(allocation),
27
28
  observations: v.array(observation),
28
29
  status: v.union(
@@ -36,6 +37,10 @@ export const claimFields = {
36
37
  attemptedAt: v.optional(v.number()),
37
38
  stripePayoutId: v.optional(v.string()),
38
39
  returnTransactionId: v.optional(v.string()),
40
+ feeRevision: v.optional(v.number()),
41
+ debitTransactionId: v.optional(v.string()),
42
+ debitFee: v.optional(v.number()),
43
+ providerFee: v.optional(v.number()),
39
44
  rejection: v.optional(
40
45
  v.object({
41
46
  requestId: v.string(),
@@ -55,6 +60,7 @@ export const claimsTable = defineTable(claimFields)
55
60
  .index("by_operation", ["operationKey"])
56
61
  .index("by_account_status", ["accountId", "status"])
57
62
  .index("by_provider_payout", ["stripePayoutId"])
63
+ .index("by_debit_receipt", ["debitTransactionId"])
58
64
  .index("by_return_receipt", ["returnTransactionId"])
59
65
  .index("by_rejection_request", ["rejection.requestId"]);
60
66
  export const sourcesTable = defineTable({
@@ -67,3 +73,10 @@ export const sourcesTable = defineTable({
67
73
  .index("by_transfer", ["stripeTransferId"])
68
74
  .index("by_account", ["accountId"]);
69
75
  export const identity = { ...scopeFields, operationKey: v.string() };
76
+
77
+ export const feeBalancesTable = defineTable({
78
+ ...scopeFields,
79
+ currency: v.string(),
80
+ amount: v.number(),
81
+ revision: v.number(),
82
+ }).index("by_account_currency", ["accountId", "currency"]);
@@ -1,4 +1,5 @@
1
1
  import {
2
+ feeBalancesTable as payoutFeeBalancesTable,
2
3
  claimsTable as payoutReleaseClaimsTable,
3
4
  sourcesTable as payoutSourceAllocationsTable,
4
5
  } from "./payoutRelease/schema.js";
@@ -36,6 +37,7 @@ import { pricesTable, productsTable } from "./products/schema";
36
37
  import { webhookEventsTable } from "./webhooks/schema";
37
38
 
38
39
  export default defineSchema({
40
+ payoutFeeBalances: payoutFeeBalancesTable,
39
41
  payoutReleaseClaims: payoutReleaseClaimsTable,
40
42
  payoutSourceAllocations: payoutSourceAllocationsTable,
41
43
  payoutPolicies: policiesTable,