abdm: add connector adapter pattern (Interface/Factory/Dreamsoft/Direct) + refactor AbdmGateway

This commit is contained in:
DevSoftTech Server 2026-05-08 23:10:24 +05:30
parent cd605d6fdc
commit 38a63a628b
6 changed files with 589 additions and 27 deletions

View File

@ -0,0 +1,51 @@
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
/**
* ABDM Connector Configuration
*
* Controls which adapter the AbdmConnectorFactory instantiates.
*
* Override via .env:
* abdm.connector = dreamsoft # or direct_abdm
* abdm.directClientId = <M3 client id>
* abdm.directClientSecret = <M3 client secret>
* 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;
}

View File

@ -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) {
}

View File

@ -0,0 +1,45 @@
<?php
namespace App\Libraries\Abdm;
/**
* AbdmConnectorFactory
*
* Instantiates the configured ABDM connector adapter.
*
* Usage:
* $connector = AbdmConnectorFactory::make();
* $result = $connector->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'."
);
}
}
}

View File

@ -0,0 +1,133 @@
<?php
namespace App\Libraries\Abdm;
/**
* AbdmConnectorInterface
*
* Contract for all ABDM connector adapters.
* Switch adapters via Config\AbdmConnector::$connector (or env abdm.connector).
*
* Available adapters:
* 'dreamsoft' -> 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<string, mixed> $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<string, mixed> $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<string, mixed> $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<string, mixed> $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<string, mixed> $payload
*/
public function shareDiagnosisReportBundle(array $payload, string $entityId = ''): array;
// -------------------------------------------------------------------------
// Scan & Share
// -------------------------------------------------------------------------
/**
* Look up a patient using QR payload from Scan & Share flow.
*
* @param array<string, mixed> $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<string, mixed> $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<string, mixed> $fullPayload Full payload built by the controller
*/
public function nhcxClaimStatusRequest(
int $documentId,
string $externalRef,
string $currentStatus,
array $fullPayload = []
): array;
}

View File

@ -0,0 +1,158 @@
<?php
namespace App\Libraries\Abdm;
/**
* DirectAbdmConnector
*
* Future adapter — calls ABDM / NHA APIs directly without any middleware.
* All methods currently throw RuntimeException with TODO notes.
*
* To activate: set env abdm.connector = direct_abdm and implement each method.
*
* Required env / Config\AbdmConnector keys:
* directAbdmBaseUrl e.g. 'https://dev.abdm.gov.in/api/v3'
* directAbdmClientId ABDM M3 client ID
* directAbdmClientSecret ABDM M3 client secret
* directAbdmTimeoutSec HTTP timeout in seconds (default 30)
*
* Reference ABDM IG: https://nrces.in/ndhm/fhir/r4/index.html
*/
class DirectAbdmConnector implements AbdmConnectorInterface
{
private string $baseUrl;
private string $clientId;
private string $clientSecret;
private int $timeoutSec;
public function __construct()
{
$config = config('AbdmConnector');
$this->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.'
);
}
}

View File

@ -0,0 +1,169 @@
<?php
namespace App\Libraries\Abdm;
use App\Libraries\BridgeSyncService;
/**
* DreamsoftConnector
*
* Routes all ABDM operations through the Dreamsoft bridge middleware
* by enqueuing events to bridge_sync_queue via BridgeSyncService.
*
* This is the default (and currently only production) adapter.
* Switch to 'direct_abdm' in Config\AbdmConnector when DirectAbdmConnector is ready.
*/
class DreamsoftConnector implements AbdmConnectorInterface
{
private BridgeSyncService $bridge;
public function __construct()
{
$this->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'];
}
}