Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
8.1 KiB
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 Single Advertisement
- Create Advertisement
- Update Advertisement
- Archive Advertisement
- Bulk Archive Advertisements
- Restore Advertisement
- Bulk Restore Advertisements
- Get Archived Advertisements
- 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:
[
{ "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
{
"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
{
"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. 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
{
"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
{ "status": "success", "message": "Advertisement updated.", "data": { ... } }
Archive Advertisement
DELETE /api/admin/advertisements/:advertisementId
Soft-deletes the advertisement (deletedAt set, status → archived).
Response 200
{ "status": "success", "message": "Advertisement archived." }
Bulk Archive Advertisements
DELETE /api/admin/advertisements/bulk
Rate-limited. Soft-deletes multiple advertisements.
Request Body
{ "ids": [1, 2, 3], "deletedBy": 1 }
Response 200
{
"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
{ "status": "success", "message": "Advertisement restored.", "data": { ... } }
Bulk Restore Advertisements
PATCH /api/admin/advertisements/bulk-restore
Request Body
{ "ids": [1, 2] }
Response 200
{
"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
{
"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
{
"status": "success",
"message": "Field values retrieved.",
"data": {
"type": ["hero", "banner", "popup", "sidebar"],
"placement": ["dashboard.hero", "dashboard.popup", "course_list.banner"],
"status": ["active", "draft"]
}
}