@fantastic.dev/better-stripe 0.14.0 → 0.16.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 (138) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/MIGRATION.md +6 -0
  3. package/README.md +40 -1
  4. package/dist/client/billing/checkoutOptions.d.ts +27 -0
  5. package/dist/client/billing/checkoutOptions.d.ts.map +1 -0
  6. package/dist/client/billing/checkoutOptions.js +2 -0
  7. package/dist/client/billing/checkoutOptions.js.map +1 -0
  8. package/dist/client/billing/intents.d.ts +32 -0
  9. package/dist/client/billing/intents.d.ts.map +1 -0
  10. package/dist/client/billing/intents.js +71 -0
  11. package/dist/client/billing/intents.js.map +1 -0
  12. package/dist/client/connect/payoutSnapshot.d.ts +2 -0
  13. package/dist/client/connect/payoutSnapshot.d.ts.map +1 -1
  14. package/dist/client/connect/payoutSnapshot.js +2 -0
  15. package/dist/client/connect/payoutSnapshot.js.map +1 -1
  16. package/dist/client/core/fees.d.ts +2 -0
  17. package/dist/client/core/fees.d.ts.map +1 -1
  18. package/dist/client/core/fees.js +7 -0
  19. package/dist/client/core/fees.js.map +1 -1
  20. package/dist/client/index.d.ts +18 -26
  21. package/dist/client/index.d.ts.map +1 -1
  22. package/dist/client/index.js +18 -16
  23. package/dist/client/index.js.map +1 -1
  24. package/dist/client/readContracts.d.ts +50 -0
  25. package/dist/client/readContracts.d.ts.map +1 -0
  26. package/dist/client/readContracts.js +24 -0
  27. package/dist/client/readContracts.js.map +1 -0
  28. package/dist/client/types/documents.d.ts +2 -0
  29. package/dist/client/types/documents.d.ts.map +1 -1
  30. package/dist/client/webhooks/processors.js +2 -0
  31. package/dist/client/webhooks/processors.js.map +1 -1
  32. package/dist/component/_generated/component.d.ts +177 -0
  33. package/dist/component/_generated/component.d.ts.map +1 -1
  34. package/dist/component/connect/mutations.d.ts +2 -0
  35. package/dist/component/connect/mutations.d.ts.map +1 -1
  36. package/dist/component/connect/mutations.js +8 -8
  37. package/dist/component/connect/mutations.js.map +1 -1
  38. package/dist/component/connect/queries.d.ts +102 -2
  39. package/dist/component/connect/queries.d.ts.map +1 -1
  40. package/dist/component/connect/queries.js +88 -45
  41. package/dist/component/connect/queries.js.map +1 -1
  42. package/dist/component/connect/schema.d.ts +12 -2
  43. package/dist/component/connect/schema.d.ts.map +1 -1
  44. package/dist/component/connect/schema.js +10 -1
  45. package/dist/component/connect/schema.js.map +1 -1
  46. package/dist/component/connect/validators.d.ts +13 -2
  47. package/dist/component/connect/validators.d.ts.map +1 -1
  48. package/dist/component/connect/validators.js +4 -0
  49. package/dist/component/connect/validators.js.map +1 -1
  50. package/dist/component/earnings/legacyReads.d.ts +51 -0
  51. package/dist/component/earnings/legacyReads.d.ts.map +1 -0
  52. package/dist/component/earnings/legacyReads.js +43 -0
  53. package/dist/component/earnings/legacyReads.js.map +1 -0
  54. package/dist/component/earnings/mutations.d.ts +9 -0
  55. package/dist/component/earnings/mutations.d.ts.map +1 -0
  56. package/dist/component/earnings/mutations.js +23 -0
  57. package/dist/component/earnings/mutations.js.map +1 -0
  58. package/dist/component/earnings/reads.d.ts +14 -0
  59. package/dist/component/earnings/reads.d.ts.map +1 -0
  60. package/dist/component/earnings/reads.js +28 -0
  61. package/dist/component/earnings/reads.js.map +1 -0
  62. package/dist/component/earnings/schema.d.ts +20 -0
  63. package/dist/component/earnings/schema.d.ts.map +1 -0
  64. package/dist/component/earnings/schema.js +12 -0
  65. package/dist/component/earnings/schema.js.map +1 -0
  66. package/dist/component/earnings/triggers.d.ts +1290 -0
  67. package/dist/component/earnings/triggers.d.ts.map +1 -0
  68. package/dist/component/earnings/triggers.js +68 -0
  69. package/dist/component/earnings/triggers.js.map +1 -0
  70. package/dist/component/products/queries.d.ts +47 -0
  71. package/dist/component/products/queries.d.ts.map +1 -1
  72. package/dist/component/products/queries.js +59 -0
  73. package/dist/component/products/queries.js.map +1 -1
  74. package/dist/component/schema.d.ts +31 -2
  75. package/dist/component/schema.js +2 -0
  76. package/dist/component/schema.js.map +1 -1
  77. package/dist/react/components/EarningsSummary.d.ts +3 -1
  78. package/dist/react/components/EarningsSummary.d.ts.map +1 -1
  79. package/dist/react/components/EarningsSummary.js +3 -3
  80. package/dist/react/components/EarningsSummary.js.map +1 -1
  81. package/dist/react/hooks/useConfirmPayment.d.ts +1 -1
  82. package/dist/react/hooks/useConfirmPayment.d.ts.map +1 -1
  83. package/dist/react/hooks/useConfirmPayment.js +13 -2
  84. package/dist/react/hooks/useConfirmPayment.js.map +1 -1
  85. package/dist/react/hooks/useCurrencyEarnings.d.ts +31 -0
  86. package/dist/react/hooks/useCurrencyEarnings.d.ts.map +1 -0
  87. package/dist/react/hooks/useCurrencyEarnings.js +30 -0
  88. package/dist/react/hooks/useCurrencyEarnings.js.map +1 -0
  89. package/dist/react/index.d.ts +5 -0
  90. package/dist/react/index.d.ts.map +1 -1
  91. package/dist/react/index.js +4 -0
  92. package/dist/react/index.js.map +1 -1
  93. package/dist/react/workflows/SaveCard.d.ts +27 -0
  94. package/dist/react/workflows/SaveCard.d.ts.map +1 -0
  95. package/dist/react/workflows/SaveCard.js +56 -0
  96. package/dist/react/workflows/SaveCard.js.map +1 -0
  97. package/docs/react-workflows.md +47 -0
  98. package/docs/read-contracts.md +136 -0
  99. package/package.json +3 -2
  100. package/src/client/billing/checkoutOptions.ts +27 -0
  101. package/src/client/billing/intents.test.ts +216 -0
  102. package/src/client/billing/intents.ts +120 -0
  103. package/src/client/billing/intentsTransport.test.ts +94 -0
  104. package/src/client/connect/payoutSnapshot.test.ts +27 -0
  105. package/src/client/connect/payoutSnapshot.ts +4 -0
  106. package/src/client/core/fees.ts +8 -0
  107. package/src/client/index.ts +40 -43
  108. package/src/client/readContracts.ts +90 -0
  109. package/src/client/readPagination.test.ts +62 -0
  110. package/src/client/types/documents.ts +2 -0
  111. package/src/client/webhooks/payout-canceled.integration.test.ts +25 -0
  112. package/src/client/webhooks/processors.ts +2 -0
  113. package/src/component/_generated/api.ts +8 -0
  114. package/src/component/_generated/component.ts +201 -0
  115. package/src/component/connect/mutations.ts +8 -8
  116. package/src/component/connect/queries.ts +110 -48
  117. package/src/component/connect/schema.ts +10 -1
  118. package/src/component/connect/validators.ts +4 -0
  119. package/src/component/currency-migration.test.ts +119 -0
  120. package/src/component/currency-totals.test.ts +95 -0
  121. package/src/component/earnings/legacyReads.ts +59 -0
  122. package/src/component/earnings/mutations.ts +21 -0
  123. package/src/component/earnings/reads.ts +41 -0
  124. package/src/component/earnings/schema.ts +11 -0
  125. package/src/component/earnings/triggers.ts +91 -0
  126. package/src/component/products/queries.ts +74 -0
  127. package/src/component/public.test.ts +1 -1
  128. package/src/component/read-pagination.test.ts +193 -0
  129. package/src/component/schema.ts +2 -0
  130. package/src/react/components/EarningsSummary.tsx +5 -2
  131. package/src/react/hooks/useConfirmPayment.test.tsx +47 -0
  132. package/src/react/hooks/useConfirmPayment.ts +16 -2
  133. package/src/react/hooks/useCurrencyEarnings.test.ts +59 -0
  134. package/src/react/hooks/useCurrencyEarnings.ts +73 -0
  135. package/src/react/index.ts +15 -0
  136. package/src/react/read-contracts.render.test.tsx +20 -0
  137. package/src/react/workflows/SaveCard.test.tsx +127 -0
  138. package/src/react/workflows/SaveCard.tsx +117 -0
