diff --git a/api-docs/apiLive.htm b/api-docs/apiLive.htm index a620dbff7..23f9b6231 100644 --- a/api-docs/apiLive.htm +++ b/api-docs/apiLive.htm @@ -1418,7 +1418,14 @@ Delete a Financial Activity to Account Mapping - + + Periodic Accrual Accounting + accrualaccounting + Execute Periodic Accrual Accounting
+ + + + @@ -29493,6 +29500,71 @@ Content-Type: application/json + + +   +
+
+

Periodic Accrual Accounting

+

+ Periodic Accrual is to accrue the loan income till the specific date or till batch job scheduled time. +

+ + + + + + + + + + + +
Field Descriptions
accrueTill
which specifies periodic accruals should happen till the given Date +
+
+
+ + + +   +
+
+

Executes Periodic Accrual Accounting

+ + + + + + + + +
Mandatory Fields
accrueTill +
+
+ +
+
+ +POST https://DomainName/api/v1/accrualaccounting + + +POST financialactivityaccounts +Content-Type: application/json +Request Body: +{ + "locale": "en", + "dateFormat": "dd MMMM yyyy", + "accrueTill":"04 June 2014" +} + + + +
+
+ + +  
diff --git a/mifosng-db/migrations/core_db/V186__added_periodic_accrual_job.sql b/mifosng-db/migrations/core_db/V186__added_periodic_accrual_job.sql new file mode 100755 index 000000000..724123e4e --- /dev/null +++ b/mifosng-db/migrations/core_db/V186__added_periodic_accrual_job.sql @@ -0,0 +1,3 @@ +INSERT INTO `job` (`name`, `display_name`, `cron_expression`, `create_time`, `task_priority`, `scheduler_group`) VALUES ('Add Periodic Accrual Transactions', 'Add Periodic Accrual Transactions', '0 2 0 1/1 * ? *', now(), 4, 3); + +UPDATE `job` SET `scheduler_group`=3 WHERE `name`='Add Accrual Transactions'; \ No newline at end of file diff --git a/mifosng-db/migrations/core_db/V187__added_permission_to_periodic_accrual.sql b/mifosng-db/migrations/core_db/V187__added_permission_to_periodic_accrual.sql new file mode 100755 index 000000000..c2233a674 --- /dev/null +++ b/mifosng-db/migrations/core_db/V187__added_permission_to_periodic_accrual.sql @@ -0,0 +1 @@ +INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) VALUES ('accounting', 'EXECUTE_PERIODICACCRUALACCOUNTING', 'PERIODICACCRUALACCOUNTING', 'EXECUTE', 0); \ No newline at end of file diff --git a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/AccountingScenarioIntegrationTest.java b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/AccountingScenarioIntegrationTest.java index a19581ec0..1a8bfbf64 100644 --- a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/AccountingScenarioIntegrationTest.java +++ b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/AccountingScenarioIntegrationTest.java @@ -8,6 +8,7 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.HashMap; +import java.util.List; import org.joda.time.LocalDate; import org.junit.Assert; @@ -21,6 +22,7 @@ import org.mifosplatform.integrationtests.common.accounting.Account; import org.mifosplatform.integrationtests.common.accounting.AccountHelper; import org.mifosplatform.integrationtests.common.accounting.JournalEntry; import org.mifosplatform.integrationtests.common.accounting.JournalEntryHelper; +import org.mifosplatform.integrationtests.common.accounting.PeriodicAccrualAccountingHelper; import org.mifosplatform.integrationtests.common.charges.ChargesHelper; import org.mifosplatform.integrationtests.common.fixeddeposit.FixedDepositAccountHelper; import org.mifosplatform.integrationtests.common.fixeddeposit.FixedDepositAccountStatusChecker; @@ -83,6 +85,7 @@ public class AccountingScenarioIntegrationTest { private RecurringDepositProductHelper recurringDepositProductHelper; private RecurringDepositAccountHelper recurringDepositAccountHelper; private SchedulerJobHelper schedulerJobHelper; + private PeriodicAccrualAccountingHelper periodicAccrualAccountingHelper; @Before public void setup() { @@ -95,6 +98,7 @@ public class AccountingScenarioIntegrationTest { this.accountHelper = new AccountHelper(this.requestSpec, this.responseSpec); this.journalEntryHelper = new JournalEntryHelper(this.requestSpec, this.responseSpec); this.schedulerJobHelper = new SchedulerJobHelper(this.requestSpec, this.responseSpec); + this.periodicAccrualAccountingHelper = new PeriodicAccrualAccountingHelper(this.requestSpec, this.responseSpec); } @Test @@ -645,6 +649,156 @@ public class AccountingScenarioIntegrationTest { System.out.println("Repayment 5 Done ......"); } + @Test + public void checkPeriodicAccrualAccountingTillCurrentDateFlow() { + final Account assetAccount = this.accountHelper.createAssetAccount(); + final Account incomeAccount = this.accountHelper.createIncomeAccount(); + final Account expenseAccount = this.accountHelper.createExpenseAccount(); + final Account overpaymentAccount = this.accountHelper.createLiabilityAccount(); + + final Integer loanProductID = createLoanProductWithPeriodicAccrualAccountingEnabled(assetAccount, incomeAccount, expenseAccount, + overpaymentAccount); + + final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, this.DATE_OF_JOINING); + final Integer loanID = applyForLoanApplication(clientID, loanProductID); + + final float FEE_PORTION = 50.0f; + final float PENALTY_PORTION = 100.0f; + Integer flat = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, String.valueOf(FEE_PORTION), false)); + Integer flatSpecifiedDueDate = ChargesHelper.createCharges(requestSpec, responseSpec, ChargesHelper.getLoanSpecifiedDueDateJSON( + ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, String.valueOf(PENALTY_PORTION), true)); + + HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, loanID); + LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); + + loanStatusHashMap = this.loanTransactionHelper.approveLoan(this.EXPECTED_DISBURSAL_DATE, loanID); + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap); + + DateFormat dateFormat = new SimpleDateFormat("dd MMMM yyyy"); + + Calendar todayDate = Calendar.getInstance(); + final String currentDate = dateFormat.format(todayDate.getTime()); + + todayDate.add(Calendar.DATE, -4); + + final String LOAN_DISBURSEMENT_DATE = dateFormat.format(todayDate.getTime()); + + todayDate.add(Calendar.MONTH, 2); + final String FIRST_REPAYMENT_DATE = dateFormat.format(todayDate.getTime()); + + todayDate = Calendar.getInstance(); + todayDate.add(Calendar.DATE, -2); + + loanStatusHashMap = this.loanTransactionHelper.disburseLoan(LOAN_DISBURSEMENT_DATE, loanID); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + + this.loanTransactionHelper.addChargesForLoan( + loanID, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(flatSpecifiedDueDate), + dateFormat.format(todayDate.getTime()), String.valueOf(PENALTY_PORTION))); + todayDate.add(Calendar.DATE, 1); + this.loanTransactionHelper.addChargesForLoan(loanID, LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON( + String.valueOf(flat), dateFormat.format(todayDate.getTime()), String.valueOf(FEE_PORTION))); + + // CHECK ACCOUNT ENTRIES + System.out.println("Entries ......"); + final float PRINCIPAL_VALUE_FOR_EACH_PERIOD = 2000.0f; + final float TOTAL_INTEREST = 1000.0f; + final JournalEntry[] assetAccountInitialEntry = { new JournalEntry(this.LP_PRINCIPAL, JournalEntry.TransactionType.CREDIT), + new JournalEntry(this.LP_PRINCIPAL, JournalEntry.TransactionType.DEBIT), }; + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, LOAN_DISBURSEMENT_DATE, assetAccountInitialEntry); + + final String jobName = "Add Periodic Accrual Transactions"; + try { + this.schedulerJobHelper.excuteJob(jobName); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + // MAKE 1 + final float FIRST_INTEREST = 200.0f; + final float INTEREST_4_DAYS = 13.33F; + this.loanTransactionHelper.checkAccrualTransactionForRepayment(getDateAsLocalDate(currentDate), INTEREST_4_DAYS, FEE_PORTION, + PENALTY_PORTION, loanID); + + } + + @Test + public void checkPeriodicAccrualAccountingAPIFlow() { + final Account assetAccount = this.accountHelper.createAssetAccount(); + final Account incomeAccount = this.accountHelper.createIncomeAccount(); + final Account expenseAccount = this.accountHelper.createExpenseAccount(); + final Account overpaymentAccount = this.accountHelper.createLiabilityAccount(); + + final Integer loanProductID = createLoanProductWithPeriodicAccrualAccountingEnabled(assetAccount, incomeAccount, expenseAccount, + overpaymentAccount); + + final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, this.DATE_OF_JOINING); + final Integer loanID = applyForLoanApplication(clientID, loanProductID); + + final float FEE_PORTION = 50.0f; + final float PENALTY_PORTION = 100.0f; + Integer flat = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, String.valueOf(FEE_PORTION), false)); + Integer flatSpecifiedDueDate = ChargesHelper.createCharges(requestSpec, responseSpec, ChargesHelper.getLoanSpecifiedDueDateJSON( + ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, String.valueOf(PENALTY_PORTION), true)); + + HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, loanID); + LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); + + loanStatusHashMap = this.loanTransactionHelper.approveLoan(this.EXPECTED_DISBURSAL_DATE, loanID); + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap); + + DateFormat dateFormat = new SimpleDateFormat("dd MMMM yyyy"); + + Calendar todayDate = Calendar.getInstance(); + final String currentDate = dateFormat.format(todayDate.getTime()); + + todayDate.add(Calendar.DATE, -4); + + final String LOAN_DISBURSEMENT_DATE = dateFormat.format(todayDate.getTime()); + + todayDate.add(Calendar.MONTH, 2); + final String FIRST_REPAYMENT_DATE = dateFormat.format(todayDate.getTime()); + + todayDate = Calendar.getInstance(); + todayDate.add(Calendar.DATE, -2); + + loanStatusHashMap = this.loanTransactionHelper.disburseLoan(LOAN_DISBURSEMENT_DATE, loanID); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + + this.loanTransactionHelper.addChargesForLoan( + loanID, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(flatSpecifiedDueDate), + dateFormat.format(todayDate.getTime()), String.valueOf(PENALTY_PORTION))); + todayDate.add(Calendar.DATE, 1); + String runOndate = dateFormat.format(todayDate.getTime()); + this.loanTransactionHelper + .addChargesForLoan( + loanID, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(flat), runOndate, + String.valueOf(FEE_PORTION))); + + // CHECK ACCOUNT ENTRIES + System.out.println("Entries ......"); + final float PRINCIPAL_VALUE_FOR_EACH_PERIOD = 2000.0f; + final float TOTAL_INTEREST = 1000.0f; + final JournalEntry[] assetAccountInitialEntry = { new JournalEntry(this.LP_PRINCIPAL, JournalEntry.TransactionType.CREDIT), + new JournalEntry(this.LP_PRINCIPAL, JournalEntry.TransactionType.DEBIT), }; + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, LOAN_DISBURSEMENT_DATE, assetAccountInitialEntry); + + this.periodicAccrualAccountingHelper.runPeriodicAccrualAccounting(runOndate); + + // MAKE 1 + final float INTEREST_3_DAYS = 10.0F; + this.loanTransactionHelper.checkAccrualTransactionForRepayment(getDateAsLocalDate(runOndate), INTEREST_3_DAYS, FEE_PORTION, + PENALTY_PORTION, loanID); + + } + private Integer createLoanProductWithPeriodicAccrualAccountingEnabled(final Account... accounts) { System.out.println("------------------------------CREATING NEW LOAN PRODUCT ---------------------------------------"); final String loanProductJSON = new LoanProductTestBuilder().withPrincipal(this.LP_PRINCIPAL.toString()).withRepaymentTypeAsMonth() @@ -774,4 +928,5 @@ public class AccountingScenarioIntegrationTest { } return date; } + } diff --git a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/accounting/PeriodicAccrualAccountingHelper.java b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/accounting/PeriodicAccrualAccountingHelper.java new file mode 100755 index 000000000..4d5dc0e10 --- /dev/null +++ b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/accounting/PeriodicAccrualAccountingHelper.java @@ -0,0 +1,35 @@ +package org.mifosplatform.integrationtests.common.accounting; + +import java.util.HashMap; + +import org.mifosplatform.integrationtests.common.Utils; + +import com.google.gson.Gson; +import com.jayway.restassured.specification.RequestSpecification; +import com.jayway.restassured.specification.ResponseSpecification; + +public class PeriodicAccrualAccountingHelper { + + private static final String PERIODIC_ACCRUAL_URL = "/mifosng-provider/api/v1/accrualaccounting"; + private final RequestSpecification requestSpec; + private final ResponseSpecification responseSpec; + + public PeriodicAccrualAccountingHelper(final RequestSpecification requestSpec, final ResponseSpecification responseSpec) { + this.requestSpec = requestSpec; + this.responseSpec = responseSpec; + } + + public Object runPeriodicAccrualAccounting(String date) { + String json = getRunPeriodicAccrual(date); + return Utils.performServerPost(this.requestSpec, this.responseSpec, PERIODIC_ACCRUAL_URL + "?" + Utils.TENANT_IDENTIFIER, json, ""); + } + + private String getRunPeriodicAccrual(String date) { + final HashMap map = new HashMap<>(); + map.put("dateFormat", "dd MMMM yyyy"); + map.put("locale", "en_GB"); + map.put("accrueTill", date); + return new Gson().toJson(map); + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/accrual/api/AccrualAccountingApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/accrual/api/AccrualAccountingApiResource.java new file mode 100755 index 000000000..e7e558b2c --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/accrual/api/AccrualAccountingApiResource.java @@ -0,0 +1,51 @@ +/** + * 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.accounting.accrual.api; + +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import org.mifosplatform.commands.domain.CommandWrapper; +import org.mifosplatform.commands.service.CommandWrapperBuilder; +import org.mifosplatform.commands.service.PortfolioCommandSourceWritePlatformService; +import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; +import org.mifosplatform.infrastructure.core.serialization.DefaultToApiJsonSerializer; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +@Path("/accrualaccounting") +@Component +@Scope("singleton") +public class AccrualAccountingApiResource { + + private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; + private final DefaultToApiJsonSerializer apiJsonSerializerService; + + @Autowired + public AccrualAccountingApiResource(final DefaultToApiJsonSerializer apiJsonSerializerService, + final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService) { + this.commandsSourceWritePlatformService = commandsSourceWritePlatformService; + this.apiJsonSerializerService = apiJsonSerializerService; + + } + + @POST + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String executePeriodicAccrualAccounting(final String jsonRequestBody) { + + final CommandWrapper commandRequest = new CommandWrapperBuilder().excuteAccrualAccounting().withJson(jsonRequestBody).build(); + + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return this.apiJsonSerializerService.serialize(result); + } + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/accrual/api/AccrualAccountingConstants.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/accrual/api/AccrualAccountingConstants.java new file mode 100755 index 000000000..88a892f8e --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/accrual/api/AccrualAccountingConstants.java @@ -0,0 +1,18 @@ +package org.mifosplatform.accounting.accrual.api; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class AccrualAccountingConstants { + + public static final String accrueTillParamName = "accrueTill"; + public static final String localeParamName = "locale"; + public static final String dateFormatParamName = "dateFormat"; + + public static final String PERIODIC_ACCRUAL_ACCOUNTING_RESOURCE_NAME = "periodicaccrual"; + public static final String PERIODIC_ACCRUAL_ACCOUNTING_EXECUTION_ERROR_CODE = "execution.failed"; + + public static final Set LOAN_PERIODIC_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(accrueTillParamName, + localeParamName, dateFormatParamName)); +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/accrual/handler/ExecutePeriodicAccrualCommandHandler.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/accrual/handler/ExecutePeriodicAccrualCommandHandler.java new file mode 100755 index 000000000..c2e41ce3a --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/accrual/handler/ExecutePeriodicAccrualCommandHandler.java @@ -0,0 +1,31 @@ +/** + * 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.accounting.accrual.handler; + +import org.mifosplatform.accounting.accrual.service.AccrualAccountingWritePlatformService; +import org.mifosplatform.commands.handler.NewCommandSourceHandler; +import org.mifosplatform.infrastructure.core.api.JsonCommand; +import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class ExecutePeriodicAccrualCommandHandler implements NewCommandSourceHandler { + + private final AccrualAccountingWritePlatformService writePlatformService; + + @Autowired + public ExecutePeriodicAccrualCommandHandler(final AccrualAccountingWritePlatformService writePlatformService) { + this.writePlatformService = writePlatformService; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + return this.writePlatformService.executeLoansPeriodicAccrual(command); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/accrual/serialization/AccrualAccountingDataValidator.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/accrual/serialization/AccrualAccountingDataValidator.java new file mode 100755 index 000000000..70429540c --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/accrual/serialization/AccrualAccountingDataValidator.java @@ -0,0 +1,66 @@ +/** + * 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.accounting.accrual.serialization; + +import static org.mifosplatform.accounting.accrual.api.AccrualAccountingConstants.LOAN_PERIODIC_REQUEST_DATA_PARAMETERS; +import static org.mifosplatform.accounting.accrual.api.AccrualAccountingConstants.PERIODIC_ACCRUAL_ACCOUNTING_RESOURCE_NAME; +import static org.mifosplatform.accounting.accrual.api.AccrualAccountingConstants.accrueTillParamName; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +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.serialization.FromApiJsonDeserializer; +import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper; +import org.mifosplatform.portfolio.loanaccount.guarantor.command.GuarantorCommand; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.google.gson.JsonElement; +import com.google.gson.reflect.TypeToken; + +/** + * Implementation of {@link FromApiJsonDeserializer} for + * {@link GuarantorCommand}'s. + */ +@Component +public final class AccrualAccountingDataValidator { + + private final FromJsonHelper fromApiJsonHelper; + + @Autowired + public AccrualAccountingDataValidator(final FromJsonHelper fromApiJsonfromApiJsonHelper) { + this.fromApiJsonHelper = fromApiJsonfromApiJsonHelper; + } + + public void validateLoanPeriodicAccrualData(final String json) { + if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); } + final Type typeOfMap = new TypeToken>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, LOAN_PERIODIC_REQUEST_DATA_PARAMETERS); + + final JsonElement element = this.fromApiJsonHelper.parse(json); + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(PERIODIC_ACCRUAL_ACCOUNTING_RESOURCE_NAME); + + final LocalDate date = this.fromApiJsonHelper.extractLocalDateNamed(accrueTillParamName, element); + baseDataValidator.reset().parameter(accrueTillParamName).value(date).notNull().validateDateBefore(LocalDate.now()); + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + + } + + public 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/accounting/accrual/service/AccrualAccountingWritePlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/accrual/service/AccrualAccountingWritePlatformService.java new file mode 100755 index 000000000..492636dcb --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/accrual/service/AccrualAccountingWritePlatformService.java @@ -0,0 +1,15 @@ +/** + * 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.accounting.accrual.service; + +import org.mifosplatform.infrastructure.core.api.JsonCommand; +import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; + +public interface AccrualAccountingWritePlatformService { + + CommandProcessingResult executeLoansPeriodicAccrual(JsonCommand command); + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/accrual/service/AccrualAccountingWritePlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/accrual/service/AccrualAccountingWritePlatformServiceImpl.java new file mode 100755 index 000000000..3649c58c3 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/accrual/service/AccrualAccountingWritePlatformServiceImpl.java @@ -0,0 +1,54 @@ +/** + * 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.accounting.accrual.service; + +import static org.mifosplatform.accounting.accrual.api.AccrualAccountingConstants.PERIODIC_ACCRUAL_ACCOUNTING_EXECUTION_ERROR_CODE; +import static org.mifosplatform.accounting.accrual.api.AccrualAccountingConstants.PERIODIC_ACCRUAL_ACCOUNTING_RESOURCE_NAME; +import static org.mifosplatform.accounting.accrual.api.AccrualAccountingConstants.accrueTillParamName; + +import java.util.ArrayList; +import java.util.List; + +import org.joda.time.LocalDate; +import org.mifosplatform.accounting.accrual.serialization.AccrualAccountingDataValidator; +import org.mifosplatform.infrastructure.core.api.JsonCommand; +import org.mifosplatform.infrastructure.core.data.ApiParameterError; +import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; +import org.mifosplatform.infrastructure.core.data.DataValidatorBuilder; +import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException; +import org.mifosplatform.portfolio.loanaccount.service.LoanAccrualWritePlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class AccrualAccountingWritePlatformServiceImpl implements AccrualAccountingWritePlatformService { + + private final LoanAccrualWritePlatformService loanAccrualWritePlatformService; + private final AccrualAccountingDataValidator accountingDataValidator; + + @Autowired + public AccrualAccountingWritePlatformServiceImpl(final LoanAccrualWritePlatformService loanAccrualWritePlatformService, + final AccrualAccountingDataValidator accountingDataValidator) { + this.loanAccrualWritePlatformService = loanAccrualWritePlatformService; + this.accountingDataValidator = accountingDataValidator; + } + + @Override + public CommandProcessingResult executeLoansPeriodicAccrual(JsonCommand command) { + this.accountingDataValidator.validateLoanPeriodicAccrualData(command.json()); + LocalDate tilldate = command.localDateValueOfParameterNamed(accrueTillParamName); + String errorlog = this.loanAccrualWritePlatformService.addPeriodicAccruals(tilldate); + if (errorlog.length() > 0) { + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(PERIODIC_ACCRUAL_ACCOUNTING_RESOURCE_NAME); + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode(PERIODIC_ACCRUAL_ACCOUNTING_EXECUTION_ERROR_CODE, errorlog); + throw new PlatformApiDataValidationException(dataValidationErrors); + } + return CommandProcessingResult.empty(); + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java b/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java index 75bef8a76..8e11783f3 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java @@ -266,6 +266,14 @@ public class CommandWrapper { public boolean isJournalEntryResource() { return this.entityName.equalsIgnoreCase("JOURNALENTRY"); } + + public boolean isPeriodicAccrualResource() { + return this.entityName.equalsIgnoreCase("PERIODICACCRUALACCOUNTING"); + } + + public boolean isExecute() { + return this.actionName.equalsIgnoreCase("EXECUTE"); + } public boolean isRevertJournalEntry() { return this.actionName.equalsIgnoreCase("REVERSE") && this.entityName.equalsIgnoreCase("JOURNALENTRY"); 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 111337068..d0093fb38 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 @@ -808,6 +808,14 @@ public class CommandWrapperBuilder { this.href = "/glclosures/" + glClosureId; return this; } + + public CommandWrapperBuilder excuteAccrualAccounting() { + this.actionName = "EXECUTE"; + this.entityName = "PERIODICACCRUALACCOUNTING"; + this.entityId = null; + this.href = "/accrualaccounting"; + return this; + } public CommandWrapperBuilder createGLAccount() { this.actionName = "CREATE"; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java index 547264c41..f6f3885a4 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java @@ -418,6 +418,10 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer } else { throw new UnsupportedCommandException(wrapper.commandName()); } + } else if (wrapper.isPeriodicAccrualResource()) { + if (wrapper.isExecute()) { + handler = this.applicationContext.getBean("executePeriodicAccrualCommandHandler", NewCommandSourceHandler.class); + } } else if (wrapper.isSavingsProductResource()) { if (wrapper.isCreate()) { handler = this.applicationContext.getBean("createSavingsProductCommandHandler", NewCommandSourceHandler.class); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/jobs/service/JobName.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/jobs/service/JobName.java index 82c6f29d7..a8a413bc7 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/jobs/service/JobName.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/jobs/service/JobName.java @@ -16,7 +16,8 @@ public enum JobName { ADD_ACCRUAL_ENTRIES("Add Accrual Transactions"), UPDATE_NPA("Update Non Performing Assets"), UPDATE_DEPOSITS_ACCOUNT_MATURITY_DETAILS("Update Deposit Accounts Maturity details"), - TRANSFER_INTEREST_TO_SAVINGS("Transfer Interest To Savings"); + TRANSFER_INTEREST_TO_SAVINGS("Transfer Interest To Savings"), + ADD_PERIODIC_ACCRUAL_ENTRIES("Add Periodic Accrual Transactions"); private final String name; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/common/domain/DaysInMonthType.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/common/domain/DaysInMonthType.java new file mode 100755 index 000000000..e80269fdb --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/common/domain/DaysInMonthType.java @@ -0,0 +1,67 @@ +/** + * 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.common.domain; + +import java.util.ArrayList; +import java.util.List; + +/** + *
    + * People typically use either of the following settings when calculating there + * interest using the daily method: + *
  • 360 and
  • + *
  • 365
  • + *
