Membership Upgrades

Overview

The membership upgrade flow allows a user to move to a higher tier mid-billing-cycle with a prorated charge. The proration is calculated based on the days remaining until the next billing date and the price difference between the new tier and the base tier price of $4.99/month.

Architecture

Component Role

User Service

Orchestrates the upgrade: validates the request, charges the user, updates the subscription, and writes the membership record

Growthbook

Provides tier configuration and pricing at runtime

Subscription Service

Returns the next billing date and processes the tier change

Payment Service

Charges the prorated amount and processes refunds on failure

Proration Calculation

Formula

daily_rate          = (new_tier_monthly_price - 4.99) / 30
days_remaining      = days from today (midnight UTC) to next billing date (midnight UTC)
proration_amount    = round(daily_rate * days_remaining, 2)

Key Rules

  • Both timestamps are truncated to midnight UTC before computing days remaining. This keeps the proration amount stable throughout the day.

  • Returns 0 if the billing date is today or in the past.

  • Returns -1 (error sentinel) if either input is nil, unparseable, or if the billing date is more than 65 days away. Billing dates beyond 65 days indicate MX has extended the user’s subscription indefinitely; these users cannot upgrade via the standard flow.

  • The client must submit the exact proration amount returned by the proration endpoint. An off-by-one-cent mismatch returns M11_PRORATION_AMOUNT_MISMATCH.

Growthbook Tier Configuration

Tiers are configured in Growthbook under global.tiers.config:

{
  "plus": {
    "current_version": "v1",
    "versions": [
      {
        "version_name": "v1",
        "price": { "monthly": 9.99 }
      }
    ]
  }
}

API

Get Proration Amount

GET /{user_id}/user/membership/upgrade/proration?upgrade_tier={tier}

Response 200 OK

{
  "proration_amount": 5.83,
  "upgrade_tier": "plus",
  "billing_date": "2024-02-15T00:00:00Z",
  "days_until_billing": 17
}

Upgrade Membership

POST /{user_id}/user/membership/upgrade
{
  "upgrade_tier": "plus",
  "upgrade_amount": 5.83
}

Response 201 Created

{
  "confirmation_id": "pay_123abc",
  "membership": {
    "user_id": "user_123",
    "tier": "PLUS",
    "term": "MONTHLY",
    "status": "",
    "start_date": "2024-01-15T10:30:00Z",
    "tier_version": "v1"
  }
}

Upgrade Flow

POST /{user_id}/user/membership/upgrade
     │
     ▼
Decode request
Fetch tier config from Growthbook ──fail──► 500 M2_CONFIG_FETCH_FAILED
     │
     ▼
Validate tier exists ──fail──► 400 M8_INVALID_TIER / M9_TIER_VERSION_NOT_FOUND
     │
     ▼
User exists and is ACTIVE? ──No──► 404 M3_USER_NOT_FOUND / 403 M4_USER_NOT_ACTIVE
     │ Yes
     ▼
Membership record exists? ──No──► save default BASE-tier record and continue
     │ Yes
     ▼
Get debit card (Payment Service) ──fail──► 500 M6_DEBIT_CARD_NOT_FOUND
     │
     ▼
Get next billing date (Subscription Service) ──fail──► 500 M7_BILLING_DETAILS_FETCH_FAILED
     │
     ▼
Calculate proration amount
  Negative result? ──► 400 M10_PRORATION_CALCULATION_FAILED
  Mismatch with request? ──► 400 M11_PRORATION_AMOUNT_MISMATCH
     │
     ▼
Charge prorated amount (Payment Service)
  Declined? ──► 402 M13_PAYMENT_DECLINED
  Error? ──► 500 M12_PAYMENT_SUBMISSION_FAILED
     │ Charged (confirmation_id)
     ▼
Upgrade subscription tier (Subscription Service)
  Error? ──► attempt refund
               Refund ok? ──► 500 M17_UPGRADE_FAILED_REFUND_ISSUED
               Refund fail? ──► 500 M16_REFUND_FAILED (critical)
     │ Success
     ▼
Write membership record (DynamoDB)
  Error? ──► 500 M18_MEMBERSHIP_RECORD_CREATE_FAILED (manual reconciliation needed)
     │ Success
     ▼
Return 201 Created

Error Reference

All upgrade errors use error_code: 8. Downgrade errors use error_code: 9. Finalize downgrade errors use error_code: 10.

Error String Status Description

M1_INVALID_REQUEST_BODY

400

Malformed JSON in the request body

M2_CONFIG_FETCH_FAILED

500

Growthbook tier configuration unavailable — safe to retry

M3_USER_NOT_FOUND

404

User does not exist

M4_USER_NOT_ACTIVE

403

User must be in ACTIVE status to upgrade

M5_MEMBERSHIP_NOT_FOUND

500

Failed to read or create the user’s membership record (the upgrade flow will create a default BASE record if none exists, so this should never fire from a missing record alone)

M6_DEBIT_CARD_NOT_FOUND

500

Failed to retrieve the user’s debit card

M7_BILLING_DETAILS_FETCH_FAILED

500

Subscription service error fetching next billing date

M8_INVALID_TIER

400

Requested tier not found in Growthbook configuration

M9_TIER_VERSION_NOT_FOUND

400

Current version not found within the tier config

M10_PRORATION_CALCULATION_FAILED

400

Proration returned a negative result

M11_PRORATION_AMOUNT_MISMATCH

400

Client amount does not match server calculation — refetch proration and retry

M12_PAYMENT_SUBMISSION_FAILED

500

Payment service communication error — no charge was made

M13_PAYMENT_DECLINED

402

Card declined by the payment processor

M16_REFUND_FAILED

500

Critical — user was charged, subscription not updated, and refund failed. Requires immediate manual intervention.

M17_UPGRADE_FAILED_REFUND_ISSUED

500

Subscription update failed but refund succeeded — user was not charged. Safe to retry.

M18_MEMBERSHIP_RECORD_CREATE_FAILED

500

Payment and subscription succeeded but DynamoDB write failed. Requires manual reconciliation.