From becff3592931cc290ed8389ddd3dabb7a38ce781 Mon Sep 17 00:00:00 2001 From: DevSoftTech Server Date: Tue, 28 Jul 2026 04:34:08 +0530 Subject: [PATCH] ABDM OPD tab: fix duplicate fetched-document rows + add dedicated Fetch Records action - M3HiuDocumentRepository::persistFromDataFetch no longer includes the per-call request_id in the document dedup hash. request_id is unique every time data_fetch runs, so it was causing a brand-new duplicate row to be inserted on every re-fetch of the exact same ABDM document. Now dedup key is transaction_id + consent_ref + care_context_reference + bundle_id, which stays stable across repeated fetches -> re-fetches UPDATE the existing row instead of duplicating it. - Added Patient::abdm_content_fetch_only() + route billing/patient/abdm_content_fetch_only/(id): a lightweight action for when consent is already GRANTED/COMPLETED. It calls data_fetch directly using the saved consent reference, skipping consent_request + reconcile polling entirely, avoiding unnecessary bridge calls/noise. - Patient_Profile_Opd_V.php: added a 'Fetch Records' button that only appears once consent phase is GRANTED or COMPLETED, wired to the new endpoint; on success it refreshes the document list in place. The original 'Fetch ABDM Records' button still owns the full consent request -> reconcile -> fetch flow and already stops/re-enables correctly on DENIED/FAILED (verified, no change needed there). - One-time cleanup: deduped existing abdm_hiu_documents rows in dev DB (13 -> 3) and recomputed doc_hash with the new stable key. --- app/Config/Routes.php | 1 + app/Controllers/Patient.php | 97 +++++++++++++++++++ .../Abdm/M3HiuDocumentRepository.php | 7 +- app/Views/billing/Patient_Profile_Opd_V.php | 68 ++++++++++++- check_after_screenshot.php | 16 +++ check_hiu_documents.php | 15 +++ check_patient11_latest.php | 22 +++++ check_patient_master_devender.php | 11 +++ check_rows_80_89.php | 17 ++++ dedupe_abdm_documents.php | 52 ++++++++++ find_abdm_tables.php | 16 +++ find_request_6dbbe21b.php | 24 +++++ verify_no_dupes.php | 6 ++ 13 files changed, 350 insertions(+), 2 deletions(-) create mode 100644 check_after_screenshot.php create mode 100644 check_hiu_documents.php create mode 100644 check_patient11_latest.php create mode 100644 check_patient_master_devender.php create mode 100644 check_rows_80_89.php create mode 100644 dedupe_abdm_documents.php create mode 100644 find_abdm_tables.php create mode 100644 find_request_6dbbe21b.php create mode 100644 verify_no_dupes.php diff --git a/app/Config/Routes.php b/app/Config/Routes.php index b3b0b10..b9d779e 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -500,6 +500,7 @@ $routes->group('billing', function($routes) { $routes->get('patient/abdm_document_detail/(:num)/(:num)', 'Patient::abdm_document_detail/$1/$2'); $routes->get('patient/abdm_content_request/(:num)', 'Patient::abdm_content_request/$1'); $routes->get('patient/abdm_content_auto_flow/(:num)', 'Patient::abdm_content_auto_flow/$1'); + $routes->get('patient/abdm_content_fetch_only/(:num)', 'Patient::abdm_content_fetch_only/$1'); $routes->get('patient/abdm_timeline/(:num)', 'Patient::abdm_timeline/$1'); $routes->post('patient/save_profile_image/(:num)', 'Patient::save_profile_image/$1'); $routes->match(['GET', 'POST'], 'patient/patient_file_upload/(:num)', 'Patient::patient_file_upload/$1'); diff --git a/app/Controllers/Patient.php b/app/Controllers/Patient.php index a56896f..4b24971 100644 --- a/app/Controllers/Patient.php +++ b/app/Controllers/Patient.php @@ -1756,6 +1756,103 @@ class Patient extends BaseController ]); } + /** + * Fetch-only action for a patient whose consent is already GRANTED. + * Unlike abdm_content_auto_flow(), this does NOT create a new consent + * request and does NOT re-run consent reconcile polling — it directly + * pulls health data using the already-saved consent reference. This + * avoids spamming the bridge with duplicate consent_request/reconcile + * calls just to re-fetch already-granted records. + */ + public function abdm_content_fetch_only(int $pno) + { + if (! $this->db->tableExists('patient_master')) { + return $this->response->setStatusCode(500)->setJSON([ + 'ok' => 0, + 'error' => 'patient_master table not found', + ]); + } + + $patientRow = $this->db->table('patient_master')->where('id', $pno)->get()->getRowArray(); + if (! is_array($patientRow)) { + return $this->response->setStatusCode(404)->setJSON([ + 'ok' => 0, + 'error' => 'Patient not found', + ]); + } + + $abhaContext = $this->buildAbhaProfileContext($patientRow); + if ($abhaContext['abha_address'] === '') { + return $this->response->setStatusCode(422)->setJSON([ + 'ok' => 0, + 'error' => 'ABHA Address not available for this patient.', + ]); + } + + $lastSync = $this->getLatestAbdmSyncSnapshot((string) ($abhaContext['abha_address'] ?? '')); + $phase = strtoupper(trim((string) ($lastSync['phase'] ?? ''))); + if (! in_array($phase, ['GRANTED', 'COMPLETED'], true)) { + return $this->response->setStatusCode(422)->setJSON([ + 'ok' => 0, + 'phase' => $phase !== '' ? $phase : 'IDLE', + 'error' => 'Consent is not granted yet. Use "Fetch ABDM Records" to send/track a consent request first.', + ]); + } + + $consentRequestRef = trim((string) ($lastSync['consent_request_id'] ?? '')); + $consentArtifactRef = trim((string) ($lastSync['consent_id'] ?? '')); + if ($consentArtifactRef === '' && $consentRequestRef === '') { + return $this->response->setStatusCode(422)->setJSON([ + 'ok' => 0, + 'phase' => $phase, + 'error' => 'No saved consent reference found. Use "Fetch ABDM Records" to re-establish consent.', + ]); + } + + $service = new \App\Libraries\Abdm\M3HiuWorkflowService(); + $fetchPayload = [ + 'abha_address' => $abhaContext['abha_address'], + ]; + if ($consentArtifactRef !== '') { + $fetchPayload['consentId'] = $consentArtifactRef; + } else { + $fetchPayload['consentRequestId'] = $consentRequestRef; + } + + try { + $fetchResult = $service->runOperation('data_fetch', $fetchPayload); + } catch (\Throwable $e) { + return $this->response->setStatusCode(500)->setJSON([ + 'ok' => 0, + 'phase' => 'FETCH_FAILED', + 'error' => 'Unable to fetch ABDM content: ' . $e->getMessage(), + ]); + } + + if ((int) ($fetchResult['ok'] ?? 0) !== 1) { + $httpCode = (int) ($fetchResult['http_code'] ?? 422); + if ($httpCode < 100 || $httpCode > 599) { + $httpCode = 422; + } + + return $this->response->setStatusCode($httpCode)->setJSON([ + 'ok' => 0, + 'phase' => 'FETCH_FAILED', + 'error' => (string) ($fetchResult['error_text'] ?? 'Data fetch failed.'), + 'data' => ['data_fetch' => $fetchResult], + ]); + } + + return $this->response->setJSON([ + 'ok' => 1, + 'phase' => 'COMPLETED', + 'message' => 'Records fetched successfully using existing granted consent.', + 'documents_persisted' => (int) ($fetchResult['documents_persisted'] ?? 0), + 'documents_updated' => (int) ($fetchResult['documents_updated'] ?? 0), + 'data' => ['data_fetch' => $fetchResult], + ]); + } + public function abdm_timeline(int $pno) { if (! $this->db->tableExists('patient_master')) { diff --git a/app/Libraries/Abdm/M3HiuDocumentRepository.php b/app/Libraries/Abdm/M3HiuDocumentRepository.php index e666999..965770d 100644 --- a/app/Libraries/Abdm/M3HiuDocumentRepository.php +++ b/app/Libraries/Abdm/M3HiuDocumentRepository.php @@ -60,8 +60,13 @@ class M3HiuDocumentRepository $resolvedAbha = $abhaAddress !== '' ? $abhaAddress : trim((string) ($summary['abha_address'] ?? '')); $patientMap = $this->mapPatientByAbha($resolvedAbha, (string) ($summary['abha_number'] ?? '')); + // NOTE: request_id is intentionally excluded from the dedup key. + // It is unique per polling/fetch call, so including it here would + // create a brand-new duplicate row every time the same document is + // re-fetched (e.g. repeated "Fetch ABDM Records" clicks or retries). + // transaction_id + consent_ref + care_context_reference + bundle_id + // identify the same underlying document across repeated fetches. $docHash = sha1(implode('|', [ - $requestId, $transactionId, $consentRef, $careContextRef, diff --git a/app/Views/billing/Patient_Profile_Opd_V.php b/app/Views/billing/Patient_Profile_Opd_V.php index 79465b8..475729e 100644 --- a/app/Views/billing/Patient_Profile_Opd_V.php +++ b/app/Views/billing/Patient_Profile_Opd_V.php @@ -316,6 +316,10 @@ if ($patientPhotoPath === '') { Fetch ABDM Records +
@@ -421,6 +425,7 @@ $(function() { var patientId = id ?? 0) ?>; var abdmDocumentsUrl = 'id ?? 0)) ?>'; var abdmAutoFlowUrl = 'id ?? 0)) ?>'; + var abdmFetchOnlyUrl = 'id ?? 0)) ?>'; var abdmDocDetailBaseUrl = 'id ?? 0)) ?>'; var currentAbdmRows = []; var autoFlowTimer = null; @@ -429,6 +434,7 @@ $(function() { var autoFlowMaxAttempts = 15; var autoFlowNeedsFreshRequest = false; var autoFlowRunning = false; + var fetchOnlyRunning = false; function updateConsentBadge(phase, needsFresh) { var text = (phase || '').toString().toUpperCase(); @@ -457,11 +463,24 @@ $(function() { } function applyConsentButtonState() { - $('#btnAutoAbdmFlow').prop('disabled', autoFlowRunning); + $('#btnAutoAbdmFlow').prop('disabled', autoFlowRunning || fetchOnlyRunning); $('#btnAutoAbdmFlow .abdm-btn-spinner').toggleClass('d-none', !autoFlowRunning); $('#btnAutoAbdmFlowLabel').text(autoFlowRunning ? 'Fetching...' : 'Fetch ABDM Records'); } + function applyFetchOnlyButtonState() { + $('#btnFetchRecordsOnly').prop('disabled', autoFlowRunning || fetchOnlyRunning); + $('#btnFetchRecordsOnly .abdm-btn-spinner').toggleClass('d-none', !fetchOnlyRunning); + $('#btnFetchRecordsOnlyLabel').text(fetchOnlyRunning ? 'Fetching...' : 'Fetch Records'); + } + + // Only show the standalone "Fetch Records" action once consent has actually + // been granted (or already completed once) — it re-pulls data using the + // saved consent reference without creating a brand-new consent request. + function showFetchRecordsButton(show) { + $('#btnFetchRecordsOnly').toggleClass('d-none', !show); + } + var zoom = 1; var minZoom = 0.5; var maxZoom = 3; @@ -677,6 +696,7 @@ $(function() { setFlowProgress(0); } + showFetchRecordsButton(lastPhase === 'GRANTED' || lastPhase === 'COMPLETED'); updateConsentBadge(lastPhase, autoFlowNeedsFreshRequest); var baseMsg = 'Loaded ' + currentAbdmRows.length + ' fetched ABDM record(s).'; @@ -763,6 +783,7 @@ $(function() { } else if (phase === 'REQUESTED' || phase === 'PENDING') { setFlowProgress(1); } + showFetchRecordsButton(phase === 'GRANTED' || phase === 'COMPLETED'); updateConsentBadge(phase, autoFlowNeedsFreshRequest); $('#abdmStatusBox').removeClass('text-danger').addClass('text-muted').text(info + (autoFlowRequestId ? (' | Request ID: ' + autoFlowRequestId) : '')); @@ -815,6 +836,46 @@ $(function() { }); } + // Standalone fetch: consent is already GRANTED, so this skips consent_request + // and reconcile polling entirely and just pulls data using the saved consent + // reference. Used by the "Fetch Records" button once consent is granted. + function runFetchOnlyStep() { + if (fetchOnlyRunning || autoFlowRunning) { + return; + } + fetchOnlyRunning = true; + applyFetchOnlyButtonState(); + applyConsentButtonState(); + $('#abdmStatusBox').removeClass('text-danger').addClass('text-muted').text('Fetching latest records using existing granted consent...'); + + fetch(abdmFetchOnlyUrl, { credentials: 'same-origin' }) + .then(function(resp) { return resp.json().then(function(data) { return { status: resp.status, data: data }; }); }) + .then(function(wrapped) { + var data = wrapped.data; + if (!data || data.ok !== 1) { + throw new Error((data && data.error) || 'Fetch failed.'); + } + + setFlowProgress(3); + showFetchRecordsButton(true); + updateConsentBadge('COMPLETED', false); + + var info = (data.message || 'Records fetched successfully.').toString(); + $('#abdmStatusBox').removeClass('text-danger').addClass('text-muted').text(info); + + fetchOnlyRunning = false; + applyFetchOnlyButtonState(); + applyConsentButtonState(); + loadAbdmDocs(); + }) + .catch(function(err) { + fetchOnlyRunning = false; + applyFetchOnlyButtonState(); + applyConsentButtonState(); + $('#abdmStatusBox').removeClass('text-muted').addClass('text-danger').text('Fetch failed: ' + (err.message || err)); + }); + } + $(document).off('click.abdmOpd', '#btnLoadAbdmDocs').on('click.abdmOpd', '#btnLoadAbdmDocs', function() { loadAbdmDocs(); }); @@ -854,6 +915,10 @@ $(function() { runAutoFlowStep(); }); + $(document).off('click.abdmOpd', '#btnFetchRecordsOnly').on('click.abdmOpd', '#btnFetchRecordsOnly', function() { + runFetchOnlyStep(); + }); + $(document).off('shown.bs.tab.abdmOpd', '[data-bs-target="#opd-abdm-tab"]').on('shown.bs.tab.abdmOpd', '[data-bs-target="#opd-abdm-tab"]', function() { if (!currentAbdmRows.length) { loadAbdmDocs(); @@ -863,6 +928,7 @@ $(function() { $('[data-bs-toggle="tooltip"]').tooltip(); applyConsentButtonState(); + applyFetchOnlyButtonState(); updateConsentBadge('IDLE', false); }); diff --git a/check_after_screenshot.php b/check_after_screenshot.php new file mode 100644 index 0000000..d287067 --- /dev/null +++ b/check_after_screenshot.php @@ -0,0 +1,16 @@ +query("SELECT MAX(id) maxid FROM abdm_hiu_workflows")->fetch_assoc()); + +echo "\n=== rows id > 89 for abha 91510165305101@sbx ===\n"; +$res = $db->query("SELECT id, operation, workflow_state, status, http_code, request_id, gateway_request_id, created_at FROM abdm_hiu_workflows WHERE abha_address = '91510165305101@sbx' AND id > 89 ORDER BY id ASC"); +while ($row = $res->fetch_assoc()) { print_r($row); } + +echo "\n=== search for 6dbbe21b anywhere ===\n"; +$res2 = $db->query("SELECT id, operation, request_id, gateway_request_id, created_at FROM abdm_hiu_workflows WHERE request_id LIKE '%6dbbe21b%' OR gateway_request_id LIKE '%6dbbe21b%'"); +while ($row = $res2->fetch_assoc()) { print_r($row); } + +echo "\n=== doc count for abha now ===\n"; +print_r($db->query("SELECT COUNT(*) c FROM abdm_hiu_documents WHERE abha_address = '91510165305101@sbx'")->fetch_assoc()); +$db->close(); diff --git a/check_hiu_documents.php b/check_hiu_documents.php new file mode 100644 index 0000000..36cad14 --- /dev/null +++ b/check_hiu_documents.php @@ -0,0 +1,15 @@ +query("SHOW COLUMNS FROM abdm_hiu_documents"); +while ($row = $res->fetch_assoc()) { echo $row['Field'] . "\n"; } + +echo "\n=== rows for abha 91510165305101@sbx ===\n"; +$res2 = $db->query("SELECT id, workflow_id, request_id, transaction_id, consent_ref, abha_address, patient_id, patient_name, care_context_reference, document_title, document_date, created_at, updated_at FROM abdm_hiu_documents WHERE abha_address = '91510165305101@sbx'"); +while ($row = $res2->fetch_assoc()) { print_r($row); } + +echo "\n=== total row count in table ===\n"; +$res3 = $db->query("SELECT COUNT(*) c FROM abdm_hiu_documents"); +print_r($res3->fetch_assoc()); +$db->close(); diff --git a/check_patient11_latest.php b/check_patient11_latest.php new file mode 100644 index 0000000..bfadb49 --- /dev/null +++ b/check_patient11_latest.php @@ -0,0 +1,22 @@ +query("SHOW COLUMNS FROM abdm_hiu_workflows"); +while ($row = $res->fetch_assoc()) { echo $row['Field'] . " (" . $row['Type'] . ")\n"; } + +echo "\n=== Latest 20 rows for {$abha} ===\n"; +$res2 = $db->query("SELECT * FROM abdm_hiu_workflows WHERE abha_address = '{$abha}' ORDER BY id DESC LIMIT 20"); +while ($row = $res2->fetch_assoc()) { + echo "--- id={$row['id']} ---\n"; + foreach ($row as $k => $v) { + if (in_array($k, ['request_payload', 'response_payload', 'raw_response'])) { + echo "$k: " . substr((string)$v, 0, 500) . "\n"; + } else { + echo "$k: $v\n"; + } + } + echo "\n"; +} +$db->close(); diff --git a/check_patient_master_devender.php b/check_patient_master_devender.php new file mode 100644 index 0000000..47e61cc --- /dev/null +++ b/check_patient_master_devender.php @@ -0,0 +1,11 @@ +query("SELECT id, p_fname, p_lname FROM patient_master WHERE p_fname LIKE '%Devender%' OR p_fname LIKE '%devender%'"); +while ($row = $res->fetch_assoc()) { print_r($row); } + +echo "\n=== Search abha columns (guess col names) ===\n"; +$res2 = $db->query("SHOW COLUMNS FROM patient_master LIKE '%abha%'"); +while ($row = $res2->fetch_assoc()) { echo $row['Field'] . "\n"; } +$db->close(); diff --git a/check_rows_80_89.php b/check_rows_80_89.php new file mode 100644 index 0000000..a12388e --- /dev/null +++ b/check_rows_80_89.php @@ -0,0 +1,17 @@ +query("SELECT * FROM abdm_hiu_workflows WHERE abha_address = '91510165305101@sbx' AND id >= 80 ORDER BY id ASC"); +while ($row = $res->fetch_assoc()) { + echo "--- id={$row['id']} ---\n"; + foreach ($row as $k => $v) { + if (in_array($k, ['request_json', 'response_json'])) { + echo "$k: " . substr((string)$v, 0, 900) . "\n"; + } else { + echo "$k: $v\n"; + } + } + echo "\n"; +} +$db->close(); diff --git a/dedupe_abdm_documents.php b/dedupe_abdm_documents.php new file mode 100644 index 0000000..5b45c3f --- /dev/null +++ b/dedupe_abdm_documents.php @@ -0,0 +1,52 @@ +query("SELECT COUNT(*) c FROM abdm_hiu_documents")->fetch_assoc()); + +// Group by the new dedup key (transaction_id, consent_ref, care_context_reference, bundle_id) +// and keep only the newest row (highest id) per group; delete the rest. +$res = $db->query(" + SELECT transaction_id, consent_ref, care_context_reference, bundle_id, GROUP_CONCAT(id ORDER BY id DESC) ids + FROM abdm_hiu_documents + GROUP BY transaction_id, consent_ref, care_context_reference, bundle_id + HAVING COUNT(*) > 1 +"); + +$totalDeleted = 0; +while ($row = $res->fetch_assoc()) { + $ids = explode(',', $row['ids']); + $keepId = array_shift($ids); // newest (first in DESC order) + if (empty($ids)) { + continue; + } + $idsList = implode(',', array_map('intval', $ids)); + $db->query("DELETE FROM abdm_hiu_documents WHERE id IN ($idsList)"); + $totalDeleted += $db->affected_rows; + echo "Kept id={$keepId}, deleted ids=[{$idsList}]\n"; +} + +echo "\nTotal deleted: {$totalDeleted}\n"; + +// Recompute doc_hash for all remaining rows using the new (request_id-free) formula, +// so future fetches correctly match and UPDATE instead of INSERT duplicates. +echo "\n=== Recomputing doc_hash for remaining rows ===\n"; +$res2 = $db->query("SELECT id, transaction_id, consent_ref, care_context_reference, bundle_id FROM abdm_hiu_documents"); +$updated = 0; +while ($row = $res2->fetch_assoc()) { + $hash = sha1(implode('|', [ + (string) $row['transaction_id'], + (string) $row['consent_ref'], + (string) $row['care_context_reference'], + (string) $row['bundle_id'], + ])); + $escapedHash = $db->real_escape_string($hash); + $db->query("UPDATE abdm_hiu_documents SET doc_hash = '{$escapedHash}' WHERE id = " . (int) $row['id']); + $updated++; +} +echo "Recomputed hash for {$updated} rows.\n"; + +echo "\n=== After cleanup: total rows ===\n"; +print_r($db->query("SELECT COUNT(*) c FROM abdm_hiu_documents")->fetch_assoc()); + +$db->close(); diff --git a/find_abdm_tables.php b/find_abdm_tables.php new file mode 100644 index 0000000..2e9c85e --- /dev/null +++ b/find_abdm_tables.php @@ -0,0 +1,16 @@ +query("SELECT MAX(id) maxid FROM abdm_hiu_workflows"); +print_r($res->fetch_assoc()); + +echo "\n=== Show tables like %health% or %record% or %m3% or %hiu% ===\n"; +$res2 = $db->query("SHOW TABLES"); +while ($row = $res2->fetch_array()) { + $t = $row[0]; + if (stripos($t, 'health') !== false || stripos($t, 'record') !== false || stripos($t, 'm3') !== false || stripos($t, 'hiu') !== false || stripos($t, 'abdm') !== false) { + echo "$t\n"; + } +} +$db->close(); diff --git a/find_request_6dbbe21b.php b/find_request_6dbbe21b.php new file mode 100644 index 0000000..e5d9e4b --- /dev/null +++ b/find_request_6dbbe21b.php @@ -0,0 +1,24 @@ +query("SELECT * FROM abdm_hiu_workflows WHERE request_id LIKE '%6dbbe21b%' OR gateway_request_id LIKE '%6dbbe21b%' OR hms_request_id LIKE '%6dbbe21b%'"); +$found = 0; +while ($row = $res->fetch_assoc()) { + $found++; + echo "--- id={$row['id']} ---\n"; + foreach ($row as $k => $v) { + if (in_array($k, ['request_json', 'response_json'])) { + echo "$k: " . substr((string)$v, 0, 800) . "\n"; + } else { + echo "$k: $v\n"; + } + } + echo "\n"; +} +echo "Found: $found\n"; + +echo "\n=== Count of rows for abha 91510165305101@sbx ===\n"; +$res2 = $db->query("SELECT COUNT(*) c, MAX(id) maxid FROM abdm_hiu_workflows WHERE abha_address = '91510165305101@sbx'"); +print_r($res2->fetch_assoc()); +$db->close(); diff --git a/verify_no_dupes.php b/verify_no_dupes.php new file mode 100644 index 0000000..23426dd --- /dev/null +++ b/verify_no_dupes.php @@ -0,0 +1,6 @@ +query("SELECT COUNT(*) c FROM abdm_hiu_documents WHERE abha_address = '91510165305101@sbx'")->fetch_assoc()); +$res = $db->query("SELECT id, request_id, care_context_reference, updated_at FROM abdm_hiu_documents WHERE abha_address = '91510165305101@sbx'"); +while ($row = $res->fetch_assoc()) { print_r($row); } +$db->close();