@@ -882,6 +882,8 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
882
882
  amount: number;
883
883
  arrivalDate?: string;
884
884
  currency: string;
885
+ failureCode?: string;
886
+ failureMessage?: string;
885
887
  metadata?: any;
886
888
  method?: string;
887
889
  status: "pending" | "paid" | "failed" | "canceled" | "in_transit";
@@ -965,6 +967,41 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
965
967
  sourceInvoiceId?: string;
966
968
  status: "pending" | "paid" | "failed" | "reversed";
967
969
  stripeTransferId: string;
970
+ totalsRecorded?: true;
971
+ }>;
972
+ },
973
+ Name
974
+ >;
975
+ getAccountEarningsByCurrency: FunctionReference<
976
+ "query",
977
+ "internal",
978
+ { destinationAccountId: string; previewLimit?: number },
979
+ {
980
+ totals: Array<{
981
+ currency: string;
982
+ gross: number;
983
+ net: number;
984
+ reversed: number;
985
+ transferCount: number;
986
+ }>;
987
+ transfers: Array<{
988
+ _creationTime: number;
989
+ _id: string;
990
+ amount: number;
991
+ currency: string;
992
+ destinationAccountId: string;
993
+ metadata?: any;
994
+ paymentId?: string;
995
+ reinstatement?: boolean;
996
+ reversalClaimedAmount?: number;
997
+ reversalStatus?: "partially_reversed" | "fully_reversed";
998
+ reversedAmount?: number;
999
+ role?: "store" | "affiliate" | "other";
1000
+ sourceChargeId?: string;
1001
+ sourceInvoiceId?: string;
1002
+ status: "pending" | "paid" | "failed" | "reversed";
1003
+ stripeTransferId: string;
1004
+ totalsRecorded?: true;
968
1005
  }>;
969
1006
  },
