"""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