feat(checkout): attach a persistent Stripe Customer at payment (stop 'Guest' sales)#264
feat(checkout): attach a persistent Stripe Customer at payment (stop 'Guest' sales)#264kilbot wants to merge 1 commit into
Conversation
Forward the signed-in customer's Medusa JWT on the payment-collection and
payment-session calls so Medusa resolves `auth_context.actor_id` to the
customer and creates/links a Stripe account holder (cus_...) on the
PaymentIntent — instead of the throwaway "Guest" that Stripe invents for
publishable-key-only requests.
The store payment-sessions workflow only creates an account holder
`when("customer-id-exists")`, and `customer_id` is derived solely from the
request's auth context; today the storefront's medusaFetch sends only the
publishable key, so actor_id is empty and every sale shows as Guest.
getCustomer() in the route already validates the token against
/store/customers/me (which rejects an empty actor_id), so the forwarded
token is guaranteed to resolve to a real customer. A missing token degrades
to the previous guest behaviour rather than breaking checkout.
Scope: attach-customer only (bookkeeping/reconciliation). Saving the card
for off-session renewals is a separate follow-up.
There was a problem hiding this comment.
kilbot has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
📝 WalkthroughWalkthroughThe Medusa client's createPaymentCollection and createPaymentSession functions now accept an optional authToken parameter, adding an Authorization Bearer header when present. The payment-sessions route retrieves this token via getAuthToken and forwards it to both calls. Tests were updated and extended accordingly. ChangesAuth token forwarding for Medusa payment calls
Estimated code review effort: 2 (Simple) | ~15 minutes Sequence Diagram(s)sequenceDiagram
participant Route as PaymentSessionsRoute
participant Auth as MedusaAuth
participant Client as MedusaClient
participant Medusa as MedusaAPI
Route->>Auth: getCustomer()
Auth-->>Route: customer
Route->>Auth: getAuthToken()
Auth-->>Route: authToken
Route->>Client: createPaymentCollection(cartId, authToken)
Client->>Medusa: POST /store/payment-collections (Authorization: Bearer authToken)
Medusa-->>Client: paymentCollection
Client-->>Route: paymentCollection
Route->>Client: createPaymentSession(collectionId, providerId, authToken)
Client->>Medusa: POST /store/payment-collections/:id/payment-sessions (Authorization: Bearer authToken)
Medusa-->>Client: paymentSession
Client-->>Route: paymentSession
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/services/core/external/medusa-client.ts (1)
353-379: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDuplicate header-forwarding logic between the two functions.
Both functions repeat the identical
...(authToken ? {headers: {Authorization: ...}} : {})pattern. A small shared helper (e.g.buildAuthHeaders(authToken)) would remove the duplication, though the current form is simple enough to be low priority.Also applies to: 392-414
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/services/core/external/medusa-client.ts` around lines 353 - 379, Both createPaymentCollection and the related createPaymentSession path repeat the same authToken-to-Authorization header spread logic, so extract it into a shared helper such as buildAuthHeaders(authToken) and use that helper from medusa-client.ts to keep the header construction in one place and remove duplication.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/services/core/external/medusa-client.ts`:
- Around line 353-379: Both createPaymentCollection and the related
createPaymentSession path repeat the same authToken-to-Authorization header
spread logic, so extract it into a shared helper such as
buildAuthHeaders(authToken) and use that helper from medusa-client.ts to keep
the header construction in one place and remove duplication.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 665050f5-b837-4905-b4f3-4f5f30dd9238
📒 Files selected for processing (4)
src/app/api/store/cart/payment-sessions/route.test.tssrc/app/api/store/cart/payment-sessions/route.tssrc/services/core/external/medusa-client.test.tssrc/services/core/external/medusa-client.ts
|
🚀 Preview: https://wcpos-p4its6x7a-wcpos.vercel.app |
What & why
Our first live sale showed in Stripe as a Guest — no
cus_...attached to the PaymentIntent. This wires the checkout so every purchase attaches a persistent Stripe Customer, linked to the Medusa customer."Guest" in Stripe ≠ guest checkout. The buyer did register a Medusa account (we have email, name, billing address). The gap is purely that no Stripe Customer was linked.
Root cause
Medusa v2 already creates a Stripe Customer automatically — we just never triggered it:
customer_id: req.auth_context?.actor_id.create-payment-sessionworkflow only creates/links a Stripe account holderwhen("customer-id-exists").medusaFetchsends onlyx-publishable-api-key— never the customer's bearer token.actor_idempty → no account holder →intentRequest.customerundefined → Stripe invents a "Guest".The change
Forward the signed-in customer's Medusa JWT on
createPaymentCollectionandcreatePaymentSession(server-side, in/api/store/cart/payment-sessions).Why this is safe: the route already calls
getCustomer(), which validates the token against/store/customers/me(Medusa rejects a JWT with an emptyactor_id). So a non-null customer guarantees the forwarded token resolves to a real customer. A missing token degrades to the previous guest behaviour rather than breaking checkout. The token param is optional, so anonymous/mock callers are unaffected.Validation
vitest run— 1155/1155 unit tests pass (added header-forwarding + null-token + updated route call-arg tests)eslint— cleantsc --noEmit— cleannext build— cleanNot verified live: confirming a real Stripe Customer (not Guest) appears after an actual card payment requires a live purchase, which I didn't run. Recommend a single small test/real purchase after deploy to confirm end-to-end.
Scope & follow-ups
This PR is attach-customer only (Level 1 — bookkeeping/reconciliation, stops Guest sprawl). Deliberately not in scope:
setup_future_usage: 'off_session'for yearly only + a consent checkbox). Needs owner decisions on consent wording/default, so it'll come as a draft.🤖 Generated with Claude Code
Summary by CodeRabbit