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