Promotion Management Endpoints Index

Promotion Endpoints

GET /api/promotions

List all promotions with optional filtering (requires administrator access)

Headers:

Authorization: Bearer ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr...

Query Parameters:

  • sort - JSON array with field name and direction ["fieldName","ASC|DESC"]
    Available sort fields: name, description, code, isActive, createdAt, updatedAt, maxRedemptions, redemptionCount, validUntil, couponName, couponType, couponTypeName, percentOff, trialDays
    Example: sort=["couponName","ASC"] - Sort by coupon name in ascending order
  • page - Page number (1-based)
    Example: page=1 - Get the first page
  • perPage - Number of items per page
    Example: perPage=10 - Show 10 items per page
  • filter - JSON object with field name/value pairs for filtering {"fieldName1":"value1","fieldName2":value2}
    Example: filter={"couponType":"DISC","couponName":"Summer"} - Filter by coupon type and name

    Available filters:
    • q - Global search across promotion name, description, code, and coupon name
    • promotionName - Filter by promotion name (partial match, case-insensitive)
    • couponType - Filter by coupon type code (DISC, TRIA, NONE). Use NONE to filter promotions without coupons
    • couponName - Filter by coupon name (partial match, case-insensitive)
    • isActive - Filter by active status (true/false)


    Special filter: Global Search
    Using the key q in the filter will search across all text columns:
    Example: filter={"q":"summer"} - Find promotions with "summer" in name, description, code, or coupon name
  • active - Filter by active status (true/false)
  • syncWithStripe - Whether to sync with Stripe before fetching data (default: true)

Example Requests:

GET /api/promotions?page=1&perPage=10&sort=["name","ASC"]&filter={"isActive":true}&syncWithStripe=true
GET /api/promotions?page=1&perPage=20&filter={"q":"summer","couponType":"DISC"}
GET /api/promotions?filter={"couponType":"NONE"}
GET /api/promotions?filter={"promotionName":"summer"}
GET /api/promotions?filter={"promotionName":"welcome","isActive":true}

Response (200 OK):

{
    "data": [
        {
            "id": 1,
            "name": "Summer Sale",
            "description": "20% off summer products",
            "code": "SUMMER20",
            "couponId": 1,
            "maxRedemptions": 100,
            "redemptionCount": 45,
            "validUntil": "2023-09-30T23:59:59Z",
            "isActive": true,
            "createdAt": "2023-07-15T14:25:00Z",
            "updatedAt": null,
            "coupon": {
                "couponId": 1,
                "couponName": "Welcome Discount",
                "couponType": "DISC",
                "couponTypeName": "Discount Percentage",
                "percentOff": 20,
                "trialDays": null
            }
        }
        // Additional promotion objects...
    ],
    "total": 2
}

Headers:

Content-Range: promotions 0-1/2
Accept-Range: promotions
Access-Control-Expose-Headers: Content-Range
X-Total-Count: 2

Error Responses:

{
    "message": "Administrator access required"
}

Notes:

  • The syncWithStripe parameter ensures promotion data is synchronized with Stripe before filtering
  • The response includes pagination metadata in headers
  • Search functionality looks across name, description, code, and coupon name fields
  • Only administrators can access this endpoint
  • Stripe IDs are not included in the response for security reasons
  • Associated coupon details are included automatically when available
GET /api/promotions/applied

List all applied promotions with filtering, sorting, and pagination (requires administrator access)

Headers:

Authorization: Bearer ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr...

Query Parameters:

  • sort - JSON array with field name and direction ["fieldName","ASC|DESC"]
    Allowed sort fields: appliedAt, promotionName, promotionCode, username, email, planName, couponTypeName, percentOff, trialDays
    Example: sort=["appliedAt","DESC"] - Sort by application date descending
  • page - Page number (1-based)
    Example: page=1 - Get the first page
  • perPage - Number of items per page
    Example: perPage=10 - Show 10 items per page
  • filter - JSON object with field name/value pairs for filtering
    Example: filter={"q":"summer","startDate":"2023-06-01","endDate":"2023-08-31"}

    Available filters:
    • q - Global search across promotion names, promotion codes, usernames, email addresses, and plan names
    • userId - Filter by user ID (number)
    • promotionId - Filter by promotion ID (number)
    • promotionName - Filter by promotion name (partial match, case-insensitive)
    • couponId - Filter by coupon ID (number)
    • subscriptionId - Filter by subscription ID (number)
    • couponType - Filter by coupon type code (DISC, TRIA, NONE). Use NONE to filter applied promotions without coupons
    • startDate - Filter by application date from this date onwards (YYYY-MM-DD format)
    • endDate - Filter by application date up to this date (YYYY-MM-DD format)


    Special filter: Global Search
    Using the key q in the filter will search across key text columns:
    Example: filter={"q":"summer"} - Find applied promotions with "summer" in promotion name, promotion code, username, email, or plan name

