diff --git a/app/femr/business/services/core/IDailyReportService.java b/app/femr/business/services/core/IDailyReportService.java new file mode 100644 index 000000000..b56a33f7d --- /dev/null +++ b/app/femr/business/services/core/IDailyReportService.java @@ -0,0 +1,42 @@ +/* + fEMR - fast Electronic Medical Records + Copyright (C) 2014 Team fEMR + + fEMR 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. + + fEMR 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 fEMR. If not, see . If + you have any questions, contact . +*/ +package femr.business.services.core; + +import femr.common.dtos.ServiceResponse; +import femr.common.models.DailyReportItem; +import org.joda.time.DateTime; + +/** + * Service interface for generating MDS Daily Reports. + * Aggregates data from the repository layer and returns it in a format + * suitable for populating the MDS-Ver1.0 Daily Report form. + */ +public interface IDailyReportService { + + /** + * Generate a complete daily report for a specific trip and date. + * This aggregates all data needed for the MDS Daily Report form. + * + * @param tripId the mission trip ID, not null + * @param date the date to generate the report for, not null + * @return a ServiceResponse containing the DailyReportItem with all aggregated data, + * or errors if the operation failed + */ + ServiceResponse generateDailyReport(int tripId, DateTime date); +} \ No newline at end of file diff --git a/app/femr/business/services/system/DailyReportService.java b/app/femr/business/services/system/DailyReportService.java new file mode 100644 index 000000000..d47f5fecc --- /dev/null +++ b/app/femr/business/services/system/DailyReportService.java @@ -0,0 +1,119 @@ +/* + fEMR - fast Electronic Medical Records + Copyright (C) 2014 Team fEMR + + fEMR 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. + + fEMR 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 fEMR. If not, see . If + you have any questions, contact . +*/ +package femr.business.services.system; + +import com.google.inject.Inject; +import femr.business.services.core.IDailyReportService; +import femr.common.dtos.ServiceResponse; +import femr.common.models.DailyReportItem; +import femr.data.daos.core.IDailyReportRepository; +import femr.data.models.core.IMissionTrip; +import org.joda.time.DateTime; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; +import play.Logger; + +import java.util.Map; + +public class DailyReportService implements IDailyReportService { + + private static final DateTimeFormatter MDS_DATE_FORMAT = DateTimeFormat.forPattern("dd/MM/yyyy"); + + private final IDailyReportRepository dailyReportRepository; + + @Inject + public DailyReportService(IDailyReportRepository dailyReportRepository) { + this.dailyReportRepository = dailyReportRepository; + } + + /** + * {@inheritDoc} + */ + @Override + public ServiceResponse generateDailyReport(int tripId, DateTime date) { + ServiceResponse response = new ServiceResponse<>(); + + try { + DailyReportItem reportItem = new DailyReportItem(); + + IMissionTrip missionTrip = dailyReportRepository.getMissionTripById(tripId); + if (missionTrip == null) { + response.addError("tripId", "Mission trip not found for ID: " + tripId); + return response; + } + + if (missionTrip.getMissionTeam() != null) { + reportItem.setTeamName(missionTrip.getMissionTeam().getName()); + } + if (missionTrip.getMissionCity() != null) { + reportItem.setTripCity(missionTrip.getMissionCity().getName()); + if (missionTrip.getMissionCity().getMissionCountry() != null) { + reportItem.setTripCountry(missionTrip.getMissionCity().getMissionCountry().getName()); + } + } + + if (missionTrip.getEndDate() != null) { + DateTime endDate = new DateTime(missionTrip.getEndDate()); + reportItem.setDepartureDate(endDate.toString(MDS_DATE_FORMAT)); + } + + reportItem.setReportDate(date.toString(MDS_DATE_FORMAT)); + + Map> demographicCounts = dailyReportRepository.getDemographicCounts(tripId, date); + mapDemographicCounts(reportItem, demographicCounts); + + response.setResponseObject(reportItem); + + } catch (Exception ex) { + Logger.error("DailyReportService-generateDailyReport", ex); + response.addError("exception", ex.getMessage()); + } + + return response; + } + + private void mapDemographicCounts(DailyReportItem reportItem, Map> counts) { + Map maleCounts = counts.get("MALE"); + if (maleCounts != null) { + reportItem.setMaleUnder1(maleCounts.getOrDefault("UNDER_1", 0)); + reportItem.setMale1To4(maleCounts.getOrDefault("AGE_1_TO_4", 0)); + reportItem.setMale5To17(maleCounts.getOrDefault("AGE_5_TO_17", 0)); + reportItem.setMale18To64(maleCounts.getOrDefault("AGE_18_TO_64", 0)); + reportItem.setMale65Plus(maleCounts.getOrDefault("AGE_65_PLUS", 0)); + } + + Map femaleNonPregnantCounts = counts.get("FEMALE_NON_PREGNANT"); + if (femaleNonPregnantCounts != null) { + reportItem.setFemaleNonPregnantUnder1(femaleNonPregnantCounts.getOrDefault("UNDER_1", 0)); + reportItem.setFemaleNonPregnant1To4(femaleNonPregnantCounts.getOrDefault("AGE_1_TO_4", 0)); + reportItem.setFemaleNonPregnant5To17(femaleNonPregnantCounts.getOrDefault("AGE_5_TO_17", 0)); + reportItem.setFemaleNonPregnant18To64(femaleNonPregnantCounts.getOrDefault("AGE_18_TO_64", 0)); + reportItem.setFemaleNonPregnant65Plus(femaleNonPregnantCounts.getOrDefault("AGE_65_PLUS", 0)); + } + + Map femalePregnantCounts = counts.get("FEMALE_PREGNANT"); + if (femalePregnantCounts != null) { + reportItem.setFemalePregnantUnder1(femalePregnantCounts.getOrDefault("UNDER_1", 0)); + reportItem.setFemalePregnant1To4(femalePregnantCounts.getOrDefault("AGE_1_TO_4", 0)); + reportItem.setFemalePregnant5To17(femalePregnantCounts.getOrDefault("AGE_5_TO_17", 0)); + reportItem.setFemalePregnant18To64(femalePregnantCounts.getOrDefault("AGE_18_TO_64", 0)); + reportItem.setFemalePregnant65Plus(femalePregnantCounts.getOrDefault("AGE_65_PLUS", 0)); + } + } +} \ No newline at end of file diff --git a/app/femr/common/models/DailyReportItem.java b/app/femr/common/models/DailyReportItem.java new file mode 100644 index 000000000..789151e08 --- /dev/null +++ b/app/femr/common/models/DailyReportItem.java @@ -0,0 +1,244 @@ +/* + fEMR - fast Electronic Medical Records + Copyright (C) 2014 Team fEMR + + fEMR 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. + + fEMR 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 fEMR. If not, see . If + you have any questions, contact . +*/ +package femr.common.models; + +public class DailyReportItem { + + private String teamName; + private String tripCity; + private String tripCountry; + private String departureDate; + private String reportDate; + + private int maleUnder1; + private int male1To4; + private int male5To17; + private int male18To64; + private int male65Plus; + + private int femaleNonPregnantUnder1; + private int femaleNonPregnant1To4; + private int femaleNonPregnant5To17; + private int femaleNonPregnant18To64; + private int femaleNonPregnant65Plus; + + private int femalePregnantUnder1; + private int femalePregnant1To4; + private int femalePregnant5To17; + private int femalePregnant18To64; + private int femalePregnant65Plus; + + public String getTeamName() { + return teamName; + } + + public void setTeamName(String teamName) { + this.teamName = teamName; + } + + public String getTripCity() { + return tripCity; + } + + public void setTripCity(String tripCity) { + this.tripCity = tripCity; + } + + public String getTripCountry() { + return tripCountry; + } + + public void setTripCountry(String tripCountry) { + this.tripCountry = tripCountry; + } + + public String getDepartureDate() { + return departureDate; + } + + public void setDepartureDate(String departureDate) { + this.departureDate = departureDate; + } + + public String getReportDate() { + return reportDate; + } + + public void setReportDate(String reportDate) { + this.reportDate = reportDate; + } + + public int getMaleUnder1() { + return maleUnder1; + } + + public void setMaleUnder1(int maleUnder1) { + this.maleUnder1 = maleUnder1; + } + + public int getMale1To4() { + return male1To4; + } + + public void setMale1To4(int male1To4) { + this.male1To4 = male1To4; + } + + public int getMale5To17() { + return male5To17; + } + + public void setMale5To17(int male5To17) { + this.male5To17 = male5To17; + } + + public int getMale18To64() { + return male18To64; + } + + public void setMale18To64(int male18To64) { + this.male18To64 = male18To64; + } + + public int getMale65Plus() { + return male65Plus; + } + + public void setMale65Plus(int male65Plus) { + this.male65Plus = male65Plus; + } + + public int getFemaleNonPregnantUnder1() { + return femaleNonPregnantUnder1; + } + + public void setFemaleNonPregnantUnder1(int femaleNonPregnantUnder1) { + this.femaleNonPregnantUnder1 = femaleNonPregnantUnder1; + } + + public int getFemaleNonPregnant1To4() { + return femaleNonPregnant1To4; + } + + public void setFemaleNonPregnant1To4(int femaleNonPregnant1To4) { + this.femaleNonPregnant1To4 = femaleNonPregnant1To4; + } + + public int getFemaleNonPregnant5To17() { + return femaleNonPregnant5To17; + } + + public void setFemaleNonPregnant5To17(int femaleNonPregnant5To17) { + this.femaleNonPregnant5To17 = femaleNonPregnant5To17; + } + + public int getFemaleNonPregnant18To64() { + return femaleNonPregnant18To64; + } + + public void setFemaleNonPregnant18To64(int femaleNonPregnant18To64) { + this.femaleNonPregnant18To64 = femaleNonPregnant18To64; + } + + public int getFemaleNonPregnant65Plus() { + return femaleNonPregnant65Plus; + } + + public void setFemaleNonPregnant65Plus(int femaleNonPregnant65Plus) { + this.femaleNonPregnant65Plus = femaleNonPregnant65Plus; + } + + public int getFemalePregnantUnder1() { + return femalePregnantUnder1; + } + + public void setFemalePregnantUnder1(int femalePregnantUnder1) { + this.femalePregnantUnder1 = femalePregnantUnder1; + } + + public int getFemalePregnant1To4() { + return femalePregnant1To4; + } + + public void setFemalePregnant1To4(int femalePregnant1To4) { + this.femalePregnant1To4 = femalePregnant1To4; + } + + public int getFemalePregnant5To17() { + return femalePregnant5To17; + } + + public void setFemalePregnant5To17(int femalePregnant5To17) { + this.femalePregnant5To17 = femalePregnant5To17; + } + + public int getFemalePregnant18To64() { + return femalePregnant18To64; + } + + public void setFemalePregnant18To64(int femalePregnant18To64) { + this.femalePregnant18To64 = femalePregnant18To64; + } + + public int getFemalePregnant65Plus() { + return femalePregnant65Plus; + } + + public void setFemalePregnant65Plus(int femalePregnant65Plus) { + this.femalePregnant65Plus = femalePregnant65Plus; + } + + public int getTotalEncounters() { + return getMaleTotal() + getFemaleNonPregnantTotal() + getFemalePregnantTotal(); + } + + public int getMaleTotal() { + return maleUnder1 + male1To4 + male5To17 + male18To64 + male65Plus; + } + + public int getFemaleNonPregnantTotal() { + return femaleNonPregnantUnder1 + femaleNonPregnant1To4 + femaleNonPregnant5To17 + + femaleNonPregnant18To64 + femaleNonPregnant65Plus; + } + + public int getFemalePregnantTotal() { + return femalePregnantUnder1 + femalePregnant1To4 + femalePregnant5To17 + + femalePregnant18To64 + femalePregnant65Plus; + } + + public int getUnder1Total() { + return maleUnder1 + femaleNonPregnantUnder1 + femalePregnantUnder1; + } + + public int get1To4Total() { + return male1To4 + femaleNonPregnant1To4 + femalePregnant1To4; + } + + public int get5To17Total() { + return male5To17 + femaleNonPregnant5To17 + femalePregnant5To17; + } + + public int get18To64Total() { + return male18To64 + femaleNonPregnant18To64 + femalePregnant18To64; + } + + public int get65PlusTotal() { + return male65Plus + femaleNonPregnant65Plus + femalePregnant65Plus; + } +} \ No newline at end of file diff --git a/app/femr/data/daos/core/IDailyReportRepository.java b/app/femr/data/daos/core/IDailyReportRepository.java new file mode 100644 index 000000000..b6ca062c5 --- /dev/null +++ b/app/femr/data/daos/core/IDailyReportRepository.java @@ -0,0 +1,47 @@ +/* + fEMR - fast Electronic Medical Records + Copyright (C) 2014 Team fEMR + + fEMR 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. + + fEMR 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 fEMR. If not, see . If + you have any questions, contact . +*/ +package femr.data.daos.core; + +import femr.data.models.core.IMissionTrip; +import org.joda.time.DateTime; + +import java.util.Map; + +public interface IDailyReportRepository { + + /** + * Get patient demographic counts for a specific trip and date. + * Returns a nested map structure: sexCategory -> (ageCategory -> count) + * + * Example: { "MALE" -> { "UNDER_1" -> 5, "AGE_1_TO_4" -> 10, ... }, ... } + * + * @param tripId the mission trip ID + * @param date the date of activity + * @return nested map of demographic counts by sex and age category + */ + Map> getDemographicCounts(int tripId, DateTime date); + + /** + * Get mission trip entity with team, city, and country eagerly fetched. + * + * @param tripId the mission trip ID + * @return IMissionTrip entity or null if not found + */ + IMissionTrip getMissionTripById(int tripId); +} \ No newline at end of file diff --git a/app/femr/data/daos/system/DailyReportRepository.java b/app/femr/data/daos/system/DailyReportRepository.java new file mode 100644 index 000000000..c687d4b3e --- /dev/null +++ b/app/femr/data/daos/system/DailyReportRepository.java @@ -0,0 +1,130 @@ +/* + fEMR - fast Electronic Medical Records + Copyright (C) 2014 Team fEMR + + fEMR 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. + + fEMR 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 fEMR. If not, see . If + you have any questions, contact . +*/ +package femr.data.daos.system; + +import io.ebean.Ebean; +import io.ebean.SqlRow; +import femr.data.daos.core.IDailyReportRepository; +import femr.data.models.core.IMissionTrip; +import femr.business.helpers.QueryProvider; +import org.joda.time.DateTime; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; +import play.Logger; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class DailyReportRepository implements IDailyReportRepository { + + private static final DateTimeFormatter DATE_FORMAT = DateTimeFormat.forPattern("yyyy-MM-dd"); + + private volatile Integer weeksPregnantVitalId; + + private Integer getWeeksPregnantVitalId() { + if (weeksPregnantVitalId == null) { + synchronized (this) { + if (weeksPregnantVitalId == null) { + String sql = "SELECT id FROM vitals WHERE name = 'weeksPregnant' AND isDeleted = 0 LIMIT 1"; + SqlRow row = Ebean.createSqlQuery(sql).findOne(); + weeksPregnantVitalId = (row != null) ? row.getInteger("id") : -1; + } + } + } + return weeksPregnantVitalId == -1 ? null : weeksPregnantVitalId; + } + + @Override + public Map> getDemographicCounts(int tripId, DateTime date) { + String dateStart = date.toString(DATE_FORMAT); + String dateEnd = date.plusDays(1).toString(DATE_FORMAT); + Integer pregnancyVitalId = getWeeksPregnantVitalId(); + + String sql = + "SELECT " + + " age_category, " + + " sex_category, " + + " COUNT(*) as cnt " + + "FROM ( " + + " SELECT " + + " CASE " + + " WHEN TIMESTAMPDIFF(MONTH, p.age, :dateStart) < 12 THEN 'UNDER_1' " + + " WHEN TIMESTAMPDIFF(YEAR, p.age, :dateStart) BETWEEN 1 AND 4 THEN 'AGE_1_TO_4' " + + " WHEN TIMESTAMPDIFF(YEAR, p.age, :dateStart) BETWEEN 5 AND 17 THEN 'AGE_5_TO_17' " + + " WHEN TIMESTAMPDIFF(YEAR, p.age, :dateStart) BETWEEN 18 AND 64 THEN 'AGE_18_TO_64' " + + " ELSE 'AGE_65_PLUS' " + + " END AS age_category, " + + " CASE " + + " WHEN p.sex = 'Male' THEN 'MALE' " + + " WHEN pev.vital_value IS NOT NULL AND pev.vital_value > 0 THEN 'FEMALE_PREGNANT' " + + " ELSE 'FEMALE_NON_PREGNANT' " + + " END AS sex_category " + + " FROM patient_encounters pe " + + " JOIN patients p ON pe.patient_id = p.id " + + " LEFT JOIN patient_encounter_vitals pev ON pe.id = pev.patient_encounter_id " + + " AND pev.vital_id = :pregnancyVitalId " + + " WHERE pe.mission_trip_id = :tripId " + + " AND pe.date_of_triage_visit >= :dateStart " + + " AND pe.date_of_triage_visit < :dateEnd " + + " AND pe.isDeleted IS NULL " + + " AND p.isDeleted IS NULL " + + ") AS categorized " + + "GROUP BY age_category, sex_category"; + + try { + List rows = Ebean.createSqlQuery(sql) + .setParameter("tripId", tripId) + .setParameter("dateStart", dateStart) + .setParameter("dateEnd", dateEnd) + .setParameter("pregnancyVitalId", pregnancyVitalId != null ? pregnancyVitalId : -1) + .findList(); + + Map> results = new HashMap<>(); + for (SqlRow row : rows) { + String sexCategory = row.getString("sex_category"); + String ageCategory = row.getString("age_category"); + Integer count = row.getInteger("cnt"); + + results.computeIfAbsent(sexCategory, k -> new HashMap<>()) + .put(ageCategory, count != null ? count : 0); + } + return results; + } catch (Exception ex) { + Logger.error("DailyReportRepository-getDemographicCounts", ex); + throw ex; + } + } + + @Override + public IMissionTrip getMissionTripById(int tripId) { + try { + return QueryProvider.getMissionTripQuery() + .fetch("missionTeam") + .fetch("missionCity") + .fetch("missionCity.missionCountry") + .where() + .eq("id", tripId) + .findOne(); + } catch (Exception ex) { + Logger.error("DailyReportRepository-getMissionTripById", ex); + throw ex; + } + } +} \ No newline at end of file diff --git a/app/femr/util/dependencyinjection/modules/BusinessLayerModule.java b/app/femr/util/dependencyinjection/modules/BusinessLayerModule.java index 3203efece..33bd3d8bc 100644 --- a/app/femr/util/dependencyinjection/modules/BusinessLayerModule.java +++ b/app/femr/util/dependencyinjection/modules/BusinessLayerModule.java @@ -47,5 +47,6 @@ protected void configure() { bind(IUserService.class).to(UserService.class); bind(IVitalService.class).to(VitalService.class); bind(IInternetStatusService.class).to(InternetStatusService.class); + bind(IDailyReportService.class).to(DailyReportService.class); } } diff --git a/app/femr/util/dependencyinjection/modules/DataLayerModule.java b/app/femr/util/dependencyinjection/modules/DataLayerModule.java index 96f5ce316..e25b78cd6 100644 --- a/app/femr/util/dependencyinjection/modules/DataLayerModule.java +++ b/app/femr/util/dependencyinjection/modules/DataLayerModule.java @@ -84,6 +84,7 @@ protected void configure() { bind(IPrescriptionRepository.class).to(PrescriptionRepository.class); bind(IInternetStatusRepository.class).to(InternetStatusRepository.class); + bind(IDailyReportRepository.class).to(DailyReportRepository.class); // Research bind(IResearchEncounter.class).toProvider(ResearchEncounterProvider.class); diff --git a/test/mock/femr/data/daos/MockDailyReportRepository.java b/test/mock/femr/data/daos/MockDailyReportRepository.java new file mode 100644 index 000000000..d9e65399c --- /dev/null +++ b/test/mock/femr/data/daos/MockDailyReportRepository.java @@ -0,0 +1,56 @@ +package mock.femr.data.daos; + +import femr.data.daos.core.IDailyReportRepository; +import femr.data.models.core.IMissionTrip; +import mock.femr.data.models.MockMissionTrip; +import org.joda.time.DateTime; + +import java.util.HashMap; +import java.util.Map; + +public class MockDailyReportRepository implements IDailyReportRepository { + + public boolean getDemographicCountsWasCalled = false; + public boolean getMissionTripByIdWasCalled = false; + + public int lastTripIdRequested = -1; + public DateTime lastDateRequested = null; + + public IMissionTrip mockMissionTrip; + public Map> mockDemographicCounts; + + public boolean returnNullMissionTrip = false; + + public MockDailyReportRepository() { + this.mockMissionTrip = new MockMissionTrip(); + this.mockDemographicCounts = new HashMap<>(); + } + + @Override + public Map> getDemographicCounts(int tripId, DateTime date) { + getDemographicCountsWasCalled = true; + lastTripIdRequested = tripId; + lastDateRequested = date; + return mockDemographicCounts; + } + + @Override + public IMissionTrip getMissionTripById(int tripId) { + getMissionTripByIdWasCalled = true; + lastTripIdRequested = tripId; + + if (returnNullMissionTrip) { + return null; + } + return mockMissionTrip; + } + + public void addDemographicCount(String ageCategory, String sexCategory, int count) { + mockDemographicCounts.computeIfAbsent(sexCategory, k -> new HashMap<>()) + .put(ageCategory, count); + } + + public void clearDemographicCounts() { + mockDemographicCounts.clear(); + } +} \ No newline at end of file diff --git a/test/mock/femr/data/models/MockMissionCity.java b/test/mock/femr/data/models/MockMissionCity.java new file mode 100644 index 000000000..ed0b8a564 --- /dev/null +++ b/test/mock/femr/data/models/MockMissionCity.java @@ -0,0 +1,40 @@ +package mock.femr.data.models; + +import femr.data.models.core.IMissionCity; +import femr.data.models.core.IMissionCountry; + +public class MockMissionCity implements IMissionCity { + + private int id = 1; + private String name = "Antigua"; + private IMissionCountry missionCountry = new MockMissionCountry(); + + @Override + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + @Override + public String getName() { + return name; + } + + @Override + public void setName(String name) { + this.name = name; + } + + @Override + public IMissionCountry getMissionCountry() { + return missionCountry; + } + + @Override + public void setMissionCountry(IMissionCountry missionCountry) { + this.missionCountry = missionCountry; + } +} \ No newline at end of file diff --git a/test/mock/femr/data/models/MockMissionCountry.java b/test/mock/femr/data/models/MockMissionCountry.java new file mode 100644 index 000000000..3c48b3020 --- /dev/null +++ b/test/mock/femr/data/models/MockMissionCountry.java @@ -0,0 +1,28 @@ +package mock.femr.data.models; + +import femr.data.models.core.IMissionCountry; + +public class MockMissionCountry implements IMissionCountry { + + private int id = 1; + private String name = "Guatemala"; + + @Override + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + @Override + public String getName() { + return name; + } + + @Override + public void setName(String name) { + this.name = name; + } +} \ No newline at end of file diff --git a/test/mock/femr/data/models/MockMissionTeam.java b/test/mock/femr/data/models/MockMissionTeam.java new file mode 100644 index 000000000..1de33e083 --- /dev/null +++ b/test/mock/femr/data/models/MockMissionTeam.java @@ -0,0 +1,76 @@ +package mock.femr.data.models; + +import femr.data.models.core.IMissionTeam; +import femr.data.models.mysql.MissionTrip; + +import java.util.ArrayList; +import java.util.List; + +public class MockMissionTeam implements IMissionTeam { + + private int id = 1; + private String name = "Team fEMR"; + private String location = "California"; + private String description = "Medical mission team"; + private String languageCode = "en"; + private List missionTrips = new ArrayList<>(); + + @Override + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + @Override + public String getName() { + return name; + } + + @Override + public void setName(String name) { + this.name = name; + } + + @Override + public String getLocation() { + return location; + } + + @Override + public void setLocation(String location) { + this.location = location; + } + + @Override + public String getDescription() { + return description; + } + + @Override + public void setDescription(String description) { + this.description = description; + } + + @Override + public List getMissionTrips() { + return missionTrips; + } + + @Override + public void setMissionTrips(List missionTrips) { + this.missionTrips = missionTrips; + } + + @Override + public String getLanguageCode() { + return languageCode; + } + + @Override + public void setLanguageCode(String languageCode) { + this.languageCode = languageCode; + } +} \ No newline at end of file diff --git a/test/mock/femr/data/models/MockMissionTrip.java b/test/mock/femr/data/models/MockMissionTrip.java new file mode 100644 index 000000000..1cdae5200 --- /dev/null +++ b/test/mock/femr/data/models/MockMissionTrip.java @@ -0,0 +1,98 @@ +package mock.femr.data.models; + +import femr.data.models.core.IMissionCity; +import femr.data.models.core.IMissionTeam; +import femr.data.models.core.IMissionTrip; +import femr.data.models.core.IUser; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +public class MockMissionTrip implements IMissionTrip { + + private int id = 1; + private IMissionTeam missionTeam = new MockMissionTeam(); + private IMissionCity missionCity = new MockMissionCity(); + private Date startDate; + private Date endDate; + private List users = new ArrayList<>(); + + public MockMissionTrip() { + Calendar cal = Calendar.getInstance(); + cal.set(2024, Calendar.MARCH, 1); + this.startDate = cal.getTime(); + cal.set(2024, Calendar.MARCH, 15); + this.endDate = cal.getTime(); + } + + @Override + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + @Override + public IMissionTeam getMissionTeam() { + return missionTeam; + } + + @Override + public void setMissionTeam(IMissionTeam missionTeam) { + this.missionTeam = missionTeam; + } + + @Override + public IMissionCity getMissionCity() { + return missionCity; + } + + @Override + public void setMissionCity(IMissionCity missionCity) { + this.missionCity = missionCity; + } + + @Override + public Date getStartDate() { + return startDate; + } + + @Override + public void setStartDate(Date startDate) { + this.startDate = startDate; + } + + @Override + public Date getEndDate() { + return endDate; + } + + @Override + public void setEndDate(Date endDate) { + this.endDate = endDate; + } + + @Override + public List getUsers() { + return users; + } + + @Override + public void setUsers(List users) { + this.users = users; + } + + @Override + public void addUser(IUser user) { + this.users.add(user); + } + + @Override + public void removeUser(int userId) { + this.users.removeIf(user -> user.getId() == userId); + } +} \ No newline at end of file diff --git a/test/unit/app/femr/business/services/DailyReportServiceTest.java b/test/unit/app/femr/business/services/DailyReportServiceTest.java new file mode 100644 index 000000000..ed04d51fd --- /dev/null +++ b/test/unit/app/femr/business/services/DailyReportServiceTest.java @@ -0,0 +1,319 @@ +package unit.app.femr.business.services; + +import femr.business.services.core.IDailyReportService; +import femr.business.services.system.DailyReportService; +import femr.common.dtos.ServiceResponse; +import femr.common.models.DailyReportItem; +import mock.femr.data.daos.MockDailyReportRepository; +import mock.femr.data.models.MockMissionCity; +import mock.femr.data.models.MockMissionCountry; +import mock.femr.data.models.MockMissionTeam; +import mock.femr.data.models.MockMissionTrip; +import org.joda.time.DateTime; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.Calendar; + +import static org.junit.Assert.*; + +public class DailyReportServiceTest { + + private IDailyReportService dailyReportService; + private MockDailyReportRepository mockDailyReportRepository; + + @Before + public void setUp() { + mockDailyReportRepository = new MockDailyReportRepository(); + dailyReportService = new DailyReportService(mockDailyReportRepository); + } + + @After + public void tearDown() { + mockDailyReportRepository = null; + dailyReportService = null; + } + + @Test + public void generateDailyReport_validTripId_repositoryCalledWithCorrectId() { + int tripId = 5; + DateTime date = new DateTime(2024, 3, 10, 0, 0); + + dailyReportService.generateDailyReport(tripId, date); + + assertTrue(mockDailyReportRepository.getMissionTripByIdWasCalled); + assertEquals(tripId, mockDailyReportRepository.lastTripIdRequested); + } + + @Test + public void generateDailyReport_validTripId_demographicCountsCalledWithCorrectParameters() { + int tripId = 5; + DateTime date = new DateTime(2024, 3, 10, 0, 0); + + dailyReportService.generateDailyReport(tripId, date); + + assertTrue(mockDailyReportRepository.getDemographicCountsWasCalled); + assertEquals(tripId, mockDailyReportRepository.lastTripIdRequested); + assertEquals(date, mockDailyReportRepository.lastDateRequested); + } + + @Test + public void generateDailyReport_tripNotFound_returnsError() { + mockDailyReportRepository.returnNullMissionTrip = true; + + ServiceResponse response = dailyReportService.generateDailyReport(999, new DateTime()); + + assertTrue(response.hasErrors()); + assertNull(response.getResponseObject()); + } + + @Test + public void generateDailyReport_tripNotFound_doesNotCallDemographicCounts() { + mockDailyReportRepository.returnNullMissionTrip = true; + + dailyReportService.generateDailyReport(999, new DateTime()); + + assertFalse(mockDailyReportRepository.getDemographicCountsWasCalled); + } + + @Test + public void generateDailyReport_validTrip_returnsTeamName() { + MockMissionTrip mockTrip = new MockMissionTrip(); + MockMissionTeam mockTeam = new MockMissionTeam(); + mockTeam.setName("Test Medical Team"); + mockTrip.setMissionTeam(mockTeam); + mockDailyReportRepository.mockMissionTrip = mockTrip; + + ServiceResponse response = dailyReportService.generateDailyReport(1, new DateTime()); + + assertFalse(response.hasErrors()); + assertEquals("Test Medical Team", response.getResponseObject().getTeamName()); + } + + @Test + public void generateDailyReport_validTrip_returnsCityName() { + MockMissionTrip mockTrip = new MockMissionTrip(); + MockMissionCity mockCity = new MockMissionCity(); + mockCity.setName("Test City"); + mockTrip.setMissionCity(mockCity); + mockDailyReportRepository.mockMissionTrip = mockTrip; + + ServiceResponse response = dailyReportService.generateDailyReport(1, new DateTime()); + + assertFalse(response.hasErrors()); + assertEquals("Test City", response.getResponseObject().getTripCity()); + } + + @Test + public void generateDailyReport_validTrip_returnsCountryName() { + MockMissionTrip mockTrip = new MockMissionTrip(); + MockMissionCity mockCity = new MockMissionCity(); + MockMissionCountry mockCountry = new MockMissionCountry(); + mockCountry.setName("Test Country"); + mockCity.setMissionCountry(mockCountry); + mockTrip.setMissionCity(mockCity); + mockDailyReportRepository.mockMissionTrip = mockTrip; + + ServiceResponse response = dailyReportService.generateDailyReport(1, new DateTime()); + + assertFalse(response.hasErrors()); + assertEquals("Test Country", response.getResponseObject().getTripCountry()); + } + + @Test + public void generateDailyReport_validTrip_formatsReportDateCorrectly() { + DateTime date = new DateTime(2024, 3, 15, 0, 0); + + ServiceResponse response = dailyReportService.generateDailyReport(1, date); + + assertFalse(response.hasErrors()); + assertEquals("15/03/2024", response.getResponseObject().getReportDate()); + } + + @Test + public void generateDailyReport_validTrip_formatsDepartureDateCorrectly() { + MockMissionTrip mockTrip = new MockMissionTrip(); + Calendar cal = Calendar.getInstance(); + cal.set(2024, Calendar.DECEMBER, 25); + mockTrip.setEndDate(cal.getTime()); + mockDailyReportRepository.mockMissionTrip = mockTrip; + + ServiceResponse response = dailyReportService.generateDailyReport(1, new DateTime()); + + assertFalse(response.hasErrors()); + assertEquals("25/12/2024", response.getResponseObject().getDepartureDate()); + } + + @Test + public void generateDailyReport_nullEndDate_departureDateIsNull() { + MockMissionTrip mockTrip = new MockMissionTrip(); + mockTrip.setEndDate(null); + mockDailyReportRepository.mockMissionTrip = mockTrip; + + ServiceResponse response = dailyReportService.generateDailyReport(1, new DateTime()); + + assertFalse(response.hasErrors()); + assertNull(response.getResponseObject().getDepartureDate()); + } + + @Test + public void generateDailyReport_nullMissionTeam_teamNameIsNull() { + MockMissionTrip mockTrip = new MockMissionTrip(); + mockTrip.setMissionTeam(null); + mockDailyReportRepository.mockMissionTrip = mockTrip; + + ServiceResponse response = dailyReportService.generateDailyReport(1, new DateTime()); + + assertFalse(response.hasErrors()); + assertNull(response.getResponseObject().getTeamName()); + } + + @Test + public void generateDailyReport_nullMissionCity_cityAndCountryAreNull() { + MockMissionTrip mockTrip = new MockMissionTrip(); + mockTrip.setMissionCity(null); + mockDailyReportRepository.mockMissionTrip = mockTrip; + + ServiceResponse response = dailyReportService.generateDailyReport(1, new DateTime()); + + assertFalse(response.hasErrors()); + assertNull(response.getResponseObject().getTripCity()); + assertNull(response.getResponseObject().getTripCountry()); + } + + @Test + public void generateDailyReport_nullMissionCountry_countryIsNull() { + MockMissionTrip mockTrip = new MockMissionTrip(); + MockMissionCity mockCity = new MockMissionCity(); + mockCity.setName("Test City"); + mockCity.setMissionCountry(null); + mockTrip.setMissionCity(mockCity); + mockDailyReportRepository.mockMissionTrip = mockTrip; + + ServiceResponse response = dailyReportService.generateDailyReport(1, new DateTime()); + + assertFalse(response.hasErrors()); + assertEquals("Test City", response.getResponseObject().getTripCity()); + assertNull(response.getResponseObject().getTripCountry()); + } + + @Test + public void generateDailyReport_maleDemographics_mappedCorrectly() { + mockDailyReportRepository.addDemographicCount("UNDER_1", "MALE", 2); + mockDailyReportRepository.addDemographicCount("AGE_1_TO_4", "MALE", 5); + mockDailyReportRepository.addDemographicCount("AGE_5_TO_17", "MALE", 10); + mockDailyReportRepository.addDemographicCount("AGE_18_TO_64", "MALE", 25); + mockDailyReportRepository.addDemographicCount("AGE_65_PLUS", "MALE", 3); + + ServiceResponse response = dailyReportService.generateDailyReport(1, new DateTime()); + + assertFalse(response.hasErrors()); + DailyReportItem report = response.getResponseObject(); + assertEquals(2, report.getMaleUnder1()); + assertEquals(5, report.getMale1To4()); + assertEquals(10, report.getMale5To17()); + assertEquals(25, report.getMale18To64()); + assertEquals(3, report.getMale65Plus()); + assertEquals(45, report.getMaleTotal()); + } + + @Test + public void generateDailyReport_femaleNonPregnantDemographics_mappedCorrectly() { + mockDailyReportRepository.addDemographicCount("UNDER_1", "FEMALE_NON_PREGNANT", 1); + mockDailyReportRepository.addDemographicCount("AGE_1_TO_4", "FEMALE_NON_PREGNANT", 4); + mockDailyReportRepository.addDemographicCount("AGE_5_TO_17", "FEMALE_NON_PREGNANT", 8); + mockDailyReportRepository.addDemographicCount("AGE_18_TO_64", "FEMALE_NON_PREGNANT", 20); + mockDailyReportRepository.addDemographicCount("AGE_65_PLUS", "FEMALE_NON_PREGNANT", 6); + + ServiceResponse response = dailyReportService.generateDailyReport(1, new DateTime()); + + assertFalse(response.hasErrors()); + DailyReportItem report = response.getResponseObject(); + assertEquals(1, report.getFemaleNonPregnantUnder1()); + assertEquals(4, report.getFemaleNonPregnant1To4()); + assertEquals(8, report.getFemaleNonPregnant5To17()); + assertEquals(20, report.getFemaleNonPregnant18To64()); + assertEquals(6, report.getFemaleNonPregnant65Plus()); + assertEquals(39, report.getFemaleNonPregnantTotal()); + } + + @Test + public void generateDailyReport_femalePregnantDemographics_mappedCorrectly() { + mockDailyReportRepository.addDemographicCount("AGE_5_TO_17", "FEMALE_PREGNANT", 2); + mockDailyReportRepository.addDemographicCount("AGE_18_TO_64", "FEMALE_PREGNANT", 7); + + ServiceResponse response = dailyReportService.generateDailyReport(1, new DateTime()); + + assertFalse(response.hasErrors()); + DailyReportItem report = response.getResponseObject(); + assertEquals(0, report.getFemalePregnantUnder1()); + assertEquals(0, report.getFemalePregnant1To4()); + assertEquals(2, report.getFemalePregnant5To17()); + assertEquals(7, report.getFemalePregnant18To64()); + assertEquals(0, report.getFemalePregnant65Plus()); + assertEquals(9, report.getFemalePregnantTotal()); + } + + @Test + public void generateDailyReport_mixedDemographics_totalEncountersCorrect() { + mockDailyReportRepository.addDemographicCount("AGE_18_TO_64", "MALE", 10); + mockDailyReportRepository.addDemographicCount("AGE_18_TO_64", "FEMALE_NON_PREGNANT", 15); + mockDailyReportRepository.addDemographicCount("AGE_18_TO_64", "FEMALE_PREGNANT", 5); + + ServiceResponse response = dailyReportService.generateDailyReport(1, new DateTime()); + + assertFalse(response.hasErrors()); + assertEquals(30, response.getResponseObject().getTotalEncounters()); + } + + @Test + public void generateDailyReport_mixedDemographics_ageTotalsCorrect() { + mockDailyReportRepository.addDemographicCount("UNDER_1", "MALE", 3); + mockDailyReportRepository.addDemographicCount("UNDER_1", "FEMALE_NON_PREGNANT", 2); + mockDailyReportRepository.addDemographicCount("AGE_65_PLUS", "MALE", 5); + mockDailyReportRepository.addDemographicCount("AGE_65_PLUS", "FEMALE_NON_PREGNANT", 4); + mockDailyReportRepository.addDemographicCount("AGE_65_PLUS", "FEMALE_PREGNANT", 1); + + ServiceResponse response = dailyReportService.generateDailyReport(1, new DateTime()); + + assertFalse(response.hasErrors()); + DailyReportItem report = response.getResponseObject(); + assertEquals(5, report.getUnder1Total()); + assertEquals(10, report.get65PlusTotal()); + } + + @Test + public void generateDailyReport_noDemographicData_allCountsZero() { + ServiceResponse response = dailyReportService.generateDailyReport(1, new DateTime()); + + assertFalse(response.hasErrors()); + DailyReportItem report = response.getResponseObject(); + assertEquals(0, report.getMaleTotal()); + assertEquals(0, report.getFemaleNonPregnantTotal()); + assertEquals(0, report.getFemalePregnantTotal()); + assertEquals(0, report.getTotalEncounters()); + } + + @Test + public void generateDailyReport_unknownSexCategory_ignored() { + mockDailyReportRepository.addDemographicCount("AGE_18_TO_64", "UNKNOWN", 10); + mockDailyReportRepository.addDemographicCount("AGE_18_TO_64", "MALE", 5); + + ServiceResponse response = dailyReportService.generateDailyReport(1, new DateTime()); + + assertFalse(response.hasErrors()); + assertEquals(5, response.getResponseObject().getTotalEncounters()); + } + + @Test + public void generateDailyReport_unknownAgeCategory_ignored() { + mockDailyReportRepository.addDemographicCount("AGE_UNKNOWN", "MALE", 10); + mockDailyReportRepository.addDemographicCount("AGE_18_TO_64", "MALE", 5); + + ServiceResponse response = dailyReportService.generateDailyReport(1, new DateTime()); + + assertFalse(response.hasErrors()); + assertEquals(5, response.getResponseObject().getMaleTotal()); + } +} \ No newline at end of file