mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
311 lines
8.1 KiB
Markdown
311 lines
8.1 KiB
Markdown
# Advertisements Controller Documentation
|
|
|
|
**File:** `controllers/admin/advertisements.controller.js`
|
|
**Base URL:** `/api/admin/advertisements`
|
|
**Guards:** `authenticate → requireAdmin() → adminLimiter`
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
- [Get All Advertisements](#get-all-advertisements)
|
|
- [Get Single Advertisement](#get-single-advertisement)
|
|
- [Create Advertisement](#create-advertisement)
|
|
- [Update Advertisement](#update-advertisement)
|
|
- [Archive Advertisement](#archive-advertisement)
|
|
- [Bulk Archive Advertisements](#bulk-archive-advertisements)
|
|
- [Restore Advertisement](#restore-advertisement)
|
|
- [Bulk Restore Advertisements](#bulk-restore-advertisements)
|
|
- [Get Archived Advertisements](#get-archived-advertisements)
|
|
- [Get Field Values](#get-field-values)
|
|
|
|
---
|
|
|
|
## Placement Registry
|
|
|
|
Every advertisement belongs to a `placement` — a page + position slug drawn from a fixed
|
|
registry (`models/advertisements/advertisements.placements.js`). The placement determines
|
|
the advertisement's `type` (visual format) automatically; `type` is **never** accepted from
|
|
the client and is denormalized from the placement on every write.
|
|
|
|
| Placement key | Page | Position | Format |
|
|
|---|---|---|---|
|
|
| `dashboard.hero` | Dashboard | Hero (top of page) | `hero` |
|
|
| `dashboard.popup` | Dashboard | Popup (on load) | `popup` |
|
|
| `course_list.banner` | Courses | Banner (above course grid) | `banner` |
|
|
| `course_details.banner` | Course Details | Banner (below hero) | `banner` |
|
|
| `course_details.sidebar` | Course Details | Sidebar (beside course content) | `sidebar` |
|
|
| `plans.banner` | Plans | Banner (above plan cards) | `banner` |
|
|
|
|
Adding a new placement is a one-line addition to that registry file plus wiring the
|
|
corresponding client page to fetch/render it — nothing else needs to change.
|
|
|
|
---
|
|
|
|
## Status Derivation
|
|
|
|
Status is **never** trusted as stored — it is recomputed on every read and write:
|
|
|
|
| Condition | Derived Status |
|
|
|-----------|----------------|
|
|
| `deletedAt` is set | `archived` |
|
|
| `is_active = false` | `draft` |
|
|
| `end_date` < now | `expired` |
|
|
| `start_date` > now | `scheduled` |
|
|
| Otherwise | `active` |
|
|
|
|
`archived` is the only status that bypasses derivation (set explicitly by archive/restore).
|
|
|
|
---
|
|
|
|
## CTAs
|
|
|
|
Each advertisement supports up to **2 CTAs**:
|
|
|
|
```json
|
|
[
|
|
{ "label": "Learn More", "link": "/courses", "variant": "default" },
|
|
{ "label": "Sign Up", "link": "/register", "variant": "outline" }
|
|
]
|
|
```
|
|
|
|
- First CTA defaults to `"default"` variant; second defaults to `"outline"`.
|
|
- An explicit valid variant from the client (`"default"` or `"outline"`) always wins.
|
|
- Items beyond 2 are silently discarded.
|
|
|
|
---
|
|
|
|
## Get All Advertisements
|
|
|
|
**`GET /api/admin/advertisements`**
|
|
|
|
Returns a paginated list of active (non-deleted) advertisements. Status is resynced on the way out.
|
|
|
|
### Query Parameters
|
|
| Parameter | Type | Required | Description |
|
|
|-----------|--------|----------|-------------|
|
|
| page | number | No | Default: `1` |
|
|
| limit | number | No | Default: `10`, max: `1000` |
|
|
| filters | array | No | JSON array of filter objects |
|
|
| sort | array | No | JSON array of sort objects |
|
|
|
|
### Response `200`
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"message": "Advertisements retrieved.",
|
|
"data": [...],
|
|
"pagination": { "page": 1, "limit": 10, "total": 5, "totalPages": 1 }
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Get Single Advertisement
|
|
|
|
**`GET /api/admin/advertisements/:advertisementId`**
|
|
|
|
Returns one advertisement with its `image` asset and audit user info.
|
|
|
|
### Response `200`
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"message": "Advertisement retrieved.",
|
|
"data": {
|
|
"advertisement_id": 1,
|
|
"uuid": "...",
|
|
"placement": "dashboard.hero",
|
|
"type": "hero",
|
|
"status": "active",
|
|
"badge_label": "New",
|
|
"headline": "Welcome",
|
|
"description": "...",
|
|
"image_url": null,
|
|
"image_asset_id": 12,
|
|
"image": { "asset_id": 12, "display_name": "hero.jpg", "file_url": "...", "thumbnail_url": "..." },
|
|
"ctas": [{ "label": "Start", "link": "/start", "variant": "default" }],
|
|
"start_date": "2026-01-01T00:00:00.000Z",
|
|
"end_date": null,
|
|
"order": 0,
|
|
"is_active": true,
|
|
"size": null,
|
|
"click_count": 0,
|
|
"creator": { "user_id": 1, "full_name": "Admin User" },
|
|
"updater": null,
|
|
"createdAt": "2026-01-01T00:00:00.000Z",
|
|
"updatedAt": "2026-01-01T00:00:00.000Z"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Error Responses
|
|
| Status | Message |
|
|
|--------|---------|
|
|
| `400` | `Invalid advertisement ID.` |
|
|
| `404` | `Advertisement not found.` |
|
|
|
|
---
|
|
|
|
## Create Advertisement
|
|
|
|
**`POST /api/admin/advertisements`**
|
|
|
|
### Request Body
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| `placement` | string | **Yes** | A placement registry key, e.g. `dashboard.hero` — see [Placement Registry](#placement-registry). Determines `type` automatically. |
|
|
| `createdBy` | number | **Yes** | User ID of creator |
|
|
| `badge_label` | string | No | Small label shown on the ad |
|
|
| `headline` | string | No | Main heading |
|
|
| `description` | string | No | Body text |
|
|
| `image_url` | string | No | Direct image URL |
|
|
| `image_asset_id` | number | No | FK to `assets` table |
|
|
| `ctas` | array | No | Up to 2 CTA objects `[{ label, link, variant }]` |
|
|
| `start_date` | date | No | ISO date string |
|
|
| `end_date` | date | No | ISO date string |
|
|
| `order` | number | No | Display order. Default: `0` |
|
|
| `is_active` | boolean | No | Default: `true` |
|
|
| `size` | string | No | `sm`, `md`, `lg` — banner only |
|
|
|
|
### Response `201`
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"message": "Advertisement created.",
|
|
"data": { ... }
|
|
}
|
|
```
|
|
|
|
### Error Responses
|
|
| Status | Message |
|
|
|--------|---------|
|
|
| `400` | `placement is required.` |
|
|
| `400` | `Invalid placement. Must be one of: dashboard.hero, dashboard.popup, ...` |
|
|
| `400` | `createdBy is required.` |
|
|
| `400` | `Invalid size. Must be one of: sm, md, lg` |
|
|
|
|
---
|
|
|
|
## Update Advertisement
|
|
|
|
**`PATCH /api/admin/advertisements/:advertisementId`**
|
|
|
|
Partial update. Only fields present in the body are changed. Status is recomputed after all fields are applied.
|
|
|
|
### Request Body
|
|
Same optional fields as Create. `type` is never accepted — it's always derived from `placement`. Accepts `updatedBy`.
|
|
|
|
### Response `200`
|
|
```json
|
|
{ "status": "success", "message": "Advertisement updated.", "data": { ... } }
|
|
```
|
|
|
|
---
|
|
|
|
## Archive Advertisement
|
|
|
|
**`DELETE /api/admin/advertisements/:advertisementId`**
|
|
|
|
Soft-deletes the advertisement (`deletedAt` set, `status` → `archived`).
|
|
|
|
### Response `200`
|
|
```json
|
|
{ "status": "success", "message": "Advertisement archived." }
|
|
```
|
|
|
|
---
|
|
|
|
## Bulk Archive Advertisements
|
|
|
|
**`DELETE /api/admin/advertisements/bulk`**
|
|
|
|
Rate-limited. Soft-deletes multiple advertisements.
|
|
|
|
### Request Body
|
|
```json
|
|
{ "ids": [1, 2, 3], "deletedBy": 1 }
|
|
```
|
|
|
|
### Response `200`
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"message": "3 advertisement(s) archived.",
|
|
"archived_ids": [1, 2, 3],
|
|
"skipped_ids": []
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Restore Advertisement
|
|
|
|
**`PATCH /api/admin/advertisements/:advertisementId/restore`**
|
|
|
|
Restores a soft-deleted advertisement.
|
|
|
|
### Response `200`
|
|
```json
|
|
{ "status": "success", "message": "Advertisement restored.", "data": { ... } }
|
|
```
|
|
|
|
---
|
|
|
|
## Bulk Restore Advertisements
|
|
|
|
**`PATCH /api/admin/advertisements/bulk-restore`**
|
|
|
|
### Request Body
|
|
```json
|
|
{ "ids": [1, 2] }
|
|
```
|
|
|
|
### Response `200`
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"message": "2 advertisement(s) restored.",
|
|
"restored_ids": [1, 2],
|
|
"skipped_ids": []
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Get Archived Advertisements
|
|
|
|
**`GET /api/admin/advertisements/archived`**
|
|
|
|
Returns paginated list of soft-deleted advertisements.
|
|
|
|
### Response `200`
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"message": "Archived advertisements retrieved.",
|
|
"data": [...],
|
|
"pagination": { ... }
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Get Field Values
|
|
|
|
**`GET /api/admin/advertisements/field-values`**
|
|
|
|
Returns distinct values for filterable advertisement fields. Used by DataTable filter dropdowns.
|
|
|
|
### Response `200`
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"message": "Field values retrieved.",
|
|
"data": {
|
|
"type": ["hero", "banner", "popup", "sidebar"],
|
|
"placement": ["dashboard.hero", "dashboard.popup", "course_list.banner"],
|
|
"status": ["active", "draft"]
|
|
}
|
|
}
|
|
```
|