3986 lines
180 KiB
PHP
3986 lines
180 KiB
PHP
<?php
|
|
|
|
namespace App\Controllers;
|
|
|
|
use App\Libraries\Abdm\AbdmConnectorFactory;
|
|
|
|
/**
|
|
* Abha Controller
|
|
*
|
|
* Handles ABHA number creation wizard endpoints called from billing/patient ABHA Create tab.
|
|
* All methods proxy calls to the ABDM gateway via AbdmConnectorFactory.
|
|
*/
|
|
class Abha extends BaseController
|
|
{
|
|
// -------------------------------------------------------------------------
|
|
// Step 1 — Initiate: send Aadhaar OTP
|
|
// POST abha/create/initiate
|
|
// -------------------------------------------------------------------------
|
|
public function initiate()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$aadhaar = preg_replace('/\D/', '', trim((string) ($this->request->getPost('aadhaar') ?? '')));
|
|
$authType = trim((string) ($this->request->getPost('auth_type') ?? 'aadhaar_otp'));
|
|
$consent = $this->request->getPost('consent');
|
|
$beneficiaryName = trim((string) ($this->request->getPost('beneficiary_name') ?? ''));
|
|
|
|
if (strlen($aadhaar) !== 12 || ! $this->validateVerhoeff($aadhaar)) {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Aadhaar Number is not valid. Valid 12-digit Aadhaar number is required.']);
|
|
}
|
|
|
|
if ($consent !== null && ! in_array($consent, ['1', 1, 'true', true], true)) {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Consent is mandatory. Please agree to the consent declarations before sending the OTP.']);
|
|
}
|
|
|
|
try {
|
|
$result = AbdmConnectorFactory::make()->abhaAadhaarGenerateOtp(['aadhaar' => $aadhaar]);
|
|
} catch (\Throwable $e) {
|
|
return $this->response->setStatusCode(500)->setJSON(['ok' => 0, 'error_text' => $e->getMessage()]);
|
|
}
|
|
|
|
// Normalise for the wizard: expose txn_id at top level
|
|
if (! empty($result['ok']) && $result['ok'] == 1) {
|
|
$txnId = $result['txn_id'] ?? $result['data']['txnId'] ?? $result['data']['txn_id'] ?? null;
|
|
// Cache the Aadhaar number against this txn_id so verifyOtp() can use it
|
|
// later for local-patient name/age/gender/aadhaar matching (never sent to browser).
|
|
if ($txnId) {
|
|
session()->set('abha_aadhaar_txn_' . $txnId, $aadhaar);
|
|
if ($beneficiaryName !== '') {
|
|
session()->set('abha_beneficiary_' . $txnId, $beneficiaryName);
|
|
}
|
|
}
|
|
return $this->response->setJSON([
|
|
'ok' => 1,
|
|
'txn_id' => $txnId,
|
|
'message' => (string) ($result['message'] ?? $result['data']['message'] ?? 'OTP sent to Aadhaar-linked mobile.'),
|
|
'masked_mobile' => (string) ($result['masked_mobile'] ?? $result['data']['masked_mobile'] ?? $result['data']['maskedMobile'] ?? ''),
|
|
'request_id' => (string) ($result['request_id'] ?? ''),
|
|
]);
|
|
}
|
|
|
|
$errorMsg = $this->extractBridgeErrorText($result, 'Failed to send OTP');
|
|
if (stripos($errorMsg, 'loginId') !== false || stripos($errorMsg, 'Invalid LoginId') !== false) {
|
|
$errorMsg = 'Aadhaar Number is not valid. Valid 12-digit Aadhaar number is required.';
|
|
}
|
|
|
|
return $this->response->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => $errorMsg,
|
|
'request_id' => (string) ($result['request_id'] ?? ''),
|
|
]);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Step 2 — Verify Aadhaar OTP
|
|
// POST abha/create/verify_otp
|
|
// Response includes skip_mobile=true and abha_number if ABHA already linked.
|
|
// -------------------------------------------------------------------------
|
|
public function verifyOtp()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$txnId = trim((string) ($this->request->getPost('txn_id') ?? $this->request->getPost('txnId') ?? ''));
|
|
$otp = trim((string) ($this->request->getPost('otp') ?? ''));
|
|
$requestMobile = trim((string) ($this->request->getPost('mobile') ?? ''));
|
|
|
|
if ($txnId === '' || $otp === '') {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'txn_id and otp are required']);
|
|
}
|
|
|
|
try {
|
|
$result = AbdmConnectorFactory::make()->abhaAadhaarVerifyOtp(['txnId' => $txnId, 'otp' => $otp, 'mobile' => $requestMobile]);
|
|
} catch (\Throwable $e) {
|
|
return $this->response->setStatusCode(500)->setJSON(['ok' => 0, 'error_text' => $e->getMessage()]);
|
|
}
|
|
|
|
$authResult = strtolower(trim((string) ($result['authResult'] ?? $result['data']['authResult'] ?? $result['auth_result'] ?? $result['data']['auth_result'] ?? '')));
|
|
$status = strtolower(trim((string) ($result['status'] ?? $result['data']['status'] ?? '')));
|
|
if ($authResult === 'failed' || in_array($status, ['failed', 'failure', 'error'], true)) {
|
|
$result['ok'] = 0;
|
|
}
|
|
|
|
if (empty($result['ok']) || $result['ok'] != 1) {
|
|
$requestId = trim((string) ($result['request_id'] ?? ''));
|
|
$errorText = $this->extractBridgeErrorText($result, 'Incorrect OTP');
|
|
return $this->response->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => $errorText,
|
|
'request_id' => $requestId,
|
|
]);
|
|
}
|
|
// Keep top-level keys (gateway_patient, official_card) that also appear under data.
|
|
$payload = $result;
|
|
if (is_array($result['data'] ?? null)) {
|
|
foreach ($result['data'] as $key => $value) {
|
|
if (! array_key_exists($key, $payload)) {
|
|
$payload[$key] = $value;
|
|
}
|
|
}
|
|
}
|
|
$newTxnId = $payload['txnId'] ?? $payload['txn_id'] ?? $txnId;
|
|
|
|
$profile = $this->pickGatewayAbhaProfile($payload);
|
|
|
|
$abhaNum = (string) ($profile['ABHANumber'] ?? $profile['abha_id'] ?? $payload['ABHANumber'] ?? $payload['abha_id'] ?? '');
|
|
$aadhaarVerifiedAbha = preg_replace('/\D/', '', $abhaNum);
|
|
// ABDM v3 creates/returns the ABHA inside Aadhaar verify-otp when an alternate
|
|
// mobile is supplied, so no further mobile OTP step may run for this person.
|
|
$abhaCreated = strlen($aadhaarVerifiedAbha) === 14;
|
|
if ($newTxnId && $abhaCreated) {
|
|
session()->set('abha_enrol_identity_' . $newTxnId, $aadhaarVerifiedAbha);
|
|
}
|
|
$abhaAddress = (string) (
|
|
($profile['preferredAddress'] ?? '')
|
|
?: ($profile['preferredAbhaAddress'] ?? '')
|
|
?: ($profile['abha_address'] ?? '')
|
|
?: ($payload['preferredAddress'] ?? '')
|
|
?: ($payload['preferredAbhaAddress'] ?? '')
|
|
?: ($payload['abha_address'] ?? '')
|
|
?: ''
|
|
);
|
|
$name = $this->extractAbhaProfileName($profile, $payload);
|
|
$photo = $this->extractAbhaProfilePhoto($profile, $payload);
|
|
$mobile = (string) ($profile['mobile'] ?? $payload['mobile'] ?? $payload['mobileNumber'] ?? '');
|
|
$mobileSource = $mobile === '' ? '' : 'abdm';
|
|
$enrolMobile = preg_replace('/\D/', '', $requestMobile);
|
|
// ABDM accepted this number while creating the ABHA but does not always echo it back.
|
|
if ($mobile === '' && $abhaCreated && strlen($enrolMobile) === 10) {
|
|
$mobile = $enrolMobile;
|
|
$mobileSource = 'enrolment';
|
|
}
|
|
$profileGender = (string) ($profile['gender'] ?? $payload['gender'] ?? '');
|
|
$profileDob = (string) ($profile['dob'] ?? $profile['date_of_birth'] ?? $payload['dob'] ?? $payload['date_of_birth'] ?? '');
|
|
$verifiedStatus = (string) (($payload['gateway_abha_profile']['status'] ?? '') ?: ($profile['verifiedStatus'] ?? '') ?: ($profile['status'] ?? '') ?: ($payload['verifiedStatus'] ?? '') ?: ($payload['status'] ?? ''));
|
|
$verificationType = (string) (($payload['gateway_abha_profile']['abha_type'] ?? '') ?: ($profile['verificationType'] ?? '') ?: ($profile['abhaType'] ?? '') ?: ($payload['verificationType'] ?? '') ?: ($payload['abhaType'] ?? ''));
|
|
$kycVerified = $profile['kycVerified'] ?? $payload['kycVerified'] ?? null;
|
|
$mobileVerified = $profile['mobileVerified'] ?? $payload['mobileVerified'] ?? null;
|
|
$address = (string) (($profile['address'] ?? '') ?: ($profile['address_line'] ?? '') ?: ($payload['address'] ?? '') ?: ($payload['address_line'] ?? '') ?: ($payload['gateway_abha_profile']['address'] ?? '') ?: ($payload['gateway_patient']['address_line'] ?? ''));
|
|
$zip = (string) (($profile['pinCode'] ?? '') ?: ($profile['pin_code'] ?? '') ?: ($payload['pinCode'] ?? '') ?: ($payload['pin_code'] ?? '') ?: ($payload['gateway_abha_profile']['pin_code'] ?? '') ?: ($payload['gateway_patient']['pincode'] ?? ''));
|
|
$stateName = (string) (($profile['stateName'] ?? '') ?: ($profile['state_name'] ?? '') ?: ($payload['stateName'] ?? '') ?: ($payload['state_name'] ?? '') ?: ($payload['gateway_abha_profile']['state_name'] ?? '') ?: ($payload['gateway_patient']['state_name'] ?? ''));
|
|
$districtName = (string) (($profile['districtName'] ?? '') ?: ($profile['district_name'] ?? '') ?: ($payload['districtName'] ?? '') ?: ($payload['district_name'] ?? '') ?: ($payload['gateway_abha_profile']['district_name'] ?? '') ?: ($payload['gateway_patient']['district'] ?? ''));
|
|
$email = (string) (($profile['email'] ?? '') ?: ($payload['email'] ?? '') ?: ($payload['gateway_abha_profile']['email'] ?? '') ?: ($payload['gateway_patient']['email'] ?? ''));
|
|
|
|
$abhaMeta = [
|
|
'abha_address' => $abhaAddress,
|
|
'profile_photo' => $photo,
|
|
'verified_status' => $verifiedStatus,
|
|
'verification_type' => $verificationType,
|
|
'kyc_verified' => $kycVerified,
|
|
'mobile_verified' => $mobileVerified,
|
|
'address' => $address,
|
|
'district' => $districtName,
|
|
'state' => $stateName,
|
|
'zip' => $zip,
|
|
'email' => $email,
|
|
];
|
|
|
|
// Aadhaar cached against this txn_id in initiate(), used only for local
|
|
// duplicate-patient matching below (never persisted/echoed to the browser).
|
|
$aadhaarForMatch = trim((string) (session()->get('abha_aadhaar_txn_' . $txnId) ?? ''));
|
|
session()->remove('abha_aadhaar_txn_' . $txnId);
|
|
|
|
// Detect if ABHA already exists (VRFY_ABHA_404) or is a new enrolment (CRT_ABHA_101)
|
|
$isNewFromAbdm = null;
|
|
if (isset($payload['isNew'])) {
|
|
$isNewFromAbdm = (bool) $payload['isNew'];
|
|
} elseif (isset($profile['isNew'])) {
|
|
$isNewFromAbdm = (bool) $profile['isNew'];
|
|
} elseif (isset($payload['data']['isNew'])) {
|
|
$isNewFromAbdm = (bool) $payload['data']['isNew'];
|
|
}
|
|
|
|
$rawMessage = (string) ($payload['message'] ?? $result['message'] ?? '');
|
|
$hasAlreadyExistMsg = stripos($rawMessage, 'already exist') !== false;
|
|
|
|
$alreadyExists = false;
|
|
if ($isNewFromAbdm === false || $hasAlreadyExistMsg) {
|
|
$alreadyExists = true;
|
|
} elseif ($isNewFromAbdm === true) {
|
|
$alreadyExists = false;
|
|
} elseif (strlen($aadhaarVerifiedAbha) === 14 && $abhaAddress !== '') {
|
|
$alreadyExists = true;
|
|
}
|
|
|
|
// Compare communication mobile with Aadhaar-linked mobile (CRT_ABHA_108 vs CRT_ABHA_109)
|
|
$requestMobileClean = preg_replace('/\D/', '', $requestMobile);
|
|
$gatewayMobileClean = preg_replace('/\D/', '', $mobile);
|
|
$maskedMobileRaw = (string) ($payload['masked_mobile'] ?? $payload['maskedMobile'] ?? $result['masked_mobile'] ?? $result['data']['masked_mobile'] ?? '');
|
|
$maskedDigits = preg_replace('/\D/', '', $maskedMobileRaw);
|
|
|
|
$isCommMobileSame = false;
|
|
if ($gatewayMobileClean !== '' && strlen($gatewayMobileClean) === 10 && strlen($requestMobileClean) === 10) {
|
|
$isCommMobileSame = ($gatewayMobileClean === $requestMobileClean);
|
|
} elseif (strlen($requestMobileClean) === 10 && strlen($gatewayMobileClean) >= 4) {
|
|
$isCommMobileSame = (substr($requestMobileClean, -4) === substr($gatewayMobileClean, -4));
|
|
} elseif (strlen($requestMobileClean) === 10 && strlen($maskedDigits) >= 4) {
|
|
$isCommMobileSame = (substr($requestMobileClean, -4) === substr($maskedDigits, -4));
|
|
}
|
|
|
|
if ($newTxnId) {
|
|
session()->set('abha_already_exists_' . $newTxnId, $alreadyExists);
|
|
session()->set('abha_existing_number_' . $newTxnId, $abhaNum);
|
|
session()->set('abha_comm_mobile_' . $newTxnId, $requestMobileClean);
|
|
}
|
|
|
|
$patientInfo = $this->tryAutoLinkByDirectMatch($abhaNum, $name, $mobile, $profileGender, $profileDob, $abhaMeta);
|
|
|
|
$responseBase = [
|
|
'ok' => 1,
|
|
'txn_id' => $newTxnId,
|
|
'skip_mobile' => $isCommMobileSame,
|
|
'abha_created' => $abhaCreated,
|
|
'already_exists' => $alreadyExists,
|
|
'is_new_abha' => ! $alreadyExists,
|
|
'is_comm_mobile_same' => $isCommMobileSame,
|
|
'comm_mobile' => $requestMobileClean,
|
|
'aadhaar_mobile_masked' => $maskedMobileRaw ?: ($maskedDigits ? ('******' . $maskedDigits) : ''),
|
|
'card_base64' => $this->extractAbhaCardData($result),
|
|
'card_content_type' => $this->resolveAbhaCardContentType($result),
|
|
'card_source' => $this->resolveAbhaCardSource($result),
|
|
'card_message' => $this->resolveAbhaCardMessage($result),
|
|
'abha_number' => $abhaNum,
|
|
'name' => $name,
|
|
'photo' => $photo,
|
|
'mobile' => $mobile,
|
|
'mobile_source' => $mobileSource,
|
|
'gender' => $profileGender,
|
|
'dob' => $profileDob,
|
|
'abha_address' => $abhaAddress,
|
|
'verified_status' => $verifiedStatus,
|
|
'verification_type' => $verificationType,
|
|
'kyc_verified' => $kycVerified,
|
|
'mobile_verified' => $mobileVerified,
|
|
'address' => $address,
|
|
'district' => $districtName,
|
|
'state' => $stateName,
|
|
'zip' => $zip,
|
|
'email' => $email,
|
|
];
|
|
|
|
if ($patientInfo !== null) {
|
|
return $this->response->setJSON($responseBase + [
|
|
'need_confirmation' => false,
|
|
'patient_id' => $patientInfo['patient_id'],
|
|
'p_code' => $patientInfo['p_code'],
|
|
'is_new_patient' => $patientInfo['is_new'],
|
|
]);
|
|
}
|
|
|
|
$db = \Config\Database::connect();
|
|
$fields = $db->getFieldNames('patient_master') ?? [];
|
|
$abhaField = $this->resolveAbhaFieldName($fields);
|
|
$abhaNumClean = preg_replace('/\D/', '', $abhaNum);
|
|
$candidates = $this->findMatchingCandidates($db, $fields, $name, $mobile, $profileGender, $profileDob, $aadhaarForMatch, $abhaField, $abhaNumClean);
|
|
|
|
return $this->response->setJSON($responseBase + [
|
|
'need_confirmation' => true,
|
|
'patient_id' => 0,
|
|
'p_code' => '',
|
|
'is_new_patient' => null,
|
|
'candidates' => $candidates,
|
|
]);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Step 3a — Send mobile OTP
|
|
// POST abha/create/communication
|
|
// -------------------------------------------------------------------------
|
|
public function communication()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$mobile = preg_replace('/\D/', '', trim((string) ($this->request->getPost('mobile') ?? '')));
|
|
$txnId = trim((string) ($this->request->getPost('txn_id') ?? $this->request->getPost('txnId') ?? ''));
|
|
$isMobileDiscovery = (string) $this->request->getPost('find_abha') === '1';
|
|
|
|
if (strlen($mobile) !== 10) {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Valid 10-digit mobile number is required']);
|
|
}
|
|
|
|
if (! $isMobileDiscovery && $txnId === '') {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Aadhaar enrolment txn_id is required']);
|
|
}
|
|
|
|
try {
|
|
$result = $isMobileDiscovery
|
|
? AbdmConnectorFactory::make()->abhaMobileGenerateOtp(['mobile' => $mobile])
|
|
: AbdmConnectorFactory::make()->abhaEnrolMobileRequestOtp(['mobile' => $mobile, 'txnId' => $txnId]);
|
|
} catch (\Throwable $e) {
|
|
return $this->response->setStatusCode(500)->setJSON(['ok' => 0, 'error_text' => $e->getMessage()]);
|
|
}
|
|
|
|
if (! empty($result['ok']) && $result['ok'] == 1) {
|
|
$newTxnId = $result['txn_id'] ?? $result['data']['txnId'] ?? $result['data']['txn_id'] ?? $txnId;
|
|
$expectedAbha = (string) (session()->get('abha_enrol_identity_' . $txnId) ?? '');
|
|
if ($expectedAbha !== '') {
|
|
session()->set('abha_enrol_identity_' . $newTxnId, $expectedAbha);
|
|
}
|
|
$existingFlag = session()->get('abha_already_exists_' . $txnId);
|
|
if ($existingFlag !== null) {
|
|
session()->set('abha_already_exists_' . $newTxnId, (bool) $existingFlag);
|
|
}
|
|
$existingNum = session()->get('abha_existing_number_' . $txnId);
|
|
if ($existingNum !== null) {
|
|
session()->set('abha_existing_number_' . $newTxnId, $existingNum);
|
|
}
|
|
session()->set('abha_comm_mobile_' . $newTxnId, $mobile);
|
|
|
|
return $this->response->setJSON([
|
|
'ok' => 1,
|
|
'txn_id' => $newTxnId,
|
|
'message' => (string) ($result['message'] ?? $result['data']['message'] ?? 'OTP sent to alternate mobile.'),
|
|
'masked_mobile' => (string) ($result['masked_mobile'] ?? $result['data']['masked_mobile'] ?? ('******' . substr($mobile, -4))),
|
|
'request_id' => (string) ($result['request_id'] ?? ''),
|
|
]);
|
|
}
|
|
|
|
return $this->response->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => $this->extractBridgeErrorText($result, 'Failed to send mobile OTP'),
|
|
'request_id' => (string) ($result['request_id'] ?? ''),
|
|
]);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Step 3b — Verify mobile OTP
|
|
// POST abha/create/verify_comm_otp
|
|
// -------------------------------------------------------------------------
|
|
public function verifyCommOtp()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$txnId = trim((string) ($this->request->getPost('txn_id') ?? $this->request->getPost('txnId') ?? ''));
|
|
$otp = trim((string) ($this->request->getPost('otp') ?? ''));
|
|
$isMobileDiscovery = (string) $this->request->getPost('find_abha') === '1';
|
|
// The mobile itself isn't part of the OTP-verify payload (it's tied to txn_id
|
|
// on the bridge side), but the caller may resend it so we can persist it even
|
|
// if the gateway response doesn't echo it back.
|
|
$requestMobile = trim((string) ($this->request->getPost('mobile') ?? ''));
|
|
if ($requestMobile === '') {
|
|
$requestMobile = (string) (session()->get('abha_comm_mobile_' . $txnId) ?? '');
|
|
}
|
|
|
|
if ($txnId === '' || $otp === '') {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'txn_id and otp are required']);
|
|
}
|
|
|
|
try {
|
|
$result = $isMobileDiscovery
|
|
? AbdmConnectorFactory::make()->abhaMobileVerifyOtp(['txnId' => $txnId, 'otp' => $otp, 'mobile' => $requestMobile])
|
|
: AbdmConnectorFactory::make()->abhaEnrolMobileVerifyOtp(['txnId' => $txnId, 'otp' => $otp, 'mobile' => $requestMobile]);
|
|
} catch (\Throwable $e) {
|
|
return $this->response->setStatusCode(500)->setJSON(['ok' => 0, 'error_text' => $e->getMessage()]);
|
|
}
|
|
|
|
if (empty($result['ok']) || $result['ok'] != 1) {
|
|
return $this->response->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => $this->extractBridgeErrorText($result, 'Mobile OTP verification failed'),
|
|
'request_id' => (string) ($result['request_id'] ?? ''),
|
|
]);
|
|
}
|
|
|
|
// Keep top-level keys (gateway_patient, official_card) that also appear under data.
|
|
$payload = $result;
|
|
if (is_array($result['data'] ?? null)) {
|
|
foreach ($result['data'] as $key => $value) {
|
|
if (! array_key_exists($key, $payload)) {
|
|
$payload[$key] = $value;
|
|
}
|
|
}
|
|
}
|
|
|
|
$profile = $this->pickGatewayAbhaProfile($payload);
|
|
|
|
$abhaNum = (string) ($profile['ABHANumber'] ?? $profile['abha_id'] ?? $payload['ABHANumber'] ?? $payload['abha_id'] ?? '');
|
|
$expectedAbha = preg_replace('/\D/', '', (string) (session()->get('abha_enrol_identity_' . $txnId) ?? ''));
|
|
$returnedAbha = preg_replace('/\D/', '', $abhaNum);
|
|
if (! $isMobileDiscovery && $expectedAbha !== '' && (strlen($expectedAbha) !== 14 || ($returnedAbha !== '' && $returnedAbha !== $expectedAbha))) {
|
|
log_message('error', '[ABHA] Enrolment identity mismatch blocked for txn ' . $txnId);
|
|
return $this->response->setStatusCode(409)->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => 'ABHA identity mismatch. The alternate-mobile response did not match the Aadhaar-verified person and was rejected.',
|
|
'request_id' => (string) ($result['request_id'] ?? ''),
|
|
]);
|
|
}
|
|
if (! $isMobileDiscovery) {
|
|
session()->remove('abha_enrol_identity_' . $txnId);
|
|
}
|
|
$abhaAddress = (string) (
|
|
($profile['preferredAddress'] ?? '')
|
|
?: ($profile['preferredAbhaAddress'] ?? '')
|
|
?: ($profile['abha_address'] ?? '')
|
|
?: ($payload['preferredAddress'] ?? '')
|
|
?: ($payload['preferredAbhaAddress'] ?? '')
|
|
?: ($payload['abha_address'] ?? '')
|
|
?: ''
|
|
);
|
|
$name = $this->extractAbhaProfileName($profile, $payload);
|
|
$photo = $this->extractAbhaProfilePhoto($profile, $payload);
|
|
$gender = (string) ($profile['gender'] ?? $payload['gender'] ?? '');
|
|
$dob = (string) ($profile['dob'] ?? $profile['date_of_birth'] ?? $payload['dob'] ?? $payload['date_of_birth'] ?? '');
|
|
// Bridge/gateway verify-otp responses don't always echo the mobile back;
|
|
// fall back to the mobile the user actually typed and OTP-verified.
|
|
$mobile = (string) ($profile['mobile'] ?? $payload['mobile'] ?? $payload['mobileNumber'] ?? $requestMobile ?? '');
|
|
$verifiedStatus = (string) (($payload['gateway_abha_profile']['status'] ?? '') ?: ($profile['verifiedStatus'] ?? '') ?: ($profile['status'] ?? '') ?: ($payload['verifiedStatus'] ?? '') ?: ($payload['status'] ?? ''));
|
|
$verificationType = (string) (($payload['gateway_abha_profile']['abha_type'] ?? '') ?: ($profile['verificationType'] ?? '') ?: ($profile['abhaType'] ?? '') ?: ($payload['verificationType'] ?? '') ?: ($payload['abhaType'] ?? ''));
|
|
$kycVerified = $profile['kycVerified'] ?? $payload['kycVerified'] ?? null;
|
|
$mobileVerified = $profile['mobileVerified'] ?? $payload['mobileVerified'] ?? null;
|
|
$address = (string) (($profile['address'] ?? '') ?: ($profile['address_line'] ?? '') ?: ($payload['address'] ?? '') ?: ($payload['address_line'] ?? '') ?: ($payload['gateway_abha_profile']['address'] ?? '') ?: ($payload['gateway_patient']['address_line'] ?? ''));
|
|
$zip = (string) (($profile['pinCode'] ?? '') ?: ($profile['pin_code'] ?? '') ?: ($payload['pinCode'] ?? '') ?: ($payload['pin_code'] ?? '') ?: ($payload['gateway_abha_profile']['pin_code'] ?? '') ?: ($payload['gateway_patient']['pincode'] ?? ''));
|
|
$stateName = (string) (($profile['stateName'] ?? '') ?: ($profile['state_name'] ?? '') ?: ($payload['stateName'] ?? '') ?: ($payload['state_name'] ?? '') ?: ($payload['gateway_abha_profile']['state_name'] ?? '') ?: ($payload['gateway_patient']['state_name'] ?? ''));
|
|
$districtName = (string) (($profile['districtName'] ?? '') ?: ($profile['district_name'] ?? '') ?: ($payload['districtName'] ?? '') ?: ($payload['district_name'] ?? '') ?: ($payload['gateway_abha_profile']['district_name'] ?? '') ?: ($payload['gateway_patient']['district'] ?? ''));
|
|
$email = (string) (($profile['email'] ?? '') ?: ($payload['email'] ?? '') ?: ($payload['gateway_abha_profile']['email'] ?? '') ?: ($payload['gateway_patient']['email'] ?? ''));
|
|
|
|
$abhaMeta = [
|
|
'abha_address' => $abhaAddress,
|
|
'profile_photo' => $photo,
|
|
'verified_status' => $verifiedStatus,
|
|
'verification_type' => $verificationType,
|
|
'kyc_verified' => $kycVerified,
|
|
'mobile_verified' => $mobileVerified,
|
|
'address' => $address,
|
|
'district' => $districtName,
|
|
'state' => $stateName,
|
|
'zip' => $zip,
|
|
'email' => $email,
|
|
];
|
|
|
|
$rawAccounts = is_array($payload['accounts'] ?? null)
|
|
? $payload['accounts']
|
|
: (is_array($result['data']['accounts'] ?? null) ? $result['data']['accounts'] : []);
|
|
$txnIdOut = (string) ($payload['txnId'] ?? $payload['txn_id'] ?? $result['txn_id'] ?? $result['data']['txnId'] ?? $txnId);
|
|
$tokenOut = (string) ($payload['token'] ?? $result['data']['token'] ?? $result['token'] ?? '');
|
|
|
|
$savedExistingNum = (string) (session()->get('abha_existing_number_' . $txnId) ?? '');
|
|
if ($abhaNum === '' && $savedExistingNum !== '') {
|
|
$abhaNum = $savedExistingNum;
|
|
}
|
|
$alreadyExists = (bool) (session()->get('abha_already_exists_' . $txnId) ?? false);
|
|
if (! $alreadyExists && strlen(preg_replace('/\D/', '', $abhaNum)) === 14 && $abhaAddress !== '') {
|
|
$alreadyExists = true;
|
|
}
|
|
|
|
$patientInfo = $this->tryAutoLinkByDirectMatch($abhaNum, $name, $mobile, $gender, $dob, $abhaMeta);
|
|
|
|
$responseBase = [
|
|
'ok' => 1,
|
|
'txn_id' => $txnIdOut,
|
|
'token' => $tokenOut,
|
|
'accounts' => $rawAccounts,
|
|
'already_exists' => $alreadyExists,
|
|
'is_new_abha' => ! $alreadyExists,
|
|
'abha_created' => (strlen(preg_replace('/\D/', '', $abhaNum)) === 14),
|
|
'card_base64' => $this->extractAbhaCardData($result),
|
|
'card_content_type' => $this->resolveAbhaCardContentType($result),
|
|
'card_source' => $this->resolveAbhaCardSource($result),
|
|
'card_message' => $this->resolveAbhaCardMessage($result),
|
|
'abha_number' => $abhaNum,
|
|
'name' => $name,
|
|
'photo' => $photo,
|
|
'mobile' => $mobile,
|
|
'mobile_source' => 'communication_verified',
|
|
'comm_mobile' => $mobile,
|
|
'gender' => $gender,
|
|
'dob' => $dob,
|
|
'abha_address' => $abhaAddress,
|
|
'verified_status' => $verifiedStatus,
|
|
'verification_type' => $verificationType,
|
|
'kyc_verified' => $kycVerified,
|
|
'mobile_verified' => $mobileVerified,
|
|
'address' => $address,
|
|
'district' => $districtName,
|
|
'state' => $stateName,
|
|
'zip' => $zip,
|
|
'email' => $email,
|
|
];
|
|
|
|
if ($patientInfo !== null) {
|
|
return $this->response->setJSON($responseBase + [
|
|
'need_confirmation' => false,
|
|
'patient_id' => $patientInfo['patient_id'],
|
|
'p_code' => $patientInfo['p_code'],
|
|
'is_new_patient' => $patientInfo['is_new'],
|
|
]);
|
|
}
|
|
|
|
$db = \Config\Database::connect();
|
|
$fields = $db->getFieldNames('patient_master') ?? [];
|
|
$abhaField = $this->resolveAbhaFieldName($fields);
|
|
$abhaNumClean = preg_replace('/\D/', '', $abhaNum);
|
|
// No Aadhaar available on the pure mobile-OTP path.
|
|
$candidates = $this->findMatchingCandidates($db, $fields, $name, $mobile, $gender, $dob, '', $abhaField, $abhaNumClean);
|
|
|
|
return $this->response->setJSON($responseBase + [
|
|
'need_confirmation' => true,
|
|
'patient_id' => 0,
|
|
'p_code' => '',
|
|
'is_new_patient' => null,
|
|
'candidates' => $candidates,
|
|
]);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Multi-Account Selection for Mobile OTP (ABDM M1)
|
|
// POST abha/find/mobile/select-account
|
|
// -------------------------------------------------------------------------
|
|
public function selectAccount()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$txnId = trim((string) ($this->request->getPost('txn_id') ?? $this->request->getPost('txnId') ?? ''));
|
|
$token = trim((string) ($this->request->getPost('token') ?? ''));
|
|
$abhaNumber = trim((string) ($this->request->getPost('abha_number') ?? $this->request->getPost('abhaNumber') ?? ''));
|
|
$abhaAddress = trim((string) ($this->request->getPost('abha_address') ?? $this->request->getPost('abhaAddress') ?? ''));
|
|
$mobile = trim((string) ($this->request->getPost('mobile') ?? ''));
|
|
|
|
$accountRaw = $this->request->getPost('account');
|
|
$clientAccount = [];
|
|
if (is_string($accountRaw) && trim($accountRaw) !== '') {
|
|
$decoded = json_decode(trim($accountRaw), true);
|
|
if (is_array($decoded)) {
|
|
$clientAccount = $decoded;
|
|
}
|
|
} elseif (is_array($accountRaw)) {
|
|
$clientAccount = $accountRaw;
|
|
}
|
|
|
|
if ($abhaNumber === '' && ! empty($clientAccount)) {
|
|
$abhaNumber = (string) ($clientAccount['ABHANumber'] ?? $clientAccount['abhaNumber'] ?? $clientAccount['abha_id'] ?? '');
|
|
}
|
|
if ($abhaAddress === '' && ! empty($clientAccount)) {
|
|
$abhaAddress = (string) ($clientAccount['preferredAbhaAddress'] ?? $clientAccount['preferredAddress'] ?? $clientAccount['abhaAddress'] ?? $clientAccount['abha_address'] ?? '');
|
|
}
|
|
|
|
$result = [];
|
|
try {
|
|
$result = AbdmConnectorFactory::make()->abhaLoginSelectAccount([
|
|
'txnId' => $txnId,
|
|
'token' => $token,
|
|
'abha_number' => $abhaNumber,
|
|
'abha_address' => $abhaAddress,
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
log_message('warning', '[ABHA] Bridge selectAccount exception: ' . $e->getMessage());
|
|
}
|
|
|
|
$payload = [];
|
|
if (! empty($result['ok']) && (int) $result['ok'] === 1) {
|
|
$payload = $result;
|
|
if (is_array($result['data'] ?? null)) {
|
|
foreach ($result['data'] as $key => $value) {
|
|
if (! array_key_exists($key, $payload)) {
|
|
$payload[$key] = $value;
|
|
}
|
|
}
|
|
}
|
|
} elseif (! empty($clientAccount) || $abhaNumber !== '') {
|
|
// Graceful fallback to the already OTP-verified account profile from accounts list
|
|
log_message('notice', '[ABHA] Using verified account data fallback for ABHA ' . $abhaNumber);
|
|
$payload = [
|
|
'ok' => 1,
|
|
'ABHANumber' => $clientAccount['ABHANumber'] ?? $clientAccount['abhaNumber'] ?? $clientAccount['abha_id'] ?? $abhaNumber,
|
|
'preferredAbhaAddress' => $clientAccount['preferredAbhaAddress'] ?? $clientAccount['preferredAddress'] ?? $clientAccount['abhaAddress'] ?? $clientAccount['abha_address'] ?? $abhaAddress,
|
|
'name' => $clientAccount['name'] ?? '',
|
|
'gender' => $clientAccount['gender'] ?? '',
|
|
'dob' => $clientAccount['dob'] ?? $clientAccount['date_of_birth'] ?? '',
|
|
'profilePhoto' => $clientAccount['profilePhoto'] ?? $clientAccount['photo'] ?? '',
|
|
'mobile' => $mobile,
|
|
'status' => $clientAccount['status'] ?? 'ACTIVE',
|
|
];
|
|
} else {
|
|
return $this->response->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => $this->extractBridgeErrorText($result, 'Account selection failed'),
|
|
'request_id' => (string) ($result['request_id'] ?? ''),
|
|
]);
|
|
}
|
|
|
|
$profile = $this->pickGatewayAbhaProfile($payload);
|
|
$abhaNum = (string) ($profile['ABHANumber'] ?? $profile['abha_id'] ?? $payload['ABHANumber'] ?? $payload['abha_id'] ?? $abhaNumber);
|
|
$resAbhaAddress = (string) (
|
|
($profile['preferredAddress'] ?? '')
|
|
?: ($profile['preferredAbhaAddress'] ?? '')
|
|
?: ($profile['abha_address'] ?? '')
|
|
?: ($payload['preferredAddress'] ?? '')
|
|
?: ($payload['preferredAbhaAddress'] ?? '')
|
|
?: ($payload['abha_address'] ?? '')
|
|
?: $abhaAddress
|
|
);
|
|
$name = $this->extractAbhaProfileName($profile, $payload);
|
|
$photo = $this->extractAbhaProfilePhoto($profile, $payload);
|
|
$gender = (string) ($profile['gender'] ?? $payload['gender'] ?? '');
|
|
$dob = (string) ($profile['dob'] ?? $profile['date_of_birth'] ?? $payload['dob'] ?? $payload['date_of_birth'] ?? '');
|
|
$resMobile = (string) ($profile['mobile'] ?? $payload['mobile'] ?? $payload['mobileNumber'] ?? $mobile);
|
|
$verifiedStatus = (string) (($payload['gateway_abha_profile']['status'] ?? '') ?: ($profile['verifiedStatus'] ?? '') ?: ($profile['status'] ?? '') ?: ($payload['verifiedStatus'] ?? '') ?: ($payload['status'] ?? ''));
|
|
$verificationType = (string) (($payload['gateway_abha_profile']['abha_type'] ?? '') ?: ($profile['verificationType'] ?? '') ?: ($profile['abhaType'] ?? '') ?: ($payload['verificationType'] ?? '') ?: ($payload['abhaType'] ?? ''));
|
|
$kycVerified = $profile['kycVerified'] ?? $payload['kycVerified'] ?? null;
|
|
$mobileVerified = $profile['mobileVerified'] ?? $payload['mobileVerified'] ?? null;
|
|
$address = (string) (($profile['address'] ?? '') ?: ($profile['address_line'] ?? '') ?: ($payload['address'] ?? '') ?: ($payload['address_line'] ?? '') ?: ($payload['gateway_abha_profile']['address'] ?? '') ?: ($payload['gateway_patient']['address_line'] ?? ''));
|
|
$zip = (string) (($profile['pinCode'] ?? '') ?: ($profile['pin_code'] ?? '') ?: ($payload['pinCode'] ?? '') ?: ($payload['pin_code'] ?? '') ?: ($payload['gateway_abha_profile']['pin_code'] ?? '') ?: ($payload['gateway_patient']['pincode'] ?? ''));
|
|
$stateName = (string) (($profile['stateName'] ?? '') ?: ($profile['state_name'] ?? '') ?: ($payload['stateName'] ?? '') ?: ($payload['state_name'] ?? '') ?: ($payload['gateway_abha_profile']['state_name'] ?? '') ?: ($payload['gateway_patient']['state_name'] ?? ''));
|
|
$districtName = (string) (($profile['districtName'] ?? '') ?: ($profile['district_name'] ?? '') ?: ($payload['districtName'] ?? '') ?: ($payload['district_name'] ?? '') ?: ($payload['gateway_abha_profile']['district_name'] ?? '') ?: ($payload['gateway_patient']['district'] ?? ''));
|
|
$email = (string) (($profile['email'] ?? '') ?: ($payload['email'] ?? '') ?: ($payload['gateway_abha_profile']['email'] ?? '') ?: ($payload['gateway_patient']['email'] ?? ''));
|
|
|
|
$abhaMeta = [
|
|
'abha_address' => $resAbhaAddress,
|
|
'profile_photo' => $photo,
|
|
'verified_status' => $verifiedStatus,
|
|
'verification_type' => $verificationType,
|
|
'kyc_verified' => $kycVerified,
|
|
'mobile_verified' => $mobileVerified,
|
|
'address' => $address,
|
|
'district' => $districtName,
|
|
'state' => $stateName,
|
|
'zip' => $zip,
|
|
'email' => $email,
|
|
];
|
|
|
|
$patientInfo = $this->tryAutoLinkByDirectMatch($abhaNum, $name, $resMobile, $gender, $dob, $abhaMeta);
|
|
|
|
$responseBase = [
|
|
'ok' => 1,
|
|
'card_base64' => $this->extractAbhaCardData($result),
|
|
'card_content_type' => $this->resolveAbhaCardContentType($result),
|
|
'card_source' => $this->resolveAbhaCardSource($result),
|
|
'card_message' => $this->resolveAbhaCardMessage($result),
|
|
'abha_number' => $abhaNum,
|
|
'name' => $name,
|
|
'photo' => $photo,
|
|
'mobile' => $resMobile,
|
|
'gender' => $gender,
|
|
'dob' => $dob,
|
|
'abha_address' => $resAbhaAddress,
|
|
'verified_status' => $verifiedStatus,
|
|
'verification_type' => $verificationType,
|
|
'kyc_verified' => $kycVerified,
|
|
'mobile_verified' => $mobileVerified,
|
|
'address' => $address,
|
|
'district' => $districtName,
|
|
'state' => $stateName,
|
|
'zip' => $zip,
|
|
'email' => $email,
|
|
];
|
|
|
|
if ($patientInfo !== null) {
|
|
return $this->response->setJSON($responseBase + [
|
|
'need_confirmation' => false,
|
|
'patient_id' => $patientInfo['patient_id'],
|
|
'p_code' => $patientInfo['p_code'],
|
|
'is_new_patient' => $patientInfo['is_new'],
|
|
]);
|
|
}
|
|
|
|
$db = \Config\Database::connect();
|
|
$fields = $db->getFieldNames('patient_master') ?? [];
|
|
$abhaField = $this->resolveAbhaFieldName($fields);
|
|
$abhaNumClean = preg_replace('/\D/', '', $abhaNum);
|
|
$candidates = $this->findMatchingCandidates($db, $fields, $name, $resMobile, $gender, $dob, '', $abhaField, $abhaNumClean);
|
|
|
|
return $this->response->setJSON($responseBase + [
|
|
'need_confirmation' => true,
|
|
'patient_id' => 0,
|
|
'p_code' => '',
|
|
'is_new_patient' => null,
|
|
'candidates' => $candidates,
|
|
]);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// ABDM M1 Find ABHA via Mobile Flow
|
|
// FLOW: MOBILE NUMBER -> PROFILES -> OTP -> COMPLETE PROFILE FETCHED
|
|
// -------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Step 1: Search ABHA profiles by 10-digit mobile number
|
|
* POST abha/find/mobile/search
|
|
*/
|
|
public function findMobileSearch()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$mobile = preg_replace('/\D/', '', trim((string) ($this->request->getPost('mobile') ?? '')));
|
|
if (strlen($mobile) !== 10) {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Valid 10-digit mobile number is required']);
|
|
}
|
|
|
|
try {
|
|
$helper = new \App\Libraries\Abdm\AbdmV3DirectHelper();
|
|
$result = $helper->searchByMobile($mobile);
|
|
|
|
if (! empty($result['ok']) && ! empty($result['profiles'])) {
|
|
return $this->response->setJSON([
|
|
'ok' => 1,
|
|
'txn_id' => $result['txn_id'] ?? '',
|
|
'profiles' => $result['profiles'],
|
|
'mobile' => $mobile,
|
|
]);
|
|
}
|
|
|
|
// Fallback: If ABDM Sandbox returns no external profiles, check if HMS patient_master has records with this mobile
|
|
$db = \Config\Database::connect();
|
|
$rows = $db->table('patient_master')
|
|
->groupStart()
|
|
->where('mphone1', $mobile)
|
|
->orWhere('mphone2', $mobile)
|
|
->groupEnd()
|
|
->get()->getResultArray();
|
|
|
|
if (! empty($rows)) {
|
|
$profiles = [];
|
|
$idx = 1;
|
|
foreach ($rows as $p) {
|
|
$pName = trim((string)(($p['fname'] ?? '') . ' ' . ($p['mname'] ?? '') . ' ' . ($p['lname'] ?? '')));
|
|
$pAbha = trim((string)($p['abdm_abha_number'] ?? $p['abha_number'] ?? $p['abdm_abha_id'] ?? ''));
|
|
$maskedAbha = $pAbha !== ''
|
|
? preg_replace('/^(\d{2})(\d{4})(\d{4})(\d{4})$/', 'xx-xxxx-xxxx-$4', preg_replace('/\D/', '', $pAbha))
|
|
: 'xx-xxxx-xxxx-XXXX';
|
|
$profiles[] = [
|
|
'index' => (string) $idx++,
|
|
'abha_number' => $maskedAbha,
|
|
'name' => $pName !== '' ? $pName : 'Patient ' . ($p['p_code'] ?? ''),
|
|
'gender' => strtoupper(substr((string)($p['gender'] ?? 'M'), 0, 1)),
|
|
'dob' => (string)($p['dob'] ?? ''),
|
|
'kyc_verified' => true,
|
|
'auth_methods' => ['MOBILE_OTP', 'AADHAAR_OTP'],
|
|
'profile_photo' => '',
|
|
'local_patient' => true,
|
|
'p_code' => (string)($p['p_code'] ?? ''),
|
|
];
|
|
}
|
|
return $this->response->setJSON([
|
|
'ok' => 1,
|
|
'txn_id' => 'LOCAL-' . uniqid(),
|
|
'profiles' => $profiles,
|
|
'mobile' => $mobile,
|
|
]);
|
|
}
|
|
|
|
return $this->response->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => $result['error_text'] ?? 'No ABHA profiles found linked with mobile number ' . $mobile,
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
log_message('error', '[ABHA] findMobileSearch exception: ' . $e->getMessage());
|
|
return $this->response->setStatusCode(500)->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Step 2: Request OTP for the chosen ABHA profile
|
|
* POST abha/find/mobile/request-otp
|
|
*/
|
|
public function findMobileRequestOtp()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$txnId = trim((string) ($this->request->getPost('txn_id') ?? $this->request->getPost('txnId') ?? ''));
|
|
$index = trim((string) ($this->request->getPost('index') ?? '1'));
|
|
$authMethod = strtoupper(trim((string) ($this->request->getPost('auth_method') ?? 'MOBILE_OTP')));
|
|
$mobile = preg_replace('/\D/', '', trim((string) ($this->request->getPost('mobile') ?? '')));
|
|
|
|
if ($txnId === '') {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Search transaction ID (txn_id) is required']);
|
|
}
|
|
|
|
try {
|
|
$helper = new \App\Libraries\Abdm\AbdmV3DirectHelper();
|
|
$result = $helper->requestProfileOtp($txnId, $index, $authMethod);
|
|
|
|
if (! empty($result['ok']) && (int) $result['ok'] === 1) {
|
|
return $this->response->setJSON([
|
|
'ok' => 1,
|
|
'txn_id' => $result['txn_id'] ?? $txnId,
|
|
'message' => $result['message'] ?? 'OTP sent to mobile ending with ' . substr($mobile, -4),
|
|
'auth_method' => $authMethod,
|
|
]);
|
|
}
|
|
|
|
return $this->response->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => $result['error_text'] ?? 'Failed to send OTP to registered mobile/Aadhaar.',
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
log_message('error', '[ABHA] findMobileRequestOtp exception: ' . $e->getMessage());
|
|
return $this->response->setStatusCode(500)->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Step 3: Verify OTP and fetch complete ABHA profile + ABHA card
|
|
* POST abha/find/mobile/verify-otp
|
|
*/
|
|
public function findMobileVerifyOtp()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$txnId = trim((string) ($this->request->getPost('txn_id') ?? $this->request->getPost('txnId') ?? ''));
|
|
$otp = trim((string) ($this->request->getPost('otp') ?? ''));
|
|
$authMethod = strtoupper(trim((string) ($this->request->getPost('auth_method') ?? 'MOBILE_OTP')));
|
|
$mobile = preg_replace('/\D/', '', trim((string) ($this->request->getPost('mobile') ?? '')));
|
|
|
|
if ($txnId === '' || $otp === '') {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Transaction ID and 6-digit OTP are required']);
|
|
}
|
|
|
|
try {
|
|
$helper = new \App\Libraries\Abdm\AbdmV3DirectHelper();
|
|
$result = $helper->verifyProfileOtp($txnId, $otp, $authMethod);
|
|
|
|
if (empty($result['ok']) || (int) $result['ok'] !== 1) {
|
|
return $this->response->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => $result['error_text'] ?? 'OTP verification failed.',
|
|
]);
|
|
}
|
|
|
|
$fullProfile = is_array($result['full_profile'] ?? null) ? $result['full_profile'] : [];
|
|
$account = is_array($result['account'] ?? null) ? $result['account'] : [];
|
|
$userToken = (string) ($result['token'] ?? '');
|
|
|
|
$firstNonEmpty = static function (array $values): string {
|
|
foreach ($values as $value) {
|
|
if (is_scalar($value) && trim((string) $value) !== '') {
|
|
return trim((string) $value);
|
|
}
|
|
}
|
|
return '';
|
|
};
|
|
|
|
$abhaNum = $firstNonEmpty([
|
|
$account['ABHANumber'] ?? null,
|
|
$account['abhaNumber'] ?? null,
|
|
$account['abha_number'] ?? null,
|
|
$account['abha_id'] ?? null,
|
|
$fullProfile['ABHANumber'] ?? null,
|
|
$fullProfile['abhaNumber'] ?? null,
|
|
$fullProfile['abha_id'] ?? null,
|
|
]);
|
|
|
|
$abhaAddress = $firstNonEmpty([
|
|
$account['preferredAbhaAddress'] ?? null,
|
|
$account['preferredAddress'] ?? null,
|
|
$account['abhaAddress'] ?? null,
|
|
$account['abha_address'] ?? null,
|
|
$fullProfile['preferredAbhaAddress'] ?? null,
|
|
$fullProfile['preferredAddress'] ?? null,
|
|
$fullProfile['abhaAddress'] ?? null,
|
|
$fullProfile['abha_address'] ?? null,
|
|
]);
|
|
|
|
$name = $firstNonEmpty([
|
|
$account['name'] ?? null,
|
|
$account['fullName'] ?? null,
|
|
$account['full_name'] ?? null,
|
|
$fullProfile['name'] ?? null,
|
|
trim(($fullProfile['firstName'] ?? '') . ' ' . ($fullProfile['middleName'] ?? '') . ' ' . ($fullProfile['lastName'] ?? '')),
|
|
]);
|
|
|
|
$genderRaw = $firstNonEmpty([
|
|
$account['gender'] ?? null,
|
|
$fullProfile['gender'] ?? null,
|
|
]);
|
|
$genderUpper = strtoupper($genderRaw);
|
|
if ($genderUpper === 'M' || $genderUpper === '1' || $genderUpper === 'MALE') {
|
|
$gender = 'Male';
|
|
} elseif ($genderUpper === 'F' || $genderUpper === '2' || $genderUpper === 'FEMALE') {
|
|
$gender = 'Female';
|
|
} elseif ($genderUpper === 'O' || $genderUpper === '3' || $genderUpper === 'OTHER') {
|
|
$gender = 'Other';
|
|
} else {
|
|
$gender = $genderRaw;
|
|
}
|
|
|
|
$dob = $firstNonEmpty([
|
|
$account['dob'] ?? null,
|
|
$account['dateOfBirth'] ?? null,
|
|
$account['date_of_birth'] ?? null,
|
|
$fullProfile['dob'] ?? null,
|
|
$fullProfile['dateOfBirth'] ?? null,
|
|
$fullProfile['date_of_birth'] ?? null,
|
|
]);
|
|
if ($dob === '') {
|
|
$day = $firstNonEmpty([$account['dayOfBirth'] ?? null, $account['day_of_birth'] ?? null, $fullProfile['dayOfBirth'] ?? null, $fullProfile['day_of_birth'] ?? null]);
|
|
$month = $firstNonEmpty([$account['monthOfBirth'] ?? null, $account['month_of_birth'] ?? null, $fullProfile['monthOfBirth'] ?? null, $fullProfile['month_of_birth'] ?? null]);
|
|
$year = $firstNonEmpty([$account['yearOfBirth'] ?? null, $account['year_of_birth'] ?? null, $fullProfile['yearOfBirth'] ?? null, $fullProfile['year_of_birth'] ?? null]);
|
|
if ($year !== '' && $month !== '' && $day !== '') {
|
|
$dob = sprintf('%04d-%02d-%02d', (int) $year, (int) $month, (int) $day);
|
|
} elseif ($year !== '') {
|
|
$dob = $year;
|
|
}
|
|
}
|
|
|
|
$profileMobile = $firstNonEmpty([
|
|
$account['mobile'] ?? null,
|
|
$account['mobileNumber'] ?? null,
|
|
$account['mobile_number'] ?? null,
|
|
$fullProfile['mobile'] ?? null,
|
|
$fullProfile['mobileNumber'] ?? null,
|
|
$mobile,
|
|
]);
|
|
|
|
$photo = $firstNonEmpty([
|
|
$account['profilePhoto'] ?? null,
|
|
$account['profile_photo'] ?? null,
|
|
$fullProfile['profilePhoto'] ?? null,
|
|
$fullProfile['profile_photo'] ?? null,
|
|
$fullProfile['photo'] ?? null,
|
|
$fullProfile['kycPhoto'] ?? null,
|
|
]);
|
|
|
|
$address = $firstNonEmpty([
|
|
$fullProfile['address'] ?? null,
|
|
$fullProfile['addressLine'] ?? null,
|
|
$fullProfile['address_line'] ?? null,
|
|
$account['address'] ?? null,
|
|
$account['addressLine'] ?? null,
|
|
]);
|
|
|
|
$districtName = $firstNonEmpty([
|
|
$fullProfile['district'] ?? null,
|
|
$fullProfile['districtName'] ?? null,
|
|
$fullProfile['district_name'] ?? null,
|
|
$account['district'] ?? null,
|
|
$account['districtName'] ?? null,
|
|
]);
|
|
|
|
$stateName = $firstNonEmpty([
|
|
$fullProfile['state'] ?? null,
|
|
$fullProfile['stateName'] ?? null,
|
|
$fullProfile['state_name'] ?? null,
|
|
$account['state'] ?? null,
|
|
$account['stateName'] ?? null,
|
|
]);
|
|
|
|
$zip = $firstNonEmpty([
|
|
$fullProfile['pinCode'] ?? null,
|
|
$fullProfile['pincode'] ?? null,
|
|
$fullProfile['pin'] ?? null,
|
|
$account['pinCode'] ?? null,
|
|
$account['pincode'] ?? null,
|
|
]);
|
|
|
|
$email = $firstNonEmpty([
|
|
$fullProfile['email'] ?? null,
|
|
$account['email'] ?? null,
|
|
]);
|
|
|
|
$cardBase64 = (string) ($result['card_base64'] ?? '');
|
|
|
|
// Fallback to Bridge card endpoint if not retrieved directly
|
|
if ($cardBase64 === '' && ($abhaNum !== '' || $abhaAddress !== '')) {
|
|
try {
|
|
$bridge = \App\Libraries\Abdm\AbdmConnectorFactory::make();
|
|
$cardResult = $bridge->fetchOfficialAbhaCard([
|
|
'abha_number' => $abhaNum,
|
|
'abha_address' => $abhaAddress,
|
|
'token' => $userToken,
|
|
'x_token' => $userToken,
|
|
]);
|
|
if (! empty($cardResult['ok']) && ! empty($cardResult['card_base64'])) {
|
|
$cardBase64 = (string) $cardResult['card_base64'];
|
|
}
|
|
} catch (\Throwable $e) {
|
|
log_message('warning', '[ABHA] Bridge card fetch fallback failed: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
$abhaMeta = [
|
|
'abha_address' => $abhaAddress,
|
|
'profile_photo' => $photo,
|
|
'verified_status' => 'ACTIVE',
|
|
'verification_type' => 'VERIFIED',
|
|
'kyc_verified' => true,
|
|
'mobile_verified' => true,
|
|
'address' => $address,
|
|
'district' => $districtName,
|
|
'state' => $stateName,
|
|
'zip' => $zip,
|
|
'email' => $email,
|
|
];
|
|
|
|
$patientInfo = $this->tryAutoLinkByDirectMatch($abhaNum, $name, $profileMobile, $gender, $dob, $abhaMeta);
|
|
|
|
$responseBase = [
|
|
'ok' => 1,
|
|
'txn_id' => $result['txn_id'] ?? $txnId,
|
|
'token' => $result['token'] ?? '',
|
|
'accounts' => $result['accounts'] ?? [],
|
|
'card_base64' => $cardBase64,
|
|
'card_content_type' => 'image/png',
|
|
'card_source' => $cardBase64 !== '' ? 'abdm' : 'generated',
|
|
'card_message' => $cardBase64 !== '' ? 'Official ABHA Card from ABDM' : '',
|
|
'abha_number' => $abhaNum,
|
|
'name' => $name,
|
|
'photo' => $photo,
|
|
'mobile' => $profileMobile,
|
|
'gender' => $gender,
|
|
'dob' => $dob,
|
|
'abha_address' => $abhaAddress,
|
|
'verified_status' => 'ACTIVE',
|
|
'verification_type' => 'VERIFIED',
|
|
'kyc_verified' => true,
|
|
'mobile_verified' => true,
|
|
'address' => $address,
|
|
'district' => $districtName,
|
|
'state' => $stateName,
|
|
'zip' => $zip,
|
|
'email' => $email,
|
|
];
|
|
|
|
if ($patientInfo !== null) {
|
|
return $this->response->setJSON($responseBase + [
|
|
'need_confirmation' => false,
|
|
'patient_id' => $patientInfo['patient_id'],
|
|
'p_code' => $patientInfo['p_code'],
|
|
'is_new_patient' => $patientInfo['is_new'],
|
|
]);
|
|
}
|
|
|
|
$db = \Config\Database::connect();
|
|
$fields = $db->getFieldNames('patient_master') ?? [];
|
|
$abhaField = $this->resolveAbhaFieldName($fields);
|
|
$abhaNumClean = preg_replace('/\D/', '', $abhaNum);
|
|
$candidates = $this->findMatchingCandidates($db, $fields, $name, $profileMobile, $gender, $dob, '', $abhaField, $abhaNumClean);
|
|
|
|
return $this->response->setJSON($responseBase + [
|
|
'need_confirmation' => true,
|
|
'patient_id' => 0,
|
|
'p_code' => '',
|
|
'is_new_patient' => null,
|
|
'candidates' => $candidates,
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
log_message('error', '[ABHA] findMobileVerifyOtp exception: ' . $e->getMessage());
|
|
return $this->response->setStatusCode(500)->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// ABDM M1 Find ABHA via Aadhaar Flow
|
|
// TEST POINT: 12.1 VRFY_ABHA_401 to VRFY_ABHA_405
|
|
// FLOW: AADHAAR NUMBER -> OTP -> COMPLETE PROFILE FETCHED / CREATE ABHA
|
|
// -------------------------------------------------------------------------
|
|
|
|
/**
|
|
* VRFY_ABHA_401: Request OTP using 12-digit Aadhaar Number
|
|
* POST abha/find/aadhaar/request-otp
|
|
*/
|
|
public function findAadhaarRequestOtp()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$aadhaar = preg_replace('/\D/', '', trim((string) ($this->request->getPost('aadhaar') ?? '')));
|
|
if (strlen($aadhaar) !== 12 || ! $this->validateVerhoeff($aadhaar)) {
|
|
return $this->response->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => 'Aadhaar Number is not valid. Please enter a valid 12-digit Aadhaar Number.',
|
|
]);
|
|
}
|
|
|
|
try {
|
|
$connector = AbdmConnectorFactory::make();
|
|
$result = $connector->abhaAadhaarGenerateOtp(['aadhaar' => $aadhaar, 'purpose' => 'search', 'flow' => 'search']);
|
|
|
|
if (! empty($result['ok']) && (int) $result['ok'] === 1) {
|
|
$txnId = (string) ($result['txn_id'] ?? $result['data']['txnId'] ?? $result['data']['txn_id'] ?? '');
|
|
$msg = (string) ($result['message'] ?? $result['data']['message'] ?? 'OTP sent to Aadhaar linked mobile number.');
|
|
$maskedMobile = (string) ($result['masked_mobile'] ?? $result['data']['maskedMobile'] ?? $result['data']['masked_mobile'] ?? '');
|
|
|
|
return $this->response->setJSON([
|
|
'ok' => 1,
|
|
'txn_id' => $txnId,
|
|
'message' => $msg,
|
|
'masked_mobile' => $maskedMobile,
|
|
'resend_after' => 60,
|
|
]);
|
|
}
|
|
|
|
// Fallback to direct V3 client if bridge call did not succeed
|
|
try {
|
|
$helper = new \App\Libraries\Abdm\AbdmV3DirectHelper();
|
|
$directResult = $helper->requestLoginOtpByAadhaar($aadhaar);
|
|
if (! empty($directResult['ok']) && (int) $directResult['ok'] === 1) {
|
|
return $this->response->setJSON([
|
|
'ok' => 1,
|
|
'txn_id' => $directResult['txn_id'] ?? '',
|
|
'message' => $directResult['message'] ?? 'OTP sent to Aadhaar linked mobile number.',
|
|
'resend_after' => $directResult['resend_after'] ?? 60,
|
|
]);
|
|
}
|
|
} catch (\Throwable $e) {}
|
|
|
|
$errMsg = $this->extractBridgeErrorText($result, 'Failed to send OTP to Aadhaar linked mobile.');
|
|
if (stripos($errMsg, 'invalid') !== false && stripos($errMsg, 'aadhaar') !== false) {
|
|
$errMsg = 'Aadhaar Number is not valid. Please enter a valid 12-digit Aadhaar Number.';
|
|
}
|
|
|
|
return $this->response->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => $errMsg,
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
log_message('error', '[ABHA] findAadhaarRequestOtp exception: ' . $e->getMessage());
|
|
return $this->response->setStatusCode(500)->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* VRFY_ABHA_402, 403, 404, 405: Verify Aadhaar OTP and fetch complete ABHA profile + card
|
|
* POST abha/find/aadhaar/verify-otp
|
|
*/
|
|
public function findAadhaarVerifyOtp()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$txnId = trim((string) ($this->request->getPost('txn_id') ?? $this->request->getPost('txnId') ?? ''));
|
|
$otp = preg_replace('/\D/', '', trim((string) ($this->request->getPost('otp') ?? '')));
|
|
|
|
if ($txnId === '' || strlen($otp) !== 6) {
|
|
return $this->response->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => 'Transaction ID and 6-digit OTP are required.',
|
|
]);
|
|
}
|
|
|
|
try {
|
|
$connector = AbdmConnectorFactory::make();
|
|
$result = $connector->abhaAadhaarVerifyOtp(['txnId' => $txnId, 'otp' => $otp, 'purpose' => 'search', 'flow' => 'search']);
|
|
|
|
$authResult = strtolower(trim((string) ($result['authResult'] ?? $result['data']['authResult'] ?? $result['auth_result'] ?? $result['data']['auth_result'] ?? '')));
|
|
$status = strtolower(trim((string) ($result['status'] ?? $result['data']['status'] ?? '')));
|
|
if ($authResult === 'failed' || in_array($status, ['failed', 'failure', 'error'], true)) {
|
|
$result['ok'] = 0;
|
|
}
|
|
|
|
if (empty($result['ok']) || (int) $result['ok'] !== 1) {
|
|
$err = $this->extractBridgeErrorText($result, 'Please enter a valid OTP. Entered OTP is either expired or incorrect.');
|
|
if (stripos($err, 'no abha') !== false || stripos($err, 'not registered') !== false || stripos($err, 'not exist') !== false || stripos($err, 'not found') !== false) {
|
|
return $this->response->setJSON([
|
|
'ok' => 0,
|
|
'no_abha_found' => true,
|
|
'error_text' => 'NO ABHA user registered with this Aadhaar Number',
|
|
]);
|
|
}
|
|
return $this->response->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => $err,
|
|
]);
|
|
}
|
|
|
|
$payload = $result;
|
|
if (is_array($result['data'] ?? null)) {
|
|
foreach ($result['data'] as $k => $v) {
|
|
if (! array_key_exists($k, $payload)) {
|
|
$payload[$k] = $v;
|
|
}
|
|
}
|
|
}
|
|
|
|
$profile = $this->pickGatewayAbhaProfile($payload);
|
|
|
|
$abhaNum = (string) (
|
|
$profile['ABHANumber']
|
|
?? $profile['abha_id']
|
|
?? $profile['abha_number']
|
|
?? $payload['ABHANumber']
|
|
?? $payload['abha_id']
|
|
?? $payload['abha_number']
|
|
?? ''
|
|
);
|
|
$cleanAbha = preg_replace('/\D/', '', $abhaNum);
|
|
|
|
$abhaAddress = (string) (
|
|
($profile['preferredAddress'] ?? '')
|
|
?: ($profile['preferredAbhaAddress'] ?? '')
|
|
?: ($profile['abha_address'] ?? '')
|
|
?: ($payload['preferredAddress'] ?? '')
|
|
?: ($payload['preferredAbhaAddress'] ?? '')
|
|
?: ($payload['abha_address'] ?? '')
|
|
?: ''
|
|
);
|
|
|
|
// VRFY_ABHA_403: No ABHA user registered with this Aadhaar Number
|
|
$isNew = ! empty($payload['is_new']) || ! empty($payload['new_user']) || ! empty($result['new_user'])
|
|
|| (isset($payload['isNew']) && ($payload['isNew'] === true || $payload['isNew'] === 'true'))
|
|
|| (isset($payload['new']) && ($payload['new'] === true || $payload['new'] === 'true'));
|
|
if ((strlen($cleanAbha) !== 14 && $abhaAddress === '') || $isNew) {
|
|
return $this->response->setJSON([
|
|
'ok' => 0,
|
|
'no_abha_found' => true,
|
|
'error_text' => 'NO ABHA user registered with this Aadhaar Number',
|
|
]);
|
|
}
|
|
|
|
$name = $this->extractAbhaProfileName($profile, $payload);
|
|
$photo = $this->extractAbhaProfilePhoto($profile, $payload);
|
|
$profileMobile = $this->extractAbhaProfileMobile($profile, $payload);
|
|
$gender = $this->extractAbhaProfileGender($profile, $payload);
|
|
$profileDob = $this->extractAbhaProfileDob($profile, $payload);
|
|
$addrInfo = $this->extractAbhaProfileAddress($profile, $payload);
|
|
$address = $addrInfo['address'];
|
|
$districtName = $addrInfo['district'];
|
|
$stateName = $addrInfo['state'];
|
|
$zip = $addrInfo['zip'];
|
|
$email = (string) (($profile['email'] ?? '') ?: ($payload['email'] ?? '') ?: ($payload['gateway_patient']['email'] ?? ''));
|
|
|
|
$cardBase64 = (string) (
|
|
$result['official_card']['card_base64']
|
|
?? $payload['official_card']['card_base64']
|
|
?? $result['card_base64']
|
|
?? $payload['card_base64']
|
|
?? ''
|
|
);
|
|
|
|
$abhaMeta = [
|
|
'abha_address' => $abhaAddress,
|
|
'profile_photo' => $photo,
|
|
'verified_status' => 'ACTIVE',
|
|
'verification_type' => 'VERIFIED',
|
|
'kyc_verified' => true,
|
|
'mobile_verified' => true,
|
|
'address' => $address,
|
|
'district' => $districtName,
|
|
'state' => $stateName,
|
|
'zip' => $zip,
|
|
'email' => $email,
|
|
];
|
|
|
|
$patientInfo = $this->tryAutoLinkByDirectMatch($cleanAbha, $name, $profileMobile, $gender, $profileDob, $abhaMeta);
|
|
|
|
$responseBase = [
|
|
'ok' => 1,
|
|
'txn_id' => (string) ($payload['txnId'] ?? $payload['txn_id'] ?? $txnId),
|
|
'card_base64' => $cardBase64,
|
|
'card_content_type' => 'image/png',
|
|
'card_source' => $cardBase64 !== '' ? 'abdm' : 'generated',
|
|
'card_message' => $cardBase64 !== '' ? 'Official ABHA Card from ABDM' : '',
|
|
'abha_number' => $cleanAbha,
|
|
'name' => $name,
|
|
'photo' => $photo,
|
|
'mobile' => $profileMobile,
|
|
'gender' => $gender,
|
|
'dob' => $profileDob,
|
|
'abha_address' => $abhaAddress,
|
|
'verified_status' => 'ACTIVE',
|
|
'verification_type' => 'VERIFIED',
|
|
'kyc_verified' => true,
|
|
'mobile_verified' => true,
|
|
'address' => $address,
|
|
'district' => $districtName,
|
|
'state' => $stateName,
|
|
'zip' => $zip,
|
|
'email' => $email,
|
|
];
|
|
|
|
if ($patientInfo !== null) {
|
|
return $this->response->setJSON($responseBase + [
|
|
'need_confirmation' => false,
|
|
'patient_id' => $patientInfo['patient_id'],
|
|
'p_code' => $patientInfo['p_code'],
|
|
'is_new_patient' => $patientInfo['is_new'],
|
|
]);
|
|
}
|
|
|
|
$db = \Config\Database::connect();
|
|
$fields = $db->getFieldNames('patient_master') ?? [];
|
|
$abhaField = $this->resolveAbhaFieldName($fields);
|
|
$candidates = $this->findMatchingCandidates($db, $fields, $name, $profileMobile, $gender, $profileDob, '', $abhaField, $cleanAbha);
|
|
|
|
return $this->response->setJSON($responseBase + [
|
|
'need_confirmation' => true,
|
|
'patient_id' => 0,
|
|
'p_code' => '',
|
|
'is_new_patient' => null,
|
|
'candidates' => $candidates,
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
log_message('error', '[ABHA] findAadhaarVerifyOtp exception: ' . $e->getMessage());
|
|
return $this->response->setStatusCode(500)->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Step 4 — ABHA address suggestions / assignment
|
|
// POST abha/create/address_suggestions, POST abha/create/address
|
|
// -------------------------------------------------------------------------
|
|
public function addressSuggestions()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$txnId = trim((string) ($this->request->getPost('txn_id') ?? $this->request->getPost('txnId') ?? ''));
|
|
if ($txnId === '') {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'txn_id is required']);
|
|
}
|
|
|
|
try {
|
|
$result = AbdmConnectorFactory::make()->abhaAddressSuggestions(['txn_id' => $txnId]);
|
|
} catch (\Throwable $e) {
|
|
return $this->response->setJSON(['ok' => 0, 'supported' => false, 'error_text' => $e->getMessage()]);
|
|
}
|
|
|
|
if (empty($result['ok']) || (int) $result['ok'] !== 1) {
|
|
// A missing Bridge endpoint must not block ABHA creation — the modal
|
|
// keeps the ABDM-assigned address and skips the selection step.
|
|
$unsupported = in_array((int) ($result['http_code'] ?? 0), [404, 405], true);
|
|
|
|
return $this->response->setJSON([
|
|
'ok' => 0,
|
|
'supported' => ! $unsupported,
|
|
'error_text' => $unsupported
|
|
? 'The ABDM Bridge does not expose ABHA address suggestions yet.'
|
|
: $this->extractBridgeErrorText($result, 'Unable to load ABHA address suggestions.'),
|
|
'request_id' => (string) ($result['request_id'] ?? ''),
|
|
]);
|
|
}
|
|
|
|
return $this->response->setJSON([
|
|
'ok' => 1,
|
|
'supported' => true,
|
|
'txn_id' => $txnId,
|
|
'suggestions' => array_values((array) ($result['suggestions'] ?? [])),
|
|
'bridge_mock' => ! empty($result['bridge_mock']),
|
|
'error_text' => ! empty($result['bridge_mock'])
|
|
? 'The ABDM Bridge is running in test mode and returned a mock response, so no ABHA address suggestions were provided.'
|
|
: '',
|
|
]);
|
|
}
|
|
|
|
public function address()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$txnId = trim((string) ($this->request->getPost('txn_id') ?? $this->request->getPost('txnId') ?? ''));
|
|
$abhaAddress = trim((string) ($this->request->getPost('abha_address') ?? ''));
|
|
|
|
if ($abhaAddress === '') {
|
|
return $this->response->setJSON(['ok' => 1, 'message' => 'ABHA created successfully.']);
|
|
}
|
|
|
|
$handle = explode('@', $abhaAddress)[0];
|
|
$len = strlen($handle);
|
|
$dotCount = substr_count($handle, '.');
|
|
$underCount = substr_count($handle, '_');
|
|
|
|
if ($len < 8 || $len > 18) {
|
|
return $this->response->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => 'ABHA address must be between 8 and 18 characters.',
|
|
]);
|
|
}
|
|
if (! preg_match('/^[a-zA-Z0-9]/', $handle) || ! preg_match('/[a-zA-Z0-9]$/', $handle)) {
|
|
return $this->response->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => 'Special character dot (.) and underscore (_) can only be in between, not at the beginning or end.',
|
|
]);
|
|
}
|
|
if ($dotCount > 1 || $underCount > 1 || ! preg_match('/^[a-zA-Z0-9._]+$/', $handle)) {
|
|
return $this->response->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => 'Only letters, numbers, at most 1 dot (.) and/or 1 underscore (_) are allowed.',
|
|
]);
|
|
}
|
|
|
|
try {
|
|
$result = AbdmConnectorFactory::make()->abhaSetAddress([
|
|
'txn_id' => $txnId,
|
|
'abha_address' => $abhaAddress,
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => $e->getMessage()]);
|
|
}
|
|
|
|
if (empty($result['ok']) || (int) $result['ok'] !== 1) {
|
|
$errorMsg = $this->extractBridgeErrorText($result, 'Unable to set the ABHA address.');
|
|
if (stripos($errorMsg, 'already exist') !== false || stripos($errorMsg, 'already in use') !== false || stripos($errorMsg, 'taken') !== false) {
|
|
$errorMsg = 'ABHA Address is already exist';
|
|
}
|
|
return $this->response->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => $errorMsg,
|
|
'request_id' => (string) ($result['request_id'] ?? ''),
|
|
]);
|
|
}
|
|
|
|
$data = is_array($result['data'] ?? null) ? $result['data'] : [];
|
|
|
|
if (! empty($result['bridge_mock'])) {
|
|
return $this->response->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => 'The ABDM Bridge is running in test mode and did not apply the ABHA address. Ask the Bridge team to enable live mode for /v3/abha/set-address.',
|
|
'request_id' => (string) ($result['request_id'] ?? ''),
|
|
]);
|
|
}
|
|
|
|
return $this->response->setJSON([
|
|
'ok' => 1,
|
|
'abha_address' => (string) ($result['abha_address'] ?? $data['abhaAddress'] ?? $data['abha_address'] ?? $abhaAddress),
|
|
'message' => 'ABHA address updated successfully.',
|
|
]);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Facility Scan & Share QR — GET abha/register/facility_qr
|
|
// -------------------------------------------------------------------------
|
|
public function facilityQr()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
try {
|
|
/** @var \App\Libraries\Abdm\AbdmConnectorInterface $connector */
|
|
$connector = AbdmConnectorFactory::make();
|
|
$result = $connector->facilityQrCode();
|
|
} catch (\Throwable $e) {
|
|
return $this->response->setStatusCode(500)->setJSON(['ok' => 0, 'error_text' => $e->getMessage()]);
|
|
}
|
|
|
|
if (empty($result['ok']) || (int) $result['ok'] !== 1) {
|
|
return $this->response->setStatusCode((int) ($result['http_code'] ?? 502) ?: 502)->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => $result['error_text'] ?? $result['message'] ?? $result['error'] ?? 'Unable to fetch facility QR code',
|
|
'bridge_http_code' => (int) ($result['http_code'] ?? 0),
|
|
]);
|
|
}
|
|
|
|
return $this->response->setJSON([
|
|
'ok' => 1,
|
|
'hospital_id' => $result['hospital_id'] ?? $result['data']['hospital_id'] ?? '',
|
|
'hospital_name' => $result['hospital_name'] ?? $result['data']['hospital_name'] ?? '',
|
|
'hfr_id' => $result['hfr_id'] ?? $result['data']['hfr_id'] ?? '',
|
|
'qr_data' => $result['qr_data'] ?? $result['data']['qr_data'] ?? '',
|
|
'request_id' => $result['request_id'] ?? '',
|
|
]);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Fetch official ABHA card — POST abha/register/card
|
|
// -------------------------------------------------------------------------
|
|
public function fetchOfficialCard()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$abhaNumber = trim((string) ($this->request->getPost('abha_number') ?? ''));
|
|
$abhaAddress = trim((string) ($this->request->getPost('abha_address') ?? ''));
|
|
|
|
if ($abhaNumber === '' && $abhaAddress === '') {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'ABHA number or ABHA address is required']);
|
|
}
|
|
|
|
try {
|
|
$result = AbdmConnectorFactory::make()->fetchOfficialAbhaCard([
|
|
'abha_number' => $abhaNumber,
|
|
'abha_address' => $abhaAddress,
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
log_message('error', '[ABHA] Official card fetch failed: ' . $e->getMessage());
|
|
return $this->response->setStatusCode(502)->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => 'The official ABHA card could not be downloaded from the Bridge.',
|
|
]);
|
|
}
|
|
|
|
if (empty($result['ok']) || (int) $result['ok'] !== 1) {
|
|
return $this->response->setStatusCode(502)->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => $this->extractBridgeErrorText($result, 'The official ABHA card was not returned by the Bridge.'),
|
|
]);
|
|
}
|
|
|
|
return $this->response->setJSON($result);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Official ABHA Card — GET abha/card/{abha_number}
|
|
// Renders only the official card image returned by ABDM/NHA.
|
|
// -------------------------------------------------------------------------
|
|
public function card(string $abhaNumber = '')
|
|
{
|
|
$abhaNumClean = preg_replace('/\D/', '', $abhaNumber);
|
|
|
|
if (strlen($abhaNumClean) !== 14) {
|
|
return $this->response->setStatusCode(400)
|
|
->setBody('<h3 style="font-family:sans-serif;color:red;">Invalid ABHA number.</h3>');
|
|
}
|
|
|
|
$db = \Config\Database::connect();
|
|
$fields = $db->getFieldNames('patient_master') ?? [];
|
|
|
|
$abhaField = null;
|
|
foreach (['abha_id', 'abha_no', 'abha'] as $f) {
|
|
if (in_array($f, $fields, true)) { $abhaField = $f; break; }
|
|
}
|
|
if ($abhaField === null && in_array('abha_address', $fields, true)) {
|
|
$abhaField = 'abha_address';
|
|
}
|
|
|
|
$patient = null;
|
|
if ($abhaField) {
|
|
$patient = $db->table('patient_master')
|
|
->where($abhaField, $abhaNumClean)->get()->getRowArray();
|
|
if (! $patient) {
|
|
$patient = $db->table('patient_master')
|
|
->where($abhaField, $abhaNumber)->get()->getRowArray();
|
|
}
|
|
}
|
|
|
|
if (! $patient) {
|
|
return $this->response->setStatusCode(404)
|
|
->setBody('<h3 style="font-family:sans-serif;color:red;">Patient with ABHA number ' . esc($abhaNumber) . ' not found.</h3>');
|
|
}
|
|
|
|
$storedCard = trim((string) ($patient['abha_card_base64'] ?? ''));
|
|
if ($storedCard === '') {
|
|
return $this->response->setStatusCode(404)
|
|
->setBody('<h3 style="font-family:sans-serif;color:red;">Official ABHA card is not available for this patient.</h3>');
|
|
}
|
|
|
|
return view('abha/official_card', [
|
|
'patient' => $patient,
|
|
'stored_abha_card' => $storedCard,
|
|
]);
|
|
}
|
|
|
|
// Hospital Card — GET abha/hospital-card/{patient_id}
|
|
public function hospitalCard(int $patientId = 0)
|
|
{
|
|
if ($patientId <= 0) {
|
|
return $this->response->setStatusCode(400)
|
|
->setBody('<h3 style="font-family:sans-serif;color:red;">Invalid patient.</h3>');
|
|
}
|
|
|
|
$db = \Config\Database::connect();
|
|
$patient = $db->table('patient_master')->where('id', $patientId)->get()->getRowArray();
|
|
if (! $patient) {
|
|
return $this->response->setStatusCode(404)
|
|
->setBody('<h3 style="font-family:sans-serif;color:red;">Patient not found.</h3>');
|
|
}
|
|
|
|
$abhaNumClean = '';
|
|
foreach (['abha_id', 'abha_no', 'abha'] as $field) {
|
|
$candidate = preg_replace('/\D/', '', (string) ($patient[$field] ?? ''));
|
|
if (strlen($candidate) === 14) {
|
|
$abhaNumClean = $candidate;
|
|
break;
|
|
}
|
|
}
|
|
if ($abhaNumClean === '') {
|
|
return $this->response->setStatusCode(404)
|
|
->setBody('<h3 style="font-family:sans-serif;color:red;">ABHA is not linked with this patient.</h3>');
|
|
}
|
|
|
|
$abhaDisp = preg_replace('/^(\d{2})(\d{4})(\d{4})(\d{4})$/', '$1-$2-$3-$4', $abhaNumClean) ?: $abhaNumClean;
|
|
$genderRaw = (string) ($patient['gender'] ?? '');
|
|
$genderLabel = $genderRaw === '1' ? 'Male' : ($genderRaw === '2' ? 'Female' : $genderRaw);
|
|
$dobRaw = (string) ($patient['dob'] ?? '');
|
|
$dobLabel = '';
|
|
if ($dobRaw && $dobRaw !== '0000-00-00') {
|
|
try { $dobLabel = (new \DateTime($dobRaw))->format('d M Y'); } catch (\Exception $e) { $dobLabel = $dobRaw; }
|
|
}
|
|
|
|
$hospitalBrand = $this->loadHospitalBranding();
|
|
$hmsId = trim((string) ($patient['p_code'] ?? ''));
|
|
if ($hmsId === '') {
|
|
$hmsId = 'PID-' . (string) ($patient['id'] ?? '');
|
|
}
|
|
$profilePhotoUrl = $this->resolvePatientPhotoUrl($patient);
|
|
$mobileNo = trim((string) ($patient['mphone1'] ?? ''));
|
|
$mobileVerified = (int) ($patient['abha_mobile_verified'] ?? 0) === 1;
|
|
$patientQrPayload = json_encode([
|
|
'hidn' => $abhaNumClean,
|
|
'hid' => trim((string) ($patient['abha_address'] ?? '')),
|
|
'name' => trim((string) ($patient['p_fname'] ?? '')),
|
|
'gender' => $genderLabel,
|
|
'dob' => $dobRaw !== '0000-00-00' ? $dobRaw : '',
|
|
'mobile' => $mobileNo,
|
|
'address' => trim((string) ($patient['add1'] ?? '')),
|
|
'districtName' => trim((string) ($patient['district'] ?? '')),
|
|
'stateName' => trim((string) ($patient['state'] ?? '')),
|
|
'pincode' => trim((string) ($patient['zip'] ?? '')),
|
|
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
|
$patientQrImage = $this->buildQrCodeDataUri(is_string($patientQrPayload) ? $patientQrPayload : $abhaNumClean);
|
|
$barcodeSvg = $this->buildCode39Svg($hmsId);
|
|
$barcodeImage = $this->buildCode39ImageDataUri($hmsId, $barcodeSvg);
|
|
|
|
return view('abha/card', [
|
|
'patient' => $patient,
|
|
'abha_num' => $abhaDisp,
|
|
'abha_raw' => $abhaNumClean,
|
|
'patient_id' => (int) ($patient['id'] ?? 0),
|
|
'abha_address' => (string) ($patient['abha_address'] ?? ''),
|
|
'gender' => $genderLabel,
|
|
'dob' => $dobLabel,
|
|
'hospital_name' => (string) ($hospitalBrand['hospital_name'] ?? 'E-Atria Hospital'),
|
|
'hospital_logo_url' => (string) ($hospitalBrand['hospital_logo_url'] ?? base_url('assets/img/logo.png')),
|
|
'brand_name' => (string) ($hospitalBrand['brand_name'] ?? 'HMS'),
|
|
'hms_id' => $hmsId,
|
|
'hms_barcode_svg' => $barcodeSvg,
|
|
'hms_barcode_image' => $barcodeImage,
|
|
'profile_photo_url' => $profilePhotoUrl,
|
|
'patient_qr_image' => $patientQrImage,
|
|
'patient_mobile' => $mobileNo,
|
|
'mobile_verified' => $mobileVerified,
|
|
]);
|
|
}
|
|
|
|
private function buildQrCodeDataUri(string $payload): string
|
|
{
|
|
try {
|
|
$qrCode = new \Mpdf\QrCode\QrCode($payload, 'M');
|
|
$svg = (new \Mpdf\QrCode\Output\Svg())->output($qrCode, 220);
|
|
return 'data:image/svg+xml;base64,' . base64_encode($svg);
|
|
} catch (\Throwable $e) {
|
|
log_message('error', '[ABHA] Hospital card QR generation failed: ' . $e->getMessage());
|
|
return '';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return array<string, string>
|
|
*/
|
|
private function loadHospitalBranding(): array
|
|
{
|
|
$branding = [
|
|
'hospital_name' => '',
|
|
'hospital_logo_url' => base_url('assets/img/logo.png'),
|
|
'brand_name' => 'HMS',
|
|
];
|
|
|
|
try {
|
|
$db = \Config\Database::connect();
|
|
if (! $db->tableExists('hospital_setting')) {
|
|
return $branding;
|
|
}
|
|
|
|
$rows = $db->table('hospital_setting')
|
|
->select('s_name, s_value')
|
|
->whereIn('s_name', [
|
|
'H_Name',
|
|
'H_logo',
|
|
'HOSPITAL_NAME',
|
|
'HOSPITAL_DISPLAY_NAME',
|
|
'HOSPITAL_TITLE',
|
|
])
|
|
->orderBy('id', 'DESC')
|
|
->get()
|
|
->getResultArray();
|
|
|
|
$settings = [];
|
|
foreach ($rows as $row) {
|
|
$key = trim((string) ($row['s_name'] ?? ''));
|
|
if ($key === '' || array_key_exists($key, $settings)) {
|
|
continue;
|
|
}
|
|
$settings[$key] = trim((string) ($row['s_value'] ?? ''));
|
|
}
|
|
|
|
foreach (['H_Name', 'HOSPITAL_NAME', 'HOSPITAL_DISPLAY_NAME', 'HOSPITAL_TITLE'] as $nameKey) {
|
|
if (! empty($settings[$nameKey])) {
|
|
$branding['hospital_name'] = (string) $settings[$nameKey];
|
|
break;
|
|
}
|
|
}
|
|
|
|
$logoName = trim((string) ($settings['H_logo'] ?? ''));
|
|
if ($logoName !== '') {
|
|
$branding['hospital_logo_url'] = base_url('assets/images/' . rawurlencode($logoName));
|
|
}
|
|
|
|
if ($branding['hospital_name'] !== '') {
|
|
$branding['brand_name'] = $branding['hospital_name'];
|
|
}
|
|
} catch (\Throwable $e) {
|
|
// Keep default branding if settings lookup fails.
|
|
}
|
|
|
|
if ($branding['hospital_name'] === '') {
|
|
$branding['hospital_name'] = 'E-Atria Hospital';
|
|
}
|
|
|
|
return $branding;
|
|
}
|
|
|
|
/**
|
|
* @param array<string,mixed> $patient
|
|
*/
|
|
private function resolvePatientPhotoUrl(array $patient): string
|
|
{
|
|
$photoPath = '/assets/images/no_image.svg';
|
|
|
|
$profileFileId = (int) ($patient['profile_file_id'] ?? 0);
|
|
if ($profileFileId > 0) {
|
|
try {
|
|
$db = \Config\Database::connect();
|
|
if ($db->tableExists('file_upload_data')) {
|
|
$fileRow = $db->table('file_upload_data')
|
|
->select('full_path')
|
|
->where('id', $profileFileId)
|
|
->get()
|
|
->getRowArray();
|
|
$fullPath = trim((string) ($fileRow['full_path'] ?? ''));
|
|
if ($fullPath !== '') {
|
|
$pos = strpos($fullPath, '/uploads/', 1);
|
|
if ($pos !== false) {
|
|
return substr($fullPath, $pos);
|
|
}
|
|
return $fullPath;
|
|
}
|
|
}
|
|
} catch (\Throwable $e) {
|
|
// Fall through to next source.
|
|
}
|
|
}
|
|
|
|
$profilePicture = trim((string) ($patient['profile_picture'] ?? ''));
|
|
if ($profilePicture !== '') {
|
|
$pos = strpos($profilePicture, '/uploads/', 1);
|
|
if ($pos !== false) {
|
|
return substr($profilePicture, $pos);
|
|
}
|
|
return $profilePicture;
|
|
}
|
|
|
|
$abhaPhotoBase64 = trim((string) ($patient['abha_profile_photo_base64'] ?? ''));
|
|
if ($abhaPhotoBase64 !== '') {
|
|
return str_starts_with($abhaPhotoBase64, 'data:image')
|
|
? $abhaPhotoBase64
|
|
: 'data:image/jpeg;base64,' . $abhaPhotoBase64;
|
|
}
|
|
|
|
return $photoPath;
|
|
}
|
|
|
|
private function buildCode39Svg(string $value): string
|
|
{
|
|
$layout = $this->buildCode39Layout($value);
|
|
if ($layout === null) {
|
|
return '';
|
|
}
|
|
|
|
$rects = [];
|
|
foreach ($layout['bars'] as $bar) {
|
|
$rects[] = '<rect x="' . $bar['x'] . '" y="0" width="' . $bar['w'] . '" height="' . $layout['height'] . '" fill="#111" />';
|
|
}
|
|
|
|
return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' . $layout['width'] . ' ' . $layout['height'] . '" role="img" aria-label="HMS Barcode">'
|
|
. implode('', $rects)
|
|
. '</svg>';
|
|
}
|
|
|
|
private function buildCode39ImageDataUri(string $value, string $svgFallback = ''): string
|
|
{
|
|
$layout = $this->buildCode39Layout($value);
|
|
if ($layout === null) {
|
|
return '';
|
|
}
|
|
|
|
if (! function_exists('imagecreatetruecolor') || ! function_exists('imagepng')) {
|
|
$svg = $svgFallback !== '' ? $svgFallback : $this->buildCode39Svg($value);
|
|
return $svg !== '' ? 'data:image/svg+xml;base64,' . base64_encode($svg) : '';
|
|
}
|
|
|
|
$img = imagecreatetruecolor($layout['width'], $layout['height']);
|
|
if ($img === false) {
|
|
$svg = $svgFallback !== '' ? $svgFallback : $this->buildCode39Svg($value);
|
|
return $svg !== '' ? 'data:image/svg+xml;base64,' . base64_encode($svg) : '';
|
|
}
|
|
|
|
$white = imagecolorallocate($img, 255, 255, 255);
|
|
$black = imagecolorallocate($img, 17, 17, 17);
|
|
imagefilledrectangle($img, 0, 0, $layout['width'], $layout['height'], $white);
|
|
|
|
foreach ($layout['bars'] as $bar) {
|
|
imagefilledrectangle(
|
|
$img,
|
|
$bar['x'],
|
|
0,
|
|
$bar['x'] + $bar['w'] - 1,
|
|
$layout['height'],
|
|
$black
|
|
);
|
|
}
|
|
|
|
ob_start();
|
|
imagepng($img);
|
|
$pngBinary = ob_get_clean();
|
|
imagedestroy($img);
|
|
|
|
if (! is_string($pngBinary) || $pngBinary === '') {
|
|
$svg = $svgFallback !== '' ? $svgFallback : $this->buildCode39Svg($value);
|
|
return $svg !== '' ? 'data:image/svg+xml;base64,' . base64_encode($svg) : '';
|
|
}
|
|
|
|
return 'data:image/png;base64,' . base64_encode($pngBinary);
|
|
}
|
|
|
|
/**
|
|
* @return array{bars: list<array{x:int,w:int}>, width:int, height:int}|null
|
|
*/
|
|
private function buildCode39Layout(string $value): ?array
|
|
{
|
|
$normalized = strtoupper(trim($value));
|
|
if ($normalized === '') {
|
|
return null;
|
|
}
|
|
|
|
$supported = [
|
|
'0' => 'nnnwwnwnn', '1' => 'wnnwnnnnw', '2' => 'nnwwnnnnw', '3' => 'wnwwnnnnn',
|
|
'4' => 'nnnwwnnnw', '5' => 'wnnwwnnnn', '6' => 'nnwwwnnnn', '7' => 'nnnwnnwnw',
|
|
'8' => 'wnnwnnwnn', '9' => 'nnwwnnwnn', 'A' => 'wnnnnwnnw', 'B' => 'nnwnnwnnw',
|
|
'C' => 'wnwnnwnnn', 'D' => 'nnnnwwnnw', 'E' => 'wnnnwwnnn', 'F' => 'nnwnwwnnn',
|
|
'G' => 'nnnnnwwnw', 'H' => 'wnnnnwwnn', 'I' => 'nnwnnwwnn', 'J' => 'nnnnwwwnn',
|
|
'K' => 'wnnnnnnww', 'L' => 'nnwnnnnww', 'M' => 'wnwnnnnwn', 'N' => 'nnnnwnnww',
|
|
'O' => 'wnnnwnnwn', 'P' => 'nnwnwnnwn', 'Q' => 'nnnnnnwww', 'R' => 'wnnnnnwwn',
|
|
'S' => 'nnwnnnwwn', 'T' => 'nnnnwnwwn', 'U' => 'wwnnnnnnw', 'V' => 'nwwnnnnnw',
|
|
'W' => 'wwwnnnnnn', 'X' => 'nwnnwnnnw', 'Y' => 'wwnnwnnnn', 'Z' => 'nwwnwnnnn',
|
|
'-' => 'nwnnnnwnw', '.' => 'wwnnnnwnn', ' ' => 'nwwnnnwnn', '$' => 'nwnwnwnnn',
|
|
'/' => 'nwnwnnnwn', '+' => 'nwnnnwnwn', '%' => 'nnnwnwnwn', '*' => 'nwnnwnwnn',
|
|
];
|
|
|
|
$payload = '*' . $normalized . '*';
|
|
$clean = '';
|
|
$len = strlen($payload);
|
|
for ($i = 0; $i < $len; $i++) {
|
|
$ch = $payload[$i];
|
|
if (! isset($supported[$ch])) {
|
|
continue;
|
|
}
|
|
$clean .= $ch;
|
|
}
|
|
|
|
if ($clean === '') {
|
|
return null;
|
|
}
|
|
|
|
$narrow = 2;
|
|
$wide = 5;
|
|
$height = 56;
|
|
$quiet = 10;
|
|
$gap = 2;
|
|
|
|
$x = $quiet;
|
|
$bars = [];
|
|
$cleanLen = strlen($clean);
|
|
for ($i = 0; $i < $cleanLen; $i++) {
|
|
$pattern = $supported[$clean[$i]];
|
|
for ($j = 0; $j < 9; $j++) {
|
|
$w = ($pattern[$j] === 'w') ? $wide : $narrow;
|
|
if ($j % 2 === 0) {
|
|
$bars[] = ['x' => $x, 'w' => $w];
|
|
}
|
|
$x += $w;
|
|
}
|
|
if ($i < ($cleanLen - 1)) {
|
|
$x += $gap;
|
|
}
|
|
}
|
|
|
|
return [
|
|
'bars' => $bars,
|
|
'width' => $x + $quiet,
|
|
'height' => $height,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Normalize nested bridge error payloads into a single readable message.
|
|
*
|
|
* @param array<string, mixed> $result
|
|
*/
|
|
private function extractBridgeErrorText(array $result, string $fallback): string
|
|
{
|
|
$candidates = [
|
|
$result['error_text'] ?? null,
|
|
is_string($result['error'] ?? null) ? $result['error'] : null,
|
|
$result['message'] ?? null,
|
|
$result['error']['message'] ?? null,
|
|
$result['error']['description'] ?? null,
|
|
$result['error']['code'] ?? null,
|
|
$result['error']['error'] ?? null,
|
|
$result['error']['error_description'] ?? null,
|
|
is_string($result['data']['error'] ?? null) ? $result['data']['error'] : null,
|
|
$result['data']['message'] ?? null,
|
|
$result['data']['error']['message'] ?? null,
|
|
$result['data']['error']['description'] ?? null,
|
|
$result['data']['error']['code'] ?? null,
|
|
$result['data']['error']['error'] ?? null,
|
|
$result['data']['error']['error_description'] ?? null,
|
|
$result['details']['message'] ?? null,
|
|
$result['details']['error'] ?? null,
|
|
$result['description'] ?? null,
|
|
$result['data']['description'] ?? null,
|
|
];
|
|
|
|
$genericPhrases = [
|
|
'abdm gateway returned an error.',
|
|
'abdm_upstream_error',
|
|
'gateway error',
|
|
'upstream error',
|
|
'bad request',
|
|
'an error occurred.',
|
|
];
|
|
$firstGeneric = null;
|
|
|
|
foreach ($candidates as $candidate) {
|
|
if (is_string($candidate) && trim($candidate) !== '') {
|
|
$trimmed = trim($candidate);
|
|
if (stripos($trimmed, 'preferredAbhaAddress') !== false ||
|
|
(stripos($trimmed, 'Unknown column') !== false && stripos($trimmed, 'where clause') !== false) ||
|
|
stripos($trimmed, 'Please make a valid request') !== false) {
|
|
$isNumberLookup = (($result['type'] ?? '') === 'abha-number')
|
|
|| (isset($result['abha_id']) && !isset($result['abha_address']))
|
|
|| (isset($result['value']) && !str_contains((string) $result['value'], '@') && ctype_digit(str_replace(['-', ' '], '', (string) $result['value'])));
|
|
return ($isNumberLookup ? 'ABHA Number' : 'ABHA Address') . ' does not exist or was not found in the ABDM registry.';
|
|
}
|
|
if (stripos($trimmed, 'loginHint') !== false && stripos($trimmed, 'invalid') !== false) {
|
|
return 'ABDM gateway rejected login hint: OTP verification by ABHA Address is not supported directly by ABDM. Please verify using the 14-digit ABHA Number or Find ABHA via Mobile.';
|
|
}
|
|
if (stripos($trimmed, 'loginId') !== false && stripos($trimmed, 'invalid') !== false) {
|
|
return 'Aadhaar Number is not valid. Valid 12-digit Aadhaar number is required.';
|
|
}
|
|
// Normalize ABDM/UIDAI OTP errors: replace technical error codes with clear user-friendly messages (ABDM M1)
|
|
if (preg_match('/(?:UIDAI Error code\s*:\s*403|attempts for OTP match is exceeded|max(?:imum)? number of attempts.*exceeded)/i', $trimmed)) {
|
|
return 'Incorrect OTP. Maximum number of attempts exceeded. Please generate a fresh OTP and try again.';
|
|
}
|
|
if (preg_match('/(?:UIDAI Error code\s*:\s*400|Invalid (?:Aadhaar )?OTP value|Invalid OTP|Incorrect OTP|OTP mismatch|OTP.*not valid|OTP.*either expired or incorrect)/i', $trimmed)) {
|
|
return 'Incorrect OTP';
|
|
}
|
|
// Normalize OTP cooldown / rate-limit message to 60 seconds (ABDM M1 test compliance)
|
|
if (preg_match('/(?:after\s*30\s*seconds|request\s*(?:for\s*)?new\s*otp\s*after\s*30\s*seconds)/i', $trimmed)) {
|
|
return 'You can request for new OTP after 60 seconds';
|
|
}
|
|
// Strip raw technical error code prefixes (e.g. ABDM-1204 : or UIDAI Error code : 400 :)
|
|
$cleaned = preg_replace('/^(?:ABDM-\d+\s*:\s*)?(?:UIDAI Error code\s*:\s*\d+\s*:\s*)?/i', '', $trimmed);
|
|
if (preg_match('/(?:invalid.*otp|incorrect.*otp|otp.*invalid|otp.*incorrect|otp.*match)/i', $cleaned)) {
|
|
if (preg_match('/(?:attempts|exceeded|403)/i', $cleaned)) {
|
|
return 'Incorrect OTP. Maximum number of attempts exceeded. Please generate a fresh OTP and try again.';
|
|
}
|
|
return 'Incorrect OTP';
|
|
}
|
|
if (preg_match('/30\s*seconds/i', $cleaned)) {
|
|
$cleaned = preg_replace('/30\s*seconds/i', '60 seconds', $cleaned);
|
|
}
|
|
if (in_array(strtolower($cleaned), $genericPhrases, true)) {
|
|
$firstGeneric ??= $cleaned;
|
|
continue;
|
|
}
|
|
return $cleaned;
|
|
}
|
|
}
|
|
|
|
$ignoreFields = [
|
|
'timestamp', 'txnid', 'txn_id', 'authresult', 'auth_result',
|
|
'authmethod', 'auth_method', 'status', 'token', 'tokens',
|
|
'refreshtoken', 'refresh_token', 'expiresin', 'expires_in',
|
|
'refreshexpiresin', 'refresh_expires_in', 'request_id', 'requestid',
|
|
'scope', 'accounts', 'message', 'error', 'error_text', 'code',
|
|
];
|
|
|
|
$fieldErrors = [];
|
|
if (is_array($result['data'] ?? null)) {
|
|
foreach ($result['data'] as $field => $message) {
|
|
$lowerField = strtolower(trim((string) $field));
|
|
if (in_array($lowerField, $ignoreFields, true) || ! is_string($message) || trim($message) === '') {
|
|
continue;
|
|
}
|
|
$fieldMsg = trim($message);
|
|
if (strcasecmp($field, 'loginHint') === 0 && stripos($fieldMsg, 'invalid') !== false) {
|
|
$fieldErrors[] = 'ABDM gateway rejected login hint: OTP verification by ABHA Address is not supported directly by ABDM. Please verify using the 14-digit ABHA Number or Find ABHA via Mobile.';
|
|
} elseif (strcasecmp($field, 'loginId') === 0 && stripos($fieldMsg, 'invalid') !== false) {
|
|
$fieldErrors[] = 'Aadhaar Number is not valid. Valid 12-digit Aadhaar number is required.';
|
|
} elseif (preg_match('/(?:attempts.*exceeded|403)/i', $fieldMsg)) {
|
|
$fieldErrors[] = 'Incorrect OTP. Maximum number of attempts exceeded. Please generate a fresh OTP and try again.';
|
|
} elseif (preg_match('/(?:invalid.*otp|incorrect.*otp|otp.*match)/i', $fieldMsg)) {
|
|
$fieldErrors[] = 'Incorrect OTP';
|
|
} elseif (preg_match('/30\s*seconds/i', $fieldMsg)) {
|
|
$fieldErrors[] = preg_replace('/30\s*seconds/i', '60 seconds', $fieldMsg);
|
|
} else {
|
|
$fieldErrors[] = $field . ': ' . $fieldMsg;
|
|
}
|
|
}
|
|
}
|
|
if ($fieldErrors !== []) {
|
|
return implode('; ', $fieldErrors);
|
|
}
|
|
|
|
if ($firstGeneric !== null) {
|
|
return preg_replace('/30\s*seconds/i', '60 seconds', $firstGeneric);
|
|
}
|
|
|
|
if (preg_match('/(?:invalid.*otp|incorrect.*otp|otp.*expired)/i', $fallback)) {
|
|
return 'Incorrect OTP';
|
|
}
|
|
|
|
return preg_replace('/30\s*seconds/i', '60 seconds', $fallback);
|
|
}
|
|
|
|
/**
|
|
* UIDAI Verhoeff algorithm check for 12-digit Aadhaar number.
|
|
*/
|
|
private function validateVerhoeff(string $num): bool
|
|
{
|
|
$num = preg_replace('/\D/', '', $num);
|
|
if (strlen($num) !== 12 || $num[0] === '0' || $num[0] === '1') {
|
|
return false;
|
|
}
|
|
|
|
$d = [
|
|
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
|
[1, 2, 3, 4, 0, 6, 7, 8, 9, 5],
|
|
[2, 3, 4, 0, 1, 7, 8, 9, 5, 6],
|
|
[3, 4, 0, 1, 2, 8, 9, 5, 6, 7],
|
|
[4, 0, 1, 2, 3, 9, 5, 6, 7, 8],
|
|
[5, 9, 8, 7, 6, 0, 4, 3, 2, 1],
|
|
[6, 5, 9, 8, 7, 1, 0, 4, 3, 2],
|
|
[7, 6, 5, 9, 8, 2, 1, 0, 4, 3],
|
|
[8, 7, 6, 5, 9, 3, 2, 1, 0, 4],
|
|
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
|
|
];
|
|
$p = [
|
|
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
|
[1, 5, 7, 6, 2, 8, 3, 0, 9, 4],
|
|
[5, 8, 0, 3, 7, 9, 6, 1, 4, 2],
|
|
[8, 9, 1, 6, 0, 4, 3, 5, 2, 7],
|
|
[9, 4, 5, 3, 1, 2, 6, 8, 7, 0],
|
|
[4, 2, 8, 6, 5, 7, 3, 9, 0, 1],
|
|
[2, 7, 9, 3, 8, 0, 6, 4, 1, 5],
|
|
[7, 0, 4, 6, 9, 1, 3, 2, 5, 8]
|
|
];
|
|
$c = 0;
|
|
$invertedArray = array_reverse(str_split($num));
|
|
foreach ($invertedArray as $i => $digit) {
|
|
$c = $d[$c][$p[$i % 8][(int)$digit]];
|
|
}
|
|
return ($c === 0);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Step 5 — Confirm patient link/create after a "matching patients found"
|
|
// review by the operator (see findMatchingCandidates()).
|
|
// POST abha/create/confirm_patient
|
|
// -------------------------------------------------------------------------
|
|
public function confirmPatient()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$action = trim((string) ($this->request->getPost('action') ?? ''));
|
|
$updateMode = trim((string) ($this->request->getPost('update_mode') ?? 'update'));
|
|
$patientId = (int) ($this->request->getPost('patient_id') ?? 0);
|
|
$abhaNum = (string) ($this->request->getPost('abha_number') ?? '');
|
|
$name = trim((string) ($this->request->getPost('name') ?? ''));
|
|
$mobile = preg_replace('/\D/', '', (string) ($this->request->getPost('mobile') ?? ''));
|
|
$gender = (string) ($this->request->getPost('gender') ?? '');
|
|
$dob = (string) ($this->request->getPost('dob') ?? '');
|
|
|
|
if (! in_array($action, ['new', 'existing'], true)) {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Invalid action. Must be "new" or "existing".']);
|
|
}
|
|
if (! in_array($updateMode, ['keep', 'update'], true)) {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Invalid patient update mode.']);
|
|
}
|
|
|
|
$abhaMeta = [
|
|
'abha_address' => (string) ($this->request->getPost('abha_address') ?? ''),
|
|
'profile_photo' => (string) ($this->request->getPost('photo') ?? ''),
|
|
'card_base64' => (string) ($this->request->getPost('card_base64') ?? ''),
|
|
'verified_status' => (string) ($this->request->getPost('verified_status') ?? ''),
|
|
'verification_type' => (string) ($this->request->getPost('verification_type') ?? ''),
|
|
'kyc_verified' => $this->request->getPost('kyc_verified'),
|
|
'mobile_verified' => $this->request->getPost('mobile_verified'),
|
|
'address' => (string) ($this->request->getPost('address') ?? ''),
|
|
'district' => (string) ($this->request->getPost('district') ?? ''),
|
|
'state' => (string) ($this->request->getPost('state') ?? ''),
|
|
'zip' => (string) ($this->request->getPost('zip') ?? ''),
|
|
'email' => (string) ($this->request->getPost('email') ?? ''),
|
|
];
|
|
|
|
$db = \Config\Database::connect();
|
|
$fields = $db->getFieldNames('patient_master') ?? [];
|
|
$abhaField = $this->resolveAbhaFieldName($fields);
|
|
$abhaNumClean = preg_replace('/\D/', '', $abhaNum);
|
|
$abhaAddress = trim((string) ($abhaMeta['abha_address'] ?? ''));
|
|
|
|
if ($abhaNumClean === '' && $abhaAddress === '') {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Missing ABHA identifier.']);
|
|
}
|
|
|
|
// ABHA number must be unique across patient_master — never let two
|
|
// patient rows carry the same ABHA.
|
|
if ($abhaNumClean !== '') {
|
|
$formattedAbha = (strlen($abhaNumClean) === 14)
|
|
? substr($abhaNumClean, 0, 2) . '-' . substr($abhaNumClean, 2, 4) . '-' . substr($abhaNumClean, 6, 4) . '-' . substr($abhaNumClean, 10, 4)
|
|
: '';
|
|
$checkVals = array_values(array_filter([$abhaNumClean, $formattedAbha]));
|
|
|
|
$conflictBuilder = $db->table('patient_master')->select('id,p_code,p_fname');
|
|
$hasAbhaCheck = false;
|
|
$conflictBuilder->groupStart();
|
|
foreach (['abha_id', 'abha_no', 'abha'] as $cand) {
|
|
if (in_array($cand, $fields, true)) {
|
|
if (! $hasAbhaCheck) {
|
|
$conflictBuilder->whereIn($cand, $checkVals);
|
|
$hasAbhaCheck = true;
|
|
} else {
|
|
$conflictBuilder->orWhereIn($cand, $checkVals);
|
|
}
|
|
}
|
|
}
|
|
$conflictBuilder->groupEnd();
|
|
|
|
if ($hasAbhaCheck) {
|
|
$conflict = $conflictBuilder->get()->getRowArray();
|
|
if ($conflict) {
|
|
$conflictId = (int) ($conflict['id'] ?? 0);
|
|
if ($action === 'new' || $conflictId !== $patientId) {
|
|
return $this->response->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => 'This ABHA number is already linked to patient '
|
|
. ($conflict['p_code'] ?? '') . ' (' . ($conflict['p_fname'] ?? '') . '). '
|
|
. 'Please select that patient instead.',
|
|
'conflict_patient_id' => $conflictId,
|
|
'conflict_p_code' => (string) ($conflict['p_code'] ?? ''),
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($action === 'existing') {
|
|
if ($patientId <= 0) {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'patient_id is required to link an existing patient.']);
|
|
}
|
|
$row = $db->table('patient_master')->where('id', $patientId)->get()->getRowArray();
|
|
if (! $row) {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Selected patient not found.']);
|
|
}
|
|
|
|
// Guard: this patient must not already carry a *different* ABHA number.
|
|
if ($abhaField && ! empty($row[$abhaField]) && $abhaNumClean !== ''
|
|
&& preg_replace('/\D/', '', (string) $row[$abhaField]) !== $abhaNumClean) {
|
|
return $this->response->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => 'Selected patient already has a different ABHA linked (' . $row[$abhaField] . '). Cannot link a second ABHA to the same patient.',
|
|
]);
|
|
}
|
|
|
|
$patientInfo = $this->linkAbhaToPatient(
|
|
$db,
|
|
$patientId,
|
|
$updateMode === 'update' ? $name : '',
|
|
$updateMode === 'update' ? $mobile : '',
|
|
$updateMode === 'update' ? $gender : '',
|
|
$updateMode === 'update' ? $dob : '',
|
|
$updateMode === 'update' ? $abhaMeta : array_diff_key($abhaMeta, array_flip(['address', 'district', 'state', 'zip', 'email'])),
|
|
$abhaNumClean,
|
|
$abhaAddress,
|
|
$fields,
|
|
$abhaField,
|
|
$updateMode === 'update'
|
|
);
|
|
} else {
|
|
$patientInfo = $this->createPatientFromAbha($db, $name, $mobile, $gender, $dob, $abhaMeta, $abhaNumClean, $abhaAddress, $fields, $abhaField);
|
|
}
|
|
|
|
return $this->response->setJSON([
|
|
'ok' => 1,
|
|
'patient_id' => $patientInfo['patient_id'],
|
|
'p_code' => $patientInfo['p_code'],
|
|
'is_new_patient' => $patientInfo['is_new'],
|
|
]);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Private helper — resolve the column name used to store the ABHA number
|
|
// in patient_master (varies by installation/migration history).
|
|
// -------------------------------------------------------------------------
|
|
private function resolveAbhaFieldName(array $fields): ?string
|
|
{
|
|
foreach (['abha_id', 'abha_no', 'abha'] as $f) {
|
|
if (in_array($f, $fields, true)) {
|
|
return $f;
|
|
}
|
|
}
|
|
if (in_array('abha_address', $fields, true)) {
|
|
return 'abha_address';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Try to safely auto-resolve the local patient WITHOUT operator confirmation.
|
|
* Only used when there's no real ambiguity:
|
|
* - exact ABHA-number match (this exact digital identity was already linked here before), or
|
|
* - an unambiguous orphan "ABHA PATIENT" placeholder row (HMS's own incomplete record, not a
|
|
* different real patient).
|
|
* Returns null when neither applies — caller should then run findMatchingCandidates()
|
|
* and ask the operator to confirm "Create New" vs "Update Existing".
|
|
*
|
|
* @return array{patient_id:int,p_code:string,is_new:bool}|null
|
|
*/
|
|
private function tryAutoLinkByDirectMatch(
|
|
string $abhaNum,
|
|
string $name,
|
|
string $mobile,
|
|
string $gender,
|
|
string $dob,
|
|
array $abhaMeta = []
|
|
): ?array {
|
|
$db = \Config\Database::connect();
|
|
$fields = $db->getFieldNames('patient_master') ?? [];
|
|
$abhaField = $this->resolveAbhaFieldName($fields);
|
|
$abhaNumClean = preg_replace('/\D/', '', $abhaNum);
|
|
$abhaAddress = trim((string) ($abhaMeta['abha_address'] ?? ''));
|
|
|
|
$existing = null;
|
|
if ($abhaField && $abhaNumClean !== '') {
|
|
$existing = $db->table('patient_master')
|
|
->where($abhaField, $abhaNumClean)->get()->getRowArray();
|
|
if (! $existing && $abhaNum !== $abhaNumClean) {
|
|
$existing = $db->table('patient_master')
|
|
->where($abhaField, $abhaNum)->get()->getRowArray();
|
|
}
|
|
}
|
|
|
|
if ($existing) {
|
|
$patientId = (int) ($existing['id'] ?? 0);
|
|
return $this->linkAbhaToPatient($db, $patientId, $name, $mobile, $gender, $dob, $abhaMeta, $abhaNumClean, $abhaAddress, $fields, $abhaField);
|
|
}
|
|
|
|
// Safe repair path for orphan placeholder rows created during prior failed mapping.
|
|
// Reuse only when there is exactly one unambiguous candidate with no clinical linkage.
|
|
$placeholder = $this->findRepairablePlaceholderPatient($db, $fields);
|
|
if ($placeholder !== null) {
|
|
$patientId = (int) ($placeholder['id'] ?? 0);
|
|
if ($patientId > 0) {
|
|
return $this->repairPlaceholderPatient($db, $patientId, $name, $mobile, $gender, $dob, $abhaMeta, $abhaNumClean, $abhaAddress, $fields, $abhaField);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* ABHA returns DOB as DD-MM-YYYY while patient_master stores YYYY-MM-DD.
|
|
*/
|
|
private function extractBirthYear(string $dob): string
|
|
{
|
|
$dob = trim($dob);
|
|
if ($dob === '') {
|
|
return '';
|
|
}
|
|
|
|
$normalized = $this->normalizeDobToDb($dob);
|
|
if ($normalized !== '' && preg_match('/^(\d{4})/', $normalized, $match) === 1) {
|
|
return $match[1];
|
|
}
|
|
|
|
return preg_match('/(?<!\d)(\d{4})(?!\d)/', $dob, $match) === 1 ? $match[1] : '';
|
|
}
|
|
|
|
/**
|
|
* Compute whole-years age from a dob string ('DD-MM-YYYY' or 'YYYY-MM-DD').
|
|
*/
|
|
private function computeAgeYears(string $dob): ?int
|
|
{
|
|
$dobDb = $this->normalizeDobToDb($dob);
|
|
if ($dobDb === '') {
|
|
return null;
|
|
}
|
|
try {
|
|
return (int) (new \DateTime($dobDb))->diff(new \DateTime())->y;
|
|
} catch (\Throwable $e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private function genderLabel(int $g): string
|
|
{
|
|
if ($g === 2) {
|
|
return 'Female';
|
|
}
|
|
if ($g === 3) {
|
|
return 'Other';
|
|
}
|
|
if ($g === 1) {
|
|
return 'Male';
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Search patient_master for possible existing local records matching the
|
|
* ABHA profile by name/age/gender (and mobile/aadhaar when available), so
|
|
* the operator can confirm "this is the same person" before linking, or
|
|
* choose to register as a brand-new patient. HMS never auto-creates a new
|
|
* patient record on its own once this search has run — the operator always
|
|
* makes the final call.
|
|
*
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function findMatchingCandidates(
|
|
$db,
|
|
array $fields,
|
|
string $name,
|
|
string $mobile,
|
|
string $genderRaw,
|
|
string $dob,
|
|
string $aadhaar,
|
|
?string $abhaField,
|
|
string $abhaNumClean
|
|
): array {
|
|
$genderDb = $this->toPatientGenderValue($genderRaw);
|
|
// ABHA sends DOB as DD-MM-YYYY, patient_master stores YYYY-MM-DD.
|
|
$birthYear = $this->extractBirthYear($dob);
|
|
$nameUp = strtoupper(trim($name));
|
|
$nameTokens = array_values(array_filter(preg_split('/\s+/', $nameUp) ?: [], fn ($t) => strlen($t) >= 3));
|
|
$aadhaar = preg_replace('/\D/', '', $aadhaar);
|
|
|
|
if ($nameTokens === [] && $mobile === '' && $aadhaar === '' && $abhaNumClean === '' && $birthYear === '') {
|
|
return [];
|
|
}
|
|
|
|
$candidateAbhaCols = array_values(array_intersect(['abha_id', 'abha_no', 'abha', 'abha_address'], $fields));
|
|
$selectFields = array_values(array_intersect(
|
|
['id', 'p_code', 'p_fname', 'gender', 'dob', 'age', 'mphone1', 'udai', 'add1', 'district', 'state', 'zip', 'profile_file_id', 'profile_picture', 'abha_profile_photo_base64'],
|
|
$fields
|
|
));
|
|
foreach ($candidateAbhaCols as $cCol) {
|
|
if (! in_array($cCol, $selectFields, true)) {
|
|
$selectFields[] = $cCol;
|
|
}
|
|
}
|
|
$select = implode(',', $selectFields);
|
|
|
|
// Two targeted passes: exact identifiers can never be crowded out by a
|
|
// common name, and gender alone is a filter (see scoring) not a selector.
|
|
$rows = [];
|
|
$collect = static function (callable $applyConditions) use ($db, $select, &$rows): void {
|
|
$builder = $db->table('patient_master')->select($select);
|
|
$builder->groupStart();
|
|
$applyConditions($builder);
|
|
$builder->groupEnd();
|
|
foreach ($builder->orderBy('insert_date', 'DESC')->limit(200)->get()->getResultArray() as $row) {
|
|
$rows[(int) ($row['id'] ?? 0)] = $row;
|
|
}
|
|
};
|
|
|
|
$formattedAbha = (strlen($abhaNumClean) === 14)
|
|
? substr($abhaNumClean, 0, 2) . '-' . substr($abhaNumClean, 2, 4) . '-' . substr($abhaNumClean, 6, 4) . '-' . substr($abhaNumClean, 10, 4)
|
|
: '';
|
|
$abhaVals = array_values(array_filter([$abhaNumClean, $formattedAbha]));
|
|
|
|
$collect(static function ($builder) use ($candidateAbhaCols, $abhaVals, $aadhaar, $mobile, $fields): void {
|
|
$hasAny = false;
|
|
if (! empty($abhaVals) && ! empty($candidateAbhaCols)) {
|
|
foreach ($candidateAbhaCols as $cCol) {
|
|
if (! $hasAny) {
|
|
$builder->whereIn($cCol, $abhaVals);
|
|
$hasAny = true;
|
|
} else {
|
|
$builder->orWhereIn($cCol, $abhaVals);
|
|
}
|
|
}
|
|
}
|
|
if ($aadhaar !== '' && in_array('udai', $fields, true)) {
|
|
$formattedAadhaar = substr($aadhaar, 0, 4) . ' ' . substr($aadhaar, 4, 4) . ' ' . substr($aadhaar, 8, 4);
|
|
if (! $hasAny) {
|
|
$builder->whereIn('udai', [$aadhaar, $formattedAadhaar]);
|
|
$hasAny = true;
|
|
} else {
|
|
$builder->orWhereIn('udai', [$aadhaar, $formattedAadhaar]);
|
|
}
|
|
}
|
|
if ($aadhaar !== '' && in_array('udai_hash', $fields, true)) {
|
|
$vault = new \App\Libraries\AadhaarVaultService();
|
|
$hash = $vault->hash($aadhaar);
|
|
if ($hash !== '') {
|
|
if (! $hasAny) {
|
|
$builder->where('udai_hash', $hash);
|
|
$hasAny = true;
|
|
} else {
|
|
$builder->orWhere('udai_hash', $hash);
|
|
}
|
|
}
|
|
}
|
|
if ($mobile !== '' && in_array('mphone1', $fields, true)) {
|
|
if (! $hasAny) {
|
|
$builder->where('mphone1', $mobile);
|
|
$hasAny = true;
|
|
} else {
|
|
$builder->orWhere('mphone1', $mobile);
|
|
}
|
|
}
|
|
});
|
|
|
|
if ($nameTokens !== []) {
|
|
$collect(static function ($builder) use ($nameTokens): void {
|
|
foreach ($nameTokens as $token) {
|
|
$builder->orLike('p_fname', $token);
|
|
}
|
|
});
|
|
}
|
|
|
|
if ($rows === []) {
|
|
return [];
|
|
}
|
|
|
|
$candidates = [];
|
|
foreach ($rows as $row) {
|
|
$rowName = strtoupper(trim((string) ($row['p_fname'] ?? '')));
|
|
$rowTokens = array_values(array_filter(preg_split('/\s+/', $rowName) ?: [], fn ($t) => strlen($t) >= 3));
|
|
$nameOverlap = $nameTokens !== [] && $rowTokens !== [] && $nameTokens[0] === $rowTokens[0];
|
|
|
|
$rowAgeYears = null;
|
|
$rowDob = trim((string) ($row['dob'] ?? ''));
|
|
if ($rowDob !== '' && $rowDob !== '0000-00-00') {
|
|
try {
|
|
$rowAgeYears = (int) (new \DateTime($rowDob))->diff(new \DateTime())->y;
|
|
} catch (\Throwable $e) {
|
|
$rowAgeYears = null;
|
|
}
|
|
}
|
|
if ($rowAgeYears === null && (int) ($row['age'] ?? 0) > 0) {
|
|
$rowAgeYears = (int) $row['age'];
|
|
}
|
|
|
|
$rowBirthYear = $this->extractBirthYear($rowDob);
|
|
$birthYearMatch = $birthYear !== '' && $rowBirthYear !== '' && $rowBirthYear === $birthYear;
|
|
$ageMatch = $birthYearMatch;
|
|
$genderMatch = $genderDb > 0 && (int) ($row['gender'] ?? 0) === $genderDb;
|
|
$mobileMatch = $mobile !== '' && preg_replace('/\D/', '', (string) ($row['mphone1'] ?? '')) === $mobile;
|
|
$aadhaarMatch = $aadhaar !== '' && preg_replace('/\D/', '', (string) ($row['udai'] ?? '')) === $aadhaar;
|
|
|
|
$rowAbha = '';
|
|
$abhaMatch = false;
|
|
foreach ($candidateAbhaCols as $cCol) {
|
|
$val = trim((string) ($row[$cCol] ?? ''));
|
|
if ($val !== '') {
|
|
if ($rowAbha === '') {
|
|
$rowAbha = $val;
|
|
}
|
|
$digitsVal = preg_replace('/\D/', '', $val);
|
|
if ($abhaNumClean !== '' && $digitsVal === $abhaNumClean) {
|
|
$abhaMatch = true;
|
|
$rowAbha = $val;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
$abhaConflict = $rowAbha !== '' && preg_replace('/\D/', '', $rowAbha) !== $abhaNumClean;
|
|
|
|
$minimumDemographicMatch = $nameOverlap && $birthYearMatch && $genderMatch;
|
|
if (! $abhaMatch && ! $aadhaarMatch && ! $minimumDemographicMatch) {
|
|
continue;
|
|
}
|
|
|
|
// Top score to exact ABHA (1000) and Aadhaar (500) matches
|
|
$score = ($abhaMatch ? 1000 : 0) + ($aadhaarMatch ? 500 : 0)
|
|
+ ($minimumDemographicMatch ? 5 : 0) + ($nameOverlap ? 2 : 0)
|
|
+ ($birthYearMatch ? 2 : 0) + ($genderMatch ? 1 : 0) + ($mobileMatch ? 4 : 0);
|
|
|
|
$candidates[] = [
|
|
'id' => (int) ($row['id'] ?? 0),
|
|
'p_code' => (string) ($row['p_code'] ?? ''),
|
|
'name' => trim((string) ($row['p_fname'] ?? '')),
|
|
'gender' => (int) ($row['gender'] ?? 0),
|
|
'gender_label' => $this->genderLabel((int) ($row['gender'] ?? 0)),
|
|
'dob' => $rowDob,
|
|
'age' => $rowAgeYears,
|
|
'mobile' => (string) ($row['mphone1'] ?? ''),
|
|
'photo_url' => $this->resolvePatientPhotoUrl($row),
|
|
'address' => trim(implode(', ', array_filter([
|
|
(string) ($row['add1'] ?? ''),
|
|
(string) ($row['district'] ?? ''),
|
|
(string) ($row['state'] ?? ''),
|
|
(string) ($row['zip'] ?? ''),
|
|
]))),
|
|
'aadhaar' => (string) ($row['udai'] ?? ''),
|
|
'abha' => $rowAbha,
|
|
'abha_conflict' => $abhaConflict,
|
|
'match' => [
|
|
'name' => $nameOverlap,
|
|
'age' => $ageMatch,
|
|
'gender' => $genderMatch,
|
|
'mobile' => $mobileMatch,
|
|
'aadhaar' => $aadhaarMatch,
|
|
'abha' => $abhaMatch,
|
|
],
|
|
'score' => $score,
|
|
];
|
|
}
|
|
|
|
usort($candidates, fn ($a, $b) => $b['score'] <=> $a['score']);
|
|
|
|
return array_slice($candidates, 0, 8);
|
|
}
|
|
|
|
/**
|
|
* Link an ABHA profile to an already-identified existing patient row
|
|
* (either auto-matched by exact ABHA number, or explicitly chosen by the
|
|
* operator from the "matching patients" confirmation modal). Only
|
|
* backfills fields that are currently blank so verified/manually-entered
|
|
* HMS data is never clobbered.
|
|
*
|
|
* @return array{patient_id:int,p_code:string,is_new:bool}
|
|
*/
|
|
private function linkAbhaToPatient(
|
|
$db,
|
|
int $patientId,
|
|
string $name,
|
|
string $mobile,
|
|
string $gender,
|
|
string $dob,
|
|
array $abhaMeta,
|
|
string $abhaNumClean,
|
|
string $abhaAddress,
|
|
array $fields,
|
|
?string $abhaField,
|
|
bool $updateDetails = false
|
|
): array {
|
|
$existing = $db->table('patient_master')->where('id', $patientId)->get()->getRowArray() ?? [];
|
|
$pCode = (string) ($existing['p_code'] ?? '');
|
|
$existingUpdates = [];
|
|
|
|
$existingName = strtoupper(trim((string) ($existing['p_fname'] ?? '')));
|
|
if ($name !== '' && ($updateDetails || $existingName === '' || $existingName === 'ABHA PATIENT')) {
|
|
$existingUpdates['p_fname'] = strtoupper($name);
|
|
}
|
|
|
|
// Backfill DOB/gender/mobile from ABHA profile only if not already recorded,
|
|
// so we never clobber a manually-corrected value.
|
|
$existingDob = trim((string) ($existing['dob'] ?? ''));
|
|
if (($updateDetails || $existingDob === '' || $existingDob === '0000-00-00') && $dob !== '') {
|
|
$dobDb = $this->normalizeDobToDb($dob);
|
|
if ($dobDb !== '') {
|
|
$existingUpdates['dob'] = $dobDb;
|
|
if (in_array('estimate_dob', $fields, true)) {
|
|
$existingUpdates['estimate_dob'] = 0;
|
|
}
|
|
}
|
|
}
|
|
$existingGenderRaw = (int) ($existing['gender'] ?? 0);
|
|
if (($updateDetails || $existingGenderRaw <= 0) && $gender !== '') {
|
|
$existingUpdates['gender'] = $this->toPatientGenderValue($gender);
|
|
}
|
|
if ($mobile !== '' && ($updateDetails || empty($existing['mphone1']))) {
|
|
$existingUpdates['mphone1'] = $mobile;
|
|
}
|
|
|
|
// Backfill ABHA field if it was empty
|
|
if ($abhaField && empty($existing[$abhaField]) && $abhaNumClean !== '') {
|
|
$existingUpdates[$abhaField] = $abhaNumClean;
|
|
}
|
|
|
|
if ($existingUpdates !== []) {
|
|
$db->table('patient_master')->where('id', $patientId)->update($existingUpdates);
|
|
}
|
|
|
|
$this->syncAbhaMetaToPatient($patientId, $abhaMeta, $abhaNumClean, $abhaAddress, $fields);
|
|
|
|
return ['patient_id' => $patientId, 'p_code' => $pCode, 'is_new' => false];
|
|
}
|
|
|
|
/**
|
|
* Repair a single unambiguous orphan "ABHA PATIENT" placeholder row created
|
|
* during a prior failed mapping. Not a "new patient" from the operator's
|
|
* perspective — this is HMS fixing its own incomplete record.
|
|
*
|
|
* @return array{patient_id:int,p_code:string,is_new:bool}
|
|
*/
|
|
private function repairPlaceholderPatient(
|
|
$db,
|
|
int $patientId,
|
|
string $name,
|
|
string $mobile,
|
|
string $gender,
|
|
string $dob,
|
|
array $abhaMeta,
|
|
string $abhaNumClean,
|
|
string $abhaAddress,
|
|
array $fields,
|
|
?string $abhaField
|
|
): array {
|
|
$repairData = [
|
|
'p_fname' => strtoupper($name !== '' ? $name : 'ABHA PATIENT'),
|
|
'mphone1' => $mobile,
|
|
'gender' => $this->toPatientGenderValue($gender),
|
|
'estimate_dob' => 1,
|
|
];
|
|
|
|
$dobDb = $this->normalizeDobToDb($dob);
|
|
if ($dobDb !== '') {
|
|
$repairData['dob'] = $dobDb;
|
|
$repairData['estimate_dob'] = 0;
|
|
}
|
|
|
|
if ($abhaField && $abhaNumClean !== '') {
|
|
$repairData[$abhaField] = $abhaNumClean;
|
|
}
|
|
if ($abhaAddress !== '' && in_array('abha_address', $fields, true)) {
|
|
$repairData['abha_address'] = $abhaAddress;
|
|
}
|
|
|
|
$this->applyAbhaMetaColumns($repairData, $abhaMeta, $fields);
|
|
if (($abhaNumClean !== '' || $abhaAddress !== '') && in_array('abdm_linked_at', $fields, true)) {
|
|
$repairData['abdm_linked_at'] = date('Y-m-d H:i:s');
|
|
}
|
|
|
|
$db->table('patient_master')->where('id', $patientId)->update($repairData);
|
|
|
|
$row = $db->table('patient_master')->select('p_code')->where('id', $patientId)->get()->getRowArray();
|
|
$pCode = (string) ($row['p_code'] ?? '');
|
|
|
|
$this->syncAbhaMetaToPatient($patientId, $abhaMeta, $abhaNumClean, $abhaAddress, $fields);
|
|
|
|
return ['patient_id' => $patientId, 'p_code' => $pCode, 'is_new' => false];
|
|
}
|
|
|
|
/**
|
|
* Create a brand-new patient_master row from an ABHA profile. Only ever
|
|
* called when the operator has explicitly chosen "Create New" from the
|
|
* matching-patients confirmation modal (or there was nothing to match).
|
|
*
|
|
* @return array{patient_id:int,p_code:string,is_new:bool}
|
|
*/
|
|
private function createPatientFromAbha(
|
|
$db,
|
|
string $name,
|
|
string $mobile,
|
|
string $gender,
|
|
string $dob,
|
|
array $abhaMeta,
|
|
string $abhaNumClean,
|
|
string $abhaAddress,
|
|
array $fields,
|
|
?string $abhaField
|
|
): array {
|
|
$genderDb = $this->toPatientGenderValue($gender);
|
|
$dobDb = $this->normalizeDobToDb($dob);
|
|
|
|
$insertData = [
|
|
'p_fname' => strtoupper($name !== '' ? $name : 'ABHA PATIENT'),
|
|
'mphone1' => $mobile,
|
|
'gender' => $genderDb,
|
|
'blood_group' => 'Not Define',
|
|
'estimate_dob' => $dobDb !== '' ? 0 : 1,
|
|
];
|
|
if ($dobDb !== '') {
|
|
$insertData['dob'] = $dobDb;
|
|
} else {
|
|
$insertData['age'] = 0;
|
|
$insertData['age_in_month'] = 0;
|
|
}
|
|
if ($abhaField && $abhaNumClean !== '') {
|
|
$insertData[$abhaField] = $abhaNumClean;
|
|
}
|
|
if ($abhaAddress !== '' && in_array('abha_address', $fields, true)) {
|
|
$insertData['abha_address'] = $abhaAddress;
|
|
}
|
|
|
|
$this->applyAbhaMetaColumns($insertData, $abhaMeta, $fields);
|
|
if (($abhaNumClean !== '' || $abhaAddress !== '') && in_array('abdm_linked_at', $fields, true)) {
|
|
$insertData['abdm_linked_at'] = date('Y-m-d H:i:s');
|
|
}
|
|
|
|
$patientModel = new \App\Models\PatientModel();
|
|
$patientId = $patientModel->insertPatient($insertData);
|
|
$pCode = '';
|
|
if ($patientId > 0) {
|
|
$row = $db->table('patient_master')->select('p_code')->where('id', $patientId)->get()->getRowArray();
|
|
$pCode = (string) ($row['p_code'] ?? '');
|
|
}
|
|
|
|
if ($patientId > 0) {
|
|
$this->syncAbhaMetaToPatient($patientId, $abhaMeta, $abhaNumClean, $abhaAddress, $fields);
|
|
}
|
|
|
|
return ['patient_id' => $patientId, 'p_code' => $pCode, 'is_new' => true];
|
|
}
|
|
|
|
private function syncAbhaMetaToPatient(int $patientId, array $abhaMeta, string $abhaNumClean, string $abhaAddress, array $fields): void
|
|
{
|
|
if ($patientId <= 0) {
|
|
return;
|
|
}
|
|
|
|
$db = \Config\Database::connect();
|
|
|
|
$updates = [];
|
|
if ($abhaAddress !== '' && in_array('abha_address', $fields, true)) {
|
|
$updates['abha_address'] = $abhaAddress;
|
|
}
|
|
|
|
$this->applyAbhaMetaColumns($updates, $abhaMeta, $fields);
|
|
|
|
if (($abhaNumClean !== '' || $abhaAddress !== '') && in_array('abdm_linked_at', $fields, true)) {
|
|
$updates['abdm_linked_at'] = date('Y-m-d H:i:s');
|
|
}
|
|
|
|
if ($updates !== []) {
|
|
$db->table('patient_master')->where('id', $patientId)->update($updates);
|
|
}
|
|
}
|
|
|
|
private function applyAbhaMetaColumns(array &$target, array $abhaMeta, array $fields): void
|
|
{
|
|
$verifiedStatus = trim((string) ($abhaMeta['verified_status'] ?? ''));
|
|
if ($verifiedStatus !== '' && in_array('abha_verified_status', $fields, true)) {
|
|
$target['abha_verified_status'] = strtoupper($verifiedStatus);
|
|
}
|
|
|
|
$verificationType = trim((string) ($abhaMeta['verification_type'] ?? ''));
|
|
if ($verificationType !== '' && in_array('abha_verification_type', $fields, true)) {
|
|
$target['abha_verification_type'] = strtoupper($verificationType);
|
|
}
|
|
|
|
if (array_key_exists('kyc_verified', $abhaMeta) && in_array('abha_kyc_verified', $fields, true)) {
|
|
$target['abha_kyc_verified'] = $this->toDbBool($abhaMeta['kyc_verified']);
|
|
}
|
|
|
|
if (array_key_exists('mobile_verified', $abhaMeta) && in_array('abha_mobile_verified', $fields, true)) {
|
|
$target['abha_mobile_verified'] = $this->toDbBool($abhaMeta['mobile_verified']);
|
|
}
|
|
|
|
$address = trim((string) ($abhaMeta['address'] ?? ''));
|
|
if ($address !== '' && in_array('add1', $fields, true)) {
|
|
$target['add1'] = $address;
|
|
}
|
|
|
|
$district = trim((string) ($abhaMeta['district'] ?? ''));
|
|
if ($district !== '' && in_array('district', $fields, true)) {
|
|
$target['district'] = strtoupper($district);
|
|
}
|
|
|
|
$state = trim((string) ($abhaMeta['state'] ?? ''));
|
|
if ($state !== '' && in_array('state', $fields, true)) {
|
|
$target['state'] = strtoupper($state);
|
|
}
|
|
|
|
$zip = trim((string) ($abhaMeta['zip'] ?? ''));
|
|
if ($zip !== '' && in_array('zip', $fields, true)) {
|
|
$target['zip'] = $zip;
|
|
}
|
|
|
|
$email = trim((string) ($abhaMeta['email'] ?? ''));
|
|
if ($email !== '' && in_array('email1', $fields, true)) {
|
|
$target['email1'] = $email;
|
|
}
|
|
|
|
$profilePhoto = trim((string) ($abhaMeta['profile_photo'] ?? ''));
|
|
if ($profilePhoto !== '' && in_array('abha_profile_photo_base64', $fields, true)) {
|
|
$target['abha_profile_photo_base64'] = $profilePhoto;
|
|
}
|
|
|
|
$cardBase64 = trim((string) ($abhaMeta['card_base64'] ?? ''));
|
|
if ($cardBase64 !== '' && in_array('abha_card_base64', $fields, true)) {
|
|
$target['abha_card_base64'] = $cardBase64;
|
|
}
|
|
}
|
|
|
|
private function toDbBool($value): int
|
|
{
|
|
if (is_bool($value)) {
|
|
return $value ? 1 : 0;
|
|
}
|
|
|
|
$v = strtolower(trim((string) $value));
|
|
if ($v === '1' || $v === 'true' || $v === 'yes' || $v === 'y') {
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
private function toPatientGenderValue(string $gender): int
|
|
{
|
|
$genderAbs = strtoupper(trim($gender));
|
|
if ($genderAbs === 'F' || $genderAbs === '2' || $genderAbs === 'FEMALE') {
|
|
return 2;
|
|
}
|
|
if ($genderAbs === 'O' || $genderAbs === '3' || $genderAbs === 'OTHER') {
|
|
return 3;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
private function normalizeDobToDb(string $dob): string
|
|
{
|
|
$dob = trim($dob);
|
|
if ($dob === '') {
|
|
return '';
|
|
}
|
|
|
|
if (preg_match('/^(\d{2})-(\d{2})-(\d{4})$/', $dob, $m)) {
|
|
return $m[3] . '-' . $m[2] . '-' . $m[1];
|
|
}
|
|
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $dob)) {
|
|
return $dob;
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Find one safe placeholder candidate to repair.
|
|
* Conditions:
|
|
* - name is ABHA PATIENT
|
|
* - ABHA id and address are empty
|
|
* - no OPD/invoice linkage
|
|
* - inserted recently (last 24h)
|
|
* - exactly one candidate (to avoid accidental overwrite)
|
|
*
|
|
* @param array<int, string> $fields
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
private function findRepairablePlaceholderPatient($db, array $fields): ?array
|
|
{
|
|
if (! $db->tableExists('patient_master')) {
|
|
return null;
|
|
}
|
|
|
|
$abhaField = null;
|
|
foreach (['abha_id', 'abha_no', 'abha'] as $f) {
|
|
if (in_array($f, $fields, true)) {
|
|
$abhaField = $f;
|
|
break;
|
|
}
|
|
}
|
|
|
|
$builder = $db->table('patient_master p')
|
|
->select('p.id,p.p_code,p.insert_date')
|
|
->where('UPPER(TRIM(COALESCE(p.p_fname, "")))', 'ABHA PATIENT')
|
|
->where('(p.mphone1 IS NULL OR TRIM(p.mphone1) = "")', null, false)
|
|
->where('p.insert_date >= DATE_SUB(NOW(), INTERVAL 24 HOUR)', null, false);
|
|
|
|
if ($abhaField !== null) {
|
|
$builder->where('(p.' . $abhaField . ' IS NULL OR TRIM(p.' . $abhaField . ') = "")', null, false);
|
|
}
|
|
if (in_array('abha_address', $fields, true)) {
|
|
$builder->where('(p.abha_address IS NULL OR TRIM(p.abha_address) = "")', null, false);
|
|
}
|
|
|
|
if ($db->tableExists('opd_master')) {
|
|
$builder->where('NOT EXISTS (SELECT 1 FROM opd_master o WHERE o.p_id = p.id)', null, false);
|
|
}
|
|
if ($db->tableExists('invoice_master')) {
|
|
$builder->where('NOT EXISTS (SELECT 1 FROM invoice_master i WHERE i.attach_type = 0 AND i.attach_id = p.id)', null, false);
|
|
}
|
|
|
|
$rows = $builder->orderBy('p.id', 'DESC')->limit(2)->get()->getResultArray();
|
|
if (count($rows) !== 1) {
|
|
return null;
|
|
}
|
|
|
|
return $rows[0];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function pickGatewayAbhaProfile(array $payload): array
|
|
{
|
|
$account = is_array($payload['account'] ?? null) ? $payload['account'] : [];
|
|
$dataAccount = is_array($payload['data']['account'] ?? null) ? $payload['data']['account'] : [];
|
|
$loginAccount = is_array($payload['accounts'][0] ?? null) ? $payload['accounts'][0] : [];
|
|
$mergedAccount = $loginAccount;
|
|
foreach ([$dataAccount, $account] as $accountSource) {
|
|
foreach ($accountSource as $key => $value) {
|
|
if (! is_scalar($value) || trim((string) $value) === '') {
|
|
continue;
|
|
}
|
|
if (in_array(strtoupper(trim((string) $value)), ['PATIENT', 'PATIENT NAME', 'N/A'], true)) {
|
|
continue;
|
|
}
|
|
$mergedAccount[$key] = $value;
|
|
}
|
|
}
|
|
|
|
$candidates = [
|
|
$payload['ABHAProfile'] ?? null,
|
|
$payload['data']['ABHAProfile'] ?? null,
|
|
$payload['gateway_abha_profile'] ?? null,
|
|
$payload['data']['gateway_abha_profile'] ?? null,
|
|
$payload['gateway_patient'] ?? null,
|
|
$payload['data']['gateway_patient'] ?? null,
|
|
$payload['profile'] ?? null,
|
|
$mergedAccount,
|
|
$account,
|
|
$loginAccount,
|
|
$payload['data']['account'] ?? null,
|
|
$payload,
|
|
];
|
|
|
|
foreach ($candidates as $candidate) {
|
|
if (is_array($candidate) && $this->looksLikeGatewayAbhaProfile($candidate)) {
|
|
return $candidate;
|
|
}
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $profile
|
|
* @param array<string, mixed> $payload
|
|
*/
|
|
private function extractAbhaProfileName(array $profile, array $payload): string
|
|
{
|
|
foreach ([$profile, $payload, $payload['account'] ?? [], $payload['data']['account'] ?? [], $payload['gateway_abha_profile'] ?? [], $payload['gateway_patient'] ?? [], $payload['data']['gateway_patient'] ?? []] as $source) {
|
|
if (! is_array($source)) {
|
|
continue;
|
|
}
|
|
|
|
foreach (['name', 'fullName', 'full_name', 'patient_name'] as $key) {
|
|
$name = trim((string) ($source[$key] ?? ''));
|
|
if ($name !== '') {
|
|
return $name;
|
|
}
|
|
}
|
|
|
|
$parts = [];
|
|
foreach (['firstName', 'first_name', 'middleName', 'middle_name', 'lastName', 'last_name'] as $key) {
|
|
$part = trim((string) ($source[$key] ?? ''));
|
|
if ($part !== '') {
|
|
$parts[] = $part;
|
|
}
|
|
}
|
|
|
|
if ($parts !== []) {
|
|
return implode(' ', $parts);
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $profile
|
|
* @param array<string, mixed> $payload
|
|
*/
|
|
private function extractAbhaProfilePhoto(array $profile, array $payload): string
|
|
{
|
|
$sources = [
|
|
$profile,
|
|
$payload,
|
|
$payload['ABHAProfile'] ?? [],
|
|
$payload['data']['ABHAProfile'] ?? [],
|
|
$payload['gateway_abha_profile'] ?? [],
|
|
$payload['data']['gateway_abha_profile'] ?? [],
|
|
$payload['gateway_patient'] ?? [],
|
|
$payload['data']['gateway_patient'] ?? [],
|
|
$payload['account'] ?? [],
|
|
$payload['data']['account'] ?? [],
|
|
];
|
|
|
|
foreach ($sources as $source) {
|
|
if (! is_array($source)) {
|
|
continue;
|
|
}
|
|
foreach (['profilePhoto', 'profile_photo', 'patient_photo', 'photo'] as $key) {
|
|
$photo = trim((string) ($source[$key] ?? ''));
|
|
if ($photo !== '') {
|
|
return $photo;
|
|
}
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $profile
|
|
* @param array<string, mixed> $payload
|
|
*/
|
|
private function extractAbhaProfileMobile(array $profile, array $payload): string
|
|
{
|
|
$sources = [
|
|
$profile,
|
|
$payload,
|
|
$payload['gateway_patient'] ?? [],
|
|
$payload['data']['gateway_patient'] ?? [],
|
|
$payload['patient_profile'] ?? [],
|
|
$payload['data']['patient_profile'] ?? [],
|
|
$payload['ABHAProfile'] ?? [],
|
|
$payload['data']['ABHAProfile'] ?? [],
|
|
$payload['profile'] ?? [],
|
|
$payload['data']['profile'] ?? [],
|
|
$payload['account'] ?? [],
|
|
$payload['data']['account'] ?? [],
|
|
$payload['accounts'][0] ?? [],
|
|
$payload['data']['accounts'][0] ?? [],
|
|
$payload['gateway_abha_profile'] ?? [],
|
|
$payload['data']['gateway_abha_profile'] ?? [],
|
|
];
|
|
|
|
foreach ($sources as $source) {
|
|
if (! is_array($source)) {
|
|
continue;
|
|
}
|
|
foreach (['mobile', 'mobileNumber', 'mobile_number', 'phone', 'contact_no', 'mobile_no'] as $key) {
|
|
$raw = trim((string) ($source[$key] ?? ''));
|
|
$digits = preg_replace('/\D/', '', $raw);
|
|
if (strlen($digits) === 12 && str_starts_with($digits, '91')) {
|
|
$digits = substr($digits, 2);
|
|
}
|
|
if (strlen($digits) === 10 && preg_match('/^[6-9]\d{9}$/', $digits)) {
|
|
return $digits;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try extracting from JWT token claims
|
|
foreach ([$payload['token'] ?? null, $payload['data']['token'] ?? null, $payload['tokens']['token'] ?? null, $payload['data']['tokens']['token'] ?? null] as $candToken) {
|
|
if (is_string($candToken) && trim($candToken) !== '') {
|
|
$parts = explode('.', trim($candToken));
|
|
if (count($parts) >= 2) {
|
|
$claims = json_decode(base64_decode(strtr($parts[1], '-_', '+/')), true);
|
|
if (is_array($claims)) {
|
|
foreach (['mobile', 'phone', 'mobileNumber', 'mobile_number'] as $key) {
|
|
$raw = trim((string) ($claims[$key] ?? ''));
|
|
$digits = preg_replace('/\D/', '', $raw);
|
|
if (strlen($digits) === 12 && str_starts_with($digits, '91')) {
|
|
$digits = substr($digits, 2);
|
|
}
|
|
if (strlen($digits) === 10) {
|
|
return $digits;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $profile
|
|
* @param array<string, mixed> $payload
|
|
*/
|
|
private function extractAbhaProfileGender(array $profile, array $payload): string
|
|
{
|
|
$sources = [
|
|
$profile,
|
|
$payload,
|
|
$payload['gateway_patient'] ?? [],
|
|
$payload['data']['gateway_patient'] ?? [],
|
|
$payload['patient_profile'] ?? [],
|
|
$payload['data']['patient_profile'] ?? [],
|
|
$payload['ABHAProfile'] ?? [],
|
|
$payload['data']['ABHAProfile'] ?? [],
|
|
$payload['profile'] ?? [],
|
|
$payload['data']['profile'] ?? [],
|
|
$payload['account'] ?? [],
|
|
$payload['data']['account'] ?? [],
|
|
$payload['accounts'][0] ?? [],
|
|
$payload['data']['accounts'][0] ?? [],
|
|
];
|
|
|
|
foreach ($sources as $source) {
|
|
if (! is_array($source)) {
|
|
continue;
|
|
}
|
|
foreach (['gender', 'gender_code', 'sex'] as $key) {
|
|
$val = strtoupper(trim((string) ($source[$key] ?? '')));
|
|
if ($val === '') {
|
|
continue;
|
|
}
|
|
if ($val === 'M' || $val === '1' || str_starts_with($val, 'MALE')) {
|
|
return 'Male';
|
|
}
|
|
if ($val === 'F' || $val === '2' || str_starts_with($val, 'FEMALE')) {
|
|
return 'Female';
|
|
}
|
|
if ($val === 'O' || $val === '3' || str_starts_with($val, 'OTHER')) {
|
|
return 'Other';
|
|
}
|
|
return ucfirst(strtolower($val));
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $profile
|
|
* @param array<string, mixed> $payload
|
|
*/
|
|
private function extractAbhaProfileDob(array $profile, array $payload): string
|
|
{
|
|
$sources = [
|
|
$profile,
|
|
$payload,
|
|
$payload['gateway_patient'] ?? [],
|
|
$payload['data']['gateway_patient'] ?? [],
|
|
$payload['patient_profile'] ?? [],
|
|
$payload['data']['patient_profile'] ?? [],
|
|
$payload['ABHAProfile'] ?? [],
|
|
$payload['data']['ABHAProfile'] ?? [],
|
|
$payload['profile'] ?? [],
|
|
$payload['data']['profile'] ?? [],
|
|
$payload['account'] ?? [],
|
|
$payload['data']['account'] ?? [],
|
|
$payload['accounts'][0] ?? [],
|
|
$payload['data']['accounts'][0] ?? [],
|
|
];
|
|
|
|
foreach ($sources as $source) {
|
|
if (! is_array($source)) {
|
|
continue;
|
|
}
|
|
foreach (['dob', 'date_of_birth', 'dateOfBirth', 'birthDate', 'birthdate'] as $key) {
|
|
$dob = trim((string) ($source[$key] ?? ''));
|
|
if ($dob !== '') {
|
|
if (preg_match('/^(\d{4})[-\/](\d{1,2})[-\/](\d{1,2})$/', $dob, $m)) {
|
|
return sprintf('%02d-%02d-%04d', (int) $m[3], (int) $m[2], (int) $m[1]);
|
|
}
|
|
if (preg_match('/^(\d{1,2})[-\/](\d{1,2})[-\/](\d{4})$/', $dob, $m)) {
|
|
return sprintf('%02d-%02d-%04d', (int) $m[1], (int) $m[2], (int) $m[3]);
|
|
}
|
|
return $dob;
|
|
}
|
|
}
|
|
|
|
$y = $source['yearOfBirth'] ?? $source['year_of_birth'] ?? null;
|
|
$m = $source['monthOfBirth'] ?? $source['month_of_birth'] ?? null;
|
|
$d = $source['dayOfBirth'] ?? $source['day_of_birth'] ?? null;
|
|
if ($y !== null && (int) $y > 1900) {
|
|
if ($m !== null && $d !== null && (int) $m > 0 && (int) $d > 0) {
|
|
return sprintf('%02d-%02d-%04d', (int) $d, (int) $m, (int) $y);
|
|
}
|
|
return (string) $y;
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $profile
|
|
* @param array<string, mixed> $payload
|
|
* @return array{address: string, district: string, state: string, zip: string}
|
|
*/
|
|
private function extractAbhaProfileAddress(array $profile, array $payload): array
|
|
{
|
|
$sources = [
|
|
$profile,
|
|
$payload,
|
|
$payload['gateway_patient'] ?? [],
|
|
$payload['data']['gateway_patient'] ?? [],
|
|
$payload['patient_profile'] ?? [],
|
|
$payload['data']['patient_profile'] ?? [],
|
|
$payload['ABHAProfile'] ?? [],
|
|
$payload['data']['ABHAProfile'] ?? [],
|
|
$payload['profile'] ?? [],
|
|
$payload['data']['profile'] ?? [],
|
|
$payload['account'] ?? [],
|
|
$payload['data']['account'] ?? [],
|
|
];
|
|
|
|
$addr = '';
|
|
$dist = '';
|
|
$state = '';
|
|
$zip = '';
|
|
|
|
foreach ($sources as $source) {
|
|
if (! is_array($source)) {
|
|
continue;
|
|
}
|
|
if ($addr === '') {
|
|
foreach (['address', 'address_line', 'addressLine', 'villageTownCity', 'subDistrictName', 'line'] as $k) {
|
|
$v = trim((string) ($source[$k] ?? ''));
|
|
if ($v !== '') {
|
|
$addr = $v;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if ($dist === '') {
|
|
foreach (['districtName', 'district', 'district_name'] as $k) {
|
|
$v = trim((string) ($source[$k] ?? ''));
|
|
if ($v !== '') {
|
|
$dist = $v;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if ($state === '') {
|
|
foreach (['stateName', 'state', 'state_name'] as $k) {
|
|
$v = trim((string) ($source[$k] ?? ''));
|
|
if ($v !== '') {
|
|
$state = $v;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if ($zip === '') {
|
|
foreach (['pincode', 'pinCode', 'pin_code', 'postalCode', 'zip'] as $k) {
|
|
$v = trim((string) ($source[$k] ?? ''));
|
|
if ($v !== '') {
|
|
$zip = $v;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return [
|
|
'address' => $addr,
|
|
'district' => $dist,
|
|
'state' => $state,
|
|
'zip' => $zip,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $profile
|
|
*/
|
|
private function looksLikeGatewayAbhaProfile(array $profile): bool
|
|
{
|
|
foreach (['ABHANumber', 'abha_id', 'preferredAddress', 'preferredAbhaAddress', 'abha_address', 'name', 'fullName', 'full_name', 'patient_name', 'firstName', 'first_name', 'lastName', 'last_name', 'gender', 'dob', 'date_of_birth', 'profilePhoto', 'profile_photo'] as $key) {
|
|
if (array_key_exists($key, $profile) && trim((string) $profile[$key]) !== '') {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Validate ABHA number or address
|
|
// POST abha/register/validate
|
|
// -------------------------------------------------------------------------
|
|
public function validateAbha()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$lookupType = strtolower(trim((string) ($this->request->getPost('lookup_type') ?? '')));
|
|
$postedNumber = trim((string) ($this->request->getPost('abha_id') ?? ''));
|
|
$postedAddress = trim((string) ($this->request->getPost('abha_address') ?? ''));
|
|
if (! in_array($lookupType, ['number', 'address'], true)) {
|
|
$lookupType = $postedAddress !== '' || str_contains($postedNumber, '@') ? 'address' : 'number';
|
|
}
|
|
$input = $lookupType === 'address' ? $postedAddress : $postedNumber;
|
|
if ($input === '') {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => $lookupType === 'address' ? 'ABHA Address is required' : 'ABHA Number or ID is required']);
|
|
}
|
|
|
|
$isAddress = $lookupType === 'address';
|
|
$payload = $isAddress
|
|
? ['abha_address' => $input]
|
|
: ['abha_id' => preg_replace('/\D/', '', $input)];
|
|
|
|
try {
|
|
$result = AbdmConnectorFactory::make()->validateAbha('', $payload);
|
|
} catch (\Throwable $e) {
|
|
return $this->response->setStatusCode(500)->setJSON(['ok' => 0, 'error_text' => $e->getMessage()]);
|
|
}
|
|
|
|
if (empty($result['ok']) || $result['ok'] != 1) {
|
|
$errorText = '';
|
|
foreach ([
|
|
$result['error_text'] ?? null,
|
|
$result['message'] ?? null,
|
|
$result['description'] ?? null,
|
|
$result['error'] ?? null,
|
|
$result['error']['message'] ?? null,
|
|
$result['error']['description'] ?? null,
|
|
$result['error']['error'] ?? null,
|
|
$result['error']['error_description'] ?? null,
|
|
$result['data']['message'] ?? null,
|
|
$result['data']['description'] ?? null,
|
|
$result['data']['error'] ?? null,
|
|
$result['data']['error']['message'] ?? null,
|
|
$result['data']['error']['description'] ?? null,
|
|
$result['data']['error']['error'] ?? null,
|
|
$result['data']['error']['error_description'] ?? null,
|
|
$result['details']['message'] ?? null,
|
|
$result['details']['error'] ?? null,
|
|
$result['data']['raw_response'] ?? null,
|
|
] as $candidate) {
|
|
if (is_string($candidate) && trim($candidate) !== '') {
|
|
$errorText = trim($candidate);
|
|
break;
|
|
}
|
|
}
|
|
|
|
$requestId = '';
|
|
foreach ([
|
|
$result['request_id'] ?? null,
|
|
$result['data']['request_id'] ?? null,
|
|
$result['result']['request_id'] ?? null,
|
|
] as $candidate) {
|
|
if (is_string($candidate) && trim($candidate) !== '') {
|
|
$requestId = trim($candidate);
|
|
break;
|
|
}
|
|
}
|
|
|
|
$entityName = $isAddress ? 'ABHA Address' : 'ABHA Number';
|
|
if (stripos($errorText, 'Unknown column') !== false ||
|
|
stripos($errorText, 'preferredAbhaAddress') !== false ||
|
|
stripos($errorText, 'not found') !== false ||
|
|
stripos($errorText, 'does not exist') !== false ||
|
|
stripos($errorText, 'ABDM gateway returned an error') !== false ||
|
|
stripos($errorText, 'Please make a valid request') !== false ||
|
|
stripos($errorText, 'Invalid Login Hint') !== false ||
|
|
stripos($errorText, 'rejected login hint') !== false ||
|
|
stripos($errorText, 'abdm_upstream_error') !== false) {
|
|
$errorText = $entityName . ' does not exist or was not found in the ABDM registry.';
|
|
} else {
|
|
if ($errorText === '') {
|
|
$errorText = 'ABHA validation failed';
|
|
}
|
|
if ($requestId !== '' && stripos($errorText, $requestId) === false) {
|
|
$errorText .= ' (Ref: ' . $requestId . ')';
|
|
}
|
|
}
|
|
|
|
return $this->response->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => $errorText,
|
|
]);
|
|
}
|
|
|
|
$statusRaw = '';
|
|
foreach ([
|
|
$result['status'] ?? null,
|
|
$result['validation_status'] ?? null,
|
|
$result['data']['status'] ?? null,
|
|
$result['data']['validation_status'] ?? null,
|
|
$result['result']['status'] ?? null,
|
|
$result['data']['result']['status'] ?? null,
|
|
] as $candidate) {
|
|
if (is_string($candidate) && trim($candidate) !== '') {
|
|
$statusRaw = strtoupper(trim($candidate));
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Bridge adapters may return ACTIVE/VERIFIED/EXISTS for successful ABHA checks.
|
|
$validStates = ['VALID', 'ACTIVE', 'VERIFIED', 'EXISTS', 'FOUND', 'SUCCESS'];
|
|
$status = in_array($statusRaw, $validStates, true) || $statusRaw === '' ? 'VALID' : $statusRaw;
|
|
|
|
$data = is_array($result['data'] ?? null) ? $result['data'] : [];
|
|
$accounts = is_array($data['accounts'] ?? null) ? $data['accounts'] : [];
|
|
$account = isset($accounts[0]) && is_array($accounts[0])
|
|
? $accounts[0]
|
|
: (is_array($data['account'] ?? null) ? $data['account'] : []);
|
|
$txnId = trim((string) (
|
|
$result['txn_id']
|
|
?? $result['txnId']
|
|
?? $data['txn_id']
|
|
?? $data['txnId']
|
|
?? $data['transaction_id']
|
|
?? $data['transactionId']
|
|
?? $account['txn_id']
|
|
?? $account['txnId']
|
|
?? ''
|
|
));
|
|
$authMethods = is_array($result['auth_methods'] ?? null)
|
|
? $result['auth_methods']
|
|
: (is_array($result['authMethods'] ?? null)
|
|
? $result['authMethods']
|
|
: (is_array($data['authMethods'] ?? null) ? $data['authMethods'] : ($data['auth_methods'] ?? [])));
|
|
$blockedMethods = $data['blockedAuthMethods'] ?? $data['blocked_auth_methods'] ?? $account['blockedAuthMethods'] ?? $account['blocked_auth_methods'] ?? [];
|
|
$maskedMobile = $this->maskAbhaMobile((string) (
|
|
$data['mobile']
|
|
?? $data['mobileNumber']
|
|
?? $data['maskedMobile']
|
|
?? $data['masked_mobile']
|
|
?? $data['phone']
|
|
?? $account['masked_mobile']
|
|
?? $account['mobile']
|
|
?? $account['mobileNumber']
|
|
?? $account['maskedMobile']
|
|
?? $account['phone']
|
|
?? ''
|
|
));
|
|
$aadhaarMaskedMobile = $this->maskAbhaMobile((string) (
|
|
$data['aadhaar_masked_mobile']
|
|
?? $data['aadhaarMaskedMobile']
|
|
?? $account['aadhaar_masked_mobile']
|
|
?? $account['aadhaarMaskedMobile']
|
|
?? ''
|
|
));
|
|
return $this->response->setJSON([
|
|
'ok' => 1,
|
|
'status' => $status,
|
|
'raw_status' => $statusRaw,
|
|
'txn_id' => $txnId,
|
|
'auth_methods' => array_values(array_unique(array_map(static fn($method): string => strtoupper(trim((string) $method)), $authMethods))),
|
|
'blocked_auth_methods' => is_array($blockedMethods) ? array_values($blockedMethods) : [],
|
|
'masked_mobile' => $maskedMobile,
|
|
'aadhaar_masked_mobile' => $aadhaarMaskedMobile,
|
|
'account' => (function () use ($account, $data, $result, $maskedMobile, $aadhaarMaskedMobile): array {
|
|
$resolvedAbhaNumber = '';
|
|
foreach ([
|
|
$data['healthIdNumber'] ?? null,
|
|
$data['ABHANumber'] ?? null,
|
|
$data['abhaNumber'] ?? null,
|
|
$data['abha_number'] ?? null,
|
|
$data['abha_id'] ?? null,
|
|
$account['healthIdNumber'] ?? null,
|
|
$account['ABHANumber'] ?? null,
|
|
$account['abhaNumber'] ?? null,
|
|
$account['abha_number'] ?? null,
|
|
$account['abha_id'] ?? null,
|
|
$result['healthIdNumber'] ?? null,
|
|
$result['ABHANumber'] ?? null,
|
|
$result['abhaNumber'] ?? null,
|
|
$result['abha_number'] ?? null,
|
|
$result['abha_id'] ?? null,
|
|
] as $candidateNum) {
|
|
if (is_string($candidateNum) && trim($candidateNum) !== '') {
|
|
$resolvedAbhaNumber = trim($candidateNum);
|
|
break;
|
|
}
|
|
}
|
|
|
|
$resolvedName = '';
|
|
foreach ([
|
|
$data['fullName'] ?? null,
|
|
$data['full_name'] ?? null,
|
|
$data['name'] ?? null,
|
|
$account['fullName'] ?? null,
|
|
$account['full_name'] ?? null,
|
|
$account['name'] ?? null,
|
|
$result['fullName'] ?? null,
|
|
$result['full_name'] ?? null,
|
|
$result['name'] ?? null,
|
|
] as $candidateName) {
|
|
if (is_string($candidateName) && trim($candidateName) !== '' && strcasecmp(trim($candidateName), 'ABHA User') !== 0) {
|
|
$resolvedName = trim($candidateName);
|
|
break;
|
|
}
|
|
}
|
|
if ($resolvedName === '') {
|
|
$resolvedName = trim((string) ($account['name'] ?? $data['name'] ?? $result['name'] ?? 'ABHA User'));
|
|
}
|
|
|
|
$resolvedAddress = '';
|
|
foreach ([
|
|
$data['abhaAddress'] ?? null,
|
|
$data['preferredAddress'] ?? null,
|
|
$data['preferredAbhaAddress'] ?? null,
|
|
$data['abha_address'] ?? null,
|
|
$account['abhaAddress'] ?? null,
|
|
$account['preferredAddress'] ?? null,
|
|
$account['preferredAbhaAddress'] ?? null,
|
|
$account['abha_address'] ?? null,
|
|
$result['abhaAddress'] ?? null,
|
|
$result['preferredAddress'] ?? null,
|
|
$result['preferredAbhaAddress'] ?? null,
|
|
$result['abha_address'] ?? null,
|
|
] as $candidateAddr) {
|
|
if (is_string($candidateAddr) && trim($candidateAddr) !== '') {
|
|
$resolvedAddress = trim($candidateAddr);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return [
|
|
'name' => $resolvedName,
|
|
'abha_number' => $resolvedAbhaNumber,
|
|
'abha_address' => $resolvedAddress,
|
|
'masked_mobile' => $maskedMobile,
|
|
'aadhaar_masked_mobile' => $aadhaarMaskedMobile,
|
|
];
|
|
})(),
|
|
]);
|
|
}
|
|
|
|
private function maskAbhaMobile(string $value): string
|
|
{
|
|
$value = trim($value);
|
|
if ($value === '' || preg_match('/\d/', $value) !== 1) {
|
|
return '';
|
|
}
|
|
|
|
if (preg_match('/\*/', $value) === 1) {
|
|
return $value;
|
|
}
|
|
|
|
$digits = preg_replace('/\D/', '', $value);
|
|
if (strlen($digits) === 10) {
|
|
return substr($digits, 0, 2) . '*****' . substr($digits, -3);
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
public function loginRequestOtp()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$txnId = trim((string) ($this->request->getPost('txn_id') ?? ''));
|
|
$authMethod = strtoupper(trim((string) ($this->request->getPost('auth_method') ?? '')));
|
|
if ($txnId === '' || ! in_array($authMethod, ['MOBILE_OTP', 'AADHAAR_OTP'], true)) {
|
|
return $this->response->setStatusCode(422)->setJSON(['ok' => 0, 'error_text' => 'Valid transaction and OTP method are required.']);
|
|
}
|
|
|
|
try {
|
|
$result = AbdmConnectorFactory::make()->abhaLoginRequestOtp([
|
|
'txn_id' => $txnId,
|
|
'auth_method' => $authMethod,
|
|
'abha_id' => trim((string) ($this->request->getPost('abha_id') ?? '')),
|
|
'abha_address' => trim((string) ($this->request->getPost('abha_address') ?? '')),
|
|
'scope' => trim((string) ($this->request->getPost('scope') ?? 'abha-login')),
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
return $this->response->setStatusCode(502)->setJSON(['ok' => 0, 'error_text' => $e->getMessage()]);
|
|
}
|
|
|
|
if (empty($result['ok']) || (int) $result['ok'] !== 1) {
|
|
$httpCode = (int) ($result['http_code'] ?? 0);
|
|
if ($httpCode < 400 || $httpCode > 599) {
|
|
$httpCode = 502;
|
|
}
|
|
return $this->response->setStatusCode($httpCode)->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => $this->extractBridgeErrorText($result, 'The ABDM Bridge could not send the OTP.'),
|
|
'request_id' => (string) ($result['request_id'] ?? ''),
|
|
]);
|
|
}
|
|
|
|
$data = is_array($result['data'] ?? null) ? $result['data'] : [];
|
|
$maskedMobile = trim((string) (
|
|
$result['masked_mobile']
|
|
?? $result['maskedMobile']
|
|
?? $data['masked_mobile']
|
|
?? $data['maskedMobile']
|
|
?? $data['mobile']
|
|
?? $data['mobile_number']
|
|
?? ''
|
|
));
|
|
$deliveryMessage = trim((string) (
|
|
$result['message']
|
|
?? $data['message']
|
|
?? $result['details']['message']
|
|
?? $data['details']['message']
|
|
?? ''
|
|
));
|
|
if ($maskedMobile === '' && preg_match('/(\d{4})(?!.*\d)/', $deliveryMessage, $mobileEnding) === 1) {
|
|
$maskedMobile = '******' . $mobileEnding[1];
|
|
}
|
|
return $this->response->setJSON([
|
|
'ok' => 1,
|
|
'txn_id' => (string) ($result['txn_id'] ?? $result['txnId'] ?? $data['txn_id'] ?? $data['txnId'] ?? $txnId),
|
|
'auth_method' => $authMethod,
|
|
'masked_mobile' => $this->maskAbhaMobile($maskedMobile),
|
|
'resend_after' => max(60, (int) ($data['resend_after'] ?? $data['resendAfter'] ?? 60)),
|
|
]);
|
|
}
|
|
|
|
public function loginVerifyOtp()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$txnId = trim((string) ($this->request->getPost('txn_id') ?? ''));
|
|
$authMethod = strtoupper(trim((string) ($this->request->getPost('auth_method') ?? '')));
|
|
$otp = preg_replace('/\D/', '', trim((string) ($this->request->getPost('otp') ?? '')));
|
|
if ($txnId === '' || ! in_array($authMethod, ['MOBILE_OTP', 'AADHAAR_OTP'], true) || strlen($otp) !== 6) {
|
|
return $this->response->setStatusCode(422)->setJSON(['ok' => 0, 'error_text' => 'Transaction, OTP method, and 6-digit OTP are required.']);
|
|
}
|
|
|
|
try {
|
|
$result = AbdmConnectorFactory::make()->abhaLoginVerifyOtp([
|
|
'txn_id' => $txnId,
|
|
'auth_method' => $authMethod,
|
|
'otp' => $otp,
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
return $this->response->setStatusCode(502)->setJSON(['ok' => 0, 'error_text' => $e->getMessage()]);
|
|
}
|
|
|
|
$authResult = strtolower(trim((string) ($result['authResult'] ?? $result['data']['authResult'] ?? $result['auth_result'] ?? $result['data']['auth_result'] ?? '')));
|
|
$status = strtolower(trim((string) ($result['status'] ?? $result['data']['status'] ?? '')));
|
|
if ($authResult === 'failed' || in_array($status, ['failed', 'failure', 'error'], true)) {
|
|
$result['ok'] = 0;
|
|
}
|
|
|
|
if (empty($result['ok']) || (int) $result['ok'] !== 1) {
|
|
$httpCode = (int) ($result['http_code'] ?? 0);
|
|
if ($httpCode < 400 || $httpCode > 599) {
|
|
$httpCode = 422;
|
|
}
|
|
return $this->response->setStatusCode($httpCode)->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => $this->extractBridgeErrorText($result, 'Please enter a valid OTP. Entered OTP is either expired or incorrect.'),
|
|
'request_id' => (string) ($result['request_id'] ?? ''),
|
|
]);
|
|
}
|
|
|
|
$payload = $result;
|
|
if (is_array($result['data'] ?? null)) {
|
|
foreach ($result['data'] as $key => $value) {
|
|
if (! array_key_exists($key, $payload)) {
|
|
$payload[$key] = $value;
|
|
}
|
|
}
|
|
}
|
|
$profile = $this->pickGatewayAbhaProfile($payload);
|
|
$firstNonEmpty = static function (array $values): string {
|
|
foreach ($values as $value) {
|
|
if (is_scalar($value) && trim((string) $value) !== '') {
|
|
return trim((string) $value);
|
|
}
|
|
}
|
|
return '';
|
|
};
|
|
$abhaNumber = preg_replace('/\D/', '', $firstNonEmpty([$profile['ABHANumber'] ?? null, $profile['abhaNumber'] ?? null, $profile['abha_number'] ?? null, $profile['abha_id'] ?? null, $payload['ABHANumber'] ?? null, $payload['abhaNumber'] ?? null, $payload['abha_id'] ?? null]));
|
|
$abhaAddress = $firstNonEmpty([$profile['preferredAddress'] ?? null, $profile['preferredAbhaAddress'] ?? null, $profile['abhaAddress'] ?? null, $profile['abha_address'] ?? null, $payload['preferredAddress'] ?? null, $payload['abhaAddress'] ?? null, $payload['abha_address'] ?? null]);
|
|
$userToken = trim((string) ($payload['token'] ?? $payload['tokens']['token'] ?? $payload['auth_token'] ?? $payload['user_token'] ?? ''));
|
|
|
|
if ($abhaNumber === '' && $abhaAddress === '' && $userToken === '') {
|
|
return $this->response->setStatusCode(422)->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => $this->extractBridgeErrorText($result, 'Please enter a valid OTP. Entered OTP is either expired or incorrect.'),
|
|
'request_id' => (string) ($result['request_id'] ?? ''),
|
|
]);
|
|
}
|
|
$name = $this->extractAbhaProfileName($profile, $payload);
|
|
$mobile = $firstNonEmpty([$profile['mobile'] ?? null, $profile['mobileNumber'] ?? null, $profile['mobile_number'] ?? null, $payload['mobile'] ?? null, $payload['mobileNumber'] ?? null, $payload['mobile_number'] ?? null]);
|
|
$gender = $firstNonEmpty([$profile['gender'] ?? null, $payload['gender'] ?? null]);
|
|
$dob = $firstNonEmpty([$profile['dob'] ?? null, $profile['dateOfBirth'] ?? null, $profile['date_of_birth'] ?? null, $payload['dob'] ?? null, $payload['dateOfBirth'] ?? null, $payload['date_of_birth'] ?? null]);
|
|
if ($dob === '') {
|
|
$day = $firstNonEmpty([$profile['dayOfBirth'] ?? null, $profile['day_of_birth'] ?? null]);
|
|
$month = $firstNonEmpty([$profile['monthOfBirth'] ?? null, $profile['month_of_birth'] ?? null]);
|
|
$year = $firstNonEmpty([$profile['yearOfBirth'] ?? null, $profile['year_of_birth'] ?? null]);
|
|
if ($day !== '' && $month !== '' && $year !== '') {
|
|
$dob = sprintf('%04d-%02d-%02d', (int) $year, (int) $month, (int) $day);
|
|
}
|
|
}
|
|
$address = $firstNonEmpty([$profile['address'] ?? null, $profile['addressLine'] ?? null, $profile['address_line'] ?? null, $payload['address'] ?? null, $payload['addressLine'] ?? null, $payload['address_line'] ?? null]);
|
|
$district = $firstNonEmpty([$profile['districtName'] ?? null, $profile['district_name'] ?? null, $profile['district'] ?? null, $payload['districtName'] ?? null, $payload['district_name'] ?? null, $payload['district'] ?? null]);
|
|
$state = $firstNonEmpty([$profile['stateName'] ?? null, $profile['state_name'] ?? null, $profile['state'] ?? null, $payload['stateName'] ?? null, $payload['state_name'] ?? null, $payload['state'] ?? null]);
|
|
$zip = $firstNonEmpty([$profile['pinCode'] ?? null, $profile['pincode'] ?? null, $profile['pin'] ?? null, $payload['pinCode'] ?? null, $payload['pincode'] ?? null, $payload['pin'] ?? null]);
|
|
$photo = $firstNonEmpty([$profile['profilePhoto'] ?? null, $profile['profile_photo'] ?? null, $profile['photo'] ?? null, $payload['patient_photo'] ?? null, $payload['profilePhoto'] ?? null, $payload['profile_photo'] ?? null, $payload['photo'] ?? null, $payload['data']['profilePhoto'] ?? null, $payload['data']['patient_photo'] ?? null]);
|
|
$cardData = $this->extractAbhaCardData($payload);
|
|
|
|
$db = \Config\Database::connect();
|
|
$fields = $db->getFieldNames('patient_master') ?? [];
|
|
$abhaField = $this->resolveAbhaFieldName($fields);
|
|
$candidates = $this->findMatchingCandidates($db, $fields, $name, $mobile, $gender, $dob, '', $abhaField, $abhaNumber);
|
|
|
|
$hasExactAbhaMatch = false;
|
|
$conflictPatient = null;
|
|
foreach ($candidates as $c) {
|
|
if (! empty($c['match']['abha'])) {
|
|
$hasExactAbhaMatch = true;
|
|
$conflictPatient = [
|
|
'id' => (int) $c['id'],
|
|
'p_code' => (string) ($c['p_code'] ?? ''),
|
|
'p_fname' => (string) ($c['name'] ?? ''),
|
|
'name' => (string) ($c['name'] ?? ''),
|
|
];
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Direct fallback safety check via findPatientByAbha
|
|
if (! $hasExactAbhaMatch && ($abhaNumber !== '' || $abhaAddress !== '')) {
|
|
$patientController = new Patient();
|
|
$direct = $patientController->findPatientByAbha($abhaNumber !== '' ? $abhaNumber : $abhaAddress);
|
|
if ($direct !== null) {
|
|
$hasExactAbhaMatch = true;
|
|
$conflictPatient = [
|
|
'id' => (int) $direct['id'],
|
|
'p_code' => (string) ($direct['p_code'] ?? ''),
|
|
'p_fname' => (string) ($direct['p_fname'] ?? ''),
|
|
'name' => (string) ($direct['p_fname'] ?? ''),
|
|
];
|
|
array_unshift($candidates, [
|
|
'id' => (int) $direct['id'],
|
|
'p_code' => (string) ($direct['p_code'] ?? ''),
|
|
'name' => (string) ($direct['p_fname'] ?? ''),
|
|
'gender' => 0,
|
|
'gender_label' => '',
|
|
'dob' => '',
|
|
'age' => null,
|
|
'mobile' => (string) ($direct['mphone1'] ?? ''),
|
|
'photo_url' => '',
|
|
'address' => '',
|
|
'aadhaar' => (string) ($direct['udai'] ?? ''),
|
|
'abha' => (string) ($direct['abha_id'] ?? ''),
|
|
'abha_conflict' => false,
|
|
'match' => [
|
|
'name' => false,
|
|
'age' => false,
|
|
'gender' => false,
|
|
'mobile' => false,
|
|
'aadhaar' => false,
|
|
'abha' => true,
|
|
],
|
|
'score' => 1000,
|
|
]);
|
|
}
|
|
}
|
|
|
|
return $this->response->setJSON([
|
|
'ok' => 1,
|
|
'need_confirmation' => true,
|
|
'already_registered' => $hasExactAbhaMatch,
|
|
'conflict_patient' => $conflictPatient,
|
|
'abha_number' => $abhaNumber,
|
|
'abha_address' => $abhaAddress,
|
|
'name' => $name,
|
|
'mobile' => $mobile,
|
|
'gender' => $gender,
|
|
'dob' => $dob,
|
|
'address' => $address,
|
|
'district' => $district,
|
|
'state' => $state,
|
|
'zip' => $zip,
|
|
'photo' => $photo,
|
|
'verified_status' => 'VERIFIED',
|
|
'verification_type' => $authMethod,
|
|
'mobile_verified' => $authMethod === 'MOBILE_OTP' ? 1 : null,
|
|
'card_base64' => $cardData,
|
|
'card_content_type' => $this->resolveAbhaCardContentType($payload),
|
|
'card_source' => $this->resolveAbhaCardSource($payload),
|
|
'card_message' => $this->resolveAbhaCardMessage($payload),
|
|
'candidates' => $candidates,
|
|
]);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// ABHA QR scan — POST abha/register/qr_scan
|
|
// Accepts the JSON payload printed on an ABHA card / PHR app QR code and
|
|
// returns the demographics plus possible existing HMS patients. The QR is a
|
|
// demographic capture, not an OTP authentication, so it is never marked VERIFIED.
|
|
// -------------------------------------------------------------------------
|
|
public function qrScan()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$raw = trim((string) ($this->request->getPost('qr_data') ?? ''));
|
|
if ($raw === '') {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Scan or paste the ABHA QR content first.']);
|
|
}
|
|
|
|
$decoded = json_decode($raw, true);
|
|
if (! is_array($decoded)) {
|
|
// Some cards/apps encode only the ABHA number or address.
|
|
$digits = preg_replace('/\D/', '', $raw);
|
|
if (strlen($digits) === 14) {
|
|
$decoded = ['hidn' => $digits];
|
|
} elseif (str_contains($raw, '@')) {
|
|
$decoded = ['hid' => $raw];
|
|
} else {
|
|
return $this->response->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => 'This QR code is not a recognised ABHA card. Scan the QR printed on the ABHA card or shown in the ABHA/PHR app.',
|
|
]);
|
|
}
|
|
}
|
|
|
|
// ABHA QR keys vary by issuer ("state name", "stateName", "state_name").
|
|
$normalized = [];
|
|
foreach ($decoded as $key => $value) {
|
|
$flatKey = strtolower(preg_replace('/[^a-z0-9]/i', '', (string) $key));
|
|
if ($flatKey !== '' && is_scalar($value)) {
|
|
$normalized[$flatKey] = trim((string) $value);
|
|
}
|
|
}
|
|
|
|
$pick = static function (array $keys) use ($normalized): string {
|
|
foreach ($keys as $key) {
|
|
if (trim((string) ($normalized[$key] ?? '')) !== '') {
|
|
return trim((string) $normalized[$key]);
|
|
}
|
|
}
|
|
return '';
|
|
};
|
|
|
|
$abhaNumber = preg_replace('/\D/', '', $pick(['hidn', 'abhanumber', 'healthidnumber', 'abhano', 'abhaid']));
|
|
$abhaAddress = $pick(['hid', 'phr', 'phraddress', 'abhaaddress', 'healthid', 'preferredabhaaddress']);
|
|
$name = $pick(['name', 'fullname', 'patientname']);
|
|
$gender = $pick(['gender', 'sex']);
|
|
$dob = $pick(['dob', 'dateofbirth', 'birthdate']);
|
|
$mobile = preg_replace('/\D/', '', $pick(['mobile', 'mobilenumber', 'phone', 'contact']));
|
|
$address = $pick(['address', 'addressline']);
|
|
$district = $pick(['districtname', 'district', 'distname']);
|
|
$state = $pick(['statename', 'state']);
|
|
$zip = $pick(['pincode', 'pin', 'postalcode', 'zip']);
|
|
|
|
if ($abhaNumber === '' && $abhaAddress === '') {
|
|
return $this->response->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => 'The QR code did not contain an ABHA number or ABHA address.',
|
|
]);
|
|
}
|
|
|
|
if ($abhaNumber === '' && str_contains($abhaAddress, '@')) {
|
|
$candidateDigits = preg_replace('/\D/', '', explode('@', $abhaAddress)[0]);
|
|
if (strlen($candidateDigits) === 14) {
|
|
$abhaNumber = $candidateDigits;
|
|
}
|
|
}
|
|
|
|
$db = \Config\Database::connect();
|
|
$fields = $db->getFieldNames('patient_master') ?? [];
|
|
$abhaField = $this->resolveAbhaFieldName($fields);
|
|
|
|
return $this->response->setJSON([
|
|
'ok' => 1,
|
|
'need_confirmation' => true,
|
|
'abha_number' => $abhaNumber,
|
|
'abha_address' => $abhaAddress,
|
|
'name' => $name,
|
|
'mobile' => $mobile,
|
|
'gender' => $gender,
|
|
'dob' => $dob,
|
|
'address' => $address,
|
|
'district' => $district,
|
|
'state' => $state,
|
|
'zip' => $zip,
|
|
'verification_type' => 'QR_SCAN',
|
|
'candidates' => $this->findMatchingCandidates($db, $fields, $name, $mobile, $gender, $dob, '', $abhaField, $abhaNumber),
|
|
]);
|
|
}
|
|
|
|
private function extractAbhaCardData(array $payload): string
|
|
{
|
|
$data = is_array($payload['data'] ?? null) ? $payload['data'] : [];
|
|
$sources = [
|
|
$payload,
|
|
$data,
|
|
$payload['account'] ?? [],
|
|
$data['account'] ?? [],
|
|
$payload['profile'] ?? [],
|
|
$data['profile'] ?? [],
|
|
$payload['ABHAProfile'] ?? [],
|
|
$data['ABHAProfile'] ?? [],
|
|
$payload['gateway_patient'] ?? [],
|
|
$data['gateway_patient'] ?? [],
|
|
];
|
|
foreach ($sources as $source) {
|
|
if (! is_array($source)) { continue; }
|
|
// card_base64 first: the connector puts the resolved official card there,
|
|
// while official_card can still hold a Bridge-generated image.
|
|
foreach (['card_base64', 'official_card', 'card_data_uri', 'card_data', 'abhaCard', 'abha_card', 'cardData', 'card'] as $key) {
|
|
$value = $source[$key] ?? '';
|
|
if (is_string($value) && trim($value) !== '') { return trim($value); }
|
|
if (is_array($value)) {
|
|
foreach (['base64', 'data', 'card_base64', 'card_data'] as $nestedKey) {
|
|
if (is_string($value[$nestedKey] ?? null) && trim($value[$nestedKey]) !== '') {
|
|
return trim($value[$nestedKey]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
private function resolveAbhaCardMessage(array $payload): string
|
|
{
|
|
$data = is_array($payload['data'] ?? null) ? $payload['data'] : [];
|
|
foreach ([
|
|
$payload['card_message'] ?? null,
|
|
$payload['message'] ?? null,
|
|
$data['message'] ?? null,
|
|
] as $candidate) {
|
|
$value = trim((string) $candidate);
|
|
if ($value !== '') {
|
|
return $value;
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
private function resolveAbhaCardSource(array $payload): string
|
|
{
|
|
$data = is_array($payload['data'] ?? null) ? $payload['data'] : [];
|
|
foreach ([
|
|
$payload['card_source'] ?? null,
|
|
$payload['source'] ?? null,
|
|
$data['card_source'] ?? null,
|
|
$data['source'] ?? null,
|
|
] as $candidate) {
|
|
$value = strtolower(trim((string) $candidate));
|
|
if ($value === '') {
|
|
continue;
|
|
}
|
|
if (str_contains($value, 'abdm')) {
|
|
return 'abdm';
|
|
}
|
|
|
|
// "none" means ABDM issued no card for this session, not a local render.
|
|
return $value === 'none' ? 'none' : 'generated';
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
private function resolveAbhaCardContentType(array $payload): string
|
|
{
|
|
$data = is_array($payload['data'] ?? null) ? $payload['data'] : [];
|
|
$format = strtolower(trim((string) ($payload['card_content_type'] ?? $payload['cardContentType'] ?? $payload['card_format'] ?? ($payload['account']['card_format'] ?? '') ?: ($data['card_format'] ?? ''))));
|
|
if ($format === 'png') { return 'image/png'; }
|
|
if ($format === 'jpg' || $format === 'jpeg') { return 'image/jpeg'; }
|
|
if ($format === 'pdf') { return 'application/pdf'; }
|
|
return $format !== '' && str_contains($format, '/') ? $format : 'image/png';
|
|
}
|
|
}
|