Files
PTSRaiAgent/app/db/identity_repo.py
ElebobaandClaude Opus 5.5 121b607414
CI / ruff · pytest (pull_request) Successful in 45s
feat(backend): data model — identity / clinical / audit, repositories, create_all
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>
2026-10-03 14:58:26 +03:00

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