feat: Notruf mit Kontaktdaten + Geoposition anreichern (Backend, Phase 1+3)
Notruf-Meldungen tragen jetzt das Notfall-Profil der auslösenden Person und —
falls vom Gerät mitgeschickt — die Geoposition. Kanal-Rollen:
- E-Mail: Voll-Dossier (Name, Adresse, Telefone, Geburtsdatum, med. Hinweise,
Koordinaten + Karten-Link + Genauigkeit + Zeit; sonst "nicht verfügbar")
- SMS: kompakt, Name + Adresse + Karten-Link
- Anruf (TTS): Name + Wohnadresse gesprochen, KEINE URL/Koordinaten, Verweis
auf SMS/E-Mail
- schemas: GeoFix + EmergencyRequest.geo; AdminUserPrefsUpdate um Profilfelder
erweitert (full_name, Adresse, Telefone, birth_date, medical_notes,
emergency_contacts/_phones, location_consent) — in user.prefs (keine Migration)
- api/chat: geo an record_manual_emergency durchgereicht
- api/admin: list_users gibt Profilfelder mit zurück (für die spätere Admin-UI)
- emergency.py: _profile/_geo_fields/_maps_link/_location_block/_email_body,
kanal-spezifische _emergency_texts; record_manual_emergency(..., geo=...)
- Tests: 9 neue (Profil-Injektion, SMS/Call/E-Mail-Texte, Geo→Map-Link,
ohne-Geo-Fallback, Payload). Suite 237 grün.
Frontend (Phase 2: Browser-Geolocation beim 🆘 + Admin-Profil-UI) steht noch aus.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ea0e3674ad
commit
5ed4a386bb
5 changed files with 265 additions and 30 deletions
108
tests/test_emergency_enrichment.py
Normal file
108
tests/test_emergency_enrichment.py
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import asyncio
|
||||
|
||||
import app.safety.emergency as em
|
||||
|
||||
|
||||
class FakeUser:
|
||||
id = "u1"
|
||||
display_name = "Anzeigename"
|
||||
|
||||
def __init__(self, prefs):
|
||||
self.prefs = prefs
|
||||
|
||||
|
||||
FULL_PREFS = {
|
||||
"full_name": "Frieda Schlüter",
|
||||
"street": "Hauptstraße 12",
|
||||
"postal_code": "24103",
|
||||
"city": "Kiel",
|
||||
"phone_mobile": "+4915100000000",
|
||||
"phone_landline": "+4943100000",
|
||||
"birth_date": "1940-05-01",
|
||||
"medical_notes": "Diabetes, Marcumar",
|
||||
}
|
||||
GEO = {"lat": 54.3233, "lon": 10.1394, "accuracy": 12.5, "ts": "2026-06-25T12:00:00Z"}
|
||||
MAPS = "https://www.google.com/maps?q=54.3233,10.1394"
|
||||
|
||||
|
||||
def test_profile_fallback_to_display_name():
|
||||
p = em._profile(FakeUser({}))
|
||||
assert p["name"] == "Anzeigename"
|
||||
assert p["address_short"] == ""
|
||||
|
||||
|
||||
def test_profile_address_compose():
|
||||
p = em._profile(FakeUser(FULL_PREFS))
|
||||
assert p["name"] == "Frieda Schlüter"
|
||||
assert p["address_short"] == "Hauptstraße 12, 24103 Kiel"
|
||||
|
||||
|
||||
def test_sms_text_has_name_address_and_maps_link():
|
||||
p = em._profile(FakeUser(FULL_PREFS))
|
||||
sms = em._emergency_texts(p, GEO)["sms"]
|
||||
assert "Frieda Schlüter" in sms
|
||||
assert "Hauptstraße 12, 24103 Kiel" in sms
|
||||
assert MAPS in sms
|
||||
|
||||
|
||||
def test_call_text_has_no_url_or_coords():
|
||||
p = em._profile(FakeUser(FULL_PREFS))
|
||||
call = em._emergency_texts(p, GEO)["call"]
|
||||
assert "Wohnadresse: Hauptstraße 12, 24103 Kiel" in call
|
||||
assert "http" not in call and "54.3233" not in call
|
||||
assert "SMS und der E-Mail" in call
|
||||
|
||||
|
||||
def test_email_body_full_dossier_with_geo():
|
||||
p = em._profile(FakeUser(FULL_PREFS))
|
||||
body = em._email_body(p, GEO)
|
||||
assert "Medizinische Hinweise: Diabetes, Marcumar" in body
|
||||
assert "Mobil: +4915100000000" in body
|
||||
assert f"Karte: {MAPS}" in body
|
||||
assert "ca. 12 m" in body # round(12.5) -> 12
|
||||
|
||||
|
||||
def test_email_body_geo_unavailable():
|
||||
body = em._email_body(em._profile(FakeUser(FULL_PREFS)), None)
|
||||
assert "nicht verfügbar" in body
|
||||
|
||||
|
||||
def test_geo_fields_handles_dict_and_partial():
|
||||
assert em._geo_fields(None) is None
|
||||
assert em._geo_fields({"lat": 1.0}) is None # lon fehlt -> None
|
||||
g = em._geo_fields({"lat": 1.0, "lon": 2.0})
|
||||
assert g["lat"] == 1.0 and g["accuracy"] is None
|
||||
|
||||
|
||||
def test_webhook_payload_includes_geo_and_messages():
|
||||
user = FakeUser(FULL_PREFS)
|
||||
p = em._profile(user)
|
||||
payload = em._webhook_payload(user, "de", ["+49"], ["sms", "call"], p, GEO)
|
||||
assert payload["geo"]["lat"] == 54.3233
|
||||
assert MAPS in payload["message"]["sms"]
|
||||
assert payload["user"]["name"] == "Frieda Schlüter"
|
||||
|
||||
|
||||
def test_record_manual_emergency_uses_profile(monkeypatch):
|
||||
captured = {}
|
||||
|
||||
def fake_send_email(to, subject, body, cfg):
|
||||
captured.update(to=to, subject=subject, body=body)
|
||||
return True
|
||||
|
||||
monkeypatch.setattr(em, "_send_email_sync", fake_send_email)
|
||||
monkeypatch.setattr(em.settings, "smtp_host", "mail.example.com")
|
||||
monkeypatch.setattr(em.settings, "emergency_contact_email", "kontakt@example.com")
|
||||
monkeypatch.setattr(em.settings, "emergency_webhook_url", "") # nur E-Mail isolieren
|
||||
|
||||
class DummyStore:
|
||||
def log_emergency(self, *a, **k):
|
||||
pass
|
||||
|
||||
res = asyncio.run(
|
||||
em.record_manual_emergency(FakeUser(FULL_PREFS), DummyStore(), language="de", geo=GEO)
|
||||
)
|
||||
assert res["email_sent"] is True and res["webhook_sent"] is False
|
||||
assert "Frieda Schlüter" in captured["subject"]
|
||||
assert "Diabetes" in captured["body"]
|
||||
assert MAPS in captured["body"]
|
||||
Loading…
Add table
Add a link
Reference in a new issue