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; + } } } 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..827cdffd301 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,103 @@ 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();