Charge back Loan Transaction updates repayment schedule

This commit is contained in:
Jose Alberto Hernandez
2022-10-12 21:08:09 +02:00
committed by Aleksandar Vidakovic
parent 6d2447e327
commit ae71df17df
21 changed files with 753 additions and 103 deletions
@@ -322,6 +322,14 @@
"null",
"bigdecimal"
]
},
{
"default": null,
"name": "totalCredits",
"type": [
"null",
"bigdecimal"
]
}
]
}
@@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.loanaccount.api;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDate;
import java.util.Set;
/**
* Created by Chirag Gupta on 12/30/17.
@@ -128,6 +129,23 @@ final class LoanTransactionsApiResourceSwagger {
public String displayLabel;
}
static final class GetLoanTransactionRelation {
private GetLoanTransactionRelation() {}
@Schema(example = "1")
public Long fromLoanTransaction;
@Schema(example = "10")
public Long toLoanTransaction;
@Schema(example = "CHARGEBACK")
private String relationType;
@Schema(example = "100.00")
private Double amount;
@Schema(example = "Repayment Adjustment Chargeback")
private String paymentType;
}
@Schema(example = "3")
public Integer id;
public GetLoansType type;
@@ -146,6 +164,7 @@ final class LoanTransactionsApiResourceSwagger {
public String reversalExternalId;
@Schema(example = "[2012, 5, 18]")
public LocalDate reversedOnDate;
public Set<GetLoanTransactionRelation> transactionRelations;
}
@Schema(description = "PostLoansLoanIdTransactionsRequest")
@@ -210,9 +229,9 @@ final class LoanTransactionsApiResourceSwagger {
public String note;
@Schema(example = "95174ff9-1a75-4d72-a413-6f9b1cb988b7")
public String reversalExternalId;
@Schema(example = "3")
@Schema(example = "1")
public Integer paymentTypeId;
@Schema(example = "95174ff9-1a75-4d72-a413-6f9b1cb988b7")
@Schema(example = "4ff9b1cb988b7")
public String externalId;
}
@@ -350,6 +350,8 @@ final class LoansApiResourceSwagger {
public Double totalActualCostOfLoanForPeriod;
@Schema(example = "200.000000")
public Double totalInstallmentAmountForPeriod;
@Schema(example = "2.000000")
public Double totalCredits;
}
static final class GetLoansLoanIdDisbursementDetails {
@@ -151,6 +151,11 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi
case LOAN_CREDIT_BALANCE_REFUND:
newState = closeObligationsMetTransition();
break;
case LOAN_CHARGEBACK:
if (anyOfAllowedWhenComingFrom(from, LoanStatus.CLOSED_OBLIGATIONS_MET, LoanStatus.OVERPAID)) {
newState = activeTransition();
}
break;
default:
break;
}
@@ -1330,7 +1330,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
this.totalOverpaid = null;
} else {
final Money overpaidBy = calculateTotalOverpayment();
this.totalOverpaid = overpaidBy.getAmountDefaultedToNullIfZero();
this.totalOverpaid = null;
if (!overpaidBy.isLessThanZero()) {
this.totalOverpaid = overpaidBy.getAmountDefaultedToNullIfZero();
}
final Money recoveredAmount = calculateTotalRecoveredPayments();
this.totalRecovered = recoveredAmount.getAmountDefaultedToNullIfZero();
@@ -1342,7 +1345,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
}
}
public void updateLoanSummarAndStatus() {
public void updateLoanSummaryAndStatus() {
updateLoanSummaryDerivedFields();
doPostLoanTransactionChecks(getLastUserTransactionDate(), loanLifecycleStateMachine);
}
@@ -3362,14 +3365,18 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
return transactions;
}
private void doPostLoanTransactionChecks(final LocalDate transactionDate, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
private boolean doPostLoanTransactionChecks(final LocalDate transactionDate,
final LoanLifecycleStateMachine loanLifecycleStateMachine) {
boolean statusChanged = false;
if (isOverPaid()) {
// FIXME - kw - update account balance to negative amount.
handleLoanOverpayment(loanLifecycleStateMachine);
statusChanged = true;
} else if (this.summary.isRepaidInFull(loanCurrency())) {
handleLoanRepaymentInFull(transactionDate, loanLifecycleStateMachine);
statusChanged = true;
}
return statusChanged;
}
private void handleLoanRepaymentInFull(final LocalDate transactionDate, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
@@ -3689,9 +3696,11 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
}
for (final LoanTransaction loanTransaction : this.loanTransactions) {
if ((loanTransaction.isRefund() || loanTransaction.isRefundForActiveLoan() || loanTransaction.isCreditBalanceRefund())
&& !loanTransaction.isReversed()) {
totalPaidInRepayments = totalPaidInRepayments.minus(loanTransaction.getAmount(currency));
if (!loanTransaction.isReversed()) {
if ((loanTransaction.isRefund() || loanTransaction.isRefundForActiveLoan() || loanTransaction.isCreditBalanceRefund()
|| loanTransaction.isChargeback())) {
totalPaidInRepayments = totalPaidInRepayments.minus(loanTransaction.getAmount(currency));
}
}
}
@@ -3898,12 +3907,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
return changedTransactionDetail;
}
public void handleChargebackTransaction(LoanTransaction chargebackTransaction,
final LoanLifecycleStateMachine loanLifecycleStateMachine) {
chargebackTransaction.updateLoan(this);
// TODO To be updated in the repayment schedule
}
/**
* Behaviour added to comply with capability of previous mifos product to support easier transition to fineract
* platform.
@@ -5678,6 +5681,14 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
if (loanTransaction.isDisbursement() || loanTransaction.isIncomePosting()) {
outstanding = outstanding.plus(loanTransaction.getAmount(getCurrency()));
loanTransaction.updateOutstandingLoanBalance(outstanding.getAmount());
} else if (loanTransaction.isChargeback()) {
Money transactionOutstanding = loanTransaction.getAmount(getCurrency());
if (!loanTransaction.getOverPaymentPortion(getCurrency()).isZero()) {
transactionOutstanding = transactionOutstanding.minus(loanTransaction.getOverPaymentPortion(getCurrency()));
}
outstanding = outstanding.plus(transactionOutstanding);
loanTransaction.updateOutstandingLoanBalance(outstanding.getAmount());
} else {
if (this.loanInterestRecalculationDetails != null
&& this.loanInterestRecalculationDetails.isCompoundingToBePostedAsTransaction()
@@ -6125,6 +6136,39 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
return changedTransactionDetail;
}
public void handleChargebackTransaction(final LoanTransaction chargebackTransaction,
final LoanLifecycleStateMachine loanLifecycleStateMachine) {
chargebackTransaction.updateLoan(this);
if (!chargebackTransaction.isChargeback()) {
final String errorMessage = "A transaction of type chargeback was expected but not received.";
throw new InvalidLoanTransactionTypeException("transaction", "is.not.a.chargeback.transaction", errorMessage);
}
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategy);
final Money overpaidAmount = calculateTotalOverpayment(); // Before Transaction
if (overpaidAmount.isGreaterThanZero()) {
Money difference = chargebackTransaction.getAmount(getCurrency()).minus(overpaidAmount);
if (difference.isLessThanZero()) {
difference = null;
}
chargebackTransaction.setOverPayments(difference);
}
if (chargebackTransaction.isNotZero(loanCurrency())) {
addLoanTransaction(chargebackTransaction);
}
loanRepaymentScheduleTransactionProcessor.handleChargeback(chargebackTransaction, getCurrency(), overpaidAmount,
getRepaymentScheduleInstallments());
updateLoanSummaryDerivedFields();
if (!doPostLoanTransactionChecks(chargebackTransaction.getTransactionDate(), loanLifecycleStateMachine)) {
loanLifecycleStateMachine.transition(LoanEvent.LOAN_CHARGEBACK, this);
}
}
public LocalDate possibleNextRefundDate() {
final LocalDate now = DateUtils.getBusinessLocalDate();
@@ -126,6 +126,12 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
@Column(name = "recalculated_interest_component", nullable = false)
private boolean recalculatedInterestComponent;
@Column(name = "is_additional", nullable = false)
private boolean additional;
@Column(name = "credits_amount", scale = 6, precision = 19, nullable = true)
private BigDecimal credits;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER, mappedBy = "loanRepaymentScheduleInstallment")
private Set<LoanInterestRecalcualtionAdditionalDetails> loanCompoundingDetails = new HashSet<>();
@@ -223,6 +229,10 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
return this.dueDate;
}
public Money getCredits(final MonetaryCurrency currency) {
return Money.of(currency, this.credits);
}
public Money getPrincipal(final MonetaryCurrency currency) {
return Money.of(currency, this.principal);
}
@@ -688,6 +698,24 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
this.principal = principal;
}
public void addToPrincipal(final LocalDate transactionDate, final Money transactionAmount) {
if (this.principal == null) {
this.principal = transactionAmount.getAmount();
} else {
this.principal = this.principal.add(transactionAmount.getAmount());
}
checkIfRepaymentPeriodObligationsAreMet(transactionDate, transactionAmount.getCurrency());
}
public void addToCredits(final BigDecimal amount) {
if (this.credits == null) {
this.credits = amount;
} else {
this.credits = this.credits.add(amount);
}
}
public static Comparator<LoanRepaymentScheduleInstallment> installmentNumberComparator = new Comparator<LoanRepaymentScheduleInstallment>() {
@Override
@@ -816,6 +844,12 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
}
}
public void updateDueChargeback(final LocalDate transactionDate, final Money transactionAmount) {
updateDueDate(transactionDate);
addToCredits(transactionAmount.getAmount());
addToPrincipal(transactionDate, transactionAmount);
}
public Money getDue(MonetaryCurrency currency) {
return getPrincipal(currency).plus(getInterestCharged(currency)).plus(getFeeChargesCharged(currency))
.plus(getPenaltyChargesCharged(currency));
@@ -851,4 +885,13 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
public Set<LoanInstallmentCharge> getInstallmentCharges() {
return installmentCharges;
}
public boolean isAdditional() {
return additional;
}
public void markAsAdditional() {
this.additional = true;
}
}
@@ -35,7 +35,7 @@ public final class LoanSummaryWrapper {
final MonetaryCurrency currency) {
Money total = Money.zero(currency);
for (final LoanRepaymentScheduleInstallment installment : repaymentScheduleInstallments) {
total = total.plus(installment.getPrincipalCompleted(currency));
total = total.plus(installment.getPrincipalCompleted(currency)).minus(installment.getCredits(currency));
}
return total;
}
@@ -158,9 +158,9 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
return new LoanTransaction(null, office, LoanTransactionType.REPAYMENT, paymentDetail, amount.getAmount(), paymentDate, externalId);
}
public static LoanTransaction chargeback(final Loan loan, final Money amount, final PaymentDetail paymentDetail,
public static LoanTransaction chargeback(final Office office, final Money amount, final PaymentDetail paymentDetail,
final LocalDate paymentDate, final String externalId) {
LoanTransaction loanTransaction = new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.CHARGEBACK, paymentDetail,
LoanTransaction loanTransaction = new LoanTransaction(null, office, LoanTransactionType.CHARGEBACK, paymentDetail,
amount.getAmount(), paymentDate, externalId);
loanTransaction.principalPortion = amount.getAmount();
return loanTransaction;
@@ -459,6 +459,12 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
this.overPaymentPortion = defaultToNullIfZero(getOverPaymentPortion(currency).plus(overPayment).getAmount());
}
public void setOverPayments(final Money overPayment) {
if (overPayment != null) {
this.overPaymentPortion = defaultToNullIfZero(overPayment.getAmount());
}
}
public Money getPrincipalPortion(final MonetaryCurrency currency) {
return Money.of(currency, this.principalPortion);
}
@@ -863,6 +869,10 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
return submittedOnDate;
}
public boolean hasLoanTransactionRelations() {
return (loanTransactionRelations != null && loanTransactionRelations.size() > 0);
}
public Set<LoanTransactionRelation> getLoanTransactionRelations() {
return loanTransactionRelations;
}
@@ -30,9 +30,11 @@ import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidDetail;
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
import org.apache.fineract.portfolio.loanaccount.domain.LoanInstallmentCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalcualtionAdditionalDetails;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
@@ -505,6 +507,71 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
return false;
}
@Override
public void handleChargeback(LoanTransaction loanTransaction, MonetaryCurrency currency, Money overpaidAmount,
List<LoanRepaymentScheduleInstallment> installments) {
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
final Comparator<LoanRepaymentScheduleInstallment> byDate = new Comparator<LoanRepaymentScheduleInstallment>() {
@Override
public int compare(LoanRepaymentScheduleInstallment ord1, LoanRepaymentScheduleInstallment ord2) {
return ord1.getDueDate().compareTo(ord2.getDueDate());
}
};
Collections.sort(installments, byDate);
final Money zeroMoney = Money.zero(currency);
Money transactionAmountUnprocessed = loanTransaction.getAmount(currency);
if (overpaidAmount.isGreaterThanZero()) {
transactionAmountUnprocessed = loanTransaction.getAmount(currency).minus(overpaidAmount);
if (transactionAmountUnprocessed.isLessThanZero()) {
transactionAmountUnprocessed = zeroMoney;
}
}
if (transactionAmountUnprocessed.isGreaterThanZero()) {
final LocalDate transactionDate = loanTransaction.getTransactionDate();
boolean loanTransactionMapped = false;
LocalDate pastDueDate = null;
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
pastDueDate = currentInstallment.getDueDate();
if (!currentInstallment.isAdditional() && currentInstallment.getDueDate().isAfter(transactionDate)) {
currentInstallment.addToPrincipal(transactionDate, transactionAmountUnprocessed);
currentInstallment.addToCredits(transactionAmountUnprocessed.getAmount());
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
transactionAmountUnprocessed, zeroMoney, zeroMoney, zeroMoney));
loanTransactionMapped = true;
break;
// If already exists an additional installment just update the due date and
// principal from the Loan charge back transaction
} else if (currentInstallment.isAdditional()) {
currentInstallment.updateDueChargeback(transactionDate, transactionAmountUnprocessed);
loanTransactionMapped = true;
break;
}
}
// New installment will be added (N+1 scenario)
if (!loanTransactionMapped) {
Loan loan = loanTransaction.getLoan();
final Set<LoanInterestRecalcualtionAdditionalDetails> compoundingDetails = null;
LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan, (installments.size() + 1),
pastDueDate, transactionDate, transactionAmountUnprocessed.getAmount(), zeroMoney.getAmount(),
zeroMoney.getAmount(), zeroMoney.getAmount(), false, compoundingDetails);
installment.markAsAdditional();
installment.addToCredits(transactionAmountUnprocessed.getAmount());
loan.addLoanRepaymentScheduleInstallment(installment);
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, installment,
transactionAmountUnprocessed, zeroMoney, zeroMoney, zeroMoney));
}
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
}
}
@Override
public void handleRefund(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, final Set<LoanCharge> charges) {
@@ -50,6 +50,9 @@ public interface LoanRepaymentScheduleTransactionProcessor {
void handleRefund(LoanTransaction loanTransaction, MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments,
Set<LoanCharge> charges);
void handleChargeback(LoanTransaction loanTransaction, MonetaryCurrency currency, Money overpaidAmount,
List<LoanRepaymentScheduleInstallment> installments);
void processTransactionsFromDerivedFields(List<LoanTransaction> transactionsPostDisbursement, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges);
@@ -53,6 +53,7 @@ public class LoanScheduleData {
private final BigDecimal totalPaidInAdvance;
private final BigDecimal totalPaidLate;
private final BigDecimal totalOutstanding;
private final BigDecimal totalCredits;
/**
* <code>periods</code> is collection of data objects containing specific information to each period of the loan
@@ -67,7 +68,7 @@ public class LoanScheduleData {
final BigDecimal totalInterestCharged, final BigDecimal totalFeeChargesCharged, final BigDecimal totalPenaltyChargesCharged,
final BigDecimal totalWaived, final BigDecimal totalWrittenOff, final BigDecimal totalRepaymentExpected,
final BigDecimal totalRepayment, final BigDecimal totalPaidInAdvance, final BigDecimal totalPaidLate,
final BigDecimal totalOutstanding) {
final BigDecimal totalOutstanding, final BigDecimal totalCredits) {
this.currency = currency;
this.periods = periods;
this.loanTermInDays = loanTermInDays;
@@ -84,6 +85,7 @@ public class LoanScheduleData {
this.totalPaidInAdvance = totalPaidInAdvance;
this.totalPaidLate = totalPaidLate;
this.totalOutstanding = totalOutstanding;
this.totalCredits = totalCredits;
}
public LoanScheduleData(final CurrencyData currency, final Collection<LoanSchedulePeriodData> periods, final Integer loanTermInDays,
@@ -105,6 +107,7 @@ public class LoanScheduleData {
this.totalPaidInAdvance = null;
this.totalPaidLate = null;
this.totalOutstanding = null;
this.totalCredits = BigDecimal.ZERO;
}
public void updateFuturePeriods(Collection<LoanSchedulePeriodData> futurePeriods) {
@@ -71,6 +71,7 @@ public final class LoanSchedulePeriodData {
private final BigDecimal totalOverdue;
private final BigDecimal totalActualCostOfLoanForPeriod;
private final BigDecimal totalInstallmentAmountForPeriod;
private final BigDecimal totalCredits;
public static LoanSchedulePeriodData disbursementOnlyPeriod(final LocalDate disbursementDate, final BigDecimal principalDisbursed,
final BigDecimal feeChargesDueAtTimeOfDisbursement, final boolean isDisbursed) {
@@ -102,7 +103,7 @@ public final class LoanSchedulePeriodData {
final BigDecimal totalDueForPeriod, final BigDecimal totalPaid, final BigDecimal totalPaidInAdvanceForPeriod,
final BigDecimal totalPaidLateForPeriod, final BigDecimal totalWaived, final BigDecimal totalWrittenOff,
final BigDecimal totalOutstanding, final BigDecimal totalActualCostOfLoanForPeriod,
final BigDecimal totalInstallmentAmountForPeriod) {
final BigDecimal totalInstallmentAmountForPeriod, final BigDecimal totalCredits) {
return new LoanSchedulePeriodData(periodNumber, fromDate, dueDate, obligationsMetOnDate, complete, principalOriginalDue,
principalPaid, principalWrittenOff, principalOutstanding, outstandingPrincipalBalanceOfLoan,
@@ -110,7 +111,7 @@ public final class LoanSchedulePeriodData {
feeChargesPaid, feeChargesWaived, feeChargesWrittenOff, feeChargesOutstanding, penaltyChargesDue, penaltyChargesPaid,
penaltyChargesWaived, penaltyChargesWrittenOff, penaltyChargesOutstanding, totalDueForPeriod, totalPaid,
totalPaidInAdvanceForPeriod, totalPaidLateForPeriod, totalWaived, totalWrittenOff, totalOutstanding,
totalActualCostOfLoanForPeriod, totalInstallmentAmountForPeriod);
totalActualCostOfLoanForPeriod, totalInstallmentAmountForPeriod, totalCredits);
}
public static LoanSchedulePeriodData withPaidDetail(final LoanSchedulePeriodData loanSchedulePeriodData, final boolean complete,
@@ -130,7 +131,8 @@ public final class LoanSchedulePeriodData {
loanSchedulePeriodData.totalPaidForPeriod, loanSchedulePeriodData.totalPaidInAdvanceForPeriod,
loanSchedulePeriodData.totalPaidLateForPeriod, loanSchedulePeriodData.totalWaivedForPeriod,
loanSchedulePeriodData.totalWrittenOffForPeriod, loanSchedulePeriodData.totalOutstandingForPeriod,
loanSchedulePeriodData.totalActualCostOfLoanForPeriod, loanSchedulePeriodData.totalInstallmentAmountForPeriod);
loanSchedulePeriodData.totalActualCostOfLoanForPeriod, loanSchedulePeriodData.totalInstallmentAmountForPeriod,
loanSchedulePeriodData.totalCredits);
}
/*
@@ -197,6 +199,7 @@ public final class LoanSchedulePeriodData {
} else {
this.totalOverdue = null;
}
this.totalCredits = BigDecimal.ZERO;
}
/*
@@ -259,6 +262,7 @@ public final class LoanSchedulePeriodData {
} else {
this.totalOverdue = null;
}
this.totalCredits = BigDecimal.ZERO;
}
/*
@@ -276,7 +280,8 @@ public final class LoanSchedulePeriodData {
final BigDecimal penaltyChargesWrittenOff, final BigDecimal penaltyChargesOutstanding, final BigDecimal totalDueForPeriod,
final BigDecimal totalPaid, final BigDecimal totalPaidInAdvanceForPeriod, final BigDecimal totalPaidLateForPeriod,
final BigDecimal totalWaived, final BigDecimal totalWrittenOff, final BigDecimal totalOutstanding,
final BigDecimal totalActualCostOfLoanForPeriod, final BigDecimal totalInstallmentAmountForPeriod) {
final BigDecimal totalActualCostOfLoanForPeriod, final BigDecimal totalInstallmentAmountForPeriod,
final BigDecimal totalCredits) {
this.period = periodNumber;
this.fromDate = fromDate;
this.dueDate = dueDate;
@@ -330,6 +335,7 @@ public final class LoanSchedulePeriodData {
} else {
this.totalOverdue = null;
}
this.totalCredits = totalCredits;
}
private BigDecimal defaultToZeroIfNull(final BigDecimal possibleNullValue) {
@@ -95,6 +95,8 @@ public final class LoanScheduleModel {
final Integer inMultiplesOf = this.totalPrincipalDisbursed.getCurrencyInMultiplesOf();
final CurrencyData currency = this.applicationCurrency.toData(decimalPlaces, inMultiplesOf);
final BigDecimal totalCredits = BigDecimal.ZERO;
final Collection<LoanSchedulePeriodData> periodsData = new ArrayList<>();
for (final LoanScheduleModelPeriod modelPeriod : this.periods) {
periodsData.add(modelPeriod.toData());
@@ -109,7 +111,7 @@ public final class LoanScheduleModel {
return new LoanScheduleData(currency, periodsData, this.loanTermInDays, this.totalPrincipalDisbursed.getAmount(),
this.totalPrincipalExpected, this.totalPrincipalPaid, this.totalInterestCharged, this.totalFeeChargesCharged,
this.totalPenaltyChargesCharged, totalWaived, totalWrittenOff, this.totalRepaymentExpected, totalRepayment,
totalPaidInAdvance, totalPaidLate, this.totalOutstanding);
totalPaidInAdvance, totalPaidLate, this.totalOutstanding, totalCredits);
}
public Collection<LoanScheduleModelPeriod> getPeriods() {
@@ -89,6 +89,8 @@ public final class LoanRescheduleModel {
final Integer inMultiplesOf = this.totalPrincipalDisbursed.getCurrencyInMultiplesOf();
final CurrencyData currency = this.applicationCurrency.toData(decimalPlaces, inMultiplesOf);
final BigDecimal totalCredits = BigDecimal.ZERO;
final Collection<LoanSchedulePeriodData> periodsData = new ArrayList<>();
for (final LoanRescheduleModalPeriod modelPeriod : this.periods) {
periodsData.add(modelPeriod.toData());
@@ -103,7 +105,7 @@ public final class LoanRescheduleModel {
return new LoanScheduleData(currency, periodsData, this.loanTermInDays, this.totalPrincipalDisbursed.getAmount(),
this.totalPrincipalExpected, this.totalPrincipalPaid, this.totalInterestCharged, this.totalFeeChargesCharged,
this.totalPenaltyChargesCharged, totalWaived, totalWrittenOff, this.totalRepaymentExpected, totalRepayment,
totalPaidInAdvance, totalPaidLate, this.totalOutstanding);
totalPaidInAdvance, totalPaidLate, this.totalOutstanding, totalCredits);
}
public Collection<LoanRescheduleModelRepaymentPeriod> getPeriods() {
@@ -1078,11 +1078,12 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
public String schema() {
return " ls.loan_id as loanId, ls.installment as period, ls.fromdate as fromDate, ls.duedate as dueDate, ls.obligations_met_on_date as obligationsMetOnDate, ls.completed_derived as complete,"
+ " ls.principal_amount as principalDue, ls.principal_completed_derived as principalPaid, ls.principal_writtenoff_derived as principalWrittenOff, "
+ " ls.principal_amount as principalDue, ls.principal_completed_derived as principalPaid, ls.principal_writtenoff_derived as principalWrittenOff, ls.is_additional as isAdditional, "
+ " ls.interest_amount as interestDue, ls.interest_completed_derived as interestPaid, ls.interest_waived_derived as interestWaived, ls.interest_writtenoff_derived as interestWrittenOff, "
+ " ls.fee_charges_amount as feeChargesDue, ls.fee_charges_completed_derived as feeChargesPaid, ls.fee_charges_waived_derived as feeChargesWaived, ls.fee_charges_writtenoff_derived as feeChargesWrittenOff, "
+ " ls.penalty_charges_amount as penaltyChargesDue, ls.penalty_charges_completed_derived as penaltyChargesPaid, ls.penalty_charges_waived_derived as penaltyChargesWaived, ls.penalty_charges_writtenoff_derived as penaltyChargesWrittenOff, "
+ " ls.total_paid_in_advance_derived as totalPaidInAdvanceForPeriod, ls.total_paid_late_derived as totalPaidLateForPeriod "
+ " ls.penalty_charges_amount as penaltyChargesDue, ls.penalty_charges_completed_derived as penaltyChargesPaid, ls.penalty_charges_waived_derived as penaltyChargesWaived, "
+ " ls.penalty_charges_writtenoff_derived as penaltyChargesWrittenOff, ls.total_paid_in_advance_derived as totalPaidInAdvanceForPeriod, "
+ " ls.total_paid_late_derived as totalPaidLateForPeriod, ls.credits_amount as totalCredits "
+ " from m_loan_repayment_schedule ls ";
}
@@ -1128,6 +1129,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
Money totalPaidInAdvance = Money.zero(monCurrency);
Money totalPaidLate = Money.zero(monCurrency);
Money totalOutstanding = Money.zero(monCurrency);
Money totalCredits = Money.zero(monCurrency);
// update totals with details of fees charged during disbursement
totalFeeChargesCharged = totalFeeChargesCharged.plus(disbursementPeriod.getFeeChargesDue().subtract(waivedChargeAmount));
@@ -1144,37 +1146,47 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
final LocalDate dueDate = JdbcSupport.getLocalDate(rs, "dueDate");
final LocalDate obligationsMetOnDate = JdbcSupport.getLocalDate(rs, "obligationsMetOnDate");
final boolean complete = rs.getBoolean("complete");
final boolean isAdditional = rs.getBoolean("isAdditional");
BigDecimal principal = BigDecimal.ZERO;
for (final DisbursementData data : disbursementData) {
if (fromDate.equals(this.disbursement.disbursementDate()) && data.disbursementDate().equals(fromDate)) {
principal = principal.add(data.getPrincipal());
LoanSchedulePeriodData periodData = null;
if (data.getChargeAmount() == null) {
periodData = LoanSchedulePeriodData.disbursementOnlyPeriod(data.disbursementDate(), data.getPrincipal(),
disbursementChargeAmount, data.isDisbursed());
} else {
periodData = LoanSchedulePeriodData.disbursementOnlyPeriod(data.disbursementDate(), data.getPrincipal(),
disbursementChargeAmount.add(data.getChargeAmount()).subtract(waivedChargeAmount), data.isDisbursed());
}
periods.add(periodData);
this.outstandingLoanPrincipalBalance = this.outstandingLoanPrincipalBalance.add(data.getPrincipal());
} else if (data.isDueForDisbursement(fromDate, dueDate)) {
if (!excludePastUndisbursed || data.isDisbursed()
|| !data.disbursementDate().isBefore(DateUtils.getBusinessLocalDate())) {
if (!isAdditional) {
for (final DisbursementData data : disbursementData) {
if (fromDate.equals(this.disbursement.disbursementDate()) && data.disbursementDate().equals(fromDate)) {
principal = principal.add(data.getPrincipal());
LoanSchedulePeriodData periodData;
LoanSchedulePeriodData periodData = null;
if (data.getChargeAmount() == null) {
periodData = LoanSchedulePeriodData.disbursementOnlyPeriod(data.disbursementDate(), data.getPrincipal(),
BigDecimal.ZERO, data.isDisbursed());
disbursementChargeAmount, data.isDisbursed());
} else {
periodData = LoanSchedulePeriodData.disbursementOnlyPeriod(data.disbursementDate(), data.getPrincipal(),
data.getChargeAmount(), data.isDisbursed());
disbursementChargeAmount.add(data.getChargeAmount()).subtract(waivedChargeAmount),
data.isDisbursed());
}
periods.add(periodData);
this.outstandingLoanPrincipalBalance = this.outstandingLoanPrincipalBalance.add(data.getPrincipal());
} else if (data.isDueForDisbursement(fromDate, dueDate)) {
if (!excludePastUndisbursed || data.isDisbursed()
|| !data.disbursementDate().isBefore(DateUtils.getBusinessLocalDate())) {
principal = principal.add(data.getPrincipal());
LoanSchedulePeriodData periodData;
if (data.getChargeAmount() == null) {
periodData = LoanSchedulePeriodData.disbursementOnlyPeriod(data.disbursementDate(), data.getPrincipal(),
BigDecimal.ZERO, data.isDisbursed());
} else {
periodData = LoanSchedulePeriodData.disbursementOnlyPeriod(data.disbursementDate(), data.getPrincipal(),
data.getChargeAmount(), data.isDisbursed());
}
periods.add(periodData);
this.outstandingLoanPrincipalBalance = this.outstandingLoanPrincipalBalance.add(data.getPrincipal());
}
}
}
}
// Add the Charge back or Credits to the initial amount to avoid negative balance
final BigDecimal credits = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "totalCredits");
if (!isAdditional) {
this.outstandingLoanPrincipalBalance = this.outstandingLoanPrincipalBalance.add(credits);
}
totalPrincipalDisbursed = totalPrincipalDisbursed.add(principal);
Integer daysInPeriod = 0;
@@ -1235,6 +1247,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
final BigDecimal totalWrittenOffForPeriod = principalWrittenOff.add(interestWrittenOff).add(feeChargesWrittenOff)
.add(penaltyChargesWrittenOff);
totalWrittenOff = totalWrittenOff.plus(totalWrittenOffForPeriod);
final BigDecimal totalOutstandingForPeriod = principalOutstanding.add(interestOutstanding).add(feeChargesOutstanding)
.add(penaltyChargesOutstanding);
@@ -1249,11 +1262,18 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
if (fromDate == null) {
fromDate = this.lastDueDate;
}
final BigDecimal outstandingPrincipalBalanceOfLoan = this.outstandingLoanPrincipalBalance.subtract(principalDue);
BigDecimal outstandingPrincipalBalanceOfLoan = this.outstandingLoanPrincipalBalance.subtract(principalDue);
if (isAdditional) {
outstandingPrincipalBalanceOfLoan = this.outstandingLoanPrincipalBalance.add(principalDue);
}
// update based on current period values
this.lastDueDate = dueDate;
this.outstandingLoanPrincipalBalance = this.outstandingLoanPrincipalBalance.subtract(principalDue);
if (isAdditional) {
this.outstandingLoanPrincipalBalance = this.outstandingLoanPrincipalBalance.add(principalDue);
}
final LoanSchedulePeriodData periodData = LoanSchedulePeriodData.repaymentPeriodWithPayments(loanId, period, fromDate,
dueDate, obligationsMetOnDate, complete, principalDue, principalPaid, principalWrittenOff, principalOutstanding,
@@ -1262,7 +1282,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
feeChargesOutstanding, penaltyChargesExpectedDue, penaltyChargesPaid, penaltyChargesWaived,
penaltyChargesWrittenOff, penaltyChargesOutstanding, totalDueForPeriod, totalPaidForPeriod,
totalPaidInAdvanceForPeriod, totalPaidLateForPeriod, totalWaivedForPeriod, totalWrittenOffForPeriod,
totalOutstandingForPeriod, totalActualCostOfLoanForPeriod, totalInstallmentAmount);
totalOutstandingForPeriod, totalActualCostOfLoanForPeriod, totalInstallmentAmount, credits);
periods.add(periodData);
}
@@ -1271,7 +1291,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
totalPrincipalPaid.getAmount(), totalInterestCharged.getAmount(), totalFeeChargesCharged.getAmount(),
totalPenaltyChargesCharged.getAmount(), totalWaived.getAmount(), totalWrittenOff.getAmount(),
totalRepaymentExpected.getAmount(), totalRepayment.getAmount(), totalPaidInAdvance.getAmount(),
totalPaidLate.getAmount(), totalOutstanding.getAmount());
totalPaidLate.getAmount(), totalOutstanding.getAmount(), totalCredits.getAmount());
}
}
@@ -2142,6 +2162,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
final BigDecimal totalOutstanding = null;
final BigDecimal totalPaid = null;
final BigDecimal totalInstallmentAmount = null;
final BigDecimal totalCredits = null;
return LoanSchedulePeriodData.repaymentPeriodWithPayments(loanId, period, fromDate, dueDate, obligationsMetOnDate, complete,
principalOriginalDue, principalPaid, principalWrittenOff, principalOutstanding, outstandingPrincipalBalanceOfLoan,
@@ -2149,7 +2170,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
feeChargesPaid, feeChargesWaived, feeChargesWrittenOff, feeChargesOutstanding, penaltyChargesDue, penaltyChargesPaid,
penaltyChargesWaived, penaltyChargesWrittenOff, penaltyChargesOutstanding, totalDueForPeriod, totalPaid,
totalPaidInAdvanceForPeriod, totalPaidLateForPeriod, totalWaived, totalWrittenOff, totalOutstanding,
totalActualCostOfLoanForPeriod, totalInstallmentAmount);
totalActualCostOfLoanForPeriod, totalInstallmentAmount, totalCredits);
}
}
@@ -1074,6 +1074,18 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
"Loan transaction:" + transactionId + " update not allowed as loan status is written off", transactionId);
}
if (transactionToAdjust.hasLoanTransactionRelations()) {
throw new PlatformServiceUnavailableException("error.msg.loan.transaction.update.not.allowed",
"Loan transaction:" + transactionId + " update not allowed as loan transaction is linked to other transactions",
transactionId);
}
if (transactionToAdjust.isChargeback()) {
throw new PlatformServiceUnavailableException("error.msg.loan.transaction.update.not.allowed", "Loan transaction:"
+ transactionId + " update not allowed as loan transaction is charge back and is linked to other transaction",
transactionId);
}
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
final String txnExternalId = command.stringValueOfParameterNamedAllowingNull("externalId");
@@ -1168,7 +1180,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
if (!transactionIds.isEmpty()) {
this.accountTransfersWritePlatformService.reverseTransfersWithFromAccountTransactions(transactionIds,
PortfolioAccountType.LOAN);
loan.updateLoanSummarAndStatus();
loan.updateLoanSummaryAndStatus();
}
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
@@ -1217,9 +1229,17 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
LoanTransaction loanTransaction = this.loanTransactionRepository.findById(transactionId)
.orElseThrow(() -> new LoanTransactionNotFoundException(transactionId));
if (!loanTransaction.isRepayment()) {
if (loanTransaction.isReversed()) {
throw new PlatformServiceUnavailableException("error.msg.loan.chargeback.operation.not.allowed",
"Loan transaction:" + transactionId + " chargeback not allowed as loan transaction is not repayment", transactionId);
"Loan transaction:" + transactionId + " chargeback not allowed as loan transaction repayment is reversed",
transactionId);
}
if (!loanTransaction.isRepayment()) {
throw new PlatformServiceUnavailableException(
"error.msg.loan.chargeback.operation.not.allowed", "Loan transaction:" + transactionId
+ " chargeback not allowed as loan transaction is not repayment, is " + loanTransaction.getTypeOf().getCode(),
transactionId);
}
businessEventNotifierService.notifyPreBusinessEvent(new LoanChargebackTransactionBusinessEvent(loanTransaction));
@@ -1236,23 +1256,23 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final Money transactionAmountAsMoney = Money.of(loan.getCurrency(), transactionAmount);
final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createPaymentDetail(command, changes);
LoanTransaction newTransaction = LoanTransaction.chargeback(loan, transactionAmountAsMoney, paymentDetail, transactionDate,
txnExternalId);
LoanTransaction newTransaction = LoanTransaction.chargeback(loan.getOffice(), transactionAmountAsMoney, paymentDetail,
transactionDate, txnExternalId);
validateLoanTransactionAmountChargeBack(loanTransaction, newTransaction);
this.paymentDetailWritePlatformService.persistPaymentDetail(paymentDetail);
loan.handleChargebackTransaction(newTransaction, defaultLoanLifecycleStateMachine());
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
// Store the Loan Transaction Relation
LoanTransactionRelation loanTransactionRelation = LoanTransactionRelation.instance(loanTransaction, newTransaction,
LoanTransactionRelationTypeEnum.CHARGEBACK);
this.loanTransactionRelationRepository.save(loanTransactionRelation);
this.loanTransactionRepository.saveAndFlush(newTransaction);
this.loanTransactionRepository.save(newTransaction);
loan.handleChargebackTransaction(newTransaction, defaultLoanLifecycleStateMachine());
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
final String noteText = command.stringValueOfParameterNamed(LoanApiConstants.noteParamName);
if (StringUtils.isNotBlank(noteText)) {
@@ -1278,8 +1298,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
actualAmount = actualAmount.add(loanTransactionRelation.getToTransaction().getPrincipalPortion());
}
}
actualAmount = actualAmount.add(chargebackTransaction.getPrincipalPortion());
if (actualAmount.compareTo(loanTransaction.getPrincipalPortion()) > 0) {
actualAmount = actualAmount.add(chargebackTransaction.getAmount());
if (loanTransaction.getPrincipalPortion() != null && actualAmount.compareTo(loanTransaction.getPrincipalPortion()) > 0) {
throw new PlatformServiceUnavailableException("error.msg.loan.chargeback.operation.not.allowed",
"Loan transaction:" + loanTransaction.getId() + " chargeback not allowed as loan transaction amount is not enough",
loanTransaction.getId());
@@ -73,4 +73,5 @@
<include file="parts/0051_external_event_table_category_info.xml" relativeToChangelogFile="true"/>
<include file="parts/0052_loan_transaction_chargeback.xml" relativeToChangelogFile="true"/>
<include file="parts/0053_add_external_events_purge_job.xml" relativeToChangelogFile="true"/>
<include file="parts/0054_additional_fields_loan_repayment_schedule.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet id="1" author="fineract">
<addColumn tableName="m_loan_repayment_schedule">
<column name="is_additional" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn>
<addColumn tableName="m_loan_repayment_schedule">
<column defaultValueComputed="NULL" name="credits_amount" type="DECIMAL(19, 6)"/>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -776,8 +776,8 @@ public class DelinquencyBucketsIntegrationTest {
log.info("Loan Id {} with Loan status {}", getLoansLoanIdResponse.getId(), getLoansLoanIdResponse.getStatus().getCode());
// Reverse the Previous Loan Repayment
PostLoansLoanIdTransactionsResponse loansLoanIdReverseTransactions = loanTransactionHelper.reverseLoanRepayment(loanId,
loansLoanIdTransactions.getResourceId(), operationDate);
PostLoansLoanIdTransactionsResponse loansLoanIdReverseTransactions = loanTransactionHelper.reverseLoanTransaction(loanId,
loansLoanIdTransactions.getResourceId(), operationDate, responseSpec);
assertNotNull(loansLoanIdReverseTransactions);
log.info("Loan repayment reverse transaction id {}", loansLoanIdReverseTransactions.getResourceId());
getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.integrationtests;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -30,12 +31,17 @@ import io.restassured.specification.ResponseSpecification;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.client.models.GetLoanProductsProductIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod;
import org.apache.fineract.client.models.GetLoansLoanIdRepaymentSchedule;
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdTransactions;
import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTransactionIdResponse;
import org.apache.fineract.client.models.GetPaymentTypesResponse;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdResponse;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.CommonConstants;
import org.apache.fineract.integrationtests.common.PaymentTypeHelper;
@@ -50,7 +56,12 @@ import org.junit.jupiter.api.Test;
public class LoanTransactionChargebackTest {
private ResponseSpecification responseSpec;
private ResponseSpecification responseSpecError;
private RequestSpecification requestSpec;
private LoanTransactionHelper loanTransactionHelper;
private final String amountVal = "1000";
private LocalDate todaysDate;
private String operationDate;
@BeforeEach
public void setup() {
@@ -58,49 +69,342 @@ public class LoanTransactionChargebackTest {
this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
this.responseSpecError = new ResponseSpecBuilder().expectStatusCode(503).build();
this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec);
this.todaysDate = Utils.getLocalDateOfTenant();
this.operationDate = Utils.dateFormatter.format(this.todaysDate);
}
@Test
public void applyLoanTransactionChargeback() {
final LoanTransactionHelper loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec);
// Client and Loan account creation
final Integer clientId = ClientHelper.createClient(this.requestSpec, this.responseSpec, "01 January 2012");
final GetLoanProductsProductIdResponse getLoanProductsProductResponse = createLoanProduct(loanTransactionHelper, null);
assertNotNull(getLoanProductsProductResponse);
log.info("Loan Product Bucket Name: {}", getLoanProductsProductResponse.getDelinquencyBucket().getName());
final LocalDate todaysDate = Utils.getLocalDateOfTenant();
// Older date to have more than one overdue installment
final LocalDate transactionDate = todaysDate.minusDays(45);
String operationDate = Utils.dateFormatter.format(transactionDate);
final Integer loanId = createLoanAccount(loanTransactionHelper, clientId.toString(),
getLoanProductsProductResponse.getId().toString(), operationDate, "1000");
final Integer loanId = createAccounts(15, 1);
GetLoansLoanIdResponse getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
assertNotNull(getLoansLoanIdResponse);
loanTransactionHelper.printRepaymentSchedule(getLoansLoanIdResponse);
operationDate = Utils.dateFormatter.format(todaysDate);
Float amount = Float.valueOf("1100.0");
Float amount = Float.valueOf(amountVal);
PostLoansLoanIdTransactionsResponse loanIdTransactionsResponse = loanTransactionHelper.makeLoanRepayment(operationDate, amount,
loanId);
assertNotNull(loanIdTransactionsResponse);
final Integer transactionId = loanIdTransactionsResponse.getResourceId();
List<GetPaymentTypesResponse> paymentTypeList = PaymentTypeHelper.getSystemPaymentType(requestSpec, responseSpec);
assertTrue(!paymentTypeList.isEmpty());
getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
assertNotNull(getLoansLoanIdResponse);
loanTransactionHelper.validateLoanStatus(getLoansLoanIdResponse, "loanStatusType.closed.obligations.met");
amount = Float.valueOf("800.0");
final String payload = createChargebackPayload(amount.toString(), paymentTypeList.get(0).getId());
PostLoansLoanIdTransactionsTransactionIdRequest postLoansTransactionCommandRequest = loanTransactionHelper
.applyLoanTransactionCommand(loanId, transactionId, "chargeback", payload);
assertNotNull(postLoansTransactionCommandRequest);
reviewLoanTransactionRelations(loanId, transactionId, 0);
final Integer chargebackTransactionId = applyChargebackTransaction(loanId, transactionId, "1000.00", 0, responseSpec);
reviewLoanTransactionRelations(loanId, chargebackTransactionId, 1);
getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
assertNotNull(getLoansLoanIdResponse);
loanTransactionHelper.validateLoanStatus(getLoansLoanIdResponse, "loanStatusType.active");
loanTransactionHelper.validateLoanPrincipalOustandingBalance(getLoansLoanIdResponse, amount.doubleValue());
// Try to reverse a Loan Transaction charge back
PostLoansLoanIdTransactionsResponse reverseTransactionResponse = loanTransactionHelper.reverseLoanTransaction(loanId,
chargebackTransactionId, operationDate, responseSpecError);
// Try to reverse a Loan Transaction repayment with linked transactions
reverseTransactionResponse = loanTransactionHelper.reverseLoanTransaction(loanId, transactionId, operationDate, responseSpecError);
}
@Test
public void applyLoanTransactionChargebackInLongTermLoan() {
// Client and Loan account creation
final Integer daysToSubtract = 1;
final Integer numberOfRepayments = 3;
final Integer loanId = createAccounts(daysToSubtract, numberOfRepayments);
GetLoansLoanIdResponse getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
assertNotNull(getLoansLoanIdResponse);
loanTransactionHelper.printRepaymentSchedule(getLoansLoanIdResponse);
Float amount = Float.valueOf("333.33");
final LocalDate transactionDate = this.todaysDate.minusMonths(numberOfRepayments - 1).plusDays(3);
String operationDate = Utils.dateFormatter.format(transactionDate);
PostLoansLoanIdTransactionsResponse loanIdTransactionsResponse = loanTransactionHelper.makeLoanRepayment(operationDate, amount,
loanId);
assertNotNull(loanIdTransactionsResponse);
final Integer transactionId = loanIdTransactionsResponse.getResourceId();
reviewLoanTransactionRelations(loanId, transactionId, 0);
final Integer chargebackTransactionId = applyChargebackTransaction(loanId, transactionId, amount.toString(), 0, responseSpec);
reviewLoanTransactionRelations(loanId, transactionId, 1);
getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
assertNotNull(getLoansLoanIdResponse);
loanTransactionHelper.validateLoanPrincipalOustandingBalance(getLoansLoanIdResponse, Double.valueOf(amountVal));
loanTransactionHelper.printRepaymentSchedule(getLoansLoanIdResponse);
GetLoansLoanIdRepaymentSchedule getLoanRepaymentSchedule = getLoansLoanIdResponse.getRepaymentSchedule();
for (GetLoansLoanIdRepaymentPeriod period : getLoanRepaymentSchedule.getPeriods()) {
if (period.getPeriod() != null && period.getPeriod() == 3) {
log.info("Period number {} for due date {} and totalDueForPeriod {}", period.getPeriod(), period.getDueDate(),
period.getTotalDueForPeriod());
assertEquals(Double.valueOf("666.67"), period.getTotalDueForPeriod());
}
}
}
@Test
public void applyLoanTransactionChargebackOverNoRepaymentType() {
// Client and Loan account creation
final Integer loanId = createAccounts(15, 1);
GetLoansLoanIdResponse getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
assertNotNull(getLoansLoanIdResponse);
Set<GetLoansLoanIdTransactions> loanTransactions = getLoansLoanIdResponse.getTransactions();
assertNotNull(loanTransactions);
log.info("Loan Id {} with {} transactions", loanId, loanTransactions.size());
assertEquals(2, loanTransactions.size());
GetLoansLoanIdTransactions loanTransaction = loanTransactions.iterator().next();
log.info("Try to apply the Charge back over transaction Id {} with type {}", loanTransaction.getId(),
loanTransaction.getType().getCode());
applyChargebackTransaction(loanId, loanTransaction.getId().intValue(), amountVal, 0, responseSpecError);
}
@Test
public void applyLoanTransactionChargebackAfterMature() {
// Client and Loan account creation
final Integer loanId = createAccounts(45, 1);
GetLoansLoanIdResponse getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
assertNotNull(getLoansLoanIdResponse);
loanTransactionHelper.printRepaymentSchedule(getLoansLoanIdResponse);
GetLoansLoanIdRepaymentSchedule getLoanRepaymentSchedule = getLoansLoanIdResponse.getRepaymentSchedule();
log.info("Loan with {} periods", getLoanRepaymentSchedule.getPeriods().size());
assertEquals(2, getLoanRepaymentSchedule.getPeriods().size());
Float amount = Float.valueOf(amountVal);
PostLoansLoanIdTransactionsResponse loanIdTransactionsResponse = loanTransactionHelper.makeLoanRepayment(operationDate, amount,
loanId);
assertNotNull(loanIdTransactionsResponse);
final Integer transactionId = loanIdTransactionsResponse.getResourceId();
getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
assertNotNull(getLoansLoanIdResponse);
loanTransactionHelper.validateLoanStatus(getLoansLoanIdResponse, "loanStatusType.closed.obligations.met");
reviewLoanTransactionRelations(loanId, transactionId, 0);
Integer chargebackTransactionId = applyChargebackTransaction(loanId, transactionId, "500.00", 0, responseSpec);
reviewLoanTransactionRelations(loanId, transactionId, 1);
getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
assertNotNull(getLoansLoanIdResponse);
loanTransactionHelper.validateLoanStatus(getLoansLoanIdResponse, "loanStatusType.active");
loanTransactionHelper.validateLoanPrincipalOustandingBalance(getLoansLoanIdResponse, Double.valueOf("500.00"));
// N+1 Scenario
loanTransactionHelper.printRepaymentSchedule(getLoansLoanIdResponse);
getLoanRepaymentSchedule = getLoansLoanIdResponse.getRepaymentSchedule();
log.info("Loan with {} periods", getLoanRepaymentSchedule.getPeriods().size());
assertEquals(3, getLoanRepaymentSchedule.getPeriods().size());
getLoanRepaymentSchedule = getLoansLoanIdResponse.getRepaymentSchedule();
for (GetLoansLoanIdRepaymentPeriod period : getLoanRepaymentSchedule.getPeriods()) {
if (period.getPeriod() != null && period.getPeriod() == 2) {
log.info("Period number {} for due date {} and totalDueForPeriod {}", period.getPeriod(), period.getDueDate(),
period.getTotalDueForPeriod());
assertEquals(Double.valueOf("500.00"), period.getPrincipalDue());
}
}
chargebackTransactionId = applyChargebackTransaction(loanId, transactionId, "300.00", 0, responseSpec);
reviewLoanTransactionRelations(loanId, transactionId, 2);
getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
assertNotNull(getLoansLoanIdResponse);
loanTransactionHelper.validateLoanStatus(getLoansLoanIdResponse, "loanStatusType.active");
loanTransactionHelper.validateLoanPrincipalOustandingBalance(getLoansLoanIdResponse, Double.valueOf("800.00"));
// N+1 Scenario -- Remains the same periods number
loanTransactionHelper.printRepaymentSchedule(getLoansLoanIdResponse);
getLoanRepaymentSchedule = getLoansLoanIdResponse.getRepaymentSchedule();
log.info("Loan with {} periods", getLoanRepaymentSchedule.getPeriods().size());
assertEquals(3, getLoanRepaymentSchedule.getPeriods().size());
getLoanRepaymentSchedule = getLoansLoanIdResponse.getRepaymentSchedule();
for (GetLoansLoanIdRepaymentPeriod period : getLoanRepaymentSchedule.getPeriods()) {
if (period.getPeriod() != null && period.getPeriod() == 2) {
log.info("Period number {} for due date {} and totalDueForPeriod {}", period.getPeriod(), period.getDueDate(),
period.getTotalDueForPeriod());
assertEquals(Double.valueOf("800.00"), period.getPrincipalDue());
}
}
}
@Test
public void applyLoanTransactionChargebackWithLoanOverpaidToLoanActive() {
// Client and Loan account creation
final Integer loanId = createAccounts(15, 1);
GetLoansLoanIdResponse getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
assertNotNull(getLoansLoanIdResponse);
loanTransactionHelper.printRepaymentSchedule(getLoansLoanIdResponse);
Float amount = Float.valueOf("1100.00");
PostLoansLoanIdTransactionsResponse loanIdTransactionsResponse = loanTransactionHelper.makeLoanRepayment(operationDate, amount,
loanId);
assertNotNull(loanIdTransactionsResponse);
final Integer transactionId = loanIdTransactionsResponse.getResourceId();
getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
assertNotNull(getLoansLoanIdResponse);
loanTransactionHelper.validateLoanStatus(getLoansLoanIdResponse, "loanStatusType.overpaid");
reviewLoanTransactionRelations(loanId, transactionId, 0);
final Integer chargebackTransactionId = applyChargebackTransaction(loanId, transactionId, "200.00", 0, responseSpec);
reviewLoanTransactionRelations(loanId, chargebackTransactionId, 1);
getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
assertNotNull(getLoansLoanIdResponse);
loanTransactionHelper.validateLoanStatus(getLoansLoanIdResponse, "loanStatusType.active");
loanTransactionHelper.validateLoanPrincipalOustandingBalance(getLoansLoanIdResponse, Double.valueOf("100.00"));
}
@Test
public void applyLoanTransactionChargebackWithLoanOverpaidToLoanClose() {
// Client and Loan account creation
final Integer loanId = createAccounts(15, 1);
GetLoansLoanIdResponse getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
assertNotNull(getLoansLoanIdResponse);
loanTransactionHelper.printRepaymentSchedule(getLoansLoanIdResponse);
Float amount = Float.valueOf("1100.00");
PostLoansLoanIdTransactionsResponse loanIdTransactionsResponse = loanTransactionHelper.makeLoanRepayment(operationDate, amount,
loanId);
assertNotNull(loanIdTransactionsResponse);
final Integer transactionId = loanIdTransactionsResponse.getResourceId();
getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
assertNotNull(getLoansLoanIdResponse);
loanTransactionHelper.validateLoanStatus(getLoansLoanIdResponse, "loanStatusType.overpaid");
reviewLoanTransactionRelations(loanId, transactionId, 0);
final Integer chargebackTransactionId = applyChargebackTransaction(loanId, transactionId, "100.00", 0, responseSpec);
reviewLoanTransactionRelations(loanId, chargebackTransactionId, 1);
getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
assertNotNull(getLoansLoanIdResponse);
loanTransactionHelper.validateLoanStatus(getLoansLoanIdResponse, "loanStatusType.closed.obligations.met");
loanTransactionHelper.validateLoanPrincipalOustandingBalance(getLoansLoanIdResponse, Double.valueOf("0.00"));
}
@Test
public void applyLoanTransactionChargebackWithLoanOverpaidToKeepAsLoanOverpaid() {
// Client and Loan account creation
final Integer loanId = createAccounts(15, 1);
GetLoansLoanIdResponse getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
assertNotNull(getLoansLoanIdResponse);
loanTransactionHelper.printRepaymentSchedule(getLoansLoanIdResponse);
Float amount = Float.valueOf("1100.00");
PostLoansLoanIdTransactionsResponse loanIdTransactionsResponse = loanTransactionHelper.makeLoanRepayment(operationDate, amount,
loanId);
assertNotNull(loanIdTransactionsResponse);
final Integer transactionId = loanIdTransactionsResponse.getResourceId();
getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
assertNotNull(getLoansLoanIdResponse);
loanTransactionHelper.validateLoanStatus(getLoansLoanIdResponse, "loanStatusType.overpaid");
reviewLoanTransactionRelations(loanId, transactionId, 0);
final Integer chargebackTransactionId = applyChargebackTransaction(loanId, transactionId, "50.00", 0, responseSpec);
reviewLoanTransactionRelations(loanId, chargebackTransactionId, 1);
getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
assertNotNull(getLoansLoanIdResponse);
loanTransactionHelper.validateLoanStatus(getLoansLoanIdResponse, "loanStatusType.overpaid");
loanTransactionHelper.validateLoanPrincipalOustandingBalance(getLoansLoanIdResponse, Double.valueOf("0.00"));
}
@Test
public void applyMultipleLoanTransactionChargeback() {
// Client and Loan account creation
final Integer loanId = createAccounts(15, 1);
GetLoansLoanIdResponse getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
assertNotNull(getLoansLoanIdResponse);
loanTransactionHelper.printRepaymentSchedule(getLoansLoanIdResponse);
Float amount = Float.valueOf(amountVal);
PostLoansLoanIdTransactionsResponse loanIdTransactionsResponse = loanTransactionHelper.makeLoanRepayment(operationDate, amount,
loanId);
assertNotNull(loanIdTransactionsResponse);
final Integer transactionId = loanIdTransactionsResponse.getResourceId();
getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
assertNotNull(getLoansLoanIdResponse);
loanTransactionHelper.validateLoanStatus(getLoansLoanIdResponse, "loanStatusType.closed.obligations.met");
// First round, empty array
reviewLoanTransactionRelations(loanId, transactionId, 0);
applyChargebackTransaction(loanId, transactionId, "200.00", 0, responseSpec);
getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
loanTransactionHelper.validateLoanPrincipalOustandingBalance(getLoansLoanIdResponse, Double.valueOf("200.00"));
// Second round, array size equal to 1
reviewLoanTransactionRelations(loanId, transactionId, 1);
applyChargebackTransaction(loanId, transactionId, "300.00", 1, responseSpec);
getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
loanTransactionHelper.validateLoanPrincipalOustandingBalance(getLoansLoanIdResponse, Double.valueOf("500.00"));
// Third round, array size equal to 2
reviewLoanTransactionRelations(loanId, transactionId, 2);
applyChargebackTransaction(loanId, transactionId, "500.00", 0, responseSpec);
getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
loanTransactionHelper.validateLoanPrincipalOustandingBalance(getLoansLoanIdResponse, Double.valueOf("1000.00"));
}
private Integer createAccounts(final Integer daysToSubtract, final Integer numberOfRepayments) {
// Client and Loan account creation
final Integer clientId = ClientHelper.createClient(this.requestSpec, this.responseSpec, "01 January 2012");
final GetLoanProductsProductIdResponse getLoanProductsProductResponse = createLoanProduct(loanTransactionHelper, null);
// Older date to have more than one overdue installment
final LocalDate transactionDate = this.todaysDate.minusDays(daysToSubtract + (30 * (numberOfRepayments - 1)));
String operationDate = Utils.dateFormatter.format(transactionDate);
return createLoanAccount(loanTransactionHelper, clientId.toString(), getLoanProductsProductResponse.getId().toString(),
operationDate, amountVal, numberOfRepayments.toString());
}
private String createChargebackPayload(final String transactionAmount, final Integer paymentTypeId) {
@@ -121,11 +425,11 @@ public class LoanTransactionChargebackTest {
}
private Integer createLoanAccount(final LoanTransactionHelper loanTransactionHelper, final String clientId, final String loanProductId,
final String operationDate, final String principalAmount) {
final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal(principalAmount).withLoanTermFrequency("12")
.withLoanTermFrequencyAsMonths().withNumberOfRepayments("12").withRepaymentEveryAfter("1")
.withRepaymentFrequencyTypeAsMonths() //
.withInterestRatePerPeriod("2") //
final String operationDate, final String principalAmount, final String numberOfRepayments) {
final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal(principalAmount)
.withLoanTermFrequency(numberOfRepayments).withLoanTermFrequencyAsMonths().withNumberOfRepayments(numberOfRepayments)
.withRepaymentEveryAfter("1").withRepaymentFrequencyTypeAsMonths() //
.withInterestRatePerPeriod("0") //
.withExpectedDisbursementDate(operationDate) //
.withInterestTypeAsDecliningBalance() //
.withSubmittedOnDate(operationDate) //
@@ -135,4 +439,29 @@ public class LoanTransactionChargebackTest {
loanTransactionHelper.disburseLoanWithNetDisbursalAmount(operationDate, loanId, principalAmount);
return loanId;
}
private Integer applyChargebackTransaction(final Integer loanId, final Integer transactionId, final String amount,
final Integer paymentTypeIdx, ResponseSpecification responseSpec) {
List<GetPaymentTypesResponse> paymentTypeList = PaymentTypeHelper.getSystemPaymentType(this.requestSpec, this.responseSpec);
assertTrue(!paymentTypeList.isEmpty());
final String payload = createChargebackPayload(amount, paymentTypeList.get(paymentTypeIdx).getId());
log.info("Loan Chargeback: {}", payload);
PostLoansLoanIdTransactionsTransactionIdResponse postLoansTransactionCommandResponse = loanTransactionHelper
.applyLoanTransactionCommand(loanId, transactionId, "chargeback", payload, responseSpec);
assertNotNull(postLoansTransactionCommandResponse);
log.info("Loan Chargeback Id: {}", postLoansTransactionCommandResponse.getResourceId());
return postLoansTransactionCommandResponse.getResourceId();
}
private void reviewLoanTransactionRelations(final Integer loanId, final Integer transactionId, final Integer expectedSize) {
GetLoansLoanIdTransactionsTransactionIdResponse getLoansTransactionResponse = loanTransactionHelper.getLoanTransaction(loanId,
transactionId);
assertNotNull(getLoansTransactionResponse);
assertNotNull(getLoansTransactionResponse.getTransactionRelations());
assertEquals(expectedSize, getLoansTransactionResponse.getTransactionRelations().size());
log.info("Loan with {} Chargeback Transactions", getLoansTransactionResponse.getTransactionRelations().size());
}
}
@@ -45,9 +45,11 @@ import org.apache.fineract.client.models.GetLoanProductsProductIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod;
import org.apache.fineract.client.models.GetLoansLoanIdRepaymentSchedule;
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdSummary;
import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTransactionIdResponse;
import org.apache.fineract.client.models.PostLoansLoanIdResponse;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdResponse;
import org.apache.fineract.client.models.PutLoansLoanIdResponse;
import org.apache.fineract.client.util.JSON;
import org.apache.fineract.integrationtests.common.CommonConstants;
@@ -95,7 +97,6 @@ public class LoanTransactionHelper {
public GetLoanProductsProductIdResponse getLoanProduct(final Integer loanProductId) {
final String GET_LOANPRODUCT_URL = "/fineract-provider/api/v1/loanproducts/" + loanProductId + "?" + Utils.TENANT_IDENTIFIER;
final String response = Utils.performServerGet(this.requestSpec, this.responseSpec, GET_LOANPRODUCT_URL);
log.info("Response {}", response);
return GSON.fromJson(response, GetLoanProductsProductIdResponse.class);
}
@@ -240,12 +241,12 @@ public class LoanTransactionHelper {
return Utils.performServerGet(requestSpec, responseSpec, GET_REPAYMENTS_URL, "loanRepaymentScheduleInstallments");
}
public PostLoansLoanIdTransactionsTransactionIdRequest applyLoanTransactionCommand(final Integer loanId, final Integer transactionId,
final String command, final String payload) {
public PostLoansLoanIdTransactionsTransactionIdResponse applyLoanTransactionCommand(final Integer loanId, final Integer transactionId,
final String command, final String payload, final ResponseSpecification responseSpec) {
final String LOAN_TRANSACTION_URL = "/fineract-provider/api/v1/loans/" + loanId + "/transactions/" + transactionId + "?command="
+ command + "&" + Utils.TENANT_IDENTIFIER;
final String response = Utils.performServerPost(requestSpec, responseSpec, LOAN_TRANSACTION_URL, payload, null);
return GSON.fromJson(response, PostLoansLoanIdTransactionsTransactionIdRequest.class);
return GSON.fromJson(response, PostLoansLoanIdTransactionsTransactionIdResponse.class);
}
public HashMap approveLoan(final String approvalDate, final Integer loanID) {
@@ -347,7 +348,6 @@ public class LoanTransactionHelper {
url = createLoanOperationURL(UNDO_DISBURSE_LOAN_COMMAND, loanId);
}
final String response = Utils.performServerPost(this.requestSpec, this.responseSpec, url, undoBodyJson, null);
log.info("Response {}", response);
return GSON.fromJson(response, PostLoansLoanIdResponse.class);
}
@@ -444,11 +444,13 @@ public class LoanTransactionHelper {
}
public PostLoansLoanIdTransactionsResponse makeLoanRepayment(final String date, final Float amountToBePaid, final Integer loanID) {
log.info("Repayment with amount {} in {} for Loan {}", amountToBePaid, date, loanID);
return postLoanTransaction(createLoanTransactionURL(MAKE_REPAYMENT_COMMAND, loanID), getRepaymentBodyAsJSON(date, amountToBePaid));
}
public PostLoansLoanIdTransactionsResponse reverseLoanRepayment(final Integer loanId, final Integer transactionId, String date) {
return postLoanTransaction(createLoanTransactionURL(UNDO, loanId, transactionId), getUndoJsonBody(date));
public PostLoansLoanIdTransactionsResponse reverseLoanTransaction(final Integer loanId, final Integer transactionId, String date,
ResponseSpecification responseSpec) {
return postLoanTransaction(createLoanTransactionURL(UNDO, loanId, transactionId), getUndoJsonBody(date), responseSpec);
}
public HashMap makeRepaymentWithPDC(final String date, final Float amountToBePaid, final Integer loanID, final Integer paymentType) {
@@ -545,6 +547,13 @@ public class LoanTransactionHelper {
return Utils.performServerGet(requestSpec, responseSpec, GET_LOAN_CHARGES_URL, param);
}
public GetLoansLoanIdTransactionsTransactionIdResponse getLoanTransaction(final Integer loanId, final Integer txnId) {
final String GET_LOAN_CHARGES_URL = "/fineract-provider/api/v1/loans/" + loanId + "/transactions/" + txnId + "?"
+ Utils.TENANT_IDENTIFIER;
final String response = Utils.performServerGet(requestSpec, responseSpec, GET_LOAN_CHARGES_URL);
return GSON.fromJson(response, GetLoansLoanIdTransactionsTransactionIdResponse.class);
}
public HashMap getPostDatedCheck(final Integer loanId, final Integer installmentId) {
final String GET_POST_DATED_TRANS_URL = "/fineract-provider/api/v1/loans/" + loanId + "/postdatedchecks/" + installmentId + "?"
+ Utils.TENANT_IDENTIFIER;
@@ -859,7 +868,12 @@ public class LoanTransactionHelper {
}
private PostLoansLoanIdTransactionsResponse postLoanTransaction(final String postURLForLoanTransaction, final String jsonToBeSent) {
final String response = Utils.performServerPost(this.requestSpec, this.responseSpec, postURLForLoanTransaction, jsonToBeSent);
return postLoanTransaction(postURLForLoanTransaction, jsonToBeSent, this.responseSpec);
}
private PostLoansLoanIdTransactionsResponse postLoanTransaction(final String postURLForLoanTransaction, final String jsonToBeSent,
ResponseSpecification responseSpec) {
final String response = Utils.performServerPost(this.requestSpec, responseSpec, postURLForLoanTransaction, jsonToBeSent);
return GSON.fromJson(response, PostLoansLoanIdTransactionsResponse.class);
}
@@ -1110,4 +1124,19 @@ public class LoanTransactionHelper {
}
}
public void validateLoanStatus(GetLoansLoanIdResponse getLoansLoanIdResponse, final String statusCodeExpected) {
final String statusCode = getLoansLoanIdResponse.getStatus().getCode();
log.info("Loan with Id {} is with Status {}", getLoansLoanIdResponse.getId(), statusCode);
assertEquals(statusCodeExpected, statusCode);
}
public void validateLoanPrincipalOustandingBalance(GetLoansLoanIdResponse getLoansLoanIdResponse, Double amountExpected) {
GetLoansLoanIdSummary getLoansLoanIdSummary = getLoansLoanIdResponse.getSummary();
if (getLoansLoanIdSummary != null) {
log.info("Loan with Principal Outstanding Balance {} expected {}", getLoansLoanIdSummary.getPrincipalOutstanding(),
amountExpected);
assertEquals(amountExpected, getLoansLoanIdSummary.getPrincipalOutstanding());
}
}
}