970
1007
  Name
@@ -983,10 +1020,41 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
983
1020
  amount: number;
984
1021
  arrivalDate?: string;
985
1022
  currency: string;
1023
+ failureCode?: string;
1024
+ failureMessage?: string;
986
1025
  metadata?: any;
987
1026
  method?: string;
988
1027
  status: "pending" | "paid" | "failed" | "canceled" | "in_transit";
989
1028
  stripePayoutId: string;
1029
+ totalsRecorded?: true;
1030
+ }>;
1031
+ },
1032
+ Name
1033
+ >;
1034
+ getAccountPayoutsByCurrency: FunctionReference<
1035
+ "query",
1036
+ "internal",
1037
+ { accountId: string; previewLimit?: number },
1038
+ {
1039
+ payouts: Array<{
1040
+ _creationTime: number;
1041
+ _id: string;
1042
+ accountId: string;
1043
+ amount: number;
1044
+ arrivalDate?: string;
1045
+ currency: string;
1046
+ failureCode?: string;
1047
+ failureMessage?: string;
1048
+ metadata?: any;
1049
+ method?: string;
1050
+ status: "pending" | "paid" | "failed" | "canceled" | "in_transit";
1051
+ stripePayoutId: string;
1052
+ totalsRecorded?: true;
1053
+ }>;
1054
+ totals: Array<{
1055
+ currency: string;
1056
+ paidOut: number;
1057
+ payoutCount: number;
990
1058
  }>;
991
1059
  },
992
1060
  Name
@@ -1072,10 +1140,13 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
1072
1140
  amount: number;
1073
1141
  arrivalDate?: string;
1074
1142
  currency: string;
1143
+ failureCode?: string;
1144
+ failureMessage?: string;
1075
1145
  metadata?: any;
1076
1146
  method?: string;
1077
1147
  status: "pending" | "paid" | "failed" | "canceled" | "in_transit";
1078
1148
  stripePayoutId: string;
