From 63751ceb8c3a345064459b0f057949519d73e69b Mon Sep 17 00:00:00 2001 From: sachinkulkarni12 Date: Tue, 5 Jan 2016 12:06:42 +0530 Subject: [PATCH] CC-32: undo last tranche disbursal feature --- .../LoanApplicationApprovalTest.java | 4 +- .../LoanApplicationUndoLastTrancheTest.java | 145 ++++++++++++++++++ .../common/loans/LoanTransactionHelper.java | 15 ++ .../service/CommandWrapperBuilder.java | 9 ++ .../BusinessEventNotificationConstants.java | 2 +- .../loanaccount/api/LoansApiResource.java | 3 + .../portfolio/loanaccount/domain/Loan.java | 97 +++++++++++- .../loanaccount/domain/LoanCharge.java | 4 + .../loanaccount/domain/LoanEvent.java | 1 + .../loanaccount/domain/LoanTransaction.java | 4 + .../UndoLastTrancheDisbursementException.java | 18 +++ .../UndoLastDisbursalLoanCommandHandler.java | 34 ++++ .../service/LoanWritePlatformService.java | 2 + ...WritePlatformServiceJpaRepositoryImpl.java | 103 ++++++++++++- .../V285__undo_last_tranche_script.sql | 3 + 15 files changed, 439 insertions(+), 5 deletions(-) create mode 100644 mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/LoanApplicationUndoLastTrancheTest.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/exception/UndoLastTrancheDisbursementException.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/handler/UndoLastDisbursalLoanCommandHandler.java create mode 100644 mifosng-provider/src/main/resources/sql/migrations/core_db/V285__undo_last_tranche_script.sql diff --git a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/LoanApplicationApprovalTest.java b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/LoanApplicationApprovalTest.java index 54706d262..214e3abb9 100644 --- a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/LoanApplicationApprovalTest.java +++ b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/LoanApplicationApprovalTest.java @@ -101,7 +101,7 @@ public class LoanApplicationApprovalTest { } - private HashMap createTrancheDetail(final String date, final String amount) { + public HashMap createTrancheDetail(final String date, final String amount) { HashMap detail = new HashMap<>(); detail.put("expectedDisbursementDate", date); detail.put("principal", amount); @@ -242,7 +242,7 @@ public class LoanApplicationApprovalTest { return this.loanTransactionHelper.getLoanId(loanApplication); } - private Integer applyForLoanApplicationWithTranches(final Integer clientID, final Integer loanProductID, String principal, + public Integer applyForLoanApplicationWithTranches(final Integer clientID, final Integer loanProductID, String principal, List tranches) { System.out.println("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------"); final String loanApplicationJSON = new LoanApplicationTestBuilder() diff --git a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/LoanApplicationUndoLastTrancheTest.java b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/LoanApplicationUndoLastTrancheTest.java new file mode 100644 index 000000000..d3eb5d27a --- /dev/null +++ b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/LoanApplicationUndoLastTrancheTest.java @@ -0,0 +1,145 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.integrationtests; + +import java.math.BigDecimal; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mifosplatform.integrationtests.common.ClientHelper; +import org.mifosplatform.integrationtests.common.Utils; +import org.mifosplatform.integrationtests.common.loans.LoanApplicationTestBuilder; +import org.mifosplatform.integrationtests.common.loans.LoanProductTestBuilder; +import org.mifosplatform.integrationtests.common.loans.LoanStatusChecker; +import org.mifosplatform.integrationtests.common.loans.LoanTransactionHelper; + +import com.jayway.restassured.builder.RequestSpecBuilder; +import com.jayway.restassured.builder.ResponseSpecBuilder; +import com.jayway.restassured.http.ContentType; +import com.jayway.restassured.specification.RequestSpecification; +import com.jayway.restassured.specification.ResponseSpecification; +import org.mifosplatform.integrationtests.LoanApplicationApprovalTest; + +@SuppressWarnings("rawtypes") +public class LoanApplicationUndoLastTrancheTest { + + private ResponseSpecification responseSpec; + private RequestSpecification requestSpec; + private LoanTransactionHelper loanTransactionHelper; + private LoanApplicationApprovalTest loanApplicationApprovalTest; + + @Before + public void setup() { + Utils.initializeRESTAssured(); + this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); + this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); + this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); + this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec); + this.loanApplicationApprovalTest = new LoanApplicationApprovalTest(); + } + + @Test + public void LoanApplicationUndoLastTranche(){ + + final String proposedAmount = "5000"; + final String approvalAmount = "2000"; + final String approveDate = "1 March 2014"; + final String expectedDisbursementDate = "1 March 2014"; + final String disbursalDate = "1 March 2014"; + + // CREATE CLIENT + final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, "01 January 2014"); + System.out.println("---------------------------------CLIENT CREATED WITH ID---------------------------------------------------" + + clientID); + + // CREATE LOAN MULTIDISBURSAL PRODUCT + final Integer loanProductID = this.loanTransactionHelper.getLoanProductId(new LoanProductTestBuilder() + .withInterestTypeAsDecliningBalance().withTranches(true).build(null)); + System.out.println("----------------------------------LOAN PRODUCT CREATED WITH ID-------------------------------------------" + + loanProductID); + + // CREATE TRANCHES + List createTranches = new ArrayList<>(); + createTranches.add(this.loanApplicationApprovalTest.createTrancheDetail("1 March 2014", "1000")); + createTranches.add(this.loanApplicationApprovalTest.createTrancheDetail("23 June 2014", "4000")); + + // APPROVE TRANCHES + List approveTranches = new ArrayList<>(); + approveTranches.add(this.loanApplicationApprovalTest.createTrancheDetail("1 March 2014", "1000")); + approveTranches.add(this.loanApplicationApprovalTest.createTrancheDetail("23 June 2014", "1000")); + + // APPLY FOR LOAN WITH TRANCHES + final Integer loanID = applyForLoanApplicationWithTranches(clientID, loanProductID, proposedAmount, createTranches); + System.out.println("-----------------------------------LOAN CREATED WITH LOANID-------------------------------------------------" + + loanID); + HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, loanID); + + // VALIDATE THE LOAN STATUS + LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); + + System.out.println("-----------------------------------APPROVE LOAN-----------------------------------------------------------"); + loanStatusHashMap = this.loanTransactionHelper.approveLoanWithApproveAmount(approveDate, expectedDisbursementDate, approvalAmount, + loanID, approveTranches); + + // VALIDATE THE LOAN IS APPROVED + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap); + + // DISBURSE A LOAN + this.loanTransactionHelper.disburseLoan(disbursalDate, loanID); + loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, loanID); + + // VALIDATE THE LOAN IS ACTIVE STATUS + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + + System.out.println("-------------Make repayment 1-----------"); + this.loanTransactionHelper.makeRepayment("01 April 2014", Float.valueOf("420"), loanID); + System.out.println("-------------Make repayment 2-----------"); + this.loanTransactionHelper.makeRepayment("01 May 2014", Float.valueOf("412"), loanID); + System.out.println("-------------Make repayment 3-----------"); + this.loanTransactionHelper.makeRepayment("01 June 2014", Float.valueOf("204"), loanID); + // DISBURSE A SECOND TRANCHE + this.loanTransactionHelper.disburseLoan("23 June 2014", loanID); + // UNDO LAST TRANCHE + Float disbursedAmount = this.loanTransactionHelper.undoLastDisbursal(loanID); + validateDisbursedAmount(disbursedAmount); + } + + private void validateDisbursedAmount(Float disbursedAmount) { + Assert.assertEquals(Float.valueOf("1000.0"), disbursedAmount); + + } + + public Integer applyForLoanApplicationWithTranches(final Integer clientID, final Integer loanProductID, String principal, + List tranches) { + System.out.println("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------"); + final String loanApplicationJSON = new LoanApplicationTestBuilder() + // + .withPrincipal(principal) + // + .withLoanTermFrequency("5") + // + .withLoanTermFrequencyAsMonths() + // + .withNumberOfRepayments("5").withRepaymentEveryAfter("1").withRepaymentFrequencyTypeAsMonths() // + .withInterestRatePerPeriod("2") // + .withExpectedDisbursementDate("1 March 2014") // + .withTranches(tranches) // + .withInterestTypeAsDecliningBalance() // + .withSubmittedOnDate("1 March 2014") // + .build(clientID.toString(), loanProductID.toString(), null); + + return this.loanTransactionHelper.getLoanId(loanApplicationJSON); + } + + +} diff --git a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanTransactionHelper.java b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanTransactionHelper.java index cfa611a35..7fb9c9952 100755 --- a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanTransactionHelper.java +++ b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanTransactionHelper.java @@ -32,6 +32,7 @@ public class LoanTransactionHelper { private static final String DISBURSE_LOAN_COMMAND = "disburse"; private static final String DISBURSE_LOAN_TO_SAVINGS_COMMAND = "disburseToSavings"; private static final String UNDO_DISBURSE_LOAN_COMMAND = "undoDisbursal"; + private static final String UNDO_LAST_DISBURSE_LOAN_COMMAND = "undolastdisbursal"; private static final String WRITE_OFF_LOAN_COMMAND = "writeoff"; private static final String WAIVE_INTEREST_COMMAND = "waiveinterest"; private static final String MAKE_REPAYMENT_COMMAND = "repayment"; @@ -167,6 +168,13 @@ public class LoanTransactionHelper { System.out.println("IN DISBURSE LOAN URL " + url); return performLoanTransaction(createLoanOperationURL(UNDO_DISBURSE_LOAN_COMMAND, loanID), undoDisburseJson); } + + public Float undoLastDisbursal(final Integer loanID) { + final String undoLastDisburseJson = "{'note' : 'UNDO LAST DISBURSAL'}"; + final String url = createLoanOperationURL(UNDO_LAST_DISBURSE_LOAN_COMMAND, loanID); + System.out.println("IN UNDO LAST DISBURSE LOAN URL " + url); + return performUndoLastLoanDisbursementTransaction(createLoanOperationURL(UNDO_LAST_DISBURSE_LOAN_COMMAND, loanID), undoLastDisburseJson); + } public void recoverFromGuarantor(final Integer loanID) { performLoanTransaction(createLoanOperationURL(RECOVER_FROM_GUARANTORS_COMMAND, loanID), "", ""); @@ -424,6 +432,13 @@ public class LoanTransactionHelper { "changes"); return (HashMap) response.get("status"); } + + private Float performUndoLastLoanDisbursementTransaction(final String postURLForLoanTransaction, final String jsonToBeSent) { + + final HashMap response = Utils.performServerPost(this.requestSpec, this.responseSpec, postURLForLoanTransaction, jsonToBeSent, + "changes"); + return (Float) response.get("disbursedAmount"); + } private Object performLoanTransaction(final String postURLForLoanTransaction, final String jsonToBeSent, final String responseAttribute) { return Utils.performServerPost(this.requestSpec, this.responseSpec, postURLForLoanTransaction, jsonToBeSent, responseAttribute); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java index 68fc7b528..727ca3fa6 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java @@ -817,6 +817,15 @@ public class CommandWrapperBuilder { this.href = "/loans/" + loanId; return this; } + + public CommandWrapperBuilder undoLastDisbursalLoanApplication(final Long loanId) { + this.actionName = "DISBURSALLASTUNDO"; + this.entityName = "LOAN"; + this.entityId = loanId; + this.loanId = loanId; + this.href = "/loans/" + loanId; + return this; + } public CommandWrapperBuilder assignLoanOfficer(final Long loanId) { this.actionName = "UPDATELOANOFFICER"; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/common/BusinessEventNotificationConstants.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/common/BusinessEventNotificationConstants.java index ea44dfd31..205cee2de 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/common/BusinessEventNotificationConstants.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/common/BusinessEventNotificationConstants.java @@ -11,7 +11,7 @@ import java.util.Set; public class BusinessEventNotificationConstants { public static enum BUSINESS_EVENTS { - LOAN_APPROVED("loan_approved"), LOAN_UNDO_APPROVAL("loan_undo_approval"), LOAN_UNDO_DISBURSAL("loan_undo_disbursal"), LOAN_UNDO_TRANSACTION( + LOAN_APPROVED("loan_approved"), LOAN_UNDO_APPROVAL("loan_undo_approval"), LOAN_UNDO_DISBURSAL("loan_undo_disbursal"), LOAN_UNDO_LASTDISBURSAL("loan_undo_lastdisbursal"),LOAN_UNDO_TRANSACTION( "loan_undo_transaction"), LOAN_ADJUST_TRANSACTION("loan_adjust_transaction"), LOAN_MAKE_REPAYMENT( "loan_repayment_transaction"), LOAN_WRITTEN_OFF("loan_writtenoff"), LOAN_UNDO_WRITTEN_OFF("loan_undo_writtenoff"), LOAN_DISBURSAL( "loan_disbursal"), LOAN_WAIVE_INTEREST("loan_waive_interest"), LOAN_CLOSE("loan_close"), LOAN_CLOSE_AS_RESCHEDULE( diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoansApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoansApiResource.java index d168997e2..cfc89540f 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoansApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoansApiResource.java @@ -661,6 +661,9 @@ public class LoansApiResource { } else if (is(commandParam, "undodisbursal")) { final CommandWrapper commandRequest = builder.undoLoanApplicationDisbursal(loanId).build(); result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + }else if (is(commandParam, "undolastdisbursal")) { + final CommandWrapper commandRequest = builder.undoLastDisbursalLoanApplication(loanId).build(); + result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); } if (is(commandParam, "assignloanofficer")) { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java index 529ad40c7..2e3a2e3f0 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java @@ -98,6 +98,7 @@ import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerAssignmentDa import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerAssignmentException; import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerUnassignmentDateException; import org.mifosplatform.portfolio.loanaccount.exception.MultiDisbursementDataRequiredException; +import org.mifosplatform.portfolio.loanaccount.exception.UndoLastTrancheDisbursementException; import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.AprCalculator; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; @@ -105,6 +106,7 @@ import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleG import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod; +import org.mifosplatform.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequest; import org.mifosplatform.portfolio.loanproduct.LoanProductConstants; import org.mifosplatform.portfolio.loanproduct.domain.AmortizationMethod; import org.mifosplatform.portfolio.loanproduct.domain.InterestCalculationPeriodMethod; @@ -4517,6 +4519,10 @@ public class Loan extends AbstractPersistable { action = "refund"; postfix = "cannot.be.made.before.client.transfer.date"; break; + case LOAN_DISBURSAL_UNDO_LAST: + errorMessage = "Cannot undo a last disbursal in another branch"; + action = "disbursal"; + postfix = "cannot.be.undone.before.client.transfer.date"; default: break; } @@ -4567,6 +4573,18 @@ public class Loan extends AbstractPersistable { } return currentTransactionDate; } + + public LocalDate getLastRepaymentDate() { + LocalDate currentTransactionDate = getDisbursementDate(); + for (final LoanTransaction previousTransaction : this.loanTransactions) { + if (previousTransaction.isRepayment()) { + if (currentTransactionDate.isBefore(previousTransaction.getTransactionDate())) { + currentTransactionDate = previousTransaction.getTransactionDate(); + } + } + } + return currentTransactionDate; + } public LocalDate getLastUserTransactionForChargeCalc() { LocalDate lastTransaction = getDisbursementDate(); @@ -4738,6 +4756,13 @@ public class Loan extends AbstractPersistable { dataValidationErrors.add(error); } break; + case LOAN_DISBURSAL_UNDO_LAST: + if (!isOpen()) { + final String defaultUserMessage = "Loan Undo last disbursal is not allowed. Loan Account is not active."; + final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.undo.last.disbursal.account.is.not.active", + defaultUserMessage); + dataValidationErrors.add(error); + } default: break; } @@ -5550,6 +5575,76 @@ public class Loan extends AbstractPersistable { trancheCharges.add(new LoanTrancheCharge(charge, this)); } } + + public Map undoLastDisbursal(ScheduleGeneratorDTO scheduleGeneratorDTO, List existingTransactionIds, + List existingReversedTransactionIds, AppUser currentUser, Loan loan) { + + validateAccountStatus(LoanEvent.LOAN_DISBURSAL_UNDO_LAST); + existingTransactionIds.addAll(findExistingTransactionIds()); + existingReversedTransactionIds.addAll(findExistingReversedTransactionIds()); + final Map actualChanges = new LinkedHashMap<>(); + validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_DISBURSAL_UNDO_LAST, getDisbursementDate()); + LocalDate actualDisbursementDate = null; + LocalDate lastTransactionDate = getDisbursementDate(); + List loanTransactions = retreiveListOfTransactionsExcludeAccruals(); + Collections.reverse(loanTransactions); + for (final LoanTransaction previousTransaction : loanTransactions) { + if (lastTransactionDate.isBefore(previousTransaction.getTransactionDate())) { + if(previousTransaction.isRepayment() || previousTransaction.isWaiver() || + previousTransaction.isChargePayment()){ + throw new UndoLastTrancheDisbursementException(previousTransaction.getId()); } + } + if(previousTransaction.isDisbursement()){ + lastTransactionDate = previousTransaction.getTransactionDate(); + break; + } + } + actualDisbursementDate = lastTransactionDate; + updateLoanToLastDisbursalState(actualDisbursementDate); + reverseExistingTransactionsTillLastDisbursal(actualDisbursementDate); + loan.recalculateScheduleFromLastTransaction(scheduleGeneratorDTO, + existingTransactionIds, existingReversedTransactionIds, currentUser); + actualChanges.put("undolastdisbursal","true"); + actualChanges.put("disbursedAmount",this.getDisbursedAmount()); + updateLoanSummaryDerivedFields(); + + return actualChanges; + } + /** + * Reverse only disbursement, accruals, and repayments at disbursal transactions + * @param actualDisbursementDate + * @return + */ + public List reverseExistingTransactionsTillLastDisbursal(LocalDate actualDisbursementDate) { + final List reversedTransactions = new ArrayList<>(); + for (final LoanTransaction transaction : this.loanTransactions) { + if((actualDisbursementDate.equals(transaction.getTransactionDate()) || actualDisbursementDate.isBefore(transaction.getTransactionDate())) + && transaction.isAllowTypeTransactionAtTheTimeOfLastUndo()){ + reversedTransactions.add(transaction); + transaction.reverse(); + } + } + return reversedTransactions; + } + + private void updateLoanToLastDisbursalState(LocalDate actualDisbursementDate) { + + for (final LoanCharge charge : charges()) { + if (charge.isOverdueInstallmentCharge()) { + charge.setActive(false); + } else if (charge.isTrancheDisbursementCharge() && actualDisbursementDate.equals(new LocalDate + (charge.getTrancheDisbursementCharge().getloanDisbursementDetails().actualDisbursementDate()))) { + charge.resetToOriginal(loanCurrency()); + } + } + for (final LoanDisbursementDetails details : this.disbursementDetails) { + if (actualDisbursementDate.equals(new LocalDate(details.actualDisbursementDate()))) { + this.loanRepaymentScheduleDetail.setPrincipal(getDisbursedAmount().subtract(details.principal())); + details.updateActualDisbursementDate(null); + } + } + updateLoanSummaryDerivedFields(); + } public Boolean getIsFloatingInterestRate() { return this.isFloatingInterestRate; @@ -5590,5 +5685,5 @@ public class Loan extends AbstractPersistable { public void setExpectedFirstRepaymentOnDate(Date expectedFirstRepaymentOnDate) { this.expectedFirstRepaymentOnDate = expectedFirstRepaymentOnDate; } - + } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanCharge.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanCharge.java index d7e13ec51..c36893d4e 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanCharge.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanCharge.java @@ -961,4 +961,8 @@ public class LoanCharge extends AbstractPersistable { public Loan getLoan() { return this.loan; } + + public boolean isTrancheDisbursementCharge() { + return ChargeTimeType.fromInt(this.chargeTime).equals(ChargeTimeType.TRANCHE_DISBURSEMENT); + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanEvent.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanEvent.java index cf1c92673..29b00fb56 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanEvent.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanEvent.java @@ -18,6 +18,7 @@ public enum LoanEvent { LOAN_RECOVERY_PAYMENT, // LOAN_DISBURSED, // LOAN_DISBURSAL_UNDO, // + LOAN_DISBURSAL_UNDO_LAST, // LOAN_REPAYMENT_OR_WAIVER, // REPAID_IN_FULL, // WRITE_OFF_OUTSTANDING, // diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransaction.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransaction.java index d9ee3e91e..93e3bebb3 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransaction.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransaction.java @@ -713,5 +713,9 @@ public final class LoanTransaction extends AbstractPersistable { public Set getLoanTransactionToRepaymentScheduleMappings() { return this.loanTransactionToRepaymentScheduleMappings; } + + public Boolean isAllowTypeTransactionAtTheTimeOfLastUndo(){ + return isDisbursement() || isAccrual() || isRepaymentAtDisbursement(); + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/exception/UndoLastTrancheDisbursementException.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/exception/UndoLastTrancheDisbursementException.java new file mode 100644 index 000000000..ef2011f78 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/exception/UndoLastTrancheDisbursementException.java @@ -0,0 +1,18 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.loanaccount.exception; + +import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class UndoLastTrancheDisbursementException extends AbstractPlatformDomainRuleException{ + + public UndoLastTrancheDisbursementException(final Object... defaultUserMessageArgs) { + super("error.msg.cannot.undo.last.disbursal.after.repayments or waivers"," Cannot undo last disbursement after repayments or waivers.", + defaultUserMessageArgs); + + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/handler/UndoLastDisbursalLoanCommandHandler.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/handler/UndoLastDisbursalLoanCommandHandler.java new file mode 100644 index 000000000..ee0393234 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/handler/UndoLastDisbursalLoanCommandHandler.java @@ -0,0 +1,34 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.loanaccount.handler; + +import org.mifosplatform.commands.annotation.CommandType; +import org.mifosplatform.commands.handler.NewCommandSourceHandler; +import org.mifosplatform.infrastructure.core.api.JsonCommand; +import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; +import org.mifosplatform.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 = "LOAN", action = "DISBURSALLASTUNDO") +public class UndoLastDisbursalLoanCommandHandler implements NewCommandSourceHandler { + + private final LoanWritePlatformService writePlatformService; + + @Autowired + public UndoLastDisbursalLoanCommandHandler(final LoanWritePlatformService writePlatformService) { + this.writePlatformService = writePlatformService; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + return this.writePlatformService.undoLastLoanDisbursal(command.entityId(), command); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformService.java index 9cfc9ee72..39770d094 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformService.java @@ -91,4 +91,6 @@ public interface LoanWritePlatformService { void recalculateInterest(long loanId); + CommandProcessingResult undoLastLoanDisbursal(Long loanId, JsonCommand command); + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index 193c11720..f74e7989e 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -3105,8 +3105,109 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf map.put(entityEvent, entity); return map; } + + @Override + @Transactional + public CommandProcessingResult undoLastLoanDisbursal(Long loanId, JsonCommand command) { + final AppUser currentUser = getAppUserIfPresent(); - private FloatingRateDTO constructFloatingRateDTO(final Loan loan) { + final Loan loan = this.loanAssembler.assembleFrom(loanId); + final LocalDate recalculateFromDate = loan.getLastRepaymentDate(); + validateIsMultiDisbursalLoanAndDisbursedMoreThanOneTranche(loan); + checkClientOrGroupActive(loan); + this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_UNDO_LASTDISBURSAL, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); + + final MonetaryCurrency currency = loan.getCurrency(); + final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency); + final List existingTransactionIds = new ArrayList<>(); + final List existingReversedTransactionIds = new ArrayList<>(); + + ScheduleGeneratorDTO scheduleGeneratorDTO = getScheduleGeneratorDTO(loan, recalculateFromDate, applicationCurrency); + + final Map changes = loan.undoLastDisbursal(scheduleGeneratorDTO, existingTransactionIds, + existingReversedTransactionIds, currentUser, loan); + if (!changes.isEmpty()) { + saveAndFlushLoanWithDataIntegrityViolationChecks(loan); + String noteText = null; + if (command.hasParameter("note")) { + noteText = command.stringValueOfParameterNamed("note"); + if (StringUtils.isNotBlank(noteText)) { + final Note note = Note.loanNote(loan, noteText); + this.noteRepository.save(note); + } + } + boolean isAccountTransfer = false; + final Map accountingBridgeData = loan.deriveAccountingBridgeData(applicationCurrency.toData(), + existingTransactionIds, existingReversedTransactionIds, isAccountTransfer); + this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData); + this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_UNDO_LASTDISBURSAL, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); + } + + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(loan.getId()) // + .withOfficeId(loan.getOfficeId()) // + .withClientId(loan.getClientId()) // + .withGroupId(loan.getGroupId()) // + .withLoanId(loanId) // + .with(changes) // + .build(); + } + + private ScheduleGeneratorDTO getScheduleGeneratorDTO(final Loan loan, final LocalDate recalculateFromDate, + final ApplicationCurrency applicationCurrency) { + + final CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByEntityId(loan.getId(), + CalendarEntityType.LOANS.getValue()); + final LocalDate actualDisbursementDate = loan.getDisbursementDate(); + final LocalDate calculatedRepaymentsStartingFromDate = this.loanAccountDomainService.getCalculatedRepaymentsStartingFromDate( + actualDisbursementDate, loan, calendarInstance); + + final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled(); + final List holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(), + actualDisbursementDate.toDate()); + final WorkingDays workingDays = this.workingDaysRepository.findOne(); + + HolidayDetailDTO holidayDetailDTO = new HolidayDetailDTO(isHolidayEnabled, holidays, workingDays); + CalendarInstance restCalendarInstance = null; + CalendarInstance compoundingCalendarInstance = null; + if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { + restCalendarInstance = calendarInstanceRepository.findCalendarInstaneByEntityId(loan.loanInterestRecalculationDetailId(), + CalendarEntityType.LOAN_RECALCULATION_REST_DETAIL.getValue()); + compoundingCalendarInstance = calendarInstanceRepository.findCalendarInstaneByEntityId( + loan.loanInterestRecalculationDetailId(), CalendarEntityType.LOAN_RECALCULATION_COMPOUNDING_DETAIL.getValue()); + } + FloatingRateDTO floatingRateDTO = constructFloatingRateDTO(loan); + Long overduePenaltyWaitPeriod = this.configurationDomainService.retrievePenaltyWaitPeriod(); + + ScheduleGeneratorDTO scheduleGeneratorDTO = new ScheduleGeneratorDTO(this.loanScheduleFactory, applicationCurrency, + calculatedRepaymentsStartingFromDate, holidayDetailDTO, restCalendarInstance, compoundingCalendarInstance, + recalculateFromDate, overduePenaltyWaitPeriod, floatingRateDTO); + + return scheduleGeneratorDTO; + } + + private void validateIsMultiDisbursalLoanAndDisbursedMoreThanOneTranche(Loan loan) { + if(!loan.isMultiDisburmentLoan()){ + final String errorMessage = "loan.product.does.not.support.multiple.disbursals.cannot.undo.last.disbursal"; + throw new LoanMultiDisbursementException(errorMessage); + } + Integer trancheDisbursedCount = 0; + for(LoanDisbursementDetails disbursementDetails : loan.getDisbursementDetails()){ + if(disbursementDetails.actualDisbursementDate() != null){ + trancheDisbursedCount++; + } + } + if(trancheDisbursedCount <=1){ + final String errorMessage = "tranches.should.be.disbursed.more.than.one.to.undo.last.disbursal"; + throw new LoanMultiDisbursementException(errorMessage); + } + + } + + private FloatingRateDTO constructFloatingRateDTO(final Loan loan) { FloatingRateDTO floatingRateDTO = null; if (loan.loanProduct().isLinkedToFloatingInterestRate()) { boolean isFloatingInterestRate = loan.getIsFloatingInterestRate(); diff --git a/mifosng-provider/src/main/resources/sql/migrations/core_db/V285__undo_last_tranche_script.sql b/mifosng-provider/src/main/resources/sql/migrations/core_db/V285__undo_last_tranche_script.sql new file mode 100644 index 000000000..36ea8952d --- /dev/null +++ b/mifosng-provider/src/main/resources/sql/migrations/core_db/V285__undo_last_tranche_script.sql @@ -0,0 +1,3 @@ +INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) VALUES ('transaction_loan', 'DISBURSALLASTUNDO_LOAN', 'LOAN', 'DISBURSALLASTUNDO', 0); +INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) VALUES ('transaction_loan', 'DISBURSALLASTUNDO_LOAN_CHECKER', 'LOAN', 'DISBURSALLASTUNDO_CHECKER', 0); +