---
feature: Create course: venue, link, preview, discount, learning points, threshold start
slug: course-create
surface: trainer
consumer: flutter
version: 0.1.0
status: in-progress
brd_version: "v1.0 (11-8-2026) + additional requirements"
brd_ids: [TRR-009, TRR-010, TRR-011, TRR-012]
business_rules: [BR-CRS-01]
decisions: [D1, D3, DEC-17, TD-23, TD-25]
schema_tables: [courses, course_translations]
related_docs: [subscriptions.md, hall-bookings.md]
last_updated: 2026-11-15
---

# Create course: venue, link, preview, discount, learning points, threshold start — Trainer App Integration Guide

> **Base URL:** `{HOST}/api/v1/trainer` · Shared rules: [API_CONVENTIONS.md](../API_CONVENTIONS.md)

## Quick reference

| | |
|---|---|
| Version | 0.1.0 (2026-11-15) — drafts, validation and submit; **not built yet:** discount (TRR-013), learning points (TRR-014), start after minimum (TRR-015) |
| Status | in-progress · BRD TRR-009..012 · D1 (venue typed, no hall link), D3 (affiliated price is a proposal), DEC-17 |
| Auth | Trainer Bearer token, account `active`. |

| Method | Path | Route name | Purpose |
|--------|------|------------|---------|
| `GET` | `/courses` | `trainer.courses.index` | Own courses (`?status=`, `?per_page=`) |
| `POST` | `/courses` | `trainer.courses.store` | Create a draft |
| `GET` | `/courses/{course}` | `trainer.courses.show` | One course (own only, else 404) |
| `PUT` | `/courses/{course}` | `trainer.courses.update` | Edit a draft or rejected course |
| `POST` | `/courses/{course}/submit` | `trainer.courses.submit` | Send for review (independent trainers) |

**Statuses:** `draft` → `under_review` (submit) → `published` / `rejected` (admin, later) → `rejected` is edited and resubmitted. Only `draft` and `rejected` are editable (`can_edit`).
**Error codes:** `BUSINESS_RULE_VIOLATED` (422: validation rules, missing data at submit, no / expired / used-up package, affiliated trainer) · `INVALID_STATE_TRANSITION` (422).
**Open decisions:** None for what is built.

## 1. Business summary

An active trainer creates a course draft with title, description, category, city, date, start time, duration, free toggle, price, seats and type. An in-person course also carries the venue typed by hand (name, address, map link, notes; no hall link, D1); an external course carries its access link and instructions, which only the owner sees here and which never appear in public details. Submitting needs the required data complete and, for an independent trainer, a usable business model: commission, or an unexpired package with courses left ([subscriptions.md](subscriptions.md)). The trainer never publishes directly. An affiliated trainer's course belongs to the center and the price entered is stored as `proposed_price`; the center submits it (center surface, not built), so `can_submit` is `false` for them.

## 2. Endpoints

### 2.1 Create / edit — `POST /courses`, `PUT /courses/{course}`

| Field | Required on create | Validation |
|-------|--------------------|-----------|
| `type` | yes | `in_person` · `external` |
| `category_id`, `city_id` | yes | uuid of an active category / city |
| `name`, `description` | yes | string (max 255 / 10000); stored once for every language (TD-23) |
| `images` | no | ≤ 5 media hashes |
| `starts_on` | yes | `Y-m-d`, not in the past |
| `start_time` | yes | `H:i` |
| `duration_minutes` | yes | 15–1440; the end is derived (can pass midnight) |
| `is_free` | yes | boolean |
| `price` | unless free | > 0, 2 decimals; must be empty / 0 when free |
| `seats` | yes | integer ≥ 1 |
| `sessions_count` | no | 1–365, information only |
| `external_link` | when `external` | URL |
| `access_instructions` | no | text |
| `venue_name`, `venue_address`, `venue_map_url`, `venue_notes` | no (needed to submit an in-person course) | |

Edit accepts any subset; rules that depend on the saved draft are re-checked on the merged state. Changing the type clears the group that no longer applies.

