diff --git a/api-docs/apiLive.htm b/api-docs/apiLive.htm index 7af3a6260..720597eed 100644 --- a/api-docs/apiLive.htm +++ b/api-docs/apiLive.htm @@ -1245,6 +1245,30 @@ + + Loan Term Variations + /loans/{loanId}/schedule?command=calculateLoanSchedule + Calculate Schedule with Loan Term Variations + + + + + + + /loans/{loanId}/schedule?command=addVariations + Create Loan Term Variations + + + + + + + /loans/{loanId}/schedule?command=deleteVariations + Remove All Loan Term Variations + + + + @@ -11765,6 +11789,603 @@ Request Body: + + +  +
+
+

Loan Rescheduling

+

Loan Term Variations provides the ability to change due dates, amounts and number of instalments before loan approval.

+ + + + + + + + + + + + + + + + + +
Field Descriptions
loanId
The ID of the loan to be modified.
exceptions
contains exception data for variations(modifiedinstallments,newinstallments and deletedinstallments described below)
+

Exception object fields

+

Modifiedinstallments Array fields

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Field Descriptions
dueDate
Schedule dueDate for which this exception should be applied.
modifiedDueDate
this is the exception data to move the due date to specified date for a instalment
installmentAmount
new instalment amount(principal+interest) for the specified instalment. supported only for interest method declining with equal instalments Amortization
principal
new Principal amount for the specified instalment. supported only for interest method declining with equal instalments Amortization and for Flat loans.
+

newinstallments Array fields

+ + + + + + + + + + + + + + + + + + + + + + +
Field Descriptions
dueDate
Schedule dueDate for inserting new instalment.
installmentAmount
new instalment amount(principal+interest) for the new instalment. supported only for interest method declining with equal instalments Amortization
principal
new Principal amount for the new instalment. supported only for interest method declining with equal instalments Amortization and for Flat loans.
+

deletedinstallments Array fields

+ + + + + + + + + + +
Field Descriptions
dueDate
Schedule dueDate for removing specific instalment.
+
+
+ + + + +   +
+
+

Calculate loan repayment schedule based on Loan term variations

+ + + + + + + +
Mandatory Fields
exceptions,locale,dateFormat
+
+
+ +POST https://DomainName/api/v1/loans/1/schedule?command=calculateLoanSchedule + + +POST loans/{loanIdd}/schedule?command=calculateLoanSchedule +Content-Type: application/json +Request Body: +{ + "locale": "en", + "dateFormat": "dd MMMM yyyy", + "exceptions": { + "modifiedinstallments": [{ + "dueDate": '01 April 2016', + "modifiedDueDate": '05 April 2016' + }, + { + "dueDate": '05 January 2016', + "modifiedDueDate": '01 January 2016' + }], + "deletedinstallments": [{ + "dueDate": "01 October 2016" + }], + newinstallments: [{ + dueDate: '10 October 2016', + installmentAmount: '1000' +}] + } +} + + +{ + "currency": { + "code": "USD", + "name": "US Dollar", + "decimalPlaces": 2, + "displaySymbol": "$", + "nameCode": "currency.USD", + "displayLabel": "US Dollar ($)" + }, + "loanTermInDays": 12, + "totalPrincipalDisbursed": 10000, + "totalPrincipalExpected": 10000, + "totalInterestCharged": 1340.98, + "totalFeeChargesCharged": 0, + "totalPenaltyChargesCharged": 0, + "totalRepaymentExpected": 11340.98, + "periods": [{ + "period": 1, + "fromDate": [2015, + 11, + 1], + "dueDate": [2015, + 12, + 1], + "daysInPeriod": 30, + "principalOriginalDue": 745.6, + "principalDue": 745.6, + "principalOutstanding": 745.6, + "principalLoanBalanceOutstanding": 9254.4, + "interestOriginalDue": 200, + "interestDue": 200, + "interestOutstanding": 200, + "feeChargesDue": 0, + "penaltyChargesDue": 0, + "totalOriginalDueForPeriod": 945.6, + "totalDueForPeriod": 945.6, + "totalPaidForPeriod": 0, + "totalOutstandingForPeriod": 945.6, + "totalOverdue": 945.6, + "totalActualCostOfLoanForPeriod": 200 + }, + { + "period": 2, + "fromDate": [2015, + 12, + 1], + "dueDate": [2016, + 1, + 1], + "daysInPeriod": 31, + "principalOriginalDue": 814.91, + "principalDue": 814.91, + "principalOutstanding": 814.91, + "principalLoanBalanceOutstanding": 8439.49, + "interestOriginalDue": 185.09, + "interestDue": 185.09, + "interestOutstanding": 185.09, + "feeChargesDue": 0, + "penaltyChargesDue": 0, + "totalOriginalDueForPeriod": 1000, + "totalDueForPeriod": 1000, + "totalPaidForPeriod": 0, + "totalOutstandingForPeriod": 1000, + "totalActualCostOfLoanForPeriod": 185.09 + }, + { + "period": 3, + "fromDate": [2016, + 1, + 1], + "dueDate": [2016, + 2, + 1], + "daysInPeriod": 31, + "principalOriginalDue": 770.75, + "principalDue": 770.75, + "principalOutstanding": 770.75, + "principalLoanBalanceOutstanding": 7668.74, + "interestOriginalDue": 168.79, + "interestDue": 168.79, + "interestOutstanding": 168.79, + "feeChargesDue": 0, + "penaltyChargesDue": 0, + "totalOriginalDueForPeriod": 939.54, + "totalDueForPeriod": 939.54, + "totalPaidForPeriod": 0, + "totalOutstandingForPeriod": 939.54, + "totalActualCostOfLoanForPeriod": 168.79 + }, + { + "period": 4, + "fromDate": [2016, + 2, + 1], + "dueDate": [2016, + 3, + 5], + "daysInPeriod": 33, + "principalOriginalDue": 786.17, + "principalDue": 786.17, + "principalOutstanding": 786.17, + "principalLoanBalanceOutstanding": 6882.57, + "interestOriginalDue": 153.37, + "interestDue": 153.37, + "interestOutstanding": 153.37, + "feeChargesDue": 0, + "penaltyChargesDue": 0, + "totalOriginalDueForPeriod": 939.54, + "totalDueForPeriod": 939.54, + "totalPaidForPeriod": 0, + "totalOutstandingForPeriod": 939.54, + "totalActualCostOfLoanForPeriod": 153.37 + }, + { + "period": 5, + "fromDate": [2016, + 3, + 5], + "dueDate": [2016, + 4, + 5], + "daysInPeriod": 31, + "principalOriginalDue": 801.89, + "principalDue": 801.89, + "principalOutstanding": 801.89, + "principalLoanBalanceOutstanding": 6080.68, + "interestOriginalDue": 137.65, + "interestDue": 137.65, + "interestOutstanding": 137.65, + "feeChargesDue": 0, + "penaltyChargesDue": 0, + "totalOriginalDueForPeriod": 939.54, + "totalDueForPeriod": 939.54, + "totalPaidForPeriod": 0, + "totalOutstandingForPeriod": 939.54, + "totalActualCostOfLoanForPeriod": 137.65 + }, + { + "period": 6, + "fromDate": [2016, + 4, + 5], + "dueDate": [2016, + 5, + 1], + "daysInPeriod": 26, + "principalOriginalDue": 817.93, + "principalDue": 817.93, + "principalOutstanding": 817.93, + "principalLoanBalanceOutstanding": 5262.75, + "interestOriginalDue": 121.61, + "interestDue": 121.61, + "interestOutstanding": 121.61, + "feeChargesDue": 0, + "penaltyChargesDue": 0, + "totalOriginalDueForPeriod": 939.54, + "totalDueForPeriod": 939.54, + "totalPaidForPeriod": 0, + "totalOutstandingForPeriod": 939.54, + "totalActualCostOfLoanForPeriod": 121.61 + }, + { + "period": 7, + "fromDate": [2016, + 5, + 1], + "dueDate": [2016, + 6, + 1], + "daysInPeriod": 31, + "principalOriginalDue": 834.28, + "principalDue": 834.28, + "principalOutstanding": 834.28, + "principalLoanBalanceOutstanding": 4428.47, + "interestOriginalDue": 105.26, + "interestDue": 105.26, + "interestOutstanding": 105.26, + "feeChargesDue": 0, + "penaltyChargesDue": 0, + "totalOriginalDueForPeriod": 939.54, + "totalDueForPeriod": 939.54, + "totalPaidForPeriod": 0, + "totalOutstandingForPeriod": 939.54, + "totalActualCostOfLoanForPeriod": 105.26 + }, + { + "period": 8, + "fromDate": [2016, + 6, + 1], + "dueDate": [2016, + 7, + 1], + "daysInPeriod": 30, + "principalOriginalDue": 850.97, + "principalDue": 850.97, + "principalOutstanding": 850.97, + "principalLoanBalanceOutstanding": 3577.5, + "interestOriginalDue": 88.57, + "interestDue": 88.57, + "interestOutstanding": 88.57, + "feeChargesDue": 0, + "penaltyChargesDue": 0, + "totalOriginalDueForPeriod": 939.54, + "totalDueForPeriod": 939.54, + "totalPaidForPeriod": 0, + "totalOutstandingForPeriod": 939.54, + "totalActualCostOfLoanForPeriod": 88.57 + }, + { + "period": 9, + "fromDate": [2016, + 7, + 1], + "dueDate": [2016, + 8, + 1], + "daysInPeriod": 31, + "principalOriginalDue": 867.99, + "principalDue": 867.99, + "principalOutstanding": 867.99, + "principalLoanBalanceOutstanding": 2709.51, + "interestOriginalDue": 71.55, + "interestDue": 71.55, + "interestOutstanding": 71.55, + "feeChargesDue": 0, + "penaltyChargesDue": 0, + "totalOriginalDueForPeriod": 939.54, + "totalDueForPeriod": 939.54, + "totalPaidForPeriod": 0, + "totalOutstandingForPeriod": 939.54, + "totalActualCostOfLoanForPeriod": 71.55 + }, + { + "period": 10, + "fromDate": [2016, + 8, + 1], + "dueDate": [2016, + 9, + 1], + "daysInPeriod": 31, + "principalOriginalDue": 885.35, + "principalDue": 885.35, + "principalOutstanding": 885.35, + "principalLoanBalanceOutstanding": 1824.16, + "interestOriginalDue": 54.19, + "interestDue": 54.19, + "interestOutstanding": 54.19, + "feeChargesDue": 0, + "penaltyChargesDue": 0, + "totalOriginalDueForPeriod": 939.54, + "totalDueForPeriod": 939.54, + "totalPaidForPeriod": 0, + "totalOutstandingForPeriod": 939.54, + "totalActualCostOfLoanForPeriod": 54.19 + }, + { + "period": 11, + "fromDate": [2016, + 9, + 1], + "dueDate": [2016, + 10, + 10], + "daysInPeriod": 39, + "principalOriginalDue": 903.05, + "principalDue": 903.05, + "principalOutstanding": 903.05, + "principalLoanBalanceOutstanding": 921.11, + "interestOriginalDue": 36.48, + "interestDue": 36.48, + "interestOutstanding": 36.48, + "feeChargesDue": 0, + "penaltyChargesDue": 0, + "totalOriginalDueForPeriod": 939.53, + "totalDueForPeriod": 939.53, + "totalPaidForPeriod": 0, + "totalOutstandingForPeriod": 939.53, + "totalActualCostOfLoanForPeriod": 36.48 + }, + { + "period": 12, + "fromDate": [2016, + 10, + 10], + "dueDate": [2016, + 11, + 1], + "daysInPeriod": 22, + "principalOriginalDue": 921.11, + "principalDue": 921.11, + "principalOutstanding": 921.11, + "principalLoanBalanceOutstanding": 0, + "interestOriginalDue": 18.42, + "interestDue": 18.42, + "interestOutstanding": 18.42, + "feeChargesDue": 0, + "penaltyChargesDue": 0, + "totalOriginalDueForPeriod": 939.53, + "totalDueForPeriod": 939.53, + "totalPaidForPeriod": 0, + "totalOutstandingForPeriod": 939.53, + "totalActualCostOfLoanForPeriod": 18.42 + }] +} + +
+
+ +   +
+
+

Updates loan repayment schedule based on Loan term variations

+ + + + + + + +
Mandatory Fields
exceptions,locale,dateFormat
+
+
+ +POST https://DomainName/api/v1/loans/1/schedule?command=addVariations + + +POST loans/{loanIdd}/schedule?command=addVariations +Content-Type: application/json +Request Body: +{ + "locale": "en", + "dateFormat": "dd MMMM yyyy", + "exceptions": { + "modifiedinstallments": [{ + "dueDate": '01 April 2016', + "modifiedDueDate": '05 April 2016' + }, + { + "dueDate": '05 January 2016', + "modifiedDueDate": '01 January 2016' + }], + "deletedinstallments": [{ + "dueDate": "01 October 2016" + }], + newinstallments: [{ + dueDate: '10 October 2016', + installmentAmount: '1000' +}] + } +} + + +{ + "loanId": 1, + "changes": { + "loanTermVariations": [{ + "id": 21, + "termType": { + "id": 4, + "code": "loanTermType.dueDate", + "value": "dueDate" + }, + "termVariationApplicableFrom": [2016, + 1, + 1], + "dateValue": [2016, + 1, + 10], + "isSpecificToInstallment": true + }, + { + "id": 22, + "termType": { + "id": 3, + "code": "loanTermType.principalAmount", + "value": "principalAmount" + }, + "termVariationApplicableFrom": [2016, + 1, + 10], + "decimalValue": 1100, + "isSpecificToInstallment": true + }, + { + "id": 23, + "termType": { + "id": 3, + "code": "loanTermType.principalAmount", + "value": "principalAmount" + }, + "termVariationApplicableFrom": [2016, + 8, + 1], + "decimalValue": 1000, + "isSpecificToInstallment": true + }, + { + "id": 24, + "termType": { + "id": 5, + "code": "loanTermType.dueDate", + "value": "insertInstallment" + }, + "termVariationApplicableFrom": [2016, + 8, + 15], + "decimalValue": 900, + "isSpecificToInstallment": true + }] + } +} + +
+
+ + +   +
+
+

Updates loan repayment schedule by removing Loan term variations

+ + + + + + + +
Mandatory Fields
+
+
+ +POST https://DomainName/api/v1/loans/1/schedule?command=deleteVariations + + +POST loans/{loanIdd}/schedule?command=deleteVariations +Content-Type: application/json +Request Body: +{} + + +{ + "loanId": 1, + "changes": { + "removedEntityIds": [{ + 21,22}] + } +} + +
+
+ + +  
@@ -40862,6 +41483,30 @@ No Request Body: + + Loan Term Variations + /loans/{loanId}/schedule?command=calculateLoanSchedule + Calculate Schedule with Loan Term Variations + + + + + + + /loans/{loanId}/schedule?command=addVariations + Create Loan Term Variations + + + + + + + /loans/{loanId}/schedule?command=deleteVariations + Remove All Loan Term Variations + + + + General Ledger Account glaccounts 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 46e063c76..a5b8a7c56 100644 --- a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/ClientLoanIntegrationTest.java +++ b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/ClientLoanIntegrationTest.java @@ -3605,9 +3605,9 @@ public class ClientLoanIntegrationTest { List> expectedvalues = new ArrayList<>(); todaysDate = Calendar.getInstance(Utils.getTimeZoneOfTenant()); addRepaymentValues(expectedvalues, todaysDate, -1, false, "2482.76", "46.15", "100.0", "0.0"); - addRepaymentValues(expectedvalues, todaysDate, 1, false, "2486.04", "42.87", "0.0", "0.0"); + addRepaymentValues(expectedvalues, todaysDate, 1, false, "2486.03", "42.88", "0.0", "0.0"); addRepaymentValues(expectedvalues, todaysDate, 1, false, "2497.5", "31.41", "200", "0.0"); - addRepaymentValues(expectedvalues, todaysDate, 1, false, "2533.7", "19.93", "0.0", "0.0"); + addRepaymentValues(expectedvalues, todaysDate, 1, false, "2533.71", "19.93", "0.0", "0.0"); verifyLoanRepaymentSchedule(loanSchedule, expectedvalues); System.out.println("-----------------------------------APPROVE LOAN-----------------------------------------"); @@ -3639,9 +3639,9 @@ public class ClientLoanIntegrationTest { expectedvalues = new ArrayList<>(); todaysDate = Calendar.getInstance(Utils.getTimeZoneOfTenant()); addRepaymentValues(expectedvalues, todaysDate, -1, false, "2482.76", "46.15", "100.0", "0.0"); - addRepaymentValues(expectedvalues, todaysDate, 1, false, "2486.04", "42.87", "0.0", "0.0"); + addRepaymentValues(expectedvalues, todaysDate, 1, false, "2486.03", "42.88", "0.0", "0.0"); addRepaymentValues(expectedvalues, todaysDate, 1, false, "2497.5", "31.41", "200", "0.0"); - addRepaymentValues(expectedvalues, todaysDate, 1, false, "2533.7", "19.93", "0.0", "0.0"); + addRepaymentValues(expectedvalues, todaysDate, 1, false, "2533.71", "19.93", "0.0", "0.0"); verifyLoanRepaymentSchedule(loanSchedule, expectedvalues); Float earlyPayment = new Float("5100"); @@ -3654,7 +3654,7 @@ public class ClientLoanIntegrationTest { expectedvalues = new ArrayList<>(); todaysDate = Calendar.getInstance(Utils.getTimeZoneOfTenant()); addRepaymentValues(expectedvalues, todaysDate, -1, false, "2482.76", "46.15", "100.0", "0.0"); - addRepaymentValues(expectedvalues, todaysDate, 1, false, "5100.0", "36.15", "0.0", "0.0"); + addRepaymentValues(expectedvalues, todaysDate, 1, false, "5100.0", "36.16", "0.0", "0.0"); addRepaymentValues(expectedvalues, todaysDate, 1, false, "0", "11.16", "200", "0.0"); addRepaymentValues(expectedvalues, todaysDate, 1, false, "2417.24", "11.16", "0.0", "0.0"); verifyLoanRepaymentSchedule(loanSchedule, expectedvalues); @@ -3672,7 +3672,7 @@ public class ClientLoanIntegrationTest { @Test public void testLoanScheduleWithInterestRecalculation_WITH_REST_WEEKLY_INTEREST_COMPOUND_INTEREST_FEE_STRATEGY_REDUCE_NEXT_INSTALLMENTS_PRE_CLOSE_INTEREST_PRE_CLOSE_DATE() { String preCloseInterestStrategy = LoanProductTestBuilder.INTEREST_APPLICABLE_STRATEGY_ON_PRE_CLOSE_DATE; - String preCloseAmount = "7766.81"; + String preCloseAmount = "7766.82"; testLoanScheduleWithInterestRecalculation_WITH_REST_WEEKLY_INTEREST_COMPOUND_INTEREST_FEE_STRATEGY_REDUCE_NEXT_INSTALLMENTS_PRE_CLOSE_INTEREST( preCloseInterestStrategy, preCloseAmount); @@ -3681,7 +3681,7 @@ public class ClientLoanIntegrationTest { @Test public void testLoanScheduleWithInterestRecalculation_WITH_REST_WEEKLY_INTEREST_COMPOUND_INTEREST_FEE_STRATEGY_REDUCE_NEXT_INSTALLMENTS_PRE_CLOSE_INTEREST_REST_DATE() { String preCloseInterestStrategy = LoanProductTestBuilder.INTEREST_APPLICABLE_STRATEGY_REST_DATE; - String preCloseAmount = "7771.79"; + String preCloseAmount = "7771.8"; testLoanScheduleWithInterestRecalculation_WITH_REST_WEEKLY_INTEREST_COMPOUND_INTEREST_FEE_STRATEGY_REDUCE_NEXT_INSTALLMENTS_PRE_CLOSE_INTEREST( preCloseInterestStrategy, preCloseAmount); @@ -3734,9 +3734,9 @@ public class ClientLoanIntegrationTest { List> expectedvalues = new ArrayList<>(); todaysDate = Calendar.getInstance(Utils.getTimeZoneOfTenant()); addRepaymentValues(expectedvalues, todaysDate, -9, true, "2482.76", "46.15", "100.0", "0.0"); - addRepaymentValues(expectedvalues, todaysDate, 1, false, "2489.31", "39.6", "0.0", "0.0"); + addRepaymentValues(expectedvalues, todaysDate, 1, false, "2489.3", "39.61", "0.0", "0.0"); addRepaymentValues(expectedvalues, todaysDate, 1, false, "2500.78", "28.13", "200", "0.0"); - addRepaymentValues(expectedvalues, todaysDate, 1, false, "2527.15", "16.61", "0.0", "0.0"); + addRepaymentValues(expectedvalues, todaysDate, 1, false, "2527.16", "16.62", "0.0", "0.0"); verifyLoanRepaymentSchedule(loanSchedule, expectedvalues); System.out.println("-----------------------------------APPROVE LOAN-----------------------------------------"); @@ -3768,9 +3768,9 @@ public class ClientLoanIntegrationTest { expectedvalues = new ArrayList<>(); todaysDate = Calendar.getInstance(Utils.getTimeZoneOfTenant()); addRepaymentValues(expectedvalues, todaysDate, -9, true, "2482.76", "46.15", "100.0", "0.0"); - addRepaymentValues(expectedvalues, todaysDate, 1, false, "2489.31", "39.6", "0.0", "0.0"); + addRepaymentValues(expectedvalues, todaysDate, 1, false, "2489.3", "39.61", "0.0", "0.0"); addRepaymentValues(expectedvalues, todaysDate, 1, false, "2500.7", "28.21", "200", "0.0"); - addRepaymentValues(expectedvalues, todaysDate, 1, false, "2527.23", "16.61", "0.0", "0.0"); + addRepaymentValues(expectedvalues, todaysDate, 1, false, "2527.24", "16.62", "0.0", "0.0"); verifyLoanRepaymentSchedule(loanSchedule, expectedvalues); HashMap prepayDetail = this.loanTransactionHelper.getPrepayAmount(this.requestSpec, this.responseSpec, loanID); diff --git a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/SchedulerJobsTestResults.java b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/SchedulerJobsTestResults.java index 3f8c94546..2b6428cfd 100644 --- a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/SchedulerJobsTestResults.java +++ b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/SchedulerJobsTestResults.java @@ -19,6 +19,7 @@ import java.util.Locale; import org.junit.Assert; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.mifosplatform.integrationtests.common.ClientHelper; import org.mifosplatform.integrationtests.common.GlobalConfigurationHelper; @@ -503,6 +504,9 @@ public class SchedulerJobsTestResults { } + // Invalid test case as it won't affect summary (Loan summary is properly + // updated before running this job) + @Ignore @Test public void testUpdateLoanSummaryJobOutcome() throws InterruptedException { this.schedulerJobHelper = new SchedulerJobHelper(this.requestSpec, this.responseSpec); @@ -552,7 +556,7 @@ public class SchedulerJobsTestResults { String JobName = "Update loan Summary"; this.schedulerJobHelper.executeJob(JobName); Float expectedSummaryAfterJob = (Float) loanSummaryBefore.get("totalExpectedRepayment") - - (Float) loanSummaryBefore.get("feeChargesPaid"); + /* - (Float) loanSummaryBefore.get("feeChargesPaid")*/; HashMap loanSummaryAfter = this.loanTransactionHelper.getLoanSummary(this.requestSpec, this.responseSpec, loanID); Assert.assertEquals("Verifying Loan Summary after Running Update Loan Summary Scheduler Job", expectedSummaryAfterJob, (Float) loanSummaryAfter.get("totalExpectedRepayment")); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java index 32c3ec403..68fc7b528 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java @@ -30,8 +30,8 @@ public class CommandWrapperBuilder { private Long templateId; public CommandWrapper build() { - return new CommandWrapper(this.officeId, this.groupId, this.clientId, this.loanId, this.savingsId, this.actionName, this.entityName, - this.entityId, this.subentityId, this.href, this.json, this.transactionId, this.productId, this.templateId); + return new CommandWrapper(this.officeId, this.groupId, this.clientId, this.loanId, this.savingsId, this.actionName, + this.entityName, this.entityId, this.subentityId, this.href, this.json, this.transactionId, this.productId, this.templateId); } public CommandWrapperBuilder withLoanId(final Long withLoanId) { @@ -1337,7 +1337,7 @@ public class CommandWrapperBuilder { this.actionName = "UPDATE"; this.entityName = "CALENDAR"; this.entityId = calendarId; - this.groupId = supportedEntityId; + this.groupId = supportedEntityId; this.href = "/" + supportedEntityType + "/" + supportedEntityId + "/calendars/" + calendarId; return this; } @@ -1815,8 +1815,7 @@ public class CommandWrapperBuilder { return this; } - public CommandWrapperBuilder saveOrUpdateAttendance(final Long entityId, final String supportedEntityType, - final Long supportedEntityId) { + public CommandWrapperBuilder saveOrUpdateAttendance(final Long entityId, final String supportedEntityType, final Long supportedEntityId) { this.actionName = "SAVEORUPDATEATTENDANCE"; this.entityName = "MEETING"; this.entityId = entityId; @@ -2457,7 +2456,7 @@ public class CommandWrapperBuilder { this.href = "/clients/" + clientId + "/transactions/" + transactionId + "?command=undo"; return this; } - + public CommandWrapperBuilder createProvisioningCategory() { this.actionName = "CREATE"; this.entityName = "PROVISIONCATEGORY"; @@ -2465,7 +2464,7 @@ public class CommandWrapperBuilder { this.href = "/provisioningcategory"; return this; } - + public CommandWrapperBuilder updateProvisioningCategory(final Long cateoryId) { this.actionName = "UPDATE"; this.entityName = "PROVISIONCATEGORY"; @@ -2473,7 +2472,7 @@ public class CommandWrapperBuilder { this.href = "/provisioningcategory/" + cateoryId; return this; } - + public CommandWrapperBuilder deleteProvisioningCategory(final Long categoryId) { this.actionName = "DELETE"; this.entityName = "PROVISIONCATEGORY"; @@ -2481,7 +2480,7 @@ public class CommandWrapperBuilder { this.href = "/provisioningcategory/" + categoryId; return this; } - + public CommandWrapperBuilder createProvisioningCriteria() { this.actionName = "CREATE"; this.entityName = "PROVISIONCRITERIA"; @@ -2489,7 +2488,7 @@ public class CommandWrapperBuilder { this.href = "/provisioningcriteria"; return this; } - + public CommandWrapperBuilder updateProvisioningCriteria(final Long criteriaId) { this.actionName = "UPDATE"; this.entityName = "PROVISIONCRITERIA"; @@ -2497,7 +2496,7 @@ public class CommandWrapperBuilder { this.href = "/provisioningcriteria/" + criteriaId; return this; } - + public CommandWrapperBuilder deleteProvisioningCriteria(final Long criteriaId) { this.actionName = "DELETE"; this.entityName = "PROVISIONCRITERIA"; @@ -2505,7 +2504,7 @@ public class CommandWrapperBuilder { this.href = "/provisioningcriteria/" + criteriaId; return this; } - + public CommandWrapperBuilder createProvisioningEntries() { this.actionName = "CREATE"; this.entityName = "PROVISIONENTRIES"; @@ -2513,20 +2512,20 @@ public class CommandWrapperBuilder { this.href = "/provisioningentries"; return this; } - + public CommandWrapperBuilder createProvisioningJournalEntries(final Long entryId) { this.actionName = "CREATE"; this.entityName = "PROVISIONJOURNALENTRIES"; this.entityId = entryId; - this.href = "/provisioningentries/"+entryId; + this.href = "/provisioningentries/" + entryId; return this; } - + public CommandWrapperBuilder reCreateProvisioningEntries(final Long entryId) { this.actionName = "RECREATE"; this.entityName = "PROVISIONENTRIES"; this.entityId = entryId; - this.href = "/provisioningentries/"+entryId; + this.href = "/provisioningentries/" + entryId; return this; } @@ -2542,7 +2541,25 @@ public class CommandWrapperBuilder { this.actionName = "UPDATE"; this.entityName = "FLOATINGRATE"; this.entityId = floatingRateId; - this.href = "/floatingrates/"+floatingRateId; + this.href = "/floatingrates/" + floatingRateId; + return this; + } + + public CommandWrapperBuilder createScheduleExceptions(final Long loanId) { + this.actionName = "CREATESCHEDULEEXCEPTIONS"; + this.entityName = "LOAN"; + this.entityId = loanId; + this.loanId = loanId; + this.href = "/loans/" + loanId + "/schedule"; + return this; + } + + public CommandWrapperBuilder deleteScheduleExceptions(final Long loanId) { + this.actionName = "DELETESCHEDULEEXCEPTIONS"; + this.entityName = "LOAN"; + this.entityId = loanId; + this.loanId = loanId; + this.href = "/loans/" + loanId + "/schedule"; return this; } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/organisation/monetary/service/CurrencyReadPlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/organisation/monetary/service/CurrencyReadPlatformService.java index e77d4110c..836779b50 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/organisation/monetary/service/CurrencyReadPlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/organisation/monetary/service/CurrencyReadPlatformService.java @@ -14,4 +14,6 @@ public interface CurrencyReadPlatformService { Collection retrieveAllowedCurrencies(); Collection retrieveAllPlatformCurrencies(); + + CurrencyData retrieveCurrency(String code); } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/organisation/monetary/service/CurrencyReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/organisation/monetary/service/CurrencyReadPlatformServiceImpl.java index 3a7ca3aaf..18c856377 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/organisation/monetary/service/CurrencyReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/organisation/monetary/service/CurrencyReadPlatformServiceImpl.java @@ -50,6 +50,14 @@ public class CurrencyReadPlatformServiceImpl implements CurrencyReadPlatformServ return this.jdbcTemplate.query(sql, this.currencyRowMapper, new Object[] {}); } + @Override + public CurrencyData retrieveCurrency(final String code) { + + final String sql = "select " + this.currencyRowMapper.schema() + " from m_currency c where c.code = ? order by c.name"; + + return this.jdbcTemplate.queryForObject(sql, this.currencyRowMapper, new Object[] { code }); + } + private static final class CurrencyMapper implements RowMapper { @Override diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/service/CalendarUtils.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/service/CalendarUtils.java index c840998da..bab40add2 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/service/CalendarUtils.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/service/CalendarUtils.java @@ -45,7 +45,42 @@ public class CalendarUtils { public static LocalDate getNextRecurringDate(final String recurringRule, final LocalDate seedDate, final LocalDate startDate) { final Recur recur = CalendarUtils.getICalRecur(recurringRule); if (recur == null) { return null; } - return getNextRecurringDate(recur, seedDate, startDate); + LocalDate nextDate = getNextRecurringDate(recur, seedDate, startDate); + nextDate = adjustDate(nextDate, seedDate, getMeetingPeriodFrequencyType(recurringRule)); + return nextDate; + } + + public static LocalDate adjustDate(final LocalDate date, final LocalDate seedDate, final PeriodFrequencyType frequencyType) { + LocalDate adjustedVal = date; + if (frequencyType.isMonthly() && seedDate.getDayOfMonth() > 28) { + switch (date.getMonthOfYear()) { + case 2: + if (date.year().isLeap()) { + adjustedVal = date.dayOfMonth().setCopy(29); + } + break; + case 4: + case 6: + case 9: + case 11: + if (seedDate.getDayOfMonth() > 30) { + adjustedVal = date.dayOfMonth().setCopy(30); + } else { + adjustedVal = date.dayOfMonth().setCopy(seedDate.getDayOfMonth()); + } + break; + case 1: + case 3: + case 5: + case 7: + case 8: + case 10: + case 12: + adjustedVal = date.dayOfMonth().setCopy(seedDate.getDayOfMonth()); + break; + } + } + return adjustedVal; } private static LocalDate getNextRecurringDate(final Recur recur, final LocalDate seedDate, final LocalDate startDate) { @@ -105,17 +140,18 @@ public class CalendarUtils { final Value value = new Value(Value.DATE.getValue()); final DateList recurringDates = recur.getDates(seed, periodStart, periodEnd, value, maxCount); - return convertToLocalDateList(recurringDates); + return convertToLocalDateList(recurringDates, seedDate, getMeetingPeriodFrequencyType(recur)); } - private static Collection convertToLocalDateList(final DateList dates) { + private static Collection convertToLocalDateList(final DateList dates, final LocalDate seedDate, + final PeriodFrequencyType frequencyType) { final Collection recurringDates = new ArrayList<>(); for (@SuppressWarnings("rawtypes") final Iterator iterator = dates.iterator(); iterator.hasNext();) { final Date date = (Date) iterator.next(); - recurringDates.add(new LocalDate(date)); + recurringDates.add(adjustDate(new LocalDate(date), seedDate, frequencyType)); } return recurringDates; @@ -259,6 +295,10 @@ public class CalendarUtils { public static PeriodFrequencyType getMeetingPeriodFrequencyType(final String recurringRule) { final Recur recur = CalendarUtils.getICalRecur(recurringRule); + return getMeetingPeriodFrequencyType(recur); + } + + private static PeriodFrequencyType getMeetingPeriodFrequencyType(final Recur recur) { PeriodFrequencyType meetingFrequencyType = PeriodFrequencyType.INVALID; if (recur.getFrequency().equals(Recur.DAILY)) { meetingFrequencyType = PeriodFrequencyType.DAYS; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/serialization/ChargeDefinitionCommandFromApiJsonDeserializer.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/serialization/ChargeDefinitionCommandFromApiJsonDeserializer.java index 3016fc567..ed3c06936 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/serialization/ChargeDefinitionCommandFromApiJsonDeserializer.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/serialization/ChargeDefinitionCommandFromApiJsonDeserializer.java @@ -40,10 +40,10 @@ public final class ChargeDefinitionCommandFromApiJsonDeserializer { /** * The parameters supported for this command. */ - private final Set supportedParameters = new HashSet<>( - Arrays.asList("name", "amount", "locale", "currencyCode", "currencyOptions", "chargeAppliesTo", "chargeTimeType", - "chargeCalculationType", "chargeCalculationTypeOptions", "penalty", "active", "chargePaymentMode", "feeOnMonthDay", - "feeInterval", "monthDayFormat", "minCap", "maxCap", "feeFrequency", ChargesApiConstants.glAccountIdParamName)); + private final Set supportedParameters = new HashSet<>(Arrays.asList("name", "amount", "locale", "currencyCode", + "currencyOptions", "chargeAppliesTo", "chargeTimeType", "chargeCalculationType", "chargeCalculationTypeOptions", "penalty", + "active", "chargePaymentMode", "feeOnMonthDay", "feeInterval", "monthDayFormat", "minCap", "maxCap", "feeFrequency", + ChargesApiConstants.glAccountIdParamName)); private final FromJsonHelper fromApiJsonHelper; @@ -104,9 +104,9 @@ public final class ChargeDefinitionCommandFromApiJsonDeserializer { baseDataValidator.reset().parameter("chargeCalculationType").value(chargeCalculationType) .isOneOfTheseValues(ChargeCalculationType.validValuesForLoan()); } - - if(chargeTimeType != null && chargeCalculationType != null){ - performChargeTimeNCalculationTypeValidation(baseDataValidator, chargeTimeType, chargeCalculationType); + + if (chargeTimeType != null && chargeCalculationType != null) { + performChargeTimeNCalculationTypeValidation(baseDataValidator, chargeTimeType, chargeCalculationType); } } else if (appliesTo.isSavingsCharge()) { @@ -122,8 +122,8 @@ public final class ChargeDefinitionCommandFromApiJsonDeserializer { if (ctt.isWeeklyFee()) { final String monthDay = this.fromApiJsonHelper.extractStringNamed("feeOnMonthDay", element); - baseDataValidator.reset().parameter("feeOnMonthDay").value(monthDay).mustBeBlankWhenParameterProvidedIs("chargeTimeType", - chargeTimeType); + baseDataValidator.reset().parameter("feeOnMonthDay").value(monthDay) + .mustBeBlankWhenParameterProvidedIs("chargeTimeType", chargeTimeType); } if (ctt.isMonthlyFee()) { @@ -305,26 +305,26 @@ public final class ChargeDefinitionCommandFromApiJsonDeserializer { throwExceptionIfValidationWarningsExist(dataValidationErrors); } - - public void validateChargeTimeNCalculationType(Integer chargeTimeType, Integer ChargeCalculationType) { + + public void validateChargeTimeNCalculationType(Integer chargeTimeType, Integer ChargeCalculationType) { final List dataValidationErrors = new ArrayList<>(); final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("charge"); performChargeTimeNCalculationTypeValidation(baseDataValidator, chargeTimeType, ChargeCalculationType); throwExceptionIfValidationWarningsExist(dataValidationErrors); - } - - private void performChargeTimeNCalculationTypeValidation(DataValidatorBuilder baseDataValidator, - final Integer chargeTimeType, final Integer chargeCalculationType){ - if (chargeTimeType == ChargeTimeType.TRANCHE_DISBURSEMENT.getValue()){ - baseDataValidator.reset().parameter("chargeCalculationType").value(chargeCalculationType) - .isOneOfTheseValues(ChargeCalculationType.validValuesForTrancheDisbursement()); - } else { - baseDataValidator.reset().parameter("chargeCalculationType").value(chargeCalculationType) - .isNotOneOfTheseValues(ChargeCalculationType.PERCENT_OF_DISBURSEMENT_AMOUNT.getValue()); - } } - private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { + private void performChargeTimeNCalculationTypeValidation(DataValidatorBuilder baseDataValidator, final Integer chargeTimeType, + final Integer chargeCalculationType) { + if (chargeTimeType.equals(ChargeTimeType.TRANCHE_DISBURSEMENT.getValue())) { + baseDataValidator.reset().parameter("chargeCalculationType").value(chargeCalculationType) + .isOneOfTheseValues(ChargeCalculationType.validValuesForTrancheDisbursement()); + } else { + baseDataValidator.reset().parameter("chargeCalculationType").value(chargeCalculationType) + .isNotOneOfTheseValues(ChargeCalculationType.PERCENT_OF_DISBURSEMENT_AMOUNT.getValue()); + } + } + + private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/service/ChargeEnumerations.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/service/ChargeEnumerations.java index 6cc8493f5..1328b1fd4 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/service/ChargeEnumerations.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/service/ChargeEnumerations.java @@ -113,7 +113,7 @@ public class ChargeEnumerations { break; case PERCENT_OF_AMOUNT: optionData = new EnumOptionData(ChargeCalculationType.PERCENT_OF_AMOUNT.getValue().longValue(), - ChargeCalculationType.PERCENT_OF_AMOUNT.getCode(), "% Approved Amount"); + ChargeCalculationType.PERCENT_OF_AMOUNT.getCode(), "% Amount"); break; case PERCENT_OF_AMOUNT_AND_INTEREST: optionData = new EnumOptionData(ChargeCalculationType.PERCENT_OF_AMOUNT_AND_INTEREST.getValue().longValue(), diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoanApiConstants.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoanApiConstants.java index b9edea8a0..583fcbb10 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoanApiConstants.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoanApiConstants.java @@ -17,8 +17,8 @@ public interface LoanApiConstants { public static final String disbursementIdParameterName = "id"; public static final String loanChargeIdParameterName = "loanChargeId"; public static final String principalDisbursedParameterName = "transactionAmount"; - public static final String chargesParameterName = "charges" ; - + public static final String chargesParameterName = "charges"; + public static final String approvedLoanAmountParameterName = "approvedLoanAmount"; public static final String approvedOnDateParameterName = "approvedOnDate"; public static final String noteParameterName = "note"; @@ -33,14 +33,14 @@ public interface LoanApiConstants { public static final String daysInMonthTypeParameterName = "daysInMonthType"; public static final String interestRecalculationCompoundingMethodParameterName = "interestRecalculationCompoundingMethod"; public static final String rescheduleStrategyMethodParameterName = "rescheduleStrategyMethod"; - + // Floating interest rate related public static final String interestRateDifferentialParameterName = "interestRateDifferential"; public static final String isFloatingInterestRateParameterName = "isFloatingInterestRate"; - + // Error codes - public static final String LOAN_CHARGE_CAN_NOT_BE_ADDED_WITH_INTEREST_CALCULATION_TYPE= "loancharge.with.calculation.type.interest.not.allowed"; - public static final String LOAN_CHARGE_CAN_NOT_BE_ADDED_WITH_PRINCIPAL_CALCULATION_TYPE= "loancharge.with.calculation.type.principal.not.allowed"; + public static final String LOAN_CHARGE_CAN_NOT_BE_ADDED_WITH_INTEREST_CALCULATION_TYPE = "loancharge.with.calculation.type.interest.not.allowed"; + public static final String LOAN_CHARGE_CAN_NOT_BE_ADDED_WITH_PRINCIPAL_CALCULATION_TYPE = "loancharge.with.calculation.type.principal.not.allowed"; public static final String DISBURSEMENT_DATE_START_WITH_ERROR = "first.disbursement.date.must.start.with.expected.disbursement.date"; public static final String PRINCIPAL_AMOUNT_SHOULD_BE_SAME = "sum.of.multi.disburse.amounts.must.equal.with.total.principal"; public static final String DISBURSEMENT_DATE_UNIQUE_ERROR = "disbursement.date.must.be.unique.for.tranches"; @@ -48,8 +48,17 @@ public interface LoanApiConstants { public static final String APPROVED_AMOUNT_IS_LESS_THAN_SUM_OF_TRANCHES = "sum.of.multi.disburse.amounts.must.be.equal.to.or.lesser.than.approved.principal"; public static final String DISBURSEMENT_DATES_NOT_IN_ORDER = "disbursements.should.be.ordered.based.on.their.disbursement.dates"; public static final String DISBURSEMENT_DATE_BEFORE_ERROR = "disbursement.date.of.tranche.cannot.be.before.expected.disbursement.date"; - + public static final String isFloatingInterestRate = "isFloatingInterestRate"; - public static final String interestRateDifferential = "interestRateDifferential"; + public static final String interestRateDifferential = "interestRateDifferential"; + + public static final String exceptionParamName = "exceptions"; + public static final String modifiedinstallmentsParamName = "modifiedinstallments"; + public static final String newinstallmentsParamName = "newinstallments"; + public static final String deletedinstallmentsParamName = "deletedinstallments"; + public static final String dueDateParamName = "dueDate"; + public static final String modifiedDueDateParamName = "modifiedDueDate"; + public static final String principalParamName = "principal"; + public static final String installmentAmountParamName = "installmentAmount"; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoanScheduleApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoanScheduleApiResource.java new file mode 100644 index 000000000..f8f74e91e --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoanScheduleApiResource.java @@ -0,0 +1,89 @@ +/** + * 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.api; + +import java.util.HashSet; + +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.UriInfo; + +import org.apache.commons.lang.StringUtils; +import org.mifosplatform.commands.domain.CommandWrapper; +import org.mifosplatform.commands.service.CommandWrapperBuilder; +import org.mifosplatform.commands.service.PortfolioCommandSourceWritePlatformService; +import org.mifosplatform.infrastructure.core.api.ApiRequestParameterHelper; +import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; +import org.mifosplatform.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; +import org.mifosplatform.infrastructure.core.serialization.DefaultToApiJsonSerializer; +import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; +import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData; +import org.mifosplatform.portfolio.loanaccount.loanschedule.service.LoanScheduleCalculationPlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +@Path("/loans/{loanId}/schedule") +@Component +@Scope("singleton") +public class LoanScheduleApiResource { + + private final String resourceNameForPermissions = "LOAN"; + private final PlatformSecurityContext context; + private final DefaultToApiJsonSerializer toApiJsonSerializer; + private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; + private final LoanScheduleCalculationPlatformService calculationPlatformService; + private final ApiRequestParameterHelper apiRequestParameterHelper; + + @Autowired + public LoanScheduleApiResource(final PlatformSecurityContext context, + final DefaultToApiJsonSerializer toApiJsonSerializer, + final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService, + final LoanScheduleCalculationPlatformService calculationPlatformService, + final ApiRequestParameterHelper apiRequestParameterHelper) { + this.context = context; + this.toApiJsonSerializer = toApiJsonSerializer; + this.commandsSourceWritePlatformService = commandsSourceWritePlatformService; + this.calculationPlatformService = calculationPlatformService; + this.apiRequestParameterHelper = apiRequestParameterHelper; + } + + @POST + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String calculateLoanScheduleOrSubmitVariableSchedule(@PathParam("loanId") final Long loanId, + @QueryParam("command") final String commandParam, @Context final UriInfo uriInfo, final String apiRequestBodyAsJson) { + + CommandWrapper commandRequest = null; + if (is(commandParam, "calculateLoanSchedule")) { + this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions); + final LoanScheduleData loanSchedule = this.calculationPlatformService.generateLoanScheduleForVariableInstallmentRequest(loanId, + apiRequestBodyAsJson); + + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.toApiJsonSerializer.serialize(settings, loanSchedule, new HashSet()); + } else if (is(commandParam, "addVariations")) { + commandRequest = new CommandWrapperBuilder().createScheduleExceptions(loanId).withJson(apiRequestBodyAsJson).build(); + } else if (is(commandParam, "deleteVariations")) { + commandRequest = new CommandWrapperBuilder().deleteScheduleExceptions(loanId).build(); + } + + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return this.toApiJsonSerializer.serialize(result); + } + + private boolean is(final String commandParam, final String commandValue) { + return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue); + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanAccountData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanAccountData.java index cba49d793..f1522246c 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanAccountData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanAccountData.java @@ -186,9 +186,14 @@ public class LoanAccountData { // Paid In Advance private final PaidInAdvanceData paidInAdvance; - + private final Collection interestRatesPeriods; + // VariableInstallments + private final Boolean isVariableInstallmentsAllowed; + private final Integer minimumGap; + private final Integer maximumGap; + /** * Used to produce a {@link LoanAccountData} with only collateral options * for now. @@ -293,14 +298,16 @@ public class LoanAccountData { final Boolean createStandingInstructionAtDisbursement = null; final PaidInAdvanceData paidInAdvance = null; final Collection interestRatesPeriods = null; - + final Boolean isVariableInstallmentsAllowed = Boolean.FALSE; + final Integer minimumGap = null; + final Integer maximumGap = null; return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, group, loanType, loanProductId, - loanProductName, loanProductDescription, isLoanProductLinkedToFloatingRate, fundId, fundName, loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName, - currencyData, proposedPrincipal, principal, principal, totalOverpaid, inArrearsTolerance, termFrequency, - termPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentFrequencyType, repaymentFrequencyNthDayType, - repaymentFrequencyDayOfWeekType, transactionProcessingStrategyId, transactionProcessingStrategyName, amortizationType, - interestRatePerPeriod, interestRateFrequencyType, annualInterestRate, interestType, isFloatingInterestRate, - interestRateDifferential, interestCalculationPeriodType, + loanProductName, loanProductDescription, isLoanProductLinkedToFloatingRate, fundId, fundName, loanPurposeId, + loanPurposeName, loanOfficerId, loanOfficerName, currencyData, proposedPrincipal, principal, principal, totalOverpaid, + inArrearsTolerance, termFrequency, termPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentFrequencyType, + repaymentFrequencyNthDayType, repaymentFrequencyDayOfWeekType, transactionProcessingStrategyId, + transactionProcessingStrategyName, amortizationType, interestRatePerPeriod, interestRateFrequencyType, annualInterestRate, + interestType, isFloatingInterestRate, interestRateDifferential, interestCalculationPeriodType, expectedFirstRepaymentOnDate, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, timeline, summary, feeChargesDueAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, calendarData, productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, @@ -311,7 +318,8 @@ public class LoanAccountData { disbursementData, multiDisburseLoan, canDefineInstallmentAmount, fixedEmiAmount, maxOutstandingLoanBalance, emiAmountVariations, memberVariations, product, inArrears, graceOnArrearsAgeing, overdueCharges, isNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, originalSchedule, - createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods); + createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods, isVariableInstallmentsAllowed, minimumGap, + maximumGap); } @@ -420,13 +428,17 @@ public class LoanAccountData { final PaidInAdvanceData paidInAdvance = null; final Collection interestRatesPeriods = null; + final Boolean isVariableInstallmentsAllowed = Boolean.FALSE; + final Integer minimumGap = null; + final Integer maximumGap = null; + return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, group, loanType, loanProductId, - loanProductName, loanProductDescription, isLoanProductLinkedToFloatingRate, fundId, fundName, loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName, - currencyData, proposedPrincipal, principal, principal, totalOverpaid, inArrearsTolerance, termFrequency, - termPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentFrequencyType, repaymentFrequencyNthDayType, - repaymentFrequencyDayOfWeekType, transactionProcessingStrategyId, transactionProcessingStrategyName, amortizationType, - interestRatePerPeriod, interestRateFrequencyType, annualInterestRate, interestType, - isFloatingInterestRate, interestRateDifferential, interestCalculationPeriodType, + loanProductName, loanProductDescription, isLoanProductLinkedToFloatingRate, fundId, fundName, loanPurposeId, + loanPurposeName, loanOfficerId, loanOfficerName, currencyData, proposedPrincipal, principal, principal, totalOverpaid, + inArrearsTolerance, termFrequency, termPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentFrequencyType, + repaymentFrequencyNthDayType, repaymentFrequencyDayOfWeekType, transactionProcessingStrategyId, + transactionProcessingStrategyName, amortizationType, interestRatePerPeriod, interestRateFrequencyType, annualInterestRate, + interestType, isFloatingInterestRate, interestRateDifferential, interestCalculationPeriodType, expectedFirstRepaymentOnDate, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, timeline, summary, feeChargesDueAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, calendarData, productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, @@ -437,34 +449,36 @@ public class LoanAccountData { disbursementData, multiDisburseLoan, canDefineInstallmentAmount, fixedEmiAmount, maxOutstandingLoanBalance, emiAmountVariations, memberVariations, product, inArrears, graceOnArrearsAgeing, overdueCharges, isNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, originalSchedule, - createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods); + createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods, isVariableInstallmentsAllowed, minimumGap, + maximumGap); } public static LoanAccountData populateClientDefaults(final LoanAccountData acc, final LoanAccountData clientAcc) { return new LoanAccountData(acc.id, acc.accountNo, acc.status, acc.externalId, clientAcc.clientId, clientAcc.clientName, - clientAcc.clientOfficeId, acc.group, acc.loanType, acc.loanProductId, acc.loanProductName, acc.loanProductDescription, acc.isLoanProductLinkedToFloatingRate, - acc.fundId, acc.fundName, acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, acc.currency, - acc.proposedPrincipal, acc.principal, acc.approvedPrincipal, acc.totalOverpaid, acc.inArrearsTolerance, acc.termFrequency, - acc.termPeriodFrequencyType, acc.numberOfRepayments, acc.repaymentEvery, acc.repaymentFrequencyType, - acc.repaymentFrequencyNthDayType, acc.repaymentFrequencyDayOfWeekType, acc.transactionProcessingStrategyId, - acc.transactionProcessingStrategyName, acc.amortizationType, acc.interestRatePerPeriod, acc.interestRateFrequencyType, - acc.annualInterestRate, acc.interestType, acc.isFloatingInterestRate, acc.interestRateDifferential, - acc.interestCalculationPeriodType, acc.expectedFirstRepaymentOnDate, - acc.graceOnPrincipalPayment, acc.graceOnInterestPayment, acc.graceOnInterestCharged, acc.interestChargedFromDate, - clientAcc.timeline, acc.summary, acc.feeChargesAtDisbursementCharged, acc.repaymentSchedule, acc.transactions, acc.charges, - acc.collateral, acc.guarantors, acc.meeting, acc.productOptions, acc.termFrequencyTypeOptions, - acc.repaymentFrequencyTypeOptions, acc.repaymentFrequencyNthDayTypeOptions, acc.repaymentFrequencyDaysOfWeekTypeOptions, - acc.transactionProcessingStrategyOptions, acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions, - acc.interestTypeOptions, acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, - acc.loanOfficerOptions, acc.loanPurposeOptions, acc.loanCollateralOptions, acc.calendarOptions, - acc.syncDisbursementWithMeeting, acc.loanCounter, acc.loanProductCounter, acc.notes, acc.accountLinkingOptions, - acc.linkedAccount, acc.disbursementDetails, acc.multiDisburseLoan, acc.canDefineInstallmentAmount, acc.fixedEmiAmount, - acc.maxOutstandingLoanBalance, acc.emiAmountVariations, acc.memberVariations, acc.product, acc.inArrears, - acc.graceOnArrearsAgeing, acc.overdueCharges, acc.isNPA, acc.daysInMonthType, acc.daysInYearType, - acc.isInterestRecalculationEnabled, acc.interestRecalculationData, acc.originalSchedule, - acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods); + clientAcc.clientOfficeId, acc.group, acc.loanType, acc.loanProductId, acc.loanProductName, acc.loanProductDescription, + acc.isLoanProductLinkedToFloatingRate, acc.fundId, acc.fundName, acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, + acc.loanOfficerName, acc.currency, acc.proposedPrincipal, acc.principal, acc.approvedPrincipal, acc.totalOverpaid, + acc.inArrearsTolerance, acc.termFrequency, acc.termPeriodFrequencyType, acc.numberOfRepayments, acc.repaymentEvery, + acc.repaymentFrequencyType, acc.repaymentFrequencyNthDayType, acc.repaymentFrequencyDayOfWeekType, + acc.transactionProcessingStrategyId, acc.transactionProcessingStrategyName, acc.amortizationType, + acc.interestRatePerPeriod, acc.interestRateFrequencyType, acc.annualInterestRate, acc.interestType, + acc.isFloatingInterestRate, acc.interestRateDifferential, acc.interestCalculationPeriodType, + acc.expectedFirstRepaymentOnDate, acc.graceOnPrincipalPayment, acc.graceOnInterestPayment, acc.graceOnInterestCharged, + acc.interestChargedFromDate, clientAcc.timeline, acc.summary, acc.feeChargesAtDisbursementCharged, acc.repaymentSchedule, + acc.transactions, acc.charges, acc.collateral, acc.guarantors, acc.meeting, acc.productOptions, + acc.termFrequencyTypeOptions, acc.repaymentFrequencyTypeOptions, acc.repaymentFrequencyNthDayTypeOptions, + acc.repaymentFrequencyDaysOfWeekTypeOptions, acc.transactionProcessingStrategyOptions, + acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions, acc.interestTypeOptions, + acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, acc.loanOfficerOptions, + acc.loanPurposeOptions, acc.loanCollateralOptions, acc.calendarOptions, acc.syncDisbursementWithMeeting, acc.loanCounter, + acc.loanProductCounter, acc.notes, acc.accountLinkingOptions, acc.linkedAccount, acc.disbursementDetails, + acc.multiDisburseLoan, acc.canDefineInstallmentAmount, acc.fixedEmiAmount, acc.maxOutstandingLoanBalance, + acc.emiAmountVariations, acc.memberVariations, acc.product, acc.inArrears, acc.graceOnArrearsAgeing, acc.overdueCharges, + acc.isNPA, acc.daysInMonthType, acc.daysInYearType, acc.isInterestRecalculationEnabled, acc.interestRecalculationData, + acc.originalSchedule, acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods, + acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap); } /** @@ -573,13 +587,17 @@ public class LoanAccountData { final PaidInAdvanceData paidInAdvance = null; final Collection interestRatesPeriods = null; + final Boolean isVariableInstallmentsAllowed = Boolean.FALSE; + final Integer minimumGap = null; + final Integer maximumGap = null; + return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, group, loanType, loanProductId, - loanProductName, loanProductDescription, isLoanProductLinkedToFloatingRate, fundId, fundName, loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName, - currencyData, proposedPrincipal, principal, principal, totalOverpaid, inArrearsTolerance, termFrequency, - termPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentFrequencyType, repaymentFrequencyNthDayType, - repaymentFrequencyDayOfWeekType, transactionProcessingStrategyId, transactionProcessingStrategyName, amortizationType, - interestRatePerPeriod, interestRateFrequencyType, annualInterestRate, interestType, - isFloatingInterestRate, interestRateDifferential, interestCalculationPeriodType, + loanProductName, loanProductDescription, isLoanProductLinkedToFloatingRate, fundId, fundName, loanPurposeId, + loanPurposeName, loanOfficerId, loanOfficerName, currencyData, proposedPrincipal, principal, principal, totalOverpaid, + inArrearsTolerance, termFrequency, termPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentFrequencyType, + repaymentFrequencyNthDayType, repaymentFrequencyDayOfWeekType, transactionProcessingStrategyId, + transactionProcessingStrategyName, amortizationType, interestRatePerPeriod, interestRateFrequencyType, annualInterestRate, + interestType, isFloatingInterestRate, interestRateDifferential, interestCalculationPeriodType, expectedFirstRepaymentOnDate, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, timeline, summary, feeChargesDueAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, calendarData, productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, @@ -590,34 +608,36 @@ public class LoanAccountData { disbursementData, multiDisburseLoan, canDefineInstallmentAmount, fixedEmiAmount, maxOutstandingBalance, emiAmountVariations, memberVariations, product, inArrears, graceOnArrearsAgeing, overdueCharges, isNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, originalSchedule, - createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods); + createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods, isVariableInstallmentsAllowed, minimumGap, + maximumGap); } public static LoanAccountData populateGroupDefaults(final LoanAccountData acc, final LoanAccountData groupAcc) { return new LoanAccountData(acc.id, acc.accountNo, acc.status, acc.externalId, acc.clientId, acc.clientName, acc.clientOfficeId, - groupAcc.group, acc.loanType, acc.loanProductId, acc.loanProductName, acc.loanProductDescription, acc.isLoanProductLinkedToFloatingRate, acc.fundId, acc.fundName, - acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, acc.currency, acc.proposedPrincipal, - acc.principal, acc.approvedPrincipal, acc.totalOverpaid, acc.inArrearsTolerance, acc.termFrequency, - acc.termPeriodFrequencyType, acc.numberOfRepayments, acc.repaymentEvery, acc.repaymentFrequencyType, - acc.repaymentFrequencyNthDayType, acc.repaymentFrequencyDayOfWeekType, acc.transactionProcessingStrategyId, - acc.transactionProcessingStrategyName, acc.amortizationType, acc.interestRatePerPeriod, acc.interestRateFrequencyType, - acc.annualInterestRate, acc.interestType, acc.isFloatingInterestRate, acc.interestRateDifferential, - acc.interestCalculationPeriodType, acc.expectedFirstRepaymentOnDate, - acc.graceOnPrincipalPayment, acc.graceOnInterestPayment, acc.graceOnInterestCharged, acc.interestChargedFromDate, - groupAcc.timeline, acc.summary, acc.feeChargesAtDisbursementCharged, acc.repaymentSchedule, acc.transactions, acc.charges, - acc.collateral, acc.guarantors, acc.meeting, acc.productOptions, acc.termFrequencyTypeOptions, - acc.repaymentFrequencyTypeOptions, acc.repaymentFrequencyNthDayTypeOptions, acc.repaymentFrequencyDaysOfWeekTypeOptions, - acc.transactionProcessingStrategyOptions, acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions, - acc.interestTypeOptions, acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, - acc.loanOfficerOptions, acc.loanPurposeOptions, acc.loanCollateralOptions, acc.calendarOptions, - acc.syncDisbursementWithMeeting, acc.loanCounter, acc.loanProductCounter, acc.notes, acc.accountLinkingOptions, - acc.linkedAccount, acc.disbursementDetails, acc.multiDisburseLoan, acc.canDefineInstallmentAmount, acc.fixedEmiAmount, - acc.maxOutstandingLoanBalance, acc.emiAmountVariations, acc.memberVariations, acc.product, acc.inArrears, - acc.graceOnArrearsAgeing, acc.overdueCharges, acc.isNPA, acc.daysInMonthType, acc.daysInYearType, - acc.isInterestRecalculationEnabled, acc.interestRecalculationData, acc.originalSchedule, - acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods); + groupAcc.group, acc.loanType, acc.loanProductId, acc.loanProductName, acc.loanProductDescription, + acc.isLoanProductLinkedToFloatingRate, acc.fundId, acc.fundName, acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, + acc.loanOfficerName, acc.currency, acc.proposedPrincipal, acc.principal, acc.approvedPrincipal, acc.totalOverpaid, + acc.inArrearsTolerance, acc.termFrequency, acc.termPeriodFrequencyType, acc.numberOfRepayments, acc.repaymentEvery, + acc.repaymentFrequencyType, acc.repaymentFrequencyNthDayType, acc.repaymentFrequencyDayOfWeekType, + acc.transactionProcessingStrategyId, acc.transactionProcessingStrategyName, acc.amortizationType, + acc.interestRatePerPeriod, acc.interestRateFrequencyType, acc.annualInterestRate, acc.interestType, + acc.isFloatingInterestRate, acc.interestRateDifferential, acc.interestCalculationPeriodType, + acc.expectedFirstRepaymentOnDate, acc.graceOnPrincipalPayment, acc.graceOnInterestPayment, acc.graceOnInterestCharged, + acc.interestChargedFromDate, groupAcc.timeline, acc.summary, acc.feeChargesAtDisbursementCharged, acc.repaymentSchedule, + acc.transactions, acc.charges, acc.collateral, acc.guarantors, acc.meeting, acc.productOptions, + acc.termFrequencyTypeOptions, acc.repaymentFrequencyTypeOptions, acc.repaymentFrequencyNthDayTypeOptions, + acc.repaymentFrequencyDaysOfWeekTypeOptions, acc.transactionProcessingStrategyOptions, + acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions, acc.interestTypeOptions, + acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, acc.loanOfficerOptions, + acc.loanPurposeOptions, acc.loanCollateralOptions, acc.calendarOptions, acc.syncDisbursementWithMeeting, acc.loanCounter, + acc.loanProductCounter, acc.notes, acc.accountLinkingOptions, acc.linkedAccount, acc.disbursementDetails, + acc.multiDisburseLoan, acc.canDefineInstallmentAmount, acc.fixedEmiAmount, acc.maxOutstandingLoanBalance, + acc.emiAmountVariations, acc.memberVariations, acc.product, acc.inArrears, acc.graceOnArrearsAgeing, acc.overdueCharges, + acc.isNPA, acc.daysInMonthType, acc.daysInYearType, acc.isInterestRecalculationEnabled, acc.interestRecalculationData, + acc.originalSchedule, acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods, + acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap); } @@ -737,14 +757,19 @@ public class LoanAccountData { final PaidInAdvanceData paidInAdvance = null; final Collection interestRatesPeriods = null; + final Boolean isVariableInstallmentsAllowed = Boolean.FALSE; + final Integer minimumGap = null; + final Integer maximumGap = null; + return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, group, loanType, - product.getId(), product.getName(), product.getDescription(), product.isLinkedToFloatingInterestRates(), product.getFundId(), product.getFundName(), loanPurposeId, - loanPurposeName, loanOfficerId, loanOfficerName, product.getCurrency(), proposedPrincipal, principal, principal, - totalOverpaid, product.getInArrearsTolerance(), termFrequency, termPeriodFrequencyType, numberOfRepayments, - product.getRepaymentEvery(), product.getRepaymentFrequencyType(), null, null, product.getTransactionProcessingStrategyId(), + product.getId(), product.getName(), product.getDescription(), product.isLinkedToFloatingInterestRates(), + product.getFundId(), product.getFundName(), loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName, + product.getCurrency(), proposedPrincipal, principal, principal, totalOverpaid, product.getInArrearsTolerance(), + termFrequency, termPeriodFrequencyType, numberOfRepayments, product.getRepaymentEvery(), + product.getRepaymentFrequencyType(), null, null, product.getTransactionProcessingStrategyId(), transactionProcessingStrategyName, product.getAmortizationType(), interestRatePerPeriod, product.getInterestRateFrequencyType(), product.getAnnualInterestRate(), product.getInterestType(), - product.isFloatingInterestRateCalculationAllowed(), product.getDefaultDifferentialLendingRate(), + product.isFloatingInterestRateCalculationAllowed(), product.getDefaultDifferentialLendingRate(), product.getInterestCalculationPeriodType(), expectedFirstRepaymentOnDate, product.getGraceOnPrincipalPayment(), product.getGraceOnInterestPayment(), product.getGraceOnInterestCharged(), interestChargedFromDate, timeline, summary, feeChargesDueAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, calendarData, @@ -757,7 +782,9 @@ public class LoanAccountData { product.getOutstandingLoanBalance(), emiAmountVariations, memberVariations, product, inArrears, product.getGraceOnArrearsAgeing(), product.overdueFeeCharges(), isNPA, product.getDaysInMonthType(), product.getDaysInYearType(), product.isInterestRecalculationEnabled(), product.toLoanInterestRecalculationData(), - originalSchedule, createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods); + originalSchedule, createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods, + product.isVariableInstallmentsAllowed(), product.getMinimumGapBetweenInstallments(), + product.getMaximumGapBetweenInstallments()); } public static LoanAccountData populateLoanProductDefaults(final LoanAccountData acc, final LoanProductData product) { @@ -794,27 +821,29 @@ public class LoanAccountData { } return new LoanAccountData(acc.id, acc.accountNo, acc.status, acc.externalId, acc.clientId, acc.clientName, acc.clientOfficeId, - acc.group, acc.loanType, product.getId(), product.getName(), product.getDescription(), product.isLinkedToFloatingInterestRates(), product.getFundId(), - product.getFundName(), acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, - product.getCurrency(), product.getPrincipal(), product.getPrincipal(), product.getPrincipal(), acc.totalOverpaid, - product.getInArrearsTolerance(), termFrequency, termPeriodFrequencyType, product.getNumberOfRepayments(), - product.getRepaymentEvery(), product.getRepaymentFrequencyType(), null, null, product.getTransactionProcessingStrategyId(), - product.getTransactionProcessingStrategyName(), product.getAmortizationType(), product.getInterestRatePerPeriod(), - product.getInterestRateFrequencyType(), product.getAnnualInterestRate(), product.getInterestType(), - product.isFloatingInterestRateCalculationAllowed(), product.getDefaultDifferentialLendingRate(), - product.getInterestCalculationPeriodType(), acc.expectedFirstRepaymentOnDate, product.getGraceOnPrincipalPayment(), - product.getGraceOnInterestPayment(), product.getGraceOnInterestCharged(), acc.interestChargedFromDate, acc.timeline, - acc.summary, acc.feeChargesAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, - calendarData, productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, null, null, - repaymentStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, - interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, - loanCollateralOptions, calendarOptions, acc.syncDisbursementWithMeeting, acc.loanCounter, acc.loanProductCounter, notes, - acc.accountLinkingOptions, acc.linkedAccount, acc.disbursementDetails, product.getMultiDisburseLoan(), - product.canDefineInstallmentAmount(), acc.fixedEmiAmount, product.getOutstandingLoanBalance(), acc.emiAmountVariations, - acc.memberVariations, product, acc.inArrears, product.getGraceOnArrearsAgeing(), product.overdueFeeCharges(), acc.isNPA, - product.getDaysInMonthType(), product.getDaysInYearType(), product.isInterestRecalculationEnabled(), - product.toLoanInterestRecalculationData(), acc.originalSchedule, acc.createStandingInstructionAtDisbursement, - paidInAdvance, acc.interestRatesPeriods); + acc.group, acc.loanType, product.getId(), product.getName(), product.getDescription(), + product.isLinkedToFloatingInterestRates(), product.getFundId(), product.getFundName(), acc.loanPurposeId, + acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, product.getCurrency(), product.getPrincipal(), + product.getPrincipal(), product.getPrincipal(), acc.totalOverpaid, product.getInArrearsTolerance(), termFrequency, + termPeriodFrequencyType, product.getNumberOfRepayments(), product.getRepaymentEvery(), product.getRepaymentFrequencyType(), + null, null, product.getTransactionProcessingStrategyId(), product.getTransactionProcessingStrategyName(), + product.getAmortizationType(), product.getInterestRatePerPeriod(), product.getInterestRateFrequencyType(), + product.getAnnualInterestRate(), product.getInterestType(), product.isFloatingInterestRateCalculationAllowed(), + product.getDefaultDifferentialLendingRate(), product.getInterestCalculationPeriodType(), acc.expectedFirstRepaymentOnDate, + product.getGraceOnPrincipalPayment(), product.getGraceOnInterestPayment(), product.getGraceOnInterestCharged(), + acc.interestChargedFromDate, acc.timeline, acc.summary, acc.feeChargesAtDisbursementCharged, repaymentSchedule, + transactions, charges, collateral, guarantors, calendarData, productOptions, termFrequencyTypeOptions, + repaymentFrequencyTypeOptions, null, null, repaymentStrategyOptions, interestRateFrequencyTypeOptions, + amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, + chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions, + acc.syncDisbursementWithMeeting, acc.loanCounter, acc.loanProductCounter, notes, acc.accountLinkingOptions, + acc.linkedAccount, acc.disbursementDetails, product.getMultiDisburseLoan(), product.canDefineInstallmentAmount(), + acc.fixedEmiAmount, product.getOutstandingLoanBalance(), acc.emiAmountVariations, acc.memberVariations, product, + acc.inArrears, product.getGraceOnArrearsAgeing(), product.overdueFeeCharges(), acc.isNPA, product.getDaysInMonthType(), + product.getDaysInYearType(), product.isInterestRecalculationEnabled(), product.toLoanInterestRecalculationData(), + acc.originalSchedule, acc.createStandingInstructionAtDisbursement, paidInAdvance, acc.interestRatesPeriods, + product.isVariableInstallmentsAllowed(), product.getMinimumGapBetweenInstallments(), + product.getMaximumGapBetweenInstallments()); } @@ -825,15 +854,15 @@ public class LoanAccountData { public static LoanAccountData basicLoanDetails(final Long id, final String accountNo, final LoanStatusEnumData status, final String externalId, final Long clientId, final String clientName, final Long clientOfficeId, final GroupGeneralData group, final EnumOptionData loanType, final Long loanProductId, final String loanProductName, final String loanProductDescription, - final boolean isLoanProductLinkedToFloatingRate, - final Long fundId, final String fundName, final Long loanPurposeId, final String loanPurposeName, final Long loanOfficerId, - final String loanOfficerName, final CurrencyData currencyData, final BigDecimal proposedPrincipal, final BigDecimal principal, - final BigDecimal approvedPrincipal, final BigDecimal totalOverpaid, final BigDecimal inArrearsTolerance, - final Integer termFrequency, final EnumOptionData termPeriodFrequencyType, final Integer numberOfRepayments, - final Integer repaymentEvery, final EnumOptionData repaymentFrequencyType, EnumOptionData repaymentFrequencyNthDayType, + final boolean isLoanProductLinkedToFloatingRate, final Long fundId, final String fundName, final Long loanPurposeId, + final String loanPurposeName, final Long loanOfficerId, final String loanOfficerName, final CurrencyData currencyData, + final BigDecimal proposedPrincipal, final BigDecimal principal, final BigDecimal approvedPrincipal, + final BigDecimal totalOverpaid, final BigDecimal inArrearsTolerance, final Integer termFrequency, + final EnumOptionData termPeriodFrequencyType, final Integer numberOfRepayments, final Integer repaymentEvery, + final EnumOptionData repaymentFrequencyType, EnumOptionData repaymentFrequencyNthDayType, EnumOptionData repaymentFrequencyDayOfWeekType, final Long transactionStrategyId, final String transactionStrategyName, final EnumOptionData amortizationType, final BigDecimal interestRatePerPeriod, final EnumOptionData interestRateFrequencyType, - final BigDecimal annualInterestRate, final EnumOptionData interestType, final boolean isFloatingInterestRate, + final BigDecimal annualInterestRate, final EnumOptionData interestType, final boolean isFloatingInterestRate, final BigDecimal interestRateDifferential, final EnumOptionData interestCalculationPeriodType, final LocalDate expectedFirstRepaymentOnDate, final Integer graceOnPrincipalPayment, final Integer graceOnInterestPayment, final Integer graceOnInterestCharged, final LocalDate interestChargedFromDate, final LoanApplicationTimelineData timeline, @@ -842,7 +871,8 @@ public class LoanAccountData { final Boolean multiDisburseLoan, Boolean canDefineInstallmentAmount, final BigDecimal fixedEmiAmont, final BigDecimal outstandingLoanBalance, final Boolean inArrears, final Integer graceOnArrearsAgeing, final Boolean isNPA, final EnumOptionData daysInMonthType, final EnumOptionData daysInYearType, final boolean isInterestRecalculationEnabled, - final LoanInterestRecalculationData interestRecalculationData, final Boolean createStandingInstructionAtDisbursement) { + final LoanInterestRecalculationData interestRecalculationData, final Boolean createStandingInstructionAtDisbursement, + final Boolean isVariableInstallmentsAllowed, Integer minimumGap, Integer maximumGap) { final LoanScheduleData repaymentSchedule = null; final Collection transactions = null; @@ -880,23 +910,24 @@ public class LoanAccountData { final Collection interestRatesPeriods = null; return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, group, loanType, loanProductId, - loanProductName, loanProductDescription, isLoanProductLinkedToFloatingRate, fundId, fundName, loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName, - currencyData, proposedPrincipal, principal, approvedPrincipal, totalOverpaid, inArrearsTolerance, termFrequency, - termPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentFrequencyType, repaymentFrequencyNthDayType, - repaymentFrequencyDayOfWeekType, transactionStrategyId, transactionStrategyName, amortizationType, interestRatePerPeriod, - interestRateFrequencyType, annualInterestRate, interestType, isFloatingInterestRate, interestRateDifferential, - interestCalculationPeriodType, expectedFirstRepaymentOnDate, - graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, timeline, loanSummary, - feeChargesDueAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, calendarData, - productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, repaymentFrequencyNthDayTypeOptions, - repaymentFrequencyDayOfWeekTypeOptions, repaymentStrategyOptions, interestRateFrequencyTypeOptions, - amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, - chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions, + loanProductName, loanProductDescription, isLoanProductLinkedToFloatingRate, fundId, fundName, loanPurposeId, + loanPurposeName, loanOfficerId, loanOfficerName, currencyData, proposedPrincipal, principal, approvedPrincipal, + totalOverpaid, inArrearsTolerance, termFrequency, termPeriodFrequencyType, numberOfRepayments, repaymentEvery, + repaymentFrequencyType, repaymentFrequencyNthDayType, repaymentFrequencyDayOfWeekType, transactionStrategyId, + transactionStrategyName, amortizationType, interestRatePerPeriod, interestRateFrequencyType, annualInterestRate, + interestType, isFloatingInterestRate, interestRateDifferential, interestCalculationPeriodType, + expectedFirstRepaymentOnDate, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, + interestChargedFromDate, timeline, loanSummary, feeChargesDueAtDisbursementCharged, repaymentSchedule, transactions, + charges, collateral, guarantors, calendarData, productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, + repaymentFrequencyNthDayTypeOptions, repaymentFrequencyDayOfWeekTypeOptions, repaymentStrategyOptions, + interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, + fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions, syncDisbursementWithMeeting, loancounter, loanProductCounter, notes, accountLinkingOptions, linkedAccount, disbursementData, multiDisburseLoan, canDefineInstallmentAmount, fixedEmiAmont, outstandingLoanBalance, emiAmountVariations, memberVariations, product, inArrears, graceOnArrearsAgeing, overdueCharges, isNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, originalSchedule, - createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods); + createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods, isVariableInstallmentsAllowed, minimumGap, + maximumGap); } /* @@ -924,28 +955,28 @@ public class LoanAccountData { if (acc.product != null) { loanProductConfigurableAttributes = acc.product.getloanProductConfigurableAttributes(); } - return new LoanAccountData(acc.id, acc.accountNo, acc.status, acc.externalId, acc.clientId, acc.clientName, acc.clientOfficeId, - acc.group, acc.loanType, acc.loanProductId, acc.loanProductName, acc.loanProductDescription, acc.isLoanProductLinkedToFloatingRate, acc.fundId, acc.fundName, - acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, acc.currency, acc.proposedPrincipal, - acc.principal, acc.approvedPrincipal, acc.totalOverpaid, acc.inArrearsTolerance, acc.termFrequency, - acc.termPeriodFrequencyType, acc.numberOfRepayments, acc.repaymentEvery, acc.repaymentFrequencyType, - acc.repaymentFrequencyNthDayType, acc.repaymentFrequencyDayOfWeekType, acc.transactionProcessingStrategyId, - acc.transactionProcessingStrategyName, acc.amortizationType, acc.interestRatePerPeriod, acc.interestRateFrequencyType, - acc.annualInterestRate, acc.interestType, acc.isFloatingInterestRate, acc.interestRateDifferential, - acc.interestCalculationPeriodType, acc.expectedFirstRepaymentOnDate, - acc.graceOnPrincipalPayment, acc.graceOnInterestPayment, acc.graceOnInterestCharged, acc.interestChargedFromDate, - acc.timeline, acc.summary, acc.feeChargesAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, - guarantors, calendarData, productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, - repaymentFrequencyNthDayTypeOptions, repaymentFrequencyDayOfWeekTypeOptions, transactionProcessingStrategyOptions, - interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, - fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions, - acc.syncDisbursementWithMeeting, acc.loanCounter, acc.loanProductCounter, notes, accountLinkingOptions, linkedAccount, - disbursementDetails, acc.multiDisburseLoan, acc.canDefineInstallmentAmount, acc.fixedEmiAmount, - acc.maxOutstandingLoanBalance, emiAmountVariations, acc.memberVariations, acc.product, acc.inArrears, + acc.group, acc.loanType, acc.loanProductId, acc.loanProductName, acc.loanProductDescription, + acc.isLoanProductLinkedToFloatingRate, acc.fundId, acc.fundName, acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, + acc.loanOfficerName, acc.currency, acc.proposedPrincipal, acc.principal, acc.approvedPrincipal, acc.totalOverpaid, + acc.inArrearsTolerance, acc.termFrequency, acc.termPeriodFrequencyType, acc.numberOfRepayments, acc.repaymentEvery, + acc.repaymentFrequencyType, acc.repaymentFrequencyNthDayType, acc.repaymentFrequencyDayOfWeekType, + acc.transactionProcessingStrategyId, acc.transactionProcessingStrategyName, acc.amortizationType, + acc.interestRatePerPeriod, acc.interestRateFrequencyType, acc.annualInterestRate, acc.interestType, + acc.isFloatingInterestRate, acc.interestRateDifferential, acc.interestCalculationPeriodType, + acc.expectedFirstRepaymentOnDate, acc.graceOnPrincipalPayment, acc.graceOnInterestPayment, acc.graceOnInterestCharged, + acc.interestChargedFromDate, acc.timeline, acc.summary, acc.feeChargesAtDisbursementCharged, repaymentSchedule, + transactions, charges, collateral, guarantors, calendarData, productOptions, termFrequencyTypeOptions, + repaymentFrequencyTypeOptions, repaymentFrequencyNthDayTypeOptions, repaymentFrequencyDayOfWeekTypeOptions, + transactionProcessingStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, + interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, + loanCollateralOptions, calendarOptions, acc.syncDisbursementWithMeeting, acc.loanCounter, acc.loanProductCounter, notes, + accountLinkingOptions, linkedAccount, disbursementDetails, acc.multiDisburseLoan, acc.canDefineInstallmentAmount, + acc.fixedEmiAmount, acc.maxOutstandingLoanBalance, emiAmountVariations, acc.memberVariations, acc.product, acc.inArrears, acc.graceOnArrearsAgeing, overdueCharges, acc.isNPA, acc.daysInMonthType, acc.daysInYearType, acc.isInterestRecalculationEnabled, acc.interestRecalculationData, acc.originalSchedule, - acc.createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods); + acc.createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods, acc.isVariableInstallmentsAllowed, + acc.minimumGap, acc.maximumGap); } public static LoanAccountData associationsAndTemplate(final LoanAccountData acc, final Collection productOptions, @@ -957,32 +988,34 @@ public class LoanAccountData { acc.transactionProcessingStrategyOptions, acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions, acc.interestTypeOptions, acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, allowedLoanOfficers, acc.loanPurposeOptions, acc.loanCollateralOptions, calendarOptions, acc.notes, accountLinkingOptions, - acc.linkedAccount, acc.disbursementDetails, acc.emiAmountVariations, acc.overdueCharges, acc.paidInAdvance, acc.interestRatesPeriods); + acc.linkedAccount, acc.disbursementDetails, acc.emiAmountVariations, acc.overdueCharges, acc.paidInAdvance, + acc.interestRatesPeriods); } public static LoanAccountData associateGroup(final LoanAccountData acc, final GroupGeneralData group) { return new LoanAccountData(acc.id, acc.accountNo, acc.status, acc.externalId, acc.clientId, acc.clientName, acc.clientOfficeId, - group, acc.loanType, acc.loanProductId, acc.loanProductName, acc.loanProductDescription, acc.isLoanProductLinkedToFloatingRate, acc.fundId, acc.fundName, - acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, acc.currency, acc.proposedPrincipal, - acc.principal, acc.approvedPrincipal, acc.totalOverpaid, acc.inArrearsTolerance, acc.termFrequency, - acc.termPeriodFrequencyType, acc.numberOfRepayments, acc.repaymentEvery, acc.repaymentFrequencyType, - acc.repaymentFrequencyNthDayType, acc.repaymentFrequencyDayOfWeekType, acc.transactionProcessingStrategyId, - acc.transactionProcessingStrategyName, acc.amortizationType, acc.interestRatePerPeriod, acc.interestRateFrequencyType, - acc.annualInterestRate, acc.interestType, acc.isFloatingInterestRate, acc.interestRateDifferential, - acc.interestCalculationPeriodType, acc.expectedFirstRepaymentOnDate, - acc.graceOnPrincipalPayment, acc.graceOnInterestPayment, acc.graceOnInterestCharged, acc.interestChargedFromDate, - acc.timeline, acc.summary, acc.feeChargesAtDisbursementCharged, acc.repaymentSchedule, acc.transactions, acc.charges, - acc.collateral, acc.guarantors, acc.meeting, acc.productOptions, acc.termFrequencyTypeOptions, - acc.repaymentFrequencyTypeOptions, acc.repaymentFrequencyNthDayTypeOptions, acc.repaymentFrequencyDaysOfWeekTypeOptions, - acc.transactionProcessingStrategyOptions, acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions, - acc.interestTypeOptions, acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, - acc.loanOfficerOptions, acc.loanPurposeOptions, acc.loanCollateralOptions, acc.calendarOptions, - acc.syncDisbursementWithMeeting, acc.loanCounter, acc.loanProductCounter, acc.notes, acc.accountLinkingOptions, - acc.linkedAccount, acc.disbursementDetails, acc.multiDisburseLoan, acc.canDefineInstallmentAmount, acc.fixedEmiAmount, - acc.maxOutstandingLoanBalance, acc.emiAmountVariations, acc.memberVariations, acc.product, acc.inArrears, - acc.graceOnArrearsAgeing, acc.overdueCharges, acc.isNPA, acc.daysInMonthType, acc.daysInYearType, - acc.isInterestRecalculationEnabled, acc.interestRecalculationData, acc.originalSchedule, - acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods); + group, acc.loanType, acc.loanProductId, acc.loanProductName, acc.loanProductDescription, + acc.isLoanProductLinkedToFloatingRate, acc.fundId, acc.fundName, acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, + acc.loanOfficerName, acc.currency, acc.proposedPrincipal, acc.principal, acc.approvedPrincipal, acc.totalOverpaid, + acc.inArrearsTolerance, acc.termFrequency, acc.termPeriodFrequencyType, acc.numberOfRepayments, acc.repaymentEvery, + acc.repaymentFrequencyType, acc.repaymentFrequencyNthDayType, acc.repaymentFrequencyDayOfWeekType, + acc.transactionProcessingStrategyId, acc.transactionProcessingStrategyName, acc.amortizationType, + acc.interestRatePerPeriod, acc.interestRateFrequencyType, acc.annualInterestRate, acc.interestType, + acc.isFloatingInterestRate, acc.interestRateDifferential, acc.interestCalculationPeriodType, + acc.expectedFirstRepaymentOnDate, acc.graceOnPrincipalPayment, acc.graceOnInterestPayment, acc.graceOnInterestCharged, + acc.interestChargedFromDate, acc.timeline, acc.summary, acc.feeChargesAtDisbursementCharged, acc.repaymentSchedule, + acc.transactions, acc.charges, acc.collateral, acc.guarantors, acc.meeting, acc.productOptions, + acc.termFrequencyTypeOptions, acc.repaymentFrequencyTypeOptions, acc.repaymentFrequencyNthDayTypeOptions, + acc.repaymentFrequencyDaysOfWeekTypeOptions, acc.transactionProcessingStrategyOptions, + acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions, acc.interestTypeOptions, + acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, acc.loanOfficerOptions, + acc.loanPurposeOptions, acc.loanCollateralOptions, acc.calendarOptions, acc.syncDisbursementWithMeeting, acc.loanCounter, + acc.loanProductCounter, acc.notes, acc.accountLinkingOptions, acc.linkedAccount, acc.disbursementDetails, + acc.multiDisburseLoan, acc.canDefineInstallmentAmount, acc.fixedEmiAmount, acc.maxOutstandingLoanBalance, + acc.emiAmountVariations, acc.memberVariations, acc.product, acc.inArrears, acc.graceOnArrearsAgeing, acc.overdueCharges, + acc.isNPA, acc.daysInMonthType, acc.daysInYearType, acc.isInterestRecalculationEnabled, acc.interestRecalculationData, + acc.originalSchedule, acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods, + acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap); } public static LoanAccountData associateMemberVariations(final LoanAccountData acc, final Map memberLoanCycle) { @@ -1024,27 +1057,28 @@ public class LoanAccountData { } return new LoanAccountData(acc.id, acc.accountNo, acc.status, acc.externalId, acc.clientId, acc.clientName, acc.clientOfficeId, - acc.group, acc.loanType, acc.loanProductId, acc.loanProductName, acc.loanProductDescription, acc.isLoanProductLinkedToFloatingRate, acc.fundId, acc.fundName, - acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, acc.currency, acc.proposedPrincipal, - acc.principal, acc.approvedPrincipal, acc.totalOverpaid, acc.inArrearsTolerance, acc.termFrequency, - acc.termPeriodFrequencyType, acc.numberOfRepayments, acc.repaymentEvery, acc.repaymentFrequencyType, - acc.repaymentFrequencyNthDayType, acc.repaymentFrequencyDayOfWeekType, acc.transactionProcessingStrategyId, - acc.transactionProcessingStrategyName, acc.amortizationType, acc.interestRatePerPeriod, acc.interestRateFrequencyType, - acc.annualInterestRate, acc.interestType, acc.isFloatingInterestRate, acc.interestRateDifferential, - acc.interestCalculationPeriodType, acc.expectedFirstRepaymentOnDate, - acc.graceOnPrincipalPayment, acc.graceOnInterestPayment, acc.graceOnInterestCharged, acc.interestChargedFromDate, - acc.timeline, acc.summary, acc.feeChargesAtDisbursementCharged, acc.repaymentSchedule, acc.transactions, acc.charges, - acc.collateral, acc.guarantors, acc.meeting, acc.productOptions, acc.termFrequencyTypeOptions, - acc.repaymentFrequencyTypeOptions, acc.repaymentFrequencyNthDayTypeOptions, acc.repaymentFrequencyDaysOfWeekTypeOptions, - acc.transactionProcessingStrategyOptions, acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions, - acc.interestTypeOptions, acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, - acc.loanOfficerOptions, acc.loanPurposeOptions, acc.loanCollateralOptions, acc.calendarOptions, - acc.syncDisbursementWithMeeting, acc.loanCounter, acc.loanProductCounter, acc.notes, acc.accountLinkingOptions, - acc.linkedAccount, acc.disbursementDetails, acc.multiDisburseLoan, acc.canDefineInstallmentAmount, acc.fixedEmiAmount, - acc.maxOutstandingLoanBalance, acc.emiAmountVariations, memberVariations, acc.product, acc.inArrears, - acc.graceOnArrearsAgeing, acc.overdueCharges, acc.isNPA, acc.daysInMonthType, acc.daysInYearType, - acc.isInterestRecalculationEnabled, acc.interestRecalculationData, acc.originalSchedule, - acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods); + acc.group, acc.loanType, acc.loanProductId, acc.loanProductName, acc.loanProductDescription, + acc.isLoanProductLinkedToFloatingRate, acc.fundId, acc.fundName, acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, + acc.loanOfficerName, acc.currency, acc.proposedPrincipal, acc.principal, acc.approvedPrincipal, acc.totalOverpaid, + acc.inArrearsTolerance, acc.termFrequency, acc.termPeriodFrequencyType, acc.numberOfRepayments, acc.repaymentEvery, + acc.repaymentFrequencyType, acc.repaymentFrequencyNthDayType, acc.repaymentFrequencyDayOfWeekType, + acc.transactionProcessingStrategyId, acc.transactionProcessingStrategyName, acc.amortizationType, + acc.interestRatePerPeriod, acc.interestRateFrequencyType, acc.annualInterestRate, acc.interestType, + acc.isFloatingInterestRate, acc.interestRateDifferential, acc.interestCalculationPeriodType, + acc.expectedFirstRepaymentOnDate, acc.graceOnPrincipalPayment, acc.graceOnInterestPayment, acc.graceOnInterestCharged, + acc.interestChargedFromDate, acc.timeline, acc.summary, acc.feeChargesAtDisbursementCharged, acc.repaymentSchedule, + acc.transactions, acc.charges, acc.collateral, acc.guarantors, acc.meeting, acc.productOptions, + acc.termFrequencyTypeOptions, acc.repaymentFrequencyTypeOptions, acc.repaymentFrequencyNthDayTypeOptions, + acc.repaymentFrequencyDaysOfWeekTypeOptions, acc.transactionProcessingStrategyOptions, + acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions, acc.interestTypeOptions, + acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, acc.loanOfficerOptions, + acc.loanPurposeOptions, acc.loanCollateralOptions, acc.calendarOptions, acc.syncDisbursementWithMeeting, acc.loanCounter, + acc.loanProductCounter, acc.notes, acc.accountLinkingOptions, acc.linkedAccount, acc.disbursementDetails, + acc.multiDisburseLoan, acc.canDefineInstallmentAmount, acc.fixedEmiAmount, acc.maxOutstandingLoanBalance, + acc.emiAmountVariations, memberVariations, acc.product, acc.inArrears, acc.graceOnArrearsAgeing, acc.overdueCharges, + acc.isNPA, acc.daysInMonthType, acc.daysInYearType, acc.isInterestRecalculationEnabled, acc.interestRecalculationData, + acc.originalSchedule, acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods, + acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap); } @@ -1054,53 +1088,55 @@ public class LoanAccountData { final LoanInterestRecalculationData interestRecalculationData = LoanInterestRecalculationData.withCalendarData( acc.interestRecalculationData, calendarData, compoundingCalendarData); return new LoanAccountData(acc.id, acc.accountNo, acc.status, acc.externalId, acc.clientId, acc.clientName, acc.clientOfficeId, - acc.group, acc.loanType, acc.loanProductId, acc.loanProductName, acc.loanProductDescription, acc.isLoanProductLinkedToFloatingRate, acc.fundId, acc.fundName, - acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, acc.currency, acc.proposedPrincipal, - acc.principal, acc.approvedPrincipal, acc.totalOverpaid, acc.inArrearsTolerance, acc.termFrequency, - acc.termPeriodFrequencyType, acc.numberOfRepayments, acc.repaymentEvery, acc.repaymentFrequencyType, - acc.repaymentFrequencyNthDayType, acc.repaymentFrequencyDayOfWeekType, acc.transactionProcessingStrategyId, - acc.transactionProcessingStrategyName, acc.amortizationType, acc.interestRatePerPeriod, acc.interestRateFrequencyType, - acc.annualInterestRate, acc.interestType, acc.isFloatingInterestRate, acc.interestRateDifferential, - acc.interestCalculationPeriodType, acc.expectedFirstRepaymentOnDate, - acc.graceOnPrincipalPayment, acc.graceOnInterestPayment, acc.graceOnInterestCharged, acc.interestChargedFromDate, - acc.timeline, acc.summary, acc.feeChargesAtDisbursementCharged, acc.repaymentSchedule, acc.transactions, acc.charges, - acc.collateral, acc.guarantors, acc.meeting, acc.productOptions, acc.termFrequencyTypeOptions, - acc.repaymentFrequencyTypeOptions, acc.repaymentFrequencyNthDayTypeOptions, acc.repaymentFrequencyDaysOfWeekTypeOptions, - acc.transactionProcessingStrategyOptions, acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions, - acc.interestTypeOptions, acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, - acc.loanOfficerOptions, acc.loanPurposeOptions, acc.loanCollateralOptions, acc.calendarOptions, - acc.syncDisbursementWithMeeting, acc.loanCounter, acc.loanProductCounter, acc.notes, acc.accountLinkingOptions, - acc.linkedAccount, acc.disbursementDetails, acc.multiDisburseLoan, acc.canDefineInstallmentAmount, acc.fixedEmiAmount, - acc.maxOutstandingLoanBalance, acc.emiAmountVariations, acc.memberVariations, acc.product, acc.inArrears, - acc.graceOnArrearsAgeing, acc.overdueCharges, acc.isNPA, acc.daysInMonthType, acc.daysInYearType, - acc.isInterestRecalculationEnabled, interestRecalculationData, acc.originalSchedule, - acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods); + acc.group, acc.loanType, acc.loanProductId, acc.loanProductName, acc.loanProductDescription, + acc.isLoanProductLinkedToFloatingRate, acc.fundId, acc.fundName, acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, + acc.loanOfficerName, acc.currency, acc.proposedPrincipal, acc.principal, acc.approvedPrincipal, acc.totalOverpaid, + acc.inArrearsTolerance, acc.termFrequency, acc.termPeriodFrequencyType, acc.numberOfRepayments, acc.repaymentEvery, + acc.repaymentFrequencyType, acc.repaymentFrequencyNthDayType, acc.repaymentFrequencyDayOfWeekType, + acc.transactionProcessingStrategyId, acc.transactionProcessingStrategyName, acc.amortizationType, + acc.interestRatePerPeriod, acc.interestRateFrequencyType, acc.annualInterestRate, acc.interestType, + acc.isFloatingInterestRate, acc.interestRateDifferential, acc.interestCalculationPeriodType, + acc.expectedFirstRepaymentOnDate, acc.graceOnPrincipalPayment, acc.graceOnInterestPayment, acc.graceOnInterestCharged, + acc.interestChargedFromDate, acc.timeline, acc.summary, acc.feeChargesAtDisbursementCharged, acc.repaymentSchedule, + acc.transactions, acc.charges, acc.collateral, acc.guarantors, acc.meeting, acc.productOptions, + acc.termFrequencyTypeOptions, acc.repaymentFrequencyTypeOptions, acc.repaymentFrequencyNthDayTypeOptions, + acc.repaymentFrequencyDaysOfWeekTypeOptions, acc.transactionProcessingStrategyOptions, + acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions, acc.interestTypeOptions, + acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, acc.loanOfficerOptions, + acc.loanPurposeOptions, acc.loanCollateralOptions, acc.calendarOptions, acc.syncDisbursementWithMeeting, acc.loanCounter, + acc.loanProductCounter, acc.notes, acc.accountLinkingOptions, acc.linkedAccount, acc.disbursementDetails, + acc.multiDisburseLoan, acc.canDefineInstallmentAmount, acc.fixedEmiAmount, acc.maxOutstandingLoanBalance, + acc.emiAmountVariations, acc.memberVariations, acc.product, acc.inArrears, acc.graceOnArrearsAgeing, acc.overdueCharges, + acc.isNPA, acc.daysInMonthType, acc.daysInYearType, acc.isInterestRecalculationEnabled, interestRecalculationData, + acc.originalSchedule, acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods, + acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap); } public static LoanAccountData withOriginalSchedule(final LoanAccountData acc, final LoanScheduleData originalSchedule) { return new LoanAccountData(acc.id, acc.accountNo, acc.status, acc.externalId, acc.clientId, acc.clientName, acc.clientOfficeId, - acc.group, acc.loanType, acc.loanProductId, acc.loanProductName, acc.loanProductDescription, acc.isLoanProductLinkedToFloatingRate, acc.fundId, acc.fundName, - acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, acc.currency, acc.proposedPrincipal, - acc.principal, acc.approvedPrincipal, acc.totalOverpaid, acc.inArrearsTolerance, acc.termFrequency, - acc.termPeriodFrequencyType, acc.numberOfRepayments, acc.repaymentEvery, acc.repaymentFrequencyType, - acc.repaymentFrequencyNthDayType, acc.repaymentFrequencyDayOfWeekType, acc.transactionProcessingStrategyId, - acc.transactionProcessingStrategyName, acc.amortizationType, acc.interestRatePerPeriod, acc.interestRateFrequencyType, - acc.annualInterestRate, acc.interestType, acc.isFloatingInterestRate, acc.interestRateDifferential, - acc.interestCalculationPeriodType, acc.expectedFirstRepaymentOnDate, - acc.graceOnPrincipalPayment, acc.graceOnInterestPayment, acc.graceOnInterestCharged, acc.interestChargedFromDate, - acc.timeline, acc.summary, acc.feeChargesAtDisbursementCharged, acc.repaymentSchedule, acc.transactions, acc.charges, - acc.collateral, acc.guarantors, acc.meeting, acc.productOptions, acc.termFrequencyTypeOptions, - acc.repaymentFrequencyTypeOptions, acc.repaymentFrequencyNthDayTypeOptions, acc.repaymentFrequencyDaysOfWeekTypeOptions, - acc.transactionProcessingStrategyOptions, acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions, - acc.interestTypeOptions, acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, - acc.loanOfficerOptions, acc.loanPurposeOptions, acc.loanCollateralOptions, acc.calendarOptions, - acc.syncDisbursementWithMeeting, acc.loanCounter, acc.loanProductCounter, acc.notes, acc.accountLinkingOptions, - acc.linkedAccount, acc.disbursementDetails, acc.multiDisburseLoan, acc.canDefineInstallmentAmount, acc.fixedEmiAmount, - acc.maxOutstandingLoanBalance, acc.emiAmountVariations, acc.memberVariations, acc.product, acc.inArrears, - acc.graceOnArrearsAgeing, acc.overdueCharges, acc.isNPA, acc.daysInMonthType, acc.daysInYearType, - acc.isInterestRecalculationEnabled, acc.interestRecalculationData, originalSchedule, - acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods); + acc.group, acc.loanType, acc.loanProductId, acc.loanProductName, acc.loanProductDescription, + acc.isLoanProductLinkedToFloatingRate, acc.fundId, acc.fundName, acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, + acc.loanOfficerName, acc.currency, acc.proposedPrincipal, acc.principal, acc.approvedPrincipal, acc.totalOverpaid, + acc.inArrearsTolerance, acc.termFrequency, acc.termPeriodFrequencyType, acc.numberOfRepayments, acc.repaymentEvery, + acc.repaymentFrequencyType, acc.repaymentFrequencyNthDayType, acc.repaymentFrequencyDayOfWeekType, + acc.transactionProcessingStrategyId, acc.transactionProcessingStrategyName, acc.amortizationType, + acc.interestRatePerPeriod, acc.interestRateFrequencyType, acc.annualInterestRate, acc.interestType, + acc.isFloatingInterestRate, acc.interestRateDifferential, acc.interestCalculationPeriodType, + acc.expectedFirstRepaymentOnDate, acc.graceOnPrincipalPayment, acc.graceOnInterestPayment, acc.graceOnInterestCharged, + acc.interestChargedFromDate, acc.timeline, acc.summary, acc.feeChargesAtDisbursementCharged, acc.repaymentSchedule, + acc.transactions, acc.charges, acc.collateral, acc.guarantors, acc.meeting, acc.productOptions, + acc.termFrequencyTypeOptions, acc.repaymentFrequencyTypeOptions, acc.repaymentFrequencyNthDayTypeOptions, + acc.repaymentFrequencyDaysOfWeekTypeOptions, acc.transactionProcessingStrategyOptions, + acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions, acc.interestTypeOptions, + acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, acc.loanOfficerOptions, + acc.loanPurposeOptions, acc.loanCollateralOptions, acc.calendarOptions, acc.syncDisbursementWithMeeting, acc.loanCounter, + acc.loanProductCounter, acc.notes, acc.accountLinkingOptions, acc.linkedAccount, acc.disbursementDetails, + acc.multiDisburseLoan, acc.canDefineInstallmentAmount, acc.fixedEmiAmount, acc.maxOutstandingLoanBalance, + acc.emiAmountVariations, acc.memberVariations, acc.product, acc.inArrears, acc.graceOnArrearsAgeing, acc.overdueCharges, + acc.isNPA, acc.daysInMonthType, acc.daysInYearType, acc.isInterestRecalculationEnabled, acc.interestRecalculationData, + originalSchedule, acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods, + acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap); } private LoanAccountData( @@ -1164,7 +1200,8 @@ public class LoanAccountData { final EnumOptionData daysInMonthType, final EnumOptionData daysInYearType, final boolean isInterestRecalculationEnabled, final LoanInterestRecalculationData interestRecalculationData, final LoanScheduleData originalSchedule, final Boolean createStandingInstructionAtDisbursement, final PaidInAdvanceData paidInAdvance, - final Collection interestRatesPeriods) { + final Collection interestRatesPeriods, final Boolean isVariableInstallmentsAllowed, + final Integer minimumGap, final Integer maximumGap) { this.id = id; this.accountNo = accountNo; @@ -1332,6 +1369,9 @@ public class LoanAccountData { this.product.setloanProductConfigurableAttributes(product.getloanProductConfigurableAttributes()); } this.interestRatesPeriods = interestRatesPeriods; + this.isVariableInstallmentsAllowed = isVariableInstallmentsAllowed; + this.minimumGap = minimumGap; + this.maximumGap = maximumGap; } public RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData() { @@ -1394,7 +1434,7 @@ public class LoanAccountData { } private static boolean isLoanCycleValuesWhenConditionEqual(Integer loanCycleNumber, LoanProductBorrowerCycleVariationData data) { - return data.getBorrowerCycleNumber() == loanCycleNumber && data.getValueConditionType().equals(LoanProductValueConditionType.EQUAL); + return data.getBorrowerCycleNumber().equals(loanCycleNumber) && data.getValueConditionType().equals(LoanProductValueConditionType.EQUAL); } public LoanProductData product() { @@ -1431,12 +1471,12 @@ public class LoanAccountData { } public boolean isMultiDisburseLoan() { - return multiDisburseLoan ; + return multiDisburseLoan; } - + public BigDecimal getTotalPaidFeeCharges() { - if(this.summary != null) return this.summary.getTotalPaidFeeCharges() ; - return BigDecimal.ZERO ; + if (this.summary != null) return this.summary.getTotalPaidFeeCharges(); + return BigDecimal.ZERO; } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanTermVariationsData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanTermVariationsData.java index b06a554c3..35b5c321f 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanTermVariationsData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanTermVariationsData.java @@ -11,38 +11,42 @@ import org.joda.time.LocalDate; import org.mifosplatform.infrastructure.core.data.EnumOptionData; import org.mifosplatform.portfolio.loanaccount.domain.LoanTermVariationType; -public class LoanTermVariationsData { +public class LoanTermVariationsData implements Comparable { @SuppressWarnings("unused") private final Long id; private final EnumOptionData termType; private final LocalDate termVariationApplicableFrom; - private final Integer termVariationApplicableFromInstallment; - private final BigDecimal termValue; + private final BigDecimal decimalValue; + private final LocalDate dateValue; + private final boolean isSpecificToInstallment; + private Boolean isProcessed; public LoanTermVariationsData(final Long id, final EnumOptionData termType, final LocalDate termVariationApplicableFrom, - final BigDecimal termValue) { + final BigDecimal decimalValue, final LocalDate dateValue, final boolean isSpecificToInstallment) { this.id = id; this.termType = termType; this.termVariationApplicableFrom = termVariationApplicableFrom; - this.termValue = termValue; - this.termVariationApplicableFromInstallment = null; + this.decimalValue = decimalValue; + this.dateValue = dateValue; + this.isSpecificToInstallment = isSpecificToInstallment; } - + public LoanTermVariationsData(final EnumOptionData termType, final LocalDate termVariationApplicableFrom, - final BigDecimal termValue) { + final BigDecimal decimalValue, LocalDate dateValue, final boolean isSpecificToInstallment) { this.id = null; this.termType = termType; this.termVariationApplicableFrom = termVariationApplicableFrom; - this.termValue = termValue; - this.termVariationApplicableFromInstallment = null; + this.decimalValue = decimalValue; + this.dateValue = dateValue; + this.isSpecificToInstallment = isSpecificToInstallment; } public EnumOptionData getTermType() { return this.termType; } - - public LoanTermVariationType getTermVariationType(){ + + public LoanTermVariationType getTermVariationType() { return LoanTermVariationType.fromInt(this.termType.getId().intValue()); } @@ -50,33 +54,51 @@ public class LoanTermVariationsData { return this.termVariationApplicableFrom; } - public BigDecimal getTermValue() { - return this.termValue; + public BigDecimal getDecimalValue() { + return this.decimalValue; } - public boolean isApplicable(final LocalDate fromDate, final LocalDate dueDate, int installmentNumber) { - return occursOnDayFromAndUpTo(fromDate, dueDate, this.termVariationApplicableFrom) - || isIntegerEquals(installmentNumber, this.termVariationApplicableFromInstallment); + public boolean isApplicable(final LocalDate fromDate, final LocalDate dueDate) { + return occursOnDayFromAndUpTo(fromDate, dueDate, this.termVariationApplicableFrom); } private boolean occursOnDayFromAndUpTo(final LocalDate fromInclusive, final LocalDate upToNotInclusive, final LocalDate target) { return target != null && !target.isBefore(fromInclusive) && target.isBefore(upToNotInclusive); } - private boolean isIntegerEquals(final Integer installmentNumber, final Integer target) { - return target != null && target.equals(installmentNumber); - } - - public boolean isApplicable(final LocalDate fromDate, int installmentNumber) { - return occursBefore(fromDate, this.termVariationApplicableFrom) - || isIntegerGreterThan(installmentNumber, this.termVariationApplicableFromInstallment); + public boolean isApplicable(final LocalDate fromDate) { + return occursBefore(fromDate, this.termVariationApplicableFrom); } private boolean occursBefore(final LocalDate date, final LocalDate target) { return target != null && target.isBefore(date); } - private boolean isIntegerGreterThan(final Integer installmentNumber, final Integer target) { - return target != null && target < installmentNumber; + public LocalDate getDateValue() { + return this.dateValue; } + + public boolean isSpecificToInstallment() { + return this.isSpecificToInstallment; + } + + public Boolean isProcessed() { + return this.isProcessed == null ? false : this.isProcessed; + } + + public void setProcessed(Boolean isProcessed) { + this.isProcessed = isProcessed; + } + + @Override + public int compareTo(LoanTermVariationsData o) { + int comparsion = getTermApplicableFrom().compareTo(o.getTermApplicableFrom()); + if (comparsion == 0) { + if (o.getTermVariationType().isDueDateVariation() || o.getTermVariationType().isInsertInstallment()) { + comparsion = 1; + } + } + return comparsion; + } + } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanTermVariationsDataWrapper.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanTermVariationsDataWrapper.java new file mode 100644 index 000000000..f9800d816 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanTermVariationsDataWrapper.java @@ -0,0 +1,110 @@ +/** + * 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.data; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.ListIterator; + +import org.joda.time.LocalDate; + +public class LoanTermVariationsDataWrapper { + + private final List exceptionData; + private final ListIterator iterator; + private final List interestRateChanges; + private final List dueDateVariation; + private final ListIterator dueDateIterator; + + public LoanTermVariationsDataWrapper(final List exceptionData) { + if (exceptionData == null) { + this.exceptionData = new ArrayList<>(1); + } else { + this.exceptionData = exceptionData; + Collections.sort(this.exceptionData); + } + this.interestRateChanges = new ArrayList<>(); + this.dueDateVariation = new ArrayList<>(); + for (LoanTermVariationsData loanTermVariationsData : this.exceptionData) { + if (loanTermVariationsData.getTermVariationType().isInterestRateVariation()) { + this.interestRateChanges.add(loanTermVariationsData); + } else if (loanTermVariationsData.getTermVariationType().isDueDateVariation()) { + this.dueDateVariation.add(loanTermVariationsData); + } + } + this.exceptionData.removeAll(this.interestRateChanges); + this.exceptionData.removeAll(this.dueDateVariation); + iterator = this.exceptionData.listIterator(); + dueDateIterator = this.dueDateVariation.listIterator(); + } + + public boolean hasVariation(final LocalDate date) { + ListIterator iterator = this.iterator; + return hasNext(date, iterator); + } + + private boolean hasNext(final LocalDate date, ListIterator iterator) { + boolean hasVariation = false; + if (iterator.hasNext()) { + LoanTermVariationsData loanTermVariationsData = iterator.next(); + if (!loanTermVariationsData.getTermApplicableFrom().isAfter(date)) { + hasVariation = true; + } + iterator.previous(); + } + return hasVariation; + } + + public boolean hasDueDateVariation(final LocalDate date) { + ListIterator iterator = this.dueDateIterator; + return hasNext(date, iterator); + } + + public LoanTermVariationsData nextVariation() { + return this.iterator.next(); + } + + public LoanTermVariationsData nextDueDateVariation() { + return this.dueDateIterator.next(); + } + + public List getInterestRateChanges() { + return this.interestRateChanges; + } + + public List getDueDateVariation() { + return this.dueDateVariation; + } + + public List getExceptionData() { + return this.exceptionData; + } + + public int adjustNumberOfRepayments() { + int repaymetsForAdjust = 0; + for (LoanTermVariationsData loanTermVariations : this.exceptionData) { + if (loanTermVariations.getTermVariationType().isInsertInstallment()) { + repaymetsForAdjust++; + } else if (loanTermVariations.getTermVariationType().isDeleteInstallment()) { + repaymetsForAdjust--; + } + } + return repaymetsForAdjust; + } + + public LoanTermVariationsData fetchLoanTermDueDateVariationsData(final LocalDate onDate) { + LoanTermVariationsData data = null; + for (LoanTermVariationsData termVariationsData : this.dueDateVariation) { + if (onDate.isEqual(termVariationsData.getTermApplicableFrom())) { + data = termVariationsData; + break; + } + } + return data; + } + +} 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 e0e60332d..957561652 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 @@ -345,7 +345,7 @@ public class Loan extends AbstractPersistable { @OrderBy(value = "termApplicableFrom, id") @LazyCollection(LazyCollectionOption.FALSE) @OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true) - private final Set loanTermVariations = new HashSet<>(); + private final List loanTermVariations = new ArrayList<>(); @Column(name = "total_recovered_derived", scale = 6, precision = 19) private BigDecimal totalRecovered; @@ -577,13 +577,9 @@ public class Loan extends AbstractPersistable { totalChargeAmt = loanCharge.amountOutstanding(); } } else { - chargeAmt = loanCharge.amount(); - if (loanCharge.isInstalmentFee()) { - chargeAmt = chargeAmt.divide(BigDecimal.valueOf(repaymentScheduleDetail().getNumberOfRepayments())); - } + chargeAmt = loanCharge.amountOrPercentage(); } - loanCharge.update(chargeAmt, loanCharge.getDueLocalDate(), amount, repaymentScheduleDetail().getNumberOfRepayments(), - totalChargeAmt); + loanCharge.update(chargeAmt, loanCharge.getDueLocalDate(), amount, fetchNumberOfInstallmensAfterExceptions(), totalChargeAmt); // NOTE: must add new loan charge to set of loan charges before // reporcessing the repayment schedule. @@ -1087,14 +1083,10 @@ public class Loan extends AbstractPersistable { totalChargeAmt = calculatePerInstallmentChargeAmount(loanCharge); } } else { - chargeAmt = loanCharge.amount(); - if (loanCharge.isInstalmentFee()) { - chargeAmt = chargeAmt.divide(BigDecimal.valueOf(repaymentScheduleDetail().getNumberOfRepayments())); - } + chargeAmt = loanCharge.amountOrPercentage(); } if (charge != null) - charge.update(chargeAmt, loanCharge.getDueLocalDate(), amount, repaymentScheduleDetail().getNumberOfRepayments(), - totalChargeAmt); + charge.update(chargeAmt, loanCharge.getDueLocalDate(), amount, fetchNumberOfInstallmensAfterExceptions(), totalChargeAmt); } @@ -1530,7 +1522,7 @@ public class Loan extends AbstractPersistable { for (final LoanCharge loanCharge : charges) { recalculateLoanCharge(loanCharge, penaltyWaitPeriod); } - updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement()); + updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement()); } public boolean isInterestRecalculationEnabledForProduct() { @@ -1603,14 +1595,10 @@ public class Loan extends AbstractPersistable { totalChargeAmt = calculatePerInstallmentChargeAmount(loanCharge); } } else { - chargeAmt = loanCharge.amount(); - if (loanCharge.isInstalmentFee()) { - chargeAmt = chargeAmt.divide(BigDecimal.valueOf(repaymentScheduleDetail().getNumberOfRepayments())); - } + chargeAmt = loanCharge.amountOrPercentage(); } if (loanCharge.isActive()) { - loanCharge.update(chargeAmt, loanCharge.getDueLocalDate(), amount, repaymentScheduleDetail().getNumberOfRepayments(), - totalChargeAmt); + loanCharge.update(chargeAmt, loanCharge.getDueLocalDate(), amount, fetchNumberOfInstallmensAfterExceptions(), totalChargeAmt); validateChargeHasValidSpecifiedDateIfApplicable(loanCharge, getDisbursementDate(), getLastRepaymentPeriodDueDate()); } @@ -2340,8 +2328,10 @@ public class Loan extends AbstractPersistable { if ((this.loanProduct.isMultiDisburseLoan() || this.loanProduct.canDefineInstallmentAmount()) && emiAmount != null && emiAmount.compareTo(retriveLastEmiAmount()) != 0) { if (this.loanProduct.isMultiDisburseLoan()) { + final Date dateValue = null; + final boolean isSpecificToInstallment = false; LoanTermVariations loanVariationTerms = new LoanTermVariations(LoanTermVariationType.EMI_AMOUNT.getValue(), - actualDisbursementDate.toDate(), emiAmount, this); + actualDisbursementDate.toDate(), emiAmount, dateValue, isSpecificToInstallment, this); this.loanTermVariations.add(loanVariationTerms); } else { this.fixedEmiAmount = emiAmount; @@ -2580,21 +2570,6 @@ public class Loan extends AbstractPersistable { for (LoanDisbursementDetails disbursementDetails : this.disbursementDetails) { disbursementData.add(disbursementDetails.toData()); } - final List loanVariationTermsData = new ArrayList<>(); - boolean isDefaultEmiAmountReq = true; - for (LoanTermVariations variationTerms : this.loanTermVariations) { - if (variationTerms.getTermType().isEMIAmountVariation()) { - if (variationTerms.getTermApplicableFrom().equals(this.getDisbursementDate().toDate())) { - isDefaultEmiAmountReq = false; - } - loanVariationTermsData.add(variationTerms.toData()); - } - } - if (isDefaultEmiAmountReq) { - LoanTermVariationsData data = new LoanTermVariationsData(null, - LoanEnumerations.loanvariationType(LoanTermVariationType.EMI_AMOUNT), this.getDisbursementDate(), this.fixedEmiAmount); - loanVariationTermsData.add(data); - } CalendarInstance restCalendarInstance = null; CalendarInstance compoundingCalendarInstance = null; @@ -2612,37 +2587,46 @@ public class Loan extends AbstractPersistable { } BigDecimal annualNominalInterestRate = this.loanRepaymentScheduleDetail.getAnnualNominalInterestRate(); - Collection loanTermVariations = new ArrayList<>(); - if (loanProduct.isLinkedToFloatingInterestRate()) { - Collection applicableRates = loanProduct.fetchInterestRates(scheduleGeneratorDTO.getFloatingRateDTO()); - LocalDate interestRateStartDate = DateUtils.getLocalDateOfTenant(); - for (FloatingRatePeriodData periodData : applicableRates) { - LoanTermVariationsData loanTermVariation = new LoanTermVariationsData( - LoanEnumerations.loanvariationType(LoanTermVariationType.INTEREST_RATE), periodData.getFromDateAsLocalDate(), - periodData.getInterestRate()); - if (interestRateStartDate.isAfter(periodData.getFromDateAsLocalDate())) { - interestRateStartDate = periodData.getFromDateAsLocalDate(); - annualNominalInterestRate = periodData.getInterestRate(); - } - loanTermVariations.add(loanTermVariation); - } - } + FloatingRateDTO floatingRateDTO = scheduleGeneratorDTO.getFloatingRateDTO(); + List loanTermVariations = new ArrayList<>(); + annualNominalInterestRate = constructLoanTermVariations(floatingRateDTO, annualNominalInterestRate, loanTermVariations); final LoanApplicationTerms loanApplicationTerms = LoanApplicationTerms.assembleFrom(scheduleGeneratorDTO.getApplicationCurrency(), loanTermFrequency, loanTermPeriodFrequencyType, nthDayType, dayOfWeekType, getDisbursementDate(), getExpectedFirstRepaymentOnDate(), scheduleGeneratorDTO.getCalculatedRepaymentsStartingFromDate(), getInArrearsTolerance(), this.loanRepaymentScheduleDetail, this.loanProduct.isMultiDisburseLoan(), this.fixedEmiAmount, disbursementData, - this.maxOutstandingLoanBalance, loanVariationTermsData, getInterestChargedFromDate(), - this.loanProduct.getPrincipalThresholdForLastInstallment(), this.loanProduct.getInstallmentAmountInMultiplesOf(), - recalculationFrequencyType, restCalendarInstance, compoundingMethod, compoundingCalendarInstance, compoundingFrequencyType, - this.loanProduct.preCloseInterestCalculationStrategy(), rescheduleStrategyMethod, getApprovedPrincipal(), - annualNominalInterestRate, loanTermVariations); + this.maxOutstandingLoanBalance, getInterestChargedFromDate(), this.loanProduct.getPrincipalThresholdForLastInstallment(), + this.loanProduct.getInstallmentAmountInMultiplesOf(), recalculationFrequencyType, restCalendarInstance, compoundingMethod, + compoundingCalendarInstance, compoundingFrequencyType, this.loanProduct.preCloseInterestCalculationStrategy(), + rescheduleStrategyMethod, getApprovedPrincipal(), annualNominalInterestRate, loanTermVariations); final LoanScheduleModel loanSchedule = loanScheduleGenerator.generate(mc, loanApplicationTerms, charges(), scheduleGeneratorDTO.getHolidayDetailDTO()); return loanSchedule; } + private BigDecimal constructFloatingInterestRates(final BigDecimal annualNominalInterestRate, final FloatingRateDTO floatingRateDTO, + final List loanTermVariations) { + final LocalDate dateValue = null; + final boolean isSpecificToInstallment = false; + BigDecimal interestRate = annualNominalInterestRate; + if (loanProduct.isLinkedToFloatingInterestRate()) { + Collection applicableRates = loanProduct.fetchInterestRates(floatingRateDTO); + LocalDate interestRateStartDate = DateUtils.getLocalDateOfTenant(); + for (FloatingRatePeriodData periodData : applicableRates) { + LoanTermVariationsData loanTermVariation = new LoanTermVariationsData( + LoanEnumerations.loanvariationType(LoanTermVariationType.INTEREST_RATE), periodData.getFromDateAsLocalDate(), + periodData.getInterestRate(), dateValue, isSpecificToInstallment); + if (interestRateStartDate.isAfter(periodData.getFromDateAsLocalDate())) { + interestRateStartDate = periodData.getFromDateAsLocalDate(); + interestRate = periodData.getInterestRate(); + } + loanTermVariations.add(loanTermVariation); + } + } + return interestRate; + } + private void handleDisbursementTransaction(final LocalDate disbursedOn, final LocalDateTime createdDate, final AppUser currentUser) { // add repayment transaction to track incoming money from client to mfi @@ -2704,8 +2688,9 @@ public class Loan extends AbstractPersistable { getApprovedOnDate()); } - if (getExpectedFirstRepaymentOnDate() != null && disbursedOn.isAfter(getExpectedFirstRepaymentOnDate()) - && disbursedOn.toDate().equals(this.actualDisbursementDate)) { + if (getExpectedFirstRepaymentOnDate() != null + && (disbursedOn.isAfter(this.fetchRepaymentScheduleInstallment(1).getDueDate()) || disbursedOn + .isAfter(getExpectedFirstRepaymentOnDate())) && disbursedOn.toDate().equals(this.actualDisbursementDate)) { final String errorMessage = "submittedOnDate cannot be after the loans expectedFirstRepaymentOnDate: " + getExpectedFirstRepaymentOnDate().toString(); throw new InvalidLoanStateTransitionException("disbursal", "cannot.be.after.expected.first.repayment.date", errorMessage, @@ -2815,8 +2800,13 @@ public class Loan extends AbstractPersistable { for (final LoanRepaymentScheduleInstallment currentInstallment : this.repaymentScheduleInstallments) { currentInstallment.resetDerivedComponents(); } - - this.loanTermVariations.clear(); + final List removeTerms = new ArrayList<>(this.loanTermVariations.size()); + for (LoanTermVariations variations : this.loanTermVariations) { + if (variations.getOnLoanStatus().equals(LoanStatus.ACTIVE.getValue())) { + removeTerms.add(variations); + } + } + this.loanTermVariations.removeAll(removeTerms); final LoanRepaymentScheduleProcessingWrapper wrapper = new LoanRepaymentScheduleProcessingWrapper(); wrapper.reprocess(getCurrency(), getDisbursementDate(), this.repaymentScheduleInstallments, charges()); @@ -4602,7 +4592,7 @@ public class Loan extends AbstractPersistable { for (final LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) { BigDecimal amount = BigDecimal.ZERO; if (loanCharge.getChargeCalculation().isFlat()) { - amount = loanCharge.amount().divide(BigDecimal.valueOf(repaymentScheduleDetail().getNumberOfRepayments())); + amount = loanCharge.amountOrPercentage(); } else { amount = calculateInstallmentChargeAmount(loanCharge.getChargeCalculation(), loanCharge.getPercentage(), installment) .getAmount(); @@ -5001,51 +4991,23 @@ public class Loan extends AbstractPersistable { for (LoanDisbursementDetails disbursementDetails : this.disbursementDetails) { disbursementData.add(disbursementDetails.toData()); } - final List loanVariationTermsData = new ArrayList<>(); - boolean isDefaultEmiAmountReq = true; - for (LoanTermVariations variationTerms : this.loanTermVariations) { - if (variationTerms.getTermType().isEMIAmountVariation()) { - if (variationTerms.getTermApplicableFrom().equals(this.getDisbursementDate().toDate())) { - isDefaultEmiAmountReq = false; - } - loanVariationTermsData.add(variationTerms.toData()); - } - } - if (isDefaultEmiAmountReq) { - LoanTermVariationsData data = new LoanTermVariationsData(null, - LoanEnumerations.loanvariationType(LoanTermVariationType.EMI_AMOUNT), this.getDisbursementDate(), this.fixedEmiAmount); - loanVariationTermsData.add(data); - } RecalculationFrequencyType recalculationFrequencyType = this.loanInterestRecalculationDetails().getRestFrequencyType(); RecalculationFrequencyType compoundingFrequencyType = this.loanInterestRecalculationDetails().getCompoundingFrequencyType(); BigDecimal annualNominalInterestRate = this.loanRepaymentScheduleDetail.getAnnualNominalInterestRate(); - Collection loanTermVariations = new ArrayList<>(); - if (loanProduct.isLinkedToFloatingInterestRate()) { - Collection applicableRates = loanProduct.fetchInterestRates(floatingRateDTO); - LocalDate interestRateStartDate = DateUtils.getLocalDateOfTenant(); - for (FloatingRatePeriodData periodData : applicableRates) { - LoanTermVariationsData loanTermVariation = new LoanTermVariationsData( - LoanEnumerations.loanvariationType(LoanTermVariationType.INTEREST_RATE), periodData.getFromDateAsLocalDate(), - periodData.getInterestRate()); - if (interestRateStartDate.isAfter(periodData.getFromDateAsLocalDate())) { - interestRateStartDate = periodData.getFromDateAsLocalDate(); - annualNominalInterestRate = periodData.getInterestRate(); - } - loanTermVariations.add(loanTermVariation); - } - } + List loanTermVariations = new ArrayList<>(); + annualNominalInterestRate = constructLoanTermVariations(floatingRateDTO, annualNominalInterestRate, loanTermVariations); final LoanApplicationTerms loanApplicationTerms = LoanApplicationTerms.assembleFrom(applicationCurrency, loanTermFrequency, loanTermPeriodFrequencyType, getDisbursementDate(), getExpectedFirstRepaymentOnDate(), calculatedRepaymentsStartingFromDate, getInArrearsTolerance(), this.loanRepaymentScheduleDetail, this.loanProduct.isMultiDisburseLoan(), this.fixedEmiAmount, disbursementData, this.maxOutstandingLoanBalance, - loanVariationTermsData, getInterestChargedFromDate(), this.loanInterestRecalculationDetails, - calendarInstanceForInterestRecalculation, recalculationFrequencyType, compoundingCalendarInstance, - compoundingFrequencyType, this.loanProduct.getPrincipalThresholdForLastInstallment(), - this.loanProduct.getInstallmentAmountInMultiplesOf(), this.loanProduct.preCloseInterestCalculationStrategy(), - getApprovedPrincipal(), annualNominalInterestRate, loanTermVariations); + getInterestChargedFromDate(), this.loanInterestRecalculationDetails, calendarInstanceForInterestRecalculation, + recalculationFrequencyType, compoundingCalendarInstance, compoundingFrequencyType, + this.loanProduct.getPrincipalThresholdForLastInstallment(), this.loanProduct.getInstallmentAmountInMultiplesOf(), + this.loanProduct.preCloseInterestCalculationStrategy(), getApprovedPrincipal(), annualNominalInterestRate, + loanTermVariations); return loanApplicationTerms; } @@ -5070,22 +5032,6 @@ public class Loan extends AbstractPersistable { for (LoanDisbursementDetails disbursementDetails : this.disbursementDetails) { disbursementData.add(disbursementDetails.toData()); } - final List loanVariationTermsData = new ArrayList<>(); - boolean isDefaultEmiAmountReq = true; - for (LoanTermVariations variationTerms : this.loanTermVariations) { - if (variationTerms.getTermType().isEMIAmountVariation()) { - if (variationTerms.getTermApplicableFrom().equals(this.getDisbursementDate().toDate())) { - isDefaultEmiAmountReq = false; - } - loanVariationTermsData.add(variationTerms.toData()); - } - } - if (isDefaultEmiAmountReq) { - LoanTermVariationsData data = new LoanTermVariationsData(null, - LoanEnumerations.loanvariationType(LoanTermVariationType.EMI_AMOUNT), this.getDisbursementDate(), - this.fixedEmiAmount); - loanVariationTermsData.add(data); - } RecalculationFrequencyType recalculationFrequencyType = null; InterestRecalculationCompoundingMethod compoundingMethod = null; @@ -5099,26 +5045,13 @@ public class Loan extends AbstractPersistable { } BigDecimal annualNominalInterestRate = this.loanRepaymentScheduleDetail.getAnnualNominalInterestRate(); - Collection loanTermVariations = new ArrayList<>(); - if (loanProduct.isLinkedToFloatingInterestRate()) { - Collection applicableRates = loanProduct.fetchInterestRates(floatingRateDTO); - LocalDate interestRateStartDate = DateUtils.getLocalDateOfTenant(); - for (FloatingRatePeriodData periodData : applicableRates) { - LoanTermVariationsData loanTermVariation = new LoanTermVariationsData( - LoanEnumerations.loanvariationType(LoanTermVariationType.INTEREST_RATE), periodData.getFromDateAsLocalDate(), - periodData.getInterestRate()); - if (interestRateStartDate.isAfter(periodData.getFromDateAsLocalDate())) { - interestRateStartDate = periodData.getFromDateAsLocalDate(); - annualNominalInterestRate = periodData.getInterestRate(); - } - loanTermVariations.add(loanTermVariation); - } - } + List loanTermVariations = new ArrayList<>(); + annualNominalInterestRate = constructLoanTermVariations(floatingRateDTO, annualNominalInterestRate, loanTermVariations); final LoanApplicationTerms loanApplicationTerms = LoanApplicationTerms.assembleFrom(null, loanTermFrequency, loanTermPeriodFrequencyType, nthDayType, dayOfWeekType, getDisbursementDate(), getExpectedFirstRepaymentOnDate(), null, getInArrearsTolerance(), this.loanRepaymentScheduleDetail, this.loanProduct.isMultiDisburseLoan(), this.fixedEmiAmount, - disbursementData, this.maxOutstandingLoanBalance, loanVariationTermsData, getInterestChargedFromDate(), + disbursementData, this.maxOutstandingLoanBalance, getInterestChargedFromDate(), this.loanProduct.getPrincipalThresholdForLastInstallment(), this.loanProduct.getInstallmentAmountInMultiplesOf(), recalculationFrequencyType, restCalendarInstance, compoundingMethod, compoundingCalendarInstance, compoundingFrequencyType, this.loanProduct.preCloseInterestCalculationStrategy(), rescheduleStrategyMethod, @@ -5134,6 +5067,15 @@ public class Loan extends AbstractPersistable { return installment; } + private BigDecimal constructLoanTermVariations(FloatingRateDTO floatingRateDTO, BigDecimal annualNominalInterestRate, + List loanTermVariations) { + for (LoanTermVariations variationTerms : this.loanTermVariations) { + loanTermVariations.add(variationTerms.toData()); + } + annualNominalInterestRate = constructFloatingInterestRates(annualNominalInterestRate, floatingRateDTO, loanTermVariations); + return annualNominalInterestRate; + } + private LoanRepaymentScheduleInstallment getTotalOutstandingOnLoan() { Money feeCharges = Money.zero(loanCurrency()); Money penaltyCharges = Money.zero(loanCurrency()); @@ -5338,11 +5280,6 @@ public class Loan extends AbstractPersistable { final List disbursementData = getDisbursmentData(); - final List loanVariationTermsData = new ArrayList(); - LoanTermVariationsData data = new LoanTermVariationsData(null, - LoanEnumerations.loanvariationType(LoanTermVariationType.EMI_AMOUNT), expectedDisbursementDate, emiAmount); - loanVariationTermsData.add(data); - RecalculationFrequencyType recalculationFrequencyType = null; InterestRecalculationCompoundingMethod compoundingMethod = null; RecalculationFrequencyType compoundingFrequencyType = null; @@ -5354,30 +5291,16 @@ public class Loan extends AbstractPersistable { rescheduleStrategyMethod = this.loanInterestRecalculationDetails.getRescheduleStrategyMethod(); } - Collection loanTermVariations = new ArrayList<>(); - if (loanProduct.isLinkedToFloatingInterestRate()) { - Collection applicableRates = loanProduct.fetchInterestRates(floatingRateDTO); - LocalDate interestRateStartDate = DateUtils.getLocalDateOfTenant(); - for (FloatingRatePeriodData periodData : applicableRates) { - LoanTermVariationsData loanTermVariation = new LoanTermVariationsData( - LoanEnumerations.loanvariationType(LoanTermVariationType.INTEREST_RATE), periodData.getFromDateAsLocalDate(), - periodData.getInterestRate()); - if (interestRateStartDate.isAfter(periodData.getFromDateAsLocalDate())) { - interestRateStartDate = periodData.getFromDateAsLocalDate(); - annualNominalInterestRate = periodData.getInterestRate(); - } - loanTermVariations.add(loanTermVariation); - } - } + List loanTermVariations = new ArrayList<>(); + annualNominalInterestRate = constructFloatingInterestRates(annualNominalInterestRate, floatingRateDTO, loanTermVariations); return LoanApplicationTerms.assembleFrom(applicationCurrency, loanTermFrequency, loanTermPeriodFrequencyType, nthDayType, dayOfWeekType, expectedDisbursementDate, repaymentsStartingFromDate, calculatedRepaymentsStartingFromDate, inArrearsToleranceMoney, this.loanRepaymentScheduleDetail, loanProduct.isMultiDisburseLoan(), emiAmount, disbursementData, - maxOutstandingBalance, loanVariationTermsData, interestChargedFromDate, - this.loanProduct.getPrincipalThresholdForLastInstallment(), this.loanProduct.getInstallmentAmountInMultiplesOf(), - recalculationFrequencyType, restCalendarInstance, compoundingMethod, compoundingCalendarInstance, compoundingFrequencyType, - this.loanProduct.preCloseInterestCalculationStrategy(), rescheduleStrategyMethod, loanCalendar, getApprovedPrincipal(), - annualNominalInterestRate, loanTermVariations); + maxOutstandingBalance, interestChargedFromDate, this.loanProduct.getPrincipalThresholdForLastInstallment(), + this.loanProduct.getInstallmentAmountInMultiplesOf(), recalculationFrequencyType, restCalendarInstance, compoundingMethod, + compoundingCalendarInstance, compoundingFrequencyType, this.loanProduct.preCloseInterestCalculationStrategy(), + rescheduleStrategyMethod, loanCalendar, getApprovedPrincipal(), annualNominalInterestRate, loanTermVariations); } /** @@ -5624,12 +5547,36 @@ public class Loan extends AbstractPersistable { return this.interestRateDifferential; } - public void setIsFloatingInterestRate(Boolean isFloatingInterestRate) { - this.isFloatingInterestRate = isFloatingInterestRate; - } + public void setIsFloatingInterestRate(Boolean isFloatingInterestRate) { + this.isFloatingInterestRate = isFloatingInterestRate; + } + + public void setInterestRateDifferential(BigDecimal interestRateDifferential) { + this.interestRateDifferential = interestRateDifferential; + } + + public List getLoanTermVariations() { + return this.loanTermVariations; + } + + private int adjustNumberOfRepayments() { + int repaymetsForAdjust = 0; + for (LoanTermVariations loanTermVariations : this.loanTermVariations) { + if (loanTermVariations.getTermType().isInsertInstallment()) { + repaymetsForAdjust++; + } else if (loanTermVariations.getTermType().isDeleteInstallment()) { + repaymetsForAdjust--; + } + } + return repaymetsForAdjust; + } + + public int fetchNumberOfInstallmensAfterExceptions() { + return this.repaymentScheduleDetail().getNumberOfRepayments() + adjustNumberOfRepayments(); + } + + public void setExpectedFirstRepaymentOnDate(Date expectedFirstRepaymentOnDate) { + this.expectedFirstRepaymentOnDate = expectedFirstRepaymentOnDate; + } - public void setInterestRateDifferential(BigDecimal interestRateDifferential) { - this.interestRateDifferential = interestRateDifferential; - } - } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanAccountDomainService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanAccountDomainService.java index 165d0bde9..61fb09332 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanAccountDomainService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanAccountDomainService.java @@ -56,4 +56,6 @@ public interface LoanAccountDomainService { public ScheduleGeneratorDTO buildScheduleGeneratorDTO(final Loan loan); + void saveLoanWithDataIntegrityViolationChecks(Loan loan); + } 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 5e84d4869..0e344882a 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 @@ -303,6 +303,28 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { "Validation errors exist.", dataValidationErrors); } } } + + @Override + public void saveLoanWithDataIntegrityViolationChecks(final Loan loan) { + try { + List installments = loan.fetchRepaymentScheduleInstallments(); + for (LoanRepaymentScheduleInstallment installment : installments) { + if (installment.getId() == null) { + this.repaymentScheduleInstallmentRepository.save(installment); + } + } + this.loanRepository.save(loan); + } catch (final DataIntegrityViolationException e) { + final Throwable realCause = e.getCause(); + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction"); + if (realCause.getMessage().toLowerCase().contains("external_id_unique")) { + baseDataValidator.reset().parameter("externalId").failWithCode("value.must.be.unique"); + } + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", + "Validation errors exist.", dataValidationErrors); } + } + } @Override @Transactional 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 aaa636ad1..d7e13ec51 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 @@ -255,7 +255,7 @@ public class LoanCharge extends AbstractPersistable { this.amountPaid = null; if (isInstalmentFee()) { if (numberOfRepayments == null) { - numberOfRepayments = this.loan.repaymentScheduleDetail().getNumberOfRepayments(); + numberOfRepayments = this.loan.fetchNumberOfInstallmensAfterExceptions(); } this.amount = chargeAmount.multiply(BigDecimal.valueOf(numberOfRepayments)); } else { @@ -366,7 +366,7 @@ public class LoanCharge extends AbstractPersistable { case FLAT: if (isInstalmentFee()) { if (numberOfRepayments == null) { - numberOfRepayments = this.loan.repaymentScheduleDetail().getNumberOfRepayments(); + numberOfRepayments = this.loan.fetchNumberOfInstallmensAfterExceptions(); } this.amount = amount.multiply(BigDecimal.valueOf(numberOfRepayments)); } else { @@ -446,7 +446,7 @@ public class LoanCharge extends AbstractPersistable { break; case FLAT: if (isInstalmentFee()) { - this.amount = newValue.multiply(BigDecimal.valueOf(this.loan.repaymentScheduleDetail().getNumberOfRepayments())); + this.amount = newValue.multiply(BigDecimal.valueOf(this.loan.fetchNumberOfInstallmensAfterExceptions())); } else { this.amount = newValue; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java index 473bd0e88..5bb6eee9e 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java @@ -32,18 +32,18 @@ public class LoanRepaymentScheduleProcessingWrapper { for (final LoanRepaymentScheduleInstallment period : repaymentPeriods) { final Money feeChargesDueForRepaymentPeriod = cumulativeFeeChargesDueWithin(startDate, period.getDueDate(), loanCharges, - currency, period, repaymentPeriods.size(), totalPrincipal, totalInterest, true); + currency, period, totalPrincipal, totalInterest, !period.isRecalculatedInterestComponent()); final Money feeChargesWaivedForRepaymentPeriod = cumulativeFeeChargesWaivedWithin(startDate, period.getDueDate(), loanCharges, - currency, true); + currency, !period.isRecalculatedInterestComponent()); final Money feeChargesWrittenOffForRepaymentPeriod = cumulativeFeeChargesWrittenOffWithin(startDate, period.getDueDate(), - loanCharges, currency, true); + loanCharges, currency, !period.isRecalculatedInterestComponent()); final Money penaltyChargesDueForRepaymentPeriod = cumulativePenaltyChargesDueWithin(startDate, period.getDueDate(), - loanCharges, currency, period, repaymentPeriods.size(), totalPrincipal, totalInterest, true); + loanCharges, currency, period, totalPrincipal, totalInterest, !period.isRecalculatedInterestComponent()); final Money penaltyChargesWaivedForRepaymentPeriod = cumulativePenaltyChargesWaivedWithin(startDate, period.getDueDate(), - loanCharges, currency, true); + loanCharges, currency, !period.isRecalculatedInterestComponent()); final Money penaltyChargesWrittenOffForRepaymentPeriod = cumulativePenaltyChargesWrittenOffWithin(startDate, - period.getDueDate(), loanCharges, currency, true); + period.getDueDate(), loanCharges, currency, !period.isRecalculatedInterestComponent()); period.updateChargePortion(feeChargesDueForRepaymentPeriod, feeChargesWaivedForRepaymentPeriod, feeChargesWrittenOffForRepaymentPeriod, penaltyChargesDueForRepaymentPeriod, penaltyChargesWaivedForRepaymentPeriod, @@ -54,8 +54,8 @@ public class LoanRepaymentScheduleProcessingWrapper { } private Money cumulativeFeeChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, final Set loanCharges, - final MonetaryCurrency monetaryCurrency, LoanRepaymentScheduleInstallment period, int numberOfRepayments, - final Money totalPrincipal, final Money totalInterest, boolean isInstallmentChargeApplicable) { + final MonetaryCurrency monetaryCurrency, LoanRepaymentScheduleInstallment period, final Money totalPrincipal, + final Money totalInterest, boolean isInstallmentChargeApplicable) { Money cumulative = Money.zero(monetaryCurrency); @@ -75,7 +75,7 @@ public class LoanRepaymentScheduleProcessingWrapper { BigDecimal loanChargeAmt = amount.multiply(loanCharge.getPercentage()).divide(BigDecimal.valueOf(100)); cumulative = cumulative.plus(loanChargeAmt); } else { - cumulative = cumulative.plus(loanCharge.amount().divide(BigDecimal.valueOf(numberOfRepayments))); + cumulative = cumulative.plus(loanCharge.amountOrPercentage()); } } else if (loanCharge.isOverdueInstallmentCharge() && loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd) @@ -146,7 +146,7 @@ public class LoanRepaymentScheduleProcessingWrapper { private Money cumulativePenaltyChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, final Set loanCharges, final MonetaryCurrency currency, LoanRepaymentScheduleInstallment period, - int numberOfRepayments, final Money totalPrincipal, final Money totalInterest, boolean isInstallmentChargeApplicable) { + final Money totalPrincipal, final Money totalInterest, boolean isInstallmentChargeApplicable) { Money cumulative = Money.zero(currency); @@ -166,7 +166,7 @@ public class LoanRepaymentScheduleProcessingWrapper { BigDecimal loanChargeAmt = amount.multiply(loanCharge.getPercentage()).divide(BigDecimal.valueOf(100)); cumulative = cumulative.plus(loanChargeAmt); } else { - cumulative = cumulative.plus(loanCharge.amount().divide(BigDecimal.valueOf(numberOfRepayments))); + cumulative = cumulative.plus(loanCharge.amountOrPercentage()); } } else if (loanCharge.isOverdueInstallmentCharge() && loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd) 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 c609a47fc..c9ade17ce 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 @@ -322,4 +322,9 @@ public final class LoanSummary { public BigDecimal getTotalPenaltyChargesWaived() { return this.totalPenaltyChargesWaived; } + + + public BigDecimal getTotalExpectedRepayment() { + return this.totalExpectedRepayment; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTermVariationType.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTermVariationType.java index e2a778d2a..33b0ade7d 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTermVariationType.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTermVariationType.java @@ -9,7 +9,11 @@ public enum LoanTermVariationType { INVALID(0, "loanTermType.invalid"), // EMI_AMOUNT(1, "loanTermType.emiAmount"), // - INTEREST_RATE(2, "loanTermType.interestRate"); + INTEREST_RATE(2, "loanTermType.interestRate"), // + PRINCIPAL_AMOUNT(3, "loanTermType.principalAmount"), // + DUE_DATE(4, "loanTermType.dueDate"), // + INSERT_INSTALLMENT(5, "loanTermType.insertInstallment"), // + DELETE_INSTALLMENT(6, "loanTermType.deleteInstallment"); private final Integer value; private final String code; @@ -37,6 +41,18 @@ public enum LoanTermVariationType { case 2: enumeration = LoanTermVariationType.INTEREST_RATE; break; + case 3: + enumeration = LoanTermVariationType.PRINCIPAL_AMOUNT; + break; + case 4: + enumeration = LoanTermVariationType.DUE_DATE; + break; + case 5: + enumeration = LoanTermVariationType.INSERT_INSTALLMENT; + break; + case 6: + enumeration = LoanTermVariationType.DELETE_INSTALLMENT; + break; } return enumeration; } @@ -44,8 +60,24 @@ public enum LoanTermVariationType { public boolean isEMIAmountVariation() { return this.value.equals(LoanTermVariationType.EMI_AMOUNT.getValue()); } - + public boolean isInterestRateVariation() { return this.value.equals(LoanTermVariationType.INTEREST_RATE.getValue()); } + + public boolean isPrincipalAmountVariation() { + return this.value.equals(LoanTermVariationType.PRINCIPAL_AMOUNT.getValue()); + } + + public boolean isDueDateVariation() { + return this.value.equals(LoanTermVariationType.DUE_DATE.getValue()); + } + + public boolean isInsertInstallment() { + return this.value.equals(LoanTermVariationType.INSERT_INSTALLMENT.getValue()); + } + + public boolean isDeleteInstallment() { + return this.value.equals(LoanTermVariationType.DELETE_INSTALLMENT.getValue()); + } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTermVariations.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTermVariations.java index 13cb0e0e6..75eaed745 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTermVariations.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTermVariations.java @@ -34,18 +34,31 @@ public class LoanTermVariations extends AbstractPersistable { private Integer termType; @Temporal(TemporalType.DATE) - @Column(name = "applicable_from") + @Column(name = "applicable_date", nullable = false) private Date termApplicableFrom; - @Column(name = "term_value", scale = 6, precision = 19) - private BigDecimal termValue; + @Column(name = "decimal_value", scale = 6, precision = 19) + private BigDecimal decimalValue; - public LoanTermVariations(final Integer termType, final Date termApplicableFrom, final BigDecimal termValue, final Loan loan) { + @Temporal(TemporalType.DATE) + @Column(name = "date_value") + private Date dateValue; + @Column(name = "is_specific_to_installment", nullable = false) + private boolean isSpecificToInstallment; + + @Column(name = "applied_on_loan_status", nullable = false) + private Integer onLoanStatus; + + public LoanTermVariations(final Integer termType, final Date termApplicableFrom, final BigDecimal decimalValue, final Date dateValue, + final boolean isSpecificToInstallment, final Loan loan) { this.loan = loan; this.termApplicableFrom = termApplicableFrom; this.termType = termType; - this.termValue = termValue; + this.decimalValue = decimalValue; + this.dateValue = dateValue; + this.isSpecificToInstallment = isSpecificToInstallment; + this.onLoanStatus = loan.status().getValue(); } protected LoanTermVariations() { @@ -58,16 +71,45 @@ public class LoanTermVariations extends AbstractPersistable { public LoanTermVariationsData toData() { LocalDate termStartDate = new LocalDate(this.termApplicableFrom); + LocalDate dateValue = null; + if (this.dateValue != null) { + dateValue = new LocalDate(this.dateValue); + } EnumOptionData type = LoanEnumerations.loanvariationType(this.termType); - return new LoanTermVariationsData(getId(), type, termStartDate, this.termValue); + return new LoanTermVariationsData(getId(), type, termStartDate, this.decimalValue, dateValue, this.isSpecificToInstallment); } public Date getTermApplicableFrom() { return this.termApplicableFrom; } + public LocalDate fetchTermApplicaDate() { + return new LocalDate(this.termApplicableFrom); + } + public BigDecimal getTermValue() { - return this.termValue; + return this.decimalValue; + } + + public Date getDateValue() { + return this.dateValue; + } + + public LocalDate fetchDateValue() { + return this.dateValue == null ? null : new LocalDate(this.dateValue); + } + + public void setTermApplicableFrom(Date termApplicableFrom) { + this.termApplicableFrom = termApplicableFrom; + } + + public void setDecimalValue(BigDecimal decimalValue) { + this.decimalValue = decimalValue; + } + + + public Integer getOnLoanStatus() { + return this.onLoanStatus; } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTermVariationsComparator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTermVariationsComparator.java new file mode 100644 index 000000000..7f9f58bdd --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTermVariationsComparator.java @@ -0,0 +1,34 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.loanaccount.domain; + +import java.util.Comparator; + +/** + * Sort loan term variations + */ +public class LoanTermVariationsComparator implements Comparator { + + @Override + public int compare(final LoanTermVariations o1, final LoanTermVariations o2) { + int compareResult = 0; + final int comparsion = o1.getTermApplicableFrom().compareTo(o2.getTermApplicableFrom()); + /** + * For Terms bearing the same effective date, we sort based on modified + * date (when available) and new inserted installment + **/ + if (comparsion == 0) { + if (o2.getTermType().isDueDateVariation() || o2.getTermType().isInsertInstallment()) { + compareResult = 1; + } + } else { + compareResult = comparsion; + } + + return compareResult; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java index e8bdddb4f..5c962cfec 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java @@ -172,8 +172,9 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen * changedTransactionDetail accordingly **/ if (LoanTransaction.transactionAmountsMatch(currency, loanTransaction, newLoanTransaction)) { - loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(newLoanTransaction.getLoanTransactionToRepaymentScheduleMappings()); - } else{ + loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(newLoanTransaction + .getLoanTransactionToRepaymentScheduleMappings()); + } else { loanTransaction.reverse(); loanTransaction.updateExternalId(null); changedTransactionDetail.getNewTransactionMappings().put(loanTransaction.getId(), newLoanTransaction); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/guarantor/service/GuarantorWritePlatformServiceJpaRepositoryIImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/guarantor/service/GuarantorWritePlatformServiceJpaRepositoryIImpl.java index 6f89f313b..dd46a988a 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/guarantor/service/GuarantorWritePlatformServiceJpaRepositoryIImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/guarantor/service/GuarantorWritePlatformServiceJpaRepositoryIImpl.java @@ -124,11 +124,11 @@ public class GuarantorWritePlatformServiceJpaRepositoryIImpl implements Guaranto Guarantor guarantor = null; for (final Guarantor avilableGuarantor : existGuarantorList) { if (entityId != null && avilableGuarantor.getEntityId() != null && avilableGuarantor.getEntityId().equals(entityId) - && avilableGuarantor.getGurantorType() == guarantorTypeId && avilableGuarantor.isActive()) { + && avilableGuarantor.getGurantorType().equals(guarantorTypeId) && avilableGuarantor.isActive()) { if (guarantorCommand.getSavingsId() == null || avilableGuarantor.hasGuarantor(guarantorCommand.getSavingsId())) { /** Get the right guarantor based on guarantorType **/ String defaultUserMessage = null; - if (guarantorTypeId == GuarantorType.STAFF.getValue()) { + if (guarantorTypeId.equals(GuarantorType.STAFF.getValue())) { defaultUserMessage = this.staffRepositoryWrapper.findOneWithNotFoundDetection(entityId).displayName(); } else { defaultUserMessage = this.clientRepositoryWrapper.findOneWithNotFoundDetection(entityId).getDisplayName(); @@ -196,7 +196,7 @@ public class GuarantorWritePlatformServiceJpaRepositoryIImpl implements Guaranto if (guarantorType.isCustomer() || guarantorType.isStaff()) { final Long entityId = guarantorCommand.getEntityId(); for (final Guarantor guarantor : existGuarantorList) { - if (guarantor.getEntityId() == entityId && guarantor.getGurantorType() == guarantorTypeId + if (guarantor.getEntityId().equals(entityId) && guarantor.getGurantorType().equals(guarantorTypeId) && !guarantorForUpdate.getId().equals(guarantor.getId())) { String defaultUserMessage = this.clientRepositoryWrapper.findOneWithNotFoundDetection(entityId).getDisplayName(); defaultUserMessage = defaultUserMessage + " is already exist as a guarantor for this loan"; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/handler/LoanScheduleCreateVariationCommandHandler.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/handler/LoanScheduleCreateVariationCommandHandler.java new file mode 100644 index 000000000..a7bac1ed7 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/handler/LoanScheduleCreateVariationCommandHandler.java @@ -0,0 +1,32 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.loanaccount.handler; + +import org.mifosplatform.commands.annotation.CommandType; +import org.mifosplatform.commands.handler.NewCommandSourceHandler; +import org.mifosplatform.infrastructure.core.api.JsonCommand; +import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; +import org.mifosplatform.portfolio.loanaccount.loanschedule.service.LoanScheduleWritePlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +@CommandType(entity = "LOAN", action = "CREATESCHEDULEEXCEPTIONS") +public class LoanScheduleCreateVariationCommandHandler implements NewCommandSourceHandler { + + private final LoanScheduleWritePlatformService loanScheduleWritePlatformService; + + @Autowired + public LoanScheduleCreateVariationCommandHandler(final LoanScheduleWritePlatformService loanScheduleWritePlatformService) { + this.loanScheduleWritePlatformService = loanScheduleWritePlatformService; + } + + @Override + public CommandProcessingResult processCommand(JsonCommand command) { + return this.loanScheduleWritePlatformService.addLoanScheduleVariations(command.getLoanId(), command); + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/handler/LoanScheduleDeleteVariationCommandHandler.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/handler/LoanScheduleDeleteVariationCommandHandler.java new file mode 100644 index 000000000..76a857091 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/handler/LoanScheduleDeleteVariationCommandHandler.java @@ -0,0 +1,32 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.loanaccount.handler; + +import org.mifosplatform.commands.annotation.CommandType; +import org.mifosplatform.commands.handler.NewCommandSourceHandler; +import org.mifosplatform.infrastructure.core.api.JsonCommand; +import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; +import org.mifosplatform.portfolio.loanaccount.loanschedule.service.LoanScheduleWritePlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +@CommandType(entity = "LOAN", action = "DELETESCHEDULEEXCEPTIONS") +public class LoanScheduleDeleteVariationCommandHandler implements NewCommandSourceHandler { + + private final LoanScheduleWritePlatformService loanScheduleWritePlatformService; + + @Autowired + public LoanScheduleDeleteVariationCommandHandler(final LoanScheduleWritePlatformService loanScheduleWritePlatformService) { + this.loanScheduleWritePlatformService = loanScheduleWritePlatformService; + } + + @Override + public CommandProcessingResult processCommand(JsonCommand command) { + return this.loanScheduleWritePlatformService.deleteLoanScheduleVariations(command.getLoanId()); + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/data/LoanScheduleParams.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/data/LoanScheduleParams.java new file mode 100644 index 000000000..809e76010 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/data/LoanScheduleParams.java @@ -0,0 +1,436 @@ +/** + * 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.loanschedule.data; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.joda.time.LocalDate; +import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; +import org.mifosplatform.organisation.monetary.domain.Money; +import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; +import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; +import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.RecalculationDetail; + +public class LoanScheduleParams { + + // Actual period Number as per the schedule + private int periodNumber; + // Actual period Number plus interest only repayments + private int instalmentNumber; + + private LocalDate periodStartDate; + private LocalDate actualRepaymentDate; + + // variables for cumulative totals + private Money totalCumulativePrincipal; + private Money totalCumulativeInterest; + private Money totalFeeChargesCharged; + private Money totalPenaltyChargesCharged; + private Money totalRepaymentExpected; + private Money totalOutstandingInterestPaymentDueToGrace; + + // early payments will be added here and as per the selected strategy + // action will be performed on this value + private Money reducePrincipal; + + // principal changes will be added along with date(after applying rest) + // from when these amounts will effect the outstanding balance for + // interest calculation + private final Map principalPortionMap; + + // compounding(principal) amounts will be added along with + // date(after applying compounding frequency) + // from when these amounts will effect the outstanding balance for + // interest calculation + private final Map latePaymentMap; + + // compounding(interest/Fee) amounts will be added along with + // date(after applying compounding frequency) + // from when these amounts will effect the outstanding balance for + // interest calculation + private final TreeMap compoundingMap; + private final Map> compoundingDateVariations = new HashMap<>(); + + // disbursement map for tranche details(will added to outstanding + // balance as per the start date) + private final Map disburseDetailMap; + private Money principalToBeScheduled; + private Money outstandingBalance; + + // total outstanding balance as per rest for interest calculation. + private Money outstandingBalanceAsPerRest; + + private final List installments; + private final Collection recalculationDetails; + private final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor; + private final LocalDate scheduleTillDate; + private final boolean partialUpdate; + private int loanTermInDays; + private final MonetaryCurrency currency; + private final boolean applyInterestRecalculation; + + private LoanScheduleParams(final int periodNumber, final int instalmentNumber, int loanTermInDays, LocalDate periodStartDate, + final LocalDate actualRepaymentDate, final Money totalCumulativePrincipal, final Money totalCumulativeInterest, + final Money totalFeeChargesCharged, final Money totalPenaltyChargesCharged, final Money totalRepaymentExpected, + Money totalOutstandingInterestPaymentDueToGrace, final Money reducePrincipal, final Map principalPortionMap, + final Map latePaymentMap, final TreeMap compoundingMap, + final Map disburseDetailMap, Money principalToBeScheduled, final Money outstandingBalance, + final Money outstandingBalanceAsPerRest, final List installments, + final Collection recalculationDetails, + final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, final LocalDate scheduleTillDate, + final boolean partialUpdate, final MonetaryCurrency currency, final boolean applyInterestRecalculation) { + this.periodNumber = periodNumber; + this.instalmentNumber = instalmentNumber; + this.loanTermInDays = loanTermInDays; + this.periodStartDate = periodStartDate; + this.actualRepaymentDate = actualRepaymentDate; + this.totalCumulativePrincipal = totalCumulativePrincipal; + this.totalCumulativeInterest = totalCumulativeInterest; + this.totalFeeChargesCharged = totalFeeChargesCharged; + this.totalPenaltyChargesCharged = totalPenaltyChargesCharged; + this.totalRepaymentExpected = totalRepaymentExpected; + this.totalOutstandingInterestPaymentDueToGrace = totalOutstandingInterestPaymentDueToGrace; + this.reducePrincipal = reducePrincipal; + this.principalPortionMap = principalPortionMap; + this.latePaymentMap = latePaymentMap; + this.compoundingMap = compoundingMap; + this.disburseDetailMap = disburseDetailMap; + this.principalToBeScheduled = principalToBeScheduled; + this.outstandingBalance = outstandingBalance; + this.outstandingBalanceAsPerRest = outstandingBalanceAsPerRest; + this.installments = installments; + this.recalculationDetails = recalculationDetails; + this.loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessor; + this.scheduleTillDate = scheduleTillDate; + this.partialUpdate = partialUpdate; + this.currency = currency; + this.applyInterestRecalculation = applyInterestRecalculation; + } + + public static LoanScheduleParams createLoanScheduleParamsForPartialUpdate(final int periodNumber, final int instalmentNumber, + int loanTermInDays, LocalDate periodStartDate, final LocalDate actualRepaymentDate, final Money totalCumulativePrincipal, + final Money totalCumulativeInterest, final Money totalFeeChargesCharged, final Money totalPenaltyChargesCharged, + final Money totalRepaymentExpected, Money totalOutstandingInterestPaymentDueToGrace, final Money reducePrincipal, + final Map principalPortionMap, final Map latePaymentMap, + final TreeMap compoundingMap, final Map disburseDetailMap, Money principalToBeScheduled, + final Money outstandingBalance, final Money outstandingBalanceAsPerRest, + final List installments, final Collection recalculationDetails, + final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, final LocalDate scheduleTillDate, + final MonetaryCurrency currency, final boolean applyInterestRecalculation) { + final boolean partialUpdate = true; + return new LoanScheduleParams(periodNumber, instalmentNumber, loanTermInDays, periodStartDate, actualRepaymentDate, + totalCumulativePrincipal, totalCumulativeInterest, totalFeeChargesCharged, totalPenaltyChargesCharged, + totalRepaymentExpected, totalOutstandingInterestPaymentDueToGrace, reducePrincipal, principalPortionMap, latePaymentMap, + compoundingMap, disburseDetailMap, principalToBeScheduled, outstandingBalance, outstandingBalanceAsPerRest, installments, + recalculationDetails, loanRepaymentScheduleTransactionProcessor, scheduleTillDate, partialUpdate, currency, + applyInterestRecalculation); + } + + public static LoanScheduleParams createLoanScheduleParamsForCompleteUpdate(final Collection recalculationDetails, + final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, final LocalDate scheduleTillDate, + final boolean applyInterestRecalculation) { + final int periodNumber = 1; + final int instalmentNumber = 1; + final LocalDate periodStartDate = null; + final LocalDate actualRepaymentDate = null; + final Money totalCumulativePrincipal = null; + final Money totalCumulativeInterest = null; + final Money totalFeeChargesCharged = null; + final Money totalPenaltyChargesCharged = null; + final Money totalRepaymentExpected = null; + final Money reducePrincipal = null; + final Map principalPortionMap = null; + final Map latePaymentMap = null; + final TreeMap compoundingMap = null; + final Map disburseDetailMap = null; + final Money principalToBeScheduled = null; + final Money outstandingBalance = null; + final Money outstandingBalanceAsPerRest = null; + final List installments = null; + final boolean partialUpdate = false; + final int loanTermInDays = 0; + final Money totalOutstandingInterestPaymentDueToGrace = null; + final MonetaryCurrency currency = null; + return new LoanScheduleParams(periodNumber, instalmentNumber, loanTermInDays, periodStartDate, actualRepaymentDate, + totalCumulativePrincipal, totalCumulativeInterest, totalFeeChargesCharged, totalPenaltyChargesCharged, + totalRepaymentExpected, totalOutstandingInterestPaymentDueToGrace, reducePrincipal, principalPortionMap, latePaymentMap, + compoundingMap, disburseDetailMap, principalToBeScheduled, outstandingBalance, outstandingBalanceAsPerRest, installments, + recalculationDetails, loanRepaymentScheduleTransactionProcessor, scheduleTillDate, partialUpdate, currency, + applyInterestRecalculation); + } + + public static LoanScheduleParams createLoanScheduleParams(final MonetaryCurrency currency, final Money chargesDueAtTimeOfDisbursement, + final LocalDate periodStartDate, final Money principalToBeScheduled) { + final int loanTermInDays = 0; + final int periodNumber = 1; + final int instalmentNumber = 1; + final Money totalCumulativePrincipal = Money.zero(currency); + final Money totalCumulativeInterest = Money.zero(currency); + final Money totalOutstandingInterestPaymentDueToGrace = Money.zero(currency); + final LocalDate actualRepaymentDate = periodStartDate; + final Money totalFeeChargesCharged = chargesDueAtTimeOfDisbursement; + final Money totalPenaltyChargesCharged = Money.zero(currency); + final Money totalRepaymentExpected = chargesDueAtTimeOfDisbursement; + final Money reducePrincipal = Money.zero(currency); + final Map principalPortionMap = new HashMap<>(); + final Map latePaymentMap = new HashMap<>(); + final TreeMap compoundingMap = new TreeMap<>(); + final Map disburseDetailMap = new TreeMap<>(); + final Money outstandingBalance = principalToBeScheduled; + final Money outstandingBalanceAsPerRest = principalToBeScheduled; + final List installments = new ArrayList<>(); + final boolean partialUpdate = false; + final Collection recalculationDetails = null; + final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = null; + final LocalDate scheduleTillDate = null; + final boolean applyInterestRecalculation = false; + return new LoanScheduleParams(periodNumber, instalmentNumber, loanTermInDays, periodStartDate, actualRepaymentDate, + totalCumulativePrincipal, totalCumulativeInterest, totalFeeChargesCharged, totalPenaltyChargesCharged, + totalRepaymentExpected, totalOutstandingInterestPaymentDueToGrace, reducePrincipal, principalPortionMap, latePaymentMap, + compoundingMap, disburseDetailMap, principalToBeScheduled, outstandingBalance, outstandingBalanceAsPerRest, installments, + recalculationDetails, loanRepaymentScheduleTransactionProcessor, scheduleTillDate, partialUpdate, currency, + applyInterestRecalculation); + } + + public static LoanScheduleParams createLoanScheduleParams(final MonetaryCurrency currency, final Money chargesDueAtTimeOfDisbursement, + final LocalDate periodStartDate, final Money principalToBeScheduled, final LoanScheduleParams loanScheduleParams) { + final int loanTermInDays = 0; + final int periodNumber = 1; + final int instalmentNumber = 1; + final Money totalCumulativePrincipal = Money.zero(currency); + final Money totalCumulativeInterest = Money.zero(currency); + final Money totalOutstandingInterestPaymentDueToGrace = Money.zero(currency); + final LocalDate actualRepaymentDate = periodStartDate; + final Money totalFeeChargesCharged = chargesDueAtTimeOfDisbursement; + final Money totalPenaltyChargesCharged = Money.zero(currency); + final Money totalRepaymentExpected = chargesDueAtTimeOfDisbursement; + final Money reducePrincipal = Money.zero(currency); + final Map principalPortionMap = new HashMap<>(); + final Map latePaymentMap = new HashMap<>(); + final TreeMap compoundingMap = new TreeMap<>(); + final Map disburseDetailMap = new TreeMap<>(); + final Money outstandingBalance = principalToBeScheduled; + final Money outstandingBalanceAsPerRest = principalToBeScheduled; + final List installments = new ArrayList<>(); + final boolean partialUpdate = false; + final Collection recalculationDetails = loanScheduleParams.recalculationDetails; + final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanScheduleParams.loanRepaymentScheduleTransactionProcessor; + final LocalDate scheduleTillDate = loanScheduleParams.scheduleTillDate; + final boolean applyInterestRecalculation = loanScheduleParams.applyInterestRecalculation; + return new LoanScheduleParams(periodNumber, instalmentNumber, loanTermInDays, periodStartDate, actualRepaymentDate, + totalCumulativePrincipal, totalCumulativeInterest, totalFeeChargesCharged, totalPenaltyChargesCharged, + totalRepaymentExpected, totalOutstandingInterestPaymentDueToGrace, reducePrincipal, principalPortionMap, latePaymentMap, + compoundingMap, disburseDetailMap, principalToBeScheduled, outstandingBalance, outstandingBalanceAsPerRest, installments, + recalculationDetails, loanRepaymentScheduleTransactionProcessor, scheduleTillDate, partialUpdate, currency, + applyInterestRecalculation); + } + + public int getPeriodNumber() { + return this.periodNumber; + } + + public int getInstalmentNumber() { + return this.instalmentNumber; + } + + public LocalDate getActualRepaymentDate() { + return this.actualRepaymentDate; + } + + public Money getTotalCumulativePrincipal() { + return this.totalCumulativePrincipal; + } + + public void addTotalCumulativePrincipal(final Money totalCumulativePrincipal) { + this.totalCumulativePrincipal = this.totalCumulativePrincipal.plus(totalCumulativePrincipal); + } + + public Money getTotalCumulativeInterest() { + return this.totalCumulativeInterest; + } + + public void addTotalCumulativeInterest(final Money totalCumulativeInterest) { + this.totalCumulativeInterest = this.totalCumulativeInterest.plus(totalCumulativeInterest); + } + + public Money getTotalFeeChargesCharged() { + return this.totalFeeChargesCharged; + } + + public void addTotalFeeChargesCharged(final Money totalFeeChargesCharged) { + this.totalFeeChargesCharged = this.totalFeeChargesCharged.plus(totalFeeChargesCharged); + } + + public Money getTotalPenaltyChargesCharged() { + return this.totalPenaltyChargesCharged; + } + + public void addTotalPenaltyChargesCharged(final Money totalPenaltyChargesCharged) { + this.totalPenaltyChargesCharged = this.totalPenaltyChargesCharged.plus(totalPenaltyChargesCharged); + } + + public Money getTotalRepaymentExpected() { + return this.totalRepaymentExpected; + } + + public void addTotalRepaymentExpected(final Money totalRepaymentExpected) { + this.totalRepaymentExpected = this.totalRepaymentExpected.plus(totalRepaymentExpected); + } + + public Money getReducePrincipal() { + return this.reducePrincipal; + } + + public Map getPrincipalPortionMap() { + return this.principalPortionMap; + } + + public Map getLatePaymentMap() { + return this.latePaymentMap; + } + + public TreeMap getCompoundingMap() { + return this.compoundingMap; + } + + public Map getDisburseDetailMap() { + return this.disburseDetailMap; + } + + public Money getOutstandingBalance() { + return this.outstandingBalance; + } + + public Money getOutstandingBalanceAsPerRest() { + return this.outstandingBalanceAsPerRest; + } + + public List getInstallments() { + return this.installments; + } + + public Collection getRecalculationDetails() { + return this.recalculationDetails; + } + + public LoanRepaymentScheduleTransactionProcessor getLoanRepaymentScheduleTransactionProcessor() { + return this.loanRepaymentScheduleTransactionProcessor; + } + + public LocalDate getScheduleTillDate() { + return this.scheduleTillDate; + } + + public boolean isPartialUpdate() { + return this.partialUpdate; + } + + public LocalDate getPeriodStartDate() { + return this.periodStartDate; + } + + public Money getPrincipalToBeScheduled() { + return this.principalToBeScheduled; + } + + public void setPeriodNumber(int periodNumber) { + this.periodNumber = periodNumber; + } + + public void incrementPeriodNumber() { + this.periodNumber++; + } + + public void incrementInstalmentNumber() { + this.instalmentNumber++; + } + + public void setPeriodStartDate(LocalDate periodStartDate) { + this.periodStartDate = periodStartDate; + } + + public void setActualRepaymentDate(LocalDate actualRepaymentDate) { + this.actualRepaymentDate = actualRepaymentDate; + } + + public void setReducePrincipal(Money reducePrincipal) { + this.reducePrincipal = reducePrincipal; + } + + public void addReducePrincipal(Money reducePrincipal) { + this.reducePrincipal = this.reducePrincipal.plus(reducePrincipal); + } + + public void reduceReducePrincipal(Money reducePrincipal) { + this.reducePrincipal = this.reducePrincipal.minus(reducePrincipal); + } + + public void setPrincipalToBeScheduled(Money principalToBeScheduled) { + this.principalToBeScheduled = principalToBeScheduled; + } + + public void addPrincipalToBeScheduled(Money principalToBeScheduled) { + this.principalToBeScheduled = this.principalToBeScheduled.plus(principalToBeScheduled); + } + + public void setOutstandingBalance(Money outstandingBalance) { + this.outstandingBalance = outstandingBalance; + } + + public void addOutstandingBalance(Money outstandingBalance) { + this.outstandingBalance = this.outstandingBalance.plus(outstandingBalance); + } + + public void reduceOutstandingBalance(Money outstandingBalance) { + this.outstandingBalance = this.outstandingBalance.minus(outstandingBalance); + } + + public void setOutstandingBalanceAsPerRest(Money outstandingBalanceAsPerRest) { + this.outstandingBalanceAsPerRest = outstandingBalanceAsPerRest; + } + + public void addOutstandingBalanceAsPerRest(Money outstandingBalanceAsPerRest) { + this.outstandingBalanceAsPerRest = this.outstandingBalanceAsPerRest.plus(outstandingBalanceAsPerRest); + } + + public void reduceOutstandingBalanceAsPerRest(Money outstandingBalanceAsPerRest) { + this.outstandingBalanceAsPerRest = this.outstandingBalanceAsPerRest.minus(outstandingBalanceAsPerRest); + } + + public int getLoanTermInDays() { + return this.loanTermInDays; + } + + public void addLoanTermInDays(int loanTermInDays) { + this.loanTermInDays += loanTermInDays; + } + + public Money getTotalOutstandingInterestPaymentDueToGrace() { + return this.totalOutstandingInterestPaymentDueToGrace; + } + + public void setTotalOutstandingInterestPaymentDueToGrace(Money totalOutstandingInterestPaymentDueToGrace) { + this.totalOutstandingInterestPaymentDueToGrace = totalOutstandingInterestPaymentDueToGrace; + } + + public Map> getCompoundingDateVariations() { + return this.compoundingDateVariations; + } + + public MonetaryCurrency getCurrency() { + return this.currency; + } + + public boolean applyInterestRecalculation() { + return this.applyInterestRecalculation; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/data/LoanScheduleRecalculationDTO.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/data/LoanScheduleRecalculationDTO.java deleted file mode 100644 index 2f9803308..000000000 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/data/LoanScheduleRecalculationDTO.java +++ /dev/null @@ -1,212 +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.loanschedule.data; - -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; - -import org.joda.time.LocalDate; -import org.mifosplatform.organisation.monetary.domain.Money; -import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; -import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; -import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.RecalculationDetail; - -public class LoanScheduleRecalculationDTO { - - private final int periodNumber; - private final int instalmentNumber; - private final LocalDate periodStartDate; - private final LocalDate actualRepaymentDate; - private final Money totalCumulativePrincipal; - private final Money totalCumulativeInterest; - private final Money totalFeeChargesCharged; - private final Money totalPenaltyChargesCharged; - private final Money totalRepaymentExpected; - private final Money reducePrincipal; - private final Map principalPortionMap; - private final Map latePaymentMap; - private final TreeMap compoundingMap; - private final Map disburseDetailMap; - private final Money principalToBeScheduled; - private final Money outstandingBalance; - private final Money outstandingBalanceAsPerRest; - private final List installments; - private final Collection recalculationDetails; - private final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor; - private final LocalDate scheduleTillDate; - private final boolean partialUpdate; - - private LoanScheduleRecalculationDTO(final int periodNumber, final int instalmentNumber, LocalDate periodStartDate, - final LocalDate actualRepaymentDate, final Money totalCumulativePrincipal, final Money totalCumulativeInterest, - final Money totalFeeChargesCharged, final Money totalPenaltyChargesCharged, final Money totalRepaymentExpected, - final Money reducePrincipal, final Map principalPortionMap, final Map latePaymentMap, - final TreeMap compoundingMap, final Map disburseDetailMap, Money principalToBeScheduled, - final Money outstandingBalance, final Money outstandingBalanceAsPerRest, - final List installments, final Collection recalculationDetails, - final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, final LocalDate scheduleTillDate, - final boolean partialUpdate) { - this.periodNumber = periodNumber; - this.instalmentNumber = instalmentNumber; - this.periodStartDate = periodStartDate; - this.actualRepaymentDate = actualRepaymentDate; - this.totalCumulativePrincipal = totalCumulativePrincipal; - this.totalCumulativeInterest = totalCumulativeInterest; - this.totalFeeChargesCharged = totalFeeChargesCharged; - this.totalPenaltyChargesCharged = totalPenaltyChargesCharged; - this.totalRepaymentExpected = totalRepaymentExpected; - this.reducePrincipal = reducePrincipal; - this.principalPortionMap = principalPortionMap; - this.latePaymentMap = latePaymentMap; - this.compoundingMap = compoundingMap; - this.disburseDetailMap = disburseDetailMap; - this.principalToBeScheduled = principalToBeScheduled; - this.outstandingBalance = outstandingBalance; - this.outstandingBalanceAsPerRest = outstandingBalanceAsPerRest; - this.installments = installments; - this.recalculationDetails = recalculationDetails; - this.loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessor; - this.scheduleTillDate = scheduleTillDate; - this.partialUpdate = partialUpdate; - } - - public static LoanScheduleRecalculationDTO createLoanScheduleDTOForPartialUpdate(final int periodNumber, final int instalmentNumber, - LocalDate periodStartDate, final LocalDate actualRepaymentDate, final Money totalCumulativePrincipal, - final Money totalCumulativeInterest, final Money totalFeeChargesCharged, final Money totalPenaltyChargesCharged, - final Money totalRepaymentExpected, final Money reducePrincipal, final Map principalPortionMap, - final Map latePaymentMap, final TreeMap compoundingMap, - final Map disburseDetailMap, Money principalToBeScheduled, final Money outstandingBalance, - final Money outstandingBalanceAsPerRest, final List installments, - final Collection recalculationDetails, - final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, final LocalDate scheduleTillDate) { - final boolean partialUpdate = true; - return new LoanScheduleRecalculationDTO(periodNumber, instalmentNumber, periodStartDate, actualRepaymentDate, - totalCumulativePrincipal, totalCumulativeInterest, totalFeeChargesCharged, totalPenaltyChargesCharged, - totalRepaymentExpected, reducePrincipal, principalPortionMap, latePaymentMap, compoundingMap, disburseDetailMap, - principalToBeScheduled, outstandingBalance, outstandingBalanceAsPerRest, installments, recalculationDetails, - loanRepaymentScheduleTransactionProcessor, scheduleTillDate, partialUpdate); - } - - public static LoanScheduleRecalculationDTO createLoanScheduleDTOForCompleteUpdate( - final Collection recalculationDetails, - final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, final LocalDate scheduleTillDate) { - final int periodNumber = 1; - final int instalmentNumber = 1; - final LocalDate periodStartDate = null; - final LocalDate actualRepaymentDate = null; - final Money totalCumulativePrincipal = null; - final Money totalCumulativeInterest = null; - final Money totalFeeChargesCharged = null; - final Money totalPenaltyChargesCharged = null; - final Money totalRepaymentExpected = null; - final Money reducePrincipal = null; - final Map principalPortionMap = null; - final Map latePaymentMap = null; - final TreeMap compoundingMap = null; - final Map disburseDetailMap = null; - final Money principalToBeScheduled = null; - final Money outstandingBalance = null; - final Money outstandingBalanceAsPerRest = null; - final List installments = null; - final boolean partialUpdate = false; - return new LoanScheduleRecalculationDTO(periodNumber, instalmentNumber, periodStartDate, actualRepaymentDate, - totalCumulativePrincipal, totalCumulativeInterest, totalFeeChargesCharged, totalPenaltyChargesCharged, - totalRepaymentExpected, reducePrincipal, principalPortionMap, latePaymentMap, compoundingMap, disburseDetailMap, - principalToBeScheduled, outstandingBalance, outstandingBalanceAsPerRest, installments, recalculationDetails, - loanRepaymentScheduleTransactionProcessor, scheduleTillDate, partialUpdate); - } - - public int getPeriodNumber() { - return this.periodNumber; - } - - public int getInstalmentNumber() { - return this.instalmentNumber; - } - - public LocalDate getActualRepaymentDate() { - return this.actualRepaymentDate; - } - - public Money getTotalCumulativePrincipal() { - return this.totalCumulativePrincipal; - } - - public Money getTotalCumulativeInterest() { - return this.totalCumulativeInterest; - } - - public Money getTotalFeeChargesCharged() { - return this.totalFeeChargesCharged; - } - - public Money getTotalPenaltyChargesCharged() { - return this.totalPenaltyChargesCharged; - } - - public Money getTotalRepaymentExpected() { - return this.totalRepaymentExpected; - } - - public Money getReducePrincipal() { - return this.reducePrincipal; - } - - public Map getPrincipalPortionMap() { - return this.principalPortionMap; - } - - public Map getLatePaymentMap() { - return this.latePaymentMap; - } - - public TreeMap getCompoundingMap() { - return this.compoundingMap; - } - - public Map getDisburseDetailMap() { - return this.disburseDetailMap; - } - - public Money getOutstandingBalance() { - return this.outstandingBalance; - } - - public Money getOutstandingBalanceAsPerRest() { - return this.outstandingBalanceAsPerRest; - } - - public List getInstallments() { - return this.installments; - } - - public Collection getRecalculationDetails() { - return this.recalculationDetails; - } - - public LoanRepaymentScheduleTransactionProcessor getLoanRepaymentScheduleTransactionProcessor() { - return this.loanRepaymentScheduleTransactionProcessor; - } - - public LocalDate getScheduleTillDate() { - return this.scheduleTillDate; - } - - public boolean isPartialUpdate() { - return this.partialUpdate; - } - - public LocalDate getPeriodStartDate() { - return this.periodStartDate; - } - - - public Money getPrincipalToBeScheduled() { - return this.principalToBeScheduled; - } - -} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java index 363bc1d77..6644dc716 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java @@ -39,18 +39,15 @@ import org.mifosplatform.portfolio.loanaccount.domain.LoanSummary; import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction; import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO; -import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleRecalculationDTO; +import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleParams; import org.mifosplatform.portfolio.loanaccount.loanschedule.exception.MultiDisbursementEmiAmountException; import org.mifosplatform.portfolio.loanaccount.loanschedule.exception.MultiDisbursementOutstandingAmoutException; +import org.mifosplatform.portfolio.loanaccount.loanschedule.exception.ScheduleDateException; import org.mifosplatform.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleModel; import org.mifosplatform.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleModelRepaymentPeriod; import org.mifosplatform.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequest; -import org.mifosplatform.portfolio.loanproduct.domain.AmortizationMethod; import org.mifosplatform.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail; -/** - * - */ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGenerator { private final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator(); @@ -59,17 +56,21 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener @Override public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTerms loanApplicationTerms, final Set loanCharges, final HolidayDetailDTO holidayDetailDTO) { - final LoanScheduleRecalculationDTO loanScheduleRecalculationDTO = null; + final LoanScheduleParams loanScheduleRecalculationDTO = null; return generate(mc, loanApplicationTerms, loanCharges, holidayDetailDTO, loanScheduleRecalculationDTO); } private LoanScheduleModel generate(final MathContext mc, final LoanApplicationTerms loanApplicationTerms, - final Set loanCharges, final HolidayDetailDTO holidayDetailDTO, - final LoanScheduleRecalculationDTO loanScheduleRecalculationDTO) { + final Set loanCharges, final HolidayDetailDTO holidayDetailDTO, final LoanScheduleParams loanScheduleParams) { final ApplicationCurrency applicationCurrency = loanApplicationTerms.getApplicationCurrency(); // generate list of proposed schedule due dates - final LocalDate loanEndDate = this.scheduledDateGenerator.getLastRepaymentDate(loanApplicationTerms, holidayDetailDTO); + LocalDate loanEndDate = this.scheduledDateGenerator.getLastRepaymentDate(loanApplicationTerms, holidayDetailDTO); + LoanTermVariationsData lastDueDateVariation = loanApplicationTerms.getLoanTermVariations().fetchLoanTermDueDateVariationsData( + loanEndDate); + if (lastDueDateVariation != null) { + loanEndDate = lastDueDateVariation.getDateValue(); + } loanApplicationTerms.updateLoanEndDate(loanEndDate); // determine the total charges due at time of disbursement @@ -78,107 +79,67 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener // setup variables for tracking important facts required for loan // schedule generation. - Money principalToBeScheduled = getPrincipalToBeScheduled(loanApplicationTerms); - final MonetaryCurrency currency = principalToBeScheduled.getCurrency(); - final int numberOfRepayments = loanApplicationTerms.getNumberOfRepayments(); + final MonetaryCurrency currency = loanApplicationTerms.getCurrency(); + final int numberOfRepayments = loanApplicationTerms.fetchNumberOfRepaymentsAfterExceptions(); - final LocalDate scheduleTillDate = loanScheduleRecalculationDTO == null ? null : loanScheduleRecalculationDTO.getScheduleTillDate(); - final Collection transactions = loanScheduleRecalculationDTO == null ? null : loanScheduleRecalculationDTO - .getRecalculationDetails(); - final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanScheduleRecalculationDTO == null ? null - : loanScheduleRecalculationDTO.getLoanRepaymentScheduleTransactionProcessor(); + LoanScheduleParams scheduleParams = null; + if (loanScheduleParams == null) { + scheduleParams = LoanScheduleParams.createLoanScheduleParams(currency, Money.of(currency, chargesDueAtTimeOfDisbursement), + loanApplicationTerms.getExpectedDisbursementDate(), getPrincipalToBeScheduled(loanApplicationTerms)); + } else if (!loanScheduleParams.isPartialUpdate()) { + scheduleParams = LoanScheduleParams + .createLoanScheduleParams(currency, Money.of(currency, chargesDueAtTimeOfDisbursement), + loanApplicationTerms.getExpectedDisbursementDate(), getPrincipalToBeScheduled(loanApplicationTerms), + loanScheduleParams); + } else { + scheduleParams = loanScheduleParams; + } - // variables for cumulative totals - int loanTermInDays = Integer.valueOf(0); - final BigDecimal totalPrincipalPaid = BigDecimal.ZERO; - BigDecimal totalFeeChargesCharged = chargesDueAtTimeOfDisbursement; - BigDecimal totalPenaltyChargesCharged = BigDecimal.ZERO; - BigDecimal totalRepaymentExpected = chargesDueAtTimeOfDisbursement; - final BigDecimal totalOutstanding = BigDecimal.ZERO; - Money totalCumulativePrincipal = principalToBeScheduled.zero(); - Money totalCumulativeInterest = principalToBeScheduled.zero(); - Money totalOutstandingInterestPaymentDueToGrace = principalToBeScheduled.zero(); + final Collection transactions = scheduleParams.getRecalculationDetails(); + final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = scheduleParams + .getLoanRepaymentScheduleTransactionProcessor(); final Collection periods = createNewLoanScheduleListWithDisbursementDetails(numberOfRepayments, loanApplicationTerms, chargesDueAtTimeOfDisbursement); // Determine the total interest owed over the full loan for FLAT // interest method . - Money totalInterestChargedForFullLoanTerm = loanApplicationTerms.calculateTotalInterestCharged( + final Money totalInterestChargedForFullLoanTerm = loanApplicationTerms.calculateTotalInterestCharged( this.paymentPeriodsInOneYearCalculator, mc); - LocalDate periodStartDate = loanApplicationTerms.getExpectedDisbursementDate(); - LocalDate actualRepaymentDate = periodStartDate; boolean isFirstRepayment = true; - LocalDate firstRepaymentdate = this.scheduledDateGenerator.generateNextRepaymentDate(periodStartDate, loanApplicationTerms, - isFirstRepayment, holidayDetailDTO); + LocalDate firstRepaymentdate = this.scheduledDateGenerator.generateNextRepaymentDate(scheduleParams.getPeriodStartDate(), + loanApplicationTerms, isFirstRepayment, holidayDetailDTO); final LocalDate idealDisbursementDate = this.scheduledDateGenerator.idealDisbursementDateBasedOnFirstRepaymentDate( loanApplicationTerms.getLoanTermPeriodFrequencyType(), loanApplicationTerms.getRepaymentEvery(), firstRepaymentdate); - LocalDate periodStartDateApplicableForInterest = periodStartDate; + if (!scheduleParams.isPartialUpdate()) { + // Set Fixed Principal Amount + updateAmortization(mc, loanApplicationTerms, scheduleParams.getPeriodNumber(), scheduleParams.getOutstandingBalance()); - // Actual period Number as per the schedule - int periodNumber = 1; - // Actual period Number plus interest only repayments - int instalmentNumber = 1; - - // actual outstanding balance for interest calculation - Money outstandingBalance = principalToBeScheduled; - - //Set Fixed Principal Amount - if(loanApplicationTerms.getAmortizationMethod().equals(AmortizationMethod.EQUAL_PRINCIPAL)) { - loanApplicationTerms.updateFixedPrincipalAmount(mc, periodNumber, outstandingBalance) ; - } - - // Set fixed EMI Amount - if (loanApplicationTerms.getFixedEmiAmount() == null) { - updateFixedInstallmentAmount(mc, loanApplicationTerms, actualRepaymentDate, periodNumber, outstandingBalance, holidayDetailDTO); - } - - // disbursement map for tranche details(will added to outstanding - // balance as per the start date) - final Map disburseDetailMap = new HashMap<>(); - if (loanApplicationTerms.isMultiDisburseLoan()) { - // fetches the first tranche amount and also updates other tranche - // details to map - BigDecimal disburseAmt = getDisbursementAmount(loanApplicationTerms, periodStartDate, periods, chargesDueAtTimeOfDisbursement, - disburseDetailMap, isInterestRecalculationRequired(loanApplicationTerms, transactions)); - principalToBeScheduled = principalToBeScheduled.zero().plus(disburseAmt); - loanApplicationTerms.setPrincipal(loanApplicationTerms.getPrincipal().zero().plus(disburseAmt)); - outstandingBalance = outstandingBalance.zero().plus(disburseAmt); + if (loanApplicationTerms.isMultiDisburseLoan()) { + // fetches the first tranche amount and also updates other + // tranche + // details to map + BigDecimal disburseAmt = getDisbursementAmount(loanApplicationTerms, scheduleParams.getPeriodStartDate(), periods, + chargesDueAtTimeOfDisbursement, scheduleParams.getDisburseDetailMap(), scheduleParams.applyInterestRecalculation()); + scheduleParams.setPrincipalToBeScheduled(Money.of(currency, disburseAmt)); + loanApplicationTerms.setPrincipal(loanApplicationTerms.getPrincipal().zero().plus(disburseAmt)); + scheduleParams.setOutstandingBalance(Money.of(currency, disburseAmt)); + scheduleParams.setOutstandingBalanceAsPerRest(Money.of(currency, disburseAmt)); + } } // charges which depends on total loan interest will be added to this // set and handled separately after all installments generated final Set nonCompoundingCharges = seperateTotalCompoundingPercentageCharges(loanCharges); - // total outstanding balance as per rest for interest calculation. - Money outstandingBalanceAsPerRest = outstandingBalance; - // early payments will be added here and as per the selected strategy - // action will be performed on this value - Money reducePrincipal = totalCumulativePrincipal.zero(); - - // principal changes will be added along with date(after applying rest) - // from when these amounts will effect the outstanding balance for - // interest calculation - final Map principalPortionMap = new HashMap<>(); - // compounding(principal) amounts will be added along with - // date(after applying compounding frequency) - // from when these amounts will effect the outstanding balance for - // interest calculation - final Map latePaymentMap = new HashMap<>(); - final List installments = new ArrayList<>(); LocalDate currentDate = DateUtils.getLocalDateOfTenant(); LocalDate lastRestDate = currentDate; if (loanApplicationTerms.getRestCalendarInstance() != null) { lastRestDate = getNextRestScheduleDate(currentDate.minusDays(1), loanApplicationTerms, holidayDetailDTO); } - // compounding(interest/Fee) amounts will be added along with - // date(after applying compounding frequency) - // from when these amounts will effect the outstanding balance for - // interest calculation - final TreeMap compoundingMap = new TreeMap<>(); - final Map> compoundingDateVariations = new HashMap<>(); + boolean isNextRepaymentAvailable = true; Boolean extendTermForDailyRepayments = false; @@ -189,117 +150,77 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener extendTermForDailyRepayments = true; } - // for applying variations - Collection loanTermVariations = new ArrayList<>(loanApplicationTerms.getLoanTermVariations()); + final Collection interestRates = loanApplicationTerms.getLoanTermVariations().getInterestRateChanges(); // this block is to start the schedule generation from specified date - if (loanScheduleRecalculationDTO != null && loanScheduleRecalculationDTO.isPartialUpdate()) { - periodNumber = loanScheduleRecalculationDTO.getPeriodNumber(); - instalmentNumber = loanScheduleRecalculationDTO.getInstalmentNumber(); - periodStartDate = loanScheduleRecalculationDTO.getPeriodStartDate(); - periodStartDateApplicableForInterest = periodStartDate; - actualRepaymentDate = loanScheduleRecalculationDTO.getActualRepaymentDate(); - totalCumulativePrincipal = loanScheduleRecalculationDTO.getTotalCumulativePrincipal(); - totalCumulativeInterest = loanScheduleRecalculationDTO.getTotalCumulativeInterest(); - totalFeeChargesCharged = loanScheduleRecalculationDTO.getTotalFeeChargesCharged().getAmount(); - totalPenaltyChargesCharged = loanScheduleRecalculationDTO.getTotalPenaltyChargesCharged().getAmount(); - totalRepaymentExpected = loanScheduleRecalculationDTO.getTotalRepaymentExpected().getAmount(); - reducePrincipal = loanScheduleRecalculationDTO.getReducePrincipal(); - principalPortionMap.clear(); - principalPortionMap.putAll(loanScheduleRecalculationDTO.getPrincipalPortionMap()); - latePaymentMap.clear(); - latePaymentMap.putAll(loanScheduleRecalculationDTO.getLatePaymentMap()); - compoundingMap.clear(); - compoundingMap.putAll(loanScheduleRecalculationDTO.getCompoundingMap()); - disburseDetailMap.clear(); - disburseDetailMap.putAll(loanScheduleRecalculationDTO.getDisburseDetailMap()); - outstandingBalance = loanScheduleRecalculationDTO.getOutstandingBalance(); - outstandingBalanceAsPerRest = loanScheduleRecalculationDTO.getOutstandingBalanceAsPerRest(); - installments.clear(); - installments.addAll(loanScheduleRecalculationDTO.getInstallments()); + if (scheduleParams.isPartialUpdate()) { if (loanApplicationTerms.isMultiDisburseLoan()) { - principalToBeScheduled = loanScheduleRecalculationDTO.getPrincipalToBeScheduled(); - loanApplicationTerms.setPrincipal(principalToBeScheduled); - } - for (LoanTermVariationsData variation : loanApplicationTerms.getLoanTermVariations()) { - if (variation.getTermVariationType().isInterestRateVariation() && variation.isApplicable(periodStartDate, periodNumber) - && variation.getTermValue() != null) { - loanApplicationTerms.updateAnnualNominalInterestRate(variation.getTermValue()); - } + loanApplicationTerms.setPrincipal(scheduleParams.getPrincipalToBeScheduled()); } + + applyLoanVariationsForPartialScheduleGenerate(loanApplicationTerms, scheduleParams, interestRates); + isFirstRepayment = false; } - - while (!outstandingBalance.isZero() || !disburseDetailMap.isEmpty()) { - - actualRepaymentDate = this.scheduledDateGenerator.generateNextRepaymentDate(actualRepaymentDate, loanApplicationTerms, - isFirstRepayment, holidayDetailDTO); + while (!scheduleParams.getOutstandingBalance().isZero() || !scheduleParams.getDisburseDetailMap().isEmpty()) { + LocalDate previousRepaymentDate = scheduleParams.getActualRepaymentDate(); + scheduleParams.setActualRepaymentDate(this.scheduledDateGenerator.generateNextRepaymentDate( + scheduleParams.getActualRepaymentDate(), loanApplicationTerms, isFirstRepayment, holidayDetailDTO)); isFirstRepayment = false; - LocalDate scheduledDueDate = this.scheduledDateGenerator.adjustRepaymentDate(actualRepaymentDate, loanApplicationTerms, - holidayDetailDTO); + LocalDate scheduledDueDate = this.scheduledDateGenerator.adjustRepaymentDate(scheduleParams.getActualRepaymentDate(), + loanApplicationTerms, holidayDetailDTO); - if (!latePaymentMap.isEmpty()) { - populateCompoundingDatesInPeriod(periodStartDate, scheduledDueDate, currentDate, loanApplicationTerms, holidayDetailDTO, - compoundingMap, loanCharges, currency); - compoundingDateVariations.put(periodStartDate, new TreeMap<>(compoundingMap)); + // calculated interest start date for the period + LocalDate periodStartDateApplicableForInterest = calculateInterestStartDateForPeriod(loanApplicationTerms, + scheduleParams.getPeriodStartDate(), idealDisbursementDate); + + // Loan Schedule Exceptions that need to be applied for Loan Account + LoanTermVariationParams termVariationParams = applyLoanTermVariations(loanApplicationTerms, scheduleParams, + previousRepaymentDate, scheduledDueDate); + + scheduledDueDate = termVariationParams.getScheduledDueDate(); + // Updates total days in term + scheduleParams.addLoanTermInDays(Days.daysBetween(scheduleParams.getPeriodStartDate(), scheduledDueDate).getDays()); + if (termVariationParams.isSkipPeriod()) { + continue; + } + + if (scheduleParams.getPeriodStartDate().isAfter(scheduledDueDate)) { throw new ScheduleDateException( + "Due date can't be before period start date", scheduledDueDate); } + + if (!scheduleParams.getLatePaymentMap().isEmpty()) { + populateCompoundingDatesInPeriod(scheduleParams.getPeriodStartDate(), scheduledDueDate, currentDate, loanApplicationTerms, + holidayDetailDTO, scheduleParams.getCompoundingMap(), loanCharges, currency); + scheduleParams.getCompoundingDateVariations().put(scheduleParams.getPeriodStartDate(), + new TreeMap<>(scheduleParams.getCompoundingMap())); } if (extendTermForDailyRepayments) { - actualRepaymentDate = scheduledDueDate; + scheduleParams.setActualRepaymentDate(scheduledDueDate); } - // calculated interest start date for the period - periodStartDateApplicableForInterest = calculateInterestStartDateForPeriod(loanApplicationTerms, periodStartDate, - idealDisbursementDate, periodStartDateApplicableForInterest); - int daysInPeriodApplicableForInterest = Days.daysBetween(periodStartDateApplicableForInterest, scheduledDueDate).getDays(); - - if (scheduleTillDate != null && !scheduledDueDate.isBefore(scheduleTillDate)) { - scheduledDueDate = scheduleTillDate; + // this block is to generate the schedule till the specified + // date(used for calculating preclosure) + if (scheduleParams.getScheduleTillDate() != null && !scheduledDueDate.isBefore(scheduleParams.getScheduleTillDate())) { + scheduledDueDate = scheduleParams.getScheduleTillDate(); isNextRepaymentAvailable = false; } // populates the collection with transactions till the due date of // the period for interest recalculation enabled loans - Collection applicableTransactions = getApplicableTransactionsForPeriod(loanApplicationTerms, - scheduledDueDate, transactions); + Collection applicableTransactions = getApplicableTransactionsForPeriod( + scheduleParams.applyInterestRecalculation(), scheduledDueDate, transactions); - Collection applicableVariations = getApplicableTermVariationsForPeriod(periodStartDate, - scheduledDueDate, periodNumber, loanTermVariations); - - double interestCalculationGraceOnRepaymentPeriodFraction = this.paymentPeriodsInOneYearCalculator + final double interestCalculationGraceOnRepaymentPeriodFraction = this.paymentPeriodsInOneYearCalculator .calculatePortionOfRepaymentPeriodInterestChargingGrace(periodStartDateApplicableForInterest, scheduledDueDate, loanApplicationTerms.getInterestChargedFromLocalDate(), loanApplicationTerms.getLoanTermPeriodFrequencyType(), loanApplicationTerms.getRepaymentEvery()); + ScheduleCurrentPeriodParams currentPeriodParams = new ScheduleCurrentPeriodParams(currency, + interestCalculationGraceOnRepaymentPeriodFraction); + if (loanApplicationTerms.isMultiDisburseLoan()) { - // Updates fixed emi amount as the date if multiple amounts - // provided - loanApplicationTerms.setFixedEmiAmountForPeriod(scheduledDueDate); - - for (Map.Entry disburseDetail : disburseDetailMap.entrySet()) { - if (disburseDetail.getKey().isAfter(periodStartDate) && !disburseDetail.getKey().isAfter(scheduledDueDate)) { - // validation check for amount not exceeds specified max - // amount as per the configuration - if (loanApplicationTerms.getMaxOutstandingBalance() != null - && outstandingBalance.plus(disburseDetail.getValue()).isGreaterThan( - loanApplicationTerms.getMaxOutstandingBalance())) { - String errorMsg = "Outstanding balance must not exceed the amount: " - + loanApplicationTerms.getMaxOutstandingBalance(); - throw new MultiDisbursementOutstandingAmoutException(errorMsg, loanApplicationTerms.getMaxOutstandingBalance() - .getAmount(), disburseDetail.getValue()); - } - - // creates and add disbursement detail to the repayments - // period - final LoanScheduleModelDisbursementPeriod disbursementPeriod = LoanScheduleModelDisbursementPeriod.disbursement( - disburseDetail.getKey(), disburseDetail.getValue(), chargesDueAtTimeOfDisbursement); - periods.add(disbursementPeriod); - // updates actual outstanding balance with new - // disbursement detail - outstandingBalance = outstandingBalance.plus(disburseDetail.getValue()); - principalToBeScheduled = principalToBeScheduled.plus(disburseDetail.getValue()); - loanApplicationTerms.setPrincipal(loanApplicationTerms.getPrincipal().plus(disburseDetail.getValue())); - } - } + updateBalanceBasedOnDisbursement(loanApplicationTerms, chargesDueAtTimeOfDisbursement, scheduleParams, periods, + scheduledDueDate); } // process repayments to the schedule as per the repayment @@ -307,503 +228,769 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener // will add a new schedule with interest till the transaction date // for a loan repayment which falls between the // two periods for interest first repayment strategies - Money earlyPaidAmount = Money.zero(currency); - LoanScheduleModelPeriod lastInstallment = null; - if (isInterestRecalculationRequired(loanApplicationTerms, transactions)) { - boolean checkForOutstanding = true; - List unprocessedTransactions = new ArrayList<>(); - LoanScheduleModelPeriod installment = null; - for (RecalculationDetail detail : applicableTransactions) { - if (detail.isProcessed()) { - continue; - } - boolean updateLatePaymentMap = false; - if (detail.getTransactionDate().isBefore(scheduledDueDate)) { - if (loanRepaymentScheduleTransactionProcessor != null - && loanRepaymentScheduleTransactionProcessor.isInterestFirstRepaymentScheduleTransactionProcessor()) { - List currentTransactions = createCurrentTransactionList(detail); - if (!detail.getTransactionDate().isEqual(periodStartDate)) { - int periodDays = Days.daysBetween(periodStartDate, detail.getTransactionDate()).getDays(); - // calculates period start date for interest - // calculation as per the configuration - periodStartDateApplicableForInterest = calculateInterestStartDateForPeriod(loanApplicationTerms, - periodStartDate, idealDisbursementDate, periodStartDateApplicableForInterest); + handleRecalculationForNonDueDateTransactions(mc, loanApplicationTerms, loanCharges, holidayDetailDTO, scheduleParams, periods, + totalInterestChargedForFullLoanTerm, idealDisbursementDate, lastRestDate, scheduledDueDate, + periodStartDateApplicableForInterest, applicableTransactions, currentPeriodParams); - int daysInPeriodApplicable = Days.daysBetween(periodStartDateApplicableForInterest, - detail.getTransactionDate()).getDays(); - Money interestForThisinstallment = Money.zero(currency); - if (daysInPeriodApplicable > 0) { - // 5 determine interest till the transaction - // date - if (!compoundingDateVariations.containsKey(periodStartDateApplicableForInterest)) { - compoundingDateVariations.put(periodStartDateApplicableForInterest, new TreeMap<>(compoundingMap)); - } - PrincipalInterest principalInterestForThisPeriod = calculatePrincipalInterestComponentsForPeriod( - this.paymentPeriodsInOneYearCalculator, interestCalculationGraceOnRepaymentPeriodFraction, - totalCumulativePrincipal.minus(reducePrincipal), totalCumulativeInterest, - totalInterestChargedForFullLoanTerm, totalOutstandingInterestPaymentDueToGrace, - outstandingBalanceAsPerRest, loanApplicationTerms, periodNumber, mc, - mergeVariationsToMap(principalPortionMap, latePaymentMap, disburseDetailMap, compoundingMap), - compoundingMap, periodStartDateApplicableForInterest, detail.getTransactionDate(), - daysInPeriodApplicableForInterest, applicableVariations); - interestForThisinstallment = principalInterestForThisPeriod.interest(); - - totalOutstandingInterestPaymentDueToGrace = principalInterestForThisPeriod.interestPaymentDueToGrace(); - } - - Money principalForThisPeriod = principalToBeScheduled.zero(); - - // applies all the applicable charges to the - // newly - // created installment - PrincipalInterest principalInterest = new PrincipalInterest(principalForThisPeriod, - interestForThisinstallment, null); - Money feeChargesForInstallment = cumulativeFeeChargesDueWithin(periodStartDate, - detail.getTransactionDate(), loanCharges, currency, principalInterest, principalToBeScheduled, - totalCumulativeInterest, numberOfRepayments, true); - Money penaltyChargesForInstallment = cumulativePenaltyChargesDueWithin(periodStartDate, - detail.getTransactionDate(), loanCharges, currency, principalInterest, principalToBeScheduled, - totalCumulativeInterest, numberOfRepayments, true); - - // sum up real totalInstallmentDue from - // components - final Money totalInstallmentDue = principalForThisPeriod.plus(interestForThisinstallment) - .plus(feeChargesForInstallment).plus(penaltyChargesForInstallment); - // create repayment period from parts - installment = LoanScheduleModelRepaymentPeriod.repayment(instalmentNumber, periodStartDate, - detail.getTransactionDate(), principalForThisPeriod, outstandingBalance, - interestForThisinstallment, feeChargesForInstallment, penaltyChargesForInstallment, - totalInstallmentDue, true); - periods.add(installment); - - // update outstanding balance for interest - // calculation as per the rest - outstandingBalanceAsPerRest = updateBalanceForInterestCalculation(principalPortionMap, - detail.getTransactionDate(), outstandingBalanceAsPerRest, false); - outstandingBalanceAsPerRest = updateBalanceForInterestCalculation(disburseDetailMap, - detail.getTransactionDate(), outstandingBalanceAsPerRest, true); - - // handle cumulative fields - loanTermInDays += periodDays; - totalRepaymentExpected = totalRepaymentExpected.add(totalInstallmentDue.getAmount()); - totalCumulativeInterest = totalCumulativeInterest.plus(interestForThisinstallment); - totalFeeChargesCharged = totalFeeChargesCharged.add(feeChargesForInstallment.getAmount()); - totalPenaltyChargesCharged = totalPenaltyChargesCharged.add(penaltyChargesForInstallment.getAmount()); - - periodStartDate = detail.getTransactionDate(); - periodStartDateApplicableForInterest = periodStartDate; - updateLatePaymentMap = true; - instalmentNumber++; - // creates and insert Loan repayment schedule - // for - // the period - addLoanRepaymentScheduleInstallment(installments, installment); - } else if (installment == null) { - installment = ((List) periods).get(periods.size() - 1); - } - // applies the transaction as per transaction - // strategy - // on scheduled installments to identify the - // unprocessed(early payment ) amounts - Money unprocessed = loanRepaymentScheduleTransactionProcessor.handleRepaymentSchedule(currentTransactions, - currency, installments); - if (unprocessed.isGreaterThanZero()) { - - if (loanApplicationTerms.getPreClosureInterestCalculationStrategy().calculateTillRestFrequencyEnabled()) { - LocalDate applicableDate = getNextRestScheduleDate(detail.getTransactionDate().minusDays(1), - loanApplicationTerms, holidayDetailDTO); - checkForOutstanding = detail.getTransactionDate().isEqual(applicableDate); - - } - // reduces actual outstanding balance - outstandingBalance = outstandingBalance.minus(unprocessed); - // if outstanding balance becomes less than zero - // then adjusts the princiapal - Money addToPrinciapal = Money.zero(currency); - if (!outstandingBalance.isGreaterThanZero()) { - addToPrinciapal = addToPrinciapal.plus(outstandingBalance); - outstandingBalance = outstandingBalance.zero(); - lastInstallment = installment; - } - // updates principal portion map with the early - // payment amounts and applicable date as per - // rest - updatePrincipalPaidPortionToMap(loanApplicationTerms, holidayDetailDTO, principalPortionMap, installment, - detail, unprocessed.plus(addToPrinciapal), installments); - totalRepaymentExpected = totalRepaymentExpected.add(unprocessed.plus(addToPrinciapal).getAmount()); - totalCumulativePrincipal = totalCumulativePrincipal.plus(unprocessed.plus(addToPrinciapal)); - - // method applies early payment strategy - reducePrincipal = reducePrincipal.plus(unprocessed); - reducePrincipal = applyEarlyPaymentStrategy(loanApplicationTerms, reducePrincipal, - totalCumulativePrincipal, periodNumber, mc, holidayDetailDTO); - } - // identify late payments and add compounding - // details to - // map for interest calculation - updateLatePaidAmountsToPrincipalMap(principalPortionMap, detail.getTransaction(), latePaymentMap, - compoundingMap, loanApplicationTerms, currency, holidayDetailDTO, lastRestDate); - compoundingDateVariations.put(periodStartDateApplicableForInterest, new TreeMap<>(compoundingMap)); - if (updateLatePaymentMap) { - updateLatePaymentsToMap(loanApplicationTerms, holidayDetailDTO, currency, latePaymentMap, scheduledDueDate, - installments, true, lastRestDate, compoundingMap); - } - } else if (loanRepaymentScheduleTransactionProcessor != null) { - LocalDate applicableDate = getNextRestScheduleDate(detail.getTransactionDate().minusDays(1), - loanApplicationTerms, holidayDetailDTO); - if (applicableDate.isBefore(scheduledDueDate)) { - List currentTransactions = createCurrentTransactionList(detail); - Money unprocessed = loanRepaymentScheduleTransactionProcessor.handleRepaymentSchedule(currentTransactions, - currency, installments); - Money arrears = fetchCompoundedArrears(loanApplicationTerms, currency, detail.getTransaction()); - if (unprocessed.isGreaterThanZero()) { - arrears = getTotalAmount(latePaymentMap, currency); - updateMapWithAmount(principalPortionMap, unprocessed, applicableDate); - earlyPaidAmount = earlyPaidAmount.plus(unprocessed); - - // this check is to identify pre-closure and - // apply interest calculation as per - // configuration - if (!outstandingBalance.isGreaterThan(unprocessed) - && !loanApplicationTerms.getPreClosureInterestCalculationStrategy() - .calculateTillRestFrequencyEnabled()) { - - LocalDate calculateTill = detail.getTransactionDate(); - if (!compoundingDateVariations.containsKey(periodStartDateApplicableForInterest)) { - compoundingDateVariations.put(periodStartDateApplicableForInterest, new TreeMap<>( - compoundingMap)); - } - PrincipalInterest principalInterestForThisPeriod = calculatePrincipalInterestComponentsForPeriod( - this.paymentPeriodsInOneYearCalculator, - interestCalculationGraceOnRepaymentPeriodFraction, - totalCumulativePrincipal.minus(reducePrincipal), - totalCumulativeInterest, - totalInterestChargedForFullLoanTerm, - totalOutstandingInterestPaymentDueToGrace, - outstandingBalanceAsPerRest, - loanApplicationTerms, - periodNumber, - mc, - mergeVariationsToMap(principalPortionMap, latePaymentMap, disburseDetailMap, compoundingMap), - compoundingMap, periodStartDateApplicableForInterest, calculateTill, - daysInPeriodApplicableForInterest, applicableVariations); - if (!principalInterestForThisPeriod.interest() - .plus(principalInterestForThisPeriod.interestPaymentDueToGrace()).plus(outstandingBalance) - .isGreaterThan(unprocessed)) { - earlyPaidAmount = earlyPaidAmount.minus(unprocessed); - updateMapWithAmount(principalPortionMap, unprocessed.negated(), applicableDate); - LoanTransaction loanTransaction = LoanTransaction.repayment(null, unprocessed, null, - detail.getTransactionDate(), null, DateUtils.getLocalDateTimeOfTenant(), null); - RecalculationDetail recalculationDetail = new RecalculationDetail(detail.getTransactionDate(), - loanTransaction); - unprocessedTransactions.add(recalculationDetail); - break; - } - } - LoanTransaction loanTransaction = LoanTransaction.repayment(null, unprocessed, null, scheduledDueDate, - null, DateUtils.getLocalDateTimeOfTenant(), null); - RecalculationDetail recalculationDetail = new RecalculationDetail(scheduledDueDate, loanTransaction); - unprocessedTransactions.add(recalculationDetail); - checkForOutstanding = false; - - outstandingBalance = outstandingBalance.minus(unprocessed); - // if outstanding balance becomes less than - // zero - // then adjusts the princiapal - Money addToPrinciapal = Money.zero(currency); - if (outstandingBalance.isLessThanZero()) { - addToPrinciapal = addToPrinciapal.plus(outstandingBalance); - outstandingBalance = outstandingBalance.zero(); - updateMapWithAmount(principalPortionMap, addToPrinciapal, applicableDate); - earlyPaidAmount = earlyPaidAmount.plus(addToPrinciapal); - } - - } - if (arrears.isGreaterThanZero() && applicableDate.isBefore(lastRestDate)) { - updateLatePaidAmountsToPrincipalMap(principalPortionMap, detail.getTransaction(), latePaymentMap, - compoundingMap, loanApplicationTerms, currency, holidayDetailDTO, lastRestDate); - compoundingDateVariations.put(periodStartDateApplicableForInterest, new TreeMap<>(compoundingMap)); - } - } - - } - } - - } - applicableTransactions.addAll(unprocessedTransactions); - if (checkForOutstanding && outstandingBalance.isZero() && disburseDetailMap.isEmpty()) { - continue; - } + if (currentPeriodParams.isSkipCurrentLoop()) { + continue; } - - int periodDays = Days.daysBetween(periodStartDate, scheduledDueDate).getDays(); - periodStartDateApplicableForInterest = calculateInterestStartDateForPeriod(loanApplicationTerms, periodStartDate, - idealDisbursementDate, periodStartDateApplicableForInterest); + periodStartDateApplicableForInterest = calculateInterestStartDateForPeriod(loanApplicationTerms, + scheduleParams.getPeriodStartDate(), idealDisbursementDate); // backup for pre-close transaction - if (compoundingDateVariations.containsKey(periodStartDateApplicableForInterest)) { - compoundingMap.clear(); - compoundingMap.putAll(compoundingDateVariations.get(periodStartDateApplicableForInterest)); - } else { - compoundingDateVariations.put(periodStartDateApplicableForInterest, new TreeMap<>(compoundingMap)); - } + updateCompoundingDetails(scheduleParams, periodStartDateApplicableForInterest); + // 5 determine principal,interest of repayment period PrincipalInterest principalInterestForThisPeriod = calculatePrincipalInterestComponentsForPeriod( - this.paymentPeriodsInOneYearCalculator, interestCalculationGraceOnRepaymentPeriodFraction, - totalCumulativePrincipal.minus(reducePrincipal), totalCumulativeInterest, totalInterestChargedForFullLoanTerm, - totalOutstandingInterestPaymentDueToGrace, outstandingBalanceAsPerRest, loanApplicationTerms, periodNumber, mc, - mergeVariationsToMap(principalPortionMap, latePaymentMap, disburseDetailMap, compoundingMap), compoundingMap, - periodStartDateApplicableForInterest, scheduledDueDate, daysInPeriodApplicableForInterest, applicableVariations); + this.paymentPeriodsInOneYearCalculator, currentPeriodParams.getInterestCalculationGraceOnRepaymentPeriodFraction(), + scheduleParams.getTotalCumulativePrincipal().minus(scheduleParams.getReducePrincipal()), + scheduleParams.getTotalCumulativeInterest(), totalInterestChargedForFullLoanTerm, + scheduleParams.getTotalOutstandingInterestPaymentDueToGrace(), scheduleParams.getOutstandingBalanceAsPerRest(), + loanApplicationTerms, scheduleParams.getPeriodNumber(), mc, mergeVariationsToMap(scheduleParams), + scheduleParams.getCompoundingMap(), periodStartDateApplicableForInterest, scheduledDueDate, interestRates); + // will check for EMI amount greater than interest calculated if (loanApplicationTerms.getFixedEmiAmount() != null - && loanApplicationTerms.getFixedEmiAmount().compareTo(principalInterestForThisPeriod.interest().getAmount()) != 1) { + && loanApplicationTerms.getFixedEmiAmount().compareTo(principalInterestForThisPeriod.interest().getAmount()) == -1) { String errorMsg = "EMI amount must be greater than : " + principalInterestForThisPeriod.interest().getAmount(); throw new MultiDisbursementEmiAmountException(errorMsg, principalInterestForThisPeriod.interest().getAmount(), loanApplicationTerms.getFixedEmiAmount()); } // update cumulative fields for principal & interest - Money interestForThisinstallment = principalInterestForThisPeriod.interest(); - Money lastTotalOutstandingInterestPaymentDueToGrace = totalOutstandingInterestPaymentDueToGrace; - totalOutstandingInterestPaymentDueToGrace = principalInterestForThisPeriod.interestPaymentDueToGrace(); - Money principalForThisPeriod = principalInterestForThisPeriod.principal(); + currentPeriodParams.setInterestForThisPeriod(principalInterestForThisPeriod.interest()); + Money lastTotalOutstandingInterestPaymentDueToGrace = scheduleParams.getTotalOutstandingInterestPaymentDueToGrace(); + scheduleParams.setTotalOutstandingInterestPaymentDueToGrace(principalInterestForThisPeriod.interestPaymentDueToGrace()); + currentPeriodParams.setPrincipalForThisPeriod(principalInterestForThisPeriod.principal()); // applies early payments on principal portion - if (principalForThisPeriod.isGreaterThan(reducePrincipal)) { - principalForThisPeriod = principalForThisPeriod.minus(reducePrincipal); - reducePrincipal = reducePrincipal.zero(); - } else { - reducePrincipal = reducePrincipal.minus(principalForThisPeriod); - principalForThisPeriod = principalForThisPeriod.zero(); - } + updatePrincipalPortionBasedOnPreviousEarlyPayments(currency, scheduleParams, currentPeriodParams); - // earlyPaidAmount is already subtracted from balancereducePrincipal - // reducePrincipal.plus(unprocessed); - Money reducedBalance = earlyPaidAmount; - earlyPaidAmount = earlyPaidAmount.minus(principalForThisPeriod); - boolean isEmiAmountChanged = false; - if (earlyPaidAmount.isGreaterThanZero()) { - reducePrincipal = reducePrincipal.plus(earlyPaidAmount); - BigDecimal fixedEmiAmount = loanApplicationTerms.getFixedEmiAmount(); - reducePrincipal = applyEarlyPaymentStrategy(loanApplicationTerms, reducePrincipal, - totalCumulativePrincipal.plus(principalForThisPeriod).plus(earlyPaidAmount), periodNumber + 1, mc, holidayDetailDTO); - if (loanApplicationTerms.getAmortizationMethod().isEqualInstallment() - && fixedEmiAmount.compareTo(loanApplicationTerms.getFixedEmiAmount()) != 0) { - isEmiAmountChanged = true; - } - principalForThisPeriod = principalForThisPeriod.plus(earlyPaidAmount); - } + // updates amounts with current earlyPaidAmount + updateAmountsBasedOnCurrentEarlyPayments(mc, loanApplicationTerms, scheduleParams, currentPeriodParams); - // update outstandingLoanBlance using current period - // 'principalDue' - outstandingBalance = outstandingBalance.minus(principalForThisPeriod.minus(reducedBalance)); - - if (outstandingBalance.isLessThanZero() || !isNextRepaymentAvailable) { - principalForThisPeriod = principalForThisPeriod.plus(outstandingBalance); - outstandingBalance = outstandingBalance.zero(); + if (scheduleParams.getOutstandingBalance().isLessThanZero() || !isNextRepaymentAvailable) { + currentPeriodParams.plusPrincipalForThisPeriod(scheduleParams.getOutstandingBalance()); + scheduleParams.setOutstandingBalance(Money.zero(currency)); } if (!isNextRepaymentAvailable) { - disburseDetailMap.clear(); + scheduleParams.getDisburseDetailMap().clear(); } // applies charges for the period - PrincipalInterest principalInterest = new PrincipalInterest(principalForThisPeriod, interestForThisinstallment, null); - Money feeChargesForInstallment = cumulativeFeeChargesDueWithin(periodStartDate, scheduledDueDate, loanCharges, currency, - principalInterest, principalToBeScheduled, totalCumulativeInterest, numberOfRepayments, true); - Money penaltyChargesForInstallment = cumulativePenaltyChargesDueWithin(periodStartDate, scheduledDueDate, loanCharges, - currency, principalInterest, principalToBeScheduled, totalCumulativeInterest, numberOfRepayments, true); - totalFeeChargesCharged = totalFeeChargesCharged.add(feeChargesForInstallment.getAmount()); - totalPenaltyChargesCharged = totalPenaltyChargesCharged.add(penaltyChargesForInstallment.getAmount()); + applyChargesForCurrentPeriod(loanCharges, currency, scheduleParams, scheduledDueDate, currentPeriodParams); // sum up real totalInstallmentDue from components - final Money totalInstallmentDue = principalForThisPeriod.plus(interestForThisinstallment).plus(feeChargesForInstallment) - .plus(penaltyChargesForInstallment); + final Money totalInstallmentDue = currentPeriodParams.fetchTotalAmountForPeriod(); // if previous installment is last then add interest to same // installment - if (lastInstallment != null && principalForThisPeriod.isZero()) { - lastInstallment.addInterestAmount(interestForThisinstallment); + if (currentPeriodParams.getLastInstallment() != null && currentPeriodParams.getPrincipalForThisPeriod().isZero()) { + currentPeriodParams.getLastInstallment().addInterestAmount(currentPeriodParams.getInterestForThisPeriod()); continue; } // create repayment period from parts - LoanScheduleModelPeriod installment = LoanScheduleModelRepaymentPeriod.repayment(instalmentNumber, periodStartDate, - scheduledDueDate, principalForThisPeriod, outstandingBalance, interestForThisinstallment, feeChargesForInstallment, - penaltyChargesForInstallment, totalInstallmentDue, false); + LoanScheduleModelPeriod installment = LoanScheduleModelRepaymentPeriod.repayment(scheduleParams.getInstalmentNumber(), + scheduleParams.getPeriodStartDate(), scheduledDueDate, currentPeriodParams.getPrincipalForThisPeriod(), + scheduleParams.getOutstandingBalance(), currentPeriodParams.getInterestForThisPeriod(), + currentPeriodParams.getFeeChargesForInstallment(), currentPeriodParams.getPenaltyChargesForInstallment(), + totalInstallmentDue, false); // apply loan transactions on installments to identify early/late // payments for interest recalculation - if (isInterestRecalculationRequired(loanApplicationTerms, transactions) && loanRepaymentScheduleTransactionProcessor != null) { - Money principalProcessed = Money.zero(currency); - addLoanRepaymentScheduleInstallment(installments, installment); - for (RecalculationDetail detail : applicableTransactions) { - if (!detail.isProcessed()) { - List currentTransactions = new ArrayList<>(2); - currentTransactions.add(detail.getTransaction()); - // applies the transaction as per transaction strategy - // on scheduled installments to identify the - // unprocessed(early payment ) amounts - Money unprocessed = loanRepaymentScheduleTransactionProcessor.handleRepaymentSchedule(currentTransactions, - currency, installments); - - if (unprocessed.isGreaterThanZero()) { - outstandingBalance = outstandingBalance.minus(unprocessed); - // pre closure check and processing - if (outstandingBalance.isLessThan(interestForThisinstallment) - && !scheduledDueDate.equals(detail.getTransactionDate())) { - LocalDate calculateTill = detail.getTransactionDate(); - if (loanApplicationTerms.getPreClosureInterestCalculationStrategy().calculateTillRestFrequencyEnabled()) { - calculateTill = getNextRestScheduleDate(calculateTill.minusDays(1), loanApplicationTerms, - holidayDetailDTO); - } - if (compoundingDateVariations.containsKey(periodStartDateApplicableForInterest)) { - compoundingMap.clear(); - compoundingMap.putAll(compoundingDateVariations.get(periodStartDateApplicableForInterest)); - } - if (isEmiAmountChanged) { - updateFixedInstallmentAmount(mc, loanApplicationTerms, - loanApplicationTerms.getExpectedDisbursementDate(), periodNumber, loanApplicationTerms - .getPrincipal().minus(totalCumulativePrincipal), holidayDetailDTO); - } - PrincipalInterest interestTillDate = calculatePrincipalInterestComponentsForPeriod( - this.paymentPeriodsInOneYearCalculator, interestCalculationGraceOnRepaymentPeriodFraction, - totalCumulativePrincipal, totalCumulativeInterest, totalInterestChargedForFullLoanTerm, - lastTotalOutstandingInterestPaymentDueToGrace, outstandingBalanceAsPerRest, loanApplicationTerms, - periodNumber, mc, - mergeVariationsToMap(principalPortionMap, latePaymentMap, disburseDetailMap, compoundingMap), - compoundingMap, periodStartDateApplicableForInterest, calculateTill, - daysInPeriodApplicableForInterest, applicableVariations); - Money diff = interestForThisinstallment.minus(interestTillDate.interest()); - if (!outstandingBalance.minus(diff).isGreaterThanZero()) { - outstandingBalance = outstandingBalance.minus(diff); - interestForThisinstallment = interestForThisinstallment.minus(diff); - principalForThisPeriod = principalForThisPeriod.plus(diff); - final Money totalDue = principalForThisPeriod// - .plus(interestForThisinstallment); - - // create and replaces repayment period - // from parts - installment = LoanScheduleModelRepaymentPeriod.repayment(instalmentNumber, periodStartDate, - detail.getTransactionDate(), principalForThisPeriod, outstandingBalance, - interestForThisinstallment, feeChargesForInstallment, penaltyChargesForInstallment, totalDue, - false); - totalOutstandingInterestPaymentDueToGrace = interestTillDate.interestPaymentDueToGrace(); - } - - } - Money addToPrinciapal = Money.zero(currency); - if (outstandingBalance.isLessThanZero()) { - addToPrinciapal = addToPrinciapal.plus(outstandingBalance); - outstandingBalance = outstandingBalance.zero(); - } - // updates principal portion map with the early - // payment amounts and applicable date as per rest - updatePrincipalPaidPortionToMap(loanApplicationTerms, holidayDetailDTO, principalPortionMap, installment, - detail, unprocessed.plus(addToPrinciapal), installments); - totalRepaymentExpected = totalRepaymentExpected.add(unprocessed.plus(addToPrinciapal).getAmount()); - totalCumulativePrincipal = totalCumulativePrincipal.plus(unprocessed.plus(addToPrinciapal)); - - reducePrincipal = reducePrincipal.plus(unprocessed); - principalForThisPeriod = principalForThisPeriod.plus(unprocessed.plus(addToPrinciapal)); - principalProcessed = principalProcessed.plus(unprocessed.plus(addToPrinciapal)); - BigDecimal fixedEmiAmount = loanApplicationTerms.getFixedEmiAmount(); - reducePrincipal = applyEarlyPaymentStrategy(loanApplicationTerms, reducePrincipal, - totalCumulativePrincipal.plus(principalForThisPeriod.minus(principalProcessed)), periodNumber + 1, mc, - holidayDetailDTO); - if (loanApplicationTerms.getAmortizationMethod().isEqualInstallment() - && fixedEmiAmount.compareTo(loanApplicationTerms.getFixedEmiAmount()) != 0) { - isEmiAmountChanged = true; - } - - } - } - } - updateLatePaymentsToMap(loanApplicationTerms, holidayDetailDTO, currency, latePaymentMap, scheduledDueDate, installments, - true, lastRestDate, compoundingMap); - principalForThisPeriod = principalForThisPeriod.minus(principalProcessed); - } - + installment = handleRecalculationForTransactions(mc, loanApplicationTerms, holidayDetailDTO, currency, scheduleParams, + loanRepaymentScheduleTransactionProcessor, totalInterestChargedForFullLoanTerm, lastRestDate, scheduledDueDate, + periodStartDateApplicableForInterest, applicableTransactions, currentPeriodParams, + lastTotalOutstandingInterestPaymentDueToGrace, installment); periods.add(installment); // Updates principal paid map with efective date for reducing // the amount from outstanding balance(interest calculation) - LocalDate amountApplicableDate = installment.periodDueDate(); - if (loanApplicationTerms.isInterestRecalculationEnabled()) { - amountApplicableDate = getNextRestScheduleDate(installment.periodDueDate().minusDays(1), loanApplicationTerms, - holidayDetailDTO); - } - updateMapWithAmount(principalPortionMap, principalForThisPeriod.minus(reducedBalance), amountApplicableDate); - - // update outstanding balance for interest calculation - outstandingBalanceAsPerRest = updateBalanceForInterestCalculation(principalPortionMap, scheduledDueDate, - outstandingBalanceAsPerRest, false); - outstandingBalanceAsPerRest = updateBalanceForInterestCalculation(disburseDetailMap, scheduledDueDate, - outstandingBalanceAsPerRest, true); + updateAmountsWithEffectiveDate(loanApplicationTerms, holidayDetailDTO, scheduleParams, scheduledDueDate, currentPeriodParams, + installment); // handle cumulative fields - loanTermInDays += periodDays; - totalCumulativePrincipal = totalCumulativePrincipal.plus(principalForThisPeriod); - totalCumulativeInterest = totalCumulativeInterest.plus(interestForThisinstallment); - totalRepaymentExpected = totalRepaymentExpected.add(totalInstallmentDue.getAmount()); - periodStartDate = scheduledDueDate; - periodStartDateApplicableForInterest = periodStartDate; - instalmentNumber++; - periodNumber++; - compoundingDateVariations.clear(); + + scheduleParams.addTotalCumulativePrincipal(currentPeriodParams.getPrincipalForThisPeriod()); + scheduleParams.addTotalRepaymentExpected(totalInstallmentDue); + scheduleParams.addTotalCumulativeInterest(currentPeriodParams.getInterestForThisPeriod()); + scheduleParams.setPeriodStartDate(scheduledDueDate); + scheduleParams.incrementInstalmentNumber(); + scheduleParams.incrementPeriodNumber(); + scheduleParams.getCompoundingDateVariations().clear(); + if (termVariationParams.isRecalculateAmounts()) { + loanApplicationTerms.setCurrentPeriodFixedEmiAmount(null); + loanApplicationTerms.setCurrentPeriodFixedPrincipalAmount(null); + adjustInstallmentOrPrincipalAmount(loanApplicationTerms, scheduleParams.getTotalCumulativePrincipal(), + scheduleParams.getPeriodNumber(), mc); + } } // this condition is to add the interest from grace period if not // already applied. - if (totalOutstandingInterestPaymentDueToGrace.isGreaterThanZero()) { + if (scheduleParams.getTotalOutstandingInterestPaymentDueToGrace().isGreaterThanZero()) { LoanScheduleModelPeriod installment = ((List) periods).get(periods.size() - 1); - installment.addInterestAmount(totalOutstandingInterestPaymentDueToGrace); - totalRepaymentExpected = totalRepaymentExpected.add(totalOutstandingInterestPaymentDueToGrace.getAmount()); - totalCumulativeInterest = totalCumulativeInterest.plus(totalOutstandingInterestPaymentDueToGrace); - totalOutstandingInterestPaymentDueToGrace = totalOutstandingInterestPaymentDueToGrace.zero(); + installment.addInterestAmount(scheduleParams.getTotalOutstandingInterestPaymentDueToGrace()); + scheduleParams.addTotalRepaymentExpected(scheduleParams.getTotalOutstandingInterestPaymentDueToGrace()); + scheduleParams.addTotalCumulativeInterest(scheduleParams.getTotalOutstandingInterestPaymentDueToGrace()); + scheduleParams.setTotalOutstandingInterestPaymentDueToGrace(Money.zero(currency)); } // determine fees and penalties for charges which depends on total // loan interest + updatePeriodsWithCharges(currency, scheduleParams, periods, nonCompoundingCharges); + + // this block is to add extra re-payment schedules with interest portion + // if the loan not paid with in loan term + + if (scheduleParams.getScheduleTillDate() != null) { + currentDate = scheduleParams.getScheduleTillDate(); + } + if (scheduleParams.applyInterestRecalculation() && scheduleParams.getLatePaymentMap().size() > 0 + && currentDate.isAfter(scheduleParams.getPeriodStartDate())) { + Money totalInterest = addInterestOnlyRepaymentScheduleForCurrentdate(mc, loanApplicationTerms, holidayDetailDTO, currency, + periods, currentDate, loanRepaymentScheduleTransactionProcessor, transactions, loanCharges, scheduleParams); + scheduleParams.addTotalCumulativeInterest(totalInterest); + } + + loanApplicationTerms.resetFixedEmiAmount(); + final BigDecimal totalPrincipalPaid = BigDecimal.ZERO; + final BigDecimal totalOutstanding = BigDecimal.ZERO; + + return LoanScheduleModel.from(periods, applicationCurrency, scheduleParams.getLoanTermInDays(), + scheduleParams.getPrincipalToBeScheduled(), scheduleParams.getTotalCumulativePrincipal().getAmount(), totalPrincipalPaid, + scheduleParams.getTotalCumulativeInterest().getAmount(), scheduleParams.getTotalFeeChargesCharged().getAmount(), + scheduleParams.getTotalPenaltyChargesCharged().getAmount(), scheduleParams.getTotalRepaymentExpected().getAmount(), + totalOutstanding); + } + + private void applyChargesForCurrentPeriod(final Set loanCharges, final MonetaryCurrency currency, + LoanScheduleParams scheduleParams, LocalDate scheduledDueDate, ScheduleCurrentPeriodParams currentPeriodParams) { + PrincipalInterest principalInterest = new PrincipalInterest(currentPeriodParams.getPrincipalForThisPeriod(), + currentPeriodParams.getInterestForThisPeriod(), null); + currentPeriodParams.setFeeChargesForInstallment(cumulativeFeeChargesDueWithin(scheduleParams.getPeriodStartDate(), + scheduledDueDate, loanCharges, currency, principalInterest, scheduleParams.getPrincipalToBeScheduled(), + scheduleParams.getTotalCumulativeInterest(), true)); + currentPeriodParams.setPenaltyChargesForInstallment(cumulativePenaltyChargesDueWithin(scheduleParams.getPeriodStartDate(), + scheduledDueDate, loanCharges, currency, principalInterest, scheduleParams.getPrincipalToBeScheduled(), + scheduleParams.getTotalCumulativeInterest(), true)); + scheduleParams.addTotalFeeChargesCharged(currentPeriodParams.getFeeChargesForInstallment()); + scheduleParams.addTotalPenaltyChargesCharged(currentPeriodParams.getPenaltyChargesForInstallment()); + } + + private void updatePeriodsWithCharges(final MonetaryCurrency currency, LoanScheduleParams scheduleParams, + final Collection periods, final Set nonCompoundingCharges) { for (LoanScheduleModelPeriod loanScheduleModelPeriod : periods) { if (loanScheduleModelPeriod.isRepaymentPeriod()) { PrincipalInterest principalInterest = new PrincipalInterest(Money.of(currency, loanScheduleModelPeriod.principalDue()), Money.of(currency, loanScheduleModelPeriod.interestDue()), null); Money feeChargesForInstallment = cumulativeFeeChargesDueWithin(loanScheduleModelPeriod.periodFromDate(), loanScheduleModelPeriod.periodDueDate(), nonCompoundingCharges, currency, principalInterest, - principalToBeScheduled, totalCumulativeInterest, numberOfRepayments, + scheduleParams.getPrincipalToBeScheduled(), scheduleParams.getTotalCumulativeInterest(), !loanScheduleModelPeriod.isRecalculatedInterestComponent()); Money penaltyChargesForInstallment = cumulativePenaltyChargesDueWithin(loanScheduleModelPeriod.periodFromDate(), loanScheduleModelPeriod.periodDueDate(), nonCompoundingCharges, currency, principalInterest, - principalToBeScheduled, totalCumulativeInterest, numberOfRepayments, + scheduleParams.getPrincipalToBeScheduled(), scheduleParams.getTotalCumulativeInterest(), !loanScheduleModelPeriod.isRecalculatedInterestComponent()); - totalFeeChargesCharged = totalFeeChargesCharged.add(feeChargesForInstallment.getAmount()); - totalPenaltyChargesCharged = totalPenaltyChargesCharged.add(penaltyChargesForInstallment.getAmount()); - totalRepaymentExpected = totalRepaymentExpected.add(feeChargesForInstallment.getAmount()).add( - penaltyChargesForInstallment.getAmount()); + scheduleParams.addTotalFeeChargesCharged(feeChargesForInstallment); + scheduleParams.addTotalPenaltyChargesCharged(penaltyChargesForInstallment); + scheduleParams.addTotalRepaymentExpected(feeChargesForInstallment.plus(penaltyChargesForInstallment)); loanScheduleModelPeriod.addLoanCharges(feeChargesForInstallment.getAmount(), penaltyChargesForInstallment.getAmount()); } } + } - // this block is to add extra re-payment schedules with interest portion - // if the loan not paid with in loan term - - if (scheduleTillDate != null) { - currentDate = scheduleTillDate; + private void updateAmountsWithEffectiveDate(final LoanApplicationTerms loanApplicationTerms, final HolidayDetailDTO holidayDetailDTO, + LoanScheduleParams scheduleParams, LocalDate scheduledDueDate, ScheduleCurrentPeriodParams currentPeriodParams, + LoanScheduleModelPeriod installment) { + LocalDate amountApplicableDate = installment.periodDueDate(); + if (loanApplicationTerms.isInterestRecalculationEnabled()) { + amountApplicableDate = getNextRestScheduleDate(installment.periodDueDate().minusDays(1), loanApplicationTerms, holidayDetailDTO); } - if (isInterestRecalculationRequired(loanApplicationTerms, transactions) && latePaymentMap.size() > 0 - && currentDate.isAfter(periodStartDate)) { - Money totalInterest = addInterestOnlyRepaymentScheduleForCurrentdate(mc, loanApplicationTerms, holidayDetailDTO, currency, - periods, periodStartDate, actualRepaymentDate, instalmentNumber, latePaymentMap, currentDate, - loanRepaymentScheduleTransactionProcessor, principalPortionMap, compoundingMap, transactions, installments, loanCharges); - totalCumulativeInterest = totalCumulativeInterest.plus(totalInterest); + updateMapWithAmount(scheduleParams.getPrincipalPortionMap(), + currentPeriodParams.getPrincipalForThisPeriod().minus(currentPeriodParams.getReducedBalance()), amountApplicableDate); + + // update outstanding balance for interest calculation + updateOutstandingBalanceAsPerRest(scheduleParams, scheduledDueDate); + } + + private LoanScheduleModelPeriod handleRecalculationForTransactions(final MathContext mc, + final LoanApplicationTerms loanApplicationTerms, final HolidayDetailDTO holidayDetailDTO, final MonetaryCurrency currency, + final LoanScheduleParams scheduleParams, + final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, + final Money totalInterestChargedForFullLoanTerm, final LocalDate lastRestDate, final LocalDate scheduledDueDate, + final LocalDate periodStartDateApplicableForInterest, final Collection applicableTransactions, + final ScheduleCurrentPeriodParams currentPeriodParams, final Money lastTotalOutstandingInterestPaymentDueToGrace, + final LoanScheduleModelPeriod installment) { + LoanScheduleModelPeriod modifiedInstallment = installment; + if (scheduleParams.applyInterestRecalculation() && loanRepaymentScheduleTransactionProcessor != null) { + Money principalProcessed = Money.zero(currency); + addLoanRepaymentScheduleInstallment(scheduleParams.getInstallments(), modifiedInstallment); + for (RecalculationDetail detail : applicableTransactions) { + if (!detail.isProcessed()) { + LocalDate transactionDate = detail.getTransactionDate(); + List currentTransactions = new ArrayList<>(2); + currentTransactions.add(detail.getTransaction()); + // applies the transaction as per transaction strategy + // on scheduled installments to identify the + // unprocessed(early payment ) amounts + Money unprocessed = loanRepaymentScheduleTransactionProcessor.handleRepaymentSchedule(currentTransactions, currency, + scheduleParams.getInstallments()); + + if (unprocessed.isGreaterThanZero()) { + scheduleParams.reduceOutstandingBalance(unprocessed); + // pre closure check and processing + modifiedInstallment = handlePrepaymentOfLoan(mc, loanApplicationTerms, holidayDetailDTO, scheduleParams, + totalInterestChargedForFullLoanTerm, scheduledDueDate, periodStartDateApplicableForInterest, + currentPeriodParams.getInterestCalculationGraceOnRepaymentPeriodFraction(), currentPeriodParams, + lastTotalOutstandingInterestPaymentDueToGrace, transactionDate, modifiedInstallment); + + Money addToPrinciapal = Money.zero(currency); + if (scheduleParams.getOutstandingBalance().isLessThanZero()) { + addToPrinciapal = addToPrinciapal.plus(scheduleParams.getOutstandingBalance()); + scheduleParams.setOutstandingBalance(Money.zero(currency)); + } + updateAmountsBasedOnEarlyPayment(loanApplicationTerms, holidayDetailDTO, scheduleParams, modifiedInstallment, + detail, unprocessed, addToPrinciapal); + + scheduleParams.addReducePrincipal(unprocessed); + currentPeriodParams.plusPrincipalForThisPeriod(unprocessed.plus(addToPrinciapal)); + principalProcessed = principalProcessed.plus(unprocessed.plus(addToPrinciapal)); + BigDecimal fixedEmiAmount = loanApplicationTerms.getFixedEmiAmount(); + scheduleParams.setReducePrincipal(applyEarlyPaymentStrategy( + loanApplicationTerms, + scheduleParams.getReducePrincipal(), + scheduleParams.getTotalCumulativePrincipal().plus( + currentPeriodParams.getPrincipalForThisPeriod().minus(principalProcessed)), + scheduleParams.getPeriodNumber() + 1, mc)); + if (loanApplicationTerms.getAmortizationMethod().isEqualInstallment() + && fixedEmiAmount.compareTo(loanApplicationTerms.getFixedEmiAmount()) != 0) { + currentPeriodParams.setEmiAmountChanged(true); + } + + } + } + } + updateLatePaymentsToMap(loanApplicationTerms, holidayDetailDTO, currency, scheduleParams.getLatePaymentMap(), scheduledDueDate, + scheduleParams.getInstallments(), true, lastRestDate, scheduleParams.getCompoundingMap()); + currentPeriodParams.minusPrincipalForThisPeriod(principalProcessed); + } + return modifiedInstallment; + } + + private LoanScheduleModelPeriod handlePrepaymentOfLoan(final MathContext mc, final LoanApplicationTerms loanApplicationTerms, + final HolidayDetailDTO holidayDetailDTO, final LoanScheduleParams scheduleParams, + final Money totalInterestChargedForFullLoanTerm, final LocalDate scheduledDueDate, + LocalDate periodStartDateApplicableForInterest, final double interestCalculationGraceOnRepaymentPeriodFraction, + final ScheduleCurrentPeriodParams currentPeriodParams, final Money lastTotalOutstandingInterestPaymentDueToGrace, + final LocalDate transactionDate, final LoanScheduleModelPeriod installment) { + LoanScheduleModelPeriod modifiedInstallment = installment; + if (scheduleParams.getOutstandingBalance().isLessThan(currentPeriodParams.getInterestForThisPeriod()) + && !scheduledDueDate.equals(transactionDate)) { + final Collection interestRates = loanApplicationTerms.getLoanTermVariations().getInterestRateChanges(); + LocalDate calculateTill = transactionDate; + if (loanApplicationTerms.getPreClosureInterestCalculationStrategy().calculateTillRestFrequencyEnabled()) { + calculateTill = getNextRestScheduleDate(calculateTill.minusDays(1), loanApplicationTerms, holidayDetailDTO); + } + if (scheduleParams.getCompoundingDateVariations().containsKey(periodStartDateApplicableForInterest)) { + scheduleParams.getCompoundingMap().clear(); + scheduleParams.getCompoundingMap().putAll( + scheduleParams.getCompoundingDateVariations().get(periodStartDateApplicableForInterest)); + } + if (currentPeriodParams.isEmiAmountChanged()) { + updateFixedInstallmentAmount(mc, loanApplicationTerms, scheduleParams.getPeriodNumber(), loanApplicationTerms + .getPrincipal().minus(scheduleParams.getTotalCumulativePrincipal())); + } + PrincipalInterest interestTillDate = calculatePrincipalInterestComponentsForPeriod(this.paymentPeriodsInOneYearCalculator, + interestCalculationGraceOnRepaymentPeriodFraction, scheduleParams.getTotalCumulativePrincipal(), + scheduleParams.getTotalCumulativeInterest(), totalInterestChargedForFullLoanTerm, + lastTotalOutstandingInterestPaymentDueToGrace, scheduleParams.getOutstandingBalanceAsPerRest(), loanApplicationTerms, + scheduleParams.getPeriodNumber(), mc, mergeVariationsToMap(scheduleParams), scheduleParams.getCompoundingMap(), + periodStartDateApplicableForInterest, calculateTill, interestRates); + Money diff = currentPeriodParams.getInterestForThisPeriod().minus(interestTillDate.interest()); + if (!scheduleParams.getOutstandingBalance().minus(diff).isGreaterThanZero()) { + scheduleParams.reduceOutstandingBalance(diff); + currentPeriodParams.minusInterestForThisPeriod(diff); + currentPeriodParams.plusPrincipalForThisPeriod(diff); + final Money totalDue = currentPeriodParams.getPrincipalForThisPeriod().plus(currentPeriodParams.getInterestForThisPeriod()); + + // create and replaces repayment period + // from parts + modifiedInstallment = LoanScheduleModelRepaymentPeriod.repayment(scheduleParams.getInstalmentNumber(), + scheduleParams.getPeriodStartDate(), transactionDate, currentPeriodParams.getPrincipalForThisPeriod(), + scheduleParams.getOutstandingBalance(), currentPeriodParams.getInterestForThisPeriod(), + currentPeriodParams.getFeeChargesForInstallment(), currentPeriodParams.getPenaltyChargesForInstallment(), totalDue, + false); + scheduleParams.setTotalOutstandingInterestPaymentDueToGrace(interestTillDate.interestPaymentDueToGrace()); + } + + } + return modifiedInstallment; + } + + private void updateAmountsBasedOnCurrentEarlyPayments(final MathContext mc, final LoanApplicationTerms loanApplicationTerms, + LoanScheduleParams scheduleParams, ScheduleCurrentPeriodParams currentPeriodParams) { + currentPeriodParams.setReducedBalance(currentPeriodParams.getEarlyPaidAmount()); + currentPeriodParams.minusEarlyPaidAmount(currentPeriodParams.getPrincipalForThisPeriod()); + if (currentPeriodParams.getEarlyPaidAmount().isGreaterThanZero()) { + scheduleParams.addReducePrincipal(currentPeriodParams.getEarlyPaidAmount()); + BigDecimal fixedEmiAmount = loanApplicationTerms.getFixedEmiAmount(); + scheduleParams.setReducePrincipal(applyEarlyPaymentStrategy( + loanApplicationTerms, + scheduleParams.getReducePrincipal(), + scheduleParams.getTotalCumulativePrincipal().plus(currentPeriodParams.getPrincipalForThisPeriod()) + .plus(currentPeriodParams.getEarlyPaidAmount()), scheduleParams.getPeriodNumber() + 1, mc)); + if (loanApplicationTerms.getAmortizationMethod().isEqualInstallment() + && fixedEmiAmount.compareTo(loanApplicationTerms.getFixedEmiAmount()) != 0) { + currentPeriodParams.setEmiAmountChanged(true); + } + currentPeriodParams.plusPrincipalForThisPeriod(currentPeriodParams.getEarlyPaidAmount()); } - loanApplicationTerms.resetFixedEmiAmount(); + // update outstandingLoanBlance using current period + // 'principalDue' + scheduleParams.reduceOutstandingBalance(currentPeriodParams.getPrincipalForThisPeriod().minus( + currentPeriodParams.getReducedBalance())); + } - return LoanScheduleModel.from(periods, applicationCurrency, loanTermInDays, principalToBeScheduled, - totalCumulativePrincipal.getAmount(), totalPrincipalPaid, totalCumulativeInterest.getAmount(), totalFeeChargesCharged, - totalPenaltyChargesCharged, totalRepaymentExpected, totalOutstanding); + private void updatePrincipalPortionBasedOnPreviousEarlyPayments(final MonetaryCurrency currency, + final LoanScheduleParams scheduleParams, final ScheduleCurrentPeriodParams currentPeriodParams) { + if (currentPeriodParams.getPrincipalForThisPeriod().isGreaterThan(scheduleParams.getReducePrincipal())) { + currentPeriodParams.minusPrincipalForThisPeriod(scheduleParams.getReducePrincipal()); + scheduleParams.setReducePrincipal(Money.zero(currency)); + } else { + scheduleParams.reduceReducePrincipal(currentPeriodParams.getPrincipalForThisPeriod()); + currentPeriodParams.setPrincipalForThisPeriod(Money.zero(currency)); + } + } + + private void updateCompoundingDetails(LoanScheduleParams scheduleParams, LocalDate periodStartDateApplicableForInterest) { + if (scheduleParams.getCompoundingDateVariations().containsKey(periodStartDateApplicableForInterest)) { + scheduleParams.getCompoundingMap().clear(); + scheduleParams.getCompoundingMap().putAll( + scheduleParams.getCompoundingDateVariations().get(periodStartDateApplicableForInterest)); + } else { + scheduleParams.getCompoundingDateVariations().put(periodStartDateApplicableForInterest, + new TreeMap<>(scheduleParams.getCompoundingMap())); + } + } + + private void handleRecalculationForNonDueDateTransactions(final MathContext mc, final LoanApplicationTerms loanApplicationTerms, + final Set loanCharges, final HolidayDetailDTO holidayDetailDTO, LoanScheduleParams scheduleParams, + final Collection periods, final Money totalInterestChargedForFullLoanTerm, + final LocalDate idealDisbursementDate, final LocalDate lastRestDate, final LocalDate scheduledDueDate, + final LocalDate periodStartDateForInterest, final Collection applicableTransactions, + final ScheduleCurrentPeriodParams currentPeriodParams) { + if (scheduleParams.applyInterestRecalculation()) { + final MonetaryCurrency currency = scheduleParams.getCurrency(); + final Collection interestRates = loanApplicationTerms.getLoanTermVariations().getInterestRateChanges(); + boolean checkForOutstanding = true; + List unprocessedTransactions = new ArrayList<>(); + LoanScheduleModelPeriod installment = null; + LocalDate periodStartDateApplicableForInterest = periodStartDateForInterest; + for (RecalculationDetail detail : applicableTransactions) { + if (detail.isProcessed()) { + continue; + } + boolean updateLatePaymentMap = false; + final LocalDate transactionDate = detail.getTransactionDate(); + if (transactionDate.isBefore(scheduledDueDate)) { + if (scheduleParams.getLoanRepaymentScheduleTransactionProcessor() != null + && scheduleParams.getLoanRepaymentScheduleTransactionProcessor() + .isInterestFirstRepaymentScheduleTransactionProcessor()) { + List currentTransactions = createCurrentTransactionList(detail); + if (!transactionDate.isEqual(scheduleParams.getPeriodStartDate())) { + + int periodDays = Days.daysBetween(scheduleParams.getPeriodStartDate(), transactionDate).getDays(); + // calculates period start date for interest + // calculation as per the configuration + periodStartDateApplicableForInterest = calculateInterestStartDateForPeriod(loanApplicationTerms, + scheduleParams.getPeriodStartDate(), idealDisbursementDate); + + int daysInPeriodApplicable = Days.daysBetween(periodStartDateApplicableForInterest, transactionDate).getDays(); + Money interestForThisinstallment = Money.zero(currency); + if (daysInPeriodApplicable > 0) { + // 5 determine interest till the transaction + // date + if (!scheduleParams.getCompoundingDateVariations().containsKey(periodStartDateApplicableForInterest)) { + scheduleParams.getCompoundingDateVariations().put(periodStartDateApplicableForInterest, + new TreeMap<>(scheduleParams.getCompoundingMap())); + } + PrincipalInterest principalInterestForThisPeriod = calculatePrincipalInterestComponentsForPeriod( + this.paymentPeriodsInOneYearCalculator, + currentPeriodParams.getInterestCalculationGraceOnRepaymentPeriodFraction(), scheduleParams + .getTotalCumulativePrincipal().minus(scheduleParams.getReducePrincipal()), + scheduleParams.getTotalCumulativeInterest(), totalInterestChargedForFullLoanTerm, + scheduleParams.getTotalOutstandingInterestPaymentDueToGrace(), + scheduleParams.getOutstandingBalanceAsPerRest(), loanApplicationTerms, + scheduleParams.getPeriodNumber(), mc, mergeVariationsToMap(scheduleParams), + scheduleParams.getCompoundingMap(), periodStartDateApplicableForInterest, transactionDate, + interestRates); + interestForThisinstallment = principalInterestForThisPeriod.interest(); + + scheduleParams.setTotalOutstandingInterestPaymentDueToGrace(principalInterestForThisPeriod + .interestPaymentDueToGrace()); + } + + Money principalForThisPeriod = Money.zero(currency); + + // applies all the applicable charges to the + // newly + // created installment + PrincipalInterest principalInterest = new PrincipalInterest(principalForThisPeriod, interestForThisinstallment, + null); + Money feeChargesForInstallment = cumulativeFeeChargesDueWithin(scheduleParams.getPeriodStartDate(), + transactionDate, loanCharges, currency, principalInterest, scheduleParams.getPrincipalToBeScheduled(), + scheduleParams.getTotalCumulativeInterest(), false); + Money penaltyChargesForInstallment = cumulativePenaltyChargesDueWithin(scheduleParams.getPeriodStartDate(), + transactionDate, loanCharges, currency, principalInterest, scheduleParams.getPrincipalToBeScheduled(), + scheduleParams.getTotalCumulativeInterest(), false); + + // sum up real totalInstallmentDue from + // components + final Money totalInstallmentDue = principalForThisPeriod.plus(interestForThisinstallment) + .plus(feeChargesForInstallment).plus(penaltyChargesForInstallment); + // create repayment period from parts + installment = LoanScheduleModelRepaymentPeriod.repayment(scheduleParams.getInstalmentNumber(), + scheduleParams.getPeriodStartDate(), transactionDate, principalForThisPeriod, + scheduleParams.getOutstandingBalance(), interestForThisinstallment, feeChargesForInstallment, + penaltyChargesForInstallment, totalInstallmentDue, true); + periods.add(installment); + + // update outstanding balance for interest + // calculation as per the rest + updateOutstandingBalanceAsPerRest(scheduleParams, transactionDate); + + // handle cumulative fields + scheduleParams.addLoanTermInDays(periodDays); + scheduleParams.addTotalRepaymentExpected(totalInstallmentDue); + scheduleParams.addTotalCumulativeInterest(interestForThisinstallment); + scheduleParams.addTotalFeeChargesCharged(feeChargesForInstallment); + scheduleParams.addTotalPenaltyChargesCharged(penaltyChargesForInstallment); + + scheduleParams.setPeriodStartDate(transactionDate); + periodStartDateApplicableForInterest = scheduleParams.getPeriodStartDate(); + updateLatePaymentMap = true; + scheduleParams.incrementInstalmentNumber(); + // creates and insert Loan repayment schedule + // for + // the period + addLoanRepaymentScheduleInstallment(scheduleParams.getInstallments(), installment); + } else if (installment == null) { + installment = ((List) periods).get(periods.size() - 1); + } + // applies the transaction as per transaction + // strategy + // on scheduled installments to identify the + // unprocessed(early payment ) amounts + Money unprocessed = scheduleParams.getLoanRepaymentScheduleTransactionProcessor().handleRepaymentSchedule( + currentTransactions, currency, scheduleParams.getInstallments()); + if (unprocessed.isGreaterThanZero()) { + + if (loanApplicationTerms.getPreClosureInterestCalculationStrategy().calculateTillRestFrequencyEnabled()) { + LocalDate applicableDate = getNextRestScheduleDate(transactionDate.minusDays(1), loanApplicationTerms, + holidayDetailDTO); + checkForOutstanding = transactionDate.isEqual(applicableDate); + + } + // reduces actual outstanding balance + scheduleParams.reduceOutstandingBalance(unprocessed); + // if outstanding balance becomes less than zero + // then adjusts the princiapal + Money addToPrinciapal = Money.zero(currency); + if (!scheduleParams.getOutstandingBalance().isGreaterThanZero()) { + addToPrinciapal = addToPrinciapal.plus(scheduleParams.getOutstandingBalance()); + scheduleParams.setOutstandingBalance(Money.zero(currency)); + currentPeriodParams.setLastInstallment(installment); + } + // updates principal portion map with the early + // payment amounts and applicable date as per + // rest + updateAmountsBasedOnEarlyPayment(loanApplicationTerms, holidayDetailDTO, scheduleParams, installment, detail, + unprocessed, addToPrinciapal); + + // method applies early payment strategy + scheduleParams.addReducePrincipal(unprocessed); + scheduleParams.setReducePrincipal(applyEarlyPaymentStrategy(loanApplicationTerms, + scheduleParams.getReducePrincipal(), scheduleParams.getTotalCumulativePrincipal(), + scheduleParams.getPeriodNumber(), mc)); + } + // identify late payments and add compounding + // details to + // map for interest calculation + handleLatePayments(loanApplicationTerms, holidayDetailDTO, currency, scheduleParams, lastRestDate, + periodStartDateApplicableForInterest, detail); + if (updateLatePaymentMap) { + updateLatePaymentsToMap(loanApplicationTerms, holidayDetailDTO, currency, scheduleParams.getLatePaymentMap(), + scheduledDueDate, scheduleParams.getInstallments(), true, lastRestDate, + scheduleParams.getCompoundingMap()); + } + } else if (scheduleParams.getLoanRepaymentScheduleTransactionProcessor() != null) { + LocalDate applicableDate = getNextRestScheduleDate(transactionDate.minusDays(1), loanApplicationTerms, + holidayDetailDTO); + if (applicableDate.isBefore(scheduledDueDate)) { + List currentTransactions = createCurrentTransactionList(detail); + Money unprocessed = scheduleParams.getLoanRepaymentScheduleTransactionProcessor().handleRepaymentSchedule( + currentTransactions, currency, scheduleParams.getInstallments()); + Money arrears = fetchCompoundedArrears(loanApplicationTerms, currency, detail.getTransaction()); + if (unprocessed.isGreaterThanZero()) { + arrears = getTotalAmount(scheduleParams.getLatePaymentMap(), currency); + updateMapWithAmount(scheduleParams.getPrincipalPortionMap(), unprocessed, applicableDate); + currentPeriodParams.plusEarlyPaidAmount(unprocessed); + + // this check is to identify pre-closure and + // apply interest calculation as per + // configuration for non due date payments + if (!scheduleParams.getOutstandingBalance().isGreaterThan(unprocessed) + && !loanApplicationTerms.getPreClosureInterestCalculationStrategy() + .calculateTillRestFrequencyEnabled()) { + + LocalDate calculateTill = transactionDate; + if (!scheduleParams.getCompoundingDateVariations().containsKey(periodStartDateApplicableForInterest)) { + scheduleParams.getCompoundingDateVariations().put(periodStartDateApplicableForInterest, + new TreeMap<>(scheduleParams.getCompoundingMap())); + } + PrincipalInterest principalInterestForThisPeriod = calculatePrincipalInterestComponentsForPeriod( + this.paymentPeriodsInOneYearCalculator, + currentPeriodParams.getInterestCalculationGraceOnRepaymentPeriodFraction(), scheduleParams + .getTotalCumulativePrincipal().minus(scheduleParams.getReducePrincipal()), + scheduleParams.getTotalCumulativeInterest(), totalInterestChargedForFullLoanTerm, + scheduleParams.getTotalOutstandingInterestPaymentDueToGrace(), + scheduleParams.getOutstandingBalanceAsPerRest(), loanApplicationTerms, + scheduleParams.getPeriodNumber(), mc, mergeVariationsToMap(scheduleParams), + scheduleParams.getCompoundingMap(), periodStartDateApplicableForInterest, calculateTill, + interestRates); + if (!principalInterestForThisPeriod.interest() + .plus(principalInterestForThisPeriod.interestPaymentDueToGrace()) + .plus(scheduleParams.getOutstandingBalance()).isGreaterThan(unprocessed)) { + currentPeriodParams.minusEarlyPaidAmount(unprocessed); + updateMapWithAmount(scheduleParams.getPrincipalPortionMap(), unprocessed.negated(), applicableDate); + LoanTransaction loanTransaction = LoanTransaction.repayment(null, unprocessed, null, + transactionDate, null, DateUtils.getLocalDateTimeOfTenant(), null); + RecalculationDetail recalculationDetail = new RecalculationDetail(transactionDate, loanTransaction); + unprocessedTransactions.add(recalculationDetail); + break; + } + } + LoanTransaction loanTransaction = LoanTransaction.repayment(null, unprocessed, null, scheduledDueDate, + null, DateUtils.getLocalDateTimeOfTenant(), null); + RecalculationDetail recalculationDetail = new RecalculationDetail(scheduledDueDate, loanTransaction); + unprocessedTransactions.add(recalculationDetail); + checkForOutstanding = false; + + scheduleParams.reduceOutstandingBalance(unprocessed); + // if outstanding balance becomes less than + // zero + // then adjusts the princiapal + Money addToPrinciapal = Money.zero(currency); + if (scheduleParams.getOutstandingBalance().isLessThanZero()) { + addToPrinciapal = addToPrinciapal.plus(scheduleParams.getOutstandingBalance()); + scheduleParams.setOutstandingBalance(Money.zero(currency)); + updateMapWithAmount(scheduleParams.getPrincipalPortionMap(), addToPrinciapal, applicableDate); + currentPeriodParams.plusEarlyPaidAmount(addToPrinciapal); + } + + } + if (arrears.isGreaterThanZero() && applicableDate.isBefore(lastRestDate)) { + handleLatePayments(loanApplicationTerms, holidayDetailDTO, currency, scheduleParams, lastRestDate, + periodStartDateApplicableForInterest, detail); + } + } + + } + } + + } + applicableTransactions.addAll(unprocessedTransactions); + if (checkForOutstanding && scheduleParams.getOutstandingBalance().isZero() && scheduleParams.getDisburseDetailMap().isEmpty()) { + currentPeriodParams.setSkipCurrentLoop(true); + } + } + } + + /** + * @param loanApplicationTerms + * @param holidayDetailDTO + * @param currency + * @param scheduleParams + * @param lastRestDate + * @param periodStartDateApplicableForInterest + * @param detail + */ + private void handleLatePayments(final LoanApplicationTerms loanApplicationTerms, final HolidayDetailDTO holidayDetailDTO, + final MonetaryCurrency currency, LoanScheduleParams scheduleParams, LocalDate lastRestDate, + LocalDate periodStartDateApplicableForInterest, RecalculationDetail detail) { + updateLatePaidAmountsToPrincipalMap(detail.getTransaction(), loanApplicationTerms, currency, holidayDetailDTO, lastRestDate, + scheduleParams); + scheduleParams.getCompoundingDateVariations().put(periodStartDateApplicableForInterest, + new TreeMap<>(scheduleParams.getCompoundingMap())); + } + + private void updateAmountsBasedOnEarlyPayment(final LoanApplicationTerms loanApplicationTerms, final HolidayDetailDTO holidayDetailDTO, + LoanScheduleParams scheduleParams, final LoanScheduleModelPeriod installment, RecalculationDetail detail, Money unprocessed, + Money addToPrinciapal) { + updatePrincipalPaidPortionToMap(loanApplicationTerms, holidayDetailDTO, scheduleParams.getPrincipalPortionMap(), installment, + detail, unprocessed.plus(addToPrinciapal), scheduleParams.getInstallments()); + scheduleParams.addTotalRepaymentExpected(unprocessed.plus(addToPrinciapal)); + scheduleParams.addTotalCumulativePrincipal(unprocessed.plus(addToPrinciapal)); + } + + private void updateOutstandingBalanceAsPerRest(final LoanScheduleParams scheduleParams, final LocalDate scheduledDueDate) { + scheduleParams.setOutstandingBalanceAsPerRest(updateBalanceForInterestCalculation(scheduleParams.getPrincipalPortionMap(), + scheduledDueDate, scheduleParams.getOutstandingBalanceAsPerRest(), false)); + scheduleParams.setOutstandingBalanceAsPerRest(updateBalanceForInterestCalculation(scheduleParams.getDisburseDetailMap(), + scheduledDueDate, scheduleParams.getOutstandingBalanceAsPerRest(), true)); + } + + /** + * Method updates outstanding balance of the loan for interest calculation + * + */ + private void updateBalanceBasedOnDisbursement(final LoanApplicationTerms loanApplicationTerms, + final BigDecimal chargesDueAtTimeOfDisbursement, LoanScheduleParams scheduleParams, + final Collection periods, final LocalDate scheduledDueDate) { + for (Map.Entry disburseDetail : scheduleParams.getDisburseDetailMap().entrySet()) { + if (disburseDetail.getKey().isAfter(scheduleParams.getPeriodStartDate()) && !disburseDetail.getKey().isAfter(scheduledDueDate)) { + // validation check for amount not exceeds specified max + // amount as per the configuration + if (loanApplicationTerms.getMaxOutstandingBalance() != null + && scheduleParams.getOutstandingBalance().plus(disburseDetail.getValue()) + .isGreaterThan(loanApplicationTerms.getMaxOutstandingBalance())) { + String errorMsg = "Outstanding balance must not exceed the amount: " + loanApplicationTerms.getMaxOutstandingBalance(); + throw new MultiDisbursementOutstandingAmoutException(errorMsg, loanApplicationTerms.getMaxOutstandingBalance() + .getAmount(), disburseDetail.getValue()); + } + + // creates and add disbursement detail to the repayments + // period + final LoanScheduleModelDisbursementPeriod disbursementPeriod = LoanScheduleModelDisbursementPeriod.disbursement( + disburseDetail.getKey(), disburseDetail.getValue(), chargesDueAtTimeOfDisbursement); + periods.add(disbursementPeriod); + // updates actual outstanding balance with new + // disbursement detail + scheduleParams.addOutstandingBalance(disburseDetail.getValue()); + scheduleParams.addPrincipalToBeScheduled(disburseDetail.getValue()); + loanApplicationTerms.setPrincipal(loanApplicationTerms.getPrincipal().plus(disburseDetail.getValue())); + } + } + } + + /** + * @param loanApplicationTerms + * @param scheduleParams + * @param priviousScheduledDueDate + * @param previousRepaymentDate + * @param scheduledDueDate + * @param scheduleDateForReversal + * @return + */ + private LoanTermVariationParams applyLoanTermVariations(final LoanApplicationTerms loanApplicationTerms, + final LoanScheduleParams scheduleParams, final LocalDate previousRepaymentDate, final LocalDate scheduledDueDate) { + boolean skipPeriod = false; + boolean recalculateAmounts = false; + LocalDate modifiedScheduledDueDate = scheduledDueDate; + + // due date changes should be applied only for that dueDate + if (loanApplicationTerms.getLoanTermVariations().hasDueDateVariation(scheduledDueDate)) { + LoanTermVariationsData loanTermVariationsData = loanApplicationTerms.getLoanTermVariations().nextDueDateVariation(); + if (loanTermVariationsData.getTermApplicableFrom().isEqual(modifiedScheduledDueDate)) { + modifiedScheduledDueDate = loanTermVariationsData.getDateValue(); + if (!loanTermVariationsData.isSpecificToInstallment()) { + scheduleParams.setActualRepaymentDate(modifiedScheduledDueDate); + } + loanTermVariationsData.setProcessed(true); + } + } + + while (loanApplicationTerms.getLoanTermVariations().hasVariation(modifiedScheduledDueDate)) { + LoanTermVariationsData loanTermVariationsData = loanApplicationTerms.getLoanTermVariations().nextVariation(); + if (loanTermVariationsData.isProcessed()) { + continue; + } + switch (loanTermVariationsData.getTermVariationType()) { + case INSERT_INSTALLMENT: + scheduleParams.setActualRepaymentDate(previousRepaymentDate); + modifiedScheduledDueDate = loanTermVariationsData.getTermApplicableFrom(); + if (loanTermVariationsData.getDecimalValue() != null) { + if (loanApplicationTerms.getInterestMethod().isDecliningBalnce() + && loanApplicationTerms.getAmortizationMethod().isEqualInstallment()) { + loanApplicationTerms.setCurrentPeriodFixedEmiAmount(loanTermVariationsData.getDecimalValue()); + } else { + loanApplicationTerms.setCurrentPeriodFixedPrincipalAmount(loanTermVariationsData.getDecimalValue()); + } + recalculateAmounts = true; + } + loanTermVariationsData.setProcessed(true); + break; + case DELETE_INSTALLMENT: + if (loanTermVariationsData.getTermApplicableFrom().isEqual(modifiedScheduledDueDate)) { + skipPeriod = true; + loanTermVariationsData.setProcessed(true); + } + break; + case EMI_AMOUNT: + if (loanTermVariationsData.isSpecificToInstallment()) { + loanApplicationTerms.setCurrentPeriodFixedEmiAmount(loanTermVariationsData.getDecimalValue()); + recalculateAmounts = true; + } else { + loanApplicationTerms.setFixedEmiAmount(loanTermVariationsData.getDecimalValue()); + } + loanTermVariationsData.setProcessed(true); + break; + case PRINCIPAL_AMOUNT: + if (loanTermVariationsData.isSpecificToInstallment()) { + loanApplicationTerms.setCurrentPeriodFixedPrincipalAmount(loanTermVariationsData.getDecimalValue()); + recalculateAmounts = true; + } else { + loanApplicationTerms.setFixedPrincipalAmount(loanTermVariationsData.getDecimalValue()); + } + loanTermVariationsData.setProcessed(true); + break; + default: + break; + + } + } + LoanTermVariationParams termVariationParams = new LoanTermVariationParams(skipPeriod, recalculateAmounts, modifiedScheduledDueDate); + return termVariationParams; + } + + /** + * @param loanApplicationTerms + * @param scheduleParams + * @param interestRates + */ + private void applyLoanVariationsForPartialScheduleGenerate(final LoanApplicationTerms loanApplicationTerms, + LoanScheduleParams scheduleParams, final Collection interestRates) { + // Applies loan variations + while (loanApplicationTerms.getLoanTermVariations().hasVariation(scheduleParams.getPeriodStartDate())) { + LoanTermVariationsData variation = loanApplicationTerms.getLoanTermVariations().nextVariation(); + if (!variation.isSpecificToInstallment()) { + switch (variation.getTermVariationType()) { + case EMI_AMOUNT: + loanApplicationTerms.setFixedEmiAmount(variation.getDecimalValue()); + break; + case PRINCIPAL_AMOUNT: + loanApplicationTerms.setFixedPrincipalAmount(variation.getDecimalValue()); + break; + default: + break; + } + } + + variation.setProcessed(true); + } + + // Applies interest rate changes + for (LoanTermVariationsData variation : interestRates) { + if (variation.getTermVariationType().isInterestRateVariation() && variation.isApplicable(scheduleParams.getPeriodStartDate()) + && variation.getDecimalValue() != null) { + loanApplicationTerms.updateAnnualNominalInterestRate(variation.getDecimalValue()); + } + } } /** @@ -820,18 +1007,16 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener return principalToBeScheduled; } - private boolean updateFixedInstallmentAmount(final MathContext mc, final LoanApplicationTerms loanApplicationTerms, - LocalDate actualRepaymentDate, int periodNumber, Money outstandingBalance, final HolidayDetailDTO holidayDetailDTO) { + private boolean updateFixedInstallmentAmount(final MathContext mc, final LoanApplicationTerms loanApplicationTerms, int periodNumber, + Money outstandingBalance) { boolean isAmountChanged = false; if (loanApplicationTerms.getActualFixedEmiAmount() == null && loanApplicationTerms.getInterestMethod().isDecliningBalnce() && loanApplicationTerms.getAmortizationMethod().isEqualInstallment()) { - LocalDate nextPeriodDate = this.scheduledDateGenerator.generateNextRepaymentDate(actualRepaymentDate, loanApplicationTerms, - false, holidayDetailDTO); if (periodNumber < loanApplicationTerms.getPrincipalGrace() + 1) { periodNumber = loanApplicationTerms.getPrincipalGrace() + 1; } - Money emiAmount = loanApplicationTerms.pmtForInstallment(this.paymentPeriodsInOneYearCalculator, - Days.daysBetween(actualRepaymentDate, nextPeriodDate).getDays(), outstandingBalance, periodNumber, mc); + Money emiAmount = loanApplicationTerms.pmtForInstallment(this.paymentPeriodsInOneYearCalculator, outstandingBalance, + periodNumber, mc); loanApplicationTerms.setFixedEmiAmount(emiAmount.getAmount()); isAmountChanged = true; } @@ -851,11 +1036,6 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener return arrears; } - private boolean isInterestRecalculationRequired(final LoanApplicationTerms loanApplicationTerms, - Collection transactions) { - return loanApplicationTerms.isInterestRecalculationEnabled() && transactions != null; - } - /** * Method calculates interest on not paid outstanding principal and interest * (if compounding is enabled) till current date and adds new repayment @@ -871,74 +1051,71 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener */ private Money addInterestOnlyRepaymentScheduleForCurrentdate(final MathContext mc, final LoanApplicationTerms loanApplicationTerms, final HolidayDetailDTO holidayDetailDTO, final MonetaryCurrency currency, final Collection periods, - LocalDate periodStartDate, LocalDate actualRepaymentDate, int instalmentNumber, Map latePaymentMap, final LocalDate currentDate, LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, - final Map principalPortionMap, TreeMap compoundingMap, - final Collection transactions, final List installments, - Set loanCharges) { + final Collection transactions, final Set loanCharges, final LoanScheduleParams params) { boolean isFirstRepayment = false; - LocalDate startDate = periodStartDate; + LocalDate startDate = params.getPeriodStartDate(); Money outstanding = Money.zero(currency); Money totalInterest = Money.zero(currency); Money totalCumulativeInterest = Money.zero(currency); - Map disburseDetailsMap = new HashMap<>(); double interestCalculationGraceOnRepaymentPeriodFraction = Double.valueOf(0); int periodNumberTemp = 1; LocalDate lastRestDate = getNextRestScheduleDate(currentDate.minusDays(1), loanApplicationTerms, holidayDetailDTO); - Collection applicableVariations = loanApplicationTerms.getLoanTermVariations(); + Collection applicableVariations = loanApplicationTerms.getLoanTermVariations().getInterestRateChanges(); do { - actualRepaymentDate = this.scheduledDateGenerator.generateNextRepaymentDate(actualRepaymentDate, loanApplicationTerms, - isFirstRepayment, holidayDetailDTO); - int daysInPeriod = Days.daysBetween(periodStartDate, actualRepaymentDate).getDays(); - if (actualRepaymentDate.isAfter(currentDate)) { - actualRepaymentDate = currentDate; + params.setActualRepaymentDate(this.scheduledDateGenerator.generateNextRepaymentDate(params.getActualRepaymentDate(), + loanApplicationTerms, isFirstRepayment, holidayDetailDTO)); + if (params.getActualRepaymentDate().isAfter(currentDate)) { + params.setActualRepaymentDate(currentDate); } - outstanding = updateOutstandingFromLatePayment(periodStartDate, latePaymentMap, outstanding); + outstanding = updateOutstandingFromLatePayment(params.getPeriodStartDate(), params.getLatePaymentMap(), outstanding); - Collection applicableTransactions = getApplicableTransactionsForPeriod(loanApplicationTerms, - actualRepaymentDate, transactions); + Collection applicableTransactions = getApplicableTransactionsForPeriod( + params.applyInterestRecalculation(), params.getActualRepaymentDate(), transactions); - if (!latePaymentMap.isEmpty()) { - populateCompoundingDatesInPeriod(periodStartDate, actualRepaymentDate, currentDate, loanApplicationTerms, holidayDetailDTO, - compoundingMap, loanCharges, currency); + if (!params.getLatePaymentMap().isEmpty()) { + populateCompoundingDatesInPeriod(params.getPeriodStartDate(), params.getActualRepaymentDate(), currentDate, + loanApplicationTerms, holidayDetailDTO, params.getCompoundingMap(), loanCharges, currency); } for (RecalculationDetail detail : applicableTransactions) { if (detail.isProcessed()) { continue; } + LocalDate transactionDate = detail.getTransactionDate(); List currentTransactions = createCurrentTransactionList(detail); - if (!periodStartDate.isEqual(detail.getTransactionDate())) { + if (!params.getPeriodStartDate().isEqual(transactionDate)) { PrincipalInterest principalInterestForThisPeriod = calculatePrincipalInterestComponentsForPeriod( this.paymentPeriodsInOneYearCalculator, interestCalculationGraceOnRepaymentPeriodFraction, totalInterest.zero(), totalInterest.zero(), totalInterest.zero(), totalInterest.zero(), outstanding, - loanApplicationTerms, periodNumberTemp, mc, - mergeVariationsToMap(principalPortionMap, latePaymentMap, disburseDetailsMap, compoundingMap), compoundingMap, - periodStartDate, detail.getTransactionDate(), daysInPeriod, applicableVariations); + loanApplicationTerms, periodNumberTemp, mc, mergeVariationsToMap(params), params.getCompoundingMap(), + params.getPeriodStartDate(), transactionDate, applicableVariations); Money interest = principalInterestForThisPeriod.interest(); totalInterest = totalInterest.plus(interest); - LoanScheduleModelRepaymentPeriod installment = LoanScheduleModelRepaymentPeriod.repayment(instalmentNumber++, - startDate, detail.getTransactionDate(), totalInterest.zero(), totalInterest.zero(), totalInterest, - totalInterest.zero(), totalInterest.zero(), totalInterest, true); + LoanScheduleModelRepaymentPeriod installment = LoanScheduleModelRepaymentPeriod.repayment(params.getInstalmentNumber(), + startDate, transactionDate, totalInterest.zero(), totalInterest.zero(), totalInterest, totalInterest.zero(), + totalInterest.zero(), totalInterest, true); + params.incrementInstalmentNumber(); periods.add(installment); totalCumulativeInterest = totalCumulativeInterest.plus(totalInterest); totalInterest = totalInterest.zero(); - addLoanRepaymentScheduleInstallment(installments, installment); - periodStartDate = detail.getTransactionDate(); - startDate = detail.getTransactionDate(); + addLoanRepaymentScheduleInstallment(params.getInstallments(), installment); + params.setPeriodStartDate(transactionDate); + startDate = transactionDate; } - loanRepaymentScheduleTransactionProcessor.handleRepaymentSchedule(currentTransactions, currency, installments); - updateLatePaymentsToMap(loanApplicationTerms, holidayDetailDTO, currency, latePaymentMap, currentDate, installments, false, - lastRestDate, compoundingMap); + loanRepaymentScheduleTransactionProcessor.handleRepaymentSchedule(currentTransactions, currency, params.getInstallments()); + updateLatePaymentsToMap(loanApplicationTerms, holidayDetailDTO, currency, params.getLatePaymentMap(), currentDate, + params.getInstallments(), false, lastRestDate, params.getCompoundingMap()); outstanding = outstanding.zero(); - outstanding = updateOutstandingFromLatePayment(periodStartDate, latePaymentMap, outstanding); - outstanding = updateBalanceForInterestCalculation(principalPortionMap, periodStartDate, outstanding, false); - if (latePaymentMap.isEmpty() && !outstanding.isGreaterThanZero()) { + outstanding = updateOutstandingFromLatePayment(params.getPeriodStartDate(), params.getLatePaymentMap(), outstanding); + outstanding = updateBalanceForInterestCalculation(params.getPrincipalPortionMap(), params.getPeriodStartDate(), + outstanding, false); + if (params.getLatePaymentMap().isEmpty() && !outstanding.isGreaterThanZero()) { break; } } @@ -947,35 +1124,35 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener PrincipalInterest principalInterestForThisPeriod = calculatePrincipalInterestComponentsForPeriod( this.paymentPeriodsInOneYearCalculator, interestCalculationGraceOnRepaymentPeriodFraction, totalInterest.zero(), totalInterest.zero(), totalInterest.zero(), totalInterest.zero(), outstanding, loanApplicationTerms, - periodNumberTemp, mc, - mergeVariationsToMap(principalPortionMap, latePaymentMap, disburseDetailsMap, compoundingMap), compoundingMap, - periodStartDate, actualRepaymentDate, daysInPeriod, applicableVariations); + periodNumberTemp, mc, mergeVariationsToMap(params), params.getCompoundingMap(), params.getPeriodStartDate(), + params.getActualRepaymentDate(), applicableVariations); Money interest = principalInterestForThisPeriod.interest(); totalInterest = totalInterest.plus(interest); if (loanApplicationTerms.getInterestRecalculationCompoundingMethod().isInterestCompoundingEnabled()) { - LocalDate compoundingEffectiveDate = getNextCompoundScheduleDate(actualRepaymentDate.minusDays(1), + LocalDate compoundingEffectiveDate = getNextCompoundScheduleDate(params.getActualRepaymentDate().minusDays(1), loanApplicationTerms, holidayDetailDTO); - latePaymentMap.put(compoundingEffectiveDate, interest); + params.getLatePaymentMap().put(compoundingEffectiveDate, interest); } } - periodStartDate = actualRepaymentDate; - } while (actualRepaymentDate.isBefore(currentDate) && outstanding.isGreaterThanZero()); + params.setPeriodStartDate(params.getActualRepaymentDate()); + } while (params.getActualRepaymentDate().isBefore(currentDate) && outstanding.isGreaterThanZero()); if (totalInterest.isGreaterThanZero()) { - LoanScheduleModelRepaymentPeriod installment = LoanScheduleModelRepaymentPeriod.repayment(instalmentNumber++, startDate, - actualRepaymentDate, totalInterest.zero(), totalInterest.zero(), totalInterest, totalInterest.zero(), - totalInterest.zero(), totalInterest, true); + LoanScheduleModelRepaymentPeriod installment = LoanScheduleModelRepaymentPeriod.repayment(params.getInstalmentNumber(), + startDate, params.getActualRepaymentDate(), totalInterest.zero(), totalInterest.zero(), totalInterest, + totalInterest.zero(), totalInterest.zero(), totalInterest, true); + params.incrementInstalmentNumber(); periods.add(installment); totalCumulativeInterest = totalCumulativeInterest.plus(totalInterest); } return totalCumulativeInterest; } - private Collection getApplicableTransactionsForPeriod(final LoanApplicationTerms loanApplicationTerms, + private Collection getApplicableTransactionsForPeriod(final boolean applyInterestRecalculation, LocalDate repaymentDate, final Collection transactions) { Collection applicableTransactions = new ArrayList<>(); - if (isInterestRecalculationRequired(loanApplicationTerms, transactions)) { + if (applyInterestRecalculation) { for (RecalculationDetail detail : transactions) { if (!detail.getTransactionDate().isAfter(repaymentDate)) { applicableTransactions.add(detail); @@ -987,10 +1164,10 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener } private Collection getApplicableTermVariationsForPeriod(final LocalDate fromDate, final LocalDate dueDate, - final int installmentNumber, final Collection variations) { + final Collection variations) { Collection applicableVariations = new ArrayList<>(); for (LoanTermVariationsData detail : variations) { - if (detail.isApplicable(fromDate, dueDate, installmentNumber)) { + if (detail.isApplicable(fromDate, dueDate)) { applicableVariations.add(detail); } } @@ -1022,30 +1199,13 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener /** * method applies early payment strategy as per the configurations provided - * - * @param totalCumulativePrincipal - * TODO - * @param periodNumber - * TODO - * @param mc - * TODO */ private Money applyEarlyPaymentStrategy(final LoanApplicationTerms loanApplicationTerms, Money reducePrincipal, - final Money totalCumulativePrincipal, int periodNumber, final MathContext mc, final HolidayDetailDTO holidayDetailDTO) { + final Money totalCumulativePrincipal, int periodNumber, final MathContext mc) { if (reducePrincipal.isGreaterThanZero()) { switch (loanApplicationTerms.getRescheduleStrategyMethod()) { case REDUCE_EMI_AMOUNT: - // in this case emi amount will be reduced but number of - // installments won't change - Money principal = getPrincipalToBeScheduled(loanApplicationTerms); - if (loanApplicationTerms.getActualFixedEmiAmount() == null) { - loanApplicationTerms.setFixedEmiAmount(null); - updateFixedInstallmentAmount(mc, loanApplicationTerms, loanApplicationTerms.getExpectedDisbursementDate(), - periodNumber, principal.minus(totalCumulativePrincipal), holidayDetailDTO); - } - if (loanApplicationTerms.getAmortizationMethod().isEqualPrincipal()) { - loanApplicationTerms.updateFixedPrincipalAmount(mc, periodNumber, principal.minus(totalCumulativePrincipal)); - } + adjustInstallmentOrPrincipalAmount(loanApplicationTerms, totalCumulativePrincipal, periodNumber, mc); reducePrincipal = reducePrincipal.zero(); break; case REDUCE_NUMBER_OF_INSTALLMENTS: @@ -1066,6 +1226,21 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener return reducePrincipal; } + private void adjustInstallmentOrPrincipalAmount(final LoanApplicationTerms loanApplicationTerms, final Money totalCumulativePrincipal, + int periodNumber, final MathContext mc) { + // in this case emi amount will be reduced but number of + // installments won't change + Money principal = getPrincipalToBeScheduled(loanApplicationTerms); + if (!principal.minus(totalCumulativePrincipal).isGreaterThanZero()) { return; } + if (loanApplicationTerms.getAmortizationMethod().isEqualPrincipal()) { + loanApplicationTerms.updateFixedPrincipalAmount(mc, periodNumber, principal.minus(totalCumulativePrincipal)); + } else if (loanApplicationTerms.getActualFixedEmiAmount() == null) { + loanApplicationTerms.setFixedEmiAmount(null); + updateFixedInstallmentAmount(mc, loanApplicationTerms, periodNumber, principal.minus(totalCumulativePrincipal)); + } + + } + /** * Identifies all the past date principal changes and apply them on * outstanding balance for future calculations @@ -1092,10 +1267,9 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener // this is to make sure even paid late payments(principal and compounded // interest/fee) should be reduced as per rest date - private void updateLatePaidAmountsToPrincipalMap(final Map principalVariationMap, - final LoanTransaction loanTransaction, final Map latePaymentsMap, final Map compoundingMap, - final LoanApplicationTerms applicationTerms, final MonetaryCurrency currency, final HolidayDetailDTO holidayDetailDTO, - final LocalDate lastRestDate) { + private void updateLatePaidAmountsToPrincipalMap(final LoanTransaction loanTransaction, final LoanApplicationTerms applicationTerms, + final MonetaryCurrency currency, final HolidayDetailDTO holidayDetailDTO, final LocalDate lastRestDate, + final LoanScheduleParams params) { LocalDate applicableDate = getNextRestScheduleDate(loanTransaction.getTransactionDate().minusDays(1), applicationTerms, holidayDetailDTO); @@ -1109,8 +1283,10 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener loanTransaction.getPenaltyChargesPortion(currency)); } - updateCompoundingAmount(principalVariationMap, latePaymentsMap, currency, lastRestDate, principalPortion, applicableDate); - updateCompoundingAmount(principalVariationMap, compoundingMap, currency, lastRestDate, compoundedLatePayments, applicableDate); + updateCompoundingAmount(params.getPrincipalPortionMap(), params.getLatePaymentMap(), currency, lastRestDate, principalPortion, + applicableDate); + updateCompoundingAmount(params.getPrincipalPortionMap(), params.getCompoundingMap(), currency, lastRestDate, + compoundedLatePayments, applicableDate); } private void updateCompoundingAmount(final Map principalVariationMap, @@ -1148,11 +1324,6 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener * effective date as per REST(for principal portion ) and compounding * (interest or fee or interest and fee portions) frequency * - * @param lastRestDate - * TODO - * @param compoundingMap - * TODO - * */ private void updateLatePaymentsToMap(final LoanApplicationTerms loanApplicationTerms, final HolidayDetailDTO holidayDetailDTO, final MonetaryCurrency currency, final Map latePaymentMap, final LocalDate scheduledDueDate, @@ -1262,9 +1433,9 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener updateMapWithAmount(compoundingMap, Money.zero(currency), compoundingDate); } else { Money feeChargesForInstallment = cumulativeFeeChargesDueWithin(lastCompoundingDate, compoundingDate, charges, currency, - null, loanApplicationTerms.getPrincipal(), null, loanApplicationTerms.getNumberOfRepayments(), false); + null, loanApplicationTerms.getPrincipal(), null, false); Money penaltyChargesForInstallment = cumulativePenaltyChargesDueWithin(lastCompoundingDate, compoundingDate, charges, - currency, null, loanApplicationTerms.getPrincipal(), null, loanApplicationTerms.getNumberOfRepayments(), false); + currency, null, loanApplicationTerms.getPrincipal(), null, false); updateMapWithAmount(compoundingMap, feeChargesForInstallment.plus(penaltyChargesForInstallment), compoundingDate); } lastCompoundingDate = compoundingDate; @@ -1304,16 +1475,11 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener * payment compounding and principal change as per rest) changes to single * map for interest calculation * - * @param compoundingDates - * TODO */ - private TreeMap mergeVariationsToMap(final Map princiaplPaidMap, - final Map latePaymentMap, final Map disburseDetailsMap, - final Map compoundingDates) { + private TreeMap mergeVariationsToMap(final LoanScheduleParams params) { TreeMap map = new TreeMap<>(); - map.putAll(latePaymentMap); - - for (Map.Entry mapEntry : disburseDetailsMap.entrySet()) { + map.putAll(params.getLatePaymentMap()); + for (Map.Entry mapEntry : params.getDisburseDetailMap().entrySet()) { Money value = mapEntry.getValue(); if (map.containsKey(mapEntry.getKey())) { value = value.plus(map.get(mapEntry.getKey())); @@ -1321,7 +1487,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener map.put(mapEntry.getKey(), value); } - for (Map.Entry mapEntry : princiaplPaidMap.entrySet()) { + for (Map.Entry mapEntry : params.getPrincipalPortionMap().entrySet()) { Money value = mapEntry.getValue().negated(); if (map.containsKey(mapEntry.getKey())) { value = value.plus(map.get(mapEntry.getKey())); @@ -1329,7 +1495,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener map.put(mapEntry.getKey(), value); } - for (Map.Entry mapEntry : compoundingDates.entrySet()) { + for (Map.Entry mapEntry : params.getCompoundingMap().entrySet()) { Money value = mapEntry.getValue(); if (!map.containsKey(mapEntry.getKey())) { map.put(mapEntry.getKey(), value.zero()); @@ -1343,7 +1509,8 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener * calculates Interest stating date as per the settings */ private LocalDate calculateInterestStartDateForPeriod(final LoanApplicationTerms loanApplicationTerms, LocalDate periodStartDate, - final LocalDate idealDisbursementDate, LocalDate periodStartDateApplicableForInterest) { + final LocalDate idealDisbursementDate) { + LocalDate periodStartDateApplicableForInterest = periodStartDate; if (periodStartDate.isBefore(idealDisbursementDate)) { if (loanApplicationTerms.getInterestChargedFromLocalDate() != null) { if (periodStartDate.isEqual(loanApplicationTerms.getExpectedDisbursementDate()) @@ -1525,12 +1692,17 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener compoundingCalendarInstance, loanCalendar, floatingRateDTO); // for applying variations - Collection loanTermVariations = new ArrayList<>(loanApplicationTerms.getLoanTermVariations()); + Collection loanTermVariations = loanApplicationTerms.getLoanTermVariations().getInterestRateChanges(); // update the number of repayments loanApplicationTerms.updateNumberOfRepayments(numberOfRepayments); LocalDate loanEndDate = this.scheduledDateGenerator.getLastRepaymentDate(loanApplicationTerms, holidayDetailDTO); + LoanTermVariationsData lastDueDateVariation = loanApplicationTerms.getLoanTermVariations().fetchLoanTermDueDateVariationsData( + loanEndDate); + if (lastDueDateVariation != null) { + loanEndDate = lastDueDateVariation.getDateValue(); + } loanApplicationTerms.updateLoanEndDate(loanEndDate); if (newInterestRate != null) { @@ -1596,14 +1768,14 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener // for applying variations Collection applicableVariations = getApplicableTermVariationsForPeriod(installmentFromDate, - adjustedInstallmentDueDate, periodNumber, loanTermVariations); + adjustedInstallmentDueDate, loanTermVariations); // determine the interest & principal for the period PrincipalInterest principalInterestForThisPeriod = calculatePrincipalInterestComponentsForPeriod( this.paymentPeriodsInOneYearCalculator, interestCalculationGraceOnRepaymentPeriodFraction, totalCumulativePrincipal, totalCumulativeInterest, totalInterestChargedForFullLoanTerm, totalOutstandingInterestPaymentDueToGrace, outstandingBalance, loanApplicationTerms, periodNumber, mathContext, - null, null, installmentFromDate, adjustedInstallmentDueDate, daysInInstallment, applicableVariations); + null, null, installmentFromDate, adjustedInstallmentDueDate, applicableVariations); // update the interest due for the period period.updateInterestDue(principalInterestForThisPeriod.interest()); @@ -1622,7 +1794,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener interestCalculationGraceOnRepaymentPeriodFraction, totalCumulativePrincipal, totalCumulativeInterest, totalInterestChargedForFullLoanTerm, totalOutstandingInterestPaymentDueToGrace, outstandingBalance, loanApplicationTerms, periodNumber, mathContext, null, null, installmentFromDate, adjustedInstallmentDueDate, - daysInInstallment, applicableVariations); + applicableVariations); period.updatePrincipalDue(principalInterestForThisPeriod.principal()); @@ -1680,16 +1852,11 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener loanSummary.getTotalPenaltyChargesCharged(), totalRepaymentExpected.getAmount(), loanSummary.getTotalOutstanding()); } - protected double calculateInterestForDays(int daysInPeriodApplicableForInterest, BigDecimal interest, int days) { - if (interest.doubleValue() == 0 || days == 0) { return 0; } - return ((interest.doubleValue()) / daysInPeriodApplicableForInterest) * days; - } - public abstract PrincipalInterest calculatePrincipalInterestComponentsForPeriod(PaymentPeriodsInOneYearCalculator calculator, double interestCalculationGraceOnRepaymentPeriodFraction, Money totalCumulativePrincipal, Money totalCumulativeInterest, Money totalInterestDueForLoan, Money cumulatingInterestPaymentDueToGrace, Money outstandingBalance, LoanApplicationTerms loanApplicationTerms, int periodNumber, MathContext mc, TreeMap principalVariation, - Map compoundingMap, LocalDate periodStartDate, LocalDate periodEndDate, int daysForInterestInFullPeriod, + Map compoundingMap, LocalDate periodStartDate, LocalDate periodEndDate, Collection termVariations); protected final boolean isLastRepaymentPeriod(final int numberOfRepayments, final int periodNumber) { @@ -1756,15 +1923,14 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener private Money cumulativeFeeChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, final Set loanCharges, final MonetaryCurrency monetaryCurrency, final PrincipalInterest principalInterestForThisPeriod, - final Money principalDisbursed, final Money totalInterestChargedForFullLoanTerm, int numberOfRepayments, - boolean isInstallmentChargeApplicable) { + final Money principalDisbursed, final Money totalInterestChargedForFullLoanTerm, boolean isInstallmentChargeApplicable) { Money cumulative = Money.zero(monetaryCurrency); for (final LoanCharge loanCharge : loanCharges) { if (!loanCharge.isDueAtDisbursement() && loanCharge.isFeeCharge()) { if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) { - cumulative = calculateInstallmentCharge(principalInterestForThisPeriod, numberOfRepayments, cumulative, loanCharge); + cumulative = calculateInstallmentCharge(principalInterestForThisPeriod, cumulative, loanCharge); } else if (loanCharge.isOverdueInstallmentCharge() && loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd) && loanCharge.getChargeCalculation().isPercentageBased()) { @@ -1797,8 +1963,8 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener return cumulative; } - private Money calculateInstallmentCharge(final PrincipalInterest principalInterestForThisPeriod, int numberOfRepayments, - Money cumulative, final LoanCharge loanCharge) { + private Money calculateInstallmentCharge(final PrincipalInterest principalInterestForThisPeriod, Money cumulative, + final LoanCharge loanCharge) { if (loanCharge.getChargeCalculation().isPercentageBased()) { BigDecimal amount = BigDecimal.ZERO; if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) { @@ -1812,7 +1978,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener BigDecimal loanChargeAmt = amount.multiply(loanCharge.getPercentage()).divide(BigDecimal.valueOf(100)); cumulative = cumulative.plus(loanChargeAmt); } else { - cumulative = cumulative.plus(loanCharge.amount().divide(BigDecimal.valueOf(numberOfRepayments))); + cumulative = cumulative.plus(loanCharge.amountOrPercentage()); } return cumulative; } @@ -1820,14 +1986,14 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener private Money cumulativePenaltyChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, final Set loanCharges, final MonetaryCurrency monetaryCurrency, final PrincipalInterest principalInterestForThisPeriod, final Money principalDisbursed, - final Money totalInterestChargedForFullLoanTerm, int numberOfRepayments, boolean isInstallmentChargeApplicable) { + final Money totalInterestChargedForFullLoanTerm, boolean isInstallmentChargeApplicable) { Money cumulative = Money.zero(monetaryCurrency); for (final LoanCharge loanCharge : loanCharges) { if (loanCharge.isPenaltyCharge()) { if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) { - cumulative = calculateInstallmentCharge(principalInterestForThisPeriod, numberOfRepayments, cumulative, loanCharge); + cumulative = calculateInstallmentCharge(principalInterestForThisPeriod, cumulative, loanCharge); } else if (loanCharge.isOverdueInstallmentCharge() && loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd) && loanCharge.getChargeCalculation().isPercentageBased()) { @@ -1861,9 +2027,11 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener recalculationDetails.add(new RecalculationDetail(loanTransaction.getTransactionDate(), LoanTransaction .copyTransactionProperties(loanTransaction))); } + final boolean applyInterestRecalculation = loanApplicationTerms.isInterestRecalculationEnabled(); + // Fixed schedule End Date for generating schedule final LocalDate scheduleTillDate = null; - LoanScheduleRecalculationDTO loanScheduleRecalculationDTO = null; + LoanScheduleParams loanScheduleParams = null; Collection periods = new ArrayList<>(); final List retainedInstallments = new ArrayList<>(); @@ -1880,8 +2048,8 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener // this is required to update total fee amounts in the // LoanScheduleModel final BigDecimal chargesDueAtTimeOfDisbursement = deriveTotalChargesDueAtTimeOfDisbursement(loanCharges); - periods = createNewLoanScheduleListWithDisbursementDetails(loanApplicationTerms.getNumberOfRepayments(), loanApplicationTerms, - chargesDueAtTimeOfDisbursement); + periods = createNewLoanScheduleListWithDisbursementDetails(loanApplicationTerms.fetchNumberOfRepaymentsAfterExceptions(), + loanApplicationTerms, chargesDueAtTimeOfDisbursement); final List newRepaymentScheduleInstallments = new ArrayList<>(); MonetaryCurrency currency = outstandingBalance.getCurrency(); @@ -1928,7 +2096,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener LocalDate lastInstallmentDate = actualRepaymentDate; LocalDate periodStartDate = loanApplicationTerms.getExpectedDisbursementDate(); // Set fixed Amortization Amounts(either EMI or Principal ) - updateAmortization(mc, loanApplicationTerms, actualRepaymentDate, periodNumber, outstandingBalance, holidayDetailDTO); + updateAmortization(mc, loanApplicationTerms, periodNumber, outstandingBalance); final Map disburseDetailMap = new HashMap<>(); if (loanApplicationTerms.isMultiDisburseLoan()) { @@ -1941,6 +2109,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener outstandingBalanceAsPerRest = outstandingBalance; principalToBeScheduled = principalToBeScheduled.zero().plus(disburseAmt); } + int loanTermInDays = 0; // Block process the installment and creates the period if it falls // before reschedule from date @@ -1962,6 +2131,14 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener break; } periodNumber++; + // check for date changes + while (loanApplicationTerms.getLoanTermVariations().hasDueDateVariation(lastInstallmentDate)) { + LoanTermVariationsData variation = loanApplicationTerms.getLoanTermVariations().nextDueDateVariation(); + if (!variation.isSpecificToInstallment()) { + actualRepaymentDate = variation.getDateValue(); + } + variation.setProcessed(true); + } } for (Map.Entry disburseDetail : disburseDetailMap.entrySet()) { @@ -1992,11 +2169,12 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener totalFeeChargesCharged = totalFeeChargesCharged.plus(installment.getFeeChargesCharged(currency)); totalPenaltyChargesCharged = totalPenaltyChargesCharged.plus(installment.getPenaltyChargesCharged(currency)); instalmentNumber++; + loanTermInDays = Days.daysBetween(installment.getFromDate(), installment.getDueDate()).getDays(); // populates the collection with transactions till the due date // of // the period for interest recalculation enabled loans - Collection applicableTransactions = getApplicableTransactionsForPeriod(loanApplicationTerms, + Collection applicableTransactions = getApplicableTransactionsForPeriod(applyInterestRecalculation, installment.getDueDate(), recalculationDetails); // calculates the expected principal value for this repayment @@ -2027,7 +2205,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener period--; } reducePrincipal = fetchEarlyPaidAmount(installment.getPrincipal(currency), principalPortionCalculated, reducePrincipal, - loanApplicationTerms, totalCumulativePrincipal, period, mc, holidayDetailDTO); + loanApplicationTerms, totalCumulativePrincipal, period, mc); // Updates principal paid map with efective date for reducing // the amount from outstanding balance(interest calculation) LocalDate amountApplicableDate = getNextRestScheduleDate(installment.getDueDate().minusDays(1), loanApplicationTerms, @@ -2050,25 +2228,26 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener newRepaymentScheduleInstallments, true, lastRestDate, compoundingMap); // for partial schedule generation - if (!newRepaymentScheduleInstallments.isEmpty()) { - loanScheduleRecalculationDTO = LoanScheduleRecalculationDTO.createLoanScheduleDTOForPartialUpdate(periodNumber, - instalmentNumber, periodStartDate, actualRepaymentDate, totalCumulativePrincipal, totalCumulativeInterest, - totalFeeChargesCharged, totalPenaltyChargesCharged, totalRepaymentExpected, reducePrincipal, principalPortionMap, - latePaymentMap, compoundingMap, disburseDetailMap, principalToBeScheduled, outstandingBalance, - outstandingBalanceAsPerRest, newRepaymentScheduleInstallments, recalculationDetails, - loanRepaymentScheduleTransactionProcessor, scheduleTillDate); + if (!newRepaymentScheduleInstallments.isEmpty() && totalCumulativeInterest.isGreaterThanZero()) { + Money totalOutstandingInterestPaymentDueToGrace = Money.zero(currency); + loanScheduleParams = LoanScheduleParams.createLoanScheduleParamsForPartialUpdate(periodNumber, instalmentNumber, + loanTermInDays, periodStartDate, actualRepaymentDate, totalCumulativePrincipal, totalCumulativeInterest, + totalFeeChargesCharged, totalPenaltyChargesCharged, totalRepaymentExpected, + totalOutstandingInterestPaymentDueToGrace, reducePrincipal, principalPortionMap, latePaymentMap, compoundingMap, + disburseDetailMap, principalToBeScheduled, outstandingBalance, outstandingBalanceAsPerRest, + newRepaymentScheduleInstallments, recalculationDetails, loanRepaymentScheduleTransactionProcessor, + scheduleTillDate, currency, applyInterestRecalculation); retainedInstallments.addAll(newRepaymentScheduleInstallments); } } // for complete schedule generation - if (loanScheduleRecalculationDTO == null) { - loanScheduleRecalculationDTO = LoanScheduleRecalculationDTO.createLoanScheduleDTOForCompleteUpdate(recalculationDetails, - loanRepaymentScheduleTransactionProcessor, scheduleTillDate); + if (loanScheduleParams == null) { + loanScheduleParams = LoanScheduleParams.createLoanScheduleParamsForCompleteUpdate(recalculationDetails, + loanRepaymentScheduleTransactionProcessor, scheduleTillDate, applyInterestRecalculation); } - LoanScheduleModel loanScheduleModel = generate(mc, loanApplicationTerms, loanCharges, holidayDetailDTO, - loanScheduleRecalculationDTO); + LoanScheduleModel loanScheduleModel = generate(mc, loanApplicationTerms, loanCharges, holidayDetailDTO, loanScheduleParams); for (LoanScheduleModelPeriod loanScheduleModelPeriod : loanScheduleModel.getPeriods()) { if (loanScheduleModelPeriod.isRepaymentPeriod()) { // adding newly created repayment periods to installments @@ -2126,10 +2305,10 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener return unprocessed; } - private void updateAmortization(final MathContext mc, final LoanApplicationTerms loanApplicationTerms, LocalDate actualRepaymentDate, - int periodNumber, Money outstandingBalance, final HolidayDetailDTO holidayDetailDTO) { + private void updateAmortization(final MathContext mc, final LoanApplicationTerms loanApplicationTerms, int periodNumber, + Money outstandingBalance) { if (loanApplicationTerms.getAmortizationMethod().isEqualInstallment()) { - updateFixedInstallmentAmount(mc, loanApplicationTerms, actualRepaymentDate, periodNumber, outstandingBalance, holidayDetailDTO); + updateFixedInstallmentAmount(mc, loanApplicationTerms, periodNumber, outstandingBalance); } else { loanApplicationTerms.updateFixedPrincipalAmount(mc, periodNumber, outstandingBalance); } @@ -2140,8 +2319,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener * strategy */ private Money fetchEarlyPaidAmount(final Money principalPortion, final Money principalPortionCalculated, final Money reducePrincipal, - final LoanApplicationTerms applicationTerms, final Money totalCumulativePrincipal, int periodNumber, final MathContext mc, - final HolidayDetailDTO holidayDetailDTO) { + final LoanApplicationTerms applicationTerms, final Money totalCumulativePrincipal, int periodNumber, final MathContext mc) { Money existingEarlyPayment = reducePrincipal.minus(principalPortionCalculated); Money earlyPaidAmount = principalPortion.plus(existingEarlyPayment); if (existingEarlyPayment.isLessThanZero()) { @@ -2156,17 +2334,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener if (isEarlyPaid) { switch (applicationTerms.getRescheduleStrategyMethod()) { case REDUCE_EMI_AMOUNT: - // in this case emi amount will be reduced but number of - // installments won't change - Money principal = getPrincipalToBeScheduled(applicationTerms); - if (applicationTerms.getActualFixedEmiAmount() == null) { - applicationTerms.setFixedEmiAmount(null); - updateFixedInstallmentAmount(mc, applicationTerms, applicationTerms.getExpectedDisbursementDate(), periodNumber, - principal.minus(totalCumulativePrincipal), holidayDetailDTO); - } - if (applicationTerms.getAmortizationMethod().isEqualPrincipal()) { - applicationTerms.updateFixedPrincipalAmount(mc, periodNumber, principal.minus(totalCumulativePrincipal)); - } + adjustInstallmentOrPrincipalAmount(applicationTerms, totalCumulativePrincipal, periodNumber, mc); earlyPaidAmount = earlyPaidAmount.zero(); break; case REDUCE_NUMBER_OF_INSTALLMENTS: @@ -2278,8 +2446,9 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener recalculationDetails.add(new RecalculationDetail(loanTransaction.getTransactionDate(), LoanTransaction .copyTransactionProperties(loanTransaction))); } - LoanScheduleRecalculationDTO loanScheduleRecalculationDTO = LoanScheduleRecalculationDTO.createLoanScheduleDTOForCompleteUpdate( - recalculationDetails, loanRepaymentScheduleTransactionProcessor, calculateTill); + final boolean applyInterestRecalculation = loanApplicationTerms.isInterestRecalculationEnabled(); + LoanScheduleParams loanScheduleRecalculationDTO = LoanScheduleParams.createLoanScheduleParamsForCompleteUpdate( + recalculationDetails, loanRepaymentScheduleTransactionProcessor, calculateTill, applyInterestRecalculation); LoanScheduleModel loanScheduleModel = generate(mc, loanApplicationTerms, charges, holidayDetailDTO, loanScheduleRecalculationDTO); List installments = fetchInstallmentsFromScheduleModel(loanScheduleModel); loanRepaymentScheduleTransactionProcessor.handleTransaction(loanApplicationTerms.getExpectedDisbursementDate(), loanTransactions, @@ -2310,4 +2479,157 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener return (value != null) ? value : 0; } + + private final class LoanTermVariationParams { + + private final boolean skipPeriod; + private final boolean recalculateAmounts; + private final LocalDate scheduledDueDate; + + public LoanTermVariationParams(final boolean skipPeriod, final boolean recalculateAmounts, final LocalDate scheduledDueDate) { + this.skipPeriod = skipPeriod; + this.recalculateAmounts = recalculateAmounts; + this.scheduledDueDate = scheduledDueDate; + } + + public boolean isSkipPeriod() { + return this.skipPeriod; + } + + public boolean isRecalculateAmounts() { + return this.recalculateAmounts; + } + + public LocalDate getScheduledDueDate() { + return this.scheduledDueDate; + } + + } + + private final class ScheduleCurrentPeriodParams { + + Money earlyPaidAmount; + LoanScheduleModelPeriod lastInstallment; + boolean skipCurrentLoop; + Money interestForThisPeriod; + Money principalForThisPeriod; + Money feeChargesForInstallment; + Money penaltyChargesForInstallment; + // for adjusting outstandingBalances + Money reducedBalance; + boolean isEmiAmountChanged; + double interestCalculationGraceOnRepaymentPeriodFraction; + + public ScheduleCurrentPeriodParams(final MonetaryCurrency currency, double interestCalculationGraceOnRepaymentPeriodFraction) { + this.earlyPaidAmount = Money.zero(currency); + this.lastInstallment = null; + this.skipCurrentLoop = false; + this.interestForThisPeriod = Money.zero(currency); + this.principalForThisPeriod = Money.zero(currency); + this.reducedBalance = Money.zero(currency); + this.feeChargesForInstallment = Money.zero(currency); + this.penaltyChargesForInstallment = Money.zero(currency); + this.isEmiAmountChanged = false; + this.interestCalculationGraceOnRepaymentPeriodFraction = interestCalculationGraceOnRepaymentPeriodFraction; + } + + public Money getEarlyPaidAmount() { + return this.earlyPaidAmount; + } + + public void plusEarlyPaidAmount(Money earlyPaidAmount) { + this.earlyPaidAmount = this.earlyPaidAmount.plus(earlyPaidAmount); + } + + public void minusEarlyPaidAmount(Money earlyPaidAmount) { + this.earlyPaidAmount = this.earlyPaidAmount.minus(earlyPaidAmount); + } + + public LoanScheduleModelPeriod getLastInstallment() { + return this.lastInstallment; + } + + public void setLastInstallment(LoanScheduleModelPeriod lastInstallment) { + this.lastInstallment = lastInstallment; + } + + public boolean isSkipCurrentLoop() { + return this.skipCurrentLoop; + } + + public void setSkipCurrentLoop(boolean skipCurrentLoop) { + this.skipCurrentLoop = skipCurrentLoop; + } + + public Money getInterestForThisPeriod() { + return this.interestForThisPeriod; + } + + public void setInterestForThisPeriod(Money interestForThisPeriod) { + this.interestForThisPeriod = interestForThisPeriod; + } + + public void minusInterestForThisPeriod(Money interestForThisPeriod) { + this.interestForThisPeriod = this.interestForThisPeriod.minus(interestForThisPeriod); + } + + public Money getPrincipalForThisPeriod() { + return this.principalForThisPeriod; + } + + public void setPrincipalForThisPeriod(Money principalForThisPeriod) { + this.principalForThisPeriod = principalForThisPeriod; + } + + public void plusPrincipalForThisPeriod(Money principalForThisPeriod) { + this.principalForThisPeriod = this.principalForThisPeriod.plus(principalForThisPeriod); + } + + public void minusPrincipalForThisPeriod(Money principalForThisPeriod) { + this.principalForThisPeriod = this.principalForThisPeriod.minus(principalForThisPeriod); + } + + public Money getReducedBalance() { + return this.reducedBalance; + } + + public void setReducedBalance(Money reducedBalance) { + this.reducedBalance = reducedBalance; + } + + public Money getFeeChargesForInstallment() { + return this.feeChargesForInstallment; + } + + public void setFeeChargesForInstallment(Money feeChargesForInstallment) { + this.feeChargesForInstallment = feeChargesForInstallment; + } + + public Money getPenaltyChargesForInstallment() { + return this.penaltyChargesForInstallment; + } + + public void setPenaltyChargesForInstallment(Money penaltyChargesForInstallment) { + this.penaltyChargesForInstallment = penaltyChargesForInstallment; + } + + public Money fetchTotalAmountForPeriod() { + return this.principalForThisPeriod.plus(interestForThisPeriod).plus(feeChargesForInstallment) + .plus(penaltyChargesForInstallment); + } + + public boolean isEmiAmountChanged() { + return this.isEmiAmountChanged; + } + + public void setEmiAmountChanged(boolean isEmiAmountChanged) { + this.isEmiAmountChanged = isEmiAmountChanged; + } + + public double getInterestCalculationGraceOnRepaymentPeriodFraction() { + return this.interestCalculationGraceOnRepaymentPeriodFraction; + } + + } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DecliningBalanceInterestLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DecliningBalanceInterestLoanScheduleGenerator.java index f72b6e384..f1853fdca 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DecliningBalanceInterestLoanScheduleGenerator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DecliningBalanceInterestLoanScheduleGenerator.java @@ -53,8 +53,7 @@ public class DecliningBalanceInterestLoanScheduleGenerator extends AbstractLoanS @SuppressWarnings("unused") final Money totalInterestDueForLoan, final Money cumulatingInterestPaymentDueToGrace, final Money outstandingBalance, final LoanApplicationTerms loanApplicationTerms, final int periodNumber, final MathContext mc, final TreeMap principalVariation, final Map compoundingMap, - final LocalDate periodStartDate, final LocalDate periodEndDate, final int daysForInterestInFullPeriod, - final Collection termVariations) { + final LocalDate periodStartDate, final LocalDate periodEndDate, final Collection termVariations) { LocalDate interestStartDate = periodStartDate; Money interestForThisInstallment = totalCumulativePrincipal.zero(); @@ -62,16 +61,15 @@ public class DecliningBalanceInterestLoanScheduleGenerator extends AbstractLoanS Money compoundedInterest = totalCumulativePrincipal.zero(); Money balanceForInterestCalculation = outstandingBalance; Money cumulatingInterestDueToGrace = cumulatingInterestPaymentDueToGrace; - final int daysInPeriodApplicableForInterest = Days.daysBetween(periodStartDate, periodEndDate).getDays(); Map interestRates = new HashMap<>(termVariations.size()); for (LoanTermVariationsData loanTermVariation : termVariations) { if (loanTermVariation.getTermVariationType().isInterestRateVariation() - && loanTermVariation.isApplicable(periodStartDate, periodEndDate, periodNumber)) { + && loanTermVariation.isApplicable(periodStartDate, periodEndDate)) { LocalDate fromDate = loanTermVariation.getTermApplicableFrom(); if (fromDate == null) { fromDate = periodStartDate; } - interestRates.put(fromDate, loanTermVariation.getTermValue()); + interestRates.put(fromDate, loanTermVariation.getDecimalValue()); if (!principalVariation.containsKey(fromDate)) { principalVariation.put(fromDate, balanceForInterestCalculation.zero()); } @@ -93,18 +91,9 @@ public class DecliningBalanceInterestLoanScheduleGenerator extends AbstractLoanS if (interestForDays > 0) { final PrincipalInterest result = loanApplicationTerms.calculateTotalInterestForPeriod(calculator, interestCalculationGraceOnRepaymentPeriodFraction, periodNumber, mc, cumulatingInterestDueToGrace, - interestForDays, balanceForInterestCalculation); - if (loanApplicationTerms.getInterestCalculationPeriodMethod().isDaily()) { - interestForThisInstallment = interestForThisInstallment.plus(result.interest()); - cumulatingInterestDueToGrace = result.interestPaymentDueToGrace(); - } else { - interestForThisInstallment = interestForThisInstallment.plus(calculateInterestForDays( - daysForInterestInFullPeriod, result.interest().getAmount(), interestForDays)); - cumulatingInterestDueToGrace = cumulatingInterestDueToGrace.plus(calculateInterestForDays( - daysForInterestInFullPeriod, result.interestPaymentDueToGrace().minus(cumulatingInterestDueToGrace) - .getAmount(), interestForDays)); - } - + balanceForInterestCalculation, interestStartDate, principal.getKey()); + interestForThisInstallment = interestForThisInstallment.plus(result.interest()); + cumulatingInterestDueToGrace = result.interestPaymentDueToGrace(); interestStartDate = principal.getKey(); } Money compoundFee = totalCumulativePrincipal.zero(); @@ -136,20 +125,12 @@ public class DecliningBalanceInterestLoanScheduleGenerator extends AbstractLoanS clearMapDetails(periodEndDate, compoundingMap); } } - int interestForDays = Days.daysBetween(interestStartDate, periodEndDate).getDays(); final PrincipalInterest result = loanApplicationTerms.calculateTotalInterestForPeriod(calculator, - interestCalculationGraceOnRepaymentPeriodFraction, periodNumber, mc, cumulatingInterestDueToGrace, interestForDays, - balanceForInterestCalculation); - if (loanApplicationTerms.getInterestCalculationPeriodMethod().isDaily()) { - interestForThisInstallment = interestForThisInstallment.plus(result.interest()); - cumulatingInterestDueToGrace = result.interestPaymentDueToGrace(); - } else { - interestForThisInstallment = interestForThisInstallment.plus(calculateInterestForDays(daysForInterestInFullPeriod, result - .interest().getAmount(), interestForDays)); - cumulatingInterestDueToGrace = cumulatingInterestDueToGrace.plus(calculateInterestForDays(daysForInterestInFullPeriod, result - .interestPaymentDueToGrace().minus(cumulatingInterestDueToGrace).getAmount(), interestForDays)); - } + interestCalculationGraceOnRepaymentPeriodFraction, periodNumber, mc, cumulatingInterestDueToGrace, + balanceForInterestCalculation, interestStartDate, periodEndDate); + interestForThisInstallment = interestForThisInstallment.plus(result.interest()); + cumulatingInterestDueToGrace = result.interestPaymentDueToGrace(); Money interestForPeriod = interestForThisInstallment; if (interestForPeriod.isGreaterThanZero()) { @@ -157,8 +138,8 @@ public class DecliningBalanceInterestLoanScheduleGenerator extends AbstractLoanS } else { interestForPeriod = cumulatingInterestDueToGrace.minus(cumulatingInterestPaymentDueToGrace); } - Money principalForThisInstallment = loanApplicationTerms.calculateTotalPrincipalForPeriod(calculator, - daysInPeriodApplicableForInterest, outstandingBalance, periodNumber, mc, interestForPeriod); + Money principalForThisInstallment = loanApplicationTerms.calculateTotalPrincipalForPeriod(calculator, outstandingBalance, + periodNumber, mc, interestForPeriod); // update cumulative fields for principal & interest final Money interestBroughtFowardDueToGrace = cumulatingInterestDueToGrace; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultLoanScheduleGeneratorFactory.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultLoanScheduleGeneratorFactory.java index 875fdb0b6..485d80448 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultLoanScheduleGeneratorFactory.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultLoanScheduleGeneratorFactory.java @@ -29,4 +29,5 @@ public class DefaultLoanScheduleGeneratorFactory implements LoanScheduleGenerato return loanScheduleGenerator; } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java index db8278b73..0ca969940 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java @@ -14,15 +14,11 @@ import org.mifosplatform.organisation.holiday.service.HolidayUtil; import org.mifosplatform.organisation.workingdays.domain.RepaymentRescheduleType; import org.mifosplatform.organisation.workingdays.service.WorkingDaysUtil; import org.mifosplatform.portfolio.calendar.domain.Calendar; -import org.mifosplatform.portfolio.calendar.domain.CalendarHistory; -import org.mifosplatform.portfolio.calendar.domain.CalendarInstance; import org.mifosplatform.portfolio.calendar.service.CalendarUtils; import org.mifosplatform.portfolio.common.domain.DayOfWeekType; import org.mifosplatform.portfolio.common.domain.PeriodFrequencyType; import org.mifosplatform.portfolio.loanaccount.data.HolidayDetailDTO; -import java.util.Set; - public class DefaultScheduledDateGenerator implements ScheduledDateGenerator { @Override @@ -52,6 +48,8 @@ public class DefaultScheduledDateGenerator implements ScheduledDateGenerator { dueRepaymentPeriodDate = getRepaymentPeriodDate(loanApplicationTerms.getRepaymentPeriodFrequencyType(), loanApplicationTerms.getRepaymentEvery(), lastRepaymentDate, loanApplicationTerms.getNthDay(), loanApplicationTerms.getWeekDayType()); + dueRepaymentPeriodDate = CalendarUtils.adjustDate(dueRepaymentPeriodDate, loanApplicationTerms.getSeedDate(), + loanApplicationTerms.getRepaymentPeriodFrequencyType()); if (currentCalendar != null) { // If we have currentCalendar object, this means there is a // calendar associated with @@ -78,8 +76,8 @@ public class DefaultScheduledDateGenerator implements ScheduledDateGenerator { loanApplicationTerms.getRepaymentEvery(), adjustedDate, loanApplicationTerms.getNthDay(), loanApplicationTerms.getWeekDayType()); - final RepaymentRescheduleType rescheduleType = RepaymentRescheduleType - .fromInt(holidayDetailDTO.getWorkingDays().getRepaymentReschedulingType()); + final RepaymentRescheduleType rescheduleType = RepaymentRescheduleType.fromInt(holidayDetailDTO.getWorkingDays() + .getRepaymentReschedulingType()); /** * Fix for https://mifosforge.jira.com/browse/MIFOSX-1357 @@ -91,6 +89,8 @@ public class DefaultScheduledDateGenerator implements ScheduledDateGenerator { nextDueRepaymentPeriodDate = getRepaymentPeriodDate(loanApplicationTerms.getRepaymentPeriodFrequencyType(), loanApplicationTerms.getRepaymentEvery(), nextDueRepaymentPeriodDate, loanApplicationTerms.getNthDay(), loanApplicationTerms.getWeekDayType()); + nextDueRepaymentPeriodDate = CalendarUtils.adjustDate(nextDueRepaymentPeriodDate, loanApplicationTerms.getSeedDate(), + loanApplicationTerms.getRepaymentPeriodFrequencyType()); } adjustedDate = WorkingDaysUtil.getOffSetDateIfNonWorkingDay(adjustedDate, nextDueRepaymentPeriodDate, diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/FlatInterestLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/FlatInterestLoanScheduleGenerator.java index 38fc72dab..e96cda5d4 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/FlatInterestLoanScheduleGenerator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/FlatInterestLoanScheduleGenerator.java @@ -10,7 +10,6 @@ import java.util.Collection; import java.util.Map; import java.util.TreeMap; -import org.joda.time.Days; import org.joda.time.LocalDate; import org.mifosplatform.organisation.monetary.domain.Money; import org.mifosplatform.portfolio.loanaccount.data.LoanTermVariationsData; @@ -24,14 +23,13 @@ public class FlatInterestLoanScheduleGenerator extends AbstractLoanScheduleGener final Money outstandingBalance, final LoanApplicationTerms loanApplicationTerms, final int periodNumber, final MathContext mc, @SuppressWarnings("unused") TreeMap principalVariation, @SuppressWarnings("unused") Map compoundingMap, LocalDate periodStartDate, LocalDate periodEndDate, - @SuppressWarnings("unused") int daysForInterestInFullPeriod, @SuppressWarnings("unused") Collection termVariations) { - final int daysInPeriodApplicableForInterest = Days.daysBetween(periodStartDate, periodEndDate).getDays(); - Money principalForThisInstallment = loanApplicationTerms.calculateTotalPrincipalForPeriod(calculator, - daysInPeriodApplicableForInterest, outstandingBalance, periodNumber, mc, null); + @SuppressWarnings("unused") Collection termVariations) { + Money principalForThisInstallment = loanApplicationTerms.calculateTotalPrincipalForPeriod(calculator, outstandingBalance, + periodNumber, mc, null); final PrincipalInterest result = loanApplicationTerms.calculateTotalInterestForPeriod(calculator, interestCalculationGraceOnRepaymentPeriodFraction, periodNumber, mc, cumulatingInterestPaymentDueToGrace, - daysInPeriodApplicableForInterest, outstandingBalance); + outstandingBalance, periodStartDate, periodEndDate); Money interestForThisInstallment = result.interest(); // update cumulative fields for principal & interest diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java index 0efc0e393..3e5b9e9a8 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java @@ -7,14 +7,16 @@ package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; import java.math.BigDecimal; import java.math.MathContext; -import java.util.Collection; import java.util.Date; import java.util.List; import org.joda.time.Days; import org.joda.time.LocalDate; +import org.joda.time.Months; import org.joda.time.Period; import org.joda.time.PeriodType; +import org.joda.time.Weeks; +import org.joda.time.Years; import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; import org.mifosplatform.organisation.monetary.domain.Money; @@ -28,6 +30,7 @@ import org.mifosplatform.portfolio.common.domain.NthDayType; import org.mifosplatform.portfolio.common.domain.PeriodFrequencyType; import org.mifosplatform.portfolio.loanaccount.data.DisbursementData; import org.mifosplatform.portfolio.loanaccount.data.LoanTermVariationsData; +import org.mifosplatform.portfolio.loanaccount.data.LoanTermVariationsDataWrapper; import org.mifosplatform.portfolio.loanaccount.domain.LoanInterestRecalculationDetails; import org.mifosplatform.portfolio.loanproduct.domain.AmortizationMethod; import org.mifosplatform.portfolio.loanproduct.domain.InterestCalculationPeriodMethod; @@ -46,6 +49,7 @@ public final class LoanApplicationTerms { private Integer loanTermFrequency; private final PeriodFrequencyType loanTermPeriodFrequencyType; private Integer numberOfRepayments; + private Integer actualNumberOfRepayments; private final Integer repaymentEvery; private final PeriodFrequencyType repaymentPeriodFrequencyType; private final Integer nthDay; @@ -119,12 +123,14 @@ public final class LoanApplicationTerms { private BigDecimal fixedPrincipalAmount; + private BigDecimal currentPeriodFixedEmiAmount; + + private BigDecimal currentPeriodFixedPrincipalAmount; + private final BigDecimal actualFixedEmiAmount; private final BigDecimal maxOutstandingBalance; - private final List emiAmountVariations; - private Money totalInterestDue; private final DaysInMonthType daysInMonthType; @@ -152,7 +158,11 @@ public final class LoanApplicationTerms { private Money approvedPrincipal = null; - private final Collection loanTermVariations; + private final LoanTermVariationsDataWrapper variationsDataWrapper; + + private Money adjustPrincipalForFlatLoans; + + private final LocalDate seedDate; public static LoanApplicationTerms assembleFrom(final ApplicationCurrency currency, final Integer loanTermFrequency, final PeriodFrequencyType loanTermPeriodFrequencyType, final Integer numberOfRepayments, final Integer repaymentEvery, @@ -164,14 +174,13 @@ public final class LoanApplicationTerms { final LocalDate calculatedRepaymentsStartingFromDate, final Integer graceOnPrincipalPayment, final Integer graceOnInterestPayment, final Integer graceOnInterestCharged, final LocalDate interestChargedFromDate, final Money inArrearsTolerance, final boolean multiDisburseLoan, final BigDecimal emiAmount, - final List disbursementDatas, final BigDecimal maxOutstandingBalance, - final List emiAmountVariations, final Integer graceOnArrearsAgeing, + final List disbursementDatas, final BigDecimal maxOutstandingBalance, final Integer graceOnArrearsAgeing, final DaysInMonthType daysInMonthType, final DaysInYearType daysInYearType, final boolean isInterestRecalculationEnabled, final RecalculationFrequencyType recalculationFrequencyType, final CalendarInstance restCalendarInstance, final CalendarInstance compoundingCalendarInstance, final RecalculationFrequencyType compoundingFrequencyType, final BigDecimal principalThresholdForLastInstalment, final Integer installmentAmountInMultiplesOf, final LoanPreClosureInterestCalculationStrategy preClosureInterestCalculationStrategy, final Calendar loanCalendar, - BigDecimal approvedAmount, Collection loanTermVariations) { + BigDecimal approvedAmount, List loanTermVariations) { final LoanRescheduleStrategyMethod rescheduleStrategyMethod = null; final InterestRecalculationCompoundingMethod interestRecalculationCompoundingMethod = null; @@ -180,7 +189,7 @@ public final class LoanApplicationTerms { interestRatePeriodFrequencyType, annualNominalInterestRate, interestCalculationPeriodMethod, principalMoney, expectedDisbursementDate, repaymentsStartingFromDate, calculatedRepaymentsStartingFromDate, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, inArrearsTolerance, multiDisburseLoan, emiAmount, - disbursementDatas, maxOutstandingBalance, emiAmountVariations, graceOnArrearsAgeing, daysInMonthType, daysInYearType, + disbursementDatas, maxOutstandingBalance, graceOnArrearsAgeing, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, rescheduleStrategyMethod, interestRecalculationCompoundingMethod, restCalendarInstance, recalculationFrequencyType, compoundingCalendarInstance, compoundingFrequencyType, principalThresholdForLastInstalment, installmentAmountInMultiplesOf, preClosureInterestCalculationStrategy, loanCalendar, approvedAmount, loanTermVariations); @@ -192,23 +201,21 @@ public final class LoanApplicationTerms { final LocalDate calculatedRepaymentsStartingFromDate, final Money inArrearsTolerance, final LoanProductRelatedDetail loanProductRelatedDetail, final boolean multiDisburseLoan, final BigDecimal emiAmount, final List disbursementDatas, final BigDecimal maxOutstandingBalance, - final List emiAmountVariations, final LocalDate interestChargedFromDate, - final BigDecimal principalThresholdForLastInstalment, final Integer installmentAmountInMultiplesOf, - final RecalculationFrequencyType recalculationFrequencyType, final CalendarInstance restCalendarInstance, - final InterestRecalculationCompoundingMethod compoundingMethod, final CalendarInstance compoundingCalendarInstance, - final RecalculationFrequencyType compoundingFrequencyType, + final LocalDate interestChargedFromDate, final BigDecimal principalThresholdForLastInstalment, + final Integer installmentAmountInMultiplesOf, final RecalculationFrequencyType recalculationFrequencyType, + final CalendarInstance restCalendarInstance, final InterestRecalculationCompoundingMethod compoundingMethod, + final CalendarInstance compoundingCalendarInstance, final RecalculationFrequencyType compoundingFrequencyType, final LoanPreClosureInterestCalculationStrategy loanPreClosureInterestCalculationStrategy, final LoanRescheduleStrategyMethod rescheduleStrategyMethod, BigDecimal approvedAmount, BigDecimal annualNominalInterestRate, - Collection loanTermVariations) { + List loanTermVariations) { final Calendar loanCalendar = null; return assembleFrom(applicationCurrency, loanTermFrequency, loanTermPeriodFrequencyType, nthDay, dayOfWeek, expectedDisbursementDate, repaymentsStartingFromDate, calculatedRepaymentsStartingFromDate, inArrearsTolerance, - loanProductRelatedDetail, multiDisburseLoan, emiAmount, disbursementDatas, maxOutstandingBalance, emiAmountVariations, - interestChargedFromDate, principalThresholdForLastInstalment, installmentAmountInMultiplesOf, recalculationFrequencyType, - restCalendarInstance, compoundingMethod, compoundingCalendarInstance, compoundingFrequencyType, - loanPreClosureInterestCalculationStrategy, rescheduleStrategyMethod, loanCalendar, approvedAmount, - annualNominalInterestRate, loanTermVariations); + loanProductRelatedDetail, multiDisburseLoan, emiAmount, disbursementDatas, maxOutstandingBalance, interestChargedFromDate, + principalThresholdForLastInstalment, installmentAmountInMultiplesOf, recalculationFrequencyType, restCalendarInstance, + compoundingMethod, compoundingCalendarInstance, compoundingFrequencyType, loanPreClosureInterestCalculationStrategy, + rescheduleStrategyMethod, loanCalendar, approvedAmount, annualNominalInterestRate, loanTermVariations); } public static LoanApplicationTerms assembleFrom(final ApplicationCurrency applicationCurrency, final Integer loanTermFrequency, @@ -217,14 +224,13 @@ public final class LoanApplicationTerms { final LocalDate calculatedRepaymentsStartingFromDate, final Money inArrearsTolerance, final LoanProductRelatedDetail loanProductRelatedDetail, final boolean multiDisburseLoan, final BigDecimal emiAmount, final List disbursementDatas, final BigDecimal maxOutstandingBalance, - final List emiAmountVariations, final LocalDate interestChargedFromDate, - final BigDecimal principalThresholdForLastInstalment, final Integer installmentAmountInMultiplesOf, - final RecalculationFrequencyType recalculationFrequencyType, final CalendarInstance restCalendarInstance, - final InterestRecalculationCompoundingMethod compoundingMethod, final CalendarInstance compoundingCalendarInstance, - final RecalculationFrequencyType compoundingFrequencyType, + final LocalDate interestChargedFromDate, final BigDecimal principalThresholdForLastInstalment, + final Integer installmentAmountInMultiplesOf, final RecalculationFrequencyType recalculationFrequencyType, + final CalendarInstance restCalendarInstance, final InterestRecalculationCompoundingMethod compoundingMethod, + final CalendarInstance compoundingCalendarInstance, final RecalculationFrequencyType compoundingFrequencyType, final LoanPreClosureInterestCalculationStrategy loanPreClosureInterestCalculationStrategy, final LoanRescheduleStrategyMethod rescheduleStrategyMethod, final Calendar loanCalendar, BigDecimal approvedAmount, - BigDecimal annualNominalInterestRate, Collection loanTermVariations) { + BigDecimal annualNominalInterestRate, final List loanTermVariations) { final Integer numberOfRepayments = loanProductRelatedDetail.getNumberOfRepayments(); final Integer repaymentEvery = loanProductRelatedDetail.getRepayEvery(); @@ -251,11 +257,11 @@ public final class LoanApplicationTerms { interestRatePerPeriod, interestRatePeriodFrequencyType, annualNominalInterestRate, interestCalculationPeriodMethod, principalMoney, expectedDisbursementDate, repaymentsStartingFromDate, calculatedRepaymentsStartingFromDate, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, inArrearsTolerance, - multiDisburseLoan, emiAmount, disbursementDatas, maxOutstandingBalance, emiAmountVariations, - loanProductRelatedDetail.getGraceOnDueDate(), daysInMonthType, daysInYearType, isInterestRecalculationEnabled, - rescheduleStrategyMethod, compoundingMethod, restCalendarInstance, recalculationFrequencyType, compoundingCalendarInstance, - compoundingFrequencyType, principalThresholdForLastInstalment, installmentAmountInMultiplesOf, - loanPreClosureInterestCalculationStrategy, loanCalendar, approvedAmount, loanTermVariations); + multiDisburseLoan, emiAmount, disbursementDatas, maxOutstandingBalance, loanProductRelatedDetail.getGraceOnDueDate(), + daysInMonthType, daysInYearType, isInterestRecalculationEnabled, rescheduleStrategyMethod, compoundingMethod, + restCalendarInstance, recalculationFrequencyType, compoundingCalendarInstance, compoundingFrequencyType, + principalThresholdForLastInstalment, installmentAmountInMultiplesOf, loanPreClosureInterestCalculationStrategy, + loanCalendar, approvedAmount, loanTermVariations); } public static LoanApplicationTerms assembleFrom(final ApplicationCurrency applicationCurrency, final Integer loanTermFrequency, @@ -263,13 +269,12 @@ public final class LoanApplicationTerms { final LocalDate repaymentsStartingFromDate, final LocalDate calculatedRepaymentsStartingFromDate, final Money inArrearsTolerance, final LoanProductRelatedDetail loanProductRelatedDetail, final boolean multiDisburseLoan, final BigDecimal emiAmount, final List disbursementDatas, final BigDecimal maxOutstandingBalance, - final List emiAmountVariations, final LocalDate interestChargedFromDate, - final LoanInterestRecalculationDetails interestRecalculationDetails, final CalendarInstance restCalendarInstance, - final RecalculationFrequencyType recalculationFrequencyType, final CalendarInstance compoundingCalendarInstance, - final RecalculationFrequencyType compoundingFrequencyType, final BigDecimal principalThresholdForLastInstalment, - final Integer installmentAmountInMultiplesOf, + final LocalDate interestChargedFromDate, final LoanInterestRecalculationDetails interestRecalculationDetails, + final CalendarInstance restCalendarInstance, final RecalculationFrequencyType recalculationFrequencyType, + final CalendarInstance compoundingCalendarInstance, final RecalculationFrequencyType compoundingFrequencyType, + final BigDecimal principalThresholdForLastInstalment, final Integer installmentAmountInMultiplesOf, final LoanPreClosureInterestCalculationStrategy loanPreClosureInterestCalculationStrategy, BigDecimal approvedAmount, - final BigDecimal annualNominalInterestRate, final Collection loanTermVariations) { + final BigDecimal annualNominalInterestRate, final List loanTermVariations) { final Integer numberOfRepayments = loanProductRelatedDetail.getNumberOfRepayments(); final Integer repaymentEvery = loanProductRelatedDetail.getRepayEvery(); @@ -303,11 +308,31 @@ public final class LoanApplicationTerms { interestRatePeriodFrequencyType, annualNominalInterestRate, interestCalculationPeriodMethod, principalMoney, expectedDisbursementDate, repaymentsStartingFromDate, calculatedRepaymentsStartingFromDate, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, inArrearsTolerance, multiDisburseLoan, emiAmount, - disbursementDatas, maxOutstandingBalance, emiAmountVariations, loanProductRelatedDetail.getGraceOnDueDate(), - daysInMonthType, daysInYearType, isInterestRecalculationEnabled, rescheduleStrategyMethod, - interestRecalculationCompoundingMethod, restCalendarInstance, recalculationFrequencyType, compoundingCalendarInstance, - compoundingFrequencyType, principalThresholdForLastInstalment, installmentAmountInMultiplesOf, - loanPreClosureInterestCalculationStrategy, loanCalendar, approvedAmount, loanTermVariations); + disbursementDatas, maxOutstandingBalance, loanProductRelatedDetail.getGraceOnDueDate(), daysInMonthType, daysInYearType, + isInterestRecalculationEnabled, rescheduleStrategyMethod, interestRecalculationCompoundingMethod, restCalendarInstance, + recalculationFrequencyType, compoundingCalendarInstance, compoundingFrequencyType, principalThresholdForLastInstalment, + installmentAmountInMultiplesOf, loanPreClosureInterestCalculationStrategy, loanCalendar, approvedAmount, loanTermVariations); + } + + public static LoanApplicationTerms assembleFrom(final LoanApplicationTerms applicationTerms, + final List loanTermVariations) { + return new LoanApplicationTerms(applicationTerms.currency, applicationTerms.loanTermFrequency, + applicationTerms.loanTermPeriodFrequencyType, applicationTerms.numberOfRepayments, applicationTerms.repaymentEvery, + applicationTerms.repaymentPeriodFrequencyType, applicationTerms.nthDay, applicationTerms.weekDayType, + applicationTerms.amortizationMethod, applicationTerms.interestMethod, applicationTerms.interestRatePerPeriod, + applicationTerms.interestRatePeriodFrequencyType, applicationTerms.annualNominalInterestRate, + applicationTerms.interestCalculationPeriodMethod, applicationTerms.principal, applicationTerms.expectedDisbursementDate, + applicationTerms.repaymentsStartingFromDate, applicationTerms.calculatedRepaymentsStartingFromDate, + applicationTerms.principalGrace, applicationTerms.interestPaymentGrace, applicationTerms.interestChargingGrace, + applicationTerms.interestChargedFromDate, applicationTerms.inArrearsTolerance, applicationTerms.multiDisburseLoan, + applicationTerms.actualFixedEmiAmount, applicationTerms.disbursementDatas, applicationTerms.maxOutstandingBalance, + applicationTerms.graceOnArrearsAgeing, applicationTerms.daysInMonthType, applicationTerms.daysInYearType, + applicationTerms.interestRecalculationEnabled, applicationTerms.rescheduleStrategyMethod, + applicationTerms.interestRecalculationCompoundingMethod, applicationTerms.restCalendarInstance, + applicationTerms.recalculationFrequencyType, applicationTerms.compoundingCalendarInstance, + applicationTerms.compoundingFrequencyType, applicationTerms.principalThresholdForLastInstalment, + applicationTerms.installmentAmountInMultiplesOf, applicationTerms.preClosureInterestCalculationStrategy, + applicationTerms.loanCalendar, applicationTerms.approvedPrincipal.getAmount(), loanTermVariations); } private LoanApplicationTerms(final ApplicationCurrency currency, final Integer loanTermFrequency, @@ -320,15 +345,15 @@ public final class LoanApplicationTerms { final LocalDate calculatedRepaymentsStartingFromDate, final Integer principalGrace, final Integer interestPaymentGrace, final Integer interestChargingGrace, final LocalDate interestChargedFromDate, final Money inArrearsTolerance, final boolean multiDisburseLoan, final BigDecimal emiAmount, final List disbursementDatas, - final BigDecimal maxOutstandingBalance, final List emiAmountVariations, - final Integer graceOnArrearsAgeing, final DaysInMonthType daysInMonthType, final DaysInYearType daysInYearType, - final boolean isInterestRecalculationEnabled, final LoanRescheduleStrategyMethod rescheduleStrategyMethod, + final BigDecimal maxOutstandingBalance, final Integer graceOnArrearsAgeing, final DaysInMonthType daysInMonthType, + final DaysInYearType daysInYearType, final boolean isInterestRecalculationEnabled, + final LoanRescheduleStrategyMethod rescheduleStrategyMethod, final InterestRecalculationCompoundingMethod interestRecalculationCompoundingMethod, final CalendarInstance restCalendarInstance, final RecalculationFrequencyType recalculationFrequencyType, final CalendarInstance compoundingCalendarInstance, final RecalculationFrequencyType compoundingFrequencyType, final BigDecimal principalThresholdForLastInstalment, final Integer installmentAmountInMultiplesOf, final LoanPreClosureInterestCalculationStrategy preClosureInterestCalculationStrategy, final Calendar loanCalendar, - BigDecimal approvedAmount, Collection loanTermVariations) { + BigDecimal approvedAmount, List loanTermVariations) { this.currency = currency; this.loanTermFrequency = loanTermFrequency; this.loanTermPeriodFrequencyType = loanTermPeriodFrequencyType; @@ -361,7 +386,6 @@ public final class LoanApplicationTerms { this.actualFixedEmiAmount = emiAmount; this.disbursementDatas = disbursementDatas; this.maxOutstandingBalance = maxOutstandingBalance; - this.emiAmountVariations = emiAmountVariations; this.graceOnArrearsAgeing = graceOnArrearsAgeing; this.daysInMonthType = daysInMonthType; this.daysInYearType = daysInYearType; @@ -378,7 +402,14 @@ public final class LoanApplicationTerms { this.loanCalendar = loanCalendar; this.approvedPrincipal = Money.of(principal.getCurrency(), approvedAmount); - this.loanTermVariations = loanTermVariations; + this.variationsDataWrapper = new LoanTermVariationsDataWrapper(loanTermVariations); + this.actualNumberOfRepayments = numberOfRepayments + getLoanTermVariations().adjustNumberOfRepayments(); + this.adjustPrincipalForFlatLoans = principal.zero(); + if (this.calculatedRepaymentsStartingFromDate == null) { + this.seedDate = this.expectedDisbursementDate; + } else { + this.seedDate = this.calculatedRepaymentsStartingFromDate; + } } public Money adjustPrincipalIfLastRepaymentPeriod(final Money principalForPeriod, final Money totalCumulativePrincipalToDate, @@ -398,7 +429,7 @@ public final class LoanApplicationTerms { if (difference.isLessThan(principalThreshold)) { adjusted = principalForPeriod.plus(difference.abs()); } - } else if (isLastRepaymentPeriod(this.numberOfRepayments, periodNumber)) { + } else if (isLastRepaymentPeriod(this.actualNumberOfRepayments, periodNumber)) { final Money difference = totalCumulativePrincipalToDate.minus(this.principal); if (difference.isLessThanZero()) { @@ -421,7 +452,7 @@ public final class LoanApplicationTerms { // paid too much interest, subtract amount that overpays from // interest paid for period. adjusted = interestForThisPeriod.minus(totalInterestRemaining.abs()); - } else if (isLastRepaymentPeriod(this.numberOfRepayments, periodNumber)) { + } else if (isLastRepaymentPeriod(this.actualNumberOfRepayments, periodNumber)) { final Money interestDifference = totalCumulativeInterestToDate.minus(totalInterestDueForLoan); if (interestDifference.isLessThanZero()) { adjusted = interestForThisPeriod.plus(interestDifference.abs()); @@ -463,22 +494,19 @@ public final class LoanApplicationTerms { return totalInterestCharged; } - public Money calculateTotalPrincipalForPeriod(final PaymentPeriodsInOneYearCalculator calculator, final int daysInPeriod, - final Money outstandingBalance, final int periodNumber, final MathContext mc, Money interestForThisInstallment) { + public Money calculateTotalPrincipalForPeriod(final PaymentPeriodsInOneYearCalculator calculator, final Money outstandingBalance, + final int periodNumber, final MathContext mc, Money interestForThisInstallment) { Money principalForInstallment = this.principal.zero(); switch (this.interestMethod) { case FLAT: - principalForInstallment = calculateTotalPrincipalPerPeriodWithoutGrace(mc); - if (isPrincipalGraceApplicableForThisPeriod(periodNumber)) { - principalForInstallment = Money.zero(principalForInstallment.getCurrency()); - } + principalForInstallment = calculateTotalPrincipalPerPeriodWithoutGrace(mc, periodNumber); break; case DECLINING_BALANCE: switch (this.amortizationMethod) { case EQUAL_INSTALLMENTS: - Money totalPmtForThisInstallment = pmtForInstallment(calculator, daysInPeriod, outstandingBalance, periodNumber, mc); + Money totalPmtForThisInstallment = pmtForInstallment(calculator, outstandingBalance, periodNumber, mc); principalForInstallment = calculatePrincipalDueForInstallment(periodNumber, totalPmtForThisInstallment, interestForThisInstallment); break; @@ -496,22 +524,47 @@ public final class LoanApplicationTerms { return principalForInstallment; } - public Money pmtForInstallment(final PaymentPeriodsInOneYearCalculator calculator, final int daysInPeriod, - final Money outstandingBalance, final int periodNumber, final MathContext mc) { + public Money pmtForInstallment(final PaymentPeriodsInOneYearCalculator calculator, final Money outstandingBalance, + final int periodNumber, final MathContext mc) { + // Calculate exact period from disbursement date + final LocalDate periodStartDate = getExpectedDisbursementDate().withDayOfMonth(1); + final LocalDate periodEndDate = getPeriodEndDate(periodStartDate); // equal installments final int periodsElapsed = periodNumber - 1; // with periodic interest for default month and year for // equal installment - final BigDecimal periodicInterestRateForRepaymentPeriod = periodicInterestRate(calculator, mc, daysInPeriod, - DaysInMonthType.DAYS_30, DaysInYearType.DAYS_365); + final BigDecimal periodicInterestRateForRepaymentPeriod = periodicInterestRate(calculator, mc, DaysInMonthType.DAYS_30, + DaysInYearType.DAYS_365, periodStartDate, periodEndDate); Money totalPmtForThisInstallment = calculateTotalDueForEqualInstallmentRepaymentPeriod(periodicInterestRateForRepaymentPeriod, outstandingBalance, periodsElapsed); return totalPmtForThisInstallment; } + private LocalDate getPeriodEndDate(final LocalDate startDate) { + LocalDate dueRepaymentPeriodDate = startDate; + switch (this.repaymentPeriodFrequencyType) { + case DAYS: + dueRepaymentPeriodDate = startDate.plusDays(this.repaymentEvery); + break; + case WEEKS: + dueRepaymentPeriodDate = startDate.plusWeeks(this.repaymentEvery); + break; + case MONTHS: + dueRepaymentPeriodDate = startDate.plusMonths(this.repaymentEvery); + break; + case YEARS: + dueRepaymentPeriodDate = startDate.plusYears(this.repaymentEvery); + break; + case INVALID: + break; + } + return dueRepaymentPeriodDate; + } + public PrincipalInterest calculateTotalInterestForPeriod(final PaymentPeriodsInOneYearCalculator calculator, final double interestCalculationGraceOnRepaymentPeriodFraction, final int periodNumber, final MathContext mc, - final Money cumulatingInterestPaymentDueToGrace, final int daysInPeriodApplicableForInterest, final Money outstandingBalance) { + final Money cumulatingInterestPaymentDueToGrace, final Money outstandingBalance, final LocalDate periodStartDate, + final LocalDate periodEndDate) { Money interestForInstallment = this.principal.zero(); Money interestBroughtForwardDueToGrace = cumulatingInterestPaymentDueToGrace.copy(); @@ -541,11 +594,11 @@ public final class LoanApplicationTerms { case DECLINING_BALANCE: final Money interestForThisInstallmentBeforeGrace = calculateDecliningInterestDueForInstallmentBeforeApplyingGrace( - calculator, mc, daysInPeriodApplicableForInterest, outstandingBalance); + calculator, mc, outstandingBalance, periodStartDate, periodEndDate); final Money interestForThisInstallmentAfterGrace = calculateDecliningInterestDueForInstallmentAfterApplyingGrace( - calculator, interestCalculationGraceOnRepaymentPeriodFraction, mc, daysInPeriodApplicableForInterest, - outstandingBalance, periodNumber); + calculator, interestCalculationGraceOnRepaymentPeriodFraction, mc, outstandingBalance, periodNumber, + periodStartDate, periodEndDate); interestForInstallment = interestForThisInstallmentAfterGrace; if (interestForThisInstallmentAfterGrace.isGreaterThanZero()) { @@ -628,12 +681,45 @@ public final class LoanApplicationTerms { case INVALID: break; case SAME_AS_REPAYMENT_PERIOD: + LocalDate startDate = getExpectedDisbursementDate(); + periodsInLoanTerm = calculatePeriodsBetweenDates(startDate, this.loanEndDate); break; } return periodsInLoanTerm; } + public BigDecimal calculatePeriodsBetweenDates(final LocalDate startDate, final LocalDate endDate) { + BigDecimal numberOfPeriods = BigDecimal.ZERO; + switch (this.repaymentPeriodFrequencyType) { + case DAYS: + int numberOfDays = Days.daysBetween(startDate, endDate).getDays(); + numberOfPeriods = BigDecimal.valueOf((double) numberOfDays); + break; + case WEEKS: + int numberOfWeeks = Weeks.weeksBetween(startDate, endDate).getWeeks(); + int daysLeftAfterWeeks = Days.daysBetween(startDate.plusWeeks(numberOfWeeks), endDate).getDays(); + numberOfPeriods = numberOfPeriods.add(BigDecimal.valueOf(numberOfWeeks)).add( + BigDecimal.valueOf((double) daysLeftAfterWeeks / 7)); + break; + case MONTHS: + int numberOfMonths = Months.monthsBetween(startDate, endDate).getMonths(); + int daysLeftAfterMonths = Days.daysBetween(startDate.plusMonths(numberOfMonths), endDate).getDays(); + numberOfPeriods = numberOfPeriods.add(BigDecimal.valueOf(numberOfMonths)).add( + BigDecimal.valueOf((double) daysLeftAfterMonths / 30)); + break; + case YEARS: + int numberOfYears = Years.yearsBetween(startDate, endDate).getYears(); + int daysLeftAfterYears = Days.daysBetween(startDate.plusYears(numberOfYears), endDate).getDays(); + numberOfPeriods = numberOfPeriods.add(BigDecimal.valueOf(numberOfYears)).add( + BigDecimal.valueOf((double) daysLeftAfterYears / 365)); + break; + default: + break; + } + return numberOfPeriods; + } + public void updateLoanEndDate(final LocalDate loanEndDate) { this.loanEndDate = loanEndDate; } @@ -642,12 +728,23 @@ public final class LoanApplicationTerms { final Money totalInterestForLoanTerm = calculateTotalFlatInterestDueWithoutGrace(calculator, mc); - return totalInterestForLoanTerm.dividedBy(Long.valueOf(this.numberOfRepayments), mc.getRoundingMode()); + return totalInterestForLoanTerm.dividedBy(Long.valueOf(this.actualNumberOfRepayments), mc.getRoundingMode()); } - private Money calculateTotalPrincipalPerPeriodWithoutGrace(final MathContext mc) { + private Money calculateTotalPrincipalPerPeriodWithoutGrace(final MathContext mc, final int periodNumber) { final int totalRepaymentsWithCapitalPayment = calculateNumberOfRepaymentsWithPrincipalPayment(); - return this.principal.dividedBy(totalRepaymentsWithCapitalPayment, mc.getRoundingMode()); + Money principalPerPeriod = this.principal.dividedBy(totalRepaymentsWithCapitalPayment, mc.getRoundingMode()).plus( + this.adjustPrincipalForFlatLoans); + if (isPrincipalGraceApplicableForThisPeriod(periodNumber)) { + principalPerPeriod = principalPerPeriod.zero(); + } + if (!isPrincipalGraceApplicableForThisPeriod(periodNumber) && currentPeriodFixedPrincipalAmount != null) { + this.adjustPrincipalForFlatLoans = this.adjustPrincipalForFlatLoans.plus(principalPerPeriod.minus( + currentPeriodFixedPrincipalAmount).dividedBy(this.actualNumberOfRepayments - periodNumber, mc.getRoundingMode())); + principalPerPeriod = this.principal.zero().plus(currentPeriodFixedPrincipalAmount); + + } + return principalPerPeriod; } private PrincipalInterest calculateTotalFlatInterestForPeriod(final PaymentPeriodsInOneYearCalculator calculator, @@ -693,7 +790,7 @@ public final class LoanApplicationTerms { final Money totalInterestFree = interestPerGracePeriod.multipliedBy(getInterestChargingGrace()); final Money realTotalInterestForLoan = totalInterestForLoanTerm.minus(totalInterestFree); - final Integer interestPaymentDuePeriods = calculateNumberOfRepaymentPeriodsWhereInterestPaymentIsDue(this.numberOfRepayments); + final Integer interestPaymentDuePeriods = calculateNumberOfRepaymentPeriodsWhereInterestPaymentIsDue(this.actualNumberOfRepayments); interestForInstallment = realTotalInterestForLoan .dividedBy(BigDecimal.valueOf(interestPaymentDuePeriods), mc.getRoundingMode()); @@ -702,8 +799,8 @@ public final class LoanApplicationTerms { return interestForInstallment; } - private BigDecimal periodicInterestRate(final PaymentPeriodsInOneYearCalculator calculator, final MathContext mc, final int actualDays, - final DaysInMonthType daysInMonthType, final DaysInYearType daysInYearType) { + private BigDecimal periodicInterestRate(final PaymentPeriodsInOneYearCalculator calculator, final MathContext mc, + final DaysInMonthType daysInMonthType, final DaysInYearType daysInYearType, LocalDate periodStartDate, LocalDate periodEndDate) { final long loanTermPeriodsInOneYear = calculatePeriodsInOneYear(calculator); @@ -711,13 +808,13 @@ public final class LoanApplicationTerms { final BigDecimal loanTermPeriodsInYearBigDecimal = BigDecimal.valueOf(loanTermPeriodsInOneYear); BigDecimal periodicInterestRate = BigDecimal.ZERO; - + BigDecimal loanTermFrequencyBigDecimal = calculatePeriodsBetweenDates(periodStartDate, periodEndDate); switch (this.interestCalculationPeriodMethod) { case INVALID: break; case DAILY: // For daily work out number of days in the period - int numberOfDaysInPeriod = actualDays; + BigDecimal numberOfDaysInPeriod = BigDecimal.valueOf(Days.daysBetween(periodStartDate, periodEndDate).getDays()); final BigDecimal oneDayOfYearInterestRate = this.annualNominalInterestRate.divide(loanTermPeriodsInYearBigDecimal, mc) .divide(divisor, mc); @@ -726,38 +823,36 @@ public final class LoanApplicationTerms { case INVALID: break; case DAYS: - final BigDecimal loanTermFrequencyBigDecimal = BigDecimal.valueOf(numberOfDaysInPeriod); - periodicInterestRate = oneDayOfYearInterestRate.multiply(loanTermFrequencyBigDecimal, mc); + periodicInterestRate = oneDayOfYearInterestRate.multiply(numberOfDaysInPeriod, mc); break; case WEEKS: - periodicInterestRate = oneDayOfYearInterestRate.multiply(BigDecimal.valueOf(numberOfDaysInPeriod), mc); + periodicInterestRate = oneDayOfYearInterestRate.multiply(numberOfDaysInPeriod, mc); break; case MONTHS: if (daysInMonthType.isDaysInMonth_30()) { - numberOfDaysInPeriod = this.repaymentEvery * 30; + numberOfDaysInPeriod = loanTermFrequencyBigDecimal.multiply(BigDecimal.valueOf(30), mc); } - periodicInterestRate = oneDayOfYearInterestRate.multiply(BigDecimal.valueOf(numberOfDaysInPeriod), mc); + periodicInterestRate = oneDayOfYearInterestRate.multiply(numberOfDaysInPeriod, mc); break; case YEARS: switch (daysInYearType) { case DAYS_360: - numberOfDaysInPeriod = this.repaymentEvery * 360; + numberOfDaysInPeriod = loanTermFrequencyBigDecimal.multiply(BigDecimal.valueOf(360), mc); break; case DAYS_364: - numberOfDaysInPeriod = this.repaymentEvery * 364; + numberOfDaysInPeriod = loanTermFrequencyBigDecimal.multiply(BigDecimal.valueOf(364), mc); break; case DAYS_365: - numberOfDaysInPeriod = this.repaymentEvery * 365; + numberOfDaysInPeriod = loanTermFrequencyBigDecimal.multiply(BigDecimal.valueOf(365), mc); break; default: break; } - periodicInterestRate = oneDayOfYearInterestRate.multiply(BigDecimal.valueOf(numberOfDaysInPeriod), mc); + periodicInterestRate = oneDayOfYearInterestRate.multiply(numberOfDaysInPeriod, mc); break; } break; case SAME_AS_REPAYMENT_PERIOD: - final BigDecimal loanTermFrequencyBigDecimal = BigDecimal.valueOf(this.repaymentEvery); periodicInterestRate = this.annualNominalInterestRate.divide(loanTermPeriodsInYearBigDecimal, mc).divide(divisor, mc) .multiply(loanTermFrequencyBigDecimal); break; @@ -808,7 +903,7 @@ public final class LoanApplicationTerms { } private int calculateNumberOfRepaymentsWithPrincipalPayment() { - return this.numberOfRepayments - getPrincipalGrace(); + return this.actualNumberOfRepayments - getPrincipalGrace(); } private Integer calculateNumberOfRepaymentPeriodsWhereInterestPaymentIsDue(final Integer totalNumberOfRepaymentPeriods) { @@ -861,11 +956,11 @@ public final class LoanApplicationTerms { private double paymentPerPeriod(final BigDecimal periodicInterestRate, final Money balance, final int periodsElapsed) { - if (this.fixedEmiAmount == null) { + if (getFixedEmiAmount() == null) { final double futureValue = 0; final double principalDouble = balance.getAmount().multiply(BigDecimal.valueOf(-1)).doubleValue(); - final Integer periodsRemaining = this.numberOfRepayments - periodsElapsed; + final Integer periodsRemaining = this.actualNumberOfRepayments - periodsElapsed; double installmentAmount = FinanicalFunctions.pmt(periodicInterestRate.doubleValue(), periodsRemaining.doubleValue(), principalDouble, futureValue, false); @@ -875,26 +970,27 @@ public final class LoanApplicationTerms { } setFixedEmiAmount(BigDecimal.valueOf(installmentAmount)); } - return this.fixedEmiAmount.doubleValue(); + return getFixedEmiAmount().doubleValue(); } private Money calculateDecliningInterestDueForInstallmentBeforeApplyingGrace(final PaymentPeriodsInOneYearCalculator calculator, - final MathContext mc, final int daysInPeriod, final Money outstandingBalance) { + final MathContext mc, final Money outstandingBalance, LocalDate periodStartDate, LocalDate periodEndDate) { Money interestDue = Money.zero(outstandingBalance.getCurrency()); - final BigDecimal periodicInterestRate = periodicInterestRate(calculator, mc, daysInPeriod, this.daysInMonthType, - this.daysInYearType); + final BigDecimal periodicInterestRate = periodicInterestRate(calculator, mc, this.daysInMonthType, this.daysInYearType, + periodStartDate, periodEndDate); interestDue = outstandingBalance.multiplyRetainScale(periodicInterestRate, mc.getRoundingMode()); return interestDue; } private Money calculateDecliningInterestDueForInstallmentAfterApplyingGrace(final PaymentPeriodsInOneYearCalculator calculator, - final double interestCalculationGraceOnRepaymentPeriodFraction, final MathContext mc, final int daysInPeriod, - final Money outstandingBalance, final int periodNumber) { + final double interestCalculationGraceOnRepaymentPeriodFraction, final MathContext mc, final Money outstandingBalance, + final int periodNumber, LocalDate periodStartDate, LocalDate periodEndDate) { - Money interest = calculateDecliningInterestDueForInstallmentBeforeApplyingGrace(calculator, mc, daysInPeriod, outstandingBalance); + Money interest = calculateDecliningInterestDueForInstallmentBeforeApplyingGrace(calculator, mc, outstandingBalance, + periodStartDate, periodEndDate); if (isInterestPaymentGraceApplicableForThisPeriod(periodNumber)) { interest = interest.zero(); @@ -927,15 +1023,13 @@ public final class LoanApplicationTerms { } private Money calculateEqualPrincipalDueForInstallment(final MathContext mc, final int periodNumber) { - - final Integer numberOfPrincipalPaymentPeriods = calculateNumberOfPrincipalPaymentPeriods(this.numberOfRepayments); Money principal = this.principal; if (this.fixedPrincipalAmount == null) { + final Integer numberOfPrincipalPaymentPeriods = calculateNumberOfPrincipalPaymentPeriods(this.actualNumberOfRepayments); principal = this.principal.dividedBy(numberOfPrincipalPaymentPeriods, mc.getRoundingMode()); this.fixedPrincipalAmount = principal.getAmount(); - } else { - principal = Money.of(getCurrency(), this.fixedPrincipalAmount); } + principal = Money.of(getCurrency(), getFixedPrincipalAmount()); if (isPrincipalGraceApplicableForThisPeriod(periodNumber)) { principal = principal.zero(); @@ -944,11 +1038,15 @@ public final class LoanApplicationTerms { } public void updateFixedPrincipalAmount(final MathContext mc, final int periodNumber, final Money outstandingAmount) { - final Integer numberOfPrincipalPaymentPeriods = calculateNumberOfPrincipalPaymentPeriods(this.numberOfRepayments); + final Integer numberOfPrincipalPaymentPeriods = calculateNumberOfPrincipalPaymentPeriods(this.actualNumberOfRepayments); Money principal = outstandingAmount.dividedBy(numberOfPrincipalPaymentPeriods - periodNumber + 1, mc.getRoundingMode()); this.fixedPrincipalAmount = principal.getAmount(); } + public void setFixedPrincipalAmount(BigDecimal fixedPrincipalAmount) { + this.fixedPrincipalAmount = fixedPrincipalAmount; + } + private Money calculatePrincipalDueForInstallment(final int periodNumber, final Money totalDuePerInstallment, final Money periodInterest) { Money principal = totalDuePerInstallment.minus(periodInterest); @@ -1067,7 +1165,11 @@ public final class LoanApplicationTerms { } public BigDecimal getFixedEmiAmount() { - return this.fixedEmiAmount; + BigDecimal fixedEmiAmount = this.fixedEmiAmount; + if (getCurrentPeriodFixedEmiAmount() != null) { + fixedEmiAmount = getCurrentPeriodFixedEmiAmount(); + } + return fixedEmiAmount; } public Integer getNthDay() { @@ -1078,19 +1180,6 @@ public final class LoanApplicationTerms { return this.weekDayType; } - public void setFixedEmiAmountForPeriod(LocalDate periodDate) { - LocalDate startDate = expectedDisbursementDate; - for (LoanTermVariationsData loanVariationTermsData : this.emiAmountVariations) { - if (!periodDate.isBefore(loanVariationTermsData.getTermApplicableFrom()) - && !startDate.isAfter(loanVariationTermsData.getTermApplicableFrom())) { - if (loanVariationTermsData.getTermValue() != null) { - this.fixedEmiAmount = loanVariationTermsData.getTermValue(); - startDate = loanVariationTermsData.getTermApplicableFrom(); - } - } - } - } - public void setFixedEmiAmount(BigDecimal fixedEmiAmount) { this.fixedEmiAmount = fixedEmiAmount; } @@ -1185,6 +1274,8 @@ public final class LoanApplicationTerms { public void updateNumberOfRepayments(final Integer numberOfRepayments) { this.numberOfRepayments = numberOfRepayments; + this.actualNumberOfRepayments = numberOfRepayments + getLoanTermVariations().adjustNumberOfRepayments(); + } public void updatePrincipalGrace(final Integer principalGrace) { @@ -1259,11 +1350,39 @@ public final class LoanApplicationTerms { } public BigDecimal getFixedPrincipalAmount() { - return this.fixedPrincipalAmount; + BigDecimal fixedPrincipalAmount = this.fixedPrincipalAmount; + if (getCurrentPeriodFixedPrincipalAmount() != null) { + fixedPrincipalAmount = getCurrentPeriodFixedPrincipalAmount(); + } + return fixedPrincipalAmount; } - public Collection getLoanTermVariations() { - return this.loanTermVariations; + public LoanTermVariationsDataWrapper getLoanTermVariations() { + return this.variationsDataWrapper; + } + + public BigDecimal getCurrentPeriodFixedEmiAmount() { + return this.currentPeriodFixedEmiAmount; + } + + public void setCurrentPeriodFixedEmiAmount(BigDecimal currentPeriodFixedEmiAmount) { + this.currentPeriodFixedEmiAmount = currentPeriodFixedEmiAmount; + } + + public BigDecimal getCurrentPeriodFixedPrincipalAmount() { + return this.currentPeriodFixedPrincipalAmount; + } + + public void setCurrentPeriodFixedPrincipalAmount(BigDecimal currentPeriodFixedPrincipalAmount) { + this.currentPeriodFixedPrincipalAmount = currentPeriodFixedPrincipalAmount; + } + + public Integer fetchNumberOfRepaymentsAfterExceptions() { + return this.actualNumberOfRepayments; + } + + public LocalDate getSeedDate() { + return this.seedDate; } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/exception/ScheduleDateException.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/exception/ScheduleDateException.java new file mode 100644 index 000000000..fefc9b1de --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/exception/ScheduleDateException.java @@ -0,0 +1,16 @@ +/** + * 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.loanschedule.exception; + +import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class ScheduleDateException extends AbstractPlatformDomainRuleException { + + public ScheduleDateException(final String defaultUserMessage, final Object... defaultUserMessageArgs) { + super("error.msg.loanschedule.schedule.due.date.not.valid", defaultUserMessage, defaultUserMessageArgs); + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java index 1417af4c8..adf569835 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java @@ -10,15 +10,25 @@ import java.math.MathContext; import java.math.RoundingMode; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; +import java.util.TreeSet; import org.apache.commons.lang.StringUtils; +import org.joda.time.Days; import org.joda.time.LocalDate; import org.mifosplatform.infrastructure.configuration.domain.ConfigurationDomainService; +import org.mifosplatform.infrastructure.core.data.ApiParameterError; +import org.mifosplatform.infrastructure.core.data.DataValidatorBuilder; +import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException; import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper; import org.mifosplatform.infrastructure.core.service.DateUtils; +import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; import org.mifosplatform.organisation.holiday.domain.Holiday; import org.mifosplatform.organisation.holiday.domain.HolidayRepository; import org.mifosplatform.organisation.holiday.domain.HolidayStatusType; @@ -36,6 +46,7 @@ import org.mifosplatform.portfolio.calendar.domain.Calendar; import org.mifosplatform.portfolio.calendar.domain.CalendarEntityType; import org.mifosplatform.portfolio.calendar.domain.CalendarFrequencyType; import org.mifosplatform.portfolio.calendar.domain.CalendarInstance; +import org.mifosplatform.portfolio.calendar.domain.CalendarInstanceRepository; import org.mifosplatform.portfolio.calendar.domain.CalendarRepository; import org.mifosplatform.portfolio.calendar.domain.CalendarType; import org.mifosplatform.portfolio.calendar.exception.CalendarNotFoundException; @@ -57,10 +68,14 @@ import org.mifosplatform.portfolio.loanaccount.api.LoanApiConstants; import org.mifosplatform.portfolio.loanaccount.data.DisbursementData; import org.mifosplatform.portfolio.loanaccount.data.HolidayDetailDTO; import org.mifosplatform.portfolio.loanaccount.data.LoanTermVariationsData; +import org.mifosplatform.portfolio.loanaccount.data.ScheduleGeneratorDTO; +import org.mifosplatform.portfolio.loanaccount.domain.Loan; import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; import org.mifosplatform.portfolio.loanaccount.domain.LoanDisbursementDetails; import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.mifosplatform.portfolio.loanaccount.domain.LoanTermVariationType; +import org.mifosplatform.portfolio.loanaccount.domain.LoanTermVariations; +import org.mifosplatform.portfolio.loanaccount.domain.LoanTermVariationsComparator; import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction; import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; import org.mifosplatform.portfolio.loanaccount.exception.LoanApplicationDateException; @@ -70,7 +85,9 @@ import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanApplicati import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; +import org.mifosplatform.portfolio.loanaccount.serialization.VariableLoanScheduleFromApiJsonValidator; import org.mifosplatform.portfolio.loanaccount.service.LoanChargeAssembler; +import org.mifosplatform.portfolio.loanaccount.service.LoanUtilService; import org.mifosplatform.portfolio.loanproduct.LoanProductConstants; import org.mifosplatform.portfolio.loanproduct.domain.AmortizationMethod; import org.mifosplatform.portfolio.loanproduct.domain.InterestCalculationPeriodMethod; @@ -80,9 +97,11 @@ import org.mifosplatform.portfolio.loanproduct.domain.LoanProduct; import org.mifosplatform.portfolio.loanproduct.domain.LoanProductInterestRecalculationDetails; import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRepository; +import org.mifosplatform.portfolio.loanproduct.domain.LoanProductVariableInstallmentConfig; import org.mifosplatform.portfolio.loanproduct.domain.RecalculationFrequencyType; import org.mifosplatform.portfolio.loanproduct.exception.LoanProductNotFoundException; import org.mifosplatform.portfolio.loanproduct.service.LoanEnumerations; +import org.mifosplatform.useradministration.domain.AppUser; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -106,6 +125,10 @@ public class LoanScheduleAssembler { private final GroupRepositoryWrapper groupRepository; private final WorkingDaysRepositoryWrapper workingDaysRepository; private final FloatingRatesReadPlatformService floatingRatesReadPlatformService; + private final VariableLoanScheduleFromApiJsonValidator variableLoanScheduleFromApiJsonValidator; + private final CalendarInstanceRepository calendarInstanceRepository; + private final PlatformSecurityContext context; + private final LoanUtilService loanUtilService; @Autowired public LoanScheduleAssembler(final FromJsonHelper fromApiJsonHelper, final LoanProductRepository loanProductRepository, @@ -115,7 +138,10 @@ public class LoanScheduleAssembler { final HolidayRepository holidayRepository, final ConfigurationDomainService configurationDomainService, final ClientRepositoryWrapper clientRepository, final GroupRepositoryWrapper groupRepository, final WorkingDaysRepositoryWrapper workingDaysRepository, - final FloatingRatesReadPlatformService floatingRatesReadPlatformService) { + final FloatingRatesReadPlatformService floatingRatesReadPlatformService, + final VariableLoanScheduleFromApiJsonValidator variableLoanScheduleFromApiJsonValidator, + final CalendarInstanceRepository calendarInstanceRepository, final PlatformSecurityContext context, + final LoanUtilService loanUtilService) { this.fromApiJsonHelper = fromApiJsonHelper; this.loanProductRepository = loanProductRepository; this.applicationCurrencyRepository = applicationCurrencyRepository; @@ -129,6 +155,10 @@ public class LoanScheduleAssembler { this.groupRepository = groupRepository; this.workingDaysRepository = workingDaysRepository; this.floatingRatesReadPlatformService = floatingRatesReadPlatformService; + this.variableLoanScheduleFromApiJsonValidator = variableLoanScheduleFromApiJsonValidator; + this.calendarInstanceRepository = calendarInstanceRepository; + this.context = context; + this.loanUtilService = loanUtilService; } public LoanApplicationTerms assembleLoanTerms(final JsonElement element) { @@ -256,11 +286,6 @@ public class LoanScheduleAssembler { final List disbursementDatas = fetchDisbursementData(element.getAsJsonObject()); - final List loanVariationTermsData = new ArrayList<>(); - LoanTermVariationsData data = new LoanTermVariationsData(null, - LoanEnumerations.loanvariationType(LoanTermVariationType.EMI_AMOUNT), expectedDisbursementDate, emiAmount); - loanVariationTermsData.add(data); - /** * Interest recalculation settings copy from product definition */ @@ -307,28 +332,29 @@ public class LoanScheduleAssembler { final Integer installmentAmountInMultiplesOf = loanProduct.getInstallmentAmountInMultiplesOf(); - Collection loanTermVariations = new ArrayList<>(); + List loanTermVariations = new ArrayList<>(); if (loanProduct.isLinkedToFloatingInterestRate()) { final BigDecimal interestRateDiff = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed( LoanApiConstants.interestRateDifferentialParameterName, element); final Boolean isFloatingInterestRate = this.fromApiJsonHelper.extractBooleanNamed( LoanApiConstants.isFloatingInterestRateParameterName, element); List baseLendingRatePeriods = null; - try{ - baseLendingRatePeriods = this.floatingRatesReadPlatformService.retrieveBaseLendingRate() - .getRatePeriods(); - }catch(final FloatingRateNotFoundException ex){ - // Do not do anything + try { + baseLendingRatePeriods = this.floatingRatesReadPlatformService.retrieveBaseLendingRate().getRatePeriods(); + } catch (final FloatingRateNotFoundException ex) { + // Do not do anything } FloatingRateDTO floatingRateDTO = new FloatingRateDTO(isFloatingInterestRate, expectedDisbursementDate, interestRateDiff, baseLendingRatePeriods); Collection applicableRates = loanProduct.fetchInterestRates(floatingRateDTO); LocalDate interestRateStartDate = DateUtils.getLocalDateOfTenant(); + final LocalDate dateValue = null; + final boolean isSpecificToInstallment = false; for (FloatingRatePeriodData periodData : applicableRates) { LoanTermVariationsData loanTermVariation = new LoanTermVariationsData( LoanEnumerations.loanvariationType(LoanTermVariationType.INTEREST_RATE), periodData.getFromDateAsLocalDate(), - periodData.getInterestRate()); + periodData.getInterestRate(), dateValue, isSpecificToInstallment); if (interestRateStartDate.isAfter(periodData.getFromDateAsLocalDate())) { interestRateStartDate = periodData.getFromDateAsLocalDate(); annualNominalInterestRate = periodData.getInterestRate(); @@ -342,11 +368,10 @@ public class LoanScheduleAssembler { interestRatePerPeriod, interestRatePeriodFrequencyType, annualNominalInterestRate, interestCalculationPeriodMethod, principalMoney, expectedDisbursementDate, repaymentsStartingFromDate, calculatedRepaymentsStartingFromDate, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, inArrearsToleranceMoney, - loanProduct.isMultiDisburseLoan(), emiAmount, disbursementDatas, maxOutstandingBalance, loanVariationTermsData, - graceOnArrearsAgeing, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, recalculationFrequencyType, - restCalendarInstance, compoundingCalendarInstance, compoundingFrequencyType, principalThresholdForLastInstalment, - installmentAmountInMultiplesOf, loanProduct.preCloseInterestCalculationStrategy(), calendar, BigDecimal.ZERO, - loanTermVariations); + loanProduct.isMultiDisburseLoan(), emiAmount, disbursementDatas, maxOutstandingBalance, graceOnArrearsAgeing, + daysInMonthType, daysInYearType, isInterestRecalculationEnabled, recalculationFrequencyType, restCalendarInstance, + compoundingCalendarInstance, compoundingFrequencyType, principalThresholdForLastInstalment, installmentAmountInMultiplesOf, + loanProduct.preCloseInterestCalculationStrategy(), calendar, BigDecimal.ZERO, loanTermVariations); } private CalendarInstance createInterestRecalculationCalendarInstance(final LocalDate calendarStartDate, @@ -534,6 +559,345 @@ public class LoanScheduleAssembler { } + public void assempleVariableScheduleFrom(final Loan loan, final String json) { + this.variableLoanScheduleFromApiJsonValidator.validateSchedule(json, loan); + + List variations = loan.getLoanTermVariations(); + List newVariations = new ArrayList<>(); + extractLoanTermVariations(loan, json, newVariations); + + final Map adjustDueDateVariations = new HashMap<>(); + + if (!variations.isEmpty()) { + List retainVariations = adjustExistingVariations(variations, newVariations, adjustDueDateVariations); + newVariations = retainVariations; + } + variations.addAll(newVariations); + Collections.sort(variations, new LoanTermVariationsComparator()); + + /* + * List loanTermVariationsDatas = new + * ArrayList<>(); + * loanTermVariationsDatas.addAll(loanApplicationTerms.getLoanTermVariations + * ().getExceptionData()); loanApplicationTerms = + * LoanApplicationTerms.assembleFrom(loanApplicationTerms, + * loanTermVariationsDatas); + */ + + // date validations + List installments = loan.fetchRepaymentScheduleInstallments(); + Set dueDates = new TreeSet<>(); + LocalDate graceApplicable = loan.getExpectedDisbursedOnLocalDate(); + Integer graceOnPrincipal = loan.getLoanProductRelatedDetail().graceOnPrincipalPayment(); + if (graceOnPrincipal == null) { + graceOnPrincipal = 0; + } + LocalDate lastDate = loan.getExpectedDisbursedOnLocalDate(); + for (LoanRepaymentScheduleInstallment installment : installments) { + dueDates.add(installment.getDueDate()); + if (lastDate.isBefore(installment.getDueDate())) { + lastDate = installment.getDueDate(); + } + if (graceOnPrincipal.equals(installment.getInstallmentNumber())) { + graceApplicable = installment.getDueDate(); + } + } + Collection keySet = adjustDueDateVariations.keySet(); + dueDates.addAll(keySet); + for (final LocalDate date : keySet) { + LocalDate removeDate = adjustDueDateVariations.get(date); + if (removeDate != null) { + dueDates.remove(removeDate); + } + } + + Set actualDueDates = new TreeSet<>(dueDates); + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan"); + List overlappings = new ArrayList<>(); + for (LoanTermVariations termVariations : variations) { + switch (termVariations.getTermType()) { + case INSERT_INSTALLMENT: + if (dueDates.contains(termVariations.fetchTermApplicaDate())) { + overlappings.add(termVariations.fetchTermApplicaDate()); + } else { + dueDates.add(termVariations.fetchTermApplicaDate()); + } + if (!graceApplicable.isBefore(termVariations.fetchTermApplicaDate())) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode( + "variable.schedule.insert.not.allowed.before.grace.period", "Loan schedule insert request invalid"); + } + if (termVariations.fetchTermApplicaDate().isAfter(lastDate)) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode( + "variable.schedule.insert.not.allowed.after.last.period.date", "Loan schedule insert request invalid"); + } else if(termVariations.fetchTermApplicaDate().isBefore(loan.getExpectedDisbursedOnLocalDate())){ + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode( + "variable.schedule.insert.not.allowed.before.disbursement.date", "Loan schedule insert request invalid"); + } + break; + case DELETE_INSTALLMENT: + if (dueDates.contains(termVariations.fetchTermApplicaDate())) { + dueDates.remove(termVariations.fetchTermApplicaDate()); + } else { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("variable.schedule.remove.date.invalid", + "Loan schedule remove request invalid"); + } + if (termVariations.fetchTermApplicaDate().isEqual(lastDate)) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode( + "variable.schedule.delete.not.allowed.for.last.period.date", "Loan schedule remove request invalid"); + } + break; + case DUE_DATE: + if (dueDates.contains(termVariations.fetchTermApplicaDate())) { + + if (overlappings.contains(termVariations.fetchTermApplicaDate())) { + overlappings.remove(termVariations.fetchTermApplicaDate()); + } else { + dueDates.remove(termVariations.fetchTermApplicaDate()); + } + } else { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("variable.schedule.modify.date.invalid", + "Loan schedule modify due date request invalid"); + } + if (dueDates.contains(termVariations.fetchDateValue())) { + overlappings.add(termVariations.fetchDateValue()); + } else { + dueDates.add(termVariations.fetchDateValue()); + } + if(termVariations.fetchDateValue().isBefore(loan.getExpectedDisbursedOnLocalDate())){ + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode( + "variable.schedule.insert.not.allowed.before.disbursement.date", "Loan schedule insert request invalid"); + } + if (termVariations.fetchTermApplicaDate().isEqual(lastDate)) { + lastDate = termVariations.fetchDateValue(); + } + break; + case PRINCIPAL_AMOUNT: + case EMI_AMOUNT: + if (!graceApplicable.isBefore(termVariations.fetchTermApplicaDate())) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode( + "variable.schedule.amount.update.not.allowed.before.grace.period", "Loan schedule modify request invalid"); + } + if (!dueDates.contains(termVariations.fetchTermApplicaDate())) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode( + "variable.schedule.amount.update.from.date.invalid", "Loan schedule modify request invalid"); + } + if (termVariations.fetchTermApplicaDate().isEqual(lastDate)) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode( + "variable.schedule.amount.update.not.allowed.for.last.period", "Loan schedule modify request invalid"); + } + break; + + default: + break; + + } + + } + if (!overlappings.isEmpty()) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("variable.schedule.modify.date.can.not.be.due.date", + overlappings); + } + LoanProductVariableInstallmentConfig installmentConfig = loan.loanProduct().loanProductVariableInstallmentConfig(); + final CalendarInstance loanCalendarInstance = calendarInstanceRepository.findCalendarInstaneByEntityId(loan.getId(), + CalendarEntityType.LOANS.getValue()); + Calendar loanCalendar = null; + if (loanCalendarInstance != null) { + loanCalendar = loanCalendarInstance.getCalendar(); + } + final Integer minGap = installmentConfig.getMinimumGap(); + final Integer maxGap = installmentConfig.getMaximumGap(); + + LocalDate previousDate = loan.getDisbursementDate(); + for (LocalDate duedate : dueDates) { + int gap = Days.daysBetween(previousDate, duedate).getDays(); + previousDate = duedate; + if (gap < minGap || (maxGap != null && gap > maxGap)) { + baseDataValidator.reset().value(duedate) + .failWithCodeNoParameterAddedToErrorCode("variable.schedule.date.must.be.in.min.max.range", "Loan schedule date invalid"); + } else if (loanCalendar != null && !actualDueDates.contains(duedate) && !loanCalendar.isValidRecurringDate(duedate)) { + baseDataValidator + .reset() + .value(duedate) + .failWithCodeNoParameterAddedToErrorCode("variable.schedule.date.not.meeting.date", + "Loan schedule date not in sync with meeting date"); + } + } + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } + if (loan.getExpectedFirstRepaymentOnDate() == null) { + loan.setExpectedFirstRepaymentOnDate(loan.fetchRepaymentScheduleInstallment(1).getDueDate().toDate()); + } + ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan); + AppUser currentUser = this.context.getAuthenticatedUserIfPresent(); + loan.regenerateRepaymentSchedule(scheduleGeneratorDTO, currentUser); + } + + private List adjustExistingVariations(List variations, List newVariations, + final Map adjustDueDateVariations) { + Map amountVariations = new HashMap<>(); + Map dueDateVariations = new HashMap<>(); + Map insertVariations = new HashMap<>(); + for (LoanTermVariations loanTermVariations : variations) { + switch (loanTermVariations.getTermType()) { + case EMI_AMOUNT: + case PRINCIPAL_AMOUNT: + amountVariations.put(loanTermVariations.fetchTermApplicaDate(), loanTermVariations); + break; + case DUE_DATE: + dueDateVariations.put(loanTermVariations.fetchDateValue(), loanTermVariations); + adjustDueDateVariations.put(loanTermVariations.fetchTermApplicaDate(), loanTermVariations.fetchDateValue()); + break; + case INSERT_INSTALLMENT: + insertVariations.put(loanTermVariations.fetchTermApplicaDate(), loanTermVariations); + adjustDueDateVariations.put(loanTermVariations.fetchTermApplicaDate(), loanTermVariations.fetchTermApplicaDate()); + break; + case DELETE_INSTALLMENT: + adjustDueDateVariations.put(loanTermVariations.fetchTermApplicaDate(), null); + break; + default: + break; + } + } + List retainVariations = new ArrayList<>(); + for (LoanTermVariations loanTermVariations : newVariations) { + boolean retain = true; + switch (loanTermVariations.getTermType()) { + case DUE_DATE: + if (amountVariations.containsKey(loanTermVariations.fetchTermApplicaDate())) { + amountVariations.get(loanTermVariations.fetchTermApplicaDate()).setTermApplicableFrom( + loanTermVariations.getDateValue()); + } else if (insertVariations.containsKey(loanTermVariations.fetchTermApplicaDate())) { + insertVariations.get(loanTermVariations.fetchTermApplicaDate()).setTermApplicableFrom( + loanTermVariations.getDateValue()); + retain = false; + } + if (dueDateVariations.containsKey(loanTermVariations.fetchTermApplicaDate())) { + LoanTermVariations existingVariation = dueDateVariations.get(loanTermVariations.fetchTermApplicaDate()); + if (existingVariation.fetchTermApplicaDate().isEqual(loanTermVariations.fetchDateValue())) { + variations.remove(existingVariation); + } else { + existingVariation.setTermApplicableFrom(loanTermVariations.getDateValue()); + } + retain = false; + } + break; + case EMI_AMOUNT: + case PRINCIPAL_AMOUNT: + if (amountVariations.containsKey(loanTermVariations.fetchTermApplicaDate())) { + amountVariations.get(loanTermVariations.fetchTermApplicaDate()).setDecimalValue(loanTermVariations.getTermValue()); + retain = false; + } else if (insertVariations.containsKey(loanTermVariations.fetchTermApplicaDate())) { + insertVariations.get(loanTermVariations.fetchTermApplicaDate()).setDecimalValue(loanTermVariations.getTermValue()); + retain = false; + } + break; + case DELETE_INSTALLMENT: + if (amountVariations.containsKey(loanTermVariations.fetchTermApplicaDate())) { + variations.remove(amountVariations.get(loanTermVariations.fetchTermApplicaDate())); + + } else if (insertVariations.containsKey(loanTermVariations.fetchTermApplicaDate())) { + variations.remove(insertVariations.get(loanTermVariations.fetchTermApplicaDate())); + retain = false; + } + if (dueDateVariations.containsKey(loanTermVariations.fetchTermApplicaDate())) { + variations.remove(amountVariations.get(loanTermVariations.fetchTermApplicaDate())); + } + break; + default: + break; + } + if (retain) { + retainVariations.add(loanTermVariations); + } + } + return retainVariations; + } + + private void extractLoanTermVariations(final Loan loan, final String json, final List loanTermVariations) { + final JsonElement element = this.fromApiJsonHelper.parse(json); + if (loan.loanProduct().allowVariabeInstallments()) { + if (element.isJsonObject() && this.fromApiJsonHelper.parameterExists(LoanApiConstants.exceptionParamName, element)) { + final JsonObject topLevelJsonElement = element.getAsJsonObject(); + final String dateFormat = this.fromApiJsonHelper.extractDateFormatParameter(topLevelJsonElement); + final Locale locale = this.fromApiJsonHelper.extractLocaleParameter(topLevelJsonElement); + final JsonObject exceptionObject = topLevelJsonElement.getAsJsonObject(LoanApiConstants.exceptionParamName); + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.modifiedinstallmentsParamName, exceptionObject) + && exceptionObject.get(LoanApiConstants.modifiedinstallmentsParamName).isJsonArray()) { + final JsonArray modificationsArray = exceptionObject.get(LoanApiConstants.modifiedinstallmentsParamName) + .getAsJsonArray(); + extractLoanTermVariations(loan, dateFormat, locale, modificationsArray, false, false, loanTermVariations); + } + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.newinstallmentsParamName, exceptionObject) + && exceptionObject.get(LoanApiConstants.newinstallmentsParamName).isJsonArray()) { + final JsonArray array = exceptionObject.get(LoanApiConstants.newinstallmentsParamName).getAsJsonArray(); + extractLoanTermVariations(loan, dateFormat, locale, array, true, false, loanTermVariations); + } + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.deletedinstallmentsParamName, exceptionObject) + && exceptionObject.get(LoanApiConstants.deletedinstallmentsParamName).isJsonArray()) { + final JsonArray array = exceptionObject.get(LoanApiConstants.deletedinstallmentsParamName).getAsJsonArray(); + extractLoanTermVariations(loan, dateFormat, locale, array, false, true, loanTermVariations); + } + } + } + } + + private void extractLoanTermVariations(final Loan loan, final String dateFormat, final Locale locale, + final JsonArray modificationsArray, final boolean isInsertInstallment, final boolean isDeleteInstallment, + final List loanTermVariations) { + for (int i = 1; i <= modificationsArray.size(); i++) { + final JsonObject arrayElement = modificationsArray.get(i - 1).getAsJsonObject(); + BigDecimal decimalValue = null; + LoanTermVariationType decimalValueVariationType = LoanTermVariationType.INVALID; + if (loan.getLoanProductRelatedDetail().getAmortizationMethod().isEqualInstallment() + && loan.getLoanProductRelatedDetail().getInterestMethod().isDecliningBalnce()) { + decimalValue = this.fromApiJsonHelper.extractBigDecimalNamed(LoanApiConstants.installmentAmountParamName, arrayElement, + locale); + decimalValueVariationType = LoanTermVariationType.EMI_AMOUNT; + } else { + decimalValue = this.fromApiJsonHelper.extractBigDecimalNamed(LoanApiConstants.principalParamName, arrayElement, locale); + decimalValueVariationType = LoanTermVariationType.PRINCIPAL_AMOUNT; + } + + LocalDate duedateLocalDate = this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.dueDateParamName, arrayElement, + dateFormat, locale); + Date dueDate = duedateLocalDate.toDate(); + + LocalDate modifiedDuedateLocalDate = this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.modifiedDueDateParamName, + arrayElement, dateFormat, locale); + Date modifiedDuedate = null; + if (modifiedDuedateLocalDate != null) { + modifiedDuedate = modifiedDuedateLocalDate.toDate(); + } + boolean isSpecificToInstallment = true; + if (isInsertInstallment) { + LoanTermVariations data = new LoanTermVariations(LoanTermVariationType.INSERT_INSTALLMENT.getValue(), dueDate, + decimalValue, modifiedDuedate, isSpecificToInstallment, loan); + loanTermVariations.add(data); + } else if (isDeleteInstallment) { + LoanTermVariations data = new LoanTermVariations(LoanTermVariationType.DELETE_INSTALLMENT.getValue(), dueDate, + decimalValue, modifiedDuedate, isSpecificToInstallment, loan); + loanTermVariations.add(data); + } else { + if (modifiedDuedate != null) { + BigDecimal amountData = null; + LoanTermVariations data = new LoanTermVariations(LoanTermVariationType.DUE_DATE.getValue(), dueDate, amountData, + modifiedDuedate, isSpecificToInstallment, loan); + loanTermVariations.add(data); + } + if (decimalValue != null) { + if (modifiedDuedate == null) { + modifiedDuedate = dueDate; + } + Date date = null; + LoanTermVariations data = new LoanTermVariations(decimalValueVariationType.getValue(), modifiedDuedate, decimalValue, + date, isSpecificToInstallment, loan); + loanTermVariations.add(data); + } + } + + } + } + private void validateDisbursementDateIsOnNonWorkingDay(final LocalDate disbursementDate, final WorkingDays workingDays) { if (!WorkingDaysUtil.isWorkingDay(workingDays, disbursementDate)) { final String errorMessage = "The expected disbursement date cannot be on a non working day"; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformService.java index d61ab58ad..1429b07c6 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformService.java @@ -14,4 +14,6 @@ public interface LoanScheduleCalculationPlatformService { LoanScheduleModel calculateLoanSchedule(JsonQuery query, Boolean validateParams); void updateFutureSchedule(LoanScheduleData loanScheduleData, Long loanId); + + LoanScheduleData generateLoanScheduleForVariableInstallmentRequest(Long loanId, String json); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformServiceImpl.java index d809a2d15..ec5da0eb6 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformServiceImpl.java @@ -8,7 +8,10 @@ package org.mifosplatform.portfolio.loanaccount.loanschedule.service; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; import java.util.List; +import java.util.Set; import org.joda.time.LocalDate; import org.mifosplatform.infrastructure.configuration.domain.ConfigurationDomainService; @@ -18,10 +21,12 @@ import org.mifosplatform.infrastructure.core.data.DataValidatorBuilder; import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException; import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper; import org.mifosplatform.infrastructure.core.service.DateUtils; +import org.mifosplatform.organisation.monetary.data.CurrencyData; import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; import org.mifosplatform.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; import org.mifosplatform.organisation.monetary.domain.Money; +import org.mifosplatform.organisation.monetary.service.CurrencyReadPlatformService; import org.mifosplatform.portfolio.accountdetails.domain.AccountType; import org.mifosplatform.portfolio.calendar.domain.CalendarEntityType; import org.mifosplatform.portfolio.calendar.domain.CalendarInstance; @@ -32,6 +37,7 @@ import org.mifosplatform.portfolio.floatingrates.exception.FloatingRateNotFoundE import org.mifosplatform.portfolio.floatingrates.service.FloatingRatesReadPlatformService; import org.mifosplatform.portfolio.loanaccount.domain.Loan; import org.mifosplatform.portfolio.loanaccount.domain.LoanAccountDomainService; +import org.mifosplatform.portfolio.loanaccount.domain.LoanDisbursementDetails; import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction; @@ -68,6 +74,7 @@ public class LoanScheduleCalculationPlatformServiceImpl implements LoanScheduleC private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory; private final ConfigurationDomainService configurationDomainService; private final FloatingRatesReadPlatformService floatingRatesReadPlatformService; + private final CurrencyReadPlatformService currencyReadPlatformService; @Autowired public LoanScheduleCalculationPlatformServiceImpl(final CalculateLoanScheduleQueryFromApiJsonHelper fromApiJsonDeserializer, @@ -78,7 +85,8 @@ public class LoanScheduleCalculationPlatformServiceImpl implements LoanScheduleC final LoanAccountDomainService accountDomainService, final CalendarInstanceRepository calendarInstanceRepository, final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory, final ConfigurationDomainService configurationDomainService, - final FloatingRatesReadPlatformService floatingRatesReadPlatformService) { + final FloatingRatesReadPlatformService floatingRatesReadPlatformService, + final CurrencyReadPlatformService currencyReadPlatformService) { this.fromApiJsonDeserializer = fromApiJsonDeserializer; this.loanScheduleAssembler = loanScheduleAssembler; this.fromJsonHelper = fromJsonHelper; @@ -93,6 +101,7 @@ public class LoanScheduleCalculationPlatformServiceImpl implements LoanScheduleC this.loanRepaymentScheduleTransactionProcessorFactory = loanRepaymentScheduleTransactionProcessorFactory; this.configurationDomainService = configurationDomainService; this.floatingRatesReadPlatformService = floatingRatesReadPlatformService; + this.currencyReadPlatformService = currencyReadPlatformService; } @Override @@ -162,18 +171,7 @@ public class LoanScheduleCalculationPlatformServiceImpl implements LoanScheduleC } } } - final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency); - final CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByEntityId(loan.getId(), - CalendarEntityType.LOANS.getValue()); - final CalendarInstance restCalendarInstance = calendarInstanceRepository.findCalendarInstaneByEntityId( - loan.loanInterestRecalculationDetailId(), CalendarEntityType.LOAN_RECALCULATION_REST_DETAIL.getValue()); - final CalendarInstance compoundingCalendarInstance = calendarInstanceRepository.findCalendarInstaneByEntityId( - loan.loanInterestRecalculationDetailId(), CalendarEntityType.LOAN_RECALCULATION_COMPOUNDING_DETAIL.getValue()); - LocalDate calculatedRepaymentsStartingFromDate = accountDomainService.getCalculatedRepaymentsStartingFromDate( - loan.getDisbursementDate(), loan, calendarInstance); - FloatingRateDTO floatingRateDTO = constructFloatingRateDTO(loan); - LoanApplicationTerms loanApplicationTerms = loan.constructLoanApplicationTerms(applicationCurrency, - calculatedRepaymentsStartingFromDate, restCalendarInstance, compoundingCalendarInstance, floatingRateDTO); + LoanApplicationTerms loanApplicationTerms = constructLoanApplicationTerms(loan); LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = this.loanScheduleAssembler.calculatePrepaymentAmount(currency, today, loanApplicationTerms, loan.charges(), loan.getOfficeId(), loan.retreiveListOfTransactionsPostDisbursementExcludeAccruals(), loanRepaymentScheduleTransactionProcessor); @@ -218,21 +216,99 @@ public class LoanScheduleCalculationPlatformServiceImpl implements LoanScheduleC loanScheduleData.updateFuturePeriods(futureInstallments); } + @Override + public LoanScheduleData generateLoanScheduleForVariableInstallmentRequest(Long loanId, final String json) { + final Loan loan = this.loanAssembler.assembleFrom(loanId); + this.loanScheduleAssembler.assempleVariableScheduleFrom(loan, json); + return constructLoanScheduleData(loan); + } + + private LoanScheduleData constructLoanScheduleData(Loan loan) { + Collection installments = loan.getRepaymentScheduleInstallments(); + final List installmentData = new ArrayList<>(); + final MonetaryCurrency currency = loan.getCurrency(); + Money outstanding = loan.getPrincpal(); + + Set disbursementDetails = new HashSet<>(); + if (loan.isMultiDisburmentLoan()) { + disbursementDetails = loan.getDisbursementDetails(); + outstanding = outstanding.zero(); + } + Money principal = outstanding; + Iterator disbursementItr = disbursementDetails.iterator(); + LoanDisbursementDetails loanDisbursementDetails = null; + if (disbursementItr.hasNext()) { + loanDisbursementDetails = disbursementItr.next(); + } + + Money totalInterest = principal.zero(); + Money totalCharge = principal.zero(); + Money totalPenalty = principal.zero(); + + for (LoanRepaymentScheduleInstallment installment : installments) { + if (loanDisbursementDetails != null + && !loanDisbursementDetails.expectedDisbursementDateAsLocalDate().isAfter(installment.getDueDate())) { + outstanding = outstanding.plus(loanDisbursementDetails.principal()); + principal = principal.plus(loanDisbursementDetails.principal()); + if (disbursementItr.hasNext()) { + loanDisbursementDetails = disbursementItr.next(); + } else { + loanDisbursementDetails = null; + } + } + outstanding = outstanding.minus(installment.getPrincipal(currency)); + LoanSchedulePeriodData loanSchedulePeriodData = LoanSchedulePeriodData.repaymentOnlyPeriod(installment.getInstallmentNumber(), + installment.getFromDate(), installment.getDueDate(), installment.getPrincipal(currency).getAmount(), + outstanding.getAmount(), installment.getInterestCharged(currency).getAmount(), + installment.getFeeChargesCharged(currency).getAmount(), installment.getPenaltyChargesCharged(currency).getAmount(), + installment.getDue(currency).getAmount()); + installmentData.add(loanSchedulePeriodData); + totalInterest = totalInterest.plus(installment.getInterestCharged(currency)); + totalCharge = totalCharge.plus(installment.getFeeChargesCharged(currency)); + totalPenalty = totalPenalty.plus(installment.getPenaltyChargesCharged(currency)); + } + + CurrencyData currencyData = this.currencyReadPlatformService.retrieveCurrency(currency.getCode()); + + LoanScheduleData scheduleData = new LoanScheduleData(currencyData, installmentData, loan.getLoanRepaymentScheduleDetail() + .getNumberOfRepayments(), principal.getAmount(), principal.getAmount(), totalInterest.getAmount(), totalCharge.getAmount(), + totalPenalty.getAmount(), principal.plus(totalCharge).plus(totalInterest).plus(totalPenalty).getAmount()); + + return scheduleData; + } + + private LoanApplicationTerms constructLoanApplicationTerms(final Loan loan) { + MonetaryCurrency currency = loan.getCurrency(); + final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency); + final CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByEntityId(loan.getId(), + CalendarEntityType.LOANS.getValue()); + final CalendarInstance restCalendarInstance = calendarInstanceRepository.findCalendarInstaneByEntityId( + loan.loanInterestRecalculationDetailId(), CalendarEntityType.LOAN_RECALCULATION_REST_DETAIL.getValue()); + final CalendarInstance compoundingCalendarInstance = calendarInstanceRepository.findCalendarInstaneByEntityId( + loan.loanInterestRecalculationDetailId(), CalendarEntityType.LOAN_RECALCULATION_COMPOUNDING_DETAIL.getValue()); + LocalDate calculatedRepaymentsStartingFromDate = accountDomainService.getCalculatedRepaymentsStartingFromDate( + loan.getDisbursementDate(), loan, calendarInstance); + FloatingRateDTO floatingRateDTO = constructFloatingRateDTO(loan); + LoanApplicationTerms loanApplicationTerms = loan.constructLoanApplicationTerms(applicationCurrency, + calculatedRepaymentsStartingFromDate, restCalendarInstance, compoundingCalendarInstance, floatingRateDTO); + return loanApplicationTerms; + } + private FloatingRateDTO constructFloatingRateDTO(final Loan loan) { FloatingRateDTO floatingRateDTO = null; if (loan.loanProduct().isLinkedToFloatingInterestRate()) { boolean isFloatingInterestRate = loan.getIsFloatingInterestRate(); BigDecimal interestRateDiff = loan.getInterestRateDifferential(); List baseLendingRatePeriods = null; - try{ - baseLendingRatePeriods = this.floatingRatesReadPlatformService.retrieveBaseLendingRate() - .getRatePeriods(); - }catch(final FloatingRateNotFoundException ex){ - // Do not do anything + try { + baseLendingRatePeriods = this.floatingRatesReadPlatformService.retrieveBaseLendingRate().getRatePeriods(); + } catch (final FloatingRateNotFoundException ex) { + // Do not do anything } floatingRateDTO = new FloatingRateDTO(isFloatingInterestRate, loan.getDisbursementDate(), interestRateDiff, baseLendingRatePeriods); } return floatingRateDTO; } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleWritePlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleWritePlatformService.java new file mode 100644 index 000000000..5caee0dfd --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleWritePlatformService.java @@ -0,0 +1,16 @@ +/** + * 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.loanschedule.service; + +import org.mifosplatform.infrastructure.core.api.JsonCommand; +import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; + +public interface LoanScheduleWritePlatformService { + + public CommandProcessingResult addLoanScheduleVariations(final Long loanId, final JsonCommand command); + + CommandProcessingResult deleteLoanScheduleVariations(final Long loanId); +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleWritePlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleWritePlatformServiceImpl.java new file mode 100644 index 000000000..43dacde27 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleWritePlatformServiceImpl.java @@ -0,0 +1,100 @@ +/** + * 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.loanschedule.service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.mifosplatform.infrastructure.core.api.JsonCommand; +import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; +import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder; +import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; +import org.mifosplatform.portfolio.loanaccount.data.LoanTermVariationsData; +import org.mifosplatform.portfolio.loanaccount.data.ScheduleGeneratorDTO; +import org.mifosplatform.portfolio.loanaccount.domain.Loan; +import org.mifosplatform.portfolio.loanaccount.domain.LoanAccountDomainService; +import org.mifosplatform.portfolio.loanaccount.domain.LoanTermVariations; +import org.mifosplatform.portfolio.loanaccount.service.LoanAssembler; +import org.mifosplatform.portfolio.loanaccount.service.LoanUtilService; +import org.mifosplatform.useradministration.domain.AppUser; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class LoanScheduleWritePlatformServiceImpl implements LoanScheduleWritePlatformService { + + private final LoanAccountDomainService loanAccountDomainService; + private final LoanAssembler loanAssembler; + private final LoanScheduleAssembler loanScheduleAssembler; + private final PlatformSecurityContext context; + private final LoanUtilService loanUtilService; + + @Autowired + public LoanScheduleWritePlatformServiceImpl(final LoanAccountDomainService loanAccountDomainService, + final LoanScheduleAssembler loanScheduleAssembler, final LoanAssembler loanAssembler, final PlatformSecurityContext context, + final LoanUtilService loanUtilService) { + this.loanAccountDomainService = loanAccountDomainService; + this.loanScheduleAssembler = loanScheduleAssembler; + this.loanAssembler = loanAssembler; + this.context = context; + this.loanUtilService = loanUtilService; + } + + @Override + public CommandProcessingResult addLoanScheduleVariations(final Long loanId, final JsonCommand command) { + final Loan loan = this.loanAssembler.assembleFrom(loanId); + Map loanTermVariations = new HashMap<>(); + for (LoanTermVariations termVariations : loan.getLoanTermVariations()) { + loanTermVariations.put(termVariations.getId(), termVariations); + } + this.loanScheduleAssembler.assempleVariableScheduleFrom(loan, command.json()); + + this.loanAccountDomainService.saveLoanWithDataIntegrityViolationChecks(loan); + final Map changes = new HashMap<>(); + List newVariationsData = new ArrayList<>(); + List modifiedVariations = loan.getLoanTermVariations(); + for (LoanTermVariations termVariations : modifiedVariations) { + if (loanTermVariations.containsKey(termVariations.getId())) { + loanTermVariations.remove(termVariations.getId()); + } else { + newVariationsData.add(termVariations.toData()); + } + } + if (!loanTermVariations.isEmpty()) { + changes.put("removedVariations", loanTermVariations.keySet()); + } + changes.put("loanTermVariations", newVariationsData); + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withLoanId(loanId) // + .with(changes) // + .build(); + } + + @Override + public CommandProcessingResult deleteLoanScheduleVariations(final Long loanId) { + final Loan loan = this.loanAssembler.assembleFrom(loanId); + List variations = loan.getLoanTermVariations(); + List deletedVariations = new ArrayList<>(variations.size()); + for (LoanTermVariations loanTermVariations : variations) { + deletedVariations.add(loanTermVariations.getId()); + } + final Map changes = new HashMap<>(); + changes.put("removedEntityIds", deletedVariations); + loan.getLoanTermVariations().clear(); + ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan); + AppUser currentUser = this.context.getAuthenticatedUserIfPresent(); + loan.regenerateRepaymentSchedule(scheduleGeneratorDTO, currentUser); + this.loanAccountDomainService.saveLoanWithDataIntegrityViolationChecks(loan); + return new CommandProcessingResultBuilder() // + .withLoanId(loanId) // + .with(changes) // + .build(); + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java index 8d64a9f0c..96b9f7b49 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java @@ -222,18 +222,22 @@ public final class LoanApplicationCommandFromApiJsonHelper { "interestRatePerPeriod param is not supported, selected Loan Product is linked with floating interest rate."); } - if(this.fromApiJsonHelper.parameterExists(LoanApiConstants.isFloatingInterestRate, element)){ - final Boolean isFloatingInterestRate = this.fromApiJsonHelper.extractBooleanNamed( - LoanApiConstants.isFloatingInterestRate, element); - if(isFloatingInterestRate != null && isFloatingInterestRate && !loanProduct.getFloatingRates().isFloatingInterestRateCalculationAllowed()){ - baseDataValidator.reset().parameter(LoanApiConstants.isFloatingInterestRate).failWithCode("true.not.supported.for.selected.loanproduct", - "isFloatingInterestRate value of true not supported for selected Loan Product."); + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.isFloatingInterestRate, element)) { + final Boolean isFloatingInterestRate = this.fromApiJsonHelper.extractBooleanNamed(LoanApiConstants.isFloatingInterestRate, + element); + if (isFloatingInterestRate != null && isFloatingInterestRate + && !loanProduct.getFloatingRates().isFloatingInterestRateCalculationAllowed()) { + baseDataValidator + .reset() + .parameter(LoanApiConstants.isFloatingInterestRate) + .failWithCode("true.not.supported.for.selected.loanproduct", + "isFloatingInterestRate value of true not supported for selected Loan Product."); } } else { baseDataValidator.reset().parameter(LoanApiConstants.isFloatingInterestRate).trueOrFalseRequired(false); } - if (interestType != null && interestType == InterestMethod.FLAT.getValue()) { + if (interestType != null && interestType.equals(InterestMethod.FLAT.getValue())) { baseDataValidator .reset() .parameter(interestTypeParameterName) @@ -595,15 +599,17 @@ public final class LoanApplicationCommandFromApiJsonHelper { } Boolean isFloatingInterestRate = existingLoanApplication.getIsFloatingInterestRate(); - if(this.fromApiJsonHelper.parameterExists(LoanApiConstants.isFloatingInterestRate, element)){ - isFloatingInterestRate = this.fromApiJsonHelper.extractBooleanNamed(LoanApiConstants.isFloatingInterestRate, element); + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.isFloatingInterestRate, element)) { + isFloatingInterestRate = this.fromApiJsonHelper.extractBooleanNamed(LoanApiConstants.isFloatingInterestRate, element); atLeastOneParameterPassedForUpdate = true; } - if(isFloatingInterestRate != null){ - if(isFloatingInterestRate - && !loanProduct.getFloatingRates().isFloatingInterestRateCalculationAllowed()){ - baseDataValidator.reset().parameter(LoanApiConstants.isFloatingInterestRate).failWithCode("true.not.supported.for.selected.loanproduct", - "isFloatingInterestRate value of true not supported for selected Loan Product."); + if (isFloatingInterestRate != null) { + if (isFloatingInterestRate && !loanProduct.getFloatingRates().isFloatingInterestRateCalculationAllowed()) { + baseDataValidator + .reset() + .parameter(LoanApiConstants.isFloatingInterestRate) + .failWithCode("true.not.supported.for.selected.loanproduct", + "isFloatingInterestRate value of true not supported for selected Loan Product."); } } else { baseDataValidator.reset().parameter(LoanApiConstants.isFloatingInterestRate).trueOrFalseRequired(false); @@ -612,7 +618,7 @@ public final class LoanApplicationCommandFromApiJsonHelper { if (interestType == null) { interestType = existingLoanApplication.getLoanProductRelatedDetail().getInterestMethod().getValue(); } - if (interestType != null && interestType == InterestMethod.FLAT.getValue()) { + if (interestType != null && interestType.equals(InterestMethod.FLAT.getValue())) { baseDataValidator .reset() .parameter(interestTypeParameterName) @@ -731,6 +737,10 @@ public final class LoanApplicationCommandFromApiJsonHelper { final LocalDate repaymentsStartingFromDate = this.fromApiJsonHelper.extractLocalDateNamed( repaymentsStartingFromDateParameterName, element); baseDataValidator.reset().parameter(repaymentsStartingFromDateParameterName).value(repaymentsStartingFromDate).ignoreIfNull(); + if (!existingLoanApplication.getLoanTermVariations().isEmpty()) { + baseDataValidator.reset().parameter(repaymentsStartingFromDateParameterName).value(repaymentsStartingFromDate) + .failWithCode("invalid.due.to.variable.installments"); + } } final String submittedOnDateParameterName = "submittedOnDate"; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/VariableLoanScheduleFromApiJsonValidator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/VariableLoanScheduleFromApiJsonValidator.java new file mode 100644 index 000000000..b2aaac5b6 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/VariableLoanScheduleFromApiJsonValidator.java @@ -0,0 +1,151 @@ +/** + * 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.serialization; + +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang.StringUtils; +import org.joda.time.LocalDate; +import org.mifosplatform.infrastructure.core.data.ApiParameterError; +import org.mifosplatform.infrastructure.core.data.DataValidatorBuilder; +import org.mifosplatform.infrastructure.core.exception.InvalidJsonException; +import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException; +import org.mifosplatform.infrastructure.core.exception.UnsupportedParameterException; +import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper; +import org.mifosplatform.portfolio.loanaccount.api.LoanApiConstants; +import org.mifosplatform.portfolio.loanaccount.domain.Loan; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.reflect.TypeToken; + +@Component +public class VariableLoanScheduleFromApiJsonValidator { + + final Set variableSchedulesupportedParameters = new HashSet<>(Arrays.asList(LoanApiConstants.exceptionParamName, + LoanApiConstants.localeParameterName, LoanApiConstants.dateFormatParameterName)); + final Set variableSchedulesupportedArrayParameters = new HashSet<>(Arrays.asList( + LoanApiConstants.modifiedinstallmentsParamName, LoanApiConstants.newinstallmentsParamName, + LoanApiConstants.deletedinstallmentsParamName)); + final Set variableScheduleModifiedParameters = new HashSet<>(Arrays.asList(LoanApiConstants.dueDateParamName, + LoanApiConstants.modifiedDueDateParamName, LoanApiConstants.principalParamName, LoanApiConstants.installmentAmountParamName)); + final Set variableScheduleNewInstallmentParameters = new HashSet<>(Arrays.asList(LoanApiConstants.dueDateParamName, + LoanApiConstants.principalParamName, LoanApiConstants.installmentAmountParamName)); + final Set variableScheduleDeleteInstallmentParameters = new HashSet<>(Arrays.asList(LoanApiConstants.dueDateParamName)); + + private final FromJsonHelper fromApiJsonHelper; + + @Autowired + public VariableLoanScheduleFromApiJsonValidator(final FromJsonHelper fromApiJsonHelper) { + this.fromApiJsonHelper = fromApiJsonHelper; + } + + public void validateSchedule(final String json, final Loan loan) { + if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); } + + final Type typeOfMap = new TypeToken>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, this.variableSchedulesupportedParameters); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan"); + + if (!loan.isSubmittedAndPendingApproval()) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("account.is.not.submitted.and.pending.state", + "Loan is not in submited state"); + throw new PlatformApiDataValidationException(dataValidationErrors); + } + + final JsonElement element = this.fromApiJsonHelper.parse(json); + if (loan.loanProduct().allowVariabeInstallments()) { + if (element.isJsonObject() && this.fromApiJsonHelper.parameterExists(LoanApiConstants.exceptionParamName, element)) { + final JsonObject topLevelJsonElement = element.getAsJsonObject(); + final String dateFormat = this.fromApiJsonHelper.extractDateFormatParameter(topLevelJsonElement); + final Locale locale = this.fromApiJsonHelper.extractLocaleParameter(topLevelJsonElement); + final JsonObject exceptionObject = topLevelJsonElement.getAsJsonObject(LoanApiConstants.exceptionParamName); + this.fromApiJsonHelper.checkForUnsupportedParameters(exceptionObject, this.variableSchedulesupportedArrayParameters); + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.modifiedinstallmentsParamName, exceptionObject) + && exceptionObject.get(LoanApiConstants.modifiedinstallmentsParamName).isJsonArray()) { + final JsonArray modificationsArray = exceptionObject.get(LoanApiConstants.modifiedinstallmentsParamName) + .getAsJsonArray(); + validateLoanTermVariations(loan, baseDataValidator, dateFormat, locale, modificationsArray, + this.variableScheduleModifiedParameters, LoanApiConstants.modifiedinstallmentsParamName); + } + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.newinstallmentsParamName, exceptionObject) + && exceptionObject.get(LoanApiConstants.newinstallmentsParamName).isJsonArray()) { + final JsonArray array = exceptionObject.get(LoanApiConstants.newinstallmentsParamName).getAsJsonArray(); + validateLoanTermVariations(loan, baseDataValidator, dateFormat, locale, array, + this.variableScheduleNewInstallmentParameters, LoanApiConstants.newinstallmentsParamName); + } + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.deletedinstallmentsParamName, exceptionObject) + && exceptionObject.get(LoanApiConstants.deletedinstallmentsParamName).isJsonArray()) { + final JsonArray array = exceptionObject.get(LoanApiConstants.deletedinstallmentsParamName).getAsJsonArray(); + validateLoanTermVariations(loan, baseDataValidator, dateFormat, locale, array, + this.variableScheduleDeleteInstallmentParameters, LoanApiConstants.deletedinstallmentsParamName); + } + } + } else { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("variable.schedule.not.supported", + "Loan schedule modification not allowed"); + } + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } + } + + private void validateLoanTermVariations(final Loan loan, final DataValidatorBuilder baseDataValidator, final String dateFormat, + final Locale locale, final JsonArray modificationsArray, final Set supportParams, final String arrayName) { + for (int i = 1; i <= modificationsArray.size(); i++) { + + final JsonObject arrayElement = modificationsArray.get(i - 1).getAsJsonObject(); + this.fromApiJsonHelper.checkForUnsupportedParameters(arrayElement, supportParams); + final BigDecimal installmentAmount = this.fromApiJsonHelper.extractBigDecimalNamed(LoanApiConstants.installmentAmountParamName, + arrayElement, locale); + baseDataValidator.reset().parameter(arrayName).parameterAtIndexArray(LoanApiConstants.installmentAmountParamName, i) + .value(installmentAmount).positiveAmount(); + final BigDecimal principalAmount = this.fromApiJsonHelper.extractBigDecimalNamed(LoanApiConstants.principalParamName, + arrayElement, locale); + baseDataValidator.reset().parameter(arrayName).parameterAtIndexArray(LoanApiConstants.principalParamName, i) + .value(principalAmount).zeroOrPositiveAmount(); + + if (loan.getLoanProductRelatedDetail().getInterestMethod().isDecliningBalnce() + && loan.getLoanProductRelatedDetail().getAmortizationMethod().isEqualInstallment() && principalAmount != null) { + List unsupportedParams = new ArrayList<>(1); + unsupportedParams.add(LoanApiConstants.principalParamName); + throw new UnsupportedParameterException(unsupportedParams); + } else if ((!loan.getLoanProductRelatedDetail().getInterestMethod().isDecliningBalnce() || loan.getLoanProductRelatedDetail() + .getAmortizationMethod().isEqualPrincipal()) + && installmentAmount != null) { + List unsupportedParams = new ArrayList<>(1); + unsupportedParams.add(LoanApiConstants.installmentAmountParamName); + throw new UnsupportedParameterException(unsupportedParams); + } + + LocalDate duedate = this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.dueDateParamName, arrayElement, dateFormat, + locale); + baseDataValidator.reset().parameter(arrayName).parameterAtIndexArray(LoanApiConstants.dueDateParamName, i).value(duedate) + .notNull().validateDateAfter(loan.getExpectedDisbursedOnLocalDate()); + + LocalDate modifiedDuedate = this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.modifiedDueDateParamName, + arrayElement, dateFormat, locale); + baseDataValidator.reset().parameter(arrayName).parameterAtIndexArray(LoanApiConstants.modifiedDueDateParamName, i) + .value(modifiedDuedate).validateDateAfter(loan.getExpectedDisbursedOnLocalDate()); + if (arrayName.equals(LoanApiConstants.modifiedinstallmentsParamName) && modifiedDuedate == null && installmentAmount == null + && principalAmount == null) { + baseDataValidator.reset().parameter(arrayName).failWithCode("variation.required", "At least one vario"); + } + + } + } +} 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 89fcb8f0f..228d78076 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 @@ -476,15 +476,15 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa loanId); } final String productIdParamName = "productId"; - LoanProduct newLoanProduct = null; + LoanProduct newLoanProduct = null; if (command.isChangeInLongParameterNamed(productIdParamName, existingLoanApplication.loanProduct().getId())) { final Long productId = command.longValueOfParameterNamed(productIdParamName); newLoanProduct = this.loanProductRepository.findOne(productId); if (newLoanProduct == null) { throw new LoanProductNotFoundException(productId); } } - - LoanProduct loanProductForValidations = newLoanProduct == null? existingLoanApplication.loanProduct(): newLoanProduct; - + + LoanProduct loanProductForValidations = newLoanProduct == null ? existingLoanApplication.loanProduct() : newLoanProduct; + this.fromApiJsonDeserializer.validateForModify(command.json(), loanProductForValidations, existingLoanApplication); checkClientOrGroupActive(existingLoanApplication); @@ -495,21 +495,23 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa LoanChargeData chargeData = new LoanChargeData(charge.getId(), charge.getDueLocalDate(), charge.amountOrPercentage()); chargesMap.put(charge.getId(), chargeData); } - Set disbursementDetails = this.loanAssembler.fetchDisbursementData(command.parsedJson().getAsJsonObject()); + Set disbursementDetails = this.loanAssembler.fetchDisbursementData(command.parsedJson() + .getAsJsonObject()); /** * Stores all charges which are passed in during modify loan * application **/ - final Set possiblyModifedLoanCharges = this.loanChargeAssembler.fromParsedJson(command.parsedJson(),disbursementDetails); + final Set possiblyModifedLoanCharges = this.loanChargeAssembler.fromParsedJson(command.parsedJson(), + disbursementDetails); /** Boolean determines if any charge has been modified **/ boolean isChargeModified = false; - Set newTrancheChages = this.loanChargeAssembler.getNewLoanTrancheCharges(command.parsedJson()) ; + Set newTrancheChages = this.loanChargeAssembler.getNewLoanTrancheCharges(command.parsedJson()); for (Charge charge : newTrancheChages) { - existingLoanApplication.addTrancheLoanCharge(charge) ; + existingLoanApplication.addTrancheLoanCharge(charge); } - + /** * If there are any charges already present, which are now not * passed in as a part of the request, deem the charges as modified @@ -579,19 +581,19 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa cycleNumber = this.loanReadPlatformService.retriveLoanCounter(clientId, newLoanProduct.getId()); } else if (groupId != null) { cycleNumber = this.loanReadPlatformService.retriveLoanCounter(groupId, AccountType.GROUP.getValue(), - newLoanProduct.getId()); + newLoanProduct.getId()); } this.loanProductCommandFromApiJsonDeserializer.validateMinMaxConstraints(command.parsedJson(), baseDataValidator, - newLoanProduct, cycleNumber); + newLoanProduct, cycleNumber); } else { this.loanProductCommandFromApiJsonDeserializer.validateMinMaxConstraints(command.parsedJson(), baseDataValidator, - newLoanProduct); + newLoanProduct); } - if(newLoanProduct.isLinkedToFloatingInterestRate()){ - existingLoanApplication.getLoanProductRelatedDetail().updateForFloatingInterestRates(); - }else{ - existingLoanApplication.setInterestRateDifferential(null); - existingLoanApplication.setIsFloatingInterestRate(null); + if (newLoanProduct.isLinkedToFloatingInterestRate()) { + existingLoanApplication.getLoanProductRelatedDetail().updateForFloatingInterestRates(); + } else { + existingLoanApplication.setInterestRateDifferential(null); + existingLoanApplication.setIsFloatingInterestRate(null); } if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } @@ -644,7 +646,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa if (changes.containsKey(chargesParamName)) { existingLoanApplication.updateLoanCharges(possiblyModifedLoanCharges); } - + if (changes.containsKey("recalculateLoanSchedule")) { changes.remove("recalculateLoanSchedule"); @@ -656,7 +658,6 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa existingLoanApplication.recalculateAllCharges(); } - this.fromApiJsonDeserializer.validateLoanTermAndRepaidEveryValues(existingLoanApplication.getTermFrequency(), existingLoanApplication.getTermPeriodFrequencyType(), productRelatedDetail.getNumberOfRepayments(), productRelatedDetail.getRepayEvery(), productRelatedDetail.getRepaymentPeriodFrequencyType().getValue(), @@ -723,7 +724,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa isModified = true; } else { final SavingsAccount savingsAccount = accountAssociations.linkedSavingsAccount(); - if (savingsAccount == null || savingsAccount.getId() != savingsAccountId) { + if (savingsAccount == null || !savingsAccount.getId().equals(savingsAccountId)) { isModified = true; } } @@ -891,7 +892,8 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa // If loan approved amount less than loan demanded amount, then need // to recompute the schedule - if (changes.containsKey(LoanApiConstants.approvedLoanAmountParameterName) || changes.containsKey("recalculateLoanSchedule")) { + if (changes.containsKey(LoanApiConstants.approvedLoanAmountParameterName) || changes.containsKey("recalculateLoanSchedule") + || changes.containsKey("expectedDisbursementDate")) { ScheduleGeneratorDTO scheduleGeneratorDTO = loanAccountDomainService.buildScheduleGeneratorDTO(loan); loan.regenerateRepaymentSchedule(scheduleGeneratorDTO, currentUser); } @@ -1082,7 +1084,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa for (LoanRepaymentScheduleInstallment installment : installments) { if (installment.getId() == null) { this.repaymentScheduleInstallmentRepository.save(installment); - } + } } this.loanRepository.saveAndFlush(loan); } catch (final DataIntegrityViolationException e) { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanChargeAssembler.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanChargeAssembler.java index b0fb14aa5..6aa6f07fc 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanChargeAssembler.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanChargeAssembler.java @@ -141,7 +141,7 @@ public class LoanChargeAssembler { } } - if (chargeDefinition.getChargeTimeType() == ChargeTimeType.DISBURSEMENT.getValue()) { + if ( ChargeTimeType.DISBURSEMENT.getValue().equals(chargeDefinition.getChargeTimeType())) { for (LoanDisbursementDetails disbursementDetail : disbursementDetails) { LoanTrancheDisbursementCharge loanTrancheDisbursementCharge = null; if (chargeDefinition.isPercentageOfApprovedAmount() @@ -164,10 +164,10 @@ public class LoanChargeAssembler { } } } - } else if (chargeDefinition.getChargeTimeType() == ChargeTimeType.TRANCHE_DISBURSEMENT.getValue()) { + } else if (ChargeTimeType.TRANCHE_DISBURSEMENT.getValue().equals(chargeDefinition.getChargeTimeType())) { LoanTrancheDisbursementCharge loanTrancheDisbursementCharge = null; for (LoanDisbursementDetails disbursementDetail : disbursementDetails) { - if (chargeDefinition.getChargeTimeType() == ChargeTimeType.TRANCHE_DISBURSEMENT.getValue()) { + if (ChargeTimeType.TRANCHE_DISBURSEMENT.getValue().equals(chargeDefinition.getChargeTimeType())) { final LoanCharge loanCharge = LoanCharge.createNewWithoutLoan(chargeDefinition, disbursementDetail.principal(), amount, chargeTime, chargeCalculation, disbursementDetail.expectedDisbursementDateAsLocalDate(), chargePaymentModeEnum, diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java index e44458ee6..cdcff7b90 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java @@ -17,11 +17,8 @@ import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.logging.Logger; import org.apache.commons.lang.StringUtils; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.joda.time.Days; import org.joda.time.LocalDate; import org.joda.time.format.DateTimeFormat; @@ -66,9 +63,9 @@ import org.mifosplatform.portfolio.client.domain.ClientEnumerations; import org.mifosplatform.portfolio.client.service.ClientReadPlatformService; import org.mifosplatform.portfolio.common.domain.PeriodFrequencyType; import org.mifosplatform.portfolio.common.service.CommonEnumerations; -import org.mifosplatform.portfolio.floatingrates.data.InterestRatePeriodData; import org.mifosplatform.portfolio.floatingrates.data.FloatingRateDTO; import org.mifosplatform.portfolio.floatingrates.data.FloatingRatePeriodData; +import org.mifosplatform.portfolio.floatingrates.data.InterestRatePeriodData; import org.mifosplatform.portfolio.floatingrates.exception.FloatingRateNotFoundException; import org.mifosplatform.portfolio.floatingrates.service.FloatingRatesReadPlatformService; import org.mifosplatform.portfolio.fund.data.FundData; @@ -559,6 +556,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { + " l.loan_type_enum as loanType, l.loanpurpose_cv_id as loanPurposeId, cv.code_value as loanPurposeName," + " lp.id as loanProductId, lp.name as loanProductName, lp.description as loanProductDescription," + " lp.is_linked_to_floating_interest_rates as isLoanProductLinkedToFloatingRate, " + + " lp.allow_variabe_installments as isvariableInstallmentsAllowed, " + " lp.allow_multiple_disbursals as multiDisburseLoan," + " lp.can_define_fixed_emi_amount as canDefineInstallmentAmount," + " c.id as clientId, c.display_name as clientName, c.office_id as clientOfficeId," @@ -630,7 +628,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { + " lir.compounding_freqency_date as compoundingFrequencyDate, " + " l.is_floating_interest_rate as isFloatingInterestRate, " + " l.interest_rate_differential as interestRateDifferential, " - + " l.create_standing_instruction_at_disbursement as createStandingInstructionAtDisbursement " + + " l.create_standing_instruction_at_disbursement as createStandingInstructionAtDisbursement, " + + " lpvi.minimum_gap as minimuminstallmentgap, lpvi.maximum_gap as maximuminstallmentgap " + " from m_loan l" // + " join m_product_loan lp on lp.id = l.product_id" // + " left join m_loan_recalculation_details lir on lir.loan_id = l.id " @@ -647,7 +646,9 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { + " left join m_appuser dbu on dbu.id = l.disbursedon_userid" + " left join m_appuser cbu on cbu.id = l.closedon_userid" + " left join m_code_value cv on cv.id = l.loanpurpose_cv_id" - + " left join ref_loan_transaction_processing_strategy lps on lps.id = l.loan_transaction_strategy_id"; + + " left join ref_loan_transaction_processing_strategy lps on lps.id = l.loan_transaction_strategy_id" + + " left join m_product_loan_variable_installment_config lpvi on lpvi.loan_product_id = l.product_id"; + } @Override @@ -735,6 +736,10 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { final LocalDate expectedMaturityDate = JdbcSupport.getLocalDate(rs, "expectedMaturityDate"); + final Boolean isvariableInstallmentsAllowed = rs.getBoolean("isvariableInstallmentsAllowed"); + final Integer minimumGap = rs.getInt("minimuminstallmentgap"); + final Integer maximumGap = rs.getInt("maximuminstallmentgap"); + final LoanApplicationTimelineData timeline = new LoanApplicationTimelineData(submittedOnDate, submittedByUsername, submittedByFirstname, submittedByLastname, rejectedOnDate, rejectedByUsername, rejectedByFirstname, rejectedByLastname, withdrawnOnDate, withdrawnByUsername, withdrawnByFirstname, withdrawnByLastname, approvedOnDate, approvedByUsername, @@ -907,17 +912,18 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { } return LoanAccountData.basicLoanDetails(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, groupData, - loanType, loanProductId, loanProductName, loanProductDescription, isLoanProductLinkedToFloatingRate, fundId, fundName, loanPurposeId, loanPurposeName, - loanOfficerId, loanOfficerName, currencyData, proposedPrincipal, principal, approvedPrincipal, totalOverpaid, - inArrearsTolerance, termFrequency, termPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentFrequencyType, - repaymentFrequencyNthDayType, repaymentFrequencyDayOfWeekType, transactionStrategyId, transactionStrategyName, - amortizationType, interestRatePerPeriod, interestRateFrequencyType, annualInterestRate, interestType, - isFloatingInterestRate, interestRateDifferential, interestCalculationPeriodType, expectedFirstRepaymentOnDate, - graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, timeline, - loanSummary, feeChargesDueAtDisbursementCharged, syncDisbursementWithMeeting, loanCounter, loanProductCounter, - multiDisburseLoan, canDefineInstallmentAmount, fixedEmiAmount, outstandingLoanBalance, inArrears, graceOnArrearsAgeing, - isNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, - createStandingInstructionAtDisbursement); + loanType, loanProductId, loanProductName, loanProductDescription, isLoanProductLinkedToFloatingRate, fundId, fundName, + loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName, currencyData, proposedPrincipal, principal, + approvedPrincipal, totalOverpaid, inArrearsTolerance, termFrequency, termPeriodFrequencyType, numberOfRepayments, + repaymentEvery, repaymentFrequencyType, repaymentFrequencyNthDayType, repaymentFrequencyDayOfWeekType, + transactionStrategyId, transactionStrategyName, amortizationType, interestRatePerPeriod, interestRateFrequencyType, + annualInterestRate, interestType, isFloatingInterestRate, interestRateDifferential, interestCalculationPeriodType, + expectedFirstRepaymentOnDate, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, + interestChargedFromDate, timeline, loanSummary, feeChargesDueAtDisbursementCharged, syncDisbursementWithMeeting, + loanCounter, loanProductCounter, multiDisburseLoan, canDefineInstallmentAmount, fixedEmiAmount, outstandingLoanBalance, + inArrears, graceOnArrearsAgeing, isNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, + interestRecalculationData, createStandingInstructionAtDisbursement, isvariableInstallmentsAllowed, minimumGap, + maximumGap); } } @@ -1468,30 +1474,21 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { } @Override - public Collection retrieveAllLoansWithOverdueInstallments(final Long penaltyWaitPeriod, final Boolean backdatePenalties) { + public Collection retrieveAllLoansWithOverdueInstallments(final Long penaltyWaitPeriod, + final Boolean backdatePenalties) { final MusoniOverdueLoanScheduleMapper rm = new MusoniOverdueLoanScheduleMapper(); final StringBuilder sqlBuilder = new StringBuilder(400); - sqlBuilder - .append("select ") - .append(rm.schema()) - .append(" where DATE_SUB(CURDATE(),INTERVAL ? DAY) > ls.duedate ") - .append( " and ls.completed_derived <> 1 and mc.charge_applies_to_enum =1 ") + sqlBuilder.append("select ").append(rm.schema()).append(" where DATE_SUB(CURDATE(),INTERVAL ? DAY) > ls.duedate ") + .append(" and ls.completed_derived <> 1 and mc.charge_applies_to_enum =1 ") .append(" and mc.charge_time_enum = 9 and ml.loan_status_id = 300 "); + if (backdatePenalties) { return this.jdbcTemplate.query(sqlBuilder.toString(), rm, new Object[] { penaltyWaitPeriod }); } + // Only apply for duedate = yesterday (so that we don't apply + // penalties on the duedate itself) + sqlBuilder.append(" and ls.duedate >= DATE_SUB(CURDATE(),INTERVAL (? + 1) DAY)"); - if(backdatePenalties) - { - return this.jdbcTemplate.query(sqlBuilder.toString(), rm, new Object[] { penaltyWaitPeriod }); - } - else - { - // Only apply for duedate = yesterday (so that we don't apply penalties on the duedate itself) - sqlBuilder.append(" and ls.duedate >= DATE_SUB(CURDATE(),INTERVAL (? + 1) DAY)"); - - return this.jdbcTemplate.query(sqlBuilder.toString(), rm, new Object[] { penaltyWaitPeriod, penaltyWaitPeriod }); - } - + return this.jdbcTemplate.query(sqlBuilder.toString(), rm, new Object[] { penaltyWaitPeriod, penaltyWaitPeriod }); } @@ -1558,7 +1555,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { private static final class LoanTermVariationsMapper implements RowMapper { public String schema() { - return "tv.id as id,tv.applicable_from as variationApplicableFrom,tv.term_value as termValue " + return "tv.id as id,tv.applicable_date as variationApplicableFrom,tv.decimal_value as decimalValue, tv.date_value as dateValue, tv.is_specific_to_installment as isSpecificToInstallment " + "from m_loan_term_variations tv"; } @@ -1566,10 +1563,13 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { public LoanTermVariationsData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { final Long id = rs.getLong("id"); final LocalDate variationApplicableFrom = JdbcSupport.getLocalDate(rs, "variationApplicableFrom"); - final BigDecimal termValue = rs.getBigDecimal("termValue"); + final BigDecimal decimalValue = rs.getBigDecimal("decimalValue"); + final LocalDate dateValue = JdbcSupport.getLocalDate(rs, "dateValue"); + final boolean isSpecificToInstallment = rs.getBoolean("isSpecificToInstallment"); final LoanTermVariationsData loanTermVariationsData = new LoanTermVariationsData(id, - LoanEnumerations.loanvariationType(LoanTermVariationType.EMI_AMOUNT), variationApplicableFrom, termValue); + LoanEnumerations.loanvariationType(LoanTermVariationType.EMI_AMOUNT), variationApplicableFrom, decimalValue, dateValue, + isSpecificToInstallment); return loanTermVariationsData; } @@ -1993,11 +1993,10 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { boolean isFloatingInterestRate = loan.getIsFloatingInterestRate(); BigDecimal interestRateDiff = loan.getInterestRateDifferential(); List baseLendingRatePeriods = null; - try{ - baseLendingRatePeriods = this.floatingRatesReadPlatformService.retrieveBaseLendingRate() - .getRatePeriods(); - }catch(final FloatingRateNotFoundException ex){ - // Do not do anything + try { + baseLendingRatePeriods = this.floatingRatesReadPlatformService.retrieveBaseLendingRate().getRatePeriods(); + } catch (final FloatingRateNotFoundException ex) { + // Do not do anything } floatingRateDTO = new FloatingRateDTO(isFloatingInterestRate, loan.getDisbursementDate(), interestRateDiff, baseLendingRatePeriods); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanSchedularServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanSchedularServiceImpl.java index 249970f1c..79e1050ae 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanSchedularServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanSchedularServiceImpl.java @@ -18,7 +18,6 @@ import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidation import org.mifosplatform.infrastructure.jobs.annotation.CronTarget; import org.mifosplatform.infrastructure.jobs.exception.JobExecutionException; import org.mifosplatform.infrastructure.jobs.service.JobName; -import org.mifosplatform.portfolio.floatingrates.service.FloatingRatesReadPlatformService; import org.mifosplatform.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,16 +31,13 @@ public class LoanSchedularServiceImpl implements LoanSchedularService { private final ConfigurationDomainService configurationDomainService; private final LoanReadPlatformService loanReadPlatformService; private final LoanWritePlatformService loanWritePlatformService; - private final FloatingRatesReadPlatformService floatingRatesReadPlatformService; @Autowired public LoanSchedularServiceImpl(final ConfigurationDomainService configurationDomainService, - final LoanReadPlatformService loanReadPlatformService, final LoanWritePlatformService loanWritePlatformService, - final FloatingRatesReadPlatformService floatingRatesReadPlatformService) { + final LoanReadPlatformService loanReadPlatformService, final LoanWritePlatformService loanWritePlatformService) { this.configurationDomainService = configurationDomainService; this.loanReadPlatformService = loanReadPlatformService; this.loanWritePlatformService = loanWritePlatformService; - this.floatingRatesReadPlatformService = floatingRatesReadPlatformService; } @Override diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanUtilService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanUtilService.java new file mode 100644 index 000000000..f87d13d42 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanUtilService.java @@ -0,0 +1,140 @@ +/** + * 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.util.List; + +import org.joda.time.LocalDate; +import org.mifosplatform.infrastructure.configuration.domain.ConfigurationDomainService; +import org.mifosplatform.organisation.holiday.domain.Holiday; +import org.mifosplatform.organisation.holiday.domain.HolidayRepository; +import org.mifosplatform.organisation.holiday.domain.HolidayStatusType; +import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; +import org.mifosplatform.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper; +import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; +import org.mifosplatform.organisation.workingdays.domain.WorkingDays; +import org.mifosplatform.organisation.workingdays.domain.WorkingDaysRepositoryWrapper; +import org.mifosplatform.portfolio.calendar.domain.Calendar; +import org.mifosplatform.portfolio.calendar.domain.CalendarEntityType; +import org.mifosplatform.portfolio.calendar.domain.CalendarInstance; +import org.mifosplatform.portfolio.calendar.domain.CalendarInstanceRepository; +import org.mifosplatform.portfolio.calendar.service.CalendarUtils; +import org.mifosplatform.portfolio.floatingrates.data.FloatingRateDTO; +import org.mifosplatform.portfolio.floatingrates.data.FloatingRatePeriodData; +import org.mifosplatform.portfolio.floatingrates.exception.FloatingRateNotFoundException; +import org.mifosplatform.portfolio.floatingrates.service.FloatingRatesReadPlatformService; +import org.mifosplatform.portfolio.loanaccount.data.HolidayDetailDTO; +import org.mifosplatform.portfolio.loanaccount.data.ScheduleGeneratorDTO; +import org.mifosplatform.portfolio.loanaccount.domain.Loan; +import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory; +import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class LoanUtilService { + + private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository; + private final CalendarInstanceRepository calendarInstanceRepository; + private final ConfigurationDomainService configurationDomainService; + private final HolidayRepository holidayRepository; + private final WorkingDaysRepositoryWrapper workingDaysRepository; + private final LoanScheduleGeneratorFactory loanScheduleFactory; + private final FloatingRatesReadPlatformService floatingRatesReadPlatformService; + + @Autowired + public LoanUtilService(final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository, + final CalendarInstanceRepository calendarInstanceRepository, final ConfigurationDomainService configurationDomainService, + final HolidayRepository holidayRepository, final WorkingDaysRepositoryWrapper workingDaysRepository, + final LoanScheduleGeneratorFactory loanScheduleFactory, final FloatingRatesReadPlatformService floatingRatesReadPlatformService) { + this.applicationCurrencyRepository = applicationCurrencyRepository; + this.calendarInstanceRepository = calendarInstanceRepository; + this.configurationDomainService = configurationDomainService; + this.holidayRepository = holidayRepository; + this.workingDaysRepository = workingDaysRepository; + this.loanScheduleFactory = loanScheduleFactory; + this.floatingRatesReadPlatformService = floatingRatesReadPlatformService; + } + + public LocalDate getCalculatedRepaymentsStartingFromDate(final LocalDate actualDisbursementDate, final Loan loan, + final CalendarInstance calendarInstance) { + final Calendar calendar = calendarInstance == null ? null : calendarInstance.getCalendar(); + LocalDate calculatedRepaymentsStartingFromDate = loan.getExpectedFirstRepaymentOnDate(); + if (calendar != null) {// sync repayments + + // TODO: AA - user provided first repayment date takes precedence + // over recalculated meeting date + if (calculatedRepaymentsStartingFromDate == null) { + // FIXME: AA - Possibility of having next meeting date + // immediately after disbursement date, + // need to have minimum number of days gap between disbursement + // and first repayment date. + final LoanProductRelatedDetail repaymentScheduleDetails = loan.repaymentScheduleDetail(); + if (repaymentScheduleDetails != null) {// Not expecting to be + // null + final Integer repayEvery = repaymentScheduleDetails.getRepayEvery(); + final String frequency = CalendarUtils.getMeetingFrequencyFromPeriodFrequencyType(repaymentScheduleDetails + .getRepaymentPeriodFrequencyType()); + calculatedRepaymentsStartingFromDate = CalendarUtils.getFirstRepaymentMeetingDate(calendar, actualDisbursementDate, + repayEvery, frequency); + } + } + } + return calculatedRepaymentsStartingFromDate; + } + + public ScheduleGeneratorDTO buildScheduleGeneratorDTO(final Loan loan) { + + final MonetaryCurrency currency = loan.getCurrency(); + ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency); + final CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByEntityId(loan.getId(), + CalendarEntityType.LOANS.getValue()); + LocalDate calculatedRepaymentsStartingFromDate = this.getCalculatedRepaymentsStartingFromDate(loan.getDisbursementDate(), loan, + calendarInstance); + final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled(); + final List holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(), loan + .getExpectedDisbursedOnLocalDate().toDate(), HolidayStatusType.ACTIVE.getValue()); + final WorkingDays workingDays = this.workingDaysRepository.findOne(); + final boolean allowTransactionsOnHoliday = this.configurationDomainService.allowTransactionsOnHolidayEnabled(); + final boolean allowTransactionsOnNonWorkingDay = this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled(); + + HolidayDetailDTO holidayDetailDTO = new HolidayDetailDTO(isHolidayEnabled, holidays, workingDays, allowTransactionsOnHoliday, + allowTransactionsOnNonWorkingDay); + CalendarInstance restCalendarInstance = null; + CalendarInstance compoundingCalendarInstance = null; + if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { + restCalendarInstance = calendarInstanceRepository.findCalendarInstaneByEntityId(loan.loanInterestRecalculationDetailId(), + CalendarEntityType.LOAN_RECALCULATION_REST_DETAIL.getValue()); + compoundingCalendarInstance = calendarInstanceRepository.findCalendarInstaneByEntityId( + loan.loanInterestRecalculationDetailId(), CalendarEntityType.LOAN_RECALCULATION_COMPOUNDING_DETAIL.getValue()); + } + FloatingRateDTO floatingRateDTO = constructFloatingRateDTO(loan); + ScheduleGeneratorDTO scheduleGeneratorDTO = new ScheduleGeneratorDTO(loanScheduleFactory, applicationCurrency, + calculatedRepaymentsStartingFromDate, holidayDetailDTO, restCalendarInstance, compoundingCalendarInstance, floatingRateDTO); + + return scheduleGeneratorDTO; + } + + private FloatingRateDTO constructFloatingRateDTO(final Loan loan) { + FloatingRateDTO floatingRateDTO = null; + if (loan.loanProduct().isLinkedToFloatingInterestRate()) { + boolean isFloatingInterestRate = loan.getIsFloatingInterestRate(); + BigDecimal interestRateDiff = loan.getInterestRateDifferential(); + List baseLendingRatePeriods = null; + try { + baseLendingRatePeriods = this.floatingRatesReadPlatformService.retrieveBaseLendingRate().getRatePeriods(); + } catch (final FloatingRateNotFoundException ex) { + // Do not do anything + } + + floatingRateDTO = new FloatingRateDTO(isFloatingInterestRate, loan.getDisbursementDate(), interestRateDiff, + baseLendingRatePeriods); + } + return floatingRateDTO; + } + +} 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 c95b223c9..3f8d0d140 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 @@ -2441,6 +2441,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } } + @Override @Transactional public void applyOverdueChargesForLoan(final Long loanId, Collection overdueLoanScheduleDatas) { 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 9cc4c9edf..0835bc738 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 @@ -102,5 +102,12 @@ public interface LoanProductConstants { public static final String inArrearsToleranceParamName = "inArrearsTolerance"; public static final String repaymentEveryParamName = "repaymentEvery"; public static final String graceOnPrincipalAndInterestPaymentParamName = "graceOnPrincipalAndInterestPayment"; + + //Variable Installments Settings + public static final String allowVariableInstallmentsParamName = "allowVariableInstallments" ; + public static final String minimumGapBetweenInstallments = "minimumGap" ; + public static final String maximumGapBetweenInstallments = "maximumGap" ; + + } 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 9eed599e0..d19103dec 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 @@ -68,7 +68,7 @@ public class LoanProductData { private final BigDecimal maxInterestRatePerPeriod; private final EnumOptionData interestRateFrequencyType; private final BigDecimal annualInterestRate; - + private final boolean isLinkedToFloatingInterestRates; private final Integer floatingRateId; private final String floatingRateName; @@ -78,6 +78,11 @@ public class LoanProductData { private final BigDecimal maxDifferentialLendingRate; private final boolean isFloatingInterestRateCalculationAllowed; + // Variable Installments Settings + private final boolean allowVariableInstallments; + private final Integer minimumGap; + private final Integer maximumGap; + // settings private final EnumOptionData amortizationType; private final EnumOptionData interestType; @@ -185,6 +190,9 @@ public class LoanProductData { final BigDecimal defaultDifferentialLendingRate = null; final BigDecimal maxDifferentialLendingRate = null; final boolean isFloatingInterestRateCalculationAllowed = false; + final boolean isVariableInstallmentsAllowed = false; + final Integer minimumGap = null; + final Integer maximumGap = null; final EnumOptionData repaymentFrequencyType = null; final EnumOptionData interestRateFrequencyType = null; final EnumOptionData amortizationType = null; @@ -238,8 +246,8 @@ public class LoanProductData { interestRecalculationData, minimumDaysBetweenDisbursalAndFirstRepayment, holdGuaranteeFunds, productGuaranteeData, principalThresholdForLastInstallment, accountMovesOutOfNPAOnlyOnArrearsCompletion, canDefineInstallmentAmount, installmentAmountInMultiplesOf, loanProductConfigurableAttributes, isLinkedToFloatingInterestRates, floatingRateId, - floatingRateName, interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate, - maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed); + floatingRateName, interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate, + maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGap, maximumGap); } @@ -266,6 +274,9 @@ public class LoanProductData { final BigDecimal defaultDifferentialLendingRate = null; final BigDecimal maxDifferentialLendingRate = null; final boolean isFloatingInterestRateCalculationAllowed = false; + final boolean isVariableInstallmentsAllowed = false; + final Integer minimumGap = null; + final Integer maximumGap = null; final EnumOptionData repaymentFrequencyType = null; final EnumOptionData interestRateFrequencyType = null; final EnumOptionData amortizationType = null; @@ -321,8 +332,8 @@ public class LoanProductData { interestRecalculationData, minimumDaysBetweenDisbursalAndFirstRepayment, holdGuaranteeFunds, productGuaranteeData, principalThresholdForLastInstallment, accountMovesOutOfNPAOnlyOnArrearsCompletion, canDefineInstallmentAmount, installmentAmountInMultiplesOf, loanProductConfigurableAttributes, isLinkedToFloatingInterestRates, floatingRateId, - floatingRateName, interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate, - maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed); + floatingRateName, interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate, + maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGap, maximumGap); } @@ -353,6 +364,9 @@ public class LoanProductData { final BigDecimal defaultDifferentialLendingRate = null; final BigDecimal maxDifferentialLendingRate = null; final boolean isFloatingInterestRateCalculationAllowed = false; + final boolean isVariableInstallmentsAllowed = false; + final Integer minimumGap = null; + final Integer maximumGap = null; final EnumOptionData repaymentFrequencyType = LoanEnumerations.repaymentFrequencyType(PeriodFrequencyType.MONTHS); final EnumOptionData interestRateFrequencyType = LoanEnumerations.interestRateFrequencyType(PeriodFrequencyType.MONTHS); final EnumOptionData amortizationType = LoanEnumerations.amortizationType(AmortizationMethod.EQUAL_INSTALLMENTS); @@ -411,9 +425,9 @@ public class LoanProductData { daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, minimumDaysBetweenDisbursalAndFirstRepayment, holdGuaranteeFunds, productGuaranteeData, principalThresholdForLastInstallment, accountMovesOutOfNPAOnlyOnArrearsCompletion, canDefineInstallmentAmount, installmentAmountInMultiplesOf, - loanProductConfigurableAttributes, isLinkedToFloatingInterestRates, floatingRateId, - floatingRateName, interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate, - maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed); + loanProductConfigurableAttributes, isLinkedToFloatingInterestRates, floatingRateId, floatingRateName, + interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate, maxDifferentialLendingRate, + isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGap, maximumGap); } @@ -449,9 +463,10 @@ public class LoanProductData { final LoanProductGuaranteeData loanProductGuaranteeData, final BigDecimal principalThresholdForLastInstallment, final boolean accountMovesOutOfNPAOnlyOnArrearsCompletion, boolean canDefineInstallmentAmount, Integer installmentAmountInMultiplesOf, LoanProductConfigurableAttributes allowAttributeOverrides, - boolean isLinkedToFloatingInterestRates, Integer floatingRateId, String floatingRateName, - BigDecimal interestRateDifferential, BigDecimal minDifferentialLendingRate, BigDecimal defaultDifferentialLendingRate, - BigDecimal maxDifferentialLendingRate, boolean isFloatingInterestRateCalculationAllowed) { + boolean isLinkedToFloatingInterestRates, Integer floatingRateId, String floatingRateName, BigDecimal interestRateDifferential, + BigDecimal minDifferentialLendingRate, BigDecimal defaultDifferentialLendingRate, BigDecimal maxDifferentialLendingRate, + boolean isFloatingInterestRateCalculationAllowed, final boolean isVariableInstallmentsAllowed, + final Integer minimumGapBetweenInstallments, final Integer maximumGapBetweenInstallments) { this.id = id; this.name = name; this.shortName = shortName; @@ -480,6 +495,9 @@ public class LoanProductData { this.defaultDifferentialLendingRate = defaultDifferentialLendingRate; this.maxDifferentialLendingRate = maxDifferentialLendingRate; this.isFloatingInterestRateCalculationAllowed = isFloatingInterestRateCalculationAllowed; + this.allowVariableInstallments = isVariableInstallmentsAllowed; + this.minimumGap = minimumGapBetweenInstallments; + this.maximumGap = maximumGapBetweenInstallments; this.repaymentFrequencyType = repaymentFrequencyType; this.interestRateFrequencyType = interestRateFrequencyType; this.amortizationType = amortizationType; @@ -587,6 +605,9 @@ public class LoanProductData { this.defaultDifferentialLendingRate = productData.defaultDifferentialLendingRate; this.maxDifferentialLendingRate = productData.maxDifferentialLendingRate; this.isFloatingInterestRateCalculationAllowed = productData.isFloatingInterestRateCalculationAllowed; + this.allowVariableInstallments = productData.allowVariableInstallments; + this.minimumGap = productData.minimumGap; + this.maximumGap = productData.maximumGap; this.repaymentFrequencyType = productData.repaymentFrequencyType; this.interestRateFrequencyType = productData.interestRateFrequencyType; this.amortizationType = productData.amortizationType; @@ -950,25 +971,35 @@ public class LoanProductData { this.allowAttributeOverrides = loanProductConfigurableAttributes; } - public boolean isLinkedToFloatingInterestRates() { - return this.isLinkedToFloatingInterestRates; - } + public boolean isLinkedToFloatingInterestRates() { + return this.isLinkedToFloatingInterestRates; + } - public BigDecimal getMinDifferentialLendingRate() { - return this.minDifferentialLendingRate; - } + public BigDecimal getMinDifferentialLendingRate() { + return this.minDifferentialLendingRate; + } - public BigDecimal getDefaultDifferentialLendingRate() { - return this.defaultDifferentialLendingRate; - } + public BigDecimal getDefaultDifferentialLendingRate() { + return this.defaultDifferentialLendingRate; + } - public BigDecimal getMaxDifferentialLendingRate() { - return this.maxDifferentialLendingRate; - } + public BigDecimal getMaxDifferentialLendingRate() { + return this.maxDifferentialLendingRate; + } - public boolean isFloatingInterestRateCalculationAllowed() { - return this.isFloatingInterestRateCalculationAllowed; - } - - + public boolean isFloatingInterestRateCalculationAllowed() { + return this.isFloatingInterestRateCalculationAllowed; + } + + public boolean isVariableInstallmentsAllowed() { + return this.allowVariableInstallments; + } + + public Integer getMinimumGapBetweenInstallments() { + return this.minimumGap; + } + + public Integer getMaximumGapBetweenInstallments() { + return this.maximumGap; + } } \ No newline at end of file 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 a381ac099..18c4a1611 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 @@ -51,7 +51,6 @@ import org.mifosplatform.portfolio.fund.domain.Fund; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.AprCalculator; import org.mifosplatform.portfolio.loanproduct.LoanProductConstants; import org.springframework.data.jpa.domain.AbstractPersistable; -import org.mifosplatform.portfolio.loanproduct.domain.LoanProductConfigurableAttributes; import com.google.gson.JsonArray; import com.google.gson.JsonObject; @@ -164,6 +163,13 @@ public class LoanProduct extends AbstractPersistable { @OneToOne(cascade = CascadeType.ALL, mappedBy = "loanProduct", optional = true, orphanRemoval = true) private LoanProductFloatingRates floatingRates; + @Column(name = "allow_variabe_installments", nullable = false) + private boolean allowVariabeInstallments; + + @LazyCollection(LazyCollectionOption.FALSE) + @OneToOne(cascade = CascadeType.ALL, mappedBy = "loanProduct", optional = true, orphanRemoval = true) + private LoanProductVariableInstallmentConfig variableInstallmentConfig; + public static LoanProduct assembleFromJson(final Fund fund, final LoanTransactionProcessingStrategy loanTransactionProcessingStrategy, final List productCharges, final JsonCommand command, final AprCalculator aprCalculator, FloatingRate floatingRate) { @@ -195,6 +201,10 @@ public class LoanProduct extends AbstractPersistable { BigDecimal maxDifferentialLendingRate = null; BigDecimal defaultDifferentialLendingRate = null; Boolean isFloatingInterestRateCalculationAllowed = null; + + Integer minimumGapBetweenInstallments = null; + Integer maximumGapBetweenInstallments = null; + final Boolean isLinkedToFloatingInterestRates = command.booleanObjectValueOfParameterNamed("isLinkedToFloatingInterestRates"); if (isLinkedToFloatingInterestRates != null && isLinkedToFloatingInterestRates) { interestRateDifferential = command.bigDecimalValueOfParameterNamed("interestRateDifferential"); @@ -211,6 +221,13 @@ public class LoanProduct extends AbstractPersistable { annualInterestRate = aprCalculator.calculateFrom(interestFrequencyType, interestRatePerPeriod); } + final Boolean isVariableInstallmentsAllowed = command + .booleanObjectValueOfParameterNamed(LoanProductConstants.allowVariableInstallmentsParamName); + if (isVariableInstallmentsAllowed != null && isVariableInstallmentsAllowed) { + minimumGapBetweenInstallments = command.integerValueOfParameterNamed(LoanProductConstants.minimumGapBetweenInstallments); + maximumGapBetweenInstallments = command.integerValueOfParameterNamed(LoanProductConstants.maximumGapBetweenInstallments); + } + final Integer repaymentEvery = command.integerValueOfParameterNamed("repaymentEvery"); final Integer numberOfRepayments = command.integerValueOfParameterNamed("numberOfRepayments"); final Integer minNumberOfRepayments = command.integerValueOfParameterNamed("minNumberOfRepayments"); @@ -304,7 +321,8 @@ public class LoanProduct extends AbstractPersistable { accountMovesOutOfNPAOnlyOnArrearsCompletion, canDefineEmiAmount, installmentAmountInMultiplesOf, loanConfigurableAttributes, isLinkedToFloatingInterestRates, floatingRate, interestRateDifferential, minDifferentialLendingRate, maxDifferentialLendingRate, defaultDifferentialLendingRate, - isFloatingInterestRateCalculationAllowed); + isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGapBetweenInstallments, + maximumGapBetweenInstallments); } @@ -534,7 +552,8 @@ public class LoanProduct extends AbstractPersistable { final Integer installmentAmountInMultiplesOf, final LoanProductConfigurableAttributes loanProductConfigurableAttributes, Boolean isLinkedToFloatingInterestRates, FloatingRate floatingRate, BigDecimal interestRateDifferential, BigDecimal minDifferentialLendingRate, BigDecimal maxDifferentialLendingRate, BigDecimal defaultDifferentialLendingRate, - Boolean isFloatingInterestRateCalculationAllowed) { + Boolean isFloatingInterestRateCalculationAllowed, final Boolean isVariableInstallmentsAllowed, + final Integer minimumGapBetweenInstallments, final Integer maximumGapBetweenInstallments) { this.fund = fund; this.transactionProcessingStrategy = transactionProcessingStrategy; this.name = name.trim(); @@ -555,6 +574,13 @@ public class LoanProduct extends AbstractPersistable { maxDifferentialLendingRate, defaultDifferentialLendingRate, isFloatingInterestRateCalculationAllowed); } + this.allowVariabeInstallments = isVariableInstallmentsAllowed == null ? false : isVariableInstallmentsAllowed; + + if (allowVariabeInstallments) { + this.variableInstallmentConfig = new LoanProductVariableInstallmentConfig(this, minimumGapBetweenInstallments, + maximumGapBetweenInstallments); + } + this.loanProductRelatedDetail = new LoanProductRelatedDetail(currency, defaultPrincipal, defaultNominalInterestRatePerPeriod, interestPeriodFrequencyType, defaultAnnualNominalInterestRate, interestMethod, interestCalculationPeriodMethod, repayEvery, repaymentFrequencyType, defaultNumberOfInstallments, graceOnPrincipalPayment, graceOnInterestPayment, @@ -674,8 +700,20 @@ public class LoanProduct extends AbstractPersistable { actualChanges.putAll(loanProductFloatingRates().update(command, floatingRate)); this.loanProductRelatedDetail.updateForFloatingInterestRates(); this.loanProductMinMaxConstraints.updateForFloatingInterestRates(); - }else{ - this.floatingRates = null; + } else { + this.floatingRates = null; + } + + if (command.isChangeInBooleanParameterNamed(LoanProductConstants.allowVariableInstallmentsParamName, this.allowVariabeInstallments)) { + final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.allowVariableInstallmentsParamName); + actualChanges.put(LoanProductConstants.allowVariableInstallmentsParamName, newValue); + this.allowVariabeInstallments = newValue; + } + + if (this.allowVariabeInstallments) { + actualChanges.putAll(loanProductVariableInstallmentConfig().update(command)); + } else { + this.variableInstallmentConfig = null; } final String accountingTypeParamName = "accountingRule"; @@ -964,13 +1002,18 @@ public class LoanProduct extends AbstractPersistable { return actualChanges; } - private LoanProductFloatingRates loanProductFloatingRates() { - this.floatingRates = this.floatingRates == null - ? new LoanProductFloatingRates(null, this, null, null, null, null, false) - : this.floatingRates; + private LoanProductFloatingRates loanProductFloatingRates() { + this.floatingRates = this.floatingRates == null ? new LoanProductFloatingRates(null, this, null, null, null, null, false) + : this.floatingRates; return this.floatingRates; } + public LoanProductVariableInstallmentConfig loanProductVariableInstallmentConfig() { + this.variableInstallmentConfig = this.variableInstallmentConfig == null ? new LoanProductVariableInstallmentConfig(this, null, null) + : this.variableInstallmentConfig; + return this.variableInstallmentConfig; + } + public boolean isAccountingDisabled() { return AccountingRuleType.NONE.getValue().equals(this.accountingRule); } @@ -1268,4 +1311,8 @@ public class LoanProduct extends AbstractPersistable { return applicableRates; } + public boolean allowVariabeInstallments() { + return this.allowVariabeInstallments; + } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProductVariableInstallmentConfig.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProductVariableInstallmentConfig.java new file mode 100644 index 000000000..ba3672f1a --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProductVariableInstallmentConfig.java @@ -0,0 +1,75 @@ +/** + * 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.loanproduct.domain; + +import java.util.LinkedHashMap; +import java.util.Map; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.OneToOne; +import javax.persistence.Table; + +import org.mifosplatform.infrastructure.core.api.JsonCommand; +import org.mifosplatform.portfolio.loanproduct.LoanProductConstants; +import org.springframework.data.jpa.domain.AbstractPersistable; + +@Entity +@Table(name = "m_product_loan_variable_installment_config") +public class LoanProductVariableInstallmentConfig extends AbstractPersistable { + + @OneToOne + @JoinColumn(name = "loan_product_id", nullable = false) + private LoanProduct loanProduct; + + @Column(name = "minimum_gap") + private Integer minimumGap; + + @Column(name = "maximum_gap") + private Integer maximumGap; + + protected LoanProductVariableInstallmentConfig() { + + } + + public LoanProductVariableInstallmentConfig(final LoanProduct loanProduct, final Integer minimumGap, final Integer maximumGap) { + this.loanProduct = loanProduct; + this.minimumGap = minimumGap; + this.maximumGap = maximumGap; + } + + public void setLoanProduct(final LoanProduct loanProduct) { + this.loanProduct = loanProduct; + } + + public Map update(JsonCommand command) { + final Map actualChanges = new LinkedHashMap<>(3); + + if (command.isChangeInIntegerParameterNamed(LoanProductConstants.minimumGapBetweenInstallments, this.minimumGap)) { + final Integer newValue = command.integerValueOfParameterNamed(LoanProductConstants.minimumGapBetweenInstallments); + actualChanges.put(LoanProductConstants.minimumGapBetweenInstallments, newValue); + this.minimumGap = newValue; + } + + if (command.isChangeInIntegerParameterNamed(LoanProductConstants.maximumGapBetweenInstallments, this.maximumGap)) { + final Integer newValue = command.integerValueOfParameterNamed(LoanProductConstants.maximumGapBetweenInstallments); + actualChanges.put(LoanProductConstants.maximumGapBetweenInstallments, newValue); + this.maximumGap = newValue; + } + + return actualChanges; + } + + public Integer getMinimumGap() { + return this.minimumGap; + } + + public Integer getMaximumGap() { + return this.maximumGap; + } + +} 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 06a9b8d3d..1f45e22ba 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 @@ -87,7 +87,9 @@ public final class LoanProductDataValidator { LoanProductConstants.minimumGuaranteeFromOwnFundsParamName, LoanProductConstants.principalThresholdForLastInstallmentParamName, LoanProductConstants.accountMovesOutOfNPAOnlyOnArrearsCompletionParamName, LoanProductConstants.canDefineEmiAmountParamName, LoanProductConstants.installmentAmountInMultiplesOfParamName, - LoanProductConstants.preClosureInterestCalculationStrategyParamName, LoanProductConstants.allowAttributeOverridesParamName)); + LoanProductConstants.preClosureInterestCalculationStrategyParamName, LoanProductConstants.allowAttributeOverridesParamName, + LoanProductConstants.allowVariableInstallmentsParamName, LoanProductConstants.minimumGapBetweenInstallments, + LoanProductConstants.maximumGapBetweenInstallments)); private final FromJsonHelper fromApiJsonHelper; @@ -290,58 +292,74 @@ public final class LoanProductDataValidator { } } - //interest rates - if(this.fromApiJsonHelper.parameterExists("isLinkedToFloatingInterestRates", element) - && this.fromApiJsonHelper.extractBooleanNamed("isLinkedToFloatingInterestRates", element) == true){ - if(this.fromApiJsonHelper.parameterExists("interestRatePerPeriod", element)){ - baseDataValidator.reset().parameter("interestRatePerPeriod").failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.true", - "interestRatePerPeriod param is not supported when isLinkedToFloatingInterestRates is true"); + // interest rates + if (this.fromApiJsonHelper.parameterExists("isLinkedToFloatingInterestRates", element) + && this.fromApiJsonHelper.extractBooleanNamed("isLinkedToFloatingInterestRates", element) == true) { + if (this.fromApiJsonHelper.parameterExists("interestRatePerPeriod", element)) { + baseDataValidator + .reset() + .parameter("interestRatePerPeriod") + .failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.true", + "interestRatePerPeriod param is not supported when isLinkedToFloatingInterestRates is true"); } - - if(this.fromApiJsonHelper.parameterExists("minInterestRatePerPeriod", element)){ - baseDataValidator.reset().parameter("minInterestRatePerPeriod").failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.true", - "minInterestRatePerPeriod param is not supported when isLinkedToFloatingInterestRates is true"); + + if (this.fromApiJsonHelper.parameterExists("minInterestRatePerPeriod", element)) { + baseDataValidator + .reset() + .parameter("minInterestRatePerPeriod") + .failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.true", + "minInterestRatePerPeriod param is not supported when isLinkedToFloatingInterestRates is true"); } - - if(this.fromApiJsonHelper.parameterExists("maxInterestRatePerPeriod", element)){ - baseDataValidator.reset().parameter("maxInterestRatePerPeriod").failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.true", - "maxInterestRatePerPeriod param is not supported when isLinkedToFloatingInterestRates is true"); + + if (this.fromApiJsonHelper.parameterExists("maxInterestRatePerPeriod", element)) { + baseDataValidator + .reset() + .parameter("maxInterestRatePerPeriod") + .failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.true", + "maxInterestRatePerPeriod param is not supported when isLinkedToFloatingInterestRates is true"); } - - if(this.fromApiJsonHelper.parameterExists("interestRateFrequencyType", element)){ - baseDataValidator.reset().parameter("interestRateFrequencyType").failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.true", - "interestRateFrequencyType param is not supported when isLinkedToFloatingInterestRates is true"); + + if (this.fromApiJsonHelper.parameterExists("interestRateFrequencyType", element)) { + baseDataValidator + .reset() + .parameter("interestRateFrequencyType") + .failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.true", + "interestRateFrequencyType param is not supported when isLinkedToFloatingInterestRates is true"); } - if((interestType == null || interestType != InterestMethod.DECLINING_BALANCE.getValue()) - || (isInterestRecalculationEnabled == null || isInterestRecalculationEnabled == false)){ - baseDataValidator.reset().parameter("isLinkedToFloatingInterestRates").failWithCode("supported.only.for.declining.balance.interest.recalculation.enabled", - "Floating interest rates are supported only for declining balance and interest recalculation enabled loan products"); - } - - final Integer floatingRatesId = this.fromApiJsonHelper.extractIntegerNamed("floatingRatesId", element, - Locale.getDefault()); + if ((interestType == null || interestType != InterestMethod.DECLINING_BALANCE.getValue()) + || (isInterestRecalculationEnabled == null || isInterestRecalculationEnabled == false)) { + baseDataValidator + .reset() + .parameter("isLinkedToFloatingInterestRates") + .failWithCode("supported.only.for.declining.balance.interest.recalculation.enabled", + "Floating interest rates are supported only for declining balance and interest recalculation enabled loan products"); + } + + final Integer floatingRatesId = this.fromApiJsonHelper.extractIntegerNamed("floatingRatesId", element, Locale.getDefault()); baseDataValidator.reset().parameter("floatingRatesId").value(floatingRatesId).notNull(); - - final BigDecimal interestRateDifferential = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("interestRateDifferential", element); - baseDataValidator.reset().parameter("interestRateDifferential").value(interestRateDifferential).notNull().zeroOrPositiveAmount(); + + final BigDecimal interestRateDifferential = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("interestRateDifferential", + element); + baseDataValidator.reset().parameter("interestRateDifferential").value(interestRateDifferential).notNull() + .zeroOrPositiveAmount(); final String minDifferentialLendingRateParameterName = "minDifferentialLendingRate"; - BigDecimal minDifferentialLendingRate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(minDifferentialLendingRateParameterName, - element); + BigDecimal minDifferentialLendingRate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed( + minDifferentialLendingRateParameterName, element); baseDataValidator.reset().parameter(minDifferentialLendingRateParameterName).value(minDifferentialLendingRate).notNull() - .zeroOrPositiveAmount(); + .zeroOrPositiveAmount(); final String defaultDifferentialLendingRateParameterName = "defaultDifferentialLendingRate"; - BigDecimal defaultDifferentialLendingRate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(defaultDifferentialLendingRateParameterName, - element); - baseDataValidator.reset().parameter(defaultDifferentialLendingRateParameterName).value(defaultDifferentialLendingRate).notNull() - .zeroOrPositiveAmount(); + BigDecimal defaultDifferentialLendingRate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed( + defaultDifferentialLendingRateParameterName, element); + baseDataValidator.reset().parameter(defaultDifferentialLendingRateParameterName).value(defaultDifferentialLendingRate) + .notNull().zeroOrPositiveAmount(); final String maxDifferentialLendingRateParameterName = "maxDifferentialLendingRate"; - BigDecimal maxDifferentialLendingRate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(maxDifferentialLendingRateParameterName, - element); + BigDecimal maxDifferentialLendingRate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed( + maxDifferentialLendingRateParameterName, element); baseDataValidator.reset().parameter(maxDifferentialLendingRateParameterName).value(maxDifferentialLendingRate).notNull() - .zeroOrPositiveAmount(); + .zeroOrPositiveAmount(); if (defaultDifferentialLendingRate != null && defaultDifferentialLendingRate.compareTo(BigDecimal.ZERO) != -1) { if (minDifferentialLendingRate != null && minDifferentialLendingRate.compareTo(BigDecimal.ZERO) != -1) { @@ -349,57 +367,76 @@ public final class LoanProductDataValidator { .notLessThanMin(minDifferentialLendingRate); } } - + if (maxDifferentialLendingRate != null && maxDifferentialLendingRate.compareTo(BigDecimal.ZERO) != -1) { if (minDifferentialLendingRate != null && minDifferentialLendingRate.compareTo(BigDecimal.ZERO) != -1) { baseDataValidator.reset().parameter("maxDifferentialLendingRate").value(maxDifferentialLendingRate) .notLessThanMin(minDifferentialLendingRate); } } - + if (maxDifferentialLendingRate != null && maxDifferentialLendingRate.compareTo(BigDecimal.ZERO) != -1) { if (defaultDifferentialLendingRate != null && defaultDifferentialLendingRate.compareTo(BigDecimal.ZERO) != -1) { baseDataValidator.reset().parameter("maxDifferentialLendingRate").value(maxDifferentialLendingRate) .notLessThanMin(defaultDifferentialLendingRate); } } - + final Boolean isFloatingInterestRateCalculationAllowed = this.fromApiJsonHelper.extractBooleanNamed( - "isFloatingInterestRateCalculationAllowed", element); - baseDataValidator.reset().parameter("isFloatingInterestRateCalculationAllowed") - .value(isFloatingInterestRateCalculationAllowed).notNull().isOneOfTheseValues(true, false); + "isFloatingInterestRateCalculationAllowed", element); + baseDataValidator.reset().parameter("isFloatingInterestRateCalculationAllowed").value(isFloatingInterestRateCalculationAllowed) + .notNull().isOneOfTheseValues(true, false); } else { - if(this.fromApiJsonHelper.parameterExists("floatingRatesId", element)){ - baseDataValidator.reset().parameter("floatingRatesId").failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.false", - "floatingRatesId param is not supported when isLinkedToFloatingInterestRates is not supplied or false"); + if (this.fromApiJsonHelper.parameterExists("floatingRatesId", element)) { + baseDataValidator + .reset() + .parameter("floatingRatesId") + .failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.false", + "floatingRatesId param is not supported when isLinkedToFloatingInterestRates is not supplied or false"); } - - if(this.fromApiJsonHelper.parameterExists("interestRateDifferential", element)){ - baseDataValidator.reset().parameter("interestRateDifferential").failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.false", - "interestRateDifferential param is not supported when isLinkedToFloatingInterestRates is not supplied or false"); + + if (this.fromApiJsonHelper.parameterExists("interestRateDifferential", element)) { + baseDataValidator + .reset() + .parameter("interestRateDifferential") + .failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.false", + "interestRateDifferential param is not supported when isLinkedToFloatingInterestRates is not supplied or false"); } - - if(this.fromApiJsonHelper.parameterExists("minDifferentialLendingRate", element)){ - baseDataValidator.reset().parameter("minDifferentialLendingRate").failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.false", - "minDifferentialLendingRate param is not supported when isLinkedToFloatingInterestRates is not supplied or false"); + + if (this.fromApiJsonHelper.parameterExists("minDifferentialLendingRate", element)) { + baseDataValidator + .reset() + .parameter("minDifferentialLendingRate") + .failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.false", + "minDifferentialLendingRate param is not supported when isLinkedToFloatingInterestRates is not supplied or false"); } - - if(this.fromApiJsonHelper.parameterExists("defaultDifferentialLendingRate", element)){ - baseDataValidator.reset().parameter("defaultDifferentialLendingRate").failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.false", - "defaultDifferentialLendingRate param is not supported when isLinkedToFloatingInterestRates is not supplied or false"); + + if (this.fromApiJsonHelper.parameterExists("defaultDifferentialLendingRate", element)) { + baseDataValidator + .reset() + .parameter("defaultDifferentialLendingRate") + .failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.false", + "defaultDifferentialLendingRate param is not supported when isLinkedToFloatingInterestRates is not supplied or false"); } - - if(this.fromApiJsonHelper.parameterExists("maxDifferentialLendingRate", element)){ - baseDataValidator.reset().parameter("maxDifferentialLendingRate").failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.false", - "maxDifferentialLendingRate param is not supported when isLinkedToFloatingInterestRates is not supplied or false"); + + if (this.fromApiJsonHelper.parameterExists("maxDifferentialLendingRate", element)) { + baseDataValidator + .reset() + .parameter("maxDifferentialLendingRate") + .failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.false", + "maxDifferentialLendingRate param is not supported when isLinkedToFloatingInterestRates is not supplied or false"); } - - if(this.fromApiJsonHelper.parameterExists("isFloatingInterestRateCalculationAllowed", element)){ - baseDataValidator.reset().parameter("isFloatingInterestRateCalculationAllowed").failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.false", - "isFloatingInterestRateCalculationAllowed param is not supported when isLinkedToFloatingInterestRates is not supplied or false"); + + if (this.fromApiJsonHelper.parameterExists("isFloatingInterestRateCalculationAllowed", element)) { + baseDataValidator + .reset() + .parameter("isFloatingInterestRateCalculationAllowed") + .failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.false", + "isFloatingInterestRateCalculationAllowed param is not supported when isLinkedToFloatingInterestRates is not supplied or false"); } - - final BigDecimal interestRatePerPeriod = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("interestRatePerPeriod", element); + + final BigDecimal interestRatePerPeriod = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("interestRatePerPeriod", + element); baseDataValidator.reset().parameter("interestRatePerPeriod").value(interestRatePerPeriod).notNull().zeroOrPositiveAmount(); final String minInterestRatePerPeriodParameterName = "minInterestRatePerPeriod"; @@ -442,7 +479,6 @@ public final class LoanProductDataValidator { baseDataValidator.reset().parameter("interestRateFrequencyType").value(interestRateFrequencyType).notNull().inMinMaxRange(0, 3); } - // Guarantee Funds Boolean holdGuaranteeFunds = false; if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.holdGuaranteeFundsParamName, element)) { @@ -563,9 +599,58 @@ public final class LoanProductDataValidator { validateLoanConfigurableAttributes(baseDataValidator, element); + validateVariableInstallmentSettings(baseDataValidator, element); + throwExceptionIfValidationWarningsExist(dataValidationErrors); } + private void validateVariableInstallmentSettings(final DataValidatorBuilder baseDataValidator, final JsonElement element) { + if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.allowVariableInstallmentsParamName, element) + && this.fromApiJsonHelper.extractBooleanNamed(LoanProductConstants.allowVariableInstallmentsParamName, element)) { + + Long minimumGapBetweenInstallments = null; + if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.minimumGapBetweenInstallments, element)) { + minimumGapBetweenInstallments = this.fromApiJsonHelper.extractLongNamed(LoanProductConstants.minimumGapBetweenInstallments, + element); + baseDataValidator.reset().parameter(LoanProductConstants.minimumGapBetweenInstallments) + .value(minimumGapBetweenInstallments).notNull(); + } else { + baseDataValidator + .reset() + .parameter(LoanProductConstants.minimumGapBetweenInstallments) + .failWithCode("is.mandatory.when.allowVariableInstallments.is.true", + "minimumGap param is mandatory when allowVariableInstallments is true"); + } + + if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.maximumGapBetweenInstallments, element)) { + final Long maximumGapBetweenInstallments = this.fromApiJsonHelper.extractLongNamed( + LoanProductConstants.maximumGapBetweenInstallments, element); + baseDataValidator.reset().parameter(LoanProductConstants.minimumGapBetweenInstallments) + .value(maximumGapBetweenInstallments).notNull(); + baseDataValidator.reset().parameter(LoanProductConstants.maximumGapBetweenInstallments) + .value(maximumGapBetweenInstallments).notNull().longGreaterThanNumber(minimumGapBetweenInstallments); + } + + } else { + if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.minimumGapBetweenInstallments, element)) { + baseDataValidator + .reset() + .parameter(LoanProductConstants.minimumGapBetweenInstallments) + .failWithCode("not.supported.when.allowVariableInstallments.is.false", + "minimumGap param is not supported when allowVariableInstallments is not supplied or false"); + } + + if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.maximumGapBetweenInstallments, element)) { + baseDataValidator + .reset() + .parameter(LoanProductConstants.maximumGapBetweenInstallments) + .failWithCode("not.supported.when.allowVariableInstallments.is.false", + "maximumGap param is not supported when allowVariableInstallments is not supplied or false"); + } + + } + } + private void validateLoanConfigurableAttributes(final DataValidatorBuilder baseDataValidator, final JsonElement element) { if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.allowAttributeOverridesParamName, element)) { @@ -752,13 +837,13 @@ public final class LoanProductDataValidator { LoanProductConstants.recalculationCompoundingFrequencyIntervalParameterName, element)) { final Integer recurrenceInterval = this.fromApiJsonHelper.extractIntegerNamed( LoanProductConstants.recalculationCompoundingFrequencyIntervalParameterName, element, Locale.getDefault()); - Integer repaymentEvery = null; + Integer repaymentEvery = null; if (loanProduct == null) { repaymentEvery = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("repaymentEvery", element); } else { repaymentEvery = loanProduct.getLoanProductRelatedDetail().getRepayEvery(); } - + baseDataValidator.reset().parameter(LoanProductConstants.recalculationCompoundingFrequencyIntervalParameterName) .value(recurrenceInterval).notNull().integerInMultiplesOfNumber(repaymentEvery); } @@ -989,82 +1074,99 @@ public final class LoanProductDataValidator { } } - //interest rates + // interest rates boolean isLinkedToFloatingInterestRates = loanProduct.isLinkedToFloatingInterestRate(); - if(this.fromApiJsonHelper.parameterExists("isLinkedToFloatingInterestRates", element)){ - isLinkedToFloatingInterestRates = this.fromApiJsonHelper.extractBooleanNamed("isLinkedToFloatingInterestRates", element); + if (this.fromApiJsonHelper.parameterExists("isLinkedToFloatingInterestRates", element)) { + isLinkedToFloatingInterestRates = this.fromApiJsonHelper.extractBooleanNamed("isLinkedToFloatingInterestRates", element); } - if(isLinkedToFloatingInterestRates){ - if(this.fromApiJsonHelper.parameterExists("interestRatePerPeriod", element)){ - baseDataValidator.reset().parameter("interestRatePerPeriod").failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.true", - "interestRatePerPeriod param is not supported when isLinkedToFloatingInterestRates is true"); + if (isLinkedToFloatingInterestRates) { + if (this.fromApiJsonHelper.parameterExists("interestRatePerPeriod", element)) { + baseDataValidator + .reset() + .parameter("interestRatePerPeriod") + .failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.true", + "interestRatePerPeriod param is not supported when isLinkedToFloatingInterestRates is true"); } - - if(this.fromApiJsonHelper.parameterExists("minInterestRatePerPeriod", element)){ - baseDataValidator.reset().parameter("minInterestRatePerPeriod").failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.true", - "minInterestRatePerPeriod param is not supported when isLinkedToFloatingInterestRates is true"); + + if (this.fromApiJsonHelper.parameterExists("minInterestRatePerPeriod", element)) { + baseDataValidator + .reset() + .parameter("minInterestRatePerPeriod") + .failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.true", + "minInterestRatePerPeriod param is not supported when isLinkedToFloatingInterestRates is true"); } - - if(this.fromApiJsonHelper.parameterExists("maxInterestRatePerPeriod", element)){ - baseDataValidator.reset().parameter("maxInterestRatePerPeriod").failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.true", - "maxInterestRatePerPeriod param is not supported when isLinkedToFloatingInterestRates is true"); + + if (this.fromApiJsonHelper.parameterExists("maxInterestRatePerPeriod", element)) { + baseDataValidator + .reset() + .parameter("maxInterestRatePerPeriod") + .failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.true", + "maxInterestRatePerPeriod param is not supported when isLinkedToFloatingInterestRates is true"); } - - if(this.fromApiJsonHelper.parameterExists("interestRateFrequencyType", element)){ - baseDataValidator.reset().parameter("interestRateFrequencyType").failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.true", - "interestRateFrequencyType param is not supported when isLinkedToFloatingInterestRates is true"); + + if (this.fromApiJsonHelper.parameterExists("interestRateFrequencyType", element)) { + baseDataValidator + .reset() + .parameter("interestRateFrequencyType") + .failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.true", + "interestRateFrequencyType param is not supported when isLinkedToFloatingInterestRates is true"); } - - Integer interestType = this.fromApiJsonHelper.parameterExists("interestType", element) - ? this.fromApiJsonHelper.extractIntegerNamed("interestType", element, Locale.getDefault()) - : loanProduct.getLoanProductRelatedDetail().getInterestMethod().getValue(); - if((interestType == null || interestType != InterestMethod.DECLINING_BALANCE.getValue()) - || (isInterestRecalculationEnabled == null || isInterestRecalculationEnabled == false)){ - baseDataValidator.reset().parameter("isLinkedToFloatingInterestRates").failWithCode("supported.only.for.declining.balance.interest.recalculation.enabled", - "Floating interest rates are supported only for declining balance and interest recalculation enabled loan products"); - } - - Long floatingRatesId = loanProduct.getFloatingRates() == null? null: loanProduct.getFloatingRates().getFloatingRate().getId(); - if(this.fromApiJsonHelper.parameterExists("floatingRatesId", element)){ + + Integer interestType = this.fromApiJsonHelper.parameterExists("interestType", element) ? this.fromApiJsonHelper + .extractIntegerNamed("interestType", element, Locale.getDefault()) : loanProduct.getLoanProductRelatedDetail() + .getInterestMethod().getValue(); + if ((interestType == null || interestType != InterestMethod.DECLINING_BALANCE.getValue()) + || (isInterestRecalculationEnabled == null || isInterestRecalculationEnabled == false)) { + baseDataValidator + .reset() + .parameter("isLinkedToFloatingInterestRates") + .failWithCode("supported.only.for.declining.balance.interest.recalculation.enabled", + "Floating interest rates are supported only for declining balance and interest recalculation enabled loan products"); + } + + Long floatingRatesId = loanProduct.getFloatingRates() == null ? null : loanProduct.getFloatingRates().getFloatingRate().getId(); + if (this.fromApiJsonHelper.parameterExists("floatingRatesId", element)) { floatingRatesId = this.fromApiJsonHelper.extractLongNamed("floatingRatesId", element); - } + } baseDataValidator.reset().parameter("floatingRatesId").value(floatingRatesId).notNull(); - BigDecimal interestRateDifferential = loanProduct.getFloatingRates() == null? null: loanProduct.getFloatingRates().getInterestRateDifferential(); - if(this.fromApiJsonHelper.parameterExists("interestRateDifferential", element)){ + BigDecimal interestRateDifferential = loanProduct.getFloatingRates() == null ? null : loanProduct.getFloatingRates() + .getInterestRateDifferential(); + if (this.fromApiJsonHelper.parameterExists("interestRateDifferential", element)) { interestRateDifferential = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("interestRateDifferential", element); - } - baseDataValidator.reset().parameter("interestRateDifferential").value(interestRateDifferential).notNull().zeroOrPositiveAmount(); + } + baseDataValidator.reset().parameter("interestRateDifferential").value(interestRateDifferential).notNull() + .zeroOrPositiveAmount(); final String minDifferentialLendingRateParameterName = "minDifferentialLendingRate"; - BigDecimal minDifferentialLendingRate = loanProduct.getFloatingRates() == null - ? null:loanProduct.getFloatingRates().getMinDifferentialLendingRate(); + BigDecimal minDifferentialLendingRate = loanProduct.getFloatingRates() == null ? null : loanProduct.getFloatingRates() + .getMinDifferentialLendingRate(); if (this.fromApiJsonHelper.parameterExists(minDifferentialLendingRateParameterName, element)) { - minDifferentialLendingRate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(minDifferentialLendingRateParameterName, - element); + minDifferentialLendingRate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed( + minDifferentialLendingRateParameterName, element); } baseDataValidator.reset().parameter(minDifferentialLendingRateParameterName).value(minDifferentialLendingRate).notNull() - .zeroOrPositiveAmount(); + .zeroOrPositiveAmount(); final String defaultDifferentialLendingRateParameterName = "defaultDifferentialLendingRate"; - BigDecimal defaultDifferentialLendingRate = loanProduct.getFloatingRates() == null - ? null:loanProduct.getFloatingRates().getDefaultDifferentialLendingRate(); + BigDecimal defaultDifferentialLendingRate = loanProduct.getFloatingRates() == null ? null : loanProduct.getFloatingRates() + .getDefaultDifferentialLendingRate(); if (this.fromApiJsonHelper.parameterExists(defaultDifferentialLendingRateParameterName, element)) { - defaultDifferentialLendingRate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(defaultDifferentialLendingRateParameterName, - element); + defaultDifferentialLendingRate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed( + defaultDifferentialLendingRateParameterName, element); } - baseDataValidator.reset().parameter(defaultDifferentialLendingRateParameterName).value(defaultDifferentialLendingRate).notNull() - .zeroOrPositiveAmount(); + baseDataValidator.reset().parameter(defaultDifferentialLendingRateParameterName).value(defaultDifferentialLendingRate) + .notNull().zeroOrPositiveAmount(); final String maxDifferentialLendingRateParameterName = "maxDifferentialLendingRate"; - BigDecimal maxDifferentialLendingRate = loanProduct.getFloatingRates() == null - ? null:loanProduct.getFloatingRates().getMaxDifferentialLendingRate(); + BigDecimal maxDifferentialLendingRate = loanProduct.getFloatingRates() == null ? null : loanProduct.getFloatingRates() + .getMaxDifferentialLendingRate(); if (this.fromApiJsonHelper.parameterExists(maxDifferentialLendingRateParameterName, element)) { - maxDifferentialLendingRate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(maxDifferentialLendingRateParameterName, - element); + maxDifferentialLendingRate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed( + maxDifferentialLendingRateParameterName, element); } baseDataValidator.reset().parameter(maxDifferentialLendingRateParameterName).value(maxDifferentialLendingRate).notNull() - .zeroOrPositiveAmount(); + .zeroOrPositiveAmount(); if (defaultDifferentialLendingRate != null && defaultDifferentialLendingRate.compareTo(BigDecimal.ZERO) != -1) { if (minDifferentialLendingRate != null && minDifferentialLendingRate.compareTo(BigDecimal.ZERO) != -1) { @@ -1072,60 +1174,78 @@ public final class LoanProductDataValidator { .notLessThanMin(minDifferentialLendingRate); } } - + if (maxDifferentialLendingRate != null && maxDifferentialLendingRate.compareTo(BigDecimal.ZERO) != -1) { if (minDifferentialLendingRate != null && minDifferentialLendingRate.compareTo(BigDecimal.ZERO) != -1) { baseDataValidator.reset().parameter("maxDifferentialLendingRate").value(maxDifferentialLendingRate) .notLessThanMin(minDifferentialLendingRate); } } - + if (maxDifferentialLendingRate != null && maxDifferentialLendingRate.compareTo(BigDecimal.ZERO) != -1) { if (defaultDifferentialLendingRate != null && defaultDifferentialLendingRate.compareTo(BigDecimal.ZERO) != -1) { baseDataValidator.reset().parameter("maxDifferentialLendingRate").value(maxDifferentialLendingRate) .notLessThanMin(defaultDifferentialLendingRate); } } - - Boolean isFloatingInterestRateCalculationAllowed = loanProduct.getFloatingRates() == null ? null - : loanProduct.getFloatingRates().isFloatingInterestRateCalculationAllowed(); - if(this.fromApiJsonHelper.parameterExists("isFloatingInterestRateCalculationAllowed", element)){ + + Boolean isFloatingInterestRateCalculationAllowed = loanProduct.getFloatingRates() == null ? null : loanProduct + .getFloatingRates().isFloatingInterestRateCalculationAllowed(); + if (this.fromApiJsonHelper.parameterExists("isFloatingInterestRateCalculationAllowed", element)) { isFloatingInterestRateCalculationAllowed = this.fromApiJsonHelper.extractBooleanNamed( - "isFloatingInterestRateCalculationAllowed", element); + "isFloatingInterestRateCalculationAllowed", element); } - baseDataValidator.reset().parameter("isFloatingInterestRateCalculationAllowed") - .value(isFloatingInterestRateCalculationAllowed).notNull().isOneOfTheseValues(true, false); + baseDataValidator.reset().parameter("isFloatingInterestRateCalculationAllowed").value(isFloatingInterestRateCalculationAllowed) + .notNull().isOneOfTheseValues(true, false); } else { - if(this.fromApiJsonHelper.parameterExists("floatingRatesId", element)){ - baseDataValidator.reset().parameter("floatingRatesId").failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.false", - "floatingRatesId param is not supported when isLinkedToFloatingInterestRates is not supplied or false"); + if (this.fromApiJsonHelper.parameterExists("floatingRatesId", element)) { + baseDataValidator + .reset() + .parameter("floatingRatesId") + .failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.false", + "floatingRatesId param is not supported when isLinkedToFloatingInterestRates is not supplied or false"); } - - if(this.fromApiJsonHelper.parameterExists("interestRateDifferential", element)){ - baseDataValidator.reset().parameter("interestRateDifferential").failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.false", - "interestRateDifferential param is not supported when isLinkedToFloatingInterestRates is not supplied or false"); + + if (this.fromApiJsonHelper.parameterExists("interestRateDifferential", element)) { + baseDataValidator + .reset() + .parameter("interestRateDifferential") + .failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.false", + "interestRateDifferential param is not supported when isLinkedToFloatingInterestRates is not supplied or false"); } - - if(this.fromApiJsonHelper.parameterExists("minDifferentialLendingRate", element)){ - baseDataValidator.reset().parameter("minDifferentialLendingRate").failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.false", - "minDifferentialLendingRate param is not supported when isLinkedToFloatingInterestRates is not supplied or false"); + + if (this.fromApiJsonHelper.parameterExists("minDifferentialLendingRate", element)) { + baseDataValidator + .reset() + .parameter("minDifferentialLendingRate") + .failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.false", + "minDifferentialLendingRate param is not supported when isLinkedToFloatingInterestRates is not supplied or false"); } - - if(this.fromApiJsonHelper.parameterExists("defaultDifferentialLendingRate", element)){ - baseDataValidator.reset().parameter("defaultDifferentialLendingRate").failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.false", - "defaultDifferentialLendingRate param is not supported when isLinkedToFloatingInterestRates is not supplied or false"); + + if (this.fromApiJsonHelper.parameterExists("defaultDifferentialLendingRate", element)) { + baseDataValidator + .reset() + .parameter("defaultDifferentialLendingRate") + .failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.false", + "defaultDifferentialLendingRate param is not supported when isLinkedToFloatingInterestRates is not supplied or false"); } - - if(this.fromApiJsonHelper.parameterExists("maxDifferentialLendingRate", element)){ - baseDataValidator.reset().parameter("maxDifferentialLendingRate").failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.false", - "maxDifferentialLendingRate param is not supported when isLinkedToFloatingInterestRates is not supplied or false"); + + if (this.fromApiJsonHelper.parameterExists("maxDifferentialLendingRate", element)) { + baseDataValidator + .reset() + .parameter("maxDifferentialLendingRate") + .failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.false", + "maxDifferentialLendingRate param is not supported when isLinkedToFloatingInterestRates is not supplied or false"); } - - if(this.fromApiJsonHelper.parameterExists("isFloatingInterestRateCalculationAllowed", element)){ - baseDataValidator.reset().parameter("isFloatingInterestRateCalculationAllowed").failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.false", - "isFloatingInterestRateCalculationAllowed param is not supported when isLinkedToFloatingInterestRates is not supplied or false"); + + if (this.fromApiJsonHelper.parameterExists("isFloatingInterestRateCalculationAllowed", element)) { + baseDataValidator + .reset() + .parameter("isFloatingInterestRateCalculationAllowed") + .failWithCode("not.supported.when.isLinkedToFloatingInterestRates.is.false", + "isFloatingInterestRateCalculationAllowed param is not supported when isLinkedToFloatingInterestRates is not supplied or false"); } - + final String minInterestRatePerPeriodParameterName = "minInterestRatePerPeriod"; BigDecimal minInterestRatePerPeriod = loanProduct.getMinNominalInterestRatePerPeriod(); if (this.fromApiJsonHelper.parameterExists(minInterestRatePerPeriodParameterName, element)) { @@ -1133,7 +1253,7 @@ public final class LoanProductDataValidator { element); } baseDataValidator.reset().parameter(minInterestRatePerPeriodParameterName).value(minInterestRatePerPeriod).ignoreIfNull() - .zeroOrPositiveAmount(); + .zeroOrPositiveAmount(); final String maxInterestRatePerPeriodParameterName = "maxInterestRatePerPeriod"; BigDecimal maxInterestRatePerPeriod = loanProduct.getMaxNominalInterestRatePerPeriod(); @@ -1142,12 +1262,11 @@ public final class LoanProductDataValidator { element); } baseDataValidator.reset().parameter(maxInterestRatePerPeriodParameterName).value(maxInterestRatePerPeriod).ignoreIfNull() - .zeroOrPositiveAmount(); + .zeroOrPositiveAmount(); BigDecimal interestRatePerPeriod = loanProduct.getLoanProductRelatedDetail().getNominalInterestRatePerPeriod(); if (this.fromApiJsonHelper.parameterExists("interestRatePerPeriod", element)) { - interestRatePerPeriod = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("interestRatePerPeriod", - element); + interestRatePerPeriod = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("interestRatePerPeriod", element); } baseDataValidator.reset().parameter("interestRatePerPeriod").value(interestRatePerPeriod).notNull().zeroOrPositiveAmount(); @@ -1275,6 +1394,7 @@ public final class LoanProductDataValidator { // validateLoanConfigurableAttributes(baseDataValidator,element); + validateVariableInstallmentSettings(baseDataValidator, element); throwExceptionIfValidationWarningsExist(dataValidationErrors); } @@ -1556,11 +1676,8 @@ public final class LoanProductDataValidator { private void validateNominalInterestRatePerPeriodMinMaxConstraint(final JsonElement element, final LoanProduct loanProduct, final DataValidatorBuilder baseDataValidator) { - if((this.fromApiJsonHelper.parameterExists("isLinkedToFloatingInterestRates", element) - && this.fromApiJsonHelper.extractBooleanNamed("isLinkedToFloatingInterestRates", element) == true) - || loanProduct.isLinkedToFloatingInterestRate()){ - return; - } + if ((this.fromApiJsonHelper.parameterExists("isLinkedToFloatingInterestRates", element) && this.fromApiJsonHelper + .extractBooleanNamed("isLinkedToFloatingInterestRates", element) == true) || loanProduct.isLinkedToFloatingInterestRate()) { return; } boolean iRPUpdated = false; boolean minIRPUpdated = false; boolean maxIRPUpdated = false; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanEnumerations.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanEnumerations.java index 7ebdf4153..11852a71c 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanEnumerations.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanEnumerations.java @@ -514,7 +514,23 @@ public class LoanEnumerations { case INTEREST_RATE: optionData = new EnumOptionData(LoanTermVariationType.INTEREST_RATE.getValue().longValue(), LoanTermVariationType.INTEREST_RATE.getCode(), "interestRate"); - break; + break; + case DELETE_INSTALLMENT: + optionData = new EnumOptionData(LoanTermVariationType.DELETE_INSTALLMENT.getValue().longValue(), + LoanTermVariationType.DELETE_INSTALLMENT.getCode(), "deleteInstallment"); + break; + case DUE_DATE: + optionData = new EnumOptionData(LoanTermVariationType.DUE_DATE.getValue().longValue(), + LoanTermVariationType.DUE_DATE.getCode(), "dueDate"); + break; + case INSERT_INSTALLMENT: + optionData = new EnumOptionData(LoanTermVariationType.INSERT_INSTALLMENT.getValue().longValue(), + LoanTermVariationType.DUE_DATE.getCode(), "insertInstallment"); + break; + case PRINCIPAL_AMOUNT: + optionData = new EnumOptionData(LoanTermVariationType.PRINCIPAL_AMOUNT.getValue().longValue(), + LoanTermVariationType.PRINCIPAL_AMOUNT.getCode(), "principalAmount"); + break; default: optionData = new EnumOptionData(LoanTermVariationType.INVALID.getValue().longValue(), LoanTermVariationType.INVALID.getCode(), "Invalid"); 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 8fcf3b1d6..f4372dc9a 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 @@ -200,7 +200,10 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo + "lfr.min_differential_lending_rate as minDifferentialLendingRate, " + "lfr.default_differential_lending_rate as defaultDifferentialLendingRate, " + "lfr.max_differential_lending_rate as maxDifferentialLendingRate, " - + "lfr.is_floating_interest_rate_calculation_allowed as isFloatingInterestRateCalculationAllowed " + + "lfr.is_floating_interest_rate_calculation_allowed as isFloatingInterestRateCalculationAllowed, " + + "lp.allow_variabe_installments as isVariableIntallmentsAllowed, " + + "lvi.minimum_gap as minimumGap, " + + "lvi.maximum_gap as maximumGap " + " from m_product_loan lp " + " left join m_fund f on f.id = lp.fund_id " + " left join m_product_loan_recalculation_details lpr on lpr.product_id=lp.id " @@ -209,7 +212,9 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo + " left join m_product_loan_configurable_attributes lca on lca.loan_product_id = lp.id " + " left join m_product_loan_floating_rates as lfr on lfr.loan_product_id = lp.id " + " left join m_floating_rates as fr on lfr.floating_rates_id = fr.id " + + " left join m_product_loan_variable_installment_config as lvi on lvi.loan_product_id = lp.id " + " join m_currency curr on curr.code = lp.currency_code"; + } @Override @@ -268,7 +273,11 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo final BigDecimal defaultDifferentialLendingRate = rs.getBigDecimal("defaultDifferentialLendingRate"); final BigDecimal maxDifferentialLendingRate = rs.getBigDecimal("maxDifferentialLendingRate"); final boolean isFloatingInterestRateCalculationAllowed = rs.getBoolean("isFloatingInterestRateCalculationAllowed"); - + + final boolean isVariableIntallmentsAllowed = rs.getBoolean("isVariableIntallmentsAllowed"); + final Integer minimumGap = rs.getInt("minimumGap"); + final Integer maximumGap = rs.getInt("maximumGap"); + final int repaymentFrequencyTypeId = JdbcSupport.getInteger(rs, "repaymentPeriodFrequency"); final EnumOptionData repaymentFrequencyType = LoanEnumerations.repaymentFrequencyType(repaymentFrequencyTypeId); @@ -341,8 +350,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo final Integer compoundingFrequencyEnumValue = JdbcSupport.getInteger(rs, "compoundingFrequencyTypeEnum"); EnumOptionData compoundingFrequencyType = null; if (compoundingFrequencyEnumValue != null) { - compoundingFrequencyType = LoanEnumerations - .interestRecalculationFrequencyType(compoundingFrequencyEnumValue); + compoundingFrequencyType = LoanEnumerations.interestRecalculationFrequencyType(compoundingFrequencyEnumValue); } final Integer compoundingInterval = JdbcSupport.getInteger(rs, "compoundingInterval"); final LocalDate compoundingFrequencyDate = JdbcSupport.getLocalDate(rs, "compoundingFrequencyDate"); @@ -397,8 +405,9 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo interestRecalculationData, minimumDaysBetweenDisbursalAndFirstRepayment, holdGuaranteeFunds, loanProductGuaranteeData, principalThresholdForLastInstallment, accountMovesOutOfNPAOnlyOnArrearsCompletion, canDefineInstallmentAmount, installmentAmountInMultiplesOf, allowAttributeOverrides, isLinkedToFloatingInterestRates, floatingRateId, - floatingRateName, interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate, - maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed); + floatingRateName, interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate, + maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableIntallmentsAllowed, minimumGap, + maximumGap); } } diff --git a/mifosng-provider/src/main/resources/sql/migrations/core_db/V283__Variable_Installments.sql b/mifosng-provider/src/main/resources/sql/migrations/core_db/V283__Variable_Installments.sql new file mode 100644 index 000000000..924ce1d7a --- /dev/null +++ b/mifosng-provider/src/main/resources/sql/migrations/core_db/V283__Variable_Installments.sql @@ -0,0 +1,26 @@ +ALTER TABLE `m_product_loan` +ADD COLUMN `allow_variabe_installments` BIT(1) NOT NULL DEFAULT 0 AFTER `is_linked_to_floating_interest_rates` ; + +CREATE TABLE `m_product_loan_variable_installment_config` ( + `id` BIGINT(20) NOT NULL AUTO_INCREMENT, + `loan_product_id` BIGINT(20) NOT NULL, + `minimum_gap` INT(4) NOT NULL, + `maximum_gap` INT(4) NOT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `FK_mappings_m_variable_product_loan_id` FOREIGN KEY (`loan_product_id`) REFERENCES `m_product_loan` (`id`) +); + +INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) VALUES ('portfolio', 'CREATESCHEDULEEXCEPTIONS_LOAN', 'LOAN', 'CREATESCHEDULEEXCEPTIONS', 0), ('portfolio', 'CREATESCHEDULEEXCEPTIONS_LOAN_CHECKER', 'LOAN', 'CREATESCHEDULEEXCEPTIONS_CHECKER', 0), ('portfolio', 'DELETESCHEDULEEXCEPTIONS_LOAN', 'LOAN', 'DELETESCHEDULEEXCEPTIONS', 0),('portfolio', 'DELETESCHEDULEEXCEPTIONS_LOAN_CHECKER', 'LOAN', 'DELETESCHEDULEEXCEPTIONS_CHECKER', 0); + + +ALTER TABLE `m_loan_term_variations` + ALTER `applicable_from` DROP DEFAULT, + ALTER `term_value` DROP DEFAULT; +ALTER TABLE `m_loan_term_variations` + CHANGE COLUMN `applicable_from` `applicable_date` DATE NOT NULL AFTER `term_type`, + CHANGE COLUMN `term_value` `decimal_value` DECIMAL(19,6) NULL AFTER `applicable_date`, + ADD COLUMN `date_value` DATE NULL AFTER `decimal_value`, + ADD COLUMN `is_specific_to_installment` TINYINT NOT NULL DEFAULT '0' AFTER `date_value`, + ADD COLUMN `applied_on_loan_status` SMALLINT(5) NOT NULL AFTER `is_specific_to_installment`; + +UPDATE `m_loan_term_variations` SET `applied_on_loan_status`=300; \ No newline at end of file