diff --git a/app/Config/AbdmConnector.php b/app/Config/AbdmConnector.php new file mode 100644 index 0000000..bf93cf6 --- /dev/null +++ b/app/Config/AbdmConnector.php @@ -0,0 +1,51 @@ + + * abdm.directClientSecret = + * abdm.directBaseUrl = https://dev.abdm.gov.in/api/v3 + * abdm.directTimeoutSec = 30 + */ +class AbdmConnector extends BaseConfig +{ + /** + * Which adapter to use. + * 'dreamsoft' -> DreamsoftConnector (current default — routes via Dreamsoft bridge) + * 'direct_abdm' -> DirectAbdmConnector (future — calls NHA ABDM APIs directly) + */ + public string $connector = 'dreamsoft'; + + // ------------------------------------------------------------------ + // DirectAbdmConnector settings + // Unused when $connector = 'dreamsoft'. + // ------------------------------------------------------------------ + + /** ABDM M3 base URL. Sandbox: https://dev.abdm.gov.in/api/v3 */ + public string $directAbdmBaseUrl = 'https://dev.abdm.gov.in/api/v3'; + + /** + * ABDM M3 client ID. + * Set in .env: abdm.directClientId = YOUR_CLIENT_ID + */ + public string $directAbdmClientId = ''; + + /** + * ABDM M3 client secret. + * Set in .env: abdm.directClientSecret = YOUR_CLIENT_SECRET + * Do NOT commit real credentials to source control. + */ + public string $directAbdmClientSecret = ''; + + /** HTTP timeout in seconds for direct ABDM API calls. */ + public int $directAbdmTimeoutSec = 30; +} diff --git a/app/Controllers/AbdmGateway.php b/app/Controllers/AbdmGateway.php index 2077aa9..6bce5bc 100644 --- a/app/Controllers/AbdmGateway.php +++ b/app/Controllers/AbdmGateway.php @@ -2,12 +2,24 @@ namespace App\Controllers; -use App\Libraries\BridgeSyncService; +use App\Libraries\Abdm\AbdmConnectorInterface; +use App\Libraries\Abdm\AbdmConnectorFactory; use App\Libraries\FhirR4Builder; use CodeIgniter\I18n\Time; class AbdmGateway extends BaseController { + private AbdmConnectorInterface $connector; + + public function initController( + \CodeIgniter\HTTP\RequestInterface $request, + \CodeIgniter\HTTP\ResponseInterface $response, + \Psr\Log\LoggerInterface $logger + ) { + parent::initController($request, $response, $logger); + $this->connector = AbdmConnectorFactory::make(); + } + public function abhaValidate() { if (! $this->request->isAJAX()) { @@ -26,8 +38,8 @@ class AbdmGateway extends BaseController $queueId = null; try { - $bridge = new BridgeSyncService(); - $queueId = $bridge->enqueue('abdm.abha.validate', $payload, 'abha', $abhaId); + $result = $this->connector->validateAbha($abhaId, $payload); + $queueId = $result['queue_id'] ?? null; } catch (\Throwable $e) { } @@ -85,8 +97,8 @@ class AbdmGateway extends BaseController $queueId = null; try { - $bridge = new BridgeSyncService(); - $queueId = $bridge->enqueue('abdm.consent.requested', $rawPayload, 'consent', $consentHandle); + $result = $this->connector->requestConsent($patientId, $abhaId, $purposeCode, $expiresAt, $consentHandle, $rawPayload); + $queueId = $result['queue_id'] ?? null; } catch (\Throwable $e) { } @@ -200,8 +212,8 @@ class AbdmGateway extends BaseController $queueId = null; try { - $bridge = new BridgeSyncService(); - $queueId = $bridge->enqueue('abdm.scan_share.lookup', $payload, 'abha_scan', $abhaId !== '' ? $abhaId : null); + $result = $this->connector->scanShareLookup($qrPayload, $abhaId, $payload); + $queueId = $result['queue_id'] ?? null; } catch (\Throwable $e) { } @@ -272,8 +284,8 @@ class AbdmGateway extends BaseController $queueId = null; try { - $bridge = new BridgeSyncService(); - $queueId = $bridge->enqueue('abdm.fhir.share.requested', $payload, 'opd_fhir_document', (string) ($bundleRow['id'] ?? '')); + $result = $this->connector->sharePrescriptionBundle($payload, (string) ($bundleRow['id'] ?? '')); + $queueId = $result['queue_id'] ?? null; } catch (\Throwable $e) { } @@ -337,8 +349,8 @@ class AbdmGateway extends BaseController $queueId = null; try { - $bridge = new BridgeSyncService(); - $queueId = $bridge->enqueue('abdm.ipd.discharge.share.requested', $payload, 'ipd_discharge', (string) $ipdId); + $result = $this->connector->shareIpdDischargeBundle($payload, (string) $ipdId); + $queueId = $result['queue_id'] ?? null; } catch (\Throwable $e) { } @@ -393,8 +405,12 @@ class AbdmGateway extends BaseController 'consent_handle' => $consentRecord['consent_handle'] ?? $consentHandle, ]; - $bridge = new BridgeSyncService(); - $queueId = $bridge->enqueue('abdm.diagnosis.report.share.requested', $payload, 'lab_request', (string) $labReqId); + $queueId = null; + try { + $result = $this->connector->shareDiagnosisReportBundle($payload, (string) $labReqId); + $queueId = $result['queue_id'] ?? null; + } catch (\Throwable $e) { + } return $this->response->setJSON([ 'ok' => 1, @@ -478,18 +494,8 @@ class AbdmGateway extends BaseController $documentId = (int) $this->db->insertID(); $queueId = null; try { - $bridge = new BridgeSyncService(); - $queueId = $bridge->enqueue( - 'nhcx.claim.created', - [ - 'nhcx_claim_document_id' => $documentId, - 'patient_id' => $patientId, - 'encounter_id' => $encounterId, - 'bundle' => $bundle, - ], - 'nhcx_claim_document', - (string) $documentId - ); + $result = $this->connector->nhcxClaimCreate($bundle, $documentId, $patientId, $encounterId); + $queueId = $result['queue_id'] ?? null; } catch (\Throwable $e) { } @@ -530,8 +536,8 @@ class AbdmGateway extends BaseController $queueId = null; try { - $bridge = new BridgeSyncService(); - $queueId = $bridge->enqueue('nhcx.claim.status.requested', $payload, 'nhcx_claim_document', (string) $documentId); + $result = $this->connector->nhcxClaimStatusRequest($documentId, $payload['external_ref'], $payload['claim_status'], $payload); + $queueId = $result['queue_id'] ?? null; } catch (\Throwable $e) { } diff --git a/app/Libraries/Abdm/AbdmConnectorFactory.php b/app/Libraries/Abdm/AbdmConnectorFactory.php new file mode 100644 index 0000000..8ebe13f --- /dev/null +++ b/app/Libraries/Abdm/AbdmConnectorFactory.php @@ -0,0 +1,45 @@ +validateAbha('12345678901234'); + * + * Configure via Config\AbdmConnector::$connector or .env: + * abdm.connector = dreamsoft # default — Dreamsoft bridge middleware + * abdm.connector = direct_abdm # future — direct NHA/ABDM API calls + */ +class AbdmConnectorFactory +{ + /** + * Build and return the configured connector adapter. + * + * @throws \InvalidArgumentException if the connector name is unknown + */ + public static function make(): AbdmConnectorInterface + { + $config = config('AbdmConnector'); + $connector = strtolower(trim((string) ($config->connector ?? 'dreamsoft'))); + + switch ($connector) { + case 'dreamsoft': + return new DreamsoftConnector(); + + case 'direct_abdm': + case 'direct': + return new DirectAbdmConnector(); + + default: + throw new \InvalidArgumentException( + "AbdmConnectorFactory: unknown connector '{$connector}'. " + . "Valid values: 'dreamsoft', 'direct_abdm'." + ); + } + } +} diff --git a/app/Libraries/Abdm/AbdmConnectorInterface.php b/app/Libraries/Abdm/AbdmConnectorInterface.php new file mode 100644 index 0000000..19d7848 --- /dev/null +++ b/app/Libraries/Abdm/AbdmConnectorInterface.php @@ -0,0 +1,133 @@ + DreamsoftConnector (routes through Dreamsoft bridge — current) + * 'direct_abdm' -> DirectAbdmConnector (calls ABDM APIs directly — future) + * + * All methods return a uniform response array: + * Success: ['ok' => 1, 'queue_id' => int|null, 'status' => string, ...] + * Failure: ['ok' => 0, 'error_text' => string] + * + * Controllers never depend on the adapter implementation: + * $connector = AbdmConnectorFactory::make(); + * $result = $connector->validateAbha($abhaId); + */ +interface AbdmConnectorInterface +{ + /** Human-readable adapter name: 'dreamsoft' | 'direct_abdm' */ + public function getConnectorName(): string; + + // ------------------------------------------------------------------------- + // ABHA + // ------------------------------------------------------------------------- + + /** + * Validate / look up a 14-digit ABHA number. + * + * @param array $fullPayload Full payload built by the controller + * (must contain at minimum 'abha_id' key) + */ + public function validateAbha(string $abhaId, array $fullPayload = []): array; + + // ------------------------------------------------------------------------- + // Consent + // ------------------------------------------------------------------------- + + /** + * Initiate a consent request for a patient. + * + * @param array $rawPayload Full raw payload to pass to the bridge + */ + public function requestConsent( + int $patientId, + string $abhaId, + string $purposeCode, + string $expiresAt, + string $consentHandle, + array $rawPayload = [] + ): array; + + // ------------------------------------------------------------------------- + // Health Record Sharing + // ------------------------------------------------------------------------- + + /** + * Share a prescription / OPD FHIR bundle. + * + * Expected payload keys: + * opd_id, opd_session_id, patient_id, abha_id, + * consent_handle, bundle_type, bundle (FHIR array) + * + * @param array $payload + */ + public function sharePrescriptionBundle(array $payload, string $entityId = ''): array; + + /** + * Share an IPD discharge summary bundle. + * + * Expected payload keys: + * ipd_id, patient_id, abha_id, consent_handle, + * ipd_code, register_date, discharge_date, discharge_time, summary_html + * + * @param array $payload + */ + public function shareIpdDischargeBundle(array $payload, string $entityId = ''): array; + + /** + * Share a diagnostic / lab report bundle. + * + * Expected payload keys: + * lab_req_id, patient_id, patient_name, abha_id, lab_type, + * invoice_id, report_html, impression, report_status, reported_time, consent_handle + * + * @param array $payload + */ + public function shareDiagnosisReportBundle(array $payload, string $entityId = ''): array; + + // ------------------------------------------------------------------------- + // Scan & Share + // ------------------------------------------------------------------------- + + /** + * Look up a patient using QR payload from Scan & Share flow. + * + * @param array $fullPayload Full payload built by the controller + */ + public function scanShareLookup(string $qrPayload, string $abhaIdHint = '', array $fullPayload = []): array; + + // ------------------------------------------------------------------------- + // NHCX Claims + // ------------------------------------------------------------------------- + + /** + * Submit a new NHCX claim bundle. + * + * @param array $bundle FHIR Claim bundle from FhirR4Builder + */ + public function nhcxClaimCreate( + array $bundle, + int $documentId, + int $patientId, + int $encounterId + ): array; + + /** + * Request a status update on an existing NHCX claim. + * + * @param array $fullPayload Full payload built by the controller + */ + public function nhcxClaimStatusRequest( + int $documentId, + string $externalRef, + string $currentStatus, + array $fullPayload = [] + ): array; +} diff --git a/app/Libraries/Abdm/DirectAbdmConnector.php b/app/Libraries/Abdm/DirectAbdmConnector.php new file mode 100644 index 0000000..c0f20f3 --- /dev/null +++ b/app/Libraries/Abdm/DirectAbdmConnector.php @@ -0,0 +1,158 @@ +baseUrl = rtrim((string) ($config->directAbdmBaseUrl ?? ''), '/'); + $this->clientId = (string) ($config->directAbdmClientId ?? ''); + $this->clientSecret = (string) ($config->directAbdmClientSecret ?? ''); + $this->timeoutSec = (int) ($config->directAbdmTimeoutSec ?? 30); + } + + public function getConnectorName(): string + { + return 'direct_abdm'; + } + + // ------------------------------------------------------------------------- + // ABHA + // ------------------------------------------------------------------------- + + public function validateAbha(string $abhaId, array $fullPayload = []): array + { + // TODO: Obtain M3 access token, then call: + // POST {baseUrl}/v1/search/existsByHealthId + // Body: { "healthId": $abhaId } + // Reference: ABDM M3 ABHA search API + throw new \RuntimeException( + 'DirectAbdmConnector::validateAbha() not implemented. ' + . 'TODO: POST /v1/search/existsByHealthId with ABHA ID.' + ); + } + + // ------------------------------------------------------------------------- + // Consent + // ------------------------------------------------------------------------- + + public function requestConsent( + int $patientId, + string $abhaId, + string $purposeCode, + string $expiresAt, + string $consentHandle, + array $rawPayload = [] + ): array { + // TODO: Obtain M3 access token, then call: + // POST {baseUrl}/v0.5/consent-requests/init + // Body: ConsentRequest FHIR resource per ABDM IG + throw new \RuntimeException( + 'DirectAbdmConnector::requestConsent() not implemented. ' + . 'TODO: POST /v0.5/consent-requests/init.' + ); + } + + // ------------------------------------------------------------------------- + // Health Record Sharing + // ------------------------------------------------------------------------- + + public function sharePrescriptionBundle(array $payload, string $entityId = ''): array + { + // TODO: Encrypt FHIR bundle using HIP public key, then call: + // POST {baseUrl}/v0.5/health-information/hip/on-request + // Reference: ABDM IG — HIP-initiated record push + throw new \RuntimeException( + 'DirectAbdmConnector::sharePrescriptionBundle() not implemented. ' + . 'TODO: Encrypt bundle and POST /v0.5/health-information/hip/on-request.' + ); + } + + public function shareIpdDischargeBundle(array $payload, string $entityId = ''): array + { + // TODO: Same flow as sharePrescriptionBundle but bundle_type = DischargeSummaryBundle + throw new \RuntimeException( + 'DirectAbdmConnector::shareIpdDischargeBundle() not implemented. ' + . 'TODO: Encrypt bundle and POST /v0.5/health-information/hip/on-request.' + ); + } + + public function shareDiagnosisReportBundle(array $payload, string $entityId = ''): array + { + // TODO: Same flow as sharePrescriptionBundle but bundle_type = DiagnosticReportBundle + throw new \RuntimeException( + 'DirectAbdmConnector::shareDiagnosisReportBundle() not implemented. ' + . 'TODO: Encrypt bundle and POST /v0.5/health-information/hip/on-request.' + ); + } + + // ------------------------------------------------------------------------- + // Scan & Share + // ------------------------------------------------------------------------- + + public function scanShareLookup(string $qrPayload, string $abhaIdHint = '', array $fullPayload = []): array + { + // TODO: Decode QR token, then call: + // POST {baseUrl}/v1.0/hip/patient/scan-share/profile + // Body: { "token": $qrPayload } + throw new \RuntimeException( + 'DirectAbdmConnector::scanShareLookup() not implemented. ' + . 'TODO: POST /v1.0/hip/patient/scan-share/profile with decoded QR token.' + ); + } + + // ------------------------------------------------------------------------- + // NHCX Claims + // ------------------------------------------------------------------------- + + public function nhcxClaimCreate( + array $bundle, + int $documentId, + int $patientId, + int $encounterId + ): array { + // TODO: Obtain NHCX access token (separate from ABDM), then call: + // POST {nhcxBaseUrl}/v0.5/nhcx/claim + // Body: FHIR Claim bundle + throw new \RuntimeException( + 'DirectAbdmConnector::nhcxClaimCreate() not implemented. ' + . 'TODO: POST NHCX /v0.5/nhcx/claim with FHIR Claim bundle.' + ); + } + + public function nhcxClaimStatusRequest( + int $documentId, + string $externalRef, + string $currentStatus, + array $fullPayload = [] + ): array { + // TODO: POST {nhcxBaseUrl}/v0.5/nhcx/claim/status with external_ref + throw new \RuntimeException( + 'DirectAbdmConnector::nhcxClaimStatusRequest() not implemented. ' + . 'TODO: POST NHCX /v0.5/nhcx/claim/status.' + ); + } +} diff --git a/app/Libraries/Abdm/DreamsoftConnector.php b/app/Libraries/Abdm/DreamsoftConnector.php new file mode 100644 index 0000000..0466c21 --- /dev/null +++ b/app/Libraries/Abdm/DreamsoftConnector.php @@ -0,0 +1,169 @@ +bridge = new BridgeSyncService(); + } + + public function getConnectorName(): string + { + return 'dreamsoft'; + } + + // ------------------------------------------------------------------------- + // ABHA + // ------------------------------------------------------------------------- + + public function validateAbha(string $abhaId, array $fullPayload = []): array + { + $payload = $fullPayload !== [] ? $fullPayload : [ + 'abha_id' => $abhaId, + 'requested_at' => date('Y-m-d H:i:s'), + ]; + $queueId = $this->bridge->enqueue('abdm.abha.validate', $payload, 'abha', $abhaId); + return ['ok' => 1, 'queue_id' => $queueId, 'status' => 'queued']; + } + + // ------------------------------------------------------------------------- + // Consent + // ------------------------------------------------------------------------- + + public function requestConsent( + int $patientId, + string $abhaId, + string $purposeCode, + string $expiresAt, + string $consentHandle, + array $rawPayload = [] + ): array { + $payload = $rawPayload !== [] ? $rawPayload : [ + 'patient_id' => $patientId, + 'abha_id' => $abhaId, + 'purpose_code' => $purposeCode, + 'expires_at' => $expiresAt, + 'consent_handle' => $consentHandle, + ]; + $queueId = $this->bridge->enqueue('abdm.consent.requested', $payload, 'consent', $consentHandle); + return ['ok' => 1, 'queue_id' => $queueId, 'status' => 'queued']; + } + + // ------------------------------------------------------------------------- + // Health Record Sharing + // ------------------------------------------------------------------------- + + public function sharePrescriptionBundle(array $payload, string $entityId = ''): array + { + $queueId = $this->bridge->enqueue( + 'abdm.fhir.share.requested', + $payload, + 'opd_fhir_document', + $entityId + ); + return ['ok' => 1, 'queue_id' => $queueId, 'status' => 'queued']; + } + + public function shareIpdDischargeBundle(array $payload, string $entityId = ''): array + { + $queueId = $this->bridge->enqueue( + 'abdm.ipd.discharge.share.requested', + $payload, + 'ipd_discharge', + $entityId + ); + return ['ok' => 1, 'queue_id' => $queueId, 'status' => 'queued']; + } + + public function shareDiagnosisReportBundle(array $payload, string $entityId = ''): array + { + $queueId = $this->bridge->enqueue( + 'abdm.diagnosis.report.share.requested', + $payload, + 'lab_request', + $entityId + ); + return ['ok' => 1, 'queue_id' => $queueId, 'status' => 'queued']; + } + + // ------------------------------------------------------------------------- + // Scan & Share + // ------------------------------------------------------------------------- + + public function scanShareLookup(string $qrPayload, string $abhaIdHint = '', array $fullPayload = []): array + { + $payload = $fullPayload !== [] ? $fullPayload : [ + 'qr_payload' => $qrPayload, + 'abha_id_hint' => $abhaIdHint, + 'requested_at' => date('Y-m-d H:i:s'), + ]; + $queueId = $this->bridge->enqueue( + 'abdm.scan_share.lookup', + $payload, + 'abha_scan', + $abhaIdHint !== '' ? $abhaIdHint : null + ); + return ['ok' => 1, 'queue_id' => $queueId, 'status' => 'queued']; + } + + // ------------------------------------------------------------------------- + // NHCX Claims + // ------------------------------------------------------------------------- + + public function nhcxClaimCreate( + array $bundle, + int $documentId, + int $patientId, + int $encounterId + ): array { + $payload = [ + 'nhcx_claim_document_id' => $documentId, + 'patient_id' => $patientId, + 'encounter_id' => $encounterId, + 'bundle' => $bundle, + ]; + $queueId = $this->bridge->enqueue( + 'nhcx.claim.created', + $payload, + 'nhcx_claim_document', + (string) $documentId + ); + return ['ok' => 1, 'queue_id' => $queueId, 'status' => 'queued']; + } + + public function nhcxClaimStatusRequest( + int $documentId, + string $externalRef, + string $currentStatus, + array $fullPayload = [] + ): array { + $payload = $fullPayload !== [] ? $fullPayload : [ + 'nhcx_claim_document_id' => $documentId, + 'external_ref' => $externalRef, + 'claim_status' => $currentStatus, + 'requested_at' => date('Y-m-d H:i:s'), + ]; + $queueId = $this->bridge->enqueue( + 'nhcx.claim.status.requested', + $payload, + 'nhcx_claim_document', + (string) $documentId + ); + return ['ok' => 1, 'queue_id' => $queueId, 'status' => 'queued']; + } +}