fix(abdm): make FHIR creation async local-only in web requests, auto-link every minute via cron, disable cooling delay

This commit is contained in:
DevSoftTech Server 2026-10-08 12:30:47 +05:30
parent a7d83551fe
commit b3e7a2175f
6 changed files with 268 additions and 152 deletions

View File

@ -2,8 +2,10 @@
namespace App\Commands;
use App\Libraries\Abdm\AbdmConnectorFactory;
use App\Libraries\Abdm\Sync\AbdmSyncWorkerService;
use App\Libraries\Abdm\Sync\AbdmTaskBoardSyncService;
use App\Libraries\FhirEncryptionService;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
@ -11,11 +13,11 @@ class AbdmPushSync extends BaseCommand
{
protected $group = 'ABDM';
protected $name = 'abdm:push-sync';
protected $description = 'Process ABDM M2 sync outbox and ABDM Work Task Board care-context linking.';
protected $description = 'Process ABDM M2 sync outbox, ABDM Work Task Board, and pending health_records care-context linking.';
protected $usage = 'abdm:push-sync [--limit 20] [--worker worker-name] [--taskboard-only]';
protected $arguments = [];
protected $options = [
'--limit' => 'Maximum outbox and taskboard rows to process in this run.',
'--limit' => 'Maximum outbox, taskboard, and health_records rows to process in this run.',
'--worker' => 'Worker identifier for lock tracking.',
'--taskboard-only' => 'Skip outbox queue and only sync ABDM Work Task Board records.',
];
@ -46,7 +48,7 @@ class AbdmPushSync extends BaseCommand
CLI::write('Skipped: ' . (int) ($summary['skipped'] ?? 0));
}
// Also sync ABDM Work Task Board records (OPD Consults & Work Queue items)
// 2. Sync ABDM Work Task Board records (OPD Consults & Work Queue items)
CLI::newLine();
CLI::write('ABDM Work Task Board Sync', 'cyan');
$tbService = new AbdmTaskBoardSyncService();
@ -54,5 +56,150 @@ class AbdmPushSync extends BaseCommand
CLI::write('OPD Consults -> Eligible: ' . ($tbSummary['opd']['eligible'] ?? 0) . ' | Linked: ' . ($tbSummary['opd']['linked'] ?? 0) . ' | Cooling: ' . ($tbSummary['opd']['cooling'] ?? 0) . ' | Failed: ' . ($tbSummary['opd']['failed'] ?? 0), 'green');
CLI::write('Work Tasks -> Eligible: ' . ($tbSummary['tasks']['eligible'] ?? 0) . ' | Linked: ' . ($tbSummary['tasks']['linked'] ?? 0) . ' | Cooling: ' . ($tbSummary['tasks']['cooling'] ?? 0) . ' | Failed: ' . ($tbSummary['tasks']['failed'] ?? 0), 'green');
// 3. Process pending health_records directly (instant background sync for newly saved records)
$this->pushPendingHealthRecords($limit);
}
private function pushPendingHealthRecords(int $limit): void
{
$db = \Config\Database::connect();
if (! $db->tableExists('health_records')) {
return;
}
$rows = $db->table('health_records')
->whereNotIn('push_status', ['pushed', 'linked', 'local_only'])
->where('care_context_reference !=', '')
->where('abha_id !=', '')
->orderBy('id', 'ASC')
->get($limit)
->getResultArray();
if (empty($rows)) {
return;
}
CLI::newLine();
CLI::write('--- Pending Health Records Sync (Immediate Cron Link) ---', 'cyan');
CLI::write('Found ' . count($rows) . ' pending record(s) to push...', 'yellow');
$connector = AbdmConnectorFactory::make();
$pushed = 0;
$failed = 0;
foreach ($rows as $row) {
$bundle = $this->resolveFhirBundle($row);
if ($bundle === null) {
CLI::write(' Record #' . $row['id'] . ' (' . ($row['hi_type'] ?? '') . ') - no valid FHIR bundle', 'red');
$failed++;
continue;
}
$patientId = (int) ($row['patient_id'] ?? 0);
$patient = $this->loadPatientDemographics($db, $patientId);
$abhaId = trim((string) ($row['abha_id'] ?? ''));
try {
$result = $connector->pushRecord([
'patient_id' => (string) $patientId,
'patient_name' => $patient['name'] !== '' ? $patient['name'] : ('PATIENT-' . $patientId),
'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'] ?? ''),
'record_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['care_context_display'] ?? (($row['hi_type'] ?? '') . ' ' . substr((string) ($row['created_at'] ?? ''), 0, 10))),
'record_data' => $bundle,
]);
$ok = ! empty($result['ok']) && (int) $result['ok'] === 1;
$httpCode = (int) ($result['http_code'] ?? 0);
$statusVal = strtolower((string) ($result['status'] ?? ''));
if ($ok || in_array($httpCode, [200, 201, 202, 409], true) || in_array($statusVal, ['queued', 'pushed', 'linked', 'duplicate'], true)) {
$db->table('health_records')->where('id', $row['id'])->update([
'push_status' => 'pushed',
'push_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
$pushed++;
CLI::write(' Record #' . $row['id'] . ' (' . ($row['hi_type'] ?? '') . ' ' . ($row['care_context_reference'] ?? '') . ') -> pushed', 'green');
} else {
$failed++;
$errMsg = (string) ($result['message'] ?? $result['error'] ?? 'Push failed');
$db->table('health_records')->where('id', $row['id'])->update([
'push_status' => 'failed',
'updated_at' => date('Y-m-d H:i:s'),
]);
CLI::write(' Record #' . $row['id'] . ' (' . ($row['hi_type'] ?? '') . ') -> failed: ' . $errMsg, 'red');
}
} catch (\Throwable $e) {
$failed++;
CLI::write(' Record #' . $row['id'] . ' (' . ($row['hi_type'] ?? '') . ') -> error: ' . $e->getMessage(), 'red');
}
}
CLI::write('Pending Records: ' . $pushed . ' pushed, ' . $failed . ' failed.', $pushed > 0 ? 'green' : 'yellow');
}
private function resolveFhirBundle(array $row): ?array
{
$plain = trim((string) ($row['fhir_bundle'] ?? $row['record_data'] ?? ''));
if ($plain === '') {
$encrypted = trim((string) ($row['fhir_bundle_enc'] ?? ''));
if ($encrypted !== '') {
try {
$plain = (new FhirEncryptionService())->decrypt($encrypted);
} catch (\Throwable $e) {
$plain = '';
}
}
}
if ($plain === '') {
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;
}
private function loadPatientDemographics($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

@ -153,10 +153,10 @@ class AbdmConnector extends BaseConfig
// ------------------------------------------------------------------
public bool $autoLinkEnabled = true;
/** Default cooling period for OPD, Lab, Radiology, and all other HI types (in minutes). */
public int $autoLinkDelayMinutes = 60;
/** Default cooling period for OPD, Lab, Radiology, and all other HI types (in minutes). 0 = immediate 1-minute cron linking. */
public int $autoLinkDelayMinutes = 0;
/** Cooling period for IPD Discharge Summary (in hours). */
public int $autoLinkDelayDischargeHours = 24;
/** Cooling period for IPD Discharge Summary (in hours). 0 = immediate 1-minute cron linking. */
public int $autoLinkDelayDischargeHours = 0;
}

View File

@ -2127,7 +2127,8 @@ class AbdmGateway extends BaseController
string $abhaId = '',
string $abhaAddressPost = '',
string $consentHandle = '',
string $careContextId = ''
string $careContextId = '',
bool $skipRemotePush = false
): array {
if ($labReqId <= 0) {
return ['ok' => 0, 'error' => 'lab_req_id is required'];
@ -2330,6 +2331,24 @@ class AbdmGateway extends BaseController
'consent_handle' => $effectiveConsent,
]);
if ($skipRemotePush) {
if ($healthRecordId > 0 && $this->db->tableExists('health_records')) {
$this->db->table('health_records')
->where('id', $healthRecordId)
->update([
'push_status' => 'pending',
'updated_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
]);
}
return [
'ok' => 1,
'status' => 'queued_for_cron',
'care_context_reference' => $ccRef,
'health_record_id' => $healthRecordId,
'message' => 'Diagnostic report stored locally and queued for 1-minute cron job link.',
];
}
if (! $hasAbha) {
if ($healthRecordId > 0 && $this->db->tableExists('health_records')) {
$this->db->table('health_records')
@ -10121,7 +10140,7 @@ class AbdmGateway extends BaseController
};
}
public function pushAdditionalHiRecord(array $payload, int $patientId, string $abhaId, string $consentHandle = '')
public function pushAdditionalHiRecord(array $payload, int $patientId, string $abhaId, string $consentHandle = '', bool $skipRemotePush = false)
{
$abhaIdentity = $this->resolvePatientAbhaIdentity($patientId, $abhaId);
$abhaNumber = $abhaIdentity['abha_id'];
@ -10157,6 +10176,24 @@ class AbdmGateway extends BaseController
'consent_handle' => $effectiveConsent,
]);
if ($skipRemotePush) {
if ($healthRecordId > 0 && $this->db->tableExists('health_records')) {
$this->db->table('health_records')->where('id', $healthRecordId)->update([
'push_status' => 'pending',
'updated_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
]);
}
$resp = [
'ok' => 1,
'status' => 'queued_for_cron',
'health_record_id' => $healthRecordId,
'care_context_reference' => $ccRef,
'message' => 'Record saved locally and queued for 1-minute cron job link.',
];
return $this->response !== null ? $this->response->setJSON($resp) : $resp;
}
if ($abhaId === '') {
if ($healthRecordId > 0 && $this->db->tableExists('health_records')) {
$this->db->table('health_records')->where('id', $healthRecordId)->update([
@ -10499,7 +10536,7 @@ class AbdmGateway extends BaseController
case 'diagnostic_report':
case 'lab':
case 'radiology':
return $this->pushDiagnosisReportBundleInternal($entityId, $patientId);
return $this->pushDiagnosisReportBundleInternal($entityId, $patientId, '', '', '', '', true);
case 'discharge':
case 'discharge_summary':
@ -10516,8 +10553,8 @@ class AbdmGateway extends BaseController
if ($payload === null) {
return ['ok' => 0, 'status' => 'payload_null'];
}
$res = $this->pushAdditionalHiRecord($payload, (int) ($payload['patient_id'] ?? $patientId), $abhaId);
return ['ok' => 1, 'status' => 'pushed'];
$res = $this->pushAdditionalHiRecord($payload, (int) ($payload['patient_id'] ?? $patientId), $abhaId, '', true);
return ['ok' => 1, 'status' => 'queued_for_cron'];
case 'invoice':
$abhaId = $patientId > 0 ? $this->resolvePatientAbhaIdentifier($patientId) : '';
@ -10525,8 +10562,8 @@ class AbdmGateway extends BaseController
if ($payload === null) {
return ['ok' => 0, 'status' => 'payload_null'];
}
$res = $this->pushAdditionalHiRecord($payload, (int) ($payload['patient_id'] ?? $patientId), $abhaId);
return ['ok' => 1, 'status' => 'pushed'];
$res = $this->pushAdditionalHiRecord($payload, (int) ($payload['patient_id'] ?? $patientId), $abhaId, '', true);
return ['ok' => 1, 'status' => 'queued_for_cron'];
case 'wellness':
$abhaId = $patientId > 0 ? $this->resolvePatientAbhaIdentifier($patientId) : '';
@ -10534,8 +10571,8 @@ class AbdmGateway extends BaseController
if ($payload === null) {
return ['ok' => 0, 'status' => 'payload_null'];
}
$res = $this->pushAdditionalHiRecord($payload, (int) ($payload['patient_id'] ?? $patientId), $abhaId);
return ['ok' => 1, 'status' => 'pushed'];
$res = $this->pushAdditionalHiRecord($payload, (int) ($payload['patient_id'] ?? $patientId), $abhaId, '', true);
return ['ok' => 1, 'status' => 'queued_for_cron'];
case 'immunization':
$abhaId = $patientId > 0 ? $this->resolvePatientAbhaIdentifier($patientId) : '';
@ -10543,16 +10580,13 @@ class AbdmGateway extends BaseController
if ($payload === null) {
return ['ok' => 0, 'status' => 'payload_null'];
}
$res = $this->pushAdditionalHiRecord($payload, (int) ($payload['patient_id'] ?? $patientId), $abhaId);
$res = $this->pushAdditionalHiRecord($payload, (int) ($payload['patient_id'] ?? $patientId), $abhaId, '', true);
$hrId = 0;
$isOk = false;
if ($res instanceof \CodeIgniter\HTTP\ResponseInterface) {
$resData = json_decode($res->getBody(), true);
$hrId = (int) ($resData['health_record_id'] ?? 0);
$isOk = ! empty($resData['ok']);
} elseif (is_array($res)) {
$hrId = (int) ($res['health_record_id'] ?? 0);
$isOk = ! empty($res['ok']);
}
if ($entityId > 0 && $this->db->tableExists('immunization_records')) {
$this->db->table('immunization_records')
@ -10560,10 +10594,10 @@ class AbdmGateway extends BaseController
->update([
'abdm_care_context_reference' => (string) ($payload['care_context_reference'] ?? ('IMM-' . $entityId)),
'abdm_health_record_id' => $hrId > 0 ? $hrId : null,
'abdm_push_status' => $isOk ? 'linked' : 'queued',
'abdm_push_status' => 'queued',
]);
}
return ['ok' => 1, 'status' => 'pushed', 'health_record_id' => $hrId, 'care_context_reference' => $payload['care_context_reference']];
return ['ok' => 1, 'status' => 'queued_for_cron', 'health_record_id' => $hrId, 'care_context_reference' => $payload['care_context_reference']];
default:
return ['ok' => 0, 'status' => 'unknown_doc_type', 'message' => 'Unknown document type: ' . $docType];
@ -10596,105 +10630,22 @@ class AbdmGateway extends BaseController
return ['ok' => 0, 'status' => 'no_abha', 'message' => 'Patient has no ABHA'];
}
// Ensure FHIR bundle is built and stored
// Ensure FHIR bundle is built and stored locally
if (class_exists('\App\Controllers\Opd_prescription')) {
try {
$rxCtrl = new \App\Controllers\Opd_prescription();
$rxCtrl->storePrescriptionFhirBundle($opdId, $sessionId, $patientRow, $opdRow);
$rxCtrl->createOpdPrescriptionWorkTask($opdId, $sessionId);
} catch (\Throwable $e) {
log_message('warning', '[autoPushPrescriptionRecord] storePrescriptionFhirBundle error: ' . $e->getMessage());
}
}
if ($sessionId <= 0 && $this->db->tableExists('opd_prescription')) {
$presRow = $this->db->table('opd_prescription')->select('id')->where('opd_id', $opdId)->orderBy('id', 'DESC')->get(1)->getRowArray();
if (! empty($presRow['id'])) {
$sessionId = (int) $presRow['id'];
}
}
$abhaIdentity = $this->resolvePatientAbhaIdentity($patientId, $abhaId);
$abhaNumber = $abhaIdentity['abha_id'];
$abhaAddress = $abhaIdentity['abha_address'];
$patientBirthYear = $this->resolvePatientBirthYear($patientRow, $abhaAddress, $abhaNumber);
$patientName = $this->patientDisplayName($patientRow);
$visitDateRaw = trim((string) ($opdRow['date_opd_visit'] ?? $opdRow['apointment_date'] ?? ''));
$visitDate = $visitDateRaw !== '' ? date('Y-m-d', strtotime($visitDateRaw)) : date('Y-m-d');
$cleanDate = str_replace('-', '', $visitDate);
$results = [];
// 1. Push OPConsultRecord
$opcDoc = $this->db->table('opd_fhir_documents')
->where('opd_id', $opdId)
->where('bundle_type', 'OPConsultRecord')
->orderBy('id', 'DESC')
->get(1)
->getRowArray();
if (! empty($opcDoc['bundle_json'])) {
$opcBundle = json_decode((string) $opcDoc['bundle_json'], true);
if (is_array($opcBundle)) {
$ccRef = 'OPD-' . $patientId . '-S' . $sessionId . '-' . $cleanDate;
$ccDisplay = 'OPD Consultation - ' . date('d M Y', strtotime($visitDate));
try {
$res = $this->connector->pushRecord([
'patient_id' => (string) $patientId,
'patient_name' => $patientName,
'abha_id' => $abhaNumber,
'abha_address' => $abhaAddress,
'year_of_birth' => $patientBirthYear,
'hi_type' => 'OPConsultRecord',
'record_type' => 'OPConsultRecord',
'visit_date' => $visitDate,
'care_context_reference' => $ccRef,
'care_context_display' => $ccDisplay,
'notes' => $ccDisplay,
'queue_id' => $ccRef,
'record_data' => $opcBundle,
]);
$results['opd'] = $res;
} catch (\Throwable $e) {
$results['opd_error'] = $e->getMessage();
}
}
}
// 2. Push PrescriptionRecord
$prescDoc = $this->db->table('opd_fhir_documents')
->where('opd_id', $opdId)
->where('bundle_type', 'PrescriptionRecord')
->orderBy('id', 'DESC')
->get(1)
->getRowArray();
if (! empty($prescDoc['bundle_json'])) {
$prescBundle = json_decode((string) $prescDoc['bundle_json'], true);
if (is_array($prescBundle)) {
$prescCcRef = 'PRESC-' . $patientId . '-S' . $sessionId . '-' . $cleanDate;
$prescCcDisplay = 'Prescription - ' . date('d M Y', strtotime($visitDate));
try {
$res = $this->connector->pushRecord([
'patient_id' => (string) $patientId,
'patient_name' => $patientName,
'abha_id' => $abhaNumber,
'abha_address' => $abhaAddress,
'year_of_birth' => $patientBirthYear,
'hi_type' => 'PrescriptionRecord',
'record_type' => 'PrescriptionRecord',
'visit_date' => $visitDate,
'care_context_reference' => $prescCcRef,
'care_context_display' => $prescCcDisplay,
'notes' => $prescCcDisplay,
'queue_id' => $prescCcRef,
'record_data' => $prescBundle,
]);
$results['prescription'] = $res;
} catch (\Throwable $e) {
$results['prescription_error'] = $e->getMessage();
}
}
}
return ['ok' => 1, 'status' => 'processed', 'results' => $results];
return [
'ok' => 1,
'status' => 'queued_for_cron',
'message' => 'Prescription FHIR bundle saved and queued for 1-minute cron linking',
];
}
private function buildHealthDocumentRecordPayload(int $patientId, string $abhaId = '', int $recordId = 0, array $patientRow = []): ?array

View File

@ -2130,17 +2130,6 @@ class Opd extends BaseController
return $this->response->setStatusCode(404)->setBody('OPD not found');
}
// Auto create/update FHIR bundle when printing consult form
try {
$resolvedSessionId = $sessionId > 0 ? $sessionId : (int) ($data['opd_prescription']->id ?? 0);
$prescCtrl = new \App\Controllers\Opd_prescription();
$patientRowArr = ! empty($data['patient_master'][0]) ? (array) $data['patient_master'][0] : null;
$opdRowArr = ! empty($data['opd_master'][0]) ? (array) $data['opd_master'][0] : null;
$prescCtrl->storePrescriptionFhirBundle($opdId, $resolvedSessionId, $patientRowArr, $opdRowArr);
} catch (\Throwable $fhirEx) {
log_message('warning', '[opd_lettre_print] FHIR bundle sync on print warning: ' . $fhirEx->getMessage());
}
$data['print_layout_mode'] = $layoutMode;
return view('billing/opd_letter_head', $data);
@ -2176,17 +2165,6 @@ class Opd extends BaseController
return $this->response->setStatusCode(404)->setBody('OPD not found');
}
// Auto create/update FHIR bundle when generating consult form PDF
try {
$resolvedSessionId = $sessionId > 0 ? $sessionId : (int) ($data['opd_prescription']->id ?? 0);
$prescCtrl = new \App\Controllers\Opd_prescription();
$patientRowArr = ! empty($data['patient_master'][0]) ? (array) $data['patient_master'][0] : null;
$opdRowArr = ! empty($data['opd_master'][0]) ? (array) $data['opd_master'][0] : null;
$prescCtrl->storePrescriptionFhirBundle($opdId, $resolvedSessionId, $patientRowArr, $opdRowArr);
} catch (\Throwable $fhirEx) {
log_message('warning', '[opd_lettre_pdf] FHIR bundle sync on PDF print warning: ' . $fhirEx->getMessage());
}
$data['print_layout_mode'] = $layoutMode;
$isComposedTemplate = $templateKey !== '' && str_starts_with($templateKey, 'compose_');

