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 = = (int) ($patient->id ?? 0) ?>;
var abdmDocumentsUrl = '= base_url('billing/patient/abdm_documents/' . (int) ($patient->id ?? 0)) ?>';
var abdmAutoFlowUrl = '= base_url('billing/patient/abdm_content_auto_flow/' . (int) ($patient->id ?? 0)) ?>';
+ var abdmFetchOnlyUrl = '= base_url('billing/patient/abdm_content_fetch_only/' . (int) ($patient->id ?? 0)) ?>';
var abdmDocDetailBaseUrl = '= base_url('billing/patient/abdm_document_detail/' . (int) ($patient->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();