diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index eff05eba297..95d7ddb89ce 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -705,14 +705,9 @@ public void addLoanCharge(final LoanCharge loanCharge) { this.summary = updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement()); // store Id's of existing loan transactions and existing reversed loan transactions - if (!loanCharge.isDueAtDisbursement() && !loanCharge.isPaid() && getStatus().isOverpaid()) { - reprocessTransactions(); // overpaid transactions will be reprocessed and pay this charge - doPostLoanTransactionChecks(loanCharge.getEffectiveDueDate(), loanLifecycleStateMachine); - } else { - final SingleLoanChargeRepaymentScheduleProcessingWrapper wrapper = new SingleLoanChargeRepaymentScheduleProcessingWrapper(); - wrapper.reprocess(getCurrency(), getDisbursementDate(), getRepaymentScheduleInstallments(), loanCharge); - updateLoanSummaryDerivedFields(); - } + final SingleLoanChargeRepaymentScheduleProcessingWrapper wrapper = new SingleLoanChargeRepaymentScheduleProcessingWrapper(); + wrapper.reprocess(getCurrency(), getDisbursementDate(), getRepaymentScheduleInstallments(), loanCharge); + updateLoanSummaryDerivedFields(); loanLifecycleStateMachine.transition(LoanEvent.LOAN_CHARGE_ADDED, this); } @@ -731,6 +726,13 @@ public ChangedTransactionDetail reprocessTransactions() { return changedTransactionDetail; } + public ChangedTransactionDetail reprocessTransactionsWithPostTransactionChecks(LocalDate transactionDate) { + ChangedTransactionDetail changedDetail = reprocessTransactions(); + doPostLoanTransactionChecks(transactionDate, loanLifecycleStateMachine); + return changedDetail; + } + + /** * Creates a loanTransaction for "Apply Charge Event" with transaction date set to "suppliedTransactionDate". The * newly created transaction is also added to the Loan on which this method is called. @@ -5484,12 +5486,12 @@ public List getLoanTransactions(Predicate pred return getLoanTransactions().stream().filter(predicate).toList(); } + public LoanTransaction getLoanTransaction(Predicate predicate) { + return getLoanTransactions().stream().filter(predicate).findFirst().orElse(null); + } + public LoanTransaction findChargedOffTransaction() { - return getLoanTransactions().stream() // - .filter(LoanTransaction::isNotReversed) // - .filter(LoanTransaction::isChargeOff) // - .findFirst() // - .orElse(null); + return getLoanTransaction(e -> e.isNotReversed() && e.isChargeOff()); } public void handleMaturityDateActivate() { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java index c1b66285c03..b12ea96bf99 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java @@ -922,12 +922,12 @@ public void manuallyAdjustedOrReversed() { public void updateLoanTransactionToRepaymentScheduleMappings(final Collection mappings) { Collection retainMappings = new ArrayList<>(); for (LoanTransactionToRepaymentScheduleMapping updatedrepaymentScheduleMapping : mappings) { - updateMapingDetail(retainMappings, updatedrepaymentScheduleMapping); + updateMappingDetail(retainMappings, updatedrepaymentScheduleMapping); } this.loanTransactionToRepaymentScheduleMappings.retainAll(retainMappings); } - private boolean updateMapingDetail(final Collection retainMappings, + private boolean updateMappingDetail(final Collection retainMappings, final LoanTransactionToRepaymentScheduleMapping updatedrepaymentScheduleMapping) { boolean isMappingUpdated = false; for (LoanTransactionToRepaymentScheduleMapping repaymentScheduleMapping : this.loanTransactionToRepaymentScheduleMappings) { @@ -952,6 +952,33 @@ private boolean updateMapingDetail(final Collection updatedMappings) { + for (LoanTransactionToRepaymentScheduleMapping updatedMapping : updatedMappings) { + addMappingDetail(updatedMapping); + } + } + + private boolean addMappingDetail(final LoanTransactionToRepaymentScheduleMapping updatedMapping) { + boolean isMappingUpdated = false; + for (LoanTransactionToRepaymentScheduleMapping existingMapping : this.loanTransactionToRepaymentScheduleMappings) { + LoanRepaymentScheduleInstallment existingInstallment = existingMapping.getLoanRepaymentScheduleInstallment(); + LoanRepaymentScheduleInstallment updatedInstallment = updatedMapping.getLoanRepaymentScheduleInstallment(); + if (existingInstallment.getDueDate().equals(updatedInstallment.getDueDate()) + && updatedInstallment.getId() != null && updatedInstallment.getId().equals(existingInstallment.getId())) { + existingMapping.updateComponents(updatedMapping.getPrincipalPortion(), + updatedMapping.getInterestPortion(), updatedMapping.getFeeChargesPortion(), + updatedMapping.getPenaltyChargesPortion()); + isMappingUpdated = true; + break; + } + } + if (!isMappingUpdated) { + updatedMapping.setLoanTransaction(this); + this.loanTransactionToRepaymentScheduleMappings.add(updatedMapping); + } + return isMappingUpdated; + } + public Set getLoanTransactionToRepaymentScheduleMappings() { return this.loanTransactionToRepaymentScheduleMappings; } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionToRepaymentScheduleMapping.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionToRepaymentScheduleMapping.java index 3249b4d1fdb..7aa586c86cc 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionToRepaymentScheduleMapping.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionToRepaymentScheduleMapping.java @@ -25,7 +25,10 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import java.math.BigDecimal; + +import jakarta.validation.constraints.NotNull; import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; +import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; @@ -100,10 +103,13 @@ public LoanRepaymentScheduleInstallment getLoanRepaymentScheduleInstallment() { return this.installment; } - public void updateComponents(final Money principal, final Money interest, final Money feeCharges, final Money penaltyCharges) { - final MonetaryCurrency currency = principal.getCurrency(); - this.principalPortion = defaultToNullIfZero(getPrincipalPortion(currency).plus(principal)); - this.interestPortion = defaultToNullIfZero(getInterestPortion(currency).plus(interest)); + public void updateComponents(@NotNull Money principal, @NotNull Money interest, @NotNull Money feeCharges, @NotNull Money penaltyCharges) { + updateComponents(principal.getAmount(), interest.getAmount(), feeCharges.getAmount(), penaltyCharges.getAmount()); + } + + void updateComponents(final BigDecimal principal, final BigDecimal interest, final BigDecimal feeCharges, final BigDecimal penaltyCharges) { + this.principalPortion = MathUtil.zeroToNull(MathUtil.add(getPrincipalPortion(), principal)); + this.interestPortion = MathUtil.zeroToNull(MathUtil.add(getInterestPortion(), interest)); updateChargesComponents(feeCharges, penaltyCharges); updateAmount(); } @@ -122,10 +128,9 @@ public void setComponents(final BigDecimal principal, final BigDecimal interest, updateAmount(); } - private void updateChargesComponents(final Money feeCharges, final Money penaltyCharges) { - final MonetaryCurrency currency = feeCharges.getCurrency(); - this.feeChargesPortion = defaultToNullIfZero(getFeeChargesPortion(currency).plus(feeCharges)); - this.penaltyChargesPortion = defaultToNullIfZero(getPenaltyChargesPortion(currency).plus(penaltyCharges)); + private void updateChargesComponents(final BigDecimal feeCharges, final BigDecimal penaltyCharges) { + this.feeChargesPortion = MathUtil.zeroToNull(MathUtil.add(getFeeChargesPortion(), feeCharges)); + this.penaltyChargesPortion = MathUtil.zeroToNull(MathUtil.add(getPenaltyChargesPortion(), penaltyCharges)); } public Money getPrincipalPortion(final MonetaryCurrency currency) { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java index 73d909b05bc..c259a7f7456 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java @@ -189,7 +189,8 @@ public ChangedTransactionDetail reprocessLoanTransactions(final LocalDate disbur loanTransaction.updateLoanTransactionToRepaymentScheduleMappings( newLoanTransaction.getLoanTransactionToRepaymentScheduleMappings()); } else { - createNewTransaction(loanTransaction, newLoanTransaction, changedTransactionDetail); + createNewTransaction(loanTransaction, newLoanTransaction); + changedTransactionDetail.getNewTransactionMappings().put(loanTransaction.getId(), newLoanTransaction); } } @@ -248,7 +249,8 @@ private void recalculateAccrualActivityTransaction(ChangedTransactionDetail chan calculateAccrualActivity(newLoanTransaction, currency, installments); if (!LoanTransaction.transactionAmountsMatch(currency, loanTransaction, newLoanTransaction)) { - createNewTransaction(loanTransaction, newLoanTransaction, changedTransactionDetail); + createNewTransaction(loanTransaction, newLoanTransaction); + changedTransactionDetail.getNewTransactionMappings().put(loanTransaction.getId(), newLoanTransaction); } } @@ -403,7 +405,8 @@ private void recalculateChargeOffTransaction(ChangedTransactionDetail changedTra newLoanTransaction.updateComponentsAndTotal(principalPortion, interestPortion, feeChargesPortion, penaltychargesPortion); if (!LoanTransaction.transactionAmountsMatch(currency, loanTransaction, newLoanTransaction)) { - createNewTransaction(loanTransaction, newLoanTransaction, changedTransactionDetail); + createNewTransaction(loanTransaction, newLoanTransaction); + changedTransactionDetail.getNewTransactionMappings().put(loanTransaction.getId(), newLoanTransaction); } } @@ -493,7 +496,8 @@ private void recalculateCreditTransaction(ChangedTransactionDetail changedTransa processCreditTransaction(newLoanTransaction, overpaymentHolder, currency, installments); if (!LoanTransaction.transactionAmountsMatch(currency, loanTransaction, newLoanTransaction)) { - createNewTransaction(loanTransaction, newLoanTransaction, changedTransactionDetail); + createNewTransaction(loanTransaction, newLoanTransaction); + changedTransactionDetail.getNewTransactionMappings().put(loanTransaction.getId(), newLoanTransaction); } } @@ -504,15 +508,13 @@ private List getMergedTransactionList(List tra return mergedList; } - protected void createNewTransaction(LoanTransaction loanTransaction, LoanTransaction newLoanTransaction, - ChangedTransactionDetail changedTransactionDetail) { + protected void createNewTransaction(LoanTransaction loanTransaction, LoanTransaction newLoanTransaction) { loanTransaction.reverse(); loanTransaction.updateExternalId(null); newLoanTransaction.copyLoanTransactionRelations(loanTransaction.getLoanTransactionRelations()); // Adding Replayed relation from newly created transaction to reversed transaction newLoanTransaction.getLoanTransactionRelations().add( LoanTransactionRelation.linkToTransaction(newLoanTransaction, loanTransaction, LoanTransactionRelationTypeEnum.REPLAYED)); - changedTransactionDetail.getNewTransactionMappings().put(loanTransaction.getId(), newLoanTransaction); } protected void processCreditTransaction(LoanTransaction loanTransaction, MoneyHolder overpaymentHolder, MonetaryCurrency currency, diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index e44d542e792..79f2c24404a 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -189,16 +189,21 @@ public Pair repr } else { LoanCharge loanCharge = chargeOrTransaction.getLoanCharge().get(); processSingleCharge(loanCharge, currency, installments, disbursementDate); - if (!loanCharge.isFullyPaid()) { + if (!loanCharge.isFullyPaid() && !overpaidTransactions.isEmpty()) { overpaidTransactions = processOverpaidTransactions(overpaidTransactions, currency, installments, charges, changedTransactionDetail, overpaymentHolder, scheduleModel); } } } + Map newTransactionMappings = changedTransactionDetail.getNewTransactionMappings(); + for (Long oldTransactionId : newTransactionMappings.keySet()) { + LoanTransaction oldTransaction = loanTransactions.stream().filter(e -> oldTransactionId.equals(e.getId())).findFirst().get(); + LoanTransaction newTransaction = newTransactionMappings.get(oldTransactionId); + createNewTransaction(oldTransaction, newTransaction); + } List txs = chargeOrTransactions.stream() // - .map(ChargeOrTransaction::getLoanTransaction) // - .filter(Optional::isPresent) // - .map(Optional::get).toList(); + .filter(ChargeOrTransaction::isTransaction) // + .map(e -> e.getLoanTransaction().get()).toList(); reprocessInstallments(disbursementDate, txs, installments, currency); return Pair.of(changedTransactionDetail, scheduleModel); } @@ -306,8 +311,9 @@ protected LoanTransaction findOriginalTransaction(LoanTransaction loanTransactio return originalTransaction.get(); } else { // when there is no id, then it might be that the original transaction is changed, so we need to look // it up from the Ctx. - Long originalChargebackTransactionId = ctx.getChangedTransactionDetail().getCurrentTransactionToOldId().get(loanTransaction); - Collection updatedTransactions = ctx.getChangedTransactionDetail().getNewTransactionMappings().values(); + ChangedTransactionDetail changedTransactionDetail = ctx.getChangedTransactionDetail(); + Long originalChargebackTransactionId = changedTransactionDetail.getCurrentTransactionToOldId().get(loanTransaction); + Collection updatedTransactions = changedTransactionDetail.getNewTransactionMappings().values(); Optional updatedTransaction = updatedTransactions.stream().filter(tr -> tr.getLoanTransactionRelations() .stream().anyMatch(this.hasMatchingToLoanTransaction(originalChargebackTransactionId, CHARGEBACK))).findFirst(); @@ -577,46 +583,33 @@ private void processSingleTransaction(LoanTransaction loanTransaction, MonetaryC MoneyHolder overpaymentHolder, ProgressiveLoanInterestScheduleModel scheduleModel) { TransactionCtx ctx = new ProgressiveTransactionCtx(currency, installments, charges, overpaymentHolder, changedTransactionDetail, scheduleModel); - if (loanTransaction.getId() == null) { - processLatestTransaction(loanTransaction, ctx); - if (loanTransaction.isInterestWaiver()) { - loanTransaction.adjustInterestComponent(currency); - } + boolean isNew = loanTransaction.getId() == null; + LoanTransaction processTransaction = loanTransaction; + if (!isNew) { + //For existing transactions, check if the re-payment breakup (principal, interest, fees, penalties) has changed. + processTransaction = LoanTransaction.copyTransactionProperties(loanTransaction); + ctx.getChangedTransactionDetail().getCurrentTransactionToOldId().put(processTransaction, loanTransaction.getId()); + } + // Reset derived component of new loan transaction and re-process transaction + processLatestTransaction(processTransaction, ctx); + if (loanTransaction.isInterestWaiver()) { + processTransaction.adjustInterestComponent(currency); + } + if (isNew) { + updateOldTransaction(loanTransaction, ctx); } else { - /* - * For existing transactions, check if the re-payment breakup (principal, interest, fees, penalties) has - * changed.
- */ - final LoanTransaction newLoanTransaction = LoanTransaction.copyTransactionProperties(loanTransaction); - ctx.getChangedTransactionDetail().getCurrentTransactionToOldId().put(newLoanTransaction, loanTransaction.getId()); - - // Reset derived component of new loan transaction and - // re-process transaction - processLatestTransaction(newLoanTransaction, ctx); - if (loanTransaction.isInterestWaiver()) { - newLoanTransaction.adjustInterestComponent(currency); - } - /* - * Check if the transaction amounts have changed or was there any transaction for the same date which was - * reverse-replayed. If so, reverse the original transaction and update changedTransactionDetail accordingly - */ - updateOrCreateProcessedTransaction(loanTransaction, newLoanTransaction, currency, changedTransactionDetail, ctx); + updateOrRegisterProcessedTransaction(loanTransaction, processTransaction, ctx); } } private List processOverpaidTransactions(List overpaidTransactions, MonetaryCurrency currency, List installments, Set charges, ChangedTransactionDetail changedTransactionDetail, MoneyHolder overpaymentHolder, ProgressiveLoanInterestScheduleModel scheduleModel) { - List newOverpaidTransactions = new ArrayList<>(); + List remainingTransactions = new ArrayList<>(overpaidTransactions); TransactionCtx ctx = new ProgressiveTransactionCtx(currency, installments, charges, overpaymentHolder, changedTransactionDetail, scheduleModel); Money zero = Money.zero(currency); - boolean hasUnprocessed = true; for (LoanTransaction transaction : overpaidTransactions) { - if (!hasUnprocessed) { - newOverpaidTransactions.add(transaction); - continue; - } LoanTransaction processTransaction = transaction; boolean isNew = transaction.getId() == null; if (!isNew) { @@ -625,34 +618,72 @@ private List processOverpaidTransactions(List } Money overpayment = processTransaction.getOverPaymentPortion(currency); processTransaction.setOverPayments(zero); - overpaymentHolder.setMoneyObject(overpaymentHolder.getMoneyObject().minus(overpayment)); + overpaymentHolder.setMoneyObject(MathUtil.minus(overpaymentHolder.getMoneyObject(), overpayment)); - processTransaction(processTransaction, ctx, overpayment); + List transactionMappings = new ArrayList<>(); + Balances balances = new Balances(zero, zero, zero, zero); + + // TODO processLatestTransaction by transactionType + Money unprocessed = processPeriods(transaction, overpayment, charges, transactionMappings, balances, ctx); + + transaction.updateComponents(balances.getAggregatedPrincipalPortion(), balances.getAggregatedInterestPortion(), + balances.getAggregatedFeeChargesPortion(), balances.getAggregatedPenaltyChargesPortion()); + transaction.addLoanTransactionToRepaymentScheduleMappings(transactionMappings); + + handleOverpayment(unprocessed, transaction, overpaymentHolder); if (processTransaction.isInterestWaiver()) { processTransaction.adjustInterestComponent(currency); } - if (!isNew) { - processTransaction = updateOrCreateProcessedTransaction(transaction, processTransaction, currency, changedTransactionDetail, - ctx); + if (isNew) { + processTransaction = updateOldTransaction(transaction, ctx); + } else { + processTransaction = updateOrRegisterProcessedTransaction(transaction, processTransaction, ctx); } + remainingTransactions.remove(transaction); if (processTransaction.isOverPaid()) { - newOverpaidTransactions.add(processTransaction); - hasUnprocessed = false; + remainingTransactions.add(processTransaction); + break; } } - return newOverpaidTransactions; + return remainingTransactions; } - private LoanTransaction updateOrCreateProcessedTransaction(LoanTransaction oldTransaction, LoanTransaction newTransaction, - MonetaryCurrency currency, ChangedTransactionDetail changedTransactionDetail, TransactionCtx ctx) { - boolean alreadyProcessed = changedTransactionDetail.getNewTransactionMappings().values().stream() + private LoanTransaction updateOldTransaction(LoanTransaction loanTransaction, TransactionCtx ctx) { + MonetaryCurrency currency = ctx.getCurrency(); + ChangedTransactionDetail changedTransactionDetail = ctx.getChangedTransactionDetail(); + Long oldTransactionId = changedTransactionDetail.getCurrentTransactionToOldId().get(loanTransaction); + if (oldTransactionId != null) { + LoanTransaction oldTransaction = loanTransaction.getLoan().getLoanTransaction(e -> oldTransactionId.equals(e.getId())); + if (LoanTransaction.transactionAmountsMatch(currency, oldTransaction, loanTransaction)) { + oldTransaction.updateLoanTransactionToRepaymentScheduleMappings(loanTransaction.getLoanTransactionToRepaymentScheduleMappings()); + changedTransactionDetail.getCurrentTransactionToOldId().remove(loanTransaction); + changedTransactionDetail.getNewTransactionMappings().remove(oldTransactionId); + return oldTransaction; + } + } + // TODO chargeback + return loanTransaction; + } + + private LoanTransaction updateOrRegisterProcessedTransaction(LoanTransaction oldTransaction, LoanTransaction newTransaction, TransactionCtx ctx) { + MonetaryCurrency currency = ctx.getCurrency(); + ChangedTransactionDetail changedTransactionDetail = ctx.getChangedTransactionDetail(); + /* + * Check if the transaction amounts have changed or was there any transaction for the same date which was + * reverse-replayed. If so, reverse the original transaction and update changedTransactionDetail accordingly + */ + Map newTransactionMappings = changedTransactionDetail.getNewTransactionMappings(); + boolean alreadyProcessed = newTransactionMappings.values().stream() .anyMatch(lt -> lt.getTransactionDate().equals(oldTransaction.getTransactionDate())); - if (!alreadyProcessed && LoanTransaction.transactionAmountsMatch(currency, oldTransaction, newTransaction)) { + boolean amountMatch = LoanTransaction.transactionAmountsMatch(currency, oldTransaction, newTransaction); + if (!alreadyProcessed && amountMatch) { oldTransaction .updateLoanTransactionToRepaymentScheduleMappings(newTransaction.getLoanTransactionToRepaymentScheduleMappings()); + changedTransactionDetail.getCurrentTransactionToOldId().remove(newTransaction); return oldTransaction; } - createNewTransaction(oldTransaction, newTransaction, changedTransactionDetail); + newTransactionMappings.put(oldTransaction.getId(), newTransaction); + // if chargeback is getting reverse-replayed // find replayed transaction with CHARGEBACK relation with reversed chargeback transaction // for replayed transaction, add relation to point to new Chargeback transaction @@ -1161,17 +1192,19 @@ private Money processPeriods(LoanTransaction transaction, Money processAmount, S LoanScheduleProcessingType scheduleProcessingType = transaction.getLoan().getLoanProductRelatedDetail() .getLoanScheduleProcessingType(); if (scheduleProcessingType.isHorizontal()) { - return processPeriodsHorizontally(transaction, currency, transactionCtx.getInstallments(), processAmount, allocationRule, + return processPeriodsHorizontally(transaction, transactionCtx, processAmount, allocationRule, + transactionMappings, charges, balances); + } + if (scheduleProcessingType.isVertical()) { + return processPeriodsVertically(transaction, currency, transactionCtx.getInstallments(), processAmount, allocationRule, transactionMappings, charges, balances); } - return processPeriodsVertically(transaction, currency, transactionCtx.getInstallments(), processAmount, allocationRule, - transactionMappings, charges, balances); + return processAmount; } - private Money processPeriodsHorizontally(LoanTransaction loanTransaction, MonetaryCurrency currency, - List installments, Money transactionAmountUnprocessed, - LoanPaymentAllocationRule paymentAllocationRule, List transactionMappings, - Set charges, Balances balances) { + private Money processPeriodsHorizontally(LoanTransaction loanTransaction, TransactionCtx transactionCtx, + Money transactionAmountUnprocessed, LoanPaymentAllocationRule paymentAllocationRule, + List transactionMappings, Set charges, Balances balances) { LinkedHashMap> paymentAllocationsMap = paymentAllocationRule.getAllocationTypes().stream() .collect(Collectors.groupingBy(PaymentAllocationType::getDueType, LinkedHashMap::new, mapping(Function.identity(), toList()))); @@ -1328,6 +1361,16 @@ private Money processAllocationsHorizontally(LoanTransaction loanTransaction, Tr return transactionAmountUnprocessed; } + @NotNull + private static Set getLoanChargesOfInstallment(Set charges, LoanRepaymentScheduleInstallment currentInstallment, + int firstNormalInstallmentNumber) { + return charges.stream().filter(loanCharge -> currentInstallment.getInstallmentNumber().equals(firstNormalInstallmentNumber) + ? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(currentInstallment.getFromDate(), + currentInstallment.getDueDate()) + : loanCharge.isDueForCollectionFromAndUpToAndIncluding(currentInstallment.getFromDate(), currentInstallment.getDueDate())) + .collect(Collectors.toSet()); + } + private Money processPeriodsVertically(LoanTransaction loanTransaction, MonetaryCurrency currency, List installments, Money transactionAmountUnprocessed, LoanPaymentAllocationRule paymentAllocationRule, List transactionMappings, @@ -1425,16 +1468,6 @@ private Money processPeriodsVertically(LoanTransaction loanTransaction, Monetary return transactionAmountUnprocessed; } - @NotNull - private static Set getLoanChargesOfInstallment(Set charges, LoanRepaymentScheduleInstallment currentInstallment, - int firstNormalInstallmentNumber) { - return charges.stream().filter(loanCharge -> currentInstallment.getInstallmentNumber().equals(firstNormalInstallmentNumber) - ? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(currentInstallment.getFromDate(), - currentInstallment.getDueDate()) - : loanCharge.isDueForCollectionFromAndUpToAndIncluding(currentInstallment.getFromDate(), currentInstallment.getDueDate())) - .collect(Collectors.toSet()); - } - private Predicate getFilterPredicate(PaymentAllocationType paymentAllocationType, MonetaryCurrency currency) { return switch (paymentAllocationType.getAllocationType()) { @@ -1534,47 +1567,4 @@ public static LoanPaymentAllocationRule getAllocationRule(LoanTransaction loanTr public static LoanPaymentAllocationRule getDefaultAllocationRule(Loan loan) { return loan.getPaymentAllocationRules().stream().filter(e -> e.getTransactionType().isDefault()).findFirst().orElseThrow(); } - - protected void adjustAdditionalInstallments(List installments, Set charges) { - if (installments == null) { - return; - } - LoanCharge latestCharge = getLatestLoanChargeWithSpecificDueDate(charges); - if (latestCharge == null) { - installments.removeIf(LoanRepaymentScheduleInstallment::isAdditional); - return; - } - LocalDate chargeDueDate = latestCharge.getEffectiveDueDate(); - LoanRepaymentScheduleInstallment latestInstallment = installments.stream().filter(i -> !i.isDownPayment() && !i.isAdditional()) - .reduce((first, second) -> second).orElseThrow(); - if (!DateUtils.isAfter(chargeDueDate, latestInstallment.getDueDate())) { - installments.removeIf(LoanRepaymentScheduleInstallment::isAdditional); - return; - } - Loan loan = latestInstallment.getLoan(); - List additionalInstallments = installments.stream() - .filter(LoanRepaymentScheduleInstallment::isAdditional).toList(); - if (additionalInstallments.isEmpty()) { - final LoanRepaymentScheduleInstallment additionalInstallment = new LoanRepaymentScheduleInstallment(loan, - (installments.size() + 1), latestInstallment.getDueDate(), chargeDueDate, BigDecimal.ZERO, BigDecimal.ZERO, - BigDecimal.ZERO, BigDecimal.ZERO, false, null); - additionalInstallment.markAsAdditional(); - loan.addLoanRepaymentScheduleInstallment(additionalInstallment); - } else { - MonetaryCurrency currency = loan.getCurrency(); - Money zero = Money.zero(currency); - Money interestAccrued = additionalInstallments.stream().map(i -> MathUtil.nullToZero(i.getInterestAccrued(currency), currency)) - .reduce(zero, Money::plus); - Money feeAccrued = additionalInstallments.stream().map(i -> MathUtil.nullToZero(i.getFeeAccrued(currency), currency)) - .reduce(zero, Money::plus); - Money penaltyAccrued = additionalInstallments.stream().map(i -> MathUtil.nullToZero(i.getPenaltyAccrued(currency), currency)) - .reduce(zero, Money::plus); - - LoanRepaymentScheduleInstallment additionalInstallment = additionalInstallments.get(0); - additionalInstallment.updateDueDate(chargeDueDate); - additionalInstallment.updateAccrualPortion(interestAccrued, feeAccrued, penaltyAccrued); - installments.removeIf(i -> i.isAdditional() && !i.getInstallmentNumber().equals(additionalInstallment.getInstallmentNumber())); - // TODO: mark as no reage - } - } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java index 34598a69d37..21beb64949d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java @@ -209,29 +209,34 @@ public CommandProcessingResult addLoanCharge(final Long loanId, final JsonComman boolean isAppliedOnBackDate = false; LoanCharge loanCharge = null; LocalDate recalculateFrom = loan.fetchInterestRecalculateFromDate(); + LocalDate transactionDate = null; if (chargeDefinition.isPercentageOfDisbursementAmount()) { LoanTrancheDisbursementCharge loanTrancheDisbursementCharge; ExternalId externalId = externalIdFactory.createFromCommand(command, "externalId"); - boolean needToGenerateNewExternalId = false; + boolean isFirst = true; for (LoanDisbursementDetails disbursementDetail : loanDisburseDetails) { if (disbursementDetail.actualDisbursementDate() == null) { // If multiple charges to be applied, only the first one will get the provided externalId, for the // rest we generate new ones (if needed) - if (needToGenerateNewExternalId) { + if (!isFirst) { externalId = externalIdFactory.create(); } + LocalDate dueDate = disbursementDetail.expectedDisbursementDateAsLocalDate(); loanCharge = loanChargeAssembler.createNewWithoutLoan(chargeDefinition, disbursementDetail.principal(), null, null, - null, disbursementDetail.expectedDisbursementDateAsLocalDate(), null, null, externalId); + null, dueDate, null, null, externalId); loanTrancheDisbursementCharge = new LoanTrancheDisbursementCharge(loanCharge, disbursementDetail); loanCharge.updateLoanTrancheDisbursementCharge(loanTrancheDisbursementCharge); businessEventNotifierService.notifyPreBusinessEvent(new LoanAddChargeBusinessEvent(loanCharge)); validateAddLoanCharge(loan, chargeDefinition, loanCharge); addCharge(loan, chargeDefinition, loanCharge); isAppliedOnBackDate = true; - if (DateUtils.isAfter(recalculateFrom, disbursementDetail.expectedDisbursementDateAsLocalDate())) { - recalculateFrom = disbursementDetail.expectedDisbursementDateAsLocalDate(); + if (DateUtils.isAfter(recalculateFrom, dueDate)) { + recalculateFrom = dueDate; } - needToGenerateNewExternalId = true; + if (isFirst) { + transactionDate = loanCharge.getEffectiveDueDate(); + } + isFirst = false; } } if (loanCharge == null) { @@ -246,35 +251,36 @@ public CommandProcessingResult addLoanCharge(final Long loanId, final JsonComman validateAddLoanCharge(loan, chargeDefinition, loanCharge); isAppliedOnBackDate = addCharge(loan, chargeDefinition, loanCharge); - if (loanCharge.getDueLocalDate() == null || DateUtils.isAfter(recalculateFrom, loanCharge.getDueLocalDate())) { + if (DateUtils.isAfter(recalculateFrom, loanCharge.getDueLocalDate())) { isAppliedOnBackDate = true; recalculateFrom = loanCharge.getDueLocalDate(); } + transactionDate = loanCharge.getEffectiveDueDate(); } boolean reprocessRequired = true; if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { if (isAppliedOnBackDate && loan.isFeeCompoundingEnabledForInterestRecalculation()) { - loan = runScheduleRecalculation(loan, recalculateFrom); reprocessRequired = false; } this.loanWritePlatformService.updateOriginalSchedule(loan); } - // [For Adv payment allocation strategy] check if charge due date is earlier than last transaction - // date, if yes trigger reprocess else no reprocessing - if (AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY.equals(loan.transactionProcessingStrategy())) { + // overpaid transactions will be reprocessed and pay this charge + boolean overpaidReprocess = !loanCharge.isDueAtDisbursement() && !loanCharge.isPaid() && loan.getStatus().isOverpaid(); + if (!overpaidReprocess && AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY.equals(loan.transactionProcessingStrategy())) { + // [For Adv payment allocation strategy] check if charge due date is earlier than last transaction + // date, if yes trigger reprocess else no reprocessing LoanTransaction lastPaymentTransaction = loan.getLastTransactionForReprocessing(); - if (lastPaymentTransaction != null) { - if (loanCharge.getEffectiveDueDate() != null - && DateUtils.isAfter(loanCharge.getEffectiveDueDate(), lastPaymentTransaction.getTransactionDate())) { + if (lastPaymentTransaction != null && DateUtils.isAfter(loanCharge.getEffectiveDueDate(), lastPaymentTransaction.getTransactionDate())) { reprocessRequired = false; - } } } if (reprocessRequired) { - ChangedTransactionDetail changedTransactionDetail = loan.reprocessTransactions(); + ChangedTransactionDetail changedTransactionDetail = overpaidReprocess + ? loan.reprocessTransactionsWithPostTransactionChecks(transactionDate) + : loan.reprocessTransactions(); if (changedTransactionDetail != null) { for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue()); @@ -284,7 +290,6 @@ public CommandProcessingResult addLoanCharge(final Long loanId, final JsonComman replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail); } loan = loanAccountDomainService.saveAndFlushLoanWithDataIntegrityViolationChecks(loan); - } postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);