Example Requests:

GET /api/promotions/applied?page=1&perPage=10&sort=["appliedAt","DESC"]&filter={"q":"summer"}
GET /api/promotions/applied?page=1&perPage=20&filter={"q":"john","startDate":"2023-06-01","endDate":"2023-08-31"}
GET /api/promotions/applied?filter={"startDate":"2023-01-01","endDate":"2023-12-31"}
GET /api/promotions/applied?filter={"userId":42,"promotionId":5}
GET /api/promotions/applied?filter={"q":"discount"}
GET /api/promotions/applied?sort=["promotionName","ASC"]&filter={"startDate":"2023-06-01"}
GET /api/promotions/applied?sort=["couponTypeName","ASC"]&filter={"couponId":3}
GET /api/promotions/applied?sort=["percentOff","DESC"]&filter={"subscriptionId":18}
GET /api/promotions/applied?filter={"couponType":"NONE"}
GET /api/promotions/applied?filter={"promotionName":"welcome"}
GET /api/promotions/applied?filter={"promotionName":"summer","couponType":"DISC"}

Response (200 OK) - with subscription:

{
    "data": [
        {
            "id": 1,
            "appliedAt": "2023-07-15T14:25:00Z",
            "promotionId": 5,
            "promotionName": "Summer Sale",
            "promotionCode": "SUMMER20",
            "couponId": 1,
            "couponType": "DISC",
            "couponTypeName": "Discount Percentage",
            "percentOff": 20,
            "trialDays": null,
            "subscriptionId": 18,
            "planName": "Premium Monthly",
            "planInterval": "month",
            "planAmount": 2999,
            "planCurrency": "usd",
            "userId": 42,
            "username": "johndoe",
            "email": "john@example.com"
        }
    ],
    "total": 42
}

Response (200 OK) - without subscription (NONS promotion):

{
    "data": [
        {
            "id": 3,
            "appliedAt": "2023-08-01T10:30:00Z",
            "promotionId": 7,
            "promotionName": "Welcome Offer",
            "promotionCode": "WELCOME10",
            "couponId": null,
            "couponType": null,
            "couponTypeName": null,
            "percentOff": null,
            "trialDays": null,
            "subscriptionId": null,
            "planName": null,
            "planInterval": null,
            "planAmount": null,
            "planCurrency": null,
            "userId": 45,
            "username": "newuser",
            "email": "newuser@example.com"
        }
    ],
    "total": 15
}

Headers:

Content-Range: applied-promotions 0-1/42
Accept-Range: applied-promotions
Access-Control-Expose-Headers: Content-Range
X-Total-Count: 42

Error Responses:

{
    "message": "Administrator access required"
}

{
    "message": "Invalid date format in filter. Use YYYY-MM-DD format."
}

Notes:

  • This endpoint provides comprehensive details about applied promotions, including information about the user, promotion, coupon, and subscription
  • User information is always available via the user_id column in promotion_redemptions
  • For NONS (non-subscribed) promotions, subscription-related fields (subscriptionId, planName, etc.) and coupon-related fields will be null
  • The default sorting is by applied_at in descending order (newest first)
  • Date filters accept dates in ISO format (YYYY-MM-DD)
  • Global search looks for matches in promotion names, promotion codes, usernames, email addresses, and plan names
  • Only administrators can access this endpoint
  • Plan amounts are in cents (e.g., 2999 = $29.99)
GET /api/promotions/:id

Get details of a specific promotion by ID (requires administrator access)

Headers:

Authorization: Bearer ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr...

Query Parameters:

  • includeCoupons - Whether to include associated coupon details (true/false, default: true)

Response (200 OK):

