ABDM M2 care-context outbound sync and server cron automation
This commit is contained in:
parent
0176079e7d
commit
3bda6f5291
|
|
@ -247,17 +247,34 @@ curl -i -H "Authorization: Bearer $TOKEN" https://abdm-bridge.e-atria.in/api/v3/
|
|||
ssh user@hms-server
|
||||
|
||||
cd /opt/hms-ci4
|
||||
# Process up to 5 queued events
|
||||
# Install/update cron jobs for HMS background workers (idempotent)
|
||||
bash scripts/install_hms_cron.sh /opt/hms-ci4
|
||||
|
||||
# Manual one-time run checks
|
||||
php spark abdm:push-sync --limit=20
|
||||
php spark bridge:sync --limit 5
|
||||
```
|
||||
|
||||
**Expected Output:**
|
||||
```
|
||||
ABDM Push Sync
|
||||
Processed: N
|
||||
Success: N
|
||||
Failed: 0
|
||||
|
||||
Bridge sync completed:
|
||||
- Processed: 5
|
||||
- Sent: 5
|
||||
- Processed: N
|
||||
- Sent: N
|
||||
- Failed: 0
|
||||
- Message: queue processing completed
|
||||
```
|
||||
|
||||
### 4. Recommended Cron Block (Installed by Script)
|
||||
```cron
|
||||
* * * * * cd /opt/hms-ci4 && php spark abdm:push-sync --limit=20 --worker=cron-abdm-m2 >> /opt/hms-ci4/writable/logs/cron/abdm_push_sync.log 2>&1
|
||||
* * * * * cd /opt/hms-ci4 && php spark bridge:sync --limit 10 >> /opt/hms-ci4/writable/logs/cron/bridge_sync.log 2>&1
|
||||
* * * * * cd /opt/hms-ci4 && php spark snomed:process-coding >> /opt/hms-ci4/writable/logs/cron/snomed_process.log 2>&1
|
||||
*/2 * * * * cd /opt/hms-ci4 && php spark abdm:hiu-poll --limit=30 >> /opt/hms-ci4/writable/logs/cron/abdm_hiu_poll.log 2>&1
|
||||
*/5 * * * * cd /opt/hms-ci4 && php spark abdm:hiu-retry --limit=20 >> /opt/hms-ci4/writable/logs/cron/abdm_hiu_retry.log 2>&1
|
||||
```
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
## Architecture Summary
|
||||
- Added dedicated sync tables: `abdm_sync_patient`, `abdm_sync_record`, `abdm_sync_outbox`.
|
||||
- Added queue/outbox service and worker command for reliable push delivery.
|
||||
- Added Gateway client for `/api/v3/records/push` with response mapping (201/409/4xx/5xx).
|
||||
- Added Gateway client for `/api/v1/abdm/gateway/care-context/link` with validation and response mapping.
|
||||
- Added FHIR generator framework with module-specific generators.
|
||||
- Added advanced coding resolver with LOINC/SNOMED/UCUM fallback logic and validation scoring.
|
||||
- Added observability endpoints:
|
||||
|
|
@ -32,8 +32,8 @@
|
|||
- Score < 70: mark review-needed (policy hook available)
|
||||
|
||||
## Outbox Retry Policy
|
||||
- 8 retries: 1m, 5m, 15m, 30m, 1h, 2h, 4h, 8h
|
||||
- After retry 8 -> `dead`
|
||||
- Care-context outbound default retries: 10s, 30s, 60s
|
||||
- Legacy schedule fallback still supported for older outbox rows
|
||||
|
||||
## Security Notes
|
||||
- Bearer token never logged.
|
||||
|
|
@ -63,6 +63,16 @@
|
|||
- Worker: `php spark abdm:push-sync --limit=20`
|
||||
- Existing bridge worker remains separate: `php spark bridge:sync`
|
||||
|
||||
## Cron Setup (HMS Server)
|
||||
- Use `bash scripts/install_hms_cron.sh /var/www/html/hms_etria`
|
||||
- Deploy script now runs this installer automatically after migrate.
|
||||
- Managed cron block includes:
|
||||
- `abdm:push-sync` every minute
|
||||
- `bridge:sync` every minute
|
||||
- `snomed:process-coding` every minute
|
||||
- `abdm:hiu-poll` every 2 minutes
|
||||
- `abdm:hiu-retry` every 5 minutes
|
||||
|
||||
## Manual Replay
|
||||
- Endpoint: `POST /AbdmSync/replay_dead/{id}`
|
||||
- Resets dead row to `pending` for reprocessing.
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ class AbdmSync extends BaseController
|
|||
$payload = [
|
||||
'local_record_id' => (string) ($this->request->getPost('local_record_id') ?? ''),
|
||||
'local_patient_id' => (int) $this->request->getPost('local_patient_id'),
|
||||
'hospital_id' => (int) ($this->request->getPost('hospital_id') ?? 0),
|
||||
'hi_type' => (string) ($this->request->getPost('hi_type') ?? ''),
|
||||
'care_context_reference' => (string) ($this->request->getPost('care_context_reference') ?? ''),
|
||||
'care_context_display' => (string) ($this->request->getPost('care_context_display') ?? ''),
|
||||
|
|
@ -59,6 +60,9 @@ class AbdmSync extends BaseController
|
|||
'hfr_id' => (string) ($this->request->getPost('hfr_id') ?? ''),
|
||||
'source_updated_at' => (string) ($this->request->getPost('source_updated_at') ?? date('Y-m-d H:i:s')),
|
||||
'patient_name' => (string) ($this->request->getPost('patient_name') ?? ''),
|
||||
'mobile' => (string) ($this->request->getPost('mobile') ?? ''),
|
||||
'gender' => (string) ($this->request->getPost('gender') ?? ''),
|
||||
'dob' => (string) ($this->request->getPost('dob') ?? ''),
|
||||
'abha_id' => (string) ($this->request->getPost('abha_id') ?? ''),
|
||||
'abha_address' => (string) ($this->request->getPost('abha_address') ?? ''),
|
||||
'fhir_bundle' => $bundle,
|
||||
|
|
|
|||
|
|
@ -7765,6 +7765,9 @@ class Ipd_discharge extends BaseController
|
|||
'hfr_id' => $hfrId,
|
||||
'source_updated_at' => $sourceUpdatedAt,
|
||||
'patient_name' => (string) ($gatewayPayload['patient_name'] ?? $patientName),
|
||||
'mobile' => (string) ($patientRow['mphone1'] ?? ''),
|
||||
'gender' => (string) ($patientRow['gender'] ?? $patientRow['xgender'] ?? ''),
|
||||
'dob' => (string) ($patientRow['dob'] ?? ''),
|
||||
'abha_id' => (string) ($gatewayPayload['abha_id'] ?? $abhaDigits),
|
||||
'abha_address' => (string) ($gatewayPayload['abha_address'] ?? $abhaAddress),
|
||||
'fhir_bundle' => (array) ($gatewayPayload['fhir_bundle'] ?? []),
|
||||
|
|
|
|||
|
|
@ -20,6 +20,16 @@ class AbdmGatewayPushClient
|
|||
'WellnessRecord',
|
||||
];
|
||||
|
||||
/** @var string[] */
|
||||
private array $allowedRecordTypes = [
|
||||
'PrescriptionRecord',
|
||||
'DiagnosticReportRecord',
|
||||
'DischargeSummaryRecord',
|
||||
'OPConsultationRecord',
|
||||
'WellnessRecord',
|
||||
'HealthDocumentRecord',
|
||||
];
|
||||
|
||||
public function __construct(?BaseConnection $db = null)
|
||||
{
|
||||
$this->db = $db ?? db_connect();
|
||||
|
|
@ -104,7 +114,7 @@ class AbdmGatewayPushClient
|
|||
$mapped['latency_ms'] = (int) round((microtime(true) - $startedAt) * 1000);
|
||||
$mapped['gateway_request_id'] = (string) ($response->getHeaderLine('X-Request-Id') ?: ($body['request_id'] ?? ''));
|
||||
|
||||
$this->logPush($payload, $mapped, $requestId);
|
||||
$this->logPush($payload, $mapped, $requestId, '/api/v3/records/push', 'abdm.sync.outbox.records_push', 'record');
|
||||
return $mapped;
|
||||
} catch (\Throwable $e) {
|
||||
$result = [
|
||||
|
|
@ -118,7 +128,105 @@ class AbdmGatewayPushClient
|
|||
'gateway_request_id' => '',
|
||||
'latency_ms' => (int) round((microtime(true) - $startedAt) * 1000),
|
||||
];
|
||||
$this->logPush($payload, $result, $requestId);
|
||||
$this->logPush($payload, $result, $requestId, '/api/v3/records/push', 'abdm.sync.outbox.records_push', 'record');
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $payload
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function pushCareContextLink(array $payload, string $idempotencyKey = ''): array
|
||||
{
|
||||
$requestId = $this->createRequestId();
|
||||
$validation = $this->validateCareContextPayload($payload);
|
||||
if (($validation['valid'] ?? false) !== true) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'status' => 'failed',
|
||||
'http_status' => 400,
|
||||
'request_id' => $requestId,
|
||||
'error' => 'validation_failed',
|
||||
'message' => implode('; ', $validation['errors'] ?? ['Invalid payload']),
|
||||
'retryable' => false,
|
||||
];
|
||||
}
|
||||
|
||||
$baseUrl = rtrim($this->readSetting('ABDM_BRIDGE_URL', 'EATRIA_BRIDGE_URL'), '/');
|
||||
if ($baseUrl === '') {
|
||||
return [
|
||||
'ok' => false,
|
||||
'status' => 'failed',
|
||||
'http_status' => 500,
|
||||
'request_id' => $requestId,
|
||||
'error' => 'bridge_url_missing',
|
||||
'message' => 'ABDM bridge URL is not configured.',
|
||||
'retryable' => false,
|
||||
];
|
||||
}
|
||||
|
||||
$token = trim($this->readSetting('EATRIA_BRIDGE_TOKEN', 'ABDM_BRIDGE_TOKEN'));
|
||||
if ($token === '') {
|
||||
return [
|
||||
'ok' => false,
|
||||
'status' => 'failed',
|
||||
'http_status' => 500,
|
||||
'request_id' => $requestId,
|
||||
'error' => 'token_missing',
|
||||
'message' => 'ABDM bridge token is not configured.',
|
||||
'retryable' => false,
|
||||
];
|
||||
}
|
||||
|
||||
$url = $this->buildGatewayUrl($baseUrl, '/api/v1/abdm/gateway/care-context/link');
|
||||
$headers = [
|
||||
'Authorization' => 'Bearer ' . $token,
|
||||
'Content-Type' => 'application/json',
|
||||
'X-Request-Id' => $requestId,
|
||||
];
|
||||
if ($idempotencyKey !== '') {
|
||||
$headers['X-Idempotency-Key'] = $idempotencyKey;
|
||||
}
|
||||
|
||||
$startedAt = microtime(true);
|
||||
try {
|
||||
$client = service('curlrequest', [
|
||||
'timeout' => 30,
|
||||
'connect_timeout' => 10,
|
||||
'http_errors' => false,
|
||||
]);
|
||||
|
||||
$response = $client->post($url, [
|
||||
'headers' => $headers,
|
||||
'json' => $payload,
|
||||
]);
|
||||
|
||||
$httpStatus = (int) $response->getStatusCode();
|
||||
$body = json_decode((string) $response->getBody(), true);
|
||||
if (! is_array($body)) {
|
||||
$body = ['raw' => (string) $response->getBody()];
|
||||
}
|
||||
|
||||
$mapped = $this->mapCareContextResponse($httpStatus, $body, $requestId);
|
||||
$mapped['latency_ms'] = (int) round((microtime(true) - $startedAt) * 1000);
|
||||
$mapped['gateway_request_id'] = (string) ($response->getHeaderLine('X-Request-Id') ?: ($body['request_id'] ?? ''));
|
||||
|
||||
$this->logPush($payload, $mapped, $requestId, '/api/v1/abdm/gateway/care-context/link', 'abdm.sync.outbox.care_context_link', 'care_context');
|
||||
return $mapped;
|
||||
} catch (\Throwable $e) {
|
||||
$result = [
|
||||
'ok' => false,
|
||||
'status' => 'retry',
|
||||
'http_status' => 0,
|
||||
'request_id' => $requestId,
|
||||
'error' => 'network_error',
|
||||
'message' => $e->getMessage(),
|
||||
'retryable' => true,
|
||||
'gateway_request_id' => '',
|
||||
'latency_ms' => (int) round((microtime(true) - $startedAt) * 1000),
|
||||
];
|
||||
$this->logPush($payload, $result, $requestId, '/api/v1/abdm/gateway/care-context/link', 'abdm.sync.outbox.care_context_link', 'care_context');
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
|
@ -164,11 +272,91 @@ class AbdmGatewayPushClient
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $payload
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function validateCareContextPayload(array $payload): array
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
if ((int) ($payload['hospital_id'] ?? 0) <= 0) {
|
||||
$errors[] = 'hospital_id is required';
|
||||
}
|
||||
|
||||
$hfrId = trim((string) ($payload['hfr_id'] ?? ''));
|
||||
if ($hfrId === '') {
|
||||
$errors[] = 'hfr_id is required';
|
||||
}
|
||||
|
||||
$patient = $payload['patient'] ?? null;
|
||||
if (! is_array($patient)) {
|
||||
$errors[] = 'patient object is required';
|
||||
} else {
|
||||
if (trim((string) ($patient['patient_id'] ?? '')) === '') {
|
||||
$errors[] = 'patient.patient_id is required';
|
||||
}
|
||||
if (trim((string) ($patient['name'] ?? '')) === '') {
|
||||
$errors[] = 'patient.name is required';
|
||||
}
|
||||
|
||||
$mobileDigits = preg_replace('/\D/', '', (string) ($patient['mobile'] ?? ''));
|
||||
if (! is_string($mobileDigits) || strlen($mobileDigits) !== 10) {
|
||||
$errors[] = 'patient.mobile must be a 10-digit number';
|
||||
}
|
||||
|
||||
$gender = strtoupper(trim((string) ($patient['gender'] ?? '')));
|
||||
if (! in_array($gender, ['M', 'F', 'O'], true)) {
|
||||
$errors[] = 'patient.gender must be M, F, or O';
|
||||
}
|
||||
|
||||
$yob = (int) ($patient['year_of_birth'] ?? 0);
|
||||
$currentYear = (int) date('Y');
|
||||
if ($yob < 1900 || $yob > $currentYear) {
|
||||
$errors[] = 'patient.year_of_birth must be a valid 4-digit year';
|
||||
}
|
||||
}
|
||||
|
||||
$contexts = $payload['care_contexts'] ?? null;
|
||||
if (! is_array($contexts) || $contexts === []) {
|
||||
$errors[] = 'care_contexts must contain at least one item';
|
||||
} else {
|
||||
foreach ($contexts as $index => $context) {
|
||||
if (! is_array($context)) {
|
||||
$errors[] = 'care_contexts[' . $index . '] must be an object';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (trim((string) ($context['reference_number'] ?? '')) === '') {
|
||||
$errors[] = 'care_contexts[' . $index . '].reference_number is required';
|
||||
}
|
||||
if (trim((string) ($context['display'] ?? '')) === '') {
|
||||
$errors[] = 'care_contexts[' . $index . '].display is required';
|
||||
}
|
||||
|
||||
$recordType = trim((string) ($context['record_type'] ?? ''));
|
||||
if (! in_array($recordType, $this->allowedRecordTypes, true)) {
|
||||
$errors[] = 'care_contexts[' . $index . '].record_type is invalid';
|
||||
}
|
||||
|
||||
$visitDate = trim((string) ($context['visit_date'] ?? ''));
|
||||
if ($visitDate === '' || ! preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $visitDate)) {
|
||||
$errors[] = 'care_contexts[' . $index . '].visit_date must be YYYY-MM-DD HH:MM:SS';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'valid' => empty($errors),
|
||||
'errors' => $errors,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $payload
|
||||
* @param array<string,mixed> $result
|
||||
*/
|
||||
private function logPush(array $payload, array $result, string $requestId): void
|
||||
private function logPush(array $payload, array $result, string $requestId, string $endpoint, string $eventType, string $entityType): void
|
||||
{
|
||||
if (! $this->db->tableExists('abdm_api_logs')) {
|
||||
return;
|
||||
|
|
@ -185,10 +373,10 @@ class AbdmGatewayPushClient
|
|||
|
||||
$this->db->table('abdm_api_logs')->insert([
|
||||
'channel' => 'eatria_bridge',
|
||||
'event_type' => 'abdm.sync.outbox.records_push',
|
||||
'endpoint' => '/api/v3/records/push',
|
||||
'event_type' => $eventType,
|
||||
'endpoint' => $endpoint,
|
||||
'http_method' => 'POST',
|
||||
'entity_type' => 'record',
|
||||
'entity_type' => $entityType,
|
||||
'entity_id' => (string) ($payload['local_record_id'] ?? ''),
|
||||
'request_json' => (string) json_encode($maskedPayload + ['request_id' => $requestId], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
'response_code' => (int) ($result['http_status'] ?? 0),
|
||||
|
|
@ -325,6 +513,77 @@ class AbdmGatewayPushClient
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $body
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function mapCareContextResponse(int $httpStatus, array $body, string $requestId): array
|
||||
{
|
||||
$message = (string) ($body['message'] ?? $body['error_text'] ?? ('HTTP ' . $httpStatus));
|
||||
$ok = (int) ($body['ok'] ?? 0) === 1;
|
||||
|
||||
if (($httpStatus >= 200 && $httpStatus < 300) && $ok) {
|
||||
return [
|
||||
'ok' => true,
|
||||
'status' => 'done',
|
||||
'http_status' => $httpStatus,
|
||||
'request_id' => $requestId,
|
||||
'gateway_record_id' => null,
|
||||
'gateway_queue_id' => null,
|
||||
'submitted' => 1,
|
||||
'duplicate' => 0,
|
||||
'message' => $message !== '' ? $message : 'Care context linked successfully',
|
||||
'retryable' => false,
|
||||
];
|
||||
}
|
||||
|
||||
if ($httpStatus >= 500 || $httpStatus === 0) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'status' => 'retry',
|
||||
'http_status' => $httpStatus,
|
||||
'request_id' => $requestId,
|
||||
'error' => 'gateway_unavailable',
|
||||
'message' => $message,
|
||||
'retryable' => true,
|
||||
'gateway_record_id' => null,
|
||||
'gateway_queue_id' => null,
|
||||
'submitted' => 0,
|
||||
'duplicate' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
if (in_array($httpStatus, [400, 401, 403, 422], true)) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'status' => 'failed',
|
||||
'http_status' => $httpStatus,
|
||||
'request_id' => $requestId,
|
||||
'error' => 'validation_or_auth_error',
|
||||
'message' => $message,
|
||||
'retryable' => false,
|
||||
'gateway_record_id' => null,
|
||||
'gateway_queue_id' => null,
|
||||
'submitted' => 0,
|
||||
'duplicate' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => false,
|
||||
'status' => 'failed',
|
||||
'http_status' => $httpStatus,
|
||||
'request_id' => $requestId,
|
||||
'error' => 'unknown_error',
|
||||
'message' => $message,
|
||||
'retryable' => false,
|
||||
'gateway_record_id' => null,
|
||||
'gateway_queue_id' => null,
|
||||
'submitted' => 0,
|
||||
'duplicate' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
private function createRequestId(): string
|
||||
{
|
||||
$bytes = random_bytes(16);
|
||||
|
|
@ -394,4 +653,10 @@ class AbdmGatewayPushClient
|
|||
|
||||
return str_repeat('*', strlen($left) - 4) . substr($left, -4) . ($right !== '' ? '@' . $right : '');
|
||||
}
|
||||
|
||||
private function buildGatewayUrl(string $baseUrl, string $path): string
|
||||
{
|
||||
$url = rtrim($baseUrl, '/') . '/' . ltrim($path, '/');
|
||||
return (string) preg_replace('#/api/api/#', '/api/', $url);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,6 +94,13 @@ class AbdmSyncOutboxService
|
|||
return null;
|
||||
}
|
||||
|
||||
if (empty($payload['outbound_event'])) {
|
||||
$payload['outbound_event'] = 'care_context_link';
|
||||
}
|
||||
if (empty($payload['retry_schedule_seconds']) || ! is_array($payload['retry_schedule_seconds'])) {
|
||||
$payload['retry_schedule_seconds'] = [10, 30, 60];
|
||||
}
|
||||
|
||||
$sourceUpdatedAt = $this->normalizeSourceUpdatedAt((string) ($payload['source_updated_at'] ?? ''));
|
||||
$idempotencyKey = $this->buildRecordIdempotencyKey($hfrId, $careContextReference, $sourceUpdatedAt);
|
||||
|
||||
|
|
@ -170,9 +177,9 @@ class AbdmSyncOutboxService
|
|||
]);
|
||||
}
|
||||
|
||||
public function markRetryOrDead(int $id, int $retryCount, string $error): void
|
||||
public function markRetryOrDead(int $id, int $retryCount, string $error, ?array $retryScheduleSeconds = null): void
|
||||
{
|
||||
$next = $this->getNextRetryAt($retryCount);
|
||||
$next = $this->getNextRetryAt($retryCount, $retryScheduleSeconds);
|
||||
$status = $next === null ? self::STATUS_DEAD : self::STATUS_FAILED;
|
||||
|
||||
$this->outboxModel->update($id, [
|
||||
|
|
@ -260,17 +267,19 @@ class AbdmSyncOutboxService
|
|||
return $hfrId . '|' . $careContextReference . '|' . $epoch;
|
||||
}
|
||||
|
||||
public function getNextRetryAt(int $retryCount): ?string
|
||||
public function getNextRetryAt(int $retryCount, ?array $retryScheduleSeconds = null): ?string
|
||||
{
|
||||
$schedule = $this->normalizeRetrySchedule($retryScheduleSeconds);
|
||||
|
||||
if ($retryCount <= 0) {
|
||||
return Time::now('Asia/Kolkata')->toDateTimeString();
|
||||
}
|
||||
|
||||
if ($retryCount > count($this->retryScheduleSeconds)) {
|
||||
if ($retryCount > count($schedule)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$delay = $this->retryScheduleSeconds[$retryCount - 1];
|
||||
$delay = $schedule[$retryCount - 1];
|
||||
return date('Y-m-d H:i:s', time() + $delay);
|
||||
}
|
||||
|
||||
|
|
@ -329,4 +338,27 @@ class AbdmSyncOutboxService
|
|||
$digits = preg_replace('/\D/', '', $abha);
|
||||
return is_string($digits) ? $digits : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, mixed>|null $retryScheduleSeconds
|
||||
* @return int[]
|
||||
*/
|
||||
private function normalizeRetrySchedule(?array $retryScheduleSeconds): array
|
||||
{
|
||||
$schedule = [];
|
||||
if (is_array($retryScheduleSeconds)) {
|
||||
foreach ($retryScheduleSeconds as $seconds) {
|
||||
$value = (int) $seconds;
|
||||
if ($value > 0) {
|
||||
$schedule[] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($schedule)) {
|
||||
return $this->retryScheduleSeconds;
|
||||
}
|
||||
|
||||
return array_values($schedule);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,8 +81,9 @@ class AbdmSyncWorkerService
|
|||
}
|
||||
|
||||
$retryCount = ((int) ($row['retry_count'] ?? 0)) + 1;
|
||||
$this->outbox->markRetryOrDead($id, $retryCount, (string) ($result['message'] ?? 'Sync failed'));
|
||||
if ($this->outbox->getNextRetryAt($retryCount) === null) {
|
||||
$retrySchedule = $this->resolveRetryScheduleSeconds($payload);
|
||||
$this->outbox->markRetryOrDead($id, $retryCount, (string) ($result['message'] ?? 'Sync failed'), $retrySchedule);
|
||||
if ($this->outbox->getNextRetryAt($retryCount, $retrySchedule) === null) {
|
||||
$summary['dead']++;
|
||||
} else {
|
||||
$summary['failed']++;
|
||||
|
|
@ -103,28 +104,45 @@ class AbdmSyncWorkerService
|
|||
return ['ok' => false, 'retryable' => false, 'message' => 'Sync record not found'];
|
||||
}
|
||||
|
||||
$fhirBundle = $payload['fhir_bundle'] ?? json_decode((string) ($record['fhir_bundle_json'] ?? '{}'), true);
|
||||
if (! is_array($fhirBundle)) {
|
||||
$fhirBundle = [];
|
||||
$patientProfile = $this->resolvePatientProfile((int) ($record['local_patient_id'] ?? 0), $payload);
|
||||
|
||||
$hospitalId = (int) ($payload['hospital_id'] ?? 0);
|
||||
if ($hospitalId <= 0) {
|
||||
$hospitalId = $this->resolveHospitalIdFromSettings();
|
||||
}
|
||||
|
||||
$gatewayPayload = [
|
||||
$recordType = $this->mapHiTypeToRecordType((string) ($record['hi_type'] ?? ''));
|
||||
$careContextPayload = [
|
||||
'hospital_id' => $hospitalId,
|
||||
'hfr_id' => (string) ($record['hfr_id'] ?? ''),
|
||||
'hi_type' => (string) ($record['hi_type'] ?? ''),
|
||||
'record_type' => (string) ($record['hi_type'] ?? ''),
|
||||
'abha_id' => $this->normalizeAbhaDigits((string) ($payload['abha_id'] ?? '')),
|
||||
'abha_address' => trim((string) ($payload['abha_address'] ?? '')),
|
||||
'patient_name' => trim((string) ($payload['patient_name'] ?? '')),
|
||||
'local_patient_id' => (string) ($record['local_patient_id'] ?? ''),
|
||||
'care_context_reference' => (string) ($record['care_context_reference'] ?? ''),
|
||||
'care_context_display' => (string) ($record['care_context_display'] ?? ''),
|
||||
'visit_date' => (string) ($record['visit_date'] ?? ''),
|
||||
'department' => (string) ($record['department'] ?? ''),
|
||||
'doctor_name' => (string) ($record['doctor_name'] ?? ''),
|
||||
'fhir_bundle' => $fhirBundle,
|
||||
'patient' => [
|
||||
'patient_id' => $patientProfile['patient_id'],
|
||||
'name' => $patientProfile['name'],
|
||||
'mobile' => $patientProfile['mobile'],
|
||||
'gender' => $patientProfile['gender'],
|
||||
'year_of_birth' => $patientProfile['year_of_birth'],
|
||||
],
|
||||
'care_contexts' => [[
|
||||
'reference_number' => (string) ($record['care_context_reference'] ?? ''),
|
||||
'display' => (string) ($record['care_context_display'] ?? $recordType),
|
||||
'record_type' => $recordType,
|
||||
'visit_date' => $this->normalizeVisitDateTime((string) ($record['visit_date'] ?? '')),
|
||||
'doctor_name' => (string) ($record['doctor_name'] ?? ''),
|
||||
'department' => (string) ($record['department'] ?? ''),
|
||||
]],
|
||||
];
|
||||
|
||||
$result = $this->gatewayClient->pushRecord($gatewayPayload, $idempotencyKey);
|
||||
$abhaAddress = trim((string) ($patientProfile['abha_address'] ?? ''));
|
||||
if ($abhaAddress !== '') {
|
||||
$careContextPayload['patient']['abha_address'] = $abhaAddress;
|
||||
}
|
||||
|
||||
$abhaNumber = trim((string) ($patientProfile['abha_number'] ?? ''));
|
||||
if ($abhaNumber !== '') {
|
||||
$careContextPayload['patient']['abha_number'] = $abhaNumber;
|
||||
}
|
||||
|
||||
$result = $this->gatewayClient->pushCareContextLink($careContextPayload, $idempotencyKey);
|
||||
|
||||
$this->recordModel->update($syncRecordId, [
|
||||
'sync_status' => (bool) ($result['ok'] ?? false) ? 'done' : ((bool) ($result['retryable'] ?? false) ? 'failed' : 'dead'),
|
||||
|
|
@ -243,4 +261,186 @@ class AbdmSyncWorkerService
|
|||
$digits = preg_replace('/\D/', '', $abha);
|
||||
return is_string($digits) ? $digits : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $payload
|
||||
* @return int[]|null
|
||||
*/
|
||||
private function resolveRetryScheduleSeconds(array $payload): ?array
|
||||
{
|
||||
$raw = $payload['retry_schedule_seconds'] ?? null;
|
||||
if (! is_array($raw)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$schedule = [];
|
||||
foreach ($raw as $seconds) {
|
||||
$value = (int) $seconds;
|
||||
if ($value > 0) {
|
||||
$schedule[] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $schedule !== [] ? array_values($schedule) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $payload
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function resolvePatientProfile(int $localPatientId, array $payload): array
|
||||
{
|
||||
$row = [];
|
||||
if ($localPatientId > 0 && $this->db->tableExists('patient_master')) {
|
||||
$row = $this->db->table('patient_master')->where('id', $localPatientId)->get(1)->getRowArray() ?? [];
|
||||
}
|
||||
|
||||
$name = trim((string) ($payload['patient_name'] ?? ''));
|
||||
if ($name === '') {
|
||||
$name = trim(trim((string) ($row['p_fname'] ?? '')) . ' ' . trim((string) ($row['p_lname'] ?? '')));
|
||||
}
|
||||
if ($name === '') {
|
||||
$name = 'PATIENT-' . $localPatientId;
|
||||
}
|
||||
|
||||
$mobile = trim((string) ($payload['mobile'] ?? $payload['patient_mobile'] ?? ''));
|
||||
if ($mobile === '') {
|
||||
foreach (['mphone1', 'mobile', 'phone', 'p_mobile'] as $field) {
|
||||
$candidate = trim((string) ($row[$field] ?? ''));
|
||||
if ($candidate !== '') {
|
||||
$mobile = $candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$mobileDigits = preg_replace('/\D/', '', $mobile);
|
||||
$mobile = is_string($mobileDigits) ? $mobileDigits : '';
|
||||
if (strlen($mobile) > 10) {
|
||||
$mobile = substr($mobile, -10);
|
||||
}
|
||||
|
||||
$genderRaw = trim((string) ($payload['gender'] ?? $payload['patient_gender'] ?? ''));
|
||||
if ($genderRaw === '') {
|
||||
$genderRaw = trim((string) ($row['gender'] ?? $row['xgender'] ?? ''));
|
||||
}
|
||||
|
||||
$dobRaw = trim((string) ($payload['dob'] ?? $payload['patient_dob'] ?? ''));
|
||||
if ($dobRaw === '') {
|
||||
foreach (['dob', 'p_dob', 'birth_date', 'date_of_birth'] as $field) {
|
||||
$candidate = trim((string) ($row[$field] ?? ''));
|
||||
if ($candidate !== '') {
|
||||
$dobRaw = $candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$abhaAddress = trim((string) ($payload['abha_address'] ?? ''));
|
||||
if ($abhaAddress === '') {
|
||||
$abhaAddress = trim((string) ($row['abha_address'] ?? ''));
|
||||
}
|
||||
|
||||
$abhaDigits = $this->normalizeAbhaDigits((string) ($payload['abha_id'] ?? ''));
|
||||
if ($abhaDigits === '') {
|
||||
foreach (['abha_id', 'abha_no', 'abha_number', 'abha'] as $field) {
|
||||
$candidate = $this->normalizeAbhaDigits((string) ($row[$field] ?? ''));
|
||||
if ($candidate !== '') {
|
||||
$abhaDigits = $candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'patient_id' => (string) ($localPatientId > 0 ? $localPatientId : ($payload['local_patient_id'] ?? '')),
|
||||
'name' => $name,
|
||||
'mobile' => $mobile,
|
||||
'gender' => $this->normalizeBridgeGenderCode($genderRaw),
|
||||
'year_of_birth' => $this->extractBirthYear($dobRaw),
|
||||
'abha_address' => $abhaAddress,
|
||||
'abha_number' => $abhaDigits,
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizeBridgeGenderCode(string $gender): string
|
||||
{
|
||||
$value = strtoupper(trim($gender));
|
||||
if ($value === 'M' || $value === 'MALE' || $value === '1') {
|
||||
return 'M';
|
||||
}
|
||||
if ($value === 'F' || $value === 'FEMALE' || $value === '2') {
|
||||
return 'F';
|
||||
}
|
||||
if ($value === 'O' || $value === 'OTHER' || $value === 'OTHERS' || $value === '3') {
|
||||
return 'O';
|
||||
}
|
||||
|
||||
return 'O';
|
||||
}
|
||||
|
||||
private function extractBirthYear(string $dob): int
|
||||
{
|
||||
$dob = trim($dob);
|
||||
if ($dob !== '') {
|
||||
$ts = strtotime($dob);
|
||||
if ($ts !== false) {
|
||||
return (int) date('Y', $ts);
|
||||
}
|
||||
}
|
||||
|
||||
return (int) date('Y');
|
||||
}
|
||||
|
||||
private function mapHiTypeToRecordType(string $hiType): string
|
||||
{
|
||||
$type = trim($hiType);
|
||||
return match ($type) {
|
||||
'PrescriptionRecord' => 'PrescriptionRecord',
|
||||
'DiagnosticReportRecord' => 'DiagnosticReportRecord',
|
||||
'DischargeSummaryRecord' => 'DischargeSummaryRecord',
|
||||
'OPConsultRecord' => 'OPConsultationRecord',
|
||||
'OPConsultationRecord' => 'OPConsultationRecord',
|
||||
'WellnessRecord' => 'WellnessRecord',
|
||||
'HealthDocumentRecord' => 'HealthDocumentRecord',
|
||||
default => 'HealthDocumentRecord',
|
||||
};
|
||||
}
|
||||
|
||||
private function normalizeVisitDateTime(string $visitDate): string
|
||||
{
|
||||
$visitDate = trim($visitDate);
|
||||
if ($visitDate === '') {
|
||||
return date('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
$ts = strtotime($visitDate);
|
||||
if ($ts === false) {
|
||||
return date('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
return date('Y-m-d H:i:s', $ts);
|
||||
}
|
||||
|
||||
private function resolveHospitalIdFromSettings(): int
|
||||
{
|
||||
if (! $this->db->tableExists('hospital_setting')) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
foreach (['ABDM_BRIDGE_HOSPITAL_ID', 'EATRIA_BRIDGE_HOSPITAL_ID', 'HOSPITAL_ID'] as $settingName) {
|
||||
$row = $this->db->table('hospital_setting')
|
||||
->select('s_value')
|
||||
->where('s_name', $settingName)
|
||||
->orderBy('id', 'DESC')
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
|
||||
$value = (int) trim((string) ($row['s_value'] ?? '0'));
|
||||
if ($value > 0) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ echo "=== Pushing to GitHub ==="
|
|||
git push origin main
|
||||
|
||||
echo "=== Deploying to server ==="
|
||||
REMOTE_CMDS="cd $PROJECT && git config core.fileMode false && git pull origin main && php spark migrate --namespace App && echo DEPLOY_OK"
|
||||
REMOTE_CMDS="cd $PROJECT && git config core.fileMode false && git pull origin main && php spark migrate --namespace App && bash scripts/install_hms_cron.sh $PROJECT && echo DEPLOY_OK"
|
||||
|
||||
if command -v plink &>/dev/null; then
|
||||
plink -ssh -batch -pw "$HMS_SSH_PASS" $SERVER "$REMOTE_CMDS"
|
||||
|
|
|
|||
43
scripts/install_hms_cron.sh
Normal file
43
scripts/install_hms_cron.sh
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_DIR="${1:-/var/www/html/hms_etria}"
|
||||
PHP_BIN="${PHP_BIN:-php}"
|
||||
LOG_DIR="${PROJECT_DIR}/writable/logs/cron"
|
||||
|
||||
BLOCK_START="# BEGIN HMS_CI4_AUTOGENERATED_CRON"
|
||||
BLOCK_END="# END HMS_CI4_AUTOGENERATED_CRON"
|
||||
|
||||
if [ ! -d "$PROJECT_DIR" ]; then
|
||||
echo "ERROR: Project directory not found: $PROJECT_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
TMP_CRON="$(mktemp)"
|
||||
trap 'rm -f "$TMP_CRON"' EXIT
|
||||
|
||||
# Keep existing non-HMS cron entries, replace only this managed block.
|
||||
crontab -l 2>/dev/null | awk -v start="$BLOCK_START" -v end="$BLOCK_END" '
|
||||
$0 == start {in_block=1; next}
|
||||
$0 == end {in_block=0; next}
|
||||
!in_block {print}
|
||||
' > "$TMP_CRON"
|
||||
|
||||
cat >> "$TMP_CRON" <<EOF
|
||||
$BLOCK_START
|
||||
* * * * * cd $PROJECT_DIR && $PHP_BIN spark abdm:push-sync --limit=20 --worker=cron-abdm-m2 >> $LOG_DIR/abdm_push_sync.log 2>&1
|
||||
* * * * * cd $PROJECT_DIR && $PHP_BIN spark bridge:sync --limit 10 >> $LOG_DIR/bridge_sync.log 2>&1
|
||||
* * * * * cd $PROJECT_DIR && $PHP_BIN spark snomed:process-coding >> $LOG_DIR/snomed_process.log 2>&1
|
||||
*/2 * * * * cd $PROJECT_DIR && $PHP_BIN spark abdm:hiu-poll --limit=30 >> $LOG_DIR/abdm_hiu_poll.log 2>&1
|
||||
*/5 * * * * cd $PROJECT_DIR && $PHP_BIN spark abdm:hiu-retry --limit=20 >> $LOG_DIR/abdm_hiu_retry.log 2>&1
|
||||
$BLOCK_END
|
||||
EOF
|
||||
|
||||
crontab "$TMP_CRON"
|
||||
|
||||
echo "Installed HMS cron jobs for project: $PROJECT_DIR"
|
||||
echo "Managed block markers: $BLOCK_START ... $BLOCK_END"
|
||||
crontab -l | sed -n '/^# BEGIN HMS_CI4_AUTOGENERATED_CRON$/,/^# END HMS_CI4_AUTOGENERATED_CRON$/p'
|
||||
Loading…
Reference in New Issue
Block a user