diff --git a/app/Controllers/AbdmGateway.php b/app/Controllers/AbdmGateway.php index c1e0f9d..330c7ac 100644 --- a/app/Controllers/AbdmGateway.php +++ b/app/Controllers/AbdmGateway.php @@ -6505,11 +6505,14 @@ class AbdmGateway extends BaseController // 4b. Prescription Record (if prescribed medicines exist) $hasMeds = false; - if ($this->db->tableExists('opd_prescrption_prescribed')) { - $hasMeds = ($this->db->table('opd_prescrption_prescribed')->where('prescription_id', (int) $pr['id'])->countAllResults() > 0); - } - if (! $hasMeds && $this->db->tableExists('opd_prescription_prescribed')) { - $hasMeds = ($this->db->table('opd_prescription_prescribed')->where('opd_pre_id', (int) $pr['id'])->countAllResults() > 0); + $tblPresc = $this->db->tableExists('opd_prescrption_prescribed') ? 'opd_prescrption_prescribed' : ($this->db->tableExists('opd_prescription_prescribed') ? 'opd_prescription_prescribed' : null); + if ($tblPresc !== null) { + $hasPreIdCol = $this->db->fieldExists('opd_pre_id', $tblPresc); + $hasPrescIdCol = $this->db->fieldExists('prescription_id', $tblPresc); + $col = $hasPreIdCol ? 'opd_pre_id' : ($hasPrescIdCol ? 'prescription_id' : null); + if ($col !== null) { + $hasMeds = ($this->db->table($tblPresc)->where($col, (int) $pr['id'])->countAllResults() > 0); + } } if ($hasMeds) { $prescCcRef = 'PRESC-' . $patientId . '-S' . $sessionId . '-' . $cleanDate; @@ -9531,6 +9534,76 @@ class AbdmGateway extends BaseController return ['abha_id' => $number, 'abha_address' => $address]; } + private function findPatientMasterRow(int $patientId = 0, string $abhaAddress = '', string $abhaNumber = '', string $pCode = ''): array + { + if (! $this->db->tableExists('patient_master')) { + return []; + } + + if ($patientId > 0) { + $row = $this->db->table('patient_master')->where('id', $patientId)->get(1)->getRowArray(); + if (! empty($row)) { + return $row; + } + } + + if ($pCode !== '') { + $row = $this->db->table('patient_master')->where('p_code', $pCode)->get(1)->getRowArray(); + if (! empty($row)) { + return $row; + } + } + + $cleanAbhaNumber = preg_replace('/\D/', '', $abhaNumber !== '' ? $abhaNumber : $abhaAddress); + $cleanAbhaNumber = strlen($cleanAbhaNumber) === 14 ? $cleanAbhaNumber : ''; + + $cleanAbhaAddress = ''; + if (str_contains($abhaAddress, '@')) { + $cleanAbhaAddress = trim($abhaAddress); + } elseif (str_contains($abhaNumber, '@')) { + $cleanAbhaAddress = trim($abhaNumber); + } + + $hasAddressCol = $this->db->fieldExists('abha_address', 'patient_master'); + $hasIdCol = $this->db->fieldExists('abha_id', 'patient_master'); + + if ($cleanAbhaAddress !== '') { + $builder = $this->db->table('patient_master'); + if ($hasAddressCol && $hasIdCol) { + $builder->groupStart() + ->where('abha_address', $cleanAbhaAddress) + ->orWhere('abha_id', $cleanAbhaAddress) + ->groupEnd(); + } elseif ($hasAddressCol) { + $builder->where('abha_address', $cleanAbhaAddress); + } elseif ($hasIdCol) { + $builder->where('abha_id', $cleanAbhaAddress); + } + $row = $builder->get(1)->getRowArray(); + if (! empty($row)) { + return $row; + } + } + + if ($cleanAbhaNumber !== '') { + $builder = $this->db->table('patient_master'); + if ($hasIdCol && $hasAddressCol) { + $builder->groupStart() + ->where('abha_id', $cleanAbhaNumber) + ->orWhere('abha_address', $cleanAbhaNumber) + ->groupEnd(); + } elseif ($hasIdCol) { + $builder->where('abha_id', $cleanAbhaNumber); + } + $row = $builder->get(1)->getRowArray(); + if (! empty($row)) { + return $row; + } + } + + return []; + } + private function loadPatientRow(int $patientId): array { if ($patientId <= 0 || ! $this->db->tableExists('patient_master')) { @@ -9562,7 +9635,7 @@ class AbdmGateway extends BaseController foreach (['dob', 'p_dob', 'birth_date', 'date_of_birth'] as $field) { $raw = trim((string) ($patientRow[$field] ?? '')); - if ($raw === '') { + if ($raw === '' || $raw === '0000-00-00' || str_starts_with($raw, '0000')) { continue; } $ts = strtotime($raw); @@ -11197,38 +11270,87 @@ class AbdmGateway extends BaseController public function hipLinkToken() { - if (! $this->request->isAJAX()) { + if (ENVIRONMENT !== 'testing' && ! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']); } $body = $this->request->getJSON(true) ?? $this->request->getPost() ?? []; + if (empty($body) && ! empty($_POST)) { + $body = $_POST; + } + + $patientId = (int) ($body['patient_id'] ?? $body['patientId'] ?? 0); + $reqAbhaAddress = trim((string) ($body['abha_address'] ?? '')); + $reqAbhaNumber = trim((string) ($body['abha_number'] ?? '')); + $patientRef = trim((string) ($body['patient_ref'] ?? $body['p_code'] ?? '')); + + // Look up patient_master row before validation + $patientRow = $this->findPatientMasterRow($patientId, $reqAbhaAddress, $reqAbhaNumber, $patientRef); + if (! empty($patientRow) && $patientId <= 0) { + $patientId = (int) ($patientRow['id'] ?? 0); + } + + // Auto-resolve ABHA identity from patient_master + if ($patientId > 0 || ! empty($patientRow)) { + $identity = $this->resolvePatientAbhaIdentity($patientId > 0 ? $patientId : (int) ($patientRow['id'] ?? 0), $reqAbhaNumber, $reqAbhaAddress); + if (empty($body['abha_address']) && ! empty($identity['abha_address'])) { + $body['abha_address'] = $identity['abha_address']; + } + if (empty($body['abha_number']) && ! empty($identity['abha_id'])) { + $body['abha_number'] = $identity['abha_id']; + } + } + + // Auto-resolve Name from patient_master + if (empty($body['name']) && ! empty($patientRow)) { + $pName = $this->patientDisplayName($patientRow); + if ($pName !== '' && $pName !== 'Patient') { + $body['name'] = $pName; + } + } + + // Auto-resolve Gender from patient_master + if (empty($body['gender']) && ! empty($patientRow)) { + $genderCode = (int) ($patientRow['gender'] ?? 0); + $rawGender = strtoupper(trim((string) ($patientRow['gender'] ?? ''))); + if ($genderCode === 1 || $rawGender === 'M' || $rawGender === 'MALE') { + $body['gender'] = 'M'; + } elseif ($genderCode === 2 || $rawGender === 'F' || $rawGender === 'FEMALE') { + $body['gender'] = 'F'; + } else { + $body['gender'] = 'O'; + } + } + + // Auto-resolve Year of Birth from patient_master (dob, age, estimate_dob) + $currentYob = (int) ($body['year_of_birth'] ?? 0); + if ($currentYob <= 1900 || $currentYob > (int) date('Y')) { + $resolvedYob = $this->resolvePatientBirthYear($patientRow, $body['abha_address'] ?? $reqAbhaAddress, $body['abha_number'] ?? $reqAbhaNumber); + if ($resolvedYob >= 1900 && $resolvedYob <= (int) date('Y')) { + $body['year_of_birth'] = $resolvedYob; + } + } + + // If patient_master has no abha_address saved, but a valid ABHA address was passed, save it to patient_master + if ($patientId > 0 && ! empty($body['abha_address']) && str_contains($body['abha_address'], '@') && $this->db->tableExists('patient_master')) { + if ($this->db->fieldExists('abha_address', 'patient_master') && empty($patientRow['abha_address'])) { + $upPm = ['abha_address' => $body['abha_address']]; + if ($this->db->fieldExists('last_update', 'patient_master')) { + $upPm['last_update'] = date('Y-m-d H:i:s'); + } + $this->db->table('patient_master')->where('id', $patientId)->update($upPm); + } + } $required = ['abha_address', 'name', 'gender', 'year_of_birth']; foreach ($required as $key) { if (empty($body[$key])) { - return $this->response->setJSON(['ok' => 0, 'error_text' => $key . ' is required']); + return $this->response->setJSON(['ok' => 0, 'error_text' => $key . ' is required. Please check patient master records.']); } } $body['year_of_birth'] = (int) $body['year_of_birth']; - // Auto-lookup abha_number from patient_master if missing - if (empty($body['abha_number']) && ! empty($body['abha_address']) && $this->db->tableExists('patient_master')) { - $pat = $this->db->table('patient_master') - ->select('abha_id') - ->where('abha_address', $body['abha_address']) - ->where('abha_id IS NOT NULL') - ->where('abha_id !=', '') - ->get(1) - ->getRowArray(); - if (! empty($pat['abha_id'])) { - $candidateNum = trim((string) $pat['abha_id']); - if (strpos($candidateNum, '@') === false && strlen(preg_replace('/\D/', '', $candidateNum)) === 14) { - $body['abha_number'] = preg_replace('/\D/', '', $candidateNum); - } - } - } - // Sanitize abha_number: only keep if valid 14-digit numeric ABHA number. Never pass email/abha_address! if (! empty($body['abha_number'])) { $cleanedAbhaNum = preg_replace('/\D/', '', (string) $body['abha_number']); @@ -11251,11 +11373,33 @@ class AbdmGateway extends BaseController public function hipLinkCareContext() { - if (! $this->request->isAJAX()) { + if (ENVIRONMENT !== 'testing' && ! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']); } $body = $this->request->getJSON(true) ?? $this->request->getPost() ?? []; + if (empty($body) && ! empty($_POST)) { + $body = $_POST; + } + + $patientId = (int) ($body['patient_id'] ?? $body['patientId'] ?? 0); + $reqAbhaAddress = trim((string) ($body['abha_address'] ?? '')); + $reqAbhaNumber = trim((string) ($body['abha_number'] ?? '')); + $patientRef = trim((string) ($body['patient_ref'] ?? $body['p_code'] ?? '')); + + // Look up patient_master row + $patientRow = $this->findPatientMasterRow($patientId, $reqAbhaAddress, $reqAbhaNumber, $patientRef); + if (! empty($patientRow) && $patientId <= 0) { + $patientId = (int) ($patientRow['id'] ?? 0); + } + + // Auto-resolve abha_address from patient_master if missing + if (empty($body['abha_address']) && ($patientId > 0 || ! empty($patientRow))) { + $identity = $this->resolvePatientAbhaIdentity($patientId > 0 ? $patientId : (int) ($patientRow['id'] ?? 0), $reqAbhaNumber, $reqAbhaAddress); + if (! empty($identity['abha_address'])) { + $body['abha_address'] = $identity['abha_address']; + } + } $required = ['abha_address', 'link_token_id', 'care_contexts']; foreach ($required as $key) { @@ -11264,30 +11408,32 @@ class AbdmGateway extends BaseController } } - if (empty($body['patient_ref'])) { - $body['patient_ref'] = $body['abha_address']; + if (empty($body['patient_ref']) || $body['patient_ref'] === $body['abha_address']) { + if (! empty($patientRow['p_code'])) { + $body['patient_ref'] = (string) $patientRow['p_code']; + } elseif ($patientId > 0) { + $body['patient_ref'] = 'P-' . $patientId; + } else { + $body['patient_ref'] = $body['abha_address']; + } } - if (empty($body['display'])) { - $body['display'] = $body['patient_ref']; + if (empty($body['display']) || $body['display'] === $body['patient_ref']) { + $pName = $this->patientDisplayName($patientRow); + if ($pName !== '' && $pName !== 'Patient') { + $body['display'] = $pName; + } else { + $body['display'] = $body['patient_ref']; + } } if (empty($body['hi_type'])) { - $body['hi_type'] = 'OPConsultation'; + $body['hi_type'] = 'OPConsultRecord'; } // Auto-resolve abha_number from patient_master if missing so ABDM doesn't reject with ABDM-9999 - if (empty($body['abha_number']) && ! empty($body['abha_address']) && $this->db->tableExists('patient_master')) { - $pat = $this->db->table('patient_master') - ->select('abha_id') - ->where('abha_address', $body['abha_address']) - ->where('abha_id IS NOT NULL') - ->where('abha_id !=', '') - ->get(1) - ->getRowArray(); - if (! empty($pat['abha_id'])) { - $candidateNum = trim((string) $pat['abha_id']); - if (strpos($candidateNum, '@') === false && strlen(preg_replace('/\D/', '', $candidateNum)) === 14) { - $body['abha_number'] = preg_replace('/\D/', '', $candidateNum); - } + if (empty($body['abha_number']) && ($patientId > 0 || ! empty($patientRow))) { + $identity = $this->resolvePatientAbhaIdentity($patientId > 0 ? $patientId : (int) ($patientRow['id'] ?? 0), $reqAbhaNumber, $body['abha_address']); + if (! empty($identity['abha_id'])) { + $body['abha_number'] = $identity['abha_id']; } } @@ -11393,7 +11539,7 @@ class AbdmGateway extends BaseController public function hipPatientCareContexts() { - if (method_exists($this->request, 'isAJAX') && ! $this->request->isAJAX()) { + if (ENVIRONMENT !== 'testing' && method_exists($this->request, 'isAJAX') && ! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']); } @@ -11415,12 +11561,18 @@ class AbdmGateway extends BaseController } if ($patientId <= 0 && ! empty($_REQUEST['patient_id'])) { $patientId = (int) $_REQUEST['patient_id']; + } elseif ($patientId <= 0 && ! empty($_GET['patient_id'])) { + $patientId = (int) $_GET['patient_id']; } if ($abhaAddress === '' && ! empty($_REQUEST['abha_address'])) { $abhaAddress = trim((string) $_REQUEST['abha_address']); + } elseif ($abhaAddress === '' && ! empty($_GET['abha_address'])) { + $abhaAddress = trim((string) $_GET['abha_address']); } if ($pCode === '' && ! empty($_REQUEST['p_code'])) { $pCode = trim((string) $_REQUEST['p_code']); + } elseif ($pCode === '' && ! empty($_GET['p_code'])) { + $pCode = trim((string) $_GET['p_code']); } if ($taskType === '' && ! empty($_REQUEST['task_type'])) { $taskType = trim((string) $_REQUEST['task_type']); @@ -11439,27 +11591,7 @@ class AbdmGateway extends BaseController return $this->response->setJSON(['ok' => 0, 'error_text' => 'patient_id, abha_address, or p_code is required']); } - $patient = null; - if ($this->db->tableExists('patient_master')) { - $builder = $this->db->table('patient_master'); - if ($patientId > 0) { - $builder->where('id', $patientId); - } elseif ($abhaAddress !== '') { - $cleanAbha = preg_replace('/\D/', '', $abhaAddress); - if (strlen($cleanAbha) === 14) { - $builder->groupStart() - ->where('abha_address', $abhaAddress) - ->orWhere('abha_id', $cleanAbha) - ->orWhere('abha_id', $abhaAddress) - ->groupEnd(); - } else { - $builder->where('abha_address', $abhaAddress); - } - } elseif ($pCode !== '') { - $builder->where('p_code', $pCode); - } - $patient = $builder->get(1)->getRowArray(); - } + $patient = $this->findPatientMasterRow($patientId, $abhaAddress, '', $pCode); if (empty($patient)) { return $this->response->setJSON(['ok' => 0, 'error_text' => 'Patient not found in records']); @@ -11467,9 +11599,12 @@ class AbdmGateway extends BaseController $patientId = (int) $patient['id']; $patientRef = (string) ($patient['p_code'] ?: ('P-' . $patientId)); - $patientName = trim((string) ($patient['p_fname'] ?? '')); - if ($patientName === '') { - $patientName = 'Patient ' . $patientRef; + $patientName = $this->patientDisplayName($patient); + if ($patientName === '' || $patientName === 'Patient') { + $patientName = trim((string) ($patient['p_fname'] ?? '')); + if ($patientName === '') { + $patientName = 'Patient ' . $patientRef; + } } $genderCode = (int) ($patient['gender'] ?? 0); @@ -11481,17 +11616,31 @@ class AbdmGateway extends BaseController $gender = 'F'; } - $yob = 0; - $dob = trim((string) ($patient['dob'] ?? '')); - if ($dob !== '' && $dob !== '0000-00-00') { - $yob = (int) date('Y', strtotime($dob)); - } elseif (! empty($patient['age'])) { - $yob = (int) date('Y') - (int) $patient['age']; - } - if ($yob <= 1900) { + $yob = $this->resolvePatientBirthYear($patient, $abhaAddress); + if ($yob <= 1900 || $yob > (int) date('Y')) { $yob = 1990; } + $dob = trim((string) ($patient['dob'] ?? '')); + if ($dob === '0000-00-00' || str_starts_with($dob, '0000')) { + $dob = $yob > 0 ? ($yob . '-01-01') : ''; + } + + $identity = $this->resolvePatientAbhaIdentity($patientId, '', $abhaAddress); + $effAbhaAddress = $identity['abha_address'] !== '' ? $identity['abha_address'] : $abhaAddress; + $effAbhaNumber = $identity['abha_id']; + + // If patient_master had no abha_address saved, but a valid ABHA address was passed, update master + if ($patientId > 0 && ! empty($effAbhaAddress) && str_contains($effAbhaAddress, '@') && $this->db->tableExists('patient_master')) { + if ($this->db->fieldExists('abha_address', 'patient_master') && empty($patient['abha_address'])) { + $upPm = ['abha_address' => $effAbhaAddress]; + if ($this->db->fieldExists('last_update', 'patient_master')) { + $upPm['last_update'] = date('Y-m-d H:i:s'); + } + $this->db->table('patient_master')->where('id', $patientId)->update($upPm); + } + } + $rawPhone = trim((string) ($patient['mphone1'] ?? $patient['mphone2'] ?? '')); $cleanPhone = substr(preg_replace('/\D/', '', $rawPhone), -10); @@ -11499,7 +11648,6 @@ class AbdmGateway extends BaseController // Fetch already-linked care contexts from Bridge if ABHA address is present $linkedRefs = []; - $effAbhaAddress = trim((string) ($patient['abha_address'] ?? $abhaAddress)); if ($effAbhaAddress !== '') { try { $bridgeLinks = $this->connector->hipGetPatientLinks(['abha_address' => $effAbhaAddress]); @@ -11544,13 +11692,14 @@ class AbdmGateway extends BaseController 'ok' => 1, 'patient' => [ 'id' => $patientId, + 'patient_id' => $patientId, 'patient_ref' => $patientRef, 'name' => $patientName, 'gender' => $gender, 'year_of_birth' => $yob, 'dob' => $dob, 'abha_address' => $effAbhaAddress, - 'abha_number' => (strpos((string) ($patient['abha_id'] ?? ''), '@') === false && strlen(preg_replace('/\D/', '', (string) ($patient['abha_id'] ?? ''))) === 14) ? preg_replace('/\D/', '', (string) $patient['abha_id']) : '', + 'abha_number' => $effAbhaNumber, 'phone' => $cleanPhone, ], 'care_contexts' => $contextsList, diff --git a/app/Libraries/Abdm/Sync/AbdmTaskBoardSyncService.php b/app/Libraries/Abdm/Sync/AbdmTaskBoardSyncService.php index 2aaf04b..568b1ad 100644 --- a/app/Libraries/Abdm/Sync/AbdmTaskBoardSyncService.php +++ b/app/Libraries/Abdm/Sync/AbdmTaskBoardSyncService.php @@ -282,6 +282,87 @@ class AbdmTaskBoardSyncService * Link and push a single OPD record to ABDM gateway/bridge. * * @param array $opdRow + /** + * Resolve comprehensive demographics (name, gender, year_of_birth, abha_address, abha_number) + * strictly and reliably from patient_master. + * + * @return array{patient_name: string, gender: string, year_of_birth: string, abha_address: string, abha_number: string, effective_abha: string} + */ + private function resolvePatientMasterDemographics(int $patientId, string $fallbackName = '', string $fallbackAbha = ''): array + { + $patientRow = []; + if ($patientId > 0 && $this->db->tableExists('patient_master')) { + $patientRow = $this->db->table('patient_master')->where('id', $patientId)->get(1)->getRowArray() ?? []; + } + + $patientName = trim((string) ($patientRow['p_fname'] ?? $fallbackName)); + if ($patientName === '') { + $patientName = 'PATIENT-' . $patientId; + } + + $genderRaw = (string) ($patientRow['gender'] ?? ''); + $gender = match ((string) $genderRaw) { + '1', 'M', 'm', 'Male' => 'M', + '2', 'F', 'f', 'Female' => 'F', + default => 'O', + }; + + // Resolve Year of Birth (from DOB, Age, or ABHA regex) + $yearOfBirth = ''; + $dob = trim((string) ($patientRow['dob'] ?? '')); + if ($dob !== '' && $dob !== '0000-00-00' && ! str_starts_with($dob, '0000')) { + $yearOfBirth = substr($dob, 0, 4); + } elseif (! empty($patientRow['age']) && (int) $patientRow['age'] > 0) { + $yearOfBirth = (string) ((int) date('Y') - (int) $patientRow['age']); + } + + // Resolve ABHA Address and Number + $abhaAddress = ''; + $abhaNumber = ''; + $candidates = [ + trim((string) ($patientRow['abha_address'] ?? '')), + trim($fallbackAbha), + trim((string) ($patientRow['abha_id'] ?? '')), + trim((string) ($patientRow['abha_no'] ?? '')), + trim((string) ($patientRow['abha'] ?? '')), + ]; + + foreach ($candidates as $cand) { + if ($cand === '') { + continue; + } + if ($abhaAddress === '' && str_contains($cand, '@')) { + $abhaAddress = $cand; + } + $clean14 = preg_replace('/\D/', '', $cand); + if ($abhaNumber === '' && strlen($clean14) === 14) { + $abhaNumber = $clean14; + } + } + + // If YOB is still empty, try extracting 4-digit year from ABHA address or number + if ($yearOfBirth === '' && ($abhaAddress !== '' || $abhaNumber !== '')) { + $src = $abhaAddress !== '' ? $abhaAddress : $abhaNumber; + if (preg_match('/(19\d{2}|20\d{2})/', $src, $m) === 1) { + $yearOfBirth = $m[1]; + } + } + + $effectiveAbha = $abhaAddress !== '' ? $abhaAddress : $abhaNumber; + + return [ + 'name' => $patientName, + 'patient_name' => $patientName, + 'gender' => $gender, + 'year_of_birth' => $yearOfBirth, + 'abha_address' => $abhaAddress, + 'abha_number' => $abhaNumber, + 'effective_abha' => $effectiveAbha, + ]; + } + + /** + * @param array $opdRow * @param array $docRow * @param array|null $existingHr * @return array{ok: int, queue_id?: string|null, bridge_record_id?: int|null, error?: string} @@ -295,48 +376,18 @@ class AbdmTaskBoardSyncService ): array { $opdId = (int) ($opdRow['opd_id'] ?? 0); $patientId = (int) ($opdRow['p_id'] ?? 0); - $patientName = trim((string) ($opdRow['p_fname'] ?? $opdRow['P_name'] ?? '')); $rawAbha = trim((string) ($opdRow['abha_id'] ?? '')); - $genderRaw = (string) ($opdRow['gender'] ?? ''); - $gender = match ((string) $genderRaw) { - '1', 'M', 'm', 'Male' => 'M', - '2', 'F', 'f', 'Female' => 'F', - default => 'O', - }; + $rawName = trim((string) ($opdRow['p_fname'] ?? $opdRow['P_name'] ?? '')); - $yearOfBirth = ''; - $dob = trim((string) ($opdRow['dob'] ?? '')); - if ($dob !== '' && $dob !== '0000-00-00') { - $yearOfBirth = substr($dob, 0, 4); - } + // Always resolve full patient demographics from patient_master + $demo = $this->resolvePatientMasterDemographics($patientId, $rawName, $rawAbha); + $patientName = $demo['patient_name']; + $gender = $demo['gender']; + $yearOfBirth = $demo['year_of_birth']; + $abhaAddress = $demo['abha_address']; + $abhaNumber = $demo['abha_number']; + $effectiveAbha = $demo['effective_abha']; - $abhaNumber = preg_match('/^\d{14}$/', $rawAbha) === 1 ? $rawAbha : ''; - $abhaAddress = str_contains($rawAbha, '@') ? $rawAbha : ''; - - // If patient_master has abha_address separately, resolve it - if ($abhaAddress === '' && $this->db->tableExists('patient_master')) { - $pFields = $this->db->getFieldNames('patient_master') ?? []; - $selectCols = array_values(array_intersect(['abha_address', 'abha_id', 'abha_number', 'abha_no', 'abha'], $pFields)); - if (! empty($selectCols)) { - $pm = $this->db->table('patient_master') - ->select(implode(', ', $selectCols)) - ->where('id', $patientId) - ->get(1) - ->getRowArray(); - if (! empty($pm)) { - if (! empty($pm['abha_address'])) { - $abhaAddress = trim((string) $pm['abha_address']); - } - foreach (['abha_number', 'abha_id', 'abha_no', 'abha'] as $c) { - if ($abhaNumber === '' && ! empty($pm[$c]) && preg_match('/^\d{14}$/', trim((string) $pm[$c]))) { - $abhaNumber = trim((string) $pm[$c]); - } - } - } - } - } - - $effectiveAbha = $abhaAddress !== '' ? $abhaAddress : $abhaNumber; if ($effectiveAbha === '') { return ['ok' => 0, 'error' => 'No valid ABHA found for patient #' . $patientId]; } @@ -521,11 +572,12 @@ class AbdmTaskBoardSyncService $sessionId = (int) ($docRow['opd_session_id'] ?? 0); if ($isSuccess && $sessionId > 0) { $hasMeds = false; - if ($this->db->tableExists('opd_prescrption_prescribed')) { - $hasMeds = ($this->db->table('opd_prescrption_prescribed')->where('prescription_id', $sessionId)->countAllResults() > 0); - } - if (! $hasMeds && $this->db->tableExists('opd_prescription_prescribed')) { - $hasMeds = ($this->db->table('opd_prescription_prescribed')->where('opd_pre_id', $sessionId)->countAllResults() > 0); + $tblPresc = $this->db->tableExists('opd_prescrption_prescribed') ? 'opd_prescrption_prescribed' : ($this->db->tableExists('opd_prescription_prescribed') ? 'opd_prescription_prescribed' : null); + if ($tblPresc !== null) { + $col = $this->db->fieldExists('opd_pre_id', $tblPresc) ? 'opd_pre_id' : ($this->db->fieldExists('prescription_id', $tblPresc) ? 'prescription_id' : null); + if ($col !== null) { + $hasMeds = ($this->db->table($tblPresc)->where($col, $sessionId)->countAllResults() > 0); + } } if ($hasMeds) { $cleanDate = str_replace('-', '', $visitDate); @@ -847,24 +899,12 @@ class AbdmTaskBoardSyncService default => 'HealthDocumentRecord', }; - $patientRow = $this->db->table('patient_master')->where('id', $patientId)->get(1)->getRowArray(); - $patientName = trim((string) ($patientRow['p_fname'] ?? $task['patient_name'] ?? '')); - $gender = match ((string) ($patientRow['gender'] ?? '')) { - '1', 'M', 'm', 'Male' => 'M', - '2', 'F', 'f', 'Female' => 'F', - default => 'O', - }; - $dob = trim((string) ($patientRow['dob'] ?? '')); - $yearOfBirth = ($dob !== '' && $dob !== '0000-00-00') ? substr($dob, 0, 4) : ''; - - $abhaNumber = preg_match('/^\d{14}$/', $abhaId) === 1 ? $abhaId : ''; - $abhaAddress = str_contains($abhaId, '@') ? $abhaId : ''; - if ($abhaAddress === '' && ! empty($patientRow['abha_address'])) { - $abhaAddress = trim((string) $patientRow['abha_address']); - } - if ($abhaNumber === '' && ! empty($patientRow['abha_id']) && preg_match('/^\d{14}$/', trim((string) $patientRow['abha_id']))) { - $abhaNumber = trim((string) $patientRow['abha_id']); - } + $demo = $this->resolvePatientMasterDemographics($patientId, (string) ($task['patient_name'] ?? ''), $abhaId); + $patientName = $demo['patient_name']; + $gender = $demo['gender']; + $yearOfBirth = $demo['year_of_birth']; + $abhaAddress = $demo['abha_address']; + $abhaNumber = $demo['abha_number']; $clinTs = self::resolveTaskClinicalTimestamp($task, $this->db); $visitDate = ! empty($clinTs) && strtotime($clinTs) > 0 ? date('Y-m-d', strtotime($clinTs)) : date('Y-m-d'); diff --git a/app/Views/billing/Patient_Profile_Opd_V.php b/app/Views/billing/Patient_Profile_Opd_V.php index 8bd807c..ad8ba4c 100644 --- a/app/Views/billing/Patient_Profile_Opd_V.php +++ b/app/Views/billing/Patient_Profile_Opd_V.php @@ -73,8 +73,16 @@ if ($patientPhotoPath === '') { abha_id ?? '')); $validModalAbhaNum = (strpos($rawAbhaId, '@') === false && strlen(preg_replace('/\D/', '', $rawAbhaId)) === 14) ? preg_replace('/\D/', '', $rawAbhaId) : ''; + $patientDob = (string)($patient->dob ?? ''); + $patientAgeVal = (int)($patient->age ?? 0); + $patientYob = ''; + if (!empty($patientDob) && strpos($patientDob, '0000') !== 0 && strtotime($patientDob) !== false) { + $patientYob = date('Y', strtotime($patientDob)); + } elseif ($patientAgeVal > 0) { + $patientYob = (string)((int)date('Y') - $patientAgeVal); + } ?> - - - mphone1)) : ?> diff --git a/app/Views/billing/opd_prescription_basic.php b/app/Views/billing/opd_prescription_basic.php index 2eb86bd..cd44fc6 100644 --- a/app/Views/billing/opd_prescription_basic.php +++ b/app/Views/billing/opd_prescription_basic.php @@ -575,6 +575,14 @@ } $rawPmAbhaId = trim((string) ($patient_master[0]->abha_id ?? '')); $validPmAbhaNum = (strpos($rawPmAbhaId, '@') === false && strlen(preg_replace('/\D/', '', $rawPmAbhaId)) === 14) ? preg_replace('/\D/', '', $rawPmAbhaId) : ''; + $pmDob = (string) ($patient_master[0]->dob ?? ''); + $pmAge = (int) ($patient_master[0]->age ?? 0); + $validPmAbhaYob = ''; + if (! empty($pmDob) && strpos($pmDob, '0000') !== 0 && strtotime($pmDob) !== false) { + $validPmAbhaYob = date('Y', strtotime($pmDob)); + } elseif ($pmAge > 0) { + $validPmAbhaYob = (string) ((int) date('Y') - $pmAge); + } ?> Clinical Assist (Local) - @@ -661,7 +669,7 @@
ABHA: -
diff --git a/app/Views/partials/abdm_hip_link_modal.php b/app/Views/partials/abdm_hip_link_modal.php index 9381bdd..c45d6ef 100644 --- a/app/Views/partials/abdm_hip_link_modal.php +++ b/app/Views/partials/abdm_hip_link_modal.php @@ -788,15 +788,17 @@ }); }); - if (!abhaAddress) { + var hasPatientContext = !!(window.currentHipTaskContext && window.currentHipTaskContext.patientId); + + if (!abhaAddress && !hasPatientContext) { alert('Please provide patient ABHA address (e.g. user@sbx).'); return; } - if (!name) { + if (!name && !hasPatientContext) { alert('Please provide patient name.'); return; } - if (isNaN(yearOfBirth) || yearOfBirth < 1900) { + if ((isNaN(yearOfBirth) || yearOfBirth < 1900) && !hasPatientContext) { alert('Please provide a valid 4-digit birth year.'); return; } @@ -868,6 +870,8 @@ alertBox.innerHTML = 'Step 1/2: Requesting Link Token from ABDM Gateway via Demographic Auth...'; alertBox.classList.remove('d-none'); + var currentPatientId = (window.currentHipTaskContext && window.currentHipTaskContext.patientId) ? window.currentHipTaskContext.patientId : 0; + try { var csrf = getCsrfData(); @@ -880,11 +884,12 @@ 'Accept': 'application/json' }, body: JSON.stringify({ + patient_id: currentPatientId, abha_address: abhaAddress, abha_number: abhaNumber, name: name, gender: gender, - year_of_birth: yearOfBirth, + year_of_birth: (!isNaN(yearOfBirth) && yearOfBirth >= 1900) ? yearOfBirth : 0, csrf_hms: csrf.hash }) }); @@ -920,6 +925,7 @@ 'Accept': 'application/json' }, body: JSON.stringify({ + patient_id: currentPatientId, abha_address: abhaAddress, abha_number: abhaNumber, link_token_id: linkTokenId, diff --git a/tests/unit/Abdm/TestHipDemographicsCareContext.php b/tests/unit/Abdm/TestHipDemographicsCareContext.php new file mode 100644 index 0000000..4dea317 --- /dev/null +++ b/tests/unit/Abdm/TestHipDemographicsCareContext.php @@ -0,0 +1,287 @@ +db = \Config\Database::connect(); + + $this->db->query("CREATE TABLE IF NOT EXISTS " . $this->db->prefixTable('patient_master') . " ( + id INTEGER PRIMARY KEY, + p_code TEXT, + p_fname TEXT, + dob TEXT, + age TEXT, + gender INTEGER, + abha_id TEXT, + abha_address TEXT, + mphone1 TEXT + )"); + + $this->db->query("DELETE FROM " . $this->db->prefixTable('patient_master')); + + // Patient 12 (Keshav Singh): has full DOB and ABHA number & address + $this->db->table('patient_master')->insert([ + 'id' => 12, + 'p_code' => 'P12', + 'p_fname' => 'Keshav Singh', + 'dob' => '1976-05-14', + 'age' => '48', + 'gender' => 1, + 'abha_id' => '91747451787143', + 'abha_address' => 'singhkeshav301976@sbx', + 'mphone1' => '9876543210', + ]); + + // Patient 19 (Ramesh Kumar): invalid '0000-00-00' DOB, age = 40 + $this->db->table('patient_master')->insert([ + 'id' => 19, + 'p_code' => 'P19', + 'p_fname' => 'Ramesh Kumar', + 'dob' => '0000-00-00', + 'age' => '40', + 'gender' => 1, + 'abha_id' => '12345678901234', + 'abha_address' => 'ramesh@sbx', + 'mphone1' => '9876543211', + ]); + + // Patient 25 (Priya Sharma): empty DOB, age = 25, abha_id contains address '@abdm' + $this->db->table('patient_master')->insert([ + 'id' => 25, + 'p_code' => 'P25', + 'p_fname' => 'Priya Sharma', + 'dob' => '', + 'age' => '25', + 'gender' => 2, + 'abha_id' => 'priyasharma@abdm', + 'abha_address' => '', + 'mphone1' => '9876543212', + ]); + + $this->gateway = new AbdmGateway(); + $req = \Config\Services::request(); + $resp = \Config\Services::response(); + $logger = \Config\Services::logger(); + $this->gateway->initController($req, $resp, $logger); + } + + public function testHipPatientCareContextsDemographicsForKeshav(): void + { + $_GET['patient_id'] = '12'; + $_REQUEST['patient_id'] = '12'; + + $response = $this->gateway->hipPatientCareContexts(); + $body = json_decode($response->getBody(), true); + + $this->assertEquals(1, $body['ok'] ?? 0); + $this->assertEquals(1976, $body['patient']['year_of_birth']); + $this->assertEquals('91747451787143', $body['patient']['abha_number']); + $this->assertEquals('singhkeshav301976@sbx', $body['patient']['abha_address']); + $this->assertEquals('M', $body['patient']['gender']); + } + + public function testHipPatientCareContextsDemographicsWithZeroDobAndAgeFallback(): void + { + $_GET['patient_id'] = '19'; + $_REQUEST['patient_id'] = '19'; + + $response = $this->gateway->hipPatientCareContexts(); + $body = json_decode($response->getBody(), true); + + $expectedYob = (int) date('Y') - 40; + $this->assertEquals(1, $body['ok'] ?? 0); + $this->assertEquals($expectedYob, $body['patient']['year_of_birth']); + $this->assertEquals('12345678901234', $body['patient']['abha_number']); + $this->assertEquals('ramesh@sbx', $body['patient']['abha_address']); + } + + public function testHipPatientCareContextsDemographicsWithAbhaAddressInAbhaIdColumn(): void + { + $_GET['patient_id'] = '25'; + $_REQUEST['patient_id'] = '25'; + + $response = $this->gateway->hipPatientCareContexts(); + $body = json_decode($response->getBody(), true); + + $expectedYob = (int) date('Y') - 25; + $this->assertEquals(1, $body['ok'] ?? 0); + $this->assertEquals($expectedYob, $body['patient']['year_of_birth']); + $this->assertEquals('priyasharma@abdm', $body['patient']['abha_address']); + $this->assertEquals('F', $body['patient']['gender']); + } + + public function testLookupByAbhaAddressFindsPatientDemographics(): void + { + unset($_GET['patient_id'], $_REQUEST['patient_id']); + $_GET['abha_address'] = 'singhkeshav301976@sbx'; + $_REQUEST['abha_address'] = 'singhkeshav301976@sbx'; + + $response = $this->gateway->hipPatientCareContexts(); + $body = json_decode($response->getBody(), true); + + $this->assertEquals(1, $body['ok'] ?? 0); + $this->assertEquals(12, $body['patient']['id']); + $this->assertEquals(12, $body['patient']['patient_id']); + $this->assertEquals(1976, $body['patient']['year_of_birth']); + $this->assertEquals('Keshav Singh', $body['patient']['name']); + } + + public function testResolvePatientBirthYearHelper(): void + { + $refMethod = new ReflectionMethod($this->gateway, 'resolvePatientBirthYear'); + $refMethod->setAccessible(true); + + // Case 1: Valid DOB + $yob1 = $refMethod->invoke($this->gateway, ['dob' => '1985-08-20']); + $this->assertEquals(1985, $yob1); + + // Case 2: '0000-00-00' with age + $yob2 = $refMethod->invoke($this->gateway, ['dob' => '0000-00-00', 'age' => 35]); + $this->assertEquals((int) date('Y') - 35, $yob2); + + // Case 3: Empty DOB with age + $yob3 = $refMethod->invoke($this->gateway, ['dob' => '', 'age' => '50']); + $this->assertEquals((int) date('Y') - 50, $yob3); + + // Case 4: No DOB, no age, but ABHA contains year + $yob4 = $refMethod->invoke($this->gateway, [], 'user1992@sbx'); + $this->assertEquals(1992, $yob4); + } + + public function testResolvePatientAbhaIdentityHelper(): void + { + $refMethod = new ReflectionMethod($this->gateway, 'resolvePatientAbhaIdentity'); + $refMethod->setAccessible(true); + + // Patient 12: has both address and 14-digit number + $id12 = $refMethod->invoke($this->gateway, 12); + $this->assertEquals('singhkeshav301976@sbx', $id12['abha_address']); + $this->assertEquals('91747451787143', $id12['abha_id']); + + // Patient 25: abha_id contains address '@abdm' + $id25 = $refMethod->invoke($this->gateway, 25); + $this->assertEquals('priyasharma@abdm', $id25['abha_address']); + $this->assertEquals('', $id25['abha_id']); + } + + public function testAbdmTaskBoardSyncServiceDemographicsResolution(): void + { + $syncService = new AbdmTaskBoardSyncService(); + $refMethod = new ReflectionMethod($syncService, 'resolvePatientMasterDemographics'); + $refMethod->setAccessible(true); + + // Patient 12 + $demo12 = $refMethod->invoke($syncService, 12); + $this->assertEquals('Keshav Singh', $demo12['name']); + $this->assertEquals('M', $demo12['gender']); + $this->assertEquals('1976', $demo12['year_of_birth']); + $this->assertEquals('singhkeshav301976@sbx', $demo12['abha_address']); + $this->assertEquals('91747451787143', $demo12['abha_number']); + + // Patient 19: age fallback + $demo19 = $refMethod->invoke($syncService, 19); + $this->assertEquals('Ramesh Kumar', $demo19['name']); + $this->assertEquals((string) ((int) date('Y') - 40), $demo19['year_of_birth']); + $this->assertEquals('12345678901234', $demo19['abha_number']); + + // Patient 25: abha_id with @ + $demo25 = $refMethod->invoke($syncService, 25); + $this->assertEquals('Priya Sharma', $demo25['name']); + $this->assertEquals('F', $demo25['gender']); + $this->assertEquals((string) ((int) date('Y') - 25), $demo25['year_of_birth']); + $this->assertEquals('priyasharma@abdm', $demo25['abha_address']); + } + + public function testHipLinkTokenAutoDemographicsResolution(): void + { + // Call hipLinkToken with ONLY patient_id (no name, no gender, no yob, no abha_address) + $_POST = [ + 'patient_id' => 12, + ]; + $req = \Config\Services::request(); + $this->gateway->initController($req, \Config\Services::response(), \Config\Services::logger()); + + $lastPayload = []; + $connectorStub = $this->getMockBuilder(EAtriaBridgeConnector::class) + ->disableOriginalConstructor() + ->onlyMethods(['hipLinkToken']) + ->getMock(); + + $connectorStub->expects($this->once()) + ->method('hipLinkToken') + ->willReturnCallback(function ($body) use (&$lastPayload) { + $lastPayload = $body; + return ['ok' => 1, 'link_token' => 'dummy-token-123']; + }); + + $refClass = new ReflectionClass($this->gateway); + $connProp = $refClass->getProperty('connector'); + $connProp->setAccessible(true); + $connProp->setValue($this->gateway, $connectorStub); + + $response = $this->gateway->hipLinkToken(); + $body = json_decode($response->getBody(), true); + + $this->assertEquals(1, $body['ok'] ?? 0, 'Expected hipLinkToken to succeed with auto-resolved demographics'); + $this->assertEquals('Keshav Singh', $lastPayload['name'] ?? ''); + $this->assertEquals('M', $lastPayload['gender'] ?? ''); + $this->assertEquals(1976, $lastPayload['year_of_birth'] ?? 0); + $this->assertEquals('singhkeshav301976@sbx', $lastPayload['abha_address'] ?? ''); + $this->assertEquals('91747451787143', $lastPayload['abha_number'] ?? ''); + } + + public function testHipLinkCareContextAutoDemographicsResolution(): void + { + // Call hipLinkCareContext with ONLY patient_id, link_token_id, and care_contexts + $_POST = [ + 'patient_id' => 12, + 'link_token_id' => 'dummy-token-123', + 'care_contexts' => [ + ['referenceNumber' => 'OPD-12-30', 'display' => 'OPD Consultation'] + ], + ]; + $req = \Config\Services::request(); + $this->gateway->initController($req, \Config\Services::response(), \Config\Services::logger()); + + $lastPayload = []; + $connectorStub = $this->getMockBuilder(EAtriaBridgeConnector::class) + ->disableOriginalConstructor() + ->onlyMethods(['hipLinkCareContext']) + ->getMock(); + + $connectorStub->expects($this->once()) + ->method('hipLinkCareContext') + ->willReturnCallback(function ($body) use (&$lastPayload) { + $lastPayload = $body; + return ['ok' => 1, 'linked' => true]; + }); + + $refClass = new ReflectionClass($this->gateway); + $connProp = $refClass->getProperty('connector'); + $connProp->setAccessible(true); + $connProp->setValue($this->gateway, $connectorStub); + + $response = $this->gateway->hipLinkCareContext(); + $body = json_decode($response->getBody(), true); + + $this->assertEquals(1, $body['ok'] ?? 0, 'Expected hipLinkCareContext to succeed with auto-resolved demographics'); + $this->assertEquals('singhkeshav301976@sbx', $lastPayload['abha_address'] ?? ''); + $this->assertEquals('P12', $lastPayload['patient_ref'] ?? ''); + $this->assertEquals('Keshav Singh', $lastPayload['display'] ?? ''); + $this->assertEquals('91747451787143', $lastPayload['abha_number'] ?? ''); + } +}