+ */ +public enum DaysInMonthType { + + INVALID(0, "DaysInMonthType.invalid"), // + ACTUAL(1, "DaysInMonthType.actual"), // + DAYS_30(30, "DaysInMonthType.days360"); + + private final Integer value; + private final String code; + + private DaysInMonthType(final Integer value, final String code) { + this.value = value; + this.code = code; + } + + public Integer getValue() { + return this.value; + } + + public String getCode() { + return this.code; + } + + public static Object[] integerValues() { + final List values = new ArrayList<>(); + for (final DaysInMonthType enumType : values()) { + if (enumType.getValue() > 0) { + values.add(enumType.getValue()); + } + } + + return values.toArray(); + } + + public static DaysInMonthType fromInt(final Integer type) { + DaysInMonthType repaymentFrequencyType = DaysInMonthType.INVALID; + if (type != null) { + switch (type) { + case 1: + repaymentFrequencyType = DaysInMonthType.ACTUAL; + break; + case 30: + repaymentFrequencyType = DaysInMonthType.DAYS_30; + break; + + } + } + return repaymentFrequencyType; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/common/domain/DaysInYearType.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/common/domain/DaysInYearType.java new file mode 100755 index 000000000..ffcb648d5 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/common/domain/DaysInYearType.java @@ -0,0 +1,74 @@ +/** + * 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.common.domain; + +import java.util.ArrayList; +import java.util.List; + +/** + *
    + * People typically use either of the following settings when calculating there + * interest using the daily method: + *
  • 360 and
  • + *
  • 365
  • + *
+ */ +public enum DaysInYearType { + + INVALID(0, "DaysInYearType.invalid"), // + ACTUAL(1, "DaysInYearType.actual"), // + DAYS_360(360, "DaysInYearType.days360"), // + DAYS_364(364, "DaysInYearType.days364"), // + DAYS_365(365, "DaysInYearType.days365"); + + private final Integer value; + private final String code; + + private DaysInYearType(final Integer value, final String code) { + this.value = value; + this.code = code; + } + + public Integer getValue() { + return this.value; + } + + public String getCode() { + return this.code; + } + + public static Object[] integerValues() { + final List values = new ArrayList<>(); + for (final DaysInYearType enumType : values()) { + if (enumType.getValue() > 0) { + values.add(enumType.getValue()); + } + } + + return values.toArray(); + } + + public static DaysInYearType fromInt(final Integer type) { + DaysInYearType repaymentFrequencyType = DaysInYearType.INVALID; + if (type != null) { + switch (type) { + case 1: + repaymentFrequencyType = DaysInYearType.ACTUAL; + break; + case 360: + repaymentFrequencyType = DaysInYearType.DAYS_360; + break; + case 364: + repaymentFrequencyType = DaysInYearType.DAYS_364; + break; + case 365: + repaymentFrequencyType = DaysInYearType.DAYS_365; + break; + } + } + return repaymentFrequencyType; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanScheduleAccrualData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanScheduleAccrualData.java index 57e06fdc0..54c406493 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanScheduleAccrualData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanScheduleAccrualData.java @@ -5,12 +5,19 @@ import java.util.Date; import org.joda.time.LocalDate; import org.mifosplatform.organisation.monetary.data.CurrencyData; +import org.mifosplatform.portfolio.common.domain.PeriodFrequencyType; public class LoanScheduleAccrualData { private final Long loanId; private final Long officeId; + private final LocalDate accruedTill; + private final PeriodFrequencyType repaymentFrequency; + private final Integer repayEvery; + private final Integer numberOfDaysInYear; + private final Integer numberOfDaysInMonth; private final LocalDate dueDate; + private final LocalDate fromDate; private final Long repaymentScheduleId; private final Long loanProductId; private final BigDecimal interestIncome; @@ -20,14 +27,22 @@ public class LoanScheduleAccrualData { private final BigDecimal accruedFeeIncome; private final BigDecimal accruedPenaltyIncome; private final CurrencyData currencyData; + private final BigDecimal dueDateFeeIncome; + private final BigDecimal dueDatePenaltyIncome; - public LoanScheduleAccrualData(final Long loanId, final Long officeId, final LocalDate dueDate, final Long repaymentScheduleId, - final Long loanProductId, final BigDecimal interestIncome, final BigDecimal feeIncome, final BigDecimal penaltyIncome, + public LoanScheduleAccrualData(final Long loanId, final Long officeId, final LocalDate accruedTill, final Integer numberOfDaysInMonth, + final Integer numberOfDaysInYear, final PeriodFrequencyType repaymentFrequency, final Integer repayEvery, + final LocalDate dueDate, final LocalDate fromDate, final Long repaymentScheduleId, final Long loanProductId, + final BigDecimal interestIncome, final BigDecimal feeIncome, final BigDecimal penaltyIncome, final BigDecimal accruedInterestIncome, final BigDecimal accruedFeeIncome, final BigDecimal accruedPenaltyIncome, - final CurrencyData currencyData) { + final CurrencyData currencyData, final BigDecimal dueDateFeeIncome, final BigDecimal dueDatePenaltyIncome) { this.loanId = loanId; this.officeId = officeId; + this.accruedTill = accruedTill; + this.numberOfDaysInMonth = numberOfDaysInMonth; + this.numberOfDaysInYear = numberOfDaysInYear; this.dueDate = dueDate; + this.fromDate = fromDate; this.repaymentScheduleId = repaymentScheduleId; this.loanProductId = loanProductId; this.interestIncome = interestIncome; @@ -37,6 +52,10 @@ public class LoanScheduleAccrualData { this.accruedInterestIncome = accruedInterestIncome; this.accruedPenaltyIncome = accruedPenaltyIncome; this.currencyData = currencyData; + this.repaymentFrequency = repaymentFrequency; + this.repayEvery = repayEvery; + this.dueDateFeeIncome = dueDateFeeIncome; + this.dueDatePenaltyIncome = dueDatePenaltyIncome; } public Long getLoanId() { @@ -91,4 +110,36 @@ public class LoanScheduleAccrualData { return this.currencyData; } + public LocalDate getAccruedTill() { + return this.accruedTill; + } + + public Integer getNumberOfDaysInYear() { + return this.numberOfDaysInYear; + } + + public Integer getNumberOfDaysInMonth() { + return this.numberOfDaysInMonth; + } + + public LocalDate getFromDateAsLocaldate() { + return this.fromDate; + } + + public PeriodFrequencyType getRepaymentFrequency() { + return this.repaymentFrequency; + } + + public Integer getRepayEvery() { + return this.repayEvery; + } + + public BigDecimal getDueDateFeeIncome() { + return this.dueDateFeeIncome; + } + + public BigDecimal getDueDatePenaltyIncome() { + return this.dueDatePenaltyIncome; + } + } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanAccrualWritePlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanAccrualWritePlatformService.java index a6eb34db2..658648231 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanAccrualWritePlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanAccrualWritePlatformService.java @@ -1,5 +1,6 @@ package org.mifosplatform.portfolio.loanaccount.service; +import org.joda.time.LocalDate; import org.mifosplatform.infrastructure.jobs.exception.JobExecutionException; @@ -7,4 +8,8 @@ public interface LoanAccrualWritePlatformService { void addAccrualAccounting() throws JobExecutionException; + void addPeriodicAccruals() throws JobExecutionException; + + String addPeriodicAccruals(LocalDate tilldate); + } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanAccrualWritePlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanAccrualWritePlatformServiceImpl.java index 4924dee22..32cca51d8 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanAccrualWritePlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanAccrualWritePlatformServiceImpl.java @@ -1,6 +1,7 @@ package org.mifosplatform.portfolio.loanaccount.service; import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; @@ -11,11 +12,15 @@ import java.util.Set; import javax.sql.DataSource; +import org.joda.time.Days; +import org.joda.time.LocalDate; import org.mifosplatform.accounting.journalentry.service.JournalEntryWritePlatformService; import org.mifosplatform.infrastructure.core.service.RoutingDataSource; 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.common.domain.DaysInMonthType; +import org.mifosplatform.portfolio.common.domain.DaysInYearType; import org.mifosplatform.portfolio.loanaccount.data.LoanScheduleAccrualData; import org.mifosplatform.portfolio.loanaccount.data.LoanTransactionEnumData; import org.mifosplatform.portfolio.loanaccount.domain.LoanTransactionType; @@ -72,11 +77,151 @@ public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlat if (sb.length() > 0) { throw new JobExecutionException(sb.toString()); } } + @Override + @CronTarget(jobName = JobName.ADD_PERIODIC_ACCRUAL_ENTRIES) + public void addPeriodicAccruals() throws JobExecutionException { + String errors = addPeriodicAccruals(LocalDate.now()); + if (errors.length() > 0) { throw new JobExecutionException(errors); } + } + + @Override + public String addPeriodicAccruals(final LocalDate tilldate) { + Collection loanScheduleAccrualDatas = this.loanReadPlatformService.retrivePeriodicAccrualData(tilldate); + return addPeriodicAccruals(tilldate, loanScheduleAccrualDatas); + } + + private String addPeriodicAccruals(final LocalDate tilldate, Collection loanScheduleAccrualDatas) { + StringBuilder sb = new StringBuilder(); + Set loansIds = new HashSet<>(); + for (final LoanScheduleAccrualData accrualData : loanScheduleAccrualDatas) { + try { + if (!loansIds.contains(accrualData.getLoanId())) { + if (accrualData.getDueDateAsLocaldate().isAfter(tilldate)) { + addAccrualTillSpecificDate(tilldate, accrualData); + } else { + addAccrualAccounting(accrualData); + } + } + } catch (Exception e) { + loansIds.add(accrualData.getLoanId()); + Throwable realCause = e; + if (e.getCause() != null) { + realCause = e.getCause(); + } + sb.append("failed to add accural transaction for repayment with id " + accrualData.getRepaymentScheduleId() + + " with message " + realCause.getMessage()); + } + } + return sb.toString(); + } + + private void addAccrualTillSpecificDate(final LocalDate tilldate, final LoanScheduleAccrualData accrualData) throws Exception { + int totalNumberOfDays = Days.daysBetween(accrualData.getFromDateAsLocaldate(), accrualData.getDueDateAsLocaldate()).getDays(); + LocalDate startDate = accrualData.getAccruedTill(); + if (startDate == null) { + startDate = accrualData.getFromDateAsLocaldate(); + } + int daysToBeAccrued = Days.daysBetween(startDate, tilldate).getDays(); + if(daysToBeAccrued < 1){ + return; + } + int daysInSchedule = Days.daysBetween(accrualData.getFromDateAsLocaldate(), tilldate).getDays(); + switch (accrualData.getRepaymentFrequency()) { + case MONTHS: + totalNumberOfDays = calculateTotalNumberOfDaysForMonth(totalNumberOfDays, accrualData); + break; + case YEARS: + totalNumberOfDays = calculateTotalNumberOfDaysForYear(totalNumberOfDays, accrualData); + break; + default: + break; + } + + double interestPerDay = accrualData.getInterestIncome().doubleValue() / totalNumberOfDays; + BigDecimal amount = BigDecimal.ZERO; + BigDecimal interestportion = null; + BigDecimal feeportion = accrualData.getDueDateFeeIncome(); + BigDecimal penaltyportion = accrualData.getDueDatePenaltyIncome(); + if (daysInSchedule >= totalNumberOfDays) { + interestportion = accrualData.getInterestIncome(); + } else { + double iterest = interestPerDay * daysToBeAccrued; + interestportion = BigDecimal.valueOf(iterest); + } + interestportion = interestportion.setScale(accrualData.getCurrencyData().decimalPlaces(), RoundingMode.HALF_EVEN); + amount = amount.add(interestportion); + + BigDecimal totalAccInterest = accrualData.getAccruedInterestIncome(); + if (totalAccInterest == null) { + totalAccInterest = BigDecimal.ZERO; + } + totalAccInterest = totalAccInterest.add(interestportion); + BigDecimal totalAccFee = accrualData.getAccruedFeeIncome(); + + if (feeportion != null) { + if (totalAccFee == null) { + totalAccFee = BigDecimal.ZERO; + } + feeportion = feeportion.subtract(totalAccFee); + amount = amount.add(feeportion); + totalAccFee = totalAccFee.add(feeportion); + if (feeportion.compareTo(BigDecimal.ZERO) == 0) { + feeportion = null; + } + } + BigDecimal totalAccPenalty = accrualData.getAccruedPenaltyIncome(); + if (penaltyportion != null) { + if (totalAccPenalty == null) { + totalAccPenalty = BigDecimal.ZERO; + } + penaltyportion = penaltyportion.subtract(totalAccPenalty); + amount = amount.add(penaltyportion); + totalAccPenalty = totalAccFee.add(penaltyportion); + if (penaltyportion.compareTo(BigDecimal.ZERO) == 0) { + penaltyportion = null; + } + } + if (amount.compareTo(BigDecimal.ZERO) != 0) { + addAccrualAccounting(accrualData, amount, interestportion, totalAccInterest, feeportion, totalAccFee, penaltyportion, + totalAccPenalty, tilldate); + } + } + + private int calculateTotalNumberOfDaysForMonth(final int totalNumberOfDays, final LoanScheduleAccrualData accrualData) { + int numberOfDays = totalNumberOfDays; + DaysInMonthType type = DaysInMonthType.fromInt(accrualData.getNumberOfDaysInMonth()); + switch (type) { + case DAYS_30: + numberOfDays = 30 * accrualData.getRepayEvery(); + break; + default: + break; + } + return numberOfDays; + } + + private int calculateTotalNumberOfDaysForYear(final int totalNumberOfDays, final LoanScheduleAccrualData accrualData) { + int numberOfDays = totalNumberOfDays; + DaysInYearType type = DaysInYearType.fromInt(accrualData.getNumberOfDaysInMonth()); + switch (type) { + case DAYS_360: + numberOfDays = 360 * accrualData.getRepayEvery(); + break; + case DAYS_364: + numberOfDays = 364 * accrualData.getRepayEvery(); + break; + case DAYS_365: + numberOfDays = 365 * accrualData.getRepayEvery(); + break; + default: + break; + } + return numberOfDays; + } + @Transactional public void addAccrualAccounting(LoanScheduleAccrualData scheduleAccrualData) throws Exception { - String transactionSql = "INSERT INTO m_loan_transaction (loan_id,office_id,is_reversed,transaction_type_enum,transaction_date,amount,interest_portion_derived," - + "fee_charges_portion_derived,penalty_charges_portion_derived) VALUES (?, ?, 0, ?, ?, ?, ?, ?, ?)"; BigDecimal amount = BigDecimal.ZERO; BigDecimal interestportion = null; BigDecimal totalAccInterest = null; @@ -119,11 +264,19 @@ public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlat penaltyportion = null; } } + addAccrualAccounting(scheduleAccrualData, amount, interestportion, totalAccInterest, feeportion, totalAccFee, penaltyportion, + totalAccPenalty, scheduleAccrualData.getDueDateAsLocaldate()); + } + + private void addAccrualAccounting(LoanScheduleAccrualData scheduleAccrualData, BigDecimal amount, BigDecimal interestportion, + BigDecimal totalAccInterest, BigDecimal feeportion, BigDecimal totalAccFee, BigDecimal penaltyportion, + BigDecimal totalAccPenalty, final LocalDate accruredTill) throws Exception { TransactionStatus transactionStatus = this.transactionManager.getTransaction(new DefaultTransactionDefinition()); try { + String transactionSql = "INSERT INTO m_loan_transaction (loan_id,office_id,is_reversed,transaction_type_enum,transaction_date,amount,interest_portion_derived," + + "fee_charges_portion_derived,penalty_charges_portion_derived) VALUES (?, ?, 0, ?, ?, ?, ?, ?, ?)"; this.jdbcTemplate.update(transactionSql, scheduleAccrualData.getLoanId(), scheduleAccrualData.getOfficeId(), - LoanTransactionType.ACCRUAL.getValue(), scheduleAccrualData.getDueDate(), amount, interestportion, feeportion, - penaltyportion); + LoanTransactionType.ACCRUAL.getValue(), accruredTill.toDate(), amount, interestportion, feeportion, penaltyportion); final Long transactonId = this.jdbcTemplate.queryForLong("SELECT LAST_INSERT_ID()"); Map transactionMap = toMapData(transactonId, amount, interestportion, feeportion, penaltyportion, @@ -135,7 +288,7 @@ public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlat scheduleAccrualData.getRepaymentScheduleId()); String updateLoan = "UPDATE m_loan SET accrued_till=? WHERE id=?"; - this.jdbcTemplate.update(updateLoan, scheduleAccrualData.getDueDate(), scheduleAccrualData.getLoanId()); + this.jdbcTemplate.update(updateLoan, accruredTill.toDate(), scheduleAccrualData.getLoanId()); final Map accountingBridgeData = deriveAccountingBridgeData(scheduleAccrualData, transactionMap); this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData); } catch (Exception e) { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanReadPlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanReadPlatformService.java index b4c133790..5e7d930af 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanReadPlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanReadPlatformService.java @@ -7,6 +7,7 @@ package org.mifosplatform.portfolio.loanaccount.service; import java.util.Collection; +import org.joda.time.LocalDate; import org.mifosplatform.infrastructure.core.service.Page; import org.mifosplatform.organisation.staff.data.StaffData; import org.mifosplatform.portfolio.calendar.data.CalendarData; @@ -84,4 +85,6 @@ public interface LoanReadPlatformService { LoanTransactionData retrieveRecoveryPaymentTemplate(Long loanId); LoanTransactionData retrieveLoanWriteoffTemplate(Long loanId); + + Collection retrivePeriodicAccrualData(LocalDate tillDate); } \ No newline at end of file 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 6ae419faa..61f6a4b3e 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 @@ -11,11 +11,15 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.apache.commons.lang.StringUtils; import org.joda.time.Days; import org.joda.time.LocalDate; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; import org.mifosplatform.accounting.common.AccountingRuleType; import org.mifosplatform.infrastructure.codes.data.CodeValueData; import org.mifosplatform.infrastructure.codes.service.CodeValueReadPlatformService; @@ -45,6 +49,7 @@ import org.mifosplatform.portfolio.charge.service.ChargeReadPlatformService; import org.mifosplatform.portfolio.client.data.ClientData; 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.fund.data.FundData; import org.mifosplatform.portfolio.fund.service.FundReadPlatformService; import org.mifosplatform.portfolio.group.data.GroupGeneralData; @@ -88,6 +93,7 @@ import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.ResultSetExtractor; import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; @@ -110,6 +116,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { private final StaffReadPlatformService staffReadPlatformService; private final PaginationHelper paginationHelper = new PaginationHelper<>(); private final LoanMapper loaanLoanMapper = new LoanMapper(); + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; + private final DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd"); @Autowired public LoanReadPlatformServiceImpl(final PlatformSecurityContext context, final LoanRepository loanRepository, @@ -134,6 +142,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { this.calendarReadPlatformService = calendarReadPlatformService; this.staffReadPlatformService = staffReadPlatformService; this.jdbcTemplate = new JdbcTemplate(dataSource); + this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource); } @Override @@ -1328,6 +1337,90 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { AccountingRuleType.ACCRUAL_PERIODIC.getValue() }); } + @Override + public Collection retrivePeriodicAccrualData(final LocalDate tillDate) { + + LoanSchedulePeriodicAccrualMapper mapper = new LoanSchedulePeriodicAccrualMapper(); + final StringBuilder sqlBuilder = new StringBuilder(400); + sqlBuilder + .append("select ") + .append(mapper.schema()) + .append(" where ((if(ls.fee_charges_waived_derived is null , ls.fee_charges_amount,(ls.fee_charges_amount-ls.fee_charges_waived_derived)) <> if(ls.accrual_fee_charges_derived is null,0, ls.accrual_fee_charges_derived))") + .append(" or (if(ls.penalty_charges_waived_derived is null , ls.penalty_charges_amount,(ls.penalty_charges_amount-ls.penalty_charges_waived_derived)) <> if(ls.accrual_penalty_charges_derived is null,0,ls.accrual_penalty_charges_derived))") + .append(" or (if(ls.interest_waived_derived is null , ls.interest_amount,(ls.interest_amount-ls.interest_waived_derived)) <> if(ls.accrual_interest_derived is null,0,ls.accrual_interest_derived)))") + .append(" and loan.loan_status_id=:active and mpl.accounting_type=:type and loan.is_npa=0 and (ls.duedate <= :tilldate or (ls.duedate > :tilldate and ls.fromdate < :tilldate)) order by loan.id,ls.duedate"); + Map paramMap = new HashMap<>(3); + paramMap.put("active", LoanStatus.ACTIVE.getValue()); + paramMap.put("type", AccountingRuleType.ACCRUAL_PERIODIC.getValue()); + paramMap.put("tilldate", formatter.print(tillDate)); + + return this.namedParameterJdbcTemplate.query(sqlBuilder.toString(), paramMap, mapper); + } + + private static final class LoanSchedulePeriodicAccrualMapper implements RowMapper { + + public String schema() { + final StringBuilder sqlBuilder = new StringBuilder(400); + sqlBuilder + .append("loan.id as loanId ,if(loan.client_id is null,mg.office_id,mc.office_id) as officeId,") + .append("loan.accrued_till as accruedTill, loan.repayment_period_frequency_enum as frequencyEnum, ") + .append("loan.repay_every as repayEvery,") + .append("(select sum(lc.amount) as dueamount from m_loan_charge lc where lc.loan_id = loan.id and lc.is_active = 1 and lc.waived = 0 and lc.is_penalty = 0 and lc.due_for_collection_as_of_date > ls.fromdate and lc.due_for_collection_as_of_date<= if(:tilldate < ls.duedate, :tilldate , ls.duedate)) as feedueIn,") + .append("(select sum(lc.amount) as dueamount from m_loan_charge lc where lc.loan_id = loan.id and lc.is_active = 1 and lc.waived = 0 and lc.is_penalty = 1 and lc.due_for_collection_as_of_date > ls.fromdate and lc.due_for_collection_as_of_date<= if(:tilldate < ls.duedate, :tilldate , ls.duedate)) as penalitydueIn,") + .append("ls.duedate as duedate,ls.fromdate as fromdate ,ls.id as scheduleId,loan.product_id as productId,") + .append("if(ls.interest_waived_derived is null,ls.interest_amount,(ls.interest_amount-ls.interest_waived_derived)) as interest,") + .append("if(ls.penalty_charges_waived_derived is null , ls.penalty_charges_amount,(ls.penalty_charges_amount-ls.penalty_charges_waived_derived)) as penalty,") + .append("if(ls.fee_charges_waived_derived is null , ls.fee_charges_amount,(ls.fee_charges_amount-ls.fee_charges_waived_derived)) as charges,") + .append("ls.accrual_interest_derived as accinterest,ls.accrual_fee_charges_derived as accfeecharege,ls.accrual_penalty_charges_derived as accpenalty,") + .append(" loan.currency_code as currencyCode,loan.currency_digits as currencyDigits,loan.currency_multiplesof as inMultiplesOf,") + .append("curr.display_symbol as currencyDisplaySymbol,curr.name as currencyName,curr.internationalized_name_code as currencyNameCode") + .append(" from m_loan_repayment_schedule ls ").append(" left join m_loan loan on loan.id=ls.loan_id ") + .append(" left join m_product_loan mpl on mpl.id = loan.product_id") + .append(" left join m_client mc on mc.id = loan.client_id ").append(" left join m_group mg on mg.id = loan.group_id") + .append(" left join m_currency curr on curr.code = loan.currency_code"); + return sqlBuilder.toString(); + } + + @Override + public LoanScheduleAccrualData mapRow(ResultSet rs, @SuppressWarnings("unused") int rowNum) throws SQLException { + + final Long loanId = rs.getLong("loanId"); + final Long officeId = rs.getLong("officeId"); + final LocalDate accruedTill = JdbcSupport.getLocalDate(rs, "accruedTill"); + final Integer numberOfDaysInMonth = 30; + final Integer numberOfDaysInYear = 360; + final Integer frequencyEnum = JdbcSupport.getInteger(rs, "frequencyEnum"); + final Integer repayEvery = JdbcSupport.getInteger(rs, "repayEvery"); + final PeriodFrequencyType frequency = PeriodFrequencyType.fromInt(frequencyEnum); + final LocalDate dueDate = JdbcSupport.getLocalDate(rs, "duedate"); + final LocalDate fromDate = JdbcSupport.getLocalDate(rs, "fromdate"); + final Long repaymentScheduleId = rs.getLong("scheduleId"); + final Long loanProductId = rs.getLong("productId"); + final BigDecimal interestIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "interest"); + final BigDecimal feeIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "charges"); + final BigDecimal penaltyIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "penalty"); + final BigDecimal accruedInterestIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accinterest"); + final BigDecimal accruedFeeIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accfeecharege"); + final BigDecimal accruedPenaltyIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accpenalty"); + + final String currencyCode = rs.getString("currencyCode"); + final String currencyName = rs.getString("currencyName"); + final String currencyNameCode = rs.getString("currencyNameCode"); + final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol"); + final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits"); + final Integer inMultiplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf"); + final CurrencyData currencyData = new CurrencyData(currencyCode, currencyName, currencyDigits, inMultiplesOf, + currencyDisplaySymbol, currencyNameCode); + + final BigDecimal dueDateFeeIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "feedueIn"); + final BigDecimal dueDatePenaltyIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "penalitydueIn"); + return new LoanScheduleAccrualData(loanId, officeId, accruedTill, numberOfDaysInMonth, numberOfDaysInYear, frequency, + repayEvery, dueDate, fromDate, repaymentScheduleId, loanProductId, interestIncome, feeIncome, penaltyIncome, + accruedInterestIncome, accruedFeeIncome, accruedPenaltyIncome, currencyData, dueDateFeeIncome, dueDatePenaltyIncome); + } + + } + private static final class LoanScheduleAccrualMapper implements RowMapper { public String schema() { @@ -1371,10 +1464,18 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { final Integer inMultiplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf"); final CurrencyData currencyData = new CurrencyData(currencyCode, currencyName, currencyDigits, inMultiplesOf, currencyDisplaySymbol, currencyNameCode); - return new LoanScheduleAccrualData(loanId, officeId, dueDate, repaymentScheduleId, loanProductId, interestIncome, feeIncome, - penaltyIncome, accruedInterestIncome, accruedFeeIncome, accruedPenaltyIncome, currencyData); + final LocalDate accruedTill = null; + final Integer numberOfDaysInMonth = null; + final Integer numberOfDaysInYear = null; + final PeriodFrequencyType frequency = null; + final Integer repayEvery = null; + final LocalDate fromDate = null; + final BigDecimal dueDateFeeincome = null; + final BigDecimal dueDatePenaltyIncome = null; + return new LoanScheduleAccrualData(loanId, officeId, accruedTill, numberOfDaysInMonth, numberOfDaysInYear, frequency, + repayEvery, dueDate, fromDate, repaymentScheduleId, loanProductId, interestIncome, feeIncome, penaltyIncome, + accruedInterestIncome, accruedFeeIncome, accruedPenaltyIncome, currencyData, dueDateFeeincome, dueDatePenaltyIncome); } - } @Override