Membership Downgrades

Overview

A membership downgrade is a two-phase process. When a user requests a downgrade, the change is scheduled for the end of the current billing cycle rather than applied immediately. The Subscription Service finalizes the downgrade by calling back into the User Service once the billing cycle ends.

User requests downgrade
        │
        ▼
Phase 1: Initiate (IsPendingDowngrade = true)
        │
        ... billing cycle ends ...
        │
        ▼
Phase 2: Finalize (Subscription Service callback)
        │
        ▼
New membership record at lower tier

Phase 1: Initiate Downgrade

POST /{user_id}/user/membership/downgrade

Request

{
  "downgrade_tier": "base"
}

Flow

Decode request
Fetch tier config from Growthbook ──fail──► 500 M2_CONFIG_FETCH_FAILED
     │
     ▼
Validate tier exists in config ──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──► 404 M5_MEMBERSHIP_NOT_FOUND
     │ Yes
     ▼
Downgrade subscription (Subscription Service)
  Returns: downgrade_date (RFC3339)
  Error? ──► 500 M19_DOWNGRADE_FAILED
     │
     ▼
Update existing membership record:
  event_type:           MEMBERSHIP_DOWNGRADE
  is_pending_downgrade: true
  downgrade_date:       <date from Subscription Service>
  event_source:         <caller identity>
     │
     ▼
Write membership record (DynamoDB) ──fail──► 500 M18_MEMBERSHIP_RECORD_CREATE_FAILED
     │
     ▼
Return 201 Created

Response 201 Created

{
  "membership": {
    "user_id": "user_123",
    "tier": "PLUS",
    "term": "MONTHLY",
    "status": "",
    "is_pending_downgrade": true,
    "downgrade_date": "2024-02-15T00:00:00Z",
    "tier_version": "v1"
  }
}

Note that tier in the response reflects the current tier, not the target. The user remains on their current tier until the billing cycle ends.

Phase 2: Finalize Downgrade

Called by the Subscription Service at the end of the billing cycle. This endpoint is internal-only and must not be exposed to the mobile app.

POST /{user_id}/user/membership/downgrade/finalize

Request

{
  "downgrade_tier": "base",
  "downgrade_version": "v1"
}

Flow

Decode request
     │
     ▼
Membership record exists? ──No──► 404 M5_MEMBERSHIP_NOT_FOUND
     │ Yes
     ▼
IsPendingDowngrade == true? ──No──► 403 M20_MEMBERSHIP_NOT_PENDING_DOWNGRADE
     │ Yes
     ▼
Write new membership record:
  tier:       downgrade_tier (uppercased)
  term:       MONTHLY
  version:    downgrade_version
  event_type: MEMBERSHIP_DOWNGRADE_FINALIZED
  event_source: <caller identity>
     │
     ▼
Return 201 Created

Error Reference

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

M3_USER_NOT_FOUND

404

User does not exist

M4_USER_NOT_ACTIVE

403

User must be in ACTIVE status to downgrade

M5_MEMBERSHIP_NOT_FOUND

404

No membership record exists for this user

M8_INVALID_TIER

400

Requested tier not found in Growthbook configuration

M9_TIER_VERSION_NOT_FOUND

400

Version not found within the tier config

M18_MEMBERSHIP_RECORD_CREATE_FAILED

500

DynamoDB write failed

M19_DOWNGRADE_FAILED

500

Subscription Service failed to schedule the downgrade

M20_MEMBERSHIP_NOT_PENDING_DOWNGRADE

403

Finalize was called but membership is not in a pending downgrade state