{
    "id": 1,
    "name": "Summer Sale 2023",
    "description": "Special discounts for summer products",
    "code": "SUMMER20",
    "couponId": 1,
    "maxRedemptions": 100,
    "redemptionCount": 45,
    "validUntil": "2023-09-30T23:59:59Z",
    "isActive": true,
    "createdAt": "2023-05-15T00:00:00Z",
    "updatedAt": null,
    "coupon": {
        "couponId": 1,
        "couponName": "Welcome Discount",
        "couponType": "DISC",
        "couponTypeName": "Discount Percentage",
        "percentOff": 20,
        "trialDays": null
    }
}

Error Responses:

{
    "message": "Administrator access required"
}

{
    "message": "Invalid promotion ID"
}

{
    "message": "Promotion not found"
}

Notes:

  • This endpoint syncs the specific promotion with Stripe before returning the data
  • The coupon field is included by default with associated coupon details
  • Stripe IDs are not included in the response for security reasons
GET /api/promotions/:id/coupons

Get coupons associated with a promotion (requires administrator access)

Headers:

Authorization: Bearer ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr...

Response (200 OK):

[
    {
        "id": 1,
        "name": "Welcome Discount",
        "couponTypeCode": "DISC",
        "couponTypeName": "Discount Percentage",
        "discountPercentage": 20,
        "trialDays": null,
        "couponDurationTypeCode": "ONCE",
        "couponDurationTypeName": "Once",
        "durationInMonths": null,
        "isActive": true,
        "createdAt": "2023-01-15T08:30:00Z",
        "updatedAt": null,
        "timesRedeemed": 45
    }
]

Error Responses:

{
    "message": "Administrator access required"
}

{
    "message": "Invalid promotion ID"
}

{
    "message": "Promotion not found"
}

Notes:

  • This endpoint syncs the promotion with Stripe before returning coupon data
  • Each promotion is typically associated with one coupon
  • The timesRedeemed field comes from Stripe data
  • Stripe IDs are not included in the response for security reasons
GET /api/promotions/validate/:code

Validate a promotion code without applying it (requires authentication)

Headers:

Authorization: Bearer ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr...

Response (200 OK) - Valid Promotion:

{
    "valid": true,
    "promotion": {
        "id": 1,
        "name": "Summer Sale",
        "description": "20% off summer products",
        "code": "SUMMER20",
        "coupon": {
            "type": "DISC",
            "name": "Summer Discount",
            "discountPercentage": 20,
            "trialDays": null
        }
    }
}

Response (200 OK) - Invalid Promotion:

{
    "valid": false,
    "message": "Promotion code not found"
}

Error Responses:

{
    "message": "Authentication required"
}

{
    "message": "Promotion code is required"
}

{
    "message": "Promotion code is inactive"
}

{
    "message": "Promotion code has expired"
}

{
    "message": "Maximum redemptions reached for this promotion code"
}

Notes:

  • This endpoint syncs the promotion with Stripe before validation
  • Validates against both local database and Stripe data
  • Does not apply the promotion, only validates its eligibility
POST /api/promotions

Create a new promotion code for an existing coupon (requires administrator access and CSRF token)

Headers:

Authorization: Bearer ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr...
x-csrf-token: a8d7f9c6e5b4a3c2d1e0f9a8d7f6c5b4a3

Request Body (for subscribed users):

{
    "name": "Holiday Special",
    "description": "Special promotions for the holiday season",
    "code": "HOLIDAY25",
    "userTypeCode": "SUBS",
    "couponId": 1,
    "maxRedemptions": 500,
    "validUntil": "2023-12-31T23:59:59Z",
    "isActive": true
}

Request Body (for non-subscribed users):

{
    "name": "Welcome Offer",
    "description": "Special promotion for new users",
    "code": "WELCOME10",
    "userTypeCode": "NONS",
    "maxRedemptions": 1000,
    "validUntil": "2023-12-31T23:59:59Z",
    "isActive": true
}

Response (201 Created):

{
    "message": "Promotion created successfully",
    "promotion": {
        "id": 3,
        "name": "Holiday Special",
        "description": "Special promotions for the holiday season",
        "code": "HOLIDAY25",
        "couponId": 1,
        "maxRedemptions": 500,
        "redemptionCount": 0,
        "validUntil": "2023-12-31T23:59:59Z",
        "isActive": true,
        "createdAt": "2023-11-01T00:00:00Z",
        "updatedAt": null
    }
}

Error Responses:

{
    "message": "Administrator access required"
}

