diff --git a/app/Commands/AbdmSyncDrugs.php b/app/Commands/AbdmSyncDrugs.php new file mode 100644 index 0000000..48172cb --- /dev/null +++ b/app/Commands/AbdmSyncDrugs.php @@ -0,0 +1,195 @@ + 'Number of pages to sync (default: 6)', + '--limit' => 'Items per page (default: 500, max: 1000)', + '--dry-run' => 'Simulate sync without writing to database', + ]; + + public function run(array $params) + { + $pages = (int) ($params['pages'] ?? CLI::getOption('pages') ?? 6); + $limit = (int) ($params['limit'] ?? CLI::getOption('limit') ?? 500); + $dryRun = (bool) (CLI::getOption('dry-run') ?? false); + + if ($pages <= 0) { + $pages = 6; + } + if ($limit <= 0 || $limit > 1000) { + $limit = 500; + } + + CLI::write("==================================================", 'cyan'); + CLI::write(" ABDM Pharmacy Drug Master Sync (Sync-Pack)", 'yellow'); + CLI::write(" Pages: {$pages} | Limit per page: {$limit} | Mode: " . ($dryRun ? 'DRY-RUN' : 'LIVE'), 'cyan'); + CLI::write("==================================================\n", 'cyan'); + + $db = \Config\Database::connect(); + if (! $db->tableExists('med_product_master')) { + CLI::error("Table 'med_product_master' does not exist in database."); + return; + } + + $connector = new \App\Libraries\Abdm\EAtriaBridgeConnector(); + + $totalFetched = 0; + $totalInserted = 0; + $totalUpdated = 0; + $totalSkipped = 0; + $now = date('Y-m-d H:i:s'); + + for ($p = 1; $p <= $pages; $p++) { + CLI::print("Fetching page {$p} of {$pages}... ", 'yellow'); + try { + $response = $connector->drugsSyncPack($p, $limit); + } catch (\Throwable $e) { + CLI::write("FAILED: " . $e->getMessage(), 'red'); + break; + } + + if (($response['ok'] ?? 0) !== 1) { + CLI::write("FAILED: " . ($response['error_text'] ?? $response['message'] ?? 'API error'), 'red'); + break; + } + + $items = $response['items'] ?? []; + $count = count($items); + CLI::write("OK ({$count} items)", 'green'); + + if ($count === 0) { + CLI::write("No more items returned. Finishing sync.", 'yellow'); + break; + } + + $totalFetched += $count; + + if ($dryRun) { + continue; + } + + foreach ($items as $item) { + $identifier = trim((string) ($item['identifier'] ?? '')); + $displayName = trim((string) ($item['display_name'] ?? '')); + $brandName = trim((string) ($item['brand_name'] ?? '')); + $genericName = trim((string) ($item['generic_name'] ?? '')); + $dosageForm = trim((string) ($item['short_formulation'] ?? $item['formulation'] ?? $item['dosage_form'] ?? '')); + $manufacturer = trim((string) ($item['manufacturer_name'] ?? $item['company_name'] ?? '')); + $type = trim((string) ($item['type'] ?? 'generic')); + + if ($identifier === '' && $displayName === '') { + $totalSkipped++; + continue; + } + + // Clean Product Name + $itemName = $this->deriveCleanItemName($displayName, $brandName, $dosageForm); + if ($itemName === '') { + $itemName = $displayName ?: $genericName; + } + + // Check if already exists by abdm_drug_identifier + $existing = null; + if ($identifier !== '') { + $existing = $db->table('med_product_master') + ->where('abdm_drug_identifier', $identifier) + ->get() + ->getRowArray(); + } + + // If not found by identifier, check by exact item_name + if (! $existing && $itemName !== '') { + $existing = $db->table('med_product_master') + ->where('item_name', $itemName) + ->get() + ->getRowArray(); + } + + $recordData = [ + 'item_name' => $itemName, + 'genericname' => $genericName ?: $displayName, + 'formulation' => $dosageForm ?: 'Tablet', + 'company_name' => $manufacturer, + 'abdm_drug_type' => $type ?: 'generic', + 'abdm_drug_identifier' => $identifier, + 'abdm_drug_display' => $displayName, + 'abdm_drug_generic' => $genericName, + 'abdm_drug_payload_json' => json_encode($item, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'abdm_drug_last_synced_at' => $now, + 'is_continue' => 1, + 'batch_applicable' => 1, + 'exp_date_applicable' => 1, + 'high_risk' => ! empty($item['high_risk']) ? 1 : 0, + ]; + + if (! empty($item['hsn_code'])) { + $recordData['HSNCODE'] = (string) $item['hsn_code']; + } + if (isset($item['cgst_percent']) && is_numeric($item['cgst_percent'])) { + $recordData['CGST_per'] = (float) $item['cgst_percent']; + } + if (isset($item['sgst_percent']) && is_numeric($item['sgst_percent'])) { + $recordData['SGST_per'] = (float) $item['sgst_percent']; + } + + if ($existing) { + $db->table('med_product_master')->where('id', $existing['id'])->update($recordData); + $totalUpdated++; + } else { + $recordData['insert_by'] = 'abdm_sync'; + $db->table('med_product_master')->insert($recordData); + $totalInserted++; + } + } + + $pagination = $response['pagination'] ?? []; + if (! empty($pagination) && empty($pagination['has_next'])) { + CLI::write("Reached last page.", 'yellow'); + break; + } + } + + CLI::write("\n--------------------------------------------------", 'cyan'); + CLI::write(" Sync Finished Successfully!", 'green'); + CLI::write(" Total Fetched: {$totalFetched}", 'cyan'); + CLI::write(" Total Inserted: {$totalInserted}", 'green'); + CLI::write(" Total Updated: {$totalUpdated}", 'yellow'); + CLI::write(" Total Skipped: {$totalSkipped}", 'white'); + CLI::write("--------------------------------------------------\n", 'cyan'); + } + + private function deriveCleanItemName(string $displayName, string $brandName, string $formulation): string + { + $label = trim($displayName); + if ($label === '') { + return $brandName; + } + + if (preg_match('/^([^()]+?)\s*\(([^)]+)\)\s*(.*)$/i', $label, $m)) { + $before = trim($m[1]); + $after = trim($m[3]); + $cleanAfter = preg_replace('/\b(oral|route|for\s+oral\s+use)\b/i', '', $after); + $cleanAfter = preg_replace('/\s+/', ' ', trim($cleanAfter)); + if ($formulation !== '' && $cleanAfter !== '') { + $re = '/' . preg_quote($formulation, '/') . '/i'; + $cleanAfter = preg_replace($re, ucfirst($formulation), $cleanAfter); + } + if ($cleanAfter !== '') { + return trim($before . ' ' . $cleanAfter); + } + return $before; + } + + return $label; + } +} diff --git a/app/Controllers/Medical.php b/app/Controllers/Medical.php index b1afe78..6eabc17 100644 --- a/app/Controllers/Medical.php +++ b/app/Controllers/Medical.php @@ -6252,90 +6252,180 @@ class Medical extends BaseController $limit = 10; } + // ------------------------------------------------------------- + // Step B: Hybrid Model - 1. Local-First Search (HMS Database) + // ------------------------------------------------------------- + $localSuggestions = []; + $seenIdentifiers = []; + + if ($this->db->tableExists('med_product_master')) { + $localBuilder = $this->db->table('med_product_master'); + $localBuilder->groupStart(); + $localBuilder->like('item_name', $q, 'after'); + $localBuilder->orLike('item_name', $q); + $localBuilder->orLike('genericname', $q); + $localBuilder->orLike('abdm_drug_display', $q); + $localBuilder->groupEnd(); + $localBuilder->where('is_continue', 1); + $localBuilder->limit($limit); + + $localRows = $localBuilder->get()->getResultArray(); + foreach ($localRows as $lr) { + $id = trim((string) ($lr['abdm_drug_identifier'] ?? '')); + $name = trim((string) ($lr['item_name'] ?? '')); + $disp = trim((string) ($lr['abdm_drug_display'] ?? '')) ?: $name; + $gen = trim((string) ($lr['genericname'] ?? '')); + $form = trim((string) ($lr['formulation'] ?? '')); + $comp = trim((string) ($lr['company_name'] ?? '')); + + if ($id !== '') { + $seenIdentifiers[$id] = true; + } + if ($name !== '') { + $seenIdentifiers[strtolower($name)] = true; + } + + $localSuggestions[] = [ + 'label' => $disp, + 'display_name' => $disp, + 'brand_name' => $name, + 'generic_name' => $gen, + 'identifier' => $id ?: ('HMS-' . $lr['id']), + 'type' => (string) ($lr['abdm_drug_type'] ?? 'brand'), + 'dosage_form' => $form, + 'route' => '', + 'manufacturer_name' => $comp, + 'strength_text' => '', + 'pack_size_text' => (string) ($lr['packing'] ?? ''), + 'source' => 'local', + ]; + } + } + + // If local database has full quota of results, return immediately (1-2ms!) + if (count($localSuggestions) >= $limit) { + return $this->response->setJSON([ + 'ok' => 1, + 'suggestions' => $localSuggestions, + 'source' => 'local', + 'error_text' => '', + ]); + } + + // ------------------------------------------------------------- + // Step B: Hybrid Model - 2. Gateway Fallback / Complement + // ------------------------------------------------------------- $cacheKey = 'abdm_drug_ac_v2_' . md5($type . '_' . strtolower($q) . '_' . $limit); $cached = cache($cacheKey); + $gatewaySuggestions = []; + if (is_array($cached) && ! empty($cached) && ($cached['ok'] ?? 0) === 1) { - return $this->response->setJSON($cached); + $gatewaySuggestions = $cached['suggestions'] ?? []; + } else { + try { + $connector = new \App\Libraries\Abdm\EAtriaBridgeConnector(); + $result = $connector->drugsAutocomplete($q, $type, $limit); + $items = $this->extractDrugResultList($result); + foreach ($items as $row) { + if (! is_array($row)) { + continue; + } + + $displayName = $this->extractDrugText($row, [ + 'display_name', 'drug_display_name', 'product_name', 'medicine_name', 'med_name', + 'brand_name', 'trade_name', 'drug_name', 'name', 'display', 'term', 'title', 'label', + ]); + $brandName = $this->extractDrugText($row, ['brand_name', 'trade_name']); + $label = $this->extractDrugText($row, [ + 'label', 'display_name', 'name', 'display', 'term', 'title', 'drug_name', 'product_name', + 'medicine_name', 'brand_name', 'trade_name', + ]); + $identifier = $this->extractDrugText($row, ['identifier', 'id', 'code', 'concept_id', 'value']); + $generic = $this->extractDrugText($row, ['generic_name', 'genericname', 'generic', 'salt', 'molecule', 'composition']); + $itemType = strtolower($this->extractDrugText($row, ['type', 'entity_type', 'category'])); + if ($itemType === '') { + $itemType = $type; + } + + if ($displayName !== '') { + $label = $displayName; + } elseif ($brandName !== '') { + $label = $brandName; + } elseif ($label === '' || $this->isDrugIdentifierLike($label)) { + $label = $identifier; + } + + if ($label === '' && $identifier === '') { + continue; + } + + $dosageForm = $this->extractDrugText($row, ['dosage_form', 'formulation', 'short_formulation', 'drug_form']); + $route = $this->extractDrugText($row, ['route', 'administration_route']); + $manufacturer = $this->extractDrugText($row, ['manufacturer_name', 'manufacturer', 'company_name', 'company']); + $strength = $this->extractDrugText($row, ['strength_text', 'strength']); + $packSize = $this->extractDrugText($row, ['pack_size_text', 'pack_size', 'package', 'packing']); + + $gatewaySuggestions[] = [ + 'label' => $label, + 'display_name' => $displayName, + 'brand_name' => $brandName, + 'generic_name' => $generic, + 'identifier' => $identifier, + 'type' => $itemType, + 'dosage_form' => $dosageForm, + 'route' => $route, + 'manufacturer_name' => $manufacturer, + 'strength_text' => $strength, + 'pack_size_text' => $packSize, + 'source' => 'gateway', + ]; + } + + if (! empty($gatewaySuggestions)) { + cache()->save($cacheKey, [ + 'ok' => 1, + 'suggestions' => $gatewaySuggestions, + ], 86400); + } + } catch (\Throwable $e) { + if (! empty($localSuggestions)) { + return $this->response->setJSON([ + 'ok' => 1, + 'suggestions' => $localSuggestions, + 'source' => 'local', + 'error_text' => '', + ]); + } + return $this->response->setJSON([ + 'ok' => 0, + 'suggestions' => [], + 'error_text' => $e->getMessage(), + ]); + } } - try { - $connector = new \App\Libraries\Abdm\EAtriaBridgeConnector(); - $result = $connector->drugsAutocomplete($q, $type, $limit); - } catch (\Throwable $e) { - return $this->response->setJSON([ - 'ok' => 0, - 'suggestions' => [], - 'error_text' => $e->getMessage(), - ]); - } - - $items = $this->extractDrugResultList($result); - $suggestions = []; - foreach ($items as $row) { - if (! is_array($row)) { + // Merge local and gateway suggestions, deduplicating by identifier / name + $merged = $localSuggestions; + foreach ($gatewaySuggestions as $gs) { + $gid = $gs['identifier'] ?? ''; + $gname = strtolower($gs['brand_name'] ?? ''); + if ($gid !== '' && isset($seenIdentifiers[$gid])) { continue; } - - $displayName = $this->extractDrugText($row, [ - 'display_name', 'drug_display_name', 'product_name', 'medicine_name', 'med_name', - 'brand_name', 'trade_name', 'drug_name', 'name', 'display', 'term', 'title', 'label', - ]); - $brandName = $this->extractDrugText($row, ['brand_name', 'trade_name']); - $label = $this->extractDrugText($row, [ - 'label', 'display_name', 'name', 'display', 'term', 'title', 'drug_name', 'product_name', - 'medicine_name', 'brand_name', 'trade_name', - ]); - $identifier = $this->extractDrugText($row, ['identifier', 'id', 'code', 'concept_id', 'value']); - $generic = $this->extractDrugText($row, ['generic_name', 'genericname', 'generic', 'salt', 'molecule', 'composition']); - $itemType = strtolower($this->extractDrugText($row, ['type', 'entity_type', 'category'])); - if ($itemType === '') { - $itemType = $type; - } - - if ($displayName !== '') { - $label = $displayName; - } elseif ($brandName !== '') { - $label = $brandName; - } elseif ($label === '' || $this->isDrugIdentifierLike($label)) { - $label = $identifier; - } - - if ($label === '' && $identifier === '') { + if ($gname !== '' && isset($seenIdentifiers[$gname])) { continue; } - - $dosageForm = $this->extractDrugText($row, ['dosage_form', 'formulation', 'short_formulation', 'drug_form']); - $route = $this->extractDrugText($row, ['route', 'administration_route']); - $manufacturer = $this->extractDrugText($row, ['manufacturer_name', 'manufacturer', 'company_name', 'company']); - $strength = $this->extractDrugText($row, ['strength_text', 'strength']); - $packSize = $this->extractDrugText($row, ['pack_size_text', 'pack_size', 'package', 'packing']); - - $suggestions[] = [ - 'label' => $label, - 'display_name' => $displayName, - 'brand_name' => $brandName, - 'generic_name' => $generic, - 'identifier' => $identifier, - 'type' => $itemType, - 'dosage_form' => $dosageForm, - 'route' => $route, - 'manufacturer_name' => $manufacturer, - 'strength_text' => $strength, - 'pack_size_text' => $packSize, - ]; + $merged[] = $gs; + if (count($merged) >= $limit) { + break; + } } - $responsePayload = [ - 'ok' => (int) ($result['ok'] ?? 0), - 'suggestions' => $suggestions, - 'error_text' => (string) ($result['error_text'] ?? $result['message'] ?? ''), - ]; - - if (($responsePayload['ok'] ?? 0) === 1 && ! empty($suggestions)) { - cache()->save($cacheKey, $responsePayload, 86400); - } - - return $this->response->setJSON($responsePayload); + return $this->response->setJSON([ + 'ok' => 1, + 'suggestions' => $merged, + 'error_text' => '', + ]); } public function drug_terminology_detail() @@ -6451,6 +6541,38 @@ class Medical extends BaseController if (($detailPayload['ok'] ?? 0) === 1 && ! empty($detailPayload['selected'])) { cache()->save($cacheKey, $detailPayload, 86400); + + // Step B: Auto-save / link in med_product_master on selection + if ($canonicalId !== '' && $this->db->tableExists('med_product_master')) { + try { + $existingProd = $this->db->table('med_product_master') + ->where('abdm_drug_identifier', $canonicalId) + ->get() + ->getRowArray(); + + if (! $existingProd) { + $cleanName = $displayName ?: ($brandName ?: $label); + $this->db->table('med_product_master')->insert([ + 'item_name' => $cleanName, + 'genericname' => $generic ?: $displayName, + 'formulation' => $dosageForm ?: ($formulation ?: 'Tablet'), + 'company_name' => $manufacturerName, + 'abdm_drug_type' => $itemType ?: 'brand', + 'abdm_drug_identifier' => $canonicalId, + 'abdm_drug_display' => $displayName ?: $label, + 'abdm_drug_generic' => $generic, + 'abdm_drug_payload_json' => $payloadJson, + 'abdm_drug_last_synced_at' => date('Y-m-d H:i:s'), + 'is_continue' => 1, + 'batch_applicable' => 1, + 'exp_date_applicable' => 1, + 'insert_by' => 'abdm_selection', + ]); + } + } catch (\Throwable $e) { + // Non-blocking auto-save + } + } } return $this->response->setJSON($detailPayload); diff --git a/app/Libraries/Abdm/EAtriaBridgeConnector.php b/app/Libraries/Abdm/EAtriaBridgeConnector.php index 1636cea..62afd8a 100644 --- a/app/Libraries/Abdm/EAtriaBridgeConnector.php +++ b/app/Libraries/Abdm/EAtriaBridgeConnector.php @@ -2019,6 +2019,19 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface return $this->get('/v3/drugs/version', $query); } + public function drugsSyncPack(int $page = 1, int $limit = 500): array + { + $query = [ + 'page' => max(1, $page), + 'limit' => max(1, min(1000, $limit)), + ]; + if ($this->hfrId !== '') { + $query['hfr_id'] = $this->hfrId; + } + + return $this->get('/v3/drugs/sync-pack', $query); + } + // ------------------------------------------------------------------------- // Pathology Terminology (LOINC) // -------------------------------------------------------------------------