Revert "ABDM: fix local submit fallback, HFR runtime usage, status sync, and link-token retry"
This reverts commit e08c18f5b2.
This commit is contained in:
parent
e08c18f5b2
commit
3072639cd5
|
|
@ -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<string,mixed> $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 === '') {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
|
|||
. '<div class="check-item pending"><i class="bi bi-circle text-muted"></i><span>Bundle type is "document"</span></div>'
|
||||
. '<div class="check-item pending"><i class="bi bi-circle text-muted"></i><span>First entry is Composition</span></div>'
|
||||
. '<div class="check-item pending"><i class="bi bi-circle text-muted"></i><span>Patient resource present</span></div>'
|
||||
. '<div class="check-item pending"><i class="bi bi-circle text-muted"></i><span>Patient has at least one identifier</span></div>'
|
||||
. '<div class="check-item pending"><i class="bi bi-circle text-muted"></i><span>At least one clinical resource</span></div>'
|
||||
. '<div class="check-item pending"><i class="bi bi-circle text-muted"></i><span>MedicationRequest entries are coded (if present)</span></div>'
|
||||
. '<div class="check-item pending"><i class="bi bi-circle text-muted"></i><span>Visit date valid</span></div>'
|
||||
. '<div class="check-item pending" id="chkBridge"><i class="bi bi-circle text-muted"></i><span>Bridge connection reachable</span></div>'
|
||||
. '</div>'
|
||||
. '<div id="strictIssues" class="small mt-2"></div>'
|
||||
. '<button class="btn btn-primary w-100 mt-3" id="btnVerify">'
|
||||
. '<i class="bi bi-shield-check me-1"></i> Verify Data'
|
||||
. '</button>'
|
||||
|
|
@ -5199,8 +5138,6 @@ class Opd_prescription extends BaseController
|
|||
. ' const btn=this;'
|
||||
. ' btn.disabled=true;btn.innerHTML=\'<span class="spinner-border spinner-border-sm me-1"></span>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?\'<i class="bi bi-check-circle-fill text-success"></i>\':\'<i class="bi bi-x-circle-fill text-danger"></i>\')+\'<span>\'+msg+\'</span>\';'
|
||||
|
|
@ -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=\'<span class="spinner-border spinner-border-sm"></span><span>Checking bridge\u2026</span>\';'
|
||||
. ' 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=\'<i class="bi bi-exclamation-triangle me-1"></i>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=\'<div class="alert alert-warning py-2 mt-2 mb-0"><strong>Strict checks:</strong> \'+issues.join("; ")+\'</div>\';}'
|
||||
. ' }'
|
||||
. '});'
|
||||
|
||||
|
|
@ -5302,11 +5224,7 @@ class Opd_prescription extends BaseController
|
|||
. ' el.innerHTML=\'<div class="alert alert-success mt-2 py-2">\u2705 \'+(res.message||"Submitted successfully.")+qPart+idPart+\'</div>\';'
|
||||
. ' }else{'
|
||||
. ' btn.disabled=false;btn.innerHTML=origHtml;'
|
||||
. ' let strictText="";'
|
||||
. ' if(res.bundle_checks){'
|
||||
. ' strictText="<div class=\"small mt-1\">Server strict checks failed. Please run Verify Data and fix highlighted items.</div>";'
|
||||
. ' }'
|
||||
. ' el.innerHTML=\'<div class="alert alert-danger mt-2 py-2">\u274c \'+(res.message||res.error_text||"Submission failed.")+(res.http_code?" (HTTP "+res.http_code+")":"")+strictText+\'</div>\';'
|
||||
. ' el.innerHTML=\'<div class="alert alert-danger mt-2 py-2">\u274c \'+(res.message||res.error_text||"Submission failed.")+(res.http_code?" (HTTP "+res.http_code+")":"")+\'</div>\';'
|
||||
. ' }'
|
||||
. ' }).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<string,mixed> $context
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
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<string,mixed> $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<string,mixed> $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<string,mixed> $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<string, int>
|
||||
*/
|
||||
|
|
@ -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.';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = [];
|
||||
|
|
|
|||
|
|
@ -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 !== '') {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user