fix(savings): stop crediting accounts automatically on activation
Activation for Savings, RD, FD and Current accounts no longer books an opening deposit or enforces a minimum opening balance -- accounts go active at zero balance and a clerk deposits funds afterward as a separate transaction, matching how these branches actually operate. Fixed Deposit and Recurring Deposit have their own override of activateWithBalance() that a prior fix to the shared SavingsAccount base class missed, so they kept auto-depositing after Savings was already fixed. Removing that auto-deposit exposed two follow-on bugs in the FD/RD activation flow, now also fixed: - the opening deposit's journal entries were posted twice (once by the deposit helper, once by the activation command's own posting call) - FD/RD maturity amount was computed from the account's real (now empty) transaction history at activation and never recalculated once a deposit actually landed, so it showed no projected interest until the account was reloaded from a lucky code path
This commit is contained in:
+4
-1
@@ -435,7 +435,10 @@ public class FixedDepositAccount extends SavingsAccount {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Money activateWithBalance() {
|
public Money activateWithBalance() {
|
||||||
return Money.of(this.currency, this.accountTermAndPreClosure.depositAmount());
|
// Activation never deposits money -- funding happens strictly through a separate deposit
|
||||||
|
// transaction (posted by a clerk) after the account is already active, regardless of the
|
||||||
|
// deposit amount configured on the account's term/pre-closure details.
|
||||||
|
return Money.zero(this.currency);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void close(final AppUser currentUser, final JsonCommand command, final Map<String, Object> actualChanges) {
|
public void close(final AppUser currentUser, final JsonCommand command, final Map<String, Object> actualChanges) {
|
||||||
|
|||||||
+4
-1
@@ -552,7 +552,10 @@ public class RecurringDepositAccount extends SavingsAccount {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Money activateWithBalance() {
|
public Money activateWithBalance() {
|
||||||
return Money.of(this.currency, this.minRequiredOpeningBalance);
|
// Activation never deposits money -- funding happens strictly through a separate deposit
|
||||||
|
// transaction (posted by a clerk) after the account is already active, regardless of any
|
||||||
|
// minimum required opening balance the product has configured.
|
||||||
|
return Money.zero(this.currency);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void processAccountUponActivation(final DateTimeFormatter fmt, final boolean postReversals,
|
protected void processAccountUponActivation(final DateTimeFormatter fmt, final boolean postReversals,
|
||||||
|
|||||||
+4
-13
@@ -19,7 +19,6 @@
|
|||||||
package org.apache.fineract.portfolio.savings.domain;
|
package org.apache.fineract.portfolio.savings.domain;
|
||||||
|
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.accountFacilityConfigParamName;
|
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.accountNoParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.accountNoParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.accountStatusParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.accountStatusParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.guardianClientIdParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.guardianClientIdParamName;
|
||||||
@@ -455,18 +454,10 @@ public class SavingsAccountAssembler {
|
|||||||
account.setLinkedAccountType(this.fromApiJsonHelper.extractStringNamed(linkedAccountTypeParamName, element));
|
account.setLinkedAccountType(this.fromApiJsonHelper.extractStringNamed(linkedAccountTypeParamName, element));
|
||||||
account.setLinkedAccountNo(this.fromApiJsonHelper.extractStringNamed(linkedAccountNoParamName, element));
|
account.setLinkedAccountNo(this.fromApiJsonHelper.extractStringNamed(linkedAccountNoParamName, element));
|
||||||
|
|
||||||
// Account Facility Config
|
// Every digital facility starts disabled at account opening, regardless of anything the
|
||||||
if (element.getAsJsonObject().has(accountFacilityConfigParamName)) {
|
// request sends or the product defaults to -- they're switched on deliberately, one at a
|
||||||
final JsonElement configEl = element.getAsJsonObject().get(accountFacilityConfigParamName);
|
// time, later, through the account's own Edit Nominee & Features screen.
|
||||||
if (configEl != null && !configEl.isJsonNull() && configEl.isJsonObject()) {
|
account.setFacilityConfig(new SavingsAccountFacilityConfig(account));
|
||||||
SavingsAccountFacilityConfig facilityConfig = account.getFacilityConfig();
|
|
||||||
if (facilityConfig == null) {
|
|
||||||
facilityConfig = new SavingsAccountFacilityConfig(account);
|
|
||||||
account.setFacilityConfig(facilityConfig);
|
|
||||||
}
|
|
||||||
facilityConfig.updateFrom(configEl.getAsJsonObject());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Joint holders
|
// Joint holders
|
||||||
if (element.getAsJsonObject().has(jointHoldersParamName)) {
|
if (element.getAsJsonObject().has(jointHoldersParamName)) {
|
||||||
|
|||||||
+3
-11
@@ -133,18 +133,10 @@ public class CurrentAccountTransactionWritePlatformServiceImpl implements Curren
|
|||||||
"Insufficient available balance. Available: " + available + ", Requested: " + amount, accountId);
|
"Insufficient available balance. Available: " + available + ", Requested: " + amount, accountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Minimum required balance validation
|
// Minimum required balance is not enforced -- a withdrawal must never be blocked for
|
||||||
|
// taking the account below its configured minimum, only for exceeding the available
|
||||||
|
// balance (checked above).
|
||||||
final BigDecimal newBalance = currentBalance.subtract(amount);
|
final BigDecimal newBalance = currentBalance.subtract(amount);
|
||||||
if (account.isEnforceMinRequiredBalance() && account.getMinRequiredBalance() != null
|
|
||||||
&& account.getMinRequiredBalance().compareTo(BigDecimal.ZERO) > 0) {
|
|
||||||
if (newBalance.compareTo(account.getMinRequiredBalance()) < 0) {
|
|
||||||
throw new GeneralPlatformDomainRuleException(
|
|
||||||
"error.msg.current.account.min.required.balance.breached",
|
|
||||||
"Withdrawal of " + amount + " would breach the minimum required balance of "
|
|
||||||
+ account.getMinRequiredBalance() + ". Balance after withdrawal would be " + newBalance,
|
|
||||||
accountId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final String refNo = command.stringValueOfParameterNamed("accountNumber");
|
final String refNo = command.stringValueOfParameterNamed("accountNumber");
|
||||||
final CurrentAccountTransaction txn = CurrentAccountTransaction.withdrawal(account, office, amount, txnDate, refNo);
|
final CurrentAccountTransaction txn = CurrentAccountTransaction.withdrawal(account, office, amount, txnDate, refNo);
|
||||||
|
|||||||
+11
-151
@@ -227,12 +227,10 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri
|
|||||||
clientNomineeWritePlatformService.saveNomineeIfPresent(clientId, accountNo, "CB", json.get("nominee"));
|
clientNomineeWritePlatformService.saveNomineeIfPresent(clientId, accountNo, "CB", json.get("nominee"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Facility config — UI sends "accountFacilityConfig", internal uses "facilityConfig"
|
// Every digital facility starts disabled at account opening, regardless of anything the
|
||||||
JsonObject fcJson = json.getAsJsonObject("accountFacilityConfig");
|
// request sends or the product defaults to -- they're switched on deliberately, one at a
|
||||||
if (fcJson == null) fcJson = json.getAsJsonObject("facilityConfig");
|
// time, later, through the account's own Edit Nominee & Features screen.
|
||||||
if (fcJson != null) {
|
account.setFacilityConfig(CurrentAccountFacilityConfig.createFor(account));
|
||||||
account.setFacilityConfig(assembleFacilityConfig(account, fcJson));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Joint holders
|
// Joint holders
|
||||||
if (json.has("jointHolders") && json.get("jointHolders").isJsonArray()) {
|
if (json.has("jointHolders") && json.get("jointHolders").isJsonArray()) {
|
||||||
@@ -294,61 +292,16 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri
|
|||||||
final JsonObject json = command.parsedJson().getAsJsonObject();
|
final JsonObject json = command.parsedJson().getAsJsonObject();
|
||||||
LocalDate activatedOnDate = parseDate(getString(json, "activatedOnDate"), getString(json, "dateFormat"), getString(json, "locale"));
|
LocalDate activatedOnDate = parseDate(getString(json, "activatedOnDate"), getString(json, "dateFormat"), getString(json, "locale"));
|
||||||
if (activatedOnDate == null) activatedOnDate = LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
|
if (activatedOnDate == null) activatedOnDate = LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
|
||||||
// Validate minimum opening balance before activation
|
|
||||||
java.math.BigDecimal openingBalanceValue = java.math.BigDecimal.ZERO;
|
|
||||||
if (json.has("openingBalance") && !json.get("openingBalance").isJsonNull()) {
|
|
||||||
try { openingBalanceValue = json.get("openingBalance").getAsBigDecimal(); } catch (Exception ignored) {
|
|
||||||
// intentionally ignored
|
|
||||||
}
|
|
||||||
}
|
|
||||||
final java.math.BigDecimal minRequired = account.getMinRequiredOpeningBalance();
|
|
||||||
if (minRequired != null && minRequired.compareTo(java.math.BigDecimal.ZERO) > 0
|
|
||||||
&& openingBalanceValue.compareTo(minRequired) < 0) {
|
|
||||||
throw new org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException(
|
|
||||||
java.util.List.of(org.apache.fineract.infrastructure.core.data.ApiParameterError.parameterError(
|
|
||||||
"openingBalance",
|
|
||||||
"Opening balance (" + openingBalanceValue + ") is less than minimum required opening balance (" + minRequired + ")",
|
|
||||||
"validation.msg.currentaccount.openingBalance.less.than.min.required",
|
|
||||||
openingBalanceValue, minRequired)));
|
|
||||||
}
|
|
||||||
|
|
||||||
account.activate(activatedOnDate, user);
|
account.activate(activatedOnDate, user);
|
||||||
applyFacilityChargesOnActivation(account);
|
|
||||||
|
|
||||||
java.math.BigDecimal currentBalance = java.math.BigDecimal.ZERO;
|
// Activation posts no transaction of its own -- no opening-balance deposit, no
|
||||||
final java.util.List<org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransaction> depositTxns = new java.util.ArrayList<>();
|
// digital-facility charges applied or collected, regardless of what the account's facility
|
||||||
final java.util.List<ChargeGlEntry> chargeEntries = new java.util.ArrayList<>();
|
// config or product defaults say. Funding and any facility charges happen strictly
|
||||||
|
// afterwards, through separate transactions a clerk processes once the account is already
|
||||||
// Opening balance deposit transaction
|
// active.
|
||||||
if (openingBalanceValue.compareTo(java.math.BigDecimal.ZERO) > 0) {
|
|
||||||
try {
|
|
||||||
final org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransaction deposit =
|
|
||||||
org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransaction
|
|
||||||
.deposit(account, account.getOffice(), openingBalanceValue, activatedOnDate, "OPENING-BALANCE");
|
|
||||||
deposit.setRunningBalanceDerived(openingBalanceValue);
|
|
||||||
account.setTotalDepositsDerived(openingBalanceValue);
|
|
||||||
currentAccountTransactionRepository.saveAndFlush(deposit);
|
|
||||||
currentBalance = openingBalanceValue;
|
|
||||||
depositTxns.add(deposit);
|
|
||||||
log.info("Opening balance deposit {} created for account id={}", openingBalanceValue, accountId);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("Could not process openingBalance for account id={}: {}", accountId, e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-deduct activation charges (chargeTimeEnum=3) from opening balance
|
|
||||||
final java.util.List<org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountCharge> activationCharges =
|
|
||||||
account.getCharges().stream()
|
|
||||||
.filter(c -> c.getChargeTimeEnum() != null && c.getChargeTimeEnum() == 3 && !c.isPaid())
|
|
||||||
.toList();
|
|
||||||
currentBalance = collectChargesAndPost(account, activationCharges, currentBalance, activatedOnDate, chargeEntries);
|
|
||||||
|
|
||||||
account.setAccountBalanceDerived(currentBalance);
|
|
||||||
currentAccountRepository.saveAndFlush(account);
|
currentAccountRepository.saveAndFlush(account);
|
||||||
|
|
||||||
// Post GL journal entries — deposits in batch, each charge individually
|
|
||||||
postGlEntries(account, depositTxns, chargeEntries);
|
|
||||||
|
|
||||||
return new CommandProcessingResultBuilder().withEntityId(accountId).build();
|
return new CommandProcessingResultBuilder().withEntityId(accountId).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -637,78 +590,6 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyFacilityChargesOnActivation(final CurrentAccount account) {
|
|
||||||
// Use account-level facility config — reflects what was selected at account opening time
|
|
||||||
org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountFacilityConfig config =
|
|
||||||
account.getFacilityConfig();
|
|
||||||
|
|
||||||
log.info("[FacilityCharge] Activating account id={}, facilityConfig={}",
|
|
||||||
account.getId(), config != null ? "FOUND" : "NULL");
|
|
||||||
|
|
||||||
if (config == null) {
|
|
||||||
// Fallback: use product-level facility config
|
|
||||||
log.info("[FacilityCharge] No account-level config, falling back to product id={}", account.getProductId());
|
|
||||||
config = currentProductRepository.findById(account.getProductId())
|
|
||||||
.map(p -> {
|
|
||||||
final var pc = p.getFacilityConfig();
|
|
||||||
if (pc == null) {
|
|
||||||
log.info("[FacilityCharge] Product has no facility config either");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// Map product config to account config fields inline
|
|
||||||
org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountFacilityConfig ac =
|
|
||||||
new org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountFacilityConfig();
|
|
||||||
ac.setAtmFacility(Boolean.TRUE.equals(pc.getAtmFacility()));
|
|
||||||
ac.setAtmWithdrawalFacility(Boolean.TRUE.equals(pc.getAtmWithdrawalFacility()));
|
|
||||||
ac.setUpiFacility(Boolean.TRUE.equals(pc.getUpiFacility()));
|
|
||||||
ac.setPosTransactionsFacility(Boolean.TRUE.equals(pc.getPosTransactionsFacility()));
|
|
||||||
ac.setChequeBook(Boolean.TRUE.equals(pc.getChequeBook()));
|
|
||||||
ac.setImpsFacility(Boolean.TRUE.equals(pc.getImpsFacility()));
|
|
||||||
ac.setSmsAlerts(Boolean.TRUE.equals(pc.getSmsAlerts()));
|
|
||||||
ac.setBbpsFacility(Boolean.TRUE.equals(pc.getBbpsFacility()));
|
|
||||||
ac.setInsuranceCover(Boolean.TRUE.equals(pc.getInsuranceCover()));
|
|
||||||
ac.setBepgFacility(Boolean.TRUE.equals(pc.getBepgFacility()));
|
|
||||||
ac.setNeftFacility(Boolean.TRUE.equals(pc.getNeftFacility()));
|
|
||||||
ac.setRtgsFacility(Boolean.TRUE.equals(pc.getRtgsFacility()));
|
|
||||||
ac.setInterBankTotalCapFacility(Boolean.TRUE.equals(pc.getInterBankTotalCapFacility()));
|
|
||||||
ac.setWithdrawalFacility(Boolean.TRUE.equals(pc.getWithdrawalFacility()));
|
|
||||||
ac.setDepositFacilityKyc(Boolean.TRUE.equals(pc.getDepositFacilityKyc()));
|
|
||||||
ac.setVirtualDebitCard(Boolean.TRUE.equals(pc.getVirtualDebitCard()));
|
|
||||||
return ac;
|
|
||||||
}).orElse(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config == null) {
|
|
||||||
log.warn("[FacilityCharge] No facility config found for account id={} — no charges applied", account.getId());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountFacilityConfig fc = config;
|
|
||||||
log.info("[FacilityCharge] Config found — ATM={} UPI={} NEFT={} RTGS={} CHEQUE={} SMS={} IMPs={} POS={} BBPS={} INS={} BEPG={} IBCAP={} WD={} KYC={} VCARD={} ATM_WD={}",
|
|
||||||
fc.isAtmFacility(), fc.isUpiFacility(), fc.isNeftFacility(), fc.isRtgsFacility(),
|
|
||||||
fc.isChequeBook(), fc.isSmsAlerts(), fc.isImpsFacility(), fc.isPosTransactionsFacility(),
|
|
||||||
fc.isBbpsFacility(), fc.isInsuranceCover(), fc.isBepgFacility(), fc.isInterBankTotalCapFacility(),
|
|
||||||
fc.isWithdrawalFacility(), fc.isDepositFacilityKyc(), fc.isVirtualDebitCard(), fc.isAtmWithdrawalFacility());
|
|
||||||
applyMapped(account, fc.isAtmFacility(), "ATM_FACILITY");
|
|
||||||
applyMapped(account, fc.isAtmWithdrawalFacility(), "ATM_WITHDRAWAL");
|
|
||||||
applyMapped(account, fc.isUpiFacility(), "UPI_FACILITY");
|
|
||||||
applyMapped(account, fc.isPosTransactionsFacility(), "POS_TRANSACTIONS");
|
|
||||||
// Cheque book: no longer charged at activation — the per-leaf issuance charge is now deducted
|
|
||||||
// when a cheque book is actually issued (ChequeBookIssuanceChargeService), not just for having
|
|
||||||
// the facility enabled.
|
|
||||||
applyMapped(account, fc.isImpsFacility(), "IMPS_FACILITY");
|
|
||||||
applyMapped(account, fc.isSmsAlerts(), "SMS_ALERTS");
|
|
||||||
applyMapped(account, fc.isBbpsFacility(), "BBPS_FACILITY");
|
|
||||||
applyMapped(account, fc.isInsuranceCover(), "INSURANCE_COVER");
|
|
||||||
applyMapped(account, fc.isBepgFacility(), "BEPG_FACILITY");
|
|
||||||
applyMapped(account, fc.isNeftFacility(), "NEFT_FACILITY");
|
|
||||||
applyMapped(account, fc.isRtgsFacility(), "RTGS_FACILITY");
|
|
||||||
applyMapped(account, fc.isInterBankTotalCapFacility(),"INTER_BANK_TOTAL_CAP");
|
|
||||||
applyMapped(account, fc.isWithdrawalFacility(), "WITHDRAWAL_FACILITY");
|
|
||||||
applyMapped(account, fc.isDepositFacilityKyc(), "DEPOSIT_FACILITY_KYC");
|
|
||||||
applyMapped(account, fc.isVirtualDebitCard(), "VIRTUAL_DEBIT_CARD");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrapper to carry a charge transaction together with its charge entity for GL posting
|
// Wrapper to carry a charge transaction together with its charge entity for GL posting
|
||||||
private record ChargeGlEntry(
|
private record ChargeGlEntry(
|
||||||
org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransaction txn,
|
org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransaction txn,
|
||||||
@@ -717,9 +598,8 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri
|
|||||||
/**
|
/**
|
||||||
* Marks each of the given (unpaid) charges as paid, deducts its amount from {@code runningBalance}, creates a
|
* Marks each of the given (unpaid) charges as paid, deducts its amount from {@code runningBalance}, creates a
|
||||||
* {@code CurrentAccountTransaction.chargeDeduction(...)} row for it, and records a {@link ChargeGlEntry} for GL
|
* {@code CurrentAccountTransaction.chargeDeduction(...)} row for it, and records a {@link ChargeGlEntry} for GL
|
||||||
* posting. Shared by {@link #activate(Long, JsonCommand)} (activation-time charges) and
|
* posting. Used by {@link #applyAndCollectFacilityCharges(Long, java.util.Set)} (edit-approval-time charges) --
|
||||||
* {@link #applyAndCollectFacilityCharges(Long, java.util.Set)} (edit-approval-time charges) so the
|
* activation itself no longer collects any charges. Returns the resulting balance.
|
||||||
* charge-collection logic isn't duplicated. Returns the resulting balance.
|
|
||||||
*/
|
*/
|
||||||
private java.math.BigDecimal collectChargesAndPost(final CurrentAccount account,
|
private java.math.BigDecimal collectChargesAndPost(final CurrentAccount account,
|
||||||
final java.util.List<org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountCharge> charges,
|
final java.util.List<org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountCharge> charges,
|
||||||
@@ -875,24 +755,4 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyMapped(final CurrentAccount account, final boolean enabled, final String facilityType) {
|
|
||||||
if (!enabled) {
|
|
||||||
log.debug("[FacilityCharge] {} disabled — skip", facilityType);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final var mappingOpt = facilityChargeMappingRepository.findByFacilityType(facilityType);
|
|
||||||
if (mappingOpt.isEmpty()) {
|
|
||||||
log.warn("[FacilityCharge] {} enabled but NO mapping found in m_facility_charge_mapping", facilityType);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final org.apache.fineract.portfolio.charge.domain.Charge charge = mappingOpt.get().getCharge();
|
|
||||||
if (charge == null) {
|
|
||||||
log.warn("[FacilityCharge] {} mapping exists but charge_id is NULL", facilityType);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
log.info("[FacilityCharge] Applying charge '{}' (id={}) for facility {}", charge.getName(), charge.getId(), facilityType);
|
|
||||||
account.getCharges().add(
|
|
||||||
org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountCharge
|
|
||||||
.createFromFacility(account, charge));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-6
@@ -172,8 +172,14 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
|||||||
|
|
||||||
if (portfolioAccountData == null) {
|
if (portfolioAccountData == null) {
|
||||||
final PaymentDetail paymentDetail = null;
|
final PaymentDetail paymentDetail = null;
|
||||||
this.depositAccountDomainService.handleFDDeposit(account, fmt, account.getActivationDate(),
|
final SavingsAccountTransaction depositTransaction = this.depositAccountDomainService.handleFDDeposit(account, fmt,
|
||||||
amountForDeposit.getAmount(), paymentDetail);
|
account.getActivationDate(), amountForDeposit.getAmount(), paymentDetail);
|
||||||
|
// handleFDDeposit() already posts this transaction's own journal entries internally --
|
||||||
|
// mark it as "existing" so the postJournalEntries(...) call below (which covers other
|
||||||
|
// activation-time changes, e.g. an activation charge payment) does not post it again.
|
||||||
|
if (depositTransaction != null && depositTransaction.getId() != null) {
|
||||||
|
existingTransactionIds.add(depositTransaction.getId());
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
final SavingsAccount fromSavingsAccount = null;
|
final SavingsAccount fromSavingsAccount = null;
|
||||||
boolean isRegularTransaction = false;
|
boolean isRegularTransaction = false;
|
||||||
@@ -202,8 +208,17 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
|||||||
}
|
}
|
||||||
|
|
||||||
final boolean isPreMatureClosure = false;
|
final boolean isPreMatureClosure = false;
|
||||||
account.updateMaturityDateAndAmount(mc, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd,
|
if (amountForDeposit.isGreaterThanZero()) {
|
||||||
financialYearBeginningMonth);
|
account.updateMaturityDateAndAmount(mc, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd,
|
||||||
|
financialYearBeginningMonth);
|
||||||
|
} else {
|
||||||
|
// No deposit has actually landed on the account yet -- project maturity using the
|
||||||
|
// configured deposit amount (the same approach used for the pre-activation preview and
|
||||||
|
// for reinvested FDs) instead of the account's real, still-empty transaction history,
|
||||||
|
// which would otherwise price in zero interest.
|
||||||
|
account.updateMaturityDateAndAmountBeforeAccountActivation(mc, isPreMatureClosure,
|
||||||
|
isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth);
|
||||||
|
}
|
||||||
List<DepositAccountOnHoldTransaction> depositAccountOnHoldTransactions = null;
|
List<DepositAccountOnHoldTransaction> depositAccountOnHoldTransactions = null;
|
||||||
if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) {
|
if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) {
|
||||||
depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository
|
depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository
|
||||||
@@ -275,8 +290,14 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
|||||||
final PortfolioAccountData portfolioAccountData = this.accountAssociationsReadPlatformService
|
final PortfolioAccountData portfolioAccountData = this.accountAssociationsReadPlatformService
|
||||||
.retriveSavingsLinkedAssociation(savingsId);
|
.retriveSavingsLinkedAssociation(savingsId);
|
||||||
if (portfolioAccountData == null) {
|
if (portfolioAccountData == null) {
|
||||||
this.depositAccountDomainService.handleRDDeposit(account, fmt, account.getActivationDate(),
|
final SavingsAccountTransaction depositTransaction = this.depositAccountDomainService.handleRDDeposit(account, fmt,
|
||||||
amountForDeposit.getAmount(), null, isRegularTransaction);
|
account.getActivationDate(), amountForDeposit.getAmount(), null, isRegularTransaction);
|
||||||
|
// handleRDDeposit() already posts this transaction's own journal entries internally --
|
||||||
|
// mark it as "existing" so the postJournalEntries(...) call below (which covers other
|
||||||
|
// activation-time changes) does not post it again.
|
||||||
|
if (depositTransaction != null && depositTransaction.getId() != null) {
|
||||||
|
existingTransactionIds.add(depositTransaction.getId());
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
final boolean isExceptionForBalanceCheck = false;
|
final boolean isExceptionForBalanceCheck = false;
|
||||||
final SavingsAccount fromSavingsAccount = null;
|
final SavingsAccount fromSavingsAccount = null;
|
||||||
@@ -368,6 +389,19 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
|||||||
final SavingsAccountTransaction deposit = this.depositAccountDomainService.handleFDDeposit(account, fmt, transactionDate,
|
final SavingsAccountTransaction deposit = this.depositAccountDomainService.handleFDDeposit(account, fmt, transactionDate,
|
||||||
transactionAmount, paymentDetail);
|
transactionAmount, paymentDetail);
|
||||||
|
|
||||||
|
// The account may have been activated with no funds yet (opening deposit is now a separate,
|
||||||
|
// later step) -- in that case its maturity amount was only ever projected off the configured
|
||||||
|
// deposit amount, with zero real transactions. Now that money has actually landed, recompute
|
||||||
|
// maturity date/amount off the account's real transaction history so it reflects reality.
|
||||||
|
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
|
||||||
|
.isSavingsInterestPostingAtCurrentPeriodEnd();
|
||||||
|
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
|
||||||
|
final MathContext mc = MathContext.DECIMAL64;
|
||||||
|
final boolean isPreMatureClosure = false;
|
||||||
|
account.updateMaturityDateAndAmount(mc, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd,
|
||||||
|
financialYearBeginningMonth);
|
||||||
|
this.savingAccountRepositoryWrapper.saveAndFlush(account);
|
||||||
|
|
||||||
this.denominationBreakdownWriter.save(command, deposit.getId(), account.getId(), account.getAccountNumber(),
|
this.denominationBreakdownWriter.save(command, deposit.getId(), account.getId(), account.getAccountNumber(),
|
||||||
account.clientId(), account.getClient() != null ? account.getClient().getCifNumber() : null, transactionDate,
|
account.clientId(), account.getClient() != null ? account.getClient().getCifNumber() : null, transactionDate,
|
||||||
"DEPOSIT");
|
"DEPOSIT");
|
||||||
|
|||||||
+13
-27
@@ -97,7 +97,6 @@ import org.apache.fineract.portfolio.note.domain.Note;
|
|||||||
import org.apache.fineract.portfolio.note.domain.NoteRepository;
|
import org.apache.fineract.portfolio.note.domain.NoteRepository;
|
||||||
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
|
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
|
||||||
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
|
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
|
||||||
import org.apache.fineract.portfolio.savings.service.FacilityChargeAutoApplyService;
|
|
||||||
import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType;
|
import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType;
|
||||||
import org.apache.fineract.portfolio.savings.SavingsApiConstants;
|
import org.apache.fineract.portfolio.savings.SavingsApiConstants;
|
||||||
import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues;
|
import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues;
|
||||||
@@ -167,7 +166,6 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
|
|||||||
private final GSIMRepositoy gsimRepository;
|
private final GSIMRepositoy gsimRepository;
|
||||||
private final SavingsAccountInterestPostingService savingsAccountInterestPostingService;
|
private final SavingsAccountInterestPostingService savingsAccountInterestPostingService;
|
||||||
private final ErrorHandler errorHandler;
|
private final ErrorHandler errorHandler;
|
||||||
private final FacilityChargeAutoApplyService facilityChargeAutoApplyService;
|
|
||||||
private final DenominationBreakdownWriter denominationBreakdownWriter;
|
private final DenominationBreakdownWriter denominationBreakdownWriter;
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -203,23 +201,9 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
|
|||||||
|
|
||||||
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false);
|
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false);
|
||||||
|
|
||||||
// Validate minimum opening balance
|
// Minimum required opening balance is not enforced -- activation must never be blocked
|
||||||
BigDecimal openingBalanceForValidation = BigDecimal.ZERO;
|
// for not meeting it; the account can be funded afterwards by whatever deposit the
|
||||||
if (command.parameterExists("openingBalance")) {
|
// customer brings (cash or a transfer), on their own timeline.
|
||||||
final BigDecimal ob = command.bigDecimalValueOfParameterNamed("openingBalance");
|
|
||||||
if (ob != null) openingBalanceForValidation = ob;
|
|
||||||
}
|
|
||||||
final BigDecimal minRequiredOpening = account.getMinRequiredOpeningBalance();
|
|
||||||
if (minRequiredOpening != null && minRequiredOpening.compareTo(BigDecimal.ZERO) > 0
|
|
||||||
&& openingBalanceForValidation.compareTo(minRequiredOpening) < 0) {
|
|
||||||
final List<ApiParameterError> errors = new ArrayList<>();
|
|
||||||
errors.add(ApiParameterError.parameterError(
|
|
||||||
"openingBalance",
|
|
||||||
"Opening balance (" + openingBalanceForValidation + ") is less than minimum required opening balance (" + minRequiredOpening + ")",
|
|
||||||
"validation.msg.savingsaccount.openingBalance.less.than.min.required",
|
|
||||||
openingBalanceForValidation, minRequiredOpening));
|
|
||||||
throw new PlatformApiDataValidationException(errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
checkClientOrGroupActive(account);
|
checkClientOrGroupActive(account);
|
||||||
final Set<Long> existingTransactionIds = new HashSet<>();
|
final Set<Long> existingTransactionIds = new HashSet<>();
|
||||||
@@ -236,14 +220,10 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
|
|||||||
final Locale locale = command.extractLocale();
|
final Locale locale = command.extractLocale();
|
||||||
final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale);
|
final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale);
|
||||||
processPostActiveActions(account, fmt, existingTransactionIds, existingReversedTransactionIds);
|
processPostActiveActions(account, fmt, existingTransactionIds, existingReversedTransactionIds);
|
||||||
// Auto-apply facility charges based on product facility config
|
// Activation posts no transaction of its own -- digital-facility charges are no longer
|
||||||
facilityChargeAutoApplyService.applyFacilityChargesOnActivation(account);
|
// auto-applied and auto-collected here. Whatever facilities are turned on later get
|
||||||
this.savingAccountRepositoryWrapper.saveAndFlush(account);
|
// charged (if at all) through the normal charge/collection flow at that time, not as a
|
||||||
// Collect any newly added facility activation charges (after flush so amountOutstanding is set)
|
// side effect of activation.
|
||||||
// payCharge() internally adds transactions to account.transactions via handleChargeTransactions
|
|
||||||
// Do NOT add to existingTransactionIds — postJournalEntries must include them for GL posting
|
|
||||||
facilityChargeAutoApplyService.collectActivationCharges(account);
|
|
||||||
this.savingAccountRepositoryWrapper.saveAndFlush(account);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false);
|
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false);
|
||||||
@@ -1521,6 +1501,12 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
|
|||||||
this.journalEntryWritePlatformService.createJournalEntriesForSavings(accountingBridgeData);
|
this.journalEntryWritePlatformService.createJournalEntriesForSavings(accountingBridgeData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postJournalEntriesForSavingsAccount(final SavingsAccount account, final Set<Long> existingTransactionIds,
|
||||||
|
final Set<Long> existingReversedTransactionIds, final boolean backdatedTxnsAllowedTill) {
|
||||||
|
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, backdatedTxnsAllowedTill);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommandProcessingResult inactivateCharge(final Long savingsAccountId, final Long savingsAccountChargeId) {
|
public CommandProcessingResult inactivateCharge(final Long savingsAccountId, final Long savingsAccountChargeId) {
|
||||||
|
|
||||||
|
|||||||
+2
-3
@@ -463,8 +463,7 @@ public class SavingsConfiguration {
|
|||||||
EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService, AppUserRepositoryWrapper appuserRepository,
|
EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService, AppUserRepositoryWrapper appuserRepository,
|
||||||
StandingInstructionRepository standingInstructionRepository, BusinessEventNotifierService businessEventNotifierService,
|
StandingInstructionRepository standingInstructionRepository, BusinessEventNotifierService businessEventNotifierService,
|
||||||
GSIMRepositoy gsimRepository, SavingsAccountInterestPostingService savingsAccountInterestPostingService,
|
GSIMRepositoy gsimRepository, SavingsAccountInterestPostingService savingsAccountInterestPostingService,
|
||||||
ErrorHandler errorHandler, FacilityChargeAutoApplyService facilityChargeAutoApplyService,
|
ErrorHandler errorHandler, DenominationBreakdownWriter denominationBreakdownWriter) {
|
||||||
DenominationBreakdownWriter denominationBreakdownWriter) {
|
|
||||||
return new SavingsAccountWritePlatformServiceJpaRepositoryImpl(context, fromApiJsonDeserializer, savingAccountRepositoryWrapper,
|
return new SavingsAccountWritePlatformServiceJpaRepositoryImpl(context, fromApiJsonDeserializer, savingAccountRepositoryWrapper,
|
||||||
staffRepository, savingsAccountTransactionRepository, savingAccountAssembler, savingsAccountTransactionDataValidator,
|
staffRepository, savingsAccountTransactionRepository, savingAccountAssembler, savingsAccountTransactionDataValidator,
|
||||||
savingsAccountChargeDataValidator, paymentDetailWritePlatformService, journalEntryWritePlatformService,
|
savingsAccountChargeDataValidator, paymentDetailWritePlatformService, journalEntryWritePlatformService,
|
||||||
@@ -472,7 +471,7 @@ public class SavingsConfiguration {
|
|||||||
chargeRepository, savingsAccountChargeRepository, holidayRepository, workingDaysRepository, configurationDomainService,
|
chargeRepository, savingsAccountChargeRepository, holidayRepository, workingDaysRepository, configurationDomainService,
|
||||||
depositAccountOnHoldTransactionRepository, entityDatatableChecksWritePlatformService, appuserRepository,
|
depositAccountOnHoldTransactionRepository, entityDatatableChecksWritePlatformService, appuserRepository,
|
||||||
standingInstructionRepository, businessEventNotifierService, gsimRepository, savingsAccountInterestPostingService,
|
standingInstructionRepository, businessEventNotifierService, gsimRepository, savingsAccountInterestPostingService,
|
||||||
errorHandler, facilityChargeAutoApplyService, denominationBreakdownWriter);
|
errorHandler, denominationBreakdownWriter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
+7
-4
@@ -3008,7 +3008,10 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Money activateWithBalance() {
|
public Money activateWithBalance() {
|
||||||
return Money.of(this.currency, this.minRequiredOpeningBalance);
|
// Activation never deposits money -- funding happens strictly through a separate deposit
|
||||||
|
// transaction (posted by a clerk) after the account is already active, regardless of any
|
||||||
|
// minimum required opening balance the product has configured.
|
||||||
|
return Money.zero(this.currency);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void approveAndActivateApplication(final LocalDate appliedonDate, final AppUser appliedBy) {
|
public void approveAndActivateApplication(final LocalDate appliedonDate, final AppUser appliedBy) {
|
||||||
@@ -3700,10 +3703,10 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Money minRequiredBalanceDerived(final MonetaryCurrency currency) {
|
private Money minRequiredBalanceDerived(final MonetaryCurrency currency) {
|
||||||
|
// Minimum required balance is not enforced -- withdrawals (and every other balance check
|
||||||
|
// that goes through this) must never be blocked by a configured minimum balance. Overdraft
|
||||||
|
// headroom is a separate concept and still applies.
|
||||||
Money minReqBalance = Money.zero(currency);
|
Money minReqBalance = Money.zero(currency);
|
||||||
if (this.enforceMinRequiredBalance) {
|
|
||||||
minReqBalance = minReqBalance.plus(this.minRequiredBalance);
|
|
||||||
}
|
|
||||||
if (this.allowOverdraft) {
|
if (this.allowOverdraft) {
|
||||||
minReqBalance = minReqBalance.minus(this.overdraftLimit);
|
minReqBalance = minReqBalance.minus(this.overdraftLimit);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user