CI / ruff · pytest (pull_request) Successful in 45s
Two DeclarativeBases on two engines (ADR-003): identity holds direct identifiers, clinical holds token-keyed pseudonymised data plus audit_log. Portable types (JSONB variant, tz-aware UTC timestamps rejecting naive values) so tests run on SQLite. init_db() runs create_all at startup. Guardrail #5 checked by AST and import-graph tests. Refs #6 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
137 lines
4.1 KiB
Python
137 lines
4.1 KiB
Python
"""Identity-DB helpers: registration, consent, connect codes, doctor lookup.
|
|
|
|
Only registration / notification code may use this module. Functions flush but never commit and take time
|
|
as an explicit argument (ADR-005).
|
|
"""
|
|
|
|
from collections.abc import Sequence
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.db.identity import ConnectCode, Doctor, Patient
|
|
|
|
|
|
async def create_patient(
|
|
session: AsyncSession,
|
|
*,
|
|
token: str,
|
|
display_name: str,
|
|
snils: str,
|
|
clinic_id: str,
|
|
doctor_id: str,
|
|
now: datetime,
|
|
phone: str | None = None,
|
|
birth_year: int | None = None,
|
|
sex: str | None = None,
|
|
) -> Patient:
|
|
patient = Patient(
|
|
token=token,
|
|
display_name=display_name,
|
|
snils=snils,
|
|
clinic_id=clinic_id,
|
|
doctor_id=doctor_id,
|
|
created_at=now,
|
|
phone=phone,
|
|
birth_year=birth_year,
|
|
sex=sex,
|
|
)
|
|
session.add(patient)
|
|
await session.flush()
|
|
return patient
|
|
|
|
|
|
async def get_patient(session: AsyncSession, token: str) -> Patient | None:
|
|
return await session.get(Patient, token)
|
|
|
|
|
|
async def get_patient_by_snils(session: AsyncSession, snils: str) -> Patient | None:
|
|
return await session.scalar(select(Patient).where(Patient.snils == snils))
|
|
|
|
|
|
async def get_patient_by_chat(session: AsyncSession, tg_chat_id: int) -> Patient | None:
|
|
return await session.scalar(select(Patient).where(Patient.tg_chat_id == tg_chat_id))
|
|
|
|
|
|
async def bind_chat(session: AsyncSession, *, token: str, tg_chat_id: int) -> Patient | None:
|
|
patient = await session.get(Patient, token)
|
|
if patient is not None:
|
|
patient.tg_chat_id = tg_chat_id
|
|
await session.flush()
|
|
return patient
|
|
|
|
|
|
async def record_consent(session: AsyncSession, *, token: str, version: str, now: datetime) -> Patient | None:
|
|
patient = await session.get(Patient, token)
|
|
if patient is not None:
|
|
patient.consent_at = now
|
|
patient.consent_ver = version
|
|
patient.revoked_at = None
|
|
await session.flush()
|
|
return patient
|
|
|
|
|
|
async def revoke_consent(session: AsyncSession, *, token: str, now: datetime) -> Patient | None:
|
|
patient = await session.get(Patient, token)
|
|
if patient is not None:
|
|
patient.revoked_at = now
|
|
await session.flush()
|
|
return patient
|
|
|
|
|
|
def has_active_consent(patient: Patient) -> bool:
|
|
"""Health data may be processed only with consent given and not revoked (152-FZ art. 10)."""
|
|
return patient.consent_at is not None and patient.revoked_at is None
|
|
|
|
|
|
async def upsert_doctor(
|
|
session: AsyncSession,
|
|
*,
|
|
doctor_id: str,
|
|
full_name: str,
|
|
clinic_id: str,
|
|
notify_chat: int | None = None,
|
|
on_call: bool = False,
|
|
) -> Doctor:
|
|
doctor = await session.get(Doctor, doctor_id)
|
|
if doctor is None:
|
|
doctor = Doctor(doctor_id=doctor_id)
|
|
session.add(doctor)
|
|
doctor.full_name = full_name
|
|
doctor.clinic_id = clinic_id
|
|
doctor.notify_chat = notify_chat
|
|
doctor.on_call = on_call
|
|
await session.flush()
|
|
return doctor
|
|
|
|
|
|
async def get_doctor(session: AsyncSession, doctor_id: str) -> Doctor | None:
|
|
return await session.get(Doctor, doctor_id)
|
|
|
|
|
|
async def doctors_on_call(session: AsyncSession, clinic_id: str | None = None) -> Sequence[Doctor]:
|
|
stmt = select(Doctor).where(Doctor.on_call.is_(True))
|
|
if clinic_id is not None:
|
|
stmt = stmt.where(Doctor.clinic_id == clinic_id)
|
|
return (await session.scalars(stmt.order_by(Doctor.doctor_id))).all()
|
|
|
|
|
|
async def create_connect_code(
|
|
session: AsyncSession, *, code: str, token: str, doctor_id: str, now: datetime
|
|
) -> ConnectCode:
|
|
row = ConnectCode(code=code, token=token, doctor_id=doctor_id, created_at=now)
|
|
session.add(row)
|
|
await session.flush()
|
|
return row
|
|
|
|
|
|
async def redeem_connect_code(session: AsyncSession, *, code: str, now: datetime) -> str | None:
|
|
"""Mark a one-time connect code as used and return the patient token; None if unknown or already used."""
|
|
row = await session.get(ConnectCode, code)
|
|
if row is None or row.used_at is not None:
|
|
return None
|
|
row.used_at = now
|
|
await session.flush()
|
|
return row.token
|