Skip to content

Centres

Audience: domain, product, backend
Status: implemented
Owns: product + backend
Depends on: Domain entities, Personas, Lifecycle, API overview

  • Centres run RPL applications (review, payment rails, facilitator / panel assignment).
  • Centre record holds identity and payout details used before facilitator assignment (PAYMENT).
  • Platform admin owns the centres table UI: overview cards (GET /admin/centres/summaryactive = approved, pending = pending, inactive = suspended only, total = pending + approved + suspended, rejected excluded), enriched list (name, registrationNo, totals, status, joinedAt = createdAt, founding SUPER_ADMIN admin with person photo), newest-first (createdAt desc), search / status filter, single + bulk approve / deny / suspend, delete unused centres (no applications, retained assessors, job postings, or wallet activity), create centre, and read-only detail tabs (staff, assessors, candidates). Founding staff is cascaded on delete; reject/suspend keep the row.
  • Platform create (POST /admin/centres) creates the centre as approved, invites a SUPER_ADMIN via OL ProvisionAccount (name, registrationNo, admin first/last/email). Owner admin (name, email, optional phone, photoAssetId + resolved photo) is the earliest SUPER_ADMIN; missing owner → admin: null. Distinct from centre logo. Duplicate registrationNo409.
  • Self-serve org onboarding (variant: organisation) remains: on submit, CAP provisions a Centre (pending) and founder CentreStaff (super_admin, active). Repair path: deploy command repair:organisation-centres or re-submit — see Console commands.
  • Centre staff are invite-only (POST /centre/staff, or the platform create invite). A person may belong to multiple centres (CentreStaff unique on (capUserId, centreId)). One person-scoped staff onboarding (variant: staff) fulfills every centre. Assessors (retained or job-listing) are not staff.
  • Invite: CAP calls OL ProvisionAccount. Staff membership status starts pending until staff onboarding is completed; then active. Centre admins may bulk-set active/inactive.
  • Acting context: X-CAP-PERSONA: centre + X-CAP-CENTRE-ID. Role (including who may invite staff) is for that centre.
  • Centres publish job postings (centre/AB-initiated; assessors apply) — renamed from the old “assessor request” framing (§27).
  • Centres review incoming retained-assessor requests (approve/reject); an approved row is the retained relationship (no separate roster table). Total assessors on the centre dashboard is this approved-retained count.
  • Centre staff use a dashboard (GET /centre/dashboard) for KPIs and UTC-year charts. Revenue fields are omitted unless the caller has centre.wallet.read (centre super_admin).
  • Payments page: GET /centre/payments/summary (all-time net wallet credits + completed/pending checkout counts) and GET /centre/payments (searchable/sortable history). GET /centre/wallet is available-to-withdraw. POST /centre/wallet/withdraw is a ledger debit (not a bank payout). Receipt modal: GET /applications/{id}/receipt (snapshot + optional URL).
  • Staff activity is a CAP-local append-only log written in the same DB transaction as the mutation — not the cross-service Audit service (§14) and not HTTP access logs.
  • Directory (GET /directory) is people search. v1 is centre-scoped (staff, candidate, assessor = retained approved). Platform-admin / messaging a centre org id are deferred. Chat itself lives on Orchestrator conversations — CAP only supplies userIds.
  • Applications queue: GET /applications with X-CAP-PERSONA: centre (filters q, tradeId, type, status, stage, sort submittedAt; interviewSchedulable=true for RPL in-progress at folder arrangement or interview with no sitting yet; ivApproved=true for IV StageHistory APPROVED — those apps already sit at external_verification, so do not use stage=internal_verification; candidate.photo is the live profile photo; facilitator is the current assignment or null). After IV, centre may mint a share token (POST /applications/{id}/share-token; public dossier GET /shared/applications/{token}). When CAP_CENTRE_CERTIFY_BYPASS is on, PATCH /centre/applications/bulk { ids, action: "certify" } marks IV-approved in-progress apps CERTIFIED (synthetic EV/certification close; no Certificate row). Flag off → 404. Overview cards: GET /centre/applications/summary (pending = in_progress on application_form; ongoing = later stages; completed = certified; archived = rejected + withdrawn). Distinct from candidate GET /applications/summary.
  • Job listings: centre closes a posting (closed) so it leaves the marketplace. There is no “mark as filled”. Shortlist maps to accepted and occupies a slot; the server sets filled when acceptedCount >= slot. Delete only if there are zero applicants.
  • Assessor roster: the Assessors page is the retained relationship at this centre (GET /centre/assessors, default status=approved). Cards: GET /centre/assessors/summary (active = approved, pending = incoming requests, inactive = revoked; rejected stays on the inbox). Detail is read-only (GET /centre/assessors/{id} — AssessorProfile.id); assigned-candidates table is GET /centre/assessors/{id}/applications. There is no POST /centre/assessors — centres do not mint assessor accounts; the UI “New Assessor” action is job postings. Trade column = sectors[]. Centre cannot edit profile or delete qualifications. Revoke an approved row with PATCH /centre/retained-requests/{id}/revoke.
  • Interview templates: reusable panels (GET/POST /centre/panels) and sittings (GET/POST /centre/interviews, filter from/to/scheduledAt). Members include name, email, live photo. A panel is 3 voters plus an IV observer who may be one of those three (dual-role) or a fourth distinct IV. POST /centre/interviews/{id}/schedule copies a frozen snapshot onto one RPL application ({ applicationId } + optional sitting overrides) or many ({ items: [{ applicationId, scheduledAt }] }, max 25, all-or-nothing). Allowed at folder arrangement or interview. Each sitting must be on or after the template clock. Not a live-sync cohort. Bookings diary: GET/PATCH /centre/interview-bookings lists seated sittings (application + candidate + panel) and reschedules that application only. Ad-hoc POST /applications/{id}/interview/panel and …/schedule remain (same 3 voters + optional overlapping IV). Template edits do not move existing copies. Occupancy / double-book is not applied.
  • Last remaining centre super_admin may still delete the Orchestrator login; GET /me/deletion-eligibility returns last_centre_super_admin so the UI can warn. The centre is not deleted; platform may later assign an owner.
  • Personal contact/address/occupation/accessibility/photo: GET/PATCH /me/profile (identity field locks when verified). Notification channels and password/delete are Orchestrator routes.

