fix(abdm): finalize and sync OPD consult and prescription FHIR bundles only on print

This commit is contained in:
DevSoftTech Server 2026-10-19 18:53:58 +05:30
parent ca7d7be953
commit a454bd178c
4 changed files with 92 additions and 35 deletions

View File

@ -8140,7 +8140,12 @@ class AbdmGateway extends BaseController
$display = $compStr . ' - ' . $dateStr;
}
$isReady = $hasStoredDoc || $hasHr || $isVisitDone || $hasFindings || $hasMeds || $hasVitals;
// For ABDM discovery (readyOnly=true), require finalized FHIR bundle to exist.
// Prevents advertising draft / incomplete consultations before the doctor prints the prescription.
$isReady = ($hasStoredDoc || $hasHr);
if (! $readyOnly) {
$isReady = $isReady || $isVisitDone || $hasFindings || $hasMeds || $hasVitals;
}
// 1. Doctor Visit / OPD Consultation record
$addContext([

View File

@ -2189,9 +2189,9 @@ class Opd extends BaseController
if ($includeContent) {
try {
$rxCtrl = new \App\Controllers\Opd_prescription();
$rxCtrl->storePrescriptionFhirBundle($opdId, $sessionId);
$rxCtrl->finalizeOpdPrescriptionOnPrint($opdId, $sessionId);
} catch (\Throwable $e) {
log_message('warning', '[opd_lettre_pdf] storePrescriptionFhirBundle error: ' . $e->getMessage());
log_message('warning', '[opd_lettre_pdf] finalizeOpdPrescriptionOnPrint error: ' . $e->getMessage());
}
}

View File

@ -707,13 +707,10 @@ class Opd_prescription extends BaseController
}
}
// Do not generate or push FHIR records during intermediate form draft/save.
// FHIR records are only generated and finalized on Print action (opd_prescription_print),
// ensuring all medications, diagnoses, and final prescription PDF are complete.
$fhirStored = false;
try {
$fhirStored = $this->storePrescriptionFhirBundle((int) $opdId, (int) $recordId, $patientRow, (array) $opdRow);
$this->createOpdPrescriptionWorkTask((int) $opdId, (int) $recordId);
} catch (\Throwable $fhirEx) {
log_message('error', 'FHIR bundle store failed during opd save: ' . $fhirEx->getMessage() . ' in ' . $fhirEx->getFile() . ':' . $fhirEx->getLine());
}
$this->syncPatientAddictionFlags((int) ($opdRow->p_id ?? 0), [
'is_smoking' => (int) $this->request->getPost('is_smoking'),
@ -13949,15 +13946,8 @@ OPD SNAPSHOT JSON: " . $payload;
public function opd_prescription_print(int $opdId, int $opdSessionId, int $printType = 0)
{
$this->createOpdPrescriptionWorkTask($opdId, $opdSessionId);
$this->markOpdVisitedOnConsultCompletion((int) $opdId);
// Regenerate complete FHIR records with latest medications and PDF on print action
try {
$this->storePrescriptionFhirBundle($opdId, $opdSessionId);
} catch (\Throwable $e) {
log_message('warning', '[opd_prescription_print] storePrescriptionFhirBundle error: ' . $e->getMessage());
}
// Finalize FHIR records with latest medications and prescription PDF on print action
$this->finalizeOpdPrescriptionOnPrint($opdId, $opdSessionId);
$printConfig = $this->resolvePrescriptionLayoutByDoctorField($opdId, $printType);
$layoutMode = (string) ($printConfig['layout'] ?? 'content_only');
@ -13974,8 +13964,35 @@ OPD SNAPSHOT JSON: " . $payload;
return redirect()->to($url);
}
/**
* Finalize OPD Consultation and Prescription FHIR bundles upon Print.
* Marks visit complete, generates complete FHIR bundles with all medicines & PDF,
* enqueues work task, and immediately pushes/syncs to ABDM Bridge.
*/
public function finalizeOpdPrescriptionOnPrint(int $opdId, int $opdSessionId): void
{
$this->db = $this->db ?? \Config\Database::connect();
$this->markOpdVisitedOnConsultCompletion((int) $opdId);
try {
$this->storePrescriptionFhirBundle($opdId, $opdSessionId);
$this->createOpdPrescriptionWorkTask($opdId, $opdSessionId);
// Immediately push finalized records (OPConsultRecord + PrescriptionRecord) to Bridge
try {
$syncService = new \App\Libraries\Abdm\Sync\AbdmTaskBoardSyncService();
$syncService->syncOpdConsultRecords(1, false, $opdId);
} catch (\Throwable $syncEx) {
log_message('warning', '[finalizeOpdPrescriptionOnPrint] immediate bridge sync error: ' . $syncEx->getMessage());
}
} catch (\Throwable $e) {
log_message('warning', '[finalizeOpdPrescriptionOnPrint] storePrescriptionFhirBundle error: ' . $e->getMessage());
}
}
private function markOpdVisitedOnConsultCompletion(int $opdId): void
{
$this->db = $this->db ?? \Config\Database::connect();
if ($opdId <= 0 || ! $this->db->tableExists('opd_master')) {
return;
}
@ -14049,6 +14066,7 @@ OPD SNAPSHOT JSON: " . $payload;
private function createOpdPrescriptionWorkTask(int $opdId, int $opdSessionId): void
{
$this->db = $this->db ?? \Config\Database::connect();
if ($opdId <= 0 || ! $this->db->tableExists('opd_master') || ! $this->db->tableExists('patient_master')) {
return;
}
@ -14072,9 +14090,24 @@ OPD SNAPSHOT JSON: " . $payload;
->get(1)
->getRowArray();
$abhaId = trim((string) ($patient['abha_id'] ?? $patient['abha_no'] ?? $patient['abha_address'] ?? $patient['abha'] ?? ''));
if (! $this->isValidAbhaNumber($abhaId)) {
return;
$abhaId = '';
foreach (['abha_address', 'abha_id', 'abha_no', 'abha'] as $col) {
$cand = trim((string) ($patient[$col] ?? ''));
if ($cand !== '' && $this->isValidAbhaNumber($cand)) {
$abhaId = $cand;
break;
}
}
if ($abhaId === '') {
$cand = trim((string) ($patient['abha_address'] ?? $patient['abha_id'] ?? $patient['abha_no'] ?? $patient['abha'] ?? ''));
$clean14 = preg_replace('/\D/', '', $cand);
if (strlen($clean14) === 14) {
$abhaId = $clean14;
} elseif ($this->isValidAbhaNumber($cand)) {
$abhaId = $cand;
} else {
return;
}
}
$taskService = new AbdmWorkTaskService();
@ -14097,6 +14130,11 @@ OPD SNAPSHOT JSON: " . $payload;
private function isValidAbhaNumber(string $abhaId): bool
{
return preg_match('/^\d{14}$/', trim($abhaId)) === 1;
$clean = preg_replace('/\D/', '', $abhaId);
if (strlen($clean) === 14) {
return true;
}
return str_contains($abhaId, '@') && preg_match('/^[a-zA-Z0-9.\-_]{3,}@[a-zA-Z]{3,}$/', $abhaId) === 1;
}
}

View File

@ -55,7 +55,7 @@ class AbdmTaskBoardSyncService
* details: array<int, array<string, mixed>>
* }
*/
public function syncOpdConsultRecords(int $limit = 20, bool $dryRun = false): array
public function syncOpdConsultRecords(int $limit = 20, bool $dryRun = false, int $targetOpdId = 0): array
{
$summary = [
'eligible' => 0,
@ -71,19 +71,32 @@ class AbdmTaskBoardSyncService
}
$patientFields = $this->db->getFieldNames('patient_master') ?? [];
$abhaCol = $this->resolveFirstExistingColumn($patientFields, ['abha_id', 'abha_no', 'abha_address', 'abha']);
if ($abhaCol === null) {
$abhaCandidates = array_values(array_filter(['abha_address', 'abha_id', 'abha_no', 'abha'], fn ($c) => in_array($c, $patientFields, true)));
if (empty($abhaCandidates)) {
return $summary;
}
// 1. Fetch done appointments from the last 30 days where patient has ABHA
$rows = $this->db->table('opd_master o')
->select('o.opd_id, o.p_id, o.P_name, o.apointment_date, o.opd_status, o.doc_name, p.' . $abhaCol . ' as abha_id, p.p_fname, p.gender, p.dob', false)
->join('patient_master p', 'p.id = o.p_id', 'left')
->where('o.opd_status', 2)
->where('DATE(o.apointment_date) >=', date('Y-m-d', strtotime('-30 days')), false)
->where('p.' . $abhaCol . ' !=', '')
->orderBy('o.opd_id', 'DESC')
$selectAbha = 'COALESCE(' . implode(', ', array_map(fn ($c) => "NULLIF(p.{$c}, '')", $abhaCandidates)) . ", '') as abha_id";
// 1. Fetch appointments where patient has ABHA (either Done or targetOpdId specified on print)
$query = $this->db->table('opd_master o')
->select('o.opd_id, o.p_id, o.P_name, o.apointment_date, o.opd_status, o.doc_name, ' . $selectAbha . ', p.p_fname, p.gender, p.dob', false)
->join('patient_master p', 'p.id = o.p_id', 'left');
if ($targetOpdId > 0) {
$query->where('o.opd_id', $targetOpdId);
} else {
$query->where('o.opd_status', 2)
->where('DATE(o.apointment_date) >=', date('Y-m-d', strtotime('-30 days')), false);
}
$query->groupStart();
foreach ($abhaCandidates as $c) {
$query->orWhere("p.{$c} !=", '');
}
$query->groupEnd();
$rows = $query->orderBy('o.opd_id', 'DESC')
->limit(max(50, $limit * 3))
->get()
->getResultArray();
@ -200,7 +213,7 @@ class AbdmTaskBoardSyncService
$lastModified = ! empty($allTimestamps) ? max($allTimestamps) : ($row['apointment_date'] ?? date('Y-m-d H:i:s'));
$cooling = self::calculateCooling('opd_prescription_publish', $lastModified);
if ($cooling['is_cooling_active']) {
if ($targetOpdId <= 0 && $cooling['is_cooling_active']) {
$summary['cooling']++;
$summary['details'][] = [
'opd_id' => $opdId,
@ -261,7 +274,8 @@ class AbdmTaskBoardSyncService
$pushStatus = strtolower(trim((string) ($hrToUse['push_status'] ?? '')));
// Already pushed/submitted/linked successfully?
if (in_array($pushStatus, ['queued', 'linked', 'pushed'], true)) {
// When targeting a specific OPD on print, re-push to ensure Bridge receives the finalized bundle
if ($targetOpdId <= 0 && in_array($pushStatus, ['queued', 'linked', 'pushed'], true)) {
$summary['skipped']++;
continue;
}