Trades and units
Trades & units (NOS)
Section titled “Trades & units (NOS)”Audience: domain, product, backend
Status: implemented
Owns: product + backend
Depends on: Domain entities, Lifecycle, API overview
Product intent
Section titled “Product intent”- Applications target a Trade and related Units from NOS documents.
- Concurrent application policy: one active non-draft path at a time (see lifecycle and policies).
- Centres review applications in the context of the selected trade/units.
- Each trade has one active NOS document; superseded versions remain for admin history.
- Platform admin UI treats the active NOS as the editable document (version switching is not exposed in v1 admin screens). Mutating levels/units/LOs and trade NOS top-level fields always resolve (or create) the active NOS.
- Trade soft-archive (
status: active | archived) backs bulk “delete” in the UI. Sector hard-delete is allowed only when the sector has no trades. - UI “Title” maps to API
name. There is notradeCode— use auto-generatedslugas the stable identifier. Purpose and objectives live on qualification levels, not on the trade. - Platform admin can list the catalogue as one row per qualification level on each trade’s active NOS (
GET /admin/levels) so a table can look like trades whileunitCountis per level. Rowidis the level id; navigation and bulk archive still usetradeId.GET /admin/tradesremains one row per trade.
v1 model (enough to build)
Section titled “v1 model (enough to build)”| Entity | Attributes (v1) | Relationship |
|---|---|---|
| Sector | id, slug (auto from name), name, description? |
Has many Trades |
| Trade | id, sectorId, slug (auto from name), name, description?, status (active | archived) |
Has many NOS versions; one active |
| NosDocument | version, title, issuedBy, issuedDate, status (active | superseded) |
Belongs to Trade; owns qualification levels |
| QualificationLevel | level (positive int, commonly 1–3), slug, purpose, objectives[], requirements[] |
Under active/superseded NOS; owns Units |
| Unit | referenceNumber, title, isMandatory, credits/GLH, structure (LO → PC tree), … |
Under a qualification level |
Learning outcomes are not a separate table — they live in Unit.structure.learningObjectives[].
Catalogue (application draft — authenticated, not /admin):
| Method | Path | Use |
|---|---|---|
GET |
/sectors |
Sector picker (unpaginated); each row includes tradeCount (active trades only) |
GET |
/sectors/{id}/trades |
Trade picker (active trades only) |
GET |
/trades/{id} |
Trade + active NOS tree (levels → units); 404 if archived or no active NOS |
GET |
/trades/{id}/units |
Flat unit list for POST /applications (?level= optional) |
GET |
/trades/{id}/evidence-types |
Union of unit assessmentMethods for Evidence Vault |
Admin (platform staff — platform.catalogue.read / .write):
| Method | Path | Use |
|---|---|---|
GET |
/admin/sectors |
Paginated list (cursor, limit, q, sort name|slug, order); each row includes tradeCount (active + archived) |
POST |
/admin/sectors |
Create (name, description?); slug auto-generated |
PATCH |
/admin/sectors/{id} |
Update name/description (rename regenerates slug) |
DELETE |
/admin/sectors/{id} |
Hard-delete; 409 catalogue.sector_has_trades if any trades exist |
PATCH |
/admin/sectors/bulk |
{ ids, action: "delete" } — all-or-nothing; same block if any have trades |
GET |
/admin/trades |
Paginated enriched list (sector, levelCount, unitCount; filter sectorId, q, includeArchived) |
GET |
/admin/levels |
Paginated flattened list: one row per qualification level on each trade’s active NOS (tradeId, trade name/slug/status/sector, level, per-level unitCount). id is the level id — not for bulk archive. Filter sectorId, level, q, includeArchived |
POST |
/admin/trades |
Create (name, sectorId, description?, levelCount positive int) → trade + active NOS v1 + stub levels |
GET |
/admin/trades/{id} |
Admin detail; active NOS optional |
PATCH |
/admin/trades/{id} |
Trade fields and/or active NOS top-level (nosTitle, issuedBy, issuedDate) |
PATCH |
/admin/trades/bulk |
{ ids, action: "archive" } — soft-delete |
GET/POST |
/admin/trades/{tradeId}/levels |
List / add level on active NOS (ensureActiveNos on create) |
PATCH/DELETE |
/admin/levels/{id} |
Update / delete level; 409 catalogue.level_in_use if any unit has ApplicationUnit refs |
GET/POST |
/admin/levels/{levelId}/units |
List / create unit (full unit body incl. LO structure) |
GET/PATCH/DELETE |
/admin/units/{id} |
Unit detail / patch / delete; 409 catalogue.unit_in_use if referenced |
POST |
/admin/units/{id}/learning-outcomes |
Append LO to structure |
PATCH/DELETE |
/admin/units/{id}/learning-outcomes/{code} |
Replace / remove LO; 422 if removing the last LO |
POST |
/admin/nos-documents |
Bulk NOS import (supersedes prior active; empty stub at same version may be replaced) |
GET |
/admin/nos-documents/{id} |
Any NOS version with full tree |
POST /admin/sectors/{id}/trades remains as a deprecated alias of POST /admin/trades (sector from path). Prefer the global create path.
See openapi/cap.yaml / Scalar — CAP REST.
See also
Section titled “See also”- Entities Trade, Unit, Application → Domain entities
- Candidate draft/submit → Experience flows — application
- API group → API overview
- Data model → CAP data model