@fantastic.dev/better-stripe 0.21.0 → 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 (50) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +6 -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/payoutReconciliation.d.ts.map +1 -1
  12. package/dist/client/connect/payoutReconciliation.js +2 -0
  13. package/dist/client/connect/payoutReconciliation.js.map +1 -1
  14. package/dist/client/payoutPolicy.d.ts +3 -0
  15. package/dist/client/payoutPolicy.d.ts.map +1 -1
  16. package/dist/client/payoutPolicy.js +4 -0
  17. package/dist/client/payoutPolicy.js.map +1 -1
  18. package/dist/component/_generated/component.d.ts +73 -0
  19. package/dist/component/_generated/component.d.ts.map +1 -1
  20. package/dist/component/earnings/triggers.d.ts +4 -2
  21. package/dist/component/earnings/triggers.d.ts.map +1 -1
  22. package/dist/component/payoutRelease/helpers.d.ts +1 -0
  23. package/dist/component/payoutRelease/helpers.d.ts.map +1 -1
  24. package/dist/component/payoutRelease/mutations.d.ts +6 -0
  25. package/dist/component/payoutRelease/mutations.d.ts.map +1 -1
  26. package/dist/component/payoutRelease/mutations.js +7 -1
  27. package/dist/component/payoutRelease/mutations.js.map +1 -1
  28. package/dist/component/payoutRelease/queries.d.ts +71 -0
  29. package/dist/component/payoutRelease/queries.d.ts.map +1 -1
  30. package/dist/component/payoutRelease/queries.js +29 -0
  31. package/dist/component/payoutRelease/queries.js.map +1 -1
  32. package/dist/component/payoutRelease/schema.d.ts +7 -2
  33. package/dist/component/payoutRelease/schema.d.ts.map +1 -1
  34. package/dist/component/payoutRelease/schema.js +1 -0
  35. package/dist/component/payoutRelease/schema.js.map +1 -1
  36. package/dist/component/schema.d.ts +3 -1
  37. package/docs/settlement.md +64 -4
  38. package/package.json +1 -1
  39. package/src/client/connect/payoutExecution.test.ts +504 -0
  40. package/src/client/connect/payoutExecution.ts +265 -0
  41. package/src/client/connect/payoutHistory.test.ts +145 -0
  42. package/src/client/connect/payoutHistory.ts +149 -0
  43. package/src/client/connect/payoutReconciliation.test.ts +13 -0
  44. package/src/client/connect/payoutReconciliation.ts +4 -0
  45. package/src/client/payoutPolicy.ts +14 -0
  46. package/src/component/_generated/component.ts +80 -0
  47. package/src/component/payout-release-snapshot.test.ts +57 -0
  48. package/src/component/payoutRelease/mutations.ts +9 -1
  49. package/src/component/payoutRelease/queries.ts +35 -0
  50. package/src/component/payoutRelease/schema.ts +1 -0
@@ -2494,6 +2494,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
2494
2494
  currency: string;
2495
2495
  debitFee?: number;
2496
2496
  debitTransactionId?: string;
2497
+ destinationId?: string;
2497
2498
  expectedRevision: number;
2498
2499
  feeRevision?: number;
