- FhirR4Builder: rewrite buildLabReportBundle() with DiagnosticReportLab profile, structured Observation valueQuantity/valueString, reference ranges, interpretation codes, Binary+DocumentReference for HTML report, Encounter resource, HFR-based bundle identifier system - FhirR4Builder: rewrite buildDischargeSummaryBundle() with optional Practitioner/Organization params, structured ABDM sections (chief complaints 422843007, medical history 1003642006, investigations 721981007, procedures 1003640003, medications 721912009, follow-up 736271009), HFR-based bundle identifier system - AbdmGateway: shareIpdDischargeBundle() now builds a real DischargeSummaryRecord FHIR bundle and posts via pushRecord() (POST /v3/records/push) instead of the stale shareIpdDischargeBundle() - AbdmGateway: shareDiagnosisReportBundle() now builds a real DiagnosticReportRecord FHIR bundle and posts via pushRecord() instead of the stale shareDiagnosisReportBundle() - AbdmGateway: add getHospitalProfileForFhir() and mapLabTypeToTitle() helpers; fix pre-existing undefined \ bug in getActiveConsentRecord() - Add ABDM_FHIR reference package, AbdmBridgeLog controller/view, bridge record migration, debug scripts and misc UI/settings updates
1870 lines
79 KiB
PHP
1870 lines
79 KiB
PHP
<?php
|
|
|
|
namespace App\Controllers;
|
|
|
|
use App\Libraries\Abdm\AbdmConnectorInterface;
|
|
use App\Libraries\Abdm\AbdmConnectorFactory;
|
|
use App\Libraries\FhirR4Builder;
|
|
use App\Libraries\FhirEncryptionService;
|
|
use App\Libraries\AbdmAuditService;
|
|
use CodeIgniter\I18n\Time;
|
|
|
|
class AbdmGateway extends BaseController
|
|
{
|
|
private AbdmConnectorInterface $connector;
|
|
|
|
public function initController(
|
|
\CodeIgniter\HTTP\RequestInterface $request,
|
|
\CodeIgniter\HTTP\ResponseInterface $response,
|
|
\Psr\Log\LoggerInterface $logger
|
|
) {
|
|
parent::initController($request, $response, $logger);
|
|
$this->connector = AbdmConnectorFactory::make();
|
|
}
|
|
|
|
public function abhaValidate()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$abhaId = trim((string) $this->request->getPost('abha_id'));
|
|
// Accept both dash-format (14-1234-5678-9012) and raw 14-digit format
|
|
$abhaDigits = str_replace('-', '', $abhaId);
|
|
if ($abhaId === '' || strlen($abhaDigits) !== 14 || ! ctype_digit($abhaDigits)) {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'ABHA ID must be 14 digits (e.g. 14-1234-5678-9012).']);
|
|
}
|
|
|
|
$payload = [
|
|
'abha_id' => $abhaId,
|
|
'requested_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
|
|
];
|
|
|
|
$queueId = null;
|
|
try {
|
|
$result = $this->connector->validateAbha($abhaId, $payload);
|
|
$queueId = $result['queue_id'] ?? null;
|
|
} catch (\Throwable $e) {
|
|
}
|
|
|
|
return $this->response->setJSON([
|
|
'ok' => 1,
|
|
'status' => 'queued',
|
|
'queue_id' => $queueId,
|
|
'message' => 'ABHA validation request queued to center server.',
|
|
]);
|
|
}
|
|
|
|
public function bridgeTestEvent()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$abhaId = trim((string) $this->request->getPost('abha_id'));
|
|
if ($abhaId === '') {
|
|
$abhaId = '14-1234-5678-9012';
|
|
}
|
|
|
|
// Accept both dash-format (14-1234-5678-9012) and raw 14-digit format
|
|
$abhaDigits = str_replace('-', '', $abhaId);
|
|
if (strlen($abhaDigits) !== 14 || ! ctype_digit($abhaDigits)) {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'ABHA ID must be 14 digits (e.g. 14-1234-5678-9012).']);
|
|
}
|
|
|
|
$payload = [
|
|
'abha_id' => $abhaId,
|
|
'requested_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
|
|
'source' => 'billing.patient.ui',
|
|
'test_ping' => 1,
|
|
];
|
|
|
|
try {
|
|
$result = $this->connector->validateAbha($abhaId, $payload);
|
|
$cfg = config('AbdmConnector');
|
|
|
|
return $this->response->setJSON([
|
|
'ok' => 1,
|
|
'message' => 'Bridge test queued successfully.',
|
|
'connector' => $this->connector->getConnectorName(),
|
|
'bridge_url' => (string) ($cfg->dreamsoftBridgeUrl ?? ''),
|
|
'queue_id' => $result['queue_id'] ?? null,
|
|
'result' => $result,
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
return $this->response->setStatusCode(500)->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
public function consentRequest()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$patientId = (int) $this->request->getPost('patient_id');
|
|
$abhaId = trim((string) $this->request->getPost('abha_id'));
|
|
$purposeCode = trim((string) $this->request->getPost('purpose_code'));
|
|
$expiresAt = trim((string) $this->request->getPost('expires_at'));
|
|
|
|
if ($patientId <= 0 || $abhaId === '') {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'patient_id and abha_id are required']);
|
|
}
|
|
|
|
if (! $this->db->tableExists('abdm_consent_records')) {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'abdm_consent_records table not found']);
|
|
}
|
|
|
|
$consentHandle = 'CH-' . date('YmdHis') . '-' . strtoupper(bin2hex(random_bytes(4)));
|
|
$requestedAt = Time::now('Asia/Kolkata')->toDateTimeString();
|
|
|
|
$rawPayload = [
|
|
'patient_id' => $patientId,
|
|
'abha_id' => $abhaId,
|
|
'purpose_code' => $purposeCode,
|
|
'expires_at' => $expiresAt,
|
|
'consent_handle' => $consentHandle,
|
|
'requested_at' => $requestedAt,
|
|
];
|
|
|
|
$this->db->table('abdm_consent_records')->insert([
|
|
'patient_id' => $patientId,
|
|
'abha_id' => $abhaId,
|
|
'consent_handle' => $consentHandle,
|
|
'consent_status' => 'requested',
|
|
'purpose_code' => $purposeCode,
|
|
'requested_at' => $requestedAt,
|
|
'expires_at' => $expiresAt !== '' ? $expiresAt : null,
|
|
'raw_payload_json' => (string) json_encode($rawPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
|
'created_at' => $requestedAt,
|
|
'updated_at' => $requestedAt,
|
|
]);
|
|
|
|
$queueId = null;
|
|
try {
|
|
$result = $this->connector->requestConsent($patientId, $abhaId, $purposeCode, $expiresAt, $consentHandle, $rawPayload);
|
|
$queueId = $result['queue_id'] ?? null;
|
|
} catch (\Throwable $e) {
|
|
}
|
|
|
|
return $this->response->setJSON([
|
|
'ok' => 1,
|
|
'consent_handle' => $consentHandle,
|
|
'queue_id' => $queueId,
|
|
'status' => 'requested',
|
|
]);
|
|
}
|
|
|
|
public function consentCallback()
|
|
{
|
|
$signatureFailure = $this->validateWebhookSignature();
|
|
if ($signatureFailure !== null) {
|
|
return $signatureFailure;
|
|
}
|
|
|
|
$payload = $this->request->getJSON(true);
|
|
if (! is_array($payload) || empty($payload['consent_handle'])) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'consent_handle is required']);
|
|
}
|
|
|
|
if (! $this->db->tableExists('abdm_consent_records')) {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'abdm_consent_records table not found']);
|
|
}
|
|
|
|
$handle = trim((string) $payload['consent_handle']);
|
|
$status = trim((string) ($payload['consent_status'] ?? 'approved'));
|
|
$expiresAt = trim((string) ($payload['expires_at'] ?? ''));
|
|
$now = Time::now('Asia/Kolkata')->toDateTimeString();
|
|
|
|
$existing = $this->db->table('abdm_consent_records')
|
|
->where('consent_handle', $handle)
|
|
->orderBy('id', 'DESC')
|
|
->get(1)
|
|
->getRowArray();
|
|
|
|
if (empty($existing)) {
|
|
return $this->response->setStatusCode(404)->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => 'consent_handle not found',
|
|
]);
|
|
}
|
|
|
|
$currentStatus = strtolower(trim((string) ($existing['consent_status'] ?? '')));
|
|
$incomingStatus = strtolower($status);
|
|
$currentExpires = trim((string) ($existing['expires_at'] ?? ''));
|
|
$incomingExpires = $expiresAt !== '' ? $expiresAt : '';
|
|
|
|
if ($this->isStaleConsentTransition($currentStatus, $incomingStatus)) {
|
|
return $this->response->setJSON([
|
|
'ok' => 1,
|
|
'consent_handle' => $handle,
|
|
'status' => $currentStatus,
|
|
'ignored' => 1,
|
|
'reason' => 'stale_transition',
|
|
]);
|
|
}
|
|
|
|
$rawPayloadJson = (string) json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
$existingPayload = trim((string) ($existing['raw_payload_json'] ?? ''));
|
|
$isDuplicate = ($currentStatus === $incomingStatus) && ($currentExpires === $incomingExpires) && ($existingPayload === $rawPayloadJson);
|
|
if ($isDuplicate) {
|
|
return $this->response->setJSON([
|
|
'ok' => 1,
|
|
'consent_handle' => $handle,
|
|
'status' => $incomingStatus,
|
|
'duplicate' => 1,
|
|
]);
|
|
}
|
|
|
|
$update = [
|
|
'consent_status' => $incomingStatus,
|
|
'granted_at' => $incomingStatus === 'approved'
|
|
? ((string) ($existing['granted_at'] ?? '') !== '' ? (string) $existing['granted_at'] : $now)
|
|
: (string) ($existing['granted_at'] ?? ''),
|
|
'expires_at' => $expiresAt !== '' ? $expiresAt : null,
|
|
'raw_payload_json' => $rawPayloadJson,
|
|
'updated_at' => $now,
|
|
];
|
|
|
|
$this->db->table('abdm_consent_records')
|
|
->where('consent_handle', $handle)
|
|
->update($update);
|
|
|
|
return $this->response->setJSON(['ok' => 1, 'consent_handle' => $handle, 'status' => $incomingStatus]);
|
|
}
|
|
|
|
public function scanShareLookup()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$qrPayload = trim((string) $this->request->getPost('qr_payload'));
|
|
if ($qrPayload === '') {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'qr_payload is required']);
|
|
}
|
|
|
|
$abhaId = '';
|
|
if (preg_match('/\b(\d{14})\b/', $qrPayload, $match) === 1) {
|
|
$abhaId = (string) ($match[1] ?? '');
|
|
}
|
|
|
|
$payload = [
|
|
'qr_payload' => $qrPayload,
|
|
'abha_id_hint' => $abhaId,
|
|
'requested_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
|
|
];
|
|
|
|
$queueId = null;
|
|
try {
|
|
$result = $this->connector->scanShareLookup($qrPayload, $abhaId, $payload);
|
|
$queueId = $result['queue_id'] ?? null;
|
|
} catch (\Throwable $e) {
|
|
}
|
|
|
|
return $this->response->setJSON([
|
|
'ok' => 1,
|
|
'status' => 'queued',
|
|
'queue_id' => $queueId,
|
|
'abha_id_hint' => $abhaId,
|
|
'message' => 'Scan & Share lookup queued to center server.',
|
|
]);
|
|
}
|
|
|
|
public function sharePrescriptionBundle()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$opdId = (int) $this->request->getPost('opd_id');
|
|
$sessionId = (int) $this->request->getPost('opd_session_id');
|
|
$patientId = (int) $this->request->getPost('patient_id');
|
|
$abhaId = trim((string) $this->request->getPost('abha_id'));
|
|
$consentHandle = trim((string) $this->request->getPost('consent_handle'));
|
|
|
|
if ($opdId <= 0 || $patientId <= 0 || $abhaId === '') {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'opd_id, patient_id and abha_id are required']);
|
|
}
|
|
|
|
if (! $this->db->tableExists('opd_fhir_documents')) {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'opd_fhir_documents table not found']);
|
|
}
|
|
|
|
$consent = $this->getActiveConsentRecord($patientId, $abhaId, $consentHandle);
|
|
if ($consent === null) {
|
|
return $this->response->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => 'No active consent found. Share blocked due to expiry/not-approved consent.',
|
|
]);
|
|
}
|
|
|
|
$builder = $this->db->table('opd_fhir_documents')
|
|
->where('opd_id', $opdId)
|
|
->where('bundle_type', 'MedicationRequestBundle');
|
|
if ($sessionId > 0) {
|
|
$builder->where('opd_session_id', $sessionId);
|
|
}
|
|
$bundleRow = $builder->orderBy('id', 'DESC')->get(1)->getRowArray();
|
|
|
|
if (empty($bundleRow)) {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'No FHIR bundle found for selected OPD/session']);
|
|
}
|
|
|
|
$bundleJson = (string) ($bundleRow['bundle_json'] ?? '{}');
|
|
$bundle = json_decode($bundleJson, true);
|
|
if (! is_array($bundle)) {
|
|
$bundle = ['raw' => $bundleJson];
|
|
}
|
|
|
|
$payload = [
|
|
'opd_id' => $opdId,
|
|
'opd_session_id' => (int) ($bundleRow['opd_session_id'] ?? 0),
|
|
'patient_id' => $patientId,
|
|
'abha_id' => $abhaId,
|
|
'consent_handle' => (string) ($consent['consent_handle'] ?? ''),
|
|
'bundle_type' => (string) ($bundleRow['bundle_type'] ?? 'MedicationRequestBundle'),
|
|
'bundle' => $bundle,
|
|
];
|
|
|
|
// Store FHIR payload in health_records before pushing
|
|
$healthRecordId = $this->storeHealthRecord([
|
|
'patient_id' => $patientId,
|
|
'abha_id' => $abhaId,
|
|
'hi_type' => 'OPConsultRecord',
|
|
'entity_type' => 'opd',
|
|
'entity_id' => (string) $opdId,
|
|
'fhir_bundle' => $bundleJson,
|
|
'consent_handle' => (string) ($consent['consent_handle'] ?? ''),
|
|
]);
|
|
|
|
$queueId = null;
|
|
$connectorError = null;
|
|
try {
|
|
$result = $this->connector->sharePrescriptionBundle($payload, (string) ($bundleRow['id'] ?? ''));
|
|
$queueId = $result['queue_id'] ?? null;
|
|
} catch (\Throwable $e) {
|
|
$connectorError = $e->getMessage();
|
|
}
|
|
|
|
// Update health_record with txn_id and create record_link
|
|
if ($healthRecordId > 0) {
|
|
$this->updateHealthRecordTxn($healthRecordId, (string) ($queueId ?? ''), $connectorError);
|
|
}
|
|
|
|
$this->getAuditService()->log([
|
|
'action' => 'push_record',
|
|
'entity_type' => 'opd',
|
|
'entity_id' => (string) $opdId,
|
|
'abha_id' => $abhaId,
|
|
'patient_id' => $patientId,
|
|
'request' => ['opd_id' => $opdId, 'hi_type' => 'OPConsultRecord', 'consent_handle' => (string) ($consent['consent_handle'] ?? '')],
|
|
'response' => ['queue_id' => $queueId],
|
|
'outcome' => $connectorError === null ? 'success' : 'failure',
|
|
'error_message' => (string) ($connectorError ?? ''),
|
|
]);
|
|
|
|
return $this->response->setJSON([
|
|
'ok' => 1,
|
|
'queue_id' => $queueId,
|
|
'consent_handle' => (string) ($consent['consent_handle'] ?? ''),
|
|
'status' => 'queued',
|
|
]);
|
|
}
|
|
|
|
public function shareIpdDischargeBundle()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$ipdId = (int) $this->request->getPost('ipd_id');
|
|
$patientId = (int) $this->request->getPost('patient_id');
|
|
$abhaId = trim((string) $this->request->getPost('abha_id'));
|
|
$consentHandle = trim((string) $this->request->getPost('consent_handle'));
|
|
|
|
if ($ipdId <= 0 || $patientId <= 0 || $abhaId === '') {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'ipd_id, patient_id and abha_id are required']);
|
|
}
|
|
|
|
$consent = $this->getActiveConsentRecord($patientId, $abhaId, $consentHandle);
|
|
if ($consent === null) {
|
|
return $this->response->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => 'No active consent found. Share blocked due to expiry/not-approved consent.',
|
|
]);
|
|
}
|
|
|
|
// ── Load IPD data ──────────────────────────────────────────────────────
|
|
$ipdRow = $this->db->tableExists('ipd_master')
|
|
? ($this->db->table('ipd_master')->where('id', $ipdId)->get(1)->getRowArray() ?? [])
|
|
: [];
|
|
|
|
$summaryHtml = '';
|
|
if ($this->db->tableExists('ipd_discharge') && $this->db->fieldExists('content', 'ipd_discharge')) {
|
|
$summaryRow = $this->db->table('ipd_discharge')
|
|
->select('content')
|
|
->where('ipd_id', $ipdId)
|
|
->orderBy('id', 'DESC')
|
|
->get(1)
|
|
->getRowArray();
|
|
$summaryHtml = trim((string) ($summaryRow['content'] ?? ''));
|
|
}
|
|
|
|
// ── Load patient data ──────────────────────────────────────────────────
|
|
$patientRow = [];
|
|
if ($this->db->tableExists('patient_master')) {
|
|
$patientRow = $this->db->table('patient_master')->where('id', $patientId)->get(1)->getRowArray() ?? [];
|
|
}
|
|
$patName = trim(
|
|
trim((string) ($patientRow['p_fname'] ?? '')) . ' ' .
|
|
trim((string) ($patientRow['p_lname'] ?? ''))
|
|
) ?: trim((string) ($ipdRow['P_name'] ?? ''));
|
|
|
|
// ── Load hospital profile (HFR ID, name) ──────────────────────────────
|
|
$hospitalProfile = $this->getHospitalProfileForFhir();
|
|
|
|
// ── Load attending doctor ─────────────────────────────────────────────
|
|
$doctorId = (int) ($ipdRow['r_doc_id'] ?? 0);
|
|
$doctorName = trim((string) ($ipdRow['r_doc_name'] ?? ''));
|
|
$doctorRegNo = '';
|
|
if ($doctorId > 0 && $this->db->tableExists('doctor_master')) {
|
|
$dFields = $this->db->getFieldNames('doctor_master') ?? [];
|
|
$dSelect = ['id'];
|
|
foreach (['p_fname', 'p_lname', 'doctor_reg_no', 'registration_no', 'reg_no'] as $f) {
|
|
if (in_array($f, $dFields, true)) {
|
|
$dSelect[] = $f;
|
|
}
|
|
}
|
|
$dRow = $this->db->table('doctor_master')->select(implode(',', $dSelect))->where('id', $doctorId)->get(1)->getRowArray() ?? [];
|
|
if ($dRow !== [] && $doctorName === '') {
|
|
$doctorName = trim(trim((string) ($dRow['p_fname'] ?? '')) . ' ' . trim((string) ($dRow['p_lname'] ?? '')));
|
|
}
|
|
foreach (['doctor_reg_no', 'registration_no', 'reg_no'] as $f) {
|
|
if (! empty($dRow[$f])) {
|
|
$doctorRegNo = trim((string) $dRow[$f]);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Parse dates ───────────────────────────────────────────────────────
|
|
$admissionRaw = trim((string) ($ipdRow['register_date'] ?? ''));
|
|
$dischargeRaw = trim((string) ($ipdRow['discharge_date'] ?? ''));
|
|
$admissionDate = $admissionRaw !== '' ? (new \DateTime($admissionRaw, new \DateTimeZone('Asia/Kolkata')))->format('Y-m-d\TH:i:sP') : '';
|
|
$dischargeDate = $dischargeRaw !== '' ? (new \DateTime($dischargeRaw, new \DateTimeZone('Asia/Kolkata')))->format('Y-m-d\TH:i:sP') : '';
|
|
$visitDate = $dischargeRaw !== '' ? date('Y-m-d', strtotime($dischargeRaw)) : ($admissionRaw !== '' ? date('Y-m-d', strtotime($admissionRaw)) : date('Y-m-d'));
|
|
|
|
// ── Build FHIR DischargeSummaryRecord bundle ────────────────────────────
|
|
$fhir = new FhirR4Builder();
|
|
|
|
$patient = [
|
|
'id' => (string) $patientId,
|
|
'name' => $patName,
|
|
'gender' => trim((string) ($patientRow['gender'] ?? '')),
|
|
'birthDate' => ! empty($patientRow['dob']) ? date('Y-m-d', strtotime((string) $patientRow['dob'])) : '',
|
|
'abhaAddress' => $abhaId,
|
|
];
|
|
|
|
$encounter = [
|
|
'id' => (string) $ipdId,
|
|
'admission_date' => $admissionDate,
|
|
'discharge_date' => $dischargeDate,
|
|
'ipd_code' => trim((string) ($ipdRow['ipd_code'] ?? '')),
|
|
];
|
|
|
|
// Parse summary HTML into structured sections where possible
|
|
$chiefComplaints = trim((string) ($ipdRow['problem'] ?? ''));
|
|
$summary = [
|
|
'title' => 'Discharge Summary',
|
|
'chief_complaints' => $chiefComplaints,
|
|
'clinical_summary_html' => $summaryHtml,
|
|
];
|
|
|
|
$practitioner = $doctorName !== ''
|
|
? ['name' => $doctorName, 'registration_number' => $doctorRegNo]
|
|
: null;
|
|
$organization = $hospitalProfile['name'] !== ''
|
|
? ['name' => $hospitalProfile['name'], 'hfr_id' => $hospitalProfile['hfr_id']]
|
|
: null;
|
|
|
|
$bundle = $fhir->buildDischargeSummaryBundle($patient, $encounter, $summary, $practitioner, $organization);
|
|
$bundleJson = (string) json_encode($bundle, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
|
|
// ── Store health_record ───────────────────────────────────────────────
|
|
$ipdCode = trim((string) ($ipdRow['ipd_code'] ?? ''));
|
|
$ccRef = 'IPD-' . $ipdId . ($ipdCode !== '' ? '-' . $ipdCode : '') . '-' . $visitDate;
|
|
$healthRecordId = $this->storeHealthRecord([
|
|
'patient_id' => $patientId,
|
|
'abha_id' => $abhaId,
|
|
'hi_type' => 'DischargeSummaryRecord',
|
|
'entity_type' => 'ipd',
|
|
'entity_id' => (string) $ipdId,
|
|
'fhir_bundle' => $bundleJson,
|
|
'consent_handle' => (string) ($consent['consent_handle'] ?? ''),
|
|
]);
|
|
|
|
// ── Push via pushRecord() (POST /v3/records/push) ─────────────────────
|
|
$queueId = null;
|
|
$bridgeRecordId = 0;
|
|
$connectorError = null;
|
|
try {
|
|
$result = $this->connector->pushRecord([
|
|
'patient_id' => (string) $patientId,
|
|
'patient_name' => $patName,
|
|
'abha_id' => $abhaId,
|
|
'hi_type' => 'DischargeSummaryRecord',
|
|
'record_type' => 'discharge_summary',
|
|
'visit_date' => $visitDate,
|
|
'doctor_name' => $doctorName,
|
|
'care_context_reference' => $ccRef,
|
|
'notes' => 'Discharge Summary - ' . ($ipdCode !== '' ? 'IPD#' . $ipdCode : 'IPD#' . $ipdId),
|
|
'record_data' => $bundle,
|
|
]);
|
|
$queueId = (string) ($result['queue_id'] ?? '');
|
|
$bridgeRecordId = (int) ($result['id'] ?? 0);
|
|
} catch (\Throwable $e) {
|
|
$connectorError = $e->getMessage();
|
|
}
|
|
|
|
if ($healthRecordId > 0) {
|
|
$this->updateHealthRecordTxn($healthRecordId, $queueId ?? '', $connectorError, $bridgeRecordId);
|
|
}
|
|
|
|
$this->getAuditService()->log([
|
|
'action' => 'push_record',
|
|
'entity_type' => 'ipd',
|
|
'entity_id' => (string) $ipdId,
|
|
'abha_id' => $abhaId,
|
|
'patient_id' => $patientId,
|
|
'request' => ['ipd_id' => $ipdId, 'hi_type' => 'DischargeSummaryRecord', 'consent_handle' => (string) ($consent['consent_handle'] ?? '')],
|
|
'response' => ['queue_id' => $queueId],
|
|
'outcome' => $connectorError === null ? 'success' : 'failure',
|
|
'error_message' => (string) ($connectorError ?? ''),
|
|
]);
|
|
|
|
return $this->response->setJSON([
|
|
'ok' => $connectorError === null ? 1 : 0,
|
|
'queue_id' => $queueId,
|
|
'consent_handle' => (string) ($consent['consent_handle'] ?? ''),
|
|
'status' => $connectorError === null ? 'queued' : 'failed',
|
|
'error' => $connectorError,
|
|
]);
|
|
}
|
|
|
|
public function shareDiagnosisReportBundle()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(403)->setJSON(['error' => 'AJAX only']);
|
|
}
|
|
|
|
$labReqId = (int) ($this->request->getPost('lab_req_id') ?? 0);
|
|
$patientId = (int) ($this->request->getPost('patient_id') ?? 0);
|
|
$abhaId = trim((string) ($this->request->getPost('abha_id') ?? ''));
|
|
$consentHandle = trim((string) ($this->request->getPost('consent_handle') ?? ''));
|
|
|
|
if ($labReqId <= 0 || $patientId <= 0 || $abhaId === '') {
|
|
return $this->response->setJSON(['ok' => 0, 'error' => 'lab_req_id, patient_id and abha_id are required']);
|
|
}
|
|
|
|
// ── Load lab request ──────────────────────────────────────────────────
|
|
$labReq = $this->db->table('lab_request')
|
|
->select('id, patient_name, lab_type, charge_id, Report_Data, report_data_Impression, status, reported_time')
|
|
->where('id', $labReqId)
|
|
->get(1)
|
|
->getRow();
|
|
|
|
if (! $labReq) {
|
|
return $this->response->setJSON(['ok' => 0, 'error' => 'Lab request not found']);
|
|
}
|
|
|
|
$consentRecord = $this->getActiveConsentRecord($patientId, $abhaId, $consentHandle);
|
|
$effectiveConsent = (string) ($consentRecord['consent_handle'] ?? $consentHandle);
|
|
|
|
// ── Load patient ──────────────────────────────────────────────────────
|
|
$patientRow = [];
|
|
if ($this->db->tableExists('patient_master')) {
|
|
$patientRow = $this->db->table('patient_master')->where('id', $patientId)->get(1)->getRowArray() ?? [];
|
|
}
|
|
$patName = trim(
|
|
trim((string) ($patientRow['p_fname'] ?? '')) . ' ' .
|
|
trim((string) ($patientRow['p_lname'] ?? ''))
|
|
) ?: trim((string) ($labReq->patient_name ?? ''));
|
|
|
|
// ── Load test / charge name ────────────────────────────────────────────
|
|
$testTitle = '';
|
|
$chargeId = (int) ($labReq->charge_id ?? 0);
|
|
if ($chargeId > 0 && $this->db->tableExists('charge_master')) {
|
|
$chargeRow = $this->db->table('charge_master')->select('charge_name, charge_type')->where('id', $chargeId)->get(1)->getRowArray() ?? [];
|
|
$testTitle = trim((string) ($chargeRow['charge_name'] ?? ''));
|
|
}
|
|
if ($testTitle === '') {
|
|
$testTitle = $this->mapLabTypeToTitle((int) ($labReq->lab_type ?? 0));
|
|
}
|
|
|
|
// ── Load hospital profile ─────────────────────────────────────────────
|
|
$hospitalProfile = $this->getHospitalProfileForFhir();
|
|
|
|
// ── Parse reported time ───────────────────────────────────────────────
|
|
$reportedRaw = trim((string) ($labReq->reported_time ?? ''));
|
|
$reportedAt = $reportedRaw !== '' ? (new \DateTime($reportedRaw, new \DateTimeZone('Asia/Kolkata')))->format('Y-m-d\TH:i:sP') : '';
|
|
$visitDate = $reportedRaw !== '' ? date('Y-m-d', strtotime($reportedRaw)) : date('Y-m-d');
|
|
|
|
// ── Build FHIR DiagnosticReportRecord bundle ───────────────────────────
|
|
$fhir = new FhirR4Builder();
|
|
|
|
$patient = [
|
|
'id' => (string) $patientId,
|
|
'name' => $patName,
|
|
'gender' => trim((string) ($patientRow['gender'] ?? '')),
|
|
'birthDate' => ! empty($patientRow['dob']) ? date('Y-m-d', strtotime((string) $patientRow['dob'])) : '',
|
|
'abhaAddress' => $abhaId,
|
|
];
|
|
|
|
$diagnosticReport = [
|
|
'id' => (string) $labReqId,
|
|
'title' => $testTitle ?: 'Laboratory Report',
|
|
'status' => $labReq->status == 1 ? 'final' : 'preliminary',
|
|
'conclusion' => trim((string) ($labReq->report_data_Impression ?? '')),
|
|
'reported_at' => $reportedAt,
|
|
'report_html' => trim((string) ($labReq->Report_Data ?? '')),
|
|
];
|
|
|
|
$organization = $hospitalProfile['name'] !== ''
|
|
? ['name' => $hospitalProfile['name'], 'hfr_id' => $hospitalProfile['hfr_id']]
|
|
: null;
|
|
|
|
$encounter = [
|
|
'id' => 'LAB-' . $labReqId,
|
|
'status' => 'finished',
|
|
'class_code' => 'AMB',
|
|
'period_start' => $reportedAt,
|
|
];
|
|
|
|
$bundle = $fhir->buildLabReportBundle($patient, $diagnosticReport, [], null, $organization, $encounter);
|
|
$bundleJson = (string) json_encode($bundle, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
|
|
// ── Store health_record ───────────────────────────────────────────────
|
|
$ccRef = 'LAB-' . $labReqId . '-' . $visitDate;
|
|
$healthRecordId = $this->storeHealthRecord([
|
|
'patient_id' => $patientId,
|
|
'abha_id' => $abhaId,
|
|
'hi_type' => 'DiagnosticReportRecord',
|
|
'entity_type' => 'lab',
|
|
'entity_id' => (string) $labReqId,
|
|
'fhir_bundle' => $bundleJson,
|
|
'consent_handle' => $effectiveConsent,
|
|
]);
|
|
|
|
// ── Push via pushRecord() (POST /v3/records/push) ─────────────────────
|
|
$queueId = null;
|
|
$bridgeRecordId = 0;
|
|
$connectorError = null;
|
|
try {
|
|
$result = $this->connector->pushRecord([
|
|
'patient_id' => (string) $patientId,
|
|
'patient_name' => $patName,
|
|
'abha_id' => $abhaId,
|
|
'hi_type' => 'DiagnosticReportRecord',
|
|
'record_type' => 'lab_report',
|
|
'visit_date' => $visitDate,
|
|
'care_context_reference' => $ccRef,
|
|
'notes' => $testTitle !== '' ? $testTitle : 'Lab Report',
|
|
'record_data' => $bundle,
|
|
]);
|
|
$queueId = (string) ($result['queue_id'] ?? '');
|
|
$bridgeRecordId = (int) ($result['id'] ?? 0);
|
|
} catch (\Throwable $e) {
|
|
$connectorError = $e->getMessage();
|
|
}
|
|
|
|
if ($healthRecordId > 0) {
|
|
$this->updateHealthRecordTxn($healthRecordId, $queueId ?? '', $connectorError, $bridgeRecordId);
|
|
}
|
|
|
|
$this->getAuditService()->log([
|
|
'action' => 'push_record',
|
|
'entity_type' => 'lab',
|
|
'entity_id' => (string) $labReqId,
|
|
'abha_id' => $abhaId,
|
|
'patient_id' => $patientId,
|
|
'request' => ['lab_req_id' => $labReqId, 'hi_type' => 'DiagnosticReportRecord', 'consent_handle' => $effectiveConsent],
|
|
'response' => ['queue_id' => $queueId],
|
|
'outcome' => $connectorError === null ? 'success' : 'failure',
|
|
'error_message' => (string) ($connectorError ?? ''),
|
|
]);
|
|
|
|
return $this->response->setJSON([
|
|
'ok' => $connectorError === null ? 1 : 0,
|
|
'queue_id' => $queueId,
|
|
'consent_handle' => $effectiveConsent,
|
|
'status' => $connectorError === null ? 'queued' : 'failed',
|
|
'error' => $connectorError,
|
|
]);
|
|
}
|
|
|
|
public function nhcxClaimCreate()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
if (! $this->db->tableExists('nhcx_claim_documents')) {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'nhcx_claim_documents table not found']);
|
|
}
|
|
|
|
$patientId = (int) $this->request->getPost('patient_id');
|
|
$encounterId = (int) $this->request->getPost('encounter_id');
|
|
$ipdId = (int) $this->request->getPost('ipd_id');
|
|
$caseId = (int) $this->request->getPost('case_id');
|
|
$totalAmount = (float) $this->request->getPost('total_amount');
|
|
$claimType = trim((string) $this->request->getPost('claim_type')) ?: 'institutional';
|
|
|
|
if ($patientId <= 0 || $encounterId <= 0) {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'patient_id and encounter_id are required']);
|
|
}
|
|
|
|
$patient = [
|
|
'id' => (string) $patientId,
|
|
'name' => (string) $this->request->getPost('patient_name'),
|
|
'gender' => (string) $this->request->getPost('patient_gender'),
|
|
'birthDate' => (string) $this->request->getPost('patient_birthdate'),
|
|
'abhaAddress' => (string) $this->request->getPost('abha_address'),
|
|
];
|
|
|
|
$encounter = [
|
|
'id' => (string) $encounterId,
|
|
'status' => 'finished',
|
|
];
|
|
|
|
$items = $this->request->getPost('items');
|
|
if (is_string($items)) {
|
|
$decoded = json_decode($items, true);
|
|
$items = is_array($decoded) ? $decoded : [];
|
|
}
|
|
if (! is_array($items)) {
|
|
$items = [];
|
|
}
|
|
|
|
$claim = [
|
|
'id' => 'claim-' . $patientId . '-' . $encounterId . '-' . date('YmdHis'),
|
|
'status' => 'active',
|
|
'use' => 'claim',
|
|
'type' => $claimType,
|
|
'total' => $totalAmount,
|
|
'provider' => (string) $this->request->getPost('provider_name'),
|
|
'insurer' => (string) $this->request->getPost('insurer_name'),
|
|
'priority' => (string) $this->request->getPost('priority'),
|
|
'items' => $items,
|
|
];
|
|
|
|
$fhir = new FhirR4Builder();
|
|
$bundle = $fhir->buildClaimBundle($patient, $encounter, $claim);
|
|
$bundleJson = (string) json_encode($bundle, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
|
|
$now = Time::now('Asia/Kolkata')->toDateTimeString();
|
|
$this->db->table('nhcx_claim_documents')->insert([
|
|
'ipd_id' => $ipdId > 0 ? $ipdId : null,
|
|
'case_id' => $caseId > 0 ? $caseId : null,
|
|
'patient_id' => $patientId,
|
|
'claim_type' => $claimType,
|
|
'claim_json' => $bundleJson,
|
|
'claim_status' => 'draft',
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
]);
|
|
|
|
$documentId = (int) $this->db->insertID();
|
|
$queueId = null;
|
|
try {
|
|
$result = $this->connector->nhcxClaimCreate($bundle, $documentId, $patientId, $encounterId);
|
|
$queueId = $result['queue_id'] ?? null;
|
|
} catch (\Throwable $e) {
|
|
}
|
|
|
|
return $this->response->setJSON([
|
|
'ok' => 1,
|
|
'document_id' => $documentId,
|
|
'queue_id' => $queueId,
|
|
'claim_status' => 'draft',
|
|
]);
|
|
}
|
|
|
|
public function nhcxClaimStatusRequest()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
if (! $this->db->tableExists('nhcx_claim_documents')) {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'nhcx_claim_documents table not found']);
|
|
}
|
|
|
|
$documentId = (int) $this->request->getPost('document_id');
|
|
if ($documentId <= 0) {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'document_id is required']);
|
|
}
|
|
|
|
$row = $this->db->table('nhcx_claim_documents')->where('id', $documentId)->get(1)->getRowArray();
|
|
if (empty($row)) {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'claim document not found']);
|
|
}
|
|
|
|
$payload = [
|
|
'nhcx_claim_document_id' => $documentId,
|
|
'external_ref' => (string) ($row['external_ref'] ?? ''),
|
|
'claim_status' => (string) ($row['claim_status'] ?? ''),
|
|
'requested_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
|
|
];
|
|
|
|
$queueId = null;
|
|
try {
|
|
$result = $this->connector->nhcxClaimStatusRequest($documentId, $payload['external_ref'], $payload['claim_status'], $payload);
|
|
$queueId = $result['queue_id'] ?? null;
|
|
} catch (\Throwable $e) {
|
|
}
|
|
|
|
return $this->response->setJSON([
|
|
'ok' => 1,
|
|
'document_id' => $documentId,
|
|
'queue_id' => $queueId,
|
|
'status' => 'queued',
|
|
]);
|
|
}
|
|
|
|
public function nhcxClaimStatusCallback()
|
|
{
|
|
$signatureFailure = $this->validateWebhookSignature();
|
|
if ($signatureFailure !== null) {
|
|
return $signatureFailure;
|
|
}
|
|
|
|
$payload = $this->request->getJSON(true);
|
|
if (! is_array($payload)) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid JSON payload']);
|
|
}
|
|
|
|
if (! $this->db->tableExists('nhcx_claim_documents')) {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'nhcx_claim_documents table not found']);
|
|
}
|
|
|
|
$documentId = (int) ($payload['document_id'] ?? 0);
|
|
$externalRef = trim((string) ($payload['external_ref'] ?? ''));
|
|
$status = trim((string) ($payload['claim_status'] ?? 'unknown'));
|
|
$errorMessage = trim((string) ($payload['error_message'] ?? ''));
|
|
|
|
if ($documentId <= 0 && $externalRef === '') {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'document_id or external_ref is required']);
|
|
}
|
|
|
|
$existingBuilder = $this->db->table('nhcx_claim_documents');
|
|
if ($documentId > 0) {
|
|
$existingBuilder->where('id', $documentId);
|
|
} else {
|
|
$existingBuilder->where('external_ref', $externalRef);
|
|
}
|
|
$existing = $existingBuilder->get(1)->getRowArray();
|
|
if (empty($existing)) {
|
|
return $this->response->setStatusCode(404)->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => 'claim document not found',
|
|
]);
|
|
}
|
|
|
|
$currentStatus = strtolower(trim((string) ($existing['claim_status'] ?? 'unknown')));
|
|
$incomingStatus = strtolower($status);
|
|
$currentExternalRef = trim((string) ($existing['external_ref'] ?? ''));
|
|
$currentError = trim((string) ($existing['error_message'] ?? ''));
|
|
|
|
if ($this->isStaleClaimTransition($currentStatus, $incomingStatus)) {
|
|
return $this->response->setJSON([
|
|
'ok' => 1,
|
|
'document_id' => (int) ($existing['id'] ?? $documentId),
|
|
'external_ref' => $currentExternalRef,
|
|
'claim_status' => $currentStatus,
|
|
'ignored' => 1,
|
|
'reason' => 'stale_transition',
|
|
]);
|
|
}
|
|
|
|
$isDuplicate = ($currentStatus === $incomingStatus)
|
|
&& ($currentExternalRef === $externalRef)
|
|
&& ($currentError === $errorMessage);
|
|
if ($isDuplicate) {
|
|
return $this->response->setJSON([
|
|
'ok' => 1,
|
|
'document_id' => (int) ($existing['id'] ?? $documentId),
|
|
'external_ref' => $currentExternalRef,
|
|
'claim_status' => $currentStatus,
|
|
'duplicate' => 1,
|
|
]);
|
|
}
|
|
|
|
$builder = $this->db->table('nhcx_claim_documents')->where('id', (int) $existing['id']);
|
|
|
|
$now = Time::now('Asia/Kolkata')->toDateTimeString();
|
|
$builder->update([
|
|
'claim_status' => $incomingStatus,
|
|
'external_ref' => $externalRef !== '' ? $externalRef : null,
|
|
'error_message' => $errorMessage !== '' ? mb_substr($errorMessage, 0, 1000) : null,
|
|
'pushed_at' => $incomingStatus === 'submitted'
|
|
? ((string) ($existing['pushed_at'] ?? '') !== '' ? (string) $existing['pushed_at'] : $now)
|
|
: (string) ($existing['pushed_at'] ?? ''),
|
|
'updated_at' => $now,
|
|
]);
|
|
|
|
return $this->response->setJSON([
|
|
'ok' => 1,
|
|
'document_id' => (int) ($existing['id'] ?? $documentId),
|
|
'external_ref' => $externalRef,
|
|
'claim_status' => $incomingStatus,
|
|
]);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Record Linked Callback — ABDM notifies HMS when a record is successfully
|
|
// linked to an ABHA address.
|
|
// POST /AbdmGateway/record_linked_callback (no auth filter — public webhook)
|
|
// =========================================================================
|
|
|
|
public function recordLinkedCallback()
|
|
{
|
|
$signatureFailure = $this->validateWebhookSignature();
|
|
if ($signatureFailure !== null) {
|
|
return $signatureFailure;
|
|
}
|
|
|
|
$payload = $this->request->getJSON(true);
|
|
if (! is_array($payload)) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid JSON payload']);
|
|
}
|
|
|
|
$abdmTxnId = trim((string) ($payload['queue_id'] ?? $payload['abdm_txn_id'] ?? ''));
|
|
$careContextRef = trim((string) ($payload['care_context_reference'] ?? $payload['care_context_ref'] ?? ''));
|
|
$abhaId = trim((string) ($payload['abha_id'] ?? ''));
|
|
$status = strtolower(trim((string) ($payload['status'] ?? 'linked')));
|
|
$now = Time::now('Asia/Kolkata')->toDateTimeString();
|
|
|
|
if ($abdmTxnId === '' && $careContextRef === '') {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'queue_id or care_context_reference is required']);
|
|
}
|
|
|
|
// Update health_records
|
|
if ($this->db->tableExists('health_records')) {
|
|
$hrBuilder = $this->db->table('health_records');
|
|
if ($abdmTxnId !== '') {
|
|
$hrBuilder->where('abdm_txn_id', $abdmTxnId);
|
|
} elseif ($careContextRef !== '') {
|
|
$hrBuilder->where('care_context_reference', $careContextRef);
|
|
}
|
|
$hrBuilder->update([
|
|
'push_status' => 'linked',
|
|
'linked_at' => $now,
|
|
'care_context_reference' => $careContextRef !== '' ? $careContextRef : null,
|
|
'updated_at' => $now,
|
|
]);
|
|
}
|
|
|
|
// Insert / update record_links
|
|
if ($this->db->tableExists('record_links')) {
|
|
$existing = $this->db->table('record_links')
|
|
->where('abdm_txn_id', $abdmTxnId)
|
|
->get(1)
|
|
->getRowArray();
|
|
|
|
$responseJson = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
|
|
if (! empty($existing)) {
|
|
$this->db->table('record_links')
|
|
->where('id', (int) $existing['id'])
|
|
->update([
|
|
'link_status' => $status,
|
|
'care_context_reference' => $careContextRef !== '' ? $careContextRef : (string) ($existing['care_context_reference'] ?? ''),
|
|
'response_json' => $responseJson,
|
|
'linked_at' => $now,
|
|
'updated_at' => $now,
|
|
]);
|
|
} else {
|
|
$this->db->table('record_links')->insert([
|
|
'abdm_txn_id' => $abdmTxnId !== '' ? $abdmTxnId : null,
|
|
'care_context_reference' => $careContextRef !== '' ? $careContextRef : null,
|
|
'abha_id' => $abhaId !== '' ? $abhaId : null,
|
|
'link_status' => $status,
|
|
'response_json' => $responseJson,
|
|
'linked_at' => $now,
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
]);
|
|
}
|
|
}
|
|
|
|
$this->getAuditService()->log([
|
|
'action' => 'record_linked',
|
|
'entity_type' => 'health_record',
|
|
'entity_id' => $abdmTxnId,
|
|
'abha_id' => $abhaId,
|
|
'response' => $payload,
|
|
'outcome' => $status === 'linked' ? 'success' : 'failure',
|
|
]);
|
|
|
|
// Update patient_master.abdm_linked_at when a record is confirmed linked
|
|
if ($status === 'linked' && $abhaId !== '' && $this->db->tableExists('patient_master')) {
|
|
$pmFields = $this->db->getFieldNames('patient_master') ?? [];
|
|
if (in_array('abdm_linked_at', $pmFields, true)) {
|
|
$abhaFieldCandidates = ['abha_id', 'abha_no', 'abha_address', 'abha'];
|
|
foreach ($abhaFieldCandidates as $col) {
|
|
if (in_array($col, $pmFields, true)) {
|
|
$this->db->table('patient_master')
|
|
->where($col, $abhaId)
|
|
->where('abdm_linked_at IS NULL', null, false)
|
|
->update(['abdm_linked_at' => $now]);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $this->response->setJSON(['ok' => 1, 'status' => $status]);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Consent Revoked Callback — ABDM notifies HMS when consent is revoked.
|
|
// POST /AbdmGateway/consent_revoked_callback (no auth filter — public webhook)
|
|
// =========================================================================
|
|
|
|
public function consentRevokedCallback()
|
|
{
|
|
$signatureFailure = $this->validateWebhookSignature();
|
|
if ($signatureFailure !== null) {
|
|
return $signatureFailure;
|
|
}
|
|
|
|
$payload = $this->request->getJSON(true);
|
|
if (! is_array($payload)) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid JSON payload']);
|
|
}
|
|
|
|
$consentHandle = trim((string) ($payload['consent_handle'] ?? ''));
|
|
$abhaId = trim((string) ($payload['abha_id'] ?? ''));
|
|
$revokedAt = trim((string) ($payload['revoked_at'] ?? ''));
|
|
$now = Time::now('Asia/Kolkata')->toDateTimeString();
|
|
|
|
if ($consentHandle === '') {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'consent_handle is required']);
|
|
}
|
|
|
|
if (! $this->db->tableExists('abdm_consent_records')) {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'abdm_consent_records table not found']);
|
|
}
|
|
|
|
$existing = $this->db->table('abdm_consent_records')
|
|
->where('consent_handle', $consentHandle)
|
|
->orderBy('id', 'DESC')
|
|
->get(1)
|
|
->getRowArray();
|
|
|
|
if (empty($existing)) {
|
|
return $this->response->setStatusCode(404)->setJSON(['ok' => 0, 'error_text' => 'consent_handle not found']);
|
|
}
|
|
|
|
// Idempotency — already revoked
|
|
if (strtolower((string) ($existing['consent_status'] ?? '')) === 'revoked') {
|
|
return $this->response->setJSON(['ok' => 1, 'consent_handle' => $consentHandle, 'status' => 'revoked', 'duplicate' => 1]);
|
|
}
|
|
|
|
$this->db->table('abdm_consent_records')
|
|
->where('consent_handle', $consentHandle)
|
|
->update([
|
|
'consent_status' => 'revoked',
|
|
'raw_payload_json' => json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
|
'updated_at' => $now,
|
|
]);
|
|
|
|
$this->getAuditService()->log([
|
|
'action' => 'consent_revoke',
|
|
'entity_type' => 'consent',
|
|
'entity_id' => $consentHandle,
|
|
'abha_id' => $abhaId,
|
|
'patient_id' => (int) ($existing['patient_id'] ?? 0),
|
|
'response' => $payload,
|
|
'outcome' => 'success',
|
|
]);
|
|
|
|
return $this->response->setJSON(['ok' => 1, 'consent_handle' => $consentHandle, 'status' => 'revoked']);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Push a specific health_record to ABDM (user-triggered retry / initial push)
|
|
// POST /AbdmGateway/push_health_record
|
|
// =========================================================================
|
|
|
|
public function pushHealthRecord()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$hrId = (int) $this->request->getPost('health_record_id');
|
|
$consentHandle = trim((string) $this->request->getPost('consent_handle'));
|
|
|
|
if ($hrId <= 0) {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'health_record_id is required']);
|
|
}
|
|
|
|
if (! $this->db->tableExists('health_records')) {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'health_records table not found']);
|
|
}
|
|
|
|
$hr = $this->db->table('health_records')
|
|
->where('id', $hrId)
|
|
->get(1)
|
|
->getRowArray();
|
|
|
|
if (empty($hr)) {
|
|
return $this->response->setStatusCode(404)->setJSON(['ok' => 0, 'error_text' => 'Health record not found']);
|
|
}
|
|
|
|
$patientId = (int) ($hr['patient_id'] ?? 0);
|
|
$abhaId = trim((string) ($hr['abha_id'] ?? ''));
|
|
$hiType = trim((string) ($hr['hi_type'] ?? ''));
|
|
|
|
if ($abhaId === '') {
|
|
// Try from POST param as override
|
|
$abhaId = trim((string) $this->request->getPost('abha_id'));
|
|
}
|
|
|
|
// Decrypt the stored payload (JSON encoded full push payload, may contain 'bundle' key)
|
|
$storedPayload = [];
|
|
$encPayload = trim((string) ($hr['fhir_bundle_enc'] ?? ''));
|
|
if ($encPayload !== '') {
|
|
try {
|
|
$enc = new \App\Libraries\FhirEncryptionService();
|
|
$decrypted = $enc->decrypt($encPayload);
|
|
$storedPayload = json_decode($decrypted, true) ?? [];
|
|
} catch (\Throwable $e) {
|
|
return $this->response->setStatusCode(500)->setJSON(['ok' => 0, 'error_text' => 'Could not decrypt FHIR bundle: ' . $e->getMessage()]);
|
|
}
|
|
}
|
|
|
|
// Extract just the FHIR bundle from the stored payload (stored as full HMS payload)
|
|
$fhirBundle = $storedPayload['bundle'] ?? $storedPayload['fhir_bundle'] ?? $storedPayload;
|
|
|
|
$entityType = trim((string) ($hr['entity_type'] ?? ''));
|
|
$entityId = trim((string) ($hr['entity_id'] ?? ''));
|
|
$visitDate = trim((string) ($storedPayload['visit_date'] ?? $storedPayload['reg_date'] ?? $storedPayload['reported_time'] ?? ''));
|
|
$doctorName = trim((string) ($storedPayload['doctor_name'] ?? $storedPayload['doctor'] ?? ''));
|
|
$patientName = trim((string) ($storedPayload['patient_name'] ?? ''));
|
|
$department = trim((string) ($storedPayload['department'] ?? $storedPayload['dept_name'] ?? ''));
|
|
|
|
$queueId = null;
|
|
$connectorErr = null;
|
|
try {
|
|
// Use the new store-and-link flow (POST /v3/records/push) for re-push:
|
|
// No consent needed — bridge stores record and serves it when ABDM requests data.
|
|
$result = $this->connector->pushRecord([
|
|
'patient_id' => (string) $patientId,
|
|
'patient_name' => $patientName,
|
|
'abha_id' => $abhaId,
|
|
'hi_type' => $hiType,
|
|
'record_type' => $this->mapHiTypeToRecordType($hiType),
|
|
'visit_date' => $visitDate !== '' ? $visitDate : date('Y-m-d'),
|
|
'doctor_name' => $doctorName,
|
|
'department' => $department,
|
|
'care_context_reference' => trim((string) ($hr['care_context_reference'] ?? '')),
|
|
'notes' => trim((string) ($storedPayload['care_context_display'] ?? '')),
|
|
'record_data' => $fhirBundle,
|
|
]);
|
|
$queueId = $result['queue_id'] ?? null;
|
|
$bridgeRecordId = (int) ($result['id'] ?? 0);
|
|
} catch (\Throwable $e) {
|
|
$connectorErr = $e->getMessage();
|
|
$bridgeRecordId = 0;
|
|
}
|
|
|
|
$this->updateHealthRecordTxn($hrId, (string) ($queueId ?? ''), $connectorErr, $bridgeRecordId);
|
|
|
|
$this->getAuditService()->log([
|
|
'action' => 'push_record',
|
|
'entity_type' => $entityType,
|
|
'entity_id' => $entityId,
|
|
'abha_id' => $abhaId,
|
|
'patient_id' => $patientId,
|
|
'request' => ['health_record_id' => $hrId, 'hi_type' => $hiType],
|
|
'response' => ['queue_id' => $queueId],
|
|
'outcome' => $connectorErr === null ? 'success' : 'failure',
|
|
'error_message' => (string) ($connectorErr ?? ''),
|
|
]);
|
|
|
|
return $this->response->setJSON([
|
|
'ok' => $connectorErr === null ? 1 : 0,
|
|
'queue_id' => $queueId,
|
|
'error' => $connectorErr,
|
|
'status' => $connectorErr === null ? 'queued' : 'failed',
|
|
]);
|
|
}
|
|
|
|
// =========================================================================
|
|
// List health_records for a patient (AJAX — used by ABDM task board)
|
|
// GET /AbdmGateway/health_records_list?patient_id=X&abha_id=Y
|
|
// =========================================================================
|
|
|
|
public function healthRecordsList()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
if (! $this->db->tableExists('health_records')) {
|
|
return $this->response->setJSON(['ok' => 1, 'records' => [], 'total' => 0]);
|
|
}
|
|
|
|
$patientId = (int) $this->request->getGet('patient_id');
|
|
$abhaId = trim((string) $this->request->getGet('abha_id'));
|
|
|
|
if ($patientId <= 0 && $abhaId === '') {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'patient_id or abha_id is required']);
|
|
}
|
|
|
|
$builder = $this->db->table('health_records')
|
|
->select('id, patient_id, abha_id, hi_type, entity_type, entity_id, push_status, abdm_txn_id, care_context_reference, consent_handle, push_at, linked_at, created_at')
|
|
->orderBy('id', 'DESC')
|
|
->limit(50);
|
|
|
|
if ($patientId > 0) {
|
|
$builder->where('patient_id', $patientId);
|
|
}
|
|
if ($abhaId !== '') {
|
|
$builder->where('abha_id', $abhaId);
|
|
}
|
|
|
|
$rows = $builder->get()->getResultArray();
|
|
|
|
return $this->response->setJSON(['ok' => 1, 'records' => $rows, 'total' => count($rows)]);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Bridge record status — proxy to GET /api/v3/records/{id}
|
|
// GET /AbdmGateway/bridge_record_status/:id
|
|
// =========================================================================
|
|
|
|
public function bridgeRecordStatus(int $bridgeId)
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$result = $this->connector->getRecord($bridgeId);
|
|
return $this->response->setJSON($result);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Trigger HIP-initiated ABDM linking — POST /api/v3/records/{id}/share
|
|
// POST /AbdmGateway/bridge_record_share/:id
|
|
// =========================================================================
|
|
|
|
public function bridgeRecordShare(int $bridgeId)
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$result = $this->connector->triggerShare($bridgeId);
|
|
return $this->response->setJSON($result);
|
|
}
|
|
|
|
// =========================================================================
|
|
// OPD running token status — GET /api/v3/opd/running-token-status
|
|
// GET /AbdmGateway/opd_running_token_status
|
|
// =========================================================================
|
|
|
|
public function opdRunningTokenStatus()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$result = $this->connector->opdRunningTokenStatus();
|
|
return $this->response->setJSON($result);
|
|
}
|
|
|
|
private function validateWebhookSignature()
|
|
{
|
|
$secret = $this->readRuntimeSetting('EKA_WEBHOOK_SECRET');
|
|
if ($secret === '') {
|
|
return null;
|
|
}
|
|
|
|
$signature = trim((string) ($this->request->getHeaderLine('X-Eka-Signature') ?: ''));
|
|
if ($signature === '') {
|
|
$signature = trim((string) ($this->request->getHeaderLine('X-Signature') ?: ''));
|
|
}
|
|
if ($signature === '') {
|
|
$signature = trim((string) ($this->request->getHeaderLine('X-Hub-Signature-256') ?: ''));
|
|
}
|
|
if ($signature === '') {
|
|
return $this->response->setStatusCode(401)->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => 'Missing signature header',
|
|
]);
|
|
}
|
|
|
|
$signature = strtolower($signature);
|
|
if (str_starts_with($signature, 'sha256=')) {
|
|
$signature = substr($signature, 7);
|
|
}
|
|
|
|
$rawBody = (string) $this->request->getBody();
|
|
$expected = hash_hmac('sha256', $rawBody, $secret);
|
|
if (! hash_equals($expected, $signature)) {
|
|
return $this->response->setStatusCode(401)->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => 'Invalid signature',
|
|
]);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function readRuntimeSetting(string $name): string
|
|
{
|
|
$envValue = getenv($name);
|
|
if ($envValue !== false) {
|
|
$value = trim((string) $envValue);
|
|
if ($value !== '') {
|
|
return $value;
|
|
}
|
|
}
|
|
|
|
if ($this->db->tableExists('hospital_setting')) {
|
|
$row = $this->db->table('hospital_setting')
|
|
->select('s_value')
|
|
->where('s_name', $name)
|
|
->get(1)
|
|
->getRowArray();
|
|
$value = trim((string) ($row['s_value'] ?? ''));
|
|
if ($value !== '') {
|
|
return $value;
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
private function isStaleConsentTransition(string $currentStatus, string $incomingStatus): bool
|
|
{
|
|
if ($incomingStatus === '' || $currentStatus === '') {
|
|
return false;
|
|
}
|
|
|
|
if ($incomingStatus === 'requested' && $currentStatus !== 'requested') {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private function isStaleClaimTransition(string $currentStatus, string $incomingStatus): bool
|
|
{
|
|
if ($incomingStatus === '' || $currentStatus === '' || $incomingStatus === 'unknown') {
|
|
return false;
|
|
}
|
|
|
|
$rank = [
|
|
'draft' => 10,
|
|
'submitted' => 20,
|
|
'in_review' => 30,
|
|
'approved' => 40,
|
|
'rejected' => 40,
|
|
'failed' => 40,
|
|
'settled' => 50,
|
|
'paid' => 50,
|
|
];
|
|
|
|
$currentRank = (int) ($rank[$currentStatus] ?? 0);
|
|
$incomingRank = (int) ($rank[$incomingStatus] ?? 0);
|
|
|
|
return $currentRank > 0 && $incomingRank > 0 && $incomingRank < $currentRank;
|
|
}
|
|
|
|
// =========================================================================
|
|
// Private helpers
|
|
// =========================================================================
|
|
|
|
/**
|
|
* Persist an encrypted FHIR bundle to health_records and return the new row id.
|
|
* Returns 0 when the table is absent or on any DB error (fail-open).
|
|
*
|
|
* @param array{patient_id: int, abha_id: string, hi_type: string, entity_type: string,
|
|
* entity_id: string, fhir_bundle: string, consent_handle?: string} $data
|
|
*/
|
|
private function storeHealthRecord(array $data): int
|
|
{
|
|
try {
|
|
if (! $this->db->tableExists('health_records')) {
|
|
return 0;
|
|
}
|
|
|
|
$enc = new FhirEncryptionService();
|
|
$encPayload = '';
|
|
$rawBundle = (string) ($data['fhir_bundle'] ?? '');
|
|
if ($rawBundle !== '') {
|
|
$encPayload = $enc->encrypt($rawBundle);
|
|
}
|
|
|
|
$session = \Config\Services::session();
|
|
$now = Time::now('Asia/Kolkata')->toDateTimeString();
|
|
|
|
$this->db->table('health_records')->insert([
|
|
'patient_id' => (int) ($data['patient_id'] ?? 0) > 0 ? (int) $data['patient_id'] : null,
|
|
'abha_id' => trim((string) ($data['abha_id'] ?? '')) ?: null,
|
|
'hi_type' => (string) ($data['hi_type'] ?? 'unknown'),
|
|
'entity_type' => (string) ($data['entity_type'] ?? ''),
|
|
'entity_id' => (string) ($data['entity_id'] ?? ''),
|
|
'fhir_bundle_enc' => $encPayload !== '' ? $encPayload : null,
|
|
'push_status' => 'queued',
|
|
'push_at' => $now,
|
|
'consent_handle' => trim((string) ($data['consent_handle'] ?? '')) ?: null,
|
|
'created_by_user_id' => (int) ($session->get('user_id') ?? 0) > 0 ? (int) $session->get('user_id') : null,
|
|
'created_by_name' => trim((string) ($session->get('full_name') ?? $session->get('name') ?? '')) ?: null,
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
]);
|
|
|
|
return (int) $this->db->insertID();
|
|
} catch (\Throwable) {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update a health_record's push status and txn_id after getting a connector response.
|
|
* Also creates a matching record_links row.
|
|
*
|
|
* @param int $healthRecordId Local health_records.id
|
|
* @param string $queueId Bridge queue_id (abdm_txn_id)
|
|
* @param string|null $error Connector error message (null = success)
|
|
* @param int $bridgeRecordId Bridge record_id (from POST /v3/records/push response)
|
|
*/
|
|
private function updateHealthRecordTxn(int $healthRecordId, string $queueId, ?string $error, int $bridgeRecordId = 0): void
|
|
{
|
|
try {
|
|
$now = Time::now('Asia/Kolkata')->toDateTimeString();
|
|
|
|
if ($this->db->tableExists('health_records')) {
|
|
$hrUpdate = [
|
|
'abdm_txn_id' => $queueId !== '' ? $queueId : null,
|
|
'push_status' => $error === null ? 'queued' : 'failed',
|
|
'updated_at' => $now,
|
|
];
|
|
// Store bridge record_id when the column exists (migration adds it)
|
|
if ($bridgeRecordId > 0) {
|
|
$hrFields = $this->db->getFieldNames('health_records') ?? [];
|
|
if (in_array('bridge_record_id', $hrFields, true)) {
|
|
$hrUpdate['bridge_record_id'] = $bridgeRecordId;
|
|
}
|
|
}
|
|
$this->db->table('health_records')
|
|
->where('id', $healthRecordId)
|
|
->update($hrUpdate);
|
|
}
|
|
|
|
if ($queueId !== '' && $this->db->tableExists('record_links')) {
|
|
// Only insert if not already present
|
|
$existing = $this->db->table('record_links')
|
|
->where('health_record_id', $healthRecordId)
|
|
->where('abdm_txn_id', $queueId)
|
|
->countAllResults();
|
|
|
|
if ($existing === 0) {
|
|
$hr = $this->db->tableExists('health_records')
|
|
? ($this->db->table('health_records')->select('abha_id')->where('id', $healthRecordId)->get(1)->getRowArray() ?? [])
|
|
: [];
|
|
|
|
$this->db->table('record_links')->insert([
|
|
'health_record_id' => $healthRecordId,
|
|
'abdm_txn_id' => $queueId,
|
|
'abha_id' => trim((string) ($hr['abha_id'] ?? '')) ?: null,
|
|
'link_status' => 'pending',
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
]);
|
|
}
|
|
}
|
|
} catch (\Throwable) {
|
|
// Fail-open
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Query hospital_setting for FHIR-required hospital profile fields.
|
|
*
|
|
* @return array{name: string, hfr_id: string}
|
|
*/
|
|
private function getHospitalProfileForFhir(): array
|
|
{
|
|
try {
|
|
$rows = $this->db->table('hospital_setting')
|
|
->select('s_name, s_value')
|
|
->whereIn('s_name', ['ABDM_HMS_NAME', 'ABDM_HFR_ID', 'H_Name'])
|
|
->get()->getResultArray();
|
|
$map = array_column($rows, 's_value', 's_name');
|
|
$name = trim((string) ($map['ABDM_HMS_NAME'] ?? $map['H_Name'] ?? ''));
|
|
$hfrId = trim((string) ($map['ABDM_HFR_ID'] ?? ''));
|
|
} catch (\Throwable $e) {
|
|
$name = '';
|
|
$hfrId = '';
|
|
}
|
|
return ['name' => $name, 'hfr_id' => $hfrId];
|
|
}
|
|
|
|
/**
|
|
* Map numeric lab_type code to a human-readable title.
|
|
*/
|
|
private function mapLabTypeToTitle(int $labType): string
|
|
{
|
|
return match ($labType) {
|
|
1 => 'Haematology',
|
|
2 => 'Biochemistry',
|
|
3 => 'Serology',
|
|
4 => 'Microbiology',
|
|
5 => 'Pathology / Cytology',
|
|
6 => 'Radiology',
|
|
7 => 'Urology',
|
|
8 => 'Molecular Diagnostics',
|
|
default => '',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Returns a lazily-constructed AbdmAuditService instance.
|
|
*/
|
|
private function getAuditService(): AbdmAuditService
|
|
{
|
|
static $svc = null;
|
|
if ($svc === null) {
|
|
$svc = new AbdmAuditService();
|
|
}
|
|
return $svc;
|
|
}
|
|
|
|
/**
|
|
* Maps HMS internal ABDM hi_type strings to bridge API record_type enum values.
|
|
* Bridge accepts: prescription | lab_report | discharge_summary | wellness_record | health_document
|
|
*/
|
|
private function mapHiTypeToRecordType(string $hiType): string
|
|
{
|
|
return match (strtolower($hiType)) {
|
|
'opconsultrecord', 'prescriptionrecord' => 'prescription',
|
|
'diagnosticreportrecord' => 'lab_report',
|
|
'dischargesummaryrecord' => 'discharge_summary',
|
|
'wellnessrecord' => 'wellness_record',
|
|
default => 'health_document',
|
|
};
|
|
}
|
|
|
|
private function getActiveConsentRecord(int $patientId, string $abhaId, string $consentHandle = ''): ?array
|
|
{
|
|
if (! $this->db->tableExists('abdm_consent_records')) {
|
|
return null;
|
|
}
|
|
|
|
$now = Time::now('Asia/Kolkata')->toDateTimeString();
|
|
|
|
$builder = $this->db->table('abdm_consent_records')
|
|
->where('patient_id', $patientId)
|
|
->where('abha_id', $abhaId)
|
|
->where('status', 'GRANTED')
|
|
->where('expiry_date >=', $now);
|
|
|
|
if ($consentHandle !== '') {
|
|
$builder->where('consent_handle', $consentHandle);
|
|
}
|
|
|
|
$row = $builder->orderBy('id', 'DESC')->get(1)->getRowArray();
|
|
return ! empty($row) ? $row : null;
|
|
}
|
|
|
|
// =========================================================================
|
|
// M1 ABHA OTP Flows
|
|
// Calls EAtriaBridgeConnector synchronously — requires eatria_bridge connector.
|
|
// =========================================================================
|
|
|
|
/**
|
|
* POST /abdm/abha/aadhaar/generate-otp
|
|
* Initiates ABHA creation/linking flow using Aadhaar OTP.
|
|
* Body: { aadhaar: "123456789012" }
|
|
*/
|
|
public function abhaAadhaarGenerateOtp()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$body = $this->request->getJSON(true) ?? [];
|
|
// Accept loginId (API native) or aadhaar (HMS form shorthand)
|
|
$loginId = trim((string) ($body['loginId'] ?? $body['aadhaar'] ?? $this->request->getPost('loginId') ?? $this->request->getPost('aadhaar') ?? ''));
|
|
|
|
if ($loginId === '' || ! preg_match('/^\d{12}$/', $loginId)) {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Valid 12-digit Aadhaar number is required']);
|
|
}
|
|
|
|
try {
|
|
$result = $this->connector->abhaAadhaarGenerateOtp(['aadhaar' => $loginId]);
|
|
} catch (\Throwable $e) {
|
|
return $this->response->setStatusCode(500)->setJSON(['ok' => 0, 'error_text' => $e->getMessage()]);
|
|
}
|
|
|
|
return $this->response->setJSON($result);
|
|
}
|
|
|
|
/**
|
|
* POST /abdm/abha/aadhaar/verify-otp
|
|
* Verifies Aadhaar OTP and returns ABHA profile.
|
|
* Body: { txn_id: "...", otp: "123456" }
|
|
*/
|
|
public function abhaAadhaarVerifyOtp()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$body = $this->request->getJSON(true) ?? [];
|
|
// Accept txnId (API native) or txn_id (HMS form shorthand)
|
|
$txnId = trim((string) ($body['txnId'] ?? $body['txn_id'] ?? $this->request->getPost('txnId') ?? $this->request->getPost('txn_id') ?? ''));
|
|
$otp = trim((string) ($body['otp'] ?? $this->request->getPost('otp') ?? ''));
|
|
|
|
if ($txnId === '' || $otp === '') {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'txnId and otp are required']);
|
|
}
|
|
|
|
try {
|
|
$result = $this->connector->abhaAadhaarVerifyOtp(['txnId' => $txnId, 'otp' => $otp]);
|
|
} catch (\Throwable $e) {
|
|
return $this->response->setStatusCode(500)->setJSON(['ok' => 0, 'error_text' => $e->getMessage()]);
|
|
}
|
|
|
|
return $this->response->setJSON($result);
|
|
}
|
|
|
|
/**
|
|
* POST /abdm/abha/mobile/generate-otp
|
|
* Sends OTP to mobile for ABHA linking.
|
|
* Body: { mobile: "9876543210" }
|
|
*/
|
|
public function abhaMobileGenerateOtp()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$body = $this->request->getJSON(true) ?? [];
|
|
// Accept loginId (API native) or mobile (HMS form shorthand)
|
|
$loginId = trim((string) ($body['loginId'] ?? $body['mobile'] ?? $this->request->getPost('loginId') ?? $this->request->getPost('mobile') ?? ''));
|
|
|
|
if ($loginId === '' || ! preg_match('/^\d{10}$/', $loginId)) {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Valid 10-digit mobile number is required']);
|
|
}
|
|
|
|
try {
|
|
$result = $this->connector->abhaMobileGenerateOtp(['mobile' => $loginId]);
|
|
} catch (\Throwable $e) {
|
|
return $this->response->setStatusCode(500)->setJSON(['ok' => 0, 'error_text' => $e->getMessage()]);
|
|
}
|
|
|
|
return $this->response->setJSON($result);
|
|
}
|
|
|
|
/**
|
|
* POST /abdm/abha/mobile/verify-otp
|
|
* Verifies mobile OTP and returns ABHA profile.
|
|
* Body: { txn_id: "...", otp: "123456" }
|
|
*/
|
|
public function abhaMobileVerifyOtp()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$body = $this->request->getJSON(true) ?? [];
|
|
// Accept txnId (API native) or txn_id (HMS form shorthand)
|
|
$txnId = trim((string) ($body['txnId'] ?? $body['txn_id'] ?? $this->request->getPost('txnId') ?? $this->request->getPost('txn_id') ?? ''));
|
|
$otp = trim((string) ($body['otp'] ?? $this->request->getPost('otp') ?? ''));
|
|
|
|
if ($txnId === '' || $otp === '') {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'txnId and otp are required']);
|
|
}
|
|
|
|
try {
|
|
$result = $this->connector->abhaMobileVerifyOtp(['txnId' => $txnId, 'otp' => $otp]);
|
|
} catch (\Throwable $e) {
|
|
return $this->response->setStatusCode(500)->setJSON(['ok' => 0, 'error_text' => $e->getMessage()]);
|
|
}
|
|
|
|
return $this->response->setJSON($result);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Gateway status — hospital info + ABDM connectivity
|
|
// GET /AbdmGateway/gateway_status
|
|
// =========================================================================
|
|
|
|
public function gatewayStatus()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$result = $this->connector->gatewayStatus();
|
|
return $this->response->setJSON($result);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Bridge records — list stored records from bridge
|
|
// GET /AbdmGateway/bridge_records_list
|
|
// =========================================================================
|
|
|
|
public function bridgeRecordsList()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$filters = array_filter([
|
|
'abha_id' => $this->request->getGet('abha_id'),
|
|
'abha_address' => $this->request->getGet('abha_address'),
|
|
'status' => $this->request->getGet('status'),
|
|
'record_type' => $this->request->getGet('record_type'),
|
|
'page' => $this->request->getGet('page'),
|
|
'per_page' => $this->request->getGet('per_page'),
|
|
]);
|
|
|
|
$result = $this->connector->getRecords($filters);
|
|
return $this->response->setJSON($result);
|
|
}
|
|
|
|
// =========================================================================
|
|
// HIP-Initiated Linking — Step 1: request link token
|
|
// POST /AbdmGateway/hip_link_token
|
|
// Body: { abha_address, name, gender, year_of_birth [, abha_number] }
|
|
// =========================================================================
|
|
|
|
public function hipLinkToken()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$body = $this->request->getJSON(true) ?? [];
|
|
|
|
$required = ['abha_address', 'name', 'gender', 'year_of_birth'];
|
|
foreach ($required as $key) {
|
|
if (empty($body[$key])) {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => $key . ' is required']);
|
|
}
|
|
}
|
|
|
|
$result = $this->connector->hipLinkToken($body);
|
|
return $this->response->setJSON($result);
|
|
}
|
|
|
|
// =========================================================================
|
|
// HIP-Initiated Linking — Step 2: link care contexts
|
|
// POST /AbdmGateway/hip_link_carecontext
|
|
// Body: { abha_address, link_token_id, patient_ref, display, hi_type, care_contexts[] }
|
|
// =========================================================================
|
|
|
|
public function hipLinkCareContext()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$body = $this->request->getJSON(true) ?? [];
|
|
|
|
$required = ['abha_address', 'link_token_id', 'patient_ref', 'display', 'hi_type', 'care_contexts'];
|
|
foreach ($required as $key) {
|
|
if (empty($body[$key])) {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => $key . ' is required']);
|
|
}
|
|
}
|
|
|
|
$result = $this->connector->hipLinkCareContext($body);
|
|
return $this->response->setJSON($result);
|
|
}
|
|
|
|
// =========================================================================
|
|
// HIP-Initiated Linking — fetch all linked care contexts for a patient
|
|
// GET /AbdmGateway/hip_patient_links?abha_address=xxx&limit=20
|
|
// =========================================================================
|
|
|
|
public function hipPatientLinks()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$filters = array_filter([
|
|
'abha_address' => $this->request->getGet('abha_address'),
|
|
'limit' => $this->request->getGet('limit'),
|
|
]);
|
|
|
|
if (empty($filters['abha_address'])) {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'abha_address is required']);
|
|
}
|
|
|
|
$result = $this->connector->hipGetPatientLinks($filters);
|
|
return $this->response->setJSON($result);
|
|
}
|
|
|
|
// =========================================================================
|
|
// HIP-Initiated Linking — notify ABDM of care-context update
|
|
// POST /AbdmGateway/hip_link_notify
|
|
// Body: { abha_address, care_context_reference, hi_type, date_of_record }
|
|
// =========================================================================
|
|
|
|
public function hipLinkNotify()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$body = $this->request->getJSON(true) ?? [];
|
|
|
|
$required = ['abha_address', 'care_context_reference', 'hi_type', 'date_of_record'];
|
|
foreach ($required as $key) {
|
|
if (empty($body[$key])) {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => $key . ' is required']);
|
|
}
|
|
}
|
|
|
|
$result = $this->connector->hipLinkNotify($body);
|
|
return $this->response->setJSON($result);
|
|
}
|
|
|
|
// =========================================================================
|
|
// HIP-Initiated Linking — send ABDM deep-link SMS to patient
|
|
// POST /AbdmGateway/hip_sms_notify
|
|
// Body: { phone_number [, hip_name] }
|
|
// =========================================================================
|
|
|
|
public function hipSmsNotify()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
$body = $this->request->getJSON(true) ?? [];
|
|
|
|
if (empty($body['phone_number'])) {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'phone_number is required']);
|
|
}
|
|
|
|
$result = $this->connector->hipSmsNotify($body);
|
|
return $this->response->setJSON($result);
|
|
}
|
|
}
|
|
|