Push M2 records to gateway for NAT deployment and cover all 8 HI types

This commit is contained in:
DevSoftTech Server 2026-08-15 03:16:32 +05:30
parent b25ab98fd7
commit 00b949ad0a
6 changed files with 502 additions and 31 deletions

View File

@ -0,0 +1,193 @@
<?php
namespace App\Commands;
use App\Libraries\Abdm\AbdmConnectorFactory;
use App\Libraries\FhirEncryptionService;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
class AbdmPushBackfill extends BaseCommand
{
protected $group = 'ABDM';
protected $name = 'abdm:push-backfill';
protected $description = 'Push stored health_records that never reached the ABDM bridge.';
protected $usage = 'abdm:push-backfill [--limit 20] [--id 5] [--dry-run]';
protected $arguments = [];
protected $options = [
'--limit' => 'Maximum records to push (default: 20).',
'--id' => 'Push a single health_records id.',
'--dry-run' => 'List eligible records without pushing.',
];
public function run(array $params)
{
$limit = (int) (CLI::getOption('limit') ?? 20);
if ($limit <= 0) {
$limit = 20;
}
$onlyId = (int) (CLI::getOption('id') ?? 0);
$dryRun = CLI::getOption('dry-run') !== null;
$db = \Config\Database::connect();
if (! $db->tableExists('health_records')) {
CLI::error('health_records table not found.');
return;
}
$builder = $db->table('health_records')
->whereNotIn('push_status', ['pushed', 'linked'])
->where('care_context_reference !=', '')
->where('abha_id !=', '')
->orderBy('id', 'ASC');
if ($onlyId > 0) {
$builder->where('id', $onlyId);
}
$rows = $builder->get($limit)->getResultArray();
CLI::write('ABDM Push Backfill' . ($dryRun ? ' (dry-run)' : ''), 'yellow');
CLI::write('Eligible: ' . count($rows));
if ($rows === [] || $dryRun) {
foreach ($rows as $row) {
CLI::write(' id=' . $row['id'] . ' ' . $row['hi_type'] . ' ' . $row['care_context_reference']);
}
return;
}
$connector = AbdmConnectorFactory::make();
$pushed = 0;
$failed = 0;
foreach ($rows as $row) {
$bundle = $this->resolveBundle($row);
if ($bundle === null) {
CLI::write(' id=' . $row['id'] . ' — no readable FHIR bundle, skipped', 'red');
$failed++;
continue;
}
$patient = $this->loadPatient($db, (int) ($row['patient_id'] ?? 0));
$abhaId = trim((string) ($row['abha_id'] ?? ''));
try {
$result = $connector->pushRecord([
'patient_id' => (string) ($row['patient_id'] ?? ''),
'patient_name' => $patient['name'] !== '' ? $patient['name'] : ('PATIENT-' . (int) $row['patient_id']),
'abha_id' => str_contains($abhaId, '@') ? '' : $abhaId,
'abha_address' => str_contains($abhaId, '@') ? $abhaId : $patient['abha_address'],
'gender' => $patient['gender'],
'year_of_birth' => $patient['year_of_birth'],
'hi_type' => (string) ($row['hi_type'] ?? ''),
'visit_date' => substr((string) ($row['created_at'] ?? date('Y-m-d')), 0, 10),
'care_context_reference' => (string) ($row['care_context_reference'] ?? ''),
'care_context_display' => (string) ($row['hi_type'] ?? '') . ' ' . substr((string) ($row['created_at'] ?? ''), 0, 10),
'record_data' => $bundle,
]);
} catch (\Throwable $e) {
CLI::write(' id=' . $row['id'] . ' — exception: ' . $e->getMessage(), 'red');
$failed++;
continue;
}
$ok = ! empty($result['ok']) && (int) $result['ok'] === 1;
$httpCode = (int) ($result['http_code'] ?? 0);
if ($ok || $httpCode === 201 || $httpCode === 409) {
$db->table('health_records')->where('id', $row['id'])->update([
'push_status' => 'pushed',
'push_at' => date('Y-m-d H:i:s'),
'bridge_record_id' => (int) ($result['record_id'] ?? 0) ?: null,
'abdm_txn_id' => (string) ($result['queue_id'] ?? $row['abdm_txn_id'] ?? ''),
'updated_at' => date('Y-m-d H:i:s'),
]);
CLI::write(' id=' . $row['id'] . ' — pushed (record_id=' . (int) ($result['record_id'] ?? 0) . ')', 'green');
$pushed++;
continue;
}
$error = trim((string) ($result['message'] ?? $result['error_text'] ?? ('HTTP ' . $httpCode)));
foreach ((array) ($result['errors'] ?? []) as $detail) {
if (is_array($detail)) {
$error .= ' | ' . trim(implode(' ', array_filter([
(string) ($detail['code'] ?? ''),
(string) ($detail['field'] ?? ''),
(string) ($detail['message'] ?? ''),
])));
}
}
$db->table('health_records')->where('id', $row['id'])->update([
'push_status' => 'failed',
'updated_at' => date('Y-m-d H:i:s'),
]);
CLI::write(' id=' . $row['id'] . ' — failed: ' . $error, 'red');
$failed++;
}
CLI::write('Pushed: ' . $pushed, 'green');
CLI::write('Failed: ' . $failed, $failed > 0 ? 'red' : 'green');
}
/**
* @param array<string, mixed> $row
* @return array<string, mixed>|null
*/
private function resolveBundle(array $row): ?array
{
$plain = trim((string) ($row['record_data'] ?? ''));
if ($plain === '') {
$encrypted = trim((string) ($row['fhir_bundle_enc'] ?? ''));
if ($encrypted === '') {
return null;
}
try {
$plain = (new FhirEncryptionService())->decrypt($encrypted);
} catch (\Throwable $e) {
return null;
}
}
$decoded = json_decode($plain, true);
if (! is_array($decoded)) {
return null;
}
$bundle = $decoded['bundle'] ?? $decoded['fhir_bundle'] ?? $decoded;
return is_array($bundle) && ($bundle['resourceType'] ?? '') === 'Bundle' ? $bundle : null;
}
/**
* @return array{name:string,gender:string,year_of_birth:string,abha_address:string}
*/
private function loadPatient($db, int $patientId): array
{
$out = ['name' => '', 'gender' => '', 'year_of_birth' => '', 'abha_address' => ''];
if ($patientId <= 0 || ! $db->tableExists('patient_master')) {
return $out;
}
$fields = $db->getFieldNames('patient_master') ?? [];
$row = $db->table('patient_master')->where('id', $patientId)->get(1)->getRowArray();
if (! $row) {
return $out;
}
$out['name'] = trim((string) ($row['p_fname'] ?? ''));
$gender = (int) ($row['gender'] ?? 0);
$out['gender'] = $gender === 1 ? 'M' : ($gender === 2 ? 'F' : '');
$dob = trim((string) ($row['dob'] ?? ''));
if ($dob !== '' && $dob !== '0000-00-00') {
$out['year_of_birth'] = substr($dob, 0, 4);
}
if (in_array('abha_address', $fields, true)) {
$out['abha_address'] = trim((string) ($row['abha_address'] ?? ''));
}
return $out;
}
}

