mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
chore: relocate backend into apps/api ahead of monorepo merge
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,406 @@
|
||||
# Tiers Controller Documentation
|
||||
|
||||
**File:** `controllers/admin/tiers.controller.js`
|
||||
**Base URL:** `/api/admin/tiers`
|
||||
**Guards:** `authenticate → requireAdmin() → adminLimiter`
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
- [Tier Plans](#tier-plans)
|
||||
- [Get All Plans](#get-all-plans)
|
||||
- [Get Single Plan](#get-single-plan)
|
||||
- [Create Plan](#create-plan)
|
||||
- [Update Plan](#update-plan)
|
||||
- [Archive Plan](#archive-plan)
|
||||
- [Bulk Archive Plans](#bulk-archive-plans)
|
||||
- [Restore Plan](#restore-plan)
|
||||
- [Bulk Restore Plans](#bulk-restore-plans)
|
||||
- [Get Plan Field Values](#get-plan-field-values)
|
||||
- [Plan Courses](#plan-courses)
|
||||
- [Get Plan Courses](#get-plan-courses)
|
||||
- [Sync Plan Courses](#sync-plan-courses)
|
||||
- [User Tiers](#user-tiers)
|
||||
- [Get User Tiers](#get-user-tiers)
|
||||
- [Grant Tier](#grant-tier)
|
||||
- [Revoke Tier](#revoke-tier)
|
||||
- [Payments](#payments)
|
||||
- [Get All Payments](#get-all-payments)
|
||||
- [Get Single Payment](#get-single-payment)
|
||||
- [Get Payment Field Values](#get-payment-field-values)
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Pending payments older than **60 minutes** are automatically expired before any payment list/detail call.
|
||||
- Revoking a tier immediately creates a new `free` tier row for the user (auto-downgrade).
|
||||
- `plan_id` is serialized as a string in create responses to avoid BigInt overflow in JS.
|
||||
- A plan's `tier` field cannot be updated — only `label`, `duration_days`, `price`, `currency`, and `is_active`.
|
||||
|
||||
---
|
||||
|
||||
## Tier Plans
|
||||
|
||||
### Get All Plans
|
||||
|
||||
**`GET /api/admin/tiers`**
|
||||
|
||||
Returns paginated plans. Pass `?archived=true` to see soft-deleted plans instead.
|
||||
|
||||
### Query Parameters
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|---------|----------|-------------|
|
||||
| `archived` | boolean | No | `true` to show archived plans only |
|
||||
| `page` | number | No | Default: `1` |
|
||||
| `limit` | number | No | Default: `10` |
|
||||
|
||||
### Response `200`
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Plans retrieved.",
|
||||
"data": [...],
|
||||
"pagination": { "page": 1, "limit": 10, "total": 4, "totalPages": 1 }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Single Plan
|
||||
|
||||
**`GET /api/admin/tiers/:id`**
|
||||
|
||||
### Response `200`
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Plan retrieved.",
|
||||
"data": {
|
||||
"plan_id": 1,
|
||||
"tier": "premium",
|
||||
"label": "Premium Monthly",
|
||||
"duration_days": 30,
|
||||
"price": "9.99",
|
||||
"currency": "USD",
|
||||
"is_active": true,
|
||||
"createdAt": "...",
|
||||
"updatedAt": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Create Plan
|
||||
|
||||
**`POST /api/admin/tiers`**
|
||||
|
||||
### Request Body
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `tier` | string | **Yes** | `premium` or `exclusive` |
|
||||
| `label` | string | **Yes** | Human-readable plan name |
|
||||
| `duration_days` | number | **Yes** | Access duration in days |
|
||||
| `price` | number | **Yes** | Plan price (decimal) |
|
||||
| `currency` | string | No | ISO 4217. Default: `USD` |
|
||||
|
||||
### Response `201`
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Plan created.",
|
||||
"data": { "plan_id": "5", "tier": "premium", ... }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Update Plan
|
||||
|
||||
**`PUT /api/admin/tiers/:id`**
|
||||
|
||||
Only `label`, `duration_days`, `price`, `currency`, and `is_active` are updatable.
|
||||
|
||||
### Response `200`
|
||||
```json
|
||||
{ "status": "success", "message": "Plan updated.", "data": { ... } }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Archive Plan
|
||||
|
||||
**`DELETE /api/admin/tiers/:id`**
|
||||
|
||||
Sets `is_active = false` then soft-deletes.
|
||||
|
||||
### Response `200`
|
||||
```json
|
||||
{ "status": "success", "message": "Plan archived successfully." }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Bulk Archive Plans
|
||||
|
||||
**`POST /api/admin/tiers/bulk/archive`**
|
||||
|
||||
### Request Body
|
||||
```json
|
||||
{ "ids": [1, 2] }
|
||||
```
|
||||
|
||||
### Response `200`
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "2 plan(s) archived successfully.",
|
||||
"archived_ids": [1, 2],
|
||||
"skipped_ids": []
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Restore Plan
|
||||
|
||||
**`POST /api/admin/tiers/:id/restore`**
|
||||
|
||||
Restores plan and sets `is_active = true`.
|
||||
|
||||
### Response `200`
|
||||
```json
|
||||
{ "status": "success", "message": "Plan restored successfully." }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Bulk Restore Plans
|
||||
|
||||
**`POST /api/admin/tiers/bulk/restore`**
|
||||
|
||||
### Request Body
|
||||
```json
|
||||
{ "ids": [1, 2] }
|
||||
```
|
||||
|
||||
### Response `200`
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "2 plan(s) restored successfully.",
|
||||
"restored_ids": [1, 2],
|
||||
"skipped_ids": []
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Plan Field Values
|
||||
|
||||
**`GET /api/admin/tiers/field-values`**
|
||||
|
||||
Returns distinct filterable field values for tier plans.
|
||||
|
||||
---
|
||||
|
||||
## Plan Courses
|
||||
|
||||
### Get Plan Courses
|
||||
|
||||
**`GET /api/admin/tiers/:id/courses`**
|
||||
|
||||
Returns the list of courses linked to this plan.
|
||||
|
||||
### Response `200`
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Plan courses retrieved.",
|
||||
"data": [
|
||||
{ "course_id": 1, "title": "Intro to Python", "course_code": "PY-101", "subscription": "premium", "level": "beginner" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Sync Plan Courses
|
||||
|
||||
**`POST /api/admin/tiers/:id/courses`**
|
||||
|
||||
Replaces the full set of courses for this plan. Removes all existing assignments first, then inserts the new set. A course can only belong to one plan at a time — existing assignments to other plans are cleared automatically.
|
||||
|
||||
### Request Body
|
||||
```json
|
||||
{ "course_ids": [1, 2, 3] }
|
||||
```
|
||||
|
||||
Pass `[]` to remove all courses from the plan.
|
||||
|
||||
### Response `200`
|
||||
```json
|
||||
{ "status": "success", "message": "Plan courses updated." }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## User Tiers
|
||||
|
||||
### Get User Tiers
|
||||
|
||||
**`GET /api/admin/tiers/users/:id/tiers`**
|
||||
|
||||
Returns the full tier history for a user, newest first. Includes `grantedByUser` and `revokedByUser` info.
|
||||
|
||||
### Response `200`
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "User tiers retrieved.",
|
||||
"data": [
|
||||
{
|
||||
"tier_id": 3,
|
||||
"user_id": 42,
|
||||
"tier": "premium",
|
||||
"status": "active",
|
||||
"starts_at": "2026-06-01T00:00:00.000Z",
|
||||
"expires_at": "2026-07-01T00:00:00.000Z",
|
||||
"granted_by": 1,
|
||||
"revoked_by": null,
|
||||
"revoked_at": null,
|
||||
"notes": "Trial promotion",
|
||||
"grantedByUser": { "user_id": 1, "email": "admin@example.com" },
|
||||
"revokedByUser": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Grant Tier
|
||||
|
||||
**`POST /api/admin/tiers/users/tiers/grant`**
|
||||
|
||||
- Expires all currently active tiers for the user before creating the new one.
|
||||
- `expires_at` is calculated as `now + plan.duration_days`.
|
||||
|
||||
### Request Body
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `user_id` | number | **Yes** | Target user |
|
||||
| `tier` | string | **Yes** | `premium` or `exclusive` |
|
||||
| `plan_id` | number | **Yes** | Must match the plan's tier |
|
||||
| `notes` | string | No | Optional admin note |
|
||||
|
||||
### Response `201`
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Tier granted.",
|
||||
"data": { "tier_id": 4, "user_id": 42, "tier": "premium", "status": "active", ... }
|
||||
}
|
||||
```
|
||||
|
||||
### Error Responses
|
||||
| Status | Message |
|
||||
|--------|---------|
|
||||
| `400` | `user_id, tier, and plan_id are required.` |
|
||||
| `400` | `Plan tier mismatch.` |
|
||||
| `404` | `User not found.` |
|
||||
| `404` | `Plan not found or inactive.` |
|
||||
|
||||
---
|
||||
|
||||
### Revoke Tier
|
||||
|
||||
**`PATCH /api/admin/tiers/users/tiers/:tid/revoke`**
|
||||
|
||||
- Sets the tier's status to `revoked` and records `revoked_by` / `revoked_at`.
|
||||
- Automatically creates a new `free` tier row for the user (auto-downgrade note: `"Auto-downgrade after revoke."`).
|
||||
|
||||
### Response `200`
|
||||
```json
|
||||
{ "status": "success", "message": "Tier revoked. User downgraded to free." }
|
||||
```
|
||||
|
||||
### Error Responses
|
||||
| Status | Message |
|
||||
|--------|---------|
|
||||
| `400` | `Tier is not active.` |
|
||||
| `404` | `Tier record not found.` |
|
||||
|
||||
---
|
||||
|
||||
## Payments
|
||||
|
||||
### Get All Payments
|
||||
|
||||
**`GET /api/admin/tiers/payments`**
|
||||
|
||||
Returns paginated payment records. Stale pending payments (>60 min old) are expired before the query runs.
|
||||
|
||||
### Query Parameters
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|--------|----------|-------------|
|
||||
| `page` | number | No | Default: `1` |
|
||||
| `limit` | number | No | Default: `10` |
|
||||
| `filters` | array | No | JSON filter array |
|
||||
|
||||
### Response `200`
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Payments retrieved.",
|
||||
"data": [
|
||||
{
|
||||
"payment_id": 1,
|
||||
"user_id": 42,
|
||||
"plan_id": 1,
|
||||
"status": "completed",
|
||||
"amount": "9.99",
|
||||
"currency": "USD",
|
||||
"promo_code": null,
|
||||
"discount": "0.00",
|
||||
"provider": "paypal",
|
||||
"paid_at": "2026-06-01T10:00:00.000Z",
|
||||
"user": { "user_id": 42, "email": "user@example.com" },
|
||||
"plan": { "plan_id": 1, "label": "Premium Monthly", "tier": "premium", "duration_days": 30 }
|
||||
}
|
||||
],
|
||||
"pagination": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Single Payment
|
||||
|
||||
**`GET /api/admin/tiers/payments/:id`**
|
||||
|
||||
Returns full payment detail including `user`, `plan`, and `tier` associations.
|
||||
|
||||
### Response `200`
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Payment retrieved.",
|
||||
"data": {
|
||||
"payment_id": 1,
|
||||
"provider_payload": { "order_id": "...", "capture_id": "...", ... },
|
||||
"user": { ... },
|
||||
"plan": { ... },
|
||||
"tier": { ... }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Payment Field Values
|
||||
|
||||
**`GET /api/admin/tiers/payments/field-values`**
|
||||
|
||||
Returns distinct filterable field values for payments. `provider_payload` is excluded.
|
||||
Reference in New Issue
Block a user