Operativsystemet för en vårdkoncern med femton kliniker

Att ersätta en spridd samling frikopplade verktyg med en plattform: publik sajt, bokning, patientjournaler, labblogistik, lager över flera lagerställen, bokföring, HR och annonsrapportering - bakom en enda admin med isolering per klinik och per region över nio personalroller.

13 kliniker på en plattform
9 personalroller, var och en med sin egen vy
8 system ersatta av ett
0 avbrott som kan stoppa en bokning
MIN ROLL
Arkitektur och all kod. Jag satte datamodellen, skrev backend och frontend, byggde adminen och sköter driften av infrastrukturen den står på.
STACK
Django 5.2, PostgreSQL, Redis, Celery, serverrenderad frontend, Nginx på härdad Ubuntu.
SVÅRAST
Inte funktionerna. Att få femton enheter att dela ett system utan att någon av dem kan se en annans patienter.

Situationen

En vårdkoncern med femton kliniker drevs på åtta saker som inte visste om varandra: en webbplats, en separat bokningswidget, ett kalkylark för lagret, ett andra kalkylark för laboratoriet, ett bokföringsprogram, en app för tidrapportering, en annonsdashboard och en delad inkorg. Varje siffra som betydde något fanns på minst två ställen, och där de inte stämde överens kunde ingen säga vilken som var rätt.

Det är inte ett teknikproblem. Det är ett affärsproblem som visar sig som ett teknikproblem, och det är precis den sortens arbete jag är bäst på.

Vad jag byggde

En enda Django-plattform som betjänar alla femton enheter: den publika webbplatsen, tidsbokningen, patientjournalerna, CRM:et, laboratorielogistiken, lagret över flera lagerställen, den fiskala bokföringen, HR och rapporteringen av annonskostnaderna. Allt bakom ett enda administrationsgränssnitt med isolering på klinik- och regionnivå för nio personalroller, så att en receptionist i en stad inte kan se en annan stads patienter och inte heller behöver tänka på varför.

Funktionerna är den ointressanta delen. Tre av systemen under dem är värda att beskriva, för de är skillnaden mellan mjukvara som fungerar den dag den lanseras och mjukvara du fortfarande litar på två år senare.

Ett samtyckesflöde som skulle hålla i domstol

Medicinskt samtycke under GDPR är inte en kryssruta med en tidsstämpel. Vad du måste kunna ta fram, senare, är exakt det dokument som en viss person godkände vid ett visst ögonblick - inte din nuvarande version av det.

Så flödet genererar dokumentet, visar det på skärmen, bekräftar identiteten med en kod skickad via SMS och lagrar den renderade PDF:en tillsammans med texten, det språk den visades på, adressen den signerades från och en hashkedja som binder den till journalen. Ändrar du mallen efteråt ändras det nästa patient ser, men ingenting av det tidigare patienter har gått med på.

Regeln under: samtycke ges till en mening, inte till en boolean. Om ditt system lagrar true har det lagrat fel sak.

Lagret som en liggare, inte som ett tal

De flesta lagersystem har en quantity-kolumn som koden skriver till. Den kolumnen blir fel förr eller senare - en race, en retry, en felklickning, en halvt genomförd överföring - och när den väl är fel går det inte att ta reda på varför, för beviset skrevs över av svaret.

Här lagras ingen kvantitet. Varje mottagning, överföring, förbrukning och korrigering är en oföränderlig post med tecken, och saldot för vilket lager som helst är summan av de poster som rör det. Korrigeringar lägger till inversen i stället för att redigera historiken. Det gör avstämning till en egenskap hos designen i stället för en månatlig uppgift, och det betyder att svaret på ”varför har vi elva av de här?” alltid finns och alltid är fullständigt.

Längre ner på sidan finns en fungerande version av idén.

En kalender som inte kan dubbelboka

Femton klinikers bokningar överlappar hela tiden, och receptionen behöver se en hel dag på en gång utan att två bokningar någonsin upptar samma plats. Det här är intervallschemaläggningsproblemet: sortera efter starttid, lägg sedan varje bokning i den första banan vars föregående bokning redan är slut.

Det som gör det värt att implementera ordentligt och inte ungefärligt är att greedy-varianten är bevisbart optimal - antalet banor den producerar är exakt det största antalet bokningar som överlappar i ett enda ögonblick, alltså en hård undre gräns som ingen algoritm kan slå. Du kan se att det håller i demon nedan.

Rumänsk verklighet

Fakturering, kassajournalen och drivrutiner för de fiskala kassaregister som lagen kräver. Protetikarbeten spårade via kurir mellan klinik och laboratorium. Tidrapportering med en liten desktopklient som skickar en heartbeat. Ingenting av det här är intressant ingenjörsarbete och allt tillsammans är skillnaden mellan ett system som klinikerna använder och ett de kringgår.

Beslutet jag skulle försvara hårdast

Varje externt beroende i plattformen gör fail open. Om SMS-leverantören ligger nere, om annons-API:et strypar förfrågningarna eller om faktureringstjänsten ger timeout, slutförs bokningen ändå och det återstående arbetet läggs i kö och markeras för en människa.

Alternativet - att neka en bokning för att en tredje part inte är tillgänglig - innebär att du lägger dina intäkter i händerna på någon annans drifttid. De interna skydden går åt andra hållet: rate limits, validering och honeypotten gör fail closed, för där finns ingen tredje part att skylla på.

Vad jag skulle göra annorlunda

Jag skulle bygga rapporteringslagret tidigare. Jag utformade liggaren och schemaläggningen rätt från början och lät rapporteringen växa fram av sig själv, vilket innebar att de första tre månadernas frågor från ledningen besvarades med frågor jag skrev för hand. Korrekt, men långsamt, och jag borde ha sett det komma.

PROVA · SCHEMALÄGGNINGSALGORITMEN

Kalenderpackaren, igång här.

Det här är intervallschemaläggningen som beskrivs ovan, i din webbläsare. Lägg till bokningar så fortsätter den använda så få krockfria banor som möjligt.

Det här är en interaktiv demo och kräver att JavaScript är aktiverat.

PROVA · LAGERLIGGAREN

Saldo härlett ur rörelser med tecken.

Samma liggardesign, förenklad till tre lagerställen. Ingenting här lagrar ett antal - bokför en rörelse och se summorna räknas om ur loggen.

Det här är en interaktiv demo och kräver att JavaScript är aktiverat.