Merge pull request #1772 from BLasan/undoWaiveCharge

Feat: Undo waive charge
This commit is contained in:
Manthan Surkar
2021-08-28 15:22:36 +05:30
committed by GitHub
21 changed files with 475 additions and 4 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,71 @@
/**
* 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() {
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;
@@ -262,4 +263,19 @@ public class LoanTransactionsApiResource {
return this.toApiJsonSerializer.serialize(result); return this.toApiJsonSerializer.serialize(result);
} }
@PUT
@Path("{transactionId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Undo a Waive Charge Transaction", description = "Undo a Waive Charge Transaction")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PutChargeTransactionChangesRequest.class)))
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PutChargeTransactionChangesResponse.class))) })
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);
}
} }
@@ -186,4 +186,35 @@ final class LoanTransactionsApiResourceSwagger {
@Schema(example = "16") @Schema(example = "16")
public Integer resourceId; public Integer resourceId;
} }
@Schema(description = "PutChargeTransactionChangesResponse")
public static final class PutChargeTransactionChangesResponse {
private PutChargeTransactionChangesResponse() {}
static final class Changes {
private Changes() {}
@Schema(example = "amount")
public String amount;
}
@Schema(example = "1")
public Integer resourceId;
@Schema(example = "48")
public Integer loanId;
public PutChargeTransactionChangesResponse.Changes changes;
}
@Schema(description = "PutChargeTransactionChangesRequest")
public static final class PutChargeTransactionChangesRequest {
private PutChargeTransactionChangesRequest() {}
@Schema(example = "1")
public Integer transactionId;
}
} }
@@ -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);
@@ -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,10 @@ public class LoanCharge extends AbstractPersistableCustom {
} }
} }
public void resetOutstandingAmount(final BigDecimal amountOutstanding) {
this.amountOutstanding = amountOutstanding;
}
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 +876,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 +1040,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 +1059,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,12 @@ 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
@Query("select lp from LoanChargePaidBy lp where lp.loanCharge=:loanCharge and lp.installmentNumber=:installmentNumber")
LoanChargePaidBy getLoanChargePaidByLoanCharge(@Param("loanCharge") LoanCharge loanCharge,
@Param("installmentNumber") 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;
@@ -223,6 +223,10 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus
return Money.of(currency, this.principalCompleted); return Money.of(currency, this.principalCompleted);
} }
public void updateLoanRepaymentSchedule(final BigDecimal amountWaived) {
this.feeChargesWaived = this.feeChargesWaived.subtract(amountWaived);
}
public Money getPrincipalWrittenOff(final MonetaryCurrency currency) { public Money getPrincipalWrittenOff(final MonetaryCurrency currency) {
return Money.of(currency, this.principalWrittenOff); return Money.of(currency, this.principalWrittenOff);
} }
@@ -840,4 +844,8 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus
this.rescheduleInterestPortion = rescheduleInterestPortion; this.rescheduleInterestPortion = rescheduleInterestPortion;
} }
public void setFeeChargesWaived(final BigDecimal newFeeChargesCharged) {
this.feeChargesWaived = 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();
} }
@@ -166,6 +166,18 @@ public class LoanTransaction extends AbstractPersistableCustom {
createdDate, appUser); createdDate, appUser);
} }
public void setLoanTransactionToRepaymentScheduleMappings(final Integer installmentId, final BigDecimal chargePerInstallment) {
for (LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping : this.loanTransactionToRepaymentScheduleMappings) {
final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = loanTransactionToRepaymentScheduleMapping
.getLoanRepaymentScheduleInstallment();
if (loanRepaymentScheduleInstallment.getInstallmentNumber().equals(installmentId)) {
loanRepaymentScheduleInstallment.updateLoanRepaymentSchedule(chargePerInstallment);
break;
}
}
}
public static LoanTransaction recoveryRepayment(final Office office, final Money amount, final PaymentDetail paymentDetail, public static LoanTransaction recoveryRepayment(final Office office, final Money amount, final PaymentDetail paymentDetail,
final LocalDate paymentDate, final String externalId, final LocalDateTime createdDate, final AppUser appUser) { final LocalDate paymentDate, final String externalId, final LocalDateTime createdDate, final AppUser appUser) {
return new LoanTransaction(null, office, LoanTransactionType.RECOVERY_REPAYMENT, paymentDetail, amount.getAmount(), paymentDate, return new LoanTransaction(null, office, LoanTransactionType.RECOVERY_REPAYMENT, paymentDetail, amount.getAmount(), paymentDate,
@@ -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,155 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
.build(); .build();
} }
@Transactional
@Override
public CommandProcessingResult undoWaiveLoanCharge(final JsonCommand command) {
LoanTransaction loanTransaction = this.loanTransactionRepository.findById(command.entityId())
.orElseThrow(() -> new LoanTransactionNotFoundException(command.entityId()));
if (!loanTransaction.getTypeOf().getCode().equals(LoanTransactionType.WAIVE_CHARGES.getCode())) {
throw new InvalidLoanTransactionTypeException("Undo Waive Charge", "Waive an Installment Charge First",
"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);
if (!loanTransaction.isNotReversed()) {
throw new LoanChargeWaiveCannotBeReversedException(LoanChargeWaiveCannotUndoReason.ALREADY_REVERSED,
loanTransaction.getId());
}
// Reverse waived transaction
loanTransaction.setReversed();
// Get installment amount waived.
BigDecimal amountWaived = chargePerInstallment.getAmountWaived(loan.getCurrency()).getAmount();
// Set manually adjusted value to `1`
loanTransaction.setManuallyAdjustedOrReversed();
// Save updated data
this.loanTransactionRepository.saveAndFlush(loanTransaction);
// 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);
} else {
throw new InstallmentNotFoundException(command.entityId());
}
}
saveLoanWithDataIntegrityViolationChecks(loan);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BusinessEvents.LOAN_WAIVE_CHARGE_UNDO,
constructEntityMap(BusinessEntity.LOAN_CHARGE, loanCharge));
LoanTransaction loanTransactionData = this.loanTransactionRepository.getOne(command.entityId());
changes.put("principalPortion", loanTransactionData.getPrincipalPortion());
changes.put("interestPortion", loanTransactionData.getInterestPortion(loan.getCurrency()));
changes.put("feeChargesPortion", loanTransactionData.getFeeChargesPortion(loan.getCurrency()));
changes.put("penaltyChargesPortion", loanTransactionData.getPenaltyChargesPortion(loan.getCurrency()));
changes.put("outstandingLoanBalance", loanTransactionData.getOutstandingLoanBalance());
changes.put("id", loanTransactionData.getId());
changes.put("date", loanTransactionData.getTransactionDate());
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(command.entityId()) //
.withLoanId(loanId) //
.with(changes).build();
}
@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) {
@@ -286,6 +286,7 @@ INSERT INTO `m_permission`
('transaction_loan', 'WRITEOFF_LOAN_CHECKER', 'LOAN', 'WRITEOFF', '0'), ('transaction_loan', 'WRITEOFF_LOAN_CHECKER', 'LOAN', 'WRITEOFF', '0'),
('transaction_loan', 'CLOSE_LOAN_CHECKER', 'LOAN', 'CLOSE', '0'), ('transaction_loan', 'CLOSE_LOAN_CHECKER', 'LOAN', 'CLOSE', '0'),
('transaction_loan', 'CLOSEASRESCHEDULED_LOAN_CHECKER', 'LOAN', 'CLOSEASRESCHEDULED', '0'), ('transaction_loan', 'CLOSEASRESCHEDULED_LOAN_CHECKER', 'LOAN', 'CLOSEASRESCHEDULED', '0'),
('transaction_loan', 'UNDO_WAIVECHARGE', 'WAIVECHARGE', 'UNDO', '0'),
('transaction_savings', 'DEPOSIT_SAVINGSACCOUNT', 'SAVINGSACCOUNT', 'DEPOSIT', '1'), ('transaction_savings', 'DEPOSIT_SAVINGSACCOUNT', 'SAVINGSACCOUNT', 'DEPOSIT', '1'),
('transaction_savings', 'DEPOSIT_SAVINGSACCOUNT_CHECKER', 'SAVINGSACCOUNT', 'DEPOSIT', '0'), ('transaction_savings', 'DEPOSIT_SAVINGSACCOUNT_CHECKER', 'SAVINGSACCOUNT', 'DEPOSIT', '0'),
('transaction_savings', 'WITHDRAWAL_SAVINGSACCOUNT', 'SAVINGSACCOUNT', 'WITHDRAWAL', '1'), ('transaction_savings', 'WITHDRAWAL_SAVINGSACCOUNT', 'SAVINGSACCOUNT', 'WITHDRAWAL', '1'),