GET /centre/dashboard?year=&applicationType=RPL|NSQ

  • Time filters and timestamps are UTC. Yearly series use submittedAt (drafts excluded).
  • KPIs: total applications, staff, assessors (approved retained), optional net revenue (gross application fee minus processing_fee_percentage).
  • Charts: revenue by month (super_admin only), applications by trade, gender distribution (closed set; unknown → unspecified; counts only), candidates by assessment stage (latest workflow template for the selected type; leftover keys → other).
  • staffActivity[] is the latest CAP activity rows for the centre.
  • The applications table stays on GET /applications with acting-persona scope (submittedAt, type, q, tradeId, stage, sort) — not a non-paginated embed. Staff detail tables use GET /centre/staff/{id}/applications. Assessor detail tables use GET /centre/assessors/{id}/applications (personal assignments at this centre). Overview cards use GET /centre/applications/summary.

GET /admin/dashboard?year=&tradeId= — platform staff (platform.orgs.read). Distinct from the centre dashboard.

  • Shared filters: UTC year (default current UTC year) and optional tradeId. Cohort is non-draft applications with submittedAt in that year (rejected/withdrawn included). Every KPI and chart uses that cohort.
  • Overview: distinct candidates; active assessments (composed assessment/IV/EV); certified candidates; distinct centres; approved assessors assigned (facilitator/panel/IV/EV) on the cohort; distinct awarding bodies (organizations); pending applications (application_form or no workflow); pending certifications (engine external_verification or certification).
  • Charts: NSQ vs RPL pie; candidates by qualification level (any positive int, max selected unit level, else NSQ induction qualificationLevelId); top trades by volume (count desc; missing trade → unknown, last among ties); monthly composite (application / approved / in assessment / certified); pipeline funnel in fixed order with atStage (current occupants) and reached (conversion). sum(atStage) may be less than pipeline.total.
  • Composed funnel (not engine catalogs): Application Registered → Centre Approved → Payment Completed → Assessment Started (RPL folder/interview, NSQ induction/regular assessment) → IV Sampled & Approved → EV Final Review → Certificate Issued. NSQ v2 uses the same form gate as RPL. In-flight NSQ v1 apps have no form gate — submitted NSQ v1 counts as centre-approved.

Recorded inside the domain transaction via recordCentreActivity. v1 actions: staff.invited, facilitator.assigned, application.reviewed, interview.scheduled, retained_request.decided. A generic Express access-log middleware is not the source of truth for this UI.

Catalogue (application draft):

Method Path Use
GET /centres Approved centres only (paginated) for centreId on create application; each row includes logoAssetId and resolved logo

Admin (platform staff — centres screens):