View File

@ -1102,7 +1102,7 @@ class AbdmGateway extends BaseController
$abhaId = trim((string) $this->request->getPost('abha_id'));
$abhaAddressPost = trim((string) $this->request->getPost('abha_address'));
$consentHandle = trim((string) $this->request->getPost('consent_handle'));
$pushToGateway = (int) $this->request->getPost('push_to_gateway') === 1;
$pushToGateway = $this->resolveGatewayPushMode();
$now = Time::now('Asia/Kolkata')->toDateTimeString();
$logBridge = function (string $status, array $responsePayload, string $errorMessage = '', string $eventType = 'abdm.opd.prescription.share') use ($opdId, $patientId, $now, $pushToGateway): void {
@ -2087,7 +2087,7 @@ class AbdmGateway extends BaseController
$abhaId = trim((string) $this->request->getPost('abha_id'));
$abhaAddressPost = trim((string) $this->request->getPost('abha_address'));
$consentHandle = trim((string) $this->request->getPost('consent_handle'));
$pushToGateway = (int) $this->request->getPost('push_to_gateway') === 1;
$pushToGateway = $this->resolveGatewayPushMode();
if ($abhaId === '' && $abhaAddressPost !== '') {
$abhaId = $abhaAddressPost;
@ -2095,6 +2095,9 @@ class AbdmGateway extends BaseController
if ($patientId <= 0) {
return $this->response->setJSON(['ok' => 0, 'error_text' => 'patient_id is required']);
}
if ($abhaId === '') {
$abhaId = $this->resolvePatientAbhaIdentifier($patientId);
}
$payload = $this->buildImmunizationGatewayPayload($patientId, $recordId, $abhaId);
if ($payload === null) {
@ -2248,6 +2251,10 @@ class AbdmGateway extends BaseController
return $this->response->setJSON(['ok' => 0, 'error_text' => 'No wellness/vital data found for this patient']);
}
if ($abhaId === '') {
$abhaId = $this->resolvePatientAbhaIdentifier($patientId);
}
return $this->pushAdditionalHiRecord($payload, $patientId, $abhaId, $consentHandle);
}
@ -2270,6 +2277,10 @@ class AbdmGateway extends BaseController
return $this->response->setJSON(['ok' => 0, 'error_text' => 'document_title with document_text/document_base64/file_path is required']);
}
if ($abhaId === '') {
$abhaId = $this->resolvePatientAbhaIdentifier($patientId);
}
return $this->pushAdditionalHiRecord($payload, $patientId, $abhaId, $consentHandle);
}
@ -2293,6 +2304,10 @@ class AbdmGateway extends BaseController
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Invoice not found or patient could not be resolved']);
}
if ($abhaId === '') {
$abhaId = $this->resolvePatientAbhaIdentifier((int) $payload['patient_id']);
}
return $this->pushAdditionalHiRecord($payload, (int) $payload['patient_id'], $abhaId, $consentHandle);
}
@ -4505,6 +4520,23 @@ class AbdmGateway extends BaseController
};
}
/**
* HMS sits behind NAT, so ABDM/bridge discovery callbacks can never reach it —
* records must be pushed to the gateway to become discoverable in the PHR.
* Set ABDM_GATEWAY_PUSH_MODE=0 only for a callback-reachable deployment.
*/
private function resolveGatewayPushMode(): bool
{
$requested = $this->request->getPost('push_to_gateway');
if ($requested !== null && trim((string) $requested) !== '') {
return (int) $requested === 1;
}
$setting = strtolower(trim($this->readRuntimeSetting('ABDM_GATEWAY_PUSH_MODE')));
return ! in_array($setting, ['0', 'off', 'false', 'no'], true);
}
private function readRuntimeSetting(string $name): string
{
$envValue = getenv($name);
@ -5771,6 +5803,32 @@ class AbdmGateway extends BaseController
];
}
/**
* Share endpoints are triggered from screens that do not carry the ABHA, and an
* empty identifier silently downgrades the record to local-only instead of pushing.
*/
private function resolvePatientAbhaIdentifier(int $patientId): string
{
$row = $this->loadPatientRow($patientId);
if ($row === []) {
return '';
}
$address = trim((string) ($row['abha_address'] ?? ''));
if ($address !== '') {
return $address;
}
foreach (['abha_id', 'abha_no', 'abha'] as $field) {
$value = trim((string) ($row[$field] ?? ''));
if ($value !== '') {
return $value;
}
}
return '';
}
private function loadPatientRow(int $patientId): array
{
if ($patientId <= 0 || ! $this->db->tableExists('patient_master')) {

View File

@ -55,7 +55,7 @@ class AbdmGatewayPushClient
];
}
$baseUrl = rtrim($this->readSetting('ABDM_BRIDGE_URL', 'EATRIA_BRIDGE_URL'), '/');
$baseUrl = $this->resolveBridgeBaseUrl();
if ($baseUrl === '') {
return [
'ok' => false,
@ -81,7 +81,7 @@ class AbdmGatewayPushClient
];
}
$url = $baseUrl . '/api/v3/records/push';
$url = $this->buildGatewayUrl($baseUrl, '/api/v3/records/push');
$headers = [
'Authorization' => 'Bearer ' . $token,
'Content-Type' => 'application/json',
@ -153,7 +153,7 @@ class AbdmGatewayPushClient
];
}
$baseUrl = rtrim($this->readSetting('ABDM_BRIDGE_URL', 'EATRIA_BRIDGE_URL'), '/');
$baseUrl = $this->resolveBridgeBaseUrl();
if ($baseUrl === '') {
return [
'ok' => false,
@ -443,6 +443,27 @@ class AbdmGatewayPushClient
$isSubmitted = $httpStatus === 201 || $isDuplicate;
$message = (string) ($body['message'] ?? $body['error_text'] ?? ('HTTP ' . $httpStatus));
// The bridge returns field-level FHIR errors here; without them a 422 is undiagnosable.
$validationErrors = is_array($body['errors'] ?? null) ? $body['errors'] : [];
if ($validationErrors !== []) {
$details = [];
foreach ($validationErrors as $error) {
if (! is_array($error)) {
$details[] = trim((string) $error);
continue;
}
$details[] = trim(implode(' ', array_filter([
trim((string) ($error['code'] ?? '')),
trim((string) ($error['field'] ?? '')),
trim((string) ($error['message'] ?? '')),
])));
}
$details = array_values(array_filter($details));
if ($details !== []) {
$message .= ' | ' . implode(' ; ', $details);
}
}
if ($isSubmitted || ((int) ($body['ok'] ?? 0) === 1 && $httpStatus >= 200 && $httpStatus < 300)) {
return [
'ok' => true,
@ -654,6 +675,25 @@ class AbdmGatewayPushClient
return str_repeat('*', strlen($left) - 4) . substr($left, -4) . ($right !== '' ? '@' . $right : '');
}
/**
* ABDM_BRIDGE_URL still points at the retired /api/v1/bridge root in older .env files,
* so normalise every source down to the bridge API root that serves /api/v3/*.
*/
private function resolveBridgeBaseUrl(): string
{
$baseUrl = trim($this->readSetting('EATRIA_BRIDGE_URL', 'ABDM_BRIDGE_URL'));
if ($baseUrl === '') {
return '';
}
$baseUrl = rtrim($baseUrl, '/');
$baseUrl = (string) preg_replace('#/api/v\d+/bridge$#i', '/api', $baseUrl);
$baseUrl = (string) preg_replace('#/v\d+/bridge$#i', '', $baseUrl);
$baseUrl = (string) preg_replace('#/api/v\d+$#i', '/api', $baseUrl);
return rtrim($baseUrl, '/');
}
private function buildGatewayUrl(string $baseUrl, string $path): string
{
$url = rtrim($baseUrl, '/') . '/' . ltrim($path, '/');

View File

@ -106,43 +106,45 @@ class AbdmSyncWorkerService
$patientProfile = $this->resolvePatientProfile((int) ($record['local_patient_id'] ?? 0), $payload);
$hospitalId = (int) ($payload['hospital_id'] ?? 0);
if ($hospitalId <= 0) {
$hospitalId = $this->resolveHospitalIdFromSettings();
$hfrId = trim((string) ($record['hfr_id'] ?? ''));
if ($hfrId === '') {
$hfrId = trim((string) ($payload['hfr_id'] ?? ''));
}
$bundle = $payload['fhir_bundle'] ?? null;
if (is_string($bundle)) {
$bundle = json_decode($bundle, true);
}
$recordType = $this->mapHiTypeToRecordType((string) ($record['hi_type'] ?? ''));
$careContextPayload = [
'hospital_id' => $hospitalId,
'hfr_id' => (string) ($record['hfr_id'] ?? ''),
'patient' => [
'patient_id' => $patientProfile['patient_id'],
'name' => $patientProfile['name'],
'mobile' => $patientProfile['mobile'],
'gender' => $patientProfile['gender'],
'year_of_birth' => $patientProfile['year_of_birth'],
],
'care_contexts' => [[
'reference_number' => (string) ($record['care_context_reference'] ?? ''),
'display' => (string) ($record['care_context_display'] ?? $recordType),
'record_type' => $recordType,
'visit_date' => $this->normalizeVisitDateTime((string) ($record['visit_date'] ?? '')),
'doctor_name' => (string) ($record['doctor_name'] ?? ''),
'department' => (string) ($record['department'] ?? ''),
]],
// NAT deployment: the bridge answers ABDM discovery/data requests from pushed
// records, so /api/v3/records/push is the only call needed (v1 link is retired).
$recordPayload = [
'hfr_id' => $hfrId,
'hi_type' => (string) ($record['hi_type'] ?? $recordType),
'care_context_reference' => (string) ($record['care_context_reference'] ?? ''),
'care_context_display' => (string) ($record['care_context_display'] ?? $recordType),
'visit_date' => $this->normalizeVisitDateTime((string) ($record['visit_date'] ?? '')),
'doctor_name' => (string) ($record['doctor_name'] ?? ''),
'department' => (string) ($record['department'] ?? ''),
'patient_name' => $patientProfile['name'],
'gender' => $patientProfile['gender'],
'mobile' => $patientProfile['mobile'],
'fhir_bundle' => is_array($bundle) ? $bundle : null,
];
$abhaAddress = trim((string) ($patientProfile['abha_address'] ?? ''));
if ($abhaAddress !== '') {
$careContextPayload['patient']['abha_address'] = $abhaAddress;
$recordPayload['abha_address'] = $abhaAddress;
}
$abhaNumber = trim((string) ($patientProfile['abha_number'] ?? ''));
if ($abhaNumber !== '') {
$careContextPayload['patient']['abha_number'] = $abhaNumber;
$recordPayload['abha_id'] = $abhaNumber;
}
$result = $this->gatewayClient->pushCareContextLink($careContextPayload, $idempotencyKey);
$result = $this->gatewayClient->pushRecord($recordPayload, $idempotencyKey);
$this->recordModel->update($syncRecordId, [
'sync_status' => (bool) ($result['ok'] ?? false) ? 'done' : ((bool) ($result['retryable'] ?? false) ? 'failed' : 'dead'),

View File

@ -1235,6 +1235,37 @@ class FhirR4Builder
if ($organizationRef !== '') {
$encounterResource['serviceProvider'] = ['reference' => $organizationRef];
}
// ABDM rejects a DischargeSummaryRecord without at least one Condition or
// Procedure resource, so the diagnosis must be a resource and not just narrative.
$diagnosisResourceText = trim((string) (
$summary['diagnosis_text']
?? $summary['final_diagnosis']
?? $summary['chief_complaints']
?? ''
));
$conditionRef = '';
if ($diagnosisResourceText !== '') {
$conditionUuid = $this->generateUuid();
$conditionRef = 'urn:uuid:' . $conditionUuid;
$encounterResource['diagnosis'] = [[
'condition' => ['reference' => $conditionRef, 'display' => 'Condition'],
]];
$resourceEntries[] = ['fullUrl' => $conditionRef, 'resource' => [
'resourceType' => 'Condition',
'id' => $conditionUuid,
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Condition']],
'clinicalStatus' => ['coding' => [['system' => 'http://terminology.hl7.org/CodeSystem/condition-clinical', 'code' => 'active', 'display' => 'Active']]],
'verificationStatus' => ['coding' => [['system' => 'http://terminology.hl7.org/CodeSystem/condition-ver-status', 'code' => 'confirmed', 'display' => 'Confirmed']]],
'category' => [['coding' => [['system' => 'http://terminology.hl7.org/CodeSystem/condition-category', 'code' => 'encounter-diagnosis', 'display' => 'Encounter Diagnosis']]]],
'code' => ['text' => $diagnosisResourceText],
'subject' => ['reference' => $patientRef, 'display' => 'Patient'],
'encounter' => ['reference' => $encounterRef],
]];
}
$resourceEntries[] = ['fullUrl' => $encounterRef, 'resource' => $encounterResource];
// ── Composition sections per DischargeSummaryRecord spec ──────────────
@ -1264,6 +1295,15 @@ class FhirR4Builder
];
}
if ($conditionRef !== '') {
$sections[] = [
'title' => 'Diagnosis',
'code' => ['coding' => [['system' => 'http://snomed.info/sct', 'code' => '721981007', 'display' => 'Diagnosis']]],
'entry' => [['reference' => $conditionRef, 'display' => 'Condition']],
'text' => ['status' => 'generated', 'div' => '<div xmlns="http://www.w3.org/1999/xhtml">' . htmlspecialchars($diagnosisResourceText) . '</div>'],
];
}
// Investigations section (SNOMED 721981007)
$investigationsText = trim((string) ($summary['investigations_text'] ?? ''));
if ($investigationsText !== '') {
@ -1434,11 +1474,81 @@ class FhirR4Builder
]],
];
// ABDM validates InvoiceRecord on a FHIR Invoice resource; Claim alone is rejected.
$invoiceLineItems = [];
foreach ($claimItems as $index => $claimItem) {
$invoiceLineItems[] = [
'sequence' => $index + 1,
'chargeItemCodeableConcept' => $claimItem['productOrService'],
'priceComponent' => [[
'type' => 'base',
'amount' => $claimItem['net'],
]],
];
}
$invoiceResource = [
'resourceType' => 'Invoice',
'id' => 'invoice-' . $claimId,
'status' => (string) ($claim['invoice_status'] ?? 'issued'),
'subject' => ['reference' => $patientRef],
'date' => $issuedAt,
'totalGross' => [
'value' => (float) ($claim['total'] ?? 0),
'currency' => (string) ($claim['currency'] ?? 'INR'),
],
'totalNet' => [
'value' => (float) ($claim['total'] ?? 0),
'currency' => (string) ($claim['currency'] ?? 'INR'),
],
];
$invoiceNo = trim((string) ($claim['invoice_no'] ?? ''));
if ($invoiceNo !== '') {
$invoiceResource['identifier'] = [['system' => 'https://ndhm.in/invoice', 'value' => $invoiceNo]];
}
if ($invoiceLineItems !== []) {
$invoiceResource['lineItem'] = $invoiceLineItems;
}
// ABDM InvoiceRecord must be a document bundle led by a Composition.
$composition = [
'resourceType' => 'Composition',
'id' => 'comp-invoice-' . $claimId,
'status' => 'final',
'type' => [
'coding' => [[
'system' => 'http://snomed.info/sct',
'code' => '736762002',
'display' => 'Invoice record',
]],
'text' => 'Invoice',
],
'subject' => ['reference' => $patientRef],
'date' => $issuedAt,
'title' => 'Invoice',
'encounter' => ['reference' => $encounterRef],
'section' => [[
'title' => 'Invoice',
'entry' => [
['reference' => 'Invoice/invoice-' . $claimId],
['reference' => 'Claim/' . $claimId],
],
]],
];
$provider = trim((string) ($claim['provider'] ?? ''));
if ($provider !== '') {
$composition['author'] = [['display' => $provider]];
}
return [
'resourceType' => 'Bundle',
'type' => 'collection',
'type' => 'document',
'timestamp' => $issuedAt,
'entry' => [[
'resource' => $composition,
], [
'resource' => $this->buildPatientResource($patient),
], [
'resource' => [
@ -1447,6 +1557,8 @@ class FhirR4Builder
'status' => (string) ($encounter['status'] ?? 'finished'),
'subject' => ['reference' => $patientRef],
],
], [
'resource' => $invoiceResource,
], [
'resource' => $claimResource,
]],
@ -2749,7 +2861,8 @@ class FhirR4Builder
if ($practitionerRef !== '') {
$composition['author'] = [['reference' => $practitionerRef]];
}
$entries[] = ['resource' => $composition];
// ABDM requires the Composition to be the first bundle entry.
array_unshift($entries, ['resource' => $composition]);
return [
'resourceType' => 'Bundle',

View File

@ -193,6 +193,26 @@
</div>
</div>
<hr class="my-2">
<div class="small fw-semibold mb-1">Share Additional HI Types</div>
<div class="row g-2 mb-2">
<div class="col-md-4">
<input type="number" min="1" class="form-control form-control-sm" id="tb_hi_patient_id" placeholder="Patient ID">
</div>
<div class="col-md-4">
<input type="number" min="1" class="form-control form-control-sm" id="tb_hi_opd_id" placeholder="OPD ID (wellness)">
</div>
<div class="col-md-4">
<input type="number" min="1" class="form-control form-control-sm" id="tb_hi_invoice_id" placeholder="Invoice ID">
</div>
</div>
<div class="d-flex flex-wrap gap-2 mb-2">
<button type="button" class="btn btn-outline-info btn-sm" data-hi-share="immunization">Immunization</button>
<button type="button" class="btn btn-outline-info btn-sm" data-hi-share="wellness">Wellness</button>
<button type="button" class="btn btn-outline-info btn-sm" data-hi-share="health_document">Health Document</button>
<button type="button" class="btn btn-outline-info btn-sm" data-hi-share="invoice">Invoice</button>
</div>
<div class="small text-muted" id="abdmSandboxStatus">Ready</div>
<div class="mt-2 d-none" id="abdmScanResolvePane"></div>
</div>
@ -1296,6 +1316,51 @@
});
});
document.querySelectorAll('[data-hi-share]').forEach(function (btn) {
btn.addEventListener('click', function () {
var kind = btn.getAttribute('data-hi-share');
var patientId = parseInt((document.getElementById('tb_hi_patient_id').value || '0'), 10) || 0;
var opdId = parseInt((document.getElementById('tb_hi_opd_id').value || '0'), 10) || 0;
var invoiceId = parseInt((document.getElementById('tb_hi_invoice_id').value || '0'), 10) || 0;
if (kind !== 'invoice' && patientId <= 0) {
setSandboxStatus('Enter Patient ID first.', 'warning');
return;
}
if (kind === 'invoice' && invoiceId <= 0) {
setSandboxStatus('Enter Invoice ID first.', 'warning');
return;
}
var urls = {
immunization: '<?= base_url('AbdmGateway/share_immunization_bundle') ?>',
wellness: '<?= base_url('AbdmGateway/share_wellness_bundle') ?>',
health_document: '<?= base_url('AbdmGateway/share_health_document_bundle') ?>',
invoice: '<?= base_url('AbdmGateway/share_invoice_bundle') ?>'
};
var payload = { patient_id: patientId, consent_handle: '' };
if (kind === 'wellness') { payload.opd_id = opdId; }
if (kind === 'invoice') { payload.invoice_id = invoiceId; }
var original = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Sharing';
setSandboxStatus('Sharing ' + kind + ' record...');
post(urls[kind], payload, function (res) {
btn.disabled = false;
btn.innerHTML = original;
if (!res || parseInt(res.ok || '0', 10) !== 1) {
setSandboxStatus(res.error_text || res.error || ('Failed to share ' + kind + ' record.'), 'danger');
return;
}
setSandboxStatus(kind + ' shared. Care context: ' + (res.care_context_reference || '-')
+ (res.queue_id ? (' | Queue: ' + res.queue_id) : ''), 'success');
});
});
});
document.getElementById('btnHideFhirDebug').addEventListener('click', function () {
if (fhirDebugCard) {
fhirDebugCard.classList.add('d-none');