feat: separate kyc/aml risk rules, dormancy clock, and savings daily interest fixes
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 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-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 - 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 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 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 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 6 of 10) (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 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 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (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
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 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-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 - 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 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 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 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 6 of 10) (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 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 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (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
This commit is contained in:
+115
-8
@@ -61,6 +61,7 @@ import java.math.MathContext;
|
||||
import java.time.LocalDate;
|
||||
import java.time.YearMonth;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
@@ -1434,10 +1435,17 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
|
||||
addTransaction(transaction);
|
||||
}
|
||||
|
||||
if (this.sub_status.equals(SavingsAccountSubStatusEnum.INACTIVE.getValue())
|
||||
|| this.sub_status.equals(SavingsAccountSubStatusEnum.DORMANT.getValue())) {
|
||||
this.sub_status = SavingsAccountSubStatusEnum.NONE.getValue();
|
||||
}
|
||||
// Nothing on the dormancy ladder is cleared by a transaction — not dormancy, and not
|
||||
// inactivity either.
|
||||
//
|
||||
// RBI's Master Direction makes periodic re-verification the thing that keeps an account
|
||||
// operative, and a lapsed one operable again only once the customer's identity has been
|
||||
// established afresh. A cash-in at the counter is not that verification, so it cannot
|
||||
// stand in for it: the sub-status is cleared by a completed re-KYC and by nothing else
|
||||
// (see reviveOnReKyc), and DormancyClock measures the ladder from that same re-KYC.
|
||||
//
|
||||
// Both used to be cleared here, which meant a ₹100 deposit quietly did the work the
|
||||
// branch was supposed to do at the counter.
|
||||
|
||||
if (backdatedTxnsAllowedTill) {
|
||||
this.summary.updateSummaryWithPivotConfig(this.currency, this.savingsAccountTransactionSummaryWrapper, transaction,
|
||||
@@ -1512,6 +1520,37 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
|
||||
throw new PlatformApiDataValidationException(dataValidationErrors);
|
||||
}
|
||||
|
||||
// A dormant account does not pay out until someone has looked at it.
|
||||
//
|
||||
// Dormancy means nobody has operated the account for the product's dormancy period — two
|
||||
// years here — and an account nobody has touched for two years is the one an impersonator
|
||||
// reaches for, because the real customer is not watching it. RBI's Master Direction on KYC
|
||||
// requires the operations of such an account to be allowed only after the customer's
|
||||
// identity is established afresh, so the debit is stopped and the account is revived only
|
||||
// by a completed re-KYC.
|
||||
//
|
||||
// Credits are deliberately not stopped: a deposit puts money in, cannot be a fraud against
|
||||
// the customer, and is the customer coming back.
|
||||
//
|
||||
// The elapsed days are counted here rather than trusting the sub-status alone. The nightly
|
||||
// job is what stamps DORMANT, so between an account passing the threshold and that job next
|
||||
// running the flag still says the account is fine — and a withdrawal in that window used to
|
||||
// go straight through. It is a real gap: the job can be behind after an outage, or the
|
||||
// business date can move by more than a day.
|
||||
if (isDormantByElapsedDays()) {
|
||||
final String defaultUserMessage = "Withdrawal is not allowed. The account is dormant — no customer-initiated"
|
||||
+ " transaction for " + dormancyPeriodDescription() + ". It must be reactivated after the customer's"
|
||||
+ " identity is re-verified.";
|
||||
final ApiParameterError error = ApiParameterError.parameterError(
|
||||
"error.msg.savingsaccount.transaction.withdrawals.blocked.account.dormant", defaultUserMessage, "transactionDate",
|
||||
transactionDTO.getTransactionDate().format(transactionDTO.getFormatter()));
|
||||
|
||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||
dataValidationErrors.add(error);
|
||||
|
||||
throw new PlatformApiDataValidationException(dataValidationErrors);
|
||||
}
|
||||
|
||||
if (DateUtils.isDateInTheFuture(transactionDTO.getTransactionDate())) {
|
||||
final String defaultUserMessage = "Transaction date cannot be in the future.";
|
||||
final ApiParameterError error = ApiParameterError.parameterError("error.msg.savingsaccount.transaction.in.the.future",
|
||||
@@ -1570,10 +1609,7 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
|
||||
addTransaction(transaction);
|
||||
}
|
||||
|
||||
if (this.sub_status.equals(SavingsAccountSubStatusEnum.INACTIVE.getValue())
|
||||
|| this.sub_status.equals(SavingsAccountSubStatusEnum.DORMANT.getValue())) {
|
||||
this.sub_status = SavingsAccountSubStatusEnum.NONE.getValue();
|
||||
}
|
||||
// A withdrawal clears nothing on the dormancy ladder either — see the deposit path for why.
|
||||
if (backdatedTxnsAllowedTill) {
|
||||
this.summary.updateSummaryWithPivotConfig(this.currency, this.savingsAccountTransactionSummaryWrapper, transaction,
|
||||
this.savingsAccountTransactions);
|
||||
@@ -1581,6 +1617,52 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
|
||||
return transaction;
|
||||
}
|
||||
|
||||
/**
|
||||
* How long the product says an account must sit untouched before it is dormant, for the message
|
||||
* the customer is shown. Read from the product so the figure quoted is the one actually applied.
|
||||
*/
|
||||
private String dormancyPeriodDescription() {
|
||||
final Long days = this.product == null ? null : this.product.getDaysToDormancy();
|
||||
if (days == null || days <= 0) {
|
||||
return "the product's dormancy period";
|
||||
}
|
||||
final long years = days / 365;
|
||||
return years >= 1 ? years + (years == 1 ? " year" : " years") : days + " days";
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this account is dormant, by the sub-status the nightly job stamps or by the days that
|
||||
* have actually elapsed since the customer last operated it.
|
||||
*
|
||||
* <p>
|
||||
* The elapsed count is what closes the window between an account crossing the dormancy
|
||||
* threshold and the nightly job next running: until that job runs the sub-status still says the
|
||||
* account is fine, and a withdrawal in that window went straight through. The job can be a day
|
||||
* behind after an outage, and a business date moved forward by more than a day skips the runs
|
||||
* in between entirely.
|
||||
*
|
||||
* <p>
|
||||
* The count mirrors the job's own query exactly, so the two can never disagree about whether an
|
||||
* account is dormant: only deposits and withdrawals count as the customer operating the
|
||||
* account — interest and charges are the bank's own movements, not the customer's — reversed
|
||||
* transactions are ignored, and an account with no such transaction is measured from its
|
||||
* activation. Dormancy tracking being switched off on the product turns this off with it.
|
||||
*/
|
||||
/**
|
||||
* Whether this account is dormant, from the sub-status the nightly job stamped.
|
||||
*
|
||||
* <p>
|
||||
* The elapsed days are not recounted here, though the read services and the clerk's withdrawal
|
||||
* check both do. The clock now runs from the customer's last completed re-KYC — see
|
||||
* DormancyClock — and this module cannot see a client's KYC record, so counting here would use
|
||||
* transactions and give a different answer from every other place that asks. A guard that
|
||||
* disagrees with the screen the officer just read is worse than one that lags the job by a
|
||||
* night, and the clerk's own check catches that gap before a request is ever raised.
|
||||
*/
|
||||
private boolean isDormantByElapsedDays() {
|
||||
return SavingsAccountSubStatusEnum.DORMANT.hasStateOf(SavingsAccountSubStatusEnum.fromInt(this.sub_status));
|
||||
}
|
||||
|
||||
public BigDecimal calculateWithdrawalFee(final BigDecimal transactionAmount) {
|
||||
BigDecimal result = BigDecimal.ZERO;
|
||||
if (isWithdrawalFeeApplicableForTransfer()) {
|
||||
@@ -4278,6 +4360,31 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Revives a dormant account, on the strength of a completed re-KYC.
|
||||
*
|
||||
* <p>
|
||||
* RBI's Master Direction allows a dormant account to be operated again only once the customer's
|
||||
* identity has been established afresh. A completed re-KYC is that verification, so it is the
|
||||
* one thing that lifts dormancy — a transaction does not, which is why {@link #withdraw} refuses
|
||||
* on a dormant account and a deposit no longer revives one.
|
||||
*
|
||||
* <p>
|
||||
* Called from the re-KYC completion itself rather than from the nightly expiry sweep: the sweep
|
||||
* runs over every customer whose KYC is merely current, and reviving there would reopen a
|
||||
* dormant account on the next nightly run without anyone having verified anything.
|
||||
*
|
||||
* @return true if the sub-status was changed
|
||||
*/
|
||||
public boolean reviveOnReKyc() {
|
||||
final SavingsAccountSubStatusEnum current = SavingsAccountSubStatusEnum.fromInt(this.sub_status);
|
||||
if (SavingsAccountSubStatusEnum.DORMANT.hasStateOf(current) || SavingsAccountSubStatusEnum.INACTIVE.hasStateOf(current)) {
|
||||
this.sub_status = SavingsAccountSubStatusEnum.NONE.getValue();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void validateForAccountBlock() {
|
||||
final SavingsAccountSubStatusEnum currentSubStatus = SavingsAccountSubStatusEnum.fromInt(this.getSubStatus());
|
||||
if (SavingsAccountSubStatusEnum.BLOCK.hasStateOf(currentSubStatus)) {
|
||||
|
||||
+48
@@ -293,4 +293,52 @@
|
||||
</sql>
|
||||
</changeSet>
|
||||
|
||||
<!--
|
||||
Clear the income GL these charges were seeded with, and let each product's own mapping decide.
|
||||
|
||||
Every insert above picks its income account with
|
||||
|
||||
SELECT id FROM acc_gl_account
|
||||
WHERE classification_enum = 4 AND account_usage = 1 AND disabled = false
|
||||
ORDER BY id LIMIT 1
|
||||
|
||||
which is "whichever income account happens to have the lowest id". That is not a choice, it is an
|
||||
accident of the order the chart of accounts was created in, and it lands somewhere different on
|
||||
every database: on one tenant it resolved to 400101 INCOME FROM LOAN, so savings facility charges
|
||||
were booked as loan income; on a tenant whose chart was seeded in another order it resolved to
|
||||
400103 DD Issuance Charges Income, so an ATM charge was booked against demand drafts. Neither is
|
||||
the account a bank would name for an ATM fee, and no screen shows the reader which one was picked.
|
||||
|
||||
A charge's own account overrides the product's Income from Fees mapping — see
|
||||
AccountingProcessorHelper.getLinkedGLAccountForSavingsCharges, which reads the charge's account
|
||||
first and only falls back to the product mapping when it is null. So while these values stand, the
|
||||
account an administrator chose on the product's Accounting tab is silently ignored. Setting them
|
||||
null restores that mapping as the answer, which is where a bank configures this and the only place
|
||||
it can be seen and changed.
|
||||
|
||||
Only rows still carrying the seeded value are touched: the account is compared against what that
|
||||
same query resolves to now, so a charge an administrator has since pointed at a real account keeps
|
||||
it. 0500_reset_facility_charge_mappings_to_unmapped.xml already decided these seeds are not an
|
||||
administrator's choice and deactivated the mappings for the same reason.
|
||||
-->
|
||||
<changeSet id="2011-19" author="cbs-nitesh">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<tableExists tableName="m_charge"/>
|
||||
</preConditions>
|
||||
<sql>
|
||||
UPDATE m_charge
|
||||
SET income_or_liability_account_id = NULL
|
||||
WHERE income_or_liability_account_id = (
|
||||
SELECT id FROM acc_gl_account
|
||||
WHERE classification_enum = 4 AND account_usage = 1 AND disabled = false
|
||||
ORDER BY id LIMIT 1)
|
||||
AND name IN ('ATM Facility Charge', 'ATM Withdrawal Charge', 'UPI Facility Charge',
|
||||
'POS Transaction Charge', 'Cheque Book Charge', 'IMPS Facility Charge',
|
||||
'SMS Alert Monthly Charge', 'BBPS Facility Charge', 'Insurance Cover Charge',
|
||||
'BEPG Facility Charge', 'NEFT Facility Charge', 'RTGS Facility Charge',
|
||||
'Inter-Bank Cap Charge', 'Withdrawal Facility Charge', 'Deposit KYC Charge',
|
||||
'Virtual Debit Card Charge');
|
||||
</sql>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
|
||||
Reference in New Issue
Block a user