# Products Controller Documentation **File:** `controllers/admin/products.controller.js` **Base URL:** `/api/admin/products` **Guards:** `authenticate → requireAdmin() → adminLimiter` --- ## Table of Contents - [Get Course Product](#get-course-product) - [Upsert Course Product](#upsert-course-product) - [Remove Course Product](#remove-course-product) - [Get Course Categories](#get-course-categories) - [Sync Course Categories](#sync-course-categories) --- ## Notes - Each course has **at most one** product listing (`products` table, unique on `course_id`). - Upsert restores a soft-deleted product if one exists rather than creating a duplicate. - Categories are managed via the `course_product_categories` junction table (many-to-many between `courses` and `categories`). - Syncing categories replaces the full set — it is a replace-all, not an append. --- ## Get Course Product **`GET /api/admin/products/courses/:courseId/product`** Returns the product listing for a course, or `null` if none exists. Includes archived products (`paranoid: false`). ### Response `200` ```json { "status": "success", "message": "Product retrieved.", "data": { "id": 1, "course_id": 5, "name": "Advanced JavaScript", "description": "Full course access", "price": "49.99", "currency": "USD", "access_days": 365, "is_active": true, "createdAt": "2026-01-01T00:00:00.000Z", "updatedAt": "2026-01-01T00:00:00.000Z", "deletedAt": null } } ``` --- ## Upsert Course Product **`PUT /api/admin/products/courses/:courseId/product`** Creates the product if it does not exist. If a soft-deleted product exists, it is restored and updated. If an active product exists, it is updated. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | string | **Yes** | Product name | | `price` | number | **Yes** | Price (decimal, e.g. `49.99`) | | `description` | string | No | | | `currency` | string | No | ISO 4217 code. Default: `USD` | | `access_days` | number | No | Days of access after purchase. `null` = lifetime | | `is_active` | boolean | No | Default: `true` | ### Response `200` (updated) / `201` (created) ```json { "status": "success", "message": "Product updated.", "data": { ... } } ``` ### Error Responses | Status | Message | |--------|---------| | `400` | `name and price are required.` | --- ## Remove Course Product **`DELETE /api/admin/products/courses/:courseId/product`** Soft-deletes the course product. ### Response `200` ```json { "status": "success", "message": "Product removed." } ``` ### Error Responses | Status | Message | |--------|---------| | `404` | `Product not found.` | --- ## Get Course Categories **`GET /api/admin/products/courses/:courseId/categories`** Returns the list of categories assigned to a course. ### Response `200` ```json { "status": "success", "message": "Course categories retrieved.", "data": [ { "id": 1, "name": "Business", "slug": "business", "is_active": true }, { "id": 3, "name": "Design", "slug": "design", "is_active": true } ] } ``` ### Error Responses | Status | Message | |--------|---------| | `404` | `Course not found.` | --- ## Sync Course Categories **`POST /api/admin/products/courses/:courseId/categories`** Replaces the full set of category assignments for a course. All existing assignments are removed first, then the new set is inserted. ### Request Body ```json { "category_ids": [1, 3, 7] } ``` Pass an empty array to clear all category assignments. ### Response `200` ```json { "status": "success", "message": "Course categories updated." } ```