Charge back Loan Transaction updates repayment schedule
This commit is contained in:
committed by
Aleksandar Vidakovic
parent
6d2447e327
commit
ae71df17df
@@ -322,6 +322,14 @@
|
||||
"null",
|
||||
"bigdecimal"
|
||||
]
|
||||
},
|
||||
{
|
||||
"default": null,
|
||||
"name": "totalCredits",
|
||||
"type": [
|
||||
"null",
|
||||
"bigdecimal"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+21
-2
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -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 {
|
||||
|
||||
+5
@@ -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;
|
||||
}
|
||||
|
||||
+57
-13
@@ -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();
|
||||
|
||||
+43
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -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;
|
||||
}
|
||||
|
||||
+12
-2
@@ -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;
|
||||
}
|
||||
|
||||
+67
@@ -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) {
|
||||
|
||||
+3
@@ -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);
|
||||
|
||||
|
||||
+4
-1
@@ -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) {
|
||||
|
||||
+10
-4
@@ -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) {
|
||||
|
||||
+3
-1
@@ -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() {
|
||||
|
||||
+3
-1
@@ -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() {
|
||||
|
||||
+47
-26
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+32
-12
@@ -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>
|
||||
|
||||
+36
@@ -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>
|
||||
+2
-2
@@ -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);
|
||||
|
||||
+358
-29
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+38
-9
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user