Method Path Use
GET /admin/dashboard Platform KPIs + charts (year, optional tradeId; pipeline atStage + reached)
GET /admin/centres/summary Cards: total / active / pending / inactive (rejected excluded from total)
GET /admin/centres Paginated enriched list (joinedAt, totalCandidates, totalApplications, totalAssessors, logoAssetId + resolved logo, founding SUPER_ADMIN admin or null; default createdAt desc then id desc; filter status, q on name/registrationNo)
POST /admin/centres Create approved centre + SUPER_ADMIN invite (name, registrationNo, admin); response includes resolved logo and owner admin
GET /admin/centres/{id} Detail + totalStaff + founding SUPER_ADMIN admin (with person photo) + resolved logo
PATCH /admin/centres/{id}/approve | reject | suspend Single lifecycle transition
DELETE /admin/centres/{id} Hard-delete unused centre (409 centre.in_use if applications, retained assessors, job postings, or wallet activity). Cascades staff / activity / pricing / empty wallet
PATCH /admin/centres/bulk { ids, status: approved | suspended | rejected } or { ids, action: "delete" } (all-or-nothing, same unused rule)
GET /admin/centres/{id}/staff (+ /{staffId}) Read-only staff tab (same shape as centre workspace)
GET /admin/centres/{id}/assessors (+ /{assessorId}) Read-only retained assessor roster
GET /admin/centres/{id}/applications (+ /{applicationId}) Read-only candidates tab (q, trade, type, status, stage; candidate.photo)

totalCandidates = distinct candidates with ≥1 application at the centre. totalApplications = application rows (all statuses, including drafts). Nested admin routes are read-only (no invite / revoke / mutate from platform tabs).

Writers: platform admin / platform staff for org create, status, and unused-centre delete. Centre-staff surfaces: dashboard, directory, staff, job postings, retained-request review, assignment/scheduling hooks, pricing (floor-validated), wallet/withdraw.

Method Path Use
GET /centre/dashboard Centre KPIs / charts / activity (revenue omitted without wallet read)
GET /directory Centre-scoped people search for chat participant pickers
GET /centre/staff List (status, q, sort createdAt)
GET /centre/staff/summary Counts: total / active / pending / inactive
POST /centre/staff Invite (name, email, role) — super_admin of acting centre
PATCH /centre/staff Bulk active | inactive
GET /centre/staff/{id} Detail + workload cards
GET /centre/staff/{id}/applications Applications table (q, trade, type, status, queue)
GET /centre/applications/summary Queue cards: total / pending / ongoing / completed / archived
PATCH /centre/applications/bulk { ids, action: "certify" } — hidden unless CAP_CENTRE_CERTIFY_BYPASS (404 otherwise); per-id updated/skipped/failed
GET / PATCH /centre/profile Identity, approval status, address, support, bank; GET nests founding-super-admin organisation onboarding and resolved logo next to logoAssetId; PATCH still takes logoAssetId (super_admin)
GET / POST / PUT /centre/pricing Latest fees; POST one type; PUT { items[] } one transaction; super_admin
GET /centre/payments / /centre/payments/summary Payments table + cards (super_admin)
GET / POST /centre/wallet / /wallet/withdraw Balance + ledger withdraw
GET /centre/assessors Roster (status default approved; qualification, q) — picker + Assessors page
GET /centre/assessors/summary Cards: total / active / pending / inactive (retained statuses)
GET /centre/assessors/{id} Detail + workload (read-only profile/certificates)
GET /centre/assessors/{id}/applications Assigned candidates (q, trade, type, status)
GET / POST /centre/job-postings List (occupancy) / publish
GET / PATCH / DELETE /centre/job-postings/{id} Get; close ({ status: "closed" }); delete if zero applicants
PATCH /centre/job-postings/bulk Close many
GET / PATCH /centre/job-postings/{id}/applications Applicant table; bulk { decision: shortlist | reject }
GET / PATCH /centre/job-postings/{id}/applications/{applicationId} Applicant detail; shortlist (accepted) or reject
GET /centre/retained-requests Incoming requests (status, q, sort requestedAt)
GET /centre/retained-requests/{id} Detail + assessor snapshot (sectors, certificates)
PATCH /centre/retained-requests/bulk { ids, decision: approve | reject } (pending only)
PATCH /centre/retained-requests/{id}/approve | reject Single pending decision
PATCH /centre/retained-requests/{id}/revoke Approved → revoked
GET / POST /centre/panels List / create reusable interview panel templates (3 voters + IV observer, who may be one of the three; members include name, email, photo)
GET / PATCH / DELETE /centre/panels/{id} Get / edit template; delete 409 if any interview template references it
GET / POST /centre/interviews List / create reusable sitting templates (from/to/scheduledAt on template clock; unscheduled omitted when filtered)
GET / PATCH / DELETE /centre/interviews/{id} Get / edit template only (does not update seated copies); delete 409 if applications hold centreInterviewId
PATCH /centre/interviews/bulk { ids, panelId } — template default panel only
POST /centre/interviews/{id}/schedule Copy frozen snapshot onto one RPL application ({ applicationId } + optional sitting overrides) or bulk { items } (max 25; sitting ≥ template clock)
GET /centre/interview-bookings Diary of seated sittings (from/to/scheduledAt/status/q); includes application, candidate, panel. Occupancy not applied
GET / PATCH /centre/interview-bookings/{id} Get / reschedule this application only (does not edit the template)

See Scalar — CAP REST (openapi/cap.yaml, lockstep with apps/cap/openapi/openapi.yaml) and API overview.