From 937bfe529cfa3dff07bda50b81140ec3cf918a3f Mon Sep 17 00:00:00 2001 From: Adam Saghy Date: Fri, 29 Aug 2025 18:00:26 +0200 Subject: [PATCH] FINERACT-2326: Improve `LoanTransactionData` DTO structure --- .../loanaccount/data/LoanTransactionData.java | 347 ++------------ .../mapper/LoanTransactionMapper.java | 16 +- .../data/LoanTransactionDataTest.java | 428 ++++++++++++++++++ .../mapper/LoanTransactionMapperTest.java | 148 ++++++ .../service/LoanReadPlatformServiceImpl.java | 101 +++-- ...ransactionBusinessEventSerializerTest.java | 14 +- 6 files changed, 703 insertions(+), 351 deletions(-) create mode 100644 fineract-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionDataTest.java create mode 100644 fineract-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanTransactionMapperTest.java diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionData.java index 9f9968f41..0e669052f 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionData.java @@ -24,6 +24,9 @@ import java.math.BigDecimal; import java.time.LocalDate; import java.util.Collection; import java.util.List; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.Setter; import org.apache.fineract.infrastructure.codes.data.CodeValueData; @@ -32,12 +35,13 @@ import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.portfolio.account.data.AccountTransferData; import org.apache.fineract.portfolio.paymentdetail.data.PaymentDetailData; import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; -import org.springframework.integration.annotation.Default; /** * Immutable data object representing a loan transaction. */ @Getter +@Builder(builderClassName = "Builder") +@AllArgsConstructor(access = AccessLevel.PUBLIC) public class LoanTransactionData implements Serializable { @Serial @@ -73,6 +77,7 @@ public class LoanTransactionData implements Serializable { private final LocalDate possibleNextRepaymentDate; private final BigDecimal availableDisbursementAmountWithOverApplied; + @Setter private Collection loanChargePaidByList; // templates @@ -104,334 +109,74 @@ public class LoanTransactionData implements Serializable { private final ExternalId reversalExternalId; private LocalDate reversedOnDate; + @Setter private List transactionRelations; private Collection chargeOffReasonOptions = null; public static LoanTransactionData importInstance(BigDecimal repaymentAmount, LocalDate lastRepaymentDate, Long repaymentTypeId, Integer rowIndex, String locale, String dateFormat) { - return new LoanTransactionData(repaymentAmount, lastRepaymentDate, repaymentTypeId, rowIndex, locale, dateFormat); - } - - private LoanTransactionData(BigDecimal transactionAmount, LocalDate transactionDate, Long paymentTypeId, Integer rowIndex, - String locale, String dateFormat) { - this.transactionAmount = transactionAmount; - this.transactionDate = transactionDate; - this.paymentTypeId = paymentTypeId; - this.rowIndex = rowIndex; - this.dateFormat = dateFormat; - this.locale = locale; - this.amount = null; - this.netDisbursalAmount = null; - this.date = null; - this.type = null; - this.id = null; - this.loanId = null; - this.externalLoanId = ExternalId.empty(); - this.officeId = null; - this.officeName = null; - this.currency = null; - this.paymentDetailData = null; - this.principalPortion = null; - this.interestPortion = null; - this.feeChargesPortion = null; - this.penaltyChargesPortion = null; - this.overpaymentPortion = null; - this.unrecognizedIncomePortion = null; - this.externalId = ExternalId.empty(); - this.transfer = null; - this.fixedEmiAmount = null; - this.outstandingLoanBalance = null; - this.submittedOnDate = null; - this.manuallyReversed = false; - this.possibleNextRepaymentDate = null; - this.paymentTypeOptions = null; - this.writeOffReasonOptions = null; - this.reversalExternalId = ExternalId.empty(); - this.availableDisbursementAmountWithOverApplied = null; + return LoanTransactionData.builder().transactionAmount(repaymentAmount).transactionDate(lastRepaymentDate) + .paymentTypeId(repaymentTypeId).rowIndex(rowIndex).locale(locale).dateFormat(dateFormat).externalLoanId(ExternalId.empty()) + .externalId(ExternalId.empty()).reversalExternalId(ExternalId.empty()).manuallyReversed(false).build(); } public static LoanTransactionData importInstance(BigDecimal repaymentAmount, LocalDate repaymentDate, Long repaymentTypeId, String accountNumber, Integer checkNumber, Integer routingCode, Integer receiptNumber, Integer bankNumber, Long loanAccountId, String transactionType, Integer rowIndex, String locale, String dateFormat) { - return new LoanTransactionData(repaymentAmount, repaymentDate, repaymentTypeId, accountNumber, checkNumber, routingCode, - receiptNumber, bankNumber, loanAccountId, "", rowIndex, locale, dateFormat); - } - - private LoanTransactionData(BigDecimal transactionAmount, LocalDate transactionDate, Long paymentTypeId, String accountNumber, - Integer checkNumber, Integer routingCode, Integer receiptNumber, Integer bankNumber, Long accountId, String transactionType, - Integer rowIndex, String locale, String dateFormat) { - this.transactionAmount = transactionAmount; - this.transactionDate = transactionDate; - this.paymentTypeId = paymentTypeId; - this.accountNumber = accountNumber; - this.checkNumber = checkNumber; - this.routingCode = routingCode; - this.receiptNumber = receiptNumber; - this.bankNumber = bankNumber; - this.accountId = accountId; - this.transactionType = transactionType; - this.rowIndex = rowIndex; - this.dateFormat = dateFormat; - this.locale = locale; - this.id = null; - this.loanId = null; - this.externalLoanId = ExternalId.empty(); - this.officeId = null; - this.officeName = null; - this.type = null; - this.date = null; - this.currency = null; - this.paymentDetailData = null; - this.amount = null; - this.netDisbursalAmount = null; - this.principalPortion = null; - this.interestPortion = null; - this.feeChargesPortion = null; - this.penaltyChargesPortion = null; - this.overpaymentPortion = null; - this.unrecognizedIncomePortion = null; - this.externalId = ExternalId.empty(); - this.transfer = null; - this.fixedEmiAmount = null; - this.outstandingLoanBalance = null; - this.submittedOnDate = null; - this.manuallyReversed = false; - this.possibleNextRepaymentDate = null; - this.paymentTypeOptions = null; - this.writeOffReasonOptions = null; - this.reversalExternalId = ExternalId.empty(); - this.availableDisbursementAmountWithOverApplied = null; + return LoanTransactionData.builder().transactionAmount(repaymentAmount).transactionDate(repaymentDate) + .paymentTypeId(repaymentTypeId).accountNumber(accountNumber).checkNumber(checkNumber).routingCode(routingCode) + .receiptNumber(receiptNumber).bankNumber(bankNumber).accountId(loanAccountId).transactionType(transactionType) + .rowIndex(rowIndex).locale(locale).dateFormat(dateFormat).externalLoanId(ExternalId.empty()).externalId(ExternalId.empty()) + .reversalExternalId(ExternalId.empty()).manuallyReversed(false).build(); } public static LoanTransactionData templateOnTop(final LoanTransactionData loanTransactionData, final Collection paymentTypeOptions) { - return new LoanTransactionData(loanTransactionData.id, loanTransactionData.officeId, loanTransactionData.officeName, - loanTransactionData.type, loanTransactionData.paymentDetailData, loanTransactionData.currency, loanTransactionData.date, - loanTransactionData.amount, loanTransactionData.netDisbursalAmount, loanTransactionData.principalPortion, - loanTransactionData.interestPortion, loanTransactionData.feeChargesPortion, loanTransactionData.penaltyChargesPortion, - loanTransactionData.overpaymentPortion, loanTransactionData.unrecognizedIncomePortion, paymentTypeOptions, - loanTransactionData.externalId, loanTransactionData.transfer, loanTransactionData.fixedEmiAmount, - loanTransactionData.outstandingLoanBalance, loanTransactionData.manuallyReversed, loanTransactionData.loanId, - loanTransactionData.externalLoanId); + return builder().id(loanTransactionData.id).officeId(loanTransactionData.officeId).officeName(loanTransactionData.officeName) + .type(loanTransactionData.type).paymentDetailData(loanTransactionData.paymentDetailData) + .currency(loanTransactionData.currency).date(loanTransactionData.date).amount(loanTransactionData.amount) + .netDisbursalAmount(loanTransactionData.netDisbursalAmount).principalPortion(loanTransactionData.principalPortion) + .interestPortion(loanTransactionData.interestPortion).feeChargesPortion(loanTransactionData.feeChargesPortion) + .penaltyChargesPortion(loanTransactionData.penaltyChargesPortion).overpaymentPortion(loanTransactionData.overpaymentPortion) + .unrecognizedIncomePortion(loanTransactionData.unrecognizedIncomePortion).paymentTypeOptions(paymentTypeOptions) + .externalId(loanTransactionData.externalId).transfer(loanTransactionData.transfer) + .fixedEmiAmount(loanTransactionData.fixedEmiAmount).outstandingLoanBalance(loanTransactionData.outstandingLoanBalance) + .manuallyReversed(loanTransactionData.manuallyReversed).loanId(loanTransactionData.loanId) + .externalLoanId(loanTransactionData.externalLoanId).build(); } public static LoanTransactionData templateOnTop(final LoanTransactionData loanTransactionData, final LoanTransactionEnumData typeOf) { - return new LoanTransactionData(loanTransactionData.id, loanTransactionData.officeId, loanTransactionData.officeName, typeOf, - loanTransactionData.paymentDetailData, loanTransactionData.currency, loanTransactionData.date, loanTransactionData.amount, - loanTransactionData.netDisbursalAmount, loanTransactionData.principalPortion, loanTransactionData.interestPortion, - loanTransactionData.feeChargesPortion, loanTransactionData.penaltyChargesPortion, loanTransactionData.overpaymentPortion, - loanTransactionData.unrecognizedIncomePortion, loanTransactionData.paymentTypeOptions, loanTransactionData.externalId, - loanTransactionData.transfer, loanTransactionData.fixedEmiAmount, loanTransactionData.outstandingLoanBalance, - loanTransactionData.manuallyReversed, loanTransactionData.loanId, loanTransactionData.externalLoanId); - } - - @Default // Default constructor for mapper - public LoanTransactionData(final Long id, final Long officeId, final String officeName, final LoanTransactionEnumData transactionType, - final PaymentDetailData paymentDetailData, final CurrencyData currency, final LocalDate date, final BigDecimal amount, - final BigDecimal netDisbursalAmount, final BigDecimal principalPortion, final BigDecimal interestPortion, - final BigDecimal feeChargesPortion, final BigDecimal penaltyChargesPortion, final BigDecimal overpaymentPortion, - final ExternalId externalId, final AccountTransferData transfer, BigDecimal fixedEmiAmount, BigDecimal outstandingLoanBalance, - final BigDecimal unrecognizedIncomePortion, final boolean manuallyReversed, Long loanId, ExternalId externalLoanId) { - this(id, officeId, officeName, transactionType, paymentDetailData, currency, date, amount, netDisbursalAmount, principalPortion, - interestPortion, feeChargesPortion, penaltyChargesPortion, overpaymentPortion, unrecognizedIncomePortion, null, externalId, - transfer, fixedEmiAmount, outstandingLoanBalance, manuallyReversed, loanId, externalLoanId); - } - - public LoanTransactionData(final Long id, final Long officeId, final String officeName, final LoanTransactionEnumData transactionType, - final PaymentDetailData paymentDetailData, final CurrencyData currency, final LocalDate date, final BigDecimal amount, - final BigDecimal netDisbursalAmount, final BigDecimal principalPortion, final BigDecimal interestPortion, - final BigDecimal feeChargesPortion, final BigDecimal penaltyChargesPortion, final BigDecimal overpaymentPortion, - BigDecimal unrecognizedIncomePortion, final Collection paymentTypeOptions, final ExternalId externalId, - final AccountTransferData transfer, final BigDecimal fixedEmiAmount, BigDecimal outstandingLoanBalance, - boolean manuallyReversed, Long loanId, ExternalId externalLoanId) { - this(id, externalLoanId, officeId, officeName, transactionType, paymentDetailData, currency, date, amount, netDisbursalAmount, - principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion, overpaymentPortion, unrecognizedIncomePortion, - paymentTypeOptions, externalId, transfer, fixedEmiAmount, outstandingLoanBalance, null, manuallyReversed, - ExternalId.empty(), null, loanId); - } - - public LoanTransactionData(final Long id, final Long officeId, final String officeName, final LoanTransactionEnumData transactionType, - final PaymentDetailData paymentDetailData, final CurrencyData currency, final LocalDate date, final BigDecimal amount, - final BigDecimal netDisbursalAmount, final BigDecimal principalPortion, final BigDecimal interestPortion, - final BigDecimal feeChargesPortion, final BigDecimal penaltyChargesPortion, final BigDecimal overpaymentPortion, - final BigDecimal unrecognizedIncomePortion, final ExternalId externalId, final AccountTransferData transfer, - BigDecimal fixedEmiAmount, BigDecimal outstandingLoanBalance, LocalDate submittedOnDate, final boolean manuallyReversed, - final ExternalId reversalExternalId, final LocalDate reversedOnDate, Long loanId, ExternalId externalLoanId) { - this(id, externalLoanId, officeId, officeName, transactionType, paymentDetailData, currency, date, amount, netDisbursalAmount, - principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion, overpaymentPortion, unrecognizedIncomePortion, - null, externalId, transfer, fixedEmiAmount, outstandingLoanBalance, submittedOnDate, manuallyReversed, reversalExternalId, - reversedOnDate, loanId); - } - - public LoanTransactionData(final Long id, final ExternalId externalLoanId, final Long officeId, final String officeName, - final LoanTransactionEnumData transactionType, final PaymentDetailData paymentDetailData, final CurrencyData currency, - final LocalDate date, final BigDecimal amount, final BigDecimal netDisbursalAmount, final BigDecimal principalPortion, - final BigDecimal interestPortion, final BigDecimal feeChargesPortion, final BigDecimal penaltyChargesPortion, - final BigDecimal overpaymentPortion, final BigDecimal unrecognizedIncomePortion, - final Collection paymentTypeOptions, final ExternalId externalId, final AccountTransferData transfer, - final BigDecimal fixedEmiAmount, BigDecimal outstandingLoanBalance, final LocalDate submittedOnDate, - final boolean manuallyReversed, final ExternalId reversalExternalId, final LocalDate reversedOnDate, Long loanId) { - this.id = id; - this.loanId = loanId; - this.externalLoanId = externalLoanId; - this.officeId = officeId; - this.officeName = officeName; - this.type = transactionType; - this.paymentDetailData = paymentDetailData; - this.currency = currency; - this.date = date; - this.amount = amount; - this.netDisbursalAmount = netDisbursalAmount; - this.principalPortion = principalPortion; - this.interestPortion = interestPortion; - this.feeChargesPortion = feeChargesPortion; - this.penaltyChargesPortion = penaltyChargesPortion; - this.unrecognizedIncomePortion = unrecognizedIncomePortion; - this.paymentTypeOptions = paymentTypeOptions; - this.externalId = externalId; - this.transfer = transfer; - this.overpaymentPortion = overpaymentPortion; - this.fixedEmiAmount = fixedEmiAmount; - this.outstandingLoanBalance = outstandingLoanBalance; - this.submittedOnDate = submittedOnDate; - this.manuallyReversed = manuallyReversed; - this.possibleNextRepaymentDate = null; - this.reversalExternalId = reversalExternalId; - this.reversedOnDate = reversedOnDate; - this.availableDisbursementAmountWithOverApplied = null; - } - - public LoanTransactionData(Long id, LoanTransactionEnumData transactionType, LocalDate date, BigDecimal totalAmount, - BigDecimal netDisbursalAmount, BigDecimal principalPortion, BigDecimal interestPortion, BigDecimal feeChargesPortion, - BigDecimal penaltyChargesPortion, BigDecimal overpaymentPortion, BigDecimal unrecognizedIncomePortion, - BigDecimal outstandingLoanBalance, final boolean manuallyReversed, ExternalId externalId, Long loanId, - ExternalId externalLoanId) { - this(id, externalLoanId, null, null, transactionType, null, null, date, totalAmount, netDisbursalAmount, principalPortion, - interestPortion, feeChargesPortion, penaltyChargesPortion, overpaymentPortion, unrecognizedIncomePortion, null, externalId, - null, null, outstandingLoanBalance, null, manuallyReversed, ExternalId.empty(), null, loanId); + return builder().id(loanTransactionData.id).officeId(loanTransactionData.officeId).officeName(loanTransactionData.officeName) + .type(typeOf).paymentDetailData(loanTransactionData.paymentDetailData).currency(loanTransactionData.currency) + .date(loanTransactionData.date).amount(loanTransactionData.amount) + .netDisbursalAmount(loanTransactionData.netDisbursalAmount).principalPortion(loanTransactionData.principalPortion) + .interestPortion(loanTransactionData.interestPortion).feeChargesPortion(loanTransactionData.feeChargesPortion) + .penaltyChargesPortion(loanTransactionData.penaltyChargesPortion).overpaymentPortion(loanTransactionData.overpaymentPortion) + .unrecognizedIncomePortion(loanTransactionData.unrecognizedIncomePortion) + .paymentTypeOptions(loanTransactionData.paymentTypeOptions).externalId(loanTransactionData.externalId) + .transfer(loanTransactionData.transfer).fixedEmiAmount(loanTransactionData.fixedEmiAmount) + .outstandingLoanBalance(loanTransactionData.outstandingLoanBalance).manuallyReversed(loanTransactionData.manuallyReversed) + .loanId(loanTransactionData.loanId).externalLoanId(loanTransactionData.externalLoanId).build(); } public static LoanTransactionData loanTransactionDataForCreditTemplate(final LoanTransactionEnumData transactionType, final LocalDate transactionDate, final BigDecimal transactionAmount, final Collection paymentOptions, final CurrencyData currency) { - final Long id = null; - final Long loanId = null; - final ExternalId externalLoanId = ExternalId.empty(); - final ExternalId externalId = ExternalId.empty(); - final Long officeId = null; - final String officeName = null; - final PaymentDetailData paymentDetailData = null; - final BigDecimal unrecognizedIncomePortion = null; - final BigDecimal principalPortion = null; - final BigDecimal interestPortion = null; - final BigDecimal feeChargesPortion = null; - final BigDecimal penaltyChargesPortion = null; - final BigDecimal overpaymentPortion = null; - final BigDecimal netDisbursalAmount = null; - final BigDecimal fixedEmiAmount = null; - final BigDecimal outstandingLoanBalance = null; - final AccountTransferData transfer = null; - final LocalDate submittedOnDate = null; - final LocalDate possibleNextRepaymentDate = null; - final boolean manuallyReversed = false; - final BigDecimal availableDisbursementAmountWithOverApplied = null; - return new LoanTransactionData(id, officeId, officeName, transactionType, paymentDetailData, currency, transactionDate, - transactionAmount, netDisbursalAmount, principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion, - overpaymentPortion, unrecognizedIncomePortion, paymentOptions, transfer, externalId, fixedEmiAmount, outstandingLoanBalance, - submittedOnDate, manuallyReversed, possibleNextRepaymentDate, loanId, externalLoanId, - availableDisbursementAmountWithOverApplied); - + return builder().type(transactionType).date(transactionDate).amount(transactionAmount).paymentTypeOptions(paymentOptions) + .currency(currency).externalLoanId(ExternalId.empty()).externalId(ExternalId.empty()).reversalExternalId(ExternalId.empty()) + .manuallyReversed(false).build(); } public static LoanTransactionData loanTransactionDataForDisbursalTemplate(final LoanTransactionEnumData transactionType, final LocalDate expectedDisbursedOnLocalDateForTemplate, final BigDecimal disburseAmountForTemplate, - final BigDecimal netDisbursalAmount, final Collection paymentOptions, final BigDecimal retriveLastEmiAmount, + final BigDecimal netDisbursalAmount, final Collection paymentOptions, final BigDecimal fixedEmiAmount, final LocalDate possibleNextRepaymentDate, final CurrencyData currency, final BigDecimal availableDisbursementAmountWithOverApplied) { - final Long id = null; - final Long loanId = null; - final ExternalId externalLoanId = ExternalId.empty(); - final Long officeId = null; - final String officeName = null; - final PaymentDetailData paymentDetailData = null; - final BigDecimal unrecognizedIncomePortion = null; - final BigDecimal principalPortion = null; - final BigDecimal interestPortion = null; - final BigDecimal feeChargesPortion = null; - final BigDecimal penaltyChargesPortion = null; - final BigDecimal overpaymentPortion = null; - final ExternalId externalId = ExternalId.empty(); - final BigDecimal outstandingLoanBalance = null; - final AccountTransferData transfer = null; - final LocalDate submittedOnDate = null; - final boolean manuallyReversed = false; - return new LoanTransactionData(id, officeId, officeName, transactionType, paymentDetailData, currency, - expectedDisbursedOnLocalDateForTemplate, disburseAmountForTemplate, netDisbursalAmount, principalPortion, interestPortion, - feeChargesPortion, penaltyChargesPortion, overpaymentPortion, unrecognizedIncomePortion, paymentOptions, transfer, - externalId, retriveLastEmiAmount, outstandingLoanBalance, submittedOnDate, manuallyReversed, possibleNextRepaymentDate, - loanId, externalLoanId, availableDisbursementAmountWithOverApplied); - - } - - private LoanTransactionData(Long id, final Long officeId, final String officeName, LoanTransactionEnumData transactionType, - final PaymentDetailData paymentDetailData, final CurrencyData currency, final LocalDate date, BigDecimal amount, - BigDecimal netDisbursalAmount, final BigDecimal principalPortion, final BigDecimal interestPortion, - final BigDecimal feeChargesPortion, final BigDecimal penaltyChargesPortion, final BigDecimal overpaymentPortion, - BigDecimal unrecognizedIncomePortion, Collection paymentOptions, final AccountTransferData transfer, - final ExternalId externalId, final BigDecimal fixedEmiAmount, BigDecimal outstandingLoanBalance, - final LocalDate submittedOnDate, final boolean manuallyReversed, final LocalDate possibleNextRepaymentDate, Long loanId, - ExternalId externalLoanId, final BigDecimal availableDisbursementAmountWithOverApplied) { - this.id = id; - this.loanId = loanId; - this.externalLoanId = externalLoanId; - this.officeId = officeId; - this.officeName = officeName; - this.type = transactionType; - this.paymentDetailData = paymentDetailData; - this.currency = currency; - this.date = date; - this.amount = amount; - this.netDisbursalAmount = netDisbursalAmount; - this.principalPortion = principalPortion; - this.interestPortion = interestPortion; - this.feeChargesPortion = feeChargesPortion; - this.penaltyChargesPortion = penaltyChargesPortion; - this.unrecognizedIncomePortion = unrecognizedIncomePortion; - this.paymentTypeOptions = paymentOptions; - this.externalId = externalId; - this.transfer = transfer; - this.overpaymentPortion = overpaymentPortion; - this.fixedEmiAmount = fixedEmiAmount; - this.outstandingLoanBalance = outstandingLoanBalance; - this.submittedOnDate = submittedOnDate; - this.manuallyReversed = manuallyReversed; - this.possibleNextRepaymentDate = possibleNextRepaymentDate; - this.reversalExternalId = ExternalId.empty(); - this.availableDisbursementAmountWithOverApplied = availableDisbursementAmountWithOverApplied; - } - - public boolean isNotDisbursement() { - return type.getId() == 1; - } - - public void setWriteOffReasonOptions(Collection writeOffReasonOptions) { - this.writeOffReasonOptions = writeOffReasonOptions; - } - - public void setChargeOffReasonOptions(Collection chargeOffReasonOptions) { - this.chargeOffReasonOptions = chargeOffReasonOptions; - } - - public void setLoanChargePaidByList(Collection loanChargePaidByList) { - this.loanChargePaidByList = loanChargePaidByList; - } - - public void setLoanTransactionRelations(List transactionRelations) { - this.transactionRelations = transactionRelations; - } - - public boolean supportTransactionRelations() { - return !type.isAccrual(); + return builder().type(transactionType).date(expectedDisbursedOnLocalDateForTemplate).amount(disburseAmountForTemplate) + .netDisbursalAmount(netDisbursalAmount).paymentTypeOptions(paymentOptions).fixedEmiAmount(fixedEmiAmount) + .possibleNextRepaymentDate(possibleNextRepaymentDate).currency(currency) + .availableDisbursementAmountWithOverApplied(availableDisbursementAmountWithOverApplied).externalLoanId(ExternalId.empty()) + .externalId(ExternalId.empty()).reversalExternalId(ExternalId.empty()).manuallyReversed(false).build(); } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanTransactionMapper.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanTransactionMapper.java index 97733c12a..be938e810 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanTransactionMapper.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanTransactionMapper.java @@ -46,8 +46,22 @@ public interface LoanTransactionMapper { @Mapping(target = "loanId", source = "loan.id") @Mapping(target = "externalLoanId", source = "loan.externalId") @Mapping(target = "netDisbursalAmount", source = "loan.netDisbursalAmount") - @Mapping(target = "transactionType", expression = "java(org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations.transactionType(loanTransaction.getTypeOf()))") + @Mapping(target = "transactionType", expression = "java(loanTransaction.getTypeOf().name())") + @Mapping(target = "type", expression = "java(org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations.transactionType(loanTransaction.getTypeOf()))") @Mapping(target = "paymentDetailData", expression = "java(loanTransaction.getPaymentDetail() != null ? loanTransaction.getPaymentDetail().toData() : null)") @Mapping(target = "currency", source = "loan.currency") + @Mapping(target = "possibleNextRepaymentDate", ignore = true) + @Mapping(target = "availableDisbursementAmountWithOverApplied", ignore = true) + @Mapping(target = "rowIndex", ignore = true) + @Mapping(target = "dateFormat", ignore = true) + @Mapping(target = "locale", ignore = true) + @Mapping(target = "paymentTypeId", ignore = true) + @Mapping(target = "accountNumber", ignore = true) + @Mapping(target = "checkNumber", ignore = true) + @Mapping(target = "routingCode", ignore = true) + @Mapping(target = "receiptNumber", ignore = true) + @Mapping(target = "bankNumber", ignore = true) + @Mapping(target = "accountId", ignore = true) + @Mapping(target = "transactionAmount", ignore = true) LoanTransactionData mapLoanTransaction(LoanTransaction loanTransaction); } diff --git a/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionDataTest.java b/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionDataTest.java new file mode 100644 index 000000000..9b690341f --- /dev/null +++ b/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionDataTest.java @@ -0,0 +1,428 @@ +/** + * 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.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.mock; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Collection; +import java.util.List; +import org.apache.fineract.infrastructure.codes.data.CodeValueData; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.organisation.monetary.data.CurrencyData; +import org.apache.fineract.portfolio.account.data.AccountTransferData; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; +import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations; +import org.apache.fineract.portfolio.paymentdetail.data.PaymentDetailData; +import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; +import org.junit.jupiter.api.Test; + +public class LoanTransactionDataTest { + + @Test + public void testLoanTransactionDataBuilder() { + // Given + // Primitive and simple types + Long id = 1L; + Long officeId = 2L; + String officeName = "Test Office"; + LocalDate date = LocalDate.of(2023, 1, 1); + BigDecimal amount = new BigDecimal("1000.50"); + boolean manuallyReversed = true; + Long loanId = 10L; + String transactionType = "REPAYMENT"; + Integer rowIndex = 1; + String dateFormat = "dd MMMM yyyy"; + String locale = "en"; + Long paymentTypeId = 1L; + String accountNumber = "AC123456"; + Integer checkNumber = 2; + Integer routingCode = 3; + Integer receiptNumber = 4; + Integer bankNumber = 5; + Long accountId = 5L; + BigDecimal transactionAmount = new BigDecimal("1000.50"); + Integer numberOfRepayments = 12; + + // Complex types + LoanTransactionEnumData type = LoanEnumerations.transactionType(LoanTransactionType.REPAYMENT); + PaymentDetailData paymentDetailData = mock(PaymentDetailData.class); + CurrencyData currency = new CurrencyData("USD", "US Dollar", 2, 0, "$", "USD"); + ExternalId externalId = ExternalId.generate(); + ExternalId externalLoanId = ExternalId.generate(); + ExternalId reversalExternalId = ExternalId.generate(); + + // Financial amounts + BigDecimal netDisbursalAmount = new BigDecimal("2000.00"); + BigDecimal principalPortion = new BigDecimal("800.00"); + BigDecimal interestPortion = new BigDecimal("100.00"); + BigDecimal feeChargesPortion = new BigDecimal("50.25"); + BigDecimal penaltyChargesPortion = new BigDecimal("50.25"); + BigDecimal overpaymentPortion = new BigDecimal("100.00"); + BigDecimal unrecognizedIncomePortion = BigDecimal.ZERO; + BigDecimal fixedEmiAmount = new BigDecimal("500.00"); + BigDecimal availableDisbursementAmountWithOverApplied = new BigDecimal("1500.00"); + + // Dates + LocalDate possibleNextRepaymentDate = LocalDate.of(2023, 2, 1); + LocalDate reversedOnDate = LocalDate.of(2023, 1, 15); + LocalDate submittedOnDate = LocalDate.of(2023, 1, 1); + + // Collections + List transactionRelations = mock(List.class); + List loanChargePaidByList = mock(List.class); + List loanRepaymentScheduleInstallments = mock(List.class); + Collection writeOffReasonOptions = mock(List.class); + Collection chargeOffReasonOptions = mock(List.class); + Collection paymentTypeOptions = mock(List.class); + AccountTransferData transfer = mock(AccountTransferData.class); + + // When + LoanTransactionData data = LoanTransactionData.builder() + // Simple fields + .id(id).officeId(officeId).officeName(officeName).date(date).amount(amount).manuallyReversed(manuallyReversed) + .loanId(loanId).transactionType(transactionType).rowIndex(rowIndex).dateFormat(dateFormat).locale(locale) + .paymentTypeId(paymentTypeId).accountNumber(accountNumber).checkNumber(checkNumber).routingCode(routingCode) + .receiptNumber(receiptNumber).bankNumber(bankNumber).accountId(accountId).transactionAmount(transactionAmount) + .numberOfRepayments(numberOfRepayments) + // Complex fields + .type(type).paymentDetailData(paymentDetailData).currency(currency).externalId(externalId).externalLoanId(externalLoanId) + .reversalExternalId(reversalExternalId) + // Financial amounts + .netDisbursalAmount(netDisbursalAmount).principalPortion(principalPortion).interestPortion(interestPortion) + .feeChargesPortion(feeChargesPortion).penaltyChargesPortion(penaltyChargesPortion).overpaymentPortion(overpaymentPortion) + .unrecognizedIncomePortion(unrecognizedIncomePortion).fixedEmiAmount(fixedEmiAmount) + .availableDisbursementAmountWithOverApplied(availableDisbursementAmountWithOverApplied) + // Dates + .possibleNextRepaymentDate(possibleNextRepaymentDate).reversedOnDate(reversedOnDate).submittedOnDate(submittedOnDate) + // Collections + .transactionRelations(transactionRelations).loanChargePaidByList(loanChargePaidByList) + .loanRepaymentScheduleInstallments(loanRepaymentScheduleInstallments).writeOffReasonOptions(writeOffReasonOptions) + .chargeOffReasonOptions(chargeOffReasonOptions).paymentTypeOptions(paymentTypeOptions).transfer(transfer).build(); + + // Then - Simple fields + assertEquals(id, data.getId()); + assertEquals(officeId, data.getOfficeId()); + assertEquals(officeName, data.getOfficeName()); + assertEquals(date, data.getDate()); + assertEquals(amount, data.getAmount()); + assertEquals(manuallyReversed, data.isManuallyReversed()); + assertEquals(loanId, data.getLoanId()); + assertEquals(transactionType, data.getTransactionType()); + assertEquals(rowIndex, data.getRowIndex()); + assertEquals(dateFormat, data.getDateFormat()); + assertEquals(locale, data.getLocale()); + assertEquals(paymentTypeId, data.getPaymentTypeId()); + assertEquals(accountNumber, data.getAccountNumber()); + assertEquals(checkNumber, data.getCheckNumber()); + assertEquals(routingCode, data.getRoutingCode()); + assertEquals(receiptNumber, data.getReceiptNumber()); + assertEquals(bankNumber, data.getBankNumber()); + assertEquals(accountId, data.getAccountId()); + assertEquals(transactionAmount, data.getTransactionAmount()); + assertEquals(numberOfRepayments, data.getNumberOfRepayments()); + + // Complex fields + assertEquals(type, data.getType()); + assertEquals(paymentDetailData, data.getPaymentDetailData()); + assertEquals(currency, data.getCurrency()); + assertEquals(externalId, data.getExternalId()); + assertEquals(externalLoanId, data.getExternalLoanId()); + assertEquals(reversalExternalId, data.getReversalExternalId()); + + // Financial amounts + assertEquals(netDisbursalAmount, data.getNetDisbursalAmount()); + assertEquals(principalPortion, data.getPrincipalPortion()); + assertEquals(interestPortion, data.getInterestPortion()); + assertEquals(feeChargesPortion, data.getFeeChargesPortion()); + assertEquals(penaltyChargesPortion, data.getPenaltyChargesPortion()); + assertEquals(overpaymentPortion, data.getOverpaymentPortion()); + assertEquals(unrecognizedIncomePortion, data.getUnrecognizedIncomePortion()); + assertEquals(fixedEmiAmount, data.getFixedEmiAmount()); + assertEquals(availableDisbursementAmountWithOverApplied, data.getAvailableDisbursementAmountWithOverApplied()); + + // Dates + assertEquals(possibleNextRepaymentDate, data.getPossibleNextRepaymentDate()); + assertEquals(reversedOnDate, data.getReversedOnDate()); + assertEquals(submittedOnDate, data.getSubmittedOnDate()); + + // Collections + assertEquals(transactionRelations, data.getTransactionRelations()); + assertEquals(loanChargePaidByList, data.getLoanChargePaidByList()); + assertEquals(loanRepaymentScheduleInstallments, data.getLoanRepaymentScheduleInstallments()); + assertEquals(writeOffReasonOptions, data.getWriteOffReasonOptions()); + assertEquals(chargeOffReasonOptions, data.getChargeOffReasonOptions()); + assertEquals(paymentTypeOptions, data.getPaymentTypeOptions()); + assertEquals(transfer, data.getTransfer()); + } + + @Test + public void testImportInstanceSimple() { + // Given + BigDecimal repaymentAmount = new BigDecimal("1500.75"); + LocalDate lastRepaymentDate = LocalDate.of(2023, 6, 15); + Long repaymentTypeId = 1L; + Integer rowIndex = 1; + String locale = "en"; + String dateFormat = "dd MMMM yyyy"; + + // When + LoanTransactionData data = LoanTransactionData.importInstance(repaymentAmount, lastRepaymentDate, repaymentTypeId, rowIndex, locale, + dateFormat); + + // Then + assertEquals(repaymentAmount, data.getTransactionAmount()); + assertEquals(lastRepaymentDate, data.getTransactionDate()); + assertEquals(repaymentTypeId, data.getPaymentTypeId()); + assertEquals(rowIndex, data.getRowIndex()); + assertEquals(locale, data.getLocale()); + assertEquals(dateFormat, data.getDateFormat()); + assertFalse(data.isManuallyReversed()); + assertEquals(ExternalId.empty(), data.getExternalId()); + assertEquals(ExternalId.empty(), data.getExternalLoanId()); + assertEquals(ExternalId.empty(), data.getReversalExternalId()); + } + + @Test + public void testImportInstanceWithAllFields() { + // Given + BigDecimal repaymentAmount = new BigDecimal("2000.50"); + LocalDate repaymentDate = LocalDate.of(2023, 7, 20); + Long repaymentTypeId = 2L; + String accountNumber = "ACC123"; + Integer checkNumber = 1001; + Integer routingCode = 12345; + Integer receiptNumber = 5001; + Integer bankNumber = 10; + Long loanAccountId = 100L; + String transactionType = "REPAYMENT"; + Integer rowIndex = 2; + String locale = "en"; + String dateFormat = "dd MMMM yyyy"; + + // When + LoanTransactionData data = LoanTransactionData.importInstance(repaymentAmount, repaymentDate, repaymentTypeId, accountNumber, + checkNumber, routingCode, receiptNumber, bankNumber, loanAccountId, transactionType, rowIndex, locale, dateFormat); + + // Then + assertEquals(repaymentAmount, data.getTransactionAmount()); + assertEquals(repaymentDate, data.getTransactionDate()); + assertEquals(repaymentTypeId, data.getPaymentTypeId()); + assertEquals(accountNumber, data.getAccountNumber()); + assertEquals(checkNumber, data.getCheckNumber()); + assertEquals(routingCode, data.getRoutingCode()); + assertEquals(receiptNumber, data.getReceiptNumber()); + assertEquals(bankNumber, data.getBankNumber()); + assertEquals(loanAccountId, data.getAccountId()); + assertEquals(transactionType, data.getTransactionType()); + assertEquals(rowIndex, data.getRowIndex()); + assertEquals(locale, data.getLocale()); + assertEquals(dateFormat, data.getDateFormat()); + assertFalse(data.isManuallyReversed()); + assertEquals(ExternalId.empty(), data.getExternalId()); + assertEquals(ExternalId.empty(), data.getExternalLoanId()); + assertEquals(ExternalId.empty(), data.getReversalExternalId()); + } + + @Test + public void testTemplateOnTopWithPaymentOptions() { + // Given + LoanTransactionData original = createSampleTransactionData(); + Collection newPaymentOptions = mock(Collection.class); + + // When + LoanTransactionData result = LoanTransactionData.templateOnTop(original, newPaymentOptions); + + // Then + assertBasicFieldsCopied(original, result); + assertEquals(newPaymentOptions, result.getPaymentTypeOptions()); + } + + @Test + public void testTemplateOnTopWithTransactionType() { + // Given + LoanTransactionData original = createSampleTransactionData(); + LoanTransactionEnumData newType = new LoanTransactionEnumData(2L, "code", "NEW_TYPE"); + + // When + LoanTransactionData result = LoanTransactionData.templateOnTop(original, newType); + + // Then + assertBasicFieldsCopied(original, result); + assertEquals(newType, result.getType()); + } + + @Test + public void testLoanTransactionDataForCreditTemplate() { + // Given + LoanTransactionEnumData transactionType = new LoanTransactionEnumData(1L, "code", "CREDIT"); + LocalDate transactionDate = LocalDate.of(2023, 8, 1); + BigDecimal transactionAmount = new BigDecimal("3000.00"); + Collection paymentOptions = mock(Collection.class); + CurrencyData currency = new CurrencyData("USD", "US Dollar", 2, 0, "$", "USD"); + + // When + LoanTransactionData result = LoanTransactionData.loanTransactionDataForCreditTemplate(transactionType, transactionDate, + transactionAmount, paymentOptions, currency); + + // Then + assertEquals(transactionType, result.getType()); + assertEquals(transactionDate, result.getDate()); + assertEquals(transactionAmount, result.getAmount()); + assertEquals(paymentOptions, result.getPaymentTypeOptions()); + assertEquals(currency, result.getCurrency()); + assertFalse(result.isManuallyReversed()); + assertEquals(ExternalId.empty(), result.getExternalId()); + assertEquals(ExternalId.empty(), result.getExternalLoanId()); + assertEquals(ExternalId.empty(), result.getReversalExternalId()); + } + + @Test + public void testLoanTransactionDataForDisbursalTemplate() { + // Given + LoanTransactionEnumData transactionType = new LoanTransactionEnumData(1L, "code", "DISBURSAL"); + LocalDate expectedDisbursedOn = LocalDate.of(2023, 8, 1); + BigDecimal disburseAmount = new BigDecimal("5000.00"); + BigDecimal netDisbursalAmount = new BigDecimal("4950.00"); + Collection paymentOptions = mock(Collection.class); + BigDecimal fixedEmiAmount = new BigDecimal("500.00"); + LocalDate possibleNextRepaymentDate = LocalDate.of(2023, 9, 1); + CurrencyData currency = new CurrencyData("USD", "US Dollar", 2, 0, "$", "USD"); + BigDecimal availableDisbursementAmount = new BigDecimal("10000.00"); + + // When + LoanTransactionData result = LoanTransactionData.loanTransactionDataForDisbursalTemplate(transactionType, expectedDisbursedOn, + disburseAmount, netDisbursalAmount, paymentOptions, fixedEmiAmount, possibleNextRepaymentDate, currency, + availableDisbursementAmount); + + // Then + assertEquals(transactionType, result.getType()); + assertEquals(expectedDisbursedOn, result.getDate()); + assertEquals(disburseAmount, result.getAmount()); + assertEquals(netDisbursalAmount, result.getNetDisbursalAmount()); + assertEquals(paymentOptions, result.getPaymentTypeOptions()); + assertEquals(fixedEmiAmount, result.getFixedEmiAmount()); + assertEquals(possibleNextRepaymentDate, result.getPossibleNextRepaymentDate()); + assertEquals(currency, result.getCurrency()); + assertEquals(availableDisbursementAmount, result.getAvailableDisbursementAmountWithOverApplied()); + assertFalse(result.isManuallyReversed()); + assertEquals(ExternalId.empty(), result.getExternalId()); + assertEquals(ExternalId.empty(), result.getExternalLoanId()); + assertEquals(ExternalId.empty(), result.getReversalExternalId()); + } + + @Test + public void testLoanTransactionDataWithNullValues() { + // When + LoanTransactionData data = LoanTransactionData.builder().build(); + + // Then - Simple fields + assertNull(data.getId()); + assertNull(data.getOfficeId()); + assertNull(data.getOfficeName()); + assertNull(data.getDate()); + assertNull(data.getAmount()); + assertFalse(data.isManuallyReversed()); // Default boolean + assertNull(data.getLoanId()); + assertNull(data.getTransactionType()); + assertNull(data.getRowIndex()); + assertNull(data.getDateFormat()); + assertNull(data.getLocale()); + assertNull(data.getPaymentTypeId()); + assertNull(data.getAccountNumber()); + assertNull(data.getCheckNumber()); + assertNull(data.getRoutingCode()); + assertNull(data.getReceiptNumber()); + assertNull(data.getBankNumber()); + assertNull(data.getAccountId()); + assertNull(data.getTransactionAmount()); + assertNull(data.getNumberOfRepayments()); + + // Complex fields + assertNull(data.getType()); + assertNull(data.getPaymentDetailData()); + assertNull(data.getCurrency()); + assertNull(data.getExternalId()); + assertNull(data.getExternalLoanId()); + assertNull(data.getReversalExternalId()); + + // Financial amounts + assertNull(data.getNetDisbursalAmount()); + assertNull(data.getPrincipalPortion()); + assertNull(data.getInterestPortion()); + assertNull(data.getFeeChargesPortion()); + assertNull(data.getPenaltyChargesPortion()); + assertNull(data.getOverpaymentPortion()); + assertNull(data.getUnrecognizedIncomePortion()); + assertNull(data.getFixedEmiAmount()); + assertNull(data.getAvailableDisbursementAmountWithOverApplied()); + + // Dates + assertNull(data.getPossibleNextRepaymentDate()); + assertNull(data.getReversedOnDate()); + assertNull(data.getSubmittedOnDate()); + + // Collections + assertNull(data.getTransactionRelations()); + assertNull(data.getLoanChargePaidByList()); + assertNull(data.getLoanRepaymentScheduleInstallments()); + assertNull(data.getWriteOffReasonOptions()); + assertNull(data.getChargeOffReasonOptions()); + assertNull(data.getPaymentTypeOptions()); + assertNull(data.getTransfer()); + } + + // Helper methods + private LoanTransactionData createSampleTransactionData() { + return LoanTransactionData.builder().id(1L).officeId(1L).officeName("Test Office") + .type(new LoanTransactionEnumData(1L, "code", "REPAYMENT")).date(LocalDate.of(2023, 1, 1)).amount(new BigDecimal("1000.00")) + .currency(new CurrencyData("USD", "US Dollar", 2, 0, "$", "USD")).paymentDetailData(mock(PaymentDetailData.class)) + .externalId(ExternalId.generate()).externalLoanId(ExternalId.generate()).reversalExternalId(ExternalId.generate()) + .manuallyReversed(false).loanId(1L).build(); + } + + private void assertBasicFieldsCopied(LoanTransactionData original, LoanTransactionData result) { + assertEquals(original.getId(), result.getId()); + assertEquals(original.getOfficeId(), result.getOfficeId()); + assertEquals(original.getOfficeName(), result.getOfficeName()); + assertEquals(original.getPaymentDetailData(), result.getPaymentDetailData()); + assertEquals(original.getCurrency(), result.getCurrency()); + assertEquals(original.getDate(), result.getDate()); + assertEquals(original.getAmount(), result.getAmount()); + assertEquals(original.getNetDisbursalAmount(), result.getNetDisbursalAmount()); + assertEquals(original.getPrincipalPortion(), result.getPrincipalPortion()); + assertEquals(original.getInterestPortion(), result.getInterestPortion()); + assertEquals(original.getFeeChargesPortion(), result.getFeeChargesPortion()); + assertEquals(original.getPenaltyChargesPortion(), result.getPenaltyChargesPortion()); + assertEquals(original.getOverpaymentPortion(), result.getOverpaymentPortion()); + assertEquals(original.getUnrecognizedIncomePortion(), result.getUnrecognizedIncomePortion()); + assertEquals(original.getExternalId(), result.getExternalId()); + assertEquals(original.getTransfer(), result.getTransfer()); + assertEquals(original.getFixedEmiAmount(), result.getFixedEmiAmount()); + assertEquals(original.getOutstandingLoanBalance(), result.getOutstandingLoanBalance()); + assertEquals(original.isManuallyReversed(), result.isManuallyReversed()); + assertEquals(original.getLoanId(), result.getLoanId()); + assertEquals(original.getExternalLoanId(), result.getExternalLoanId()); + } + +} diff --git a/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanTransactionMapperTest.java b/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanTransactionMapperTest.java new file mode 100644 index 000000000..643fbb8ad --- /dev/null +++ b/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanTransactionMapperTest.java @@ -0,0 +1,148 @@ +/** + * 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.mapper; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.ZoneId; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.organisation.monetary.data.CurrencyData; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.mapper.CurrencyMapper; +import org.apache.fineract.organisation.office.domain.Office; +import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData; +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.paymentdetail.data.PaymentDetailData; +import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; +import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class LoanTransactionMapperTest { + + @Mock + private CurrencyMapper currencyMapper; + + @Mock + private LoanTransaction loanTransaction; + + @Mock + private Loan loan; + + @Mock + private PaymentDetail paymentDetail; + + @InjectMocks + private LoanTransactionMapperImpl mapper; + + @BeforeEach + void setUp() { + // Setup common mocks + when(loanTransaction.getLoan()).thenReturn(loan); + when(loan.getId()).thenReturn(1L); + when(loan.getCurrency()).thenReturn(new MonetaryCurrency("USD", 2, 0)); + when(currencyMapper.map(any())).thenReturn(new CurrencyData("USD", "US Dollar", 2, 0, "$", "code")); + } + + @Test + public void testMapLoanTransaction_MapsAllFieldsCorrectly() { + // Given + LocalDate transactionDate = LocalDate.now(ZoneId.of("UTC")); + + // Setup transaction mocks + when(loanTransaction.getId()).thenReturn(1L); + when(loanTransaction.getTypeOf()).thenReturn(LoanTransactionType.REPAYMENT); + when(loanTransaction.getTransactionDate()).thenReturn(transactionDate); + when(loanTransaction.getDateOf()).thenReturn(transactionDate); + when(loanTransaction.getAmount()).thenReturn(BigDecimal.valueOf(1000)); + when(loanTransaction.getPrincipalPortion()).thenReturn(BigDecimal.valueOf(800)); + when(loanTransaction.getInterestPortion()).thenReturn(BigDecimal.valueOf(100)); + when(loanTransaction.getFeeChargesPortion()).thenReturn(BigDecimal.valueOf(50)); + when(loanTransaction.getPenaltyChargesPortion()).thenReturn(BigDecimal.valueOf(50)); + when(loanTransaction.getPaymentDetail()).thenReturn(paymentDetail); + when(loanTransaction.getExternalId()).thenReturn(ExternalId.generate()); + when(loanTransaction.isManuallyAdjustedOrReversed()).thenReturn(false); + when(loanTransaction.getOffice()).thenReturn(Office.headOffice("Test Office", LocalDate.of(2022, 2, 12), null)); + when(loanTransaction.getLoan().getNetDisbursalAmount()).thenReturn(BigDecimal.valueOf(2000)); + + // Setup payment detail mocks + when(paymentDetail.toData()).thenReturn(new PaymentDetailData(1L, PaymentTypeData.instance(1L, "Cash"), "accountNumber", + "checkNumber", "routingCode", "receiptNumber", "bankNumber")); + + // When + LoanTransactionData result = mapper.mapLoanTransaction(loanTransaction); + + // Then - Verify all mapped fields + assertNotNull(result); + assertEquals(1L, result.getId()); + assertEquals(transactionDate, result.getDate()); + assertEquals(BigDecimal.valueOf(1000), result.getAmount()); + assertEquals(BigDecimal.valueOf(800), result.getPrincipalPortion()); + assertEquals(BigDecimal.valueOf(100), result.getInterestPortion()); + assertEquals(BigDecimal.valueOf(50), result.getFeeChargesPortion()); + assertEquals(BigDecimal.valueOf(50), result.getPenaltyChargesPortion()); + assertFalse(result.isManuallyReversed()); + assertEquals("REPAYMENT", result.getTransactionType()); + assertNotNull(result.getType()); + assertNotNull(result.getPaymentDetailData()); + assertNull(result.getOfficeId()); + assertEquals("Test Office", result.getOfficeName()); + assertEquals(1L, result.getLoanId()); + assertEquals(loan.getExternalId(), result.getExternalLoanId()); + assertEquals(BigDecimal.valueOf(2000), result.getNetDisbursalAmount()); + assertNotNull(result.getCurrency()); + + // Verify ignored fields are null + assertNull(result.getNumberOfRepayments()); + assertNull(result.getLoanRepaymentScheduleInstallments()); + assertNull(result.getWriteOffReasonOptions()); + assertNull(result.getChargeOffReasonOptions()); + assertNull(result.getPaymentTypeOptions()); + assertNull(result.getOverpaymentPortion()); + assertNull(result.getTransfer()); + assertNull(result.getFixedEmiAmount()); + assertNull(result.getPossibleNextRepaymentDate()); + assertNull(result.getAvailableDisbursementAmountWithOverApplied()); + assertNull(result.getRowIndex()); + assertNull(result.getDateFormat()); + assertNull(result.getLocale()); + assertNull(result.getPaymentTypeId()); + assertNull(result.getAccountNumber()); + assertNull(result.getCheckNumber()); + assertNull(result.getRoutingCode()); + assertNull(result.getReceiptNumber()); + assertNull(result.getBankNumber()); + assertNull(result.getAccountId()); + assertNull(result.getTransactionAmount()); + } +} 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 2332a4782..f9e609b25 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 @@ -316,7 +316,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa final List loanChargePaidByDatas = loanChargePaidByReadService .fetchLoanChargesPaidByDataTransactionId(loanIds); for (LoanTransactionData loanTransaction : loanTransactionData) { - loanTransaction.setLoanTransactionRelations(loanTransactionRelationDatas.stream().filter( + loanTransaction.setTransactionRelations(loanTransactionRelationDatas.stream().filter( loanTransactionRelationData -> loanTransactionRelationData.getFromLoanTransaction().equals(loanTransaction.getId())) .toList()); loanTransaction.setLoanChargePaidByList(loanChargePaidByDatas.stream() @@ -580,11 +580,13 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa BigDecimal adjustedChargeAmount = adjustPrepayInstallmentCharge(loan, onDate); BigDecimal totalAdjusted = outstandingAmounts.getTotalOutstanding().getAmount().subtract(adjustedChargeAmount); - return new LoanTransactionData(null, null, null, transactionType, null, currencyData, earliestUnpaidInstallmentDate, totalAdjusted, - loan.getNetDisbursalAmount(), outstandingAmounts.principal().getAmount(), outstandingAmounts.interest().getAmount(), - outstandingAmounts.feeCharges().getAmount().subtract(adjustedChargeAmount), outstandingAmounts.penaltyCharges().getAmount(), - null, unrecognizedIncomePortion, paymentOptions, ExternalId.empty(), null, null, outstandingLoanBalance, false, loanId, - loan.getExternalId()); + return LoanTransactionData.builder().type(transactionType).currency(currencyData).date(earliestUnpaidInstallmentDate) + .amount(totalAdjusted).netDisbursalAmount(loan.getNetDisbursalAmount()) + .principalPortion(outstandingAmounts.principal().getAmount()).interestPortion(outstandingAmounts.interest().getAmount()) + .feeChargesPortion(outstandingAmounts.feeCharges().getAmount().subtract(adjustedChargeAmount)) + .penaltyChargesPortion(outstandingAmounts.penaltyCharges().getAmount()).unrecognizedIncomePortion(unrecognizedIncomePortion) + .paymentTypeOptions(paymentOptions).externalId(ExternalId.empty()).outstandingLoanBalance(outstandingLoanBalance) + .manuallyReversed(false).loanId(loanId).externalLoanId(loan.getExternalId()).build(); } private BigDecimal adjustPrepayInstallmentCharge(Loan loan, final LocalDate onDate) { @@ -621,9 +623,10 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa final BigDecimal outstandingLoanBalance = null; final BigDecimal unrecognizedIncomePortion = null; - return new LoanTransactionData(null, null, null, transactionType, null, currencyData, waiveOfInterest.getTransactionDate(), amount, - loan.getNetDisbursalAmount(), null, null, null, null, null, ExternalId.empty(), null, null, outstandingLoanBalance, - unrecognizedIncomePortion, false, loanId, loan.getExternalId()); + return LoanTransactionData.builder().type(transactionType).currency(currencyData).date(waiveOfInterest.getTransactionDate()) + .amount(amount).netDisbursalAmount(loan.getNetDisbursalAmount()).outstandingLoanBalance(outstandingLoanBalance) + .unrecognizedIncomePortion(unrecognizedIncomePortion).externalId(ExternalId.empty()).manuallyReversed(false).loanId(loanId) + .externalLoanId(loan.getExternalId()).build(); } @Override @@ -633,9 +636,9 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa final BigDecimal outstandingLoanBalance = null; final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.WRITEOFF); final BigDecimal unrecognizedIncomePortion = null; - return new LoanTransactionData(null, null, null, transactionType, null, null, DateUtils.getBusinessLocalDate(), null, null, null, - null, null, null, null, ExternalId.empty(), null, null, outstandingLoanBalance, unrecognizedIncomePortion, false, null, - null); + return LoanTransactionData.builder().type(transactionType).date(DateUtils.getBusinessLocalDate()).externalId(ExternalId.empty()) + .outstandingLoanBalance(outstandingLoanBalance).unrecognizedIncomePortion(unrecognizedIncomePortion).manuallyReversed(false) + .build(); } @@ -695,7 +698,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa final LoanTransactionsMapper rm = new LoanTransactionsMapper(sqlGenerator); final String sql = "select " + rm.loanPaymentsSchema() + " where l.id = ? and tr.id = ? "; LoanTransactionData loanTransactionData = this.jdbcTemplate.queryForObject(sql, rm, loanId, transactionId); // NOSONAR - loanTransactionData.setLoanTransactionRelations( + loanTransactionData.setTransactionRelations( loanTransactionRelationReadService.fetchLoanTransactionRelationDataFrom(loanTransactionData.getId())); return loanTransactionData; } catch (final EmptyResultDataAccessException e) { @@ -1749,10 +1752,14 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa toTransferDescription, toTransferReversed); } - return new LoanTransactionData(id, officeId, officeName, transactionType, paymentDetailData, currencyData, date, totalAmount, - netDisbursalAmount, principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion, overPaymentPortion, - unrecognizedIncomePortion, externalId, transfer, null, outstandingLoanBalance, submittedOnDate, manuallyReversed, - reversalExternalId, reversedOnDate, loanId, externalLoanId); + return LoanTransactionData.builder().id(id).officeId(officeId).officeName(officeName).type(transactionType) + .paymentDetailData(paymentDetailData).currency(currencyData).date(date).amount(totalAmount) + .netDisbursalAmount(netDisbursalAmount).principalPortion(principalPortion).interestPortion(interestPortion) + .feeChargesPortion(feeChargesPortion).penaltyChargesPortion(penaltyChargesPortion) + .overpaymentPortion(overPaymentPortion).unrecognizedIncomePortion(unrecognizedIncomePortion).externalId(externalId) + .transfer(transfer).outstandingLoanBalance(outstandingLoanBalance).submittedOnDate(submittedOnDate) + .manuallyReversed(manuallyReversed).reversalExternalId(reversalExternalId).reversedOnDate(reversedOnDate).loanId(loanId) + .externalLoanId(externalLoanId).build(); } } @@ -2007,9 +2014,10 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa final Collection paymentOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes(); BigDecimal outstandingLoanBalance = null; final BigDecimal unrecognizedIncomePortion = null; - return new LoanTransactionData(null, null, null, transactionType, null, null, null, loan.getTotalWrittenOff(), - loan.getNetDisbursalAmount(), null, null, null, null, null, unrecognizedIncomePortion, paymentOptions, ExternalId.empty(), - null, null, outstandingLoanBalance, false, loanId, loan.getExternalId()); + return LoanTransactionData.builder().type(transactionType).amount(loan.getTotalWrittenOff()) + .netDisbursalAmount(loan.getNetDisbursalAmount()).unrecognizedIncomePortion(unrecognizedIncomePortion) + .paymentTypeOptions(paymentOptions).externalId(ExternalId.empty()).outstandingLoanBalance(outstandingLoanBalance) + .manuallyReversed(false).loanId(loanId).externalLoanId(loan.getExternalId()).build(); } @@ -2021,10 +2029,10 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa final BigDecimal totalOutstanding = loan.getSummary() != null ? loan.getSummary().getTotalOutstanding() : null; final List writeOffReasonOptions = new ArrayList<>( this.codeValueReadPlatformService.retrieveCodeValuesByCode(LoanApiConstants.WRITEOFFREASONS)); - LoanTransactionData loanTransactionData = new LoanTransactionData(null, null, null, transactionType, null, loan.getCurrency(), - DateUtils.getBusinessLocalDate(), totalOutstanding, loan.getNetDisbursalAmount(), null, null, null, null, null, - ExternalId.empty(), null, null, null, null, false, loanId, loan.getExternalId()); - loanTransactionData.setWriteOffReasonOptions(writeOffReasonOptions); + LoanTransactionData loanTransactionData = LoanTransactionData.builder().type(transactionType).currency(loan.getCurrency()) + .date(DateUtils.getBusinessLocalDate()).amount(totalOutstanding).netDisbursalAmount(loan.getNetDisbursalAmount()) + .externalId(ExternalId.empty()).manuallyReversed(false).loanId(loanId).externalLoanId(loan.getExternalId()) + .writeOffReasonOptions(writeOffReasonOptions).build(); return loanTransactionData; } @@ -2040,11 +2048,12 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa final BigDecimal totalPenaltyOutstanding = loan.getSummary() != null ? loan.getSummary().getPenaltyChargesOutstanding() : null; final List chargeOffReasonOptions = new ArrayList<>( this.codeValueReadPlatformService.retrieveCodeValuesByCode(LoanApiConstants.CHARGE_OFF_REASONS)); - LoanTransactionData loanTransactionData = new LoanTransactionData(null, null, null, transactionType, null, loan.getCurrency(), - DateUtils.getBusinessLocalDate(), totalOutstanding, loan.getNetDisbursalAmount(), totalPrincipalOutstanding, - totalInterestOutstanding, totalFeeOutstanding, totalPenaltyOutstanding, null, ExternalId.empty(), null, null, null, null, - false, loanId, loan.getExternalId()); - loanTransactionData.setChargeOffReasonOptions(chargeOffReasonOptions); + LoanTransactionData loanTransactionData = LoanTransactionData.builder().type(transactionType).currency(loan.getCurrency()) + .date(DateUtils.getBusinessLocalDate()).amount(totalOutstanding).netDisbursalAmount(loan.getNetDisbursalAmount()) + .principalPortion(totalPrincipalOutstanding).interestPortion(totalInterestOutstanding) + .feeChargesPortion(totalFeeOutstanding).penaltyChargesPortion(totalPenaltyOutstanding).externalId(ExternalId.empty()) + .manuallyReversed(false).loanId(loanId).externalLoanId(loan.getExternalId()).chargeOffReasonOptions(chargeOffReasonOptions) + .build(); return loanTransactionData; } @@ -2216,9 +2225,9 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa final LocalDate currentDate = LocalDate.now(DateUtils.getDateTimeZoneOfTenant()); final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(loanTransactionType); - return new LoanTransactionData(null, null, null, transactionType, null, currencyData, currentDate, transactionAmount, null, - netDisbursal, null, null, null, null, null, paymentOptions, ExternalId.empty(), null, null, null, false, loanId, - externalLoanId); + return LoanTransactionData.builder().type(transactionType).currency(currencyData).date(currentDate).amount(transactionAmount) + .netDisbursalAmount(netDisbursal).paymentTypeOptions(paymentOptions).externalId(ExternalId.empty()).manuallyReversed(false) + .loanId(loanId).externalLoanId(externalLoanId).build(); } @Override @@ -2309,14 +2318,15 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa final Money outStandingAmount = loanRepaymentScheduleInstallment.getTotalOutstanding(currency); - return new LoanTransactionData(null, null, null, transactionType, null, currencyData, earliestUnpaidInstallmentDate, - outStandingAmount.getAmount(), loan.getNetDisbursalAmount(), - loanRepaymentScheduleInstallment.getPrincipalOutstanding(currency).getAmount(), - loanRepaymentScheduleInstallment.getInterestOutstanding(currency).getAmount(), - loanRepaymentScheduleInstallment.getFeeChargesOutstanding(currency).getAmount(), - loanRepaymentScheduleInstallment.getPenaltyChargesOutstanding(currency).getAmount(), null, unrecognizedIncomePortion, - paymentTypeOptions, ExternalId.empty(), null, null, outstandingLoanBalance, isManuallyReversed, loanId, - loan.getExternalId()); + return LoanTransactionData.builder().type(transactionType).currency(currencyData).date(earliestUnpaidInstallmentDate) + .amount(outStandingAmount.getAmount()).netDisbursalAmount(loan.getNetDisbursalAmount()) + .principalPortion(loanRepaymentScheduleInstallment.getPrincipalOutstanding(currency).getAmount()) + .interestPortion(loanRepaymentScheduleInstallment.getInterestOutstanding(currency).getAmount()) + .feeChargesPortion(loanRepaymentScheduleInstallment.getFeeChargesOutstanding(currency).getAmount()) + .penaltyChargesPortion(loanRepaymentScheduleInstallment.getPenaltyChargesOutstanding(currency).getAmount()) + .unrecognizedIncomePortion(unrecognizedIncomePortion).paymentTypeOptions(paymentTypeOptions).externalId(ExternalId.empty()) + .outstandingLoanBalance(outstandingLoanBalance).manuallyReversed(isManuallyReversed).loanId(loanId) + .externalLoanId(loan.getExternalId()).build(); } private static final class CurrencyMapper implements RowMapper { @@ -2395,10 +2405,13 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa boolean manuallyReversed = false; final PaymentDetailData paymentDetailData = null; final AccountTransferData transfer = null; - final BigDecimal fixedEmiAmount = null; - return new LoanTransactionData(id, officeId, officeName, transactionType, paymentDetailData, currencyData, date, totalDue, - netDisbursalAmount, principalPortion, interestDue, feeDue, penaltyDue, overPaymentPortion, ExternalId.empty(), transfer, - fixedEmiAmount, outstandingLoanBalance, unrecognizedIncomePortion, manuallyReversed, loanId, ExternalId.empty()); + return LoanTransactionData.builder().id(id).officeId(officeId).officeName(officeName).type(transactionType) + .paymentDetailData(paymentDetailData).currency(currencyData).date(date).amount(totalDue) + .netDisbursalAmount(netDisbursalAmount).principalPortion(principalPortion).interestPortion(interestDue) + .feeChargesPortion(feeDue).penaltyChargesPortion(penaltyDue).overpaymentPortion(overPaymentPortion) + .externalId(ExternalId.empty()).transfer(transfer).outstandingLoanBalance(outstandingLoanBalance) + .unrecognizedIncomePortion(unrecognizedIncomePortion).manuallyReversed(manuallyReversed).loanId(loanId) + .externalLoanId(ExternalId.empty()).build(); } } diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanAdjustTransactionBusinessEventSerializerTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanAdjustTransactionBusinessEventSerializerTest.java index 1bbe3f7cd..e4e66c6ff 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanAdjustTransactionBusinessEventSerializerTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanAdjustTransactionBusinessEventSerializerTest.java @@ -137,11 +137,15 @@ public class LoanAdjustTransactionBusinessEventSerializerTest { String reversedLocalDate = reversedOnDate.format(DateTimeFormatter.ISO_DATE); LoanAdjustTransactionBusinessEvent businessEvent = new LoanAdjustTransactionBusinessEvent(loanAdjustTransactionBusinessEventData); - LoanTransactionData transactionToAdjustData = new LoanTransactionData(1L, 1L, "", LoanEnumerations.transactionType(2), null, null, - LocalDate.now(ZoneId.systemDefault()), BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), - BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), - new ExternalId("testExternalId"), null, null, BigDecimal.valueOf(0.0), LocalDate.now(ZoneId.systemDefault()).minusDays(4), - true, new ExternalId("testReversalExternalId"), reversedOnDate, 1L, new ExternalId("testExternalLoanId")); + LoanTransactionData transactionToAdjustData = LoanTransactionData.builder().id(1L).officeId(1L).officeName("") + .type(LoanEnumerations.transactionType(2)).date(LocalDate.now(ZoneId.systemDefault())).amount(BigDecimal.valueOf(0.0)) + .netDisbursalAmount(BigDecimal.valueOf(0.0)).principalPortion(BigDecimal.valueOf(0.0)) + .interestPortion(BigDecimal.valueOf(0.0)).feeChargesPortion(BigDecimal.valueOf(0.0)) + .penaltyChargesPortion(BigDecimal.valueOf(0.0)).overpaymentPortion(BigDecimal.valueOf(0.0)) + .unrecognizedIncomePortion(BigDecimal.valueOf(0.0)).externalId(new ExternalId("testExternalId")) + .outstandingLoanBalance(BigDecimal.valueOf(0.0)).submittedOnDate(LocalDate.now(ZoneId.systemDefault()).minusDays(4)) + .manuallyReversed(true).reversalExternalId(new ExternalId("testReversalExternalId")).reversedOnDate(reversedOnDate) + .loanId(1L).externalLoanId(new ExternalId("testExternalLoanId")).build(); when(service.retrieveLoanTransaction(anyLong(), anyLong())).thenReturn(transactionToAdjustData); when(loanChargePaidByReadService.fetchLoanChargesPaidByDataTransactionId(anyLong())).thenReturn(new ArrayList<>());