Membership Upgrades

Overview

The membership upgrade system allows users to upgrade their membership tier mid-cycle with automatic prorated billing. The system calculates the exact amount to charge based on the time remaining until the next billing cycle and handles payment processing, subscription updates, and error recovery with automatic refunds.

Architecture

Key Components

  • User Service - Orchestrates the upgrade flow, validates requests, and manages membership records

  • Subscription Service - Manages recurring billing and subscription state

  • Payment Service - Processes debit card charges and refunds

  • Growthbook - Provides membership tier configuration and pricing

Data Flow

The upgrade process follows these steps:

  1. Frontend requests proration calculation

  2. User confirms upgrade with calculated amount

  3. Backend validates user, tier, and amount

  4. Payment is charged via Payment Service

  5. Subscription is updated via Subscription Service

  6. Membership record is created

  7. If any step fails after payment, automatic refund is issued

API Endpoints

Get Proration Amount

Calculate the exact charge for upgrading to a specific tier.

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

Parameters

  • user_id (path) - The user’s unique identifier

  • upgrade_tier (query) - The tier to upgrade to (e.g., "plus", "premium")

Response 200 OK

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

Response Fields

  • proration_amount - Exact charge in dollars, rounded to nearest cent

  • upgrade_tier - The tier being upgraded to

  • billing_date - Next billing date in RFC3339 format

  • days_until_billing - Number of days until next billing cycle

Error Responses

  • 400 Bad Request - Invalid tier or calculation failed

    • Error Code: 8, Error String: M8_INVALID_TIER or M10_PRORATION_CALCULATION_FAILED

  • 404 Not Found - User not found

    • Error Code: 8, Error String: M3_USER_NOT_FOUND

  • 500 Internal Server Error - Config fetch or billing details failed

    • Error Code: 8, Error String: M2_CONFIG_FETCH_FAILED or M7_BILLING_DETAILS_FETCH_FAILED

Upgrade Membership Tier

Execute the membership upgrade with payment processing.

POST /{user_id}/user/membership/upgrade

Request Body

{
  "upgrade_tier": "plus",
  "upgrade_amount": 15.67
}

Request Fields

  • upgrade_tier - The tier to upgrade to (must match Growthbook config)

  • upgrade_amount - The proration amount (must match server calculation exactly)

Response 201 Created

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

Error Responses

All upgrade errors return with error_code: 8 and a specific error_string:

  • 400 Bad Request

    • M1_INVALID_REQUEST_BODY - Malformed JSON request

    • M8_INVALID_TIER - Tier not found in configuration

    • M9_TIER_VERSION_NOT_FOUND - Version not found in tier config

    • M10_PRORATION_CALCULATION_FAILED - Negative proration amount

    • M11_PRORATION_AMOUNT_MISMATCH - Client amount doesn’t match server calculation

  • 402 Payment Required

    • M13_PAYMENT_DECLINED - Payment processor declined the charge

  • 403 Forbidden

    • M4_USER_NOT_ACTIVE - User must be in ACTIVE status

  • 404 Not Found

    • M3_USER_NOT_FOUND - User doesn’t exist

    • M5_MEMBERSHIP_NOT_FOUND - No membership record found

  • 500 Internal Server Error

    • M2_CONFIG_FETCH_FAILED - Growthbook config fetch failed

    • M6_DEBIT_CARD_NOT_FOUND - Failed to retrieve debit card

    • M7_BILLING_DETAILS_FETCH_FAILED - Subscription service error

    • M12_PAYMENT_SUBMISSION_FAILED - Payment service communication error

    • M16_REFUND_FAILED - Critical: upgrade failed and refund failed

    • M17_UPGRADE_FAILED_REFUND_ISSUED - Upgrade failed but refund succeeded

    • M18_MEMBERSHIP_RECORD_CREATE_FAILED - Database write failed

Error Handling

Error Response Format

All membership upgrade errors follow a standard format:

{
  "error_code": 8,
  "error_string": "M13_PAYMENT_DECLINED",
  "message": "Payment declined",
  "status_code": 402
}

Error Categories

Validation Errors (M1, M8-M11)

These occur before any payment is attempted:

  • Invalid request format

  • Invalid tier configuration

  • Proration amount mismatch

Action Required: Fix the request and retry

User State Errors (M3-M5)

User or membership state is invalid:

  • User not found or not active

  • No membership record exists

Action Required: Verify user state before retrying

External Service Errors (M2, M6-M7, M12)

Temporary failures communicating with external services:

  • Growthbook configuration unavailable

  • Payment or subscription service errors

Action Required: Safe to retry after brief delay

Payment Errors (M13)

Payment processor declined the charge:

  • Insufficient funds

  • Card declined

  • Invalid card details

Action Required: User must update payment method

Refund Scenarios (M16-M17)

Payment succeeded but subsequent steps failed:

  • M17_UPGRADE_FAILED_REFUND_ISSUED - Safe state, refund succeeded

  • M16_REFUND_FAILED - Critical error requiring manual intervention

Action Required: M16 requires immediate admin investigation

Database Errors (M18)

Membership record creation failed after payment and subscription update:

Action Required: Requires manual reconciliation between payment and database state

Proration Calculation

Formula

The prorated amount is calculated based on the daily rate and remaining days:

daily_rate = monthly_price / 30
days_remaining = (next_billing_date - now) / 24_hours
proration_amount = round(daily_rate * days_remaining, 2)

Key Points

  • Always assumes 30 days per month for consistency

  • Rounds to nearest cent (2 decimal places)

  • Returns 0 if billing date is in the past

  • Returns -1 on calculation errors (nil inputs, parse failures)

Exact Match Requirement

The client-provided upgrade_amount must match the server calculation exactly (to the cent). This ensures:

  • Client displays the same amount that will be charged

  • No floating-point precision issues

  • Clear audit trail of expected vs. actual charges

Frontend Integration

  1. Display Upgrade Options

    // Fetch proration for each tier option
    GET /user/membership/upgrade/proration?upgrade_tier=plus
    GET /user/membership/upgrade/proration?upgrade_tier=premium
  2. Show Confirmation Dialog

    Display the exact proration_amount to the user with billing date and days remaining

  3. Execute Upgrade

    POST /user/membership/upgrade
    {
      "upgrade_tier": "plus",
      "upgrade_amount": 15.67  // Exact value from proration endpoint
    }
  4. Handle Errors

    if (response.error_code === 8) {
      switch (response.error_string) {
        case "M11_PRORATION_AMOUNT_MISMATCH":
          // Refetch proration amount and show updated price
          break;
        case "M13_PAYMENT_DECLINED":
          // Prompt user to update payment method
          break;
        case "M17_UPGRADE_FAILED_REFUND_ISSUED":
          // Show "Upgrade failed, no charges made" message
          break;
        // ... handle other cases
      }
    }

Best Practices

  • Always fetch fresh proration amount before upgrade

  • Display exact amount with 2 decimal places

  • Show billing date and days remaining for transparency

  • Handle M11_PRORATION_AMOUNT_MISMATCH by refetching (billing date may have passed)

  • Show clear error messages for payment failures

  • For M16/M17, inform user to contact support

Sequence Diagrams

Successful Upgrade Flow

@startuml
actor User
participant Frontend
participant "User Service" as US
participant "Growthbook" as GB
participant "Subscription Service" as SS
participant "Payment Service" as PS
database "DynamoDB" as DB

User -> Frontend: Click "Upgrade to Plus"
Frontend -> US: GET /user/membership/upgrade/proration?upgrade_tier=plus
US -> GB: Get tier config
GB --> US: Tier config with price
US -> SS: Get billing details
SS --> US: Next billing date
US -> US: Calculate proration
US --> Frontend: {proration_amount: 15.67, billing_date: ...}

