[FINERACT-167] Loan foreclosure implementation and integration test cases for the same.
Project: http://git-wip-us.apache.org/repos/asf/incubator-fineract/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-fineract/commit/b9b345a5 Tree: http://git-wip-us.apache.org/repos/asf/incubator-fineract/tree/b9b345a5 Diff: http://git-wip-us.apache.org/repos/asf/incubator-fineract/diff/b9b345a5 Branch: refs/heads/develop Commit: b9b345a563a5e14583fb4fbac7710addfc422af1 Parents: b43d3ef Author: sachinkulkarni12 <sachin.kulka...@confluxtechnologies.com> Authored: Fri Jun 10 18:29:02 2016 +0530 Committer: sachinkulkarni12 <sachin.kulka...@confluxtechnologies.com> Committed: Fri Jun 10 18:59:26 2016 +0530 ---------------------------------------------------------------------- api-docs/apiLive.htm | 51 ++++ .../ClientLoanIntegrationTest.java | 53 ++++ .../common/loans/LoanStatusChecker.java | 14 + .../common/loans/LoanTransactionHelper.java | 17 ++ .../commands/service/CommandWrapperBuilder.java | 9 + .../loanaccount/api/LoanApiConstants.java | 4 + .../api/LoanTransactionsApiResource.java | 11 + .../loanaccount/data/LoanAccountData.java | 40 +-- .../portfolio/loanaccount/domain/Loan.java | 259 ++++++++++++++++++- .../domain/LoanAccountDomainService.java | 2 + .../domain/LoanAccountDomainServiceJpa.java | 112 +++++++- .../portfolio/loanaccount/domain/LoanEvent.java | 3 +- .../LoanRepaymentScheduleInstallment.java | 6 + .../loanaccount/domain/LoanSubStatus.java | 80 ++++++ .../loanaccount/domain/LoanTransaction.java | 117 +++++---- ...anRepaymentScheduleTransactionProcessor.java | 86 ++++++ ...anRepaymentScheduleTransactionProcessor.java | 3 + .../exception/LoanForeclosureException.java | 28 ++ .../handler/ForeClosureCommandHandler.java | 45 ++++ .../LoanEventApiJsonValidator.java | 22 ++ .../service/LoanReadPlatformService.java | 3 + .../service/LoanReadPlatformServiceImpl.java | 42 ++- .../service/LoanWritePlatformService.java | 4 +- ...anWritePlatformServiceJpaRepositoryImpl.java | 34 +++ .../core_db/V311__foreclosure_details.sql | 7 + 25 files changed, 974 insertions(+), 78 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-fineract/blob/b9b345a5/api-docs/apiLive.htm ---------------------------------------------------------------------- diff --git a/api-docs/apiLive.htm b/api-docs/apiLive.htm index a6320c8..696e066 100644 --- a/api-docs/apiLive.htm +++ b/api-docs/apiLive.htm @@ -1304,6 +1304,14 @@ <td></td> <td></td> </tr> + <tr> + <td></td> + <td>loans/{loanId}/transactions?command=foreclosure</td> + <td><a href="#loans_transaction_foreclosure">Foreclose an Active Loan</a></td> + <td></td> + <td></td> + <td></td> + </tr> <tr> <td><a href="#loans_charges">Loan Charges</a></td> <td>loans/{loanId}/charges</td> @@ -10141,6 +10149,13 @@ No Request Body: "total" "amount" is set to the total amount paid in advance. </dd> + <dd> + <b>'foreclosure'</b><br> "transaction date" is set to the current date by default<br> + "transaction amount" is set + to the sum of total loan outstanding principal + and total Interest/ Fee/ Charges / Penalties + till foreclosure date. + </dd> </dl> <p>Example Request:</p> <div class=apiClick>loans/1/transactions/template?command=repayment</div> @@ -10266,6 +10281,42 @@ Request Body: </div> </div> + <a id="loans_transaction_foreclosure" name="loans_transaction_foreclosure" class="old-syle-anchor"> </a> + <div class="method-section"> + <div class="method-description"> + <h4>Foreclosure of an Active Loan</h4> + </div> + <div class="method-example"> + <code class="method-declaration"> +POST https://DomainName/api/v1/loans/{loanId}/transactions?command=foreclosure + </code> + <code class="method-request"> +POST loans/5/transactions?command=foreclosure +Content-Type: application/json +Request Body: +{ + "dateFormat": "dd MMMM yyyy", + "locale": "en", + "transactionDate": "10 December 2012", + "note": "Customer Death" +} + </code> + <code class="method-response"> +{ + "changes": { + "note": "Foreclosure Made!!!", + "eventAmount": 7573.76, + "transactionDate": [2012, + 12, + 10], + "transactions": [15818] + }, + "loanId": 5 +} + </code> + </div> + </div> + <a id="loans_transaction_waiveinterest" name="loans_transaction_waiveinterest" class="old-syle-anchor"> </a> <div class="method-section"> http://git-wip-us.apache.org/repos/asf/incubator-fineract/blob/b9b345a5/fineract-provider/src/integrationTest/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/integrationTest/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java b/fineract-provider/src/integrationTest/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java index 5696143..9a241a2 100644 --- a/fineract-provider/src/integrationTest/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java +++ b/fineract-provider/src/integrationTest/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java @@ -4956,6 +4956,59 @@ public class ClientLoanIntegrationTest { this.validateIfValuesAreNotOverridden(loanID, loanProductID); } + /** + * Test case to verify Loan Foreclosure. + */ + @Test + public void testLoanForeclosure() { + this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec); + + final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec); + ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientID); + final Integer loanProductID = createLoanProduct(false, NONE); + + List<HashMap> charges = new ArrayList<>(); + + Integer flatAmountChargeOne = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "50", false)); + addCharges(charges, flatAmountChargeOne, "50", "01 October 2011"); + Integer flatAmountChargeTwo = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "100", true)); + addCharges(charges, flatAmountChargeTwo, "100", "15 December 2011"); + + final Integer loanID = applyForLoanApplication(clientID, loanProductID, charges, null, "10,000.00"); + Assert.assertNotNull(loanID); + + HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, loanID); + LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); + + System.out.println("----------------------------------- APPROVE LOAN -----------------------------------------"); + loanStatusHashMap = this.loanTransactionHelper.approveLoan("20 September 2011", loanID); + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap); + + System.out.println("----------------------------------- DISBURSE LOAN ----------------------------------------"); + loanStatusHashMap = this.loanTransactionHelper.disburseLoan("20 September 2011", loanID, "10,000.00"); + System.out.println("DISBURSE " + loanStatusHashMap); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + + System.out.println("---------------------------------- Make repayment 1 --------------------------------------"); + this.loanTransactionHelper.makeRepayment("20 October 2011", Float.valueOf("2676.24"), loanID); + + System.out.println("---------------------------------- FORECLOSE LOAN ----------------------------------------"); + this.loanTransactionHelper.forecloseLoan("08 November 2011", loanID); + + // retrieving the loan status + loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, loanID); + // verifying the loan status is closed + LoanStatusChecker.verifyLoanAccountIsClosed(loanStatusHashMap); + // retrieving the loan sub-status + loanStatusHashMap = LoanStatusChecker.getSubStatusOfLoan(this.requestSpec, this.responseSpec, loanID); + // verifying the loan sub-status is foreclosed + LoanStatusChecker.verifyLoanAccountForeclosed(loanStatusHashMap); + + } + private void validateIfValuesAreNotOverridden(Integer loanID, Integer loanProductID) { String loanProductDetails = this.loanTransactionHelper.getLoanProductDetails(this.requestSpec, this.responseSpec, loanProductID); String loanDetails = this.loanTransactionHelper.getLoanDetails(this.requestSpec, this.responseSpec, loanID); http://git-wip-us.apache.org/repos/asf/incubator-fineract/blob/b9b345a5/fineract-provider/src/integrationTest/java/org/apache/fineract/integrationtests/common/loans/LoanStatusChecker.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/integrationTest/java/org/apache/fineract/integrationtests/common/loans/LoanStatusChecker.java b/fineract-provider/src/integrationTest/java/org/apache/fineract/integrationtests/common/loans/LoanStatusChecker.java index 3535dfe..72d9ce8 100644 --- a/fineract-provider/src/integrationTest/java/org/apache/fineract/integrationtests/common/loans/LoanStatusChecker.java +++ b/fineract-provider/src/integrationTest/java/org/apache/fineract/integrationtests/common/loans/LoanStatusChecker.java @@ -20,6 +20,7 @@ package org.apache.fineract.integrationtests.common.loans; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertEquals; import java.util.HashMap; @@ -59,14 +60,27 @@ public class LoanStatusChecker { assertTrue(getStatus(loanStatusHashMap, "overpaid")); } + public static void verifyLoanAccountForeclosed(final HashMap loanSubStatusHashMap) { + assertEquals("Foreclosed", getSubStatus(loanSubStatusHashMap, "value")); + } + public static HashMap<String, Object> getStatusOfLoan(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, final Integer loanID) { final String url = "/fineract-provider/api/v1/loans/" + loanID + "?" + Utils.TENANT_IDENTIFIER; return Utils.performServerGet(requestSpec, responseSpec, url, "status"); } + public static HashMap<String, Object> getSubStatusOfLoan(final RequestSpecification requestSpec, + final ResponseSpecification responseSpec, final Integer loanID) { + final String url = "/fineract-provider/api/v1/loans/" + loanID + "?" + Utils.TENANT_IDENTIFIER; + return Utils.performServerGet(requestSpec, responseSpec, url, "subStatus"); + } + private static boolean getStatus(final HashMap loanStatusMap, final String nameOfLoanStatusString) { return (Boolean) loanStatusMap.get(nameOfLoanStatusString); } + private static String getSubStatus(final HashMap loanStatusMap, final String nameOfLoanStatusString) { + return (String) loanStatusMap.get(nameOfLoanStatusString); + } } http://git-wip-us.apache.org/repos/asf/incubator-fineract/blob/b9b345a5/fineract-provider/src/integrationTest/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/integrationTest/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java b/fineract-provider/src/integrationTest/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java index f416bfc..4f2c8cb 100755 --- a/fineract-provider/src/integrationTest/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java +++ b/fineract-provider/src/integrationTest/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java @@ -52,6 +52,7 @@ public class LoanTransactionHelper { private static final String WITHDRAW_LOAN_APPLICATION_COMMAND = "withdrawnByApplicant"; private static final String RECOVER_FROM_GUARANTORS_COMMAND = "recoverGuarantees"; private static final String MAKE_REFUND_BY_CASH_COMMAND = "refundByCash"; + private static final String FORECLOSURE_COMMAND = "foreclosure"; public LoanTransactionHelper(final RequestSpecification requestSpec, final ResponseSpecification responseSpec) { this.requestSpec = requestSpec; @@ -217,6 +218,11 @@ public class LoanTransactionHelper { getRepaymentBodyAsJSON(date, amountToBePaid), ""); } + public HashMap forecloseLoan(final String transactionDate, final Integer loanID) { + return (HashMap) performLoanTransaction(createLoanTransactionURL(FORECLOSURE_COMMAND, loanID), + getForeclosureBodyAsJSON(transactionDate, loanID), ""); + } + public HashMap withdrawLoanApplicationByClient(final String date, final Integer loanID) { return performLoanTransaction(createLoanOperationURL(WITHDRAW_LOAN_APPLICATION_COMMAND, loanID), getWithdrawLoanApplicationBodyAsJSON(date)); @@ -326,6 +332,17 @@ public class LoanTransactionHelper { return new Gson().toJson(map); } + private String getForeclosureBodyAsJSON(final String transactionDate, final Integer loanId) { + final HashMap<String, Object> map = new HashMap<>(); + map.put("locale", "en"); + map.put("dateFormat", "dd MMMM yyyy"); + map.put("transactionDate", transactionDate); + map.put("note", "Foreclosure Made!!!"); + String json = new Gson().toJson(map); + System.out.println(json); + return json; + } + private String getWriteOffBodyAsJSON(final String transactionDate) { final HashMap<String, String> map = new HashMap<>(); map.put("dateFormat", "dd MMMM yyyy"); http://git-wip-us.apache.org/repos/asf/incubator-fineract/blob/b9b345a5/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index 5751e0e..3b26e1d 100755 --- a/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -723,6 +723,15 @@ public class CommandWrapperBuilder { return this; } + public CommandWrapperBuilder loanForeclosure(final Long loanId) { + this.actionName = "FORECLOSURE"; + this.entityName = "LOAN"; + this.entityId = null; + this.loanId = loanId; + this.href = "/loans/" + loanId + "/transactions?command=foreclosure"; + return this; + } + public CommandWrapperBuilder createLoanApplication() { this.actionName = "CREATE"; this.entityName = "LOAN"; http://git-wip-us.apache.org/repos/asf/incubator-fineract/blob/b9b345a5/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java index 5f28010..ee38428 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java @@ -113,4 +113,8 @@ public interface LoanApiConstants { //loan write off public static final String WRITEOFFREASONS = "WriteOffReasons"; + // fore closure constants + public static final String transactionDateParamName = "transactionDate"; + public static final String noteParamName = "note"; + } http://git-wip-us.apache.org/repos/asf/incubator-fineract/blob/b9b345a5/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java index 04603c4..e4b3036 100755 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java @@ -129,6 +129,14 @@ public class LoanTransactionsApiResource { transactionData = this.loanReadPlatformService.retrieveRefundByCashTemplate(loanId); } else if (is(commandParam, "refundbytransfer")) { transactionData = this.loanReadPlatformService.retrieveDisbursalTemplate(loanId, true); + } else if (is(commandParam, "foreclosure")) { + LocalDate transactionDate = null; + if (transactionDateParam == null) { + transactionDate = DateUtils.getLocalDateOfTenant(); + } else { + transactionDate = LocalDate.fromDateFields(transactionDateParam.getDate("transactionDate", dateFormat, locale)); + } + transactionData = this.loanReadPlatformService.retrieveLoanForeclosureTemplate(loanId, transactionDate); } else { throw new UnrecognizedQueryParamException("command", commandParam); } @@ -189,6 +197,9 @@ public class LoanTransactionsApiResource { } else if (is(commandParam, "refundByCash")) { final CommandWrapper commandRequest = builder.refundLoanTransactionByCash(loanId).build(); result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "foreclosure")) { + final CommandWrapper commandRequest = builder.loanForeclosure(loanId).build(); + result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); } if (result == null) { throw new UnrecognizedQueryParamException("command", commandParam); } http://git-wip-us.apache.org/repos/asf/incubator-fineract/blob/b9b345a5/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java index 5975ec3..555ed3b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java @@ -39,6 +39,7 @@ import org.apache.fineract.portfolio.floatingrates.data.InterestRatePeriodData; import org.apache.fineract.portfolio.fund.data.FundData; import org.apache.fineract.portfolio.group.data.GroupGeneralData; import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; +import org.apache.fineract.portfolio.loanaccount.domain.LoanSubStatus; import org.apache.fineract.portfolio.loanaccount.guarantor.data.GuarantorData; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData; import org.apache.fineract.portfolio.loanproduct.data.LoanProductBorrowerCycleVariationData; @@ -47,6 +48,7 @@ import org.apache.fineract.portfolio.loanproduct.data.TransactionProcessingStrat import org.apache.fineract.portfolio.loanproduct.domain.LoanProductConfigurableAttributes; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductValueConditionType; import org.apache.fineract.portfolio.note.data.NoteData; +import org.hibernate.dialect.Sybase11Dialect; import org.joda.time.LocalDate; import org.springframework.util.CollectionUtils; @@ -65,6 +67,7 @@ public class LoanAccountData { // status private final LoanStatusEnumData status; + private final EnumOptionData subStatus; // related to private final Long clientId; @@ -214,6 +217,7 @@ public class LoanAccountData { final Long id = null; final String accountNo = null; final LoanStatusEnumData status = null; + final EnumOptionData subStatus = null; final String externalId = null; final Long clientId = null; final String clientName = null; @@ -335,7 +339,7 @@ public class LoanAccountData { maxOutstandingLoanBalance, emiAmountVariations, memberVariations, product, inArrears, graceOnArrearsAgeing, overdueCharges, isNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, originalSchedule, createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods, isVariableInstallmentsAllowed, minimumGap, - maximumGap); + maximumGap, subStatus); } @@ -348,6 +352,7 @@ public class LoanAccountData { final Long id = null; final String accountNo = null; final LoanStatusEnumData status = null; + final EnumOptionData subStatus = null; final String externalId = null; final GroupGeneralData group = null; final EnumOptionData loanType = null; @@ -468,7 +473,7 @@ public class LoanAccountData { maxOutstandingLoanBalance, emiAmountVariations, memberVariations, product, inArrears, graceOnArrearsAgeing, overdueCharges, isNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, originalSchedule, createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods, isVariableInstallmentsAllowed, minimumGap, - maximumGap); + maximumGap, subStatus); } @@ -497,7 +502,7 @@ public class LoanAccountData { 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); + acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus); } /** @@ -509,6 +514,7 @@ public class LoanAccountData { final Long id = null; final String accountNo = null; final LoanStatusEnumData status = null; + final EnumOptionData subStatus = null; final String externalId = null; final Long clientId = null; final String clientAccountNo = null; @@ -631,7 +637,7 @@ public class LoanAccountData { maxOutstandingBalance, emiAmountVariations, memberVariations, product, inArrears, graceOnArrearsAgeing, overdueCharges, isNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, originalSchedule, createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods, isVariableInstallmentsAllowed, minimumGap, - maximumGap); + maximumGap, subStatus); } @@ -660,7 +666,7 @@ public class LoanAccountData { 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); + acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus); } @@ -678,6 +684,7 @@ public class LoanAccountData { final Long id = null; final String accountNo = null; final LoanStatusEnumData status = null; + final EnumOptionData subStatus = null; final String externalId = null; final Long clientId = null; final String clientAccountNo = null; @@ -808,7 +815,7 @@ public class LoanAccountData { product.getDaysInYearType(), product.isInterestRecalculationEnabled(), product.toLoanInterestRecalculationData(), originalSchedule, createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods, product.isVariableInstallmentsAllowed(), product.getMinimumGapBetweenInstallments(), - product.getMaximumGapBetweenInstallments()); + product.getMaximumGapBetweenInstallments(), subStatus); } public static LoanAccountData populateLoanProductDefaults(final LoanAccountData acc, final LoanProductData product) { @@ -867,7 +874,7 @@ public class LoanAccountData { product.getDaysInMonthType(), product.getDaysInYearType(), product.isInterestRecalculationEnabled(), product.toLoanInterestRecalculationData(), acc.originalSchedule, acc.createStandingInstructionAtDisbursement, paidInAdvance, acc.interestRatesPeriods, product.isVariableInstallmentsAllowed(), - product.getMinimumGapBetweenInstallments(), product.getMaximumGapBetweenInstallments()); + product.getMinimumGapBetweenInstallments(), product.getMaximumGapBetweenInstallments(), acc.subStatus); } @@ -897,7 +904,7 @@ public class LoanAccountData { final Integer graceOnArrearsAgeing, final Boolean isNPA, final EnumOptionData daysInMonthType, final EnumOptionData daysInYearType, final boolean isInterestRecalculationEnabled, final LoanInterestRecalculationData interestRecalculationData, final Boolean createStandingInstructionAtDisbursement, - final Boolean isVariableInstallmentsAllowed, Integer minimumGap, Integer maximumGap) { + final Boolean isVariableInstallmentsAllowed, Integer minimumGap, Integer maximumGap, final EnumOptionData subStatus) { final LoanScheduleData repaymentSchedule = null; final Collection<LoanTransactionData> transactions = null; @@ -952,7 +959,7 @@ public class LoanAccountData { outstandingLoanBalance, emiAmountVariations, memberVariations, product, inArrears, graceOnArrearsAgeing, overdueCharges, isNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, originalSchedule, createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods, isVariableInstallmentsAllowed, minimumGap, - maximumGap); + maximumGap, subStatus); } /* @@ -1003,7 +1010,7 @@ public class LoanAccountData { acc.graceOnArrearsAgeing, overdueCharges, acc.isNPA, acc.daysInMonthType, acc.daysInYearType, acc.isInterestRecalculationEnabled, acc.interestRecalculationData, acc.originalSchedule, acc.createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods, acc.isVariableInstallmentsAllowed, - acc.minimumGap, acc.maximumGap); + acc.minimumGap, acc.maximumGap, acc.subStatus); } public static LoanAccountData associationsAndTemplate(final LoanAccountData acc, final Collection<LoanProductData> productOptions, @@ -1044,7 +1051,7 @@ public class LoanAccountData { 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); + acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus); } public static LoanAccountData associateMemberVariations(final LoanAccountData acc, final Map<Long, Integer> memberLoanCycle) { @@ -1108,7 +1115,7 @@ public class LoanAccountData { 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); + acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus); } @@ -1141,7 +1148,7 @@ public class LoanAccountData { 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); + acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus); } public static LoanAccountData withLoanCalendarData(final LoanAccountData acc, final CalendarData calendarData) { @@ -1168,7 +1175,7 @@ public class LoanAccountData { 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); + acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus); } public static LoanAccountData withOriginalSchedule(final LoanAccountData acc, final LoanScheduleData originalSchedule) { @@ -1196,7 +1203,7 @@ public class LoanAccountData { 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); + acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus); } private LoanAccountData( @@ -1263,11 +1270,12 @@ public class LoanAccountData { final LoanInterestRecalculationData interestRecalculationData, final LoanScheduleData originalSchedule, final Boolean createStandingInstructionAtDisbursement, final PaidInAdvanceData paidInAdvance, final Collection<InterestRatePeriodData> interestRatesPeriods, final Boolean isVariableInstallmentsAllowed, - final Integer minimumGap, final Integer maximumGap) { + final Integer minimumGap, final Integer maximumGap, final EnumOptionData subStatus) { this.id = id; this.accountNo = accountNo; this.status = status; + this.subStatus = subStatus; this.externalId = externalId; this.clientId = clientId; this.clientAccountNo = clientAccountNo; http://git-wip-us.apache.org/repos/asf/incubator-fineract/blob/b9b345a5/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index 535bb9a..c5e494b 100755 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -107,6 +107,7 @@ import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactio import org.apache.fineract.portfolio.loanaccount.exception.InvalidRefundDateException; import org.apache.fineract.portfolio.loanaccount.exception.LoanApplicationDateException; import org.apache.fineract.portfolio.loanaccount.exception.LoanDisbursalException; +import org.apache.fineract.portfolio.loanaccount.exception.LoanForeclosureException; import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerAssignmentDateException; import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerAssignmentException; import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerUnassignmentDateException; @@ -132,6 +133,7 @@ import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; import org.apache.fineract.useradministration.domain.AppUser; import org.hibernate.annotations.LazyCollection; import org.hibernate.annotations.LazyCollectionOption; +import org.joda.time.Days; import org.joda.time.LocalDate; import org.joda.time.LocalDateTime; import org.joda.time.format.DateTimeFormat; @@ -395,6 +397,9 @@ public class Loan extends AbstractPersistable<Long> { @JoinColumn(name = "writeoff_reason_cv_id", nullable = true) private CodeValue writeOffReason; + @Column(name = "loan_sub_status_id", nullable = true) + private Integer loanSubStatus; + public static Loan newIndividualLoanApplication(final String accountNo, final Client client, final Integer loanType, final LoanProduct loanProduct, final Fund fund, final Staff officer, final CodeValue loanPurpose, final LoanTransactionProcessingStrategy transactionProcessingStrategy, @@ -4814,6 +4819,14 @@ public class Loan extends AbstractPersistable<Long> { dataValidationErrors.add(error); } break; + case LOAN_FORECLOSURE: + if (!isOpen()) { + final String defaultUserMessage = "Loan foreclosure is not allowed. Loan Account is not active."; + final ApiParameterError error = ApiParameterError.generalError( + "error.msg.loan.foreclosure.account.is.not.active", defaultUserMessage); + dataValidationErrors.add(error); + } + break; default: break; } @@ -5982,12 +5995,246 @@ public class Loan extends AbstractPersistable<Long> { this.writeOffReason = writeOffReason; } - public Group getGroup() { - return group; - } - public LoanProduct getLoanProduct() { - return loanProduct; - } + public Group getGroup() { + return group; + } + + public LoanProduct getLoanProduct() { + return loanProduct; + } + public LoanRepaymentScheduleInstallment fetchLoanForeclosureDetail(final LocalDate closureDate) { + Money totalPrincipal = Money.of(getCurrency(), this.getSummary().getTotalPrincipalOutstanding()); + Money[] receivables = retriveIncomeOutstandingTillDate(closureDate); + final List<LoanInterestRecalcualtionAdditionalDetails> compoundingDetails = null; + final LocalDate currentDate = DateUtils.getLocalDateOfTenant(); + return new LoanRepaymentScheduleInstallment(null, 0, currentDate, currentDate, totalPrincipal.getAmount(), + receivables[0].getAmount(), receivables[1].getAmount(), receivables[2].getAmount(), false, compoundingDetails); + } + + public Money[] retriveIncomeOutstandingTillDate(final LocalDate paymentDate) { + Money[] balances = new Money[3]; + final MonetaryCurrency currency = getCurrency(); + Money interest = Money.zero(currency); + Money fee = Money.zero(currency); + Money penalty = Money.zero(currency); + boolean isArrearsPresent = false; + for (final LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) { + if (installment.isNotFullyPaidOff()) { + if (!isArrearsPresent || !installment.getDueDate().isAfter(paymentDate)) { + interest = interest.plus(installment.getInterestOutstanding(currency)); + fee = fee.plus(installment.getFeeChargesOutstanding(currency)); + penalty = penalty.plus(installment.getPenaltyChargesOutstanding(currency)); + isArrearsPresent = true; + } else if (installment.getFromDate().isBefore(paymentDate)) { + Money[] balancesForCurrentPeroid = fetchInterestFeeAndPenalty(paymentDate, currency, installment); + interest = interest.plus(balancesForCurrentPeroid[0]); + fee = fee.plus(balancesForCurrentPeroid[1]); + penalty = penalty.plus(balancesForCurrentPeroid[2]); + } + } + } + balances[0] = interest; + balances[1] = fee; + balances[2] = penalty; + return balances; + } + + private Money[] fetchInterestFeeAndPenalty(final LocalDate paymentDate, final MonetaryCurrency currency, final LoanRepaymentScheduleInstallment installment) { + Money penaltyForCurrentPeriod = Money.zero(getCurrency()); + Money feeForCurrentPeriod = Money.zero(getCurrency()); + int totalPeriodDays = Days.daysBetween(installment.getFromDate(), installment.getDueDate()).getDays(); + int tillDays = Days.daysBetween(installment.getFromDate(), paymentDate).getDays(); + Money interestForCurrentPeriod = Money.of(getCurrency(),BigDecimal.valueOf(calculateInterestForDays(totalPeriodDays, installment.getInterestOutstanding(getCurrency()) + .getAmount(), tillDays))); + for (LoanCharge loanCharge : this.charges) { + if (loanCharge.isActive() + && loanCharge.isDueForCollectionFromAndUpToAndIncluding(installment.getFromDate(), paymentDate)) { + if (loanCharge.isPenaltyCharge()) { + penaltyForCurrentPeriod = loanCharge.getAmountOutstanding(getCurrency()); + } else { + feeForCurrentPeriod = loanCharge.getAmountOutstanding(currency); + } + } + } + + Money[] balances = new Money[3]; + balances[0] = interestForCurrentPeriod; + balances[1] = feeForCurrentPeriod; + balances[2] = penaltyForCurrentPeriod; + + return balances; + } + + + public Money[] retriveIncomeForOverlappingPeriod(final LocalDate paymentDate) { + Money[] balances = new Money[3]; + final MonetaryCurrency currency = getCurrency(); + for (final LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) { + if (installment.getDueDate().isEqual(paymentDate)){ + Money interest = installment.getInterestOutstanding(currency); + Money fee = installment.getFeeChargesOutstanding(currency); + Money penalty = installment.getPenaltyChargesOutstanding(currency); + balances[0] = interest; + balances[1] = fee; + balances[2] = penalty; + break; + }else if(installment.getDueDate().isAfter(paymentDate) && installment.getFromDate().isBefore(paymentDate)){ + balances = fetchInterestFeeAndPenalty(paymentDate, currency, installment); + break; + } + } + + return balances; + } + private double calculateInterestForDays(int daysInPeriod, BigDecimal interest, int days) { + if (interest.doubleValue() == 0) { return 0; } + return ((interest.doubleValue()) / daysInPeriod) * days; + } + + public Money[] getReceivableIncome(final LocalDate tillDate) { + MonetaryCurrency currency = getCurrency(); + Money receivableInterest = Money.zero(currency); + Money receivableFee = Money.zero(currency); + Money receivablePenalty = Money.zero(currency); + Money[] receivables = new Money[3]; + for (final LoanTransaction transaction : this.loanTransactions) { + if (transaction.isNotReversed() && !transaction.isRepaymentAtDisbursement() && !transaction.isDisbursement() + && !transaction.getTransactionDate().isAfter(tillDate)) { + if (transaction.isAccrual()) { + receivableInterest = receivableInterest.plus(transaction.getInterestPortion(currency)); + receivableFee = receivableFee.plus(transaction.getFeeChargesPortion(currency)); + receivablePenalty = receivablePenalty.plus(transaction.getPenaltyChargesPortion(currency)); + } else if (transaction.isRepayment() || transaction.isChargePayment()) { + receivableInterest = receivableInterest.minus(transaction.getInterestPortion(currency)); + receivableFee = receivableFee.minus(transaction.getFeeChargesPortion(currency)); + receivablePenalty = receivablePenalty.minus(transaction.getPenaltyChargesPortion(currency)); + } + } + if (receivableInterest.isLessThanZero()) { + receivableInterest = receivableInterest.zero(); + } + if (receivableFee.isLessThanZero()) { + receivableFee = receivableFee.zero(); + } + if (receivablePenalty.isLessThanZero()) { + receivablePenalty = receivablePenalty.zero(); + } + } + receivables[0] = receivableInterest; + receivables[1] = receivableFee; + receivables[2] = receivablePenalty; + return receivables; + } + + public void handleForeClosureTransactions(final List<LoanTransaction> repaymentTransaction, + final LocalDate foreClosureDate, final LoanLifecycleStateMachine loanLifecycleStateMachine) { + + LoanEvent event = LoanEvent.LOAN_FORECLOSURE; + + validateAccountStatus(event); + + MonetaryCurrency currency = getCurrency(); + + final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory + .determineProcessor(this.transactionProcessingStrategy); + + loanRepaymentScheduleTransactionProcessor.processTransactionsFromDerivedFields(repaymentTransaction, currency, + this.repaymentScheduleInstallments, charges()); + this.loanTransactions.addAll(repaymentTransaction); + this.loanSubStatus = LoanSubStatus.FORECLOSED.getValue(); + updateLoanSummaryDerivedFields(); + doPostLoanTransactionChecks(foreClosureDate, loanLifecycleStateMachine); + } + + public Money retrieveAccruedAmountAfterDate(final LocalDate tillDate) { + Money totalAmountAccrued = Money.zero(getCurrency()); + Money actualAmountTobeAccrued = Money.zero(getCurrency()); + for (final LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) { + totalAmountAccrued = totalAmountAccrued.plus(installment.getInterestAccrued(getCurrency())); + + if (tillDate.isAfter(installment.getFromDate()) && tillDate.isBefore(installment.getDueDate())) { + int daysInPeriod = Days.daysBetween(installment.getFromDate(), installment.getDueDate()).getDays(); + int tillDays = Days.daysBetween(installment.getFromDate(), tillDate).getDays(); + double interest = calculateInterestForDays(daysInPeriod, installment.getInterestCharged(getCurrency()).getAmount(), + tillDays); + actualAmountTobeAccrued = actualAmountTobeAccrued.plus(interest); + } else if ((tillDate.isAfter(installment.getFromDate()) && tillDate.isEqual(installment.getDueDate())) + || (tillDate.isEqual(installment.getFromDate()) && tillDate.isEqual(installment.getDueDate())) + || (tillDate.isAfter(installment.getFromDate()) && tillDate.isAfter(installment.getDueDate()))) { + actualAmountTobeAccrued = actualAmountTobeAccrued.plus(installment.getInterestAccrued(getCurrency())); + } + } + Money accredAmountAfterDate = totalAmountAccrued.minus(actualAmountTobeAccrued); + if (accredAmountAfterDate.isLessThanZero()) { + accredAmountAfterDate = Money.zero(getCurrency()); + } + return accredAmountAfterDate; + } + + public void validateForForeclosure(final LocalDate transactionDate) { + + if (isInterestRecalculationEnabledForProduct()) { + final String defaultUserMessage = "The loan with interest recalculation enabled cannot be foreclosed."; + throw new LoanForeclosureException("loan.with.interest.recalculation.enabled.cannot.be.foreclosured", defaultUserMessage, + getId()); + } + + LocalDate lastUserTransactionDate = getLastUserTransactionDate(); + + if (DateUtils.isDateInTheFuture(transactionDate)) { + final String defaultUserMessage = "The transactionDate cannot be in the future."; + throw new LoanForeclosureException("loan.foreclosure.transaction.date.is.in.future", defaultUserMessage, transactionDate); + } + + if (lastUserTransactionDate.isAfter(transactionDate)) { + final String defaultUserMessage = "The transactionDate cannot be in the future."; + throw new LoanForeclosureException("loan.foreclosure.transaction.date.cannot.before.the.last.transaction.date", + defaultUserMessage, transactionDate); + } + } + + public void updateInstallmentsPostDate(LocalDate transactionDate) { + List<LoanRepaymentScheduleInstallment> newInstallments = new ArrayList<>(this.repaymentScheduleInstallments); + final MonetaryCurrency currency = getCurrency(); + Money totalPrincipal = Money.zero(currency); + Money [] balances = retriveIncomeForOverlappingPeriod(transactionDate); + for (final LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) { + if (!installment.getDueDate().isBefore(transactionDate)) { + totalPrincipal = totalPrincipal.plus(installment.getPrincipalOutstanding(currency)); + newInstallments.remove(installment); + } + } + + LoanRepaymentScheduleInstallment newInstallment = new LoanRepaymentScheduleInstallment(null, newInstallments.size() + 1, + newInstallments.get((newInstallments.size() - 1)).getDueDate(), transactionDate, totalPrincipal.getAmount(), + balances[0].getAmount(), balances[1].getAmount(), balances[2].getAmount(), true, null); + newInstallment.updateInstallmentNumber(newInstallments.size() + 1); + newInstallments.add(newInstallment); + updateLoanScheduleOnForeclosure(newInstallments); + + Set<LoanCharge> charges = this.charges(); + int penaltyWaitPeriod = 0; + for (LoanCharge loanCharge : charges) { + if (loanCharge.getDueLocalDate() != null + && (loanCharge.getDueLocalDate().isAfter(transactionDate))) { + loanCharge.setActive(false); + } else if (loanCharge.getDueLocalDate() == null) { + recalculateLoanCharge(loanCharge, penaltyWaitPeriod); + } + } + } + + public void updateLoanScheduleOnForeclosure(final Collection<LoanRepaymentScheduleInstallment> installments) { + this.repaymentScheduleInstallments.clear(); + for (final LoanRepaymentScheduleInstallment installment : installments) { + addRepaymentScheduleInstallment(installment); + } + } + + public Integer getLoanSubStatus() { + return this.loanSubStatus; + } + } http://git-wip-us.apache.org/repos/asf/incubator-fineract/blob/b9b345a5/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainService.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainService.java index 3dfbc6a..8273d9e 100755 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainService.java @@ -19,6 +19,7 @@ package org.apache.fineract.portfolio.loanaccount.domain; import java.math.BigDecimal; +import java.util.Map; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; @@ -56,4 +57,5 @@ public interface LoanAccountDomainService { void saveLoanWithDataIntegrityViolationChecks(Loan loan); + Map<String, Object> foreCloseLoan(final Loan loan, final LocalDate foreClourseDate, String noteText); } http://git-wip-us.apache.org/repos/asf/incubator-fineract/blob/b9b345a5/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java index ba0788f..63796cc 100755 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java @@ -19,11 +19,12 @@ package org.apache.fineract.portfolio.loanaccount.domain; import java.math.BigDecimal; -import java.util.Date; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Date; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -574,9 +575,118 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { return newRefundTransaction; } + @Override + public Map<String, Object> foreCloseLoan(final Loan loan, final LocalDate foreClosureDate, final String noteText) { + MonetaryCurrency currency = loan.getCurrency(); + LocalDateTime createdDate = DateUtils.getLocalDateTimeOfTenant(); + final Map<String, Object> changes = new LinkedHashMap<>(); + List<LoanTransaction> newTransactions = new ArrayList<>(); + + final List<Long> existingTransactionIds = new ArrayList<>(); + final List<Long> existingReversedTransactionIds = new ArrayList<>(); + existingTransactionIds.addAll(loan.findExistingTransactionIds()); + existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); + final LoanRepaymentScheduleInstallment foreCloseDetail = loan.fetchLoanForeclosureDetail(foreClosureDate); + if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct() + && (loan.getAccruedTill() == null || !foreClosureDate.isEqual(loan.getAccruedTill()))) { + Money[] accruedReceivables = loan.getReceivableIncome(foreClosureDate); + Money interestPortion = foreCloseDetail.getInterestCharged(currency).minus(accruedReceivables[0]); + Money feePortion = foreCloseDetail.getFeeChargesCharged(currency).minus(accruedReceivables[1]); + Money penaltyPortion = foreCloseDetail.getPenaltyChargesCharged(currency).minus(accruedReceivables[2]); + Money total = interestPortion.plus(feePortion).plus(penaltyPortion); + if (total.isGreaterThanZero()) { + LoanTransaction accrualTransaction = LoanTransaction.accrual(loan, loan.getOffice(), total, interestPortion, feePortion, + penaltyPortion, foreClosureDate); + LocalDate fromDate = loan.getDisbursementDate(); + if (loan.getAccruedTill() != null) { + fromDate = loan.getAccruedTill(); + } + LoanRepaymentScheduleInstallment installment = fetchLoanRepaymentScheduleInstallment(fromDate, loan); + installment.updateAccrualPortion(interestPortion, feePortion, penaltyPortion); + accrualTransaction.updateCreatedDate(createdDate.toDate()); + createdDate = createdDate.plusSeconds(1); + newTransactions.add(accrualTransaction); + Set<LoanChargePaidBy> accrualCharges = accrualTransaction.getLoanChargesPaid(); + for (LoanCharge loanCharge : loan.charges()) { + if (loanCharge.isActive() + && !loanCharge.isPaid() + && (loanCharge.isDueForCollectionFromAndUpToAndIncluding(fromDate, foreClosureDate) || loanCharge + .isInstalmentFee())) { + final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(accrualTransaction, loanCharge, loanCharge + .getAmountOutstanding(currency).getAmount(), null); + accrualCharges.add(loanChargePaidBy); + } + } + } + } + + Money interestPayable = foreCloseDetail.getInterestCharged(currency); + + Money feePayable = foreCloseDetail.getFeeChargesCharged(currency); + Money penaltyPayable = foreCloseDetail.getPenaltyChargesCharged(currency); + + Money payPrincipal = foreCloseDetail.getPrincipal(currency); + + LoanTransaction payment = null; + if (payPrincipal.plus(interestPayable).plus(feePayable).plus(penaltyPayable).isGreaterThanZero()) { + AppUser appUser = null; + final PaymentDetail paymentDetail = null; + String externalId = null; + final LocalDateTime currentDateTime = DateUtils.getLocalDateTimeOfTenant(); + payment = LoanTransaction.repayment(loan.getOffice(), payPrincipal.plus(interestPayable).plus(feePayable).plus(penaltyPayable), + paymentDetail, foreClosureDate, externalId, currentDateTime, appUser); + createdDate = createdDate.plusSeconds(1); + payment.updateCreatedDate(createdDate.toDate()); + payment.updateComponents(payPrincipal, interestPayable, feePayable, penaltyPayable); + payment.updateLoan(loan); + newTransactions.add(payment); + } + List<Long> transactionIds = new ArrayList<>(); + loan.handleForeClosureTransactions(newTransactions, foreClosureDate, defaultLoanLifecycleStateMachine()); + for (LoanTransaction newTransaction : newTransactions) { + saveLoanTransactionWithDataIntegrityViolationChecks(newTransaction); + transactionIds.add(newTransaction.getId()); + } + changes.put("transactions", transactionIds); + changes.put("eventAmount", payPrincipal.getAmount().negate()); + + /*** + * TODO Vishwas Batch save is giving me a + * HibernateOptimisticLockingFailureException, looping and saving for + * the time being, not a major issue for now as this loop is entered + * only in edge cases (when a payment is made before the latest payment + * recorded against the loan) + ***/ + + saveAndFlushLoanWithDataIntegrityViolationChecks(loan); + + if (StringUtils.isNotBlank(noteText)) { + changes.put("note", noteText); + final Note note = Note.loanNote(loan, noteText); + this.noteRepository.save(note); + } + + postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, false); + + return changes; + + } + + private LoanRepaymentScheduleInstallment fetchLoanRepaymentScheduleInstallment(LocalDate fromDate, final Loan loan) { + LoanRepaymentScheduleInstallment installment = null; + for (LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : loan.getRepaymentScheduleInstallments()) { + if (fromDate.equals(loanRepaymentScheduleInstallment.getFromDate())) { + installment = loanRepaymentScheduleInstallment; + break; + } + } + return installment; + } + private Map<BUSINESS_ENTITY, Object> constructEntityMap(final BUSINESS_ENTITY entityEvent, Object entity) { Map<BUSINESS_ENTITY, Object> map = new HashMap<>(1); map.put(entityEvent, entity); return map; } + } \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-fineract/blob/b9b345a5/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanEvent.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanEvent.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanEvent.java index 5b7f4c1..fe46017 100755 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanEvent.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanEvent.java @@ -42,5 +42,6 @@ public enum LoanEvent { LOAN_CHARGE_PAYMENT, // LOAN_CLOSED, // LOAN_EDIT_MULTI_DISBURSE_DATE, // - LOAN_REFUND; + LOAN_REFUND, // + LOAN_FORECLOSURE; } http://git-wip-us.apache.org/repos/asf/incubator-fineract/blob/b9b345a5/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java index 711a313..c305514 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java @@ -785,4 +785,10 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus public List<LoanInterestRecalcualtionAdditionalDetails> getLoanCompoundingDetails() { return this.loanCompoundingDetails; } + + public Money getAccruedInterestOutstanding(final MonetaryCurrency currency) { + final Money interestAccountedFor = getInterestPaid(currency).plus(getInterestWaived(currency)) + .plus(getInterestWrittenOff(currency)); + return getInterestAccrued(currency).minus(interestAccountedFor); + } } \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-fineract/blob/b9b345a5/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSubStatus.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSubStatus.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSubStatus.java new file mode 100644 index 0000000..afe3247 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSubStatus.java @@ -0,0 +1,80 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.domain; + +import org.apache.fineract.infrastructure.core.data.EnumOptionData; + +public enum LoanSubStatus { + INVALID(0, "loanSubStatusType.invalid"), // + FORECLOSED(100, "loanSubStatusType.foreclosed"); + + private final Integer value; + private final String code; + + public static LoanSubStatus fromInt(final Integer statusValue) { + + LoanSubStatus enumeration = LoanSubStatus.INVALID; + switch (statusValue) { + case 100: + enumeration = LoanSubStatus.FORECLOSED; + break; + } + return enumeration; + } + + private LoanSubStatus(final Integer value, final String code) { + this.value = value; + this.code = code; + } + + public boolean hasStateOf(final LoanSubStatus state) { + return this.value.equals(state.getValue()); + } + + public Integer getValue() { + return this.value; + } + + public String getCode() { + return this.code; + } + + public boolean isForeclosed() { + return this.value.equals(LoanSubStatus.FORECLOSED.getValue()); + } + + public static EnumOptionData loanSubStatus(final int id) { + return loanSubStatusEnum(LoanSubStatus.fromInt(id)); + } + + public static EnumOptionData loanSubStatusEnum(final LoanSubStatus type) { + final String codePrefix = "loanSubStatus."; + EnumOptionData optionData = null; + switch (type) { + case FORECLOSED: + optionData = new EnumOptionData(LoanSubStatus.FORECLOSED.getValue().longValue(), codePrefix + + LoanSubStatus.FORECLOSED.getCode(), "Foreclosed"); + break; + default: + optionData = new EnumOptionData(LoanSubStatus.INVALID.getValue().longValue(), LoanSubStatus.INVALID.getCode(), "Invalid"); + break; + } + return optionData; + } +} http://git-wip-us.apache.org/repos/asf/incubator-fineract/blob/b9b345a5/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java index a97f8dd..70a2238 100755 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java @@ -18,6 +18,27 @@ */ package org.apache.fineract.portfolio.loanaccount.domain; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import javax.persistence.UniqueConstraint; + import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; @@ -36,26 +57,6 @@ import org.joda.time.LocalDate; import org.joda.time.LocalDateTime; import org.springframework.data.jpa.domain.AbstractPersistable; -import javax.persistence.CascadeType; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; -import javax.persistence.OneToMany; -import javax.persistence.Table; -import javax.persistence.Temporal; -import javax.persistence.TemporalType; -import javax.persistence.UniqueConstraint; -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Date; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - /** * All monetary transactions against a loan are modelled through this entity. * Disbursements, Repayments, Waivers, Write-off etc @@ -116,7 +117,7 @@ public final class LoanTransaction extends AbstractPersistable<Long> { @Temporal(TemporalType.TIMESTAMP) @Column(name = "created_date", nullable = false) - private final Date createdDate; + private Date createdDate; @ManyToOne @JoinColumn(name = "appuser_id", nullable = true) @@ -133,8 +134,8 @@ public final class LoanTransaction extends AbstractPersistable<Long> { private boolean manuallyAdjustedOrReversed; @LazyCollection(LazyCollectionOption.FALSE) - @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) - @JoinColumn(name = "loan_transaction_id", referencedColumnName= "id" , nullable = false) + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) + @JoinColumn(name = "loan_transaction_id", referencedColumnName = "id", nullable = false) private Set<LoanTransactionToRepaymentScheduleMapping> loanTransactionToRepaymentScheduleMappings = new HashSet<>(); protected LoanTransaction() { @@ -146,19 +147,20 @@ public final class LoanTransaction extends AbstractPersistable<Long> { this.appUser = null; } - public static LoanTransaction incomePosting(final Loan loan, final Office office, final Date dateOf, - final BigDecimal amount, final BigDecimal interestPortion, final BigDecimal feeChargesPortion, - final BigDecimal penaltyChargesPortion, final AppUser appUser){ - final Integer typeOf = LoanTransactionType.INCOME_POSTING.getValue(); - final BigDecimal principalPortion = BigDecimal.ZERO; - final BigDecimal overPaymentPortion = BigDecimal.ZERO; - final boolean reversed = false; - final PaymentDetail paymentDetail = null; - final String externalId = null; - final LocalDateTime createdDate = DateUtils.getLocalDateTimeOfTenant(); - return new LoanTransaction(loan, office, typeOf, dateOf, amount, principalPortion, interestPortion, feeChargesPortion, + public static LoanTransaction incomePosting(final Loan loan, final Office office, final Date dateOf, final BigDecimal amount, + final BigDecimal interestPortion, final BigDecimal feeChargesPortion, final BigDecimal penaltyChargesPortion, + final AppUser appUser) { + final Integer typeOf = LoanTransactionType.INCOME_POSTING.getValue(); + final BigDecimal principalPortion = BigDecimal.ZERO; + final BigDecimal overPaymentPortion = BigDecimal.ZERO; + final boolean reversed = false; + final PaymentDetail paymentDetail = null; + final String externalId = null; + final LocalDateTime createdDate = DateUtils.getLocalDateTimeOfTenant(); + return new LoanTransaction(loan, office, typeOf, dateOf, amount, principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion, overPaymentPortion, reversed, paymentDetail, externalId, createdDate, appUser); } + public static LoanTransaction disbursement(final Office office, final Money amount, final PaymentDetail paymentDetail, final LocalDate disbursementDate, final String externalId, final LocalDateTime createdDate, final AppUser appUser) { return new LoanTransaction(null, office, LoanTransactionType.DISBURSEMENT, paymentDetail, amount.getAmount(), disbursementDate, @@ -212,17 +214,25 @@ public final class LoanTransaction extends AbstractPersistable<Long> { principalPortion, interestPortion, feesPortion, penaltiesPortion, overPaymentPortion, reversed, paymentDetail, externalId, createdDate, appUser); } + + public static LoanTransaction accrual(final Loan loan, final Office office, final Money amount, final Money interest, + final Money feeCharges, final Money penaltyCharges, final LocalDate transactionDate) { + final AppUser appUser = null; + return accrueTransaction(loan, office, transactionDate, amount.getAmount(), interest.getAmount(), feeCharges.getAmount(), + penaltyCharges.getAmount(), appUser); + } + public static LoanTransaction accrueTransaction(final Loan loan, final Office office, final LocalDate dateOf, final BigDecimal amount, - final BigDecimal interestPortion, final BigDecimal feeChargesPortion, - final BigDecimal penaltyChargesPortion, final AppUser appUser) { + final BigDecimal interestPortion, final BigDecimal feeChargesPortion, final BigDecimal penaltyChargesPortion, + final AppUser appUser) { BigDecimal principalPortion = null; BigDecimal overPaymentPortion = null; boolean reversed = false; PaymentDetail paymentDetail = null; String externalId = null; LocalDateTime createdDate = DateUtils.getLocalDateTimeOfTenant(); - return new LoanTransaction(loan, office, LoanTransactionType.ACCRUAL.getValue(), dateOf.toDate(), amount, - principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion, overPaymentPortion, reversed, paymentDetail, externalId, + return new LoanTransaction(loan, office, LoanTransactionType.ACCRUAL.getValue(), dateOf.toDate(), amount, principalPortion, + interestPortion, feeChargesPortion, penaltyChargesPortion, overPaymentPortion, reversed, paymentDetail, externalId, createdDate, appUser); } @@ -664,15 +674,14 @@ public final class LoanTransaction extends AbstractPersistable<Long> { public boolean isAccrual() { return LoanTransactionType.ACCRUAL.equals(getTypeOf()) && isNotReversed(); } - - public boolean isNonMonetaryTransaction(){ - return isNotReversed() - && (LoanTransactionType.CONTRA.equals(getTypeOf()) - || LoanTransactionType.MARKED_FOR_RESCHEDULING.equals(getTypeOf()) - || LoanTransactionType.APPROVE_TRANSFER.equals(getTypeOf()) - || LoanTransactionType.INITIATE_TRANSFER.equals(getTypeOf()) - || LoanTransactionType.REJECT_TRANSFER.equals(getTypeOf()) - || LoanTransactionType.WITHDRAW_TRANSFER.equals(getTypeOf())); + + public boolean isNonMonetaryTransaction() { + return isNotReversed() + && (LoanTransactionType.CONTRA.equals(getTypeOf()) || LoanTransactionType.MARKED_FOR_RESCHEDULING.equals(getTypeOf()) + || LoanTransactionType.APPROVE_TRANSFER.equals(getTypeOf()) + || LoanTransactionType.INITIATE_TRANSFER.equals(getTypeOf()) + || LoanTransactionType.REJECT_TRANSFER.equals(getTypeOf()) || LoanTransactionType.WITHDRAW_TRANSFER + .equals(getTypeOf())); } public void updateOutstandingLoanBalance(BigDecimal outstandingLoanBalance) { @@ -756,13 +765,19 @@ public final class LoanTransaction extends AbstractPersistable<Long> { return isMappingUpdated; } - public Set<LoanTransactionToRepaymentScheduleMapping> getLoanTransactionToRepaymentScheduleMappings() { return this.loanTransactionToRepaymentScheduleMappings; } - - public Boolean isAllowTypeTransactionAtTheTimeOfLastUndo(){ - return isDisbursement() || isAccrual() || isRepaymentAtDisbursement(); + + public Boolean isAllowTypeTransactionAtTheTimeOfLastUndo() { + return isDisbursement() || isAccrual() || isRepaymentAtDisbursement(); + } + + public void updateCreatedDate(Date createdDate) { + this.createdDate = createdDate; } + public boolean isAccrualTransaction() { + return isAccrual(); + } } \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-fineract/blob/b9b345a5/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java index 78cd9d7..5cc5918 100755 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java @@ -627,4 +627,90 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen return latestPaidCharge; } + @Override + public void processTransactionsFromDerivedFields(List<LoanTransaction> transactionsPostDisbursement, MonetaryCurrency currency, + List<LoanRepaymentScheduleInstallment> installments, final Set<LoanCharge> charges) { + for (final LoanTransaction loanTransaction : transactionsPostDisbursement) { + if (!loanTransaction.isAccrualTransaction()) { + processTransactionFromDerivedFields(loanTransaction, currency, installments, charges); + } + } + } + + private void processTransactionFromDerivedFields(final LoanTransaction loanTransaction, MonetaryCurrency currency, + List<LoanRepaymentScheduleInstallment> installments, final Set<LoanCharge> charges) { + Money principal = loanTransaction.getPrincipalPortion(currency); + Money interest = loanTransaction.getInterestPortion(currency); + if (loanTransaction.isInterestWaiver()) { + interest = loanTransaction.getAmount(currency); + } + Money feeCharges = loanTransaction.getFeeChargesPortion(currency); + Money penaltyCharges = loanTransaction.getPenaltyChargesPortion(currency); + final LocalDate transactionDate = loanTransaction.getTransactionDate(); + if (principal.isGreaterThanZero() || interest.isGreaterThanZero() || feeCharges.isGreaterThanZero() + || penaltyCharges.isGreaterThanZero()) { + for (final LoanRepaymentScheduleInstallment currentInstallment : installments) { + if (currentInstallment.isNotFullyPaidOff()) { + if (penaltyCharges.isGreaterThanZero()) { + Money penaltyChargesPortion = Money.zero(currency); + if (loanTransaction.isWaiver()) { + penaltyChargesPortion = currentInstallment.waivePenaltyChargesComponent(transactionDate, penaltyCharges); + } else { + penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, penaltyCharges); + } + penaltyCharges = penaltyCharges.minus(penaltyChargesPortion); + } + + if (feeCharges.isGreaterThanZero()) { + Money feeChargesPortion = Money.zero(currency); + if (loanTransaction.isWaiver()) { + feeChargesPortion = currentInstallment.waiveFeeChargesComponent(transactionDate, feeCharges); + } else { + feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, feeCharges); + } + feeCharges = feeCharges.minus(feeChargesPortion); + } + + if (interest.isGreaterThanZero()) { + Money interestPortion = Money.zero(currency); + if (loanTransaction.isWaiver()) { + interestPortion = currentInstallment.waiveInterestComponent(transactionDate, interest); + } else { + interestPortion = currentInstallment.payInterestComponent(transactionDate, interest); + } + interest = interest.minus(interestPortion); + } + + if (principal.isGreaterThanZero()) { + Money principalPortion = currentInstallment.payPrincipalComponent(transactionDate, principal); + principal = principal.minus(principalPortion); + } + } + if (!(principal.isGreaterThanZero() || interest.isGreaterThanZero() || feeCharges.isGreaterThanZero() || penaltyCharges + .isGreaterThanZero())) { + break; + } + } + } + + final Set<LoanCharge> loanFees = extractFeeCharges(charges); + final Set<LoanCharge> loanPenalties = extractPenaltyCharges(charges); + Integer installmentNumber = null; + if (loanTransaction.isChargePayment() && installments.size() == 1) { + installmentNumber = installments.get(0).getInstallmentNumber(); + } + + if (loanTransaction.isNotWaiver()) { + feeCharges = loanTransaction.getFeeChargesPortion(currency); + penaltyCharges = loanTransaction.getPenaltyChargesPortion(currency); + if (feeCharges.isGreaterThanZero()) { + updateChargesPaidAmountBy(loanTransaction, feeCharges, loanFees, installmentNumber); + } + + if (penaltyCharges.isGreaterThanZero()) { + updateChargesPaidAmountBy(loanTransaction, penaltyCharges, loanPenalties, installmentNumber); + } + } + } + } \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-fineract/blob/b9b345a5/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java index b303d30..5b55df5 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java @@ -51,5 +51,8 @@ public interface LoanRepaymentScheduleTransactionProcessor { void handleRefund(LoanTransaction loanTransaction, MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments, final Set<LoanCharge> charges); + + void processTransactionsFromDerivedFields(List<LoanTransaction> transactionsPostDisbursement, MonetaryCurrency currency, + List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges); } \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-fineract/blob/b9b345a5/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanForeclosureException.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanForeclosureException.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanForeclosureException.java new file mode 100644 index 0000000..79eb7d9 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanForeclosureException.java @@ -0,0 +1,28 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class LoanForeclosureException extends AbstractPlatformDomainRuleException { + + public LoanForeclosureException(final String errorCode, final String errorMessage, final Object... defaultUserMessageArgs) { + super(errorCode, errorMessage, defaultUserMessageArgs); + } +} http://git-wip-us.apache.org/repos/asf/incubator-fineract/blob/b9b345a5/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/ForeClosureCommandHandler.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/ForeClosureCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/ForeClosureCommandHandler.java new file mode 100644 index 0000000..e249dc9 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/ForeClosureCommandHandler.java @@ -0,0 +1,45 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.handler; + +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +@CommandType(entity = "LOAN", action = "FORECLOSURE") +public class ForeClosureCommandHandler implements NewCommandSourceHandler { + + private final LoanWritePlatformService writePlatformService; + + @Autowired + public ForeClosureCommandHandler(final LoanWritePlatformService writePlatformService) { + this.writePlatformService = writePlatformService; + } + + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + return writePlatformService.forecloseLoan(command.getLoanId(), command); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-fineract/blob/b9b345a5/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java index bdceb74..d284130 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java @@ -447,4 +447,26 @@ public final class LoanEventApiJsonValidator { throwExceptionIfValidationWarningsExist(dataValidationErrors); } + public void validateLoanForeclosure(final String json) { + + if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); } + + final Set<String> foreclosureParameters = new HashSet<>(Arrays.asList("transactionDate", "note", "locale", "dateFormat")); + + final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, foreclosureParameters); + + final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan"); + + final JsonElement element = this.fromApiJsonHelper.parse(json); + final LocalDate transactionDate = this.fromApiJsonHelper.extractLocalDateNamed("transactionDate", element); + baseDataValidator.reset().parameter("transactionDate").value(transactionDate).notNull(); + + final String note = this.fromApiJsonHelper.extractStringNamed("note", element); + baseDataValidator.reset().parameter("note").value(note).notExceedingLengthOf(1000); + + validatePaymentDetails(baseDataValidator, element); + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } } \ No newline at end of file