FINERACT-2326: Improve LoanTransactionData DTO structure

This commit is contained in:
Adam Saghy
2025-09-02 20:20:33 +02:00
committed by Arnold Gálovics
parent d06eb618d0
commit 937bfe529c
6 changed files with 703 additions and 351 deletions
@@ -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<LoanChargePaidByData> loanChargePaidByList;
// templates
@@ -104,334 +109,74 @@ public class LoanTransactionData implements Serializable {
private final ExternalId reversalExternalId;
private LocalDate reversedOnDate;
@Setter
private List<LoanTransactionRelationData> transactionRelations;
private Collection<CodeValueData> 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<PaymentTypeData> 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<PaymentTypeData> 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<PaymentTypeData> 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<PaymentTypeData> 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<PaymentTypeData> paymentOptions, final BigDecimal retriveLastEmiAmount,
final BigDecimal netDisbursalAmount, final Collection<PaymentTypeData> 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<PaymentTypeData> 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<CodeValueData> writeOffReasonOptions) {
this.writeOffReasonOptions = writeOffReasonOptions;
}
public void setChargeOffReasonOptions(Collection<CodeValueData> chargeOffReasonOptions) {
this.chargeOffReasonOptions = chargeOffReasonOptions;
}
public void setLoanChargePaidByList(Collection<LoanChargePaidByData> loanChargePaidByList) {
this.loanChargePaidByList = loanChargePaidByList;
}
public void setLoanTransactionRelations(List<LoanTransactionRelationData> 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();
}
}
@@ -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);
}
@@ -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<LoanTransactionRelationData> transactionRelations = mock(List.class);
List<LoanChargePaidByData> loanChargePaidByList = mock(List.class);
List<LoanRepaymentScheduleInstallmentData> loanRepaymentScheduleInstallments = mock(List.class);
Collection<CodeValueData> writeOffReasonOptions = mock(List.class);
Collection<CodeValueData> chargeOffReasonOptions = mock(List.class);
Collection<PaymentTypeData> 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<PaymentTypeData> 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<PaymentTypeData> 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<PaymentTypeData> 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());
}
}
@@ -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());
}
}
@@ -316,7 +316,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
final List<LoanChargePaidByData> 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<PaymentTypeData> 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<CodeValueData> 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<CodeValueData> 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<CurrencyData> {
@@ -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();
}
}
@@ -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<>());