Centres
Centres
Section titled “Centres”Audience: domain, product, backend
Status: implemented
Owns: product + backend
Depends on: Domain entities, Personas, Lifecycle, API overview
Product intent
Section titled “Product intent”- 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/summary— active = approved, pending = pending, inactive = suspended only, total = pending + approved + suspended, rejected excluded), enriched list (name,registrationNo, totals, status,joinedAt=createdAt, founding SUPER_ADMINadminwith personphoto), newest-first (createdAtdesc), 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 asapproved, invites aSUPER_ADMINvia OLProvisionAccount(name,registrationNo, admin first/last/email). Owneradmin(name, email, optional phone,photoAssetId+ resolvedphoto) is the earliest SUPER_ADMIN; missing owner →admin: null. Distinct from centrelogo. DuplicateregistrationNo→ 409. - Self-serve org onboarding (
variant: organisation) remains: on submit, CAP provisions aCentre(pending) and founderCentreStaff(super_admin, active). Repair path: deploy commandrepair:organisation-centresor 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 (CentreStaffunique 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 startspendinguntil staff onboarding is completed; thenactive. Centre admins may bulk-setactive/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 hascentre.wallet.read(centresuper_admin). - Payments page:
GET /centre/payments/summary(all-time net wallet credits + completed/pending checkout counts) andGET /centre/payments(searchable/sortable history).GET /centre/walletis available-to-withdraw.POST /centre/wallet/withdrawis 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 suppliesuserIds. - Applications queue:
GET /applicationswithX-CAP-PERSONA: centre(filtersq,tradeId,type,status,stage, sortsubmittedAt;interviewSchedulable=truefor RPL in-progress at folder arrangement or interview with no sitting yet;ivApproved=truefor IV StageHistory APPROVED — those apps already sit atexternal_verification, so do not usestage=internal_verification;candidate.photois the live profile photo;facilitatoris the current assignment or null). After IV, centre may mint a share token (POST /applications/{id}/share-token; public dossierGET /shared/applications/{token}). WhenCAP_CENTRE_CERTIFY_BYPASSis on,PATCH /centre/applications/bulk{ ids, action: "certify" }marks IV-approved in-progress appsCERTIFIED(synthetic EV/certification close; no Certificate row). Flag off → 404. Overview cards:GET /centre/applications/summary(pending= in_progress onapplication_form;ongoing= later stages;completed= certified;archived= rejected + withdrawn). Distinct from candidateGET /applications/summary. - Job listings: centre closes a posting (
closed) so it leaves the marketplace. There is no “mark as filled”. Shortlist maps toacceptedand occupies a slot; the server setsfilledwhenacceptedCount >= slot. Delete only if there are zero applicants. - Assessor roster: the Assessors page is the retained relationship at this centre (
GET /centre/assessors, defaultstatus=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 isGET /centre/assessors/{id}/applications. There is noPOST /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 withPATCH /centre/retained-requests/{id}/revoke. - Interview templates: reusable panels (
GET/POST/centre/panels) and sittings (GET/POST/centre/interviews, filterfrom/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}/schedulecopies 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-bookingslists seated sittings (application + candidate + panel) and reschedules that application only. Ad-hocPOST /applications/{id}/interview/paneland…/scheduleremain (same 3 voters + optional overlapping IV). Template edits do not move existing copies. Occupancy / double-book is not applied. - Last remaining centre
super_adminmay still delete the Orchestrator login;GET /me/deletion-eligibilityreturnslast_centre_super_adminso 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.
Centre dashboard
Section titled “Centre dashboard”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 /applicationswith acting-persona scope (submittedAt,type,q,tradeId,stage, sort) — not a non-paginated embed. Staff detail tables useGET /centre/staff/{id}/applications. Assessor detail tables useGET /centre/assessors/{id}/applications(personal assignments at this centre). Overview cards useGET /centre/applications/summary.
Platform admin dashboard
Section titled “Platform admin dashboard”GET /admin/dashboard?year=&tradeId= — platform staff (platform.orgs.read). Distinct from the centre dashboard.
- Shared filters: UTC
year(default current UTC year) and optionaltradeId. Cohort is non-draft applications withsubmittedAtin 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_formor no workflow); pending certifications (engineexternal_verificationorcertification). - 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 withatStage(current occupants) andreached(conversion).sum(atStage)may be less thanpipeline.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.
Staff activity (CAP-local)
Section titled “Staff activity (CAP-local)”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.
API (v1)
Section titled “API (v1)”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.
See also
Section titled “See also”- Flows → Experience — Centre
- Payment → Payment module
- RBAC → RBAC matrix