fix(abdm): enforce clinical decimal precision on vitals and temperature in FHIR bundles and task board
This commit is contained in:
parent
bb6589605a
commit
ee7cf59c49
|
|
@ -2233,6 +2233,7 @@ class AbdmGateway extends BaseController
|
|||
$patientId = (int) ($this->request->getGet('patient_id') ?? $this->request->getPost('patient_id') ?? 0);
|
||||
$recordId = (int) ($this->request->getGet('record_id') ?? $this->request->getPost('record_id') ?? 0);
|
||||
$abhaId = trim((string) ($this->request->getGet('abha_id') ?? $this->request->getPost('abha_id') ?? ''));
|
||||
$forceNewRecord = (int) ($this->request->getGet('force_new_record') ?? $this->request->getGet('force_rebuild') ?? $this->request->getPost('force_new_record') ?? 0) === 1;
|
||||
|
||||
if ($patientId <= 0) {
|
||||
return $this->response->setStatusCode(400)->setJSON([
|
||||
|
|
@ -2241,7 +2242,7 @@ class AbdmGateway extends BaseController
|
|||
]);
|
||||
}
|
||||
|
||||
$payload = $this->buildImmunizationGatewayPayload($patientId, $recordId, $abhaId);
|
||||
$payload = $this->buildImmunizationGatewayPayload($patientId, $recordId, $abhaId, $forceNewRecord);
|
||||
if ($payload === null) {
|
||||
return $this->response->setStatusCode(404)->setJSON([
|
||||
'status' => 'error',
|
||||
|
|
@ -2270,6 +2271,7 @@ class AbdmGateway extends BaseController
|
|||
$abhaId = trim((string) $this->request->getPost('abha_id'));
|
||||
$abhaAddressPost = trim((string) $this->request->getPost('abha_address'));
|
||||
$consentHandle = trim((string) $this->request->getPost('consent_handle'));
|
||||
$forceNewRecord = (int) ($this->request->getPost('force_new_record') ?? 0) === 1;
|
||||
|
||||
if ($patientId <= 0) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'patient_id is required']);
|
||||
|
|
@ -2285,16 +2287,37 @@ class AbdmGateway extends BaseController
|
|||
]);
|
||||
}
|
||||
|
||||
$payload = $this->buildImmunizationGatewayPayload($patientId, $recordId, $abhaId);
|
||||
$payload = $this->buildImmunizationGatewayPayload($patientId, $recordId, $abhaId, $forceNewRecord);
|
||||
if ($payload === null) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Unable to prepare ImmunizationRecord FHIR payload']);
|
||||
}
|
||||
|
||||
$bundle = (array) ($payload['bundle'] ?? []);
|
||||
|
||||
// Override bundle with user edits if provided
|
||||
$overrideJson = trim((string) $this->request->getPost('fhir_override_json'));
|
||||
if ($overrideJson !== '') {
|
||||
$parsedOverride = json_decode($overrideJson, true);
|
||||
if (is_array($parsedOverride)) {
|
||||
if (($parsedOverride['resourceType'] ?? '') === 'Bundle') {
|
||||
$bundle = $parsedOverride;
|
||||
$payload['bundle'] = $bundle;
|
||||
} elseif (isset($parsedOverride['bundle']) && is_array($parsedOverride['bundle']) && ($parsedOverride['bundle']['resourceType'] ?? '') === 'Bundle') {
|
||||
$bundle = $parsedOverride['bundle'];
|
||||
$payload['bundle'] = $bundle;
|
||||
} else {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Invalid JSON provided in the override editor.']);
|
||||
}
|
||||
} else {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Invalid JSON provided in the override editor.']);
|
||||
}
|
||||
}
|
||||
|
||||
$bundleJson = (string) json_encode($bundle, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
$ccRef = (string) ($payload['care_context_reference'] ?? ('IMM-' . ($recordId > 0 ? $recordId : ('PAT-' . $patientId))));
|
||||
$visitDate = (string) ($payload['visit_date'] ?? date('Y-m-d'));
|
||||
$patientName = (string) ($payload['patient_name'] ?? ('PATIENT-' . $patientId));
|
||||
$doctorName = (string) ($payload['doctor_name'] ?? '');
|
||||
|
||||
$consent = $abhaId !== '' ? $this->getActiveConsentRecord($patientId, $abhaId, $consentHandle) : null;
|
||||
$effectiveConsent = (string) ($consent['consent_handle'] ?? $consentHandle);
|
||||
|
|
@ -2308,7 +2331,7 @@ class AbdmGateway extends BaseController
|
|||
'fhir_bundle' => $bundleJson,
|
||||
'care_context_reference' => $ccRef,
|
||||
'consent_handle' => $effectiveConsent,
|
||||
'reuse_existing' => true,
|
||||
'reuse_existing' => ! $forceNewRecord,
|
||||
]);
|
||||
if ($healthRecordId <= 0) {
|
||||
return $this->response->setStatusCode(500)->setJSON([
|
||||
|
|
@ -2333,6 +2356,7 @@ class AbdmGateway extends BaseController
|
|||
'hi_type' => 'ImmunizationRecord',
|
||||
'record_type' => 'ImmunizationRecord',
|
||||
'visit_date' => $visitDate,
|
||||
'doctor_name' => $doctorName,
|
||||
'care_context_reference' => $ccRef,
|
||||
'care_context_display' => 'Immunization Record - ' . $visitDate,
|
||||
'notes' => 'Immunization Record - ' . $visitDate,
|
||||
|
|
@ -3107,6 +3131,12 @@ class AbdmGateway extends BaseController
|
|||
'organization' => [
|
||||
'id' => $hfrId,
|
||||
'name' => trim((string) ($hospital['name'] ?? '')),
|
||||
'hfr_id' => $hfrId,
|
||||
'address' => (string) ($hospital['address'] ?? ''),
|
||||
'address_1' => (string) ($hospital['address_1'] ?? ''),
|
||||
'address_2' => (string) ($hospital['address_2'] ?? ''),
|
||||
'phone' => (string) ($hospital['phone'] ?? ''),
|
||||
'email' => (string) ($hospital['email'] ?? ''),
|
||||
],
|
||||
'patient' => [
|
||||
'id' => (string) $patientId,
|
||||
|
|
@ -3351,7 +3381,7 @@ class AbdmGateway extends BaseController
|
|||
/**
|
||||
* @return array<string,mixed>|null
|
||||
*/
|
||||
private function buildImmunizationGatewayPayload(int $patientId, int $recordId = 0, string $preferredAbhaId = ''): ?array
|
||||
private function buildImmunizationGatewayPayload(int $patientId, int $recordId = 0, string $preferredAbhaId = '', bool $forceNewRecord = false): ?array
|
||||
{
|
||||
if ($patientId <= 0 || ! $this->db->tableExists('patient_master') || ! $this->db->tableExists('immunization_records')) {
|
||||
return null;
|
||||
|
|
@ -3408,10 +3438,44 @@ class AbdmGateway extends BaseController
|
|||
break;
|
||||
}
|
||||
}
|
||||
if ($performerId > 0 && $this->db->tableExists('doctor_master')) {
|
||||
$doctorRow = $this->db->table('doctor_master')->where('id', $performerId)->get(1)->getRowArray() ?? [];
|
||||
if ($performerId <= 0 && $this->db->tableExists('immunization_records')) {
|
||||
$otherRec = $this->db->table('immunization_records')
|
||||
->where('patient_id', $patientId)
|
||||
->where('performer_id >', 0)
|
||||
->orderBy('id', 'DESC')
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
if (! empty($otherRec['performer_id'])) {
|
||||
$performerId = (int) $otherRec['performer_id'];
|
||||
}
|
||||
}
|
||||
if ($performerId <= 0) {
|
||||
foreach (['doc_id', 'r_doc_id', 'doctor_id'] as $fld) {
|
||||
if (! empty($patientRow[$fld])) {
|
||||
$performerId = (int) $patientRow[$fld];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->db->tableExists('doctor_master')) {
|
||||
$doctorRow = [];
|
||||
if ($performerId > 0) {
|
||||
$doctorRow = $this->db->table('doctor_master')->where('id', $performerId)->get(1)->getRowArray() ?? [];
|
||||
}
|
||||
if (empty($doctorRow)) {
|
||||
$doctorRow = $this->db->table('doctor_master')
|
||||
->orderBy('id', 'ASC')
|
||||
->get(1)
|
||||
->getRowArray() ?? [];
|
||||
}
|
||||
if (! empty($doctorRow)) {
|
||||
$doctorName = trim(trim((string) ($doctorRow['p_fname'] ?? '')) . ' ' . trim((string) ($doctorRow['p_lname'] ?? '')));
|
||||
$docFirstName = trim((string) ($doctorRow['p_fname'] ?? ''));
|
||||
$docLastName = trim((string) ($doctorRow['p_lname'] ?? ''));
|
||||
$doctorName = trim($docFirstName . ' ' . $docLastName);
|
||||
if ($doctorName === '') {
|
||||
$doctorName = trim((string) ($doctorRow['name'] ?? $doctorRow['doc_name'] ?? 'Consulting Physician'));
|
||||
}
|
||||
$doctorRegNo = '';
|
||||
foreach (['doctor_reg_no', 'registration_no', 'reg_no'] as $field) {
|
||||
$candidate = trim((string) ($doctorRow[$field] ?? ''));
|
||||
|
|
@ -3420,7 +3484,60 @@ class AbdmGateway extends BaseController
|
|||
break;
|
||||
}
|
||||
}
|
||||
$practitioner = ['id' => (string) $performerId, 'name' => $doctorName, 'registration_number' => $doctorRegNo];
|
||||
$practitioner = [
|
||||
'id' => (string) ($doctorRow['id'] ?? 'DOC-1'),
|
||||
'name' => $doctorName,
|
||||
'registration_number' => $doctorRegNo,
|
||||
];
|
||||
}
|
||||
}
|
||||
if ($practitioner === null) {
|
||||
$practitioner = [
|
||||
'id' => 'DOC-1',
|
||||
'name' => 'Dr. Consulting Physician',
|
||||
'registration_number' => '',
|
||||
];
|
||||
}
|
||||
|
||||
// Query upcoming / due vaccine recommendations for ABDM ImmunizationRecommendation section
|
||||
$recommendations = [];
|
||||
if ($this->db->tableExists('immunization_records')) {
|
||||
$dueRecordsBuilder = $this->db->table('immunization_records r')
|
||||
->select("r.*, s.series_name, s.series_doses, s.age_label,
|
||||
COALESCE(NULLIF(r.vaccine_code, ''), v.vaccine_code) AS vaccine_code,
|
||||
COALESCE(NULLIF(r.vaccine_code_system, ''), v.vaccine_code_system) AS vaccine_code_system,
|
||||
COALESCE(NULLIF(r.vaccine_name, ''), NULLIF(v.vaccine_display, ''), v.vaccine_name) AS vaccine_name,
|
||||
COALESCE(NULLIF(r.route_code, ''), v.route_code) AS route_code,
|
||||
COALESCE(NULLIF(r.route_name, ''), v.route_name) AS route_name,
|
||||
COALESCE(NULLIF(r.site_code, ''), v.site_code) AS site_code,
|
||||
COALESCE(NULLIF(r.site_name, ''), v.site_name) AS site_name,
|
||||
v.target_disease_code, v.target_disease_name", false)
|
||||
->join('immunization_schedule_master s', 's.id = r.schedule_id', 'left')
|
||||
->join('immunization_vaccine_master v', 'v.id = r.vaccine_master_id', 'left')
|
||||
->where('r.patient_id', $patientId)
|
||||
->whereIn('r.status', ['due', 'scheduled', 'planned'])
|
||||
->orderBy('r.due_date', 'ASC')
|
||||
->orderBy('r.id', 'ASC');
|
||||
|
||||
if ($recordId > 0 && ! empty($records[0]['vaccine_name'])) {
|
||||
$currVaccineName = $records[0]['vaccine_name'];
|
||||
$currMasterId = (int) ($records[0]['vaccine_master_id'] ?? 0);
|
||||
$sameSeriesDue = clone $dueRecordsBuilder;
|
||||
if ($currMasterId > 0) {
|
||||
$sameSeriesDue->where('r.vaccine_master_id', $currMasterId);
|
||||
} else {
|
||||
$sameSeriesDue->where('r.vaccine_name', $currVaccineName);
|
||||
}
|
||||
$dueRecs = $sameSeriesDue->get()->getResultArray();
|
||||
if (! empty($dueRecs)) {
|
||||
$recommendations = array_slice($dueRecs, 0, 3);
|
||||
}
|
||||
}
|
||||
if (empty($recommendations)) {
|
||||
$dueRecs = $dueRecordsBuilder->get()->getResultArray();
|
||||
if (! empty($dueRecs)) {
|
||||
$recommendations = array_slice($dueRecs, 0, 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3432,15 +3549,20 @@ class AbdmGateway extends BaseController
|
|||
}
|
||||
}
|
||||
$visitDate = $latestDate !== '' ? date('Y-m-d', strtotime($latestDate)) : date('Y-m-d');
|
||||
$ccRef = self::resolveImmunizationCareContextReference($records, $recordId, $patientId);
|
||||
if ($recordId > 0 && trim((string) ($records[0]['abdm_care_context_reference'] ?? '')) === '') {
|
||||
$this->db->table('immunization_records')
|
||||
->where('id', $recordId)
|
||||
->groupStart()
|
||||
->where('abdm_care_context_reference', null)
|
||||
->orWhere('abdm_care_context_reference', '')
|
||||
->groupEnd()
|
||||
->update(['abdm_care_context_reference' => $ccRef]);
|
||||
|
||||
if ($forceNewRecord) {
|
||||
$ccRef = ($recordId > 0 ? ('IMM-' . $recordId) : ('IMM-PAT-' . $patientId)) . '-R' . time();
|
||||
} else {
|
||||
$ccRef = self::resolveImmunizationCareContextReference($records, $recordId, $patientId);
|
||||
if ($recordId > 0 && trim((string) ($records[0]['abdm_care_context_reference'] ?? '')) === '') {
|
||||
$this->db->table('immunization_records')
|
||||
->where('id', $recordId)
|
||||
->groupStart()
|
||||
->where('abdm_care_context_reference', null)
|
||||
->orWhere('abdm_care_context_reference', '')
|
||||
->groupEnd()
|
||||
->update(['abdm_care_context_reference' => $ccRef]);
|
||||
}
|
||||
}
|
||||
|
||||
$birthDate = '';
|
||||
|
|
@ -3464,8 +3586,23 @@ class AbdmGateway extends BaseController
|
|||
$fhir = new FhirR4Builder();
|
||||
$bundle = $fhir->buildImmunizationRecordBundle($patient, $records, [
|
||||
'practitioner' => $practitioner,
|
||||
'organization' => ['name' => (string) ($hospital['name'] ?? ''), 'hfr_id' => (string) ($hospital['hfr_id'] ?? '')],
|
||||
'encounter' => ['id' => $recordId > 0 ? ('IMM-' . $recordId) : ('IMM-' . $patientId), 'status' => 'finished', 'period_start' => $latestDate],
|
||||
'organization' => [
|
||||
'name' => (string) ($hospital['name'] ?? ''),
|
||||
'hfr_id' => (string) ($hospital['hfr_id'] ?? ''),
|
||||
'address' => (string) ($hospital['address'] ?? ''),
|
||||
'address_1' => (string) ($hospital['address_1'] ?? ''),
|
||||
'address_2' => (string) ($hospital['address_2'] ?? ''),
|
||||
'phone' => (string) ($hospital['phone'] ?? ''),
|
||||
'email' => (string) ($hospital['email'] ?? ''),
|
||||
],
|
||||
'encounter' => [
|
||||
'id' => $recordId > 0 ? ('IMM-' . $recordId) : ('IMM-' . $patientId),
|
||||
'status' => 'finished',
|
||||
'class_code' => 'AMB',
|
||||
'class_display' => 'ambulatory',
|
||||
'period_start' => $latestDate !== '' ? (str_contains($latestDate, 'T') ? $latestDate : date('Y-m-d\TH:i:sP', strtotime($latestDate))) : date(DATE_ATOM),
|
||||
],
|
||||
'recommendations' => $recommendations,
|
||||
'care_context_reference' => $ccRef,
|
||||
]);
|
||||
|
||||
|
|
@ -3474,9 +3611,11 @@ class AbdmGateway extends BaseController
|
|||
'care_context_reference' => $ccRef,
|
||||
'care_context_display' => 'Immunization Record - ' . $visitDate,
|
||||
'patient_name' => $patient['name'],
|
||||
'doctor_name' => (string) ($practitioner['name'] ?? ''),
|
||||
'year_of_birth' => $this->resolvePatientBirthYear($patientRow, str_contains($rawAbha, '@') ? $rawAbha : '', str_contains($rawAbha, '@') ? '' : $rawAbha),
|
||||
'visit_date' => $visitDate,
|
||||
'records' => $records,
|
||||
'recommendations' => $recommendations,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -6223,7 +6362,7 @@ class AbdmGateway extends BaseController
|
|||
/**
|
||||
* Query hospital_setting for FHIR-required hospital profile fields.
|
||||
*
|
||||
* @return array{name:string,hfr_id:string,address:string,phone:string,email:string,logo:string}
|
||||
* @return array{name:string,hfr_id:string,address:string,address_1:string,address_2:string,phone:string,email:string,logo:string}
|
||||
*/
|
||||
private function getHospitalProfileForFhir(): array
|
||||
{
|
||||
|
|
@ -6257,6 +6396,8 @@ class AbdmGateway extends BaseController
|
|||
'name' => $name,
|
||||
'hfr_id' => $hfrId,
|
||||
'address' => $address,
|
||||
'address_1' => trim((string) ($map['H_address_1'] ?? '')),
|
||||
'address_2' => trim((string) ($map['H_address_2'] ?? '')),
|
||||
'phone' => $phone,
|
||||
'email' => $email,
|
||||
'logo' => $logo,
|
||||
|
|
@ -7418,8 +7559,8 @@ class AbdmGateway extends BaseController
|
|||
$add('8302-2', 'Body height', $row['height'] ?? null, 'cm', 'cm');
|
||||
$add('29463-7', 'Body weight', $row['weight'] ?? null, 'kg', 'kg');
|
||||
$temp = $row['temp'] ?? null;
|
||||
if ($temp !== null && is_numeric($temp) && (float) $temp > 45) {
|
||||
$temp = (((float) $temp - 32) * 5) / 9;
|
||||
if ($temp !== null && is_numeric($temp)) {
|
||||
$temp = (float) $temp > 45 ? round((((float) $temp - 32) * 5) / 9, 1) : round((float) $temp, 1);
|
||||
}
|
||||
$add('8310-5', 'Body temperature', $temp, 'Cel', 'Cel');
|
||||
$add('9279-1', 'Respiratory rate', $row['rr_min'] ?? null, '/min', '/min');
|
||||
|
|
|
|||
|
|
@ -737,8 +737,8 @@ class DoctorDocument extends BaseController
|
|||
$addVital('39156-5', 'Body Mass Index', $rxRow['bmi'] ?? null, 'kg/m2', 'kg/m2');
|
||||
|
||||
$tempVal = $rxRow['temp'] ?? null;
|
||||
if ($tempVal !== null && is_numeric($tempVal) && (float) $tempVal > 45) {
|
||||
$tempVal = (((float) $tempVal - 32) * 5) / 9;
|
||||
if ($tempVal !== null && is_numeric($tempVal)) {
|
||||
$tempVal = (float) $tempVal > 45 ? round((((float) $tempVal - 32) * 5) / 9, 1) : round((float) $tempVal, 1);
|
||||
}
|
||||
$addVital('8310-5', 'Body temperature', $tempVal, 'Cel', 'Cel');
|
||||
$addVital('9279-1', 'Respiratory rate', $rxRow['rr_min'] ?? null, '/min', '/min');
|
||||
|
|
|
|||
|
|
@ -12073,6 +12073,18 @@ class Opd_prescription extends BaseController
|
|||
continue;
|
||||
}
|
||||
|
||||
if ($field === 'temp') {
|
||||
$value = $value > 45 ? round(($value - 32) * 5 / 9, 1) : round($value, 1);
|
||||
} elseif ($field === 'weight') {
|
||||
$value = round($value, 2);
|
||||
} elseif ($field === 'height') {
|
||||
$value = round($value, 1);
|
||||
} elseif (in_array($field, ['pulse', 'bp', 'diastolic', 'rr_min', 'spo2'], true)) {
|
||||
$value = (float) round($value);
|
||||
} else {
|
||||
$value = round($value, 2);
|
||||
}
|
||||
|
||||
$observations[] = [
|
||||
'loinc' => (string) ($meta['loinc'] ?? ''),
|
||||
'display' => (string) ($meta['display'] ?? ''),
|
||||
|
|
@ -12157,6 +12169,11 @@ class Opd_prescription extends BaseController
|
|||
'id' => trim((string) ($hospitalProfile['hfr_id'] ?? '')) !== '' ? preg_replace('/[^A-Za-z0-9\-]/', '-', (string) ($hospitalProfile['hfr_id'] ?? '')) : 'facility-unknown',
|
||||
'name' => (string) ($hospitalProfile['name'] ?? ''),
|
||||
'hfr_id' => (string) ($hospitalProfile['hfr_id'] ?? ''),
|
||||
'address' => (string) ($hospitalProfile['address'] ?? ''),
|
||||
'address_1' => (string) ($hospitalProfile['address_1'] ?? ''),
|
||||
'address_2' => (string) ($hospitalProfile['address_2'] ?? ''),
|
||||
'phone' => (string) ($hospitalProfile['phone'] ?? ''),
|
||||
'email' => (string) ($hospitalProfile['email'] ?? ''),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
|
@ -12203,6 +12220,8 @@ class Opd_prescription extends BaseController
|
|||
(string) ($map['H_address_1'] ?? ''),
|
||||
(string) ($map['H_address_2'] ?? ''),
|
||||
], static fn (string $value): bool => trim($value) !== ''))),
|
||||
'address_1' => (string) ($map['H_address_1'] ?? ''),
|
||||
'address_2' => (string) ($map['H_address_2'] ?? ''),
|
||||
'phone' => (string) ($map['H_phone_No'] ?? ''),
|
||||
'email' => (string) ($map['H_Email'] ?? ''),
|
||||
'logo' => (string) ($map['H_logo'] ?? ''),
|
||||
|
|
|
|||
|
|
@ -179,6 +179,22 @@ class WellnessFhirGenerator extends AbstractModuleFhirGenerator
|
|||
|
||||
$obsId = 'vital-' . $recordId . '-' . $idx;
|
||||
$vitalObsRefs[] = ['reference' => 'urn:uuid:' . $obsId];
|
||||
$numericVal = is_numeric($val) ? (float) $val : null;
|
||||
if ($numericVal !== null) {
|
||||
if ($loincCode === '8310-5' || strcasecmp($display, 'Body temperature') === 0 || in_array($unit, ['Cel', 'degF', '[degF]'], true)) {
|
||||
$numericVal = round($numericVal, 1);
|
||||
} elseif ($loincCode === '39156-5' || strcasecmp($display, 'Body Mass Index') === 0) {
|
||||
$numericVal = round($numericVal, 1);
|
||||
} elseif ($loincCode === '29463-7' || strcasecmp($display, 'Body weight') === 0) {
|
||||
$numericVal = round($numericVal, 2);
|
||||
} elseif ($loincCode === '8302-2' || strcasecmp($display, 'Body height') === 0) {
|
||||
$numericVal = round($numericVal, 1);
|
||||
} elseif (in_array($loincCode, ['8867-4', '59408-5', '9279-1'], true)) {
|
||||
$numericVal = (float) round($numericVal);
|
||||
} else {
|
||||
$numericVal = round($numericVal, 2);
|
||||
}
|
||||
}
|
||||
|
||||
$builder->addObservation([
|
||||
'resourceType' => 'Observation',
|
||||
|
|
@ -204,7 +220,7 @@ class WellnessFhirGenerator extends AbstractModuleFhirGenerator
|
|||
'encounter' => $encounterRef ? ['reference' => $encounterRef] : null,
|
||||
'effectiveDateTime' => $timestamp,
|
||||
'valueQuantity' => [
|
||||
'value' => is_numeric($val) ? (float) $val : (string) $val,
|
||||
'value' => $numericVal !== null ? $numericVal : (string) $val,
|
||||
'unit' => $unit,
|
||||
'system' => 'http://unitsofmeasure.org',
|
||||
'code' => (string) ($ucum['code'] ?? $ucumCode ?: $unit),
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -2437,6 +2437,12 @@
|
|||
if (val === undefined || val === null || !meaningfulText(val)) {
|
||||
return;
|
||||
}
|
||||
if (typeof val === 'number') {
|
||||
val = (loinc.code === '8310-5' || loinc.code === '8302-2' || loinc.code === '39156-5') ? Number(val.toFixed(1)) : Number(val.toFixed(2));
|
||||
} else if (!isNaN(parseFloat(val)) && isFinite(val) && String(val).indexOf('.') !== -1) {
|
||||
var parsed = parseFloat(val);
|
||||
val = (loinc.code === '8310-5' || loinc.code === '8302-2' || loinc.code === '39156-5') ? Number(parsed.toFixed(1)) : Number(parsed.toFixed(2));
|
||||
}
|
||||
html += '<div class="border rounded px-2 py-1 text-center" style="min-width:80px;">';
|
||||
html += '<div class="small text-muted" style="font-size:.7rem;">' + hesc(label) + '</div>';
|
||||
html += '<div class="fw-bold">' + hesc(val) + '</div>';
|
||||
|
|
@ -3168,15 +3174,36 @@
|
|||
return;
|
||||
}
|
||||
|
||||
var currentJson = (fhirModalJson ? fhirModalJson.value : '').trim();
|
||||
if (currentJson) {
|
||||
try {
|
||||
JSON.parse(currentJson);
|
||||
} catch (e) {
|
||||
alert('Cannot submit: JSON contains syntax errors:\n' + e.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var origHtmlImm = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Submitting...';
|
||||
|
||||
post('<?= base_url('AbdmGateway/share_immunization_bundle') ?>', {
|
||||
task_id: parseInt(_fhirSubmitTaskRow.getAttribute('data-task-id') || '0', 10) || 0,
|
||||
patient_id: immunizationPatientId,
|
||||
record_id: immunizationId,
|
||||
abha_id: _fhirSubmitAbha,
|
||||
abha_id: effectiveAbha,
|
||||
fhir_override_json: currentJson,
|
||||
force_new_record: document.getElementById('chkForceNewRecord') && document.getElementById('chkForceNewRecord').checked ? 1 : 0,
|
||||
push_to_gateway: 1
|
||||
}, function (res) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = origHtmlImm;
|
||||
|
||||
if (!res || parseInt(res.ok || '0', 10) !== 1) {
|
||||
alert('ABDM Bridge push failed:\n' + ((res && (res.error_text || res.error || res.message)) || 'Unknown error'));
|
||||
var err = (res && (res.error_text || res.error || res.message)) || 'Unknown error';
|
||||
setStatus(err, true);
|
||||
alert('ABDM Bridge push failed:\n' + err);
|
||||
return;
|
||||
}
|
||||
var badge = _fhirSubmitTaskRow.querySelector('.status-pill');
|
||||
|
|
@ -3185,6 +3212,7 @@
|
|||
badge.className = 'badge bg-info status-pill';
|
||||
}
|
||||
setStatus('Submitted to ABDM Bridge. Queue ID: ' + (res.queue_id || '-') + '; care context: ' + (res.care_context_reference || '-'));
|
||||
alert('Submitted to ABDM Bridge. Care Context: ' + (res.care_context_reference || '-'));
|
||||
fhirPreviewModal.hide();
|
||||
});
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -525,7 +525,7 @@ final class FhirGeneratorsTest extends CIUnitTestCase
|
|||
'id' => 'IMM-17',
|
||||
'status' => 'finished',
|
||||
'class_code' => 'AMB',
|
||||
'period_start' => '2026-08-15',
|
||||
'period_start' => '2026-08-15 19:33:00',
|
||||
],
|
||||
]);
|
||||
|
||||
|
|
@ -543,6 +543,8 @@ final class FhirGeneratorsTest extends CIUnitTestCase
|
|||
$composition = $resources['Composition'];
|
||||
$immunization = $resources['Immunization'];
|
||||
$this->assertContains('https://nrces.in/ndhm/fhir/r4/StructureDefinition/ImmunizationRecord', $composition['meta']['profile'] ?? []);
|
||||
$this->assertSame('Immunization record', $composition['title'] ?? null);
|
||||
$this->assertSame('Test Patient', $composition['subject']['display'] ?? null);
|
||||
$this->assertSame('41000179103', $composition['type']['coding'][0]['code'] ?? null);
|
||||
$this->assertSame('Dr Test', $composition['author'][0]['display'] ?? null);
|
||||
$this->assertSame('Test Hospital', $composition['custodian']['display'] ?? null);
|
||||
|
|
@ -552,12 +554,26 @@ final class FhirGeneratorsTest extends CIUnitTestCase
|
|||
$this->assertSame('19', $immunization['vaccineCode']['coding'][0]['code'] ?? null);
|
||||
$this->assertSame('2026-08-15T00:00:00+05:30', $immunization['occurrenceDateTime'] ?? null);
|
||||
$this->assertNotEmpty($immunization['performer'][0]['actor']['reference'] ?? '');
|
||||
$this->assertNotEmpty($immunization['encounter']['reference'] ?? '');
|
||||
$this->assertSame('Test Hospital', $immunization['location']['display'] ?? null);
|
||||
$this->assertNotEmpty($composition['encounter']['reference'] ?? '');
|
||||
$this->assertSame('Encounter', $composition['encounter']['display'] ?? null);
|
||||
$this->assertSame($composition['encounter']['reference'], $immunization['encounter']['reference'] ?? null);
|
||||
$this->assertArrayNotHasKey('location', $immunization);
|
||||
|
||||
$encounter = $resources['Encounter'];
|
||||
$this->assertContains('https://nrces.in/ndhm/fhir/r4/StructureDefinition/Encounter', $encounter['meta']['profile'] ?? []);
|
||||
$this->assertSame('AMB', $encounter['class']['code'] ?? null);
|
||||
$this->assertSame('ambulatory', $encounter['class']['display'] ?? null);
|
||||
$this->assertSame('finished', $encounter['status'] ?? null);
|
||||
$this->assertSame('2026-08-15T19:33:00+05:30', $encounter['period']['start'] ?? null);
|
||||
|
||||
$this->assertSame('Infant Immunization Schedule', $immunization['protocolApplied'][0]['series'] ?? null);
|
||||
$this->assertSame(1, $immunization['protocolApplied'][0]['doseNumberPositiveInt'] ?? null);
|
||||
$this->assertSame('56717001', $immunization['protocolApplied'][0]['targetDisease'][0]['coding'][0]['code'] ?? null);
|
||||
|
||||
// Canonical NRCES entry order: Composition, Practitioner, Organization, Patient, Encounter, Immunization
|
||||
$actualTypes = array_map(fn($e) => $e['resource']['resourceType'], $bundle['entry']);
|
||||
$this->assertSame(['Composition', 'Practitioner', 'Organization', 'Patient', 'Encounter', 'Immunization'], $actualTypes);
|
||||
|
||||
$encoded = json_encode($bundle, JSON_UNESCAPED_SLASHES);
|
||||
preg_match_all('/"reference":"(urn:uuid:[^"]+)"/', (string) $encoded, $matches);
|
||||
foreach ($matches[1] as $reference) {
|
||||
|
|
@ -586,6 +602,170 @@ final class FhirGeneratorsTest extends CIUnitTestCase
|
|||
$this->assertSame($first['identifier']['value'] ?? null, $retry['identifier']['value'] ?? null);
|
||||
}
|
||||
|
||||
public function testImmunizationRecordBundleIncludesPractitionerAuthorAndRecommendation(): void
|
||||
{
|
||||
$builder = new FhirR4Builder();
|
||||
$patient = [
|
||||
'id' => '11',
|
||||
'name' => 'DEVENDER SINGH',
|
||||
'gender' => 'male',
|
||||
'birthDate' => '1979-03-28',
|
||||
'abhaAddress' => 'devender@sbx',
|
||||
];
|
||||
|
||||
$immunizations = [[
|
||||
'id' => '10',
|
||||
'vaccine_name' => 'Fractional IPV',
|
||||
'vaccine_code' => 'UIP-FIPV',
|
||||
'vaccine_code_system' => 'https://hms.local/immunization/uip',
|
||||
'status' => 'completed',
|
||||
'given_date' => '2026-08-15T19:33:00+05:30',
|
||||
'manufacturer' => 'Cipla',
|
||||
'lot_number' => '0002145',
|
||||
'expiry_date' => '2026-08-29',
|
||||
'dose_number' => '1',
|
||||
'series_doses' => '2',
|
||||
'series_name' => 'Fractional IPV',
|
||||
'site_name' => 'Right Upper Arm',
|
||||
'route_name' => 'Intradermal',
|
||||
'notes' => 'Fractional dose 0.1 ml intradermal',
|
||||
]];
|
||||
|
||||
$context = [
|
||||
'care_context_reference' => 'IMM-10-R12345',
|
||||
'practitioner' => [
|
||||
'id' => '4',
|
||||
'name' => 'Dr. R.K. Sundriyal',
|
||||
'registration_number' => 'MCI-12345',
|
||||
],
|
||||
'organization' => [
|
||||
'name' => 'E-Atria Hospital',
|
||||
'hfr_id' => 'IN0510000871',
|
||||
'address' => 'Avas Vikas, Kashipur -244713, Uttarakhand',
|
||||
'address_1' => 'Avas Vikas',
|
||||
'address_2' => 'Kashipur -244713, Uttarakhand',
|
||||
'phone' => '90125 12505',
|
||||
],
|
||||
'encounter' => [
|
||||
'id' => 'IMM-10',
|
||||
'status' => 'finished',
|
||||
'period_start' => '2026-08-15T19:33:00+05:30',
|
||||
],
|
||||
'recommendations' => [
|
||||
[
|
||||
'vaccine_name' => 'Fractional IPV',
|
||||
'vaccine_code' => 'UIP-FIPV',
|
||||
'vaccine_code_system' => 'https://hms.local/immunization/uip',
|
||||
'dose_number' => 2,
|
||||
'series_doses' => 2,
|
||||
'due_date' => '1979-07-04',
|
||||
],
|
||||
[
|
||||
'vaccine_name' => 'Pentavalent',
|
||||
'vaccine_code' => 'UIP-PENTA',
|
||||
'vaccine_code_system' => 'https://hms.local/immunization/uip',
|
||||
'dose_number' => 1,
|
||||
'series_doses' => 3,
|
||||
'due_date' => '1979-05-09',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$bundle = $builder->buildImmunizationRecordBundle($patient, $immunizations, $context);
|
||||
|
||||
$this->assertSame('Bundle', $bundle['resourceType'] ?? null);
|
||||
$this->assertSame('document', $bundle['type'] ?? null);
|
||||
|
||||
$resourcesByType = [];
|
||||
$fullUrls = array_column($bundle['entry'], 'fullUrl');
|
||||
foreach ($bundle['entry'] as $entry) {
|
||||
$r = $entry['resource'] ?? [];
|
||||
$resourcesByType[(string) ($r['resourceType'] ?? '')][] = $r;
|
||||
}
|
||||
|
||||
// Must include Composition, Practitioner, Organization, Patient, Encounter, Immunization, ImmunizationRecommendation
|
||||
$this->assertArrayHasKey('Composition', $resourcesByType);
|
||||
$this->assertArrayHasKey('Practitioner', $resourcesByType);
|
||||
$this->assertArrayHasKey('Organization', $resourcesByType);
|
||||
$this->assertArrayHasKey('Patient', $resourcesByType);
|
||||
$this->assertArrayHasKey('Encounter', $resourcesByType);
|
||||
$this->assertArrayHasKey('Immunization', $resourcesByType);
|
||||
$this->assertArrayHasKey('ImmunizationRecommendation', $resourcesByType);
|
||||
|
||||
// Canonical NRCES entry order: Composition, Practitioner, Organization, Patient, Encounter, Immunization, ImmunizationRecommendation
|
||||
$actualTypes = array_map(fn($e) => $e['resource']['resourceType'], $bundle['entry']);
|
||||
$this->assertSame(['Composition', 'Practitioner', 'Organization', 'Patient', 'Encounter', 'Immunization', 'ImmunizationRecommendation'], $actualTypes);
|
||||
|
||||
$composition = $resourcesByType['Composition'][0];
|
||||
$practitioner = $resourcesByType['Practitioner'][0];
|
||||
$encounter = $resourcesByType['Encounter'][0];
|
||||
$immunization = $resourcesByType['Immunization'][0];
|
||||
$recommendation = $resourcesByType['ImmunizationRecommendation'][0];
|
||||
|
||||
$this->assertSame('Immunization record', $composition['title'] ?? null);
|
||||
$this->assertSame('DEVENDER SINGH', $composition['subject']['display'] ?? null);
|
||||
$this->assertNotEmpty($composition['encounter']['reference'] ?? '');
|
||||
$this->assertSame('Encounter', $composition['encounter']['display'] ?? null);
|
||||
$this->assertSame($composition['encounter']['reference'], $immunization['encounter']['reference'] ?? null);
|
||||
$this->assertArrayNotHasKey('location', $immunization);
|
||||
|
||||
// Encounter has ambulatory class (eliminates NA below hospital name in PHR app)
|
||||
$this->assertSame('AMB', $encounter['class']['code'] ?? null);
|
||||
$this->assertSame('ambulatory', $encounter['class']['display'] ?? null);
|
||||
$this->assertSame('finished', $encounter['status'] ?? null);
|
||||
|
||||
// Author MUST be Practitioner (eliminates NA in PHR app)
|
||||
$this->assertSame('Dr. R.K. Sundriyal', $practitioner['name'][0]['text'] ?? null);
|
||||
$this->assertSame('Dr. R.K. Sundriyal', $composition['author'][0]['display'] ?? null);
|
||||
$this->assertStringStartsWith('urn:uuid:', $composition['author'][0]['reference'] ?? '');
|
||||
|
||||
// Custodian is Organization
|
||||
$this->assertSame('E-Atria Hospital', $composition['custodian']['display'] ?? null);
|
||||
|
||||
// Organization resource has MustSupport address and telecom (eliminates NA below hospital name in PHR app)
|
||||
$organization = $resourcesByType['Organization'][0];
|
||||
$this->assertSame('E-Atria Hospital', $organization['name'] ?? null);
|
||||
$this->assertNotEmpty($organization['address'] ?? []);
|
||||
$this->assertSame('Avas Vikas, Kashipur -244713, Uttarakhand', $organization['address'][0]['text'] ?? null);
|
||||
$this->assertSame(['Avas Vikas'], $organization['address'][0]['line'] ?? []);
|
||||
$this->assertSame('Kashipur', $organization['address'][0]['city'] ?? null);
|
||||
$this->assertSame('Uttarakhand', $organization['address'][0]['state'] ?? null);
|
||||
$this->assertSame('244713', $organization['address'][0]['postalCode'] ?? null);
|
||||
$this->assertSame('90125 12505', $organization['telecom'][0]['value'] ?? null);
|
||||
|
||||
// Section entries contains both Immunization and ImmunizationRecommendation
|
||||
$this->assertCount(2, $composition['section'][0]['entry'] ?? []);
|
||||
$this->assertSame('Immunization', $composition['section'][0]['entry'][0]['type'] ?? null);
|
||||
$this->assertSame('ImmunizationRecommendation', $composition['section'][0]['entry'][1]['type'] ?? null);
|
||||
|
||||
// Immunization resource has site, route, target disease
|
||||
$this->assertSame('368209003', $immunization['site']['coding'][0]['code'] ?? null);
|
||||
$this->assertSame('372464004', $immunization['route']['coding'][0]['code'] ?? null);
|
||||
$this->assertSame('398102009', $immunization['protocolApplied'][0]['targetDisease'][0]['coding'][0]['code'] ?? null);
|
||||
$this->assertSame('10', $immunization['vaccineCode']['coding'][0]['code'] ?? null);
|
||||
|
||||
// Performer references Practitioner
|
||||
$this->assertSame('Dr. R.K. Sundriyal', $immunization['performer'][0]['actor']['display'] ?? null);
|
||||
|
||||
// ImmunizationRecommendation forecast status and dose
|
||||
$this->assertCount(2, $recommendation['recommendation']);
|
||||
$this->assertSame('due', $recommendation['recommendation'][0]['forecastStatus']['coding'][0]['code'] ?? null);
|
||||
$this->assertSame(2, $recommendation['recommendation'][0]['doseNumberPositiveInt'] ?? null);
|
||||
$this->assertNotEmpty($recommendation['recommendation'][0]['supportingImmunization'][0]['reference'] ?? '');
|
||||
|
||||
// Second recommendation item is Pentavalent with CVX 198
|
||||
$this->assertSame('198', $recommendation['recommendation'][1]['vaccineCode'][0]['coding'][0]['code'] ?? null);
|
||||
$this->assertSame('Pentavalent', $recommendation['recommendation'][1]['vaccineCode'][0]['coding'][0]['display'] ?? null);
|
||||
$this->assertSame(1, $recommendation['recommendation'][1]['doseNumberPositiveInt'] ?? null);
|
||||
|
||||
// Verify all references resolve
|
||||
$encoded = json_encode($bundle, JSON_UNESCAPED_SLASHES);
|
||||
preg_match_all('/"reference":"(urn:uuid:[^"]+)"/', (string) $encoded, $matches);
|
||||
foreach ($matches[1] as $ref) {
|
||||
$this->assertContains($ref, $fullUrls, 'Unresolved reference: ' . $ref);
|
||||
}
|
||||
}
|
||||
|
||||
public function testLegacyNumericGenderMapsToFhirGender(): void
|
||||
{
|
||||
$controller = new AbdmGateway();
|
||||
|
|
@ -694,7 +874,7 @@ final class FhirGeneratorsTest extends CIUnitTestCase
|
|||
['loinc_code' => '8480-6', 'display' => 'Systolic blood pressure', 'value' => 120, 'unit' => 'mmHg', 'ucum_code' => 'mm[Hg]'],
|
||||
['loinc_code' => '8462-4', 'display' => 'Diastolic blood pressure', 'value' => 80, 'unit' => 'mmHg', 'ucum_code' => 'mm[Hg]'],
|
||||
['loinc_code' => '8867-4', 'display' => 'Heart rate', 'value' => 72, 'unit' => '/min', 'ucum_code' => '/min'],
|
||||
['loinc_code' => '8310-5', 'display' => 'Body temperature', 'value' => 36.8, 'unit' => 'Cel', 'ucum_code' => 'Cel'],
|
||||
['loinc_code' => '8310-5', 'display' => 'Body temperature', 'value' => 36.666666666666664, 'unit' => 'Cel', 'ucum_code' => 'Cel'],
|
||||
['loinc_code' => '59408-5', 'display' => 'Oxygen saturation', 'value' => 98, 'unit' => '%', 'ucum_code' => '%'],
|
||||
],
|
||||
'physical_examination' => [
|
||||
|
|
@ -742,6 +922,13 @@ final class FhirGeneratorsTest extends CIUnitTestCase
|
|||
|
||||
$this->assertGreaterThanOrEqual(1, count($composition['section']));
|
||||
$this->assertNotEmpty($composition['section'][0]['entry']);
|
||||
|
||||
$tempObsEntries = array_values(array_filter($bundle['entry'], static function ($e) {
|
||||
$r = $e['resource'] ?? [];
|
||||
return ($r['resourceType'] ?? '') === 'Observation' && (($r['code']['coding'][0]['code'] ?? '') === '8310-5');
|
||||
}));
|
||||
$this->assertNotEmpty($tempObsEntries);
|
||||
$this->assertSame(36.7, $tempObsEntries[0]['resource']['valueQuantity']['value']);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -937,3 +1124,4 @@ final class FhirGeneratorsTest extends CIUnitTestCase
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user