From 697e9ad874372fd78de153644fc17324f01edcfa Mon Sep 17 00:00:00 2001 From: Gowtham M Date: Fri, 8 May 2026 11:56:39 +0530 Subject: [PATCH 1/6] GWM : API Swagger for TAP --- app/Config/Acl.php | 1 + app/Config/Routes.php | 2 + app/Controllers/SwaggerController.php | 80 ++- app/Views/swagger/index.php | 34 +- openapi/tpa-external-integrations.yaml | 922 +++++++++++++++++++++++++ 5 files changed, 1027 insertions(+), 12 deletions(-) create mode 100644 openapi/tpa-external-integrations.yaml diff --git a/app/Config/Acl.php b/app/Config/Acl.php index b59905ff..67edbd35 100644 --- a/app/Config/Acl.php +++ b/app/Config/Acl.php @@ -42,6 +42,7 @@ class Acl '#^/sales#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, STAFF_ROLE_ID]], '#^/expense#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, STAFF_ROLE_ID]], '#^/sendDataToTPA#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID, MANAGER_ROLE_ID]], + '#^/swagger#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID, MANAGER_ROLE_ID]], diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 0129af3d..081b44cd 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -29,6 +29,8 @@ $routes->get('/chatbottest', 'ChatbotControllerNew::chatbotest'); $routes->get('/chatbot', 'ChatbotControllerNew::chatbot'); $routes->get('/swagger', 'SwaggerController::index', ['filter' => 'authMVC']); +$routes->get('/swagger/tpa', 'SwaggerController::tpa', ['filter' => 'authMVC']); +$routes->get('/swagger/tpa-spec', 'SwaggerController::tpaSpec', ['filter' => 'authMVC']); $routes->get('/fedeploy', 'DeployController::fedeploy_view', ['filter' => 'authMVC']); $routes->post('/fedeploy', 'DeployController::fedeploy', ['filter' => 'authMVC']); $routes->get('/visitOffBoardCheck', 'EmployeeController::visitOffBoardCheck'); diff --git a/app/Controllers/SwaggerController.php b/app/Controllers/SwaggerController.php index 75955f60..c84dcc08 100755 --- a/app/Controllers/SwaggerController.php +++ b/app/Controllers/SwaggerController.php @@ -1,10 +1,84 @@ base_url('assets/api.yaml'), + 'pageTitle' => 'Swagger UI', + 'swaggerPreauthorize' => [], + ]); + } + + /** + * Swagger UI for TPA external integrations OpenAPI spec (openapi/tpa-external-integrations.yaml). + * Pre-fills Authorize from the same `.env` keys the API controllers use (`getenv`). + */ + public function tpa() + { + return view('swagger/index', [ + 'specUrl' => base_url('swagger/tpa-spec'), + 'pageTitle' => 'TPA external APIs — Swagger UI', + 'swaggerPreauthorize' => $this->buildTpaSwaggerPreauthorize(), + ]); + } + + /** + * Values for Swagger UI Authorize (apiKey / basic). Uses runtime env — keep `/swagger/tpa` access-controlled. + * + * @return list> + */ + protected function buildTpaSwaggerPreauthorize(): array + { + return [ + ['kind' => 'apiKey', 'scheme' => 'MediAssistUsername', 'value' => $this->readEnvString('MEDI_ASSIST_API_USERNAME')], + ['kind' => 'apiKey', 'scheme' => 'MediAssistPassword', 'value' => $this->readEnvString('MEDI_ASSIST_API_PASSWORD')], + ['kind' => 'apiKey', 'scheme' => 'VidalSubscriptionKey', 'value' => $this->readEnvString('VIDAL_API_SUBSCRIPTION_KEY')], + ['kind' => 'apiKey', 'scheme' => 'VidalIrSubscriptionKey', 'value' => $this->readEnvString('VIDAL_SUBSCRIPTION_KEY')], + ['kind' => 'apiKey', 'scheme' => 'VidalWellnessSubscriptionKey', 'value' => $this->readEnvString('VIDAL_WELLNESS_SUBSCRIPTION_KEY')], + [ + 'kind' => 'basic', + 'scheme' => 'HealthIndiaTokenBasic', + 'username' => $this->readEnvString('HEALTH_INDIA_USERNAME'), + 'password' => $this->readEnvString('HEALTH_INDIA_PASSWORD'), + ], + ]; + } + + /** + * Read `.env` the same way as the rest of CodeIgniter (`env()` then `getenv()`). + */ + protected function readEnvString(string $key): string + { + if (function_exists('env')) { + $v = \env($key); + if ($v !== null) { + return \is_bool($v) ? '' : (string) $v; + } + } + + $g = \getenv($key); + + return $g === false ? '' : (string) $g; + } + + /** + * Serves the OpenAPI YAML from project root /openapi (not under public/). + */ + public function tpaSpec() + { + $path = ROOTPATH . 'openapi/tpa-external-integrations.yaml'; + if (! is_readable($path)) { + throw PageNotFoundException::forPageNotFound('OpenAPI spec not found.'); + } + + return $this->response + ->setHeader('Content-Type', 'application/yaml; charset=UTF-8') + ->setHeader('Cache-Control', 'public, max-age=300') + ->setBody(file_get_contents($path)); + } } \ No newline at end of file diff --git a/app/Views/swagger/index.php b/app/Views/swagger/index.php index 865b2a23..6ec0d909 100755 --- a/app/Views/swagger/index.php +++ b/app/Views/swagger/index.php @@ -4,7 +4,7 @@ - Swagger UI + <?= esc($pageTitle ?? 'Swagger UI') ?> @@ -31,15 +31,15 @@
- - + + diff --git a/openapi/tpa-external-integrations.yaml b/openapi/tpa-external-integrations.yaml new file mode 100644 index 00000000..af8fc6b0 --- /dev/null +++ b/openapi/tpa-external-integrations.yaml @@ -0,0 +1,922 @@ +openapi: 3.0.3 +info: + title: Nhance — TPA external API integrations + description: | + OpenAPI description of **outbound** HTTP calls made by Nhance to third-party TPA systems. + Base URLs and credentials come from environment variables (see **Authorize** / security schemes below). + This is **not** the public REST API of the Nhance application itself. + + Controllers: `MediAssistApiController`, `VidalApiController`, `VoloApiController`, + `FhplApiController`, `HealthIndiaApiController`. + + **Credentials (.env)** — use the same variable names as `getenv()` / `env()` in PHP: + MediAssist: `MEDI_ASSIST_API_USERNAME`, `MEDI_ASSIST_API_PASSWORD`; URLs: `MEDI_ASSIST_API_BASE_URL_*`. + Vidal: `VIDAL_API_SUBSCRIPTION_KEY`, `VIDAL_SUBSCRIPTION_KEY` (IR only), `VIDAL_WELLNESS_SUBSCRIPTION_KEY`; base `VIDAL_API_BASE_URL`. + Volo login: `VOLO_API_EMAIL`, `VOLO_API_PASSWORD`, `VOLO_LOGGED_IN_PORTAL`; bases `VOLO_API_ADMIN_BASE_URL`, `VOLO_API_CONSUMER_BASE_URL`. + FHPL: `FHPL_TOKEN_URL`, `FHPL_USER_NAME`, `FHPL_PASSWORD`, `FHPL_GRANT_TYPE`; API `FHPL_BASE_URL`. + Health India: `HEALTH_INDIA_TOKEN_URL`, `HEALTH_INDIA_USERNAME`, `HEALTH_INDIA_PASSWORD`; API `HEALTH_INDIA_BASE_URL`. + version: 1.0.0 + +tags: + - name: MediAssist + - name: Vidal + - name: Volo + - name: FHPL + - name: HealthIndia + +servers: + - url: https://example.invalid + description: > + Default placeholder only. Each TPA uses its own base URL from environment variables; + many MediAssist endpoints use a full URL per env var (see operation descriptions). + +# ---------------------------------------------------------------------------- +# MediAssist — servers vary per env var (often full URL including path). +# Default hosts shown are illustrative; override via env. +# ---------------------------------------------------------------------------- +paths: + + /mediassist/claim-submit: + post: + tags: [MediAssist] + security: + - MediAssistUsername: [] + MediAssistPassword: [] + summary: Submit claim (claim push) + description: | + URL from `MEDI_ASSIST_API_BASE_URL_CLAIMSUBMIT` (full URL). + Headers `Username` / `Password` from `MEDI_ASSIST_API_USERNAME` / `MEDI_ASSIST_API_PASSWORD`. + operationId: mediassistSubmitClaim + servers: + - url: https://apiintegration.mediassist.in + description: Example host — replace with env URL + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/MediAssistSubmitClaimBody' + responses: + '200': + description: Wrapped by app helper; success includes `claimReferenceNo` in nested payload + + /mediassist/ecard: + post: + tags: [MediAssist] + security: + - MediAssistUsername: [] + MediAssistPassword: [] + summary: E-card URL request + description: URL from `MEDI_ASSIST_API_BASE_URL_ECARDREQUEST`. + operationId: mediassistEcardRequest + servers: + - url: https://apiintegration.mediassist.in + requestBody: + content: + application/json: + schema: + type: object + required: [employeeId, policyNo] + properties: + employeeId: { type: string } + policyNo: { type: string } + responses: + '200': + description: Response includes `ecardUrl` when successful + + /mediassist/benef-details: + post: + tags: [MediAssist] + security: + - MediAssistUsername: [] + MediAssistPassword: [] + summary: Get beneficiary / enrollment (TPA ID pull) + description: URL from `MEDI_ASSIST_API_BASE_URL_FETCHTPA`. Paginated with `startIndex` / `range`. + operationId: mediassistGetBenefDetails + requestBody: + content: + application/json: + schema: + type: object + required: [policyNo] + properties: + policyNo: { type: string } + startDate: { type: string, default: "" } + endDate: { type: string, default: "" } + isDeActivedata: { type: boolean, default: false } + startIndex: { type: integer } + range: { type: integer, example: 100 } + employeeId: { type: string, default: "" } + responses: + '200': + description: Expects `benefDetails` and `count` in data + + /mediassist/claim-status: + post: + tags: [MediAssist] + security: + - MediAssistUsername: [] + MediAssistPassword: [] + summary: Claim status / claims data + description: URL from `MEDI_ASSIST_API_BASE_URL_CLAIMSTATUS`. + operationId: mediassistClaimStatus + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/MediAssistClaimStatusBody' + responses: + '200': + description: Expects `claimsData` array in response data + + /mediassist/ir-submission: + post: + tags: [MediAssist] + security: + - MediAssistUsername: [] + MediAssistPassword: [] + summary: IR (information) submission with attachment URLs + description: URL from `MEDI_ASSIST_API_BASE_URL_IRSUBMISSION`. + operationId: mediassistIRSubmission + requestBody: + content: + application/json: + schema: + type: object + required: [ClaimID, Attachments] + properties: + ClaimID: { type: string } + Attachments: + type: array + items: + type: object + properties: + AttachmentName: { type: string } + AttachmentPath: { type: string, description: Public download URL } + responses: + '200': + description: App treats `status` from helper as success/failure + + /mediassist/network-hospital: + post: + tags: [MediAssist] + security: + - MediAssistUsername: [] + MediAssistPassword: [] + summary: Network hospital list + description: | + Built as `{MEDI_ASSIST_API_BASE_URL}/NetworkHospital` (env base + path). + operationId: mediassistNetworkHospital + requestBody: + content: + application/json: + example: + startIndex: 0 + endIndex: 10 + policyNumber: "97000063250400000031" + responses: + '200': + description: Third-party response passed through + + /mediassist/intimate-claim: + post: + tags: [MediAssist] + security: + - MediAssistUsername: [] + MediAssistPassword: [] + summary: Intimate claim (UAT sample in code) + description: | + **Note:** `IntimateClaim()` in code uses a hardcoded URL and test credentials; + production should use env-driven URL and secrets. + operationId: mediassistIntimateClaimDev + deprecated: true + requestBody: + content: + application/json: + schema: + type: object + properties: + Username: { type: string } + Password: { type: string } + policyNo: { type: string } + memberId: { type: number } + DateOfAdmisssion: { type: string, format: date } + HospitalName: { type: string } + AilmentDescription: { type: string } + ContactNo: { type: string } + + # -------------------------------------------------------------------------- + # Vidal Health TPA (Azure APIM) + # -------------------------------------------------------------------------- + /partner-integration/api/files/upload-url: + post: + tags: [Vidal] + security: + - VidalSubscriptionKey: [] + summary: Get signed URL for document upload + description: | + `VIDAL_API_BASE_URL` + `/files/upload-url`. + Header `Ocp-Apim-Subscription-Key` = `VIDAL_API_SUBSCRIPTION_KEY`. + Second step uploads file via PUT to returned `signedUrl` (Azure Blob). + operationId: vidalFileUploadUrl + servers: + - url: https://devapigw.vidalhealthtpa.com + requestBody: + content: + application/json: + schema: + type: object + properties: + scope: { type: string, example: document type } + fileName: { type: string } + responses: + '200': + description: Expects `data.signedUrl`, `data.fileId` + + /partner-integration/api/claims/submit: + post: + tags: [Vidal] + security: + - VidalSubscriptionKey: [] + summary: Submit claim + operationId: vidalClaimsSubmit + servers: + - url: https://devapigw.vidalhealthtpa.com + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/VidalClaimSubmitBody' + responses: + '200': + description: Success path checks `data.status == SUCCESS` and nested claim numbers + + /partner-integration/api/claims/claim-dependent-info: + post: + tags: [Vidal] + security: + - VidalSubscriptionKey: [] + summary: Claim dependent info / status poll + operationId: vidalClaimDependentInfo + servers: + - url: https://devapigw.vidalhealthtpa.com + requestBody: + content: + application/json: + schema: + type: object + properties: + empNO: { type: string, default: "" } + tpaCardID: { type: string, default: "" } + claimID: { type: string, description: Set when known } + emailID: { type: string, default: "" } + mobileNO: { type: string, default: "" } + responses: + '200': + description: Expects `data.data.claims[]` + + /partner-integration/enrollment/info: + post: + tags: [Vidal] + security: + - VidalSubscriptionKey: [] + summary: Enrollment / beneficiary pull (Vidal V2) + description: | + URL from `vidalEnrollmentInfoApiUrl()`: strips trailing `/api` from `VIDAL_API_BASE_URL` + and appends `/enrollment/info`, or falls back to dev URL. + Paginates with `startIndex` / `endIndex`. + operationId: vidalEnrollmentInfo + servers: + - url: https://devapigw.vidalhealthtpa.com + requestBody: + content: + application/json: + schema: + type: object + properties: + policyNo: { type: string } + startIndex: { type: integer, example: 1 } + endIndex: { type: integer, example: 100 } + + /vidal/wellness-sso: + post: + tags: [Vidal] + security: + - VidalWellnessSubscriptionKey: [] + summary: Wellness SSO (encrypted payload) + description: | + `VIDAL_WELLNESS_BASE_URL`. Headers include `Ocp-Apim-Subscription-Key`, + `apiver`, `mode: encrypt`. Body uses AES-encrypted payload + `source` / `subPartnerId`. + operationId: vidalWellnessSSO + requestBody: + content: + application/json: + schema: + type: object + properties: + payload: { type: string, description: iv:ciphertext base64 format } + source: { type: string } + subPartnerId: { type: string } + + /vidal/ir-submission: + post: + tags: [Vidal] + security: + - VidalIrSubscriptionKey: [] + summary: IR submission (shortfall documents) + description: | + Full URL from `VIDAL_API_BASE_URL_IRSUBMISSION`. + Uses `VIDAL_SUBSCRIPTION_KEY` (note distinct from `VIDAL_API_SUBSCRIPTION_KEY` in code). + Body sent as JSON string in some call paths. + operationId: vidalIRSubmission + requestBody: + content: + application/json: + schema: + type: object + properties: + shortFallNo: { type: string } + fileId: { type: string, description: Single file } + fileIdList: + type: array + items: { type: string } + description: Multiple files + + # -------------------------------------------------------------------------- + # Volo / TrueCover (admin + consumer bases) + # -------------------------------------------------------------------------- + /external/login: + post: + tags: [Volo] + security: [] + summary: Login — obtain access token + description: | + `VOLO_API_ADMIN_BASE_URL` + `/external/login`. + Body uses `VOLO_API_EMAIL`, `VOLO_API_PASSWORD`, `VOLO_LOGGED_IN_PORTAL`. + operationId: voloLogin + requestBody: + content: + application/json: + schema: + type: object + properties: + emailId: { type: string } + password: { type: string } + loggedInPortal: + type: string + example: POLICY_CONFIGURATION_PORTAL + responses: + '200': + description: Expects `accessToken` for Authorization header on subsequent calls + + /trueclaim/tpa/get-Enrollment-dump: + get: + tags: [Volo] + security: + - VoloBearerToken: [] + summary: Enrollment dump by endorsement + operationId: voloGetEnrollmentDumpByEndorsement + parameters: + - in: query + name: insurerPolicyNumber + required: true + schema: { type: string } + - in: query + name: endorsmentNo + required: true + schema: { type: string } + + /trueclaim/tpa/getHospitalByInsurerName: + get: + tags: [Volo] + security: + - VoloBearerToken: [] + summary: Hospitals by insurer name + operationId: voloGetHospitalByInsurerName + parameters: + - in: query + name: insurerName + required: true + schema: { type: string } + + /trueclaim/tpa/doc: + post: + tags: [Volo] + security: + - VoloBearerToken: [] + summary: Upload document (body defined by TPA) + operationId: voloUploadDocument + requestBody: + content: + application/json: + schema: + type: object + additionalProperties: true + + /trueclaim/tpa/get-Enroll-dump: + get: + tags: [Volo] + security: + - VoloBearerToken: [] + summary: Enrollment dump by insurer policy number + operationId: voloGetEnrollmentDump + parameters: + - in: query + name: insurerPolicyNumber + required: true + schema: { type: string } + + /truecover/external-service/claim-status: + post: + tags: [Volo] + security: + - VoloBearerToken: [] + summary: External claim status (consumer base) + description: | + `VOLO_API_CONSUMER_BASE_URL` + `/truecover/external-service/claim-status`. + operationId: voloFetchExternalClaimStatus + servers: + - url: https://consumer.example.ewatpa.com + description: Replace with VOLO_API_CONSUMER_BASE_URL + requestBody: + content: + application/json: + schema: + type: object + required: [tpaClaimNo] + properties: + tpaClaimNo: { type: string } + + /trueclaim/tpa/getTpaData: + post: + tags: [Volo] + security: + - VoloBearerToken: [] + summary: TPA data by date range and entity + operationId: voloGetTpaData + requestBody: + content: + application/json: + schema: + type: object + required: [startDate, endDate, entityId] + properties: + startDate: { type: string } + endDate: { type: string } + entityId: { type: string } + + /trueclaim/create-new-all-member-id-card-pdf: + get: + tags: [Volo] + security: + - VoloBearerToken: [] + summary: E-card PDF (base64 in response body) + operationId: voloGetEcardPdf + parameters: + - in: query + name: employeeId + required: true + schema: { type: string } + - in: query + name: entityId + required: true + schema: { type: string } + + /trueclaim/get-entity-from-policy: + get: + tags: [Volo] + security: + - VoloBearerToken: [] + summary: Resolve entity id from policy number + operationId: voloGetEntityFromPolicy + parameters: + - in: query + name: policyNumber + required: true + schema: { type: string } + + /trueclaim/policy-bazaar/intimate-claim: + post: + tags: [Volo] + security: + - VoloBearerToken: [] + summary: Intimate claim (claim push) + description: | + Built payload includes `hospitalId` (`VOLO_DEFAULT_HOSPITAL_ID`), `memberId`, + dates, `potentialClaimAmount`, `claimDocuments` (pdf URL), `entityId`, `insurerPolicyNumber`. + operationId: voloIntimateClaim + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/VoloIntimateClaimBody' + + /trueclaim/tpa/getClaimData: + post: + tags: [Volo] + security: + - VoloBearerToken: [] + summary: Get claim data + operationId: voloGetClaimData + parameters: + - in: query + name: claimId + required: true + schema: { type: string } + requestBody: + content: + application/json: + schema: + type: object + description: Empty JSON object sent as body + + # -------------------------------------------------------------------------- + # FHPL + # -------------------------------------------------------------------------- + /oauth/token: + get: + tags: [FHPL] + security: [] + summary: Generate OAuth token + description: | + URL from `FHPL_TOKEN_URL`. **Note:** implementation uses GET with `application/x-www-form-urlencoded` + body (`UserName`, `Password`, `grant_type`) — align with FHPL spec / Postman. + operationId: fhplGenerateToken + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + UserName: { type: string } + Password: { type: string } + grant_type: { type: string } + + /api/ClaimSubmission: + post: + tags: [FHPL] + summary: Claim submission + description: | + `{FHPL_BASE_URL}/api/ClaimSubmission` with Bearer token from token endpoint. + operationId: fhplClaimSubmission + security: + - FhplBearerAuth: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/FhplClaimSubmissionBody' + + /api/GetTPA_ClaimsDetails: + post: + tags: [FHPL] + summary: TPA claim details / MIS (status sync) + operationId: fhplGetTpaClaimsDetails + security: + - FhplBearerAuth: [] + requestBody: + content: + application/json: + schema: + type: object + properties: + UserName: { type: string } + Password: { type: string } + PolicyNumber: { type: string } + Fromdate: { type: string, format: date } + Todate: { type: string, format: date } + + /api/GetEcard: + post: + tags: [FHPL] + summary: Get e-card URL + operationId: fhplGetEcard + security: + - FhplBearerAuth: [] + requestBody: + content: + application/json: + schema: + type: object + properties: + UserName: { type: string } + Password: { type: string } + PolicyNumber: { type: string } + EmployeeID: { type: string } + + /api/GetEnrollmentDetailsPolicy: + post: + tags: [FHPL] + summary: Enrollment details for policy (paginated) + operationId: fhplGetEnrollmentDetailsPolicy + security: + - FhplBearerAuth: [] + requestBody: + content: + application/json: + schema: + type: object + properties: + UserName: { type: string } + Password: { type: string } + PolicyNumber: { type: string } + StartIndex: { type: integer } + Range: { type: integer, example: 100 } + + # -------------------------------------------------------------------------- + # Health India TPA + # -------------------------------------------------------------------------- + /JWT/GenerateJWTAuth: + post: + tags: [HealthIndia] + summary: Generate JWT (Basic auth) + description: | + `HEALTH_INDIA_TOKEN_URL`. HTTP Basic with `HEALTH_INDIA_USERNAME` / `HEALTH_INDIA_PASSWORD`. + Body is `{}`. + operationId: healthIndiaGenerateJWT + security: + - HealthIndiaTokenBasic: [] + requestBody: + content: + application/json: + schema: + type: object + + /Intimation/GetClaimIntimation: + post: + tags: [HealthIndia] + summary: Claim intimation / reimbursement submission + description: > + HEALTH_INDIA_BASE_URL + /Intimation/GetClaimIntimation + operationId: healthIndiaClaimIntimation + security: + - HealthIndiaBearerAuth: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/HealthIndiaClaimIntimationBody' + + /Claims/GetClaims: + post: + tags: [HealthIndia] + summary: Get claim(s) by CCN + operationId: healthIndiaGetClaims + security: + - HealthIndiaBearerAuth: [] + requestBody: + content: + application/json: + schema: + type: object + properties: + policY_NUMBER: { type: string } + CCN: { type: string } + CCN_EXT: { type: string } + + /Member/GetMemberEcard: + post: + tags: [HealthIndia] + summary: Member e-card + operationId: healthIndiaGetMemberEcard + security: + - HealthIndiaBearerAuth: [] + requestBody: + content: + application/json: + schema: + type: object + properties: + policY_NUMBER: { type: string } + employeE_CODE: { type: string } + membeR_ID: { type: string } + + /Enrollment/GetEnrollmentData: + post: + tags: [HealthIndia] + summary: Bulk enrollment data (TPA ID pull) + operationId: healthIndiaGetEnrollmentData + security: + - HealthIndiaBearerAuth: [] + requestBody: + content: + application/json: + schema: + type: object + properties: + policY_NUMBER: { type: string } + + /ClaimsMIS/GetClaimsMIS: + post: + tags: [HealthIndia] + summary: Claims MIS / sync + operationId: healthIndiaGetClaimsMIS + security: + - HealthIndiaBearerAuth: [] + requestBody: + content: + application/json: + schema: + type: object + properties: + policY_NUMBER: { type: string } + +components: + securitySchemes: + MediAssistUsername: + type: apiKey + in: header + name: Username + description: | + `.env`: `MEDI_ASSIST_API_USERNAME` — same value as `getenv('MEDI_ASSIST_API_USERNAME')` in PHP. + + MediAssistPassword: + type: apiKey + in: header + name: Password + description: | + `.env`: `MEDI_ASSIST_API_PASSWORD` — same value as `getenv('MEDI_ASSIST_API_PASSWORD')`. + + VidalSubscriptionKey: + type: apiKey + in: header + name: Ocp-Apim-Subscription-Key + description: | + `.env`: `VIDAL_API_SUBSCRIPTION_KEY` — used for partner-integration APIs (`VIDAL_API_BASE_URL`). + + VidalIrSubscriptionKey: + type: apiKey + in: header + name: Ocp-Apim-Subscription-Key + description: | + `.env`: `VIDAL_SUBSCRIPTION_KEY` — used **only** for IR submission (`VIDAL_API_BASE_URL_IRSUBMISSION`). + Same header name as main Vidal key but different secret. + + VidalWellnessSubscriptionKey: + type: apiKey + in: header + name: Ocp-Apim-Subscription-Key + description: | + `.env`: `VIDAL_WELLNESS_SUBSCRIPTION_KEY`. Wellness also uses `VIDAL_WELLNESS_BASE_URL`, + `VIDAL_WELLNESS_BASE64_KEY`, `VIDAL_WELLNESS_URL_IDENTIFIER`, `VIDAL_WELLNESS_SUB_PARTNER_ID`. + + VoloBearerToken: + type: http + scheme: bearer + bearerFormat: JWT + description: | + Obtain token from `POST .../external/login` using body fields from `.env`: + `VOLO_API_EMAIL`, `VOLO_API_PASSWORD`, `VOLO_LOGGED_IN_PORTAL`. + Send the returned `accessToken` as the Bearer value (as required by the API, sometimes including a `Token ` prefix). + + FhplBearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: | + Bearer `access_token` returned from `FHPL_TOKEN_URL` using form fields + `UserName`=`FHPL_USER_NAME`, `Password`=`FHPL_PASSWORD`, `grant_type`=`FHPL_GRANT_TYPE`. + + HealthIndiaTokenBasic: + type: http + scheme: basic + description: | + For JWT generation call only. `.env`: `HEALTH_INDIA_USERNAME`, `HEALTH_INDIA_PASSWORD` + (HTTP Basic to `HEALTH_INDIA_TOKEN_URL`). + + HealthIndiaBearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: | + Bearer JWT from the GenerateJWTAuth response (not a fixed `.env` value). + Use `result[0].access_token` after calling token URL with Basic credentials above. + + schemas: + MediAssistSubmitClaimBody: + type: object + properties: + policyNo: { type: string } + memberId: { type: string } + mobileNo: { type: string } + emailId: { type: string } + claimDateOfAdmission: { type: string } + claimDateOfDischarge: { type: string } + hospName: { type: string } + hospAddress: { type: string } + reasonForHospitalization: { type: string } + disease: { type: string } + claimAmount: { type: number } + claimType: + type: string + example: HOSPITALIZATION + claimSubmissionAttachments: + type: object + properties: + fileName: { type: string } + filePath: { type: string, description: Public URL to PDF } + + MediAssistClaimStatusBody: + type: object + properties: + policyNo: { type: string } + startDate: { type: string } + endDate: { type: string } + employeeCode: { type: string } + memberID: { type: string } + claimNo: { type: string } + claimRefNo: { type: string } + + VidalClaimSubmitBody: + type: object + properties: + policyNo: { type: string } + dependentUniqueId: { type: string } + typeOfClaim: { type: string, example: Main hospitalization claim } + claimSubType: { type: string } + requestedAmount: { type: string } + ailmentType: { type: string, example: Non covid } + admissionDate: { type: string, description: dd-mm-yyyy } + dischargeDate: { type: string } + hospitalName: { type: string } + empanelmentNo: { type: number } + ailmentName: { type: string } + hospitalAddress: { type: string, nullable: true } + hospitalState: { type: string, nullable: true } + hospitalCity: { type: string, nullable: true } + hospitalPinCode: { type: string, nullable: true } + hospitalPhoneNo: { type: string, nullable: true } + fileId: { type: string } + bankDetails: + type: object + properties: + accountHolderName: { type: string, nullable: true } + accountType: { type: string, nullable: true } + accountNo: { type: string, nullable: true } + ifscCode: { type: string, nullable: true } + + VoloIntimateClaimBody: + type: object + properties: + hospitalId: { type: string } + memberId: { type: string } + dateOfAdmission: { type: string, format: date } + dateOfDischarge: { type: string, format: date } + potentialClaimAmount: { type: number } + claimDocuments: + type: array + items: + type: object + properties: + documentName: { type: string } + documentType: { type: string, example: medical_report } + extension: { type: string, example: pdf } + url: { type: string, format: uri } + entityId: { type: string } + insurerPolicyNumber: { type: string } + + FhplClaimSubmissionBody: + type: object + properties: + IssueID: { type: string } + Userid: + type: string + description: Same as `.env` `FHPL_USER_NAME` where the app sends Userid. + PolicyNo: { type: string } + UhidNo: { type: string } + ClaimID: { type: string } + DOA: { type: string, format: date } + DateofDischarge: { type: string, format: date, nullable: true } + ClaimedAmount: { type: number } + DocumentType: { type: integer, example: 20, description: Fresh claim } + PayeeName: { type: string } + HospitalName: { type: string } + MobileNo: { type: string } + Documents: + type: array + items: + type: object + properties: + documentName: { type: string } + documentCategory: { type: string, example: IRR } + filecontent: { type: string, description: Base64 PDF } + + HealthIndiaClaimIntimationBody: + type: object + description: Field casing matches Health India API (mixedCase keys). + properties: + policY_NUMBER: { type: string } + employeE_CODE: { type: string } + membeR_ID: { type: string } + claiM_TYPE: { type: string, example: Reimbursement } + benefiT_TYPE: { type: string, enum: [IPD, OPD] } + claimeD_AMOUNT: { type: string } + datE_OF_ADMISSION: { type: string, format: date } + ailmenT_DESCRIPTION: { type: string } + hospitaL_CODE: { type: string } + hospitaL_NAME: { type: string } + hospitaL_ADDRESS: { type: string } + hospitaL_NUMBER: { type: string } + pdF_BYTES: + type: array + items: { type: string, description: Base64-encoded PDF } From 98b2583ccdda488029012de48928ba895ef1e52d Mon Sep 17 00:00:00 2001 From: Gowtham M Date: Fri, 8 May 2026 14:29:42 +0530 Subject: [PATCH 2/6] GWM : Merge multiple file for claims --- app/Controllers/EmployeeRestController.php | 10 ++ app/Controllers/TicketController.php | 10 ++ app/Helpers/merge_pdf_helper.php | 194 +++++++++++++++++++++ 3 files changed, 214 insertions(+) create mode 100644 app/Helpers/merge_pdf_helper.php diff --git a/app/Controllers/EmployeeRestController.php b/app/Controllers/EmployeeRestController.php index fd4a3419..193762d9 100755 --- a/app/Controllers/EmployeeRestController.php +++ b/app/Controllers/EmployeeRestController.php @@ -3923,6 +3923,16 @@ class EmployeeRestController extends AdminController } } + // Merge all uploaded PDFs for this ticket into a single combined PDF + if ($pdf_exist_in_the_file) { + try { + helper('merge_pdf'); + merge_ticket_pdfs((int) $ticket_id); + } catch (\Throwable $e) { + log_message('error', 'EmployeeRestController::handleCliamFiles | merge_ticket_pdfs failed | ticket_id=' . $ticket_id . ' | ' . $e->getMessage()); + } + } + $count = $this->ticketMaster->where('id', $ticket_id)->where('is_active', 1)->where('tpa_claim_push_reference_no IS NULL')->countAllResults(); log_message('error', 'Ticket validation | Ticket ID: ' . $ticket_id . ' | Matching active tickets with NULL TPA reference: ' . $count); if ($count > 0 && $pdf_exist_in_the_file && $tpa_claim_push == true) { diff --git a/app/Controllers/TicketController.php b/app/Controllers/TicketController.php index 5691c819..5fee206e 100644 --- a/app/Controllers/TicketController.php +++ b/app/Controllers/TicketController.php @@ -1621,6 +1621,16 @@ class TicketController extends BaseController $this->ticketMasterModel->where('id', $return_value)->set($updateUploadedLetterData)->update(); } + // Merge all uploaded PDFs for this ticket into a single combined PDF + try { + helper('merge_pdf'); + merge_ticket_pdfs((int) $return_value, [ + 'ticket_type' => (int) ($ticket_data['ticket_type_id'] ?? 1), + ]); + } catch (\Throwable $e) { + log_message('error', 'TicketController::createTicket | merge_ticket_pdfs failed | ticket_id=' . $return_value . ' | ' . $e->getMessage()); + } + //mail trigger part $this->putHistoryAfterInsert($ticket_data, $return_value); $mail_responce = $this->sendAutoMailTrigger($return_value); diff --git a/app/Helpers/merge_pdf_helper.php b/app/Helpers/merge_pdf_helper.php new file mode 100644 index 00000000..9316017f --- /dev/null +++ b/app/Helpers/merge_pdf_helper.php @@ -0,0 +1,194 @@ + true, + 'created_by' => null, + 'ticket_type' => 1, + 'include_file_types' => [2, 3], + ]; + + $result = [ + 'status' => false, + 'merged_file_id' => null, + 'file_name' => null, + 'pages' => 0, + 'source_count' => 0, + 'message' => '', + ]; + + if ($ticket_master_id <= 0) { + $result['message'] = 'Invalid ticket_master_id'; + return $result; + } + + $claimFiles = new ClaimFilesModel(); + + $rows = $claimFiles + ->where('ticket_id', $ticket_master_id) + ->where('is_active', 1) + ->where('mime_type', 'application/pdf') + ->whereIn('file_type', $opts['include_file_types']) + ->orderBy('id', 'ASC') + ->findAll(); + + if (empty($rows)) { + $result['status'] = true; + $result['message'] = 'No PDF files to merge'; + return $result; + } + + $uploadDir = rtrim(WRITEPATH, '/\\') . DIRECTORY_SEPARATOR + . 'uploads' . DIRECTORY_SEPARATOR + . 'claim_files' . DIRECTORY_SEPARATOR; + + $sourcePaths = []; + foreach ($rows as $row) { + $name = ! empty($row['url']) ? $row['url'] : ($row['file_name'] ?? ''); + if (empty($name)) { + continue; + } + // url column may sometimes hold a full URL; we only care about the file basename on disk. + $full = $uploadDir . basename($name); + if (is_file($full) && is_readable($full)) { + $sourcePaths[] = $full; + } else { + log_message('error', "merge_ticket_pdfs | missing PDF on disk | claim_file_id={$row['id']} | path={$full}"); + } + } + + if (empty($sourcePaths)) { + $result['message'] = 'No readable PDF files on disk'; + return $result; + } + + $result['source_count'] = count($sourcePaths); + + $tempDir = rtrim(WRITEPATH, '/\\') . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR . 'mpdf'; + if (! is_dir($tempDir)) { + @mkdir($tempDir, 0775, true); + } + + $mergedName = 'merged_' . $ticket_master_id . '_' . time() . '_' . bin2hex(random_bytes(5)) . '.pdf'; + $mergedPath = $uploadDir . $mergedName; + + try { + $mpdf = new Mpdf([ + 'tempDir' => $tempDir, + 'mode' => 'utf-8', + ]); + + $totalPages = 0; + foreach ($sourcePaths as $src) { + try { + $pageCount = $mpdf->setSourceFile($src); + for ($p = 1; $p <= $pageCount; $p++) { + $tplId = $mpdf->importPage($p); + $size = $mpdf->getTemplateSize($tplId); + $mpdf->AddPageByArray([ + 'orientation' => ($size['width'] > $size['height']) ? 'L' : 'P', + 'sheet-size' => [$size['width'], $size['height']], + ]); + $mpdf->useTemplate($tplId); + $totalPages++; + } + } catch (\Throwable $e) { + log_message('error', "merge_ticket_pdfs | failed to import {$src} | " . $e->getMessage()); + } + } + + if ($totalPages === 0) { + $result['message'] = 'All source PDFs failed to import'; + return $result; + } + + $mpdf->Output($mergedPath, Destination::FILE); + } catch (\Throwable $e) { + log_message('error', 'merge_ticket_pdfs | mpdf failure | ticket_id=' . $ticket_master_id . ' | ' . $e->getMessage()); + $result['message'] = 'Merge failed: ' . $e->getMessage(); + return $result; + } + + if (! is_file($mergedPath)) { + $result['message'] = 'Merged file was not created'; + return $result; + } + + if ($opts['replace']) { + $claimFiles + ->where('ticket_id', $ticket_master_id) + ->where('file_type', MERGED_CLAIM_FILE_TYPE) + ->where('is_active', 1) + ->set(['is_active' => 0]) + ->update(); + } + + $insertData = [ + 'ticket_id' => $ticket_master_id, + 'ticket_type' => $opts['ticket_type'], + 'file_type' => MERGED_CLAIM_FILE_TYPE, + 'doc_name' => 'MERGED_CLAIM_DOCS_PDF', + 'file_name' => $mergedName, + 'url' => $mergedName, + 'mime_type' => 'application/pdf', + 'is_active' => 1, + ]; + if (! empty($opts['created_by'])) { + $insertData['created_by'] = $opts['created_by']; + } + + $insertedId = $claimFiles->insert($insertData); + + if (! $insertedId) { + log_message('error', 'merge_ticket_pdfs | DB insert failed for merged file | ticket_id=' . $ticket_master_id); + @unlink($mergedPath); + $result['message'] = 'Failed to register merged file in claim_files'; + return $result; + } + + log_message( + 'info', + "merge_ticket_pdfs | OK | ticket_id={$ticket_master_id} | sources={$result['source_count']} | pages={$totalPages} | claim_file_id={$insertedId}" + ); + + $result['status'] = true; + $result['merged_file_id'] = (int) $insertedId; + $result['file_name'] = $mergedName; + $result['pages'] = $totalPages; + $result['message'] = 'Merged successfully'; + return $result; + } +} From a7dee02f9165120b8225f0849283440ef2ed269f Mon Sep 17 00:00:00 2001 From: Gowtham M Date: Fri, 8 May 2026 14:45:16 +0530 Subject: [PATCH 3/6] GWM : Merge multiple file for claims --- app/Controllers/TicketController.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/Controllers/TicketController.php b/app/Controllers/TicketController.php index 5fee206e..bd293ae4 100644 --- a/app/Controllers/TicketController.php +++ b/app/Controllers/TicketController.php @@ -1621,16 +1621,6 @@ class TicketController extends BaseController $this->ticketMasterModel->where('id', $return_value)->set($updateUploadedLetterData)->update(); } - // Merge all uploaded PDFs for this ticket into a single combined PDF - try { - helper('merge_pdf'); - merge_ticket_pdfs((int) $return_value, [ - 'ticket_type' => (int) ($ticket_data['ticket_type_id'] ?? 1), - ]); - } catch (\Throwable $e) { - log_message('error', 'TicketController::createTicket | merge_ticket_pdfs failed | ticket_id=' . $return_value . ' | ' . $e->getMessage()); - } - //mail trigger part $this->putHistoryAfterInsert($ticket_data, $return_value); $mail_responce = $this->sendAutoMailTrigger($return_value); @@ -2076,6 +2066,16 @@ class TicketController extends BaseController $return_value = $this->ticketMasterModel->where('id', $ticket_id)->set($ticket_data)->update(); if ($return_value) { + // Merge all uploaded PDFs for this ticket into a single combined PDF + try { + helper('merge_pdf'); + merge_ticket_pdfs((int) $ticket_id, [ + 'ticket_type' => (int) ($ticket_data['ticket_type_id'] ?? 1), + ]); + } catch (\Throwable $e) { + log_message('error', 'TicketController::updateTicket | merge_ticket_pdfs failed | ticket_id=' . $ticket_id . ' | ' . $e->getMessage()); + } + $mail_responce = null; if($old_ticket_data['claim_status_id'] != $ticket_data['claim_status_id']){ //mail trigger part From caf5e794291290a86257b5c6b8e46d54e649744d Mon Sep 17 00:00:00 2001 From: velz Date: Fri, 8 May 2026 15:25:04 +0530 Subject: [PATCH 4/6] FIX_IMPROVED_FILE_GUARD --- app/Filters/GlobalPostFileUploadGuard.php | 275 +++++++++++++++--- app/Filters/GlobalPostFileUploadGuard_OLD.php | 150 ++++++++++ 2 files changed, 380 insertions(+), 45 deletions(-) create mode 100644 app/Filters/GlobalPostFileUploadGuard_OLD.php diff --git a/app/Filters/GlobalPostFileUploadGuard.php b/app/Filters/GlobalPostFileUploadGuard.php index 5b303056..a3303e2f 100644 --- a/app/Filters/GlobalPostFileUploadGuard.php +++ b/app/Filters/GlobalPostFileUploadGuard.php @@ -10,43 +10,62 @@ use finfo; class GlobalPostFileUploadGuard implements FilterInterface { - /** - * Max file size (in bytes) → 25MB - */ protected int $maxFileSize = 25 * 1024 * 1024; - /** - * Allowed MIME types mapped to extensions - */ protected array $allowedMimeMap = [ 'image/jpeg' => ['jpg', 'jpeg'], 'image/png' => ['png'], 'image/gif' => ['gif'], 'image/webp' => ['webp'], - 'image/svg+xml' => ['svg'], + // SVG removed — dangerous without server-side sanitization 'application/pdf' => ['pdf'], 'application/msword' => ['doc'], 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => ['docx'], 'application/vnd.oasis.opendocument.text' => ['odt'], - 'text/rtf' => ['rtf'], + 'text/rtf' => ['rtf'], 'application/rtf' => ['rtf'], - 'application/vnd.ms-excel' => ['xls', 'xlsx'], + 'application/vnd.ms-excel' => ['xls'], // removed xlsx from here — it has its own MIME 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => ['xlsx'], 'application/vnd.oasis.opendocument.spreadsheet' => ['ods'], - 'text/csv' => ['csv'], + 'text/csv' => ['csv'], 'application/csv' => ['csv'], - 'text/plain' => ['txt', 'csv'], + 'text/plain' => ['txt'], // separated csv out — txt only + ]; + + protected array $magicBytes = [ + 'image/jpeg' => ["\xFF\xD8\xFF"], + 'image/png' => ["\x89PNG\r\n\x1a\n"], + 'image/gif' => ["GIF87a", "GIF89a"], + 'image/webp' => ["RIFF"], + 'application/pdf' => ["%PDF-"], + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => ["PK\x03\x04"], + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => ["PK\x03\x04"], + 'application/vnd.oasis.opendocument.text' => ["PK\x03\x04"], + 'application/vnd.oasis.opendocument.spreadsheet' => ["PK\x03\x04"], + 'application/msword' => ["\xD0\xCF\x11\xE0"], + 'application/vnd.ms-excel' => ["\xD0\xCF\x11\xE0"], ]; protected array $blockedExtensions = [ - 'php', 'phtml', 'html', 'pht', 'phar', 'php3', 'php4', 'php5', 'php7', 'php8', 'phps', - 'cgi', 'fcgi', 'pl', 'py', 'rb', 'lua', 'tcl', 'go', 'rs', 'jar', 'class', - 'exe', 'dll', 'com', 'bat', 'cmd', 'msi', 'vbs', 'ps1', 'scr', - 'sh', 'bash', 'zsh', 'apk', 'app', 'deb', 'rpm', 'bin', 'run', - 'js', 'mjs', 'jsp', 'asp', 'aspx', 'cer', 'swf', - 'env', 'ini', 'user.ini', 'htaccess', 'htpasswd', 'conf', 'config', 'log', 'sql', - 'zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz', 'iso', - 'lnk', 'url', 'reg', 'sys', 'drv', 'vxd', 'tmp', 'bak', 'old', 'backup', 'key', 'pem' + 'php', 'phtml', 'html', 'htm', 'pht', 'phar', + 'php3', 'php4', 'php5', 'php7', 'php8', 'phps', + 'cgi', 'fcgi', 'pl', 'py', 'rb', 'lua', 'tcl', + 'go', 'rs', 'jar', 'class', + 'exe', 'dll', 'com', 'bat', 'cmd', 'msi', + 'vbs', 'ps1', 'ps2', 'scr', 'hta', + 'sh', 'bash', 'zsh', 'fish', + 'apk', 'app', 'deb', 'rpm', + 'bin', 'run', 'elf', + 'js', 'mjs', 'ts', 'jsx', 'tsx', + 'jsp', 'jspx', 'asp', 'aspx', 'ascx', 'ashx', 'axd', + 'cer', 'swf', 'xhtml', + 'env', 'ini', 'user.ini', 'htaccess', 'htpasswd', + 'conf', 'config', 'log', 'sql', 'db', 'sqlite', + 'zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz', 'iso', 'cab', + 'lnk', 'url', 'reg', 'sys', 'drv', 'vxd', + 'tmp', 'bak', 'old', 'backup', + 'key', 'pem', 'p12', 'pfx', 'crt', 'csr', + 'xml', 'xsl', 'xslt', 'svg', ]; public function before(RequestInterface $request, $arguments = null) @@ -68,8 +87,8 @@ class GlobalPostFileUploadGuard implements FilterInterface private function validateFileInput($fileData, string $inputName): void { if (is_array($fileData)) { - foreach ($fileData as $file) { - $this->validateSingleFile($file, $inputName); + foreach ($fileData as $key => $item) { + $this->validateFileInput($item, $inputName . '[' . $key . ']'); } } else { $this->validateSingleFile($fileData, $inputName); @@ -78,7 +97,7 @@ class GlobalPostFileUploadGuard implements FilterInterface private function validateSingleFile($file, string $inputName): void { - $request = Services::request(); + $request = Services::request(); $clientIp = $request->getIPAddress(); $uri = $request->getUri()->getPath(); @@ -86,45 +105,211 @@ class GlobalPostFileUploadGuard implements FilterInterface if ($file->getError() === UPLOAD_ERR_INI_SIZE || $file->getError() === UPLOAD_ERR_FORM_SIZE) { $this->block("File exceeds server-side size limit", $clientIp, $uri, $inputName, $file->getClientName(), 'unknown', 'unknown', 0); } - return; + return; } - $originalName = $file->getClientName(); - $extension = strtolower($file->getExtension()); - $mime = $file->getMimeType(); - $size = $file->getSize(); + $originalName = $file->getClientName(); + $clientExtension = strtolower($file->getClientExtension()); + $tempPath = $file->getTempName(); + $size = $file->getSize(); - // --- 1. Fixed Null Byte & Path Traversal Check --- - if (preg_match('/\0|[\/\\\]/', $originalName)) { - $this->block("Malicious filename characters", $clientIp, $uri, $inputName, $originalName, $mime, $extension, $size); + // echo $file->getExtension(); + // echo '@@@@@@@@@@@@@@@@@@@@@@@'; + // echo $file->getMimeType(); + // --- 1. Null Byte & Path Traversal Check --- + if (preg_match('/\0|[\/\\\\]/', $originalName)) { + $this->block("Malicious filename characters", $clientIp, $uri, $inputName, $originalName, 'unknown', $clientExtension, $size); } - // --- 2. Double Extension Attack Check --- - if (preg_match('/\.(php|html|phtml|phar|exe|sh|bat|cmd|js|jsp|asp|aspx|py|pl)\./i', $originalName)) { - $this->block("Double extension attack", $clientIp, $uri, $inputName, $originalName, $mime, $extension, $size); + // --- 2. Filename length check --- + if (strlen($originalName) > 255) { + $this->block("Filename too long", $clientIp, $uri, $inputName, substr($originalName, 0, 100) . '...', 'unknown', $clientExtension, $size); } - // --- 3. Forbidden Extension --- - if (in_array($extension, $this->blockedExtensions, true)) { - $this->block("Forbidden extension", $clientIp, $uri, $inputName, $originalName, $mime, $extension, $size); + // --- 3. Double/Multiple Extension Attack --- + $nameParts = explode('.', $originalName); + if (count($nameParts) > 2) { + $allExtensions = array_slice($nameParts, 1); + foreach ($allExtensions as $part) { + if (in_array(strtolower($part), $this->blockedExtensions, true)) { + $this->block("Double/multiple extension attack detected", $clientIp, $uri, $inputName, $originalName, 'unknown', $clientExtension, $size); + } + } } - // --- 4. File Size Limit --- + // --- 4. Forbidden Extension --- + if (in_array($clientExtension, $this->blockedExtensions, true)) { + $this->block("Forbidden extension", $clientIp, $uri, $inputName, $originalName, 'unknown', $clientExtension, $size); + } + + // --- 5. File Size Limit --- if ($size > $this->maxFileSize) { - $this->block("File too large", $clientIp, $uri, $inputName, $originalName, $mime, $extension, $size); + $this->block("File too large", $clientIp, $uri, $inputName, $originalName, 'unknown', $clientExtension, $size); } - // --- 5. MIME Allow-list Check --- - if (!array_key_exists($mime, $this->allowedMimeMap)) { - $this->block("MIME type not allowed ($mime)", $clientIp, $uri, $inputName, $originalName, $mime, $extension, $size); + // --- 6. Real MIME Detection via finfo on actual temp file --- + $finfo = new finfo(FILEINFO_MIME_TYPE); + $detectedMime = $finfo->file($tempPath); + + // --- 7. Resolve Expected MIME From Extension --- + $expectedMime = null; + + foreach ($this->allowedMimeMap as $mime => $extensions) { + + if (in_array($clientExtension, $extensions, true)) { + $expectedMime = $mime; + break; + } } - // --- 6. MIME-Extension Consistency --- - if (!in_array($extension, $this->allowedMimeMap[$mime], true)) { - $this->block("MIME-extension mismatch", $clientIp, $uri, $inputName, $originalName, $mime, $extension, $size); + if ($expectedMime === null) { + $this->block( + "Extension not allowed", + $clientIp, + $uri, + $inputName, + $originalName, + $detectedMime, + $clientExtension, + $size + ); + } + + // --- 8. Magic Bytes Deep Validation --- + // validate using EXPECTED MIME instead of detected MIME + $this->validateMagicBytes( + $tempPath, + $expectedMime, + $clientIp, + $uri, + $inputName, + $originalName, + $clientExtension, + $size + ); + + // // --- 9. Embedded Code Scan --- + // $this->scanForEmbeddedCode( + // $tempPath, + // $expectedMime, + // $clientIp, + // $uri, + // $inputName, + // $originalName, + // $clientExtension, + // $size + // ); + + // --- 10. Final MIME Validation --- + // strict match only: generic octet-stream is not accepted + if ($detectedMime !== $expectedMime) { + $this->block( + "MIME-extension mismatch", + $clientIp, + $uri, + $inputName, + $originalName, + $detectedMime, + $clientExtension, + $size + ); + } + + // --- 10. Embedded Code Scan --- + // $this->scanForEmbeddedCode($tempPath, $detectedMime, $clientIp, $uri, $inputName, $originalName, $clientExtension, $size); + } + + private function validateMagicBytes( + string $tempPath, + string $mime, + string $ip, + string $uri, + string $field, + string $filename, + string $ext, + int $size + ): void { + if (!isset($this->magicBytes[$mime])) { + return; + } + // echo $mime;print_r($this->magicBytes[$mime]); + $handle = fopen($tempPath, 'rb'); + if (!$handle) { + $this->block("Cannot read uploaded file", $ip, $uri, $field, $filename, $mime, $ext, $size); + } + + $header = fread($handle, 12); + fclose($handle); + + $matched = false; + foreach ($this->magicBytes[$mime] as $signature) { + if (str_starts_with($header, $signature)) { + $matched = true; + break; + } + } + // echo $matched;die; + // WebP special case: RIFF....WEBP + if ($mime === 'image/webp') { + $matched = (substr($header, 0, 4) === 'RIFF' && substr($header, 8, 4) === 'WEBP'); + } + + if (!$matched) { + $this->block("Magic bytes mismatch for MIME: $mime", $ip, $uri, $field, $filename, $mime, $ext, $size); } } + private function scanForEmbeddedCode( + string $tempPath, + string $mime, + string $ip, + string $uri, + string $field, + string $filename, + string $ext, + int $size + ): void { + $scanLimit = 1024 * 1024; + + $content = ''; + if ($size <= $scanLimit) { + $content = file_get_contents($tempPath); + } else { + $handle = fopen($tempPath, 'rb'); + if ($handle) { + $content = fread($handle, 10240); + fseek($handle, -10240, SEEK_END); + $content .= fread($handle, 10240); + fclose($handle); + } + } + + if (empty($content)) { + return; + } + + $dangerousPatterns = [ + '/<\?php/i', + '/<\?=/i', + '/<\?/i', + '/<%/i', + '/block("Embedded code pattern detected in file", $ip, $uri, $field, $filename, $mime, $ext, $size); + } + } + } + + // block() signature and body kept EXACTLY as original — only $ext source changed upstream private function block(string $reason, string $ip, string $uri, string $field, string $filename, string $mime, string $ext, int $size): void { log_message('critical', @@ -144,4 +329,4 @@ class GlobalPostFileUploadGuard implements FilterInterface } public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) {} -} \ No newline at end of file +} diff --git a/app/Filters/GlobalPostFileUploadGuard_OLD.php b/app/Filters/GlobalPostFileUploadGuard_OLD.php new file mode 100644 index 00000000..75d7588b --- /dev/null +++ b/app/Filters/GlobalPostFileUploadGuard_OLD.php @@ -0,0 +1,150 @@ + ['jpg', 'jpeg'], + 'image/png' => ['png'], + 'image/gif' => ['gif'], + 'image/webp' => ['webp'], + 'image/svg+xml' => ['svg'], + 'application/pdf' => ['pdf'], + 'application/msword' => ['doc'], + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => ['docx'], + 'application/vnd.oasis.opendocument.text' => ['odt'], + 'text/rtf' => ['rtf'], + 'application/rtf' => ['rtf'], + 'application/vnd.ms-excel' => ['xls', 'xlsx'], + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => ['xlsx'], + 'application/vnd.oasis.opendocument.spreadsheet' => ['ods'], + 'text/csv' => ['csv'], + 'application/csv' => ['csv'], + 'text/plain' => ['txt', 'csv'], + ]; + + protected array $blockedExtensions = [ + 'php', 'phtml', 'html', 'pht', 'phar', 'php3', 'php4', 'php5', 'php7', 'php8', 'phps', + 'cgi', 'fcgi', 'pl', 'py', 'rb', 'lua', 'tcl', 'go', 'rs', 'jar', 'class', + 'exe', 'dll', 'com', 'bat', 'cmd', 'msi', 'vbs', 'ps1', 'scr', + 'sh', 'bash', 'zsh', 'apk', 'app', 'deb', 'rpm', 'bin', 'run', + 'js', 'mjs', 'jsp', 'asp', 'aspx', 'cer', 'swf', + 'env', 'ini', 'user.ini', 'htaccess', 'htpasswd', 'conf', 'config', 'log', 'sql', + 'zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz', 'iso', + 'lnk', 'url', 'reg', 'sys', 'drv', 'vxd', 'tmp', 'bak', 'old', 'backup', 'key', 'pem' + ]; + + public function before(RequestInterface $request, $arguments = null) + { + if ($request->getMethod() !== 'post') { + return; + } + + $files = $request->getFiles(); + if (empty($files)) { + return; + } + + foreach ($files as $inputName => $fileData) { + $this->validateFileInput($fileData, $inputName); + } + } + + private function validateFileInput($fileData, string $inputName): void + { + if (is_array($fileData)) { + foreach ($fileData as $file) { + $this->validateSingleFile($file, $inputName); + } + } else { + $this->validateSingleFile($fileData, $inputName); + } + } + + private function validateSingleFile($file, string $inputName): void + { + $request = Services::request(); + $clientIp = $request->getIPAddress(); + $uri = $request->getUri()->getPath(); + + if (!$file->isValid()) { + if ($file->getError() === UPLOAD_ERR_INI_SIZE || $file->getError() === UPLOAD_ERR_FORM_SIZE) { + $this->block("File exceeds server-side size limit", $clientIp, $uri, $inputName, $file->getClientName(), 'unknown', 'unknown', 0); + } + return; + } + + $originalName = $file->getClientName(); + $extension = strtolower($file->getExtension()); + $client_extension = strtolower($file->getClientExtension()); + $mime = $file->getMimeType(); + $size = $file->getSize(); + + echo $extension.' - '.$client_extension;die; + + // --- 1. Fixed Null Byte & Path Traversal Check --- + if (preg_match('/\0|[\/\\\]/', $originalName)) { + $this->block("Malicious filename characters", $clientIp, $uri, $inputName, $originalName, $mime, $extension, $size); + } + + // --- 2. Double Extension Attack Check --- + if (preg_match('/\.(php|html|phtml|phar|exe|sh|bat|cmd|js|jsp|asp|aspx|py|pl)\./i', $originalName)) { + $this->block("Double extension attack", $clientIp, $uri, $inputName, $originalName, $mime, $extension, $size); + } + + // --- 3. Forbidden Extension --- + if (in_array($extension, $this->blockedExtensions, true)) { + $this->block("Forbidden extension", $clientIp, $uri, $inputName, $originalName, $mime, $extension, $size); + } + + // --- 4. File Size Limit --- + if ($size > $this->maxFileSize) { + $this->block("File too large", $clientIp, $uri, $inputName, $originalName, $mime, $extension, $size); + } + + // --- 5. MIME Allow-list Check --- + if (!array_key_exists($mime, $this->allowedMimeMap)) { + $this->block("MIME type not allowed ($mime)", $clientIp, $uri, $inputName, $originalName, $mime, $extension, $size); + } + + // --- 6. MIME-Extension Consistency --- + if (!in_array($extension, $this->allowedMimeMap[$mime], true)) { + $this->block("MIME-extension mismatch", $clientIp, $uri, $inputName, $originalName, $mime, $extension, $size); + } + } + + private function block(string $reason, string $ip, string $uri, string $field, string $filename, string $mime, string $ext, int $size): void + { + log_message('critical', + '[UPLOAD_BLOCKED] {reason} | IP: {ip} | URI: {uri} | Field: {field} | File: {file} | MIME: {mime} | EXT: {ext} | SIZE: {size}', + ['reason'=>$reason, 'ip'=>$ip, 'uri'=>$uri, 'field'=>$field, 'file'=>$filename, 'mime'=>$mime, 'ext'=>$ext, 'size'=>$size] + ); + + $response = Services::response(); + $response->setStatusCode(403) + ->setJSON([ + 'status' => 'error', + 'message' => 'File upload rejected: Security policy violation.' . ($reason ? " Reason: $reason." : ''), + 'debug' => (ENVIRONMENT === 'development') ? $reason : null + ]) + ->send(); + exit; + } + + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) {} +} \ No newline at end of file From c64d32876a5745ae91ba88c836ca2eece6e89b24 Mon Sep 17 00:00:00 2001 From: Gowtham M Date: Fri, 8 May 2026 16:04:46 +0530 Subject: [PATCH 5/6] GWM : merger claim files to be pushed to TAP --- app/Controllers/EmployeeRestController.php | 26 ++++++++++++-------- app/Controllers/FhplApiController.php | 2 +- app/Controllers/HealthIndiaApiController.php | 2 +- app/Controllers/MediAssistApiController.php | 2 +- app/Controllers/TicketController.php | 21 +++++++--------- app/Controllers/VidalApiController.php | 2 +- app/Controllers/VoloApiController.php | 2 +- app/Views/claim_files_upload.php | 12 ++++++--- 8 files changed, 39 insertions(+), 30 deletions(-) diff --git a/app/Controllers/EmployeeRestController.php b/app/Controllers/EmployeeRestController.php index 193762d9..9518d202 100755 --- a/app/Controllers/EmployeeRestController.php +++ b/app/Controllers/EmployeeRestController.php @@ -3785,6 +3785,14 @@ class EmployeeRestController extends AdminController $this->handleCliamFiles($file_data, $ticket_id, $ticket_message_id); + // Merge all uploaded PDFs for this ticket into a single combined PDF + try { + helper('merge_pdf'); + merge_ticket_pdfs((int) $ticket_id); + } catch (\Throwable $e) { + log_message('error', 'EmployeeRestController::initiateClaim | merge_ticket_pdfs failed | ticket_id=' . $ticket_id . ' | ' . $e->getMessage()); + } + $mail_sent_status = ($this->ticketController->sendAutoMailTrigger($ticket_id)); if (gettype($mail_sent_status) == 'array') { $message = 'Claim Iniated Successfully'; @@ -3923,16 +3931,6 @@ class EmployeeRestController extends AdminController } } - // Merge all uploaded PDFs for this ticket into a single combined PDF - if ($pdf_exist_in_the_file) { - try { - helper('merge_pdf'); - merge_ticket_pdfs((int) $ticket_id); - } catch (\Throwable $e) { - log_message('error', 'EmployeeRestController::handleCliamFiles | merge_ticket_pdfs failed | ticket_id=' . $ticket_id . ' | ' . $e->getMessage()); - } - } - $count = $this->ticketMaster->where('id', $ticket_id)->where('is_active', 1)->where('tpa_claim_push_reference_no IS NULL')->countAllResults(); log_message('error', 'Ticket validation | Ticket ID: ' . $ticket_id . ' | Matching active tickets with NULL TPA reference: ' . $count); if ($count > 0 && $pdf_exist_in_the_file && $tpa_claim_push == true) { @@ -5454,6 +5452,14 @@ class EmployeeRestController extends AdminController $result = $this->handleCliamFiles($file_data, $ticket_id, null, false, true); if (! empty($result)) { + // Merge all uploaded PDFs for this ticket into a single combined PDF + try { + helper('merge_pdf'); + merge_ticket_pdfs((int) $ticket_id); + } catch (\Throwable $e) { + log_message('error', 'EmployeeRestController::uploadIRDocs | merge_ticket_pdfs failed | ticket_id=' . $ticket_id . ' | ' . $e->getMessage()); + } + // $this->ticketMaster->where('id', $ticket_id)->set(['required_docs', $required_docs])->update(); db_connect()->query( "UPDATE ticket_master SET required_docs = ? WHERE id = ?", diff --git a/app/Controllers/FhplApiController.php b/app/Controllers/FhplApiController.php index 542f461b..dfa7db1b 100644 --- a/app/Controllers/FhplApiController.php +++ b/app/Controllers/FhplApiController.php @@ -92,7 +92,7 @@ class FhplApiController extends BaseController ->join('employees e', 'e.id = tm.emp_id', 'left') ->join('client_policy cp', 'tm.client_policy_id = cp.id', 'left') ->join('ticket_notes tn', 'tn.ticket_id = tm.id', 'left') - ->join('claim_files cf', "cf.ticket_id = tm.id AND cf.file_type = 2 AND cf.mime_type='application/pdf'", 'left') + ->join('claim_files cf', "cf.ticket_id = tm.id AND cf.file_type = 4 AND cf.is_active = 1 AND cf.mime_type = 'application/pdf'", 'left') ->where('tm.id', $claimId) ->get() ->getRowArray(); diff --git a/app/Controllers/HealthIndiaApiController.php b/app/Controllers/HealthIndiaApiController.php index db356888..46b67043 100644 --- a/app/Controllers/HealthIndiaApiController.php +++ b/app/Controllers/HealthIndiaApiController.php @@ -113,7 +113,7 @@ class HealthIndiaApiController extends BaseController ->join('employees e', 'e.id = tm.emp_id', 'left') ->join('client_policy cp', 'tm.client_policy_id = cp.id', 'left') ->join('ticket_notes tn', 'tn.ticket_id = tm.id', 'left') - ->join('claim_files cf', "cf.ticket_id = tm.id AND cf.file_type = 2 AND cf.mime_type='application/pdf'", 'left') + ->join('claim_files cf', "cf.ticket_id = tm.id AND cf.file_type = 4 AND cf.is_active = 1 AND cf.mime_type = 'application/pdf'", 'left') ->where('tm.id', $claimId) ->get() ->getRowArray(); diff --git a/app/Controllers/MediAssistApiController.php b/app/Controllers/MediAssistApiController.php index 341e3f9b..cb39d93c 100644 --- a/app/Controllers/MediAssistApiController.php +++ b/app/Controllers/MediAssistApiController.php @@ -67,7 +67,7 @@ class MediAssistApiController extends BaseController ->join('employees e', 'e.id = tm.emp_id', 'left') ->join('client_policy cp', 'tm.client_policy_id = cp.id', 'left') ->join('ticket_notes tn', 'tn.ticket_id = tm.id', 'left') - ->join('claim_files cf', "cf.ticket_id = tm.id AND cf.file_type = 2 and cf.mime_type = 'application/pdf'", 'left') + ->join('claim_files cf', "cf.ticket_id = tm.id AND cf.file_type = 4 AND cf.is_active = 1 AND cf.mime_type = 'application/pdf'", 'left') ->where('tm.id', $claimId) ->get() ->getRowArray(); // single record diff --git a/app/Controllers/TicketController.php b/app/Controllers/TicketController.php index bd293ae4..6f64777c 100644 --- a/app/Controllers/TicketController.php +++ b/app/Controllers/TicketController.php @@ -2066,16 +2066,6 @@ class TicketController extends BaseController $return_value = $this->ticketMasterModel->where('id', $ticket_id)->set($ticket_data)->update(); if ($return_value) { - // Merge all uploaded PDFs for this ticket into a single combined PDF - try { - helper('merge_pdf'); - merge_ticket_pdfs((int) $ticket_id, [ - 'ticket_type' => (int) ($ticket_data['ticket_type_id'] ?? 1), - ]); - } catch (\Throwable $e) { - log_message('error', 'TicketController::updateTicket | merge_ticket_pdfs failed | ticket_id=' . $ticket_id . ' | ' . $e->getMessage()); - } - $mail_responce = null; if($old_ticket_data['claim_status_id'] != $ticket_data['claim_status_id']){ //mail trigger part @@ -2413,7 +2403,7 @@ class TicketController extends BaseController $claimFiles = new ClaimFilesModel(); $file_record = $claimFiles->where('id', $claim_file_id)->where('is_active', 1)->first(); - if ($file_record === null || (int) ($file_record['file_type'] ?? 0) !== 2) { + if ($file_record === null || ! in_array((int) ($file_record['file_type'] ?? 0), [2, 4], true)) { throw PageNotFoundException::forPageNotFound(); } @@ -3687,6 +3677,13 @@ class TicketController extends BaseController $employeeRest = new EmployeeRestController(); $result = $employeeRest->handleCliamFiles($file_data, $ticket_id); if(!empty($result)){ + // Merge all uploaded PDFs for this ticket into a single combined PDF + try { + helper('merge_pdf'); + merge_ticket_pdfs((int) $ticket_id); + } catch (\Throwable $e) { + log_message('error', 'TicketController::upload_url | merge_ticket_pdfs failed | ticket_id=' . $ticket_id . ' | ' . $e->getMessage()); + } return $this->respond(['status' => true, 'message' => 'File uploaded successfully ']); }else{ return $this->respond(['status' => false, 'message' => 'Failed to upload file ']); @@ -3714,7 +3711,7 @@ class TicketController extends BaseController ->select(" *, CASE - WHEN file_type = 2 AND url IS NOT NULL AND url != '' + WHEN file_type IN (2, 4) AND url IS NOT NULL AND url != '' THEN CONCAT('" . base_url('downloadClaimFile/') . "', id) ELSE url END AS url diff --git a/app/Controllers/VidalApiController.php b/app/Controllers/VidalApiController.php index d1fd6b40..889ae745 100644 --- a/app/Controllers/VidalApiController.php +++ b/app/Controllers/VidalApiController.php @@ -168,7 +168,7 @@ class VidalApiController extends BaseController ->join('policy_type pt', 'pt.id = cp.policy_type_id', 'left') ->join('employee_polices ep', 'ep.employee_id = e.id AND ep.client_policy_id = cp.id', 'left') ->join('ticket_notes tn', 'tn.ticket_id = tm.id', 'left') - ->join('claim_files cf', "cf.ticket_id = tm.id AND cf.file_type = 2 and cf.mime_type = 'application/pdf'", 'left') + ->join('claim_files cf', "cf.ticket_id = tm.id AND cf.file_type = 4 AND cf.is_active = 1 AND cf.mime_type = 'application/pdf'", 'left') ->where('tm.id', $claimId) ->get() ->getRowArray(); // single record diff --git a/app/Controllers/VoloApiController.php b/app/Controllers/VoloApiController.php index 63e63c6d..b1bc0a4b 100644 --- a/app/Controllers/VoloApiController.php +++ b/app/Controllers/VoloApiController.php @@ -696,7 +696,7 @@ class VoloApiController extends BaseController cf.url as filePath ') ->join('client_policy cp', 'tm.client_policy_id = cp.id', 'left') - ->join('claim_files cf', "cf.ticket_id = tm.id AND cf.file_type = 2 AND cf.mime_type = 'application/pdf'", 'left') + ->join('claim_files cf', "cf.ticket_id = tm.id AND cf.file_type = 4 AND cf.is_active = 1 AND cf.mime_type = 'application/pdf'", 'left') ->where('tm.id', $claimId) ->get() ->getRowArray(); diff --git a/app/Views/claim_files_upload.php b/app/Views/claim_files_upload.php index 1f1d7e39..6ea03e37 100644 --- a/app/Views/claim_files_upload.php +++ b/app/Views/claim_files_upload.php @@ -475,13 +475,19 @@ } } + function claimFileIsLocalUpload(item) { + // file_type 2 = uploaded claim doc, 4 = merged combined PDF (both stored on disk) + return item.file_type == 2 || item.file_type === '2' + || item.file_type == 4 || item.file_type === '4'; + } + function claimFileViewSupported(item) { var mime = String(item.mime_type || '').toLowerCase(); if (mime === 'application/pdf' || mime === 'image/png' || mime === 'image/jpeg') { return true; } var ext = ''; - if (item.file_type == 2 || item.file_type === '2') { + if (claimFileIsLocalUpload(item)) { ext = claimFileListExtension(item.file_name) || claimFileListExtension(item.doc_name); } else { ext = claimFileListExtensionFromUrl(item.url); @@ -493,14 +499,14 @@ } function claimFileViewOpenUrl(item, base_url) { - if (item.file_type == 2 || item.file_type === '2') { + if (claimFileIsLocalUpload(item)) { return base_url + 'viewClaimFile/' + encodeURIComponent(item.id); } return String(item.url || ''); } function claimFileDownloadOpenUrl(item, base_url) { - if (item.file_type == 2 || item.file_type === '2') { + if (claimFileIsLocalUpload(item)) { return base_url + 'downloadClaimFile/' + encodeURIComponent(item.id); } return String(item.url || ''); From 942ba8b37693360abcba6a0eeda0ee947a7ddf83 Mon Sep 17 00:00:00 2001 From: "sanjeev.p" Date: Fri, 8 May 2026 16:45:26 +0530 Subject: [PATCH 6/6] FIX_Dashboard --- app/Views/DashBoard.php | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/app/Views/DashBoard.php b/app/Views/DashBoard.php index 5e2b1ee1..561e8098 100755 --- a/app/Views/DashBoard.php +++ b/app/Views/DashBoard.php @@ -185,6 +185,27 @@ + +