diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java index 688b6feca..642fb8f08 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java @@ -435,7 +435,10 @@ public class FixedDepositAccount extends SavingsAccount { @Override 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 actualChanges) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RecurringDepositAccount.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RecurringDepositAccount.java index 45f24fd81..c4c07b19e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RecurringDepositAccount.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RecurringDepositAccount.java @@ -552,7 +552,10 @@ public class RecurringDepositAccount extends SavingsAccount { @Override 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, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java index 95d28d2d9..4125e1df2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java @@ -19,7 +19,6 @@ 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.accountFacilityConfigParamName; 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.guardianClientIdParamName; @@ -455,18 +454,10 @@ public class SavingsAccountAssembler { account.setLinkedAccountType(this.fromApiJsonHelper.extractStringNamed(linkedAccountTypeParamName, element)); account.setLinkedAccountNo(this.fromApiJsonHelper.extractStringNamed(linkedAccountNoParamName, element)); - // Account Facility Config - if (element.getAsJsonObject().has(accountFacilityConfigParamName)) { - final JsonElement configEl = element.getAsJsonObject().get(accountFacilityConfigParamName); - if (configEl != null && !configEl.isJsonNull() && configEl.isJsonObject()) { - SavingsAccountFacilityConfig facilityConfig = account.getFacilityConfig(); - if (facilityConfig == null) { - facilityConfig = new SavingsAccountFacilityConfig(account); - account.setFacilityConfig(facilityConfig); - } - facilityConfig.updateFrom(configEl.getAsJsonObject()); - } - } + // Every digital facility starts disabled at account opening, regardless of anything the + // request sends or the product defaults to -- they're switched on deliberately, one at a + // time, later, through the account's own Edit Nominee & Features screen. + account.setFacilityConfig(new SavingsAccountFacilityConfig(account)); // Joint holders if (element.getAsJsonObject().has(jointHoldersParamName)) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountTransactionWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountTransactionWritePlatformServiceImpl.java index ef3d5c60b..c359556d1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountTransactionWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountTransactionWritePlatformServiceImpl.java @@ -133,18 +133,10 @@ public class CurrentAccountTransactionWritePlatformServiceImpl implements Curren "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); - 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 CurrentAccountTransaction txn = CurrentAccountTransaction.withdrawal(account, office, amount, txnDate, refNo); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountWritePlatformServiceImpl.java index e4ec06386..a5a221774 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountWritePlatformServiceImpl.java @@ -227,12 +227,10 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri clientNomineeWritePlatformService.saveNomineeIfPresent(clientId, accountNo, "CB", json.get("nominee")); } - // Facility config — UI sends "accountFacilityConfig", internal uses "facilityConfig" - JsonObject fcJson = json.getAsJsonObject("accountFacilityConfig"); - if (fcJson == null) fcJson = json.getAsJsonObject("facilityConfig"); - if (fcJson != null) { - account.setFacilityConfig(assembleFacilityConfig(account, fcJson)); - } + // Every digital facility starts disabled at account opening, regardless of anything the + // request sends or the product defaults to -- they're switched on deliberately, one at a + // time, later, through the account's own Edit Nominee & Features screen. + account.setFacilityConfig(CurrentAccountFacilityConfig.createFor(account)); // Joint holders if (json.has("jointHolders") && json.get("jointHolders").isJsonArray()) { @@ -294,61 +292,16 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri final JsonObject json = command.parsedJson().getAsJsonObject(); LocalDate activatedOnDate = parseDate(getString(json, "activatedOnDate"), getString(json, "dateFormat"), getString(json, "locale")); 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); - applyFacilityChargesOnActivation(account); - java.math.BigDecimal currentBalance = java.math.BigDecimal.ZERO; - final java.util.List depositTxns = new java.util.ArrayList<>(); - final java.util.List chargeEntries = new java.util.ArrayList<>(); - - // Opening balance deposit transaction - 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 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); + // Activation posts no transaction of its own -- no opening-balance deposit, no + // digital-facility charges applied or collected, regardless of what the account's facility + // config or product defaults say. Funding and any facility charges happen strictly + // afterwards, through separate transactions a clerk processes once the account is already + // active. currentAccountRepository.saveAndFlush(account); - // Post GL journal entries — deposits in batch, each charge individually - postGlEntries(account, depositTxns, chargeEntries); - 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 private record ChargeGlEntry( 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 * {@code CurrentAccountTransaction.chargeDeduction(...)} row for it, and records a {@link ChargeGlEntry} for GL - * posting. Shared by {@link #activate(Long, JsonCommand)} (activation-time charges) and - * {@link #applyAndCollectFacilityCharges(Long, java.util.Set)} (edit-approval-time charges) so the - * charge-collection logic isn't duplicated. Returns the resulting balance. + * posting. Used by {@link #applyAndCollectFacilityCharges(Long, java.util.Set)} (edit-approval-time charges) -- + * activation itself no longer collects any charges. Returns the resulting balance. */ private java.math.BigDecimal collectChargesAndPost(final CurrentAccount account, final java.util.List 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)); - } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java index dd1cd5a21..ae121f8dd 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java @@ -172,8 +172,14 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo if (portfolioAccountData == null) { final PaymentDetail paymentDetail = null; - this.depositAccountDomainService.handleFDDeposit(account, fmt, account.getActivationDate(), - amountForDeposit.getAmount(), paymentDetail); + final SavingsAccountTransaction depositTransaction = this.depositAccountDomainService.handleFDDeposit(account, fmt, + 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 { final SavingsAccount fromSavingsAccount = null; boolean isRegularTransaction = false; @@ -202,8 +208,17 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo } final boolean isPreMatureClosure = false; - account.updateMaturityDateAndAmount(mc, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth); + if (amountForDeposit.isGreaterThanZero()) { + 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 depositAccountOnHoldTransactions = null; if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository @@ -275,8 +290,14 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo final PortfolioAccountData portfolioAccountData = this.accountAssociationsReadPlatformService .retriveSavingsLinkedAssociation(savingsId); if (portfolioAccountData == null) { - this.depositAccountDomainService.handleRDDeposit(account, fmt, account.getActivationDate(), - amountForDeposit.getAmount(), null, isRegularTransaction); + final SavingsAccountTransaction depositTransaction = this.depositAccountDomainService.handleRDDeposit(account, fmt, + 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 { final boolean isExceptionForBalanceCheck = false; final SavingsAccount fromSavingsAccount = null; @@ -368,6 +389,19 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo final SavingsAccountTransaction deposit = this.depositAccountDomainService.handleFDDeposit(account, fmt, transactionDate, 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(), account.clientId(), account.getClient() != null ? account.getClient().getCifNumber() : null, transactionDate, "DEPOSIT"); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java index 33a14e8c0..88dd3727f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java @@ -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.paymentdetail.domain.PaymentDetail; 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.SavingsApiConstants; import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues; @@ -167,7 +166,6 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi private final GSIMRepositoy gsimRepository; private final SavingsAccountInterestPostingService savingsAccountInterestPostingService; private final ErrorHandler errorHandler; - private final FacilityChargeAutoApplyService facilityChargeAutoApplyService; private final DenominationBreakdownWriter denominationBreakdownWriter; @Transactional @@ -203,23 +201,9 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); - // Validate minimum opening balance - BigDecimal openingBalanceForValidation = BigDecimal.ZERO; - if (command.parameterExists("openingBalance")) { - 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 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); - } + // Minimum required opening balance is not enforced -- activation must never be blocked + // for not meeting it; the account can be funded afterwards by whatever deposit the + // customer brings (cash or a transfer), on their own timeline. checkClientOrGroupActive(account); final Set existingTransactionIds = new HashSet<>(); @@ -236,14 +220,10 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi final Locale locale = command.extractLocale(); final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); processPostActiveActions(account, fmt, existingTransactionIds, existingReversedTransactionIds); - // Auto-apply facility charges based on product facility config - facilityChargeAutoApplyService.applyFacilityChargesOnActivation(account); - this.savingAccountRepositoryWrapper.saveAndFlush(account); - // Collect any newly added facility activation charges (after flush so amountOutstanding is set) - // 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); + // Activation posts no transaction of its own -- digital-facility charges are no longer + // auto-applied and auto-collected here. Whatever facilities are turned on later get + // charged (if at all) through the normal charge/collection flow at that time, not as a + // side effect of activation. } postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false); @@ -1521,6 +1501,12 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi this.journalEntryWritePlatformService.createJournalEntriesForSavings(accountingBridgeData); } + @Override + public void postJournalEntriesForSavingsAccount(final SavingsAccount account, final Set existingTransactionIds, + final Set existingReversedTransactionIds, final boolean backdatedTxnsAllowedTill) { + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, backdatedTxnsAllowedTill); + } + @Override public CommandProcessingResult inactivateCharge(final Long savingsAccountId, final Long savingsAccountChargeId) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java index 45d888cdf..722d25070 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java @@ -463,8 +463,7 @@ public class SavingsConfiguration { EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService, AppUserRepositoryWrapper appuserRepository, StandingInstructionRepository standingInstructionRepository, BusinessEventNotifierService businessEventNotifierService, GSIMRepositoy gsimRepository, SavingsAccountInterestPostingService savingsAccountInterestPostingService, - ErrorHandler errorHandler, FacilityChargeAutoApplyService facilityChargeAutoApplyService, - DenominationBreakdownWriter denominationBreakdownWriter) { + ErrorHandler errorHandler, DenominationBreakdownWriter denominationBreakdownWriter) { return new SavingsAccountWritePlatformServiceJpaRepositoryImpl(context, fromApiJsonDeserializer, savingAccountRepositoryWrapper, staffRepository, savingsAccountTransactionRepository, savingAccountAssembler, savingsAccountTransactionDataValidator, savingsAccountChargeDataValidator, paymentDetailWritePlatformService, journalEntryWritePlatformService, @@ -472,7 +471,7 @@ public class SavingsConfiguration { chargeRepository, savingsAccountChargeRepository, holidayRepository, workingDaysRepository, configurationDomainService, depositAccountOnHoldTransactionRepository, entityDatatableChecksWritePlatformService, appuserRepository, standingInstructionRepository, businessEventNotifierService, gsimRepository, savingsAccountInterestPostingService, - errorHandler, facilityChargeAutoApplyService, denominationBreakdownWriter); + errorHandler, denominationBreakdownWriter); } @Bean diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java index 791b19d4a..85b0bc8bb 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java @@ -3008,7 +3008,10 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom } 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) { @@ -3700,10 +3703,10 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom } 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); - if (this.enforceMinRequiredBalance) { - minReqBalance = minReqBalance.plus(this.minRequiredBalance); - } if (this.allowOverdraft) { minReqBalance = minReqBalance.minus(this.overdraftLimit); }