From 3072639cd50f9caa25d3bdf6cf4afbdbb9d99595 Mon Sep 17 00:00:00 2001 From: DevSoftTech Server Date: Tue, 30 Jun 2026 18:11:34 +0530 Subject: [PATCH] Revert "ABDM: fix local submit fallback, HFR runtime usage, status sync, and link-token retry" This reverts commit e08c18f5b2a99508b8c98773ce1673228cd49dc5. --- app/Controllers/AbdmGateway.php | 71 +-- app/Controllers/AbdmTaskBoard.php | 2 +- app/Controllers/Opd_prescription.php | 594 +------------------ app/Libraries/Abdm/EAtriaBridgeConnector.php | 37 +- app/Libraries/FhirR4Builder.php | 185 +----- 5 files changed, 39 insertions(+), 850 deletions(-) diff --git a/app/Controllers/AbdmGateway.php b/app/Controllers/AbdmGateway.php index c00ed18..b948f5f 100644 --- a/app/Controllers/AbdmGateway.php +++ b/app/Controllers/AbdmGateway.php @@ -983,11 +983,10 @@ 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')); - $pushToGatewayRequested = (int) $this->request->getPost('push_to_gateway') === 1; - $pushToGateway = $pushToGatewayRequested || $this->isGatewayPushForcedByDeployment(); + $pushToGateway = (int) $this->request->getPost('push_to_gateway') === 1; $now = Time::now('Asia/Kolkata')->toDateTimeString(); - $logBridge = function (string $status, array $responsePayload, string $errorMessage = '', string $eventType = 'abdm.opd.prescription.share') use ($opdId, $patientId, $now, $pushToGatewayRequested, $pushToGateway): void { + $logBridge = function (string $status, array $responsePayload, string $errorMessage = '', string $eventType = 'abdm.opd.prescription.share') use ($opdId, $patientId, $now, $pushToGateway): void { if (! $this->db->tableExists('abdm_api_logs')) { return; } @@ -1002,7 +1001,6 @@ class AbdmGateway extends BaseController 'request_json' => (string) json_encode([ 'opd_id' => $opdId, 'patient_id' => $patientId, - 'push_to_gateway_requested' => $pushToGatewayRequested ? 1 : 0, 'push_to_gateway' => $pushToGateway ? 1 : 0, ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 'response_code' => 200, @@ -2966,24 +2964,6 @@ class AbdmGateway extends BaseController try { $result = $this->connector->linkAndShare($bridgeId); - - if ($this->shouldRetryBridgeLinkAndShareForTokenHipMismatch($result) - && method_exists($this->connector, 'linkAndShareWithFreshToken')) { - /** @var callable $retryCallable */ - $retryCallable = [$this->connector, 'linkAndShareWithFreshToken']; - $retry = $retryCallable($bridgeId); - if (is_array($retry)) { - $retry['retry_mode'] = 'fresh_link_token'; - if ((int) ($retry['ok'] ?? 0) === 1) { - return $this->response->setJSON($retry); - } - // Prefer clearer guidance if retry still fails. - if (! isset($retry['message']) && isset($retry['error_text'])) { - $retry['message'] = (string) $retry['error_text']; - } - $result = $retry; - } - } } catch (\Throwable $e) { // Backward compatibility with older connectors. $result = $this->connector->triggerShare($bridgeId); @@ -2991,35 +2971,6 @@ class AbdmGateway extends BaseController return $this->response->setJSON($result); } - /** - * @param array $result - */ - private function shouldRetryBridgeLinkAndShareForTokenHipMismatch(array $result): bool - { - $httpCode = (int) ($result['http_code'] ?? 0); - $msg = strtolower(trim((string) ( - $result['message'] - ?? $result['error_text'] - ?? $result['error'] - ?? '' - ))); - - if ($httpCode !== 409 || $msg === '') { - return false; - } - - $isLinkTokenIssue = str_contains($msg, 'link token') - || str_contains($msg, 'link-token') - || str_contains($msg, 'link_and_share_link_token') - || str_contains($msg, 'stored token'); - - $isHipMismatch = str_contains($msg, 'hip id') - || str_contains($msg, 'hfr_id') - || str_contains($msg, 'belongs to hip'); - - return $isLinkTokenIssue && $isHipMismatch; - } - // ========================================================================= // Explicit e-Atria orchestration aliases for HMS integration docs. // ========================================================================= @@ -3711,24 +3662,6 @@ class AbdmGateway extends BaseController return ''; } - private function isGatewayPushForcedByDeployment(): bool - { - foreach (['ABDM_FORCE_GATEWAY_PUSH', 'ABDM_HMS_BEHIND_NAT', 'ABDM_GATEWAY_HOSTED_DISCOVERY'] as $key) { - $raw = strtolower(trim((string) $this->readRuntimeSetting($key))); - if ($raw === '') { - continue; - } - if (in_array($raw, ['1', 'true', 'yes', 'on', 'y'], true)) { - return true; - } - if (in_array($raw, ['0', 'false', 'no', 'off', 'n'], true)) { - return false; - } - } - - return false; - } - private function isStaleConsentTransition(string $currentStatus, string $incomingStatus): bool { if ($incomingStatus === '' || $currentStatus === '') { diff --git a/app/Controllers/AbdmTaskBoard.php b/app/Controllers/AbdmTaskBoard.php index f97655a..fbdce1e 100644 --- a/app/Controllers/AbdmTaskBoard.php +++ b/app/Controllers/AbdmTaskBoard.php @@ -640,7 +640,7 @@ class AbdmTaskBoard extends BaseController $statusTone = 'success'; $statusNote = 'Care context link confirmed by callback.'; } elseif ($linkStatus === 'pending_discovery' && $statusLabel === 'Discovery Ready') { - $statusNote = 'Waiting for discovery/fetch workflow completion (callback or bridge sync).'; + $statusNote = 'Waiting for ABDM discovery and consent fetch callbacks.'; } elseif ($linkStatus === 'failed') { $statusLabel = 'Link Failed'; $statusTone = 'danger'; diff --git a/app/Controllers/Opd_prescription.php b/app/Controllers/Opd_prescription.php index 701fdca..ef2f9f5 100644 --- a/app/Controllers/Opd_prescription.php +++ b/app/Controllers/Opd_prescription.php @@ -3,7 +3,6 @@ namespace App\Controllers; use App\Controllers\BaseController; -use App\Libraries\Abdm\AbdmConnectorFactory; use App\Libraries\AbdmWorkTaskService; use App\Libraries\BridgeSyncService; use App\Libraries\CsnotkTerminologyService; @@ -4724,7 +4723,8 @@ class Opd_prescription extends BaseController try { // Rebuild bundle using current builder - $abhaIdentity = $this->resolvePatientAbhaIdentifiersForFhir($patientRow); + $abhaField = $this->resolvePatientAbhaField(); + $abhaAddress = $abhaField !== null ? trim((string) ($patientRow[$abhaField] ?? '')) : ''; $patFirstName = $this->sanitizePersonNamePart((string) ($patientRow['p_fname'] ?? '')); $patLastName = $this->sanitizePersonNamePart((string) ($patientRow['p_lname'] ?? '')); @@ -4744,8 +4744,7 @@ class Opd_prescription extends BaseController 'family_name' => $patLastName, 'gender' => $this->normalizeGender((string) ($patientRow['gender'] ?? '')), 'birthDate' => ! empty($patientRow['dob']) ? date('Y-m-d', strtotime((string) $patientRow['dob'])) : '', - 'abhaAddress' => $abhaIdentity['address'], - 'abhaId' => $abhaIdentity['id'], + 'abhaAddress' => $abhaAddress, 'phone' => $patPhone, ]; @@ -4861,56 +4860,6 @@ class Opd_prescription extends BaseController // AJAX callers (modal preview) get JSON directly if ($this->request->isAJAX()) { - $entryList = is_array($bundle['entry'] ?? null) ? $bundle['entry'] : []; - $firstEntryResourceType = (string) ($entryList[0]['resource']['resourceType'] ?? ''); - $compositionFirst = $firstEntryResourceType === 'Composition'; - $patientHasIdentifier = false; - $structuredMedicationRequests = 0; - $totalMedicationRequests = 0; - - foreach ($entryList as $entry) { - $resource = is_array($entry['resource'] ?? null) ? $entry['resource'] : []; - $rt = (string) ($resource['resourceType'] ?? ''); - - if ($rt === 'Patient') { - $identifiers = is_array($resource['identifier'] ?? null) ? $resource['identifier'] : []; - $patientHasIdentifier = ! empty($identifiers); - } - - if ($rt === 'MedicationRequest') { - $totalMedicationRequests++; - $coding = (array) ($resource['medicationCodeableConcept']['coding'] ?? []); - if (! empty($coding)) { - $structuredMedicationRequests++; - } - } - } - - $identityDebug = [ - 'patient_id' => 0, - 'local_identifier_system' => 'https://hms.local/patient-id', - 'local_identifier_value' => '', - 'abha_id' => '', - 'abha_address' => '', - 'has_abha_id' => false, - 'has_abha_address' => false, - ]; - - if ($this->db->tableExists('opd_master')) { - $opdRow = $this->db->table('opd_master')->select('p_id')->where('opd_id', (int) $opdId)->get(1)->getRowArray() ?? []; - $patientId = (int) ($opdRow['p_id'] ?? 0); - if ($patientId > 0 && $this->db->tableExists('patient_master')) { - $patientRow = $this->db->table('patient_master')->where('id', $patientId)->get(1)->getRowArray() ?? []; - $abhaIdentity = $this->resolvePatientAbhaIdentifiersForFhir($patientRow); - $identityDebug['patient_id'] = $patientId; - $identityDebug['local_identifier_value'] = (string) $patientId; - $identityDebug['abha_id'] = (string) ($abhaIdentity['id'] ?? ''); - $identityDebug['abha_address'] = (string) ($abhaIdentity['address'] ?? ''); - $identityDebug['has_abha_id'] = trim((string) ($abhaIdentity['id'] ?? '')) !== ''; - $identityDebug['has_abha_address'] = trim((string) ($abhaIdentity['address'] ?? '')) !== ''; - } - } - // Fetch live complaint and diagnosis terms from current prescription row $liveComplaints = []; $liveDiagnoses = []; @@ -4955,13 +4904,6 @@ class Opd_prescription extends BaseController 'bundle_type' => $bundleType, 'opd_id' => (int) $opdId, 'opd_session_id' => (int) $sessionId, - 'bundle_checks' => [ - 'composition_first' => $compositionFirst, - 'patient_has_identifier' => $patientHasIdentifier, - 'medicationrequest_total' => $totalMedicationRequests, - 'medicationrequest_structured' => $structuredMedicationRequests, - ], - 'identity_debug' => $identityDebug, 'live_complaints' => $liveComplaints, 'live_diagnoses' => $liveDiagnoses, ]); @@ -5146,13 +5088,10 @@ class Opd_prescription extends BaseController . '
Bundle type is "document"
' . '
First entry is Composition
' . '
Patient resource present
' - . '
Patient has at least one identifier
' . '
At least one clinical resource
' - . '
MedicationRequest entries are coded (if present)
' . '
Visit date valid
' . '
Bridge connection reachable
' . '' - . '
' . '' @@ -5199,8 +5138,6 @@ class Opd_prescription extends BaseController . ' const btn=this;' . ' btn.disabled=true;btn.innerHTML=\'Verifying\u2026\';' . ' const checks=document.querySelectorAll("#checkList .check-item");' - . ' const strictIssuesEl=document.getElementById("strictIssues");' - . ' strictIssuesEl.innerHTML="";' . ' function setCheck(i,pass,msg){' . ' checks[i].className="check-item "+(pass?"pass":"fail");' . ' checks[i].innerHTML=(pass?\'\':\'\')+\'\'+msg+\'\';' @@ -5223,27 +5160,16 @@ class Opd_prescription extends BaseController . ' const hasPatient=(BUNDLE.entry||[]).some(e=>e.resource&&e.resource.resourceType==="Patient");' . ' setCheck(3,hasPatient,hasPatient?"Patient resource present":"Patient resource missing from bundle");' - // Check 5: Patient identifier present - . ' const patientResource=(BUNDLE.entry||[]).map(e=>e&&e.resource?e.resource:null).find(r=>r&&r.resourceType==="Patient")||null;' - . ' const patientHasIdentifier=!!(patientResource&&Array.isArray(patientResource.identifier)&&patientResource.identifier.length>0);' - . ' setCheck(4,patientHasIdentifier,patientHasIdentifier?"Patient identifier present":"Patient identifier missing");' - - // Check 6: Clinical resource + // Check 5: Clinical resource . ' const clinTypes=["MedicationRequest","Condition","Observation","AllergyIntolerance","DiagnosticReport"];' . ' const clinCount=(BUNDLE.entry||[]).filter(e=>e.resource&&clinTypes.includes(e.resource.resourceType)).length;' - . ' setCheck(5,clinCount>0,"Clinical resources: "+clinCount+(clinCount>0?" present":" — bundle has no medications/conditions/observations"));' + . ' setCheck(4,clinCount>0,"Clinical resources: "+clinCount+(clinCount>0?" present":" — bundle has no medications/conditions/observations"));' - // Check 7: Structured MedicationRequest coding - . ' const medReqs=(BUNDLE.entry||[]).filter(e=>e.resource&&e.resource.resourceType==="MedicationRequest").map(e=>e.resource);' - . ' const medReqStructured=medReqs.filter(m=>Array.isArray(((m.medicationCodeableConcept||{}).coding||[]))&&((m.medicationCodeableConcept||{}).coding||[]).length>0).length;' - . ' const medStructuredOk=medReqs.length===0||medReqStructured>0;' - . ' setCheck(6,medStructuredOk,medReqs.length===0?"No MedicationRequest entries in bundle":"Structured MedicationRequest: "+medReqStructured+"/"+medReqs.length);' - - // Check 8: visit date + // Check 6: visit date . ' const dateOk=CTX.visitDate&&/^\d{4}-\d{2}-\d{2}$/.test(CTX.visitDate);' - . ' setCheck(7,dateOk,dateOk?"Visit date: "+CTX.visitDate:"Visit date missing or invalid");' + . ' setCheck(5,dateOk,dateOk?"Visit date: "+CTX.visitDate:"Visit date missing or invalid");' - // Check 9: Bridge connectivity (AJAX) + // Check 7: Bridge connectivity (AJAX) . ' document.getElementById("chkBridge").className="check-item pending";' . ' document.getElementById("chkBridge").innerHTML=\'Checking bridge\u2026\';' . ' let bridgeOk=false;' @@ -5252,11 +5178,11 @@ class Opd_prescription extends BaseController . ' const j=await r.json();' . ' bridgeOk=r.ok&&(j.ok===1||j.status==="ok");' . ' const info=bridgeOk?(j.authenticated_as?(" ("+j.authenticated_as+")"):""):" — "+( j.error_text||j.message||"unreachable");' - . ' setCheck(8,bridgeOk,bridgeOk?"Bridge connected"+info:"Bridge unreachable"+info);' - . ' }catch(e){setCheck(8,false,"Bridge check failed: "+e.message);}' + . ' setCheck(6,bridgeOk,bridgeOk?"Bridge connected"+info:"Bridge unreachable"+info);' + . ' }catch(e){setCheck(6,false,"Bridge check failed: "+e.message);}' // Evaluate overall result - . ' const allPass=hasAbha&&isDoc&&hasPatient&&patientHasIdentifier&&clinCount>0&&medStructuredOk&&dateOk&&bridgeOk;' + . ' const allPass=hasAbha&&isDoc&&isComp&&hasPatient&&clinCount>0&&dateOk&&bridgeOk;' . ' btn.disabled=false;' . ' if(allPass){' . ' btn.className="btn btn-outline-success w-100 mt-3";' @@ -5267,10 +5193,6 @@ class Opd_prescription extends BaseController . ' btn.className="btn btn-warning w-100 mt-3";' . ' btn.innerHTML=\'Issues Found — Fix & Re-verify\';' . ' document.getElementById("submitCard").style.display="none";' - . ' const issues=[];' - . ' if(!patientHasIdentifier) issues.push("Patient identifier missing");' - . ' if(!medStructuredOk) issues.push("MedicationRequest entries are not coded");' - . ' if(issues.length){strictIssuesEl.innerHTML=\'
Strict checks: \'+issues.join("; ")+\'
\';}' . ' }' . '});' @@ -5302,11 +5224,7 @@ class Opd_prescription extends BaseController . ' el.innerHTML=\'
\u2705 \'+(res.message||"Submitted successfully.")+qPart+idPart+\'
\';' . ' }else{' . ' btn.disabled=false;btn.innerHTML=origHtml;' - . ' let strictText="";' - . ' if(res.bundle_checks){' - . ' strictText="
Server strict checks failed. Please run Verify Data and fix highlighted items.
";' - . ' }' - . ' el.innerHTML=\'
\u274c \'+(res.message||res.error_text||"Submission failed.")+(res.http_code?" (HTTP "+res.http_code+")":"")+strictText+\'
\';' + . ' el.innerHTML=\'
\u274c \'+(res.message||res.error_text||"Submission failed.")+(res.http_code?" (HTTP "+res.http_code+")":"")+\'
\';' . ' }' . ' }).catch(e=>{' . ' clearTimeout(submitTimer);' @@ -5387,55 +5305,6 @@ class Opd_prescription extends BaseController ]); } - // Strict pre-submit validation to prevent avoidable ABDM rejections. - $entryList = is_array($bundle['entry'] ?? null) ? $bundle['entry'] : []; - $firstEntryResourceType = (string) ($entryList[0]['resource']['resourceType'] ?? ''); - $compositionFirst = $firstEntryResourceType === 'Composition'; - $patientHasIdentifier = false; - $totalMedicationRequests = 0; - $structuredMedicationRequests = 0; - - foreach ($entryList as $entry) { - $resource = is_array($entry['resource'] ?? null) ? $entry['resource'] : []; - $rt = (string) ($resource['resourceType'] ?? ''); - - if ($rt === 'Patient') { - $identifiers = is_array($resource['identifier'] ?? null) ? $resource['identifier'] : []; - $patientHasIdentifier = ! empty($identifiers); - } - - if ($rt === 'MedicationRequest') { - $totalMedicationRequests++; - $coding = (array) ($resource['medicationCodeableConcept']['coding'] ?? []); - if (! empty($coding)) { - $structuredMedicationRequests++; - } - } - } - - $strictErrors = []; - if (! $patientHasIdentifier) { - $strictErrors[] = 'FHIR validation failed: Patient.identifier is missing.'; - } - if ($totalMedicationRequests > 0 && $structuredMedicationRequests === 0) { - $strictErrors[] = 'FHIR validation failed: MedicationRequest resources are present but none contain coded medicationCodeableConcept.'; - } - - if (! empty($strictErrors)) { - return $this->response->setJSON([ - 'ok' => 0, - 'error_text' => implode(' ', $strictErrors), - 'bundle_checks' => [ - 'composition_first' => $compositionFirst, - 'patient_has_identifier' => $patientHasIdentifier, - 'medicationrequest_total' => $totalMedicationRequests, - 'medicationrequest_structured' => $structuredMedicationRequests, - ], - 'csrfName' => csrf_token(), - 'csrfHash' => csrf_hash(), - ]); - } - // Gather patient / OPD context for the push payload $patientId = 0; $patientName = ''; @@ -5497,31 +5366,6 @@ class Opd_prescription extends BaseController $cookieHeader = (string) $this->request->getHeaderLine('Cookie'); $handoffTimeoutSec = $this->resolveFhirGatewayHandoffTimeoutSec(); - // In PHP built-in server mode, posting back into the same app can deadlock - // because only one request is processed at a time. - if ($this->isLocalSingleThreadHandoffRisk($gatewayUrl)) { - $directResult = $this->submitFhirBundleDirectBridgePush([ - 'opd_id' => $opdId, - 'opd_session_id' => $sessionId, - 'patient_id' => $patientId, - 'patient_name' => $patientName, - 'abha_id' => $abhaId, - 'bundle' => $bundle, - 'bundle_row' => $bundleRow, - 'visit_date' => $visitDate, - ]); - - $directResult['message'] = (string) ($directResult['message'] ?? ($directResult['ok'] ?? 0 ? 'Direct bridge push completed.' : 'Direct bridge push failed.')); - $directResult['local_direct_push'] = 1; - $directResult['csrfName'] = csrf_token(); - $directResult['csrfHash'] = csrf_hash(); - - return $this->response->setJSON([ - ...$directResult, - 'message' => '[Local fallback] ' . $directResult['message'], - ]); - } - // Avoid self-call deadlock: this request forwards the same session cookie // to an internal endpoint protected by permission filter. // Releasing session lock allows the internal request to proceed. @@ -5640,8 +5484,7 @@ class Opd_prescription extends BaseController private function resolveFhirGatewayHandoffTimeoutSec(): int { - // Keep below PHP max_execution_time to avoid shutdown fatals that return HTML instead of JSON. - $timeout = 60; + $timeout = 120; if ($this->db->tableExists('hospital_setting')) { $row = $this->db->table('hospital_setting') @@ -5657,332 +5500,16 @@ class Opd_prescription extends BaseController } } - if ($timeout < 15) { - $timeout = 15; + if ($timeout < 30) { + $timeout = 30; } - if ($timeout > 90) { - $timeout = 90; + if ($timeout > 240) { + $timeout = 240; } return $timeout; } - private function isLocalSingleThreadHandoffRisk(string $gatewayUrl): bool - { - if (PHP_SAPI !== 'cli-server') { - return false; - } - - $targetHost = strtolower((string) (parse_url($gatewayUrl, PHP_URL_HOST) ?? '')); - $targetPort = (int) (parse_url($gatewayUrl, PHP_URL_PORT) ?? 0); - $requestHostHeader = strtolower(trim((string) $this->request->getHeaderLine('Host'))); - $requestHost = $requestHostHeader; - $requestPort = 0; - - if (str_contains($requestHostHeader, ':')) { - [$requestHost, $portPart] = explode(':', $requestHostHeader, 2); - $requestPort = (int) $portPart; - } - - if ($requestPort <= 0) { - $requestPort = (int) ($this->request->getServer('SERVER_PORT') ?? 0); - } - - if ($targetHost === '' || $requestHost === '') { - return false; - } - - if ($targetPort <= 0) { - $targetPort = $requestPort; - } - - return $targetHost === strtolower($requestHost) && $targetPort === $requestPort; - } - - private function resolveLatestBridgeShareErrorForOpd(int $opdId): string - { - if ($opdId <= 0 || ! $this->db->tableExists('abdm_api_logs')) { - return ''; - } - - try { - $row = $this->db->table('abdm_api_logs') - ->select('error_message,response_json,status,created_at') - ->where('channel', 'bridge') - ->where('event_type', 'abdm.opd.prescription.share.result') - ->where('entity_type', 'opd') - ->where('entity_id', (string) $opdId) - ->orderBy('id', 'DESC') - ->get(1) - ->getRowArray(); - - if (empty($row)) { - return ''; - } - - $createdAtRaw = trim((string) ($row['created_at'] ?? '')); - if ($createdAtRaw !== '') { - $createdTs = strtotime($createdAtRaw); - if ($createdTs !== false && (time() - $createdTs) > (20 * 60)) { - // Do not surface stale bridge errors in submit alerts. - return ''; - } - } - - $error = trim((string) ($row['error_message'] ?? '')); - if ($error !== '') { - return $error; - } - - $response = json_decode((string) ($row['response_json'] ?? ''), true); - if (is_array($response)) { - $message = trim((string) ($response['message'] ?? $response['error_text'] ?? $response['error'] ?? '')); - if ($message !== '') { - return $message; - } - } - } catch (\Throwable) { - return ''; - } - - return ''; - } - - /** - * @param array $context - * @return array - */ - private function submitFhirBundleDirectBridgePush(array $context): array - { - $opdId = (int) ($context['opd_id'] ?? 0); - $sessionId = (int) ($context['opd_session_id'] ?? 0); - $patientId = (int) ($context['patient_id'] ?? 0); - $patientName = trim((string) ($context['patient_name'] ?? '')); - $abhaRaw = trim((string) ($context['abha_id'] ?? '')); - $bundle = $context['bundle'] ?? []; - $bundleRow = is_array($context['bundle_row'] ?? null) ? $context['bundle_row'] : []; - $visitDate = trim((string) ($context['visit_date'] ?? date('Y-m-d'))); - - if ($opdId <= 0 || $patientId <= 0 || ! is_array($bundle)) { - return [ - 'ok' => 0, - 'message' => 'Local fallback failed: invalid OPD/patient/bundle context.', - 'http_code' => 0, - ]; - } - - $abhaAddress = $this->isValidAbhaAddress($abhaRaw) ? $abhaRaw : ''; - $abhaDigits = preg_replace('/\D/', '', $abhaRaw); - $abhaNumber = (is_string($abhaDigits) && strlen($abhaDigits) === 14) ? $abhaDigits : ''; - - if ($abhaAddress === '' && $abhaNumber === '') { - return [ - 'ok' => 0, - 'message' => 'ABHA address or ABHA number is required for bridge push.', - 'http_code' => 0, - ]; - } - - $sessionForRef = $sessionId > 0 ? $sessionId : (int) ($bundleRow['opd_session_id'] ?? 0); - $careContextRef = 'OPD-' . $opdId . '-S' . ($sessionForRef > 0 ? $sessionForRef : 0) . '-' . $visitDate; - $careContextDisplay = 'OPD Visit - ' . $visitDate; - $bundleType = trim((string) ($bundleRow['bundle_type'] ?? 'OPConsultRecord')); - $hiType = match ($bundleType) { - 'MedicationRequestBundle', 'Prescription', 'PrescriptionRecord' => 'PrescriptionRecord', - default => 'OPConsultRecord', - }; - - if ($patientName === '') { - $patientName = 'PATIENT-' . $patientId; - } - - try { - $connector = AbdmConnectorFactory::make(); - $result = $connector->pushRecord([ - 'patient_id' => (string) $patientId, - 'patient_name' => $patientName, - 'abha_id' => $abhaNumber, - 'abha_address' => $abhaAddress, - 'hi_type' => $hiType, - 'record_type' => $hiType, - 'visit_date' => $visitDate, - 'care_context_reference' => $careContextRef, - 'care_context_display' => $careContextDisplay, - 'notes' => $careContextDisplay, - 'queue_id' => $careContextRef, - 'record_data' => $bundle, - ]); - - $ok = (int) ($result['ok'] ?? 0) === 1 ? 1 : 0; - $queueId = (string) ($result['queue_id'] ?? ''); - $bridgeId = (int) ($result['id'] ?? $result['bridge_record_id'] ?? 0); - $httpCode = (int) ($result['http_code'] ?? 200); - $message = trim((string) ($result['message'] ?? $result['error_text'] ?? $result['error'] ?? '')); - if ($message === '') { - $message = $ok === 1 ? 'Record pushed to bridge successfully.' : 'Bridge push failed.'; - } - - $this->persistDirectFallbackSubmissionState( - $opdId, - $patientId, - $abhaAddress !== '' ? $abhaAddress : $abhaNumber, - $careContextRef, - $queueId, - $bridgeId, - $ok === 1, - [ - 'http_code' => $httpCode, - 'message' => $message, - 'raw' => $result, - ] - ); - - return [ - 'ok' => $ok, - 'queue_id' => $queueId !== '' ? $queueId : null, - 'bridge_id' => $bridgeId > 0 ? $bridgeId : null, - 'http_code' => $httpCode, - 'message' => $message, - ]; - } catch (\Throwable $e) { - return [ - 'ok' => 0, - 'message' => 'Direct bridge push failed: ' . $e->getMessage(), - 'http_code' => 0, - ]; - } - } - - /** - * @param array $responsePayload - */ - private function persistDirectFallbackSubmissionState( - int $opdId, - int $patientId, - string $abhaId, - string $careContextRef, - string $queueId, - int $bridgeId, - bool $ok, - array $responsePayload - ): void { - try { - $now = Time::now('Asia/Kolkata')->toDateTimeString(); - - if ($this->db->tableExists('health_records')) { - $fields = $this->db->getFieldNames('health_records') ?? []; - $builder = $this->db->table('health_records')->select('id'); - - if (in_array('entity_type', $fields, true) && in_array('entity_id', $fields, true)) { - $builder->where('entity_type', 'opd')->where('entity_id', (string) $opdId); - } - if ($careContextRef !== '' && in_array('care_context_reference', $fields, true)) { - $builder->where('care_context_reference', $careContextRef); - } - - $hrRow = $builder->orderBy('id', 'DESC')->get(1)->getRowArray(); - $hrId = (int) ($hrRow['id'] ?? 0); - - if ($hrId > 0) { - $update = []; - if (in_array('push_status', $fields, true)) { - $update['push_status'] = $ok ? 'queued' : 'failed'; - } - if (in_array('abdm_txn_id', $fields, true) && $queueId !== '') { - $update['abdm_txn_id'] = $queueId; - } - if (in_array('bridge_record_id', $fields, true) && $bridgeId > 0) { - $update['bridge_record_id'] = $bridgeId; - } - if (in_array('care_context_reference', $fields, true) && $careContextRef !== '') { - $update['care_context_reference'] = $careContextRef; - } - if (in_array('updated_at', $fields, true)) { - $update['updated_at'] = $now; - } - - if (! empty($update)) { - $this->db->table('health_records')->where('id', $hrId)->update($update); - } - } - } - - if ($this->db->tableExists('record_links')) { - $fields = $this->db->getFieldNames('record_links') ?? []; - $existingBuilder = $this->db->table('record_links')->select('id'); - - if ($careContextRef !== '' && in_array('care_context_reference', $fields, true)) { - $existingBuilder->where('care_context_reference', $careContextRef); - } - if ($abhaId !== '' && in_array('abha_id', $fields, true)) { - $existingBuilder->where('abha_id', $abhaId); - } - - $existing = $existingBuilder->orderBy('id', 'DESC')->get(1)->getRowArray(); - $linkUpdate = []; - - if (in_array('abha_id', $fields, true) && $abhaId !== '') { - $linkUpdate['abha_id'] = $abhaId; - } - if (in_array('abdm_txn_id', $fields, true) && $queueId !== '') { - $linkUpdate['abdm_txn_id'] = $queueId; - } - if (in_array('care_context_reference', $fields, true) && $careContextRef !== '') { - $linkUpdate['care_context_reference'] = $careContextRef; - } - if (in_array('link_status', $fields, true)) { - $linkUpdate['link_status'] = $ok ? 'pending' : 'failed'; - } - if (in_array('response_json', $fields, true)) { - $linkUpdate['response_json'] = (string) json_encode($responsePayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - } - if (in_array('updated_at', $fields, true)) { - $linkUpdate['updated_at'] = $now; - } - if ($ok && in_array('linked_at', $fields, true)) { - $linkUpdate['linked_at'] = null; - } - - if (! empty($existing)) { - if (! empty($linkUpdate)) { - $this->db->table('record_links')->where('id', (int) ($existing['id'] ?? 0))->update($linkUpdate); - } - } else { - if (in_array('created_at', $fields, true)) { - $linkUpdate['created_at'] = $now; - } - if (! empty($linkUpdate)) { - $this->db->table('record_links')->insert($linkUpdate); - } - } - } - - if ($this->db->tableExists('abdm_api_logs')) { - $this->db->table('abdm_api_logs')->insert([ - 'channel' => 'bridge', - 'event_type' => 'abdm.opd.prescription.share.result', - 'endpoint' => '/Opd_prescription/fhir_bundle_submit(local_fallback)', - 'http_method' => 'POST', - 'entity_type' => 'opd', - 'entity_id' => (string) $opdId, - 'request_json' => (string) json_encode([ - 'opd_id' => $opdId, - 'patient_id' => $patientId, - 'care_context_reference' => $careContextRef, - 'mode' => 'local_direct_push', - ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), - 'response_code' => (int) ($responsePayload['http_code'] ?? 200), - 'response_json' => (string) json_encode($responsePayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), - 'status' => $ok ? 'success' : 'error', - 'error_message' => $ok ? null : mb_substr((string) ($responsePayload['message'] ?? 'bridge push failed'), 0, 1000), - 'created_at' => $now, - ]); - } - } catch (\Throwable $e) { - log_message('warning', '[fhir_bundle_submit] local fallback state sync failed for OPD#' . $opdId . ': ' . $e->getMessage()); - } - } - private function isTransientFhirGatewayHandoffError(\Throwable $e): bool { $msg = strtolower(trim($e->getMessage())); @@ -10681,81 +10208,6 @@ class Opd_prescription extends BaseController return null; } - /** - * Resolve best-available ABHA value for FHIR Patient.identifier. - * - * Preference order: - * 1) Valid ABHA Address from abha_address. - * 2) Valid ABHA Address stored in legacy ABHA columns. - * 3) 14-digit ABHA number from any ABHA column. - * 4) First non-empty ABHA-like value as last fallback. - * - * @param array $patientRow - */ - private function resolvePatientAbhaForFhir(array $patientRow): string - { - $resolved = $this->resolvePatientAbhaIdentifiersForFhir($patientRow); - if ($resolved['address'] !== '') { - return $resolved['address']; - } - if ($resolved['id'] !== '') { - return $resolved['id']; - } - - return ''; - } - - /** - * Resolve ABHA identity values separately for FHIR identifiers. - * - * @param array $patientRow - * @return array{id:string,address:string} - */ - private function resolvePatientAbhaIdentifiersForFhir(array $patientRow): array - { - $fields = $this->db->tableExists('patient_master') - ? ($this->db->getFieldNames('patient_master') ?? []) - : []; - - $address = ''; - $abhaId = ''; - - if (in_array('abha_address', $fields, true)) { - $candidate = trim((string) ($patientRow['abha_address'] ?? '')); - if ($candidate !== '' && $this->isValidAbhaAddress($candidate)) { - $address = $candidate; - } - } - - foreach (['abha_id', 'abha_no', 'abha', 'abha_number'] as $field) { - if (! in_array($field, $fields, true)) { - continue; - } - $value = trim((string) ($patientRow[$field] ?? '')); - if ($value === '') { - continue; - } - - if ($address === '' && $this->isValidAbhaAddress($value)) { - $address = $value; - } - - $digits = preg_replace('/\D/', '', $value); - if ($abhaId === '' && is_string($digits) && strlen($digits) === 14) { - $abhaId = $digits; - } - } - - if ($abhaId === '' && in_array('abha_address', $fields, true)) { - $digits = preg_replace('/\D/', '', trim((string) ($patientRow['abha_address'] ?? ''))); - if (is_string($digits) && strlen($digits) === 14) { - $abhaId = $digits; - } - } - - return ['id' => $abhaId, 'address' => $address]; - } - /** * @return array */ @@ -11397,7 +10849,8 @@ class Opd_prescription extends BaseController return false; } - $abhaIdentity = $this->resolvePatientAbhaIdentifiersForFhir($patientRow); + $abhaField = $this->resolvePatientAbhaField(); + $abhaAddress = $abhaField !== null ? trim((string) ($patientRow[$abhaField] ?? '')) : ''; $patFirstName = $this->sanitizePersonNamePart((string) ($patientRow['p_fname'] ?? '')); $patLastName = $this->sanitizePersonNamePart((string) ($patientRow['p_lname'] ?? '')); @@ -11418,8 +10871,7 @@ class Opd_prescription extends BaseController 'family_name' => $patLastName, 'gender' => $this->normalizeGender((string) ($patientRow['gender'] ?? '')), 'birthDate' => ! empty($patientRow['dob']) ? date('Y-m-d', strtotime((string) $patientRow['dob'])) : '', - 'abhaAddress' => $abhaIdentity['address'], - 'abhaId' => $abhaIdentity['id'], + 'abhaAddress' => $abhaAddress, 'phone' => $patPhone, ]; @@ -11566,7 +11018,6 @@ class Opd_prescription extends BaseController 'dosage' => implode(' | ', $dosageParts), 'route_text' => trim((string) ($row['dosage_where'] ?? '')), 'frequency_text' => trim((string) ($row['dosage_freq'] ?? '')), - 'no_of_days' => (int) ($row['no_of_days'] ?? 0), 'med_id' => $mid, 'generic_name' => $genericName, 'med_type' => $medType, @@ -11926,8 +11377,9 @@ class Opd_prescription extends BaseController { $errors = []; - $abha = $this->resolvePatientAbhaIdentifiersForFhir($patientRow); - if (trim((string) ($abha['address'] ?? '')) === '') { + $abhaField = $this->resolvePatientAbhaField(); + $abha = $abhaField !== null ? trim((string) ($patientRow[$abhaField] ?? '')) : ''; + if ($abha === '') { $errors[] = 'ABDM requires ABHA Address before saving consult.'; } diff --git a/app/Libraries/Abdm/EAtriaBridgeConnector.php b/app/Libraries/Abdm/EAtriaBridgeConnector.php index 19f2647..317215b 100644 --- a/app/Libraries/Abdm/EAtriaBridgeConnector.php +++ b/app/Libraries/Abdm/EAtriaBridgeConnector.php @@ -18,6 +18,8 @@ namespace App\Libraries\Abdm; */ class EAtriaBridgeConnector implements AbdmConnectorInterface { + private const REQUIRED_HFR_ID = 'IN0510000870'; + private string $baseUrl; private string $token; private string $hfrId; @@ -40,7 +42,7 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface $this->tokenSourceByValue[$this->token] = 'config.eatriaBridgeToken'; $this->tokenSource = 'config.eatriaBridgeToken'; } - $this->hfrId = ''; + $this->hfrId = self::REQUIRED_HFR_ID; $this->bridgeHospitalId = ''; $this->timeoutSec = (int) ($config->eatriaBridgeTimeoutSec ?? 30); @@ -133,11 +135,10 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface ?? $dbSettings['ABDM_HOSPITAL_HFR_ID'] ?? '' )); - if ($dbHfrId === '') { - log_message('warning', '[EAtriaBridge] ABDM_HFR_ID is empty in hospital_setting; bridge requests may fail with hfr_id errors.'); - } else { - $this->hfrId = $dbHfrId; + if ($dbHfrId !== '' && strcasecmp($dbHfrId, self::REQUIRED_HFR_ID) !== 0) { + log_message('warning', '[EAtriaBridge] Overriding configured HFR ID with mandated HFR ' . self::REQUIRED_HFR_ID . ' (found ' . $dbHfrId . ')'); } + $this->hfrId = self::REQUIRED_HFR_ID; $dbBridgeHospitalId = trim((string) ( $dbSettings['ABDM_BRIDGE_HOSPITAL_ID'] @@ -423,7 +424,7 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface } $this->dbLog($method, $path, $url, $body, $httpCode, (string) $raw, $ok === 1 ? 'success' : 'error', $logErr); if (($httpCode === 401 || $httpCode === 403) && ! isset($decoded['auth_hint'])) { - $decoded['auth_hint'] = 'Verify hospital_setting.EATRIA_BRIDGE_TOKEN is mapped to the same HFR as hospital_setting.ABDM_HFR_ID. Current token source=' . $this->tokenSource . ', HFR=' . ($this->hfrId !== '' ? $this->hfrId : '(empty)'); + $decoded['auth_hint'] = 'Verify hospital_setting.EATRIA_BRIDGE_TOKEN mapped to HFR IN0510000870. Current token source=' . $this->tokenSource . ', HFR=' . ($this->hfrId !== '' ? $this->hfrId : '(empty)'); } $decoded['auth_debug'] = $authDebug; @@ -826,30 +827,6 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface return $this->post('/v3/records/' . $bridgeId . '/link-and-share', $body); } - /** - * Retry variant for stale link-token scenarios after HFR/HIP changes. - * Sends explicit regeneration hints; gateway may ignore unknown flags safely. - */ - public function linkAndShareWithFreshToken(int $bridgeId): array - { - $body = [ - 'regenerate_link_token' => true, - 'force_regenerate_link_token' => true, - 'refresh_link_token' => true, - ]; - - if ($this->hfrId !== '') { - $body['hfr_id'] = $this->hfrId; - } - - $result = $this->post('/v3/records/' . $bridgeId . '/link-and-share', $body); - if (is_array($result)) { - $result['retry_mode'] = 'fresh_link_token'; - } - - return $result; - } - public function workflowStatus(int $bridgeId): array { $query = []; diff --git a/app/Libraries/FhirR4Builder.php b/app/Libraries/FhirR4Builder.php index 90cd11a..e42df43 100644 --- a/app/Libraries/FhirR4Builder.php +++ b/app/Libraries/FhirR4Builder.php @@ -356,9 +356,6 @@ class FhirR4Builder $snomedCode = trim((string) ($medication['snomed_code'] ?? '')); $atcCode = strtoupper(trim((string) ($medication['atc_code'] ?? ''))); - if ($snomedCode === '' && $atcCode === '') { - $atcCode = $this->inferAtcCodeFromMedicationText($displayText, $drugName, $genericName); - } if ($snomedCode !== '') { $medicationCodeableConcept['coding'] = [[ 'system' => 'http://snomed.info/sct', @@ -373,36 +370,7 @@ class FhirR4Builder ]]; } - $dosageText = trim((string) ($medication['dosage'] ?? '')); - $dosageInstruction = []; - if ($dosageText !== '') { - $dosageInstruction['text'] = $dosageText; - } - - $repeat = []; - $frequency = $this->resolveFrequencyPerDayFromText((string) ($medication['frequency_text'] ?? '')); - if ($frequency === null && $dosageText !== '') { - $frequency = $this->resolveFrequencyPerDayFromText($dosageText); - } - if ($frequency !== null) { - $repeat['frequency'] = $frequency; - $repeat['period'] = 1; - $repeat['periodUnit'] = 'd'; - } - - $durationDays = (int) ($medication['no_of_days'] ?? 0); - if ($durationDays <= 0 && $dosageText !== '') { - $durationDays = $this->extractDaysFromDosageText($dosageText); - } - if ($durationDays > 0) { - $repeat['duration'] = $durationDays; - $repeat['durationUnit'] = 'd'; - } - - if (! empty($repeat)) { - $dosageInstruction['timing'] = ['repeat' => $repeat]; - } - + $dosageInstruction = ['text' => (string) ($medication['dosage'] ?? '')]; $routeText = trim((string) ($medication['route_text'] ?? '')); if ($routeText !== '') { $routeEntry = ['text' => $routeText]; @@ -443,7 +411,7 @@ class FhirR4Builder 'medicationCodeableConcept' => $medicationCodeableConcept, 'subject' => ['reference' => $patientRef, 'display' => 'Patient'], 'encounter' => ['reference' => $encounterRef], - 'authoredOn' => $this->normalizeIsoDateTime((string) ($medication['authored_on'] ?? ''), $issuedAt), + 'authoredOn' => $issuedAt, 'dosageInstruction' => [$dosageInstruction], ]; if ($practitionerRef !== '') { @@ -1494,19 +1462,6 @@ class FhirR4Builder 'name' => [$nameEntry], ]; - $localPatientId = trim((string) ($patient['id'] ?? '')); - if ($localPatientId !== '') { - $resource['identifier'] = [[ - 'type' => ['coding' => [[ - 'system' => 'http://terminology.hl7.org/CodeSystem/v2-0203', - 'code' => 'MR', - 'display' => 'Medical record number', - ]]], - 'system' => 'https://hms.local/patient-id', - 'value' => $localPatientId, - ]]; - } - $gender = $this->normalizeAdministrativeGender($patient['gender'] ?? null); if ($gender !== '') { $resource['gender'] = $gender; @@ -1516,44 +1471,11 @@ class FhirR4Builder $resource['birthDate'] = (string) $patient['birthDate']; } - $abhaAddress = trim((string) ($patient['abhaAddress'] ?? ($patient['abha_address'] ?? ''))); - $abhaIdRaw = trim((string) ($patient['abhaId'] ?? ($patient['abha_id'] ?? ''))); - $abhaIdDigits = preg_replace('/\D/', '', $abhaIdRaw); - $abhaId = is_string($abhaIdDigits) && strlen($abhaIdDigits) === 14 ? $abhaIdDigits : ''; - - if ($abhaAddress !== '' || $abhaId !== '') { + $abhaAddress = trim((string) ($patient['abhaAddress'] ?? '')); + if ($abhaAddress !== '') { $resource['meta'] = ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Patient']]; // NI = National unique individual identifier (ABHA number / address) - if (! isset($resource['identifier']) || ! is_array($resource['identifier'])) { - $resource['identifier'] = []; - } - - if ($abhaId !== '') { - $resource['identifier'][] = [ - 'type' => ['coding' => [[ - 'system' => 'http://terminology.hl7.org/CodeSystem/v2-0203', - 'code' => 'NI', - 'display' => 'National unique individual identifier', - ]]], - 'system' => 'https://healthid.ndhm.gov.in', - 'value' => $abhaId, - ]; - } - - if ($abhaAddress !== '' && strcasecmp($abhaAddress, $abhaId) !== 0) { - $resource['identifier'][] = [ - 'type' => ['coding' => [[ - 'system' => 'http://terminology.hl7.org/CodeSystem/v2-0203', - 'code' => 'NI', - 'display' => 'National unique individual identifier', - ]]], - 'system' => 'https://healthid.ndhm.gov.in', - 'value' => $abhaAddress, - ]; - } - - if (empty($resource['identifier'])) { - $resource['identifier'][] = [ + $resource['identifier'] = [[ 'type' => ['coding' => [[ 'system' => 'http://terminology.hl7.org/CodeSystem/v2-0203', 'code' => 'NI', @@ -1561,8 +1483,7 @@ class FhirR4Builder ]]], 'system' => 'https://healthid.ndhm.gov.in', 'value' => $abhaAddress, - ]; - } + ]]; } // Phone / mobile telecom @@ -1651,96 +1572,6 @@ class FhirR4Builder return $map[$key] ?? ''; } - /** - * Best-effort ATC inference from medicine text when master coding is missing. - */ - private function inferAtcCodeFromMedicationText(string ...$texts): string - { - $haystack = strtolower(trim(implode(' ', $texts))); - if ($haystack === '') { - return ''; - } - - $map = [ - // Common OPD examples and analgesics - 'paracetamol' => 'N02BE01', - 'acetaminophen' => 'N02BE01', - 'aspirin' => 'B01AC06', - 'acetylsalicylic' => 'B01AC06', - 'disprin' => 'B01AC06', - // Acid suppression - 'ranitidine' => 'A02BA02', - 'aciloc' => 'A02BA02', - 'pantoprazole' => 'A02BC02', - 'omeprazole' => 'A02BC01', - // Frequent antibiotics - 'azithromycin' => 'J01FA10', - 'amoxicillin' => 'J01CA04', - 'cefixime' => 'J01DD08', - // Antihistamines - 'cetirizine' => 'R06AE07', - 'levocetirizine' => 'R06AE09', - ]; - - foreach ($map as $needle => $code) { - if (str_contains($haystack, $needle)) { - return $code; - } - } - - return ''; - } - - /** - * Infer per-day dosage frequency from common OPD text labels. - */ - private function resolveFrequencyPerDayFromText(string $text): ?int - { - $value = strtolower(trim($text)); - if ($value === '') { - return null; - } - - if (preg_match('/\b(qid|q\.i\.d|four\s*times?)\b/', $value) === 1) { - return 4; - } - if (preg_match('/\b(tds|tid|t\.d\.s|t\.i\.d|three\s*times?)\b/', $value) === 1) { - return 3; - } - if (preg_match('/\b(bd|bid|b\.d|b\.i\.d|two\s*times?|twice)\b/', $value) === 1) { - return 2; - } - if (preg_match('/\b(od|qd|o\.d|q\.d|once\s*daily|daily|once)\b/', $value) === 1) { - return 1; - } - - if (preg_match('/\b([1-9])\s*(x|times?)\b/', $value, $m) === 1) { - return (int) ($m[1] ?? 0) ?: null; - } - - if (preg_match('/\b([1-9])\s*\/\s*day\b/', $value, $m) === 1) { - return (int) ($m[1] ?? 0) ?: null; - } - - return null; - } - - /** - * Extract course duration in days from dosage text (e.g. "for 5 days"). - */ - private function extractDaysFromDosageText(string $text): int - { - if (preg_match('/\bfor\s+(\d{1,3})\s*day(?:s)?\b/i', $text, $m) === 1) { - return max(0, (int) ($m[1] ?? 0)); - } - - if (preg_match('/\b(\d{1,3})\s*day(?:s)?\b/i', $text, $m) === 1) { - return max(0, (int) ($m[1] ?? 0)); - } - - return 0; - } - // ========================================================================= // Enhanced FHIR Bundle Builders (ABDM M3 Compliant) // ========================================================================= @@ -2256,10 +2087,6 @@ class FhirR4Builder $mc = ['text' => $drugName]; $snomedCode = is_array($med) ? trim((string) ($med['snomed_code'] ?? '')) : ''; $atcCode = is_array($med) ? strtoupper(trim((string) ($med['atc_code'] ?? ''))) : ''; - if ($snomedCode === '' && $atcCode === '') { - $generic = is_array($med) ? trim((string) ($med['generic_name'] ?? '')) : ''; - $atcCode = $this->inferAtcCodeFromMedicationText($drugName, $generic); - } if ($snomedCode !== '') { $mc['coding'] = [['system' => 'http://snomed.info/sct', 'code' => $snomedCode, 'display' => $drugName]]; } elseif ($atcCode !== '') {