max / makenotwork
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. |