import { Pool } from 'pg'; import { FollowupRepository } from './followup.repository'; import { FollowUp, FollowUpOutcome, FollowUpStatus, TargetOperator } from './followup.types'; import { FieldSealer } from '../../common/crypto/field-sealer'; interface SensitiveFollowUp { targetOperator: TargetOperator; targetValue: number; windowDays: number; evaluatedObservationId: string | null; } interface FollowUpRow { id: string; disposition_id: string; patient_id: string; indicator: string; status: string; outcome: string | null; enc: string; due_at: Date; created_at: Date; evaluated_at: Date | null; } const iso = (v: Date | string): string => (v instanceof Date ? v.toISOString() : String(v)); const isoOrNull = (v: Date | string | null): string | null => (v == null ? null : iso(v)); /** PostgreSQL 跟进项仓储(T-D.2)。目标值/复测观测ID 加密入 enc;indicator/status/due 留列供匹配。 */ export class PostgresFollowupRepository extends FollowupRepository { constructor( private readonly pool: Pool, private readonly sealer: FieldSealer, ) { super(); } async save(f: FollowUp): Promise { const sensitive: SensitiveFollowUp = { targetOperator: f.targetOperator, targetValue: f.targetValue, windowDays: f.windowDays, evaluatedObservationId: f.evaluatedObservationId ?? null, }; await this.pool.query( `INSERT INTO followups (id, disposition_id, patient_id, indicator, status, outcome, enc, due_at, created_at, evaluated_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) ON CONFLICT (id) DO UPDATE SET status = EXCLUDED.status, outcome = EXCLUDED.outcome, enc = EXCLUDED.enc, evaluated_at = EXCLUDED.evaluated_at`, [ f.id, f.dispositionId, f.patientId, f.indicator, f.status, f.outcome ?? null, this.sealer.seal(sensitive), f.dueAt, f.createdAt, f.evaluatedAt ?? null, ], ); return f; } async findById(id: string): Promise { const res = await this.pool.query('SELECT * FROM followups WHERE id = $1', [id]); return res.rows[0] ? this.toDomain(res.rows[0]) : null; } async findByPatient(patientId: string): Promise { const res = await this.pool.query( 'SELECT * FROM followups WHERE patient_id = $1 ORDER BY created_at', [patientId], ); return res.rows.map((r) => this.toDomain(r)); } async findByDisposition(dispositionId: string): Promise { const res = await this.pool.query( 'SELECT * FROM followups WHERE disposition_id = $1 ORDER BY created_at', [dispositionId], ); return res.rows.map((r) => this.toDomain(r)); } async findAll(): Promise { const res = await this.pool.query('SELECT * FROM followups ORDER BY created_at DESC'); return res.rows.map((r) => this.toDomain(r)); } async findPendingByIndicator(patientId: string, indicator: string): Promise { const res = await this.pool.query( `SELECT * FROM followups WHERE patient_id = $1 AND indicator = $2 AND status <> 'evaluated' ORDER BY created_at`, [patientId, indicator], ); return res.rows.map((r) => this.toDomain(r)); } private toDomain(row: FollowUpRow): FollowUp { const s = this.sealer.open(row.enc); return { id: row.id, dispositionId: row.disposition_id, patientId: row.patient_id, indicator: row.indicator, targetOperator: s.targetOperator, targetValue: s.targetValue, windowDays: s.windowDays, dueAt: iso(row.due_at), status: row.status as FollowUpStatus, outcome: (row.outcome as FollowUpOutcome | null) ?? null, evaluatedObservationId: s.evaluatedObservationId, evaluatedAt: isoOrNull(row.evaluated_at), createdAt: iso(row.created_at), }; } }