From c688551bee46ca427da798dad9c3b589241f467d Mon Sep 17 00:00:00 2001 From: "aya.abdallah" Date: Fri, 10 Jul 2026 13:48:26 +0300 Subject: [PATCH 1/2] FINERACT-2683: Fix date filters and add username filter on Maker-Checker inbox endpoint The GET /v1/makercheckers date-range filters had two issues: 1. The filters compared against made_on_date but records created since the UTC migration are stored in made_on_date_utc. Fix applies the same OR-pattern already used by AuditsApiResource (check both columns) so filtering works regardless of which column a given record uses. 2. The date getter in MakerCheckerRequest delegated entirely to DateUtils.convertDateTimeStringToOffsetDateTime, which requires the caller to pass dateFormat and locale query params. Clients that send dates in "dd MMMM yyyy" format without those params got a 500. Fix adds a tryParseDayMonthYear fallback that handles this format before delegating to DateUtils for ISO and other formats. Also adds a username query parameter to filter entries by the maker's username via a LIKE match against mk.username, which is already joined in the underlying query. Signed-off-by: aya.abdallah --- .../api/MakercheckersApiResource.java | 21 +++++++++++-- .../data/request/MakerCheckerRequest.java | 30 +++++++++++++++++-- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/api/MakercheckersApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/commands/api/MakercheckersApiResource.java index eb2685c49e2..d54dae31d4e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/commands/api/MakercheckersApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/commands/api/MakercheckersApiResource.java @@ -131,8 +131,25 @@ private SQLBuilder getExtraCriteria(MakerCheckerRequest makerCheckerRequest) { } extraCriteria.addNonNullCriteria("aud.resource_id = ", makerCheckerRequest.getResourceId()); extraCriteria.addNonNullCriteria("aud.maker_id = ", makerCheckerRequest.getMakerId()); - extraCriteria.addNonNullCriteria("aud.made_on_date >= ", makerCheckerRequest.getMakerDateTimeFrom()); - extraCriteria.addNonNullCriteria("aud.made_on_date <= ", makerCheckerRequest.getMakerDateTimeTo()); + if (StringUtils.isNotBlank(makerCheckerRequest.getUsername())) { + extraCriteria.addCriteria("mk.username like ", makerCheckerRequest.getUsername().trim() + "%"); + } + if (makerCheckerRequest.getMakerDateTimeFrom() != null) { + extraCriteria.addSubOperation((SQLBuilder criteria) -> { + criteria.addNonNullCriteria("aud.made_on_date >= ", makerCheckerRequest.getMakerDateTimeFrom(), + SQLBuilder.WhereLogicalOperator.NONE); + criteria.addNonNullCriteria("aud.made_on_date_utc >= ", makerCheckerRequest.getMakerDateTimeFrom(), + SQLBuilder.WhereLogicalOperator.OR); + }); + } + if (makerCheckerRequest.getMakerDateTimeTo() != null) { + extraCriteria.addSubOperation((SQLBuilder criteria) -> { + criteria.addNonNullCriteria("aud.made_on_date <= ", makerCheckerRequest.getMakerDateTimeTo(), + SQLBuilder.WhereLogicalOperator.NONE); + criteria.addNonNullCriteria("aud.made_on_date_utc <= ", makerCheckerRequest.getMakerDateTimeTo(), + SQLBuilder.WhereLogicalOperator.OR); + }); + } extraCriteria.addNonNullCriteria("aud.office_id = ", makerCheckerRequest.getOfficeId()); extraCriteria.addNonNullCriteria("aud.group_id = ", makerCheckerRequest.getGroupId()); extraCriteria.addNonNullCriteria("aud.client_id = ", makerCheckerRequest.getClientId()); diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/data/request/MakerCheckerRequest.java b/fineract-provider/src/main/java/org/apache/fineract/commands/data/request/MakerCheckerRequest.java index 0843789b28c..673a585d591 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/commands/data/request/MakerCheckerRequest.java +++ b/fineract-provider/src/main/java/org/apache/fineract/commands/data/request/MakerCheckerRequest.java @@ -21,11 +21,17 @@ import jakarta.ws.rs.QueryParam; import java.io.Serial; import java.io.Serializable; +import java.time.LocalDate; import java.time.LocalTime; import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Locale; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.apache.commons.lang3.StringUtils; import org.apache.fineract.infrastructure.core.service.DateUtils; @Setter @@ -44,6 +50,8 @@ public class MakerCheckerRequest implements Serializable { private Long resourceId; @QueryParam("makerId") private Long makerId; + @QueryParam("username") + private String username; @QueryParam("makerDateTimeFrom") private String makerDateTimeFrom; @QueryParam("makerDateTimeTo") @@ -64,10 +72,28 @@ public class MakerCheckerRequest implements Serializable { private String locale; public OffsetDateTime getMakerDateTimeFrom() { - return DateUtils.convertDateTimeStringToOffsetDateTime(makerDateTimeFrom, dateFormat, locale, LocalTime.MIN); + OffsetDateTime parsed = tryParseDayMonthYear(makerDateTimeFrom, LocalTime.MIN); + return parsed != null ? parsed + : DateUtils.convertDateTimeStringToOffsetDateTime(makerDateTimeFrom, dateFormat, locale, LocalTime.MIN); } public OffsetDateTime getMakerDateTimeTo() { - return DateUtils.convertDateTimeStringToOffsetDateTime(makerDateTimeTo, dateFormat, locale, LocalTime.MAX); + OffsetDateTime parsed = tryParseDayMonthYear(makerDateTimeTo, LocalTime.MAX); + return parsed != null ? parsed + : DateUtils.convertDateTimeStringToOffsetDateTime(makerDateTimeTo, dateFormat, locale, LocalTime.MAX); + } + + private static final DateTimeFormatter DAY_MONTH_YEAR = DateTimeFormatter.ofPattern("dd MMMM yyyy", Locale.ENGLISH); + + private static OffsetDateTime tryParseDayMonthYear(String value, LocalTime time) { + if (StringUtils.isBlank(value)) { + return null; + } + try { + LocalDate date = LocalDate.parse(value.trim(), DAY_MONTH_YEAR); + return date.atTime(time).atOffset(ZoneOffset.UTC); + } catch (DateTimeParseException e) { + return null; + } } } From a0c6ed437086cbd2f4d3d890a7299ed29bd18120 Mon Sep 17 00:00:00 2001 From: "aya.abdallah" Date: Fri, 24 Jul 2026 10:47:51 +0300 Subject: [PATCH 2/2] FINERACT-2683: Add integration tests for username filter and date format in maker-checker inbox --- .../integrationtests/MakercheckerTest.java | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/MakercheckerTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/MakercheckerTest.java index a6a92a64158..2224212790b 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/MakercheckerTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/MakercheckerTest.java @@ -271,6 +271,106 @@ public void testRejectDatatableCreationCleansUpOrphanedTable(String apptableName } } + @Test + public void testMakerCheckerUsernameFilter() { + globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.MAKER_CHECKER, + new PutGlobalConfigurationsRequest().enabled(true)); + globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_SAME_MAKER_CHECKER, + new PutGlobalConfigurationsRequest().enabled(false)); + + try { + PutPermissionsRequest putPermissionsRequest = new PutPermissionsRequest().putPermissionsItem("CREATE_CLIENT", true); + rolesHelper.updatePermissions(putPermissionsRequest); + + Integer roleId = RolesHelper.createRole(requestSpec, responseSpec); + Map permissionMap = Map.of("CREATE_CLIENT", true, "CREATE_CLIENT_CHECKER", true); + RolesHelper.addPermissionsToRole(requestSpec, responseSpec, roleId, permissionMap); + final Integer staffId = StaffHelper.createStaff(this.requestSpec, this.responseSpec); + + String maker1 = Utils.uniqueRandomStringGenerator("user", 8); + String maker2 = Utils.uniqueRandomStringGenerator("user", 8); + UserHelper.createUser(this.requestSpec, this.responseSpec, roleId, staffId, maker1, "A1b2c3d4e5f$", "resourceId"); + UserHelper.createUser(this.requestSpec, this.responseSpec, roleId, staffId, maker2, "A1b2c3d4e5f$", "resourceId"); + + RequestSpecification maker1RequestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build() + .header("Authorization", + "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey(maker1, "A1b2c3d4e5f$")); + RequestSpecification maker2RequestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build() + .header("Authorization", + "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey(maker2, "A1b2c3d4e5f$")); + + ClientHelper.createClient(maker1RequestSpec, this.responseSpec); + ClientHelper.createClient(maker2RequestSpec, this.responseSpec); + + List> maker1Results = makercheckersHelper + .getMakerCheckerList(Map.of("username", maker1, "actionName", "CREATE", "entityName", "CLIENT")); + assertEquals(1, maker1Results.size(), "Username filter should return only maker1's commands"); + assertEquals(maker1, maker1Results.get(0).get("maker")); + + List> maker2Results = makercheckersHelper + .getMakerCheckerList(Map.of("username", maker2, "actionName", "CREATE", "entityName", "CLIENT")); + assertEquals(1, maker2Results.size(), "Username filter should return only maker2's commands"); + assertEquals(maker2, maker2Results.get(0).get("maker")); + + List> noResults = makercheckersHelper.getMakerCheckerList(Map.of("username", "nonexistentuserxyz_999")); + assertEquals(0, noResults.size(), "Unknown username should return no results"); + } finally { + globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.MAKER_CHECKER, + new PutGlobalConfigurationsRequest().enabled(false)); + globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_SAME_MAKER_CHECKER, + new PutGlobalConfigurationsRequest().enabled(true)); + PutPermissionsRequest putPermissionsRequest = new PutPermissionsRequest().putPermissionsItem("CREATE_CLIENT", false); + rolesHelper.updatePermissions(putPermissionsRequest); + } + } + + @Test + public void testMakerCheckerDateFilterWithDayMonthYearFormat() { + globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.MAKER_CHECKER, + new PutGlobalConfigurationsRequest().enabled(true)); + globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_SAME_MAKER_CHECKER, + new PutGlobalConfigurationsRequest().enabled(false)); + + try { + PutPermissionsRequest putPermissionsRequest = new PutPermissionsRequest().putPermissionsItem("CREATE_CLIENT", true); + rolesHelper.updatePermissions(putPermissionsRequest); + + Integer roleId = RolesHelper.createRole(requestSpec, responseSpec); + Map permissionMap = Map.of("CREATE_CLIENT", true, "CREATE_CLIENT_CHECKER", true); + RolesHelper.addPermissionsToRole(requestSpec, responseSpec, roleId, permissionMap); + final Integer staffId = StaffHelper.createStaff(this.requestSpec, this.responseSpec); + + String maker = Utils.uniqueRandomStringGenerator("user", 8); + final Integer makerUserId = (Integer) UserHelper.createUser(this.requestSpec, this.responseSpec, roleId, staffId, maker, + "A1b2c3d4e5f$", "resourceId"); + RequestSpecification makerRequestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build() + .header("Authorization", + "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey(maker, "A1b2c3d4e5f$")); + + ClientHelper.createClient(makerRequestSpec, this.responseSpec); + + // "dd MMMM yyyy" format without dateFormat/locale — previously caused 500 error + List> fromOnly = makercheckersHelper.getMakerCheckerList( + Map.of("makerId", makerUserId.toString(), "makerDateTimeFrom", "01 January 2020")); + assertEquals(1, fromOnly.size(), "'dd MMMM yyyy' from-date filter should include today's pending command"); + + List> fromAndTo = makercheckersHelper.getMakerCheckerList(Map.of("makerId", makerUserId.toString(), + "makerDateTimeFrom", "01 January 2020", "makerDateTimeTo", "31 December 2030")); + assertEquals(1, fromAndTo.size(), "'dd MMMM yyyy' date range filter should include today's pending command"); + + List> pastRange = makercheckersHelper.getMakerCheckerList(Map.of("makerId", makerUserId.toString(), + "makerDateTimeFrom", "01 January 2020", "makerDateTimeTo", "31 December 2020")); + assertEquals(0, pastRange.size(), "Past date range should exclude today's pending command"); + } finally { + globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.MAKER_CHECKER, + new PutGlobalConfigurationsRequest().enabled(false)); + globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_SAME_MAKER_CHECKER, + new PutGlobalConfigurationsRequest().enabled(true)); + PutPermissionsRequest putPermissionsRequest = new PutPermissionsRequest().putPermissionsItem("CREATE_CLIENT", false); + rolesHelper.updatePermissions(putPermissionsRequest); + } + } + private Integer createSavingsProductDailyPosting() { final String savingsProductJSON = this.savingsProductHelper.withInterestCompoundingPeriodTypeAsDaily() .withInterestPostingPeriodTypeAsDaily().withInterestCalculationPeriodTypeAsDailyBalance().build();