2499
2500
  observations: Array<{
@@ -2542,6 +2543,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
2542
2543
  currency: string;
2543
2544
  debitFee?: number;
2544
2545
  debitTransactionId?: string;
2546
+ destinationId?: string;
2545
2547
  expectedRevision: number;
2546
2548
  feeRevision?: number;
2547
2549
  observations: Array<{
@@ -2578,6 +2580,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
2578
2580
  {
2579
2581
  accountId: string;
2580
2582
  currency: string;
2583
+ destinationId?: string;
2581
2584
  expectedRevision: number;
2582
2585
  independentLiabilities: number;
2583
2586
  observations: Array<{
@@ -2607,6 +2610,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
2607
2610
  currency: string;
2608
2611
  debitFee?: number;
2609
2612
  debitTransactionId?: string;
2613
+ destinationId?: string;
2610
2614
  expectedRevision: number;
2611
2615
  feeRevision?: number;
2612
2616
  observations: Array<{
@@ -2678,6 +2682,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
2678
2682
  currency: string;
2679
2683
  debitFee?: number;
2680
2684
  debitTransactionId?: string;
2685
+ destinationId?: string;
2681
2686
  expectedRevision: number;
2682
2687
  feeRevision?: number;
2683
2688
  observations: Array<{
@@ -2732,6 +2737,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
2732
2737
  currency: string;
2733
2738
  debitFee?: number;
2734
2739
  debitTransactionId?: string;
2740
+ destinationId?: string;
2735
2741
  expectedRevision: number;
2736
2742
  feeRevision?: number;
2737
2743
  observations: Array<{
@@ -2789,6 +2795,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
2789
2795
  currency: string;
2790
2796
  debitFee?: number;
2791
2797
  debitTransactionId?: string;
2798
+ destinationId?: string;
2792
2799
  expectedRevision: number;
2793
2800
  feeRevision?: number;
2794
2801
  observations: Array<{
@@ -2851,6 +2858,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
2851
2858
  currency: string;
2852
2859
  debitFee?: number;
2853
2860
  debitTransactionId?: string;
2861
+ destinationId?: string;
2854
2862
  expectedRevision: number;
2855
2863
  feeRevision?: number;
2856
2864
  observations: Array<{
@@ -2882,6 +2890,78 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
2882
2890
  },
2883
2891
  Name
2884
2892
  >;
2893
+ snapshot: FunctionReference<
2894
+ "query",
2895
+ "internal",
2896
+ { accountId: string; ownerKey: string },
2897
+ | { reason: "snapshot_limit"; status: "held" }
2898
+ | {
2899
+ claims: Array<{
2900
+ _creationTime: number;
2901
+ _id: string;
2902
+ accountId: string;
2903
+ allocations: Array<{
2904
+ amount: number;
2905
+ role?: "store" | "affiliate" | "other";
2906
+ stripeTransferId: string;
2907
+ }>;
2908
+ amount: number;
2909
+ attemptedAt?: number;
2910
+ currency: string;
2911
+ debitFee?: number;
2912
+ debitTransactionId?: string;
2913
+ destinationId?: string;
2914
+ expectedRevision: number;
2915
+ feeRevision?: number;
2916
+ observations: Array<{
2917
+ currency: string;
2918
+ ledgerFingerprint: string;
2919
+ matureNet: number;
2920
+ observedAt: number;
2921
+ pendingCredits: number;
2922
+ stripeTransferId: string;
2923
+ transactionIds: Array<string>;
2924
+ }>;
2925
+ operationKey: string;
2926
+ ownerKey: string;
2927
+ providerFee?: number;
2928
+ rejection?: {
2929
+ code: "balance_insufficient" | "amount_too_small";
2930
+ requestId: string;
2931
+ };
2932
+ returnTransactionId?: string;
2933
+ status:
2934
+ | "reserved"
2935
+ | "submitted"
2936
+ | "paid"
2937
+ | "returned"
2938
+ | "canceled"
2939
+ | "rejected";
2940
+ stripePayoutId?: string;
2941
+ }>;
2942
+ status: "ready";
2943
+ transfers: Array<{
2944
+ _creationTime: number;
2945
+ _id: string;
2946
+ amount: number;
2947
+ currency: string;
2948
+ destinationAccountId: string;
2949
+ metadata?: any;
2950
+ paymentId?: string;
2951
+ reinstatement?: boolean;
2952
+ reversalClaimedAmount?: number;
2953
+ reversalStatus?: "partially_reversed" | "fully_reversed";
2954
+ reversedAmount?: number;
2955
+ role?: "store" | "affiliate" | "other";
2956
+ sourceChargeId?: string;
2957
+ sourceInvoiceId?: string;
2958
+ status: "pending" | "paid" | "failed" | "reversed";
2959
+ stripeTransferId: string;
2960
+ totalsRecorded?: true;
2961
+ }>;
2962
+ },
2963
+ Name
2964
+ >;
2885
2965
  };
2886
2966
  };
2887
2967
  products: {
@@ -0,0 +1,57 @@
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 snapshot = makeFunctionReference<"query">(
8
+ "payoutRelease/queries:snapshot",
9
+ );
10
+ const scope = { accountId: "acct_recipient", ownerKey: "owner" };
11
+ async function setup(count = 2) {
12
+ const t = convexTest(schema, modules);
13
+ await t.run(async (ctx) => {
14
+ await ctx.db.insert("payoutPolicies", {
15
+ ...scope,
16
+ mode: "custom",
17
+ status: "ready",
18
+ revision: 0,
19
+ operationKey: "enable",
20
+ thresholds: [{ currency: "usd", amount: 1000 }],
21
+ country: "US",
22
+ previousSchedule: { interval: "daily" },
23
+ holdDeadline: Date.now() + 86400000,
24
+ });
25
+ for (let i = 0; i < count; i++)
26
+ await ctx.db.insert("transfers", {
27
+ stripeTransferId: `tr_${i}`,
28
+ destinationAccountId: scope.accountId,
29
+ amount: 500,
30
+ currency: "usd",
31
+ status: "paid",
32
+ role: i % 2 ? "affiliate" : "store",
33
+ });
34
+ });
35
+ return t;
36
+ }
37
+ it("returns complete source membership and historical claims for the owned account", async () => {
38
+ const t = await setup();
39
+ expect(await t.query(snapshot, scope)).toMatchObject({
40
+ status: "ready",
41
+ transfers: [{ stripeTransferId: "tr_0" }, { stripeTransferId: "tr_1" }],
42
+ claims: [],
43
+ });
44
+ });
45
+ it("holds an account exceeding the atomic claim bound rather than qualifying a partial page", async () => {
46
+ const t = await setup(101);
47
+ expect(await t.query(snapshot, scope)).toEqual({
48
+ status: "held",
49
+ reason: "snapshot_limit",
50
+ });
51
+ });
52
+ it("rejects another owner", async () => {
53
+ const t = await setup();
54
+ await expect(
55
+ t.query(snapshot, { ...scope, ownerKey: "other" }),
56
+ ).rejects.toThrow(/owner/i);
57
+ });
@@ -31,6 +31,7 @@ export const claim = mutation({
31
31
  ...identity,
32
32
  expectedRevision: v.number(),
33
33
  currency: v.string(),
34
+ destinationId: v.optional(v.string()),
34
35
  providerAvailable: v.number(),
35
36
  independentLiabilities: v.number(),
36
37
  observations: v.array(observation),
@@ -38,11 +39,17 @@ export const claim = mutation({
38
39
  returns: v.union(claimDoc, held),
39
40
  handler: async (ctx, args) => {
40
41
  validateIdentity(args);
42
+ if (
43
+ args.destinationId !== undefined &&
44
+ !/^ba_\w+$/.test(args.destinationId)
45
+ )
46
+ throw new Error("Invalid payout bank destination.");
41
47
  const existing = await claimByKey(ctx, args);
42
48
  if (existing) {
43
49
  if (
44
50
  existing.currency !== args.currency ||
45
- existing.expectedRevision !== args.expectedRevision
51
+ existing.expectedRevision !== args.expectedRevision ||
52
+ existing.destinationId !== args.destinationId
46
53
  )
47
54
  throw new Error("Payout claim identity cannot be reused.");
48
55
  return existing;
@@ -122,6 +129,7 @@ export const claim = mutation({
122
129
  expectedRevision: args.expectedRevision,
123
130
  currency: args.currency,
124
131
  amount: plan.amount,
132
+ ...(args.destinationId ? { destinationId: args.destinationId } : {}),
125
133
  allocations: plan.allocations,
126
134
  observations: args.observations,
127
135
  feeRevision: fees?.revision ?? 0,
@@ -1,3 +1,4 @@
1
+ import { transferDocValidator } from "../connect/validators.js";
1
2
  import { readFees } from "./fees.js";
2
3
  import { paginationOptsValidator } from "convex/server";
3
4
  import { v } from "convex/values";
@@ -65,3 +66,37 @@ export const fees = query({
65
66
  return { amount: balance?.amount ?? 0, revision: balance?.revision ?? 0 };
66
67
  },
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(