feat: completely remove HealthPlix integration, controllers, views, and database columns
This commit is contained in:
parent
df2e41c9a4
commit
9d568463f1
|
|
@ -448,12 +448,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gallery-item" onclick="openLb(this)" data-caption="Admin Panel — Doctor Master, User Management, AI Settings, HealthPlix, ABDM Gateway, Hospital Profile, Insurance, Bank, Bed Management, Stock">
|
<div class="gallery-item" onclick="openLb(this)" data-caption="Admin Panel — Doctor Master, User Management, AI Settings, ABDM Gateway, Hospital Profile, Insurance, Bank, Bed Management, Stock">
|
||||||
<img class="screen-img" src="assets/img/screenshots/image8.png" alt="Admin Panel">
|
<img class="screen-img" src="assets/img/screenshots/image8.png" alt="Admin Panel">
|
||||||
<div class="gallery-caption">
|
<div class="gallery-caption">
|
||||||
<div class="cap-badge">Admin & Settings</div>
|
<div class="cap-badge">Admin & Settings</div>
|
||||||
<h4>Admin Control Panel</h4>
|
<h4>Admin Control Panel</h4>
|
||||||
<p>Tile-based admin panel: Doctor Master, User & Role Management, AI Settings, HealthPlix Integration, ABDM Gateway, Hospital Profile, Insurance Master, Bank & Payment, Medical Bank, Bed Management, Hospital Stock.</p>
|
<p>Tile-based admin panel: Doctor Master, User & Role Management, AI Settings, ABDM Gateway, Hospital Profile, Insurance Master, Bank & Payment, Medical Bank, Bed Management, Hospital Stock.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -293,7 +293,6 @@
|
||||||
|
|
||||||
### System Integration Settings
|
### System Integration Settings
|
||||||
- AI settings (model, API key, usage monitoring)
|
- AI settings (model, API key, usage monitoring)
|
||||||
- HealthPlix integration
|
|
||||||
- ABDM gateway configuration (HFR ID, bridge token)
|
- ABDM gateway configuration (HFR ID, bridge token)
|
||||||
- ABDM report doctor mapping
|
- ABDM report doctor mapping
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -189,15 +189,6 @@ EKA_BEARER_TOKEN = your-token
|
||||||
# EKA_WEBHOOK_SECRET = shared-secret-for-callback-signature
|
# EKA_WEBHOOK_SECRET = shared-secret-for-callback-signature
|
||||||
```
|
```
|
||||||
|
|
||||||
### HealthPlix callback secret fallback
|
|
||||||
|
|
||||||
If `HEALTHPLIX_FETCH_SECRET` is set in `.env`, `POST /healthplix/fetch` uses it as fallback
|
|
||||||
when the hospital setting value is empty.
|
|
||||||
|
|
||||||
```dotenv
|
|
||||||
HEALTHPLIX_FETCH_SECRET = your-shared-secret
|
|
||||||
```
|
|
||||||
|
|
||||||
Then process queue normally:
|
Then process queue normally:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -84,8 +84,6 @@ class Filters extends BaseFilters
|
||||||
'api/*',
|
'api/*',
|
||||||
'AbdmGateway/*',
|
'AbdmGateway/*',
|
||||||
'AbdmHiu/*',
|
'AbdmHiu/*',
|
||||||
'HealthplixCallback/*',
|
|
||||||
'HealthplixGateway/*',
|
|
||||||
'records',
|
'records',
|
||||||
'records/*',
|
'records/*',
|
||||||
'v3/*',
|
'v3/*',
|
||||||
|
|
|
||||||
|
|
@ -1383,9 +1383,6 @@ $routes->group('setting', static function($routes) {
|
||||||
$routes->post('admin/ai-settings/save', 'Setting\\AiSettings::save');
|
$routes->post('admin/ai-settings/save', 'Setting\\AiSettings::save');
|
||||||
$routes->post('admin/ai-settings/test', 'Setting\\AiSettings::test');
|
$routes->post('admin/ai-settings/test', 'Setting\\AiSettings::test');
|
||||||
$routes->post('admin/ai-settings/usage', 'Setting\\AiSettings::usage');
|
$routes->post('admin/ai-settings/usage', 'Setting\\AiSettings::usage');
|
||||||
$routes->get('admin/healthplix-settings', 'Setting\\HealthplixSettings::index', ['filter' => $settingsAdminFilter]);
|
|
||||||
$routes->post('admin/healthplix-settings/save', 'Setting\\HealthplixSettings::save', ['filter' => $settingsAdminFilter]);
|
|
||||||
$routes->post('admin/healthplix-settings/test', 'Setting\\HealthplixSettings::test', ['filter' => $settingsAdminFilter]);
|
|
||||||
$routes->get('admin/abdm-gateway', 'Setting\\AbdmGatewaySettings::index', ['filter' => $settingsAdminFilter]);
|
$routes->get('admin/abdm-gateway', 'Setting\\AbdmGatewaySettings::index', ['filter' => $settingsAdminFilter]);
|
||||||
$routes->post('admin/abdm-gateway/save', 'Setting\\AbdmGatewaySettings::save', ['filter' => $settingsAdminFilter]);
|
$routes->post('admin/abdm-gateway/save', 'Setting\\AbdmGatewaySettings::save', ['filter' => $settingsAdminFilter]);
|
||||||
$routes->post('admin/abdm-gateway/test', 'Setting\\AbdmGatewaySettings::testConnection', ['filter' => $settingsAdminFilter]);
|
$routes->post('admin/abdm-gateway/test', 'Setting\\AbdmGatewaySettings::testConnection', ['filter' => $settingsAdminFilter]);
|
||||||
|
|
|
||||||
|
|
@ -1,98 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Controllers;
|
|
||||||
|
|
||||||
class HealthplixCallback extends BaseController
|
|
||||||
{
|
|
||||||
public function fetch()
|
|
||||||
{
|
|
||||||
$rawBody = (string) $this->request->getBody();
|
|
||||||
$payload = $this->resolvePayload($rawBody);
|
|
||||||
|
|
||||||
$expectedSecret = $this->resolveExpectedSecret();
|
|
||||||
$providedSecret = trim((string) $this->request->getHeaderLine('X-Healthplix-Secret'));
|
|
||||||
|
|
||||||
if ($expectedSecret === '' || ! hash_equals($expectedSecret, $providedSecret)) {
|
|
||||||
$this->logFetchEvent('error', 401, [
|
|
||||||
'message' => 'Missing or invalid callback secret',
|
|
||||||
'payload_bytes' => strlen($rawBody),
|
|
||||||
'payload_keys' => array_keys($payload),
|
|
||||||
], 'Unauthorized callback request');
|
|
||||||
|
|
||||||
return $this->response->setStatusCode(401)->setJSON([
|
|
||||||
'ok' => 0,
|
|
||||||
'error_text' => 'Unauthorized callback request',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->logFetchEvent('success', 200, [
|
|
||||||
'message' => 'Callback payload received',
|
|
||||||
'payload_bytes' => strlen($rawBody),
|
|
||||||
'payload_keys' => array_keys($payload),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $this->response->setJSON([
|
|
||||||
'ok' => 1,
|
|
||||||
'message' => 'HealthPlix fetch callback received',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
private function resolvePayload(string $rawBody): array
|
|
||||||
{
|
|
||||||
if (trim($rawBody) === '') {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$decoded = $this->request->getJSON(true);
|
|
||||||
return is_array($decoded) ? $decoded : [];
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
$decoded = json_decode($rawBody, true);
|
|
||||||
return is_array($decoded) ? $decoded : [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function resolveExpectedSecret(): string
|
|
||||||
{
|
|
||||||
$secret = trim((string) hospital_setting_value('HEALTHPLIX_FETCH_SECRET', ''));
|
|
||||||
if ($secret !== '') {
|
|
||||||
return $secret;
|
|
||||||
}
|
|
||||||
|
|
||||||
return trim((string) env('HEALTHPLIX_FETCH_SECRET', ''));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $response
|
|
||||||
*/
|
|
||||||
private function logFetchEvent(string $status, int $responseCode, array $response, string $errorMessage = ''): void
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
if (! $this->db || ! method_exists($this->db, 'tableExists') || ! $this->db->tableExists('abdm_api_logs')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->db->table('abdm_api_logs')->insert([
|
|
||||||
'channel' => 'healthplix',
|
|
||||||
'event_type' => 'healthplix.fetch.callback',
|
|
||||||
'endpoint' => (string) $this->request->getUri()->getPath(),
|
|
||||||
'http_method' => strtoupper($this->request->getMethod()),
|
|
||||||
'entity_type' => 'callback',
|
|
||||||
'entity_id' => trim((string) ($this->request->getHeaderLine('X-Request-Id') ?: 'n/a')),
|
|
||||||
'request_json' => json_encode([
|
|
||||||
'ip' => $this->request->getIPAddress(),
|
|
||||||
'content_type' => (string) $this->request->getHeaderLine('Content-Type'),
|
|
||||||
], JSON_UNESCAPED_SLASHES),
|
|
||||||
'response_code' => $responseCode,
|
|
||||||
'response_json' => json_encode($response, JSON_UNESCAPED_SLASHES),
|
|
||||||
'status' => $status,
|
|
||||||
'error_message' => $errorMessage,
|
|
||||||
'created_at' => date('Y-m-d H:i:s'),
|
|
||||||
]);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,134 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Controllers;
|
|
||||||
|
|
||||||
use App\Libraries\HealthplixService;
|
|
||||||
|
|
||||||
class HealthplixGateway extends BaseController
|
|
||||||
{
|
|
||||||
private function ensureAuthenticated()
|
|
||||||
{
|
|
||||||
if (! function_exists('auth') || ! auth()->loggedIn()) {
|
|
||||||
return $this->response->setStatusCode(401)->setJSON(['ok' => 0, 'error_text' => 'Authentication required']);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function generateToken()
|
|
||||||
{
|
|
||||||
if ($deny = $this->ensureAuthenticated()) {
|
|
||||||
return $deny;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $this->request->isAJAX()) {
|
|
||||||
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
||||||
}
|
|
||||||
|
|
||||||
$service = new HealthplixService();
|
|
||||||
$result = $service->generateToken();
|
|
||||||
if (($result['ok'] ?? false) !== true) {
|
|
||||||
return $this->response->setJSON([
|
|
||||||
'ok' => 0,
|
|
||||||
'error_text' => (string) ($result['error'] ?? 'HealthPlix token generation failed'),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->response->setJSON([
|
|
||||||
'ok' => 1,
|
|
||||||
'status' => (int) ($result['status'] ?? 200),
|
|
||||||
'message' => 'HealthPlix token generated',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function registerPatient()
|
|
||||||
{
|
|
||||||
if ($deny = $this->ensureAuthenticated()) {
|
|
||||||
return $deny;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $this->request->isAJAX()) {
|
|
||||||
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
||||||
}
|
|
||||||
|
|
||||||
$payload = $this->resolvePayload();
|
|
||||||
if (empty($payload)) {
|
|
||||||
return $this->response->setJSON([
|
|
||||||
'ok' => 0,
|
|
||||||
'error_text' => 'Patient payload is required',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$service = new HealthplixService();
|
|
||||||
$result = $service->registerPatient($payload);
|
|
||||||
if (($result['ok'] ?? false) !== true) {
|
|
||||||
return $this->response->setJSON([
|
|
||||||
'ok' => 0,
|
|
||||||
'error_text' => (string) ($result['error'] ?? 'HealthPlix patient registration failed'),
|
|
||||||
'status' => (int) ($result['status'] ?? 0),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->response->setJSON([
|
|
||||||
'ok' => 1,
|
|
||||||
'status' => (int) ($result['status'] ?? 200),
|
|
||||||
'data' => $result['data'] ?? [],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function bookAppointment()
|
|
||||||
{
|
|
||||||
if ($deny = $this->ensureAuthenticated()) {
|
|
||||||
return $deny;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $this->request->isAJAX()) {
|
|
||||||
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
|
||||||
}
|
|
||||||
|
|
||||||
$payload = $this->resolvePayload();
|
|
||||||
if (empty($payload)) {
|
|
||||||
return $this->response->setJSON([
|
|
||||||
'ok' => 0,
|
|
||||||
'error_text' => 'Appointment payload is required',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$service = new HealthplixService();
|
|
||||||
$result = $service->bookAppointment($payload);
|
|
||||||
if (($result['ok'] ?? false) !== true) {
|
|
||||||
return $this->response->setJSON([
|
|
||||||
'ok' => 0,
|
|
||||||
'error_text' => (string) ($result['error'] ?? 'HealthPlix appointment booking failed'),
|
|
||||||
'status' => (int) ($result['status'] ?? 0),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->response->setJSON([
|
|
||||||
'ok' => 1,
|
|
||||||
'status' => (int) ($result['status'] ?? 200),
|
|
||||||
'data' => $result['data'] ?? [],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
private function resolvePayload(): array
|
|
||||||
{
|
|
||||||
$json = $this->request->getJSON(true);
|
|
||||||
if (is_array($json) && ! empty($json)) {
|
|
||||||
return $json;
|
|
||||||
}
|
|
||||||
|
|
||||||
$payloadJson = trim((string) $this->request->getPost('payload_json'));
|
|
||||||
if ($payloadJson !== '') {
|
|
||||||
$decoded = json_decode($payloadJson, true);
|
|
||||||
if (is_array($decoded)) {
|
|
||||||
return $decoded;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -4,7 +4,6 @@ namespace App\Controllers;
|
||||||
|
|
||||||
use App\Controllers\BaseController;
|
use App\Controllers\BaseController;
|
||||||
use App\Libraries\Abdm\AbdmConnectorFactory;
|
use App\Libraries\Abdm\AbdmConnectorFactory;
|
||||||
use App\Libraries\HealthplixService;
|
|
||||||
use App\Models\ClinicalAuditTrailModel;
|
use App\Models\ClinicalAuditTrailModel;
|
||||||
use App\Models\OpdModel;
|
use App\Models\OpdModel;
|
||||||
use App\Models\PaymentModel;
|
use App\Models\PaymentModel;
|
||||||
|
|
@ -1884,173 +1883,9 @@ class Opd extends BaseController
|
||||||
$query = $this->db->query($sql);
|
$query = $this->db->query($sql);
|
||||||
$data['bank_data'] = $query->getResult();
|
$data['bank_data'] = $query->getResult();
|
||||||
|
|
||||||
// ── HealthPlix sync status for invoice button ─────────────────────────
|
|
||||||
$data['healthplix_sync_applicable'] = false;
|
|
||||||
$data['healthplix_sync_done'] = false;
|
|
||||||
$data['healthplix_log'] = null;
|
|
||||||
|
|
||||||
$healthplixEnabled = $this->readHealthplixSetting('HEALTHPLIX_ENABLED') === '1';
|
|
||||||
if ($healthplixEnabled && (int) ($opdRow->payment_status ?? 0) === 1) {
|
|
||||||
$docId = (int) ($opdRow->doc_id ?? 0);
|
|
||||||
$doctorIdentifier = '';
|
|
||||||
if ($docId > 0 && $this->db->tableExists('doctor_master')) {
|
|
||||||
$identifierField = $this->resolveDoctorHealthplixIdentifierField();
|
|
||||||
if ($identifierField !== null) {
|
|
||||||
$docRow2 = $this->db->table('doctor_master')
|
|
||||||
->select($identifierField)
|
|
||||||
->where('id', $docId)
|
|
||||||
->get(1)
|
|
||||||
->getRowArray();
|
|
||||||
$doctorIdentifier = trim((string) ($docRow2[$identifierField] ?? ''));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($doctorIdentifier !== '') {
|
|
||||||
$data['healthplix_sync_applicable'] = true;
|
|
||||||
if ($this->db->tableExists('abdm_api_logs')) {
|
|
||||||
$hpLog = $this->db->table('abdm_api_logs')
|
|
||||||
->where('channel', 'healthplix')
|
|
||||||
->where('event_type', 'healthplix.appointment.book')
|
|
||||||
->where('entity_type', 'opd')
|
|
||||||
->where('entity_id', (string) $opdId)
|
|
||||||
->where('status', 'success')
|
|
||||||
->orderBy('id', 'DESC')
|
|
||||||
->get(1)
|
|
||||||
->getRowArray();
|
|
||||||
if (! empty($hpLog)) {
|
|
||||||
$data['healthplix_sync_done'] = true;
|
|
||||||
$data['healthplix_log'] = $hpLog;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// OPD-level sync ID stored in table after successful appointment push.
|
|
||||||
$data['healthplix_opd_sync_id'] = trim((string) ($opdRow->healthplix_sync_id ?? ''));
|
|
||||||
// ─────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
return view('billing/opd_invoice_V', $data);
|
return view('billing/opd_invoice_V', $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function healthplix_push(int $opdId)
|
|
||||||
{
|
|
||||||
if (! $this->request->isAJAX()) {
|
|
||||||
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'message' => 'Invalid request']);
|
|
||||||
}
|
|
||||||
|
|
||||||
$opdId = (int) $opdId;
|
|
||||||
if ($opdId <= 0) {
|
|
||||||
return $this->response->setJSON([
|
|
||||||
'ok' => 0,
|
|
||||||
'message' => 'Invalid OPD ID',
|
|
||||||
'csrfName' => csrf_token(),
|
|
||||||
'csrfHash' => csrf_hash(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If a success log already exists, just return it.
|
|
||||||
if ($this->db->tableExists('abdm_api_logs')) {
|
|
||||||
$existing = $this->db->table('abdm_api_logs')
|
|
||||||
->where('channel', 'healthplix')
|
|
||||||
->where('event_type', 'healthplix.appointment.book')
|
|
||||||
->where('entity_type', 'opd')
|
|
||||||
->where('entity_id', (string) $opdId)
|
|
||||||
->where('status', 'success')
|
|
||||||
->orderBy('id', 'DESC')
|
|
||||||
->get(1)
|
|
||||||
->getRowArray();
|
|
||||||
if (! empty($existing)) {
|
|
||||||
return $this->response->setJSON([
|
|
||||||
'ok' => 1,
|
|
||||||
'already_synced' => true,
|
|
||||||
'message' => 'Already synced to HealthPlix.',
|
|
||||||
'log' => $existing,
|
|
||||||
'csrfName' => csrf_token(),
|
|
||||||
'csrfHash' => csrf_hash(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->syncPaidOpdToHealthplix($opdId);
|
|
||||||
|
|
||||||
// Check result
|
|
||||||
$synced = false;
|
|
||||||
$log = null;
|
|
||||||
$errorMsg = 'Sync failed. Check HealthPlix settings or logs.';
|
|
||||||
if ($this->db->tableExists('abdm_api_logs')) {
|
|
||||||
$log = $this->db->table('abdm_api_logs')
|
|
||||||
->where('channel', 'healthplix')
|
|
||||||
->where('event_type', 'healthplix.appointment.book')
|
|
||||||
->where('entity_type', 'opd')
|
|
||||||
->where('entity_id', (string) $opdId)
|
|
||||||
->where('status', 'success')
|
|
||||||
->orderBy('id', 'DESC')
|
|
||||||
->get(1)
|
|
||||||
->getRowArray();
|
|
||||||
$synced = ! empty($log);
|
|
||||||
|
|
||||||
if (! $synced) {
|
|
||||||
$errLog = $this->db->table('abdm_api_logs')
|
|
||||||
->where('channel', 'healthplix')
|
|
||||||
->where('status', 'error')
|
|
||||||
->orderBy('id', 'DESC')
|
|
||||||
->get(1)
|
|
||||||
->getRowArray();
|
|
||||||
// If no OPD-level error, look for any recent healthplix error (token/patient)
|
|
||||||
if (empty($errLog)) {
|
|
||||||
$errLog = $this->db->table('abdm_api_logs')
|
|
||||||
->where('channel', 'healthplix')
|
|
||||||
->where('status', 'error')
|
|
||||||
->where('created_at >=', date('Y-m-d H:i:s', time() - 30))
|
|
||||||
->orderBy('id', 'DESC')
|
|
||||||
->get(1)
|
|
||||||
->getRowArray();
|
|
||||||
}
|
|
||||||
if (! empty($errLog)) {
|
|
||||||
$errDetail = trim((string) ($errLog['error_message'] ?? ''));
|
|
||||||
$errCode = (int) ($errLog['response_code'] ?? 0);
|
|
||||||
$errEvent = trim((string) ($errLog['event_type'] ?? ''));
|
|
||||||
if ($errCode > 0 && $errDetail !== '') {
|
|
||||||
$errorMsg = '[' . $errEvent . '] HTTP ' . $errCode . ': ' . $errDetail;
|
|
||||||
} elseif ($errCode > 0) {
|
|
||||||
$errorMsg = '[' . $errEvent . '] HealthPlix API returned HTTP ' . $errCode . '.';
|
|
||||||
} elseif ($errDetail !== '') {
|
|
||||||
$errorMsg = '[' . $errEvent . '] ' . $errDetail;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No API log at all — likely a gate/config issue.
|
|
||||||
$healthplixEnabled = $this->readHealthplixSetting('HEALTHPLIX_ENABLED') === '1';
|
|
||||||
if (! $healthplixEnabled) {
|
|
||||||
$errorMsg = 'HealthPlix integration is disabled in settings.';
|
|
||||||
} else {
|
|
||||||
$identifierField = $this->resolveDoctorHealthplixIdentifierField();
|
|
||||||
$hasDoctorId = false;
|
|
||||||
if ($identifierField !== null) {
|
|
||||||
$opdRow2 = $this->db->table('opd_master')->select('doc_id')->where('opd_id', $opdId)->get(1)->getRowArray();
|
|
||||||
$docId2 = (int) ($opdRow2['doc_id'] ?? 0);
|
|
||||||
if ($docId2 > 0) {
|
|
||||||
$docRow2 = $this->db->table('doctor_master')->select($identifierField)->where('id', $docId2)->get(1)->getRowArray();
|
|
||||||
$hasDoctorId = trim((string) ($docRow2[$identifierField] ?? '')) !== '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (! $hasDoctorId) {
|
|
||||||
$errorMsg = 'Doctor does not have a HealthPlix Doctor Identifier set. Add it in Doctor Master.';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->response->setJSON([
|
|
||||||
'ok' => $synced ? 1 : 0,
|
|
||||||
'synced' => $synced,
|
|
||||||
'message' => $synced
|
|
||||||
? 'Appointment registered in HealthPlix.'
|
|
||||||
: $errorMsg,
|
|
||||||
'log' => $log,
|
|
||||||
'csrfName' => csrf_token(),
|
|
||||||
'csrfHash' => csrf_hash(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function invoice_print(int $opdId)
|
public function invoice_print(int $opdId)
|
||||||
{
|
{
|
||||||
$data = $this->buildOpdInvoicePrintData($opdId);
|
$data = $this->buildOpdInvoicePrintData($opdId);
|
||||||
|
|
@ -5445,7 +5280,7 @@ class Opd extends BaseController
|
||||||
$this->db->table('opd_master')->where('opd_id', $oid)->update($data);
|
$this->db->table('opd_master')->where('opd_id', $oid)->update($data);
|
||||||
|
|
||||||
$this->autoCreateOpdQueue($oid);
|
$this->autoCreateOpdQueue($oid);
|
||||||
// Keep zero-amount confirm fast; user can trigger HealthPlix sync manually from invoice.
|
// Keep zero-amount confirm fast.
|
||||||
|
|
||||||
return $this->response->setJSON([
|
return $this->response->setJSON([
|
||||||
'update' => 1,
|
'update' => 1,
|
||||||
|
|
@ -5499,7 +5334,6 @@ class Opd extends BaseController
|
||||||
$this->db->table('opd_master')->where('opd_id', $oid)->update($data);
|
$this->db->table('opd_master')->where('opd_id', $oid)->update($data);
|
||||||
|
|
||||||
$this->autoCreateOpdQueue($oid);
|
$this->autoCreateOpdQueue($oid);
|
||||||
$this->syncPaidOpdToHealthplix($oid);
|
|
||||||
|
|
||||||
return $this->response->setJSON([
|
return $this->response->setJSON([
|
||||||
'update' => 1,
|
'update' => 1,
|
||||||
|
|
@ -5565,7 +5399,6 @@ class Opd extends BaseController
|
||||||
$this->db->table('opd_master')->where('opd_id', $oid)->update($data);
|
$this->db->table('opd_master')->where('opd_id', $oid)->update($data);
|
||||||
|
|
||||||
$this->autoCreateOpdQueue($oid);
|
$this->autoCreateOpdQueue($oid);
|
||||||
$this->syncPaidOpdToHealthplix($oid);
|
|
||||||
|
|
||||||
return $this->response->setJSON([
|
return $this->response->setJSON([
|
||||||
'update' => 1,
|
'update' => 1,
|
||||||
|
|
@ -5588,7 +5421,6 @@ class Opd extends BaseController
|
||||||
$this->db->table('opd_master')->where('opd_id', $oid)->update($data);
|
$this->db->table('opd_master')->where('opd_id', $oid)->update($data);
|
||||||
|
|
||||||
$this->autoCreateOpdQueue($oid);
|
$this->autoCreateOpdQueue($oid);
|
||||||
$this->syncPaidOpdToHealthplix($oid);
|
|
||||||
|
|
||||||
return $this->response->setJSON([
|
return $this->response->setJSON([
|
||||||
'update' => 1,
|
'update' => 1,
|
||||||
|
|
@ -5606,249 +5438,7 @@ class Opd extends BaseController
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function syncPaidOpdToHealthplix(int $opdId): void
|
|
||||||
{
|
|
||||||
if ($opdId <= 0 || ! $this->db->tableExists('opd_master')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip duplicate push if one success log already exists for this OPD.
|
|
||||||
if ($this->db->tableExists('abdm_api_logs')) {
|
|
||||||
$existing = $this->db->table('abdm_api_logs')
|
|
||||||
->where('channel', 'healthplix')
|
|
||||||
->where('event_type', 'healthplix.appointment.book')
|
|
||||||
->where('entity_type', 'opd')
|
|
||||||
->where('entity_id', (string) $opdId)
|
|
||||||
->where('status', 'success')
|
|
||||||
->get(1)
|
|
||||||
->getRowArray();
|
|
||||||
if (! empty($existing)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$opd = $this->db->table('opd_master')
|
|
||||||
->where('opd_id', $opdId)
|
|
||||||
->get(1)
|
|
||||||
->getRowArray();
|
|
||||||
if (! is_array($opd) || empty($opd)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$patientId = (int) ($opd['p_id'] ?? 0);
|
|
||||||
$patient = [];
|
|
||||||
if ($patientId > 0 && $this->db->tableExists('patient_master')) {
|
|
||||||
$patient = $this->db->table('patient_master')
|
|
||||||
->where('id', $patientId)
|
|
||||||
->get(1)
|
|
||||||
->getRowArray() ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$doctorRow = [];
|
|
||||||
$doctorId = (int) ($opd['doc_id'] ?? 0);
|
|
||||||
if ($doctorId > 0 && $this->db->tableExists('doctor_master')) {
|
|
||||||
$doctorRow = $this->db->table('doctor_master')
|
|
||||||
->where('id', $doctorId)
|
|
||||||
->get(1)
|
|
||||||
->getRowArray() ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$doctorIdentifier = $this->readHealthplixSetting('HEALTHPLIX_DOCTOR_IDENTIFIER');
|
|
||||||
$doctorEmail = $this->readHealthplixSetting('HEALTHPLIX_DOCTOR_EMAIL');
|
|
||||||
$serviceIdentifier = $this->readHealthplixSetting('HEALTHPLIX_SERVICE_IDENTIFIER');
|
|
||||||
$serviceName = $this->readHealthplixSetting('HEALTHPLIX_SERVICE_NAME');
|
|
||||||
|
|
||||||
if (! empty($doctorRow)) {
|
|
||||||
$doctorIdentifierField = $this->resolveDoctorHealthplixIdentifierField();
|
|
||||||
if ($doctorIdentifierField !== null) {
|
|
||||||
$rowDoctorIdentifier = trim((string) ($doctorRow[$doctorIdentifierField] ?? ''));
|
|
||||||
if ($rowDoctorIdentifier !== '') {
|
|
||||||
$doctorIdentifier = $rowDoctorIdentifier;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$rowDoctorEmail = trim((string) ($doctorRow['email1'] ?? ''));
|
|
||||||
if ($rowDoctorEmail !== '') {
|
|
||||||
$doctorEmail = $rowDoctorEmail;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Appointment booking should use a valid HealthPlix doctor UUID, not local doctor id.
|
|
||||||
if ($doctorIdentifier === '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$service = new HealthplixService();
|
|
||||||
|
|
||||||
$patientPayload = [
|
|
||||||
'external_patient_id' => (string) $patientId,
|
|
||||||
'patient_code' => trim((string) ($patient['p_code'] ?? $patientId)),
|
|
||||||
'full_name' => trim((string) ($patient['p_fname'] ?? ($opd['P_name'] ?? ''))),
|
|
||||||
'mobile' => trim((string) ($patient['mphone1'] ?? '')),
|
|
||||||
'email' => trim((string) ($patient['email1'] ?? '')),
|
|
||||||
'gender' => $this->mapPatientGenderToHealthplix((string) ($patient['gender'] ?? '')),
|
|
||||||
'dob' => trim((string) ($patient['dob'] ?? '')),
|
|
||||||
'estimate_dob' => trim((string) ($patient['estimate_dob'] ?? '')),
|
|
||||||
'age_years' => trim((string) ($patient['age'] ?? '')),
|
|
||||||
'age_months' => trim((string) ($patient['age_in_month'] ?? '')),
|
|
||||||
'address_line1' => trim((string) ($patient['add1'] ?? '')),
|
|
||||||
'city' => trim((string) ($patient['city'] ?? '')),
|
|
||||||
'district' => trim((string) ($patient['district'] ?? '')),
|
|
||||||
'state' => trim((string) ($patient['state'] ?? '')),
|
|
||||||
'zip' => trim((string) ($patient['zip'] ?? '')),
|
|
||||||
'aadhar' => trim((string) ($patient['udai'] ?? '')),
|
|
||||||
'abha_id' => '',
|
|
||||||
'trigger' => 'opd.payment.confirmed',
|
|
||||||
];
|
|
||||||
|
|
||||||
$patientResult = $service->registerPatient($patientPayload);
|
|
||||||
$patientStatus = (int) ($patientResult['status'] ?? 0);
|
|
||||||
$patientAlreadyExists = $patientStatus === 409;
|
|
||||||
if (($patientResult['ok'] ?? false) !== true && ! $patientAlreadyExists) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Persist patient sync id when HealthPlix accepted/recognized this patient.
|
|
||||||
$patientSyncFallback = trim((string) ($patient['p_code'] ?? $patientId));
|
|
||||||
$patientSyncId = $this->extractHealthplixSyncId($patientResult, $patientSyncFallback);
|
|
||||||
$this->persistHealthplixSyncId('patient_master', 'id', $patientId, $patientSyncId);
|
|
||||||
|
|
||||||
$payload = [
|
|
||||||
'external_appointment_id' => (string) $opdId,
|
|
||||||
'appointment_id' => (string) ($opd['opd_code'] ?? ''),
|
|
||||||
'appointment_datetime' => trim((string) ($opd['apointment_date'] ?? '')),
|
|
||||||
'patient_identifier' => (string) ($patient['p_code'] ?? $patientId),
|
|
||||||
'patient_name' => trim((string) ($patient['p_fname'] ?? ($opd['P_name'] ?? ''))),
|
|
||||||
'patient_mobile' => trim((string) ($patient['mphone1'] ?? '')),
|
|
||||||
'doctor_identifier' => $doctorIdentifier,
|
|
||||||
'doctor_name' => trim((string) ($opd['doc_name'] ?? '')),
|
|
||||||
'doctor_email' => $doctorEmail,
|
|
||||||
'service_identifier' => $serviceIdentifier,
|
|
||||||
'service_name' => $serviceName !== '' ? $serviceName : trim((string) ($opd['opd_fee_desc'] ?? 'Consultation')),
|
|
||||||
'price' => (float) ($opd['opd_fee_amount'] ?? 0),
|
|
||||||
'local_trigger' => 'opd.payment.confirmed',
|
|
||||||
];
|
|
||||||
|
|
||||||
$appointmentResult = $service->bookAppointment($payload);
|
|
||||||
if (($appointmentResult['ok'] ?? false) === true) {
|
|
||||||
$appointmentSyncFallback = trim((string) ($opd['opd_code'] ?? $opdId));
|
|
||||||
$appointmentSyncId = $this->extractHealthplixSyncId($appointmentResult, $appointmentSyncFallback);
|
|
||||||
$this->persistHealthplixSyncId('opd_master', 'opd_id', $opdId, $appointmentSyncId);
|
|
||||||
}
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
// Never block OPD payment flow due to external integration errors.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $result
|
|
||||||
*/
|
|
||||||
private function extractHealthplixSyncId(array $result, string $fallback = ''): string
|
|
||||||
{
|
|
||||||
$data = (array) ($result['data'] ?? []);
|
|
||||||
$candidates = [
|
|
||||||
$data['sync_id'] ?? null,
|
|
||||||
$data['reference_id'] ?? null,
|
|
||||||
$data['referenceId'] ?? null,
|
|
||||||
$data['request_id'] ?? null,
|
|
||||||
$data['appointment_id'] ?? null,
|
|
||||||
$data['appointment_identifier'] ?? null,
|
|
||||||
$data['patient_id'] ?? null,
|
|
||||||
$data['patient_identifier'] ?? null,
|
|
||||||
$data['id'] ?? null,
|
|
||||||
$data['uuid'] ?? null,
|
|
||||||
$result['sync_id'] ?? null,
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($candidates as $candidate) {
|
|
||||||
$value = trim((string) $candidate);
|
|
||||||
if ($value !== '') {
|
|
||||||
return $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return trim($fallback);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function persistHealthplixSyncId(string $table, string $idField, int $entityId, string $syncId): void
|
|
||||||
{
|
|
||||||
if ($entityId <= 0 || $syncId === '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (! $this->db->tableExists($table)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$fields = array_map(
|
|
||||||
static fn (object $field): string => (string) ($field->name ?? ''),
|
|
||||||
$this->db->getFieldData($table)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (! in_array($idField, $fields, true) || ! in_array('healthplix_sync_id', $fields, true)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->db->table($table)
|
|
||||||
->where($idField, $entityId)
|
|
||||||
->update(['healthplix_sync_id' => $syncId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function mapPatientGenderToHealthplix(string $gender): string
|
|
||||||
{
|
|
||||||
$gender = trim(strtolower($gender));
|
|
||||||
if ($gender === '1' || $gender === 'male' || $gender === 'm') {
|
|
||||||
return 'male';
|
|
||||||
}
|
|
||||||
if ($gender === '2' || $gender === 'female' || $gender === 'f') {
|
|
||||||
return 'female';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'other';
|
|
||||||
}
|
|
||||||
|
|
||||||
private function readHealthplixSetting(string $name): string
|
|
||||||
{
|
|
||||||
$value = trim((string) env($name, ''));
|
|
||||||
if ($value !== '') {
|
|
||||||
return $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (defined($name)) {
|
|
||||||
$defined = trim((string) constant($name));
|
|
||||||
if ($defined !== '') {
|
|
||||||
return $defined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $this->db->tableExists('hospital_setting')) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
$row = $this->db->table('hospital_setting')
|
|
||||||
->select('s_value')
|
|
||||||
->where('s_name', $name)
|
|
||||||
->get(1)
|
|
||||||
->getRowArray();
|
|
||||||
|
|
||||||
return trim((string) ($row['s_value'] ?? ''));
|
|
||||||
}
|
|
||||||
|
|
||||||
private function resolveDoctorHealthplixIdentifierField(): ?string
|
|
||||||
{
|
|
||||||
if (! $this->db->tableExists('doctor_master')) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$fields = $this->db->getFieldNames('doctor_master') ?? [];
|
|
||||||
foreach (['healthplix_doctor_identifier', 'healthplix_identifier'] as $field) {
|
|
||||||
if (in_array($field, $fields, true)) {
|
|
||||||
return $field;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function autoCreateOpdQueue(int $opdId): void
|
private function autoCreateOpdQueue(int $opdId): void
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -372,7 +372,7 @@ class AbdmGatewaySettings extends BaseController
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Helpers (same pattern as HealthplixSettings)
|
// Helpers
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
private function canManageSettings(): bool
|
private function canManageSettings(): bool
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@ class Doctor extends BaseController
|
||||||
'opd_print_template_options' => $templateContext['opd_print_options'],
|
'opd_print_template_options' => $templateContext['opd_print_options'],
|
||||||
'template_fields' => $templateContext['fields'],
|
'template_fields' => $templateContext['fields'],
|
||||||
'hospital_default_opd_valid_no_days' => $this->getHospitalDefaultOpdValidityDays(),
|
'hospital_default_opd_valid_no_days' => $this->getHospitalDefaultOpdValidityDays(),
|
||||||
'healthplix_enabled' => $this->isHealthplixEnabled(),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,7 +49,6 @@ class Doctor extends BaseController
|
||||||
'opd_print_template_options' => $templateContext['opd_print_options'],
|
'opd_print_template_options' => $templateContext['opd_print_options'],
|
||||||
'template_fields' => $templateContext['fields'],
|
'template_fields' => $templateContext['fields'],
|
||||||
'hospital_default_opd_valid_no_days' => $this->getHospitalDefaultOpdValidityDays(),
|
'hospital_default_opd_valid_no_days' => $this->getHospitalDefaultOpdValidityDays(),
|
||||||
'healthplix_enabled' => $this->isHealthplixEnabled(),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,7 +58,6 @@ class Doctor extends BaseController
|
||||||
$doctorModel = new DoctorModel();
|
$doctorModel = new DoctorModel();
|
||||||
$regField = $this->resolveDoctorRegField();
|
$regField = $this->resolveDoctorRegField();
|
||||||
$hprField = $this->resolveDoctorHprField();
|
$hprField = $this->resolveDoctorHprField();
|
||||||
$healthplixDoctorIdentifier = trim((string) $this->request->getPost('input_healthplix_doctor_identifier'));
|
|
||||||
$doctorFields = $this->db->tableExists('doctor_master') ? ($this->db->getFieldNames('doctor_master') ?? []) : [];
|
$doctorFields = $this->db->tableExists('doctor_master') ? ($this->db->getFieldNames('doctor_master') ?? []) : [];
|
||||||
$shortDescription = trim((string) $this->request->getPost('txt_doc_sign'));
|
$shortDescription = trim((string) $this->request->getPost('txt_doc_sign'));
|
||||||
$regInput = trim((string) $this->request->getPost('input_doc_reg_no'));
|
$regInput = trim((string) $this->request->getPost('input_doc_reg_no'));
|
||||||
|
|
@ -79,7 +76,6 @@ class Doctor extends BaseController
|
||||||
'template_options' => $templateContext['options'],
|
'template_options' => $templateContext['options'],
|
||||||
'opd_print_template_options' => $templateContext['opd_print_options'],
|
'opd_print_template_options' => $templateContext['opd_print_options'],
|
||||||
'template_fields' => $templateContext['fields'],
|
'template_fields' => $templateContext['fields'],
|
||||||
'healthplix_enabled' => $this->isHealthplixEnabled(),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -125,9 +121,6 @@ class Doctor extends BaseController
|
||||||
if ($hprField !== null) {
|
if ($hprField !== null) {
|
||||||
$data[$hprField] = $hprInput;
|
$data[$hprField] = $hprInput;
|
||||||
}
|
}
|
||||||
if (in_array('healthplix_doctor_identifier', $doctorFields, true)) {
|
|
||||||
$data['healthplix_doctor_identifier'] = $healthplixDoctorIdentifier;
|
|
||||||
}
|
|
||||||
|
|
||||||
$appPin = trim((string) ($this->request->getPost('app_pin') ?? ''));
|
$appPin = trim((string) ($this->request->getPost('app_pin') ?? ''));
|
||||||
if ($appPin !== '') {
|
if ($appPin !== '') {
|
||||||
|
|
@ -150,7 +143,6 @@ class Doctor extends BaseController
|
||||||
'opd_print_template_options' => $templateContext['opd_print_options'],
|
'opd_print_template_options' => $templateContext['opd_print_options'],
|
||||||
'template_fields' => $templateContext['fields'],
|
'template_fields' => $templateContext['fields'],
|
||||||
'hospital_default_opd_valid_no_days' => $this->getHospitalDefaultOpdValidityDays(),
|
'hospital_default_opd_valid_no_days' => $this->getHospitalDefaultOpdValidityDays(),
|
||||||
'healthplix_enabled' => $this->isHealthplixEnabled(),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -180,7 +172,6 @@ class Doctor extends BaseController
|
||||||
'template_fields' => $templateContext['fields'],
|
'template_fields' => $templateContext['fields'],
|
||||||
'hospital_default_opd_valid_no_days' => $this->getHospitalDefaultOpdValidityDays(),
|
'hospital_default_opd_valid_no_days' => $this->getHospitalDefaultOpdValidityDays(),
|
||||||
'doctor_opd_valid_no_days' => $this->resolveDoctorOpdValidityDaysForView($doctorRow),
|
'doctor_opd_valid_no_days' => $this->resolveDoctorOpdValidityDaysForView($doctorRow),
|
||||||
'healthplix_enabled' => $this->isHealthplixEnabled(),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -194,7 +185,6 @@ class Doctor extends BaseController
|
||||||
$docId = (int) $this->request->getPost('doc_id');
|
$docId = (int) $this->request->getPost('doc_id');
|
||||||
$regField = $this->resolveDoctorRegField();
|
$regField = $this->resolveDoctorRegField();
|
||||||
$hprField = $this->resolveDoctorHprField();
|
$hprField = $this->resolveDoctorHprField();
|
||||||
$healthplixDoctorIdentifier = trim((string) $this->request->getPost('input_healthplix_doctor_identifier'));
|
|
||||||
$doctorFields = $this->db->tableExists('doctor_master') ? ($this->db->getFieldNames('doctor_master') ?? []) : [];
|
$doctorFields = $this->db->tableExists('doctor_master') ? ($this->db->getFieldNames('doctor_master') ?? []) : [];
|
||||||
$shortDescription = trim((string) $this->request->getPost('txt_doc_sign'));
|
$shortDescription = trim((string) $this->request->getPost('txt_doc_sign'));
|
||||||
$regInput = trim((string) $this->request->getPost('input_doc_reg_no'));
|
$regInput = trim((string) $this->request->getPost('input_doc_reg_no'));
|
||||||
|
|
@ -252,9 +242,6 @@ class Doctor extends BaseController
|
||||||
if ($hprField !== null) {
|
if ($hprField !== null) {
|
||||||
$data[$hprField] = $hprInput;
|
$data[$hprField] = $hprInput;
|
||||||
}
|
}
|
||||||
if (in_array('healthplix_doctor_identifier', $doctorFields, true)) {
|
|
||||||
$data['healthplix_doctor_identifier'] = $healthplixDoctorIdentifier;
|
|
||||||
}
|
|
||||||
|
|
||||||
$appPin = trim((string) ($this->request->getPost('app_pin') ?? ''));
|
$appPin = trim((string) ($this->request->getPost('app_pin') ?? ''));
|
||||||
if ($appPin !== '') {
|
if ($appPin !== '') {
|
||||||
|
|
@ -861,24 +848,4 @@ class Doctor extends BaseController
|
||||||
return 5;
|
return 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function isHealthplixEnabled(): bool
|
|
||||||
{
|
|
||||||
$envValue = trim((string) env('HEALTHPLIX_ENABLED', ''));
|
|
||||||
if ($envValue !== '') {
|
|
||||||
return $envValue === '1';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $this->db->tableExists('hospital_setting')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$row = $this->db->table('hospital_setting')
|
|
||||||
->select('s_value')
|
|
||||||
->where('s_name', 'HEALTHPLIX_ENABLED')
|
|
||||||
->get(1)
|
|
||||||
->getRowArray();
|
|
||||||
|
|
||||||
return trim((string) ($row['s_value'] ?? '0')) === '1';
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,239 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Controllers\Setting;
|
|
||||||
|
|
||||||
use App\Controllers\BaseController;
|
|
||||||
use App\Libraries\HealthplixService;
|
|
||||||
|
|
||||||
class HealthplixSettings extends BaseController
|
|
||||||
{
|
|
||||||
public function index()
|
|
||||||
{
|
|
||||||
if (! $this->canManageSettings()) {
|
|
||||||
return $this->response->setStatusCode(403)->setBody('Access denied');
|
|
||||||
}
|
|
||||||
|
|
||||||
return view('Setting/Admin/healthplix_settings', [
|
|
||||||
'healthplix_enabled' => $this->readSettingValue('HEALTHPLIX_ENABLED') === '1' ? '1' : '0',
|
|
||||||
'healthplix_base_url' => $this->readSettingValue('HEALTHPLIX_BASE_URL') ?: 'https://consultation-edge-dev.healthplix.com',
|
|
||||||
'healthplix_fetch_secret_masked' => $this->maskKey($this->readSettingValue('HEALTHPLIX_FETCH_SECRET')),
|
|
||||||
'healthplix_fetch_secret_exists' => $this->readSettingValue('HEALTHPLIX_FETCH_SECRET') !== '',
|
|
||||||
'healthplix_tenant_id' => $this->readSettingValue('HEALTHPLIX_TENANT_ID'),
|
|
||||||
'healthplix_tenant_key_masked' => $this->maskKey($this->readSettingValue('HEALTHPLIX_TENANT_KEY')),
|
|
||||||
'healthplix_tenant_key_exists' => $this->readSettingValue('HEALTHPLIX_TENANT_KEY') !== '',
|
|
||||||
'healthplix_doctor_identifier' => $this->readSettingValue('HEALTHPLIX_DOCTOR_IDENTIFIER'),
|
|
||||||
'healthplix_doctor_email' => $this->readSettingValue('HEALTHPLIX_DOCTOR_EMAIL'),
|
|
||||||
'healthplix_service_identifier' => $this->readSettingValue('HEALTHPLIX_SERVICE_IDENTIFIER'),
|
|
||||||
'healthplix_service_name' => $this->readSettingValue('HEALTHPLIX_SERVICE_NAME'),
|
|
||||||
'healthplix_tenant_header_name' => $this->readSettingValue('HEALTHPLIX_TENANT_HEADER_NAME') ?: 'tenant_id',
|
|
||||||
'healthplix_token_path' => $this->readSettingValue('HEALTHPLIX_TOKEN_PATH') ?: 'v1/generate/token',
|
|
||||||
'healthplix_patient_path' => $this->readSettingValue('HEALTHPLIX_PATIENT_PATH') ?: 'v1/patient/register',
|
|
||||||
'healthplix_appointment_path' => $this->readSettingValue('HEALTHPLIX_APPOINTMENT_PATH') ?: 'v1/appointment/register',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function save()
|
|
||||||
{
|
|
||||||
if (! $this->request->isAJAX()) {
|
|
||||||
return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $this->canManageSettings()) {
|
|
||||||
return $this->response->setStatusCode(403)->setJSON(['update' => 0, 'error_text' => 'Access denied']);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $this->db->tableExists('hospital_setting')) {
|
|
||||||
return $this->response->setJSON(['update' => 0, 'error_text' => 'hospital_setting table not found']);
|
|
||||||
}
|
|
||||||
|
|
||||||
$fields = [
|
|
||||||
'HEALTHPLIX_ENABLED' => $this->request->getPost('healthplix_enabled') === '1' ? '1' : '0',
|
|
||||||
'HEALTHPLIX_BASE_URL' => trim((string) $this->request->getPost('healthplix_base_url')),
|
|
||||||
'HEALTHPLIX_FETCH_SECRET' => trim((string) $this->request->getPost('healthplix_fetch_secret')),
|
|
||||||
'HEALTHPLIX_TENANT_ID' => trim((string) $this->request->getPost('healthplix_tenant_id')),
|
|
||||||
'HEALTHPLIX_TENANT_KEY' => trim((string) $this->request->getPost('healthplix_tenant_key')),
|
|
||||||
'HEALTHPLIX_DOCTOR_IDENTIFIER' => trim((string) $this->request->getPost('healthplix_doctor_identifier')),
|
|
||||||
'HEALTHPLIX_DOCTOR_EMAIL' => trim((string) $this->request->getPost('healthplix_doctor_email')),
|
|
||||||
'HEALTHPLIX_SERVICE_IDENTIFIER' => trim((string) $this->request->getPost('healthplix_service_identifier')),
|
|
||||||
'HEALTHPLIX_SERVICE_NAME' => trim((string) $this->request->getPost('healthplix_service_name')),
|
|
||||||
'HEALTHPLIX_TENANT_HEADER_NAME' => trim((string) $this->request->getPost('healthplix_tenant_header_name')),
|
|
||||||
'HEALTHPLIX_TOKEN_PATH' => trim((string) $this->request->getPost('healthplix_token_path')),
|
|
||||||
'HEALTHPLIX_PATIENT_PATH' => trim((string) $this->request->getPost('healthplix_patient_path')),
|
|
||||||
'HEALTHPLIX_APPOINTMENT_PATH' => trim((string) $this->request->getPost('healthplix_appointment_path')),
|
|
||||||
];
|
|
||||||
|
|
||||||
$savedCount = 0;
|
|
||||||
foreach ($fields as $name => $value) {
|
|
||||||
if ($value === '' && $name !== 'HEALTHPLIX_ENABLED') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->upsertSettingValue($name, $value)) {
|
|
||||||
$savedCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($savedCount === 0) {
|
|
||||||
return $this->response->setJSON([
|
|
||||||
'update' => 0,
|
|
||||||
'error_text' => 'Provide at least one setting value to save',
|
|
||||||
'csrfName' => csrf_token(),
|
|
||||||
'csrfHash' => csrf_hash(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$storedFetchSecret = $this->readSettingValue('HEALTHPLIX_FETCH_SECRET');
|
|
||||||
|
|
||||||
return $this->response->setJSON([
|
|
||||||
'update' => 1,
|
|
||||||
'error_text' => 'Saved ' . $savedCount . ' setting(s)',
|
|
||||||
'healthplixFetchSecretConfigured' => $storedFetchSecret !== '',
|
|
||||||
'healthplixFetchSecretMasked' => $this->maskKey($storedFetchSecret),
|
|
||||||
'csrfName' => csrf_token(),
|
|
||||||
'csrfHash' => csrf_hash(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test()
|
|
||||||
{
|
|
||||||
if (! $this->request->isAJAX()) {
|
|
||||||
return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $this->canManageSettings()) {
|
|
||||||
return $this->response->setStatusCode(403)->setJSON(['update' => 0, 'error_text' => 'Access denied']);
|
|
||||||
}
|
|
||||||
|
|
||||||
$overrides = [
|
|
||||||
'HEALTHPLIX_ENABLED' => $this->request->getPost('healthplix_enabled') === '1' ? '1' : '0',
|
|
||||||
];
|
|
||||||
|
|
||||||
$overrideFields = [
|
|
||||||
'HEALTHPLIX_BASE_URL' => 'healthplix_base_url',
|
|
||||||
'HEALTHPLIX_TENANT_HEADER_NAME' => 'healthplix_tenant_header_name',
|
|
||||||
'HEALTHPLIX_TENANT_ID' => 'healthplix_tenant_id',
|
|
||||||
'HEALTHPLIX_TENANT_KEY' => 'healthplix_tenant_key',
|
|
||||||
'HEALTHPLIX_TOKEN_PATH' => 'healthplix_token_path',
|
|
||||||
'HEALTHPLIX_PATIENT_PATH' => 'healthplix_patient_path',
|
|
||||||
'HEALTHPLIX_APPOINTMENT_PATH' => 'healthplix_appointment_path',
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($overrideFields as $settingKey => $postKey) {
|
|
||||||
$value = trim((string) $this->request->getPost($postKey));
|
|
||||||
if ($value !== '') {
|
|
||||||
$overrides[$settingKey] = $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$service = new HealthplixService();
|
|
||||||
$result = $service->generateToken($overrides);
|
|
||||||
if (($result['ok'] ?? false) !== true) {
|
|
||||||
return $this->response->setJSON([
|
|
||||||
'update' => 0,
|
|
||||||
'error_text' => (string) ($result['error'] ?? 'HealthPlix token test failed'),
|
|
||||||
'csrfName' => csrf_token(),
|
|
||||||
'csrfHash' => csrf_hash(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->response->setJSON([
|
|
||||||
'update' => 1,
|
|
||||||
'error_text' => 'HealthPlix token generated successfully',
|
|
||||||
'csrfName' => csrf_token(),
|
|
||||||
'csrfHash' => csrf_hash(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function canManageSettings(): bool
|
|
||||||
{
|
|
||||||
if (! function_exists('auth') || ! auth()->loggedIn()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function readSettingValue(string $name): string
|
|
||||||
{
|
|
||||||
if (defined($name)) {
|
|
||||||
$definedValue = trim((string) constant($name));
|
|
||||||
if ($definedValue !== '') {
|
|
||||||
return $definedValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $this->db->tableExists('hospital_setting')) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
$row = $this->db->table('hospital_setting')
|
|
||||||
->select('s_value')
|
|
||||||
->where('s_name', $name)
|
|
||||||
->get(1)
|
|
||||||
->getRowArray();
|
|
||||||
|
|
||||||
$value = trim((string) ($row['s_value'] ?? ''));
|
|
||||||
if ($value !== '') {
|
|
||||||
return $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
$rows = $this->db->table('hospital_setting')
|
|
||||||
->select('s_name, s_value')
|
|
||||||
->get()
|
|
||||||
->getResultArray();
|
|
||||||
|
|
||||||
$target = strtoupper(trim($name));
|
|
||||||
foreach ($rows as $item) {
|
|
||||||
$itemName = strtoupper(trim((string) ($item['s_name'] ?? '')));
|
|
||||||
if ($itemName === $target) {
|
|
||||||
$itemValue = trim((string) ($item['s_value'] ?? ''));
|
|
||||||
if ($itemValue !== '') {
|
|
||||||
return $itemValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
private function upsertSettingValue(string $name, string $value): bool
|
|
||||||
{
|
|
||||||
$existing = $this->db->table('hospital_setting')
|
|
||||||
->select('id, s_value')
|
|
||||||
->where('s_name', $name)
|
|
||||||
->get(1)
|
|
||||||
->getRowArray();
|
|
||||||
|
|
||||||
$oldValue = trim((string) ($existing['s_value'] ?? ''));
|
|
||||||
if ($existing) {
|
|
||||||
$ok = $this->db->table('hospital_setting')
|
|
||||||
->where('id', (int) ($existing['id'] ?? 0))
|
|
||||||
->update(['s_value' => $value]);
|
|
||||||
} else {
|
|
||||||
$ok = $this->db->table('hospital_setting')->insert([
|
|
||||||
's_name' => $name,
|
|
||||||
's_value' => $value,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($ok) {
|
|
||||||
$this->auditClinicalUpdate('hospital_setting', $name, $name, $oldValue, $value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (bool) $ok;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function maskKey(string $key): string
|
|
||||||
{
|
|
||||||
$key = trim($key);
|
|
||||||
if ($key === '') {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mb_strlen($key) <= 8) {
|
|
||||||
return str_repeat('*', mb_strlen($key));
|
|
||||||
}
|
|
||||||
|
|
||||||
return mb_substr($key, 0, 4) . str_repeat('*', mb_strlen($key) - 8) . mb_substr($key, -4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use CodeIgniter\Database\Migration;
|
||||||
|
|
||||||
|
class RemoveHealthplixColumns extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if ($this->tableExists('doctor_master') && $this->fieldExists('doctor_master', 'healthplix_doctor_identifier')) {
|
||||||
|
$this->forge->dropColumn('doctor_master', 'healthplix_doctor_identifier');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->tableExists('patient_master') && $this->fieldExists('patient_master', 'healthplix_sync_id')) {
|
||||||
|
$this->forge->dropColumn('patient_master', 'healthplix_sync_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->tableExists('opd_master') && $this->fieldExists('opd_master', 'healthplix_sync_id')) {
|
||||||
|
$this->forge->dropColumn('opd_master', 'healthplix_sync_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->tableExists('hospital_setting')) {
|
||||||
|
$this->db->query("DELETE FROM hospital_setting WHERE s_name LIKE 'HEALTHPLIX_%'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
if ($this->tableExists('doctor_master') && ! $this->fieldExists('doctor_master', 'healthplix_doctor_identifier')) {
|
||||||
|
$this->forge->addColumn('doctor_master', [
|
||||||
|
'healthplix_doctor_identifier' => [
|
||||||
|
'type' => 'VARCHAR',
|
||||||
|
'constraint' => 120,
|
||||||
|
'null' => true,
|
||||||
|
'after' => 'email1',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->tableExists('patient_master') && ! $this->fieldExists('patient_master', 'healthplix_sync_id')) {
|
||||||
|
$this->forge->addColumn('patient_master', [
|
||||||
|
'healthplix_sync_id' => [
|
||||||
|
'type' => 'VARCHAR',
|
||||||
|
'constraint' => 120,
|
||||||
|
'null' => true,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->tableExists('opd_master') && ! $this->fieldExists('opd_master', 'healthplix_sync_id')) {
|
||||||
|
$this->forge->addColumn('opd_master', [
|
||||||
|
'healthplix_sync_id' => [
|
||||||
|
'type' => 'VARCHAR',
|
||||||
|
'constraint' => 120,
|
||||||
|
'null' => true,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function tableExists(string $table): bool
|
||||||
|
{
|
||||||
|
$dbName = (string) ($this->db->database ?? '');
|
||||||
|
if ($dbName === '' || $table === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$row = $this->db->query(
|
||||||
|
'SELECT COUNT(*) AS total FROM information_schema.tables WHERE table_schema = ? AND table_name = ?',
|
||||||
|
[$dbName, $table]
|
||||||
|
)->getRowArray();
|
||||||
|
|
||||||
|
return ((int) ($row['total'] ?? 0)) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function fieldExists(string $table, string $field): bool
|
||||||
|
{
|
||||||
|
$dbName = (string) ($this->db->database ?? '');
|
||||||
|
if ($dbName === '' || $table === '' || $field === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$row = $this->db->query(
|
||||||
|
'SELECT COUNT(*) AS total FROM information_schema.columns WHERE table_schema = ? AND table_name = ? AND column_name = ?',
|
||||||
|
[$dbName, $table, $field]
|
||||||
|
)->getRowArray();
|
||||||
|
|
||||||
|
return ((int) ($row['total'] ?? 0)) > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,497 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Libraries;
|
|
||||||
|
|
||||||
class HealthplixService
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param array<string, string> $overrides
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
public function generateToken(array $overrides = []): array
|
|
||||||
{
|
|
||||||
if (! $this->isEnabled($overrides)) {
|
|
||||||
return ['ok' => false, 'error' => 'HealthPlix integration is disabled for this hospital.'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$baseUrl = rtrim($this->readSetting('HEALTHPLIX_BASE_URL', $overrides), '/');
|
|
||||||
$tokenPath = trim($this->readSetting('HEALTHPLIX_TOKEN_PATH', $overrides));
|
|
||||||
$tenantId = trim($this->readSetting('HEALTHPLIX_TENANT_ID', $overrides));
|
|
||||||
$tenantKey = trim($this->readSetting('HEALTHPLIX_TENANT_KEY', $overrides));
|
|
||||||
|
|
||||||
if ($baseUrl === '' || $tokenPath === '') {
|
|
||||||
return ['ok' => false, 'error' => 'HEALTHPLIX_BASE_URL and HEALTHPLIX_TOKEN_PATH are required.'];
|
|
||||||
}
|
|
||||||
if ($tenantId === '' || $tenantKey === '') {
|
|
||||||
return ['ok' => false, 'error' => 'HEALTHPLIX_TENANT_ID and HEALTHPLIX_TENANT_KEY are required.'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$endpoint = $this->buildUrl($baseUrl, $tokenPath);
|
|
||||||
// Token endpoint uses Basic Auth only — no request body per HealthPlix API doc v1.7
|
|
||||||
$headers = [
|
|
||||||
'Authorization' => 'Basic ' . base64_encode($tenantId . ':' . $tenantKey),
|
|
||||||
'Accept' => 'application/json',
|
|
||||||
];
|
|
||||||
|
|
||||||
$result = $this->request('POST', $endpoint, $headers, [], 'healthplix.token.generate', 'tenant', $tenantId);
|
|
||||||
if (($result['ok'] ?? false) !== true) {
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = (array) ($result['data'] ?? []);
|
|
||||||
$token = trim((string) ($data['token'] ?? ($data['access_token'] ?? '')));
|
|
||||||
if ($token === '') {
|
|
||||||
return ['ok' => false, 'error' => 'HealthPlix token response did not include token/access_token.'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'ok' => true,
|
|
||||||
'token' => $token,
|
|
||||||
'status' => (int) ($result['status'] ?? 200),
|
|
||||||
'data' => $data,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $payload
|
|
||||||
* @param array<string, string> $overrides
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
public function registerPatient(array $payload, array $overrides = []): array
|
|
||||||
{
|
|
||||||
if (! $this->isEnabled($overrides)) {
|
|
||||||
return ['ok' => false, 'error' => 'HealthPlix integration is disabled for this hospital.'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$baseUrl = rtrim($this->readSetting('HEALTHPLIX_BASE_URL', $overrides), '/');
|
|
||||||
$path = trim($this->readSetting('HEALTHPLIX_PATIENT_PATH', $overrides));
|
|
||||||
if ($baseUrl === '' || $path === '') {
|
|
||||||
return ['ok' => false, 'error' => 'HEALTHPLIX_BASE_URL and HEALTHPLIX_PATIENT_PATH are required.'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$tokenResult = $this->generateToken($overrides);
|
|
||||||
if (($tokenResult['ok'] ?? false) !== true) {
|
|
||||||
return $tokenResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
$token = (string) ($tokenResult['token'] ?? '');
|
|
||||||
$endpoint = $this->buildUrl($baseUrl, $path);
|
|
||||||
$tenantHeaderName = trim($this->readSetting('HEALTHPLIX_TENANT_HEADER_NAME', $overrides));
|
|
||||||
if ($tenantHeaderName === '') {
|
|
||||||
$tenantHeaderName = 'tenant_id';
|
|
||||||
}
|
|
||||||
$tenantId = trim($this->readSetting('HEALTHPLIX_TENANT_ID', $overrides));
|
|
||||||
|
|
||||||
$headers = [
|
|
||||||
'Authorization' => 'Bearer ' . $token,
|
|
||||||
'Accept' => 'application/json',
|
|
||||||
'Content-Type' => 'application/json',
|
|
||||||
$tenantHeaderName => $tenantId,
|
|
||||||
];
|
|
||||||
|
|
||||||
$entityId = trim((string) ($payload['external_patient_id'] ?? ($payload['patient_id'] ?? '')));
|
|
||||||
|
|
||||||
$apiPayload = $this->buildPatientApiPayload($payload);
|
|
||||||
return $this->request('POST', $endpoint, $headers, $apiPayload, 'healthplix.patient.register', 'patient', $entityId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $payload
|
|
||||||
* @param array<string, string> $overrides
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
public function bookAppointment(array $payload, array $overrides = []): array
|
|
||||||
{
|
|
||||||
if (! $this->isEnabled($overrides)) {
|
|
||||||
return ['ok' => false, 'error' => 'HealthPlix integration is disabled for this hospital.'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$baseUrl = rtrim($this->readSetting('HEALTHPLIX_BASE_URL', $overrides), '/');
|
|
||||||
$path = trim($this->readSetting('HEALTHPLIX_APPOINTMENT_PATH', $overrides));
|
|
||||||
if ($baseUrl === '' || $path === '') {
|
|
||||||
return ['ok' => false, 'error' => 'HEALTHPLIX_BASE_URL and HEALTHPLIX_APPOINTMENT_PATH are required.'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$tokenResult = $this->generateToken($overrides);
|
|
||||||
if (($tokenResult['ok'] ?? false) !== true) {
|
|
||||||
return $tokenResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
$token = (string) ($tokenResult['token'] ?? '');
|
|
||||||
$endpoint = $this->buildUrl($baseUrl, $path);
|
|
||||||
$tenantHeaderName = trim($this->readSetting('HEALTHPLIX_TENANT_HEADER_NAME', $overrides));
|
|
||||||
if ($tenantHeaderName === '') {
|
|
||||||
$tenantHeaderName = 'tenant_id';
|
|
||||||
}
|
|
||||||
$tenantId = trim($this->readSetting('HEALTHPLIX_TENANT_ID', $overrides));
|
|
||||||
|
|
||||||
$headers = [
|
|
||||||
'Authorization' => 'Bearer ' . $token,
|
|
||||||
'Accept' => 'application/json',
|
|
||||||
'Content-Type' => 'application/json',
|
|
||||||
$tenantHeaderName => $tenantId,
|
|
||||||
];
|
|
||||||
|
|
||||||
$entityId = trim((string) ($payload['external_appointment_id'] ?? ($payload['appointment_id'] ?? '')));
|
|
||||||
|
|
||||||
$apiPayload = $this->buildAppointmentApiPayload($payload);
|
|
||||||
return $this->request('POST', $endpoint, $headers, $apiPayload, 'healthplix.appointment.book', 'opd', $entityId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, string> $overrides
|
|
||||||
*/
|
|
||||||
private function isEnabled(array $overrides = []): bool
|
|
||||||
{
|
|
||||||
return $this->readSetting('HEALTHPLIX_ENABLED', $overrides) === '1';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map internal patient payload to HealthPlix API v1 field names.
|
|
||||||
* Ref: API Spec Doc v1.7 - Register Patient (POST v1/patient/register)
|
|
||||||
* @param array<string, mixed> $payload
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
private function buildPatientApiPayload(array $payload): array
|
|
||||||
{
|
|
||||||
$genderRaw = strtolower(trim((string) ($payload['gender'] ?? '')));
|
|
||||||
$genderMap = [
|
|
||||||
'male' => 'Male', 'm' => 'Male', '1' => 'Male',
|
|
||||||
'female' => 'Female', 'f' => 'Female', '2' => 'Female',
|
|
||||||
'other' => 'Others', 'o' => 'Others',
|
|
||||||
];
|
|
||||||
$gender = $genderMap[$genderRaw] ?? 'Others';
|
|
||||||
|
|
||||||
$honorific = trim((string) ($payload['honorific'] ?? ''));
|
|
||||||
if ($honorific === '') {
|
|
||||||
$honorific = ($gender === 'Female') ? 'Mrs.' : 'Mr.';
|
|
||||||
}
|
|
||||||
|
|
||||||
$patientCode = trim((string) ($payload['patient_code'] ?? ($payload['external_patient_id'] ?? '')));
|
|
||||||
$patientIdentifier = $patientCode !== '' ? $patientCode : (string) ($payload['external_patient_id'] ?? '');
|
|
||||||
|
|
||||||
// Minimum 3 characters required by API
|
|
||||||
if (mb_strlen($patientIdentifier) < 3) {
|
|
||||||
$patientIdentifier = str_pad($patientIdentifier, 3, '0', STR_PAD_LEFT);
|
|
||||||
}
|
|
||||||
|
|
||||||
$api = [
|
|
||||||
'patient_identifier' => $patientIdentifier,
|
|
||||||
'name' => trim((string) ($payload['full_name'] ?? '')),
|
|
||||||
'phone' => trim((string) ($payload['mobile'] ?? '')),
|
|
||||||
'gender' => $gender,
|
|
||||||
'honorific' => $honorific,
|
|
||||||
];
|
|
||||||
|
|
||||||
// HealthPlix expects dob; derive from estimate_dob/age when explicit DOB is missing.
|
|
||||||
$resolvedDob = $this->resolveDobForApi($payload);
|
|
||||||
if ($resolvedDob !== '') {
|
|
||||||
$api['dob'] = $resolvedDob;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optional fields — only send if non-empty
|
|
||||||
foreach ([
|
|
||||||
'email' => 'email',
|
|
||||||
'address_line1' => 'address',
|
|
||||||
'city' => 'city',
|
|
||||||
'zip' => 'pincode',
|
|
||||||
'phone_secondary' => 'phone_secondary',
|
|
||||||
'occupation' => 'occupation',
|
|
||||||
] as $internalKey => $apiKey) {
|
|
||||||
$val = trim((string) ($payload[$internalKey] ?? ''));
|
|
||||||
if ($val !== '') {
|
|
||||||
$api[$apiKey] = $val;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $api;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve DOB in YYYY-MM-DD from explicit dob, estimate_dob, or age values.
|
|
||||||
*
|
|
||||||
* @param array<string, mixed> $payload
|
|
||||||
*/
|
|
||||||
private function resolveDobForApi(array $payload): string
|
|
||||||
{
|
|
||||||
$dob = $this->normalizeDateForApi((string) ($payload['dob'] ?? ''));
|
|
||||||
if ($dob !== '') {
|
|
||||||
return $dob;
|
|
||||||
}
|
|
||||||
|
|
||||||
$estimateDob = $this->normalizeDateForApi((string) ($payload['estimate_dob'] ?? ''));
|
|
||||||
if ($estimateDob !== '') {
|
|
||||||
return $estimateDob;
|
|
||||||
}
|
|
||||||
|
|
||||||
$ageYears = $this->parseNonNegativeInt($payload['age_years'] ?? ($payload['age'] ?? null));
|
|
||||||
$ageMonths = $this->parseNonNegativeInt($payload['age_months'] ?? ($payload['age_in_month'] ?? null));
|
|
||||||
|
|
||||||
if ($ageYears === null && $ageMonths === null) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$dobFromAge = new \DateTime('today');
|
|
||||||
if ($ageYears !== null && $ageYears > 0) {
|
|
||||||
$dobFromAge->sub(new \DateInterval('P' . $ageYears . 'Y'));
|
|
||||||
}
|
|
||||||
if ($ageMonths !== null && $ageMonths > 0) {
|
|
||||||
$dobFromAge->sub(new \DateInterval('P' . $ageMonths . 'M'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return $dobFromAge->format('Y-m-d');
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function normalizeDateForApi(string $value): string
|
|
||||||
{
|
|
||||||
$value = trim($value);
|
|
||||||
if ($value === '' || $value === '0000-00-00') {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return (new \DateTime($value))->format('Y-m-d');
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param mixed $value
|
|
||||||
*/
|
|
||||||
private function parseNonNegativeInt($value): ?int
|
|
||||||
{
|
|
||||||
if (is_int($value)) {
|
|
||||||
return $value >= 0 ? $value : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$text = trim((string) $value);
|
|
||||||
if ($text === '') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preg_match('/\d+/', $text, $m) !== 1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return max(0, (int) $m[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map internal appointment payload to HealthPlix API v1 field names.
|
|
||||||
* Ref: API Spec Doc v1.7 - Register Appointment (POST v1/appointment/register)
|
|
||||||
* @param array<string, mixed> $payload
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
private function buildAppointmentApiPayload(array $payload): array
|
|
||||||
{
|
|
||||||
$patientCode = trim((string) ($payload['patient_identifier'] ?? ''));
|
|
||||||
if (mb_strlen($patientCode) < 3) {
|
|
||||||
$patientCode = str_pad($patientCode, 3, '0', STR_PAD_LEFT);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse appointment date/time from stored datetime string
|
|
||||||
$datetimeRaw = trim((string) ($payload['appointment_datetime'] ?? ''));
|
|
||||||
$appntDate = '';
|
|
||||||
$appntTime = '';
|
|
||||||
|
|
||||||
if ($datetimeRaw !== '') {
|
|
||||||
try {
|
|
||||||
$dt = new \DateTime($datetimeRaw);
|
|
||||||
$appntDate = $dt->format('Y-m-d');
|
|
||||||
$appntTime = $dt->format('H:i:s');
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
// fallback to today below
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($appntDate === '') {
|
|
||||||
$appntDate = date('Y-m-d');
|
|
||||||
}
|
|
||||||
if ($appntTime === '') {
|
|
||||||
$appntTime = date('H:i:s');
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'patient_identifier' => $patientCode,
|
|
||||||
'doctor_identifier' => trim((string) ($payload['doctor_identifier'] ?? '')),
|
|
||||||
'appnt_date' => $appntDate,
|
|
||||||
'appnt_time' => $appntTime,
|
|
||||||
'appnt_duration' => (int) ($payload['appnt_duration'] ?? 30),
|
|
||||||
'service_identifier' => trim((string) ($payload['service_identifier'] ?? '')),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, string> $overrides
|
|
||||||
*/
|
|
||||||
private function readSetting(string $name, array $overrides = []): string
|
|
||||||
{
|
|
||||||
if (array_key_exists($name, $overrides)) {
|
|
||||||
return trim((string) $overrides[$name]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$envValue = trim((string) env($name, ''));
|
|
||||||
if ($envValue !== '') {
|
|
||||||
return $envValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (defined($name)) {
|
|
||||||
$defined = trim((string) constant($name));
|
|
||||||
if ($defined !== '') {
|
|
||||||
return $defined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$db = db_connect();
|
|
||||||
if ($db && method_exists($db, 'tableExists') && $db->tableExists('hospital_setting')) {
|
|
||||||
$row = $db->table('hospital_setting')
|
|
||||||
->select('s_value')
|
|
||||||
->where('s_name', $name)
|
|
||||||
->get(1)
|
|
||||||
->getRowArray();
|
|
||||||
|
|
||||||
$value = trim((string) ($row['s_value'] ?? ''));
|
|
||||||
if ($value !== '') {
|
|
||||||
return $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildUrl(string $baseUrl, string $path): string
|
|
||||||
{
|
|
||||||
if (preg_match('~^https?://~i', $path) === 1) {
|
|
||||||
return $path;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $baseUrl . '/' . ltrim($path, '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $json
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
private function request(
|
|
||||||
string $method,
|
|
||||||
string $url,
|
|
||||||
array $headers,
|
|
||||||
array $json,
|
|
||||||
string $eventType = '',
|
|
||||||
string $entityType = '',
|
|
||||||
string $entityId = ''
|
|
||||||
): array
|
|
||||||
{
|
|
||||||
$options = [
|
|
||||||
'timeout' => 20,
|
|
||||||
'http_errors' => false,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (defined('ENVIRONMENT') && in_array((string) ENVIRONMENT, ['development', 'testing'], true)) {
|
|
||||||
$options['verify'] = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$client = service('curlrequest', $options);
|
|
||||||
$requestOptions = ['headers' => $headers];
|
|
||||||
if (! empty($json)) {
|
|
||||||
$requestOptions['json'] = $json;
|
|
||||||
}
|
|
||||||
$response = $client->request($method, $url, $requestOptions);
|
|
||||||
|
|
||||||
$status = $response->getStatusCode();
|
|
||||||
$body = (string) $response->getBody();
|
|
||||||
$decoded = json_decode($body, true);
|
|
||||||
if (! is_array($decoded)) {
|
|
||||||
$decoded = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($status < 200 || $status >= 300) {
|
|
||||||
$this->logApiCall($eventType, $url, $method, $entityType, $entityId, $json, $status, $body, 'error', 'HTTP ' . $status);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'ok' => false,
|
|
||||||
'status' => $status,
|
|
||||||
'error' => 'HealthPlix API request failed (HTTP ' . $status . ')',
|
|
||||||
'body' => mb_substr(trim($body), 0, 600),
|
|
||||||
'data' => $decoded,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->logApiCall($eventType, $url, $method, $entityType, $entityId, $json, $status, $body, 'success', '');
|
|
||||||
|
|
||||||
return [
|
|
||||||
'ok' => true,
|
|
||||||
'status' => $status,
|
|
||||||
'data' => $decoded,
|
|
||||||
'body' => mb_substr(trim($body), 0, 600),
|
|
||||||
];
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
$this->logApiCall($eventType, $url, $method, $entityType, $entityId, $json, 0, '', 'error', $e->getMessage());
|
|
||||||
|
|
||||||
return [
|
|
||||||
'ok' => false,
|
|
||||||
'status' => 0,
|
|
||||||
'error' => 'HealthPlix API request failed: ' . $e->getMessage(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $requestPayload
|
|
||||||
*/
|
|
||||||
private function logApiCall(
|
|
||||||
string $eventType,
|
|
||||||
string $endpoint,
|
|
||||||
string $method,
|
|
||||||
string $entityType,
|
|
||||||
string $entityId,
|
|
||||||
array $requestPayload,
|
|
||||||
int $responseCode,
|
|
||||||
string $responseBody,
|
|
||||||
string $status,
|
|
||||||
string $errorMessage
|
|
||||||
): void {
|
|
||||||
if ($eventType === '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$db = db_connect();
|
|
||||||
if (! $db || ! method_exists($db, 'tableExists') || ! $db->tableExists('abdm_api_logs')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$requestJson = (string) json_encode($requestPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
||||||
$decodedBody = json_decode($responseBody, true);
|
|
||||||
$responseJson = is_array($decodedBody)
|
|
||||||
? (string) json_encode($decodedBody, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
|
|
||||||
: trim($responseBody);
|
|
||||||
|
|
||||||
$db->table('abdm_api_logs')->insert([
|
|
||||||
'channel' => 'healthplix',
|
|
||||||
'event_type' => $eventType,
|
|
||||||
'endpoint' => $endpoint,
|
|
||||||
'http_method' => strtoupper($method),
|
|
||||||
'entity_type' => $entityType !== '' ? $entityType : null,
|
|
||||||
'entity_id' => $entityId !== '' ? $entityId : null,
|
|
||||||
'request_json' => $requestJson,
|
|
||||||
'response_code' => $responseCode > 0 ? $responseCode : null,
|
|
||||||
'response_json' => $responseJson !== '' ? mb_substr($responseJson, 0, 4000) : null,
|
|
||||||
'status' => $status,
|
|
||||||
'error_message' => $errorMessage !== '' ? mb_substr($errorMessage, 0, 1000) : null,
|
|
||||||
'created_at' => date('Y-m-d H:i:s'),
|
|
||||||
]);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
// Logging must never block business flow.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,233 +0,0 @@
|
||||||
<div class="card">
|
|
||||||
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
|
|
||||||
<h3 class="card-title mb-0">HealthPlix Integration</h3>
|
|
||||||
<span class="badge bg-primary fs-6">API Version: V1</span>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<?= csrf_field() ?>
|
|
||||||
<div class="alert alert-info py-2 mb-3" style="font-size:13px">
|
|
||||||
<strong>Base URL (QA/Dev):</strong> <code>https://consultation-edge-dev.healthplix.com</code> |
|
|
||||||
<strong>Doc version:</strong> 1.7 (26-Apr-2025) |
|
|
||||||
All paths below are pre-filled as per official API Specification Doc.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-check form-switch mb-3">
|
|
||||||
<input class="form-check-input" type="checkbox" id="healthplix_enabled" <?= (($healthplix_enabled ?? '0') === '1') ? 'checked' : '' ?>>
|
|
||||||
<label class="form-check-label" for="healthplix_enabled">Enable HealthPlix for this hospital</label>
|
|
||||||
<div class="form-text">Keep disabled for hospitals that do not use HealthPlix.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label class="form-label">HealthPlix API Base URL</label>
|
|
||||||
<input type="text" class="form-control" id="healthplix_base_url" placeholder="https://consultation-edge-dev.healthplix.com" value="<?= esc($healthplix_base_url ?? '') ?>">
|
|
||||||
<div class="form-text">QA/Dev URL from HealthPlix doc. Production URL is provided by HealthPlix team.</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label class="form-label">Tenant Header Name <span class="badge bg-secondary">Fixed by HealthPlix</span></label>
|
|
||||||
<input type="text" class="form-control" id="healthplix_tenant_header_name" placeholder="tenant_id" value="<?= esc($healthplix_tenant_header_name ?? 'tenant_id') ?>">
|
|
||||||
<div class="form-text">Header name for tenant ID in API requests. Default: <code>tenant_id</code> (do NOT put your Tenant ID value here).</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label class="form-label">Fetch Callback Secret</label>
|
|
||||||
<input type="password" class="form-control" id="healthplix_fetch_secret" placeholder="Enter to set/update callback secret">
|
|
||||||
<div class="form-text" id="healthplix_fetch_secret_hint">
|
|
||||||
<?php if (!empty($healthplix_fetch_secret_exists)) : ?>
|
|
||||||
<span class="text-success">✓ Secret saved (<?= esc($healthplix_fetch_secret_masked ?? '') ?>). Leave blank to keep existing.</span>
|
|
||||||
<?php else : ?>
|
|
||||||
No callback secret configured yet.
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class="my-2">
|
|
||||||
<h6 class="mb-2">API Endpoints <span class="badge bg-secondary">V1</span></h6>
|
|
||||||
<div class="form-text mb-2">Paths are relative to the Base URL above. Pre-filled from HealthPlix API Specification Doc v1.7.</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-4 mb-3">
|
|
||||||
<label class="form-label">Token Path</label>
|
|
||||||
<input type="text" class="form-control" id="healthplix_token_path" placeholder="v1/generate/token" value="<?= esc($healthplix_token_path ?? 'v1/generate/token') ?>">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4 mb-3">
|
|
||||||
<label class="form-label">Patient Registration Path</label>
|
|
||||||
<input type="text" class="form-control" id="healthplix_patient_path" placeholder="v1/patient/register" value="<?= esc($healthplix_patient_path ?? 'v1/patient/register') ?>">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4 mb-3">
|
|
||||||
<label class="form-label">Appointment Booking Path</label>
|
|
||||||
<input type="text" class="form-control" id="healthplix_appointment_path" placeholder="v1/appointment/register" value="<?= esc($healthplix_appointment_path ?? 'v1/appointment/register') ?>">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label class="form-label">Tenant ID (Basic Auth Username)</label>
|
|
||||||
<input type="text" class="form-control" id="healthplix_tenant_id" value="<?= esc($healthplix_tenant_id ?? '') ?>">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label class="form-label">Tenant Key (Basic Auth Password)</label>
|
|
||||||
<input type="password" class="form-control" id="healthplix_tenant_key" placeholder="Enter to set/update key">
|
|
||||||
<div class="form-text" id="healthplix_tenant_key_hint">
|
|
||||||
<?php if (!empty($healthplix_tenant_key_exists)) : ?>
|
|
||||||
<span class="text-success">✓ Key saved (<?= esc($healthplix_tenant_key_masked ?? '') ?>). Leave blank to keep existing.</span>
|
|
||||||
<?php else : ?>
|
|
||||||
No tenant key configured yet.
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label class="form-label">Default Doctor Identifier</label>
|
|
||||||
<input type="text" class="form-control" id="healthplix_doctor_identifier" value="<?= esc($healthplix_doctor_identifier ?? '') ?>">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label class="form-label">Doctor Email</label>
|
|
||||||
<input type="email" class="form-control" id="healthplix_doctor_email" value="<?= esc($healthplix_doctor_email ?? '') ?>">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label class="form-label">Service Identifier</label>
|
|
||||||
<input type="text" class="form-control" id="healthplix_service_identifier" value="<?= esc($healthplix_service_identifier ?? '') ?>">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label class="form-label">Service Name</label>
|
|
||||||
<input type="text" class="form-control" id="healthplix_service_name" value="<?= esc($healthplix_service_name ?? '') ?>">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex gap-2 flex-wrap">
|
|
||||||
<button type="button" class="btn btn-primary" id="btn_save_healthplix">Save Settings</button>
|
|
||||||
<button type="button" class="btn btn-outline-success" id="btn_test_healthplix">Test Token</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="healthplix_settings_msg" class="mt-3"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
(function() {
|
|
||||||
function getCsrfPair() {
|
|
||||||
var input = document.querySelector('input[name="<?= csrf_token() ?>"]');
|
|
||||||
if (!input) {
|
|
||||||
return { name: '<?= csrf_token() ?>', value: '<?= csrf_hash() ?>' };
|
|
||||||
}
|
|
||||||
return { name: input.getAttribute('name'), value: input.value };
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateCsrf(data) {
|
|
||||||
if (!data || !data.csrfName || !data.csrfHash) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var input = document.querySelector('input[name="' + data.csrfName + '"]');
|
|
||||||
if (input) {
|
|
||||||
input.value = data.csrfHash;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showMessage(type, text) {
|
|
||||||
var cls = type === 'success' ? 'alert alert-success' : 'alert alert-danger';
|
|
||||||
$('#healthplix_settings_msg').html('<div class="' + cls + '">' + $('<div>').text(text || '').html() + '</div>');
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateFetchSecretHintFromServer(data) {
|
|
||||||
if (data && data.healthplixFetchSecretConfigured) {
|
|
||||||
var maskedSecret = data.healthplixFetchSecretMasked || '********';
|
|
||||||
$('#healthplix_fetch_secret_hint').html('<span class="text-success">✓ Secret saved (' + $('<div>').text(maskedSecret).html() + '). Leave blank to keep existing.</span>');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$('#healthplix_fetch_secret_hint').html('<span class="text-muted">No callback secret configured yet.</span>');
|
|
||||||
}
|
|
||||||
|
|
||||||
function postJson(url, payload, cb) {
|
|
||||||
var csrf = getCsrfPair();
|
|
||||||
payload = payload || {};
|
|
||||||
payload[csrf.name] = csrf.value;
|
|
||||||
|
|
||||||
$.post(url, payload, function(data) {
|
|
||||||
updateCsrf(data);
|
|
||||||
cb(data || {});
|
|
||||||
}, 'json').fail(function(xhr) {
|
|
||||||
var response = (xhr && xhr.responseJSON) ? xhr.responseJSON : {};
|
|
||||||
cb(response || {});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function payload() {
|
|
||||||
return {
|
|
||||||
healthplix_enabled: $('#healthplix_enabled').is(':checked') ? '1' : '0',
|
|
||||||
healthplix_base_url: ($('#healthplix_base_url').val() || '').trim(),
|
|
||||||
healthplix_fetch_secret: ($('#healthplix_fetch_secret').val() || '').trim(),
|
|
||||||
healthplix_tenant_header_name: ($('#healthplix_tenant_header_name').val() || '').trim(),
|
|
||||||
healthplix_tenant_id: ($('#healthplix_tenant_id').val() || '').trim(),
|
|
||||||
healthplix_tenant_key: ($('#healthplix_tenant_key').val() || '').trim(),
|
|
||||||
healthplix_doctor_identifier: ($('#healthplix_doctor_identifier').val() || '').trim(),
|
|
||||||
healthplix_doctor_email: ($('#healthplix_doctor_email').val() || '').trim(),
|
|
||||||
healthplix_service_identifier: ($('#healthplix_service_identifier').val() || '').trim(),
|
|
||||||
healthplix_service_name: ($('#healthplix_service_name').val() || '').trim(),
|
|
||||||
healthplix_token_path: ($('#healthplix_token_path').val() || '').trim(),
|
|
||||||
healthplix_patient_path: ($('#healthplix_patient_path').val() || '').trim(),
|
|
||||||
healthplix_appointment_path: ($('#healthplix_appointment_path').val() || '').trim()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update hint live as user types
|
|
||||||
$('#healthplix_tenant_key').on('input', function() {
|
|
||||||
var val = $(this).val().trim();
|
|
||||||
if (val.length > 0) {
|
|
||||||
$('#healthplix_tenant_key_hint').html('<span class="text-warning">✎ New key will be saved on Save Settings.</span>');
|
|
||||||
} else {
|
|
||||||
$('#healthplix_tenant_key_hint').html('<span class="text-muted">Leave blank to keep existing key.</span>');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#healthplix_fetch_secret').on('input', function() {
|
|
||||||
var val = $(this).val().trim();
|
|
||||||
if (val.length > 0) {
|
|
||||||
$('#healthplix_fetch_secret_hint').html('<span class="text-warning">✎ New secret will be saved on Save Settings.</span>');
|
|
||||||
} else {
|
|
||||||
$('#healthplix_fetch_secret_hint').html('<span class="text-muted">Leave blank to keep existing callback secret.</span>');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#btn_save_healthplix').on('click', function() {
|
|
||||||
postJson('<?= base_url('setting/admin/healthplix-settings/save') ?>', payload(), function(data) {
|
|
||||||
if ((data.update || 0) !== 1) {
|
|
||||||
showMessage('error', data.error_text || 'Unable to save settings');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showMessage('success', data.error_text || 'Settings saved');
|
|
||||||
var savedKey = ($('#healthplix_tenant_key').val() || '').trim();
|
|
||||||
if (savedKey.length > 0) {
|
|
||||||
var masked = savedKey.substring(0, 4) + '****' + savedKey.slice(-4);
|
|
||||||
$('#healthplix_tenant_key_hint').html('<span class="text-success">✓ Key saved (' + masked + '). Leave blank to keep existing.</span>');
|
|
||||||
} else {
|
|
||||||
$('#healthplix_tenant_key_hint').html('<span class="text-muted">Leave blank to keep existing key.</span>');
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFetchSecretHintFromServer(data);
|
|
||||||
|
|
||||||
$('#healthplix_tenant_key').val('');
|
|
||||||
$('#healthplix_fetch_secret').val('');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#btn_test_healthplix').on('click', function() {
|
|
||||||
postJson('<?= base_url('setting/admin/healthplix-settings/test') ?>', payload(), function(data) {
|
|
||||||
if ((data.update || 0) !== 1) {
|
|
||||||
showMessage('error', data.error_text || 'HealthPlix token test failed');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showMessage('success', data.error_text || 'HealthPlix token test successful');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
|
|
@ -89,12 +89,7 @@
|
||||||
<span>AI Settings</span>
|
<span>AI Settings</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 col-md-2 col-lg-2">
|
|
||||||
<a class="admin-tile" href="javascript:openAdminSubPage('<?= base_url('setting/admin/healthplix-settings') ?>','HealthPlix Integration');">
|
|
||||||
<i class="bi bi-link-45deg"></i>
|
|
||||||
<span>HealthPlix Integration</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="col-6 col-md-2 col-lg-2">
|
<div class="col-6 col-md-2 col-lg-2">
|
||||||
<a class="admin-tile" href="javascript:openAdminSubPage('<?= base_url('setting/admin/abdm-gateway') ?>','ABDM Gateway Config');">
|
<a class="admin-tile" href="javascript:openAdminSubPage('<?= base_url('setting/admin/abdm-gateway') ?>','ABDM Gateway Config');">
|
||||||
<i class="bi bi-hdd-network" style="color:#0d6efd"></i>
|
<i class="bi bi-hdd-network" style="color:#0d6efd"></i>
|
||||||
|
|
|
||||||
|
|
@ -75,12 +75,7 @@
|
||||||
<label class="form-label">HPR ID</label>
|
<label class="form-label">HPR ID</label>
|
||||||
<input class="form-control" name="input_hpr_id" placeholder="ABDM HPR ID" type="text" value="<?= esc($formData['input_hpr_id'] ?? '') ?>">
|
<input class="form-control" name="input_hpr_id" placeholder="ABDM HPR ID" type="text" value="<?= esc($formData['input_hpr_id'] ?? '') ?>">
|
||||||
</div>
|
</div>
|
||||||
<?php if (! empty($healthplix_enabled)) : ?>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label">HealthPlix Doctor Identifier</label>
|
|
||||||
<input class="form-control" name="input_healthplix_doctor_identifier" placeholder="UUID from HealthPlix" type="text" value="<?= esc($formData['input_healthplix_doctor_identifier'] ?? '') ?>">
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label">Date of Birth</label>
|
<label class="form-label">Date of Birth</label>
|
||||||
<input class="form-control" name="datepicker_dob" type="date" value="<?= esc($doctorDobValue) ?>">
|
<input class="form-control" name="datepicker_dob" type="date" value="<?= esc($doctorDobValue) ?>">
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
$doctorRegNo = '';
|
$doctorRegNo = '';
|
||||||
$doctorHprId = '';
|
$doctorHprId = '';
|
||||||
$doctorHealthplixIdentifier = '';
|
|
||||||
$hospitalDefaultOpdDays = (int) ($hospital_default_opd_valid_no_days ?? 5);
|
$hospitalDefaultOpdDays = (int) ($hospital_default_opd_valid_no_days ?? 5);
|
||||||
if ($hospitalDefaultOpdDays < 1) {
|
if ($hospitalDefaultOpdDays < 1) {
|
||||||
$hospitalDefaultOpdDays = 5;
|
$hospitalDefaultOpdDays = 5;
|
||||||
|
|
@ -37,13 +36,6 @@ if (!empty($data) && !empty($data[0])) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (['healthplix_doctor_identifier', 'healthplix_identifier'] as $hpField) {
|
|
||||||
if (isset($data[0]->{$hpField}) && trim((string) $data[0]->{$hpField}) !== '') {
|
|
||||||
$doctorHealthplixIdentifier = trim((string) $data[0]->{$hpField});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
<?php
|
<?php
|
||||||
|
|
@ -110,12 +102,7 @@ if (!empty($data) && !empty($data[0]) && !empty($data[0]->dob)) {
|
||||||
<label class="form-label">HPR ID</label>
|
<label class="form-label">HPR ID</label>
|
||||||
<input class="form-control" name="input_hpr_id" placeholder="ABDM HPR ID" type="text" value="<?= esc($doctorHprId) ?>">
|
<input class="form-control" name="input_hpr_id" placeholder="ABDM HPR ID" type="text" value="<?= esc($doctorHprId) ?>">
|
||||||
</div>
|
</div>
|
||||||
<?php if (! empty($healthplix_enabled)) : ?>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">HealthPlix Doctor Identifier</label>
|
|
||||||
<input class="form-control" name="input_healthplix_doctor_identifier" placeholder="UUID from HealthPlix" type="text" value="<?= esc($doctorHealthplixIdentifier) ?>">
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label">Date of Birth</label>
|
<label class="form-label">Date of Birth</label>
|
||||||
<input class="form-control" name="datepicker_dob" value="<?= esc($doctorDobValue) ?>" type="date" />
|
<input class="form-control" name="datepicker_dob" value="<?= esc($doctorDobValue) ?>" type="date" />
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@
|
||||||
.badge-channel-bridge { background:#0d6efd; }
|
.badge-channel-bridge { background:#0d6efd; }
|
||||||
.badge-channel-eatria { background:#198754; }
|
.badge-channel-eatria { background:#198754; }
|
||||||
.badge-channel-csnotk { background:#6f42c1; }
|
.badge-channel-csnotk { background:#6f42c1; }
|
||||||
.badge-channel-healthplix{ background:#fd7e14; }
|
|
||||||
.status-success { color:#198754; font-weight:600; }
|
.status-success { color:#198754; font-weight:600; }
|
||||||
.status-error { color:#dc3545; font-weight:600; }
|
.status-error { color:#dc3545; font-weight:600; }
|
||||||
.status-pending { color:#fd7e14; font-weight:600; }
|
.status-pending { color:#fd7e14; font-weight:600; }
|
||||||
|
|
@ -64,7 +63,6 @@
|
||||||
<option value="bridge">bridge</option>
|
<option value="bridge">bridge</option>
|
||||||
<option value="csnotk">csnotk</option>
|
<option value="csnotk">csnotk</option>
|
||||||
<option value="abdm">abdm</option>
|
<option value="abdm">abdm</option>
|
||||||
<option value="healthplix">healthplix</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
|
|
@ -194,7 +192,7 @@ function statusClass(s) {
|
||||||
return 'status-pending';
|
return 'status-pending';
|
||||||
}
|
}
|
||||||
function channelBadge(ch) {
|
function channelBadge(ch) {
|
||||||
const map = { eatria_bridge:'success', bridge:'primary', csnotk:'purple', abdm:'info', healthplix:'warning' };
|
const map = { eatria_bridge:'success', bridge:'primary', csnotk:'purple', abdm:'info' };
|
||||||
const color = map[ch] ?? 'secondary';
|
const color = map[ch] ?? 'secondary';
|
||||||
return `<span class="badge bg-${color}">${escHtml(ch ?? '-')}</span>`;
|
return `<span class="badge bg-${color}">${escHtml(ch ?? '-')}</span>`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -314,15 +314,7 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php $hpPatientSyncId = trim((string) ($data[0]->healthplix_sync_id ?? '')); ?>
|
|
||||||
<?php if ($hpPatientSyncId !== '') : ?>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-3 col-md-4 label">HealthPlix ID</div>
|
|
||||||
<div class="col-lg-9 col-md-8">
|
|
||||||
<span class="badge bg-success-subtle text-success border border-success-subtle"><?= esc($hpPatientSyncId) ?></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-3 col-md-4 label">Phone</div>
|
<div class="col-lg-3 col-md-4 label">Phone</div>
|
||||||
<div class="col-lg-9 col-md-8">
|
<div class="col-lg-9 col-md-8">
|
||||||
|
|
|
||||||
|
|
@ -625,7 +625,7 @@ $historyFields = [
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<input type="hidden" name="complaint_remove_id" id="complaint_remove_id" value="0">
|
<input type="hidden" name="complaint_remove_id" id="complaint_remove_id" value="0">
|
||||||
|
|
||||||
<!-- Healthplix-style inline complaint table (OPD-like Autotext) -->
|
<!-- Inline complaint table (OPD-like Autotext) -->
|
||||||
<table class="table table-sm table-bordered align-middle mb-1" id="discharge_complaint_table">
|
<table class="table table-sm table-bordered align-middle mb-1" id="discharge_complaint_table">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
||||||
|
|
@ -1023,7 +1023,7 @@
|
||||||
title="Save as custom keyword">+keyword</button>
|
title="Save as custom keyword">+keyword</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Healthplix-style inline complaint table -->
|
<!-- Inline complaint table -->
|
||||||
<table class="table table-sm table-bordered align-middle mb-1" id="complaint_table">
|
<table class="table table-sm table-bordered align-middle mb-1" id="complaint_table">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -1100,7 +1100,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Healthplix-style inline diagnosis table -->
|
<!-- Inline diagnosis table -->
|
||||||
<table class="table table-sm table-bordered align-middle mb-1" id="diagnosis_table">
|
<table class="table table-sm table-bordered align-middle mb-1" id="diagnosis_table">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
||||||
|
|
@ -1,156 +0,0 @@
|
||||||
# HealthPlix Reverse Proxy Setup (CentOS 7 + Apache)
|
|
||||||
|
|
||||||
This guide is for a central public Apache server that forwards each hospital domain to its own local HMS over VPN.
|
|
||||||
|
|
||||||
Example topology:
|
|
||||||
- Public Apache server: 139.59.13.39
|
|
||||||
- Hospital A domain: 100001.dhms.in -> A record -> 139.59.13.39
|
|
||||||
- Hospital A HMS private VPN IP: 10.7.0.32
|
|
||||||
- Hospital B domain: 100002.dhms.in -> A record -> 139.59.13.39
|
|
||||||
- Hospital B HMS private VPN IP: 10.8.0.45
|
|
||||||
|
|
||||||
Recommended exposure model:
|
|
||||||
- Expose only callback/fetch path to internet.
|
|
||||||
- Do not expose full HMS unless required.
|
|
||||||
|
|
||||||
## 1) Install and enable required Apache modules
|
|
||||||
|
|
||||||
Run on CentOS 7 Apache host:
|
|
||||||
|
|
||||||
sudo yum install -y mod_ssl
|
|
||||||
sudo httpd -M | egrep "proxy|ssl|headers|rewrite"
|
|
||||||
|
|
||||||
If any are missing, enable in Apache config and restart httpd.
|
|
||||||
|
|
||||||
## 2) Reusable virtual-host template (callback-only)
|
|
||||||
|
|
||||||
Create one file per hospital in /etc/httpd/conf.d/
|
|
||||||
|
|
||||||
Template file name:
|
|
||||||
- /etc/httpd/conf.d/DOMAIN.conf
|
|
||||||
|
|
||||||
Template content:
|
|
||||||
|
|
||||||
<VirtualHost *:80>
|
|
||||||
ServerName DOMAIN
|
|
||||||
RewriteEngine On
|
|
||||||
RewriteRule ^/(.*)$ https://DOMAIN/$1 [R=301,L]
|
|
||||||
</VirtualHost>
|
|
||||||
|
|
||||||
<VirtualHost *:443>
|
|
||||||
ServerName DOMAIN
|
|
||||||
|
|
||||||
SSLEngine On
|
|
||||||
SSLCertificateFile /etc/letsencrypt/live/DOMAIN/fullchain.pem
|
|
||||||
SSLCertificateKeyFile /etc/letsencrypt/live/DOMAIN/privkey.pem
|
|
||||||
|
|
||||||
ProxyRequests Off
|
|
||||||
ProxyPreserveHost On
|
|
||||||
SSLProxyEngine On
|
|
||||||
|
|
||||||
RequestHeader set X-Forwarded-Proto "https"
|
|
||||||
RequestHeader append X-Forwarded-For %{REMOTE_ADDR}s
|
|
||||||
|
|
||||||
ProxyTimeout 60
|
|
||||||
Timeout 60
|
|
||||||
|
|
||||||
# Callback/fetch endpoint only
|
|
||||||
ProxyPass /healthplix/fetch http://VPN_HMS_IP/healthplix/fetch
|
|
||||||
ProxyPassReverse /healthplix/fetch http://VPN_HMS_IP/healthplix/fetch
|
|
||||||
|
|
||||||
ErrorLog /var/log/httpd/DOMAIN-healthplix-error.log
|
|
||||||
CustomLog /var/log/httpd/DOMAIN-healthplix-access.log combined
|
|
||||||
</VirtualHost>
|
|
||||||
|
|
||||||
Replace:
|
|
||||||
- DOMAIN with hospital DNS name (example 100001.dhms.in)
|
|
||||||
- VPN_HMS_IP with local HMS VPN IP (example 10.7.0.32)
|
|
||||||
|
|
||||||
## 3) Hardened variant (recommended)
|
|
||||||
|
|
||||||
Use this inside the :443 VirtualHost when HealthPlix source IPs and secret are available.
|
|
||||||
|
|
||||||
# 3.1 Limit endpoint to known HealthPlix source IPs
|
|
||||||
<Location /healthplix/fetch>
|
|
||||||
Require all denied
|
|
||||||
Require ip 203.0.113.10
|
|
||||||
Require ip 203.0.113.11
|
|
||||||
</Location>
|
|
||||||
|
|
||||||
# 3.2 Shared-secret header validation at edge
|
|
||||||
RewriteEngine On
|
|
||||||
RewriteCond %{REQUEST_URI} ^/healthplix/fetch$
|
|
||||||
RewriteCond %{HTTP:X-Healthplix-Secret} !^REPLACE_WITH_STRONG_SECRET$
|
|
||||||
RewriteRule ^ - [F,L]
|
|
||||||
|
|
||||||
# 3.3 Restrict methods
|
|
||||||
<Location /healthplix/fetch>
|
|
||||||
<LimitExcept POST>
|
|
||||||
Require all denied
|
|
||||||
</LimitExcept>
|
|
||||||
</Location>
|
|
||||||
|
|
||||||
# 3.4 Body size limits (adjust to payload)
|
|
||||||
LimitRequestBody 1048576
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- Replace placeholder IPs with official HealthPlix outbound IPs.
|
|
||||||
- Replace secret with a long random value.
|
|
||||||
- Validate same secret again in HMS controller before processing.
|
|
||||||
|
|
||||||
## 4) Multi-hospital mapping examples
|
|
||||||
|
|
||||||
Hospital A:
|
|
||||||
- Domain: 100001.dhms.in
|
|
||||||
- Target: 10.7.0.32
|
|
||||||
|
|
||||||
Hospital B:
|
|
||||||
- Domain: 100002.dhms.in
|
|
||||||
- Target: 10.8.0.45
|
|
||||||
|
|
||||||
Hospital C:
|
|
||||||
- Domain: 100003.dhms.in
|
|
||||||
- Target: 10.9.0.20
|
|
||||||
|
|
||||||
All domains can point to same public IP 139.59.13.39.
|
|
||||||
Apache routes by ServerName.
|
|
||||||
|
|
||||||
## 5) Enable and reload Apache
|
|
||||||
|
|
||||||
sudo apachectl configtest
|
|
||||||
sudo systemctl reload httpd
|
|
||||||
|
|
||||||
If using firewalld:
|
|
||||||
|
|
||||||
sudo firewall-cmd --add-service=http --permanent
|
|
||||||
sudo firewall-cmd --add-service=https --permanent
|
|
||||||
sudo firewall-cmd --reload
|
|
||||||
|
|
||||||
## 6) HMS application-side settings
|
|
||||||
|
|
||||||
For outbound sync from HMS to HealthPlix:
|
|
||||||
- Keep HEALTHPLIX_BASE_URL as official HealthPlix API base URL.
|
|
||||||
- Keep token/patient/appointment paths as currently configured.
|
|
||||||
|
|
||||||
For inbound fetch/callback from HealthPlix:
|
|
||||||
- Give HealthPlix callback URL as:
|
|
||||||
https://DOMAIN/healthplix/fetch
|
|
||||||
|
|
||||||
## 7) Validation checklist
|
|
||||||
|
|
||||||
1. DNS resolves correctly:
|
|
||||||
nslookup DOMAIN
|
|
||||||
2. TLS certificate is valid:
|
|
||||||
curl -Iv https://DOMAIN/healthplix/fetch
|
|
||||||
3. Reverse proxy reaches HMS private node:
|
|
||||||
curl -i -X POST https://DOMAIN/healthplix/fetch -H "X-Healthplix-Secret: REPLACE_WITH_STRONG_SECRET" -d '{}'
|
|
||||||
4. Apache logs show routed request in domain-specific log file.
|
|
||||||
5. HMS receives request and returns expected response.
|
|
||||||
|
|
||||||
## 8) Operations notes
|
|
||||||
|
|
||||||
- Keep one vhost file per hospital for isolation.
|
|
||||||
- Keep one log file pair per hospital for easier troubleshooting.
|
|
||||||
- Rotate logs and monitor 4xx/5xx rates.
|
|
||||||
- Keep VPN routes and ACLs documented and version-controlled.
|
|
||||||
- Do not log bearer tokens or patient-sensitive payloads in plaintext.
|
|
||||||
Binary file not shown.
|
|
@ -1,51 +0,0 @@
|
||||||
<VirtualHost *:80>
|
|
||||||
ServerName 100001.dhms.in
|
|
||||||
RewriteEngine On
|
|
||||||
RewriteRule ^/(.*)$ https://100001.dhms.in/$1 [R=301,L]
|
|
||||||
</VirtualHost>
|
|
||||||
|
|
||||||
<VirtualHost *:443>
|
|
||||||
ServerName 100001.dhms.in
|
|
||||||
|
|
||||||
SSLEngine On
|
|
||||||
SSLCertificateFile /etc/letsencrypt/live/100001.dhms.in/fullchain.pem
|
|
||||||
SSLCertificateKeyFile /etc/letsencrypt/live/100001.dhms.in/privkey.pem
|
|
||||||
|
|
||||||
ProxyRequests Off
|
|
||||||
ProxyPreserveHost On
|
|
||||||
SSLProxyEngine On
|
|
||||||
SSLProxyVerify require
|
|
||||||
SSLProxyCheckPeerName on
|
|
||||||
SSLProxyCheckPeerExpire on
|
|
||||||
|
|
||||||
RequestHeader set X-Forwarded-Proto "https"
|
|
||||||
RequestHeader append X-Forwarded-For %{REMOTE_ADDR}s
|
|
||||||
|
|
||||||
ProxyTimeout 60
|
|
||||||
Timeout 60
|
|
||||||
|
|
||||||
# Replace with official HealthPlix source IPs
|
|
||||||
<Location /healthplix/fetch>
|
|
||||||
Require all denied
|
|
||||||
Require ip 203.0.113.10
|
|
||||||
Require ip 203.0.113.11
|
|
||||||
<LimitExcept POST>
|
|
||||||
Require all denied
|
|
||||||
</LimitExcept>
|
|
||||||
</Location>
|
|
||||||
|
|
||||||
# Replace with your strong shared secret
|
|
||||||
RewriteEngine On
|
|
||||||
RewriteCond %{REQUEST_URI} ^/healthplix/fetch$
|
|
||||||
RewriteCond %{HTTP:X-Healthplix-Secret} !^CHANGE_ME_TO_STRONG_SECRET$
|
|
||||||
RewriteRule ^ - [F,L]
|
|
||||||
|
|
||||||
LimitRequestBody 1048576
|
|
||||||
|
|
||||||
# Use internal DNS name reachable via VPN so backend TLS/certificate can be used.
|
|
||||||
ProxyPass /healthplix/fetch https://khrc.dhms.in/healthplix/fetch
|
|
||||||
ProxyPassReverse /healthplix/fetch https://khrc.dhms.in/healthplix/fetch
|
|
||||||
|
|
||||||
ErrorLog /var/log/httpd/100001-healthplix-error.log
|
|
||||||
CustomLog /var/log/httpd/100001-healthplix-access.log combined
|
|
||||||
</VirtualHost>
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
<VirtualHost *:80>
|
|
||||||
ServerName 100002.dhms.in
|
|
||||||
RewriteEngine On
|
|
||||||
RewriteRule ^/(.*)$ https://100002.dhms.in/$1 [R=301,L]
|
|
||||||
</VirtualHost>
|
|
||||||
|
|
||||||
<VirtualHost *:443>
|
|
||||||
ServerName 100002.dhms.in
|
|
||||||
|
|
||||||
SSLEngine On
|
|
||||||
SSLCertificateFile /etc/letsencrypt/live/100002.dhms.in/fullchain.pem
|
|
||||||
SSLCertificateKeyFile /etc/letsencrypt/live/100002.dhms.in/privkey.pem
|
|
||||||
|
|
||||||
ProxyRequests Off
|
|
||||||
ProxyPreserveHost On
|
|
||||||
SSLProxyEngine On
|
|
||||||
|
|
||||||
RequestHeader set X-Forwarded-Proto "https"
|
|
||||||
RequestHeader append X-Forwarded-For %{REMOTE_ADDR}s
|
|
||||||
|
|
||||||
ProxyTimeout 60
|
|
||||||
Timeout 60
|
|
||||||
|
|
||||||
# Replace with official HealthPlix source IPs
|
|
||||||
<Location /healthplix/fetch>
|
|
||||||
Require all denied
|
|
||||||
Require ip 203.0.113.10
|
|
||||||
Require ip 203.0.113.11
|
|
||||||
<LimitExcept POST>
|
|
||||||
Require all denied
|
|
||||||
</LimitExcept>
|
|
||||||
</Location>
|
|
||||||
|
|
||||||
# Replace with your strong shared secret
|
|
||||||
RewriteEngine On
|
|
||||||
RewriteCond %{REQUEST_URI} ^/healthplix/fetch$
|
|
||||||
RewriteCond %{HTTP:X-Healthplix-Secret} !^CHANGE_ME_TO_STRONG_SECRET$
|
|
||||||
RewriteRule ^ - [F,L]
|
|
||||||
|
|
||||||
LimitRequestBody 1048576
|
|
||||||
|
|
||||||
ProxyPass /healthplix/fetch http://10.8.0.45/healthplix/fetch
|
|
||||||
ProxyPassReverse /healthplix/fetch http://10.8.0.45/healthplix/fetch
|
|
||||||
|
|
||||||
ErrorLog /var/log/httpd/100002-healthplix-error.log
|
|
||||||
CustomLog /var/log/httpd/100002-healthplix-access.log combined
|
|
||||||
</VirtualHost>
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
<VirtualHost *:80>
|
|
||||||
ServerName 100003.dhms.in
|
|
||||||
RewriteEngine On
|
|
||||||
RewriteRule ^/(.*)$ https://100003.dhms.in/$1 [R=301,L]
|
|
||||||
</VirtualHost>
|
|
||||||
|
|
||||||
<VirtualHost *:443>
|
|
||||||
ServerName 100003.dhms.in
|
|
||||||
|
|
||||||
SSLEngine On
|
|
||||||
SSLCertificateFile /etc/letsencrypt/live/100003.dhms.in/fullchain.pem
|
|
||||||
SSLCertificateKeyFile /etc/letsencrypt/live/100003.dhms.in/privkey.pem
|
|
||||||
|
|
||||||
ProxyRequests Off
|
|
||||||
ProxyPreserveHost On
|
|
||||||
SSLProxyEngine On
|
|
||||||
|
|
||||||
RequestHeader set X-Forwarded-Proto "https"
|
|
||||||
RequestHeader append X-Forwarded-For %{REMOTE_ADDR}s
|
|
||||||
|
|
||||||
ProxyTimeout 60
|
|
||||||
Timeout 60
|
|
||||||
|
|
||||||
# Replace with official HealthPlix source IPs
|
|
||||||
<Location /healthplix/fetch>
|
|
||||||
Require all denied
|
|
||||||
Require ip 203.0.113.10
|
|
||||||
Require ip 203.0.113.11
|
|
||||||
<LimitExcept POST>
|
|
||||||
Require all denied
|
|
||||||
</LimitExcept>
|
|
||||||
</Location>
|
|
||||||
|
|
||||||
# Replace with your strong shared secret
|
|
||||||
RewriteEngine On
|
|
||||||
RewriteCond %{REQUEST_URI} ^/healthplix/fetch$
|
|
||||||
RewriteCond %{HTTP:X-Healthplix-Secret} !^CHANGE_ME_TO_STRONG_SECRET$
|
|
||||||
RewriteRule ^ - [F,L]
|
|
||||||
|
|
||||||
LimitRequestBody 1048576
|
|
||||||
|
|
||||||
ProxyPass /healthplix/fetch http://10.9.0.20/healthplix/fetch
|
|
||||||
ProxyPassReverse /healthplix/fetch http://10.9.0.20/healthplix/fetch
|
|
||||||
|
|
||||||
ErrorLog /var/log/httpd/100003-healthplix-error.log
|
|
||||||
CustomLog /var/log/httpd/100003-healthplix-access.log combined
|
|
||||||
</VirtualHost>
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
# Local-First Validation Checklist (My Host + VPN Server)
|
|
||||||
|
|
||||||
## A) My Host local test (10.7.0.31 machine)
|
|
||||||
|
|
||||||
1. Start CI4 locally:
|
|
||||||
php spark serve --host localhost --port 8080
|
|
||||||
|
|
||||||
2. Add hosts entry from my-host/hosts.example:
|
|
||||||
127.0.0.1 100001.localtest
|
|
||||||
|
|
||||||
3. Enable local Apache vhost:
|
|
||||||
my-host/100001.localtest.conf
|
|
||||||
|
|
||||||
4. Reload local Apache.
|
|
||||||
|
|
||||||
5. Test through local domain (same callback path):
|
|
||||||
curl -i -X POST http://100001.localtest/healthplix/fetch -H "X-Healthplix-Secret: CHANGE_ME_TO_STRONG_SECRET" -d '{}'
|
|
||||||
|
|
||||||
Expected:
|
|
||||||
- Request reaches CI4 route at localhost:8080/healthplix/fetch.
|
|
||||||
|
|
||||||
## B) VPN server test (10.7.0.1)
|
|
||||||
|
|
||||||
1. Install config:
|
|
||||||
vpn-server/100001.dhms.in.conf -> /etc/httpd/conf.d/100001.dhms.in.conf
|
|
||||||
|
|
||||||
2. Validate and reload Apache:
|
|
||||||
apachectl configtest
|
|
||||||
systemctl reload httpd
|
|
||||||
|
|
||||||
3. From your VPN client machine (10.7.0.31), verify DNS and TLS:
|
|
||||||
nslookup 100001.dhms.in
|
|
||||||
curl -Iv https://100001.dhms.in/healthplix/fetch
|
|
||||||
|
|
||||||
4. Verify callback path through edge proxy:
|
|
||||||
curl -i -X POST https://100001.dhms.in/healthplix/fetch -H "X-Healthplix-Secret: CHANGE_ME_TO_STRONG_SECRET" -d '{}'
|
|
||||||
|
|
||||||
Expected:
|
|
||||||
- Edge server accepts request.
|
|
||||||
- Request is forwarded to khrc.dhms.in over VPN.
|
|
||||||
- HMS responds with your fetch endpoint response.
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- Replace placeholder source IPs and secret before production.
|
|
||||||
- If backend HMS certificate is private/self-signed, import internal CA cert to proxy trust store.
|
|
||||||
- Keep healthplix/fetch endpoint method-restricted to POST.
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
# Local machine Apache config for pre-production testing
|
|
||||||
# Purpose: test callback flow locally by forwarding to CI4 dev server (php spark serve)
|
|
||||||
# Place on local Apache host and adapt paths/ports for your OS.
|
|
||||||
|
|
||||||
<VirtualHost *:80>
|
|
||||||
ServerName 100001.localtest
|
|
||||||
|
|
||||||
ProxyRequests Off
|
|
||||||
ProxyPreserveHost On
|
|
||||||
|
|
||||||
RequestHeader set X-Forwarded-Proto "http"
|
|
||||||
RequestHeader append X-Forwarded-For %{REMOTE_ADDR}s
|
|
||||||
|
|
||||||
ProxyTimeout 60
|
|
||||||
Timeout 60
|
|
||||||
|
|
||||||
# Keep same route shape as production, but target local spark server.
|
|
||||||
ProxyPass /healthplix/fetch http://localhost:8080/healthplix/fetch
|
|
||||||
ProxyPassReverse /healthplix/fetch http://localhost:8080/healthplix/fetch
|
|
||||||
|
|
||||||
ErrorLog logs/100001-localtest-error.log
|
|
||||||
CustomLog logs/100001-localtest-access.log combined
|
|
||||||
</VirtualHost>
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
# Add to your local hosts file for local-domain testing
|
|
||||||
# Windows path: C:\Windows\System32\drivers\etc\hosts
|
|
||||||
# Linux path: /etc/hosts
|
|
||||||
|
|
||||||
127.0.0.1 100001.localtest
|
|
||||||
|
|
||||||
# Optional for testing real VPN domain from your machine over VPN
|
|
||||||
# 10.7.0.1 100001.dhms.in
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
# CentOS 7 VPN/public proxy server config
|
|
||||||
# Place on VPN server: /etc/httpd/conf.d/100001.dhms.in.conf
|
|
||||||
|
|
||||||
<VirtualHost *:80>
|
|
||||||
ServerName 100001.dhms.in
|
|
||||||
RewriteEngine On
|
|
||||||
RewriteRule ^/(.*)$ https://100001.dhms.in/$1 [R=301,L]
|
|
||||||
</VirtualHost>
|
|
||||||
|
|
||||||
<VirtualHost *:443>
|
|
||||||
ServerName 100001.dhms.in
|
|
||||||
|
|
||||||
SSLEngine On
|
|
||||||
SSLCertificateFile /etc/letsencrypt/live/100001.dhms.in/fullchain.pem
|
|
||||||
SSLCertificateKeyFile /etc/letsencrypt/live/100001.dhms.in/privkey.pem
|
|
||||||
|
|
||||||
ProxyRequests Off
|
|
||||||
ProxyPreserveHost On
|
|
||||||
SSLProxyEngine On
|
|
||||||
|
|
||||||
RequestHeader set X-Forwarded-Proto "https"
|
|
||||||
RequestHeader append X-Forwarded-For %{REMOTE_ADDR}s
|
|
||||||
|
|
||||||
ProxyTimeout 60
|
|
||||||
Timeout 60
|
|
||||||
|
|
||||||
# Replace with official HealthPlix source IPs
|
|
||||||
<Location /healthplix/fetch>
|
|
||||||
Require all denied
|
|
||||||
Require ip 203.0.113.10
|
|
||||||
Require ip 203.0.113.11
|
|
||||||
<LimitExcept POST>
|
|
||||||
Require all denied
|
|
||||||
</LimitExcept>
|
|
||||||
</Location>
|
|
||||||
|
|
||||||
# Replace with your strong shared secret
|
|
||||||
RewriteEngine On
|
|
||||||
RewriteCond %{REQUEST_URI} ^/healthplix/fetch$
|
|
||||||
RewriteCond %{HTTP:X-Healthplix-Secret} !^CHANGE_ME_TO_STRONG_SECRET$
|
|
||||||
RewriteRule ^ - [F,L]
|
|
||||||
|
|
||||||
LimitRequestBody 1048576
|
|
||||||
|
|
||||||
# Production target: HMS server over VPN
|
|
||||||
ProxyPass /healthplix/fetch https://khrc.dhms.in/healthplix/fetch
|
|
||||||
ProxyPassReverse /healthplix/fetch https://khrc.dhms.in/healthplix/fetch
|
|
||||||
|
|
||||||
ErrorLog /var/log/httpd/100001-healthplix-error.log
|
|
||||||
CustomLog /var/log/httpd/100001-healthplix-access.log combined
|
|
||||||
</VirtualHost>
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
# Copy to /etc/httpd/conf.d/DOMAIN.conf and replace placeholders.
|
|
||||||
# This variant includes source IP allowlist and shared-secret header check.
|
|
||||||
|
|
||||||
<VirtualHost *:80>
|
|
||||||
ServerName __DOMAIN__
|
|
||||||
RewriteEngine On
|
|
||||||
RewriteRule ^/(.*)$ https://__DOMAIN__/$1 [R=301,L]
|
|
||||||
</VirtualHost>
|
|
||||||
|
|
||||||
<VirtualHost *:443>
|
|
||||||
ServerName __DOMAIN__
|
|
||||||
|
|
||||||
SSLEngine On
|
|
||||||
SSLCertificateFile /etc/letsencrypt/live/__DOMAIN__/fullchain.pem
|
|
||||||
SSLCertificateKeyFile /etc/letsencrypt/live/__DOMAIN__/privkey.pem
|
|
||||||
|
|
||||||
ProxyRequests Off
|
|
||||||
ProxyPreserveHost On
|
|
||||||
SSLProxyEngine On
|
|
||||||
|
|
||||||
RequestHeader set X-Forwarded-Proto "https"
|
|
||||||
RequestHeader append X-Forwarded-For %{REMOTE_ADDR}s
|
|
||||||
|
|
||||||
ProxyTimeout 60
|
|
||||||
Timeout 60
|
|
||||||
|
|
||||||
# Allow only known HealthPlix source IPs (replace with official list)
|
|
||||||
<Location /healthplix/fetch>
|
|
||||||
Require all denied
|
|
||||||
Require ip __HEALTHPLIX_IP_1__
|
|
||||||
Require ip __HEALTHPLIX_IP_2__
|
|
||||||
<LimitExcept POST>
|
|
||||||
Require all denied
|
|
||||||
</LimitExcept>
|
|
||||||
</Location>
|
|
||||||
|
|
||||||
# Block requests without valid shared secret header
|
|
||||||
RewriteEngine On
|
|
||||||
RewriteCond %{REQUEST_URI} ^/healthplix/fetch$
|
|
||||||
RewriteCond %{HTTP:X-Healthplix-Secret} !^__STRONG_SHARED_SECRET__$
|
|
||||||
RewriteRule ^ - [F,L]
|
|
||||||
|
|
||||||
# Limit payload size to 1 MB (adjust as needed)
|
|
||||||
LimitRequestBody 1048576
|
|
||||||
|
|
||||||
ProxyPass /healthplix/fetch http://__VPN_HMS_IP__/healthplix/fetch
|
|
||||||
ProxyPassReverse /healthplix/fetch http://__VPN_HMS_IP__/healthplix/fetch
|
|
||||||
|
|
||||||
ErrorLog /var/log/httpd/__DOMAIN__-healthplix-error.log
|
|
||||||
CustomLog /var/log/httpd/__DOMAIN__-healthplix-access.log combined
|
|
||||||
</VirtualHost>
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
# Copy to /etc/httpd/conf.d/DOMAIN.conf and replace placeholders.
|
|
||||||
|
|
||||||
<VirtualHost *:80>
|
|
||||||
ServerName __DOMAIN__
|
|
||||||
RewriteEngine On
|
|
||||||
RewriteRule ^/(.*)$ https://__DOMAIN__/$1 [R=301,L]
|
|
||||||
</VirtualHost>
|
|
||||||
|
|
||||||
<VirtualHost *:443>
|
|
||||||
ServerName __DOMAIN__
|
|
||||||
|
|
||||||
SSLEngine On
|
|
||||||
SSLCertificateFile /etc/letsencrypt/live/__DOMAIN__/fullchain.pem
|
|
||||||
SSLCertificateKeyFile /etc/letsencrypt/live/__DOMAIN__/privkey.pem
|
|
||||||
|
|
||||||
ProxyRequests Off
|
|
||||||
ProxyPreserveHost On
|
|
||||||
SSLProxyEngine On
|
|
||||||
|
|
||||||
RequestHeader set X-Forwarded-Proto "https"
|
|
||||||
RequestHeader append X-Forwarded-For %{REMOTE_ADDR}s
|
|
||||||
|
|
||||||
ProxyTimeout 60
|
|
||||||
Timeout 60
|
|
||||||
|
|
||||||
ProxyPass /healthplix/fetch http://__VPN_HMS_IP__/healthplix/fetch
|
|
||||||
ProxyPassReverse /healthplix/fetch http://__VPN_HMS_IP__/healthplix/fetch
|
|
||||||
|
|
||||||
ErrorLog /var/log/httpd/__DOMAIN__-healthplix-error.log
|
|
||||||
CustomLog /var/log/httpd/__DOMAIN__-healthplix-access.log combined
|
|
||||||
</VirtualHost>
|
|
||||||
22
env
22
env
|
|
@ -100,28 +100,6 @@
|
||||||
# If empty, signature validation is skipped.
|
# If empty, signature validation is skipped.
|
||||||
# EKA_WEBHOOK_SECRET =
|
# EKA_WEBHOOK_SECRET =
|
||||||
|
|
||||||
#--------------------------------------------------------------------
|
|
||||||
# HEALTHPLIX INTEGRATION (OPTIONAL)
|
|
||||||
#--------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Enable per hospital using Admin panel by default.
|
|
||||||
# These env keys can be used as deployment fallback defaults.
|
|
||||||
|
|
||||||
# HEALTHPLIX_ENABLED = 0
|
|
||||||
# HEALTHPLIX_BASE_URL =
|
|
||||||
# HEALTHPLIX_TENANT_HEADER_NAME = tenant_id
|
|
||||||
# HEALTHPLIX_TENANT_ID =
|
|
||||||
# HEALTHPLIX_TENANT_KEY =
|
|
||||||
# HEALTHPLIX_TOKEN_PATH =
|
|
||||||
# HEALTHPLIX_PATIENT_PATH =
|
|
||||||
# HEALTHPLIX_APPOINTMENT_PATH =
|
|
||||||
# HEALTHPLIX_DOCTOR_IDENTIFIER =
|
|
||||||
# HEALTHPLIX_DOCTOR_EMAIL =
|
|
||||||
# HEALTHPLIX_SERVICE_IDENTIFIER =
|
|
||||||
# HEALTHPLIX_SERVICE_NAME =
|
|
||||||
# Optional fallback secret used by POST /healthplix/fetch when hospital_setting has no value.
|
|
||||||
# HEALTHPLIX_FETCH_SECRET =
|
|
||||||
|
|
||||||
#--------------------------------------------------------------------
|
#--------------------------------------------------------------------
|
||||||
# SNOMED / CSNOTK TERMINOLOGY (OPTIONAL)
|
# SNOMED / CSNOTK TERMINOLOGY (OPTIONAL)
|
||||||
#--------------------------------------------------------------------
|
#--------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -449,12 +449,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gallery-item" onclick="openLb(this)" data-caption="Admin Panel — Doctor Master, User Management, AI Settings, HealthPlix, ABDM Gateway, Hospital Profile, Insurance, Bank, Bed Management, Stock">
|
<div class="gallery-item" onclick="openLb(this)" data-caption="Admin Panel — Doctor Master, User Management, AI Settings, ABDM Gateway, Hospital Profile, Insurance, Bank, Bed Management, Stock">
|
||||||
<img class="screen-img" src="assets/img/screenshots/image8.png" alt="Admin Panel">
|
<img class="screen-img" src="assets/img/screenshots/image8.png" alt="Admin Panel">
|
||||||
<div class="gallery-caption">
|
<div class="gallery-caption">
|
||||||
<div class="cap-badge">Admin & Settings</div>
|
<div class="cap-badge">Admin & Settings</div>
|
||||||
<h4>Admin Control Panel</h4>
|
<h4>Admin Control Panel</h4>
|
||||||
<p>Tile-based admin panel: Doctor Master, User & Role Management, AI Settings, HealthPlix Integration, ABDM Gateway, Hospital Profile, Insurance Master, Bank & Payment, Medical Bank, Bed Management, Hospital Stock.</p>
|
<p>Tile-based admin panel: Doctor Master, User & Role Management, AI Settings, ABDM Gateway, Hospital Profile, Insurance Master, Bank & Payment, Medical Bank, Bed Management, Hospital Stock.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user