This is an automated email from the ASF dual-hosted git repository.
aleks pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push:
new ae71df17d Charge back Loan Transaction updates repayment schedule
ae71df17d is described below
commit ae71df17df586e72a663e7069bb94a7b5dcaf2c4
Author: Jose Alberto Hernandez <[email protected]>
AuthorDate: Mon Oct 3 07:59:12 2022 -0500
Charge back Loan Transaction updates repayment schedule
---
.../avro/loan/v1/LoanSchedulePeriodDataV1.avsc | 8 +
.../api/LoanTransactionsApiResourceSwagger.java | 23 +-
.../loanaccount/api/LoansApiResourceSwagger.java | 2 +
.../domain/DefaultLoanLifecycleStateMachine.java | 5 +
.../portfolio/loanaccount/domain/Loan.java | 70 +++-
.../domain/LoanRepaymentScheduleInstallment.java | 43 +++
.../loanaccount/domain/LoanSummaryWrapper.java | 2 +-
.../loanaccount/domain/LoanTransaction.java | 14 +-
...tLoanRepaymentScheduleTransactionProcessor.java | 67 ++++
.../LoanRepaymentScheduleTransactionProcessor.java | 3 +
.../loanschedule/data/LoanScheduleData.java | 5 +-
.../loanschedule/data/LoanSchedulePeriodData.java | 14 +-
.../loanschedule/domain/LoanScheduleModel.java | 4 +-
.../rescheduleloan/domain/LoanRescheduleModel.java | 4 +-
.../service/LoanReadPlatformServiceImpl.java | 73 ++--
.../LoanWritePlatformServiceJpaRepositoryImpl.java | 44 ++-
.../db/changelog/tenant/changelog-tenant.xml | 1 +
...4_additional_fields_loan_repayment_schedule.xml | 36 ++
.../DelinquencyBucketsIntegrationTest.java | 4 +-
.../LoanTransactionChargebackTest.java | 379 +++++++++++++++++++--
.../common/loans/LoanTransactionHelper.java | 47 ++-
21 files changed, 749 insertions(+), 99 deletions(-)
diff --git
a/fineract-avro-schemas/src/main/avro/loan/v1/LoanSchedulePeriodDataV1.avsc
b/fineract-avro-schemas/src/main/avro/loan/v1/LoanSchedulePeriodDataV1.avsc
index f7a0dd15e..cb6b8492f 100644
--- a/fineract-avro-schemas/src/main/avro/loan/v1/LoanSchedulePeriodDataV1.avsc
+++ b/fineract-avro-schemas/src/main/avro/loan/v1/LoanSchedulePeriodDataV1.avsc
@@ -322,6 +322,14 @@
"null",
"bigdecimal"
]
+ },
+ {
+ "default": null,
+ "name": "totalCredits",
+ "type": [
+ "null",
+ "bigdecimal"
+ ]
}
]
}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java
index 8ca36db06..0aa23eda2 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java
@@ -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;
}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java
index 1cffd9d25..98bc435ff 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java
@@ -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 {
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachine.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachine.java
index 8f1219397..5c34901bc 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachine.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachine.java
@@ -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;
}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
index d70c6ab28..c84784782 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
@@ -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();
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java
index 078e051b0..47d57fc93 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java
@@ -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;
+ }
+
}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummaryWrapper.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummaryWrapper.java
index 0c8b82fb1..437eead01 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummaryWrapper.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummaryWrapper.java
@@ -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;
}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
index 22313cc2b..82d0da802 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
@@ -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;
}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java
index 5a10c2298..acf79de72 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java
@@ -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) {
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java
index c262897df..b65d2d28a 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java
@@ -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);
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanScheduleData.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanScheduleData.java
index 3c9ea4e19..4f7a4d153 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanScheduleData.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanScheduleData.java
@@ -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) {
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java
index b03f44735..fc74cbfa7 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java
@@ -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)
{
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModel.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModel.java
index c38636cdd..e7e8d92c2 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModel.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModel.java
@@ -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() {
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/domain/LoanRescheduleModel.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/domain/LoanRescheduleModel.java
index 9f87bc32e..f9ec64f97 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/domain/LoanRescheduleModel.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/domain/LoanRescheduleModel.java
@@ -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() {
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java
index 7005b8adb..54d901578 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java
@@ -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);
}
}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
index 7d010b191..0ee373287 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
@@ -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());
diff --git
a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
index 933c2b523..c7e64d63a 100644
---
a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
+++
b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
@@ -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>
diff --git
a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0054_additional_fields_loan_repayment_schedule.xml
b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0054_additional_fields_loan_repayment_schedule.xml
new file mode 100644
index 000000000..b5d41b42b
--- /dev/null
+++
b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0054_additional_fields_loan_repayment_schedule.xml
@@ -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>
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java
index 17814e645..282ee48f1 100644
---
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java
@@ -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);
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionChargebackTest.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionChargebackTest.java
index cd07d376d..aa031f3a1 100644
---
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionChargebackTest.java
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionChargebackTest.java
@@ -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 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");
+
+ 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 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 Integer daysToSubtract = 1;
+ final Integer numberOfRepayments = 3;
+ final Integer loanId = createAccounts(daysToSubtract,
numberOfRepayments);
- final LocalDate todaysDate = Utils.getLocalDateOfTenant();
- // Older date to have more than one overdue installment
- final LocalDate transactionDate = todaysDate.minusDays(45);
+ 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);
- final Integer loanId = createLoanAccount(loanTransactionHelper,
clientId.toString(),
- getLoanProductsProductResponse.getId().toString(),
operationDate, "1000");
+ 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());
- 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");
+
+ 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);
- 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);
+ 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());
+ }
+
}
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
index aeb79b8d9..58a1ddf32 100644
---
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
@@ -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());
+ }
+ }
+
}