Compare commits

...
2 Commits
Author SHA1 Message Date
nitesh b1cfa996b4 feat(clients): fromDate/toDate submittedon_date range filter on GET /clients
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
Backs the Client Approval tab's Today/Yesterday quick filters server-side.
offset/limit/displayName/orderBy/sortOrder/status already existed on this
endpoint; added fromDate/toDate (yyyy-MM-dd, inclusive) alongside them,
filtering on c.submittedon_date, following the same ::date-cast convention
already used by ClientPendingEditReadPlatformServiceImpl for the
equivalent pending-edits filter. Updated the one other caller
(SelfClientsApiResource) to pass null/null.
2026-07-30 10:33:48 +05:30
nitesh 02541eed6c feat(sanctions): expose the loaded list's data-currency date on every screening result
Screening only ever told a caller whether a name matched and when the
check ran — not which dated snapshot of the UN Security Council list it
ran against. Added SanctionsEntryRepository.findListLastLoadedOn()
(MAX(createdDate) over active entries) and threaded it through
SanctionsScreeningResult as listDataAsOf, so the frontend can show "List
Data As Of: <date>" alongside "Screened On".
2026-07-30 10:33:33 +05:30
6 changed files with 48 additions and 14 deletions
@@ -18,7 +18,9 @@
*/
package org.apache.fineract.portfolio.sanctions.domain;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
@@ -40,4 +42,11 @@ public interface SanctionsEntryRepository extends JpaRepository<SanctionsEntry,
void deleteAllByActiveTrue();
long countByActiveTrue();
/**
* Timestamp the active sanctions list was last loaded into the database — i.e. the data
* currency shown to callers so they know which list snapshot a screening ran against.
*/
@Query("SELECT MAX(s.createdDate) FROM SanctionsEntry s WHERE s.active = true")
Optional<OffsetDateTime> findListLastLoadedOn();
}
@@ -44,6 +44,7 @@ import java.time.LocalDate;
import java.util.Collection;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.commands.data.AuditData;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.AuditReadPlatformService;
@@ -142,7 +143,8 @@ public class ClientsApiResource {
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List Clients", description = "The list capability of clients can support pagination and sorting.\n\n"
+ "Example Requests:\n" + "\n" + "clients\n" + "\n" + "clients?fields=displayName,officeName,timeline\n" + "\n"
+ "clients?offset=10&limit=50\n" + "\n" + "clients?orderBy=displayName&sortOrder=DESC")
+ "clients?offset=10&limit=50\n" + "\n" + "clients?orderBy=displayName&sortOrder=DESC\n\n"
+ "clients?fromDate=2026-01-01&toDate=2026-01-31 (submittedon_date range, yyyy-MM-dd, inclusive)")
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = ClientsApiResourceSwagger.GetClientsResponse.class)))
public String retrieveAll(@Context final UriInfo uriInfo,
@QueryParam("officeId") @Parameter(description = "officeId") final Long officeId,
@@ -157,10 +159,15 @@ public class ClientsApiResource {
@QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy,
@QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder,
@QueryParam("orphansOnly") @Parameter(description = "orphansOnly") final Boolean orphansOnly,
@QueryParam("legalForm") final Integer legalForm) {
@QueryParam("legalForm") final Integer legalForm,
@QueryParam("fromDate") @Parameter(description = "fromDate") final String fromDate,
@QueryParam("toDate") @Parameter(description = "toDate") final String toDate) {
final LocalDate fromDateParsed = StringUtils.isNotBlank(fromDate) ? LocalDate.parse(fromDate) : null;
final LocalDate toDateParsed = StringUtils.isNotBlank(toDate) ? LocalDate.parse(toDate) : null;
return retrieveAll(uriInfo, officeId, externalId, displayName, firstname, lastname, status, legalForm, hierarchy, offset, limit,
orderBy, sortOrder, orphansOnly, false);
orderBy, sortOrder, orphansOnly, false, fromDateParsed, toDateParsed);
}
@GET
@@ -437,7 +444,7 @@ public class ClientsApiResource {
public String retrieveAll(final UriInfo uriInfo, final Long officeId, final String externalId, final String displayName,
final String firstname, final String lastname, final String status, final Integer legalForm, final String hierarchy,
final Integer offset, final Integer limit, final String orderBy, final String sortOrder, final Boolean orphansOnly,
final boolean isSelfUser) {
final boolean isSelfUser, final LocalDate fromDate, final LocalDate toDate) {
context.authenticatedUser().validateHasReadPermission(ClientApiConstants.CLIENT_RESOURCE_NAME);
sqlValidator.validate(orderBy);
sqlValidator.validate(sortOrder);
@@ -445,7 +452,8 @@ public class ClientsApiResource {
sqlValidator.validate(hierarchy);
final SearchParameters searchParameters = SearchParameters.builder().limit(limit).officeId(officeId).externalId(externalId)
.name(displayName).hierarchy(hierarchy).firstname(firstname).lastname(lastname).status(status).orphansOnly(orphansOnly)
.isSelfUser(isSelfUser).offset(offset).orderBy(orderBy).sortOrder(sortOrder).legalForm(legalForm).build();
.isSelfUser(isSelfUser).offset(offset).orderBy(orderBy).sortOrder(sortOrder).legalForm(legalForm).fromDate(fromDate)
.toDate(toDate).build();
final Page<ClientData> clientData = clientReadPlatformService.retrieveAll(searchParameters);
final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return toApiJsonSerializer.serialize(settings, clientData, ClientApiConstants.CLIENT_RESPONSE_DATA_PARAMETERS);
@@ -178,6 +178,16 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
extraCriteria += " and c.status_enum = " + clientStatus.getValue().toString() + " ";
}
if (searchParameters.hasFromDate()) {
paramList.add(searchParameters.getFromDate().toString());
extraCriteria += " and c.submittedon_date >= ?::date ";
}
if (searchParameters.hasToDate()) {
paramList.add(searchParameters.getToDate().toString());
extraCriteria += " and c.submittedon_date <= ?::date ";
}
if (firstname != null) {
paramList.add(firstname);
extraCriteria += " and c.firstname like ? ";
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.portfolio.sanctions.data;
import java.time.OffsetDateTime;
import java.util.List;
/**
@@ -32,10 +33,11 @@ public final class SanctionsScreeningResult {
private final String message;
private final int matchCount;
private final List<SanctionsMatchData> matches;
private final OffsetDateTime listDataAsOf;
private SanctionsScreeningResult(final Long screeningLogId, final String screenedName, final boolean matchFound,
final String status, final String message, final int matchCount,
final List<SanctionsMatchData> matches) {
final List<SanctionsMatchData> matches, final OffsetDateTime listDataAsOf) {
this.screeningLogId = screeningLogId;
this.screenedName = screenedName;
this.matchFound = matchFound;
@@ -43,20 +45,22 @@ public final class SanctionsScreeningResult {
this.message = message;
this.matchCount = matchCount;
this.matches = matches;
this.listDataAsOf = listDataAsOf;
}
public static SanctionsScreeningResult matched(final Long screeningLogId, final String screenedName,
final List<SanctionsMatchData> matches) {
final List<SanctionsMatchData> matches, final OffsetDateTime listDataAsOf) {
return new SanctionsScreeningResult(screeningLogId, screenedName, true, "FLAGGED",
"Potential match found in UN Security Council consolidated sanctions list. "
+ "Manual review required before proceeding.",
matches.size(), matches);
matches.size(), matches, listDataAsOf);
}
public static SanctionsScreeningResult clear(final Long screeningLogId, final String screenedName) {
public static SanctionsScreeningResult clear(final Long screeningLogId, final String screenedName,
final OffsetDateTime listDataAsOf) {
return new SanctionsScreeningResult(screeningLogId, screenedName, false, "CLEAR",
"No match found in UN Security Council consolidated sanctions list.",
0, List.of());
0, List.of(), listDataAsOf);
}
public Long getScreeningLogId() { return screeningLogId; }
@@ -66,4 +70,5 @@ public final class SanctionsScreeningResult {
public String getMessage() { return message; }
public int getMatchCount() { return matchCount; }
public List<SanctionsMatchData> getMatches() { return matches; }
public OffsetDateTime getListDataAsOf() { return listDataAsOf; }
}
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.portfolio.sanctions.service;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
@@ -54,18 +55,19 @@ public class SanctionsScreeningServiceImpl implements SanctionsScreeningService
final Long clientId, final String ipAddress) {
if (name == null || name.isBlank()) {
final Long logId = saveLog(name, "CLEAR", 0, null, null, source, clientId, ipAddress);
return SanctionsScreeningResult.clear(logId, name);
return SanctionsScreeningResult.clear(logId, name, sanctionsEntryRepository.findListLastLoadedOn().orElse(null));
}
final String trimmedName = name.trim();
LOG.info("Screening name against sanctions list: {}", trimmedName);
final OffsetDateTime listDataAsOf = sanctionsEntryRepository.findListLastLoadedOn().orElse(null);
final List<SanctionsEntry> matches = sanctionsEntryRepository.searchByNameOrAlias(trimmedName);
if (matches.isEmpty()) {
LOG.info("No sanctions match found for: {}", trimmedName);
final Long logId = saveLog(trimmedName, "CLEAR", 0, null, null, source, clientId, ipAddress);
return SanctionsScreeningResult.clear(logId, trimmedName);
return SanctionsScreeningResult.clear(logId, trimmedName, listDataAsOf);
}
LOG.warn("Sanctions match found for '{}': {} potential match(es)", trimmedName, matches.size());
@@ -84,7 +86,7 @@ public class SanctionsScreeningServiceImpl implements SanctionsScreeningService
final Long logId = saveLog(trimmedName, "FLAGGED", matches.size(), matchedEntryIds, matchedNames, source, clientId, ipAddress);
return SanctionsScreeningResult.matched(logId, trimmedName, matchDataList);
return SanctionsScreeningResult.matched(logId, trimmedName, matchDataList, listDataAsOf);
}
@Override
@@ -135,7 +135,7 @@ public class SelfClientsApiResource {
final String hierarchy = null;
final Boolean orphansOnly = null;
return this.clientApiResource.retrieveAll(uriInfo, officeId, externalId, displayName, firstname, lastname, status, legalForm,
hierarchy, offset, limit, orderBy, sortOrder, orphansOnly, true);
hierarchy, offset, limit, orderBy, sortOrder, orphansOnly, true, null, null);
}
@GET