fix(abdm): unify consult visit into single OPConsultRecord bundle and auto-sync on print

This commit is contained in:
DevSoftTech Server 2026-10-06 16:40:30 +05:30
parent d46f222fa3
commit f367603962
4 changed files with 108 additions and 292 deletions

View File

@ -6459,7 +6459,16 @@ class AbdmGateway extends BaseController
if ($ccRef === '') {
$ccRef = 'HR-' . (int) ($row['id'] ?? 0);
}
// Skip redundant/legacy separate PRESC- and WELLNESS- contexts (now unified under OPD-)
if (str_starts_with($ccRef, 'PRESC-') || str_starts_with($ccRef, 'WELLNESS-')) {
continue;
}
$hiType = trim((string) ($row['hi_type'] ?? 'HealthDocumentRecord'));
if (str_starts_with($ccRef, 'INVOICE-') || stripos($hiType, 'invoice') !== false) {
$hiType = 'InvoiceRecord';
} elseif (str_starts_with($ccRef, 'OPD-')) {
$hiType = 'OPConsultRecord';
}
$dateStr = date('d M Y', strtotime((string) ($row['created_at'] ?? $row['updated_at'] ?? 'now')));
$display = $hiType . ' - ' . $dateStr;
@ -6570,7 +6579,6 @@ class AbdmGateway extends BaseController
$aliasRef = 'OPD-' . $patientId . '-S' . $opdId . '-' . $cleanDate;
$docName = trim((string) ($om['doc_name'] ?? ''));
$display = 'OPConsultRecord - ' . ($docName !== '' ? 'Dr. ' . $docName . ' - ' : '') . $dateStr;
// Determine FHIR readiness: check stored doc, health_records, status, or clinical findings
$hasStoredDoc = false;
@ -6622,8 +6630,35 @@ class AbdmGateway extends BaseController
}
}
$isReady = $hasStoredDoc || $hasHr || $isVisitDone || $hasFindings || $hasMeds;
// Check vitals / wellness
$hasVitals = ! empty($pr) && (! empty($pr['bp']) || ! empty($pr['pulse']) || ! empty($pr['temp']) || ! empty($pr['spo2']) || ! empty($pr['weight']) || ! empty($pr['height']));
// Check scanned or uploaded documents
$hasDocs = false;
if ($this->db->tableExists('file_upload_data')) {
$hasDocs = ($this->db->table('file_upload_data')->where('opd_id', $opdId)->countAllResults() > 0);
}
// Build unified clinical components label
$components = ['OPD Consult'];
if ($hasVitals) {
$components[] = 'Wellness';
}
if ($hasMeds) {
$components[] = 'Prescription';
}
if ($hasDocs) {
$components[] = 'HealthDocument';
}
$compStr = implode(', ', $components);
$display = $compStr . ' - ' . ($docName !== '' ? 'Dr. ' . $docName . ' - ' : '') . $dateStr;
if (strlen($display) > 85) {
$display = $compStr . ' - ' . $dateStr;
}
$isReady = $hasStoredDoc || $hasHr || $isVisitDone || $hasFindings || $hasMeds || $hasVitals;
// Single unified care context covering consult note, wellness, prescription, and documents
$addContext([
'careContextId' => $ccRef,
'referenceNumber' => $ccRef,
@ -6635,37 +6670,6 @@ class AbdmGateway extends BaseController
'is_primary' => false,
]);
$seenRefs[$aliasRef] = true;
// 4b. Prescription Record (if prescribed medicines exist)
if ($hasMeds) {
$prescCcRef = 'PRESC-' . $patientId . '-S' . $sessionId . '-' . $cleanDate;
$prescDisplay = 'Prescription - ' . ($docName !== '' ? 'Dr. ' . $docName . ' - ' : '') . $dateStr;
$addContext([
'careContextId' => $prescCcRef,
'referenceNumber' => $prescCcRef,
'display' => $prescDisplay,
'record_type' => 'PrescriptionRecord',
'patient_id' => $patientId,
'is_fhir_ready' => true,
'is_primary' => false,
]);
}
// 4c. Wellness Record (if vitals exist)
$hasVitals = ! empty($pr) && (! empty($pr['bp']) || ! empty($pr['pulse']) || ! empty($pr['temp']) || ! empty($pr['spo2']) || ! empty($pr['weight']) || ! empty($pr['height']));
if ($hasVitals) {
$wellCcRef = 'WELLNESS-' . $sessionId . '-' . $cleanDate;
$wellDisplay = 'Wellness Record - ' . $dateStr;
$addContext([
'careContextId' => $wellCcRef,
'referenceNumber' => $wellCcRef,
'display' => $wellDisplay,
'record_type' => 'WellnessRecord',
'patient_id' => $patientId,
'is_fhir_ready' => true,
'is_primary' => false,
]);
}
}
}
@ -6688,16 +6692,6 @@ class AbdmGateway extends BaseController
|| trim((string) ($pr['diagnosis'] ?? '')) !== ''
|| trim((string) ($pr['Provisional_diagnosis'] ?? '')) !== '';
$ccRef = 'OPD-' . $patientId . '-S' . $sessionId . '-' . $cleanDate;
$display = 'OPConsultRecord - ' . $dateStr;
$addContext([
'careContextId' => $ccRef,
'referenceNumber' => $ccRef,
'display' => $display,
'record_type' => 'OPConsultRecord',
'patient_id' => $patientId,
'is_fhir_ready' => $hasFindings,
'is_primary' => false,
]);
// Also check if prescribed medicines exist for orphan
$hasMeds = false;
@ -6710,35 +6704,29 @@ class AbdmGateway extends BaseController
$hasMeds = ($this->db->table($tblPresc)->where($col, (int) $pr['id'])->countAllResults() > 0);
}
}
if ($hasMeds) {
$prescCcRef = 'PRESC-' . $patientId . '-S' . $sessionId . '-' . $cleanDate;
$prescDisplay = 'Prescription - ' . $dateStr;
$addContext([
'careContextId' => $prescCcRef,
'referenceNumber' => $prescCcRef,
'display' => $prescDisplay,
'record_type' => 'PrescriptionRecord',
'patient_id' => $patientId,
'is_fhir_ready' => true,
'is_primary' => false,
]);
}
// Also check if vitals exist for orphan
$hasVitals = ! empty($pr['bp']) || ! empty($pr['pulse']) || ! empty($pr['temp']) || ! empty($pr['spo2']) || ! empty($pr['weight']) || ! empty($pr['height']);
$components = ['OPD Consult'];
if ($hasVitals) {
$wellCcRef = 'WELLNESS-' . $sessionId . '-' . $cleanDate;
$wellDisplay = 'Wellness Record - ' . $dateStr;
$addContext([
'careContextId' => $wellCcRef,
'referenceNumber' => $wellCcRef,
'display' => $wellDisplay,
'record_type' => 'WellnessRecord',
'patient_id' => $patientId,
'is_fhir_ready' => true,
'is_primary' => false,
]);
$components[] = 'Wellness';
}
if ($hasMeds) {
$components[] = 'Prescription';
}
$compStr = implode(', ', $components);
$display = $compStr . ' - ' . $dateStr;
$addContext([
'careContextId' => $ccRef,
'referenceNumber' => $ccRef,
'display' => $display,
'record_type' => 'OPConsultRecord',
'patient_id' => $patientId,
'is_fhir_ready' => $hasFindings || $hasMeds || $hasVitals,
'is_primary' => false,
]);
}
}

