Ga naar inhoud

Nazendingen (Backend)

Technisch Ontwerp

De module bestaat uit drie samenhangende lagen, allemaal tenant-gescopet via het schema:

  1. 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.
  2. 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 NazendingImportProfile en verloopt in twee stappen: een voorbeeld (preview) zonder schrijfacties en een bevestiging (confirm) die de wijzigingen atomic toepast.
  3. 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_service voor de parse/plan/apply-pijplijn van de groothandel-import.
  • features.nazendingen.wholesalers voor de per-groothandel parsers en de registry.
  • features.nazendingen.farmanco_service voor verificatie en access-state.
  • features.nazendingen.services voor de Excel-export (build_xlsx).
  • features.nazendingen.tasks voor 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 een farmanco-blok (in_shortage, impact_state, expected_delivery_text / expected_delivery_kind, afhandeling_label, reason, suggested_alternatives, source_url, last_scraped_at); anders is farmanco altijd null.
  • POST /api/nazendingen/ - handmatige nazending toevoegen (minimaal geneesmiddelnaam).
  • GET /api/nazendingen/{id}/ - detail.
  • PATCH /api/nazendingen/{id}/ - bewerken. Bij geimporteerde regels (source_profile gezet) is alleen actie aanpasbaar; wijzigen van bron-velden (zi_nummer, geneesmiddelnaam, verwacht_geleverd) geeft 400.
  • 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 op resolved zetten (resolved_reason=manual).
  • POST /api/nazendingen/{id}/reactivate/ - regel terug op active zetten.
  • GET /api/nazendingen/export/ - Excel-export met twee tabbladen (Actief en Afgehandeld). Rate-limited via ScopedRateThrottle scope nazending_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) plus reappeared, active_count en cleanup_warning.
  • POST /api/nazendingen/import/confirm/ - zelfde payload plus reactivate_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 (optioneel supplier-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. supplier is onveranderbaar (400 bij wijziging).
  • DELETE /api/nazendingen/profiles/{id}/ - profiel verwijderen; 409 als er nog actieve regels naar verwijzen (PROTECT).
  • GET /api/nazendingen/settings/ en PATCH /api/nazendingen/settings/ - auto_purge_resolved_days (aantal dagen, of null om 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 dan auto_purge_resolved_days definitief verwijdert. Staat de instelling op null, dan gebeurt er niets.

Foutafhandeling & Statuscodes

  • 400 - validatie-fout: lege geneesmiddelnaam, ongeldig ZI-nummer, bron-veld van een geimporteerde regel bewerken, of supplier van 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.edit of farmanco.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.view is nodig voor lezen en export, nazending.edit voor alle mutaties op de werklijst, de import en de profielen/instellingen.
  • farmanco.verify is 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.edit mag elke regel binnen de tenant muteren. De enige beperkingen zijn afkomstig van de import (geimporteerde regels: alleen actie aanpasbaar; 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) en NAZENDINGEN_IMPORT_MAX_ROWS (20000). De cleanup_warning waarschuwt bij grote opschoonacties (NAZENDINGEN_IMPORT_CLEANUP_WARN_RATIO 0.5 en minimaal NAZENDINGEN_IMPORT_CLEANUP_WARN_MIN_ACTIVE 5 actieve regels).

Bestandsstructuur & Verantwoordelijkheden

  • models.py - Nazending, NazendingImportProfile, NazendingenSettings, TenantFarmancoAccess en de bijbehorende TextChoices.
  • 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_for voor de N+1-veilige bulk-lookup.
  • import_service.py - parse_upload, build_plan en apply_plan: de stateless diff-pijplijn met counts, reappeared-logica en cleanup.
  • wholesalers/ - base.py (datastructuren en WholesalerParser), registry.py (Alliance + Pluripharm), normalise.py (make_source_key, datum- en aantal-normalisatie) en de parsers onder parsers/.
  • farmanco_service.py - eenmalige login-roundtrip naar Farmanco en persistlogica voor TenantFarmancoAccess.
  • services.py - build_xlsx voor 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.py
  • backend/features/nazendingen/import_service.py
  • backend/features/nazendingen/wholesalers/registry.py
  • backend/features/nazendingen/farmanco_service.py
  • backend/features/nazendingen/tasks.py
  • backend/features/scraper/tasks.py (run_farmanco_scraper)

API & Communicatie (Swagger)