1735 lines
59 KiB
Markdown
1735 lines
59 KiB
Markdown
# Enterprise Componenten Uitbreiding — Voorbereiding & Interfaces
|
||
|
||
> **Type**: Architectuur Specificatiedocument
|
||
> **Versie**: 1.0
|
||
> **Datum**: 2026-02-14
|
||
> **Doelgroep**: LLM-agents, developers, architecten
|
||
> **Taal**: Nederlands (tekst), Engels (technische termen en code)
|
||
> **Relatie**: Uitbreiding op `usecases-technische-componenten.md` — voegt 10 nieuwe componenten toe (C34–C43)
|
||
> **Status**: Voorbereiding — interfaces en integratiepunten gedefinieerd, nog niet gebouwd
|
||
|
||
---
|
||
|
||
## Leeswijzer
|
||
|
||
Dit document beschrijft 10 aanvullende technische componenten die nodig zijn voor een enterprise-ready EPD. Ze zijn nog **niet gebouwd**, maar de architectuur is zo voorbereid dat ze naadloos ingepast kunnen worden:
|
||
|
||
- **Interface definities** — TypeScript interfaces die de contracten vastleggen
|
||
- **Database schema** — Tabellen die aangemaakt kunnen worden wanneer nodig
|
||
- **Integratiepunten** — Hoe elk component aansluit op bestaande componenten (C01–C33)
|
||
- **Technologiekeuzes** — Welke libraries/tools en waarom
|
||
- **Fasering** — Wanneer bouwen in relatie tot het bouwplan
|
||
|
||
De componenten volgen de nummering uit `usecases-technische-componenten.md` (C34+).
|
||
|
||
---
|
||
|
||
## Component Overzicht
|
||
|
||
```
|
||
LAAG 6: PLATFORM SERVICES (nieuw)
|
||
├── C34 Notificatie Service (in-app + email + escalatie)
|
||
├── C35 Workflow Engine (state machine voor klinische workflows)
|
||
├── C36 Full-Text Search Service (klinische notities doorzoeken)
|
||
├── C37 Caching Layer (AI responses, API responses, sessiedata)
|
||
├── C38 Scheduled Jobs Service (BullMQ repeatable jobs)
|
||
└── C39 Webhook System (outbound event webhooks)
|
||
|
||
LAAG 7: EXTERNE INTEGRATIES (nieuw)
|
||
├── C40 ZPM Facturatie Service (Zorgprestatiemodel)
|
||
├── C41 Vecozo Connector (voorbereiding)
|
||
└── C42 Zorgdomein Connector (voorbereiding)
|
||
|
||
LAAG 8: ANALYTICS (nieuw)
|
||
└── C43 BI Module (materialized views, data-export)
|
||
```
|
||
|
||
### Prioriteit & fasering
|
||
|
||
| Component | Prioriteit | Bouwfase | Reden |
|
||
|-----------|-----------|----------|-------|
|
||
| C34 Notificatie | Must-have | Fase 2 (EPD core) | Overdracht, shifts, escalaties |
|
||
| C35 Workflow Engine | Must-have | Fase 2 | Intake, behandelplan, Wvggz |
|
||
| C36 Full-Text Search | Should-have | Fase 3 | Zoeken in rapportage, overdracht |
|
||
| C37 Caching | Should-have | Fase 2 | Performance, AI cost reductie |
|
||
| C38 Scheduled Jobs | Must-have | Fase 1 (fundament) | Audit cleanup, protocol sync, facturatie |
|
||
| C39 Webhook System | Should-have | Fase 4 (connectiviteit) | Connector events, externe integraties |
|
||
| C40 ZPM Facturatie | Must-have | Fase 3 | Inkomsten, wettelijk verplicht |
|
||
| C41 Vecozo | Later | Fase 4+ | Pas als EPD core stabiel is |
|
||
| C42 Zorgdomein | Later | Fase 4+ | Pas als EPD core stabiel is |
|
||
| C43 BI Module | Should-have | Fase 5 | Management rapportages |
|
||
|
||
---
|
||
|
||
## C34 — Notificatie Service
|
||
|
||
### Doel
|
||
|
||
Centraal systeem voor alle notificaties: in-app meldingen, email, en escalatieketens. Elke service in het systeem stuurt notificaties via dit component — geen directe email of push vanuit business logic.
|
||
|
||
### Waarom nodig
|
||
|
||
- Overdracht: melden dat AI-samenvatting klaar is
|
||
- Shifts: herinnering aan overdracht, ongelezen rapportages
|
||
- Incidenten: escalatie naar teamleider als MIC-melding niet afgehandeld
|
||
- Wvggz: wettelijke termijnen die aflopen (dwangmaatregel verlenging)
|
||
- Medicatie: herinneringen aan medicatie-evaluaties
|
||
- Protocol Engine: nudges die niet in-app getoond konden worden
|
||
|
||
### Interface
|
||
|
||
```typescript
|
||
// lib/notifications/types.ts
|
||
|
||
type NotificationChannel = 'in_app' | 'email' | 'both';
|
||
type NotificationPriority = 'low' | 'normal' | 'high' | 'urgent';
|
||
type NotificationCategory =
|
||
| 'overdracht'
|
||
| 'shift'
|
||
| 'incident'
|
||
| 'wvggz_termijn'
|
||
| 'medicatie'
|
||
| 'behandelplan'
|
||
| 'protocol_nudge'
|
||
| 'systeem'
|
||
| 'taak';
|
||
|
||
interface NotificationPayload {
|
||
recipientUserId: string;
|
||
tenantId: string;
|
||
category: NotificationCategory;
|
||
priority: NotificationPriority;
|
||
channel: NotificationChannel;
|
||
title: string; // NL, max 120 chars
|
||
body: string; // NL, max 500 chars
|
||
actionUrl?: string; // Deep link naar EPD pagina
|
||
relatedPatientId?: string; // Voor audit trail koppeling
|
||
relatedEntityType?: string; // 'report' | 'appointment' | 'treatment_plan'
|
||
relatedEntityId?: string;
|
||
metadata?: Record<string, unknown>;
|
||
expiresAt?: Date; // Automatisch opruimen
|
||
}
|
||
|
||
interface NotificationPreferences {
|
||
userId: string;
|
||
tenantId: string;
|
||
channelDefaults: Record<NotificationCategory, NotificationChannel>;
|
||
quietHours?: { start: string; end: string }; // "22:00" - "07:00"
|
||
emailDigest: 'immediate' | 'hourly' | 'daily' | 'off';
|
||
}
|
||
|
||
interface EscalationRule {
|
||
id: string;
|
||
tenantId: string;
|
||
category: NotificationCategory;
|
||
triggerCondition: string; // bv. "unread_after_minutes > 60"
|
||
escalateToRole: string; // bv. "teamleider"
|
||
escalateChannel: NotificationChannel;
|
||
maxEscalations: number;
|
||
}
|
||
|
||
// Service interface
|
||
interface NotificationService {
|
||
send(payload: NotificationPayload): Promise<{ notificationId: string }>;
|
||
sendBulk(payloads: NotificationPayload[]): Promise<{ sent: number; failed: number }>;
|
||
markAsRead(notificationId: string, userId: string): Promise<void>;
|
||
markAllAsRead(userId: string, category?: NotificationCategory): Promise<void>;
|
||
getUnread(userId: string, limit?: number): Promise<Notification[]>;
|
||
getUnreadCount(userId: string): Promise<Record<NotificationCategory, number>>;
|
||
updatePreferences(prefs: NotificationPreferences): Promise<void>;
|
||
registerEscalationRule(rule: EscalationRule): Promise<void>;
|
||
}
|
||
```
|
||
|
||
### Database schema
|
||
|
||
```sql
|
||
-- Notificaties tabel
|
||
CREATE TABLE notifications (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
tenant_id UUID NOT NULL REFERENCES tenants(id),
|
||
recipient_id UUID NOT NULL REFERENCES users(id),
|
||
category TEXT NOT NULL,
|
||
priority TEXT NOT NULL DEFAULT 'normal',
|
||
channel TEXT NOT NULL DEFAULT 'in_app',
|
||
title TEXT NOT NULL,
|
||
body TEXT NOT NULL,
|
||
action_url TEXT,
|
||
related_patient_id UUID REFERENCES patients(id),
|
||
related_entity_type TEXT,
|
||
related_entity_id UUID,
|
||
metadata JSONB DEFAULT '{}',
|
||
read_at TIMESTAMPTZ,
|
||
sent_at TIMESTAMPTZ DEFAULT NOW(),
|
||
expires_at TIMESTAMPTZ,
|
||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||
);
|
||
|
||
-- Indexes voor snelle queries
|
||
CREATE INDEX idx_notifications_recipient_unread
|
||
ON notifications (recipient_id, read_at)
|
||
WHERE read_at IS NULL;
|
||
|
||
CREATE INDEX idx_notifications_tenant_category
|
||
ON notifications (tenant_id, category, created_at DESC);
|
||
|
||
-- RLS: gebruiker ziet alleen eigen notificaties
|
||
ALTER TABLE notifications ENABLE ROW LEVEL SECURITY;
|
||
|
||
CREATE POLICY notifications_own ON notifications
|
||
FOR ALL USING (
|
||
tenant_id = current_setting('app.current_tenant_id')::UUID
|
||
AND recipient_id = current_setting('app.current_user_id')::UUID
|
||
);
|
||
|
||
-- Voorkeuren per gebruiker
|
||
CREATE TABLE notification_preferences (
|
||
user_id UUID NOT NULL REFERENCES users(id),
|
||
tenant_id UUID NOT NULL REFERENCES tenants(id),
|
||
channel_defaults JSONB NOT NULL DEFAULT '{}',
|
||
quiet_hours_start TIME,
|
||
quiet_hours_end TIME,
|
||
email_digest TEXT NOT NULL DEFAULT 'immediate',
|
||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||
PRIMARY KEY (user_id, tenant_id)
|
||
);
|
||
|
||
-- Escalatie regels per tenant
|
||
CREATE TABLE escalation_rules (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
tenant_id UUID NOT NULL REFERENCES tenants(id),
|
||
category TEXT NOT NULL,
|
||
trigger_condition TEXT NOT NULL,
|
||
escalate_to_role TEXT NOT NULL,
|
||
escalate_channel TEXT NOT NULL DEFAULT 'both',
|
||
max_escalations INT NOT NULL DEFAULT 3,
|
||
active BOOLEAN DEFAULT TRUE,
|
||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||
);
|
||
```
|
||
|
||
### Integratiepunten
|
||
|
||
| Bestaand component | Integratie |
|
||
|-------------------|------------|
|
||
| C10 Protocol Engine | Nudges die niet in-app getoond worden → notificatie |
|
||
| C15 Rapportage Service | Nieuwe rapportage voor overdracht → notificatie aan team |
|
||
| C16 Overdracht Service | AI-samenvatting gereed → notificatie |
|
||
| C17 Agenda Service | Afspraakherinnering → notificatie |
|
||
| C22 Toestemming Service | Wvggz termijn nadert → escalatie notificatie |
|
||
| C25 RBAC + ABAC | Escalatie naar rol, niet naar specifieke persoon |
|
||
| C38 Scheduled Jobs | Periodieke digest emails, escalatie checks |
|
||
|
||
### Technologiekeuze
|
||
|
||
- **In-app**: Server-Sent Events (SSE) vanuit Next.js API route — simpeler dan WebSockets, voldoende voor notificaties
|
||
- **Email**: Postal (self-hosted, D19) via BullMQ job queue
|
||
- **Escalatie**: BullMQ delayed jobs — check na X minuten of notificatie gelezen is
|
||
|
||
### Trade-offs
|
||
|
||
| Keuze | Alternatief | Rationale |
|
||
|-------|------------|-----------|
|
||
| SSE voor real-time | WebSockets | Eenvoudiger, minder infra, unidirectioneel is voldoende |
|
||
| Escalatie via BullMQ | Aparte escalatie service | Hergebruik bestaande infra (C38), KISS |
|
||
| Notificatie in PostgreSQL | Aparte message store (Redis) | Eén bron van waarheid, auditbaar, RLS |
|
||
|
||
---
|
||
|
||
## C35 — Workflow Engine (State Machine)
|
||
|
||
### Doel
|
||
|
||
Generieke state machine die klinische workflows bestuurt: intake-trajecten, behandelplannen, Wvggz-procedures, MIC-meldingen. Dwingt af dat stappen in de juiste volgorde gebeuren, door de juiste rollen, binnen wettelijke termijnen.
|
||
|
||
### Waarom nodig
|
||
|
||
- **Intake**: Aanmelding → Triage → Diagnostiek → Behandelplan → Start behandeling
|
||
- **Wvggz**: Verzoekschrift → Beoordeling → Machtiging → Uitvoering → Evaluatie (wettelijke termijnen!)
|
||
- **MIC-melding**: Registratie → Analyse → Maatregelen → Evaluatie → Afsluiting
|
||
- **Behandelplan**: Concept → Review → Akkoord patiënt → Actief → Evaluatie
|
||
- **Ontslag**: Voorbereiding → Nazorgplan → Overdracht → Ontslag
|
||
|
||
### Interface
|
||
|
||
```typescript
|
||
// lib/workflow/types.ts
|
||
|
||
interface WorkflowDefinition {
|
||
id: string;
|
||
name: string; // bv. "intake_ggz"
|
||
version: number;
|
||
tenantId: string | null; // null = systeembreed
|
||
states: WorkflowState[];
|
||
transitions: WorkflowTransition[];
|
||
initialState: string;
|
||
terminalStates: string[];
|
||
}
|
||
|
||
interface WorkflowState {
|
||
name: string; // bv. "triage", "diagnostiek"
|
||
label: string; // NL label: "Triage"
|
||
requiredRole?: string; // Welke rol mag in deze state werken
|
||
maxDurationDays?: number; // Wettelijke of interne deadline
|
||
onEnter?: WorkflowAction[]; // Acties bij betreden state
|
||
onExit?: WorkflowAction[]; // Acties bij verlaten state
|
||
requiredFields?: string[]; // Welke velden ingevuld moeten zijn
|
||
}
|
||
|
||
interface WorkflowTransition {
|
||
from: string;
|
||
to: string;
|
||
trigger: string; // bv. "approve", "reject", "timeout"
|
||
requiredRole?: string; // Wie mag deze transitie uitvoeren
|
||
guard?: string; // Conditie (bv. "all_required_fields_filled")
|
||
actions?: WorkflowAction[]; // Acties bij transitie
|
||
}
|
||
|
||
interface WorkflowAction {
|
||
type: 'notify' | 'create_task' | 'update_field' | 'escalate' | 'audit_log';
|
||
config: Record<string, unknown>;
|
||
}
|
||
|
||
interface WorkflowInstance {
|
||
id: string;
|
||
workflowDefinitionId: string;
|
||
tenantId: string;
|
||
entityType: string; // 'intake' | 'behandelplan' | 'wvggz' | 'mic_melding'
|
||
entityId: string; // ID van het gerelateerde object
|
||
currentState: string;
|
||
stateHistory: StateTransitionRecord[];
|
||
createdAt: Date;
|
||
updatedAt: Date;
|
||
completedAt: Date | null;
|
||
}
|
||
|
||
interface StateTransitionRecord {
|
||
fromState: string;
|
||
toState: string;
|
||
trigger: string;
|
||
userId: string;
|
||
timestamp: Date;
|
||
comment?: string;
|
||
metadata?: Record<string, unknown>;
|
||
}
|
||
|
||
// Service interface
|
||
interface WorkflowService {
|
||
// Definitie beheer
|
||
registerDefinition(def: WorkflowDefinition): Promise<void>;
|
||
getDefinition(name: string, version?: number): Promise<WorkflowDefinition>;
|
||
|
||
// Instance beheer
|
||
startWorkflow(params: {
|
||
definitionName: string;
|
||
entityType: string;
|
||
entityId: string;
|
||
tenantId: string;
|
||
initiatorUserId: string;
|
||
}): Promise<WorkflowInstance>;
|
||
|
||
transition(params: {
|
||
instanceId: string;
|
||
trigger: string;
|
||
userId: string;
|
||
comment?: string;
|
||
}): Promise<WorkflowInstance>;
|
||
|
||
getCurrentState(instanceId: string): Promise<WorkflowInstance>;
|
||
getAvailableTransitions(instanceId: string, userId: string): Promise<WorkflowTransition[]>;
|
||
getInstancesByEntity(entityType: string, entityId: string): Promise<WorkflowInstance[]>;
|
||
|
||
// Deadline monitoring
|
||
getOverdueInstances(tenantId: string): Promise<WorkflowInstance[]>;
|
||
}
|
||
```
|
||
|
||
### Database schema
|
||
|
||
```sql
|
||
-- Workflow definities (versioned)
|
||
CREATE TABLE workflow_definitions (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
name TEXT NOT NULL,
|
||
version INT NOT NULL DEFAULT 1,
|
||
tenant_id UUID REFERENCES tenants(id), -- NULL = systeembreed
|
||
definition JSONB NOT NULL, -- Volledige WorkflowDefinition
|
||
active BOOLEAN DEFAULT TRUE,
|
||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||
UNIQUE(name, version, tenant_id)
|
||
);
|
||
|
||
-- Workflow instances
|
||
CREATE TABLE workflow_instances (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
workflow_def_id UUID NOT NULL REFERENCES workflow_definitions(id),
|
||
tenant_id UUID NOT NULL REFERENCES tenants(id),
|
||
entity_type TEXT NOT NULL,
|
||
entity_id UUID NOT NULL,
|
||
current_state TEXT NOT NULL,
|
||
completed_at TIMESTAMPTZ,
|
||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||
);
|
||
|
||
CREATE INDEX idx_workflow_instances_entity
|
||
ON workflow_instances (entity_type, entity_id);
|
||
|
||
CREATE INDEX idx_workflow_instances_state
|
||
ON workflow_instances (tenant_id, current_state)
|
||
WHERE completed_at IS NULL;
|
||
|
||
-- State transitie historie (immutable)
|
||
CREATE TABLE workflow_transitions_log (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
instance_id UUID NOT NULL REFERENCES workflow_instances(id),
|
||
from_state TEXT NOT NULL,
|
||
to_state TEXT NOT NULL,
|
||
trigger TEXT NOT NULL,
|
||
user_id UUID NOT NULL REFERENCES users(id),
|
||
comment TEXT,
|
||
metadata JSONB DEFAULT '{}',
|
||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||
);
|
||
|
||
-- Immutable: geen UPDATE of DELETE
|
||
CREATE RULE workflow_transitions_no_update AS
|
||
ON UPDATE TO workflow_transitions_log DO INSTEAD NOTHING;
|
||
CREATE RULE workflow_transitions_no_delete AS
|
||
ON DELETE TO workflow_transitions_log DO INSTEAD NOTHING;
|
||
|
||
-- RLS
|
||
ALTER TABLE workflow_instances ENABLE ROW LEVEL SECURITY;
|
||
ALTER TABLE workflow_transitions_log ENABLE ROW LEVEL SECURITY;
|
||
|
||
CREATE POLICY workflow_instances_tenant ON workflow_instances
|
||
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::UUID);
|
||
|
||
CREATE POLICY workflow_transitions_tenant ON workflow_transitions_log
|
||
FOR ALL USING (
|
||
instance_id IN (
|
||
SELECT id FROM workflow_instances
|
||
WHERE tenant_id = current_setting('app.current_tenant_id')::UUID
|
||
)
|
||
);
|
||
```
|
||
|
||
### Voorgedefinieerde workflows
|
||
|
||
```typescript
|
||
// lib/workflow/definitions/intake-ggz.ts
|
||
const intakeGGZ: WorkflowDefinition = {
|
||
id: 'wf-intake-ggz',
|
||
name: 'intake_ggz',
|
||
version: 1,
|
||
tenantId: null,
|
||
initialState: 'aangemeld',
|
||
terminalStates: ['gestart', 'afgewezen'],
|
||
states: [
|
||
{ name: 'aangemeld', label: 'Aangemeld', maxDurationDays: 5,
|
||
onEnter: [{ type: 'notify', config: { category: 'taak', role: 'triagist' } }] },
|
||
{ name: 'triage', label: 'Triage', requiredRole: 'triagist', maxDurationDays: 3 },
|
||
{ name: 'diagnostiek', label: 'Diagnostiek', requiredRole: 'behandelaar', maxDurationDays: 14 },
|
||
{ name: 'behandelplan', label: 'Behandelplan opstellen', requiredRole: 'behandelaar', maxDurationDays: 7,
|
||
requiredFields: ['diagnose', 'behandeldoelen'] },
|
||
{ name: 'akkoord_patient', label: 'Akkoord patiënt', maxDurationDays: 5 },
|
||
{ name: 'gestart', label: 'Behandeling gestart' },
|
||
{ name: 'afgewezen', label: 'Afgewezen (doorverwijzing)' },
|
||
],
|
||
transitions: [
|
||
{ from: 'aangemeld', to: 'triage', trigger: 'start_triage', requiredRole: 'triagist' },
|
||
{ from: 'triage', to: 'diagnostiek', trigger: 'accept', requiredRole: 'triagist' },
|
||
{ from: 'triage', to: 'afgewezen', trigger: 'reject', requiredRole: 'triagist',
|
||
actions: [{ type: 'notify', config: { category: 'taak', message: 'Doorverwijsbrief opstellen' } }] },
|
||
{ from: 'diagnostiek', to: 'behandelplan', trigger: 'diagnose_complete', requiredRole: 'behandelaar' },
|
||
{ from: 'behandelplan', to: 'akkoord_patient', trigger: 'submit_plan', guard: 'all_required_fields_filled' },
|
||
{ from: 'akkoord_patient', to: 'gestart', trigger: 'patient_agrees' },
|
||
{ from: 'akkoord_patient', to: 'behandelplan', trigger: 'patient_disagrees' },
|
||
],
|
||
};
|
||
```
|
||
|
||
### Integratiepunten
|
||
|
||
| Bestaand component | Integratie |
|
||
|-------------------|------------|
|
||
| C10 Protocol Engine | Nudge kan workflow transitie suggereren |
|
||
| C15 Rapportage Service | MIC-melding start `mic_workflow` instance |
|
||
| C18 Behandelplan Service | Behandelplan lifecycle via workflow |
|
||
| C20 Intake Service | Intake trajectory via workflow |
|
||
| C22 Toestemming Service | Wvggz-procedure als workflow met wettelijke deadlines |
|
||
| C25 RBAC + ABAC | `requiredRole` in transitions → RBAC check |
|
||
| C26 Audit Trail | Elke transitie → audit log entry |
|
||
| C34 Notificatie | `onEnter`, `onExit`, transitie acties → notificaties |
|
||
| C38 Scheduled Jobs | Deadline monitoring: check dagelijks op `maxDurationDays` overschrijding |
|
||
|
||
### Technologiekeuze
|
||
|
||
- **Geen externe library**: State machine logica is ~200 regels TypeScript. XState is overkill voor server-side workflow.
|
||
- **Definities in code + database**: Systeem-workflows in code (version controlled), tenant-specifieke aanpassingen in database.
|
||
- **Transitie log immutable**: Net als audit trail — compliance vereist onveranderbare historie.
|
||
|
||
---
|
||
|
||
## C36 — Full-Text Search Service
|
||
|
||
### Doel
|
||
|
||
Zoeken door klinische notities, rapportages en patiëntgegevens op basis van vrije tekst. Ondersteunt het intent "zoeken" (C06/C07) en de traditionele EPD zoekbalk.
|
||
|
||
### Waarom nodig
|
||
|
||
- Zorgverlener zoekt: "alle rapportages over agressie bij Jan Janssen afgelopen maand"
|
||
- Compliance officer zoekt: "alle incidenten met fixatie in 2025"
|
||
- Intent systeem: `zoeken` intent moet rapportages kunnen doorzoeken
|
||
|
||
### Interface
|
||
|
||
```typescript
|
||
// lib/search/types.ts
|
||
|
||
interface SearchQuery {
|
||
tenantId: string;
|
||
query: string; // Vrije tekst, NL
|
||
filters?: {
|
||
entityTypes?: ('report' | 'patient' | 'appointment' | 'treatment_plan')[];
|
||
patientId?: string;
|
||
dateRange?: { from: Date; to: Date };
|
||
reportCategories?: string[];
|
||
authorId?: string;
|
||
departmentId?: string;
|
||
};
|
||
pagination?: { offset: number; limit: number };
|
||
highlight?: boolean; // Markeer matches in resultaat
|
||
}
|
||
|
||
interface SearchResult {
|
||
entityType: string;
|
||
entityId: string;
|
||
score: number;
|
||
title: string;
|
||
snippet: string; // Fragment met context
|
||
highlights?: string[]; // Gemarkeerde matches
|
||
metadata: {
|
||
patientName?: string;
|
||
authorName?: string;
|
||
createdAt: Date;
|
||
category?: string;
|
||
};
|
||
}
|
||
|
||
interface SearchResponse {
|
||
results: SearchResult[];
|
||
total: number;
|
||
took: number; // milliseconds
|
||
query: string;
|
||
}
|
||
|
||
// Service interface
|
||
interface SearchService {
|
||
search(query: SearchQuery): Promise<SearchResponse>;
|
||
indexDocument(params: {
|
||
entityType: string;
|
||
entityId: string;
|
||
tenantId: string;
|
||
content: string;
|
||
metadata: Record<string, unknown>;
|
||
}): Promise<void>;
|
||
removeDocument(entityType: string, entityId: string): Promise<void>;
|
||
reindexAll(tenantId: string): Promise<{ indexed: number }>;
|
||
}
|
||
```
|
||
|
||
### Database schema
|
||
|
||
```sql
|
||
-- Full-text search configuratie voor Nederlands
|
||
CREATE TEXT SEARCH CONFIGURATION dutch_medical (COPY = dutch);
|
||
|
||
-- Medische synoniemen dictionary (uitbreidbaar)
|
||
-- In productie: custom dictionary file met medische termen
|
||
-- ALTER TEXT SEARCH CONFIGURATION dutch_medical
|
||
-- ALTER MAPPING FOR asciiword WITH dutch_medical_syn, dutch_stem;
|
||
|
||
-- Search index op reports
|
||
ALTER TABLE reports ADD COLUMN IF NOT EXISTS
|
||
search_vector TSVECTOR
|
||
GENERATED ALWAYS AS (
|
||
setweight(to_tsvector('dutch', coalesce(title, '')), 'A') ||
|
||
setweight(to_tsvector('dutch', coalesce(content, '')), 'B') ||
|
||
setweight(to_tsvector('dutch', coalesce(
|
||
structured_data->>'category', ''
|
||
)), 'C')
|
||
) STORED;
|
||
|
||
CREATE INDEX idx_reports_search ON reports USING GIN (search_vector);
|
||
|
||
-- Search index op patients
|
||
ALTER TABLE patients ADD COLUMN IF NOT EXISTS
|
||
search_vector TSVECTOR
|
||
GENERATED ALWAYS AS (
|
||
setweight(to_tsvector('dutch', coalesce(first_name, '')), 'A') ||
|
||
setweight(to_tsvector('dutch', coalesce(last_name, '')), 'A') ||
|
||
setweight(to_tsvector('dutch', coalesce(bsn, '')), 'A')
|
||
) STORED;
|
||
|
||
CREATE INDEX idx_patients_search ON patients USING GIN (search_vector);
|
||
|
||
-- Zoekquery met ranking
|
||
-- Voorbeeld: zoek "agressie medicatie" in rapportages van een patiënt
|
||
-- SELECT id, title,
|
||
-- ts_rank_cd(search_vector, query) AS rank,
|
||
-- ts_headline('dutch', content, query, 'MaxWords=35, MinWords=15') AS snippet
|
||
-- FROM reports,
|
||
-- plainto_tsquery('dutch', 'agressie medicatie') query
|
||
-- WHERE search_vector @@ query
|
||
-- AND patient_id = $1
|
||
-- AND tenant_id = current_setting('app.current_tenant_id')::UUID
|
||
-- AND deleted_at IS NULL
|
||
-- ORDER BY rank DESC
|
||
-- LIMIT 20;
|
||
```
|
||
|
||
### Integratiepunten
|
||
|
||
| Bestaand component | Integratie |
|
||
|-------------------|------------|
|
||
| C06 Reflex / C07 Orchestrator | `zoeken` intent → SearchService.search() |
|
||
| C09 Action System | Search resultaten als block in canvas |
|
||
| C15 Rapportage Service | Bij create/update → automatisch geïndexeerd (GENERATED column) |
|
||
| C21 Patiënt Service | Patiënt zoeken → search_vector op patients |
|
||
| C24 Multi-tenancy | RLS op search queries: tenant_id filter |
|
||
|
||
### Technologiekeuze
|
||
|
||
- **PostgreSQL tsvector** (geen Elasticsearch): Eenvoudiger stack, geen extra service, voldoende voor GGZ-schaal (~100k-1M documenten)
|
||
- **GENERATED ALWAYS AS STORED**: Automatische index-update bij write, geen aparte indexer nodig
|
||
- **Nederlandse stemmer**: Ingebouwd in PostgreSQL, uitbreidbaar met medische synoniemen dictionary
|
||
|
||
### Trade-offs
|
||
|
||
| Keuze | Alternatief | Rationale |
|
||
|-------|------------|-----------|
|
||
| PostgreSQL tsvector | Elasticsearch, Meilisearch, Typesense | Geen extra service, voldoende voor verwacht volume |
|
||
| GENERATED column | Trigger-based indexing | Eenvoudiger, altijd in sync, geen race conditions |
|
||
| `ts_rank_cd` | Custom scoring | Standaard PostgreSQL, goed genoeg voor eerste iteratie |
|
||
|
||
### Schaalgrens
|
||
|
||
PostgreSQL full-text search werkt goed tot ~5M documenten. Als het systeem voorbij die grens groeit, kan Meilisearch (self-hosted, Rust, <100MB RAM) als drop-in replacement dienen zonder interface-wijzigingen.
|
||
|
||
---
|
||
|
||
## C37 — Caching Layer
|
||
|
||
### Doel
|
||
|
||
Caching van dure operaties: AI-responses, API-responses, sessiedata, en veelgevraagde queries. Vermindert latency, kosten (minder AI API calls), en database load.
|
||
|
||
### Waarom nodig
|
||
|
||
- **AI kosten**: Dezelfde overdracht-samenvatting wordt meerdere keren bekeken → cache
|
||
- **Performance**: Patient zoekresultaten, dashboard aggregaties, agenda views
|
||
- **Protocol Engine**: RAG results cachen zodat dezelfde protocol-query niet steeds opnieuw draait
|
||
- **Sessiedata**: Active patient context, recente navigatie
|
||
|
||
### Interface
|
||
|
||
```typescript
|
||
// lib/cache/types.ts
|
||
|
||
type CacheNamespace =
|
||
| 'ai:summary' // Overdracht AI samenvattingen
|
||
| 'ai:classification' // Intent classificatie resultaten
|
||
| 'ai:rag' // RAG pipeline resultaten
|
||
| 'api:patients' // Patient zoekresultaten
|
||
| 'api:agenda' // Agenda views
|
||
| 'api:dashboard' // Dashboard aggregaties
|
||
| 'session:context' // Active patient, recente acties
|
||
| 'config:protocols'; // Protocol definities
|
||
|
||
interface CacheOptions {
|
||
ttl: number; // Seconds
|
||
namespace: CacheNamespace;
|
||
tags?: string[]; // Voor gerichte invalidatie
|
||
}
|
||
|
||
interface CacheService {
|
||
get<T>(key: string, namespace: CacheNamespace): Promise<T | null>;
|
||
set<T>(key: string, value: T, options: CacheOptions): Promise<void>;
|
||
delete(key: string, namespace: CacheNamespace): Promise<void>;
|
||
invalidateByTag(tag: string): Promise<number>; // Returns count
|
||
invalidateNamespace(namespace: CacheNamespace): Promise<void>;
|
||
|
||
// Convenience: get-or-set pattern
|
||
getOrSet<T>(
|
||
key: string,
|
||
factory: () => Promise<T>,
|
||
options: CacheOptions
|
||
): Promise<T>;
|
||
|
||
// Stats
|
||
getStats(): Promise<{
|
||
hitRate: number;
|
||
memoryUsage: number;
|
||
keyCount: number;
|
||
}>;
|
||
}
|
||
```
|
||
|
||
### TTL strategie
|
||
|
||
| Namespace | TTL | Invalidatie |
|
||
|-----------|-----|-------------|
|
||
| `ai:summary` | 30 min | Bij nieuwe rapportage voor die patiënt |
|
||
| `ai:classification` | 5 min | Zelden — zelfde input = zelfde output |
|
||
| `ai:rag` | 60 min | Bij protocol update |
|
||
| `api:patients` | 10 min | Bij patient update |
|
||
| `api:agenda` | 5 min | Bij appointment create/update/delete |
|
||
| `api:dashboard` | 15 min | Periodiek (C38 scheduled job) |
|
||
| `session:context` | 60 min | Bij navigatie-actie |
|
||
| `config:protocols` | 24 uur | Bij protocol wijziging |
|
||
|
||
### Implementatie
|
||
|
||
```typescript
|
||
// lib/cache/redis-cache.ts
|
||
|
||
import Redis from 'ioredis';
|
||
|
||
const NAMESPACE_PREFIX: Record<CacheNamespace, string> = {
|
||
'ai:summary': 'ai:sum:',
|
||
'ai:classification': 'ai:cls:',
|
||
'ai:rag': 'ai:rag:',
|
||
'api:patients': 'api:pat:',
|
||
'api:agenda': 'api:agd:',
|
||
'api:dashboard': 'api:dsh:',
|
||
'session:context': 'ses:ctx:',
|
||
'config:protocols': 'cfg:prt:',
|
||
};
|
||
|
||
// Cache key inclusief tenant voor isolatie
|
||
function buildKey(namespace: CacheNamespace, tenantId: string, key: string): string {
|
||
return `${NAMESPACE_PREFIX[namespace]}${tenantId}:${key}`;
|
||
}
|
||
|
||
// Tag-based invalidatie via Redis Sets
|
||
// Set "tag:{tagName}" bevat alle keys met die tag
|
||
// Bij invalidateByTag: SMEMBERS + DEL pipeline
|
||
```
|
||
|
||
### Integratiepunten
|
||
|
||
| Bestaand component | Integratie |
|
||
|-------------------|------------|
|
||
| C07 Orchestrator | Cache classificatie resultaten (zelfde input → zelfde output) |
|
||
| C12 RAG Pipeline | Cache vector search resultaten per query hash |
|
||
| C16 Overdracht Service | Cache AI-samenvattingen per patiënt + shift |
|
||
| C24 Multi-tenancy | Cache keys bevatten tenant_id — volledige isolatie |
|
||
| C31 Circuit Breaker | Bij AI service down → serve from cache (stale-while-revalidate) |
|
||
|
||
### Technologiekeuze
|
||
|
||
- **Redis** (al in stack voor BullMQ, D18): Hergebruik bestaande Redis instance
|
||
- **ioredis**: Standaard Node.js Redis client, pipeline support, cluster-ready
|
||
|
||
### Geen gevoelige data in cache
|
||
|
||
BSN, diagnoses en andere PII worden **nooit** direct gecached. Alleen:
|
||
- Hashed keys met verwijzing naar encrypted database records
|
||
- AI-gegenereerde samenvattingen (geen ruwe patiëntdata)
|
||
- Aggregaties en counts
|
||
|
||
---
|
||
|
||
## C38 — Scheduled Jobs Service
|
||
|
||
### Doel
|
||
|
||
Betrouwbare uitvoering van periodieke en geplande taken: audit trail cleanup, protocol synchronisatie, facturatie batches, deadline monitoring, email digests.
|
||
|
||
### Waarom nodig
|
||
|
||
- **Audit trail**: Partities aanmaken, oude partities archiveren
|
||
- **Workflow deadlines**: Dagelijks checken of workflows hun `maxDurationDays` overschrijden
|
||
- **Notificatie digests**: Dagelijkse email samenvattingen
|
||
- **ZPM facturatie**: Maandelijkse facturatie batch
|
||
- **Protocol sync**: Periodiek nieuwe protocollen ophalen/indexeren
|
||
- **Caching**: Periodiek dashboard aggregaties vernieuwen
|
||
|
||
### Interface
|
||
|
||
```typescript
|
||
// lib/jobs/types.ts
|
||
|
||
interface JobDefinition {
|
||
name: string; // Unieke naam: "audit.partition.create"
|
||
handler: (payload: unknown) => Promise<void>;
|
||
schedule?: string; // Cron syntax: "0 2 * * *" (02:00 dagelijks)
|
||
retries?: number; // Default: 3
|
||
backoff?: { type: 'exponential' | 'fixed'; delay: number };
|
||
timeout?: number; // Milliseconds
|
||
concurrency?: number; // Max parallel workers
|
||
}
|
||
|
||
interface ScheduledJobsService {
|
||
register(definition: JobDefinition): void;
|
||
enqueue(jobName: string, payload?: unknown, options?: {
|
||
delay?: number; // Milliseconds
|
||
priority?: number; // 1 (highest) - 10 (lowest)
|
||
tenantId?: string;
|
||
}): Promise<string>; // Returns job ID
|
||
|
||
getStatus(jobId: string): Promise<JobStatus>;
|
||
getFailedJobs(limit?: number): Promise<FailedJob[]>;
|
||
retryFailed(jobId: string): Promise<void>;
|
||
getMetrics(): Promise<JobMetrics>;
|
||
}
|
||
|
||
interface JobMetrics {
|
||
active: number;
|
||
waiting: number;
|
||
completed: number;
|
||
failed: number;
|
||
delayed: number;
|
||
}
|
||
```
|
||
|
||
### Geplande jobs
|
||
|
||
```typescript
|
||
// lib/jobs/definitions.ts
|
||
|
||
const scheduledJobs: JobDefinition[] = [
|
||
{
|
||
name: 'audit.partition.create',
|
||
schedule: '0 0 25 * *', // 25e van de maand, partitie voor volgende maand
|
||
handler: createNextAuditPartition,
|
||
retries: 3,
|
||
},
|
||
{
|
||
name: 'workflow.deadline.check',
|
||
schedule: '0 8 * * *', // Dagelijks 08:00
|
||
handler: checkWorkflowDeadlines, // Overschrijdingen → escalatie notificatie
|
||
retries: 2,
|
||
},
|
||
{
|
||
name: 'notifications.digest',
|
||
schedule: '0 7 * * *', // Dagelijks 07:00
|
||
handler: sendDailyDigests, // Email digest aan users met die voorkeur
|
||
retries: 2,
|
||
},
|
||
{
|
||
name: 'zpm.facturatie.batch',
|
||
schedule: '0 2 1 * *', // 1e van de maand, 02:00
|
||
handler: runZPMFacturatieBatch, // Maandelijkse facturatie
|
||
retries: 3,
|
||
timeout: 600_000, // 10 minuten
|
||
},
|
||
{
|
||
name: 'protocol.sync',
|
||
schedule: '0 3 * * 0', // Zondag 03:00
|
||
handler: syncProtocolDocuments, // RAG pipeline re-index
|
||
retries: 2,
|
||
timeout: 300_000,
|
||
},
|
||
{
|
||
name: 'cache.dashboard.refresh',
|
||
schedule: '*/15 * * * *', // Elke 15 minuten
|
||
handler: refreshDashboardCache,
|
||
retries: 1,
|
||
},
|
||
{
|
||
name: 'notifications.escalation.check',
|
||
schedule: '*/5 * * * *', // Elke 5 minuten
|
||
handler: checkEscalations, // Ongelezen urgente notificaties → escaleer
|
||
retries: 1,
|
||
},
|
||
];
|
||
```
|
||
|
||
### Integratiepunten
|
||
|
||
| Bestaand component | Integratie |
|
||
|-------------------|------------|
|
||
| C26 Audit Trail | Partitie management, archivering |
|
||
| C34 Notificatie | Digest emails, escalatie checks |
|
||
| C35 Workflow Engine | Deadline monitoring |
|
||
| C37 Caching | Dashboard cache verversing |
|
||
| C40 ZPM Facturatie | Maandelijkse batch |
|
||
| C12 RAG Pipeline | Protocol re-indexering |
|
||
|
||
### Technologiekeuze
|
||
|
||
- **BullMQ** (al gekozen, D18): Betrouwbare job queue met Redis backing, cron support, retry logic, dashboard (Bull Board)
|
||
- **Bull Board**: Web UI voor job monitoring — beschikbaar voor admin users
|
||
|
||
---
|
||
|
||
## C39 — Webhook System
|
||
|
||
### Doel
|
||
|
||
Outbound webhooks voor externe integraties: externe systemen kunnen zich abonneren op events in het EPD platform. Essentieel voor de connector-architectuur (D17) en toekomstige integraties.
|
||
|
||
### Waarom nodig
|
||
|
||
- **Connector API**: Extern EPD wil weten wanneer er een nieuw rapport is aangemaakt
|
||
- **Externe systemen**: Facturatiesysteem, CRM, onderzoeksplatform willen events ontvangen
|
||
- **Toekomstige integraties**: Vecozo, Zorgdomein, BI-tools
|
||
|
||
### Interface
|
||
|
||
```typescript
|
||
// lib/webhooks/types.ts
|
||
|
||
type WebhookEvent =
|
||
| 'report.created'
|
||
| 'report.updated'
|
||
| 'report.deleted'
|
||
| 'patient.admitted'
|
||
| 'patient.discharged'
|
||
| 'appointment.created'
|
||
| 'appointment.updated'
|
||
| 'appointment.cancelled'
|
||
| 'treatment_plan.activated'
|
||
| 'treatment_plan.updated'
|
||
| 'workflow.state_changed'
|
||
| 'incident.reported'
|
||
| 'wvggz.status_changed';
|
||
|
||
interface WebhookRegistration {
|
||
id: string;
|
||
tenantId: string;
|
||
url: string; // HTTPS endpoint
|
||
events: WebhookEvent[]; // Welke events
|
||
secret: string; // HMAC-SHA256 signing secret
|
||
active: boolean;
|
||
description?: string;
|
||
createdBy: string;
|
||
headers?: Record<string, string>; // Extra headers (auth tokens etc.)
|
||
retryPolicy?: {
|
||
maxRetries: number; // Default: 5
|
||
backoffMs: number; // Default: 1000, exponential
|
||
};
|
||
}
|
||
|
||
interface WebhookPayload {
|
||
id: string; // Unique event ID
|
||
event: WebhookEvent;
|
||
tenantId: string;
|
||
timestamp: string; // ISO 8601
|
||
data: Record<string, unknown>; // Event-specifieke data
|
||
// NOOIT PII in webhook payload — alleen IDs en metadata
|
||
}
|
||
|
||
interface WebhookDeliveryLog {
|
||
id: string;
|
||
webhookId: string;
|
||
event: WebhookEvent;
|
||
payload: WebhookPayload;
|
||
responseStatus: number | null;
|
||
responseBody?: string;
|
||
attempt: number;
|
||
deliveredAt: Date | null;
|
||
nextRetryAt: Date | null;
|
||
error?: string;
|
||
}
|
||
|
||
// Service interface
|
||
interface WebhookService {
|
||
register(registration: Omit<WebhookRegistration, 'id' | 'secret'>): Promise<WebhookRegistration>;
|
||
update(id: string, updates: Partial<WebhookRegistration>): Promise<WebhookRegistration>;
|
||
delete(id: string): Promise<void>;
|
||
list(tenantId: string): Promise<WebhookRegistration[]>;
|
||
|
||
// Event dispatching (intern, aangeroepen door andere services)
|
||
dispatch(event: WebhookEvent, tenantId: string, data: Record<string, unknown>): Promise<void>;
|
||
|
||
// Delivery monitoring
|
||
getDeliveryLog(webhookId: string, limit?: number): Promise<WebhookDeliveryLog[]>;
|
||
retryDelivery(deliveryLogId: string): Promise<void>;
|
||
}
|
||
```
|
||
|
||
### Database schema
|
||
|
||
```sql
|
||
-- Webhook registraties
|
||
CREATE TABLE webhook_registrations (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
tenant_id UUID NOT NULL REFERENCES tenants(id),
|
||
url TEXT NOT NULL,
|
||
events TEXT[] NOT NULL,
|
||
secret TEXT NOT NULL, -- HMAC signing secret (encrypted at rest)
|
||
active BOOLEAN DEFAULT TRUE,
|
||
description TEXT,
|
||
headers JSONB DEFAULT '{}',
|
||
retry_max INT DEFAULT 5,
|
||
retry_backoff INT DEFAULT 1000,
|
||
created_by UUID NOT NULL REFERENCES users(id),
|
||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||
);
|
||
|
||
-- Delivery log (voor monitoring en retry)
|
||
CREATE TABLE webhook_delivery_log (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
webhook_id UUID NOT NULL REFERENCES webhook_registrations(id),
|
||
event TEXT NOT NULL,
|
||
payload JSONB NOT NULL,
|
||
response_status INT,
|
||
response_body TEXT,
|
||
attempt INT NOT NULL DEFAULT 1,
|
||
delivered_at TIMESTAMPTZ,
|
||
next_retry_at TIMESTAMPTZ,
|
||
error TEXT,
|
||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||
);
|
||
|
||
-- Partitie delivery log per maand (groeit snel)
|
||
-- CREATE TABLE webhook_delivery_log_2026_02 PARTITION OF webhook_delivery_log
|
||
-- FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
|
||
|
||
CREATE INDEX idx_webhook_delivery_pending
|
||
ON webhook_delivery_log (next_retry_at)
|
||
WHERE delivered_at IS NULL AND next_retry_at IS NOT NULL;
|
||
|
||
-- RLS
|
||
ALTER TABLE webhook_registrations ENABLE ROW LEVEL SECURITY;
|
||
ALTER TABLE webhook_delivery_log ENABLE ROW LEVEL SECURITY;
|
||
|
||
CREATE POLICY webhook_reg_tenant ON webhook_registrations
|
||
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::UUID);
|
||
```
|
||
|
||
### Beveiliging
|
||
|
||
```typescript
|
||
// Webhook signing — HMAC-SHA256
|
||
import crypto from 'node:crypto';
|
||
|
||
function signPayload(payload: WebhookPayload, secret: string): string {
|
||
const body = JSON.stringify(payload);
|
||
return crypto.createHmac('sha256', secret).update(body).digest('hex');
|
||
}
|
||
|
||
// Headers bij elke delivery:
|
||
// X-Webhook-Id: <registration id>
|
||
// X-Webhook-Event: report.created
|
||
// X-Webhook-Signature: sha256=<hmac>
|
||
// X-Webhook-Timestamp: <unix timestamp>
|
||
```
|
||
|
||
### Integratiepunten
|
||
|
||
| Bestaand component | Integratie |
|
||
|-------------------|------------|
|
||
| C15 Rapportage Service | report.created/updated/deleted → dispatch |
|
||
| C17 Agenda Service | appointment.* → dispatch |
|
||
| C18 Behandelplan Service | treatment_plan.* → dispatch |
|
||
| C35 Workflow Engine | workflow.state_changed → dispatch |
|
||
| C38 Scheduled Jobs | Retry queue voor gefaalde deliveries |
|
||
| C40-C42 Externe integraties | Ontvangen events via webhook registraties |
|
||
|
||
### Belangrijk: geen PII in payloads
|
||
|
||
Webhook payloads bevatten **alleen IDs en metadata**, nooit PII:
|
||
|
||
```typescript
|
||
// GOED:
|
||
{ event: 'report.created', data: { reportId: 'uuid', patientId: 'uuid', type: 'observatie' } }
|
||
|
||
// FOUT (nooit):
|
||
{ event: 'report.created', data: { patientName: 'Jan Janssen', bsn: '123456789', content: '...' } }
|
||
```
|
||
|
||
De ontvanger haalt volledige data op via de API met eigen authenticatie.
|
||
|
||
---
|
||
|
||
## C40 — ZPM Facturatie Service
|
||
|
||
### Doel
|
||
|
||
Voorbereiding op Zorgprestatiemodel (ZPM) facturatie: registratie van zorgprestaties, koppeling aan DBC-trajecten, en generatie van facturatiebatches. ZPM is sinds 2022 het bekostigingssysteem voor GGZ.
|
||
|
||
### Waarom nodig
|
||
|
||
- **Wettelijk verplicht**: Elke GGZ-instelling moet factureren volgens ZPM
|
||
- **Zorgprestaties**: Elke behandelactiviteit (consult, groepssessie, diagnostiek) → prestatiecode
|
||
- **DBC-trajecten**: Patiënt heeft een lopend DBC (Diagnose Behandel Combinatie) traject
|
||
- **Facturatiebatch**: Maandelijks → declaratiebericht naar zorgverzekeraar (via Vecozo)
|
||
|
||
### Interface
|
||
|
||
```typescript
|
||
// lib/facturatie/types.ts
|
||
|
||
// NZa prestatiecode structuur
|
||
interface ZorgPrestatie {
|
||
id: string;
|
||
tenantId: string;
|
||
patientId: string;
|
||
dbcTrajectId: string;
|
||
prestatieCode: string; // NZa code, bv. "OVP-0001"
|
||
prestatieType: 'consult' | 'groep' | 'diagnostiek' | 'verblijf' | 'crisis' | 'overig';
|
||
beroep: string; // BIG-registratie beroep behandelaar
|
||
duur: number; // Minuten (voor directe tijd)
|
||
indirecteTijd?: number; // Minuten indirect
|
||
datum: Date;
|
||
behandelaarId: string;
|
||
appointmentId?: string; // Koppeling aan agenda
|
||
reportId?: string; // Koppeling aan rapportage
|
||
status: 'concept' | 'definitief' | 'gedeclareerd' | 'betaald' | 'afgewezen';
|
||
metadata?: Record<string, unknown>;
|
||
}
|
||
|
||
interface DBCTraject {
|
||
id: string;
|
||
tenantId: string;
|
||
patientId: string;
|
||
diagnoseCode: string; // DSM-5 code
|
||
zorgtype: string; // Initieel / vervolg
|
||
startDatum: Date;
|
||
eindDatum?: Date;
|
||
status: 'open' | 'gesloten' | 'afgesloten_declaratie';
|
||
verzekeringId: string; // Verwijzing naar verzekeringgegevens
|
||
}
|
||
|
||
interface DeclaratieBatch {
|
||
id: string;
|
||
tenantId: string;
|
||
periode: string; // "2026-02"
|
||
prestaties: string[]; // Prestatie IDs
|
||
totaalBedrag: number; // Euro's
|
||
status: 'concept' | 'gecontroleerd' | 'verstuurd' | 'verwerkt' | 'deels_afgewezen';
|
||
vecozo_referentie?: string; // Na verzending
|
||
gegenereerd_op: Date;
|
||
verstuurd_op?: Date;
|
||
}
|
||
|
||
// Service interface
|
||
interface FacturatieService {
|
||
// Prestatie registratie
|
||
registreerPrestatie(prestatie: Omit<ZorgPrestatie, 'id' | 'status'>): Promise<ZorgPrestatie>;
|
||
koppelAanAfspraak(prestatieId: string, appointmentId: string): Promise<void>;
|
||
koppelAanRapportage(prestatieId: string, reportId: string): Promise<void>;
|
||
|
||
// DBC beheer
|
||
openTraject(traject: Omit<DBCTraject, 'id' | 'status'>): Promise<DBCTraject>;
|
||
sluitTraject(trajectId: string): Promise<void>;
|
||
|
||
// Facturatie
|
||
genereerBatch(tenantId: string, periode: string): Promise<DeclaratieBatch>;
|
||
controleerBatch(batchId: string): Promise<ValidationResult[]>; // Controle regels
|
||
markeerVerstuurd(batchId: string, vecozoRef: string): Promise<void>;
|
||
|
||
// Overzichten
|
||
getOpenPrestaties(tenantId: string, filters?: object): Promise<ZorgPrestatie[]>;
|
||
getTrajecten(patientId: string): Promise<DBCTraject[]>;
|
||
getOmzetOverzicht(tenantId: string, periode: string): Promise<OmzetOverzicht>;
|
||
}
|
||
|
||
interface ValidationResult {
|
||
prestatieId: string;
|
||
valid: boolean;
|
||
errors: string[]; // NL foutmeldingen
|
||
warnings: string[];
|
||
}
|
||
|
||
interface OmzetOverzicht {
|
||
periode: string;
|
||
totaalGedeclareerd: number;
|
||
totaalBetaald: number;
|
||
totaalAfgewezen: number;
|
||
totaalOpen: number;
|
||
perBeroepsgroep: Record<string, number>;
|
||
perZorgtype: Record<string, number>;
|
||
}
|
||
```
|
||
|
||
### Database schema
|
||
|
||
```sql
|
||
-- DBC trajecten
|
||
CREATE TABLE dbc_trajecten (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
tenant_id UUID NOT NULL REFERENCES tenants(id),
|
||
patient_id UUID NOT NULL REFERENCES patients(id),
|
||
diagnose_code TEXT NOT NULL,
|
||
zorgtype TEXT NOT NULL DEFAULT 'initieel',
|
||
start_datum DATE NOT NULL,
|
||
eind_datum DATE,
|
||
status TEXT NOT NULL DEFAULT 'open',
|
||
verzekering_id UUID, -- Toekomstig: verzekeringsgegevens tabel
|
||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||
);
|
||
|
||
-- Zorgprestaties
|
||
CREATE TABLE zorgprestaties (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
tenant_id UUID NOT NULL REFERENCES tenants(id),
|
||
patient_id UUID NOT NULL REFERENCES patients(id),
|
||
dbc_traject_id UUID NOT NULL REFERENCES dbc_trajecten(id),
|
||
prestatie_code TEXT NOT NULL,
|
||
prestatie_type TEXT NOT NULL,
|
||
beroep TEXT NOT NULL,
|
||
duur_minuten INT NOT NULL,
|
||
indirecte_tijd INT DEFAULT 0,
|
||
datum DATE NOT NULL,
|
||
behandelaar_id UUID NOT NULL REFERENCES users(id),
|
||
appointment_id UUID REFERENCES appointments(id),
|
||
report_id UUID REFERENCES reports(id),
|
||
status TEXT NOT NULL DEFAULT 'concept',
|
||
metadata JSONB DEFAULT '{}',
|
||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||
);
|
||
|
||
CREATE INDEX idx_zorgprestaties_patient ON zorgprestaties (patient_id, datum DESC);
|
||
CREATE INDEX idx_zorgprestaties_status ON zorgprestaties (tenant_id, status);
|
||
CREATE INDEX idx_zorgprestaties_traject ON zorgprestaties (dbc_traject_id);
|
||
|
||
-- Declaratie batches
|
||
CREATE TABLE declaratie_batches (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
tenant_id UUID NOT NULL REFERENCES tenants(id),
|
||
periode TEXT NOT NULL, -- "2026-02"
|
||
prestatie_ids UUID[] NOT NULL,
|
||
totaal_bedrag DECIMAL(10,2),
|
||
status TEXT NOT NULL DEFAULT 'concept',
|
||
vecozo_ref TEXT,
|
||
gegenereerd_op TIMESTAMPTZ DEFAULT NOW(),
|
||
verstuurd_op TIMESTAMPTZ,
|
||
metadata JSONB DEFAULT '{}'
|
||
);
|
||
|
||
-- RLS
|
||
ALTER TABLE dbc_trajecten ENABLE ROW LEVEL SECURITY;
|
||
ALTER TABLE zorgprestaties ENABLE ROW LEVEL SECURITY;
|
||
ALTER TABLE declaratie_batches ENABLE ROW LEVEL SECURITY;
|
||
|
||
CREATE POLICY dbc_tenant ON dbc_trajecten
|
||
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::UUID);
|
||
|
||
CREATE POLICY zorgprestaties_tenant ON zorgprestaties
|
||
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::UUID);
|
||
|
||
CREATE POLICY declaratie_tenant ON declaratie_batches
|
||
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::UUID);
|
||
```
|
||
|
||
### Integratiepunten
|
||
|
||
| Bestaand component | Integratie |
|
||
|-------------------|------------|
|
||
| C17 Agenda Service | Afspraak → automatisch zorgprestatie concept aanmaken |
|
||
| C15 Rapportage Service | Rapport met contactmoment → koppelen aan prestatie |
|
||
| C25 RBAC + ABAC | Alleen financieel medewerker mag batches genereren |
|
||
| C26 Audit Trail | Elke facturatie-actie → audit log |
|
||
| C35 Workflow Engine | Declaratie workflow: concept → controle → verzending |
|
||
| C38 Scheduled Jobs | Maandelijkse batch generatie |
|
||
| C41 Vecozo | Declaratiebatch versturen naar Vecozo (toekomstig) |
|
||
|
||
### Controleregels
|
||
|
||
```typescript
|
||
// lib/facturatie/validatie.ts
|
||
|
||
// Voorbeelden van validatieregels die bij batch-controle draaien:
|
||
const validatieRegels = [
|
||
// Geen prestatie zonder open DBC traject
|
||
'prestatie.datum BETWEEN dbc.start_datum AND COALESCE(dbc.eind_datum, NOW())',
|
||
// Maximaal 12 uur directe tijd per dag per behandelaar
|
||
'SUM(duur_minuten) per behandelaar per dag <= 720',
|
||
// Prestatiecode moet geldig zijn voor beroep
|
||
'prestatie_code IN (valid_codes_for_beroep)',
|
||
// Geen dubbele prestatie (zelfde patient, dag, code, behandelaar)
|
||
'UNIQUE(patient_id, datum, prestatie_code, behandelaar_id)',
|
||
];
|
||
```
|
||
|
||
---
|
||
|
||
## C41 — Vecozo Connector (voorbereiding)
|
||
|
||
### Doel
|
||
|
||
Interface-voorbereiding voor koppeling met Vecozo: het landelijke communicatiepunt voor declaraties, COV-checks (Controle Op Verzekeringsrecht), en machtigingen in de zorg.
|
||
|
||
### Status: alleen interface — geen implementatie
|
||
|
||
Vecozo-koppeling vereist een formeel aansluittraject (UZI-certificaat, testomgeving, acceptatie). Dit component definieert **alleen de interfaces** zodat C40 (ZPM Facturatie) er straks naadloos op aan kan sluiten.
|
||
|
||
### Interface
|
||
|
||
```typescript
|
||
// lib/integrations/vecozo/types.ts
|
||
|
||
interface VecozoConnector {
|
||
// COV-check: is patiënt verzekerd?
|
||
checkVerzekering(params: {
|
||
bsn: string;
|
||
peildatum: Date;
|
||
}): Promise<VerzekeringStatus>;
|
||
|
||
// Declaratie versturen
|
||
submitDeclaratie(params: {
|
||
batchId: string;
|
||
declaratieXml: string; // Vektis EI-standaard
|
||
}): Promise<DeclaratieResponse>;
|
||
|
||
// Declaratie status opvragen
|
||
getDeclaratieStatus(vecozoRef: string): Promise<DeclaratieStatusResponse>;
|
||
|
||
// Machtiging aanvragen (voor bepaalde zorgtypen)
|
||
requestMachtiging(params: {
|
||
patientBsn: string;
|
||
zorgtype: string;
|
||
motivatie: string;
|
||
}): Promise<MachtigingResponse>;
|
||
|
||
// Health check
|
||
isAvailable(): Promise<boolean>;
|
||
}
|
||
|
||
interface VerzekeringStatus {
|
||
verzekerd: boolean;
|
||
verzekeraar?: string;
|
||
uzovi?: string; // UZOVI-code verzekeraar
|
||
polisnummer?: string;
|
||
ingangsDatum?: Date;
|
||
eindDatum?: Date;
|
||
}
|
||
|
||
interface DeclaratieResponse {
|
||
accepted: boolean;
|
||
vecozoReferentie: string;
|
||
errors?: string[];
|
||
}
|
||
|
||
interface DeclaratieStatusResponse {
|
||
status: 'ontvangen' | 'in_behandeling' | 'goedgekeurd' | 'deels_afgewezen' | 'afgewezen';
|
||
details?: string;
|
||
betaalDatum?: Date;
|
||
}
|
||
|
||
interface MachtigingResponse {
|
||
status: 'toegekend' | 'afgewezen' | 'in_behandeling';
|
||
machtigingNummer?: string;
|
||
geldigTot?: Date;
|
||
}
|
||
```
|
||
|
||
### Vereisten voor aansluiting (niet nu, wel documenteren)
|
||
|
||
1. **UZI-certificaat**: Nodig voor mTLS communicatie met Vecozo
|
||
2. **AGB-code**: Instelling moet geregistreerd zijn
|
||
3. **Testomgeving**: Vecozo biedt een acceptatieomgeving
|
||
4. **Vektis standaarden**: Declaratieberichten in EI-standaard XML formaat
|
||
5. **BSN-verificatie**: Koppeling met BRP via Vecozo
|
||
|
||
### Placeholder implementatie
|
||
|
||
```typescript
|
||
// lib/integrations/vecozo/mock-connector.ts
|
||
|
||
export class MockVecozoConnector implements VecozoConnector {
|
||
async checkVerzekering(params: { bsn: string; peildatum: Date }): Promise<VerzekeringStatus> {
|
||
console.warn('[Vecozo] Mock connector — niet verbonden met Vecozo');
|
||
return {
|
||
verzekerd: true,
|
||
verzekeraar: 'Mock Verzekeraar',
|
||
uzovi: '0000',
|
||
};
|
||
}
|
||
|
||
async submitDeclaratie(): Promise<DeclaratieResponse> {
|
||
throw new Error('[Vecozo] Declaratie versturen niet beschikbaar — Vecozo connector niet geconfigureerd');
|
||
}
|
||
|
||
// ... etc
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## C42 — Zorgdomein Connector (voorbereiding)
|
||
|
||
### Doel
|
||
|
||
Interface-voorbereiding voor koppeling met Zorgdomein: het landelijke platform voor elektronische verwijzingen van huisarts naar GGZ.
|
||
|
||
### Status: alleen interface — geen implementatie
|
||
|
||
Net als Vecozo vereist Zorgdomein een formeel aansluittraject. Dit component definieert de interfaces.
|
||
|
||
### Interface
|
||
|
||
```typescript
|
||
// lib/integrations/zorgdomein/types.ts
|
||
|
||
interface ZorgdomeinConnector {
|
||
// Nieuwe verwijzingen ophalen
|
||
getNewReferrals(since: Date): Promise<ZorgdomeinVerwijzing[]>;
|
||
|
||
// Verwijzing accepteren/afwijzen
|
||
acceptReferral(verwijzingId: string): Promise<void>;
|
||
rejectReferral(verwijzingId: string, reden: string): Promise<void>;
|
||
|
||
// Status terugkoppelen
|
||
updateReferralStatus(verwijzingId: string, status: string): Promise<void>;
|
||
|
||
// Health check
|
||
isAvailable(): Promise<boolean>;
|
||
}
|
||
|
||
interface ZorgdomeinVerwijzing {
|
||
id: string;
|
||
verwijzerAgbCode: string; // Huisarts AGB-code
|
||
verwijzerNaam: string;
|
||
patientBsn: string;
|
||
patientNaam: string;
|
||
verwijsDatum: Date;
|
||
urgentie: 'normaal' | 'spoed';
|
||
klacht: string;
|
||
anamnese?: string;
|
||
diagnose?: string;
|
||
medicatie?: string;
|
||
bijlagen?: { naam: string; type: string; url: string }[];
|
||
}
|
||
```
|
||
|
||
### Integratie met intake workflow
|
||
|
||
```typescript
|
||
// Wanneer Zorgdomein actief is:
|
||
// 1. C38 (Scheduled Jobs) pollt Zorgdomein elke 15 minuten voor nieuwe verwijzingen
|
||
// 2. Nieuwe verwijzing → automatisch patient aanmaken (of matchen op BSN)
|
||
// 3. C35 (Workflow Engine) start intake_ggz workflow
|
||
// 4. C34 (Notificatie) meldt aan triagist: "Nieuwe verwijzing via Zorgdomein"
|
||
```
|
||
|
||
---
|
||
|
||
## C43 — BI Module (voorbereiding)
|
||
|
||
### Doel
|
||
|
||
Business Intelligence laag: materialized views, data-export en voorbereiding op dashboards voor management rapportages. Niet een volledige BI-tool, maar de datalaag die BI mogelijk maakt.
|
||
|
||
### Waarom nodig
|
||
|
||
- **Stuurinformatie**: Productiecijfers per behandelaar, team, afdeling
|
||
- **Wachttijden**: Gemiddelde wachttijd aanmelding → intake (Treeknormen)
|
||
- **Kwaliteitsindicatoren**: ROM-scores, uitval, no-shows
|
||
- **Financieel**: Omzet per periode, ongedeclareerde prestaties
|
||
- **Bezetting**: Bed/verblijfsbezetting, personeel/patiënt ratio
|
||
|
||
### Interface
|
||
|
||
```typescript
|
||
// lib/bi/types.ts
|
||
|
||
interface BIQuery {
|
||
tenantId: string;
|
||
metric: BIMetric;
|
||
dimensions?: BIDimension[];
|
||
dateRange: { from: Date; to: Date };
|
||
filters?: Record<string, string | string[]>;
|
||
}
|
||
|
||
type BIMetric =
|
||
| 'productie_uren'
|
||
| 'aantal_consulten'
|
||
| 'wachttijd_intake'
|
||
| 'no_show_percentage'
|
||
| 'bezettingsgraad'
|
||
| 'omzet'
|
||
| 'ongedeclareerd'
|
||
| 'gemiddelde_behandelduur'
|
||
| 'uitval_percentage'
|
||
| 'incidenten_per_maand';
|
||
|
||
type BIDimension =
|
||
| 'behandelaar'
|
||
| 'team'
|
||
| 'afdeling'
|
||
| 'locatie'
|
||
| 'zorgtype'
|
||
| 'diagnose_groep'
|
||
| 'maand'
|
||
| 'week';
|
||
|
||
interface BIResult {
|
||
metric: BIMetric;
|
||
dimensions: Record<string, string>;
|
||
value: number;
|
||
previousValue?: number; // Vorige periode voor vergelijking
|
||
trend?: 'up' | 'down' | 'stable';
|
||
}
|
||
|
||
interface BIService {
|
||
query(query: BIQuery): Promise<BIResult[]>;
|
||
refreshViews(tenantId: string): Promise<void>;
|
||
exportCSV(query: BIQuery): Promise<string>; // CSV content
|
||
getAvailableMetrics(): BIMetric[];
|
||
}
|
||
```
|
||
|
||
### Materialized views
|
||
|
||
```sql
|
||
-- Productie per behandelaar per maand
|
||
CREATE MATERIALIZED VIEW mv_productie_per_behandelaar AS
|
||
SELECT
|
||
z.tenant_id,
|
||
z.behandelaar_id,
|
||
u.display_name AS behandelaar_naam,
|
||
d.name AS afdeling,
|
||
DATE_TRUNC('month', z.datum) AS maand,
|
||
COUNT(*) AS aantal_prestaties,
|
||
SUM(z.duur_minuten) AS directe_minuten,
|
||
SUM(z.indirecte_tijd) AS indirecte_minuten,
|
||
SUM(z.duur_minuten + COALESCE(z.indirecte_tijd, 0)) AS totaal_minuten
|
||
FROM zorgprestaties z
|
||
JOIN users u ON u.id = z.behandelaar_id
|
||
LEFT JOIN departments d ON d.id = u.department_id
|
||
WHERE z.status != 'concept'
|
||
GROUP BY z.tenant_id, z.behandelaar_id, u.display_name, d.name, DATE_TRUNC('month', z.datum);
|
||
|
||
CREATE UNIQUE INDEX idx_mv_productie
|
||
ON mv_productie_per_behandelaar (tenant_id, behandelaar_id, maand);
|
||
|
||
-- Wachttijden (aanmelding → intake)
|
||
CREATE MATERIALIZED VIEW mv_wachttijden AS
|
||
SELECT
|
||
wi.tenant_id,
|
||
DATE_TRUNC('month', wi.created_at) AS maand,
|
||
wi.current_state,
|
||
COUNT(*) AS aantal,
|
||
AVG(EXTRACT(DAY FROM (
|
||
CASE WHEN wtl.to_state = 'triage'
|
||
THEN wtl.created_at ELSE NULL END
|
||
) - wi.created_at)) AS gem_dagen_tot_triage,
|
||
AVG(EXTRACT(DAY FROM (
|
||
CASE WHEN wtl.to_state = 'gestart'
|
||
THEN wtl.created_at ELSE NULL END
|
||
) - wi.created_at)) AS gem_dagen_tot_start
|
||
FROM workflow_instances wi
|
||
LEFT JOIN workflow_transitions_log wtl ON wtl.instance_id = wi.id
|
||
WHERE wi.entity_type = 'intake'
|
||
GROUP BY wi.tenant_id, DATE_TRUNC('month', wi.created_at), wi.current_state;
|
||
|
||
-- No-show percentage per maand
|
||
CREATE MATERIALIZED VIEW mv_no_shows AS
|
||
SELECT
|
||
a.tenant_id,
|
||
DATE_TRUNC('month', a.start_time) AS maand,
|
||
COUNT(*) AS totaal_afspraken,
|
||
COUNT(*) FILTER (WHERE a.status = 'no_show') AS no_shows,
|
||
ROUND(
|
||
COUNT(*) FILTER (WHERE a.status = 'no_show')::DECIMAL /
|
||
NULLIF(COUNT(*), 0) * 100, 1
|
||
) AS no_show_percentage
|
||
FROM appointments a
|
||
WHERE a.start_time < NOW()
|
||
GROUP BY a.tenant_id, DATE_TRUNC('month', a.start_time);
|
||
|
||
-- Refresh via scheduled job (C38)
|
||
-- REFRESH MATERIALIZED VIEW CONCURRENTLY mv_productie_per_behandelaar;
|
||
-- REFRESH MATERIALIZED VIEW CONCURRENTLY mv_wachttijden;
|
||
-- REFRESH MATERIALIZED VIEW CONCURRENTLY mv_no_shows;
|
||
```
|
||
|
||
### Integratiepunten
|
||
|
||
| Bestaand component | Integratie |
|
||
|-------------------|------------|
|
||
| C17 Agenda Service | No-show data, bezettingsdata |
|
||
| C35 Workflow Engine | Wachttijden (intake doorlooptijden) |
|
||
| C37 Caching | BI resultaten cachen (namespace `api:dashboard`) |
|
||
| C38 Scheduled Jobs | `REFRESH MATERIALIZED VIEW CONCURRENTLY` elke 15-60 min |
|
||
| C40 ZPM Facturatie | Omzet en productiecijfers |
|
||
| C25 RBAC + ABAC | Alleen management/admin rollen mogen BI queries draaien |
|
||
|
||
### Toekomstige uitbreiding
|
||
|
||
Als BI-behoeften groeien voorbij wat materialized views aankunnen:
|
||
- **Metabase** (self-hosted, open source) als BI frontend bovenop dezelfde PostgreSQL
|
||
- **dbt** voor data transformaties als de views complexer worden
|
||
- Export naar extern data warehouse indien nodig
|
||
|
||
---
|
||
|
||
## Bijgewerkt Component Overzicht (volledig)
|
||
|
||
```
|
||
LAAG 1: INTERFACE
|
||
├── C01 Traditionele EPD UI
|
||
├── C02 Intent Command Center
|
||
├── C03 Spraak Input
|
||
└── C04 Block/Artifact systeem
|
||
|
||
LAAG 2: INTENT SYSTEEM
|
||
├── C05 Intent Registry
|
||
├── C06 Reflex Classifier
|
||
├── C07 Orchestrator
|
||
├── C08 Entity Extractor + Date Parser
|
||
├── C09 Action System
|
||
└── C10 Protocol Engine / Nudge
|
||
|
||
LAAG 3: KNOWLEDGE LAYER
|
||
├── C11 Protocol Rules Store
|
||
├── C12 RAG Pipeline
|
||
├── C13 Regelvalidatie UI
|
||
└── C14 Kennisbron Connectors
|
||
|
||
LAAG 4: DOMEIN / BUSINESS LOGIC
|
||
├── C15 Rapportage Service
|
||
├── C16 Overdracht Service
|
||
├── C17 Agenda Service
|
||
├── C18 Behandelplan Service
|
||
├── C19 Medicatie Service
|
||
├── C20 Intake Service
|
||
├── C21 Patiënt Service
|
||
└── C22 Toestemming Service
|
||
|
||
LAAG 5: ENTERPRISE FUNDAMENT
|
||
├── C23 Auth + SSO (Keycloak)
|
||
├── C24 Multi-tenancy (RLS)
|
||
├── C25 RBAC + ABAC Engine
|
||
├── C26 Audit Trail (NEN 7513)
|
||
├── C27 Field Encryption
|
||
├── C28 FHIR Export Service
|
||
├── C29 CDS Hooks Service
|
||
├── C30 Observability
|
||
├── C31 Circuit Breaker / Resilience
|
||
├── C32 Health Check Service
|
||
└── C33 PII Filter
|
||
|
||
LAAG 6: PLATFORM SERVICES ← NIEUW
|
||
├── C34 Notificatie Service
|
||
├── C35 Workflow Engine (State Machine)
|
||
├── C36 Full-Text Search Service
|
||
├── C37 Caching Layer
|
||
├── C38 Scheduled Jobs Service
|
||
└── C39 Webhook System
|
||
|
||
LAAG 7: EXTERNE INTEGRATIES ← NIEUW
|
||
├── C40 ZPM Facturatie Service
|
||
├── C41 Vecozo Connector (voorbereiding)
|
||
└── C42 Zorgdomein Connector (voorbereiding)
|
||
|
||
LAAG 8: ANALYTICS ← NIEUW
|
||
└── C43 BI Module (materialized views)
|
||
|
||
TOTAAL: 43 componenten (33 bestaand + 10 nieuw)
|
||
```
|
||
|
||
---
|
||
|
||
## Bijgewerkt Bouwplan (fasen)
|
||
|
||
De 10 nieuwe componenten passen als volgt in het bestaande 5-fasen bouwplan:
|
||
|
||
```
|
||
FASE 1: FUNDAMENT (week 1-4) — uitbreiding
|
||
├── (bestaand) PostgreSQL, Keycloak, Drizzle, Docker, Caddy
|
||
├── (bestaand) Multi-tenancy, Audit Trail, RBAC
|
||
├── C38 Scheduled Jobs ← NIEUW (fundament voor alles)
|
||
└── C37 Caching Layer ← NIEUW (Redis al aanwezig voor BullMQ)
|
||
|
||
FASE 2: EPD CORE (week 5-10) — uitbreiding
|
||
├── (bestaand) Rapportage, Overdracht, Agenda, Behandelplan
|
||
├── (bestaand) Intent systeem, Reflex, Orchestrator
|
||
├── C34 Notificatie Service ← NIEUW
|
||
├── C35 Workflow Engine ← NIEUW
|
||
└── C40 ZPM Facturatie ← NIEUW (basisregistratie, geen Vecozo)
|
||
|
||
FASE 3: KNOWLEDGE LAYER (week 11-14) — uitbreiding
|
||
├── (bestaand) Protocol Rules, RAG Pipeline, Nudge
|
||
├── C36 Full-Text Search ← NIEUW
|
||
└── C43 BI Module ← NIEUW (materialized views aanmaken)
|
||
|
||
FASE 4: CONNECTIVITEIT (week 15-18) — uitbreiding
|
||
├── (bestaand) FHIR Connector, Connector admin UI
|
||
├── C39 Webhook System ← NIEUW
|
||
├── C41 Vecozo Connector ← NIEUW (interface + mock, geen live)
|
||
└── C42 Zorgdomein Connector ← NIEUW (interface + mock, geen live)
|
||
|
||
FASE 5: COMPLIANCE & HARDENING (week 19-22)
|
||
├── (bestaand) Encryption, Wvggz, Observability, Load test, Pentest
|
||
└── Vecozo/Zorgdomein live aansluiting (indien traject loopt)
|
||
```
|
||
|
||
---
|
||
|
||
## Beslissingenlog (aanvullingen)
|
||
|
||
| # | Beslissing | Alternatieven | Rationale |
|
||
|---|-----------|---------------|-----------|
|
||
| D20 | Notificaties via SSE (geen WebSockets) | WebSockets, polling | Eenvoudiger, minder infra, notificaties zijn unidirectioneel |
|
||
| D21 | Eigen state machine (geen XState) | XState, Temporal.io | ~200 regels code, server-side, geen dependency bloat |
|
||
| D22 | PostgreSQL tsvector (geen Elasticsearch) | Elasticsearch, Meilisearch | Geen extra service, voldoende voor GGZ-schaal |
|
||
| D23 | Materialized views voor BI (geen apart warehouse) | Metabase direct, dbt, Snowflake | Start simpel, upgrade later als nodig |
|
||
| D24 | Webhook signing met HMAC-SHA256 | JWT, API keys | Industriestandaard (GitHub, Stripe), simpel te valideren |
|
||
| D25 | Geen PII in webhook payloads | Encrypted payloads | Defense in depth, ontvanger haalt data via API |
|
||
|
||
---
|
||
|
||
## Appendix: Cross-referentie nieuwe componenten × use cases
|
||
|
||
| Use Case | C34 | C35 | C36 | C37 | C38 | C39 | C40 | C41 | C42 | C43 |
|
||
|----------|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|
|
||
| UC-01 Dagnotitie | | | ● | ● | | ● | | | | |
|
||
| UC-05 Overdracht | ● | | ● | ● | | | | | | |
|
||
| UC-07 Zoeken | | | ● | ● | | | | | | |
|
||
| UC-10 Afspraak | ● | | | | | ● | ● | | | |
|
||
| UC-13 Intake status | ● | ● | | | | | | | ● | ● |
|
||
| UC-20 Nudge | ● | | | ● | | | | | | |
|
||
| UC-21 Behandelplan | ● | ● | | | | ● | ● | | | ● |
|
||
| UC-22 Medicatie | ● | | ● | | | | | | | |
|
||
| UC-23 Incident | ● | ● | ● | | | ● | | | | ● |
|
||
| UC-24 Wvggz | ● | ● | | | ● | | | | | |
|
||
| UC-26 Audit log | | | ● | | ● | | | | | |
|
||
| UC-30 Dashboard | | | | ● | ● | | | | | ● |
|
||
| UC-32 Groepstherapie | | | | | | | ● | | | ● |
|
||
| UC-33 Break-the-glass | ● | | | | | | | | | |
|
||
|
||
● = component is betrokken bij deze use case
|
||
|
||
---
|
||
|
||
## Appendix: Relatie tussen alle architectuurdocumenten (bijgewerkt)
|
||
|
||
```
|
||
docs/architecture/
|
||
│
|
||
├── intent-system-architectuur-nl.md
|
||
│ Beschrijft: Hoe het intent systeem intern werkt
|
||
│ Scope: 5 bouwblokken, classificatie, entity resolution, nudge
|
||
│
|
||
├── enterprise-epd-architectuur.md
|
||
│ Beschrijft: Enterprise EPD schil (multi-tenancy, audit, security)
|
||
│ Stack: Supabase + Vercel (VERVANGEN door platform-connectiviteit-selfhosted.md)
|
||
│ Status: Functioneel nog geldig, stack is gewijzigd
|
||
│
|
||
├── usecases-technische-componenten.md
|
||
│ Beschrijft: 33 use cases × 34 componenten validatie
|
||
│ Scope: Gap analyse, bouwvolgorde, kruistabel
|
||
│ Status: Uitgebreid met 10 nieuwe componenten (zie dit document)
|
||
│
|
||
├── platform-connectiviteit-selfhosted.md
|
||
│ Beschrijft: Self-hosted stack, platform splitsing, connectors
|
||
│ Vervangt: Stack keuzes uit enterprise-epd-architectuur.md
|
||
│ Voegt toe: Connector API, deployment modellen, Keycloak, Drizzle
|
||
│
|
||
└── enterprise-componenten-uitbreiding.md ← DIT DOCUMENT
|
||
Beschrijft: 10 nieuwe componenten (C34–C43) als voorbereiding
|
||
Scope: Interfaces, database schemas, integratiepunten, fasering
|
||
Status: Voorbereiding — nog niet gebouwd
|
||
```
|