Skip to main content

max / makenotwork

docs: handoff doc for async-stripe rc.5 migration State, findings, remaining phases, the stuck testaccount123 event to resend post-deploy, and pre-invite gates.
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-16 23:19 UTC
Commit: 7effb4098f8c5c827f2c2ae17fb344e084971044
Parent: 12a3512
1 file changed, +195 insertions, -0 deletions
@@ -0,0 +1,195 @@
1 + # async-stripe 0.37 → 1.0.0-rc.5 migration
2 +
3 + Status as of 2026-05-16: Phases 0–2 complete and committed on branch
4 + `worktree-stripe-sdk-1.0-rc.5` at `MNW/.claude/worktrees/stripe-sdk-1.0-rc.5/`.
5 +
6 + ## Why this exists
7 +
8 + Stripe deprecated all API versions that async-stripe 0.37 understands. Webhook
9 + payloads (api_version `2026-01-28.clover`) fail to deserialize because:
10 +
11 + - `Subscription.current_period_{start,end}` moved to `items.data[0]`
12 + - `Invoice.subscription` moved to `parent.subscription_details.subscription`
13 + - `InvoiceLineItem.proration` was removed (was required in 0.37's struct)
14 + - Stripe Dashboard no longer lets endpoints pin to old API versions
15 +
16 + Every paid transaction since 2026-05-12 is stuck `status='pending'` in our DB
17 + because the `checkout.session.completed` webhook never parses.
18 +
19 + There is one real stuck transaction: testaccount123's $5 PWYW for "Audiofiles
20 + Desktop App", session `cs_live_a1o3Ky7bRCXbnKNUYrGFS1JSmBFYLCxQ8zEk6gtmYfCPsyGAiJJfLNFwGm`,
21 + event `evt_1TXpkh0AcRNJbwd4J9O7c5Up` on max's connected account
22 + `acct_1T8oxK0AcRNJbwd4`. The charge succeeded at Stripe; the library entry
23 + is missing on our side. After this migration deploys, resend that event.
24 +
25 + ## Key findings that shape the migration
26 +
27 + 1. **rc.5 splits the SDK across per-domain crates.** The umbrella `async-stripe`
28 + provides only the HTTP client. Resource types live in `async-stripe-shared`,
29 + `async-stripe-billing`, `async-stripe-checkout`, `async-stripe-connect`,
30 + `async-stripe-core`, `async-stripe-payment`, `async-stripe-product`,
31 + `async-stripe-types`. Crate names in code are `stripe_shared`, `stripe_billing`,
32 + etc. (package name uses dashes, library name uses underscores).
33 +
34 + 2. **`Deserialize` is feature-gated** behind `feature = "deserialize"` on each
35 + resource crate. Cargo.toml already enables this on every sub-crate we depend
36 + on. Without it, `serde_json::from_value::<Subscription>(...)` won't compile.
37 +
38 + 3. **rc.5 has NO built-in webhook helper.** No `Webhook::construct_event`, no
39 + typed `Event`/`EventObject`/`EventType`. We keep our existing
40 + `payments::webhooks::verify_signature` HMAC implementation. For the event
41 + envelope, write a thin local struct (id, type as String, data.object as
42 + `serde_json::Value`) — rc.5's `stripe_shared::Event` requires
43 + `previous_attributes` which isn't present on `*.completed` events, so the
44 + SDK's Event struct is unusable for inbound webhooks.
45 +
46 + 4. **Connect account header is per-request, not per-client.** In 0.37 we did
47 + `client.clone().with_stripe_account(...)`. In rc.5 the pattern is to pass
48 + `Stripe-Account` as a request header on each call. Check the rc.5 docs for
49 + the exact pattern (`RequestStrategy` / per-call headers).
50 +
51 + 5. **Currency moved to `stripe_types::Currency`.**
52 +
53 + 6. **Connect events** may need a separate Stripe dashboard subscription. The
54 + testaccount123 stuck event is a Connect event (`evt_1TXpkh0...0AcRNJbwd4...`).
55 + The May 12 events that did process were platform-level. Confirm Connect
56 + event subscription on the `mnw-alpha` endpoint as part of A2.7.
57 +
58 + 7. **Smoke test proved rc.5 parses our fixtures.** See `/tmp/stripe-rc5-parse/`
59 + (scratch crate, can be regenerated; main.rs in that crate is the working
60 + reference for how to deserialize each event payload).
61 +
62 + ## What's already done (committed on this branch)
63 +
64 + ```
65 + 12a3512 build(server): swap async-stripe 0.37.3 → 1.0.0-rc.5 sub-crates ← intentionally non-compiling
66 + d107447 test: capture 2026-01-28 Stripe webhook fixtures
67 + ```
68 +
69 + - `server/Cargo.toml`: rc.5 sub-crates added with `deserialize` feature
70 + - `server/tests/fixtures/webhooks/*.json`: 7 fixtures captured live via Stripe API
71 + including the testaccount123 stuck Connect event
72 +
73 + ## Remaining phases
74 +
75 + ### Phase 3a — Migrate `server/src/payments/` (~5 files)
76 +
77 + Order matters because later modules import from earlier:
78 +
79 + 1. `payments/mod.rs` — `use stripe::Client` stays; remove `pub use webhooks::*`
80 + exports of stripe-typed extractors that no longer exist; the `PaymentProvider`
81 + trait's `verify_webhook` return type changes from `stripe::Event` to our new
82 + `UntypedEvent` struct.
83 + 2. `payments/webhooks.rs` — biggest rewrite. Replace SDK `Webhook::construct_event`
84 + with raw signature verify (existing `verify_signature` fn) + manual
85 + `serde_json::from_str` to a local `UntypedEvent { id, type_, data_object }`.
86 + Replace all `extract_*` functions to take `&UntypedEvent` and return the rc.5
87 + typed objects (`stripe_billing::Subscription`, `stripe_checkout::CheckoutSession`,
88 + etc.) parsed via `serde_json::from_value(&event.data_object)`. Keep
89 + `AccountUpdate` and `ChargeRefundData` view structs as-is.
90 + 3. `payments/checkout_metadata.rs` — the `is_*_checkout` guards take
91 + `&CheckoutSession`. Just swap the import to `stripe_checkout::CheckoutSession`.
92 + Field access (`session.metadata`, `session.mode`, etc.) is mostly identical.
93 + 4. `payments/checkout.rs` — Request struct shapes changed. `CreateCheckoutSession`,
94 + `CreateCheckoutSessionLineItems`, etc. now live in `stripe_checkout` and have
95 + builder-style construction. Reference rc.5 docs and examples in
96 + `~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-stripe-checkout-1.0.0-rc.5/examples/`
97 + if any.
98 + 5. `payments/connect.rs` — `CreateAccount`, `CreateAccountLink`, `Balance` — all
99 + moved to `stripe_connect` / `stripe_shared`. `with_stripe_account` is gone;
100 + pass the account header per-request.
101 +
102 + ### Phase 3b — `server/src/routes/stripe/` checkout handlers
103 +
104 + `routes/stripe/checkout/{item,project,subscriptions,cart}.rs` and
105 + `routes/stripe/connect.rs`. Each constructs `CreateCheckoutSession` params and
106 + calls into `payments::checkout`. Update imports + builder syntax.
107 +
108 + ### Phase 3c — `server/src/routes/stripe/webhook/` (the bug-fix path)
109 +
110 + `routes/stripe/webhook/mod.rs` dispatcher: change `event.type_` (enum) to
111 + `event.type_` (String) match. Each handler in `webhook/{checkout,subscriptions,billing}.rs`
112 + takes a typed rc.5 object now. The subscription/invoice handlers must read
113 + `current_period_*` from `items.data[0]` / `parent.subscription_details.subscription`
114 + instead of top-level — the new rc.5 structs already model this correctly, so
115 + field paths just change.
116 +
117 + ### Phase 3d — `server/src/scheduler/webhooks.rs` and any remaining `stripe::` refs
118 +
119 + `scheduler/webhooks.rs` is the retry queue worker; it reparses stored payloads.
120 + Update to use the same `UntypedEvent` flow.
121 +
122 + Grep for stragglers: `grep -rln "stripe::" server/src/ | grep -v test`.
123 +
124 + ### Phase 4 — Local smoke test
125 +
126 + ```bash
127 + cd server
128 + cargo check # must be green
129 + cargo test
130 + # In one terminal:
131 + stripe listen --forward-to localhost:3000/stripe/webhook
132 + # In another:
133 + cargo run
134 + # Trigger each event type:
135 + stripe trigger checkout.session.completed
136 + stripe trigger customer.subscription.updated
137 + stripe trigger invoice.payment_succeeded
138 + stripe trigger account.updated
139 + ```
140 +
141 + Confirm `received webhook event` logs for each, no parse errors. Walk a real
142 + PWYW checkout in browser at localhost using test card `4242…`.
143 +
144 + ### Phase 5 — Deploy
145 +
146 + 1. Patch-bump `server/Cargo.toml` version (per `MNW/CLAUDE.md` rule, ask user
147 + for version number before bumping).
148 + 2. Merge `worktree-stripe-sdk-1.0-rc.5` to main via fast-forward or PR-style
149 + merge (no GitHub).
150 + 3. `cd server && ./deploy/deploy.sh root@100.120.174.96`.
151 + 4. Watch `journalctl -u makenotwork -f` for `received webhook event` lines.
152 + 5. In Stripe Dashboard, find event `evt_1TXpkh0AcRNJbwd4J9O7c5Up` on max's
153 + connected account, click "Resend". Verify the transaction in DB flips
154 + from `pending` to `completed` and testaccount123 sees the item in their
155 + library.
156 + 6. Trigger one fresh test purchase end-to-end as the final smoke.
157 +
158 + ### Pre-invite gates (separate, post-deploy)
159 +
160 + - **A2.6**: Add an integration test against Stripe test mode that exercises
161 + every outgoing API call (create session × 3 modes, create subscription,
162 + fetch balance, fetch account, fetch subscription, refund). Run in CI.
163 + Catches future schema breaks before prod sees them.
164 + - **A2.7**: In Stripe Dashboard, confirm `mnw-alpha` (or the new endpoint we
165 + ended up with) is subscribed to **Connect events on connected accounts**,
166 + not just platform events. Connect events drive every creator transaction;
167 + if this checkbox is off, deployment success doesn't translate to working
168 + purchases.
169 +
170 + ## State of related infrastructure
171 +
172 + - Webhook signing secret was rotated to `whsec_pNXOKYm9P5bFuVwsbJPxZYw1OOy1cGsu`
173 + on prod at `/opt/makenotwork/.env` (backup at
174 + `/opt/makenotwork/.env.bak-<timestamp>`). Service was restarted. Signature
175 + verification now succeeds; parsing fails, which is what the rest of this
176 + migration fixes.
177 + - Old endpoint `mnw-alpha` is still active in Stripe Dashboard. Decision
178 + pending: keep it on the rotated secret or create a fresh endpoint pinned to
179 + the rc.5-supported API version. Right now we plan to keep the existing
180 + endpoint since rc.5 handles the current API version natively.
181 +
182 + ## Other bugs filed during this session (for reference, not migration scope)
183 +
184 + In `server/docs/todo.md` § Global UX:
185 + - Stuck "Verbing..." buttons (HTMX + per-file fetch loading-state restoration)
186 + - Library page scroll-in-scroll + `...` dropdown UX
187 + - Checkout error messages not surfaced to user (frontend swallows
188 + `AppError::BadRequest` bodies)
189 + - Misleading webhook error message ("Invalid webhook signature" returned for
190 + parse failures too; should distinguish)
191 +
192 + In `MNW/server/deploy/human_testing.md`:
193 + - P0 sections done: Signup→Verify→Login→Logout (11/11), Account Lockout
194 + + Recovery (4/5, 1 N/A), Password Reset (7/7), Free Item Claim (4/4).
195 + Remaining P0 sections gated on this migration shipping.