View File

@ -12057,16 +12057,7 @@ class Opd_prescription extends BaseController
$this->db->table('health_records')
->whereIn('care_context_reference', $redundantRefs)
->delete();
// If patient has ABHA, auto-push to Bridge Gateway so care context is linked immediately
try {
$hasValidAbha = (str_contains($abhaAddress, '@') || (strlen(preg_replace('/\D/', '', $abhaAddress)) === 14));
if ($hasValidAbha) {
\App\Controllers\AbdmGateway::autoPushRecord('prescription', $opdId, $patientId, ['session_id' => $sessionId]);
}
} catch (\Throwable $e) {
// Fail-safe
}
// Record is stored with push_status = pending and queued for the 1-minute cron job
} catch (\Throwable $e) {
log_message('warning', '[storePrescriptionFhirBundle] auto health_records insert error: ' . $e->getMessage());
}
@ -13961,12 +13952,6 @@ OPD SNAPSHOT JSON: " . $payload;
$this->createOpdPrescriptionWorkTask($opdId, $opdSessionId);
$this->markOpdVisitedOnConsultCompletion((int) $opdId);
// Auto-link/push prescription to ABDM if patient has ABHA (fail-safe)
try {
\App\Controllers\AbdmGateway::autoPushRecord('prescription', $opdId, 0, ['session_id' => $opdSessionId]);
} catch (\Throwable $e) {
log_message('warning', 'ABDM Prescription print auto-link failed: ' . $e->getMessage());
}
$printConfig = $this->resolvePrescriptionLayoutByDoctorField($opdId, $printType);
$layoutMode = (string) ($printConfig['layout'] ?? 'content_only');

