@fantastic.dev/better-stripe 0.6.0 → 0.7.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 (39) hide show
  1. package/CHANGELOG.md +73 -45
  2. package/README.md +22 -14
  3. package/dist/client/webhooks/processors.js +9 -5
  4. package/dist/client/webhooks/processors.js.map +1 -1
  5. package/dist/component/_generated/component.d.ts +34 -0
  6. package/dist/component/_generated/component.d.ts.map +1 -1
  7. package/dist/component/billing/queries.d.ts +36 -0
  8. package/dist/component/billing/queries.d.ts.map +1 -1
  9. package/dist/component/billing/queries.js +10 -0
  10. package/dist/component/billing/queries.js.map +1 -1
  11. package/dist/component/billing/schema.d.ts +1 -0
  12. package/dist/component/billing/schema.d.ts.map +1 -1
  13. package/dist/component/billing/schema.js +1 -0
  14. package/dist/component/billing/schema.js.map +1 -1
  15. package/dist/component/connect/mutations.d.ts.map +1 -1
  16. package/dist/component/connect/mutations.js +46 -8
  17. package/dist/component/connect/mutations.js.map +1 -1
  18. package/dist/component/connect/queries.d.ts +3 -4
  19. package/dist/component/connect/queries.d.ts.map +1 -1
  20. package/dist/component/connect/queries.js +19 -10
  21. package/dist/component/connect/queries.js.map +1 -1
  22. package/dist/component/schema.d.ts +1 -0
  23. package/dist/react/hooks/useSplitBreakdown.d.ts +4 -6
  24. package/dist/react/hooks/useSplitBreakdown.d.ts.map +1 -1
  25. package/dist/react/hooks/useSplitBreakdown.js +4 -6
  26. package/dist/react/hooks/useSplitBreakdown.js.map +1 -1
  27. package/package.json +1 -1
  28. package/src/client/webhooks/processors.test.ts +16 -0
  29. package/src/client/webhooks/processors.ts +8 -3
  30. package/src/component/_generated/component.ts +46 -0
  31. package/src/component/billing/queries.ts +12 -0
  32. package/src/component/billing/schema.ts +1 -0
  33. package/src/component/connect/mutations.ts +68 -17
  34. package/src/component/connect/queries.ts +18 -10
  35. package/src/component/reversal-races.test.ts +41 -1
  36. package/src/component/seller-case-ownership.test.ts +61 -0
  37. package/src/component/store-subscriptions.test.ts +32 -0
  38. package/src/component/transfers.test.ts +69 -0
  39. package/src/react/hooks/useSplitBreakdown.ts +4 -6
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ - Return every original split leg by default from `listTransfersByCharge`,
6
+ excluding reinstatement rows before applying any explicit preview limit.
7
+ Split breakdowns and reversal claims now cover sales with more than 50 legs.
8
+ Queries and atomic claims fail on Convex resource limits rather than silently
9
+ truncate financial data. `listTransfersByAccount` remains a preview API;
10
+ earnings hooks use exact server-side aggregates.
11
+ - Update the former roadmap to describe the existing product-level first-charge
12
+ descriptors, first-invoice fee recovery, retry-safe reversal claims, and exact
13
+ earnings aggregates, including their practical limitations.
14
+ - Remove internal tracker references from public documentation and add a
15
+ regression check that prevents their reintroduction.
16
+
17
+ ## [0.7.0](https://github.com/FantasticDevHQ/better-stripe/compare/v0.6.1...v0.7.0) (2026-10-13)
18
+
19
+
20
+ ### Features
21
+
22
+ * expand marketplace example with seller billing and sandbox fixtures ([#129](https://github.com/FantasticDevHQ/better-stripe/issues/129)) ([7f21f53](https://github.com/FantasticDevHQ/better-stripe/commit/7f21f5365ce82cab9ecc984b94f444408223f8b8))
23
+
24
+ ## [0.6.1](https://github.com/FantasticDevHQ/better-stripe/compare/v0.6.0...v0.6.1) (2026-10-13)
25
+
26
+
27
+ ### Bug Fixes
28
+
29
+ * complete large split reversals and keep public docs current ([#127](https://github.com/FantasticDevHQ/better-stripe/issues/127)) ([e7cc0a9](https://github.com/FantasticDevHQ/better-stripe/commit/e7cc0a9b0ea01f4993447f9d73edebe71b364081))
30
+
3
31
  ## [0.6.0](https://github.com/FantasticDevHQ/better-stripe/compare/v0.5.0...v0.6.0) (2026-10-12)
4
32
 
5
33
 
@@ -10,7 +38,7 @@
10
38
  ### Features
11
39
 
12
40
  * complete Stripe Endive compatibility and release 0.5.0 ([#126](https://github.com/FantasticDevHQ/better-stripe/issues/126)) ([135695f](https://github.com/FantasticDevHQ/better-stripe/commit/135695f51906ea156a36dbe8d0d43cf1d99e5fc1))
13
- * publish as [@fantastic](https://github.com/fantastic).dev/better-stripe via Release Please and trusted publishing (BTS-114) ([#120](https://github.com/FantasticDevHQ/better-stripe/issues/120)) ([7451eeb](https://github.com/FantasticDevHQ/better-stripe/commit/7451eeb514b09a09656355c74c4bec083e1ddce7))
41
+ * publish as [@fantastic](https://github.com/fantastic).dev/better-stripe via Release Please and trusted publishing ([#120](https://github.com/FantasticDevHQ/better-stripe/issues/120)) ([7451eeb](https://github.com/FantasticDevHQ/better-stripe/commit/7451eeb514b09a09656355c74c4bec083e1ddce7))
14
42
 
15
43
 
16
44
  ### Bug Fixes
@@ -20,7 +48,7 @@
20
48
 
21
49
  ### Maintenance
22
50
 
23
- * **deps:** update dependencies to latest (BTS-115) ([#122](https://github.com/FantasticDevHQ/better-stripe/issues/122)) ([a7e4161](https://github.com/FantasticDevHQ/better-stripe/commit/a7e416179d20afdd2356668cbd4482e2e3fa4eff))
51
+ * **deps:** update dependencies to latest ([#122](https://github.com/FantasticDevHQ/better-stripe/issues/122)) ([a7e4161](https://github.com/FantasticDevHQ/better-stripe/commit/a7e416179d20afdd2356668cbd4482e2e3fa4eff))
24
52
 
25
53
 
26
54
  ### Continuous Integration
@@ -69,13 +97,13 @@ changed by this release.
69
97
 
70
98
  ### Minor Changes
71
99
 
72
- - [#76](https://github.com/kellykampen/better-stripe/pull/76) [`c9f1489`](https://github.com/kellykampen/better-stripe/commit/c9f148948f2f5ea77f343c5fc1a78faffbb5ac1e) Thanks [@kellykampen](https://github.com/kellykampen)! - Add `getAccountBalance` for per-account balance retrieval (BTS-65, [#62](https://github.com/kellykampen/better-stripe/issues/62)).
100
+ - [#76](https://github.com/kellykampen/better-stripe/pull/76) [`c9f1489`](https://github.com/kellykampen/better-stripe/commit/c9f148948f2f5ea77f343c5fc1a78faffbb5ac1e) Thanks [@kellykampen](https://github.com/kellykampen)! - Add `getAccountBalance` for per-account balance retrieval ([#62](https://github.com/kellykampen/better-stripe/issues/62)).
73
101
 
74
102
  New `BetterStripe.getAccountBalance({ stripeAccountId })` reads a connected account's live Stripe balance, scoped via the `Stripe-Account` header (mirroring the other per-account calls like `createPayout`). Pairs Stripe's separate `available`/`pending` per-currency lists into one `AccountBalance` (`{ available, pending, currency }`, minor units) per currency — a currency present on only one side defaults to `0` on the other. `AccountBalance` is re-exported at the package root, and its shape matches the `PayoutSchedule` component's `balance` prop directly.
75
103
 
76
104
  Purely additive.
77
105
 
78
- - [#76](https://github.com/kellykampen/better-stripe/pull/76) [`c9f1489`](https://github.com/kellykampen/better-stripe/commit/c9f148948f2f5ea77f343c5fc1a78faffbb5ac1e) Thanks [@kellykampen](https://github.com/kellykampen)! - Add headless `BuyerBillingView` component (BTS-39, [#50](https://github.com/kellykampen/better-stripe/issues/50)).
106
+ - [#76](https://github.com/kellykampen/better-stripe/pull/76) [`c9f1489`](https://github.com/kellykampen/better-stripe/commit/c9f148948f2f5ea77f343c5fc1a78faffbb5ac1e) Thanks [@kellykampen](https://github.com/kellykampen)! - Add headless `BuyerBillingView` component ([#50](https://github.com/kellykampen/better-stripe/issues/50)).
79
107
 
80
108
  The buyer's embedded-only, in-app billing surface: shows a buyer's subscriptions grouped by store (via the existing `groupSubscriptionsByStore` util, not recomputed), with per-store cancel/reactivate through the shared `SubscriptionCard`/`SubscriptionActions`, the one saved payment method reusable across every store, and an embedded (no-redirect) card-update slot.
81
109
 
@@ -88,20 +116,20 @@ changed by this release.
88
116
 
89
117
  Previously `DEFAULT_ACCOUNT_CONFIGURATION` only requested the merchant configuration, so there was no first-class path to create a customer-billable account.
90
118
 
91
- - [#76](https://github.com/kellykampen/better-stripe/pull/76) [`c9f1489`](https://github.com/kellykampen/better-stripe/commit/c9f148948f2f5ea77f343c5fc1a78faffbb5ac1e) Thanks [@kellykampen](https://github.com/kellykampen)! - Add `DisputeDetail` + `EvidenceForm` headless React components (BTS-54, [#40](https://github.com/kellykampen/better-stripe/issues/40)).
119
+ - [#76](https://github.com/kellykampen/better-stripe/pull/76) [`c9f1489`](https://github.com/kellykampen/better-stripe/commit/c9f148948f2f5ea77f343c5fc1a78faffbb5ac1e) Thanks [@kellykampen](https://github.com/kellykampen)! - Add `DisputeDetail` + `EvidenceForm` headless React components ([#40](https://github.com/kellykampen/better-stripe/issues/40)).
92
120
 
93
121
  - `DisputeDetail` — renders a dispute's amount, reason, status, evidence-due countdown message, and linked transfer ids. Loading/empty states, i18n label overrides.
94
122
  - `EvidenceForm` — headless controlled form for the four Skool-style evidence fields (product description, access activity, additional info, customer communication); `stage()` saves a draft (no `submit` key), `submit()` finalizes with `submit: true`. Supports `stripeAccountId` scoping and `onStaged`/`onSubmitted`/`onError` callbacks.
95
123
 
96
- Both are headless/themeable (`className` plus a `children` render-prop) and consume the existing BTS-31/BTS-53 core with no duplicated logic. Purely additive.
124
+ Both are headless/themeable (`className` plus a `children` render-prop) and consume the existing dispute handling/dispute hooks core with no duplicated logic. Purely additive.
97
125
 
98
- - [#76](https://github.com/kellykampen/better-stripe/pull/76) [`c9f1489`](https://github.com/kellykampen/better-stripe/commit/c9f148948f2f5ea77f343c5fc1a78faffbb5ac1e) Thanks [@kellykampen](https://github.com/kellykampen)! - Add headless `DisputesList` component with due-by countdown badges (BTS-40, [#41](https://github.com/kellykampen/better-stripe/issues/41)).
126
+ - [#76](https://github.com/kellykampen/better-stripe/pull/76) [`c9f1489`](https://github.com/kellykampen/better-stripe/commit/c9f148948f2f5ea77f343c5fc1a78faffbb5ac1e) Thanks [@kellykampen](https://github.com/kellykampen)! - Add headless `DisputesList` component with due-by countdown badges ([#41](https://github.com/kellykampen/better-stripe/issues/41)).
99
127
 
100
- Pure, hook-free presentational list of a seller's disputes, sorted by evidence deadline (soonest first, no-deadline rows last, overdue rows naturally at the top). Rows accept the exact shape `getDisputeWithCountdown`/`useDisputeWithCountdown` (BTS-53) produce, so the component never recomputes deadlines itself.
128
+ Pure, hook-free presentational list of a seller's disputes, sorted by evidence deadline (soonest first, no-deadline rows last, overdue rows naturally at the top). Rows accept the exact shape `getDisputeWithCountdown`/`useDisputeWithCountdown` produce, so the component never recomputes deadlines itself.
101
129
 
102
130
  Headless overrides: `className`, i18n labels (`emptyLabel`, `loadingLabel`, `dueLabel`/`overdueLabel` with `{days}` templating), a per-row `renderRow` slot, and a whole-list `children` render prop. Default markup is semantic `ul[role=list] > li[data-dispute-id]` with a `span[role=status]` badge carrying `data-overdue`. Purely additive.
103
131
 
104
- - [#76](https://github.com/kellykampen/better-stripe/pull/76) [`c9f1489`](https://github.com/kellykampen/better-stripe/commit/c9f148948f2f5ea77f343c5fc1a78faffbb5ac1e) Thanks [@kellykampen](https://github.com/kellykampen)! - Add dispute React hooks: `createUseDisputes` + `createUseDisputeWithCountdown` (BTS-53, [#36](https://github.com/kellykampen/better-stripe/issues/36)).
132
+ - [#76](https://github.com/kellykampen/better-stripe/pull/76) [`c9f1489`](https://github.com/kellykampen/better-stripe/commit/c9f148948f2f5ea77f343c5fc1a78faffbb5ac1e) Thanks [@kellykampen](https://github.com/kellykampen)! - Add dispute React hooks: `createUseDisputes` + `createUseDisputeWithCountdown` ([#36](https://github.com/kellykampen/better-stripe/issues/36)).
105
133
 
106
134
  Two new factory hooks following the existing `createUse*` convention:
107
135
 
@@ -110,7 +138,7 @@ changed by this release.
110
138
 
111
139
  Both wired into the `@getdojo/better-stripe/react` entry export; `StripeComponentDispute` re-exported for consumers. Purely additive.
112
140
 
113
- - [#51](https://github.com/kellykampen/better-stripe/pull/51) [`6345352`](https://github.com/kellykampen/better-stripe/commit/6345352cf4b3ad4247c865090693e877da6bf5ef) Thanks [@kellykampen](https://github.com/kellykampen)! - Smart-retry dunning: surface failed subscription cycles (BTS-33).
141
+ - [#51](https://github.com/kellykampen/better-stripe/pull/51) [`6345352`](https://github.com/kellykampen/better-stripe/commit/6345352cf4b3ad4247c865090693e877da6bf5ef) Thanks [@kellykampen](https://github.com/kellykampen)! - Smart-retry dunning: surface failed subscription cycles.
114
142
 
115
143
  `invoice.payment_failed` previously upserted the invoice but fired **no** async hook, so apps couldn't react to a failed subscription cycle. It now schedules a new invoice-level hook:
116
144
 
@@ -124,7 +152,7 @@ changed by this release.
124
152
 
125
153
  Note: covered by unit tests (hook scheduling, invoice state transition + retry metadata, `past_due`/`unpaid` persistence). The live `invoice.payment_failed` path (real failed-card subscription cycle) should be exercised by the E2E webhook run before release.
126
154
 
127
- - [#64](https://github.com/kellykampen/better-stripe/pull/64) [`477e282`](https://github.com/kellykampen/better-stripe/commit/477e282ea1cfd2fc198f1aa19cc376eea96b38ca) Thanks [@kellykampen](https://github.com/kellykampen)! - Exact earnings totals — fix silently truncated financial figures past 50 rows (BTS-64).
155
+ - [#64](https://github.com/kellykampen/better-stripe/pull/64) [`477e282`](https://github.com/kellykampen/better-stripe/commit/477e282ea1cfd2fc198f1aa19cc376eea96b38ca) Thanks [@kellykampen](https://github.com/kellykampen)! - Exact earnings totals — fix silently truncated financial figures past 50 rows.
128
156
 
129
157
  `listTransfersByAccount` and `listPayouts` cap their results at 50 rows, and the `useEarnings` hook summed those rows client-side — so an account with more than 50 transfers or payouts reported **silently wrong** (understated) gross / reversed / net / paid-out totals, with no error.
130
158
 
@@ -139,37 +167,37 @@ changed by this release.
139
167
 
140
168
  **Behavior change:** if you wired `createUseEarnings` against `listTransfersByAccount` / `listPayouts`, repoint the refs at the new aggregate queries. The list queries and `BetterStripe.listPayouts()` are unchanged, now documented as capped drill-down views. `listTransfersByCharge` (per-sale split legs) is unchanged and safe in practice — a sale has few legs.
141
169
 
142
- - [#76](https://github.com/kellykampen/better-stripe/pull/76) [`c9f1489`](https://github.com/kellykampen/better-stripe/commit/c9f148948f2f5ea77f343c5fc1a78faffbb5ac1e) Thanks [@kellykampen](https://github.com/kellykampen)! - Add `createUseEarnings` + `createUseSplitBreakdown` React hook factories (BTS-36, [#44](https://github.com/kellykampen/better-stripe/issues/44)).
170
+ - [#76](https://github.com/kellykampen/better-stripe/pull/76) [`c9f1489`](https://github.com/kellykampen/better-stripe/commit/c9f148948f2f5ea77f343c5fc1a78faffbb5ac1e) Thanks [@kellykampen](https://github.com/kellykampen)! - Add `createUseEarnings` + `createUseSplitBreakdown` React hook factories ([#44](https://github.com/kellykampen/better-stripe/issues/44)).
143
171
 
144
172
  - `createUseEarnings(useQuery, transfersRef, payoutsRef)` → `useEarnings(accountId)` reads a recipient's transfer ledger and payouts, deriving `gross` (total transferred in), `reversed` (pulled back via fee collection + refund/dispute clawbacks — deliberately not labeled "fees", since a true platform-fees figure lives on the payments rows' `feeCollectedAmount`), `net`, `payouts`/`paidOut`, and the raw `transfers` for drill-down.
145
173
  - `createUseSplitBreakdown(useQuery, transfersByChargeRef)` → `useSplitBreakdown({ sourceChargeId, saleAmount? })` sums a sale's split legs by role (`store`/`affiliate`/`other`), each net of reversals, with the `platform` share derived when the caller supplies the sale amount.
146
174
 
147
- Both follow the `createUse*` factory convention from BTS-53. Purely additive.
175
+ Both follow the `createUse*` factory convention from dispute hooks. Purely additive.
148
176
 
149
- - [#76](https://github.com/kellykampen/better-stripe/pull/76) [`c9f1489`](https://github.com/kellykampen/better-stripe/commit/c9f148948f2f5ea77f343c5fc1a78faffbb5ac1e) Thanks [@kellykampen](https://github.com/kellykampen)! - Add embedded Stripe Connect dispute components (BTS-55, [#48](https://github.com/kellykampen/better-stripe/issues/48)).
177
+ - [#76](https://github.com/kellykampen/better-stripe/pull/76) [`c9f1489`](https://github.com/kellykampen/better-stripe/commit/c9f148948f2f5ea77f343c5fc1a78faffbb5ac1e) Thanks [@kellykampen](https://github.com/kellykampen)! - Add embedded Stripe Connect dispute components ([#48](https://github.com/kellykampen/better-stripe/issues/48)).
150
178
 
151
179
  The Stripe-hosted alternative to the headless dispute components, built on `@stripe/react-connect-js`:
152
180
 
153
- - `ConnectProvider` — initializes ConnectJS once per mount via `loadConnectAndInitialize({ publishableKey, fetchClientSecret, appearance?, locale? })` and provides the instance through `ConnectComponentsProvider`. `fetchClientSecret` wires to an app action wrapping `stripe.createDisputeSession({ stripeAccountId })` (BTS-30).
181
+ - `ConnectProvider` — initializes ConnectJS once per mount via `loadConnectAndInitialize({ publishableKey, fetchClientSecret, appearance?, locale? })` and provides the instance through `ConnectComponentsProvider`. `fetchClientSecret` wires to an app action wrapping `stripe.createDisputeSession({ stripeAccountId })`.
154
182
  - `EmbeddedDisputes` — mounts Stripe's `disputes_list` embedded component, or `payment_disputes` scoped to one payment via the `payment` prop. Must render inside `ConnectProvider`.
155
183
 
156
184
  Adds `@stripe/connect-js` and `@stripe/react-connect-js` as regular dependencies, mirroring the existing `@stripe/stripe-js`/`@stripe/react-stripe-js` convention. Purely additive.
157
185
 
158
- - [#76](https://github.com/kellykampen/better-stripe/pull/76) [`c9f1489`](https://github.com/kellykampen/better-stripe/commit/c9f148948f2f5ea77f343c5fc1a78faffbb5ac1e) Thanks [@kellykampen](https://github.com/kellykampen)! - Add headless `PayoutSchedule` component for recipient balance + payout display (BTS-38, [#46](https://github.com/kellykampen/better-stripe/issues/46)).
186
+ - [#76](https://github.com/kellykampen/better-stripe/pull/76) [`c9f1489`](https://github.com/kellykampen/better-stripe/commit/c9f148948f2f5ea77f343c5fc1a78faffbb5ac1e) Thanks [@kellykampen](https://github.com/kellykampen)! - Add headless `PayoutSchedule` component for recipient balance + payout display ([#46](https://github.com/kellykampen/better-stripe/issues/46)).
159
187
 
160
188
  Pure, hook-free component showing a seller's available/pending balance (from a `RecipientBalance` snapshot prop) and next payout — the earliest `pending`/`in_transit` payout row with an arrival date, templated as "Next payout of $40.00 expected Jul 10, 2026", falling back to a rolling-schedule message when none is upcoming (payouts are automatic/rolling; the component deliberately offers no manual withdrawal). Optional `manageUrl` link out to the hosted Express dashboard.
161
189
 
162
190
  Headless conventions: `className`, i18n label overrides, loading/empty states, and a `children` render-prop with all computed state. Data comes entirely via props (no data-fetching hook yet); purely additive.
163
191
 
164
- - [#76](https://github.com/kellykampen/better-stripe/pull/76) [`c9f1489`](https://github.com/kellykampen/better-stripe/commit/c9f148948f2f5ea77f343c5fc1a78faffbb5ac1e) Thanks [@kellykampen](https://github.com/kellykampen)! - Add product-level `statement_descriptor` so it covers a subscription's first charge (BTS-67, [#63](https://github.com/kellykampen/better-stripe/issues/63)).
192
+ - [#76](https://github.com/kellykampen/better-stripe/pull/76) [`c9f1489`](https://github.com/kellykampen/better-stripe/commit/c9f148948f2f5ea77f343c5fc1a78faffbb5ac1e) Thanks [@kellykampen](https://github.com/kellykampen)! - Add product-level `statement_descriptor` so it covers a subscription's first charge ([#63](https://github.com/kellykampen/better-stripe/issues/63)).
165
193
 
166
- The invoice-time descriptor path (BTS-32) sets the descriptor at `invoice.created`, which never reaches a subscription's first invoice — Stripe finalizes and (for `charge_automatically`/Checkout subscription-mode) pays invoice [#1](https://github.com/kellykampen/better-stripe/issues/1) synchronously at creation, before that handler runs. Per Stripe's descriptor precedence (Invoice → Product → charge-type default), a product-level `statement_descriptor` covers every cycle including the first.
194
+ The invoice-time descriptor path sets the descriptor at `invoice.created`, which never reaches a subscription's first invoice — Stripe finalizes and (for `charge_automatically`/Checkout subscription-mode) pays invoice [#1](https://github.com/kellykampen/better-stripe/issues/1) synchronously at creation, before that handler runs. Per Stripe's descriptor precedence (Invoice → Product → charge-type default), a product-level `statement_descriptor` covers every cycle including the first.
167
195
 
168
- `createProduct`/`updateProduct` now resolve the per-store descriptor (the store's stored suffix, else the platform default — mirroring the BTS-32 account-level resolution) and stamp it onto the Stripe Product's `statement_descriptor`, for products with an `accountId`. No public API change to existing call shapes.
196
+ `createProduct`/`updateProduct` now resolve the per-store descriptor (the store's stored suffix, else the platform default — mirroring the account-level descriptor resolution) and stamp it onto the Stripe Product's `statement_descriptor`, for products with an `accountId`. No public API change to existing call shapes.
169
197
 
170
- - [#70](https://github.com/kellykampen/better-stripe/pull/70) [`306d402`](https://github.com/kellykampen/better-stripe/commit/306d402b0148e0794a2239a5c83a0d3780300c38) Thanks [@kellykampen](https://github.com/kellykampen)! - Reclaim/expiry of dead reversal claims (BTS-74).
198
+ - [#70](https://github.com/kellykampen/better-stripe/pull/70) [`306d402`](https://github.com/kellykampen/better-stripe/commit/306d402b0148e0794a2239a5c83a0d3780300c38) Thanks [@kellykampen](https://github.com/kellykampen)! - Reclaim/expiry of dead reversal claims.
171
199
 
172
- BTS-63 reserves reversal slices atomically before money moves; the executor now refuses to leapfrog an unexecuted predecessor claim. The residual, by-design failure mode: if the operation that reserved a slice dies permanently (its webhook exhausts Stripe's retries and never confirms), the claim frontier stays ahead of the confirmed `reversedAmount` forever, and every successor reversal on that leg blocks with `predecessor claim unexecuted`. This adds an ops path to resolve such wedged legs.
200
+ Atomic reversal claims reserve reversal slices atomically before money moves; the executor now refuses to leapfrog an unexecuted predecessor claim. The residual, by-design failure mode: if the operation that reserved a slice dies permanently (its webhook exhausts Stripe's retries and never confirms), the claim frontier stays ahead of the confirmed `reversedAmount` forever, and every successor reversal on that leg blocks with `predecessor claim unexecuted`. This adds an ops path to resolve such wedged legs.
173
201
 
174
202
  - **`BetterStripe.listWedgedReversalClaims(ctx)`** — lists legs where `reversalClaimedAmount > reversedAmount` and the recorded ops holding the blocking (unexecuted) slice on each. An ops/admin diagnostic; scans the ledger to completion (exact, and loud past Convex's 16,384-doc read limit rather than silently truncating).
175
203
  - **`BetterStripe.reclaimReversalClaim(ctx, { operationId, mode })`** — resolves a dead claim:
@@ -178,13 +206,13 @@ changed by this release.
178
206
 
179
207
  New component functions: `connect/queries/listWedgedReversalClaims`, `connect/queries/getReversalOp`, `connect/mutations/releaseReversalClaim`. No behavior change to existing reversal paths.
180
208
 
181
- - [#76](https://github.com/kellykampen/better-stripe/pull/76) [`c9f1489`](https://github.com/kellykampen/better-stripe/commit/c9f148948f2f5ea77f343c5fc1a78faffbb5ac1e) Thanks [@kellykampen](https://github.com/kellykampen)! - Add refund actor scoping — seller (own sales) vs platform admin (BTS-35, [#56](https://github.com/kellykampen/better-stripe/issues/56)).
209
+ - [#76](https://github.com/kellykampen/better-stripe/pull/76) [`c9f1489`](https://github.com/kellykampen/better-stripe/commit/c9f148948f2f5ea77f343c5fc1a78faffbb5ac1e) Thanks [@kellykampen](https://github.com/kellykampen)! - Add refund actor scoping — seller (own sales) vs platform admin ([#56](https://github.com/kellykampen/better-stripe/issues/56)).
182
210
 
183
- `createRefund` (BTS-34) previously had no notion of who may initiate a refund. It now takes an optional `actor` option (`{ type: "admin" }` or `{ type: "seller", accountId }`) and enforces the rule before any Stripe call is made — an unauthorized refund never moves money. A platform admin may refund any sale; a seller may refund only sales routed to their own connected account (destination charge, or one of the split legs in a separate-charges split), resolved from the payment's own component-ledger routing. Fails closed: a seller is denied when the payment's routing can't be resolved.
211
+ `createRefund` previously had no notion of who may initiate a refund. It now takes an optional `actor` option (`{ type: "admin" }` or `{ type: "seller", accountId }`) and enforces the rule before any Stripe call is made — an unauthorized refund never moves money. A platform admin may refund any sale; a seller may refund only sales routed to their own connected account (destination charge, or one of the split legs in a separate-charges split), resolved from the payment's own component-ledger routing. Fails closed: a seller is denied when the payment's routing can't be resolved.
184
212
 
185
- Additive and backwards compatible — omitting `actor` preserves the prior unrestricted behavior. The refund fee/transfer-reversal math from BTS-34 is unchanged.
213
+ Additive and backwards compatible — omitting `actor` preserves the prior unrestricted behavior. The refund fee/transfer-reversal math from refund handling is unchanged.
186
214
 
187
- - [#76](https://github.com/kellykampen/better-stripe/pull/76) [`c9f1489`](https://github.com/kellykampen/better-stripe/commit/c9f148948f2f5ea77f343c5fc1a78faffbb5ac1e) Thanks [@kellykampen](https://github.com/kellykampen)! - Refunds now return the platform fee pro-rata and reverse recipient transfers (BTS-34, [#45](https://github.com/kellykampen/better-stripe/issues/45)).
215
+ - [#76](https://github.com/kellykampen/better-stripe/pull/76) [`c9f1489`](https://github.com/kellykampen/better-stripe/commit/c9f148948f2f5ea77f343c5fc1a78faffbb5ac1e) Thanks [@kellykampen](https://github.com/kellykampen)! - Refunds now return the platform fee pro-rata and reverse recipient transfers ([#45](https://github.com/kellykampen/better-stripe/issues/45)).
188
216
 
189
217
  **Destination charges:** `createRefund` gains `refundApplicationFee` and `reverseTransfer` options, both defaulting `true`, sent only when the charge actually carries an application fee / destination transfer (sending either on an incapable charge is a hard Stripe error). Stripe pro-rates both natively for partial refunds; an explicit `false` always wins.
190
218
 
@@ -212,14 +240,14 @@ changed by this release.
212
240
 
213
241
  Note: V2 `customer_account` + `pause_collection`/dispute interplay and the refund-via-API path are covered by unit tests; the live E2E (`refund.created` via real API call, dispute triggers) should be run before release — `charge.dispute.funds_withdrawn`/`funds_reinstated` cannot be reliably triggered in test mode.
214
242
 
215
- - [#76](https://github.com/kellykampen/better-stripe/pull/76) [`c9f1489`](https://github.com/kellykampen/better-stripe/commit/c9f148948f2f5ea77f343c5fc1a78faffbb5ac1e) Thanks [@kellykampen](https://github.com/kellykampen)! - Add `SplitBreakdown` + `EarningsSummary` headless components (BTS-37, [#49](https://github.com/kellykampen/better-stripe/issues/49)).
243
+ - [#76](https://github.com/kellykampen/better-stripe/pull/76) [`c9f1489`](https://github.com/kellykampen/better-stripe/commit/c9f148948f2f5ea77f343c5fc1a78faffbb5ac1e) Thanks [@kellykampen](https://github.com/kellykampen)! - Add `SplitBreakdown` + `EarningsSummary` headless components ([#49](https://github.com/kellykampen/better-stripe/issues/49)).
216
244
 
217
245
  - `SplitBreakdown` renders a sale's store + affiliate + platform lines (the `other` bucket only when it carries money) from `useSplitBreakdown`'s totals. When the platform share is underivable (no sale amount given to the hook) it renders an explicit "Unknown without the sale amount" state — never a silent zero.
218
246
  - `EarningsSummary` renders gross / reversals / net / paid-out plus the latest payout status line. Its props are a subset of `useEarnings`'s result shape, so `<EarningsSummary {...useEarnings(accountId)} />` works directly.
219
247
 
220
- Consumes the BTS-36 hooks ([#44](https://github.com/kellykampen/better-stripe/issues/44)). Both headless/themeable (`className` + `children` render-prop). Purely additive.
248
+ Consumes the earnings hooks hooks ([#44](https://github.com/kellykampen/better-stripe/issues/44)). Both headless/themeable (`className` + `children` render-prop). Purely additive.
221
249
 
222
- - [#76](https://github.com/kellykampen/better-stripe/pull/76) [`c9f1489`](https://github.com/kellykampen/better-stripe/commit/c9f148948f2f5ea77f343c5fc1a78faffbb5ac1e) Thanks [@kellykampen](https://github.com/kellykampen)! - Add per-store statement descriptors on destination charges (BTS-32, [#47](https://github.com/kellykampen/better-stripe/issues/47)).
250
+ - [#76](https://github.com/kellykampen/better-stripe/pull/76) [`c9f1489`](https://github.com/kellykampen/better-stripe/commit/c9f148948f2f5ea77f343c5fc1a78faffbb5ac1e) Thanks [@kellykampen](https://github.com/kellykampen)! - Add per-store statement descriptors on destination charges ([#47](https://github.com/kellykampen/better-stripe/issues/47)).
223
251
 
224
252
  - New `BetterStripe.setAccountStatementDescriptor` method (validates first; `null` clears) writes a `statementDescriptor` field on the component's account record via a new `core/mutations/setStatementDescriptor` mutation.
225
253
  - New constructor option `statementDescriptorSuffix` sets a configurable platform-wide default (validated at construction, same pattern as `platformFee`), used whenever a store account carries no suffix of its own. Resolution order: store's stored suffix → configured default → none.
@@ -242,9 +270,9 @@ changed by this release.
242
270
  - `listTransfersByCharge(ctx, { sourceChargeId, limit? })` — a sale's original split legs (reinstatement rows excluded), the read behind the `useSplitBreakdown` hook / per-sale breakdown UIs.
243
271
  - `listTransfersByAccount(ctx, { destinationAccountId, limit? })` — a recipient's transfer ledger (the earnings view, includes reinstatements), the read behind the `useEarnings` hook.
244
272
 
245
- The underlying component queries already existed, but there was no client-side method to reach them, so an app couldn't wire the `useSplitBreakdown` / `useEarnings` hook factories to real data. These thin read wrappers close that gap (surfaced while building the affiliate-split demo, BTS-43).
273
+ The underlying component queries already existed, but there was no client-side method to reach them, so an app couldn't wire the `useSplitBreakdown` / `useEarnings` hook factories to real data. These thin read wrappers close that gap (surfaced while building the affiliate-split demo).
246
274
 
247
- - [#58](https://github.com/kellykampen/better-stripe/pull/58) [`61f755e`](https://github.com/kellykampen/better-stripe/commit/61f755ebf38065d5c3cc73db18e7dfd1c3d8c394) Thanks [@kellykampen](https://github.com/kellykampen)! - `updateDispute` now stages evidence by default instead of submitting it (BTS-61).
275
+ - [#58](https://github.com/kellykampen/better-stripe/pull/58) [`61f755e`](https://github.com/kellykampen/better-stripe/commit/61f755ebf38065d5c3cc73db18e7dfd1c3d8c394) Thanks [@kellykampen](https://github.com/kellykampen)! - `updateDispute` now stages evidence by default instead of submitting it.
248
276
 
249
277
  Stripe's dispute-update `submit` parameter **defaults to `true`**, so evidence sent without an explicit `submit` is submitted to the bank — a one-shot, outcome-affecting action. The previous docblock incorrectly said omitting `submit` "saves a draft", and the method forwarded `submit` only when explicitly set, so a caller following the docs would submit unintentionally.
250
278
 
@@ -252,52 +280,52 @@ changed by this release.
252
280
 
253
281
  - A bare metadata/status update (no `evidence`) is unchanged — no `submit` is forced.
254
282
  - An explicit `submit: true` / `submit: false` is always honored.
255
- - `EvidenceForm` (BTS-54) already passes `submit` explicitly, so it is unaffected.
283
+ - `EvidenceForm` already passes `submit` explicitly, so it is unaffected.
256
284
 
257
285
  If you relied on the old behavior of submitting evidence by omitting `submit`, pass `submit: true` explicitly.
258
286
 
259
287
  ### Patch Changes
260
288
 
261
- - [#76](https://github.com/kellykampen/better-stripe/pull/76) [`c9f1489`](https://github.com/kellykampen/better-stripe/commit/c9f148948f2f5ea77f343c5fc1a78faffbb5ac1e) Thanks [@kellykampen](https://github.com/kellykampen)! - Regenerate the component's `_generated` API so transfer/dispute queries are typed for consumers (BTS-70, [#71](https://github.com/kellykampen/better-stripe/issues/71)).
289
+ - [#76](https://github.com/kellykampen/better-stripe/pull/76) [`c9f1489`](https://github.com/kellykampen/better-stripe/commit/c9f148948f2f5ea77f343c5fc1a78faffbb5ac1e) Thanks [@kellykampen](https://github.com/kellykampen)! - Regenerate the component's `_generated` API so transfer/dispute queries are typed for consumers ([#71](https://github.com/kellykampen/better-stripe/issues/71)).
262
290
 
263
- `src/component/_generated/component.ts` was stale — it predated `listTransfersByCharge`/`listTransfersByAccount` (BTS-50) and `getDisputeByStripeId`/`listDisputes` (BTS-31), so `components.betterStripe.connect.queries.*` for those methods was untyped for any consumer reaching into the raw component API. Regenerated via `npx convex codegen`; no source logic changed, generated bindings only.
291
+ `src/component/_generated/component.ts` was stale — it predated `listTransfersByCharge`/`listTransfersByAccount` and `getDisputeByStripeId`/`listDisputes`, so `components.betterStripe.connect.queries.*` for those methods was untyped for any consumer reaching into the raw component API. Regenerated via `npx convex codegen`; no source logic changed, generated bindings only.
264
292
 
265
- - [#76](https://github.com/kellykampen/better-stripe/pull/76) [`c9f1489`](https://github.com/kellykampen/better-stripe/commit/c9f148948f2f5ea77f343c5fc1a78faffbb5ac1e) Thanks [@kellykampen](https://github.com/kellykampen)! - Fix: `updateDispute` now sends an explicit `submit: false` on every call, including metadata-only updates (BTS-72, [#67](https://github.com/kellykampen/better-stripe/issues/67)).
293
+ - [#76](https://github.com/kellykampen/better-stripe/pull/76) [`c9f1489`](https://github.com/kellykampen/better-stripe/commit/c9f148948f2f5ea77f343c5fc1a78faffbb5ac1e) Thanks [@kellykampen](https://github.com/kellykampen)! - Fix: `updateDispute` now sends an explicit `submit: false` on every call, including metadata-only updates ([#67](https://github.com/kellykampen/better-stripe/issues/67)).
266
294
 
267
- BTS-61 made evidence-bearing `updateDispute` calls stage by default (`submit: false`), but left metadata-only updates sending no `submit` key at all — inheriting Stripe's server-side default, which Stripe's docs state is `true` on every update. Empirically, a metadata-only update does not submit previously staged evidence today, but that leniency contradicts the documented default and submission is one-shot and outcome-affecting. `updateDispute` now never omits `submit`: every call — evidence, metadata, or both — sends an explicit `submit: false` unless the caller passes `submit: true` to finalize.
295
+ Earlier evidence handling made evidence-bearing `updateDispute` calls stage by default (`submit: false`), but left metadata-only updates sending no `submit` key at all — inheriting Stripe's server-side default, which Stripe's docs state is `true` on every update. Empirically, a metadata-only update does not submit previously staged evidence today, but that leniency contradicts the documented default and submission is one-shot and outcome-affecting. `updateDispute` now never omits `submit`: every call — evidence, metadata, or both — sends an explicit `submit: false` unless the caller passes `submit: true` to finalize.
268
296
 
269
297
  No public API change; the safer default only ever narrows behavior (no call that previously submitted now stages, and vice versa).
270
298
 
271
- - [#68](https://github.com/kellykampen/better-stripe/pull/68) [`4d1b843`](https://github.com/kellykampen/better-stripe/commit/4d1b843cb9891a313c9b9a270ca067e36c686420) Thanks [@kellykampen](https://github.com/kellykampen)! - Fix a first-invoice fee double-reversal crack (BTS-69).
299
+ - [#68](https://github.com/kellykampen/better-stripe/pull/68) [`4d1b843`](https://github.com/kellykampen/better-stripe/commit/4d1b843cb9891a313c9b9a270ca067e36c686420) Thanks [@kellykampen](https://github.com/kellykampen)! - Fix a first-invoice fee double-reversal crack.
272
300
 
273
301
  `applyFirstInvoiceFee` recomputed the platform fee from **live** subscription metadata (`bsFeeConfig`) on every webhook retry. If the transfer reversal succeeded but the `bsFeeCollected` invoice-marker write then failed (event marked `failed`), and the platform edited `bsFeeConfig` before Stripe redelivered, a retry landing **after** Stripe's ≤24h idempotency-key window would recompute a _different_ fee and create a **second** reversal — double-collecting the fee.
274
302
 
275
- **Fix:** before reversing, `applyFirstInvoiceFee` now lists the destination transfer's existing reversals and adopts one tagged `metadata.bsFeeFor:<invoiceId>` as the idempotency source of truth. That tag survives both the marker-write failure and the idempotency-key TTL, so a retry re-records the marker with the already-reversed amount instead of reversing again — mirroring BTS-60's frozen-config guarantee. No API surface change.
303
+ **Fix:** before reversing, `applyFirstInvoiceFee` now lists the destination transfer's existing reversals and adopts one tagged `metadata.bsFeeFor:<invoiceId>` as the idempotency source of truth. That tag survives both the marker-write failure and the idempotency-key TTL, so a retry re-records the marker with the already-reversed amount instead of reversing again — mirroring the per-charge fee collector's frozen-config guarantee. No API surface change.
276
304
 
277
- - [#76](https://github.com/kellykampen/better-stripe/pull/76) [`c9f1489`](https://github.com/kellykampen/better-stripe/commit/c9f148948f2f5ea77f343c5fc1a78faffbb5ac1e) Thanks [@kellykampen](https://github.com/kellykampen)! - Fix: collect the fixed/tier platform fee on a subscription's first invoice (BTS-68, [#52](https://github.com/kellykampen/better-stripe/issues/52)).
305
+ - [#76](https://github.com/kellykampen/better-stripe/pull/76) [`c9f1489`](https://github.com/kellykampen/better-stripe/commit/c9f148948f2f5ea77f343c5fc1a78faffbb5ac1e) Thanks [@kellykampen](https://github.com/kellykampen)! - Fix: collect the fixed/tier platform fee on a subscription's first invoice ([#52](https://github.com/kellykampen/better-stripe/issues/52)).
278
306
 
279
- `applyPerInvoiceFee` (BTS-51) applies a fixed/tier `application_fee_amount` at `invoice.created`, which only works while the invoice is still a draft. Stripe finalizes a `charge_automatically` subscription's first invoice synchronously at creation (and Checkout subscription-mode pays it during checkout), so `invoice.created` fires with an already-`open` invoice — the update is silently rejected and the first cycle's platform fee was lost on every destination-charge subscription with a fixed/tier fee. Percent-only fees and renewal invoices (which get Stripe's ~1h draft window) were unaffected.
307
+ `applyPerInvoiceFee` applies a fixed/tier `application_fee_amount` at `invoice.created`, which only works while the invoice is still a draft. Stripe finalizes a `charge_automatically` subscription's first invoice synchronously at creation (and Checkout subscription-mode pays it during checkout), so `invoice.created` fires with an already-`open` invoice — the update is silently rejected and the first cycle's platform fee was lost on every destination-charge subscription with a fixed/tier fee. Percent-only fees and renewal invoices (which get Stripe's ~1h draft window) were unaffected.
280
308
 
281
- Fixed with a new `applyFirstInvoiceFee` processor hooked at `invoice.paid`: it collects the missed fee via a partial reversal of the automatic destination transfer, sized from `amount_paid` — the same mechanism BTS-60's `applyPerChargeFee` uses for one-time charges. Skips any invoice whose fee the draft path already collected, so there's no double-collection. No public API change.
309
+ Fixed with a new `applyFirstInvoiceFee` processor hooked at `invoice.paid`: it collects the missed fee via a partial reversal of the automatic destination transfer, sized from `amount_paid` — the same mechanism the per-charge fee collector's `applyPerChargeFee` uses for one-time charges. Skips any invoice whose fee the draft path already collected, so there's no double-collection. No public API change.
282
310
 
283
- - [#21](https://github.com/kellykampen/better-stripe/pull/21) [`1f43345`](https://github.com/kellykampen/better-stripe/commit/1f433459ec6eb11d75982f1ed883cff9414d4e15) Thanks [@kellykampen](https://github.com/kellykampen)! - Fix `createPrice` orphaning a Stripe price when the product is missing from the component DB (BTS-2).
311
+ - [#21](https://github.com/kellykampen/better-stripe/pull/21) [`1f43345`](https://github.com/kellykampen/better-stripe/commit/1f433459ec6eb11d75982f1ed883cff9414d4e15) Thanks [@kellykampen](https://github.com/kellykampen)! - Fix `createPrice` orphaning a Stripe price when the product is missing from the component DB.
284
312
 
285
313
  `createPrice` previously called `stripe.prices.create(...)` **before** verifying the product existed in the component DB, then threw `PRODUCT_NOT_FOUND` afterward — leaving an orphaned price in Stripe with no corresponding component record. The product lookup now runs **first**, so a missing/unsynced product throws before any Stripe write happens. No API change; behavior only differs on the error path.
286
314
 
287
- - [#76](https://github.com/kellykampen/better-stripe/pull/76) [`c9f1489`](https://github.com/kellykampen/better-stripe/commit/c9f148948f2f5ea77f343c5fc1a78faffbb5ac1e) Thanks [@kellykampen](https://github.com/kellykampen)! - Fix: collect deferred `per_charge` platform fees via transfer reversal (BTS-60, [#38](https://github.com/kellykampen/better-stripe/issues/38)).
315
+ - [#76](https://github.com/kellykampen/better-stripe/pull/76) [`c9f1489`](https://github.com/kellykampen/better-stripe/commit/c9f148948f2f5ea77f343c5fc1a78faffbb5ac1e) Thanks [@kellykampen](https://github.com/kellykampen)! - Fix: collect deferred `per_charge` platform fees via transfer reversal ([#38](https://github.com/kellykampen/better-stripe/issues/38)).
288
316
 
289
317
  `bsFeeMode="per_charge"` (used when a one-time destination charge's final amount isn't known up front — discounts, `custom_unit_amount` prices, or a `sessionOverrides`-replaced `line_items`) had no webhook consumer, so the fixed/tiered platform fee was silently never collected on those charges.
290
318
 
291
319
  Fixed with a new `applyPerChargeFee` processor at `payment_intent.succeeded`: it collects the fee the same way Stripe itself settles application fees on destination charges — a partial reversal of the automatic destination transfer, sized by `computeFee` from the amount actually charged (`amount_received`). No public API change; charges that already pass `amount` up front (computing `application_fee_amount` at session creation) are unaffected.
292
320
 
293
- - [#66](https://github.com/kellykampen/better-stripe/pull/66) [`a47d64d`](https://github.com/kellykampen/better-stripe/commit/a47d64d39ece42b4cc0d96508e9b87925c6bf8e9) Thanks [@kellykampen](https://github.com/kellykampen)! - Fix `addRecipientConfiguration` not persisting `appliedConfigurations` to the component DB (BTS-71).
321
+ - [#66](https://github.com/kellykampen/better-stripe/pull/66) [`a47d64d`](https://github.com/kellykampen/better-stripe/commit/a47d64d39ece42b4cc0d96508e9b87925c6bf8e9) Thanks [@kellykampen](https://github.com/kellykampen)! - Fix `addRecipientConfiguration` not persisting `appliedConfigurations` to the component DB.
294
322
 
295
323
  `addRecipientConfiguration` applied the V2 recipient configuration on Stripe but never wrote the result back to the component `accounts` row, unlike `addCustomerConfiguration` which re-fetches and records `applied_configurations` after every update. Callers had to manually run `syncAllAccounts` afterward to reconcile the component DB, leaving it stale in between — anything reading `appliedConfigurations` off the component row (e.g. billing-view/UI gating) saw an out-of-date picture. `addRecipientConfiguration` now follows the same pattern: re-fetch the account after the Stripe update, read `applied_configurations`, recover the owning `userId` from the existing component record (failing closed with `ACCOUNT_NOT_FOUND` if the account isn't in the component DB yet), and `upsertAccount`. No public API change on the call side; the return shape now includes `appliedConfigurations` alongside `success`, matching `addCustomerConfiguration`.
296
324
 
297
- - [#76](https://github.com/kellykampen/better-stripe/pull/76) [`c9f1489`](https://github.com/kellykampen/better-stripe/commit/c9f148948f2f5ea77f343c5fc1a78faffbb5ac1e) Thanks [@kellykampen](https://github.com/kellykampen)! - Fix three transfer-reversal money races via atomic reversal-slice claiming (BTS-63, [#61](https://github.com/kellykampen/better-stripe/issues/61)).
325
+ - [#76](https://github.com/kellykampen/better-stripe/pull/76) [`c9f1489`](https://github.com/kellykampen/better-stripe/commit/c9f148948f2f5ea77f343c5fc1a78faffbb5ac1e) Thanks [@kellykampen](https://github.com/kellykampen)! - Fix three transfer-reversal money races via atomic reversal-slice claiming ([#61](https://github.com/kellykampen/better-stripe/issues/61)).
298
326
 
299
327
  - Dispute clawback (amount mode): a retry after partial success recomputed per-leg amounts from mutable ledger state — a same-params replay could double-record the ledger, or a shifted remainder could resend different params under an already-used Stripe idempotency key, causing a permanent event-retry loop.
300
- - Refund clawback (delta-to-target, BTS-34): two distinct refunds racing between the ledger read and the Stripe call could both compute the same reversal target from stale state, over-reversing a leg and under-recording the ledger.
328
+ - Refund clawback (delta-to-target): two distinct refunds racing between the ledger read and the Stripe call could both compute the same reversal target from stale state, over-reversing a leg and under-recording the ledger.
301
329
  - `payment_intent.succeeded` redelivery could have `upsertPayment`'s blanket patch rewrite `feeCollectedAmount` back to the gross fee, bypassing the monotonic fee-refund guard.
302
330
 
303
331
  Fixed by claiming the exact reversal slice atomically, in a new component mutation, before any money moves — so a retry re-adopts the already-claimed slice instead of recomputing and re-reversing it. No public API change.
package/README.md CHANGED
@@ -6,7 +6,7 @@ Built for Convex + Next.js applications. Follows the conventions established by
6
6
 
7
7
  ## Status
8
8
 
9
- **Version**: `0.6.0` <!-- x-release-please-version --> (pre-1.0). API may change between minor versions until 1.0.
9
+ **Version**: `0.7.0` <!-- x-release-please-version --> (pre-1.0). API may change between minor versions until 1.0.
10
10
 
11
11
  Used in production by its authors. Webhook pipeline is covered by unit tests and a live E2E harness (`npm run e2e:webhooks`) that fires real Stripe-signed events and asserts the ledger.
12
12
 
@@ -35,6 +35,14 @@ and `pnpm dev` to start the example at http://localhost:5173. The example needs
35
35
  Node.js 22.12+ and a configured Convex development deployment; see
36
36
  [the example setup guide](example/README.md).
37
37
 
38
+ The BetterStripe example separates visitor, customer, affiliate, seller, and
39
+ platform-admin workflows. Its resumable Stripe sandbox fixtures populate sales,
40
+ referrals, refunds, disputes, subscriptions, and payouts. Seller plans include
41
+ monthly billing, actual Stripe fee reconciliation, and refund/dispute cost
42
+ recovery. The example also demonstrates optional balance-funded subscriptions
43
+ using Stripe's Accounts v2 preview. See the setup guide for consent requirements,
44
+ renewal tests, and payment-recovery limitations.
45
+
38
46
  Builds and typechecks use TypeScript 7 (`@typescript/native` aliases
39
47
  `typescript`). ESLint and Convex code generation still need the TypeScript 6
40
48
  API, so `typescript` aliases Microsoft's `@typescript/typescript6` compatibility
@@ -61,11 +69,11 @@ pnpm codegen:example # regen example/convex/_generated only
61
69
 
62
70
  All three require a configured dev deployment (`CONVEX_DEPLOYMENT` env var, normally set by a prior `npx convex dev`). They write only the local `_generated/` files — they don't push code or data to the deployment.
63
71
 
64
- **CI gate (BTS-104 / BTS-111 / BTS-112).** Two layers verify the committed files are not stale:
72
+ **CI gate.** Two layers verify the committed files are not stale:
65
73
 
66
74
  1. **Structural drift check** (`pnpm codegen:check`, always runs in CI): two independent scans, each checking both directions (every source export appears in the generated file, and every generated entry still has a matching source), neither needing a deployment:
67
- - Library component — scans `src/component/**/*.{queries,mutations,actions}.ts` and verifies every exported function appears in `src/component/_generated/component.ts`. This is the [BTS-31](https://linear.app/dojoco/issue/BTS-31)/[BTS-50](https://linear.app/dojoco/issue/BTS-50) and [BTS-74](https://linear.app/dojoco/issue/BTS-74) incident class. The reverse direction also flags a stale entry left behind by a *deleted* query/mutation/action (BTS-112). The export-detection regex intentionally matches only direct `query(...)`/`mutation(...)`/`action(...)` exports — not wrapper or re-export forms like `customQuery(...)` (convex-helpers) or `export { fn as name }`, which this component has none of today; widen the regex (and the reverse scan) rather than assuming that stays true.
68
- - Example app — scans `example/convex/**/*.ts` and verifies every source module is imported and registered in `example/convex/_generated/api.ts`. Catches the common case of adding a new `example/convex/<module>.ts` (or nested file like `example/convex/lib/<module>.ts`) without regenerating (BTS-111). The reverse direction likewise flags a stale import left behind by a *deleted* module (BTS-112).
75
+ - Library component — scans `src/component/**/*.{queries,mutations,actions}.ts` and verifies every exported function appears in `src/component/_generated/component.ts`. The reverse direction also flags a stale entry left behind by a *deleted* query/mutation/action. The export-detection regex intentionally matches only direct `query(...)`/`mutation(...)`/`action(...)` exports — not wrapper or re-export forms like `customQuery(...)` (convex-helpers) or `export { fn as name }`, which this component has none of today; widen the regex (and the reverse scan) rather than assuming that stays true.
76
+ - Example app — scans `example/convex/**/*.ts` and verifies every source module is imported and registered in `example/convex/_generated/api.ts`. Catches the common case of adding a new `example/convex/<module>.ts` (or nested file like `example/convex/lib/<module>.ts`) without regenerating. The reverse direction likewise flags a stale import left behind by a *deleted* module.
69
77
  2. **Authoritative regen-and-diff** (runs in CI when `CONVEX_DEPLOYMENT` is configured): runs `pnpm codegen` against the configured dev deployment and fails if `git diff` shows changes. Catches every drift case, including validator-shape changes the structural check can't see. To enable on a fork, set the `CONVEX_DEPLOYMENT` repo secret to a dev deployment name and authorize the CI runner — see Convex's [GitHub Actions guide](https://docs.convex.dev/production/integration/deployments#github-actions).
70
78
 
71
79
  ### Relation to `@convex-dev/stripe`
@@ -96,12 +104,12 @@ See [Roadmap / Known Gaps](#roadmap--known-gaps) for what's still in flight.
96
104
 
97
105
  ## Roadmap / Known Gaps
98
106
 
99
- The marketplace economics layer described above — platform fees, the split engine, rolling payouts, dispute clawback/reinstate, refund fee/transfer reversal, and per-store statement descriptors — is **implemented and merged**, tracked as issues on the `BTS` team in Linear. This section lists what's honestly still open, by ticket, rather than letting the feature list overstate the current state:
107
+ The marketplace economics layer includes platform fees, split transfers, rolling payouts, dispute recovery, refund reversals, and per-store statement descriptors. The former roadmap gaps are covered as follows:
100
108
 
101
- - **[BTS-67](https://linear.app/dojoco/issue/BTS-67)** — the first charge of every destination-charge subscription carries no per-store statement descriptor. Direct `charge_automatically` subscriptions (and Checkout subscription mode) finalize and pay their first invoice synchronously at creation, before the `invoice.created` handler that sets the descriptor can run. Later invoices are unaffected. A product-level `statement_descriptor` fix (which would cover the first charge too) is proposed but not built.
102
- - **[BTS-68](https://linear.app/dojoco/issue/BTS-68)** — investigating whether the fixed/tiered per-invoice platform fee has the same first-invoice gap as BTS-67 (same `invoice.created` timing), which would be a fee-revenue miss rather than a cosmetic one. Not yet confirmed or fixed.
103
- - **[BTS-63](https://linear.app/dojoco/issue/BTS-63)** — dispute clawback (`reverseTransfers`) retrying after a partial success can hit Stripe idempotency-key conflicts on the legs that already succeeded.
104
- - **[BTS-64](https://linear.app/dojoco/issue/BTS-64)** — the earnings/split ledger queries backing `useEarnings` and `useSplitBreakdown` (`listTransfersByAccount`, `listTransfersByCharge`) silently cap at 50 rows, which can understate gross/net totals for high-volume accounts or heavily-split sales.
109
+ - **First subscription charge descriptor** — `createProduct` and `updateProduct` stamp the store's descriptor (or platform fallback) onto store-owned Stripe products. The product descriptor covers the first invoice for direct subscriptions and Checkout subscription mode; the invoice-time descriptor still takes precedence on later invoices. Existing products need `updateProduct` to receive the descriptor, and a later store descriptor change requires refreshing its products. Shared products have one descriptor and should not be reused across stores that need different descriptors.
110
+ - **First-invoice fees** — finalized first invoices bypass the draft update window. At `invoice.paid`, the processor collects the fixed/tiered fee through a partial reversal of the destination transfer. Invoice markers and invoice-tagged reversals prevent duplicate collection, including recovery after a failed marker write. Renewals whose fees were set on the draft invoice are skipped.
111
+ - **Dispute reversal retries** — reversal slices are claimed atomically before any Stripe call. Partial-success retries reuse the recorded amounts and idempotency keys, skip confirmed legs, and reconcile money already moved before a ledger-write failure.
112
+ - **Earnings and split totals** — `useEarnings` reads exact server-side aggregates with bounded drill-down previews. `listTransfersByAccount` remains a preview API; do not sum it for financial totals. `listTransfersByCharge` now returns every original split leg by default, and reversal claims cover all of those legs. An explicit list limit requests a preview. Convex read/write/response limits fail loudly rather than silently truncate financial data.
105
113
 
106
114
  ## Installation
107
115
 
@@ -598,7 +606,7 @@ const { feeAmount, percentApplied, fixedApplied, tier } = computeFee(2999, {
598
606
 
599
607
  - **Percent-only** fees (`isPercentOnlyFee(config)` — no `fixed`, no `tiers`) map directly to Stripe's `application_fee_percent` on the subscription/PaymentIntent — no per-invoice computation needed.
600
608
  - **Fixed or tiered** fees can't be expressed as a flat Stripe percentage, so they're computed and applied per invoice (`applyPerInvoiceFee`, at `invoice.created`) or per one-time charge (`applyPerChargeFee`) in the webhook processor, via `application_fee_amount`.
601
- - This per-invoice path shares the same first-invoice timing gap as statement descriptors — see [BTS-68](#roadmap--known-gaps).
609
+ - For synchronously finalized first invoices, `invoice.paid` collects the missed fixed/tiered fee through a partial destination-transfer reversal. See [first-invoice fee handling](#roadmap--known-gaps).
602
610
 
603
611
  Fees only apply to **destination charges** (`destinationAccountId` set, or a single-recipient `split`). The **separate-charges-and-transfers** path (multi-recipient `split`) never uses `application_fee` — the platform's cut is realized by transferring less than was charged (see Split Engine below).
604
612
 
@@ -690,7 +698,7 @@ await stripe.setAccountStatementDescriptor(ctx, {
690
698
  });
691
699
  ```
692
700
 
693
- Validated against Stripe's rules (`validateStatementDescriptorSuffix`: 1–22 chars, at least one Latin letter, no `< > \ ' " *`, no non-Latin — rejects rather than truncates) before writing. Resolution order per charge: the store's stored suffix → the platform's configured `statementDescriptorSuffix` default (constructor option) → none. One-time payments set the suffix directly on the PaymentIntent at checkout; subscriptions set it on each invoice via the webhook processor — see [BTS-67](#roadmap--known-gaps) for the one gap in that mechanism (the first subscription charge).
701
+ Validated against Stripe's rules (`validateStatementDescriptorSuffix`: 1–22 chars, at least one Latin letter, no `< > \ ' " *`, no non-Latin — rejects rather than truncates) before writing. Resolution order per charge: the store's stored suffix → the platform's configured `statementDescriptorSuffix` default (constructor option) → none. One-time payments set the suffix directly on the PaymentIntent at checkout. Store-owned products carry the descriptor for subscriptions' first charges; the webhook processor sets it on later invoices. Refresh existing products with `updateProduct` after changing a store's descriptor.
694
702
 
695
703
  ## React Hooks
696
704
 
@@ -711,8 +719,8 @@ Read hooks are exported as factory functions that take the app's `useQuery` bind
711
719
  | `createUsePaymentMethods` | `(accountId?)` | `{ methods, defaultMethod, isLoading }` |
712
720
  | `createUseInvoices` | `({ userId?, subscriptionId?, status? }?)` | `{ invoices, isLoading }` |
713
721
  | `createUseAccountOnboarding` | `(accountId?)` | `{ account, status, isReady, missingRequirements, isLoading }` |
714
- | `createUseEarnings` | `(accountId?)` | `{ gross, reversed, net, payouts, paidOut, transfers, isLoading }` — recipient balance/earnings (BTS-36) |
715
- | `createUseSplitBreakdown` | `({ sourceChargeId?, saleAmount? })` | `{ breakdown: { legs, store, affiliate, other, recipientsTotal, platform? } \| null, isLoading }` — per-sale split (BTS-36) |
722
+ | `createUseEarnings` | `(accountId?)` | `{ gross, reversed, net, payouts, paidOut, transfers, isLoading }` — recipient balance/earnings |
723
+ | `createUseSplitBreakdown` | `({ sourceChargeId?, saleAmount? })` | `{ breakdown: { legs, store, affiliate, other, recipientsTotal, platform? } \| null, isLoading }` — per-sale split |
716
724
  | `createUseDisputes` | `({ accountId?, status? }?)` | `{ disputes, isLoading }` |
717
725
  | `createUseDisputeWithCountdown` | `(stripeDisputeId?)` | `{ dispute, countdown: { dueBy?, daysRemaining?, isOverdue } \| null, isLoading }` |
718
726
 
@@ -919,7 +927,7 @@ views and responds to their own disputes via Stripe's embedded `disputes_list`
919
927
  **Dependencies:** the wrapper uses [`@stripe/react-connect-js`](https://www.npmjs.com/package/@stripe/react-connect-js) and [`@stripe/connect-js`](https://www.npmjs.com/package/@stripe/connect-js), which ship as regular dependencies of this package (same convention as `@stripe/react-stripe-js` / `@stripe/stripe-js`) -- no extra install step. ConnectJS components are client-only iframes; render them in client components.
920
928
 
921
929
  ```tsx
922
- // convex/stripe.ts — expose the account session (server side, BTS-30)
930
+ // convex/stripe.ts — expose the account session (server side)
923
931
  export const createDisputeSession = action({
924
932
  args: { stripeAccountId: v.string() },
925
933
  handler: async (ctx, args) => stripe.createDisputeSession(ctx, args),
@@ -71,7 +71,7 @@ export async function processEvent(whCtx, event) {
71
71
  case "payout.updated":
72
72
  case "payout.paid":
73
73
  case "payout.failed":
74
- await handlePayoutEvent(whCtx, obj);
74
+ await handlePayoutEvent(whCtx, obj, event.account);
75
75
  break;
76
76
  case "refund.created":
77
77
  case "refund.updated":
@@ -741,12 +741,16 @@ async function upsertPaymentFromStripe(whCtx, paymentIntent, perChargeFee) {
741
741
  metadata: paymentIntent.metadata ?? undefined,
742
742
  });
743
743
  }
744
- async function handlePayoutEvent(whCtx, payout) {
744
+ async function handlePayoutEvent(whCtx, payout, connectedAccountId) {
745
745
  await dispatchUpsert(whCtx, "payoutUpserted", {
746
746
  stripePayoutId: payout.id,
747
- accountId: typeof payout.destination === "string"
748
- ? payout.destination
749
- : (payout.destination?.id ?? ""),
747
+ accountId: connectedAccountId ??
748
+ (payout.metadata?.accountId?.startsWith("acct_")
749
+ ? payout.metadata.accountId
750
+ : undefined) ??
751
+ (typeof payout.destination === "string"
752
+ ? payout.destination
753
+ : (payout.destination?.id ?? "")),
750
754
  amount: payout.amount,
751
755
  currency: payout.currency,
752
756
  status: payout.status,