ABDM M2: records push, discover/fetch, non-ABHA local storage, consent table compat
- EAtriaBridgeConnector: fix OPConsultRecord hi_type normalization (was wrongly
mapping to PrescriptionRecord); allow push with either abha_address or abha_id;
add careContextId alias for care_context_reference in pushRecord payload
- AbdmGateway: migrate OPD share to records/push semantics with unique
care_context_reference, ABHA id/address resolution, bridge_record_id capture,
and 409 DUPLICATE_RECORD non-fatal handling
- AbdmGateway: add non-ABHA local-only storage path for OPD/IPD/Lab share
(push_status=local_only, no bridge call, audit logged)
- AbdmGateway: add POST /records/discover endpoint with ABHA-first + fallback
matching by mobile, hospital ID, patient UHID+birthYear (PHR-compatible)
- AbdmGateway: add GET /records/fetch/{careContextId} endpoint returning
decrypted FHIR payload from health_records
- AbdmGateway: dual-write consent artefacts to abdm_consent_records AND
consents table when both present; getActiveConsentRecord falls back to
consents table when abdm_consent_records is absent
- AbdmAuditService: write to audit_logs (generic schema) when present,
fallback to abdm_audit_logs; field-aware insert supports both schemas
- Routes: register /records/discover and /records/fetch/(:any)
- Webhook signature validator: check EKA_WEBHOOK_SECRET then HMS_WEBHOOK_SECRET
for gateway compatibility
This commit is contained in:
parent
5a1a87ebe1
commit
1790be3fca
|
|
@ -351,6 +351,25 @@ $routes->post('Storestock/store_Stock_result', 'Storestock::store_stock_result')
|
|||
$routes->match(['get', 'post'], 'Storestock/get_product_stock/(:num)', 'Storestock::get_product_stock/$1');
|
||||
$routes->get('Storestock/Report_2', 'Storestock::report_2');
|
||||
$routes->get('Storestock/main_store', 'Storestock::main_store');
|
||||
$routes->get('Storestock/Purchase', 'Storestock::Purchase');
|
||||
$routes->match(['get', 'post'], 'Storestock/PurchaseInvoice', 'Storestock::PurchaseInvoice');
|
||||
$routes->get('Storestock/PurchaseNew', 'Storestock::PurchaseNew');
|
||||
$routes->post('Storestock/CreatePurchase', 'Storestock::CreatePurchase');
|
||||
$routes->get('Storestock/PurchaseMasterEdit/(:num)', 'Storestock::PurchaseMasterEdit/$1');
|
||||
$routes->get('Storestock/PurchaseInvoiceEdit/(:num)', 'Storestock::PurchaseInvoiceEdit/$1');
|
||||
$routes->post('Storestock/UpdatePurchase', 'Storestock::UpdatePurchase');
|
||||
$routes->get('Storestock/UpdatePurchaseInvoiceStatus/(:num)/(:num)', 'Storestock::UpdatePurchaseInvoiceStatus/$1/$2');
|
||||
$routes->post('Storestock/purchase_invoice_delete/(:num)', 'Storestock::purchase_invoice_delete/$1');
|
||||
$routes->get('Storestock/print_purchase/(:num)', 'Storestock::print_purchase/$1');
|
||||
$routes->get('Storestock/Purchase_return', 'Storestock::Purchase_return');
|
||||
$routes->get('Storestock/PurchaseReturnNew', 'Storestock::PurchaseReturnNew');
|
||||
$routes->match(['get', 'post'], 'Storestock/PurchaseReturnInvoice', 'Storestock::PurchaseReturnInvoice');
|
||||
$routes->get('Storestock/PurchaseReturnInvoiceEdit/(:num)', 'Storestock::PurchaseReturnInvoiceEdit/$1');
|
||||
$routes->get('Storestock/PurchaseReturn_invoice_item_list/(:num)', 'Storestock::PurchaseReturn_invoice_item_list/$1');
|
||||
$routes->post('Storestock/create_purchase_return', 'Storestock::create_purchase_return');
|
||||
$routes->post('Storestock/add_remove_item', 'Storestock::add_remove_item');
|
||||
$routes->post('Storestock/remove_item_invoice/(:num)', 'Storestock::remove_item_invoice/$1');
|
||||
$routes->get('Storestock/print_purchase_return/(:num)', 'Storestock::print_purchase_return/$1');
|
||||
|
||||
// Legacy compatibility: some IPD charge screens still refresh via old IpdNew URL.
|
||||
$routes->get('IpdNew/show_ipd_items/(:num)', 'Billing\\Ipd::panelTab/$1/ipd-charges');
|
||||
|
|
@ -604,6 +623,10 @@ $routes->post('AbdmGateway/consent_request', 'AbdmGateway::consentRequest', ['fi
|
|||
$routes->post('AbdmGateway/consent_callback', 'AbdmGateway::consentCallback');
|
||||
$routes->post('AbdmGateway/consent_revoked_callback', 'AbdmGateway::consentRevokedCallback');
|
||||
$routes->post('AbdmGateway/record_linked_callback', 'AbdmGateway::recordLinkedCallback');
|
||||
// ABDM M2 compatibility endpoints (Gateway -> HMS)
|
||||
$routes->post('records/discover', 'AbdmGateway::recordsDiscover');
|
||||
$routes->get('records/fetch/(:any)', 'AbdmGateway::recordsFetch/$1');
|
||||
$routes->post('records/fetch/(:any)', 'AbdmGateway::recordsFetch/$1');
|
||||
$routes->post('AbdmGateway/share_prescription_bundle', 'AbdmGateway::sharePrescriptionBundle', ['filter' => $abdmPermFilter]);
|
||||
// SNOMED Coding Panel
|
||||
$routes->get('AbdmCodingPanel', 'AbdmCodingPanel::index', ['filter' => $abdmPermFilter]);
|
||||
|
|
@ -778,7 +801,9 @@ $routes->get('Opd_prescription/debug_investigation_fields', 'Opd_prescription::d
|
|||
$routes->get('Opd_prescription/investigation_list/(:num)', 'Opd_prescription::investigation_list/$1');
|
||||
$routes->get('Opd_prescription/investigation_list/(:num)/(:num)', 'Opd_prescription::investigation_list/$1/$2');
|
||||
$routes->post('Opd_prescription/investigation_add', 'Opd_prescription::investigation_add');
|
||||
$routes->post('Opd_prescription/investigation_add_bulk', 'Opd_prescription::investigation_add_bulk');
|
||||
$routes->post('Opd_prescription/investigation_remove/(:num)', 'Opd_prescription::investigation_remove/$1');
|
||||
$routes->post('Opd_prescription/investigation_remove_bulk', 'Opd_prescription::investigation_remove_bulk');
|
||||
$routes->get('Opd_prescription/opd_invest_master', 'Opd_prescription::opd_invest_master', ['filter' => 'permission:doctor_work.medicine.manage,doctor_work.access']);
|
||||
$routes->get('Opd_prescription/opd_invest_master_specs', 'Opd_prescription::opd_invest_master_specs');
|
||||
$routes->get('Opd_prescription/opd_invest_master_data', 'Opd_prescription::opd_invest_master_data');
|
||||
|
|
|
|||
|
|
@ -116,8 +116,10 @@ class AbdmGateway extends BaseController
|
|||
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']);
|
||||
$hasAbdmConsentTable = $this->db->tableExists('abdm_consent_records');
|
||||
$hasConsentAliasTable = $this->db->tableExists('consents');
|
||||
if (! $hasAbdmConsentTable && ! $hasConsentAliasTable) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'consent storage table not found']);
|
||||
}
|
||||
|
||||
$consentHandle = 'CH-' . date('YmdHis') . '-' . strtoupper(bin2hex(random_bytes(4)));
|
||||
|
|
@ -132,18 +134,40 @@ class AbdmGateway extends BaseController
|
|||
'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,
|
||||
]);
|
||||
if ($hasAbdmConsentTable) {
|
||||
$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,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($hasConsentAliasTable) {
|
||||
$consentFields = $this->db->getFieldNames('consents') ?? [];
|
||||
$consentRow = [];
|
||||
if (in_array('patient_id', $consentFields, true)) {
|
||||
$consentRow['patient_id'] = $patientId;
|
||||
}
|
||||
if (in_array('token', $consentFields, true)) {
|
||||
$consentRow['token'] = $consentHandle;
|
||||
}
|
||||
if (in_array('expiry', $consentFields, true)) {
|
||||
$consentRow['expiry'] = $expiresAt !== '' ? $expiresAt : null;
|
||||
}
|
||||
if (in_array('scope', $consentFields, true)) {
|
||||
$consentRow['scope'] = $purposeCode !== '' ? $purposeCode : 'TREATMENT';
|
||||
}
|
||||
if (! empty($consentRow)) {
|
||||
$this->db->table('consents')->insert($consentRow);
|
||||
}
|
||||
}
|
||||
|
||||
$queueId = null;
|
||||
$gatewayConsentId = '';
|
||||
|
|
@ -152,7 +176,7 @@ class AbdmGateway extends BaseController
|
|||
$queueId = $result['queue_id'] ?? null;
|
||||
$gatewayConsentId = trim((string) ($result['gateway_consent_id'] ?? $result['consent_id'] ?? ''));
|
||||
|
||||
if ($gatewayConsentId !== '') {
|
||||
if ($gatewayConsentId !== '' && $hasAbdmConsentTable) {
|
||||
$tableFields = $this->db->getFieldNames('abdm_consent_records') ?? [];
|
||||
$updateData = [
|
||||
'updated_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
|
||||
|
|
@ -196,8 +220,10 @@ class AbdmGateway extends BaseController
|
|||
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']);
|
||||
$hasAbdmConsentTable = $this->db->tableExists('abdm_consent_records');
|
||||
$hasConsentAliasTable = $this->db->tableExists('consents');
|
||||
if (! $hasAbdmConsentTable && ! $hasConsentAliasTable) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'consent storage table not found']);
|
||||
}
|
||||
|
||||
$handle = trim((string) $payload['consent_handle']);
|
||||
|
|
@ -206,11 +232,30 @@ class AbdmGateway extends BaseController
|
|||
$incomingConsentId = trim((string) ($payload['consent_id'] ?? $payload['gateway_consent_id'] ?? ''));
|
||||
$now = Time::now('Asia/Kolkata')->toDateTimeString();
|
||||
|
||||
$existing = $this->db->table('abdm_consent_records')
|
||||
->where('consent_handle', $handle)
|
||||
->orderBy('id', 'DESC')
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
$existing = [];
|
||||
if ($hasAbdmConsentTable) {
|
||||
$existing = $this->db->table('abdm_consent_records')
|
||||
->where('consent_handle', $handle)
|
||||
->orderBy('id', 'DESC')
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
}
|
||||
|
||||
if (empty($existing) && $hasConsentAliasTable) {
|
||||
$consentAliasRow = $this->db->table('consents')
|
||||
->where('token', $handle)
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
if (! empty($consentAliasRow)) {
|
||||
$existing = [
|
||||
'patient_id' => (int) ($consentAliasRow['patient_id'] ?? 0),
|
||||
'consent_status' => 'approved',
|
||||
'expires_at' => (string) ($consentAliasRow['expiry'] ?? ''),
|
||||
'raw_payload_json' => '',
|
||||
'granted_at' => null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($existing)) {
|
||||
return $this->response->setStatusCode(404)->setJSON([
|
||||
|
|
@ -265,9 +310,25 @@ class AbdmGateway extends BaseController
|
|||
}
|
||||
}
|
||||
|
||||
$this->db->table('abdm_consent_records')
|
||||
->where('consent_handle', $handle)
|
||||
->update($update);
|
||||
if ($hasAbdmConsentTable) {
|
||||
$this->db->table('abdm_consent_records')
|
||||
->where('consent_handle', $handle)
|
||||
->update($update);
|
||||
}
|
||||
|
||||
if ($hasConsentAliasTable) {
|
||||
$consentFields = $this->db->getFieldNames('consents') ?? [];
|
||||
$consentUpdate = [];
|
||||
if (in_array('expiry', $consentFields, true)) {
|
||||
$consentUpdate['expiry'] = $expiresAt !== '' ? $expiresAt : null;
|
||||
}
|
||||
if (in_array('scope', $consentFields, true) && $incomingStatus !== '') {
|
||||
$consentUpdate['scope'] = strtoupper($incomingStatus);
|
||||
}
|
||||
if (! empty($consentUpdate)) {
|
||||
$this->db->table('consents')->where('token', $handle)->update($consentUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
|
|
@ -588,22 +649,31 @@ class AbdmGateway extends BaseController
|
|||
$sessionId = (int) $this->request->getPost('opd_session_id');
|
||||
$patientId = (int) $this->request->getPost('patient_id');
|
||||
$abhaId = trim((string) $this->request->getPost('abha_id'));
|
||||
$abhaAddressPost = trim((string) $this->request->getPost('abha_address'));
|
||||
$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 ($abhaId === '' && $abhaAddressPost !== '') {
|
||||
$abhaId = $abhaAddressPost;
|
||||
}
|
||||
|
||||
if ($opdId <= 0 || $patientId <= 0) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'opd_id and patient_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.',
|
||||
]);
|
||||
$hasAbha = $abhaId !== '' || $abhaAddressPost !== '';
|
||||
$consent = null;
|
||||
if ($hasAbha) {
|
||||
$consent = $this->getActiveConsentRecord($patientId, $abhaId !== '' ? $abhaId : $abhaAddressPost, $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')
|
||||
|
|
@ -624,6 +694,83 @@ class AbdmGateway extends BaseController
|
|||
$bundle = ['raw' => $bundleJson];
|
||||
}
|
||||
|
||||
// Resolve ABHA identifiers from request + patient profile.
|
||||
$abhaAddress = str_contains($abhaId, '@') ? $abhaId : '';
|
||||
$abhaNumber = '';
|
||||
if ($abhaAddressPost !== '' && str_contains($abhaAddressPost, '@')) {
|
||||
$abhaAddress = $abhaAddressPost;
|
||||
}
|
||||
if ($abhaAddress === '') {
|
||||
$digits = preg_replace('/\D/', '', $abhaId);
|
||||
if (strlen((string) $digits) === 14) {
|
||||
$abhaNumber = (string) $digits;
|
||||
}
|
||||
}
|
||||
|
||||
$patientName = '';
|
||||
if ($this->db->tableExists('patient_master')) {
|
||||
$pmFields = $this->db->getFieldNames('patient_master') ?? [];
|
||||
$pmSelect = ['id'];
|
||||
foreach (['p_fname', 'p_lname', 'abha_address', 'abha_id', 'abha_no', 'abha'] as $f) {
|
||||
if (in_array($f, $pmFields, true)) {
|
||||
$pmSelect[] = $f;
|
||||
}
|
||||
}
|
||||
|
||||
$patientRow = $this->db->table('patient_master')
|
||||
->select(implode(',', array_unique($pmSelect)))
|
||||
->where('id', $patientId)
|
||||
->get(1)
|
||||
->getRowArray() ?? [];
|
||||
|
||||
$patientName = trim(
|
||||
trim((string) ($patientRow['p_fname'] ?? '')) . ' ' .
|
||||
trim((string) ($patientRow['p_lname'] ?? ''))
|
||||
);
|
||||
|
||||
if ($abhaAddress === '') {
|
||||
foreach (['abha_address', 'abha', 'abha_id', 'abha_no'] as $field) {
|
||||
$value = trim((string) ($patientRow[$field] ?? ''));
|
||||
if ($value !== '' && str_contains($value, '@')) {
|
||||
$abhaAddress = $value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($abhaNumber === '') {
|
||||
foreach (['abha_id', 'abha_no', 'abha'] as $field) {
|
||||
$value = trim((string) ($patientRow[$field] ?? ''));
|
||||
$digits = preg_replace('/\D/', '', $value);
|
||||
if (strlen((string) $digits) === 14) {
|
||||
$abhaNumber = (string) $digits;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($abhaAddress === '' && $abhaNumber === '') {
|
||||
return $this->response->setJSON([
|
||||
'ok' => 0,
|
||||
'error_text' => 'ABHA address or ABHA number is required for record push.',
|
||||
]);
|
||||
}
|
||||
|
||||
$bundleType = trim((string) ($bundleRow['bundle_type'] ?? 'OPConsultRecord'));
|
||||
$hiType = match ($bundleType) {
|
||||
'MedicationRequestBundle', 'Prescription', 'PrescriptionRecord' => 'PrescriptionRecord',
|
||||
default => 'OPConsultRecord',
|
||||
};
|
||||
$sessionForRef = $sessionId > 0 ? $sessionId : (int) ($bundleRow['opd_session_id'] ?? 0);
|
||||
$visitDateRaw = trim((string) ($bundleRow['created_at'] ?? ''));
|
||||
$visitDate = $visitDateRaw !== '' ? date('Y-m-d', strtotime($visitDateRaw)) : date('Y-m-d');
|
||||
$careContextRef = trim((string) ($this->request->getPost('careContextId') ?? $this->request->getPost('care_context_reference') ?? ''));
|
||||
if ($careContextRef === '') {
|
||||
$careContextRef = 'OPD-' . $opdId . '-S' . ($sessionForRef > 0 ? $sessionForRef : 0) . '-' . $visitDate;
|
||||
}
|
||||
$careContextDisplay = 'OPD Visit - ' . $visitDate;
|
||||
|
||||
$payload = [
|
||||
'opd_id' => $opdId,
|
||||
'opd_session_id' => (int) ($bundleRow['opd_session_id'] ?? 0),
|
||||
|
|
@ -638,26 +785,98 @@ class AbdmGateway extends BaseController
|
|||
// Store FHIR payload in health_records before pushing
|
||||
$healthRecordId = $this->storeHealthRecord([
|
||||
'patient_id' => $patientId,
|
||||
'abha_id' => $abhaId,
|
||||
'abha_id' => $hasAbha ? $abhaId : '',
|
||||
'hi_type' => 'OPConsultRecord',
|
||||
'entity_type' => 'opd',
|
||||
'entity_id' => (string) $opdId,
|
||||
'fhir_bundle' => $bundleJson,
|
||||
'care_context_reference' => $careContextRef,
|
||||
'consent_handle' => (string) ($consent['consent_handle'] ?? ''),
|
||||
]);
|
||||
|
||||
if (! $hasAbha) {
|
||||
if ($healthRecordId > 0 && $this->db->tableExists('health_records')) {
|
||||
$this->db->table('health_records')
|
||||
->where('id', $healthRecordId)
|
||||
->update([
|
||||
'push_status' => 'local_only',
|
||||
'updated_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
|
||||
]);
|
||||
}
|
||||
|
||||
$this->getAuditService()->log([
|
||||
'action' => 'store_local_record',
|
||||
'entity_type' => 'opd',
|
||||
'entity_id' => (string) $opdId,
|
||||
'patient_id' => $patientId,
|
||||
'request' => ['opd_id' => $opdId, 'care_context_reference' => $careContextRef],
|
||||
'response' => ['health_record_id' => $healthRecordId],
|
||||
'outcome' => 'success',
|
||||
]);
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'status' => 'local_stored',
|
||||
'queue_id' => null,
|
||||
'bridge_record_id' => null,
|
||||
'care_context_reference' => $careContextRef,
|
||||
'message' => 'ABHA not available. Record stored locally for later discovery.',
|
||||
]);
|
||||
}
|
||||
|
||||
$queueId = null;
|
||||
$bridgeRecordId = 0;
|
||||
$status = 'queued';
|
||||
$connectorError = null;
|
||||
try {
|
||||
$result = $this->connector->sharePrescriptionBundle($payload, (string) ($bundleRow['id'] ?? ''));
|
||||
$queueId = $result['queue_id'] ?? null;
|
||||
$result = $this->connector->pushRecord([
|
||||
'patient_id' => (string) $patientId,
|
||||
'patient_name' => $patientName !== '' ? $patientName : ('PATIENT-' . $patientId),
|
||||
'abha_id' => $abhaNumber,
|
||||
'abha_address' => $abhaAddress,
|
||||
'hi_type' => $hiType,
|
||||
'record_type' => $hiType,
|
||||
'visit_date' => $visitDate,
|
||||
'care_context_reference' => $careContextRef,
|
||||
'care_context_display' => $careContextDisplay,
|
||||
'notes' => $careContextDisplay,
|
||||
'queue_id' => $careContextRef,
|
||||
'record_data' => $bundle,
|
||||
]);
|
||||
|
||||
$queueId = (string) ($result['queue_id'] ?? '');
|
||||
$bridgeRecordId = (int) ($result['id'] ?? 0);
|
||||
|
||||
$httpCode = (int) ($result['http_code'] ?? 0);
|
||||
$resultOk = (int) ($result['ok'] ?? 0) === 1;
|
||||
$errorCode = strtoupper(trim((string) ($result['error_code'] ?? '')));
|
||||
$isDuplicate = $httpCode === 409 || $errorCode === 'DUPLICATE_RECORD';
|
||||
|
||||
if ($isDuplicate) {
|
||||
$bridgeRecordId = (int) ($result['existing_record_id'] ?? $bridgeRecordId);
|
||||
$resultOk = true;
|
||||
$status = 'duplicate';
|
||||
}
|
||||
|
||||
if (! $resultOk) {
|
||||
$connectorError = trim((string) (
|
||||
$result['message']
|
||||
?? $result['error_text']
|
||||
?? ($result['error_code'] ?? '')
|
||||
));
|
||||
if ($connectorError === '') {
|
||||
$connectorError = 'Bridge push failed';
|
||||
}
|
||||
$status = 'failed';
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$connectorError = $e->getMessage();
|
||||
$status = 'failed';
|
||||
}
|
||||
|
||||
// Update health_record with txn_id and create record_link
|
||||
if ($healthRecordId > 0) {
|
||||
$this->updateHealthRecordTxn($healthRecordId, (string) ($queueId ?? ''), $connectorError);
|
||||
$this->updateHealthRecordTxn($healthRecordId, (string) ($queueId ?? ''), $connectorError, $bridgeRecordId);
|
||||
}
|
||||
|
||||
$this->getAuditService()->log([
|
||||
|
|
@ -673,11 +892,13 @@ class AbdmGateway extends BaseController
|
|||
]);
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'ok' => $connectorError === null ? 1 : 0,
|
||||
'queue_id' => $queueId,
|
||||
'bridge_record_id' => $bridgeRecordId > 0 ? $bridgeRecordId : null,
|
||||
'consent_handle' => (string) ($consent['consent_handle'] ?? ''),
|
||||
'gateway_consent_id' => $this->resolveConsentExternalId($consent) !== '' ? $this->resolveConsentExternalId($consent) : null,
|
||||
'status' => 'queued',
|
||||
'status' => $status,
|
||||
'error' => $connectorError,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -692,16 +913,20 @@ class AbdmGateway extends BaseController
|
|||
$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']);
|
||||
if ($ipdId <= 0 || $patientId <= 0) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'ipd_id and patient_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.',
|
||||
]);
|
||||
$hasAbha = $abhaId !== '';
|
||||
$consent = null;
|
||||
if ($hasAbha) {
|
||||
$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 ──────────────────────────────────────────────────────
|
||||
|
|
@ -810,9 +1035,39 @@ class AbdmGateway extends BaseController
|
|||
'entity_type' => 'ipd',
|
||||
'entity_id' => (string) $ipdId,
|
||||
'fhir_bundle' => $bundleJson,
|
||||
'care_context_reference' => $ccRef,
|
||||
'consent_handle' => (string) ($consent['consent_handle'] ?? ''),
|
||||
]);
|
||||
|
||||
if (! $hasAbha) {
|
||||
if ($healthRecordId > 0 && $this->db->tableExists('health_records')) {
|
||||
$this->db->table('health_records')
|
||||
->where('id', $healthRecordId)
|
||||
->update([
|
||||
'push_status' => 'local_only',
|
||||
'updated_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
|
||||
]);
|
||||
}
|
||||
|
||||
$this->getAuditService()->log([
|
||||
'action' => 'store_local_record',
|
||||
'entity_type' => 'ipd',
|
||||
'entity_id' => (string) $ipdId,
|
||||
'patient_id' => $patientId,
|
||||
'request' => ['ipd_id' => $ipdId, 'care_context_reference' => $ccRef],
|
||||
'response' => ['health_record_id' => $healthRecordId],
|
||||
'outcome' => 'success',
|
||||
]);
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'status' => 'local_stored',
|
||||
'queue_id' => null,
|
||||
'consent_handle' => null,
|
||||
'message' => 'ABHA not available. Record stored locally for later discovery.',
|
||||
]);
|
||||
}
|
||||
|
||||
// ── Push via pushRecord() (POST /v3/records/push) ─────────────────────
|
||||
$queueId = null;
|
||||
$bridgeRecordId = 0;
|
||||
|
|
@ -874,8 +1129,8 @@ class AbdmGateway extends BaseController
|
|||
$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']);
|
||||
if ($labReqId <= 0 || $patientId <= 0) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error' => 'lab_req_id and patient_id are required']);
|
||||
}
|
||||
|
||||
// ── Load lab request ──────────────────────────────────────────────────
|
||||
|
|
@ -889,7 +1144,17 @@ class AbdmGateway extends BaseController
|
|||
return $this->response->setJSON(['ok' => 0, 'error' => 'Lab request not found']);
|
||||
}
|
||||
|
||||
$consentRecord = $this->getActiveConsentRecord($patientId, $abhaId, $consentHandle);
|
||||
$hasAbha = $abhaId !== '';
|
||||
$consentRecord = null;
|
||||
if ($hasAbha) {
|
||||
$consentRecord = $this->getActiveConsentRecord($patientId, $abhaId, $consentHandle);
|
||||
if ($consentRecord === null) {
|
||||
return $this->response->setJSON([
|
||||
'ok' => 0,
|
||||
'error' => 'No active consent found. Share blocked due to expiry/not-approved consent.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
$effectiveConsent = (string) ($consentRecord['consent_handle'] ?? $consentHandle);
|
||||
|
||||
// ── Load patient ──────────────────────────────────────────────────────
|
||||
|
|
@ -1003,9 +1268,39 @@ class AbdmGateway extends BaseController
|
|||
'entity_type' => 'lab',
|
||||
'entity_id' => (string) $labReqId,
|
||||
'fhir_bundle' => $bundleJson,
|
||||
'care_context_reference' => $ccRef,
|
||||
'consent_handle' => $effectiveConsent,
|
||||
]);
|
||||
|
||||
if (! $hasAbha) {
|
||||
if ($healthRecordId > 0 && $this->db->tableExists('health_records')) {
|
||||
$this->db->table('health_records')
|
||||
->where('id', $healthRecordId)
|
||||
->update([
|
||||
'push_status' => 'local_only',
|
||||
'updated_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
|
||||
]);
|
||||
}
|
||||
|
||||
$this->getAuditService()->log([
|
||||
'action' => 'store_local_record',
|
||||
'entity_type' => 'lab',
|
||||
'entity_id' => (string) $labReqId,
|
||||
'patient_id' => $patientId,
|
||||
'request' => ['lab_req_id' => $labReqId, 'care_context_reference' => $ccRef],
|
||||
'response' => ['health_record_id' => $healthRecordId],
|
||||
'outcome' => 'success',
|
||||
]);
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'status' => 'local_stored',
|
||||
'queue_id' => null,
|
||||
'consent_handle' => null,
|
||||
'message' => 'ABHA not available. Record stored locally for later discovery.',
|
||||
]);
|
||||
}
|
||||
|
||||
// ── Push via pushRecord() (POST /v3/records/push) ─────────────────────
|
||||
$queueId = null;
|
||||
$bridgeRecordId = 0;
|
||||
|
|
@ -1655,15 +1950,33 @@ class AbdmGateway extends BaseController
|
|||
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']);
|
||||
$hasAbdmConsentTable = $this->db->tableExists('abdm_consent_records');
|
||||
$hasConsentAliasTable = $this->db->tableExists('consents');
|
||||
if (! $hasAbdmConsentTable && ! $hasConsentAliasTable) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'consent storage table not found']);
|
||||
}
|
||||
|
||||
$existing = $this->db->table('abdm_consent_records')
|
||||
->where('consent_handle', $consentHandle)
|
||||
->orderBy('id', 'DESC')
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
$existing = [];
|
||||
if ($hasAbdmConsentTable) {
|
||||
$existing = $this->db->table('abdm_consent_records')
|
||||
->where('consent_handle', $consentHandle)
|
||||
->orderBy('id', 'DESC')
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
}
|
||||
|
||||
if (empty($existing) && $hasConsentAliasTable) {
|
||||
$consentAliasRow = $this->db->table('consents')
|
||||
->where('token', $consentHandle)
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
if (! empty($consentAliasRow)) {
|
||||
$existing = [
|
||||
'patient_id' => (int) ($consentAliasRow['patient_id'] ?? 0),
|
||||
'consent_status' => 'approved',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($existing)) {
|
||||
return $this->response->setStatusCode(404)->setJSON(['ok' => 0, 'error_text' => 'consent_handle not found']);
|
||||
|
|
@ -1674,13 +1987,26 @@ class AbdmGateway extends BaseController
|
|||
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,
|
||||
]);
|
||||
if ($hasAbdmConsentTable) {
|
||||
$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,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($hasConsentAliasTable) {
|
||||
$consentFields = $this->db->getFieldNames('consents') ?? [];
|
||||
$consentUpdate = [];
|
||||
if (in_array('scope', $consentFields, true)) {
|
||||
$consentUpdate['scope'] = 'REVOKED';
|
||||
}
|
||||
if (! empty($consentUpdate)) {
|
||||
$this->db->table('consents')->where('token', $consentHandle)->update($consentUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
$this->getAuditService()->log([
|
||||
'action' => 'consent_revoke',
|
||||
|
|
@ -1851,6 +2177,245 @@ class AbdmGateway extends BaseController
|
|||
return $this->response->setJSON(['ok' => 1, 'records' => $rows, 'total' => count($rows)]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// M2 Discovery endpoint for gateway callbacks.
|
||||
// POST /records/discover
|
||||
// =========================================================================
|
||||
|
||||
public function recordsDiscover()
|
||||
{
|
||||
$signatureFailure = $this->validateWebhookSignature();
|
||||
if ($signatureFailure !== null) {
|
||||
return $signatureFailure;
|
||||
}
|
||||
|
||||
if (! $this->db->tableExists('health_records')) {
|
||||
return $this->response->setJSON(['ok' => 1, 'careContexts' => [], 'count' => 0]);
|
||||
}
|
||||
|
||||
$payload = $this->request->getJSON(true);
|
||||
if (! is_array($payload)) {
|
||||
$payload = [];
|
||||
}
|
||||
|
||||
$abhaAddress = trim((string) (
|
||||
$payload['abha_address']
|
||||
?? $payload['abhaAddress']
|
||||
?? $payload['patient']['abha_address']
|
||||
?? $payload['patient']['id']
|
||||
?? ''
|
||||
));
|
||||
$mobile = preg_replace('/\D/', '', (string) (
|
||||
$payload['mobile']
|
||||
?? $payload['phone']
|
||||
?? $payload['patient']['mobile']
|
||||
?? ''
|
||||
));
|
||||
$patientIdentifier = trim((string) (
|
||||
$payload['patient_id']
|
||||
?? $payload['patientId']
|
||||
?? $payload['uhid']
|
||||
?? $payload['UHID']
|
||||
?? $payload['patient']['patient_id']
|
||||
?? $payload['patient']['patientId']
|
||||
?? $payload['patient']['uhid']
|
||||
?? ''
|
||||
));
|
||||
$birthYear = (int) (
|
||||
$payload['birth_year']
|
||||
?? $payload['birthYear']
|
||||
?? $payload['year_of_birth']
|
||||
?? $payload['patient']['birth_year']
|
||||
?? $payload['patient']['birthYear']
|
||||
?? $payload['patient']['year_of_birth']
|
||||
?? 0
|
||||
);
|
||||
$hospitalId = trim((string) (
|
||||
$payload['hospital_id']
|
||||
?? $payload['patient_id']
|
||||
?? $payload['patientRef']
|
||||
?? $payload['patient_ref']
|
||||
?? ''
|
||||
));
|
||||
|
||||
$patientIds = [];
|
||||
if ($this->db->tableExists('patient_master')) {
|
||||
$pmFields = $this->db->getFieldNames('patient_master') ?? [];
|
||||
$abhaCols = array_values(array_filter(['abha_address', 'abha_id', 'abha_no', 'abha'], fn ($c) => in_array($c, $pmFields, true)));
|
||||
|
||||
if ($abhaAddress !== '' && ! empty($abhaCols)) {
|
||||
$b = $this->db->table('patient_master')->select('id');
|
||||
$b->groupStart();
|
||||
foreach ($abhaCols as $col) {
|
||||
$b->orWhere($col, $abhaAddress);
|
||||
}
|
||||
$b->groupEnd();
|
||||
$rows = $b->get()->getResultArray();
|
||||
foreach ($rows as $r) {
|
||||
$patientIds[] = (int) ($r['id'] ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
if ($mobile !== '' && in_array('mphone1', $pmFields, true)) {
|
||||
$rows = $this->db->table('patient_master')->select('id')->where('mphone1', $mobile)->get()->getResultArray();
|
||||
foreach ($rows as $r) {
|
||||
$patientIds[] = (int) ($r['id'] ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
// PHR non-ABHA lookup: patient ID/UHID with optional birth year.
|
||||
if ($patientIdentifier !== '') {
|
||||
$idCols = array_values(array_filter(['p_code', 'uhid', 'uhid_no', 'patient_id', 'patient_code'], fn ($c) => in_array($c, $pmFields, true)));
|
||||
$b = $this->db->table('patient_master')->select('id');
|
||||
$b->groupStart();
|
||||
foreach ($idCols as $col) {
|
||||
$b->orWhere($col, $patientIdentifier);
|
||||
}
|
||||
if (ctype_digit($patientIdentifier)) {
|
||||
$b->orWhere('id', (int) $patientIdentifier);
|
||||
}
|
||||
$b->groupEnd();
|
||||
|
||||
if ($birthYear >= 1900 && $birthYear <= (int) date('Y') && in_array('dob', $pmFields, true)) {
|
||||
$b->where('YEAR(dob)', $birthYear, false);
|
||||
}
|
||||
|
||||
$rows = $b->get()->getResultArray();
|
||||
foreach ($rows as $r) {
|
||||
$patientIds[] = (int) ($r['id'] ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
if ($hospitalId !== '' && in_array('p_code', $pmFields, true)) {
|
||||
$rows = $this->db->table('patient_master')->select('id')->where('p_code', $hospitalId)->get()->getResultArray();
|
||||
foreach ($rows as $r) {
|
||||
$patientIds[] = (int) ($r['id'] ?? 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$patientIds = array_values(array_unique(array_filter($patientIds, fn ($v) => $v > 0)));
|
||||
|
||||
$hrBuilder = $this->db->table('health_records')
|
||||
->select('id, patient_id, abha_id, hi_type, care_context_reference, created_at, updated_at')
|
||||
->orderBy('id', 'DESC')
|
||||
->limit(200);
|
||||
|
||||
if ($abhaAddress !== '') {
|
||||
$hrBuilder->groupStart()
|
||||
->where('abha_id', $abhaAddress)
|
||||
->orWhere('abha_id', preg_replace('/\D/', '', $abhaAddress))
|
||||
->groupEnd();
|
||||
} elseif (! empty($patientIds)) {
|
||||
$hrBuilder->whereIn('patient_id', $patientIds);
|
||||
} else {
|
||||
return $this->response->setJSON(['ok' => 1, 'careContexts' => [], 'count' => 0]);
|
||||
}
|
||||
|
||||
$rows = $hrBuilder->get()->getResultArray();
|
||||
$careContexts = [];
|
||||
foreach ($rows as $row) {
|
||||
$careContextId = trim((string) ($row['care_context_reference'] ?? ''));
|
||||
if ($careContextId === '') {
|
||||
$careContextId = 'HR-' . (int) ($row['id'] ?? 0);
|
||||
}
|
||||
$careContexts[] = [
|
||||
'careContextId' => $careContextId,
|
||||
'display' => (string) (($row['hi_type'] ?? 'HealthDocumentRecord') . ' - ' . ($row['created_at'] ?? $row['updated_at'] ?? '')),
|
||||
'record_type' => (string) ($row['hi_type'] ?? ''),
|
||||
'patient_id' => (int) ($row['patient_id'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
$this->getAuditService()->log([
|
||||
'action' => 'discovery_records',
|
||||
'entity_type' => 'health_record',
|
||||
'request' => $payload,
|
||||
'response' => ['count' => count($careContexts)],
|
||||
'outcome' => 'success',
|
||||
]);
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'careContexts' => $careContexts,
|
||||
'count' => count($careContexts),
|
||||
]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// M2/M3 Fetch endpoint for care-context specific FHIR payload.
|
||||
// GET /records/fetch/{careContextId}
|
||||
// =========================================================================
|
||||
|
||||
public function recordsFetch(string $careContextId)
|
||||
{
|
||||
$signatureFailure = $this->validateWebhookSignature();
|
||||
if ($signatureFailure !== null) {
|
||||
return $signatureFailure;
|
||||
}
|
||||
|
||||
if (! $this->db->tableExists('health_records')) {
|
||||
return $this->response->setStatusCode(404)->setJSON(['ok' => 0, 'error_text' => 'health_records table not found']);
|
||||
}
|
||||
|
||||
$careContextId = trim($careContextId);
|
||||
if ($careContextId === '') {
|
||||
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'careContextId is required']);
|
||||
}
|
||||
|
||||
$builder = $this->db->table('health_records')
|
||||
->select('id, patient_id, abha_id, hi_type, care_context_reference, record_data, fhir_bundle_enc, created_at, updated_at')
|
||||
->limit(1);
|
||||
|
||||
if (preg_match('/^HR\-(\d+)$/i', $careContextId, $m) === 1) {
|
||||
$builder->where('id', (int) ($m[1] ?? 0));
|
||||
} else {
|
||||
$builder->where('care_context_reference', $careContextId);
|
||||
}
|
||||
|
||||
$row = $builder->get()->getRowArray();
|
||||
if (empty($row)) {
|
||||
return $this->response->setStatusCode(404)->setJSON(['ok' => 0, 'error_text' => 'Record not found for careContextId']);
|
||||
}
|
||||
|
||||
$payload = [];
|
||||
$plainPayload = trim((string) ($row['record_data'] ?? ''));
|
||||
if ($plainPayload !== '') {
|
||||
$payload = json_decode($plainPayload, true) ?? [];
|
||||
} else {
|
||||
$encPayload = trim((string) ($row['fhir_bundle_enc'] ?? ''));
|
||||
if ($encPayload !== '') {
|
||||
try {
|
||||
$enc = new FhirEncryptionService();
|
||||
$decrypted = $enc->decrypt($encPayload);
|
||||
$payload = json_decode($decrypted, true) ?? [];
|
||||
} catch (\Throwable $e) {
|
||||
return $this->response->setStatusCode(500)->setJSON(['ok' => 0, 'error_text' => 'Unable to decode stored payload']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->getAuditService()->log([
|
||||
'action' => 'fetch_record',
|
||||
'entity_type' => 'health_record',
|
||||
'entity_id' => (string) ($row['id'] ?? ''),
|
||||
'patient_id' => (int) ($row['patient_id'] ?? 0),
|
||||
'abha_id' => (string) ($row['abha_id'] ?? ''),
|
||||
'request' => ['careContextId' => $careContextId],
|
||||
'response' => ['ok' => 1],
|
||||
'outcome' => 'success',
|
||||
]);
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'careContextId' => $careContextId,
|
||||
'record_type' => (string) ($row['hi_type'] ?? ''),
|
||||
'patient_id' => (int) ($row['patient_id'] ?? 0),
|
||||
'abha_address' => (string) ($row['abha_id'] ?? ''),
|
||||
'fhir_payload' => $payload,
|
||||
]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Bridge record status — proxy to GET /api/v3/records/{id}
|
||||
// GET /AbdmGateway/bridge_record_status/:id
|
||||
|
|
@ -2283,6 +2848,9 @@ class AbdmGateway extends BaseController
|
|||
private function validateWebhookSignature()
|
||||
{
|
||||
$secret = $this->readRuntimeSetting('EKA_WEBHOOK_SECRET');
|
||||
if ($secret === '') {
|
||||
$secret = $this->readRuntimeSetting('HMS_WEBHOOK_SECRET');
|
||||
}
|
||||
if ($secret === '') {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -2388,8 +2956,8 @@ class AbdmGateway extends BaseController
|
|||
* Stores both a plain-text copy in record_data and an encrypted copy in fhir_bundle_enc.
|
||||
* 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
|
||||
* @param array{patient_id: int, abha_id: string, hi_type: string, entity_type: string,
|
||||
* entity_id: string, fhir_bundle: string, care_context_reference?: string, consent_handle?: string} $data
|
||||
*/
|
||||
private function storeHealthRecord(array $data): int
|
||||
{
|
||||
|
|
@ -2408,7 +2976,7 @@ class AbdmGateway extends BaseController
|
|||
$session = \Config\Services::session();
|
||||
$now = Time::now('Asia/Kolkata')->toDateTimeString();
|
||||
|
||||
$this->db->table('health_records')->insert([
|
||||
$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'),
|
||||
|
|
@ -2423,7 +2991,14 @@ class AbdmGateway extends BaseController
|
|||
'created_by_name' => trim((string) ($session->get('full_name') ?? $session->get('name') ?? '')) ?: null,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
];
|
||||
|
||||
$hrFields = $this->db->getFieldNames('health_records') ?? [];
|
||||
if (in_array('care_context_reference', $hrFields, true)) {
|
||||
$insert['care_context_reference'] = trim((string) ($data['care_context_reference'] ?? '')) ?: null;
|
||||
}
|
||||
|
||||
$this->db->table('health_records')->insert($insert);
|
||||
|
||||
return (int) $this->db->insertID();
|
||||
} catch (\Throwable) {
|
||||
|
|
@ -2977,7 +3552,8 @@ class AbdmGateway extends BaseController
|
|||
private function mapHiTypeToRecordType(string $hiType): string
|
||||
{
|
||||
return match (strtolower($hiType)) {
|
||||
'opconsultrecord', 'prescriptionrecord', 'prescription' => 'PrescriptionRecord',
|
||||
'opconsultrecord', 'opconsultation' => 'OPConsultRecord',
|
||||
'prescriptionrecord', 'prescription' => 'PrescriptionRecord',
|
||||
'diagnosticreportrecord', 'lab_report', 'diagnosticreport' => 'DiagnosticReportRecord',
|
||||
'dischargesummaryrecord', 'discharge_summary', 'dischargesummary' => 'DischargeSummaryRecord',
|
||||
'wellnessrecord' => 'WellnessRecord',
|
||||
|
|
@ -2990,35 +3566,72 @@ class AbdmGateway extends BaseController
|
|||
|
||||
private function getActiveConsentRecord(int $patientId, string $abhaId, string $consentHandle = ''): ?array
|
||||
{
|
||||
if (! $this->db->tableExists('abdm_consent_records')) {
|
||||
$hasAbdmConsentTable = $this->db->tableExists('abdm_consent_records');
|
||||
$hasConsentAliasTable = $this->db->tableExists('consents');
|
||||
if (! $hasAbdmConsentTable && ! $hasConsentAliasTable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$now = Time::now('Asia/Kolkata')->toDateTimeString();
|
||||
$consentFields = $this->db->getFieldNames('abdm_consent_records') ?? [];
|
||||
if ($hasAbdmConsentTable) {
|
||||
$consentFields = $this->db->getFieldNames('abdm_consent_records') ?? [];
|
||||
|
||||
$builder = $this->db->table('abdm_consent_records')
|
||||
->where('patient_id', $patientId)
|
||||
->where('abha_id', $abhaId)
|
||||
->whereIn('consent_status', ['approved', 'granted'])
|
||||
->groupStart()
|
||||
->where('expires_at IS NULL', null, false)
|
||||
->orWhere('expires_at >=', $now)
|
||||
->groupEnd();
|
||||
$builder = $this->db->table('abdm_consent_records')
|
||||
->where('patient_id', $patientId)
|
||||
->where('abha_id', $abhaId)
|
||||
->whereIn('consent_status', ['approved', 'granted'])
|
||||
->groupStart()
|
||||
->where('expires_at IS NULL', null, false)
|
||||
->orWhere('expires_at >=', $now)
|
||||
->groupEnd();
|
||||
|
||||
if ($consentHandle !== '') {
|
||||
$builder->groupStart()->where('consent_handle', $consentHandle);
|
||||
if (in_array('consent_id', $consentFields, true)) {
|
||||
$builder->orWhere('consent_id', $consentHandle);
|
||||
if ($consentHandle !== '') {
|
||||
$builder->groupStart()->where('consent_handle', $consentHandle);
|
||||
if (in_array('consent_id', $consentFields, true)) {
|
||||
$builder->orWhere('consent_id', $consentHandle);
|
||||
}
|
||||
if (in_array('gateway_consent_id', $consentFields, true)) {
|
||||
$builder->orWhere('gateway_consent_id', $consentHandle);
|
||||
}
|
||||
$builder->groupEnd();
|
||||
}
|
||||
if (in_array('gateway_consent_id', $consentFields, true)) {
|
||||
$builder->orWhere('gateway_consent_id', $consentHandle);
|
||||
|
||||
$row = $builder->orderBy('id', 'DESC')->get(1)->getRowArray();
|
||||
if (! empty($row)) {
|
||||
return $row;
|
||||
}
|
||||
$builder->groupEnd();
|
||||
}
|
||||
|
||||
$row = $builder->orderBy('id', 'DESC')->get(1)->getRowArray();
|
||||
return ! empty($row) ? $row : null;
|
||||
if ($hasConsentAliasTable) {
|
||||
$consentFields = $this->db->getFieldNames('consents') ?? [];
|
||||
$builder = $this->db->table('consents');
|
||||
if (in_array('patient_id', $consentFields, true)) {
|
||||
$builder->where('patient_id', $patientId);
|
||||
}
|
||||
if ($consentHandle !== '' && in_array('token', $consentFields, true)) {
|
||||
$builder->where('token', $consentHandle);
|
||||
}
|
||||
if (in_array('expiry', $consentFields, true)) {
|
||||
$builder->groupStart()
|
||||
->where('expiry IS NULL', null, false)
|
||||
->orWhere('expiry >=', $now)
|
||||
->groupEnd();
|
||||
}
|
||||
|
||||
$row = $builder->orderBy(in_array('consent_id', $consentFields, true) ? 'consent_id' : 'patient_id', 'DESC')->get(1)->getRowArray();
|
||||
if (! empty($row)) {
|
||||
return [
|
||||
'patient_id' => (int) ($row['patient_id'] ?? $patientId),
|
||||
'abha_id' => $abhaId,
|
||||
'consent_handle' => (string) ($row['token'] ?? $consentHandle),
|
||||
'consent_status' => 'approved',
|
||||
'expires_at' => (string) ($row['expiry'] ?? ''),
|
||||
'purpose_code' => (string) ($row['scope'] ?? 'TREATMENT'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -356,7 +356,8 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface
|
|||
|
||||
$normalizeHiType = static function (string $value): string {
|
||||
return match ($value) {
|
||||
'OPConsultation', 'OPConsultRecord', 'Prescription', 'PrescriptionRecord' => 'PrescriptionRecord',
|
||||
'OPConsultation', 'OPConsultRecord' => 'OPConsultRecord',
|
||||
'Prescription', 'PrescriptionRecord' => 'PrescriptionRecord',
|
||||
'DiagnosticReport', 'DiagnosticReportRecord', 'Lab_Report', 'lab_report' => 'DiagnosticReportRecord',
|
||||
'DischargeSummary', 'DischargeSummaryRecord' => 'DischargeSummaryRecord',
|
||||
'ImmunizationRecord' => 'ImmunizationRecord',
|
||||
|
|
@ -387,15 +388,15 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface
|
|||
$patientName = trim((string) ($data['patient_name'] ?? ''));
|
||||
$abhaAddress = trim((string) ($data['abha_address'] ?? ''));
|
||||
$abhaId = trim((string) ($data['abha_id'] ?? ''));
|
||||
$careContextReference = trim((string) ($data['care_context_reference'] ?? ''));
|
||||
$careContextReference = trim((string) ($data['care_context_reference'] ?? $data['careContextId'] ?? ''));
|
||||
$careContextDisplay = trim((string) ($data['care_context_display'] ?? $data['notes'] ?? ''));
|
||||
$queueId = trim((string) ($data['queue_id'] ?? ''));
|
||||
|
||||
if ($patientName === '') {
|
||||
return ['ok' => 0, 'http_code' => 0, 'error_text' => 'patient_name is required'];
|
||||
}
|
||||
if ($abhaAddress === '') {
|
||||
return ['ok' => 0, 'http_code' => 0, 'error_text' => 'abha_address is required before pushing health record'];
|
||||
if ($abhaAddress === '' && $abhaId === '') {
|
||||
return ['ok' => 0, 'http_code' => 0, 'error_text' => 'Either abha_address or abha_id is required before pushing health record'];
|
||||
}
|
||||
if ($careContextReference === '') {
|
||||
return ['ok' => 0, 'http_code' => 0, 'error_text' => 'care_context_reference is required'];
|
||||
|
|
@ -418,10 +419,16 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface
|
|||
'fhir_bundle' => $data['record_data'] ?? $data['bundle'] ?? (object) [],
|
||||
'care_context_reference' => $careContextReference,
|
||||
'care_context_display' => $careContextDisplay,
|
||||
'abha_address' => $abhaAddress,
|
||||
'queue_id' => $queueId,
|
||||
];
|
||||
|
||||
if ($abhaAddress !== '') {
|
||||
$body['abha_address'] = $abhaAddress;
|
||||
}
|
||||
if ($abhaId !== '') {
|
||||
$body['abha_id'] = $abhaId;
|
||||
}
|
||||
|
||||
// Also send ABDM-style careContexts wrapper so consent linkage remains explicit.
|
||||
$body['careContexts'] = [[
|
||||
'careContextReference' => $careContextReference,
|
||||
|
|
|
|||
|
|
@ -55,10 +55,19 @@ class AbdmAuditService
|
|||
public function log(array $data): void
|
||||
{
|
||||
try {
|
||||
if (! $this->db->tableExists('abdm_audit_logs')) {
|
||||
$auditTable = null;
|
||||
if ($this->tableExistsSafe('audit_logs')) {
|
||||
$auditTable = 'audit_logs';
|
||||
} elseif ($this->tableExistsSafe('abdm_audit_logs')) {
|
||||
$auditTable = 'abdm_audit_logs';
|
||||
}
|
||||
|
||||
if ($auditTable === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$auditFields = $this->getFieldNamesSafe($auditTable);
|
||||
|
||||
$httpRequest = \Config\Services::request();
|
||||
$session = \Config\Services::session();
|
||||
|
||||
|
|
@ -79,24 +88,101 @@ class AbdmAuditService
|
|||
$entityId = (string) ($data['entity_id'] ?? '');
|
||||
$patientId = (int) ($data['patient_id'] ?? 0);
|
||||
|
||||
$this->db->table('abdm_audit_logs')->insert([
|
||||
'actor_user_id' => $actorUserId > 0 ? $actorUserId : null,
|
||||
'actor_name' => $actorName !== '' ? $actorName : null,
|
||||
'action' => (string) ($data['action'] ?? 'unknown'),
|
||||
'entity_type' => (string) ($data['entity_type'] ?? ''),
|
||||
'entity_id' => $entityId !== '' ? $entityId : null,
|
||||
'abha_id' => trim((string) ($data['abha_id'] ?? '')) ?: null,
|
||||
'patient_id' => $patientId > 0 ? $patientId : null,
|
||||
'request_json' => $requestJson,
|
||||
'response_json' => $responseJson,
|
||||
'ip_address' => $httpRequest->getIPAddress(),
|
||||
'user_agent' => mb_substr((string) $httpRequest->getUserAgent(), 0, 255),
|
||||
'outcome' => (string) ($data['outcome'] ?? 'success'),
|
||||
'error_message' => trim((string) ($data['error_message'] ?? '')) ?: null,
|
||||
'created_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
|
||||
]);
|
||||
$insert = [];
|
||||
|
||||
// Preferred generic schema: audit_logs(event_id, patient_id, action, timestamp, user_id)
|
||||
if (in_array('patient_id', $auditFields, true)) {
|
||||
$insert['patient_id'] = $patientId > 0 ? $patientId : null;
|
||||
}
|
||||
if (in_array('action', $auditFields, true)) {
|
||||
$insert['action'] = (string) ($data['action'] ?? 'unknown');
|
||||
}
|
||||
if (in_array('timestamp', $auditFields, true)) {
|
||||
$insert['timestamp'] = Time::now('Asia/Kolkata')->toDateTimeString();
|
||||
}
|
||||
if (in_array('user_id', $auditFields, true)) {
|
||||
$insert['user_id'] = $actorUserId > 0 ? $actorUserId : null;
|
||||
}
|
||||
|
||||
// Extended ABDM schema fields (abdm_audit_logs)
|
||||
if (in_array('actor_user_id', $auditFields, true)) {
|
||||
$insert['actor_user_id'] = $actorUserId > 0 ? $actorUserId : null;
|
||||
}
|
||||
if (in_array('actor_name', $auditFields, true)) {
|
||||
$insert['actor_name'] = $actorName !== '' ? $actorName : null;
|
||||
}
|
||||
if (in_array('entity_type', $auditFields, true)) {
|
||||
$insert['entity_type'] = (string) ($data['entity_type'] ?? '');
|
||||
}
|
||||
if (in_array('entity_id', $auditFields, true)) {
|
||||
$insert['entity_id'] = $entityId !== '' ? $entityId : null;
|
||||
}
|
||||
if (in_array('abha_id', $auditFields, true)) {
|
||||
$insert['abha_id'] = trim((string) ($data['abha_id'] ?? '')) ?: null;
|
||||
}
|
||||
if (in_array('request_json', $auditFields, true)) {
|
||||
$insert['request_json'] = $requestJson;
|
||||
}
|
||||
if (in_array('response_json', $auditFields, true)) {
|
||||
$insert['response_json'] = $responseJson;
|
||||
}
|
||||
if (in_array('ip_address', $auditFields, true)) {
|
||||
$insert['ip_address'] = $httpRequest->getIPAddress();
|
||||
}
|
||||
if (in_array('user_agent', $auditFields, true)) {
|
||||
$insert['user_agent'] = mb_substr((string) $httpRequest->getUserAgent(), 0, 255);
|
||||
}
|
||||
if (in_array('outcome', $auditFields, true)) {
|
||||
$insert['outcome'] = (string) ($data['outcome'] ?? 'success');
|
||||
}
|
||||
if (in_array('error_message', $auditFields, true)) {
|
||||
$insert['error_message'] = trim((string) ($data['error_message'] ?? '')) ?: null;
|
||||
}
|
||||
if (in_array('created_at', $auditFields, true)) {
|
||||
$insert['created_at'] = Time::now('Asia/Kolkata')->toDateTimeString();
|
||||
}
|
||||
|
||||
if (! empty($insert)) {
|
||||
$this->db->table($auditTable)->insert($insert);
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// Fail-open: audit failure must never break the main application flow
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lightweight table existence check that works without relying on
|
||||
* ConnectionInterface helper methods.
|
||||
*/
|
||||
private function tableExistsSafe(string $table): bool
|
||||
{
|
||||
try {
|
||||
$result = $this->db->query('SHOW TABLES LIKE ' . $this->db->escape($table))->getRowArray();
|
||||
return ! empty($result);
|
||||
} catch (\Throwable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns DB column names using SHOW COLUMNS to stay adapter-safe.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function getFieldNamesSafe(string $table): array
|
||||
{
|
||||
try {
|
||||
$rows = $this->db->query('SHOW COLUMNS FROM `' . str_replace('`', '', $table) . '`')->getResultArray();
|
||||
$fields = [];
|
||||
foreach ($rows as $row) {
|
||||
$field = (string) ($row['Field'] ?? '');
|
||||
if ($field !== '') {
|
||||
$fields[] = $field;
|
||||
}
|
||||
}
|
||||
return $fields;
|
||||
} catch (\Throwable) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user