User -> Frontend: Confirm upgrade
Frontend -> US: POST /user/membership/upgrade
US -> GB: Get tier config
US -> US: Validate user is ACTIVE
US -> US: Validate tier exists
US -> SS: Get billing details
US -> US: Calculate & verify amount
US -> PS: Submit payment ($15.67)
PS --> US: {confirmation_id: "pay_123"}
US -> SS: Upgrade subscription tier
SS --> US: 201 Created
US -> DB: Create membership record
DB --> US: Success
US --> Frontend: 201 {confirmation_id, membership}
Frontend --> User: "Upgrade successful!"
@enduml

Failed Upgrade with Refund Flow

@startuml
participant Frontend
participant "User Service" as US
participant "Subscription Service" as SS
participant "Payment Service" as PS

Frontend -> US: POST /user/membership/upgrade
US -> PS: Submit payment
PS --> US: {confirmation_id: "pay_123"}
US -> SS: Upgrade subscription
SS --> US: 500 Internal Server Error
US -> PS: Submit refund (pay_123)
PS --> US: Refund successful
US --> Frontend: 500 {error_code: 8, error_string: "M17_UPGRADE_FAILED_REFUND_ISSUED"}
Frontend --> Frontend: Show "Upgrade failed, no charges made"
@enduml

Monitoring and Alerts

Key Metrics

  • Upgrade Success Rate - Track M17 errors vs successful upgrades

  • M16 Critical Errors - Alert immediately on refund failures

  • M11 Mismatch Rate - May indicate timing issues if high

  • Average Proration Amount - Monitor for unexpected spikes

Logging

All upgrade operations log:

  • User ID and email

  • Tier upgrade (from → to)

  • Calculated proration amount

  • Confirmation ID

  • Error codes and reasons for failures

Critical Alerts

  • M16_REFUND_FAILED - Immediate page-worthy alert

  • M18_MEMBERSHIP_RECORD_CREATE_FAILED - Requires manual reconciliation

  • High M17 Rate - May indicate subscription service issues

Testing

Unit Tests

The codebase includes comprehensive tests for:

  • Proration calculation with various scenarios

  • Rounding to nearest cent validation

  • Exact amount matching

  • All error code paths

  • Successful upgrade flow

  • Refund scenarios

Test Scenarios

  1. Valid upgrade with exact amount match

  2. Invalid tier name

  3. User not active

  4. Amount mismatch (off by $0.01)

  5. Payment declined

  6. Subscription service failure with successful refund

  7. Subscription service failure with failed refund

  8. Membership record creation failure

  9. Proration calculation with fractional cents

  10. Billing date in past (should return $0)

Configuration

Growthbook Tier Config

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

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

Required Fields

  • current_version - Which version is currently active

  • versions - Array of tier versions

  • version_name - Unique identifier for version

  • price.monthly - Monthly price in dollars

Troubleshooting

Common Issues

"Proration amount does not match" (M11)

Cause: Client amount calculated at different time than server

Solution: 1. Refetch proration amount 2. Display new amount to user 3. Retry upgrade with fresh amount

"Payment declined" (M13)

Cause: Insufficient funds, expired card, or fraud detection

Solution: 1. Prompt user to update payment method 2. Verify card details in Payment Service 3. Check for fraud holds

"Upgrade failed; refund issued" (M17)

Cause: Subscription service unavailable or returned error

Solution: 1. Check Subscription Service health 2. Verify tier configuration in Growthbook 3. User can safely retry after services recover

"Upgrade failed and refund failed" (M16)

Cause: Payment succeeded but both subscription update and refund failed

Solution: 1. IMMEDIATE ACTION REQUIRED 2. Check Payment Service logs for payment status 3. Manually process refund if needed 4. Contact user if charge will remain 5. File incident report

Debug Checklist

  1. Verify user status is ACTIVE

  2. Check Growthbook tier configuration

  3. Verify billing details available from Subscription Service

  4. Check Payment Service connectivity

  5. Review logs for confirmation IDs

  6. Verify DynamoDB write permissions