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; package org.apache.fineract.portfolio.sanctions.domain;
import java.time.OffsetDateTime;
import java.util.List; import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
@@ -40,4 +42,11 @@ public interface SanctionsEntryRepository extends JpaRepository<SanctionsEntry,
void deleteAllByActiveTrue(); void deleteAllByActiveTrue();
long countByActiveTrue(); 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.Collection;
import java.util.List; import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.commands.data.AuditData; import org.apache.fineract.commands.data.AuditData;
import org.apache.fineract.commands.domain.CommandWrapper; import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.AuditReadPlatformService; import org.apache.fineract.commands.service.AuditReadPlatformService;
@@ -142,7 +143,8 @@ public class ClientsApiResource {
@Produces({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List Clients", description = "The list capability of clients can support pagination and sorting.\n\n" @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" + "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))) @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = ClientsApiResourceSwagger.GetClientsResponse.class)))
public String retrieveAll(@Context final UriInfo uriInfo, public String retrieveAll(@Context final UriInfo uriInfo,
@QueryParam("officeId") @Parameter(description = "officeId") final Long officeId, @QueryParam("officeId") @Parameter(description = "officeId") final Long officeId,
@@ -157,10 +159,15 @@ public class ClientsApiResource {
@QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy, @QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy,
@QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder, @QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder,
@QueryParam("orphansOnly") @Parameter(description = "orphansOnly") final Boolean orphansOnly, @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, return retrieveAll(uriInfo, officeId, externalId, displayName, firstname, lastname, status, legalForm, hierarchy, offset, limit,
orderBy, sortOrder, orphansOnly, false); orderBy, sortOrder, orphansOnly, false, fromDateParsed, toDateParsed);
} }
@GET @GET
@@ -437,7 +444,7 @@ public class ClientsApiResource {
public String retrieveAll(final UriInfo uriInfo, final Long officeId, final String externalId, final String displayName, 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 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 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); context.authenticatedUser().validateHasReadPermission(ClientApiConstants.CLIENT_RESOURCE_NAME);
sqlValidator.validate(orderBy); sqlValidator.validate(orderBy);
sqlValidator.validate(sortOrder); sqlValidator.validate(sortOrder);
@@ -445,7 +452,8 @@ public class ClientsApiResource {
sqlValidator.validate(hierarchy); sqlValidator.validate(hierarchy);
final SearchParameters searchParameters = SearchParameters.builder().limit(limit).officeId(officeId).externalId(externalId) final SearchParameters searchParameters = SearchParameters.builder().limit(limit).officeId(officeId).externalId(externalId)
.name(displayName).hierarchy(hierarchy).firstname(firstname).lastname(lastname).status(status).orphansOnly(orphansOnly) .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 Page<ClientData> clientData = clientReadPlatformService.retrieveAll(searchParameters);
final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters()); final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return toApiJsonSerializer.serialize(settings, clientData, ClientApiConstants.CLIENT_RESPONSE_DATA_PARAMETERS); 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() + " "; 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) { if (firstname != null) {
paramList.add(firstname); paramList.add(firstname);
extraCriteria += " and c.firstname like ? "; extraCriteria += " and c.firstname like ? ";
@@ -18,6 +18,7 @@
*/ */
package org.apache.fineract.portfolio.sanctions.data; package org.apache.fineract.portfolio.sanctions.data;
import java.time.OffsetDateTime;
import java.util.List; import java.util.List;
/** /**
@@ -32,10 +33,11 @@ public final class SanctionsScreeningResult {
private final String message; private final String message;
private final int matchCount; private final int matchCount;
private final List<SanctionsMatchData> matches; private final List<SanctionsMatchData> matches;
private final OffsetDateTime listDataAsOf;
private SanctionsScreeningResult(final Long screeningLogId, final String screenedName, final boolean matchFound, private SanctionsScreeningResult(final Long screeningLogId, final String screenedName, final boolean matchFound,
final String status, final String message, final int matchCount, final String status, final String message, final int matchCount,
final List<SanctionsMatchData> matches) { final List<SanctionsMatchData> matches, final OffsetDateTime listDataAsOf) {
this.screeningLogId = screeningLogId; this.screeningLogId = screeningLogId;
this.screenedName = screenedName; this.screenedName = screenedName;
this.matchFound = matchFound; this.matchFound = matchFound;
@@ -43,20 +45,22 @@ public final class SanctionsScreeningResult {
this.message = message; this.message = message;
this.matchCount = matchCount; this.matchCount = matchCount;
this.matches = matches; this.matches = matches;
this.listDataAsOf = listDataAsOf;
} }
public static SanctionsScreeningResult matched(final Long screeningLogId, final String screenedName, 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", return new SanctionsScreeningResult(screeningLogId, screenedName, true, "FLAGGED",
"Potential match found in UN Security Council consolidated sanctions list. " "Potential match found in UN Security Council consolidated sanctions list. "
+ "Manual review required before proceeding.", + "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", return new SanctionsScreeningResult(screeningLogId, screenedName, false, "CLEAR",
"No match found in UN Security Council consolidated sanctions list.", "No match found in UN Security Council consolidated sanctions list.",
0, List.of()); 0, List.of(), listDataAsOf);
} }
public Long getScreeningLogId() { return screeningLogId; } public Long getScreeningLogId() { return screeningLogId; }
@@ -66,4 +70,5 @@ public final class SanctionsScreeningResult {
public String getMessage() { return message; } public String getMessage() { return message; }
public int getMatchCount() { return matchCount; } public int getMatchCount() { return matchCount; }
public List<SanctionsMatchData> getMatches() { return matches; } public List<SanctionsMatchData> getMatches() { return matches; }
public OffsetDateTime getListDataAsOf() { return listDataAsOf; }
} }
@@ -18,6 +18,7 @@
*/ */
package org.apache.fineract.portfolio.sanctions.service; package org.apache.fineract.portfolio.sanctions.service;
import java.time.OffsetDateTime;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -54,18 +55,19 @@ public class SanctionsScreeningServiceImpl implements SanctionsScreeningService
final Long clientId, final String ipAddress) { final Long clientId, final String ipAddress) {
if (name == null || name.isBlank()) { if (name == null || name.isBlank()) {
final Long logId = saveLog(name, "CLEAR", 0, null, null, source, clientId, ipAddress); 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(); final String trimmedName = name.trim();
LOG.info("Screening name against sanctions list: {}", trimmedName); LOG.info("Screening name against sanctions list: {}", trimmedName);
final OffsetDateTime listDataAsOf = sanctionsEntryRepository.findListLastLoadedOn().orElse(null);
final List<SanctionsEntry> matches = sanctionsEntryRepository.searchByNameOrAlias(trimmedName); final List<SanctionsEntry> matches = sanctionsEntryRepository.searchByNameOrAlias(trimmedName);
if (matches.isEmpty()) { if (matches.isEmpty()) {
LOG.info("No sanctions match found for: {}", trimmedName); LOG.info("No sanctions match found for: {}", trimmedName);
final Long logId = saveLog(trimmedName, "CLEAR", 0, null, null, source, clientId, ipAddress); 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()); 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); 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 @Override
@@ -135,7 +135,7 @@ public class SelfClientsApiResource {
final String hierarchy = null; final String hierarchy = null;
final Boolean orphansOnly = null; final Boolean orphansOnly = null;
return this.clientApiResource.retrieveAll(uriInfo, officeId, externalId, displayName, firstname, lastname, status, legalForm, 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 @GET