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 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 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 installmentNumberComparator = new Comparator() { @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 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 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 installments) { + List transactionMappings = new ArrayList<>(); + final Comparator byDate = new Comparator() { + + @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 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 installments, final Set 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 installments, Set charges); + void handleChargeback(LoanTransaction loanTransaction, MonetaryCurrency currency, Money overpaidAmount, + List installments); + void processTransactionsFromDerivedFields(List transactionsPostDisbursement, MonetaryCurrency currency, List installments, Set 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; /** * periods 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 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 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 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 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 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 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 @@ + 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 @@ + + + + + + + + + + + + + + + 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 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 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 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 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()); + } + } + }