diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index 29f6dd64a..1fb0f4c90 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -870,6 +870,15 @@ public class CommandWrapperBuilder { 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() { this.actionName = "CREATE"; this.entityName = "LOAN"; diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/domain/Money.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/domain/Money.java index 6479a4f64..eafa09fdf 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/domain/Money.java +++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/domain/Money.java @@ -182,6 +182,19 @@ public class Money implements Comparable { 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) { if (amountToSubtract == null || amountToSubtract.compareTo(BigDecimal.ZERO) == 0) { return this; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/exception/LoanChargeWaiveCannotBeReversedException.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/exception/LoanChargeWaiveCannotBeReversedException.java new file mode 100644 index 000000000..1ed8e194e --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/exception/LoanChargeWaiveCannotBeReversedException.java @@ -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); + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/common/BusinessEventNotificationConstants.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/common/BusinessEventNotificationConstants.java index 53aee65cf..eb014492d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/common/BusinessEventNotificationConstants.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/common/BusinessEventNotificationConstants.java @@ -41,6 +41,7 @@ public class BusinessEventNotificationConstants { LOAN_CLOSE_AS_RESCHEDULE("loan_close_as_reschedule"), // LOAN_ADD_CHARGE("loan_add_charge"), // LOAN_UPDATE_CHARGE("loan_update_charge"), // + LOAN_WAIVE_CHARGE_UNDO("loan_waive_charge_undo"), // LOAN_WAIVE_CHARGE("loan_waive_charge"), // LOAN_DELETE_CHARGE("loan_delete_charge"), // LOAN_CHARGE_PAYMENT("loan_charge_payment"), // diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanChargesApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanChargesApiResource.java index 14021d376..032e94a5d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanChargesApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanChargesApiResource.java @@ -234,6 +234,7 @@ public class LoanChargesApiResource { } else { throw new UnrecognizedQueryParamException("command", commandParam); } + if (result == null) { throw new UnrecognizedQueryParamException("command", commandParam); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java index fec81bfc0..af5e2d3d5 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java @@ -34,6 +34,7 @@ import java.util.Set; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.POST; +import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; @@ -262,4 +263,19 @@ public class LoanTransactionsApiResource { 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); + } } 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 53b1c669f..d5bd220ca 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 @@ -186,4 +186,35 @@ final class LoanTransactionsApiResourceSwagger { @Schema(example = "16") 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; + + } } 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 1e62691df..5974c3d83 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 @@ -529,6 +529,11 @@ public class Loan extends AbstractPersistableCustom { 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() { Money chargesDue = Money.of(getCurrency(), BigDecimal.ZERO); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java index 5301746d3..88a622f00 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java @@ -131,6 +131,9 @@ public class LoanCharge extends AbstractPersistableCustom { @OneToOne(mappedBy = "loancharge", cascade = CascadeType.ALL, optional = true, orphanRemoval = true, fetch = FetchType.EAGER) private LoanTrancheDisbursementCharge loanTrancheDisbursementCharge; + @OneToMany(mappedBy = "loanCharge", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) + private Set loanChargePaidBySet; + public static LoanCharge createNewFromJson(final Loan loan, final Charge chargeDefinition, final JsonCommand command) { final LocalDate dueDate = command.localDateValueOfParameterNamed("dueDate"); 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) { if (isInstalmentFee()) { final LoanInstallmentCharge chargePerInstallment = getInstallmentLoanCharge(loanInstallmentNumber); @@ -869,6 +876,18 @@ public class LoanCharge extends AbstractPersistableCustom { 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() { this.loanInstallmentCharge.clear(); } @@ -1021,6 +1040,10 @@ public class LoanCharge extends AbstractPersistableCustom { return paidChargePerInstallment; } + public Set getLoanChargePaidBySet() { + return this.loanChargePaidBySet; + } + public Loan getLoan() { return this.loan; } @@ -1036,4 +1059,12 @@ public class LoanCharge extends AbstractPersistableCustom { public boolean isDueDateCharge() { return this.dueDate != null; } + + public void setAmountWaived(final BigDecimal amountWaived) { + this.amountWaived = amountWaived; + } + + public void undoWaived() { + this.waived = false; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargePaidBy.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargePaidBy.java index 8924ba16c..ac067afc7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargePaidBy.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargePaidBy.java @@ -19,7 +19,6 @@ package org.apache.fineract.portfolio.loanaccount.domain; import java.math.BigDecimal; -import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.JoinColumn; @@ -35,7 +34,7 @@ public class LoanChargePaidBy extends AbstractPersistableCustom { @JoinColumn(name = "loan_transaction_id", nullable = false) private LoanTransaction loanTransaction; - @ManyToOne(cascade = CascadeType.ALL) + @ManyToOne(optional = false) @JoinColumn(name = "loan_charge_id", nullable = false) private LoanCharge loanCharge; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargePaidByRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargePaidByRepository.java index 0c7df943a..dc8d36a4f 100755 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargePaidByRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargePaidByRepository.java @@ -20,7 +20,12 @@ package org.apache.fineract.portfolio.loanaccount.domain; import org.springframework.data.jpa.repository.JpaRepository; 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, JpaSpecificationExecutor { - // 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); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInstallmentCharge.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInstallmentCharge.java index 5737155e2..eb207c82b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInstallmentCharge.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInstallmentCharge.java @@ -109,6 +109,11 @@ public class LoanInstallmentCharge extends AbstractPersistableCustom implements return BigDecimal.ZERO.compareTo(calculateOutstanding()) == 0; } + public void undoWaive(final BigDecimal amountOutstanding, final BigDecimal amountWaived) { + this.amountOutstanding = amountOutstanding; + this.amountWaived = amountWaived; + } + private BigDecimal calculateOutstanding() { if (this.amount == null) { return null; @@ -221,6 +226,18 @@ public class LoanInstallmentCharge extends AbstractPersistableCustom implements 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) { this.amountPaid = BigDecimal.ZERO; this.amountWaived = BigDecimal.ZERO; 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 e190ebf6f..8219bd480 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 @@ -223,6 +223,10 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus return Money.of(currency, this.principalCompleted); } + public void updateLoanRepaymentSchedule(final BigDecimal amountWaived) { + this.feeChargesWaived = this.feeChargesWaived.subtract(amountWaived); + } + public Money getPrincipalWrittenOff(final MonetaryCurrency currency) { return Money.of(currency, this.principalWrittenOff); } @@ -840,4 +844,8 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus this.rescheduleInterestPortion = rescheduleInterestPortion; } + public void setFeeChargesWaived(final BigDecimal newFeeChargesCharged) { + this.feeChargesWaived = newFeeChargesCharged; + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummary.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummary.java index bf5ca2f21..9eea2641d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummary.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummary.java @@ -141,6 +141,14 @@ public final class LoanSummary { 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) { return getTotalOutstanding(currency).isZero(); } 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 65f8ff5f6..c3bbeaf4e 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 @@ -166,6 +166,18 @@ public class LoanTransaction extends AbstractPersistableCustom { 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, final LocalDate paymentDate, final String externalId, final LocalDateTime createdDate, final AppUser appUser) { return new LoanTransaction(null, office, LoanTransactionType.RECOVERY_REPAYMENT, paymentDetail, amount.getAmount(), paymentDate, @@ -494,6 +506,14 @@ public class LoanTransaction extends AbstractPersistableCustom { return !isReversed(); } + public void setReversed() { + this.reversed = true; + } + + public void setManuallyAdjustedOrReversed() { + this.manuallyAdjustedOrReversed = true; + } + public boolean isAnyTypeOfRepayment() { return isRepayment() || isRepaymentAtDisbursement() || isRecoveryRepayment(); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java index 0c2851cd5..b053327ab 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java @@ -34,6 +34,7 @@ public enum LoanTransactionType { */ RECOVERY_REPAYMENT(8, "loanTransactionType.recoveryRepayment"), // WAIVE_CHARGES(9, "loanTransactionType.waiveCharges"), // + // UNDO_WAIVE_CHARGE(20, "loanTransactionType.undoWaiveCharge"), /** * Transaction represents an Accrual (For either interest, charge or a penalty **/ diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/InstallmentNotFoundException.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/InstallmentNotFoundException.java new file mode 100644 index 000000000..cf478b35a --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/InstallmentNotFoundException.java @@ -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"); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/UndoLoanChargeWaiveCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/UndoLoanChargeWaiveCommandHandler.java new file mode 100644 index 000000000..a737d3407 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/UndoLoanChargeWaiveCommandHandler.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java index 4370ea27b..9be2fdbe8 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java @@ -65,6 +65,8 @@ public interface LoanWritePlatformService { CommandProcessingResult waiveLoanCharge(Long loanId, Long loanChargeId, JsonCommand command); + CommandProcessingResult undoWaiveLoanCharge(JsonCommand command); + CommandProcessingResult loanReassignment(Long loanId, JsonCommand command); CommandProcessingResult bulkLoanReassignment(JsonCommand command); 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 4bf120ede..c2fe45a55 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 @@ -22,6 +22,7 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import java.math.BigDecimal; +import java.time.Instant; import java.time.LocalDate; import java.time.format.DateTimeFormatter; 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.LoanChargeCannotBeWaivedReason; 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.exception.ClientNotActiveException; 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.LoanAccountDomainService; 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.LoanDisbursementDetails; 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.exception.DateMismatchException; 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.LoanForeclosureException; import org.apache.fineract.portfolio.loanaccount.exception.LoanMultiDisbursementException; @@ -238,6 +245,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf private final CashierTransactionDataValidator cashierTransactionDataValidator; private final GLIMAccountInfoRepository glimRepository; private final LoanRepository loanRepository; + private final LoanChargePaidByRepository loanChargePaidByRepository; @Autowired public LoanWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context, @@ -267,7 +275,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessingStrategy, final CodeValueRepositoryWrapper codeValueRepository, final LoanRepositoryWrapper loanRepositoryWrapper, final CashierTransactionDataValidator cashierTransactionDataValidator, final GLIMAccountInfoRepository glimRepository, - final LoanRepository loanRepository) { + final LoanRepository loanRepository, final LoanChargePaidByRepository loanChargePaidByRepository) { this.context = context; this.loanEventApiJsonValidator = loanEventApiJsonValidator; this.loanAssembler = loanAssembler; @@ -308,6 +316,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf this.cashierTransactionDataValidator = cashierTransactionDataValidator; this.loanRepository = loanRepository; this.glimRepository = glimRepository; + this.loanChargePaidByRepository = loanChargePaidByRepository; } private LoanLifecycleStateMachine defaultLoanLifecycleStateMachine() { @@ -1590,6 +1599,155 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf .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 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 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 @Override public CommandProcessingResult waiveLoanCharge(final Long loanId, final Long loanChargeId, final JsonCommand command) { diff --git a/fineract-provider/src/main/resources/sql/migrations/core_db/V3__mifosx-permissions-and-authorisation-utf8.sql b/fineract-provider/src/main/resources/sql/migrations/core_db/V3__mifosx-permissions-and-authorisation-utf8.sql index 6c9d94880..fc9a66190 100644 --- a/fineract-provider/src/main/resources/sql/migrations/core_db/V3__mifosx-permissions-and-authorisation-utf8.sql +++ b/fineract-provider/src/main/resources/sql/migrations/core_db/V3__mifosx-permissions-and-authorisation-utf8.sql @@ -286,6 +286,7 @@ INSERT INTO `m_permission` ('transaction_loan', 'WRITEOFF_LOAN_CHECKER', 'LOAN', 'WRITEOFF', '0'), ('transaction_loan', 'CLOSE_LOAN_CHECKER', 'LOAN', 'CLOSE', '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_CHECKER', 'SAVINGSACCOUNT', 'DEPOSIT', '0'), ('transaction_savings', 'WITHDRAWAL_SAVINGSACCOUNT', 'SAVINGSACCOUNT', 'WITHDRAWAL', '1'),