```json
{"status":"success","message":"Created successfully","data":{"id":"a33aebf1-2426-49ab-afb7-ebf4027f93ca","code":"MKEGUD9PVYEZAPNPGMAN","status":{"value":"draft","label":"مسودة"},"type":{"value":"in_person","label":"حضوري"},"name":"Laravel for beginners","description":"A practical introduction","images":["https://ui-avatars.com/api/?name=N&background=f3f4f6&color=9ca3af&size=200"],"category":{"id":"a33aebf1-1f5a-487f-931d-40b49258a170","name":"تصنيف 822228"},"city":{"id":"a33aebf1-2092-42e7-a127-0cac66278a68","name":"مدينة 901291"},"is_free":false,"price":"350.00","proposed_price":null,"seats":25,"confirmed_count":0,"sessions_count":1,"starts_on":"2026-11-25","start_time":"18:30","duration_minutes":150,"ends_at":"2026-11-25T21:00:00+00:00","external_link":null,"access_instructions":null,"venue":{"name":"Riyadh Hall","address":"King Fahd Road","map_url":null,"notes":null},"rejection_reason":null,"points":null,"submitted_at":null,"can_edit":true,"can_submit":true,"created_at":"2026-11-15T09:23:38+00:00"}}
```

### 2.2 Submit — `POST /courses/{course}/submit`

```json
{"status":"success","message":"Your course has been submitted for review.","data":{"id":"a33aebf1-2426-49ab-afb7-ebf4027f93ca","code":"MKEGUD9PVYEZAPNPGMAN","status":{"value":"under_review","label":"قيد المراجعة"},"type":{"value":"in_person","label":"حضوري"},"name":"Laravel for beginners","description":"A practical introduction","images":["https://ui-avatars.com/api/?name=N&background=f3f4f6&color=9ca3af&size=200"],"category":{"id":"a33aebf1-1f5a-487f-931d-40b49258a170","name":"تصنيف 822228"},"city":{"id":"a33aebf1-2092-42e7-a127-0cac66278a68","name":"مدينة 901291"},"is_free":false,"price":"350.00","proposed_price":null,"seats":25,"confirmed_count":0,"sessions_count":1,"starts_on":"2026-11-25","start_time":"18:30","duration_minutes":150,"ends_at":"2026-11-25T21:00:00+00:00","external_link":null,"access_instructions":null,"venue":{"name":"Riyadh Hall","address":"King Fahd Road","map_url":null,"notes":null},"rejection_reason":null,"points":null,"submitted_at":"2026-11-15T09:23:38+00:00","can_edit":false,"can_submit":false,"created_at":"2026-11-15T09:23:38+00:00"}}
```

Submitting a course that is no longer a draft:

```json
{"status":"error","message":"This course cannot be submitted in its current status.","data":{"error_code":"INVALID_STATE_TRANSITION","correlation_id":"037176c6-9282-4d74-bd95-e3cb0b227930"}}
```

## 3. Business rules enforced

| Rule | Behaviour |
|------|-----------|
| TRR-009 | Only an active trainer; free → no price; seats > 0; date not in the past. |
| TRR-006 | Allowance counted at submit under the subscription row lock, so two submits cannot share the last slot; an expired package blocks submission and keeps existing courses. |
| TRR-012 | Every course is reviewed; required data must be complete (`BUSINESS_RULE_VIOLATED` names the missing items). |
| D3 | Affiliated trainer: `training_center_id` set, price is `proposed_price`, no submit. |

## 4. Known limitations

- Editing a published course (review of edits, TRR-018) and the admin review (ADM) are sprint 2.
- Discount, learning points and the threshold start mode are not built; `start_mode` is always `fixed_date`.

## Version history

| Version | Date | Type | Summary | App action required |
|---------|------|------|---------|---------------------|
| 0.1.0 | 2026-11-15 | Initial (partial) | Draft create / edit, validation, submit with plan enforcement. | Build create / edit / submit. |

## Changelog

### 0.1.0 — 2026-11-15
- **Added:** `courses` CRUD without delete, `courses/{id}/submit`; `CourseDetailsAreValid`, `CourseIsReadyForReview`, `TrainerHasCourseAllowance`.
- **Breaking:** No
- **App action required:** Build create / edit / submit.
- **BRD / decisions:** TRR-009..012, D1, D3, TD-25