View File

@ -635,6 +635,8 @@ class AbdmTaskBoardSyncService
'immunization_record_publish',
'wellness_record_publish',
'health_document_publish',
'invoice_record_publish',
'invoice_publish',
'ipd_discharge_publish',
];
@ -806,6 +808,7 @@ class AbdmTaskBoardSyncService
'immunization_record_publish' => 'ImmunizationRecord',
'wellness_record_publish' => 'WellnessRecord',
'health_document_publish' => 'HealthDocumentRecord',
'invoice_record_publish', 'invoice_publish' => 'InvoiceRecord',
'ipd_discharge_publish' => 'DischargeSummaryRecord',
default => 'HealthDocumentRecord',
};
@ -825,6 +828,7 @@ class AbdmTaskBoardSyncService
'immunization_record_publish' => 'IMM-',
'wellness_record_publish' => 'WELLNESS-',
'health_document_publish' => 'DOC-',
'invoice_record_publish', 'invoice_publish' => 'INV-',
'ipd_discharge_publish' => 'DISCHARGE-',
default => 'REC-',
};
@ -935,6 +939,45 @@ class AbdmTaskBoardSyncService
log_message('warning', '[processIndividualWorkTask] Immunization bundle generation error: ' . $ie->getMessage());
}
}
} elseif (in_array($taskType, ['invoice_record_publish', 'invoice_publish'], true)) {
if (empty($pushData['bundle']) && class_exists('\App\Controllers\AbdmGateway')) {
try {
$gw = new \App\Controllers\AbdmGateway();
$invPayload = $gw->buildInvoiceRecordPayload((int) $entityId, $patientId, $abhaAddress !== '' ? $abhaAddress : $abhaNumber);
if (! empty($invPayload['bundle'])) {
$pushData['bundle'] = $invPayload['bundle'];
$pushData['fhir_bundle'] = $invPayload['bundle'];
$pushData['record_data'] = $invPayload['bundle'];
$bundleJson = (string) json_encode($invPayload['bundle'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
} catch (\Throwable $inve) {
log_message('warning', '[processIndividualWorkTask] Invoice bundle generation error: ' . $inve->getMessage());
}
}
}
// Generic fallback: check if bundle already exists in health_records
if (empty($pushData['bundle']) && $this->db->tableExists('health_records')) {
$hr = $this->db->table('health_records')
->where('entity_id', (string) $entityId)
->where('patient_id', $patientId)
->orderBy('id', 'DESC')
->get(1)
->getRowArray();
if (! empty($hr['fhir_bundle'])) {
$decoded = json_decode((string) $hr['fhir_bundle'], true);
if (is_array($decoded) && ! empty($decoded)) {
$pushData['bundle'] = $decoded;
$pushData['fhir_bundle'] = $decoded;
$pushData['record_data'] = $decoded;
$bundleJson = (string) $hr['fhir_bundle'];
if (! empty($hr['care_context_reference'])) {
$careContextRef = (string) $hr['care_context_reference'];
$pushData['care_context_reference'] = $careContextRef;
$pushData['queue_id'] = $careContextRef;
}
}
}
}
try {
@ -1093,13 +1136,25 @@ class AbdmTaskBoardSyncService
$typeLower = strtolower(trim($taskType));
if (in_array($typeLower, ['ipd_discharge_publish', 'ipd_discharge', 'dischargesummaryrecord', 'ipd'], true)) {
$hours = max(1, (int) ($cfg->autoLinkDelayDischargeHours ?? 24));
$hours = max(0, (int) ($cfg->autoLinkDelayDischargeHours ?? 0));
$requiredSeconds = $hours * 3600;
} else {
$minutes = max(1, (int) ($cfg->autoLinkDelayMinutes ?? 60));
$minutes = max(0, (int) ($cfg->autoLinkDelayMinutes ?? 0));
$requiredSeconds = $minutes * 60;
}
if ($requiredSeconds <= 0) {
return [
'is_cooling_active' => false,
'cooling_active' => false,
'remaining_seconds' => 0,
'remaining_minutes' => 0,
'required_seconds' => 0,
'last_modified' => (string) $lastModified,
'auto_link_at' => date('Y-m-d H:i:s'),
];
}
$lastTimestamp = ! empty($lastModified) ? strtotime($lastModified) : 0;
if ($lastTimestamp <= 0) {
$lastTimestamp = time();