What to build
Ship the public read endpoints that Module 04's patient-side discovery UI will consume. No UI in this slice — the PRD assigns the patient-facing pages to Module 04, but the API surface belongs in Module 03 alongside the schema it queries.
- Fill in
doctorContract with listDoctors and listUpcomingSlots.
GET /doctors?search= behind requireAuth() only: returns a flat array of { userId, firstName, lastName, specialization }. Empty or absent search returns all doctors. With search, applies a case-insensitive substring match (Postgres ILIKE '%term%') against any of first_name, last_name, specialization. No pagination.
GET /doctors/{doctorId}/slots?upcoming=true behind requireAuth() only: returns slots with status='free' and starts_at >= now() UTC, sorted ascending by starts_at. The upcoming=true query param is required for this slice; behaviour without it is undefined and may 400.
- Use case + repository for both, encapsulating the search/filter SQL.
Acceptance criteria
User stories covered
Blocked by
What to build
Ship the public read endpoints that Module 04's patient-side discovery UI will consume. No UI in this slice — the PRD assigns the patient-facing pages to Module 04, but the API surface belongs in Module 03 alongside the schema it queries.
doctorContractwithlistDoctorsandlistUpcomingSlots.GET /doctors?search=behindrequireAuth()only: returns a flat array of{ userId, firstName, lastName, specialization }. Empty or absentsearchreturns all doctors. Withsearch, applies a case-insensitive substring match (PostgresILIKE '%term%') against any offirst_name,last_name,specialization. No pagination.GET /doctors/{doctorId}/slots?upcoming=truebehindrequireAuth()only: returns slots withstatus='free'andstarts_at >= now()UTC, sorted ascending bystarts_at. Theupcoming=truequery param is required for this slice; behaviour without it is undefined and may 400.Acceptance criteria
GET /doctors(no search) returns all doctors as a flat array{ userId, firstName, lastName, specialization }.GET /doctors?search=KOWALmatches a doctor whoselast_nameis "Kowalski" (case-insensitive substring).GET /doctors?search=cardiomatches a doctor whosespecializationcontains "Cardiology".GET /doctors/{doctorId}/slots?upcoming=trueexcludes slots withstarts_at < now()and excludes slots with statusbooked.GET /doctors/{doctorId}/slots?upcoming=truereturns slots sorted ascending bystarts_at.pnpm verifyexits 0.User stories covered
Blocked by