diff --git a/src/main/java/com/iemr/flw/controller/BadgeController.java b/src/main/java/com/iemr/flw/controller/BadgeController.java new file mode 100644 index 000000000..aa5fb4855 --- /dev/null +++ b/src/main/java/com/iemr/flw/controller/BadgeController.java @@ -0,0 +1,108 @@ +package com.iemr.flw.controller; + +import com.iemr.flw.dto.iemr.BadgeEarnedPushDTO; +import com.iemr.flw.service.BadgeService; +import com.iemr.flw.utils.JwtUtil; +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.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.HashMap; +import java.util.Map; + +/** + * Badges API consumed by the SAKHI app's BadgeSyncWorker: + * GET /badges/config -> { "config": { key: value } } + * GET /badges/freezes -> { "freezes": [ { badgeId, startDate, endDate } ] } + * GET /badges/earned -> { "earned": [ { badgeId, level, earnedAt } ] } + * POST /badges/earned <- { userId, badges: [ { badgeId, level, earnedAt } ] } + * The ASHA is always identified from the JWT, never from the payload. + */ +@RestController +@RequestMapping(value = "/badges", produces = "application/json") +public class BadgeController { + + private final Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName()); + private final BadgeService badgeService; + private final JwtUtil jwtUtil; + + public BadgeController(BadgeService badgeService, JwtUtil jwtUtil) { + this.badgeService = badgeService; + this.jwtUtil = jwtUtil; + } + + @GetMapping("/config") + public ResponseEntity> getConfig() { + Map response = ok(); + try { + response.put("config", badgeService.getConfig()); + return ResponseEntity.ok(response); + } catch (Exception e) { + return error(e); + } + } + + @GetMapping("/freezes") + public ResponseEntity> getFreezes( + @RequestHeader(value = "JwtToken") String jwtToken) { + Map response = ok(); + try { + Integer userId = jwtUtil.extractUserId(jwtToken); + response.put("freezes", badgeService.getFreezes(userId)); + return ResponseEntity.ok(response); + } catch (Exception e) { + return error(e); + } + } + + @GetMapping("/earned") + public ResponseEntity> getEarned( + @RequestHeader(value = "JwtToken") String jwtToken) { + Map response = ok(); + try { + Integer userId = jwtUtil.extractUserId(jwtToken); + response.put("earned", badgeService.getEarned(userId)); + return ResponseEntity.ok(response); + } catch (Exception e) { + return error(e); + } + } + + @PostMapping(value = "/earned", consumes = "application/json") + public ResponseEntity> postEarned( + @RequestHeader(value = "JwtToken") String jwtToken, + @RequestBody BadgeEarnedPushDTO body) { + Map response = ok(); + try { + Integer userId = jwtUtil.extractUserId(jwtToken); + int inserted = badgeService.saveEarned(userId, body == null ? null : body.getBadges()); + response.put("inserted", inserted); + return ResponseEntity.ok(response); + } catch (Exception e) { + return error(e); + } + } + + private Map ok() { + Map response = new HashMap<>(); + response.put("statusCode", 200); + response.put("status", "Success"); + return response; + } + + private ResponseEntity> error(Exception e) { + logger.error("Badges API error:", e); + Map response = new HashMap<>(); + response.put("statusCode", 500); + response.put("status", "Error"); + response.put("errorMessage", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } +} diff --git a/src/main/java/com/iemr/flw/controller/DiseaseControlController.java b/src/main/java/com/iemr/flw/controller/DiseaseControlController.java index 764ebeaf6..57b4e3b49 100644 --- a/src/main/java/com/iemr/flw/controller/DiseaseControlController.java +++ b/src/main/java/com/iemr/flw/controller/DiseaseControlController.java @@ -49,6 +49,9 @@ public class DiseaseControlController { @Autowired private DiseaseControlService diseaseControlService; + @Autowired + private ObjectMapper objectMapper; + @RequestMapping(value = "malaria/saveAll", method = RequestMethod.POST, consumes = "application/json", produces = "application/json",headers = "Authorization") public ResponseEntity> saveMalaria(@RequestBody MalariaDTO malariaDTO) { @@ -375,4 +378,72 @@ public ResponseEntity> getVisitDetails( } + @RequestMapping(value = "generalCheckUp/getAll", method = RequestMethod.POST) + public ResponseEntity> getGeneralCheckUp(@RequestBody GetBenRequestHandler getBenRequestHandler,@RequestHeader(value = "JwtToken") String token) { + + Map response = new LinkedHashMap<>(); + + try { + List result = + diseaseControlService.getGeneralCheckUp(token); + + if (result != null && !result.isEmpty()) { + response.put("statusCode", HttpStatus.OK.value()); + response.put("data", result); + return ResponseEntity.ok(response); + } else { + response.put("statusCode", HttpStatus.NOT_FOUND.value()); + response.put("message", "No records found"); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response); + } + + } catch (Exception e) { + logger.error("Error fetching Chronic Disease Visit :", e); + response.put("statusCode", HttpStatus.INTERNAL_SERVER_ERROR.value()); + response.put("errorMessage", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + + @RequestMapping(value = "generalCheckUp/saveAll", method = RequestMethod.POST) + public ResponseEntity> savGeneralCheckUp( + @RequestBody List requestList,@RequestHeader(value = "JwtToken") String token) { + + Map response = new LinkedHashMap<>(); + logger.info("####################################################"); + try { + logger.info("General Checkup Save Request: {}", + objectMapper.writerWithDefaultPrettyPrinter() + .writeValueAsString(requestList)); + } catch (Exception e) { + logger.error("Failed to convert request to JSON", e); + } + logger.info("####################################################"); + + try { + List savedList = + diseaseControlService.saveGeneralCheckUp(requestList,token); + + if (savedList != null && !savedList.isEmpty()) { + response.put("statusCode", HttpStatus.OK.value()); + response.put("message", "Data saved successfully"); + response.put("data", savedList); + return ResponseEntity.ok(response); + } else { + response.put("statusCode", HttpStatus.BAD_REQUEST.value()); + response.put("message", "No data saved"); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response); + } + + } catch (Exception e) { + logger.error("Error saving Chronic Disease Visit :", e); + response.put("statusCode", HttpStatus.INTERNAL_SERVER_ERROR.value()); + response.put("errorMessage", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + + } \ No newline at end of file diff --git a/src/main/java/com/iemr/flw/controller/GeneralCheckUpFollowUpController.java b/src/main/java/com/iemr/flw/controller/GeneralCheckUpFollowUpController.java new file mode 100644 index 000000000..ca450d670 --- /dev/null +++ b/src/main/java/com/iemr/flw/controller/GeneralCheckUpFollowUpController.java @@ -0,0 +1,121 @@ +/* +* 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 +*/ +package com.iemr.flw.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.iemr.flw.dto.identity.GetBenRequestHandler; +import com.iemr.flw.dto.iemr.*; +import com.iemr.flw.service.DiseaseControlService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping(value = "/generalCheckUpFollowUp",headers = "Authorization") +public class GeneralCheckUpFollowUpController { + private final Logger logger = LoggerFactory.getLogger(GeneralCheckUpFollowUpController.class); + + @Autowired + private DiseaseControlService diseaseControlService; + + @Autowired + private ObjectMapper objectMapper; + + + + @RequestMapping(value = "getAll", method = RequestMethod.POST) + public ResponseEntity> getGeneralCheckUpFollowUp(@RequestBody GetBenRequestHandler getBenRequestHandler,@RequestHeader(value = "JwtToken") String token) { + + Map response = new LinkedHashMap<>(); + + try { + List result = + diseaseControlService.getGeneralCheckUpFollowUp(token); + + if (result != null && !result.isEmpty()) { + response.put("statusCode", HttpStatus.OK.value()); + response.put("data", result); + return ResponseEntity.ok(response); + } else { + response.put("statusCode", HttpStatus.NOT_FOUND.value()); + response.put("message", "No records found"); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response); + } + + } catch (Exception e) { + logger.error("Error fetching Chronic Disease Visit :", e); + response.put("statusCode", HttpStatus.INTERNAL_SERVER_ERROR.value()); + response.put("errorMessage", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + + @RequestMapping(value = "saveAll", method = RequestMethod.POST) + public ResponseEntity> savGeneralCheckUpFollowUp( + @RequestBody List requestList,@RequestHeader(value = "JwtToken") String token) { + + Map response = new LinkedHashMap<>(); + logger.info("####################################################"); + try { + logger.info("General Checkup Save Request: {}", + objectMapper.writerWithDefaultPrettyPrinter() + .writeValueAsString(requestList)); + } catch (Exception e) { + logger.error("Failed to convert request to JSON", e); + } + logger.info("####################################################"); + + try { + List savedList = + diseaseControlService.saveGeneralCheckUpFollowUp(requestList,token); + + if (savedList != null && !savedList.isEmpty()) { + response.put("statusCode", HttpStatus.OK.value()); + response.put("message", "Data saved successfully"); + response.put("data", savedList); + return ResponseEntity.ok(response); + } else { + response.put("statusCode", HttpStatus.BAD_REQUEST.value()); + response.put("message", "No data saved"); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response); + } + + } catch (Exception e) { + logger.error("Error saving Chronic Disease Visit :", e); + response.put("statusCode", HttpStatus.INTERNAL_SERVER_ERROR.value()); + response.put("errorMessage", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + +} \ No newline at end of file diff --git a/src/main/java/com/iemr/flw/domain/iemr/AHDForm.java b/src/main/java/com/iemr/flw/domain/iemr/AHDForm.java index 808ba7827..f5565b3d6 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/AHDForm.java +++ b/src/main/java/com/iemr/flw/domain/iemr/AHDForm.java @@ -26,11 +26,13 @@ public class AHDForm { @Column(name = "ahd_date") private String ahdDate; + @Lob @Column(name = "image1") - private String image1; + private byte[] image1; + @Lob @Column(name = "image2") - private String image2; + private byte[] image2; @Column(name = "user_id") private Long userId; diff --git a/src/main/java/com/iemr/flw/domain/iemr/BadgeConfig.java b/src/main/java/com/iemr/flw/domain/iemr/BadgeConfig.java new file mode 100644 index 000000000..909871114 --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/BadgeConfig.java @@ -0,0 +1,28 @@ +package com.iemr.flw.domain.iemr; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Badge tuning as key/value rows (Badge LLD §5.2). Overrides the app's + * compiled defaults without a release, e.g. "milestones.steady_syncer" -> "2,4,6,8". + */ +@Entity +@Data +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "badge_config", schema = "db_iemr") +public class BadgeConfig { + + @Id + @Column(name = "config_key", length = 100) + private String key; + + @Column(name = "config_value", length = 500, nullable = false) + private String value; +} diff --git a/src/main/java/com/iemr/flw/domain/iemr/BadgeEarned.java b/src/main/java/com/iemr/flw/domain/iemr/BadgeEarned.java new file mode 100644 index 000000000..382113f94 --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/BadgeEarned.java @@ -0,0 +1,62 @@ +package com.iemr.flw.domain.iemr; + +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 jakarta.persistence.UniqueConstraint; +import lombok.Data; + +/** + * Append-only badge award log (Badge LLD §4.1, "once earned, never revoked"). + * The unique key mirrors the device's local constraint so re-uploads after a + * reinstall or a retried sync are idempotent. + * + * The key includes award_key because (user, badge, level) is not unique for every badge. + * Quarterly badges are re-earned each quarter at the same level, and per-case badges are + * earned once per beneficiary, so without it the second and later awards collapse into the + * first on upload and cannot be told apart on restore. + */ +@Entity +@Data +@Table(name = "badge_earned", schema = "db_iemr", + uniqueConstraints = @UniqueConstraint( + name = "uk_badge_earned_user_badge_level_key", + columnNames = {"user_id", "badge_id", "level", "award_key"})) +public class BadgeEarned { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "user_id", nullable = false) + private Integer userId; + + @Column(name = "badge_id", length = 50, nullable = false) + private String badgeId; + + @Column(name = "level", nullable = false) + private Integer level; + + /** + * What makes two awards of the same badge and level different: a quarter key such as + * "2026-Q3", or an opaque digest for per-case badges. Empty for streak and cumulative + * badges, which are earned once per level. + * + * Never a beneficiary identifier. The device hashes those before they are sent, so this + * column can separate two awards without the server learning who either was about + * (Badge LLD §4). + */ + @Column(name = "award_key", length = 64, nullable = false) + private String awardKey = ""; + + /** Epoch millis when the device awarded it. */ + @Column(name = "earned_at", nullable = false) + private Long earnedAt; + + /** Epoch millis when the server first received it. */ + @Column(name = "received_at", nullable = false) + private Long receivedAt; +} diff --git a/src/main/java/com/iemr/flw/domain/iemr/BadgeStreakFreeze.java b/src/main/java/com/iemr/flw/domain/iemr/BadgeStreakFreeze.java new file mode 100644 index 000000000..afc7a9870 --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/BadgeStreakFreeze.java @@ -0,0 +1,37 @@ +package com.iemr.flw.domain.iemr; + +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; + +/** + * Streak-freeze window (Badge LLD §3.3): illness / alternate duty. A null userId + * applies to every ASHA; a null or empty badgeId applies to every streak badge. + */ +@Entity +@Data +@Table(name = "badge_streak_freeze", schema = "db_iemr") +public class BadgeStreakFreeze { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "user_id") + private Integer userId; + + @Column(name = "badge_id", length = 50) + private String badgeId; + + /** Epoch millis, inclusive. */ + @Column(name = "start_date", nullable = false) + private Long startDate; + + /** Epoch millis, inclusive. */ + @Column(name = "end_date", nullable = false) + private Long endDate; +} diff --git a/src/main/java/com/iemr/flw/domain/iemr/DewormingForm.java b/src/main/java/com/iemr/flw/domain/iemr/DewormingForm.java index 1638e52e5..3df5156b2 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/DewormingForm.java +++ b/src/main/java/com/iemr/flw/domain/iemr/DewormingForm.java @@ -29,11 +29,13 @@ public class DewormingForm { @Column(name = "age_group") private Integer ageGroup; + @Lob @Column(name = "image1") - private String image1; + private byte[] image1; + @Lob @Column(name = "image2") - private String image2; + private byte[] image2; @Column(name = "user_id") private Long userId; diff --git a/src/main/java/com/iemr/flw/domain/iemr/DiagnosticOrder.java b/src/main/java/com/iemr/flw/domain/iemr/DiagnosticOrder.java index c6bf2695a..ae7dc52db 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/DiagnosticOrder.java +++ b/src/main/java/com/iemr/flw/domain/iemr/DiagnosticOrder.java @@ -53,7 +53,7 @@ public class DiagnosticOrder { @Column(name = "error_message", columnDefinition = "TEXT") private String errorMessage; - @Column(name = "reason_for_refusal", columnDefinition = "TEXT") + @Column(name = "reason_to_close", columnDefinition = "TEXT") private String reasonToClose; @Column(name = "push_response_json", columnDefinition = "LONGTEXT") diff --git a/src/main/java/com/iemr/flw/domain/iemr/GeneralCheckUp.java b/src/main/java/com/iemr/flw/domain/iemr/GeneralCheckUp.java new file mode 100644 index 000000000..ec9f9c64b --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/GeneralCheckUp.java @@ -0,0 +1,72 @@ +package com.iemr.flw.domain.iemr; + +import java.sql.Timestamp; +import java.time.LocalDate; + +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 +@Table(name = "general_check_up", schema = "db_iemr") +@Data +public class GeneralCheckUp { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column(name = "ben_id") + private Long benId; + + @Column(name = "household_id") + private Long householdId; + + @Column(name = "asha_id", nullable = false) + private Integer ashaId; + + @Column(name = "checkup_date", nullable = false) + private LocalDate checkupDate; + + @Column(name = "problem_type", length = 200) + private String problemType; + + @Column(name = "problems", columnDefinition = "json") + private String problems; + + @Column(name = "checkup_id", length = 50, unique = true) + private String checkupId; + + @Column(name = "problem_type_label", length = 200) + private String problemTypeLabel; + + @Column(name = "other_text", length = 500) + private String otherText; + + @Column(name = "priority", length = 20) + private String priority; + + + @Column(name = "referral_reason", length = 500) + private String referralReason; + + @Column(name = "referral_date") + private LocalDate referralDate; + + @Column(name = "created_by", length = 100) + private String createdBy; + + @Column(name = "created_date", insertable = false, updatable = false) + private Timestamp createdDate; + + @Column(name = "synced_date") + private Timestamp syncedDate; + + @Column(name = "updated_date", insertable = false, updatable = false) + private Timestamp updatedDate; +} \ No newline at end of file diff --git a/src/main/java/com/iemr/flw/domain/iemr/GeneralCheckUpFollowUp.java b/src/main/java/com/iemr/flw/domain/iemr/GeneralCheckUpFollowUp.java new file mode 100644 index 000000000..3457fdf4f --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/GeneralCheckUpFollowUp.java @@ -0,0 +1,54 @@ +package com.iemr.flw.domain.iemr; + +import jakarta.persistence.*; +import lombok.Data; + +import java.sql.Timestamp; +import java.time.LocalDate; + +@Entity +@Table(name = "general_checkup_followup", schema = "db_iemr") +@Data +public class GeneralCheckUpFollowUp { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column(name = "ben_id") + private Long benId; + + @Column(name = "household_id") + private Long householdId; + + @Column(name = "asha_id", nullable = false) + private Integer ashaId; + + @Column(name = "follow_up_date", nullable = false) + private LocalDate followUpDate; + + @Column(name = "followup_status", length = 200) + private String followupStatus; + + @Column(name = "referred_to", columnDefinition = "json") + private String referredTo; + + @Column(name = "created_by", length = 100) + private String createdBy; + + @Column(name = "created_date", insertable = false, updatable = false) + private Timestamp createdDate; + + @Column(name = "synced_date") + private Timestamp syncedDate; + + @Column(name = "updated_date", insertable = false, updatable = false) + private Timestamp updatedDate; + + @Column(name = "referralReason") + private String referralReason; + + @Column(name = "referredOnDate") + private LocalDate referredOnDate; +} \ No newline at end of file diff --git a/src/main/java/com/iemr/flw/domain/iemr/PHCReviewForm.java b/src/main/java/com/iemr/flw/domain/iemr/PHCReviewForm.java index 42665c94c..9670f08f1 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/PHCReviewForm.java +++ b/src/main/java/com/iemr/flw/domain/iemr/PHCReviewForm.java @@ -26,11 +26,13 @@ public class PHCReviewForm { @Column(name = "no_of_beneficiaries_attended") private Integer noOfBeneficiariesAttended; + @Lob @Column(name = "image1") - private String image1; + private byte[] image1; + @Lob @Column(name = "image2") - private String image2; + private byte[] image2; @Column(name = "user_id") private Long userId; diff --git a/src/main/java/com/iemr/flw/domain/iemr/VHNDForm.java b/src/main/java/com/iemr/flw/domain/iemr/VHNDForm.java index 2de5f6062..c940e8ea3 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/VHNDForm.java +++ b/src/main/java/com/iemr/flw/domain/iemr/VHNDForm.java @@ -28,11 +28,13 @@ public class VHNDForm { @Column(name = "no_of_beneficiaries_attended") private Integer noOfBeneficiariesAttended; + @Lob @Column(name = "image1") - private String image1; + private byte[] image1; + @Lob @Column(name = "image2") - private String image2; + private byte[] image2; @Column(name = "form_type") private String formType; diff --git a/src/main/java/com/iemr/flw/domain/iemr/VhncForm.java b/src/main/java/com/iemr/flw/domain/iemr/VhncForm.java index 858554daf..25bec9354 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/VhncForm.java +++ b/src/main/java/com/iemr/flw/domain/iemr/VhncForm.java @@ -25,12 +25,14 @@ public class VhncForm { @Column(name = "no_of_beneficiaries_attended") private Integer noOfBeneficiariesAttended; - + + @Lob @Column(name = "image1") - private String image1; - + private byte[] image1; + + @Lob @Column(name = "image2") - private String image2; + private byte[] image2; @Column(name = "user_id") private Long userId; diff --git a/src/main/java/com/iemr/flw/dto/iemr/AhDMeetingFormDTO.java b/src/main/java/com/iemr/flw/dto/iemr/AhDMeetingFormDTO.java index 0ecc31be8..fdb6b9bbf 100644 --- a/src/main/java/com/iemr/flw/dto/iemr/AhDMeetingFormDTO.java +++ b/src/main/java/com/iemr/flw/dto/iemr/AhDMeetingFormDTO.java @@ -8,6 +8,6 @@ public class AhDMeetingFormDTO { private String mobilizedForAHD; private String ahdPlace; private String ahdDate; - private String image1; - private String image2; + private byte[] image1; + private byte[] image2; } \ No newline at end of file diff --git a/src/main/java/com/iemr/flw/dto/iemr/BadgeEarnedDTO.java b/src/main/java/com/iemr/flw/dto/iemr/BadgeEarnedDTO.java new file mode 100644 index 000000000..b7bdbb5a0 --- /dev/null +++ b/src/main/java/com/iemr/flw/dto/iemr/BadgeEarnedDTO.java @@ -0,0 +1,22 @@ +package com.iemr.flw.dto.iemr; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** Wire shape shared with the app's BadgeEarnedDTO (Moshi). */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class BadgeEarnedDTO { + private String badgeId; + private Integer level; + private Long earnedAt; + + /** + * Stable discriminator for badges that can be earned more than once at the same level. + * A quarter key, or an opaque digest for per-case awards; empty otherwise. Never a + * beneficiary identifier — see {@link com.iemr.flw.domain.iemr.BadgeEarned#getAwardKey()}. + */ + private String awardKey; +} diff --git a/src/main/java/com/iemr/flw/dto/iemr/BadgeEarnedPushDTO.java b/src/main/java/com/iemr/flw/dto/iemr/BadgeEarnedPushDTO.java new file mode 100644 index 000000000..dddca01c1 --- /dev/null +++ b/src/main/java/com/iemr/flw/dto/iemr/BadgeEarnedPushDTO.java @@ -0,0 +1,15 @@ +package com.iemr.flw.dto.iemr; + +import lombok.Data; + +import java.util.List; + +/** + * POST /badges/earned body. The userId field is accepted for wire compatibility + * but never trusted: the owner is always taken from the JWT. + */ +@Data +public class BadgeEarnedPushDTO { + private Integer userId; + private List badges; +} diff --git a/src/main/java/com/iemr/flw/dto/iemr/DewormingFormDTO.java b/src/main/java/com/iemr/flw/dto/iemr/DewormingFormDTO.java index 3c3feb613..f7d6fa2bb 100644 --- a/src/main/java/com/iemr/flw/dto/iemr/DewormingFormDTO.java +++ b/src/main/java/com/iemr/flw/dto/iemr/DewormingFormDTO.java @@ -12,6 +12,6 @@ public class DewormingFormDTO { private String dewormingDate; private String dewormingLocation; private Integer ageGroup; - private String image1; - private String image2; + private byte[] image1; + private byte[] image2; } \ No newline at end of file diff --git a/src/main/java/com/iemr/flw/dto/iemr/GeneralCheckUpDto.java b/src/main/java/com/iemr/flw/dto/iemr/GeneralCheckUpDto.java new file mode 100644 index 000000000..da0594c3a --- /dev/null +++ b/src/main/java/com/iemr/flw/dto/iemr/GeneralCheckUpDto.java @@ -0,0 +1,12 @@ +package com.iemr.flw.dto.iemr; + +import lombok.Data; + +@Data +public class GeneralCheckUpDto { + private Long id; + private Long beneficiaryId; + private Long houseHoldId; + private String visitDate; + private GeneralCheckUpListDto fields; +} diff --git a/src/main/java/com/iemr/flw/dto/iemr/GeneralCheckUpFollowUpDto.java b/src/main/java/com/iemr/flw/dto/iemr/GeneralCheckUpFollowUpDto.java new file mode 100644 index 000000000..9840e0d0e --- /dev/null +++ b/src/main/java/com/iemr/flw/dto/iemr/GeneralCheckUpFollowUpDto.java @@ -0,0 +1,16 @@ +package com.iemr.flw.dto.iemr; + +import lombok.Data; + +import java.time.LocalDate; + +@Data +public class GeneralCheckUpFollowUpDto { + private Long id; + private Long beneficiaryId; + private Long houseHoldId; + private String visitDate; + private String referralReason; + private LocalDate referredOnDate; + private GeneralCheckUpFollowUpListDto fields; +} diff --git a/src/main/java/com/iemr/flw/dto/iemr/GeneralCheckUpFollowUpListDto.java b/src/main/java/com/iemr/flw/dto/iemr/GeneralCheckUpFollowUpListDto.java new file mode 100644 index 000000000..fc8bdd588 --- /dev/null +++ b/src/main/java/com/iemr/flw/dto/iemr/GeneralCheckUpFollowUpListDto.java @@ -0,0 +1,19 @@ +package com.iemr.flw.dto.iemr; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +@Data +public class GeneralCheckUpFollowUpListDto { + @JsonProperty("follow_up_date") + private String followUpDate; + + @JsonProperty("followup_status") + private String followupStatus; + + @JsonProperty("referred_to") + private String referredTo; + +} diff --git a/src/main/java/com/iemr/flw/dto/iemr/GeneralCheckUpListDto.java b/src/main/java/com/iemr/flw/dto/iemr/GeneralCheckUpListDto.java new file mode 100644 index 000000000..27f58e695 --- /dev/null +++ b/src/main/java/com/iemr/flw/dto/iemr/GeneralCheckUpListDto.java @@ -0,0 +1,51 @@ +package com.iemr.flw.dto.iemr; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +@Data +public class GeneralCheckUpListDto { + @JsonProperty("check_up_date") + private String check_up_date; + + @JsonProperty("problem_type") + private String problemType; + + @JsonProperty("problems_g6") + private String problemsG6; + + @JsonProperty("other_text") + private String otherTextLegacy; + + private String checkupId; + + private Integer ashaId; + + private String checkupDate; + + private String problemTypeId; + + private String problemTypeLabel; + + private List problems; + + private String otherText; + + private String priority; + + private Integer masterVersion; + + private String referralReason; + + private String referralDate; + + private String createdBy; + + private Long createdDate; + + private Long updatedDate; + + +} diff --git a/src/main/java/com/iemr/flw/dto/iemr/PhcReviewMeetingFormDTO.java b/src/main/java/com/iemr/flw/dto/iemr/PhcReviewMeetingFormDTO.java index b08386c6e..4a88d0bd4 100644 --- a/src/main/java/com/iemr/flw/dto/iemr/PhcReviewMeetingFormDTO.java +++ b/src/main/java/com/iemr/flw/dto/iemr/PhcReviewMeetingFormDTO.java @@ -13,8 +13,8 @@ public class PhcReviewMeetingFormDTO { private String mitaninHistory; private String mitaninActivityCheckList; private Integer placeId; - private String image1; - private String image2; + private byte[] image1; + private byte[] image2; diff --git a/src/main/java/com/iemr/flw/dto/iemr/ProblemDto.java b/src/main/java/com/iemr/flw/dto/iemr/ProblemDto.java new file mode 100644 index 000000000..44de6eabd --- /dev/null +++ b/src/main/java/com/iemr/flw/dto/iemr/ProblemDto.java @@ -0,0 +1,11 @@ +package com.iemr.flw.dto.iemr; + +import lombok.Data; + +@Data +public class ProblemDto { + + private String problemId; + + private String label; +} \ No newline at end of file diff --git a/src/main/java/com/iemr/flw/dto/iemr/VHNDFormDTO.java b/src/main/java/com/iemr/flw/dto/iemr/VHNDFormDTO.java index aa99186a1..3ab437857 100644 --- a/src/main/java/com/iemr/flw/dto/iemr/VHNDFormDTO.java +++ b/src/main/java/com/iemr/flw/dto/iemr/VHNDFormDTO.java @@ -35,8 +35,8 @@ public class VHNDFormDTO { private String vhndDate; private String place; private Integer noOfBeneficiariesAttended; - private String image1; - private String image2; + private byte[] image1; + private byte[] image2; private Integer vhndPlaceId; private String pregnantWomenAnc; private String lactatingMothersPnc; diff --git a/src/main/java/com/iemr/flw/dto/iemr/VhncFormDTO.java b/src/main/java/com/iemr/flw/dto/iemr/VhncFormDTO.java index a2515b421..45f159689 100644 --- a/src/main/java/com/iemr/flw/dto/iemr/VhncFormDTO.java +++ b/src/main/java/com/iemr/flw/dto/iemr/VhncFormDTO.java @@ -32,8 +32,8 @@ public class VhncFormDTO { private String vhncDate; private String place; private Integer noOfBeneficiariesAttended; - private String image1; - private String image2; + private byte[] image1; + private byte[] image2; private String villageName; private Integer anm; private Integer aww; diff --git a/src/main/java/com/iemr/flw/repo/iemr/BadgeConfigRepo.java b/src/main/java/com/iemr/flw/repo/iemr/BadgeConfigRepo.java new file mode 100644 index 000000000..0341fa8f1 --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/BadgeConfigRepo.java @@ -0,0 +1,9 @@ +package com.iemr.flw.repo.iemr; + +import com.iemr.flw.domain.iemr.BadgeConfig; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface BadgeConfigRepo extends JpaRepository { +} diff --git a/src/main/java/com/iemr/flw/repo/iemr/BadgeEarnedRepo.java b/src/main/java/com/iemr/flw/repo/iemr/BadgeEarnedRepo.java new file mode 100644 index 000000000..6452e6225 --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/BadgeEarnedRepo.java @@ -0,0 +1,15 @@ +package com.iemr.flw.repo.iemr; + +import com.iemr.flw.domain.iemr.BadgeEarned; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface BadgeEarnedRepo extends JpaRepository { + List findByUserIdOrderByEarnedAtAsc(Integer userId); + + boolean existsByUserIdAndBadgeIdAndLevelAndAwardKey( + Integer userId, String badgeId, Integer level, String awardKey); +} diff --git a/src/main/java/com/iemr/flw/repo/iemr/BadgeStreakFreezeRepo.java b/src/main/java/com/iemr/flw/repo/iemr/BadgeStreakFreezeRepo.java new file mode 100644 index 000000000..adc063d94 --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/BadgeStreakFreezeRepo.java @@ -0,0 +1,13 @@ +package com.iemr.flw.repo.iemr; + +import com.iemr.flw.domain.iemr.BadgeStreakFreeze; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface BadgeStreakFreezeRepo extends JpaRepository { + /** Global windows (user_id IS NULL) plus this ASHA's own. */ + List findByUserIdIsNullOrUserId(Integer userId); +} diff --git a/src/main/java/com/iemr/flw/repo/iemr/BenReferDetailsRepo.java b/src/main/java/com/iemr/flw/repo/iemr/BenReferDetailsRepo.java index 25a0b652a..dbb76975e 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/BenReferDetailsRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/BenReferDetailsRepo.java @@ -47,6 +47,9 @@ public interface BenReferDetailsRepo extends JpaRepository findByCreatedBy(String userName); + @Query(value = "SELECT COUNT(*) FROM db_iemr.t_benreferdetails WHERE CreatedBy = :userName AND revisitDate >= :startDate AND revisitDate < :endDate", nativeQuery = true) + Long countMonthlyReferralsByDateRange(@Param("userName") String userName, @Param("startDate") java.sql.Timestamp startDate, @Param("endDate") java.sql.Timestamp endDate); + @Query(value = """ SELECT COUNT(*) FROM db_iemr.t_benreferdetails diff --git a/src/main/java/com/iemr/flw/repo/iemr/CbacIemrDetailsRepo.java b/src/main/java/com/iemr/flw/repo/iemr/CbacIemrDetailsRepo.java index 5e93dfad6..82a96c807 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/CbacIemrDetailsRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/CbacIemrDetailsRepo.java @@ -37,6 +37,9 @@ public interface CbacIemrDetailsRepo extends CrudRepository findByCreatedBy(String userName); + @Query("SELECT c.beneficiaryRegId, c.createdDate FROM CbacDetailsImer c WHERE c.createdBy = :userName AND c.beneficiaryRegId IS NOT NULL AND c.createdDate IS NOT NULL") + List findBenRegIdAndCreatedDateByCreatedBy(@Param("userName") String userName); + @Query(value = "SELECT BeneficiaryID FROM db_identity.m_beneficiaryregidmapping " + "WHERE BenRegId = :benRegId AND Deleted = 0", nativeQuery = true) Long getBeneficiaryId(@Param("benRegId") Long benRegId); diff --git a/src/main/java/com/iemr/flw/repo/iemr/EligibleCoupleRegisterRepo.java b/src/main/java/com/iemr/flw/repo/iemr/EligibleCoupleRegisterRepo.java index 218c5b2b3..d9418df08 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/EligibleCoupleRegisterRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/EligibleCoupleRegisterRepo.java @@ -16,6 +16,8 @@ public interface EligibleCoupleRegisterRepo extends JpaRepository findByCreatedBy(String userName); + long countByCreatedBy(String userName); + @Query(" SELECT ecr FROM EligibleCoupleRegister ecr WHERE ecr.createdBy = :userId and ecr.createdDate >= :fromDate and ecr.createdDate <= :toDate") List getECRegRecords(@Param("userId") String userId, @Param("fromDate") Timestamp fromDate, @Param("toDate") Timestamp toDate); diff --git a/src/main/java/com/iemr/flw/repo/iemr/GeneralCheckUpFollowUpRepository.java b/src/main/java/com/iemr/flw/repo/iemr/GeneralCheckUpFollowUpRepository.java new file mode 100644 index 000000000..cef915267 --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/GeneralCheckUpFollowUpRepository.java @@ -0,0 +1,22 @@ +package com.iemr.flw.repo.iemr; + +import com.iemr.flw.domain.iemr.GeneralCheckUpFollowUp; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +@Repository +public interface GeneralCheckUpFollowUpRepository + extends JpaRepository { + List findByAshaId(Integer userId); + + Optional + findFirstByBenIdAndReferralReasonAndReferredOnDateGreaterThanEqualAndReferredOnDateLessThan( + Long benId, + String referralReason, + LocalDate monthStart, + LocalDate nextMonthStart);} + diff --git a/src/main/java/com/iemr/flw/repo/iemr/GeneralCheckUpRepo.java b/src/main/java/com/iemr/flw/repo/iemr/GeneralCheckUpRepo.java new file mode 100644 index 000000000..557f82308 --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/GeneralCheckUpRepo.java @@ -0,0 +1,12 @@ +package com.iemr.flw.repo.iemr; + +import com.iemr.flw.domain.iemr.GeneralCheckUp; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface GeneralCheckUpRepo extends JpaRepository { + List findByCreatedBy(String userName); +} diff --git a/src/main/java/com/iemr/flw/repo/iemr/IFAFormSubmissionRepository.java b/src/main/java/com/iemr/flw/repo/iemr/IFAFormSubmissionRepository.java index 0b1b5ff72..88201f45b 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/IFAFormSubmissionRepository.java +++ b/src/main/java/com/iemr/flw/repo/iemr/IFAFormSubmissionRepository.java @@ -11,6 +11,8 @@ public interface IFAFormSubmissionRepository extends JpaRepository { List findByUserId(Integer userName); + + long countByUserId(Integer userId); @Query("SELECT i FROM IFAFormSubmissionData i " + "WHERE i.userId = :userId " + "AND i.visitDate = :visitDate") diff --git a/src/main/java/com/iemr/flw/repo/iemr/IncentiveRecordRepo.java b/src/main/java/com/iemr/flw/repo/iemr/IncentiveRecordRepo.java index 74acfa5f8..d72c069d6 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/IncentiveRecordRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/IncentiveRecordRepo.java @@ -38,6 +38,21 @@ IncentiveActivityRecord findRecordByActivityIdCreatedDateBenId( Optional findById(Long id); + @Query("SELECT record FROM IncentiveActivityRecord record WHERE record.ashaId = :ashaId ORDER BY record.id DESC") + List findRecordsByAshaOrderByIdDesc(@Param("ashaId") Integer ashaId); + + @Query("SELECT r.activityId FROM IncentiveActivityRecord r WHERE r.activityId IN :activityIds AND r.benId = :benId AND r.ashaId = :ashaId AND r.createdDate >= :startOfMonth AND r.createdDate < :startOfNextMonth") + List findExistingActivityIdsByMonth(@Param("activityIds") List activityIds, @Param("benId") Long benId, @Param("ashaId") Integer ashaId, @Param("startOfMonth") Timestamp startOfMonth, @Param("startOfNextMonth") Timestamp startOfNextMonth); + + @Query("SELECT r.benId, r.startDate FROM IncentiveActivityRecord r WHERE r.activityId = :activityId AND r.ashaId = :ashaId") + List findBenIdAndStartDateByActivityIdAndAshaId(@Param("activityId") Long activityId, @Param("ashaId") Integer ashaId); + + @Query("SELECT r FROM IncentiveActivityRecord r WHERE r.ashaId = :ashaId AND r.isClaimed = true AND r.createdDate >= :startDate AND r.createdDate < :endDate") + List findClaimedRecordsByAshaAndCreatedDateRange(@Param("ashaId") Integer ashaId, @Param("startDate") Timestamp startDate, @Param("endDate") Timestamp endDate); + + @Query("SELECT r FROM IncentiveActivityRecord r WHERE r.ashaId = :ashaId AND r.activityId = :activityId AND r.createdDate >= :startDate AND r.createdDate < :endDate") + List findRecordsByAshaAndActivityAndCreatedDateRange(@Param("ashaId") Integer ashaId, @Param("activityId") Long activityId, @Param("startDate") Timestamp startDate, @Param("endDate") Timestamp endDate); + @Query(value = """ SELECT * diff --git a/src/main/java/com/iemr/flw/repo/iemr/IncentivesRepo.java b/src/main/java/com/iemr/flw/repo/iemr/IncentivesRepo.java index d1cbc7f84..10faf62f5 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/IncentivesRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/IncentivesRepo.java @@ -45,6 +45,9 @@ IncentiveActivity findIncentiveMasterById( List findByGroupNotAndIsDeleted(String group, Boolean isDeleted); + @Query("SELECT i FROM IncentiveActivity i WHERE i.name IN :names AND i.isDeleted = false") + List findByNamesAndIsDeletedFalse(@Param("names") List names); + // IncentivesRepo — replaces N calls to findIncentiveMasterById() @Query("SELECT i.id FROM IncentiveActivity i WHERE i.id IN :ids AND i.isDeleted = false AND " + "(:isCG = true AND i.group = 'ACTIVITY' OR :isCG = false AND i.group != 'ACTIVITY')") diff --git a/src/main/java/com/iemr/flw/seeder/BadgeConfigSeeder.java b/src/main/java/com/iemr/flw/seeder/BadgeConfigSeeder.java new file mode 100644 index 000000000..1c83833a5 --- /dev/null +++ b/src/main/java/com/iemr/flw/seeder/BadgeConfigSeeder.java @@ -0,0 +1,49 @@ +package com.iemr.flw.seeder; + +import com.iemr.flw.domain.iemr.BadgeConfig; +import com.iemr.flw.repo.iemr.BadgeConfigRepo; +import jakarta.annotation.PostConstruct; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * Seeds the default badge tuning once (table empty). Values mirror the app's + * compiled defaults so a fresh server changes nothing until someone edits a row. + */ +@Component +public class BadgeConfigSeeder { + + private static final Logger log = LoggerFactory.getLogger(BadgeConfigSeeder.class); + + private final BadgeConfigRepo repo; + + public BadgeConfigSeeder(BadgeConfigRepo repo) { + this.repo = repo; + } + + @PostConstruct + public void seed() { + try { + if (repo.count() > 0) return; + repo.saveAll(List.of( + new BadgeConfig("feature_enabled", "true"), + new BadgeConfig("copy_version", "1"), + new BadgeConfig("milestones.steady_syncer", "2,4,6,8"), + new BadgeConfig("grace.steady_syncer", "1"), + new BadgeConfig("milestones.timely_reporter", "1,3,6,12"), + new BadgeConfig("milestones.complete_worker", "3"), + new BadgeConfig("milestones.community_voice", "2"), + new BadgeConfig("milestones.maternal_journey", "1,5,15,30"), + new BadgeConfig("milestones.child_fully_protected", "3,10,25,60"), + new BadgeConfig("milestones.digital_identity", "10,25,75,150"), + new BadgeConfig("enabled.critical_referral", "false") + )); + log.info("Badge config defaults seeded"); + } catch (Exception e) { + log.warn("Badge config seeding skipped: {}", e.getMessage()); + } + } +} diff --git a/src/main/java/com/iemr/flw/service/BadgeService.java b/src/main/java/com/iemr/flw/service/BadgeService.java new file mode 100644 index 000000000..7d30fe25e --- /dev/null +++ b/src/main/java/com/iemr/flw/service/BadgeService.java @@ -0,0 +1,85 @@ +package com.iemr.flw.service; + +import com.iemr.flw.domain.iemr.BadgeConfig; +import com.iemr.flw.domain.iemr.BadgeEarned; +import com.iemr.flw.domain.iemr.BadgeStreakFreeze; +import com.iemr.flw.dto.iemr.BadgeEarnedDTO; +import com.iemr.flw.repo.iemr.BadgeConfigRepo; +import com.iemr.flw.repo.iemr.BadgeEarnedRepo; +import com.iemr.flw.repo.iemr.BadgeStreakFreezeRepo; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Badges backend (Badge LLD §4). The device owns evaluation; the server only + * distributes tuning + freeze windows and keeps the durable award log so a + * reinstall restores progress. + */ +@Service +public class BadgeService { + + private final BadgeConfigRepo configRepo; + private final BadgeStreakFreezeRepo freezeRepo; + private final BadgeEarnedRepo earnedRepo; + + public BadgeService(BadgeConfigRepo configRepo, BadgeStreakFreezeRepo freezeRepo, + BadgeEarnedRepo earnedRepo) { + this.configRepo = configRepo; + this.freezeRepo = freezeRepo; + this.earnedRepo = earnedRepo; + } + + public Map getConfig() { + Map out = new LinkedHashMap<>(); + for (BadgeConfig row : configRepo.findAll()) { + out.put(row.getKey(), row.getValue()); + } + return out; + } + + public List getFreezes(Integer userId) { + return freezeRepo.findByUserIdIsNullOrUserId(userId); + } + + public List getEarned(Integer userId) { + return earnedRepo.findByUserIdOrderByEarnedAtAsc(userId).stream() + .map(e -> new BadgeEarnedDTO( + e.getBadgeId(), e.getLevel(), e.getEarnedAt(), e.getAwardKey())) + .toList(); + } + + /** + * Idempotent: rows already known for (user, badge, level, awardKey) are skipped. Returns + * rows inserted. + * + * A missing awardKey is stored as empty rather than rejected, which is what a streak or + * cumulative badge sends and also what an older client sends for everything. That client + * then behaves exactly as it did before this field existed. + */ + @Transactional + public int saveEarned(Integer userId, List badges) { + if (badges == null || badges.isEmpty()) return 0; + long now = System.currentTimeMillis(); + int inserted = 0; + for (BadgeEarnedDTO dto : badges) { + if (dto == null || dto.getBadgeId() == null || dto.getLevel() == null) continue; + String awardKey = dto.getAwardKey() != null ? dto.getAwardKey() : ""; + if (earnedRepo.existsByUserIdAndBadgeIdAndLevelAndAwardKey( + userId, dto.getBadgeId(), dto.getLevel(), awardKey)) continue; + BadgeEarned row = new BadgeEarned(); + row.setUserId(userId); + row.setBadgeId(dto.getBadgeId()); + row.setLevel(dto.getLevel()); + row.setAwardKey(awardKey); + row.setEarnedAt(dto.getEarnedAt() != null ? dto.getEarnedAt() : now); + row.setReceivedAt(now); + earnedRepo.save(row); + inserted++; + } + return inserted; + } +} diff --git a/src/main/java/com/iemr/flw/service/DiseaseControlService.java b/src/main/java/com/iemr/flw/service/DiseaseControlService.java index 65e315dce..363dcb5d2 100644 --- a/src/main/java/com/iemr/flw/service/DiseaseControlService.java +++ b/src/main/java/com/iemr/flw/service/DiseaseControlService.java @@ -49,4 +49,9 @@ public interface DiseaseControlService { List saveChronicDiseaseVisit(List requestList,String token) throws IEMRException; List getCdtfVisits(GetBenRequestHandler getBenRequestHandler); + + List saveGeneralCheckUp(List generalCheckUpRequest,String token) throws IEMRException; + List saveGeneralCheckUpFollowUp(List generalCheckUpFollowUpDtos,String token) throws IEMRException; + List getGeneralCheckUp(String token); + List getGeneralCheckUpFollowUp(String token); } \ No newline at end of file diff --git a/src/main/java/com/iemr/flw/service/impl/DiseaseControlServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/DiseaseControlServiceImpl.java index 4cbfeb874..aa00718be 100644 --- a/src/main/java/com/iemr/flw/service/impl/DiseaseControlServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/DiseaseControlServiceImpl.java @@ -24,6 +24,8 @@ */ package com.iemr.flw.service.impl; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.iemr.flw.domain.identity.RMNCHBeneficiaryDetailsRmnch; import com.iemr.flw.domain.iemr.*; @@ -94,6 +96,15 @@ public class DiseaseControlServiceImpl implements DiseaseControlService { @Autowired private JwtUtil jwtUtil; + private static final DateTimeFormatter VISIT_DATE_FMT = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + + @Autowired + private GeneralCheckUpRepo generalCheckUpRepo; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired private IncentiveLogicService incentiveLogicService; @@ -102,8 +113,9 @@ public class DiseaseControlServiceImpl implements DiseaseControlService { @Autowired private BeneficiaryRepo beneficiaryRepo; - - +@Autowired + private GeneralCheckUpFollowUpRepository + generalCheckUpFollowUpRepository; private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); @Override @@ -1581,4 +1593,344 @@ public List getCdtfVisits(GetBenRequestHandler getBenReq return dtoList; } + @Override + @Transactional + public List saveGeneralCheckUp(List generalCheckUpRequest, String token) throws IEMRException { + + + if (generalCheckUpRequest == null || generalCheckUpRequest.isEmpty()) { + return Collections.emptyList(); + } + + List entities = new ArrayList<>(); + + for (GeneralCheckUpDto dto : generalCheckUpRequest) { + GeneralCheckUpListDto fields = dto.getFields(); + if (fields == null) { + throw new IllegalArgumentException("fields missing for beneficiary " + dto.getBeneficiaryId()); + } + + GeneralCheckUp entity = new GeneralCheckUp(); + entity.setBenId(dto.getBeneficiaryId()); + entity.setHouseholdId(dto.getHouseHoldId()); + entity.setAshaId(jwtUtil.extractUserId(token)); + entity.setCheckupDate(LocalDate.parse(dto.getVisitDate(), VISIT_DATE_FMT)); + entity.setProblemType(fields.getProblemTypeId()); + entity.setProblemTypeLabel(fields.getProblemTypeLabel()); + entity.setPriority(fields.getPriority()); + entity.setOtherText(fields.getOtherText()); + + try { + entity.setProblems(fields.getProblems() != null + ? objectMapper.writeValueAsString(fields.getProblems()) + : null); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Invalid problems payload for checkup " + + fields.getCheckupId(), e); + } + + entity.setReferralReason(fields.getReferralReason()); + entity.setReferralDate(fields.getReferralDate() != null + ? LocalDate.parse(fields.getReferralDate()) + : null); + entity.setCreatedBy(fields.getCreatedBy()); + entity.setSyncedDate(new Timestamp(System.currentTimeMillis())); + + entities.add(entity); + } + + List saved = generalCheckUpRepo.saveAll(entities); + + for (int i = 0; i < saved.size(); i++) { + generalCheckUpRequest.get(i).setId(saved.get(i).getId()); + } + + return generalCheckUpRequest; + } + + + @Override + @Transactional + public List saveGeneralCheckUpFollowUp( + List dtos, + String token) throws IEMRException { + + if (dtos == null || dtos.isEmpty()) { + return Collections.emptyList(); + } + + try { + Integer userId = jwtUtil.extractUserId(token); + String username = jwtUtil.extractUsername(token); + + List result = new ArrayList<>(); + + for (GeneralCheckUpFollowUpDto dto : dtos) { + + if (dto == null || dto.getFields() == null) { + throw new IllegalArgumentException( + "Follow-up details are required"); + } + + if (dto.getBeneficiaryId() == null) { + throw new IllegalArgumentException( + "Beneficiary ID is required"); + } + + // Referral-specific fields + String referralReason = dto.getReferralReason(); + LocalDate referredOnDate = dto.getReferredOnDate(); + + if (referralReason == null || referralReason.isBlank()) { + throw new IllegalArgumentException( + "Referral reason is required"); + } + + if (referredOnDate == null) { + throw new IllegalArgumentException( + "Referral date is required"); + } + + + + LocalDate monthStart = referredOnDate.withDayOfMonth(1); + LocalDate nextMonthStart = monthStart.plusMonths(1); + + GeneralCheckUpFollowUpListDto fields = dto.getFields(); + + String date = fields.getFollowUpDate(); + + if (date == null || date.isBlank()) { + date = dto.getVisitDate(); + } + + if (date == null || date.isBlank()) { + throw new IllegalArgumentException( + "Follow-up date is required"); + } + + LocalDate followUpDate = + LocalDate.parse(date.substring(0, 10)); + + String status = fields.getFollowupStatus(); + + if (status == null || status.isBlank()) { + throw new IllegalArgumentException( + "Follow-up status is required"); + } + + boolean referredToHigherFacility = + "referred_to_higher_facility" + .equalsIgnoreCase(status); + + if (referredToHigherFacility + && (fields.getReferredTo() == null + || fields.getReferredTo().isBlank())) { + throw new IllegalArgumentException( + "Referred-to facility is required"); + } + + // Find the existing record for this specific referral. + GeneralCheckUpFollowUp entity = + generalCheckUpFollowUpRepository + .findFirstByBenIdAndReferralReasonAndReferredOnDateGreaterThanEqualAndReferredOnDateLessThan( + dto.getBeneficiaryId(), + referralReason, + monthStart, + nextMonthStart) + .orElseGet(GeneralCheckUpFollowUp::new); + + // Insert if no matching record exists; otherwise update it. + entity.setBenId(dto.getBeneficiaryId()); + entity.setHouseholdId(dto.getHouseHoldId()); + + entity.setReferralReason(referralReason); + entity.setReferredOnDate(referredOnDate); + + entity.setFollowUpDate(followUpDate); + entity.setFollowupStatus(status); + + entity.setReferredTo( + referredToHigherFacility + ? fields.getReferredTo() + : null + ); + + entity.setAshaId(userId); + entity.setCreatedBy(username); + entity.setReferredTo(dto.getFields().getReferredTo()); + + GeneralCheckUpFollowUp savedEntity = + generalCheckUpFollowUpRepository.save(entity); + + // Return the saved record with referral information. + GeneralCheckUpFollowUpDto response = + new GeneralCheckUpFollowUpDto(); + + response.setId(savedEntity.getId()); + response.setBeneficiaryId(savedEntity.getBenId()); + response.setHouseHoldId(savedEntity.getHouseholdId()); + + response.setReferralReason(savedEntity.getReferralReason()); + response.setReferredOnDate( + savedEntity.getReferredOnDate() != null + ? savedEntity.getReferredOnDate() + : null + ); + + GeneralCheckUpFollowUpListDto responseFields = + new GeneralCheckUpFollowUpListDto(); + + responseFields.setFollowUpDate( + savedEntity.getFollowUpDate().toString()); + responseFields.setFollowupStatus( + savedEntity.getFollowupStatus()); + responseFields.setReferredTo(savedEntity.getReferredTo()); + + response.setFields(responseFields); + + result.add(response); + } + + return result; + + } catch (Exception e) { + throw new RuntimeException( + "Error saving General Check-up follow-up", e); + } + } + + @Override + @Transactional() + public List getGeneralCheckUpFollowUp( + String token) { + try { + Integer userId = jwtUtil.extractUserId(token); + + List entities = + generalCheckUpFollowUpRepository.findByAshaId(userId); + + return entities.stream() + .map(entity -> { + GeneralCheckUpFollowUpDto dto = + new GeneralCheckUpFollowUpDto(); + + dto.setId(entity.getId()); + dto.setBeneficiaryId(entity.getBenId()); + dto.setHouseHoldId(entity.getHouseholdId()); + dto.setReferralReason(entity.getReferralReason()); + dto.setReferredOnDate(entity.getReferredOnDate()); + + GeneralCheckUpFollowUpListDto fields = + new GeneralCheckUpFollowUpListDto(); + + fields.setFollowUpDate( + entity.getFollowUpDate() != null + ? entity.getFollowUpDate().toString() + : null); + + fields.setFollowupStatus(entity.getFollowupStatus()); + fields.setReferredTo(entity.getReferredTo()); + + + dto.setFields(fields); + + dto.setVisitDate( + entity.getFollowUpDate() != null + ? entity.getFollowUpDate().toString() + : null); + + return dto; + }) + .collect(Collectors.toList()); + } catch (Exception e) { + // Replace with your project's standard IEMRException constructor. + throw new RuntimeException( + "Error saving General Check-up follow-up", e); + } + + } + + @Override + public List getGeneralCheckUp(String token) { + + logger.info("===== getGeneralCheckUp() called ====="); + logger.info("Received token: {}", token); + + Integer userName = null; + try { + userName = jwtUtil.extractUserId(token); + logger.info("Extracted username from token: {}", userName); + } catch (Exception e) { + logger.error("Error while extracting username from token", e); + } + + List rows = null; + try { + rows = generalCheckUpRepo.findByCreatedBy(userRepo.getUserNamedByUserId(userName)); + logger.info("Records fetched from DB for user [{}]: {}", userName, rows != null ? rows.size() : 0); + } catch (Exception e) { + logger.error("Error while fetching GeneralCheckUp records for user: {}", userName, e); + throw e; + } + + List result = new ArrayList<>(); + + if (rows == null || rows.isEmpty()) { + logger.warn("No GeneralCheckUp records found for user: {}", userName); + return result; + } + + for (GeneralCheckUp e : rows) { + logger.debug("Processing GeneralCheckUp id: {}, checkupId: {}", e.getId(), e.getCheckupId()); + + String visitDate = e.getCheckupDate() != null + ? e.getCheckupDate().format(VISIT_DATE_FMT) : null; + + GeneralCheckUpListDto f = new GeneralCheckUpListDto(); + f.setCheck_up_date(visitDate); + f.setProblemType(e.getProblemType()); + f.setOtherText(e.getOtherText()); + f.setCheckupId(e.getId().toString()); + f.setAshaId(e.getAshaId()); + f.setCheckupDate(e.getCheckupDate() != null ? e.getCheckupDate().toString() : null); // "2026-10-22" + f.setProblemTypeId(e.getProblemType()); + f.setProblemTypeLabel(e.getProblemTypeLabel()); + f.setOtherText(e.getOtherText()); + f.setPriority(e.getPriority()); + f.setReferralReason(e.getReferralReason()); + f.setReferralDate(e.getReferralDate() != null ? e.getReferralDate().toString() : null); + f.setCreatedBy(e.getCreatedBy()); + f.setCreatedDate(e.getCreatedDate() != null ? e.getCreatedDate().getTime() : null); + f.setUpdatedDate(e.getUpdatedDate() != null ? e.getUpdatedDate().getTime() : null); + + // json string -> List + try { + if (e.getProblems() != null) { + f.setProblems(objectMapper.readValue( + e.getProblems(), new TypeReference>() {})); + logger.debug("Successfully parsed problems JSON for id: {}", e.getId()); + } else { + logger.debug("Problems field is null for id: {}", e.getId()); + } + } catch (JsonProcessingException ex) { + logger.error("Invalid problems JSON for id: {}. JSON: {}", e.getId(), e.getProblems(), ex); + throw new IllegalStateException("Invalid problems json for id " + e.getId(), ex); + } + + GeneralCheckUpDto dto = new GeneralCheckUpDto(); + dto.setId(e.getId()); + dto.setBeneficiaryId(e.getBenId()); + dto.setHouseHoldId(e.getHouseholdId()); + dto.setVisitDate(visitDate); + dto.setFields(f); + + result.add(dto); + } + + logger.info("===== getGeneralCheckUp() completed successfully. Total records returned: {} =====", result.size()); + + + return result; + } } \ No newline at end of file diff --git a/src/main/java/com/iemr/flw/service/impl/IncentiveServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/IncentiveServiceImpl.java index b89cb137f..7fa1ef08a 100644 --- a/src/main/java/com/iemr/flw/service/impl/IncentiveServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/IncentiveServiceImpl.java @@ -199,197 +199,198 @@ public String getIncentiveMaster(IncentiveRequestDTO incentiveRequestDTO) { } return null; } + @Override public String getAllIncentivesByUserId(GetBenRequestHandler request) { - int page = 0; - int size = 200; // bumped from 20 — fewer round trips for large histories - Page pageResult; - List finalDtos = new ArrayList<>(); + Integer ashaId = request.getAshaId(); - // Fetch user detail ONCE instead of twice - UserServiceRoleDTO userDetail = userService.getUserDetail(request.getAshaId()); - Integer stateCode = userDetail.getStateId(); - String userName = userDetail.getUserName(); + // Fetch user stateId and userName in ONE lightweight query instead of heavy getUserDetail() (which builds facilityData) + List userRoles = userRepo.getUserRole(ashaId); + UserServiceRoleDTO userDetail = (userRoles != null && !userRoles.isEmpty()) + ? userRoles.get(0) + : userService.getUserDetail(ashaId); + Integer stateCode = userDetail != null ? userDetail.getStateId() : null; + String userName = userDetail != null ? userDetail.getUserName() : null; try { - if (stateCode.equals(StateCode.AM.getStateCode())) { - checkMonthlyAshaIncentive(request.getAshaId()); - } - if (stateCode.equals(StateCode.CG.getStateCode())) { - checkMonthlyAshaIncentiveForCg(request.getAshaId()); + if (stateCode != null && stateCode.equals(StateCode.AM.getStateCode())) { + checkMonthlyAshaIncentive(ashaId, userName); + } else if (stateCode != null && stateCode.equals(StateCode.CG.getStateCode())) { + checkMonthlyAshaIncentiveForCg(ashaId, userName); } } catch (Exception e) { logger.error("Error in checkMonthlyAshaIncentive: ", e); } try { - addIncentiveForIronTablets(request.getAshaId()); - incentiveOfNcdReferal(request.getAshaId(), request.getVillageID(),request); + addIncentiveForIronTablets(ashaId, userName, stateCode); + Integer effectiveStateId = request.getVillageID() != null ? request.getVillageID() : stateCode; + incentiveOfNcdReferal(ashaId, effectiveStateId, request, userName); } catch (Exception e) { logger.error("Error in incentiveOfNcdReferal: ", e); } boolean isCG = stateCode != null && stateCode.equals(StateCode.CG.getStateCode()); - do { - Pageable pageable = PageRequest.of(page, size, Sort.by("id").descending()); + // Fetch all records for this ASHA in a single query ordered by id DESC (avoids Page COUNT(DISTINCT) + multi-page round trips) + List entities = recordRepo.findRecordsByAshaOrderByIdDesc(ashaId); - pageResult = recordRepo.findRecordsByAsha(request.getAshaId(), pageable); + if (entities.isEmpty()) { + return new Gson().toJson(Collections.emptyList()); + } - List entities = pageResult.getContent(); + // Bulk fetch valid activity ids + List activityIds = entities.stream() + .map(IncentiveActivityRecord::getActivityId) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); - if (entities.isEmpty()) { - return new Gson().toJson(Collections.emptyList()); - } + if (activityIds.isEmpty()) { + return new Gson().toJson(Collections.emptyList()); + } - // Bulk fetch valid activity ids - List activityIds = entities.stream() - .map(IncentiveActivityRecord::getActivityId) - .distinct() - .collect(Collectors.toList()); + Set validActivityIds = isCG + ? incentivesRepo.findValidActivityIds(activityIds, true) + : incentivesRepo.findValidActivityIds(activityIds, false); - Set validActivityIds = isCG - ? incentivesRepo.findValidActivityIds(activityIds, true) - : incentivesRepo.findValidActivityIds(activityIds, false); + entities = entities.stream() + .filter(e -> validActivityIds.contains(e.getActivityId())) + .collect(Collectors.toList()); - entities = entities.stream() - .filter(e -> validActivityIds.contains(e.getActivityId())) - .collect(Collectors.toList()); + if (entities.isEmpty()) { + return new Gson().toJson(Collections.emptyList()); + } - // Bulk fetch beneficiary names - List benIdsToFetch = entities.stream() - .filter(e -> e.getName() == null && e.getBenId() != null && e.getBenId() > 0) - .map(IncentiveActivityRecord::getBenId) - .distinct() - .collect(Collectors.toList()); + // Bulk fetch beneficiary names + List benIdsToFetch = entities.stream() + .filter(e -> e.getName() == null && e.getBenId() != null && e.getBenId() > 0) + .map(IncentiveActivityRecord::getBenId) + .distinct() + .collect(Collectors.toList()); - Map benIdToNameMap = new HashMap<>(); - Map regIdToBenIdMap = new HashMap<>(); + Map benIdToNameMap = new HashMap<>(); + Map regIdToBenIdMap = new HashMap<>(); - if (benIdsToFetch != null && !benIdsToFetch.isEmpty()) { + if (!benIdsToFetch.isEmpty()) { - // First, fetch names using BeneficiaryId - List benDetails = - beneficiaryRepo.findBenNamesByBenIds(benIdsToFetch); + // First, fetch names using BeneficiaryId + List benDetails = + beneficiaryRepo.findBenNamesByBenIds(benIdsToFetch); - Set foundBenIds = new HashSet<>(); + Set foundBenIds = new HashSet<>(); - for (Object[] row : benDetails) { - Long benId = ((Number) row[0]).longValue(); + for (Object[] row : benDetails) { + Long benId = ((Number) row[0]).longValue(); - String first = row[1] != null ? row[1].toString() : ""; - String last = row[2] != null ? row[2].toString() : ""; + String first = row[1] != null ? row[1].toString() : ""; + String last = row[2] != null ? row[2].toString() : ""; - benIdToNameMap.put(benId, (first + " " + last).trim()); - foundBenIds.add(benId); - } + benIdToNameMap.put(benId, (first + " " + last).trim()); + foundBenIds.add(benId); + } - // IDs not found as BeneficiaryId - List unresolvedIds = benIdsToFetch.stream() - .filter(Objects::nonNull) - .filter(id -> !foundBenIds.contains(id)) - .distinct() - .toList(); + // IDs not found as BeneficiaryId + List unresolvedIds = benIdsToFetch.stream() + .filter(Objects::nonNull) + .filter(id -> !foundBenIds.contains(id)) + .distinct() + .toList(); - if (!unresolvedIds.isEmpty()) { + if (!unresolvedIds.isEmpty()) { - // Resolve BenRegId -> BeneficiaryId - List mappings = - beneficiaryRepo.getBenIdsFromRegIds(unresolvedIds); + // Resolve BenRegId -> BeneficiaryId + List mappings = + beneficiaryRepo.getBenIdsFromRegIds(unresolvedIds); - Set resolvedBenIds = new HashSet<>(); + Set resolvedBenIds = new HashSet<>(); - for (Object[] row : mappings) { - Long regId = ((Number) row[0]).longValue(); - Long benId = ((Number) row[1]).longValue(); + for (Object[] row : mappings) { + Long regId = ((Number) row[0]).longValue(); + Long benId = ((Number) row[1]).longValue(); - regIdToBenIdMap.put(regId, benId); - resolvedBenIds.add(benId); - } + regIdToBenIdMap.put(regId, benId); + resolvedBenIds.add(benId); + } - // Fetch names for resolved BeneficiaryIds - if (!resolvedBenIds.isEmpty()) { - List resolvedDetails = - beneficiaryRepo.findBenNamesByBenIds( - new ArrayList<>(resolvedBenIds)); + // Fetch names for resolved BeneficiaryIds + if (!resolvedBenIds.isEmpty()) { + List resolvedDetails = + beneficiaryRepo.findBenNamesByBenIds( + new ArrayList<>(resolvedBenIds)); - for (Object[] row : resolvedDetails) { - Long benId = ((Number) row[0]).longValue(); + for (Object[] row : resolvedDetails) { + Long benId = ((Number) row[0]).longValue(); - String first = row[1] != null ? row[1].toString() : ""; - String last = row[2] != null ? row[2].toString() : ""; + String first = row[1] != null ? row[1].toString() : ""; + String last = row[2] != null ? row[2].toString() : ""; - benIdToNameMap.put(benId, (first + " " + last).trim()); - } + benIdToNameMap.put(benId, (first + " " + last).trim()); } } } - // NEW: Bulk fetch verifier (supervisor) roles — replaces N+1 userRepo.getUserRole() calls - // Bulk fetch verifier (supervisor) roles — replaces N+1 userRepo.getUserRole() calls - List verifierIds = entities.stream() - .filter(e -> e.getName() == null && e.getVerifiedByUserId() != null) - .map(e -> e.getVerifiedByUserId().intValue()) // adjust if getVerifiedByUserId() already returns Integer - .distinct() - .collect(Collectors.toList()); + } - Map verifierRoleMap = new HashMap<>(); - if (!verifierIds.isEmpty()) { - List roles = userRepo.getUserRolesByIds(verifierIds); - for (UserServiceRoleDTO r : roles) { - verifierRoleMap.put(r.getUserId(), r); - } - } + // Bulk fetch verifier (supervisor) roles — replaces N+1 userRepo.getUserRole() calls + List verifierIds = entities.stream() + .filter(e -> e.getName() == null && e.getVerifiedByUserId() != null) + .map(IncentiveActivityRecord::getVerifiedByUserId) + .distinct() + .collect(Collectors.toList()); - // Map entities to DTOs - List dtos = entities.stream().map(entry -> { - if (entry.getName() == null) { - Long originalId = entry.getBenId(); + Map verifierRoleMap = new HashMap<>(); + if (!verifierIds.isEmpty()) { + List roles = userRepo.getUserRolesByIds(verifierIds); + for (UserServiceRoleDTO r : roles) { + verifierRoleMap.put(r.getUserId(), r); + } + } - if (originalId != null && originalId > 0) { + // Map entities to DTOs + List finalDtos = entities.stream().map(entry -> { + if (entry.getName() == null) { + Long originalId = entry.getBenId(); - Long actualBenId = regIdToBenIdMap.getOrDefault( - originalId, originalId); + if (originalId != null && originalId > 0) { - String name = benIdToNameMap.getOrDefault(actualBenId, ""); + Long actualBenId = regIdToBenIdMap.getOrDefault( + originalId, originalId); - entry.setBenId(actualBenId); - entry.setName(name); + String name = benIdToNameMap.getOrDefault(actualBenId, ""); - if (isCG) { - entry.setIsEligible(true); - } + entry.setBenId(actualBenId); + entry.setName(name); - } else { - entry.setName(""); + if (isCG) { + entry.setIsEligible(true); } - if (entry.getVerifiedByUserId() != null) { - UserServiceRoleDTO role = verifierRoleMap.get(entry.getVerifiedByUserId()); - if (role != null) { - entry.setSupervisorRole(role.getRoleName()); - entry.setVerifiedByUserName(role.getName()); - } - } + } else { + entry.setName(""); + } - if (entry.getAshaId() != null) { - if (entry.getCreatedBy() == null) { - entry.setCreatedBy(userName); - } - if (entry.getUpdatedBy() == null) { - entry.setUpdatedBy(userName); - } - if (entry.getIsEligible() == null) { - entry.setIsEligible(true); - } + if (entry.getVerifiedByUserId() != null) { + UserServiceRoleDTO role = verifierRoleMap.get(entry.getVerifiedByUserId()); + if (role != null) { + entry.setSupervisorRole(role.getRoleName()); + entry.setVerifiedByUserName(role.getName()); } } - return modelMapper.map(entry, IncentiveRecordDTO.class); - }).collect(Collectors.toList()); - finalDtos.addAll(dtos); - page++; - - } while (pageResult.hasNext()); + if (entry.getAshaId() != null) { + if (entry.getCreatedBy() == null) { + entry.setCreatedBy(userName); + } + if (entry.getUpdatedBy() == null) { + entry.setUpdatedBy(userName); + } + if (entry.getIsEligible() == null) { + entry.setIsEligible(true); + } + } + } + return modelMapper.map(entry, IncentiveRecordDTO.class); + }).collect(Collectors.toList()); Gson gson = new GsonBuilder().setDateFormat("MMM dd, yyyy h:mm:ss a").create(); return gson.toJson(finalDtos); @@ -420,7 +421,10 @@ public void resetRejectedIncentivesForPreviousMonths() { @Transactional(readOnly = true) public String getAllIncentivesGroupedSummary(GetBenRequestHandler request, Integer userId) { - String roleName = userService.getUserDetail(userId).getRoleName(); + List callerRoles = userRepo.getUserRole(userId); + String roleName = (callerRoles != null && !callerRoles.isEmpty()) + ? callerRoles.get(0).getRoleName() + : userService.getUserDetail(userId).getRoleName(); Integer reqStatus = request.getApprovalStatus(); Integer stateId = userRepo.getUserRole(request.getUserId()).get(0).getStateId(); @@ -428,24 +432,30 @@ public String getAllIncentivesGroupedSummary(GetBenRequestHandler request, Integ logger.info("Request getAllIncentivesGroupedSummary: {}", reqStatus); + LocalDate monthStart = LocalDate.of(request.getYear(), request.getMonth(), 1); + Timestamp startTs = Timestamp.valueOf(monthStart.atStartOfDay()); + Timestamp endTs = Timestamp.valueOf(monthStart.plusMonths(1).atStartOfDay()); + + // Filter by date range and isClaimed=true in DB instead of loading all historical records List records = - recordRepo.findRecordsByAsha(request.getUserId()) + recordRepo.findClaimedRecordsByAshaAndCreatedDateRange(request.getUserId(), startTs, endTs) .stream() - .filter(r -> r.getCreatedDate() != null) - .filter(r -> { - LocalDateTime created = r.getCreatedDate().toLocalDateTime(); - return created.getMonthValue() == request.getMonth() - && created.getYear() == request.getYear(); - }) - .filter(r -> Boolean.TRUE.equals(r.getIsClaimed())) .filter(r -> matchesRequestedStatus(r.getApprovalStatus(), reqStatus)) .collect(Collectors.toList()); + if (records.isEmpty()) { + return new Gson().toJson(Collections.emptyList()); + } + List activityIds = records.stream() .map(IncentiveActivityRecord::getActivityId) + .filter(Objects::nonNull) + .distinct() .collect(Collectors.toList()); - Set validActivityIds = incentivesRepo.findValidActivityIds(activityIds, isCG); + Set validActivityIds = activityIds.isEmpty() + ? Collections.emptySet() + : incentivesRepo.findValidActivityIds(activityIds, isCG); if (validActivityIds == null) { validActivityIds = Collections.emptySet(); } @@ -547,6 +557,12 @@ public String getAllIncentivesGroupedSummary(GetBenRequestHandler request, Integ Map> grouped = records.stream().collect(Collectors.groupingBy(IncentiveActivityRecord::getActivityId)); + // Bulk fetch all IncentiveActivity masters in ONE query instead of N findById() calls in loop + Map activityByIdMap = grouped.isEmpty() + ? Collections.emptyMap() + : incentivesRepo.findAllById(grouped.keySet()).stream() + .collect(Collectors.toMap(IncentiveActivity::getId, Function.identity())); + List> result = new ArrayList<>(); for (Map.Entry> entry : grouped.entrySet()) { @@ -554,7 +570,7 @@ public String getAllIncentivesGroupedSummary(GetBenRequestHandler request, Integ Long activityId = entry.getKey(); List list = entry.getValue(); - IncentiveActivity activity = incentivesRepo.findById(activityId).orElse(null); + IncentiveActivity activity = activityByIdMap.get(activityId); if (activity == null) continue; IncentiveActivityRecord latest = list.stream() @@ -648,19 +664,15 @@ public String getAllIncentivesGroupedActivity(GetBenRequestHandler request) { Timestamp startTs = Timestamp.valueOf(startDate.atStartOfDay()); Timestamp endTs = Timestamp.valueOf(endDate.atStartOfDay()); + // Filter by ashaId, activityId, and date range in DB instead of loading all records for the ASHA List records = - recordRepo.findRecordsByAsha(request.getUserId()) - .stream() - .filter(r -> - r.getActivityId() != null - && Objects.equals( - r.getActivityId(), - request.getActivityId() - ) - && r.getCreatedDate() != null - && !r.getCreatedDate().before(startTs) - && r.getCreatedDate().before(endTs) + recordRepo.findRecordsByAshaAndActivityAndCreatedDateRange( + request.getUserId(), + request.getActivityId(), + startTs, + endTs ) + .stream() .filter(r -> { Integer requestedStatus = @@ -743,7 +755,9 @@ public String getAllIncentivesGroupedActivity(GetBenRequestHandler request) { incentivesRepo.findById(request.getActivityId()).orElse(null); String groupName = activity != null ? activity.getGroup() : ""; - String groupCategoryName = activity != null ? activity.getGroupCategoryName() :activity.getGroup(); + String groupCategoryName = activity != null + ? (activity.getGroupCategoryName() != null ? activity.getGroupCategoryName() : activity.getGroup()) + : ""; String description = activity != null ? activity.getDescription() : ""; // 🔹 Map result @@ -888,11 +902,13 @@ public String updateClaimStatus(Integer ashaId, Integer month, Integer year, Boo if (updated > 0) { + Integer supervisorUserId = dashboardRepo.getSupervisorUserIdByAshaId(ashaId); + Map data = new HashMap<>(); data.put("notification_type", "INCENTIVE_APPROVAL"); data.put("nav_id", "INCENTIVE_HISTORY"); data.put("sender_user_id", String.valueOf(ashaId)); - data.put("receiver_user_id", String.valueOf(dashboardRepo.getSupervisorUserIdByAshaId(ashaId))); + data.put("receiver_user_id", String.valueOf(supervisorUserId)); data.put("month", String.valueOf(month)); data.put("year", String.valueOf(year)); data.put("approval_status", String.valueOf(102)); @@ -912,11 +928,16 @@ public String updateClaimStatus(Integer ashaId, Integer month, Integer year, Boo body, "INCENTIVE_CLAIMED", "INCENTIVE", - dashboardRepo.getSupervisorUserIdByAshaId(ashaId) + supervisorUserId ); + List ashaRoles = userRepo.getUserRole(ashaId); + String ashaName = (ashaRoles != null && !ashaRoles.isEmpty()) + ? ashaRoles.get(0).getName() + : userService.getUserDetail(ashaId).getName(); + String supervisorBody = - userService.getUserDetail(ashaId).getName() + ashaName + " successfully claimed incentive for month of " + Month.of(month).name() + " " @@ -929,7 +950,7 @@ public String updateClaimStatus(Integer ashaId, Integer month, Integer year, Boo supervisorBody, "INCENTIVE_CLAIMED", "INCENTIVE", - dashboardRepo.getSupervisorUserIdByAshaId(ashaId) + supervisorUserId ); } } @@ -942,69 +963,125 @@ public String updateClaimStatus(Integer ashaId, Integer month, Integer year, Boo } } - private void incentiveOfNcdReferal(Integer ashaId, Integer stateId, GetBenRequestHandler request) { + private void incentiveOfNcdReferal(Integer ashaId, Integer stateId, GetBenRequestHandler request, String userName) { try { - String userName = userRepo.getUserNamedByUserId(ashaId); + if (userName == null) { + userName = userRepo.getUserNamedByUserId(ashaId); + } + if (userName == null) { + return; + } String groupName = resolveGroupName(stateId); Map activityMap = incentivesRepo.findIncentiveMasterByNameAndGroup( List.of("NCD_POP_ENUMERATION", "HWC_REFERRAL_10_CASES"), groupName - ).stream().collect(Collectors.toMap(IncentiveActivity::getName, Function.identity())); + ).stream().collect(Collectors.toMap(IncentiveActivity::getName, Function.identity(), (a, b) -> a)); IncentiveActivity ncdPopEnumeration = activityMap.get("NCD_POP_ENUMERATION"); - IncentiveActivity hwcReferralEnumeration = activityMap.get("HWC_REFERRAL_10_CASES"); - CompletableFuture> benReferFuture = - CompletableFuture.supplyAsync(() -> benReferDetailsRepo.findByCreatedBy(userName)); - CompletableFuture> cbacFuture = - CompletableFuture.supplyAsync(() -> cbacIemrDetailsRepo.findByCreatedBy(userName)); - - List cbacDetailsImer = cbacFuture.get(); - - List recordsToSave = new ArrayList<>(); - + // 1. Optimize NCD_POP_ENUMERATION: fetch only (beneficiaryRegId, createdDate) instead of all 65 columns, + // check existing records in 1 bulk query instead of N individual queries, and batch insert. + if (ncdPopEnumeration != null) { + List cbacRows = cbacIemrDetailsRepo.findBenRegIdAndCreatedDateByCreatedBy(userName); + if (cbacRows != null && !cbacRows.isEmpty()) { + List existingRows = recordRepo.findBenIdAndStartDateByActivityIdAndAshaId( + ncdPopEnumeration.getId(), ashaId); + Set existingMonthKeys = new HashSet<>(); + if (existingRows != null) { + for (Object[] row : existingRows) { + Long existingBenId = row[0] != null ? ((Number) row[0]).longValue() : null; + Timestamp existingStart = (Timestamp) row[1]; + if (existingBenId != null && existingStart != null) { + LocalDateTime ldt = existingStart.toLocalDateTime(); + existingMonthKeys.add(existingBenId + "_" + ldt.getYear() + "_" + ldt.getMonthValue()); + } + } + } - if (ncdPopEnumeration != null && !cbacDetailsImer.isEmpty()) { + Timestamp nowTs = Timestamp.valueOf(LocalDateTime.now()); + Long rate = Long.valueOf(ncdPopEnumeration.getRate()); + List recordsToSave = new ArrayList<>(); + + for (Object[] cbacRow : cbacRows) { + if (cbacRow == null || cbacRow[0] == null || cbacRow[1] == null) continue; + Long benId = ((Number) cbacRow[0]).longValue(); + Timestamp createdDate = (Timestamp) cbacRow[1]; + LocalDateTime ldt = createdDate.toLocalDateTime(); + String monthKey = benId + "_" + ldt.getYear() + "_" + ldt.getMonthValue(); + + if (existingMonthKeys.add(monthKey)) { + IncentiveActivityRecord record = new IncentiveActivityRecord(); + record.setActivityId(ncdPopEnumeration.getId()); + record.setCreatedDate(createdDate); + record.setCreatedBy(userName); + record.setStartDate(createdDate); + record.setEndDate(createdDate); + record.setUpdatedDate(nowTs); + record.setUpdatedBy(userName); + record.setBenId(benId); + record.setAshaId(ashaId); + record.setAmount(rate); + recordsToSave.add(record); + } + } - final IncentiveActivity activity = ncdPopEnumeration; - cbacDetailsImer.forEach(cbacDetailsImer1 -> { - if(cbacDetailsImer1!=null){ - addNCDandCBACIncentiveRecord(activity, ashaId, cbacDetailsImer1.getBeneficiaryRegId(), cbacDetailsImer1.getCreatedDate(), userName); + if (!recordsToSave.isEmpty()) { + recordRepo.saveAll(recordsToSave); } - }); + } } + // 2. Optimize HWC_REFERRAL_10_CASES: check if monthly incentive already exists FIRST, + // and use index-friendly date range query instead of MONTH()/YEAR() functions. if (hwcReferralEnumeration != null) { - logger.info("hwcReferralEnumeration: {}", hwcReferralEnumeration.getDescription()); - logger.info("userName={}", userName); - - LocalDate requestDate = request.getFromDate() - .toInstant() - .atZone(ZoneId.systemDefault()) - .toLocalDate(); - - Long referralCount = benReferDetailsRepo.countMonthlyReferrals( - userName, - requestDate.getMonthValue(), - requestDate.getYear() - ); + LocalDate now = LocalDate.now(); + Timestamp startOfMonth = Timestamp.valueOf(now.withDayOfMonth(1).atStartOfDay()); + Timestamp endOfMonth = Timestamp.valueOf(now.withDayOfMonth(now.lengthOfMonth()).atTime(23, 59, 59)); - logger.info("referralCount: {}", referralCount); + IncentiveActivityRecord existingHwcRecord = recordRepo.findRecordByActivityIdCreatedDateBenId( + hwcReferralEnumeration.getId(), + 0L, + ashaId, + startOfMonth, + endOfMonth + ); - if (referralCount >= 10) { - addHwcReferalAshaIncentiveRecord( - hwcReferralEnumeration, - ashaId, - userName + if (existingHwcRecord == null) { + LocalDate requestDate = (request != null && request.getFromDate() != null) + ? request.getFromDate().toInstant().atZone(ZoneId.systemDefault()).toLocalDate() + : now; + LocalDate reqMonthFirst = requestDate.withDayOfMonth(1); + Timestamp reqStart = Timestamp.valueOf(reqMonthFirst.atStartOfDay()); + Timestamp reqEnd = Timestamp.valueOf(reqMonthFirst.plusMonths(1).atStartOfDay()); + + Long referralCount = benReferDetailsRepo.countMonthlyReferralsByDateRange( + userName, + reqStart, + reqEnd ); - } - } - // ---- Single batch insert instead of N individual saves ---- - if (!recordsToSave.isEmpty()) { - recordRepo.saveAll(recordsToSave); + logger.info("referralCount: {}", referralCount); + + if (referralCount != null && referralCount >= 10) { + Timestamp timestamp = Timestamp.valueOf(LocalDateTime.now()); + IncentiveActivityRecord record = new IncentiveActivityRecord(); + record.setActivityId(hwcReferralEnumeration.getId()); + record.setCreatedDate(timestamp); + record.setCreatedBy(userName); + record.setStartDate(timestamp); + record.setEndDate(timestamp); + record.setUpdatedDate(timestamp); + record.setUpdatedBy(userName); + record.setBenId(0L); + record.setAshaId(ashaId); + record.setIsEligible(true); + record.setIsDefaultActivity(false); + record.setAmount(Long.valueOf(hwcReferralEnumeration.getRate())); + recordRepo.save(record); + } + } } } catch (Exception e) { @@ -1012,39 +1089,8 @@ private void incentiveOfNcdReferal(Integer ashaId, Integer stateId, GetBenReques } } - // Helper — record banana - private void addNCDandCBACIncentiveRecord(IncentiveActivity activity, Integer ashaId, - Long benId, Timestamp createdDate, String userName) { - - String lockKey = activity.getId() + "_" + benId + "_" + createdDate; - - Object lock = lockMap.computeIfAbsent(lockKey, k -> new Object()); - synchronized (lock){ - IncentiveActivityRecord incentiveActivityRecord = recordRepo.findRecordByActivityIdCreatedDateBenId(activity.getId(),createdDate,benId,ashaId); - if(incentiveActivityRecord==null){ - Timestamp now = Timestamp.valueOf(LocalDateTime.now()); - IncentiveActivityRecord record = new IncentiveActivityRecord(); - record.setActivityId(activity.getId()); - record.setCreatedDate(createdDate); - record.setCreatedBy(userName); - record.setStartDate(createdDate); - record.setEndDate(createdDate); - record.setUpdatedDate(now); - record.setUpdatedBy(userName); - record.setBenId(benId); - record.setAshaId(ashaId); - record.setAmount(Long.valueOf(activity.getRate())); - recordRepo.save(record); - } - lockMap.remove(lockKey, lock); - - } - - - } - private String resolveGroupName(Integer stateId) { - if (stateId.equals(StateCode.CG.getStateCode())) { + if (stateId != null && stateId.equals(StateCode.CG.getStateCode())) { logger.info("State id: {}", StateCode.CG.getStateCode()); return GroupName.ACTIVITY.getDisplayName(); } @@ -1052,198 +1098,159 @@ private String resolveGroupName(Integer stateId) { return GroupName.NCD.getDisplayName(); } - private void checkMonthlyAshaIncentive(Integer ashaId) { - + private void checkMonthlyAshaIncentive(Integer ashaId, String userName) { try { - String userName = userRepo.getUserNamedByUserId(ashaId); - - IncentiveActivity MOBILEBILLREIMB_ACTIVITY = incentivesRepo.findIncentiveMasterByNameAndGroup("MOBILE_BILL_REIMB", GroupName.OTHER_INCENTIVES.getDisplayName()); - IncentiveActivity ADDITIONAL_ASHA_INCENTIVE = incentivesRepo.findIncentiveMasterByNameAndGroup("ADDITIONAL_ASHA_INCENTIVE", GroupName.ADDITIONAL_INCENTIVE.getDisplayName()); - IncentiveActivity ASHA_MONTHLY_ROUTINE = incentivesRepo.findIncentiveMasterByNameAndGroup("ASHA_MONTHLY_ROUTINE", GroupName.ASHA_MONTHLY_ROUTINE.getDisplayName()); - if (MOBILEBILLREIMB_ACTIVITY != null) { - addMonthlyAshaIncentiveRecord(MOBILEBILLREIMB_ACTIVITY, ashaId, userName,false); - } - if (ADDITIONAL_ASHA_INCENTIVE != null) { - addMonthlyAshaIncentiveRecord(ADDITIONAL_ASHA_INCENTIVE, ashaId, userName,false); - + if (userName == null) { + userName = userRepo.getUserNamedByUserId(ashaId); } - - if (ASHA_MONTHLY_ROUTINE != null) { - addMonthlyAshaIncentiveRecord(ASHA_MONTHLY_ROUTINE, ashaId, userName,false); - + List candidates = incentivesRepo.findByNamesAndIsDeletedFalse( + List.of("MOBILE_BILL_REIMB", "ADDITIONAL_ASHA_INCENTIVE", "ASHA_MONTHLY_ROUTINE") + ); + List validActivities = new ArrayList<>(); + for (IncentiveActivity act : candidates) { + if (("MOBILE_BILL_REIMB".equals(act.getName()) && GroupName.OTHER_INCENTIVES.getDisplayName().equals(act.getGroup())) + || ("ADDITIONAL_ASHA_INCENTIVE".equals(act.getName()) && GroupName.ADDITIONAL_INCENTIVE.getDisplayName().equals(act.getGroup())) + || ("ASHA_MONTHLY_ROUTINE".equals(act.getName()) && GroupName.ASHA_MONTHLY_ROUTINE.getDisplayName().equals(act.getGroup()))) { + validActivities.add(act); + } } + addMonthlyAshaIncentiveRecordsBatch(validActivities, ashaId, userName, Collections.emptySet()); } catch (Exception e) { - logger.error("Error in addMonthlyAshaIncentiveRecord", e); - + logger.error("Error in checkMonthlyAshaIncentive", e); } - } - private void checkMonthlyAshaIncentiveForCg(Integer ashaId) { + private void checkMonthlyAshaIncentiveForCg(Integer ashaId, String userName) { try { - String userName = userRepo.getUserNamedByUserId(ashaId); - - IncentiveActivity MONTHLY_HONORARIUM = incentivesRepo.findIncentiveMasterByNameAndGroup("MONTHLY_HONORARIUM", GroupName.ACTIVITY.getDisplayName()); - IncentiveActivity MITANIN_REGISTER_5_INFO_FILL = incentivesRepo.findIncentiveMasterByNameAndGroup("MITANIN_REGISTER_5_INFO_FILL", GroupName.ACTIVITY.getDisplayName()); - IncentiveActivity MITANIN_REGISTER = incentivesRepo.findIncentiveMasterByNameAndGroup("MITANIN_REGISTER", GroupName.ACTIVITY.getDisplayName()); - if (MONTHLY_HONORARIUM != null) { - addMonthlyAshaIncentiveRecord(MONTHLY_HONORARIUM, ashaId, userName,true); - } - if (MITANIN_REGISTER_5_INFO_FILL != null) { - addMonthlyAshaIncentiveRecord(MITANIN_REGISTER_5_INFO_FILL, ashaId, userName,false); - - } - - if (MITANIN_REGISTER != null) { - addMonthlyAshaIncentiveRecord(MITANIN_REGISTER, ashaId, userName,false); - + if (userName == null) { + userName = userRepo.getUserNamedByUserId(ashaId); } + List validActivities = incentivesRepo.findIncentiveMasterByNameAndGroup( + List.of("MONTHLY_HONORARIUM", "MITANIN_REGISTER_5_INFO_FILL", "MITANIN_REGISTER"), + GroupName.ACTIVITY.getDisplayName() + ); + addMonthlyAshaIncentiveRecordsBatch(validActivities, ashaId, userName, Set.of("MONTHLY_HONORARIUM")); } catch (Exception e) { - logger.error("Error in addMonthlyAshaIncentiveRecord", e); - + logger.error("Error in checkMonthlyAshaIncentiveForCg", e); } - } - private void addMonthlyAshaIncentiveRecord(IncentiveActivity incentiveActivity, Integer ashaId, String userName,Boolean isApproved) { + private void addMonthlyAshaIncentiveRecordsBatch( + List activities, + Integer ashaId, + String userName, + Set preApprovedNames) { + if (activities == null || activities.isEmpty()) { + return; + } try { + LocalDate now = LocalDate.now(); Timestamp timestamp = Timestamp.valueOf(LocalDateTime.now()); + Timestamp startOfMonth = Timestamp.valueOf(now.withDayOfMonth(1).atStartOfDay()); + Timestamp endOfMonth = Timestamp.valueOf(now.withDayOfMonth(now.lengthOfMonth()).atTime(23, 59, 59)); - Timestamp startOfMonth = Timestamp.valueOf(LocalDate.now().withDayOfMonth(1).atStartOfDay()); - Timestamp endOfMonth = Timestamp.valueOf(LocalDate.now().withDayOfMonth(LocalDate.now().lengthOfMonth()).atTime(23, 59, 59)); - - IncentiveActivityRecord record = recordRepo.findRecordByActivityIdCreatedDateBenId( - incentiveActivity.getId(), - 0L, - ashaId, - startOfMonth, - endOfMonth - ); - + List activityIds = activities.stream() + .map(IncentiveActivity::getId) + .filter(Objects::nonNull) + .collect(Collectors.toList()); - if (record == null) { - record = new IncentiveActivityRecord(); - record.setActivityId(incentiveActivity.getId()); - record.setCreatedDate(timestamp); - record.setCreatedBy(userName); - record.setStartDate(timestamp); - record.setEndDate(timestamp); - record.setUpdatedDate(timestamp); - record.setUpdatedBy(userName); - record.setBenId(0L); - record.setAshaId(ashaId); - record.setIsEligible(true); - record.setIsDefaultActivity(true); - record.setIsApproved(isApproved); - record.setAmount(Long.valueOf(incentiveActivity.getRate())); - recordRepo.save(record); + if (activityIds.isEmpty()) { + return; } - } catch (Exception e) { - logger.error("Error in addMonthlyAshaIncentiveRecord", e); - - } - } - - private void addHwcReferalAshaIncentiveRecord(IncentiveActivity incentiveActivity, Integer ashaId, String userName) { - try { - Timestamp timestamp = Timestamp.valueOf(LocalDateTime.now()); - - Timestamp startOfMonth = Timestamp.valueOf(LocalDate.now().withDayOfMonth(1).atStartOfDay()); - Timestamp endOfMonth = Timestamp.valueOf(LocalDate.now().withDayOfMonth(LocalDate.now().lengthOfMonth()).atTime(23, 59, 59)); - - IncentiveActivityRecord record = recordRepo.findRecordByActivityIdCreatedDateBenId( - incentiveActivity.getId(), - 0L, - ashaId, - startOfMonth, - endOfMonth + Set existingIds = new HashSet<>( + recordRepo.findExistingActivityIdsByMonth(activityIds, 0L, ashaId, startOfMonth, endOfMonth) ); + List newRecords = new ArrayList<>(); + for (IncentiveActivity activity : activities) { + if (activity != null && !existingIds.contains(activity.getId())) { + IncentiveActivityRecord record = new IncentiveActivityRecord(); + record.setActivityId(activity.getId()); + record.setCreatedDate(timestamp); + record.setCreatedBy(userName); + record.setStartDate(timestamp); + record.setEndDate(timestamp); + record.setUpdatedDate(timestamp); + record.setUpdatedBy(userName); + record.setBenId(0L); + record.setAshaId(ashaId); + record.setIsEligible(true); + record.setIsDefaultActivity(true); + record.setIsApproved(preApprovedNames != null && preApprovedNames.contains(activity.getName())); + record.setAmount(Long.valueOf(activity.getRate())); + newRecords.add(record); + } + } - if (record == null) { - record = new IncentiveActivityRecord(); - record.setActivityId(incentiveActivity.getId()); - record.setCreatedDate(timestamp); - record.setCreatedBy(userName); - record.setStartDate(timestamp); - record.setEndDate(timestamp); - record.setUpdatedDate(timestamp); - record.setUpdatedBy(userName); - record.setBenId(0L); - record.setAshaId(ashaId); - record.setIsEligible(true); - record.setIsDefaultActivity(false); - record.setAmount(Long.valueOf(incentiveActivity.getRate())); - recordRepo.save(record); + if (!newRecords.isEmpty()) { + recordRepo.saveAll(newRecords); } } catch (Exception e) { - logger.error("Error in addMonthlyAshaIncentiveRecord", e); - + logger.error("Error in addMonthlyAshaIncentiveRecordsBatch", e); } - } - private void addIncentiveForIronTablets(Integer userId) { - IncentiveActivity incentiveActivityAM = incentivesRepo.findIncentiveMasterByNameAndGroup("NATIONAL_IRON_PLUS", GroupName.CHILD_HEALTH.getDisplayName()); - IncentiveActivity incentiveActivityCG = incentivesRepo.findIncentiveMasterByNameAndGroup("NATIONAL_IRON_PLUS", GroupName.ACTIVITY.getDisplayName()); - - String userName = userService.getUserDetail(userId).getUserName(); - Integer stateId = userService.getUserDetail(userId).getStateId(); - if (userName != null) { - List eligibleCoupleRegisters = eligibleCoupleRegisterRepo.findByCreatedBy(userName); - List ifaFormSubmissionData = ifaFormSubmissionRepository.findByUserId(userId); - logger.info("eligibleCoupleRegisters :" + eligibleCoupleRegisters.size()); - logger.info("ifaFormSubmissionData :" + ifaFormSubmissionData.size()); - - if (!eligibleCoupleRegisters.isEmpty() && !ifaFormSubmissionData.isEmpty()) { - - int percentage = (int) (((double) ifaFormSubmissionData.size() - / eligibleCoupleRegisters.size()) * 100); - - logger.info("IFA Count : {}", ifaFormSubmissionData.size()); - logger.info("Eligible Couple Count : {}", eligibleCoupleRegisters.size()); - logger.info("Percentage : {}", percentage); + private void addIncentiveForIronTablets(Integer userId, String userName, Integer stateId) { + if (userName == null || stateId == null) { + return; + } - if (percentage >= 50) { + IncentiveActivity targetActivity = null; + if (stateId.equals(StateCode.AM.getStateCode())) { + targetActivity = incentivesRepo.findIncentiveMasterByNameAndGroup("NATIONAL_IRON_PLUS", GroupName.CHILD_HEALTH.getDisplayName()); + } else if (stateId.equals(StateCode.CG.getStateCode())) { + targetActivity = incentivesRepo.findIncentiveMasterByNameAndGroup("NATIONAL_IRON_PLUS", GroupName.ACTIVITY.getDisplayName()); + } - if (stateId.equals(StateCode.AM.getStateCode())) { - addIFAIncentive( - ifaFormSubmissionData.get(ifaFormSubmissionData.size() - 1), - incentiveActivityAM, - userId, - userName); - } + if (targetActivity == null) { + return; + } - if (stateId.equals(StateCode.CG.getStateCode())) { - addIFAIncentive( - ifaFormSubmissionData.get(ifaFormSubmissionData.size() - 1), - incentiveActivityCG, - userId, - userName); - } - } - } + LocalDate now = LocalDate.now(); + Timestamp startOfMonth = Timestamp.valueOf(now.withDayOfMonth(1).atStartOfDay()); + Timestamp endOfMonth = Timestamp.valueOf(now.withDayOfMonth(now.lengthOfMonth()).atTime(23, 59, 59)); + + // Check if IFA incentive record already exists for this month BEFORE querying EC & IFA tables + IncentiveActivityRecord existingRecord = recordRepo.findRecordByActivityIdCreatedDateBenId( + targetActivity.getId(), + 0L, + userId, + startOfMonth, + endOfMonth + ); + if (existingRecord != null) { + return; + } + // Use COUNT(*) queries instead of loading full entities (and LONGTEXT photos) into memory + long ecCount = eligibleCoupleRegisterRepo.countByCreatedBy(userName); + if (ecCount <= 0) { + return; + } + long ifaCount = ifaFormSubmissionRepository.countByUserId(userId); + if (ifaCount <= 0) { + return; } + int percentage = (int) (((double) ifaCount / ecCount) * 100); + logger.info("IFA Count : {}, Eligible Couple Count : {}, Percentage : {}", ifaCount, ecCount, percentage); + if (percentage >= 50) { + addIFAIncentive(targetActivity, userId, userName, startOfMonth, endOfMonth); + } } - private void addIFAIncentive(IFAFormSubmissionData ifaFormSubmissionData, IncentiveActivity incentiveActivityAM, Integer userId, String userName) { - Timestamp startOfMonth = Timestamp.valueOf(LocalDate.now().withDayOfMonth(1).atStartOfDay()); - Timestamp endOfMonth = Timestamp.valueOf(LocalDate.now().withDayOfMonth(LocalDate.now().lengthOfMonth()).atTime(23, 59, 59)); - - String lockKey = incentiveActivityAM.getId() + "_" + 0 + "_" + startOfMonth; - + private void addIFAIncentive(IncentiveActivity incentiveActivity, Integer userId, String userName, + Timestamp startOfMonth, Timestamp endOfMonth) { + String lockKey = incentiveActivity.getId() + "_" + 0 + "_" + startOfMonth; Object lock = lockMap.computeIfAbsent(lockKey, k -> new Object()); Timestamp timestamp = Timestamp.valueOf(LocalDateTime.now()); - logger.info("IFA incentive"); - synchronized (lock){ + synchronized (lock) { IncentiveActivityRecord incentiveActivityRecord = recordRepo.findRecordByActivityIdCreatedDateBenId( - incentiveActivityAM.getId(), + incentiveActivity.getId(), 0L, userId, startOfMonth, @@ -1251,26 +1258,21 @@ private void addIFAIncentive(IFAFormSubmissionData ifaFormSubmissionData, Incent ); if (incentiveActivityRecord == null) { incentiveActivityRecord = new IncentiveActivityRecord(); - incentiveActivityRecord.setActivityId(incentiveActivityAM.getId()); + incentiveActivityRecord.setActivityId(incentiveActivity.getId()); incentiveActivityRecord.setCreatedDate(timestamp); incentiveActivityRecord.setStartDate(timestamp); incentiveActivityRecord.setEndDate(timestamp); incentiveActivityRecord.setUpdatedDate(timestamp); - incentiveActivityRecord.setUpdatedBy(ifaFormSubmissionData.getUserName()); - incentiveActivityRecord.setCreatedBy(ifaFormSubmissionData.getUserName()); + incentiveActivityRecord.setUpdatedBy(userName); + incentiveActivityRecord.setCreatedBy(userName); incentiveActivityRecord.setBenId(0L); incentiveActivityRecord.setAshaId(userId); - incentiveActivityRecord.setAmount(Long.valueOf(incentiveActivityAM.getRate())); + incentiveActivityRecord.setAmount(Long.valueOf(incentiveActivity.getRate())); recordRepo.save(incentiveActivityRecord); logger.info("saved IFA incentive"); - } - } lockMap.remove(lockKey, lock); - - } - } diff --git a/src/main/java/com/iemr/flw/service/impl/VillageLevelFormServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/VillageLevelFormServiceImpl.java index ff1482e08..52a33bc0a 100644 --- a/src/main/java/com/iemr/flw/service/impl/VillageLevelFormServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/VillageLevelFormServiceImpl.java @@ -233,8 +233,8 @@ private Boolean saveVhndFormData(VHNDFormDTO vhndFormDTO, Integer userID) { VHNDForm vhndForm = new VHNDForm(); vhndForm.setUserId(userID); vhndForm.setVhndDate(vhndFormDTO.getVhndDate()); - vhndForm.setImage2(vhndFormDTO.getImage2() != null ? vhndFormDTO.getImage2() : ""); - vhndForm.setImage1(vhndFormDTO.getImage1() != null ? vhndFormDTO.getImage1() : ""); + vhndForm.setImage2(vhndFormDTO.getImage2() != null ? vhndFormDTO.getImage2() : null); + vhndForm.setImage1(vhndFormDTO.getImage1() != null ? vhndFormDTO.getImage1() : null); vhndForm.setPlace(vhndFormDTO.getPlace() != null ? vhndFormDTO.getPlace() : ""); vhndForm.setNoOfBeneficiariesAttended( diff --git a/src/test/java/com/iemr/flw/controller/BadgeControllerTest.java b/src/test/java/com/iemr/flw/controller/BadgeControllerTest.java new file mode 100644 index 000000000..4e61d0b52 --- /dev/null +++ b/src/test/java/com/iemr/flw/controller/BadgeControllerTest.java @@ -0,0 +1,89 @@ +package com.iemr.flw.controller; + +import com.iemr.flw.dto.iemr.BadgeEarnedDTO; +import com.iemr.flw.service.BadgeService; +import com.iemr.flw.utils.JwtUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class BadgeControllerTest { + + private MockMvc mockMvc; + + @Mock + private BadgeService badgeService; + + @Mock + private JwtUtil jwtUtil; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + mockMvc = MockMvcBuilders.standaloneSetup(new BadgeController(badgeService, jwtUtil)).build(); + } + + @Test + void config_returnsKeyValueMapUnderConfig() throws Exception { + when(badgeService.getConfig()).thenReturn(Map.of("milestones.steady_syncer", "2,4,6,8")); + + mockMvc.perform(get("/badges/config")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.config['milestones.steady_syncer']").value("2,4,6,8")); + } + + @Test + void earned_usesUserIdFromJwtNotFromBody() throws Exception { + when(jwtUtil.extractUserId("tok")).thenReturn(960); + when(badgeService.saveEarned(eq(960), any())).thenReturn(1); + + mockMvc.perform(post("/badges/earned") + .header("JwtToken", "tok") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"userId\":12345,\"badges\":[{\"badgeId\":\"steady_syncer\",\"level\":2,\"earnedAt\":1}]}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.inserted").value(1)); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(badgeService).saveEarned(eq(960), captor.capture()); + assertEquals("steady_syncer", captor.getValue().get(0).getBadgeId()); + } + + @Test + void getEarned_returnsListUnderEarned() throws Exception { + when(jwtUtil.extractUserId("tok")).thenReturn(960); + when(badgeService.getEarned(960)).thenReturn(List.of(new BadgeEarnedDTO("timely_reporter", 1, 5L, ""))); + + mockMvc.perform(get("/badges/earned").header("JwtToken", "tok")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.earned[0].badgeId").value("timely_reporter")) + .andExpect(jsonPath("$.earned[0].level").value(1)); + } + + @Test + void invalidJwt_returns500WithErrorShape() throws Exception { + when(jwtUtil.extractUserId("bad")).thenThrow(new RuntimeException("Invalid JWT token.")); + + mockMvc.perform(get("/badges/earned").header("JwtToken", "bad")) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.status").value("Error")); + } +} diff --git a/src/test/java/com/iemr/flw/service/BadgeServiceTest.java b/src/test/java/com/iemr/flw/service/BadgeServiceTest.java new file mode 100644 index 000000000..217ac1eeb --- /dev/null +++ b/src/test/java/com/iemr/flw/service/BadgeServiceTest.java @@ -0,0 +1,91 @@ +package com.iemr.flw.service; + +import com.iemr.flw.domain.iemr.BadgeEarned; +import com.iemr.flw.dto.iemr.BadgeEarnedDTO; +import com.iemr.flw.repo.iemr.BadgeConfigRepo; +import com.iemr.flw.repo.iemr.BadgeEarnedRepo; +import com.iemr.flw.repo.iemr.BadgeStreakFreezeRepo; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.ArgumentCaptor; +import org.mockito.MockitoAnnotations; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class BadgeServiceTest { + + @Mock private BadgeConfigRepo configRepo; + @Mock private BadgeStreakFreezeRepo freezeRepo; + @Mock private BadgeEarnedRepo earnedRepo; + + private BadgeService service; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + service = new BadgeService(configRepo, freezeRepo, earnedRepo); + } + + @Test + void saveEarned_skipsRowsAlreadyKnown_idempotentReupload() { + when(earnedRepo.existsByUserIdAndBadgeIdAndLevelAndAwardKey(960, "steady_syncer", 1, "")) + .thenReturn(true); + when(earnedRepo.existsByUserIdAndBadgeIdAndLevelAndAwardKey(960, "steady_syncer", 2, "")) + .thenReturn(false); + + int inserted = service.saveEarned(960, List.of( + new BadgeEarnedDTO("steady_syncer", 1, 10L, ""), + new BadgeEarnedDTO("steady_syncer", 2, 20L, ""))); + + assertEquals(1, inserted); + verify(earnedRepo, times(1)).save(any(BadgeEarned.class)); + } + + /** + * The whole point of the award key: a quarterly badge earned in two quarters is two + * awards at the same level, and without the key the second one is silently dropped. + */ + @Test + void saveEarned_keepsTwoAwardsOfTheSameLevelWithDifferentKeys() { + when(earnedRepo.existsByUserIdAndBadgeIdAndLevelAndAwardKey( + eq(960), eq("complete_worker"), eq(1), anyString())).thenReturn(false); + + int inserted = service.saveEarned(960, List.of( + new BadgeEarnedDTO("complete_worker", 1, 10L, "2026-Q2"), + new BadgeEarnedDTO("complete_worker", 1, 20L, "2026-Q3"))); + + assertEquals(2, inserted); + verify(earnedRepo, times(2)).save(any(BadgeEarned.class)); + } + + /** An older client sends no key at all; it must behave as it did before the field existed. */ + @Test + void saveEarned_treatsAMissingAwardKeyAsEmpty() { + when(earnedRepo.existsByUserIdAndBadgeIdAndLevelAndAwardKey(960, "steady_syncer", 1, "")) + .thenReturn(false); + + assertEquals(1, service.saveEarned(960, List.of( + new BadgeEarnedDTO("steady_syncer", 1, 10L, null)))); + + ArgumentCaptor saved = ArgumentCaptor.forClass(BadgeEarned.class); + verify(earnedRepo).save(saved.capture()); + assertEquals("", saved.getValue().getAwardKey()); + } + + @Test + void saveEarned_ignoresMalformedEntriesAndEmptyLists() { + assertEquals(0, service.saveEarned(960, null)); + assertEquals(0, service.saveEarned(960, List.of(new BadgeEarnedDTO(null, null, null, "")))); + verify(earnedRepo, never()).save(any()); + } +}