refactor: Update Stripe flows for nullable stripe_customer_id (#205) #210

Closed
opened 2026-02-16 14:53:14 +00:00 by egullickson · 0 comments
Owner

Relates to #205

Milestone 4: Service - Stripe flows + null handling

Files

  • backend/src/features/subscriptions/domain/subscriptions.service.ts
  • backend/src/features/subscriptions/api/subscriptions.controller.ts (comment cleanup only)

Changes

  1. Extract ensureStripeCustomer() private method (replaces resolveStripeCustomerId()):

    • Creates Stripe customer if stripeCustomerId is null, returns existing if set
    • Includes cleanup logic: if repository.update() fails after createCustomer() succeeds, attempts deleteCustomer() to prevent orphaned Stripe customers
    • Used by both upgradeSubscription() and updatePaymentMethod()
  2. upgradeSubscription() (lines 203-280):

    • Replace resolveStripeCustomerId() call (line 220) with ensureStripeCustomer()
    • If stripeCustomerId is null, creates Stripe customer directly in service layer
  3. updatePaymentMethod() (lines 965-973):

    • Same pattern: replace resolveStripeCustomerId() call with ensureStripeCustomer()
  4. getInvoices() (line 981):

    • Change from prefix check to null check
    • if (!subscription?.stripeCustomerId) { return []; }
  5. Edge case hardening for cancelSubscription() and reactivateSubscription() (service layer):

    • Add null guard: throw BadRequestError if stripeCustomerId is NULL
    • Message: "Cannot cancel/reactivate subscription without active Stripe billing"
    • Users with admin-set tiers (no Stripe) cannot cancel/reactivate via Stripe - admin must change tier directly
  6. Controller comment cleanup: Remove any stale comments in subscriptions.controller.ts referencing admin_override_ pattern

Tests

  • Verify ensureStripeCustomer() creates Stripe customer when stripeCustomerId is null
  • Verify ensureStripeCustomer() cleans up orphaned customer on DB failure
  • Verify ensureStripeCustomer() returns existing customer when stripeCustomerId is set
  • Verify getInvoices() returns [] for null stripeCustomerId
  • Verify cancelSubscription() throws BadRequestError for null stripeCustomerId
  • Verify reactivateSubscription() throws BadRequestError for null stripeCustomerId

Mobile + Desktop Validation

  • Verify SubscriptionPage and SubscriptionMobileScreen render correctly at 320px, 768px, 1920px viewports when user has NULL stripe_customer_id

Acceptance Criteria

  • ensureStripeCustomer() extracted with orphan cleanup logic
  • Upgrade from admin-set tier creates real Stripe customer
  • Payment method update works for admin-set users
  • Invoices return [] for null stripe_customer_id
  • Cancel/reactivate throw BadRequestError for null stripeCustomerId (service layer)
  • Controller comments cleaned up (no admin_override_ references)
  • Mobile and desktop render correctly
  • All linters pass
  • All tests pass
Relates to #205 ## Milestone 4: Service - Stripe flows + null handling ### Files - `backend/src/features/subscriptions/domain/subscriptions.service.ts` - `backend/src/features/subscriptions/api/subscriptions.controller.ts` (comment cleanup only) ### Changes 1. **Extract `ensureStripeCustomer()` private method** (replaces `resolveStripeCustomerId()`): - Creates Stripe customer if stripeCustomerId is null, returns existing if set - Includes cleanup logic: if repository.update() fails after createCustomer() succeeds, attempts deleteCustomer() to prevent orphaned Stripe customers - Used by both upgradeSubscription() and updatePaymentMethod() 2. **`upgradeSubscription()`** (lines 203-280): - Replace `resolveStripeCustomerId()` call (line 220) with `ensureStripeCustomer()` - If stripeCustomerId is null, creates Stripe customer directly in service layer 3. **`updatePaymentMethod()`** (lines 965-973): - Same pattern: replace `resolveStripeCustomerId()` call with `ensureStripeCustomer()` 4. **`getInvoices()`** (line 981): - Change from prefix check to null check - `if (!subscription?.stripeCustomerId) { return []; }` 5. **Edge case hardening** for `cancelSubscription()` and `reactivateSubscription()` (service layer): - Add null guard: throw BadRequestError if stripeCustomerId is NULL - Message: "Cannot cancel/reactivate subscription without active Stripe billing" - Users with admin-set tiers (no Stripe) cannot cancel/reactivate via Stripe - admin must change tier directly 6. **Controller comment cleanup**: Remove any stale comments in subscriptions.controller.ts referencing admin_override_ pattern ### Tests - Verify ensureStripeCustomer() creates Stripe customer when stripeCustomerId is null - Verify ensureStripeCustomer() cleans up orphaned customer on DB failure - Verify ensureStripeCustomer() returns existing customer when stripeCustomerId is set - Verify getInvoices() returns [] for null stripeCustomerId - Verify cancelSubscription() throws BadRequestError for null stripeCustomerId - Verify reactivateSubscription() throws BadRequestError for null stripeCustomerId ### Mobile + Desktop Validation - Verify SubscriptionPage and SubscriptionMobileScreen render correctly at 320px, 768px, 1920px viewports when user has NULL stripe_customer_id ### Acceptance Criteria - [ ] ensureStripeCustomer() extracted with orphan cleanup logic - [ ] Upgrade from admin-set tier creates real Stripe customer - [ ] Payment method update works for admin-set users - [ ] Invoices return [] for null stripe_customer_id - [ ] Cancel/reactivate throw BadRequestError for null stripeCustomerId (service layer) - [ ] Controller comments cleaned up (no admin_override_ references) - [ ] Mobile and desktop render correctly - [ ] All linters pass - [ ] All tests pass
egullickson added the
status
backlog
type
feature
labels 2026-02-16 14:53:34 +00:00
egullickson added this to the Sprint 2026-02-02 milestone 2026-02-16 14:53:37 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: egullickson/motovaultpro#210