1149
+ totalsRecorded?: true;
1079
1150
  } | null,
1080
1151
  Name
1081
1152
  >;
@@ -1090,10 +1161,13 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
1090
1161
  amount: number;
1091
1162
  arrivalDate?: string;
1092
1163
  currency: string;
1164
+ failureCode?: string;
1165
+ failureMessage?: string;
1093
1166
  metadata?: any;
1094
1167
  method?: string;
1095
1168
  status: "pending" | "paid" | "failed" | "canceled" | "in_transit";
1096
1169
  stripePayoutId: string;
1170
+ totalsRecorded?: true;
1097
1171
  } | null,
1098
1172
  Name
1099
1173
  >;
@@ -1164,6 +1238,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
1164
1238
  sourceInvoiceId?: string;
1165
1239
  status: "pending" | "paid" | "failed" | "reversed";
1166
1240
  stripeTransferId: string;
1241
+ totalsRecorded?: true;
1167
1242
  } | null,
1168
1243
  Name
1169
1244
  >;
@@ -1228,10 +1303,13 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
1228
1303
  amount: number;
1229
1304
  arrivalDate?: string;
1230
1305
  currency: string;
1306
+ failureCode?: string;
1307
+ failureMessage?: string;
1231
1308
  metadata?: any;
1232
1309
  method?: string;
1233
1310
  status: "pending" | "paid" | "failed" | "canceled" | "in_transit";
1234
1311
  stripePayoutId: string;
1312
+ totalsRecorded?: true;
1235
1313
  }>,
1236
1314
  Name
1237
1315
  >;
@@ -1295,6 +1373,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
1295
1373
  sourceInvoiceId?: string;
1296
1374
  status: "pending" | "paid" | "failed" | "reversed";
1297
1375
  stripeTransferId: string;
1376
+ totalsRecorded?: true;
1298
1377
  }>,
1299
1378
  Name
1300
1379
  >;
@@ -1319,6 +1398,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
1319
1398
  sourceInvoiceId?: string;
1320
1399
  status: "pending" | "paid" | "failed" | "reversed";
1321
1400
  stripeTransferId: string;
1401
+ totalsRecorded?: true;
1322
1402
  }>,
1323
1403
  Name
1324
1404
  >;
@@ -1340,6 +1420,44 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
1340
1420
  }>,
1341
1421
  Name
1342
1422
  >;
1423
+ paginatePayouts: FunctionReference<
1424
+ "query",
1425
+ "internal",
1426
+ {
1427
+ accountId: string;
1428
+ paginationOpts: {
1429
+ cursor: string | null;
1430
+ endCursor?: string | null;
1431
+ id?: number;
1432
+ maximumBytesRead?: number;
1433
+ maximumRowsRead?: number;
1434
+ numItems: number;
1435
+ };
1436
+ status?: "pending" | "paid" | "failed" | "canceled" | "in_transit";
1437
+ },
1438
+ {
1439
+ continueCursor: string;
1440
+ isDone: boolean;
1441
+ page: Array<{
1442
+ _creationTime: number;
1443
+ _id: string;
1444
+ accountId: string;
1445
+ amount: number;
1446
+ arrivalDate?: string;
1447
+ currency: string;
1448
+ failureCode?: string;
1449
+ failureMessage?: string;
1450
+ metadata?: any;
1451
+ method?: string;
1452
+ status: "pending" | "paid" | "failed" | "canceled" | "in_transit";
1453
+ stripePayoutId: string;
1454
+ totalsRecorded?: true;
1455
+ }>;
1456
+ pageStatus?: "SplitRecommended" | "SplitRequired" | null;
1457
+ splitCursor?: string | null;
1458
+ },
1459
+ Name
1460
+ >;
1343
1461
  paginateTransfersByAccount: FunctionReference<
1344
1462
  "query",
1345
1463
  "internal",
@@ -1374,6 +1492,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
1374
1492
  sourceInvoiceId?: string;
1375
1493
  status: "pending" | "paid" | "failed" | "reversed";
1376
1494
  stripeTransferId: string;
1495
+ totalsRecorded?: true;
1377
1496
  }>;
1378
1497
  pageStatus?: "SplitRecommended" | "SplitRequired" | null;
1379
1498
  splitCursor?: string | null;
@@ -2064,6 +2183,17 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
2064
2183
  >;
2065
2184
  };
2066
2185
  };
2186
+ earnings: {
2187
+ mutations: {
2188
+ backfillCurrencyTotals: FunctionReference<
2189
+ "mutation",
2190
+ "internal",
2191
+ { cursor: string | null; kind: "earnings" | "payouts" },
2192
+ { continueCursor: string; isDone: boolean },
2193
+ Name
2194
+ >;
2195
+ };
2196
+ };
2067
2197
  products: {
2068
2198
  mutations: {
2069
2199
  upsertPrice: FunctionReference<
@@ -2233,6 +2363,77 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
2233
2363
  }>,
2234
2364
  Name
2235
2365
  >;
2366
+ paginatePrices: FunctionReference<
2367
+ "query",
2368
+ "internal",
2369
+ {
2370
+ active?: boolean;
2371
+ paginationOpts: {
2372
+ cursor: string | null;
2373
+ endCursor?: string | null;
2374
+ id?: number;
2375
+ maximumBytesRead?: number;
2376
+ maximumRowsRead?: number;
2377
+ numItems: number;
2378
+ };
2379
+ productId?: string;
2380
+ },
2381
+ {
2382
+ continueCursor: string;
2383
+ isDone: boolean;
2384
+ page: Array<{
2385
+ _creationTime: number;
2386
+ _id: string;
2387
+ active: boolean;
2388
+ currency: string;
2389
+ interval?: "day" | "week" | "month" | "year";
2390
+ intervalCount?: number;
2391
+ metadata?: any;
2392
+ nickname?: string;
2393
+ productId: string;
2394
+ stripePriceId: string;
2395
+ stripeProductId: string;
2396
+ type: "one_time" | "recurring";
2397
+ unitAmount: number;
2398
+ }>;
2399
+ pageStatus?: "SplitRecommended" | "SplitRequired" | null;
2400
+ splitCursor?: string | null;
2401
+ },
2402
+ Name
2403
+ >;
2404
+ paginateProducts: FunctionReference<
2405
+ "query",
2406
+ "internal",
2407
+ {
2408
+ accountId?: string;
2409
+ active?: boolean;
2410
+ paginationOpts: {
2411
+ cursor: string | null;
2412
+ endCursor?: string | null;
2413
+ id?: number;
2414
+ maximumBytesRead?: number;
2415
+ maximumRowsRead?: number;
2416
+ numItems: number;
2417
+ };
2418
+ },
2419
+ {
2420
+ continueCursor: string;
2421
+ isDone: boolean;
2422
+ page: Array<{
2423
+ _creationTime: number;
2424
+ _id: string;
2425
+ accountId?: string;
2426
+ active: boolean;
2427
+ description?: string;
2428
+ metadata?: any;
2429
+ name: string;
2430
+ stripeProductId: string;
2431
+ }>;
2432
+ pageStatus?: "SplitRecommended" | "SplitRequired" | null;
2433
+ splitCursor?: string | null;
2434
+ },
2435
+ Name
2436
+ >;
2236
2437
  };
2237
2438
  };