{
    "message": "Missing required fields: name, description, and userTypeCode are required"
}

{
    "message": "Invalid userTypeCode. Must be either 'SUBS' or 'NONS'"
}

{
    "message": "couponId is required when userTypeCode is 'SUBS'"
}

{
    "message": "Promotion code already exists"
}

{
    "message": "Coupon not found"
}

{
    "message": "Failed to create promotion",
    "error": "Error creating promotion in Stripe"
}

Notes:

  • The userTypeCode parameter determines the target user type: "SUBS" (subscribed) or "NONS" (non-subscribed)
  • When userTypeCode is "SUBS", the couponId is required and the promotion is created in both Stripe and the local database
  • When userTypeCode is "NONS", the couponId should be omitted and the promotion is created only in the local database
  • The code must be unique across all promotions
POST /api/promotions/apply

Apply a promotion code to the user's subscription or grant subscription exemption (requires authentication and CSRF token)

Headers:

Authorization: Bearer ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr...
x-csrf-token: a8d7f9c6e5b4a3c2d1e0f9a8d7f6c5b4a3

Request Body:

{
    "code": "SUMMER20"
}

Response (200 OK) - Percentage Discount (Subscribed Users):

{
    "message": "Promotion code applied successfully",
    "discount": {
        "type": "percentage",
        "percentOff": 20,
        "amountOff": null,
        "currency": null
    }
}

Response (200 OK) - Amount Discount (Subscribed Users):

{
    "message": "Promotion code applied successfully",
    "discount": {
        "type": "amount",
        "percentOff": null,
        "amountOff": 500,
        "currency": "usd"
    }
}

Response (200 OK) - Subscription Exemption with End Date (Non-Subscribed Users):

{
    "message": "Promotion code applied successfully. Your account has been updated with extended access.",
    "discount": {
        "type": "exemption",
        "exemptionEndsAt": "2023-12-31T23:59:59Z"
    }
}

Response (200 OK) - Unlimited Subscription Exemption (Non-Subscribed Users):

{
    "message": "Promotion code applied successfully. Your account has been granted unlimited access.",
    "discount": {
        "type": "exemption",
        "exemptionEndsAt": null
    }
}

Error Responses:

{
    "message": "Authentication required"
}

{
    "message": "Promotion code is required"
}

{
    "message": "Promotion code not found"
}

{
    "message": "Promotion code is inactive"
}

{
    "message": "Promotion code has expired"
}

{
    "message": "Maximum redemptions reached for this promotion code"
}

{
    "message": "You must have an active subscription to apply a promotion code"
}

{
    "message": "Your subscription already has a promotion code applied"
}

Notes:

  • This endpoint handles two types of promotions based on whether the promotion has an associated coupon
  • For promotions with coupons (couponId not null):
    • User must have an active subscription to apply the promotion
    • Only one promotion can be active per subscription
    • The promotion is applied immediately to the Stripe subscription
    • Amount discounts are in cents (e.g., 500 = $5.00)
  • For promotions without coupons (couponId is null):
    • Does not require an active subscription
    • Updates the user's subscription_exemption_ends_at date to the promotion's validUntil date
    • If the promotion has no validUntil date, the subscription_exemption_ends_at is set to null, granting unlimited access
    • Changes the user's type to non-subscribed (NONS)
    • Records the redemption without linking to a subscription or Stripe discount
    • Grants extended or unlimited access to the platform depending on the promotion's expiration date
POST /api/promotions/apply-to-user

Apply a promotion code to a specified user's subscription or grant subscription exemption (requires administrator access and CSRF token)

Headers:

Authorization: Bearer ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr...
x-csrf-token: a8d7f9c6e5b4a3c2d1e0f9a8d7f6c5b4a3

Request Body:

{
    "userId": 42,
    "code": "SUMMER20"
}

Response (200 OK) - Percentage Discount (Subscribed Users):

{
    "message": "Promotion code applied successfully",
    "discount": {
        "type": "percentage",
        "percentOff": 20,
        "amountOff": null,
        "currency": null
    }
}

Response (200 OK) - Amount Discount (Subscribed Users):

{
    "message": "Promotion code applied successfully",
    "discount": {
        "type": "amount",
        "percentOff": null,
        "amountOff": 500,
        "currency": "usd"
    }
}

Response (200 OK) - Subscription Exemption with End Date (Non-Subscribed Users):

