ABDM records/push: treat 201/409 as submitted, persist record IDs, and update OPD UI state
This commit is contained in:
parent
5d6acdd480
commit
c84ea383e4
|
|
@ -1451,31 +1451,28 @@ class AbdmGateway extends BaseController
|
|||
'queue_id' => $careContextRef,
|
||||
'record_data' => $bundle,
|
||||
]);
|
||||
$this->logGatewayPushResolution('opd', $result);
|
||||
|
||||
$queueId = (string) ($result['queue_id'] ?? '');
|
||||
$bridgeRecordId = (int) ($result['id'] ?? 0);
|
||||
$queueId = $this->extractGatewayPushQueueId($result);
|
||||
$bridgeRecordId = $this->extractGatewayPushRecordId($result);
|
||||
|
||||
$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';
|
||||
$httpCode = $this->extractGatewayPushHttpCode($result);
|
||||
$resultOk = $this->isGatewayPushSubmitted($result);
|
||||
$isDuplicate = $this->isGatewayPushDuplicate($result);
|
||||
|
||||
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'] ?? '')
|
||||
));
|
||||
$connectorError = $this->extractGatewayPushErrorText($result);
|
||||
if ($connectorError === '') {
|
||||
$connectorError = 'Bridge push failed';
|
||||
}
|
||||
$status = 'failed';
|
||||
} elseif (! $isDuplicate) {
|
||||
$status = 'queued';
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$connectorError = $e->getMessage();
|
||||
|
|
@ -1628,8 +1625,15 @@ class AbdmGateway extends BaseController
|
|||
'queue_id' => 'IPD-' . $ipdId . '-' . $visitDate,
|
||||
'record_data' => $bundle,
|
||||
]);
|
||||
$queueId = (string) ($result['queue_id'] ?? '');
|
||||
$bridgeRecordId = (int) ($result['id'] ?? 0);
|
||||
$this->logGatewayPushResolution('ipd_discharge', $result);
|
||||
$queueId = $this->extractGatewayPushQueueId($result);
|
||||
$bridgeRecordId = $this->extractGatewayPushRecordId($result);
|
||||
if (! $this->isGatewayPushSubmitted($result)) {
|
||||
$connectorError = $this->extractGatewayPushErrorText($result);
|
||||
if ($connectorError === '') {
|
||||
$connectorError = 'Bridge push failed';
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$connectorError = $e->getMessage();
|
||||
}
|
||||
|
|
@ -1861,8 +1865,15 @@ class AbdmGateway extends BaseController
|
|||
'queue_id' => 'LAB-' . $labReqId . '-' . $visitDate,
|
||||
'record_data' => $bundle,
|
||||
]);
|
||||
$queueId = (string) ($result['queue_id'] ?? '');
|
||||
$bridgeRecordId = (int) ($result['id'] ?? 0);
|
||||
$this->logGatewayPushResolution('diagnostic_report', $result);
|
||||
$queueId = $this->extractGatewayPushQueueId($result);
|
||||
$bridgeRecordId = $this->extractGatewayPushRecordId($result);
|
||||
if (! $this->isGatewayPushSubmitted($result)) {
|
||||
$connectorError = $this->extractGatewayPushErrorText($result);
|
||||
if ($connectorError === '') {
|
||||
$connectorError = 'Bridge push failed';
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$connectorError = $e->getMessage();
|
||||
}
|
||||
|
|
@ -2160,25 +2171,13 @@ class AbdmGateway extends BaseController
|
|||
'queue_id' => $ccRef,
|
||||
'record_data' => $bundle,
|
||||
]);
|
||||
$this->logGatewayPushResolution('immunization', $result);
|
||||
$bridgeResponse = $result;
|
||||
$queueId = (string) ($result['queue_id'] ?? $result['existing_queue_id'] ?? '');
|
||||
$bridgeRecordId = (int) ($result['record_id'] ?? $result['id'] ?? $result['existing_record_id'] ?? 0);
|
||||
$queueId = $this->extractGatewayPushQueueId($result);
|
||||
$bridgeRecordId = $this->extractGatewayPushRecordId($result);
|
||||
|
||||
$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) {
|
||||
$resultOk = true;
|
||||
}
|
||||
|
||||
if (! $resultOk) {
|
||||
$connectorError = trim((string) (
|
||||
$result['message']
|
||||
?? $result['error_text']
|
||||
?? ($result['error_code'] ?? '')
|
||||
));
|
||||
if (! $this->isGatewayPushSubmitted($result)) {
|
||||
$connectorError = $this->extractGatewayPushErrorText($result);
|
||||
if ($connectorError === '' && ! empty($result['errors'])) {
|
||||
$connectorError = (string) json_encode($result['errors'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
|
@ -3363,8 +3362,15 @@ class AbdmGateway extends BaseController
|
|||
'queue_id' => trim((string) ($hr['care_context_reference'] ?? '')),
|
||||
'record_data' => $fhirBundle,
|
||||
]);
|
||||
$queueId = $result['queue_id'] ?? null;
|
||||
$bridgeRecordId = (int) ($result['id'] ?? 0);
|
||||
$this->logGatewayPushResolution('health_record_repush', $result);
|
||||
$queueId = $this->extractGatewayPushQueueId($result);
|
||||
$bridgeRecordId = $this->extractGatewayPushRecordId($result);
|
||||
if (! $this->isGatewayPushSubmitted($result)) {
|
||||
$connectorErr = $this->extractGatewayPushErrorText($result);
|
||||
if ($connectorErr === '') {
|
||||
$connectorErr = 'Bridge push failed';
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$connectorErr = $e->getMessage();
|
||||
$bridgeRecordId = 0;
|
||||
|
|
@ -4740,6 +4746,209 @@ class AbdmGateway extends BaseController
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $result
|
||||
*/
|
||||
private function extractGatewayPushRecordMeta(array $result): array
|
||||
{
|
||||
$candidates = [
|
||||
'record_id' => $result['record_id'] ?? null,
|
||||
'existing_record_id' => $result['existing_record_id'] ?? null,
|
||||
'id' => $result['id'] ?? null,
|
||||
'response.record_id' => $result['response']['record_id'] ?? null,
|
||||
'response.existing_record_id' => $result['response']['existing_record_id'] ?? null,
|
||||
'response.id' => $result['response']['id'] ?? null,
|
||||
'data.record_id' => $result['data']['record_id'] ?? null,
|
||||
'data.existing_record_id' => $result['data']['existing_record_id'] ?? null,
|
||||
'data.id' => $result['data']['id'] ?? null,
|
||||
];
|
||||
|
||||
foreach ($candidates as $source => $value) {
|
||||
$id = (int) $value;
|
||||
if ($id > 0) {
|
||||
return ['id' => $id, 'source' => $source];
|
||||
}
|
||||
}
|
||||
|
||||
return ['id' => 0, 'source' => 'none'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $result
|
||||
*/
|
||||
private function extractGatewayPushRecordId(array $result): int
|
||||
{
|
||||
$meta = $this->extractGatewayPushRecordMeta($result);
|
||||
return (int) ($meta['id'] ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $result
|
||||
*/
|
||||
private function extractGatewayPushQueueMeta(array $result): array
|
||||
{
|
||||
$candidates = [
|
||||
'queue_id' => $result['queue_id'] ?? null,
|
||||
'existing_queue_id' => $result['existing_queue_id'] ?? null,
|
||||
'response.queue_id' => $result['response']['queue_id'] ?? null,
|
||||
'response.existing_queue_id' => $result['response']['existing_queue_id'] ?? null,
|
||||
'data.queue_id' => $result['data']['queue_id'] ?? null,
|
||||
'data.existing_queue_id' => $result['data']['existing_queue_id'] ?? null,
|
||||
];
|
||||
|
||||
foreach ($candidates as $source => $value) {
|
||||
$queueId = trim((string) $value);
|
||||
if ($queueId !== '') {
|
||||
return ['id' => $queueId, 'source' => $source];
|
||||
}
|
||||
}
|
||||
|
||||
return ['id' => '', 'source' => 'none'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $result
|
||||
*/
|
||||
private function extractGatewayPushQueueId(array $result): string
|
||||
{
|
||||
$meta = $this->extractGatewayPushQueueMeta($result);
|
||||
return (string) ($meta['id'] ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $result
|
||||
*/
|
||||
private function extractGatewayPushHttpCode(array $result): int
|
||||
{
|
||||
$candidates = [
|
||||
$result['http_code'] ?? null,
|
||||
$result['response']['http_code'] ?? null,
|
||||
$result['data']['http_code'] ?? null,
|
||||
];
|
||||
|
||||
foreach ($candidates as $value) {
|
||||
$httpCode = (int) $value;
|
||||
if ($httpCode > 0) {
|
||||
return $httpCode;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $result
|
||||
*/
|
||||
private function isGatewayPushDuplicate(array $result): bool
|
||||
{
|
||||
$httpCode = $this->extractGatewayPushHttpCode($result);
|
||||
if ($httpCode === 409) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$codes = [
|
||||
$result['error_code'] ?? null,
|
||||
$result['response']['error_code'] ?? null,
|
||||
$result['data']['error_code'] ?? null,
|
||||
];
|
||||
foreach ($codes as $code) {
|
||||
if (strtoupper(trim((string) $code)) === 'DUPLICATE_RECORD') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP 201 and HTTP 409 (duplicate) are treated as submitted success states.
|
||||
*
|
||||
* @param array<string,mixed> $result
|
||||
*/
|
||||
private function isGatewayPushSubmitted(array $result): bool
|
||||
{
|
||||
$httpCode = $this->extractGatewayPushHttpCode($result);
|
||||
if ($httpCode === 201 || $httpCode === 409) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->isGatewayPushDuplicate($result)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$okValues = [
|
||||
$result['ok'] ?? null,
|
||||
$result['response']['ok'] ?? null,
|
||||
$result['data']['ok'] ?? null,
|
||||
];
|
||||
|
||||
foreach ($okValues as $ok) {
|
||||
if ((int) $ok === 1 && $httpCode >= 200 && $httpCode < 300) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $result
|
||||
*/
|
||||
private function extractGatewayPushErrorText(array $result): string
|
||||
{
|
||||
$messages = [
|
||||
$result['message'] ?? null,
|
||||
$result['error_text'] ?? null,
|
||||
$result['response']['message'] ?? null,
|
||||
$result['response']['error_text'] ?? null,
|
||||
$result['data']['message'] ?? null,
|
||||
$result['data']['error_text'] ?? null,
|
||||
$result['error_code'] ?? null,
|
||||
$result['response']['error_code'] ?? null,
|
||||
$result['data']['error_code'] ?? null,
|
||||
];
|
||||
|
||||
foreach ($messages as $message) {
|
||||
$text = trim((string) $message);
|
||||
if ($text !== '') {
|
||||
return $text;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit one diagnostic line to show exactly which gateway response fields
|
||||
* supplied queue/record identifiers for records/push integration.
|
||||
*
|
||||
* @param array<string,mixed> $result
|
||||
*/
|
||||
private function logGatewayPushResolution(string $context, array $result): void
|
||||
{
|
||||
try {
|
||||
$recordMeta = $this->extractGatewayPushRecordMeta($result);
|
||||
$queueMeta = $this->extractGatewayPushQueueMeta($result);
|
||||
$httpCode = $this->extractGatewayPushHttpCode($result);
|
||||
$submitted = $this->isGatewayPushSubmitted($result) ? 1 : 0;
|
||||
$duplicate = $this->isGatewayPushDuplicate($result) ? 1 : 0;
|
||||
|
||||
log_message(
|
||||
'info',
|
||||
'[abdm.records.push.resolve][' . $context . '] '
|
||||
. 'http_code=' . $httpCode
|
||||
. ' submitted=' . $submitted
|
||||
. ' duplicate=' . $duplicate
|
||||
. ' record_id=' . (int) ($recordMeta['id'] ?? 0)
|
||||
. ' record_source=' . (string) ($recordMeta['source'] ?? 'none')
|
||||
. ' queue_id=' . (string) ($queueMeta['id'] ?? '')
|
||||
. ' queue_source=' . (string) ($queueMeta['source'] ?? 'none')
|
||||
);
|
||||
} catch (\Throwable) {
|
||||
// Fail-open diagnostics
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a health_record's push status and txn_id after getting a connector response.
|
||||
* Also creates a matching record_links row.
|
||||
|
|
@ -5364,12 +5573,11 @@ class AbdmGateway extends BaseController
|
|||
'queue_id' => $ccRef,
|
||||
'record_data' => $bundle,
|
||||
]);
|
||||
$queueId = (string) ($result['queue_id'] ?? $result['existing_queue_id'] ?? '');
|
||||
$bridgeRecordId = (int) ($result['record_id'] ?? $result['id'] ?? $result['existing_record_id'] ?? 0);
|
||||
$httpCode = (int) ($result['http_code'] ?? 0);
|
||||
$errorCode = strtoupper(trim((string) ($result['error_code'] ?? '')));
|
||||
if ((int) ($result['ok'] ?? 0) !== 1 && $httpCode !== 409 && $errorCode !== 'DUPLICATE_RECORD') {
|
||||
$connectorError = trim((string) ($result['message'] ?? $result['error_text'] ?? $errorCode));
|
||||
$this->logGatewayPushResolution('additional_hi_record', $result);
|
||||
$queueId = $this->extractGatewayPushQueueId($result);
|
||||
$bridgeRecordId = $this->extractGatewayPushRecordId($result);
|
||||
if (! $this->isGatewayPushSubmitted($result)) {
|
||||
$connectorError = $this->extractGatewayPushErrorText($result);
|
||||
if ($connectorError === '') {
|
||||
$connectorError = 'Bridge push failed';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -621,7 +621,7 @@ class AbdmTaskBoard extends BaseController
|
|||
$statusTone = 'secondary';
|
||||
$statusNote = 'Stored locally only (ABHA/consent not ready).';
|
||||
} elseif ($pushStatus === 'queued') {
|
||||
$statusLabel = 'Gateway Queued';
|
||||
$statusLabel = 'Submitted';
|
||||
$statusTone = 'warning';
|
||||
$statusNote = 'Submitted to gateway and waiting for link workflow.';
|
||||
} elseif ($pushStatus === 'linked') {
|
||||
|
|
|
|||
|
|
@ -4873,6 +4873,7 @@ class Opd_prescription extends BaseController
|
|||
$medications = $this->getPrescriptionMedicines($sessionId);
|
||||
$conditions = $this->getPrescriptionConditions($sessionId);
|
||||
$clinicalContext = $sessionId > 0 ? $this->getPrescriptionClinicalContext($sessionId, $opdRow) : [];
|
||||
$clinicalContext['attachments'] = $this->collectOpdFhirAttachments($opdId);
|
||||
$bundle = $this->fhirR4Builder->buildPrescriptionBundle($patient, $encounter, $medications, $conditions, $clinicalContext);
|
||||
$bundleJson = (string) json_encode($bundle, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
|
|
@ -4960,6 +4961,7 @@ class Opd_prescription extends BaseController
|
|||
'document_id' => (int) ($row['id'] ?? 0),
|
||||
]);
|
||||
}
|
||||
$bundle = $this->appendOpdFhirAttachmentsToBundle($bundle, (int) $opdId);
|
||||
|
||||
$prettyJson = (string) json_encode($bundle, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
|
|
@ -5420,6 +5422,16 @@ class Opd_prescription extends BaseController
|
|||
'csrfHash' => csrf_hash(),
|
||||
]);
|
||||
}
|
||||
$bundleWithAttachments = $this->appendOpdFhirAttachmentsToBundle($bundle, $opdId);
|
||||
if ($bundleWithAttachments !== $bundle) {
|
||||
$bundle = $bundleWithAttachments;
|
||||
$this->db->table('opd_fhir_documents')
|
||||
->where('id', (int) ($bundleRow['id'] ?? 0))
|
||||
->update([
|
||||
'bundle_json' => (string) json_encode($bundle, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
'generated_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
|
||||
]);
|
||||
}
|
||||
|
||||
// Gather patient / OPD context for the push payload
|
||||
$patientId = 0;
|
||||
|
|
@ -11096,6 +11108,268 @@ class Opd_prescription extends BaseController
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, string>>
|
||||
*/
|
||||
private function collectOpdFhirAttachments(int $opdId, int $limit = 5): array
|
||||
{
|
||||
if ($opdId <= 0 || ! $this->db->tableExists('file_upload_data')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$fields = $this->db->getFieldNames('file_upload_data') ?? [];
|
||||
$select = [];
|
||||
foreach (['id', 'full_path', 'public_path', 'file_ext', 'file_type', 'insert_date', 'document_type', 'scan_type', 'content_description', 'name', 'file_name', 'orig_name', 'client_name'] as $field) {
|
||||
if (in_array($field, $fields, true)) {
|
||||
$select[] = $field;
|
||||
}
|
||||
}
|
||||
if (! in_array('id', $select, true) || ! in_array('full_path', $select, true)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$builder = $this->db->table('file_upload_data')
|
||||
->select(implode(',', array_unique($select)))
|
||||
->where('opd_id', $opdId);
|
||||
if (in_array('show_type', $fields, true)) {
|
||||
$builder->where('show_type', 0);
|
||||
}
|
||||
|
||||
$rows = $builder->orderBy('id', 'DESC')
|
||||
->limit(max(1, min(10, $limit)))
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$attachments = [];
|
||||
foreach ($rows as $row) {
|
||||
$path = $this->resolveOpdFhirAttachmentPath($row);
|
||||
if ($path === '' || ! is_readable($path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$size = (int) (@filesize($path) ?: 0);
|
||||
if ($size <= 0 || $size > 8 * 1024 * 1024) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$contentType = $this->normalizeOpdFhirAttachmentMimeType((string) ($row['file_type'] ?? ''), (string) ($row['file_ext'] ?? pathinfo($path, PATHINFO_EXTENSION)));
|
||||
if (! in_array($contentType, ['application/pdf', 'image/jpeg', 'image/png', 'image/webp'], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$raw = @file_get_contents($path);
|
||||
if ($raw === false || $raw === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$titleParts = [];
|
||||
foreach (['document_type', 'scan_type', 'content_description'] as $field) {
|
||||
$value = trim((string) ($row[$field] ?? ''));
|
||||
if ($value !== '' && strtolower($value) !== 'general' && strtolower($value) !== 'queued for ai analysis') {
|
||||
$titleParts[] = $value;
|
||||
}
|
||||
}
|
||||
$title = trim(implode(' - ', array_unique($titleParts)));
|
||||
if ($title === '') {
|
||||
$title = 'OPD scanned/uploaded document';
|
||||
}
|
||||
|
||||
$attachments[] = [
|
||||
'id' => (string) ((int) ($row['id'] ?? 0)),
|
||||
'title' => $title,
|
||||
'content_type' => $contentType,
|
||||
'data_base64' => base64_encode($raw),
|
||||
'date' => ! empty($row['insert_date']) ? date(DATE_ATOM, strtotime((string) $row['insert_date'])) : Time::now('Asia/Kolkata')->format(DATE_ATOM),
|
||||
];
|
||||
}
|
||||
|
||||
return array_reverse($attachments);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $row
|
||||
*/
|
||||
private function resolveOpdFhirAttachmentPath(array $row): string
|
||||
{
|
||||
$candidates = [];
|
||||
foreach (['full_path', 'public_path'] as $field) {
|
||||
$value = trim((string) ($row[$field] ?? ''));
|
||||
if ($value !== '') {
|
||||
$candidates[] = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $value);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($candidates as $candidate) {
|
||||
$checks = [$candidate];
|
||||
if (! preg_match('/^[A-Za-z]:\\\\/', $candidate)) {
|
||||
$relative = ltrim($candidate, DIRECTORY_SEPARATOR);
|
||||
$checks[] = ROOTPATH . $relative;
|
||||
$checks[] = FCPATH . $relative;
|
||||
}
|
||||
|
||||
foreach ($checks as $check) {
|
||||
$real = realpath($check);
|
||||
if ($real !== false && is_file($real) && is_readable($real)) {
|
||||
return $real;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private function normalizeOpdFhirAttachmentMimeType(string $mimeType, string $extension): string
|
||||
{
|
||||
$mimeType = strtolower(trim($mimeType));
|
||||
if (in_array($mimeType, ['application/pdf', 'image/jpeg', 'image/png', 'image/webp'], true)) {
|
||||
return $mimeType;
|
||||
}
|
||||
|
||||
$extension = strtolower(ltrim(trim($extension), '.'));
|
||||
return match ($extension) {
|
||||
'pdf' => 'application/pdf',
|
||||
'png' => 'image/png',
|
||||
'webp' => 'image/webp',
|
||||
'jpg', 'jpeg' => 'image/jpeg',
|
||||
default => 'application/octet-stream',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $bundle
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function appendOpdFhirAttachmentsToBundle(array $bundle, int $opdId): array
|
||||
{
|
||||
$attachments = $this->collectOpdFhirAttachments($opdId);
|
||||
if (empty($attachments) || empty($bundle['entry']) || ! is_array($bundle['entry'])) {
|
||||
return $bundle;
|
||||
}
|
||||
|
||||
$compositionIndex = null;
|
||||
foreach ($bundle['entry'] as $index => $entry) {
|
||||
$resource = is_array($entry) ? ($entry['resource'] ?? null) : null;
|
||||
if (is_array($resource) && (string) ($resource['resourceType'] ?? '') === 'Composition') {
|
||||
$compositionIndex = $index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($compositionIndex === null) {
|
||||
return $bundle;
|
||||
}
|
||||
|
||||
$patientRef = (string) ($bundle['entry'][$compositionIndex]['resource']['subject']['reference'] ?? '');
|
||||
if ($patientRef === '') {
|
||||
foreach ($bundle['entry'] as $entry) {
|
||||
$resource = is_array($entry) ? ($entry['resource'] ?? null) : null;
|
||||
if (is_array($resource) && (string) ($resource['resourceType'] ?? '') === 'Patient') {
|
||||
$patientRef = (string) ($entry['fullUrl'] ?? ('Patient/' . (string) ($resource['id'] ?? 'patient')));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($patientRef === '') {
|
||||
$patientRef = 'Patient/unknown';
|
||||
}
|
||||
|
||||
$stripGenerated = static function (array $entry): bool {
|
||||
$resource = $entry['resource'] ?? null;
|
||||
if (! is_array($resource)) {
|
||||
return true;
|
||||
}
|
||||
$id = (string) ($resource['id'] ?? '');
|
||||
return ! str_starts_with($id, 'opd-scan-') && ! str_starts_with($id, 'binary-opd-scan-');
|
||||
};
|
||||
$bundle['entry'] = array_values(array_filter($bundle['entry'], $stripGenerated));
|
||||
|
||||
$sectionRefs = [];
|
||||
foreach ($attachments as $attachment) {
|
||||
$attachmentId = preg_replace('/[^A-Za-z0-9\-\.]/', '-', (string) ($attachment['id'] ?? '')) ?: (string) random_int(1, 999999);
|
||||
$suffix = bin2hex(random_bytes(6));
|
||||
$docId = 'opd-scan-' . $attachmentId . '-' . $suffix;
|
||||
$binaryId = 'binary-' . $docId;
|
||||
$docRef = 'urn:uuid:' . $docId;
|
||||
$binaryRef = 'urn:uuid:' . $binaryId;
|
||||
$contentType = (string) ($attachment['content_type'] ?? 'application/octet-stream');
|
||||
$title = (string) ($attachment['title'] ?? 'OPD scanned/uploaded document');
|
||||
$dataBase64 = (string) ($attachment['data_base64'] ?? '');
|
||||
$date = ! empty($attachment['date']) ? date(DATE_ATOM, strtotime((string) $attachment['date'])) : Time::now('Asia/Kolkata')->format(DATE_ATOM);
|
||||
|
||||
if ($dataBase64 === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sectionRefs[] = ['reference' => $docRef, 'display' => 'DocumentReference'];
|
||||
$bundle['entry'][] = ['fullUrl' => $binaryRef, 'resource' => [
|
||||
'resourceType' => 'Binary',
|
||||
'id' => $binaryId,
|
||||
'contentType' => $contentType,
|
||||
'data' => $dataBase64,
|
||||
]];
|
||||
$bundle['entry'][] = ['fullUrl' => $docRef, 'resource' => [
|
||||
'resourceType' => 'DocumentReference',
|
||||
'id' => $docId,
|
||||
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/DocumentReference']],
|
||||
'status' => 'current',
|
||||
'type' => ['text' => $title],
|
||||
'subject' => ['reference' => $patientRef, 'display' => 'Patient'],
|
||||
'date' => $date,
|
||||
'content' => [[
|
||||
'attachment' => [
|
||||
'contentType' => $contentType,
|
||||
'url' => $binaryRef,
|
||||
'data' => $dataBase64,
|
||||
'title' => $title,
|
||||
],
|
||||
]],
|
||||
]];
|
||||
}
|
||||
|
||||
if (empty($sectionRefs)) {
|
||||
return $bundle;
|
||||
}
|
||||
|
||||
$sections = (array) ($bundle['entry'][$compositionIndex]['resource']['section'] ?? []);
|
||||
foreach ($sections as $index => $section) {
|
||||
if (! is_array($section)) {
|
||||
continue;
|
||||
}
|
||||
$entries = (array) ($section['entry'] ?? []);
|
||||
$sections[$index]['entry'] = array_values(array_filter($entries, static function ($entry): bool {
|
||||
return ! is_array($entry) || ! str_contains((string) ($entry['reference'] ?? ''), 'opd-scan-');
|
||||
}));
|
||||
}
|
||||
|
||||
$documentSectionIndex = null;
|
||||
foreach ($sections as $index => $section) {
|
||||
if (strcasecmp((string) ($section['title'] ?? ''), 'Document Reference') === 0) {
|
||||
$documentSectionIndex = $index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($documentSectionIndex === null) {
|
||||
$sections[] = [
|
||||
'title' => 'Document Reference',
|
||||
'code' => ['coding' => [[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => '371530004',
|
||||
'display' => 'Clinical consultation report',
|
||||
]]],
|
||||
'entry' => $sectionRefs,
|
||||
];
|
||||
} else {
|
||||
$existingEntries = (array) ($sections[$documentSectionIndex]['entry'] ?? []);
|
||||
$sections[$documentSectionIndex]['entry'] = array_values(array_merge($existingEntries, $sectionRefs));
|
||||
}
|
||||
|
||||
$bundle['entry'][$compositionIndex]['resource']['section'] = array_values(array_filter($sections, static function ($section): bool {
|
||||
return ! is_array($section) || ! empty($section['entry']);
|
||||
}));
|
||||
|
||||
return $bundle;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
* @param array<int, string> $fields
|
||||
|
|
@ -11163,6 +11437,7 @@ class Opd_prescription extends BaseController
|
|||
$medications = $this->getPrescriptionMedicines($sessionId);
|
||||
$conditions = $this->getPrescriptionConditions($sessionId);
|
||||
$clinicalContext = $this->getPrescriptionClinicalContext($sessionId, $opdRow);
|
||||
$clinicalContext['attachments'] = $this->collectOpdFhirAttachments($opdId);
|
||||
$bundle = $this->fhirR4Builder->buildPrescriptionBundle($patient, $encounter, $medications, $conditions, $clinicalContext);
|
||||
$bundleJson = (string) json_encode($bundle, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
|
|
|
|||
|
|
@ -205,24 +205,57 @@ class AbdmGatewayPushClient
|
|||
*/
|
||||
private function mapResponse(int $httpStatus, array $body, string $requestId): array
|
||||
{
|
||||
$queueId = (string) ($body['queue_id'] ?? $body['existing_queue_id'] ?? '');
|
||||
$recordId = (int) ($body['record_id'] ?? $body['id'] ?? $body['existing_record_id'] ?? 0);
|
||||
$message = (string) ($body['message'] ?? $body['error_text'] ?? ('HTTP ' . $httpStatus));
|
||||
|
||||
if ($httpStatus === 201 || ((int) ($body['ok'] ?? 0) === 1 && $httpStatus >= 200 && $httpStatus < 300)) {
|
||||
return [
|
||||
'ok' => true,
|
||||
'status' => 'done',
|
||||
'http_status' => $httpStatus,
|
||||
'request_id' => $requestId,
|
||||
'gateway_record_id' => $recordId > 0 ? $recordId : null,
|
||||
'gateway_queue_id' => $queueId !== '' ? $queueId : null,
|
||||
'message' => $message,
|
||||
'retryable' => false,
|
||||
];
|
||||
$queueMeta = [
|
||||
'queue_id' => $body['queue_id'] ?? null,
|
||||
'existing_queue_id' => $body['existing_queue_id'] ?? null,
|
||||
'response.queue_id' => $body['response']['queue_id'] ?? null,
|
||||
'response.existing_queue_id' => $body['response']['existing_queue_id'] ?? null,
|
||||
'data.queue_id' => $body['data']['queue_id'] ?? null,
|
||||
'data.existing_queue_id' => $body['data']['existing_queue_id'] ?? null,
|
||||
];
|
||||
$queueId = '';
|
||||
$queueSource = 'none';
|
||||
foreach ($queueMeta as $source => $value) {
|
||||
$candidate = trim((string) $value);
|
||||
if ($candidate !== '') {
|
||||
$queueId = $candidate;
|
||||
$queueSource = $source;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($httpStatus === 409 || strtoupper((string) ($body['error_code'] ?? '')) === 'DUPLICATE_RECORD') {
|
||||
$recordMeta = [
|
||||
'record_id' => $body['record_id'] ?? null,
|
||||
'existing_record_id' => $body['existing_record_id'] ?? null,
|
||||
'id' => $body['id'] ?? null,
|
||||
'response.record_id' => $body['response']['record_id'] ?? null,
|
||||
'response.existing_record_id' => $body['response']['existing_record_id'] ?? null,
|
||||
'response.id' => $body['response']['id'] ?? null,
|
||||
'data.record_id' => $body['data']['record_id'] ?? null,
|
||||
'data.existing_record_id' => $body['data']['existing_record_id'] ?? null,
|
||||
'data.id' => $body['data']['id'] ?? null,
|
||||
];
|
||||
$recordId = 0;
|
||||
$recordSource = 'none';
|
||||
foreach ($recordMeta as $source => $value) {
|
||||
$candidate = (int) $value;
|
||||
if ($candidate > 0) {
|
||||
$recordId = $candidate;
|
||||
$recordSource = $source;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$errorCode = strtoupper(trim((string) (
|
||||
$body['error_code']
|
||||
?? ($body['response']['error_code'] ?? null)
|
||||
?? ($body['data']['error_code'] ?? '')
|
||||
)));
|
||||
$isDuplicate = $httpStatus === 409 || $errorCode === 'DUPLICATE_RECORD';
|
||||
$isSubmitted = $httpStatus === 201 || $isDuplicate;
|
||||
$message = (string) ($body['message'] ?? $body['error_text'] ?? ('HTTP ' . $httpStatus));
|
||||
|
||||
if ($isSubmitted || ((int) ($body['ok'] ?? 0) === 1 && $httpStatus >= 200 && $httpStatus < 300)) {
|
||||
return [
|
||||
'ok' => true,
|
||||
'status' => 'done',
|
||||
|
|
@ -230,7 +263,11 @@ class AbdmGatewayPushClient
|
|||
'request_id' => $requestId,
|
||||
'gateway_record_id' => $recordId > 0 ? $recordId : null,
|
||||
'gateway_queue_id' => $queueId !== '' ? $queueId : null,
|
||||
'message' => $message !== '' ? $message : 'Duplicate record handled as success.',
|
||||
'gateway_record_source' => $recordSource,
|
||||
'gateway_queue_source' => $queueSource,
|
||||
'submitted' => $isSubmitted ? 1 : 0,
|
||||
'duplicate' => $isDuplicate ? 1 : 0,
|
||||
'message' => $message,
|
||||
'retryable' => false,
|
||||
];
|
||||
}
|
||||
|
|
@ -246,6 +283,10 @@ class AbdmGatewayPushClient
|
|||
'retryable' => true,
|
||||
'gateway_record_id' => null,
|
||||
'gateway_queue_id' => null,
|
||||
'gateway_record_source' => $recordSource,
|
||||
'gateway_queue_source' => $queueSource,
|
||||
'submitted' => 0,
|
||||
'duplicate' => $isDuplicate ? 1 : 0,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -260,6 +301,10 @@ class AbdmGatewayPushClient
|
|||
'retryable' => false,
|
||||
'gateway_record_id' => null,
|
||||
'gateway_queue_id' => null,
|
||||
'gateway_record_source' => $recordSource,
|
||||
'gateway_queue_source' => $queueSource,
|
||||
'submitted' => 0,
|
||||
'duplicate' => $isDuplicate ? 1 : 0,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -273,6 +318,10 @@ class AbdmGatewayPushClient
|
|||
'retryable' => false,
|
||||
'gateway_record_id' => null,
|
||||
'gateway_queue_id' => null,
|
||||
'gateway_record_source' => $recordSource,
|
||||
'gateway_queue_source' => $queueSource,
|
||||
'submitted' => 0,
|
||||
'duplicate' => $isDuplicate ? 1 : 0,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ class FhirR4Builder
|
|||
* @param array<string, mixed> $encounter
|
||||
* @param array<int, array<string, mixed>> $medications
|
||||
* @param array<int, array<string, mixed>> $conditions
|
||||
* @param array<string, mixed> $context
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
|
|
@ -24,6 +25,7 @@ class FhirR4Builder
|
|||
$complaints = is_array($context['complaints'] ?? null) ? (array) $context['complaints'] : [];
|
||||
$serviceRequests = is_array($context['service_requests'] ?? null) ? (array) $context['service_requests'] : [];
|
||||
$appointments = is_array($context['appointments'] ?? null) ? (array) $context['appointments'] : [];
|
||||
$attachments = is_array($context['attachments'] ?? null) ? (array) $context['attachments'] : [];
|
||||
|
||||
// UUID-based identity for every resource (ABDM IG v6.5.0 requirement)
|
||||
$bundleUuid = $this->generateUuid();
|
||||
|
|
@ -49,6 +51,7 @@ class FhirR4Builder
|
|||
$medicationRefs = [];
|
||||
$serviceRequestRefs = [];
|
||||
$appointmentRefs = [];
|
||||
$documentRefs = [];
|
||||
|
||||
// Resource entries collected here; Composition prepended at the end.
|
||||
$resourceEntries = [];
|
||||
|
|
@ -475,6 +478,56 @@ class FhirR4Builder
|
|||
$resourceEntries[] = ['fullUrl' => 'urn:uuid:' . $apptUuid, 'resource' => $apptRes];
|
||||
}
|
||||
|
||||
// ── OPD scan/upload attachments (DocumentReference + Binary) ─────────
|
||||
foreach ($attachments as $index => $attachment) {
|
||||
if (! is_array($attachment)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$dataBase64 = trim((string) ($attachment['data_base64'] ?? ''));
|
||||
if ($dataBase64 === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$contentType = trim((string) ($attachment['content_type'] ?? 'application/octet-stream'));
|
||||
$title = trim((string) ($attachment['title'] ?? 'OPD scanned document'));
|
||||
$date = $this->normalizeIsoDateTime((string) ($attachment['date'] ?? ''), $issuedAt);
|
||||
$attachmentId = preg_replace('/[^A-Za-z0-9\-\.]/', '-', trim((string) ($attachment['id'] ?? '')));
|
||||
if ($attachmentId === '' || $attachmentId === null) {
|
||||
$attachmentId = (string) ($index + 1);
|
||||
}
|
||||
|
||||
$docUuid = 'opd-scan-' . $attachmentId . '-' . $this->generateUuid();
|
||||
$binaryUuid = 'binary-' . $docUuid;
|
||||
$docRef = 'urn:uuid:' . $docUuid;
|
||||
$binaryRef = 'urn:uuid:' . $binaryUuid;
|
||||
|
||||
$documentRefs[] = ['reference' => $docRef, 'display' => 'DocumentReference'];
|
||||
$resourceEntries[] = ['fullUrl' => $binaryRef, 'resource' => [
|
||||
'resourceType' => 'Binary',
|
||||
'id' => $binaryUuid,
|
||||
'contentType' => $contentType,
|
||||
'data' => $dataBase64,
|
||||
]];
|
||||
$resourceEntries[] = ['fullUrl' => $docRef, 'resource' => [
|
||||
'resourceType' => 'DocumentReference',
|
||||
'id' => $docUuid,
|
||||
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/DocumentReference']],
|
||||
'status' => 'current',
|
||||
'type' => ['text' => $title],
|
||||
'subject' => ['reference' => $patientRef, 'display' => 'Patient'],
|
||||
'date' => $date,
|
||||
'content' => [[
|
||||
'attachment' => [
|
||||
'contentType' => $contentType,
|
||||
'url' => $binaryRef,
|
||||
'data' => $dataBase64,
|
||||
'title' => $title,
|
||||
],
|
||||
]],
|
||||
]];
|
||||
}
|
||||
|
||||
// ── Composition sections (ABDM SNOMED section codes) ─────────────────
|
||||
$compositionSections = [];
|
||||
if (! empty($complaintRefs)) {
|
||||
|
|
@ -556,6 +609,17 @@ class FhirR4Builder
|
|||
'entry' => $appointmentRefs,
|
||||
];
|
||||
}
|
||||
if (! empty($documentRefs)) {
|
||||
$compositionSections[] = [
|
||||
'title' => 'Document Reference',
|
||||
'code' => ['coding' => [[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => '371530004',
|
||||
'display' => 'Clinical consultation report',
|
||||
]]],
|
||||
'entry' => $documentRefs,
|
||||
];
|
||||
}
|
||||
|
||||
// ── Composition (first entry per ABDM spec) ───────────────────────────
|
||||
$composition = [
|
||||
|
|
|
|||
|
|
@ -297,8 +297,8 @@
|
|||
<td><span class="text-primary small"><?= esc((string) ($r['abha_id'] ?? '')) ?></span></td>
|
||||
<td><?= esc(substr((string) ($r['apointment_date'] ?? ''), 0, 16)) ?></td>
|
||||
<td><?= esc((string) ($r['doc_name'] ?? '')) ?></td>
|
||||
<td><span class="<?= esc($statusBadgeClass) ?>"><?= esc($statusLabel) ?></span></td>
|
||||
<td class="small text-muted" style="min-width:260px;max-width:340px;">
|
||||
<td><span class="<?= esc($statusBadgeClass) ?> opd-consult-status-badge"><?= esc($statusLabel) ?></span></td>
|
||||
<td class="small text-muted opd-consult-details" style="min-width:260px;max-width:340px;">
|
||||
<?php if ($statusNote !== ''): ?>
|
||||
<div><?= esc($statusNote) ?></div>
|
||||
<?php endif; ?>
|
||||
|
|
@ -2423,6 +2423,29 @@
|
|||
if (res.ok == 1) {
|
||||
btn.className = 'btn btn-sm btn-success';
|
||||
btn.innerHTML = '<i class="bi bi-check-circle"></i> Submitted ✓';
|
||||
|
||||
var rowSubmitBtn = document.querySelector('.btn-opd-consult-submit-gateway[data-opd-id="' + String(_fhirOpdId) + '"]');
|
||||
if (rowSubmitBtn) {
|
||||
var row = rowSubmitBtn.closest('tr');
|
||||
if (row) {
|
||||
row.setAttribute('data-push-status', 'queued');
|
||||
var statusBadge = row.querySelector('.opd-consult-status-badge');
|
||||
if (statusBadge) {
|
||||
statusBadge.textContent = 'Submitted';
|
||||
statusBadge.className = 'badge bg-warning text-dark opd-consult-status-badge';
|
||||
}
|
||||
var detailsCell = row.querySelector('.opd-consult-details');
|
||||
if (detailsCell) {
|
||||
var queueHtml = res.queue_id ? ('<div><strong>Queue:</strong> ' + String(res.queue_id) + '</div>') : '';
|
||||
var bridgeHtml = res.bridge_record_id ? ('<div><strong>Bridge:</strong> #' + String(res.bridge_record_id) + '</div>') : '';
|
||||
detailsCell.innerHTML = '<div>Submitted to gateway and waiting for link workflow.</div>' + queueHtml + bridgeHtml;
|
||||
}
|
||||
}
|
||||
rowSubmitBtn.disabled = true;
|
||||
rowSubmitBtn.className = 'btn btn-sm btn-success ms-1';
|
||||
rowSubmitBtn.textContent = 'Submitted';
|
||||
}
|
||||
|
||||
var qInfo = res.queue_id ? ' Queue ID: ' + res.queue_id : '';
|
||||
alert('✅ Bundle submitted to ABDM bridge.\n' + (res.message || '') + qInfo);
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user