Files
starr-philproperties/controllers/admin/documentation/advertisements.md
T
kennethobsequio 1d12f04967 new commits
Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
2026-07-03 16:20:58 +08:00

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"]
}
}
```