Spec 02 — API Contracts (NestJS, REST /api/v1)
Conventions: JSON; DTO validation (class-validator), strict — unknown properties are rejected, not stripped; request bodies are size-capped; errors { statusCode, code, message, messageAr }; pagination ?page=1&limit=20 → { items, total, page, limit }; auth via httpOnly cookies nutqi_at / nutqi_rt; guard decorators @Roles(...); Swagger annotated and served only when explicitly enabled in production.
Read paths return flat rows (D-26): names resolved to a single string, calendar dates as YYYY-MM-DD, avatars as ready-to-use URLs, counters computed server-side, relations dropped — the database layout never leaks to a client.
Rate limiting is per IP, with a much tighter budget on /auth/login and the OTP routes; it complements (does not replace) the account lockout of G15. JWT_ACCESS_SECRET and CORS_ORIGIN are required in production — the app refuses to boot rather than fall back to an insecure default.
health module#
GET /health(public, unauthenticated) → liveness plus a real database check. Deliberately free of version numbers, connection strings and error text; consumed by the reverse proxy and the container healthcheck.
auth module#
POST /auth/register{ role: PERSONAL_PATIENT|GUARDIAN|SPECIALIST|CENTER, firstName, lastName, email, phone, password, gender?, birthDate?, center? {nameAr, licenseNumber?, governorate?, city?, branchName?} } → creates user (status PENDING_VERIFICATION) (+Center+first Branch when role CENTER per G03); sends email+phone OTP (console provider in dev). 409 on dup email/phone.POST /auth/verify-otp{ target, code, purpose: "verify" | "reset" } → on verify: marks verified, status → ACTIVE (patient/guardian) or PENDING_ACTIVATION (specialist/center); sets cookies. On reset: returns one-time resetToken.POST /auth/resend-otp{ target, channel? } — throttled 60s (matches UI countdown).POST /auth/login{ email, password } → cookies; wrong password increments counter; locked → 423 withlockedUntil(G15).POST /auth/forgot{ email, phone } → OTP (purpose reset).POST /auth/reset-password{ resetToken, password }.POST /auth/refresh,POST /auth/logout.GET /auth/me→ user + role-specific profile summary + activation completeness % (drives the gate ring).PATCH /auth/password{ current, next } (settings).
users module#
PATCH /users/me(names ar/en, nationalId, DOB, gender, avatar, locale, address fields).PUT /users/me/languages[{language, level}] (spoken languages, G13-b).GET/PUT /users/me/notification-channels[{channel, target, enabled}];POST /users/me/notification-channels/:channel/verify(OTP flow).
patients module (guardian portal)#
GET /patients(mine) /POST /patients(add child, G09) /GET|PATCH /patients/:id.POST /patients/:id/documents(multipart, kind DOC|VIDEO) /GET /patients/:id/documents/DELETE …/:docId.- Diagnosis:
GET /forms/diagnosis-template→ system template;POST /patients/:id/diagnosis-response(creates/updates response via answers API below).
specialists module#
GET /specialists— public directory (G12): filters q, governorate, specialty, sessionType, priceMin/Max, rating; only ACTIVE.GET /specialists/:id/profile→ overview payload (stats: bookings timeline, type distribution, top programs — computed), data (certificates, trainings, videos), clinics, reviews summary.GET /specialists/:id/schedule?weekStart=→ sessions grid + prices;GET /specialists/:id/slots?date=&clinicId?=→ available 30-min slots (work hours − bookings − days off) (D-09).- Me-scoped:
GET/PUT /specialists/me/profile,POST/PATCH/DELETE /specialists/me/work-info,…/certificates,…/videos,…/clinics,PUT /specialists/me/schedule(weekday blocks),POST /specialists/me/days-off. PUT /specialists/me/booking-settings{ availableForWork, acceptsOnline, acceptsOffline, acceptsConsultation }.GET/POST/DELETE /specialists/me/blocklist.GET /specialists/me/stats?period=all|year|month|week|day→ { dailyAvgCases, totalCases, earningsCents, pending: bool }.
bookings module#
POST /bookings{ patientId, specialistId, clinicId?|type ONLINE, sessionType, date, startTime } → PENDING; validates slot free, specialist active+available, not blocked; price resolved server-side. 409 slot taken.GET /bookingsrole-scoped list w/ filters (status, type, q, dateRange) +?export=csv.PATCH /bookings/:id/status{ status, meetingUrl?, newDate?/newTime? for POSTPONED } — allowed transitions per state machine (spec docs-content/02 §5); guardian may CANCEL own PENDING/WAITING.GET /bookings/upcoming→ next booking for banner (E10).
sessions module#
POST /sessions(from booking or special case) /PATCH /sessions/:id{ progressPercent, evaluation, notes }.GET /patients/:id/sessions,GET /special-cases/:id/sessions.
special-cases module (specialist)#
- CRUD
/special-cases; attachments/special-cases/:id/attachments(phase BEFORE|AFTER); notes CRUD (soft delete + undo window G18).
forms module (plans & assessments engine)#
- Templates:
GET/POST /forms/templates(mine),GET/PATCH/DELETE /forms/templates/:id(+pages/questions nested payload, versioned). - Assign:
POST /forms/assignments{ templateId, patientId|specialCaseId, dueDate? } → notification to guardian. GET /forms/assignments?role=guardian|specialist&status=→ cards data (Not answered / Answered).- Answer:
GET /forms/assignments/:id/response(or create),PUT /forms/responses/:id/answers/:questionId{ value } — autosave upsert (E01),POST /forms/responses/:id/submit→ validates required → assignment ANSWERED. - Results:
GET /forms/assignments/:id/result(Q + A read view).
reviews module#
POST /reviews{ specialistId, bookingId?, stars, text, kind } (author must have DONE booking with specialist for SESSION kind).GET /specialists/:id/reviews?kind=&period=;PATCH /reviews/:id/like,PATCH /reviews/:id/reply(specialist),POST /reviews/:id/report(G17).
payments module#
GET /payments/mine(guardian ledger, G01) → rows + totals.POST /bookings/:id/payment{ method, status } (secretary/specialist records; creates wallet EARNING txn on PAID).GET /wallet(specialist) → { balanceCents, withdrawnCents };GET /wallet/transactions.POST /wallet/withdrawals{ amountCents, method, target } (G19);GET /wallet/withdrawals.
centers module#
GET/PATCH /centers/me(owner/manager); branches CRUD/centers/me/branches.- Staff:
GET/POST /centers/me/staff(create staff user w/ role+branch+salary),PATCH/DELETE /centers/me/staff/:id. - Patients of center:
GET /centers/me/patients(+profile passthroughs).
hr module (center)#
POST /hr/attendance/clock-in|clock-out(self, CENTER_SPECIALIST/SECRETARY) — branchId optional (G20).GET /hr/attendance?staffId?&range(owner/manager see all; staff self).- CRUD-ish:
/hr/absences,/hr/overtime,/hr/penalties(with deductionCents), owner/manager only. - Requests:
POST /hr/requests(staff),GET /hr/requests(scoped),PATCH /hr/requests/:id/decision{ status: APPROVED|REJECTED } (owner/manager) → notification.
jobs module#
- Center:
GET/POST /jobs/postings,PATCH /jobs/postings/:id(draft→published→closed), applications:GET /jobs/postings/:id/applications,PATCH /jobs/applications/:id/decision→ on ACCEPTED offers CenterStaff creation payload. - Specialist:
GET /jobs/market(published, filters, G08),POST /jobs/postings/:id/apply,GET /jobs/applications/mine.
notifications module#
GET /notifications?unread=,PATCH /notifications/:id/read,PATCH /notifications/read-all.GET /notifications/stream— SSE (E13).- Emitter service used by other modules; dispatches IN_APP always + enabled verified channels (email via console/dev SMTP; WhatsApp/Telegram = interface stubs logging payloads, D-17).
files module#
POST /filesmultipart (auth) → StoredFile;GET /files/:id(authz by ownership/linkage); size/type limits (images 5MB, docs 10MB, video 100MB).
admin module (API-only v1)#
GET /admin/activations(pending specialists/centers),PATCH /admin/activations/:userId{ approve|reject }.GET /admin/withdrawals,PATCH /admin/withdrawals/:id{ TRANSFERRED|REJECTED }.GET /admin/reported-reviews.
Events → notifications (minimum)#
booking.created (→specialist/secretary), booking.status_changed (→guardian), assignment.created (→guardian), assignment.answered (→specialist), application.decided (→specialist), hr.request.decided (→staff), withdrawal.processed (→specialist), activation.decided (→user).