Epic 3 compleet: Backend integratie voor Swift Agenda Planning. Nieuwe endpoints: - GET /api/swift/agenda - Query afspraken op datumrange - POST /api/swift/agenda/create - Nieuwe afspraak aanmaken - POST /api/swift/agenda/cancel - Afspraak annuleren - POST /api/swift/agenda/reschedule - Afspraak verzetten - GET /api/swift/patients/search - Fuzzy patiënt zoeken Alle endpoints bevatten: - Supabase authenticatie + resource ownership checks - Zod validatie met Nederlandse foutmeldingen - Hergebruik van bestaande agenda server actions Test documentatie: docs/swift/test-plan-epic3-backend.md Progress: 3/7 Epics compleet (E0, E1, E2, E3) Story points: 11 SP (E3.S1: 3, E3.S2: 3, E3.S3: 3, E3.S4: 2)
14 KiB
Test Plan - Epic 3: Backend Integration
Epic: Swift Agenda Planning - Backend Integration Version: 1.0 Date: 2025-12-27 Status: ✅ Implementation Complete
🎯 Overview
Epic 3 implements the backend API layer for Swift Agenda Planning, providing RESTful endpoints for:
- Querying appointments by date range
- Creating new appointments
- Canceling appointments
- Rescheduling appointments
- Searching for patients (for disambiguation)
All endpoints include:
- ✅ Authentication via Supabase Auth
- ✅ Input validation with Zod schemas
- ✅ Dutch error messages
- ✅ Security checks (user owns the resource)
- ✅ Reuse of existing server actions
📋 Implementation Summary
Story E3.S1: Agenda Query API ✅
File: app/api/swift/agenda/route.ts
- Endpoint:
GET /api/swift/agenda?start=YYYY-MM-DD&end=YYYY-MM-DD - Auth: Required (Supabase)
- Filters: Automatically filters by current user's practitioner_id
- Response: List of appointments (encounters) with patient details
Story E3.S2: Create Appointment API ✅
File: app/api/swift/agenda/create/route.ts
- Endpoint:
POST /api/swift/agenda/create - Body:
{ patientId, datetime: { date, time }, type, location, notes? } - Validation: Date cannot be in past, appointment type and location must be valid
- Mapping:
type→ FHIR encounter typeCodelocation→ FHIR classCode (AMB/VR/HH)- Duration: 1 hour default
Story E3.S3: Cancel/Reschedule APIs ✅
Files:
app/api/swift/agenda/cancel/route.tsapp/api/swift/agenda/reschedule/route.ts
Cancel Endpoint: POST /api/swift/agenda/cancel
- Body:
{ encounterId } - Security: Verifies user owns the appointment
- Validation: Cannot cancel already cancelled appointment
- Action: Soft delete (status → 'cancelled')
Reschedule Endpoint: POST /api/swift/agenda/reschedule
- Body:
{ encounterId, newDatetime: { date, time } } - Security: Verifies user owns the appointment
- Validation: Cannot reschedule to past, cannot reschedule cancelled appointments
- Duration: Preserves original appointment duration
Story E3.S4: Patient Search API ✅
File: app/api/swift/patients/search/route.ts
- Endpoint:
GET /api/swift/patients/search?q=<query> - Search: Fuzzy match on name_family and name_given
- Limit: 10 results
- Response: Array of patients with id, name, bsn, birthDate
🧪 Manual Test Scenarios
Test Category 1: Agenda Query (E3.S1)
Test 1.1: Query appointments for today
Prerequisites: User is logged in, has appointments for today
curl -X GET 'http://localhost:3000/api/swift/agenda?start=2025-12-27&end=2025-12-27' \
-H 'Cookie: <session-cookie>'
Expected:
- Status: 200
- Response:
{ appointments: [...], count: N, dateRange: { start, end } } - Appointments filtered by current user
Test 1.2: Query with missing parameters
curl -X GET 'http://localhost:3000/api/swift/agenda?start=2025-12-27' \
-H 'Cookie: <session-cookie>'
Expected:
- Status: 400
- Error: "start en end parameters zijn verplicht"
Test 1.3: Query with invalid date format
curl -X GET 'http://localhost:3000/api/swift/agenda?start=invalid&end=2025-12-27' \
-H 'Cookie: <session-cookie>'
Expected:
- Status: 400
- Error: "start moet een geldige datum zijn"
Test 1.4: Query without authentication
curl -X GET 'http://localhost:3000/api/swift/agenda?start=2025-12-27&end=2025-12-27'
Expected:
- Status: 401
- Error: "Niet geautoriseerd. Log opnieuw in."
Test Category 2: Create Appointment (E3.S2)
Test 2.1: Create valid appointment
Prerequisites: User is logged in, valid patient ID available
curl -X POST 'http://localhost:3000/api/swift/agenda/create' \
-H 'Content-Type: application/json' \
-H 'Cookie: <session-cookie>' \
-d '{
"patientId": "<uuid>",
"datetime": {
"date": "2025-12-28",
"time": "14:00"
},
"type": "intake",
"location": "praktijk",
"notes": "Eerste afspraak"
}'
Expected:
- Status: 201
- Response:
{ success: true, encounterId: "<uuid>", appointment: {...} } - Appointment visible in agenda
Test 2.2: Create appointment in the past
curl -X POST 'http://localhost:3000/api/swift/agenda/create' \
-H 'Content-Type: application/json' \
-H 'Cookie: <session-cookie>' \
-d '{
"patientId": "<uuid>",
"datetime": {
"date": "2020-01-01",
"time": "14:00"
},
"type": "intake",
"location": "praktijk"
}'
Expected:
- Status: 400
- Error: "Kan geen afspraken in het verleden maken"
Test 2.3: Create appointment with invalid type
curl -X POST 'http://localhost:3000/api/swift/agenda/create' \
-H 'Content-Type: application/json' \
-H 'Cookie: <session-cookie>' \
-d '{
"patientId": "<uuid>",
"datetime": {
"date": "2025-12-28",
"time": "14:00"
},
"type": "invalid_type",
"location": "praktijk"
}'
Expected:
- Status: 400
- Error: "type moet een geldig afspraaktype zijn"
Test 2.4: Create appointment with missing required fields
curl -X POST 'http://localhost:3000/api/swift/agenda/create' \
-H 'Content-Type: application/json' \
-H 'Cookie: <session-cookie>' \
-d '{
"patientId": "<uuid>",
"datetime": {
"date": "2025-12-28"
},
"type": "intake",
"location": "praktijk"
}'
Expected:
- Status: 400
- Error: Contains validation error about missing time
Test Category 3: Cancel Appointment (E3.S3a)
Test 3.1: Cancel valid appointment
Prerequisites: User has an upcoming appointment
curl -X POST 'http://localhost:3000/api/swift/agenda/cancel' \
-H 'Content-Type: application/json' \
-H 'Cookie: <session-cookie>' \
-d '{
"encounterId": "<uuid>"
}'
Expected:
- Status: 200
- Response:
{ success: true, encounterId: "<uuid>", message: "Afspraak succesvol geannuleerd" } - Appointment status updated to 'cancelled'
Test 3.2: Cancel already cancelled appointment
curl -X POST 'http://localhost:3000/api/swift/agenda/cancel' \
-H 'Content-Type: application/json' \
-H 'Cookie: <session-cookie>' \
-d '{
"encounterId": "<uuid-of-cancelled-appointment>"
}'
Expected:
- Status: 400
- Error: "Deze afspraak is al geannuleerd"
Test 3.3: Cancel non-existent appointment
curl -X POST 'http://localhost:3000/api/swift/agenda/cancel' \
-H 'Content-Type: application/json' \
-H 'Cookie: <session-cookie>' \
-d '{
"encounterId": "00000000-0000-0000-0000-000000000000"
}'
Expected:
- Status: 404
- Error: "Afspraak niet gevonden"
Test 3.4: Cancel appointment owned by another user
Prerequisites: Have another user's appointment ID
curl -X POST 'http://localhost:3000/api/swift/agenda/cancel' \
-H 'Content-Type: application/json' \
-H 'Cookie: <session-cookie>' \
-d '{
"encounterId": "<uuid-of-other-users-appointment>"
}'
Expected:
- Status: 403
- Error: "Je hebt geen toegang tot deze afspraak"
Test Category 4: Reschedule Appointment (E3.S3b)
Test 4.1: Reschedule valid appointment
Prerequisites: User has an upcoming appointment
curl -X POST 'http://localhost:3000/api/swift/agenda/reschedule' \
-H 'Content-Type: application/json' \
-H 'Cookie: <session-cookie>' \
-d '{
"encounterId": "<uuid>",
"newDatetime": {
"date": "2025-12-29",
"time": "15:00"
}
}'
Expected:
- Status: 200
- Response:
{ success: true, encounterId: "<uuid>", appointment: {...}, message: "Afspraak succesvol verzet" } - Appointment period_start and period_end updated
Test 4.2: Reschedule to past date
curl -X POST 'http://localhost:3000/api/swift/agenda/reschedule' \
-H 'Content-Type: application/json' \
-H 'Cookie: <session-cookie>' \
-d '{
"encounterId": "<uuid>",
"newDatetime": {
"date": "2020-01-01",
"time": "15:00"
}
}'
Expected:
- Status: 400
- Error: "Kan geen afspraken in het verleden verzetten"
Test 4.3: Reschedule cancelled appointment
curl -X POST 'http://localhost:3000/api/swift/agenda/reschedule' \
-H 'Content-Type: application/json' \
-H 'Cookie: <session-cookie>' \
-d '{
"encounterId": "<uuid-of-cancelled-appointment>",
"newDatetime": {
"date": "2025-12-29",
"time": "15:00"
}
}'
Expected:
- Status: 400
- Error: "Kan een geannuleerde afspraak niet verzetten"
Test 4.4: Reschedule with preserved duration
Prerequisites: Create an appointment with 2-hour duration
# First, create appointment manually with 2-hour duration
# Then reschedule it
curl -X POST 'http://localhost:3000/api/swift/agenda/reschedule' \
-H 'Content-Type: application/json' \
-H 'Cookie: <session-cookie>' \
-d '{
"encounterId": "<uuid>",
"newDatetime": {
"date": "2025-12-29",
"time": "10:00"
}
}'
Expected:
- Status: 200
- Response period_end is 2 hours after period_start (preserves original duration)
Test Category 5: Patient Search (E3.S4)
Test 5.1: Search for existing patient
Prerequisites: Patient "Jan de Vries" exists in database
curl -X GET 'http://localhost:3000/api/swift/patients/search?q=jan' \
-H 'Cookie: <session-cookie>'
Expected:
- Status: 200
- Response:
{ patients: [{id, name, bsn, birthDate}], count: N, query: "jan" } - Results include matching patients
Test 5.2: Search with partial name
curl -X GET 'http://localhost:3000/api/swift/patients/search?q=vri' \
-H 'Cookie: <session-cookie>'
Expected:
- Status: 200
- Response includes patients with "vri" in their name (e.g., "de Vries")
Test 5.3: Search with no matches
curl -X GET 'http://localhost:3000/api/swift/patients/search?q=zzzzzzz' \
-H 'Cookie: <session-cookie>'
Expected:
- Status: 200
- Response:
{ patients: [], count: 0, query: "zzzzzzz" }
Test 5.4: Search without query parameter
curl -X GET 'http://localhost:3000/api/swift/patients/search' \
-H 'Cookie: <session-cookie>'
Expected:
- Status: 400
- Error: "Query parameter 'q' is verplicht"
Test 5.5: Search with empty query
curl -X GET 'http://localhost:3000/api/swift/patients/search?q=' \
-H 'Cookie: <session-cookie>'
Expected:
- Status: 400
- Error: "Zoekterm mag niet leeg zijn"
🔐 Security Checklist
| Check | Status | Notes |
|---|---|---|
| All endpoints require authentication | ✅ | Returns 401 if not authenticated |
| User can only access their own appointments | ✅ | Filtered by practitioner_id |
| User cannot cancel/reschedule others' appointments | ✅ | Ownership verification in place |
| Input validation on all endpoints | ✅ | Zod schemas with Dutch error messages |
| SQL injection prevention | ✅ | Using Supabase client with parameterized queries |
| XSS prevention | ✅ | No direct HTML rendering |
| CSRF protection | ✅ | Next.js built-in CSRF protection |
🐛 Known Issues / Edge Cases
Issue 1: Patient Search Query Performance
- Description: The patient search uses
ilikewhich may be slow on large datasets - Mitigation: Limited to 10 results
- Future: Consider adding database index on name_family/name_given
Issue 2: Timezone Handling
- Description: All dates stored in UTC, client must handle timezone conversion
- Current: Using ISO string format
- Future: Consider explicit timezone handling in API
Issue 3: Appointment Conflicts
- Description: No conflict detection when creating/rescheduling appointments
- Status: Out of scope for MVP
- Future: Add conflict warning in Epic 4 (UI layer)
📊 Test Results Summary
| Story | Total Tests | Passed | Failed | Blocked | Coverage |
|---|---|---|---|---|---|
| E3.S1 | 4 | - | - | - | Auth, Validation, Happy Path |
| E3.S2 | 4 | - | - | - | Validation, Security, Happy Path |
| E3.S3a | 4 | - | - | - | Security, Validation, Happy Path |
| E3.S3b | 4 | - | - | - | Security, Validation, Duration |
| E3.S4 | 5 | - | - | - | Search, Validation, Empty State |
| Total | 21 | TBD | TBD | TBD | All scenarios |
🚀 Next Steps
After manual testing is complete:
- ✅ Epic 3 Complete → Move to Epic 4 (AgendaBlock UI)
- 📝 Update Build Plan → Mark Epic 3 stories as "Done"
- 🧪 Integration Testing → Test with actual UI when Epic 4 is ready
- 📚 API Documentation → Generate OpenAPI/Swagger docs (optional)
📁 Files Created
app/api/swift/
├── agenda/
│ ├── route.ts # E3.S1: Query endpoint
│ ├── create/
│ │ └── route.ts # E3.S2: Create endpoint
│ ├── cancel/
│ │ └── route.ts # E3.S3a: Cancel endpoint
│ └── reschedule/
│ └── route.ts # E3.S3b: Reschedule endpoint
└── patients/
└── search/
└── route.ts # E3.S4: Patient search endpoint
🎓 Lessons Learned
- Zod Validation: Use
validation.error.issuesnotvalidation.error.errors - Database Column Names: Check generated types carefully (e.g.,
identifier_bsnvsbsn) - Security First: Always verify resource ownership before mutations
- Reuse Actions: Existing server actions (
getEncounters,createEncounter, etc.) work perfectly - Type Safety: TypeScript catches errors early - run
pnpm exec tscbefore testing
Test Plan Status: ✅ Ready for Manual Testing Implementation Status: ✅ Complete (All 4 stories) Type Check: ✅ Passing Next Epic: Epic 4 - AgendaBlock UI