diff --git a/.github/workflows/swagger-json.yml b/.github/workflows/swagger-json.yml new file mode 100644 index 00000000..413643f7 --- /dev/null +++ b/.github/workflows/swagger-json.yml @@ -0,0 +1,109 @@ +name: Sync Swagger to AMRIT-Docs + +on: + push: + branches: [ main ] + workflow_dispatch: + +jobs: + swagger-sync: + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Checkout API repo + uses: actions/checkout@v4 + + - name: Set up Java 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + cache: maven + + - name: Build API (skip tests) + run: mvn -B clean package -DskipTests + + - name: Install jq + run: sudo apt-get update && sudo apt-get install -y jq + + - name: Run API in swagger profile + run: | + mvn spring-boot:run \ + -Dspring-boot.run.profiles=swagger \ + -Dspring-boot.run.arguments=--server.port=9090 \ + > app.log 2>&1 & + echo $! > api_pid.txt + + - name: Wait for API & fetch Swagger + run: | + for i in {1..40}; do + CODE=$(curl --connect-timeout 2 --max-time 5 -s -o swagger_raw.json -w "%{http_code}" http://localhost:9090/v3/api-docs || true) + + if [ "$CODE" = "200" ]; then + jq . swagger_raw.json > common-api.json || { + echo "Swagger JSON invalid" + cat swagger_raw.json + exit 1 + } + + if [ "$(jq '.paths | length' common-api.json)" -eq 0 ]; then + echo "Swagger paths empty – failing" + exit 1 + fi + + echo "Swagger generated successfully" + exit 0 + fi + + echo "Waiting for API... ($i)" + sleep 4 + done + + echo "Swagger not generated" + cat app.log || true + exit 1 + + - name: Stop API + if: always() + run: | + # Graceful shutdown of the process group + sleep 5 + # Force kill the process group if still running + if [ -f api_pid.txt ]; then + PID=$(cat api_pid.txt) + kill -TERM -- -"$PID" 2>/dev/null || true + sleep 2 + kill -9 -- -"$PID" 2>/dev/null || true + fi + # Fallback: kill any remaining java process on port 9090 + fuser -k 9090/tcp 2>/dev/null || true + + - name: Checkout AMRIT-Docs + uses: actions/checkout@v4 + with: + repository: PSMRI/AMRIT-Docs + token: ${{ secrets.DOCS_REPO_TOKEN }} + path: amrit-docs + fetch-depth: 0 + + - name: Copy Swagger JSON + run: | + mkdir -p amrit-docs/docs/swagger + cp common-api.json amrit-docs/docs/swagger/common-api.json + + # Use a fixed branch name for PRs to avoid accumulating stale PRs. + # This ensures only one open PR is updated per run; delete-branch: true cleans up after merge. + - name: Create Pull Request + uses: peter-evans/create-pull-request@v8 + with: + token: ${{ secrets.DOCS_REPO_TOKEN }} + path: amrit-docs + branch: auto/swagger-update-common-api + base: main + commit-message: "chore(docs): auto-update Common-API swagger" + title: "chore(docs): auto-update Common-API swagger" + delete-branch: true + body: | + This PR automatically updates Common-API Swagger JSON + from the latest main branch build. diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..7b016a89 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.compile.nullAnalysis.mode": "automatic" +} \ No newline at end of file diff --git a/README.md b/README.md index 59d3361b..169fc005 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # AMRIT - Common Service -[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) ![branch parameter](https://github.com/PSMRI/Common-API/actions/workflows/sast-and-package.yml/badge.svg) +[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) +[![DeepWiki](https://img.shields.io/badge/DeepWiki-PSMRI/Common--API-blue)](https://deepwiki.com/PSMRI/Common-API) Common API is a microservice whch acts as a gateway for AMRIT. There are many APIs that are exposed by Common-API. It contains APIs of common integrators like c-Zentrix, Everwell, Openkm and some master APIs like location master, alerts, notification,language and location messages. @@ -87,4 +88,3 @@ If you encounter any issues, bugs, or have feature requests, please file them in We’d love to have you join our community discussions and get real-time support! Join our [Discord server](https://discord.gg/FVQWsf5ENS) to connect with contributors, ask questions, and stay updated. - diff --git a/pom.xml b/pom.xml index 171ab162..195de75b 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.iemr.common-API common-api - 3.6.1 + 3.7.0 war Common-API @@ -54,6 +54,12 @@ + + + com.h2database + h2 + runtime + org.springframework.boot @@ -520,6 +526,32 @@ ${artifactId}-${version} + + io.github.git-commit-id + git-commit-id-maven-plugin + 9.0.2 + + + get-the-git-infos + + revision + + initialize + + + + true + ${project.build.outputDirectory}/git.properties + + ^git.branch$ + ^git.commit.id.abbrev$ + ^git.build.version$ + ^git.build.time$ + + false + false + + org.apache.maven.plugins maven-jar-plugin diff --git a/src/main/environment/common_ci.properties b/src/main/environment/common_ci.properties index 9f54a35d..f2b774a3 100644 --- a/src/main/environment/common_ci.properties +++ b/src/main/environment/common_ci.properties @@ -203,5 +203,9 @@ platform.feedback.ratelimit.day-limit=@env.PLATFORM_FEEDBACK_RATELIMIT_DAY_LIMIT platform.feedback.ratelimit.user-day-limit=@env.PLATFORM_FEEDBACK_RATELIMIT_USER_DAY_LIMIT@ platform.feedback.ratelimit.fail-window-minutes=@env.PLATFORM_FEEDBACK_RATELIMIT_FAIL_WINDOW_MINUTES@ platform.feedback.ratelimit.backoff-minutes=@env.PLATFORM_FEEDBACK_RATELIMIT_BACKOFF_MINUTES@ +otp.ratelimit.enabled=@env.OTP_RATELIMIT_ENABLED@ +otp.ratelimit.minute-limit=@env.OTP_RATELIMIT_MINUTE_LIMIT@ +otp.ratelimit.hour-limit=@env.OTP_RATELIMIT_HOUR_LIMIT@ +otp.ratelimit.day-limit=@env.OTP_RATELIMIT_DAY_LIMIT@ generateBeneficiaryIDs-api-url=@env.GEN_BENEFICIARY_IDS_API_URL@ diff --git a/src/main/environment/common_docker.properties b/src/main/environment/common_docker.properties index 59cb580d..a5c633e4 100644 --- a/src/main/environment/common_docker.properties +++ b/src/main/environment/common_docker.properties @@ -206,4 +206,8 @@ platform.feedback.ratelimit.day-limit=${PLATFORM_FEEDBACK_RATELIMIT_DAY_LIMIT} platform.feedback.ratelimit.user-day-limit=${PLATFORM_FEEDBACK_RATELIMIT_USER_DAY_LIMIT} platform.feedback.ratelimit.fail-window-minutes=${PLATFORM_FEEDBACK_RATELIMIT_FAIL_WINDOW_MINUTES} platform.feedback.ratelimit.backoff-minutes=${PLATFORM_FEEDBACK_RATELIMIT_BACKOFF_MINUTES} +otp.ratelimit.enabled=${OTP_RATELIMIT_ENABLED} +otp.ratelimit.minute-limit=${OTP_RATELIMIT_MINUTE_LIMIT} +otp.ratelimit.hour-limit=${OTP_RATELIMIT_HOUR_LIMIT} +otp.ratelimit.day-limit=${OTP_RATELIMIT_DAY_LIMIT} generateBeneficiaryIDs-api-url={GEN_BENEFICIARY_IDS_API_URL} diff --git a/src/main/environment/common_example.properties b/src/main/environment/common_example.properties index aca73ddb..a3b74327 100644 --- a/src/main/environment/common_example.properties +++ b/src/main/environment/common_example.properties @@ -25,6 +25,8 @@ km-root-path=/okm:personal/users/ km-guest-user=guest km-guest-password=guest +tempFilePath=/tmp + # CTI Config cti-server-ip=10.208.122.99 cti-logger_base_url=http://10.208.122.99/logger @@ -224,5 +226,10 @@ platform.feedback.ratelimit.user-day-limit=50 platform.feedback.ratelimit.fail-window-minutes=5 platform.feedback.ratelimit.backoff-minutes=15 +# --- OTP Rate Limiting (per mobile number) --- +otp.ratelimit.minute-limit=3 +otp.ratelimit.hour-limit=10 +otp.ratelimit.day-limit=20 + ### generate Beneficiary IDs URL generateBeneficiaryIDs-api-url=/generateBeneficiaryController/generateBeneficiaryIDs diff --git a/src/main/java/com/iemr/common/config/PrimaryDBConfig.java b/src/main/java/com/iemr/common/config/PrimaryDBConfig.java index 36463ab9..8a77a74a 100644 --- a/src/main/java/com/iemr/common/config/PrimaryDBConfig.java +++ b/src/main/java/com/iemr/common/config/PrimaryDBConfig.java @@ -39,6 +39,7 @@ import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.context.annotation.Profile; import com.iemr.common.utils.config.ConfigProperties; @@ -47,7 +48,8 @@ @Configuration @EnableTransactionManagement @EnableJpaRepositories(entityManagerFactoryRef = "entityManagerFactory", basePackages = { "com.iemr.common.repository", - "com.iemr.common.repo", "com.iemr.common.notification.agent", "com.iemr.common.covidVaccination", "com.iemr.common.repository.everwell.*", "com.iemr.common.data.grievance", "com.iemr.common.repository.users" }) + "com.iemr.common.repo", "com.iemr.common.notification.agent", "com.iemr.common.covidVaccination", "com.iemr.common.repository.everwell.*", "com.iemr.common.data.grievance", "com.iemr.common.repository.users" }) +@Profile("!swagger") public class PrimaryDBConfig { Logger logger = LoggerFactory.getLogger(this.getClass().getName()); diff --git a/src/main/java/com/iemr/common/config/SecondaryDBConfig.java b/src/main/java/com/iemr/common/config/SecondaryDBConfig.java index 8a3928cb..3244612f 100644 --- a/src/main/java/com/iemr/common/config/SecondaryDBConfig.java +++ b/src/main/java/com/iemr/common/config/SecondaryDBConfig.java @@ -43,10 +43,13 @@ import jakarta.persistence.EntityManagerFactory; +import org.springframework.context.annotation.Profile; + @Configuration @EnableTransactionManagement @EnableJpaRepositories(entityManagerFactoryRef = "secondaryEntityManagerFactory", transactionManagerRef = "secondaryTransactionManager", basePackages = { - "com.iemr.common.secondary.repository.callreport" }) + "com.iemr.common.secondary.repository.callreport" }) +@Profile("!swagger") public class SecondaryDBConfig { Logger logger = LoggerFactory.getLogger(this.getClass().getName()); diff --git a/src/main/java/com/iemr/common/config/SwaggerConfig.java b/src/main/java/com/iemr/common/config/SwaggerConfig.java index 793f3a25..04bcec21 100644 --- a/src/main/java/com/iemr/common/config/SwaggerConfig.java +++ b/src/main/java/com/iemr/common/config/SwaggerConfig.java @@ -2,6 +2,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; @@ -11,14 +12,23 @@ @Configuration public class SwaggerConfig { - - @Bean - public OpenAPI customOpenAPI() { - return new OpenAPI().info(new - Info().title("Common API").version("version").description("A microservice for the creation and management of beneficiaries.")) - .addSecurityItem(new SecurityRequirement().addList("my security")) - .components(new Components().addSecuritySchemes("my security", - new SecurityScheme().name("my security").type(SecurityScheme.Type.HTTP).scheme("bearer"))); + private static final String DEFAULT_SERVER_URL = "http://localhost:9090"; + + @Bean + public OpenAPI customOpenAPI(Environment env) { + String devUrl = env.getProperty("api.dev.url", DEFAULT_SERVER_URL); + String uatUrl = env.getProperty("api.uat.url", DEFAULT_SERVER_URL); + String demoUrl = env.getProperty("api.demo.url", DEFAULT_SERVER_URL); + return new OpenAPI() + .info(new Info().title("Common API").version("version").description("A microservice for the creation and management of beneficiaries.")) + .addSecurityItem(new SecurityRequirement().addList("my security")) + .components(new Components().addSecuritySchemes("my security", + new SecurityScheme().name("my security").type(SecurityScheme.Type.HTTP).scheme("bearer"))) + .servers(java.util.Arrays.asList( + new io.swagger.v3.oas.models.servers.Server().url(devUrl).description("Dev"), + new io.swagger.v3.oas.models.servers.Server().url(uatUrl).description("UAT"), + new io.swagger.v3.oas.models.servers.Server().url(demoUrl).description("Demo") + )); } } diff --git a/src/main/java/com/iemr/common/config/quartz/ScheduleJobForNHMDashboardData.java b/src/main/java/com/iemr/common/config/quartz/ScheduleJobForNHMDashboardData.java index fda36f0d..c9b29c62 100644 --- a/src/main/java/com/iemr/common/config/quartz/ScheduleJobForNHMDashboardData.java +++ b/src/main/java/com/iemr/common/config/quartz/ScheduleJobForNHMDashboardData.java @@ -31,9 +31,11 @@ import org.springframework.transaction.annotation.Transactional; import com.iemr.common.service.nhm_dashboard.NHM_DashboardService; +import org.springframework.context.annotation.Profile; @Service @Transactional +@Profile("!swagger") public class ScheduleJobForNHMDashboardData implements Job { private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); diff --git a/src/main/java/com/iemr/common/controller/beneficiary/BeneficiaryRegistrationController.java b/src/main/java/com/iemr/common/controller/beneficiary/BeneficiaryRegistrationController.java index 3784f3a9..67f57981 100644 --- a/src/main/java/com/iemr/common/controller/beneficiary/BeneficiaryRegistrationController.java +++ b/src/main/java/com/iemr/common/controller/beneficiary/BeneficiaryRegistrationController.java @@ -40,6 +40,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; +import org.springframework.context.annotation.Profile; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; @@ -83,6 +84,7 @@ @RequestMapping({ "/beneficiary" }) @RestController +@Profile("!swagger") public class BeneficiaryRegistrationController { private InputMapper inputMapper = new InputMapper(); diff --git a/src/main/java/com/iemr/common/controller/beneficiaryConsent/BeneficiaryConsentController.java b/src/main/java/com/iemr/common/controller/beneficiaryConsent/BeneficiaryConsentController.java index 77492d89..8750c0a1 100644 --- a/src/main/java/com/iemr/common/controller/beneficiaryConsent/BeneficiaryConsentController.java +++ b/src/main/java/com/iemr/common/controller/beneficiaryConsent/BeneficiaryConsentController.java @@ -22,6 +22,7 @@ package com.iemr.common.controller.beneficiaryConsent; import com.iemr.common.data.beneficiaryConsent.BeneficiaryConsentRequest; +import com.iemr.common.exception.OtpRateLimitException; import com.iemr.common.service.beneficiaryOTPHandler.BeneficiaryOTPHandler; import com.iemr.common.utils.mapper.InputMapper; import com.iemr.common.utils.response.OutputResponse; @@ -58,7 +59,9 @@ public String sendConsent(@Param(value = "{\"mobNo\":\"String\"}") @RequestBody logger.info(success.toString()); response.setResponse(success); - + } catch (OtpRateLimitException e) { + logger.warn("OTP rate limit hit for sendConsent: " + e.getMessage()); + response.setError(429, e.getMessage()); } catch (Exception e) { response.setError(500, "error : " + e); } @@ -105,6 +108,9 @@ public String resendConsent(@Param(value = "{\"mobNo\":\"String\"}") @RequestBod else response.setError(500, "failure"); + } catch (OtpRateLimitException e) { + logger.warn("OTP rate limit hit for resendConsent: " + e.getMessage()); + response.setError(429, e.getMessage()); } catch (Exception e) { logger.error("error in re-sending Consent : " + e); response.setError(500, "error : " + e); diff --git a/src/main/java/com/iemr/common/controller/dynamicForm/DynamicFormController.java b/src/main/java/com/iemr/common/controller/dynamicForm/DynamicFormController.java index 30a1bc3f..62bf7e7c 100644 --- a/src/main/java/com/iemr/common/controller/dynamicForm/DynamicFormController.java +++ b/src/main/java/com/iemr/common/controller/dynamicForm/DynamicFormController.java @@ -84,9 +84,9 @@ public ResponseEntity> deleteField(@PathVariable Long fieldId) { } @GetMapping(value = "form/{formId}/fields") - public ResponseEntity> getStructuredForm(@PathVariable String formId, @RequestParam(name = "lang", defaultValue = "en") String lang) { + public ResponseEntity> getStructuredForm(@PathVariable String formId, @RequestParam(name = "lang", defaultValue = "en") String lang,@RequestHeader(value = "jwttoken") String token) { try { - Object result = formMasterService.getStructuredFormByFormId(formId,lang); + Object result = formMasterService.getStructuredFormByFormId(formId,lang,token); return ResponseEntity.status(HttpStatus.OK) .body(ApiResponse.success("Form structure fetched successfully", HttpStatus.OK.value(), result)); } catch (Exception e) { diff --git a/src/main/java/com/iemr/common/controller/health/HealthController.java b/src/main/java/com/iemr/common/controller/health/HealthController.java new file mode 100644 index 00000000..abfb536d --- /dev/null +++ b/src/main/java/com/iemr/common/controller/health/HealthController.java @@ -0,0 +1,66 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package com.iemr.common.controller.health; + +import com.iemr.common.service.health.HealthService; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Health check controller for Common-API. + * Verifies application liveness and dependency health (DB, Redis). + * + * @author vaishnavbhosale + */ +@RestController +public class HealthController { + + private static final Logger logger = LoggerFactory.getLogger(HealthController.class); + + private final HealthService healthService; + + public HealthController(HealthService healthService) { + this.healthService = healthService; + } + + @GetMapping("/health") + public ResponseEntity> health() { + logger.info("Health check endpoint called"); + + + Map healthStatus = healthService.checkHealth(); + + // Standard HTTP Status logic + String status = (String) healthStatus.get("status"); + HttpStatus httpStatus = "UP".equals(status) ? HttpStatus.OK : HttpStatus.SERVICE_UNAVAILABLE; + + logger.info("Health check completed with status: {}", status); + + return ResponseEntity.status(httpStatus).body(healthStatus); + } +} \ No newline at end of file diff --git a/src/main/java/com/iemr/common/controller/secondaryReport/CustomerRelationshipSecondaryReports.java b/src/main/java/com/iemr/common/controller/secondaryReport/CustomerRelationshipSecondaryReports.java index 47d73255..88a8c863 100644 --- a/src/main/java/com/iemr/common/controller/secondaryReport/CustomerRelationshipSecondaryReports.java +++ b/src/main/java/com/iemr/common/controller/secondaryReport/CustomerRelationshipSecondaryReports.java @@ -32,6 +32,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.context.annotation.Profile; import org.springframework.web.bind.annotation.RestController; import com.fasterxml.jackson.databind.ObjectMapper; @@ -46,6 +47,7 @@ import io.swagger.v3.oas.annotations.Operation; import jakarta.servlet.http.HttpServletRequest; +@Profile("!swagger") @RequestMapping({ "/crmReports" }) @RestController public class CustomerRelationshipSecondaryReports { diff --git a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java index 554500f3..fce80930 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -279,6 +279,16 @@ public ResponseEntity refreshToken(@RequestBody Map request) String userId = claims.get("userId", String.class); User user = iemrAdminUserServiceImpl.getUserById(Long.parseLong(userId)); + // validate if user account is locked or de-activated + if(user.getDeleted()){ + logger.warn("Your account is locked or de-activated. Please contact administrator"); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Your account is locked or de-activated. Please contact administrator."); + } + if(user.getStatusID()>2){ + logger.warn("Your account is not active. Please contact administrator"); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Your account is not active. Please contact administrator."); + } + // Validate that the user still exists and is active if (user == null) { logger.warn("Token validation failed: user not found for userId in token."); diff --git a/src/main/java/com/iemr/common/controller/version/VersionController.java b/src/main/java/com/iemr/common/controller/version/VersionController.java index 705fccdc..69b067db 100644 --- a/src/main/java/com/iemr/common/controller/version/VersionController.java +++ b/src/main/java/com/iemr/common/controller/version/VersionController.java @@ -1,77 +1,90 @@ /* -* AMRIT – Accessible Medical Records via Integrated Technology -* Integrated EHR (Electronic Health Records) Solution -* -* Copyright (C) "Piramal Swasthya Management and Research Institute" -* -* This file is part of AMRIT. -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +/** + * REST controller exposing application version and build metadata. + *

+ * Provides the /version endpoint which returns Git metadata + * in a standardized JSON format consistent across all AMRIT APIs. + *

+ * + * @author Vaishnav Bhosale + */ package com.iemr.common.controller.version; -import java.io.BufferedReader; -import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Properties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; -import com.iemr.common.utils.response.OutputResponse; - import io.swagger.v3.oas.annotations.Operation; - @RestController public class VersionController { - private Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName()); + private final Logger logger = + LoggerFactory.getLogger(this.getClass().getSimpleName()); + + private static final String UNKNOWN_VALUE = "unknown"; - @Operation(summary = "Get version") - @RequestMapping(value = "/version", method = { RequestMethod.GET }) - public String versionInformation() { - OutputResponse output = new OutputResponse(); + @Operation(summary = "Get version information") + @GetMapping(value = "/version", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> versionInformation() { + Map response = new LinkedHashMap<>(); try { logger.info("version Controller Start"); - output.setResponse(readGitProperties()); + Properties gitProperties = loadGitProperties(); + response.put("buildTimestamp", gitProperties.getProperty("git.build.time", UNKNOWN_VALUE)); + response.put("version", gitProperties.getProperty("git.build.version", UNKNOWN_VALUE)); + response.put("branch", gitProperties.getProperty("git.branch", UNKNOWN_VALUE)); + response.put("commitHash", gitProperties.getProperty("git.commit.id.abbrev", UNKNOWN_VALUE)); } catch (Exception e) { - output.setError(e); + logger.error("Failed to load version information", e); + response.put("buildTimestamp", UNKNOWN_VALUE); + response.put("version", UNKNOWN_VALUE); + response.put("branch", UNKNOWN_VALUE); + response.put("commitHash", UNKNOWN_VALUE); } - logger.info("version Controller End"); - return output.toString(); + return ResponseEntity.ok(response); } - private String readGitProperties() throws Exception { - ClassLoader classLoader = getClass().getClassLoader(); - InputStream inputStream = classLoader.getResourceAsStream("git.properties"); - - return readFromInputStream(inputStream); - } + - private String readFromInputStream(InputStream inputStream) throws IOException { - StringBuilder resultStringBuilder = new StringBuilder(); - try (BufferedReader br = new BufferedReader(new InputStreamReader(inputStream))) { - String line; - while ((line = br.readLine()) != null) { - resultStringBuilder.append(line).append("\n"); + private Properties loadGitProperties() throws IOException { + Properties properties = new Properties(); + try (InputStream input = getClass().getClassLoader() + .getResourceAsStream("git.properties")) { + if (input != null) { + properties.load(input); } } - return resultStringBuilder.toString(); + return properties; } -} +} \ No newline at end of file diff --git a/src/main/java/com/iemr/common/controller/version/VersionInfo.java b/src/main/java/com/iemr/common/controller/version/VersionInfo.java new file mode 100644 index 00000000..20f560a1 --- /dev/null +++ b/src/main/java/com/iemr/common/controller/version/VersionInfo.java @@ -0,0 +1,46 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +/** + * DTO for exposing build and version metadata. + * + * @author vaishnavbhosale + */ +package com.iemr.common.controller.version; + +public class VersionInfo { + + private String commitHash; + private String buildTime; + + public VersionInfo(String commitHash, String buildTime) { + this.commitHash = commitHash; + this.buildTime = buildTime; + } + + public String getCommitHash() { + return commitHash; + } + + public String getBuildTime() { + return buildTime; + } +} diff --git a/src/main/java/com/iemr/common/data/dynamic_from/FormField.java b/src/main/java/com/iemr/common/data/dynamic_from/FormField.java index 39785ae9..1b195db9 100644 --- a/src/main/java/com/iemr/common/data/dynamic_from/FormField.java +++ b/src/main/java/com/iemr/common/data/dynamic_from/FormField.java @@ -53,7 +53,15 @@ public class FormField { @Column(name = "sequence") private Integer sequence; + @Column(name = "is_editable") + private Boolean isEditable; + + @Column(name = "state_code") + private Integer stateCode; + @Column(name = "created_at") private LocalDateTime createdAt = LocalDateTime.now(); + + } diff --git a/src/main/java/com/iemr/common/data/mmuDrugHistory/PrescribedMMUDrugDetail.java b/src/main/java/com/iemr/common/data/mmuDrugHistory/PrescribedMMUDrugDetail.java new file mode 100644 index 00000000..e478420d --- /dev/null +++ b/src/main/java/com/iemr/common/data/mmuDrugHistory/PrescribedMMUDrugDetail.java @@ -0,0 +1,119 @@ +package com.iemr.common.data.mmuDrugHistory; + +import java.sql.Date; +import java.sql.Timestamp; + +import com.google.gson.annotations.Expose; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; +import lombok.Data; + +@Entity +@Data +@Table(name = "t_prescribeddrug") +public class PrescribedMMUDrugDetail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Expose + @Column(name = "PrescribedDrugID") + private Long prescribedDrugID; + + @Expose + @Column(name = "BeneficiaryRegID") + private Long beneficiaryRegID; + + @Expose + @Column(name = "BenVisitID") + private Long benVisitID; + + @Expose + @Column(name = "ProviderServiceMapID") + private Integer providerServiceMapID; + + @Expose + @Column(name = "VisitCode") + private Long visitCode; + + @Expose + @Column(name = "PrescriptionID") + private Long prescriptionID; + + @OneToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "PrescriptionID", referencedColumnName = "PrescriptionID", insertable = false, updatable = false) + private PrescriptionMMU prescription; + + @Expose + @Column(name = "DrugForm") + private String formName; + + @Expose + @Column(name = "DrugTradeOrBrandName") + private String drugTradeOrBrandName; + + @Expose + @Column(name = "DrugID") + private Integer drugID; + + @Expose + @Column(name = "GenericDrugName") + private String drugName; + + @Expose + @Column(name = "DrugStrength") + private String drugStrength; + + @Expose + @Column(name = "Dose") + private String dose; + + @Expose + @Column(name = "Route") + private String route; + + @Expose + @Column(name = "Frequency") + private String frequency; + + @Expose + @Column(name = "Duration") + private String duration; + + @Expose + @Column(name = "DuartionUnit") + private String unit; + + @Expose + @Column(name = "RelationToFood") + private String relationToFood; + + @Expose + @Column(name = "SpecialInstruction") + private String instructions; + + @Expose + @Column(name = "QtyPrescribed") + private Integer qtyPrescribed; + + @Expose + @Column(name = "Deleted", insertable = false, updatable = true) + private Boolean deleted; + + @Expose + @Column(name = "Processed", insertable = false, updatable = true) + private String processed; + + @Expose + @Column(name = "CreatedBy") + private String createdBy; + +} \ No newline at end of file diff --git a/src/main/java/com/iemr/common/data/mmuDrugHistory/PrescriptionMMU.java b/src/main/java/com/iemr/common/data/mmuDrugHistory/PrescriptionMMU.java new file mode 100644 index 00000000..cd3f655e --- /dev/null +++ b/src/main/java/com/iemr/common/data/mmuDrugHistory/PrescriptionMMU.java @@ -0,0 +1,43 @@ +package com.iemr.common.data.mmuDrugHistory; + +import com.google.gson.annotations.Expose; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Data; + +@Entity +@Data +@Table(name = "t_prescription") +public class PrescriptionMMU { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Expose + @Column(name = "PrescriptionID", insertable = false, updatable = false) + private Long prescriptionID; + + @Expose + @Column(name = "BenVisitID") + private Long benVisitID; + + @Expose + @Column(name = "ProviderServiceMapID") + private Integer providerServiceMapID; + + @Expose + @Column(name = "DiagnosisProvided") + private String diagnosisProvided; + + @Expose + @Column(name = "Remarks") + private String remarks; + + @Expose + @Column(name = "Deleted", insertable = false, updatable = true) + private Boolean deleted; + +} diff --git a/src/main/java/com/iemr/common/data/translation/Translation.java b/src/main/java/com/iemr/common/data/translation/Translation.java index 81a906fa..0dad116d 100644 --- a/src/main/java/com/iemr/common/data/translation/Translation.java +++ b/src/main/java/com/iemr/common/data/translation/Translation.java @@ -18,6 +18,8 @@ public class Translation { private String english; @Column(name = "hindi_translation") private String hindiTranslation; + @Column(name = "assamese_translation") + private String assameseTranslation; @Column(name = "is_active") private Boolean isActive; } diff --git a/src/main/java/com/iemr/common/data/users/UserServiceRole.java b/src/main/java/com/iemr/common/data/users/UserServiceRole.java new file mode 100644 index 00000000..935940d5 --- /dev/null +++ b/src/main/java/com/iemr/common/data/users/UserServiceRole.java @@ -0,0 +1,364 @@ +package com.iemr.common.data.users; + +import jakarta.persistence.*; +import java.util.Objects; + +@Entity +@Table(name = "v_userservicerolemapping", schema = "db_iemr") +public class UserServiceRole { + private Integer userId; + private int usrMappingId; + private String name; + private String userName; + private Short serviceId; + private String serviceName; + private Boolean isNational; + private Integer stateId; + private String stateName; + private Integer workingDistrictId; + private String workingDistrictName; + private Integer workingLocationId; + private Short serviceProviderId; + private String locationName; + private String workingLocationAddress; + private Integer roleId; + private String roleName; + private Integer providerServiceMapId; + private String agentId; + private Short psmStatusId; + private String psmStatus; + private Boolean userServciceRoleDeleted; + private Boolean userDeleted; + private Boolean serviceProviderDeleted; + private Boolean roleDeleted; + private Boolean providerServiceMappingDeleted; + private Boolean isInbound; + private Boolean isOutbound; + private Integer blockid; + private String blockname; + private String villageid; + private String villagename; + + @Basic + @Column(name = "UserID") + public Integer getUserId() { + return userId; + } + + public void setUserId(Integer userId) { + this.userId = userId; + } + + @Basic + @Column(name = "USRMappingID") + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + public int getUsrMappingId() { + return usrMappingId; + } + + public void setUsrMappingId(int usrMappingId) { + this.usrMappingId = usrMappingId; + } + + @Basic + @Column(name = "Name") + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Basic + @Column(name = "UserName") + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + @Basic + @Column(name = "ServiceID") + public Short getServiceId() { + return serviceId; + } + + public void setServiceId(Short serviceId) { + this.serviceId = serviceId; + } + + @Basic + @Column(name = "ServiceName") + public String getServiceName() { + return serviceName; + } + + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + @Basic + @Column(name = "IsNational") + public Boolean getNational() { + return isNational; + } + + public void setNational(Boolean national) { + isNational = national; + } + + @Basic + @Column(name = "StateID") + public Integer getStateId() { + return stateId; + } + + public void setStateId(Integer stateId) { + this.stateId = stateId; + } + + @Basic + @Column(name = "StateName") + public String getStateName() { + return stateName; + } + + public void setStateName(String stateName) { + this.stateName = stateName; + } + + @Basic + @Column(name = "WorkingDistrictID") + public Integer getWorkingDistrictId() { + return workingDistrictId; + } + + public void setWorkingDistrictId(Integer workingDistrictId) { + this.workingDistrictId = workingDistrictId; + } + + @Basic + @Column(name = "WorkingDistrictName") + public String getWorkingDistrictName() { + return workingDistrictName; + } + + public void setWorkingDistrictName(String workingDistrictName) { + this.workingDistrictName = workingDistrictName; + } + + @Basic + @Column(name = "WorkingLocationID") + public Integer getWorkingLocationId() { + return workingLocationId; + } + + public void setWorkingLocationId(Integer workingLocationId) { + this.workingLocationId = workingLocationId; + } + + @Basic + @Column(name = "ServiceProviderID") + public Short getServiceProviderId() { + return serviceProviderId; + } + + public void setServiceProviderId(Short serviceProviderId) { + this.serviceProviderId = serviceProviderId; + } + + @Basic + @Column(name = "LocationName") + public String getLocationName() { + return locationName; + } + + public void setLocationName(String locationName) { + this.locationName = locationName; + } + + @Basic + @Column(name = "WorkingLocationAddress") + public String getWorkingLocationAddress() { + return workingLocationAddress; + } + + public void setWorkingLocationAddress(String workingLocationAddress) { + this.workingLocationAddress = workingLocationAddress; + } + + @Basic + @Column(name = "RoleID") + public Integer getRoleId() { + return roleId; + } + + public void setRoleId(Integer roleId) { + this.roleId = roleId; + } + + @Basic + @Column(name = "RoleName") + public String getRoleName() { + return roleName; + } + + public void setRoleName(String roleName) { + this.roleName = roleName; + } + + @Basic + @Column(name = "ProviderServiceMapID") + public Integer getProviderServiceMapId() { + return providerServiceMapId; + } + + public void setProviderServiceMapId(Integer providerServiceMapId) { + this.providerServiceMapId = providerServiceMapId; + } + + @Basic + @Column(name = "AgentID") + public String getAgentId() { + return agentId; + } + + public void setAgentId(String agentId) { + this.agentId = agentId; + } + + @Basic + @Column(name = "PSMStatusID") + public Short getPsmStatusId() { + return psmStatusId; + } + + public void setPsmStatusId(Short psmStatusId) { + this.psmStatusId = psmStatusId; + } + + @Basic + @Column(name = "PSMStatus") + public String getPsmStatus() { + return psmStatus; + } + + public void setPsmStatus(String psmStatus) { + this.psmStatus = psmStatus; + } + + @Basic + @Column(name = "UserServciceRoleDeleted") + public Boolean getUserServciceRoleDeleted() { + return userServciceRoleDeleted; + } + + public void setUserServciceRoleDeleted(Boolean userServciceRoleDeleted) { + this.userServciceRoleDeleted = userServciceRoleDeleted; + } + + @Basic + @Column(name = "UserDeleted") + public Boolean getUserDeleted() { + return userDeleted; + } + + public void setUserDeleted(Boolean userDeleted) { + this.userDeleted = userDeleted; + } + + @Basic + @Column(name = "ServiceProviderDeleted") + public Boolean getServiceProviderDeleted() { + return serviceProviderDeleted; + } + + public void setServiceProviderDeleted(Boolean serviceProviderDeleted) { + this.serviceProviderDeleted = serviceProviderDeleted; + } + + @Basic + @Column(name = "RoleDeleted") + public Boolean getRoleDeleted() { + return roleDeleted; + } + + public void setRoleDeleted(Boolean roleDeleted) { + this.roleDeleted = roleDeleted; + } + + @Basic + @Column(name = "ProviderServiceMappingDeleted") + public Boolean getProviderServiceMappingDeleted() { + return providerServiceMappingDeleted; + } + + public void setProviderServiceMappingDeleted(Boolean providerServiceMappingDeleted) { + this.providerServiceMappingDeleted = providerServiceMappingDeleted; + } + + @Basic + @Column(name = "isInbound") + public Boolean getInbound() { + return isInbound; + } + + public void setInbound(Boolean inbound) { + isInbound = inbound; + } + + @Basic + @Column(name = "isOutbound") + public Boolean getOutbound() { + return isOutbound; + } + + public void setOutbound(Boolean outbound) { + isOutbound = outbound; + } + + @Basic + @Column(name = "blockid") + public Integer getBlockid() { + return blockid; + } + + public void setBlockid(Integer blockid) { + this.blockid = blockid; + } + + @Basic + @Column(name = "blockname") + public String getBlockname() { + return blockname; + } + + public void setBlockname(String blockname) { + this.blockname = blockname; + } + + @Basic + @Column(name = "villageid") + public String getVillageid() { + return villageid; + } + + public void setVillageid(String villageid) { + this.villageid = villageid; + } + + @Basic + @Column(name = "villagename") + public String getVillagename() { + return villagename; + } + + public void setVillagename(String villagename) { + this.villagename = villagename; + } + +} diff --git a/src/main/java/com/iemr/common/dto/dynamicForm/FieldResponseDTO.java b/src/main/java/com/iemr/common/dto/dynamicForm/FieldResponseDTO.java index 3415d91a..e41f8e80 100644 --- a/src/main/java/com/iemr/common/dto/dynamicForm/FieldResponseDTO.java +++ b/src/main/java/com/iemr/common/dto/dynamicForm/FieldResponseDTO.java @@ -18,6 +18,8 @@ public class FieldResponseDTO { private String defaultValue; private String placeholder; private Integer sequence; + private Boolean isEditable; + private Integer stateCode; private List options; private Map validation; private Map conditional; diff --git a/src/main/java/com/iemr/common/dto/sms/SMSTemplateDTO.java b/src/main/java/com/iemr/common/dto/sms/SMSTemplateDTO.java new file mode 100644 index 00000000..ab71d8cc --- /dev/null +++ b/src/main/java/com/iemr/common/dto/sms/SMSTemplateDTO.java @@ -0,0 +1,114 @@ +package com.iemr.common.dto.sms; + +public class SMSTemplateDTO { + private Integer smsTemplateID; + private String smsTemplateName; + private String smsTemplate; + private String dltTemplateId; + private String smsSenderID; + private Integer smsTypeID; + private Integer providerServiceMapID; + private Boolean deleted; + private String createdBy; + private String modifiedBy; + private String createdDate; + private String lastModDate; + + // Getters and Setters for all fields + + public Integer getSmsTemplateID() { + return smsTemplateID; + } + + public void setSmsTemplateID(Integer smsTemplateID) { + this.smsTemplateID = smsTemplateID; + } + + public String getSmsTemplateName() { + return smsTemplateName; + } + + public void setSmsTemplateName(String smsTemplateName) { + this.smsTemplateName = smsTemplateName; + } + + public String getSmsTemplate() { + return smsTemplate; + } + + public void setSmsTemplate(String smsTemplate) { + this.smsTemplate = smsTemplate; + } + + public String getDltTemplateId() { + return dltTemplateId; + } + + public void setDltTemplateId(String dltTemplateId) { + this.dltTemplateId = dltTemplateId; + } + + public String getSmsSenderID() { + return smsSenderID; + } + + public void setSmsSenderID(String smsSenderID) { + this.smsSenderID = smsSenderID; + } + + public Integer getSmsTypeID() { + return smsTypeID; + } + + public void setSmsTypeID(Integer smsTypeID) { + this.smsTypeID = smsTypeID; + } + + public Integer getProviderServiceMapID() { + return providerServiceMapID; + } + + public void setProviderServiceMapID(Integer providerServiceMapID) { + this.providerServiceMapID = providerServiceMapID; + } + + public Boolean getDeleted() { + return deleted; + } + + public void setDeleted(Boolean deleted) { + this.deleted = deleted; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + + public String getModifiedBy() { + return modifiedBy; + } + + public void setModifiedBy(String modifiedBy) { + this.modifiedBy = modifiedBy; + } + + public String getCreatedDate() { + return createdDate; + } + + public void setCreatedDate(String createdDate) { + this.createdDate = createdDate; + } + + public String getLastModDate() { + return lastModDate; + } + + public void setLastModDate(String lastModDate) { + this.lastModDate = lastModDate; + } +} diff --git a/src/main/java/com/iemr/common/exception/OtpRateLimitException.java b/src/main/java/com/iemr/common/exception/OtpRateLimitException.java new file mode 100644 index 00000000..a0f3b53f --- /dev/null +++ b/src/main/java/com/iemr/common/exception/OtpRateLimitException.java @@ -0,0 +1,30 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.common.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.TOO_MANY_REQUESTS) +public class OtpRateLimitException extends RuntimeException { + public OtpRateLimitException(String message) { super(message); } +} diff --git a/src/main/java/com/iemr/common/mapper/sms/SMSMapper.java b/src/main/java/com/iemr/common/mapper/sms/SMSMapper.java index 7fe629f1..1982a74f 100644 --- a/src/main/java/com/iemr/common/mapper/sms/SMSMapper.java +++ b/src/main/java/com/iemr/common/mapper/sms/SMSMapper.java @@ -21,7 +21,9 @@ */ package com.iemr.common.mapper.sms; +import java.text.SimpleDateFormat; import java.util.List; +import java.sql.Timestamp; import org.mapstruct.IterableMapping; import org.mapstruct.Mapper; @@ -33,6 +35,7 @@ import com.iemr.common.data.sms.SMSParametersMap; import com.iemr.common.data.sms.SMSTemplate; import com.iemr.common.data.sms.SMSType; +import com.iemr.common.dto.sms.SMSTemplateDTO; import com.iemr.common.model.sms.CreateSMSRequest; import com.iemr.common.model.sms.FullSMSTemplateResponse; import com.iemr.common.model.sms.SMSParameterMapModel; @@ -110,4 +113,25 @@ public interface SMSMapper @IterableMapping(elementTargetType = FullSMSTemplateResponse.class) List smsTemplateToFullResponse(List smsTemplate); + @Mapping(source = "smsTemplateID", target = "smsTemplateID") + @Mapping(source = "smsTemplateName", target = "smsTemplateName") + @Mapping(source = "smsTemplate", target = "smsTemplate") + @Mapping(source = "dltTemplateId", target = "dltTemplateId") + @Mapping(source = "smsSenderID", target = "smsSenderID") + @Mapping(source = "smsTypeID", target = "smsTypeID") + @Mapping(source = "providerServiceMapID", target = "providerServiceMapID") + @Mapping(source = "deleted", target = "deleted") + @Mapping(source = "createdBy", target = "createdBy") + @Mapping(source = "modifiedBy", target = "modifiedBy") + @Mapping(target = "createdDate", expression = "java(formatDate(template.getCreatedDate()))") + @Mapping(target = "lastModDate", expression = "java(formatDate(template.getLastModDate()))") + SMSTemplateDTO smsTemplateToDTO(SMSTemplate template); + + default String formatDate(Timestamp timestamp) { + if (timestamp == null) { + return null; + } + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); + return sdf.format(timestamp); + } } diff --git a/src/main/java/com/iemr/common/repository/mmuDrugHistory/PrescribedMMUDrugRepository.java b/src/main/java/com/iemr/common/repository/mmuDrugHistory/PrescribedMMUDrugRepository.java new file mode 100644 index 00000000..17eb2c13 --- /dev/null +++ b/src/main/java/com/iemr/common/repository/mmuDrugHistory/PrescribedMMUDrugRepository.java @@ -0,0 +1,11 @@ +package com.iemr.common.repository.mmuDrugHistory; + +import com.iemr.common.data.mmuDrugHistory.PrescribedMMUDrugDetail; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PrescribedMMUDrugRepository extends CrudRepository { + PrescribedMMUDrugDetail findByPrescribedDrugID(Long prescribedDrugID); + +} diff --git a/src/main/java/com/iemr/common/repository/users/UserServiceRoleRepo.java b/src/main/java/com/iemr/common/repository/users/UserServiceRoleRepo.java new file mode 100644 index 00000000..cfb85d0a --- /dev/null +++ b/src/main/java/com/iemr/common/repository/users/UserServiceRoleRepo.java @@ -0,0 +1,12 @@ +package com.iemr.common.repository.users; + +import com.iemr.common.data.users.UserServiceRole; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface UserServiceRoleRepo extends JpaRepository { + List findByUserName(String userName); +} diff --git a/src/main/java/com/iemr/common/secondary/repository/callreport/CallReportSecondaryRepo.java b/src/main/java/com/iemr/common/secondary/repository/callreport/CallReportSecondaryRepo.java index 0a5dee7f..8e7e7ea8 100644 --- a/src/main/java/com/iemr/common/secondary/repository/callreport/CallReportSecondaryRepo.java +++ b/src/main/java/com/iemr/common/secondary/repository/callreport/CallReportSecondaryRepo.java @@ -27,10 +27,12 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.query.Param; +import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Repository; import common.iemr.common.secondary.data.report.SecondaryCallReport; +@Profile("!swagger") @Repository public interface CallReportSecondaryRepo extends CrudRepository { @Query(value="call Pr_104QAReport(:startDateTime,:endDateTime,:receivedRoleName,:agentID,:providerServiceMapID)", nativeQuery=true) diff --git a/src/main/java/com/iemr/common/service/beneficiary/BenRelationshipTypeServiceImpl.java b/src/main/java/com/iemr/common/service/beneficiary/BenRelationshipTypeServiceImpl.java index 01a98eb2..31d9b227 100644 --- a/src/main/java/com/iemr/common/service/beneficiary/BenRelationshipTypeServiceImpl.java +++ b/src/main/java/com/iemr/common/service/beneficiary/BenRelationshipTypeServiceImpl.java @@ -31,7 +31,9 @@ import com.iemr.common.data.beneficiary.BenRelationshipType; import com.iemr.common.repository.beneficiary.BeneficiaryRelationshipTypeRepository; +import org.springframework.context.annotation.Profile; @Service +@Profile("!swagger") public class BenRelationshipTypeServiceImpl implements BenRelationshipTypeService { private BeneficiaryRelationshipTypeRepository beneficiaryRelationshipTypeRepository; diff --git a/src/main/java/com/iemr/common/service/beneficiary/BeneficiaryOccupationServiceImpl.java b/src/main/java/com/iemr/common/service/beneficiary/BeneficiaryOccupationServiceImpl.java index b272cb05..32bb9565 100644 --- a/src/main/java/com/iemr/common/service/beneficiary/BeneficiaryOccupationServiceImpl.java +++ b/src/main/java/com/iemr/common/service/beneficiary/BeneficiaryOccupationServiceImpl.java @@ -31,8 +31,10 @@ import com.iemr.common.data.beneficiary.BeneficiaryOccupation; import com.iemr.common.repository.beneficiary.BeneficiaryOccupationRepository; +import org.springframework.context.annotation.Profile; @Service +@Profile("!swagger") public class BeneficiaryOccupationServiceImpl implements BeneficiaryOccupationService { private BeneficiaryOccupationRepository beneficiaryOccupationRepository; diff --git a/src/main/java/com/iemr/common/service/beneficiary/RegisterBenificiaryServiceImpl.java b/src/main/java/com/iemr/common/service/beneficiary/RegisterBenificiaryServiceImpl.java index 7d5f1de0..82acb58f 100644 --- a/src/main/java/com/iemr/common/service/beneficiary/RegisterBenificiaryServiceImpl.java +++ b/src/main/java/com/iemr/common/service/beneficiary/RegisterBenificiaryServiceImpl.java @@ -60,12 +60,13 @@ import com.iemr.common.utils.validator.Validator; import jakarta.servlet.http.HttpServletRequest; - +import org.springframework.context.annotation.Profile; /** * @author WA875423 * */ @Service +@Profile("!swagger") public class RegisterBenificiaryServiceImpl implements RegisterBenificiaryService { @Autowired diff --git a/src/main/java/com/iemr/common/service/beneficiary/SexualOrientationServiceImpl.java b/src/main/java/com/iemr/common/service/beneficiary/SexualOrientationServiceImpl.java index cd6d54c1..f910ced6 100644 --- a/src/main/java/com/iemr/common/service/beneficiary/SexualOrientationServiceImpl.java +++ b/src/main/java/com/iemr/common/service/beneficiary/SexualOrientationServiceImpl.java @@ -31,8 +31,9 @@ import com.iemr.common.data.beneficiary.SexualOrientation; import com.iemr.common.repository.userbeneficiarydata.SexualOrientationRepository; - +import org.springframework.context.annotation.Profile; @Service +@Profile("!swagger") public class SexualOrientationServiceImpl implements SexualOrientationService { private SexualOrientationRepository sexualOrientationRepository; diff --git a/src/main/java/com/iemr/common/service/beneficiaryOTPHandler/BeneficiaryOTPHandlerImpl.java b/src/main/java/com/iemr/common/service/beneficiaryOTPHandler/BeneficiaryOTPHandlerImpl.java index 42e0acfe..3cc0a709 100644 --- a/src/main/java/com/iemr/common/service/beneficiaryOTPHandler/BeneficiaryOTPHandlerImpl.java +++ b/src/main/java/com/iemr/common/service/beneficiaryOTPHandler/BeneficiaryOTPHandlerImpl.java @@ -32,6 +32,7 @@ import com.iemr.common.repository.sms.SMSTemplateRepository; import com.iemr.common.repository.sms.SMSTypeRepository; import com.iemr.common.service.otp.OTPHandler; +import com.iemr.common.service.otp.OtpRateLimiterService; import com.iemr.common.service.users.IEMRAdminUserServiceImpl; import com.iemr.common.utils.config.ConfigProperties; import com.iemr.common.utils.http.HttpUtils; @@ -59,6 +60,8 @@ public class BeneficiaryOTPHandlerImpl implements BeneficiaryOTPHandler { HttpUtils httpUtils; @Autowired private IEMRAdminUserServiceImpl iEMRAdminUserServiceImpl; + @Autowired + private OtpRateLimiterService otpRateLimiterService; final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); @Autowired @@ -107,6 +110,7 @@ public String load(String key) { */ @Override public String sendOTP(BeneficiaryConsentRequest obj) throws Exception { + otpRateLimiterService.checkRateLimit(obj.getMobNo()); int otp = generateOTP(obj.getMobNo()); return sendSMS(otp, obj); } @@ -141,6 +145,7 @@ public JSONObject validateOTP(BeneficiaryConsentRequest obj) throws Exception { */ @Override public String resendOTP(BeneficiaryConsentRequest obj) throws Exception { + otpRateLimiterService.checkRateLimit(obj.getMobNo()); int otp = generateOTP(obj.getMobNo()); return sendSMS(otp, obj); } diff --git a/src/main/java/com/iemr/common/service/dynamicForm/FormMasterService.java b/src/main/java/com/iemr/common/service/dynamicForm/FormMasterService.java index 6d22e59a..e00663b7 100644 --- a/src/main/java/com/iemr/common/service/dynamicForm/FormMasterService.java +++ b/src/main/java/com/iemr/common/service/dynamicForm/FormMasterService.java @@ -15,8 +15,6 @@ public interface FormMasterService { FormDefinition createForm(FormDTO dto); List createField(List dto); FormField updateField(FieldDTO dto); - - FormResponseDTO getStructuredFormByFormId(String formId,String lang); - + FormResponseDTO getStructuredFormByFormId(String formId,String lang,String token); void deleteField(Long fieldId); } diff --git a/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java b/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java index df019de7..acbcb4e7 100644 --- a/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java +++ b/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java @@ -1,38 +1,48 @@ package com.iemr.common.service.dynamicForm; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.iemr.common.data.dynamic_from.FormDefinition; import com.iemr.common.data.dynamic_from.FormField; import com.iemr.common.data.dynamic_from.FormModule; import com.iemr.common.data.translation.Translation; +import com.iemr.common.data.users.UserServiceRole; import com.iemr.common.dto.dynamicForm.*; import com.iemr.common.repository.dynamic_form.FieldRepository; import com.iemr.common.repository.dynamic_form.FormRepository; import com.iemr.common.repository.dynamic_form.ModuleRepository; import com.iemr.common.repository.translation.TranslationRepo; +import com.iemr.common.repository.users.UserServiceRoleRepo; +import com.iemr.common.utils.JwtUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.fasterxml.jackson.core.type.TypeReference; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; @Service public class FormMasterServiceImpl implements FormMasterService { + final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); @Autowired private ModuleRepository moduleRepo; - @Autowired private FormRepository formRepo; - @Autowired private FieldRepository fieldRepo; + @Autowired + private FormRepository formRepo; + @Autowired + private FieldRepository fieldRepo; @Autowired private TranslationRepo translationRepo; + @Autowired + private UserServiceRoleRepo userServiceRoleRepo; + + @Autowired + private JwtUtil jwtUtil; + @Override public FormModule createModule(ModuleDTO dto) { FormModule module = new FormModule(); @@ -85,7 +95,7 @@ public List createField(List dtoList) { public FormField updateField(FieldDTO dto) { FormField field = fieldRepo.findById(dto.getId()) .orElseThrow(() -> new IllegalArgumentException("Field not found: " + dto.getId())); - field.setId(dto.getId()); + field.setId(dto.getId()); field.setSectionTitle(dto.getSectionTitle()); field.setLabel(dto.getLabel()); field.setType(dto.getType()); @@ -103,96 +113,124 @@ public FormField updateField(FieldDTO dto) { } @Override - public FormResponseDTO getStructuredFormByFormId(String formId,String lang) { - FormDefinition form = formRepo.findByFormId(formId) - .orElseThrow(() -> new IllegalArgumentException("Invalid form ID")); + public FormResponseDTO getStructuredFormByFormId(String formId, String lang, String token) { + Integer stateId = 0; + try { + String username = jwtUtil.getUsernameFromToken(token); - List fields = fieldRepo.findByForm_FormIdOrderBySequenceAsc(formId); - ObjectMapper objectMapper = new ObjectMapper(); + stateId = userServiceRoleRepo.findByUserName(username) + .stream() + .findFirst() + .map(UserServiceRole::getStateId) + .filter(Objects::nonNull) + .orElse(null); - List fieldDtos = fields.stream() - .map(field -> { - String labelKey = field.getFieldId(); // field label already contains label_key - Translation t = translationRepo.findByLabelKeyAndIsActive(labelKey, true) - .orElse(null); + FormDefinition form = formRepo.findByFormId(formId) + .orElseThrow(() -> new IllegalArgumentException("Invalid form ID")); - String translatedLabel = field.getLabel(); // fallback + List fields = fieldRepo.findByForm_FormIdOrderBySequenceAsc(formId); + ObjectMapper objectMapper = new ObjectMapper(); - if (t != null) { - if ("hi".equalsIgnoreCase(lang)) { - translatedLabel = t.getHindiTranslation(); - } else { - translatedLabel = t.getEnglish(); - } - } - - FieldResponseDTO dto = new FieldResponseDTO(); - dto.setId(field.getId()); - dto.setVisible(field.getIsVisible()); - dto.setFormId(field.getForm().getFormId()); - dto.setSectionTitle(field.getSectionTitle()); - dto.setFieldId(field.getFieldId()); - dto.setLabel(translatedLabel); - dto.setType(field.getType()); - dto.setIsRequired(field.getIsRequired()); - dto.setDefaultValue(field.getDefaultValue()); - dto.setPlaceholder(field.getPlaceholder()); - dto.setSequence(field.getSequence()); - - try { - // Handle options - if (field.getOptions() != null && !field.getOptions().isBlank()) { - JsonNode node = objectMapper.readTree(field.getOptions()); - List options = null; - if (node.isArray()) { - options = objectMapper.convertValue(node, new TypeReference<>() {}); - } else if (node.has("options")) { - options = objectMapper.convertValue(node.get("options"), new TypeReference<>() {}); - } - dto.setOptions(options == null || options.isEmpty() ? null : options); - } else { - dto.setOptions(null); - } + Integer finalStateId = stateId; + List fieldDtos = fields.stream().filter(formField -> (formField.getStateCode().equals(0) || formField.getStateCode().equals(finalStateId))) + .map(field -> { + String labelKey = field.getFieldId(); // field label already contains label_key - // Handle validation - if (field.getValidation() != null && !field.getValidation().isBlank()) { - Map validation = objectMapper.readValue(field.getValidation(), new TypeReference<>() {}); - dto.setValidation(validation.isEmpty() ? null : validation); - } else { - dto.setValidation(null); - } + Translation t = translationRepo.findByLabelKeyAndIsActive(labelKey, true) + .orElse(null); + + String translatedLabel = field.getLabel(); // fallback + + if (t != null) { + if ("hi".equalsIgnoreCase(lang)) { + translatedLabel = t.getHindiTranslation(); + } else if ("as".equalsIgnoreCase(lang)) { + translatedLabel = t.getAssameseTranslation(); + } else if ("en".equalsIgnoreCase(lang)) { + translatedLabel = t.getEnglish(); - // Handle conditional - if (field.getConditional() != null && !field.getConditional().isBlank()) { - Map conditional = objectMapper.readValue(field.getConditional(), new TypeReference<>() {}); - dto.setConditional(conditional.isEmpty() ? null : conditional); - } else { - dto.setConditional(null); + } } - } catch (Exception e) { - System.err.println("JSON Parsing Error in field: " + field.getFieldId()); - throw new RuntimeException("Failed to parse JSON for field: " + field.getFieldId(), e); - } + FieldResponseDTO dto = new FieldResponseDTO(); + dto.setId(field.getId()); + dto.setIsEditable(field.getIsEditable()); + dto.setStateCode(field.getStateCode()); + dto.setVisible(field.getIsVisible()); + dto.setFormId(field.getForm().getFormId()); + dto.setSectionTitle(field.getSectionTitle()); + dto.setFieldId(field.getFieldId()); + dto.setLabel(translatedLabel); + dto.setType(field.getType()); + dto.setIsRequired(field.getIsRequired()); + dto.setDefaultValue(field.getDefaultValue()); + dto.setPlaceholder(field.getPlaceholder()); + dto.setSequence(field.getSequence()); + + try { + // Handle options + if (field.getOptions() != null && !field.getOptions().isBlank()) { + JsonNode node = objectMapper.readTree(field.getOptions()); + List options = null; + if (node.isArray()) { + options = objectMapper.convertValue(node, new TypeReference<>() { + }); + } else if (node.has("options")) { + options = objectMapper.convertValue(node.get("options"), new TypeReference<>() { + }); + } + dto.setOptions(options == null || options.isEmpty() ? null : options); + } else { + dto.setOptions(null); + } - return dto; - }) - .sorted(Comparator.comparing(FieldResponseDTO::getId)) - .collect(Collectors.toList()); + // Handle validation + if (field.getValidation() != null && !field.getValidation().isBlank()) { + Map validation = objectMapper.readValue(field.getValidation(), new TypeReference<>() { + }); + dto.setValidation(validation.isEmpty() ? null : validation); + } else { + dto.setValidation(null); + } + // Handle conditional + if (field.getConditional() != null && !field.getConditional().isBlank()) { + Map conditional = objectMapper.readValue(field.getConditional(), new TypeReference<>() { + }); + dto.setConditional(conditional.isEmpty() ? null : conditional); + } else { + dto.setConditional(null); + } + } catch (Exception e) { - GroupedFieldResponseDTO singleSection = new GroupedFieldResponseDTO(); - singleSection.setSectionTitle(singleSection.getSectionTitle()); // your custom section title - singleSection.setFields(fieldDtos); + System.err.println("JSON Parsing Error in field: " + field.getFieldId()); + throw new RuntimeException("Failed to parse JSON for field: " + field.getFieldId(), e); + } - FormResponseDTO response = new FormResponseDTO(); - response.setVersion(form.getVersion()); - response.setFormId(form.getFormId()); - response.setFormName(form.getFormName()); - response.setSections(List.of(singleSection)); + return dto; + }) + .sorted(Comparator.comparing(FieldResponseDTO::getId)) + .collect(Collectors.toList()); + + + GroupedFieldResponseDTO singleSection = new GroupedFieldResponseDTO(); + singleSection.setFields(fieldDtos); + singleSection.setSectionTitle( + Objects.requireNonNullElse(singleSection.getSectionTitle(), "Section Title") + ); + FormResponseDTO response = new FormResponseDTO(); + response.setVersion(form.getVersion()); + response.setFormId(form.getFormId()); + response.setFormName(form.getFormName()); + response.setSections(List.of(singleSection)); + return response; + + } catch (Exception e) { + logger.error("Exception while building form response", e); + throw new RuntimeException("Failed to build form structure"); + } - return response; } diff --git a/src/main/java/com/iemr/common/service/health/HealthService.java b/src/main/java/com/iemr/common/service/health/HealthService.java new file mode 100644 index 00000000..7714efce --- /dev/null +++ b/src/main/java/com/iemr/common/service/health/HealthService.java @@ -0,0 +1,423 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.common.service.health; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.stereotype.Service; + +import jakarta.annotation.PreDestroy; +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.Statement; +import java.time.Instant; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +@Service +public class HealthService { + + private static final Logger logger = LoggerFactory.getLogger(HealthService.class); + + // Event log constants + private static final String LOG_EVENT_STUCK_PROCESS = "MYSQL_STUCK_PROCESS"; + private static final String LOG_EVENT_LOCK_WAIT = "MYSQL_LOCK_WAIT"; + private static final String LOG_EVENT_DEADLOCK = "MYSQL_DEADLOCK"; + private static final String LOG_EVENT_SLOW_QUERIES = "MYSQL_SLOW_QUERIES"; + private static final String LOG_EVENT_CONN_USAGE = "MYSQL_CONNECTION_USAGE"; + private static final String LOG_EVENT_POOL_EXHAUSTED = "MYSQL_POOL_EXHAUSTED"; + + // Response field constants + private static final String FIELD_STATUS = "status"; + private static final String FIELD_SEVERITY = "severity"; + private static final String FIELD_MYSQL = "mysql"; + private static final String FIELD_REDIS = "redis"; + private static final String FIELD_CHECKED_AT = "checkedAt"; + + // Severity constants + private static final String SEVERITY_CRITICAL = "CRITICAL"; + private static final String SEVERITY_WARNING = "WARNING"; + private static final String SEVERITY_OK = "OK"; + private static final String SEVERITY_INFO = "INFO"; + + // Database query constants + private static final String STATUS_VALUE = "Value"; + private static final String STATUS_UP = "UP"; + private static final String STATUS_DOWN = "DOWN"; + private static final String STATUS_DEGRADED = "DEGRADED"; + private static final String STATUS_NOT_CONFIGURED = "NOT_CONFIGURED"; + + // Thresholds + private static final long RESPONSE_TIME_SLOW_MS = 2000; // > 2s → SLOW + private static final int STUCK_PROCESS_THRESHOLD = 5; // > 5 stuck → WARNING + private static final int STUCK_PROCESS_SECONDS = 30; // process age in seconds + private static final int LONG_TXN_WARNING_THRESHOLD = 1; // ≥1 long txn → WARNING + private static final int LONG_TXN_CRITICAL_THRESHOLD = 5; // ≥5 long txns → CRITICAL + private static final int LONG_TXN_SECONDS = 60; // transaction age threshold + private static final int CONNECTION_USAGE_WARNING = 80; // > 80% → WARNING + private static final int CONNECTION_USAGE_CRITICAL= 95; // > 95% → CRITICAL + private static final long DIAGNOSTIC_INTERVAL_SEC = 30; // background run interval + private static final long DIAGNOSTIC_GUARD_SEC = 25; // safety dedup guard + private final DataSource dataSource; + private final RedisConnectionFactory redisConnectionFactory; + + private final ScheduledExecutorService diagnosticScheduler = + Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "mysql-diagnostic-thread"); + t.setDaemon(true); + return t; + }); + + private final AtomicLong lastDiagnosticRunAt = new AtomicLong(0); + private final AtomicReference cachedDbSeverity = + new AtomicReference<>(SEVERITY_OK); + private final AtomicLong previousDeadlockCount = new AtomicLong(0); + private final AtomicLong previousSlowQueryCount = new AtomicLong(0); + public HealthService(ObjectProvider dataSourceProvider, + ObjectProvider redisProvider) { + this.dataSource = dataSourceProvider.getIfAvailable(); + this.redisConnectionFactory = redisProvider.getIfAvailable(); + + // Start background diagnostics only if DB is configured. + // Initial delay = 0 so the first run happens at startup. + if (this.dataSource != null) { + diagnosticScheduler.scheduleAtFixedRate( + this::runAdvancedMySQLDiagnostics, + 0, + DIAGNOSTIC_INTERVAL_SEC, + TimeUnit.SECONDS + ); + } + } + + @PreDestroy + public void shutdownDiagnostics() { + logger.info("[HEALTH_SERVICE_SHUTDOWN] Shutting down diagnostic scheduler..."); + diagnosticScheduler.shutdown(); + try { + if (!diagnosticScheduler.awaitTermination(5, TimeUnit.SECONDS)) { + logger.warn("[HEALTH_SERVICE_SHUTDOWN] Diagnostic scheduler did not terminate gracefully"); + diagnosticScheduler.shutdownNow(); + } + logger.info("[HEALTH_SERVICE_SHUTDOWN] Diagnostic scheduler shut down successfully"); + } catch (InterruptedException e) { + logger.error("[HEALTH_SERVICE_SHUTDOWN] Interrupted while shutting down scheduler", e); + diagnosticScheduler.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + + // PUBLIC — Called by the /health controller + public Map checkHealth() { + Map response = new LinkedHashMap<>(); + + Map mysqlResult = checkDatabaseConnectivity(); + Map redisResult = checkRedisConnectivity(); + + String mysqlStatus = (String) mysqlResult.get(FIELD_STATUS); + String redisStatus = (String) redisResult.get(FIELD_STATUS); + + boolean overallUp = !STATUS_DOWN.equals(mysqlStatus) && !STATUS_DOWN.equals(redisStatus); + + response.put(FIELD_STATUS, overallUp ? STATUS_UP : STATUS_DOWN); + response.put(FIELD_CHECKED_AT, Instant.now().toString()); + + // Expose only status and severity, keep diagnostics internal + Map mysqlSummary = new LinkedHashMap<>(); + mysqlSummary.put(FIELD_STATUS, mysqlResult.get(FIELD_STATUS)); + mysqlSummary.put(FIELD_SEVERITY, mysqlResult.get(FIELD_SEVERITY)); + + Map redisSummary = new LinkedHashMap<>(); + redisSummary.put(FIELD_STATUS, redisResult.get(FIELD_STATUS)); + redisSummary.put(FIELD_SEVERITY, redisResult.get(FIELD_SEVERITY)); + + response.put(FIELD_MYSQL, mysqlSummary); + response.put(FIELD_REDIS, redisSummary); + + return response; + } + // Runs only SELECT 1 with a hard 3-second timeout on query execution. + // NOTE: getConnection() is NOT bounded by this timeout — it respects the pool's + // connectionTimeout (default 30s in HikariCP). For true 3-second /health guarantees, + // configure the DataSource connectionTimeout ≤ 3 seconds or wrap in an ExecutorService timeout. + private Map checkDatabaseConnectivity() { + Map result = new LinkedHashMap<>(); + + if (dataSource == null) { + result.put(FIELD_STATUS, STATUS_NOT_CONFIGURED); + result.put(FIELD_SEVERITY, SEVERITY_INFO); + return result; + } + + try (Connection conn = dataSource.getConnection(); + Statement stmt = conn.createStatement()) { + + stmt.setQueryTimeout(3); // Bounds only the SELECT 1 execution + stmt.execute("SELECT 1"); + + // If SELECT 1 succeeds, use cached severity from background diagnostics + String severity = cachedDbSeverity.get(); + result.put(FIELD_STATUS, resolveDatabaseStatus(severity)); + result.put(FIELD_SEVERITY, severity); + + } catch (Exception e) { + // Log connection failure as a structured event + logger.error( + "[MYSQL_CONNECT_FAILED] MySQL connectivity check failed | error=\"{}\"", + e.getMessage() + ); + + result.put(FIELD_STATUS, STATUS_DOWN); + result.put(FIELD_SEVERITY, SEVERITY_CRITICAL); + } + + return result; + } + + private Map checkRedisConnectivity() { + Map result = new LinkedHashMap<>(); + + if (redisConnectionFactory == null) { + result.put(FIELD_STATUS, STATUS_NOT_CONFIGURED); + result.put(FIELD_SEVERITY, SEVERITY_INFO); + return result; + } + + try (RedisConnection conn = redisConnectionFactory.getConnection()) { + conn.ping(); + result.put(FIELD_STATUS, STATUS_UP); + result.put(FIELD_SEVERITY, SEVERITY_OK); + + } catch (Exception e) { + logger.error( + "[REDIS_CONNECT_FAILED] Redis connectivity check failed | error=\"{}\"", + e.getMessage() + ); + + result.put(FIELD_STATUS, STATUS_DOWN); + result.put(FIELD_SEVERITY, SEVERITY_CRITICAL); + } + + return result; + } + + private void runAdvancedMySQLDiagnostics() { + // Dedup guard: skip if last run was within the past 25 seconds + long now = System.currentTimeMillis(); + if (now - lastDiagnosticRunAt.get() < TimeUnit.SECONDS.toMillis(DIAGNOSTIC_GUARD_SEC)) { + return; + } + lastDiagnosticRunAt.set(now); + + String worstSeverity = SEVERITY_OK; + + try (Connection conn = dataSource.getConnection()) { + worstSeverity = escalate(worstSeverity, performStuckProcessCheck(conn)); + worstSeverity = escalate(worstSeverity, performLongTransactionCheck(conn)); + worstSeverity = escalate(worstSeverity, performDeadlockCheck(conn)); + worstSeverity = escalate(worstSeverity, performSlowQueryCheck(conn)); + worstSeverity = escalate(worstSeverity, performConnectionUsageCheck(conn)); + + } catch (Exception e) { + logger.error( + "[MYSQL_DIAGNOSTIC_ERROR] Could not open connection for diagnostics | error=\"{}\"", + e.getMessage() + ); + worstSeverity = SEVERITY_CRITICAL; + } + + cachedDbSeverity.set(worstSeverity); + logger.debug( + "[MYSQL_DIAGNOSTIC_COMPLETE] Background diagnostic cycle complete | severity={}", + worstSeverity + ); + } + + private String performStuckProcessCheck(Connection conn) { + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT COUNT(*) AS cnt FROM information_schema.PROCESSLIST " + + "WHERE TIME > " + STUCK_PROCESS_SECONDS + " AND COMMAND != 'Sleep'")) { + + if (rs.next()) { + int stuckCount = rs.getInt("cnt"); + if (stuckCount > 0) { + if (stuckCount > STUCK_PROCESS_THRESHOLD) { + logger.warn( + "[{}] Stuck MySQL processes detected above threshold | count={} | threshold={} | thresholdSeconds={}", + LOG_EVENT_STUCK_PROCESS, stuckCount, STUCK_PROCESS_THRESHOLD, STUCK_PROCESS_SECONDS + ); + return SEVERITY_WARNING; + } else { + logger.info( + "[{}] Stuck MySQL processes below threshold | count={} | threshold={} | thresholdSeconds={}", + LOG_EVENT_STUCK_PROCESS, stuckCount, STUCK_PROCESS_THRESHOLD, STUCK_PROCESS_SECONDS + ); + } + } + } + } catch (Exception e) { + logger.error("[MYSQL_DIAGNOSTIC_ERROR] Stuck process check failed | error=\"{}\"", + e.getMessage()); + } + return SEVERITY_OK; + } + + private String performLongTransactionCheck(Connection conn) { + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT COUNT(*) AS cnt FROM information_schema.INNODB_TRX " + + "WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > " + LONG_TXN_SECONDS)) { + + if (rs.next()) { + int lockCount = rs.getInt("cnt"); + if (lockCount >= LONG_TXN_WARNING_THRESHOLD) { + logger.warn( + "[{}] InnoDB long-running transaction(s) detected | count={} | thresholdSeconds={}", + LOG_EVENT_LOCK_WAIT, lockCount, LONG_TXN_SECONDS + ); + // Graduated escalation: WARNING for 1-4, CRITICAL for 5+ + return lockCount >= LONG_TXN_CRITICAL_THRESHOLD + ? SEVERITY_CRITICAL : SEVERITY_WARNING; + } + } + } catch (Exception e) { + logger.error("[MYSQL_DIAGNOSTIC_ERROR] Long transaction check failed | error=\"{}\"", + e.getMessage()); + } + return SEVERITY_OK; + } + + private String performDeadlockCheck(Connection conn) { + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SHOW STATUS LIKE 'Innodb_deadlocks'")) { + + if (rs.next()) { + long currentDeadlocks = rs.getLong(STATUS_VALUE); + long previousDeadlocks = previousDeadlockCount.getAndSet(currentDeadlocks); + + if (currentDeadlocks > previousDeadlocks) { + long deltaDeadlocks = currentDeadlocks - previousDeadlocks; + logger.warn( + "[{}] InnoDB deadlocks detected since last run | deltaCount={} | cumulativeCount={}", + LOG_EVENT_DEADLOCK, deltaDeadlocks, currentDeadlocks + ); + return SEVERITY_WARNING; + } + } + } catch (Exception e) { + logger.error("[MYSQL_DIAGNOSTIC_ERROR] Deadlock check failed | error=\"{}\"", + e.getMessage()); + } + return SEVERITY_OK; + } + + private String performSlowQueryCheck(Connection conn) { + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SHOW STATUS LIKE 'Slow_queries'")) { + + if (rs.next()) { + long slowQueries = rs.getLong(STATUS_VALUE); + long previousSlow = previousSlowQueryCount.getAndSet(slowQueries); + + // Only warn if slow queries have *increased* since last run + if (slowQueries > previousSlow) { + long delta = slowQueries - previousSlow; + logger.warn( + "[{}] New slow queries detected since last run | deltaCount={} | cumulativeCount={}", + LOG_EVENT_SLOW_QUERIES, delta, slowQueries + ); + return SEVERITY_WARNING; + } + } + } catch (Exception e) { + logger.error("[MYSQL_DIAGNOSTIC_ERROR] Slow query check failed | error=\"{}\"", + e.getMessage()); + } + return SEVERITY_OK; + } + + private String performConnectionUsageCheck(Connection conn) { + try (Statement stmt = conn.createStatement()) { + int threadsConnected = 0; + int maxConnections = 0; + + try (ResultSet rs = stmt.executeQuery("SHOW STATUS LIKE 'Threads_connected'")) { + if (rs.next()) threadsConnected = rs.getInt(STATUS_VALUE); + } + + try (ResultSet rs = stmt.executeQuery("SHOW VARIABLES LIKE 'max_connections'")) { + if (rs.next()) maxConnections = rs.getInt(STATUS_VALUE); + } + + if (maxConnections > 0) { + int usagePct = (int) ((threadsConnected * 100.0) / maxConnections); + + if (usagePct >= CONNECTION_USAGE_CRITICAL) { + logger.error( + "[{}] MySQL connection pool near exhaustion | threadsConnected={} | maxConnections={} | usagePercent={}", + LOG_EVENT_POOL_EXHAUSTED, threadsConnected, maxConnections, usagePct + ); + return SEVERITY_CRITICAL; + + } else if (usagePct > CONNECTION_USAGE_WARNING) { + logger.warn( + "[{}] MySQL connection usage is high | threadsConnected={} | maxConnections={} | usagePercent={}", + LOG_EVENT_CONN_USAGE, threadsConnected, maxConnections, usagePct + ); + return SEVERITY_WARNING; + } + } + } catch (Exception e) { + logger.error("[MYSQL_DIAGNOSTIC_ERROR] Connection usage check failed | error=\"{}\"", + e.getMessage()); + } + return SEVERITY_OK; + } + private String resolveDatabaseStatus(String severity) { + return switch (severity) { + case SEVERITY_CRITICAL -> STATUS_DOWN; + case SEVERITY_WARNING -> STATUS_DEGRADED; + default -> STATUS_UP; + }; + } + private String escalate(String current, String candidate) { + return severityRank(candidate) > severityRank(current) ? candidate : current; + } + + private int severityRank(String severity) { + return switch (severity) { + case SEVERITY_CRITICAL -> 2; + case SEVERITY_WARNING -> 1; + default -> 0; + }; + } +} \ No newline at end of file diff --git a/src/main/java/com/iemr/common/service/kmfilemanager/KMFileManagerServiceImpl.java b/src/main/java/com/iemr/common/service/kmfilemanager/KMFileManagerServiceImpl.java index 4297022a..43c5c1f2 100644 --- a/src/main/java/com/iemr/common/service/kmfilemanager/KMFileManagerServiceImpl.java +++ b/src/main/java/com/iemr/common/service/kmfilemanager/KMFileManagerServiceImpl.java @@ -89,6 +89,9 @@ public void setSubCategoryRepository(SubCategoryRepository subCategoryRepository @Value("${allowed.file.extensions}") private String allowedFileExtensions; + @Value("${tempFilePath}") + private String tempFilePath; + @Override public String getKMFileLists(String request) throws Exception { ObjectMapper objectMapper = new ObjectMapper(); @@ -183,7 +186,6 @@ private ArrayList addKMFile(Iterable kmFileManager .replace("}", "").replace("[", "").replace("]", "").replace("|", "").replace("\\", "") .replace(":", "").replace(";", "").replace("-", "").replace("_", "").replace("+", "") .replace("=", "").replace("\"", "").replace("'", "")); - String tempFilePath = ConfigProperties.getPropertyByName("tempFilePath"); newFile = new FileOutputStream(tempFilePath + "/" + kmFileManager.getFileName()); newFile.write(Base64.getDecoder().decode(kmFileManager.getFileContent())); newFile.flush(); diff --git a/src/main/java/com/iemr/common/service/otp/OtpRateLimiterService.java b/src/main/java/com/iemr/common/service/otp/OtpRateLimiterService.java new file mode 100644 index 00000000..da06a64b --- /dev/null +++ b/src/main/java/com/iemr/common/service/otp/OtpRateLimiterService.java @@ -0,0 +1,104 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.common.service.otp; + +import com.iemr.common.exception.OtpRateLimitException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.concurrent.TimeUnit; + +/** + * Rate-limits OTP send/resend requests per mobile number using Redis counters. + * + * Limits (configurable via properties): + * otp.ratelimit.minute-limit – max OTPs per minute (default 3) + * otp.ratelimit.hour-limit – max OTPs per hour (default 10) + * otp.ratelimit.day-limit – max OTPs per day (default 20) + * + * Redis key pattern: + * rl:otp:min:{mobNo}:{minuteSlot} TTL 60 s + * rl:otp:hr:{mobNo}:{hourSlot} TTL 3600 s + * rl:otp:day:{mobNo}:{yyyyMMdd} TTL 86400 s + */ +@Component +public class OtpRateLimiterService { + + private final StringRedisTemplate redis; + + @Value("${otp.ratelimit.enabled:true}") + private boolean enabled; + + @Value("${otp.ratelimit.minute-limit:3}") + private int minuteLimit; + + @Value("${otp.ratelimit.hour-limit:10}") + private int hourLimit; + + @Value("${otp.ratelimit.day-limit:20}") + private int dayLimit; + + public OtpRateLimiterService(StringRedisTemplate redis) { + this.redis = redis; + } + + /** + * Checks all three rate-limit windows for the given mobile number. + * Throws {@link OtpRateLimitException} if any limit is exceeded. + * No-op when otp.ratelimit.enabled=false. + */ + public void checkRateLimit(String mobNo) { + if (!enabled) return; + String today = LocalDate.now(ZoneId.of("Asia/Kolkata")) + .toString().replaceAll("-", ""); // yyyyMMdd + long minuteSlot = System.currentTimeMillis() / 60_000L; + long hourSlot = System.currentTimeMillis() / 3_600_000L; + + String minKey = "rl:otp:min:" + mobNo + ":" + minuteSlot; + String hourKey = "rl:otp:hr:" + mobNo + ":" + hourSlot; + String dayKey = "rl:otp:day:" + mobNo + ":" + today; + + if (incrementWithExpire(minKey, 60L) > minuteLimit) { + throw new OtpRateLimitException( + "OTP request limit exceeded. Maximum " + minuteLimit + " OTPs allowed per minute. Please try again later."); + } + if (incrementWithExpire(hourKey, 3600L) > hourLimit) { + throw new OtpRateLimitException( + "OTP request limit exceeded. Maximum " + hourLimit + " OTPs allowed per hour. Please try again later."); + } + if (incrementWithExpire(dayKey, 86400L) > dayLimit) { + throw new OtpRateLimitException( + "OTP request limit exceeded. Maximum " + dayLimit + " OTPs allowed per day. Please try again tomorrow."); + } + } + + private long incrementWithExpire(String key, long ttlSeconds) { + Long value = redis.opsForValue().increment(key, 1L); + if (value != null && value == 1L) { + redis.expire(key, ttlSeconds, TimeUnit.SECONDS); + } + return value == null ? 0L : value; + } +} diff --git a/src/main/java/com/iemr/common/service/reportSecondary/SecondaryReportService.java b/src/main/java/com/iemr/common/service/reportSecondary/SecondaryReportService.java index a6ddfbfe..c6ebe089 100644 --- a/src/main/java/com/iemr/common/service/reportSecondary/SecondaryReportService.java +++ b/src/main/java/com/iemr/common/service/reportSecondary/SecondaryReportService.java @@ -19,6 +19,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ + package com.iemr.common.service.reportSecondary; import java.io.ByteArrayInputStream; diff --git a/src/main/java/com/iemr/common/service/reportSecondary/SecondaryReportServiceImpl.java b/src/main/java/com/iemr/common/service/reportSecondary/SecondaryReportServiceImpl.java index 4107eb13..81a18611 100644 --- a/src/main/java/com/iemr/common/service/reportSecondary/SecondaryReportServiceImpl.java +++ b/src/main/java/com/iemr/common/service/reportSecondary/SecondaryReportServiceImpl.java @@ -35,6 +35,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; import com.iemr.common.data.callhandling.CallType; @@ -53,6 +54,7 @@ import com.iemr.common.utils.mapper.InputMapper; +@Profile("!swagger") @Service public class SecondaryReportServiceImpl implements SecondaryReportService { private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); diff --git a/src/main/java/com/iemr/common/service/scheme/SchemeServiceImpl.java b/src/main/java/com/iemr/common/service/scheme/SchemeServiceImpl.java index 44e1efaa..ada35dd4 100644 --- a/src/main/java/com/iemr/common/service/scheme/SchemeServiceImpl.java +++ b/src/main/java/com/iemr/common/service/scheme/SchemeServiceImpl.java @@ -30,6 +30,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import com.fasterxml.jackson.databind.DeserializationFeature; @@ -59,6 +60,18 @@ public class SchemeServiceImpl implements SchemeService { private KMFileManagerService kmFileManagerService; + @Value("${km-api-base-protocol}") + private String dmsProtocol; + + @Value("${km-api-base-url}") + private String dmsPath; + + @Value("${km-guest-user}") + private String userName; + + @Value("${km-guest-password}") + private String userPassword; + @Autowired public void setKmFileManagerService(KMFileManagerService kmFileManagerService) { this.kmFileManagerService = kmFileManagerService; @@ -104,16 +117,16 @@ public String getFilePath(KMFileManager kmFileManager) { String fileUIDAsURI = null; if (kmFileManager != null && kmFileManager.getFileUID() != null) { String fileUID = kmFileManager.getFileUID(); - String dmsPath = ConfigProperties.getPropertyByName("km-base-path"); - String dmsProtocol = ConfigProperties.getPropertyByName("km-base-protocol"); - String userName = ConfigProperties.getPropertyByName("km-guest-user"); - String userPassword = ConfigProperties.getPassword("km-guest-user"); + // String dmsPath = ConfigProperties.getPropertyByName("km-base-path"); + // String dmsProtocol = ConfigProperties.getPropertyByName("km-base-protocol"); + // String userName = ConfigProperties.getPropertyByName("km-guest-user"); + // String userPassword = ConfigProperties.getPassword("km-guest-user"); fileUIDAsURI = dmsProtocol + "://" + userName + ":" + userPassword + "@" + dmsPath + "/Download?uuid=" + fileUID; } - // return fileUIDAsURI; - String message = kmFileManager.getFileUID() ; - return message; + return fileUIDAsURI; + // String message = kmFileManager.getFileUID() ; + // return message; } @Override diff --git a/src/main/java/com/iemr/common/service/services/CommonServiceImpl.java b/src/main/java/com/iemr/common/service/services/CommonServiceImpl.java index ff6f83e9..cd05dbde 100644 --- a/src/main/java/com/iemr/common/service/services/CommonServiceImpl.java +++ b/src/main/java/com/iemr/common/service/services/CommonServiceImpl.java @@ -64,7 +64,18 @@ public class CommonServiceImpl implements CommonService { private Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName()); + @Value("${km-base-path}") + private String dmsPath; + @Value("${km-guest-user}") + private String userName; + + @Value("${km-guest-password}") + private String userPassword; + + @Value("${km-base-protocol}") + private String dmsProtocol; + private static final String FILE_PATH = "filePath"; /** @@ -177,13 +188,13 @@ private String getFilePath(String fileUID) { String fileUIDAsURI = null; - String dmsPath = ConfigProperties.getPropertyByName("km-base-path"); - String dmsProtocol = ConfigProperties.getPropertyByName("km-base-protocol"); - String userName = ConfigProperties.getPropertyByName("km-guest-user"); - String userPassword = ConfigProperties.getPassword("km-guest-user"); + // String dmsPath = ConfigProperties.getPropertyByName("km-base-path"); + // String dmsProtocol = ConfigProperties.getPropertyByName("km-base-protocol"); + // String userName = ConfigProperties.getPropertyByName("km-guest-user"); + // String userPassword = ConfigProperties.getPassword("km-guest-user"); fileUIDAsURI = dmsProtocol + "://" + userName + ":" + userPassword + "@" + dmsPath + "/Download?uuid=" + fileUID; - + logger.info("file url="+fileUIDAsURI); return fileUIDAsURI; } diff --git a/src/main/java/com/iemr/common/service/sms/SMSServiceImpl.java b/src/main/java/com/iemr/common/service/sms/SMSServiceImpl.java index af9cbf1c..efe0d16a 100644 --- a/src/main/java/com/iemr/common/service/sms/SMSServiceImpl.java +++ b/src/main/java/com/iemr/common/service/sms/SMSServiceImpl.java @@ -68,6 +68,7 @@ import com.iemr.common.data.location.States; import com.iemr.common.data.mctshistory.MctsDataReaderDetail; import com.iemr.common.data.mctshistory.MctsOutboundCall; +import com.iemr.common.data.mmuDrugHistory.PrescribedMMUDrugDetail; import com.iemr.common.data.sms.SMSNotification; import com.iemr.common.data.sms.SMSParameters; import com.iemr.common.data.sms.SMSParametersMap; @@ -76,6 +77,7 @@ import com.iemr.common.data.telemedicine.PrescribedDrugDetail; import com.iemr.common.data.users.User; import com.iemr.common.data.videocall.VideoCallParameters; +import com.iemr.common.dto.sms.SMSTemplateDTO; import com.iemr.common.mapper.sms.SMSMapper; import com.iemr.common.model.beneficiary.BeneficiaryModel; import com.iemr.common.model.sms.CreateSMSRequest; @@ -95,6 +97,7 @@ import com.iemr.common.repository.location.LocationDistrictRepository; import com.iemr.common.repository.location.LocationStateRepository; import com.iemr.common.repository.mctshistory.OutboundHistoryRepository; +import com.iemr.common.repository.mmuDrugHistory.PrescribedMMUDrugRepository; import com.iemr.common.repository.sms.SMSNotificationRepository; import com.iemr.common.repository.sms.SMSParameterMapRepository; import com.iemr.common.repository.sms.SMSParameterRepository; @@ -164,6 +167,9 @@ public class SMSServiceImpl implements SMSService { @Autowired PrescribedDrugRepository prescribedDrugRepository; + @Autowired + PrescribedMMUDrugRepository prescribedMMUDrugRepository; + @Autowired OutboundHistoryRepository outboundHistoryRepository; @@ -218,10 +224,16 @@ public String saveSMSTemplate(CreateSMSRequest smsRequest) throws Exception { } SMSTemplate smsTemplate; SMSTemplate request = smsMapper.createRequestToSMSTemplate(smsRequest); - smsTemplate = smsTemplateRepository.save(request); - saveSMSParameterMaps(smsRequest, smsTemplate.getSmsTemplateID()); - smsTemplate = smsTemplateRepository.findBySmsTemplateID(smsTemplate.getSmsTemplateID()); - return OutputMapper.gsonWithoutExposeRestriction().toJson(smsMapper.smsTemplateToResponse(smsTemplate)); + SMSTemplate savedTemplate = smsTemplateRepository.save(request); + + saveSMSParameterMaps(smsRequest, savedTemplate.getSmsTemplateID()); + + + SMSTemplate fetchedTemplate = smsTemplateRepository.findBySmsTemplateID(savedTemplate.getSmsTemplateID()); + + SMSTemplateDTO responseDTO = smsMapper.smsTemplateToDTO(fetchedTemplate); + + return OutputMapper.gsonWithoutExposeRestriction().toJson(responseDTO); } /** @@ -267,7 +279,13 @@ private void saveSMSParameterMaps(CreateSMSRequest smsRequest, Integer smsTempla List smsParameterMapModels = smsRequest.getSmsParameterMaps(); for (SMSParameterMapModel smsParameterMapModel : smsParameterMapModels) { smsParameterMapModel.setSmsTemplateID(smsTemplateID); - smsParameterMapRepository.save(smsMapper.smsParameterMapModelToSMSParametersMap(smsParameterMapModel)); + SMSParametersMap entity = smsMapper.smsParameterMapModelToSMSParametersMap(smsParameterMapModel); + + if (entity.getCreatedBy() == null) { + entity.setCreatedBy(smsRequest.getCreatedBy()); + } + + SMSParametersMap savedEntity = smsParameterMapRepository.save(entity); } } @@ -684,61 +702,64 @@ private SMSNotification prepareSMS( String methodName = smsParametersMap.getSmsParameter().getDataName(); // DataVariableName String variableValue = ""; switch (paramType) { - case "Beneficiary": - variableValue = getBeneficiaryData(className, methodName, request, beneficiary); - benID = variableValue; - break; - case "Institute": - if (request.getIs1097() == true) { - variableValue = getInstituteData(className, methodName, request, authToken); - } else { - variableValue = getDirectoryserviceData(className, methodName, request); - } - break; - case "User": - variableValue = getUserData(className, methodName, request, authToken); - break; - case "Feedback": - variableValue = getFeedbackData(className, methodName, request, authToken); - break; - case "Prescription": - variableValue = getPrescriptionData(className, methodName, request, beneficiary); - break; - case "Blood on Call": - variableValue = getBloodOnCallData(className, methodName, request, beneficiary); - break; - case "Food Safety Complaint": - variableValue = getFoodSafetyComplaintData(className, methodName, request); - break; - case "Epidemic Outbreak Complaint": - variableValue = getEpidemicComplaintData(className, methodName, request); - break; - case "Grievance Tracking": - variableValue = getGrievanceData(className, methodName, request, authToken, beneficiary); - break; - case "MCTS Call Alert": - variableValue = getMCTSCallAlertData(className, methodName, request); - break; - case "Organ Donation": - variableValue = getOrganDonationData(className, methodName, request); - break; - case "TM Schedule": - variableValue = getSpecializationAndTcDateInfo(className, methodName, request); - break; - case "COVID-19": - variableValue = getCOVIDData(className, methodName, request); - break; - case "IMRMMR": - variableValue = getIMRMMRData(className, methodName, request); - break; - case "104 appointment": - variableValue = getUptsuData(className, methodName, request); - break; - case "Grievance": - variableValue = getGrievanceData(className, methodName, request, authToken, beneficiary); - break; - default: - break; + case "Beneficiary": + variableValue = getBeneficiaryData(className, methodName, request, beneficiary); + benID = variableValue; + break; + case "Institute": + if (request.getIs1097() == true) { + variableValue = getInstituteData(className, methodName, request, authToken); + } else { + variableValue = getDirectoryserviceData(className, methodName, request); + } + break; + case "User": + variableValue = getUserData(className, methodName, request, authToken); + break; + case "Feedback": + variableValue = getFeedbackData(className, methodName, request, authToken); + break; + case "Prescription": + variableValue = getPrescriptionData(className, methodName, request, beneficiary); + break; + case "Blood on Call": + variableValue = getBloodOnCallData(className, methodName, request, beneficiary); + break; + case "Food Safety Complaint": + variableValue = getFoodSafetyComplaintData(className, methodName, request); + break; + case "Epidemic Outbreak Complaint": + variableValue = getEpidemicComplaintData(className, methodName, request); + break; + case "Grievance Tracking": + variableValue = getGrievanceData(className, methodName, request, authToken, beneficiary); + break; + case "MCTS Call Alert": + variableValue = getMCTSCallAlertData(className, methodName, request); + break; + case "Organ Donation": + variableValue = getOrganDonationData(className, methodName, request); + break; + case "TM Schedule": + variableValue = getSpecializationAndTcDateInfo(className, methodName, request); + break; + case "COVID-19": + variableValue = getCOVIDData(className, methodName, request); + break; + case "IMRMMR": + variableValue = getIMRMMRData(className, methodName, request); + break; + case "104 appointment": + variableValue = getUptsuData(className, methodName, request); + break; + case "Grievance": + variableValue = getGrievanceData(className, methodName, request, authToken, beneficiary); + break; + case "MMUPrescription": + variableValue = getMMUPrescriptionData(className, methodName, request, beneficiary); + break; + default: + break; } if (variable.equalsIgnoreCase("SMS_PHONE_NO")) { if (request.getIsBloodBankSMS() == true) { @@ -1304,6 +1325,108 @@ private String getEpidemicComplaintData(String className, String methodName, SMS return variableValue; } + private String getMMUPrescriptionData(String className, String methodName, SMSRequest request, + BeneficiaryModel beneficiary) throws NoSuchMethodException, SecurityException, IllegalAccessException, + IllegalArgumentException, InvocationTargetException, ClassNotFoundException { + PrescribedMMUDrugDetail prescribedMMUDrug = prescribedMMUDrugRepository + .findByPrescribedDrugID(request.getPrescribedDrugID()); + String variableValue = ""; + switch (methodName.toLowerCase()) { + case "diagnosis": + // Format: "Diagnosis, DrugName(DrugStrength)" + StringBuilder diagnosisBuilder = new StringBuilder(); + + // Add diagnosis + String diagnosis = prescribedMMUDrug.getPrescription() != null + && prescribedMMUDrug.getPrescription().getDiagnosisProvided() != null + ? prescribedMMUDrug.getPrescription().getDiagnosisProvided().trim() + : ""; + + if (!diagnosis.isEmpty()) { + diagnosisBuilder.append(diagnosis); + } + + // Add drug name with strength + String drugName = prescribedMMUDrug.getDrugName(); + String drugStrength = prescribedMMUDrug.getDrugStrength(); + + if (drugName != null && !drugName.trim().isEmpty()) { + if (diagnosisBuilder.length() > 0) { + diagnosisBuilder.append(", "); + } + diagnosisBuilder.append(drugName.trim()); + + if (drugStrength != null && !drugStrength.trim().isEmpty()) { + diagnosisBuilder.append(" (").append(drugStrength.trim()).append(")"); + } + } + + variableValue = diagnosisBuilder.toString(); + break; + case "dosage": + // Format: "duration unit, dose, frequency" + StringBuilder dosageBuilder = new StringBuilder(); + + // Add duration with unit + String duration = prescribedMMUDrug.getDuration(); + String unit = prescribedMMUDrug.getUnit(); + + if (duration != null && !duration.trim().isEmpty()) { + dosageBuilder.append(duration.trim()); + if (unit != null && !unit.trim().isEmpty()) { + dosageBuilder.append(" ").append(unit.trim()); + } + } + + // Add dose + String dose = prescribedMMUDrug.getDose(); + if (dose != null && !dose.trim().isEmpty()) { + if (dosageBuilder.length() > 0) { + dosageBuilder.append(", "); + } + dosageBuilder.append(dose.trim()); + } + + // Add frequency + String frequency = prescribedMMUDrug.getFrequency(); + if (frequency != null && !frequency.trim().isEmpty()) { + if (dosageBuilder.length() > 0) { + dosageBuilder.append(", "); + } + dosageBuilder.append(frequency.trim()); + } + + variableValue = dosageBuilder.toString(); + break; + + case "by": + // Format: "route, By: CreatedBy" + StringBuilder doctorBuilder = new StringBuilder(); + + // Add route + String route = prescribedMMUDrug.getRoute(); + if (route != null && !route.trim().isEmpty()) { + doctorBuilder.append(route.trim()); + } + + // Add created by + String createdBy = prescribedMMUDrug.getCreatedBy(); + if (createdBy != null && !createdBy.trim().isEmpty()) { + if (doctorBuilder.length() > 0) { + doctorBuilder.append(", "); + } + doctorBuilder.append("By: ").append(createdBy.trim()); + } + + variableValue = doctorBuilder.toString(); + break; + default: + break; + } + return variableValue; + } + + private String getGrievanceData(String className, String methodName, SMSRequest request, String authToken, BeneficiaryModel beneficiary) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { diff --git a/src/main/java/com/iemr/common/utils/FilterConfig.java b/src/main/java/com/iemr/common/utils/FilterConfig.java index 42bd04ad..9144a296 100644 --- a/src/main/java/com/iemr/common/utils/FilterConfig.java +++ b/src/main/java/com/iemr/common/utils/FilterConfig.java @@ -33,6 +33,7 @@ import org.springframework.data.redis.core.StringRedisTemplate; @Configuration +@org.springframework.context.annotation.Profile("!swagger") public class FilterConfig { private static final Logger log = LoggerFactory.getLogger(FilterConfig.class); diff --git a/src/main/java/com/iemr/common/utils/IEMRApplBeans.java b/src/main/java/com/iemr/common/utils/IEMRApplBeans.java index 92d3c339..7747f6ee 100644 --- a/src/main/java/com/iemr/common/utils/IEMRApplBeans.java +++ b/src/main/java/com/iemr/common/utils/IEMRApplBeans.java @@ -40,12 +40,12 @@ @Configuration public class IEMRApplBeans { - @Bean - public KMService getOpenKMService() - { - KMService kmService = new OpenKMServiceImpl(); - return kmService; - } + // @Bean + // public KMService getOpenKMService() + // { + // KMService kmService = new OpenKMServiceImpl(); + // return kmService; + // } @Bean public Validator getVaidator() diff --git a/src/main/java/com/iemr/common/utils/JwtAuthenticationUtil.java b/src/main/java/com/iemr/common/utils/JwtAuthenticationUtil.java index 1e9f589d..381f64de 100644 --- a/src/main/java/com/iemr/common/utils/JwtAuthenticationUtil.java +++ b/src/main/java/com/iemr/common/utils/JwtAuthenticationUtil.java @@ -20,6 +20,7 @@ import jakarta.servlet.http.HttpServletRequest; @Component +@org.springframework.context.annotation.Profile("!swagger") public class JwtAuthenticationUtil { @Autowired diff --git a/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java b/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java index 81d79221..364aa12d 100644 --- a/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java +++ b/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java @@ -251,7 +251,9 @@ private boolean shouldSkipAuthentication(String path, String contextPath) { || path.startsWith(contextPath + "/user/userLogout") || path.startsWith(contextPath + "/user/validateSecurityQuestionAndAnswer") || path.startsWith(contextPath + "/user/logOutUserFromConcurrentSession") - || path.startsWith(contextPath + "/user/refreshToken"); + || path.startsWith(contextPath + "/user/refreshToken") + || path.equals(contextPath + "/health") + || path.equals(contextPath + "/version"); } private String getJwtTokenFromCookies(HttpServletRequest request) { diff --git a/src/main/java/com/iemr/common/utils/km/openkm/OpenKMServiceImpl.java b/src/main/java/com/iemr/common/utils/km/openkm/OpenKMServiceImpl.java index 2be04cfc..86a57dda 100644 --- a/src/main/java/com/iemr/common/utils/km/openkm/OpenKMServiceImpl.java +++ b/src/main/java/com/iemr/common/utils/km/openkm/OpenKMServiceImpl.java @@ -46,44 +46,47 @@ import com.openkm.sdk4j.exception.VirusDetectedException; import com.openkm.sdk4j.exception.WebserviceException; -import org.glassfish.jersey.client.ClientConfig; -import org.glassfish.jersey.client.ClientProperties; -import org.glassfish.jersey.client.JerseyClientBuilder; +import jakarta.annotation.PostConstruct; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Service; + +@Service public class OpenKMServiceImpl implements KMService { - // private ConfigProperties configProperties; - // - // @Autowired - // public void setConfigProperties(ConfigProperties configProperties) - // { - // this.configProperties = configProperties; - // } + private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); - private static String url; - private static String username; - private static String password; - private static String kmRootPath; - private static String guestUser; - private static String guestPassword; + @Value("${km-base-url}") + private String url; + + @Value("${km-username}") + private String username; + + @Value("${km-password}") + private String password; + + @Value("${km-root-path}") + private String kmRootPath; + + @Value("${km-guest-user}") + private String guestUser; + + @Value("${km-guest-password}") + private String guestPassword; public OpenKMServiceImpl() { } - private static OKMWebservices connector = null; + private OKMWebservices connector; + @PostConstruct public void init() { - if (connector == null) { - url = ConfigProperties.getPropertyByName("km-base-url"); - username = ConfigProperties.getPropertyByName("km-username"); - password = ConfigProperties.getPropertyByName("km-password"); - kmRootPath = ConfigProperties.getPropertyByName("km-root-path"); - guestUser = ConfigProperties.getPropertyByName("km-guest-user"); - guestPassword = ConfigProperties.getPropertyByName("km-guest-password"); - connector = OpenKMConnector.initialize(url, username, password); + logger.info("KM URL={}",url); + connector = OpenKMConnector.initialize(url, username, password); - } - } + } @Override public String getDocumentRoot() { diff --git a/src/main/java/common/iemr/common/secondary/data/report/SecondaryCallReport.java b/src/main/java/common/iemr/common/secondary/data/report/SecondaryCallReport.java index 66f2777d..6c68f26d 100644 --- a/src/main/java/common/iemr/common/secondary/data/report/SecondaryCallReport.java +++ b/src/main/java/common/iemr/common/secondary/data/report/SecondaryCallReport.java @@ -29,6 +29,7 @@ import com.iemr.common.utils.mapper.OutputMapper; import jakarta.persistence.Column; +import org.springframework.context.annotation.Profile; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -36,6 +37,7 @@ import jakarta.persistence.Table; import jakarta.persistence.Transient; +@Profile("!swagger") @Entity @Table(name = "fact_bencall", schema = "db_reporting") public class SecondaryCallReport implements Serializable diff --git a/src/main/resources/application-swagger.properties b/src/main/resources/application-swagger.properties new file mode 100644 index 00000000..f73e6fd2 --- /dev/null +++ b/src/main/resources/application-swagger.properties @@ -0,0 +1,32 @@ +cors.allowed-origins=${CORS_ALLOWED_ORIGINS:http://localhost:9090,http://localhost:8080} +# ---- Embedded DB for Swagger documentation generation +spring.datasource.url=jdbc:h2:mem:swaggerdb +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= + +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.jpa.hibernate.ddl-auto=none +spring.jpa.show-sql=false + +spring.sql.init.mode=never + +# Use placeholders for sensitive values +jwt.secret=JWT_SECRET +jwt.expiration=3600000 +sms-password= +sms-username= +start-grievancedatasync-scheduler=false +sms-consent-source-address= +send-message-url=http://localhost:8080/sms/sendMessage +secondary.datasource.username= +secondary.datasource.password= +secondary.datasource.url=jdbc:h2:mem:reportingdb +secondary.datasource.driver-class-name=org.h2.Driver + +springdoc.api-docs.enabled=true +springdoc.swagger-ui.enabled=true + +api.dev.url=${API_DEV_URL:https://amritwprdev.piramalswasthya.org} +api.uat.url=${API_UAT_URL:https://uatamrit.piramalswasthya.org} +api.demo.url=${API_DEMO_URL:https://amritdemo.piramalswasthya.org} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 5263ca41..fef088ff 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -27,8 +27,6 @@ BeneficiarySmsTemplate=Beneficiary UPTSU SMS ######Project specific settings future-days=7 -## Path where the files would be stored before uploading to KM server -tempFilePath=c:/temp/ ## swaasa file path lungAssessmentPath=c:/swaasa_audio/ @@ -373,6 +371,4 @@ video-call-url = allowed.file.extensions=msg,pdf,png,jpeg,doc,docx,xlsx,xls,csv,txt ##sms details for beneficiary otp cosent -sms-template-name = otp_consent - - +sms-template-name = otp_consent \ No newline at end of file