From b3e7a2175fc79d583007455ec3bd217058bad25c Mon Sep 17 00:00:00 2001 From: DevSoftTech Server Date: Thu, 8 Oct 2026 12:30:47 +0530 Subject: [PATCH] fix(abdm): make FHIR creation async local-only in web requests, auto-link every minute via cron, disable cooling delay --- app/Commands/AbdmPushSync.php | 153 ++++++++++++++++- app/Config/AbdmConnector.php | 8 +- app/Controllers/AbdmGateway.php | 161 ++++++------------ app/Controllers/Opd.php | 22 --- app/Controllers/Opd_prescription.php | 17 +- .../Abdm/Sync/AbdmTaskBoardSyncService.php | 59 ++++++- 6 files changed, 268 insertions(+), 152 deletions(-) diff --git a/app/Commands/AbdmPushSync.php b/app/Commands/AbdmPushSync.php index 3820419..d68e3d8 100644 --- a/app/Commands/AbdmPushSync.php +++ b/app/Commands/AbdmPushSync.php @@ -2,8 +2,10 @@ namespace App\Commands; +use App\Libraries\Abdm\AbdmConnectorFactory; use App\Libraries\Abdm\Sync\AbdmSyncWorkerService; use App\Libraries\Abdm\Sync\AbdmTaskBoardSyncService; +use App\Libraries\FhirEncryptionService; use CodeIgniter\CLI\BaseCommand; use CodeIgniter\CLI\CLI; @@ -11,11 +13,11 @@ class AbdmPushSync extends BaseCommand { protected $group = 'ABDM'; protected $name = 'abdm:push-sync'; - protected $description = 'Process ABDM M2 sync outbox and ABDM Work Task Board care-context linking.'; + protected $description = 'Process ABDM M2 sync outbox, ABDM Work Task Board, and pending health_records care-context linking.'; protected $usage = 'abdm:push-sync [--limit 20] [--worker worker-name] [--taskboard-only]'; protected $arguments = []; protected $options = [ - '--limit' => 'Maximum outbox and taskboard rows to process in this run.', + '--limit' => 'Maximum outbox, taskboard, and health_records rows to process in this run.', '--worker' => 'Worker identifier for lock tracking.', '--taskboard-only' => 'Skip outbox queue and only sync ABDM Work Task Board records.', ]; @@ -46,7 +48,7 @@ class AbdmPushSync extends BaseCommand CLI::write('Skipped: ' . (int) ($summary['skipped'] ?? 0)); } - // Also sync ABDM Work Task Board records (OPD Consults & Work Queue items) + // 2. Sync ABDM Work Task Board records (OPD Consults & Work Queue items) CLI::newLine(); CLI::write('ABDM Work Task Board Sync', 'cyan'); $tbService = new AbdmTaskBoardSyncService(); @@ -54,5 +56,150 @@ class AbdmPushSync extends BaseCommand CLI::write('OPD Consults -> Eligible: ' . ($tbSummary['opd']['eligible'] ?? 0) . ' | Linked: ' . ($tbSummary['opd']['linked'] ?? 0) . ' | Cooling: ' . ($tbSummary['opd']['cooling'] ?? 0) . ' | Failed: ' . ($tbSummary['opd']['failed'] ?? 0), 'green'); CLI::write('Work Tasks -> Eligible: ' . ($tbSummary['tasks']['eligible'] ?? 0) . ' | Linked: ' . ($tbSummary['tasks']['linked'] ?? 0) . ' | Cooling: ' . ($tbSummary['tasks']['cooling'] ?? 0) . ' | Failed: ' . ($tbSummary['tasks']['failed'] ?? 0), 'green'); + + // 3. Process pending health_records directly (instant background sync for newly saved records) + $this->pushPendingHealthRecords($limit); + } + + private function pushPendingHealthRecords(int $limit): void + { + $db = \Config\Database::connect(); + if (! $db->tableExists('health_records')) { + return; + } + + $rows = $db->table('health_records') + ->whereNotIn('push_status', ['pushed', 'linked', 'local_only']) + ->where('care_context_reference !=', '') + ->where('abha_id !=', '') + ->orderBy('id', 'ASC') + ->get($limit) + ->getResultArray(); + + if (empty($rows)) { + return; + } + + CLI::newLine(); + CLI::write('--- Pending Health Records Sync (Immediate Cron Link) ---', 'cyan'); + CLI::write('Found ' . count($rows) . ' pending record(s) to push...', 'yellow'); + + $connector = AbdmConnectorFactory::make(); + $pushed = 0; + $failed = 0; + + foreach ($rows as $row) { + $bundle = $this->resolveFhirBundle($row); + if ($bundle === null) { + CLI::write(' Record #' . $row['id'] . ' (' . ($row['hi_type'] ?? '') . ') - no valid FHIR bundle', 'red'); + $failed++; + continue; + } + + $patientId = (int) ($row['patient_id'] ?? 0); + $patient = $this->loadPatientDemographics($db, $patientId); + $abhaId = trim((string) ($row['abha_id'] ?? '')); + + try { + $result = $connector->pushRecord([ + 'patient_id' => (string) $patientId, + 'patient_name' => $patient['name'] !== '' ? $patient['name'] : ('PATIENT-' . $patientId), + 'abha_id' => str_contains($abhaId, '@') ? '' : $abhaId, + 'abha_address' => str_contains($abhaId, '@') ? $abhaId : $patient['abha_address'], + 'gender' => $patient['gender'], + 'year_of_birth' => $patient['year_of_birth'], + 'hi_type' => (string) ($row['hi_type'] ?? ''), + 'record_type' => (string) ($row['hi_type'] ?? ''), + 'visit_date' => substr((string) ($row['created_at'] ?? date('Y-m-d')), 0, 10), + 'care_context_reference' => (string) ($row['care_context_reference'] ?? ''), + 'care_context_display' => (string) ($row['care_context_display'] ?? (($row['hi_type'] ?? '') . ' ' . substr((string) ($row['created_at'] ?? ''), 0, 10))), + 'record_data' => $bundle, + ]); + + $ok = ! empty($result['ok']) && (int) $result['ok'] === 1; + $httpCode = (int) ($result['http_code'] ?? 0); + $statusVal = strtolower((string) ($result['status'] ?? '')); + + if ($ok || in_array($httpCode, [200, 201, 202, 409], true) || in_array($statusVal, ['queued', 'pushed', 'linked', 'duplicate'], true)) { + $db->table('health_records')->where('id', $row['id'])->update([ + 'push_status' => 'pushed', + 'push_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), + ]); + $pushed++; + CLI::write(' Record #' . $row['id'] . ' (' . ($row['hi_type'] ?? '') . ' ' . ($row['care_context_reference'] ?? '') . ') -> pushed', 'green'); + } else { + $failed++; + $errMsg = (string) ($result['message'] ?? $result['error'] ?? 'Push failed'); + $db->table('health_records')->where('id', $row['id'])->update([ + 'push_status' => 'failed', + 'updated_at' => date('Y-m-d H:i:s'), + ]); + CLI::write(' Record #' . $row['id'] . ' (' . ($row['hi_type'] ?? '') . ') -> failed: ' . $errMsg, 'red'); + } + } catch (\Throwable $e) { + $failed++; + CLI::write(' Record #' . $row['id'] . ' (' . ($row['hi_type'] ?? '') . ') -> error: ' . $e->getMessage(), 'red'); + } + } + + CLI::write('Pending Records: ' . $pushed . ' pushed, ' . $failed . ' failed.', $pushed > 0 ? 'green' : 'yellow'); + } + + private function resolveFhirBundle(array $row): ?array + { + $plain = trim((string) ($row['fhir_bundle'] ?? $row['record_data'] ?? '')); + if ($plain === '') { + $encrypted = trim((string) ($row['fhir_bundle_enc'] ?? '')); + if ($encrypted !== '') { + try { + $plain = (new FhirEncryptionService())->decrypt($encrypted); + } catch (\Throwable $e) { + $plain = ''; + } + } + } + + if ($plain === '') { + return null; + } + + $decoded = json_decode($plain, true); + if (! is_array($decoded)) { + return null; + } + + $bundle = $decoded['bundle'] ?? $decoded['fhir_bundle'] ?? $decoded; + + return is_array($bundle) && ($bundle['resourceType'] ?? '') === 'Bundle' ? $bundle : null; + } + + private function loadPatientDemographics($db, int $patientId): array + { + $out = ['name' => '', 'gender' => '', 'year_of_birth' => '', 'abha_address' => '']; + if ($patientId <= 0 || ! $db->tableExists('patient_master')) { + return $out; + } + + $fields = $db->getFieldNames('patient_master') ?? []; + $row = $db->table('patient_master')->where('id', $patientId)->get(1)->getRowArray(); + if (! $row) { + return $out; + } + + $out['name'] = trim((string) ($row['p_fname'] ?? '')); + $gender = (int) ($row['gender'] ?? 0); + $out['gender'] = $gender === 1 ? 'M' : ($gender === 2 ? 'F' : ''); + + $dob = trim((string) ($row['dob'] ?? '')); + if ($dob !== '' && $dob !== '0000-00-00') { + $out['year_of_birth'] = substr($dob, 0, 4); + } + + if (in_array('abha_address', $fields, true)) { + $out['abha_address'] = trim((string) ($row['abha_address'] ?? '')); + } + + return $out; } } diff --git a/app/Config/AbdmConnector.php b/app/Config/AbdmConnector.php index dd8feb4..bed94cc 100644 --- a/app/Config/AbdmConnector.php +++ b/app/Config/AbdmConnector.php @@ -153,10 +153,10 @@ class AbdmConnector extends BaseConfig // ------------------------------------------------------------------ public bool $autoLinkEnabled = true; - /** Default cooling period for OPD, Lab, Radiology, and all other HI types (in minutes). */ - public int $autoLinkDelayMinutes = 60; + /** Default cooling period for OPD, Lab, Radiology, and all other HI types (in minutes). 0 = immediate 1-minute cron linking. */ + public int $autoLinkDelayMinutes = 0; - /** Cooling period for IPD Discharge Summary (in hours). */ - public int $autoLinkDelayDischargeHours = 24; + /** Cooling period for IPD Discharge Summary (in hours). 0 = immediate 1-minute cron linking. */ + public int $autoLinkDelayDischargeHours = 0; } diff --git a/app/Controllers/AbdmGateway.php b/app/Controllers/AbdmGateway.php index 9b4a8e8..2be0f4c 100644 --- a/app/Controllers/AbdmGateway.php +++ b/app/Controllers/AbdmGateway.php @@ -2127,7 +2127,8 @@ class AbdmGateway extends BaseController string $abhaId = '', string $abhaAddressPost = '', string $consentHandle = '', - string $careContextId = '' + string $careContextId = '', + bool $skipRemotePush = false ): array { if ($labReqId <= 0) { return ['ok' => 0, 'error' => 'lab_req_id is required']; @@ -2330,6 +2331,24 @@ class AbdmGateway extends BaseController 'consent_handle' => $effectiveConsent, ]); + if ($skipRemotePush) { + if ($healthRecordId > 0 && $this->db->tableExists('health_records')) { + $this->db->table('health_records') + ->where('id', $healthRecordId) + ->update([ + 'push_status' => 'pending', + 'updated_at' => Time::now('Asia/Kolkata')->toDateTimeString(), + ]); + } + return [ + 'ok' => 1, + 'status' => 'queued_for_cron', + 'care_context_reference' => $ccRef, + 'health_record_id' => $healthRecordId, + 'message' => 'Diagnostic report stored locally and queued for 1-minute cron job link.', + ]; + } + if (! $hasAbha) { if ($healthRecordId > 0 && $this->db->tableExists('health_records')) { $this->db->table('health_records') @@ -10121,7 +10140,7 @@ class AbdmGateway extends BaseController }; } - public function pushAdditionalHiRecord(array $payload, int $patientId, string $abhaId, string $consentHandle = '') + public function pushAdditionalHiRecord(array $payload, int $patientId, string $abhaId, string $consentHandle = '', bool $skipRemotePush = false) { $abhaIdentity = $this->resolvePatientAbhaIdentity($patientId, $abhaId); $abhaNumber = $abhaIdentity['abha_id']; @@ -10157,6 +10176,24 @@ class AbdmGateway extends BaseController 'consent_handle' => $effectiveConsent, ]); + if ($skipRemotePush) { + if ($healthRecordId > 0 && $this->db->tableExists('health_records')) { + $this->db->table('health_records')->where('id', $healthRecordId)->update([ + 'push_status' => 'pending', + 'updated_at' => Time::now('Asia/Kolkata')->toDateTimeString(), + ]); + } + + $resp = [ + 'ok' => 1, + 'status' => 'queued_for_cron', + 'health_record_id' => $healthRecordId, + 'care_context_reference' => $ccRef, + 'message' => 'Record saved locally and queued for 1-minute cron job link.', + ]; + return $this->response !== null ? $this->response->setJSON($resp) : $resp; + } + if ($abhaId === '') { if ($healthRecordId > 0 && $this->db->tableExists('health_records')) { $this->db->table('health_records')->where('id', $healthRecordId)->update([ @@ -10499,7 +10536,7 @@ class AbdmGateway extends BaseController case 'diagnostic_report': case 'lab': case 'radiology': - return $this->pushDiagnosisReportBundleInternal($entityId, $patientId); + return $this->pushDiagnosisReportBundleInternal($entityId, $patientId, '', '', '', '', true); case 'discharge': case 'discharge_summary': @@ -10516,8 +10553,8 @@ class AbdmGateway extends BaseController if ($payload === null) { return ['ok' => 0, 'status' => 'payload_null']; } - $res = $this->pushAdditionalHiRecord($payload, (int) ($payload['patient_id'] ?? $patientId), $abhaId); - return ['ok' => 1, 'status' => 'pushed']; + $res = $this->pushAdditionalHiRecord($payload, (int) ($payload['patient_id'] ?? $patientId), $abhaId, '', true); + return ['ok' => 1, 'status' => 'queued_for_cron']; case 'invoice': $abhaId = $patientId > 0 ? $this->resolvePatientAbhaIdentifier($patientId) : ''; @@ -10525,8 +10562,8 @@ class AbdmGateway extends BaseController if ($payload === null) { return ['ok' => 0, 'status' => 'payload_null']; } - $res = $this->pushAdditionalHiRecord($payload, (int) ($payload['patient_id'] ?? $patientId), $abhaId); - return ['ok' => 1, 'status' => 'pushed']; + $res = $this->pushAdditionalHiRecord($payload, (int) ($payload['patient_id'] ?? $patientId), $abhaId, '', true); + return ['ok' => 1, 'status' => 'queued_for_cron']; case 'wellness': $abhaId = $patientId > 0 ? $this->resolvePatientAbhaIdentifier($patientId) : ''; @@ -10534,8 +10571,8 @@ class AbdmGateway extends BaseController if ($payload === null) { return ['ok' => 0, 'status' => 'payload_null']; } - $res = $this->pushAdditionalHiRecord($payload, (int) ($payload['patient_id'] ?? $patientId), $abhaId); - return ['ok' => 1, 'status' => 'pushed']; + $res = $this->pushAdditionalHiRecord($payload, (int) ($payload['patient_id'] ?? $patientId), $abhaId, '', true); + return ['ok' => 1, 'status' => 'queued_for_cron']; case 'immunization': $abhaId = $patientId > 0 ? $this->resolvePatientAbhaIdentifier($patientId) : ''; @@ -10543,16 +10580,13 @@ class AbdmGateway extends BaseController if ($payload === null) { return ['ok' => 0, 'status' => 'payload_null']; } - $res = $this->pushAdditionalHiRecord($payload, (int) ($payload['patient_id'] ?? $patientId), $abhaId); + $res = $this->pushAdditionalHiRecord($payload, (int) ($payload['patient_id'] ?? $patientId), $abhaId, '', true); $hrId = 0; - $isOk = false; if ($res instanceof \CodeIgniter\HTTP\ResponseInterface) { $resData = json_decode($res->getBody(), true); $hrId = (int) ($resData['health_record_id'] ?? 0); - $isOk = ! empty($resData['ok']); } elseif (is_array($res)) { $hrId = (int) ($res['health_record_id'] ?? 0); - $isOk = ! empty($res['ok']); } if ($entityId > 0 && $this->db->tableExists('immunization_records')) { $this->db->table('immunization_records') @@ -10560,10 +10594,10 @@ class AbdmGateway extends BaseController ->update([ 'abdm_care_context_reference' => (string) ($payload['care_context_reference'] ?? ('IMM-' . $entityId)), 'abdm_health_record_id' => $hrId > 0 ? $hrId : null, - 'abdm_push_status' => $isOk ? 'linked' : 'queued', + 'abdm_push_status' => 'queued', ]); } - return ['ok' => 1, 'status' => 'pushed', 'health_record_id' => $hrId, 'care_context_reference' => $payload['care_context_reference']]; + return ['ok' => 1, 'status' => 'queued_for_cron', 'health_record_id' => $hrId, 'care_context_reference' => $payload['care_context_reference']]; default: return ['ok' => 0, 'status' => 'unknown_doc_type', 'message' => 'Unknown document type: ' . $docType]; @@ -10596,105 +10630,22 @@ class AbdmGateway extends BaseController return ['ok' => 0, 'status' => 'no_abha', 'message' => 'Patient has no ABHA']; } - // Ensure FHIR bundle is built and stored + // Ensure FHIR bundle is built and stored locally if (class_exists('\App\Controllers\Opd_prescription')) { try { $rxCtrl = new \App\Controllers\Opd_prescription(); $rxCtrl->storePrescriptionFhirBundle($opdId, $sessionId, $patientRow, $opdRow); + $rxCtrl->createOpdPrescriptionWorkTask($opdId, $sessionId); } catch (\Throwable $e) { log_message('warning', '[autoPushPrescriptionRecord] storePrescriptionFhirBundle error: ' . $e->getMessage()); } } - if ($sessionId <= 0 && $this->db->tableExists('opd_prescription')) { - $presRow = $this->db->table('opd_prescription')->select('id')->where('opd_id', $opdId)->orderBy('id', 'DESC')->get(1)->getRowArray(); - if (! empty($presRow['id'])) { - $sessionId = (int) $presRow['id']; - } - } - - $abhaIdentity = $this->resolvePatientAbhaIdentity($patientId, $abhaId); - $abhaNumber = $abhaIdentity['abha_id']; - $abhaAddress = $abhaIdentity['abha_address']; - $patientBirthYear = $this->resolvePatientBirthYear($patientRow, $abhaAddress, $abhaNumber); - $patientName = $this->patientDisplayName($patientRow); - $visitDateRaw = trim((string) ($opdRow['date_opd_visit'] ?? $opdRow['apointment_date'] ?? '')); - $visitDate = $visitDateRaw !== '' ? date('Y-m-d', strtotime($visitDateRaw)) : date('Y-m-d'); - $cleanDate = str_replace('-', '', $visitDate); - - $results = []; - - // 1. Push OPConsultRecord - $opcDoc = $this->db->table('opd_fhir_documents') - ->where('opd_id', $opdId) - ->where('bundle_type', 'OPConsultRecord') - ->orderBy('id', 'DESC') - ->get(1) - ->getRowArray(); - if (! empty($opcDoc['bundle_json'])) { - $opcBundle = json_decode((string) $opcDoc['bundle_json'], true); - if (is_array($opcBundle)) { - $ccRef = 'OPD-' . $patientId . '-S' . $sessionId . '-' . $cleanDate; - $ccDisplay = 'OPD Consultation - ' . date('d M Y', strtotime($visitDate)); - try { - $res = $this->connector->pushRecord([ - 'patient_id' => (string) $patientId, - 'patient_name' => $patientName, - 'abha_id' => $abhaNumber, - 'abha_address' => $abhaAddress, - 'year_of_birth' => $patientBirthYear, - 'hi_type' => 'OPConsultRecord', - 'record_type' => 'OPConsultRecord', - 'visit_date' => $visitDate, - 'care_context_reference' => $ccRef, - 'care_context_display' => $ccDisplay, - 'notes' => $ccDisplay, - 'queue_id' => $ccRef, - 'record_data' => $opcBundle, - ]); - $results['opd'] = $res; - } catch (\Throwable $e) { - $results['opd_error'] = $e->getMessage(); - } - } - } - - // 2. Push PrescriptionRecord - $prescDoc = $this->db->table('opd_fhir_documents') - ->where('opd_id', $opdId) - ->where('bundle_type', 'PrescriptionRecord') - ->orderBy('id', 'DESC') - ->get(1) - ->getRowArray(); - if (! empty($prescDoc['bundle_json'])) { - $prescBundle = json_decode((string) $prescDoc['bundle_json'], true); - if (is_array($prescBundle)) { - $prescCcRef = 'PRESC-' . $patientId . '-S' . $sessionId . '-' . $cleanDate; - $prescCcDisplay = 'Prescription - ' . date('d M Y', strtotime($visitDate)); - try { - $res = $this->connector->pushRecord([ - 'patient_id' => (string) $patientId, - 'patient_name' => $patientName, - 'abha_id' => $abhaNumber, - 'abha_address' => $abhaAddress, - 'year_of_birth' => $patientBirthYear, - 'hi_type' => 'PrescriptionRecord', - 'record_type' => 'PrescriptionRecord', - 'visit_date' => $visitDate, - 'care_context_reference' => $prescCcRef, - 'care_context_display' => $prescCcDisplay, - 'notes' => $prescCcDisplay, - 'queue_id' => $prescCcRef, - 'record_data' => $prescBundle, - ]); - $results['prescription'] = $res; - } catch (\Throwable $e) { - $results['prescription_error'] = $e->getMessage(); - } - } - } - - return ['ok' => 1, 'status' => 'processed', 'results' => $results]; + return [ + 'ok' => 1, + 'status' => 'queued_for_cron', + 'message' => 'Prescription FHIR bundle saved and queued for 1-minute cron linking', + ]; } private function buildHealthDocumentRecordPayload(int $patientId, string $abhaId = '', int $recordId = 0, array $patientRow = []): ?array diff --git a/app/Controllers/Opd.php b/app/Controllers/Opd.php index db34ec3..a50caeb 100644 --- a/app/Controllers/Opd.php +++ b/app/Controllers/Opd.php @@ -2130,17 +2130,6 @@ class Opd extends BaseController return $this->response->setStatusCode(404)->setBody('OPD not found'); } - // Auto create/update FHIR bundle when printing consult form - try { - $resolvedSessionId = $sessionId > 0 ? $sessionId : (int) ($data['opd_prescription']->id ?? 0); - $prescCtrl = new \App\Controllers\Opd_prescription(); - $patientRowArr = ! empty($data['patient_master'][0]) ? (array) $data['patient_master'][0] : null; - $opdRowArr = ! empty($data['opd_master'][0]) ? (array) $data['opd_master'][0] : null; - $prescCtrl->storePrescriptionFhirBundle($opdId, $resolvedSessionId, $patientRowArr, $opdRowArr); - } catch (\Throwable $fhirEx) { - log_message('warning', '[opd_lettre_print] FHIR bundle sync on print warning: ' . $fhirEx->getMessage()); - } - $data['print_layout_mode'] = $layoutMode; return view('billing/opd_letter_head', $data); @@ -2176,17 +2165,6 @@ class Opd extends BaseController return $this->response->setStatusCode(404)->setBody('OPD not found'); } - // Auto create/update FHIR bundle when generating consult form PDF - try { - $resolvedSessionId = $sessionId > 0 ? $sessionId : (int) ($data['opd_prescription']->id ?? 0); - $prescCtrl = new \App\Controllers\Opd_prescription(); - $patientRowArr = ! empty($data['patient_master'][0]) ? (array) $data['patient_master'][0] : null; - $opdRowArr = ! empty($data['opd_master'][0]) ? (array) $data['opd_master'][0] : null; - $prescCtrl->storePrescriptionFhirBundle($opdId, $resolvedSessionId, $patientRowArr, $opdRowArr); - } catch (\Throwable $fhirEx) { - log_message('warning', '[opd_lettre_pdf] FHIR bundle sync on PDF print warning: ' . $fhirEx->getMessage()); - } - $data['print_layout_mode'] = $layoutMode; $isComposedTemplate = $templateKey !== '' && str_starts_with($templateKey, 'compose_'); diff --git a/app/Controllers/Opd_prescription.php b/app/Controllers/Opd_prescription.php index 0710a2e..7b54f18 100644 --- a/app/Controllers/Opd_prescription.php +++ b/app/Controllers/Opd_prescription.php @@ -12057,16 +12057,7 @@ class Opd_prescription extends BaseController $this->db->table('health_records') ->whereIn('care_context_reference', $redundantRefs) ->delete(); - - // If patient has ABHA, auto-push to Bridge Gateway so care context is linked immediately - try { - $hasValidAbha = (str_contains($abhaAddress, '@') || (strlen(preg_replace('/\D/', '', $abhaAddress)) === 14)); - if ($hasValidAbha) { - \App\Controllers\AbdmGateway::autoPushRecord('prescription', $opdId, $patientId, ['session_id' => $sessionId]); - } - } catch (\Throwable $e) { - // Fail-safe - } + // Record is stored with push_status = pending and queued for the 1-minute cron job } catch (\Throwable $e) { log_message('warning', '[storePrescriptionFhirBundle] auto health_records insert error: ' . $e->getMessage()); } @@ -13961,12 +13952,6 @@ OPD SNAPSHOT JSON: " . $payload; $this->createOpdPrescriptionWorkTask($opdId, $opdSessionId); $this->markOpdVisitedOnConsultCompletion((int) $opdId); - // Auto-link/push prescription to ABDM if patient has ABHA (fail-safe) - try { - \App\Controllers\AbdmGateway::autoPushRecord('prescription', $opdId, 0, ['session_id' => $opdSessionId]); - } catch (\Throwable $e) { - log_message('warning', 'ABDM Prescription print auto-link failed: ' . $e->getMessage()); - } $printConfig = $this->resolvePrescriptionLayoutByDoctorField($opdId, $printType); $layoutMode = (string) ($printConfig['layout'] ?? 'content_only'); diff --git a/app/Libraries/Abdm/Sync/AbdmTaskBoardSyncService.php b/app/Libraries/Abdm/Sync/AbdmTaskBoardSyncService.php index b062dfc..695770f 100644 --- a/app/Libraries/Abdm/Sync/AbdmTaskBoardSyncService.php +++ b/app/Libraries/Abdm/Sync/AbdmTaskBoardSyncService.php @@ -635,6 +635,8 @@ class AbdmTaskBoardSyncService 'immunization_record_publish', 'wellness_record_publish', 'health_document_publish', + 'invoice_record_publish', + 'invoice_publish', 'ipd_discharge_publish', ]; @@ -806,6 +808,7 @@ class AbdmTaskBoardSyncService 'immunization_record_publish' => 'ImmunizationRecord', 'wellness_record_publish' => 'WellnessRecord', 'health_document_publish' => 'HealthDocumentRecord', + 'invoice_record_publish', 'invoice_publish' => 'InvoiceRecord', 'ipd_discharge_publish' => 'DischargeSummaryRecord', default => 'HealthDocumentRecord', }; @@ -825,6 +828,7 @@ class AbdmTaskBoardSyncService 'immunization_record_publish' => 'IMM-', 'wellness_record_publish' => 'WELLNESS-', 'health_document_publish' => 'DOC-', + 'invoice_record_publish', 'invoice_publish' => 'INV-', 'ipd_discharge_publish' => 'DISCHARGE-', default => 'REC-', }; @@ -935,6 +939,45 @@ class AbdmTaskBoardSyncService log_message('warning', '[processIndividualWorkTask] Immunization bundle generation error: ' . $ie->getMessage()); } } + } elseif (in_array($taskType, ['invoice_record_publish', 'invoice_publish'], true)) { + if (empty($pushData['bundle']) && class_exists('\App\Controllers\AbdmGateway')) { + try { + $gw = new \App\Controllers\AbdmGateway(); + $invPayload = $gw->buildInvoiceRecordPayload((int) $entityId, $patientId, $abhaAddress !== '' ? $abhaAddress : $abhaNumber); + if (! empty($invPayload['bundle'])) { + $pushData['bundle'] = $invPayload['bundle']; + $pushData['fhir_bundle'] = $invPayload['bundle']; + $pushData['record_data'] = $invPayload['bundle']; + $bundleJson = (string) json_encode($invPayload['bundle'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } + } catch (\Throwable $inve) { + log_message('warning', '[processIndividualWorkTask] Invoice bundle generation error: ' . $inve->getMessage()); + } + } + } + + // Generic fallback: check if bundle already exists in health_records + if (empty($pushData['bundle']) && $this->db->tableExists('health_records')) { + $hr = $this->db->table('health_records') + ->where('entity_id', (string) $entityId) + ->where('patient_id', $patientId) + ->orderBy('id', 'DESC') + ->get(1) + ->getRowArray(); + if (! empty($hr['fhir_bundle'])) { + $decoded = json_decode((string) $hr['fhir_bundle'], true); + if (is_array($decoded) && ! empty($decoded)) { + $pushData['bundle'] = $decoded; + $pushData['fhir_bundle'] = $decoded; + $pushData['record_data'] = $decoded; + $bundleJson = (string) $hr['fhir_bundle']; + if (! empty($hr['care_context_reference'])) { + $careContextRef = (string) $hr['care_context_reference']; + $pushData['care_context_reference'] = $careContextRef; + $pushData['queue_id'] = $careContextRef; + } + } + } } try { @@ -1093,13 +1136,25 @@ class AbdmTaskBoardSyncService $typeLower = strtolower(trim($taskType)); if (in_array($typeLower, ['ipd_discharge_publish', 'ipd_discharge', 'dischargesummaryrecord', 'ipd'], true)) { - $hours = max(1, (int) ($cfg->autoLinkDelayDischargeHours ?? 24)); + $hours = max(0, (int) ($cfg->autoLinkDelayDischargeHours ?? 0)); $requiredSeconds = $hours * 3600; } else { - $minutes = max(1, (int) ($cfg->autoLinkDelayMinutes ?? 60)); + $minutes = max(0, (int) ($cfg->autoLinkDelayMinutes ?? 0)); $requiredSeconds = $minutes * 60; } + if ($requiredSeconds <= 0) { + return [ + 'is_cooling_active' => false, + 'cooling_active' => false, + 'remaining_seconds' => 0, + 'remaining_minutes' => 0, + 'required_seconds' => 0, + 'last_modified' => (string) $lastModified, + 'auto_link_at' => date('Y-m-d H:i:s'), + ]; + } + $lastTimestamp = ! empty($lastModified) ? strtotime($lastModified) : 0; if ($lastTimestamp <= 0) { $lastTimestamp = time();