Add CC accounts tracking, AML rule trial and branch approval fixes
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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (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 E2E Tests / E2E Tests (Shard 6 of 10) (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 - 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 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 Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 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 - 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-5) (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 - 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 Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (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 E2E Tests / E2E Tests (Shard 6 of 10) (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 - 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 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 Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 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 - 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-5) (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 - 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 Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
This commit is contained in:
+2
@@ -42,6 +42,8 @@ public enum JobName {
|
||||
POST_SHARE_ENTRY_FEE_TO_PROFIT("Post Share Entry Fee To Profit"), //
|
||||
UPDATE_SAVINGS_DORMANT_ACCOUNTS("Update Savings Dormant Accounts"), //
|
||||
APPLY_KYC_EXPIRY_RESTRICTIONS("Apply KYC Expiry Restrictions"), //
|
||||
SEND_KYC_REMINDERS("Send KYC Reminders"), //
|
||||
PROCESS_KYC_NOTICE_QUEUE("Process KYC Notice Queue"), //
|
||||
SAVINGS_DAILY_INTEREST("Savings Daily Interest Accrual And Posting"), //
|
||||
SAVINGS_INTEREST_POSTING_REPORT("Generate Savings Interest Posting Report"), //
|
||||
FD_INTEREST_POSTING_REPORT("Generate Fixed Deposit Interest Posting Report"), //
|
||||
|
||||
+10
-1
@@ -83,15 +83,24 @@ public class SavingsAccountSummaryData {
|
||||
* on. Lets a client's account list collapse a renewal chain into one FD instead of listing each term separately.
|
||||
*/
|
||||
private final Integer renewalCycleNo;
|
||||
/**
|
||||
* The branch this account is held at: the account's own office when it was opened at another branch than the
|
||||
* client's, else the client's (or the group's) office. Lets a customer with accounts at several branches tell
|
||||
* them apart on the client page.
|
||||
*/
|
||||
private final Long officeId;
|
||||
private final String officeName;
|
||||
|
||||
public SavingsAccountSummaryData(final Long id, final String accountNo, final String externalId, final Long productId,
|
||||
final String productName, final String shortProductName, final SavingsAccountStatusEnumData status, final CurrencyData currency,
|
||||
final BigDecimal accountBalance, final BigDecimal onHoldFunds, final BigDecimal savingsAmountOnHold,
|
||||
final BigDecimal availableBalance, final EnumOptionData accountType, final SavingsAccountApplicationTimelineData timeline,
|
||||
final EnumOptionData depositType, final SavingsAccountSubStatusEnumData subStatus, final LocalDate lastActiveTransactionDate,
|
||||
final String fdAccountNumber, final Integer renewalCycleNo) {
|
||||
final String fdAccountNumber, final Integer renewalCycleNo, final Long officeId, final String officeName) {
|
||||
this.fdAccountNumber = fdAccountNumber;
|
||||
this.renewalCycleNo = renewalCycleNo;
|
||||
this.officeId = officeId;
|
||||
this.officeName = officeName;
|
||||
this.id = id;
|
||||
this.accountNo = accountNo;
|
||||
this.externalId = externalId;
|
||||
|
||||
+5
-1
@@ -21,7 +21,11 @@ package org.apache.fineract.portfolio.aml.domain;
|
||||
/** Which transaction type(s) an {@link AmlTransactionRule} evaluates against. */
|
||||
public enum AmlAppliesTo {
|
||||
|
||||
SAVINGS, LOAN, TRANSFER, ALL;
|
||||
/**
|
||||
* CURRENT is a current / Cash Credit account. The engine is not yet fed current-account transactions, so a rule written
|
||||
* for it is catalogued but does not fire until it is.
|
||||
*/
|
||||
SAVINGS, LOAN, TRANSFER, CURRENT, ALL;
|
||||
|
||||
public boolean matches(AmlTransactionType transactionType) {
|
||||
return this == ALL || this.name().equals(transactionType.name());
|
||||
|
||||
+2
-12
@@ -20,14 +20,12 @@ package org.apache.fineract.portfolio.aml.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
import org.apache.fineract.portfolio.client.domain.Client;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
|
||||
@@ -40,11 +38,7 @@ import org.apache.fineract.useradministration.domain.AppUser;
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "m_aml_recurring_review")
|
||||
public class AmlRecurringReview {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
public class AmlRecurringReview extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "client_id", nullable = false)
|
||||
@@ -107,10 +101,6 @@ public class AmlRecurringReview {
|
||||
this.alertsActioned = count;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public Client getClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
+9
@@ -152,6 +152,15 @@ public class AmlTransactionRule {
|
||||
return rule;
|
||||
}
|
||||
|
||||
/**
|
||||
* Narrows a rule to one direction - CREDIT (money in) or DEBIT (money out), null for either - and, when asked, to cash
|
||||
* only. The engine already honours both; a rule written without them applies to every credit and debit, cash or not.
|
||||
*/
|
||||
public void restrictTo(final String direction, final boolean cashOnly) {
|
||||
this.direction = direction;
|
||||
this.cashOnly = cashOnly;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.client.domain;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* The comparable fingerprint of an Aadhaar number: the SHA-256 of the 12 digits, as 64 lowercase hex characters.
|
||||
*
|
||||
* <p>
|
||||
* The hash is computed by the client (browser) and only the hash is sent. The KYC create/update calls go through the
|
||||
* command source, which records the request body in an audit table and in the log, so the 12-digit number must not be
|
||||
* in the body. The server therefore never sees the number and only checks that what it was given has the shape of a
|
||||
* SHA-256 digest.
|
||||
*
|
||||
* <p>
|
||||
* A plain, unkeyed SHA-256 of a 12-digit number can be reversed by trying every possible number, so this column is a
|
||||
* lookup key, not protection against someone who can read the table.
|
||||
*/
|
||||
public final class AadhaarHash {
|
||||
|
||||
private static final Pattern SHA256_HEX = Pattern.compile("^[0-9a-f]{64}$");
|
||||
|
||||
private AadhaarHash() {}
|
||||
|
||||
/** True when the value has the shape of a lowercase hex SHA-256 digest. */
|
||||
public static boolean isValid(final String value) {
|
||||
return value != null && SHA256_HEX.matcher(value).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* The value trimmed and lower-cased when it is a well-formed digest; null for blank input. A non-blank value that
|
||||
* is not a digest is rejected rather than silently dropped, so a client bug cannot disable the duplicate check.
|
||||
*/
|
||||
public static String normalizeOrNull(final String raw) {
|
||||
if (raw == null || raw.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
final String candidate = raw.trim().toLowerCase();
|
||||
if (!isValid(candidate)) {
|
||||
throw new IllegalArgumentException("aadhaarHash must be a 64-character hex SHA-256 digest");
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
+27
@@ -151,6 +151,13 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
@Column(name = "aadhaar_number_masked", length = 12)
|
||||
private String aadhaarNumberMasked;
|
||||
|
||||
/**
|
||||
* SHA-256 of the 12-digit Aadhaar, computed client-side (see {@link AadhaarHash}). The masked form cannot tell two
|
||||
* people apart; this can. Null for rows that predate the column and for clients with no Aadhaar.
|
||||
*/
|
||||
@Column(name = "aadhaar_hash", length = 64)
|
||||
private String aadhaarHash;
|
||||
|
||||
@Column(name = "aadhaar_verified", nullable = false)
|
||||
private boolean aadhaarVerified;
|
||||
|
||||
@@ -391,6 +398,7 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
command.stringValueOfParameterNamed("aadhaarNumberMasked"),
|
||||
command.stringValueOfParameterNamed("gstinNumber"),
|
||||
command.stringValueOfParameterNamed("remarks"));
|
||||
kyc.aadhaarHash = AadhaarHash.normalizeOrNull(command.stringValueOfParameterNamed("aadhaarHash"));
|
||||
kyc.applyExtendedFields(command);
|
||||
return kyc;
|
||||
}
|
||||
@@ -493,6 +501,21 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
actualChanges.put(aadhaarParam, newValue);
|
||||
recordOverwrite(aadhaarParam, this.aadhaarNumberMasked);
|
||||
this.aadhaarNumberMasked = StringUtils.defaultIfEmpty(newValue, null);
|
||||
// The Aadhaar was removed: the fingerprint of the old number must not outlive it.
|
||||
if (this.aadhaarNumberMasked == null) {
|
||||
this.aadhaarHash = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Only the fingerprint is sent, never the number. Reported as a flag, not a value, because the changes map
|
||||
// is echoed back in the API response.
|
||||
final String aadhaarHashParam = "aadhaarHash";
|
||||
if (command.parameterExists(aadhaarHashParam)) {
|
||||
final String newHash = AadhaarHash.normalizeOrNull(command.stringValueOfParameterNamed(aadhaarHashParam));
|
||||
if (newHash != null && !newHash.equals(this.aadhaarHash)) {
|
||||
this.aadhaarHash = newHash;
|
||||
actualChanges.put("aadhaarIdentityUpdated", true);
|
||||
}
|
||||
}
|
||||
|
||||
final String gstinParam = "gstinNumber";
|
||||
@@ -1050,6 +1073,10 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
return aadhaarNumberMasked;
|
||||
}
|
||||
|
||||
public String getAadhaarHash() {
|
||||
return aadhaarHash;
|
||||
}
|
||||
|
||||
public boolean isAadhaarVerified() {
|
||||
return aadhaarVerified;
|
||||
}
|
||||
|
||||
+7
@@ -27,6 +27,13 @@ public interface ClientKycContactLogRepository extends JpaRepository<ClientKycCo
|
||||
|
||||
List<ClientKycContactLog> findByClientIdOrderByContactedAtAsc(Long clientId);
|
||||
|
||||
/** True when a notice carrying this marker has already reached a final outcome on the channel. */
|
||||
boolean existsByClientIdAndChannelAndNotesStartingWithAndDeliveryStatusIn(Long clientId, KycContactChannel channel,
|
||||
String markerPrefix, java.util.Collection<KycContactDeliveryStatus> statuses);
|
||||
|
||||
/** How many times a marked notice has been tried on the channel — bounds the retries of a failing gateway. */
|
||||
long countByClientIdAndChannelAndNotesStartingWith(Long clientId, KycContactChannel channel, String markerPrefix);
|
||||
|
||||
/**
|
||||
* Every contact log for the given clients in one round trip, so the exceptions grid can render
|
||||
* "last contacted" for the whole page without an N+1 query per row.
|
||||
|
||||
+12
@@ -61,6 +61,18 @@ public interface ClientKycRepository extends JpaRepository<ClientKyc, Long> {
|
||||
+ "AND k.kycStatus <> :rejectedStatus ORDER BY k.expiryDate ASC")
|
||||
List<ClientKyc> findLapsedAsOn(@Param("asOn") LocalDate asOn, @Param("rejectedStatus") Integer rejectedStatus);
|
||||
|
||||
/**
|
||||
* KYC records whose re-KYC falls due after {@code asOn} but no later than {@code until}, soonest first.
|
||||
*
|
||||
* <p>The complement of {@link #findLapsedAsOn}: a record is in one list or the other, never both, so the
|
||||
* two together cover everything that needs the branch's attention. Rejected records are excluded for
|
||||
* the same reason as there.
|
||||
*/
|
||||
@Query("SELECT k FROM ClientKyc k WHERE k.expiryDate IS NOT NULL AND k.expiryDate > :asOn "
|
||||
+ "AND k.expiryDate <= :until AND k.kycStatus <> :rejectedStatus ORDER BY k.expiryDate ASC")
|
||||
List<ClientKyc> findExpiringBetween(@Param("asOn") LocalDate asOn, @Param("until") LocalDate until,
|
||||
@Param("rejectedStatus") Integer rejectedStatus);
|
||||
|
||||
// ── Pre-KYC uniqueness probes ───────────────────────────────────────────
|
||||
// Why: stop a second client from being created with the same PAN / Aadhaar / GSTIN BEFORE the
|
||||
// external KYC API is called, so we don't burn vendor calls on duplicate-identity records. PAN
|
||||
|
||||
+47
@@ -27,6 +27,7 @@ import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
|
||||
|
||||
@Entity
|
||||
@@ -110,11 +111,57 @@ public class ClientRiskRule {
|
||||
@Column(name = "rule_kind", length = 10)
|
||||
private String ruleKind;
|
||||
|
||||
/**
|
||||
* Whether this scenario applies to Cash Credit accounts - a current account that is allowed to draw an overdraft
|
||||
* against a sanctioned limit. Lets the Rules Master show, in one place, every scenario that watches a CC account,
|
||||
* whatever its coverage status.
|
||||
*/
|
||||
@Column(name = "cc_relevant", nullable = false)
|
||||
private boolean ccRelevant;
|
||||
|
||||
/**
|
||||
* A rule authored from the Rules Master starts on trial: it is listed first and marked new until an officer marks it
|
||||
* normal. Every rule that was already in the catalog is normal.
|
||||
*/
|
||||
@Column(name = "on_trial", nullable = false)
|
||||
private boolean onTrial;
|
||||
|
||||
@Column(name = "trial_started_on")
|
||||
private LocalDate trialStartedOn;
|
||||
|
||||
public static final String KIND_KYC = "KYC";
|
||||
public static final String KIND_AML = "AML";
|
||||
|
||||
protected ClientRiskRule() {}
|
||||
|
||||
public boolean isCcRelevant() {
|
||||
return ccRelevant;
|
||||
}
|
||||
|
||||
public boolean isOnTrial() {
|
||||
return onTrial;
|
||||
}
|
||||
|
||||
public LocalDate getTrialStartedOn() {
|
||||
return trialStartedOn;
|
||||
}
|
||||
|
||||
/** Puts a newly authored rule on trial from {@code startedOn}. */
|
||||
public void startTrial(final LocalDate startedOn) {
|
||||
this.onTrial = true;
|
||||
this.trialStartedOn = startedOn;
|
||||
}
|
||||
|
||||
/** Ends the trial: the rule is a normal part of the catalog. */
|
||||
public void markNormal() {
|
||||
this.onTrial = false;
|
||||
}
|
||||
|
||||
/** Marks the scenario as one that watches Cash Credit accounts, so the Rules Master CC chip lists it. */
|
||||
public void markCcRelevant(final boolean ccRelevant) {
|
||||
this.ccRelevant = ccRelevant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a rule authored from the AML Rules Master screen. Occupation and occupation-category
|
||||
* are attached separately by the caller, which owns the CodeValue lookup.
|
||||
|
||||
+2
-1
@@ -26,7 +26,8 @@ public interface SearchingClientRepository {
|
||||
/**
|
||||
* @param legalFormValue null = no filter; 1 = Individual only; 2 = Non-Individual only.
|
||||
* Driven by the legal-form enum on Client (LegalForm.PERSON / .ENTITY).
|
||||
* @param officeId null = no filter; otherwise only clients of that office, still inside officeHierarchy.
|
||||
*/
|
||||
Page<SearchedClient> searchByText(String searchText, Pageable pageable, String officeHierarchy, boolean activeOnly,
|
||||
Integer legalFormValue);
|
||||
Integer legalFormValue, Long officeId);
|
||||
}
|
||||
|
||||
+34
-2
@@ -53,7 +53,7 @@ public class SearchingClientRepositoryImpl implements SearchingClientRepository
|
||||
|
||||
@Override
|
||||
public Page<SearchedClient> searchByText(String searchText, Pageable pageable, String officeHierarchy, boolean activeOnly,
|
||||
Integer legalFormValue) {
|
||||
Integer legalFormValue, Long officeId) {
|
||||
/*
|
||||
* this whole thing can be replaced with Spring Data JPA 3+ with a findBy(Specification, Pageable) call but at
|
||||
* this point the upgrade is too costly
|
||||
@@ -62,6 +62,9 @@ public class SearchingClientRepositoryImpl implements SearchingClientRepository
|
||||
*/
|
||||
String hierarchyLikeValue = officeHierarchy + "%";
|
||||
|
||||
// Customers of other branches that hold an account at the caller's branch are listed here too.
|
||||
final List<Long> linkedClientIds = clientIdsWithAccountAtBranch(hierarchyLikeValue);
|
||||
|
||||
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
|
||||
CriteriaQuery<SearchedClient> query = cb.createQuery(SearchedClient.class);
|
||||
|
||||
@@ -74,7 +77,8 @@ public class SearchingClientRepositoryImpl implements SearchingClientRepository
|
||||
Join<Client, ClientIdentifier> identity = r.join("identifiers", JoinType.LEFT);
|
||||
|
||||
List<Predicate> predicates = new ArrayList<>();
|
||||
predicates.add(cb.like(o.get("hierarchy"), hierarchyLikeValue));
|
||||
final Predicate inHierarchy = cb.like(o.get("hierarchy"), hierarchyLikeValue);
|
||||
predicates.add(linkedClientIds.isEmpty() ? inHierarchy : cb.or(inHierarchy, r.get("id").in(linkedClientIds)));
|
||||
|
||||
String searchLikeValue = "%" + searchText + "%";
|
||||
predicates.add(cb.or(cb.like(r.get("accountNumber"), searchLikeValue), cb.like(r.get("cifNumber"), searchLikeValue),
|
||||
@@ -93,6 +97,11 @@ public class SearchingClientRepositoryImpl implements SearchingClientRepository
|
||||
predicates.add(cb.equal(r.get("legalForm"), legalFormValue));
|
||||
}
|
||||
|
||||
// Branch filter. ANDed with the hierarchy predicate above, so it can only narrow results.
|
||||
if (officeId != null) {
|
||||
predicates.add(cb.equal(o.get("id"), officeId));
|
||||
}
|
||||
|
||||
return cb.and(predicates.toArray(new Predicate[0]));
|
||||
};
|
||||
criteriaQueryFactory.applySpecificationToCriteria(root, spec, query);
|
||||
@@ -108,4 +117,27 @@ public class SearchingClientRepositoryImpl implements SearchingClientRepository
|
||||
|
||||
return criteriaQueryFactory.readPage(queryToExecute, Client.class, pageable, spec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ids of clients holding an account (savings, fixed or recurring deposit, or current) at an office at or below the
|
||||
* given hierarchy pattern, whatever office the client itself belongs to. Only accounts opened at a branch other than
|
||||
* the client's carry their own office, so this is empty for a branch with no such customers, and for Head Office.
|
||||
* Being linked to a branch is deliberately not enough to be listed there.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<Long> clientIdsWithAccountAtBranch(final String hierarchyLikeValue) {
|
||||
final List<Object> rows = entityManager.createNativeQuery(
|
||||
"SELECT sa.client_id FROM m_savings_account sa JOIN m_office ao ON ao.id = sa.office_id "
|
||||
+ "WHERE sa.client_id IS NOT NULL AND ao.hierarchy LIKE ?1 "
|
||||
+ "UNION SELECT ca.client_id FROM m_current_account ca JOIN m_office co ON co.id = ca.office_id "
|
||||
+ "WHERE ca.client_id IS NOT NULL AND co.hierarchy LIKE ?2")
|
||||
// Positional, not named: EclipseLink passes a native query's ":h" to PostgreSQL as written, which fails
|
||||
// with a syntax error and took the whole client list down with it.
|
||||
.setParameter(1, hierarchyLikeValue).setParameter(2, hierarchyLikeValue).getResultList();
|
||||
final List<Long> ids = new ArrayList<>(rows.size());
|
||||
for (final Object row : rows) {
|
||||
ids.add(((Number) row).longValue());
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ public enum NoteType {
|
||||
GROUP(600, "noteType.group", "groups", "Group note"), //
|
||||
SHARE_ACCOUNT(700, "noteType.shares", "accounts/share", "Share account note"), //
|
||||
SAVINGS_TRANSACTION(800, "noteType.savings.transaction", "savingsTransactions", "Savings transaction note"), //
|
||||
CURRENT_ACCOUNT(900, "noteType.currentaccount", "currentaccounts", "Current account note"), //
|
||||
;
|
||||
|
||||
public static final NoteType[] VALUES = values();
|
||||
|
||||
Reference in New Issue
Block a user