diff --git a/fineract-avro-schemas/src/main/avro/loan/v1/LoanTransactionDataV1.avsc b/fineract-avro-schemas/src/main/avro/loan/v1/LoanTransactionDataV1.avsc index 86bfe92be..605876fcb 100644 --- a/fineract-avro-schemas/src/main/avro/loan/v1/LoanTransactionDataV1.avsc +++ b/fineract-avro-schemas/src/main/avro/loan/v1/LoanTransactionDataV1.avsc @@ -205,6 +205,17 @@ "items": "org.apache.fineract.avro.loan.v1.LoanChargePaidByDataV1" } ] + }, + { + "default": null, + "name": "transactionRelations", + "type": [ + "null", + { + "type": "array", + "items": "org.apache.fineract.avro.loan.v1.LoanTransactionRelationDataV1" + } + ] } ] } diff --git a/fineract-avro-schemas/src/main/avro/loan/v1/LoanTransactionRelationDataV1.avsc b/fineract-avro-schemas/src/main/avro/loan/v1/LoanTransactionRelationDataV1.avsc new file mode 100644 index 000000000..56b296edd --- /dev/null +++ b/fineract-avro-schemas/src/main/avro/loan/v1/LoanTransactionRelationDataV1.avsc @@ -0,0 +1,47 @@ +{ + "name": "LoanTransactionRelationDataV1", + "namespace": "org.apache.fineract.avro.loan.v1", + "type": "record", + "fields": [ + { + "name": "fromLoanTransaction", + "type": "int" + }, + { + "default": null, + "name": "toLoanTransaction", + "type": [ + "null", + "int" + ] + }, + { + "default": null, + "name": "toLoanCharge", + "type": [ + "null", + "int" + ] + }, + { + "default": null, + "name": "amount", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "name": "relationType", + "type": "string" + }, + { + "default": null, + "name": "paymentType", + "type": [ + "null", + "string" + ] + } + ] +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/glaccount/api/GLAccountsApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/glaccount/api/GLAccountsApiResourceSwagger.java index 34c85fe2e..56a2c4518 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/glaccount/api/GLAccountsApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/glaccount/api/GLAccountsApiResourceSwagger.java @@ -120,16 +120,16 @@ final class GLAccountsApiResourceSwagger { public Boolean manualEntriesAllowed; @Schema(example = "1") - public String type; + public Integer type; @Schema(example = "10") - public String tagId; + public Long tagId; @Schema(example = "1") public Long parentId; @Schema(example = "1") - public String usage; + public Integer usage; @Schema(example = "Desc") public String description; @@ -144,7 +144,7 @@ final class GLAccountsApiResourceSwagger { } @Schema(example = "22") - public int resourceId; + public Long resourceId; } @Schema(description = "PutGLAccountsRequest") @@ -175,7 +175,7 @@ final class GLAccountsApiResourceSwagger { } @Schema(example = "1") - public int resourceId; + public Long resourceId; public PutGLAccountsResponsechangesSwagger changes; } @@ -193,7 +193,7 @@ final class GLAccountsApiResourceSwagger { } @Schema(example = "1") - public int resourceId; + public Long resourceId; private DeleteGLAccountsRequestchangesSwagger changes; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanTransactionDTO.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanTransactionDTO.java index 9924cfb6e..bd9ebf290 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanTransactionDTO.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanTransactionDTO.java @@ -24,6 +24,7 @@ import java.util.List; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.Setter; +import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData; import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionEnumData; @RequiredArgsConstructor @@ -58,4 +59,5 @@ public class LoanTransactionDTO { private boolean isLoanToLoanTransfer; private final String chargeRefundChargeType; + private final LoanChargeData loanChargeData; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java index f33fb53b6..4d8e854de 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java @@ -66,6 +66,7 @@ import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatfor import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper; import org.apache.fineract.portfolio.client.domain.ClientTransaction; import org.apache.fineract.portfolio.client.domain.ClientTransactionRepository; +import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData; import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionEnumData; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; @@ -125,6 +126,7 @@ public class AccountingProcessorHelper { final boolean reversed = (Boolean) map.get("reversed"); final Long paymentTypeId = (Long) map.get("paymentTypeId"); final String chargeRefundChargeType = (String) map.get("chargeRefundChargeType"); + final LoanChargeData loanChargeData = (LoanChargeData) map.get("loanChargeData"); final List feePaymentDetails = new ArrayList<>(); final List penaltyPaymentDetails = new ArrayList<>(); @@ -152,7 +154,7 @@ public class AccountingProcessorHelper { } final LoanTransactionDTO transaction = new LoanTransactionDTO(transactionOfficeId, paymentTypeId, transactionId, transactionDate, transactionType, amount, principal, interest, fees, penalties, overPayments, reversed, - penaltyPaymentDetails, feePaymentDetails, isAccountTransfer, chargeRefundChargeType); + penaltyPaymentDetails, feePaymentDetails, isAccountTransfer, chargeRefundChargeType, loanChargeData); Boolean isLoanToLoanTransfer = (Boolean) accountingBridgeData.get("isLoanToLoanTransfer"); transaction.setLoanToLoanTransfer(isLoanToLoanTransfer != null && isLoanToLoanTransfer); newLoanTransactions.add(transaction); @@ -622,6 +624,17 @@ public class AccountingProcessorHelper { } } + public void createDebitJournalEntryOrReversalForLoanCharges(final Office office, final String currencyCode, + final int accountMappingTypeId, final Long loanProductId, final Long chargeId, final Long loanId, final String transactionId, + final LocalDate transactionDate, final BigDecimal amount, final Boolean isReversal) { + final GLAccount account = getLinkedGLAccountForLoanCharges(loanProductId, accountMappingTypeId, chargeId); + if (isReversal) { + createCreditJournalEntryForLoan(office, currencyCode, account, loanId, transactionId, transactionDate, amount); + } else { + createDebitJournalEntryForLoan(office, currencyCode, account, loanId, transactionId, transactionDate, amount); + } + } + public void createCreditJournalEntryOrReversalForLoanCharges(final Office office, final String currencyCode, final int accountMappingTypeId, final Long loanProductId, final Long loanId, final String transactionId, final LocalDate transactionDate, final BigDecimal totalAmount, final Boolean isReversal, diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java index 096bc2d19..29feeba36 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java @@ -61,9 +61,10 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess /*** * Handle repayments, loan refunds, repayments at disbursement and reversal of Repayments and Repayments at - * disbursement + * disbursement (except charge adjustment) ***/ - else if (loanTransactionDTO.getTransactionType().isRepaymentType() + else if ((loanTransactionDTO.getTransactionType().isRepaymentType() + && !loanTransactionDTO.getTransactionType().isChargeAdjustment()) || loanTransactionDTO.getTransactionType().isRepaymentAtDisbursement() || loanTransactionDTO.getTransactionType().isChargePayment()) { createJournalEntriesForRepaymentsAndWriteOffs(loanDTO, loanTransactionDTO, office, false, @@ -99,6 +100,109 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess else if (loanTransactionDTO.getTransactionType().isChargeback()) { createJournalEntriesForChargeback(loanDTO, loanTransactionDTO, office); } + // Logic for Charge Adjustment + else if (loanTransactionDTO.getTransactionType().isChargeAdjustment()) { + createJournalEntriesForChargeAdjustment(loanDTO, loanTransactionDTO, office); + } + } + } + + private void createJournalEntriesForChargeAdjustment(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO, Office office) { + // loan properties + final Long loanProductId = loanDTO.getLoanProductId(); + final Long loanId = loanDTO.getLoanId(); + final String currencyCode = loanDTO.getCurrencyCode(); + + // transaction properties + final String transactionId = loanTransactionDTO.getTransactionId(); + final LocalDate transactionDate = loanTransactionDTO.getTransactionDate(); + final BigDecimal principalAmount = loanTransactionDTO.getPrincipal(); + final BigDecimal interestAmount = loanTransactionDTO.getInterest(); + final BigDecimal feesAmount = loanTransactionDTO.getFees(); + final BigDecimal penaltiesAmount = loanTransactionDTO.getPenalties(); + final BigDecimal overPaymentAmount = loanTransactionDTO.getOverPayment(); + final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId(); + final boolean isReversal = loanTransactionDTO.isReversed(); + + BigDecimal totalDebitAmount = new BigDecimal(0); + + Map accountMap = new HashMap<>(); + + // handle principal payment (and reversals) + if (principalAmount != null && !(principalAmount.compareTo(BigDecimal.ZERO) == 0)) { + totalDebitAmount = totalDebitAmount.add(principalAmount); + GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, + AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), paymentTypeId); + accountMap.put(account, principalAmount); + } + + // handle interest payment (and reversals) + if (interestAmount != null && !(interestAmount.compareTo(BigDecimal.ZERO) == 0)) { + totalDebitAmount = totalDebitAmount.add(interestAmount); + GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, + AccrualAccountsForLoan.INTEREST_RECEIVABLE.getValue(), paymentTypeId); + if (accountMap.containsKey(account)) { + BigDecimal amount = accountMap.get(account).add(interestAmount); + accountMap.put(account, amount); + } else { + accountMap.put(account, interestAmount); + } + } + + // handle fees payment (and reversals) + if (feesAmount != null && !(feesAmount.compareTo(BigDecimal.ZERO) == 0)) { + totalDebitAmount = totalDebitAmount.add(feesAmount); + GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, + AccrualAccountsForLoan.FEES_RECEIVABLE.getValue(), paymentTypeId); + if (accountMap.containsKey(account)) { + BigDecimal amount = accountMap.get(account).add(feesAmount); + accountMap.put(account, amount); + } else { + accountMap.put(account, feesAmount); + } + } + + // handle penalties payment (and reversals) + if (penaltiesAmount != null && !(penaltiesAmount.compareTo(BigDecimal.ZERO) == 0)) { + totalDebitAmount = totalDebitAmount.add(penaltiesAmount); + GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, + AccrualAccountsForLoan.PENALTIES_RECEIVABLE.getValue(), paymentTypeId); + if (accountMap.containsKey(account)) { + BigDecimal amount = accountMap.get(account).add(penaltiesAmount); + accountMap.put(account, amount); + } else { + accountMap.put(account, penaltiesAmount); + } + } + + // handle overpayment + if (overPaymentAmount != null && !(overPaymentAmount.compareTo(BigDecimal.ZERO) == 0)) { + totalDebitAmount = totalDebitAmount.add(overPaymentAmount); + GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, AccrualAccountsForLoan.OVERPAYMENT.getValue(), + paymentTypeId); + if (accountMap.containsKey(account)) { + BigDecimal amount = accountMap.get(account).add(overPaymentAmount); + accountMap.put(account, amount); + } else { + accountMap.put(account, overPaymentAmount); + } + } + + for (Map.Entry entry : accountMap.entrySet()) { + this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, loanId, transactionId, transactionDate, + entry.getValue(), isReversal, entry.getKey()); + } + + if (!(totalDebitAmount.compareTo(BigDecimal.ZERO) == 0)) { + Long chargeId = loanTransactionDTO.getLoanChargeData().getChargeId(); + Integer accountMappingTypeId; + if (loanTransactionDTO.getLoanChargeData().isPenalty()) { + accountMappingTypeId = AccrualAccountsForLoan.INCOME_FROM_PENALTIES.getValue(); + } else { + accountMappingTypeId = AccrualAccountsForLoan.INCOME_FROM_FEES.getValue(); + } + this.helper.createDebitJournalEntryOrReversalForLoanCharges(office, currencyCode, accountMappingTypeId, loanProductId, chargeId, + loanId, transactionId, transactionDate, totalDebitAmount, isReversal); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForLoan.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForLoan.java index 709882c0c..f2947463a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForLoan.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForLoan.java @@ -21,11 +21,15 @@ package org.apache.fineract.accounting.journalentry.service; import java.math.BigDecimal; import java.time.LocalDate; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import lombok.RequiredArgsConstructor; import org.apache.fineract.accounting.closure.domain.GLClosure; +import org.apache.fineract.accounting.common.AccountingConstants; import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForLoan; import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity; +import org.apache.fineract.accounting.glaccount.domain.GLAccount; import org.apache.fineract.accounting.journalentry.data.ChargePaymentDTO; import org.apache.fineract.accounting.journalentry.data.LoanDTO; import org.apache.fineract.accounting.journalentry.data.LoanTransactionDTO; @@ -58,9 +62,10 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF } /*** * Logic for repayments, repayments at disbursement and reversal of Repayments and Repayments at - * disbursement + * disbursement (except charge adjustment) ***/ - else if (loanTransactionDTO.getTransactionType().isRepaymentType() + else if ((loanTransactionDTO.getTransactionType().isRepaymentType() + && !loanTransactionDTO.getTransactionType().isChargeAdjustment()) || loanTransactionDTO.getTransactionType().isRepaymentAtDisbursement() || loanTransactionDTO.getTransactionType().isChargePayment()) { createJournalEntriesForRepayments(loanDTO, loanTransactionDTO, office); @@ -106,6 +111,109 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF else if (loanTransactionDTO.getTransactionType().isChargeback()) { createJournalEntriesForChargeback(loanDTO, loanTransactionDTO, office); } + // Logic for Charge Adjustment + else if (loanTransactionDTO.getTransactionType().isChargeAdjustment()) { + createJournalEntriesForChargeAdjustment(loanDTO, loanTransactionDTO, office); + } + } + } + + private void createJournalEntriesForChargeAdjustment(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO, Office office) { + // loan properties + final Long loanProductId = loanDTO.getLoanProductId(); + final Long loanId = loanDTO.getLoanId(); + final String currencyCode = loanDTO.getCurrencyCode(); + + // transaction properties + final String transactionId = loanTransactionDTO.getTransactionId(); + final LocalDate transactionDate = loanTransactionDTO.getTransactionDate(); + final BigDecimal principalAmount = loanTransactionDTO.getPrincipal(); + final BigDecimal interestAmount = loanTransactionDTO.getInterest(); + final BigDecimal feesAmount = loanTransactionDTO.getFees(); + final BigDecimal penaltiesAmount = loanTransactionDTO.getPenalties(); + final BigDecimal overPaymentAmount = loanTransactionDTO.getOverPayment(); + final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId(); + final boolean isReversal = loanTransactionDTO.isReversed(); + + BigDecimal totalDebitAmount = new BigDecimal(0); + + Map accountMap = new HashMap<>(); + + // handle principal payment (and reversals) + if (principalAmount != null && !(principalAmount.compareTo(BigDecimal.ZERO) == 0)) { + totalDebitAmount = totalDebitAmount.add(principalAmount); + GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, + AccountingConstants.CashAccountsForLoan.LOAN_PORTFOLIO.getValue(), paymentTypeId); + accountMap.put(account, principalAmount); + } + + // handle interest payment (and reversals) + if (interestAmount != null && !(interestAmount.compareTo(BigDecimal.ZERO) == 0)) { + totalDebitAmount = totalDebitAmount.add(interestAmount); + GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, + AccountingConstants.CashAccountsForLoan.INTEREST_ON_LOANS.getValue(), paymentTypeId); + if (accountMap.containsKey(account)) { + BigDecimal amount = accountMap.get(account).add(interestAmount); + accountMap.put(account, amount); + } else { + accountMap.put(account, interestAmount); + } + } + + // handle fees payment (and reversals) + if (feesAmount != null && !(feesAmount.compareTo(BigDecimal.ZERO) == 0)) { + totalDebitAmount = totalDebitAmount.add(feesAmount); + GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, + AccountingConstants.CashAccountsForLoan.INCOME_FROM_FEES.getValue(), paymentTypeId); + if (accountMap.containsKey(account)) { + BigDecimal amount = accountMap.get(account).add(feesAmount); + accountMap.put(account, amount); + } else { + accountMap.put(account, feesAmount); + } + } + + // handle penalties payment (and reversals) + if (penaltiesAmount != null && !(penaltiesAmount.compareTo(BigDecimal.ZERO) == 0)) { + totalDebitAmount = totalDebitAmount.add(penaltiesAmount); + GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, + CashAccountsForLoan.INCOME_FROM_PENALTIES.getValue(), paymentTypeId); + if (accountMap.containsKey(account)) { + BigDecimal amount = accountMap.get(account).add(penaltiesAmount); + accountMap.put(account, amount); + } else { + accountMap.put(account, penaltiesAmount); + } + } + + // handle overpayment + if (overPaymentAmount != null && !(overPaymentAmount.compareTo(BigDecimal.ZERO) == 0)) { + totalDebitAmount = totalDebitAmount.add(overPaymentAmount); + GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, + AccountingConstants.CashAccountsForLoan.OVERPAYMENT.getValue(), paymentTypeId); + if (accountMap.containsKey(account)) { + BigDecimal amount = accountMap.get(account).add(overPaymentAmount); + accountMap.put(account, amount); + } else { + accountMap.put(account, overPaymentAmount); + } + } + + for (Map.Entry entry : accountMap.entrySet()) { + this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, loanId, transactionId, transactionDate, + entry.getValue(), isReversal, entry.getKey()); + } + + if (!(totalDebitAmount.compareTo(BigDecimal.ZERO) == 0)) { + Long chargeId = loanTransactionDTO.getLoanChargeData().getChargeId(); + Integer accountMappingTypeId; + if (loanTransactionDTO.getLoanChargeData().isPenalty()) { + accountMappingTypeId = AccountingConstants.CashAccountsForLoan.INCOME_FROM_PENALTIES.getValue(); + } else { + accountMappingTypeId = AccountingConstants.CashAccountsForLoan.INCOME_FROM_FEES.getValue(); + } + this.helper.createDebitJournalEntryOrReversalForLoanCharges(office, currencyCode, accountMappingTypeId, loanProductId, chargeId, + loanId, transactionId, transactionDate, totalDebitAmount, isReversal); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/ApplyChargeToOverdueLoansBusinessStep.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/ApplyChargeToOverdueLoansBusinessStep.java index 42ac49451..3b89896bc 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/ApplyChargeToOverdueLoansBusinessStep.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/ApplyChargeToOverdueLoansBusinessStep.java @@ -22,8 +22,8 @@ import java.util.Collection; import lombok.RequiredArgsConstructor; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData; +import org.apache.fineract.portfolio.loanaccount.service.LoanChargeWritePlatformService; import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; -import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService; import org.springframework.stereotype.Component; @Component @@ -31,14 +31,14 @@ import org.springframework.stereotype.Component; public class ApplyChargeToOverdueLoansBusinessStep implements LoanCOBBusinessStep { private final LoanReadPlatformService loanReadPlatformService; - private final LoanWritePlatformService loanWritePlatformService; + private final LoanChargeWritePlatformService loanChargeWritePlatformService; @Override public Loan execute(Loan loan) { final Collection overdueLoanScheduleDataList = loanReadPlatformService .retrieveAllOverdueInstallmentsForLoan(loan); - loanWritePlatformService.applyOverdueChargesForLoan(loan.getId(), overdueLoanScheduleDataList); + loanChargeWritePlatformService.applyOverdueChargesForLoan(loan.getId(), overdueLoanScheduleDataList); return loan; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index b25af17ef..4ccc6a0ff 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -787,6 +787,15 @@ public class CommandWrapperBuilder { return this; } + public CommandWrapperBuilder adjustmentForLoanCharge(final Long loanId, final Long loanChargeId) { + this.actionName = "ADJUSTMENT"; + this.entityName = "LOANCHARGE"; + this.entityId = loanChargeId; + this.loanId = loanId; + this.href = "/loans/" + loanId + "/charges/" + loanChargeId; + return this; + } + public CommandWrapperBuilder deleteLoanCharge(final Long loanId, final Long loanChargeId) { this.actionName = "DELETE"; this.entityName = "LOANCHARGE"; diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanChargeAdjustmentPostBusinessEvent.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanChargeAdjustmentPostBusinessEvent.java new file mode 100644 index 000000000..b8b6e7f23 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanChargeAdjustmentPostBusinessEvent.java @@ -0,0 +1,35 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.event.business.domain.loan.transaction; + +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; + +public class LoanChargeAdjustmentPostBusinessEvent extends LoanTransactionBusinessEvent { + + private static final String TYPE = "LoanChargeAdjustmentPostBusinessEvent"; + + public LoanChargeAdjustmentPostBusinessEvent(LoanTransaction value) { + super(value); + } + + @Override + public String getType() { + return TYPE; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanChargeAdjustmentPreBusinessEvent.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanChargeAdjustmentPreBusinessEvent.java new file mode 100644 index 000000000..8840c78fd --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanChargeAdjustmentPreBusinessEvent.java @@ -0,0 +1,36 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.event.business.domain.loan.transaction; + +import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBusinessEvent; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; + +public class LoanChargeAdjustmentPreBusinessEvent extends LoanBusinessEvent { + + private static final String TYPE = "LoanChargeAdjustmentPreBusinessEvent"; + + public LoanChargeAdjustmentPreBusinessEvent(Loan value) { + super(value); + } + + @Override + public String getType() { + return TYPE; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/data/CurrencyData.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/data/CurrencyData.java index 808ce564b..2e15a6d3b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/data/CurrencyData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/data/CurrencyData.java @@ -74,7 +74,10 @@ public class CurrencyData implements Serializable { private String generateDisplayLabel() { - final StringBuilder builder = new StringBuilder(this.name).append(' '); + final StringBuilder builder = new StringBuilder(20); + if (this.name != null) { + builder.append(this.name).append(' '); + } if (this.displaySymbol != null && !"".equalsIgnoreCase(this.displaySymbol.trim())) { builder.append('(').append(this.displaySymbol).append(')'); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/api/ChargesApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/api/ChargesApiResourceSwagger.java index 5aece98ff..d9745ded2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/api/ChargesApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/api/ChargesApiResourceSwagger.java @@ -52,7 +52,7 @@ final class ChargesApiResourceSwagger { private GetChargesTimeTypeResponse() {} @Schema(example = "1") - public Integer id; + public Long id; @Schema(example = "chargeTimeType.disbursement") public String code; @Schema(example = "Disbursement") @@ -64,7 +64,7 @@ final class ChargesApiResourceSwagger { private GetChargesAppliesToResponse() {} @Schema(example = "1") - public Integer id; + public Long id; @Schema(example = "chargeAppliesTo.loan") public String code; @Schema(example = "Loan") @@ -76,7 +76,7 @@ final class ChargesApiResourceSwagger { private GetChargesCalculationTypeResponse() {} @Schema(example = "1") - public Integer id; + public Long id; @Schema(example = "chargeCalculationType.flat") public String code; @Schema(example = "Flat") @@ -88,7 +88,7 @@ final class ChargesApiResourceSwagger { private GetChargesPaymentModeResponse() {} @Schema(example = "1") - public Integer id; + public Long id; @Schema(example = "chargepaymentmode.accounttransfer") public String code; @Schema(example = "Account Transfer") @@ -100,12 +100,12 @@ final class ChargesApiResourceSwagger { @Schema(example = "Loan Service fee") public String name; @Schema(example = "true") - public String active; + public boolean active; @Schema(example = "false") - public String penalty; + public boolean penalty; public GetChargesCurrencyResponse currency; @Schema(example = "230.56") - public Float amount; + public Double amount; public GetChargesTimeTypeResponse chargeTimeType; public GetChargesAppliesToResponse chargeAppliesTo; public GetChargesCalculationTypeResponse chargeCalculationType; @@ -126,7 +126,7 @@ final class ChargesApiResourceSwagger { @Schema(example = "en") public String locale; @Schema(example = "230.56") - public Float amount; + public Double amount; @Schema(example = "1") public Integer chargeTimeType; @Schema(example = "1") @@ -134,11 +134,11 @@ final class ChargesApiResourceSwagger { @Schema(example = "1") public Integer chargePaymentMode; @Schema(example = "true") - public String active; + public boolean active; @Schema(example = "dd MMMM") public String monthDayFormat; @Schema(example = "false") - public String penalty; + public boolean penalty; } @Schema(description = "PostChargesResponse") @@ -147,7 +147,7 @@ final class ChargesApiResourceSwagger { private PostChargesResponse() {} @Schema(example = "1") - public Integer resourceId; + public Long resourceId; } @Schema(description = "PutChargesChargeIdRequest") @@ -165,7 +165,7 @@ final class ChargesApiResourceSwagger { private PutChargesChargeIdResponse() {} @Schema(example = "1") - public Integer resourceId; + public Long resourceId; public PutChargesChargeIdRequest changes; } @@ -175,7 +175,7 @@ final class ChargesApiResourceSwagger { private DeleteChargesChargeIdResponse() {} @Schema(example = "1") - public Integer resourceId; + public Long resourceId; } @Schema(description = "GetChargesTemplateResponse") @@ -188,7 +188,7 @@ final class ChargesApiResourceSwagger { private GetChargesTemplateLoanChargeCalculationTypeOptions() {} @Schema(example = "1") - public Integer id; + public Long id; @Schema(example = "chargeCalculationType.flat") public String code; @Schema(example = "Flat") @@ -200,7 +200,7 @@ final class ChargesApiResourceSwagger { private GetChargesTemplateLoanChargeTimeTypeOptions() {} @Schema(example = "2") - public Integer id; + public Long id; @Schema(example = "chargeTimeType.specifiedDueDate") public String code; @Schema(example = "Specified due date") @@ -212,7 +212,7 @@ final class ChargesApiResourceSwagger { private GetChargesTemplateFeeFrequencyOptions() {} @Schema(example = "0") - public Integer id; + public Long id; @Schema(example = "loanTermFrequency.periodFrequencyType.days") public String code; @Schema(example = "Days") @@ -220,9 +220,9 @@ final class ChargesApiResourceSwagger { } @Schema(example = "false") - public String active; + public boolean active; @Schema(example = "false") - public String penalty; + public boolean penalty; public Set currencyOptions; public Set chargeCalculationTypeOptions; public Set chargeAppliesToOptions; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/domain/Charge.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/domain/Charge.java index 45270e9d1..b9e66373c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/domain/Charge.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/domain/Charge.java @@ -657,8 +657,11 @@ public class Charge extends AbstractPersistableCustom { final EnumOptionData chargeTimeType = ChargeEnumerations.chargeTimeType(this.chargeTimeType); final EnumOptionData chargeAppliesTo = ChargeEnumerations.chargeAppliesTo(this.chargeAppliesTo); final EnumOptionData chargeCalculationType = ChargeEnumerations.chargeCalculationType(this.chargeCalculation); - final EnumOptionData chargePaymentmode = ChargeEnumerations.chargePaymentMode(this.chargePaymentMode); - final EnumOptionData feeFrequencyType = ChargeEnumerations.chargePaymentMode(this.feeFrequency); + final EnumOptionData chargePaymentMode = ChargeEnumerations.chargePaymentMode(this.chargePaymentMode); + EnumOptionData feeFrequencyType = null; + if (this.feeFrequency != null) { + feeFrequencyType = ChargeEnumerations.feeFrequencyType(this.feeFrequency); + } GLAccountData accountData = null; if (account != null) { accountData = new GLAccountData().setId(account.getId()).setName(account.getName()).setGlCode(account.getGlCode()); @@ -675,7 +678,7 @@ public class Charge extends AbstractPersistableCustom { final CurrencyData currency = new CurrencyData(this.currencyCode, null, 0, 0, null, null); return ChargeData.instance(getId(), this.name, this.amount, currency, chargeTimeType, chargeAppliesTo, chargeCalculationType, - chargePaymentmode, getFeeOnMonthDay(), this.feeInterval, this.penalty, this.active, this.enableFreeWithdrawal, + chargePaymentMode, getFeeOnMonthDay(), this.feeInterval, this.penalty, this.active, this.enableFreeWithdrawal, this.freeWithdrawalFrequency, this.restartFrequency, this.restartFrequencyEnum, this.enablePaymentType, paymentTypeData, this.minCap, this.maxCap, feeFrequencyType, accountData, taxGroupData); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeEnumerations.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeEnumerations.java index a8af2ad6d..44a8d7f5a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeEnumerations.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeEnumerations.java @@ -23,6 +23,7 @@ import org.apache.fineract.portfolio.charge.domain.ChargeAppliesTo; import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType; import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode; import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; +import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; public final class ChargeEnumerations { @@ -191,4 +192,26 @@ public final class ChargeEnumerations { return optionData; } + public static EnumOptionData feeFrequencyType(final int id) { + return feeFrequencyType(PeriodFrequencyType.fromInt(id)); + } + + public static EnumOptionData feeFrequencyType(final PeriodFrequencyType frequencyType) { + EnumOptionData optionData; + switch (frequencyType) { + case DAYS -> optionData = new EnumOptionData(PeriodFrequencyType.DAYS.getValue().longValue(), + PeriodFrequencyType.DAYS.getCode(), "Daily"); + case WEEKS -> optionData = new EnumOptionData(PeriodFrequencyType.WEEKS.getValue().longValue(), + PeriodFrequencyType.WEEKS.getCode(), "Weekly"); + case MONTHS -> optionData = new EnumOptionData(PeriodFrequencyType.MONTHS.getValue().longValue(), + PeriodFrequencyType.MONTHS.getCode(), "Monthly"); + case YEARS -> optionData = new EnumOptionData(PeriodFrequencyType.YEARS.getValue().longValue(), + PeriodFrequencyType.YEARS.getCode(), "Yearly"); + case WHOLE_TERM -> optionData = new EnumOptionData(PeriodFrequencyType.WHOLE_TERM.getValue().longValue(), + PeriodFrequencyType.WHOLE_TERM.getCode(), "Whole term"); + default -> throw new UnsupportedOperationException(frequencyType + " is not supported"); + } + return optionData; + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanChargesApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanChargesApiResource.java index 61e0f522d..2559c8a38 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanChargesApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanChargesApiResource.java @@ -43,7 +43,6 @@ import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.UriInfo; -import org.apache.commons.lang3.StringUtils; import org.apache.fineract.commands.domain.CommandWrapper; import org.apache.fineract.commands.service.CommandWrapperBuilder; import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; @@ -52,6 +51,7 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException; import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; +import org.apache.fineract.infrastructure.core.service.CommandParameterUtil; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.charge.data.ChargeData; import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; @@ -77,6 +77,9 @@ public class LoanChargesApiResource { "amount", "amountPaid", "chargeOptions", "installmentChargeData")); private static final String RESOURCE_NAME_FOR_PERMISSIONS = "LOAN"; + public static final String COMMAND_PAY = "pay"; + public static final String COMMAND_WAIVE = "waive"; + public static final String COMMAND_ADJUSTMENT = "adjustment"; private final PlatformSecurityContext context; private final ChargeReadPlatformService chargeReadPlatformService; @@ -98,10 +101,6 @@ public class LoanChargesApiResource { this.commandsSourceWritePlatformService = commandsSourceWritePlatformService; } - private boolean is(final String commandParam, final String commandValue) { - return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue); - } - @GET @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) @@ -176,8 +175,8 @@ public class LoanChargesApiResource { @QueryParam("command") @Parameter(description = "command") final String commandParam, @Parameter(hidden = true) final String apiRequestBodyAsJson) { - CommandProcessingResult result = null; - if (is(commandParam, "pay")) { + CommandProcessingResult result; + if (CommandParameterUtil.is(commandParam, COMMAND_PAY)) { final CommandWrapper commandRequest = new CommandWrapperBuilder().payLoanCharge(loanId, null).withJson(apiRequestBodyAsJson) .build(); result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); @@ -224,13 +223,16 @@ public class LoanChargesApiResource { @Parameter(hidden = true) final String apiRequestBodyAsJson) { final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson); - CommandProcessingResult result = null; - if (is(commandParam, "waive")) { + CommandProcessingResult result; + if (CommandParameterUtil.is(commandParam, COMMAND_WAIVE)) { final CommandWrapper commandRequest = builder.waiveLoanCharge(loanId, loanChargeId).build(); result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "pay")) { + } else if (CommandParameterUtil.is(commandParam, COMMAND_PAY)) { final CommandWrapper commandRequest = builder.payLoanCharge(loanId, loanChargeId).build(); result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (CommandParameterUtil.is(commandParam, COMMAND_ADJUSTMENT)) { + final CommandWrapper commandRequest = builder.adjustmentForLoanCharge(loanId, loanChargeId).build(); + result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); } else { throw new UnrecognizedQueryParamException("command", commandParam); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanChargesApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanChargesApiResourceSwagger.java index 5f97c02a5..d4eb1ca4f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanChargesApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanChargesApiResourceSwagger.java @@ -39,7 +39,7 @@ final class LoanChargesApiResourceSwagger { private GetLoanChargeTimeType() {} @Schema(example = "1") - public Integer id; + public Long id; @Schema(example = "chargeTimeType.disbursement") public String code; @Schema(example = "Disbursement") @@ -51,7 +51,7 @@ final class LoanChargesApiResourceSwagger { private GetLoanChargeCalculationType() {} @Schema(example = "1") - public Integer id; + public Long id; @Schema(example = "chargeCalculationType.flat") public String code; @Schema(example = "Flat") @@ -77,9 +77,9 @@ final class LoanChargesApiResourceSwagger { } @Schema(example = "1") - public Integer id; + public Long id; @Schema(example = "1") - public Integer chargeId; + public Long chargeId; @Schema(example = "Loan Processing fee") public String name; public GetLoanChargeTimeType chargeTimeType; @@ -121,7 +121,7 @@ final class LoanChargesApiResourceSwagger { private GetLoanChargeTemplateChargeTimeType() {} @Schema(example = "2") - public Integer id; + public Long id; @Schema(example = "chargeTimeType.specifiedDueDate") public String code; @Schema(example = "Specified due date") @@ -133,7 +133,7 @@ final class LoanChargesApiResourceSwagger { private GetLoanChargeTemplateChargeAppliesTo() {} @Schema(example = "1 ") - public Integer id; + public Long id; @Schema(example = "chargeAppliesTo.loan") public String code; @Schema(example = "Loan") @@ -141,7 +141,7 @@ final class LoanChargesApiResourceSwagger { } @Schema(example = "1") - public Integer id; + public Long id; @Schema(example = "Collection fee") public String name; @Schema(example = "true") @@ -173,7 +173,7 @@ final class LoanChargesApiResourceSwagger { private PostLoansLoanIdChargesRequest() {} @Schema(example = "2") - public Integer chargeId; + public Long chargeId; @Schema(example = "en") public String locale; @Schema(example = "100.00") @@ -198,7 +198,7 @@ final class LoanChargesApiResourceSwagger { @Schema(example = "1") public Long loanId; @Schema(example = "31") - public Integer resourceId; + public Long resourceId; } @Schema(description = " PutLoansLoanIdChargesChargeIdRequest") @@ -228,7 +228,7 @@ final class LoanChargesApiResourceSwagger { @Schema(example = "1") public Long loanId; @Schema(example = "6") - public Integer resourceId; + public Long resourceId; public PutLoansLoanIdChargesChargeIdRequest changes; } @@ -243,6 +243,16 @@ final class LoanChargesApiResourceSwagger { public String locale; @Schema(example = "19 September 2013") public String transactionDate; + @Schema(example = "1") + public Long chargeId; + @Schema(example = "19 September 2013") + public String dueDate; + @Schema(example = "1") + public Long installmentNumber; + @Schema(example = "100.00") + public Double amount; + @Schema(example = "786444UUUYYH7") + public String externalId; } @Schema(description = "PostLoansLoanIdChargesChargeIdResponse") @@ -259,7 +269,26 @@ final class LoanChargesApiResourceSwagger { @Schema(example = "1") public Long savingsId; @Schema(example = "12") - public Integer resourceId; + public Long resourceId; + public PostLoansLoanIdChargesChargeIdChanges changes; + + static final class PostLoansLoanIdChargesChargeIdChanges { + + @Schema(example = "en") + public String locale; + @Schema(example = "19 September 2013") + public LocalDate transactionDate; + @Schema(example = "1") + public Long chargeId; + @Schema(example = "19 September 2013") + public LocalDate dueDate; + @Schema(example = "1") + public Long installmentNumber; + @Schema(example = "100.00") + public Double amount; + @Schema(example = "786444UUUYYH7") + public String externalId; + } } @Schema(description = "DeleteLoansLoanIdChargesChargeIdResponse") @@ -274,6 +303,6 @@ final class LoanChargesApiResourceSwagger { @Schema(example = "1") public Long loanId; @Schema(example = "2") - public Integer resourceId; + public Long resourceId; } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java index 8e1fe85c0..839a6d027 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java @@ -39,7 +39,7 @@ final class LoanTransactionsApiResourceSwagger { private GetLoansTransactionType() {} @Schema(example = "2") - public Integer id; + public Long id; @Schema(example = "loanTransactionType.repayment") public String code; @Schema(example = "Repayment") @@ -88,7 +88,7 @@ final class LoanTransactionsApiResourceSwagger { private GetLoansType() {} @Schema(example = "2") - public Integer id; + public Long id; @Schema(example = "loanTransactionType.repayment") public String code; @Schema(example = "Repayment") @@ -137,17 +137,19 @@ final class LoanTransactionsApiResourceSwagger { public Long fromLoanTransaction; @Schema(example = "10") public Long toLoanTransaction; + @Schema(example = "10") + public Long toLoanCharge; @Schema(example = "CHARGEBACK") - private String relationType; + public String relationType; @Schema(example = "100.00") - private Double amount; + public Double amount; @Schema(example = "Repayment Adjustment Chargeback") - private String paymentType; + public String paymentType; } @Schema(example = "3") - public Integer id; + public Long id; public GetLoansType type; @Schema(example = "[2012, 5, 14]") public LocalDate date; @@ -162,6 +164,8 @@ final class LoanTransactionsApiResourceSwagger { public Double interestPortion; @Schema(example = "20120514") public String reversalExternalId; + @Schema(example = "20120514") + public String externalId; @Schema(example = "[2012, 5, 18]") public LocalDate reversedOnDate; @Schema(example = "1000.00") @@ -201,7 +205,7 @@ final class LoanTransactionsApiResourceSwagger { @Schema(example = "3e7791ce-aa10-11ec-b909-0242ac120002") public String externalId; @Schema(example = "3") - public Integer paymentTypeId; + public Long paymentTypeId; @Schema(example = "acc123") public String accountNumber; @Schema(example = "che123") @@ -223,11 +227,11 @@ final class LoanTransactionsApiResourceSwagger { private PostLoansLoanIdTransactionsResponse() {} @Schema(example = "1") - public Integer officeId; + public Long officeId; @Schema(example = "1") - public Integer clientId; + public Long clientId; @Schema(example = "22") - public Integer resourceId; + public Long resourceId; } @Schema(description = "PostLoansLoanIdTransactionsTransactionIdRequest") @@ -248,7 +252,7 @@ final class LoanTransactionsApiResourceSwagger { @Schema(example = "95174ff9-1a75-4d72-a413-6f9b1cb988b7") public String reversalExternalId; @Schema(example = "1") - public Integer paymentTypeId; + public Long paymentTypeId; @Schema(example = "4ff9b1cb988b7") public String externalId; } @@ -259,7 +263,7 @@ final class LoanTransactionsApiResourceSwagger { private PostLoansLoanIdTransactionsTransactionIdResponse() {} @Schema(example = "16") - public Integer resourceId; + public Long resourceId; } @Schema(description = "PutChargeTransactionChangesResponse") @@ -276,9 +280,9 @@ final class LoanTransactionsApiResourceSwagger { } @Schema(example = "1") - public Integer resourceId; + public Long resourceId; @Schema(example = "48") - public Integer loanId; + public Long loanId; public PutChargeTransactionChangesResponse.Changes changes; } @@ -289,8 +293,8 @@ final class LoanTransactionsApiResourceSwagger { private PutChargeTransactionChangesRequest() {} @Schema(example = "1") - public Integer id; + public Long id; @Schema(example = "2") - public Integer loanId; + public Long loanId; } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java index 1049893b2..dd07b6807 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java @@ -267,7 +267,7 @@ final class LoansApiResourceSwagger { public Double totalPaidLate; @Schema(example = "0.00") public Double totalOutstanding; - public Set periods; + public List periods; } static final class GetLoansLoanIdRepaymentPeriod { @@ -635,6 +635,8 @@ final class LoansApiResourceSwagger { public boolean refundForActiveLoans; @Schema(example = "false") public boolean creditBalanceRefund; + @Schema(example = "false") + public boolean chargeAdjustment; } static final class GetLoansLoanIdPaymentDetailData { @@ -964,13 +966,13 @@ final class LoansApiResourceSwagger { public GetLoansLoanIdSummary summary; public GetLoansLoanIdRepaymentSchedule repaymentSchedule; @Schema(description = "Set of GetLoansLoanIdTransactions") - public Set transactions; + public List transactions; @Schema(description = "Set of GetLoansLoanIdDisbursementDetails") public Set disbursementDetails; @Schema(description = "Delinquent data") public GetLoansLoanIdCollectionData delinquent; @Schema(description = "Set of charges") - public Set charges; + public List charges; public GetDelinquencyRangesResponse delinquencyRange; @Schema(example = "false") public Boolean fraud; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanChargeData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanChargeData.java index b22825ad1..9acde0759 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanChargeData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanChargeData.java @@ -21,6 +21,8 @@ package org.apache.fineract.portfolio.loanaccount.data; import java.math.BigDecimal; import java.time.LocalDate; import java.util.Collection; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import org.apache.fineract.infrastructure.core.data.EnumOptionData; import org.apache.fineract.organisation.monetary.data.CurrencyData; @@ -32,6 +34,8 @@ import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; * Immutable data object for loan charge data. */ @Getter +@Builder +@AllArgsConstructor public class LoanChargeData { private final Long id; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanInstallmentChargeData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanInstallmentChargeData.java index e9cd73852..d0c6c2fe9 100755 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanInstallmentChargeData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanInstallmentChargeData.java @@ -20,9 +20,11 @@ package org.apache.fineract.portfolio.loanaccount.data; import java.math.BigDecimal; import java.time.LocalDate; +import lombok.Builder; import lombok.Getter; @Getter +@Builder public class LoanInstallmentChargeData { private final Integer installmentNumber; @@ -32,47 +34,6 @@ public class LoanInstallmentChargeData { private final BigDecimal amountWaived; private final boolean paid; private final boolean waived; - private BigDecimal amountAccrued; private BigDecimal amountUnrecognized; - - public LoanInstallmentChargeData(final Integer installmentNumber, final LocalDate dueDate, final BigDecimal amount, - final BigDecimal amountOutstanding, BigDecimal amountWaived, final boolean paid, final boolean waived) { - this.installmentNumber = installmentNumber; - this.dueDate = dueDate; - this.amount = amount; - this.amountOutstanding = amountOutstanding; - this.paid = paid; - this.waived = waived; - this.amountWaived = amountWaived; - } - - public LoanInstallmentChargeData(final Integer installmentNumber, final LocalDate dueDate, final BigDecimal amount, - final BigDecimal amountOutstanding, final BigDecimal amountWaived, final boolean paid, final boolean waived, - final BigDecimal amountAccrued) { - this.installmentNumber = installmentNumber; - this.dueDate = dueDate; - this.amount = amount; - this.amountOutstanding = amountOutstanding; - this.paid = paid; - this.waived = waived; - this.amountWaived = amountWaived; - this.amountAccrued = amountAccrued; - } - - public LoanInstallmentChargeData(final LoanInstallmentChargeData installmentChargeData, final BigDecimal amountUnrecognized) { - this.installmentNumber = installmentChargeData.installmentNumber; - this.dueDate = installmentChargeData.dueDate; - this.amount = installmentChargeData.amount; - this.amountOutstanding = installmentChargeData.amountOutstanding; - this.paid = installmentChargeData.paid; - this.waived = installmentChargeData.waived; - this.amountWaived = installmentChargeData.amountWaived; - this.amountAccrued = installmentChargeData.amountAccrued; - this.amountUnrecognized = amountUnrecognized; - } - - public boolean isPaymentPending() { - return !(this.paid || this.waived); - } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java index 9ba692ed1..e11dcb6ea 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java @@ -51,7 +51,7 @@ public class LoanTransactionEnumData { private final boolean refund; private final boolean refundForActiveLoans; private final boolean creditBalanceRefund; - + private final boolean chargeAdjustment; private final boolean chargeback; public LoanTransactionEnumData(final Long id, final String code, final String value) { @@ -80,6 +80,7 @@ public class LoanTransactionEnumData { this.refundForActiveLoans = Long.valueOf(18).equals(this.id); this.creditBalanceRefund = Long.valueOf(20).equals(this.id); this.chargeback = Long.valueOf(25).equals(this.id); + this.chargeAdjustment = Long.valueOf(26).equals(this.id); } /** @@ -96,7 +97,8 @@ public class LoanTransactionEnumData { } public boolean isRepaymentType() { - if (isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund()) { + if (isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund() + || isChargeAdjustment()) { return true; } return false; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionRelationData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionRelationData.java index aa848ed72..cba5bbe74 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionRelationData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionRelationData.java @@ -32,6 +32,7 @@ public class LoanTransactionRelationData implements Serializable { private Long fromLoanTransaction; private Long toLoanTransaction; + private Long toLoanCharge; private LoanTransactionRelationTypeEnum relationType; private BigDecimal amount; private String paymentType; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachine.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachine.java index be9a71fcd..5b12b72ba 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachine.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachine.java @@ -18,23 +18,20 @@ */ package org.apache.fineract.portfolio.loanaccount.domain; -import java.util.Arrays; import java.util.List; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanStatusChangedBusinessEvent; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; +import org.springframework.stereotype.Component; // TODO: introduce tests for the state machine +@Component @RequiredArgsConstructor public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachine { - private final List allowedLoanStatuses; + private static final List ALLOWED_LOAN_STATUSES = List.of(LoanStatus.values()); private final BusinessEventNotifierService businessEventNotifierService; - public DefaultLoanLifecycleStateMachine(LoanStatus[] allowedLoanStatuses, BusinessEventNotifierService businessEventNotifierService) { - this(Arrays.asList(allowedLoanStatuses), businessEventNotifierService); - } - @Override public LoanStatus dryTransition(final LoanEvent loanEvent, final Loan loan) { LoanStatus newStatus = getNextStatus(loanEvent, loan); @@ -164,6 +161,11 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi newState = activeTransition(); } break; + case LOAN_CHARGE_ADJUSTMENT: + if (from.hasStateOf(LoanStatus.CLOSED_OBLIGATIONS_MET)) { + newState = overpaidTransition(); + } + break; default: break; } @@ -171,47 +173,47 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi } private LoanStatus transferOnHold() { - return stateOf(LoanStatus.TRANSFER_ON_HOLD, this.allowedLoanStatuses); + return stateOf(LoanStatus.TRANSFER_ON_HOLD, ALLOWED_LOAN_STATUSES); } private LoanStatus transferInProgress() { - return stateOf(LoanStatus.TRANSFER_IN_PROGRESS, this.allowedLoanStatuses); + return stateOf(LoanStatus.TRANSFER_IN_PROGRESS, ALLOWED_LOAN_STATUSES); } private LoanStatus overpaidTransition() { - return stateOf(LoanStatus.OVERPAID, this.allowedLoanStatuses); + return stateOf(LoanStatus.OVERPAID, ALLOWED_LOAN_STATUSES); } private LoanStatus closedRescheduleOutstandingAmountTransition() { - return stateOf(LoanStatus.CLOSED_RESCHEDULE_OUTSTANDING_AMOUNT, this.allowedLoanStatuses); + return stateOf(LoanStatus.CLOSED_RESCHEDULE_OUTSTANDING_AMOUNT, ALLOWED_LOAN_STATUSES); } private LoanStatus closedWrittenOffTransition() { - return stateOf(LoanStatus.CLOSED_WRITTEN_OFF, this.allowedLoanStatuses); + return stateOf(LoanStatus.CLOSED_WRITTEN_OFF, ALLOWED_LOAN_STATUSES); } private LoanStatus closeObligationsMetTransition() { - return stateOf(LoanStatus.CLOSED_OBLIGATIONS_MET, this.allowedLoanStatuses); + return stateOf(LoanStatus.CLOSED_OBLIGATIONS_MET, ALLOWED_LOAN_STATUSES); } private LoanStatus activeTransition() { - return stateOf(LoanStatus.ACTIVE, this.allowedLoanStatuses); + return stateOf(LoanStatus.ACTIVE, ALLOWED_LOAN_STATUSES); } private LoanStatus withdrawnByClientTransition() { - return stateOf(LoanStatus.WITHDRAWN_BY_CLIENT, this.allowedLoanStatuses); + return stateOf(LoanStatus.WITHDRAWN_BY_CLIENT, ALLOWED_LOAN_STATUSES); } private LoanStatus approvedTransition() { - return stateOf(LoanStatus.APPROVED, this.allowedLoanStatuses); + return stateOf(LoanStatus.APPROVED, ALLOWED_LOAN_STATUSES); } private LoanStatus rejectedTransition() { - return stateOf(LoanStatus.REJECTED, this.allowedLoanStatuses); + return stateOf(LoanStatus.REJECTED, ALLOWED_LOAN_STATUSES); } private LoanStatus submittedTransition() { - return stateOf(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL, this.allowedLoanStatuses); + return stateOf(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL, ALLOWED_LOAN_STATUSES); } private LoanStatus stateOf(final LoanStatus state, final List allowedLoanStatuses) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index 6b5779d9f..2e26c00cc 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -3379,7 +3379,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { private boolean doPostLoanTransactionChecks(final LocalDate transactionDate, final LoanLifecycleStateMachine loanLifecycleStateMachine) { boolean statusChanged = false; - if (isOverPaid()) { + boolean isOverpaid = getTotalOverpaid() != null && getTotalOverpaid().compareTo(BigDecimal.ZERO) > 0; + if (isOverpaid) { // FIXME - kw - update account balance to negative amount. handleLoanOverpayment(loanLifecycleStateMachine); statusChanged = true; @@ -3620,7 +3621,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { if (transactionForAdjustment.isNotRepaymentType() && transactionForAdjustment.isNotWaiver() && transactionForAdjustment.isNotCreditBalanceRefund()) { - final String errorMessage = "Only transactions of type repayment, waiver or credit balance refund can be adjusted."; + final String errorMessage = "Only (non-reversed) transactions of type repayment, waiver or credit balance refund can be adjusted."; throw new InvalidLoanTransactionTypeException("transaction", "adjustment.is.only.allowed.to.repayment.or.waiver.or.creditbalancerefund.transactions", errorMessage); } @@ -4935,8 +4936,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { final List dataValidationErrors = new ArrayList<>(); switch (event) { - case LOAN_CREATED: - break; case LOAN_APPROVED: if (!isSubmittedAndPendingApproval()) { final String defaultUserMessage = "Loan Account Approval is not allowed. Loan Account is not in submitted and pending approval state."; @@ -5016,8 +5015,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { dataValidationErrors.add(error); } break; - case REPAID_IN_FULL: - break; case LOAN_CHARGE_PAYMENT: if (!isOpen()) { final String defaultUserMessage = "Charge payment is not allowed. Loan Account is not Active."; @@ -5082,6 +5079,14 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { dataValidationErrors.add(error); } break; + case LOAN_CHARGE_ADJUSTMENT: + if (!(getStatus().isActive() || getStatus().isClosedObligationsMet() || getStatus().isOverpaid())) { + final String defaultUserMessage = "Loan Charge Adjustment is not allowed. Loan Account must be either Active, Fully repaid or Overpaid."; + final ApiParameterError error = ApiParameterError + .generalError("error.msg.loan.charge.adjustment.account.is.not.in.valid.state", defaultUserMessage); + dataValidationErrors.add(error); + } + break; default: break; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainService.java index 7d0491d9f..7c0e52f05 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainService.java @@ -30,7 +30,7 @@ public interface LoanAccountDomainService { LoanTransaction makeRepayment(LoanTransactionType repaymentTransactionType, Loan loan, CommandProcessingResultBuilder builderResult, LocalDate transactionDate, BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, String txnExternalId, - boolean isRecoveryRepayment, String chargeRefundChargeType, boolean isAccountTransfer, HolidayDetailDTO holidatDetailDto, + boolean isRecoveryRepayment, String chargeRefundChargeType, boolean isAccountTransfer, HolidayDetailDTO holidayDetailDto, Boolean isHolidayValidationDone); LoanTransaction makeRefund(Long accountId, CommandProcessingResultBuilder builderResult, LocalDate transactionDate, @@ -76,6 +76,10 @@ public interface LoanAccountDomainService { boolean isRecoveryRepayment, String chargeRefundChargeType, boolean isAccountTransfer, HolidayDetailDTO holidayDetailDto, Boolean isHolidayValidationDone, boolean isLoanToLoanTransfer); + void saveLoanTransactionWithDataIntegrityViolationChecks(LoanTransaction newRepaymentTransaction); + + void saveAndFlushLoanWithDataIntegrityViolationChecks(Loan loan); + void saveLoanWithDataIntegrityViolationChecks(Loan loan); Map foreCloseLoan(Loan loan, LocalDate foreClourseDate, String noteText); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java index 54d1989e8..996bd00a4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java @@ -123,6 +123,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { private final PostDatedChecksRepository postDatedChecksRepository; private final LoanCollateralManagementRepository loanCollateralManagementRepository; private final DelinquencyWritePlatformService delinquencyWritePlatformService; + private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine; @Transactional @Override @@ -193,7 +194,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { holidayDetailDto); final ChangedTransactionDetail changedTransactionDetail = loan.makeRepayment(newRepaymentTransaction, - defaultLoanLifecycleStateMachine(), existingTransactionIds, existingReversedTransactionIds, isRecoveryRepayment, + defaultLoanLifecycleStateMachine, existingTransactionIds, existingReversedTransactionIds, isRecoveryRepayment, scheduleGeneratorDTO, isHolidayValidationDone); saveLoanTransactionWithDataIntegrityViolationChecks(newRepaymentTransaction); @@ -310,38 +311,23 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { return repaymentEvent; } - private void saveLoanTransactionWithDataIntegrityViolationChecks(LoanTransaction newRepaymentTransaction) { + @Override + public void saveLoanTransactionWithDataIntegrityViolationChecks(LoanTransaction newRepaymentTransaction) { try { this.loanTransactionRepository.saveAndFlush(newRepaymentTransaction); } catch (final JpaSystemException | DataIntegrityViolationException e) { - final Throwable realCause = e.getCause(); - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction"); - if (realCause.getMessage().toLowerCase().contains("external_id_unique")) { - baseDataValidator.reset().parameter("externalId").value(newRepaymentTransaction.getExternalId()) - .failWithCode("value.must.be.unique"); - } - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", - dataValidationErrors, e); - } + raiseValidationExceptionForUniqueConstraintViolation(e); + throw e; } } - private void saveAndFlushLoanWithDataIntegrityViolationChecks(final Loan loan) { + @Override + public void saveAndFlushLoanWithDataIntegrityViolationChecks(final Loan loan) { try { this.loanRepositoryWrapper.saveAndFlush(loan); } catch (final JpaSystemException | DataIntegrityViolationException e) { - final Throwable realCause = e.getCause(); - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction"); - if (realCause.getMessage().toLowerCase().contains("external_id_unique")) { - baseDataValidator.reset().parameter("externalId").failWithCode("value.must.be.unique"); - } - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", - dataValidationErrors, e); - } + raiseValidationExceptionForUniqueConstraintViolation(e); + throw e; } } @@ -350,16 +336,22 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { try { this.loanRepositoryWrapper.save(loan); } catch (final JpaSystemException | DataIntegrityViolationException e) { - final Throwable realCause = e.getCause(); - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction"); - if (realCause.getMessage().toLowerCase().contains("external_id_unique")) { - baseDataValidator.reset().parameter("externalId").failWithCode("value.must.be.unique"); - } - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", - dataValidationErrors, e); - } + raiseValidationExceptionForUniqueConstraintViolation(e); + throw e; + } + } + + private void raiseValidationExceptionForUniqueConstraintViolation(Exception e) { + final Throwable realCause = e.getCause(); + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction"); + if (realCause.getMessage().toLowerCase().contains("external_id_unique") || realCause.getMessage() + .contains("duplicate key value violates unique constraint \"m_loan_transaction_external_id_key\"")) { + baseDataValidator.reset().parameter("externalId").failWithCode("value.must.be.unique"); + } + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", + dataValidationErrors, e); } } @@ -393,7 +385,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { HolidayDetailDTO holidayDetailDTO = new HolidayDetailDTO(isHolidayEnabled, holidays, workingDays, allowTransactionsOnHoliday, allowTransactionsOnNonWorkingDay); - loan.makeChargePayment(chargeId, defaultLoanLifecycleStateMachine(), existingTransactionIds, existingReversedTransactionIds, + loan.makeChargePayment(chargeId, defaultLoanLifecycleStateMachine, existingTransactionIds, existingReversedTransactionIds, holidayDetailDTO, newPaymentTransaction, installmentNumber); } saveLoanTransactionWithDataIntegrityViolationChecks(newPaymentTransaction); @@ -427,10 +419,6 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData); } - private LoanLifecycleStateMachine defaultLoanLifecycleStateMachine() { - return new DefaultLoanLifecycleStateMachine(LoanStatus.values(), businessEventNotifierService); - } - private void checkClientOrGroupActive(final Loan loan) { final Client client = loan.client(); if (client != null) { @@ -466,7 +454,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { final WorkingDays workingDays = this.workingDaysRepository.findOne(); final boolean allowTransactionsOnNonWorkingDay = this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled(); - loan.makeRefund(newRefundTransaction, defaultLoanLifecycleStateMachine(), existingTransactionIds, existingReversedTransactionIds, + loan.makeRefund(newRefundTransaction, defaultLoanLifecycleStateMachine, existingTransactionIds, existingReversedTransactionIds, allowTransactionsOnHoliday, holidays, workingDays, allowTransactionsOnNonWorkingDay); saveLoanTransactionWithDataIntegrityViolationChecks(newRefundTransaction); @@ -659,7 +647,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { final LoanTransaction newCreditBalanceRefundTransaction = LoanTransaction.creditBalanceRefund(loan, loan.getOffice(), refundAmount, transactionDate, externalId); - loan.creditBalanceRefund(newCreditBalanceRefundTransaction, defaultLoanLifecycleStateMachine(), existingTransactionIds, + loan.creditBalanceRefund(newCreditBalanceRefundTransaction, defaultLoanLifecycleStateMachine, existingTransactionIds, existingReversedTransactionIds); this.loanTransactionRepository.saveAndFlush(newCreditBalanceRefundTransaction); @@ -699,7 +687,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { final WorkingDays workingDays = this.workingDaysRepository.findOne(); final boolean allowTransactionsOnNonWorkingDay = this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled(); - loan.makeRefundForActiveLoan(newRefundTransaction, defaultLoanLifecycleStateMachine(), existingTransactionIds, + loan.makeRefundForActiveLoan(newRefundTransaction, defaultLoanLifecycleStateMachine, existingTransactionIds, existingReversedTransactionIds, allowTransactionsOnHoliday, holidays, workingDays, allowTransactionsOnNonWorkingDay); this.loanTransactionRepository.saveAndFlush(newRefundTransaction); @@ -782,7 +770,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { List transactionIds = new ArrayList<>(); final ChangedTransactionDetail changedTransactionDetail = loan.handleForeClosureTransactions(payment, - defaultLoanLifecycleStateMachine(), scheduleGeneratorDTO); + defaultLoanLifecycleStateMachine, scheduleGeneratorDTO); /*** * TODO Vishwas Batch save is giving me a HibernateOptimisticLockingFailureException, looping and saving for the diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java index 92f2b0d70..545a74cc9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java @@ -30,6 +30,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; @@ -41,6 +42,7 @@ import javax.persistence.OneToOne; import javax.persistence.Table; import javax.persistence.UniqueConstraint; import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.EnumOptionData; import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; @@ -52,7 +54,9 @@ import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode; import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; import org.apache.fineract.portfolio.charge.exception.LoanChargeWithoutMandatoryFieldException; import org.apache.fineract.portfolio.loanaccount.command.LoanChargeCommand; +import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData; import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidDetail; +import org.apache.fineract.portfolio.loanaccount.data.LoanInstallmentChargeData; @Entity @Table(name = "m_loan_charge", uniqueConstraints = { @UniqueConstraint(columnNames = { "external_id" }, name = "external_id") }) @@ -1101,4 +1105,22 @@ public class LoanCharge extends AbstractPersistableCustom { return ChargeTimeType.fromInt(this.chargeTime); } + public LoanChargeData toData() { + EnumOptionData chargeTimeTypeData = new EnumOptionData((long) getChargeTimeType().ordinal(), getChargeTimeType().getCode(), + String.valueOf(getChargeTimeType().getValue())); + EnumOptionData chargeCalculationTypeData = new EnumOptionData((long) getChargeCalculation().ordinal(), + getChargeCalculation().getCode(), String.valueOf(getChargeCalculation().getValue())); + EnumOptionData chargePaymentModeData = new EnumOptionData((long) getChargePaymentMode().ordinal(), getChargePaymentMode().getCode(), + String.valueOf(getChargePaymentMode().getValue())); + Set loanInstallmentChargeDataSet = installmentCharges().stream().map(LoanInstallmentCharge::toData) + .collect(Collectors.toSet()); + + return LoanChargeData.builder().id(getId()).chargeId(getCharge().getId()).name(getCharge().getName()) + .currency(getCharge().toData().getCurrency()).amount(amount).amountPaid(amountPaid).amountWaived(amountWaived) + .amountWrittenOff(amountWrittenOff).amountOutstanding(amountOutstanding).chargeTimeType(chargeTimeTypeData) + .submittedOnDate(submittedOnDate).dueDate(dueDate).chargeCalculationType(chargeCalculationTypeData).percentage(percentage) + .amountPercentageAppliedTo(amountPercentageAppliedTo).amountOrPercentage(amountOrPercentage).penalty(penaltyCharge) + .chargePaymentMode(chargePaymentModeData).paid(paid).waived(waived).loanId(loan.getId()).minCap(minCap).maxCap(maxCap) + .installmentChargeData(loanInstallmentChargeDataSet).externalId(externalId).build(); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanEvent.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanEvent.java index f814dae82..e171edc5a 100755 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanEvent.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanEvent.java @@ -50,5 +50,6 @@ public enum LoanEvent { LOAN_WITHDRAW_TRANSFER, // LOAN_CREDIT_BALANCE_REFUND, // LOAN_CHARGEBACK, // - LOAN_CHARGE_ADDED; + LOAN_CHARGE_ADDED, // + LOAN_CHARGE_ADJUSTMENT; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInstallmentCharge.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInstallmentCharge.java index 06ad91843..ca1190b6e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInstallmentCharge.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInstallmentCharge.java @@ -27,6 +27,7 @@ import javax.persistence.Table; import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.portfolio.loanaccount.data.LoanInstallmentChargeData; @Entity @Table(name = "m_loan_installment_charge") @@ -322,4 +323,9 @@ public class LoanInstallmentCharge extends AbstractPersistableCustom implements public void setInstallment(LoanRepaymentScheduleInstallment installment) { this.installment = installment; } + + public LoanInstallmentChargeData toData() { + return LoanInstallmentChargeData.builder().installmentNumber(installment.getInstallmentNumber()).dueDate(installment.getDueDate()) + .amount(amount).amountOutstanding(amountOutstanding).amountWaived(amountWaived).paid(paid).waived(waived).build(); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java index 64d2c3992..06bf97d11 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java @@ -172,6 +172,11 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { chargeRefundChargeType); } + public static LoanTransaction chargeAdjustment(final Loan loan, final BigDecimal amount, final LocalDate transactionDate, + final String externalId) { + return new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.CHARGE_ADJUSTMENT, amount, transactionDate, externalId); + } + public void setLoanTransactionToRepaymentScheduleMappings(final Integer installmentId, final BigDecimal chargePerInstallment) { for (LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping : this.loanTransactionToRepaymentScheduleMappings) { final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = loanTransactionToRepaymentScheduleMapping @@ -296,15 +301,12 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { public static boolean transactionAmountsMatch(final MonetaryCurrency currency, final LoanTransaction loanTransaction, final LoanTransaction newLoanTransaction) { - if (loanTransaction.getAmount(currency).isEqualTo(newLoanTransaction.getAmount(currency)) + return loanTransaction.getAmount(currency).isEqualTo(newLoanTransaction.getAmount(currency)) && loanTransaction.getPrincipalPortion(currency).isEqualTo(newLoanTransaction.getPrincipalPortion(currency)) && loanTransaction.getInterestPortion(currency).isEqualTo(newLoanTransaction.getInterestPortion(currency)) && loanTransaction.getFeeChargesPortion(currency).isEqualTo(newLoanTransaction.getFeeChargesPortion(currency)) && loanTransaction.getPenaltyChargesPortion(currency).isEqualTo(newLoanTransaction.getPenaltyChargesPortion(currency)) - && loanTransaction.getOverPaymentPortion(currency).isEqualTo(newLoanTransaction.getOverPaymentPortion(currency))) { - return true; - } - return false; + && loanTransaction.getOverPaymentPortion(currency).isEqualTo(newLoanTransaction.getOverPaymentPortion(currency)); } private LoanTransaction(final Loan loan, final Office office, final Integer typeOf, final LocalDate dateOf, final BigDecimal amount, @@ -526,7 +528,8 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { } public boolean isRepaymentType() { - return isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund(); + return isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund() + || isChargeAdjustment(); } public boolean isRepayment() { @@ -549,6 +552,10 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { return LoanTransactionType.CHARGE_REFUND.equals(getTypeOf()) && isNotReversed(); } + public boolean isChargeAdjustment() { + return LoanTransactionType.CHARGE_ADJUSTMENT.equals(getTypeOf()) && isNotReversed(); + } + public boolean isNotRepaymentType() { return !isRepaymentType(); } @@ -717,6 +724,13 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { thisTransactionData.put("loanChargesPaid", loanChargesPaidData); } + LoanTransactionRelation loanTransactionRelation = loanTransactionRelations.stream() + .filter(e -> LoanTransactionRelationTypeEnum.CHARGE_ADJUSTMENT.equals(e.getRelationType())).findAny().orElse(null); + if (loanTransactionRelation != null) { + LoanCharge loanCharge = loanTransactionRelation.getToCharge(); + thisTransactionData.put("loanChargeData", loanCharge.toData()); + } + return thisTransactionData; } @@ -874,10 +888,22 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { return (loanTransactionRelations != null && loanTransactionRelations.size() > 0); } + public boolean hasChargebackLoanTransactionRelations() { + return (loanTransactionRelations != null + && loanTransactionRelations.stream().anyMatch(e -> LoanTransactionRelationTypeEnum.CHARGEBACK.equals(e.getRelationType()))); + } + public Set getLoanTransactionRelations() { return loanTransactionRelations; } + public void copyLoanTransactionRelations(Set sourceLoanTransactionRelations) { + for (LoanTransactionRelation existingLoanTransactionRelation : sourceLoanTransactionRelations) { + loanTransactionRelations.add(new LoanTransactionRelation(this, existingLoanTransactionRelation.getToTransaction(), + existingLoanTransactionRelation.getToCharge(), existingLoanTransactionRelation.getRelationType())); + } + } + public BigDecimal getAmount() { return amount; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelation.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelation.java index 07cc3a57c..6f57e73a0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelation.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelation.java @@ -23,6 +23,7 @@ import javax.persistence.Entity; import javax.persistence.EnumType; import javax.persistence.Enumerated; import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; import javax.persistence.Table; import javax.persistence.Version; import javax.validation.constraints.NotNull; @@ -36,12 +37,18 @@ import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDa @Table(name = "m_loan_transaction_relation") public class LoanTransactionRelation extends AbstractAuditableWithUTCDateTimeCustom { + @ManyToOne @JoinColumn(name = "from_loan_transaction_id", nullable = false) private LoanTransaction fromTransaction; - @JoinColumn(name = "to_loan_transaction_id", nullable = false) + @ManyToOne + @JoinColumn(name = "to_loan_transaction_id") private LoanTransaction toTransaction; + @ManyToOne + @JoinColumn(name = "to_loan_charge_id") + private LoanCharge toCharge; + @Enumerated(EnumType.ORDINAL) @Column(name = "relation_type_enum", nullable = false) private LoanTransactionRelationTypeEnum relationType; @@ -51,15 +58,21 @@ public class LoanTransactionRelation extends AbstractAuditableWithUTCDateTimeCus protected LoanTransactionRelation() {} - protected LoanTransactionRelation(@NotNull LoanTransaction fromTransaction, @NotNull LoanTransaction toTransaction, + protected LoanTransactionRelation(@NotNull LoanTransaction fromTransaction, LoanTransaction toTransaction, LoanCharge toCharge, LoanTransactionRelationTypeEnum relationType) { this.fromTransaction = fromTransaction; this.toTransaction = toTransaction; + this.toCharge = toCharge; this.relationType = relationType; } - public static LoanTransactionRelation instance(@NotNull LoanTransaction fromTransaction, @NotNull LoanTransaction toTransaction, + public static LoanTransactionRelation linkToTransaction(@NotNull LoanTransaction fromTransaction, + @NotNull LoanTransaction toTransaction, LoanTransactionRelationTypeEnum relation) { + return new LoanTransactionRelation(fromTransaction, toTransaction, null, relation); + } + + public static LoanTransactionRelation linkToCharge(@NotNull LoanTransaction fromTransaction, @NotNull LoanCharge loanCharge, LoanTransactionRelationTypeEnum relation) { - return new LoanTransactionRelation(fromTransaction, toTransaction, relation); + return new LoanTransactionRelation(fromTransaction, null, loanCharge, relation); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelationRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelationRepository.java index 6eeec08ea..7d2afe217 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelationRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelationRepository.java @@ -25,7 +25,6 @@ import org.springframework.data.jpa.repository.JpaSpecificationExecutor; public interface LoanTransactionRelationRepository extends JpaRepository, JpaSpecificationExecutor { - List findByFromTransactionAndRelationType(LoanTransaction loanTransaction, - LoanTransactionRelationTypeEnum relationType); + List findByFromTransaction(LoanTransaction fromTransaction); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelationTypeEnum.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelationTypeEnum.java index d07987798..2cfcedd96 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelationTypeEnum.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelationTypeEnum.java @@ -21,7 +21,8 @@ package org.apache.fineract.portfolio.loanaccount.domain; public enum LoanTransactionRelationTypeEnum { INVALID(0, "loanTransactionType.invalid"), // - CHARGEBACK(1, "loanTransactionRelationType.chargeback"); + CHARGEBACK(1, "loanTransactionRelationType.chargeback"), // + CHARGE_ADJUSTMENT(2, "loanTransactionRelationType.chargeAdjustment"); private final Integer value; private final String code; @@ -47,6 +48,7 @@ public enum LoanTransactionRelationTypeEnum { return switch (transactionType) { case 1 -> LoanTransactionRelationTypeEnum.CHARGEBACK; + case 2 -> LoanTransactionRelationTypeEnum.CHARGE_ADJUSTMENT; default -> LoanTransactionRelationTypeEnum.INVALID; }; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java index 7fee78b88..b510f91d9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java @@ -57,7 +57,8 @@ public enum LoanTransactionType { PAYOUT_REFUND(22, "loanTransactionType.payoutRefund"), // GOODWILL_CREDIT(23, "loanTransactionType.goodwillCredit"), // CHARGE_REFUND(24, "loanTransactionType.chargeRefund"), // - CHARGEBACK(25, "loanTransactionType.chargeback"); + CHARGEBACK(25, "loanTransactionType.chargeback"), // + CHARGE_ADJUSTMENT(26, "loanTransactionType.chargeAdjustment"); private final Integer value; private final String code; @@ -67,14 +68,6 @@ public enum LoanTransactionType { this.code = code; } - public Integer getValue() { - return this.value; - } - - public String getCode() { - return this.code; - } - public static LoanTransactionType fromInt(final Integer transactionType) { if (transactionType == null) { @@ -106,36 +99,45 @@ public enum LoanTransactionType { case 23 -> LoanTransactionType.GOODWILL_CREDIT; case 24 -> LoanTransactionType.CHARGE_REFUND; case 25 -> LoanTransactionType.CHARGEBACK; + case 26 -> LoanTransactionType.CHARGE_ADJUSTMENT; default -> LoanTransactionType.INVALID; }; } + public Integer getValue() { + return this.value; + } + + public String getCode() { + return this.code; + } + public boolean isDisbursement() { - return this.value.equals(LoanTransactionType.DISBURSEMENT.getValue()); + return this.equals(LoanTransactionType.DISBURSEMENT); } public boolean isRepaymentAtDisbursement() { - return this.value.equals(LoanTransactionType.REPAYMENT_AT_DISBURSEMENT.getValue()); + return this.equals(LoanTransactionType.REPAYMENT_AT_DISBURSEMENT); } public boolean isRepayment() { - return this.value.equals(LoanTransactionType.REPAYMENT.getValue()); + return this.equals(LoanTransactionType.REPAYMENT); } public boolean isMerchantIssuedRefund() { - return this.value.equals(LoanTransactionType.MERCHANT_ISSUED_REFUND.getValue()); + return this.equals(LoanTransactionType.MERCHANT_ISSUED_REFUND); } public boolean isPayoutRefund() { - return this.value.equals(LoanTransactionType.PAYOUT_REFUND.getValue()); + return this.equals(LoanTransactionType.PAYOUT_REFUND); } public boolean isGoodwillCredit() { - return this.value.equals(LoanTransactionType.GOODWILL_CREDIT.getValue()); + return this.equals(LoanTransactionType.GOODWILL_CREDIT); } public boolean isChargeRefund() { - return this.value.equals(LoanTransactionType.CHARGE_REFUND.getValue()); + return this.equals(LoanTransactionType.CHARGE_REFUND); } public boolean isRepaymentType() { @@ -143,39 +145,43 @@ public enum LoanTransactionType { } public boolean isRecoveryRepayment() { - return this.value.equals(LoanTransactionType.RECOVERY_REPAYMENT.getValue()); + return this.equals(LoanTransactionType.RECOVERY_REPAYMENT); } public boolean isWaiveInterest() { - return this.value.equals(LoanTransactionType.WAIVE_INTEREST.getValue()); + return this.equals(LoanTransactionType.WAIVE_INTEREST); } public boolean isWaiveCharges() { - return this.value.equals(LoanTransactionType.WAIVE_CHARGES.getValue()); + return this.equals(LoanTransactionType.WAIVE_CHARGES); } public boolean isAccrual() { - return this.value.equals(LoanTransactionType.ACCRUAL.getValue()); + return this.equals(LoanTransactionType.ACCRUAL); } public boolean isWriteOff() { - return this.value.equals(LoanTransactionType.WRITEOFF.getValue()); + return this.equals(LoanTransactionType.WRITEOFF); } public boolean isChargePayment() { - return this.value.equals(LoanTransactionType.CHARGE_PAYMENT.getValue()); + return this.equals(LoanTransactionType.CHARGE_PAYMENT); } public boolean isRefundForActiveLoan() { - return this.value.equals(LoanTransactionType.REFUND_FOR_ACTIVE_LOAN.getValue()); + return this.equals(LoanTransactionType.REFUND_FOR_ACTIVE_LOAN); } public boolean isIncomePosting() { - return this.value.equals(LoanTransactionType.INCOME_POSTING.getValue()); + return this.equals(LoanTransactionType.INCOME_POSTING); } public boolean isChargeback() { - return this.value.equals(LoanTransactionType.CHARGEBACK.getValue()); + return this.equals(LoanTransactionType.CHARGEBACK); + } + + public boolean isChargeAdjustment() { + return this.equals(LoanTransactionType.CHARGE_ADJUSTMENT); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java index a52a241ca..080a7a09c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java @@ -185,6 +185,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen } else { loanTransaction.reverse(loanTransaction.getExternalId()); loanTransaction.updateExternalId(null); + newLoanTransaction.copyLoanTransactionRelations(loanTransaction.getLoanTransactionRelations()); changedTransactionDetail.getNewTransactionMappings().put(loanTransaction.getId(), newLoanTransaction); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanChargeAdjustmentException.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanChargeAdjustmentException.java new file mode 100644 index 000000000..da5e52918 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanChargeAdjustmentException.java @@ -0,0 +1,29 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class LoanChargeAdjustmentException extends AbstractPlatformDomainRuleException { + + public LoanChargeAdjustmentException(final String errorCode, final String defaultUserMessage, final Object... defaultUserMessageArgs) { + super("error.msg." + errorCode, defaultUserMessage, defaultUserMessageArgs); + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/AddLoanChargeCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/AddLoanChargeCommandHandler.java index 4f02b1724..46c50bb5b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/AddLoanChargeCommandHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/AddLoanChargeCommandHandler.java @@ -24,7 +24,7 @@ import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.infrastructure.DataIntegrityErrorHandler; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService; +import org.apache.fineract.portfolio.loanaccount.service.LoanChargeWritePlatformService; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.orm.jpa.JpaSystemException; import org.springframework.stereotype.Service; @@ -35,7 +35,7 @@ import org.springframework.transaction.annotation.Transactional; @CommandType(entity = "LOANCHARGE", action = "CREATE") public class AddLoanChargeCommandHandler implements NewCommandSourceHandler { - private final LoanWritePlatformService writePlatformService; + private final LoanChargeWritePlatformService writePlatformService; private final DataIntegrityErrorHandler dataIntegrityErrorHandler; @Transactional diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/DeleteLoanChargeCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/DeleteLoanChargeCommandHandler.java index b9b61e805..b758e4d25 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/DeleteLoanChargeCommandHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/DeleteLoanChargeCommandHandler.java @@ -23,7 +23,7 @@ import org.apache.fineract.commands.annotation.CommandType; import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService; +import org.apache.fineract.portfolio.loanaccount.service.LoanChargeWritePlatformService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -32,7 +32,7 @@ import org.springframework.transaction.annotation.Transactional; @CommandType(entity = "LOANCHARGE", action = "DELETE") public class DeleteLoanChargeCommandHandler implements NewCommandSourceHandler { - private final LoanWritePlatformService writePlatformService; + private final LoanChargeWritePlatformService writePlatformService; @Transactional @Override diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanChargeAdjustmentCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanChargeAdjustmentCommandHandler.java new file mode 100644 index 000000000..67769a842 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanChargeAdjustmentCommandHandler.java @@ -0,0 +1,42 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.handler; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.loanaccount.service.LoanChargeWritePlatformService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@CommandType(entity = "LOANCHARGE", action = "ADJUSTMENT") +public class LoanChargeAdjustmentCommandHandler implements NewCommandSourceHandler { + + private final LoanChargeWritePlatformService writePlatformService; + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + return writePlatformService.adjustmentForLoanCharge(command.getLoanId(), command.entityId(), command); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanChargeRefundCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanChargeRefundCommandHandler.java index 32b86b2b1..245cf2473 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanChargeRefundCommandHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanChargeRefundCommandHandler.java @@ -24,7 +24,7 @@ import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.infrastructure.DataIntegrityErrorHandler; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService; +import org.apache.fineract.portfolio.loanaccount.service.LoanChargeWritePlatformService; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.orm.jpa.JpaSystemException; import org.springframework.stereotype.Service; @@ -35,7 +35,7 @@ import org.springframework.transaction.annotation.Transactional; @CommandType(entity = "LOAN", action = "CHARGEREFUND") public class LoanChargeRefundCommandHandler implements NewCommandSourceHandler { - private final LoanWritePlatformService writePlatformService; + private final LoanChargeWritePlatformService writePlatformService; private final DataIntegrityErrorHandler dataIntegrityErrorHandler; @Transactional diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/PayLoanChargeCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/PayLoanChargeCommandHandler.java index a893675ad..1d3a25101 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/PayLoanChargeCommandHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/PayLoanChargeCommandHandler.java @@ -23,7 +23,7 @@ import org.apache.fineract.commands.annotation.CommandType; import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService; +import org.apache.fineract.portfolio.loanaccount.service.LoanChargeWritePlatformService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -32,7 +32,7 @@ import org.springframework.transaction.annotation.Transactional; @CommandType(entity = "LOANCHARGE", action = "PAY") public class PayLoanChargeCommandHandler implements NewCommandSourceHandler { - private final LoanWritePlatformService writePlatformService; + private final LoanChargeWritePlatformService writePlatformService; @Transactional @Override diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/UndoLoanChargeWaiveCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/UndoLoanChargeWaiveCommandHandler.java index 6654742d8..9bd0b61da 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/UndoLoanChargeWaiveCommandHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/UndoLoanChargeWaiveCommandHandler.java @@ -23,7 +23,7 @@ import org.apache.fineract.commands.annotation.CommandType; import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService; +import org.apache.fineract.portfolio.loanaccount.service.LoanChargeWritePlatformService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -32,7 +32,7 @@ import org.springframework.transaction.annotation.Transactional; @CommandType(entity = "WAIVECHARGE", action = "UNDO") public class UndoLoanChargeWaiveCommandHandler implements NewCommandSourceHandler { - final LoanWritePlatformService loanWritePlatformService; + final LoanChargeWritePlatformService loanWritePlatformService; @Transactional @Override diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/UpdateLoanChargeCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/UpdateLoanChargeCommandHandler.java index 866ce4af9..7392b95ae 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/UpdateLoanChargeCommandHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/UpdateLoanChargeCommandHandler.java @@ -23,7 +23,7 @@ import org.apache.fineract.commands.annotation.CommandType; import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService; +import org.apache.fineract.portfolio.loanaccount.service.LoanChargeWritePlatformService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -32,7 +32,7 @@ import org.springframework.transaction.annotation.Transactional; @CommandType(entity = "LOANCHARGE", action = "UPDATE") public class UpdateLoanChargeCommandHandler implements NewCommandSourceHandler { - private final LoanWritePlatformService writePlatformService; + private final LoanChargeWritePlatformService writePlatformService; @Transactional @Override diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/WaiveLoanChargeCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/WaiveLoanChargeCommandHandler.java index 2917e139e..1537393f3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/WaiveLoanChargeCommandHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/WaiveLoanChargeCommandHandler.java @@ -23,7 +23,7 @@ import org.apache.fineract.commands.annotation.CommandType; import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService; +import org.apache.fineract.portfolio.loanaccount.service.LoanChargeWritePlatformService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -32,7 +32,7 @@ import org.springframework.transaction.annotation.Transactional; @CommandType(entity = "LOANCHARGE", action = "WAIVE") public class WaiveLoanChargeCommandHandler implements NewCommandSourceHandler { - private final LoanWritePlatformService writePlatformService; + private final LoanChargeWritePlatformService writePlatformService; @Transactional @Override diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/applychargetooverdueloaninstallment/ApplyChargeToOverdueLoanInstallmentConfig.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/applychargetooverdueloaninstallment/ApplyChargeToOverdueLoanInstallmentConfig.java index 925fb7ff9..e5316cd6e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/applychargetooverdueloaninstallment/ApplyChargeToOverdueLoanInstallmentConfig.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/applychargetooverdueloaninstallment/ApplyChargeToOverdueLoanInstallmentConfig.java @@ -20,8 +20,8 @@ package org.apache.fineract.portfolio.loanaccount.jobs.applychargetooverdueloani import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.jobs.service.JobName; +import org.apache.fineract.portfolio.loanaccount.service.LoanChargeWritePlatformService; import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; -import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService; import org.springframework.batch.core.Job; import org.springframework.batch.core.Step; import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; @@ -44,7 +44,7 @@ public class ApplyChargeToOverdueLoanInstallmentConfig { @Autowired private LoanReadPlatformService loanReadPlatformService; @Autowired - private LoanWritePlatformService loanWritePlatformService; + private LoanChargeWritePlatformService loanChargeWritePlatformService; @Bean protected Step applyChargeToOverdueLoanInstallmentStep() { @@ -61,6 +61,6 @@ public class ApplyChargeToOverdueLoanInstallmentConfig { @Bean public ApplyChargeToOverdueLoanInstallmentTasklet applyChargeToOverdueLoanInstallmentTasklet() { return new ApplyChargeToOverdueLoanInstallmentTasklet(configurationDomainService, loanReadPlatformService, - loanWritePlatformService); + loanChargeWritePlatformService); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/applychargetooverdueloaninstallment/ApplyChargeToOverdueLoanInstallmentTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/applychargetooverdueloaninstallment/ApplyChargeToOverdueLoanInstallmentTasklet.java index 16b9bb68f..bce7e33c4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/applychargetooverdueloaninstallment/ApplyChargeToOverdueLoanInstallmentTasklet.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/applychargetooverdueloaninstallment/ApplyChargeToOverdueLoanInstallmentTasklet.java @@ -31,8 +31,8 @@ import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainR import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData; +import org.apache.fineract.portfolio.loanaccount.service.LoanChargeWritePlatformService; import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; -import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService; import org.springframework.batch.core.StepContribution; import org.springframework.batch.core.scope.context.ChunkContext; import org.springframework.batch.core.step.tasklet.Tasklet; @@ -44,7 +44,7 @@ public class ApplyChargeToOverdueLoanInstallmentTasklet implements Tasklet { private final ConfigurationDomainService configurationDomainService; private final LoanReadPlatformService loanReadPlatformService; - private final LoanWritePlatformService loanWritePlatformService; + private final LoanChargeWritePlatformService loanChargeWritePlatformService; @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { @@ -68,7 +68,7 @@ public class ApplyChargeToOverdueLoanInstallmentTasklet implements Tasklet { List exceptions = new ArrayList<>(); for (final Long loanId : overdueScheduleData.keySet()) { try { - loanWritePlatformService.applyOverdueChargesForLoan(loanId, overdueScheduleData.get(loanId)); + loanChargeWritePlatformService.applyOverdueChargesForLoan(loanId, overdueScheduleData.get(loanId)); } catch (final PlatformApiDataValidationException e) { final List errors = e.getErrors(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanTransactionRelationMapper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanTransactionRelationMapper.java index 7ff3f8ae1..93c09effd 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanTransactionRelationMapper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanTransactionRelationMapper.java @@ -30,6 +30,7 @@ public interface LoanTransactionRelationMapper { @Mapping(target = "fromLoanTransaction", source = "source.fromTransaction.id") @Mapping(target = "toLoanTransaction", source = "source.toTransaction.id") + @Mapping(target = "toLoanCharge", source = "source.toCharge.id") @Mapping(target = "amount", source = "source.toTransaction.amount") @Mapping(target = "paymentType", source = "source.toTransaction.paymentDetail.paymentType.name") LoanTransactionRelationData map(LoanTransactionRelation source); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java index 5afc20029..50b2fabf2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java @@ -47,13 +47,10 @@ import org.apache.fineract.portfolio.collateralmanagement.domain.ClientCollatera import org.apache.fineract.portfolio.collateralmanagement.domain.ClientCollateralManagementRepositoryWrapper; import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; import org.apache.fineract.portfolio.loanaccount.domain.Loan; -import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; -import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.exception.InvalidAmountOfCollateralQuantity; import org.apache.fineract.portfolio.loanaccount.exception.InvalidAmountOfCollaterals; import org.apache.fineract.portfolio.loanproduct.LoanProductConstants; -import org.apache.fineract.portfolio.loanproduct.data.TransactionProcessingStrategyData; import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod; import org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod; import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod; @@ -107,7 +104,7 @@ public final class LoanApplicationCommandFromApiJsonHelper { private final FromJsonHelper fromApiJsonHelper; private final CalculateLoanScheduleQueryFromApiJsonHelper apiJsonHelper; private final ClientCollateralManagementRepositoryWrapper clientCollateralManagementRepositoryWrapper; - + private final LoanChargeApiJsonValidator loanChargeApiJsonValidator; private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory; public void validateForCreate(final String json, final boolean isMeetingMandatoryForJLGLoans, final LoanProduct loanProduct) { @@ -391,15 +388,8 @@ public final class LoanApplicationCommandFromApiJsonHelper { .extractStringNamed(LoanApiConstants.transactionProcessingStrategyCodeParameterName, element); baseDataValidator.reset().parameter(LoanApiConstants.transactionProcessingStrategyCodeParameterName) .value(transactionProcessingStrategy).notNull(); - LoanRepaymentScheduleTransactionProcessor processor = loanRepaymentScheduleTransactionProcessorFactory - .determineProcessor(transactionProcessingStrategy); - - if (processor == null) { - Object[] values = loanRepaymentScheduleTransactionProcessorFactory.getStrategies().stream() - .map(TransactionProcessingStrategyData::getCode).toList().toArray(new String[0]); - - baseDataValidator.reset().parameter(LoanApiConstants.transactionProcessingStrategyCodeParameterName).isOneOfTheseValues(values); - } + // Validating whether the processor is existing + loanRepaymentScheduleTransactionProcessorFactory.determineProcessor(transactionProcessingStrategy); if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.linkAccountIdParameterName, element)) { final Long linkAccountId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.linkAccountIdParameterName, element); @@ -640,16 +630,8 @@ public final class LoanApplicationCommandFromApiJsonHelper { .extractStringNamed(LoanApiConstants.transactionProcessingStrategyCodeParameterName, element); baseDataValidator.reset().parameter(LoanApiConstants.transactionProcessingStrategyCodeParameterName) .value(transactionProcessingStrategy).notNull(); - LoanRepaymentScheduleTransactionProcessor processor = loanRepaymentScheduleTransactionProcessorFactory - .determineProcessor(transactionProcessingStrategy); - - if (processor == null) { - Object[] values = loanRepaymentScheduleTransactionProcessorFactory.getStrategies().stream() - .map(TransactionProcessingStrategyData::getCode).toList().toArray(new String[0]); - - baseDataValidator.reset().parameter(LoanApiConstants.transactionProcessingStrategyCodeParameterName) - .isOneOfTheseValues(values); - } + // Validating whether the processor is existing + loanRepaymentScheduleTransactionProcessorFactory.determineProcessor(transactionProcessingStrategy); } BigDecimal principal = null; @@ -1282,46 +1264,10 @@ public final class LoanApplicationCommandFromApiJsonHelper { } - public void validateLoanCharges(final Set charges, final List dataValidationErrors) { - if (charges == null) { - return; - } - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan"); - for (LoanCharge loanCharge : charges) { - String errorcode = null; - switch (loanCharge.getChargeCalculation()) { - case PERCENT_OF_AMOUNT: - if (loanCharge.isInstalmentFee()) { - errorcode = "installment." + LoanApiConstants.LOAN_CHARGE_CAN_NOT_BE_ADDED_WITH_PRINCIPAL_CALCULATION_TYPE; - - } - break; - case PERCENT_OF_AMOUNT_AND_INTEREST: - if (loanCharge.isInstalmentFee()) { - errorcode = "installment." + LoanApiConstants.LOAN_CHARGE_CAN_NOT_BE_ADDED_WITH_PRINCIPAL_CALCULATION_TYPE; - } else if (loanCharge.isSpecifiedDueDate()) { - errorcode = "specific." + LoanApiConstants.LOAN_CHARGE_CAN_NOT_BE_ADDED_WITH_INTEREST_CALCULATION_TYPE; - } - break; - case PERCENT_OF_INTEREST: - if (loanCharge.isSpecifiedDueDate()) { - errorcode = "specific." + LoanApiConstants.LOAN_CHARGE_CAN_NOT_BE_ADDED_WITH_INTEREST_CALCULATION_TYPE; - } - break; - - default: - break; - } - if (errorcode != null) { - baseDataValidator.reset().parameter("charges").failWithCode(errorcode); - } - } - } - public void validateLoanForInterestRecalculation(final Loan loan) { final List dataValidationErrors = new ArrayList<>(); - validateLoanCharges(loan.getActiveCharges(), dataValidationErrors); + loanChargeApiJsonValidator.validateLoanCharges(loan.getActiveCharges(), dataValidationErrors); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanChargeApiJsonValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanChargeApiJsonValidator.java new file mode 100644 index 000000000..d4f470997 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanChargeApiJsonValidator.java @@ -0,0 +1,271 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.serialization; + +import com.google.gson.JsonElement; +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import lombok.AllArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.infrastructure.core.data.ApiParameterError; +import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; +import org.apache.fineract.infrastructure.core.exception.InvalidJsonException; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; +import org.springframework.stereotype.Component; + +@Component +@AllArgsConstructor +public final class LoanChargeApiJsonValidator { + + private final FromJsonHelper fromApiJsonHelper; + + private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", + dataValidationErrors); + } + } + + public void validateAddLoanCharge(final String json) { + if (StringUtils.isBlank(json)) { + throw new InvalidJsonException(); + } + + final Set disbursementParameters = new HashSet<>( + Arrays.asList("chargeId", "amount", "dueDate", "locale", "dateFormat", "externalId")); + + final Type typeOfMap = new TypeToken>() { + + }.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, disbursementParameters); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loanCharge"); + + final JsonElement element = this.fromApiJsonHelper.parse(json); + final Long chargeId = this.fromApiJsonHelper.extractLongNamed("chargeId", element); + baseDataValidator.reset().parameter("chargeId").value(chargeId).notNull().integerGreaterThanZero(); + + final BigDecimal amount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("amount", element); + baseDataValidator.reset().parameter("amount").value(amount).notNull().positiveAmount(); + + if (this.fromApiJsonHelper.parameterExists("dueDate", element)) { + final LocalDate dueDate = this.fromApiJsonHelper.extractLocalDateNamed("dueDate", element); + baseDataValidator.reset().parameter("dueDate").value(dueDate).notBlank(); + } + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + + public void validateUpdateOfLoanCharge(final String json) { + if (StringUtils.isBlank(json)) { + throw new InvalidJsonException(); + } + + final Set disbursementParameters = new HashSet<>(Arrays.asList("amount", "dueDate", "locale", "dateFormat")); + + final Type typeOfMap = new TypeToken>() { + + }.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, disbursementParameters); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loanCharge"); + + final JsonElement element = this.fromApiJsonHelper.parse(json); + + final BigDecimal amount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("amount", element); + baseDataValidator.reset().parameter("amount").value(amount).notNull().positiveAmount(); + + if (this.fromApiJsonHelper.parameterExists("dueDate", element)) { + this.fromApiJsonHelper.extractLocalDateNamed("dueDate", element); + } + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + + public void validateChargePaymentTransaction(final String json, final boolean isChargeIdIncluded) { + + if (StringUtils.isBlank(json)) { + throw new InvalidJsonException(); + } + Set transactionParameters = null; + if (isChargeIdIncluded) { + transactionParameters = new HashSet<>( + Arrays.asList("transactionDate", "locale", "dateFormat", "chargeId", "dueDate", "installmentNumber")); + } else { + transactionParameters = new HashSet<>(Arrays.asList("transactionDate", "locale", "dateFormat", "dueDate", "installmentNumber")); + } + + final Type typeOfMap = new TypeToken>() { + + }.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, transactionParameters); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource("loan.charge.payment.transaction"); + + final JsonElement element = this.fromApiJsonHelper.parse(json); + final LocalDate transactionDate = this.fromApiJsonHelper.extractLocalDateNamed("transactionDate", element); + if (isChargeIdIncluded) { + final Long chargeId = this.fromApiJsonHelper.extractLongNamed("chargeId", element); + baseDataValidator.reset().parameter("chargeId").value(chargeId).notNull().integerGreaterThanZero(); + } + baseDataValidator.reset().parameter("transactionDate").value(transactionDate).notNull(); + final Integer installmentNumber = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("installmentNumber", element); + baseDataValidator.reset().parameter("installmentNumber").value(installmentNumber).ignoreIfNull().integerGreaterThanZero(); + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + + public void validateLoanChargeRefundTransaction(final String json) { + + if (StringUtils.isBlank(json)) { + throw new InvalidJsonException(); + } + final String loanChargeIdParam = "loanChargeId"; + final String installmentNumberParam = "installmentNumber"; + final String dueDateParam = "dueDate"; + Set transactionParameters = new HashSet<>( + Arrays.asList(loanChargeIdParam, dueDateParam, "locale", "dateFormat", installmentNumberParam, // + // remainder below relate to payment part of refund and not validated here + "transactionAmount", "externalId", "note", "locale", "dateFormat", "paymentTypeId", "accountNumber", "checkNumber", + "routingCode", "receiptNumber", "bankNumber")); + + final Type typeOfMap = new TypeToken>() { + + }.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, transactionParameters); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource("loan.charge.refund.transaction"); + + JsonElement element = this.fromApiJsonHelper.parse(json); + + final Long chargeId = this.fromApiJsonHelper.extractLongNamed(loanChargeIdParam, element); + baseDataValidator.reset().parameter(loanChargeIdParam).value(chargeId).notNull().integerGreaterThanZero(); + + final Integer installmentNumber = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(installmentNumberParam, element); + baseDataValidator.reset().parameter(installmentNumberParam).value(installmentNumber).ignoreIfNull().integerGreaterThanZero(); + + final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("transactionAmount", element); + baseDataValidator.reset().parameter("transactionAmount").value(transactionAmount).ignoreIfNull().positiveAmount(); + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + + public void validateLoanAdjustmentRequest(final Long loanId, final Long loanChargeId, final String json) { + + if (StringUtils.isBlank(json)) { + throw new InvalidJsonException(); + } + + Set transactionParameters = new HashSet<>(List.of("amount", "externalId", "locale")); + + final Type typeOfMap = new TypeToken>() { + + }.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, transactionParameters); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource("loan.charge.adjustment.request"); + + JsonElement element = this.fromApiJsonHelper.parse(json); + + final BigDecimal amount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("amount", element); + baseDataValidator.reset().parameter("amount").value(amount).ignoreIfNull().positiveAmount(); + baseDataValidator.reset().parameter("loanId").value(loanId).notNull().positiveAmount(); + baseDataValidator.reset().parameter("loanChargeId").value(loanChargeId).notNull().positiveAmount(); + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + + public void validateInstallmentChargeTransaction(final String json) { + + if (StringUtils.isBlank(json)) { + return; + } + Set transactionParameters = new HashSet<>(Arrays.asList("dueDate", "locale", "dateFormat", "installmentNumber")); + + final Type typeOfMap = new TypeToken>() { + + }.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, transactionParameters); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource("loan.charge.waive.transaction"); + + final JsonElement element = this.fromApiJsonHelper.parse(json); + + final Integer installmentNumber = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("installmentNumber", element); + baseDataValidator.reset().parameter("installmentNumber").value(installmentNumber).ignoreIfNull().integerGreaterThanZero(); + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + + public void validateLoanCharges(final Set charges, final List dataValidationErrors) { + if (charges == null) { + return; + } + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan"); + for (LoanCharge loanCharge : charges) { + String errorcode = null; + switch (loanCharge.getChargeCalculation()) { + case PERCENT_OF_AMOUNT: + if (loanCharge.isInstalmentFee()) { + errorcode = "installment." + LoanApiConstants.LOAN_CHARGE_CAN_NOT_BE_ADDED_WITH_PRINCIPAL_CALCULATION_TYPE; + + } + break; + case PERCENT_OF_AMOUNT_AND_INTEREST: + if (loanCharge.isInstalmentFee()) { + errorcode = "installment." + LoanApiConstants.LOAN_CHARGE_CAN_NOT_BE_ADDED_WITH_PRINCIPAL_CALCULATION_TYPE; + } else if (loanCharge.isSpecifiedDueDate()) { + errorcode = "specific." + LoanApiConstants.LOAN_CHARGE_CAN_NOT_BE_ADDED_WITH_INTEREST_CALCULATION_TYPE; + } + break; + case PERCENT_OF_INTEREST: + if (loanCharge.isSpecifiedDueDate()) { + errorcode = "specific." + LoanApiConstants.LOAN_CHARGE_CAN_NOT_BE_ADDED_WITH_INTEREST_CALCULATION_TYPE; + } + break; + + default: + break; + } + if (errorcode != null) { + baseDataValidator.reset().parameter("charges").failWithCode(errorcode); + } + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java index 01a76dc94..0ff3dddde 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java @@ -338,60 +338,6 @@ public final class LoanEventApiJsonValidator { throwExceptionIfValidationWarningsExist(dataValidationErrors); } - public void validateAddLoanCharge(final String json) { - if (StringUtils.isBlank(json)) { - throw new InvalidJsonException(); - } - - final Set disbursementParameters = new HashSet<>( - Arrays.asList("chargeId", "amount", "dueDate", "locale", "dateFormat", "externalId")); - - final Type typeOfMap = new TypeToken>() {}.getType(); - this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, disbursementParameters); - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loanCharge"); - - final JsonElement element = this.fromApiJsonHelper.parse(json); - final Long chargeId = this.fromApiJsonHelper.extractLongNamed("chargeId", element); - baseDataValidator.reset().parameter("chargeId").value(chargeId).notNull().integerGreaterThanZero(); - - final BigDecimal amount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("amount", element); - baseDataValidator.reset().parameter("amount").value(amount).notNull().positiveAmount(); - - if (this.fromApiJsonHelper.parameterExists("dueDate", element)) { - final LocalDate dueDate = this.fromApiJsonHelper.extractLocalDateNamed("dueDate", element); - baseDataValidator.reset().parameter("dueDate").value(dueDate).notBlank(); - } - - throwExceptionIfValidationWarningsExist(dataValidationErrors); - } - - public void validateUpdateOfLoanCharge(final String json) { - if (StringUtils.isBlank(json)) { - throw new InvalidJsonException(); - } - - final Set disbursementParameters = new HashSet<>(Arrays.asList("amount", "dueDate", "locale", "dateFormat")); - - final Type typeOfMap = new TypeToken>() {}.getType(); - this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, disbursementParameters); - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loanCharge"); - - final JsonElement element = this.fromApiJsonHelper.parse(json); - - final BigDecimal amount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("amount", element); - baseDataValidator.reset().parameter("amount").value(amount).notNull().positiveAmount(); - - if (this.fromApiJsonHelper.parameterExists("dueDate", element)) { - this.fromApiJsonHelper.extractLocalDateNamed("dueDate", element); - } - - throwExceptionIfValidationWarningsExist(dataValidationErrors); - } - public void validateUpdateOfLoanOfficer(final String json) { if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); @@ -450,74 +396,6 @@ public final class LoanEventApiJsonValidator { throwExceptionIfValidationWarningsExist(dataValidationErrors); } - public void validateChargePaymentTransaction(final String json, final boolean isChargeIdIncluded) { - - if (StringUtils.isBlank(json)) { - throw new InvalidJsonException(); - } - Set transactionParameters = null; - if (isChargeIdIncluded) { - transactionParameters = new HashSet<>( - Arrays.asList("transactionDate", "locale", "dateFormat", "chargeId", "dueDate", "installmentNumber")); - } else { - transactionParameters = new HashSet<>(Arrays.asList("transactionDate", "locale", "dateFormat", "dueDate", "installmentNumber")); - } - - final Type typeOfMap = new TypeToken>() {}.getType(); - this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, transactionParameters); - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource("loan.charge.payment.transaction"); - - final JsonElement element = this.fromApiJsonHelper.parse(json); - final LocalDate transactionDate = this.fromApiJsonHelper.extractLocalDateNamed("transactionDate", element); - if (isChargeIdIncluded) { - final Long chargeId = this.fromApiJsonHelper.extractLongNamed("chargeId", element); - baseDataValidator.reset().parameter("chargeId").value(chargeId).notNull().integerGreaterThanZero(); - } - baseDataValidator.reset().parameter("transactionDate").value(transactionDate).notNull(); - final Integer installmentNumber = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("installmentNumber", element); - baseDataValidator.reset().parameter("installmentNumber").value(installmentNumber).ignoreIfNull().integerGreaterThanZero(); - - throwExceptionIfValidationWarningsExist(dataValidationErrors); - } - - public void validateLoanChargeRefundTransaction(final String json) { - - if (StringUtils.isBlank(json)) { - throw new InvalidJsonException(); - } - final String loanChargeIdParam = "loanChargeId"; - final String installmentNumberParam = "installmentNumber"; - final String dueDateParam = "dueDate"; - Set transactionParameters = new HashSet<>( - Arrays.asList(loanChargeIdParam, dueDateParam, "locale", "dateFormat", installmentNumberParam, // - // remainder below relate to payment part of refund and not validated here - "transactionAmount", "externalId", "note", "locale", "dateFormat", "paymentTypeId", "accountNumber", "checkNumber", - "routingCode", "receiptNumber", "bankNumber")); - - final Type typeOfMap = new TypeToken>() {}.getType(); - this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, transactionParameters); - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource("loan.charge.refund.transaction"); - - JsonElement element = this.fromApiJsonHelper.parse(json); - - final Long chargeId = this.fromApiJsonHelper.extractLongNamed(loanChargeIdParam, element); - baseDataValidator.reset().parameter(loanChargeIdParam).value(chargeId).notNull().integerGreaterThanZero(); - - final Integer installmentNumber = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(installmentNumberParam, element); - baseDataValidator.reset().parameter(installmentNumberParam).value(installmentNumber).ignoreIfNull().integerGreaterThanZero(); - - final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("transactionAmount", element); - baseDataValidator.reset().parameter("transactionAmount").value(transactionAmount).ignoreIfNull().positiveAmount(); - - throwExceptionIfValidationWarningsExist(dataValidationErrors); - } - public void validateMarkAsFraudLoan(final String json) { if (StringUtils.isBlank(json)) { return; @@ -539,27 +417,6 @@ public final class LoanEventApiJsonValidator { throwExceptionIfValidationWarningsExist(dataValidationErrors); } - public void validateInstallmentChargeTransaction(final String json) { - - if (StringUtils.isBlank(json)) { - return; - } - Set transactionParameters = new HashSet<>(Arrays.asList("dueDate", "locale", "dateFormat", "installmentNumber")); - - final Type typeOfMap = new TypeToken>() {}.getType(); - this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, transactionParameters); - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource("loan.charge.waive.transaction"); - - final JsonElement element = this.fromApiJsonHelper.parse(json); - - final Integer installmentNumber = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("installmentNumber", element); - baseDataValidator.reset().parameter("installmentNumber").value(installmentNumber).ignoreIfNull().integerGreaterThanZero(); - throwExceptionIfValidationWarningsExist(dataValidationErrors); - } - public void validateUpdateDisbursementDateAndAmount(final String json, LoanDisbursementDetails loanDisbursementDetails) { if (StringUtils.isBlank(json)) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java index f3153e7d4..8bb8f7396 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java @@ -98,7 +98,6 @@ import org.apache.fineract.portfolio.group.exception.GroupNotActiveException; import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData; import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; -import org.apache.fineract.portfolio.loanaccount.domain.DefaultLoanLifecycleStateMachine; import org.apache.fineract.portfolio.loanaccount.domain.GLIMAccountInfoRepository; import org.apache.fineract.portfolio.loanaccount.domain.GroupLoanIndividualMonitoringAccount; import org.apache.fineract.portfolio.loanaccount.domain.Loan; @@ -192,10 +191,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa private final GLIMAccountInfoRepository glimRepository; private final LoanRepository loanRepository; private final GSIMReadPlatformService gsimReadPlatformService; - - private LoanLifecycleStateMachine defaultLoanLifecycleStateMachine() { - return new DefaultLoanLifecycleStateMachine(LoanStatus.values(), businessEventNotifierService); - } + private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine; @Transactional @Override @@ -1355,7 +1351,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa } final Map changes = loan.loanApplicationApproval(currentUser, command, disbursementDataArray, - defaultLoanLifecycleStateMachine()); + defaultLoanLifecycleStateMachine); entityDatatableChecksWritePlatformService.runTheCheckForProduct(loanId, EntityTables.LOAN.getName(), StatusEnum.APPROVE.getCode().longValue(), EntityTables.LOAN.getForeignKeyColumnNameOnDatatable(), loan.productId()); @@ -1462,7 +1458,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa final Loan loan = retrieveLoanBy(loanId); checkClientOrGroupActive(loan); - final Map changes = loan.undoApproval(defaultLoanLifecycleStateMachine()); + final Map changes = loan.undoApproval(defaultLoanLifecycleStateMachine); if (!changes.isEmpty()) { // If loan approved amount is not same as loan amount demanded, then @@ -1544,7 +1540,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa entityDatatableChecksWritePlatformService.runTheCheckForProduct(loanId, EntityTables.LOAN.getName(), StatusEnum.REJECTED.getCode().longValue(), EntityTables.LOAN.getForeignKeyColumnNameOnDatatable(), loan.productId()); - final Map changes = loan.loanApplicationRejection(currentUser, command, defaultLoanLifecycleStateMachine()); + final Map changes = loan.loanApplicationRejection(currentUser, command, defaultLoanLifecycleStateMachine); if (!changes.isEmpty()) { this.loanRepositoryWrapper.saveAndFlush(loan); @@ -1581,7 +1577,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa StatusEnum.WITHDRAWN.getCode().longValue(), EntityTables.LOAN.getForeignKeyColumnNameOnDatatable(), loan.productId()); final Map changes = loan.loanApplicationWithdrawnByApplicant(currentUser, command, - defaultLoanLifecycleStateMachine()); + defaultLoanLifecycleStateMachine); // Release attached collaterals if (AccountType.fromInt(loan.getLoanType()).isIndividualAccount()) { @@ -1619,7 +1615,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa private Loan retrieveLoanBy(final Long loanId) { final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true); - loan.setHelpers(defaultLoanLifecycleStateMachine(), this.loanSummaryWrapper, this.loanRepaymentScheduleTransactionProcessorFactory); + loan.setHelpers(defaultLoanLifecycleStateMachine, this.loanSummaryWrapper, this.loanRepaymentScheduleTransactionProcessorFactory); return loan; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java index 9635efad6..1e6b3c6a2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java @@ -34,7 +34,6 @@ import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDoma import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.EnumOptionData; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; -import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.organisation.holiday.domain.Holiday; import org.apache.fineract.organisation.holiday.domain.HolidayRepository; import org.apache.fineract.organisation.holiday.domain.HolidayStatusType; @@ -60,7 +59,6 @@ import org.apache.fineract.portfolio.group.exception.ClientNotInGroupException; import org.apache.fineract.portfolio.group.exception.GroupNotActiveException; import org.apache.fineract.portfolio.group.exception.GroupNotFoundException; import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; -import org.apache.fineract.portfolio.loanaccount.domain.DefaultLoanLifecycleStateMachine; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagement; @@ -68,7 +66,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails; import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; -import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; import org.apache.fineract.portfolio.loanaccount.domain.LoanSummaryWrapper; import org.apache.fineract.portfolio.loanaccount.exception.ExceedingTrancheCountException; import org.apache.fineract.portfolio.loanaccount.exception.InvalidAmountOfCollaterals; @@ -109,18 +106,18 @@ public class LoanAssembler { private final WorkingDaysRepositoryWrapper workingDaysRepository; private final LoanUtilService loanUtilService; private final RateAssembler rateAssembler; - private final BusinessEventNotifierService businessEventNotifierService; + private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine; public Loan assembleFrom(final Long accountId) { final Loan loanAccount = this.loanRepository.findOneWithNotFoundDetection(accountId, true); - loanAccount.setHelpers(defaultLoanLifecycleStateMachine(), this.loanSummaryWrapper, + loanAccount.setHelpers(defaultLoanLifecycleStateMachine, this.loanSummaryWrapper, this.loanRepaymentScheduleTransactionProcessorFactory); return loanAccount; } public void setHelpers(final Loan loanAccount) { - loanAccount.setHelpers(defaultLoanLifecycleStateMachine(), this.loanSummaryWrapper, + loanAccount.setHelpers(defaultLoanLifecycleStateMachine, this.loanSummaryWrapper, this.loanRepaymentScheduleTransactionProcessorFactory); } @@ -296,7 +293,7 @@ public class LoanAssembler { if (loanApplication == null) { throw new IllegalStateException("No loan application exists for either a client or group (or both)."); } - loanApplication.setHelpers(defaultLoanLifecycleStateMachine(), this.loanSummaryWrapper, + loanApplication.setHelpers(defaultLoanLifecycleStateMachine, this.loanSummaryWrapper, this.loanRepaymentScheduleTransactionProcessorFactory); if (loanProduct.isMultiDisburseLoan()) { @@ -314,16 +311,12 @@ public class LoanAssembler { final boolean allowTransactionsOnHoliday = this.configurationDomainService.allowTransactionsOnHolidayEnabled(); final LoanScheduleModel loanScheduleModel = this.loanScheduleAssembler.assembleLoanScheduleFrom(loanApplicationTerms, isHolidayEnabled, holidays, workingDays, element, disbursementDetails); - loanApplication.loanApplicationSubmittal(loanScheduleModel, loanApplicationTerms, defaultLoanLifecycleStateMachine(), - submittedOnDate, externalId, allowTransactionsOnHoliday, holidays, workingDays, allowTransactionsOnNonWorkingDay); + loanApplication.loanApplicationSubmittal(loanScheduleModel, loanApplicationTerms, defaultLoanLifecycleStateMachine, submittedOnDate, + externalId, allowTransactionsOnHoliday, holidays, workingDays, allowTransactionsOnNonWorkingDay); return loanApplication; } - private LoanLifecycleStateMachine defaultLoanLifecycleStateMachine() { - return new DefaultLoanLifecycleStateMachine(LoanStatus.values(), businessEventNotifierService); - } - public CodeValue findCodeValueByIdIfProvided(final Long codeValueId) { CodeValue codeValue = null; if (codeValueId != null) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeReadPlatformService.java index e90943e63..7c66cd080 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeReadPlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeReadPlatformService.java @@ -43,6 +43,6 @@ public interface LoanChargeReadPlatformService { Collection retrieveLoanChargesForAccural(Long loanId); - Collection retriveLoanChargesPaidBy(Long chargeId, LoanTransactionType transactionType, + Collection retrieveLoanChargesPaidBy(Long chargeId, LoanTransactionType transactionType, Integer installmentNumber); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeReadPlatformServiceImpl.java index 69725b0d7..518e548ab 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeReadPlatformServiceImpl.java @@ -65,7 +65,7 @@ public class LoanChargeReadPlatformServiceImpl implements LoanChargeReadPlatform + "lc.calculation_percentage as percentageOf, lc.calculation_on_amount as amountPercentageAppliedTo, " + "lc.charge_time_enum as chargeTime, " + "lc.is_penalty as penalty, " + "lc.due_for_collection_as_of_date as dueAsOfDate, " + "lc.charge_calculation_enum as chargeCalculation, " - + "lc.charge_payment_mode_enum as chargePaymentMode, " + "lc.is_paid_derived as paid, " + "lc.waived as waied, " + + "lc.charge_payment_mode_enum as chargePaymentMode, " + "lc.is_paid_derived as paid, " + "lc.waived as waived, " + "lc.min_cap as minCap, lc.max_cap as maxCap, " + "lc.charge_amount_or_percentage as amountOrPercentage, " + "lc.loan_id as loanId, c.currency_code as currencyCode, oc.name as currencyName, " + "date(coalesce(dd.disbursedon_date,dd.expected_disburse_date)) as disbursementDate, " @@ -112,7 +112,7 @@ public class LoanChargeReadPlatformServiceImpl implements LoanChargeReadPlatform final int chargePaymentMode = rs.getInt("chargePaymentMode"); final EnumOptionData paymentMode = ChargeEnumerations.chargePaymentMode(chargePaymentMode); final boolean paid = rs.getBoolean("paid"); - final boolean waived = rs.getBoolean("waied"); + final boolean waived = rs.getBoolean("waived"); final BigDecimal minCap = rs.getBigDecimal("minCap"); final BigDecimal maxCap = rs.getBigDecimal("maxCap"); final BigDecimal amountOrPercentage = rs.getBigDecimal("amountOrPercentage"); @@ -227,7 +227,7 @@ public class LoanChargeReadPlatformServiceImpl implements LoanChargeReadPlatform public String schema() { return " lsi.installment as installmentNumber, lsi.duedate as dueAsOfDate, " + "lic.amount_outstanding_derived as amountOutstanding," + "lic.amount as amount, " + "lic.is_paid_derived as paid, " - + "lic.amount_waived_derived as amountWaived, " + "lic.waived as waied " + "from m_loan_installment_charge lic " + + "lic.amount_waived_derived as amountWaived, " + "lic.waived as waived " + "from m_loan_installment_charge lic " + "join m_loan_repayment_schedule lsi on lsi.id = lic.loan_schedule_id "; } @@ -239,9 +239,10 @@ public class LoanChargeReadPlatformServiceImpl implements LoanChargeReadPlatform final BigDecimal amount = rs.getBigDecimal("amount"); final BigDecimal amountWaived = rs.getBigDecimal("amountWaived"); final boolean paid = rs.getBoolean("paid"); - final boolean waived = rs.getBoolean("waied"); + final boolean waived = rs.getBoolean("waived"); - return new LoanInstallmentChargeData(installmentNumber, dueAsOfDate, amount, amountOutstanding, amountWaived, paid, waived); + return LoanInstallmentChargeData.builder().installmentNumber(installmentNumber).dueDate(dueAsOfDate).amount(amount) + .amountOutstanding(amountOutstanding).amountWaived(amountWaived).paid(paid).waived(waived).build(); } } @@ -419,7 +420,7 @@ public class LoanChargeReadPlatformServiceImpl implements LoanChargeReadPlatform sb.append("lic.is_paid_derived as paid, "); sb.append("lic.amount_waived_derived as amountWaived, "); sb.append(" sum(cp.amount) as amountAccrued, "); - sb.append("lic.waived as waied "); + sb.append("lic.waived as waived "); sb.append("from m_loan_installment_charge lic "); sb.append("join m_loan_repayment_schedule lsi on lsi.id = lic.loan_schedule_id "); sb.append("left join ("); @@ -443,11 +444,12 @@ public class LoanChargeReadPlatformServiceImpl implements LoanChargeReadPlatform final BigDecimal amount = rs.getBigDecimal("amount"); final BigDecimal amountWaived = rs.getBigDecimal("amountWaived"); final boolean paid = rs.getBoolean("paid"); - final boolean waived = rs.getBoolean("waied"); + final boolean waived = rs.getBoolean("waived"); final BigDecimal amountAccrued = rs.getBigDecimal("amountAccrued"); - return new LoanInstallmentChargeData(installmentNumber, dueAsOfDate, amount, amountOutstanding, amountWaived, paid, waived, - amountAccrued); + return LoanInstallmentChargeData.builder().installmentNumber(installmentNumber).dueDate(dueAsOfDate).amount(amount) + .amountOutstanding(amountOutstanding).amountWaived(amountWaived).paid(paid).waived(waived).amountAccrued(amountAccrued) + .build(); } } @@ -483,12 +485,16 @@ public class LoanChargeReadPlatformServiceImpl implements LoanChargeReadPlatform final Integer installmentNumber = rs.getInt("installmentNumber"); final BigDecimal amountUnrecognized = rs.getBigDecimal("amountUnrecognized"); LoanInstallmentChargeData installmentChargeData = this.installmentChargeDatas.get(installmentNumber); - return new LoanInstallmentChargeData(installmentChargeData, amountUnrecognized); + return LoanInstallmentChargeData.builder().installmentNumber(installmentChargeData.getInstallmentNumber()) + .dueDate(installmentChargeData.getDueDate()).amount(installmentChargeData.getAmount()) + .amountOutstanding(installmentChargeData.getAmountOutstanding()).amountWaived(installmentChargeData.getAmountWaived()) + .paid(installmentChargeData.isPaid()).waived(installmentChargeData.isWaived()) + .amountAccrued(installmentChargeData.getAmountAccrued()).amountUnrecognized(amountUnrecognized).build(); } } @Override - public Collection retriveLoanChargesPaidBy(Long chargeId, final LoanTransactionType transactionType, + public Collection retrieveLoanChargesPaidBy(Long chargeId, final LoanTransactionType transactionType, final Integer installmentNumber) { LoanChargesPaidByMapper rm = new LoanChargesPaidByMapper(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformService.java new file mode 100644 index 000000000..49ab0e970 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformService.java @@ -0,0 +1,45 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.service; + +import java.util.Collection; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData; + +public interface LoanChargeWritePlatformService { + + CommandProcessingResult addLoanCharge(Long loanId, JsonCommand command); + + CommandProcessingResult loanChargeRefund(Long loanId, JsonCommand command); + + CommandProcessingResult undoWaiveLoanCharge(JsonCommand command); + + CommandProcessingResult updateLoanCharge(Long loanId, Long loanChargeId, JsonCommand command); + + CommandProcessingResult waiveLoanCharge(Long loanId, Long loanChargeId, JsonCommand command); + + CommandProcessingResult deleteLoanCharge(Long loanId, Long loanChargeId, JsonCommand command); + + CommandProcessingResult payLoanCharge(Long loanId, Long loanChargeId, JsonCommand command, boolean isChargeIdIncludedInJson); + + CommandProcessingResult adjustmentForLoanCharge(Long loanId, Long loanChargeId, JsonCommand command); + + void applyOverdueChargesForLoan(Long loanId, Collection overdueLoanScheduleDataList); +} 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 new file mode 100644 index 000000000..a99e8750a --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java @@ -0,0 +1,1245 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.service; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService; +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.ApiParameterError; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.event.business.domain.loan.LoanApplyOverdueChargeBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBalanceChangedBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.loan.charge.LoanAddChargeBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.loan.charge.LoanDeleteChargeBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.loan.charge.LoanUpdateChargeBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.loan.charge.LoanWaiveChargeBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.loan.charge.LoanWaiveChargeUndoBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeAdjustmentPostBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeAdjustmentPreBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeRefundBusinessEvent; +import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.portfolio.account.PortfolioAccountType; +import org.apache.fineract.portfolio.account.data.AccountTransferDTO; +import org.apache.fineract.portfolio.account.data.PortfolioAccountData; +import org.apache.fineract.portfolio.account.domain.AccountTransferType; +import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformService; +import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService; +import org.apache.fineract.portfolio.accountdetails.domain.AccountType; +import org.apache.fineract.portfolio.charge.domain.Charge; +import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper; +import org.apache.fineract.portfolio.charge.exception.ChargeCannotBeAppliedToException; +import org.apache.fineract.portfolio.charge.exception.ChargeCannotBeUpdatedException; +import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBeAddedException; +import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBeDeletedException; +import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBePayedException; +import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBeUpdatedException; +import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBeWaivedException; +import org.apache.fineract.portfolio.charge.exception.LoanChargeNotFoundException; +import org.apache.fineract.portfolio.charge.exception.LoanChargeWaiveCannotBeReversedException; +import org.apache.fineract.portfolio.client.domain.Client; +import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; +import org.apache.fineract.portfolio.collateralmanagement.domain.ClientCollateralManagement; +import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; +import org.apache.fineract.portfolio.group.domain.Group; +import org.apache.fineract.portfolio.group.exception.GroupNotActiveException; +import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidByData; +import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; +import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; +import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy; +import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeRepository; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagement; +import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails; +import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent; +import org.apache.fineract.portfolio.loanaccount.domain.LoanInstallmentCharge; +import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalcualtionAdditionalDetails; +import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine; +import org.apache.fineract.portfolio.loanaccount.domain.LoanOverdueInstallmentCharge; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTrancheDisbursementCharge; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.exception.InstallmentNotFoundException; +import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException; +import org.apache.fineract.portfolio.loanaccount.exception.LoanChargeAdjustmentException; +import org.apache.fineract.portfolio.loanaccount.exception.LoanChargeRefundException; +import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.DefaultScheduledDateGenerator; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeApiJsonValidator; +import org.apache.fineract.portfolio.loanproduct.data.LoanOverdueDTO; +import org.apache.fineract.portfolio.loanproduct.exception.InvalidCurrencyException; +import org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException; +import org.apache.fineract.portfolio.savings.domain.SavingsAccount; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Slf4j +@RequiredArgsConstructor +public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatformService { + + private static final String AMOUNT = "amount"; + private final LoanChargeApiJsonValidator loanChargeApiJsonValidator; + private final LoanAssembler loanAssembler; + private final ChargeRepositoryWrapper chargeRepository; + private final BusinessEventNotifierService businessEventNotifierService; + private final LoanTransactionRepository loanTransactionRepository; + private final AccountTransfersWritePlatformService accountTransfersWritePlatformService; + private final LoanRepositoryWrapper loanRepositoryWrapper; + private final JournalEntryWritePlatformService journalEntryWritePlatformService; + private final LoanAccountDomainService loanAccountDomainService; + private final LoanChargeRepository loanChargeRepository; + private final LoanWritePlatformService loanWritePlatformService; + private final LoanUtilService loanUtilService; + private final LoanChargeReadPlatformService loanChargeReadPlatformService; + private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine; + private final AccountAssociationsReadPlatformService accountAssociationsReadPlatformService; + private final FromJsonHelper fromApiJsonHelper; + private final ConfigurationDomainService configurationDomainService; + private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory; + + private static boolean isPartOfThisInstallment(LoanCharge loanCharge, LoanRepaymentScheduleInstallment e) { + return e.getFromDate().isBefore(loanCharge.getDueDate()) && !loanCharge.getDueDate().isAfter(e.getDueDate()); + } + + @Transactional + @Override + public CommandProcessingResult addLoanCharge(final Long loanId, final JsonCommand command) { + + this.loanChargeApiJsonValidator.validateAddLoanCharge(command.json()); + + final Loan loan = this.loanAssembler.assembleFrom(loanId); + checkClientOrGroupActive(loan); + + List loanDisburseDetails = loan.getDisbursementDetails(); + final Long chargeDefinitionId = command.longValueOfParameterNamed("chargeId"); + final Charge chargeDefinition = this.chargeRepository.findOneWithNotFoundDetection(chargeDefinitionId); + + if (loan.isDisbursed() && chargeDefinition.isDisbursementCharge()) { + // validates whether any pending disbursements are available to + // apply this charge + validateAddingNewChargeAllowed(loanDisburseDetails); + } + final List existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds()); + final List existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds()); + + boolean isAppliedOnBackDate = false; + LoanCharge loanCharge = null; + LocalDate recalculateFrom = loan.fetchInterestRecalculateFromDate(); + if (chargeDefinition.isPercentageOfDisbursementAmount()) { + LoanTrancheDisbursementCharge loanTrancheDisbursementCharge; + for (LoanDisbursementDetails disbursementDetail : loanDisburseDetails) { + if (disbursementDetail.actualDisbursementDate() == null) { + loanCharge = LoanCharge.createNewWithoutLoan(chargeDefinition, disbursementDetail.principal(), null, null, null, + disbursementDetail.expectedDisbursementDateAsLocalDate(), null, null); + loanTrancheDisbursementCharge = new LoanTrancheDisbursementCharge(loanCharge, disbursementDetail); + loanCharge.updateLoanTrancheDisbursementCharge(loanTrancheDisbursementCharge); + businessEventNotifierService.notifyPreBusinessEvent(new LoanAddChargeBusinessEvent(loanCharge)); + validateAddLoanCharge(loan, chargeDefinition, loanCharge); + addCharge(loan, chargeDefinition, loanCharge); + isAppliedOnBackDate = true; + if (recalculateFrom.isAfter(disbursementDetail.expectedDisbursementDateAsLocalDate())) { + recalculateFrom = disbursementDetail.expectedDisbursementDateAsLocalDate(); + } + } + } + if (loanCharge == null) { + final String errorMessage = "Charge with identifier " + chargeDefinition.getId() + + " cannot be applied: No valid loan disbursement available"; + throw new ChargeCannotBeAppliedToException("loan", errorMessage, chargeDefinition.getId()); + } + loan.addTrancheLoanCharge(chargeDefinition); + } else { + loanCharge = LoanCharge.createNewFromJson(loan, chargeDefinition, command); + businessEventNotifierService.notifyPreBusinessEvent(new LoanAddChargeBusinessEvent(loanCharge)); + + validateAddLoanCharge(loan, chargeDefinition, loanCharge); + isAppliedOnBackDate = addCharge(loan, chargeDefinition, loanCharge); + if (loanCharge.getDueLocalDate() == null || recalculateFrom.isAfter(loanCharge.getDueLocalDate())) { + isAppliedOnBackDate = true; + recalculateFrom = loanCharge.getDueLocalDate(); + } + } + + boolean reprocessRequired = true; + if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { + if (isAppliedOnBackDate && loan.isFeeCompoundingEnabledForInterestRecalculation()) { + + runScheduleRecalculation(loan, recalculateFrom); + reprocessRequired = false; + } + this.loanWritePlatformService.updateOriginalSchedule(loan); + } + if (reprocessRequired) { + ChangedTransactionDetail changedTransactionDetail = loan.reprocessTransactions(); + if (changedTransactionDetail != null) { + for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { + this.loanTransactionRepository.save(mapEntry.getValue()); + // update loan with references to the newly created + // transactions + loan.addLoanTransaction(mapEntry.getValue()); + this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); + } + } + this.loanRepositoryWrapper.save(loan); + } + + postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); + + if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() && isAppliedOnBackDate + && loan.isFeeCompoundingEnabledForInterestRecalculation()) { + this.loanAccountDomainService.recalculateAccruals(loan); + } + this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); + + businessEventNotifierService.notifyPostBusinessEvent(new LoanAddChargeBusinessEvent(loanCharge)); + return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(loanCharge.getId()) + .withOfficeId(loan.getOfficeId()).withClientId(loan.getClientId()).withGroupId(loan.getGroupId()).withLoanId(loanId) + .build(); + } + + @Transactional + @Override + public CommandProcessingResult loanChargeRefund(final Long loanId, final JsonCommand command) { + + this.loanChargeApiJsonValidator.validateLoanChargeRefundTransaction(command.json()); + + final Long loanChargeId = command.longValueOfParameterNamed("loanChargeId"); + final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId); + + final Integer installmentNumber = command.integerValueOfParameterNamed("installmentNumber"); + final LocalDate dueDate = command.localDateValueOfParameterNamed("dueDate"); + final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); + + final LoanInstallmentCharge installmentChargeEntry = loanChargeRefundEntranceValidation(loanCharge, installmentNumber, dueDate); + Integer installmentNumberIdentified = null; + if (installmentChargeEntry != null) { + installmentNumberIdentified = installmentChargeEntry.getRepaymentInstallment().getInstallmentNumber(); + } + final BigDecimal fullRefundAbleAmount = loanChargeValidateRefundAmount(loanCharge, installmentChargeEntry, transactionAmount); + + JsonCommand repaymentJsonCommand = adaptLoanChargeRefundCommandForFurtherRepaymentProcessing(command, fullRefundAbleAmount); + + boolean isRecoveryRepayment = false; + String chargeRefundChargeType = "F"; + if (loanCharge.isPenaltyCharge()) { + chargeRefundChargeType = "P"; + } + // chargeRefundChargeType only included as a parameter for accounting reason - in order to identify whether fee + // or penalty GL account is relevant + CommandProcessingResult result = loanWritePlatformService.makeLoanRepaymentWithChargeRefundChargeType( + LoanTransactionType.CHARGE_REFUND, repaymentJsonCommand.getLoanId(), repaymentJsonCommand, isRecoveryRepayment, + chargeRefundChargeType); + + Long loanChargeRefundTransactionId = result.getResourceId(); + LoanTransaction newChargeRefundTxn = null; + for (LoanTransaction chargeRefundTxn : loanCharge.getLoan().getLoanTransactions()) { + if (loanChargeRefundTransactionId.equals(chargeRefundTxn.getId())) { + newChargeRefundTxn = chargeRefundTxn; + final BigDecimal appliedRefundAmount = newChargeRefundTxn.getAmount(loanCharge.getLoan().getCurrency()).getAmount() + .multiply(BigDecimal.valueOf(-1)); + final LoanChargePaidBy loanChargePaidByForChargeRefund = new LoanChargePaidBy(newChargeRefundTxn, loanCharge, + appliedRefundAmount, installmentNumberIdentified); + newChargeRefundTxn.getLoanChargesPaid().add(loanChargePaidByForChargeRefund); + loanCharge.getLoanChargePaidBySet().add(loanChargePaidByForChargeRefund); + break; + } + } + + businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(newChargeRefundTxn.getLoan())); + businessEventNotifierService.notifyPostBusinessEvent(new LoanChargeRefundBusinessEvent(newChargeRefundTxn)); + return result; + } + + @Transactional + @Override + public CommandProcessingResult undoWaiveLoanCharge(final JsonCommand command) { + + LoanTransaction loanTransaction = this.loanTransactionRepository.findByIdAndLoanId(command.entityId(), command.getLoanId()) + .orElseThrow(() -> new LoanTransactionNotFoundException(command.entityId(), command.getLoanId())); + if (!loanTransaction.getTypeOf().getCode().equals(LoanTransactionType.WAIVE_CHARGES.getCode())) { + throw new InvalidLoanTransactionTypeException("transaction", "undo.waive.charge", "Transaction is not a waive charge type."); + } + if (!loanTransaction.isNotReversed()) { + throw new LoanChargeWaiveCannotBeReversedException( + LoanChargeWaiveCannotBeReversedException.LoanChargeWaiveCannotUndoReason.ALREADY_REVERSED, loanTransaction.getId()); + } + + Set loanChargePaidBySet = loanTransaction.getLoanChargesPaid(); + LoanChargePaidBy loanChargePaidBy = loanChargePaidBySet.stream().findFirst().orElseThrow(LoanChargeNotFoundException::new); + final LoanCharge loanCharge = loanChargePaidBy.getLoanCharge(); + // Validate loan charge is not already paid + if (loanCharge.isPaid()) { + throw new LoanChargeWaiveCannotBeReversedException( + LoanChargeWaiveCannotBeReversedException.LoanChargeWaiveCannotUndoReason.ALREADY_PAID, loanCharge.getId()); + } + + final Long loanId = loanTransaction.getLoan().getId(); + final Loan loan = this.loanAssembler.assembleFrom(loanId); + checkClientOrGroupActive(loan); + // Charges may be waived only when the loan associated with them are + // active + if (!loan.getStatus().isActive()) { + throw new LoanChargeWaiveCannotBeReversedException( + LoanChargeWaiveCannotBeReversedException.LoanChargeWaiveCannotUndoReason.LOAN_INACTIVE, loanCharge.getId()); + } + + final Map changes = new LinkedHashMap<>(3); + + businessEventNotifierService.notifyPreBusinessEvent(new LoanWaiveChargeUndoBusinessEvent(loanCharge)); + + undoWaivedCharge(changes, loan, loanTransaction, loanChargePaidBy); + + businessEventNotifierService.notifyPostBusinessEvent(new LoanWaiveChargeUndoBusinessEvent(loanCharge)); + + changes.put("principalPortion", loanTransaction.getPrincipalPortion()); + changes.put("interestPortion", loanTransaction.getInterestPortion(loan.getCurrency())); + changes.put("feeChargesPortion", loanTransaction.getFeeChargesPortion(loan.getCurrency())); + changes.put("penaltyChargesPortion", loanTransaction.getPenaltyChargesPortion(loan.getCurrency())); + changes.put("outstandingLoanBalance", loanTransaction.getOutstandingLoanBalance()); + changes.put("id", loanTransaction.getId()); + changes.put("date", loanTransaction.getTransactionDate()); + + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(loanCharge.getId()) // + .withLoanId(loanId) // + .with(changes).build(); + } + + @Transactional + @Override + public CommandProcessingResult updateLoanCharge(final Long loanId, final Long loanChargeId, final JsonCommand command) { + + this.loanChargeApiJsonValidator.validateUpdateOfLoanCharge(command.json()); + + final Loan loan = this.loanAssembler.assembleFrom(loanId); + checkClientOrGroupActive(loan); + final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId); + + // Charges may be edited only when the loan associated with them are + // yet to be approved (are in submitted and pending status) + if (!loan.getStatus().isSubmittedAndPendingApproval()) { + throw new LoanChargeCannotBeUpdatedException( + LoanChargeCannotBeUpdatedException.LoanChargeCannotBeUpdatedReason.LOAN_NOT_IN_SUBMITTED_AND_PENDING_APPROVAL_STAGE, + loanCharge.getId()); + } + businessEventNotifierService.notifyPreBusinessEvent(new LoanUpdateChargeBusinessEvent(loanCharge)); + + final Map changes = loan.updateLoanCharge(loanCharge, command); + + this.loanRepositoryWrapper.save(loan); + businessEventNotifierService.notifyPostBusinessEvent(new LoanUpdateChargeBusinessEvent(loanCharge)); + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(loanChargeId) // + .withOfficeId(loan.getOfficeId()) // + .withClientId(loan.getClientId()) // + .withGroupId(loan.getGroupId()) // + .withLoanId(loanId) // + .with(changes) // + .build(); + } + + @Transactional + @Override + public CommandProcessingResult waiveLoanCharge(final Long loanId, final Long loanChargeId, final JsonCommand command) { + + final Loan loan = this.loanAssembler.assembleFrom(loanId); + checkClientOrGroupActive(loan); + this.loanChargeApiJsonValidator.validateInstallmentChargeTransaction(command.json()); + final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId); + + // Charges may be waived only when the loan associated with them are + // active + if (!loan.getStatus().isActive()) { + throw new LoanChargeCannotBeWaivedException(LoanChargeCannotBeWaivedException.LoanChargeCannotBeWaivedReason.LOAN_INACTIVE, + loanCharge.getId()); + } + + // validate loan charge is not already paid or waived + if (loanCharge.isWaived()) { + throw new LoanChargeCannotBeWaivedException(LoanChargeCannotBeWaivedException.LoanChargeCannotBeWaivedReason.ALREADY_WAIVED, + loanCharge.getId()); + } else if (loanCharge.isPaid()) { + throw new LoanChargeCannotBeWaivedException(LoanChargeCannotBeWaivedException.LoanChargeCannotBeWaivedReason.ALREADY_PAID, + loanCharge.getId()); + } + businessEventNotifierService.notifyPreBusinessEvent(new LoanWaiveChargeBusinessEvent(loanCharge)); + Integer loanInstallmentNumber = null; + if (loanCharge.isInstalmentFee()) { + LoanInstallmentCharge chargePerInstallment = null; + if (!StringUtils.isBlank(command.json())) { + final LocalDate dueDate = command.localDateValueOfParameterNamed("dueDate"); + final Integer installmentNumber = command.integerValueOfParameterNamed("installmentNumber"); + if (dueDate != null) { + chargePerInstallment = loanCharge.getInstallmentLoanCharge(dueDate); + } else if (installmentNumber != null) { + chargePerInstallment = loanCharge.getInstallmentLoanCharge(installmentNumber); + } + } + if (chargePerInstallment == null) { + chargePerInstallment = loanCharge.getUnpaidInstallmentLoanCharge(); + } + if (chargePerInstallment.isWaived()) { + throw new LoanChargeCannotBePayedException(LoanChargeCannotBePayedException.LoanChargeCannotBePayedReason.ALREADY_WAIVED, + loanCharge.getId()); + } else if (chargePerInstallment.isPaid()) { + throw new LoanChargeCannotBePayedException(LoanChargeCannotBePayedException.LoanChargeCannotBePayedReason.ALREADY_PAID, + loanCharge.getId()); + } + loanInstallmentNumber = chargePerInstallment.getRepaymentInstallment().getInstallmentNumber(); + } + + final Map changes = new LinkedHashMap<>(3); + + final List existingTransactionIds = new ArrayList<>(); + final List existingReversedTransactionIds = new ArrayList<>(); + LocalDate recalculateFrom = null; + ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); + + Money accruedCharge = Money.zero(loan.getCurrency()); + if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) { + Collection chargePaidByCollection = this.loanChargeReadPlatformService + .retrieveLoanChargesPaidBy(loanCharge.getId(), LoanTransactionType.ACCRUAL, loanInstallmentNumber); + for (LoanChargePaidByData chargePaidByData : chargePaidByCollection) { + accruedCharge = accruedCharge.plus(chargePaidByData.getAmount()); + } + } + + final LoanTransaction waiveTransaction = loan.waiveLoanCharge(loanCharge, defaultLoanLifecycleStateMachine, changes, + existingTransactionIds, existingReversedTransactionIds, loanInstallmentNumber, scheduleGeneratorDTO, accruedCharge); + + this.loanTransactionRepository.saveAndFlush(waiveTransaction); + this.loanRepositoryWrapper.save(loan); + + postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); + this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); + + businessEventNotifierService.notifyPostBusinessEvent(new LoanWaiveChargeBusinessEvent(loanCharge)); + + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(loanChargeId) // + .withOfficeId(loan.getOfficeId()) // + .withClientId(loan.getClientId()) // + .withGroupId(loan.getGroupId()) // + .withLoanId(loanId) // + .with(changes) // + .build(); + } + + @Transactional + @Override + public CommandProcessingResult deleteLoanCharge(final Long loanId, final Long loanChargeId, final JsonCommand command) { + + final Loan loan = this.loanAssembler.assembleFrom(loanId); + checkClientOrGroupActive(loan); + final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId); + + // Charges may be deleted only when the loan associated with them are + // yet to be approved (are in submitted and pending status) + if (!loan.getStatus().isSubmittedAndPendingApproval()) { + throw new LoanChargeCannotBeDeletedException( + LoanChargeCannotBeDeletedException.LoanChargeCannotBeDeletedReason.LOAN_NOT_IN_SUBMITTED_AND_PENDING_APPROVAL_STAGE, + loanCharge.getId()); + } + businessEventNotifierService.notifyPreBusinessEvent(new LoanDeleteChargeBusinessEvent(loanCharge)); + + loan.removeLoanCharge(loanCharge); + this.loanRepositoryWrapper.save(loan); + businessEventNotifierService.notifyPostBusinessEvent(new LoanDeleteChargeBusinessEvent(loanCharge)); + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(loanChargeId) // + .withOfficeId(loan.getOfficeId()) // + .withClientId(loan.getClientId()) // + .withGroupId(loan.getGroupId()) // + .withLoanId(loanId) // + .build(); + } + + @Transactional + @Override + public CommandProcessingResult payLoanCharge(final Long loanId, Long loanChargeId, final JsonCommand command, + final boolean isChargeIdIncludedInJson) { + + this.loanChargeApiJsonValidator.validateChargePaymentTransaction(command.json(), isChargeIdIncludedInJson); + if (isChargeIdIncludedInJson) { + loanChargeId = command.longValueOfParameterNamed("chargeId"); + } + final Loan loan = this.loanAssembler.assembleFrom(loanId); + checkClientOrGroupActive(loan); + final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId); + + // Charges may be waived only when the loan associated with them are + // active + if (!loan.getStatus().isActive()) { + throw new LoanChargeCannotBePayedException(LoanChargeCannotBePayedException.LoanChargeCannotBePayedReason.LOAN_INACTIVE, + loanCharge.getId()); + } + + // validate loan charge is not already paid or waived + if (loanCharge.isWaived()) { + throw new LoanChargeCannotBePayedException(LoanChargeCannotBePayedException.LoanChargeCannotBePayedReason.ALREADY_WAIVED, + loanCharge.getId()); + } else if (loanCharge.isPaid()) { + throw new LoanChargeCannotBePayedException(LoanChargeCannotBePayedException.LoanChargeCannotBePayedReason.ALREADY_PAID, + loanCharge.getId()); + } + + if (!loanCharge.getChargePaymentMode().isPaymentModeAccountTransfer()) { + throw new LoanChargeCannotBePayedException( + LoanChargeCannotBePayedException.LoanChargeCannotBePayedReason.CHARGE_NOT_ACCOUNT_TRANSFER, loanCharge.getId()); + } + + final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); + + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); + Integer loanInstallmentNumber = null; + BigDecimal amount = loanCharge.amountOutstanding(); + if (loanCharge.isInstalmentFee()) { + LoanInstallmentCharge chargePerInstallment = null; + final LocalDate dueDate = command.localDateValueOfParameterNamed("dueDate"); + final Integer installmentNumber = command.integerValueOfParameterNamed("installmentNumber"); + if (dueDate != null) { + chargePerInstallment = loanCharge.getInstallmentLoanCharge(dueDate); + } else if (installmentNumber != null) { + chargePerInstallment = loanCharge.getInstallmentLoanCharge(installmentNumber); + } + if (chargePerInstallment == null) { + chargePerInstallment = loanCharge.getUnpaidInstallmentLoanCharge(); + } + if (chargePerInstallment.isWaived()) { + throw new LoanChargeCannotBePayedException(LoanChargeCannotBePayedException.LoanChargeCannotBePayedReason.ALREADY_WAIVED, + loanCharge.getId()); + } else if (chargePerInstallment.isPaid()) { + throw new LoanChargeCannotBePayedException(LoanChargeCannotBePayedException.LoanChargeCannotBePayedReason.ALREADY_PAID, + loanCharge.getId()); + } + loanInstallmentNumber = chargePerInstallment.getRepaymentInstallment().getInstallmentNumber(); + amount = chargePerInstallment.getAmountOutstanding(); + } + + final PortfolioAccountData portfolioAccountData = this.accountAssociationsReadPlatformService.retriveLoanLinkedAssociation(loanId); + if (portfolioAccountData == null) { + final String errorMessage = "Charge with id:" + loanChargeId + " requires linked savings account for payment"; + throw new LinkedAccountRequiredException("loanCharge.pay", errorMessage, loanChargeId); + } + final SavingsAccount fromSavingsAccount = null; + final boolean isRegularTransaction = true; + final boolean isExceptionForBalanceCheck = false; + final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, amount, PortfolioAccountType.SAVINGS, + PortfolioAccountType.LOAN, portfolioAccountData.getId(), loanId, "Loan Charge Payment", locale, fmt, null, null, + LoanTransactionType.CHARGE_PAYMENT.getValue(), loanChargeId, loanInstallmentNumber, + AccountTransferType.CHARGE_PAYMENT.getValue(), null, null, null, null, null, fromSavingsAccount, isRegularTransaction, + isExceptionForBalanceCheck); + this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(loanChargeId) // + .withOfficeId(loan.getOfficeId()) // + .withClientId(loan.getClientId()) // + .withGroupId(loan.getGroupId()) // + .withLoanId(loanId) // + .withSavingsId(portfolioAccountData.getId()).build(); + } + + @Transactional + @Override + public CommandProcessingResult adjustmentForLoanCharge(Long loanId, Long loanChargeId, JsonCommand command) { + this.loanChargeApiJsonValidator.validateLoanAdjustmentRequest(loanId, loanChargeId, command.json()); + + final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId); + final LocalDate transactionDate = DateUtils.getBusinessLocalDate(); + final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("amount"); + final String txnExternalId = command.stringValueOfParameterNamedAllowingNull("externalId"); + final String locale = command.locale(); + + loanChargeAdjustmentEntranceValidation(loanCharge, transactionAmount); + final Loan loan = loanAssembler.assembleFrom(loanId); + + final CommandProcessingResultBuilder commandProcessingResultBuilder = new CommandProcessingResultBuilder(); + + LoanTransaction loanTransaction = applyChargeAdjustment(loan, loanCharge, transactionAmount, transactionDate, txnExternalId); + + // Update loan transaction on repayment. + if (AccountType.fromInt(loan.getLoanType()).isIndividualAccount()) { + Set loanCollateralManagements = loan.getLoanCollateralManagements(); + for (LoanCollateralManagement loanCollateralManagement : loanCollateralManagements) { + loanCollateralManagement.setLoanTransactionData(loanTransaction); + ClientCollateralManagement clientCollateralManagement = loanCollateralManagement.getClientCollateralManagement(); + + if (loan.getStatus().isClosed()) { + loanCollateralManagement.setIsReleased(true); + BigDecimal quantity = loanCollateralManagement.getQuantity(); + clientCollateralManagement.updateQuantity(clientCollateralManagement.getQuantity().add(quantity)); + loanCollateralManagement.setClientCollateralManagement(clientCollateralManagement); + } + } + this.loanAccountDomainService.updateLoanCollateralTransaction(loanCollateralManagements); + } + businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); + businessEventNotifierService.notifyPostBusinessEvent(new LoanChargeAdjustmentPostBusinessEvent(loanTransaction)); + Map changes = new HashMap<>(); + changes.put("externalId", txnExternalId); + changes.put("amount", transactionAmount); + changes.put("transactionDate", transactionDate); + changes.put("locale", locale); + return commandProcessingResultBuilder.withCommandId(command.commandId()) // + .withLoanId(loanId) // + .withEntityId(loanTransaction.getId()).with(changes) // + .build(); + } + + private LoanTransaction applyChargeAdjustment(final Loan loan, final LoanCharge loanCharge, final BigDecimal transactionAmount, + final LocalDate transactionDate, final String txnExternalId) { + businessEventNotifierService.notifyPreBusinessEvent(new LoanChargeAdjustmentPreBusinessEvent(loan)); + final List existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds()); + final List existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds()); + + LoanTransaction loanChargeAdjustmentTransaction = LoanTransaction.chargeAdjustment(loan, transactionAmount, transactionDate, + txnExternalId); + LoanTransactionRelation loanTransactionRelation = LoanTransactionRelation.linkToCharge(loanChargeAdjustmentTransaction, loanCharge, + LoanTransactionRelationTypeEnum.CHARGE_ADJUSTMENT); + loanChargeAdjustmentTransaction.getLoanTransactionRelations().add(loanTransactionRelation); + + loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(loanChargeAdjustmentTransaction); + + defaultLoanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, loan); + final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory + .determineProcessor(loan.transactionProcessingStrategy()); + loanRepaymentScheduleTransactionProcessor.handleTransaction(loanChargeAdjustmentTransaction, loan.getCurrency(), + loan.getRepaymentScheduleInstallments(), loan.getActiveCharges()); + + loan.addLoanTransaction(loanChargeAdjustmentTransaction); + loan.updateLoanSummaryAndStatus(); + + loanAccountDomainService.saveAndFlushLoanWithDataIntegrityViolationChecks(loan); + + final Map accountingBridgeData = loan.deriveAccountingBridgeData(loan.getCurrency().getCode(), + existingTransactionIds, existingReversedTransactionIds, false); + this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData); + + loanAccountDomainService.setLoanDelinquencyTag(loan, transactionDate); + + return loanChargeAdjustmentTransaction; + } + + @Transactional + @Override + public void applyOverdueChargesForLoan(final Long loanId, Collection overdueLoanScheduleDataList) { + + Loan loan = this.loanAssembler.assembleFrom(loanId); + final List existingTransactionIds = loan.findExistingTransactionIds(); + final List existingReversedTransactionIds = loan.findExistingReversedTransactionIds(); + boolean runInterestRecalculation = false; + LocalDate recalculateFrom = DateUtils.getBusinessLocalDate(); + LocalDate lastChargeDate = null; + for (final OverdueLoanScheduleData overdueInstallment : overdueLoanScheduleDataList) { + + final JsonElement parsedCommand = this.fromApiJsonHelper.parse(overdueInstallment.toString()); + final JsonCommand command = JsonCommand.from(overdueInstallment.toString(), parsedCommand, this.fromApiJsonHelper, null, null, + null, null, null, loanId, null, null, null, null, null, null, null); + LoanOverdueDTO overdueDTO = applyChargeToOverdueLoanInstallment(loan, overdueInstallment.getChargeId(), + overdueInstallment.getPeriodNumber(), command); + loan = overdueDTO.getLoan(); + runInterestRecalculation = runInterestRecalculation || overdueDTO.isRunInterestRecalculation(); + if (recalculateFrom.isAfter(overdueDTO.getRecalculateFrom())) { + recalculateFrom = overdueDTO.getRecalculateFrom(); + } + if (lastChargeDate == null || overdueDTO.getLastChargeAppliedDate().isAfter(lastChargeDate)) { + lastChargeDate = overdueDTO.getLastChargeAppliedDate(); + } + } + if (loan != null) { + boolean reprocessRequired = true; + LocalDate recalculatedTill = loan.fetchInterestRecalculateFromDate(); + if (recalculateFrom.isAfter(recalculatedTill)) { + recalculateFrom = recalculatedTill; + } + + if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { + if (runInterestRecalculation && loan.isFeeCompoundingEnabledForInterestRecalculation()) { + runScheduleRecalculation(loan, recalculateFrom); + reprocessRequired = false; + } + this.loanWritePlatformService.updateOriginalSchedule(loan); + } + + if (reprocessRequired) { + addInstallmentIfPenaltyAppliedAfterLastDueDate(loan, lastChargeDate); + ChangedTransactionDetail changedTransactionDetail = loan.reprocessTransactions(); + if (changedTransactionDetail != null) { + for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings() + .entrySet()) { + this.loanTransactionRepository.save(mapEntry.getValue()); + // update loan with references to the newly created + // transactions + loan.addLoanTransaction(mapEntry.getValue()); + this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); + } + } + } + + postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); + + if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() && runInterestRecalculation + && loan.isFeeCompoundingEnabledForInterestRecalculation()) { + this.loanAccountDomainService.recalculateAccruals(loan); + } + this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); + businessEventNotifierService.notifyPostBusinessEvent(new LoanApplyOverdueChargeBusinessEvent(loan)); + } + } + + private void undoWaivedCharge(final Map changes, final Loan loan, final LoanTransaction loanTransaction, + final LoanChargePaidBy loanChargePaidBy) { + switch (loanChargePaidBy.getLoanCharge().getChargeTimeType()) { + case SPECIFIED_DUE_DATE -> undoSpecifiedDueDateCharge(changes, loan, loanTransaction, loanChargePaidBy); + case INSTALMENT_FEE -> undoInstalmentFee(changes, loan, loanTransaction, loanChargePaidBy); + default -> throw new UnsupportedOperationException( + "Undo waive charge is not support for this charge: " + loanChargePaidBy.getLoanCharge().getChargeTimeType()); + } + } + + private void undoInstalmentFee(Map changes, Loan loan, LoanTransaction loanTransaction, + LoanChargePaidBy loanChargePaidBy) { + final List existingTransactionIds = loan.findExistingTransactionIds(); + final List existingReversedTransactionIds = loan.findExistingReversedTransactionIds(); + LoanCharge loanCharge = loanChargePaidBy.getLoanCharge(); + final Integer installmentNumber = loanChargePaidBy.getInstallmentNumber(); + LoanInstallmentCharge chargePerInstallment; + // final Integer installmentNumber = command.integerValueOfParameterNamed("installmentNumber"); + if (installmentNumber != null) { + // Get installment charge. + chargePerInstallment = loanCharge.getInstallmentLoanCharge(installmentNumber); + // Get installment amount waived. + BigDecimal amountWaived = chargePerInstallment.getAmountWaived(loan.getCurrency()).getAmount(); + // Check whether the installment charge is not waived. If so throw new error + if (!chargePerInstallment.isWaived() || amountWaived == null) { + throw new LoanChargeWaiveCannotBeReversedException( + LoanChargeWaiveCannotBeReversedException.LoanChargeWaiveCannotUndoReason.NOT_WAIVED, loanCharge.getId()); + } + // Reverse waived transaction + loanTransaction.reverse(); + // Set manually adjusted value to `1` + loanTransaction.setManuallyAdjustedOrReversed(); + // Get loan charge outstanding amount + BigDecimal amountOutstanding = loanCharge.getAmountOutstanding(loan.getCurrency()).getAmount(); + // Add the amount waived to outstanding amount + loanCharge.setOutstandingAmount(amountOutstanding.add(amountWaived)); + // Get loan charge total amount waived + BigDecimal totalAmountWaved = loanCharge.getAmountWaived(loan.getCurrency()).getAmount(); + // Subtract the amount waived from the existing amount waived. + loanCharge.setAmountWaived(totalAmountWaved.subtract(amountWaived)); + // Get installment outstanding amount + BigDecimal amountOutstandingPerInstallment = chargePerInstallment.getAmountOutstanding(); + // Add the amount waived to the outstanding amount of the installment + chargePerInstallment.setOutstandingAmount(amountOutstandingPerInstallment.add(amountWaived)); + // Set the amount waived value to ZERO + chargePerInstallment.setAmountWaived(null); + // Reset waived flag + chargePerInstallment.undoWaiveFlag(); + // Update installment balances + updateRepaymentInstalmentWithWaivedAmount(loanCharge, chargePerInstallment.getInstallment(), amountWaived); + // Update loan charge. + loanCharge.setInstallmentLoanCharge(chargePerInstallment, chargePerInstallment.getInstallment().getInstallmentNumber()); + if (loanCharge.amount().compareTo(loanCharge.amountOutstanding()) == 0 && loanCharge.isWaived()) { + loanCharge.undoWaived(); + } + loan.updateLoanSummaryForUndoWaiveCharge(amountWaived, loanCharge.isPenaltyCharge()); + postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); + changes.put(AMOUNT, amountWaived); + } else { + throw new InstallmentNotFoundException(loanTransaction.getId()); + } + } + + private void undoSpecifiedDueDateCharge(final Map changes, final Loan loan, final LoanTransaction loanTransaction, + final LoanChargePaidBy loanChargePaidBy) { + + final List existingTransactionIds = loan.findExistingTransactionIds(); + final List existingReversedTransactionIds = loan.findExistingReversedTransactionIds(); + LoanCharge loanCharge = loanChargePaidBy.getLoanCharge(); + BigDecimal amountWaived = loanCharge.getAmountWaived(loan.getCurrency()).getAmount(); + if (!loanCharge.isWaived() || amountWaived == null) { + throw new LoanChargeWaiveCannotBeReversedException( + LoanChargeWaiveCannotBeReversedException.LoanChargeWaiveCannotUndoReason.NOT_WAIVED, loanCharge.getId()); + } + loanTransaction.reverse(); + loanTransaction.setManuallyAdjustedOrReversed(); + loanCharge.setOutstandingAmount(loanCharge.amountOutstanding().add(amountWaived)); + loanCharge.setAmountWaived(null); + loanCharge.undoWaived(); + LoanRepaymentScheduleInstallment installment = loan.getRepaymentScheduleInstallments().stream() + .filter(e -> isPartOfThisInstallment(loanCharge, e)).findFirst().orElseThrow(); + updateRepaymentInstalmentWithWaivedAmount(loanCharge, installment, amountWaived); + loan.updateLoanSummaryForUndoWaiveCharge(amountWaived, loanCharge.isPenaltyCharge()); + postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); + changes.put(AMOUNT, amountWaived); + } + + private void updateRepaymentInstalmentWithWaivedAmount(final LoanCharge loanCharge, final LoanRepaymentScheduleInstallment installment, + final BigDecimal amountWaived) { + if (loanCharge.isPenaltyCharge()) { + // Get the penalty charges waived amount per installment + BigDecimal penaltyChargesWaivedAmount = installment.getPenaltyChargesWaived(loanCharge.getLoan().getCurrency()).getAmount(); + // Subtract the amount waived from the existing fee charges waived amount. + installment.setPenaltyChargesWaived(penaltyChargesWaivedAmount.subtract(amountWaived)); + } else { + // Get the fee charges waived amount per installment + BigDecimal feeChargesWaivedAmount = installment.getFeeChargesWaived(loanCharge.getLoan().getCurrency()).getAmount(); + // Subtract the amount waived from the existing fee charges waived amount. + installment.setFeeChargesWaived(feeChargesWaivedAmount.subtract(amountWaived)); + } + } + + private void validateAddingNewChargeAllowed(List loanDisburseDetails) { + boolean pendingDisbursementAvailable = false; + for (LoanDisbursementDetails disbursementDetail : loanDisburseDetails) { + if (disbursementDetail.actualDisbursementDate() == null) { + pendingDisbursementAvailable = true; + break; + } + } + if (!pendingDisbursementAvailable) { + throw new ChargeCannotBeUpdatedException("error.msg.charge.cannot.be.updated.no.pending.disbursements.in.loan", + "This charge cannot be added, No disbursement is pending"); + } + } + + private void validateAddLoanCharge(final Loan loan, final Charge chargeDefinition, final LoanCharge loanCharge) { + if (chargeDefinition.isOverdueInstallment()) { + final String defaultUserMessage = "Installment charge cannot be added to the loan."; + throw new LoanChargeCannotBeAddedException("loanCharge", "overdue.charge", defaultUserMessage, null, + chargeDefinition.getName()); + } else if (loanCharge.getDueLocalDate() != null + && loanCharge.getDueLocalDate().isBefore(loan.getLastUserTransactionForChargeCalc())) { + final String defaultUserMessage = "charge with date before last transaction date can not be added to loan."; + throw new LoanChargeCannotBeAddedException("loanCharge", "date.is.before.last.transaction.date", defaultUserMessage, null, + chargeDefinition.getName()); + } else if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { + + if (loanCharge.isInstalmentFee() && loan.getStatus().isActive()) { + final String defaultUserMessage = "installment charge addition not allowed after disbursement"; + throw new LoanChargeCannotBeAddedException("loanCharge", "installment.charge", defaultUserMessage, null, + chargeDefinition.getName()); + } + final List dataValidationErrors = new ArrayList<>(); + final Set loanCharges = new HashSet<>(1); + loanCharges.add(loanCharge); + this.loanChargeApiJsonValidator.validateLoanCharges(loanCharges, dataValidationErrors); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + } + + private boolean addCharge(final Loan loan, final Charge chargeDefinition, final LoanCharge loanCharge) { + + if (!loan.hasCurrencyCodeOf(chargeDefinition.getCurrencyCode())) { + final String errorMessage = "Charge and Loan must have the same currency."; + throw new InvalidCurrencyException("loanCharge", "attach.to.loan", errorMessage); + } + + if (loanCharge.getChargePaymentMode().isPaymentModeAccountTransfer()) { + final PortfolioAccountData portfolioAccountData = this.accountAssociationsReadPlatformService + .retriveLoanLinkedAssociation(loan.getId()); + if (portfolioAccountData == null) { + final String errorMessage = loanCharge.name() + "Charge requires linked savings account for payment"; + throw new LinkedAccountRequiredException("loanCharge.add", errorMessage, loanCharge.name()); + } + } + + if (!loan.isInterestBearing() && loanCharge.isSpecifiedDueDate() && loanCharge.getDueDate().isAfter(loan.getMaturityDate())) { + LoanRepaymentScheduleInstallment latestRepaymentScheduleInstalment = loan.getRepaymentScheduleInstallments() + .get(loan.getLoanRepaymentScheduleInstallmentsSize() - 1); + if (loanCharge.getDueDate().isAfter(latestRepaymentScheduleInstalment.getDueDate())) { + if (latestRepaymentScheduleInstalment.isAdditional()) { + latestRepaymentScheduleInstalment.updateDueDate(loanCharge.getDueDate()); + } else { + final LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan, + (loan.getLoanRepaymentScheduleInstallmentsSize() + 1), latestRepaymentScheduleInstalment.getDueDate(), + loanCharge.getDueDate(), BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, false, null); + installment.markAsAdditional(); + loan.addLoanRepaymentScheduleInstallment(installment); + } + } + } + + loan.addLoanCharge(loanCharge); + + this.loanChargeRepository.saveAndFlush(loanCharge); + + /** + * we want to apply charge transactions only for those loans charges that are applied when a loan is active and + * the loan product uses Upfront Accruals + **/ + if (loan.getStatus().isActive() && loan.isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct()) { + final LoanTransaction applyLoanChargeTransaction = loan.handleChargeAppliedTransaction(loanCharge, null); + this.loanTransactionRepository.saveAndFlush(applyLoanChargeTransaction); + } + return loanCharge.getDueLocalDate() == null || DateUtils.getBusinessLocalDate().isAfter(loanCharge.getDueLocalDate()); + } + + private LoanOverdueDTO applyChargeToOverdueLoanInstallment(final Loan loan, final Long loanChargeId, final Integer periodNumber, + final JsonCommand command) { + boolean runInterestRecalculation = false; + final Charge chargeDefinition = this.chargeRepository.findOneWithNotFoundDetection(loanChargeId); + + Collection frequencyNumbers = loanChargeReadPlatformService.retrieveOverdueInstallmentChargeFrequencyNumber(loan, + chargeDefinition, periodNumber); + + Integer feeFrequency = chargeDefinition.feeFrequency(); + final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator(); + Map scheduleDates = new HashMap<>(); + final Long penaltyWaitPeriodValue = this.configurationDomainService.retrievePenaltyWaitPeriod(); + final Long penaltyPostingWaitPeriodValue = this.configurationDomainService.retrieveGraceOnPenaltyPostingPeriod(); + final LocalDate dueDate = command.localDateValueOfParameterNamed("dueDate"); + long diff = penaltyWaitPeriodValue + 1 - penaltyPostingWaitPeriodValue; + if (diff < 1) { + diff = 1L; + } + LocalDate startDate = dueDate.plusDays(penaltyWaitPeriodValue + 1L); + int frequencyNumber = 1; + if (feeFrequency == null) { + scheduleDates.put(frequencyNumber++, startDate.minusDays(diff)); + } else { + while (!startDate.isAfter(DateUtils.getBusinessLocalDate())) { + scheduleDates.put(frequencyNumber++, startDate.minusDays(diff)); + + startDate = scheduledDateGenerator.getRepaymentPeriodDate(PeriodFrequencyType.fromInt(feeFrequency), + chargeDefinition.feeInterval(), startDate); + } + } + + for (Integer frequency : frequencyNumbers) { + scheduleDates.remove(frequency); + } + + LoanRepaymentScheduleInstallment installment = null; + LocalDate lastChargeAppliedDate = dueDate; + if (!scheduleDates.isEmpty()) { + installment = loan.fetchRepaymentScheduleInstallment(periodNumber); + lastChargeAppliedDate = installment.getDueDate(); + } + LocalDate recalculateFrom = DateUtils.getBusinessLocalDate(); + + if (loan != null) { + businessEventNotifierService.notifyPreBusinessEvent(new LoanApplyOverdueChargeBusinessEvent(loan)); + for (Map.Entry entry : scheduleDates.entrySet()) { + + final LoanCharge loanCharge = LoanCharge.createNewFromJson(loan, chargeDefinition, command, entry.getValue()); + + if (BigDecimal.ZERO.compareTo(loanCharge.amount()) == 0) { + continue; + } + LoanOverdueInstallmentCharge overdueInstallmentCharge = new LoanOverdueInstallmentCharge(loanCharge, installment, + entry.getKey()); + loanCharge.updateOverdueInstallmentCharge(overdueInstallmentCharge); + + boolean isAppliedOnBackDate = addCharge(loan, chargeDefinition, loanCharge); + runInterestRecalculation = runInterestRecalculation || isAppliedOnBackDate; + if (entry.getValue().isBefore(recalculateFrom)) { + recalculateFrom = entry.getValue(); + } + if (entry.getValue().isAfter(lastChargeAppliedDate)) { + lastChargeAppliedDate = entry.getValue(); + } + } + } + + return new LoanOverdueDTO(loan, runInterestRecalculation, recalculateFrom, lastChargeAppliedDate); + } + + private void addInstallmentIfPenaltyAppliedAfterLastDueDate(Loan loan, LocalDate lastChargeDate) { + if (lastChargeDate != null) { + List installments = loan.getRepaymentScheduleInstallments(); + LoanRepaymentScheduleInstallment lastInstallment = loan.fetchRepaymentScheduleInstallment(installments.size()); + if (lastChargeDate.isAfter(lastInstallment.getDueDate())) { + if (lastInstallment.isRecalculatedInterestComponent()) { + installments.remove(lastInstallment); + lastInstallment = loan.fetchRepaymentScheduleInstallment(installments.size()); + } + boolean recalculatedInterestComponent = true; + BigDecimal principal = BigDecimal.ZERO; + BigDecimal interest = BigDecimal.ZERO; + BigDecimal feeCharges = BigDecimal.ZERO; + BigDecimal penaltyCharges = BigDecimal.ONE; + final Set compoundingDetails = null; + LoanRepaymentScheduleInstallment newEntry = new LoanRepaymentScheduleInstallment(loan, installments.size() + 1, + lastInstallment.getDueDate(), lastChargeDate, principal, interest, feeCharges, penaltyCharges, + recalculatedInterestComponent, compoundingDetails); + loan.addLoanRepaymentScheduleInstallment(newEntry); + } + } + } + + public void runScheduleRecalculation(final Loan loan, final LocalDate recalculateFrom) { + if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { + ScheduleGeneratorDTO generatorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); + ChangedTransactionDetail changedTransactionDetail = loan + .handleRegenerateRepaymentScheduleWithInterestRecalculation(generatorDTO); + this.loanRepositoryWrapper.save(loan); + if (changedTransactionDetail != null) { + for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { + this.loanTransactionRepository.save(mapEntry.getValue()); + // update loan with references to the newly created + // transactions + loan.addLoanTransaction(mapEntry.getValue()); + this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); + } + } + } + } + + private JsonCommand adaptLoanChargeRefundCommandForFurtherRepaymentProcessing(JsonCommand command, BigDecimal fullRefundAbleAmount) { + // creates JsonCommand for onward repayment processing + JsonObject jsonObject = (JsonObject) this.fromApiJsonHelper.parse(command.json()); + + String dateFormat; + if (this.fromApiJsonHelper.parameterExists("dateFormat", jsonObject)) { + dateFormat = this.fromApiJsonHelper.extractStringNamed("dateFormat", jsonObject); + } else { + dateFormat = "dd MMMM yyyy"; + jsonObject.addProperty("dateFormat", dateFormat); + } + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(dateFormat); + LocalDate transactionDate = DateUtils.getLocalDateOfTenant(); + String transactionDateString = transactionDate.format(dateTimeFormatter); + jsonObject.addProperty("transactionDate", transactionDateString); + if (!this.fromApiJsonHelper.parameterExists("transactionAmount", jsonObject)) { + jsonObject.addProperty("transactionAmount", fullRefundAbleAmount.toString()); + } + jsonObject.remove("loanChargeId"); + jsonObject.remove("installmentNumber"); + jsonObject.remove("dueDate"); + + return JsonCommand.fromExistingCommand(command, jsonObject); + } + + private BigDecimal loanChargeValidateRefundAmount(LoanCharge loanCharge, LoanInstallmentCharge installmentChargeEntry, + BigDecimal transactionAmount) { + // if transactionAmount not provided return max refundable amount (amount paid minus previous refunds) + BigDecimal chargeAmountPaid; + BigDecimal chargeAmountRefunded = BigDecimal.ZERO; + MonetaryCurrency loanCurrency = loanCharge.getLoan().getCurrency(); + if (loanCharge.isInstalmentFee()) { + final Integer installmentNumber = installmentChargeEntry.getRepaymentInstallment().getInstallmentNumber(); + chargeAmountPaid = installmentChargeEntry.getAmountPaid(loanCurrency).getAmount(); + for (LoanChargePaidBy loanChargePaidBy : loanCharge.getLoanChargePaidBySet()) { + if (installmentNumber.equals(loanChargePaidBy.getInstallmentNumber()) && isRefundElementOfChargeRefund(loanChargePaidBy)) { + chargeAmountRefunded = chargeAmountRefunded.add(loanChargePaidBy.getAmount()); + } + } + } else { + chargeAmountPaid = loanCharge.getAmountPaid(loanCurrency).getAmount(); + for (LoanChargePaidBy loanChargePaidBy : loanCharge.getLoanChargePaidBySet()) { + if (isRefundElementOfChargeRefund(loanChargePaidBy)) { + chargeAmountRefunded = chargeAmountRefunded.add(loanChargePaidBy.getAmount()); + } + } + } + chargeAmountRefunded = chargeAmountRefunded.multiply(BigDecimal.valueOf(-1)); + + if (chargeAmountRefunded.compareTo(chargeAmountPaid) > 0) { + final String errorMessage = "loan.charge.more.refunded.than.paid.unexpected.system.error"; + final String details = "Paid: " + chargeAmountPaid.toString() + " Refunded: " + chargeAmountPaid; + throw new LoanChargeRefundException(errorMessage, details); + } + + BigDecimal refundableAmount = chargeAmountPaid.subtract(chargeAmountRefunded); + // refund amount was provided. + if (transactionAmount != null && transactionAmount.compareTo(refundableAmount) > 0) { + final String errorMessage = "loan.charge.transaction.amount.is.more.than.is.refundable"; + final String details = "transactionAmount: " + transactionAmount + " Refundable: " + refundableAmount; + throw new LoanChargeRefundException(errorMessage, details); + } + + return refundableAmount; + } + + private void postJournalEntries(final Loan loan, final List existingTransactionIds, + final List existingReversedTransactionIds) { + + final MonetaryCurrency currency = loan.getCurrency(); + boolean isAccountTransfer = false; + final Map accountingBridgeData = loan.deriveAccountingBridgeData(currency.getCode(), existingTransactionIds, + existingReversedTransactionIds, isAccountTransfer); + this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData); + } + + private LoanCharge retrieveLoanChargeBy(final Long loanId, final Long loanChargeId) { + final LoanCharge loanCharge = this.loanChargeRepository.findById(loanChargeId) + .orElseThrow(() -> new LoanChargeNotFoundException(loanChargeId)); + + if (loanCharge.hasNotLoanIdentifiedBy(loanId)) { + throw new LoanChargeNotFoundException(loanChargeId, loanId); + } + return loanCharge; + } + + private boolean isRefundElementOfChargeRefund(LoanChargePaidBy loanChargePaidBy) { + // The Refund Element is always negative + return (loanChargePaidBy.getLoanTransaction().isChargeRefund() && loanChargePaidBy.getAmount().compareTo(BigDecimal.ZERO) < 0); + } + + private LoanInstallmentCharge loanChargeRefundEntranceValidation(LoanCharge loanCharge, Integer installmentNumber, LocalDate dueDate) { + + LoanInstallmentCharge installmentChargeEntry = null; + + Loan loan = loanCharge.getLoan(); + if (!(loan.isOpen() || loan.getStatus().isClosedObligationsMet() || loan.getStatus().isOverpaid())) { + final String errorMessage = "loan.charge.refund.invalid.status"; + throw new LoanChargeRefundException(errorMessage, loan.getStatus().toString()); + } + + if (dueDate != null && installmentNumber != null) { + throwLoanChargeRefundException("loan.charge.refund.dueDate.and.installmentNumber.provided.use.only.one", installmentNumber, + dueDate); + } + + if (loanCharge.isInstalmentFee()) { // identify specific installment + if (dueDate == null && installmentNumber == null) { + throwLoanChargeRefundException( + "loan.charge.refund.neither.dueDate.nor.installmentNumber.provided.for.this.installment.charge", installmentNumber, + dueDate); + } + + if (dueDate != null) { + installmentChargeEntry = loanCharge.getInstallmentLoanCharge(dueDate); + } else if (installmentNumber != null) { + installmentChargeEntry = loanCharge.getInstallmentLoanCharge(installmentNumber); + } + + if (installmentChargeEntry == null) { + throwLoanChargeRefundException("loan.charge.refund.installment.not.found", installmentNumber, dueDate); + } + } else { + + if (dueDate != null || installmentNumber != null) { + throwLoanChargeRefundException( + "loan.charge.refund.dueDate.or.installmentNumber.provided.but.this.is.not.an.installment.charge", installmentNumber, + dueDate); + } + } + + return installmentChargeEntry; + } + + private void loanChargeAdjustmentEntranceValidation(final LoanCharge loanCharge, final BigDecimal transactionAmount) { + final Loan loan = loanCharge.getLoan(); + if (!(loan.isOpen() || loan.getStatus().isClosedObligationsMet() || loan.getStatus().isOverpaid())) { + final String errorCode = "loan.charge.adjustment.invalid.status"; + throw new LoanChargeAdjustmentException(errorCode, + "Adjustment is not supported for the status of " + loan.getStatus().toString()); + } + + if (transactionAmount.compareTo(loanCharge.amount()) > 0) { + final String errorCode = "loan.charge.adjustment.invalid.amount"; + throw new LoanChargeAdjustmentException(errorCode, + "Transaction amount cannot be higher than the charge amount: " + loanCharge.amount()); + } + + BigDecimal availableAmountForAdjustment = calculateAvailableAmountForChargeAdjustment(loanCharge); + if (transactionAmount.compareTo(availableAmountForAdjustment) > 0) { + final String errorCode = "loan.charge.adjustment.invalid.amount"; + throw new LoanChargeAdjustmentException(errorCode, + "Transaction amount cannot be higher than the available charge amount for adjustment: " + availableAmountForAdjustment); + } + checkClientOrGroupActive(loan); + loan.validateAccountStatus(LoanEvent.LOAN_CHARGE_ADJUSTMENT); + } + + private BigDecimal calculateAvailableAmountForChargeAdjustment(final LoanCharge loanCharge) { + BigDecimal availableAmountForAdjustment = loanCharge.amount(); + for (LoanTransaction loanTransaction : loanCharge.getLoan().getLoanTransactions()) { + if (loanTransaction.isNotReversed() && loanTransaction.getTypeOf().isChargeAdjustment()) { + LoanTransactionRelation loanTransactionRelation = loanTransaction.getLoanTransactionRelations().stream() + .filter(e -> e.getToCharge() != null).findFirst().orElseThrow(); + if (loanCharge.equals(loanTransactionRelation.getToCharge())) { + availableAmountForAdjustment = availableAmountForAdjustment.subtract(loanTransaction.getAmount()); + } + } + } + return availableAmountForAdjustment; + } + + private void throwLoanChargeRefundException(String errorMessage, Integer installmentNumber, LocalDate dueDate) { + String dueDateValue = ""; + String installmentNumberValue = ""; + if (dueDate != null) { + dueDateValue = dueDate.toString(); + } + if (installmentNumber != null) { + installmentNumberValue = installmentNumber.toString(); + } + throw new LoanChargeRefundException(errorMessage, "dueDate: " + dueDateValue + " installmentNumber: " + installmentNumberValue); + } + + private void checkClientOrGroupActive(final Loan loan) { + final Client client = loan.client(); + if (client != null) { + if (client.isNotActive()) { + throw new ClientNotActiveException(client.getId()); + } + } + final Group group = loan.group(); + if (group != null) { + if (group.isNotActive()) { + throw new GroupNotActiveException(group.getId()); + } + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java index 250902151..09f5cf98f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java @@ -110,7 +110,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationRepository; -import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException; @@ -264,11 +263,11 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { final String sql = "select " + rm.loanPaymentsSchema() + " where tr.loan_id = ? and tr.transaction_type_enum not in (0, 3) " + " and (tr.is_reversed=false or tr.manually_adjusted_or_reversed = true) order by tr.transaction_date ASC,id "; Collection loanTransactionData = this.jdbcTemplate.query(sql, rm, loanId); // NOSONAR + // TODO: would worth to rework in the future. It is not nice to fetch relations one by one... might worth to + // give a try to get rid of native queries for (LoanTransactionData loanTransaction : loanTransactionData) { - if (loanTransaction.getType().isRepaymentType()) { - loanTransaction - .setLoanTransactionRelations(this.retrieveLoanTransactionRelationsByLoanTransactionId(loanTransaction.getId())); - } + loanTransaction + .setLoanTransactionRelations(this.retrieveLoanTransactionRelationsByLoanTransactionId(loanTransaction.getId())); } return loanTransactionData; } catch (final EmptyResultDataAccessException e) { @@ -2399,7 +2398,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { public List retrieveLoanTransactionRelationsByLoanTransactionId(Long loanTransactionId) { final LoanTransaction loanTransaction = this.loanTransactionRepository.getReferenceById(loanTransactionId); List loanTransactionRelations = this.loanTransactionRelationRepository - .findByFromTransactionAndRelationType(loanTransaction, LoanTransactionRelationTypeEnum.CHARGEBACK); + .findByFromTransaction(loanTransaction); return loanTransactionRelationMapper.map(loanTransactionRelations); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java index d1db8f194..f69352caf 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java @@ -32,7 +32,7 @@ import org.apache.fineract.portfolio.collectionsheet.command.CollectionSheetBulk import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; -import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData; +import org.springframework.transaction.annotation.Transactional; public interface LoanWritePlatformService { @@ -46,6 +46,10 @@ public interface LoanWritePlatformService { CommandProcessingResult makeLoanRepayment(LoanTransactionType repaymentTransactionType, Long loanId, JsonCommand command, boolean isRecoveryRepayment); + @Transactional + CommandProcessingResult makeLoanRepaymentWithChargeRefundChargeType(LoanTransactionType repaymentTransactionType, Long loanId, + JsonCommand command, boolean isRecoveryRepayment, String chargeRefundChargeType); + Map makeLoanBulkRepayment(CollectionSheetBulkRepaymentCommand bulkRepaymentCommand); CommandProcessingResult adjustLoanTransaction(Long loanId, Long transactionId, JsonCommand command); @@ -60,18 +64,6 @@ public interface LoanWritePlatformService { CommandProcessingResult closeAsRescheduled(Long loanId, JsonCommand command); - CommandProcessingResult addLoanCharge(Long loanId, JsonCommand command); - - CommandProcessingResult updateLoanCharge(Long loanId, Long loanChargeId, JsonCommand command); - - CommandProcessingResult deleteLoanCharge(Long loanId, Long loanChargeId, JsonCommand command); - - CommandProcessingResult waiveLoanCharge(Long loanId, Long loanChargeId, JsonCommand command); - - CommandProcessingResult undoWaiveLoanCharge(JsonCommand command); - - CommandProcessingResult loanChargeRefund(Long loanId, JsonCommand command); - CommandProcessingResult loanReassignment(Long loanId, JsonCommand command); CommandProcessingResult bulkLoanReassignment(JsonCommand command); @@ -89,8 +81,6 @@ public interface LoanWritePlatformService { LoanTransaction acceptLoanTransfer(Loan loan, LocalDate transferDate, Office acceptedInOffice, Staff loanOfficer); - CommandProcessingResult payLoanCharge(Long loanId, Long loanChargeId, JsonCommand command, boolean isChargeIdIncludedInJson); - CommandProcessingResult undoWriteOff(Long loanId); CommandProcessingResult updateDisbursementDateAndAmountForTranche(Long loanId, Long disbursementId, JsonCommand command); @@ -103,8 +93,6 @@ public interface LoanWritePlatformService { CommandProcessingResult addAndDeleteLoanDisburseDetails(Long loanId, JsonCommand command); - void applyOverdueChargesForLoan(Long loanId, Collection overdueLoanScheduleDatas); - void recalculateInterest(long loanId); CommandProcessingResult undoLastLoanDisbursal(Long loanId, JsonCommand command); @@ -117,6 +105,8 @@ public interface LoanWritePlatformService { CommandProcessingResult makeGLIMLoanRepayment(Long loanId, JsonCommand command); + void updateOriginalSchedule(Loan loan); + CommandProcessingResult creditBalanceRefund(Long loanId, JsonCommand command); CommandProcessingResult markLoanAsFraud(Long loanId, JsonCommand command); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index 15050010c..0d15b9d97 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -29,7 +29,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; -import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; @@ -60,7 +59,6 @@ import org.apache.fineract.infrastructure.dataqueries.data.StatusEnum; import org.apache.fineract.infrastructure.dataqueries.service.EntityDatatableChecksWritePlatformService; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAcceptTransferBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAdjustTransactionBusinessEvent; -import org.apache.fineract.infrastructure.event.business.domain.loan.LoanApplyOverdueChargeBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBalanceChangedBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanChargebackTransactionBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanCloseAsRescheduleBusinessEvent; @@ -76,12 +74,6 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.LoanUndoDis import org.apache.fineract.infrastructure.event.business.domain.loan.LoanUndoLastDisbursalBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanUpdateDisbursementDataBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanWithdrawTransferBusinessEvent; -import org.apache.fineract.infrastructure.event.business.domain.loan.charge.LoanAddChargeBusinessEvent; -import org.apache.fineract.infrastructure.event.business.domain.loan.charge.LoanDeleteChargeBusinessEvent; -import org.apache.fineract.infrastructure.event.business.domain.loan.charge.LoanUpdateChargeBusinessEvent; -import org.apache.fineract.infrastructure.event.business.domain.loan.charge.LoanWaiveChargeBusinessEvent; -import org.apache.fineract.infrastructure.event.business.domain.loan.charge.LoanWaiveChargeUndoBusinessEvent; -import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeRefundBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanDisbursalTransactionBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanUndoWrittenOffBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanWaiveInterestBusinessEvent; @@ -125,22 +117,7 @@ import org.apache.fineract.portfolio.calendar.domain.CalendarInstanceRepository; import org.apache.fineract.portfolio.calendar.domain.CalendarRepository; import org.apache.fineract.portfolio.calendar.domain.CalendarType; import org.apache.fineract.portfolio.calendar.exception.CalendarParameterUpdateNotSupportedException; -import org.apache.fineract.portfolio.charge.domain.Charge; import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper; -import org.apache.fineract.portfolio.charge.exception.ChargeCannotBeAppliedToException; -import org.apache.fineract.portfolio.charge.exception.ChargeCannotBeUpdatedException; -import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBeAddedException; -import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBeDeletedException; -import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBeDeletedException.LoanChargeCannotBeDeletedReason; -import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBePayedException; -import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBePayedException.LoanChargeCannotBePayedReason; -import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBeUpdatedException; -import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBeUpdatedException.LoanChargeCannotBeUpdatedReason; -import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBeWaivedException; -import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBeWaivedException.LoanChargeCannotBeWaivedReason; -import org.apache.fineract.portfolio.charge.exception.LoanChargeNotFoundException; -import org.apache.fineract.portfolio.charge.exception.LoanChargeWaiveCannotBeReversedException; -import org.apache.fineract.portfolio.charge.exception.LoanChargeWaiveCannotBeReversedException.LoanChargeWaiveCannotUndoReason; import org.apache.fineract.portfolio.client.domain.Client; import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; import org.apache.fineract.portfolio.collateralmanagement.domain.ClientCollateralManagement; @@ -149,31 +126,24 @@ import org.apache.fineract.portfolio.collectionsheet.command.CollectionSheetBulk import org.apache.fineract.portfolio.collectionsheet.command.CollectionSheetBulkRepaymentCommand; import org.apache.fineract.portfolio.collectionsheet.command.SingleDisbursalCommand; import org.apache.fineract.portfolio.collectionsheet.command.SingleRepaymentCommand; -import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.group.domain.Group; import org.apache.fineract.portfolio.group.exception.GroupNotActiveException; import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; import org.apache.fineract.portfolio.loanaccount.command.LoanUpdateCommand; import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; -import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidByData; import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail; -import org.apache.fineract.portfolio.loanaccount.domain.DefaultLoanLifecycleStateMachine; import org.apache.fineract.portfolio.loanaccount.domain.GLIMAccountInfoRepository; import org.apache.fineract.portfolio.loanaccount.domain.GroupLoanIndividualMonitoringAccount; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; -import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy; import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagement; import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails; import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetailsRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent; -import org.apache.fineract.portfolio.loanaccount.domain.LoanInstallmentCharge; -import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalcualtionAdditionalDetails; import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine; -import org.apache.fineract.portfolio.loanaccount.domain.LoanOverdueInstallmentCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallmentRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; @@ -182,7 +152,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; import org.apache.fineract.portfolio.loanaccount.domain.LoanSubStatus; import org.apache.fineract.portfolio.loanaccount.domain.LoanSummaryWrapper; -import org.apache.fineract.portfolio.loanaccount.domain.LoanTrancheDisbursementCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationRepository; @@ -191,10 +160,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepositor import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; import org.apache.fineract.portfolio.loanaccount.exception.DateMismatchException; import org.apache.fineract.portfolio.loanaccount.exception.ExceedingTrancheCountException; -import org.apache.fineract.portfolio.loanaccount.exception.InstallmentNotFoundException; -import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException; import org.apache.fineract.portfolio.loanaccount.exception.InvalidPaidInAdvanceAmountException; -import org.apache.fineract.portfolio.loanaccount.exception.LoanChargeRefundException; import org.apache.fineract.portfolio.loanaccount.exception.LoanForeclosureException; import org.apache.fineract.portfolio.loanaccount.exception.LoanMultiDisbursementException; import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerAssignmentException; @@ -203,19 +169,14 @@ import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFou import org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementDataNotAllowedException; import org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementDataRequiredException; import org.apache.fineract.portfolio.loanaccount.guarantor.service.GuarantorDomainService; -import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData; -import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.DefaultScheduledDateGenerator; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod; -import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator; import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleHistoryWritePlatformService; import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequest; import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationCommandFromApiJsonHelper; import org.apache.fineract.portfolio.loanaccount.serialization.LoanEventApiJsonValidator; import org.apache.fineract.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer; -import org.apache.fineract.portfolio.loanproduct.data.LoanOverdueDTO; import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; -import org.apache.fineract.portfolio.loanproduct.exception.InvalidCurrencyException; import org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException; import org.apache.fineract.portfolio.note.domain.Note; import org.apache.fineract.portfolio.note.domain.NoteRepository; @@ -237,7 +198,6 @@ import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatformService { - public static final String AMOUNT = "amount"; private final PlatformSecurityContext context; private final LoanEventApiJsonValidator loanEventApiJsonValidator; private final LoanUpdateCommandFromApiJsonDeserializer loanUpdateCommandFromApiJsonDeserializer; @@ -281,16 +241,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf private final PostDatedChecksRepository postDatedChecksRepository; private final LoanDisbursementDetailsRepository loanDisbursementDetailsRepository; private final LoanRepaymentScheduleInstallmentRepository loanRepaymentScheduleInstallmentRepository; + private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine; private final LoanAccountLockService loanAccountLockService; - private static boolean isPartOfThisInstallment(LoanCharge loanCharge, LoanRepaymentScheduleInstallment e) { - return e.getFromDate().isBefore(loanCharge.getDueDate()) && !loanCharge.getDueDate().isAfter(e.getDueDate()); - } - - private LoanLifecycleStateMachine defaultLoanLifecycleStateMachine() { - return new DefaultLoanLifecycleStateMachine(LoanStatus.values(), businessEventNotifierService); - } - @Transactional @Override public CommandProcessingResult disburseGLIMLoan(final Long loanId, final JsonCommand command) { @@ -944,7 +897,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf chargeRefundChargeType); } - private CommandProcessingResult makeLoanRepaymentWithChargeRefundChargeType(final LoanTransactionType repaymentTransactionType, + @Transactional + @Override + public CommandProcessingResult makeLoanRepaymentWithChargeRefundChargeType(final LoanTransactionType repaymentTransactionType, final Long loanId, final JsonCommand command, final boolean isRecoveryRepayment, final String chargeRefundChargeType) { this.loanUtilService.validateRepaymentTransactionType(repaymentTransactionType); @@ -1080,7 +1035,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf "Loan transaction:" + transactionId + " update not allowed as loan status is written off", transactionId); } - if (transactionToAdjust.hasLoanTransactionRelations()) { + if (transactionToAdjust.hasChargebackLoanTransactionRelations()) { throw new PlatformServiceUnavailableException("error.msg.loan.transaction.update.not.allowed", "Loan transaction:" + transactionId + " update not allowed as loan transaction is linked to other transactions", transactionId); @@ -1135,7 +1090,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); final ChangedTransactionDetail changedTransactionDetail = loan.adjustExistingTransaction(newTransactionDetail, - defaultLoanLifecycleStateMachine(), transactionToAdjust, existingTransactionIds, existingReversedTransactionIds, + defaultLoanLifecycleStateMachine, transactionToAdjust, existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO, reversalExternalId); if (newTransactionDetail.isGreaterThanZero(loan.getPrincipal().getCurrency())) { @@ -1270,13 +1225,13 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf this.paymentDetailWritePlatformService.persistPaymentDetail(paymentDetail); // Store the Loan Transaction Relation - LoanTransactionRelation loanTransactionRelation = LoanTransactionRelation.instance(loanTransaction, newTransaction, + LoanTransactionRelation loanTransactionRelation = LoanTransactionRelation.linkToTransaction(loanTransaction, newTransaction, LoanTransactionRelationTypeEnum.CHARGEBACK); this.loanTransactionRelationRepository.save(loanTransactionRelation); this.loanTransactionRepository.save(newTransaction); - loan.handleChargebackTransaction(newTransaction, defaultLoanLifecycleStateMachine()); + loan.handleChargebackTransaction(newTransaction, defaultLoanLifecycleStateMachine); loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan); @@ -1352,7 +1307,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); final ChangedTransactionDetail changedTransactionDetail = loan.waiveInterest(waiveInterestTransaction, - defaultLoanLifecycleStateMachine(), existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO); + defaultLoanLifecycleStateMachine, existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO); this.loanTransactionRepository.saveAndFlush(waiveInterestTransaction); @@ -1428,8 +1383,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); - final ChangedTransactionDetail changedTransactionDetail = loan.closeAsWrittenOff(command, defaultLoanLifecycleStateMachine(), - changes, existingTransactionIds, existingReversedTransactionIds, currentUser, scheduleGeneratorDTO); + final ChangedTransactionDetail changedTransactionDetail = loan.closeAsWrittenOff(command, defaultLoanLifecycleStateMachine, changes, + existingTransactionIds, existingReversedTransactionIds, currentUser, scheduleGeneratorDTO); LoanTransaction writeOff = changedTransactionDetail.getNewTransactionMappings().remove(0L); this.loanTransactionRepository.saveAndFlush(writeOff); for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { @@ -1480,7 +1435,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); - ChangedTransactionDetail changedTransactionDetail = loan.close(command, defaultLoanLifecycleStateMachine(), changes, + ChangedTransactionDetail changedTransactionDetail = loan.close(command, defaultLoanLifecycleStateMachine, changes, existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO); final LoanTransaction possibleClosingTransaction = changedTransactionDetail.getNewTransactionMappings().remove(0L); if (possibleClosingTransaction != null) { @@ -1558,7 +1513,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf changes.put("locale", command.locale()); changes.put("dateFormat", command.dateFormat()); - loan.closeAsMarkedForReschedule(command, defaultLoanLifecycleStateMachine(), changes); + loan.closeAsMarkedForReschedule(command, defaultLoanLifecycleStateMachine, changes); saveLoanWithDataIntegrityViolationChecks(loan); @@ -1600,773 +1555,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf .build(); } - private void validateAddingNewChargeAllowed(List loanDisburseDetails) { - boolean pendingDisbursementAvailable = false; - for (LoanDisbursementDetails disbursementDetail : loanDisburseDetails) { - if (disbursementDetail.actualDisbursementDate() == null) { - pendingDisbursementAvailable = true; - break; - } - } - if (!pendingDisbursementAvailable) { - throw new ChargeCannotBeUpdatedException("error.msg.charge.cannot.be.updated.no.pending.disbursements.in.loan", - "This charge cannot be added, No disbursement is pending"); - } - } - - @Transactional - @Override - public CommandProcessingResult addLoanCharge(final Long loanId, final JsonCommand command) { - - this.loanEventApiJsonValidator.validateAddLoanCharge(command.json()); - - final Loan loan = this.loanAssembler.assembleFrom(loanId); - checkClientOrGroupActive(loan); - - List loanDisburseDetails = loan.getDisbursementDetails(); - final Long chargeDefinitionId = command.longValueOfParameterNamed("chargeId"); - final Charge chargeDefinition = this.chargeRepository.findOneWithNotFoundDetection(chargeDefinitionId); - - if (loan.isDisbursed() && chargeDefinition.isDisbursementCharge()) { - // validates whether any pending disbursements are available to - // apply this charge - validateAddingNewChargeAllowed(loanDisburseDetails); - } - final List existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds()); - final List existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds()); - - boolean isAppliedOnBackDate = false; - LoanCharge loanCharge = null; - LocalDate recalculateFrom = loan.fetchInterestRecalculateFromDate(); - if (chargeDefinition.isPercentageOfDisbursementAmount()) { - LoanTrancheDisbursementCharge loanTrancheDisbursementCharge; - for (LoanDisbursementDetails disbursementDetail : loanDisburseDetails) { - if (disbursementDetail.actualDisbursementDate() == null) { - loanCharge = LoanCharge.createNewWithoutLoan(chargeDefinition, disbursementDetail.principal(), null, null, null, - disbursementDetail.expectedDisbursementDateAsLocalDate(), null, null); - loanTrancheDisbursementCharge = new LoanTrancheDisbursementCharge(loanCharge, disbursementDetail); - loanCharge.updateLoanTrancheDisbursementCharge(loanTrancheDisbursementCharge); - businessEventNotifierService.notifyPreBusinessEvent(new LoanAddChargeBusinessEvent(loanCharge)); - validateAddLoanCharge(loan, chargeDefinition, loanCharge); - addCharge(loan, chargeDefinition, loanCharge); - isAppliedOnBackDate = true; - if (recalculateFrom.isAfter(disbursementDetail.expectedDisbursementDateAsLocalDate())) { - recalculateFrom = disbursementDetail.expectedDisbursementDateAsLocalDate(); - } - } - } - if (loanCharge == null) { - final String errorMessage = "Charge with identifier " + chargeDefinition.getId() - + " cannot be applied: No valid loan disbursement available"; - throw new ChargeCannotBeAppliedToException("loan", errorMessage, chargeDefinition.getId()); - } - loan.addTrancheLoanCharge(chargeDefinition); - } else { - loanCharge = LoanCharge.createNewFromJson(loan, chargeDefinition, command); - businessEventNotifierService.notifyPreBusinessEvent(new LoanAddChargeBusinessEvent(loanCharge)); - - validateAddLoanCharge(loan, chargeDefinition, loanCharge); - isAppliedOnBackDate = addCharge(loan, chargeDefinition, loanCharge); - if (loanCharge.getDueLocalDate() == null || recalculateFrom.isAfter(loanCharge.getDueLocalDate())) { - isAppliedOnBackDate = true; - recalculateFrom = loanCharge.getDueLocalDate(); - } - } - - boolean reprocessRequired = true; - if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { - if (isAppliedOnBackDate && loan.isFeeCompoundingEnabledForInterestRecalculation()) { - - runScheduleRecalculation(loan, recalculateFrom); - reprocessRequired = false; - } - updateOriginalSchedule(loan); - } - if (reprocessRequired) { - ChangedTransactionDetail changedTransactionDetail = loan.reprocessTransactions(); - if (changedTransactionDetail != null) { - for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { - this.loanTransactionRepository.save(mapEntry.getValue()); - // update loan with references to the newly created - // transactions - loan.addLoanTransaction(mapEntry.getValue()); - this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); - } - } - saveLoanWithDataIntegrityViolationChecks(loan); - } - - postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); - - if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() && isAppliedOnBackDate - && loan.isFeeCompoundingEnabledForInterestRecalculation()) { - this.loanAccountDomainService.recalculateAccruals(loan); - } - this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); - - businessEventNotifierService.notifyPostBusinessEvent(new LoanAddChargeBusinessEvent(loanCharge)); - return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(loanCharge.getId()) - .withOfficeId(loan.getOfficeId()).withClientId(loan.getClientId()).withGroupId(loan.getGroupId()).withLoanId(loanId) - .build(); - } - - private void validateAddLoanCharge(final Loan loan, final Charge chargeDefinition, final LoanCharge loanCharge) { - if (chargeDefinition.isOverdueInstallment()) { - final String defaultUserMessage = "Installment charge cannot be added to the loan."; - throw new LoanChargeCannotBeAddedException("loanCharge", "overdue.charge", defaultUserMessage, null, - chargeDefinition.getName()); - } else if (loanCharge.getDueLocalDate() != null - && loanCharge.getDueLocalDate().isBefore(loan.getLastUserTransactionForChargeCalc())) { - final String defaultUserMessage = "charge with date before last transaction date can not be added to loan."; - throw new LoanChargeCannotBeAddedException("loanCharge", "date.is.before.last.transaction.date", defaultUserMessage, null, - chargeDefinition.getName()); - } else if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { - - if (loanCharge.isInstalmentFee() && loan.getStatus().isActive()) { - final String defaultUserMessage = "installment charge addition not allowed after disbursement"; - throw new LoanChargeCannotBeAddedException("loanCharge", "installment.charge", defaultUserMessage, null, - chargeDefinition.getName()); - } - final List dataValidationErrors = new ArrayList<>(); - final Set loanCharges = new HashSet<>(1); - loanCharges.add(loanCharge); - this.loanApplicationCommandFromApiJsonHelper.validateLoanCharges(loanCharges, dataValidationErrors); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - } - - public void runScheduleRecalculation(final Loan loan, final LocalDate recalculateFrom) { - if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { - ScheduleGeneratorDTO generatorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); - ChangedTransactionDetail changedTransactionDetail = loan - .handleRegenerateRepaymentScheduleWithInterestRecalculation(generatorDTO); - saveLoanWithDataIntegrityViolationChecks(loan); - if (changedTransactionDetail != null) { - for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { - this.loanTransactionRepository.save(mapEntry.getValue()); - // update loan with references to the newly created - // transactions - loan.addLoanTransaction(mapEntry.getValue()); - this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); - } - } - } - } - - public void updateOriginalSchedule(Loan loan) { - if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { - final LocalDate recalculateFrom = null; - ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); - createLoanScheduleArchive(loan, scheduleGeneratorDTO); - } - } - - private boolean addCharge(final Loan loan, final Charge chargeDefinition, final LoanCharge loanCharge) { - - if (!loan.hasCurrencyCodeOf(chargeDefinition.getCurrencyCode())) { - final String errorMessage = "Charge and Loan must have the same currency."; - throw new InvalidCurrencyException("loanCharge", "attach.to.loan", errorMessage); - } - - if (loanCharge.getChargePaymentMode().isPaymentModeAccountTransfer()) { - final PortfolioAccountData portfolioAccountData = this.accountAssociationsReadPlatformService - .retriveLoanLinkedAssociation(loan.getId()); - if (portfolioAccountData == null) { - final String errorMessage = loanCharge.name() + "Charge requires linked savings account for payment"; - throw new LinkedAccountRequiredException("loanCharge.add", errorMessage, loanCharge.name()); - } - } - - if (!loan.isInterestBearing() && loanCharge.isSpecifiedDueDate() && loanCharge.getDueDate().isAfter(loan.getMaturityDate())) { - LoanRepaymentScheduleInstallment latestRepaymentScheduleInstalment = loan.getRepaymentScheduleInstallments() - .get(loan.getLoanRepaymentScheduleInstallmentsSize() - 1); - if (loanCharge.getDueDate().isAfter(latestRepaymentScheduleInstalment.getDueDate())) { - if (latestRepaymentScheduleInstalment.isAdditional()) { - latestRepaymentScheduleInstalment.updateDueDate(loanCharge.getDueDate()); - } else { - final LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan, - (loan.getLoanRepaymentScheduleInstallmentsSize() + 1), latestRepaymentScheduleInstalment.getDueDate(), - loanCharge.getDueDate(), BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, false, null); - installment.markAsAdditional(); - loan.addLoanRepaymentScheduleInstallment(installment); - } - } - } - - loan.addLoanCharge(loanCharge); - - this.loanChargeRepository.saveAndFlush(loanCharge); - - /** - * we want to apply charge transactions only for those loans charges that are applied when a loan is active and - * the loan product uses Upfront Accruals - **/ - if (loan.getStatus().isActive() && loan.isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct()) { - final LoanTransaction applyLoanChargeTransaction = loan.handleChargeAppliedTransaction(loanCharge, null); - this.loanTransactionRepository.saveAndFlush(applyLoanChargeTransaction); - } - return loanCharge.getDueLocalDate() == null || DateUtils.getBusinessLocalDate().isAfter(loanCharge.getDueLocalDate()); - } - - @Transactional - @Override - public CommandProcessingResult updateLoanCharge(final Long loanId, final Long loanChargeId, final JsonCommand command) { - - this.loanEventApiJsonValidator.validateUpdateOfLoanCharge(command.json()); - - final Loan loan = this.loanAssembler.assembleFrom(loanId); - checkClientOrGroupActive(loan); - final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId); - - // Charges may be edited only when the loan associated with them are - // yet to be approved (are in submitted and pending status) - if (!loan.getStatus().isSubmittedAndPendingApproval()) { - throw new LoanChargeCannotBeUpdatedException(LoanChargeCannotBeUpdatedReason.LOAN_NOT_IN_SUBMITTED_AND_PENDING_APPROVAL_STAGE, - loanCharge.getId()); - } - businessEventNotifierService.notifyPreBusinessEvent(new LoanUpdateChargeBusinessEvent(loanCharge)); - - final Map changes = loan.updateLoanCharge(loanCharge, command); - - saveLoanWithDataIntegrityViolationChecks(loan); - businessEventNotifierService.notifyPostBusinessEvent(new LoanUpdateChargeBusinessEvent(loanCharge)); - return new CommandProcessingResultBuilder() // - .withCommandId(command.commandId()) // - .withEntityId(loanChargeId) // - .withOfficeId(loan.getOfficeId()) // - .withClientId(loan.getClientId()) // - .withGroupId(loan.getGroupId()) // - .withLoanId(loanId) // - .with(changes) // - .build(); - } - - @Transactional - @Override - public CommandProcessingResult loanChargeRefund(final Long loanId, final JsonCommand command) { - - this.loanEventApiJsonValidator.validateLoanChargeRefundTransaction(command.json()); - - final Long loanChargeId = command.longValueOfParameterNamed("loanChargeId"); - final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId); - - final Integer installmentNumber = command.integerValueOfParameterNamed("installmentNumber"); - final LocalDate dueDate = command.localDateValueOfParameterNamed("dueDate"); - final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); - - final LoanInstallmentCharge installmentChargeEntry = loanChargeRefundEntranceValidation(loanCharge, installmentNumber, dueDate); - Integer installmentNumberIdentified = null; - if (installmentChargeEntry != null) { - installmentNumberIdentified = installmentChargeEntry.getRepaymentInstallment().getInstallmentNumber(); - } - final BigDecimal fullRefundAbleAmount = loanChargeValidateRefundAmount(loanCharge, installmentChargeEntry, transactionAmount); - - JsonCommand repaymentJsonCommand = adaptLoanChargeRefundCommandForFurtherRepaymentProcessing(command, fullRefundAbleAmount); - - boolean isRecoveryRepayment = false; - String chargeRefundChargeType = "F"; - if (loanCharge.isPenaltyCharge()) { - chargeRefundChargeType = "P"; - } - // chargeRefundChargeType only included as a parameter for accounting reason - in order to identify whether fee - // or penalty GL account is relevant - CommandProcessingResult result = makeLoanRepaymentWithChargeRefundChargeType(LoanTransactionType.CHARGE_REFUND, - repaymentJsonCommand.getLoanId(), repaymentJsonCommand, isRecoveryRepayment, chargeRefundChargeType); - - Long loanChargeRefundTransactionId = result.getResourceId(); - LoanTransaction newChargeRefundTxn = null; - for (LoanTransaction chargeRefundTxn : loanCharge.getLoan().getLoanTransactions()) { - if (loanChargeRefundTransactionId.equals(chargeRefundTxn.getId())) { - newChargeRefundTxn = chargeRefundTxn; - final BigDecimal appliedRefundAmount = newChargeRefundTxn.getAmount(loanCharge.getLoan().getCurrency()).getAmount() - .multiply(BigDecimal.valueOf(-1)); - final LoanChargePaidBy loanChargePaidByForChargeRefund = new LoanChargePaidBy(newChargeRefundTxn, loanCharge, - appliedRefundAmount, installmentNumberIdentified); - newChargeRefundTxn.getLoanChargesPaid().add(loanChargePaidByForChargeRefund); - loanCharge.getLoanChargePaidBySet().add(loanChargePaidByForChargeRefund); - break; - } - } - - businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(newChargeRefundTxn.getLoan())); - businessEventNotifierService.notifyPostBusinessEvent(new LoanChargeRefundBusinessEvent(newChargeRefundTxn)); - return result; - } - - private JsonCommand adaptLoanChargeRefundCommandForFurtherRepaymentProcessing(JsonCommand command, BigDecimal fullRefundAbleAmount) { - // creates JsonCommand for onward repayment processing - JsonObject jsonObject = (JsonObject) this.fromApiJsonHelper.parse(command.json()); - - String dateFormat; - if (this.fromApiJsonHelper.parameterExists("dateFormat", jsonObject)) { - dateFormat = this.fromApiJsonHelper.extractStringNamed("dateFormat", jsonObject); - } else { - dateFormat = "dd MMMM yyyy"; - jsonObject.addProperty("dateFormat", dateFormat); - } - DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(dateFormat); - LocalDate transactionDate = DateUtils.getLocalDateOfTenant(); - String transactionDateString = transactionDate.format(dateTimeFormatter); - jsonObject.addProperty("transactionDate", transactionDateString); - if (!this.fromApiJsonHelper.parameterExists("transactionAmount", jsonObject)) { - jsonObject.addProperty("transactionAmount", fullRefundAbleAmount.toString()); - } - jsonObject.remove("loanChargeId"); - jsonObject.remove("installmentNumber"); - jsonObject.remove("dueDate"); - - return JsonCommand.fromExistingCommand(command, jsonObject); - } - - private BigDecimal loanChargeValidateRefundAmount(LoanCharge loanCharge, LoanInstallmentCharge installmentChargeEntry, - BigDecimal transactionAmount) { - // if transactionAmount not provided return max refundable amount (amount paid minus previous refunds) - BigDecimal chargeAmountPaid; - BigDecimal chargeAmountRefunded = BigDecimal.ZERO; - MonetaryCurrency loanCurrency = loanCharge.getLoan().getCurrency(); - if (loanCharge.isInstalmentFee()) { - final Integer installmentNumber = installmentChargeEntry.getRepaymentInstallment().getInstallmentNumber(); - chargeAmountPaid = installmentChargeEntry.getAmountPaid(loanCurrency).getAmount(); - for (LoanChargePaidBy loanChargePaidBy : loanCharge.getLoanChargePaidBySet()) { - if (installmentNumber.equals(loanChargePaidBy.getInstallmentNumber()) && isRefundElementOfChargeRefund(loanChargePaidBy)) { - chargeAmountRefunded = chargeAmountRefunded.add(loanChargePaidBy.getAmount()); - } - } - } else { - chargeAmountPaid = loanCharge.getAmountPaid(loanCurrency).getAmount(); - for (LoanChargePaidBy loanChargePaidBy : loanCharge.getLoanChargePaidBySet()) { - if (isRefundElementOfChargeRefund(loanChargePaidBy)) { - chargeAmountRefunded = chargeAmountRefunded.add(loanChargePaidBy.getAmount()); - } - } - } - chargeAmountRefunded = chargeAmountRefunded.multiply(BigDecimal.valueOf(-1)); - - if (chargeAmountRefunded.compareTo(chargeAmountPaid) > 0) { - final String errorMessage = "loan.charge.more.refunded.than.paid.unexpected.system.error"; - final String details = "Paid: " + chargeAmountPaid.toString() + " Refunded: " + chargeAmountPaid; - throw new LoanChargeRefundException(errorMessage, details); - } - - BigDecimal refundableAmount = chargeAmountPaid.subtract(chargeAmountRefunded); - // refund amount was provided. - if (transactionAmount != null && transactionAmount.compareTo(refundableAmount) > 0) { - final String errorMessage = "loan.charge.transaction.amount.is.more.than.is.refundable"; - final String details = "transactionAmount: " + transactionAmount + " Refundable: " + refundableAmount; - throw new LoanChargeRefundException(errorMessage, details); - } - - return refundableAmount; - } - - private boolean isRefundElementOfChargeRefund(LoanChargePaidBy loanChargePaidBy) { - // The Refund Element is always negative - return (loanChargePaidBy.getLoanTransaction().isChargeRefund() && loanChargePaidBy.getAmount().compareTo(BigDecimal.ZERO) < 0); - } - - private LoanInstallmentCharge loanChargeRefundEntranceValidation(LoanCharge loanCharge, Integer installmentNumber, LocalDate dueDate) { - - LoanInstallmentCharge installmentChargeEntry = null; - - Loan loan = loanCharge.getLoan(); - if (!(loan.isOpen() || loan.getStatus().isClosedObligationsMet() || loan.getStatus().isOverpaid())) { - final String errorMessage = "loan.charge.refund.invalid.status"; - throw new LoanChargeRefundException(errorMessage, loan.getStatus().toString()); - } - - if (dueDate != null && installmentNumber != null) { - throwLoanChargeRefundException("loan.charge.refund.dueDate.and.installmentNumber.provided.use.only.one", installmentNumber, - dueDate); - } - - if (loanCharge.isInstalmentFee()) { // identify specific installment - if (dueDate == null && installmentNumber == null) { - throwLoanChargeRefundException( - "loan.charge.refund.neither.dueDate.nor.installmentNumber.provided.for.this.installment.charge", installmentNumber, - dueDate); - } - - if (dueDate != null) { - installmentChargeEntry = loanCharge.getInstallmentLoanCharge(dueDate); - } else if (installmentNumber != null) { - installmentChargeEntry = loanCharge.getInstallmentLoanCharge(installmentNumber); - } - - if (installmentChargeEntry == null) { - throwLoanChargeRefundException("loan.charge.refund.installment.not.found", installmentNumber, dueDate); - } - } else { - - if (dueDate != null || installmentNumber != null) { - throwLoanChargeRefundException( - "loan.charge.refund.dueDate.or.installmentNumber.provided.but.this.is.not.an.installment.charge", installmentNumber, - dueDate); - } - } - - return installmentChargeEntry; - } - - private void throwLoanChargeRefundException(String errorMessage, Integer installmentNumber, LocalDate dueDate) { - String dueDateValue = ""; - String installmentNumberValue = ""; - if (dueDate != null) { - dueDateValue = dueDate.toString(); - } - if (installmentNumber != null) { - installmentNumberValue = installmentNumber.toString(); - } - throw new LoanChargeRefundException(errorMessage, "dueDate: " + dueDateValue + " installmentNumber: " + installmentNumberValue); - } - - @Transactional - @Override - public CommandProcessingResult undoWaiveLoanCharge(final JsonCommand command) { - - LoanTransaction loanTransaction = this.loanTransactionRepository.findByIdAndLoanId(command.entityId(), command.getLoanId()) - .orElseThrow(() -> new LoanTransactionNotFoundException(command.entityId(), command.getLoanId())); - if (!loanTransaction.getTypeOf().getCode().equals(LoanTransactionType.WAIVE_CHARGES.getCode())) { - throw new InvalidLoanTransactionTypeException("transaction", "undo.waive.charge", "Transaction is not a waive charge type."); - } - if (!loanTransaction.isNotReversed()) { - throw new LoanChargeWaiveCannotBeReversedException(LoanChargeWaiveCannotUndoReason.ALREADY_REVERSED, loanTransaction.getId()); - } - - Set loanChargePaidBySet = loanTransaction.getLoanChargesPaid(); - LoanChargePaidBy loanChargePaidBy = loanChargePaidBySet.stream().findFirst().orElseThrow(LoanChargeNotFoundException::new); - final LoanCharge loanCharge = loanChargePaidBy.getLoanCharge(); - // Validate loan charge is not already paid - if (loanCharge.isPaid()) { - throw new LoanChargeWaiveCannotBeReversedException(LoanChargeWaiveCannotUndoReason.ALREADY_PAID, loanCharge.getId()); - } - - final Long loanId = loanTransaction.getLoan().getId(); - final Loan loan = this.loanAssembler.assembleFrom(loanId); - checkClientOrGroupActive(loan); - // Charges may be waived only when the loan associated with them are - // active - if (!loan.getStatus().isActive()) { - throw new LoanChargeWaiveCannotBeReversedException(LoanChargeWaiveCannotUndoReason.LOAN_INACTIVE, loanCharge.getId()); - } - - final Map changes = new LinkedHashMap<>(3); - - businessEventNotifierService.notifyPreBusinessEvent(new LoanWaiveChargeUndoBusinessEvent(loanCharge)); - - undoWaivedCharge(changes, loan, loanTransaction, loanChargePaidBy); - - businessEventNotifierService.notifyPostBusinessEvent(new LoanWaiveChargeUndoBusinessEvent(loanCharge)); - - changes.put("principalPortion", loanTransaction.getPrincipalPortion()); - changes.put("interestPortion", loanTransaction.getInterestPortion(loan.getCurrency())); - changes.put("feeChargesPortion", loanTransaction.getFeeChargesPortion(loan.getCurrency())); - changes.put("penaltyChargesPortion", loanTransaction.getPenaltyChargesPortion(loan.getCurrency())); - changes.put("outstandingLoanBalance", loanTransaction.getOutstandingLoanBalance()); - changes.put("id", loanTransaction.getId()); - changes.put("date", loanTransaction.getTransactionDate()); - - return new CommandProcessingResultBuilder() // - .withCommandId(command.commandId()) // - .withEntityId(loanCharge.getId()) // - .withLoanId(loanId) // - .with(changes).build(); - } - - private void undoWaivedCharge(final Map changes, final Loan loan, final LoanTransaction loanTransaction, - final LoanChargePaidBy loanChargePaidBy) { - switch (loanChargePaidBy.getLoanCharge().getChargeTimeType()) { - case SPECIFIED_DUE_DATE -> undoSpecifiedDueDateCharge(changes, loan, loanTransaction, loanChargePaidBy); - case INSTALMENT_FEE -> undoInstalmentFee(changes, loan, loanTransaction, loanChargePaidBy); - default -> throw new UnsupportedOperationException( - "Undo waive charge is not support for this charge: " + loanChargePaidBy.getLoanCharge().getChargeTimeType()); - } - } - - private void undoInstalmentFee(Map changes, Loan loan, LoanTransaction loanTransaction, - LoanChargePaidBy loanChargePaidBy) { - final List existingTransactionIds = loan.findExistingTransactionIds(); - final List existingReversedTransactionIds = loan.findExistingReversedTransactionIds(); - LoanCharge loanCharge = loanChargePaidBy.getLoanCharge(); - final Integer installmentNumber = loanChargePaidBy.getInstallmentNumber(); - LoanInstallmentCharge chargePerInstallment; - // final Integer installmentNumber = command.integerValueOfParameterNamed("installmentNumber"); - if (installmentNumber != null) { - // Get installment charge. - chargePerInstallment = loanCharge.getInstallmentLoanCharge(installmentNumber); - // Get installment amount waived. - BigDecimal amountWaived = chargePerInstallment.getAmountWaived(loan.getCurrency()).getAmount(); - // Check whether the installment charge is not waived. If so throw new error - if (!chargePerInstallment.isWaived() || amountWaived == null) { - throw new LoanChargeWaiveCannotBeReversedException(LoanChargeWaiveCannotUndoReason.NOT_WAIVED, loanCharge.getId()); - } - // Reverse waived transaction - loanTransaction.reverse(); - // Set manually adjusted value to `1` - loanTransaction.setManuallyAdjustedOrReversed(); - // Get loan charge outstanding amount - BigDecimal amountOutstanding = loanCharge.getAmountOutstanding(loan.getCurrency()).getAmount(); - // Add the amount waived to outstanding amount - loanCharge.setOutstandingAmount(amountOutstanding.add(amountWaived)); - // Get loan charge total amount waived - BigDecimal totalAmountWaved = loanCharge.getAmountWaived(loan.getCurrency()).getAmount(); - // Subtract the amount waived from the existing amount waived. - loanCharge.setAmountWaived(totalAmountWaved.subtract(amountWaived)); - // Get installment outstanding amount - BigDecimal amountOutstandingPerInstallment = chargePerInstallment.getAmountOutstanding(); - // Add the amount waived to the outstanding amount of the installment - chargePerInstallment.setOutstandingAmount(amountOutstandingPerInstallment.add(amountWaived)); - // Set the amount waived value to ZERO - chargePerInstallment.setAmountWaived(null); - // Reset waived flag - chargePerInstallment.undoWaiveFlag(); - // Update installment balances - updateRepaymentInstalmentWithWaivedAmount(loanCharge, chargePerInstallment.getInstallment(), amountWaived); - // Update loan charge. - loanCharge.setInstallmentLoanCharge(chargePerInstallment, chargePerInstallment.getInstallment().getInstallmentNumber()); - if (loanCharge.amount().compareTo(loanCharge.amountOutstanding()) == 0 && loanCharge.isWaived()) { - loanCharge.undoWaived(); - } - loan.updateLoanSummaryForUndoWaiveCharge(amountWaived, loanCharge.isPenaltyCharge()); - postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); - changes.put(AMOUNT, amountWaived); - } else { - throw new InstallmentNotFoundException(loanTransaction.getId()); - } - } - - private void undoSpecifiedDueDateCharge(final Map changes, final Loan loan, final LoanTransaction loanTransaction, - final LoanChargePaidBy loanChargePaidBy) { - - final List existingTransactionIds = loan.findExistingTransactionIds(); - final List existingReversedTransactionIds = loan.findExistingReversedTransactionIds(); - LoanCharge loanCharge = loanChargePaidBy.getLoanCharge(); - BigDecimal amountWaived = loanCharge.getAmountWaived(loan.getCurrency()).getAmount(); - if (!loanCharge.isWaived() || amountWaived == null) { - throw new LoanChargeWaiveCannotBeReversedException(LoanChargeWaiveCannotUndoReason.NOT_WAIVED, loanCharge.getId()); - } - loanTransaction.reverse(); - loanTransaction.setManuallyAdjustedOrReversed(); - loanCharge.setOutstandingAmount(loanCharge.amountOutstanding().add(amountWaived)); - loanCharge.setAmountWaived(null); - loanCharge.undoWaived(); - LoanRepaymentScheduleInstallment installment = loan.getRepaymentScheduleInstallments().stream() - .filter(e -> isPartOfThisInstallment(loanCharge, e)).findFirst().orElseThrow(); - updateRepaymentInstalmentWithWaivedAmount(loanCharge, installment, amountWaived); - loan.updateLoanSummaryForUndoWaiveCharge(amountWaived, loanCharge.isPenaltyCharge()); - postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); - changes.put(AMOUNT, amountWaived); - } - - private void updateRepaymentInstalmentWithWaivedAmount(final LoanCharge loanCharge, final LoanRepaymentScheduleInstallment installment, - final BigDecimal amountWaived) { - if (loanCharge.isPenaltyCharge()) { - // Get the penalty charges waived amount per installment - BigDecimal penaltyChargesWaivedAmount = installment.getPenaltyChargesWaived(loanCharge.getLoan().getCurrency()).getAmount(); - // Subtract the amount waived from the existing fee charges waived amount. - installment.setPenaltyChargesWaived(penaltyChargesWaivedAmount.subtract(amountWaived)); - } else { - // Get the fee charges waived amount per installment - BigDecimal feeChargesWaivedAmount = installment.getFeeChargesWaived(loanCharge.getLoan().getCurrency()).getAmount(); - // Subtract the amount waived from the existing fee charges waived amount. - installment.setFeeChargesWaived(feeChargesWaivedAmount.subtract(amountWaived)); - } - } - - @Transactional - @Override - public CommandProcessingResult waiveLoanCharge(final Long loanId, final Long loanChargeId, final JsonCommand command) { - - final Loan loan = this.loanAssembler.assembleFrom(loanId); - checkClientOrGroupActive(loan); - this.loanEventApiJsonValidator.validateInstallmentChargeTransaction(command.json()); - final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId); - - // Charges may be waived only when the loan associated with them are - // active - if (!loan.getStatus().isActive()) { - throw new LoanChargeCannotBeWaivedException(LoanChargeCannotBeWaivedReason.LOAN_INACTIVE, loanCharge.getId()); - } - - // validate loan charge is not already paid or waived - if (loanCharge.isWaived()) { - throw new LoanChargeCannotBeWaivedException(LoanChargeCannotBeWaivedReason.ALREADY_WAIVED, loanCharge.getId()); - } else if (loanCharge.isPaid()) { - throw new LoanChargeCannotBeWaivedException(LoanChargeCannotBeWaivedReason.ALREADY_PAID, loanCharge.getId()); - } - businessEventNotifierService.notifyPreBusinessEvent(new LoanWaiveChargeBusinessEvent(loanCharge)); - Integer loanInstallmentNumber = null; - if (loanCharge.isInstalmentFee()) { - LoanInstallmentCharge chargePerInstallment = null; - if (!StringUtils.isBlank(command.json())) { - final LocalDate dueDate = command.localDateValueOfParameterNamed("dueDate"); - final Integer installmentNumber = command.integerValueOfParameterNamed("installmentNumber"); - if (dueDate != null) { - chargePerInstallment = loanCharge.getInstallmentLoanCharge(dueDate); - } else if (installmentNumber != null) { - chargePerInstallment = loanCharge.getInstallmentLoanCharge(installmentNumber); - } - } - if (chargePerInstallment == null) { - chargePerInstallment = loanCharge.getUnpaidInstallmentLoanCharge(); - } - if (chargePerInstallment.isWaived()) { - throw new LoanChargeCannotBePayedException(LoanChargeCannotBePayedReason.ALREADY_WAIVED, loanCharge.getId()); - } else if (chargePerInstallment.isPaid()) { - throw new LoanChargeCannotBePayedException(LoanChargeCannotBePayedReason.ALREADY_PAID, loanCharge.getId()); - } - loanInstallmentNumber = chargePerInstallment.getRepaymentInstallment().getInstallmentNumber(); - } - - final Map changes = new LinkedHashMap<>(3); - - final List existingTransactionIds = new ArrayList<>(); - final List existingReversedTransactionIds = new ArrayList<>(); - LocalDate recalculateFrom = null; - ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); - - Money accruedCharge = Money.zero(loan.getCurrency()); - if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) { - Collection chargePaidByCollection = this.loanChargeReadPlatformService - .retriveLoanChargesPaidBy(loanCharge.getId(), LoanTransactionType.ACCRUAL, loanInstallmentNumber); - for (LoanChargePaidByData chargePaidByData : chargePaidByCollection) { - accruedCharge = accruedCharge.plus(chargePaidByData.getAmount()); - } - } - - final LoanTransaction waiveTransaction = loan.waiveLoanCharge(loanCharge, defaultLoanLifecycleStateMachine(), changes, - existingTransactionIds, existingReversedTransactionIds, loanInstallmentNumber, scheduleGeneratorDTO, accruedCharge); - - this.loanTransactionRepository.saveAndFlush(waiveTransaction); - saveLoanWithDataIntegrityViolationChecks(loan); - - postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); - this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); - - businessEventNotifierService.notifyPostBusinessEvent(new LoanWaiveChargeBusinessEvent(loanCharge)); - - return new CommandProcessingResultBuilder() // - .withCommandId(command.commandId()) // - .withEntityId(loanChargeId) // - .withOfficeId(loan.getOfficeId()) // - .withClientId(loan.getClientId()) // - .withGroupId(loan.getGroupId()) // - .withLoanId(loanId) // - .with(changes) // - .build(); - } - - @Transactional - @Override - public CommandProcessingResult deleteLoanCharge(final Long loanId, final Long loanChargeId, final JsonCommand command) { - - final Loan loan = this.loanAssembler.assembleFrom(loanId); - checkClientOrGroupActive(loan); - final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId); - - // Charges may be deleted only when the loan associated with them are - // yet to be approved (are in submitted and pending status) - if (!loan.getStatus().isSubmittedAndPendingApproval()) { - throw new LoanChargeCannotBeDeletedException(LoanChargeCannotBeDeletedReason.LOAN_NOT_IN_SUBMITTED_AND_PENDING_APPROVAL_STAGE, - loanCharge.getId()); - } - businessEventNotifierService.notifyPreBusinessEvent(new LoanDeleteChargeBusinessEvent(loanCharge)); - - loan.removeLoanCharge(loanCharge); - saveLoanWithDataIntegrityViolationChecks(loan); - businessEventNotifierService.notifyPostBusinessEvent(new LoanDeleteChargeBusinessEvent(loanCharge)); - return new CommandProcessingResultBuilder() // - .withCommandId(command.commandId()) // - .withEntityId(loanChargeId) // - .withOfficeId(loan.getOfficeId()) // - .withClientId(loan.getClientId()) // - .withGroupId(loan.getGroupId()) // - .withLoanId(loanId) // - .build(); - } - - @Override - @Transactional - public CommandProcessingResult payLoanCharge(final Long loanId, Long loanChargeId, final JsonCommand command, - final boolean isChargeIdIncludedInJson) { - - this.loanEventApiJsonValidator.validateChargePaymentTransaction(command.json(), isChargeIdIncludedInJson); - if (isChargeIdIncludedInJson) { - loanChargeId = command.longValueOfParameterNamed("chargeId"); - } - final Loan loan = this.loanAssembler.assembleFrom(loanId); - checkClientOrGroupActive(loan); - final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId); - - // Charges may be waived only when the loan associated with them are - // active - if (!loan.getStatus().isActive()) { - throw new LoanChargeCannotBePayedException(LoanChargeCannotBePayedReason.LOAN_INACTIVE, loanCharge.getId()); - } - - // validate loan charge is not already paid or waived - if (loanCharge.isWaived()) { - throw new LoanChargeCannotBePayedException(LoanChargeCannotBePayedReason.ALREADY_WAIVED, loanCharge.getId()); - } else if (loanCharge.isPaid()) { - throw new LoanChargeCannotBePayedException(LoanChargeCannotBePayedReason.ALREADY_PAID, loanCharge.getId()); - } - - if (!loanCharge.getChargePaymentMode().isPaymentModeAccountTransfer()) { - throw new LoanChargeCannotBePayedException(LoanChargeCannotBePayedReason.CHARGE_NOT_ACCOUNT_TRANSFER, loanCharge.getId()); - } - - final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); - - final Locale locale = command.extractLocale(); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); - Integer loanInstallmentNumber = null; - BigDecimal amount = loanCharge.amountOutstanding(); - if (loanCharge.isInstalmentFee()) { - LoanInstallmentCharge chargePerInstallment = null; - final LocalDate dueDate = command.localDateValueOfParameterNamed("dueDate"); - final Integer installmentNumber = command.integerValueOfParameterNamed("installmentNumber"); - if (dueDate != null) { - chargePerInstallment = loanCharge.getInstallmentLoanCharge(dueDate); - } else if (installmentNumber != null) { - chargePerInstallment = loanCharge.getInstallmentLoanCharge(installmentNumber); - } - if (chargePerInstallment == null) { - chargePerInstallment = loanCharge.getUnpaidInstallmentLoanCharge(); - } - if (chargePerInstallment.isWaived()) { - throw new LoanChargeCannotBePayedException(LoanChargeCannotBePayedReason.ALREADY_WAIVED, loanCharge.getId()); - } else if (chargePerInstallment.isPaid()) { - throw new LoanChargeCannotBePayedException(LoanChargeCannotBePayedReason.ALREADY_PAID, loanCharge.getId()); - } - loanInstallmentNumber = chargePerInstallment.getRepaymentInstallment().getInstallmentNumber(); - amount = chargePerInstallment.getAmountOutstanding(); - } - - final PortfolioAccountData portfolioAccountData = this.accountAssociationsReadPlatformService.retriveLoanLinkedAssociation(loanId); - if (portfolioAccountData == null) { - final String errorMessage = "Charge with id:" + loanChargeId + " requires linked savings account for payment"; - throw new LinkedAccountRequiredException("loanCharge.pay", errorMessage, loanChargeId); - } - final SavingsAccount fromSavingsAccount = null; - final boolean isRegularTransaction = true; - final boolean isExceptionForBalanceCheck = false; - final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, amount, PortfolioAccountType.SAVINGS, - PortfolioAccountType.LOAN, portfolioAccountData.getId(), loanId, "Loan Charge Payment", locale, fmt, null, null, - LoanTransactionType.CHARGE_PAYMENT.getValue(), loanChargeId, loanInstallmentNumber, - AccountTransferType.CHARGE_PAYMENT.getValue(), null, null, null, null, null, fromSavingsAccount, isRegularTransaction, - isExceptionForBalanceCheck); - this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); - return new CommandProcessingResultBuilder() // - .withCommandId(command.commandId()) // - .withEntityId(loanChargeId) // - .withOfficeId(loan.getOfficeId()) // - .withClientId(loan.getClientId()) // - .withGroupId(loan.getGroupId()) // - .withLoanId(loanId) // - .withSavingsId(portfolioAccountData.getId()).build(); - } - private void disburseLoanToLoan(final Loan loan, final JsonCommand command, final BigDecimal amount) { final LocalDate transactionDate = command.localDateValueOfParameterNamed("actualDisbursementDate"); @@ -2406,16 +1594,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } - private LoanCharge retrieveLoanChargeBy(final Long loanId, final Long loanChargeId) { - final LoanCharge loanCharge = this.loanChargeRepository.findById(loanChargeId) - .orElseThrow(() -> new LoanChargeNotFoundException(loanChargeId)); - - if (loanCharge.hasNotLoanIdentifiedBy(loanId)) { - throw new LoanChargeNotFoundException(loanChargeId, loanId); - } - return loanCharge; - } - @Transactional @Override public LoanTransaction initiateLoanTransfer(final Loan loan, final LocalDate transferDate) { @@ -2431,7 +1609,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final LoanTransaction newTransferTransaction = LoanTransaction.initiateTransfer(loan.getOffice(), loan, transferDate); loan.addLoanTransaction(newTransferTransaction); - LoanLifecycleStateMachine loanLifecycleStateMachine = defaultLoanLifecycleStateMachine(); + LoanLifecycleStateMachine loanLifecycleStateMachine = defaultLoanLifecycleStateMachine; loanLifecycleStateMachine.transition(LoanEvent.LOAN_INITIATE_TRANSFER, loan); this.loanTransactionRepository.saveAndFlush(newTransferTransaction); @@ -2453,7 +1631,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final LoanTransaction newTransferAcceptanceTransaction = LoanTransaction.approveTransfer(acceptedInOffice, loan, transferDate); loan.addLoanTransaction(newTransferAcceptanceTransaction); - LoanLifecycleStateMachine loanLifecycleStateMachine = defaultLoanLifecycleStateMachine(); + LoanLifecycleStateMachine loanLifecycleStateMachine = defaultLoanLifecycleStateMachine; if (loan.getTotalOverpaid() != null) { loanLifecycleStateMachine.transition(LoanEvent.LOAN_OVERPAYMENT, loan); } else { @@ -2483,7 +1661,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final LoanTransaction newTransferAcceptanceTransaction = LoanTransaction.withdrawTransfer(loan.getOffice(), loan, transferDate); loan.addLoanTransaction(newTransferAcceptanceTransaction); - LoanLifecycleStateMachine loanLifecycleStateMachine = defaultLoanLifecycleStateMachine(); + LoanLifecycleStateMachine loanLifecycleStateMachine = defaultLoanLifecycleStateMachine; loanLifecycleStateMachine.transition(LoanEvent.LOAN_WITHDRAW_TRANSFER, loan); this.loanTransactionRepository.saveAndFlush(newTransferAcceptanceTransaction); @@ -2500,7 +1678,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf public void rejectLoanTransfer(final Loan loan) { this.loanAssembler.setHelpers(loan); businessEventNotifierService.notifyPreBusinessEvent(new LoanRejectTransferBusinessEvent(loan)); - LoanLifecycleStateMachine loanLifecycleStateMachine = defaultLoanLifecycleStateMachine(); + LoanLifecycleStateMachine loanLifecycleStateMachine = defaultLoanLifecycleStateMachine; loanLifecycleStateMachine.transition(LoanEvent.LOAN_REJECT_TRANSFER, loan); saveLoanWithDataIntegrityViolationChecks(loan); businessEventNotifierService.notifyPostBusinessEvent(new LoanRejectTransferBusinessEvent(loan)); @@ -2859,166 +2037,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } } - @Override - @Transactional - public void applyOverdueChargesForLoan(final Long loanId, Collection overdueLoanScheduleDataList) { - - Loan loan = this.loanAssembler.assembleFrom(loanId); - final List existingTransactionIds = loan.findExistingTransactionIds(); - final List existingReversedTransactionIds = loan.findExistingReversedTransactionIds(); - boolean runInterestRecalculation = false; - LocalDate recalculateFrom = DateUtils.getBusinessLocalDate(); - LocalDate lastChargeDate = null; - for (final OverdueLoanScheduleData overdueInstallment : overdueLoanScheduleDataList) { - - final JsonElement parsedCommand = this.fromApiJsonHelper.parse(overdueInstallment.toString()); - final JsonCommand command = JsonCommand.from(overdueInstallment.toString(), parsedCommand, this.fromApiJsonHelper, null, null, - null, null, null, loanId, null, null, null, null, null, null, null); - LoanOverdueDTO overdueDTO = applyChargeToOverdueLoanInstallment(loan, overdueInstallment.getChargeId(), - overdueInstallment.getPeriodNumber(), command); - loan = overdueDTO.getLoan(); - runInterestRecalculation = runInterestRecalculation || overdueDTO.isRunInterestRecalculation(); - if (recalculateFrom.isAfter(overdueDTO.getRecalculateFrom())) { - recalculateFrom = overdueDTO.getRecalculateFrom(); - } - if (lastChargeDate == null || overdueDTO.getLastChargeAppliedDate().isAfter(lastChargeDate)) { - lastChargeDate = overdueDTO.getLastChargeAppliedDate(); - } - } - if (loan != null) { - boolean reprocessRequired = true; - LocalDate recalculatedTill = loan.fetchInterestRecalculateFromDate(); - if (recalculateFrom.isAfter(recalculatedTill)) { - recalculateFrom = recalculatedTill; - } - - if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { - if (runInterestRecalculation && loan.isFeeCompoundingEnabledForInterestRecalculation()) { - runScheduleRecalculation(loan, recalculateFrom); - reprocessRequired = false; - } - updateOriginalSchedule(loan); - } - - if (reprocessRequired) { - addInstallmentIfPenaltyAppliedAfterLastDueDate(loan, lastChargeDate); - ChangedTransactionDetail changedTransactionDetail = loan.reprocessTransactions(); - if (changedTransactionDetail != null) { - for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings() - .entrySet()) { - this.loanTransactionRepository.save(mapEntry.getValue()); - // update loan with references to the newly created - // transactions - loan.addLoanTransaction(mapEntry.getValue()); - this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); - } - } - } - - postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); - - if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() && runInterestRecalculation - && loan.isFeeCompoundingEnabledForInterestRecalculation()) { - this.loanAccountDomainService.recalculateAccruals(loan); - } - this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); - businessEventNotifierService.notifyPostBusinessEvent(new LoanApplyOverdueChargeBusinessEvent(loan)); - } - } - - private void addInstallmentIfPenaltyAppliedAfterLastDueDate(Loan loan, LocalDate lastChargeDate) { - if (lastChargeDate != null) { - List installments = loan.getRepaymentScheduleInstallments(); - LoanRepaymentScheduleInstallment lastInstallment = loan.fetchRepaymentScheduleInstallment(installments.size()); - if (lastChargeDate.isAfter(lastInstallment.getDueDate())) { - if (lastInstallment.isRecalculatedInterestComponent()) { - installments.remove(lastInstallment); - lastInstallment = loan.fetchRepaymentScheduleInstallment(installments.size()); - } - boolean recalculatedInterestComponent = true; - BigDecimal principal = BigDecimal.ZERO; - BigDecimal interest = BigDecimal.ZERO; - BigDecimal feeCharges = BigDecimal.ZERO; - BigDecimal penaltyCharges = BigDecimal.ONE; - final Set compoundingDetails = null; - LoanRepaymentScheduleInstallment newEntry = new LoanRepaymentScheduleInstallment(loan, installments.size() + 1, - lastInstallment.getDueDate(), lastChargeDate, principal, interest, feeCharges, penaltyCharges, - recalculatedInterestComponent, compoundingDetails); - loan.addLoanRepaymentScheduleInstallment(newEntry); - } - } - } - - public LoanOverdueDTO applyChargeToOverdueLoanInstallment(final Loan loan, final Long loanChargeId, final Integer periodNumber, - final JsonCommand command) { - boolean runInterestRecalculation = false; - final Charge chargeDefinition = this.chargeRepository.findOneWithNotFoundDetection(loanChargeId); - - Collection frequencyNumbers = loanChargeReadPlatformService.retrieveOverdueInstallmentChargeFrequencyNumber(loan, - chargeDefinition, periodNumber); - - Integer feeFrequency = chargeDefinition.feeFrequency(); - final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator(); - Map scheduleDates = new HashMap<>(); - final Long penaltyWaitPeriodValue = this.configurationDomainService.retrievePenaltyWaitPeriod(); - final Long penaltyPostingWaitPeriodValue = this.configurationDomainService.retrieveGraceOnPenaltyPostingPeriod(); - final LocalDate dueDate = command.localDateValueOfParameterNamed("dueDate"); - long diff = penaltyWaitPeriodValue + 1 - penaltyPostingWaitPeriodValue; - if (diff < 1) { - diff = 1L; - } - LocalDate startDate = dueDate.plusDays(penaltyWaitPeriodValue + 1L); - int frequencyNumber = 1; - if (feeFrequency == null) { - scheduleDates.put(frequencyNumber++, startDate.minusDays(diff)); - } else { - while (!startDate.isAfter(DateUtils.getBusinessLocalDate())) { - scheduleDates.put(frequencyNumber++, startDate.minusDays(diff)); - - startDate = scheduledDateGenerator.getRepaymentPeriodDate(PeriodFrequencyType.fromInt(feeFrequency), - chargeDefinition.feeInterval(), startDate); - } - } - - for (Integer frequency : frequencyNumbers) { - scheduleDates.remove(frequency); - } - - LoanRepaymentScheduleInstallment installment = null; - LocalDate lastChargeAppliedDate = dueDate; - if (!scheduleDates.isEmpty()) { - installment = loan.fetchRepaymentScheduleInstallment(periodNumber); - lastChargeAppliedDate = installment.getDueDate(); - } - LocalDate recalculateFrom = DateUtils.getBusinessLocalDate(); - - if (loan != null) { - businessEventNotifierService.notifyPreBusinessEvent(new LoanApplyOverdueChargeBusinessEvent(loan)); - for (Map.Entry entry : scheduleDates.entrySet()) { - - final LoanCharge loanCharge = LoanCharge.createNewFromJson(loan, chargeDefinition, command, entry.getValue()); - - if (BigDecimal.ZERO.compareTo(loanCharge.amount()) == 0) { - continue; - } - LoanOverdueInstallmentCharge overdueInstallmentCharge = new LoanOverdueInstallmentCharge(loanCharge, installment, - entry.getKey()); - loanCharge.updateOverdueInstallmentCharge(overdueInstallmentCharge); - - boolean isAppliedOnBackDate = addCharge(loan, chargeDefinition, loanCharge); - runInterestRecalculation = runInterestRecalculation || isAppliedOnBackDate; - if (entry.getValue().isBefore(recalculateFrom)) { - recalculateFrom = entry.getValue(); - } - if (entry.getValue().isAfter(lastChargeAppliedDate)) { - lastChargeAppliedDate = entry.getValue(); - } - } - } - - return new LoanOverdueDTO(loan, runInterestRecalculation, recalculateFrom, lastChargeAppliedDate); - } - @Override public CommandProcessingResult undoWriteOff(Long loanId) { @@ -3036,7 +2054,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); - ChangedTransactionDetail changedTransactionDetail = loan.undoWrittenOff(defaultLoanLifecycleStateMachine(), existingTransactionIds, + ChangedTransactionDetail changedTransactionDetail = loan.undoWrittenOff(defaultLoanLifecycleStateMachine, existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO); if (changedTransactionDetail != null) { for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { @@ -3264,6 +2282,15 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf throw new RuntimeException(t); } + @Override + public void updateOriginalSchedule(Loan loan) { + if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { + final LocalDate recalculateFrom = null; + ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); + createLoanScheduleArchive(loan, scheduleGeneratorDTO); + } + } + private void updateLoanTransaction(final Long loanTransactionId, final LoanTransaction newLoanTransaction) { final AccountTransferTransaction transferTransaction = this.accountTransferRepository.findByToLoanTransactionId(loanTransactionId); if (transferTransaction != null) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java index db87368d7..bcdd51a9d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java @@ -46,9 +46,7 @@ import org.apache.fineract.portfolio.calendar.service.CalendarUtils; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; -import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanproduct.LoanProductConstants; -import org.apache.fineract.portfolio.loanproduct.data.TransactionProcessingStrategyData; import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod; import org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod; import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod; @@ -315,17 +313,9 @@ public final class LoanProductDataValidator { final String transactionProcessingStrategyCode = this.fromApiJsonHelper.extractStringNamed(TRANSACTION_PROCESSING_STRATEGY_CODE, element); - baseDataValidator.reset().parameter(TRANSACTION_PROCESSING_STRATEGY_CODE).value(transactionProcessingStrategyCode).notNull(); - - LoanRepaymentScheduleTransactionProcessor processor = loanRepaymentScheduleTransactionProcessorFactory - .determineProcessor(transactionProcessingStrategyCode); - - if (processor == null) { - Object[] values = loanRepaymentScheduleTransactionProcessorFactory.getStrategies().stream() - .map(TransactionProcessingStrategyData::getCode).toList().toArray(new String[0]); - - baseDataValidator.reset().parameter(TRANSACTION_PROCESSING_STRATEGY_CODE).isOneOfTheseValues(values); - } + baseDataValidator.reset().parameter(TRANSACTION_PROCESSING_STRATEGY_CODE).value(transactionProcessingStrategyCode).notBlank(); + // Validating whether the processor is existing + loanRepaymentScheduleTransactionProcessorFactory.determineProcessor(transactionProcessingStrategyCode); if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.DELINQUENCY_BUCKET_PARAM_NAME, element)) { final Long delinquencyBucketId = this.fromApiJsonHelper.extractLongNamed(LoanProductConstants.DELINQUENCY_BUCKET_PARAM_NAME, @@ -1093,8 +1083,9 @@ public final class LoanProductDataValidator { if (this.fromApiJsonHelper.parameterExists(TRANSACTION_PROCESSING_STRATEGY_CODE, element)) { final String transactionProcessingStrategyCode = this.fromApiJsonHelper.extractStringNamed(TRANSACTION_PROCESSING_STRATEGY_CODE, element); - baseDataValidator.reset().parameter(TRANSACTION_PROCESSING_STRATEGY_CODE).value(transactionProcessingStrategyCode).notNull() - .integerGreaterThanZero(); + baseDataValidator.reset().parameter(TRANSACTION_PROCESSING_STRATEGY_CODE).value(transactionProcessingStrategyCode).notBlank(); + // Validating whether the processor is existing + loanRepaymentScheduleTransactionProcessorFactory.determineProcessor(transactionProcessingStrategyCode); } // grace validation diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java index 8f2f01d61..972e3f49c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java @@ -289,6 +289,8 @@ public final class LoanEnumerations { LoanTransactionType.CHARGE_REFUND.getCode(), "Charge Refund"); case CHARGEBACK -> new LoanTransactionEnumData(LoanTransactionType.CHARGEBACK.getValue().longValue(), LoanTransactionType.CHARGEBACK.getCode(), "Chargeback"); + case CHARGE_ADJUSTMENT -> new LoanTransactionEnumData(LoanTransactionType.CHARGE_ADJUSTMENT.getValue().longValue(), + LoanTransactionType.CHARGE_ADJUSTMENT.getCode(), "Charge Adjustment"); }; } diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index 886cd6d04..c945903f4 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -87,4 +87,6 @@ + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0068_loan_charge_adjustment.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0068_loan_charge_adjustment.xml new file mode 100644 index 000000000..0204b516c --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0068_loan_charge_adjustment.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0069_add_unique_constraint_for_reversal_external_id_of_loan_transactions.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0069_add_unique_constraint_for_reversal_external_id_of_loan_transactions.xml new file mode 100644 index 000000000..a84cf0d53 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0069_add_unique_constraint_for_reversal_external_id_of_loan_transactions.xml @@ -0,0 +1,29 @@ + + + + + + + diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java index 85b98499f..3c3599429 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java @@ -88,7 +88,8 @@ public class ExternalEventConfigurationValidationServiceTest { "RecurringDepositAccountCreateBusinessEvent", "SavingsActivateBusinessEvent", "SavingsApproveBusinessEvent", "SavingsCloseBusinessEvent", "SavingsCreateBusinessEvent", "SavingsDepositBusinessEvent", "SavingsPostInterestBusinessEvent", "SavingsRejectBusinessEvent", "SavingsWithdrawalBusinessEvent", - "ShareAccountApproveBusinessEvent", "ShareAccountCreateBusinessEvent", "ShareProductDividentsCreateBusinessEvent"); + "ShareAccountApproveBusinessEvent", "ShareAccountCreateBusinessEvent", "ShareProductDividentsCreateBusinessEvent", + "LoanChargeAdjustmentPostBusinessEvent", "LoanChargeAdjustmentPreBusinessEvent"); List tenants = Arrays .asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null)); @@ -160,7 +161,8 @@ public class ExternalEventConfigurationValidationServiceTest { "RecurringDepositAccountCreateBusinessEvent", "SavingsActivateBusinessEvent", "SavingsApproveBusinessEvent", "SavingsCloseBusinessEvent", "SavingsCreateBusinessEvent", "SavingsDepositBusinessEvent", "SavingsPostInterestBusinessEvent", "SavingsRejectBusinessEvent", "SavingsWithdrawalBusinessEvent", - "ShareAccountApproveBusinessEvent", "ShareAccountCreateBusinessEvent", "ShareProductDividentsCreateBusinessEvent"); + "ShareAccountApproveBusinessEvent", "ShareAccountCreateBusinessEvent", "ShareProductDividentsCreateBusinessEvent", + "LoanChargeAdjustmentPostBusinessEvent", "LoanChargeAdjustmentPreBusinessEvent"); List tenants = Arrays .asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null)); diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachineTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachineTest.java index 56f1443be..c901e7d34 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachineTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachineTest.java @@ -41,7 +41,7 @@ class DefaultLoanLifecycleStateMachineTest { @BeforeEach public void setUp() { - underTest = new DefaultLoanLifecycleStateMachine(LoanStatus.values(), businessEventNotifierService); + underTest = new DefaultLoanLifecycleStateMachine(businessEventNotifierService); } @Test diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java index 54276d6c0..d06b5a878 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java @@ -20,6 +20,8 @@ package org.apache.fineract.integrationtests; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import com.google.gson.Gson; import com.google.gson.JsonObject; @@ -45,6 +47,28 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.UUID; +import org.apache.fineract.accounting.glaccount.domain.GLAccountType; +import org.apache.fineract.client.models.BusinessDateRequest; +import org.apache.fineract.client.models.GetLoanTransactionRelation; +import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod; +import org.apache.fineract.client.models.GetLoansLoanIdResponse; +import org.apache.fineract.client.models.GetLoansLoanIdSummary; +import org.apache.fineract.client.models.GetLoansLoanIdTransactions; +import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTransactionIdResponse; +import org.apache.fineract.client.models.PostChargesRequest; +import org.apache.fineract.client.models.PostChargesResponse; +import org.apache.fineract.client.models.PostClientsResponse; +import org.apache.fineract.client.models.PostGLAccountsRequest; +import org.apache.fineract.client.models.PostGLAccountsResponse; +import org.apache.fineract.client.models.PostLoansLoanIdChargesChargeIdRequest; +import org.apache.fineract.client.models.PostLoansLoanIdChargesChargeIdResponse; +import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; +import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; +import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest; +import org.apache.fineract.client.util.CallFailedRuntimeException; +import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; +import org.apache.fineract.integrationtests.common.BusinessDateHelper; import org.apache.fineract.integrationtests.common.ClientHelper; import org.apache.fineract.integrationtests.common.CollateralManagementHelper; import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper; @@ -64,6 +88,9 @@ import org.apache.fineract.integrationtests.common.savings.AccountTransferHelper import org.apache.fineract.integrationtests.common.savings.SavingsAccountHelper; import org.apache.fineract.integrationtests.common.savings.SavingsProductHelper; import org.apache.fineract.integrationtests.common.savings.SavingsStatusChecker; +import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType; +import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode; +import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -77,10 +104,9 @@ import org.slf4j.LoggerFactory; @SuppressWarnings({ "rawtypes", "unchecked" }) public class ClientLoanIntegrationTest { - private static final Logger LOG = LoggerFactory.getLogger(ClientLoanIntegrationTest.class); - public static final String MINIMUM_OPENING_BALANCE = "1000.0"; public static final String ACCOUNT_TYPE_INDIVIDUAL = "INDIVIDUAL"; + private static final Logger LOG = LoggerFactory.getLogger(ClientLoanIntegrationTest.class); private static final String NONE = "1"; private static final String CASH_BASED = "2"; private static final String ACCRUAL_PERIODIC = "3"; @@ -97,6 +123,9 @@ public class ClientLoanIntegrationTest { private AccountTransferHelper accountTransferHelper; private DateTimeFormatter dateFormatter = new DateTimeFormatterBuilder().appendPattern("dd MMMM yyyy").toFormatter(); + private BusinessDateHelper businessDateHelper; + private ChargesHelper chargesHelper; + private ClientHelper clientHelper; @BeforeEach public void setup() { @@ -111,6 +140,9 @@ public class ClientLoanIntegrationTest { this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec); this.periodicAccrualAccountingHelper = new PeriodicAccrualAccountingHelper(this.requestSpec, this.responseSpec); this.journalEntryHelper = new JournalEntryHelper(this.requestSpec, this.responseSpec); + this.businessDateHelper = new BusinessDateHelper(); + this.chargesHelper = new ChargesHelper(); + this.clientHelper = new ClientHelper(this.requestSpec, this.responseSpec); } @Test @@ -1112,39 +1144,37 @@ public class ClientLoanIntegrationTest { private void validateCharge(Integer amountPercentage, final List loanCharges, final String amount, final String outstanding, String amountPaid, String amountWaived) { HashMap chargeDetail = getloanCharge(amountPercentage, loanCharges); - Assertions.assertTrue(Float.valueOf(amount).compareTo(Float.valueOf(String.valueOf(chargeDetail.get("amountOrPercentage")))) == 0); - Assertions.assertTrue( - Float.valueOf(outstanding).compareTo(Float.valueOf(String.valueOf(chargeDetail.get("amountOutstanding")))) == 0); - Assertions.assertTrue(Float.valueOf(amountPaid).compareTo(Float.valueOf(String.valueOf(chargeDetail.get("amountPaid")))) == 0); - Assertions.assertTrue(Float.valueOf(amountWaived).compareTo(Float.valueOf(String.valueOf(chargeDetail.get("amountWaived")))) == 0); + assertTrue(Float.valueOf(amount).compareTo(Float.valueOf(String.valueOf(chargeDetail.get("amountOrPercentage")))) == 0); + assertTrue(Float.valueOf(outstanding).compareTo(Float.valueOf(String.valueOf(chargeDetail.get("amountOutstanding")))) == 0); + assertTrue(Float.valueOf(amountPaid).compareTo(Float.valueOf(String.valueOf(chargeDetail.get("amountPaid")))) == 0); + assertTrue(Float.valueOf(amountWaived).compareTo(Float.valueOf(String.valueOf(chargeDetail.get("amountWaived")))) == 0); } private void validateChargeExcludePrecission(Integer amountPercentage, final List loanCharges, final String amount, final String outstanding, String amountPaid, String amountWaived) { DecimalFormat twoDForm = new DecimalFormat("#"); HashMap chargeDetail = getloanCharge(amountPercentage, loanCharges); - Assertions.assertTrue(Float.valueOf(twoDForm.format(Float.valueOf(amount))) + assertTrue(Float.valueOf(twoDForm.format(Float.valueOf(amount))) .compareTo(Float.parseFloat(twoDForm.format(Float.valueOf(String.valueOf(chargeDetail.get("amountOrPercentage")))))) == 0); - Assertions.assertTrue(Float.valueOf(twoDForm.format(Float.valueOf(outstanding))) + assertTrue(Float.valueOf(twoDForm.format(Float.valueOf(outstanding))) .compareTo(Float.valueOf(twoDForm.format(Float.parseFloat(String.valueOf(chargeDetail.get("amountOutstanding")))))) == 0); - Assertions.assertTrue(Float.valueOf(twoDForm.format(Float.parseFloat(amountPaid))) + assertTrue(Float.valueOf(twoDForm.format(Float.parseFloat(amountPaid))) .compareTo(Float.valueOf(twoDForm.format(Float.parseFloat(String.valueOf(chargeDetail.get("amountPaid")))))) == 0); - Assertions.assertTrue(Float.valueOf(twoDForm.format(Float.parseFloat(amountWaived))) + assertTrue(Float.valueOf(twoDForm.format(Float.parseFloat(amountWaived))) .compareTo(Float.valueOf(twoDForm.format(Float.parseFloat(String.valueOf(chargeDetail.get("amountWaived")))))) == 0); } private void validateNumberForEqual(String val, String val2) { - Assertions.assertTrue(Float.valueOf(val).compareTo(Float.valueOf(val2)) == 0); + assertTrue(Float.valueOf(val).compareTo(Float.valueOf(val2)) == 0); } private void validateNumberForEqualWithMsg(String msg, String val, String val2) { - Assertions.assertTrue(Float.valueOf(val).compareTo(Float.valueOf(val2)) == 0, msg + "expected " + val + " but was " + val2); + assertTrue(Float.valueOf(val).compareTo(Float.valueOf(val2)) == 0, msg + "expected " + val + " but was " + val2); } private void validateNumberForEqualExcludePrecission(String val, String val2) { DecimalFormat twoDForm = new DecimalFormat("#"); - Assertions.assertTrue( - Float.valueOf(twoDForm.format(Float.valueOf(val))).compareTo(Float.valueOf(twoDForm.format(Float.valueOf(val2)))) == 0, + assertTrue(Float.valueOf(twoDForm.format(Float.valueOf(val))).compareTo(Float.valueOf(twoDForm.format(Float.valueOf(val2)))) == 0, String.format("%s is not equal to %s", val, val2)); } @@ -5286,7 +5316,7 @@ public class ClientLoanIntegrationTest { HashMap modifications = this.savingsAccountHelper.updateSavingsAccount(clientID, savingsProductID, savingsId, ACCOUNT_TYPE_INDIVIDUAL); - Assertions.assertTrue(modifications.containsKey("submittedOnDate")); + assertTrue(modifications.containsKey("submittedOnDate")); HashMap savingsStatusHashMap = SavingsStatusChecker.getStatusOfSavings(this.requestSpec, this.responseSpec, savingsId); SavingsStatusChecker.verifySavingsIsPending(savingsStatusHashMap); @@ -5991,6 +6021,632 @@ public class ClientLoanIntegrationTest { ((Map) ((List) response.get("errors")).get(0)).get("defaultUserMessage")); } + @Test + public void chargeAdjustmentChargeWrongParams() { + CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class, + () -> loanTransactionHelper.chargeAdjustment(0L, 0L, new PostLoansLoanIdChargesChargeIdRequest().amount(0.0))); + assertEquals(400, exception.getResponse().code()); + assertTrue(exception.getMessage().contains("validation.msg.loan.charge.adjustment.request.amount.not.greater.than.zero")); + assertTrue(exception.getMessage().contains("validation.msg.loan.charge.adjustment.request.loanId.not.greater.than.zero")); + assertTrue(exception.getMessage().contains("validation.msg.loan.charge.adjustment.request.loanChargeId.not.greater.than.zero")); + exception = assertThrows(CallFailedRuntimeException.class, + () -> loanTransactionHelper.chargeAdjustment(1L, 0L, new PostLoansLoanIdChargesChargeIdRequest().amount(0.0))); + assertEquals(400, exception.getResponse().code()); + assertTrue(exception.getMessage().contains("validation.msg.loan.charge.adjustment.request.amount.not.greater.than.zero")); + assertTrue(exception.getMessage().contains("validation.msg.loan.charge.adjustment.request.loanChargeId.not.greater.than.zero")); + exception = assertThrows(CallFailedRuntimeException.class, + () -> loanTransactionHelper.chargeAdjustment(1L, 1L, new PostLoansLoanIdChargesChargeIdRequest().amount(0.0))); + assertEquals(400, exception.getResponse().code()); + assertTrue(exception.getMessage().contains("validation.msg.loan.charge.adjustment.request.amount.not.greater.than.zero")); + } + + @Test + public void chargeAdjustmentChargeDoesNotExist() { + final Account assetAccount = this.accountHelper.createAssetAccount(); + final Account incomeAccount = this.accountHelper.createIncomeAccount(); + final Account expenseAccount = this.accountHelper.createExpenseAccount(); + final Account overpaymentAccount = this.accountHelper.createLiabilityAccount(); + + final Integer loanProductID = createLoanProductWithPeriodicAccrualAccountingNoInterest(assetAccount, incomeAccount, expenseAccount, + overpaymentAccount); + + final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec, "01 January 2011"); + + final Integer loanID = applyForLoanApplication(clientID, loanProductID); + + CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class, + () -> loanTransactionHelper.chargeAdjustment((long) loanID, 1L, new PostLoansLoanIdChargesChargeIdRequest().amount(1.0))); + assertEquals(404, exception.getResponse().code()); + assertTrue(exception.getMessage().contains("error.msg.loanCharge.id.invalid")); + } + + @Test + public void chargeAdjustmentChargeDoesNotExistForLoan() { + final Account assetAccount = this.accountHelper.createAssetAccount(); + final Account incomeAccount = this.accountHelper.createIncomeAccount(); + final Account expenseAccount = this.accountHelper.createExpenseAccount(); + final Account overpaymentAccount = this.accountHelper.createLiabilityAccount(); + + final Integer loanProductID = createLoanProductWithPeriodicAccrualAccountingNoInterest(assetAccount, incomeAccount, expenseAccount, + overpaymentAccount); + + final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec, "01 January 2011"); + + final Integer loanID = applyForLoanApplication(clientID, loanProductID); + + HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID); + LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); + + loanStatusHashMap = this.loanTransactionHelper.approveLoan("02 September 2022", loanID); + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap); + + loanStatusHashMap = this.loanTransactionHelper.disburseLoanWithNetDisbursalAmount("03 September 2022", loanID, "1000"); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + + Integer penalty = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "10", true)); + LocalDate targetDate = LocalDate.of(2022, 9, 7); + final String penaltyCharge1AddedDate = dateFormatter.format(targetDate); + Integer penalty1LoanChargeId = this.loanTransactionHelper.addChargesForLoan(loanID, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), penaltyCharge1AddedDate, "10")); + + final Integer loanID2 = applyForLoanApplication(clientID, loanProductID); + + CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class, () -> loanTransactionHelper + .chargeAdjustment((long) loanID2, (long) penalty1LoanChargeId, new PostLoansLoanIdChargesChargeIdRequest().amount(1.0))); + assertEquals(404, exception.getResponse().code()); + assertTrue(exception.getMessage().contains("error.msg.loanCharge.id.invalid.for.given.loan")); + } + + @Test + public void chargeAdjustmentForUnpaidCharge() { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()).date("2022.11.01") + .dateFormat("yyyy.MM.dd").locale("en")); + final Account assetAccount = this.accountHelper.createAssetAccount(); + final Account incomeAccount = this.accountHelper.createIncomeAccount(); + final Account expenseAccount = this.accountHelper.createExpenseAccount(); + final Account overpaymentAccount = this.accountHelper.createLiabilityAccount(); + + Integer penalty = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "10", true)); + final Integer loanProductID = createLoanProductWithPeriodicAccrualAccountingNoInterest(assetAccount, incomeAccount, expenseAccount, + overpaymentAccount); + + final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec, "01 January 2011"); + + final Integer loanID = applyForLoanApplication(clientID, loanProductID); + + HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID); + LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); + + loanStatusHashMap = this.loanTransactionHelper.approveLoan("02 September 2022", loanID); + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap); + + loanStatusHashMap = this.loanTransactionHelper.disburseLoanWithNetDisbursalAmount("03 September 2022", loanID, "1000"); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + + ArrayList loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(requestSpec, responseSpec, loanID); + assertEquals(2, loanSchedule.size()); + assertEquals(0, loanSchedule.get(1).get("penaltyChargesDue")); + assertEquals(0, loanSchedule.get(1).get("penaltyChargesOutstanding")); + assertEquals(1000.0f, loanSchedule.get(1).get("totalDueForPeriod")); + assertEquals(1000.0f, loanSchedule.get(1).get("totalOutstandingForPeriod")); + LocalDate targetDate = LocalDate.of(2022, 9, 7); + final String penaltyCharge1AddedDate = dateFormatter.format(targetDate); + Integer penalty1LoanChargeId = this.loanTransactionHelper.addChargesForLoan(loanID, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), penaltyCharge1AddedDate, "10")); + + this.loanTransactionHelper.noAccrualTransactionForRepayment(loanID); + + loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(requestSpec, responseSpec, loanID); + assertEquals(2, loanSchedule.size()); + assertEquals(10.0f, loanSchedule.get(1).get("penaltyChargesDue")); + assertEquals(10.0f, loanSchedule.get(1).get("penaltyChargesOutstanding")); + assertEquals(1010.0f, loanSchedule.get(1).get("totalDueForPeriod")); + assertEquals(1010.0f, loanSchedule.get(1).get("totalOutstandingForPeriod")); + assertEquals(0, loanSchedule.get(1).get("totalWaivedForPeriod")); + + HashMap loanSummary = this.loanTransactionHelper.getLoanDetail(this.requestSpec, this.responseSpec, loanID, "summary"); + assertEquals(10.0f, loanSummary.get("penaltyChargesCharged")); + assertEquals(10.0f, loanSummary.get("penaltyChargesOutstanding")); + assertEquals(0.0f, loanSummary.get("penaltyChargesWaived")); + assertEquals(1010.0f, loanSummary.get("totalOutstanding")); + assertEquals(0.0f, loanSummary.get("totalWaived")); + + String externalId = UUID.randomUUID().toString(); + PostLoansLoanIdChargesChargeIdResponse chargeAdjustmentResponse = this.loanTransactionHelper.chargeAdjustment((long) loanID, + (long) penalty1LoanChargeId, new PostLoansLoanIdChargesChargeIdRequest().amount(10.0).externalId(externalId)); + + loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(requestSpec, responseSpec, loanID); + assertEquals(2, loanSchedule.size()); + assertEquals(10.0f, loanSchedule.get(1).get("penaltyChargesDue")); + assertEquals(10.0f, loanSchedule.get(1).get("penaltyChargesPaid")); + assertEquals(0.0f, loanSchedule.get(1).get("penaltyChargesOutstanding")); + assertEquals(1010.0f, loanSchedule.get(1).get("totalDueForPeriod")); + assertEquals(1000.0f, loanSchedule.get(1).get("totalOutstandingForPeriod")); + assertEquals(10.0f, loanSchedule.get(1).get("totalPaidForPeriod")); + + loanSummary = this.loanTransactionHelper.getLoanDetail(this.requestSpec, this.responseSpec, loanID, "summary"); + assertEquals(10.0f, loanSummary.get("penaltyChargesCharged")); + assertEquals(0.0f, loanSummary.get("penaltyChargesOutstanding")); + assertEquals(10.0f, loanSummary.get("penaltyChargesPaid")); + assertEquals(1000.0f, loanSummary.get("totalOutstanding")); + + GetLoansLoanIdTransactionsTransactionIdResponse chargeAdjustmentTransaction = this.loanTransactionHelper + .getLoanTransactionDetails((long) loanID, chargeAdjustmentResponse.getResourceId()); + assertEquals(10.0, chargeAdjustmentTransaction.getAmount()); + assertEquals(10.0, chargeAdjustmentTransaction.getPenaltyChargesPortion()); + assertEquals("loanTransactionType.chargeAdjustment", chargeAdjustmentTransaction.getType().getCode()); + assertEquals(externalId, chargeAdjustmentTransaction.getExternalId()); + GetLoanTransactionRelation transactionRelation = chargeAdjustmentTransaction.getTransactionRelations().iterator().next(); + assertEquals(chargeAdjustmentResponse.getResourceId(), transactionRelation.getFromLoanTransaction()); + assertEquals((long) penalty1LoanChargeId, transactionRelation.getToLoanCharge()); + assertEquals("CHARGE_ADJUSTMENT", transactionRelation.getRelationType()); + + PostLoansLoanIdTransactionsResponse repaymentResult = loanTransactionHelper.makeLoanRepayment((long) loanID, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("06 September 2022").locale("en") + .transactionAmount(5.0)); + + loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(requestSpec, responseSpec, loanID); + assertEquals(2, loanSchedule.size()); + assertEquals(10.0f, loanSchedule.get(1).get("penaltyChargesDue")); + assertEquals(10.0f, loanSchedule.get(1).get("penaltyChargesPaid")); + assertEquals(0.0f, loanSchedule.get(1).get("penaltyChargesOutstanding")); + assertEquals(1000.0f, loanSchedule.get(1).get("principalDue")); + assertEquals(5.0f, loanSchedule.get(1).get("principalPaid")); + assertEquals(995.0f, loanSchedule.get(1).get("principalOutstanding")); + assertEquals(1010.0f, loanSchedule.get(1).get("totalDueForPeriod")); + assertEquals(995.0f, loanSchedule.get(1).get("totalOutstandingForPeriod")); + assertEquals(15.0f, loanSchedule.get(1).get("totalPaidForPeriod")); + + loanSummary = this.loanTransactionHelper.getLoanDetail(this.requestSpec, this.responseSpec, loanID, "summary"); + assertEquals(10.0f, loanSummary.get("penaltyChargesCharged")); + assertEquals(0.0f, loanSummary.get("penaltyChargesOutstanding")); + assertEquals(10.0f, loanSummary.get("penaltyChargesPaid")); + assertEquals(1000.0f, loanSummary.get("principalDisbursed")); + assertEquals(995.0f, loanSummary.get("principalOutstanding")); + assertEquals(5.0f, loanSummary.get("principalPaid")); + assertEquals(995.0f, loanSummary.get("totalOutstanding")); + + Long chargeAdjustmentTxnId = repaymentResult.getResourceId() + 1; + chargeAdjustmentTransaction = this.loanTransactionHelper.getLoanTransactionDetails((long) loanID, chargeAdjustmentTxnId); + assertEquals(10.0, chargeAdjustmentTransaction.getAmount()); + assertEquals(5.0, chargeAdjustmentTransaction.getPenaltyChargesPortion()); + assertEquals(5.0, chargeAdjustmentTransaction.getPrincipalPortion()); + assertEquals("loanTransactionType.chargeAdjustment", chargeAdjustmentTransaction.getType().getCode()); + assertEquals(externalId, chargeAdjustmentTransaction.getExternalId()); + transactionRelation = chargeAdjustmentTransaction.getTransactionRelations().iterator().next(); + assertEquals(chargeAdjustmentTxnId, transactionRelation.getFromLoanTransaction()); + assertEquals((long) penalty1LoanChargeId, transactionRelation.getToLoanCharge()); + assertEquals("CHARGE_ADJUSTMENT", transactionRelation.getRelationType()); + + String uuid = UUID.randomUUID().toString(); + this.loanTransactionHelper.reverseLoanTransaction((long) loanID, chargeAdjustmentTxnId, + new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat("dd MMMM yyyy").transactionDate("08 September 2022") + .transactionAmount(0.0).locale("en").reversalExternalId(uuid)); + + // Should fail due to external id collusion + assertThrows(CallFailedRuntimeException.class, + () -> this.loanTransactionHelper.reverseLoanTransaction((long) loanID, repaymentResult.getResourceId(), + new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat("dd MMMM yyyy") + .transactionDate("08 September 2022").transactionAmount(0.0).locale("en").reversalExternalId(uuid))); + + loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(requestSpec, responseSpec, loanID); + assertEquals(2, loanSchedule.size()); + assertEquals(10.0f, loanSchedule.get(1).get("penaltyChargesDue")); + assertEquals(5.0f, loanSchedule.get(1).get("penaltyChargesPaid")); + assertEquals(5.0f, loanSchedule.get(1).get("penaltyChargesOutstanding")); + assertEquals(1000.0f, loanSchedule.get(1).get("principalDue")); + assertEquals(0, loanSchedule.get(1).get("principalPaid")); + assertEquals(1000.0f, loanSchedule.get(1).get("principalOutstanding")); + assertEquals(1010.0f, loanSchedule.get(1).get("totalDueForPeriod")); + assertEquals(1005.0f, loanSchedule.get(1).get("totalOutstandingForPeriod")); + assertEquals(5.0f, loanSchedule.get(1).get("totalPaidForPeriod")); + + loanSummary = this.loanTransactionHelper.getLoanDetail(this.requestSpec, this.responseSpec, loanID, "summary"); + assertEquals(10.0f, loanSummary.get("penaltyChargesCharged")); + assertEquals(5.0f, loanSummary.get("penaltyChargesOutstanding")); + assertEquals(5.0f, loanSummary.get("penaltyChargesPaid")); + assertEquals(1000.0f, loanSummary.get("principalDisbursed")); + assertEquals(1000.0f, loanSummary.get("principalOutstanding")); + assertEquals(0.0f, loanSummary.get("principalPaid")); + assertEquals(1005.0f, loanSummary.get("totalOutstanding")); + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + + @Test + public void chargeAdjustmentAccountingValidation() { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()).date("2022.11.01") + .dateFormat("yyyy.MM.dd").locale("en")); + final Account assetAccount = this.accountHelper.createAssetAccount(); + final Account assetFeeAndPenaltyAccount = this.accountHelper.createAssetAccount(); + final Account incomeAccount = this.accountHelper.createIncomeAccount(); + final Account expenseAccount = this.accountHelper.createExpenseAccount(); + final Account overpaymentAccount = this.accountHelper.createLiabilityAccount(); + final PostGLAccountsResponse uniqueIncomeAccountForFee = this.accountHelper + .createGLAccount(new PostGLAccountsRequest().type(GLAccountType.INCOME.getValue()) + .glCode(Utils.randomStringGenerator("UNIQUE_FEE_INCOME" + Calendar.getInstance().getTimeInMillis(), 5)) + .manualEntriesAllowed(true) + .name(Utils.randomStringGenerator("UNIQUE_FEE_INCOME" + Calendar.getInstance().getTimeInMillis(), 5)).usage(1)); + final PostGLAccountsResponse uniqueIncomeAccountForPenalty = this.accountHelper + .createGLAccount(new PostGLAccountsRequest().type(GLAccountType.INCOME.getValue()) + .glCode(Utils.randomStringGenerator("UNIQUE_PENALTY_INCOME" + Calendar.getInstance().getTimeInMillis(), 5)) + .manualEntriesAllowed(true) + .name(Utils.randomStringGenerator("UNIQUE_PENALTY_INCOME" + Calendar.getInstance().getTimeInMillis(), 5)).usage(1)); + + PostChargesResponse penaltyCharge = chargesHelper.createCharges(new PostChargesRequest().penalty(true).amount(10.0) + .chargeCalculationType(ChargeCalculationType.FLAT.getValue()).chargeTimeType(ChargeTimeType.SPECIFIED_DUE_DATE.getValue()) + .chargePaymentMode(ChargePaymentMode.REGULAR.getValue()).currencyCode("USD") + .name(Utils.randomStringGenerator("PENALTY_" + Calendar.getInstance().getTimeInMillis(), 5)).chargeAppliesTo(1).locale("en") + .active(true)); + + PostChargesResponse feeCharge = chargesHelper.createCharges(new PostChargesRequest().penalty(false).amount(9.0) + .chargeCalculationType(ChargeCalculationType.FLAT.getValue()).chargeTimeType(ChargeTimeType.SPECIFIED_DUE_DATE.getValue()) + .chargePaymentMode(ChargePaymentMode.REGULAR.getValue()).currencyCode("USD") + .name(Utils.randomStringGenerator("FEE_" + Calendar.getInstance().getTimeInMillis(), 5)).chargeAppliesTo(1).locale("en") + .active(true)); + + final String loanProductJSON = new LoanProductTestBuilder().withPrincipal("1000").withRepaymentTypeAsMonth() + .withRepaymentAfterEvery("1").withNumberOfRepayments("1").withRepaymentTypeAsMonth().withinterestRatePerPeriod("0") + .withInterestRateFrequencyTypeAsMonths().withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsFlat() + .withAccountingRulePeriodicAccrual(new Account[] { assetAccount, incomeAccount, expenseAccount, overpaymentAccount }) + .withDaysInMonth("30").withDaysInYear("365").withMoratorium("0", "0") + .withFeeToIncomeAccountMapping(feeCharge.getResourceId(), uniqueIncomeAccountForFee.getResourceId()) + .withPenaltyToIncomeAccountMapping(penaltyCharge.getResourceId(), uniqueIncomeAccountForPenalty.getResourceId()) + .withFeeAndPenaltyAssetAccount(assetFeeAndPenaltyAccount).build(null); + final Integer loanProductID = this.loanTransactionHelper.getLoanProductId(loanProductJSON); + + final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()); + + final Integer loanID = applyForLoanApplication(client.getClientId().intValue(), loanProductID); + + HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID); + LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); + + loanStatusHashMap = this.loanTransactionHelper.approveLoan("02 September 2022", loanID); + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap); + + loanStatusHashMap = this.loanTransactionHelper.disburseLoanWithNetDisbursalAmount("03 September 2022", loanID, "1000"); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + + GetLoansLoanIdResponse loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanID); + List loanSchedulePeriods = loanDetails.getRepaymentSchedule().getPeriods(); + assertEquals(2, loanSchedulePeriods.size()); + assertEquals(0.0, loanSchedulePeriods.get(1).getPenaltyChargesDue()); + assertEquals(0.0, loanSchedulePeriods.get(1).getPenaltyChargesOutstanding()); + assertEquals(1000.0, loanSchedulePeriods.get(1).getTotalDueForPeriod()); + assertEquals(1000.0, loanSchedulePeriods.get(1).getTotalOutstandingForPeriod()); + + LocalDate targetDate = LocalDate.of(2022, 9, 7); + final String penaltyCharge1AddedDate = dateFormatter.format(targetDate); + Integer penaltyLoanChargeId = this.loanTransactionHelper.addChargesForLoan(loanID, LoanTransactionHelper + .getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penaltyCharge.getResourceId()), penaltyCharge1AddedDate, "10")); + + final String penalty1LoanChargeDate = dateFormatter.format(targetDate); + this.periodicAccrualAccountingHelper.runPeriodicAccrualAccounting(penalty1LoanChargeDate); + + loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanID); + List transactions = loanDetails.getTransactions(); + assertEquals(10.0, transactions.get(1).getAmount()); + assertTrue(transactions.get(1).getType().getAccrual()); + assertEquals(10.0, transactions.get(1).getPenaltyChargesPortion()); + Long accrualTransactionId = transactions.get(1).getId(); + + List journalEntries = this.journalEntryHelper.getJournalEntriesByTransactionId("L" + accrualTransactionId); + assertEquals(10.0f, (float) journalEntries.get(0).get("amount")); + assertEquals(uniqueIncomeAccountForPenalty.getResourceId().intValue(), (int) journalEntries.get(0).get("glAccountId")); + assertEquals("CREDIT", ((HashMap) journalEntries.get(0).get("entryType")).get("value")); + assertEquals(10.0f, (float) journalEntries.get(1).get("amount")); + assertEquals(assetFeeAndPenaltyAccount.getAccountID(), (int) journalEntries.get(1).get("glAccountId")); + assertEquals("DEBIT", ((HashMap) journalEntries.get(1).get("entryType")).get("value")); + + loanSchedulePeriods = loanDetails.getRepaymentSchedule().getPeriods(); + assertEquals(2, loanSchedulePeriods.size()); + assertEquals(10.0, loanSchedulePeriods.get(1).getPenaltyChargesDue()); + assertEquals(10.0, loanSchedulePeriods.get(1).getPenaltyChargesOutstanding()); + assertEquals(1010.0, loanSchedulePeriods.get(1).getTotalDueForPeriod()); + assertEquals(1010.0, loanSchedulePeriods.get(1).getTotalOutstandingForPeriod()); + + GetLoansLoanIdSummary loanSummary = loanDetails.getSummary(); + assertEquals(10.0, loanSummary.getPenaltyChargesCharged()); + assertEquals(10.0, loanSummary.getPenaltyChargesOutstanding()); + assertEquals(1010.0, loanSummary.getTotalOutstanding()); + + String externalId = UUID.randomUUID().toString(); + PostLoansLoanIdChargesChargeIdResponse chargeAdjustmentResponse = this.loanTransactionHelper.chargeAdjustment((long) loanID, + (long) penaltyLoanChargeId, new PostLoansLoanIdChargesChargeIdRequest().amount(10.0).externalId(externalId)); + + loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanID); + + loanSchedulePeriods = loanDetails.getRepaymentSchedule().getPeriods(); + assertEquals(2, loanSchedulePeriods.size()); + assertEquals(10.0, loanSchedulePeriods.get(1).getPenaltyChargesDue()); + assertEquals(10.0, loanSchedulePeriods.get(1).getPenaltyChargesPaid()); + assertEquals(0.0, loanSchedulePeriods.get(1).getPenaltyChargesOutstanding()); + assertEquals(1010.0, loanSchedulePeriods.get(1).getTotalDueForPeriod()); + assertEquals(1000.0, loanSchedulePeriods.get(1).getTotalOutstandingForPeriod()); + assertEquals(10.0, loanSchedulePeriods.get(1).getTotalPaidForPeriod()); + + loanSummary = loanDetails.getSummary(); + assertEquals(10.0, loanSummary.getPenaltyChargesCharged()); + assertEquals(0.0, loanSummary.getPenaltyChargesOutstanding()); + assertEquals(10.0, loanSummary.getPenaltyChargesPaid()); + assertEquals(1000.0, loanSummary.getTotalOutstanding()); + + transactions = loanDetails.getTransactions(); + assertEquals(10.0, transactions.get(2).getAmount()); + assertTrue(transactions.get(2).getType().getChargeAdjustment()); + assertEquals(10.0, transactions.get(2).getPenaltyChargesPortion()); + Long chargeAdjustmentTransactionId = transactions.get(2).getId(); + + journalEntries = this.journalEntryHelper.getJournalEntriesByTransactionId("L" + chargeAdjustmentTransactionId); + assertEquals(10.0f, (float) journalEntries.get(0).get("amount")); + assertEquals(uniqueIncomeAccountForPenalty.getResourceId().intValue(), (int) journalEntries.get(0).get("glAccountId")); + assertEquals("DEBIT", ((HashMap) journalEntries.get(0).get("entryType")).get("value")); + assertEquals(10.0f, (float) journalEntries.get(1).get("amount")); + assertEquals(assetFeeAndPenaltyAccount.getAccountID(), (int) journalEntries.get(1).get("glAccountId")); + assertEquals("CREDIT", ((HashMap) journalEntries.get(1).get("entryType")).get("value")); + + String uuid = UUID.randomUUID().toString(); + this.loanTransactionHelper.reverseLoanTransaction((long) loanID, chargeAdjustmentTransactionId, + new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat("dd MMMM yyyy").transactionDate("08 September 2022") + .transactionAmount(0.0).locale("en").reversalExternalId(uuid)); + + journalEntries = this.journalEntryHelper.getJournalEntriesByTransactionId("L" + chargeAdjustmentTransactionId); + assertEquals(10.0f, (float) journalEntries.get(0).get("amount")); + assertEquals(uniqueIncomeAccountForPenalty.getResourceId().intValue(), (int) journalEntries.get(0).get("glAccountId")); + assertEquals("CREDIT", ((HashMap) journalEntries.get(0).get("entryType")).get("value")); + assertEquals(10.0f, (float) journalEntries.get(1).get("amount")); + assertEquals(assetFeeAndPenaltyAccount.getAccountID(), (int) journalEntries.get(1).get("glAccountId")); + assertEquals("DEBIT", ((HashMap) journalEntries.get(1).get("entryType")).get("value")); + assertEquals(10.0f, (float) journalEntries.get(2).get("amount")); + assertEquals(uniqueIncomeAccountForPenalty.getResourceId().intValue(), (int) journalEntries.get(2).get("glAccountId")); + assertEquals("DEBIT", ((HashMap) journalEntries.get(2).get("entryType")).get("value")); + assertEquals(10.0f, (float) journalEntries.get(3).get("amount")); + assertEquals(assetFeeAndPenaltyAccount.getAccountID(), (int) journalEntries.get(3).get("glAccountId")); + assertEquals("CREDIT", ((HashMap) journalEntries.get(3).get("entryType")).get("value")); + + targetDate = LocalDate.of(2022, 9, 10); + final String feeCharge1AddedDate = dateFormatter.format(targetDate); + Integer feeLoanChargeId = this.loanTransactionHelper.addChargesForLoan(loanID, LoanTransactionHelper + .getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(feeCharge.getResourceId()), feeCharge1AddedDate, "3")); + + final String feeLoanChargeDate = dateFormatter.format(targetDate); + this.periodicAccrualAccountingHelper.runPeriodicAccrualAccounting(feeLoanChargeDate); + + loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanID); + transactions = loanDetails.getTransactions(); + assertEquals(3.0, transactions.get(2).getAmount()); + assertTrue(transactions.get(2).getType().getAccrual()); + assertEquals(3.0, transactions.get(2).getFeeChargesPortion()); + accrualTransactionId = transactions.get(2).getId(); + + journalEntries = this.journalEntryHelper.getJournalEntriesByTransactionId("L" + accrualTransactionId); + assertEquals(3.0f, (float) journalEntries.get(0).get("amount")); + assertEquals(uniqueIncomeAccountForFee.getResourceId().intValue(), (int) journalEntries.get(0).get("glAccountId")); + assertEquals("CREDIT", ((HashMap) journalEntries.get(0).get("entryType")).get("value")); + assertEquals(3.0f, (float) journalEntries.get(1).get("amount")); + assertEquals(assetFeeAndPenaltyAccount.getAccountID(), (int) journalEntries.get(1).get("glAccountId")); + assertEquals("DEBIT", ((HashMap) journalEntries.get(1).get("entryType")).get("value")); + + loanSchedulePeriods = loanDetails.getRepaymentSchedule().getPeriods(); + assertEquals(2, loanSchedulePeriods.size()); + assertEquals(10.0, loanSchedulePeriods.get(1).getPenaltyChargesDue()); + assertEquals(10.0, loanSchedulePeriods.get(1).getPenaltyChargesOutstanding()); + assertEquals(3.0, loanSchedulePeriods.get(1).getFeeChargesDue()); + assertEquals(3.0, loanSchedulePeriods.get(1).getFeeChargesOutstanding()); + assertEquals(1013.0, loanSchedulePeriods.get(1).getTotalDueForPeriod()); + assertEquals(1013.0, loanSchedulePeriods.get(1).getTotalOutstandingForPeriod()); + + loanSummary = loanDetails.getSummary(); + assertEquals(10.0, loanSummary.getPenaltyChargesCharged()); + assertEquals(10.0, loanSummary.getPenaltyChargesOutstanding()); + assertEquals(3.0, loanSummary.getFeeChargesCharged()); + assertEquals(3.0, loanSummary.getFeeChargesOutstanding()); + assertEquals(1013.0, loanSummary.getTotalOutstanding()); + + loanTransactionHelper.makeLoanRepayment((long) loanID, new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy") + .transactionDate("11 September 2022").locale("en").transactionAmount(5.0)); + + externalId = UUID.randomUUID().toString(); + chargeAdjustmentResponse = this.loanTransactionHelper.chargeAdjustment((long) loanID, (long) feeLoanChargeId, + new PostLoansLoanIdChargesChargeIdRequest().amount(2.0).externalId(externalId)); + + loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanID); + + loanSchedulePeriods = loanDetails.getRepaymentSchedule().getPeriods(); + assertEquals(2, loanSchedulePeriods.size()); + assertEquals(10.0, loanSchedulePeriods.get(1).getPenaltyChargesDue()); + assertEquals(7.0, loanSchedulePeriods.get(1).getPenaltyChargesPaid()); + assertEquals(3.0, loanSchedulePeriods.get(1).getPenaltyChargesOutstanding()); + assertEquals(3.0, loanSchedulePeriods.get(1).getFeeChargesDue()); + assertEquals(0.0, loanSchedulePeriods.get(1).getFeeChargesPaid()); + assertEquals(3.0, loanSchedulePeriods.get(1).getFeeChargesOutstanding()); + assertEquals(1000.0, loanSchedulePeriods.get(1).getPrincipalDue()); + assertEquals(0.0, loanSchedulePeriods.get(1).getPrincipalPaid()); + assertEquals(1000.0, loanSchedulePeriods.get(1).getPrincipalOutstanding()); + assertEquals(1013.0, loanSchedulePeriods.get(1).getTotalDueForPeriod()); + assertEquals(1006.0, loanSchedulePeriods.get(1).getTotalOutstandingForPeriod()); + assertEquals(7.0, loanSchedulePeriods.get(1).getTotalPaidForPeriod()); + + loanSummary = loanDetails.getSummary(); + assertEquals(10.0, loanSummary.getPenaltyChargesCharged()); + assertEquals(3.0, loanSummary.getPenaltyChargesOutstanding()); + assertEquals(7.0, loanSummary.getPenaltyChargesPaid()); + assertEquals(3.0, loanSummary.getFeeChargesCharged()); + assertEquals(3.0, loanSummary.getFeeChargesOutstanding()); + assertEquals(0.0, loanSummary.getFeeChargesPaid()); + assertEquals(1000.0, loanSummary.getPrincipalOutstanding()); + assertEquals(0.0, loanSummary.getPrincipalPaid()); + assertEquals(7.0, loanSummary.getTotalRepayment()); + assertEquals(1006.0, loanSummary.getTotalOutstanding()); + + transactions = loanDetails.getTransactions(); + assertEquals(2.0, transactions.get(5).getAmount()); + assertTrue(transactions.get(4).getType().getChargeAdjustment()); + assertEquals(2.0, transactions.get(5).getPenaltyChargesPortion()); + assertEquals(0.0, transactions.get(5).getFeeChargesPortion()); + assertEquals(0.0, transactions.get(5).getPrincipalPortion()); + chargeAdjustmentTransactionId = transactions.get(5).getId(); + + journalEntries = this.journalEntryHelper.getJournalEntriesByTransactionId("L" + chargeAdjustmentTransactionId); + assertEquals(2.0f, (float) journalEntries.get(0).get("amount")); + assertEquals(uniqueIncomeAccountForFee.getResourceId().intValue(), (int) journalEntries.get(0).get("glAccountId")); + assertEquals("DEBIT", ((HashMap) journalEntries.get(0).get("entryType")).get("value")); + assertEquals(2.0f, (float) journalEntries.get(1).get("amount")); + assertEquals(assetFeeAndPenaltyAccount.getAccountID(), (int) journalEntries.get(1).get("glAccountId")); + assertEquals("CREDIT", ((HashMap) journalEntries.get(1).get("entryType")).get("value")); + + externalId = UUID.randomUUID().toString(); + chargeAdjustmentResponse = this.loanTransactionHelper.chargeAdjustment((long) loanID, (long) penaltyLoanChargeId, + new PostLoansLoanIdChargesChargeIdRequest().amount(7.0).externalId(externalId)); + + loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanID); + + loanSchedulePeriods = loanDetails.getRepaymentSchedule().getPeriods(); + assertEquals(2, loanSchedulePeriods.size()); + assertEquals(10.0, loanSchedulePeriods.get(1).getPenaltyChargesDue()); + assertEquals(10.0, loanSchedulePeriods.get(1).getPenaltyChargesPaid()); + assertEquals(0.0, loanSchedulePeriods.get(1).getPenaltyChargesOutstanding()); + assertEquals(3.0, loanSchedulePeriods.get(1).getFeeChargesDue()); + assertEquals(3.0, loanSchedulePeriods.get(1).getFeeChargesPaid()); + assertEquals(0.0, loanSchedulePeriods.get(1).getFeeChargesOutstanding()); + assertEquals(1000.0, loanSchedulePeriods.get(1).getPrincipalDue()); + assertEquals(1.0, loanSchedulePeriods.get(1).getPrincipalPaid()); + assertEquals(999.0, loanSchedulePeriods.get(1).getPrincipalOutstanding()); + assertEquals(1013.0, loanSchedulePeriods.get(1).getTotalDueForPeriod()); + assertEquals(999.0, loanSchedulePeriods.get(1).getTotalOutstandingForPeriod()); + assertEquals(14.0, loanSchedulePeriods.get(1).getTotalPaidForPeriod()); + + loanSummary = loanDetails.getSummary(); + assertEquals(10.0, loanSummary.getPenaltyChargesCharged()); + assertEquals(0.0, loanSummary.getPenaltyChargesOutstanding()); + assertEquals(10.0, loanSummary.getPenaltyChargesPaid()); + assertEquals(3.0, loanSummary.getFeeChargesCharged()); + assertEquals(0.0, loanSummary.getFeeChargesOutstanding()); + assertEquals(3.0, loanSummary.getFeeChargesPaid()); + assertEquals(999.0, loanSummary.getPrincipalOutstanding()); + assertEquals(1.0, loanSummary.getPrincipalPaid()); + assertEquals(14.0, loanSummary.getTotalRepayment()); + assertEquals(999.0, loanSummary.getTotalOutstanding()); + + transactions = loanDetails.getTransactions(); + assertEquals(7.0, transactions.get(6).getAmount()); + assertTrue(transactions.get(6).getType().getChargeAdjustment()); + assertEquals(3.0, transactions.get(6).getPenaltyChargesPortion()); + assertEquals(3.0, transactions.get(6).getFeeChargesPortion()); + assertEquals(1.0, transactions.get(6).getPrincipalPortion()); + chargeAdjustmentTransactionId = transactions.get(6).getId(); + + journalEntries = this.journalEntryHelper.getJournalEntriesByTransactionId("L" + chargeAdjustmentTransactionId); + assertEquals(7.0f, (float) journalEntries.get(0).get("amount")); + assertEquals(uniqueIncomeAccountForPenalty.getResourceId().intValue(), (int) journalEntries.get(0).get("glAccountId")); + assertEquals("DEBIT", ((HashMap) journalEntries.get(0).get("entryType")).get("value")); + if (assetAccount.getAccountID() == (int) journalEntries.get(1).get("glAccountId")) { + assertEquals(1.0f, (float) journalEntries.get(1).get("amount")); + assertEquals(assetAccount.getAccountID(), (int) journalEntries.get(1).get("glAccountId")); + assertEquals("CREDIT", ((HashMap) journalEntries.get(1).get("entryType")).get("value")); + + assertEquals(6.0f, (float) journalEntries.get(2).get("amount")); + assertEquals(assetFeeAndPenaltyAccount.getAccountID(), (int) journalEntries.get(2).get("glAccountId")); + assertEquals("CREDIT", ((HashMap) journalEntries.get(2).get("entryType")).get("value")); + } else { + assertEquals(1.0f, (float) journalEntries.get(2).get("amount")); + assertEquals(assetAccount.getAccountID(), (int) journalEntries.get(2).get("glAccountId")); + assertEquals("CREDIT", ((HashMap) journalEntries.get(2).get("entryType")).get("value")); + + assertEquals(6.0f, (float) journalEntries.get(1).get("amount")); + assertEquals(assetFeeAndPenaltyAccount.getAccountID(), (int) journalEntries.get(1).get("glAccountId")); + assertEquals("CREDIT", ((HashMap) journalEntries.get(1).get("entryType")).get("value")); + } + + loanTransactionHelper.makeLoanRepayment((long) loanID, new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy") + .transactionDate("13 September 2022").locale("en").transactionAmount(998.0)); + + externalId = UUID.randomUUID().toString(); + chargeAdjustmentResponse = this.loanTransactionHelper.chargeAdjustment((long) loanID, (long) feeLoanChargeId, + new PostLoansLoanIdChargesChargeIdRequest().amount(1.0).externalId(externalId)); + + loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanID); + + loanSchedulePeriods = loanDetails.getRepaymentSchedule().getPeriods(); + assertEquals(2, loanSchedulePeriods.size()); + assertEquals(10.0, loanSchedulePeriods.get(1).getPenaltyChargesDue()); + assertEquals(10.0, loanSchedulePeriods.get(1).getPenaltyChargesPaid()); + assertEquals(0.0, loanSchedulePeriods.get(1).getPenaltyChargesOutstanding()); + assertEquals(3.0, loanSchedulePeriods.get(1).getFeeChargesDue()); + assertEquals(3.0, loanSchedulePeriods.get(1).getFeeChargesPaid()); + assertEquals(0.0, loanSchedulePeriods.get(1).getFeeChargesOutstanding()); + assertEquals(1000.0, loanSchedulePeriods.get(1).getPrincipalDue()); + assertEquals(1000.0, loanSchedulePeriods.get(1).getPrincipalPaid()); + assertEquals(0.0, loanSchedulePeriods.get(1).getPrincipalOutstanding()); + assertEquals(1013.0, loanSchedulePeriods.get(1).getTotalDueForPeriod()); + assertEquals(0.0, loanSchedulePeriods.get(1).getTotalOutstandingForPeriod()); + assertEquals(1013.0, loanSchedulePeriods.get(1).getTotalPaidForPeriod()); + + loanSummary = loanDetails.getSummary(); + assertEquals(10.0, loanSummary.getPenaltyChargesCharged()); + assertEquals(0.0, loanSummary.getPenaltyChargesOutstanding()); + assertEquals(10.0, loanSummary.getPenaltyChargesPaid()); + assertEquals(3.0, loanSummary.getFeeChargesCharged()); + assertEquals(0.0, loanSummary.getFeeChargesOutstanding()); + assertEquals(3.0, loanSummary.getFeeChargesPaid()); + assertEquals(0.0, loanSummary.getPrincipalOutstanding()); + assertEquals(1000.0, loanSummary.getPrincipalPaid()); + assertEquals(1013.0, loanSummary.getTotalRepayment()); + assertEquals(0.0, loanSummary.getTotalOutstanding()); + + transactions = loanDetails.getTransactions(); + assertEquals(1.0, transactions.get(8).getAmount()); + assertTrue(transactions.get(8).getType().getChargeAdjustment()); + assertEquals(0.0, transactions.get(8).getPenaltyChargesPortion()); + assertEquals(0.0, transactions.get(8).getFeeChargesPortion()); + assertEquals(1.0, transactions.get(8).getPrincipalPortion()); + chargeAdjustmentTransactionId = transactions.get(8).getId(); + + journalEntries = this.journalEntryHelper.getJournalEntriesByTransactionId("L" + chargeAdjustmentTransactionId); + assertEquals(1.0f, (float) journalEntries.get(0).get("amount")); + assertEquals(uniqueIncomeAccountForFee.getResourceId().intValue(), (int) journalEntries.get(0).get("glAccountId")); + assertEquals("DEBIT", ((HashMap) journalEntries.get(0).get("entryType")).get("value")); + assertEquals(1.0f, (float) journalEntries.get(1).get("amount")); + assertEquals(assetAccount.getAccountID(), (int) journalEntries.get(1).get("glAccountId")); + assertEquals("CREDIT", ((HashMap) journalEntries.get(1).get("entryType")).get("value")); + + assertTrue(loanDetails.getStatus().getClosedObligationsMet()); + + externalId = UUID.randomUUID().toString(); + chargeAdjustmentResponse = this.loanTransactionHelper.chargeAdjustment((long) loanID, (long) penaltyLoanChargeId, + new PostLoansLoanIdChargesChargeIdRequest().amount(1.0).externalId(externalId)); + + loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanID); + + transactions = loanDetails.getTransactions(); + assertEquals(1.0, transactions.get(9).getAmount()); + assertTrue(transactions.get(9).getType().getChargeAdjustment()); + assertEquals(0.0, transactions.get(9).getPenaltyChargesPortion()); + assertEquals(0.0, transactions.get(9).getFeeChargesPortion()); + assertEquals(0.0, transactions.get(9).getPrincipalPortion()); + assertEquals(1.0, transactions.get(9).getOverpaymentPortion()); + chargeAdjustmentTransactionId = transactions.get(9).getId(); + + journalEntries = this.journalEntryHelper.getJournalEntriesByTransactionId("L" + chargeAdjustmentTransactionId); + assertEquals(1.0f, (float) journalEntries.get(0).get("amount")); + assertEquals(uniqueIncomeAccountForPenalty.getResourceId().intValue(), (int) journalEntries.get(0).get("glAccountId")); + assertEquals("DEBIT", ((HashMap) journalEntries.get(0).get("entryType")).get("value")); + assertEquals(1.0f, (float) journalEntries.get(1).get("amount")); + assertEquals(overpaymentAccount.getAccountID(), (int) journalEntries.get(1).get("glAccountId")); + assertEquals("CREDIT", ((HashMap) journalEntries.get(1).get("entryType")).get("value")); + + assertTrue(loanDetails.getStatus().getOverpaid()); + + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + @Test public void undoWaivedChargeWaiveTransactionDoesNotExist() { final Account assetAccount = this.accountHelper.createAssetAccount(); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyAndChargebackIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyAndChargebackIntegrationTest.java index ee25a5c40..9ed8bfbe9 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyAndChargebackIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyAndChargebackIntegrationTest.java @@ -118,14 +118,14 @@ public class DelinquencyAndChargebackIntegrationTest { PostLoansLoanIdTransactionsResponse loanIdTransactionsResponse = loanTransactionHelper.makeLoanRepayment(operationDate, transactionAmount, loanId); assertNotNull(loanIdTransactionsResponse); - final Integer transactionId = loanIdTransactionsResponse.getResourceId(); - loanTransactionHelper.reviewLoanTransactionRelations(loanId, transactionId, 0); + final Long transactionId = loanIdTransactionsResponse.getResourceId(); + loanTransactionHelper.reviewLoanTransactionRelations(loanId, transactionId.intValue(), 0); getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); validateLoanAccount(getLoansLoanIdResponse, "0.00", "1100.00", 0, doubleZERO); - final Integer chargebackTransactionId = loanTransactionHelper.applyChargebackTransaction(loanId, transactionId, amountVal, 0, - responseSpec); - loanTransactionHelper.reviewLoanTransactionRelations(loanId, transactionId, 1); + final Long chargebackTransactionId = loanTransactionHelper.applyChargebackTransaction(loanId, transactionId.intValue(), amountVal, + 0, responseSpec); + loanTransactionHelper.reviewLoanTransactionRelations(loanId, transactionId.intValue(), 1); getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); assertNotNull(getLoansLoanIdResponse); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java index fbc2e73ed..b78af8743 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java @@ -597,7 +597,7 @@ public class DelinquencyBucketsIntegrationTest { // Reverse the Previous Loan Repayment PostLoansLoanIdTransactionsResponse loansLoanIdReverseTransactions = loanTransactionHelper.reverseLoanTransaction(loanId, - loansLoanIdTransactions.getResourceId(), operationDate, responseSpec); + loansLoanIdTransactions.getResourceId().intValue(), operationDate, responseSpec); assertNotNull(loansLoanIdReverseTransactions); log.info("Loan repayment reverse transaction id {}", loansLoanIdReverseTransactions.getResourceId()); getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargeSpecificDueDateTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargeSpecificDueDateTest.java index 37c62dcdf..571dd834d 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargeSpecificDueDateTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargeSpecificDueDateTest.java @@ -90,7 +90,7 @@ public class LoanChargeSpecificDueDateTest { String payloadJSON = ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, feeAmount, false); final PostChargesResponse postChargesResponse = ChargesHelper.createLoanCharge(requestSpec, responseSpec, payloadJSON); assertNotNull(postChargesResponse); - final Integer loanChargeId = postChargesResponse.getResourceId(); + final Long loanChargeId = postChargesResponse.getResourceId(); payloadJSON = LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(loanChargeId.toString(), operationDate, feeAmount); PostLoansLoanIdChargesResponse postLoansLoanIdChargesResponse = loanTransactionHelper.addChargeForLoan(loanId, payloadJSON, diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionChargebackTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionChargebackTest.java index b4426d739..41bdb0374 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionChargebackTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionChargebackTest.java @@ -29,7 +29,7 @@ import io.restassured.specification.RequestSpecification; import io.restassured.specification.ResponseSpecification; import java.time.LocalDate; import java.util.HashMap; -import java.util.Set; +import java.util.List; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.client.models.GetDelinquencyBucketsResponse; import org.apache.fineract.client.models.GetDelinquencyRangesResponse; @@ -87,19 +87,19 @@ public class LoanTransactionChargebackTest { PostLoansLoanIdTransactionsResponse loanIdTransactionsResponse = loanTransactionHelper.makeLoanRepayment(operationDate, amount, loanId); assertNotNull(loanIdTransactionsResponse); - final Integer transactionId = loanIdTransactionsResponse.getResourceId(); + final Long transactionId = loanIdTransactionsResponse.getResourceId(); getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); assertNotNull(getLoansLoanIdResponse); loanTransactionHelper.validateLoanStatus(getLoansLoanIdResponse, "loanStatusType.closed.obligations.met"); - reviewLoanTransactionRelations(loanId, transactionId, 0, Double.valueOf("0.00")); + reviewLoanTransactionRelations(loanId, transactionId.intValue(), 0, Double.valueOf("0.00")); - final Integer chargebackTransactionId = loanTransactionHelper.applyChargebackTransaction(loanId, transactionId, "1000.00", 0, - responseSpec); + final Long chargebackTransactionId = loanTransactionHelper.applyChargebackTransaction(loanId, transactionId.intValue(), "1000.00", + 0, responseSpec); - reviewLoanTransactionRelations(loanId, transactionId, 1, Double.valueOf("0.00")); - reviewLoanTransactionRelations(loanId, chargebackTransactionId, 0, Double.valueOf("1000.00")); + reviewLoanTransactionRelations(loanId, transactionId.intValue(), 1, Double.valueOf("0.00")); + reviewLoanTransactionRelations(loanId, chargebackTransactionId.intValue(), 0, Double.valueOf("1000.00")); getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); assertNotNull(getLoansLoanIdResponse); @@ -109,10 +109,11 @@ public class LoanTransactionChargebackTest { // Try to reverse a Loan Transaction charge back PostLoansLoanIdTransactionsResponse reverseTransactionResponse = loanTransactionHelper.reverseLoanTransaction(loanId, - chargebackTransactionId, operationDate, responseSpecError); + chargebackTransactionId.intValue(), operationDate, responseSpecError); // Try to reverse a Loan Transaction repayment with linked transactions - reverseTransactionResponse = loanTransactionHelper.reverseLoanTransaction(loanId, transactionId, operationDate, responseSpecError); + reverseTransactionResponse = loanTransactionHelper.reverseLoanTransaction(loanId, transactionId.intValue(), operationDate, + responseSpecError); } @Test @@ -135,13 +136,13 @@ public class LoanTransactionChargebackTest { PostLoansLoanIdTransactionsResponse loanIdTransactionsResponse = loanTransactionHelper.makeLoanRepayment(operationDate, amount, loanId); assertNotNull(loanIdTransactionsResponse); - final Integer transactionId = loanIdTransactionsResponse.getResourceId(); - reviewLoanTransactionRelations(loanId, transactionId, 0, Double.valueOf("666.67")); + final Long transactionId = loanIdTransactionsResponse.getResourceId(); + reviewLoanTransactionRelations(loanId, transactionId.intValue(), 0, Double.valueOf("666.67")); - final Integer chargebackTransactionId = loanTransactionHelper.applyChargebackTransaction(loanId, transactionId, amount.toString(), - 0, responseSpec); - reviewLoanTransactionRelations(loanId, transactionId, 1, Double.valueOf("666.67")); - reviewLoanTransactionRelations(loanId, chargebackTransactionId, 0, Double.valueOf("1000.00")); + final Long chargebackTransactionId = loanTransactionHelper.applyChargebackTransaction(loanId, transactionId.intValue(), + amount.toString(), 0, responseSpec); + reviewLoanTransactionRelations(loanId, transactionId.intValue(), 1, Double.valueOf("666.67")); + reviewLoanTransactionRelations(loanId, chargebackTransactionId.intValue(), 0, Double.valueOf("1000.00")); getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); assertNotNull(getLoansLoanIdResponse); @@ -170,7 +171,7 @@ public class LoanTransactionChargebackTest { GetLoansLoanIdResponse getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); assertNotNull(getLoansLoanIdResponse); - Set loanTransactions = getLoansLoanIdResponse.getTransactions(); + List loanTransactions = getLoansLoanIdResponse.getTransactions(); assertNotNull(loanTransactions); log.info("Loan Id {} with {} transactions", loanId, loanTransactions.size()); assertEquals(2, loanTransactions.size()); @@ -203,19 +204,19 @@ public class LoanTransactionChargebackTest { PostLoansLoanIdTransactionsResponse loanIdTransactionsResponse = loanTransactionHelper.makeLoanRepayment(operationDate, amount, loanId); assertNotNull(loanIdTransactionsResponse); - final Integer transactionId = loanIdTransactionsResponse.getResourceId(); + final Long transactionId = loanIdTransactionsResponse.getResourceId(); getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); assertNotNull(getLoansLoanIdResponse); loanTransactionHelper.validateLoanStatus(getLoansLoanIdResponse, "loanStatusType.closed.obligations.met"); - reviewLoanTransactionRelations(loanId, transactionId, 0, Double.valueOf("0.00")); + reviewLoanTransactionRelations(loanId, transactionId.intValue(), 0, Double.valueOf("0.00")); - Integer chargebackTransactionId = loanTransactionHelper.applyChargebackTransaction(loanId, transactionId, "500.00", 0, + Long chargebackTransactionId = loanTransactionHelper.applyChargebackTransaction(loanId, transactionId.intValue(), "500.00", 0, responseSpec); - reviewLoanTransactionRelations(loanId, transactionId, 1, Double.valueOf("0.00")); - reviewLoanTransactionRelations(loanId, chargebackTransactionId, 0, Double.valueOf("500.00")); + reviewLoanTransactionRelations(loanId, transactionId.intValue(), 1, Double.valueOf("0.00")); + reviewLoanTransactionRelations(loanId, chargebackTransactionId.intValue(), 0, Double.valueOf("500.00")); getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); assertNotNull(getLoansLoanIdResponse); @@ -237,10 +238,11 @@ public class LoanTransactionChargebackTest { } } - chargebackTransactionId = loanTransactionHelper.applyChargebackTransaction(loanId, transactionId, "300.00", 0, responseSpec); + chargebackTransactionId = loanTransactionHelper.applyChargebackTransaction(loanId, transactionId.intValue(), "300.00", 0, + responseSpec); - reviewLoanTransactionRelations(loanId, transactionId, 2, Double.valueOf("0.00")); - reviewLoanTransactionRelations(loanId, chargebackTransactionId, 0, Double.valueOf("800.00")); + reviewLoanTransactionRelations(loanId, transactionId.intValue(), 2, Double.valueOf("0.00")); + reviewLoanTransactionRelations(loanId, chargebackTransactionId.intValue(), 0, Double.valueOf("800.00")); getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); assertNotNull(getLoansLoanIdResponse); @@ -281,19 +283,19 @@ public class LoanTransactionChargebackTest { PostLoansLoanIdTransactionsResponse loanIdTransactionsResponse = loanTransactionHelper.makeLoanRepayment(operationDate, amount, loanId); assertNotNull(loanIdTransactionsResponse); - final Integer transactionId = loanIdTransactionsResponse.getResourceId(); + final Long transactionId = loanIdTransactionsResponse.getResourceId(); getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); assertNotNull(getLoansLoanIdResponse); loanTransactionHelper.validateLoanStatus(getLoansLoanIdResponse, "loanStatusType.overpaid"); - reviewLoanTransactionRelations(loanId, transactionId, 0, Double.valueOf("0.00")); + reviewLoanTransactionRelations(loanId, transactionId.intValue(), 0, Double.valueOf("0.00")); - final Integer chargebackTransactionId = loanTransactionHelper.applyChargebackTransaction(loanId, transactionId, "200.00", 0, + final Long chargebackTransactionId = loanTransactionHelper.applyChargebackTransaction(loanId, transactionId.intValue(), "200.00", 0, responseSpec); - reviewLoanTransactionRelations(loanId, transactionId, 1, Double.valueOf("0.00")); - reviewLoanTransactionRelations(loanId, chargebackTransactionId, 0, Double.valueOf("100.00")); + reviewLoanTransactionRelations(loanId, transactionId.intValue(), 1, Double.valueOf("0.00")); + reviewLoanTransactionRelations(loanId, chargebackTransactionId.intValue(), 0, Double.valueOf("100.00")); getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); assertNotNull(getLoansLoanIdResponse); @@ -320,19 +322,19 @@ public class LoanTransactionChargebackTest { PostLoansLoanIdTransactionsResponse loanIdTransactionsResponse = loanTransactionHelper.makeLoanRepayment(operationDate, amount, loanId); assertNotNull(loanIdTransactionsResponse); - final Integer transactionId = loanIdTransactionsResponse.getResourceId(); + final Long transactionId = loanIdTransactionsResponse.getResourceId(); getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); assertNotNull(getLoansLoanIdResponse); loanTransactionHelper.validateLoanStatus(getLoansLoanIdResponse, "loanStatusType.overpaid"); - reviewLoanTransactionRelations(loanId, transactionId, 0, Double.valueOf("0.00")); + reviewLoanTransactionRelations(loanId, transactionId.intValue(), 0, Double.valueOf("0.00")); - final Integer chargebackTransactionId = loanTransactionHelper.applyChargebackTransaction(loanId, transactionId, "100.00", 0, + final Long chargebackTransactionId = loanTransactionHelper.applyChargebackTransaction(loanId, transactionId.intValue(), "100.00", 0, responseSpec); - reviewLoanTransactionRelations(loanId, transactionId, 1, Double.valueOf("0.00")); - reviewLoanTransactionRelations(loanId, chargebackTransactionId, 0, Double.valueOf("0.00")); + reviewLoanTransactionRelations(loanId, transactionId.intValue(), 1, Double.valueOf("0.00")); + reviewLoanTransactionRelations(loanId, chargebackTransactionId.intValue(), 0, Double.valueOf("0.00")); getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); assertNotNull(getLoansLoanIdResponse); @@ -355,21 +357,21 @@ public class LoanTransactionChargebackTest { PostLoansLoanIdTransactionsResponse loanIdTransactionsResponse = loanTransactionHelper.makeLoanRepayment(operationDate, amount, loanId); assertNotNull(loanIdTransactionsResponse); - final Integer transactionId = loanIdTransactionsResponse.getResourceId(); + final Long transactionId = loanIdTransactionsResponse.getResourceId(); getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); assertNotNull(getLoansLoanIdResponse); loanTransactionHelper.validateLoanStatus(getLoansLoanIdResponse, "loanStatusType.overpaid"); - reviewLoanTransactionRelations(loanId, transactionId, 0, Double.valueOf("0.00")); + reviewLoanTransactionRelations(loanId, transactionId.intValue(), 0, Double.valueOf("0.00")); GetDelinquencyRangesResponse delinquencyRange = getLoansLoanIdResponse.getDelinquencyRange(); assertNull(delinquencyRange); log.info("Loan Delinquency Range is null {}", (delinquencyRange == null)); - final Integer chargebackTransactionId = loanTransactionHelper.applyChargebackTransaction(loanId, transactionId, "50.00", 0, + final Long chargebackTransactionId = loanTransactionHelper.applyChargebackTransaction(loanId, transactionId.intValue(), "50.00", 0, responseSpec); - reviewLoanTransactionRelations(loanId, transactionId, 1, Double.valueOf("0.00")); - reviewLoanTransactionRelations(loanId, chargebackTransactionId, 0, Double.valueOf("0.00")); + reviewLoanTransactionRelations(loanId, transactionId.intValue(), 1, Double.valueOf("0.00")); + reviewLoanTransactionRelations(loanId, chargebackTransactionId.intValue(), 0, Double.valueOf("0.00")); getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); assertNotNull(getLoansLoanIdResponse); @@ -396,16 +398,16 @@ public class LoanTransactionChargebackTest { PostLoansLoanIdTransactionsResponse loanIdTransactionsResponse = loanTransactionHelper.makeLoanRepayment(operationDate, amount, loanId); assertNotNull(loanIdTransactionsResponse); - final Integer transactionId = loanIdTransactionsResponse.getResourceId(); + final Long transactionId = loanIdTransactionsResponse.getResourceId(); getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); assertNotNull(getLoansLoanIdResponse); loanTransactionHelper.validateLoanStatus(getLoansLoanIdResponse, "loanStatusType.closed.obligations.met"); // First round, empty array - reviewLoanTransactionRelations(loanId, transactionId, 0, Double.valueOf("0.00")); + reviewLoanTransactionRelations(loanId, transactionId.intValue(), 0, Double.valueOf("0.00")); - loanTransactionHelper.applyChargebackTransaction(loanId, transactionId, "200.00", 0, responseSpec); + loanTransactionHelper.applyChargebackTransaction(loanId, transactionId.intValue(), "200.00", 0, responseSpec); Double expectedAmount = Double.valueOf("200.00"); getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); @@ -416,9 +418,9 @@ public class LoanTransactionChargebackTest { DelinquencyBucketsHelper.evaluateLoanCollectionData(getLoansLoanIdResponse, 0, expectedAmount); // Second round, array size equal to 1 - reviewLoanTransactionRelations(loanId, transactionId, 1, Double.valueOf("0.00")); + reviewLoanTransactionRelations(loanId, transactionId.intValue(), 1, Double.valueOf("0.00")); - loanTransactionHelper.applyChargebackTransaction(loanId, transactionId, "300.00", 1, responseSpec); + loanTransactionHelper.applyChargebackTransaction(loanId, transactionId.intValue(), "300.00", 1, responseSpec); expectedAmount = Double.valueOf("500.00"); getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); @@ -428,9 +430,9 @@ public class LoanTransactionChargebackTest { DelinquencyBucketsHelper.evaluateLoanCollectionData(getLoansLoanIdResponse, 0, expectedAmount); // Third round, array size equal to 2 - reviewLoanTransactionRelations(loanId, transactionId, 2, Double.valueOf("0.00")); + reviewLoanTransactionRelations(loanId, transactionId.intValue(), 2, Double.valueOf("0.00")); - loanTransactionHelper.applyChargebackTransaction(loanId, transactionId, "500.00", 0, responseSpec); + loanTransactionHelper.applyChargebackTransaction(loanId, transactionId.intValue(), "500.00", 0, responseSpec); expectedAmount = Double.valueOf("1000.00"); getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BusinessDateHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BusinessDateHelper.java index d3a24790b..cb06f13e7 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BusinessDateHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BusinessDateHelper.java @@ -23,13 +23,17 @@ import io.restassured.specification.RequestSpecification; import io.restassured.specification.ResponseSpecification; import java.time.LocalDate; import java.util.HashMap; +import java.util.List; import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.client.models.BusinessDateRequest; +import org.apache.fineract.client.models.BusinessDateResponse; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; +import org.apache.fineract.integrationtests.client.IntegrationTest; @Slf4j -public final class BusinessDateHelper { +public final class BusinessDateHelper extends IntegrationTest { - private BusinessDateHelper() {} + public BusinessDateHelper() {} public static HashMap updateBusinessDate(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, final BusinessDateType type, final LocalDate date) { @@ -39,6 +43,20 @@ public final class BusinessDateHelper { return Utils.performServerPost(requestSpec, responseSpec, BUSINESS_DATE_API, buildBusinessDateRequest(type, date), "changes"); } + public BusinessDateResponse updateBusinessDate(final BusinessDateRequest request) { + log.info("------------------UPDATE BUSINESS DATE----------------------"); + log.info("------------------Type: {}, date: {}----------------------", request.getType(), request.getDate()); + return ok(fineract().businessDateManagement.updateBusinessDate(request)); + } + + public BusinessDateResponse getBusinessDate(final String type) { + return ok(fineract().businessDateManagement.getBusinessDate(type)); + } + + public List getBusinessDates() { + return ok(fineract().businessDateManagement.getBusinessDates()); + } + private static String buildBusinessDateRequest(BusinessDateType type, LocalDate date) { final HashMap map = new HashMap<>(); map.put("type", type.name()); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ClientHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ClientHelper.java index 3318787e5..7b696d733 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ClientHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ClientHelper.java @@ -41,15 +41,17 @@ import org.apache.fineract.client.models.GetClientsClientIdResponse; import org.apache.fineract.client.models.PostClientClientIdAddressesRequest; import org.apache.fineract.client.models.PostClientClientIdAddressesResponse; import org.apache.fineract.client.models.PostClientsRequest; +import org.apache.fineract.client.models.PostClientsResponse; import org.apache.fineract.client.util.JSON; import org.apache.fineract.infrastructure.bulkimport.data.GlobalEntityType; +import org.apache.fineract.integrationtests.client.IntegrationTest; import org.apache.fineract.integrationtests.common.system.CodeHelper; import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.ss.usermodel.Workbook; @Slf4j @RequiredArgsConstructor -public class ClientHelper { +public class ClientHelper extends IntegrationTest { private static final String CREATE_CLIENT_URL = "/fineract-provider/api/v1/clients?" + Utils.TENANT_IDENTIFIER; private static final String CLIENT_URL = "/fineract-provider/api/v1/clients"; @@ -77,6 +79,10 @@ public class ClientHelper { return Utils.performServerPost(requestSpec, responseSpec, CREATE_CLIENT_URL, requestBody, "clientId"); } + public PostClientsResponse createClient(final PostClientsRequest request) { + return ok(fineract().clients.create6(request)); + } + public static Integer createClient(final RequestSpecification requestSpec, final ResponseSpecification responseSpec) { return createClient(requestSpec, responseSpec, "04 March 2011"); } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java index b3dbcf9d0..af6cf7a6d 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java @@ -425,6 +425,16 @@ public class ExternalEventConfigurationHelper { shareProductDividentsCreateBusinessEvent.put("enabled", false); defaults.add(shareProductDividentsCreateBusinessEvent); + Map loanChargeAdjustmentPostBusinessEvent = new HashMap<>(); + loanChargeAdjustmentPostBusinessEvent.put("type", "LoanChargeAdjustmentPostBusinessEvent"); + loanChargeAdjustmentPostBusinessEvent.put("enabled", false); + defaults.add(loanChargeAdjustmentPostBusinessEvent); + + Map loanChargeAdjustmentPreBusinessEvent = new HashMap<>(); + loanChargeAdjustmentPreBusinessEvent.put("type", "LoanChargeAdjustmentPreBusinessEvent"); + loanChargeAdjustmentPreBusinessEvent.put("enabled", false); + defaults.add(loanChargeAdjustmentPreBusinessEvent); + return defaults; } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/accounting/AccountHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/accounting/AccountHelper.java index 4691657ad..cc98632cc 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/accounting/AccountHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/accounting/AccountHelper.java @@ -22,10 +22,13 @@ import io.restassured.specification.RequestSpecification; import io.restassured.specification.ResponseSpecification; import java.util.ArrayList; import java.util.HashMap; +import org.apache.fineract.client.models.PostGLAccountsRequest; +import org.apache.fineract.client.models.PostGLAccountsResponse; +import org.apache.fineract.integrationtests.client.IntegrationTest; import org.apache.fineract.integrationtests.common.Utils; @SuppressWarnings("rawtypes") -public class AccountHelper { +public class AccountHelper extends IntegrationTest { private static final String CREATE_GL_ACCOUNT_URL = "/fineract-provider/api/v1/glaccounts?" + Utils.TENANT_IDENTIFIER; private static final String GL_ACCOUNT_ID_RESPONSE = "resourceId"; @@ -79,4 +82,8 @@ public class AccountHelper { return accountRunningBalance; } + public PostGLAccountsResponse createGLAccount(final PostGLAccountsRequest request) { + return ok(fineract().glAccounts.createGLAccount1(request)); + } + } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/charges/ChargesHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/charges/ChargesHelper.java index a10472e75..91ab7dca9 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/charges/ChargesHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/charges/ChargesHelper.java @@ -23,8 +23,10 @@ import io.restassured.specification.RequestSpecification; import io.restassured.specification.ResponseSpecification; import java.util.ArrayList; import java.util.HashMap; +import org.apache.fineract.client.models.PostChargesRequest; import org.apache.fineract.client.models.PostChargesResponse; import org.apache.fineract.client.util.JSON; +import org.apache.fineract.integrationtests.client.IntegrationTest; import org.apache.fineract.integrationtests.common.CommonConstants; import org.apache.fineract.integrationtests.common.Utils; import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; @@ -32,9 +34,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; @SuppressWarnings({ "rawtypes", "unchecked" }) -public final class ChargesHelper { +public final class ChargesHelper extends IntegrationTest { - private ChargesHelper() { + public ChargesHelper() { } @@ -522,4 +524,8 @@ public final class ChargesHelper { "status"); } + + public PostChargesResponse createCharges(PostChargesRequest request) { + return ok(fineract().charges.createCharge(request)); + } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java index d7251c427..a55b808fc 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java @@ -91,6 +91,10 @@ public class LoanProductTestBuilder { private String maxPrincipal = "10000000.00"; private Account[] accountList = null; + private List> feeToIncomeAccountMappings = null; + private List> penaltyToIncomeAccountMappings = null; + private Account feeAndPenaltyAssetAccount; + private Boolean multiDisburseLoan = false; private final String outstandingLoanBalance = "35000"; private String maxTrancheCount = "3"; @@ -196,7 +200,7 @@ public class LoanProductTestBuilder { } if (this.accountingRule.equals(ACCRUAL_UPFRONT) || this.accountingRule.equals(ACCRUAL_PERIODIC)) { - map.putAll(getAccountMappingForAccrualBased()); + map.putAll(getAccountMappingForAccrualBased(this.feeAndPenaltyAssetAccount)); } else if (this.accountingRule.equals(CASH_BASED)) { map.putAll(getAccountMappingForCashBased()); } @@ -250,6 +254,13 @@ public class LoanProductTestBuilder { map.put("delinquencyBucketId", delinquencyBucketId); } + if (this.feeToIncomeAccountMappings != null) { + map.put("feeToIncomeAccountMappings", this.feeToIncomeAccountMappings); + } + if (this.penaltyToIncomeAccountMappings != null) { + map.put("penaltyToIncomeAccountMappings", this.penaltyToIncomeAccountMappings); + } + return map; } @@ -436,7 +447,7 @@ public class LoanProductTestBuilder { return map; } - private Map getAccountMappingForAccrualBased() { + private Map getAccountMappingForAccrualBased(Account feeAndPenaltyAssetAccount) { final Map map = new HashMap<>(); for (int i = 0; i < this.accountList.length; i++) { if (this.accountList[i].getAccountType().equals(Account.AccountType.ASSET)) { @@ -444,9 +455,14 @@ public class LoanProductTestBuilder { map.put("fundSourceAccountId", ID); map.put("loanPortfolioAccountId", ID); map.put("transfersInSuspenseAccountId", ID); + if (feeAndPenaltyAssetAccount != null) { + map.put("receivableFeeAccountId", feeAndPenaltyAssetAccount.getAccountID().toString()); + map.put("receivablePenaltyAccountId", feeAndPenaltyAssetAccount.getAccountID().toString()); + } else { + map.put("receivableFeeAccountId", ID); + map.put("receivablePenaltyAccountId", ID); + } map.put("receivableInterestAccountId", ID); - map.put("receivableFeeAccountId", ID); - map.put("receivablePenaltyAccountId", ID); } if (this.accountList[i].getAccountType().equals(Account.AccountType.INCOME)) { @@ -587,4 +603,30 @@ public class LoanProductTestBuilder { return this; } + public LoanProductTestBuilder withFeeToIncomeAccountMapping(final Long chargeId, final Long accountId) { + if (this.feeToIncomeAccountMappings == null) { + this.feeToIncomeAccountMappings = new ArrayList<>(); + } + Map newMap = new HashMap<>(); + newMap.put("chargeId", chargeId); + newMap.put("incomeAccountId", accountId); + this.feeToIncomeAccountMappings.add(newMap); + return this; + } + + public LoanProductTestBuilder withPenaltyToIncomeAccountMapping(final Long chargeId, final Long accountId) { + if (this.penaltyToIncomeAccountMappings == null) { + this.penaltyToIncomeAccountMappings = new ArrayList<>(); + } + Map newMap = new HashMap<>(); + newMap.put("chargeId", chargeId); + newMap.put("incomeAccountId", accountId); + this.penaltyToIncomeAccountMappings.add(newMap); + return this; + } + + public LoanProductTestBuilder withFeeAndPenaltyAssetAccount(final Account account) { + this.feeAndPenaltyAssetAccount = account; + return this; + } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java index a0a624ae6..79e380f98 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java @@ -51,12 +51,17 @@ import org.apache.fineract.client.models.GetLoansLoanIdResponse; import org.apache.fineract.client.models.GetLoansLoanIdSummary; import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTransactionIdResponse; import org.apache.fineract.client.models.GetPaymentTypesResponse; +import org.apache.fineract.client.models.PostLoansLoanIdChargesChargeIdRequest; +import org.apache.fineract.client.models.PostLoansLoanIdChargesChargeIdResponse; import org.apache.fineract.client.models.PostLoansLoanIdChargesResponse; import org.apache.fineract.client.models.PostLoansLoanIdResponse; +import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; +import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdResponse; import org.apache.fineract.client.models.PutLoansLoanIdResponse; import org.apache.fineract.client.util.JSON; +import org.apache.fineract.integrationtests.client.IntegrationTest; import org.apache.fineract.integrationtests.common.CommonConstants; import org.apache.fineract.integrationtests.common.PaymentTypeHelper; import org.apache.fineract.integrationtests.common.Utils; @@ -65,11 +70,9 @@ import org.apache.poi.ss.usermodel.Workbook; @Slf4j @SuppressWarnings({ "rawtypes", "unchecked" }) -public class LoanTransactionHelper { - - private final RequestSpecification requestSpec; - private final ResponseSpecification responseSpec; +public class LoanTransactionHelper extends IntegrationTest { + public static final String DATE_TIME_FORMAT = "dd MMMM yyyy HH:mm"; private static final String CREATE_LOAN_PRODUCT_URL = "/fineract-provider/api/v1/loanproducts?" + Utils.TENANT_IDENTIFIER; private static final String APPLY_LOAN_URL = "/fineract-provider/api/v1/loans?" + Utils.TENANT_IDENTIFIER; private static final String LOAN_ACCOUNT_URL = "/fineract-provider/api/v1/loans"; @@ -90,10 +93,9 @@ public class LoanTransactionHelper { private static final String RECOVER_FROM_GUARANTORS_COMMAND = "recoverGuarantees"; private static final String MAKE_REFUND_BY_CASH_COMMAND = "refundByCash"; private static final String FORECLOSURE_COMMAND = "foreclosure"; - - public static final String DATE_TIME_FORMAT = "dd MMMM yyyy HH:mm"; - private static final Gson GSON = new JSON().getGson(); + private final RequestSpecification requestSpec; + private final ResponseSpecification responseSpec; public LoanTransactionHelper(final RequestSpecification requestSpec, final ResponseSpecification responseSpec) { this.requestSpec = requestSpec; @@ -221,7 +223,9 @@ public class LoanTransactionHelper { final ResponseSpecification responseSpec, final Integer loanID) { final String URL = "/fineract-provider/api/v1/loans/" + loanID + "/delinquencytags?" + Utils.TENANT_IDENTIFIER; final String response = Utils.performServerGet(requestSpec, responseSpec, URL); - Type delinquencyTagsListType = new TypeToken>() {}.getType(); + Type delinquencyTagsListType = new TypeToken>() { + + }.getType(); return GSON.fromJson(response, delinquencyTagsListType); } @@ -461,11 +465,20 @@ public class LoanTransactionHelper { return postLoanTransaction(createLoanTransactionURL(MAKE_REPAYMENT_COMMAND, loanID), getRepaymentBodyAsJSON(date, amountToBePaid)); } + public PostLoansLoanIdTransactionsResponse makeLoanRepayment(final Long loanId, final PostLoansLoanIdTransactionsRequest request) { + return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "repayment")); + } + public PostLoansLoanIdTransactionsResponse reverseLoanTransaction(final Integer loanId, final Integer transactionId, String date, ResponseSpecification responseSpec) { return postLoanTransaction(createLoanTransactionURL(UNDO, loanId, transactionId), getUndoJsonBody(date), responseSpec); } + public PostLoansLoanIdTransactionsTransactionIdResponse reverseLoanTransaction(final Long loanId, final Long transactionId, + final PostLoansLoanIdTransactionsTransactionIdRequest request) { + return ok(fineract().loanTransactions.adjustLoanTransaction(loanId, transactionId, request, "undo")); + } + public HashMap makeRepaymentWithPDC(final String date, final Float amountToBePaid, final Integer loanID, final Integer paymentType) { return (HashMap) performLoanTransaction(createLoanTransactionURL(MAKE_REPAYMENT_COMMAND, loanID), getRepaymentWithPDCBodyAsJSON(date, amountToBePaid, paymentType), ""); @@ -540,6 +553,11 @@ public class LoanTransactionHelper { return Utils.performServerPut(requestSpec, responseSpec, TRANSAC_URL, body, ""); } + public PostLoansLoanIdChargesChargeIdResponse chargeAdjustment(final Long loanId, final Long chargeId, + final PostLoansLoanIdChargesChargeIdRequest request) { + return ok(fineract().loanCharges.executeLoanCharge1(loanId, chargeId, request, "adjustment")); + } + public Integer undoWaiveChargesForLoanReturnResourceId(final Integer loanId, final Integer transactionId, final String body) { log.info("--------------------------------- UNDO WAIVE CHARGES FOR LOAN --------------------------------"); final String TRANSAC_URL = "/fineract-provider/api/v1/loans/" + loanId + "/transactions/" + transactionId + "?" @@ -575,6 +593,14 @@ public class LoanTransactionHelper { return Utils.performServerGet(requestSpec, responseSpec, GET_LOAN_CHARGES_URL, param); } + public GetLoansLoanIdTransactionsTransactionIdResponse getLoanTransactionDetails(final Long loanId, final Long transactionId) { + return ok(fineract().loanTransactions.retrieveTransaction(loanId, transactionId, null)); + } + + public GetLoansLoanIdResponse getLoanDetails(final Long loanId) { + return ok(fineract().loans.retrieveLoan(loanId, false, "all", null, null)); + } + public GetLoansLoanIdTransactionsTransactionIdResponse getLoanTransaction(final Integer loanId, final Integer txnId) { final String GET_LOAN_CHARGES_URL = "/fineract-provider/api/v1/loans/" + loanId + "/transactions/" + txnId + "?" + Utils.TENANT_IDENTIFIER; @@ -1203,7 +1229,7 @@ public class LoanTransactionHelper { } } - public Integer applyChargebackTransaction(final Integer loanId, final Integer transactionId, final String amount, + public Long applyChargebackTransaction(final Integer loanId, final Integer transactionId, final String amount, final Integer paymentTypeIdx, ResponseSpecification responseSpec) { List paymentTypeList = PaymentTypeHelper.getSystemPaymentType(this.requestSpec, this.responseSpec); assertTrue(!paymentTypeList.isEmpty());