Skip to content

feat(checkout): attach a persistent Stripe Customer at payment (stop 'Guest' sales)#264

Open
kilbot wants to merge 1 commit into
mainfrom
feat/stripe-customer-capture
Open

feat(checkout): attach a persistent Stripe Customer at payment (stop 'Guest' sales)#264
kilbot wants to merge 1 commit into
mainfrom
feat/stripe-customer-capture

Conversation

@kilbot

@kilbot kilbot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

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:

  1. The store payment-sessions route derives the customer from the JWT: customer_id: req.auth_context?.actor_id.
  2. The create-payment-session workflow only creates/links a Stripe account holder when("customer-id-exists").
  3. The storefront's medusaFetch sends only x-publishable-api-key — never the customer's bearer token.
  4. → Medusa sees an unauthenticated request → actor_id empty → no account holder → intentRequest.customer undefined → Stripe invents a "Guest".

The change

Forward the signed-in customer's Medusa JWT on createPaymentCollection and createPaymentSession (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 empty actor_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 run1155/1155 unit tests pass (added header-forwarding + null-token + updated route call-arg tests)
  • eslint — clean
  • tsc --noEmit — clean
  • next build — clean

Not 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:

  • Save card for off-session renewals (Level 2) — separate PR (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.
  • Q2 — order↔customer linkage: the rest of the checkout (cart create/complete) is still publishable-key-only, so orders may still be linked to a guest customer record. This PR fixes the Stripe side; confirming/authenticating the whole checkout wants live DB verification (blocked here by the 1Password ssh-signing constraint).
  • Renewal charge flow (Level 3) — the actual payoff of saving a card; larger, own PR, needs SCA fallback (EU/PL) + Card Account Updater.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved cart payment session creation so authenticated customer tokens are forwarded through payment requests, helping preserve the correct customer context for payments.
    • If no token is available, payment creation still succeeds while safely omitting the authorization header.
    • Updated coverage to reflect consistent behavior across payment collection creation, payment session creation, and existing cart payment flows.

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.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kilbot has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The 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.

Changes

Auth token forwarding for Medusa payment calls

Layer / File(s) Summary
Medusa client: optional authToken support
src/services/core/external/medusa-client.ts, src/services/core/external/medusa-client.test.ts
createPaymentCollection and createPaymentSession accept an optional authToken parameter and conditionally add an Authorization: Bearer header; tests verify header presence/absence.
Route: retrieve and forward auth token
src/app/api/store/cart/payment-sessions/route.ts
The POST handler imports getAuthToken, retrieves the token after customer validation, and passes it into both createPaymentCollection and createPaymentSession calls.
Route tests: verify token propagation and missing-token handling
src/app/api/store/cart/payment-sessions/route.test.ts
Mocks getAuthToken, adds an AUTH_TOKEN constant, updates existing scenario assertions to expect the token, and adds tests for JWT forwarding and graceful handling of a missing token (returns 200, passes null).

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
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the checkout change to forward auth and attach a persistent Stripe Customer, matching the PR objective.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/stripe-customer-capture

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/services/core/external/medusa-client.ts (1)

353-379: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate 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

📥 Commits

Reviewing files that changed from the base of the PR and between 2ef1925 and a76edf1.

📒 Files selected for processing (4)
  • src/app/api/store/cart/payment-sessions/route.test.ts
  • src/app/api/store/cart/payment-sessions/route.ts
  • src/services/core/external/medusa-client.test.ts
  • src/services/core/external/medusa-client.ts

@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown

🚀 Preview: https://wcpos-p4its6x7a-wcpos.vercel.app

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant