diff --git a/api-docs/apiLive.htm b/api-docs/apiLive.htm index 37cce1b5f..c2206ef0e 100644 --- a/api-docs/apiLive.htm +++ b/api-docs/apiLive.htm @@ -11467,6 +11467,13 @@ An enumeration that indicates the type of transaction processing strategy to be be overdue before being classified as a NPA (non performing asset) + + accountMovesOutOfNPAOnlyOnArrearsCompletion + + + Optional: Boolean - if provided as true, Loan Account moves out of NPA state only when all arrears are cleared + + accountingRule @@ -11528,6 +11535,12 @@ An enumeration that indicates the type of transaction processing strategy to be Specifies rest frequency start date for interest recalculation.
+ + isArrearsBasedOnOriginalSchedule + + + If Specified as true, arrears will be identified based on original schedule.
+ @@ -11642,7 +11655,8 @@ GET https://DomainName/api/v1/loanproducts "id":1, "code":"interestRecalculationFrequencyType.same.as.repayment.period", "value":"Same as repayment period" - } + }, + "isArrearsBasedOnOriginalSchedule" : true }, "accountingRule": { "id": 2, @@ -12772,7 +12786,7 @@ GET https://DomainName/api/v1/loanproducts/{productId} numberOfRepaymentVariationsForBorrowerCycle, interestRateVariationsForBorrowerCycle, multiDisburseLoan,maxTrancheCount, - outstandingLoanBalance,overdueDaysForNPA,holdGuaranteeFunds, principalThresholdForLastInstalment + outstandingLoanBalance,overdueDaysForNPA,holdGuaranteeFunds, principalThresholdForLastInstalment, accountMovesOutOfNPAOnlyOnArrearsCompletion @@ -12806,6 +12820,14 @@ GET https://DomainName/api/v1/loanproducts/{productId} + + + + + + +
Additional Optional Fields if interest recalculation is enabled(true)
isArrearsBasedOnOriginalSchedule
+ diff --git a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/ClientLoanIntegrationTest.java b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/ClientLoanIntegrationTest.java index 93afd772e..58f9c8358 100644 --- a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/ClientLoanIntegrationTest.java +++ b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/ClientLoanIntegrationTest.java @@ -3584,7 +3584,7 @@ public class ClientLoanIntegrationTest { final Integer loanProductID = createLoanProductWithInterestRecalculation(LoanProductTestBuilder.MIFOS_STANDARD_STRATEGY, LoanProductTestBuilder.RECALCULATION_COMPOUNDING_METHOD_INTEREST_AND_FEE, LoanProductTestBuilder.RECALCULATION_STRATEGY_RESCHEDULE_NEXT_REPAYMENTS, - LoanProductTestBuilder.RECALCULATION_FREQUENCY_TYPE_DAILY, "1", REST_START_DATE, null, overdueFeeChargeId.toString()); + LoanProductTestBuilder.RECALCULATION_FREQUENCY_TYPE_DAILY, "1", REST_START_DATE, null, overdueFeeChargeId.toString(), false); final Integer loanID = applyForLoanApplicationForInterestRecalculation(clientID, loanProductID, LOAN_DISBURSEMENT_DATE, REST_START_DATE, LoanApplicationTestBuilder.MIFOS_STANDARD_STRATEGY, null); @@ -3766,6 +3766,156 @@ public class ClientLoanIntegrationTest { } + @Test + public void testLoanScheduleWithInterestRecalculation_WITH_CURRENT_REPAYMENT_BASED_ARREARS_AGEING() { + this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec); + + DateFormat dateFormat = new SimpleDateFormat("dd MMMM yyyy"); + + Calendar todaysDate = Calendar.getInstance(); + todaysDate.add(Calendar.DAY_OF_MONTH, -14); + final String LOAN_DISBURSEMENT_DATE = dateFormat.format(todaysDate.getTime()); + + final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec); + ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientID); + final Integer loanProductID = createLoanProductWithInterestRecalculation(LoanProductTestBuilder.RBI_INDIA_STRATEGY, + LoanProductTestBuilder.RECALCULATION_COMPOUNDING_METHOD_INTEREST, + LoanProductTestBuilder.RECALCULATION_STRATEGY_RESCHEDULE_NEXT_REPAYMENTS, + LoanProductTestBuilder.RECALCULATION_FREQUENCY_TYPE_DAILY, "1", LOAN_DISBURSEMENT_DATE, null); + + final Integer loanID = applyForLoanApplicationForInterestRecalculation(clientID, loanProductID, LOAN_DISBURSEMENT_DATE, + LOAN_DISBURSEMENT_DATE, LoanApplicationTestBuilder.RBI_INDIA_STRATEGY, new ArrayList(0)); + + Assert.assertNotNull(loanID); + HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, loanID); + LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); + + ArrayList loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID); + List> expectedvalues = new ArrayList<>(); + todaysDate = Calendar.getInstance(); + addRepaymentValues(expectedvalues, todaysDate, -1, "2482.76", "46.15", "0.0", "0.0"); + addRepaymentValues(expectedvalues, todaysDate, 1, "2494.22", "34.69", "0.0", "0.0"); + addRepaymentValues(expectedvalues, todaysDate, 1, "2505.73", "23.18", "0.0", "0.0"); + addRepaymentValues(expectedvalues, todaysDate, 1, "2517.29", "11.62", "0.0", "0.0"); + + verifyLoanRepaymentSchedule(loanSchedule, expectedvalues); + + System.out.println("-----------------------------------APPROVE LOAN-----------------------------------------"); + loanStatusHashMap = this.loanTransactionHelper.approveLoan(LOAN_DISBURSEMENT_DATE, loanID); + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap); + + System.out.println("-------------------------------DISBURSE LOAN-------------------------------------------"); + loanStatusHashMap = this.loanTransactionHelper.disburseLoan(LOAN_DISBURSEMENT_DATE, loanID); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + + loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID); + expectedvalues = new ArrayList<>(); + todaysDate = Calendar.getInstance(); + addRepaymentValues(expectedvalues, todaysDate, -1, "2482.76", "46.15", "0.0", "0.0"); + addRepaymentValues(expectedvalues, todaysDate, 1, "2482.55", "46.36", "0.0", "0.0"); + addRepaymentValues(expectedvalues, todaysDate, 1, "2505.67", "23.24", "0.0", "0.0"); + addRepaymentValues(expectedvalues, todaysDate, 1, "2529.02", "11.67", "0.0", "0.0"); + verifyLoanRepaymentSchedule(loanSchedule, expectedvalues); + + todaysDate = Calendar.getInstance(); + todaysDate.add(Calendar.DAY_OF_MONTH, -7); + HashMap loanSummary = this.loanTransactionHelper.getLoanSummary(this.requestSpec, this.responseSpec, loanID); + List dates = (List) loanSummary.get("overdueSinceDate"); + assertEquals(todaysDate.get(Calendar.YEAR), dates.get(0)); + assertEquals(todaysDate.get(Calendar.MONTH) + 1, dates.get(1)); + assertEquals(todaysDate.get(Calendar.DAY_OF_MONTH), dates.get(2)); + + todaysDate = Calendar.getInstance(); + todaysDate.add(Calendar.DAY_OF_MONTH, -8); + final String LOAN_FIRST_REPAYMENT_DATE = dateFormat.format(todaysDate.getTime()); + Float totalDueForCurrentPeriod = (Float) loanSchedule.get(1).get("totalDueForPeriod"); + this.loanTransactionHelper.makeRepayment(LOAN_FIRST_REPAYMENT_DATE, totalDueForCurrentPeriod, loanID); + + todaysDate = Calendar.getInstance(); + todaysDate.add(Calendar.DAY_OF_MONTH, -7); + loanSummary = this.loanTransactionHelper.getLoanSummary(this.requestSpec, this.responseSpec, loanID); + dates = (List) loanSummary.get("overdueSinceDate"); + assertEquals(todaysDate.get(Calendar.YEAR), dates.get(0)); + assertEquals(todaysDate.get(Calendar.MONTH) + 1, dates.get(1)); + assertEquals(todaysDate.get(Calendar.DAY_OF_MONTH), dates.get(2)); + + } + + @Test + public void testLoanScheduleWithInterestRecalculation_WITH_ORIGINAL_REPAYMENT_BASED_ARREARS_AGEING() { + this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec); + + DateFormat dateFormat = new SimpleDateFormat("dd MMMM yyyy"); + + Calendar todaysDate = Calendar.getInstance(); + todaysDate.add(Calendar.DAY_OF_MONTH, -14); + final String LOAN_DISBURSEMENT_DATE = dateFormat.format(todaysDate.getTime()); + + final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec); + ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientID); + final Integer loanProductID = createLoanProductWithInterestRecalculation(LoanProductTestBuilder.RBI_INDIA_STRATEGY, + LoanProductTestBuilder.RECALCULATION_COMPOUNDING_METHOD_INTEREST, + LoanProductTestBuilder.RECALCULATION_STRATEGY_RESCHEDULE_NEXT_REPAYMENTS, + LoanProductTestBuilder.RECALCULATION_FREQUENCY_TYPE_DAILY, "1", LOAN_DISBURSEMENT_DATE, null, null, true); + + final Integer loanID = applyForLoanApplicationForInterestRecalculation(clientID, loanProductID, LOAN_DISBURSEMENT_DATE, + LOAN_DISBURSEMENT_DATE, LoanApplicationTestBuilder.RBI_INDIA_STRATEGY, new ArrayList(0)); + + Assert.assertNotNull(loanID); + HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, loanID); + LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); + + ArrayList loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID); + List> expectedvalues = new ArrayList<>(); + todaysDate = Calendar.getInstance(); + addRepaymentValues(expectedvalues, todaysDate, -1, "2482.76", "46.15", "0.0", "0.0"); + addRepaymentValues(expectedvalues, todaysDate, 1, "2494.22", "34.69", "0.0", "0.0"); + addRepaymentValues(expectedvalues, todaysDate, 1, "2505.73", "23.18", "0.0", "0.0"); + addRepaymentValues(expectedvalues, todaysDate, 1, "2517.29", "11.62", "0.0", "0.0"); + + verifyLoanRepaymentSchedule(loanSchedule, expectedvalues); + + System.out.println("-----------------------------------APPROVE LOAN-----------------------------------------"); + loanStatusHashMap = this.loanTransactionHelper.approveLoan(LOAN_DISBURSEMENT_DATE, loanID); + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap); + + System.out.println("-------------------------------DISBURSE LOAN-------------------------------------------"); + loanStatusHashMap = this.loanTransactionHelper.disburseLoan(LOAN_DISBURSEMENT_DATE, loanID); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + + loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID); + expectedvalues = new ArrayList<>(); + todaysDate = Calendar.getInstance(); + addRepaymentValues(expectedvalues, todaysDate, -1, "2482.76", "46.15", "0.0", "0.0"); + addRepaymentValues(expectedvalues, todaysDate, 1, "2482.55", "46.36", "0.0", "0.0"); + addRepaymentValues(expectedvalues, todaysDate, 1, "2505.67", "23.24", "0.0", "0.0"); + addRepaymentValues(expectedvalues, todaysDate, 1, "2529.02", "11.67", "0.0", "0.0"); + verifyLoanRepaymentSchedule(loanSchedule, expectedvalues); + + todaysDate = Calendar.getInstance(); + todaysDate.add(Calendar.DAY_OF_MONTH, -7); + HashMap loanSummary = this.loanTransactionHelper.getLoanSummary(this.requestSpec, this.responseSpec, loanID); + List dates = (List) loanSummary.get("overdueSinceDate"); + assertEquals(todaysDate.get(Calendar.YEAR), dates.get(0)); + assertEquals(todaysDate.get(Calendar.MONTH) + 1, dates.get(1)); + assertEquals(todaysDate.get(Calendar.DAY_OF_MONTH), dates.get(2)); + + todaysDate = Calendar.getInstance(); + todaysDate.add(Calendar.DAY_OF_MONTH, -8); + final String LOAN_FIRST_REPAYMENT_DATE = dateFormat.format(todaysDate.getTime()); + Float totalDueForCurrentPeriod = (Float) loanSchedule.get(1).get("totalDueForPeriod"); + this.loanTransactionHelper.makeRepayment(LOAN_FIRST_REPAYMENT_DATE, totalDueForCurrentPeriod, loanID); + + todaysDate = Calendar.getInstance(); + todaysDate.add(Calendar.DAY_OF_MONTH, -7); + loanSummary = this.loanTransactionHelper.getLoanSummary(this.requestSpec, this.responseSpec, loanID); + dates = (List) loanSummary.get("overdueSinceDate"); + Assert.assertNull(dates); + + } + private void addRepaymentValues(List> expectedvalues, Calendar todaysDate, int addMonths, String principalDue, String interestDue, String feeChargesDue, String penaltyChargesDue) { Map values = new HashMap<>(3); @@ -3795,13 +3945,14 @@ public class ClientLoanIntegrationTest { final String recalculationRestFrequencyDate, final Account[] accounts) { return createLoanProductWithInterestRecalculation(repaymentStrategy, interestRecalculationCompoundingMethod, rescheduleStrategyMethod, recalculationRestFrequencyType, recalculationRestFrequencyInterval, - recalculationRestFrequencyDate, accounts, null); + recalculationRestFrequencyDate, accounts, null, false); } private Integer createLoanProductWithInterestRecalculation(final String repaymentStrategy, final String interestRecalculationCompoundingMethod, final String rescheduleStrategyMethod, final String recalculationRestFrequencyType, final String recalculationRestFrequencyInterval, - final String recalculationRestFrequencyDate, final Account[] accounts, final String chargeId) { + final String recalculationRestFrequencyDate, final Account[] accounts, final String chargeId, + boolean isArrearsBasedOnOriginalSchedule) { System.out.println("------------------------------CREATING NEW LOAN PRODUCT ---------------------------------------"); LoanProductTestBuilder builder = new LoanProductTestBuilder() .withPrincipal("10000000.00") @@ -3820,6 +3971,8 @@ public class ClientLoanIntegrationTest { builder = builder.withAccountingRulePeriodicAccrual(accounts); } + if (isArrearsBasedOnOriginalSchedule) builder = builder.withArrearsConfiguration(); + final String loanProductJSON = builder.build(chargeId); return this.loanTransactionHelper.getLoanProductId(loanProductJSON); } diff --git a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanProductTestBuilder.java b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanProductTestBuilder.java index a83fb9517..d383a841f 100644 --- a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanProductTestBuilder.java +++ b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanProductTestBuilder.java @@ -92,6 +92,7 @@ public class LoanProductTestBuilder { private String mandatoryGuarantee = null; private String minimumGuaranteeFromOwnFunds = null; private String minimumGuaranteeFromGuarantor = null; + private String isArrearsBasedOnOriginalSchedule = null; public String build(final String chargeId) { final HashMap map = new HashMap<>(); @@ -148,6 +149,9 @@ public class LoanProductTestBuilder { map.put("recalculationRestFrequencyType", recalculationRestFrequencyType); map.put("recalculationRestFrequencyInterval", recalculationRestFrequencyInterval); map.put("recalculationRestFrequencyDate", recalculationRestFrequencyDate); + if (isArrearsBasedOnOriginalSchedule != null) { + map.put("isArrearsBasedOnOriginalSchedule", isArrearsBasedOnOriginalSchedule); + } } if (holdGuaranteeFunds != null) { map.put("holdGuaranteeFunds", this.holdGuaranteeFunds); @@ -398,6 +402,11 @@ public class LoanProductTestBuilder { return this; } + public LoanProductTestBuilder withArrearsConfiguration() { + this.isArrearsBasedOnOriginalSchedule = "true"; + return this; + } + public LoanProductTestBuilder withOnHoldFundDetails(final String mandatoryGuarantee, final String minimumGuaranteeFromGuarantor, final String minimumGuaranteeFromOwnFunds) { this.holdGuaranteeFunds = true; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java index 310c2ff5b..33d852cfd 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java @@ -13,7 +13,6 @@ import static org.mifosplatform.portfolio.account.api.AccountTransfersApiConstan import static org.mifosplatform.portfolio.account.api.AccountTransfersApiConstants.transferDateParamName; import java.math.BigDecimal; -import java.util.Date; import java.util.List; import java.util.Locale; @@ -37,7 +36,6 @@ import org.mifosplatform.portfolio.loanaccount.domain.LoanAccountDomainService; import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction; import org.mifosplatform.portfolio.loanaccount.domain.LoanTransactionType; import org.mifosplatform.portfolio.loanaccount.exception.InvalidPaidInAdvanceAmountException; -import org.mifosplatform.portfolio.loanaccount.exception.InvalidRefundDateException; import org.mifosplatform.portfolio.loanaccount.service.LoanAssembler; import org.mifosplatform.portfolio.loanaccount.service.LoanReadPlatformService; import org.mifosplatform.portfolio.paymentdetail.domain.PaymentDetail; 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 1e159da76..ea44dfd31 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 @@ -12,8 +12,14 @@ 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_undo_transaction"), LOAN_MAKE_REPAYMENT("loan_repayment_transaction"), LOAN_WRITTEN_OFF("loan_writtenoff"), LOAN_UNDO_WRITTEN_OFF( - "loan_undo_writtenoff"); + "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( + "loan_close_as_reschedule"), LOAN_ADD_CHARGE("loan_add_charge"), LOAN_UPDATE_CHARGE("loan_update_charge"), LOAN_WAIVE_CHARGE( + "loan_waive_charge"), LOAN_DELETE_CHARGE("loan_delete_charge"), LOAN_CHARGE_PAYMENT("loan_charge_payment"), LOAN_INITIATE_TRANSFER( + "loan_initiate_transfer"), LOAN_ACCEPT_TRANSFER("loan_accept_transfer"), LOAN_WITHDRAW_TRANSFER("loan_withdraw_transfer"), LOAN_REJECT_TRANSFER( + "loan_reject_transfer"), LOAN_REASSIGN_OFFICER("loan_reassign_officer"), LOAN_REMOVE_OFFICER("loan_remove_officer"), LOAN_APPLY_OVERDUE_CHARGE( + "loan_apply_overdue_charge"), LOAN_INTEREST_RECALCULATION("loan_interest_recalculation"), LOAN_REFUND("loan_refund"); private final String value; @@ -36,4 +42,19 @@ public class BusinessEventNotificationConstants { return this.value; } } + + public static enum BUSINESS_ENTITY { + LOAN("loan"), LOAN_TRANSACTION("loan_transaction"), LOAN_CHARGE("loan_charge"), LOAN_ADJUSTED_TRANSACTION( + "loan_adjusted_transaction"); + + private final String value; + + private BUSINESS_ENTITY(final String value) { + this.value = value; + } + + public String getValue() { + return this.value; + } + } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/common/service/BusinessEventListner.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/common/service/BusinessEventListner.java index ce2195a4e..310bacdb6 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/common/service/BusinessEventListner.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/common/service/BusinessEventListner.java @@ -5,7 +5,9 @@ */ package org.mifosplatform.portfolio.common.service; -import org.springframework.data.jpa.domain.AbstractPersistable; +import java.util.Map; + +import org.mifosplatform.portfolio.common.BusinessEventNotificationConstants.BUSINESS_ENTITY; /** * The interface to be implemented by classes that want to be informed when a @@ -18,11 +20,11 @@ public interface BusinessEventListner { /** * Implement this method for notifications before executing Business Event */ - public void businessEventToBeExecuted(AbstractPersistable businessEventEntity); + public void businessEventToBeExecuted(Map businessEventEntity); /** * Implement this method for notifications after executing Business Event */ - public void businessEventWasExecuted(AbstractPersistable businessEventEntity); + public void businessEventWasExecuted(Map businessEventEntity); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/common/service/BusinessEventNotifierService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/common/service/BusinessEventNotifierService.java index 0fc55d1fd..0a48ee9bc 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/common/service/BusinessEventNotifierService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/common/service/BusinessEventNotifierService.java @@ -6,7 +6,8 @@ package org.mifosplatform.portfolio.common.service; import org.mifosplatform.portfolio.common.BusinessEventNotificationConstants.BUSINESS_EVENTS; -import org.springframework.data.jpa.domain.AbstractPersistable; +import org.mifosplatform.portfolio.common.BusinessEventNotificationConstants.BUSINESS_ENTITY; +import java.util.Map; /** * Implemented class is responsible for notifying the business event to @@ -19,13 +20,13 @@ public interface BusinessEventNotifierService { * Method should be called to notify listeners before Business event * execution for any pre-processing of event */ - public void notifyBusinessEventToBeExecuted(BUSINESS_EVENTS businessEvent, AbstractPersistable businessEventEntity); + public void notifyBusinessEventToBeExecuted(BUSINESS_EVENTS businessEvent, Map businessEventEntity); /** * Method should be called to notify listeners after Business event * execution for any post-processing of event */ - public void notifyBusinessEventWasExecuted(BUSINESS_EVENTS businessEvent, AbstractPersistable businessEventEntity); + public void notifyBusinessEventWasExecuted(BUSINESS_EVENTS businessEvent, Map businessEventEntity); /** * Method is to register a class as listener for pre-processing of any diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/common/service/BusinessEventNotifierServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/common/service/BusinessEventNotifierServiceImpl.java index 53637b9ef..e044ddc51 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/common/service/BusinessEventNotifierServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/common/service/BusinessEventNotifierServiceImpl.java @@ -10,8 +10,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.mifosplatform.portfolio.common.BusinessEventNotificationConstants.BUSINESS_ENTITY; import org.mifosplatform.portfolio.common.BusinessEventNotificationConstants.BUSINESS_EVENTS; -import org.springframework.data.jpa.domain.AbstractPersistable; import org.springframework.stereotype.Service; @Service @@ -31,7 +31,7 @@ public class BusinessEventNotifierServiceImpl implements BusinessEventNotifierSe * org.springframework.data.jpa.domain.AbstractPersistable) */ @Override - public void notifyBusinessEventToBeExecuted(BUSINESS_EVENTS businessEvent, AbstractPersistable businessEventEntity) { + public void notifyBusinessEventToBeExecuted(BUSINESS_EVENTS businessEvent, Map businessEventEntity) { List businessEventListners = this.preListners.get(businessEvent); if (businessEventListners != null) { for (BusinessEventListner eventListner : businessEventListners) { @@ -51,7 +51,7 @@ public class BusinessEventNotifierServiceImpl implements BusinessEventNotifierSe * org.springframework.data.jpa.domain.AbstractPersistable) */ @Override - public void notifyBusinessEventWasExecuted(BUSINESS_EVENTS businessEvent, AbstractPersistable businessEventEntity) { + public void notifyBusinessEventWasExecuted(BUSINESS_EVENTS businessEvent, Map businessEventEntity) { List businessEventListners = this.postListners.get(businessEvent); if (businessEventListners != null) { for (BusinessEventListner eventListner : businessEventListners) { 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 6d17123f0..650d33012 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 @@ -299,9 +299,6 @@ public class Loan extends AbstractPersistable { @Embedded private LoanSummary summary; - @OneToOne(mappedBy = "loan", cascade = CascadeType.ALL, optional = true, orphanRemoval = true, fetch = FetchType.LAZY) - private LoanSummaryArrearsAging summaryArrearsAging; - @Transient private boolean accountNumberRequiresAutoGeneration = false; @Transient @@ -1146,7 +1143,6 @@ public class Loan extends AbstractPersistable { if (isNotDisbursed()) { this.summary.zeroFields(); - this.summaryArrearsAging = null; this.totalOverpaid = null; } else { final Money overpaidBy = calculateTotalOverpayment(); @@ -1158,13 +1154,6 @@ public class Loan extends AbstractPersistable { final Money principal = this.loanRepaymentScheduleDetail.getPrincipal(); this.summary.updateSummary(loanCurrency(), principal, this.repaymentScheduleInstallments, this.loanSummaryWrapper, isDisbursed()); - if (this.summaryArrearsAging == null) { - this.summaryArrearsAging = new LoanSummaryArrearsAging(this); - } - this.summaryArrearsAging.updateSummary(loanCurrency(), this.repaymentScheduleInstallments, this.loanSummaryWrapper); - if (this.summaryArrearsAging.isNotInArrears(loanCurrency())) { - this.summaryArrearsAging = null; - } updateLoanOutstandingBalaces(); } } @@ -3378,7 +3367,7 @@ public class Loan extends AbstractPersistable { return status().isRejected(); } - private boolean isOpen() { + public boolean isOpen() { return status().isActive(); } @@ -5117,4 +5106,5 @@ public class Loan extends AbstractPersistable { return lastTransactionDate == null ? now : lastTransactionDate; } + } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java index 69e3024a2..0a497e550 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java @@ -9,6 +9,7 @@ import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -44,6 +45,7 @@ import org.mifosplatform.portfolio.calendar.domain.CalendarInstanceRepository; import org.mifosplatform.portfolio.calendar.service.CalendarUtils; import org.mifosplatform.portfolio.client.domain.Client; import org.mifosplatform.portfolio.client.exception.ClientNotActiveException; +import org.mifosplatform.portfolio.common.BusinessEventNotificationConstants.BUSINESS_ENTITY; import org.mifosplatform.portfolio.common.BusinessEventNotificationConstants.BUSINESS_EVENTS; import org.mifosplatform.portfolio.common.domain.PeriodFrequencyType; import org.mifosplatform.portfolio.common.service.BusinessEventNotifierService; @@ -126,6 +128,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { final String txnExternalId, final boolean isRecoveryRepayment, boolean isAccountTransfer) { AppUser currentUser = getAppUserIfPresent(); checkClientOrGroupActive(loan); + this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_MAKE_REPAYMENT, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); // TODO: Is it required to validate transaction date with meeting dates // if repayments is synced with meeting? @@ -221,7 +225,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { recalculateAccruals(loan); - this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_MAKE_REPAYMENT, newRepaymentTransaction); + this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_MAKE_REPAYMENT, + constructEntityMap(BUSINESS_ENTITY.LOAN_TRANSACTION, newRepaymentTransaction)); builderResult.withEntityId(newRepaymentTransaction.getId()) // .withOfficeId(loan.getOfficeId()) // @@ -276,7 +281,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { AppUser currentUser = getAppUserIfPresent(); boolean isAccountTransfer = true; checkClientOrGroupActive(loan); - + this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_CHARGE_PAYMENT, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); final List existingTransactionIds = new ArrayList<>(); final List existingReversedTransactionIds = new ArrayList<>(); @@ -311,6 +317,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer); recalculateAccruals(loan); + this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_CHARGE_PAYMENT, + constructEntityMap(BUSINESS_ENTITY.LOAN_TRANSACTION, newPaymentTransaction)); return newPaymentTransaction; } @@ -349,7 +357,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { boolean isAccountTransfer = true; final Loan loan = this.loanAccountAssembler.assembleFrom(accountId); checkClientOrGroupActive(loan); - + this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_REFUND, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); final List existingTransactionIds = new ArrayList<>(); final List existingReversedTransactionIds = new ArrayList<>(); @@ -374,7 +383,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { } postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer); - + this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_REFUND, + constructEntityMap(BUSINESS_ENTITY.LOAN_TRANSACTION, newRefundTransaction)); builderResult.withEntityId(newRefundTransaction.getId()) // .withOfficeId(loan.getOfficeId()) // .withClientId(loan.getClientId()) // @@ -556,7 +566,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, String txnExternalId) { final Loan loan = this.loanAccountAssembler.assembleFrom(accountId); checkClientOrGroupActive(loan); - + this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_REFUND, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); final List existingTransactionIds = new ArrayList(); final List existingReversedTransactionIds = new ArrayList(); AppUser currentUser = getAppUserIfPresent(); @@ -584,6 +595,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, false); recalculateAccruals(loan); + this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_REFUND, + constructEntityMap(BUSINESS_ENTITY.LOAN_TRANSACTION, newRefundTransaction)); builderResult.withEntityId(newRefundTransaction.getId()) // .withOfficeId(loan.getOfficeId()) // @@ -592,4 +605,10 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { return newRefundTransaction; } + + private Map constructEntityMap(final BUSINESS_ENTITY entityEvent, Object entity) { + Map map = new HashMap<>(1); + map.put(entityEvent, entity); + return map; + } } \ No newline at end of file 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 b985f4748..c2cbc4f67 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 @@ -927,4 +927,9 @@ public class LoanCharge extends AbstractPersistable { } return paidChargePerInstallment; } + + + public Loan getLoan() { + return this.loan; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanSummary.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanSummary.java index 463121248..ef9ee710f 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanSummary.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanSummary.java @@ -306,4 +306,29 @@ public final class LoanSummary { public BigDecimal getTotalPrincipalWrittenOff() { return this.totalPrincipalWrittenOff; } + + + public BigDecimal getTotalInterestWaived() { + return this.totalInterestWaived; + } + + + public BigDecimal getTotalFeeChargesRepaid() { + return this.totalFeeChargesRepaid; + } + + + public BigDecimal getTotalFeeChargesWaived() { + return this.totalFeeChargesWaived; + } + + + public BigDecimal getTotalPenaltyChargesRepaid() { + return this.totalPenaltyChargesRepaid; + } + + + public BigDecimal getTotalPenaltyChargesWaived() { + return this.totalPenaltyChargesWaived; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanSummaryArrearsAging.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanSummaryArrearsAging.java deleted file mode 100644 index 8f3e08d7e..000000000 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanSummaryArrearsAging.java +++ /dev/null @@ -1,130 +0,0 @@ -/** - * 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.domain; - -import java.math.BigDecimal; -import java.util.Date; -import java.util.List; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.OneToOne; -import javax.persistence.PrimaryKeyJoinColumn; -import javax.persistence.Table; -import javax.persistence.Temporal; -import javax.persistence.TemporalType; - -import org.joda.time.LocalDate; -import org.mifosplatform.infrastructure.core.service.DateUtils; -import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; -import org.mifosplatform.organisation.monetary.domain.Money; -import org.springframework.data.domain.Persistable; - -/** - *

- * A specialist view of arrears & aging summary details of a {@link Loan}. - *

- * - *

- * This is intentionally split from the loan summary concept and maps to its own - * table that contains only arrears and aging information. The reason is to make - * any overnight batch update of these details simpler and improve performance. - *

- */ -@Entity -@Table(name = "m_loan_arrears_aging") -@org.hibernate.annotations.GenericGenerator(name = "loan-primarykey", strategy = "foreign", parameters = { // -@org.hibernate.annotations.Parameter(name = "property", value = "loan") // -}) -public final class LoanSummaryArrearsAging implements Persistable { - - @Id - @GeneratedValue(generator = "loan-primarykey") - @Column(name = "loan_id", unique = true, nullable = false) - private Long loanId; - - @Column(name = "principal_overdue_derived", scale = 6, precision = 19) - private BigDecimal totalPrincipalOverdue; - - @Column(name = "interest_overdue_derived", scale = 6, precision = 19) - private BigDecimal totalInterestOverdue; - - @Column(name = "fee_charges_overdue_derived", scale = 6, precision = 19) - private BigDecimal totalFeeChargesOverdue; - - @Column(name = "penalty_charges_overdue_derived", scale = 6, precision = 19) - private BigDecimal totalPenaltyChargesOverdue; - - @Column(name = "total_overdue_derived", scale = 6, precision = 19) - private BigDecimal totalOverdue; - - @Temporal(TemporalType.DATE) - @Column(name = "overdue_since_date_derived") - private Date overdueSinceDate; - - @OneToOne(optional = false) - @PrimaryKeyJoinColumn - private Loan loan; - - protected LoanSummaryArrearsAging() { - // - } - - public LoanSummaryArrearsAging(final Loan loan) { - this.loan = loan; - zeroFields(); - } - - @Override - public Long getId() { - return this.loanId; - } - - @Override - public boolean isNew() { - return null == getId(); - } - - public void zeroFields() { - this.totalPrincipalOverdue = BigDecimal.ZERO; - this.totalInterestOverdue = BigDecimal.ZERO; - this.totalFeeChargesOverdue = BigDecimal.ZERO; - this.totalPenaltyChargesOverdue = BigDecimal.ZERO; - this.totalOverdue = BigDecimal.ZERO; - this.overdueSinceDate = null; - } - - public void updateSummary(final MonetaryCurrency currency, final List repaymentScheduleInstallments, - final LoanSummaryWrapper summaryWrapper) { - - this.totalPrincipalOverdue = summaryWrapper.calculateTotalPrincipalOverdueOn(repaymentScheduleInstallments, currency, - DateUtils.getLocalDateOfTenant()).getAmount(); - this.totalInterestOverdue = summaryWrapper.calculateTotalInterestOverdueOn(repaymentScheduleInstallments, currency, - DateUtils.getLocalDateOfTenant()).getAmount(); - this.totalFeeChargesOverdue = summaryWrapper.calculateTotalFeeChargesOverdueOn(repaymentScheduleInstallments, currency, - DateUtils.getLocalDateOfTenant()).getAmount(); - this.totalPenaltyChargesOverdue = summaryWrapper.calculateTotalPenaltyChargesOverdueOn(repaymentScheduleInstallments, currency, - DateUtils.getLocalDateOfTenant()).getAmount(); - - final Money totalOverdue = Money.of(currency, this.totalPrincipalOverdue).plus(this.totalInterestOverdue) - .plus(this.totalFeeChargesOverdue).plus(this.totalPenaltyChargesOverdue); - this.totalOverdue = totalOverdue.getAmount(); - - final LocalDate overdueSinceLocalDate = summaryWrapper.determineOverdueSinceDateFrom(repaymentScheduleInstallments, currency, - DateUtils.getLocalDateOfTenant()); - if (overdueSinceLocalDate != null) { - this.overdueSinceDate = overdueSinceLocalDate.toDate(); - } else { - this.overdueSinceDate = null; - } - } - - public boolean isNotInArrears(final MonetaryCurrency currency) { - return Money.of(currency, this.totalOverdue).isZero(); - } -} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/guarantor/service/GuarantorDomainServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/guarantor/service/GuarantorDomainServiceImpl.java index 552e47df8..00210e740 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/guarantor/service/GuarantorDomainServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/guarantor/service/GuarantorDomainServiceImpl.java @@ -26,6 +26,7 @@ import org.mifosplatform.portfolio.account.data.AccountTransferDTO; import org.mifosplatform.portfolio.account.domain.AccountTransferDetails; import org.mifosplatform.portfolio.account.domain.AccountTransferType; import org.mifosplatform.portfolio.account.service.AccountTransfersWritePlatformService; +import org.mifosplatform.portfolio.common.BusinessEventNotificationConstants.BUSINESS_ENTITY; import org.mifosplatform.portfolio.common.BusinessEventNotificationConstants.BUSINESS_EVENTS; import org.mifosplatform.portfolio.common.service.BusinessEventListner; import org.mifosplatform.portfolio.common.service.BusinessEventNotifierService; @@ -46,7 +47,6 @@ import org.mifosplatform.portfolio.savings.domain.DepositAccountOnHoldTransactio import org.mifosplatform.portfolio.savings.domain.SavingsAccount; import org.mifosplatform.portfolio.savings.exception.InsufficientAccountBalanceException; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.jpa.domain.AbstractPersistable; import org.springframework.stereotype.Service; @Service @@ -83,8 +83,8 @@ public class GuarantorDomainServiceImpl implements GuarantorDomainService { this.businessEventNotifierService.addBusinessEventPostListners(BUSINESS_EVENTS.LOAN_UNDO_APPROVAL, new UndoAllFundTransactions()); this.businessEventNotifierService.addBusinessEventPostListners(BUSINESS_EVENTS.LOAN_UNDO_DISBURSAL, new ReverseAllFundsOnBusinessEvent()); - this.businessEventNotifierService.addBusinessEventPostListners(BUSINESS_EVENTS.LOAN_UNDO_TRANSACTION, - new ReverseFundsOnBusinessEvent()); + this.businessEventNotifierService.addBusinessEventPostListners(BUSINESS_EVENTS.LOAN_ADJUST_TRANSACTION, + new AdjustFundsOnBusinessEvent()); this.businessEventNotifierService.addBusinessEventPostListners(BUSINESS_EVENTS.LOAN_MAKE_REPAYMENT, new ReleaseFundsOnBusinessEvent()); this.businessEventNotifierService.addBusinessEventPostListners(BUSINESS_EVENTS.LOAN_WRITTEN_OFF, new ReleaseAllFunds()); @@ -236,8 +236,9 @@ public class GuarantorDomainServiceImpl implements GuarantorDomainService { releaseLoanIds.put(loanId, guarantorFundingDetails.getId()); try { BigDecimal remainingAmount = guarantorFundingDetails.getAmountRemaining(); - if(loan.getGuaranteeAmount().compareTo(loan.getPrincpal().getAmount()) == 1){ - remainingAmount = remainingAmount.multiply(loan.getPrincpal().getAmount()).divide(loan.getGuaranteeAmount(), roundingMode); + if (loan.getGuaranteeAmount().compareTo(loan.getPrincpal().getAmount()) == 1) { + remainingAmount = remainingAmount.multiply(loan.getPrincpal().getAmount()).divide(loan.getGuaranteeAmount(), + roundingMode); } AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, remainingAmount, fromAccountType, toAccountType, fromAccountId, toAccountId, description, locale, fmt, paymentDetail, fromTransferType, @@ -499,12 +500,13 @@ public class GuarantorDomainServiceImpl implements GuarantorDomainService { private class ValidateOnBusinessEvent implements BusinessEventListner { @Override - public void businessEventToBeExecuted(@SuppressWarnings("unused") AbstractPersistable businessEventEntity) {} + public void businessEventToBeExecuted(@SuppressWarnings("unused") Map businessEventEntity) {} @Override - public void businessEventWasExecuted(AbstractPersistable businessEventEntity) { - if (businessEventEntity instanceof Loan) { - Loan loan = (Loan) businessEventEntity; + public void businessEventWasExecuted(Map businessEventEntity) { + Object entity = businessEventEntity.get(BUSINESS_ENTITY.LOAN); + if (entity instanceof Loan) { + Loan loan = (Loan) entity; validateGuarantorBusinessRules(loan); } } @@ -513,12 +515,13 @@ public class GuarantorDomainServiceImpl implements GuarantorDomainService { private class HoldFundsOnBusinessEvent implements BusinessEventListner { @Override - public void businessEventToBeExecuted(@SuppressWarnings("unused") AbstractPersistable businessEventEntity) {} + public void businessEventToBeExecuted(@SuppressWarnings("unused") Map businessEventEntity) {} @Override - public void businessEventWasExecuted(AbstractPersistable businessEventEntity) { - if (businessEventEntity instanceof Loan) { - Loan loan = (Loan) businessEventEntity; + public void businessEventWasExecuted(Map businessEventEntity) { + Object entity = businessEventEntity.get(BUSINESS_ENTITY.LOAN); + if (entity instanceof Loan) { + Loan loan = (Loan) entity; holdGuarantorFunds(loan); } } @@ -527,12 +530,13 @@ public class GuarantorDomainServiceImpl implements GuarantorDomainService { private class ReleaseFundsOnBusinessEvent implements BusinessEventListner { @Override - public void businessEventToBeExecuted(@SuppressWarnings("unused") AbstractPersistable businessEventEntity) {} + public void businessEventToBeExecuted(@SuppressWarnings("unused") Map businessEventEntity) {} @Override - public void businessEventWasExecuted(AbstractPersistable businessEventEntity) { - if (businessEventEntity instanceof LoanTransaction) { - LoanTransaction loanTransaction = (LoanTransaction) businessEventEntity; + public void businessEventWasExecuted(Map businessEventEntity) { + Object entity = businessEventEntity.get(BUSINESS_ENTITY.LOAN_TRANSACTION); + if (entity instanceof LoanTransaction) { + LoanTransaction loanTransaction = (LoanTransaction) entity; if (releaseLoanIds.containsKey(loanTransaction.getLoan().getId())) { completeGuarantorFund(loanTransaction); } else { @@ -545,12 +549,13 @@ public class GuarantorDomainServiceImpl implements GuarantorDomainService { private class ReverseFundsOnBusinessEvent implements BusinessEventListner { @Override - public void businessEventToBeExecuted(@SuppressWarnings("unused") AbstractPersistable businessEventEntity) {} + public void businessEventToBeExecuted(@SuppressWarnings("unused") Map businessEventEntity) {} @Override - public void businessEventWasExecuted(AbstractPersistable businessEventEntity) { - if (businessEventEntity instanceof LoanTransaction) { - LoanTransaction loanTransaction = (LoanTransaction) businessEventEntity; + public void businessEventWasExecuted(Map businessEventEntity) { + Object entity = businessEventEntity.get(BUSINESS_ENTITY.LOAN_TRANSACTION); + if (entity instanceof LoanTransaction) { + LoanTransaction loanTransaction = (LoanTransaction) entity; List reersedTransactions = new ArrayList<>(1); reersedTransactions.add(loanTransaction.getId()); reverseTransaction(reersedTransactions); @@ -558,15 +563,38 @@ public class GuarantorDomainServiceImpl implements GuarantorDomainService { } } + private class AdjustFundsOnBusinessEvent implements BusinessEventListner { + + @Override + public void businessEventToBeExecuted(@SuppressWarnings("unused") Map businessEventEntity) {} + + @Override + public void businessEventWasExecuted(Map businessEventEntity) { + Object entity = businessEventEntity.get(BUSINESS_ENTITY.LOAN_ADJUSTED_TRANSACTION); + if (entity instanceof LoanTransaction) { + LoanTransaction loanTransaction = (LoanTransaction) entity; + List reersedTransactions = new ArrayList<>(1); + reersedTransactions.add(loanTransaction.getId()); + reverseTransaction(reersedTransactions); + } + Object transactionentity = businessEventEntity.get(BUSINESS_ENTITY.LOAN_TRANSACTION); + if (transactionentity != null && transactionentity instanceof LoanTransaction) { + LoanTransaction loanTransaction = (LoanTransaction) transactionentity; + releaseGuarantorFunds(loanTransaction); + } + } + } + private class ReverseAllFundsOnBusinessEvent implements BusinessEventListner { @Override - public void businessEventToBeExecuted(@SuppressWarnings("unused") AbstractPersistable businessEventEntity) {} + public void businessEventToBeExecuted(@SuppressWarnings("unused") Map businessEventEntity) {} @Override - public void businessEventWasExecuted(AbstractPersistable businessEventEntity) { - if (businessEventEntity instanceof Loan) { - Loan loan = (Loan) businessEventEntity; + public void businessEventWasExecuted(Map businessEventEntity) { + Object entity = businessEventEntity.get(BUSINESS_ENTITY.LOAN); + if (entity instanceof Loan) { + Loan loan = (Loan) entity; List reersedTransactions = new ArrayList<>(1); reersedTransactions.addAll(loan.findExistingTransactionIds()); reverseTransaction(reersedTransactions); @@ -577,12 +605,13 @@ public class GuarantorDomainServiceImpl implements GuarantorDomainService { private class UndoAllFundTransactions implements BusinessEventListner { @Override - public void businessEventToBeExecuted(@SuppressWarnings("unused") AbstractPersistable businessEventEntity) {} + public void businessEventToBeExecuted(@SuppressWarnings("unused") Map businessEventEntity) {} @Override - public void businessEventWasExecuted(AbstractPersistable businessEventEntity) { - if (businessEventEntity instanceof Loan) { - Loan loan = (Loan) businessEventEntity; + public void businessEventWasExecuted(Map businessEventEntity) { + Object entity = businessEventEntity.get(BUSINESS_ENTITY.LOAN); + if (entity instanceof Loan) { + Loan loan = (Loan) entity; reverseAllFundTransaction(loan); } } @@ -591,12 +620,13 @@ public class GuarantorDomainServiceImpl implements GuarantorDomainService { private class ReleaseAllFunds implements BusinessEventListner { @Override - public void businessEventToBeExecuted(@SuppressWarnings("unused") AbstractPersistable businessEventEntity) {} + public void businessEventToBeExecuted(@SuppressWarnings("unused") Map businessEventEntity) {} @Override - public void businessEventWasExecuted(AbstractPersistable businessEventEntity) { - if (businessEventEntity instanceof LoanTransaction) { - LoanTransaction loanTransaction = (LoanTransaction) businessEventEntity; + public void businessEventWasExecuted(Map businessEventEntity) { + Object entity = businessEventEntity.get(BUSINESS_ENTITY.LOAN_TRANSACTION); + if (entity instanceof LoanTransaction) { + LoanTransaction loanTransaction = (LoanTransaction) entity; releaseAllGuarantors(loanTransaction); } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java index e6f06e236..6f96a3c50 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java @@ -19,13 +19,10 @@ public class LoanSchedulePeriodData { private final Integer period; private final LocalDate fromDate; private final LocalDate dueDate; - @SuppressWarnings("unused") private final LocalDate obligationsMetOnDate; - @SuppressWarnings("unused") private final Boolean complete; private final Integer daysInPeriod; private final BigDecimal principalDisbursed; - @SuppressWarnings("unused") private final BigDecimal principalOriginalDue; private final BigDecimal principalDue; private final BigDecimal principalPaid; @@ -52,21 +49,14 @@ public class LoanSchedulePeriodData { @SuppressWarnings("unused") private final BigDecimal totalOriginalDueForPeriod; - @SuppressWarnings("unused") private final BigDecimal totalDueForPeriod; - @SuppressWarnings("unused") private final BigDecimal totalPaidForPeriod; - @SuppressWarnings("unused") private final BigDecimal totalPaidInAdvanceForPeriod; - @SuppressWarnings("unused") private final BigDecimal totalPaidLateForPeriod; - @SuppressWarnings("unused") private final BigDecimal totalWaivedForPeriod; - @SuppressWarnings("unused") private final BigDecimal totalWrittenOffForPeriod; private final BigDecimal totalOutstandingForPeriod; private final BigDecimal totalOverdue; - @SuppressWarnings("unused") private final BigDecimal totalActualCostOfLoanForPeriod; public static LoanSchedulePeriodData disbursementOnlyPeriod(final LocalDate disbursementDate, final BigDecimal principalDisbursed, @@ -108,6 +98,26 @@ public class LoanSchedulePeriodData { totalActualCostOfLoanForPeriod); } + public static LoanSchedulePeriodData WithPaidDetail(final LoanSchedulePeriodData loanSchedulePeriodData, final boolean complete, + final BigDecimal principalPaid, final BigDecimal interestPaid, final BigDecimal feeChargesPaid, + final BigDecimal penaltyChargesPaid) { + + return new LoanSchedulePeriodData(loanSchedulePeriodData.period, loanSchedulePeriodData.fromDate, loanSchedulePeriodData.dueDate, + loanSchedulePeriodData.obligationsMetOnDate, complete, loanSchedulePeriodData.principalOriginalDue, principalPaid, + loanSchedulePeriodData.principalWrittenOff, loanSchedulePeriodData.principalOutstanding, + loanSchedulePeriodData.principalLoanBalanceOutstanding, loanSchedulePeriodData.interestDue, interestPaid, + loanSchedulePeriodData.interestWaived, loanSchedulePeriodData.interestWrittenOff, + loanSchedulePeriodData.interestOutstanding, loanSchedulePeriodData.feeChargesDue, feeChargesPaid, + loanSchedulePeriodData.feeChargesWaived, loanSchedulePeriodData.feeChargesWrittenOff, + loanSchedulePeriodData.feeChargesOutstanding, loanSchedulePeriodData.penaltyChargesDue, penaltyChargesPaid, + loanSchedulePeriodData.penaltyChargesWaived, loanSchedulePeriodData.penaltyChargesWrittenOff, + loanSchedulePeriodData.penaltyChargesOutstanding, loanSchedulePeriodData.totalDueForPeriod, + loanSchedulePeriodData.totalPaidForPeriod, loanSchedulePeriodData.totalPaidInAdvanceForPeriod, + loanSchedulePeriodData.totalPaidLateForPeriod, loanSchedulePeriodData.totalWaivedForPeriod, + loanSchedulePeriodData.totalWrittenOffForPeriod, loanSchedulePeriodData.totalOutstandingForPeriod, + loanSchedulePeriodData.totalActualCostOfLoanForPeriod); + } + /* * constructor used for creating period on loan schedule that is only a * disbursement (typically first period) @@ -417,4 +427,9 @@ public class LoanSchedulePeriodData { public BigDecimal principalLoanBalanceOutstanding() { return this.principalLoanBalanceOutstanding; } + + + public Boolean getComplete() { + return this.complete; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java index e754aedf7..3951be4dd 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java @@ -48,6 +48,7 @@ import org.mifosplatform.portfolio.client.domain.ClientRepositoryWrapper; import org.mifosplatform.portfolio.client.exception.ClientNotActiveException; import org.mifosplatform.portfolio.collateral.domain.LoanCollateral; import org.mifosplatform.portfolio.collateral.service.CollateralAssembler; +import org.mifosplatform.portfolio.common.BusinessEventNotificationConstants.BUSINESS_ENTITY; import org.mifosplatform.portfolio.common.BusinessEventNotificationConstants.BUSINESS_EVENTS; import org.mifosplatform.portfolio.common.service.BusinessEventNotifierService; import org.mifosplatform.portfolio.fund.domain.Fund; @@ -762,7 +763,8 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa this.noteRepository.save(note); } - this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_APPROVED, loan); + this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_APPROVED, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); } return new CommandProcessingResultBuilder() // @@ -806,7 +808,8 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa final Note note = Note.loanNote(loan, noteText); this.noteRepository.save(note); } - this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_UNDO_APPROVAL, loan); + this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_UNDO_APPROVAL, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); } return new CommandProcessingResultBuilder() // @@ -962,4 +965,10 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa return user; } + private Map constructEntityMap(final BUSINESS_ENTITY entityEvent, Object entity) { + Map map = new HashMap<>(1); + map.put(entityEvent, entity); + return map; + } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanArrearsAgingService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanArrearsAgingService.java new file mode 100755 index 000000000..f57bb5971 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanArrearsAgingService.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.service; + +import org.mifosplatform.portfolio.loanaccount.domain.Loan; + +public interface LoanArrearsAgingService { + + void updateLoanArrearsAgeingDetails(); + + void updateLoanArrearsAgeingDetailsWithOriginalSchedule(Loan loan); + + void updateLoanArrearsAgeingDetails(Loan loan); + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanArrearsAgingServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanArrearsAgingServiceImpl.java new file mode 100755 index 000000000..9be637a8f --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanArrearsAgingServiceImpl.java @@ -0,0 +1,503 @@ +/** + * 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.service; + +import java.math.BigDecimal; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.annotation.PostConstruct; + +import org.joda.time.LocalDate; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; +import org.mifosplatform.infrastructure.core.domain.JdbcSupport; +import org.mifosplatform.infrastructure.core.service.RoutingDataSource; +import org.mifosplatform.infrastructure.core.service.ThreadLocalContextUtil; +import org.mifosplatform.infrastructure.jobs.annotation.CronTarget; +import org.mifosplatform.infrastructure.jobs.service.JobName; +import org.mifosplatform.portfolio.common.BusinessEventNotificationConstants.BUSINESS_ENTITY; +import org.mifosplatform.portfolio.common.BusinessEventNotificationConstants.BUSINESS_EVENTS; +import org.mifosplatform.portfolio.common.service.BusinessEventListner; +import org.mifosplatform.portfolio.common.service.BusinessEventNotifierService; +import org.mifosplatform.portfolio.loanaccount.domain.Loan; +import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; +import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; +import org.mifosplatform.portfolio.loanaccount.domain.LoanSummary; +import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction; +import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; +import org.mifosplatform.scheduledjobs.service.ScheduledJobRunnerServiceImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.ResultSetExtractor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class LoanArrearsAgingServiceImpl implements LoanArrearsAgingService, BusinessEventListner { + + private final static Logger logger = LoggerFactory.getLogger(ScheduledJobRunnerServiceImpl.class); + private final BusinessEventNotifierService businessEventNotifierService; + private final DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd"); + private final JdbcTemplate jdbcTemplate; + + @Autowired + public LoanArrearsAgingServiceImpl(final RoutingDataSource dataSource, final BusinessEventNotifierService businessEventNotifierService) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + this.businessEventNotifierService = businessEventNotifierService; + } + + @PostConstruct + public void registerForNotification() { + this.businessEventNotifierService.addBusinessEventPostListners(BUSINESS_EVENTS.LOAN_REFUND, this); + this.businessEventNotifierService.addBusinessEventPostListners(BUSINESS_EVENTS.LOAN_ADJUST_TRANSACTION, this); + this.businessEventNotifierService.addBusinessEventPostListners(BUSINESS_EVENTS.LOAN_MAKE_REPAYMENT, this); + this.businessEventNotifierService.addBusinessEventPostListners(BUSINESS_EVENTS.LOAN_UNDO_WRITTEN_OFF, this); + this.businessEventNotifierService.addBusinessEventPostListners(BUSINESS_EVENTS.LOAN_WAIVE_INTEREST, this); + this.businessEventNotifierService.addBusinessEventPostListners(BUSINESS_EVENTS.LOAN_ADD_CHARGE, this); + this.businessEventNotifierService.addBusinessEventPostListners(BUSINESS_EVENTS.LOAN_WAIVE_CHARGE, this); + this.businessEventNotifierService.addBusinessEventPostListners(BUSINESS_EVENTS.LOAN_CHARGE_PAYMENT, this); + this.businessEventNotifierService.addBusinessEventPostListners(BUSINESS_EVENTS.LOAN_APPLY_OVERDUE_CHARGE, this); + this.businessEventNotifierService.addBusinessEventPostListners(BUSINESS_EVENTS.LOAN_DISBURSAL, new DisbursementEventListner()); + } + + @Transactional + @Override + @CronTarget(jobName = JobName.UPDATE_LOAN_ARREARS_AGEING) + public void updateLoanArrearsAgeingDetails() { + + this.jdbcTemplate.execute("truncate table m_loan_arrears_aging"); + + final StringBuilder updateSqlBuilder = new StringBuilder(900); + + updateSqlBuilder + .append("INSERT INTO m_loan_arrears_aging(`loan_id`,`principal_overdue_derived`,`interest_overdue_derived`,`fee_charges_overdue_derived`,`penalty_charges_overdue_derived`,`total_overdue_derived`,`overdue_since_date_derived`)"); + updateSqlBuilder.append("select ml.id as loanId,"); + updateSqlBuilder + .append("SUM((ifnull(mr.principal_amount,0) - ifnull(mr.principal_completed_derived, 0))) as principal_overdue_derived,"); + updateSqlBuilder + .append("SUM((ifnull(mr.interest_amount,0) - ifnull(mr.interest_completed_derived, 0))) as interest_overdue_derived,"); + updateSqlBuilder + .append("SUM((ifnull(mr.fee_charges_amount,0) - ifnull(mr.fee_charges_completed_derived, 0))) as fee_charges_overdue_derived,"); + updateSqlBuilder + .append("SUM((ifnull(mr.penalty_charges_amount,0) - ifnull(mr.penalty_charges_completed_derived, 0))) as penalty_charges_overdue_derived,"); + updateSqlBuilder.append("SUM((ifnull(mr.principal_amount,0) - ifnull(mr.principal_completed_derived, 0))) +"); + updateSqlBuilder.append("SUM((ifnull(mr.interest_amount,0) - ifnull(mr.interest_completed_derived, 0))) +"); + updateSqlBuilder.append("SUM((ifnull(mr.fee_charges_amount,0) - ifnull(mr.fee_charges_completed_derived, 0))) +"); + updateSqlBuilder + .append("SUM((ifnull(mr.penalty_charges_amount,0) - ifnull(mr.penalty_charges_completed_derived, 0))) as total_overdue_derived,"); + updateSqlBuilder.append("MIN(mr.duedate) as overdue_since_date_derived "); + updateSqlBuilder.append(" FROM m_loan ml "); + updateSqlBuilder.append(" INNER JOIN m_loan_repayment_schedule mr on mr.loan_id = ml.id "); + updateSqlBuilder.append(" left join m_product_loan_recalculation_details prd on prd.product_id = ml.product_id "); + updateSqlBuilder.append(" WHERE ml.loan_status_id = 300 "); // active + updateSqlBuilder.append(" and mr.completed_derived is false "); + updateSqlBuilder.append(" and mr.duedate < SUBDATE(CURDATE(),INTERVAL ifnull(ml.grace_on_arrears_ageing,0) day) "); + updateSqlBuilder.append(" and (prd.arrears_based_on_original_schedule = 0 or prd.arrears_based_on_original_schedule is null) "); + updateSqlBuilder.append(" GROUP BY ml.id"); + + List insertStatements = updateLoanArrearsAgeingDetailsWithOriginalSchedule(); + insertStatements.add(0, updateSqlBuilder.toString()); + final int[] results = this.jdbcTemplate.batchUpdate(insertStatements.toArray(new String[0])); + int result = 0; + for (int i : results) { + result += i; + } + + logger.info(ThreadLocalContextUtil.getTenant().getName() + ": Results affected by update: " + result); + } + + @Override + public void updateLoanArrearsAgeingDetailsWithOriginalSchedule(final Loan loan) { + int count = this.jdbcTemplate.queryForObject("select count(mla.loan_id) from m_loan_arrears_aging mla where mla.loan_id =?", + Integer.class, loan.getId()); + List updateStatement = new ArrayList<>(); + OriginalScheduleExtractor originalScheduleExtractor = new OriginalScheduleExtractor(loan.getId().toString()); + Map> scheduleDate = this.jdbcTemplate.query(originalScheduleExtractor.schema, + originalScheduleExtractor); + if (scheduleDate.size() > 0) { + List> transactions = getLoanSummary(loan.getId(), loan.getLoanSummary()); + updateSchheduleWithPaidDetail(scheduleDate, transactions); + createInsertStatements(updateStatement, scheduleDate, count == 0); + if (updateStatement.size() == 1) { + this.jdbcTemplate.update(updateStatement.get(0)); + } else { + String deletestatement = "DELETE FROM `m_loan_arrears_aging` WHERE `loan_id`=" + loan.getId(); + this.jdbcTemplate.update(deletestatement); + } + } + } + + @Override + public void updateLoanArrearsAgeingDetails(final Loan loan) { + int count = this.jdbcTemplate.queryForObject("select count(mla.loan_id) from m_loan_arrears_aging mla where mla.loan_id =?", + Integer.class, loan.getId()); + String updateStatement = constructUpdateStatement(loan, count == 0); + if (updateStatement == null) { + String deletestatement = "DELETE FROM `m_loan_arrears_aging` WHERE `loan_id`=" + loan.getId(); + this.jdbcTemplate.update(deletestatement); + } else { + this.jdbcTemplate.update(updateStatement); + } + } + + private String constructUpdateStatement(final Loan loan, boolean isInsertStatement) { + String updateSql = null; + List installments = loan.getRepaymentScheduleInstallments(); + BigDecimal principalOverdue = BigDecimal.ZERO; + BigDecimal interestOverdue = BigDecimal.ZERO; + BigDecimal feeOverdue = BigDecimal.ZERO; + BigDecimal penaltyOverdue = BigDecimal.ZERO; + LocalDate overDueSince = LocalDate.now(); + for (LoanRepaymentScheduleInstallment installment : installments) { + if (installment.getDueDate().isBefore(LocalDate.now())) { + principalOverdue = principalOverdue.add(installment.getPrincipalOutstanding(loan.getCurrency()).getAmount()); + interestOverdue = interestOverdue.add(installment.getInterestOutstanding(loan.getCurrency()).getAmount()); + feeOverdue = feeOverdue.add(installment.getFeeChargesOutstanding(loan.getCurrency()).getAmount()); + penaltyOverdue = penaltyOverdue.add(installment.getPenaltyChargesOutstanding(loan.getCurrency()).getAmount()); + if (installment.isNotFullyPaidOff() && overDueSince.isAfter(installment.getDueDate())) { + overDueSince = installment.getDueDate(); + } + } + } + + BigDecimal totalOverDue = principalOverdue.add(interestOverdue).add(feeOverdue).add(penaltyOverdue); + if (totalOverDue.compareTo(BigDecimal.ZERO) == 1) { + if (isInsertStatement) { + updateSql = constructInsertStatement(loan.getId(), principalOverdue, interestOverdue, feeOverdue, penaltyOverdue, + overDueSince); + } else { + updateSql = constructUpdateStatement(loan.getId(), principalOverdue, interestOverdue, feeOverdue, penaltyOverdue, + overDueSince); + } + } + return updateSql; + } + + private List updateLoanArrearsAgeingDetailsWithOriginalSchedule() { + List insertStatement = new ArrayList<>(); + + final StringBuilder loanIdentifier = new StringBuilder(); + loanIdentifier.append("select ml.id as loanId FROM m_loan ml "); + loanIdentifier.append("INNER JOIN m_loan_repayment_schedule mr on mr.loan_id = ml.id "); + loanIdentifier + .append("inner join m_product_loan_recalculation_details prd on prd.product_id = ml.product_id and prd.arrears_based_on_original_schedule = 1 "); + loanIdentifier + .append("WHERE ml.loan_status_id = 300 and mr.completed_derived is false and mr.duedate < SUBDATE(CURDATE(),INTERVAL ifnull(ml.grace_on_arrears_ageing,0) day) group by ml.id"); + List loanIds = this.jdbcTemplate.queryForList(loanIdentifier.toString(), Long.class); + if (!loanIds.isEmpty()) { + String loanIdsAsString = loanIds.toString(); + loanIdsAsString = loanIdsAsString.substring(1, loanIdsAsString.length() - 1); + OriginalScheduleExtractor originalScheduleExtractor = new OriginalScheduleExtractor(loanIdsAsString); + Map> scheduleDate = this.jdbcTemplate.query(originalScheduleExtractor.schema, + originalScheduleExtractor); + + List> loanSummary = getLoanSummary(loanIdsAsString); + updateSchheduleWithPaidDetail(scheduleDate, loanSummary); + createInsertStatements(insertStatement, scheduleDate, true); + } + + return insertStatement; + + } + + private List> getLoanSummary(final String loanIdsAsString) { + final StringBuilder transactionsSql = new StringBuilder(); + transactionsSql.append("select ml.id as loanId, "); + transactionsSql + .append("ml.principal_repaid_derived as principalAmtPaid, ml.principal_writtenoff_derived as principalAmtWrittenoff, "); + transactionsSql.append(" ml.interest_repaid_derived as interestAmtPaid, ml.interest_waived_derived as interestAmtWaived, "); + transactionsSql.append("ml.fee_charges_repaid_derived as feeAmtPaid, ml.fee_charges_waived_derived as feeAmtWaived, "); + transactionsSql + .append("ml.penalty_charges_repaid_derived as penaltyAmtPaid, ml.penalty_charges_waived_derived as penaltyAmtWaived "); + transactionsSql.append("from m_loan ml "); + transactionsSql.append("where ml.id IN (").append(loanIdsAsString).append(") order by ml.id"); + + List> loanSummary = this.jdbcTemplate.queryForList(transactionsSql.toString()); + return loanSummary; + } + + private List> getLoanSummary(final Long loanId, final LoanSummary loanSummary) { + List> transactionDetail = new ArrayList<>(); + Map transactionMap = new HashMap<>(); + + transactionMap.put("loanId", loanId); + transactionMap.put("principalAmtPaid", loanSummary.getTotalPrincipalRepaid()); + transactionMap.put("principalAmtWrittenoff", loanSummary.getTotalPrincipalWrittenOff()); + transactionMap.put("interestAmtPaid", loanSummary.getTotalInterestRepaid()); + transactionMap.put("interestAmtWaived", loanSummary.getTotalInterestWaived()); + transactionMap.put("feeAmtPaid", loanSummary.getTotalFeeChargesRepaid()); + transactionMap.put("feeAmtWaived", loanSummary.getTotalFeeChargesWaived()); + transactionMap.put("penaltyAmtPaid", loanSummary.getTotalPenaltyChargesRepaid()); + transactionMap.put("penaltyAmtWaived", loanSummary.getTotalPenaltyChargesWaived()); + transactionDetail.add(transactionMap); + return transactionDetail; + + } + + private void createInsertStatements(List insertStatement, Map> scheduleDate, + boolean isInsertStatement) { + for (Map.Entry> entry : scheduleDate.entrySet()) { + final Long loanId = entry.getKey(); + BigDecimal principalOverdue = BigDecimal.ZERO; + BigDecimal interestOverdue = BigDecimal.ZERO; + BigDecimal feeOverdue = BigDecimal.ZERO; + BigDecimal penaltyOverdue = BigDecimal.ZERO; + LocalDate overDueSince = LocalDate.now(); + + for (LoanSchedulePeriodData loanSchedulePeriodData : entry.getValue()) { + if (!loanSchedulePeriodData.getComplete()) { + principalOverdue = principalOverdue.add(loanSchedulePeriodData.principalDue().subtract( + loanSchedulePeriodData.principalPaid())); + interestOverdue = interestOverdue.add(loanSchedulePeriodData.interestDue().subtract( + loanSchedulePeriodData.interestPaid())); + feeOverdue = feeOverdue.add(loanSchedulePeriodData.feeChargesDue().subtract(loanSchedulePeriodData.feeChargesPaid())); + penaltyOverdue = penaltyOverdue.add(loanSchedulePeriodData.penaltyChargesDue().subtract( + loanSchedulePeriodData.penaltyChargesPaid())); + if (overDueSince.isAfter(loanSchedulePeriodData.periodDueDate()) + && loanSchedulePeriodData.principalDue().subtract(loanSchedulePeriodData.principalPaid()) + .compareTo(BigDecimal.ZERO) == 1) { + overDueSince = loanSchedulePeriodData.periodDueDate(); + } + } + } + if (principalOverdue.compareTo(BigDecimal.ZERO) == 1) { + String sqlStatement = null; + if (isInsertStatement) { + sqlStatement = constructInsertStatement(loanId, principalOverdue, interestOverdue, feeOverdue, penaltyOverdue, + overDueSince); + } else { + sqlStatement = constructUpdateStatement(loanId, principalOverdue, interestOverdue, feeOverdue, penaltyOverdue, + overDueSince); + } + insertStatement.add(sqlStatement); + } + + } + } + + private String constructInsertStatement(final Long loanId, BigDecimal principalOverdue, BigDecimal interestOverdue, + BigDecimal feeOverdue, BigDecimal penaltyOverdue, LocalDate overDueSince) { + final StringBuilder insertStatementBuilder = new StringBuilder(900); + insertStatementBuilder + .append("INSERT INTO m_loan_arrears_aging(`loan_id`,`principal_overdue_derived`,`interest_overdue_derived`,") + .append("`fee_charges_overdue_derived`,`penalty_charges_overdue_derived`,`total_overdue_derived`,`overdue_since_date_derived`) VALUES("); + insertStatementBuilder.append(loanId).append(","); + insertStatementBuilder.append(principalOverdue).append(","); + insertStatementBuilder.append(interestOverdue).append(","); + insertStatementBuilder.append(feeOverdue).append(","); + insertStatementBuilder.append(penaltyOverdue).append(","); + BigDecimal totalOverDue = principalOverdue.add(interestOverdue).add(feeOverdue).add(penaltyOverdue); + insertStatementBuilder.append(totalOverDue).append(",'"); + insertStatementBuilder.append(this.formatter.print(overDueSince)).append("')"); + return insertStatementBuilder.toString(); + } + + private String constructUpdateStatement(final Long loanId, BigDecimal principalOverdue, BigDecimal interestOverdue, + BigDecimal feeOverdue, BigDecimal penaltyOverdue, LocalDate overDueSince) { + final StringBuilder insertStatementBuilder = new StringBuilder(900); + insertStatementBuilder.append("UPDATE m_loan_arrears_aging mla SET mla.principal_overdue_derived="); + insertStatementBuilder.append(principalOverdue).append(", mla.interest_overdue_derived="); + insertStatementBuilder.append(interestOverdue).append(", mla.fee_charges_overdue_derived="); + insertStatementBuilder.append(feeOverdue).append(", mla.penalty_charges_overdue_derived="); + insertStatementBuilder.append(penaltyOverdue).append(", mla.total_overdue_derived="); + BigDecimal totalOverDue = principalOverdue.add(interestOverdue).add(feeOverdue).add(penaltyOverdue); + insertStatementBuilder.append(totalOverDue).append(",mla.overdue_since_date_derived= '"); + insertStatementBuilder.append(this.formatter.print(overDueSince)).append("' "); + insertStatementBuilder.append("WHERE mla.loan_id=").append(loanId); + return insertStatementBuilder.toString(); + } + + private void updateSchheduleWithPaidDetail(Map> scheduleDate, List> loanSummary) { + for (Map transactionMap : loanSummary) { + Long loanId = (Long) transactionMap.get("loanId"); + + BigDecimal principalAmtPaid = (BigDecimal) transactionMap.get("principalAmtPaid"); + BigDecimal principalAmtWrittenoff = (BigDecimal) transactionMap.get("principalAmtWrittenoff"); + BigDecimal interestAmtPaid = (BigDecimal) transactionMap.get("interestAmtPaid"); + BigDecimal interestAmtWaived = (BigDecimal) transactionMap.get("interestAmtWaived"); + BigDecimal feeAmtPaid = (BigDecimal) transactionMap.get("feeAmtPaid"); + BigDecimal feeAmtWaived = (BigDecimal) transactionMap.get("feeAmtWaived"); + BigDecimal penaltyAmtPaid = (BigDecimal) transactionMap.get("penaltyAmtPaid"); + BigDecimal penaltyAmtWaived = (BigDecimal) transactionMap.get("penaltyAmtWaived"); + + BigDecimal principalAmt = principalAmtPaid.add(principalAmtWrittenoff); + BigDecimal interestAmt = interestAmtPaid.add(interestAmtWaived); + BigDecimal feeAmt = feeAmtPaid.add(feeAmtWaived); + BigDecimal penaltyAmt = penaltyAmtPaid.add(penaltyAmtWaived); + + List loanSchedulePeriodDatas = scheduleDate.get(loanId); + if (loanSchedulePeriodDatas != null) { + List updatedPeriodData = new ArrayList<>(loanSchedulePeriodDatas.size()); + for (LoanSchedulePeriodData loanSchedulePeriodData : loanSchedulePeriodDatas) { + BigDecimal principalPaid = null; + BigDecimal interestPaid = null; + BigDecimal feeChargesPaid = null; + BigDecimal penaltyChargesPaid = null; + Boolean isComplete = true; + if (loanSchedulePeriodData.principalDue().compareTo(principalAmt) == 1) { + principalPaid = principalAmt; + principalAmt = BigDecimal.ZERO; + isComplete = false; + } else { + principalPaid = loanSchedulePeriodData.principalDue(); + principalAmt = principalAmt.subtract(loanSchedulePeriodData.principalDue()); + } + + if (loanSchedulePeriodData.interestDue().compareTo(interestAmt) == 1) { + interestPaid = interestAmt; + interestAmt = BigDecimal.ZERO; + isComplete = false; + } else { + interestPaid = loanSchedulePeriodData.interestDue(); + interestAmt = interestAmt.subtract(loanSchedulePeriodData.interestDue()); + } + if (loanSchedulePeriodData.feeChargesDue().compareTo(feeAmt) == 1) { + feeChargesPaid = feeAmt; + feeAmt = BigDecimal.ZERO; + isComplete = false; + } else { + feeChargesPaid = loanSchedulePeriodData.feeChargesDue(); + feeAmt = feeAmt.subtract(loanSchedulePeriodData.feeChargesDue()); + } + if (loanSchedulePeriodData.penaltyChargesDue().compareTo(penaltyAmt) == 1) { + penaltyChargesPaid = penaltyAmt; + penaltyAmt = BigDecimal.ZERO; + isComplete = false; + } else { + penaltyChargesPaid = loanSchedulePeriodData.penaltyChargesDue(); + penaltyAmt = penaltyAmt.subtract(loanSchedulePeriodData.penaltyChargesDue()); + } + + LoanSchedulePeriodData periodData = LoanSchedulePeriodData.WithPaidDetail(loanSchedulePeriodData, isComplete, + principalPaid, interestPaid, feeChargesPaid, penaltyChargesPaid); + updatedPeriodData.add(periodData); + } + loanSchedulePeriodDatas.clear(); + loanSchedulePeriodDatas.addAll(updatedPeriodData); + } + } + } + + private static final class OriginalScheduleExtractor implements ResultSetExtractor>> { + + private final String schema; + + public OriginalScheduleExtractor(final String loanIdsAsString) { + final StringBuilder scheduleDetail = new StringBuilder(); + scheduleDetail.append("select ml.id as loanId, mr.duedate as dueDate, mr.principal_amount as principalAmount, "); + scheduleDetail + .append("mr.interest_amount as interestAmount, mr.fee_charges_amount as feeAmount, mr.penalty_charges_amount as penaltyAmount "); + scheduleDetail.append("from m_loan ml INNER JOIN m_loan_repayment_schedule_history mr on mr.loan_id = ml.id "); + scheduleDetail.append("where mr.duedate < SUBDATE(CURDATE(),INTERVAL ifnull(ml.grace_on_arrears_ageing,0) day) and "); + scheduleDetail.append("ml.id IN(").append(loanIdsAsString).append(") and mr.version = ("); + scheduleDetail.append("select max(lrs.version) from m_loan_repayment_schedule_history lrs where mr.loan_id = lrs.loan_id"); + scheduleDetail.append(") order by ml.id,mr.duedate"); + this.schema = scheduleDetail.toString(); + } + + @Override + public Map> extractData(ResultSet rs) throws SQLException, DataAccessException { + Map> scheduleDate = new HashMap<>(); + + while (rs.next()) { + Long loanId = rs.getLong("loanId"); + List periodDatas = new ArrayList<>(); + LoanSchedulePeriodData loanSchedulePeriodData = fetchLoanSchedulePeriodData(rs); + periodDatas.add(loanSchedulePeriodData); + while (rs.next()) { + Long tempLoanId = rs.getLong("loanId"); + if (loanId.equals(tempLoanId)) { + periodDatas.add(fetchLoanSchedulePeriodData(rs)); + } else { + rs.previous(); + break; + } + } + scheduleDate.put(loanId, periodDatas); + } + + return scheduleDate; + } + + private LoanSchedulePeriodData fetchLoanSchedulePeriodData(ResultSet rs) throws SQLException { + final LocalDate dueDate = JdbcSupport.getLocalDate(rs, "dueDate"); + final BigDecimal principalDue = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalAmount"); + final BigDecimal interestDueOnPrincipalOutstanding = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "interestAmount"); + final BigDecimal feeChargesDueForPeriod = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feeAmount"); + final BigDecimal penaltyChargesDueForPeriod = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penaltyAmount"); + final Integer periodNumber = null; + final LocalDate fromDate = null; + final BigDecimal principalOutstanding = null; + final BigDecimal totalDueForPeriod = null; + return LoanSchedulePeriodData.repaymentOnlyPeriod(periodNumber, fromDate, dueDate, principalDue, principalOutstanding, + interestDueOnPrincipalOutstanding, feeChargesDueForPeriod, penaltyChargesDueForPeriod, totalDueForPeriod); + + } + } + + @SuppressWarnings("unused") + @Override + public void businessEventToBeExecuted(Map businessEventEntity) { + // TODO Auto-generated method stub + + } + + @Override + public void businessEventWasExecuted(Map businessEventEntity) { + Loan loan = null; + Object loanEntity = businessEventEntity.get(BUSINESS_ENTITY.LOAN); + Object loanTransactionEntity = businessEventEntity.get(BUSINESS_ENTITY.LOAN_TRANSACTION); + Object loanAdjustTransactionEntity = businessEventEntity.get(BUSINESS_ENTITY.LOAN_ADJUSTED_TRANSACTION); + Object loanChargeEntity = businessEventEntity.get(BUSINESS_ENTITY.LOAN_CHARGE); + if (loanEntity != null) { + loan = (Loan) loanEntity; + } else if (loanTransactionEntity != null) { + LoanTransaction loanTransaction = (LoanTransaction) loanTransactionEntity; + loan = loanTransaction.getLoan(); + } else if (loanAdjustTransactionEntity != null) { + LoanTransaction loanTransaction = (LoanTransaction) loanAdjustTransactionEntity; + loan = loanTransaction.getLoan(); + } else if (loanChargeEntity != null) { + LoanCharge loanCharge = (LoanCharge) loanChargeEntity; + loan = loanCharge.getLoan(); + } + if (loan != null && loan.isOpen() && loan.repaymentScheduleDetail().isInterestRecalculationEnabled() + && loan.loanProduct().isArrearsBasedOnOriginalSchedule()) { + updateLoanArrearsAgeingDetailsWithOriginalSchedule(loan); + } else { + updateLoanArrearsAgeingDetails(loan); + } + } + + private class DisbursementEventListner implements BusinessEventListner { + + @SuppressWarnings("unused") + @Override + public void businessEventToBeExecuted(Map businessEventEntity) { + // TODO Auto-generated method stub + + } + + @Override + public void businessEventWasExecuted(Map businessEventEntity) { + Object loanEntity = businessEventEntity.get(BUSINESS_ENTITY.LOAN); + if (loanEntity != null) { + Loan loan = (Loan) loanEntity; + updateLoanArrearsAgeingDetails(loan); + } + + } + + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanAssembler.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanAssembler.java index 05b1b3652..20fb4c1ce 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanAssembler.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanAssembler.java @@ -159,8 +159,9 @@ public class LoanAssembler { final Long transactionProcessingStrategyId = this.fromApiJsonHelper.extractLongNamed("transactionProcessingStrategyId", element); final Long loanPurposeId = this.fromApiJsonHelper.extractLongNamed("loanPurposeId", element); final Boolean syncDisbursementWithMeeting = this.fromApiJsonHelper.extractBooleanNamed("syncDisbursementWithMeeting", element); - final Boolean createStandingInstructionAtDisbursement = this.fromApiJsonHelper.extractBooleanNamed("createStandingInstructionAtDisbursement", element); - + final Boolean createStandingInstructionAtDisbursement = this.fromApiJsonHelper.extractBooleanNamed( + "createStandingInstructionAtDisbursement", element); + final LoanProduct loanProduct = this.loanProductRepository.findOne(productId); if (loanProduct == null) { throw new LoanProductNotFoundException(productId); } @@ -234,14 +235,14 @@ public class LoanAssembler { loanApplication = Loan.newIndividualLoanApplicationFromGroup(accountNo, client, group, loanType.getId().intValue(), loanProduct, fund, loanOfficer, loanPurpose, loanTransactionProcessingStrategy, loanProductRelatedDetail, loanCharges, - collateral, syncDisbursementWithMeeting, fixedEmiAmount, disbursementDetails, maxOutstandingLoanBalance, + collateral, syncDisbursementWithMeeting, fixedEmiAmount, disbursementDetails, maxOutstandingLoanBalance, createStandingInstructionAtDisbursement); } else if (group != null) { loanApplication = Loan.newGroupLoanApplication(accountNo, group, loanType.getId().intValue(), loanProduct, fund, loanOfficer, loanPurpose, loanTransactionProcessingStrategy, loanProductRelatedDetail, loanCharges, collateral, - syncDisbursementWithMeeting, fixedEmiAmount, disbursementDetails, maxOutstandingLoanBalance, + syncDisbursementWithMeeting, fixedEmiAmount, disbursementDetails, maxOutstandingLoanBalance, createStandingInstructionAtDisbursement); } else if (client != null) { @@ -264,8 +265,9 @@ public class LoanAssembler { loanDisbursementDetails.updateLoan(loanApplication); } } - - final LocalDate recalculationRestFrequencyDate = this.fromApiJsonHelper.extractLocalDateNamed(LoanProductConstants.recalculationRestFrequencyDateParamName, element); + + final LocalDate recalculationRestFrequencyDate = this.fromApiJsonHelper.extractLocalDateNamed( + LoanProductConstants.recalculationRestFrequencyDateParamName, element); final LoanApplicationTerms loanApplicationTerms = this.loanScheduleAssembler.assembleLoanTerms(element); final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled(); 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 17e834bfd..9fe8ebdca 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 @@ -92,6 +92,7 @@ import org.mifosplatform.portfolio.collectionsheet.command.CollectionSheetBulkDi import org.mifosplatform.portfolio.collectionsheet.command.CollectionSheetBulkRepaymentCommand; import org.mifosplatform.portfolio.collectionsheet.command.SingleDisbursalCommand; import org.mifosplatform.portfolio.collectionsheet.command.SingleRepaymentCommand; +import org.mifosplatform.portfolio.common.BusinessEventNotificationConstants.BUSINESS_ENTITY; import org.mifosplatform.portfolio.common.BusinessEventNotificationConstants.BUSINESS_EVENTS; import org.mifosplatform.portfolio.common.domain.PeriodFrequencyType; import org.mifosplatform.portfolio.common.service.BusinessEventNotifierService; @@ -289,6 +290,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf this.loanEventApiJsonValidator.validateDisbursementDateWithMeetingDate(actualDisbursementDate, calendarInstance); } + this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_DISBURSAL, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); + final MonetaryCurrency currency = loan.getCurrency(); final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency); @@ -403,6 +407,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf updateRecurringCalendarDatesForInterestRecalculation(loan); this.loanAccountDomainService.recalculateAccruals(loan); + this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_DISBURSAL, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // .withEntityId(loan.getId()) // @@ -538,6 +544,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final Loan loan = this.loanAssembler.assembleFrom(singleLoanDisbursalCommand.getLoanId()); checkClientOrGroupActive(loan); + this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_DISBURSAL, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); final MonetaryCurrency currency = loan.getCurrency(); final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency); @@ -646,6 +654,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } updateRecurringCalendarDatesForInterestRecalculation(loan); this.loanAccountDomainService.recalculateAccruals(loan); + this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_DISBURSAL, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); } return changes; @@ -659,6 +669,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final Loan loan = this.loanAssembler.assembleFrom(loanId); checkClientOrGroupActive(loan); + this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_UNDO_DISBURSAL, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); removeLoanCycle(loan); // @@ -701,7 +713,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final Map accountingBridgeData = loan.deriveAccountingBridgeData(applicationCurrency.toData(), existingTransactionIds, existingReversedTransactionIds, isAccountTransfer); this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData); - this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_UNDO_DISBURSAL, loan); + this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_UNDO_DISBURSAL, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); } return new CommandProcessingResultBuilder() // @@ -788,10 +801,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final Loan loan = this.loanAssembler.assembleFrom(loanId); checkClientOrGroupActive(loan); - final LoanTransaction transactionToAdjust = this.loanTransactionRepository.findOne(transactionId); if (transactionToAdjust == null) { throw new LoanTransactionNotFoundException(transactionId); } - + this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_ADJUST_TRANSACTION, + constructEntityMap(BUSINESS_ENTITY.LOAN_ADJUSTED_TRANSACTION, transactionToAdjust)); if (this.accountTransfersReadPlatformService.isAccountTransfer(transactionId, PortfolioAccountType.LOAN)) { throw new PlatformServiceUnavailableException( "error.msg.loan.transfer.transaction.update.not.allowed", "Loan transaction:" + transactionId + " update not allowed as it involves in account transfer", transactionId); } @@ -913,10 +926,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); this.loanAccountDomainService.recalculateAccruals(loan); - this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_UNDO_TRANSACTION, transactionToAdjust); + Map entityMap = constructEntityMap(BUSINESS_ENTITY.LOAN_ADJUSTED_TRANSACTION, transactionToAdjust); if (newTransactionDetail.isRepayment() && newTransactionDetail.isGreaterThanZero(loan.getPrincpal().getCurrency())) { - this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_MAKE_REPAYMENT, newTransactionDetail); + entityMap.put(BUSINESS_ENTITY.LOAN_TRANSACTION, newTransactionDetail); } + this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_ADJUST_TRANSACTION, entityMap); return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // @@ -963,7 +977,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } final LoanTransaction waiveInterestTransaction = LoanTransaction.waiver(loan.getOffice(), loan, transactionAmountAsMoney, transactionDate, interestComponent, unrecognizedIncome, DateUtils.getLocalDateTimeOfTenant(), currentUser); - + this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_WAIVE_INTEREST, + constructEntityMap(BUSINESS_ENTITY.LOAN_TRANSACTION, waiveInterestTransaction)); CalendarInstance restCalendarInstance = null; ApplicationCurrency applicationCurrency = null; LocalDate calculatedRepaymentsStartingFromDate = null; @@ -1026,6 +1041,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); this.loanAccountDomainService.recalculateAccruals(loan); + this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_WAIVE_INTEREST, + constructEntityMap(BUSINESS_ENTITY.LOAN_TRANSACTION, waiveInterestTransaction)); return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // .withEntityId(waiveInterestTransaction.getId()) // @@ -1051,6 +1068,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final Loan loan = this.loanAssembler.assembleFrom(loanId); checkClientOrGroupActive(loan); + this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_WRITTEN_OFF, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); removeLoanCycle(loan); final List existingTransactionIds = new ArrayList<>(); @@ -1101,7 +1120,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); this.loanAccountDomainService.recalculateAccruals(loan); - this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_WRITTEN_OFF, writeoff); + this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_WRITTEN_OFF, + constructEntityMap(BUSINESS_ENTITY.LOAN_TRANSACTION, writeoff)); return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // .withEntityId(writeoff.getId()) // @@ -1123,6 +1143,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final Loan loan = this.loanAssembler.assembleFrom(loanId); checkClientOrGroupActive(loan); + this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_CLOSE, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); final Map changes = new LinkedHashMap<>(); changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate")); @@ -1183,6 +1205,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); } this.loanAccountDomainService.recalculateAccruals(loan); + this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_CLOSE, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); CommandProcessingResult result = null; if (possibleClosingTransaction != null) { @@ -1219,6 +1243,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final Loan loan = this.loanAssembler.assembleFrom(loanId); checkClientOrGroupActive(loan); removeLoanCycle(loan); + this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_CLOSE_AS_RESCHEDULE, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); final Map changes = new LinkedHashMap<>(); changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate")); @@ -1235,7 +1261,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final Note note = Note.loanNote(loan, noteText); this.noteRepository.save(note); } - + this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_CLOSE_AS_RESCHEDULE, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // .withEntityId(loanId) // @@ -1260,6 +1287,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final Charge chargeDefinition = this.chargeRepository.findOneWithNotFoundDetection(chargeDefinitionId); final LoanCharge loanCharge = LoanCharge.createNewFromJson(loan, chargeDefinition, command); + this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_ADD_CHARGE, + constructEntityMap(BUSINESS_ENTITY.LOAN_CHARGE, loanCharge)); validateAddLoanCharge(loan, chargeDefinition, loanCharge); @@ -1282,6 +1311,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf && loan.isFeeCompoundingEnabledForInterestRecalculation()) { this.loanAccountDomainService.recalculateAccruals(loan); } + this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_ADD_CHARGE, + constructEntityMap(BUSINESS_ENTITY.LOAN_CHARGE, loanCharge)); return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // .withEntityId(loanCharge.getId()) // @@ -1454,10 +1485,14 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf if (!loan.status().isSubmittedAndPendingApproval()) { throw new LoanChargeCannotBeUpdatedException( LOAN_CHARGE_CANNOT_BE_UPDATED_REASON.LOAN_NOT_IN_SUBMITTED_AND_PENDING_APPROVAL_STAGE, loanCharge.getId()); } + this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_UPDATE_CHARGE, + constructEntityMap(BUSINESS_ENTITY.LOAN_CHARGE, loanCharge)); + final Map changes = loan.updateLoanCharge(loanCharge, command); saveLoanWithDataIntegrityViolationChecks(loan); - + this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_UPDATE_CHARGE, + constructEntityMap(BUSINESS_ENTITY.LOAN_CHARGE, loanCharge)); return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // .withEntityId(loanChargeId) // @@ -1490,6 +1525,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf throw new LoanChargeCannotBeWaivedException(LOAN_CHARGE_CANNOT_BE_WAIVED_REASON.ALREADY_WAIVED, loanCharge.getId()); } else if (loanCharge.isPaid()) { throw new LoanChargeCannotBeWaivedException(LOAN_CHARGE_CANNOT_BE_WAIVED_REASON.ALREADY_PAID, loanCharge.getId()); } + this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_WAIVE_CHARGE, + constructEntityMap(BUSINESS_ENTITY.LOAN_CHARGE, loanCharge)); Integer loanInstallmentNumber = null; if (loanCharge.isInstalmentFee()) { LoanInstallmentCharge chargePerInstallment = null; @@ -1565,6 +1602,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); + this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_WAIVE_CHARGE, + constructEntityMap(BUSINESS_ENTITY.LOAN_CHARGE, loanCharge)); + return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // .withEntityId(loanChargeId) // @@ -1588,10 +1628,13 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf // yet to be approved (are in submitted and pending status) if (!loan.status().isSubmittedAndPendingApproval()) { throw new LoanChargeCannotBeDeletedException( LOAN_CHARGE_CANNOT_BE_DELETED_REASON.LOAN_NOT_IN_SUBMITTED_AND_PENDING_APPROVAL_STAGE, loanCharge.getId()); } + this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_DELETE_CHARGE, + constructEntityMap(BUSINESS_ENTITY.LOAN_CHARGE, loanCharge)); loan.removeLoanCharge(loanCharge); saveLoanWithDataIntegrityViolationChecks(loan); - + this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_DELETE_CHARGE, + constructEntityMap(BUSINESS_ENTITY.LOAN_CHARGE, loanCharge)); return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // .withEntityId(loanChargeId) // @@ -1669,7 +1712,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf AccountTransferType.CHARGE_PAYMENT.getValue(), null, null, null, null, null, fromSavingsAccount, isRegularTransaction, isExceptionForBalanceCheck); this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); - return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // .withEntityId(loanChargeId) // @@ -1786,6 +1828,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final Loan loan = this.loanAssembler.assembleFrom(accountId); checkClientOrGroupActive(loan); + this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_INITIATE_TRANSFER, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); final List existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds()); final List existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds()); @@ -1799,7 +1843,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf saveLoanWithDataIntegrityViolationChecks(loan); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); - + this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_INITIATE_TRANSFER, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); return newTransferTransaction; } @@ -1811,7 +1856,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf AppUser currentUser = getAppUserIfPresent(); final Loan loan = this.loanAssembler.assembleFrom(accountId); - + this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_ACCEPT_TRANSFER, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); final List existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds()); final List existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds()); @@ -1831,6 +1877,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf saveLoanWithDataIntegrityViolationChecks(loan); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); + this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_ACCEPT_TRANSFER, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); return newTransferAcceptanceTransaction; } @@ -1842,6 +1890,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf AppUser currentUser = getAppUserIfPresent(); final Loan loan = this.loanAssembler.assembleFrom(accountId); + this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_WITHDRAW_TRANSFER, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); final List existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds()); final List existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds()); @@ -1855,6 +1905,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf saveLoanWithDataIntegrityViolationChecks(loan); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); + this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_WITHDRAW_TRANSFER, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); return newTransferAcceptanceTransaction; } @@ -1863,8 +1915,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf @Override public void rejectLoanTransfer(final Long accountId) { final Loan loan = this.loanAssembler.assembleFrom(accountId); + this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_REJECT_TRANSFER, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); loan.setLoanStatus(LoanStatus.TRANSFER_ON_HOLD.getValue()); saveLoanWithDataIntegrityViolationChecks(loan); + this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_REJECT_TRANSFER, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); } @Transactional @@ -1882,12 +1938,15 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final Loan loan = this.loanAssembler.assembleFrom(loanId); checkClientOrGroupActive(loan); - + this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_REASSIGN_OFFICER, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); if (!loan.hasLoanOfficer(fromLoanOfficer)) { throw new LoanOfficerAssignmentException(loanId, fromLoanOfficerId); } loan.reassignLoanOfficer(toLoanOfficer, dateOfLoanOfficerAssignment); saveLoanWithDataIntegrityViolationChecks(loan); + this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_REASSIGN_OFFICER, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // @@ -1917,12 +1976,16 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf for (final String loanIdString : loanIds) { final Long loanId = Long.valueOf(loanIdString); final Loan loan = this.loanAssembler.assembleFrom(loanId); + this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_REASSIGN_OFFICER, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); checkClientOrGroupActive(loan); if (!loan.hasLoanOfficer(fromLoanOfficer)) { throw new LoanOfficerAssignmentException(loanId, fromLoanOfficerId); } loan.reassignLoanOfficer(toLoanOfficer, dateOfLoanOfficerAssignment); saveLoanWithDataIntegrityViolationChecks(loan); + this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_REASSIGN_OFFICER, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); } this.loanRepository.flush(); @@ -1945,10 +2008,14 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf checkClientOrGroupActive(loan); if (loan.getLoanOfficer() == null) { throw new LoanOfficerUnassignmentException(loanId); } + this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_REMOVE_OFFICER, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); loan.removeLoanOfficer(dateOfLoanOfficerunAssigned); saveLoanWithDataIntegrityViolationChecks(loan); + this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_REMOVE_OFFICER, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // @@ -2334,6 +2401,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf && loan.isFeeCompoundingEnabledForInterestRecalculation()) { this.loanAccountDomainService.recalculateAccruals(loan); } + this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_APPLY_OVERDUE_CHARGE, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); + } } @@ -2384,7 +2454,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf installment = loan.fetchRepaymentScheduleInstallment(periodNumber); } if (loan != null) { - + this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_APPLY_OVERDUE_CHARGE, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); for (Map.Entry entry : scheduleDates.entrySet()) { final LoanCharge loanCharge = LoanCharge.createNewFromJson(loan, chargeDefinition, command, entry.getValue()); @@ -2422,6 +2493,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf Long overdurPenaltyWaitPeriod = null; LocalDate lastTransactionDate = null; LoanTransaction writeOffTransaction = loan.findWriteOffTransaction(); + this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_UNDO_WRITTEN_OFF, + constructEntityMap(BUSINESS_ENTITY.LOAN_TRANSACTION, writeOffTransaction)); if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { restCalendarInstance = calendarInstanceRepository.findCalendarInstaneByEntityId(loan.loanInterestRecalculationDetailId(), CalendarEntityType.LOAN_RECALCULATION_DETAIL.getValue()); @@ -2457,7 +2530,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); this.loanAccountDomainService.recalculateAccruals(loan); if (writeOffTransaction != null) { - this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_UNDO_WRITTEN_OFF, writeOffTransaction); + this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_UNDO_WRITTEN_OFF, + constructEntityMap(BUSINESS_ENTITY.LOAN_TRANSACTION, writeOffTransaction)); } return new CommandProcessingResultBuilder() // .withOfficeId(loan.getOfficeId()) // @@ -2614,6 +2688,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf public void recalculateInterest(final long loanId) { AppUser currentUser = getAppUserIfPresent(); Loan loan = this.loanAssembler.assembleFrom(loanId); + this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_INTEREST_RECALCULATION, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); CalendarInstance restCalendarInstance = null; ApplicationCurrency applicationCurrency = null; LocalDate calculatedRepaymentsStartingFromDate = null; @@ -2659,6 +2735,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); this.loanAccountDomainService.recalculateAccruals(loan); + this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_INTEREST_RECALCULATION, + constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); } @Override @@ -2762,4 +2840,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf return user; } + private Map constructEntityMap(final BUSINESS_ENTITY entityEvent, Object entity) { + Map map = new HashMap<>(1); + map.put(entityEvent, entity); + return map; + } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/LoanProductConstants.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/LoanProductConstants.java index ba0a01102..76c31f658 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/LoanProductConstants.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/LoanProductConstants.java @@ -63,6 +63,7 @@ public interface LoanProductConstants { public static final String graceOnArrearsAgeingParameterName = "graceOnArrearsAgeing"; public static final String overdueDaysForNPAParameterName = "overdueDaysForNPA"; public static final String minimumDaysBetweenDisbursalAndFirstRepayment = "minimumDaysBetweenDisbursalAndFirstRepayment"; + public static final String accountMovesOutOfNPAOnlyOnArrearsCompletionParamName = "accountMovesOutOfNPAOnlyOnArrearsCompletion"; // Interest recalculation related public static final String isInterestRecalculationEnabledParameterName = "isInterestRecalculationEnabled"; @@ -73,6 +74,7 @@ public interface LoanProductConstants { public static final String recalculationRestFrequencyTypeParameterName = "recalculationRestFrequencyType"; public static final String recalculationRestFrequencyIntervalParameterName = "recalculationRestFrequencyInterval"; public static final String recalculationRestFrequencyDateParamName = "recalculationRestFrequencyDate"; + public static final String isArrearsBasedOnOriginalScheduleParamName = "isArrearsBasedOnOriginalSchedule"; // Guarantee related public static final String holdGuaranteeFundsParamName = "holdGuaranteeFunds"; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/data/LoanProductData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/data/LoanProductData.java index e6e8234b7..f1baba9d3 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/data/LoanProductData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/data/LoanProductData.java @@ -138,6 +138,7 @@ public class LoanProductData { private final Boolean holdGuaranteeFunds; private final LoanProductGuaranteeData productGuaranteeData; + private final Boolean accountMovesOutOfNPAOnlyOnArrearsCompletion; /** * Used when returning lookup information about loan product for dropdowns. @@ -189,6 +190,7 @@ public class LoanProductData { final LoanProductGuaranteeData productGuaranteeData = null; final Boolean holdGuaranteeFunds = false; final BigDecimal principalThresholdForLastInstalment = null; + final boolean accountMovesOutOfNPAOnlyOnArrearsCompletion = false; final EnumOptionData daysInMonthType = null; final EnumOptionData daysInYearType = null; @@ -205,7 +207,7 @@ public class LoanProductData { interestRateVariations, numberOfRepaymentVariations, multiDisburseLoan, maxTrancheCount, outstandingLoanBalance, graceOnArrearsAgeing, overdueDaysForNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, minimumDaysBetweenDisbursalAndFirstRepayment, holdGuaranteeFunds, productGuaranteeData, - principalThresholdForLastInstalment); + principalThresholdForLastInstalment, accountMovesOutOfNPAOnlyOnArrearsCompletion); } public static LoanProductData lookupWithCurrency(final Long id, final String name, final CurrencyData currency) { @@ -262,6 +264,7 @@ public class LoanProductData { final Boolean holdGuaranteeFunds = false; final LoanProductGuaranteeData productGuaranteeData = null; final BigDecimal principalThresholdForLastInstalment = null; + final boolean accountMovesOutOfNPAOnlyOnArrearsCompletion = false; return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -272,7 +275,7 @@ public class LoanProductData { interestRateVariations, numberOfRepaymentVariations, multiDisburseLoan, maxTrancheCount, outstandingLoanBalance, graceOnArrearsAgeing, overdueDaysForNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, minimumDaysBetweenDisbursalAndFirstRepayment, holdGuaranteeFunds, productGuaranteeData, - principalThresholdForLastInstalment); + principalThresholdForLastInstalment, accountMovesOutOfNPAOnlyOnArrearsCompletion); } public static LoanProductData sensibleDefaultsForNewLoanProductCreation() { @@ -336,6 +339,7 @@ public class LoanProductData { final Boolean holdGuaranteeFunds = false; final LoanProductGuaranteeData productGuaranteeData = null; final BigDecimal principalThresholdForLastInstalment = null; + final boolean accountMovesOutOfNPAOnlyOnArrearsCompletion = false; return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -346,7 +350,7 @@ public class LoanProductData { principalVariationsForBorrowerCycle, interestRateVariationsForBorrowerCycle, numberOfRepaymentVariationsForBorrowerCycle, multiDisburseLoan, maxTrancheCount, outstandingLoanBalance, graceOnArrearsAgeing, overdueDaysForNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, minimumDaysBetweenDisbursalAndFirstRepayment, - holdGuaranteeFunds, productGuaranteeData, principalThresholdForLastInstalment); + holdGuaranteeFunds, productGuaranteeData, principalThresholdForLastInstalment, accountMovesOutOfNPAOnlyOnArrearsCompletion); } public static LoanProductData withAccountingDetails(final LoanProductData productData, final Map accountingMappings, @@ -378,7 +382,8 @@ public class LoanProductData { final Integer overdueDaysForNPA, final EnumOptionData daysInMonthType, final EnumOptionData daysInYearType, final boolean isInterestRecalculationEnabled, final LoanProductInterestRecalculationData interestRecalculationData, final Integer minimumDaysBetweenDisbursalAndFirstRepayment, boolean holdGuaranteeFunds, - final LoanProductGuaranteeData loanProductGuaranteeData, final BigDecimal principalThresholdForLastInstalment) { + final LoanProductGuaranteeData loanProductGuaranteeData, final BigDecimal principalThresholdForLastInstalment, + final boolean accountMovesOutOfNPAOnlyOnArrearsCompletion) { this.id = id; this.name = name; this.shortName = shortName; @@ -452,6 +457,7 @@ public class LoanProductData { this.holdGuaranteeFunds = holdGuaranteeFunds; this.productGuaranteeData = loanProductGuaranteeData; this.principalThresholdForLastInstalment = principalThresholdForLastInstalment; + this.accountMovesOutOfNPAOnlyOnArrearsCompletion = accountMovesOutOfNPAOnlyOnArrearsCompletion; this.daysInMonthTypeOptions = null; this.daysInYearTypeOptions = null; @@ -556,6 +562,7 @@ public class LoanProductData { this.holdGuaranteeFunds = productData.holdGuaranteeFunds; this.productGuaranteeData = productData.productGuaranteeData; this.principalThresholdForLastInstalment = productData.principalThresholdForLastInstalment; + this.accountMovesOutOfNPAOnlyOnArrearsCompletion = productData.accountMovesOutOfNPAOnlyOnArrearsCompletion; this.daysInMonthTypeOptions = daysInMonthTypeOptions; this.daysInYearTypeOptions = daysInYearTypeOptions; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/data/LoanProductInterestRecalculationData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/data/LoanProductInterestRecalculationData.java index c076b93c3..400f32f68 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/data/LoanProductInterestRecalculationData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/data/LoanProductInterestRecalculationData.java @@ -24,11 +24,13 @@ public class LoanProductInterestRecalculationData { private final EnumOptionData recalculationRestFrequencyType; private final Integer recalculationRestFrequencyInterval; private final LocalDate recalculationRestFrequencyDate; + @SuppressWarnings("unused") + private final boolean isArrearsBasedOnOriginalSchedule; public LoanProductInterestRecalculationData(final Long id, final Long productId, final EnumOptionData interestRecalculationCompoundingType, final EnumOptionData rescheduleStrategyType, final EnumOptionData recalculationRestFrequencyType, final Integer recalculationRestFrequencyInterval, - final LocalDate recalculationRestFrequencyDate) { + final LocalDate recalculationRestFrequencyDate, boolean isArrearsBasedOnOriginalSchedule) { this.id = id; this.productId = productId; this.interestRecalculationCompoundingType = interestRecalculationCompoundingType; @@ -36,6 +38,7 @@ public class LoanProductInterestRecalculationData { this.recalculationRestFrequencyType = recalculationRestFrequencyType; this.recalculationRestFrequencyInterval = recalculationRestFrequencyInterval; this.recalculationRestFrequencyDate = recalculationRestFrequencyDate; + this.isArrearsBasedOnOriginalSchedule = isArrearsBasedOnOriginalSchedule; } public static LoanProductInterestRecalculationData sensibleDefaultsForNewLoanProductCreation() { @@ -46,8 +49,10 @@ public class LoanProductInterestRecalculationData { final EnumOptionData recalculationRestFrequencyType = null; final Integer recalculationRestFrequencyInterval = null; final LocalDate recalculationRestFrequencyDate = null; + final boolean isArrearsBasedOnOriginalSchedule = false; return new LoanProductInterestRecalculationData(id, productId, interestRecalculationCompoundingType, rescheduleStrategyType, - recalculationRestFrequencyType, recalculationRestFrequencyInterval, recalculationRestFrequencyDate); + recalculationRestFrequencyType, recalculationRestFrequencyInterval, recalculationRestFrequencyDate, + isArrearsBasedOnOriginalSchedule); } public EnumOptionData getInterestRecalculationCompoundingType() { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProduct.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProduct.java index 6c335e106..f85e153eb 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProduct.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProduct.java @@ -139,6 +139,9 @@ public class LoanProduct extends AbstractPersistable { @Column(name = "principal_threshold_for_last_instalment", scale = 2, precision = 5, nullable = false) private BigDecimal principalThresholdForLastInstalment; + @Column(name = "account_moves_out_of_npa_only_on_arrears_completion") + private boolean accountMovesOutOfNPAOnlyOnArrearsCompletion; + public static LoanProduct assembleFromJson(final Fund fund, final LoanTransactionProcessingStrategy loanTransactionProcessingStrategy, final List productCharges, final JsonCommand command, final AprCalculator aprCalculator) { @@ -235,6 +238,8 @@ public class LoanProduct extends AbstractPersistable { principalThresholdForLastInstalment = multiDisburseLoan ? LoanProductConstants.DEFAULT_PRINCIPAL_THRESHOLD_FOR_MULTI_DISBURSE_LOAN : LoanProductConstants.DEFAULT_PRINCIPAL_THRESHOLD_FOR_SINGLE_DISBURSE_LOAN; } + final boolean accountMovesOutOfNPAOnlyOnArrearsCompletion = command + .booleanPrimitiveValueOfParameterNamed(LoanProductConstants.accountMovesOutOfNPAOnlyOnArrearsCompletionParamName); return new LoanProduct(fund, loanTransactionProcessingStrategy, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, interestRatePerPeriod, minInterestRatePerPeriod, maxInterestRatePerPeriod, interestFrequencyType, @@ -244,7 +249,8 @@ public class LoanProduct extends AbstractPersistable { startDate, closeDate, externalId, useBorrowerCycle, loanProductBorrowerCycleVariations, multiDisburseLoan, maxTrancheCount, outstandingLoanBalance, graceOnArrearsAgeing, overdueDaysForNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, interestRecalculationSettings, minimumDaysBetweenDisbursalAndFirstRepayment, - holdGuarantorFunds, loanProductGuaranteeDetails, principalThresholdForLastInstalment); + holdGuarantorFunds, loanProductGuaranteeDetails, principalThresholdForLastInstalment, + accountMovesOutOfNPAOnlyOnArrearsCompletion); } @@ -469,7 +475,8 @@ public class LoanProduct extends AbstractPersistable { final boolean isInterestRecalculationEnabled, final LoanProductInterestRecalculationDetails productInterestRecalculationDetails, final Integer minimumDaysBetweenDisbursalAndFirstRepayment, final boolean holdGuarantorFunds, - final LoanProductGuaranteeDetails loanProductGuaranteeDetails, final BigDecimal principalThresholdForLastInstalment) { + final LoanProductGuaranteeDetails loanProductGuaranteeDetails, final BigDecimal principalThresholdForLastInstalment, + final boolean accountMovesOutOfNPAOnlyOnArrearsCompletion) { this.fund = fund; this.transactionProcessingStrategy = transactionProcessingStrategy; this.name = name.trim(); @@ -522,6 +529,7 @@ public class LoanProduct extends AbstractPersistable { this.holdGuaranteeFunds = holdGuarantorFunds; this.loanProductGuaranteeDetails = loanProductGuaranteeDetails; this.principalThresholdForLastInstalment = principalThresholdForLastInstalment; + this.accountMovesOutOfNPAOnlyOnArrearsCompletion = accountMovesOutOfNPAOnlyOnArrearsCompletion; } public MonetaryCurrency getCurrency() { @@ -756,6 +764,13 @@ public class LoanProduct extends AbstractPersistable { actualChanges.put(LoanProductConstants.principalThresholdForLastInstalmentParamName, newValue); this.principalThresholdForLastInstalment = newValue; } + if (command.isChangeInBooleanParameterNamed(LoanProductConstants.accountMovesOutOfNPAOnlyOnArrearsCompletionParamName, + this.accountMovesOutOfNPAOnlyOnArrearsCompletion)) { + final boolean newValue = command + .booleanPrimitiveValueOfParameterNamed(LoanProductConstants.accountMovesOutOfNPAOnlyOnArrearsCompletionParamName); + actualChanges.put(LoanProductConstants.accountMovesOutOfNPAOnlyOnArrearsCompletionParamName, newValue); + this.accountMovesOutOfNPAOnlyOnArrearsCompletion = newValue; + } return actualChanges; } @@ -1013,4 +1028,12 @@ public class LoanProduct extends AbstractPersistable { return this.principalThresholdForLastInstalment; } + public boolean isArrearsBasedOnOriginalSchedule() { + boolean isBasedOnOriginalSchedule = false; + if (getProductInterestRecalculationDetails() != null) { + isBasedOnOriginalSchedule = getProductInterestRecalculationDetails().isArrearsBasedOnOriginalSchedule(); + } + return isBasedOnOriginalSchedule; + } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProductInterestRecalculationDetails.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProductInterestRecalculationDetails.java index dd8817c46..870037252 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProductInterestRecalculationDetails.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProductInterestRecalculationDetails.java @@ -57,6 +57,9 @@ public class LoanProductInterestRecalculationDetails extends AbstractPersistable @Column(name = "rest_freqency_date") private Date restFrequencyDate; + @Column(name = "arrears_based_on_original_schedule") + private boolean isArrearsBasedOnOriginalSchedule; + protected LoanProductInterestRecalculationDetails() { // } @@ -75,6 +78,8 @@ public class LoanProductInterestRecalculationDetails extends AbstractPersistable .localDateValueOfParameterNamed(LoanProductConstants.recalculationRestFrequencyDateParamName); Integer recurrenceInterval = command .integerValueOfParameterNamed(LoanProductConstants.recalculationRestFrequencyIntervalParameterName); + final boolean isArrearsBasedOnOriginalSchedule = command + .booleanPrimitiveValueOfParameterNamed(LoanProductConstants.isArrearsBasedOnOriginalScheduleParamName); RecalculationFrequencyType frequencyType = RecalculationFrequencyType.fromInt(recurrenceFrequency); Date recurrenceOnDate = null; if (recurrenceOnLocalDate != null) { @@ -87,17 +92,18 @@ public class LoanProductInterestRecalculationDetails extends AbstractPersistable } return new LoanProductInterestRecalculationDetails(interestRecalculationCompoundingMethod, loanRescheduleStrategyMethod, - recurrenceFrequency, recurrenceInterval, recurrenceOnDate); + recurrenceFrequency, recurrenceInterval, recurrenceOnDate, isArrearsBasedOnOriginalSchedule); } private LoanProductInterestRecalculationDetails(final Integer interestRecalculationCompoundingMethod, final Integer rescheduleStrategyMethod, final Integer restFrequencyType, final Integer restInterval, - final Date restFrequencyDate) { + final Date restFrequencyDate, final boolean isArrearsBasedOnOriginalSchedule) { this.interestRecalculationCompoundingMethod = interestRecalculationCompoundingMethod; this.rescheduleStrategyMethod = rescheduleStrategyMethod; this.restFrequencyType = restFrequencyType; this.restInterval = restInterval; this.restFrequencyDate = restFrequencyDate; + this.isArrearsBasedOnOriginalSchedule = isArrearsBasedOnOriginalSchedule; } public void updateProduct(final LoanProduct loanProduct) { @@ -169,6 +175,14 @@ public class LoanProductInterestRecalculationDetails extends AbstractPersistable this.restFrequencyDate = recurrenceOnDate; } } + if (command.isChangeInBooleanParameterNamed(LoanProductConstants.isArrearsBasedOnOriginalScheduleParamName, + this.isArrearsBasedOnOriginalSchedule)) { + final boolean newValue = command + .booleanPrimitiveValueOfParameterNamed(LoanProductConstants.isArrearsBasedOnOriginalScheduleParamName); + actualChanges.put(LoanProductConstants.isArrearsBasedOnOriginalScheduleParamName, newValue); + this.isArrearsBasedOnOriginalSchedule = newValue; + } + } public LocalDate getRestFrequencyLocalDate() { @@ -186,4 +200,9 @@ public class LoanProductInterestRecalculationDetails extends AbstractPersistable public Integer getRestInterval() { return this.restInterval; } + + + public boolean isArrearsBasedOnOriginalSchedule() { + return this.isArrearsBasedOnOriginalSchedule; + } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/serialization/LoanProductDataValidator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/serialization/LoanProductDataValidator.java index ecc59d2f9..c50139585 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/serialization/LoanProductDataValidator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/serialization/LoanProductDataValidator.java @@ -71,9 +71,11 @@ public final class LoanProductDataValidator { LoanProductConstants.recalculationRestFrequencyDateParamName, LoanProductConstants.recalculationRestFrequencyIntervalParameterName, LoanProductConstants.recalculationRestFrequencyTypeParameterName, + LoanProductConstants.isArrearsBasedOnOriginalScheduleParamName, LoanProductConstants.minimumDaysBetweenDisbursalAndFirstRepayment, LoanProductConstants.mandatoryGuaranteeParamName, LoanProductConstants.holdGuaranteeFundsParamName, LoanProductConstants.minimumGuaranteeFromGuarantorParamName, - LoanProductConstants.minimumGuaranteeFromOwnFundsParamName, LoanProductConstants.principalThresholdForLastInstalmentParamName)); + LoanProductConstants.minimumGuaranteeFromOwnFundsParamName, LoanProductConstants.principalThresholdForLastInstalmentParamName, + LoanProductConstants.accountMovesOutOfNPAOnlyOnArrearsCompletionParamName)); private final FromJsonHelper fromApiJsonHelper; @@ -299,6 +301,13 @@ public final class LoanProductDataValidator { baseDataValidator.reset().parameter(LoanProductConstants.daysInMonthTypeParameterName).value(daysInMonthType).notNull() .isOneOfTheseValues(1, 30); + if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.accountMovesOutOfNPAOnlyOnArrearsCompletionParamName, element)) { + Boolean npaChangeConfig = this.fromApiJsonHelper.extractBooleanNamed( + LoanProductConstants.accountMovesOutOfNPAOnlyOnArrearsCompletionParamName, element); + baseDataValidator.reset().parameter(LoanProductConstants.accountMovesOutOfNPAOnlyOnArrearsCompletionParamName) + .value(npaChangeConfig).notNull().isOneOfTheseValues(true, false); + } + // Interest recalculation settings final Boolean isInterestRecalculationEnabled = this.fromApiJsonHelper.extractBooleanNamed( LoanProductConstants.isInterestRecalculationEnabledParameterName, element); @@ -508,6 +517,12 @@ public final class LoanProductDataValidator { } } + if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.isArrearsBasedOnOriginalScheduleParamName, element)) { + final Boolean isArrearsBasedOnOriginalSchedule = this.fromApiJsonHelper.extractBooleanNamed( + LoanProductConstants.isArrearsBasedOnOriginalScheduleParamName, element); + baseDataValidator.reset().parameter(LoanProductConstants.isArrearsBasedOnOriginalScheduleParamName) + .value(isArrearsBasedOnOriginalSchedule).notNull().isOneOfTheseValues(true, false); + } } public void validateForUpdate(final String json, final LoanProduct loanProduct) { @@ -724,6 +739,13 @@ public final class LoanProductDataValidator { .isOneOfTheseValues(1, 30); } + if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.accountMovesOutOfNPAOnlyOnArrearsCompletionParamName, element)) { + Boolean npaChangeConfig = this.fromApiJsonHelper.extractBooleanNamed( + LoanProductConstants.accountMovesOutOfNPAOnlyOnArrearsCompletionParamName, element); + baseDataValidator.reset().parameter(LoanProductConstants.accountMovesOutOfNPAOnlyOnArrearsCompletionParamName) + .value(npaChangeConfig).notNull().isOneOfTheseValues(true, false); + } + // Interest recalculation settings Boolean isInterestRecalculationEnabled = loanProduct.isInterestRecalculationEnabled(); if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.isInterestRecalculationEnabledParameterName, element)) { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java index 064171076..3b710a31b 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java @@ -162,11 +162,12 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo + "lp.days_in_month_enum as daysInMonth, lp.days_in_year_enum as daysInYear, lp.interest_recalculation_enabled as isInterestRecalculationEnabled, " + "lpr.id as lprId, lpr.product_id as productId, lpr.compound_type_enum as compoundType, lpr.reschedule_strategy_enum as rescheduleStrategy, " + "lpr.rest_frequency_type_enum as restFrequencyEnum, lpr.rest_frequency_interval as restFrequencyInterval, " - + "lpr.rest_freqency_date as restFrequencyDate, " + + "lpr.rest_freqency_date as restFrequencyDate, lpr.arrears_based_on_original_schedule as isArrearsBasedOnOriginalSchedule, " + "lp.hold_guarantee_funds as holdGuaranteeFunds, " + "lp.principal_threshold_for_last_instalment as principalThresholdForLastInstalment, " + "lpg.id as lpgId, lpg.mandatory_guarantee as mandatoryGuarantee, " + "lpg.minimum_guarantee_from_own_funds as minimumGuaranteeFromOwnFunds, lpg.minimum_guarantee_from_guarantor_funds as minimumGuaranteeFromGuarantor, " + + "lp.account_moves_out_of_npa_only_on_arrears_completion as accountMovesOutOfNPAOnlyOnArrearsCompletion, " + "curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, curr.display_symbol as currencyDisplaySymbol, lp.external_id as externalId " + " from m_product_loan lp " + " left join m_fund f on f.id = lp.fund_id " @@ -291,10 +292,10 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo final EnumOptionData restFrequencyType = LoanEnumerations.interestRecalculationFrequencyType(restFrequencyEnumValue); final int restFrequencyInterval = JdbcSupport.getInteger(rs, "restFrequencyInterval"); final LocalDate restFrequencyDate = JdbcSupport.getLocalDate(rs, "restFrequencyDate"); - + final boolean isArrearsBasedOnOriginalSchedule = rs.getBoolean("isArrearsBasedOnOriginalSchedule"); interestRecalculationData = new LoanProductInterestRecalculationData(lprId, productId, interestRecalculationCompoundingType, rescheduleStrategyType, restFrequencyType, restFrequencyInterval, - restFrequencyDate); + restFrequencyDate, isArrearsBasedOnOriginalSchedule); } final boolean holdGuaranteeFunds = rs.getBoolean("holdGuaranteeFunds"); @@ -309,6 +310,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo } final BigDecimal principalThresholdForLastInstalment = rs.getBigDecimal("principalThresholdForLastInstalment"); + final boolean accountMovesOutOfNPAOnlyOnArrearsCompletion = rs.getBoolean("accountMovesOutOfNPAOnlyOnArrearsCompletion"); return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -320,7 +322,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo numberOfRepaymentVariationsForBorrowerCycle, multiDisburseLoan, maxTrancheCount, outstandingLoanBalance, graceOnArrearsAgeing, overdueDaysForNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, minimumDaysBetweenDisbursalAndFirstRepayment, holdGuaranteeFunds, loanProductGuaranteeData, - principalThresholdForLastInstalment); + principalThresholdForLastInstalment, accountMovesOutOfNPAOnlyOnArrearsCompletion); } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerService.java b/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerService.java index f9969e132..9b8a3f094 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerService.java @@ -11,8 +11,6 @@ public interface ScheduledJobRunnerService { void updateLoanSummaryDetails(); - void updateLoanArrearsAgeingDetails(); - void updateLoanPaidInAdvance(); void applyAnnualFeeForSavings(); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerServiceImpl.java index 20f60e71b..e7fa3836a 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerServiceImpl.java @@ -136,46 +136,6 @@ public class ScheduledJobRunnerServiceImpl implements ScheduledJobRunnerService logger.info(ThreadLocalContextUtil.getTenant().getName() + ": Results affected by update: " + result); } - @Transactional - @Override - @CronTarget(jobName = JobName.UPDATE_LOAN_ARREARS_AGEING) - public void updateLoanArrearsAgeingDetails() { - - final JdbcTemplate jdbcTemplate = new JdbcTemplate(this.dataSourceServiceFactory.determineDataSourceService().retrieveDataSource()); - - jdbcTemplate.execute("truncate table m_loan_arrears_aging"); - - final StringBuilder updateSqlBuilder = new StringBuilder(900); - - updateSqlBuilder - .append("INSERT INTO m_loan_arrears_aging(`loan_id`,`principal_overdue_derived`,`interest_overdue_derived`,`fee_charges_overdue_derived`,`penalty_charges_overdue_derived`,`total_overdue_derived`,`overdue_since_date_derived`)"); - updateSqlBuilder.append("select ml.id as loanId,"); - updateSqlBuilder - .append("SUM((ifnull(mr.principal_amount,0) - ifnull(mr.principal_completed_derived, 0))) as principal_overdue_derived,"); - updateSqlBuilder - .append("SUM((ifnull(mr.interest_amount,0) - ifnull(mr.interest_completed_derived, 0))) as interest_overdue_derived,"); - updateSqlBuilder - .append("SUM((ifnull(mr.fee_charges_amount,0) - ifnull(mr.fee_charges_completed_derived, 0))) as fee_charges_overdue_derived,"); - updateSqlBuilder - .append("SUM((ifnull(mr.penalty_charges_amount,0) - ifnull(mr.penalty_charges_completed_derived, 0))) as penalty_charges_overdue_derived,"); - updateSqlBuilder.append("SUM((ifnull(mr.principal_amount,0) - ifnull(mr.principal_completed_derived, 0))) +"); - updateSqlBuilder.append("SUM((ifnull(mr.interest_amount,0) - ifnull(mr.interest_completed_derived, 0))) +"); - updateSqlBuilder.append("SUM((ifnull(mr.fee_charges_amount,0) - ifnull(mr.fee_charges_completed_derived, 0))) +"); - updateSqlBuilder - .append("SUM((ifnull(mr.penalty_charges_amount,0) - ifnull(mr.penalty_charges_completed_derived, 0))) as total_overdue_derived,"); - updateSqlBuilder.append("MIN(mr.duedate) as overdue_since_date_derived "); - updateSqlBuilder.append(" FROM m_loan ml "); - updateSqlBuilder.append(" INNER JOIN m_loan_repayment_schedule mr on mr.loan_id = ml.id "); - updateSqlBuilder.append(" WHERE ml.loan_status_id = 300 "); // active - updateSqlBuilder.append(" and mr.completed_derived is false "); - updateSqlBuilder.append(" and mr.duedate < SUBDATE(CURDATE(),INTERVAL ifnull(ml.grace_on_arrears_ageing,0) day) "); - updateSqlBuilder.append(" GROUP BY ml.id"); - - final int result = jdbcTemplate.update(updateSqlBuilder.toString()); - - logger.info(ThreadLocalContextUtil.getTenant().getName() + ": Results affected by update: " + result); - } - @Transactional @Override @CronTarget(jobName = JobName.UPDATE_LOAN_PAID_IN_ADVANCE) @@ -273,18 +233,28 @@ public class ScheduledJobRunnerServiceImpl implements ScheduledJobRunnerService final JdbcTemplate jdbcTemplate = new JdbcTemplate(this.dataSourceServiceFactory.determineDataSourceService().retrieveDataSource()); - jdbcTemplate.update("UPDATE m_loan ml SET ml.is_npa=0"); + final StringBuilder resetNPASqlBuilder = new StringBuilder(900); + resetNPASqlBuilder.append("update m_loan loan "); + resetNPASqlBuilder.append("left join m_loan_arrears_aging laa on laa.loan_id = loan.id "); + resetNPASqlBuilder.append("inner join m_product_loan mpl on mpl.id = loan.product_id and mpl.overdue_days_for_npa is not null "); + resetNPASqlBuilder.append("set loan.is_npa = 0 "); + resetNPASqlBuilder.append("where loan.loan_status_id = 300 and mpl.account_moves_out_of_npa_only_on_arrears_completion = 0 "); + resetNPASqlBuilder + .append("or (mpl.account_moves_out_of_npa_only_on_arrears_completion = 1 and laa.overdue_since_date_derived is null)"); + + jdbcTemplate.update(resetNPASqlBuilder.toString()); final StringBuilder updateSqlBuilder = new StringBuilder(900); updateSqlBuilder.append("UPDATE m_loan as ml,"); - updateSqlBuilder.append(" (select loan.id from m_loan_repayment_schedule mr "); - updateSqlBuilder - .append(" INNER JOIN m_loan loan on mr.loan_id = loan.id INNER JOIN m_product_loan mpl on mpl.id = loan.product_id AND mpl.overdue_days_for_npa is not null "); - updateSqlBuilder.append("WHERE loan.loan_status_id = 300 and mr.completed_derived is false "); - updateSqlBuilder - .append(" and mr.duedate < SUBDATE(CURDATE(),INTERVAL ifnull(mpl.overdue_days_for_npa,0) day) group by loan.id) as sl "); - updateSqlBuilder.append("SET ml.is_npa=1 where ml.id=sl.id"); + updateSqlBuilder.append(" (select loan.id "); + updateSqlBuilder.append("from m_loan_arrears_aging laa"); + updateSqlBuilder.append(" INNER JOIN m_loan loan on laa.loan_id = loan.id "); + updateSqlBuilder.append(" INNER JOIN m_product_loan mpl on mpl.id = loan.product_id AND mpl.overdue_days_for_npa is not null "); + updateSqlBuilder.append("WHERE loan.loan_status_id = 300 and "); + updateSqlBuilder.append("laa.overdue_since_date_derived < SUBDATE(CURDATE(),INTERVAL ifnull(mpl.overdue_days_for_npa,0) day) "); + updateSqlBuilder.append("group by loan.id) as sl "); + updateSqlBuilder.append("SET ml.is_npa=1 where ml.id=sl.id "); final int result = jdbcTemplate.update(updateSqlBuilder.toString()); diff --git a/mifosng-provider/src/main/resources/sql/migrations/core_db/V240__arrears_aging_config_for_interest_recalculation.sql b/mifosng-provider/src/main/resources/sql/migrations/core_db/V240__arrears_aging_config_for_interest_recalculation.sql new file mode 100644 index 000000000..d1e7bbd09 --- /dev/null +++ b/mifosng-provider/src/main/resources/sql/migrations/core_db/V240__arrears_aging_config_for_interest_recalculation.sql @@ -0,0 +1,5 @@ +ALTER TABLE `m_product_loan_recalculation_details` + ADD COLUMN `arrears_based_on_original_schedule` TINYINT(1) NOT NULL DEFAULT '0'; + +ALTER TABLE `m_product_loan` + ADD COLUMN `account_moves_out_of_npa_only_on_arrears_completion` TINYINT(1) NOT NULL DEFAULT '0'; \ No newline at end of file
Additional Optional Fields if interest recalculation is enabled(true) and recalculationRestFrequencyType is not same as repayment period