Nazendingen (Backend)
Technisch Ontwerp
De module bestaat uit drie samenhangende lagen, allemaal tenant-gescopet via het schema:
- Een werklijst (
Nazending) met een statusmodel (active/resolved). Elke regel houdt geneesmiddel, ZI-nummer, aantal, verwachte leverdatum en de nog uit te voeren actie bij. Regels kunnen handmatig zijn ingevoerd of afkomstig zijn uit een groothandel-import. - Een groothandel-import die backorder-bestanden van Alliance Healthcare en Pluripharm inleest, diff-t tegen de bestaande regels en de werklijst bijwerkt. De import werkt per
NazendingImportProfileen verloopt in twee stappen: een voorbeeld (preview) zonder schrijfacties en een bevestiging (confirm) die de wijzigingen atomic toepast. - Een verrijkings-laag die per nazending met ZI-nummer een Farmanco-blok toevoegt zodra de apotheek haar eigen KNMP Farmanco-abonnement heeft geverifieerd (
TenantFarmancoAccess). De Farmanco-snapshot wordt elke nacht door de scraper-module ververst (features.scraper).
Import-pijplijn (preview en confirm)
De import is bewust stateless: het bestand wordt zowel bij preview als bij confirm meegestuurd en het plan wordt beide keren deterministisch herberekend. Er wordt geen tussenstand op de server bewaard.
flowchart TD
Upload[Upload bestand + profiel] --> Parse["parse_upload(profile, bytes)"]
Parse -->|wholesaler-parser| Rows["ParseResult: rows + errors"]
Rows --> Plan["build_plan(...)"]
Plan --> Diff{Per regel}
Diff -->|nieuw| New[exclude_recent_order_days filter]
Diff -->|bestaand| Refresh[functionele velden + last_seen_at]
Refresh --> Reappear{Eerder handmatig afgehandeld en verdwenen?}
Reappear -->|ja| Choice[reappeared lijst: gebruikerskeuze]
Reappear -->|nee/missing_from_import| Auto[auto-heractiveren]
Plan --> Cleanup{cleanup_missing?}
Cleanup -->|ja| Mark[afwezige active regels: resolved missing_from_import]
Plan -->|preview| Counts[counts + reappeared + cleanup_warning]
Plan -->|confirm| Tx["transaction.atomic"]
Tx --> Lock["select_for_update op profiel"]
Lock --> Apply["apply_plan: bulk_create / bulk_update"]
Apply --> Save[profiel.last_uploaded_at + instellingen]
Farmanco-verrijking en scraper
flowchart TD
UI[Frontend nazendingen] -->|GET /api/nazendingen/| List[NazendingenListCreateView]
List -->|_build_farmanco_index_for| Verify{is_tenant_verified}
Verify -->|nee| Empty[Geen farmanco-blok]
Verify -->|ja| FarmancoZi[(FarmancoZi snapshot public-schema)]
FarmancoZi --> Block[farmanco-blok per nazending]
UI -->|POST farmanco/verify| Verifier[FarmancoVerifyView]
Verifier --> RBAC{HasPermissionCode farmanco.verify}
RBAC -->|denied| F403[403]
Verifier --> Rate{Rate-limit 5/10min}
Verifier --> Service["verify_farmanco_credentials"]
Service --> Farmanco[(farmanco.knmp.nl)]
Service -->|ok| Mark["mark_verified TTL 90d"]
Mark --> Access[(TenantFarmancoAccess)]
Bedrijfslogica leeft in services:
features.nazendingen.import_servicevoor de parse/plan/apply-pijplijn van de groothandel-import.features.nazendingen.wholesalersvoor de per-groothandel parsers en de registry.features.nazendingen.farmanco_servicevoor verificatie en access-state.features.nazendingen.servicesvoor de Excel-export (build_xlsx).features.nazendingen.tasksvoor de nachtelijke opschoning van afgehandelde regels (auto_purge_resolved_nazendingen).
De nightly Farmanco-scraper (features.scraper.tasks.run_farmanco_scraper) logt eenmalig in met het platform-account uit KNMP_USERNAME / KNMP_PASSWORD en mirrort de volledige snapshot atomic. De scraper deelt geen code met de tenant-verificatie-flow; dat blijft een bewuste scheiding zodat per-tenant credentials nooit met de scraper-pipeline in aanraking komen.
Datamodel (ERD)
erDiagram
NazendingImportProfile ||--o{ Nazending : source_profile
Nazending ||--o| User : created_by
Nazending {
uuid id PK
string zi_nummer
string geneesmiddelnaam
date verwacht_geleverd
string verwacht_geleverd_tekst
int aantal
text actie
string status
datetime resolved_at
string resolved_reason
string source_type
uuid source_profile_id FK
string source_key
datetime last_seen_at
bigint created_by_id FK
datetime created_at
datetime updated_at
}
NazendingImportProfile {
uuid id PK
string supplier
string name
smallint exclude_recent_order_days
bool cleanup_missing
datetime last_uploaded_at
datetime created_at
datetime updated_at
}
NazendingenSettings {
int id PK
smallint auto_purge_resolved_days
datetime created_at
datetime updated_at
}
TenantFarmancoAccess ||--o| User : verified_by
TenantFarmancoAccess {
int id PK
string status
datetime verified_at
bigint verified_by_id FK
datetime expires_at
datetime last_check_at
}
FarmancoShortage ||--o{ FarmancoZi : zi_rows
FarmancoShortage {
int id PK
string source_url
string source_id
string name
string atc_code
int impact_state
string afhandeling_label
date last_edit_date
datetime last_scraped_at
}
FarmancoZi {
int id PK
int shortage_id FK
string zi_nummer
string kind
string solution_type
string product_name
string manufacturer
string package_size
string expected_delivery_text
string expected_delivery_kind
string reason
}
FarmancoScrapeRun {
int id PK
datetime started_at
datetime finished_at
string status
int parents_count
int zi_count
text error_message
}
Nazending }o..o{ FarmancoZi : "lookup op zi_nummer"
Nazending, NazendingImportProfile, NazendingenSettings (singleton id=1) en TenantFarmancoAccess (singleton id=1) leven in het tenant-schema. FarmancoShortage, FarmancoZi en FarmancoScrapeRun leven in het public-schema (scraper-module) zodat een nightly run alle tenants tegelijk bedient. Nazending.source_key is een stabiele SHA256 (make_source_key) die idempotentie per profiel garandeert; de unieke constraint (source_profile, source_key) voorkomt dubbele regels bij herhaalde uploads.
API & Communicatie
Werklijst
GET /api/nazendingen/- pagina-genummerde lijst (results,page,page_size,total,total_pages,status_counts). Filters:q(ZI/naam),status(active/resolved),source(manual/alliance/pluripharm),profile(UUID),sort(naam/zi/datum, op- of aflopend). Voor geverifieerde apotheken bevat elke regel met ZI-nummer eenfarmanco-blok (in_shortage,impact_state,expected_delivery_text/expected_delivery_kind,afhandeling_label,reason,suggested_alternatives,source_url,last_scraped_at); anders isfarmancoaltijdnull.POST /api/nazendingen/- handmatige nazending toevoegen (minimaalgeneesmiddelnaam).GET /api/nazendingen/{id}/- detail.PATCH /api/nazendingen/{id}/- bewerken. Bij geimporteerde regels (source_profilegezet) is alleenactieaanpasbaar; wijzigen van bron-velden (zi_nummer,geneesmiddelnaam,verwacht_geleverd) geeft400.DELETE /api/nazendingen/{id}/- verwijderen. Een actieve geimporteerde regel kan niet worden verwijderd (409); die moet eerst worden afgehandeld.POST /api/nazendingen/{id}/resolve/- regel opresolvedzetten (resolved_reason=manual).POST /api/nazendingen/{id}/reactivate/- regel terug opactivezetten.GET /api/nazendingen/export/- Excel-export met twee tabbladen (Actief en Afgehandeld). Rate-limited viaScopedRateThrottlescopenazending_export.
Groothandel-import
GET /api/nazendingen/import/wholesalers/- registry van ondersteunde groothandels met geaccepteerde bestandsextensies en MIME-types.POST /api/nazendingen/import/preview/- multipart (file,profile,exclude_recent_order_days,cleanup_missing). Berekent het plan zonder te schrijven en geeft de counts terug (nieuw,bijgewerkt,opnieuw_actief,heractiveerd,afgehandeld_cleanup,uitgesloten,fouten) plusreappeared,active_countencleanup_warning.POST /api/nazendingen/import/confirm/- zelfde payload plusreactivate_source_keys. Vergrendelt het profiel (select_for_update), herberekent het plan deterministisch uit het opnieuw meegestuurde bestand en past het atomic toe.
Profielen en instellingen
GET /api/nazendingen/profiles/- lijst importprofielen (optioneelsupplier-filter).POST /api/nazendingen/profiles/- profiel aanmaken (naam wordt automatisch gevuld met de tenantnaam als die leeg is).PATCH /api/nazendingen/profiles/{id}/- profiel bijwerken.supplieris onveranderbaar (400bij wijziging).DELETE /api/nazendingen/profiles/{id}/- profiel verwijderen;409als er nog actieve regels naar verwijzen (PROTECT).GET /api/nazendingen/settings/enPATCH /api/nazendingen/settings/-auto_purge_resolved_days(aantal dagen, ofnullom de automatische opschoning uit te zetten).
KNMP Farmanco-koppeling
GET /api/nazendingen/farmanco/access/- huidige koppelingsstaat (status,verified_at,expires_at,last_check_at,is_currently_verified).POST /api/nazendingen/farmanco/verify/- eenmalige verificatie. Verstuurt KNMP-nummer en wachtwoord naar de KNMP-loginpagina, controleert de respons, gooit de credentials weg en markeert de tenant als geverifieerd.POST /api/nazendingen/farmanco/disconnect/- koppeling intrekken.
Achtergrondtaken
auto_purge_resolved_nazendingen- nachtelijke Celery-taak die per tenant de afgehandelde regels die ouder zijn danauto_purge_resolved_daysdefinitief verwijdert. Staat de instelling opnull, dan gebeurt er niets.
Foutafhandeling & Statuscodes
400- validatie-fout: lege geneesmiddelnaam, ongeldig ZI-nummer, bron-veld van een geimporteerde regel bewerken, ofsuppliervan een profiel wijzigen.401- verificatie tegen Farmanco mislukt (ongeldige credentials, netwerkfout of upstream-fout). Het foutbericht is mens-leesbaar maar bevat nooit het ingevoerde wachtwoord.403- onvoldoende rolrechten (nazending.view,nazending.editoffarmanco.verify).404- nazending of profiel niet gevonden of niet binnen de tenant.409- een actieve geimporteerde regel verwijderen, of een profiel verwijderen waar nog actieve regels naar verwijzen.429- meer dan 5 mislukte verificatie-pogingen in 10 minuten voor dezelfde apotheek, of de export-throttle.
Autorisatie & Beveiliging
- Alle endpoints zijn tenant-gescopet via het schema en
HasPermissionCode. nazending.viewis nodig voor lezen en export,nazending.editvoor alle mutaties op de werklijst, de import en de profielen/instellingen.farmanco.verifyis nodig voor de verificatie- en ontkoppelings-endpoints en voor het tonen van de koppel-knop in de UI.- Er is geen aanmaker-gebonden beperking op bewerken of verwijderen: elke gebruiker met
nazending.editmag elke regel binnen de tenant muteren. De enige beperkingen zijn afkomstig van de import (geimporteerde regels: alleenactieaanpasbaar; actieve geimporteerde regels niet verwijderbaar). - Verificatie van tenant-credentials gebeurt eenmalig in
verify_farmanco_credentials. Het wachtwoord wordt direct na de POST-call uit de lokale variabele losgelaten, komt nooit in de database, nooit in Celery, nooit in logging en nooit in e-mailrapporten. Een dedicated test (PasswordNeverLoggedTests) bewaakt dat het wachtwoord niet in logging-output verschijnt voor het faalpad. - KNMP Kennisbank-licenties lopen per kalenderjaar en worden stilzwijgend verlengd. Remedice valideert echter elke 90 dagen opnieuw zodat ook tussentijdse veranderingen worden opgevangen (gewijzigd KNMP-wachtwoord, opgezegd abonnement). De vervaldatum staat zichtbaar als "Geldig tot dd-mm-jjjj" op de statuskaart. Na verloop verdwijnt de Farmanco-knop op alle rijen totdat er opnieuw is geverifieerd. De TTL is configureerbaar via
FARMANCO_VERIFICATION_TTL_DAYS(standaard 90). - Het uploadbestand wordt begrensd via
NAZENDINGEN_IMPORT_MAX_BYTES(10 MB) enNAZENDINGEN_IMPORT_MAX_ROWS(20000). Decleanup_warningwaarschuwt bij grote opschoonacties (NAZENDINGEN_IMPORT_CLEANUP_WARN_RATIO0.5 en minimaalNAZENDINGEN_IMPORT_CLEANUP_WARN_MIN_ACTIVE5 actieve regels).
Bestandsstructuur & Verantwoordelijkheden
models.py-Nazending,NazendingImportProfile,NazendingenSettings,TenantFarmancoAccessen de bijbehorendeTextChoices.serializers.py- serializers voor de werklijst, het Farmanco-blok, de import (preview/confirm request en response), de profielen, de instellingen en de access-state.views.py- de werklijst-views (list/create, detail, resolve, reactivate), de Excel-export, de import-views (wholesalers, preview, confirm), de profiel- en settings-views en de drie Farmanco-endpoints. Bevat_build_farmanco_index_forvoor de N+1-veilige bulk-lookup.import_service.py-parse_upload,build_planenapply_plan: de stateless diff-pijplijn met counts, reappeared-logica en cleanup.wholesalers/-base.py(datastructuren enWholesalerParser),registry.py(Alliance + Pluripharm),normalise.py(make_source_key, datum- en aantal-normalisatie) en de parsers onderparsers/.farmanco_service.py- eenmalige login-roundtrip naar Farmanco en persistlogica voorTenantFarmancoAccess.services.py-build_xlsxvoor de Excel-export.tasks.py-auto_purge_resolved_nazendingen.urls.py- routedefinities.tests/- CRUD-tests, import-pijplijn (parse/plan/apply, reappeared, cleanup), Farmanco-access (gelukkig pad, ongeldig wachtwoord, netwerkfout, RBAC, rate-limit, wachtwoord-niet-gelogd) en enrichment.
Belangrijke bestanden
backend/features/nazendingen/views.pybackend/features/nazendingen/import_service.pybackend/features/nazendingen/wholesalers/registry.pybackend/features/nazendingen/farmanco_service.pybackend/features/nazendingen/tasks.pybackend/features/scraper/tasks.py(run_farmanco_scraper)