Edit: /opt/mawid/PROJECT_PLAN.md (12997B)
# Project: WhatsApp-First Booking & No-Show Reduction SaaS for Clinics
# (Working name: "Mawid" — rename freely)
> **How to use this file:** This is the master build plan. Work through phases **in order**.
> Do not start a phase until the previous phase's acceptance criteria all pass.
> At the end of each phase, write a short summary of what was built into `PROGRESS.md`.
---
## 1. Product Summary
A SaaS for small service businesses (pilot vertical: **dental/aesthetic clinics in Turkey**) where:
- Patients book, confirm, reschedule, and cancel appointments **entirely over WhatsApp**, in **Arabic and Turkish** (English fallback).
- An AI agent (Claude API) handles the conversation: understands intent, checks availability, books slots.
- The system sends **smart reminders** (24h + 2h before) with one-tap Confirm / Cancel buttons.
- When a slot is cancelled, the system **auto-refills it** from a waitlist.
- The clinic owner gets a **minimal web dashboard** (calendar, patients, settings) but 90% of their interaction is also via WhatsApp (weekly summary messages).
**Primary success metric for pilot:** reduce clinic no-show rate by ≥30% within 2 months.
---
## 2. Tech Stack (fixed — do not substitute without asking)
| Layer | Choice | Notes |
|---|---|---|
| Backend | **NestJS 11 + Fastify** | TypeScript, modular |
| DB | **PostgreSQL 16 + Prisma** | single DB, multi-tenant by `clinicId` |
| Queue/Jobs | **BullMQ + Redis** | reminders, waitlist refill, scheduled sends |
| AI | **OpenAI API** (`gpt-5.6-terra` for conversations, `gpt-5.6-luna` for classification/cheap tasks) | tool-calling agent. Changed from Claude API by owner request, 2026-07-18 |
| Messaging | **WhatsApp Business Cloud API (Meta)** | webhooks + template messages |
| Dashboard | **Next.js 15 (App Router)** + Tailwind + shadcn/ui | minimal, mobile-first |
| Auth | Simple email+password with JWT (owner accounts only for MVP) | no OAuth needed yet |
| Deployment | Docker Compose → single VPS (DigitalOcean droplet) for pilot | keep it boring |
| i18n | All patient-facing strings in **ar / tr / en** | agent replies in the patient's language |
**Monorepo layout:**
```
/apps
/api → NestJS backend
/dashboard → Next.js owner dashboard
/packages
/db → Prisma schema + client
/shared → shared types, zod schemas, i18n message templates
docker-compose.yml
PROJECT_PLAN.md (this file)
PROGRESS.md
```
---
## 3. Data Model (Prisma — Phase 1 deliverable)
Core entities (add fields as needed, keep names):
- **Clinic** — id, name, phone (WhatsApp number), timezone (default `Europe/Istanbul`), defaultLanguage, workingHours (JSON per weekday), settings (JSON: reminder offsets, deposit on/off)
- **Staff** — id, clinicId, name, role, workingHours (JSON), services (relation)
- **Service** — id, clinicId, name (i18n JSON), durationMinutes, price, active
- **Patient** — id, clinicId, waPhone (unique per clinic), name, language, notes, tags, lastVisitAt
- **Appointment** — id, clinicId, patientId, staffId, serviceId, startsAt, endsAt, status (`pending | confirmed | cancelled | no_show | completed`), source (`whatsapp | dashboard`), cancelReason
- **WaitlistEntry** — id, clinicId, patientId, serviceId, preferredWindow (JSON: date ranges/times), status, createdAt
- **Conversation** — id, clinicId, patientId, state (JSON — agent memory/slots), lastMessageAt
- **Message** — id, conversationId, direction (`in | out`), waMessageId, type, body, payload (JSON), createdAt
- **ReminderJob** — id, appointmentId, kind (`24h | 2h`), scheduledFor, status
- **AuditLog** — id, clinicId, actor, action, meta (JSON)
Multi-tenancy rule: **every query must be scoped by `clinicId`.** Enforce via a Prisma middleware or repository pattern.
---
## 4. Phases
### Phase 0 — Scaffold & Infrastructure
**Goal:** running skeleton, nothing functional.
Tasks:
1. Init monorepo (pnpm workspaces), NestJS app, Next.js app, Prisma package, shared package.
2. Docker Compose: postgres, redis, api, dashboard.
3. ENV management: `.env.example` with all vars (see §6). Config module with zod validation — app must refuse to boot with missing env.
4. Health endpoints: `GET /health` (api), basic page (dashboard).
5. Set up ESLint + Prettier + a minimal CI script (`pnpm lint && pnpm build && pnpm test`).
**Acceptance:** `docker compose up` boots everything; health checks green; CI script passes.
---
### Phase 1 — Database, Auth, Clinic Setup
**Goal:** a clinic owner can exist and configure their clinic.
Tasks:
1. Implement full Prisma schema (§3) + migrations + seed script (1 demo clinic, 2 staff, 4 services, 10 fake patients).
2. Auth module: register/login (email+password, argon2), JWT access token, guard on all clinic routes.
3. CRUD APIs: clinic settings, staff, services, working hours.
4. Unit tests for auth + tenancy scoping (a user from clinic A must never read clinic B data — write an explicit test for this).
**Acceptance:** seed runs; API CRUD works via REST client; tenancy test passes.
---
### Phase 2 — WhatsApp Integration (plumbing, no AI yet)
**Goal:** reliable two-way messaging.
Tasks:
1. Webhook endpoint for Meta WhatsApp Cloud API (`GET` verify + `POST` receive). Validate signature (`X-Hub-Signature-256`).
2. Outbound message service: text, interactive buttons, list messages, template messages. Retry with backoff via BullMQ.
3. Persist all inbound/outbound messages to `Message` + `Conversation`.
4. Echo mode behind a feature flag: reply "received: {text}" — used only to verify the pipe.
5. **24-hour window handling:** free-form messages only inside the 24h customer service window; outside it, use approved template messages. Build a `canSendFreeform(conversation)` helper.
**Acceptance:** send a WhatsApp message to the test number → it's stored, echoed back, visible in DB. Signature validation rejects tampered payloads (test).
---
### Phase 3 — AI Booking Agent
**Goal:** the core magic — natural-language booking in ar/tr/en.
Architecture:
- One agent loop per inbound message: load `Conversation.state` → call Claude with **tool use** → execute tools → persist state → reply.
- **Tools to expose to Claude:**
- `get_services()` — list clinic services
- `get_availability(serviceId, staffId?, dateRange)` — computed from working hours minus existing appointments
- `create_appointment(patientId, serviceId, staffId, startsAt)` — creates as `pending`
- `reschedule_appointment(appointmentId, newStartsAt)`
- `cancel_appointment(appointmentId, reason?)`
- `add_to_waitlist(serviceId, preferredWindow)`
- `get_patient_appointments(patientId)`
- `handoff_to_human(reason)` — flags conversation for owner, agent stops
- System prompt requirements:
- Detect and reply in the patient's language (Levantine Arabic / Turkish / English).
- Never invent availability — always call tools.
- Confirm details (service, staff, date, time) before booking.
- Concise, warm, WhatsApp-appropriate tone. No markdown.
- If asked anything medical → politely deflect + offer to book a consultation.
- Escalate to `handoff_to_human` on anger, complaints, or 2 consecutive failed understandings.
- Guardrails: max 15 agent turns per conversation per hour (rate limit), strip PII from logs, log every tool call to `AuditLog`.
- Use `claude-haiku-4-5` first for a cheap intent gate (booking-related vs FAQ vs other), route to `claude-sonnet-4-6` only when needed.
Tasks:
1. Availability engine (pure functions + tests): working hours × staff × service duration × existing appointments → free slots. **This must be exhaustively unit-tested** (edge cases: overlaps, breaks, timezone, DST).
2. Agent service with tool loop as above.
3. Conversation state persistence (compact — store facts/slots, not full transcripts; transcripts already live in `Message`).
4. E2E test with mocked Claude responses covering: happy booking path, reschedule, cancel, waitlist, handoff.
**Acceptance:** from a real WhatsApp number, in Arabic and in Turkish: book, reschedule, and cancel an appointment successfully. Availability never conflicts (test proves it).
---
### Phase 4 — Reminders & Confirmations
**Goal:** the no-show killer.
Tasks:
1. On appointment creation/update, schedule BullMQ jobs: reminder at T-24h and T-2h (offsets configurable per clinic).
2. Reminder message = WhatsApp **template** with interactive buttons: ✅ Confirm / ❌ Cancel / 🔁 Reschedule.
3. Button replies route back through the agent tools (confirm → status `confirmed`; cancel → Phase 5 refill flow; reschedule → agent conversation).
4. If no response to 24h reminder by T-4h → send the 2h reminder as a stronger nudge; mark appointment `unconfirmed_risk` flag for the dashboard.
5. Cancel/reschedule of the appointment must cancel/rebuild its pending jobs (idempotent).
**Acceptance:** create an appointment 25h out (use fake timers/short offsets in test mode) → both reminders fire, buttons work end-to-end, job cleanup verified.
---
### Phase 5 — Waitlist & Auto-Refill
**Goal:** cancelled slots turn back into revenue.
Tasks:
1. When an appointment is cancelled ≥3h before start: find matching `WaitlistEntry`s (service + time window), rank by createdAt.
2. Message the top candidate: "A slot opened {date} {time} — want it?" with Accept/Decline buttons. Hold the slot for 20 minutes (configurable), then cascade to next candidate.
3. Concurrency safety: slot hold must be atomic (DB transaction / row lock) — two candidates must never book the same slot. Write a test for this.
4. Notify the owner via WhatsApp when a slot is refilled ("Slot recovered: {patient}, {service}, {time}").
**Acceptance:** cancel a booked slot with 2 waitlisted patients → first gets offer; on decline/timeout the second gets it; double-booking test passes.
---### Phase 6 — Owner Dashboard (minimal)
**Goal:** enough UI to run a pilot, nothing more.
Pages (Next.js, mobile-first, Arabic/Turkish UI toggle):
1. **Calendar** — day/week view of appointments, color by status, click → details, manual create/edit/cancel.
2. **Patients** — list, search, profile (history, notes, tags).
3. **Conversations** — read-only transcript viewer + "take over" button (pauses agent for that conversation).
4. **Settings** — working hours, services, staff, reminder offsets, language.
5. **Weekly summary** — also sent as a WhatsApp message to the owner every Monday 09:00 clinic time: appointments count, cancellations, no-shows prevented (confirmed-after-reminder count), refilled slots.
**Acceptance:** owner can fully operate a week of the clinic from the dashboard alone; weekly summary job renders correct numbers from seeded data.
---
### Phase 7 — Pilot Hardening
**Goal:** safe to put a real clinic on it.
Tasks:
1. Structured logging (pino) + request IDs; error tracking (Sentry or self-hosted GlitchTip).
2. Backups: nightly `pg_dump` to object storage; document restore procedure.
3. Rate limiting on webhook + API; input validation everywhere (zod).
4. KVKK/GDPR basics: data export per patient, delete-on-request endpoint, PII minimization in logs.
5. Deployment: Docker Compose on VPS + Caddy (auto-HTTPS); `deploy.sh`; staging vs production env split.
6. Load sanity test: 50 concurrent conversations without message loss.
7. Write `RUNBOOK.md`: how to onboard a clinic, rotate WhatsApp tokens, common failures.
**Acceptance:** staging deploy from clean clone using only README + RUNBOOK; restore-from-backup drill succeeds.
---
## 5. Explicitly OUT of scope for MVP
- Payments/deposits (Phase 2 of the product, post-pilot)
- Multi-clinic chains / franchise features
- iOS/Android apps
- Marketing campaigns / re-activation blasts
- Advanced analytics dashboards
- Instagram/Telegram channels
Do not build these even if tempting. Note ideas in `IDEAS.md` instead.
---
## 6. Environment Variables (`.env.example`)
```
DATABASE_URL=
REDIS_URL=
JWT_SECRET=
ANTHROPIC_API_KEY=
WA_PHONE_NUMBER_ID=
WA_BUSINESS_ACCOUNT_ID=
WA_ACCESS_TOKEN=
WA_WEBHOOK_VERIFY_TOKEN=
WA_APP_SECRET=
APP_BASE_URL=
DASHBOARD_BASE_URL=
NODE_ENV=
TZ=Europe/Istanbul
FEATURE_ECHO_MODE=false
```
---
## 7. Working Rules for Claude Code
1. **One phase at a time.** Finish, test, summarize in `PROGRESS.md`, then ask before moving on.
2. **Tests are not optional** for: availability engine, tenancy isolation, reminder job lifecycle, waitlist concurrency.
3. Small commits with conventional messages (`feat:`, `fix:`, `test:`...).
4. All patient-facing text lives in `/packages/shared/i18n` — never hardcode strings in agent prompts or services.
5. Timezone discipline: store UTC in DB, convert at the edges using clinic timezone. Never use server local time.
6. If a Meta/WhatsApp API detail is uncertain, stub it behind an interface and flag it in `PROGRESS.md` rather than guessing.
7. Keep dependencies minimal; prefer stdlib/Nest built-ins.