View File

@ -2130,6 +2130,17 @@ 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);
@ -2165,6 +2176,17 @@ 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_');

View File

@ -11799,12 +11799,27 @@ class Opd_prescription extends BaseController
* @param array<string, mixed> $opdRow
* @param int $sessionId
*/
private function storePrescriptionFhirBundle(int $opdId, int $sessionId, array $patientRow, array $opdRow): bool
public function storePrescriptionFhirBundle(int $opdId, int $sessionId = 0, ?array $patientRow = null, ?array $opdRow = null): bool
{
if ($this->db === null) {
$this->db = \Config\Database::connect();
}
if (! $this->db->tableExists('opd_fhir_documents')) {
return false;
}
if (empty($opdRow) && $this->db->tableExists('opd_master')) {
$opdRow = $this->db->table('opd_master')->where('opd_id', $opdId)->get(1)->getRowArray() ?? [];
}
if (empty($patientRow) && ! empty($opdRow['p_id']) && $this->db->tableExists('patient_master')) {
$patientRow = $this->db->table('patient_master')->where('id', (int) $opdRow['p_id'])->get(1)->getRowArray() ?? [];
}
if (empty($opdRow) || empty($patientRow)) {
return false;
}
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'])) {
@ -11853,13 +11868,6 @@ class Opd_prescription extends BaseController
'period_end' => Time::now('Asia/Kolkata')->toDateTimeString(),
];
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'];
}
}
$medications = $this->getPrescriptionMedicines($sessionId);
$conditions = $this->getPrescriptionConditions($sessionId);
$clinicalContext = $this->getPrescriptionClinicalContext($sessionId, $opdRow);
@ -11869,7 +11877,9 @@ class Opd_prescription extends BaseController
if ($prescriptionPdf !== null) {
$clinicalContext['attachments'][] = $prescriptionPdf;
}
$bundle = $this->fhirR4Builder->buildOpConsultBundle($patient, $encounter, $medications, $conditions, $clinicalContext);
$builder = $this->fhirR4Builder ?? new \App\Libraries\FhirR4Builder();
$bundle = $builder->buildOpConsultBundle($patient, $encounter, $medications, $conditions, $clinicalContext);
$bundleJson = (string) json_encode($bundle, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$userId = null;
@ -11913,45 +11923,6 @@ class Opd_prescription extends BaseController
]);
}
// Also generate and store PrescriptionRecord FHIR bundle if medicines were prescribed
$prescBundleJson = null;
if (! empty($medications)) {
$prescContext = $clinicalContext;
$prescContext['bundle_type'] = 'PrescriptionRecord';
$prescBundle = $this->fhirR4Builder->buildOpConsultBundle($patient, $encounter, $medications, $conditions, $prescContext);
$prescBundleJson = (string) json_encode($prescBundle, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$existingPrescDoc = $this->db->table('opd_fhir_documents')
->where('opd_id', $opdId)
->where('opd_session_id', $sessionId)
->where('bundle_type', 'PrescriptionRecord')
->orderBy('id', 'DESC')
->get(1)
->getRowArray();
if (! empty($existingPrescDoc['id'])) {
$this->db->table('opd_fhir_documents')
->where('id', (int) $existingPrescDoc['id'])
->update([
'bundle_json' => $prescBundleJson,
'generated_by' => $generatedBy,
'generated_at' => $now,
'updated_at' => $now,
]);
} else {
$this->db->table('opd_fhir_documents')->insert([
'opd_id' => $opdId,
'opd_session_id' => $sessionId,
'bundle_type' => 'PrescriptionRecord',
'bundle_json' => $prescBundleJson,
'generated_by' => $generatedBy,
'generated_at' => $now,
'created_at' => $now,
'updated_at' => $now,
]);
}
}
if (! $inserted) {
return false;
}
@ -11964,7 +11935,7 @@ class Opd_prescription extends BaseController
$cleanDate = str_replace('-', '', $visitDate);
$careContextRef = 'OPD-' . $patientId . '-S' . $sessionId . '-' . $cleanDate;
// 1. Upsert OPConsultRecord
// 1. Upsert single unified OPConsultRecord (covers Consult, Rx, Vitals, and Attachments)
$existingHr = $this->db->table('health_records')
->select('id')
->where('care_context_reference', $careContextRef)
@ -11990,73 +11961,14 @@ class Opd_prescription extends BaseController
$this->db->table('health_records')->insert($hrPayload);
}
// 2. Upsert PrescriptionRecord (if medications exist)
if (! empty($medications) && ! empty($prescBundleJson)) {
$prescCcRef = 'PRESC-' . $patientId . '-S' . $sessionId . '-' . $cleanDate;
$existingPrescHr = $this->db->table('health_records')
->select('id')
->where('care_context_reference', $prescCcRef)
->get(1)
->getRowArray();
$prescHrPayload = [
'patient_id' => $patientId > 0 ? $patientId : null,
'abha_id' => $abhaAddress !== '' ? $abhaAddress : null,
'hi_type' => 'PrescriptionRecord',
'entity_type' => 'opd_prescription',
'entity_id' => (string) $sessionId,
'record_data' => $prescBundleJson,
'care_context_reference' => $prescCcRef,
'push_status' => 'local_discovery_ready',
'updated_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
];
if (! empty($existingPrescHr)) {
$this->db->table('health_records')->where('id', (int) $existingPrescHr['id'])->update($prescHrPayload);
} else {
$prescHrPayload['created_at'] = Time::now('Asia/Kolkata')->toDateTimeString();
$this->db->table('health_records')->insert($prescHrPayload);
}
}
// 3. Upsert WellnessRecord (if vitals exist)
$hasVitals = ! empty($clinicalContext['observations']);
if ($hasVitals && class_exists('\App\Controllers\DoctorDocument')) {
try {
$docCtrl = new \App\Controllers\DoctorDocument();
$wSource = $docCtrl->buildWellnessRecordSource($patientId, $sessionId);
if (! empty($wSource) && ! empty($wSource['vitals'])) {
$wFactory = new \App\Libraries\Abdm\Fhir\FhirGeneratorFactory();
$wGen = $wFactory->wellness()->generate($wSource);
if (! empty($wGen['bundle'])) {
$wellBundleJson = json_encode($wGen['bundle'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$wellCcRef = 'WELLNESS-' . $sessionId . '-' . $cleanDate;
$existingWellHr = $this->db->table('health_records')
->select('id')
->where('care_context_reference', $wellCcRef)
->get(1)
->getRowArray();
$wellHrPayload = [
'patient_id' => $patientId > 0 ? $patientId : null,
'abha_id' => $abhaAddress !== '' ? $abhaAddress : null,
'hi_type' => 'WellnessRecord',
'entity_type' => 'opd_vitals',
'entity_id' => (string) $sessionId,
'record_data' => $wellBundleJson,
'care_context_reference' => $wellCcRef,
'push_status' => 'local_discovery_ready',
'updated_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
];
if (! empty($existingWellHr)) {
$this->db->table('health_records')->where('id', (int) $existingWellHr['id'])->update($wellHrPayload);
} else {
$wellHrPayload['created_at'] = Time::now('Asia/Kolkata')->toDateTimeString();
$this->db->table('health_records')->insert($wellHrPayload);
}
}
}
} catch (\Throwable $we) {
log_message('warning', '[storePrescriptionFhirBundle] auto wellness error: ' . $we->getMessage());
}
}
// Delete redundant standalone PRESC / WELLNESS records so only the single unified bundle is kept
$redundantRefs = [
'PRESC-' . $patientId . '-S' . $sessionId . '-' . $cleanDate,
'WELLNESS-' . $sessionId . '-' . $cleanDate,
];
$this->db->table('health_records')
->whereIn('care_context_reference', $redundantRefs)
->delete();
} catch (\Throwable $e) {
log_message('warning', '[storePrescriptionFhirBundle] auto health_records insert error: ' . $e->getMessage());
}

View File

@ -568,115 +568,9 @@ class AbdmTaskBoardSyncService
}
}
// 5b. If medications exist and we linked OPConsultRecord, also link PrescriptionRecord care context to bridge
$sessionId = (int) ($docRow['opd_session_id'] ?? 0);
if ($isSuccess && $sessionId > 0) {
$hasMeds = false;
$tblPresc = $this->db->tableExists('opd_prescrption_prescribed') ? 'opd_prescrption_prescribed' : ($this->db->tableExists('opd_prescription_prescribed') ? 'opd_prescription_prescribed' : null);
if ($tblPresc !== null) {
$col = $this->db->fieldExists('opd_pre_id', $tblPresc) ? 'opd_pre_id' : ($this->db->fieldExists('prescription_id', $tblPresc) ? 'prescription_id' : null);
if ($col !== null) {
$hasMeds = ($this->db->table($tblPresc)->where($col, $sessionId)->countAllResults() > 0);
}
}
if ($hasMeds) {
$cleanDate = str_replace('-', '', $visitDate);
$prescCcRef = 'PRESC-' . $patientId . '-S' . $sessionId . '-' . $cleanDate;
$prescDisplay = 'Prescription - ' . $visitDate;
try {
$opdCtrl = new \App\Controllers\Opd_prescription();
$opdCtrl->initController(\Config\Services::request(), \Config\Services::response(), service('logger'));
$prescRegen = $opdCtrl->regenerateFhirBundleInternal($opdId, $sessionId, 'PrescriptionRecord');
$prescBundleJson = ! empty($prescRegen['bundle']) ? json_encode($prescRegen['bundle'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : null;
$prescPushData = [
'patient_id' => (string) $patientId,
'patient_name' => $patientName !== '' ? $patientName : ('PATIENT-' . $patientId),
'abha_id' => $abhaNumber,
'abha_address' => $abhaAddress,
'year_of_birth' => $yearOfBirth,
'gender' => $gender,
'hi_type' => 'PrescriptionRecord',
'record_type' => 'PrescriptionRecord',
'visit_date' => $visitDate,
'care_context_reference' => $prescCcRef,
'care_context_display' => $prescDisplay,
'notes' => $prescDisplay,
'queue_id' => $prescCcRef,
];
if (! empty($prescRegen['bundle'])) {
$prescPushData['bundle'] = $prescRegen['bundle'];
$prescPushData['fhir_bundle'] = $prescRegen['bundle'];
}
$prescRes = $this->connector->pushRecord($prescPushData);
$this->recordLinkedState(
$patientId,
$effectiveAbha,
'PrescriptionRecord',
'opd_prescription',
(string) $sessionId,
$prescCcRef,
(string) ($prescRes['queue_id'] ?? $prescCcRef),
(int) ($prescRes['record_id'] ?? 0),
$prescBundleJson
);
} catch (\Throwable $pe) {
log_message('warning', '[linkAndPushOpdRecord] Prescription care context push error: ' . $pe->getMessage());
}
}
// 5c. If vitals exist, also link WellnessRecord care context to bridge
try {
$pRow = $this->db->table('opd_prescription')->where('id', $sessionId)->get(1)->getRowArray();
$hasVitals = ! empty($pRow['bp']) || ! empty($pRow['pulse']) || ! empty($pRow['temp']) || ! empty($pRow['spo2']) || ! empty($pRow['weight']);
if ($hasVitals && class_exists('\App\Controllers\DoctorDocument')) {
$cleanDate = str_replace('-', '', $visitDate);
$wellCcRef = 'WELLNESS-' . $sessionId . '-' . $cleanDate;
$docCtrl = new \App\Controllers\DoctorDocument();
$wSource = $docCtrl->buildWellnessRecordSource($patientId, $sessionId);
if (! empty($wSource) && ! empty($wSource['vitals'])) {
$wFactory = new \App\Libraries\Abdm\Fhir\FhirGeneratorFactory();
$wGen = $wFactory->wellness()->generate($wSource);
$wellBundleJson = ! empty($wGen['bundle']) ? json_encode($wGen['bundle'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : null;
$wellDisplay = 'Wellness Record - ' . $visitDate;
$wellPushData = [
'patient_id' => (string) $patientId,
'patient_name' => $patientName !== '' ? $patientName : ('PATIENT-' . $patientId),
'abha_id' => $abhaNumber,
'abha_address' => $abhaAddress,
'year_of_birth' => $yearOfBirth,
'gender' => $gender,
'hi_type' => 'WellnessRecord',
'record_type' => 'WellnessRecord',
'visit_date' => $visitDate,
'care_context_reference' => $wellCcRef,
'care_context_display' => $wellDisplay,
'notes' => $wellDisplay,
'queue_id' => $wellCcRef,
];
if (! empty($wGen['bundle'])) {
$wellPushData['bundle'] = $wGen['bundle'];
$wellPushData['fhir_bundle'] = $wGen['bundle'];
}
$wellRes = $this->connector->pushRecord($wellPushData);
$this->recordLinkedState(
$patientId,
$effectiveAbha,
'WellnessRecord',
'opd_vitals',
(string) $sessionId,
$wellCcRef,
(string) ($wellRes['queue_id'] ?? $wellCcRef),
(int) ($wellRes['record_id'] ?? 0),
$wellBundleJson
);
}
}
} catch (\Throwable $we) {
log_message('warning', '[linkAndPushOpdRecord] Wellness care context push error: ' . $we->getMessage());
}
}
// In ABDM standard, OPConsultRecord is a single unified bundle that already includes
// consultation note, physical examination (wellness/vitals), prescriptions (medications),
// and attached health documents. We intentionally do NOT create duplicate care contexts.
if ($isSuccess) {
return [