Messungen importieren
POST /api/v1/import
Authentifiziert. Der zentrale Endpunkt. Ein Request lädt einen Batch von 1–50 Messungen für einen Patienten hoch. Der mode-Diskriminator bestimmt, wie der Patient identifiziert wird.
Die vier Modi
Das Top-Level-Objekt ist eine discriminated union auf mode. Jede Variante ist .strict() — unbekannte Felder führen zu 400 (bewusst, um Adapter-Bugs früh zu erkennen).
a) follow_up — bekannter Patient (bereits verknüpft)
{ "mode": "follow_up", "mkClientId": "<uuid>", "measurements": [ … ] }
Der Client kennt die interne Client-UUID (aus dem Mapping-Sync). Messungen werden dem Patienten direkt zugeordnet. Voraussetzung: gültige Einwilligung (Behandlungseinwilligung) für den Patienten — andernfalls 403 CONSENT_MISSING / 403 CONSENT_WITHDRAWN.
b) first_visit — unbekannter Patient (erster Upload)
{
"mode": "first_visit",
"devicePseudonym": "<32 hex>",
"initialsHint": "AB",
"dobMonthYearHint": "1985-03",
"measurements": [ … ]
}
Landet in der Pending-Queue zur manuellen Verknüpfung im Portal.
devicePseudonym: genau 32 Hex-Zeichen (^[0-9a-f]{32}$, Groß-/Kleinschreibung egal, wird kleingeschrieben). SiehedevicePseudonymunten.initialsHint: 1–3 Großbuchstaben^[A-ZÄÖÜ]+$(nur visueller Hinweis im Portal).dobMonthYearHint:^\d{4}-(0[1-9]|1[0-2])$(Geburtsmonat/-jahr, nur visueller Hinweis).
c) pseudonym_only — Pseudonym ohne Hinweise (Fallback)
{ "mode": "pseudonym_only", "devicePseudonym": "<32 hex>", "measurements": [ … ] }
Messungen werden nur akzeptiert, wenn das Pseudonym bereits serverseitig verknüpft ist. Andernfalls kein Puffern → Response mit Warnung resend_with_hints (der Client soll mit first_visit + Hinweisen erneut senden).
d) token — persistenter Re-Export (bevorzugt für Folgesendungen)
{ "mode": "token", "persistentToken": "tk_<32 hex>", "devicePseudonym": "<32 hex>?", "measurements": [ … ] }
persistentToken (^tk_[0-9a-f]{32}$) identifiziert stabil einen bereits verknüpften Patienten. Ein optionales devicePseudonym wird nur zur Stale-Erkennung verwendet (weicht es vom Patienten des Tokens ab, gewinnt der Token; Warnung stale_pseudonym). Ein fremder Token (andere Praxis) → 403 TOKEN_FOREIGN.
Messobjekt (identisch in allen Modi)
Pflichtfelder je Messung: eye (OD|OS), measuredAt (ISO-8601), sourceAdapterId (1–64), sourceDevice (1–128). Optional dateOnly: true (Datum ohne Uhrzeit → Mitternacht UTC, Portal zeigt nur das Datum). Alle klinischen Felder sind optional/nullable; jedes hat ein Plausibilitäts-min/max (Bereichsverletzung → 400).
| Gruppe | Felder (Einheit) |
|---|---|
| Quelle | sourceAdapterId, sourceSerial?, sourceDataId?, sourceDevice, sourceDeviceName? |
| Refraktion | sphere (−30…30 D), cylinder (−15…15 D), axis (0…180°), visualAcuitySC/visualAcuityCC (0…2), cycloplegia (bool), method (≤32), addition (0…5 D), vertex (5…25 mm), prism (0…40), prismBase (0…360°), sphericalEquivalent (−30…30 D) |
| Biometrie | axialLength (10…45 mm), anteriorChamberDepth (1…7 mm), lensThickness (1…8 mm), vitreousChamberDepth (5…35 mm), axialSnr (≥0), axialMeasurementCount (int ≥0), axialStandardDeviation (≥0), cornealThickness (100…1500 µm), choroidalThickness (10…1000 µm), pupilDiameter (1…12 mm), pupilLighting (≤32), pupilCenterX/pupilCenterY, cornealDiameter (5…18 mm), bodyHeight (30…250 cm) |
| Keratometrie | cornealAstigmatism (0…20 D), cornealAxis1/cornealAxis2 (0…180°), cornealRadius1/cornealRadius2 (4…12 mm) |
| Patient | gender, ethnicity (feste Enums) |
sourceSerial + sourceDataId sind die Bausteine der Duplikatserkennung.
Erfolgsresponse (200)
Der Import antwortet immer mit einem Zählerobjekt (auch bei Teilfehlern):
{
"success": true,
"data": {
"total": 4,
"uploaded": 3,
"duplicate": 1,
"assigned": 0,
"unmatched": 3,
"errors": 0,
"results": [ { "eye": "OD", "status": "stored" }, { "eye": "OS", "status": "duplicate" } ],
"persistent_token": null,
"skip_pseudonyms": [ "a1b2…", "f5e6…" ],
"warnings": [ { "code": "rejected_pseudonym", "message": "…" } ]
}
}
| Feld | Bedeutung |
|---|---|
total | Anzahl gesendeter Messungen |
uploaded | Neu gespeichert |
duplicate | Als Duplikat erkannt (nicht erneut gespeichert) |
assigned | Direkt einem Patienten zugeordnet (nur follow_up/token) |
unmatched | In die Pending-Queue eingestellt (nur first_visit) |
errors | Messungen ohne verwertbare Werte u.ä. |
results[] | Pro Messung: eye + status (stored|duplicate|rejected) + optionaler errorCode |
persistent_token | Bei verknüpftem Patienten das stabile tk_…-Token für künftige token-Uploads, sonst null |
skip_pseudonyms[] | Immer vorhanden — alle aktuell abgelehnten Pseudonyme der Praxis; der Client cacht sie und überspringt sie beim nächsten Export (identisch mit GET /rejected-clients) |
warnings[] | z.B. rejected_pseudonym, resend_with_hints, stale_pseudonym |
Duplikat-Semantik
Ein Import ist ein Duplikat, wenn dieselbe logische Messung bereits existiert. Serverseitig via INSERT … ON CONFLICT DO NOTHING/UPDATE auf dem Natural Key (account_id, device_pseudonym, eye, measured_at in der Pending-Queue, oder Mess-Uniques beim verknüpften Patienten). duplicate: true bedeutet nur „bereits vorhanden” — der Status der vorhandenen Zeile (auch rejected) bleibt unverändert.
devicePseudonym — der Patientenbezeichner
- Ein stabiler, clientseitig berechneter Hash pro Patient (in der Referenzimplementierung: 128-Bit-gekürztes HMAC-SHA256 → 32 Hex-Zeichen). Der Server berechnet ihn nicht und prüft nur das Format (
^[0-9a-f]{32}$). - Für die OEM-Integration ist er ein opaker, stabiler Schlüssel: gleicher Patient + gleicher Client ⇒ gleiches Pseudonym. Das Backend behandelt ihn pro Praxis (
account_id) als eindeutigen Patientenschlüssel — mehr muss es nicht darüber wissen. - Er ist pro Praxis eindeutig (tenant-scoped), nicht global.