Feat: Add undo waive charge

This commit is contained in:
BLasan
2021-06-18 20:55:30 +05:30
parent f87b66d4e1
commit 6d08fa5661
19 changed files with 598 additions and 5 deletions
@@ -870,6 +870,15 @@ public class CommandWrapperBuilder {
return this; return this;
} }
public CommandWrapperBuilder undoWaiveChargeTransaction(final Long loanId, final Long transactionId) {
this.actionName = "UNDO";
this.entityName = "WAIVECHARGE";
this.entityId = transactionId;
this.loanId = loanId;
this.href = "/loans/" + loanId + "/transactions?command=undo";
return this;
}
public CommandWrapperBuilder createLoanApplication() { public CommandWrapperBuilder createLoanApplication() {
this.actionName = "CREATE"; this.actionName = "CREATE";
this.entityName = "LOAN"; this.entityName = "LOAN";
@@ -182,6 +182,19 @@ public class Money implements Comparable<Money> {
return this.minus(toSubtract.getAmount()); return this.minus(toSubtract.getAmount());
} }
public Money add(final Money moneyToAdd) {
final Money toAdd = checkCurrencyEqual(moneyToAdd);
return this.add(toAdd.getAmount());
}
public Money add(final BigDecimal amountToAdd) {
if (amountToAdd == null || amountToAdd.compareTo(BigDecimal.ZERO) == 0) {
return this;
}
final BigDecimal newAmount = this.amount.add(amountToAdd);
return Money.of(monetaryCurrency(), newAmount);
}
public Money minus(final BigDecimal amountToSubtract) { public Money minus(final BigDecimal amountToSubtract) {
if (amountToSubtract == null || amountToSubtract.compareTo(BigDecimal.ZERO) == 0) { if (amountToSubtract == null || amountToSubtract.compareTo(BigDecimal.ZERO) == 0) {
return this; return this;
@@ -0,0 +1,73 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.charge.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class LoanChargeWaiveCannotBeReversedException extends AbstractPlatformDomainRuleException {
/*** enum of reasons of why Loan Charge waive cannot undo **/
public enum LoanChargeWaiveCannotUndoReason {
ALREADY_PAID, ALREADY_WAIVED, LOAN_INACTIVE, WAIVE_NOT_ALLOWED_FOR_CHARGE, NOT_WAIVED, ALREADY_REVERSED;
public String errorMessage() {
/**
* TODO: Recheck the error messages
*/
if (name().toString().equalsIgnoreCase("ALREADY_PAID")) {
return "This loan charge has been completely paid";
} else if (name().toString().equalsIgnoreCase("ALREADY_WAIVED")) {
return "This loan charge has already been waived";
} else if (name().toString().equalsIgnoreCase("LOAN_INACTIVE")) {
return "This loan charge can be waived as the loan associated with it is currently inactive";
} else if (name().toString().equalsIgnoreCase("WAIVE_NOT_ALLOWED_FOR_CHARGE")) {
return "This loan charge can be waived";
} else if (name().toString().equalsIgnoreCase("NOT_WAIVED")) {
return "This loan charge waive cannot be reversed as this charge is not waived";
} else if (name().toString().equalsIgnoreCase("ALREADY_REVERSED")) {
return "This loan charge waive cannot be reversed as this transaction is not reversed";
}
return name().toString();
}
public String errorCode() {
if (name().toString().equalsIgnoreCase("ALREADY_PAID")) {
return "error.msg.loan.charge.already.paid";
} else if (name().toString().equalsIgnoreCase("ALREADY_WAIVED")) {
return "error.msg.loan.charge.already.waived";
} else if (name().toString().equalsIgnoreCase("LOAN_INACTIVE")) {
return "error.msg.loan.charge.associated.loan.inactive";
} else if (name().toString().equalsIgnoreCase("WAIVE_NOT_ALLOWED_FOR_CHARGE")) {
return "error.msg.loan.charge.waive.not.allowed";
} else if (name().toString().equalsIgnoreCase("NOT_WAIVED")) {
return "error.msg.loan.charge.waive.cannot.undo";
} else if (name().toString().equalsIgnoreCase("ALREADY_REVERSED")) {
return "error.msg.transaction.cannot.reverse";
}
return name().toString();
}
}
public LoanChargeWaiveCannotBeReversedException(final LoanChargeWaiveCannotUndoReason reason, final Long id) {
super(reason.errorCode(), reason.errorMessage(), id);
}
}
@@ -41,6 +41,7 @@ public class BusinessEventNotificationConstants {
LOAN_CLOSE_AS_RESCHEDULE("loan_close_as_reschedule"), // LOAN_CLOSE_AS_RESCHEDULE("loan_close_as_reschedule"), //
LOAN_ADD_CHARGE("loan_add_charge"), // LOAN_ADD_CHARGE("loan_add_charge"), //
LOAN_UPDATE_CHARGE("loan_update_charge"), // LOAN_UPDATE_CHARGE("loan_update_charge"), //
LOAN_WAIVE_CHARGE_UNDO("loan_waive_charge_undo"), //
LOAN_WAIVE_CHARGE("loan_waive_charge"), // LOAN_WAIVE_CHARGE("loan_waive_charge"), //
LOAN_DELETE_CHARGE("loan_delete_charge"), // LOAN_DELETE_CHARGE("loan_delete_charge"), //
LOAN_CHARGE_PAYMENT("loan_charge_payment"), // LOAN_CHARGE_PAYMENT("loan_charge_payment"), //
@@ -234,6 +234,7 @@ public class LoanChargesApiResource {
} else { } else {
throw new UnrecognizedQueryParamException("command", commandParam); throw new UnrecognizedQueryParamException("command", commandParam);
} }
if (result == null) { if (result == null) {
throw new UnrecognizedQueryParamException("command", commandParam); throw new UnrecognizedQueryParamException("command", commandParam);
} }
@@ -34,6 +34,7 @@ import java.util.Set;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.POST; import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam; import javax.ws.rs.PathParam;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
@@ -255,11 +256,31 @@ public class LoanTransactionsApiResource {
@Parameter(hidden = true) final String apiRequestBodyAsJson) { @Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson); final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
final CommandWrapper commandRequest = builder.adjustTransaction(loanId, transactionId).build(); CommandProcessingResult result = null;
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); final CommandWrapper commandRequest = builder.adjustTransaction(loanId, transactionId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.toApiJsonSerializer.serialize(result); return this.toApiJsonSerializer.serialize(result);
} }
/**
* TODO: Add Swagger annotations.
*
* @param loanId
* @param transactionId
* @return
*/
@PUT
@Path("{transactionId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Undo a Waive Charge Transaction", description = "Undo a Waive Charge Transaction")
public String undoWaiveCharge(@PathParam("loanId") @Parameter(description = "loanId") final Long loanId,
@PathParam("transactionId") @Parameter(description = "transactionId") final Long transactionId) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().undoWaiveChargeTransaction(loanId, transactionId).build();
CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.toApiJsonSerializer.serialize(result);
}
} }
@@ -529,6 +529,11 @@ public class Loan extends AbstractPersistableCustom {
return this.summary; return this.summary;
} }
public void updateLoanSummaryForUndoWaiveCharge(final BigDecimal amountWaived) {
this.summary.updateFeeChargesWaived(this.summary.getTotalFeeChargesWaived().subtract(amountWaived));
this.summary.updateFeeChargeOutstanding(this.summary.getTotalFeeChargesOutstanding().add(amountWaived));
}
private BigDecimal deriveSumTotalOfChargesDueAtDisbursement() { private BigDecimal deriveSumTotalOfChargesDueAtDisbursement() {
Money chargesDue = Money.of(getCurrency(), BigDecimal.ZERO); Money chargesDue = Money.of(getCurrency(), BigDecimal.ZERO);
@@ -972,6 +977,98 @@ public class Loan extends AbstractPersistableCustom {
return amount; return amount;
} }
// public LoanTransaction undoWaiveLoanCharge(final LoanCharge loanCharge, final LoanLifecycleStateMachine
// loanLifecycleStateMachine,
// final Map<String, Object> changes, final List<Long> existingTransactionIds, final List<Long>
// existingReversedTransactionIds,
// final Integer loanInstallmentNumber, final ScheduleGeneratorDTO scheduleGeneratorDTO, final Money accruedCharge,
// final AppUser currentUser) {
//
// validateLoanIsNotClosed(loanCharge);
//
// //final Money amountUndoWaived = loanCharge.undoWaive(loanCurrency(), loanInstallmentNumber);
//
// changes.put("amount", amountUndoWaived.getAmount());
//
// Money unrecognizedIncome = amountUndoWaived.zero();
// Money chargeComponent = amountUndoWaived;
// if (isPeriodicAccrualAccountingEnabledOnLoanProduct()) {
// Money receivableCharge = Money.zero(getCurrency());
// if (loanInstallmentNumber != null) {
// receivableCharge = accruedCharge
// .add(loanCharge.getInstallmentLoanCharge(loanInstallmentNumber).getAmountPaid(getCurrency()));
// } else {
// receivableCharge = accruedCharge.add(loanCharge.getAmountPaid(getCurrency()));
// }
//
// if (amountUndoWaived.isLessThan(receivableCharge)) {
// chargeComponent = receivableCharge;
// unrecognizedIncome = amountUndoWaived.add(receivableCharge);
// }
// }
// Money feeChargesUndoWaived = chargeComponent;
// Money penaltyChargesUndoWaived = Money.zero(loanCurrency());
// if (loanCharge.isPenaltyCharge()) {
// penaltyChargesUndoWaived = chargeComponent;
// feeChargesUndoWaived = Money.zero(loanCurrency());
// }
//
// LocalDate transactionDate = getDisbursementDate();
// if (loanCharge.isDueDateCharge()) {
// if (loanCharge.getDueLocalDate().isAfter(DateUtils.getLocalDateOfTenant())) {
// transactionDate = DateUtils.getLocalDateOfTenant();
// } else {
// transactionDate = loanCharge.getDueLocalDate();
// }
// } else if (loanCharge.isInstalmentFee()) {
// transactionDate =
// loanCharge.getInstallmentLoanCharge(loanInstallmentNumber).getRepaymentInstallment().getDueDate();
// }
//
// scheduleGeneratorDTO.setRecalculateFrom(transactionDate);
//
// updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement());
//
// // existingTransactionIds.addAll(findExistingTransactionIds());
// // existingReversedTransactionIds.addAll(findExistingReversedTransactionIds());
//
// final LoanTransaction waiveLoanChargeTransaction = LoanTransaction.waiveLoanCharge(this, getOffice(),
// amountUndoWaived,
// transactionDate, feeChargesUndoWaived, penaltyChargesUndoWaived, unrecognizedIncome,
// DateUtils.getLocalDateTimeOfTenant(),
// currentUser);
// final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(waiveLoanChargeTransaction, loanCharge,
// waiveLoanChargeTransaction.getAmount(getCurrency()).getAmount(), loanInstallmentNumber);
// waiveLoanChargeTransaction.getLoanChargesPaid().add(loanChargePaidBy);
// addLoanTransaction(waiveLoanChargeTransaction);
// if (this.repaymentScheduleDetail().isInterestRecalculationEnabled() && (loanCharge.getDueLocalDate() == null
// || LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).isAfter(loanCharge.getDueLocalDate()))) {
// regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO, currentUser);
// }
// // Waive of charges whose due date falls after latest 'repayment'
// // transaction dont require entire loan schedule to be reprocessed.
// final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor =
// this.transactionProcessorFactory
// .determineProcessor(this.transactionProcessingStrategy);
// if (!loanCharge.isDueAtDisbursement() && loanCharge.isPaidOrPartiallyPaid(loanCurrency())) {
// final List<LoanTransaction> allNonContraTransactionsPostDisbursement =
// retreiveListOfTransactionsPostDisbursement();
// loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(),
// allNonContraTransactionsPostDisbursement,
// getCurrency(), getRepaymentScheduleInstallments(), charges());
// } else {
// // reprocess loan schedule based on charge been waived.
// final LoanRepaymentScheduleProcessingWrapper wrapper = new LoanRepaymentScheduleProcessingWrapper();
// wrapper.reprocess(getCurrency(), getDisbursementDate(), getRepaymentScheduleInstallments(), charges());
// }
//
// updateLoanSummaryDerivedFields();
//
// doPostLoanTransactionChecks(waiveLoanChargeTransaction.getTransactionDate(), loanLifecycleStateMachine);
//
// return waiveLoanChargeTransaction;
// }
public LoanTransaction waiveLoanCharge(final LoanCharge loanCharge, final LoanLifecycleStateMachine loanLifecycleStateMachine, public LoanTransaction waiveLoanCharge(final LoanCharge loanCharge, final LoanLifecycleStateMachine loanLifecycleStateMachine,
final Map<String, Object> changes, final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds, final Map<String, Object> changes, final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds,
final Integer loanInstallmentNumber, final ScheduleGeneratorDTO scheduleGeneratorDTO, final Money accruedCharge, final Integer loanInstallmentNumber, final ScheduleGeneratorDTO scheduleGeneratorDTO, final Money accruedCharge,
@@ -131,6 +131,9 @@ public class LoanCharge extends AbstractPersistableCustom {
@OneToOne(mappedBy = "loancharge", cascade = CascadeType.ALL, optional = true, orphanRemoval = true, fetch = FetchType.EAGER) @OneToOne(mappedBy = "loancharge", cascade = CascadeType.ALL, optional = true, orphanRemoval = true, fetch = FetchType.EAGER)
private LoanTrancheDisbursementCharge loanTrancheDisbursementCharge; private LoanTrancheDisbursementCharge loanTrancheDisbursementCharge;
@OneToMany(mappedBy = "loanCharge", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
private Set<LoanChargePaidBy> loanChargePaidBySet;
public static LoanCharge createNewFromJson(final Loan loan, final Charge chargeDefinition, final JsonCommand command) { public static LoanCharge createNewFromJson(final Loan loan, final Charge chargeDefinition, final JsonCommand command) {
final LocalDate dueDate = command.localDateValueOfParameterNamed("dueDate"); final LocalDate dueDate = command.localDateValueOfParameterNamed("dueDate");
if (chargeDefinition.getChargeTimeType().equals(ChargeTimeType.SPECIFIED_DUE_DATE.getValue()) && dueDate == null) { if (chargeDefinition.getChargeTimeType().equals(ChargeTimeType.SPECIFIED_DUE_DATE.getValue()) && dueDate == null) {
@@ -346,6 +349,34 @@ public class LoanCharge extends AbstractPersistableCustom {
} }
} }
public void resetOutstandingAmount(final BigDecimal amountOutstanding) {
this.amountOutstanding = amountOutstanding;
}
// public Money undoWaive(final MonetaryCurrency currency, final Integer loanInstallmentNumber) {
// if (isInstalmentFee()) {
// final LoanInstallmentCharge chargePerInstallment = getInstallmentLoanCharge(loanInstallmentNumber);
// final Money amountWaived = chargePerInstallment.waive(currency);
// if (this.amountWaived != null) {
// this.amountWaived = this.amountWaived.add(amountWaived.getAmount());
// } else {
// }
//
// this.amountOutstanding = this.amountOutstanding.add(amountWaived.getAmount());
// if (determineIfFullyPaid()) {
// this.paid = false;
// this.waived = false;
// }
// return amountWaived;
// }
// this.amountWaived = BigDecimal.ZERO;
// // this.amountOutstanding = BigDecimal.ZERO;
// this.paid = false;
// this.waived = false;
// return getAmountWaived(currency);
//
// }
public Money waive(final MonetaryCurrency currency, final Integer loanInstallmentNumber) { public Money waive(final MonetaryCurrency currency, final Integer loanInstallmentNumber) {
if (isInstalmentFee()) { if (isInstalmentFee()) {
final LoanInstallmentCharge chargePerInstallment = getInstallmentLoanCharge(loanInstallmentNumber); final LoanInstallmentCharge chargePerInstallment = getInstallmentLoanCharge(loanInstallmentNumber);
@@ -869,6 +900,18 @@ public class LoanCharge extends AbstractPersistableCustom {
return null; return null;
} }
public void setInstallmentLoanCharge(final LoanInstallmentCharge loanInstallmentCharge, final Integer installmentNumber) {
LoanInstallmentCharge loanInstallmentChargeToBeRemoved = null;
for (final LoanInstallmentCharge loanChargePerInstallment : this.loanInstallmentCharge) {
if (installmentNumber.equals(loanChargePerInstallment.getRepaymentInstallment().getInstallmentNumber().intValue())) {
loanInstallmentChargeToBeRemoved = loanChargePerInstallment;
break;
}
}
this.loanInstallmentCharge.remove(loanInstallmentChargeToBeRemoved);
this.loanInstallmentCharge.add(loanInstallmentCharge);
}
public void clearLoanInstallmentCharges() { public void clearLoanInstallmentCharges() {
this.loanInstallmentCharge.clear(); this.loanInstallmentCharge.clear();
} }
@@ -1021,6 +1064,10 @@ public class LoanCharge extends AbstractPersistableCustom {
return paidChargePerInstallment; return paidChargePerInstallment;
} }
public Set<LoanChargePaidBy> getLoanChargePaidBySet() {
return this.loanChargePaidBySet;
}
public Loan getLoan() { public Loan getLoan() {
return this.loan; return this.loan;
} }
@@ -1036,4 +1083,12 @@ public class LoanCharge extends AbstractPersistableCustom {
public boolean isDueDateCharge() { public boolean isDueDateCharge() {
return this.dueDate != null; return this.dueDate != null;
} }
public void setAmountWaived(final BigDecimal amountWaived) {
this.amountWaived = amountWaived;
}
public void undoWaived() {
this.waived = false;
}
} }
@@ -19,7 +19,6 @@
package org.apache.fineract.portfolio.loanaccount.domain; package org.apache.fineract.portfolio.loanaccount.domain;
import java.math.BigDecimal; import java.math.BigDecimal;
import javax.persistence.CascadeType;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.JoinColumn; import javax.persistence.JoinColumn;
@@ -35,7 +34,7 @@ public class LoanChargePaidBy extends AbstractPersistableCustom {
@JoinColumn(name = "loan_transaction_id", nullable = false) @JoinColumn(name = "loan_transaction_id", nullable = false)
private LoanTransaction loanTransaction; private LoanTransaction loanTransaction;
@ManyToOne(cascade = CascadeType.ALL) @ManyToOne(optional = false)
@JoinColumn(name = "loan_charge_id", nullable = false) @JoinColumn(name = "loan_charge_id", nullable = false)
private LoanCharge loanCharge; private LoanCharge loanCharge;
@@ -20,7 +20,13 @@ package org.apache.fineract.portfolio.loanaccount.domain;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface LoanChargePaidByRepository extends JpaRepository<LoanChargePaidBy, Long>, JpaSpecificationExecutor<LoanCharge> { public interface LoanChargePaidByRepository extends JpaRepository<LoanChargePaidBy, Long>, JpaSpecificationExecutor<LoanCharge> {
// no added behaviour // no added behaviour
@Query("select lp from LoanChargePaidBy lp where lp.loanCharge=:loanCharge and lp.installmentNumber=:installmentNumber")
LoanChargePaidBy getLoanChargePaidByLoanCharge(@Param("loanCharge") final LoanCharge loanCharge,
@Param("installmentNumber") final Integer installmentNo);
} }
@@ -109,6 +109,11 @@ public class LoanInstallmentCharge extends AbstractPersistableCustom implements
return BigDecimal.ZERO.compareTo(calculateOutstanding()) == 0; return BigDecimal.ZERO.compareTo(calculateOutstanding()) == 0;
} }
public void undoWaive(final BigDecimal amountOutstanding, final BigDecimal amountWaived) {
this.amountOutstanding = amountOutstanding;
this.amountWaived = amountWaived;
}
private BigDecimal calculateOutstanding() { private BigDecimal calculateOutstanding() {
if (this.amount == null) { if (this.amount == null) {
return null; return null;
@@ -221,6 +226,18 @@ public class LoanInstallmentCharge extends AbstractPersistableCustom implements
this.paid = false; this.paid = false;
} }
public void resetAmountWaived(final BigDecimal amountWaived) {
this.amountWaived = amountWaived;
}
public void undoWaiveFlag() {
this.waived = false;
}
public void resetOutstandingAmount(final BigDecimal amountOutstanding) {
this.amountOutstanding = amountOutstanding;
}
public void resetToOriginal(final MonetaryCurrency currency) { public void resetToOriginal(final MonetaryCurrency currency) {
this.amountPaid = BigDecimal.ZERO; this.amountPaid = BigDecimal.ZERO;
this.amountWaived = BigDecimal.ZERO; this.amountWaived = BigDecimal.ZERO;
@@ -840,4 +840,8 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus
this.rescheduleInterestPortion = rescheduleInterestPortion; this.rescheduleInterestPortion = rescheduleInterestPortion;
} }
public void setFeeChargesWaived(final BigDecimal newFeeChargesCharged) {
this.feeChargesCharged = newFeeChargesCharged;
}
} }
@@ -141,6 +141,14 @@ public final class LoanSummary {
return Money.of(currency, this.totalOutstanding); return Money.of(currency, this.totalOutstanding);
} }
public void updateFeeChargeOutstanding(final BigDecimal totalFeeChargesOutstanding) {
this.totalFeeChargesOutstanding = totalFeeChargesOutstanding;
}
public void updateFeeChargesWaived(final BigDecimal totalFeeChargesWaived) {
this.totalFeeChargesWaived = totalFeeChargesWaived;
}
public boolean isRepaidInFull(final MonetaryCurrency currency) { public boolean isRepaidInFull(final MonetaryCurrency currency) {
return getTotalOutstanding(currency).isZero(); return getTotalOutstanding(currency).isZero();
} }
@@ -334,6 +334,18 @@ public class LoanTransaction extends AbstractPersistableCustom {
return waiver; return waiver;
} }
// public static LoanTransaction undoWaiveLoanCharge(final Loan loan, final Office office, final Money undoWaived,
// final LocalDate undoWaiveDate, final Money feeChargesWaived, final Money penaltyChargesWaived, final Money
// unrecognizedCharge,
// final LocalDateTime createdDate, final AppUser appUser) {
// final LoanTransaction undoWaiver = new LoanTransaction(loan, office, LoanTransactionType.UNDO_WAIVE_CHARGE,
// undoWaived.getAmount(),
// undoWaiveDate, null, createdDate, appUser);
// undoWaiver.updateChargesComponents(feeChargesWaived, penaltyChargesWaived, unrecognizedCharge);
//
// return undoWaiver;
// }
public static LoanTransaction writeoff(final Loan loan, final Office office, final LocalDate writeOffDate, final String externalId, public static LoanTransaction writeoff(final Loan loan, final Office office, final LocalDate writeOffDate, final String externalId,
final LocalDateTime createdDate, final AppUser appUser) { final LocalDateTime createdDate, final AppUser appUser) {
return new LoanTransaction(loan, office, LoanTransactionType.WRITEOFF, null, writeOffDate, externalId, createdDate, appUser); return new LoanTransaction(loan, office, LoanTransactionType.WRITEOFF, null, writeOffDate, externalId, createdDate, appUser);
@@ -494,6 +506,14 @@ public class LoanTransaction extends AbstractPersistableCustom {
return !isReversed(); return !isReversed();
} }
public void setReversed() {
this.reversed = true;
}
public void setManuallyAdjustedOrReversed() {
this.manuallyAdjustedOrReversed = true;
}
public boolean isAnyTypeOfRepayment() { public boolean isAnyTypeOfRepayment() {
return isRepayment() || isRepaymentAtDisbursement() || isRecoveryRepayment(); return isRepayment() || isRepaymentAtDisbursement() || isRecoveryRepayment();
} }
@@ -34,6 +34,7 @@ public enum LoanTransactionType {
*/ */
RECOVERY_REPAYMENT(8, "loanTransactionType.recoveryRepayment"), // RECOVERY_REPAYMENT(8, "loanTransactionType.recoveryRepayment"), //
WAIVE_CHARGES(9, "loanTransactionType.waiveCharges"), // WAIVE_CHARGES(9, "loanTransactionType.waiveCharges"), //
// UNDO_WAIVE_CHARGE(20, "loanTransactionType.undoWaiveCharge"),
/** /**
* Transaction represents an Accrual (For either interest, charge or a penalty * Transaction represents an Accrual (For either interest, charge or a penalty
**/ **/
@@ -0,0 +1,28 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
public class InstallmentNotFoundException extends AbstractPlatformResourceNotFoundException {
public InstallmentNotFoundException(final Long id) {
super("error.msg.transaction.id.invalid", "Transaction with identifier " + id + " does not contain any installment");
}
}
@@ -0,0 +1,46 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.handler;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "WAIVECHARGE", action = "UNDO")
public class UndoLoanChargeWaiveCommandHandler implements NewCommandSourceHandler {
final LoanWritePlatformService loanWritePlatformService;
@Autowired
public UndoLoanChargeWaiveCommandHandler(final LoanWritePlatformService loanWritePlatformService) {
this.loanWritePlatformService = loanWritePlatformService;
}
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.loanWritePlatformService.undoWaiveLoanCharge(command);
}
}
@@ -65,6 +65,8 @@ public interface LoanWritePlatformService {
CommandProcessingResult waiveLoanCharge(Long loanId, Long loanChargeId, JsonCommand command); CommandProcessingResult waiveLoanCharge(Long loanId, Long loanChargeId, JsonCommand command);
CommandProcessingResult undoWaiveLoanCharge(JsonCommand command);
CommandProcessingResult loanReassignment(Long loanId, JsonCommand command); CommandProcessingResult loanReassignment(Long loanId, JsonCommand command);
CommandProcessingResult bulkLoanReassignment(JsonCommand command); CommandProcessingResult bulkLoanReassignment(JsonCommand command);
@@ -22,6 +22,7 @@ import com.google.gson.JsonArray;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
@@ -109,6 +110,8 @@ import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBeUpdatedE
import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBeWaivedException; import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBeWaivedException;
import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBeWaivedException.LoanChargeCannotBeWaivedReason; import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBeWaivedException.LoanChargeCannotBeWaivedReason;
import org.apache.fineract.portfolio.charge.exception.LoanChargeNotFoundException; import org.apache.fineract.portfolio.charge.exception.LoanChargeNotFoundException;
import org.apache.fineract.portfolio.charge.exception.LoanChargeWaiveCannotBeReversedException;
import org.apache.fineract.portfolio.charge.exception.LoanChargeWaiveCannotBeReversedException.LoanChargeWaiveCannotUndoReason;
import org.apache.fineract.portfolio.client.domain.Client; import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; import org.apache.fineract.portfolio.client.exception.ClientNotActiveException;
import org.apache.fineract.portfolio.collectionsheet.command.CollectionSheetBulkDisbursalCommand; import org.apache.fineract.portfolio.collectionsheet.command.CollectionSheetBulkDisbursalCommand;
@@ -135,6 +138,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.GroupLoanIndividualMonit
import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService; import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidByRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails; import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails;
import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent; import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent;
@@ -156,6 +161,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepositor
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.apache.fineract.portfolio.loanaccount.exception.DateMismatchException; import org.apache.fineract.portfolio.loanaccount.exception.DateMismatchException;
import org.apache.fineract.portfolio.loanaccount.exception.ExceedingTrancheCountException; import org.apache.fineract.portfolio.loanaccount.exception.ExceedingTrancheCountException;
import org.apache.fineract.portfolio.loanaccount.exception.InstallmentNotFoundException;
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException;
import org.apache.fineract.portfolio.loanaccount.exception.InvalidPaidInAdvanceAmountException; import org.apache.fineract.portfolio.loanaccount.exception.InvalidPaidInAdvanceAmountException;
import org.apache.fineract.portfolio.loanaccount.exception.LoanForeclosureException; import org.apache.fineract.portfolio.loanaccount.exception.LoanForeclosureException;
import org.apache.fineract.portfolio.loanaccount.exception.LoanMultiDisbursementException; import org.apache.fineract.portfolio.loanaccount.exception.LoanMultiDisbursementException;
@@ -238,6 +245,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
private final CashierTransactionDataValidator cashierTransactionDataValidator; private final CashierTransactionDataValidator cashierTransactionDataValidator;
private final GLIMAccountInfoRepository glimRepository; private final GLIMAccountInfoRepository glimRepository;
private final LoanRepository loanRepository; private final LoanRepository loanRepository;
private final LoanChargePaidByRepository loanChargePaidByRepository;
@Autowired @Autowired
public LoanWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context, public LoanWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context,
@@ -267,7 +275,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessingStrategy, final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessingStrategy,
final CodeValueRepositoryWrapper codeValueRepository, final LoanRepositoryWrapper loanRepositoryWrapper, final CodeValueRepositoryWrapper codeValueRepository, final LoanRepositoryWrapper loanRepositoryWrapper,
final CashierTransactionDataValidator cashierTransactionDataValidator, final GLIMAccountInfoRepository glimRepository, final CashierTransactionDataValidator cashierTransactionDataValidator, final GLIMAccountInfoRepository glimRepository,
final LoanRepository loanRepository) { final LoanRepository loanRepository, final LoanChargePaidByRepository loanChargePaidByRepository) {
this.context = context; this.context = context;
this.loanEventApiJsonValidator = loanEventApiJsonValidator; this.loanEventApiJsonValidator = loanEventApiJsonValidator;
this.loanAssembler = loanAssembler; this.loanAssembler = loanAssembler;
@@ -308,6 +316,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
this.cashierTransactionDataValidator = cashierTransactionDataValidator; this.cashierTransactionDataValidator = cashierTransactionDataValidator;
this.loanRepository = loanRepository; this.loanRepository = loanRepository;
this.glimRepository = glimRepository; this.glimRepository = glimRepository;
this.loanChargePaidByRepository = loanChargePaidByRepository;
} }
private LoanLifecycleStateMachine defaultLoanLifecycleStateMachine() { private LoanLifecycleStateMachine defaultLoanLifecycleStateMachine() {
@@ -1590,6 +1599,189 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
.build(); .build();
} }
@Transactional
@Override
public CommandProcessingResult undoWaiveLoanCharge(final JsonCommand command) {
// validateUndoWaiveCharge(command);
LoanTransaction loanTransaction = this.loanTransactionRepository.findById(command.entityId())
.orElseThrow(() -> new LoanTransactionNotFoundException(command.entityId()));
if (!loanTransaction.getTypeOf().getCode().equals(LoanTransactionType.WAIVE_CHARGES.getCode())) {
/**
* TODO: Add proper validation
*/
throw new InvalidLoanTransactionTypeException("", "", "Transaction is not a waive charge type.");
}
Set<LoanChargePaidBy> loanChargePaidBySet = loanTransaction.getLoanChargesPaid();
Integer installmentNumber = null;
Long loanChargeId = null;
final Long loanId = loanTransaction.getLoan().getId();
for (LoanChargePaidBy loanChargePaidBy : loanChargePaidBySet) {
installmentNumber = loanChargePaidBy.getInstallmentNumber();
loanChargeId = loanChargePaidBy.getLoanCharge().getId();
break;
}
AppUser currentUser = getAppUserIfPresent();
final Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan);
final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId);
// Charges may be waived only when the loan associated with them are
// active
if (!loan.status().isActive()) {
throw new LoanChargeWaiveCannotBeReversedException(LoanChargeWaiveCannotUndoReason.LOAN_INACTIVE, loanCharge.getId());
}
// Validate loan charge is not already paid
if (loanCharge.isPaid()) {
throw new LoanChargeWaiveCannotBeReversedException(LoanChargeWaiveCannotUndoReason.ALREADY_PAID, loanCharge.getId());
}
final Map<String, Object> changes = new LinkedHashMap<>(3);
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BusinessEvents.LOAN_WAIVE_CHARGE_UNDO,
constructEntityMap(BusinessEntity.LOAN_CHARGE, loanCharge));
if (loanCharge.isInstalmentFee()) {
LoanInstallmentCharge chargePerInstallment = null;
// final Integer installmentNumber = command.integerValueOfParameterNamed("installmentNumber");
if (installmentNumber != null) {
// Get installment charge.
chargePerInstallment = loanCharge.getInstallmentLoanCharge(installmentNumber);
// // Get relevant LoanChargePaidBy row
// final LoanChargePaidBy loanChargePaidBy =
// this.loanChargePaidByRepository.getLoanChargePaidByLoanCharge(loanCharge,
// chargePerInstallment.getInstallment().getInstallmentNumber());
// LoanTransaction loanTransaction = loanChargePaidBy.getLoanTransaction();
if (!loanTransaction.isNotReversed()) {
throw new LoanChargeWaiveCannotBeReversedException(LoanChargeWaiveCannotUndoReason.ALREADY_REVERSED,
loanTransaction.getId());
}
// Reverse waived transaction
loanTransaction.setReversed();
// Set manually adjusted value to `1`
loanTransaction.setManuallyAdjustedOrReversed();
// Save updated data
this.loanTransactionRepository.saveAndFlush(loanTransaction);
// Get installment amount waived.
BigDecimal amountWaived = chargePerInstallment.getAmountWaived(loan.getCurrency()).getAmount();
// Get installment outstanding amount
BigDecimal amountOutstandingPerInstallment = chargePerInstallment.getAmountOutstanding();
// Check whether the installment charge is not waived. If so throw new error
if (!chargePerInstallment.isWaived() || amountWaived == null) {
throw new LoanChargeWaiveCannotBeReversedException(LoanChargeWaiveCannotUndoReason.NOT_WAIVED, loanChargeId);
}
// Get loan charge total amount waived
BigDecimal totalAmountWaved = loanCharge.getAmountWaived(loan.getCurrency()).getAmount();
// Get loan charge outstanding amount
BigDecimal amountOutstanding = loanCharge.getAmountOutstanding(loan.getCurrency()).getAmount();
// Add the amount waived to outstanding amount
loanCharge.resetOutstandingAmount(amountOutstanding.add(amountWaived));
// Subtract the amount waived from the existing amount waived.
loanCharge.setAmountWaived(totalAmountWaved.subtract(amountWaived));
// Add the amount waived to the outstanding amount of the installment
chargePerInstallment.resetOutstandingAmount(amountOutstandingPerInstallment.add(amountWaived));
// Set the amount waived value to ZERO
chargePerInstallment.resetAmountWaived(BigDecimal.ZERO);
// Reset waived flag
chargePerInstallment.undoWaiveFlag();
// Get the fee charges waived amount per installment
BigDecimal feeChargesWaivedAmount = chargePerInstallment.getInstallment().getFeeChargesWaived(loan.getCurrency())
.getAmount();
// Subtract the amount waived from the existing fee charges waived amount.
chargePerInstallment.getInstallment().setFeeChargesWaived(feeChargesWaivedAmount.subtract(amountWaived));
// Set the last modification date.
chargePerInstallment.getInstallment().setLastModifiedDate(Instant.now());
// Update loan charge.
loanCharge.setInstallmentLoanCharge(chargePerInstallment, chargePerInstallment.getInstallment().getInstallmentNumber());
if (loanCharge.getAmount(loan.getCurrency()).compareTo(loanCharge.getAmountOutstanding(loan.getCurrency())) == 0
&& loanCharge.isWaived()) {
loanCharge.undoWaived();
}
this.loanChargeRepository.saveAndFlush(loanCharge);
loan.updateLoanSummaryForUndoWaiveCharge(amountWaived);
changes.put("amount", amountWaived);
// final Set<LoanChargePaidBy> loanChargePaidBySet = loanCharge.getLoanChargePaidBySet();
// for (LoanChargePaidBy loanChargePaidBy: loanChargePaidBySet) {
// if (installmentNumber.equals(loanChargePaidBy.getInstallmentNumber())) {
// LoanTransaction transaction = loanChargePaidBy.getLoanTransaction();
// if (transaction.isNotReversed()) {
// transaction.setReversed();
// }
// }
// }
} else {
/**
* TODO: Throw installment number should not be empty error.
*/
throw new InstallmentNotFoundException(command.entityId());
}
}
saveLoanWithDataIntegrityViolationChecks(loan);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BusinessEvents.LOAN_WAIVE_CHARGE_UNDO,
constructEntityMap(BusinessEntity.LOAN_CHARGE, loanCharge));
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(command.entityId()) //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
.withGroupId(loan.getGroupId()) //
.withLoanId(loanId) //
.with(changes).build();
}
// private void validateUndoWaiveCharge(JsonCommand command) {
// final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
// final DataValidatorBuilder baseDataValidator = new
// DataValidatorBuilder(dataValidationErrors).resource("transaction");
//
// if (!command.parameterExists("transactionId")) {
// baseDataValidator.reset().parameter("transactionId").failWithCode("trasactionId.not.exists");
// } else {
// final Long transactionId = command.longValueOfParameterNamed("transactionId");
// baseDataValidator.reset().parameter("transactionId").value(transactionId).notNull().notBlank();
// }
//
// if (!dataValidationErrors.isEmpty()) {
// throw new PlatformApiDataValidationException(dataValidationErrors);
// }
// }
@Transactional @Transactional
@Override @Override
public CommandProcessingResult waiveLoanCharge(final Long loanId, final Long loanChargeId, final JsonCommand command) { public CommandProcessingResult waiveLoanCharge(final Long loanId, final Long loanChargeId, final JsonCommand command) {