From abc7eb22a28a8212a17295b6c944e0505025b61b Mon Sep 17 00:00:00 2001 From: keshav Date: Sat, 1 Aug 2026 11:54:30 +0530 Subject: [PATCH] oss file updated, profitl loss as per the day month and also with date select --- .../aml/domain/AmlTransactionRule.java | 16 +++ .../service/AmlRuleEvaluationServiceImpl.java | 35 +++++- .../client/data/KycRiskRuleData.java | 39 ++++++- .../KycRiskRulesReadPlatformServiceImpl.java | 19 ++- .../db/changelog/tenant/changelog-tenant.xml | 3 + ...tiered_thresholds_and_low_balance_rule.xml | 85 ++++++++++++++ ...client_risk_rule_income_turnover_nulls.xml | 61 ++++++++++ .../parts/0440_seed_aml_test_alerts.xml | 108 ++++++++++++++++++ 8 files changed, 359 insertions(+), 7 deletions(-) create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0438_add_tiered_thresholds_and_low_balance_rule.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0439_fill_client_risk_rule_income_turnover_nulls.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0440_seed_aml_test_alerts.xml diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/aml/domain/AmlTransactionRule.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/aml/domain/AmlTransactionRule.java index 103c16bc8..1099f7f37 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/aml/domain/AmlTransactionRule.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/aml/domain/AmlTransactionRule.java @@ -52,6 +52,14 @@ public class AmlTransactionRule { @Column(name = "threshold_amount", precision = 19, scale = 6) private BigDecimal thresholdAmount; + /** Risk-category-tiered overrides of {@link #thresholdAmount}. Null on both means the rule is + * not tiered; {@link #thresholdAmount} is used for every client regardless of risk category. */ + @Column(name = "threshold_amount_medium_risk", precision = 19, scale = 6) + private BigDecimal thresholdAmountMediumRisk; + + @Column(name = "threshold_amount_high_risk", precision = 19, scale = 6) + private BigDecimal thresholdAmountHighRisk; + @Column(name = "rule_type", nullable = false, length = 20) private String ruleType; @@ -86,6 +94,14 @@ public class AmlTransactionRule { return thresholdAmount; } + public BigDecimal getThresholdAmountMediumRisk() { + return thresholdAmountMediumRisk; + } + + public BigDecimal getThresholdAmountHighRisk() { + return thresholdAmountHighRisk; + } + public AmlRuleType getRuleType() { return AmlRuleType.valueOf(ruleType); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/aml/service/AmlRuleEvaluationServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/aml/service/AmlRuleEvaluationServiceImpl.java index 8063dae09..dea405fb5 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/aml/service/AmlRuleEvaluationServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/aml/service/AmlRuleEvaluationServiceImpl.java @@ -18,6 +18,7 @@ */ package org.apache.fineract.portfolio.aml.service; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import lombok.RequiredArgsConstructor; @@ -27,6 +28,8 @@ import org.apache.fineract.portfolio.aml.domain.AmlAlertRepository; import org.apache.fineract.portfolio.aml.domain.AmlAppliesTo; import org.apache.fineract.portfolio.aml.domain.AmlTransactionRule; import org.apache.fineract.portfolio.aml.domain.AmlTransactionRuleRepository; +import org.apache.fineract.portfolio.client.domain.ClientKyc; +import org.apache.fineract.portfolio.client.domain.ClientKycRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -35,8 +38,12 @@ import org.springframework.transaction.annotation.Transactional; @Transactional public class AmlRuleEvaluationServiceImpl implements AmlRuleEvaluationService { + private static final int MEDIUM_RISK = 2; + private static final int HIGH_RISK = 3; + private final AmlTransactionRuleRepository ruleRepository; private final AmlAlertRepository alertRepository; + private final ClientKycRepository clientKycRepository; @Override public List evaluate(AmlTransactionContext context) { @@ -56,20 +63,42 @@ public class AmlRuleEvaluationServiceImpl implements AmlRuleEvaluationService { } private boolean isHit(AmlTransactionRule rule, AmlTransactionContext context) { - if (rule.getThresholdAmount() == null) { + final BigDecimal threshold = resolveThreshold(rule, context); + if (threshold == null) { // Pattern-only VELOCITY rules (e.g. same-day movement) need bespoke detection logic // per rule and are not yet evaluated here - they exist in the catalog for future work. return false; } switch (rule.getRuleType()) { case THRESHOLD: - return context.getAmount().compareTo(rule.getThresholdAmount()) >= 0; + return context.getAmount().compareTo(threshold) >= 0; case STRUCTURING: case VELOCITY: return context.getWindowAggregateAmount() != null - && context.getWindowAggregateAmount().compareTo(rule.getThresholdAmount()) >= 0; + && context.getWindowAggregateAmount().compareTo(threshold) >= 0; default: return false; } } + + /** + * Rules with both tiered columns null use {@code thresholdAmount} for every client, unchanged + * from prior behaviour. Tiered rules look up the transacting client's current KYC risk category + * and pick the matching tier, falling back to {@code thresholdAmount} for Low-risk/unclassified + * clients. + */ + private BigDecimal resolveThreshold(AmlTransactionRule rule, AmlTransactionContext context) { + if (rule.getThresholdAmountMediumRisk() == null && rule.getThresholdAmountHighRisk() == null) { + return rule.getThresholdAmount(); + } + final Integer riskCategory = clientKycRepository.findByClientId(context.getClient().getId()).map(ClientKyc::getRiskCategoryValue) + .orElse(null); + if (riskCategory != null && riskCategory == HIGH_RISK && rule.getThresholdAmountHighRisk() != null) { + return rule.getThresholdAmountHighRisk(); + } + if (riskCategory != null && riskCategory == MEDIUM_RISK && rule.getThresholdAmountMediumRisk() != null) { + return rule.getThresholdAmountMediumRisk(); + } + return rule.getThresholdAmount(); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/data/KycRiskRuleData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/data/KycRiskRuleData.java index 7bf4afad6..2c0ac90f4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/data/KycRiskRuleData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/data/KycRiskRuleData.java @@ -46,6 +46,16 @@ public final class KycRiskRuleData implements Serializable { private final String willfulDefaulter; private final String ecgcSalFlag; private final String dormantFlag; + /** Populated only for rules that also have a live-transaction-monitoring counterpart in + * {@code m_aml_transaction_rule} (linked via source_client_risk_rule_id) - distinct from the + * client-onboarding income/turnover fields above, which drive KYC-time risk categorization. */ + private final BigDecimal txnThresholdAmount; + private final String txnRuleType; + private final Integer txnWindowDays; + /** Risk-category-tiered overrides of {@link #txnThresholdAmount}, populated only when the + * linked {@code m_aml_transaction_rule} row has tiering configured (e.g. RTGS monitoring). */ + private final BigDecimal txnThresholdAmountMediumRisk; + private final BigDecimal txnThresholdAmountHighRisk; public KycRiskRuleData(final Long id, final String ruleName, final Integer legalFormId, final Long occupationCategoryCvId, final String occupationCategoryName, @@ -55,7 +65,9 @@ public final class KycRiskRuleData implements Serializable { final BigDecimal minAnnualTurnover, final BigDecimal maxAnnualTurnover, final Integer riskCategory, final String reason, final Integer orderPosition, final String amlCheckStatus, final String bannedEntityMatch, - final String willfulDefaulter, final String ecgcSalFlag, final String dormantFlag) { + final String willfulDefaulter, final String ecgcSalFlag, final String dormantFlag, + final BigDecimal txnThresholdAmount, final String txnRuleType, final Integer txnWindowDays, + final BigDecimal txnThresholdAmountMediumRisk, final BigDecimal txnThresholdAmountHighRisk) { this.id = id; this.ruleName = ruleName; this.legalFormId = legalFormId; @@ -77,6 +89,11 @@ public final class KycRiskRuleData implements Serializable { this.willfulDefaulter = willfulDefaulter; this.ecgcSalFlag = ecgcSalFlag; this.dormantFlag = dormantFlag; + this.txnThresholdAmount = txnThresholdAmount; + this.txnRuleType = txnRuleType; + this.txnWindowDays = txnWindowDays; + this.txnThresholdAmountMediumRisk = txnThresholdAmountMediumRisk; + this.txnThresholdAmountHighRisk = txnThresholdAmountHighRisk; } public Long getId() { @@ -162,4 +179,24 @@ public final class KycRiskRuleData implements Serializable { public String getDormantFlag() { return dormantFlag; } + + public BigDecimal getTxnThresholdAmount() { + return txnThresholdAmount; + } + + public String getTxnRuleType() { + return txnRuleType; + } + + public Integer getTxnWindowDays() { + return txnWindowDays; + } + + public BigDecimal getTxnThresholdAmountMediumRisk() { + return txnThresholdAmountMediumRisk; + } + + public BigDecimal getTxnThresholdAmountHighRisk() { + return txnThresholdAmountHighRisk; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/KycRiskRulesReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/KycRiskRulesReadPlatformServiceImpl.java index 285ecbe70..649ee68b4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/KycRiskRulesReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/KycRiskRulesReadPlatformServiceImpl.java @@ -19,8 +19,12 @@ package org.apache.fineract.portfolio.client.service; import java.util.List; +import java.util.Map; +import java.util.function.Function; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; +import org.apache.fineract.portfolio.aml.domain.AmlTransactionRule; +import org.apache.fineract.portfolio.aml.domain.AmlTransactionRuleRepository; import org.apache.fineract.portfolio.client.data.KycRiskRuleData; import org.apache.fineract.portfolio.client.domain.ClientRiskRule; import org.apache.fineract.portfolio.client.domain.ClientRiskRuleRepository; @@ -33,14 +37,18 @@ import org.springframework.transaction.annotation.Transactional; public class KycRiskRulesReadPlatformServiceImpl implements KycRiskRulesReadPlatformService { private final ClientRiskRuleRepository repository; + private final AmlTransactionRuleRepository amlTransactionRuleRepository; @Override public List retrieveAllRules() { final List rules = this.repository.findAllOrderedByOrderPosition(); - return rules.stream().map(this::mapToData).collect(Collectors.toList()); + final Map txnRulesBySourceId = this.amlTransactionRuleRepository.findAll().stream() + .filter(txn -> txn.getSourceClientRiskRuleId() != null) + .collect(Collectors.toMap(AmlTransactionRule::getSourceClientRiskRuleId, Function.identity(), (a, b) -> a)); + return rules.stream().map(rule -> mapToData(rule, txnRulesBySourceId.get(rule.getId()))).collect(Collectors.toList()); } - private KycRiskRuleData mapToData(final ClientRiskRule rule) { + private KycRiskRuleData mapToData(final ClientRiskRule rule, final AmlTransactionRule txnRule) { final Long occCatId = rule.getOccupationCategory() != null ? rule.getOccupationCategory().getId() : null; final String occCatName = rule.getOccupationCategory() != null ? rule.getOccupationCategory().getLabel() : null; final Long occId = rule.getOccupation() != null ? rule.getOccupation().getId() : null; @@ -67,7 +75,12 @@ public class KycRiskRulesReadPlatformServiceImpl implements KycRiskRulesReadPlat rule.getBannedEntityMatch(), rule.getWillfulDefaulter(), rule.getEcgcSalFlag(), - rule.getDormantFlag() + rule.getDormantFlag(), + txnRule != null ? txnRule.getThresholdAmount() : null, + txnRule != null ? txnRule.getRuleType().name() : null, + txnRule != null ? txnRule.getWindowDays() : null, + txnRule != null ? txnRule.getThresholdAmountMediumRisk() : null, + txnRule != null ? txnRule.getThresholdAmountHighRisk() : null ); } } diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index 137e10214..1d5428c9d 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -490,4 +490,7 @@ + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0438_add_tiered_thresholds_and_low_balance_rule.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0438_add_tiered_thresholds_and_low_balance_rule.xml new file mode 100644 index 000000000..7bc7454ea --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0438_add_tiered_thresholds_and_low_balance_rule.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SELECT COUNT(*) FROM m_aml_transaction_rule WHERE rule_code = 'RTGS_HIGH_VALUE' AND threshold_amount_medium_risk IS NOT NULL + + + UPDATE m_aml_transaction_rule + SET threshold_amount_medium_risk = 500000, + threshold_amount_high_risk = 2000000 + WHERE rule_code = 'RTGS_HIGH_VALUE'; + + + + + + + SELECT COUNT(*) FROM m_aml_transaction_rule WHERE rule_code = 'LOW_BALANCE_HIGH_ACTIVITY' + + + INSERT INTO m_aml_transaction_rule + (rule_code, description, applies_to, threshold_amount, rule_type, window_days, source_client_risk_rule_id, is_active) + VALUES + ('LOW_BALANCE_HIGH_ACTIVITY', 'Maintaining a low or overdrawn balance with high activity', 'SAVINGS', 1000000, 'THRESHOLD', null, + (SELECT id FROM m_client_risk_rule WHERE order_position = 1 AND aml_check_status IS NOT NULL), false); + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0439_fill_client_risk_rule_income_turnover_nulls.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0439_fill_client_risk_rule_income_turnover_nulls.xml new file mode 100644 index 000000000..f51108229 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0439_fill_client_risk_rule_income_turnover_nulls.xml @@ -0,0 +1,61 @@ + + + + + + * + + -- 1. High Risk Occupations (Real Estate, Importer/Exporter, Commission Agent, Financial Advisor) + UPDATE m_client_risk_rule + SET min_monthly_income = 200000.0, + min_annual_income = 2400000.0 + WHERE order_position IN (20, 30, 40, 50); + + -- 2. Medium Risk Occupations + UPDATE m_client_risk_rule + SET min_monthly_income = 50000.0, + max_monthly_income = 200000.0, + min_annual_income = 600000.0, + max_annual_income = 2400000.0 + WHERE order_position BETWEEN 90 AND 240; + + -- 3. Low Risk Occupations + UPDATE m_client_risk_rule + SET min_monthly_income = 0.0, + max_monthly_income = 50000.0, + min_annual_income = 0.0, + max_annual_income = 600000.0 + WHERE order_position BETWEEN 280 AND 450; + + -- 4. Set all remaining NULL values to 0.00 + UPDATE m_client_risk_rule SET min_monthly_income = 0.0 WHERE min_monthly_income IS NULL; + UPDATE m_client_risk_rule SET max_monthly_income = 0.0 WHERE max_monthly_income IS NULL; + UPDATE m_client_risk_rule SET min_annual_income = 0.0 WHERE min_annual_income IS NULL; + UPDATE m_client_risk_rule SET max_annual_income = 0.0 WHERE max_annual_income IS NULL; + UPDATE m_client_risk_rule SET min_annual_turnover = 0.0 WHERE min_annual_turnover IS NULL; + UPDATE m_client_risk_rule SET max_annual_turnover = 0.0 WHERE max_annual_turnover IS NULL; + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0440_seed_aml_test_alerts.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0440_seed_aml_test_alerts.xml new file mode 100644 index 000000000..00d74d25f --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0440_seed_aml_test_alerts.xml @@ -0,0 +1,108 @@ + + + + + + + SELECT COUNT(*) FROM m_client WHERE external_id = 'CLIENT_RAJESH' + + + INSERT INTO m_client ( + account_no, external_id, status_enum, office_id, display_name, + fullname, is_staff, activation_date, office_joining_date, + submittedon_date, legal_form_enum, + created_on_utc, created_by, + last_modified_on_utc, last_modified_by, + cif_number + ) + VALUES + ( + 'ACC0000002', 'CLIENT_RAJESH', 300, 1, 'Rajesh Kumar', + 'Rajesh Kumar', false, CURRENT_DATE, CURRENT_DATE, + CURRENT_DATE, 1, + NOW(), 1, + NOW(), 1, + 'CIF_RAJESH' + ), + ( + 'ACC0000003', 'CLIENT_SONIA', 300, 1, 'Sonia Sharma', + 'Sonia Sharma', false, CURRENT_DATE, CURRENT_DATE, + CURRENT_DATE, 1, + NOW(), 1, + NOW(), 1, + 'CIF_SONIA' + ), + ( + 'ACC0000004', 'CLIENT_VIKRAM', 300, 1, 'Vikram Malhotra', + 'Vikram Malhotra', false, CURRENT_DATE, CURRENT_DATE, + CURRENT_DATE, 1, + NOW(), 1, + NOW(), 1, + 'CIF_VIKRAM' + ); + + + + + + + SELECT COUNT(*) FROM m_aml_alert a + JOIN m_client c ON a.client_id = c.id + WHERE c.external_id IN ('CLIENT_RAJESH', 'CLIENT_SONIA', 'CLIENT_VIKRAM') + + + + INSERT INTO m_aml_alert ( + transaction_type, transaction_id, client_id, rule_id, amount, currency_code, + raised_on, status, created_by, created_on_utc, last_modified_by, last_modified_on_utc, version + ) + VALUES + ( + 'SAVINGS', 1001, + (SELECT id FROM m_client WHERE external_id = 'CLIENT_RAJESH'), + (SELECT id FROM m_aml_transaction_rule WHERE rule_code = 'LARGE_CASH_TXN_25L'), + 3500000.000000, 'INR', + NOW(), 'RAISED', + 1, NOW(), 1, NOW(), 0 + ), + ( + 'SAVINGS', 1002, + (SELECT id FROM m_client WHERE external_id = 'CLIENT_SONIA'), + (SELECT id FROM m_aml_transaction_rule WHERE rule_code = 'STRUCTURING_BELOW_10L'), + 995000.000000, 'INR', + NOW(), 'RAISED', + 1, NOW(), 1, NOW(), 0 + ), + ( + 'SAVINGS', 1003, + (SELECT id FROM m_client WHERE external_id = 'CLIENT_VIKRAM'), + (SELECT id FROM m_aml_transaction_rule WHERE rule_code = 'PEP_ACCOUNT_MONITORING'), + 75000.000000, 'INR', + NOW(), 'RAISED', + 1, NOW(), 1, NOW(), 0 + ); + + + +