From a454bd178c354c0714468a9f5ec6b4c3b9eb2cd9 Mon Sep 17 00:00:00 2001 From: DevSoftTech Server Date: Mon, 19 Oct 2026 18:53:58 +0530 Subject: [PATCH] fix(abdm): finalize and sync OPD consult and prescription FHIR bundles only on print --- app/Controllers/AbdmGateway.php | 7 +- app/Controllers/Opd.php | 4 +- app/Controllers/Opd_prescription.php | 76 ++++++++++++++----- .../Abdm/Sync/AbdmTaskBoardSyncService.php | 40 ++++++---- 4 files changed, 92 insertions(+), 35 deletions(-) diff --git a/app/Controllers/AbdmGateway.php b/app/Controllers/AbdmGateway.php index 2f047a1..ea57abb 100644 --- a/app/Controllers/AbdmGateway.php +++ b/app/Controllers/AbdmGateway.php @@ -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([ diff --git a/app/Controllers/Opd.php b/app/Controllers/Opd.php index a5a5fc3..14f0a23 100644 --- a/app/Controllers/Opd.php +++ b/app/Controllers/Opd.php @@ -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()); } } diff --git a/app/Controllers/Opd_prescription.php b/app/Controllers/Opd_prescription.php index 49ba1e1..481c747 100644 --- a/app/Controllers/Opd_prescription.php +++ b/app/Controllers/Opd_prescription.php @@ -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; } } diff --git a/app/Libraries/Abdm/Sync/AbdmTaskBoardSyncService.php b/app/Libraries/Abdm/Sync/AbdmTaskBoardSyncService.php index c71670e..7b534fb 100644 --- a/app/Libraries/Abdm/Sync/AbdmTaskBoardSyncService.php +++ b/app/Libraries/Abdm/Sync/AbdmTaskBoardSyncService.php @@ -55,7 +55,7 @@ class AbdmTaskBoardSyncService * details: array> * } */ - 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; }