2238
2439
  settlement: {
@@ -1,7 +1,7 @@
1
1
  import { v } from "convex/values";
2
2
 
3
3
  import type { Doc, Id } from "../_generated/dataModel";
4
- import { mutation } from "../_generated/server";
4
+ import { mutation } from "../earnings/triggers.js";
5
5
  import { feeRoutingFields } from "../lib/fees";
6
6
  import { computeReversalSlices, deriveReversalState } from "../lib/reversals";
7
7
  import { paymentSeller } from "../lib/paymentSeller";
@@ -121,6 +121,8 @@ export const upsertPayout = mutation({
121
121
  status: payoutStatusValidator,
122
122
  arrivalDate: v.optional(v.string()),
123
123
  method: v.optional(v.string()),
124
+ failureCode: v.optional(v.string()),
125
+ failureMessage: v.optional(v.string()),
124
126
  metadata: v.optional(v.any()),
125
127
  },
126
128
  returns: v.null(),
@@ -133,7 +135,11 @@ export const upsertPayout = mutation({
133
135
  .first();
134
136
 
135
137
  if (existing) {
136
- await ctx.db.patch("payouts", existing._id, args);
138
+ await ctx.db.patch("payouts", existing._id, {
139
+ ...args,
140
+ failureCode: args.failureCode,
141
+ failureMessage: args.failureMessage,
142
+ });
137
143
  } else {
138
144
  await ctx.db.insert("payouts", args);
139
145
  }
@@ -142,9 +148,7 @@ export const upsertPayout = mutation({
142
148
  },
143
149
  });
144
150
 
145
- // =============================================================================
146
151
  // REFUND MUTATIONS
147
- // =============================================================================
148
152
 
149
153
  export const upsertRefund = mutation({
150
154
  args: refundFields,
@@ -283,9 +287,7 @@ export const recordPaymentFeeRefund = mutation({
283
287
  },
284
288
  });
285
289
 
286
- // =============================================================================
287
290
  // DISPUTE MUTATIONS
288
- // =============================================================================
289
291
 
290
292
  export const upsertDispute = mutation({
291
293
  args: disputeFields,
@@ -311,9 +313,7 @@ export const upsertDispute = mutation({
311
313
  },
312
314
  });
313
315
 
314
- // =============================================================================
315
316
  // TRANSFER MUTATIONS (ledger — BTS-12)
316
- // =============================================================================
317
317
 
318
318
  export const upsertTransfer = mutation({
319
319
  args: transferFields,
@@ -1,3 +1,8 @@
1
+ import {
2
+ getAccountEarnings as getAccountEarningsLegacy,
3
+ getAccountPayouts as getAccountPayoutsLegacy,
4
+ } from "../earnings/legacyReads.js";
5
+ import { currencyTotals, previewSize } from "../earnings/reads.js";
1
6
  import { v } from "convex/values";
2
7
  import { paginator } from "convex-helpers/server/pagination";
3
8
  import schema from "../schema.js";
@@ -21,9 +26,7 @@ import {
21
26
  wedgedReversalClaimValidator,
22
27
  } from "./validators";
23
28
 
24
- // =============================================================================
25
29
  // PAYMENT QUERIES
26
- // =============================================================================
27
30
 
28
31
  export const getPaymentByStripeId = query({
29
32
  args: { stripePaymentIntentId: v.string() },
@@ -38,9 +41,7 @@ export const getPaymentByStripeId = query({
38
41
  },
39
42
  });
40
43
 
41
- // =============================================================================
42
44
  // PAYOUT QUERIES
43
- // =============================================================================
44
45
 
45
46
  export const getPayout = query({
46
47
  args: { payoutId: v.id("payouts") },
@@ -111,9 +112,7 @@ export const listPayouts = query({
111
112
  },
112
113
  });
113
114
 
114
- // =============================================================================
115
115
  // REFUND QUERIES
116
- // =============================================================================
117
116
 
118
117
  export const getRefundByStripeId = query({
119
118
  args: { stripeRefundId: v.string() },
@@ -196,9 +195,7 @@ export const listRefunds = query({
196
195
  },
197
196
  });
198
197
 
199
- // =============================================================================
200
198
  // DISPUTE QUERIES
201
- // =============================================================================
202
199
 
203
200
  export const getDisputeByStripeId = query({
204
201
  args: { stripeDisputeId: v.string() },
@@ -279,9 +276,7 @@ export const listDisputes = query({
279
276
  },
280
277
  });
281
278
 
282
- // =============================================================================
283
279
  // TRANSFER QUERIES (ledger — BTS-12)
284
- // =============================================================================
285
280
 
286
281
  export const getTransferByStripeId = query({
287
282
  args: { stripeTransferId: v.string() },
@@ -366,9 +361,7 @@ export const paginateTransfersByAccount = query({
366
361
  .paginate(args.paginationOpts),
367
362
  });
368
363
 
369
- // =============================================================================
370
364
  // REVERSAL-CLAIM RECLAIM (BTS-74) — ops diagnostics for wedged legs
371
- // =============================================================================
372
365
 
373
366
  /**
374
367
  * Fetch one reversal operation's claim record (BTS-63/74) by its id, or null.
@@ -447,9 +440,7 @@ export const listWedgedReversalClaims = query({
447
440
  },
448
441
  });
449
442
 
450
- // =============================================================================
451
443
  // EARNINGS AGGREGATES (BTS-64)
452
- // =============================================================================
453
444
  //
454
445
  // The row-list queries above `.take(50)`, so summing their rows client-side
455
446
  // (as the earnings hooks did) silently understated totals for accounts with
@@ -479,27 +470,7 @@ export const getAccountEarnings = query({
479
470
  transferCount: v.number(),
480
471
  transfers: v.array(transferDocValidator),
481
472
  }),
482
- handler: async (ctx, args) => {
483
- const previewLimit = args.previewLimit ?? 50;
484
- let gross = 0;
485
- let reversed = 0;
486
- let transferCount = 0;
487
- const transfers: Doc<"transfers">[] = [];
488
-
489
- for await (const tr of ctx.db
490
- .query("transfers")
491
- .withIndex("by_destination_account_id", (q) =>
492
- q.eq("destinationAccountId", args.destinationAccountId),
493
- )
494
- .order("desc")) {
495
- gross += tr.amount;
496
- reversed += tr.reversedAmount ?? 0;
497
- transferCount += 1;
498
- if (transfers.length < previewLimit) transfers.push(tr);
499
- }
500
-
501
- return { gross, reversed, transferCount, transfers };
502
- },
473
+ handler: getAccountEarningsLegacy,
503
474
  });
504
475
 
505
476
  /**
@@ -515,20 +486,111 @@ export const getAccountPayouts = query({
515
486
  payoutCount: v.number(),
516
487
  payouts: v.array(payoutDocValidator),
517
488
  }),
518
- handler: async (ctx, args) => {
519
- const previewLimit = args.previewLimit ?? 50;
520
- let paidOut = 0;
521
- let payoutCount = 0;
522
- const payouts: Doc<"payouts">[] = [];
489
+ handler: getAccountPayoutsLegacy,
490
+ });
523
491
 
524
- for await (const p of ctx.db
525
- .query("payouts")
526
- .withIndex("by_account_id", (q) => q.eq("accountId", args.accountId))) {
527
- payoutCount += 1;
528
- if (p.status === "paid") paidOut += p.amount;
529
- if (payouts.length < previewLimit) payouts.push(p);
530
- }
492
+ /** Reactive, account-scoped history. A page is never a financial total. */
493
+ export const paginatePayouts = query({
494
+ args: {
495
+ accountId: v.string(),
496
+ status: v.optional(payoutStatusValidator),
497
+ paginationOpts: paginationOptsValidator,
498
+ },
499
+ returns: paginationResultValidator(payoutDocValidator),
500
+ handler: (ctx, args) => {
501
+ if (
502
+ !Number.isInteger(args.paginationOpts.numItems) ||
503
+ args.paginationOpts.numItems < 1 ||
504
+ args.paginationOpts.numItems > 100
505
+ )
506
+ throw new Error("Page size must be an integer from 1 to 100");
507
+ const rows = paginator(ctx.db, schema).query("payouts");
508
+ const scoped =
509
+ args.status === undefined
510
+ ? rows.withIndex("by_account_id", (q) =>
511
+ q.eq("accountId", args.accountId),
512
+ )
513
+ : rows.withIndex("by_account_status", (q) =>
514
+ q.eq("accountId", args.accountId).eq("status", args.status!),
515
+ );
516
+ return scoped.order("desc").paginate({
517
+ ...args.paginationOpts,
518
+ maximumRowsRead: 200,
519
+ maximumBytesRead: 1_000_000,
520
+ });
521
+ },
522
+ });
531
523
 
532
- return { paidOut, payoutCount, payouts };
524
+ export const getAccountEarningsByCurrency = query({
525
+ args: {
526
+ destinationAccountId: v.string(),
527
+ previewLimit: v.optional(v.number()),
528
+ },
529
+ returns: v.object({
530
+ totals: v.array(
531
+ v.object({
532
+ currency: v.string(),
533
+ gross: v.number(),
534
+ reversed: v.number(),
535
+ net: v.number(),
536
+ transferCount: v.number(),
537
+ }),
538
+ ),
539
+ transfers: v.array(transferDocValidator),
540
+ }),
541
+ handler: async (ctx, args) => {
542
+ const limit = previewSize(args.previewLimit);
543
+ const rows = await currencyTotals(
544
+ ctx,
545
+ args.destinationAccountId,
546
+ "earnings",
547
+ );
548
+ const transfers = await ctx.db
549
+ .query("transfers")
550
+ .withIndex("by_destination_account_id", (q) =>
551
+ q.eq("destinationAccountId", args.destinationAccountId),
552
+ )
553
+ .order("desc")
554
+ .take(limit);
555
+ return {
556
+ totals: rows.map((r) => ({
557
+ currency: r.currency,
558
+ gross: r.gross,
559
+ reversed: r.reversed,
560
+ net: r.gross - r.reversed,
561
+ transferCount: r.count,
562
+ })),
563
+ transfers,
564
+ };
565
+ },
566
+ });
567
+ export const getAccountPayoutsByCurrency = query({
568
+ args: { accountId: v.string(), previewLimit: v.optional(v.number()) },
569
+ returns: v.object({
570
+ totals: v.array(
571
+ v.object({
572
+ currency: v.string(),
573
+ paidOut: v.number(),
574
+ payoutCount: v.number(),
575
+ }),
576
+ ),
577
+ payouts: v.array(payoutDocValidator),
578
+ }),
579
+ handler: async (ctx, args) => {
580
+ const limit = previewSize(args.previewLimit);
581
+ const rows = await currencyTotals(ctx, args.accountId, "payouts");
582
+ const payouts = await ctx.db
583
+ .query("payouts")
584
+ .withIndex("by_account_id", (q) => q.eq("accountId", args.accountId))
585
+ .order("desc")
586
+ .take(limit);
587
+ return {
588
+ totals: rows.map((r) => ({
589
+ currency: r.currency,
590
+ paidOut: r.paidOut,
591
+ payoutCount: r.count,
592
+ })),
593
+ payouts,
594
+ };
533
595
  },
534
596
  });
@@ -1,3 +1,4 @@
1
+ import { v } from "convex/values";
1
2
  import { defineTable } from "convex/server";
2
3
 
3
4
  import {
@@ -22,10 +23,14 @@ export const paymentsTable = defineTable(paymentFields)
22
23
  /**
23
24
  * Payouts to V2 accounts (marketplace).
24
25
  */
25
- export const payoutsTable = defineTable(payoutFields)
26
+ export const payoutsTable = defineTable({
27
+ ...payoutFields,
28
+ totalsRecorded: v.optional(v.literal(true)),
29
+ })
26
30
  .index("by_stripe_payout_id", ["stripePayoutId"])
27
31
  .index("by_account_id", ["accountId"])
28
32
  .index("by_status", ["status"])
33
+ .index("by_account_totals_recorded", ["accountId", "totalsRecorded"])
29
34
  .index("by_account_status", ["accountId", "status"]);
30
35
 
31
36
  /**
@@ -63,6 +68,10 @@ export const transfersTable = defineTable({
63
68
  ...transferPersistedFields,
64
69
  })
65
70
  .index("by_stripe_transfer_id", ["stripeTransferId"])
71
+ .index("by_account_totals_recorded", [
72
+ "destinationAccountId",
73
+ "totalsRecorded",
74
+ ])
66
75
  .index("by_destination_account_id", ["destinationAccountId"])
67
76
  .index("by_source_charge_id", ["sourceChargeId"]);
68
77
 
@@ -72,6 +72,8 @@ export const payoutFields = {
72
72
  status: payoutStatusValidator,
73
73
  arrivalDate: v.optional(v.string()),
74
74
  method: v.optional(v.string()),
75
+ failureCode: v.optional(v.string()),
76
+ failureMessage: v.optional(v.string()),
75
77
  metadata: v.optional(v.any()),
76
78
  };
77
79
 
@@ -80,6 +82,7 @@ export const payoutDocValidator = v.object({
80
82
  _id: v.id("payouts"),
81
83
  _creationTime: v.number(),
82
84
  ...payoutFields,
85
+ totalsRecorded: v.optional(v.literal(true)),
83
86
  });
84
87
 
85
88
  // =============================================================================
@@ -266,6 +269,7 @@ export const transferFields = {
266
269
  * redelivered base payload must not clobber an in-flight reversal claim.
267
270
  */
268
271
  export const transferPersistedFields = {
272
+ totalsRecorded: v.optional(v.literal(true)),
269
273
  /**
270
274
  * Reversal claim frontier (BTS-63): the highest cumulative reversal amount
271
275
  * reserved by `claimReversalSlices` before money moves. Always ≥