{
    "message": "Promotion code applied successfully. Your account has been updated with extended access.",
    "discount": {
        "type": "exemption",
        "exemptionEndsAt": "2023-12-31T23:59:59Z"
    }
}

Response (200 OK) - Unlimited Subscription Exemption (Non-Subscribed Users):

{
    "message": "Promotion code applied successfully. Your account has been granted unlimited access.",
    "discount": {
        "type": "exemption",
        "exemptionEndsAt": null
    }
}

Error Responses:

{
    "message": "Administrator access required"
}

{
    "message": "User ID is required"
}

{
    "message": "Promotion code is required"
}

{
    "message": "Promotion code not found"
}

{
    "message": "Promotion code is inactive"
}

{
    "message": "Promotion code has expired"
}

{
    "message": "Maximum redemptions reached for this promotion code"
}

{
    "message": "You must have an active subscription to apply a promotion code"
}

{
    "message": "Your subscription already has a promotion code applied"
}

Notes:

  • This endpoint is only accessible to administrator users (type code: ADMI)
  • This endpoint handles two types of promotions based on whether the promotion has an associated coupon
  • For promotions with coupons (couponId not null):
    • User must have an active subscription to apply the promotion
    • Only one promotion can be active per subscription
    • The promotion is applied immediately to the Stripe subscription
    • Amount discounts are in cents (e.g., 500 = $5.00)
  • For promotions without coupons (couponId is null):
    • Does not require an active subscription
    • Updates the user's subscription_exemption_ends_at date to the promotion's validUntil date
    • If the promotion has no validUntil date, the subscription_exemption_ends_at is set to null, granting unlimited access
    • Changes the user's type to non-subscribed (NONS)
    • Records the redemption without linking to a subscription or Stripe discount
    • Grants extended or unlimited access to the platform depending on the promotion's expiration date
  • The promotion is synced with Stripe before application to ensure up-to-date data
PUT /api/promotions/:id

Update a promotion's details (requires administrator access and CSRF token)

Note: Only certain fields can be updated. The code, couponId, and stripePromotionCodeId cannot be changed once created.

Headers:

Authorization: Bearer ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr...
x-csrf-token: a8d7f9c6e5b4a3c2d1e0f9a8d7f6c5b4a3

Request Body:

{
    "name": "Summer Sale 2023 Extended",
    "description": "Extended summer discounts for clearance",
    "isActive": true
}

Response (200 OK):

{
    "message": "Promotion updated successfully",
    "promotion": {
        "id": 1,
        "name": "Summer Sale 2023 Extended",
        "description": "Extended summer discounts for clearance",
        "code": "SUMMER20",
        "couponId": 1,
        "maxRedemptions": 200,
        "redemptionCount": 45,
        "validUntil": "2023-09-30T23:59:59Z",
        "isActive": true,
        "createdAt": "2023-05-15T00:00:00Z",
        "updatedAt": "2023-09-01T00:00:00Z"
    }
}

Error Responses:

{
    "message": "Administrator access required"
}

{
    "message": "Invalid promotion ID"
}

{
    "message": "No update parameters provided"
}

{
    "message": "Promotion not found"
}

{
    "message": "Failed to update promotion",
    "error": "Error updating promotion in Stripe"
}

Notes:

  • Updates are synchronized with Stripe before and after the operation
  • Only name, description, maxRedemptions, and isActive can be updated
  • maxRedemptions is updated only in the local database - Stripe does not allow updating this field after promotion code creation
  • isActive updates are sent to Stripe as the active field
  • name and description updates are stored in Stripe metadata
DELETE /api/promotions/:id

Delete a promotion (requires administrator access and CSRF token)

Headers:

Authorization: Bearer ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr...
x-csrf-token: a8d7f9c6e5b4a3c2d1e0f9a8d7f6c5b4a3

Response (200 OK):

{
    "message": "Promotion deleted successfully"
}

Error Responses:

{
    "message": "Administrator access required"
}

{
    "message": "Invalid promotion ID"
}

{
    "message": "Promotion not found"
}

{
    "message": "Cannot delete promotion with associated coupons. Remove all coupons first."
}

Notes:

  • Deletion is synchronized with Stripe - the promotion is removed from both systems
  • This is a permanent operation and cannot be undone
  • The promotion is synced with Stripe before deletion to ensure data consistency