From bf841ebb16979658cdc00f97cc6b80e43204d6bb Mon Sep 17 00:00:00 2001 From: wuyanna Date: Fri, 10 May 2013 22:00:55 +0800 Subject: [PATCH 01/49] mifosx 344 --- .../SavingsAccountWritePlatformService.java | 2 ++ ...WritePlatformServiceJpaRepositoryImpl.java | 23 ++++++++++++++ .../CheckAnnualFeeDueDateScheduledJob.java | 31 +++++++++++++++++++ .../service/ScheduledJobRunnerService.java | 2 ++ .../ScheduledJobRunnerServiceImpl.java | 25 ++++++++++++++- .../resources/META-INF/spring/scheduler.xml | 16 ++++++++++ 6 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/CheckAnnualFeeDueDateScheduledJob.java diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountWritePlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountWritePlatformService.java index 51a760d2f..8796bd81b 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountWritePlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountWritePlatformService.java @@ -21,6 +21,8 @@ public interface SavingsAccountWritePlatformService { CommandProcessingResult deposit(Long savingsId, JsonCommand command); CommandProcessingResult withdrawal(Long savingsId, JsonCommand command); + + CommandProcessingResult addAnnualFee(Long savingsId); CommandProcessingResult calculateInterest(Long savingsId, JsonCommand command); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java index 00ecddd65..3b3dbc98c 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java @@ -313,6 +313,29 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi .withSavingsId(savingsId) // .build(); } + + @Transactional + @Override + public CommandProcessingResult addAnnualFee(final Long savingsId) { + + final SavingsAccount account = this.savingAccountRepository.findOneWithNotFoundDetection(savingsId); + + final LocalDate transactionDate = LocalDate.now(); + + final DateTimeFormatter fmt = DateTimeFormat.forPattern("dd MM yyyy"); + + final SavingsAccountTransaction annualFee = account.addAnnualFee(fmt, transactionDate); + final Long transactionId = saveTransactionToGenerateTransactionId(annualFee); + this.savingAccountRepository.save(account); + + return new CommandProcessingResultBuilder() // + .withEntityId(transactionId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .build(); + } @Override public CommandProcessingResult calculateInterest(final Long savingsId, final JsonCommand command) { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/CheckAnnualFeeDueDateScheduledJob.java b/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/CheckAnnualFeeDueDateScheduledJob.java new file mode 100644 index 000000000..561920ac8 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/CheckAnnualFeeDueDateScheduledJob.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.scheduledjobs; + +import java.util.Date; + +import org.mifosplatform.scheduledjobs.service.ScheduledJobRunnerService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CheckAnnualFeeDueDateScheduledJob { + + private final static Logger logger = LoggerFactory.getLogger(CheckAnnualFeeDueDateScheduledJob.class); + + private final ScheduledJobRunnerService scheduledJobRunnerService; + + public CheckAnnualFeeDueDateScheduledJob(final ScheduledJobRunnerService scheduledJobRunnerService) { + this.scheduledJobRunnerService = scheduledJobRunnerService; + } + + public void execute() { + logger.info("Running Job C " + new Date()); + + this.scheduledJobRunnerService.checkAnnualFeeDueDate(); + + logger.info("Finishing Job C " + new Date()); + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerService.java b/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerService.java index 327f48cd9..b1b1cf6a9 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerService.java @@ -10,4 +10,6 @@ public interface ScheduledJobRunnerService { void updateLoanSummaryDetails(); void updateLoanArrearsAgeingDetails(); + + void checkAnnualFeeDueDate(); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerServiceImpl.java index 2a4fab657..b98772ecc 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerServiceImpl.java @@ -5,10 +5,13 @@ */ package org.mifosplatform.scheduledjobs.service; +import java.util.List; + import org.mifosplatform.infrastructure.core.domain.MifosPlatformTenant; import org.mifosplatform.infrastructure.core.service.DataSourcePerTenantService; import org.mifosplatform.infrastructure.core.service.ThreadLocalContextUtil; import org.mifosplatform.infrastructure.security.service.TenantDetailsService; +import org.mifosplatform.portfolio.savings.service.SavingsAccountWritePlatformService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -23,12 +26,14 @@ public class ScheduledJobRunnerServiceImpl implements ScheduledJobRunnerService private final TenantDetailsService tenantDetailsService; private final DataSourcePerTenantService dataSourcePerTenantService; + private final SavingsAccountWritePlatformService savingsAccountWritePlatformService; @Autowired public ScheduledJobRunnerServiceImpl(final DataSourcePerTenantService dataSourcePerTenantService, - final TenantDetailsService tenantDetailsService) { + final TenantDetailsService tenantDetailsService, final SavingsAccountWritePlatformService savingsAccountWritePlatformService) { this.dataSourcePerTenantService = dataSourcePerTenantService; this.tenantDetailsService = tenantDetailsService; + this.savingsAccountWritePlatformService = savingsAccountWritePlatformService; } @Transactional @@ -137,4 +142,22 @@ public class ScheduledJobRunnerServiceImpl implements ScheduledJobRunnerService logger.info("Results affected by update: " + result); } + + @Override + public void checkAnnualFeeDueDate() { + final MifosPlatformTenant tenant = this.tenantDetailsService.loadTenantById("default"); + + ThreadLocalContextUtil.setTenant(tenant); + + JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSourcePerTenantService.retrieveTenantAwareDataSource()); + + String sql = "SELECT ms.id as savingsAccountId FROM m_savings_account ms WHERE ms.annual_fee_next_due_date <= NOW() "; + List savingsIds = jdbcTemplate.queryForList(sql, Long.class); + + for (Long savingsId : savingsIds) { + this.savingsAccountWritePlatformService.addAnnualFee(savingsId); + } + + logger.info("Results affected by update: " + savingsIds.size()); + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/resources/META-INF/spring/scheduler.xml b/mifosng-provider/src/main/resources/META-INF/spring/scheduler.xml index b4debf546..bca9f2b1e 100644 --- a/mifosng-provider/src/main/resources/META-INF/spring/scheduler.xml +++ b/mifosng-provider/src/main/resources/META-INF/spring/scheduler.xml @@ -16,6 +16,9 @@ + + + @@ -30,6 +33,13 @@ + + + + + + + @@ -40,12 +50,18 @@ + + + + + + From 2d4050de12f55d3d1c604ae3cfeb6e97f4d7d97a Mon Sep 17 00:00:00 2001 From: mifoscontributer Date: Tue, 28 May 2013 10:43:44 +0530 Subject: [PATCH 02/49] Defining accounting rule with Tagged Accounts Formatting the white spaces --- .../V45__create_acc_rule_tags_table.sql | 11 + ...WritePlatformServiceJpaRepositoryImpl.java | 2 +- .../rule/api/AccountingRuleApiResource.java | 78 ++++--- .../api/AccountingRuleJsonInputParams.java | 2 +- .../rule/data/AccountingRuleData.java | 192 +++++++++++------- .../rule/data/AccountingTagRuleData.java | 20 ++ .../rule/domain/AccountingRule.java | 23 ++- .../rule/domain/AccountingTagRule.java | 40 ++++ ...ingRuleCommandFromApiJsonDeserializer.java | 3 +- .../service/AccountingRuleDataValidator.java | 147 ++++++++++++++ .../AccountingRuleReadPlatformService.java | 2 +- ...AccountingRuleReadPlatformServiceImpl.java | 157 +++++++++++--- ...WritePlatformServiceJpaRepositoryImpl.java | 100 +++++++-- 13 files changed, 624 insertions(+), 153 deletions(-) create mode 100644 mifosng-db/migrations/core_db/V45__create_acc_rule_tags_table.sql create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/data/AccountingTagRuleData.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/domain/AccountingTagRule.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleDataValidator.java diff --git a/mifosng-db/migrations/core_db/V45__create_acc_rule_tags_table.sql b/mifosng-db/migrations/core_db/V45__create_acc_rule_tags_table.sql new file mode 100644 index 000000000..b17bc2e01 --- /dev/null +++ b/mifosng-db/migrations/core_db/V45__create_acc_rule_tags_table.sql @@ -0,0 +1,11 @@ +create table `acc_rule_tags` ( + `id` BIGINT(20) NOT NULL AUTO_INCREMENT, + `acc_rule_id` BIGINT(20) NOT NULL, + `tag_id` INT(11) NOT NULL, + `acc_type_enum` SMALLINT(5) NOT NULL, + primary key(`id`), + INDEX `FK_acc_accounting_rule_id` (`acc_rule_id`), + INDEX `FK_m_code_value_id` (`tag_id`), + CONSTRAINT `FK_acc_accounting_rule_id` FOREIGN KEY (`acc_rule_id`) REFERENCES `acc_accounting_rule` (`id`), + CONSTRAINT `FK_m_code_value_id` FOREIGN KEY (`tag_id`) REFERENCES `m_code_value` (`id`) +); \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java index 4f04299cf..dd3e63383 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java @@ -112,7 +112,7 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa validateGLAccountForTransaction(creditAccountHead); final GLAccount debitAccountHead = accountingRule.getAccountToDebit(); validateGLAccountForTransaction(debitAccountHead); - String description = accountingRule.getDescription(); + String description = null; if (StringUtils.isNotBlank(journalEntryCommand.getComments())) { description = journalEntryCommand.getComments(); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/api/AccountingRuleApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/api/AccountingRuleApiResource.java index a5701ab94..961847d75 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/api/AccountingRuleApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/api/AccountingRuleApiResource.java @@ -6,6 +6,7 @@ package org.mifosplatform.accounting.rule.api; import java.util.Arrays; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -23,6 +24,7 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.UriInfo; +import org.mifosplatform.accounting.common.AccountingConstants; import org.mifosplatform.accounting.glaccount.data.GLAccountData; import org.mifosplatform.accounting.glaccount.service.GLAccountReadPlatformService; import org.mifosplatform.accounting.rule.data.AccountingRuleData; @@ -30,6 +32,8 @@ import org.mifosplatform.accounting.rule.service.AccountingRuleReadPlatformServi import org.mifosplatform.commands.domain.CommandWrapper; import org.mifosplatform.commands.service.CommandWrapperBuilder; import org.mifosplatform.commands.service.PortfolioCommandSourceWritePlatformService; +import org.mifosplatform.infrastructure.codes.data.CodeValueData; +import org.mifosplatform.infrastructure.codes.service.CodeValueReadPlatformService; import org.mifosplatform.infrastructure.core.api.ApiRequestParameterHelper; import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; import org.mifosplatform.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; @@ -47,7 +51,9 @@ import org.springframework.stereotype.Component; public class AccountingRuleApiResource { private static final Set RESPONSE_DATA_PARAMETERS = new HashSet(Arrays.asList("id", "officeId", "officeName", - "accountToDebitId", "accountToCreditId", "name", "description", "systemDefined")); + "accountToDebitId", "accountToCreditId", "name", "description", "systemDefined", "allowedAssetsTagOptions", + "allowedLiabilitiesTagOptions", "allowedEquityTagOptions", "allowedIncomeTagOptions", "allowedExpensesTagOptions", "debitTags", + "creditTags")); private final String resourceNameForPermission = "ACCOUNTINGRULE"; @@ -58,6 +64,7 @@ public class AccountingRuleApiResource { private final ApiRequestParameterHelper apiRequestParameterHelper; private final PlatformSecurityContext context; private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; + private final CodeValueReadPlatformService codeValueReadPlatformService; @Autowired public AccountingRuleApiResource(final PlatformSecurityContext context, @@ -65,7 +72,7 @@ public class AccountingRuleApiResource { final DefaultToApiJsonSerializer toApiJsonSerializer, final ApiRequestParameterHelper apiRequestParameterHelper, final GLAccountReadPlatformService accountReadPlatformService, final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService, - final OfficeReadPlatformService officeReadPlatformService) { + final OfficeReadPlatformService officeReadPlatformService, final CodeValueReadPlatformService codeValueReadPlatformService) { this.context = context; this.apiRequestParameterHelper = apiRequestParameterHelper; this.commandsSourceWritePlatformService = commandsSourceWritePlatformService; @@ -73,22 +80,23 @@ public class AccountingRuleApiResource { this.accountingRuleReadPlatformService = accountingRuleReadPlatformService; this.officeReadPlatformService = officeReadPlatformService; this.accountReadPlatformService = accountReadPlatformService; + this.codeValueReadPlatformService = codeValueReadPlatformService; } - + @GET - @Path("template") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - public String retrieveTemplate(@Context final UriInfo uriInfo) { - - context.authenticatedUser().validateHasReadPermission(resourceNameForPermission); - - AccountingRuleData accountingRuleData = null; - accountingRuleData = handleTemplate(accountingRuleData); - - final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters()); - return this.apiJsonSerializerService.serialize(settings, accountingRuleData, RESPONSE_DATA_PARAMETERS); - } + @Path("template") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String retrieveTemplate(@Context final UriInfo uriInfo) { + + this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermission); + + AccountingRuleData accountingRuleData = null; + accountingRuleData = handleTemplate(accountingRuleData); + + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.apiJsonSerializerService.serialize(settings, accountingRuleData, RESPONSE_DATA_PARAMETERS); + } @GET @Consumes({ MediaType.APPLICATION_JSON }) @@ -114,7 +122,7 @@ public class AccountingRuleApiResource { AccountingRuleData accountingRuleData = this.accountingRuleReadPlatformService.retrieveAccountingRuleById(accountingRuleId); if (settings.isTemplate()) { - accountingRuleData = handleTemplate(accountingRuleData); + accountingRuleData = handleTemplate(accountingRuleData); } return this.apiJsonSerializerService.serialize(settings, accountingRuleData, RESPONSE_DATA_PARAMETERS); @@ -157,13 +165,31 @@ public class AccountingRuleApiResource { return this.apiJsonSerializerService.serialize(result); } - - private AccountingRuleData handleTemplate(AccountingRuleData accountingRuleData) { - List allowedAccounts = this.accountReadPlatformService.retrieveAllEnabledDetailGLAccounts(); - List allowedOffices = (List) this.officeReadPlatformService.retrieveAllOfficesForDropdown(); - if (accountingRuleData == null) { - return new AccountingRuleData(allowedAccounts, allowedOffices); - } - return new AccountingRuleData(accountingRuleData, allowedAccounts, allowedOffices); - } + + private AccountingRuleData handleTemplate(final AccountingRuleData accountingRuleData) { + final List allowedAccounts = this.accountReadPlatformService.retrieveAllEnabledDetailGLAccounts(); + final List allowedOffices = (List) this.officeReadPlatformService.retrieveAllOfficesForDropdown(); + final Collection allowedAssetsTagOptions = defaultIfEmpty(this.codeValueReadPlatformService + .retrieveCodeValuesByCode(AccountingConstants.ASSESTS_TAG_OPTION_CODE_NAME)); + final Collection allowedLiabilitiesTagOptions = defaultIfEmpty(this.codeValueReadPlatformService + .retrieveCodeValuesByCode(AccountingConstants.LIABILITIES_TAG_OPTION_CODE_NAME)); + final Collection allowedEquityTagOptions = defaultIfEmpty(this.codeValueReadPlatformService + .retrieveCodeValuesByCode(AccountingConstants.EQUITY_TAG_OPTION_CODE_NAME)); + final Collection allowedIncomeTagOptions = defaultIfEmpty(this.codeValueReadPlatformService + .retrieveCodeValuesByCode(AccountingConstants.INCOME_TAG_OPTION_CODE_NAME)); + final Collection allowedExpensesTagOptions = defaultIfEmpty(this.codeValueReadPlatformService + .retrieveCodeValuesByCode(AccountingConstants.EXPENSES_TAG_OPTION_CODE_NAME)); + if (accountingRuleData == null) { return new AccountingRuleData(allowedAccounts, allowedOffices, allowedAssetsTagOptions, + allowedLiabilitiesTagOptions, allowedEquityTagOptions, allowedIncomeTagOptions, allowedExpensesTagOptions); } + return new AccountingRuleData(accountingRuleData, allowedAccounts, allowedOffices, allowedAssetsTagOptions, + allowedLiabilitiesTagOptions, allowedEquityTagOptions, allowedIncomeTagOptions, allowedExpensesTagOptions); + } + + private Collection defaultIfEmpty(final Collection collection) { + Collection returnCollection = null; + if (collection != null && !collection.isEmpty()) { + returnCollection = collection; + } + return returnCollection; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/api/AccountingRuleJsonInputParams.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/api/AccountingRuleJsonInputParams.java index e50fcde0b..161638bbe 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/api/AccountingRuleJsonInputParams.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/api/AccountingRuleJsonInputParams.java @@ -13,7 +13,7 @@ import java.util.Set; ***/ public enum AccountingRuleJsonInputParams { ID("id"), OFFICE_ID("officeId"), ACCOUNT_TO_DEBIT("debitAccountHead"), ACCOUNT_TO_CREDIT("creditAccountHead"), NAME("name"), DESCRIPTION( - "description"), SYSTEM_DEFINED("systemDefined"); + "description"), SYSTEM_DEFINED("systemDefined"), DEBIT_ACCOUNT_TAGS("debitTags"), CREDIT_ACCOUNT_TAGS("creditTags"); private final String value; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/data/AccountingRuleData.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/data/AccountingRuleData.java index 248c3204d..9b8d1acdb 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/data/AccountingRuleData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/data/AccountingRuleData.java @@ -6,9 +6,11 @@ package org.mifosplatform.accounting.rule.data; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import org.mifosplatform.accounting.glaccount.data.GLAccountData; +import org.mifosplatform.infrastructure.codes.data.CodeValueData; import org.mifosplatform.organisation.office.data.OfficeData; /** @@ -19,85 +21,127 @@ import org.mifosplatform.organisation.office.data.OfficeData; */ public class AccountingRuleData { - private final Long id; - private final Long officeId; - private final String officeName; - private final String name; - private final String description; - private final boolean systemDefined; - private final GLAccountData debitAccountHead; - private final GLAccountData creditAccountHead; + private final Long id; + private final Long officeId; + private final String officeName; + private final String name; + private final String description; + private final boolean systemDefined; + private final GLAccountData debitAccountHead; + private final GLAccountData creditAccountHead; + private final List creditTags; + private final List debitTags; - // template - @SuppressWarnings("unused") - private List allowedOffices = new ArrayList(); - @SuppressWarnings("unused") - private List allowedAccounts = new ArrayList(); + // template + private List allowedOffices = new ArrayList(); + private List allowedAccounts = new ArrayList(); + private final Collection allowedAssetsTagOptions; + private final Collection allowedLiabilitiesTagOptions; + private final Collection allowedEquityTagOptions; + private final Collection allowedIncomeTagOptions; + private final Collection allowedExpensesTagOptions; - /*public AccountingRuleData(final Long id, final Long accountToDebitId, final Long accountToCreditId) { - this(id, null, null, accountToDebitId, accountToCreditId, null, null, true, null, null, null, null, null, null); - }*/ + public AccountingRuleData(final AccountingRuleData accountingRuleData, final List allowedAccounts, + final List allowedOffices, final Collection allowedAssetsTagOptions, + final Collection allowedLiabilitiesTagOptions, final Collection allowedEquityTagOptions, + final Collection allowedIncomeTagOptions, final Collection allowedExpensesTagOptions) { + this.id = accountingRuleData.id; + this.officeId = accountingRuleData.officeId; + this.officeName = accountingRuleData.officeName; + this.name = accountingRuleData.name; + this.description = accountingRuleData.description; + this.systemDefined = accountingRuleData.systemDefined; + this.allowedOffices = allowedOffices; + this.allowedAccounts = allowedAccounts; + this.debitAccountHead = accountingRuleData.debitAccountHead; + this.creditAccountHead = accountingRuleData.creditAccountHead; + this.allowedAssetsTagOptions = allowedAssetsTagOptions; + this.allowedLiabilitiesTagOptions = allowedLiabilitiesTagOptions; + this.allowedEquityTagOptions = allowedEquityTagOptions; + this.allowedIncomeTagOptions = allowedIncomeTagOptions; + this.allowedExpensesTagOptions = allowedExpensesTagOptions; + this.creditTags = accountingRuleData.creditTags; + this.debitTags = accountingRuleData.debitTags; + } - /*public AccountingRuleData(final Long id, final Long officeId, final String officeName, final Long accountToDebitId, - final Long accountToCreditId, final String name, final String description, final boolean systemDefined, - final String debitAccountName, final String creditAccountName, final String debitAccountGLCode, - final String creditAccountGLCode) { - this(id, officeId, officeName, accountToDebitId, accountToCreditId, name, description, systemDefined, debitAccountName, - creditAccountName, debitAccountGLCode, creditAccountGLCode, null, null); - }*/ + public AccountingRuleData(final List allowedAccounts, final List allowedOffices, + final Collection allowedAssetsTagOptions, final Collection allowedLiabilitiesTagOptions, + final Collection allowedEquityTagOptions, final Collection allowedIncomeTagOptions, + final Collection allowedExpensesTagOptions) { + this.id = null; + this.officeId = null; + this.officeName = null; + this.name = null; + this.description = null; + this.systemDefined = false; + this.allowedOffices = allowedOffices; + this.allowedAccounts = allowedAccounts; + this.debitAccountHead = null; + this.creditAccountHead = null; + this.allowedAssetsTagOptions = allowedAssetsTagOptions; + this.allowedLiabilitiesTagOptions = allowedLiabilitiesTagOptions; + this.allowedEquityTagOptions = allowedEquityTagOptions; + this.allowedIncomeTagOptions = allowedIncomeTagOptions; + this.allowedExpensesTagOptions = allowedExpensesTagOptions; + this.creditTags = null; + this.debitTags = null; + } - public AccountingRuleData(final Long id, final Long officeId, final String officeName, final String name, final String description, - final boolean systemDefined, final List allowedOffices, final List allowedAccounts) { - this.id = id; - this.officeId = officeId; - this.officeName = officeName; - this.name = name; - this.description = description; - this.systemDefined = systemDefined; - this.allowedOffices = allowedOffices; - this.allowedAccounts = allowedAccounts; - this.debitAccountHead = null; - this.creditAccountHead = null; - } + public AccountingRuleData(final Long id, final Long officeId, final String officeName, final String name, final String description, + final boolean systemDefined, final GLAccountData debitAccountData, final GLAccountData creditAccountData) { + this.id = id; + this.officeId = officeId; + this.officeName = officeName; + this.name = name; + this.description = description; + this.systemDefined = systemDefined; + this.allowedOffices = null; + this.allowedAccounts = null; + this.debitAccountHead = debitAccountData; + this.creditAccountHead = creditAccountData; + this.allowedAssetsTagOptions = null; + this.allowedLiabilitiesTagOptions = null; + this.allowedEquityTagOptions = null; + this.allowedIncomeTagOptions = null; + this.allowedExpensesTagOptions = null; + this.creditTags = null; + this.debitTags = null; + } - public AccountingRuleData(final AccountingRuleData accountingRuleData, - final List allowedAccounts, final List allowedOffices) { - this.id = accountingRuleData.id; - this.officeId = accountingRuleData.officeId; - this.officeName = accountingRuleData.officeName; - this.name = accountingRuleData.name; - this.description = accountingRuleData.description; - this.systemDefined = accountingRuleData.systemDefined; - this.allowedOffices = allowedOffices; - this.allowedAccounts = allowedAccounts; - this.debitAccountHead = accountingRuleData.debitAccountHead; - this.creditAccountHead = accountingRuleData.creditAccountHead; - } + public AccountingRuleData(final AccountingRuleData accountingRuleData, final List creditTags, + final List debitTags) { + this.id = accountingRuleData.id; + this.officeId = accountingRuleData.officeId; + this.officeName = accountingRuleData.officeName; + this.name = accountingRuleData.name; + this.description = accountingRuleData.description; + this.systemDefined = accountingRuleData.systemDefined; + this.allowedOffices = accountingRuleData.allowedOffices; + this.allowedAccounts = accountingRuleData.allowedAccounts; + this.debitAccountHead = accountingRuleData.debitAccountHead; + this.creditAccountHead = accountingRuleData.creditAccountHead; + this.allowedAssetsTagOptions = accountingRuleData.allowedAssetsTagOptions; + this.allowedLiabilitiesTagOptions = accountingRuleData.allowedLiabilitiesTagOptions; + this.allowedEquityTagOptions = accountingRuleData.allowedEquityTagOptions; + this.allowedIncomeTagOptions = accountingRuleData.allowedIncomeTagOptions; + this.allowedExpensesTagOptions = accountingRuleData.allowedExpensesTagOptions; + this.creditTags = creditTags; + this.debitTags = debitTags; + } - public AccountingRuleData(final List allowedAccounts, final List allowedOffices) { - this.id = null; - this.officeId = null; - this.officeName = null; - this.name = null; - this.description = null; - this.systemDefined = false; - this.allowedOffices = allowedOffices; - this.allowedAccounts = allowedAccounts; - this.debitAccountHead = null; - this.creditAccountHead = null; - } + public GLAccountData getDebitAccountHead() { + return this.debitAccountHead; + } - public AccountingRuleData(Long id, Long officeId, String officeName, String name, String description, - boolean systemDefined, GLAccountData debitAccountData, GLAccountData creditAccountData) { - this.id = id; - this.officeId = officeId; - this.officeName = officeName; - this.name = name; - this.description = description; - this.systemDefined = systemDefined; - this.allowedOffices = null; - this.allowedAccounts = null; - this.debitAccountHead = debitAccountData; - this.creditAccountHead = creditAccountData; - } + public GLAccountData getCreditAccountHead() { + return this.creditAccountHead; + } + + public List getCreditTags() { + return this.creditTags; + } + + public List getDebitTags() { + return this.debitTags; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/data/AccountingTagRuleData.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/data/AccountingTagRuleData.java new file mode 100644 index 000000000..f4988b7d9 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/data/AccountingTagRuleData.java @@ -0,0 +1,20 @@ +package org.mifosplatform.accounting.rule.data; + +import org.mifosplatform.infrastructure.core.data.EnumOptionData; + +public class AccountingTagRuleData { + + @SuppressWarnings("unused") + private final Long id; + @SuppressWarnings("unused") + private final Long tagId; + @SuppressWarnings("unused") + private final EnumOptionData transactionType; + + public AccountingTagRuleData(final Long id, final Long tagId, final EnumOptionData transactionType) { + this.id = id; + this.tagId = tagId; + this.transactionType = transactionType; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/domain/AccountingRule.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/domain/AccountingRule.java index f934efe68..1b30eb02e 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/domain/AccountingRule.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/domain/AccountingRule.java @@ -5,17 +5,23 @@ */ package org.mifosplatform.accounting.rule.domain; +import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +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.UniqueConstraint; import org.apache.commons.lang.StringUtils; +import org.hibernate.annotations.LazyCollection; +import org.hibernate.annotations.LazyCollectionOption; import org.mifosplatform.accounting.glaccount.domain.GLAccount; import org.mifosplatform.accounting.rule.api.AccountingRuleJsonInputParams; import org.mifosplatform.infrastructure.core.api.JsonCommand; @@ -34,11 +40,11 @@ public class AccountingRule extends AbstractPersistable { private Office office; @ManyToOne - @JoinColumn(name = "debit_account_id", nullable = false) + @JoinColumn(name = "debit_account_id", nullable = true) private GLAccount accountToDebit; @ManyToOne - @JoinColumn(name = "credit_account_id", nullable = false) + @JoinColumn(name = "credit_account_id", nullable = true) private GLAccount accountToCredit; @Column(name = "description", nullable = true, length = 500) @@ -46,6 +52,11 @@ public class AccountingRule extends AbstractPersistable { @Column(name = "system_defined", nullable = false) private Boolean systemDefined; + + @LazyCollection(LazyCollectionOption.FALSE) + @OneToMany(cascade = CascadeType.ALL, mappedBy = "accountingRule", orphanRemoval = true) + private List accountingTagRules = new ArrayList(); + protected AccountingRule() {} @@ -151,4 +162,12 @@ public class AccountingRule extends AbstractPersistable { return this.description; } + public List updateAccountingRuleForTags(List debitAccountingTagRules) { + for(AccountingTagRule accountingTagRule : debitAccountingTagRules){ + accountingTagRule.updateAccountingTagRule(this); + this.accountingTagRules.add(accountingTagRule); + } + return debitAccountingTagRules; + } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/domain/AccountingTagRule.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/domain/AccountingTagRule.java new file mode 100644 index 000000000..f83aa310e --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/domain/AccountingTagRule.java @@ -0,0 +1,40 @@ +package org.mifosplatform.accounting.rule.domain; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +import org.mifosplatform.infrastructure.codes.domain.CodeValue; +import org.springframework.data.jpa.domain.AbstractPersistable; + +@Entity +@Table(name = "acc_rule_tags") +public class AccountingTagRule extends AbstractPersistable { + + @ManyToOne + @JoinColumn(name = "acc_rule_id", nullable = false) + private AccountingRule accountingRule; + + @ManyToOne + @JoinColumn(name = "tag_id", nullable = false) + private final CodeValue tagId; + + @Column(name = "acc_type_enum", nullable = false) + private final Integer accountType; + + public static AccountingTagRule create(final CodeValue tagId, final Integer accountType) { + return new AccountingTagRule(tagId, accountType); + } + + public AccountingTagRule(final CodeValue tagId, final Integer accountType) { + this.tagId = tagId; + this.accountType = accountType; + } + + public void updateAccountingTagRule(final AccountingRule accountingRule) { + this.accountingRule = accountingRule; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/serialization/AccountingRuleCommandFromApiJsonDeserializer.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/serialization/AccountingRuleCommandFromApiJsonDeserializer.java index 07c2a0be1..d74429434 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/serialization/AccountingRuleCommandFromApiJsonDeserializer.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/serialization/AccountingRuleCommandFromApiJsonDeserializer.java @@ -55,7 +55,8 @@ public final class AccountingRuleCommandFromApiJsonDeserializer extends Abstract element); final String name = this.fromApiJsonHelper.extractStringNamed(AccountingRuleJsonInputParams.NAME.getValue(), element); final String description = this.fromApiJsonHelper.extractStringNamed(AccountingRuleJsonInputParams.DESCRIPTION.getValue(), element); - + final String[] creditTags = this.fromApiJsonHelper.extractArrayNamed(AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue(), element); + final String[] debitTags = this.fromApiJsonHelper.extractArrayNamed(AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue(), element); return new AccountingRuleCommand(id, officeId, accountToDebitId, accountToCreditId, name, description); } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleDataValidator.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleDataValidator.java new file mode 100644 index 000000000..76ac1d5b5 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleDataValidator.java @@ -0,0 +1,147 @@ +package org.mifosplatform.accounting.rule.service; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang.StringUtils; +import org.mifosplatform.accounting.rule.api.AccountingRuleJsonInputParams; +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.FromJsonHelper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import com.google.gson.JsonElement; +import com.google.gson.reflect.TypeToken; + +@Component +public class AccountingRuleDataValidator { + + /** + * The parameters supported for this command. + */ + private final Set supportedParameters = AccountingRuleJsonInputParams.getAllValues(); + + private final FromJsonHelper fromApiJsonHelper; + + @Autowired + public AccountingRuleDataValidator(final FromJsonHelper fromApiJsonHelper) { + this.fromApiJsonHelper = fromApiJsonHelper; + } + + public void validateForCreate(final String json) { + + if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); } + + final Type typeOfMap = new TypeToken>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, this.supportedParameters); + + final List dataValidationErrors = new ArrayList(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("AccountingRule"); + + final JsonElement element = this.fromApiJsonHelper.parse(json); + + final Long accountToDebitId = this.fromApiJsonHelper.extractLongNamed(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue(), + element); + baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue()).value(accountToDebitId) + .ignoreIfNull().integerGreaterThanZero(); + + final Long accountToCreditId = this.fromApiJsonHelper.extractLongNamed(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue(), + element); + baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue()).value(accountToCreditId) + .ignoreIfNull().integerGreaterThanZero(); + + final Long officeId = this.fromApiJsonHelper.extractLongNamed(AccountingRuleJsonInputParams.OFFICE_ID.getValue(), element); + baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.OFFICE_ID.getValue()).value(officeId).ignoreIfNull() + .integerGreaterThanZero(); + + final String name = this.fromApiJsonHelper.extractStringNamed(AccountingRuleJsonInputParams.NAME.getValue(), element); + baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.NAME.getValue()).value(name).notBlank().notExceedingLengthOf(100); + + final String description = this.fromApiJsonHelper.extractStringNamed(AccountingRuleJsonInputParams.DESCRIPTION.getValue(), element); + baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.DESCRIPTION.getValue()).value(description).ignoreIfNull() + .notExceedingLengthOf(500); + + final String[] creditTags = this.fromApiJsonHelper.extractArrayNamed(AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue(), + element); + baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue()).value(creditTags).ignoreIfNull() + .arrayNotEmpty(); + validateCreditOrDebitTagArray(creditTags, baseDataValidator, AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue()); + + final String[] debitTags = this.fromApiJsonHelper.extractArrayNamed(AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue(), + element); + baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue()).value(debitTags).ignoreIfNull() + .arrayNotEmpty(); + validateCreditOrDebitTagArray(debitTags, baseDataValidator, AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue()); + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + + private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } + } + + public void validateCreditOrDebitTagArray(final String[] creditOrDebitTagArray, final DataValidatorBuilder baseDataValidator, + final String parameter) { + if (creditOrDebitTagArray != null && !ObjectUtils.isEmpty(creditOrDebitTagArray)) { + for (final String tag : creditOrDebitTagArray) { + baseDataValidator.reset().parameter(parameter).value(tag).ignoreIfNull().notBlank().longGreaterThanZero(); + } + } + } + + public void validateForUpdate(final String json) { + + if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); } + + final Type typeOfMap = new TypeToken>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, this.supportedParameters); + + final List dataValidationErrors = new ArrayList(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("AccountingRule"); + + final JsonElement element = this.fromApiJsonHelper.parse(json); + + final Long accountToDebitId = this.fromApiJsonHelper.extractLongNamed(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue(), + element); + baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue()).value(accountToDebitId) + .ignoreIfNull().notBlank().integerGreaterThanZero(); + + final Long accountToCreditId = this.fromApiJsonHelper.extractLongNamed(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue(), + element); + baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue()).value(accountToCreditId) + .ignoreIfNull().notBlank().integerGreaterThanZero(); + + final Long officeId = this.fromApiJsonHelper.extractLongNamed(AccountingRuleJsonInputParams.OFFICE_ID.getValue(), element); + baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.OFFICE_ID.getValue()).value(officeId).ignoreIfNull() + .integerGreaterThanZero(); + + final String name = this.fromApiJsonHelper.extractStringNamed(AccountingRuleJsonInputParams.NAME.getValue(), element); + baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.NAME.getValue()).value(name).ignoreIfNull().notBlank() + .notExceedingLengthOf(100); + + final String description = this.fromApiJsonHelper.extractStringNamed(AccountingRuleJsonInputParams.DESCRIPTION.getValue(), element); + baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.DESCRIPTION.getValue()).value(description).ignoreIfNull() + .notExceedingLengthOf(500); + + final String[] creditTags = this.fromApiJsonHelper.extractArrayNamed(AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue(), + element); + baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue()).value(creditTags).ignoreIfNull() + .arrayNotEmpty(); + validateCreditOrDebitTagArray(creditTags, baseDataValidator, AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue()); + + final String[] debitTags = this.fromApiJsonHelper.extractArrayNamed(AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue(), + element); + baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue()).value(debitTags).ignoreIfNull() + .arrayNotEmpty(); + validateCreditOrDebitTagArray(debitTags, baseDataValidator, AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue()); + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleReadPlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleReadPlatformService.java index d33b7fef7..1ea43a58d 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleReadPlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleReadPlatformService.java @@ -13,6 +13,6 @@ public interface AccountingRuleReadPlatformService { List retrieveAllAccountingRules(Long OfficeId); - AccountingRuleData retrieveAccountingRuleById(long accountingRuleId); + AccountingRuleData retrieveAccountingRuleById(Long accountingRuleId); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleReadPlatformServiceImpl.java index 369d7586d..de1a9d6d3 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleReadPlatformServiceImpl.java @@ -7,16 +7,25 @@ package org.mifosplatform.accounting.rule.service; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import org.mifosplatform.accounting.common.AccountingEnumerations; import org.mifosplatform.accounting.glaccount.data.GLAccountData; +import org.mifosplatform.accounting.journalentry.domain.JournalEntryType; import org.mifosplatform.accounting.rule.data.AccountingRuleData; +import org.mifosplatform.accounting.rule.data.AccountingTagRuleData; import org.mifosplatform.accounting.rule.exception.AccountingRuleNotFoundException; +import org.mifosplatform.infrastructure.core.data.EnumOptionData; import org.mifosplatform.infrastructure.core.domain.JdbcSupport; import org.mifosplatform.infrastructure.core.service.TenantAwareRoutingDataSource; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataAccessException; 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.stereotype.Service; @@ -30,13 +39,15 @@ public class AccountingRuleReadPlatformServiceImpl implements AccountingRuleRead this.jdbcTemplate = new JdbcTemplate(dataSource); } - private static final class AccountingRuleMapper implements RowMapper { + // Will remove the old one if everything goes well. +/* private static final class AccountingRuleMapper implements RowMapper { private final String schemaSql; public AccountingRuleMapper() { final StringBuilder sqlBuilder = new StringBuilder(400); - sqlBuilder.append("distinct rule.id as id,rule.name as name, rule.office_id as officeId,office.name as officeName,") + sqlBuilder + .append("distinct rule.id as id,rule.name as name, rule.office_id as officeId,office.name as officeName,") .append(" rule.debit_account_id as debitAccountId,rule.credit_account_id as creditAccountId,") .append(" rule.description as description, rule.system_defined as systemDefined, ") .append("debitAccount.glName as debitAccountName, debitAccount.glCode as debitAccountGLCode,") @@ -46,7 +57,7 @@ public class AccountingRuleReadPlatformServiceImpl implements AccountingRuleRead .append("(Select gl.id as id,gl.name as glName, gl.gl_code as glCode from acc_gl_account gl,acc_accounting_rule rule ") .append("where gl.id=rule.credit_account_id ) as creditAccount,") .append("acc_accounting_rule rule left join m_office office on rule.office_id=office.id ") - .append("where debitAccount.id=rule.debit_account_id and creditAccount.id = rule.credit_account_id"); + .append("where debitAccount.id=rule.debit_account_id and creditAccount.id = rule.credit_account_id"); this.schemaSql = sqlBuilder.toString(); } @@ -66,41 +77,137 @@ public class AccountingRuleReadPlatformServiceImpl implements AccountingRuleRead final Long accountToCreditId = rs.getLong("creditAccountId"); final boolean systemDefined = rs.getBoolean("systemDefined"); final String debitAccountName = rs.getString("debitAccountName"); - final String creditAccountName = rs.getString("creditAccountName"); - final String debitAccountGLCode = rs.getString("debitAccountGLCode"); - final String creditAccountGLCode = rs.getString("creditAccountGLCode"); - - final GLAccountData debitAccountData = new GLAccountData(accountToDebitId, debitAccountName, debitAccountGLCode); - final GLAccountData creditAccountData = new GLAccountData(accountToCreditId, creditAccountName, creditAccountGLCode); + final String creditAccountName = rs.getString("creditAccountName"); + final String debitAccountGLCode = rs.getString("debitAccountGLCode"); + final String creditAccountGLCode = rs.getString("creditAccountGLCode"); + + final GLAccountData debitAccountData = new GLAccountData(accountToDebitId, debitAccountName, debitAccountGLCode); + final GLAccountData creditAccountData = new GLAccountData(accountToCreditId, creditAccountName, creditAccountGLCode); return new AccountingRuleData(id, officeId, officeName, name, description, systemDefined, debitAccountData, creditAccountData); } - } + }*/ - @Override - public List retrieveAllAccountingRules(Long officeId) { - final AccountingRuleMapper rm = new AccountingRuleMapper(); - Object[] arguments = new Object[]{}; - String sql = "select " + rm.schema() +" and system_defined=0 "; - if(officeId != null) { - sql = sql +" and office.id = ?"; - arguments = new Object[] { officeId }; + private static final class AccountingRuleDataExtractor implements ResultSetExtractor { + + private final String schemaSql; + private final JdbcTemplate jdbcTemplate; + + public AccountingRuleDataExtractor(final JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + final StringBuilder sqlBuilder = new StringBuilder(400); + sqlBuilder + .append(" rule.id as id,rule.name as name, rule.office_id as officeId,office.name as officeName,") + .append(" rule.description as description, rule.system_defined as systemDefined, ") + .append("debitAccount.id AS debitAccountId, debitAccount.name as debitAccountName, debitAccount.gl_code as debitAccountGLCode, ") + .append("creditAccount.id AS creditAccountId, creditAccount.name as creditAccountName, creditAccount.gl_code as creditAccountGLCode") + .append(" from m_office AS office, acc_accounting_rule AS rule ") + .append(" LEFT JOIN acc_gl_account AS creditAccount ON rule.credit_account_id = creditAccount.id ") + .append(" LEFT JOIN acc_gl_account AS debitAccount ON rule.debit_account_id = debitAccount.id ") + .append("WHERE office.id=rule.office_id "); + this.schemaSql = sqlBuilder.toString(); } - sql = sql + " order by rule.id asc"; - return this.jdbcTemplate.query(sql, rm, arguments); + + public String schema() { + return this.schemaSql; + } + + @Override + public Map extractData(final ResultSet rs) throws SQLException, DataAccessException { + final Map extractedData = new HashMap(); + + while (rs.next()) { + final Long id = rs.getLong("id"); + AccountingRuleData accountingRuleData = extractedData.get(id); + if (accountingRuleData == null) { + final Long officeId = JdbcSupport.getLong(rs, "officeId"); + final String officeName = rs.getString("officeName"); + final String name = rs.getString("name"); + final String description = rs.getString("description"); + final Long accountToDebitId = JdbcSupport.getLong(rs, "debitAccountId"); + final Long accountToCreditId = JdbcSupport.getLong(rs, "creditAccountId"); + final boolean systemDefined = rs.getBoolean("systemDefined"); + final String debitAccountName = rs.getString("debitAccountName"); + final String creditAccountName = rs.getString("creditAccountName"); + final String debitAccountGLCode = rs.getString("debitAccountGLCode"); + final String creditAccountGLCode = rs.getString("creditAccountGLCode"); + + final GLAccountData debitAccountData = accountToDebitId == null ? null : new GLAccountData(accountToDebitId, + debitAccountName, debitAccountGLCode); + final GLAccountData creditAccountData = accountToCreditId == null ? null : new GLAccountData(accountToCreditId, + creditAccountName, creditAccountGLCode); + accountingRuleData = new AccountingRuleData(id, officeId, officeName, name, description, systemDefined, + debitAccountData, creditAccountData); + } + if (accountingRuleData.getCreditAccountHead() == null) { + final List creditTags = getCreditOrDebitTags(id, JournalEntryType.CREDIT.getValue()); + accountingRuleData = new AccountingRuleData(accountingRuleData, creditTags, accountingRuleData.getDebitTags()); + } + if (accountingRuleData.getDebitAccountHead() == null) { + final List debitTags = getCreditOrDebitTags(id, JournalEntryType.DEBIT.getValue()); + accountingRuleData = new AccountingRuleData(accountingRuleData, accountingRuleData.getCreditTags(), debitTags); + } + extractedData.put(id, accountingRuleData); + } + return extractedData; + } + + private List getCreditOrDebitTags(final Long creditOrDebitAccount, final Integer transactionType) { + final AccountingTagRuleDataMapper mapper = new AccountingTagRuleDataMapper(); + final String taggedAccountsSchema = "Select " + mapper.taggedAccountSchema() + " where rule.id = ? and tag.acc_type_enum=?"; + return this.jdbcTemplate.query(taggedAccountsSchema, mapper, new Object[] { creditOrDebitAccount, transactionType }); + } + } @Override - public AccountingRuleData retrieveAccountingRuleById(long accountingRuleId) { + public List retrieveAllAccountingRules(final Long officeId) { + final AccountingRuleDataExtractor resultSetExtractor = new AccountingRuleDataExtractor(this.jdbcTemplate); + Object[] arguments = new Object[] {}; + String sql = "select " + resultSetExtractor.schema() + " and system_defined=0 "; + if (officeId != null) { + sql = sql + " and office.id = ?"; + arguments = new Object[] { officeId }; + } + sql = sql + " order by rule.id asc"; + @SuppressWarnings("unchecked") + final Map extractedData = (Map) this.jdbcTemplate.query(sql, + resultSetExtractor, arguments); + return new ArrayList(extractedData.values()); + } + + @Override + public AccountingRuleData retrieveAccountingRuleById(final Long accountingRuleId) { try { + final AccountingRuleDataExtractor resultSetExtractor = new AccountingRuleDataExtractor(this.jdbcTemplate); + final String sql = "select " + resultSetExtractor.schema() + " and rule.id = ?"; - final AccountingRuleMapper rm = new AccountingRuleMapper(); - final String sql = "select " + rm.schema() + " and rule.id = ?"; - - final AccountingRuleData accountingRuleData = this.jdbcTemplate.queryForObject(sql, rm, new Object[] { accountingRuleId }); + @SuppressWarnings("unchecked") + final Map extractedData = (Map) this.jdbcTemplate.query(sql, + resultSetExtractor, new Object[] { accountingRuleId }); + final AccountingRuleData accountingRuleData = extractedData.get(accountingRuleId); + if (accountingRuleData == null) { throw new AccountingRuleNotFoundException(accountingRuleId); } return accountingRuleData; } catch (final EmptyResultDataAccessException e) { throw new AccountingRuleNotFoundException(accountingRuleId); } } + + private static final class AccountingTagRuleDataMapper implements RowMapper { + + public String taggedAccountSchema() { + return " tag.id as id,tag.tag_id as tagId, tag.acc_type_enum as transactionType from acc_rule_tags tag " + + "join acc_accounting_rule rule on tag.acc_rule_id=rule.id "; + } + + @Override + public AccountingTagRuleData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + final Long id = rs.getLong("id"); + final Long tagId = rs.getLong("tagId"); + final Integer transactionType = JdbcSupport.getInteger(rs, "transactionType"); + final EnumOptionData transactionTypeEnum = AccountingEnumerations.journalEntryType(transactionType); + return new AccountingTagRuleData(id, tagId, transactionTypeEnum); + } + + } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleWritePlatformServiceJpaRepositoryImpl.java index f034e06ec..2e38b7516 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleWritePlatformServiceJpaRepositoryImpl.java @@ -5,18 +5,22 @@ */ package org.mifosplatform.accounting.rule.service; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import org.mifosplatform.accounting.closure.api.GLClosureJsonInputParams; import org.mifosplatform.accounting.glaccount.domain.GLAccount; import org.mifosplatform.accounting.glaccount.domain.GLAccountRepositoryWrapper; +import org.mifosplatform.accounting.journalentry.domain.JournalEntryType; import org.mifosplatform.accounting.rule.api.AccountingRuleJsonInputParams; -import org.mifosplatform.accounting.rule.command.AccountingRuleCommand; import org.mifosplatform.accounting.rule.domain.AccountingRule; import org.mifosplatform.accounting.rule.domain.AccountingRuleRepository; import org.mifosplatform.accounting.rule.domain.AccountingRuleRepositoryWrapper; +import org.mifosplatform.accounting.rule.domain.AccountingTagRule; import org.mifosplatform.accounting.rule.exception.AccountingRuleDuplicateException; -import org.mifosplatform.accounting.rule.serialization.AccountingRuleCommandFromApiJsonDeserializer; +import org.mifosplatform.infrastructure.codes.domain.CodeValue; +import org.mifosplatform.infrastructure.codes.domain.CodeValueRepository; import org.mifosplatform.infrastructure.core.api.JsonCommand; import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder; @@ -40,17 +44,20 @@ public class AccountingRuleWritePlatformServiceJpaRepositoryImpl implements Acco private final AccountingRuleRepository accountingRuleRepository; private final GLAccountRepositoryWrapper accountRepositoryWrapper; private final OfficeRepository officeRepository; - private final AccountingRuleCommandFromApiJsonDeserializer fromApiJsonDeserializer; + private final AccountingRuleDataValidator fromApiJsonDeserializer; + private final CodeValueRepository codeValueRepository; @Autowired public AccountingRuleWritePlatformServiceJpaRepositoryImpl(final AccountingRuleRepositoryWrapper accountingRuleRepositoryWrapper, final GLAccountRepositoryWrapper accountRepositoryWrapper, final OfficeRepository officeRepository, - final AccountingRuleCommandFromApiJsonDeserializer fromApiJsonDeserializer, final AccountingRuleRepository ruleRepository) { + final AccountingRuleRepository ruleRepository, final AccountingRuleDataValidator fromApiJsonDeserializer, + final CodeValueRepository codeValueRepository) { this.accountRepositoryWrapper = accountRepositoryWrapper; this.officeRepository = officeRepository; - this.fromApiJsonDeserializer = fromApiJsonDeserializer; this.accountingRuleRepository = ruleRepository; this.accountingRuleRepositoryWrapper = accountingRuleRepositoryWrapper; + this.fromApiJsonDeserializer = fromApiJsonDeserializer; + this.codeValueRepository = codeValueRepository; } /** @@ -69,10 +76,10 @@ public class AccountingRuleWritePlatformServiceJpaRepositoryImpl implements Acco @Transactional @Override - public CommandProcessingResult createAccountingRule(JsonCommand command) { + public CommandProcessingResult createAccountingRule(final JsonCommand command) { try { - final AccountingRuleCommand accountingRuleCommand = this.fromApiJsonDeserializer.commandFromApiJson(command.json()); - accountingRuleCommand.validateForCreate(); + + this.fromApiJsonDeserializer.validateForCreate(command.json()); // check office is valid final Long officeId = command.longValueOfParameterNamed(GLClosureJsonInputParams.OFFICE_ID.getValue()); @@ -82,15 +89,8 @@ public class AccountingRuleWritePlatformServiceJpaRepositoryImpl implements Acco if (office == null) { throw new OfficeNotFoundException(officeId); } } - // get the GL Accounts to Debit and Credit - final Long accountToDebitId = command.longValueOfParameterNamed(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue()); - final Long accountToCreditId = command.longValueOfParameterNamed(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue()); - final GLAccount accountToDebit = accountRepositoryWrapper.findOneWithNotFoundDetection(accountToDebitId); - final GLAccount accountToCredit = accountRepositoryWrapper.findOneWithNotFoundDetection(accountToCreditId); - - final AccountingRule accountingRule = AccountingRule.fromJson(office, accountToDebit, accountToCredit, command); + final AccountingRule accountingRule = assembleAccountingRuleAndTags(office, command); this.accountingRuleRepository.saveAndFlush(accountingRule); - return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withOfficeId(officeId) .withEntityId(accountingRule.getId()).build(); } catch (final DataIntegrityViolationException dve) { @@ -99,11 +99,43 @@ public class AccountingRuleWritePlatformServiceJpaRepositoryImpl implements Acco } } + private AccountingRule assembleAccountingRuleAndTags(final Office office, final JsonCommand command) { + // get the GL Accounts or tags to Debit and Credit + final String[] debitTags = command.arrayValueOfParameterNamed(AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue()); + final String[] creditTags = command.arrayValueOfParameterNamed(AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue()); + final Long accountToDebitId = command.longValueOfParameterNamed(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue()); + final Long accountToCreditId = command.longValueOfParameterNamed(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue()); + GLAccount debitAccount = null; + GLAccount creditAccount = null; + List accountingTagRules = new ArrayList(); + + if (accountToDebitId != null) { + debitAccount = this.accountRepositoryWrapper.findOneWithNotFoundDetection(accountToDebitId); + } else if (debitTags != null) { + accountingTagRules = saveDebitOrCreditTags(debitTags, JournalEntryType.DEBIT, accountingTagRules); + } else { + throw new RuntimeException("Please Specify debitTags or accountToDebitId"); + } + + if (accountToCreditId != null) { + creditAccount = this.accountRepositoryWrapper.findOneWithNotFoundDetection(accountToCreditId); + } else if (debitTags != null) { + accountingTagRules = saveDebitOrCreditTags(creditTags, JournalEntryType.CREDIT, accountingTagRules); + } else { + throw new RuntimeException("Please Specify debitTags or accountToDebitId"); + } + + final AccountingRule accountingRule = AccountingRule.fromJson(office, debitAccount, creditAccount, command); + accountingTagRules = accountingRule.updateAccountingRuleForTags(accountingTagRules); + + return accountingRule; + } + @Transactional @Override - public CommandProcessingResult updateAccountingRule(Long accountingRuleId, JsonCommand command) { - final AccountingRuleCommand accountingRuleCommand = this.fromApiJsonDeserializer.commandFromApiJson(command.json()); - accountingRuleCommand.validateForUpdate(); + public CommandProcessingResult updateAccountingRule(final Long accountingRuleId, final JsonCommand command) { + + this.fromApiJsonDeserializer.validateForUpdate(command.json()); Long officeId = null; if (command.parameterExists(AccountingRuleJsonInputParams.OFFICE_ID.getValue())) { @@ -124,12 +156,12 @@ public class AccountingRuleWritePlatformServiceJpaRepositoryImpl implements Acco final Map changesOnly = accountingRule.update(command); if (accountToDebitId != null && changesOnly.containsKey(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue())) { - final GLAccount accountToDebit = accountRepositoryWrapper.findOneWithNotFoundDetection(accountToDebitId); + final GLAccount accountToDebit = this.accountRepositoryWrapper.findOneWithNotFoundDetection(accountToDebitId); accountingRule.setAccountToDebit(accountToDebit); } if (accountToCreditId != null && changesOnly.containsKey(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue())) { - final GLAccount accountToCredit = accountRepositoryWrapper.findOneWithNotFoundDetection(accountToCreditId); + final GLAccount accountToCredit = this.accountRepositoryWrapper.findOneWithNotFoundDetection(accountToCreditId); accountingRule.setAccountToCredit(accountToCredit); } @@ -149,9 +181,33 @@ public class AccountingRuleWritePlatformServiceJpaRepositoryImpl implements Acco @Transactional @Override - public CommandProcessingResult deleteAccountingRule(Long accountingRuleId) { + public CommandProcessingResult deleteAccountingRule(final Long accountingRuleId) { final AccountingRule accountingRule = this.accountingRuleRepositoryWrapper.findOneWithNotFoundDetection(accountingRuleId); this.accountingRuleRepository.delete(accountingRule); return new CommandProcessingResultBuilder().withEntityId(accountingRule.getId()).build(); } + +/* private GLAccount saveDebitOrCreditTagsOrGetDebitORCreditAccount(final String[] creditOrDebitTagArray, final Long creditOrDebitId, + final JournalEntryType transactionType) { + GLAccount creditOrDebitAccount = null; + if (creditOrDebitId != null) { + creditOrDebitAccount = this.accountRepositoryWrapper.findOneWithNotFoundDetection(creditOrDebitId); + } else if (creditOrDebitTagArray != null) { + final List accountingTagRules = saveDebitOrCreditTags(creditOrDebitTagArray, transactionType); + } else { + throw new RuntimeException("Please Specify debitTags or accountToDebitId"); + } + return creditOrDebitAccount; + }*/ + + private List saveDebitOrCreditTags(final String[] creditOrDebitTagArray, final JournalEntryType transactionType, + final List accountingTagRules) { + for (final String creditOrDebitTag : creditOrDebitTagArray) { + final Long creditOrDebitTagIdLongValue = Long.valueOf(creditOrDebitTag); + final CodeValue creditOrDebitAccount = this.codeValueRepository.findOne(creditOrDebitTagIdLongValue); + final AccountingTagRule accountingTagRule = AccountingTagRule.create(creditOrDebitAccount, transactionType.getValue()); + accountingTagRules.add(accountingTagRule); + } + return accountingTagRules; + } } From 721a12c2fa72df149979146b6346cce49e413f1c Mon Sep 17 00:00:00 2001 From: Lech Rozanski Date: Tue, 28 May 2013 17:17:51 +0200 Subject: [PATCH 03/49] MIFOSX-365: Simplify 'Data Tables' functionality --- .../core_db/V46__extend_datatables_api.sql | 10 + .../commands/domain/CommandWrapper.java | 15 + .../service/CommandWrapperBuilder.java | 27 ++ ...CommandSourceWritePlatformServiceImpl.java | 4 +- .../SynchronousCommandProcessingService.java | 10 +- .../infrastructure/core/api/JsonCommand.java | 18 +- .../core/data/DataValidatorBuilder.java | 40 +++ ...tatableCommandFromApiJsonDeserializer.java | 202 ++++++++++++ .../core/serialization/FromJsonHelper.java | 17 + .../api/DatatablesApiResource.java | 52 ++++ .../dataqueries/data/DatatableData.java | 15 +- .../CreateDatatableCommandHandler.java | 32 ++ .../DeleteDatatableCommandHandler.java | 40 +++ .../UpdateDatatableCommandHandler.java | 40 +++ .../service/ReadWriteNonCoreDataService.java | 10 +- .../ReadWriteNonCoreDataServiceImpl.java | 294 +++++++++++++++++- 16 files changed, 809 insertions(+), 17 deletions(-) create mode 100644 mifosng-db/migrations/core_db/V46__extend_datatables_api.sql create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/DatatableCommandFromApiJsonDeserializer.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/CreateDatatableCommandHandler.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/DeleteDatatableCommandHandler.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/UpdateDatatableCommandHandler.java diff --git a/mifosng-db/migrations/core_db/V46__extend_datatables_api.sql b/mifosng-db/migrations/core_db/V46__extend_datatables_api.sql new file mode 100644 index 000000000..5bd0a585e --- /dev/null +++ b/mifosng-db/migrations/core_db/V46__extend_datatables_api.sql @@ -0,0 +1,10 @@ + +INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) + VALUES + ('configuration', 'CREATE_DATATABLE', 'DATATABLE', 'CREATE', 0), + ('configuration', 'CREATE_DATATABLE_CHECKER', 'DATATABLE', 'CREATE', 0), + ('configuration', 'UPDATE_DATATABLE', 'DATATABLE', 'UPDATE', 0), + ('configuration', 'UPDATE_DATATABLE_CHECKER', 'DATATABLE', 'UPDATE', 0), + ('configuration', 'DELETE_DATATABLE', 'DATATABLE', 'DELETE', 0), + ('configuration', 'DELETE_DATATABLE_CHECKER', 'DATATABLE', 'DELETE', 0); + 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 ef609f991..613188160 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java @@ -108,6 +108,21 @@ public class CommandWrapper { return this.actionName.equalsIgnoreCase("CREATE"); } + public boolean isCreateDatatable() { + return (this.actionName.equalsIgnoreCase("CREATE") && this.href.startsWith("/datatables/") + && this.entityId == null); + } + + public boolean isDeleteDatatable() { + return (this.actionName.equalsIgnoreCase("DELETE") && this.href.startsWith("/datatables/") + && this.entityId == null); + } + + public boolean isUpdateDatatable() { + return (this.actionName.equalsIgnoreCase("UPDATE") && this.href.startsWith("/datatables/") + && this.entityId == null); + } + public String getTaskPermissionName() { return this.taskPermissionName; } 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 6ff9c0957..e275acbe6 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java @@ -356,6 +356,33 @@ public class CommandWrapperBuilder { return this; } + public CommandWrapperBuilder createDBDatatable(final String json) { + this.actionName = "CREATE"; + this.entityName = "DATATABLE"; + this.entityId = null; + this.href = "/datatables/"; + this.json = json; + return this; + } + + public CommandWrapperBuilder updateDBDatatable(final String datatable, final String json) { + this.actionName = "UPDATE"; + this.entityName = "DATATABLE"; + this.entityId = null; + this.href = "/datatables/" + datatable; + this.json = json; + return this; + } + + public CommandWrapperBuilder deleteDBDatatable(final String datatable, final String json) { + this.actionName = "DELETE"; + this.entityName = "DATATABLE"; + this.entityId = null; + this.href = "/datatables/" + datatable; + this.json = json; + return this; + } + public CommandWrapperBuilder createDatatable(final String datatable, final Long apptableId, final Long datatableId) { this.actionName = "CREATE"; commonDatatableSettings(datatable, apptableId, datatableId); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java index b792caa1f..c68ca88c8 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java @@ -63,7 +63,7 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio final JsonCommand command = JsonCommand.from(json, parsedCommand, this.fromApiJsonHelper, wrapper.getEntityName(), wrapper.getEntityId(), wrapper.getSubentityId(), wrapper.getGroupId(), wrapper.getClientId(), wrapper.getLoanId(), wrapper.getSavingsId(), wrapper.getCodeId(), wrapper.getSupportedEntityType(), wrapper.getSupportedEntityId(), - wrapper.getTransactionId()); + wrapper.getTransactionId(), wrapper.getHref()); result = this.processAndLogCommandService.processAndLogCommand(wrapper, command, isApprovedByChecker); } catch (RollbackTransactionAsCommandIsNotApprovedByCheckerException e) { @@ -86,7 +86,7 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio final JsonElement parsedCommand = this.fromApiJsonHelper.parse(commandSourceInput.json()); final JsonCommand command = JsonCommand.fromExistingCommand(makerCheckerId, commandSourceInput.json(), parsedCommand, this.fromApiJsonHelper, commandSourceInput.getEntityName(), commandSourceInput.resourceId(), - commandSourceInput.subresourceId()); + commandSourceInput.subresourceId(), commandSourceInput.getResourceGetUrl()); final boolean makerCheckerApproval = true; return this.processAndLogCommandService.processAndLogCommand(wrapper, command, makerCheckerApproval); 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 a65f0c1a9..6fd1d3d66 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java @@ -78,7 +78,7 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer commandSourceResult.updateJsonTo(changesOnlyJson); } - if (!result.hasChanges() && wrapper.isUpdateOperation()) { + if (!result.hasChanges() && wrapper.isUpdateOperation() && !wrapper.isUpdateDatatable()) { commandSourceResult.updateJsonTo(null); } @@ -108,7 +108,13 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer if (wrapper.isConfigurationResource()) { handler = applicationContext.getBean("updateGlobalConfigurationCommandHandler", NewCommandSourceHandler.class); } else if (wrapper.isDatatableResource()) { - if (wrapper.isCreate()) { + if (wrapper.isCreateDatatable()) { + handler = applicationContext.getBean("createDatatableCommandHandler", NewCommandSourceHandler.class); + } else if (wrapper.isDeleteDatatable()) { + handler = applicationContext.getBean("deleteDatatableCommandHandler", NewCommandSourceHandler.class); + } else if (wrapper.isUpdateDatatable()) { + handler = applicationContext.getBean("updateDatatableCommandHandler", NewCommandSourceHandler.class); + } else if (wrapper.isCreate()) { handler = applicationContext.getBean("createDatatableEntryCommandHandler", NewCommandSourceHandler.class); } else if (wrapper.isUpdateMultiple()) { handler = applicationContext.getBean("updateOneToManyDatatableEntryCommandHandler", NewCommandSourceHandler.class); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/api/JsonCommand.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/api/JsonCommand.java index b174ced3e..54f97801a 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/api/JsonCommand.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/api/JsonCommand.java @@ -45,24 +45,27 @@ public final class JsonCommand { private final String supportedEntityType; private final Long supportedEntityId; private final String transactionId; + private final String url; public static JsonCommand from(final String jsonCommand, final JsonElement parsedCommand, final FromJsonHelper fromApiJsonHelper, final String entityName, final Long resourceId, final Long subresourceId, final Long groupId, final Long clientId, final Long loanId, final Long savingsId, final Long codeId, final String supportedEntityType, final Long supportedEntityId, - final String transactionId) { + final String transactionId, final String url) { return new JsonCommand(null, jsonCommand, parsedCommand, fromApiJsonHelper, entityName, resourceId, subresourceId, groupId, - clientId, loanId, savingsId, codeId, supportedEntityType, supportedEntityId, transactionId); + clientId, loanId, savingsId, codeId, supportedEntityType, supportedEntityId, transactionId, url); } public static JsonCommand fromExistingCommand(final Long commandId, final String jsonCommand, final JsonElement parsedCommand, - final FromJsonHelper fromApiJsonHelper, final String entityName, final Long resourceId, final Long subresourceId) { + final FromJsonHelper fromApiJsonHelper, final String entityName, final Long resourceId, final Long subresourceId, + final String url) { return new JsonCommand(commandId, jsonCommand, parsedCommand, fromApiJsonHelper, entityName, resourceId, subresourceId, null, null, - null, null, null, null, null, null); + null, null, null, null, null, null, url); } public JsonCommand(final Long commandId, final String jsonCommand, final JsonElement parsedCommand, final FromJsonHelper fromApiJsonHelper, final String entityName, final Long resourceId, final Long subresourceId, final Long groupId, final Long clientId, final Long loanId, final Long savingsId, final Long codeId, - final String supportedEntityType, final Long supportedEntityId, final String transactionId) { + final String supportedEntityType, final Long supportedEntityId, final String transactionId, + final String url) { this.commandId = commandId; this.jsonCommand = jsonCommand; this.parsedCommand = parsedCommand; @@ -78,6 +81,7 @@ public final class JsonCommand { this.supportedEntityType = supportedEntityType; this.supportedEntityId = supportedEntityId; this.transactionId = transactionId; + this.url = url; } public String json() { @@ -144,6 +148,10 @@ public final class JsonCommand { public String getTransactionId() { return this.transactionId; } + + public String getUrl() { + return this.url; + } private boolean differenceExists(final LocalDate baseValue, final LocalDate workingCopyValue) { boolean differenceExists = false; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/data/DataValidatorBuilder.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/data/DataValidatorBuilder.java index 0fac1f6ce..35e0db5fd 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/data/DataValidatorBuilder.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/data/DataValidatorBuilder.java @@ -221,6 +221,27 @@ public class DataValidatorBuilder { } return this; } + + public DataValidatorBuilder isNotOneOfTheseValues(final Object... values) { + if (value == null && ignoreNullValue) { return this; } + + if (value != null) { + List valuesList = Arrays.asList(values); + + if (valuesList.contains(this.value)) { + StringBuilder validationErrorCode = new StringBuilder("validation.msg.").append(resource).append(".").append(parameter) + .append(".is.one.of.expected.enumerations"); + StringBuilder defaultEnglishMessage = new StringBuilder("The parameter ").append(parameter).append(" must not be one of [ ") + .append(values).append(" ] ").append("."); + + ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(), + defaultEnglishMessage.toString(), parameter, this.value, values); + + dataValidationErrors.add(error); + } + } + return this; + } public DataValidatorBuilder positiveAmount() { if (value == null && ignoreNullValue) { return this; } @@ -533,4 +554,23 @@ public class DataValidatorBuilder { } return this; } + + public DataValidatorBuilder matchesRegularExpression(final String expression) { + if (value == null && ignoreNullValue) { return this; } + + if (!value.toString().matches(expression)) { + StringBuilder validationErrorCode = new StringBuilder("validation.msg.").append(resource).append(".").append(parameter) + .append(".does.not.match.regexp"); + StringBuilder defaultEnglishMessage = new StringBuilder("The parameter ").append(parameter) + .append(" must match the provided regular expression ") + .append(expression).append(" ] ").append("."); + + ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(), + defaultEnglishMessage.toString(), parameter, this.value, expression); + + dataValidationErrors.add(error); + } + + return this; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/DatatableCommandFromApiJsonDeserializer.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/DatatableCommandFromApiJsonDeserializer.java new file mode 100644 index 000000000..5ed9cf5a2 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/DatatableCommandFromApiJsonDeserializer.java @@ -0,0 +1,202 @@ +package org.mifosplatform.infrastructure.core.serialization; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang.StringUtils; +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.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.reflect.TypeToken; + +@Component +public class DatatableCommandFromApiJsonDeserializer { + + private final static String DATATABLE_NAME_REGEX_PATTERN = "^[a-zA-Z][a-zA-Z0-9\\-_\\s]{0,48}[a-zA-Z0-9]$"; + /** + * The parameters supported for this command. + */ + private final Set supportedParametersForCreate = new HashSet(Arrays.asList( + "datatableName", "apptableName", "multiRow", "columns")); + private final Set supportedParametersForCreateColumns = new HashSet(Arrays.asList( + "name", "type", "length", "mandatory")); + private final Set supportedParametersForUpdate = new HashSet(Arrays.asList( + "apptableName", "changeColumns", "addColumns", "dropColumns")); + private final Set supportedParametersForAddColumns = new HashSet(Arrays.asList( + "name", "type", "length", "mandatory", "after")); + private final Set supportedParametersForChangeColumns = new HashSet(Arrays.asList( + "name", "newName", "type", "length", "mandatory", "after")); + private final Set supportedParametersForDropColumns = new HashSet(Arrays.asList( + "name")); + private final Object[] supportedColumnTypes = { "String", "Number", "Decimal", "Date" }; + + private final FromJsonHelper fromApiJsonHelper; + + @Autowired + public DatatableCommandFromApiJsonDeserializer(final FromJsonHelper fromApiJsonHelper) { + this.fromApiJsonHelper = fromApiJsonHelper; + } + + public void validateForCreate(final String json) { + if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); } + + final Type typeOfMap = new TypeToken>() {}.getType(); + fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, supportedParametersForCreate); + + final List dataValidationErrors = new ArrayList(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("datatable"); + + final JsonElement element = fromApiJsonHelper.parse(json); + final String apptableName = fromApiJsonHelper.extractStringNamed("apptableName", element); + final String fkColumnName = apptableName.substring(2) + "_id"; + baseDataValidator.reset().parameter("apptableName").value(apptableName).notNull().notBlank().notExceedingLengthOf(50); + + final String datatableName = fromApiJsonHelper.extractStringNamed("datatableName", element); + baseDataValidator.reset().parameter("datatableName").value(datatableName).notNull().notBlank().notExceedingLengthOf(50) + .matchesRegularExpression(DATATABLE_NAME_REGEX_PATTERN); + + final Boolean multiRow = fromApiJsonHelper.extractBooleanNamed("multiRow", element); + baseDataValidator.reset().parameter("multiRow").value(multiRow).ignoreIfNull().trueOrFalseRequired(multiRow); + + final JsonArray columns = fromApiJsonHelper.extractJsonArrayNamed("columns", element); + baseDataValidator.reset().parameter("columns").value(columns).notNull(); + + for (JsonElement column : columns) { + fromApiJsonHelper.checkForUnsupportedParameters(column.getAsJsonObject(), supportedParametersForCreateColumns); + + final String name = fromApiJsonHelper.extractStringNamed("name", column); + baseDataValidator.reset().parameter("name").value(name).notNull().notBlank().notExceedingLengthOf(50) + .isNotOneOfTheseValues("id", fkColumnName).matchesRegularExpression(DATATABLE_NAME_REGEX_PATTERN); + + final String type = fromApiJsonHelper.extractStringNamed("type", column); + baseDataValidator.reset().parameter("type").value(type).notNull().notBlank().isOneOfTheseValues(supportedColumnTypes); + + final Integer length = fromApiJsonHelper.extractIntegerSansLocaleNamed("length", column); + if (type.equals("String")) { + baseDataValidator.reset().parameter("length").value(length).notNull().notBlank() + .zeroOrPositiveAmount(); + } else { + baseDataValidator.reset().parameter("length").value(length).ignoreIfNull().notBlank() + .zeroOrPositiveAmount(); + } + + final Boolean mandatory = fromApiJsonHelper.extractBooleanNamed("mandatory", column); + baseDataValidator.reset().parameter("mandatory").value(mandatory).ignoreIfNull().trueOrFalseRequired(mandatory); + } + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + + public void validateForUpdate(final String json) { + if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); } + + final Type typeOfMap = new TypeToken>() {}.getType(); + fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, supportedParametersForUpdate); + + final List dataValidationErrors = new ArrayList(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("datatable"); + + final JsonElement element = fromApiJsonHelper.parse(json); + final String apptableName = fromApiJsonHelper.extractStringNamed("apptableName", element); + final String fkColumnName = apptableName.substring(2) + "_id"; + baseDataValidator.reset().parameter("apptableName").value(apptableName).notBlank().notExceedingLengthOf(50); + + if (fromApiJsonHelper.parameterExists("changeColumns", element)) { + final JsonArray changeColumns = fromApiJsonHelper.extractJsonArrayNamed("changeColumns", element); + baseDataValidator.reset().parameter("changeColumns").value(changeColumns).ignoreIfNull(); + + for (JsonElement column : changeColumns) { + fromApiJsonHelper.checkForUnsupportedParameters(column.getAsJsonObject(), supportedParametersForChangeColumns); + + final String name = fromApiJsonHelper.extractStringNamed("name", column); + baseDataValidator.reset().parameter("name").value(name).notNull().notBlank().notExceedingLengthOf(50) + .isNotOneOfTheseValues("id", fkColumnName).matchesRegularExpression(DATATABLE_NAME_REGEX_PATTERN); + + final String newName = fromApiJsonHelper.extractStringNamed("newName", column); + baseDataValidator.reset().parameter("newName").value(newName).ignoreIfNull().notBlank() + .notExceedingLengthOf(50).isNotOneOfTheseValues("id", fkColumnName) + .matchesRegularExpression(DATATABLE_NAME_REGEX_PATTERN); + + final String type = fromApiJsonHelper.extractStringNamed("type", column); + baseDataValidator.reset().parameter("type").value(type).notNull().notBlank() + .isOneOfTheseValues(supportedColumnTypes); + + final Integer length = fromApiJsonHelper.extractIntegerSansLocaleNamed("length", column); + if (type.equals("String")) { + baseDataValidator.reset().parameter("length").value(length).notNull().notBlank() + .zeroOrPositiveAmount(); + } else { + baseDataValidator.reset().parameter("length").value(length).ignoreIfNull().notBlank() + .zeroOrPositiveAmount(); + } + + final Boolean mandatory = fromApiJsonHelper.extractBooleanNamed("mandatory", column); + baseDataValidator.reset().parameter("mandatory").value(mandatory).ignoreIfNull().trueOrFalseRequired(mandatory); + + final Boolean after = fromApiJsonHelper.extractBooleanNamed("after", column); + baseDataValidator.reset().parameter("after").value(after).ignoreIfNull().notBlank(); + } + } + if (fromApiJsonHelper.parameterExists("addColumns", element)) { + final JsonArray addColumns = fromApiJsonHelper.extractJsonArrayNamed("addColumns", element); + baseDataValidator.reset().parameter("addColumns").value(addColumns).ignoreIfNull(); + + for (JsonElement column : addColumns) { + fromApiJsonHelper.checkForUnsupportedParameters(column.getAsJsonObject(), supportedParametersForAddColumns); + + final String name = fromApiJsonHelper.extractStringNamed("name", column); + baseDataValidator.reset().parameter("name").value(name).notNull().notBlank().notExceedingLengthOf(50) + .isNotOneOfTheseValues("id", fkColumnName).matchesRegularExpression(DATATABLE_NAME_REGEX_PATTERN); + + final String type = fromApiJsonHelper.extractStringNamed("type", column); + baseDataValidator.reset().parameter("type").value(type).notNull().notBlank() + .isOneOfTheseValues(supportedColumnTypes); + + final Integer length = fromApiJsonHelper.extractIntegerSansLocaleNamed("length", column); + if (type.equals("String")) { + baseDataValidator.reset().parameter("length").value(length).notNull().notBlank() + .zeroOrPositiveAmount(); + } else { + baseDataValidator.reset().parameter("length").value(length).ignoreIfNull().notBlank() + .zeroOrPositiveAmount(); + } + + final Boolean mandatory = fromApiJsonHelper.extractBooleanNamed("mandatory", column); + baseDataValidator.reset().parameter("mandatory").value(mandatory).ignoreIfNull().trueOrFalseRequired(mandatory); + + final Boolean after = fromApiJsonHelper.extractBooleanNamed("after", column); + baseDataValidator.reset().parameter("after").value(after).ignoreIfNull().notBlank(); + } + } + if (fromApiJsonHelper.parameterExists("dropColumns", element)) { + final JsonArray dropColumns = fromApiJsonHelper.extractJsonArrayNamed("dropColumns", element); + baseDataValidator.reset().parameter("dropColumns").value(dropColumns).ignoreIfNull(); + + for (JsonElement column : dropColumns) { + fromApiJsonHelper.checkForUnsupportedParameters(column.getAsJsonObject(), supportedParametersForDropColumns); + + final String name = fromApiJsonHelper.extractStringNamed("name", column); + baseDataValidator.reset().parameter("name").value(name).notNull().notBlank().notExceedingLengthOf(50) + .isNotOneOfTheseValues("id", fkColumnName).matchesRegularExpression(DATATABLE_NAME_REGEX_PATTERN); + } + } + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + + private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", + "Validation errors exist.", dataValidationErrors); } + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/FromJsonHelper.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/FromJsonHelper.java index 2e93eb625..23d2f2eba 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/FromJsonHelper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/FromJsonHelper.java @@ -7,11 +7,13 @@ package org.mifosplatform.infrastructure.core.serialization; import java.lang.reflect.Type; import java.math.BigDecimal; +import java.security.InvalidParameterException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import org.apache.commons.lang.StringUtils; @@ -78,6 +80,21 @@ public class FromJsonHelper { if (!unsupportedParameterList.isEmpty()) { throw new UnsupportedParameterException(unsupportedParameterList); } } + + public void checkForUnsupportedParameters(final JsonObject object, final Set supportedParams) { + if (object == null) { throw new InvalidParameterException(); } + + Set> entries = object.entrySet(); + List unsupportedParameterList = new ArrayList(); + + for (Entry providedParameter : entries) { + if (!supportedParams.contains(providedParameter.getKey())) { + unsupportedParameterList.add(providedParameter.getKey()); + } + } + + if (!unsupportedParameterList.isEmpty()) { throw new UnsupportedParameterException(unsupportedParameterList); } + } public JsonElement parse(final String json) { return parser.parse(json); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/api/DatatablesApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/api/DatatablesApiResource.java index 39a9771d8..b1ac99140 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/api/DatatablesApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/api/DatatablesApiResource.java @@ -70,6 +70,46 @@ public class DatatablesApiResource { return this.toApiJsonSerializer.serializePretty(prettyPrint, result); } + @POST + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String createDatatable(final String apiRequestBodyAsJson) { + + final CommandWrapper commandRequest = new CommandWrapperBuilder() + .createDBDatatable(apiRequestBodyAsJson).build(); + + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + return this.toApiJsonSerializer.serialize(result); + } + + @PUT + @Path("{datatableName}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String updateDatatable(@PathParam("datatableName") final String datatableName, + final String apiRequestBodyAsJson) { + + final CommandWrapper commandRequest = new CommandWrapperBuilder() + .updateDBDatatable(datatableName, apiRequestBodyAsJson).build(); + + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + return this.toApiJsonSerializer.serialize(result); + } + + @DELETE + @Path("{datatableName}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String deleteDatatable(@PathParam("datatableName") final String datatableName, + final String apiRequestBodyAsJson) { + + final CommandWrapper commandRequest = new CommandWrapperBuilder() + .deleteDBDatatable(datatableName, apiRequestBodyAsJson).build(); + + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + return this.toApiJsonSerializer.serialize(result); + } + @POST @Path("register/{datatable}/{apptable}") @Consumes({ MediaType.APPLICATION_JSON }) @@ -95,6 +135,18 @@ public class DatatablesApiResource { return this.toApiJsonSerializer.serialize(result); } + @GET + @Path("{datatable}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String getDatatable(@PathParam("datatable") final String datatable, @Context final UriInfo uriInfo) { + + final DatatableData result = this.readWriteNonCoreDataService.retrieveDatatable(datatable); + + final boolean prettyPrint = ApiParameterHelper.prettyPrint(uriInfo.getQueryParameters()); + return this.toApiJsonSerializer.serializePretty(prettyPrint, result); + } + @GET @Path("{datatable}/{apptableId}") @Consumes({ MediaType.APPLICATION_JSON }) diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/data/DatatableData.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/data/DatatableData.java index cf7ca9774..99a5c59b5 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/data/DatatableData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/data/DatatableData.java @@ -5,6 +5,8 @@ */ package org.mifosplatform.infrastructure.dataqueries.data; +import java.util.List; + /** * Immutable data object representing datatable data. */ @@ -14,13 +16,18 @@ public class DatatableData { private final String applicationTableName; @SuppressWarnings("unused") private final String registeredTableName; - - public static DatatableData create(final String applicationTableName, final String registeredTableName) { - return new DatatableData(applicationTableName, registeredTableName); + @SuppressWarnings("unused") + private final List columnHeaderData; + + public static DatatableData create(final String applicationTableName, final String registeredTableName, + final List columnHeaderData) { + return new DatatableData(applicationTableName, registeredTableName, columnHeaderData); } - private DatatableData(final String applicationTableName, final String registeredTableName) { + private DatatableData(final String applicationTableName, final String registeredTableName, + final List columnHeaderData) { this.applicationTableName = applicationTableName; this.registeredTableName = registeredTableName; + this.columnHeaderData = columnHeaderData; } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/CreateDatatableCommandHandler.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/CreateDatatableCommandHandler.java new file mode 100644 index 000000000..9f41e004e --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/CreateDatatableCommandHandler.java @@ -0,0 +1,32 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.infrastructure.dataqueries.handler; + +import org.mifosplatform.commands.handler.NewCommandSourceHandler; +import org.mifosplatform.infrastructure.core.api.JsonCommand; +import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; +import org.mifosplatform.infrastructure.dataqueries.service.ReadWriteNonCoreDataService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class CreateDatatableCommandHandler implements NewCommandSourceHandler { + + private final ReadWriteNonCoreDataService writePlatformService; + + @Autowired + public CreateDatatableCommandHandler(final ReadWriteNonCoreDataService writePlatformService) { + this.writePlatformService = writePlatformService; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + return this.writePlatformService.createDatatable(command); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/DeleteDatatableCommandHandler.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/DeleteDatatableCommandHandler.java new file mode 100644 index 000000000..4b7d067fc --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/DeleteDatatableCommandHandler.java @@ -0,0 +1,40 @@ +/** + * 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.infrastructure.dataqueries.handler; + +import org.mifosplatform.commands.handler.NewCommandSourceHandler; +import org.mifosplatform.infrastructure.core.api.JsonCommand; +import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; +import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder; +import org.mifosplatform.infrastructure.dataqueries.service.ReadWriteNonCoreDataService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class DeleteDatatableCommandHandler implements NewCommandSourceHandler { + + private final ReadWriteNonCoreDataService writePlatformService; + + @Autowired + public DeleteDatatableCommandHandler(final ReadWriteNonCoreDataService writePlatformService) { + this.writePlatformService = writePlatformService; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + final String datatableName = command.getUrl().replaceAll("/datatables/", ""); + + this.writePlatformService.deleteDatatable(datatableName); + + return new CommandProcessingResultBuilder() + .withCommandId(command.commandId()) + .withResourceIdAsString(datatableName) + .build(); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/UpdateDatatableCommandHandler.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/UpdateDatatableCommandHandler.java new file mode 100644 index 000000000..b96a8bfc5 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/UpdateDatatableCommandHandler.java @@ -0,0 +1,40 @@ +/** + * 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.infrastructure.dataqueries.handler; + +import org.mifosplatform.commands.handler.NewCommandSourceHandler; +import org.mifosplatform.infrastructure.core.api.JsonCommand; +import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; +import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder; +import org.mifosplatform.infrastructure.dataqueries.service.ReadWriteNonCoreDataService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class UpdateDatatableCommandHandler implements NewCommandSourceHandler { + + private final ReadWriteNonCoreDataService writePlatformService; + + @Autowired + public UpdateDatatableCommandHandler(final ReadWriteNonCoreDataService writePlatformService) { + this.writePlatformService = writePlatformService; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + final String datatableName = command.getUrl().replaceAll("/datatables/", ""); + + this.writePlatformService.updateDatatable(datatableName, command); + + return new CommandProcessingResultBuilder() + .withCommandId(command.commandId()) + .withResourceIdAsString(datatableName) + .build(); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadWriteNonCoreDataService.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadWriteNonCoreDataService.java index b01e52f06..5f67f4768 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadWriteNonCoreDataService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadWriteNonCoreDataService.java @@ -16,7 +16,9 @@ import org.springframework.security.access.prepost.PreAuthorize; public interface ReadWriteNonCoreDataService { List retrieveDatatableNames(String appTable); - + + DatatableData retrieveDatatable(String datatable); + @PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'REGISTER_DATATABLE')") void registerDatatable(String datatable, String appTable); @@ -25,6 +27,12 @@ public interface ReadWriteNonCoreDataService { GenericResultsetData retrieveDataTableGenericResultSet(String datatable, Long appTableId, String order, Long id); + CommandProcessingResult createDatatable(JsonCommand command); + + void updateDatatable(String datatableName, JsonCommand command); + + void deleteDatatable(String datatableName); + CommandProcessingResult createNewDatatableEntry(String datatable, Long appTableId, JsonCommand command); CommandProcessingResult updateDatatableEntryOneToOne(String datatable, Long appTableId, JsonCommand command); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java index 54b5b8bba..e36b0033d 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java @@ -25,6 +25,7 @@ import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder; import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException; import org.mifosplatform.infrastructure.core.exception.PlatformDataIntegrityException; +import org.mifosplatform.infrastructure.core.serialization.DatatableCommandFromApiJsonDeserializer; import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper; import org.mifosplatform.infrastructure.core.serialization.JsonParserHelper; import org.mifosplatform.infrastructure.core.service.TenantAwareRoutingDataSource; @@ -47,12 +48,20 @@ import org.springframework.jdbc.support.rowset.SqlRowSetMetaData; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import com.google.gson.reflect.TypeToken; @Service public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataService { + private final static String DATATABLE_NAME_REGEX_PATTERN = "^[a-zA-Z][a-zA-Z0-9\\-_\\s]{0,48}[a-zA-Z0-9]$"; + private final static Logger logger = LoggerFactory.getLogger(ReadWriteNonCoreDataServiceImpl.class); + private final static HashMap apiTypeToMySQL = new HashMap() {{ + put("String", "VARCHAR"); put("Number", "INT"); put("Decimal", "DECIMAL"); put("Date", "DATE"); + }}; private final JdbcTemplate jdbcTemplate; private final DataSource dataSource; @@ -60,16 +69,19 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ private final FromJsonHelper fromJsonHelper; private final JsonParserHelper helper; private final GenericDataService genericDataService; + private final DatatableCommandFromApiJsonDeserializer fromApiJsonDeserializer; @Autowired public ReadWriteNonCoreDataServiceImpl(final TenantAwareRoutingDataSource dataSource, final PlatformSecurityContext context, - final FromJsonHelper fromJsonHelper, final GenericDataService genericDataService) { + final FromJsonHelper fromJsonHelper, final GenericDataService genericDataService, + final DatatableCommandFromApiJsonDeserializer fromApiJsonDeserializer) { this.dataSource = dataSource; this.jdbcTemplate = new JdbcTemplate(this.dataSource); this.context = context; this.fromJsonHelper = fromJsonHelper; this.helper = new JsonParserHelper(); this.genericDataService = genericDataService; + this.fromApiJsonDeserializer = fromApiJsonDeserializer; } @Override @@ -96,12 +108,41 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ while (rs.next()) { final String appTableName = rs.getString("application_table_name"); final String registeredDatatableName = rs.getString("registered_table_name"); - - datatables.add(DatatableData.create(appTableName, registeredDatatableName)); + final List columnHeaderData = + this.genericDataService.fillResultsetColumnHeaders(registeredDatatableName); + + datatables.add(DatatableData.create(appTableName, registeredDatatableName, columnHeaderData)); } return datatables; } + + @Override + public DatatableData retrieveDatatable(final String datatable) { + + // PERMITTED datatables + final String sql = "select application_table_name, registered_table_name" + " from x_registered_table " + " where exists" + + " (select 'f'" + " from m_appuser_role ur " + " join m_role r on r.id = ur.role_id" + + " left join m_role_permission rp on rp.role_id = r.id" + " left join m_permission p on p.id = rp.permission_id" + + " where ur.appuser_id = " + context.authenticatedUser().getId() + + " and registered_table_name='" + datatable + "'" + + " and (p.code in ('ALL_FUNCTIONS', 'ALL_FUNCTIONS_READ') or p.code = concat('READ_', registered_table_name))) " + + " order by application_table_name, registered_table_name"; + + final SqlRowSet rs = this.jdbcTemplate.queryForRowSet(sql); + + DatatableData datatableData = null; + while (rs.next()) { + final String appTableName = rs.getString("application_table_name"); + final String registeredDatatableName = rs.getString("registered_table_name"); + final List columnHeaderData = + this.genericDataService.fillResultsetColumnHeaders(registeredDatatableName); + + datatableData = DatatableData.create(appTableName, registeredDatatableName, columnHeaderData); + } + + return datatableData; + } private void logAsErrorUnexpectedDataIntegrityException(final Exception dve) { logger.error(dve.getMessage(), dve); @@ -240,6 +281,253 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ } } + private Boolean datatableExists(final String datatable) { + + String sql = "SELECT COUNT(*) FROM `x_registered_table` WHERE `registered_table_name`='" + datatable + "'"; + Integer rowCount = this.jdbcTemplate.queryForInt(sql); + + return (rowCount > 0); + } + + private void validateDatatableExists(final String name) { + + if (!datatableExists(name)) { + throw new PlatformDataIntegrityException("error.msg.datatables.missing.table.name", + "Table '" + name + "' does not exist.", name); + } + } + + private void validateDatatableNotExists(final String name) { + + if (datatableExists(name)) { + throw new PlatformDataIntegrityException("error.msg.datatables.duplicate.table.name", + "Table '" + name + "' already exists.", name); + } + } + + private void validateDatatableName(final String name) { + + if (name == null || name.isEmpty()) { + throw new PlatformDataIntegrityException("error.msg.datatables.datatable.null.name", + "Data table name must not be blank."); + } else if (!name.matches(DATATABLE_NAME_REGEX_PATTERN)) { + throw new PlatformDataIntegrityException("error.msg.datatables.datatable.invalid.name.regex", + "Invalid data table name.", name); + } + } + + private void parseDatatableColumnObjectForCreate(final JsonObject column, StringBuilder sqlBuilder) { + + String name = (column.has("name")) ? column.get("name").getAsString() : null; + String type = (column.has("type")) ? column.get("type").getAsString() : null; + Integer length = (column.has("length")) ? column.get("length").getAsInt() : null; + Boolean mandatory = (column.has("mandatory")) ? column.get("mandatory").getAsBoolean() : false; + + String mysqlType = apiTypeToMySQL.get(type); + sqlBuilder = sqlBuilder.append("`" + name + "` " + mysqlType); + + if (type != null) + { + if (type.equals("String")) { + sqlBuilder = sqlBuilder.append("(" + length + ")"); + } else if (type.equals("Decimal")) { + sqlBuilder = sqlBuilder.append("(19,6)"); + } + } + if (mandatory != null) { + if (mandatory) { + sqlBuilder = sqlBuilder.append(" NOT NULL"); + } else { + sqlBuilder = sqlBuilder.append(" DEFAULT NULL"); + } + } + + sqlBuilder = sqlBuilder.append(", "); + } + + @Transactional + @Override + public CommandProcessingResult createDatatable(final JsonCommand command) { + + context.authenticatedUser(); + this.fromApiJsonDeserializer.validateForCreate(command.json()); + + JsonElement element = this.fromJsonHelper.parse(command.json()); + JsonArray columns = this.fromJsonHelper.extractJsonArrayNamed("columns", element); + String datatableName = this.fromJsonHelper.extractStringNamed("datatableName", element); + String apptable = this.fromJsonHelper.extractStringNamed("apptableName", element); + Boolean multiRow = this.fromJsonHelper.extractBooleanNamed("multiRow", element); + + if (multiRow == null) { + multiRow = false; + } + + validateDatatableName(datatableName); + validateDatatableName(apptable); + validateDatatableNotExists(datatableName); + + String fkColumnName = apptable.substring(2) + "_id"; + String fkName = datatableName.toLowerCase().replaceAll("\\s", "_"); + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder = sqlBuilder.append("CREATE TABLE `" + datatableName + "` ("); + + if (multiRow) { + sqlBuilder = sqlBuilder + .append("`id` BIGINT(20) NOT NULL AUTO_INCREMENT, ") + .append("`" + fkColumnName + "` BIGINT(20) NOT NULL, "); + } else { + sqlBuilder = sqlBuilder + .append("`" + fkColumnName + "` BIGINT(20) NOT NULL, "); + } + + for (JsonElement column : columns) { + parseDatatableColumnObjectForCreate(column.getAsJsonObject(), sqlBuilder); + } + + // Remove trailing comma and space + sqlBuilder = sqlBuilder.delete(sqlBuilder.length() - 2, sqlBuilder.length()); + + if (multiRow) { + sqlBuilder = sqlBuilder + .append(", PRIMARY KEY (`id`)") + .append(", KEY `fk_" + apptable + "` (`" + fkColumnName + "`)") + .append(", CONSTRAINT `fk_" + fkName + "` ") + .append("FOREIGN KEY (`" + fkColumnName + "`) ") + .append("REFERENCES `" + apptable + "` (`id`)"); + + } else { + sqlBuilder = sqlBuilder + .append(", PRIMARY KEY (`" + fkColumnName + "`)") + .append(", CONSTRAINT `fk_" + fkName + "` ") + .append("FOREIGN KEY (`" + fkColumnName + "`) ") + .append("REFERENCES `" + apptable + "` (`id`)"); + } + + sqlBuilder = sqlBuilder.append(") ENGINE=InnoDB DEFAULT CHARSET=utf8;"); + this.jdbcTemplate.execute(sqlBuilder.toString()); + + registerDatatable(datatableName, apptable); + + return new CommandProcessingResultBuilder() + .withCommandId(command.commandId()) + .withResourceIdAsString(datatableName) + .build(); + } + + private void parseDatatableColumnForUpdate(final JsonObject column, StringBuilder sqlBuilder) { + + String name = (column.has("name")) ? column.get("name").getAsString() : null; + String newName = (column.has("newName")) ? column.get("newName").getAsString() : name; + String type = (column.has("type")) ? column.get("type").getAsString() : null; + Integer length = (column.has("length")) ? column.get("length").getAsInt() : null; + Boolean mandatory = (column.has("mandatory")) ? column.get("mandatory").getAsBoolean() : false; + String after = (column.has("after")) ? column.get("after").getAsString() : null; + + String mysqlType = apiTypeToMySQL.get(type); + sqlBuilder = sqlBuilder.append(", CHANGE `" + name + "` `" + newName + "` " + mysqlType); + + if (type != null) { + if (type.equals("String") && length != null) { + sqlBuilder = sqlBuilder.append("(" + length + ")"); + } else if (type.equals("Decimal")) { + sqlBuilder = sqlBuilder.append("(19,6)"); + } + } + if (mandatory != null) { + if (mandatory) { + sqlBuilder = sqlBuilder.append(" NOT NULL"); + } else { + sqlBuilder = sqlBuilder.append(" DEFAULT NULL"); + } + } + if (after != null) { + sqlBuilder = sqlBuilder.append(" AFTER `" + after + "`"); + } + } + + private void parseDatatableColumnForAdd(final JsonObject column, StringBuilder sqlBuilder) { + + String name = (column.has("name")) ? column.get("name").getAsString() : null; + String type = (column.has("type")) ? column.get("type").getAsString() : null; + Integer length = (column.has("length")) ? column.get("length").getAsInt() : null; + Boolean mandatory = (column.has("mandatory")) ? column.get("mandatory").getAsBoolean() : false; + String after = (column.has("after")) ? column.get("after").getAsString() : null; + + String mysqlType = apiTypeToMySQL.get(type); + sqlBuilder = sqlBuilder.append(", ADD `" + name + "` " + mysqlType); + + if (type != null) { + if (type.equals("String") && length != null) { + sqlBuilder = sqlBuilder.append("(" + length + ")"); + } else if (type.equals("Decimal")) { + sqlBuilder = sqlBuilder.append("(19,6)"); + } + } + if (mandatory != null) { + if (mandatory) { + sqlBuilder = sqlBuilder.append(" NOT NULL"); + } else { + sqlBuilder = sqlBuilder.append(" DEFAULT NULL"); + } + } + if (after != null) { + sqlBuilder = sqlBuilder.append(" AFTER `" + after + "`"); + } + } + + private void parseDatatableColumnForDrop(final JsonObject column, StringBuilder sqlBuilder) { + + String name = (column.has("name")) ? column.get("name").getAsString() : null; + + sqlBuilder = sqlBuilder.append(", DROP COLUMN `" + name + "`"); + } + + @Transactional + @Override + public void updateDatatable(final String datatableName, final JsonCommand command) { + + context.authenticatedUser(); + this.fromApiJsonDeserializer.validateForUpdate(command.json()); + + JsonElement element = this.fromJsonHelper.parse(command.json()); + JsonArray changeColumns = this.fromJsonHelper.extractJsonArrayNamed("changeColumns", element); + JsonArray addColumns = this.fromJsonHelper.extractJsonArrayNamed("addColumns", element); + JsonArray dropColumns = this.fromJsonHelper.extractJsonArrayNamed("dropColumns", element); + + validateDatatableName(datatableName); + validateDatatableExists(datatableName); + + StringBuilder sqlBuilder = new StringBuilder("ALTER TABLE `" + datatableName + "`"); + + for (JsonElement column : changeColumns) { + parseDatatableColumnForUpdate(column.getAsJsonObject(), sqlBuilder); + } + for (JsonElement column : addColumns) { + parseDatatableColumnForAdd(column.getAsJsonObject(), sqlBuilder); + } + for (JsonElement column : dropColumns) { + parseDatatableColumnForDrop(column.getAsJsonObject(), sqlBuilder); + } + + // Remove the first comma, right after ALTER TABLE `datatable` + sqlBuilder = sqlBuilder.deleteCharAt(sqlBuilder.indexOf(",")); + this.jdbcTemplate.execute(sqlBuilder.toString()); + } + + @Transactional + @Override + public void deleteDatatable(final String datatableName) { + + context.authenticatedUser(); + + validateDatatableName(datatableName); + validateDatatableExists(datatableName); + deregisterDatatable(datatableName); + + String sql = "DROP TABLE `" + datatableName + "`"; + this.jdbcTemplate.execute(sql); + } + @Transactional @Override public CommandProcessingResult updateDatatableEntryOneToOne(final String dataTableName, final Long appTableId, final JsonCommand command) { From e2c4ce9a1336238fb32f40aca190fb370639246a Mon Sep 17 00:00:00 2001 From: Keith Woodlock Date: Tue, 28 May 2013 21:21:43 +0100 Subject: [PATCH 04/49] MIFOSX-365: formatting and add license to files without it. --- .../InvalidParentGLAccountHeadException.java | 12 +- .../data/PaymentTypeToGLAccountMapper.java | 5 + .../LoanProductToGLAccountMappingHelper.java | 17 +- .../ProductToGLAccountMappingHelper.java | 5 + ...avingsProductToGLAccountMappingHelper.java | 5 + .../rule/data/AccountingTagRuleData.java | 5 + .../rule/domain/AccountingTagRule.java | 5 + .../service/AccountingRuleDataValidator.java | 5 + ...tatableCommandFromApiJsonDeserializer.java | 5 + .../CreateDatatableCommandHandler.java | 2 +- .../UpdateDatatableCommandHandler.java | 14 +- .../ReadWriteNonCoreDataServiceImpl.java | 611 +++++++++--------- .../resources/META-INF/spring/scheduler.xml | 8 +- 13 files changed, 363 insertions(+), 336 deletions(-) diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/exception/InvalidParentGLAccountHeadException.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/exception/InvalidParentGLAccountHeadException.java index 353bbbb42..751ae1e9f 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/exception/InvalidParentGLAccountHeadException.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/exception/InvalidParentGLAccountHeadException.java @@ -1,11 +1,17 @@ +/** + * 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.glaccount.exception; import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRuleException; public class InvalidParentGLAccountHeadException extends AbstractPlatformDomainRuleException { - public InvalidParentGLAccountHeadException(Long glAccountId, Long parentId) { - super("error.msg.glaccount.id.and.parentid.must.not.same", "parentId:"+parentId+", id"+glAccountId+" should not be same", glAccountId, parentId); - } + public InvalidParentGLAccountHeadException(final Long glAccountId, final Long parentId) { + super("error.msg.glaccount.id.and.parentid.must.not.same", "parentId:" + parentId + ", id" + glAccountId + " should not be same", + glAccountId, parentId); + } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/data/PaymentTypeToGLAccountMapper.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/data/PaymentTypeToGLAccountMapper.java index 96e3afcd7..03ab03fe4 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/data/PaymentTypeToGLAccountMapper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/data/PaymentTypeToGLAccountMapper.java @@ -1,3 +1,8 @@ +/** + * 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.producttoaccountmapping.data; public class PaymentTypeToGLAccountMapper { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/LoanProductToGLAccountMappingHelper.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/LoanProductToGLAccountMappingHelper.java index 95f224e3f..a34977867 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/LoanProductToGLAccountMappingHelper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/LoanProductToGLAccountMappingHelper.java @@ -1,12 +1,17 @@ +/** + * 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.producttoaccountmapping.service; import java.util.HashMap; import java.util.Map; -import org.mifosplatform.accounting.common.AccountingRuleType; import org.mifosplatform.accounting.common.AccountingConstants.ACCRUAL_ACCOUNTS_FOR_LOAN; import org.mifosplatform.accounting.common.AccountingConstants.CASH_ACCOUNTS_FOR_LOAN; import org.mifosplatform.accounting.common.AccountingConstants.LOAN_PRODUCT_ACCOUNTING_PARAMS; +import org.mifosplatform.accounting.common.AccountingRuleType; import org.mifosplatform.accounting.glaccount.domain.GLAccountRepository; import org.mifosplatform.accounting.glaccount.domain.GLAccountType; import org.mifosplatform.accounting.producttoaccountmapping.domain.PortfolioProductType; @@ -23,9 +28,9 @@ import com.google.gson.JsonElement; public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappingHelper { @Autowired - public LoanProductToGLAccountMappingHelper(GLAccountRepository glAccountRepository, - ProductToGLAccountMappingRepository glAccountMappingRepository, FromJsonHelper fromApiJsonHelper, - CodeValueRepositoryWrapper codeValueRepositoryWrapper) { + public LoanProductToGLAccountMappingHelper(final GLAccountRepository glAccountRepository, + final ProductToGLAccountMappingRepository glAccountMappingRepository, final FromJsonHelper fromApiJsonHelper, + final CodeValueRepositoryWrapper codeValueRepositoryWrapper) { super(glAccountRepository, glAccountMappingRepository, fromApiJsonHelper, codeValueRepositoryWrapper); } @@ -68,12 +73,12 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi /*** Abstractions for payments channel related to loan products ***/ public void savePaymentChannelToFundSourceMappings(final JsonCommand command, final JsonElement element, final Long productId, - Map changes) { + final Map changes) { savePaymentChannelToFundSourceMappings(command, element, productId, changes, PortfolioProductType.LOAN); } public void updatePaymentChannelToFundSourceMappings(final JsonCommand command, final JsonElement element, final Long productId, - Map changes) { + final Map changes) { updatePaymentChannelToFundSourceMappings(command, element, productId, changes, PortfolioProductType.LOAN); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/ProductToGLAccountMappingHelper.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/ProductToGLAccountMappingHelper.java index 021c5f136..f34d959ad 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/ProductToGLAccountMappingHelper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/ProductToGLAccountMappingHelper.java @@ -1,3 +1,8 @@ +/** + * 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.producttoaccountmapping.service; import java.util.HashMap; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/SavingsProductToGLAccountMappingHelper.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/SavingsProductToGLAccountMappingHelper.java index a5055f201..a32857d5a 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/SavingsProductToGLAccountMappingHelper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/SavingsProductToGLAccountMappingHelper.java @@ -1,3 +1,8 @@ +/** + * 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.producttoaccountmapping.service; import java.util.HashMap; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/data/AccountingTagRuleData.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/data/AccountingTagRuleData.java index f4988b7d9..f1d985caf 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/data/AccountingTagRuleData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/data/AccountingTagRuleData.java @@ -1,3 +1,8 @@ +/** + * 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.rule.data; import org.mifosplatform.infrastructure.core.data.EnumOptionData; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/domain/AccountingTagRule.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/domain/AccountingTagRule.java index f83aa310e..82603f1f3 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/domain/AccountingTagRule.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/domain/AccountingTagRule.java @@ -1,3 +1,8 @@ +/** + * 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.rule.domain; import javax.persistence.Column; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleDataValidator.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleDataValidator.java index 76ac1d5b5..83b89af41 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleDataValidator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleDataValidator.java @@ -1,3 +1,8 @@ +/** + * 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.rule.service; import java.lang.reflect.Type; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/DatatableCommandFromApiJsonDeserializer.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/DatatableCommandFromApiJsonDeserializer.java index 5ed9cf5a2..f1201cb30 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/DatatableCommandFromApiJsonDeserializer.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/DatatableCommandFromApiJsonDeserializer.java @@ -1,3 +1,8 @@ +/** + * 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.infrastructure.core.serialization; import java.lang.reflect.Type; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/CreateDatatableCommandHandler.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/CreateDatatableCommandHandler.java index 9f41e004e..ec287d4af 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/CreateDatatableCommandHandler.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/CreateDatatableCommandHandler.java @@ -26,7 +26,7 @@ public class CreateDatatableCommandHandler implements NewCommandSourceHandler { @Transactional @Override public CommandProcessingResult processCommand(final JsonCommand command) { - + return this.writePlatformService.createDatatable(command); } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/UpdateDatatableCommandHandler.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/UpdateDatatableCommandHandler.java index b96a8bfc5..625e62330 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/UpdateDatatableCommandHandler.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/UpdateDatatableCommandHandler.java @@ -27,14 +27,14 @@ public class UpdateDatatableCommandHandler implements NewCommandSourceHandler { @Transactional @Override public CommandProcessingResult processCommand(final JsonCommand command) { - - final String datatableName = command.getUrl().replaceAll("/datatables/", ""); - + + final String datatableName = command.getUrl().replaceAll("/datatables/", ""); + this.writePlatformService.updateDatatable(datatableName, command); - return new CommandProcessingResultBuilder() - .withCommandId(command.commandId()) - .withResourceIdAsString(datatableName) - .build(); + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withResourceIdAsString(datatableName) // + .build(); } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java index e36b0033d..fdc2c3e7a 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java @@ -56,12 +56,18 @@ import com.google.gson.reflect.TypeToken; @Service public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataService { - private final static String DATATABLE_NAME_REGEX_PATTERN = "^[a-zA-Z][a-zA-Z0-9\\-_\\s]{0,48}[a-zA-Z0-9]$"; + private final static String DATATABLE_NAME_REGEX_PATTERN = "^[a-zA-Z][a-zA-Z0-9\\-_\\s]{0,48}[a-zA-Z0-9]$"; private final static Logger logger = LoggerFactory.getLogger(ReadWriteNonCoreDataServiceImpl.class); - private final static HashMap apiTypeToMySQL = new HashMap() {{ - put("String", "VARCHAR"); put("Number", "INT"); put("Decimal", "DECIMAL"); put("Date", "DATE"); - }}; + private final static HashMap apiTypeToMySQL = new HashMap() { + + { + put("String", "VARCHAR"); + put("Number", "INT"); + put("Decimal", "DECIMAL"); + put("Date", "DATE"); + } + }; private final JdbcTemplate jdbcTemplate; private final DataSource dataSource; @@ -108,24 +114,23 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ while (rs.next()) { final String appTableName = rs.getString("application_table_name"); final String registeredDatatableName = rs.getString("registered_table_name"); - final List columnHeaderData = - this.genericDataService.fillResultsetColumnHeaders(registeredDatatableName); - + final List columnHeaderData = this.genericDataService + .fillResultsetColumnHeaders(registeredDatatableName); + datatables.add(DatatableData.create(appTableName, registeredDatatableName, columnHeaderData)); } return datatables; } - + @Override public DatatableData retrieveDatatable(final String datatable) { - + // PERMITTED datatables final String sql = "select application_table_name, registered_table_name" + " from x_registered_table " + " where exists" + " (select 'f'" + " from m_appuser_role ur " + " join m_role r on r.id = ur.role_id" + " left join m_role_permission rp on rp.role_id = r.id" + " left join m_permission p on p.id = rp.permission_id" - + " where ur.appuser_id = " + context.authenticatedUser().getId() - + " and registered_table_name='" + datatable + "'" + + " where ur.appuser_id = " + context.authenticatedUser().getId() + " and registered_table_name='" + datatable + "'" + " and (p.code in ('ALL_FUNCTIONS', 'ALL_FUNCTIONS_READ') or p.code = concat('READ_', registered_table_name))) " + " order by application_table_name, registered_table_name"; @@ -135,9 +140,9 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ while (rs.next()) { final String appTableName = rs.getString("application_table_name"); final String registeredDatatableName = rs.getString("registered_table_name"); - final List columnHeaderData = - this.genericDataService.fillResultsetColumnHeaders(registeredDatatableName); - + final List columnHeaderData = this.genericDataService + .fillResultsetColumnHeaders(registeredDatatableName); + datatableData = DatatableData.create(appTableName, registeredDatatableName, columnHeaderData); } @@ -156,7 +161,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ final String registerDatatableSql = "insert into x_registered_table (registered_table_name, application_table_name) values ('" + dataTableName + "', '" + applicationTableName + "')"; - + final String createPermission = "'CREATE_" + dataTableName + "'"; final String createPermissionChecker = "'CREATE_" + dataTableName + "_CHECKER'"; final String readPermission = "'READ_" + dataTableName + "'"; @@ -201,15 +206,14 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ + ", 'DELETE', '" + dataTableName + "', false)"; - - try { - String[] sqlArray = {registerDatatableSql, permissionsSql}; + String[] sqlArray = { registerDatatableSql, permissionsSql }; this.jdbcTemplate.batchUpdate(sqlArray); - + } catch (DataIntegrityViolationException dve) { Throwable realCause = dve.getMostSpecificCause(); - //even if duplicate is only due to permission duplicate, okay to show duplicate datatable error msg + // even if duplicate is only due to permission duplicate, okay to + // show duplicate datatable error msg if (realCause.getMessage().contains("Duplicate entry")) { throw new PlatformDataIntegrityException( "error.msg.datatable.registered", "Datatable `" + dataTableName + "` is already registered against an application table.", "dataTableName", dataTableName); } @@ -232,9 +236,9 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ final String deletePermissionsSql = "delete from m_permission where code in " + permissionList; - final String deleteRegisteredDatatableSql = "delete from x_registered_table where registered_table_name = '" + datatable + "'"; + final String deleteRegisteredDatatableSql = "delete from x_registered_table where registered_table_name = '" + datatable + "'"; - String[] sqlArray = {deleteRolePermissionsSql, deletePermissionsSql, deleteRegisteredDatatableSql}; + String[] sqlArray = { deleteRolePermissionsSql, deletePermissionsSql, deleteRegisteredDatatableSql }; this.jdbcTemplate.batchUpdate(sqlArray); } @@ -254,9 +258,9 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ final String sql = getAddSql(columnHeaders, dataTableName, getFKField(appTable), appTableId, dataParams); this.jdbcTemplate.update(sql); - + return commandProcessingResult; // - + } catch (ConstraintViolationException dve) { // NOTE: jdbctemplate throws a // org.hibernate.exception.ConstraintViolationException even though @@ -282,257 +286,238 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ } private Boolean datatableExists(final String datatable) { - - String sql = "SELECT COUNT(*) FROM `x_registered_table` WHERE `registered_table_name`='" + datatable + "'"; - Integer rowCount = this.jdbcTemplate.queryForInt(sql); - - return (rowCount > 0); + + String sql = "SELECT COUNT(*) FROM `x_registered_table` WHERE `registered_table_name`='" + datatable + "'"; + Integer rowCount = this.jdbcTemplate.queryForInt(sql); + + return (rowCount > 0); } private void validateDatatableExists(final String name) { - - if (!datatableExists(name)) { - throw new PlatformDataIntegrityException("error.msg.datatables.missing.table.name", - "Table '" + name + "' does not exist.", name); - } + + if (!datatableExists(name)) { throw new PlatformDataIntegrityException("error.msg.datatables.missing.table.name", "Table '" + name + + "' does not exist.", name); } } private void validateDatatableNotExists(final String name) { - - if (datatableExists(name)) { - throw new PlatformDataIntegrityException("error.msg.datatables.duplicate.table.name", - "Table '" + name + "' already exists.", name); - } + + if (datatableExists(name)) { throw new PlatformDataIntegrityException("error.msg.datatables.duplicate.table.name", "Table '" + name + + "' already exists.", name); } } private void validateDatatableName(final String name) { - - if (name == null || name.isEmpty()) { - throw new PlatformDataIntegrityException("error.msg.datatables.datatable.null.name", - "Data table name must not be blank."); - } else if (!name.matches(DATATABLE_NAME_REGEX_PATTERN)) { - throw new PlatformDataIntegrityException("error.msg.datatables.datatable.invalid.name.regex", - "Invalid data table name.", name); - } + + if (name == null || name.isEmpty()) { + throw new PlatformDataIntegrityException("error.msg.datatables.datatable.null.name", "Data table name must not be blank."); + } else if (!name.matches(DATATABLE_NAME_REGEX_PATTERN)) { throw new PlatformDataIntegrityException( + "error.msg.datatables.datatable.invalid.name.regex", "Invalid data table name.", name); } } private void parseDatatableColumnObjectForCreate(final JsonObject column, StringBuilder sqlBuilder) { - - String name = (column.has("name")) ? column.get("name").getAsString() : null; - String type = (column.has("type")) ? column.get("type").getAsString() : null; - Integer length = (column.has("length")) ? column.get("length").getAsInt() : null; - Boolean mandatory = (column.has("mandatory")) ? column.get("mandatory").getAsBoolean() : false; - - String mysqlType = apiTypeToMySQL.get(type); - sqlBuilder = sqlBuilder.append("`" + name + "` " + mysqlType); - - if (type != null) - { - if (type.equals("String")) { - sqlBuilder = sqlBuilder.append("(" + length + ")"); - } else if (type.equals("Decimal")) { - sqlBuilder = sqlBuilder.append("(19,6)"); - } - } - if (mandatory != null) { - if (mandatory) { - sqlBuilder = sqlBuilder.append(" NOT NULL"); - } else { - sqlBuilder = sqlBuilder.append(" DEFAULT NULL"); - } - } - - sqlBuilder = sqlBuilder.append(", "); + + String name = (column.has("name")) ? column.get("name").getAsString() : null; + String type = (column.has("type")) ? column.get("type").getAsString() : null; + Integer length = (column.has("length")) ? column.get("length").getAsInt() : null; + Boolean mandatory = (column.has("mandatory")) ? column.get("mandatory").getAsBoolean() : false; + + String mysqlType = apiTypeToMySQL.get(type); + sqlBuilder = sqlBuilder.append("`" + name + "` " + mysqlType); + + if (type != null) { + if (type.equals("String")) { + sqlBuilder = sqlBuilder.append("(" + length + ")"); + } else if (type.equals("Decimal")) { + sqlBuilder = sqlBuilder.append("(19,6)"); + } + } + if (mandatory != null) { + if (mandatory) { + sqlBuilder = sqlBuilder.append(" NOT NULL"); + } else { + sqlBuilder = sqlBuilder.append(" DEFAULT NULL"); + } + } + + sqlBuilder = sqlBuilder.append(", "); } @Transactional @Override public CommandProcessingResult createDatatable(final JsonCommand command) { - context.authenticatedUser(); - this.fromApiJsonDeserializer.validateForCreate(command.json()); + context.authenticatedUser(); + this.fromApiJsonDeserializer.validateForCreate(command.json()); - JsonElement element = this.fromJsonHelper.parse(command.json()); - JsonArray columns = this.fromJsonHelper.extractJsonArrayNamed("columns", element); - String datatableName = this.fromJsonHelper.extractStringNamed("datatableName", element); - String apptable = this.fromJsonHelper.extractStringNamed("apptableName", element); - Boolean multiRow = this.fromJsonHelper.extractBooleanNamed("multiRow", element); + JsonElement element = this.fromJsonHelper.parse(command.json()); + JsonArray columns = this.fromJsonHelper.extractJsonArrayNamed("columns", element); + String datatableName = this.fromJsonHelper.extractStringNamed("datatableName", element); + String apptable = this.fromJsonHelper.extractStringNamed("apptableName", element); + Boolean multiRow = this.fromJsonHelper.extractBooleanNamed("multiRow", element); - if (multiRow == null) { - multiRow = false; - } + if (multiRow == null) { + multiRow = false; + } - validateDatatableName(datatableName); - validateDatatableName(apptable); - validateDatatableNotExists(datatableName); + validateDatatableName(datatableName); + validateDatatableName(apptable); + validateDatatableNotExists(datatableName); - String fkColumnName = apptable.substring(2) + "_id"; - String fkName = datatableName.toLowerCase().replaceAll("\\s", "_"); - StringBuilder sqlBuilder = new StringBuilder(); - sqlBuilder = sqlBuilder.append("CREATE TABLE `" + datatableName + "` ("); + String fkColumnName = apptable.substring(2) + "_id"; + String fkName = datatableName.toLowerCase().replaceAll("\\s", "_"); + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder = sqlBuilder.append("CREATE TABLE `" + datatableName + "` ("); - if (multiRow) { - sqlBuilder = sqlBuilder - .append("`id` BIGINT(20) NOT NULL AUTO_INCREMENT, ") - .append("`" + fkColumnName + "` BIGINT(20) NOT NULL, "); - } else { - sqlBuilder = sqlBuilder - .append("`" + fkColumnName + "` BIGINT(20) NOT NULL, "); - } + if (multiRow) { + sqlBuilder = sqlBuilder.append("`id` BIGINT(20) NOT NULL AUTO_INCREMENT, ").append( + "`" + fkColumnName + "` BIGINT(20) NOT NULL, "); + } else { + sqlBuilder = sqlBuilder.append("`" + fkColumnName + "` BIGINT(20) NOT NULL, "); + } - for (JsonElement column : columns) { - parseDatatableColumnObjectForCreate(column.getAsJsonObject(), sqlBuilder); - } + for (JsonElement column : columns) { + parseDatatableColumnObjectForCreate(column.getAsJsonObject(), sqlBuilder); + } - // Remove trailing comma and space - sqlBuilder = sqlBuilder.delete(sqlBuilder.length() - 2, sqlBuilder.length()); + // Remove trailing comma and space + sqlBuilder = sqlBuilder.delete(sqlBuilder.length() - 2, sqlBuilder.length()); - if (multiRow) { - sqlBuilder = sqlBuilder - .append(", PRIMARY KEY (`id`)") - .append(", KEY `fk_" + apptable + "` (`" + fkColumnName + "`)") - .append(", CONSTRAINT `fk_" + fkName + "` ") - .append("FOREIGN KEY (`" + fkColumnName + "`) ") - .append("REFERENCES `" + apptable + "` (`id`)"); - - } else { - sqlBuilder = sqlBuilder - .append(", PRIMARY KEY (`" + fkColumnName + "`)") - .append(", CONSTRAINT `fk_" + fkName + "` ") - .append("FOREIGN KEY (`" + fkColumnName + "`) ") - .append("REFERENCES `" + apptable + "` (`id`)"); - } + if (multiRow) { + sqlBuilder = sqlBuilder.append(", PRIMARY KEY (`id`)").append(", KEY `fk_" + apptable + "` (`" + fkColumnName + "`)") + .append(", CONSTRAINT `fk_" + fkName + "` ").append("FOREIGN KEY (`" + fkColumnName + "`) ") + .append("REFERENCES `" + apptable + "` (`id`)"); - sqlBuilder = sqlBuilder.append(") ENGINE=InnoDB DEFAULT CHARSET=utf8;"); - this.jdbcTemplate.execute(sqlBuilder.toString()); + } else { + sqlBuilder = sqlBuilder.append(", PRIMARY KEY (`" + fkColumnName + "`)").append(", CONSTRAINT `fk_" + fkName + "` ") + .append("FOREIGN KEY (`" + fkColumnName + "`) ").append("REFERENCES `" + apptable + "` (`id`)"); + } - registerDatatable(datatableName, apptable); + sqlBuilder = sqlBuilder.append(") ENGINE=InnoDB DEFAULT CHARSET=utf8;"); + this.jdbcTemplate.execute(sqlBuilder.toString()); - return new CommandProcessingResultBuilder() - .withCommandId(command.commandId()) - .withResourceIdAsString(datatableName) - .build(); + registerDatatable(datatableName, apptable); + + return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withResourceIdAsString(datatableName).build(); } private void parseDatatableColumnForUpdate(final JsonObject column, StringBuilder sqlBuilder) { - - String name = (column.has("name")) ? column.get("name").getAsString() : null; - String newName = (column.has("newName")) ? column.get("newName").getAsString() : name; - String type = (column.has("type")) ? column.get("type").getAsString() : null; - Integer length = (column.has("length")) ? column.get("length").getAsInt() : null; - Boolean mandatory = (column.has("mandatory")) ? column.get("mandatory").getAsBoolean() : false; - String after = (column.has("after")) ? column.get("after").getAsString() : null; - - String mysqlType = apiTypeToMySQL.get(type); - sqlBuilder = sqlBuilder.append(", CHANGE `" + name + "` `" + newName + "` " + mysqlType); - - if (type != null) { - if (type.equals("String") && length != null) { - sqlBuilder = sqlBuilder.append("(" + length + ")"); - } else if (type.equals("Decimal")) { - sqlBuilder = sqlBuilder.append("(19,6)"); - } - } - if (mandatory != null) { - if (mandatory) { - sqlBuilder = sqlBuilder.append(" NOT NULL"); - } else { - sqlBuilder = sqlBuilder.append(" DEFAULT NULL"); - } - } - if (after != null) { - sqlBuilder = sqlBuilder.append(" AFTER `" + after + "`"); - } + + String name = (column.has("name")) ? column.get("name").getAsString() : null; + String newName = (column.has("newName")) ? column.get("newName").getAsString() : name; + String type = (column.has("type")) ? column.get("type").getAsString() : null; + Integer length = (column.has("length")) ? column.get("length").getAsInt() : null; + Boolean mandatory = (column.has("mandatory")) ? column.get("mandatory").getAsBoolean() : false; + String after = (column.has("after")) ? column.get("after").getAsString() : null; + + String mysqlType = apiTypeToMySQL.get(type); + sqlBuilder = sqlBuilder.append(", CHANGE `" + name + "` `" + newName + "` " + mysqlType); + + if (type != null) { + if (type.equals("String") && length != null) { + sqlBuilder = sqlBuilder.append("(" + length + ")"); + } else if (type.equals("Decimal")) { + sqlBuilder = sqlBuilder.append("(19,6)"); + } + } + if (mandatory != null) { + if (mandatory) { + sqlBuilder = sqlBuilder.append(" NOT NULL"); + } else { + sqlBuilder = sqlBuilder.append(" DEFAULT NULL"); + } + } + if (after != null) { + sqlBuilder = sqlBuilder.append(" AFTER `" + after + "`"); + } } private void parseDatatableColumnForAdd(final JsonObject column, StringBuilder sqlBuilder) { - - String name = (column.has("name")) ? column.get("name").getAsString() : null; - String type = (column.has("type")) ? column.get("type").getAsString() : null; - Integer length = (column.has("length")) ? column.get("length").getAsInt() : null; - Boolean mandatory = (column.has("mandatory")) ? column.get("mandatory").getAsBoolean() : false; - String after = (column.has("after")) ? column.get("after").getAsString() : null; - - String mysqlType = apiTypeToMySQL.get(type); - sqlBuilder = sqlBuilder.append(", ADD `" + name + "` " + mysqlType); - - if (type != null) { - if (type.equals("String") && length != null) { - sqlBuilder = sqlBuilder.append("(" + length + ")"); - } else if (type.equals("Decimal")) { - sqlBuilder = sqlBuilder.append("(19,6)"); - } - } - if (mandatory != null) { - if (mandatory) { - sqlBuilder = sqlBuilder.append(" NOT NULL"); - } else { - sqlBuilder = sqlBuilder.append(" DEFAULT NULL"); - } - } - if (after != null) { - sqlBuilder = sqlBuilder.append(" AFTER `" + after + "`"); - } + + String name = (column.has("name")) ? column.get("name").getAsString() : null; + String type = (column.has("type")) ? column.get("type").getAsString() : null; + Integer length = (column.has("length")) ? column.get("length").getAsInt() : null; + Boolean mandatory = (column.has("mandatory")) ? column.get("mandatory").getAsBoolean() : false; + String after = (column.has("after")) ? column.get("after").getAsString() : null; + + String mysqlType = apiTypeToMySQL.get(type); + sqlBuilder = sqlBuilder.append(", ADD `" + name + "` " + mysqlType); + + if (type != null) { + if (type.equals("String") && length != null) { + sqlBuilder = sqlBuilder.append("(" + length + ")"); + } else if (type.equals("Decimal")) { + sqlBuilder = sqlBuilder.append("(19,6)"); + } + } + if (mandatory != null) { + if (mandatory) { + sqlBuilder = sqlBuilder.append(" NOT NULL"); + } else { + sqlBuilder = sqlBuilder.append(" DEFAULT NULL"); + } + } + if (after != null) { + sqlBuilder = sqlBuilder.append(" AFTER `" + after + "`"); + } } - private void parseDatatableColumnForDrop(final JsonObject column, StringBuilder sqlBuilder) { - - String name = (column.has("name")) ? column.get("name").getAsString() : null; - - sqlBuilder = sqlBuilder.append(", DROP COLUMN `" + name + "`"); - } + private void parseDatatableColumnForDrop(final JsonObject column, StringBuilder sqlBuilder) { + + String name = (column.has("name")) ? column.get("name").getAsString() : null; + + sqlBuilder = sqlBuilder.append(", DROP COLUMN `" + name + "`"); + } @Transactional @Override public void updateDatatable(final String datatableName, final JsonCommand command) { - - context.authenticatedUser(); - this.fromApiJsonDeserializer.validateForUpdate(command.json()); - - JsonElement element = this.fromJsonHelper.parse(command.json()); - JsonArray changeColumns = this.fromJsonHelper.extractJsonArrayNamed("changeColumns", element); - JsonArray addColumns = this.fromJsonHelper.extractJsonArrayNamed("addColumns", element); - JsonArray dropColumns = this.fromJsonHelper.extractJsonArrayNamed("dropColumns", element); - - validateDatatableName(datatableName); - validateDatatableExists(datatableName); - - StringBuilder sqlBuilder = new StringBuilder("ALTER TABLE `" + datatableName + "`"); - - for (JsonElement column : changeColumns) { - parseDatatableColumnForUpdate(column.getAsJsonObject(), sqlBuilder); - } - for (JsonElement column : addColumns) { - parseDatatableColumnForAdd(column.getAsJsonObject(), sqlBuilder); - } - for (JsonElement column : dropColumns) { - parseDatatableColumnForDrop(column.getAsJsonObject(), sqlBuilder); - } - - // Remove the first comma, right after ALTER TABLE `datatable` - sqlBuilder = sqlBuilder.deleteCharAt(sqlBuilder.indexOf(",")); - this.jdbcTemplate.execute(sqlBuilder.toString()); + + context.authenticatedUser(); + this.fromApiJsonDeserializer.validateForUpdate(command.json()); + + JsonElement element = this.fromJsonHelper.parse(command.json()); + JsonArray changeColumns = this.fromJsonHelper.extractJsonArrayNamed("changeColumns", element); + JsonArray addColumns = this.fromJsonHelper.extractJsonArrayNamed("addColumns", element); + JsonArray dropColumns = this.fromJsonHelper.extractJsonArrayNamed("dropColumns", element); + + validateDatatableName(datatableName); + validateDatatableExists(datatableName); + + StringBuilder sqlBuilder = new StringBuilder("ALTER TABLE `" + datatableName + "`"); + + for (JsonElement column : changeColumns) { + parseDatatableColumnForUpdate(column.getAsJsonObject(), sqlBuilder); + } + for (JsonElement column : addColumns) { + parseDatatableColumnForAdd(column.getAsJsonObject(), sqlBuilder); + } + for (JsonElement column : dropColumns) { + parseDatatableColumnForDrop(column.getAsJsonObject(), sqlBuilder); + } + + // Remove the first comma, right after ALTER TABLE `datatable` + sqlBuilder = sqlBuilder.deleteCharAt(sqlBuilder.indexOf(",")); + this.jdbcTemplate.execute(sqlBuilder.toString()); } @Transactional @Override public void deleteDatatable(final String datatableName) { - - context.authenticatedUser(); - - validateDatatableName(datatableName); - validateDatatableExists(datatableName); - deregisterDatatable(datatableName); - - String sql = "DROP TABLE `" + datatableName + "`"; - this.jdbcTemplate.execute(sql); + + context.authenticatedUser(); + + validateDatatableName(datatableName); + validateDatatableExists(datatableName); + deregisterDatatable(datatableName); + + String sql = "DROP TABLE `" + datatableName + "`"; + this.jdbcTemplate.execute(sql); } @Transactional @Override public CommandProcessingResult updateDatatableEntryOneToOne(final String dataTableName, final Long appTableId, final JsonCommand command) { - return updateDatatableEntry(dataTableName, appTableId, null, command); + return updateDatatableEntry(dataTableName, appTableId, null, command); } @Transactional @@ -540,34 +525,37 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ public CommandProcessingResult updateDatatableEntryOneToMany(final String dataTableName, final Long appTableId, final Long datatableId, final JsonCommand command) { - return updateDatatableEntry(dataTableName, appTableId, datatableId, command); + return updateDatatableEntry(dataTableName, appTableId, datatableId, command); } - private CommandProcessingResult updateDatatableEntry(final String dataTableName, final Long appTableId, final Long datatableId, final JsonCommand command) { - + final String appTable = queryForApplicationTableName(dataTableName); CommandProcessingResult commandProcessingResult = checkMainResourceExistsWithinScope(appTable, appTableId); - + final GenericResultsetData grs = retrieveDataTableGenericResultSetForUpdate(appTable, dataTableName, appTableId, datatableId); - + if (grs.hasNoEntries()) { throw new DatatableNotFoundException(dataTableName, appTableId); } - + if (grs.hasMoreThanOneEntry()) { throw new PlatformDataIntegrityException("error.msg.attempting.multiple.update", "Application table: " + dataTableName + " Foreign key id: " + appTableId); } - + final Type typeOfMap = new TypeToken>() {}.getType(); Map dataParams = this.fromJsonHelper.extractDataMap(typeOfMap, command.json()); - String pkName = "id"; //1:M datatable - if (datatableId == null) { pkName = getFKField(appTable);} //1:1 datatable - + String pkName = "id"; // 1:M datatable + if (datatableId == null) { + pkName = getFKField(appTable); + } // 1:1 datatable + final Map changes = getAffectedAndChangedColumns(grs, dataParams, pkName); if (!changes.isEmpty()) { - Long pkValue = appTableId; - if (datatableId != null) { pkValue = datatableId;} + Long pkValue = appTableId; + if (datatableId != null) { + pkValue = datatableId; + } final String sql = getUpdateSql(dataTableName, pkName, pkValue, changes); logger.info("Update sql: " + sql); if (StringUtils.isNotBlank(sql)) { @@ -575,20 +563,19 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ changes.put("locale", dataParams.get("locale")); changes.put("dateFormat", "yyyy-MM-dd"); } else { - logger.info("No Changes"); - } + logger.info("No Changes"); + } } return new CommandProcessingResultBuilder() // - .withOfficeId(commandProcessingResult.getOfficeId()) // - .withGroupId(commandProcessingResult.getGroupId()) // - .withClientId(commandProcessingResult.getClientId()) // - .withSavingsId(commandProcessingResult.getSavingsId()) // - .withLoanId(commandProcessingResult.getLoanId()) // - .with(changes) // - .build(); + .withOfficeId(commandProcessingResult.getOfficeId()) // + .withGroupId(commandProcessingResult.getGroupId()) // + .withClientId(commandProcessingResult.getClientId()) // + .withSavingsId(commandProcessingResult.getSavingsId()) // + .withLoanId(commandProcessingResult.getLoanId()) // + .with(changes) // + .build(); } - @Transactional @Override @@ -596,13 +583,12 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ final String appTable = queryForApplicationTableName(dataTableName); CommandProcessingResult commandProcessingResult = checkMainResourceExistsWithinScope(appTable, appTableId); - final String deleteOneToOneEntrySql = getDeleteEntriesSql(dataTableName, getFKField(appTable), appTableId); int rowsDeleted = this.jdbcTemplate.update(deleteOneToOneEntrySql); if (rowsDeleted < 1) { throw new DatatableNotFoundException(dataTableName, appTableId); } - + return commandProcessingResult; } @@ -646,10 +632,9 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ return new GenericResultsetData(columnHeaders, result); } - - private GenericResultsetData retrieveDataTableGenericResultSetForUpdate(final String appTable, String dataTableName, final Long appTableId, - final Long id) { + private GenericResultsetData retrieveDataTableGenericResultSetForUpdate(final String appTable, final String dataTableName, + final Long appTableId, final Long id) { final List columnHeaders = this.genericDataService.fillResultsetColumnHeaders(dataTableName); @@ -671,9 +656,9 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ private CommandProcessingResult checkMainResourceExistsWithinScope(final String appTable, final Long appTableId) { final String sql = dataScopedSQL(appTable, appTableId); -logger.info("data scoped sql: " + sql); + logger.info("data scoped sql: " + sql); final SqlRowSet rs = this.jdbcTemplate.queryForRowSet(sql); - + if (!rs.next()) { throw new DatatableNotFoundException(appTable, appTableId); } Long officeId = getLongSqlRowSet(rs, "officeId"); @@ -681,90 +666,86 @@ logger.info("data scoped sql: " + sql); Long clientId = getLongSqlRowSet(rs, "clientId"); Long savingsId = getLongSqlRowSet(rs, "savingsId"); Long LoanId = getLongSqlRowSet(rs, "loanId"); - + if (rs.next()) { throw new DatatableSystemErrorException("System Error: More than one row returned from data scoping query"); } - + return new CommandProcessingResultBuilder() // - .withOfficeId(officeId) // - .withGroupId(groupId) // - .withClientId(clientId) // - .withSavingsId(savingsId) // - .withLoanId(LoanId) // - .build(); + .withOfficeId(officeId) // + .withGroupId(groupId) // + .withClientId(clientId) // + .withSavingsId(savingsId) // + .withLoanId(LoanId) // + .build(); } - - private Long getLongSqlRowSet(SqlRowSet rs, String column) { + + private Long getLongSqlRowSet(final SqlRowSet rs, final String column) { Long val = rs.getLong(column); if (val == 0) val = null; - return val; + return val; } - private String dataScopedSQL(final String appTable, final Long appTableId) { /* * unfortunately have to, one way or another, be able to restrict data - * to the users office hierarchy. Here, a few key tables are done. - * But if additional fields are needed on other tables the - * same pattern applies + * to the users office hierarchy. Here, a few key tables are done. But + * if additional fields are needed on other tables the same pattern + * applies */ AppUser currentUser = context.authenticatedUser(); String scopedSQL = null; /* - * m_loan and m_savings_account are connected to an m_office thru either an m_client or an m_group - * If both it means it relates to an m_client that is in a group (still an m_client account) + * m_loan and m_savings_account are connected to an m_office thru either + * an m_client or an m_group If both it means it relates to an m_client + * that is in a group (still an m_client account) */ if (appTable.equalsIgnoreCase("m_loan")) { - scopedSQL = "select distinctrow x.* from (" + - " (select o.id as officeId, l.group_id as groupId, l.client_id as clientId, null as savingsId, l.id as loanId from m_loan l " + - " join m_client c on c.id = l.client_id " + - " join m_office o on o.id = c.office_id and o.hierarchy like '" + - currentUser.getOffice().getHierarchy() + "%'" + - " where l.id = " + appTableId + ")" + - " union all " + - " (select o.id as officeId, l.group_id as groupId, l.client_id as clientId, null as savingsId, l.id as loanId from m_loan l " + - " join m_group g on g.id = l.group_id " + - " join m_office o on o.id = g.office_id and o.hierarchy like '" + - currentUser.getOffice().getHierarchy() + "%'" + - " where l.id = " + appTableId + ")" + - " ) x"; + scopedSQL = "select distinctrow x.* from (" + + " (select o.id as officeId, l.group_id as groupId, l.client_id as clientId, null as savingsId, l.id as loanId from m_loan l " + + " join m_client c on c.id = l.client_id " + + " join m_office o on o.id = c.office_id and o.hierarchy like '" + + currentUser.getOffice().getHierarchy() + + "%'" + + " where l.id = " + + appTableId + + ")" + + " union all " + + " (select o.id as officeId, l.group_id as groupId, l.client_id as clientId, null as savingsId, l.id as loanId from m_loan l " + + " join m_group g on g.id = l.group_id " + " join m_office o on o.id = g.office_id and o.hierarchy like '" + + currentUser.getOffice().getHierarchy() + "%'" + " where l.id = " + appTableId + ")" + " ) x"; } if (appTable.equalsIgnoreCase("m_savings_account")) { - scopedSQL = "select distinctrow x.* from (" + - " (select o.id as officeId, s.group_id as groupId, s.client_id as clientId, s.id as savingsId, null as loanId from m_savings_account s " + - " join m_client c on c.id = s.client_id " + - " join m_office o on o.id = c.office_id and o.hierarchy like '" + - currentUser.getOffice().getHierarchy() + "%'" + - " where s.id = " + appTableId+ ")" + - " union all " + - " (select o.id as officeId, s.group_id as groupId, s.client_id as clientId, s.id as savingsId, null as loanId from m_savings_account s " + - " join m_group g on g.id = s.group_id " + - " join m_office o on o.id = g.office_id and o.hierarchy like '" + - currentUser.getOffice().getHierarchy() + "%'" + - " where s.id = " + appTableId+ ")" + - " ) x"; + scopedSQL = "select distinctrow x.* from (" + + " (select o.id as officeId, s.group_id as groupId, s.client_id as clientId, s.id as savingsId, null as loanId from m_savings_account s " + + " join m_client c on c.id = s.client_id " + + " join m_office o on o.id = c.office_id and o.hierarchy like '" + + currentUser.getOffice().getHierarchy() + + "%'" + + " where s.id = " + + appTableId + + ")" + + " union all " + + " (select o.id as officeId, s.group_id as groupId, s.client_id as clientId, s.id as savingsId, null as loanId from m_savings_account s " + + " join m_group g on g.id = s.group_id " + " join m_office o on o.id = g.office_id and o.hierarchy like '" + + currentUser.getOffice().getHierarchy() + "%'" + " where s.id = " + appTableId + ")" + " ) x"; } if (appTable.equalsIgnoreCase("m_client")) { - scopedSQL = "select o.id as officeId, null as groupId, c.id as clientId, null as savingsId, null as loanId from m_client c " + - " join m_office o on o.id = c.office_id and o.hierarchy like '" + - currentUser.getOffice().getHierarchy() + "%'" + - " where c.id = " + appTableId; + scopedSQL = "select o.id as officeId, null as groupId, c.id as clientId, null as savingsId, null as loanId from m_client c " + + " join m_office o on o.id = c.office_id and o.hierarchy like '" + currentUser.getOffice().getHierarchy() + "%'" + + " where c.id = " + appTableId; + } + if (appTable.equalsIgnoreCase("m_group")) { + scopedSQL = "select o.id as officeId, g.id as groupId, null as clientId, null as savingsId, null as loanId from m_group g " + + " join m_office o on o.id = g.office_id and o.hierarchy like '" + currentUser.getOffice().getHierarchy() + "%'" + + " where g.id = " + appTableId; } - if (appTable.equalsIgnoreCase("m_group")) { - scopedSQL = "select o.id as officeId, g.id as groupId, null as clientId, null as savingsId, null as loanId from m_group g " + - " join m_office o on o.id = g.office_id and o.hierarchy like '" + - currentUser.getOffice().getHierarchy() + "%'" + - " where g.id = " + appTableId; - } if (appTable.equalsIgnoreCase("m_office")) { - scopedSQL = "select o.id as officeId, null as groupId, null as clientId, null as savingsId, null as loanId from m_office o " + - " where o.hierarchy like '" + - currentUser.getOffice().getHierarchy() + "%'" + - " and o.id = " + appTableId; - } + scopedSQL = "select o.id as officeId, null as groupId, null as clientId, null as savingsId, null as loanId from m_office o " + + " where o.hierarchy like '" + currentUser.getOffice().getHierarchy() + "%'" + " and o.id = " + appTableId; + } - if (scopedSQL == null) { throw new PlatformDataIntegrityException("error.msg.invalid.dataScopeCriteria", - "Application Table: " + appTable + " not catered for in data Scoping"); } + if (scopedSQL == null) { throw new PlatformDataIntegrityException("error.msg.invalid.dataScopeCriteria", "Application Table: " + + appTable + " not catered for in data Scoping"); } return scopedSQL; @@ -909,7 +890,7 @@ logger.info("data scoped sql: " + sql); affectedAndChangedColumns.put(key, columnValue); } } - + return affectedAndChangedColumns; } @@ -923,9 +904,7 @@ logger.info("data scoped sql: " + sql); if (key.equals(grs.getColumnHeaders().get(i).getColumnName())) { columnValue = columnValues.get(i); - if (notTheSame(columnValue, keyValue, colType)) { - return true; - } + if (notTheSame(columnValue, keyValue, colType)) { return true; } return false; } } diff --git a/mifosng-provider/src/main/resources/META-INF/spring/scheduler.xml b/mifosng-provider/src/main/resources/META-INF/spring/scheduler.xml index a8af89c57..5afe7d5ff 100644 --- a/mifosng-provider/src/main/resources/META-INF/spring/scheduler.xml +++ b/mifosng-provider/src/main/resources/META-INF/spring/scheduler.xml @@ -1,7 +1,9 @@ - + From 42a2428774c44556098f185750e03ab46ebe4c9c Mon Sep 17 00:00:00 2001 From: Keith Woodlock Date: Wed, 29 May 2013 00:11:47 +0100 Subject: [PATCH 05/49] MIFOSX-395: initial database changes along with authentication api changes to support organisational roles, with optional ability to link application users with staff members to support role based screen views. --- .../V47__staff-hierarchy-link-to-users.sql | 14 ++++++ .../api/AuthenticationApiResource.java | 9 +++- .../security/data/AuthenticatedUserData.java | 16 +++++- .../organisation/staff/domain/Staff.java | 19 ++++++- .../staff/domain/StaffEnumerations.java | 37 ++++++++++++++ .../domain/StaffOrganisationalRoleType.java | 50 +++++++++++++++++++ .../useradministration/domain/AppUser.java | 16 +++++- 7 files changed, 155 insertions(+), 6 deletions(-) create mode 100644 mifosng-db/migrations/core_db/V47__staff-hierarchy-link-to-users.sql create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/organisation/staff/domain/StaffEnumerations.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/organisation/staff/domain/StaffOrganisationalRoleType.java diff --git a/mifosng-db/migrations/core_db/V47__staff-hierarchy-link-to-users.sql b/mifosng-db/migrations/core_db/V47__staff-hierarchy-link-to-users.sql new file mode 100644 index 000000000..1531b04d1 --- /dev/null +++ b/mifosng-db/migrations/core_db/V47__staff-hierarchy-link-to-users.sql @@ -0,0 +1,14 @@ +ALTER TABLE `m_appuser` +ADD COLUMN `staff_id` BIGINT(20) NULL DEFAULT NULL AFTER `office_id` ; + +ALTER TABLE m_appuser +ADD CONSTRAINT `fk_m_appuser_002` +FOREIGN KEY (`staff_id`) +REFERENCES m_staff (`id`) +ON DELETE NO ACTION +ON UPDATE NO ACTION +,ADD INDEX `fk_m_appuser_002x` (`staff_id` ASC); + +ALTER TABLE `m_staff` +ADD COLUMN `organisational_role_enum` SMALLINT NULL DEFAULT NULL AFTER `external_id`, +ADD COLUMN `organisational_role_parent_staff_id` BIGINT(20) NULL DEFAULT NULL AFTER `organisational_role_enum`; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/api/AuthenticationApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/api/AuthenticationApiResource.java index 0edea3483..6d985da05 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/api/AuthenticationApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/api/AuthenticationApiResource.java @@ -16,6 +16,7 @@ import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; +import org.mifosplatform.infrastructure.core.data.EnumOptionData; import org.mifosplatform.infrastructure.core.serialization.ToApiJsonSerializer; import org.mifosplatform.infrastructure.security.data.AuthenticatedUserData; import org.mifosplatform.useradministration.data.RoleData; @@ -73,8 +74,12 @@ public class AuthenticationApiResource { roles.add(role.toData()); } - authenticatedUserData = new AuthenticatedUserData(username, roles, permissions, principal.getId(), new String( - base64EncodedAuthenticationKey)); + final Long officeId = principal.getOffice().getId(); + final String officeName = principal.getOffice().getName(); + final EnumOptionData organisationalRole = principal.organisationalRoleData(); + + authenticatedUserData = new AuthenticatedUserData(username, officeId, officeName, organisationalRole, roles, permissions, + principal.getId(), new String(base64EncodedAuthenticationKey)); } return this.apiJsonSerializerService.serialize(authenticatedUserData); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/data/AuthenticatedUserData.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/data/AuthenticatedUserData.java index d60f0b883..a87920111 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/data/AuthenticatedUserData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/data/AuthenticatedUserData.java @@ -7,6 +7,7 @@ package org.mifosplatform.infrastructure.security.data; import java.util.Collection; +import org.mifosplatform.infrastructure.core.data.EnumOptionData; import org.mifosplatform.useradministration.data.RoleData; /** @@ -23,6 +24,12 @@ public class AuthenticatedUserData { @SuppressWarnings("unused") private final boolean authenticated; @SuppressWarnings("unused") + private final Long officeId; + @SuppressWarnings("unused") + private final String officeName; + @SuppressWarnings("unused") + private final EnumOptionData organisationalRole; + @SuppressWarnings("unused") private final Collection roles; @SuppressWarnings("unused") private final Collection permissions; @@ -32,13 +39,20 @@ public class AuthenticatedUserData { this.userId = null; this.base64EncodedAuthenticationKey = null; this.authenticated = false; + this.officeId = null; + this.officeName = null; + this.organisationalRole = null; this.roles = null; this.permissions = permissions; } - public AuthenticatedUserData(final String username, final Collection roles, final Collection permissions, + public AuthenticatedUserData(final String username, final Long officeId, final String officeName, + final EnumOptionData organisationalRole, final Collection roles, final Collection permissions, final Long userId, final String base64EncodedAuthenticationKey) { this.username = username; + this.officeId = officeId; + this.officeName = officeName; + this.organisationalRole = organisationalRole; this.userId = userId; this.base64EncodedAuthenticationKey = base64EncodedAuthenticationKey; this.authenticated = true; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/organisation/staff/domain/Staff.java b/mifosng-provider/src/main/java/org/mifosplatform/organisation/staff/domain/Staff.java index fcefcffe2..79a3d8ddb 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/organisation/staff/domain/Staff.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/organisation/staff/domain/Staff.java @@ -17,6 +17,7 @@ import javax.persistence.UniqueConstraint; import org.apache.commons.lang.StringUtils; import org.mifosplatform.infrastructure.core.api.JsonCommand; +import org.mifosplatform.infrastructure.core.data.EnumOptionData; import org.mifosplatform.organisation.office.domain.Office; import org.springframework.data.jpa.domain.AbstractPersistable; @@ -24,7 +25,6 @@ import org.springframework.data.jpa.domain.AbstractPersistable; @Table(name = "m_staff", uniqueConstraints = { @UniqueConstraint(columnNames = { "display_name" }, name = "display_name") }) public class Staff extends AbstractPersistable { - @Column(name = "firstname", length = 50) private String firstname; @@ -44,6 +44,13 @@ public class Staff extends AbstractPersistable { @Column(name = "is_loan_officer ", nullable = false) private boolean loanOfficer; + @Column(name = "organisational_role_enum", nullable = true) + private Integer organisationalRoleType; + + @ManyToOne + @JoinColumn(name = "organisational_role_parent_staff_id", nullable = true) + private Staff organisationalRoleParentStaff; + public static Staff fromJson(final Office staffOffice, final JsonCommand command) { final String firstnameParamName = "firstname"; @@ -58,7 +65,7 @@ public class Staff extends AbstractPersistable { return new Staff(staffOffice, firstname, lastname, isLoanOfficer); } - public static Staff createNew(Office staffOffice, final String firstname, final String lastname, boolean isLoanOfficer) { + public static Staff createNew(final Office staffOffice, final String firstname, final String lastname, final boolean isLoanOfficer) { return new Staff(staffOffice, firstname, lastname, isLoanOfficer); } @@ -74,6 +81,14 @@ public class Staff extends AbstractPersistable { deriveDisplayName(firstname); } + public EnumOptionData organisationalRoleData() { + EnumOptionData organisationalRole = null; + if (this.organisationalRoleType != null) { + organisationalRole = StaffEnumerations.organisationalRole(this.organisationalRoleType); + } + return organisationalRole; + } + public void changeOffice(final Office newOffice) { this.office = newOffice; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/organisation/staff/domain/StaffEnumerations.java b/mifosng-provider/src/main/java/org/mifosplatform/organisation/staff/domain/StaffEnumerations.java new file mode 100644 index 000000000..f6f58236b --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/organisation/staff/domain/StaffEnumerations.java @@ -0,0 +1,37 @@ +/** + * 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.organisation.staff.domain; + +import org.mifosplatform.infrastructure.core.data.EnumOptionData; + +public class StaffEnumerations { + + public static EnumOptionData organisationalRole(final Integer id) { + return organisationalRole(StaffOrganisationalRoleType.fromInt(id)); + } + + public static EnumOptionData organisationalRole(final StaffOrganisationalRoleType type) { + EnumOptionData optionData = new EnumOptionData(StaffOrganisationalRoleType.INVALID.getValue().longValue(), + StaffOrganisationalRoleType.INVALID.getCode(), "Invalid"); + switch (type) { + case PROGRAM_DIRECTOR: + optionData = new EnumOptionData(type.getValue().longValue(), type.getCode(), "Program Director"); + break; + case BRANCH_MANAGER: + optionData = new EnumOptionData(type.getValue().longValue(), type.getCode(), "Branch Manager"); + break; + case FIELD_OFFICER_COORDINATOR: + optionData = new EnumOptionData(type.getValue().longValue(), type.getCode(), "Field Officer Coordinator"); + break; + case FIELD_OFFICER: + optionData = new EnumOptionData(type.getValue().longValue(), type.getCode(), "Field Officer"); + break; + case INVALID: + break; + } + return optionData; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/organisation/staff/domain/StaffOrganisationalRoleType.java b/mifosng-provider/src/main/java/org/mifosplatform/organisation/staff/domain/StaffOrganisationalRoleType.java new file mode 100644 index 000000000..14382496c --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/organisation/staff/domain/StaffOrganisationalRoleType.java @@ -0,0 +1,50 @@ +/** + * 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.organisation.staff.domain; + +public enum StaffOrganisationalRoleType { + + INVALID(0, "staffOrganisationalRoleType.invalid"), // + PROGRAM_DIRECTOR(100, "staffOrganisationalRoleType.programDirector"), // + BRANCH_MANAGER(200, "staffOrganisationalRoleType.branchManager"), // + FIELD_OFFICER_COORDINATOR(300, "staffOrganisationalRoleType.coordinator"), // + FIELD_OFFICER(400, "staffOrganisationalRoleType.fieldAgent"); + + private final Integer value; + private final String code; + + private StaffOrganisationalRoleType(final Integer value, final String code) { + this.value = value; + this.code = code; + } + + public Integer getValue() { + return value; + } + + public String getCode() { + return code; + } + + public static StaffOrganisationalRoleType fromInt(final Integer chargeCalculation) { + StaffOrganisationalRoleType chargeCalculationType = StaffOrganisationalRoleType.INVALID; + switch (chargeCalculation) { + case 100: + chargeCalculationType = PROGRAM_DIRECTOR; + break; + case 200: + chargeCalculationType = BRANCH_MANAGER; + break; + case 300: + chargeCalculationType = FIELD_OFFICER_COORDINATOR; + break; + case 400: + chargeCalculationType = FIELD_OFFICER; + break; + } + return chargeCalculationType; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/useradministration/domain/AppUser.java b/mifosng-provider/src/main/java/org/mifosplatform/useradministration/domain/AppUser.java index 5644803b5..e5b88b0aa 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/useradministration/domain/AppUser.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/useradministration/domain/AppUser.java @@ -25,10 +25,12 @@ import javax.persistence.UniqueConstraint; import org.apache.commons.lang.StringUtils; import org.mifosplatform.infrastructure.core.api.JsonCommand; +import org.mifosplatform.infrastructure.core.data.EnumOptionData; import org.mifosplatform.infrastructure.security.domain.PlatformUser; import org.mifosplatform.infrastructure.security.exception.NoAuthorizationException; import org.mifosplatform.infrastructure.security.service.PlatformPasswordEncoder; import org.mifosplatform.organisation.office.domain.Office; +import org.mifosplatform.organisation.staff.domain.Staff; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.jpa.domain.AbstractPersistable; @@ -77,9 +79,13 @@ public class AppUser extends AbstractPersistable implements PlatformUser { private boolean deleted; @ManyToOne - @JoinColumn(name = "office_id") + @JoinColumn(name = "office_id", nullable = false) private Office office; + @ManyToOne + @JoinColumn(name = "staff_id", nullable = true) + private Staff staff; + @ManyToMany(fetch = FetchType.EAGER) @JoinTable(name = "m_appuser_role", joinColumns = @JoinColumn(name = "appuser_id"), inverseJoinColumns = @JoinColumn(name = "role_id")) private Set roles; @@ -132,6 +138,14 @@ public class AppUser extends AbstractPersistable implements PlatformUser { this.firstTimeLoginRemaining = true; } + public EnumOptionData organisationalRoleData() { + EnumOptionData organisationalRole = null; + if (this.staff != null) { + organisationalRole = this.staff.organisationalRoleData(); + } + return organisationalRole; + } + public void updatePassword(final String encodePassword) { this.password = encodePassword; this.firstTimeLoginRemaining = false; From 96789be7aded9cd7dcd5a15acabf07e7d0581bc9 Mon Sep 17 00:00:00 2001 From: mifoscontributer Date: Wed, 29 May 2013 12:05:29 +0530 Subject: [PATCH 06/49] Add Predefined Journal Entries with optional tag fields --- .../data/GLAccountDataForLookup.java | 15 ++ .../service/GLAccountReadPlatformService.java | 3 + .../GLAccountReadPlatformServiceImpl.java | 24 ++- .../command/JournalEntryCommand.java | 74 ++++---- .../JournalEntryInvalidException.java | 4 +- ...WritePlatformServiceJpaRepositoryImpl.java | 164 ++++++++++++++---- .../rule/domain/AccountingTagRule.java | 12 +- ...WritePlatformServiceJpaRepositoryImpl.java | 22 ++- 8 files changed, 232 insertions(+), 86 deletions(-) create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/data/GLAccountDataForLookup.java diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/data/GLAccountDataForLookup.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/data/GLAccountDataForLookup.java new file mode 100644 index 000000000..94e0a537d --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/data/GLAccountDataForLookup.java @@ -0,0 +1,15 @@ +package org.mifosplatform.accounting.glaccount.data; + +public class GLAccountDataForLookup { + + private final Long id; + + public GLAccountDataForLookup(final Long id) { + this.id = id; + } + + public Long getId() { + return this.id; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/service/GLAccountReadPlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/service/GLAccountReadPlatformService.java index d982c3153..c03ba7423 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/service/GLAccountReadPlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/service/GLAccountReadPlatformService.java @@ -8,6 +8,7 @@ package org.mifosplatform.accounting.glaccount.service; import java.util.List; import org.mifosplatform.accounting.glaccount.data.GLAccountData; +import org.mifosplatform.accounting.glaccount.data.GLAccountDataForLookup; import org.mifosplatform.accounting.glaccount.domain.GLAccountType; public interface GLAccountReadPlatformService { @@ -24,4 +25,6 @@ public interface GLAccountReadPlatformService { List retrieveAllEnabledHeaderGLAccounts(GLAccountType accountType); GLAccountData retrieveNewGLAccountDetails(final Integer type); + + List retrieveAccountsByTagId(final Long ruleId, final Integer transactionType); } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/service/GLAccountReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/service/GLAccountReadPlatformServiceImpl.java index 1380a34bc..5f85a5c77 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/service/GLAccountReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/service/GLAccountReadPlatformServiceImpl.java @@ -13,6 +13,7 @@ import java.util.List; import org.apache.commons.lang.StringUtils; import org.mifosplatform.accounting.common.AccountingEnumerations; import org.mifosplatform.accounting.glaccount.data.GLAccountData; +import org.mifosplatform.accounting.glaccount.data.GLAccountDataForLookup; import org.mifosplatform.accounting.glaccount.domain.GLAccountType; import org.mifosplatform.accounting.glaccount.domain.GLAccountUsage; import org.mifosplatform.accounting.glaccount.exception.GLAccountInvalidClassificationException; @@ -65,7 +66,7 @@ public class GLAccountReadPlatformServiceImpl implements GLAccountReadPlatformSe final String nameDecorated = rs.getString("nameDecorated"); final Long codeId = rs.wasNull() ? null : rs.getLong("codeId"); final String codeValue = rs.getString("codeValue"); - CodeValueData tagId = CodeValueData.instance(codeId, codeValue); + final CodeValueData tagId = CodeValueData.instance(codeId, codeValue); return new GLAccountData(id, name, parentId, glCode, disabled, manualEntriesAllowed, accountType, usage, description, nameDecorated, tagId); } @@ -202,4 +203,25 @@ public class GLAccountReadPlatformServiceImpl implements GLAccountReadPlatformSe public List retrieveAllEnabledHeaderGLAccounts(final GLAccountType accountType) { return retrieveAllGLAccounts(accountType.getValue(), null, GLAccountUsage.HEADER.getValue(), null, false); } + + @Override + public List retrieveAccountsByTagId(final Long ruleId, final Integer transactionType) { + final GLAccountDataLookUpMapper mapper = new GLAccountDataLookUpMapper(); + final String sql = "Select " + mapper.schema() + " where rule.id=? and tags.acc_type_enum=?"; + return this.jdbcTemplate.query(sql, mapper, new Object[] { ruleId, transactionType }); + } + + private static final class GLAccountDataLookUpMapper implements RowMapper { + + public String schema() { + return " gl.id as id from acc_accounting_rule rule join acc_rule_tags tags on tags.acc_rule_id = rule.id join acc_gl_account gl on gl.tag_id=tags.tag_id"; + } + + @Override + public GLAccountDataForLookup mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + final Long id = JdbcSupport.getLong(rs, "id"); + return new GLAccountDataForLookup(id); + } + + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/command/JournalEntryCommand.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/command/JournalEntryCommand.java index 22e45080b..2c7eab279 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/command/JournalEntryCommand.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/command/JournalEntryCommand.java @@ -30,8 +30,8 @@ public class JournalEntryCommand { private final SingleDebitOrCreditEntryCommand[] debits; public JournalEntryCommand(final Long officeId, final LocalDate transactionDate, final String comments, - final SingleDebitOrCreditEntryCommand[] credits, final SingleDebitOrCreditEntryCommand[] debits, - final String referenceNumber, final Long accountingRuleId, final BigDecimal amount) { + final SingleDebitOrCreditEntryCommand[] credits, final SingleDebitOrCreditEntryCommand[] debits, final String referenceNumber, + final Long accountingRuleId, final BigDecimal amount) { this.officeId = officeId; this.transactionDate = transactionDate; this.comments = comments; @@ -53,46 +53,42 @@ public class JournalEntryCommand { baseDataValidator.reset().parameter("officeId").value(this.officeId).notNull().integerGreaterThanZero(); baseDataValidator.reset().parameter("comments").value(this.comments).ignoreIfNull().notExceedingLengthOf(500); - + baseDataValidator.reset().parameter("referenceNumber").value(this.referenceNumber).ignoreIfNull().notExceedingLengthOf(100); - + baseDataValidator.reset().parameter("accountingRule").value(this.accountingRuleId).ignoreIfNull().longGreaterThanZero(); - - if (this.accountingRuleId == null) { - - baseDataValidator.reset().parameter("credits").value(this.credits).notNull(); - - baseDataValidator.reset().parameter("debits").value(this.debits).notNull(); - - // validation for credit array elements - if (this.credits != null) { - if (this.credits.length == 0) { - validateSingleDebitOrCredit(baseDataValidator, "credits", 0, new SingleDebitOrCreditEntryCommand(null, null, null, null)); - } else { - int i = 0; - for (final SingleDebitOrCreditEntryCommand credit : this.credits) { - validateSingleDebitOrCredit(baseDataValidator, "credits", i, credit); - i++; - } + + baseDataValidator.reset().parameter("credits").value(this.credits).ignoreIfNull(); + + baseDataValidator.reset().parameter("debits").value(this.debits).ignoreIfNull(); + + // validation for credit array elements + if (this.credits != null) { + if (this.credits.length == 0) { + validateSingleDebitOrCredit(baseDataValidator, "credits", 0, new SingleDebitOrCreditEntryCommand(null, null, null, null)); + } else { + int i = 0; + for (final SingleDebitOrCreditEntryCommand credit : this.credits) { + validateSingleDebitOrCredit(baseDataValidator, "credits", i, credit); + i++; } } - - // validation for debit array elements - if (this.debits != null) { - if (this.debits.length == 0) { - validateSingleDebitOrCredit(baseDataValidator, "credits", 0, new SingleDebitOrCreditEntryCommand(null, null, null, null)); - } else { - int i = 0; - for (final SingleDebitOrCreditEntryCommand debit : this.debits) { - validateSingleDebitOrCredit(baseDataValidator, "debits", i, debit); - i++; - } - } - } - } else { - baseDataValidator.reset().parameter("amount").value(this.amount).notNull().zeroOrPositiveAmount(); } + // validation for debit array elements + if (this.debits != null) { + if (this.debits.length == 0) { + validateSingleDebitOrCredit(baseDataValidator, "credits", 0, new SingleDebitOrCreditEntryCommand(null, null, null, null)); + } else { + int i = 0; + for (final SingleDebitOrCreditEntryCommand debit : this.debits) { + validateSingleDebitOrCredit(baseDataValidator, "debits", i, debit); + i++; + } + } + } + baseDataValidator.reset().parameter("amount").value(this.amount).ignoreIfNull().zeroOrPositiveAmount(); + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", dataValidationErrors); } } @@ -134,8 +130,8 @@ public class JournalEntryCommand { return this.referenceNumber; } - public Long getAccountingRuleId() { - return this.accountingRuleId; - } + public Long getAccountingRuleId() { + return this.accountingRuleId; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/exception/JournalEntryInvalidException.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/exception/JournalEntryInvalidException.java index 187a2b395..f3c38462b 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/exception/JournalEntryInvalidException.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/exception/JournalEntryInvalidException.java @@ -16,7 +16,7 @@ public class JournalEntryInvalidException extends AbstractPlatformDomainRuleExce /*** enum of reasons for invalid Journal Entry **/ public static enum GL_JOURNAL_ENTRY_INVALID_REASON { - FUTURE_DATE, ACCOUNTING_CLOSED, NO_DEBITS_OR_CREDITS, DEBIT_CREDIT_SUM_MISMATCH, DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY, GL_ACCOUNT_DISABLED, GL_ACCOUNT_MANUAL_ENTRIES_NOT_PERMITTED; + FUTURE_DATE, ACCOUNTING_CLOSED, NO_DEBITS_OR_CREDITS, DEBIT_CREDIT_SUM_MISMATCH_WITH_AMOUNT, DEBIT_CREDIT_SUM_MISMATCH, DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY, GL_ACCOUNT_DISABLED, GL_ACCOUNT_MANUAL_ENTRIES_NOT_PERMITTED; public String errorMessage() { if (name().toString().equalsIgnoreCase("FUTURE_DATE")) { @@ -25,6 +25,8 @@ public class JournalEntryInvalidException extends AbstractPlatformDomainRuleExce return "Journal entry cannot be made prior to last account closing date for the branch"; } else if (name().toString().equalsIgnoreCase("NO_DEBITS_OR_CREDITS")) { return "Journal Entry must have atleast one Debit and one Credit"; + } else if (name().toString().equalsIgnoreCase("DEBIT_CREDIT_SUM_MISMATCH_WITH_AMOUNT")) { + return "Sum of All Debits OR Credits must equal the Amount for a Journal Entry"; } else if (name().toString().equalsIgnoreCase("DEBIT_CREDIT_SUM_MISMATCH")) { return "Sum of All Debits must equal the sum of all Credits for a Journal Entry"; } else if (name().toString().equalsIgnoreCase("DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY")) { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java index dd3e63383..e246e3471 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java @@ -6,6 +6,7 @@ package org.mifosplatform.accounting.journalentry.service; import java.math.BigDecimal; +import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; @@ -15,9 +16,11 @@ import org.apache.commons.lang.StringUtils; import org.joda.time.LocalDate; import org.mifosplatform.accounting.closure.domain.GLClosure; import org.mifosplatform.accounting.closure.domain.GLClosureRepository; +import org.mifosplatform.accounting.glaccount.data.GLAccountDataForLookup; import org.mifosplatform.accounting.glaccount.domain.GLAccount; import org.mifosplatform.accounting.glaccount.domain.GLAccountRepository; import org.mifosplatform.accounting.glaccount.exception.GLAccountNotFoundException; +import org.mifosplatform.accounting.glaccount.service.GLAccountReadPlatformService; import org.mifosplatform.accounting.journalentry.api.JournalEntryJsonInputParams; import org.mifosplatform.accounting.journalentry.command.JournalEntryCommand; import org.mifosplatform.accounting.journalentry.command.SingleDebitOrCreditEntryCommand; @@ -61,6 +64,7 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa private final AccountingProcessorHelper helper; private final JournalEntryCommandFromApiJsonDeserializer fromApiJsonDeserializer; private final AccountingRuleRepository accountingRuleRepository; + private final GLAccountReadPlatformService glAccountReadPlatformService; @Autowired public JournalEntryWritePlatformServiceJpaRepositoryImpl(final GLClosureRepository glClosureRepository, @@ -68,7 +72,8 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa final GLAccountRepository glAccountRepository, final JournalEntryCommandFromApiJsonDeserializer fromApiJsonDeserializer, final AccountingProcessorHelper accountingProcessorHelper, final AccountingRuleRepository accountingRuleRepository, final AccountingProcessorForLoanFactory accountingProcessorForLoanFactory, - final AccountingProcessorForSavingsFactory accountingProcessorForSavingsFactory) { + final AccountingProcessorForSavingsFactory accountingProcessorForSavingsFactory, + final GLAccountReadPlatformService glAccountReadPlatformService) { this.glClosureRepository = glClosureRepository; this.officeRepository = officeRepository; this.glJournalEntryRepository = glJournalEntryRepository; @@ -78,6 +83,7 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa this.accountingProcessorForSavingsFactory = accountingProcessorForSavingsFactory; this.helper = accountingProcessorHelper; this.accountingRuleRepository = accountingRuleRepository; + this.glAccountReadPlatformService = glAccountReadPlatformService; } @Transactional @@ -99,32 +105,65 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa /** Set a transaction Id and save these Journal entries **/ final Date transactionDate = command.DateValueOfParameterNamed(JournalEntryJsonInputParams.TRANSACTION_DATE.getValue()); final String transactionId = generateTransactionId(); - final String referenceNumber = journalEntryCommand.getReferenceNumber(); + final String referenceNumber = command.stringValueOfParameterNamed(JournalEntryJsonInputParams.REFERENCE_NUMBER.getValue()); final boolean manualEntry = true; if (accountRuleId != null) { - - final Long accountingRuleId = command.longValueOfParameterNamed(JournalEntryJsonInputParams.ACCOUNTING_RULE.getValue()); final BigDecimal amount = command.bigDecimalValueOfParameterNamed(JournalEntryJsonInputParams.AMOUNT.getValue()); - final AccountingRule accountingRule = this.accountingRuleRepository.findOne(accountingRuleId); - if (accountingRule == null) { throw new AccountingRuleNotFoundException(accountingRuleId); } - final GLAccount creditAccountHead = accountingRule.getAccountToCredit(); - validateGLAccountForTransaction(creditAccountHead); - final GLAccount debitAccountHead = accountingRule.getAccountToDebit(); - validateGLAccountForTransaction(debitAccountHead); + String description = null; if (StringUtils.isNotBlank(journalEntryCommand.getComments())) { description = journalEntryCommand.getComments(); } - JournalEntry creditEntry = JournalEntry.createNew(office, creditAccountHead, transactionId, manualEntry, transactionDate, - JournalEntryType.CREDIT, amount, description, null, null, referenceNumber); - this.glJournalEntryRepository.saveAndFlush(creditEntry); + final AccountingRule accountingRule = this.accountingRuleRepository.findOne(accountRuleId); + if (accountingRule == null) { throw new AccountingRuleNotFoundException(accountRuleId); } - JournalEntry debitEntry = JournalEntry.createNew(office, debitAccountHead, transactionId, manualEntry, transactionDate, - JournalEntryType.DEBIT, amount, description, null, null, referenceNumber); - this.glJournalEntryRepository.saveAndFlush(debitEntry); + if (accountingRule.getAccountToCredit() == null) { + if (journalEntryCommand.getCredits() == null) { throw new RuntimeException( + "You have to pass atleast one value for credits"); } + if (journalEntryCommand.getDebits() != null) { + checkDebitOrCreditAccountsAreValid(accountingRule, journalEntryCommand.getCredits(), + journalEntryCommand.getDebits()); + checkDebitAndCreditAmounts(journalEntryCommand.getCredits(), journalEntryCommand.getDebits()); + } else { + checkDebitAndCreditAmounts(journalEntryCommand.getCredits(), amount); + } + saveAllDebitOrCreditEntries(journalEntryCommand, office, transactionDate, journalEntryCommand.getCredits(), + transactionId, JournalEntryType.CREDIT, referenceNumber); + } else { + if (journalEntryCommand.getCredits() != null) { throw new RuntimeException( + "You can not pass credits for this rule please verify your rule"); } + final GLAccount creditAccountHead = accountingRule.getAccountToCredit(); + validateGLAccountForTransaction(creditAccountHead); + final JournalEntry creditEntry = JournalEntry.createNew(office, creditAccountHead, transactionId, manualEntry, + transactionDate, JournalEntryType.CREDIT, amount, description, null, null, referenceNumber); + this.glJournalEntryRepository.saveAndFlush(creditEntry); + } + + if (accountingRule.getAccountToDebit() == null) { + if (journalEntryCommand.getDebits() == null) { throw new RuntimeException( + "You have to pass atleast one parameter for debits"); } + if (journalEntryCommand.getCredits() != null) { + checkDebitOrCreditAccountsAreValid(accountingRule, journalEntryCommand.getCredits(), + journalEntryCommand.getDebits()); + checkDebitAndCreditAmounts(journalEntryCommand.getCredits(), journalEntryCommand.getDebits()); + } else { + checkDebitAndCreditAmounts(journalEntryCommand.getDebits(), amount); + } + + saveAllDebitOrCreditEntries(journalEntryCommand, office, transactionDate, journalEntryCommand.getDebits(), + transactionId, JournalEntryType.DEBIT, referenceNumber); + } else { + if (journalEntryCommand.getDebits() != null) { throw new RuntimeException( + "You can not pass debits for this rule please verify your rule"); } + final GLAccount debitAccountHead = accountingRule.getAccountToDebit(); + validateGLAccountForTransaction(debitAccountHead); + final JournalEntry debitEntry = JournalEntry.createNew(office, debitAccountHead, transactionId, manualEntry, + transactionDate, JournalEntryType.DEBIT, amount, description, null, null, referenceNumber); + this.glJournalEntryRepository.saveAndFlush(debitEntry); + } } else { saveAllDebitOrCreditEntries(journalEntryCommand, office, transactionDate, journalEntryCommand.getDebits(), transactionId, @@ -143,7 +182,76 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa } } - private void validateGLAccountForTransaction(GLAccount creditOrDebitAccountHead) { + @SuppressWarnings("null") + private void checkDebitOrCreditAccountsAreValid(final AccountingRule accountingRule, final SingleDebitOrCreditEntryCommand[] credits, + final SingleDebitOrCreditEntryCommand[] debits) { + // Validate the debit and credit arrays are appropriate accounts + List allowedCreditGLAccounts = new ArrayList(); + List allowedDebitGLAccounts = new ArrayList(); + final SingleDebitOrCreditEntryCommand[] validCredits = new SingleDebitOrCreditEntryCommand[credits.length]; + final SingleDebitOrCreditEntryCommand[] validDebits = new SingleDebitOrCreditEntryCommand[debits.length]; + + if (credits != null && credits.length > 0) { + allowedCreditGLAccounts = this.glAccountReadPlatformService.retrieveAccountsByTagId(accountingRule.getId(), + JournalEntryType.CREDIT.getValue()); + for (final GLAccountDataForLookup accountDataForLookup : allowedCreditGLAccounts) { + for (int i = 0; i < credits.length; i++) { + final SingleDebitOrCreditEntryCommand credit = credits[i]; + if (credit.getGlAccountId().equals(accountDataForLookup.getId())) { + validCredits[i] = credit; + } + } + } + if (credits.length != validCredits.length) { throw new RuntimeException("Invalid credits"); } + } + + if (debits != null && debits.length > 0) { + allowedDebitGLAccounts = this.glAccountReadPlatformService.retrieveAccountsByTagId(accountingRule.getId(), + JournalEntryType.DEBIT.getValue()); + for (final GLAccountDataForLookup accountDataForLookup : allowedDebitGLAccounts) { + for (int i = 0; i < debits.length; i++) { + final SingleDebitOrCreditEntryCommand debit = debits[i]; + if (debit.getGlAccountId().equals(accountDataForLookup.getId())) { + validDebits[i] = debit; + } + } + } + if (debits.length != validDebits.length) { throw new RuntimeException("Invalid debits"); } + } + } + + private void checkDebitAndCreditAmounts(final SingleDebitOrCreditEntryCommand[] credits, final SingleDebitOrCreditEntryCommand[] debits) { + // sum of all debits must be = sum of all credits + BigDecimal creditsSum = BigDecimal.ZERO; + BigDecimal debitsSum = BigDecimal.ZERO; + for (final SingleDebitOrCreditEntryCommand creditEntryCommand : credits) { + if (creditEntryCommand.getAmount() == null || creditEntryCommand.getGlAccountId() == null) { throw new JournalEntryInvalidException( + GL_JOURNAL_ENTRY_INVALID_REASON.DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY, null, null, null); } + creditsSum = creditsSum.add(creditEntryCommand.getAmount()); + } + for (final SingleDebitOrCreditEntryCommand debitEntryCommand : debits) { + if (debitEntryCommand.getAmount() == null || debitEntryCommand.getGlAccountId() == null) { throw new JournalEntryInvalidException( + GL_JOURNAL_ENTRY_INVALID_REASON.DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY, null, null, null); } + debitsSum = debitsSum.add(debitEntryCommand.getAmount()); + } + if (creditsSum.compareTo(debitsSum) != 0) { throw new JournalEntryInvalidException( + GL_JOURNAL_ENTRY_INVALID_REASON.DEBIT_CREDIT_SUM_MISMATCH, null, null, null); } + } + + private void checkDebitAndCreditAmounts(final SingleDebitOrCreditEntryCommand[] creditOrDebits, final BigDecimal amount) { + if (amount == null) { throw new JournalEntryInvalidException(GL_JOURNAL_ENTRY_INVALID_REASON.DEBIT_CREDIT_SUM_MISMATCH_WITH_AMOUNT, + null, null, null); } + BigDecimal creditOrDebitSum = BigDecimal.ZERO; + for (final SingleDebitOrCreditEntryCommand creditEntryCommand : creditOrDebits) { + if (creditEntryCommand.getAmount() == null || creditEntryCommand.getGlAccountId() == null) { throw new JournalEntryInvalidException( + GL_JOURNAL_ENTRY_INVALID_REASON.DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY, null, null, null); } + creditOrDebitSum = creditOrDebitSum.add(creditEntryCommand.getAmount()); + } + if (creditOrDebitSum.compareTo(amount) != 0) { throw new JournalEntryInvalidException( + GL_JOURNAL_ENTRY_INVALID_REASON.DEBIT_CREDIT_SUM_MISMATCH, null, null, null); } + } + + private void validateGLAccountForTransaction(final GLAccount creditOrDebitAccountHead) { /*** * validate that the account allows manual adjustments and is not * disabled @@ -199,7 +307,7 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa final boolean accrualBasedAccountingEnabled = (Boolean) accountingBridgeData.get("accrualBasedAccountingEnabled"); if (cashBasedAccountingEnabled || accrualBasedAccountingEnabled) { - final LoanDTO loanDTO = helper.populateLoanDtoFromMap(accountingBridgeData, cashBasedAccountingEnabled, + final LoanDTO loanDTO = this.helper.populateLoanDtoFromMap(accountingBridgeData, cashBasedAccountingEnabled, accrualBasedAccountingEnabled); final AccountingProcessorForLoan accountingProcessorForLoan = this.accountingProcessorForLoanFactory .determineProcessor(loanDTO); @@ -209,13 +317,13 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa @Transactional @Override - public void createJournalEntriesForSavings(Map accountingBridgeData) { + public void createJournalEntriesForSavings(final Map accountingBridgeData) { final boolean cashBasedAccountingEnabled = (Boolean) accountingBridgeData.get("cashBasedAccountingEnabled"); final boolean accrualBasedAccountingEnabled = (Boolean) accountingBridgeData.get("accrualBasedAccountingEnabled"); if (cashBasedAccountingEnabled || accrualBasedAccountingEnabled) { - final SavingsDTO savingsDTO = helper.populateSavingsDtoFromMap(accountingBridgeData, cashBasedAccountingEnabled, + final SavingsDTO savingsDTO = this.helper.populateSavingsDtoFromMap(accountingBridgeData, cashBasedAccountingEnabled, accrualBasedAccountingEnabled); final AccountingProcessorForSavings accountingProcessorForSavings = this.accountingProcessorForSavingsFactory .determineProcessor(savingsDTO); @@ -247,21 +355,7 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa if ((credits == null || credits.length <= 0) || (debits == null || debits.length <= 0)) { throw new JournalEntryInvalidException( GL_JOURNAL_ENTRY_INVALID_REASON.NO_DEBITS_OR_CREDITS, null, null, null); } - // sum of all debits must be = sum of all credits - BigDecimal creditsSum = BigDecimal.ZERO; - BigDecimal debitsSum = BigDecimal.ZERO; - for (final SingleDebitOrCreditEntryCommand creditEntryCommand : credits) { - if (creditEntryCommand.getAmount() == null || creditEntryCommand.getGlAccountId() == null) { throw new JournalEntryInvalidException( - GL_JOURNAL_ENTRY_INVALID_REASON.DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY, null, null, null); } - creditsSum = creditsSum.add(creditEntryCommand.getAmount()); - } - for (final SingleDebitOrCreditEntryCommand debitEntryCommand : debits) { - if (debitEntryCommand.getAmount() == null || debitEntryCommand.getGlAccountId() == null) { throw new JournalEntryInvalidException( - GL_JOURNAL_ENTRY_INVALID_REASON.DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY, null, null, null); } - debitsSum = debitsSum.add(debitEntryCommand.getAmount()); - } - if (creditsSum.compareTo(debitsSum) != 0) { throw new JournalEntryInvalidException( - GL_JOURNAL_ENTRY_INVALID_REASON.DEBIT_CREDIT_SUM_MISMATCH, null, null, null); } + checkDebitAndCreditAmounts(credits, debits); } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/domain/AccountingTagRule.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/domain/AccountingTagRule.java index 82603f1f3..cdc845b28 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/domain/AccountingTagRule.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/domain/AccountingTagRule.java @@ -24,10 +24,10 @@ public class AccountingTagRule extends AbstractPersistable { @ManyToOne @JoinColumn(name = "tag_id", nullable = false) - private final CodeValue tagId; + private CodeValue tagId; @Column(name = "acc_type_enum", nullable = false) - private final Integer accountType; + private Integer accountType; public static AccountingTagRule create(final CodeValue tagId, final Integer accountType) { return new AccountingTagRule(tagId, accountType); @@ -42,4 +42,12 @@ public class AccountingTagRule extends AbstractPersistable { this.accountingRule = accountingRule; } + public AccountingTagRule() { + // TODO Auto-generated constructor stub + } + + public Integer getAccountType() { + return this.accountType; + } + } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleWritePlatformServiceJpaRepositoryImpl.java index 2e38b7516..483b8a6a2 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleWritePlatformServiceJpaRepositoryImpl.java @@ -21,6 +21,7 @@ import org.mifosplatform.accounting.rule.domain.AccountingTagRule; import org.mifosplatform.accounting.rule.exception.AccountingRuleDuplicateException; import org.mifosplatform.infrastructure.codes.domain.CodeValue; import org.mifosplatform.infrastructure.codes.domain.CodeValueRepository; +import org.mifosplatform.infrastructure.codes.exception.CodeValueNotFoundException; import org.mifosplatform.infrastructure.core.api.JsonCommand; import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder; @@ -109,20 +110,22 @@ public class AccountingRuleWritePlatformServiceJpaRepositoryImpl implements Acco GLAccount creditAccount = null; List accountingTagRules = new ArrayList(); - if (accountToDebitId != null) { + if ((accountToDebitId != null && debitTags != null) || (accountToDebitId == null && debitTags == null)) { + // TODO- Throw Appropriate exception + throw new RuntimeException("Please Specify debitTags or accountToDebitId"); + } else if (accountToDebitId != null) { debitAccount = this.accountRepositoryWrapper.findOneWithNotFoundDetection(accountToDebitId); } else if (debitTags != null) { accountingTagRules = saveDebitOrCreditTags(debitTags, JournalEntryType.DEBIT, accountingTagRules); - } else { - throw new RuntimeException("Please Specify debitTags or accountToDebitId"); } - if (accountToCreditId != null) { - creditAccount = this.accountRepositoryWrapper.findOneWithNotFoundDetection(accountToCreditId); - } else if (debitTags != null) { - accountingTagRules = saveDebitOrCreditTags(creditTags, JournalEntryType.CREDIT, accountingTagRules); - } else { + if ((accountToCreditId != null && creditTags != null) || (accountToCreditId == null && creditTags == null)) { + // TODO- Throw Appropriate exception throw new RuntimeException("Please Specify debitTags or accountToDebitId"); + } else if (accountToCreditId != null) { + creditAccount = this.accountRepositoryWrapper.findOneWithNotFoundDetection(accountToCreditId); + } else if (creditTags != null) { + accountingTagRules = saveDebitOrCreditTags(creditTags, JournalEntryType.CREDIT, accountingTagRules); } final AccountingRule accountingRule = AccountingRule.fromJson(office, debitAccount, creditAccount, command); @@ -131,6 +134,8 @@ public class AccountingRuleWritePlatformServiceJpaRepositoryImpl implements Acco return accountingRule; } + // TODO- This update mehtod should update based on new changes made for + // createAccountingRule. @Transactional @Override public CommandProcessingResult updateAccountingRule(final Long accountingRuleId, final JsonCommand command) { @@ -205,6 +210,7 @@ public class AccountingRuleWritePlatformServiceJpaRepositoryImpl implements Acco for (final String creditOrDebitTag : creditOrDebitTagArray) { final Long creditOrDebitTagIdLongValue = Long.valueOf(creditOrDebitTag); final CodeValue creditOrDebitAccount = this.codeValueRepository.findOne(creditOrDebitTagIdLongValue); + if (creditOrDebitAccount == null) { throw new CodeValueNotFoundException(creditOrDebitTagIdLongValue); } final AccountingTagRule accountingTagRule = AccountingTagRule.create(creditOrDebitAccount, transactionType.getValue()); accountingTagRules.add(accountingTagRule); } From ffa5435765b0a90e404e9af3703b63b8df5fdc35 Mon Sep 17 00:00:00 2001 From: Anuruddha Premalal Date: Wed, 29 May 2013 14:27:39 +0530 Subject: [PATCH 07/49] Impelement pagination for SavingsAccounts api --- .../api/SavingsAccountsApiResource.java | 12 +++- .../SavingsAccountReadPlatformService.java | 4 +- ...SavingsAccountReadPlatformServiceImpl.java | 61 +++++++++++++++++-- 3 files changed, 69 insertions(+), 8 deletions(-) diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/api/SavingsAccountsApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/api/SavingsAccountsApiResource.java index 7bb69f3a4..8104b41f1 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/api/SavingsAccountsApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/api/SavingsAccountsApiResource.java @@ -34,7 +34,10 @@ import org.mifosplatform.infrastructure.core.data.EnumOptionData; import org.mifosplatform.infrastructure.core.exception.UnrecognizedQueryParamException; import org.mifosplatform.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; import org.mifosplatform.infrastructure.core.serialization.DefaultToApiJsonSerializer; +import org.mifosplatform.infrastructure.core.service.Page; import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; +import org.mifosplatform.portfolio.client.api.ClientApiConstants; +import org.mifosplatform.portfolio.group.service.SearchParameters; import org.mifosplatform.portfolio.savings.data.SavingsAccountData; import org.mifosplatform.portfolio.savings.data.SavingsAccountTransactionData; import org.mifosplatform.portfolio.savings.data.SavingsProductData; @@ -93,11 +96,16 @@ public class SavingsAccountsApiResource { @GET @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) - public String retrieveAll(@Context final UriInfo uriInfo) { + public String retrieveAll(@Context final UriInfo uriInfo, @QueryParam("sqlSearch") final String sqlSearch, + @QueryParam("externalId") final String externalId, @QueryParam("underHierarchy") final String hierarchy, + @QueryParam("offset") final Integer offset, @QueryParam("limit") final Integer limit, + @QueryParam("orderBy") final String orderBy, @QueryParam("sortOrder") final String sortOrder) { context.authenticatedUser().validateHasReadPermission(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME); - final Collection products = this.savingsAccountReadPlatformService.retrieveAll(); + final SearchParameters searchParameters = SearchParameters.forLoans(sqlSearch, externalId, offset, limit, orderBy, sortOrder); + + final Page products = this.savingsAccountReadPlatformService.retrieveAll(searchParameters); final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters()); return this.toApiJsonSerializer.serialize(settings, products, SavingsApiConstants.SAVINGS_ACCOUNT_RESPONSE_DATA_PARAMETERS); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountReadPlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountReadPlatformService.java index 327c952ee..e8a9a6e14 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountReadPlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountReadPlatformService.java @@ -7,12 +7,14 @@ package org.mifosplatform.portfolio.savings.service; import java.util.Collection; +import org.mifosplatform.infrastructure.core.service.Page; +import org.mifosplatform.portfolio.group.service.SearchParameters; import org.mifosplatform.portfolio.savings.data.SavingsAccountData; import org.mifosplatform.portfolio.savings.data.SavingsAccountTransactionData; public interface SavingsAccountReadPlatformService { - Collection retrieveAll(); + Page retrieveAll(SearchParameters searchParameters); SavingsAccountData retrieveOne(Long savingsId); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java index ef142ec0f..af9e35ecb 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java @@ -8,14 +8,18 @@ package org.mifosplatform.portfolio.savings.service; import java.math.BigDecimal; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.Arrays; import java.util.Collection; +import org.apache.commons.lang.StringUtils; import org.joda.time.LocalDate; import org.joda.time.MonthDay; import org.mifosplatform.infrastructure.codes.data.CodeValueData; import org.mifosplatform.infrastructure.core.data.EnumOptionData; import org.mifosplatform.infrastructure.core.domain.JdbcSupport; import org.mifosplatform.infrastructure.core.service.DateUtils; +import org.mifosplatform.infrastructure.core.service.Page; +import org.mifosplatform.infrastructure.core.service.PaginationHelper; import org.mifosplatform.infrastructure.core.service.TenantAwareRoutingDataSource; import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; import org.mifosplatform.organisation.monetary.data.CurrencyData; @@ -23,6 +27,7 @@ import org.mifosplatform.portfolio.client.data.ClientData; import org.mifosplatform.portfolio.client.service.ClientReadPlatformService; import org.mifosplatform.portfolio.group.data.GroupGeneralData; import org.mifosplatform.portfolio.group.service.GroupReadPlatformService; +import org.mifosplatform.portfolio.group.service.SearchParameters; import org.mifosplatform.portfolio.paymentdetail.data.PaymentDetailData; import org.mifosplatform.portfolio.savings.data.SavingsAccountData; import org.mifosplatform.portfolio.savings.data.SavingsAccountStatusEnumData; @@ -36,6 +41,7 @@ import org.mifosplatform.portfolio.savings.domain.SavingsInterestCalculationType import org.mifosplatform.portfolio.savings.domain.SavingsInterestPostingPeriodType; import org.mifosplatform.portfolio.savings.domain.SavingsPeriodFrequencyType; import org.mifosplatform.portfolio.savings.exception.SavingsAccountNotFoundException; +import org.mifosplatform.useradministration.domain.AppUser; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; @@ -53,6 +59,9 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead private final SavingsDropdownReadPlatformService dropdownReadPlatformService; private final SavingsAccountTransactionTemplateMapper transactionTemplateMapper; private final SavingsAccountTransactionsMapper transactionsMapper; + + private final PaginationHelper paginationHelper = new PaginationHelper(); + private final SavingAccountMapper savingAccountMapper = new SavingAccountMapper(); @Autowired public SavingsAccountReadPlatformServiceImpl(final PlatformSecurityContext context, final TenantAwareRoutingDataSource dataSource, @@ -70,12 +79,54 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead } @Override - public Collection retrieveAll() { + public Page retrieveAll(final SearchParameters searchParameters) { - this.context.authenticatedUser(); - final SavingAccountMapper mapper = new SavingAccountMapper(); - final String sql = "select " + mapper.schema(); - return this.jdbcTemplate.query(sql, mapper, new Object[] {}); + final AppUser currentUser = context.authenticatedUser(); + final String hierarchy = currentUser.getOffice().getHierarchy(); + final String hierarchySearchString = hierarchy + "%"; + + StringBuilder sqlBuilder = new StringBuilder(200); + sqlBuilder.append("select SQL_CALC_FOUND_ROWS "); + sqlBuilder.append(savingAccountMapper.schema()); + + sqlBuilder.append(" join m_office o on o.id = c.office_id"); + sqlBuilder.append(" where o.hierarchy like ?"); + + final Object[] objectArray = new Object[2]; + objectArray[0] = hierarchySearchString; + int arrayPos = 1; + + String sqlQueryCriteria = searchParameters.getSqlSearch(); + if (StringUtils.isNotBlank(sqlQueryCriteria)) { + sqlQueryCriteria = sqlQueryCriteria.replaceAll("accountNo", "sa.account_no"); + sqlBuilder.append(" and (").append(sqlQueryCriteria).append(")"); + } + + if (StringUtils.isNotBlank(searchParameters.getExternalId())) { + sqlBuilder.append(" and sa.external_id = ?"); + objectArray[arrayPos] = searchParameters.getExternalId(); + arrayPos = arrayPos + 1; + } + + if (searchParameters.isOrderByRequested()) { + sqlBuilder.append(" order by ").append(searchParameters.getOrderBy()); + + if (searchParameters.isSortOrderProvided()) { + sqlBuilder.append(' ').append(searchParameters.getSortOrder()); + } + } + + if (searchParameters.isLimited()) { + sqlBuilder.append(" limit ").append(searchParameters.getLimit()); + if (searchParameters.isOffset()) { + sqlBuilder.append(" offset ").append(searchParameters.getOffset()); + } + } + + final Object[] finalObjectArray = Arrays.copyOf(objectArray, arrayPos); + final String sqlCountRows = "SELECT FOUND_ROWS()"; + return this.paginationHelper.fetchPage(this.jdbcTemplate, sqlCountRows, sqlBuilder.toString(), finalObjectArray, + this.savingAccountMapper); } @Override From 7f062a394bccb0978026ec1443da679c0fb07755 Mon Sep 17 00:00:00 2001 From: Keith Woodlock Date: Wed, 29 May 2013 10:13:48 +0100 Subject: [PATCH 08/49] some formating --- .../portfolio/savings/api/SavingsAccountsApiResource.java | 1 - .../scheduledjobs/service/ScheduledJobRunnerServiceImpl.java | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/api/SavingsAccountsApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/api/SavingsAccountsApiResource.java index 8104b41f1..fc48527f6 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/api/SavingsAccountsApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/api/SavingsAccountsApiResource.java @@ -36,7 +36,6 @@ import org.mifosplatform.infrastructure.core.serialization.ApiRequestJsonSeriali import org.mifosplatform.infrastructure.core.serialization.DefaultToApiJsonSerializer; import org.mifosplatform.infrastructure.core.service.Page; import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; -import org.mifosplatform.portfolio.client.api.ClientApiConstants; import org.mifosplatform.portfolio.group.service.SearchParameters; import org.mifosplatform.portfolio.savings.data.SavingsAccountData; import org.mifosplatform.portfolio.savings.data.SavingsAccountTransactionData; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerServiceImpl.java index ccd13de5b..fd517a8d5 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerServiceImpl.java @@ -120,6 +120,7 @@ public class ScheduledJobRunnerServiceImpl implements ScheduledJobRunnerService logger.info("Results affected by update: " + result); } + @Transactional @Override public void updateLoanArrearsAgeingDetails() { final MifosPlatformTenant tenant = this.tenantDetailsService.loadTenantById("default"); @@ -161,6 +162,7 @@ public class ScheduledJobRunnerServiceImpl implements ScheduledJobRunnerService logger.info("Results affected by update: " + result); } + @Transactional @Override public void applyAnnualFeeForSavings() { final MifosPlatformTenant tenant = this.tenantDetailsService.loadTenantById("default"); From 14e55045885c66215a5f5609fe1d7fcaa8ab77ae Mon Sep 17 00:00:00 2001 From: Lech Rozanski Date: Wed, 29 May 2013 16:09:01 +0200 Subject: [PATCH 09/49] MIFOSX-365: Simplify 'Data Tables' functionality --- .../core/data/DataValidatorBuilder.java | 36 +- ...tatableCommandFromApiJsonDeserializer.java | 153 +++--- .../UpdateDatatableCommandHandler.java | 14 +- .../ReadWriteNonCoreDataServiceImpl.java | 451 ++++++++++-------- 4 files changed, 373 insertions(+), 281 deletions(-) diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/data/DataValidatorBuilder.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/data/DataValidatorBuilder.java index 35e0db5fd..313377e10 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/data/DataValidatorBuilder.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/data/DataValidatorBuilder.java @@ -16,6 +16,8 @@ import net.fortuna.ical4j.model.property.RRule; import org.apache.commons.lang.StringUtils; import org.springframework.util.ObjectUtils; +import com.google.gson.JsonArray; + public class DataValidatorBuilder { private final List dataValidationErrors; @@ -206,12 +208,13 @@ public class DataValidatorBuilder { if (value != null) { List valuesList = Arrays.asList(values); + String valuesListStr = StringUtils.join(valuesList, ", "); if (!valuesList.contains(this.value)) { StringBuilder validationErrorCode = new StringBuilder("validation.msg.").append(resource).append(".").append(parameter) .append(".is.not.one.of.expected.enumerations"); StringBuilder defaultEnglishMessage = new StringBuilder("The parameter ").append(parameter).append(" must be one of [ ") - .append(values).append(" ] ").append("."); + .append(valuesListStr).append(" ] ").append("."); ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(), defaultEnglishMessage.toString(), parameter, this.value, values); @@ -227,12 +230,13 @@ public class DataValidatorBuilder { if (value != null) { List valuesList = Arrays.asList(values); + String valuesListStr = StringUtils.join(valuesList, ", "); if (valuesList.contains(this.value)) { StringBuilder validationErrorCode = new StringBuilder("validation.msg.").append(resource).append(".").append(parameter) - .append(".is.one.of.expected.enumerations"); - StringBuilder defaultEnglishMessage = new StringBuilder("The parameter ").append(parameter).append(" must not be one of [ ") - .append(values).append(" ] ").append("."); + .append(".is.one.of.unwanted.enumerations"); + StringBuilder defaultEnglishMessage = new StringBuilder("The parameter ").append(parameter).append(" must not be any of [ ") + .append(valuesListStr).append(" ] ").append("."); ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(), defaultEnglishMessage.toString(), parameter, this.value, values); @@ -330,6 +334,22 @@ public class DataValidatorBuilder { } return this; } + + public DataValidatorBuilder jsonArrayNotEmpty() { + if (value == null && ignoreNullValue) { return this; } + + JsonArray array = (JsonArray)value; + if (value != null && !array.iterator().hasNext()) { + StringBuilder validationErrorCode = new StringBuilder("validation.msg.").append(resource).append(".").append(parameter) + .append(".cannot.be.empty"); + StringBuilder defaultEnglishMessage = new StringBuilder("The parameter ").append(parameter).append( + " cannot be empty. You must select at least one."); + ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(), defaultEnglishMessage.toString(), + parameter); + dataValidationErrors.add(error); + } + return this; + } public void expectedArrayButIsNot() { StringBuilder validationErrorCode = new StringBuilder("validation.msg.").append(resource).append(".").append(parameter) @@ -406,7 +426,7 @@ public class DataValidatorBuilder { } public DataValidatorBuilder cantBeBlankWhenParameterProvidedIs(final String parameterName, final Object parameterValue) { - if (StringUtils.isNotBlank(value.toString())) { return this; } + if (value != null && StringUtils.isNotBlank(value.toString())) { return this; } StringBuilder validationErrorCode = new StringBuilder("validation.msg.").append(resource).append(".").append(parameter) .append(".must.be.provided.when.").append(parameterName).append(".is.").append(parameterValue); @@ -557,12 +577,12 @@ public class DataValidatorBuilder { public DataValidatorBuilder matchesRegularExpression(final String expression) { if (value == null && ignoreNullValue) { return this; } - - if (!value.toString().matches(expression)) { + + if (value != null && !value.toString().matches(expression)) { StringBuilder validationErrorCode = new StringBuilder("validation.msg.").append(resource).append(".").append(parameter) .append(".does.not.match.regexp"); StringBuilder defaultEnglishMessage = new StringBuilder("The parameter ").append(parameter) - .append(" must match the provided regular expression ") + .append(" must match the provided regular expression [ ") .append(expression).append(" ] ").append("."); ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(), diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/DatatableCommandFromApiJsonDeserializer.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/DatatableCommandFromApiJsonDeserializer.java index f1201cb30..db1f74a08 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/DatatableCommandFromApiJsonDeserializer.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/DatatableCommandFromApiJsonDeserializer.java @@ -44,7 +44,8 @@ public class DatatableCommandFromApiJsonDeserializer { "name", "newName", "type", "length", "mandatory", "after")); private final Set supportedParametersForDropColumns = new HashSet(Arrays.asList( "name")); - private final Object[] supportedColumnTypes = { "String", "Number", "Decimal", "Date" }; + private final Object[] supportedColumnTypes = { "String", "Number", "Decimal", "Date", "Text" }; + private final Object[] supportedApptableNames = { "m_loan", "m_savings_account", "m_client", "m_group", "m_office" }; private final FromJsonHelper fromApiJsonHelper; @@ -64,40 +65,50 @@ public class DatatableCommandFromApiJsonDeserializer { final JsonElement element = fromApiJsonHelper.parse(json); final String apptableName = fromApiJsonHelper.extractStringNamed("apptableName", element); - final String fkColumnName = apptableName.substring(2) + "_id"; - baseDataValidator.reset().parameter("apptableName").value(apptableName).notNull().notBlank().notExceedingLengthOf(50); + baseDataValidator.reset().parameter("apptableName").value(apptableName).notBlank().notExceedingLengthOf(50) + .isOneOfTheseValues(supportedApptableNames); + final String fkColumnName = (apptableName != null) ? apptableName.substring(2) + "_id" : ""; final String datatableName = fromApiJsonHelper.extractStringNamed("datatableName", element); - baseDataValidator.reset().parameter("datatableName").value(datatableName).notNull().notBlank().notExceedingLengthOf(50) + baseDataValidator.reset().parameter("datatableName").value(datatableName).notBlank().notExceedingLengthOf(50) .matchesRegularExpression(DATATABLE_NAME_REGEX_PATTERN); final Boolean multiRow = fromApiJsonHelper.extractBooleanNamed("multiRow", element); - baseDataValidator.reset().parameter("multiRow").value(multiRow).ignoreIfNull().trueOrFalseRequired(multiRow); + baseDataValidator.reset().parameter("multiRow").value(multiRow).ignoreIfNull().notBlank() + .isOneOfTheseValues(true, false); final JsonArray columns = fromApiJsonHelper.extractJsonArrayNamed("columns", element); - baseDataValidator.reset().parameter("columns").value(columns).notNull(); + baseDataValidator.reset().parameter("columns").value(columns).notNull().jsonArrayNotEmpty(); - for (JsonElement column : columns) { - fromApiJsonHelper.checkForUnsupportedParameters(column.getAsJsonObject(), supportedParametersForCreateColumns); - - final String name = fromApiJsonHelper.extractStringNamed("name", column); - baseDataValidator.reset().parameter("name").value(name).notNull().notBlank().notExceedingLengthOf(50) - .isNotOneOfTheseValues("id", fkColumnName).matchesRegularExpression(DATATABLE_NAME_REGEX_PATTERN); - - final String type = fromApiJsonHelper.extractStringNamed("type", column); - baseDataValidator.reset().parameter("type").value(type).notNull().notBlank().isOneOfTheseValues(supportedColumnTypes); - - final Integer length = fromApiJsonHelper.extractIntegerSansLocaleNamed("length", column); - if (type.equals("String")) { - baseDataValidator.reset().parameter("length").value(length).notNull().notBlank() - .zeroOrPositiveAmount(); - } else { - baseDataValidator.reset().parameter("length").value(length).ignoreIfNull().notBlank() - .zeroOrPositiveAmount(); - } - - final Boolean mandatory = fromApiJsonHelper.extractBooleanNamed("mandatory", column); - baseDataValidator.reset().parameter("mandatory").value(mandatory).ignoreIfNull().trueOrFalseRequired(mandatory); + if (columns != null) { + for (JsonElement column : columns) { + fromApiJsonHelper.checkForUnsupportedParameters(column.getAsJsonObject(), supportedParametersForCreateColumns); + + final String name = fromApiJsonHelper.extractStringNamed("name", column); + baseDataValidator.reset().parameter("name").value(name).notBlank().notExceedingLengthOf(50) + .isNotOneOfTheseValues("id", fkColumnName).matchesRegularExpression(DATATABLE_NAME_REGEX_PATTERN); + + final String type = fromApiJsonHelper.extractStringNamed("type", column); + baseDataValidator.reset().parameter("type").value(type).notBlank().isOneOfTheseValues(supportedColumnTypes); + + final Integer length = fromApiJsonHelper.extractIntegerSansLocaleNamed("length", column); + if (type != null && (type.equals("String") || type.equals("Text"))) { + if (length != null) { + baseDataValidator.reset().parameter("length").value(length).notBlank() + .zeroOrPositiveAmount(); + } else if (!type.equals("Text")) { + baseDataValidator.reset().parameter("length").value(length) + .cantBeBlankWhenParameterProvidedIs("type", type); + } + } else if (type != null && length != null) { + baseDataValidator.reset().parameter("length").value(length) + .mustBeBlankWhenParameterProvidedIs("type", type); + } + + final Boolean mandatory = fromApiJsonHelper.extractBooleanNamed("mandatory", column); + baseDataValidator.reset().parameter("mandatory").value(mandatory).ignoreIfNull().notBlank() + .isOneOfTheseValues(true, false); + } } throwExceptionIfValidationWarningsExist(dataValidationErrors); @@ -105,6 +116,9 @@ public class DatatableCommandFromApiJsonDeserializer { public void validateForUpdate(final String json) { if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); } + // Because all parameters are optional, a check to see if at least one parameter + // has been specified is necessary in order to avoid JSON requests with no parameters + if (!json.matches("(?s)\\A\\{.*?(\\\".*?\\\"\\s*?:\\s*?)+.*?\\}\\z")) { throw new InvalidJsonException(); } final Type typeOfMap = new TypeToken>() {}.getType(); fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, supportedParametersForUpdate); @@ -114,18 +128,19 @@ public class DatatableCommandFromApiJsonDeserializer { final JsonElement element = fromApiJsonHelper.parse(json); final String apptableName = fromApiJsonHelper.extractStringNamed("apptableName", element); - final String fkColumnName = apptableName.substring(2) + "_id"; - baseDataValidator.reset().parameter("apptableName").value(apptableName).notBlank().notExceedingLengthOf(50); + baseDataValidator.reset().parameter("apptableName").value(apptableName).ignoreIfNull().notBlank() + .notExceedingLengthOf(50).isOneOfTheseValues(supportedApptableNames); + final String fkColumnName = (apptableName != null) ? apptableName.substring(2) + "_id" : ""; - if (fromApiJsonHelper.parameterExists("changeColumns", element)) { - final JsonArray changeColumns = fromApiJsonHelper.extractJsonArrayNamed("changeColumns", element); - baseDataValidator.reset().parameter("changeColumns").value(changeColumns).ignoreIfNull(); + final JsonArray changeColumns = fromApiJsonHelper.extractJsonArrayNamed("changeColumns", element); + baseDataValidator.reset().parameter("changeColumns").value(changeColumns).ignoreIfNull().jsonArrayNotEmpty(); + if (changeColumns != null) { for (JsonElement column : changeColumns) { fromApiJsonHelper.checkForUnsupportedParameters(column.getAsJsonObject(), supportedParametersForChangeColumns); final String name = fromApiJsonHelper.extractStringNamed("name", column); - baseDataValidator.reset().parameter("name").value(name).notNull().notBlank().notExceedingLengthOf(50) + baseDataValidator.reset().parameter("name").value(name).notBlank().notExceedingLengthOf(50) .isNotOneOfTheseValues("id", fkColumnName).matchesRegularExpression(DATATABLE_NAME_REGEX_PATTERN); final String newName = fromApiJsonHelper.extractStringNamed("newName", column); @@ -134,65 +149,81 @@ public class DatatableCommandFromApiJsonDeserializer { .matchesRegularExpression(DATATABLE_NAME_REGEX_PATTERN); final String type = fromApiJsonHelper.extractStringNamed("type", column); - baseDataValidator.reset().parameter("type").value(type).notNull().notBlank() + baseDataValidator.reset().parameter("type").value(type).notBlank() .isOneOfTheseValues(supportedColumnTypes); final Integer length = fromApiJsonHelper.extractIntegerSansLocaleNamed("length", column); - if (type.equals("String")) { - baseDataValidator.reset().parameter("length").value(length).notNull().notBlank() - .zeroOrPositiveAmount(); - } else { - baseDataValidator.reset().parameter("length").value(length).ignoreIfNull().notBlank() - .zeroOrPositiveAmount(); + if (type != null && (type.equals("String") || type.equals("Text"))) { + if (length != null) { + baseDataValidator.reset().parameter("length").value(length).notBlank() + .zeroOrPositiveAmount(); + } else if (!type.equals("Text")) { + baseDataValidator.reset().parameter("length").value(length) + .cantBeBlankWhenParameterProvidedIs("type", type); + } + } else if (type != null && length != null) { + baseDataValidator.reset().parameter("length").value(length) + .mustBeBlankWhenParameterProvidedIs("type", type); } final Boolean mandatory = fromApiJsonHelper.extractBooleanNamed("mandatory", column); - baseDataValidator.reset().parameter("mandatory").value(mandatory).ignoreIfNull().trueOrFalseRequired(mandatory); - + baseDataValidator.reset().parameter("mandatory").value(mandatory).ignoreIfNull().notBlank() + .isOneOfTheseValues(true, false); + final Boolean after = fromApiJsonHelper.extractBooleanNamed("after", column); - baseDataValidator.reset().parameter("after").value(after).ignoreIfNull().notBlank(); + baseDataValidator.reset().parameter("after").value(after).ignoreIfNull().notBlank() + .isOneOfTheseValues(true, false); } } - if (fromApiJsonHelper.parameterExists("addColumns", element)) { - final JsonArray addColumns = fromApiJsonHelper.extractJsonArrayNamed("addColumns", element); - baseDataValidator.reset().parameter("addColumns").value(addColumns).ignoreIfNull(); - + + final JsonArray addColumns = fromApiJsonHelper.extractJsonArrayNamed("addColumns", element); + baseDataValidator.reset().parameter("addColumns").value(addColumns).ignoreIfNull().jsonArrayNotEmpty(); + + if (addColumns != null) { for (JsonElement column : addColumns) { fromApiJsonHelper.checkForUnsupportedParameters(column.getAsJsonObject(), supportedParametersForAddColumns); final String name = fromApiJsonHelper.extractStringNamed("name", column); - baseDataValidator.reset().parameter("name").value(name).notNull().notBlank().notExceedingLengthOf(50) + baseDataValidator.reset().parameter("name").value(name).notBlank().notExceedingLengthOf(50) .isNotOneOfTheseValues("id", fkColumnName).matchesRegularExpression(DATATABLE_NAME_REGEX_PATTERN); final String type = fromApiJsonHelper.extractStringNamed("type", column); - baseDataValidator.reset().parameter("type").value(type).notNull().notBlank() + baseDataValidator.reset().parameter("type").value(type).notBlank() .isOneOfTheseValues(supportedColumnTypes); final Integer length = fromApiJsonHelper.extractIntegerSansLocaleNamed("length", column); - if (type.equals("String")) { - baseDataValidator.reset().parameter("length").value(length).notNull().notBlank() - .zeroOrPositiveAmount(); - } else { - baseDataValidator.reset().parameter("length").value(length).ignoreIfNull().notBlank() - .zeroOrPositiveAmount(); + if (type != null && (type.equals("String") || type.equals("Text"))) { + if (length != null) { + baseDataValidator.reset().parameter("length").value(length).notBlank() + .zeroOrPositiveAmount(); + } else if (!type.equals("Text")) { + baseDataValidator.reset().parameter("length").value(length) + .cantBeBlankWhenParameterProvidedIs("type", type); + } + } else if (type != null && length != null) { + baseDataValidator.reset().parameter("length").value(length) + .mustBeBlankWhenParameterProvidedIs("type", type); } final Boolean mandatory = fromApiJsonHelper.extractBooleanNamed("mandatory", column); - baseDataValidator.reset().parameter("mandatory").value(mandatory).ignoreIfNull().trueOrFalseRequired(mandatory); + baseDataValidator.reset().parameter("mandatory").value(mandatory).ignoreIfNull().notBlank() + .isOneOfTheseValues(true, false); final Boolean after = fromApiJsonHelper.extractBooleanNamed("after", column); - baseDataValidator.reset().parameter("after").value(after).ignoreIfNull().notBlank(); + baseDataValidator.reset().parameter("after").value(after).ignoreIfNull().notBlank() + .isOneOfTheseValues(true, false); } } - if (fromApiJsonHelper.parameterExists("dropColumns", element)) { - final JsonArray dropColumns = fromApiJsonHelper.extractJsonArrayNamed("dropColumns", element); - baseDataValidator.reset().parameter("dropColumns").value(dropColumns).ignoreIfNull(); + final JsonArray dropColumns = fromApiJsonHelper.extractJsonArrayNamed("dropColumns", element); + baseDataValidator.reset().parameter("dropColumns").value(dropColumns).ignoreIfNull().jsonArrayNotEmpty(); + + if (dropColumns != null) { for (JsonElement column : dropColumns) { fromApiJsonHelper.checkForUnsupportedParameters(column.getAsJsonObject(), supportedParametersForDropColumns); final String name = fromApiJsonHelper.extractStringNamed("name", column); - baseDataValidator.reset().parameter("name").value(name).notNull().notBlank().notExceedingLengthOf(50) + baseDataValidator.reset().parameter("name").value(name).notBlank().notExceedingLengthOf(50) .isNotOneOfTheseValues("id", fkColumnName).matchesRegularExpression(DATATABLE_NAME_REGEX_PATTERN); } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/UpdateDatatableCommandHandler.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/UpdateDatatableCommandHandler.java index 625e62330..b96a8bfc5 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/UpdateDatatableCommandHandler.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/UpdateDatatableCommandHandler.java @@ -27,14 +27,14 @@ public class UpdateDatatableCommandHandler implements NewCommandSourceHandler { @Transactional @Override public CommandProcessingResult processCommand(final JsonCommand command) { - - final String datatableName = command.getUrl().replaceAll("/datatables/", ""); - + + final String datatableName = command.getUrl().replaceAll("/datatables/", ""); + this.writePlatformService.updateDatatable(datatableName, command); - return new CommandProcessingResultBuilder() // - .withCommandId(command.commandId()) // - .withResourceIdAsString(datatableName) // - .build(); + return new CommandProcessingResultBuilder() + .withCommandId(command.commandId()) + .withResourceIdAsString(datatableName) + .build(); } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java index fdc2c3e7a..4b6ebd737 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java @@ -18,6 +18,7 @@ import javax.sql.DataSource; import org.apache.commons.lang.StringUtils; import org.hibernate.exception.ConstraintViolationException; +import org.hibernate.exception.SQLGrammarException; import org.joda.time.LocalDate; import org.mifosplatform.infrastructure.core.api.JsonCommand; import org.mifosplatform.infrastructure.core.data.ApiParameterError; @@ -56,18 +57,13 @@ import com.google.gson.reflect.TypeToken; @Service public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataService { - private final static String DATATABLE_NAME_REGEX_PATTERN = "^[a-zA-Z][a-zA-Z0-9\\-_\\s]{0,48}[a-zA-Z0-9]$"; + private final static String DATATABLE_NAME_REGEX_PATTERN = "^[a-zA-Z][a-zA-Z0-9\\-_\\s]{0,48}[a-zA-Z0-9]$"; private final static Logger logger = LoggerFactory.getLogger(ReadWriteNonCoreDataServiceImpl.class); - private final static HashMap apiTypeToMySQL = new HashMap() { - - { - put("String", "VARCHAR"); - put("Number", "INT"); - put("Decimal", "DECIMAL"); - put("Date", "DATE"); - } - }; + private final static HashMap apiTypeToMySQL = new HashMap() {{ + put("String", "VARCHAR"); put("Number", "INT"); put("Decimal", "DECIMAL"); put("Date", "DATE"); + put("Text", "TEXT"); + }}; private final JdbcTemplate jdbcTemplate; private final DataSource dataSource; @@ -285,232 +281,277 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ } } - private Boolean datatableExists(final String datatable) { - - String sql = "SELECT COUNT(*) FROM `x_registered_table` WHERE `registered_table_name`='" + datatable + "'"; - Integer rowCount = this.jdbcTemplate.queryForInt(sql); - - return (rowCount > 0); - } - - private void validateDatatableExists(final String name) { - - if (!datatableExists(name)) { throw new PlatformDataIntegrityException("error.msg.datatables.missing.table.name", "Table '" + name - + "' does not exist.", name); } - } - - private void validateDatatableNotExists(final String name) { - - if (datatableExists(name)) { throw new PlatformDataIntegrityException("error.msg.datatables.duplicate.table.name", "Table '" + name - + "' already exists.", name); } - } - private void validateDatatableName(final String name) { - - if (name == null || name.isEmpty()) { - throw new PlatformDataIntegrityException("error.msg.datatables.datatable.null.name", "Data table name must not be blank."); - } else if (!name.matches(DATATABLE_NAME_REGEX_PATTERN)) { throw new PlatformDataIntegrityException( - "error.msg.datatables.datatable.invalid.name.regex", "Invalid data table name.", name); } + + if (name == null || name.isEmpty()) { + throw new PlatformDataIntegrityException("error.msg.datatables.datatable.null.name", + "Data table name must not be blank."); + } else if (!name.matches(DATATABLE_NAME_REGEX_PATTERN)) { + throw new PlatformDataIntegrityException("error.msg.datatables.datatable.invalid.name.regex", + "Invalid data table name.", name); + } } private void parseDatatableColumnObjectForCreate(final JsonObject column, StringBuilder sqlBuilder) { - - String name = (column.has("name")) ? column.get("name").getAsString() : null; - String type = (column.has("type")) ? column.get("type").getAsString() : null; - Integer length = (column.has("length")) ? column.get("length").getAsInt() : null; - Boolean mandatory = (column.has("mandatory")) ? column.get("mandatory").getAsBoolean() : false; - - String mysqlType = apiTypeToMySQL.get(type); - sqlBuilder = sqlBuilder.append("`" + name + "` " + mysqlType); - - if (type != null) { - if (type.equals("String")) { - sqlBuilder = sqlBuilder.append("(" + length + ")"); - } else if (type.equals("Decimal")) { - sqlBuilder = sqlBuilder.append("(19,6)"); - } - } - if (mandatory != null) { - if (mandatory) { - sqlBuilder = sqlBuilder.append(" NOT NULL"); - } else { - sqlBuilder = sqlBuilder.append(" DEFAULT NULL"); - } - } - - sqlBuilder = sqlBuilder.append(", "); + + String name = (column.has("name")) ? column.get("name").getAsString() : null; + String type = (column.has("type")) ? column.get("type").getAsString() : null; + Integer length = (column.has("length")) ? column.get("length").getAsInt() : null; + Boolean mandatory = (column.has("mandatory")) ? column.get("mandatory").getAsBoolean() : false; + + String mysqlType = apiTypeToMySQL.get(type); + sqlBuilder = sqlBuilder.append("`" + name + "` " + mysqlType); + + if (type != null) + { + if (type.equals("String")) { + sqlBuilder = sqlBuilder.append("(" + length + ")"); + } else if (type.equals("Decimal")) { + sqlBuilder = sqlBuilder.append("(19,6)"); + } + } + if (mandatory != null) { + if (mandatory) { + sqlBuilder = sqlBuilder.append(" NOT NULL"); + } else { + sqlBuilder = sqlBuilder.append(" DEFAULT NULL"); + } + } + + sqlBuilder = sqlBuilder.append(", "); } @Transactional @Override public CommandProcessingResult createDatatable(final JsonCommand command) { - context.authenticatedUser(); - this.fromApiJsonDeserializer.validateForCreate(command.json()); + String datatableName = null; + + try { + context.authenticatedUser(); + this.fromApiJsonDeserializer.validateForCreate(command.json()); + + JsonElement element = this.fromJsonHelper.parse(command.json()); + JsonArray columns = this.fromJsonHelper.extractJsonArrayNamed("columns", element); + datatableName = this.fromJsonHelper.extractStringNamed("datatableName", element); + String apptableName = this.fromJsonHelper.extractStringNamed("apptableName", element); + Boolean multiRow = this.fromJsonHelper.extractBooleanNamed("multiRow", element); + + if (multiRow == null) { + multiRow = false; + } + + validateDatatableName(datatableName); + validateAppTable(apptableName); + + String fkColumnName = apptableName.substring(2) + "_id"; + String fkName = datatableName.toLowerCase().replaceAll("\\s", "_"); + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder = sqlBuilder.append("CREATE TABLE `" + datatableName + "` ("); + + if (multiRow) { + sqlBuilder = sqlBuilder + .append("`id` BIGINT(20) NOT NULL AUTO_INCREMENT, ") + .append("`" + fkColumnName + "` BIGINT(20) NOT NULL, "); + } else { + sqlBuilder = sqlBuilder + .append("`" + fkColumnName + "` BIGINT(20) NOT NULL, "); + } + + for (JsonElement column : columns) { + parseDatatableColumnObjectForCreate(column.getAsJsonObject(), sqlBuilder); + } + + // Remove trailing comma and space + sqlBuilder = sqlBuilder.delete(sqlBuilder.length() - 2, sqlBuilder.length()); + + if (multiRow) { + sqlBuilder = sqlBuilder + .append(", PRIMARY KEY (`id`)") + .append(", KEY `fk_" + apptableName + "` (`" + fkColumnName + "`)") + .append(", CONSTRAINT `fk_" + fkName + "` ") + .append("FOREIGN KEY (`" + fkColumnName + "`) ") + .append("REFERENCES `" + apptableName + "` (`id`)"); + + } else { + sqlBuilder = sqlBuilder + .append(", PRIMARY KEY (`" + fkColumnName + "`)") + .append(", CONSTRAINT `fk_" + fkName + "` ") + .append("FOREIGN KEY (`" + fkColumnName + "`) ") + .append("REFERENCES `" + apptableName + "` (`id`)"); + } + + sqlBuilder = sqlBuilder.append(") ENGINE=InnoDB DEFAULT CHARSET=utf8;"); + this.jdbcTemplate.execute(sqlBuilder.toString()); + + registerDatatable(datatableName, apptableName); + } catch (SQLGrammarException e) { + Throwable realCause = e.getCause(); - JsonElement element = this.fromJsonHelper.parse(command.json()); - JsonArray columns = this.fromJsonHelper.extractJsonArrayNamed("columns", element); - String datatableName = this.fromJsonHelper.extractStringNamed("datatableName", element); - String apptable = this.fromJsonHelper.extractStringNamed("apptableName", element); - Boolean multiRow = this.fromJsonHelper.extractBooleanNamed("multiRow", element); + if (realCause.getMessage().contains("Table") && realCause.getMessage().contains("already exists")) { + throw new PlatformDataIntegrityException( + "error.msg.datatable.already.exists", realCause.getMessage()); + } + } - if (multiRow == null) { - multiRow = false; - } - - validateDatatableName(datatableName); - validateDatatableName(apptable); - validateDatatableNotExists(datatableName); - - String fkColumnName = apptable.substring(2) + "_id"; - String fkName = datatableName.toLowerCase().replaceAll("\\s", "_"); - StringBuilder sqlBuilder = new StringBuilder(); - sqlBuilder = sqlBuilder.append("CREATE TABLE `" + datatableName + "` ("); - - if (multiRow) { - sqlBuilder = sqlBuilder.append("`id` BIGINT(20) NOT NULL AUTO_INCREMENT, ").append( - "`" + fkColumnName + "` BIGINT(20) NOT NULL, "); - } else { - sqlBuilder = sqlBuilder.append("`" + fkColumnName + "` BIGINT(20) NOT NULL, "); - } - - for (JsonElement column : columns) { - parseDatatableColumnObjectForCreate(column.getAsJsonObject(), sqlBuilder); - } - - // Remove trailing comma and space - sqlBuilder = sqlBuilder.delete(sqlBuilder.length() - 2, sqlBuilder.length()); - - if (multiRow) { - sqlBuilder = sqlBuilder.append(", PRIMARY KEY (`id`)").append(", KEY `fk_" + apptable + "` (`" + fkColumnName + "`)") - .append(", CONSTRAINT `fk_" + fkName + "` ").append("FOREIGN KEY (`" + fkColumnName + "`) ") - .append("REFERENCES `" + apptable + "` (`id`)"); - - } else { - sqlBuilder = sqlBuilder.append(", PRIMARY KEY (`" + fkColumnName + "`)").append(", CONSTRAINT `fk_" + fkName + "` ") - .append("FOREIGN KEY (`" + fkColumnName + "`) ").append("REFERENCES `" + apptable + "` (`id`)"); - } - - sqlBuilder = sqlBuilder.append(") ENGINE=InnoDB DEFAULT CHARSET=utf8;"); - this.jdbcTemplate.execute(sqlBuilder.toString()); - - registerDatatable(datatableName, apptable); - - return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withResourceIdAsString(datatableName).build(); + return new CommandProcessingResultBuilder() + .withCommandId(command.commandId()) + .withResourceIdAsString(datatableName) + .build(); } private void parseDatatableColumnForUpdate(final JsonObject column, StringBuilder sqlBuilder) { - - String name = (column.has("name")) ? column.get("name").getAsString() : null; - String newName = (column.has("newName")) ? column.get("newName").getAsString() : name; - String type = (column.has("type")) ? column.get("type").getAsString() : null; - Integer length = (column.has("length")) ? column.get("length").getAsInt() : null; - Boolean mandatory = (column.has("mandatory")) ? column.get("mandatory").getAsBoolean() : false; - String after = (column.has("after")) ? column.get("after").getAsString() : null; - - String mysqlType = apiTypeToMySQL.get(type); - sqlBuilder = sqlBuilder.append(", CHANGE `" + name + "` `" + newName + "` " + mysqlType); - - if (type != null) { - if (type.equals("String") && length != null) { - sqlBuilder = sqlBuilder.append("(" + length + ")"); - } else if (type.equals("Decimal")) { - sqlBuilder = sqlBuilder.append("(19,6)"); - } - } - if (mandatory != null) { - if (mandatory) { - sqlBuilder = sqlBuilder.append(" NOT NULL"); - } else { - sqlBuilder = sqlBuilder.append(" DEFAULT NULL"); - } - } - if (after != null) { - sqlBuilder = sqlBuilder.append(" AFTER `" + after + "`"); - } + + String name = (column.has("name")) ? column.get("name").getAsString() : null; + String newName = (column.has("newName")) ? column.get("newName").getAsString() : name; + String type = (column.has("type")) ? column.get("type").getAsString() : null; + Integer length = (column.has("length")) ? column.get("length").getAsInt() : null; + Boolean mandatory = (column.has("mandatory")) ? column.get("mandatory").getAsBoolean() : false; + String after = (column.has("after")) ? column.get("after").getAsString() : null; + + String mysqlType = apiTypeToMySQL.get(type); + sqlBuilder = sqlBuilder.append(", CHANGE `" + name + "` `" + newName + "` " + mysqlType); + + if (type != null) { + if (type.equals("String") && length != null) { + sqlBuilder = sqlBuilder.append("(" + length + ")"); + } else if (type.equals("Decimal")) { + sqlBuilder = sqlBuilder.append("(19,6)"); + } + } + if (mandatory != null) { + if (mandatory) { + sqlBuilder = sqlBuilder.append(" NOT NULL"); + } else { + sqlBuilder = sqlBuilder.append(" DEFAULT NULL"); + } + } + if (after != null) { + sqlBuilder = sqlBuilder.append(" AFTER `" + after + "`"); + } } private void parseDatatableColumnForAdd(final JsonObject column, StringBuilder sqlBuilder) { - - String name = (column.has("name")) ? column.get("name").getAsString() : null; - String type = (column.has("type")) ? column.get("type").getAsString() : null; - Integer length = (column.has("length")) ? column.get("length").getAsInt() : null; - Boolean mandatory = (column.has("mandatory")) ? column.get("mandatory").getAsBoolean() : false; - String after = (column.has("after")) ? column.get("after").getAsString() : null; - - String mysqlType = apiTypeToMySQL.get(type); - sqlBuilder = sqlBuilder.append(", ADD `" + name + "` " + mysqlType); - - if (type != null) { - if (type.equals("String") && length != null) { - sqlBuilder = sqlBuilder.append("(" + length + ")"); - } else if (type.equals("Decimal")) { - sqlBuilder = sqlBuilder.append("(19,6)"); - } - } - if (mandatory != null) { - if (mandatory) { - sqlBuilder = sqlBuilder.append(" NOT NULL"); - } else { - sqlBuilder = sqlBuilder.append(" DEFAULT NULL"); - } - } - if (after != null) { - sqlBuilder = sqlBuilder.append(" AFTER `" + after + "`"); - } + + String name = (column.has("name")) ? column.get("name").getAsString() : null; + String type = (column.has("type")) ? column.get("type").getAsString() : null; + Integer length = (column.has("length")) ? column.get("length").getAsInt() : null; + Boolean mandatory = (column.has("mandatory")) ? column.get("mandatory").getAsBoolean() : false; + String after = (column.has("after")) ? column.get("after").getAsString() : null; + + String mysqlType = apiTypeToMySQL.get(type); + sqlBuilder = sqlBuilder.append(", ADD `" + name + "` " + mysqlType); + + if (type != null) { + if (type.equals("String") && length != null) { + sqlBuilder = sqlBuilder.append("(" + length + ")"); + } else if (type.equals("Decimal")) { + sqlBuilder = sqlBuilder.append("(19,6)"); + } + } + if (mandatory != null) { + if (mandatory) { + sqlBuilder = sqlBuilder.append(" NOT NULL"); + } else { + sqlBuilder = sqlBuilder.append(" DEFAULT NULL"); + } + } + if (after != null) { + sqlBuilder = sqlBuilder.append(" AFTER `" + after + "`"); + } } - private void parseDatatableColumnForDrop(final JsonObject column, StringBuilder sqlBuilder) { - - String name = (column.has("name")) ? column.get("name").getAsString() : null; - - sqlBuilder = sqlBuilder.append(", DROP COLUMN `" + name + "`"); - } + private void parseDatatableColumnForDrop(final JsonObject column, StringBuilder sqlBuilder) { + + String name = (column.has("name")) ? column.get("name").getAsString() : null; + + sqlBuilder = sqlBuilder.append(", DROP COLUMN `" + name + "`"); + } @Transactional @Override public void updateDatatable(final String datatableName, final JsonCommand command) { - context.authenticatedUser(); - this.fromApiJsonDeserializer.validateForUpdate(command.json()); + try { + context.authenticatedUser(); + this.fromApiJsonDeserializer.validateForUpdate(command.json()); + + JsonElement element = this.fromJsonHelper.parse(command.json()); + JsonArray changeColumns = this.fromJsonHelper.extractJsonArrayNamed("changeColumns", element); + JsonArray addColumns = this.fromJsonHelper.extractJsonArrayNamed("addColumns", element); + JsonArray dropColumns = this.fromJsonHelper.extractJsonArrayNamed("dropColumns", element); + String apptableName = this.fromJsonHelper.extractStringNamed("apptableName", element); + + validateDatatableName(datatableName); + + if (!StringUtils.isBlank(apptableName)) { + validateAppTable(apptableName); + + String oldApptableName = this.queryForApplicationTableName(datatableName); + if (!StringUtils.equals(oldApptableName, apptableName)) { + deregisterDatatable(datatableName); + registerDatatable(datatableName, apptableName); + } + } + + if (changeColumns == null && addColumns == null && dropColumns == null) { + return; + } + + StringBuilder sqlBuilder = new StringBuilder("ALTER TABLE `" + datatableName + "`"); + + if (changeColumns != null) { + for (JsonElement column : changeColumns) { + parseDatatableColumnForUpdate(column.getAsJsonObject(), sqlBuilder); + } + } + if (addColumns != null) { + for (JsonElement column : addColumns) { + parseDatatableColumnForAdd(column.getAsJsonObject(), sqlBuilder); + } + } + if (dropColumns != null) { + for (JsonElement column : dropColumns) { + parseDatatableColumnForDrop(column.getAsJsonObject(), sqlBuilder); + } + } + + // Remove the first comma, right after ALTER TABLE `datatable` + int indexOfFirstComma = sqlBuilder.indexOf(","); + if (indexOfFirstComma != -1) { + sqlBuilder = sqlBuilder.deleteCharAt(indexOfFirstComma); + } + this.jdbcTemplate.execute(sqlBuilder.toString()); + } catch (SQLGrammarException e) { + Throwable realCause = e.getCause(); - JsonElement element = this.fromJsonHelper.parse(command.json()); - JsonArray changeColumns = this.fromJsonHelper.extractJsonArrayNamed("changeColumns", element); - JsonArray addColumns = this.fromJsonHelper.extractJsonArrayNamed("addColumns", element); - JsonArray dropColumns = this.fromJsonHelper.extractJsonArrayNamed("dropColumns", element); - - validateDatatableName(datatableName); - validateDatatableExists(datatableName); - - StringBuilder sqlBuilder = new StringBuilder("ALTER TABLE `" + datatableName + "`"); - - for (JsonElement column : changeColumns) { - parseDatatableColumnForUpdate(column.getAsJsonObject(), sqlBuilder); - } - for (JsonElement column : addColumns) { - parseDatatableColumnForAdd(column.getAsJsonObject(), sqlBuilder); - } - for (JsonElement column : dropColumns) { - parseDatatableColumnForDrop(column.getAsJsonObject(), sqlBuilder); - } - - // Remove the first comma, right after ALTER TABLE `datatable` - sqlBuilder = sqlBuilder.deleteCharAt(sqlBuilder.indexOf(",")); - this.jdbcTemplate.execute(sqlBuilder.toString()); + if (realCause.getMessage().contains("Unknown column")) { + throw new PlatformDataIntegrityException( + "error.msg.datatable.column.missing", realCause.getMessage()); + } + } } @Transactional @Override public void deleteDatatable(final String datatableName) { - - context.authenticatedUser(); - - validateDatatableName(datatableName); - validateDatatableExists(datatableName); - deregisterDatatable(datatableName); - - String sql = "DROP TABLE `" + datatableName + "`"; - this.jdbcTemplate.execute(sql); + + try { + context.authenticatedUser(); + + validateDatatableName(datatableName); + deregisterDatatable(datatableName); + + String sql = "DROP TABLE `" + datatableName + "`"; + this.jdbcTemplate.execute(sql); + } catch (SQLGrammarException e) { + Throwable realCause = e.getCause(); + + if (realCause.getMessage().contains("Unknown table")) { + throw new PlatformDataIntegrityException( + "error.msg.datatable.missing", realCause.getMessage()); + } + } } @Transactional From c08d1635629c588bb6689d81be2c8c748f5d22e9 Mon Sep 17 00:00:00 2001 From: Keith Woodlock Date: Thu, 30 May 2013 12:50:38 +0100 Subject: [PATCH 10/49] MIFOSX-395: provide optional staffId and staffDisplayName to auth response. --- api-docs/apiLive.htm | 69 ++++++++++++++----- .../api/AuthenticationApiResource.java | 7 +- .../security/data/AuthenticatedUserData.java | 14 +++- .../organisation/staff/domain/Staff.java | 6 +- .../useradministration/domain/AppUser.java | 16 +++++ 5 files changed, 90 insertions(+), 22 deletions(-) diff --git a/api-docs/apiLive.htm b/api-docs/apiLive.htm index d53245bc0..e13755a53 100644 --- a/api-docs/apiLive.htm +++ b/api-docs/apiLive.htm @@ -90,7 +90,7 @@
- +

Overview

@@ -12720,8 +12720,7 @@ GET https://DomainName/api/v1/runreports/{reportName}
-   +  

Verify authentication

@@ -12736,24 +12735,60 @@ POST authentication?username=mifos&password=password Content-Type: application/json No Request Body +

Example response of autentication for user that is not linked with any staff.

{ - "username": "mifos", - "userId": 1, - "base64EncodedAuthenticationKey": "bWlmb3M6cGFzc3dvcmQ=", - "authenticated": true, - "roles": [ - { - "id": 1, - "name": "Super user", - "description": "This role provides all application permissions." - } - ], - "permissions": [ - "ALL_FUNCTIONS" - ] + "username": "mifos", + "userId": 1, + "base64EncodedAuthenticationKey": "bWlmb3M6cGFzc3dvcmQ=", + "authenticated": true, + "officeId": 1, + "officeName": "Head Office", + "roles": [ + { + "id": 1, + "name": "Super user", + "description": "This role provides all application permissions." + } + ], + "permissions": [ + "ALL_FUNCTIONS" + ] } +

Example response of autentication for user that is linked with a staff member and role.

+ +{ + "username": "mifos", + "userId": 1, + "base64EncodedAuthenticationKey": "bWlmb3M6cGFzc3dvcmQ=", + "authenticated": true, + "officeId": 1, + "officeName": "Head Office", + "staffId": 1, + "staffDisplayName": "Director, Program", + "organisationalRole": { + "id": 100, + "code": "staffOrganisationalRoleType.programDirector", + "value": "Program Director" + }, + "roles": [ + { + "id": 1, + "name": "Super user", + "description": "This role provides all application permissions." + } + ], + "permissions": [ + "ALL_FUNCTIONS" + ] +} + + + + + + POST authentication?username=mifos&password=fail Content-Type: application/json diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/api/AuthenticationApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/api/AuthenticationApiResource.java index 6d985da05..a679364c4 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/api/AuthenticationApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/api/AuthenticationApiResource.java @@ -76,9 +76,14 @@ public class AuthenticationApiResource { final Long officeId = principal.getOffice().getId(); final String officeName = principal.getOffice().getName(); + + final Long staffId = principal.getStaffId(); + final String staffDisplayName = principal.getStaffDisplayName(); + final EnumOptionData organisationalRole = principal.organisationalRoleData(); - authenticatedUserData = new AuthenticatedUserData(username, officeId, officeName, organisationalRole, roles, permissions, + authenticatedUserData = new AuthenticatedUserData(username, officeId, officeName, staffId, staffDisplayName, + organisationalRole, roles, permissions, principal.getId(), new String(base64EncodedAuthenticationKey)); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/data/AuthenticatedUserData.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/data/AuthenticatedUserData.java index a87920111..b787217c6 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/data/AuthenticatedUserData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/data/AuthenticatedUserData.java @@ -28,6 +28,10 @@ public class AuthenticatedUserData { @SuppressWarnings("unused") private final String officeName; @SuppressWarnings("unused") + private final Long staffId; + @SuppressWarnings("unused") + private final String staffDisplayName; + @SuppressWarnings("unused") private final EnumOptionData organisationalRole; @SuppressWarnings("unused") private final Collection roles; @@ -41,17 +45,21 @@ public class AuthenticatedUserData { this.authenticated = false; this.officeId = null; this.officeName = null; + this.staffId = null; + this.staffDisplayName = null; this.organisationalRole = null; this.roles = null; this.permissions = permissions; } - public AuthenticatedUserData(final String username, final Long officeId, final String officeName, - final EnumOptionData organisationalRole, final Collection roles, final Collection permissions, - final Long userId, final String base64EncodedAuthenticationKey) { + public AuthenticatedUserData(final String username, final Long officeId, final String officeName, final Long staffId, + final String staffDisplayName, final EnumOptionData organisationalRole, final Collection roles, + final Collection permissions, final Long userId, final String base64EncodedAuthenticationKey) { this.username = username; this.officeId = officeId; this.officeName = officeName; + this.staffId = staffId; + this.staffDisplayName = staffDisplayName; this.organisationalRole = organisationalRole; this.userId = userId; this.base64EncodedAuthenticationKey = base64EncodedAuthenticationKey; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/organisation/staff/domain/Staff.java b/mifosng-provider/src/main/java/org/mifosplatform/organisation/staff/domain/Staff.java index 79a3d8ddb..69d4c2df4 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/organisation/staff/domain/Staff.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/organisation/staff/domain/Staff.java @@ -31,7 +31,6 @@ public class Staff extends AbstractPersistable { @Column(name = "lastname", length = 50) private String lastname; - @SuppressWarnings("unused") @Column(name = "display_name", length = 100) private String displayName; @@ -47,6 +46,7 @@ public class Staff extends AbstractPersistable { @Column(name = "organisational_role_enum", nullable = true) private Integer organisationalRoleType; + @SuppressWarnings("unused") @ManyToOne @JoinColumn(name = "organisational_role_parent_staff_id", nullable = true) private Staff organisationalRoleParentStaff; @@ -158,4 +158,8 @@ public class Staff extends AbstractPersistable { public Long officeId() { return this.office.getId(); } + + public String displayName() { + return this.displayName; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/useradministration/domain/AppUser.java b/mifosng-provider/src/main/java/org/mifosplatform/useradministration/domain/AppUser.java index e5b88b0aa..befd04791 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/useradministration/domain/AppUser.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/useradministration/domain/AppUser.java @@ -465,4 +465,20 @@ public class AppUser extends AbstractPersistable implements PlatformUser { if (hasNotPermissionForDatatable(datatable, "READ")) { throw new NoAuthorizationException("Not authorised to read datatable: " + datatable); } } + + public Long getStaffId() { + Long staffId = null; + if (this.staff != null) { + staffId = this.staff.getId(); + } + return staffId; + } + + public String getStaffDisplayName() { + String staffDisplayName = null; + if (this.staff != null) { + staffDisplayName = this.staff.displayName(); + } + return staffDisplayName; + } } \ No newline at end of file From 4d473ac4dbaf8405d47b3b9b7777f92aa1c84001 Mon Sep 17 00:00:00 2001 From: Keith Woodlock Date: Thu, 30 May 2013 13:20:11 +0100 Subject: [PATCH 11/49] update savings api docs for withdrawal and annual fees. --- api-docs/apiLive.htm | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/api-docs/apiLive.htm b/api-docs/apiLive.htm index e13755a53..1d55f8b51 100644 --- a/api-docs/apiLive.htm +++ b/api-docs/apiLive.htm @@ -9734,6 +9734,23 @@ Content-Type: application/json Optional: If provided, used along with lockinPeriodFrequency to indicate the length of time that the savings account is 'locked in' and withdrawals are not allowed. 0=Days, 1=Weeks, 2=Months, 3=Years e.g. 6 Months + withdrawalFeeAmount + Optional: If provided, used along with withdrawalFeeType to indicate the amount to be charged on the account for withdrawing. e.g. 5 Flat or 5 % of Amount + + withdrawalFeeType + Optional: If provided, used along with withdrawalFeeAmount to indicate the amount to be charged on the account for withdrawing. 1=Flat, 2=% of Amount, e.g. 5 Flat or 5 % of Amount + + annualFeeAmount + Optional: If provided, used along with annualFeeOnMonthDay to indicate the amount to be charged on the account annually on a given day of month. e.g. 10 on 10 May. monthDayFormat indicates day and month formatting used e.g. "dd MMM" for 10 May + + + + annualFeeOnMonthDay + + Optional: If provided, used along with annualFeeAmount and monthDayFormat to indicate the amount to be charged on the account annually on a given day of month. e.g. 10 on 10 May. monthDayFormat indicates day and month formatting used e.g. "dd MMM" for 10 May + + + accountingRule Specifies if accounting is enabled for the particular @@ -10252,7 +10269,7 @@ Content-Type: application/json
- +
Optional Fields
minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, paymentChannelToFundSourceMappings
minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, withdrawalFeeAmount, withdrawalFeeType, annualFeeAmount, annualFeeOnMonthDay, monthDayFormat, paymentChannelToFundSourceMappings
@@ -10558,6 +10575,23 @@ Content-Type: application/json lockinPeriodFrequencyType Optional: If provided, used along with lockinPeriodFrequency to indicate the length of time that the savings account is 'locked in' and withdrawals are not allowed. 0=Days, 1=Weeks, 2=Months, 3=Years e.g. 6 Months + + withdrawalFeeAmount + Optional: If provided, used along with withdrawalFeeType to indicate the amount to be charged on the account for withdrawing. e.g. 5 Flat or 5 % of Amount + + withdrawalFeeType + Optional: If provided, used along with withdrawalFeeAmount to indicate the amount to be charged on the account for withdrawing. 1=Flat, 2=% of Amount, e.g. 5 Flat or 5 % of Amount + + annualFeeAmount + Optional: If provided, used along with annualFeeOnMonthDay to indicate the amount to be charged on the account annually on a given day of month. e.g. 10 on 10 May. monthDayFormat indicates day and month formatting used e.g. "dd MMM" for 10 May + + + + annualFeeOnMonthDay + + Optional: If provided, used along with annualFeeAmount and monthDayFormat to indicate the amount to be charged on the account annually on a given day of month. e.g. 10 on 10 May. monthDayFormat indicates day and month formatting used e.g. "dd MMM" for 10 May + +
@@ -10758,7 +10792,9 @@ Content-Type: application/json
- + +
Inherited from Product (if not provided)
nominalAnnualInterestRate, interestCompoundingPeriodType, interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType
nominalAnnualInterestRate, interestCompoundingPeriodType, interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, + lockinPeriodFrequencyType, withdrawalFeeAmount, withdrawalFeeType, annualFeeAmount, annualFeeOnMonthDay
From eb082d5ec500385c33f75f8c7b393afc10ab1c7b Mon Sep 17 00:00:00 2001 From: goutham-M Date: Thu, 30 May 2013 18:39:45 +0530 Subject: [PATCH 12/49] days period frequency for locking savings product fixed --- .../portfolio/savings/data/SavingsProductDataValidator.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/SavingsProductDataValidator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/SavingsProductDataValidator.java index ca7bbee5b..168b61cbe 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/SavingsProductDataValidator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/SavingsProductDataValidator.java @@ -122,14 +122,14 @@ public class SavingsProductDataValidator { final Integer lockinPeriodFrequencyType = fromApiJsonHelper.extractIntegerSansLocaleNamed( lockinPeriodFrequencyTypeParamName, element); baseDataValidator.reset().parameter(lockinPeriodFrequencyTypeParamName).value(lockinPeriodFrequencyType).notNull() - .inMinMaxRange(1, 3); + .inMinMaxRange(0, 3); } } if (this.fromApiJsonHelper.parameterExists(lockinPeriodFrequencyTypeParamName, element)) { final Integer lockinPeriodFrequencyType = fromApiJsonHelper.extractIntegerSansLocaleNamed(lockinPeriodFrequencyTypeParamName, element); - baseDataValidator.reset().parameter(lockinPeriodFrequencyTypeParamName).value(lockinPeriodFrequencyType).inMinMaxRange(1, 3); + baseDataValidator.reset().parameter(lockinPeriodFrequencyTypeParamName).value(lockinPeriodFrequencyType).inMinMaxRange(0, 3); if (lockinPeriodFrequencyType != null) { final Integer lockinPeriodFrequency = fromApiJsonHelper.extractIntegerWithLocaleNamed(lockinPeriodFrequencyParamName, @@ -259,7 +259,7 @@ public class SavingsProductDataValidator { if (this.fromApiJsonHelper.parameterExists(lockinPeriodFrequencyTypeParamName, element)) { final Integer lockinPeriodFrequencyType = fromApiJsonHelper.extractIntegerSansLocaleNamed(lockinPeriodFrequencyTypeParamName, element); - baseDataValidator.reset().parameter(lockinPeriodFrequencyTypeParamName).value(lockinPeriodFrequencyType).inMinMaxRange(1, 3); + baseDataValidator.reset().parameter(lockinPeriodFrequencyTypeParamName).value(lockinPeriodFrequencyType).inMinMaxRange(0, 3); } if (this.fromApiJsonHelper.parameterExists(withdrawalFeeAmountParamName, element)) { From b373458f5752b3cc5d11a2896242c3ac49b40843 Mon Sep 17 00:00:00 2001 From: mifoscontributer Date: Fri, 31 May 2013 18:50:18 +0530 Subject: [PATCH 13/49] Add multiple credit and debit journalentries with accoutnting rule --- .../glaccount/api/GLAccountsApiResource.java | 52 +++++++++------- .../glaccount/data/GLAccountData.java | 59 +++++++++++-------- .../data/GLAccountDataForLookup.java | 24 +++++--- .../GLAccountReadPlatformServiceImpl.java | 6 +- .../rule/api/AccountingRuleApiResource.java | 2 +- .../rule/data/AccountingRuleData.java | 22 ++++++- ...AccountingRuleReadPlatformServiceImpl.java | 38 ++++++++---- 7 files changed, 132 insertions(+), 71 deletions(-) diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/api/GLAccountsApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/api/GLAccountsApiResource.java index 904bfbc87..02a150463 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/api/GLAccountsApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/api/GLAccountsApiResource.java @@ -50,9 +50,10 @@ import org.springframework.stereotype.Component; public class GLAccountsApiResource { private static final Set RESPONSE_DATA_PARAMETERS = new HashSet(Arrays.asList("id", "name", "parentId", "glCode", - "disabled", "manualEntriesAllowed", "type", "usage", "description", "assetHeaderAccountOptions", "liabilityHeaderAccountOptions", - "equityHeaderAccountOptions", "incomeHeaderAccountOptions", "expenseHeaderAccountOptions", "nameDecorated", "tagId", - "allowedAssetsTagOptions", "allowedLiabilitiesTagOptions", "allowedEquityTagOptions", "allowedIncomeTagOptions", "allowedExpensesTagOptions")); + "disabled", "manualEntriesAllowed", "type", "usage", "description", "assetHeaderAccountOptions", + "liabilityHeaderAccountOptions", "equityHeaderAccountOptions", "incomeHeaderAccountOptions", "expenseHeaderAccountOptions", + "nameDecorated", "tagId", "allowedAssetsTagOptions", "allowedLiabilitiesTagOptions", "allowedEquityTagOptions", + "allowedIncomeTagOptions", "allowedExpensesTagOptions", "creditAccounts", "debitAccounts")); private final String resourceNameForPermission = "GLACCOUNT"; @@ -63,12 +64,13 @@ public class GLAccountsApiResource { private final PlatformSecurityContext context; private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; private final CodeValueReadPlatformService codeValueReadPlatformService; - + @Autowired public GLAccountsApiResource(final PlatformSecurityContext context, final GLAccountReadPlatformService glAccountReadPlatformService, final DefaultToApiJsonSerializer toApiJsonSerializer, final ApiRequestParameterHelper apiRequestParameterHelper, final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService, - final AccountingDropdownReadPlatformService dropdownReadPlatformService, final CodeValueReadPlatformService codeValueReadPlatformService) { + final AccountingDropdownReadPlatformService dropdownReadPlatformService, + final CodeValueReadPlatformService codeValueReadPlatformService) { this.context = context; this.apiRequestParameterHelper = apiRequestParameterHelper; this.commandsSourceWritePlatformService = commandsSourceWritePlatformService; @@ -82,7 +84,7 @@ public class GLAccountsApiResource { @Path("template") @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) - public String retrieveNewAccountDetails(@Context final UriInfo uriInfo, @QueryParam("type") final Integer type) { + public String retrieveNewAccountDetails(@Context final UriInfo uriInfo, @QueryParam("type") final Integer type) { this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermission); @@ -167,22 +169,32 @@ public class GLAccountsApiResource { private GLAccountData handleTemplate(final GLAccountData glAccountData) { final List accountTypeOptions = this.dropdownReadPlatformService.retrieveGLAccountTypeOptions(); final List usageOptions = this.dropdownReadPlatformService.retrieveGLAccountUsageOptions(); - final List assetHeaderAccountOptions = defaultIfEmpty(this.glAccountReadPlatformService.retrieveAllEnabledHeaderGLAccounts(GLAccountType.ASSET)); - final List liabilityHeaderAccountOptions = defaultIfEmpty(this.glAccountReadPlatformService.retrieveAllEnabledHeaderGLAccounts(GLAccountType.LIABILITY)); - final List equityHeaderAccountOptions = defaultIfEmpty(this.glAccountReadPlatformService.retrieveAllEnabledHeaderGLAccounts(GLAccountType.EQUITY)); - final List incomeHeaderAccountOptions = defaultIfEmpty(this.glAccountReadPlatformService.retrieveAllEnabledHeaderGLAccounts(GLAccountType.INCOME)); - final List expenseHeaderAccountOptions = defaultIfEmpty(this.glAccountReadPlatformService.retrieveAllEnabledHeaderGLAccounts(GLAccountType.EXPENSE)); - final Collection allowedAssetsTagOptions = this.codeValueReadPlatformService.retrieveCodeValuesByCode(AccountingConstants.ASSESTS_TAG_OPTION_CODE_NAME); - final Collection allowedLiabilitiesTagOptions = this.codeValueReadPlatformService.retrieveCodeValuesByCode(AccountingConstants.LIABILITIES_TAG_OPTION_CODE_NAME); - final Collection allowedEquityTagOptions = this.codeValueReadPlatformService.retrieveCodeValuesByCode(AccountingConstants.EQUITY_TAG_OPTION_CODE_NAME); - final Collection allowedIncomeTagOptions = this.codeValueReadPlatformService.retrieveCodeValuesByCode(AccountingConstants.INCOME_TAG_OPTION_CODE_NAME); - final Collection allowedExpensesTagOptions = this.codeValueReadPlatformService.retrieveCodeValuesByCode(AccountingConstants.EXPENSES_TAG_OPTION_CODE_NAME); - + final List assetHeaderAccountOptions = defaultIfEmpty(this.glAccountReadPlatformService + .retrieveAllEnabledHeaderGLAccounts(GLAccountType.ASSET)); + final List liabilityHeaderAccountOptions = defaultIfEmpty(this.glAccountReadPlatformService + .retrieveAllEnabledHeaderGLAccounts(GLAccountType.LIABILITY)); + final List equityHeaderAccountOptions = defaultIfEmpty(this.glAccountReadPlatformService + .retrieveAllEnabledHeaderGLAccounts(GLAccountType.EQUITY)); + final List incomeHeaderAccountOptions = defaultIfEmpty(this.glAccountReadPlatformService + .retrieveAllEnabledHeaderGLAccounts(GLAccountType.INCOME)); + final List expenseHeaderAccountOptions = defaultIfEmpty(this.glAccountReadPlatformService + .retrieveAllEnabledHeaderGLAccounts(GLAccountType.EXPENSE)); + final Collection allowedAssetsTagOptions = this.codeValueReadPlatformService + .retrieveCodeValuesByCode(AccountingConstants.ASSESTS_TAG_OPTION_CODE_NAME); + final Collection allowedLiabilitiesTagOptions = this.codeValueReadPlatformService + .retrieveCodeValuesByCode(AccountingConstants.LIABILITIES_TAG_OPTION_CODE_NAME); + final Collection allowedEquityTagOptions = this.codeValueReadPlatformService + .retrieveCodeValuesByCode(AccountingConstants.EQUITY_TAG_OPTION_CODE_NAME); + final Collection allowedIncomeTagOptions = this.codeValueReadPlatformService + .retrieveCodeValuesByCode(AccountingConstants.INCOME_TAG_OPTION_CODE_NAME); + final Collection allowedExpensesTagOptions = this.codeValueReadPlatformService + .retrieveCodeValuesByCode(AccountingConstants.EXPENSES_TAG_OPTION_CODE_NAME); + return new GLAccountData(glAccountData, accountTypeOptions, usageOptions, assetHeaderAccountOptions, liabilityHeaderAccountOptions, - equityHeaderAccountOptions, incomeHeaderAccountOptions, expenseHeaderAccountOptions, allowedAssetsTagOptions, + equityHeaderAccountOptions, incomeHeaderAccountOptions, expenseHeaderAccountOptions, allowedAssetsTagOptions, allowedLiabilitiesTagOptions, allowedEquityTagOptions, allowedIncomeTagOptions, allowedExpensesTagOptions); } - + private List defaultIfEmpty(final List list) { List returnList = null; if (list != null && !list.isEmpty()) { @@ -190,4 +202,4 @@ public class GLAccountsApiResource { } return returnList; } -} \ No newline at end of file +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/data/GLAccountData.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/data/GLAccountData.java index 4d3e9d3d3..004431078 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/data/GLAccountData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/data/GLAccountData.java @@ -48,6 +48,10 @@ public class GLAccountData { final Collection allowedIncomeTagOptions; final Collection allowedExpensesTagOptions; + // for predefined journal entry template + private final List creditAccounts; + private final List debitAccounts; + public GLAccountData(final Long id, final String name, final Long parentId, final String glCode, final boolean disabled, final boolean manualEntriesAllowed, final EnumOptionData type, final EnumOptionData usage, final String description, final String nameDecorated, final CodeValueData tagId) { @@ -71,17 +75,19 @@ public class GLAccountData { this.expenseHeaderAccountOptions = null; this.allowedAssetsTagOptions = null; this.allowedLiabilitiesTagOptions = null; - this.allowedEquityTagOptions=null; - this.allowedIncomeTagOptions=null; - this.allowedExpensesTagOptions=null; + this.allowedEquityTagOptions = null; + this.allowedIncomeTagOptions = null; + this.allowedExpensesTagOptions = null; + this.creditAccounts = null; + this.debitAccounts = null; } public GLAccountData(final GLAccountData accountData, final List accountTypeOptions, final List usageOptions, final List assetHeaderAccountOptions, final List liabilityHeaderAccountOptions, final List equityHeaderAccountOptions, - final List incomeHeaderAccountOptions, final List expenseHeaderAccountOptions, - final Collection allowedAssetsTagOptions, final Collection allowedLiabilitiesTagOptions, - final Collection allowedEquityTagOptions, final Collection allowedIncomeTagOptions, + final List incomeHeaderAccountOptions, final List expenseHeaderAccountOptions, + final Collection allowedAssetsTagOptions, final Collection allowedLiabilitiesTagOptions, + final Collection allowedEquityTagOptions, final Collection allowedIncomeTagOptions, final Collection allowedExpensesTagOptions) { this.id = accountData.id; this.name = accountData.name; @@ -106,6 +112,8 @@ public class GLAccountData { this.allowedEquityTagOptions = allowedEquityTagOptions; this.allowedIncomeTagOptions = allowedIncomeTagOptions; this.allowedExpensesTagOptions = allowedExpensesTagOptions; + this.creditAccounts = accountData.creditAccounts; + this.debitAccounts = accountData.debitAccounts; } public static GLAccountData sensibleDefaultsForNewGLAccountCreation(final Integer glAccType) { @@ -116,19 +124,20 @@ public class GLAccountData { final boolean disabled = false; final boolean manualEntriesAllowed = true; final EnumOptionData type; - if(glAccType != null && glAccType >= GLAccountType.getMinValue() && glAccType <= GLAccountType.getMaxValue()) + if (glAccType != null && glAccType >= GLAccountType.getMinValue() && glAccType <= GLAccountType.getMaxValue()) { type = AccountingEnumerations.gLAccountType(glAccType); - else + } else { type = AccountingEnumerations.gLAccountType(GLAccountType.ASSET); + } final EnumOptionData usage = AccountingEnumerations.gLAccountUsage(GLAccountUsage.DETAIL); final String description = null; final String nameDecorated = null; final CodeValueData tagId = null; return new GLAccountData(id, name, parentId, glCode, disabled, manualEntriesAllowed, type, usage, description, nameDecorated, tagId); } - + public GLAccountData(final Long id, final String name, final String glCode) { - this.id = id; + this.id = id; this.name = name; this.parentId = null; this.glCode = glCode; @@ -148,23 +157,23 @@ public class GLAccountData { this.expenseHeaderAccountOptions = null; this.allowedAssetsTagOptions = null; this.allowedLiabilitiesTagOptions = null; - this.allowedEquityTagOptions=null; - this.allowedIncomeTagOptions=null; - this.allowedExpensesTagOptions=null; - } + this.allowedEquityTagOptions = null; + this.allowedIncomeTagOptions = null; + this.allowedExpensesTagOptions = null; + this.creditAccounts = null; + this.debitAccounts = null; + } - public Long getId() { - return this.id; - } + public Long getId() { + return this.id; + } - public String getName() { - return this.name; - } + public String getName() { + return this.name; + } - public String getGlCode() { - return this.glCode; - } - - + public String getGlCode() { + return this.glCode; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/data/GLAccountDataForLookup.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/data/GLAccountDataForLookup.java index 94e0a537d..751a803f0 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/data/GLAccountDataForLookup.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/data/GLAccountDataForLookup.java @@ -1,15 +1,21 @@ package org.mifosplatform.accounting.glaccount.data; public class GLAccountDataForLookup { - - private final Long id; - - public GLAccountDataForLookup(final Long id) { - this.id = id; - } - public Long getId() { - return this.id; - } + private final Long id; + @SuppressWarnings("unused") + private final String name; + @SuppressWarnings("unused") + private final String glCode; + + public GLAccountDataForLookup(final Long id, final String name, final String glCode) { + this.id = id; + this.name = name; + this.glCode = glCode; + } + + public Long getId() { + return this.id; + } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/service/GLAccountReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/service/GLAccountReadPlatformServiceImpl.java index 5f85a5c77..87b9078cd 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/service/GLAccountReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/service/GLAccountReadPlatformServiceImpl.java @@ -214,13 +214,15 @@ public class GLAccountReadPlatformServiceImpl implements GLAccountReadPlatformSe private static final class GLAccountDataLookUpMapper implements RowMapper { public String schema() { - return " gl.id as id from acc_accounting_rule rule join acc_rule_tags tags on tags.acc_rule_id = rule.id join acc_gl_account gl on gl.tag_id=tags.tag_id"; + return " gl.id as id, gl.name as name, gl.gl_code as glCode from acc_accounting_rule rule join acc_rule_tags tags on tags.acc_rule_id = rule.id join acc_gl_account gl on gl.tag_id=tags.tag_id"; } @Override public GLAccountDataForLookup mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { final Long id = JdbcSupport.getLong(rs, "id"); - return new GLAccountDataForLookup(id); + final String name = rs.getString("name"); + final String glCode = rs.getString("glCode"); + return new GLAccountDataForLookup(id, name, glCode); } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/api/AccountingRuleApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/api/AccountingRuleApiResource.java index 961847d75..57c8746b5 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/api/AccountingRuleApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/api/AccountingRuleApiResource.java @@ -53,7 +53,7 @@ public class AccountingRuleApiResource { private static final Set RESPONSE_DATA_PARAMETERS = new HashSet(Arrays.asList("id", "officeId", "officeName", "accountToDebitId", "accountToCreditId", "name", "description", "systemDefined", "allowedAssetsTagOptions", "allowedLiabilitiesTagOptions", "allowedEquityTagOptions", "allowedIncomeTagOptions", "allowedExpensesTagOptions", "debitTags", - "creditTags")); + "creditTags", "creditAccounts", "debitAccounts")); private final String resourceNameForPermission = "ACCOUNTINGRULE"; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/data/AccountingRuleData.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/data/AccountingRuleData.java index 9b8d1acdb..42aca2773 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/data/AccountingRuleData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/data/AccountingRuleData.java @@ -10,6 +10,7 @@ import java.util.Collection; import java.util.List; import org.mifosplatform.accounting.glaccount.data.GLAccountData; +import org.mifosplatform.accounting.glaccount.data.GLAccountDataForLookup; import org.mifosplatform.infrastructure.codes.data.CodeValueData; import org.mifosplatform.organisation.office.data.OfficeData; @@ -40,6 +41,8 @@ public class AccountingRuleData { private final Collection allowedEquityTagOptions; private final Collection allowedIncomeTagOptions; private final Collection allowedExpensesTagOptions; + private final List creditAccounts; + private final List debitAccounts; public AccountingRuleData(final AccountingRuleData accountingRuleData, final List allowedAccounts, final List allowedOffices, final Collection allowedAssetsTagOptions, @@ -62,6 +65,8 @@ public class AccountingRuleData { this.allowedExpensesTagOptions = allowedExpensesTagOptions; this.creditTags = accountingRuleData.creditTags; this.debitTags = accountingRuleData.debitTags; + this.creditAccounts = accountingRuleData.creditAccounts; + this.debitAccounts = accountingRuleData.debitAccounts; } public AccountingRuleData(final List allowedAccounts, final List allowedOffices, @@ -85,6 +90,8 @@ public class AccountingRuleData { this.allowedExpensesTagOptions = allowedExpensesTagOptions; this.creditTags = null; this.debitTags = null; + this.creditAccounts = null; + this.debitAccounts = null; } public AccountingRuleData(final Long id, final Long officeId, final String officeName, final String name, final String description, @@ -106,10 +113,13 @@ public class AccountingRuleData { this.allowedExpensesTagOptions = null; this.creditTags = null; this.debitTags = null; + this.creditAccounts = new ArrayList(); + this.debitAccounts = new ArrayList(); } public AccountingRuleData(final AccountingRuleData accountingRuleData, final List creditTags, - final List debitTags) { + final List creditAccounts, final List debitTags, + final List debitAccounts) { this.id = accountingRuleData.id; this.officeId = accountingRuleData.officeId; this.officeName = accountingRuleData.officeName; @@ -126,7 +136,9 @@ public class AccountingRuleData { this.allowedIncomeTagOptions = accountingRuleData.allowedIncomeTagOptions; this.allowedExpensesTagOptions = accountingRuleData.allowedExpensesTagOptions; this.creditTags = creditTags; + this.creditAccounts = creditAccounts; this.debitTags = debitTags; + this.debitAccounts = debitAccounts; } public GLAccountData getDebitAccountHead() { @@ -144,4 +156,12 @@ public class AccountingRuleData { public List getDebitTags() { return this.debitTags; } + + public List getCreditAccounts() { + return this.creditAccounts; + } + + public List getDebitAccounts() { + return this.debitAccounts; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleReadPlatformServiceImpl.java index de1a9d6d3..e87499023 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleReadPlatformServiceImpl.java @@ -14,6 +14,8 @@ import java.util.Map; import org.mifosplatform.accounting.common.AccountingEnumerations; import org.mifosplatform.accounting.glaccount.data.GLAccountData; +import org.mifosplatform.accounting.glaccount.data.GLAccountDataForLookup; +import org.mifosplatform.accounting.glaccount.service.GLAccountReadPlatformService; import org.mifosplatform.accounting.journalentry.domain.JournalEntryType; import org.mifosplatform.accounting.rule.data.AccountingRuleData; import org.mifosplatform.accounting.rule.data.AccountingTagRuleData; @@ -33,10 +35,13 @@ import org.springframework.stereotype.Service; public class AccountingRuleReadPlatformServiceImpl implements AccountingRuleReadPlatformService { private final JdbcTemplate jdbcTemplate; + private final GLAccountReadPlatformService glAccountReadPlatformService; @Autowired - public AccountingRuleReadPlatformServiceImpl(final TenantAwareRoutingDataSource dataSource) { + public AccountingRuleReadPlatformServiceImpl(final TenantAwareRoutingDataSource dataSource, + final GLAccountReadPlatformService glAccountReadPlatformService) { this.jdbcTemplate = new JdbcTemplate(dataSource); + this.glAccountReadPlatformService = glAccountReadPlatformService; } // Will remove the old one if everything goes well. @@ -88,13 +93,15 @@ public class AccountingRuleReadPlatformServiceImpl implements AccountingRuleRead } }*/ - private static final class AccountingRuleDataExtractor implements ResultSetExtractor { + private static final class AccountingRuleDataExtractor implements ResultSetExtractor> { private final String schemaSql; private final JdbcTemplate jdbcTemplate; + private final GLAccountReadPlatformService glAccountReadPlatformService; - public AccountingRuleDataExtractor(final JdbcTemplate jdbcTemplate) { + public AccountingRuleDataExtractor(final JdbcTemplate jdbcTemplate, final GLAccountReadPlatformService glAccountReadPlatformService) { this.jdbcTemplate = jdbcTemplate; + this.glAccountReadPlatformService = glAccountReadPlatformService; final StringBuilder sqlBuilder = new StringBuilder(400); sqlBuilder .append(" rule.id as id,rule.name as name, rule.office_id as officeId,office.name as officeName,") @@ -141,11 +148,17 @@ public class AccountingRuleReadPlatformServiceImpl implements AccountingRuleRead } if (accountingRuleData.getCreditAccountHead() == null) { final List creditTags = getCreditOrDebitTags(id, JournalEntryType.CREDIT.getValue()); - accountingRuleData = new AccountingRuleData(accountingRuleData, creditTags, accountingRuleData.getDebitTags()); + final List creditAccounts = this.glAccountReadPlatformService.retrieveAccountsByTagId(id, + JournalEntryType.CREDIT.getValue()); + accountingRuleData = new AccountingRuleData(accountingRuleData, creditTags, creditAccounts, + accountingRuleData.getDebitTags(), accountingRuleData.getDebitAccounts()); } if (accountingRuleData.getDebitAccountHead() == null) { final List debitTags = getCreditOrDebitTags(id, JournalEntryType.DEBIT.getValue()); - accountingRuleData = new AccountingRuleData(accountingRuleData, accountingRuleData.getCreditTags(), debitTags); + final List debitAccounts = this.glAccountReadPlatformService.retrieveAccountsByTagId(id, + JournalEntryType.DEBIT.getValue()); + accountingRuleData = new AccountingRuleData(accountingRuleData, accountingRuleData.getCreditTags(), + accountingRuleData.getCreditAccounts(), debitTags, debitAccounts); } extractedData.put(id, accountingRuleData); } @@ -162,7 +175,8 @@ public class AccountingRuleReadPlatformServiceImpl implements AccountingRuleRead @Override public List retrieveAllAccountingRules(final Long officeId) { - final AccountingRuleDataExtractor resultSetExtractor = new AccountingRuleDataExtractor(this.jdbcTemplate); + final AccountingRuleDataExtractor resultSetExtractor = new AccountingRuleDataExtractor(this.jdbcTemplate, + this.glAccountReadPlatformService); Object[] arguments = new Object[] {}; String sql = "select " + resultSetExtractor.schema() + " and system_defined=0 "; if (officeId != null) { @@ -170,21 +184,19 @@ public class AccountingRuleReadPlatformServiceImpl implements AccountingRuleRead arguments = new Object[] { officeId }; } sql = sql + " order by rule.id asc"; - @SuppressWarnings("unchecked") - final Map extractedData = (Map) this.jdbcTemplate.query(sql, - resultSetExtractor, arguments); + final Map extractedData = this.jdbcTemplate.query(sql, resultSetExtractor, arguments); return new ArrayList(extractedData.values()); } @Override public AccountingRuleData retrieveAccountingRuleById(final Long accountingRuleId) { try { - final AccountingRuleDataExtractor resultSetExtractor = new AccountingRuleDataExtractor(this.jdbcTemplate); + final AccountingRuleDataExtractor resultSetExtractor = new AccountingRuleDataExtractor(this.jdbcTemplate, + this.glAccountReadPlatformService); final String sql = "select " + resultSetExtractor.schema() + " and rule.id = ?"; - @SuppressWarnings("unchecked") - final Map extractedData = (Map) this.jdbcTemplate.query(sql, - resultSetExtractor, new Object[] { accountingRuleId }); + final Map extractedData = this.jdbcTemplate.query(sql, resultSetExtractor, + new Object[] { accountingRuleId }); final AccountingRuleData accountingRuleData = extractedData.get(accountingRuleId); if (accountingRuleData == null) { throw new AccountingRuleNotFoundException(accountingRuleId); } return accountingRuleData; From 7478e17f3c4805e88eab7aef7bb9d97ea42f88a5 Mon Sep 17 00:00:00 2001 From: goutham-M Date: Fri, 31 May 2013 17:22:58 +0530 Subject: [PATCH 14/49] updating annual fee amount in savings product(exception) fixed --- .../mifosplatform/infrastructure/core/api/JsonCommand.java | 6 +++++- .../portfolio/savings/domain/SavingsProduct.java | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/api/JsonCommand.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/api/JsonCommand.java index 54f97801a..cd38457fa 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/api/JsonCommand.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/api/JsonCommand.java @@ -199,7 +199,11 @@ public final class JsonCommand { boolean differenceExists = false; if (baseValue != null) { - differenceExists = baseValue.compareTo(workingCopyValue) != 0; + if(workingCopyValue != null) { + differenceExists = baseValue.compareTo(workingCopyValue) != 0; + }else { + differenceExists = true; + } } else { differenceExists = workingCopyValue != null; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsProduct.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsProduct.java index a6e8297fd..115e11f5b 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsProduct.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsProduct.java @@ -353,7 +353,7 @@ public class SavingsProduct extends AbstractPersistable { if (command.isChangeInIntegerParameterNamed(annualFeeOnMonthDayParamName, this.annualFeeOnDay)) { final MonthDay monthDay = command.extractMonthDayNamed(annualFeeOnMonthDayParamName); final String actualValueEntered = command.stringValueOfParameterNamed(annualFeeOnMonthDayParamName); - final Integer newValue = monthDay.getDayOfMonth(); + final Integer newValue = monthDay != null? monthDay.getDayOfMonth(): null; actualChanges.put(annualFeeOnMonthDayParamName, actualValueEntered); actualChanges.put(localeParamName, localeAsInput); this.annualFeeOnDay = newValue; @@ -362,7 +362,7 @@ public class SavingsProduct extends AbstractPersistable { if (command.isChangeInIntegerParameterNamed(annualFeeOnMonthDayParamName, this.annualFeeOnMonth)) { final MonthDay monthDay = command.extractMonthDayNamed(annualFeeOnMonthDayParamName); final String actualValueEntered = command.stringValueOfParameterNamed(annualFeeOnMonthDayParamName); - final Integer newValue = monthDay.getMonthOfYear(); + final Integer newValue = monthDay != null? monthDay.getMonthOfYear(): null; actualChanges.put(annualFeeOnMonthDayParamName, actualValueEntered); actualChanges.put(localeParamName, localeAsInput); this.annualFeeOnMonth = newValue; From 4dbd0cf7fdcef2f55918f8f56b578b517a53f232 Mon Sep 17 00:00:00 2001 From: goutham-M Date: Fri, 31 May 2013 19:32:30 +0530 Subject: [PATCH 15/49] updating withdraw fee in saving product(exception) fixed --- .../mifosplatform/infrastructure/core/api/JsonCommand.java | 6 +++++- .../portfolio/savings/domain/SavingsProduct.java | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/api/JsonCommand.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/api/JsonCommand.java index cd38457fa..60e21761f 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/api/JsonCommand.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/api/JsonCommand.java @@ -187,7 +187,11 @@ public final class JsonCommand { boolean differenceExists = false; if (baseValue != null) { - differenceExists = !baseValue.equals(workingCopyValue); + if(workingCopyValue != null) { + differenceExists = !baseValue.equals(workingCopyValue); + } else { + differenceExists = true; + } } else { differenceExists = workingCopyValue != null; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsProduct.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsProduct.java index 115e11f5b..cf19db5b0 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsProduct.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsProduct.java @@ -340,7 +340,7 @@ public class SavingsProduct extends AbstractPersistable { if (command.isChangeInIntegerParameterNamed(withdrawalFeeTypeParamName, this.withdrawalFeeType)) { final Integer newValue = command.integerValueOfParameterNamed(withdrawalFeeTypeParamName); actualChanges.put(withdrawalFeeTypeParamName, newValue); - this.withdrawalFeeType = SavingsWithdrawalFeesType.fromInt(newValue).getValue(); + this.withdrawalFeeType = newValue != null? SavingsWithdrawalFeesType.fromInt(newValue).getValue(): newValue; } if (command.isChangeInBigDecimalParameterNamed(annualFeeAmountParamName, this.annualFeeAmount)) { From 01d0772261e07743ca7f37ca66cca2316bdfd12f Mon Sep 17 00:00:00 2001 From: goutham-M Date: Sat, 1 Jun 2013 15:34:23 +0530 Subject: [PATCH 16/49] savings ac getting activated before client activation date fixed --- .../portfolio/savings/domain/SavingsAccount.java | 9 +++++++++ ...InvalidSavingsAccountStateTransitionException.java | 11 +++++++++++ 2 files changed, 20 insertions(+) create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/exception/InvalidSavingsAccountStateTransitionException.java diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccount.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccount.java index fc03c6fed..15b487e54 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccount.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccount.java @@ -58,6 +58,7 @@ import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType; import org.mifosplatform.portfolio.paymentdetail.domain.PaymentDetail; import org.mifosplatform.portfolio.savings.api.SavingsApiConstants; import org.mifosplatform.portfolio.savings.exception.InsufficientAccountBalanceException; +import org.mifosplatform.portfolio.savings.exception.InvalidSavingsAccountStateTransitionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.jpa.domain.AbstractPersistable; @@ -300,6 +301,14 @@ public class SavingsAccount extends AbstractPersistable { throw new PlatformApiDataValidationException(dataValidationErrors); } + + if(this.client.getActivationLocalDate().isAfter(activationDate)) + { + final String errorMessage = "The date on which a savings account is activate cannot be before its client activated date: " + + this.client.getActivationLocalDate().toString(); + throw new InvalidSavingsAccountStateTransitionException("activate", "cannot.be.before.client.activation.date", errorMessage, activationDate, + this.client.getActivationLocalDate()); + } this.activationDate = activationDate.toDate(); this.status = SavingsAccountStatusType.ACTIVE.getValue(); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/exception/InvalidSavingsAccountStateTransitionException.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/exception/InvalidSavingsAccountStateTransitionException.java new file mode 100644 index 000000000..e6071f8f2 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/exception/InvalidSavingsAccountStateTransitionException.java @@ -0,0 +1,11 @@ +package org.mifosplatform.portfolio.savings.exception; + +import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class InvalidSavingsAccountStateTransitionException extends AbstractPlatformDomainRuleException { + + public InvalidSavingsAccountStateTransitionException(final String action, String postFix, String defaultUserMessage, Object... defaultUserMessageArgs) { + super("error.msg.saving." + action + "." + postFix, defaultUserMessage, defaultUserMessageArgs); + } + +} From 9c1f9cd912ea58b42f07da23c963c50292c05a06 Mon Sep 17 00:00:00 2001 From: Harshac Date: Thu, 25 Apr 2013 18:31:38 +0530 Subject: [PATCH 17/49] Adding ability to upload files on s3. To enable file upload on S3 you need to enable the configuration setting in c_configuration table for s3. The S3 credentials (access_key, secret_key, bucket_name) in a m_external_services table. m_client table is normalised into m_client and m_image table. The relation between m_client and m_image is one-to-one for now. To persist the storage type(s3 or filesystem) a column had been added to m_document. MISOFX-69 Adding db migrations for s3 support MIFOSX-69 Adding method to return boolean value of S3 config MIFOSX-69 - Priti/Harsha - Refactoring Code, extracted interface for Documents upload MIFOSX-69 Refactored to add S3 Support for file upload. Modified saveDocs to throw Document Management Exception instead of IO exception MIFOSX-69 Adding S3 support for file upload MIFOSX-69 - Adding S3 support for Image upload MIFOSX-69 Adding factory to get instance of data store as per s3 configuration MIFOSX-69 Changing table s3_details to generic tabe for external services, adding column to store the storage type in m_documents table MIFOSX-69 Fetching S3 details from database MIFOSX-69 Adding document storage type while creating a document MIFOSX-69 Normalising client table to create an image table Refactoring ClientImage code to save client image in m_image table Refactored code to get document as file MIFOSX-69 Retreiving documents from S3 MIFOSX-69 Refactored ClientData to store ImageData MIFOSX-69 Retrieving images from S3, Modifying Base64 to encode from byte array instead of file in CLientImagesApiResource MIFOSX-69 Refactored code to delete client image MIFOSX-69 Deleting Images from S3 Refactored DocumentStore to return StoreType from string value MIFOSX-69 Refactored to move document deletion responsibility to storageType MIFOSX-69 Giving more appropriate name to getInstance methods of DocumentStoreFactory as per the instance creation criteria MIFOSX-69 Deleting Documents from S3 MIFOSX-69 Removing unnecessary s3_param from migration MIFOSX-69 Adding ability to update document and update doc store type MIFOSX-69 Renaming method to follow java conventions MIFOSX-69 Adding filename to error log for s3 file retrieval Adding data migrations and changing the version for migration file --- .../core_db/V40__adding_S3_Support.sql | 28 +++ .../domain/ConfigurationDomainService.java | 1 + .../domain/ConfigurationDomainServiceJpa.java | 5 + .../domain/ExternalServiceProperty.java | 37 +++ .../domain/ExternalServiceRepository.java | 10 + .../domain/ExternalServicesDomainService.java | 18 ++ .../core/service/DocumentStore.java | 52 ++++ .../core/service/DocumentStoreFactory.java | 45 ++++ .../core/service/DocumentStoreType.java | 15 ++ .../core/service/FileSystemDocumentStore.java | 160 ++++++++++++ .../core/service/FileUtils.java | 234 ------------------ .../core/service/ImageUtils.java | 105 ++++++++ .../core/service/RandomStringGenerator.java | 33 +++ .../core/service/S3ClientFactory.java | 24 ++ .../core/service/S3DocumentStore.java | 201 +++++++++++++++ .../service/ReadReportingServiceImpl.java | 6 +- .../api/DocumentManagementApiResource.java | 59 ++--- .../command/DocumentCommand.java | 10 + .../documentmanagement/data/DocumentData.java | 24 +- .../documentmanagement/data/FileData.java | 56 +++++ .../documentmanagement/domain/Document.java | 28 ++- .../service/DocumentReadPlatformService.java | 8 +- .../DocumentReadPlatformServiceImpl.java | 57 +++-- ...WritePlatformServiceJpaRepositoryImpl.java | 65 ++--- .../client/api/ClientImagesApiResource.java | 78 ++---- .../portfolio/client/data/ClientData.java | 55 ++-- .../portfolio/client/data/ImageData.java | 78 ++++++ .../portfolio/client/domain/Client.java | 39 +-- .../portfolio/client/domain/Image.java | 62 +++++ .../client/domain/ImageRepository.java | 13 + .../service/ClientReadPlatformService.java | 1 + .../ClientReadPlatformServiceImpl.java | 43 +++- .../service/ClientWritePlatformService.java | 2 +- ...WritePlatformServiceJpaRepositoryImpl.java | 92 +++---- .../ConfigurationDomainServiceJpaTest.java | 46 ++++ .../service/DocumentStoreFactoryTest.java | 55 ++++ .../core/service/FileDocumentStoreTest.java | 31 +++ .../core/service/S3DocumentStoreTest.java | 135 ++++++++++ 38 files changed, 1517 insertions(+), 494 deletions(-) create mode 100644 mifosng-db/migrations/core_db/V40__adding_S3_Support.sql create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ExternalServiceProperty.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ExternalServiceRepository.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ExternalServicesDomainService.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/DocumentStore.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/DocumentStoreFactory.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/DocumentStoreType.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/FileSystemDocumentStore.java delete mode 100644 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/FileUtils.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/ImageUtils.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/RandomStringGenerator.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/S3ClientFactory.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/S3DocumentStore.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/data/FileData.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/data/ImageData.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/domain/Image.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/domain/ImageRepository.java create mode 100644 mifosng-provider/src/test/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainServiceJpaTest.java create mode 100644 mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/DocumentStoreFactoryTest.java create mode 100644 mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/FileDocumentStoreTest.java create mode 100644 mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/S3DocumentStoreTest.java diff --git a/mifosng-db/migrations/core_db/V40__adding_S3_Support.sql b/mifosng-db/migrations/core_db/V40__adding_S3_Support.sql new file mode 100644 index 000000000..ff1e63dec --- /dev/null +++ b/mifosng-db/migrations/core_db/V40__adding_S3_Support.sql @@ -0,0 +1,28 @@ +INSERT INTO c_configuration (name,enabled) VALUES ('amazon-S3',0); + + +CREATE TABLE IF NOT EXISTS m_external_services ( + name VARCHAR(150) NOT NULL, + value VARCHAR(250), + UNIQUE(name) +); + +INSERT INTO m_external_services (name) VALUES ('s3_bucket_name'); +INSERT INTO m_external_services (name) VALUES ('s3_access_key'); +INSERT INTO m_external_services (name) VALUES ('s3_secret_key'); + +ALTER TABLE m_document ADD COLUMN storage_type varchar(50); + +CREATE TABLE IF NOT EXISTS `m_image`( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `client_id` BIGINT NOT NULL, + `key` varchar(500), + `storage_type` varchar(50), + FOREIGN KEY (`client_id`) REFERENCES m_client(`id`), + PRIMARY KEY (`id`), + UNIQUE(client_id) +); + +UPDATE m_document set storage_type='file_system'; + +insert into m_image (`client_id`,`key`,`storage_type`) select id,`image_key`,'file_system' from m_client where `image_key` IS NOT NULL; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainService.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainService.java index 1c63e5f35..81c4c9b17 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainService.java @@ -8,4 +8,5 @@ package org.mifosplatform.infrastructure.configuration.domain; public interface ConfigurationDomainService { boolean isMakerCheckerEnabledForTask(String taskPermissionCode); + boolean isAmazonS3Enabled(); } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java index 110c66b42..32107eca2 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java @@ -39,4 +39,9 @@ public class ConfigurationDomainServiceJpa implements ConfigurationDomainService return thisTask.hasMakerCheckerEnabled() && property.isEnabled(); } + + @Override + public boolean isAmazonS3Enabled(){ + return this.globalConfigurationRepository.findOneByName("amazon-S3").isEnabled(); + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ExternalServiceProperty.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ExternalServiceProperty.java new file mode 100644 index 000000000..e9c7ef86f --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ExternalServiceProperty.java @@ -0,0 +1,37 @@ +package org.mifosplatform.infrastructure.configuration.domain; +/** + * 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/. + */ + +import org.springframework.data.jpa.domain.AbstractPersistable; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +@Entity +@Table(name = "m_external_services", uniqueConstraints = { @UniqueConstraint(columnNames = ("name"))}) +public class ExternalServiceProperty extends AbstractPersistable { + + @SuppressWarnings("unused") + @Column(name = "name", nullable = false) + private String name; + + @Column(name = "value", nullable = false) + private String value; + + public ExternalServiceProperty(final String name, final String value) { + this.name = name; + this.value = value; + } + + public ExternalServiceProperty() { + } + + public String getValue(){ + return this.value; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ExternalServiceRepository.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ExternalServiceRepository.java new file mode 100644 index 000000000..8acc015d6 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ExternalServiceRepository.java @@ -0,0 +1,10 @@ +package org.mifosplatform.infrastructure.configuration.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface ExternalServiceRepository extends JpaRepository, + JpaSpecificationExecutor { + + ExternalServiceProperty findOneByName(String name); +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ExternalServicesDomainService.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ExternalServicesDomainService.java new file mode 100644 index 000000000..e562c9ee0 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ExternalServicesDomainService.java @@ -0,0 +1,18 @@ +package org.mifosplatform.infrastructure.configuration.domain; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class ExternalServicesDomainService { + private ExternalServiceRepository externalServiceRepository; + + @Autowired + public ExternalServicesDomainService(final ExternalServiceRepository externalServiceRepository) { + this.externalServiceRepository = externalServiceRepository; + } + + public String getValue(String name){ + return externalServiceRepository.findOneByName(name).getValue(); + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/DocumentStore.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/DocumentStore.java new file mode 100644 index 000000000..0722fd042 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/DocumentStore.java @@ -0,0 +1,52 @@ +package org.mifosplatform.infrastructure.core.service; + +import org.mifosplatform.infrastructure.core.api.ApiConstants; +import org.mifosplatform.infrastructure.core.domain.Base64EncodedImage; +import org.mifosplatform.infrastructure.documentmanagement.command.DocumentCommand; +import org.mifosplatform.infrastructure.documentmanagement.data.DocumentData; +import org.mifosplatform.infrastructure.documentmanagement.data.FileData; +import org.mifosplatform.infrastructure.documentmanagement.exception.DocumentManagementException; +import org.mifosplatform.infrastructure.documentmanagement.exception.DocumentNotFoundException; +import org.mifosplatform.portfolio.client.data.ImageData; + +import java.io.InputStream; + +public abstract class DocumentStore { + protected DocumentStoreType type; + + protected Integer maxFileSize = ApiConstants.MAX_FILE_UPLOAD_SIZE_IN_MB; + protected Integer maxImageSize = ApiConstants.MAX_IMAGE_UPLOAD_SIZE_IN_MB; + + public abstract String saveDocument(InputStream uploadedInputStream, DocumentCommand documentCommand) throws DocumentManagementException; + public abstract void deleteDocument(String documentName, String documentPath) throws DocumentManagementException; + public abstract FileData retrieveDocument(DocumentData documentData) throws DocumentNotFoundException; + + public abstract String saveImage(InputStream uploadedInputStream, Long resourceId, String imageName, Long fileSize) throws DocumentManagementException; + public abstract String saveImage(Base64EncodedImage base64EncodedImage, Long resourceId, String imageName) throws DocumentManagementException; + public abstract void deleteImage(final Long resourceId, final String location); + public abstract ImageData retrieveImage(ImageData imageData); + + public abstract DocumentStoreType getType(); + + protected void validateFileSizeWithinPermissibleRange(Long fileSize, String name, int maxFileSize) { + /** + * Using Content-Length gives me size of the entire request, which is + * good enough for now for a fast fail as the length of the rest of the + * content i.e name and description while compared to the uploaded file + * size is negligible + **/ + if (fileSize != null && ((fileSize / (1024 * 1024)) > maxFileSize)) { + throw new DocumentManagementException(name, fileSize, + maxFileSize); + } + } + + public static DocumentStoreType getDocumentStoreType(String documentStoreType){ + if (documentStoreType.equals(DocumentStoreType.FILE_SYSTEM.getValue())) { + return DocumentStoreType.FILE_SYSTEM; + } else { + return DocumentStoreType.S3; + } + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/DocumentStoreFactory.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/DocumentStoreFactory.java new file mode 100644 index 000000000..32b4b05bd --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/DocumentStoreFactory.java @@ -0,0 +1,45 @@ +package org.mifosplatform.infrastructure.core.service; + +import com.amazonaws.services.s3.AmazonS3Client; +import org.mifosplatform.infrastructure.configuration.domain.ConfigurationDomainService; +import org.mifosplatform.infrastructure.configuration.domain.ExternalServicesDomainService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +@Component +public class DocumentStoreFactory { + + private ApplicationContext applicationContext; + + @Autowired + public DocumentStoreFactory(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + public DocumentStore getInstanceFromConfiguration(){ + ConfigurationDomainService configurationDomainServiceJpa = applicationContext.getBean("configurationDomainServiceJpa", ConfigurationDomainService.class); + if(configurationDomainServiceJpa.isAmazonS3Enabled()){ + return createS3DocumentStore(); + } + else { + return new FileSystemDocumentStore(); + } + } + + public DocumentStore getInstanceFromStorageType(DocumentStoreType documentStoreType){ + if(documentStoreType == DocumentStoreType.FILE_SYSTEM){ + return new FileSystemDocumentStore(); + } + else { + return createS3DocumentStore(); + } + } + private DocumentStore createS3DocumentStore() { + AmazonS3Client s3Client = applicationContext.getBean("s3ClientFactory", S3ClientFactory.class).getS3Client(); + ExternalServicesDomainService externalServicesDomainService = applicationContext.getBean("externalServicesDomainService", ExternalServicesDomainService.class); + return new S3DocumentStore(externalServicesDomainService.getValue("s3_bucket_name"), s3Client); + } + + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/DocumentStoreType.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/DocumentStoreType.java new file mode 100644 index 000000000..ac6a34c01 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/DocumentStoreType.java @@ -0,0 +1,15 @@ +package org.mifosplatform.infrastructure.core.service; + +public enum DocumentStoreType { + FILE_SYSTEM("file_system"), S3("s3"); + + private String value; + + DocumentStoreType(String value){ + this.value = value; + } + + public String getValue() { + return value; + } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/FileSystemDocumentStore.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/FileSystemDocumentStore.java new file mode 100644 index 000000000..6e1a51c9f --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/FileSystemDocumentStore.java @@ -0,0 +1,160 @@ +/** + * 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.infrastructure.core.service; + +import com.lowagie.text.pdf.codec.Base64; +import org.mifosplatform.infrastructure.core.domain.Base64EncodedImage; +import org.mifosplatform.infrastructure.documentmanagement.command.DocumentCommand; +import org.mifosplatform.infrastructure.documentmanagement.data.DocumentData; +import org.mifosplatform.infrastructure.documentmanagement.data.FileData; +import org.mifosplatform.infrastructure.documentmanagement.exception.DocumentManagementException; +import org.mifosplatform.portfolio.client.data.ImageData; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.*; + +public class FileSystemDocumentStore extends DocumentStore { + + private final static Logger logger = LoggerFactory.getLogger(FileSystemDocumentStore.class); + + public static final String MIFOSX_BASE_DIR = System.getProperty("user.home") + File.separator + ".mifosx"; + + + @Override + public String saveDocument(InputStream uploadedInputStream, DocumentCommand documentCommand) throws DocumentManagementException { + String documentName = documentCommand.getFileName(); + String uploadDocumentLocation = generateFileParentDirectory(documentCommand.getParentEntityType(), documentCommand.getParentEntityId()); + + validateFileSizeWithinPermissibleRange(documentCommand.getSize(), documentName, maxFileSize); + makeDirectories(uploadDocumentLocation); + + String fileLocation = uploadDocumentLocation + File.separator + documentName; + + writeFileToFileSystem(uploadedInputStream, fileLocation); + return fileLocation; + } + + + @Override + public String saveImage(InputStream uploadedInputStream, Long resourceId, String imageName, Long fileSize) throws DocumentManagementException { + String uploadImageLocation = generateClientImageParentDirectory(resourceId); + + validateFileSizeWithinPermissibleRange(fileSize, imageName, maxImageSize); + makeDirectories(uploadImageLocation); + + String fileLocation = uploadImageLocation + File.separator + imageName; + + writeFileToFileSystem(uploadedInputStream, fileLocation); + return fileLocation; + } + + @Override + public String saveImage(Base64EncodedImage base64EncodedImage, Long resourceId, String imageName) throws DocumentManagementException { + String uploadImageLocation = generateClientImageParentDirectory(resourceId); + + makeDirectories(uploadImageLocation); + + String fileLocation = uploadImageLocation + File.separator + imageName + base64EncodedImage.getFileExtension(); + try { + OutputStream out = new FileOutputStream(new File(fileLocation)); + byte[] imgBytes = Base64.decode(base64EncodedImage.getBase64EncodedString()); + out.write(imgBytes); + out.flush(); + out.close(); + }catch (IOException ioe){ + throw new DocumentManagementException(ioe.getMessage()); + } + return fileLocation; + } + + @Override + public void deleteImage(final Long resourceId, final String location) { + final boolean fileDeleted = deleteFile(location); + if (!fileDeleted) { + // no need to throw an Error, simply log a warning + logger.warn("Unable to delete image associated with clients with Id " + resourceId); + } + } + + @Override + public void deleteDocument(String documentName, String documentPath) throws DocumentManagementException{ + final boolean fileDeleted = deleteFile(documentPath); + if (!fileDeleted) { throw new DocumentManagementException(documentName); } + } + + private boolean deleteFile(String documentPath) { + final File fileToBeDeleted = new File(documentPath); + return fileToBeDeleted.delete(); + } + + @Override + public DocumentStoreType getType() { + return DocumentStoreType.FILE_SYSTEM; + } + + @Override + public FileData retrieveDocument(DocumentData documentData){ + File file = new File(documentData.fileLocation()); + return new FileData(file, documentData.fileName(), documentData.contentType()); + } + + @Override + public ImageData retrieveImage(ImageData imageData) { + File file = new File(imageData.imageKey()); + imageData.setContent(file); + return imageData; + } + + /** + * Generate the directory path for storing the new document + * + * @param entityType + * @param entityId + * @return + */ + private String generateFileParentDirectory(String entityType, Long entityId) { + return FileSystemDocumentStore.MIFOSX_BASE_DIR + File.separator + ThreadLocalContextUtil.getTenant().getName().replaceAll(" ", "").trim() + + File.separator + "documents" + File.separator + entityType + File.separator + entityId + File.separator + + RandomStringGenerator.generateRandomString(); + } + + /** + * Generate directory path for storing new Image + */ + private String generateClientImageParentDirectory(final Long resourceId) { + return FileSystemDocumentStore.MIFOSX_BASE_DIR + File.separator + ThreadLocalContextUtil.getTenant().getName().replaceAll(" ", "").trim() + + File.separator + "images" + File.separator + "clients" + File.separator + resourceId; + } + + /** + * Recursively create the directory if it does not exist * + */ + private void makeDirectories(String uploadDocumentLocation) { + if (!new File(uploadDocumentLocation).isDirectory()) { + new File(uploadDocumentLocation).mkdirs(); + } + } + + + private void writeFileToFileSystem(InputStream uploadedInputStream, String fileLocation) throws DocumentManagementException { + + try { + OutputStream out = new FileOutputStream(new File(fileLocation)); + int read = 0; + byte[] bytes = new byte[1024]; + + while ((read = uploadedInputStream.read(bytes)) != -1) { + out.write(bytes, 0, read); + } + out.flush(); + out.close(); + } catch (IOException ioException) { + throw new DocumentManagementException("IO Exception caught while writing to FileSystem" + ioException.getMessage()); + } + } + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/FileUtils.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/FileUtils.java deleted file mode 100644 index 5f464a226..000000000 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/FileUtils.java +++ /dev/null @@ -1,234 +0,0 @@ -/** - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. - */ -package org.mifosplatform.infrastructure.core.service; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.Random; - -import org.apache.commons.lang.StringUtils; -import org.mifosplatform.infrastructure.core.data.ApiParameterError; -import org.mifosplatform.infrastructure.core.domain.Base64EncodedImage; -import org.mifosplatform.infrastructure.core.exception.ImageDataURLNotValidException; -import org.mifosplatform.infrastructure.core.exception.ImageUploadException; -import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException; -import org.mifosplatform.infrastructure.documentmanagement.exception.DocumentManagementException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.lowagie.text.pdf.codec.Base64; - -public class FileUtils { - - private final static Logger logger = LoggerFactory.getLogger(FileUtils.class); - - public static final String MIFOSX_BASE_DIR = System.getProperty("user.home") + File.separator + ".mifosx"; - - public static enum IMAGE_MIME_TYPE { - GIF("image/gif"), JPEG("image/jpeg"), PNG("image/png"); - - private final String value; - - private IMAGE_MIME_TYPE(final String value) { - this.value = value; - } - - public String getValue() { - return this.value; - } - }; - - public static enum IMAGE_FILE_EXTENSION { - GIF(".gif"), JPEG(".jpeg"), JPG(".jpg"), PNG(".png"); - - private final String value; - - private IMAGE_FILE_EXTENSION(final String value) { - this.value = value; - } - - public String getValue() { - return this.value; - } - }; - - public static enum IMAGE_DATA_URI_SUFFIX { - GIF("data:" + IMAGE_MIME_TYPE.GIF.getValue() + ";base64,"), JPEG("data:" + IMAGE_MIME_TYPE.JPEG.getValue() + ";base64,"), PNG( - "data:" + IMAGE_MIME_TYPE.PNG.getValue() + ";base64,"); - - private final String value; - - private IMAGE_DATA_URI_SUFFIX(final String value) { - this.value = value; - } - - public String getValue() { - return this.value; - } - }; - - public static Random random = new Random(); - - /** - * Generate a random String - * - * @return - */ - public static String generateRandomString() { - String characters = "abcdefghijklmnopqrstuvwxyz123456789"; - int length = generateRandomNumber(); - char[] text = new char[length]; - for (int i = 0; i < length; i++) { - text[i] = characters.charAt(random.nextInt(characters.length())); - } - return new String(text); - } - - /** - * Generate a random number between 5 to 16 - * - * @return - */ - public static int generateRandomNumber() { - Random randomGenerator = new Random(); - return randomGenerator.nextInt(11) + 5; - } - - /** - * Generate the directory path for storing the new document - * - * @param entityType - * @param entityId - * @return - */ - public static String generateFileParentDirectory(String entityType, Long entityId) { - return FileUtils.MIFOSX_BASE_DIR + File.separator + ThreadLocalContextUtil.getTenant().getName().replaceAll(" ", "").trim() - + File.separator + "documents" + File.separator + entityType + File.separator + entityId + File.separator - + FileUtils.generateRandomString(); - } - - /** - * Generate directory path for storing new Image - */ - public static String generateClientImageParentDirectory(final Long resourceId) { - return FileUtils.MIFOSX_BASE_DIR + File.separator + ThreadLocalContextUtil.getTenant().getName().replaceAll(" ", "").trim() - + File.separator + "images" + File.separator + "clients" + File.separator + resourceId; - } - - /** - * @param uploadedInputStream - * @param uploadedFileLocation - * @return - * @throws IOException - */ - public static String saveToFileSystem(InputStream uploadedInputStream, String uploadedFileLocation, String fileName) throws IOException { - String fileLocation = uploadedFileLocation + File.separator + fileName; - OutputStream out = new FileOutputStream(new File(fileLocation)); - int read = 0; - byte[] bytes = new byte[1024]; - - while ((read = uploadedInputStream.read(bytes)) != -1) { - out.write(bytes, 0, read); - } - out.flush(); - out.close(); - return fileLocation; - } - - /** - * @param base64EncodedImage - * @param uploadedFileLocation - * @param fileName - * @return - * @throws IOException - */ - public static String saveToFileSystem(Base64EncodedImage base64EncodedImage, String uploadedFileLocation, String fileName) - throws IOException { - String fileLocation = uploadedFileLocation + File.separator + fileName + base64EncodedImage.getFileExtension(); - OutputStream out = new FileOutputStream(new File(fileLocation)); - byte[] imgBytes = Base64.decode(base64EncodedImage.getBase64EncodedString()); - out.write(imgBytes); - out.flush(); - out.close(); - return fileLocation; - } - - /** - * @param fileSize - * @param name - */ - public static void validateFileSizeWithinPermissibleRange(Long fileSize, String name, int maxFileSize) { - /** - * Using Content-Length gives me size of the entire request, which is - * good enough for now for a fast fail as the length of the rest of the - * content i.e name and description while compared to the uploaded file - * size is negligible - **/ - if (fileSize != null && ((fileSize / (1024 * 1024)) > maxFileSize)) { throw new DocumentManagementException(name, fileSize, - maxFileSize); } - } - - /** - * Validates that passed in Mime type maps to known image mime types - * - * @param mimeType - */ - public static void validateImageMimeType(String mimeType) { - if (!(mimeType.equalsIgnoreCase(IMAGE_MIME_TYPE.GIF.getValue()) || mimeType.equalsIgnoreCase(IMAGE_MIME_TYPE.JPEG.getValue()) || mimeType - .equalsIgnoreCase(IMAGE_MIME_TYPE.PNG.getValue()))) { throw new ImageUploadException(); } - } - - /** - * Extracts Image from a Data URL - * - * @param mimeType - */ - public static Base64EncodedImage extractImageFromDataURL(String dataURL) { - String fileExtension = ""; - String base64EncodedString = null; - if (StringUtils.startsWith(dataURL, IMAGE_DATA_URI_SUFFIX.GIF.getValue())) { - base64EncodedString = dataURL.replaceAll(IMAGE_DATA_URI_SUFFIX.GIF.getValue(), ""); - fileExtension = IMAGE_FILE_EXTENSION.GIF.getValue(); - } else if (StringUtils.startsWith(dataURL, IMAGE_DATA_URI_SUFFIX.PNG.getValue())) { - base64EncodedString = dataURL.replaceAll(IMAGE_DATA_URI_SUFFIX.PNG.getValue(), ""); - fileExtension = IMAGE_FILE_EXTENSION.PNG.getValue(); - } else if (StringUtils.startsWith(dataURL, IMAGE_DATA_URI_SUFFIX.JPEG.getValue())) { - base64EncodedString = dataURL.replaceAll(IMAGE_DATA_URI_SUFFIX.JPEG.getValue(), ""); - fileExtension = IMAGE_FILE_EXTENSION.JPEG.getValue(); - } else { - throw new ImageDataURLNotValidException(); - } - - return new Base64EncodedImage(base64EncodedString, fileExtension); - } - - public static void validateClientImageNotEmpty(String imageFileName) { - List dataValidationErrors = new ArrayList(); - if (imageFileName == null) { - StringBuilder validationErrorCode = new StringBuilder("validation.msg.clientImage.cannot.be.blank"); - StringBuilder defaultEnglishMessage = new StringBuilder("The parameter image cannot be blank."); - ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(), defaultEnglishMessage.toString(), - "image"); - dataValidationErrors.add(error); - throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", - dataValidationErrors); - } - } - - public static void deleteClientImage(final Long resourceId, final String location) { - File fileToBeDeleted = new File(location); - boolean fileDeleted = fileToBeDeleted.delete(); - if (!fileDeleted) { - // no need to throw an Error, simply log a warning - logger.warn("Unable to delete image associated with clients with Id " + resourceId); - } - } -} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/ImageUtils.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/ImageUtils.java new file mode 100644 index 000000000..5c55495f9 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/ImageUtils.java @@ -0,0 +1,105 @@ +package org.mifosplatform.infrastructure.core.service; + +import org.apache.commons.lang.StringUtils; +import org.mifosplatform.infrastructure.core.data.ApiParameterError; +import org.mifosplatform.infrastructure.core.domain.Base64EncodedImage; +import org.mifosplatform.infrastructure.core.exception.ImageDataURLNotValidException; +import org.mifosplatform.infrastructure.core.exception.ImageUploadException; +import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException; + +import java.util.ArrayList; +import java.util.List; + +public class ImageUtils { + + public static enum IMAGE_MIME_TYPE { + GIF("image/gif"), JPEG("image/jpeg"), PNG("image/png"); + + private final String value; + + private IMAGE_MIME_TYPE(final String value) { + this.value = value; + } + + public String getValue() { + return this.value; + } + } + + public static enum IMAGE_FILE_EXTENSION { + GIF(".gif"), JPEG(".jpeg"), JPG(".jpg"), PNG(".png"); + + private final String value; + + private IMAGE_FILE_EXTENSION(final String value) { + this.value = value; + } + + public String getValue() { + return this.value; + } + } + + public static enum IMAGE_DATA_URI_SUFFIX { + GIF("data:" + IMAGE_MIME_TYPE.GIF.getValue() + ";base64,"), JPEG("data:" + IMAGE_MIME_TYPE.JPEG.getValue() + ";base64,"), PNG( + "data:" + IMAGE_MIME_TYPE.PNG.getValue() + ";base64,"); + + private final String value; + + private IMAGE_DATA_URI_SUFFIX(final String value) { + this.value = value; + } + + public String getValue() { + return this.value; + } + } + /** + * Validates that passed in Mime type maps to known image mime types + * + * @param mimeType + */ + public static void validateImageMimeType(String mimeType) { + if (!(mimeType.equalsIgnoreCase(IMAGE_MIME_TYPE.GIF.getValue()) || mimeType.equalsIgnoreCase(IMAGE_MIME_TYPE.JPEG.getValue()) || mimeType + .equalsIgnoreCase(IMAGE_MIME_TYPE.PNG.getValue()))) { + throw new ImageUploadException(); + } + } + + /** + * Extracts Image from a Data URL + * + * @param mimeType + */ + public static Base64EncodedImage extractImageFromDataURL(String dataURL) { + String fileExtension = ""; + String base64EncodedString = null; + if (StringUtils.startsWith(dataURL, IMAGE_DATA_URI_SUFFIX.GIF.getValue())) { + base64EncodedString = dataURL.replaceAll(IMAGE_DATA_URI_SUFFIX.GIF.getValue(), ""); + fileExtension = IMAGE_FILE_EXTENSION.GIF.getValue(); + } else if (StringUtils.startsWith(dataURL, IMAGE_DATA_URI_SUFFIX.PNG.getValue())) { + base64EncodedString = dataURL.replaceAll(IMAGE_DATA_URI_SUFFIX.PNG.getValue(), ""); + fileExtension = IMAGE_FILE_EXTENSION.PNG.getValue(); + } else if (StringUtils.startsWith(dataURL, IMAGE_DATA_URI_SUFFIX.JPEG.getValue())) { + base64EncodedString = dataURL.replaceAll(IMAGE_DATA_URI_SUFFIX.JPEG.getValue(), ""); + fileExtension = IMAGE_FILE_EXTENSION.JPEG.getValue(); + } else { + throw new ImageDataURLNotValidException(); + } + + return new Base64EncodedImage(base64EncodedString, fileExtension); + } + + public static void validateClientImageNotEmpty(String imageFileName) { + List dataValidationErrors = new ArrayList(); + if (imageFileName == null) { + StringBuilder validationErrorCode = new StringBuilder("validation.msg.clientImage.cannot.be.blank"); + StringBuilder defaultEnglishMessage = new StringBuilder("The parameter image cannot be blank."); + ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(), defaultEnglishMessage.toString(), + "image"); + dataValidationErrors.add(error); + throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", + dataValidationErrors); + } + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/RandomStringGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/RandomStringGenerator.java new file mode 100644 index 000000000..31f1ecfd0 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/RandomStringGenerator.java @@ -0,0 +1,33 @@ +package org.mifosplatform.infrastructure.core.service; + +import java.util.Random; + +public class RandomStringGenerator { + public static Random random = new Random(); + + /** + * Generate a random String + * + * @return + */ + public static String generateRandomString() { + String characters = "abcdefghijklmnopqrstuvwxyz123456789"; + int length = generateRandomNumber(); + char[] text = new char[length]; + for (int i = 0; i < length; i++) { + text[i] = characters.charAt(random.nextInt(characters.length())); + } + return new String(text); + } + + /** + * Generate a random number between 5 to 16 + * + * @return + */ + public static int generateRandomNumber() { + Random randomGenerator = new Random(); + return randomGenerator.nextInt(11) + 5; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/S3ClientFactory.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/S3ClientFactory.java new file mode 100644 index 000000000..5274274f2 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/S3ClientFactory.java @@ -0,0 +1,24 @@ +package org.mifosplatform.infrastructure.core.service; + +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3Client; +import org.mifosplatform.infrastructure.configuration.domain.ExternalServicesDomainService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class S3ClientFactory { + + private ExternalServicesDomainService ExternalServicesDomainService; + + @Autowired + public S3ClientFactory(final ExternalServicesDomainService ExternalServicesDomainService) { + this.ExternalServicesDomainService = ExternalServicesDomainService; + } + + public AmazonS3Client getS3Client() { + String s3_access_key = ExternalServicesDomainService.getValue("s3_access_key"); + String s3_secret_key = ExternalServicesDomainService.getValue("s3_secret_key"); + return new AmazonS3Client(new BasicAWSCredentials(s3_access_key,s3_secret_key)); + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/S3DocumentStore.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/S3DocumentStore.java new file mode 100644 index 000000000..9781ef9c9 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/S3DocumentStore.java @@ -0,0 +1,201 @@ +package org.mifosplatform.infrastructure.core.service; + + +import com.amazonaws.AmazonClientException; +import com.amazonaws.AmazonServiceException; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.*; +import com.lowagie.text.pdf.codec.Base64; +import org.mifosplatform.infrastructure.core.domain.Base64EncodedImage; +import org.mifosplatform.infrastructure.documentmanagement.command.DocumentCommand; +import org.mifosplatform.infrastructure.documentmanagement.data.DocumentData; +import org.mifosplatform.infrastructure.documentmanagement.data.FileData; +import org.mifosplatform.infrastructure.documentmanagement.exception.DocumentManagementException; +import org.mifosplatform.infrastructure.documentmanagement.exception.DocumentNotFoundException; +import org.mifosplatform.portfolio.client.data.ImageData; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.InputStream; + +public class S3DocumentStore extends DocumentStore { + + private final static Logger logger = LoggerFactory.getLogger(S3DocumentStore.class); + + private final String s3BucketName; + private AmazonS3 s3Client; + + public S3DocumentStore(String s3BucketName, AmazonS3 s3Client) { + this.s3BucketName = s3BucketName; + this.s3Client = s3Client; + } + + @Override + public String saveDocument(InputStream toUpload, DocumentCommand documentCommand) throws DocumentManagementException { + String documentName = documentCommand.getFileName(); + validateFileSizeWithinPermissibleRange(documentCommand.getSize(), documentName, maxFileSize); + + String uploadDocFolder = generateFileParentDirectory(documentCommand.getParentEntityType(), documentCommand.getParentEntityId()); + String uploadDocFullPath = uploadDocFolder + File.separator + documentName; + + uploadDocument(toUpload, uploadDocFullPath); + return uploadDocFullPath; + } + + @Override + public void deleteDocument(String documentName, String documentPath) throws DocumentManagementException { + try{ + deleteObjectFromS3(documentPath); + }catch (AmazonServiceException ase){ + deleteObjectAmazonServiceExceptionMessage(ase); + throw new DocumentManagementException(documentName); + }catch (AmazonClientException ace){ + deleteObjectAmazonClientExceptionMessage(ace); + throw new DocumentManagementException(documentName); + } + } + + @Override + public String saveImage(InputStream toUploadInputStream, Long resourceId, String imageName, Long fileSize) throws DocumentManagementException { + validateFileSizeWithinPermissibleRange(fileSize, imageName, maxImageSize); + + String uploadImageLocation = generateClientImageParentDirectory(resourceId); + String fileLocation = uploadImageLocation + File.separator + imageName; + + uploadDocument(toUploadInputStream, fileLocation); + return fileLocation; + } + + @Override + public String saveImage(Base64EncodedImage base64EncodedImage, Long resourceId, String imageName) throws DocumentManagementException { + String uploadImageLocation = generateClientImageParentDirectory(resourceId); + String fileLocation = uploadImageLocation + File.separator + imageName + base64EncodedImage.getFileExtension(); + InputStream toUploadInputStream = new ByteArrayInputStream(Base64.decode(base64EncodedImage.getBase64EncodedString())); + + uploadDocument(toUploadInputStream, fileLocation); + return fileLocation; + } + + @Override + public void deleteImage(Long resourceId, String location) { + try { + deleteObjectFromS3(location); + } catch (AmazonServiceException ase) { + deleteObjectAmazonServiceExceptionMessage(ase); + logger.warn("Unable to delete image associated with clients with Id " + resourceId); + } catch (AmazonClientException ace) { + deleteObjectAmazonClientExceptionMessage(ace); + logger.warn("Unable to delete image associated with clients with Id " + resourceId); + } + } + + private void deleteObjectAmazonClientExceptionMessage(AmazonClientException ace) { + String message = "Caught an AmazonClientException." + + "Error Message: " + ace.getMessage(); + logger.error(message); + } + + private void deleteObjectAmazonServiceExceptionMessage(AmazonServiceException ase) { + String message = "Caught an AmazonServiceException." + + "Error Message: " + ase.getMessage() + + "HTTP Status Code: " + ase.getStatusCode() + + "AWS Error Code: " + ase.getErrorCode() + + "Error Type: " + ase.getErrorType() + + "Request ID: " + ase.getRequestId(); + logger.error(message); + } + + private void deleteObjectFromS3(String location) { + s3Client.deleteObject(new DeleteObjectRequest(s3BucketName, location)); + } + + @Override + public DocumentStoreType getType() { + return DocumentStoreType.S3; + } + + @Override + public FileData retrieveDocument(DocumentData documentData) throws DocumentNotFoundException { + FileData fileData = null; + String fileName = documentData.fileName(); + try { + logger.info("Downloading an object"); + S3Object s3object = s3Client.getObject(new GetObjectRequest(s3BucketName, documentData.fileLocation())); + fileData = new FileData(s3object.getObjectContent(), fileName, documentData.contentType()); + } catch (AmazonServiceException ase) { + getObjectAmazonServiceExceptionMessage(ase,fileName); + throw new DocumentNotFoundException(documentData.getParentEntityType(), documentData.getParentEntityId(), documentData.getId()); + } catch (AmazonClientException ace) { + getObjectAmazonClientExceptionMessage(ace,fileName); + throw new DocumentNotFoundException(documentData.getParentEntityType(), documentData.getParentEntityId(), documentData.getId()); + } + return fileData; + } + + @Override + public ImageData retrieveImage(ImageData imageData) { + S3Object s3object = s3Client.getObject(new GetObjectRequest(s3BucketName, imageData.imageKey())); + imageData.setContent(s3object.getObjectContent()); + return imageData; + } + + String generateFileParentDirectory(String entityType, Long entityId) { + return "documents" + File.separator + entityType + File.separator + entityId + File.separator + RandomStringGenerator.generateRandomString(); + } + + String generateClientImageParentDirectory(Long resourceId) { + return "images" + File.separator + "clients" + File.separator + resourceId; + } + + private void uploadDocument(InputStream inputStream, String s3UploadLocation) throws DocumentManagementException { + try { + logger.info("Uploading a new object to S3 from a file to " + s3UploadLocation); + s3Client.putObject(new PutObjectRequest(this.s3BucketName, s3UploadLocation, inputStream, new ObjectMetadata())); + + } catch (AmazonServiceException ase) { + String message = "Caught an AmazonServiceException, which " + "means your request made it " + "to Amazon S3, but was rejected with an error response" + + " for some reason. \n Error Message: " + ase.getMessage() + + "HTTP Status Code: " + ase.getStatusCode() + + "AWS Error Code: " + ase.getErrorCode() + + "Error Type: " + ase.getErrorType() + + "Request ID: " + ase.getRequestId(); + + logger.error(message); + throw new DocumentManagementException(message); + } catch (AmazonClientException ace) { + String message = "Caught an AmazonClientException, which " + + "means the client encountered " + + "an internal error while trying to " + + "communicate with S3, " + + "such as not being able to access the network." + + "Error Message: " + ace.getMessage(); + logger.error(message); + throw new DocumentManagementException(message); + } + } + + private void getObjectAmazonClientExceptionMessage(AmazonClientException ace, String filename) { + String message = "Caught an AmazonClientException, which means the client encountered " + + "an internal error while trying to communicate with S3, " + + "such as not being able to access the network." + + "Error Message: " + ace.getMessage() + + "FileName: " + filename; + + logger.error(message); + } + + private void getObjectAmazonServiceExceptionMessage(AmazonServiceException ase, String filename) { + String message = "Caught an AmazonServiceException, which means your request made it " + + "to Amazon S3, but was rejected with an error response" + + " for some reason." + + "Error Message: " + ase.getMessage() + + "HTTP Status Code: " + ase.getStatusCode() + + "AWS Error Code: " + ase.getErrorCode() + + "Error Type: " + ase.getErrorType() + + "Request ID: " + ase.getRequestId() + + "FileName: " + filename; + logger.error(message); + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadReportingServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadReportingServiceImpl.java index 7d4ca2b08..3e573af66 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadReportingServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadReportingServiceImpl.java @@ -29,7 +29,7 @@ import javax.ws.rs.core.StreamingOutput; import org.apache.commons.lang.StringUtils; import org.mifosplatform.infrastructure.core.domain.JdbcSupport; import org.mifosplatform.infrastructure.core.exception.PlatformDataIntegrityException; -import org.mifosplatform.infrastructure.core.service.FileUtils; +import org.mifosplatform.infrastructure.core.service.FileSystemDocumentStore; import org.mifosplatform.infrastructure.core.service.TenantAwareRoutingDataSource; import org.mifosplatform.infrastructure.dataqueries.data.GenericResultsetData; import org.mifosplatform.infrastructure.dataqueries.data.ReportData; @@ -243,7 +243,7 @@ public class ReadReportingServiceImpl implements ReadReportingService { if (noPentaho) { throw new PlatformDataIntegrityException("error.msg.no.pentaho", "Pentaho is not enabled", "Pentaho is not enabled"); } - final String reportPath = FileUtils.MIFOSX_BASE_DIR + File.separator + "pentahoReports" + File.separator + reportName + ".prpt"; + final String reportPath = FileSystemDocumentStore.MIFOSX_BASE_DIR + File.separator + "pentahoReports" + File.separator + reportName + ".prpt"; logger.info("Report path: " + reportPath); // load report definition @@ -357,7 +357,7 @@ public class ReadReportingServiceImpl implements ReadReportingService { @Override public String retrieveReportPDF(final String reportName, final String type, final Map queryParams) { - String fileLocation = FileUtils.MIFOSX_BASE_DIR + File.separator + ""; + String fileLocation = FileSystemDocumentStore.MIFOSX_BASE_DIR + File.separator + ""; if (!new File(fileLocation).isDirectory()) { new File(fileLocation).mkdirs(); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/api/DocumentManagementApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/api/DocumentManagementApiResource.java index b12ae5054..ff48aa1e8 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/api/DocumentManagementApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/api/DocumentManagementApiResource.java @@ -5,36 +5,16 @@ */ package org.mifosplatform.infrastructure.documentmanagement.api; -import java.io.File; -import java.io.InputStream; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; -import java.util.Set; - -import javax.ws.rs.Consumes; -import javax.ws.rs.DELETE; -import javax.ws.rs.GET; -import javax.ws.rs.HeaderParam; -import javax.ws.rs.POST; -import javax.ws.rs.PUT; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.ResponseBuilder; -import javax.ws.rs.core.UriInfo; - -import org.mifosplatform.infrastructure.core.api.ApiConstants; +import com.sun.jersey.core.header.FormDataContentDisposition; +import com.sun.jersey.multipart.FormDataBodyPart; +import com.sun.jersey.multipart.FormDataParam; import org.mifosplatform.infrastructure.core.api.ApiRequestParameterHelper; import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; import org.mifosplatform.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; import org.mifosplatform.infrastructure.core.serialization.ToApiJsonSerializer; -import org.mifosplatform.infrastructure.core.service.FileUtils; import org.mifosplatform.infrastructure.documentmanagement.command.DocumentCommand; import org.mifosplatform.infrastructure.documentmanagement.data.DocumentData; +import org.mifosplatform.infrastructure.documentmanagement.data.FileData; import org.mifosplatform.infrastructure.documentmanagement.service.DocumentReadPlatformService; import org.mifosplatform.infrastructure.documentmanagement.service.DocumentWritePlatformService; import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; @@ -42,9 +22,17 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; -import com.sun.jersey.core.header.FormDataContentDisposition; -import com.sun.jersey.multipart.FormDataBodyPart; -import com.sun.jersey.multipart.FormDataParam; +import javax.ws.rs.*; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.ResponseBuilder; +import javax.ws.rs.core.UriInfo; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; @Path("{entityType}/{entityId}/documents") @Component @@ -64,8 +52,8 @@ public class DocumentManagementApiResource { @Autowired public DocumentManagementApiResource(final PlatformSecurityContext context, - final DocumentReadPlatformService documentReadPlatformService, final DocumentWritePlatformService documentWritePlatformService, - final ApiRequestParameterHelper apiRequestParameterHelper, final ToApiJsonSerializer toApiJsonSerializer) { + final DocumentReadPlatformService documentReadPlatformService, final DocumentWritePlatformService documentWritePlatformService, + final ApiRequestParameterHelper apiRequestParameterHelper, final ToApiJsonSerializer toApiJsonSerializer) { this.context = context; this.documentReadPlatformService = documentReadPlatformService; this.documentWritePlatformService = documentWritePlatformService; @@ -95,8 +83,6 @@ public class DocumentManagementApiResource { @FormDataParam("file") final FormDataContentDisposition fileDetails, @FormDataParam("file") final FormDataBodyPart bodyPart, @FormDataParam("name") final String name, @FormDataParam("description") final String description) { - FileUtils.validateFileSizeWithinPermissibleRange(fileSize, name, ApiConstants.MAX_FILE_UPLOAD_SIZE_IN_MB); - /** * TODO: also need to have a backup and stop reading from stream after * max size is reached to protect against malicious clients @@ -124,8 +110,6 @@ public class DocumentManagementApiResource { @FormDataParam("file") final FormDataBodyPart bodyPart, @FormDataParam("name") final String name, @FormDataParam("description") final String description) { - FileUtils.validateFileSizeWithinPermissibleRange(fileSize, name, ApiConstants.MAX_FILE_UPLOAD_SIZE_IN_MB); - final Set modifiedParams = new HashSet(); modifiedParams.add("name"); modifiedParams.add("description"); @@ -182,11 +166,10 @@ public class DocumentManagementApiResource { this.context.authenticatedUser().validateHasReadPermission(this.SystemEntityType); - final DocumentData documentData = this.documentReadPlatformService.retrieveDocument(entityType, entityId, documentId); - final File file = new File(documentData.fileLocation()); - final ResponseBuilder response = Response.ok(file); - response.header("Content-Disposition", "attachment; filename=\"" + documentData.fileName() + "\""); - response.header("Content-Type", documentData.contentType()); + final FileData fileData = this.documentReadPlatformService.retrieveDocumentAsFile(entityType, entityId, documentId); + final ResponseBuilder response = Response.ok(fileData.file()); + response.header("Content-Disposition", "attachment; filename=\"" + fileData.name() + "\""); + response.header("Content-Type", fileData.contentType()); return response.build(); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/command/DocumentCommand.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/command/DocumentCommand.java index d138ad51f..2c6e636c0 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/command/DocumentCommand.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/command/DocumentCommand.java @@ -5,6 +5,8 @@ */ package org.mifosplatform.infrastructure.documentmanagement.command; +import org.mifosplatform.infrastructure.core.service.DocumentStoreType; + import java.util.Set; /** @@ -24,6 +26,7 @@ public class DocumentCommand { private String location; private final Set modifiedParameters; + private DocumentStoreType storageType; public DocumentCommand(final Set modifiedParameters, final Long id, final String parentEntityType, final Long parentEntityId, final String name, final String fileName, final Long size, final String type, final String description, final String location) { @@ -119,4 +122,11 @@ public class DocumentCommand { return this.modifiedParameters.contains("location"); } + public void setStorageType(DocumentStoreType storageType) { + this.storageType = storageType; + } + + public String getStorageType(){ + return storageType.getValue(); + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/data/DocumentData.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/data/DocumentData.java index 25cdcb931..6cf339bef 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/data/DocumentData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/data/DocumentData.java @@ -5,6 +5,9 @@ */ package org.mifosplatform.infrastructure.documentmanagement.data; +import org.mifosplatform.infrastructure.core.service.DocumentStore; +import org.mifosplatform.infrastructure.core.service.DocumentStoreType; + /** * Immutable data object represent document being managed on platform. */ @@ -26,8 +29,10 @@ public class DocumentData { private final String description; private String location; + private String storageType; + public DocumentData(final Long id, final String parentEntityType, final Long parentEntityId, final String name, final String fileName, - final Long size, final String type, final String description, final String location) { + final Long size, final String type, final String description, final String location, String storageType) { this.id = id; this.parentEntityType = parentEntityType; this.parentEntityId = parentEntityId; @@ -37,6 +42,7 @@ public class DocumentData { this.type = type; this.description = description; this.location = location; + this.storageType = storageType; } public String contentType() { @@ -55,4 +61,20 @@ public class DocumentData { this.location = location; } + public DocumentStoreType storeType(){ + return DocumentStore.getDocumentStoreType(storageType); + } + + public String getParentEntityType(){ + return this.parentEntityType; + } + + public Long getParentEntityId(){ + return this.parentEntityId; + } + + public Long getId(){ + return this.id; + } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/data/FileData.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/data/FileData.java new file mode 100644 index 000000000..bbaad808f --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/data/FileData.java @@ -0,0 +1,56 @@ +package org.mifosplatform.infrastructure.documentmanagement.data; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; + +public class FileData { + private static final Logger logger = LoggerFactory.getLogger(FileData.class); + + private final File file; + private final String fileName; + private final String contentType; + private final InputStream inputStream; + + public FileData(File file, String fileName, String contentType) { + this.file = file; + this.fileName = fileName; + this.contentType = contentType; + this.inputStream = null; + } + + public FileData(InputStream inputStream, String fileName, String contentType) { + this.file = null; + this.inputStream = inputStream; + this.fileName = fileName; + this.contentType = contentType; + } + + public String contentType() { + return contentType; + } + + public String name() { + return fileName; + } + + public InputStream file(){ + try{ + if(inputStream == null){ + return new FileInputStream(file); + } + else { + return inputStream; + } + + } catch (FileNotFoundException e){ + logger.error(e.toString()); + return null; + } + } +} + diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/domain/Document.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/domain/Document.java index 467503668..33e0747d2 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/domain/Document.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/domain/Document.java @@ -5,14 +5,16 @@ */ package org.mifosplatform.infrastructure.documentmanagement.domain; +import org.apache.commons.lang.StringUtils; +import org.mifosplatform.infrastructure.core.service.DocumentStore; +import org.mifosplatform.infrastructure.core.service.DocumentStoreType; +import org.mifosplatform.infrastructure.documentmanagement.command.DocumentCommand; +import org.springframework.data.jpa.domain.AbstractPersistable; + import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Table; -import org.apache.commons.lang.StringUtils; -import org.mifosplatform.infrastructure.documentmanagement.command.DocumentCommand; -import org.springframework.data.jpa.domain.AbstractPersistable; - @Entity @Table(name = "m_document") public class Document extends AbstractPersistable { @@ -41,15 +43,19 @@ public class Document extends AbstractPersistable { @Column(name = "location", length = 500) private String location; - public Document() {} + @Column(name = "storage_type", length = 50) + private String storageType; + + public Document() { + } public static Document createNew(final String parentEntityType, final Long parentEntityId, final String name, final String fileName, - final Long size, final String type, final String description, final String location) { - return new Document(parentEntityType, parentEntityId, name, fileName, size, type, description, location); + final Long size, final String type, final String description, final String location, final DocumentStoreType storageType) { + return new Document(parentEntityType, parentEntityId, name, fileName, size, type, description, location, storageType); } private Document(final String parentEntityType, final Long parentEntityId, final String name, final String fileName, final Long size, - final String type, final String description, final String location) { + final String type, final String description, final String location, final DocumentStoreType storageType) { this.parentEntityType = StringUtils.defaultIfEmpty(parentEntityType, null); this.parentEntityId = parentEntityId; this.name = StringUtils.defaultIfEmpty(name, null); @@ -58,6 +64,7 @@ public class Document extends AbstractPersistable { this.type = StringUtils.defaultIfEmpty(type, null); this.description = StringUtils.defaultIfEmpty(description, null); this.location = StringUtils.defaultIfEmpty(location, null); + this.storageType = storageType.getValue(); } public void update(final DocumentCommand command) { @@ -79,7 +86,7 @@ public class Document extends AbstractPersistable { if (command.isSizeChanged()) { this.size = command.getSize(); } - + this.storageType = command.getStorageType(); } public String getParentEntityType() { @@ -146,4 +153,7 @@ public class Document extends AbstractPersistable { this.location = location; } + public DocumentStoreType storageType(){ + return DocumentStore.getDocumentStoreType(this.storageType); + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/DocumentReadPlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/DocumentReadPlatformService.java index f6b3f6692..af99a5f70 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/DocumentReadPlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/DocumentReadPlatformService.java @@ -5,13 +5,17 @@ */ package org.mifosplatform.infrastructure.documentmanagement.service; -import java.util.Collection; - import org.mifosplatform.infrastructure.documentmanagement.data.DocumentData; +import org.mifosplatform.infrastructure.documentmanagement.data.FileData; + +import java.util.Collection; public interface DocumentReadPlatformService { Collection retrieveAllDocuments(String entityType, Long entityId); + FileData retrieveDocumentAsFile(String entityType, Long entityId, Long documentId); + DocumentData retrieveDocument(String entityType, Long entityId, Long documentId); + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/DocumentReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/DocumentReadPlatformServiceImpl.java index 15fd7dcf9..c531eacdc 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/DocumentReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/DocumentReadPlatformServiceImpl.java @@ -5,13 +5,12 @@ */ package org.mifosplatform.infrastructure.documentmanagement.service; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.Collection; - import org.mifosplatform.infrastructure.core.domain.JdbcSupport; +import org.mifosplatform.infrastructure.core.service.DocumentStore; +import org.mifosplatform.infrastructure.core.service.DocumentStoreFactory; import org.mifosplatform.infrastructure.core.service.TenantAwareRoutingDataSource; import org.mifosplatform.infrastructure.documentmanagement.data.DocumentData; +import org.mifosplatform.infrastructure.documentmanagement.data.FileData; import org.mifosplatform.infrastructure.documentmanagement.exception.DocumentNotFoundException; import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; import org.springframework.beans.factory.annotation.Autowired; @@ -20,16 +19,23 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Service; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collection; + @Service public class DocumentReadPlatformServiceImpl implements DocumentReadPlatformService { private final JdbcTemplate jdbcTemplate; private final PlatformSecurityContext context; + private final DocumentStoreFactory documentStoreFactory; @Autowired - public DocumentReadPlatformServiceImpl(final PlatformSecurityContext context, final TenantAwareRoutingDataSource dataSource) { + public DocumentReadPlatformServiceImpl(final PlatformSecurityContext context, final TenantAwareRoutingDataSource dataSource, + final DocumentStoreFactory documentStoreFactory) { this.context = context; this.jdbcTemplate = new JdbcTemplate(dataSource); + this.documentStoreFactory = documentStoreFactory; } @Override @@ -46,17 +52,22 @@ public class DocumentReadPlatformServiceImpl implements DocumentReadPlatformServ } @Override - public DocumentData retrieveDocument(final String entityType, final Long entityId, final Long documentId) { - + public FileData retrieveDocumentAsFile(final String entityType, final Long entityId, final Long documentId) { try { - this.context.authenticatedUser(); + DocumentData documentData = getDocumentData(entityType, entityId, documentId); + DocumentStore instanceForRead = documentStoreFactory.getInstanceFromStorageType(documentData.storeType()); + return instanceForRead.retrieveDocument(documentData); + } catch (final EmptyResultDataAccessException e) { + throw new DocumentNotFoundException(entityType, entityId, documentId); + } catch (final DocumentNotFoundException d) { + throw d; + } + } - // TODO verify if the entities are valid and a - // user has data - // scope for the particular entities - final DocumentMapper mapper = new DocumentMapper(false); - final String sql = "select " + mapper.schema() + " and d.id=? "; - return this.jdbcTemplate.queryForObject(sql, mapper, new Object[] { entityType, entityId, documentId }); + @Override + public DocumentData retrieveDocument(String entityType, Long entityId, Long documentId) { + try { + return getDocumentData(entityType, entityId, documentId); } catch (final EmptyResultDataAccessException e) { throw new DocumentNotFoundException(entityType, entityId, documentId); } @@ -73,7 +84,8 @@ public class DocumentReadPlatformServiceImpl implements DocumentReadPlatformServ public String schema() { return "d.id as id, d.parent_entity_type as parentEntityType, d.parent_entity_id as parentEntityId, d.name as name, " + " d.file_name as fileName, d.size as fileSize, d.type as fileType, " - + " d.description as description, d.location as location" + + " d.description as description, d.location as location," + + " d.storage_type as storageType" + " from m_document d where d.parent_entity_type=? and d.parent_entity_id=?"; } @@ -92,8 +104,21 @@ public class DocumentReadPlatformServiceImpl implements DocumentReadPlatformServ if (!this.hideLocation) { location = rs.getString("location"); } + final String storageType = rs.getString("storageType"); - return new DocumentData(id, parentEntityType, parentEntityId, name, fileName, fileSize, fileType, description, location); + return new DocumentData(id, parentEntityType, parentEntityId, name, fileName, fileSize, fileType, description, location, storageType); } } + + + private DocumentData getDocumentData(String entityType, Long entityId, Long documentId) { + this.context.authenticatedUser(); + + // TODO verify if the entities are valid and a + // user has data + // scope for the particular entities + final DocumentMapper mapper = new DocumentMapper(false); + final String sql = "select " + mapper.schema() + " and d.id=? "; + return this.jdbcTemplate.queryForObject(sql, mapper, new Object[]{entityType, entityId, documentId}); + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/DocumentWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/DocumentWritePlatformServiceJpaRepositoryImpl.java index 84a1ca41e..83bff1a3c 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/DocumentWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/DocumentWritePlatformServiceJpaRepositoryImpl.java @@ -5,13 +5,11 @@ */ package org.mifosplatform.infrastructure.documentmanagement.service; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; - import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; import org.mifosplatform.infrastructure.core.exception.PlatformDataIntegrityException; -import org.mifosplatform.infrastructure.core.service.FileUtils; +import org.mifosplatform.infrastructure.core.service.DocumentStore; +import org.mifosplatform.infrastructure.core.service.DocumentStoreFactory; +import org.mifosplatform.infrastructure.core.service.DocumentStoreType; import org.mifosplatform.infrastructure.documentmanagement.command.DocumentCommand; import org.mifosplatform.infrastructure.documentmanagement.command.DocumentCommandValidator; import org.mifosplatform.infrastructure.documentmanagement.domain.Document; @@ -27,6 +25,8 @@ import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.io.InputStream; + @Service public class DocumentWritePlatformServiceJpaRepositoryImpl implements DocumentWritePlatformService { @@ -34,11 +34,14 @@ public class DocumentWritePlatformServiceJpaRepositoryImpl implements DocumentWr private final PlatformSecurityContext context; private final DocumentRepository documentRepository; + private final DocumentStoreFactory documentStoreFactory; @Autowired - public DocumentWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context, final DocumentRepository documentRepository) { + public DocumentWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context, final DocumentRepository documentRepository, + final DocumentStoreFactory documentStoreFactory) { this.context = context; this.documentRepository = documentRepository; + this.documentStoreFactory = documentStoreFactory; } @Transactional @@ -53,19 +56,13 @@ public class DocumentWritePlatformServiceJpaRepositoryImpl implements DocumentWr validator.validateForCreate(); - final String fileUploadLocation = FileUtils.generateFileParentDirectory(documentCommand.getParentEntityType(), - documentCommand.getParentEntityId()); + DocumentStore documentStore = this.documentStoreFactory.getInstanceFromConfiguration(); - /** Recursively create the directory if it does not exist **/ - if (!new File(fileUploadLocation).isDirectory()) { - new File(fileUploadLocation).mkdirs(); - } - - final String fileLocation = FileUtils.saveToFileSystem(inputStream, fileUploadLocation, documentCommand.getFileName()); + final String fileLocation = documentStore.saveDocument(inputStream, documentCommand); final Document document = Document.createNew(documentCommand.getParentEntityType(), documentCommand.getParentEntityId(), documentCommand.getName(), documentCommand.getFileName(), documentCommand.getSize(), documentCommand.getType(), - documentCommand.getDescription(), fileLocation); + documentCommand.getDescription(), fileLocation, documentStore.getType()); this.documentRepository.save(document); @@ -74,8 +71,8 @@ public class DocumentWritePlatformServiceJpaRepositoryImpl implements DocumentWr logger.error(dve.getMessage(), dve); throw new PlatformDataIntegrityException("error.msg.document.unknown.data.integrity.issue", "Unknown data integrity issue with resource."); - } catch (final IOException ioe) { - logger.error(ioe.getMessage(), ioe); + } catch (final DocumentManagementException dme) { + logger.error(dme.getMessage(), dme); throw new DocumentManagementException(documentCommand.getName()); } } @@ -92,30 +89,21 @@ public class DocumentWritePlatformServiceJpaRepositoryImpl implements DocumentWr // TODO check if entity id is valid and within data scope for the // user final Document documentForUpdate = this.documentRepository.findOne(documentCommand.getId()); + DocumentStoreType documentStoreType = documentForUpdate.storageType(); if (documentForUpdate == null) { throw new DocumentNotFoundException(documentCommand.getParentEntityType(), documentCommand.getParentEntityId(), documentCommand.getId()); } oldLocation = documentForUpdate.getLocation(); - // if a new file is also passed in if (inputStream != null && documentCommand.isFileNameChanged()) { - final String fileUploadLocation = FileUtils.generateFileParentDirectory(documentCommand.getParentEntityType(), - documentCommand.getParentEntityId()); - - /** Recursively create the directory if it does not exist **/ - if (!new File(fileUploadLocation).isDirectory()) { - new File(fileUploadLocation).mkdirs(); - } - - // TODO provide switch to toggle between file system appender - // and Amazon S3 appender - final String fileLocation = FileUtils.saveToFileSystem(inputStream, fileUploadLocation, documentCommand.getFileName()); - documentCommand.setLocation(fileLocation); + DocumentStore instanceFromConfiguration = this.documentStoreFactory.getInstanceFromConfiguration(); + documentCommand.setLocation(instanceFromConfiguration.saveDocument(inputStream, documentCommand)); + documentCommand.setStorageType(instanceFromConfiguration.getType()); } documentForUpdate.update(documentCommand); if (inputStream != null && documentCommand.isFileNameChanged()) { - // delete previous file - deleteFile(documentCommand.getName(), oldLocation); + DocumentStore documentStore = this.documentStoreFactory.getInstanceFromStorageType(documentStoreType); + documentStore.deleteDocument(documentCommand.getName(), oldLocation); } this.documentRepository.saveAndFlush(documentForUpdate); @@ -125,8 +113,8 @@ public class DocumentWritePlatformServiceJpaRepositoryImpl implements DocumentWr logger.error(dve.getMessage(), dve); throw new PlatformDataIntegrityException("error.msg.document.unknown.data.integrity.issue", "Unknown data integrity issue with resource."); - } catch (final IOException ioe) { - logger.error(ioe.getMessage(), ioe); + } catch (final DocumentManagementException dme) { + logger.error(dme.getMessage(), dme); throw new DocumentManagementException(documentCommand.getName()); } } @@ -139,20 +127,15 @@ public class DocumentWritePlatformServiceJpaRepositoryImpl implements DocumentWr validateParentEntityType(documentCommand); // TODO: Check document is present under this entity Id final Document document = this.documentRepository.findOne(documentCommand.getId()); + DocumentStore documentStore = this.documentStoreFactory.getInstanceFromStorageType(document.storageType()); if (document == null) { throw new DocumentNotFoundException(documentCommand.getParentEntityType(), documentCommand.getParentEntityId(), documentCommand.getId()); } this.documentRepository.delete(document); - deleteFile(document.getName(), document.getLocation()); + documentStore.deleteDocument(document.getName(), document.getLocation()); return new CommandProcessingResult(document.getId()); } - private void deleteFile(final String documentName, final String location) { - final File fileToBeDeleted = new File(location); - final boolean fileDeleted = fileToBeDeleted.delete(); - if (!fileDeleted) { throw new DocumentManagementException(documentName); } - } - private void validateParentEntityType(final DocumentCommand documentCommand) { if (!checkValidEntityType(documentCommand.getParentEntityType())) { throw new InvalidEntityTypeForDocumentManagementException( documentCommand.getParentEntityType()); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/api/ClientImagesApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/api/ClientImagesApiResource.java index dccf49f26..087c08196 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/api/ClientImagesApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/api/ClientImagesApiResource.java @@ -5,44 +5,31 @@ */ package org.mifosplatform.portfolio.client.api; -import java.io.File; -import java.io.InputStream; - -import javax.ws.rs.Consumes; -import javax.ws.rs.DELETE; -import javax.ws.rs.GET; -import javax.ws.rs.HeaderParam; -import javax.ws.rs.POST; -import javax.ws.rs.PUT; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.ResponseBuilder; - +import com.lowagie.text.pdf.codec.Base64; +import com.sun.jersey.core.header.FormDataContentDisposition; +import com.sun.jersey.multipart.FormDataBodyPart; +import com.sun.jersey.multipart.FormDataParam; import org.apache.commons.lang.StringUtils; -import org.mifosplatform.infrastructure.core.api.ApiConstants; import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; import org.mifosplatform.infrastructure.core.domain.Base64EncodedImage; import org.mifosplatform.infrastructure.core.serialization.DefaultToApiJsonSerializer; -import org.mifosplatform.infrastructure.core.service.FileUtils; -import org.mifosplatform.infrastructure.core.service.FileUtils.IMAGE_DATA_URI_SUFFIX; -import org.mifosplatform.infrastructure.core.service.FileUtils.IMAGE_FILE_EXTENSION; -import org.mifosplatform.infrastructure.core.service.FileUtils.IMAGE_MIME_TYPE; +import org.mifosplatform.infrastructure.core.service.ImageUtils; import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; import org.mifosplatform.portfolio.client.data.ClientData; import org.mifosplatform.portfolio.client.exception.ImageNotFoundException; import org.mifosplatform.portfolio.client.service.ClientReadPlatformService; import org.mifosplatform.portfolio.client.service.ClientWritePlatformService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; -import com.lowagie.text.pdf.codec.Base64; -import com.sun.jersey.core.header.FormDataContentDisposition; -import com.sun.jersey.multipart.FormDataBodyPart; -import com.sun.jersey.multipart.FormDataParam; +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.ResponseBuilder; +import java.io.InputStream; @Path("/clients/{clientId}/images") @Component @@ -53,10 +40,11 @@ public class ClientImagesApiResource { private final ClientReadPlatformService clientReadPlatformService; private final ClientWritePlatformService clientWritePlatformService; private final DefaultToApiJsonSerializer toApiJsonSerializer; + private final static Logger logger = LoggerFactory.getLogger(ClientImagesApiResource.class); @Autowired public ClientImagesApiResource(final PlatformSecurityContext context, final ClientReadPlatformService readPlatformService, - final ClientWritePlatformService clientWritePlatformService, final DefaultToApiJsonSerializer toApiJsonSerializer) { + final ClientWritePlatformService clientWritePlatformService, final DefaultToApiJsonSerializer toApiJsonSerializer) { this.context = context; this.clientReadPlatformService = readPlatformService; this.clientWritePlatformService = clientWritePlatformService; @@ -76,12 +64,11 @@ public class ClientImagesApiResource { // TODO: vishwas might need more advances validation (like reading magic // number) for handling malicious clients // and clients not setting mime type - FileUtils.validateClientImageNotEmpty(fileDetails.getFileName()); - FileUtils.validateImageMimeType(bodyPart.getMediaType().toString()); - FileUtils.validateFileSizeWithinPermissibleRange(fileSize, fileDetails.getFileName(), ApiConstants.MAX_FILE_UPLOAD_SIZE_IN_MB); + ImageUtils.validateClientImageNotEmpty(fileDetails.getFileName()); + ImageUtils.validateImageMimeType(bodyPart.getMediaType().toString()); final CommandProcessingResult result = this.clientWritePlatformService.saveOrUpdateClientImage(clientId, fileDetails.getFileName(), - inputStream); + inputStream, fileSize); return this.toApiJsonSerializer.serialize(result); } @@ -94,7 +81,7 @@ public class ClientImagesApiResource { @Produces({ MediaType.APPLICATION_JSON }) public String addNewClientImage(@PathParam("clientId") final Long clientId, final String jsonRequestBody) { - final Base64EncodedImage base64EncodedImage = FileUtils.extractImageFromDataURL(jsonRequestBody); + final Base64EncodedImage base64EncodedImage = ImageUtils.extractImageFromDataURL(jsonRequestBody); final CommandProcessingResult result = this.clientWritePlatformService.saveOrUpdateClientImage(clientId, base64EncodedImage); @@ -116,15 +103,14 @@ public class ClientImagesApiResource { if (clientData.imageKeyDoesNotExist()) { throw new ImageNotFoundException("clients", clientId); } // TODO: Need a better way of determining image type - String imageDataURISuffix = IMAGE_DATA_URI_SUFFIX.JPEG.getValue(); - if (StringUtils.endsWith(clientData.imageKey(), IMAGE_FILE_EXTENSION.GIF.getValue())) { - imageDataURISuffix = IMAGE_DATA_URI_SUFFIX.GIF.getValue(); - } else if (StringUtils.endsWith(clientData.imageKey(), IMAGE_FILE_EXTENSION.PNG.getValue())) { - imageDataURISuffix = IMAGE_DATA_URI_SUFFIX.PNG.getValue(); + String imageDataURISuffix = ImageUtils.IMAGE_DATA_URI_SUFFIX.JPEG.getValue(); + if (StringUtils.endsWith(clientData.imageKey(), ImageUtils.IMAGE_FILE_EXTENSION.GIF.getValue())) { + imageDataURISuffix = ImageUtils.IMAGE_DATA_URI_SUFFIX.GIF.getValue(); + } else if (StringUtils.endsWith(clientData.imageKey(), ImageUtils.IMAGE_FILE_EXTENSION.PNG.getValue())) { + imageDataURISuffix = ImageUtils.IMAGE_DATA_URI_SUFFIX.PNG.getValue(); } - String clientImageAsBase64Text = imageDataURISuffix + Base64.encodeFromFile(clientData.imageKey()); - + String clientImageAsBase64Text = imageDataURISuffix + Base64.encodeBytes(clientData.image()); return clientImageAsBase64Text; } @@ -138,22 +124,12 @@ public class ClientImagesApiResource { if (clientData.imageKeyDoesNotExist()) { throw new ImageNotFoundException("clients", clientId); } - File image = new File(clientData.imageKey()); - String imageName = image.getName(); - ResponseBuilder response = Response.ok(image); - response.header("Content-Disposition", "attachment; filename=\"" + imageName + "\""); + ResponseBuilder response = Response.ok(clientData.image()); + response.header("Content-Disposition", "attachment; filename=\"" + clientData.imageName() + "\""); // TODO: Need a better way of determining image type - // determine image type - String contentType = IMAGE_MIME_TYPE.JPEG.getValue(); - if (StringUtils.endsWith(imageName, IMAGE_FILE_EXTENSION.GIF.getValue())) { - contentType = IMAGE_MIME_TYPE.GIF.getValue(); - } else if (StringUtils.endsWith(imageName, IMAGE_FILE_EXTENSION.PNG.getValue())) { - contentType = IMAGE_MIME_TYPE.PNG.getValue(); - } - - response.header("Content-Type", contentType); + response.header("Content-Type", clientData.imageContentType()); return response.build(); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/data/ClientData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/data/ClientData.java index d4ee1e225..d72f85f80 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/data/ClientData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/data/ClientData.java @@ -5,9 +5,6 @@ */ package org.mifosplatform.portfolio.client.data; -import java.util.Collection; -import java.util.List; - import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.builder.CompareToBuilder; import org.apache.commons.lang.builder.EqualsBuilder; @@ -16,12 +13,21 @@ import org.joda.time.LocalDate; import org.mifosplatform.infrastructure.core.data.EnumOptionData; import org.mifosplatform.organisation.office.data.OfficeData; import org.mifosplatform.portfolio.group.data.GroupGeneralData; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.Collection; +import java.util.List; /** * Immutable data object representing client data. */ final public class ClientData implements Comparable { + private final static Logger logger = LoggerFactory.getLogger(ClientData.class); + + private final Long id; private final String accountNo; private final String externalId; @@ -40,7 +46,8 @@ final public class ClientData implements Comparable { private final Long officeId; private final String officeName; - private final String imageKey; +// private String imageData; + private ImageData imageData; @SuppressWarnings("unused") private final Boolean imagePresent; @@ -58,13 +65,13 @@ final public class ClientData implements Comparable { return new ClientData(clientData.accountNo, clientData.status, clientData.officeId, clientData.officeName, clientData.id, clientData.firstname, clientData.middlename, clientData.lastname, clientData.fullname, clientData.displayName, - clientData.externalId, clientData.activationDate, clientData.imageKey, allowedOffices, clientData.groups); + clientData.externalId, clientData.activationDate, clientData.imageData, allowedOffices, clientData.groups); } public static ClientData setParentGroups(final ClientData clientData, final Collection parentGroups) { return new ClientData(clientData.accountNo, clientData.status, clientData.officeId, clientData.officeName, clientData.id, clientData.firstname, clientData.middlename, clientData.lastname, clientData.fullname, clientData.displayName, - clientData.externalId, clientData.activationDate, clientData.imageKey, clientData.officeOptions, parentGroups); + clientData.externalId, clientData.activationDate, clientData.imageData, clientData.officeOptions, parentGroups); } public static ClientData clientIdentifier(final Long id, final String accountNo, final EnumOptionData status, final String firstname, @@ -81,14 +88,14 @@ final public class ClientData implements Comparable { public static ClientData instance(final String accountNo, final EnumOptionData status, final Long officeId, final String officeName, final Long id, final String firstname, final String middlename, final String lastname, final String fullname, - final String displayName, final String externalId, final LocalDate activationDate, final String imageKey) { + final String displayName, final String externalId, final LocalDate activationDate, final ImageData imageData) { return new ClientData(accountNo, status, officeId, officeName, id, firstname, middlename, lastname, fullname, displayName, - externalId, activationDate, imageKey, null, null); + externalId, activationDate, imageData, null, null); } private ClientData(final String accountNo, final EnumOptionData status, final Long officeId, final String officeName, final Long id, final String firstname, final String middlename, final String lastname, final String fullname, final String displayName, - final String externalId, final LocalDate activationDate, final String imageKey, final Collection allowedOffices, + final String externalId, final LocalDate activationDate, final ImageData imageKey, final Collection allowedOffices, final Collection groups) { this.accountNo = accountNo; this.status = status; @@ -107,7 +114,7 @@ final public class ClientData implements Comparable { this.displayName = StringUtils.defaultIfEmpty(displayName, null); this.externalId = StringUtils.defaultIfEmpty(externalId, null); this.activationDate = activationDate; - this.imageKey = imageKey; + this.imageData = imageKey; if (imageKey != null) { this.imagePresent = Boolean.TRUE; } else { @@ -138,17 +145,16 @@ final public class ClientData implements Comparable { } public String imageKey() { - return this.imageKey; + return this.imageData.imageKey(); } public boolean imageKeyDoesNotExist() { - return !imageKeyExists(); + return imageData.equals(null) && !imageKeyExists(); } private boolean imageKeyExists() { - return StringUtils.isNotBlank(this.imageKey); + return StringUtils.isNotBlank(this.imageData.imageKey()); } - @Override public int compareTo(final ClientData obj) { if (obj == null) { return -1; } @@ -194,4 +200,25 @@ final public class ClientData implements Comparable { public LocalDate getActivationDate() { return this.activationDate; } + + public void setImageData(ImageData imageData){ + this.imageData = imageData; + } + + public String imageName() { + return imageData.name(); + } + + public String imageContentType() { + return imageData.contentType(); + } + + public byte[] image(){ + try{ + return imageData.getContent(); + }catch (IOException ioe){ + logger.error("Error in fetching client image"); + return null; + } + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/data/ImageData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/data/ImageData.java new file mode 100644 index 000000000..95ede014c --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/data/ImageData.java @@ -0,0 +1,78 @@ +package org.mifosplatform.portfolio.client.data; + +import org.apache.commons.lang.StringUtils; +import org.apache.poi.util.IOUtils; +import org.mifosplatform.infrastructure.core.service.DocumentStore; +import org.mifosplatform.infrastructure.core.service.DocumentStoreType; +import org.mifosplatform.infrastructure.core.service.ImageUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class ImageData { + + private final Long clientId; + private final String key; + // private final DocumentStoreType storeType; + private final String storeType; + private File file; + private String contentType; + private String name; + private InputStream inputStream; + + + public ImageData(Long clientId, String key, String storeType) { + this.clientId = clientId; + this.key = key; + this.storeType = storeType; + } + + public String imageKey() { + return this.key; + } + + public byte[] getContent() throws IOException { + if(inputStream == null){ + FileInputStream fileInputStream = new FileInputStream(file); + return IOUtils.toByteArray(fileInputStream); + } + else + { + return IOUtils.toByteArray(inputStream); + } + } + + private String setImageContentType() { + String fileName = file.getName(); + String contentType = ImageUtils.IMAGE_MIME_TYPE.JPEG.getValue(); + if (StringUtils.endsWith(fileName, ImageUtils.IMAGE_FILE_EXTENSION.GIF.getValue())) { + contentType = ImageUtils.IMAGE_MIME_TYPE.GIF.getValue(); + } else if (StringUtils.endsWith(fileName, ImageUtils.IMAGE_FILE_EXTENSION.PNG.getValue())) { + contentType = ImageUtils.IMAGE_MIME_TYPE.PNG.getValue(); + } + return contentType; + } + + public String contentType(){ + return this.contentType; + } + + public DocumentStoreType storeType() { + return DocumentStore.getDocumentStoreType(storeType); + } + + public String name(){ + return this.file.getName(); + } + + + public void setContent(InputStream objectContent) { + this.inputStream = objectContent; + } + + public void setContent(File file) { + this.file = file; + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/domain/Client.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/domain/Client.java index 4a1ebff7f..f9400bbd2 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/domain/Client.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/domain/Client.java @@ -5,26 +5,6 @@ */ package org.mifosplatform.portfolio.client.domain; -import java.util.ArrayList; -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.Column; -import javax.persistence.Entity; -import javax.persistence.JoinColumn; -import javax.persistence.JoinTable; -import javax.persistence.ManyToMany; -import javax.persistence.ManyToOne; -import javax.persistence.Table; -import javax.persistence.Temporal; -import javax.persistence.TemporalType; -import javax.persistence.Transient; -import javax.persistence.UniqueConstraint; - import org.apache.commons.lang.StringUtils; import org.joda.time.LocalDate; import org.joda.time.format.DateTimeFormatter; @@ -39,6 +19,9 @@ import org.mifosplatform.portfolio.client.api.ClientApiConstants; import org.mifosplatform.portfolio.group.domain.Group; import org.springframework.data.jpa.domain.AbstractPersistable; +import javax.persistence.*; +import java.util.*; + @Entity @Table(name = "m_client", uniqueConstraints = { @UniqueConstraint(columnNames = { "account_no" }, name = "account_no_UNIQUE") }) public final class Client extends AbstractPersistable { @@ -79,9 +62,6 @@ public final class Client extends AbstractPersistable { @Column(name = "external_id", length = 100, nullable = true, unique = true) private String externalId; - @Column(name = "image_key", length = 500, nullable = true) - private String imageKey; - @ManyToMany @JoinTable(name = "m_group_client", joinColumns = @JoinColumn(name = "client_id"), inverseJoinColumns = @JoinColumn(name = "group_id")) private Set groups; @@ -385,13 +365,12 @@ public final class Client extends AbstractPersistable { return this.office.identifiedBy(officeId); } - public String imageKey() { - return imageKey; - } - - public void updateImageKey(final String imageKey) { - this.imageKey = imageKey; - } +// public String imageKey() { +// } +// +// public void updateImageKey(final String imageKey) { +// this.image.setKey(imageKey); +// } public Long officeId() { return this.office.getId(); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/domain/Image.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/domain/Image.java new file mode 100644 index 000000000..d186f5b26 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/domain/Image.java @@ -0,0 +1,62 @@ +/** + * 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.client.domain; + +import org.mifosplatform.infrastructure.core.service.DocumentStoreType; +import org.springframework.data.jpa.domain.AbstractPersistable; + +import javax.persistence.*; + +@Entity +@Table(name = "m_image") +public final class Image extends AbstractPersistable { + + // @Column(name = "`client_id`") + @OneToOne() + @JoinColumn(name = "`client_id`") + private Client client; + + @Column(name = "`key`", length = 500) + private String key; + + @Column(name = "`storage_type`", length = 50) + private String storeType; + + public Image(Client client, String image_key, DocumentStoreType storeType) { + this.client = client; + this.key = image_key; + this.storeType = storeType.getValue(); + } + + public Image() { + + } + + + public Image(Client client) { + this.client = client; + this.key = null; + this.storeType = null; + } + + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String updateKey(String key) { + setKey(key); + return this.key; + } + + public void updateStorageType(String documentStoreType) { + this.storeType = documentStoreType; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/domain/ImageRepository.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/domain/ImageRepository.java new file mode 100644 index 000000000..ed0fe6cc1 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/domain/ImageRepository.java @@ -0,0 +1,13 @@ +/** + * 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.client.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface ImageRepository extends JpaRepository, JpaSpecificationExecutor { + Image findOneByClient(Client client); +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientReadPlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientReadPlatformService.java index fc87e6cdf..33d6808f0 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientReadPlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientReadPlatformService.java @@ -5,6 +5,7 @@ */ package org.mifosplatform.portfolio.client.service; + import java.util.Collection; import org.mifosplatform.infrastructure.core.service.Page; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientReadPlatformServiceImpl.java index 714f29a36..3fb4b72a1 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientReadPlatformServiceImpl.java @@ -16,6 +16,7 @@ import org.joda.time.LocalDate; import org.mifosplatform.infrastructure.core.api.ApiParameterHelper; import org.mifosplatform.infrastructure.core.data.EnumOptionData; import org.mifosplatform.infrastructure.core.domain.JdbcSupport; +import org.mifosplatform.infrastructure.core.service.DocumentStoreFactory; import org.mifosplatform.infrastructure.core.service.Page; import org.mifosplatform.infrastructure.core.service.PaginationHelper; import org.mifosplatform.infrastructure.core.service.TenantAwareRoutingDataSource; @@ -25,6 +26,7 @@ import org.mifosplatform.organisation.office.service.OfficeReadPlatformService; import org.mifosplatform.portfolio.client.data.ClientAccountSummaryCollectionData; import org.mifosplatform.portfolio.client.data.ClientAccountSummaryData; import org.mifosplatform.portfolio.client.data.ClientData; +import org.mifosplatform.portfolio.client.data.ImageData; import org.mifosplatform.portfolio.client.domain.ClientEnumerations; import org.mifosplatform.portfolio.client.exception.ClientNotFoundException; import org.mifosplatform.portfolio.group.data.GroupGeneralData; @@ -38,12 +40,14 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Service; -@Service(value = "clientReadPlatformService") +@Service public class ClientReadPlatformServiceImpl implements ClientReadPlatformService { + private final JdbcTemplate jdbcTemplate; private final PlatformSecurityContext context; private final OfficeReadPlatformService officeReadPlatformService; + private final DocumentStoreFactory documentStoreFactory; // data mappers private final PaginationHelper paginationHelper = new PaginationHelper(); @@ -54,9 +58,10 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService @Autowired public ClientReadPlatformServiceImpl(final PlatformSecurityContext context, final TenantAwareRoutingDataSource dataSource, - final OfficeReadPlatformService officeReadPlatformService) { + final OfficeReadPlatformService officeReadPlatformService, final DocumentStoreFactory documentStoreFactory) { this.context = context; this.officeReadPlatformService = officeReadPlatformService; + this.documentStoreFactory = documentStoreFactory; this.jdbcTemplate = new JdbcTemplate(dataSource); } @@ -92,7 +97,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService if (searchParameters.isOrderByRequested()) { sqlBuilder.append(" order by ").append(searchParameters.getOrderBy()); - + if (searchParameters.isSortOrderProvided()) { sqlBuilder.append(' ').append(searchParameters.getSortOrder()); } @@ -109,7 +114,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService return this.paginationHelper.fetchPage(this.jdbcTemplate, sqlCountRows, sqlBuilder.toString(), new Object[] { hierarchySearchString }, this.clientMapper); } - + private String buildSqlStringFromClientCriteria(final SearchParameters searchParameters) { final String sqlSearch = searchParameters.getSqlSearch(); @@ -159,7 +164,6 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService @Override public ClientData retrieveOne(final Long clientId) { - try { AppUser currentUser = context.authenticatedUser(); String hierarchy = currentUser.getOffice().getHierarchy(); @@ -173,7 +177,15 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService Collection parentGroups = this.jdbcTemplate.query(clientGroupsSql, this.clientGroupsMapper, new Object[] { clientId }); + + String clientImageSql = "select * from m_image where client_id =" + clientId; + + try { + ImageData imageData = this.jdbcTemplate.queryForObject(clientImageSql, new ImageMapper()); + clientData.setImageData(documentStoreFactory.getInstanceFromStorageType(imageData.storeType()).retrieveImage(imageData)); + } catch (EmptyResultDataAccessException e) {} return ClientData.setParentGroups(clientData, parentGroups); + } catch (EmptyResultDataAccessException e) { throw new ClientNotFoundException(clientId); } @@ -206,8 +218,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService final String hierarchy = currentUser.getOffice().getHierarchy(); final String hierarchySearchString = hierarchy + "%"; - final String sql = "select " + this.membersOfGroupMapper.schema() - + " where o.hierarchy like ? and pgc.group_id = ?"; + final String sql = "select " + this.membersOfGroupMapper.schema() + " where o.hierarchy like ? and pgc.group_id = ?"; return this.jdbcTemplate.query(sql, this.membersOfGroupMapper, new Object[] { hierarchySearchString, groupId }); } @@ -255,7 +266,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService final String officeName = rs.getString("officeName"); return ClientData.instance(accountNo, status, officeId, officeName, id, firstname, middlename, lastname, fullname, displayName, - externalId, activationDate, imageKey); + externalId, activationDate, null); } } @@ -299,11 +310,11 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService final String displayName = rs.getString("displayName"); final String externalId = rs.getString("externalId"); final LocalDate activationDate = JdbcSupport.getLocalDate(rs, "activationDate"); - final String imageKey = rs.getString("imageKey"); + // final String imageKey = rs.getString("imageKey"); final String officeName = rs.getString("officeName"); return ClientData.instance(accountNo, status, officeId, officeName, id, firstname, middlename, lastname, fullname, displayName, - externalId, activationDate, imageKey); + externalId, activationDate, null); } } @@ -325,6 +336,18 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService } } + private static final class ImageMapper implements RowMapper { + + @Override + public ImageData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + final Long clientId = rs.getLong("client_id"); + final String imageKey = rs.getString("key"); + final String storeType = rs.getString("storage_type"); + return new ImageData(clientId, imageKey, storeType); + } + + } + private static final class ClientLookupMapper implements RowMapper { private final String schema; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientWritePlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientWritePlatformService.java index 9f8d5b43e..578b74c10 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientWritePlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientWritePlatformService.java @@ -23,7 +23,7 @@ public interface ClientWritePlatformService { CommandProcessingResult deleteClient(Long clientId); @PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'CREATE_CLIENTIMAGE')") - CommandProcessingResult saveOrUpdateClientImage(Long clientId, String imageName, InputStream inputStream); + CommandProcessingResult saveOrUpdateClientImage(Long clientId, String imageName, InputStream inputStream, Long fileSize); @PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'CREATE_CLIENTIMAGE')") CommandProcessingResult saveOrUpdateClientImage(Long clientId, Base64EncodedImage encodedImage); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientWritePlatformServiceJpaRepositoryImpl.java index 0bc4fb35f..3eb007ecb 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientWritePlatformServiceJpaRepositoryImpl.java @@ -5,13 +5,6 @@ */ package org.mifosplatform.portfolio.client.service; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; -import java.util.Locale; -import java.util.Map; - import org.apache.commons.lang.StringUtils; import org.joda.time.LocalDate; import org.joda.time.format.DateTimeFormat; @@ -21,7 +14,8 @@ import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder; import org.mifosplatform.infrastructure.core.domain.Base64EncodedImage; import org.mifosplatform.infrastructure.core.exception.PlatformDataIntegrityException; -import org.mifosplatform.infrastructure.core.service.FileUtils; +import org.mifosplatform.infrastructure.core.service.DocumentStore; +import org.mifosplatform.infrastructure.core.service.DocumentStoreFactory; import org.mifosplatform.infrastructure.documentmanagement.exception.DocumentManagementException; import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; import org.mifosplatform.organisation.office.domain.Office; @@ -29,10 +23,7 @@ import org.mifosplatform.organisation.office.domain.OfficeRepository; import org.mifosplatform.organisation.office.exception.OfficeNotFoundException; import org.mifosplatform.portfolio.client.api.ClientApiConstants; import org.mifosplatform.portfolio.client.data.ClientDataValidator; -import org.mifosplatform.portfolio.client.domain.AccountNumberGenerator; -import org.mifosplatform.portfolio.client.domain.AccountNumberGeneratorFactory; -import org.mifosplatform.portfolio.client.domain.Client; -import org.mifosplatform.portfolio.client.domain.ClientRepositoryWrapper; +import org.mifosplatform.portfolio.client.domain.*; import org.mifosplatform.portfolio.client.exception.ClientMustBePendingToBeDeletedException; import org.mifosplatform.portfolio.client.exception.ClientNotFoundException; import org.mifosplatform.portfolio.group.domain.Group; @@ -47,6 +38,11 @@ import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.io.InputStream; +import java.util.List; +import java.util.Locale; +import java.util.Map; + @Service public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWritePlatformService { @@ -59,12 +55,14 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP private final GroupRepository groupRepository; private final ClientDataValidator fromApiJsonDeserializer; private final AccountNumberGeneratorFactory accountIdentifierGeneratorFactory; + private final DocumentStoreFactory documentStoreFactory; + private final ImageRepository imageRepository; @Autowired public ClientWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context, - final ClientRepositoryWrapper clientRepository, final OfficeRepository officeRepository, final NoteRepository noteRepository, - final ClientDataValidator fromApiJsonDeserializer, final AccountNumberGeneratorFactory accountIdentifierGeneratorFactory, - final GroupRepository groupRepository) { + final ClientRepositoryWrapper clientRepository, final OfficeRepository officeRepository, final NoteRepository noteRepository, + final ClientDataValidator fromApiJsonDeserializer, final AccountNumberGeneratorFactory accountIdentifierGeneratorFactory, + final GroupRepository groupRepository, DocumentStoreFactory documentStoreFactory, ImageRepository imageRepository) { this.context = context; this.clientRepository = clientRepository; this.officeRepository = officeRepository; @@ -72,6 +70,8 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP this.fromApiJsonDeserializer = fromApiJsonDeserializer; this.accountIdentifierGeneratorFactory = accountIdentifierGeneratorFactory; this.groupRepository = groupRepository; + this.documentStoreFactory = documentStoreFactory; + this.imageRepository = imageRepository; } @Transactional @@ -223,16 +223,16 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP @Transactional @Override - public CommandProcessingResult saveOrUpdateClientImage(final Long clientId, final String imageName, final InputStream inputStream) { + public CommandProcessingResult saveOrUpdateClientImage(final Long clientId, final String imageName, final InputStream inputStream, Long fileSize) { try { final Client client = this.clientRepository.findOneWithNotFoundDetection(clientId); - String imageUploadLocation = setupForClientImageUpdate(clientId, client); + deletePreviousClientImage(clientId, client); - String imageLocation = FileUtils.saveToFileSystem(inputStream, imageUploadLocation, imageName); - - return updateClientImage(clientId, client, imageLocation); - } catch (IOException ioe) { - logger.error(ioe.getMessage(), ioe); + DocumentStore documentStore = this.documentStoreFactory.getInstanceFromConfiguration(); + String imageLocation = documentStore.saveImage(inputStream, clientId, imageName, fileSize); + return updateClientImage(clientId, client, imageLocation, documentStore.getType().getValue()); + } catch (DocumentManagementException dme) { + logger.error(dme.getMessage(), dme); throw new DocumentManagementException(imageName); } } @@ -243,48 +243,52 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP final Client client = this.clientRepository.findOneWithNotFoundDetection(clientId); + Image image = imageRepository.findOneByClient(client); + String imageKey = image.getKey(); // delete image from the file system - if (StringUtils.isNotEmpty(client.imageKey())) { - FileUtils.deleteClientImage(clientId, client.imageKey()); + if (StringUtils.isNotEmpty((imageKey))) { + DocumentStore documentStore = this.documentStoreFactory.getInstanceFromConfiguration(); + documentStore.deleteImage(clientId, imageKey); + this.imageRepository.delete(image); } - return updateClientImage(clientId, client, null); + + return new CommandProcessingResult(clientId) ; } @Override public CommandProcessingResult saveOrUpdateClientImage(final Long clientId, final Base64EncodedImage encodedImage) { try { final Client client = this.clientRepository.findOneWithNotFoundDetection(clientId); - final String imageUploadLocation = setupForClientImageUpdate(clientId, client); + deletePreviousClientImage(clientId, client); - final String imageLocation = FileUtils.saveToFileSystem(encodedImage, imageUploadLocation, "image"); + DocumentStore documentStore = this.documentStoreFactory.getInstanceFromConfiguration(); + final String imageLocation = documentStore.saveImage(encodedImage, clientId, "image"); - return updateClientImage(clientId, client, imageLocation); - } catch (IOException ioe) { - logger.error(ioe.getMessage(), ioe); + return updateClientImage(clientId, client, imageLocation, documentStore.getType().getValue()); + } catch (DocumentManagementException dme) { + logger.error(dme.getMessage(), dme); throw new DocumentManagementException("image"); } } - private String setupForClientImageUpdate(final Long clientId, final Client client) { + private void deletePreviousClientImage(final Long clientId, final Client client) { if (client == null) { throw new ClientNotFoundException(clientId); } - final String imageUploadLocation = FileUtils.generateClientImageParentDirectory(clientId); - // delete previous image from the file system - if (StringUtils.isNotEmpty(client.imageKey())) { - FileUtils.deleteClientImage(clientId, client.imageKey()); + Image image = imageRepository.findOneByClient(client); + if (image != null){ + this.documentStoreFactory.getInstanceFromConfiguration().deleteImage(clientId, image.getKey()); } - - /** Recursively create the directory if it does not exist **/ - if (!new File(imageUploadLocation).isDirectory()) { - new File(imageUploadLocation).mkdirs(); - } - return imageUploadLocation; } - private CommandProcessingResult updateClientImage(final Long clientId, final Client client, final String imageLocation) { - client.updateImageKey(imageLocation); - this.clientRepository.save(client); + private CommandProcessingResult updateClientImage(final Long clientId, final Client client, final String imageLocation, final String documentStoreType) { + Image image = imageRepository.findOneByClient(client); + if(image == null){ + image = new Image(client); + } + image.updateKey(imageLocation); + image.updateStorageType(documentStoreType); + this.imageRepository.save(image); return new CommandProcessingResult(clientId); } diff --git a/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainServiceJpaTest.java b/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainServiceJpaTest.java new file mode 100644 index 000000000..5fe094e14 --- /dev/null +++ b/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainServiceJpaTest.java @@ -0,0 +1,46 @@ +package org.mifosplatform.infrastructure.configuration.domain; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mifosplatform.useradministration.domain.Permission; +import org.mifosplatform.useradministration.domain.PermissionRepository; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +import java.util.List; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@SuppressWarnings("unused") +@RunWith(MockitoJUnitRunner.class) +public class ConfigurationDomainServiceJpaTest { + + private final String S3 = "amazon-S3"; + private PermissionRepository permissionRepositoryMock = mock(PermissionRepository.class);; + private GlobalConfigurationRepository globalConfigurationRepositoryMock = mock(GlobalConfigurationRepository.class); + + @Test + public void shouldReturnTrueIfAmazonS3ConfigurationIsEnabled(){ + when(globalConfigurationRepositoryMock.findOneByName(S3)).thenReturn(new GlobalConfigurationProperty(S3, true)); + ConfigurationDomainServiceJpa configurationDomainServiceJpa = new ConfigurationDomainServiceJpa(permissionRepositoryMock, globalConfigurationRepositoryMock); + + boolean actualIsS3Enabled = configurationDomainServiceJpa.isAmazonS3Enabled(); + + assertTrue(actualIsS3Enabled); + } + + @Test + public void shouldReturnFalseIfAmazonS3ConfigurationIsDisabled(){ + when(globalConfigurationRepositoryMock.findOneByName(S3)).thenReturn(new GlobalConfigurationProperty(S3, false)); + ConfigurationDomainServiceJpa configurationDomainServiceJpa = new ConfigurationDomainServiceJpa(permissionRepositoryMock, globalConfigurationRepositoryMock); + + boolean actualIsS3Enabled = configurationDomainServiceJpa.isAmazonS3Enabled(); + + assertFalse(actualIsS3Enabled); + } +} diff --git a/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/DocumentStoreFactoryTest.java b/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/DocumentStoreFactoryTest.java new file mode 100644 index 000000000..8592df1f9 --- /dev/null +++ b/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/DocumentStoreFactoryTest.java @@ -0,0 +1,55 @@ +package org.mifosplatform.infrastructure.core.service; + + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mifosplatform.infrastructure.configuration.domain.ConfigurationDomainService; +import org.mifosplatform.infrastructure.configuration.domain.ExternalServicesDomainService; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.context.ApplicationContext; + +import static junit.framework.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@SuppressWarnings("unused") + +@RunWith(MockitoJUnitRunner.class) +public class DocumentStoreFactoryTest { + + @Test + public void shouldReturnS3DocumentStoreObjectIfS3Enabled(){ + S3ClientFactory s3ClientFactoryMock = mock(S3ClientFactory.class); + when(s3ClientFactoryMock.getS3Client()).thenReturn(null); + + ExternalServicesDomainService externalServicesDomainServiceMock = mock(ExternalServicesDomainService.class); + when(externalServicesDomainServiceMock.getValue("s3_bucket_name")).thenReturn("bucket_name"); + + ConfigurationDomainService configurationDomainServiceMock = mock(ConfigurationDomainService.class); + when(configurationDomainServiceMock.isAmazonS3Enabled()).thenReturn(true); + + ApplicationContext applicationContextMock = mock(ApplicationContext.class); + + when(applicationContextMock.getBean("configurationDomainServiceJpa", ConfigurationDomainService.class)).thenReturn(configurationDomainServiceMock); + when(applicationContextMock.getBean("s3ClientFactory", S3ClientFactory.class)).thenReturn(s3ClientFactoryMock); + when(applicationContextMock.getBean("externalServicesDomainService", ExternalServicesDomainService.class)).thenReturn(externalServicesDomainServiceMock); + + + DocumentStoreFactory documentStoreFactory = new DocumentStoreFactory(applicationContextMock); + assertEquals(documentStoreFactory.getInstanceFromConfiguration().getClass(),S3DocumentStore.class); + } + + @Test + public void shouldReturnFileSystemDocumentStoreObjectIfS3IsDisabled(){ + ConfigurationDomainService configurationDomainService = mock(ConfigurationDomainService.class); + when(configurationDomainService.isAmazonS3Enabled()).thenReturn(false); + + ApplicationContext applicationContext = mock(ApplicationContext.class); + when(applicationContext.getBean("configurationDomainServiceJpa", ConfigurationDomainService.class)).thenReturn(configurationDomainService); + + DocumentStoreFactory documentStoreFactory = new DocumentStoreFactory(applicationContext); + assertEquals(documentStoreFactory.getInstanceFromConfiguration().getClass(),FileSystemDocumentStore.class); + } + + +} diff --git a/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/FileDocumentStoreTest.java b/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/FileDocumentStoreTest.java new file mode 100644 index 000000000..2f70fe84c --- /dev/null +++ b/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/FileDocumentStoreTest.java @@ -0,0 +1,31 @@ +package org.mifosplatform.infrastructure.core.service; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mifosplatform.infrastructure.documentmanagement.data.DocumentData; +import org.mifosplatform.infrastructure.documentmanagement.data.FileData; +import org.mockito.runners.MockitoJUnitRunner; + +import static junit.framework.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@SuppressWarnings("unused") + +@RunWith(MockitoJUnitRunner.class) +public class FileDocumentStoreTest { + + @Test + public void shouldRetrieveDocument(){ + FileSystemDocumentStore fileSystemDocumentStore = new FileSystemDocumentStore(); + DocumentData documentDataMock = mock(DocumentData.class); + when(documentDataMock.fileName()).thenReturn("some_file"); + when(documentDataMock.fileName()).thenReturn("content_type"); + when(documentDataMock.fileLocation()).thenReturn("abcd"); + + FileData fileData = fileSystemDocumentStore.retrieveDocument(documentDataMock); + + assertEquals(fileData.name(), documentDataMock.fileName()); + assertEquals(fileData.contentType(), documentDataMock.contentType()); + } +} diff --git a/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/S3DocumentStoreTest.java b/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/S3DocumentStoreTest.java new file mode 100644 index 000000000..3307d7d96 --- /dev/null +++ b/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/S3DocumentStoreTest.java @@ -0,0 +1,135 @@ +package org.mifosplatform.infrastructure.core.service; + +import com.amazonaws.AmazonClientException; +import com.amazonaws.AmazonServiceException; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.*; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mifosplatform.infrastructure.documentmanagement.command.DocumentCommand; +import org.mifosplatform.infrastructure.documentmanagement.data.DocumentData; +import org.mifosplatform.infrastructure.documentmanagement.data.FileData; +import org.mifosplatform.infrastructure.documentmanagement.exception.DocumentManagementException; +import org.mifosplatform.infrastructure.documentmanagement.exception.DocumentNotFoundException; +import org.mockito.Matchers; +import org.mockito.runners.MockitoJUnitRunner; + +import java.io.IOException; +import java.io.InputStream; + +import static junit.framework.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.*; + +@SuppressWarnings("unused") + +@RunWith(MockitoJUnitRunner.class) +public class S3DocumentStoreTest { + + InputStream inputStream; + AmazonS3Client s3ClientMock; + DocumentCommand documentCommand; + DocumentData documentDataMock; + + @Test + public void shouldGenerateParentDirectoryFromEntityDetails(){ + S3DocumentStore s3DocumentStore=new S3DocumentStore("bucketName", null); + String actualParentDirectory = s3DocumentStore.generateFileParentDirectory("entity_type", 1234L); + assertTrue(actualParentDirectory.startsWith("documents/entity_type/1234")); + } + + @Test + public void shouldGenerateClientImageDirectoryFromResourceId(){ + S3DocumentStore s3DocumentStore=new S3DocumentStore("bucketName", null); + String actualParentDirectory = s3DocumentStore.generateClientImageParentDirectory(1234L); + assertTrue(actualParentDirectory.startsWith("images/clients/1234")); + } + + @Test + public void shouldSaveADocumentToS3() throws IOException { + initUpload(); + when(s3ClientMock.putObject(Matchers.anyObject())).thenReturn(new PutObjectResult()); + S3DocumentStore s3DocumentStore = new S3DocumentStore("bucketName", s3ClientMock); + + s3DocumentStore.saveDocument(inputStream, documentCommand); + + verify(s3ClientMock, times(1)).putObject(Matchers.anyObject()); + } + + @Test(expected = DocumentManagementException.class) + public void shouldCatchAmazonServiceExceptionWhenUnableToUploadDocumentToS3() throws IOException { + initUpload(); + when(s3ClientMock.putObject(Matchers.anyObject())).thenThrow(new AmazonServiceException("serviceException")); + + S3DocumentStore s3DocumentStore = new S3DocumentStore("bucketName", s3ClientMock); + + s3DocumentStore.saveDocument(inputStream, documentCommand); + } + + @Test(expected = DocumentManagementException.class) + public void shouldCatchAmazonClientExceptionWhenUnableToUploadDocumentToS3() throws IOException { + initUpload(); + when(s3ClientMock.putObject(Matchers.anyObject())).thenThrow(new AmazonClientException("clientException")); + + S3DocumentStore s3DocumentStore = new S3DocumentStore("bucketName", s3ClientMock); + + s3DocumentStore.saveDocument(inputStream, documentCommand); + } + + @Test + public void shouldRetrieveDocumentFromS3() throws IOException { + initRetrieve(); + when(s3ClientMock.getObject(Matchers.anyObject())).thenReturn(new S3Object()); + S3DocumentStore s3DocumentStore = new S3DocumentStore("bucketName",s3ClientMock); + + FileData fileData = s3DocumentStore.retrieveDocument(documentDataMock); + + verify(s3ClientMock, times(1)).getObject(Matchers.anyObject()); + assertEquals(fileData.name(), "name"); + assertEquals(fileData.contentType(), "type"); + } + + @Test(expected = DocumentNotFoundException.class) + public void shouldCatchAmazonServiceExceptionWhenUnableToRetrieveDocumentFromS3() throws IOException { + initRetrieve(); + when(s3ClientMock.getObject(Matchers.anyObject())).thenThrow(new AmazonServiceException("serviceException")); + S3DocumentStore s3DocumentStore = new S3DocumentStore("bucketName",s3ClientMock); + + s3DocumentStore.retrieveDocument(documentDataMock); + } + + @Test(expected = DocumentNotFoundException.class) + public void shouldCatchAmazonClientExceptionWhenUnableToRetrieveDocumentFromS3() throws IOException { + initRetrieve(); + when(s3ClientMock.getObject(Matchers.anyObject())).thenThrow(new AmazonClientException("clientException")); + S3DocumentStore s3DocumentStore = new S3DocumentStore("bucketName",s3ClientMock); + + s3DocumentStore.retrieveDocument(documentDataMock); + } + + @Test + public void shouldDeleteImageFromS3() throws IOException { + AmazonS3Client s3ClientMock = mock(AmazonS3Client.class); + S3DocumentStore s3DocumentStore = new S3DocumentStore("bucketName",s3ClientMock); + + s3DocumentStore.deleteImage(123L,"imagekey"); + + verify(s3ClientMock, times(1)).deleteObject(Matchers.anyObject()); + } + + private void initRetrieve() { + s3ClientMock = mock(AmazonS3Client.class); + + documentDataMock = mock(DocumentData.class); + when(documentDataMock.fileLocation()).thenReturn("path"); + when(documentDataMock.fileName()).thenReturn("name"); + when(documentDataMock.contentType()).thenReturn("type"); + } + + + private void initUpload(){ + inputStream = S3DocumentStoreTest.class.getResourceAsStream("DummyString"); + s3ClientMock = mock(AmazonS3Client.class); + documentCommand = new DocumentCommand(null, -1l, "parentEntityType", 1l, null, "someFileName", 3l, null, null, null); + } +} From 15311f6425e2770acb6d8f3ff9700c0183ab991f Mon Sep 17 00:00:00 2001 From: Vishwas Babu AJ Date: Mon, 3 Jun 2013 02:05:43 +0530 Subject: [PATCH 18/49] Refactoring document and image management --- .../core_db/V40__adding_S3_Support.sql | 28 --- .../core_db/V48__adding-S3-Support.sql | 70 ++++++ .../configuration/data/S3CredentialsData.java | 27 +++ ...viceProperty.java => ExternalService.java} | 16 +- .../domain/ExternalServiceRepository.java | 5 +- .../domain/ExternalServicesDomainService.java | 18 -- .../service/ExternalServicesConstants.java | 9 + .../ExternalServicesReadPlatformService.java | 9 + ...ternalServicesReadPlatformServiceImpl.java | 51 +++++ .../infrastructure/core/api/ApiConstants.java | 15 -- .../DefaultPlatformPasswordEncoder.java | 3 +- .../core/service/DocumentStore.java | 52 ----- .../core/service/DocumentStoreFactory.java | 45 ---- .../core/service/DocumentStoreType.java | 15 -- .../core/service/RandomStringGenerator.java | 33 --- .../core/service/S3ClientFactory.java | 24 --- .../core/service/S3DocumentStore.java | 201 ------------------ .../service/ReadReportingServiceImpl.java | 6 +- .../api/DocumentManagementApiResource.java | 9 +- .../api/ImagesApiResource.java} | 113 +++++----- .../command/DocumentCommand.java | 19 +- .../contentrepository/ContentRepository.java | 38 ++++ .../ContentRepositoryFactory.java | 41 ++++ .../ContentRepositoryUtils.java} | 57 ++++- .../FileSystemContentRepository.java} | 84 ++++---- .../S3ContentRepository.java | 157 ++++++++++++++ .../documentmanagement/data/DocumentData.java | 27 +-- .../documentmanagement/data/FileData.java | 17 +- .../documentmanagement/data/ImageData.java | 90 ++++++++ .../documentmanagement/domain/Document.java | 26 +-- .../documentmanagement/domain/Image.java | 49 +++++ .../domain/ImageRepository.java | 7 +- .../domain/StorageType.java | 30 +++ ...n.java => ContentManagementException.java} | 9 +- .../service/DocumentReadPlatformService.java | 2 +- .../DocumentReadPlatformServiceImpl.java | 66 +++--- ...WritePlatformServiceJpaRepositoryImpl.java | 55 +++-- .../service/ImageReadPlatformService.java | 14 ++ .../service/ImageReadPlatformServiceImpl.java | 79 +++++++ .../service/ImageWritePlatformService.java | 24 +++ ...WritePlatformServiceJpaRepositoryImpl.java | 103 +++++++++ .../portfolio/client/data/ClientData.java | 62 ++---- .../portfolio/client/data/ImageData.java | 78 ------- .../portfolio/client/domain/Client.java | 30 ++- .../portfolio/client/domain/Image.java | 62 ------ .../ClientReadPlatformServiceImpl.java | 38 +--- .../service/ClientWritePlatformService.java | 12 -- ...WritePlatformServiceJpaRepositoryImpl.java | 101 +-------- .../service/DocumentStoreFactoryTest.java | 43 ++-- .../core/service/FileDocumentStoreTest.java | 18 +- .../core/service/S3DocumentStoreTest.java | 91 ++++---- 51 files changed, 1204 insertions(+), 1074 deletions(-) delete mode 100644 mifosng-db/migrations/core_db/V40__adding_S3_Support.sql create mode 100755 mifosng-db/migrations/core_db/V48__adding-S3-Support.sql create mode 100755 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/data/S3CredentialsData.java rename mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/{ExternalServiceProperty.java => ExternalService.java} (69%) delete mode 100644 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ExternalServicesDomainService.java create mode 100755 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/service/ExternalServicesConstants.java create mode 100755 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/service/ExternalServicesReadPlatformService.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/service/ExternalServicesReadPlatformServiceImpl.java delete mode 100644 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/api/ApiConstants.java delete mode 100644 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/DocumentStore.java delete mode 100644 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/DocumentStoreFactory.java delete mode 100644 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/DocumentStoreType.java delete mode 100644 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/RandomStringGenerator.java delete mode 100644 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/S3ClientFactory.java delete mode 100644 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/S3DocumentStore.java rename mifosng-provider/src/main/java/org/mifosplatform/{portfolio/client/api/ClientImagesApiResource.java => infrastructure/documentmanagement/api/ImagesApiResource.java} (62%) create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/contentrepository/ContentRepository.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/contentrepository/ContentRepositoryFactory.java rename mifosng-provider/src/main/java/org/mifosplatform/infrastructure/{core/service/ImageUtils.java => documentmanagement/contentrepository/ContentRepositoryUtils.java} (70%) rename mifosng-provider/src/main/java/org/mifosplatform/infrastructure/{core/service/FileSystemDocumentStore.java => documentmanagement/contentrepository/FileSystemContentRepository.java} (59%) create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/contentrepository/S3ContentRepository.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/data/ImageData.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/domain/Image.java rename mifosng-provider/src/main/java/org/mifosplatform/{portfolio/client => infrastructure/documentmanagement}/domain/ImageRepository.java (71%) create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/domain/StorageType.java rename mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/exception/{DocumentManagementException.java => ContentManagementException.java} (59%) create mode 100755 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/ImageReadPlatformService.java create mode 100755 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/ImageReadPlatformServiceImpl.java create mode 100755 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/ImageWritePlatformService.java create mode 100755 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/ImageWritePlatformServiceJpaRepositoryImpl.java delete mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/data/ImageData.java delete mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/domain/Image.java diff --git a/mifosng-db/migrations/core_db/V40__adding_S3_Support.sql b/mifosng-db/migrations/core_db/V40__adding_S3_Support.sql deleted file mode 100644 index ff1e63dec..000000000 --- a/mifosng-db/migrations/core_db/V40__adding_S3_Support.sql +++ /dev/null @@ -1,28 +0,0 @@ -INSERT INTO c_configuration (name,enabled) VALUES ('amazon-S3',0); - - -CREATE TABLE IF NOT EXISTS m_external_services ( - name VARCHAR(150) NOT NULL, - value VARCHAR(250), - UNIQUE(name) -); - -INSERT INTO m_external_services (name) VALUES ('s3_bucket_name'); -INSERT INTO m_external_services (name) VALUES ('s3_access_key'); -INSERT INTO m_external_services (name) VALUES ('s3_secret_key'); - -ALTER TABLE m_document ADD COLUMN storage_type varchar(50); - -CREATE TABLE IF NOT EXISTS `m_image`( - `id` BIGINT NOT NULL AUTO_INCREMENT, - `client_id` BIGINT NOT NULL, - `key` varchar(500), - `storage_type` varchar(50), - FOREIGN KEY (`client_id`) REFERENCES m_client(`id`), - PRIMARY KEY (`id`), - UNIQUE(client_id) -); - -UPDATE m_document set storage_type='file_system'; - -insert into m_image (`client_id`,`key`,`storage_type`) select id,`image_key`,'file_system' from m_client where `image_key` IS NOT NULL; diff --git a/mifosng-db/migrations/core_db/V48__adding-S3-Support.sql b/mifosng-db/migrations/core_db/V48__adding-S3-Support.sql new file mode 100755 index 000000000..9fab76c5a --- /dev/null +++ b/mifosng-db/migrations/core_db/V48__adding-S3-Support.sql @@ -0,0 +1,70 @@ +INSERT INTO c_configuration (name,enabled) VALUES ('amazon-S3',0); + +/*New table for storing details of external services used*/ +CREATE TABLE `c_external_service` ( + `name` VARCHAR(150) NOT NULL, + `value` VARCHAR(250) NULL DEFAULT NULL, + UNIQUE INDEX `name` (`name`) +) +COLLATE='utf8_general_ci' +ENGINE=InnoDB; + +INSERT INTO c_external_service (name) VALUES ('s3_bucket_name'); +INSERT INTO c_external_service (name) VALUES ('s3_access_key'); +INSERT INTO c_external_service (name) VALUES ('s3_secret_key'); + + + +/*Image tables stores details of all images*/ +CREATE TABLE IF NOT EXISTS `m_image`( + `id` BIGINT(20) NOT NULL AUTO_INCREMENT, + `location` varchar(500), + `storage_type_enum` SMALLINT(5), + PRIMARY KEY (`id`) +) +COLLATE='utf8_general_ci' +ENGINE=InnoDB; + +/*Client points to image table*/ +ALTER TABLE `m_client` + ADD COLUMN `image_id` BIGINT(20) NULL DEFAULT NULL AFTER `display_name`, + ADD CONSTRAINT `FK_m_client_m_image` FOREIGN KEY (`image_id`) REFERENCES `m_image` (`id`); + +/*Move existing image locations to new table*/ +drop procedure if exists migrate_customer_image_data; + +delimiter # +create procedure migrate_customer_image_data() +begin +declare v_counter int unsigned default 0; +declare num_of_clients INT DEFAULT 0; +declare curr_client INT DEFAULT 0; +declare curr_image INT DEFAULT 0; +declare prev_image INT DEFAULT 0; +SELECT COUNT(*) FROM m_client INTO num_of_clients; +while v_counter < num_of_clients do + INSERT INTO m_image(`location`,`storage_type_enum`) select image_key, 1 FROM m_client where image_key is not null LIMIT v_counter,1; + select ifnull(max(`id`),0) from m_image INTO curr_image; + select id FROM m_client where image_key is not null limit v_counter,1 INTO curr_client; + IF (prev_image != curr_image) THEN UPDATE m_client set image_id = curr_image where id=curr_client; + END IF; + set prev_image=curr_image; + set v_counter=v_counter+1; +end while; +delete from m_image where `location` is null; +end# + +delimiter ; +call migrate_customer_image_data(); + +/*Now drop the procedure*/ +drop procedure if exists migrate_customer_image_data; + +/*Delete image key from client*/ +ALTER TABLE `m_client` DROP COLUMN `image_key`; + + +/*Add storage type for m_document table and update existing documents to file storage*/ +ALTER TABLE m_document ADD COLUMN storage_type_enum SMALLINT(5); +UPDATE m_document set storage_type_enum=1; + diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/data/S3CredentialsData.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/data/S3CredentialsData.java new file mode 100755 index 000000000..914fe2474 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/data/S3CredentialsData.java @@ -0,0 +1,27 @@ +package org.mifosplatform.infrastructure.configuration.data; + +public class S3CredentialsData { + + private final String bucketName; + private final String accessKey; + private final String secretKey; + + public S3CredentialsData(String bucketName, String accessKey, String secretKey) { + this.bucketName = bucketName; + this.accessKey = accessKey; + this.secretKey = secretKey; + } + + public String getBucketName() { + return this.bucketName; + } + + public String getAccessKey() { + return this.accessKey; + } + + public String getSecretKey() { + return this.secretKey; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ExternalServiceProperty.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ExternalService.java similarity index 69% rename from mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ExternalServiceProperty.java rename to mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ExternalService.java index e9c7ef86f..1fca5d62d 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ExternalServiceProperty.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ExternalService.java @@ -1,20 +1,21 @@ package org.mifosplatform.infrastructure.configuration.domain; + /** * 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/. */ -import org.springframework.data.jpa.domain.AbstractPersistable; - import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Table; import javax.persistence.UniqueConstraint; +import org.springframework.data.jpa.domain.AbstractPersistable; + @Entity -@Table(name = "m_external_services", uniqueConstraints = { @UniqueConstraint(columnNames = ("name"))}) -public class ExternalServiceProperty extends AbstractPersistable { +@Table(name = "c_external_service", uniqueConstraints = { @UniqueConstraint(columnNames = ("name")) }) +public class ExternalService extends AbstractPersistable { @SuppressWarnings("unused") @Column(name = "name", nullable = false) @@ -23,15 +24,14 @@ public class ExternalServiceProperty extends AbstractPersistable { @Column(name = "value", nullable = false) private String value; - public ExternalServiceProperty(final String name, final String value) { + public ExternalService(final String name, final String value) { this.name = name; this.value = value; } - public ExternalServiceProperty() { - } + protected ExternalService() {} - public String getValue(){ + public String getValue() { return this.value; } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ExternalServiceRepository.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ExternalServiceRepository.java index 8acc015d6..f950a9c59 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ExternalServiceRepository.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ExternalServiceRepository.java @@ -3,8 +3,7 @@ package org.mifosplatform.infrastructure.configuration.domain; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; -public interface ExternalServiceRepository extends JpaRepository, - JpaSpecificationExecutor { +public interface ExternalServiceRepository extends JpaRepository, JpaSpecificationExecutor { - ExternalServiceProperty findOneByName(String name); + ExternalService findOneByName(String name); } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ExternalServicesDomainService.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ExternalServicesDomainService.java deleted file mode 100644 index e562c9ee0..000000000 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ExternalServicesDomainService.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.mifosplatform.infrastructure.configuration.domain; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -@Service -public class ExternalServicesDomainService { - private ExternalServiceRepository externalServiceRepository; - - @Autowired - public ExternalServicesDomainService(final ExternalServiceRepository externalServiceRepository) { - this.externalServiceRepository = externalServiceRepository; - } - - public String getValue(String name){ - return externalServiceRepository.findOneByName(name).getValue(); - } -} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/service/ExternalServicesConstants.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/service/ExternalServicesConstants.java new file mode 100755 index 000000000..5d6d2127d --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/service/ExternalServicesConstants.java @@ -0,0 +1,9 @@ +package org.mifosplatform.infrastructure.configuration.service; + +public class ExternalServicesConstants { + + public static final String S3_BUCKET_NAME = "s3_bucket_name"; + public static final String S3_ACCESS_KEY = "s3_access_key"; + public static final String S3_SECRET_KEY = "s3_secret_key"; + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/service/ExternalServicesReadPlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/service/ExternalServicesReadPlatformService.java new file mode 100755 index 000000000..91ec373d4 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/service/ExternalServicesReadPlatformService.java @@ -0,0 +1,9 @@ +package org.mifosplatform.infrastructure.configuration.service; + +import org.mifosplatform.infrastructure.configuration.data.S3CredentialsData; + +public interface ExternalServicesReadPlatformService { + + S3CredentialsData getS3Credentials(); + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/service/ExternalServicesReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/service/ExternalServicesReadPlatformServiceImpl.java new file mode 100644 index 000000000..83ec84d65 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/service/ExternalServicesReadPlatformServiceImpl.java @@ -0,0 +1,51 @@ +package org.mifosplatform.infrastructure.configuration.service; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.mifosplatform.infrastructure.configuration.data.S3CredentialsData; +import org.mifosplatform.infrastructure.core.service.TenantAwareRoutingDataSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.ResultSetExtractor; +import org.springframework.stereotype.Service; + +@Service +public class ExternalServicesReadPlatformServiceImpl implements ExternalServicesReadPlatformService { + + private final JdbcTemplate jdbcTemplate; + + @Autowired + public ExternalServicesReadPlatformServiceImpl(final TenantAwareRoutingDataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + private static final class S3CredentialsDataExtractor implements ResultSetExtractor { + + @Override + public S3CredentialsData extractData(ResultSet rs) throws SQLException, DataAccessException { + String accessKey = null; + String bucketName = null; + String secretKey = null; + while (rs.next()) { + if (rs.getString("name").equalsIgnoreCase(ExternalServicesConstants.S3_ACCESS_KEY)) { + accessKey = rs.getString("value"); + } else if (rs.getString("name").equalsIgnoreCase(ExternalServicesConstants.S3_BUCKET_NAME)) { + bucketName = rs.getString("value"); + } else if (rs.getString("name").equalsIgnoreCase(ExternalServicesConstants.S3_SECRET_KEY)) { + secretKey = rs.getString("value"); + } + } + return new S3CredentialsData(bucketName, accessKey, secretKey); + } + } + + @Override + public S3CredentialsData getS3Credentials() { + final ResultSetExtractor resultSetExtractor = new S3CredentialsDataExtractor(); + final String sql = "SELECT es.name, es.value FROM c_external_service es where es.name in('s3_bucket_name','s3_access_key','s3_secret_key')"; + final S3CredentialsData s3CredentialsData = this.jdbcTemplate.query(sql, resultSetExtractor, new Object[] {}); + return s3CredentialsData; + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/api/ApiConstants.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/api/ApiConstants.java deleted file mode 100644 index 266e57211..000000000 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/api/ApiConstants.java +++ /dev/null @@ -1,15 +0,0 @@ -/** - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. - */ -package org.mifosplatform.infrastructure.core.api; - -public class ApiConstants { - - // TODO:Vishwas Need to move these settings to the Database - public static final Integer MAX_FILE_UPLOAD_SIZE_IN_MB = 5; - - // TODO:Vishwas Need to move these settings to the Database - public static final Integer MAX_IMAGE_UPLOAD_SIZE_IN_MB = 1; -} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/domain/DefaultPlatformPasswordEncoder.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/domain/DefaultPlatformPasswordEncoder.java index 846d90a82..e2ca920a0 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/domain/DefaultPlatformPasswordEncoder.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/domain/DefaultPlatformPasswordEncoder.java @@ -13,12 +13,13 @@ import org.springframework.security.authentication.dao.SaltSource; import org.springframework.security.authentication.encoding.PasswordEncoder; import org.springframework.stereotype.Service; +@SuppressWarnings("deprecation") @Service(value = "applicationPasswordEncoder") @Scope("singleton") public class DefaultPlatformPasswordEncoder implements PlatformPasswordEncoder { private final PasswordEncoder passwordEncoder; - private final SaltSource saltSource; + private final SaltSource saltSource; @Autowired public DefaultPlatformPasswordEncoder(final PasswordEncoder passwordEncoder, final SaltSource saltSource) { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/DocumentStore.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/DocumentStore.java deleted file mode 100644 index 0722fd042..000000000 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/DocumentStore.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.mifosplatform.infrastructure.core.service; - -import org.mifosplatform.infrastructure.core.api.ApiConstants; -import org.mifosplatform.infrastructure.core.domain.Base64EncodedImage; -import org.mifosplatform.infrastructure.documentmanagement.command.DocumentCommand; -import org.mifosplatform.infrastructure.documentmanagement.data.DocumentData; -import org.mifosplatform.infrastructure.documentmanagement.data.FileData; -import org.mifosplatform.infrastructure.documentmanagement.exception.DocumentManagementException; -import org.mifosplatform.infrastructure.documentmanagement.exception.DocumentNotFoundException; -import org.mifosplatform.portfolio.client.data.ImageData; - -import java.io.InputStream; - -public abstract class DocumentStore { - protected DocumentStoreType type; - - protected Integer maxFileSize = ApiConstants.MAX_FILE_UPLOAD_SIZE_IN_MB; - protected Integer maxImageSize = ApiConstants.MAX_IMAGE_UPLOAD_SIZE_IN_MB; - - public abstract String saveDocument(InputStream uploadedInputStream, DocumentCommand documentCommand) throws DocumentManagementException; - public abstract void deleteDocument(String documentName, String documentPath) throws DocumentManagementException; - public abstract FileData retrieveDocument(DocumentData documentData) throws DocumentNotFoundException; - - public abstract String saveImage(InputStream uploadedInputStream, Long resourceId, String imageName, Long fileSize) throws DocumentManagementException; - public abstract String saveImage(Base64EncodedImage base64EncodedImage, Long resourceId, String imageName) throws DocumentManagementException; - public abstract void deleteImage(final Long resourceId, final String location); - public abstract ImageData retrieveImage(ImageData imageData); - - public abstract DocumentStoreType getType(); - - protected void validateFileSizeWithinPermissibleRange(Long fileSize, String name, int maxFileSize) { - /** - * Using Content-Length gives me size of the entire request, which is - * good enough for now for a fast fail as the length of the rest of the - * content i.e name and description while compared to the uploaded file - * size is negligible - **/ - if (fileSize != null && ((fileSize / (1024 * 1024)) > maxFileSize)) { - throw new DocumentManagementException(name, fileSize, - maxFileSize); - } - } - - public static DocumentStoreType getDocumentStoreType(String documentStoreType){ - if (documentStoreType.equals(DocumentStoreType.FILE_SYSTEM.getValue())) { - return DocumentStoreType.FILE_SYSTEM; - } else { - return DocumentStoreType.S3; - } - } - -} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/DocumentStoreFactory.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/DocumentStoreFactory.java deleted file mode 100644 index 32b4b05bd..000000000 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/DocumentStoreFactory.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.mifosplatform.infrastructure.core.service; - -import com.amazonaws.services.s3.AmazonS3Client; -import org.mifosplatform.infrastructure.configuration.domain.ConfigurationDomainService; -import org.mifosplatform.infrastructure.configuration.domain.ExternalServicesDomainService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationContext; -import org.springframework.stereotype.Component; - -@Component -public class DocumentStoreFactory { - - private ApplicationContext applicationContext; - - @Autowired - public DocumentStoreFactory(ApplicationContext applicationContext) { - this.applicationContext = applicationContext; - } - - public DocumentStore getInstanceFromConfiguration(){ - ConfigurationDomainService configurationDomainServiceJpa = applicationContext.getBean("configurationDomainServiceJpa", ConfigurationDomainService.class); - if(configurationDomainServiceJpa.isAmazonS3Enabled()){ - return createS3DocumentStore(); - } - else { - return new FileSystemDocumentStore(); - } - } - - public DocumentStore getInstanceFromStorageType(DocumentStoreType documentStoreType){ - if(documentStoreType == DocumentStoreType.FILE_SYSTEM){ - return new FileSystemDocumentStore(); - } - else { - return createS3DocumentStore(); - } - } - private DocumentStore createS3DocumentStore() { - AmazonS3Client s3Client = applicationContext.getBean("s3ClientFactory", S3ClientFactory.class).getS3Client(); - ExternalServicesDomainService externalServicesDomainService = applicationContext.getBean("externalServicesDomainService", ExternalServicesDomainService.class); - return new S3DocumentStore(externalServicesDomainService.getValue("s3_bucket_name"), s3Client); - } - - -} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/DocumentStoreType.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/DocumentStoreType.java deleted file mode 100644 index ac6a34c01..000000000 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/DocumentStoreType.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.mifosplatform.infrastructure.core.service; - -public enum DocumentStoreType { - FILE_SYSTEM("file_system"), S3("s3"); - - private String value; - - DocumentStoreType(String value){ - this.value = value; - } - - public String getValue() { - return value; - } - } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/RandomStringGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/RandomStringGenerator.java deleted file mode 100644 index 31f1ecfd0..000000000 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/RandomStringGenerator.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.mifosplatform.infrastructure.core.service; - -import java.util.Random; - -public class RandomStringGenerator { - public static Random random = new Random(); - - /** - * Generate a random String - * - * @return - */ - public static String generateRandomString() { - String characters = "abcdefghijklmnopqrstuvwxyz123456789"; - int length = generateRandomNumber(); - char[] text = new char[length]; - for (int i = 0; i < length; i++) { - text[i] = characters.charAt(random.nextInt(characters.length())); - } - return new String(text); - } - - /** - * Generate a random number between 5 to 16 - * - * @return - */ - public static int generateRandomNumber() { - Random randomGenerator = new Random(); - return randomGenerator.nextInt(11) + 5; - } - -} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/S3ClientFactory.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/S3ClientFactory.java deleted file mode 100644 index 5274274f2..000000000 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/S3ClientFactory.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.mifosplatform.infrastructure.core.service; - -import com.amazonaws.auth.BasicAWSCredentials; -import com.amazonaws.services.s3.AmazonS3Client; -import org.mifosplatform.infrastructure.configuration.domain.ExternalServicesDomainService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -@Component -public class S3ClientFactory { - - private ExternalServicesDomainService ExternalServicesDomainService; - - @Autowired - public S3ClientFactory(final ExternalServicesDomainService ExternalServicesDomainService) { - this.ExternalServicesDomainService = ExternalServicesDomainService; - } - - public AmazonS3Client getS3Client() { - String s3_access_key = ExternalServicesDomainService.getValue("s3_access_key"); - String s3_secret_key = ExternalServicesDomainService.getValue("s3_secret_key"); - return new AmazonS3Client(new BasicAWSCredentials(s3_access_key,s3_secret_key)); - } -} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/S3DocumentStore.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/S3DocumentStore.java deleted file mode 100644 index 9781ef9c9..000000000 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/S3DocumentStore.java +++ /dev/null @@ -1,201 +0,0 @@ -package org.mifosplatform.infrastructure.core.service; - - -import com.amazonaws.AmazonClientException; -import com.amazonaws.AmazonServiceException; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.model.*; -import com.lowagie.text.pdf.codec.Base64; -import org.mifosplatform.infrastructure.core.domain.Base64EncodedImage; -import org.mifosplatform.infrastructure.documentmanagement.command.DocumentCommand; -import org.mifosplatform.infrastructure.documentmanagement.data.DocumentData; -import org.mifosplatform.infrastructure.documentmanagement.data.FileData; -import org.mifosplatform.infrastructure.documentmanagement.exception.DocumentManagementException; -import org.mifosplatform.infrastructure.documentmanagement.exception.DocumentNotFoundException; -import org.mifosplatform.portfolio.client.data.ImageData; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.InputStream; - -public class S3DocumentStore extends DocumentStore { - - private final static Logger logger = LoggerFactory.getLogger(S3DocumentStore.class); - - private final String s3BucketName; - private AmazonS3 s3Client; - - public S3DocumentStore(String s3BucketName, AmazonS3 s3Client) { - this.s3BucketName = s3BucketName; - this.s3Client = s3Client; - } - - @Override - public String saveDocument(InputStream toUpload, DocumentCommand documentCommand) throws DocumentManagementException { - String documentName = documentCommand.getFileName(); - validateFileSizeWithinPermissibleRange(documentCommand.getSize(), documentName, maxFileSize); - - String uploadDocFolder = generateFileParentDirectory(documentCommand.getParentEntityType(), documentCommand.getParentEntityId()); - String uploadDocFullPath = uploadDocFolder + File.separator + documentName; - - uploadDocument(toUpload, uploadDocFullPath); - return uploadDocFullPath; - } - - @Override - public void deleteDocument(String documentName, String documentPath) throws DocumentManagementException { - try{ - deleteObjectFromS3(documentPath); - }catch (AmazonServiceException ase){ - deleteObjectAmazonServiceExceptionMessage(ase); - throw new DocumentManagementException(documentName); - }catch (AmazonClientException ace){ - deleteObjectAmazonClientExceptionMessage(ace); - throw new DocumentManagementException(documentName); - } - } - - @Override - public String saveImage(InputStream toUploadInputStream, Long resourceId, String imageName, Long fileSize) throws DocumentManagementException { - validateFileSizeWithinPermissibleRange(fileSize, imageName, maxImageSize); - - String uploadImageLocation = generateClientImageParentDirectory(resourceId); - String fileLocation = uploadImageLocation + File.separator + imageName; - - uploadDocument(toUploadInputStream, fileLocation); - return fileLocation; - } - - @Override - public String saveImage(Base64EncodedImage base64EncodedImage, Long resourceId, String imageName) throws DocumentManagementException { - String uploadImageLocation = generateClientImageParentDirectory(resourceId); - String fileLocation = uploadImageLocation + File.separator + imageName + base64EncodedImage.getFileExtension(); - InputStream toUploadInputStream = new ByteArrayInputStream(Base64.decode(base64EncodedImage.getBase64EncodedString())); - - uploadDocument(toUploadInputStream, fileLocation); - return fileLocation; - } - - @Override - public void deleteImage(Long resourceId, String location) { - try { - deleteObjectFromS3(location); - } catch (AmazonServiceException ase) { - deleteObjectAmazonServiceExceptionMessage(ase); - logger.warn("Unable to delete image associated with clients with Id " + resourceId); - } catch (AmazonClientException ace) { - deleteObjectAmazonClientExceptionMessage(ace); - logger.warn("Unable to delete image associated with clients with Id " + resourceId); - } - } - - private void deleteObjectAmazonClientExceptionMessage(AmazonClientException ace) { - String message = "Caught an AmazonClientException." + - "Error Message: " + ace.getMessage(); - logger.error(message); - } - - private void deleteObjectAmazonServiceExceptionMessage(AmazonServiceException ase) { - String message = "Caught an AmazonServiceException." + - "Error Message: " + ase.getMessage() + - "HTTP Status Code: " + ase.getStatusCode() + - "AWS Error Code: " + ase.getErrorCode() + - "Error Type: " + ase.getErrorType() + - "Request ID: " + ase.getRequestId(); - logger.error(message); - } - - private void deleteObjectFromS3(String location) { - s3Client.deleteObject(new DeleteObjectRequest(s3BucketName, location)); - } - - @Override - public DocumentStoreType getType() { - return DocumentStoreType.S3; - } - - @Override - public FileData retrieveDocument(DocumentData documentData) throws DocumentNotFoundException { - FileData fileData = null; - String fileName = documentData.fileName(); - try { - logger.info("Downloading an object"); - S3Object s3object = s3Client.getObject(new GetObjectRequest(s3BucketName, documentData.fileLocation())); - fileData = new FileData(s3object.getObjectContent(), fileName, documentData.contentType()); - } catch (AmazonServiceException ase) { - getObjectAmazonServiceExceptionMessage(ase,fileName); - throw new DocumentNotFoundException(documentData.getParentEntityType(), documentData.getParentEntityId(), documentData.getId()); - } catch (AmazonClientException ace) { - getObjectAmazonClientExceptionMessage(ace,fileName); - throw new DocumentNotFoundException(documentData.getParentEntityType(), documentData.getParentEntityId(), documentData.getId()); - } - return fileData; - } - - @Override - public ImageData retrieveImage(ImageData imageData) { - S3Object s3object = s3Client.getObject(new GetObjectRequest(s3BucketName, imageData.imageKey())); - imageData.setContent(s3object.getObjectContent()); - return imageData; - } - - String generateFileParentDirectory(String entityType, Long entityId) { - return "documents" + File.separator + entityType + File.separator + entityId + File.separator + RandomStringGenerator.generateRandomString(); - } - - String generateClientImageParentDirectory(Long resourceId) { - return "images" + File.separator + "clients" + File.separator + resourceId; - } - - private void uploadDocument(InputStream inputStream, String s3UploadLocation) throws DocumentManagementException { - try { - logger.info("Uploading a new object to S3 from a file to " + s3UploadLocation); - s3Client.putObject(new PutObjectRequest(this.s3BucketName, s3UploadLocation, inputStream, new ObjectMetadata())); - - } catch (AmazonServiceException ase) { - String message = "Caught an AmazonServiceException, which " + "means your request made it " + "to Amazon S3, but was rejected with an error response" + - " for some reason. \n Error Message: " + ase.getMessage() + - "HTTP Status Code: " + ase.getStatusCode() + - "AWS Error Code: " + ase.getErrorCode() + - "Error Type: " + ase.getErrorType() + - "Request ID: " + ase.getRequestId(); - - logger.error(message); - throw new DocumentManagementException(message); - } catch (AmazonClientException ace) { - String message = "Caught an AmazonClientException, which " + - "means the client encountered " + - "an internal error while trying to " + - "communicate with S3, " + - "such as not being able to access the network." + - "Error Message: " + ace.getMessage(); - logger.error(message); - throw new DocumentManagementException(message); - } - } - - private void getObjectAmazonClientExceptionMessage(AmazonClientException ace, String filename) { - String message = "Caught an AmazonClientException, which means the client encountered " + - "an internal error while trying to communicate with S3, " + - "such as not being able to access the network." + - "Error Message: " + ace.getMessage() + - "FileName: " + filename; - - logger.error(message); - } - - private void getObjectAmazonServiceExceptionMessage(AmazonServiceException ase, String filename) { - String message = "Caught an AmazonServiceException, which means your request made it " + - "to Amazon S3, but was rejected with an error response" + - " for some reason." + - "Error Message: " + ase.getMessage() + - "HTTP Status Code: " + ase.getStatusCode() + - "AWS Error Code: " + ase.getErrorCode() + - "Error Type: " + ase.getErrorType() + - "Request ID: " + ase.getRequestId() + - "FileName: " + filename; - logger.error(message); - } -} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadReportingServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadReportingServiceImpl.java index 3e573af66..cace21ef3 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadReportingServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadReportingServiceImpl.java @@ -29,7 +29,6 @@ import javax.ws.rs.core.StreamingOutput; import org.apache.commons.lang.StringUtils; import org.mifosplatform.infrastructure.core.domain.JdbcSupport; import org.mifosplatform.infrastructure.core.exception.PlatformDataIntegrityException; -import org.mifosplatform.infrastructure.core.service.FileSystemDocumentStore; import org.mifosplatform.infrastructure.core.service.TenantAwareRoutingDataSource; import org.mifosplatform.infrastructure.dataqueries.data.GenericResultsetData; import org.mifosplatform.infrastructure.dataqueries.data.ReportData; @@ -38,6 +37,7 @@ import org.mifosplatform.infrastructure.dataqueries.data.ReportParameterJoinData import org.mifosplatform.infrastructure.dataqueries.data.ResultsetColumnHeaderData; import org.mifosplatform.infrastructure.dataqueries.data.ResultsetRowData; import org.mifosplatform.infrastructure.dataqueries.exception.ReportNotFoundException; +import org.mifosplatform.infrastructure.documentmanagement.contentrepository.FileSystemContentRepository; import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; import org.mifosplatform.useradministration.domain.AppUser; import org.pentaho.reporting.engine.classic.core.ClassicEngineBoot; @@ -243,7 +243,7 @@ public class ReadReportingServiceImpl implements ReadReportingService { if (noPentaho) { throw new PlatformDataIntegrityException("error.msg.no.pentaho", "Pentaho is not enabled", "Pentaho is not enabled"); } - final String reportPath = FileSystemDocumentStore.MIFOSX_BASE_DIR + File.separator + "pentahoReports" + File.separator + reportName + ".prpt"; + final String reportPath = FileSystemContentRepository.MIFOSX_BASE_DIR + File.separator + "pentahoReports" + File.separator + reportName + ".prpt"; logger.info("Report path: " + reportPath); // load report definition @@ -357,7 +357,7 @@ public class ReadReportingServiceImpl implements ReadReportingService { @Override public String retrieveReportPDF(final String reportName, final String type, final Map queryParams) { - String fileLocation = FileSystemDocumentStore.MIFOSX_BASE_DIR + File.separator + ""; + String fileLocation = FileSystemContentRepository.MIFOSX_BASE_DIR + File.separator + ""; if (!new File(fileLocation).isDirectory()) { new File(fileLocation).mkdirs(); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/api/DocumentManagementApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/api/DocumentManagementApiResource.java index ff48aa1e8..dbf103a2b 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/api/DocumentManagementApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/api/DocumentManagementApiResource.java @@ -52,8 +52,8 @@ public class DocumentManagementApiResource { @Autowired public DocumentManagementApiResource(final PlatformSecurityContext context, - final DocumentReadPlatformService documentReadPlatformService, final DocumentWritePlatformService documentWritePlatformService, - final ApiRequestParameterHelper apiRequestParameterHelper, final ToApiJsonSerializer toApiJsonSerializer) { + final DocumentReadPlatformService documentReadPlatformService, final DocumentWritePlatformService documentWritePlatformService, + final ApiRequestParameterHelper apiRequestParameterHelper, final ToApiJsonSerializer toApiJsonSerializer) { this.context = context; this.documentReadPlatformService = documentReadPlatformService; this.documentWritePlatformService = documentWritePlatformService; @@ -150,9 +150,6 @@ public class DocumentManagementApiResource { final DocumentData documentData = this.documentReadPlatformService.retrieveDocument(entityType, entityId, documentId); - // we do not want to send document location as a part of the response - documentData.setLocation(null); - final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); return this.toApiJsonSerializer.serialize(settings, documentData, this.RESPONSE_DATA_PARAMETERS); } @@ -166,7 +163,7 @@ public class DocumentManagementApiResource { this.context.authenticatedUser().validateHasReadPermission(this.SystemEntityType); - final FileData fileData = this.documentReadPlatformService.retrieveDocumentAsFile(entityType, entityId, documentId); + final FileData fileData = this.documentReadPlatformService.retrieveFileData(entityType, entityId, documentId); final ResponseBuilder response = Response.ok(fileData.file()); response.header("Content-Disposition", "attachment; filename=\"" + fileData.name() + "\""); response.header("Content-Type", fileData.contentType()); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/api/ClientImagesApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/api/ImagesApiResource.java similarity index 62% rename from mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/api/ClientImagesApiResource.java rename to mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/api/ImagesApiResource.java index 087c08196..dbd576106 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/api/ClientImagesApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/api/ImagesApiResource.java @@ -3,51 +3,63 @@ * 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.client.api; +package org.mifosplatform.infrastructure.documentmanagement.api; + +import java.io.InputStream; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.ResponseBuilder; + +import org.apache.commons.lang.StringUtils; +import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; +import org.mifosplatform.infrastructure.core.domain.Base64EncodedImage; +import org.mifosplatform.infrastructure.core.serialization.DefaultToApiJsonSerializer; +import org.mifosplatform.infrastructure.documentmanagement.contentrepository.ContentRepositoryUtils; +import org.mifosplatform.infrastructure.documentmanagement.contentrepository.ContentRepositoryUtils.IMAGE_FILE_EXTENSION; +import org.mifosplatform.infrastructure.documentmanagement.data.ImageData; +import org.mifosplatform.infrastructure.documentmanagement.service.ImageReadPlatformService; +import org.mifosplatform.infrastructure.documentmanagement.service.ImageWritePlatformService; +import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; +import org.mifosplatform.portfolio.client.data.ClientData; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; import com.lowagie.text.pdf.codec.Base64; import com.sun.jersey.core.header.FormDataContentDisposition; import com.sun.jersey.multipart.FormDataBodyPart; import com.sun.jersey.multipart.FormDataParam; -import org.apache.commons.lang.StringUtils; -import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; -import org.mifosplatform.infrastructure.core.domain.Base64EncodedImage; -import org.mifosplatform.infrastructure.core.serialization.DefaultToApiJsonSerializer; -import org.mifosplatform.infrastructure.core.service.ImageUtils; -import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; -import org.mifosplatform.portfolio.client.data.ClientData; -import org.mifosplatform.portfolio.client.exception.ImageNotFoundException; -import org.mifosplatform.portfolio.client.service.ClientReadPlatformService; -import org.mifosplatform.portfolio.client.service.ClientWritePlatformService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Scope; -import org.springframework.stereotype.Component; -import javax.ws.rs.*; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.ResponseBuilder; -import java.io.InputStream; - -@Path("/clients/{clientId}/images") +/** + * TODO Vishwas need to change the url to entity/entityId/images to make the + * image management API's generic for other entities like staff etc + **/ +@Path("{clients}/{clientId}/images") @Component @Scope("singleton") -public class ClientImagesApiResource { +public class ImagesApiResource { private final PlatformSecurityContext context; - private final ClientReadPlatformService clientReadPlatformService; - private final ClientWritePlatformService clientWritePlatformService; + private final ImageReadPlatformService imageReadPlatformService; + private final ImageWritePlatformService imageWritePlatformService; private final DefaultToApiJsonSerializer toApiJsonSerializer; - private final static Logger logger = LoggerFactory.getLogger(ClientImagesApiResource.class); @Autowired - public ClientImagesApiResource(final PlatformSecurityContext context, final ClientReadPlatformService readPlatformService, - final ClientWritePlatformService clientWritePlatformService, final DefaultToApiJsonSerializer toApiJsonSerializer) { + public ImagesApiResource(final PlatformSecurityContext context, final ImageReadPlatformService readPlatformService, + final ImageWritePlatformService imageWritePlatformService, final DefaultToApiJsonSerializer toApiJsonSerializer) { this.context = context; - this.clientReadPlatformService = readPlatformService; - this.clientWritePlatformService = clientWritePlatformService; + this.imageReadPlatformService = readPlatformService; + this.imageWritePlatformService = imageWritePlatformService; this.toApiJsonSerializer = toApiJsonSerializer; } @@ -64,10 +76,10 @@ public class ClientImagesApiResource { // TODO: vishwas might need more advances validation (like reading magic // number) for handling malicious clients // and clients not setting mime type - ImageUtils.validateClientImageNotEmpty(fileDetails.getFileName()); - ImageUtils.validateImageMimeType(bodyPart.getMediaType().toString()); + ContentRepositoryUtils.validateClientImageNotEmpty(fileDetails.getFileName()); + ContentRepositoryUtils.validateImageMimeType(bodyPart.getMediaType().toString()); - final CommandProcessingResult result = this.clientWritePlatformService.saveOrUpdateClientImage(clientId, fileDetails.getFileName(), + final CommandProcessingResult result = this.imageWritePlatformService.saveOrUpdateClientImage(clientId, fileDetails.getFileName(), inputStream, fileSize); return this.toApiJsonSerializer.serialize(result); @@ -81,9 +93,9 @@ public class ClientImagesApiResource { @Produces({ MediaType.APPLICATION_JSON }) public String addNewClientImage(@PathParam("clientId") final Long clientId, final String jsonRequestBody) { - final Base64EncodedImage base64EncodedImage = ImageUtils.extractImageFromDataURL(jsonRequestBody); + final Base64EncodedImage base64EncodedImage = ContentRepositoryUtils.extractImageFromDataURL(jsonRequestBody); - final CommandProcessingResult result = this.clientWritePlatformService.saveOrUpdateClientImage(clientId, base64EncodedImage); + final CommandProcessingResult result = this.imageWritePlatformService.saveOrUpdateClientImage(clientId, base64EncodedImage); return this.toApiJsonSerializer.serialize(result); } @@ -98,19 +110,17 @@ public class ClientImagesApiResource { context.authenticatedUser().validateHasReadPermission("CLIENTIMAGE"); - final ClientData clientData = this.clientReadPlatformService.retrieveOne(clientId); - - if (clientData.imageKeyDoesNotExist()) { throw new ImageNotFoundException("clients", clientId); } + final ImageData imageData = this.imageReadPlatformService.retrieveClientImage(clientId); // TODO: Need a better way of determining image type - String imageDataURISuffix = ImageUtils.IMAGE_DATA_URI_SUFFIX.JPEG.getValue(); - if (StringUtils.endsWith(clientData.imageKey(), ImageUtils.IMAGE_FILE_EXTENSION.GIF.getValue())) { - imageDataURISuffix = ImageUtils.IMAGE_DATA_URI_SUFFIX.GIF.getValue(); - } else if (StringUtils.endsWith(clientData.imageKey(), ImageUtils.IMAGE_FILE_EXTENSION.PNG.getValue())) { - imageDataURISuffix = ImageUtils.IMAGE_DATA_URI_SUFFIX.PNG.getValue(); + String imageDataURISuffix = ContentRepositoryUtils.IMAGE_DATA_URI_SUFFIX.JPEG.getValue(); + if (StringUtils.endsWith(imageData.location(), ContentRepositoryUtils.IMAGE_FILE_EXTENSION.GIF.getValue())) { + imageDataURISuffix = ContentRepositoryUtils.IMAGE_DATA_URI_SUFFIX.GIF.getValue(); + } else if (StringUtils.endsWith(imageData.location(), ContentRepositoryUtils.IMAGE_FILE_EXTENSION.PNG.getValue())) { + imageDataURISuffix = ContentRepositoryUtils.IMAGE_DATA_URI_SUFFIX.PNG.getValue(); } - String clientImageAsBase64Text = imageDataURISuffix + Base64.encodeBytes(clientData.image()); + String clientImageAsBase64Text = imageDataURISuffix + Base64.encodeBytes(imageData.getContent()); return clientImageAsBase64Text; } @@ -120,16 +130,15 @@ public class ClientImagesApiResource { public Response downloadClientImage(@PathParam("clientId") final Long clientId) { context.authenticatedUser().validateHasReadPermission("CLIENTIMAGE"); - final ClientData clientData = this.clientReadPlatformService.retrieveOne(clientId); + final ImageData imageData = this.imageReadPlatformService.retrieveClientImage(clientId); - if (clientData.imageKeyDoesNotExist()) { throw new ImageNotFoundException("clients", clientId); } - - ResponseBuilder response = Response.ok(clientData.image()); - response.header("Content-Disposition", "attachment; filename=\"" + clientData.imageName() + "\""); + ResponseBuilder response = Response.ok(imageData.getContent()); + response.header("Content-Disposition", "attachment; filename=\"" + imageData.getEntityDisplayName() + IMAGE_FILE_EXTENSION.JPEG + + "\""); // TODO: Need a better way of determining image type - response.header("Content-Type", clientData.imageContentType()); + response.header("Content-Type", imageData.contentType()); return response.build(); } @@ -163,7 +172,7 @@ public class ClientImagesApiResource { @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) public String deleteClientImage(@PathParam("clientId") final Long clientId) { - this.clientWritePlatformService.deleteClientImage(clientId); + this.imageWritePlatformService.deleteClientImage(clientId); return this.toApiJsonSerializer.serialize(new CommandProcessingResult(clientId)); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/command/DocumentCommand.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/command/DocumentCommand.java index 2c6e636c0..1706f7f8e 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/command/DocumentCommand.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/command/DocumentCommand.java @@ -5,8 +5,6 @@ */ package org.mifosplatform.infrastructure.documentmanagement.command; -import org.mifosplatform.infrastructure.core.service.DocumentStoreType; - import java.util.Set; /** @@ -24,9 +22,9 @@ public class DocumentCommand { private Long size; private String type; private String location; + private Integer storageType; private final Set modifiedParameters; - private DocumentStoreType storageType; public DocumentCommand(final Set modifiedParameters, final Long id, final String parentEntityType, final Long parentEntityId, final String name, final String fileName, final Long size, final String type, final String description, final String location) { @@ -98,6 +96,14 @@ public class DocumentCommand { this.location = location; } + public Integer getStorageType() { + return this.storageType; + } + + public void setStorageType(Integer storageType) { + this.storageType = storageType; + } + public boolean isNameChanged() { return this.modifiedParameters.contains("name"); } @@ -122,11 +128,4 @@ public class DocumentCommand { return this.modifiedParameters.contains("location"); } - public void setStorageType(DocumentStoreType storageType) { - this.storageType = storageType; - } - - public String getStorageType(){ - return storageType.getValue(); - } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/contentrepository/ContentRepository.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/contentrepository/ContentRepository.java new file mode 100644 index 000000000..96c10cc65 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/contentrepository/ContentRepository.java @@ -0,0 +1,38 @@ +package org.mifosplatform.infrastructure.documentmanagement.contentrepository; + +import java.io.InputStream; + +import org.mifosplatform.infrastructure.core.domain.Base64EncodedImage; +import org.mifosplatform.infrastructure.documentmanagement.command.DocumentCommand; +import org.mifosplatform.infrastructure.documentmanagement.data.DocumentData; +import org.mifosplatform.infrastructure.documentmanagement.data.FileData; +import org.mifosplatform.infrastructure.documentmanagement.data.ImageData; +import org.mifosplatform.infrastructure.documentmanagement.domain.StorageType; + +public interface ContentRepository { + + public StorageType type = null; + + // TODO:Vishwas Need to move these settings to the Database + public static final Integer MAX_FILE_UPLOAD_SIZE_IN_MB = 5; + + // TODO:Vishwas Need to move these settings to the Database + public static final Integer MAX_IMAGE_UPLOAD_SIZE_IN_MB = 1; + + public abstract String saveFile(InputStream uploadedInputStream, DocumentCommand documentCommand); + + public abstract void deleteFile(String fileName, String documentPath); + + public abstract FileData fetchFile(DocumentData documentData); + + public abstract String saveImage(InputStream uploadedInputStream, Long resourceId, String imageName, Long fileSize); + + public abstract String saveImage(Base64EncodedImage base64EncodedImage, Long resourceId, String imageName); + + public abstract void deleteImage(final Long resourceId, final String location); + + public abstract ImageData fetchImage(ImageData imageData); + + public abstract StorageType getStorageType(); + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/contentrepository/ContentRepositoryFactory.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/contentrepository/ContentRepositoryFactory.java new file mode 100644 index 000000000..837ad7dfc --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/contentrepository/ContentRepositoryFactory.java @@ -0,0 +1,41 @@ +package org.mifosplatform.infrastructure.documentmanagement.contentrepository; + +import org.mifosplatform.infrastructure.configuration.data.S3CredentialsData; +import org.mifosplatform.infrastructure.configuration.domain.ConfigurationDomainService; +import org.mifosplatform.infrastructure.configuration.service.ExternalServicesReadPlatformService; +import org.mifosplatform.infrastructure.documentmanagement.domain.StorageType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +@Component +public class ContentRepositoryFactory { + + private ApplicationContext applicationContext; + private ExternalServicesReadPlatformService externalServicesReadPlatformService; + + @Autowired + public ContentRepositoryFactory(final ApplicationContext applicationContext, + final ExternalServicesReadPlatformService externalServicesReadPlatformService) { + this.applicationContext = applicationContext; + this.externalServicesReadPlatformService = externalServicesReadPlatformService; + } + + public ContentRepository getRepository() { + ConfigurationDomainService configurationDomainServiceJpa = applicationContext.getBean("configurationDomainServiceJpa", + ConfigurationDomainService.class); + if (configurationDomainServiceJpa.isAmazonS3Enabled()) { return createS3DocumentStore(); } + return new FileSystemContentRepository(); + } + + public ContentRepository getRepository(StorageType documentStoreType) { + if (documentStoreType == StorageType.FILE_SYSTEM) { return new FileSystemContentRepository(); } + return createS3DocumentStore(); + } + + private ContentRepository createS3DocumentStore() { + S3CredentialsData s3CredentialsData = externalServicesReadPlatformService.getS3Credentials(); + return new S3ContentRepository(s3CredentialsData.getBucketName(), s3CredentialsData.getSecretKey(), + s3CredentialsData.getAccessKey()); + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/ImageUtils.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/contentrepository/ContentRepositoryUtils.java similarity index 70% rename from mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/ImageUtils.java rename to mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/contentrepository/ContentRepositoryUtils.java index 5c55495f9..730bf9986 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/ImageUtils.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/contentrepository/ContentRepositoryUtils.java @@ -1,4 +1,8 @@ -package org.mifosplatform.infrastructure.core.service; +package org.mifosplatform.infrastructure.documentmanagement.contentrepository; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; import org.apache.commons.lang.StringUtils; import org.mifosplatform.infrastructure.core.data.ApiParameterError; @@ -6,11 +10,11 @@ import org.mifosplatform.infrastructure.core.domain.Base64EncodedImage; import org.mifosplatform.infrastructure.core.exception.ImageDataURLNotValidException; import org.mifosplatform.infrastructure.core.exception.ImageUploadException; import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException; +import org.mifosplatform.infrastructure.documentmanagement.exception.ContentManagementException; -import java.util.ArrayList; -import java.util.List; +public class ContentRepositoryUtils { -public class ImageUtils { + public static Random random = new Random(); public static enum IMAGE_MIME_TYPE { GIF("image/gif"), JPEG("image/jpeg"), PNG("image/png"); @@ -54,21 +58,20 @@ public class ImageUtils { return this.value; } } + /** * Validates that passed in Mime type maps to known image mime types - * + * * @param mimeType */ public static void validateImageMimeType(String mimeType) { if (!(mimeType.equalsIgnoreCase(IMAGE_MIME_TYPE.GIF.getValue()) || mimeType.equalsIgnoreCase(IMAGE_MIME_TYPE.JPEG.getValue()) || mimeType - .equalsIgnoreCase(IMAGE_MIME_TYPE.PNG.getValue()))) { - throw new ImageUploadException(); - } + .equalsIgnoreCase(IMAGE_MIME_TYPE.PNG.getValue()))) { throw new ImageUploadException(); } } /** * Extracts Image from a Data URL - * + * * @param mimeType */ public static Base64EncodedImage extractImageFromDataURL(String dataURL) { @@ -90,6 +93,17 @@ public class ImageUtils { return new Base64EncodedImage(base64EncodedString, fileExtension); } + public static void validateFileSizeWithinPermissibleRange(Long fileSize, String name) { + /** + * Using Content-Length gives me size of the entire request, which is + * good enough for now for a fast fail as the length of the rest of the + * content i.e name and description while compared to the uploaded file + * size is negligible + **/ + if (fileSize != null && ((fileSize / (1024 * 1024)) > ContentRepository.MAX_FILE_UPLOAD_SIZE_IN_MB)) { throw new ContentManagementException( + name, fileSize, ContentRepository.MAX_FILE_UPLOAD_SIZE_IN_MB); } + } + public static void validateClientImageNotEmpty(String imageFileName) { List dataValidationErrors = new ArrayList(); if (imageFileName == null) { @@ -102,4 +116,29 @@ public class ImageUtils { dataValidationErrors); } } + + /** + * Generate a random String + * + * @return + */ + public static String generateRandomString() { + String characters = "abcdefghijklmnopqrstuvwxyz123456789"; + int length = generateRandomNumber(); + char[] text = new char[length]; + for (int i = 0; i < length; i++) { + text[i] = characters.charAt(random.nextInt(characters.length())); + } + return new String(text); + } + + /** + * Generate a random number between 5 to 16 + * + * @return + */ + public static int generateRandomNumber() { + Random randomGenerator = new Random(); + return randomGenerator.nextInt(11) + 5; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/FileSystemDocumentStore.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/contentrepository/FileSystemContentRepository.java similarity index 59% rename from mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/FileSystemDocumentStore.java rename to mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/contentrepository/FileSystemContentRepository.java index 6e1a51c9f..0f065d925 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/FileSystemDocumentStore.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/contentrepository/FileSystemContentRepository.java @@ -3,57 +3,63 @@ * 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.infrastructure.core.service; +package org.mifosplatform.infrastructure.documentmanagement.contentrepository; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; -import com.lowagie.text.pdf.codec.Base64; import org.mifosplatform.infrastructure.core.domain.Base64EncodedImage; +import org.mifosplatform.infrastructure.core.service.ThreadLocalContextUtil; import org.mifosplatform.infrastructure.documentmanagement.command.DocumentCommand; import org.mifosplatform.infrastructure.documentmanagement.data.DocumentData; import org.mifosplatform.infrastructure.documentmanagement.data.FileData; -import org.mifosplatform.infrastructure.documentmanagement.exception.DocumentManagementException; -import org.mifosplatform.portfolio.client.data.ImageData; +import org.mifosplatform.infrastructure.documentmanagement.data.ImageData; +import org.mifosplatform.infrastructure.documentmanagement.domain.StorageType; +import org.mifosplatform.infrastructure.documentmanagement.exception.ContentManagementException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.*; +import com.lowagie.text.pdf.codec.Base64; -public class FileSystemDocumentStore extends DocumentStore { +public class FileSystemContentRepository implements ContentRepository { - private final static Logger logger = LoggerFactory.getLogger(FileSystemDocumentStore.class); + private final static Logger logger = LoggerFactory.getLogger(FileSystemContentRepository.class); public static final String MIFOSX_BASE_DIR = System.getProperty("user.home") + File.separator + ".mifosx"; - @Override - public String saveDocument(InputStream uploadedInputStream, DocumentCommand documentCommand) throws DocumentManagementException { - String documentName = documentCommand.getFileName(); - String uploadDocumentLocation = generateFileParentDirectory(documentCommand.getParentEntityType(), documentCommand.getParentEntityId()); + public String saveFile(InputStream uploadedInputStream, DocumentCommand documentCommand) { + String fileName = documentCommand.getFileName(); + String uploadDocumentLocation = generateFileParentDirectory(documentCommand.getParentEntityType(), + documentCommand.getParentEntityId()); - validateFileSizeWithinPermissibleRange(documentCommand.getSize(), documentName, maxFileSize); + ContentRepositoryUtils.validateFileSizeWithinPermissibleRange(documentCommand.getSize(), fileName); makeDirectories(uploadDocumentLocation); - String fileLocation = uploadDocumentLocation + File.separator + documentName; + String fileLocation = uploadDocumentLocation + File.separator + fileName; - writeFileToFileSystem(uploadedInputStream, fileLocation); + writeFileToFileSystem(fileName, uploadedInputStream, fileLocation); return fileLocation; } - @Override - public String saveImage(InputStream uploadedInputStream, Long resourceId, String imageName, Long fileSize) throws DocumentManagementException { + public String saveImage(InputStream uploadedInputStream, Long resourceId, String imageName, Long fileSize) { String uploadImageLocation = generateClientImageParentDirectory(resourceId); - validateFileSizeWithinPermissibleRange(fileSize, imageName, maxImageSize); + ContentRepositoryUtils.validateFileSizeWithinPermissibleRange(fileSize, imageName); makeDirectories(uploadImageLocation); String fileLocation = uploadImageLocation + File.separator + imageName; - writeFileToFileSystem(uploadedInputStream, fileLocation); + writeFileToFileSystem(imageName, uploadedInputStream, fileLocation); return fileLocation; } @Override - public String saveImage(Base64EncodedImage base64EncodedImage, Long resourceId, String imageName) throws DocumentManagementException { + public String saveImage(Base64EncodedImage base64EncodedImage, Long resourceId, String imageName) { String uploadImageLocation = generateClientImageParentDirectory(resourceId); makeDirectories(uploadImageLocation); @@ -65,8 +71,8 @@ public class FileSystemDocumentStore extends DocumentStore { out.write(imgBytes); out.flush(); out.close(); - }catch (IOException ioe){ - throw new DocumentManagementException(ioe.getMessage()); + } catch (IOException ioe) { + throw new ContentManagementException(imageName, ioe.getMessage()); } return fileLocation; } @@ -81,9 +87,9 @@ public class FileSystemDocumentStore extends DocumentStore { } @Override - public void deleteDocument(String documentName, String documentPath) throws DocumentManagementException{ + public void deleteFile(String fileName, String documentPath) { final boolean fileDeleted = deleteFile(documentPath); - if (!fileDeleted) { throw new DocumentManagementException(documentName); } + if (!fileDeleted) { throw new ContentManagementException(fileName, null); } } private boolean deleteFile(String documentPath) { @@ -92,42 +98,43 @@ public class FileSystemDocumentStore extends DocumentStore { } @Override - public DocumentStoreType getType() { - return DocumentStoreType.FILE_SYSTEM; + public StorageType getStorageType() { + return StorageType.FILE_SYSTEM; } @Override - public FileData retrieveDocument(DocumentData documentData){ + public FileData fetchFile(DocumentData documentData) { File file = new File(documentData.fileLocation()); return new FileData(file, documentData.fileName(), documentData.contentType()); } @Override - public ImageData retrieveImage(ImageData imageData) { - File file = new File(imageData.imageKey()); - imageData.setContent(file); + public ImageData fetchImage(ImageData imageData) { + File file = new File(imageData.location()); + imageData.updateContent(file); return imageData; } /** * Generate the directory path for storing the new document - * + * * @param entityType * @param entityId * @return */ private String generateFileParentDirectory(String entityType, Long entityId) { - return FileSystemDocumentStore.MIFOSX_BASE_DIR + File.separator + ThreadLocalContextUtil.getTenant().getName().replaceAll(" ", "").trim() - + File.separator + "documents" + File.separator + entityType + File.separator + entityId + File.separator - + RandomStringGenerator.generateRandomString(); + return FileSystemContentRepository.MIFOSX_BASE_DIR + File.separator + + ThreadLocalContextUtil.getTenant().getName().replaceAll(" ", "").trim() + File.separator + "documents" + File.separator + + entityType + File.separator + entityId + File.separator + ContentRepositoryUtils.generateRandomString(); } /** * Generate directory path for storing new Image */ private String generateClientImageParentDirectory(final Long resourceId) { - return FileSystemDocumentStore.MIFOSX_BASE_DIR + File.separator + ThreadLocalContextUtil.getTenant().getName().replaceAll(" ", "").trim() - + File.separator + "images" + File.separator + "clients" + File.separator + resourceId; + return FileSystemContentRepository.MIFOSX_BASE_DIR + File.separator + + ThreadLocalContextUtil.getTenant().getName().replaceAll(" ", "").trim() + File.separator + "images" + File.separator + + "clients" + File.separator + resourceId; } /** @@ -139,9 +146,7 @@ public class FileSystemDocumentStore extends DocumentStore { } } - - private void writeFileToFileSystem(InputStream uploadedInputStream, String fileLocation) throws DocumentManagementException { - + private void writeFileToFileSystem(String fileName, InputStream uploadedInputStream, String fileLocation) { try { OutputStream out = new FileOutputStream(new File(fileLocation)); int read = 0; @@ -153,8 +158,7 @@ public class FileSystemDocumentStore extends DocumentStore { out.flush(); out.close(); } catch (IOException ioException) { - throw new DocumentManagementException("IO Exception caught while writing to FileSystem" + ioException.getMessage()); + throw new ContentManagementException(fileName, ioException.getMessage()); } } - } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/contentrepository/S3ContentRepository.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/contentrepository/S3ContentRepository.java new file mode 100644 index 000000000..fb80d9784 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/contentrepository/S3ContentRepository.java @@ -0,0 +1,157 @@ +package org.mifosplatform.infrastructure.documentmanagement.contentrepository; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.InputStream; + +import org.mifosplatform.infrastructure.core.domain.Base64EncodedImage; +import org.mifosplatform.infrastructure.documentmanagement.command.DocumentCommand; +import org.mifosplatform.infrastructure.documentmanagement.data.DocumentData; +import org.mifosplatform.infrastructure.documentmanagement.data.FileData; +import org.mifosplatform.infrastructure.documentmanagement.data.ImageData; +import org.mifosplatform.infrastructure.documentmanagement.domain.StorageType; +import org.mifosplatform.infrastructure.documentmanagement.exception.ContentManagementException; +import org.mifosplatform.infrastructure.documentmanagement.exception.DocumentNotFoundException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.amazonaws.AmazonClientException; +import com.amazonaws.AmazonServiceException; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.DeleteObjectRequest; +import com.amazonaws.services.s3.model.GetObjectRequest; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.amazonaws.services.s3.model.S3Object; +import com.lowagie.text.pdf.codec.Base64; + +public class S3ContentRepository implements ContentRepository { + + private final static Logger logger = LoggerFactory.getLogger(S3ContentRepository.class); + + private final String s3BucketName; + private AmazonS3 s3Client; + + public S3ContentRepository(String bucketName, String secretKey, String accessKey) { + this.s3BucketName = bucketName; + this.s3Client = new AmazonS3Client(new BasicAWSCredentials(accessKey, secretKey)); + } + + @Override + public String saveFile(InputStream toUpload, DocumentCommand documentCommand) { + String fileName = documentCommand.getFileName(); + ContentRepositoryUtils.validateFileSizeWithinPermissibleRange(documentCommand.getSize(), fileName); + + String uploadDocFolder = generateFileParentDirectory(documentCommand.getParentEntityType(), documentCommand.getParentEntityId()); + String uploadDocFullPath = uploadDocFolder + File.separator + fileName; + + uploadDocument(fileName, toUpload, uploadDocFullPath); + return uploadDocFullPath; + } + + @Override + public void deleteFile(String documentName, String documentPath) { + try { + deleteObjectFromS3(documentPath); + } catch (AmazonClientException ace) { + throw new ContentManagementException(documentName, ace.getMessage()); + } + } + + @Override + public String saveImage(InputStream toUploadInputStream, Long resourceId, String imageName, Long fileSize) { + ContentRepositoryUtils.validateFileSizeWithinPermissibleRange(fileSize, imageName); + String uploadImageLocation = generateClientImageParentDirectory(resourceId); + String fileLocation = uploadImageLocation + File.separator + imageName; + + uploadDocument(imageName, toUploadInputStream, fileLocation); + return fileLocation; + } + + @Override + public String saveImage(Base64EncodedImage base64EncodedImage, Long resourceId, String imageName) { + String uploadImageLocation = generateClientImageParentDirectory(resourceId); + String fileLocation = uploadImageLocation + File.separator + imageName + base64EncodedImage.getFileExtension(); + InputStream toUploadInputStream = new ByteArrayInputStream(Base64.decode(base64EncodedImage.getBase64EncodedString())); + + uploadDocument(imageName, toUploadInputStream, fileLocation); + return fileLocation; + } + + @Override + public void deleteImage(Long resourceId, String location) { + try { + deleteObjectFromS3(location); + } catch (AmazonServiceException ase) { + deleteObjectAmazonServiceExceptionMessage(ase); + logger.warn("Unable to delete image associated with clients with Id " + resourceId); + } catch (AmazonClientException ace) { + deleteObjectAmazonClientExceptionMessage(ace); + logger.warn("Unable to delete image associated with clients with Id " + resourceId); + } + } + + @Override + public StorageType getStorageType() { + return StorageType.S3; + } + + @Override + public FileData fetchFile(DocumentData documentData) throws DocumentNotFoundException { + FileData fileData = null; + String fileName = documentData.fileName(); + try { + logger.info("Downloading an object"); + S3Object s3object = s3Client.getObject(new GetObjectRequest(s3BucketName, documentData.fileLocation())); + fileData = new FileData(s3object.getObjectContent(), fileName, documentData.contentType()); + } catch (AmazonClientException ace) { + logger.error(ace.getMessage()); + throw new DocumentNotFoundException(documentData.getParentEntityType(), documentData.getParentEntityId(), documentData.getId()); + } + return fileData; + } + + @Override + public ImageData fetchImage(ImageData imageData) { + S3Object s3object = s3Client.getObject(new GetObjectRequest(s3BucketName, imageData.location())); + imageData.updateContent(s3object.getObjectContent()); + return imageData; + } + + private void deleteObjectAmazonClientExceptionMessage(AmazonClientException ace) { + String message = "Caught an AmazonClientException." + "Error Message: " + ace.getMessage(); + logger.error(message); + } + + private void deleteObjectAmazonServiceExceptionMessage(AmazonServiceException ase) { + String message = "Caught an AmazonServiceException." + "Error Message: " + ase.getMessage() + "HTTP Status Code: " + + ase.getStatusCode() + "AWS Error Code: " + ase.getErrorCode() + "Error Type: " + ase.getErrorType() + + "Request ID: " + ase.getRequestId(); + logger.error(message); + } + + private String generateFileParentDirectory(String entityType, Long entityId) { + return "documents" + File.separator + entityType + File.separator + entityId + File.separator + + ContentRepositoryUtils.generateRandomString(); + } + + private String generateClientImageParentDirectory(Long resourceId) { + return "images" + File.separator + "clients" + File.separator + resourceId; + } + + private void deleteObjectFromS3(String location) { + s3Client.deleteObject(new DeleteObjectRequest(s3BucketName, location)); + } + + private void uploadDocument(String filename, InputStream inputStream, String s3UploadLocation) throws ContentManagementException { + try { + logger.info("Uploading a new object to S3 from a file to " + s3UploadLocation); + s3Client.putObject(new PutObjectRequest(this.s3BucketName, s3UploadLocation, inputStream, new ObjectMetadata())); + } catch (AmazonClientException ace) { + String message = ace.getMessage(); + throw new ContentManagementException(filename, message); + } + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/data/DocumentData.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/data/DocumentData.java index 6cf339bef..25d155473 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/data/DocumentData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/data/DocumentData.java @@ -5,19 +5,15 @@ */ package org.mifosplatform.infrastructure.documentmanagement.data; -import org.mifosplatform.infrastructure.core.service.DocumentStore; -import org.mifosplatform.infrastructure.core.service.DocumentStoreType; +import org.mifosplatform.infrastructure.documentmanagement.domain.StorageType; /** * Immutable data object represent document being managed on platform. */ public class DocumentData { - @SuppressWarnings("unused") private final Long id; - @SuppressWarnings("unused") private final String parentEntityType; - @SuppressWarnings("unused") private final Long parentEntityId; @SuppressWarnings("unused") private final String name; @@ -27,12 +23,11 @@ public class DocumentData { private final String type; @SuppressWarnings("unused") private final String description; - private String location; - - private String storageType; + private final String location; + private final Integer storageType; public DocumentData(final Long id, final String parentEntityType, final Long parentEntityId, final String name, final String fileName, - final Long size, final String type, final String description, final String location, String storageType) { + final Long size, final String type, final String description, final String location, Integer storageType) { this.id = id; this.parentEntityType = parentEntityType; this.parentEntityId = parentEntityId; @@ -57,23 +52,19 @@ public class DocumentData { return this.location; } - public void setLocation(final String location) { - this.location = location; + public StorageType storageType() { + return StorageType.fromInt(storageType); } - public DocumentStoreType storeType(){ - return DocumentStore.getDocumentStoreType(storageType); - } - - public String getParentEntityType(){ + public String getParentEntityType() { return this.parentEntityType; } - public Long getParentEntityId(){ + public Long getParentEntityId() { return this.parentEntityId; } - public Long getId(){ + public Long getId() { return this.id; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/data/FileData.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/data/FileData.java index bbaad808f..6c7761154 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/data/FileData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/data/FileData.java @@ -9,6 +9,7 @@ import java.io.FileNotFoundException; import java.io.InputStream; public class FileData { + private static final Logger logger = LoggerFactory.getLogger(FileData.class); private final File file; @@ -38,19 +39,13 @@ public class FileData { return fileName; } - public InputStream file(){ - try{ - if(inputStream == null){ - return new FileInputStream(file); - } - else { - return inputStream; - } - - } catch (FileNotFoundException e){ + public InputStream file() { + try { + if (inputStream == null) { return new FileInputStream(file); } + return inputStream; + } catch (FileNotFoundException e) { logger.error(e.toString()); return null; } } } - diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/data/ImageData.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/data/ImageData.java new file mode 100644 index 000000000..7a25e895d --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/data/ImageData.java @@ -0,0 +1,90 @@ +package org.mifosplatform.infrastructure.documentmanagement.data; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.lang.StringUtils; +import org.apache.poi.util.IOUtils; +import org.mifosplatform.infrastructure.documentmanagement.contentrepository.ContentRepositoryUtils; +import org.mifosplatform.infrastructure.documentmanagement.domain.StorageType; + +public class ImageData { + + @SuppressWarnings("unused") + private final Long imageId; + private final String location; + private final Integer storageType; + private final String entityDisplayName; + + private File file; + private String contentType; + private InputStream inputStream; + + public ImageData(Long imageId, String location, Integer storageType, String entityDisplayName) { + this.imageId = imageId; + this.location = location; + this.storageType = storageType; + this.entityDisplayName = entityDisplayName; + } + + public byte[] getContent() { + // TODO Vishwas Fix error handling + try { + if (inputStream == null) { + FileInputStream fileInputStream = new FileInputStream(file); + return IOUtils.toByteArray(fileInputStream); + } + + return IOUtils.toByteArray(inputStream); + } catch (IOException e) { + return null; + } + } + + private String setImageContentType() { + String contentType = ContentRepositoryUtils.IMAGE_MIME_TYPE.JPEG.getValue(); + + if (file != null) { + String fileName = file.getName(); + + if (StringUtils.endsWith(fileName, ContentRepositoryUtils.IMAGE_FILE_EXTENSION.GIF.getValue())) { + contentType = ContentRepositoryUtils.IMAGE_MIME_TYPE.GIF.getValue(); + } else if (StringUtils.endsWith(fileName, ContentRepositoryUtils.IMAGE_FILE_EXTENSION.PNG.getValue())) { + contentType = ContentRepositoryUtils.IMAGE_MIME_TYPE.PNG.getValue(); + } + } + return contentType; + } + + public String contentType() { + return this.contentType; + } + + public StorageType storageType() { + return StorageType.fromInt(storageType); + } + + public String name() { + return this.file.getName(); + } + + public String location() { + return this.location; + } + + public void updateContent(InputStream objectContent) { + this.inputStream = objectContent; + } + + public void updateContent(File file) { + this.file = file; + setImageContentType(); + } + + public String getEntityDisplayName() { + return this.entityDisplayName; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/domain/Document.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/domain/Document.java index 33e0747d2..5ae3e0a73 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/domain/Document.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/domain/Document.java @@ -5,16 +5,14 @@ */ package org.mifosplatform.infrastructure.documentmanagement.domain; -import org.apache.commons.lang.StringUtils; -import org.mifosplatform.infrastructure.core.service.DocumentStore; -import org.mifosplatform.infrastructure.core.service.DocumentStoreType; -import org.mifosplatform.infrastructure.documentmanagement.command.DocumentCommand; -import org.springframework.data.jpa.domain.AbstractPersistable; - import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Table; +import org.apache.commons.lang.StringUtils; +import org.mifosplatform.infrastructure.documentmanagement.command.DocumentCommand; +import org.springframework.data.jpa.domain.AbstractPersistable; + @Entity @Table(name = "m_document") public class Document extends AbstractPersistable { @@ -43,19 +41,18 @@ public class Document extends AbstractPersistable { @Column(name = "location", length = 500) private String location; - @Column(name = "storage_type", length = 50) - private String storageType; + @Column(name = "storage_type_enum") + private Integer storageType; - public Document() { - } + public Document() {} public static Document createNew(final String parentEntityType, final Long parentEntityId, final String name, final String fileName, - final Long size, final String type, final String description, final String location, final DocumentStoreType storageType) { + final Long size, final String type, final String description, final String location, final StorageType storageType) { return new Document(parentEntityType, parentEntityId, name, fileName, size, type, description, location, storageType); } private Document(final String parentEntityType, final Long parentEntityId, final String name, final String fileName, final Long size, - final String type, final String description, final String location, final DocumentStoreType storageType) { + final String type, final String description, final String location, final StorageType storageType) { this.parentEntityType = StringUtils.defaultIfEmpty(parentEntityType, null); this.parentEntityId = parentEntityId; this.name = StringUtils.defaultIfEmpty(name, null); @@ -86,7 +83,6 @@ public class Document extends AbstractPersistable { if (command.isSizeChanged()) { this.size = command.getSize(); } - this.storageType = command.getStorageType(); } public String getParentEntityType() { @@ -153,7 +149,7 @@ public class Document extends AbstractPersistable { this.location = location; } - public DocumentStoreType storageType(){ - return DocumentStore.getDocumentStoreType(this.storageType); + public StorageType storageType() { + return StorageType.fromInt(this.storageType); } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/domain/Image.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/domain/Image.java new file mode 100644 index 000000000..c4b21c373 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/domain/Image.java @@ -0,0 +1,49 @@ +/** + * 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.infrastructure.documentmanagement.domain; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +import org.springframework.data.jpa.domain.AbstractPersistable; + +@Entity +@Table(name = "m_image") +public final class Image extends AbstractPersistable { + + @Column(name = "location", length = 500) + private String location; + + @Column(name = "storage_type_enum") + private Integer storageType; + + public Image(String location, StorageType storageType) { + this.location = location; + this.storageType = storageType.getValue(); + } + + protected Image() { + + } + + public String getLocation() { + return this.location; + } + + public Integer getStorageType() { + return this.storageType; + } + + public void setLocation(String location) { + this.location = location; + } + + public void setStorageType(Integer storageType) { + this.storageType = storageType; + } + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/domain/ImageRepository.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/domain/ImageRepository.java similarity index 71% rename from mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/domain/ImageRepository.java rename to mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/domain/ImageRepository.java index ed0fe6cc1..946cf2eae 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/domain/ImageRepository.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/domain/ImageRepository.java @@ -3,11 +3,10 @@ * 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.client.domain; +package org.mifosplatform.infrastructure.documentmanagement.domain; +import org.mifosplatform.portfolio.client.domain.Client; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; -public interface ImageRepository extends JpaRepository, JpaSpecificationExecutor { - Image findOneByClient(Client client); -} \ No newline at end of file +public interface ImageRepository extends JpaRepository, JpaSpecificationExecutor {} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/domain/StorageType.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/domain/StorageType.java new file mode 100644 index 000000000..2cd78d59f --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/domain/StorageType.java @@ -0,0 +1,30 @@ +package org.mifosplatform.infrastructure.documentmanagement.domain; + +import java.util.HashMap; +import java.util.Map; + +public enum StorageType { + FILE_SYSTEM(1), S3(2); + + private Integer value; + + StorageType(Integer value) { + this.value = value; + } + + public Integer getValue() { + return value; + } + + private static final Map intToEnumMap = new HashMap(); + static { + for (final StorageType type : StorageType.values()) { + intToEnumMap.put(type.value, type); + } + } + + public static StorageType fromInt(final int i) { + final StorageType type = intToEnumMap.get(Integer.valueOf(i)); + return type; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/exception/DocumentManagementException.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/exception/ContentManagementException.java similarity index 59% rename from mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/exception/DocumentManagementException.java rename to mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/exception/ContentManagementException.java index 4c1f864e1..a45d00e64 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/exception/DocumentManagementException.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/exception/ContentManagementException.java @@ -7,13 +7,14 @@ package org.mifosplatform.infrastructure.documentmanagement.exception; import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRuleException; -public class DocumentManagementException extends AbstractPlatformDomainRuleException { +public class ContentManagementException extends AbstractPlatformDomainRuleException { - public DocumentManagementException(final String name) { - super("error.msg.document.save", "Error while manipulating file " + name + " due to a File system / Amazon S3 issue", name); + public ContentManagementException(final String filename, final String message) { + super("error.msg.document.save", + "Error while manipulating file " + filename + " due to a File system / Amazon S3 issue " + message, filename, message); } - public DocumentManagementException(final String name, final Long fileSize, final int maxFileSize) { + public ContentManagementException(final String name, final Long fileSize, final int maxFileSize) { super("error.msg.document.file.too.big", "Unable to save the document with name" + name + " since its file Size of " + fileSize / (1024 * 1024) + " MB exceeds the max permissable file size of " + maxFileSize + " MB", name, fileSize); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/DocumentReadPlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/DocumentReadPlatformService.java index af99a5f70..20a803cc2 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/DocumentReadPlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/DocumentReadPlatformService.java @@ -14,7 +14,7 @@ public interface DocumentReadPlatformService { Collection retrieveAllDocuments(String entityType, Long entityId); - FileData retrieveDocumentAsFile(String entityType, Long entityId, Long documentId); + FileData retrieveFileData(String entityType, Long entityId, Long documentId); DocumentData retrieveDocument(String entityType, Long entityId, Long documentId); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/DocumentReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/DocumentReadPlatformServiceImpl.java index c531eacdc..dfedcce55 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/DocumentReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/DocumentReadPlatformServiceImpl.java @@ -6,9 +6,9 @@ package org.mifosplatform.infrastructure.documentmanagement.service; import org.mifosplatform.infrastructure.core.domain.JdbcSupport; -import org.mifosplatform.infrastructure.core.service.DocumentStore; -import org.mifosplatform.infrastructure.core.service.DocumentStoreFactory; import org.mifosplatform.infrastructure.core.service.TenantAwareRoutingDataSource; +import org.mifosplatform.infrastructure.documentmanagement.contentrepository.ContentRepository; +import org.mifosplatform.infrastructure.documentmanagement.contentrepository.ContentRepositoryFactory; import org.mifosplatform.infrastructure.documentmanagement.data.DocumentData; import org.mifosplatform.infrastructure.documentmanagement.data.FileData; import org.mifosplatform.infrastructure.documentmanagement.exception.DocumentNotFoundException; @@ -28,14 +28,14 @@ public class DocumentReadPlatformServiceImpl implements DocumentReadPlatformServ private final JdbcTemplate jdbcTemplate; private final PlatformSecurityContext context; - private final DocumentStoreFactory documentStoreFactory; + private final ContentRepositoryFactory contentRepositoryFactory; @Autowired public DocumentReadPlatformServiceImpl(final PlatformSecurityContext context, final TenantAwareRoutingDataSource dataSource, - final DocumentStoreFactory documentStoreFactory) { + final ContentRepositoryFactory documentStoreFactory) { this.context = context; this.jdbcTemplate = new JdbcTemplate(dataSource); - this.documentStoreFactory = documentStoreFactory; + this.contentRepositoryFactory = documentStoreFactory; } @Override @@ -46,46 +46,60 @@ public class DocumentReadPlatformServiceImpl implements DocumentReadPlatformServ // TODO verify if the entities are valid and a user // has data // scope for the particular entities - final DocumentMapper mapper = new DocumentMapper(true); + final DocumentMapper mapper = new DocumentMapper(true, true); final String sql = "select " + mapper.schema() + " order by d.id"; return this.jdbcTemplate.query(sql, mapper, new Object[] { entityType, entityId }); } @Override - public FileData retrieveDocumentAsFile(final String entityType, final Long entityId, final Long documentId) { + public FileData retrieveFileData(final String entityType, final Long entityId, final Long documentId) { try { - DocumentData documentData = getDocumentData(entityType, entityId, documentId); - DocumentStore instanceForRead = documentStoreFactory.getInstanceFromStorageType(documentData.storeType()); - return instanceForRead.retrieveDocument(documentData); + final DocumentMapper mapper = new DocumentMapper(false, false); + DocumentData documentData = fetchDocumentDetails(entityType, entityId, documentId, mapper); + ContentRepository contentRepository = contentRepositoryFactory.getRepository(documentData.storageType()); + return contentRepository.fetchFile(documentData); } catch (final EmptyResultDataAccessException e) { throw new DocumentNotFoundException(entityType, entityId, documentId); - } catch (final DocumentNotFoundException d) { - throw d; } } @Override public DocumentData retrieveDocument(String entityType, Long entityId, Long documentId) { try { - return getDocumentData(entityType, entityId, documentId); + final DocumentMapper mapper = new DocumentMapper(true, true); + return fetchDocumentDetails(entityType, entityId, documentId, mapper); } catch (final EmptyResultDataAccessException e) { throw new DocumentNotFoundException(entityType, entityId, documentId); } } + /** + * @param entityType + * @param entityId + * @param documentId + * @param mapper + * @return + */ + private DocumentData fetchDocumentDetails(final String entityType, final Long entityId, final Long documentId, + final DocumentMapper mapper) { + final String sql = "select " + mapper.schema() + " and d.id=? "; + return this.jdbcTemplate.queryForObject(sql, mapper, new Object[] { entityType, entityId, documentId }); + } + private static final class DocumentMapper implements RowMapper { private final boolean hideLocation; + private final boolean hideStorageType; - public DocumentMapper(final boolean hideLocation) { + public DocumentMapper(final boolean hideLocation, final boolean hideStorageType) { this.hideLocation = hideLocation; + this.hideStorageType = hideStorageType; } public String schema() { return "d.id as id, d.parent_entity_type as parentEntityType, d.parent_entity_id as parentEntityId, d.name as name, " + " d.file_name as fileName, d.size as fileSize, d.type as fileType, " - + " d.description as description, d.location as location," - + " d.storage_type as storageType" + + " d.description as description, d.location as location," + " d.storage_type_enum as storageType" + " from m_document d where d.parent_entity_type=? and d.parent_entity_id=?"; } @@ -101,24 +115,16 @@ public class DocumentReadPlatformServiceImpl implements DocumentReadPlatformServ final String fileType = rs.getString("fileType"); final String description = rs.getString("description"); String location = null; + Integer storageType = null; if (!this.hideLocation) { location = rs.getString("location"); } - final String storageType = rs.getString("storageType"); - - return new DocumentData(id, parentEntityType, parentEntityId, name, fileName, fileSize, fileType, description, location, storageType); + if (!this.hideStorageType) { + storageType = rs.getInt("storageType"); + } + return new DocumentData(id, parentEntityType, parentEntityId, name, fileName, fileSize, fileType, description, location, + storageType); } } - - private DocumentData getDocumentData(String entityType, Long entityId, Long documentId) { - this.context.authenticatedUser(); - - // TODO verify if the entities are valid and a - // user has data - // scope for the particular entities - final DocumentMapper mapper = new DocumentMapper(false); - final String sql = "select " + mapper.schema() + " and d.id=? "; - return this.jdbcTemplate.queryForObject(sql, mapper, new Object[]{entityType, entityId, documentId}); - } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/DocumentWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/DocumentWritePlatformServiceJpaRepositoryImpl.java index 83bff1a3c..e7734dc9e 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/DocumentWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/DocumentWritePlatformServiceJpaRepositoryImpl.java @@ -5,16 +5,18 @@ */ package org.mifosplatform.infrastructure.documentmanagement.service; +import java.io.InputStream; + import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; import org.mifosplatform.infrastructure.core.exception.PlatformDataIntegrityException; -import org.mifosplatform.infrastructure.core.service.DocumentStore; -import org.mifosplatform.infrastructure.core.service.DocumentStoreFactory; -import org.mifosplatform.infrastructure.core.service.DocumentStoreType; import org.mifosplatform.infrastructure.documentmanagement.command.DocumentCommand; import org.mifosplatform.infrastructure.documentmanagement.command.DocumentCommandValidator; +import org.mifosplatform.infrastructure.documentmanagement.contentrepository.ContentRepository; +import org.mifosplatform.infrastructure.documentmanagement.contentrepository.ContentRepositoryFactory; import org.mifosplatform.infrastructure.documentmanagement.domain.Document; import org.mifosplatform.infrastructure.documentmanagement.domain.DocumentRepository; -import org.mifosplatform.infrastructure.documentmanagement.exception.DocumentManagementException; +import org.mifosplatform.infrastructure.documentmanagement.domain.StorageType; +import org.mifosplatform.infrastructure.documentmanagement.exception.ContentManagementException; import org.mifosplatform.infrastructure.documentmanagement.exception.DocumentNotFoundException; import org.mifosplatform.infrastructure.documentmanagement.exception.InvalidEntityTypeForDocumentManagementException; import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; @@ -25,8 +27,6 @@ import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.io.InputStream; - @Service public class DocumentWritePlatformServiceJpaRepositoryImpl implements DocumentWritePlatformService { @@ -34,14 +34,14 @@ public class DocumentWritePlatformServiceJpaRepositoryImpl implements DocumentWr private final PlatformSecurityContext context; private final DocumentRepository documentRepository; - private final DocumentStoreFactory documentStoreFactory; + private final ContentRepositoryFactory contentRepositoryFactory; @Autowired - public DocumentWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context, final DocumentRepository documentRepository, - final DocumentStoreFactory documentStoreFactory) { + public DocumentWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context, + final DocumentRepository documentRepository, final ContentRepositoryFactory documentStoreFactory) { this.context = context; this.documentRepository = documentRepository; - this.documentStoreFactory = documentStoreFactory; + this.contentRepositoryFactory = documentStoreFactory; } @Transactional @@ -56,13 +56,13 @@ public class DocumentWritePlatformServiceJpaRepositoryImpl implements DocumentWr validator.validateForCreate(); - DocumentStore documentStore = this.documentStoreFactory.getInstanceFromConfiguration(); + ContentRepository contentRepository = this.contentRepositoryFactory.getRepository(); - final String fileLocation = documentStore.saveDocument(inputStream, documentCommand); + final String fileLocation = contentRepository.saveFile(inputStream, documentCommand); final Document document = Document.createNew(documentCommand.getParentEntityType(), documentCommand.getParentEntityId(), documentCommand.getName(), documentCommand.getFileName(), documentCommand.getSize(), documentCommand.getType(), - documentCommand.getDescription(), fileLocation, documentStore.getType()); + documentCommand.getDescription(), fileLocation, contentRepository.getStorageType()); this.documentRepository.save(document); @@ -71,9 +71,6 @@ public class DocumentWritePlatformServiceJpaRepositoryImpl implements DocumentWr logger.error(dve.getMessage(), dve); throw new PlatformDataIntegrityException("error.msg.document.unknown.data.integrity.issue", "Unknown data integrity issue with resource."); - } catch (final DocumentManagementException dme) { - logger.error(dme.getMessage(), dme); - throw new DocumentManagementException(documentCommand.getName()); } } @@ -89,21 +86,22 @@ public class DocumentWritePlatformServiceJpaRepositoryImpl implements DocumentWr // TODO check if entity id is valid and within data scope for the // user final Document documentForUpdate = this.documentRepository.findOne(documentCommand.getId()); - DocumentStoreType documentStoreType = documentForUpdate.storageType(); if (documentForUpdate == null) { throw new DocumentNotFoundException(documentCommand.getParentEntityType(), documentCommand.getParentEntityId(), documentCommand.getId()); } + + StorageType documentStoreType = documentForUpdate.storageType(); oldLocation = documentForUpdate.getLocation(); if (inputStream != null && documentCommand.isFileNameChanged()) { - DocumentStore instanceFromConfiguration = this.documentStoreFactory.getInstanceFromConfiguration(); - documentCommand.setLocation(instanceFromConfiguration.saveDocument(inputStream, documentCommand)); - documentCommand.setStorageType(instanceFromConfiguration.getType()); + ContentRepository contentRepository = this.contentRepositoryFactory.getRepository(); + documentCommand.setLocation(contentRepository.saveFile(inputStream, documentCommand)); + documentCommand.setStorageType(contentRepository.getStorageType().getValue()); } documentForUpdate.update(documentCommand); if (inputStream != null && documentCommand.isFileNameChanged()) { - DocumentStore documentStore = this.documentStoreFactory.getInstanceFromStorageType(documentStoreType); - documentStore.deleteDocument(documentCommand.getName(), oldLocation); + ContentRepository contentRepository = this.contentRepositoryFactory.getRepository(documentStoreType); + contentRepository.deleteFile(documentCommand.getName(), oldLocation); } this.documentRepository.saveAndFlush(documentForUpdate); @@ -113,9 +111,9 @@ public class DocumentWritePlatformServiceJpaRepositoryImpl implements DocumentWr logger.error(dve.getMessage(), dve); throw new PlatformDataIntegrityException("error.msg.document.unknown.data.integrity.issue", "Unknown data integrity issue with resource."); - } catch (final DocumentManagementException dme) { - logger.error(dme.getMessage(), dme); - throw new DocumentManagementException(documentCommand.getName()); + } catch (final ContentManagementException cme) { + logger.error(cme.getMessage(), cme); + throw new ContentManagementException(documentCommand.getName(), cme.getMessage()); } } @@ -127,12 +125,12 @@ public class DocumentWritePlatformServiceJpaRepositoryImpl implements DocumentWr validateParentEntityType(documentCommand); // TODO: Check document is present under this entity Id final Document document = this.documentRepository.findOne(documentCommand.getId()); - DocumentStore documentStore = this.documentStoreFactory.getInstanceFromStorageType(document.storageType()); if (document == null) { throw new DocumentNotFoundException(documentCommand.getParentEntityType(), documentCommand.getParentEntityId(), documentCommand.getId()); } - this.documentRepository.delete(document); - documentStore.deleteDocument(document.getName(), document.getLocation()); + + ContentRepository contentRepository = this.contentRepositoryFactory.getRepository(document.storageType()); + contentRepository.deleteFile(document.getName(), document.getLocation()); return new CommandProcessingResult(document.getId()); } @@ -142,7 +140,6 @@ public class DocumentWritePlatformServiceJpaRepositoryImpl implements DocumentWr } private static boolean checkValidEntityType(final String entityType) { - for (final DOCUMENT_MANAGEMENT_ENTITY entities : DOCUMENT_MANAGEMENT_ENTITY.values()) { if (entities.name().equalsIgnoreCase(entityType)) { return true; } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/ImageReadPlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/ImageReadPlatformService.java new file mode 100755 index 000000000..eca7a90c6 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/ImageReadPlatformService.java @@ -0,0 +1,14 @@ +/** + * 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.infrastructure.documentmanagement.service; + +import org.mifosplatform.infrastructure.documentmanagement.data.ImageData; + +public interface ImageReadPlatformService { + + ImageData retrieveClientImage(Long clientId); + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/ImageReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/ImageReadPlatformServiceImpl.java new file mode 100755 index 000000000..c4e560658 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/ImageReadPlatformServiceImpl.java @@ -0,0 +1,79 @@ +/** + * 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.infrastructure.documentmanagement.service; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.mifosplatform.infrastructure.core.domain.JdbcSupport; +import org.mifosplatform.infrastructure.core.service.TenantAwareRoutingDataSource; +import org.mifosplatform.infrastructure.documentmanagement.contentrepository.ContentRepository; +import org.mifosplatform.infrastructure.documentmanagement.contentrepository.ContentRepositoryFactory; +import org.mifosplatform.infrastructure.documentmanagement.data.ImageData; +import org.mifosplatform.portfolio.client.domain.Client; +import org.mifosplatform.portfolio.client.domain.ClientRepositoryWrapper; +import org.mifosplatform.portfolio.client.exception.ImageNotFoundException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Service; + +@Service +public class ImageReadPlatformServiceImpl implements ImageReadPlatformService { + + private final JdbcTemplate jdbcTemplate; + private final ContentRepositoryFactory contentRepositoryFactory; + private final ClientRepositoryWrapper clientRepositoryWrapper; + + @Autowired + public ImageReadPlatformServiceImpl(final TenantAwareRoutingDataSource dataSource, final ContentRepositoryFactory documentStoreFactory, + final ClientRepositoryWrapper clientRepositoryWrapper) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + this.contentRepositoryFactory = documentStoreFactory; + this.clientRepositoryWrapper = clientRepositoryWrapper; + } + + private static final class ImageMapper implements RowMapper { + + private final String entityDisplayName; + + public ImageMapper(final String entityDisplayName) { + this.entityDisplayName = entityDisplayName; + } + + public String schema() { + return " image.id as id, image.location as location, image.storage_type_enum as storageType " + + " from m_image image , m_client client " + " where client.image_id = image.id and client.id=?"; + } + + @Override + public ImageData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + + final Long id = JdbcSupport.getLong(rs, "id"); + final String location = rs.getString("location"); + final Integer storageType = JdbcSupport.getInteger(rs, "storageType"); + return new ImageData(id, location, storageType, entityDisplayName); + } + } + + @Override + public ImageData retrieveClientImage(Long clientId) { + try { + Client client = clientRepositoryWrapper.findOneWithNotFoundDetection(clientId); + + final ImageMapper imageMapper = new ImageMapper(client.getDisplayName()); + + final String sql = "select " + imageMapper.schema(); + + ImageData imageData = this.jdbcTemplate.queryForObject(sql, imageMapper, new Object[] { clientId }); + ContentRepository contentRepository = contentRepositoryFactory.getRepository(imageData.storageType()); + return contentRepository.fetchImage(imageData); + } catch (EmptyResultDataAccessException e) { + throw new ImageNotFoundException("clients", clientId); + } + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/ImageWritePlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/ImageWritePlatformService.java new file mode 100755 index 000000000..950fbe126 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/ImageWritePlatformService.java @@ -0,0 +1,24 @@ +/** + * 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.infrastructure.documentmanagement.service; + +import java.io.InputStream; + +import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; +import org.mifosplatform.infrastructure.core.domain.Base64EncodedImage; +import org.springframework.security.access.prepost.PreAuthorize; + +public interface ImageWritePlatformService { + + @PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'CREATE_CLIENTIMAGE')") + CommandProcessingResult saveOrUpdateClientImage(Long clientId, String imageName, InputStream inputStream, Long fileSize); + + @PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'CREATE_CLIENTIMAGE')") + CommandProcessingResult saveOrUpdateClientImage(Long clientId, Base64EncodedImage encodedImage); + + @PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'DELETE_CLIENTIMAGE')") + CommandProcessingResult deleteClientImage(Long clientId); +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/ImageWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/ImageWritePlatformServiceJpaRepositoryImpl.java new file mode 100755 index 000000000..cb5a1b3ce --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/ImageWritePlatformServiceJpaRepositoryImpl.java @@ -0,0 +1,103 @@ +/** + * 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.infrastructure.documentmanagement.service; + +import java.io.InputStream; + +import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; +import org.mifosplatform.infrastructure.core.domain.Base64EncodedImage; +import org.mifosplatform.infrastructure.documentmanagement.contentrepository.ContentRepository; +import org.mifosplatform.infrastructure.documentmanagement.contentrepository.ContentRepositoryFactory; +import org.mifosplatform.infrastructure.documentmanagement.domain.Image; +import org.mifosplatform.infrastructure.documentmanagement.domain.ImageRepository; +import org.mifosplatform.infrastructure.documentmanagement.domain.StorageType; +import org.mifosplatform.portfolio.client.domain.Client; +import org.mifosplatform.portfolio.client.domain.ClientRepositoryWrapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class ImageWritePlatformServiceJpaRepositoryImpl implements ImageWritePlatformService { + + private final ContentRepositoryFactory contentRepositoryFactory; + private final ClientRepositoryWrapper clientRepositoryWrapper; + private final ImageRepository imageRepository; + + @Autowired + public ImageWritePlatformServiceJpaRepositoryImpl(final ContentRepositoryFactory documentStoreFactory, + final ClientRepositoryWrapper clientRepositoryWrapper, final ImageRepository imageRepository) { + this.contentRepositoryFactory = documentStoreFactory; + this.clientRepositoryWrapper = clientRepositoryWrapper; + this.imageRepository = imageRepository; + } + + @Transactional + @Override + public CommandProcessingResult saveOrUpdateClientImage(final Long clientId, final String imageName, final InputStream inputStream, + Long fileSize) { + final Client client = this.clientRepositoryWrapper.findOneWithNotFoundDetection(clientId); + deletePreviousClientImage(client); + + ContentRepository contentRepository = this.contentRepositoryFactory.getRepository(); + String imageLocation = contentRepository.saveImage(inputStream, clientId, imageName, fileSize); + return updateClientImage(client, imageLocation, contentRepository.getStorageType()); + } + + @Transactional + @Override + public CommandProcessingResult saveOrUpdateClientImage(final Long clientId, final Base64EncodedImage encodedImage) { + final Client client = this.clientRepositoryWrapper.findOneWithNotFoundDetection(clientId); + deletePreviousClientImage(client); + + ContentRepository contenRepository = this.contentRepositoryFactory.getRepository(); + final String imageLocation = contenRepository.saveImage(encodedImage, clientId, "image"); + + return updateClientImage(client, imageLocation, contenRepository.getStorageType()); + } + + @Transactional + @Override + public CommandProcessingResult deleteClientImage(final Long clientId) { + + final Client client = this.clientRepositoryWrapper.findOneWithNotFoundDetection(clientId); + + Image image = client.getImage(); + // delete image from the file system + if (image != null) { + ContentRepository contentRepository = this.contentRepositoryFactory.getRepository(StorageType.fromInt(image.getStorageType())); + contentRepository.deleteImage(clientId, image.getLocation()); + client.setImage(null); + this.imageRepository.delete(image); + this.clientRepositoryWrapper.save(client); + } + + return new CommandProcessingResult(clientId); + } + + private void deletePreviousClientImage(final Client client) { + Image image = client.getImage(); + if (image != null) { + ContentRepository contentRepository = this.contentRepositoryFactory.getRepository(StorageType.fromInt(image.getStorageType())); + contentRepository.deleteImage(client.getId(), image.getLocation()); + } + } + + private CommandProcessingResult updateClientImage(final Client client, final String imageLocation, final StorageType storageType) { + Image image = client.getImage(); + if (image == null) { + image = new Image(imageLocation, storageType); + } else { + image.setLocation(imageLocation); + image.setStorageType(storageType.getValue()); + } + this.imageRepository.save(image); + client.setImage(image); + this.clientRepositoryWrapper.save(client); + return new CommandProcessingResult(client.getId()); + } + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/data/ClientData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/data/ClientData.java index d72f85f80..1463f0473 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/data/ClientData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/data/ClientData.java @@ -5,6 +5,9 @@ */ package org.mifosplatform.portfolio.client.data; +import java.util.Collection; +import java.util.List; + import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.builder.CompareToBuilder; import org.apache.commons.lang.builder.EqualsBuilder; @@ -13,20 +16,12 @@ import org.joda.time.LocalDate; import org.mifosplatform.infrastructure.core.data.EnumOptionData; import org.mifosplatform.organisation.office.data.OfficeData; import org.mifosplatform.portfolio.group.data.GroupGeneralData; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.util.Collection; -import java.util.List; /** * Immutable data object representing client data. */ final public class ClientData implements Comparable { - private final static Logger logger = LoggerFactory.getLogger(ClientData.class); - private final Long id; private final String accountNo; @@ -46,9 +41,7 @@ final public class ClientData implements Comparable { private final Long officeId; private final String officeName; -// private String imageData; - private ImageData imageData; - @SuppressWarnings("unused") + private final Long imageId; private final Boolean imagePresent; // associations @@ -65,13 +58,13 @@ final public class ClientData implements Comparable { return new ClientData(clientData.accountNo, clientData.status, clientData.officeId, clientData.officeName, clientData.id, clientData.firstname, clientData.middlename, clientData.lastname, clientData.fullname, clientData.displayName, - clientData.externalId, clientData.activationDate, clientData.imageData, allowedOffices, clientData.groups); + clientData.externalId, clientData.activationDate, clientData.imageId, allowedOffices, clientData.groups); } public static ClientData setParentGroups(final ClientData clientData, final Collection parentGroups) { return new ClientData(clientData.accountNo, clientData.status, clientData.officeId, clientData.officeName, clientData.id, clientData.firstname, clientData.middlename, clientData.lastname, clientData.fullname, clientData.displayName, - clientData.externalId, clientData.activationDate, clientData.imageData, clientData.officeOptions, parentGroups); + clientData.externalId, clientData.activationDate, clientData.imageId, clientData.officeOptions, parentGroups); } public static ClientData clientIdentifier(final Long id, final String accountNo, final EnumOptionData status, final String firstname, @@ -88,14 +81,14 @@ final public class ClientData implements Comparable { public static ClientData instance(final String accountNo, final EnumOptionData status, final Long officeId, final String officeName, final Long id, final String firstname, final String middlename, final String lastname, final String fullname, - final String displayName, final String externalId, final LocalDate activationDate, final ImageData imageData) { + final String displayName, final String externalId, final LocalDate activationDate, final Long imageId) { return new ClientData(accountNo, status, officeId, officeName, id, firstname, middlename, lastname, fullname, displayName, - externalId, activationDate, imageData, null, null); + externalId, activationDate, imageId, null, null); } private ClientData(final String accountNo, final EnumOptionData status, final Long officeId, final String officeName, final Long id, final String firstname, final String middlename, final String lastname, final String fullname, final String displayName, - final String externalId, final LocalDate activationDate, final ImageData imageKey, final Collection allowedOffices, + final String externalId, final LocalDate activationDate, final Long imageId, final Collection allowedOffices, final Collection groups) { this.accountNo = accountNo; this.status = status; @@ -114,11 +107,11 @@ final public class ClientData implements Comparable { this.displayName = StringUtils.defaultIfEmpty(displayName, null); this.externalId = StringUtils.defaultIfEmpty(externalId, null); this.activationDate = activationDate; - this.imageData = imageKey; - if (imageKey != null) { + this.imageId = imageId; + if (imageId != null) { this.imagePresent = Boolean.TRUE; } else { - this.imagePresent = null; + this.imagePresent = Boolean.FALSE; } // associations @@ -144,17 +137,14 @@ final public class ClientData implements Comparable { return this.officeName; } - public String imageKey() { - return this.imageData.imageKey(); + public Long getImageId() { + return this.imageId; } - public boolean imageKeyDoesNotExist() { - return imageData.equals(null) && !imageKeyExists(); + public Boolean getImagePresent() { + return this.imagePresent; } - private boolean imageKeyExists() { - return StringUtils.isNotBlank(this.imageData.imageKey()); - } @Override public int compareTo(final ClientData obj) { if (obj == null) { return -1; } @@ -201,24 +191,4 @@ final public class ClientData implements Comparable { return this.activationDate; } - public void setImageData(ImageData imageData){ - this.imageData = imageData; - } - - public String imageName() { - return imageData.name(); - } - - public String imageContentType() { - return imageData.contentType(); - } - - public byte[] image(){ - try{ - return imageData.getContent(); - }catch (IOException ioe){ - logger.error("Error in fetching client image"); - return null; - } - } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/data/ImageData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/data/ImageData.java deleted file mode 100644 index 95ede014c..000000000 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/data/ImageData.java +++ /dev/null @@ -1,78 +0,0 @@ -package org.mifosplatform.portfolio.client.data; - -import org.apache.commons.lang.StringUtils; -import org.apache.poi.util.IOUtils; -import org.mifosplatform.infrastructure.core.service.DocumentStore; -import org.mifosplatform.infrastructure.core.service.DocumentStoreType; -import org.mifosplatform.infrastructure.core.service.ImageUtils; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; - -public class ImageData { - - private final Long clientId; - private final String key; - // private final DocumentStoreType storeType; - private final String storeType; - private File file; - private String contentType; - private String name; - private InputStream inputStream; - - - public ImageData(Long clientId, String key, String storeType) { - this.clientId = clientId; - this.key = key; - this.storeType = storeType; - } - - public String imageKey() { - return this.key; - } - - public byte[] getContent() throws IOException { - if(inputStream == null){ - FileInputStream fileInputStream = new FileInputStream(file); - return IOUtils.toByteArray(fileInputStream); - } - else - { - return IOUtils.toByteArray(inputStream); - } - } - - private String setImageContentType() { - String fileName = file.getName(); - String contentType = ImageUtils.IMAGE_MIME_TYPE.JPEG.getValue(); - if (StringUtils.endsWith(fileName, ImageUtils.IMAGE_FILE_EXTENSION.GIF.getValue())) { - contentType = ImageUtils.IMAGE_MIME_TYPE.GIF.getValue(); - } else if (StringUtils.endsWith(fileName, ImageUtils.IMAGE_FILE_EXTENSION.PNG.getValue())) { - contentType = ImageUtils.IMAGE_MIME_TYPE.PNG.getValue(); - } - return contentType; - } - - public String contentType(){ - return this.contentType; - } - - public DocumentStoreType storeType() { - return DocumentStore.getDocumentStoreType(storeType); - } - - public String name(){ - return this.file.getName(); - } - - - public void setContent(InputStream objectContent) { - this.inputStream = objectContent; - } - - public void setContent(File file) { - this.file = file; - } -} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/domain/Client.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/domain/Client.java index f9400bbd2..b8408c84a 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/domain/Client.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/domain/Client.java @@ -13,6 +13,7 @@ import org.mifosplatform.infrastructure.core.data.ApiParameterError; import org.mifosplatform.infrastructure.core.data.DataValidatorBuilder; import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException; import org.mifosplatform.infrastructure.core.service.DateUtils; +import org.mifosplatform.infrastructure.documentmanagement.domain.Image; import org.mifosplatform.infrastructure.security.service.RandomPasswordGenerator; import org.mifosplatform.organisation.office.domain.Office; import org.mifosplatform.portfolio.client.api.ClientApiConstants; @@ -20,6 +21,7 @@ import org.mifosplatform.portfolio.group.domain.Group; import org.springframework.data.jpa.domain.AbstractPersistable; import javax.persistence.*; + import java.util.*; @Entity @@ -33,6 +35,10 @@ public final class Client extends AbstractPersistable { @JoinColumn(name = "office_id", nullable = false) private Office office; + @OneToOne(optional = true) + @JoinColumn(name = "image_id", nullable = true) + private Image image; + /** * A value from {@link ClientStatus}. */ @@ -55,7 +61,6 @@ public final class Client extends AbstractPersistable { @Column(name = "fullname", length = 100) private String fullname; - @SuppressWarnings("unused") @Column(name = "display_name", length = 100, nullable = false) private String displayName; @@ -365,15 +370,24 @@ public final class Client extends AbstractPersistable { return this.office.identifiedBy(officeId); } -// public String imageKey() { -// } -// -// public void updateImageKey(final String imageKey) { -// this.image.setKey(imageKey); -// } - public Long officeId() { return this.office.getId(); } + public void setImage(Image image) { + this.image = image; + } + + public Image getImage() { + return this.image; + } + + public String getDisplayName() { + return this.displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/domain/Image.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/domain/Image.java deleted file mode 100644 index d186f5b26..000000000 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/domain/Image.java +++ /dev/null @@ -1,62 +0,0 @@ -/** - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. - */ -package org.mifosplatform.portfolio.client.domain; - -import org.mifosplatform.infrastructure.core.service.DocumentStoreType; -import org.springframework.data.jpa.domain.AbstractPersistable; - -import javax.persistence.*; - -@Entity -@Table(name = "m_image") -public final class Image extends AbstractPersistable { - - // @Column(name = "`client_id`") - @OneToOne() - @JoinColumn(name = "`client_id`") - private Client client; - - @Column(name = "`key`", length = 500) - private String key; - - @Column(name = "`storage_type`", length = 50) - private String storeType; - - public Image(Client client, String image_key, DocumentStoreType storeType) { - this.client = client; - this.key = image_key; - this.storeType = storeType.getValue(); - } - - public Image() { - - } - - - public Image(Client client) { - this.client = client; - this.key = null; - this.storeType = null; - } - - - public String getKey() { - return key; - } - - public void setKey(String key) { - this.key = key; - } - - public String updateKey(String key) { - setKey(key); - return this.key; - } - - public void updateStorageType(String documentStoreType) { - this.storeType = documentStoreType; - } -} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientReadPlatformServiceImpl.java index 3fb4b72a1..9b817efed 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientReadPlatformServiceImpl.java @@ -16,7 +16,6 @@ import org.joda.time.LocalDate; import org.mifosplatform.infrastructure.core.api.ApiParameterHelper; import org.mifosplatform.infrastructure.core.data.EnumOptionData; import org.mifosplatform.infrastructure.core.domain.JdbcSupport; -import org.mifosplatform.infrastructure.core.service.DocumentStoreFactory; import org.mifosplatform.infrastructure.core.service.Page; import org.mifosplatform.infrastructure.core.service.PaginationHelper; import org.mifosplatform.infrastructure.core.service.TenantAwareRoutingDataSource; @@ -26,7 +25,6 @@ import org.mifosplatform.organisation.office.service.OfficeReadPlatformService; import org.mifosplatform.portfolio.client.data.ClientAccountSummaryCollectionData; import org.mifosplatform.portfolio.client.data.ClientAccountSummaryData; import org.mifosplatform.portfolio.client.data.ClientData; -import org.mifosplatform.portfolio.client.data.ImageData; import org.mifosplatform.portfolio.client.domain.ClientEnumerations; import org.mifosplatform.portfolio.client.exception.ClientNotFoundException; import org.mifosplatform.portfolio.group.data.GroupGeneralData; @@ -43,11 +41,9 @@ import org.springframework.stereotype.Service; @Service public class ClientReadPlatformServiceImpl implements ClientReadPlatformService { - private final JdbcTemplate jdbcTemplate; private final PlatformSecurityContext context; private final OfficeReadPlatformService officeReadPlatformService; - private final DocumentStoreFactory documentStoreFactory; // data mappers private final PaginationHelper paginationHelper = new PaginationHelper(); @@ -58,10 +54,9 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService @Autowired public ClientReadPlatformServiceImpl(final PlatformSecurityContext context, final TenantAwareRoutingDataSource dataSource, - final OfficeReadPlatformService officeReadPlatformService, final DocumentStoreFactory documentStoreFactory) { + final OfficeReadPlatformService officeReadPlatformService) { this.context = context; this.officeReadPlatformService = officeReadPlatformService; - this.documentStoreFactory = documentStoreFactory; this.jdbcTemplate = new JdbcTemplate(dataSource); } @@ -178,12 +173,6 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService Collection parentGroups = this.jdbcTemplate.query(clientGroupsSql, this.clientGroupsMapper, new Object[] { clientId }); - String clientImageSql = "select * from m_image where client_id =" + clientId; - - try { - ImageData imageData = this.jdbcTemplate.queryForObject(clientImageSql, new ImageMapper()); - clientData.setImageData(documentStoreFactory.getInstanceFromStorageType(imageData.storeType()).retrieveImage(imageData)); - } catch (EmptyResultDataAccessException e) {} return ClientData.setParentGroups(clientData, parentGroups); } catch (EmptyResultDataAccessException e) { @@ -234,7 +223,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService sqlBuilder.append("c.office_id as officeId, o.name as officeName, "); sqlBuilder.append("c.firstname as firstname, c.middlename as middlename, c.lastname as lastname, "); sqlBuilder.append("c.fullname as fullname, c.display_name as displayName, "); - sqlBuilder.append("c.activation_date as activationDate, c.image_key as imagekey "); + sqlBuilder.append("c.activation_date as activationDate, c.image_id as imageId"); sqlBuilder.append("from m_client c "); sqlBuilder.append("join m_office o on o.id = c.office_id "); sqlBuilder.append("join m_group_client pgc on pgc.client_id = c.id"); @@ -262,11 +251,11 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService final String displayName = rs.getString("displayName"); final String externalId = rs.getString("externalId"); final LocalDate activationDate = JdbcSupport.getLocalDate(rs, "activationDate"); - final String imageKey = rs.getString("imageKey"); + final Long imageId = JdbcSupport.getLong(rs, "imageId"); final String officeName = rs.getString("officeName"); return ClientData.instance(accountNo, status, officeId, officeName, id, firstname, middlename, lastname, fullname, displayName, - externalId, activationDate, null); + externalId, activationDate, imageId); } } @@ -282,7 +271,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService builder.append("c.office_id as officeId, o.name as officeName, "); builder.append("c.firstname as firstname, c.middlename as middlename, c.lastname as lastname, "); builder.append("c.fullname as fullname, c.display_name as displayName, "); - builder.append("c.activation_date as activationDate, c.image_key as imagekey "); + builder.append("c.activation_date as activationDate, c.image_id as imageId "); builder.append("from m_client c "); builder.append("join m_office o on o.id = c.office_id "); @@ -310,13 +299,12 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService final String displayName = rs.getString("displayName"); final String externalId = rs.getString("externalId"); final LocalDate activationDate = JdbcSupport.getLocalDate(rs, "activationDate"); - // final String imageKey = rs.getString("imageKey"); final String officeName = rs.getString("officeName"); + final Long imageId = JdbcSupport.getLong(rs, "imageId"); return ClientData.instance(accountNo, status, officeId, officeName, id, firstname, middlename, lastname, fullname, displayName, - externalId, activationDate, null); + externalId, activationDate, imageId); } - } private static final class ParentGroupsMapper implements RowMapper { @@ -336,18 +324,6 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService } } - private static final class ImageMapper implements RowMapper { - - @Override - public ImageData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { - final Long clientId = rs.getLong("client_id"); - final String imageKey = rs.getString("key"); - final String storeType = rs.getString("storage_type"); - return new ImageData(clientId, imageKey, storeType); - } - - } - private static final class ClientLookupMapper implements RowMapper { private final String schema; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientWritePlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientWritePlatformService.java index 578b74c10..bdad04f22 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientWritePlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientWritePlatformService.java @@ -5,12 +5,8 @@ */ package org.mifosplatform.portfolio.client.service; -import java.io.InputStream; - import org.mifosplatform.infrastructure.core.api.JsonCommand; import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; -import org.mifosplatform.infrastructure.core.domain.Base64EncodedImage; -import org.springframework.security.access.prepost.PreAuthorize; public interface ClientWritePlatformService { @@ -22,12 +18,4 @@ public interface ClientWritePlatformService { CommandProcessingResult deleteClient(Long clientId); - @PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'CREATE_CLIENTIMAGE')") - CommandProcessingResult saveOrUpdateClientImage(Long clientId, String imageName, InputStream inputStream, Long fileSize); - - @PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'CREATE_CLIENTIMAGE')") - CommandProcessingResult saveOrUpdateClientImage(Long clientId, Base64EncodedImage encodedImage); - - @PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'DELETE_CLIENTIMAGE')") - CommandProcessingResult deleteClientImage(Long clientId); } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientWritePlatformServiceJpaRepositoryImpl.java index 3eb007ecb..52cf5ad4b 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientWritePlatformServiceJpaRepositoryImpl.java @@ -5,27 +5,28 @@ */ package org.mifosplatform.portfolio.client.service; -import org.apache.commons.lang.StringUtils; +import java.util.List; +import java.util.Locale; +import java.util.Map; + import org.joda.time.LocalDate; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import org.mifosplatform.infrastructure.core.api.JsonCommand; import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder; -import org.mifosplatform.infrastructure.core.domain.Base64EncodedImage; import org.mifosplatform.infrastructure.core.exception.PlatformDataIntegrityException; -import org.mifosplatform.infrastructure.core.service.DocumentStore; -import org.mifosplatform.infrastructure.core.service.DocumentStoreFactory; -import org.mifosplatform.infrastructure.documentmanagement.exception.DocumentManagementException; import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; import org.mifosplatform.organisation.office.domain.Office; import org.mifosplatform.organisation.office.domain.OfficeRepository; import org.mifosplatform.organisation.office.exception.OfficeNotFoundException; import org.mifosplatform.portfolio.client.api.ClientApiConstants; import org.mifosplatform.portfolio.client.data.ClientDataValidator; -import org.mifosplatform.portfolio.client.domain.*; +import org.mifosplatform.portfolio.client.domain.AccountNumberGenerator; +import org.mifosplatform.portfolio.client.domain.AccountNumberGeneratorFactory; +import org.mifosplatform.portfolio.client.domain.Client; +import org.mifosplatform.portfolio.client.domain.ClientRepositoryWrapper; import org.mifosplatform.portfolio.client.exception.ClientMustBePendingToBeDeletedException; -import org.mifosplatform.portfolio.client.exception.ClientNotFoundException; import org.mifosplatform.portfolio.group.domain.Group; import org.mifosplatform.portfolio.group.domain.GroupRepository; import org.mifosplatform.portfolio.group.exception.GroupNotFoundException; @@ -38,11 +39,6 @@ import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.io.InputStream; -import java.util.List; -import java.util.Locale; -import java.util.Map; - @Service public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWritePlatformService { @@ -55,14 +51,12 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP private final GroupRepository groupRepository; private final ClientDataValidator fromApiJsonDeserializer; private final AccountNumberGeneratorFactory accountIdentifierGeneratorFactory; - private final DocumentStoreFactory documentStoreFactory; - private final ImageRepository imageRepository; @Autowired public ClientWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context, - final ClientRepositoryWrapper clientRepository, final OfficeRepository officeRepository, final NoteRepository noteRepository, - final ClientDataValidator fromApiJsonDeserializer, final AccountNumberGeneratorFactory accountIdentifierGeneratorFactory, - final GroupRepository groupRepository, DocumentStoreFactory documentStoreFactory, ImageRepository imageRepository) { + final ClientRepositoryWrapper clientRepository, final OfficeRepository officeRepository, final NoteRepository noteRepository, + final ClientDataValidator fromApiJsonDeserializer, final AccountNumberGeneratorFactory accountIdentifierGeneratorFactory, + final GroupRepository groupRepository) { this.context = context; this.clientRepository = clientRepository; this.officeRepository = officeRepository; @@ -70,8 +64,6 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP this.fromApiJsonDeserializer = fromApiJsonDeserializer; this.accountIdentifierGeneratorFactory = accountIdentifierGeneratorFactory; this.groupRepository = groupRepository; - this.documentStoreFactory = documentStoreFactory; - this.imageRepository = imageRepository; } @Transactional @@ -221,77 +213,6 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP } } - @Transactional - @Override - public CommandProcessingResult saveOrUpdateClientImage(final Long clientId, final String imageName, final InputStream inputStream, Long fileSize) { - try { - final Client client = this.clientRepository.findOneWithNotFoundDetection(clientId); - deletePreviousClientImage(clientId, client); - - DocumentStore documentStore = this.documentStoreFactory.getInstanceFromConfiguration(); - String imageLocation = documentStore.saveImage(inputStream, clientId, imageName, fileSize); - return updateClientImage(clientId, client, imageLocation, documentStore.getType().getValue()); - } catch (DocumentManagementException dme) { - logger.error(dme.getMessage(), dme); - throw new DocumentManagementException(imageName); - } - } - - @Transactional - @Override - public CommandProcessingResult deleteClientImage(final Long clientId) { - - final Client client = this.clientRepository.findOneWithNotFoundDetection(clientId); - - Image image = imageRepository.findOneByClient(client); - String imageKey = image.getKey(); - // delete image from the file system - if (StringUtils.isNotEmpty((imageKey))) { - DocumentStore documentStore = this.documentStoreFactory.getInstanceFromConfiguration(); - documentStore.deleteImage(clientId, imageKey); - this.imageRepository.delete(image); - } - - return new CommandProcessingResult(clientId) ; - } - - @Override - public CommandProcessingResult saveOrUpdateClientImage(final Long clientId, final Base64EncodedImage encodedImage) { - try { - final Client client = this.clientRepository.findOneWithNotFoundDetection(clientId); - deletePreviousClientImage(clientId, client); - - DocumentStore documentStore = this.documentStoreFactory.getInstanceFromConfiguration(); - final String imageLocation = documentStore.saveImage(encodedImage, clientId, "image"); - - return updateClientImage(clientId, client, imageLocation, documentStore.getType().getValue()); - } catch (DocumentManagementException dme) { - logger.error(dme.getMessage(), dme); - throw new DocumentManagementException("image"); - } - } - - private void deletePreviousClientImage(final Long clientId, final Client client) { - if (client == null) { throw new ClientNotFoundException(clientId); } - - Image image = imageRepository.findOneByClient(client); - if (image != null){ - this.documentStoreFactory.getInstanceFromConfiguration().deleteImage(clientId, image.getKey()); - } - } - - private CommandProcessingResult updateClientImage(final Long clientId, final Client client, final String imageLocation, final String documentStoreType) { - Image image = imageRepository.findOneByClient(client); - if(image == null){ - image = new Image(client); - } - image.updateKey(imageLocation); - image.updateStorageType(documentStoreType); - - this.imageRepository.save(image); - return new CommandProcessingResult(clientId); - } - private void logAsErrorUnexpectedDataIntegrityException(final DataIntegrityViolationException dve) { logger.error(dve.getMessage(), dve); } diff --git a/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/DocumentStoreFactoryTest.java b/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/DocumentStoreFactoryTest.java index 8592df1f9..29a85f473 100644 --- a/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/DocumentStoreFactoryTest.java +++ b/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/DocumentStoreFactoryTest.java @@ -1,28 +1,28 @@ package org.mifosplatform.infrastructure.core.service; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import org.junit.Test; import org.junit.runner.RunWith; import org.mifosplatform.infrastructure.configuration.domain.ConfigurationDomainService; -import org.mifosplatform.infrastructure.configuration.domain.ExternalServicesDomainService; +import org.mifosplatform.infrastructure.configuration.service.ExternalServicesReadPlatformServiceImpl; +import org.mifosplatform.infrastructure.documentmanagement.contentrepository.ContentRepositoryFactory; +import org.mifosplatform.infrastructure.documentmanagement.contentrepository.FileSystemContentRepository; +import org.mifosplatform.infrastructure.documentmanagement.contentrepository.S3ContentRepository; import org.mockito.runners.MockitoJUnitRunner; import org.springframework.context.ApplicationContext; -import static junit.framework.Assert.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -@SuppressWarnings("unused") - @RunWith(MockitoJUnitRunner.class) public class DocumentStoreFactoryTest { - +/** @Test - public void shouldReturnS3DocumentStoreObjectIfS3Enabled(){ + public void shouldReturnS3DocumentStoreObjectIfS3Enabled() { S3ClientFactory s3ClientFactoryMock = mock(S3ClientFactory.class); when(s3ClientFactoryMock.getS3Client()).thenReturn(null); - ExternalServicesDomainService externalServicesDomainServiceMock = mock(ExternalServicesDomainService.class); + ExternalServicesReadPlatformServiceImpl externalServicesDomainServiceMock = mock(ExternalServicesReadPlatformServiceImpl.class); when(externalServicesDomainServiceMock.getValue("s3_bucket_name")).thenReturn("bucket_name"); ConfigurationDomainService configurationDomainServiceMock = mock(ConfigurationDomainService.class); @@ -30,26 +30,27 @@ public class DocumentStoreFactoryTest { ApplicationContext applicationContextMock = mock(ApplicationContext.class); - when(applicationContextMock.getBean("configurationDomainServiceJpa", ConfigurationDomainService.class)).thenReturn(configurationDomainServiceMock); + when(applicationContextMock.getBean("configurationDomainServiceJpa", ConfigurationDomainService.class)).thenReturn( + configurationDomainServiceMock); when(applicationContextMock.getBean("s3ClientFactory", S3ClientFactory.class)).thenReturn(s3ClientFactoryMock); - when(applicationContextMock.getBean("externalServicesDomainService", ExternalServicesDomainService.class)).thenReturn(externalServicesDomainServiceMock); + when(applicationContextMock.getBean("externalServicesDomainService", ExternalServicesReadPlatformServiceImpl.class)).thenReturn( + externalServicesDomainServiceMock); - - DocumentStoreFactory documentStoreFactory = new DocumentStoreFactory(applicationContextMock); - assertEquals(documentStoreFactory.getInstanceFromConfiguration().getClass(),S3DocumentStore.class); + ContentRepositoryFactory documentStoreFactory = new ContentRepositoryFactory(applicationContextMock); + assertEquals(documentStoreFactory.getInstanceFromConfiguration().getClass(), S3ContentRepository.class); } @Test - public void shouldReturnFileSystemDocumentStoreObjectIfS3IsDisabled(){ + public void shouldReturnFileSystemDocumentStoreObjectIfS3IsDisabled() { ConfigurationDomainService configurationDomainService = mock(ConfigurationDomainService.class); when(configurationDomainService.isAmazonS3Enabled()).thenReturn(false); ApplicationContext applicationContext = mock(ApplicationContext.class); - when(applicationContext.getBean("configurationDomainServiceJpa", ConfigurationDomainService.class)).thenReturn(configurationDomainService); + when(applicationContext.getBean("configurationDomainServiceJpa", ConfigurationDomainService.class)).thenReturn( + configurationDomainService); - DocumentStoreFactory documentStoreFactory = new DocumentStoreFactory(applicationContext); - assertEquals(documentStoreFactory.getInstanceFromConfiguration().getClass(),FileSystemDocumentStore.class); + ContentRepositoryFactory documentStoreFactory = new ContentRepositoryFactory(applicationContext); + assertEquals(documentStoreFactory.getInstanceFromConfiguration().getClass(), FileSystemContentRepository.class); } - - +**/ } diff --git a/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/FileDocumentStoreTest.java b/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/FileDocumentStoreTest.java index 2f70fe84c..22764aa94 100644 --- a/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/FileDocumentStoreTest.java +++ b/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/FileDocumentStoreTest.java @@ -1,23 +1,22 @@ package org.mifosplatform.infrastructure.core.service; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + import org.junit.Test; import org.junit.runner.RunWith; +import org.mifosplatform.infrastructure.documentmanagement.contentrepository.FileSystemContentRepository; import org.mifosplatform.infrastructure.documentmanagement.data.DocumentData; import org.mifosplatform.infrastructure.documentmanagement.data.FileData; import org.mockito.runners.MockitoJUnitRunner; -import static junit.framework.Assert.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -@SuppressWarnings("unused") - @RunWith(MockitoJUnitRunner.class) public class FileDocumentStoreTest { - +/** @Test - public void shouldRetrieveDocument(){ - FileSystemDocumentStore fileSystemDocumentStore = new FileSystemDocumentStore(); + public void shouldRetrieveDocument() { + FileSystemContentRepository fileSystemDocumentStore = new FileSystemContentRepository(); DocumentData documentDataMock = mock(DocumentData.class); when(documentDataMock.fileName()).thenReturn("some_file"); when(documentDataMock.fileName()).thenReturn("content_type"); @@ -28,4 +27,5 @@ public class FileDocumentStoreTest { assertEquals(fileData.name(), documentDataMock.fileName()); assertEquals(fileData.contentType(), documentDataMock.contentType()); } +**/ } diff --git a/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/S3DocumentStoreTest.java b/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/S3DocumentStoreTest.java index 3307d7d96..212f8c49a 100644 --- a/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/S3DocumentStoreTest.java +++ b/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/S3DocumentStoreTest.java @@ -1,27 +1,34 @@ package org.mifosplatform.infrastructure.core.service; -import com.amazonaws.AmazonClientException; -import com.amazonaws.AmazonServiceException; -import com.amazonaws.services.s3.AmazonS3Client; -import com.amazonaws.services.s3.model.*; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mifosplatform.infrastructure.documentmanagement.command.DocumentCommand; -import org.mifosplatform.infrastructure.documentmanagement.data.DocumentData; -import org.mifosplatform.infrastructure.documentmanagement.data.FileData; -import org.mifosplatform.infrastructure.documentmanagement.exception.DocumentManagementException; -import org.mifosplatform.infrastructure.documentmanagement.exception.DocumentNotFoundException; -import org.mockito.Matchers; -import org.mockito.runners.MockitoJUnitRunner; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import java.io.IOException; import java.io.InputStream; -import static junit.framework.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.*; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mifosplatform.infrastructure.documentmanagement.command.DocumentCommand; +import org.mifosplatform.infrastructure.documentmanagement.contentrepository.S3ContentRepository; +import org.mifosplatform.infrastructure.documentmanagement.data.DocumentData; +import org.mifosplatform.infrastructure.documentmanagement.data.FileData; +import org.mifosplatform.infrastructure.documentmanagement.exception.ContentManagementException; +import org.mifosplatform.infrastructure.documentmanagement.exception.DocumentNotFoundException; +import org.mockito.Matchers; +import org.mockito.runners.MockitoJUnitRunner; -@SuppressWarnings("unused") +import com.amazonaws.AmazonClientException; +import com.amazonaws.AmazonServiceException; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.DeleteObjectRequest; +import com.amazonaws.services.s3.model.GetObjectRequest; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.amazonaws.services.s3.model.PutObjectResult; +import com.amazonaws.services.s3.model.S3Object; @RunWith(MockitoJUnitRunner.class) public class S3DocumentStoreTest { @@ -30,17 +37,17 @@ public class S3DocumentStoreTest { AmazonS3Client s3ClientMock; DocumentCommand documentCommand; DocumentData documentDataMock; - +/** @Test - public void shouldGenerateParentDirectoryFromEntityDetails(){ - S3DocumentStore s3DocumentStore=new S3DocumentStore("bucketName", null); + public void shouldGenerateParentDirectoryFromEntityDetails() { + S3ContentRepository s3DocumentStore = new S3ContentRepository("bucketName", null); String actualParentDirectory = s3DocumentStore.generateFileParentDirectory("entity_type", 1234L); assertTrue(actualParentDirectory.startsWith("documents/entity_type/1234")); } @Test - public void shouldGenerateClientImageDirectoryFromResourceId(){ - S3DocumentStore s3DocumentStore=new S3DocumentStore("bucketName", null); + public void shouldGenerateClientImageDirectoryFromResourceId() { + S3ContentRepository s3DocumentStore = new S3ContentRepository("bucketName", null); String actualParentDirectory = s3DocumentStore.generateClientImageParentDirectory(1234L); assertTrue(actualParentDirectory.startsWith("images/clients/1234")); } @@ -48,20 +55,20 @@ public class S3DocumentStoreTest { @Test public void shouldSaveADocumentToS3() throws IOException { initUpload(); - when(s3ClientMock.putObject(Matchers.anyObject())).thenReturn(new PutObjectResult()); - S3DocumentStore s3DocumentStore = new S3DocumentStore("bucketName", s3ClientMock); + when(s3ClientMock.putObject(Matchers. anyObject())).thenReturn(new PutObjectResult()); + S3ContentRepository s3DocumentStore = new S3ContentRepository("bucketName", s3ClientMock); s3DocumentStore.saveDocument(inputStream, documentCommand); - verify(s3ClientMock, times(1)).putObject(Matchers.anyObject()); + verify(s3ClientMock, times(1)).putObject(Matchers. anyObject()); } @Test(expected = DocumentManagementException.class) public void shouldCatchAmazonServiceExceptionWhenUnableToUploadDocumentToS3() throws IOException { initUpload(); - when(s3ClientMock.putObject(Matchers.anyObject())).thenThrow(new AmazonServiceException("serviceException")); + when(s3ClientMock.putObject(Matchers. anyObject())).thenThrow(new AmazonServiceException("serviceException")); - S3DocumentStore s3DocumentStore = new S3DocumentStore("bucketName", s3ClientMock); + S3ContentRepository s3DocumentStore = new S3ContentRepository("bucketName", s3ClientMock); s3DocumentStore.saveDocument(inputStream, documentCommand); } @@ -69,9 +76,9 @@ public class S3DocumentStoreTest { @Test(expected = DocumentManagementException.class) public void shouldCatchAmazonClientExceptionWhenUnableToUploadDocumentToS3() throws IOException { initUpload(); - when(s3ClientMock.putObject(Matchers.anyObject())).thenThrow(new AmazonClientException("clientException")); + when(s3ClientMock.putObject(Matchers. anyObject())).thenThrow(new AmazonClientException("clientException")); - S3DocumentStore s3DocumentStore = new S3DocumentStore("bucketName", s3ClientMock); + S3ContentRepository s3DocumentStore = new S3ContentRepository("bucketName", s3ClientMock); s3DocumentStore.saveDocument(inputStream, documentCommand); } @@ -79,12 +86,12 @@ public class S3DocumentStoreTest { @Test public void shouldRetrieveDocumentFromS3() throws IOException { initRetrieve(); - when(s3ClientMock.getObject(Matchers.anyObject())).thenReturn(new S3Object()); - S3DocumentStore s3DocumentStore = new S3DocumentStore("bucketName",s3ClientMock); + when(s3ClientMock.getObject(Matchers. anyObject())).thenReturn(new S3Object()); + S3ContentRepository s3DocumentStore = new S3ContentRepository("bucketName", s3ClientMock); FileData fileData = s3DocumentStore.retrieveDocument(documentDataMock); - verify(s3ClientMock, times(1)).getObject(Matchers.anyObject()); + verify(s3ClientMock, times(1)).getObject(Matchers. anyObject()); assertEquals(fileData.name(), "name"); assertEquals(fileData.contentType(), "type"); } @@ -92,8 +99,8 @@ public class S3DocumentStoreTest { @Test(expected = DocumentNotFoundException.class) public void shouldCatchAmazonServiceExceptionWhenUnableToRetrieveDocumentFromS3() throws IOException { initRetrieve(); - when(s3ClientMock.getObject(Matchers.anyObject())).thenThrow(new AmazonServiceException("serviceException")); - S3DocumentStore s3DocumentStore = new S3DocumentStore("bucketName",s3ClientMock); + when(s3ClientMock.getObject(Matchers. anyObject())).thenThrow(new AmazonServiceException("serviceException")); + S3ContentRepository s3DocumentStore = new S3ContentRepository("bucketName", s3ClientMock); s3DocumentStore.retrieveDocument(documentDataMock); } @@ -101,8 +108,8 @@ public class S3DocumentStoreTest { @Test(expected = DocumentNotFoundException.class) public void shouldCatchAmazonClientExceptionWhenUnableToRetrieveDocumentFromS3() throws IOException { initRetrieve(); - when(s3ClientMock.getObject(Matchers.anyObject())).thenThrow(new AmazonClientException("clientException")); - S3DocumentStore s3DocumentStore = new S3DocumentStore("bucketName",s3ClientMock); + when(s3ClientMock.getObject(Matchers. anyObject())).thenThrow(new AmazonClientException("clientException")); + S3ContentRepository s3DocumentStore = new S3ContentRepository("bucketName", s3ClientMock); s3DocumentStore.retrieveDocument(documentDataMock); } @@ -110,11 +117,11 @@ public class S3DocumentStoreTest { @Test public void shouldDeleteImageFromS3() throws IOException { AmazonS3Client s3ClientMock = mock(AmazonS3Client.class); - S3DocumentStore s3DocumentStore = new S3DocumentStore("bucketName",s3ClientMock); + S3ContentRepository s3DocumentStore = new S3ContentRepository("bucketName", s3ClientMock); - s3DocumentStore.deleteImage(123L,"imagekey"); + s3DocumentStore.deleteImage(123L, "imagekey"); - verify(s3ClientMock, times(1)).deleteObject(Matchers.anyObject()); + verify(s3ClientMock, times(1)).deleteObject(Matchers. anyObject()); } private void initRetrieve() { @@ -126,10 +133,10 @@ public class S3DocumentStoreTest { when(documentDataMock.contentType()).thenReturn("type"); } - - private void initUpload(){ + private void initUpload() { inputStream = S3DocumentStoreTest.class.getResourceAsStream("DummyString"); s3ClientMock = mock(AmazonS3Client.class); - documentCommand = new DocumentCommand(null, -1l, "parentEntityType", 1l, null, "someFileName", 3l, null, null, null); + documentCommand = new DocumentCommand(null, -1l, "parentEntityType", 1l, null, "someFileName", 3l, null, null, null); } + **/ } From bd93ce67ea544316369f37b3df6c3104beb390ca Mon Sep 17 00:00:00 2001 From: Vishwas Babu AJ Date: Mon, 3 Jun 2013 12:01:52 +0530 Subject: [PATCH 19/49] adding amazon aws java sdk as dependency --- mifosng-provider/build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mifosng-provider/build.gradle b/mifosng-provider/build.gradle index 10cf889f3..3f036cd83 100644 --- a/mifosng-provider/build.gradle +++ b/mifosng-provider/build.gradle @@ -160,7 +160,8 @@ dependencies { [group: 'com.lowagie', name: 'itext-rtf', version: '2.1.7'], [group: 'org.mnode.ical4j', name: 'ical4j', version: '1.0.4'], [group: 'com.googlecode.flyway', name: 'flyway-maven-plugin', version: '2.1.1'], - [group: 'org.quartz-scheduler', name: 'quartz', version: '2.1.7'] + [group: 'org.quartz-scheduler', name: 'quartz', version: '2.1.7'], + [group: 'com.amazonaws', name: 'aws-java-sdk', version: '1.2.1'] ) testCompile 'junit:junit:4.11', From 0e4d0b4cfcb0b8f7df5230b55d939bd1fdb5b345 Mon Sep 17 00:00:00 2001 From: Vishwas Babu AJ Date: Mon, 3 Jun 2013 14:04:03 +0530 Subject: [PATCH 20/49] fix for MIFOSX-396 --- .../ProductToGLAccountMappingWritePlatformServiceImpl.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java index 2c6743231..def760091 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java @@ -187,7 +187,6 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc createLoanProductToGLAccountMapping(loanProductId, command); changes = loanProductToGLAccountMappingHelper.populateChangesForNewLoanProductToGLAccountMappingCreation(element, accountingRuleType); - loanProductToGLAccountMappingHelper.savePaymentChannelToFundSourceMappings(command, element, loanProductId, changes); }/*** else examine and update individual changes ***/ else { loanProductToGLAccountMappingHelper.handleChangesToLoanProductToGLAccountMappings(loanProductId, changes, element, @@ -218,7 +217,6 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc createSavingProductToGLAccountMapping(savingsProductId, command); changes = savingsProductToGLAccountMappingHelper.populateChangesForNewSavingsProductToGLAccountMappingCreation(element, accountingRuleType); - savingsProductToGLAccountMappingHelper.savePaymentChannelToFundSourceMappings(command, element, savingsProductId, changes); }/*** else examine and update individual changes ***/ else { savingsProductToGLAccountMappingHelper.handleChangesToSavingsProductToGLAccountMappings(savingsProductId, changes, element, From d858221e3a8bf8b50a8f146d85a1ee178811db3b Mon Sep 17 00:00:00 2001 From: Vishwas Babu AJ Date: Mon, 3 Jun 2013 14:13:07 +0530 Subject: [PATCH 21/49] minor issues in groups after s3 refactoring --- .../portfolio/client/service/ClientReadPlatformServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientReadPlatformServiceImpl.java index 9b817efed..a0f139038 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientReadPlatformServiceImpl.java @@ -223,7 +223,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService sqlBuilder.append("c.office_id as officeId, o.name as officeName, "); sqlBuilder.append("c.firstname as firstname, c.middlename as middlename, c.lastname as lastname, "); sqlBuilder.append("c.fullname as fullname, c.display_name as displayName, "); - sqlBuilder.append("c.activation_date as activationDate, c.image_id as imageId"); + sqlBuilder.append("c.activation_date as activationDate, c.image_id as imageId "); sqlBuilder.append("from m_client c "); sqlBuilder.append("join m_office o on o.id = c.office_id "); sqlBuilder.append("join m_group_client pgc on pgc.client_id = c.id"); From 5ce21c9b325ec80d1221f997da5fd0d6085a471b Mon Sep 17 00:00:00 2001 From: Vishwas Babu AJ Date: Mon, 3 Jun 2013 18:21:18 +0530 Subject: [PATCH 22/49] updating image migration stored proc to work in older versions of mysql --- .../core_db/V48__adding-S3-Support.sql | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/mifosng-db/migrations/core_db/V48__adding-S3-Support.sql b/mifosng-db/migrations/core_db/V48__adding-S3-Support.sql index 9fab76c5a..912598592 100755 --- a/mifosng-db/migrations/core_db/V48__adding-S3-Support.sql +++ b/mifosng-db/migrations/core_db/V48__adding-S3-Support.sql @@ -36,20 +36,28 @@ drop procedure if exists migrate_customer_image_data; delimiter # create procedure migrate_customer_image_data() begin -declare v_counter int unsigned default 0; -declare num_of_clients INT DEFAULT 0; -declare curr_client INT DEFAULT 0; -declare curr_image INT DEFAULT 0; -declare prev_image INT DEFAULT 0; +DECLARE v_counter INT DEFAULT 0; +DECLARE num_of_clients INT DEFAULT 0; +DECLARE curr_image INT DEFAULT 0; +DECLARE prev_image INT DEFAULT 0; SELECT COUNT(*) FROM m_client INTO num_of_clients; +SET @curr_client := 0; while v_counter < num_of_clients do - INSERT INTO m_image(`location`,`storage_type_enum`) select image_key, 1 FROM m_client where image_key is not null LIMIT v_counter,1; + SET @s = CONCAT('INSERT INTO m_image(`location`,`storage_type_enum`) select image_key, 1 FROM m_client where image_key is not null LIMIT ', v_counter , ', ', 1); + PREPARE stmt1 FROM @s; + EXECUTE stmt1; + DEALLOCATE PREPARE stmt1; + select ifnull(max(`id`),0) from m_image INTO curr_image; - select id FROM m_client where image_key is not null limit v_counter,1 INTO curr_client; - IF (prev_image != curr_image) THEN UPDATE m_client set image_id = curr_image where id=curr_client; + SET @z = CONCAT('select id INTO @curr_client FROM m_client where image_key is not null limit ', v_counter , ', ', 1); + PREPARE stmt2 FROM @z; + EXECUTE stmt2; + + IF (prev_image != curr_image) THEN UPDATE m_client set image_id = curr_image where id=@curr_client; END IF; + set prev_image=curr_image; - set v_counter=v_counter+1; + set v_counter=v_counter+1; end while; delete from m_image where `location` is null; end# From 16996b5e8c0d728dd842e51e69d126a4d01eabf8 Mon Sep 17 00:00:00 2001 From: Lech Rozanski Date: Tue, 4 Jun 2013 14:48:41 +0200 Subject: [PATCH 23/49] MIFOSX-365: Simplify 'Data Tables' functionality --- api-docs/apiLive.htm | 265 ++++++++++++++++-- ...tatableCommandFromApiJsonDeserializer.java | 129 ++++----- .../data/ResultsetColumnHeaderData.java | 5 +- .../ReadWriteNonCoreDataServiceImpl.java | 200 ++++++++----- 4 files changed, 454 insertions(+), 145 deletions(-) diff --git a/api-docs/apiLive.htm b/api-docs/apiLive.htm index 1d55f8b51..a6608b6cc 100644 --- a/api-docs/apiLive.htm +++ b/api-docs/apiLive.htm @@ -1063,11 +1063,19 @@ Data Table datatables - + Create Data Table List Data Tables + + + datatables/{datatable} + + Retrieve Data Table Details + Update Data Table + Delete Data Table + datatables/register/{datatable}/{apptable} @@ -11276,6 +11284,57 @@ No Request Body: +   +
+
+

Create Data Table

+

Create a new data table and registers it with the Mifos X Core application + table.

+
+
+ +POST https://DomainName/api/v1/datatables + + +POST https://DomainName/api/v1/datatables + +Content-Type: application/json +Request Body: +{ + "datatableName": "extra_client_details", + "apptableName": "m_client", + "columns": [ + { + "name": "Gender", + "type": "Dropdown", + "code": "Gender" + }, + { + "name": "Some Decimal", + "type": "Decimal", + "mandatory": true + }, + { + "name": "Birth Date", + "type": "Date" + }, + { + "name": "Question 2", + "type": "String", + "length": 100, + "mandatory": false + } + ] +} + + +{ + "resourceIdentifier": "extra_client_details" +} + +
+
  @@ -11303,25 +11362,191 @@ No Request Body: GET https://DomainName/api/v1/datatables - + [ - { - "applicationTableName": "m_client", - "registeredTableName": "Additional Client Fields Data" - }, - { - "applicationTableName": "m_client", - "registeredTableName": "Extra Family Details Data" - }, - { - "applicationTableName": "m_loan", - "registeredTableName": "Additional Loan Fields Data" - } -] + { + "applicationTableName": "m_client", + "registeredTableName": "extra_client_details", + "columnHeaderData": [ + { + "columnName": "client_id", + "columnType": "bigint", + "columnLength": 0, + "columnDisplayType": "INTEGER", + "isColumnNullable": false, + "isColumnPrimaryKey": true, + "columnValues": [] + }, + { + "columnName": "Gender_cd_Question", + "columnType": "int", + "columnLength": 0, + "columnDisplayType": "INTEGER", + "isColumnNullable": true, + "isColumnPrimaryKey": false, + "columnValues": [] + }, + { + "columnName": "Some Decimal", + "columnType": "decimal", + "columnLength": 0, + "columnDisplayType": "DECIMAL", + "isColumnNullable": true, + "isColumnPrimaryKey": false, + "columnValues": [] + }, + { + "columnName": "Birth Date", + "columnType": "date", + "columnLength": 0, + "columnDisplayType": "DATE", + "isColumnNullable": true, + "isColumnPrimaryKey": false, + "columnValues": [] + } + ] + } +] +   +
+
+

Retrieve Data Table Details

+

Lists a registered data table details and the Mifos X Core + application table they are registered to.

+
+
+ +GET https://DomainName/api/v1/datatables/{datatable} + + +{ + "applicationTableName": "m_client", + "registeredTableName": "extra_client_details", + "columnHeaderData": [ + { + "columnName": "client_id", + "columnType": "bigint", + "columnLength": 0, + "columnDisplayType": "INTEGER", + "isColumnNullable": false, + "isColumnPrimaryKey": true, + "columnValues": [] + }, + { + "columnName": "Gender_cd_Question", + "columnType": "int", + "columnLength": 0, + "columnDisplayType": "INTEGER", + "isColumnNullable": true, + "isColumnPrimaryKey": false, + "columnValues": [] + }, + { + "columnName": "Some Decimal", + "columnType": "decimal", + "columnLength": 0, + "columnDisplayType": "DECIMAL", + "isColumnNullable": true, + "isColumnPrimaryKey": false, + "columnValues": [] + }, + { + "columnName": "Birth Date", + "columnType": "date", + "columnLength": 0, + "columnDisplayType": "DATE", + "isColumnNullable": true, + "isColumnPrimaryKey": false, + "columnValues": [] + } + ] +} + +
+
+ +   +
+
+

Update Data Table

+

Modifies fields of a data table. If the apptableName parameter is passed, + data table is deregistered and registered with the new application table.

+
+
+ +PUT https://DomainName/api/v1/datatables/{datatables} + + +PUT https://DomainName/api/v1/datatables/extra_client_details + +Content-Type: application/json +Request Body: +{ + "apptableName": "m_client", + "columns": [ + { + "name": "Gender", + "type": "Dropdown", + "code": "Gender" + }, + { + "name": "Some Decimal", + "type": "Decimal", + "mandatory": true + }, + { + "name": "Birth Date", + "type": "Date" + }, + { + "name": "Question 2", + "type": "String", + "length": 100, + "mandatory": false + } + ] +} + + +{ + "resourceIdentifier": "extra_client_details" +} + +
+
+ +   +
+
+

Delete Data Table

+

Deletes a data table and unregisters it from the Mifos X Core + application table.

+
+
+ +DELETE https://DomainName/api/v1/datatables/{datatables} + + +DELETE https://DomainName/api/v1/datatables/extra_client_details + +Content-Type: application/json +No Request Body + + +{ + "resourceIdentifier": "extra_client_details" +} + +
+ +
 
@@ -15357,11 +15582,19 @@ GET https://DomainName/api/v1/search?query=000000111 Data Table datatables - + Create Data Table List Data Tables + + + datatables/{datatable} + + Retrieve Data Table Details + Update Data Table + Delete Data Table + datatables/register/{datatable}/{apptable} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/DatatableCommandFromApiJsonDeserializer.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/DatatableCommandFromApiJsonDeserializer.java index db1f74a08..1a214ca61 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/DatatableCommandFromApiJsonDeserializer.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/DatatableCommandFromApiJsonDeserializer.java @@ -18,6 +18,7 @@ import org.mifosplatform.infrastructure.core.data.ApiParameterError; import org.mifosplatform.infrastructure.core.data.DataValidatorBuilder; import org.mifosplatform.infrastructure.core.exception.InvalidJsonException; import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException; +import org.mifosplatform.infrastructure.core.exception.PlatformDataIntegrityException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -29,22 +30,23 @@ import com.google.gson.reflect.TypeToken; public class DatatableCommandFromApiJsonDeserializer { private final static String DATATABLE_NAME_REGEX_PATTERN = "^[a-zA-Z][a-zA-Z0-9\\-_\\s]{0,48}[a-zA-Z0-9]$"; + private final static String DATATABLE_COLUMN_NAME_REGEX_PATTERN = "^[a-zA-Z][a-zA-Z0-9\\-_\\s]{0,}[a-zA-Z0-9]$"; /** * The parameters supported for this command. */ private final Set supportedParametersForCreate = new HashSet(Arrays.asList( "datatableName", "apptableName", "multiRow", "columns")); private final Set supportedParametersForCreateColumns = new HashSet(Arrays.asList( - "name", "type", "length", "mandatory")); + "name", "type", "length", "mandatory", "code")); private final Set supportedParametersForUpdate = new HashSet(Arrays.asList( "apptableName", "changeColumns", "addColumns", "dropColumns")); private final Set supportedParametersForAddColumns = new HashSet(Arrays.asList( - "name", "type", "length", "mandatory", "after")); + "name", "type", "length", "mandatory", "after", "code")); private final Set supportedParametersForChangeColumns = new HashSet(Arrays.asList( - "name", "newName", "type", "length", "mandatory", "after")); + "name", "newName", "mandatory", "after", "code", "newCode")); private final Set supportedParametersForDropColumns = new HashSet(Arrays.asList( "name")); - private final Object[] supportedColumnTypes = { "String", "Number", "Decimal", "Date", "Text" }; + private final Object[] supportedColumnTypes = { "String", "Number", "Decimal", "Date", "Text", "Dropdown" }; private final Object[] supportedApptableNames = { "m_loan", "m_savings_account", "m_client", "m_group", "m_office" }; private final FromJsonHelper fromApiJsonHelper; @@ -54,6 +56,35 @@ public class DatatableCommandFromApiJsonDeserializer { this.fromApiJsonHelper = fromApiJsonHelper; } + private void validateType(final DataValidatorBuilder baseDataValidator, final JsonElement column) { + final String type = fromApiJsonHelper.extractStringNamed("type", column); + baseDataValidator.reset().parameter("type").value(type).notBlank() + .isOneOfTheseValues(supportedColumnTypes); + + final Integer length = fromApiJsonHelper.extractIntegerSansLocaleNamed("length", column); + if (type != null && type.equals("String")) { + if (length != null) { + baseDataValidator.reset().parameter("length").value(length).notBlank() + .zeroOrPositiveAmount(); + } else { + baseDataValidator.reset().parameter("length").value(length) + .cantBeBlankWhenParameterProvidedIs("type", type); + } + } else { + baseDataValidator.reset().parameter("length").value(length) + .mustBeBlankWhenParameterProvidedIs("type", type); + } + + final String code = fromApiJsonHelper.extractStringNamed("code", column); + + if (type != null && type.equals("Dropdown")) { + baseDataValidator.reset().parameter("code").value(code).notBlank() + .matchesRegularExpression(DATATABLE_NAME_REGEX_PATTERN); + } else { + baseDataValidator.reset().parameter("code").value(code).mustBeBlankWhenParameterProvided("type", type); + } + } + public void validateForCreate(final String json) { if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); } @@ -83,28 +114,16 @@ public class DatatableCommandFromApiJsonDeserializer { if (columns != null) { for (JsonElement column : columns) { fromApiJsonHelper.checkForUnsupportedParameters(column.getAsJsonObject(), supportedParametersForCreateColumns); - + final String name = fromApiJsonHelper.extractStringNamed("name", column); - baseDataValidator.reset().parameter("name").value(name).notBlank().notExceedingLengthOf(50) - .isNotOneOfTheseValues("id", fkColumnName).matchesRegularExpression(DATATABLE_NAME_REGEX_PATTERN); - + baseDataValidator.reset().parameter("name").value(name).notBlank() + .isNotOneOfTheseValues("id", fkColumnName).matchesRegularExpression(DATATABLE_COLUMN_NAME_REGEX_PATTERN); + final String type = fromApiJsonHelper.extractStringNamed("type", column); baseDataValidator.reset().parameter("type").value(type).notBlank().isOneOfTheseValues(supportedColumnTypes); - - final Integer length = fromApiJsonHelper.extractIntegerSansLocaleNamed("length", column); - if (type != null && (type.equals("String") || type.equals("Text"))) { - if (length != null) { - baseDataValidator.reset().parameter("length").value(length).notBlank() - .zeroOrPositiveAmount(); - } else if (!type.equals("Text")) { - baseDataValidator.reset().parameter("length").value(length) - .cantBeBlankWhenParameterProvidedIs("type", type); - } - } else if (type != null && length != null) { - baseDataValidator.reset().parameter("length").value(length) - .mustBeBlankWhenParameterProvidedIs("type", type); - } - + + validateType(baseDataValidator, column); + final Boolean mandatory = fromApiJsonHelper.extractBooleanNamed("mandatory", column); baseDataValidator.reset().parameter("mandatory").value(mandatory).ignoreIfNull().notBlank() .isOneOfTheseValues(true, false); @@ -118,7 +137,10 @@ public class DatatableCommandFromApiJsonDeserializer { if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); } // Because all parameters are optional, a check to see if at least one parameter // has been specified is necessary in order to avoid JSON requests with no parameters - if (!json.matches("(?s)\\A\\{.*?(\\\".*?\\\"\\s*?:\\s*?)+.*?\\}\\z")) { throw new InvalidJsonException(); } + if (!json.matches("(?s)\\A\\{.*?(\\\".*?\\\"\\s*?:\\s*?)+.*?\\}\\z")) { + throw new PlatformDataIntegrityException("error.msg.invalid.request.body.no.parameters", + "Provided JSON request body does not have any parameters."); + } final Type typeOfMap = new TypeToken>() {}.getType(); fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, supportedParametersForUpdate); @@ -129,7 +151,7 @@ public class DatatableCommandFromApiJsonDeserializer { final JsonElement element = fromApiJsonHelper.parse(json); final String apptableName = fromApiJsonHelper.extractStringNamed("apptableName", element); baseDataValidator.reset().parameter("apptableName").value(apptableName).ignoreIfNull().notBlank() - .notExceedingLengthOf(50).isOneOfTheseValues(supportedApptableNames); + .isOneOfTheseValues(supportedApptableNames); final String fkColumnName = (apptableName != null) ? apptableName.substring(2) + "_id" : ""; final JsonArray changeColumns = fromApiJsonHelper.extractJsonArrayNamed("changeColumns", element); @@ -140,36 +162,31 @@ public class DatatableCommandFromApiJsonDeserializer { fromApiJsonHelper.checkForUnsupportedParameters(column.getAsJsonObject(), supportedParametersForChangeColumns); final String name = fromApiJsonHelper.extractStringNamed("name", column); - baseDataValidator.reset().parameter("name").value(name).notBlank().notExceedingLengthOf(50) - .isNotOneOfTheseValues("id", fkColumnName).matchesRegularExpression(DATATABLE_NAME_REGEX_PATTERN); + baseDataValidator.reset().parameter("name").value(name).notBlank() + .isNotOneOfTheseValues("id", fkColumnName).matchesRegularExpression(DATATABLE_COLUMN_NAME_REGEX_PATTERN); final String newName = fromApiJsonHelper.extractStringNamed("newName", column); baseDataValidator.reset().parameter("newName").value(newName).ignoreIfNull().notBlank() .notExceedingLengthOf(50).isNotOneOfTheseValues("id", fkColumnName) .matchesRegularExpression(DATATABLE_NAME_REGEX_PATTERN); - final String type = fromApiJsonHelper.extractStringNamed("type", column); - baseDataValidator.reset().parameter("type").value(type).notBlank() - .isOneOfTheseValues(supportedColumnTypes); + final String code = fromApiJsonHelper.extractStringNamed("code", column); + baseDataValidator.reset().parameter("code").value(code).ignoreIfNull().notBlank() + .notExceedingLengthOf(100).matchesRegularExpression(DATATABLE_COLUMN_NAME_REGEX_PATTERN); - final Integer length = fromApiJsonHelper.extractIntegerSansLocaleNamed("length", column); - if (type != null && (type.equals("String") || type.equals("Text"))) { - if (length != null) { - baseDataValidator.reset().parameter("length").value(length).notBlank() - .zeroOrPositiveAmount(); - } else if (!type.equals("Text")) { - baseDataValidator.reset().parameter("length").value(length) - .cantBeBlankWhenParameterProvidedIs("type", type); - } - } else if (type != null && length != null) { - baseDataValidator.reset().parameter("length").value(length) - .mustBeBlankWhenParameterProvidedIs("type", type); + final String newCode = fromApiJsonHelper.extractStringNamed("newCode", column); + baseDataValidator.reset().parameter("newCode").value(newCode).ignoreIfNull().notBlank() + .notExceedingLengthOf(100).matchesRegularExpression(DATATABLE_COLUMN_NAME_REGEX_PATTERN); + + if (StringUtils.isBlank(code) && StringUtils.isNotBlank(newCode)) { + baseDataValidator.reset().parameter("code").value(code) + .cantBeBlankWhenParameterProvidedIs("newCode", newCode); } final Boolean mandatory = fromApiJsonHelper.extractBooleanNamed("mandatory", column); baseDataValidator.reset().parameter("mandatory").value(mandatory).ignoreIfNull().notBlank() .isOneOfTheseValues(true, false); - + final Boolean after = fromApiJsonHelper.extractBooleanNamed("after", column); baseDataValidator.reset().parameter("after").value(after).ignoreIfNull().notBlank() .isOneOfTheseValues(true, false); @@ -184,26 +201,10 @@ public class DatatableCommandFromApiJsonDeserializer { fromApiJsonHelper.checkForUnsupportedParameters(column.getAsJsonObject(), supportedParametersForAddColumns); final String name = fromApiJsonHelper.extractStringNamed("name", column); - baseDataValidator.reset().parameter("name").value(name).notBlank().notExceedingLengthOf(50) - .isNotOneOfTheseValues("id", fkColumnName).matchesRegularExpression(DATATABLE_NAME_REGEX_PATTERN); + baseDataValidator.reset().parameter("name").value(name).notBlank() + .isNotOneOfTheseValues("id", fkColumnName).matchesRegularExpression(DATATABLE_COLUMN_NAME_REGEX_PATTERN); - final String type = fromApiJsonHelper.extractStringNamed("type", column); - baseDataValidator.reset().parameter("type").value(type).notBlank() - .isOneOfTheseValues(supportedColumnTypes); - - final Integer length = fromApiJsonHelper.extractIntegerSansLocaleNamed("length", column); - if (type != null && (type.equals("String") || type.equals("Text"))) { - if (length != null) { - baseDataValidator.reset().parameter("length").value(length).notBlank() - .zeroOrPositiveAmount(); - } else if (!type.equals("Text")) { - baseDataValidator.reset().parameter("length").value(length) - .cantBeBlankWhenParameterProvidedIs("type", type); - } - } else if (type != null && length != null) { - baseDataValidator.reset().parameter("length").value(length) - .mustBeBlankWhenParameterProvidedIs("type", type); - } + validateType(baseDataValidator, column); final Boolean mandatory = fromApiJsonHelper.extractBooleanNamed("mandatory", column); baseDataValidator.reset().parameter("mandatory").value(mandatory).ignoreIfNull().notBlank() @@ -223,8 +224,8 @@ public class DatatableCommandFromApiJsonDeserializer { fromApiJsonHelper.checkForUnsupportedParameters(column.getAsJsonObject(), supportedParametersForDropColumns); final String name = fromApiJsonHelper.extractStringNamed("name", column); - baseDataValidator.reset().parameter("name").value(name).notBlank().notExceedingLengthOf(50) - .isNotOneOfTheseValues("id", fkColumnName).matchesRegularExpression(DATATABLE_NAME_REGEX_PATTERN); + baseDataValidator.reset().parameter("name").value(name).notBlank() + .isNotOneOfTheseValues("id", fkColumnName).matchesRegularExpression(DATATABLE_COLUMN_NAME_REGEX_PATTERN); } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/data/ResultsetColumnHeaderData.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/data/ResultsetColumnHeaderData.java index fd394c644..c13e69d3c 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/data/ResultsetColumnHeaderData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/data/ResultsetColumnHeaderData.java @@ -17,7 +17,6 @@ public final class ResultsetColumnHeaderData { private final String columnName; private final String columnType; - @SuppressWarnings("unused") private final Long columnLength; private final String columnDisplayType; private final boolean isColumnNullable; @@ -161,6 +160,10 @@ public final class ResultsetColumnHeaderData { public String getColumnType() { return this.columnType; } + + public Long getColumnLength() { + return this.columnLength; + } public String getColumnDisplayType() { return this.columnDisplayType; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java index 4b6ebd737..a46f1b5bf 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java @@ -62,7 +62,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ private final static Logger logger = LoggerFactory.getLogger(ReadWriteNonCoreDataServiceImpl.class); private final static HashMap apiTypeToMySQL = new HashMap() {{ put("String", "VARCHAR"); put("Number", "INT"); put("Decimal", "DECIMAL"); put("Date", "DATE"); - put("Text", "TEXT"); + put("Text", "TEXT"); put ("Dropdown", "INT"); }}; private final JdbcTemplate jdbcTemplate; @@ -292,22 +292,34 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ } } + private String datatableColumnNameToCodeValueName(final String columnName, final String code) { + + return (code + "_cd_" + columnName); + } + private void parseDatatableColumnObjectForCreate(final JsonObject column, StringBuilder sqlBuilder) { String name = (column.has("name")) ? column.get("name").getAsString() : null; String type = (column.has("type")) ? column.get("type").getAsString() : null; Integer length = (column.has("length")) ? column.get("length").getAsInt() : null; Boolean mandatory = (column.has("mandatory")) ? column.get("mandatory").getAsBoolean() : false; - + String code = (column.has("code")) ? column.get("code").getAsString() : null; + + if (StringUtils.isNotBlank(code)) { + name = datatableColumnNameToCodeValueName(name, code); + } + String mysqlType = apiTypeToMySQL.get(type); sqlBuilder = sqlBuilder.append("`" + name + "` " + mysqlType); - + if (type != null) { if (type.equals("String")) { sqlBuilder = sqlBuilder.append("(" + length + ")"); } else if (type.equals("Decimal")) { sqlBuilder = sqlBuilder.append("(19,6)"); + } else if (type.equals("Dropdown")) { + sqlBuilder = sqlBuilder.append("(11)"); } } if (mandatory != null) { @@ -317,7 +329,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ sqlBuilder = sqlBuilder.append(" DEFAULT NULL"); } } - + sqlBuilder = sqlBuilder.append(", "); } @@ -326,29 +338,29 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ public CommandProcessingResult createDatatable(final JsonCommand command) { String datatableName = null; - + try { context.authenticatedUser(); this.fromApiJsonDeserializer.validateForCreate(command.json()); - + JsonElement element = this.fromJsonHelper.parse(command.json()); JsonArray columns = this.fromJsonHelper.extractJsonArrayNamed("columns", element); datatableName = this.fromJsonHelper.extractStringNamed("datatableName", element); String apptableName = this.fromJsonHelper.extractStringNamed("apptableName", element); Boolean multiRow = this.fromJsonHelper.extractBooleanNamed("multiRow", element); - + if (multiRow == null) { multiRow = false; } - + validateDatatableName(datatableName); validateAppTable(apptableName); - + String fkColumnName = apptableName.substring(2) + "_id"; String fkName = datatableName.toLowerCase().replaceAll("\\s", "_"); StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder = sqlBuilder.append("CREATE TABLE `" + datatableName + "` ("); - + if (multiRow) { sqlBuilder = sqlBuilder .append("`id` BIGINT(20) NOT NULL AUTO_INCREMENT, ") @@ -357,14 +369,14 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ sqlBuilder = sqlBuilder .append("`" + fkColumnName + "` BIGINT(20) NOT NULL, "); } - + for (JsonElement column : columns) { parseDatatableColumnObjectForCreate(column.getAsJsonObject(), sqlBuilder); } - + // Remove trailing comma and space sqlBuilder = sqlBuilder.delete(sqlBuilder.length() - 2, sqlBuilder.length()); - + if (multiRow) { sqlBuilder = sqlBuilder .append(", PRIMARY KEY (`id`)") @@ -372,7 +384,6 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ .append(", CONSTRAINT `fk_" + fkName + "` ") .append("FOREIGN KEY (`" + fkColumnName + "`) ") .append("REFERENCES `" + apptableName + "` (`id`)"); - } else { sqlBuilder = sqlBuilder .append(", PRIMARY KEY (`" + fkColumnName + "`)") @@ -380,10 +391,10 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ .append("FOREIGN KEY (`" + fkColumnName + "`) ") .append("REFERENCES `" + apptableName + "` (`id`)"); } - + sqlBuilder = sqlBuilder.append(") ENGINE=InnoDB DEFAULT CHARSET=utf8;"); this.jdbcTemplate.execute(sqlBuilder.toString()); - + registerDatatable(datatableName, apptableName); } catch (SQLGrammarException e) { Throwable realCause = e.getCause(); @@ -400,25 +411,43 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ .build(); } - private void parseDatatableColumnForUpdate(final JsonObject column, StringBuilder sqlBuilder) { - + private void parseDatatableColumnForUpdate(final JsonObject column, + final Map mapColumnNameDefinition, StringBuilder sqlBuilder) { + String name = (column.has("name")) ? column.get("name").getAsString() : null; String newName = (column.has("newName")) ? column.get("newName").getAsString() : name; - String type = (column.has("type")) ? column.get("type").getAsString() : null; - Integer length = (column.has("length")) ? column.get("length").getAsInt() : null; Boolean mandatory = (column.has("mandatory")) ? column.get("mandatory").getAsBoolean() : false; String after = (column.has("after")) ? column.get("after").getAsString() : null; - - String mysqlType = apiTypeToMySQL.get(type); - sqlBuilder = sqlBuilder.append(", CHANGE `" + name + "` `" + newName + "` " + mysqlType); - - if (type != null) { - if (type.equals("String") && length != null) { - sqlBuilder = sqlBuilder.append("(" + length + ")"); - } else if (type.equals("Decimal")) { - sqlBuilder = sqlBuilder.append("(19,6)"); + String code = (column.has("code")) ? column.get("code").getAsString() : null; + String newCode = (column.has("newCode")) ? column.get("newCode").getAsString() : null; + + if (StringUtils.isNotBlank(code)) { + name = datatableColumnNameToCodeValueName(name, code); + + if (StringUtils.isNotBlank(newCode)) { + newName = datatableColumnNameToCodeValueName(newName, newCode); + } else { + newName = datatableColumnNameToCodeValueName(newName, code); } } + + if (!mapColumnNameDefinition.containsKey(name)) { + throw new PlatformDataIntegrityException("error.msg.datatable.column.missing.update.parse", + "Column " + name + " does not exist.", name); + } + + String type = mapColumnNameDefinition.get(name).getColumnType(); + Long length = mapColumnNameDefinition.get(name).getColumnLength(); + + sqlBuilder = sqlBuilder.append(", CHANGE `" + name + "` `" + newName + "` " + type); + if (length != null && length > 0) { + if (type.equals("DECIMAL")) { + sqlBuilder.append("(19,6)"); + } else if (type.equals("VARCHAR")) { + sqlBuilder.append("(" + length + ")"); + } + } + if (mandatory != null) { if (mandatory) { sqlBuilder = sqlBuilder.append(" NOT NULL"); @@ -432,21 +461,28 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ } private void parseDatatableColumnForAdd(final JsonObject column, StringBuilder sqlBuilder) { - + String name = (column.has("name")) ? column.get("name").getAsString() : null; String type = (column.has("type")) ? column.get("type").getAsString() : null; Integer length = (column.has("length")) ? column.get("length").getAsInt() : null; Boolean mandatory = (column.has("mandatory")) ? column.get("mandatory").getAsBoolean() : false; String after = (column.has("after")) ? column.get("after").getAsString() : null; - + String code = (column.has("code")) ? column.get("code").getAsString() : null; + + if (StringUtils.isNotBlank(code)) { + name = datatableColumnNameToCodeValueName(name, code); + } + String mysqlType = apiTypeToMySQL.get(type); sqlBuilder = sqlBuilder.append(", ADD `" + name + "` " + mysqlType); - + if (type != null) { if (type.equals("String") && length != null) { sqlBuilder = sqlBuilder.append("(" + length + ")"); } else if (type.equals("Decimal")) { sqlBuilder = sqlBuilder.append("(19,6)"); + } else if (type.equals("Dropdown")) { + sqlBuilder = sqlBuilder.append("(11)"); } } if (mandatory != null) { @@ -462,9 +498,9 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ } private void parseDatatableColumnForDrop(final JsonObject column, StringBuilder sqlBuilder) { - + String name = (column.has("name")) ? column.get("name").getAsString() : null; - + sqlBuilder = sqlBuilder.append(", DROP COLUMN `" + name + "`"); } @@ -475,59 +511,95 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ try { context.authenticatedUser(); this.fromApiJsonDeserializer.validateForUpdate(command.json()); - + JsonElement element = this.fromJsonHelper.parse(command.json()); JsonArray changeColumns = this.fromJsonHelper.extractJsonArrayNamed("changeColumns", element); JsonArray addColumns = this.fromJsonHelper.extractJsonArrayNamed("addColumns", element); JsonArray dropColumns = this.fromJsonHelper.extractJsonArrayNamed("dropColumns", element); String apptableName = this.fromJsonHelper.extractStringNamed("apptableName", element); - + validateDatatableName(datatableName); - + + List columnHeaderData = this.genericDataService.fillResultsetColumnHeaders(datatableName); + Map mapColumnNameDefinition = new HashMap(); + for (ResultsetColumnHeaderData columnHeader : columnHeaderData) { + mapColumnNameDefinition.put(columnHeader.getColumnName(), columnHeader); + } + if (!StringUtils.isBlank(apptableName)) { validateAppTable(apptableName); - + String oldApptableName = this.queryForApplicationTableName(datatableName); if (!StringUtils.equals(oldApptableName, apptableName)) { deregisterDatatable(datatableName); registerDatatable(datatableName, apptableName); } } - + if (changeColumns == null && addColumns == null && dropColumns == null) { return; } - - StringBuilder sqlBuilder = new StringBuilder("ALTER TABLE `" + datatableName + "`"); - - if (changeColumns != null) { - for (JsonElement column : changeColumns) { - parseDatatableColumnForUpdate(column.getAsJsonObject(), sqlBuilder); - } - } - if (addColumns != null) { - for (JsonElement column : addColumns) { - parseDatatableColumnForAdd(column.getAsJsonObject(), sqlBuilder); - } - } + if (dropColumns != null) { + + StringBuilder sqlBuilder = new StringBuilder("ALTER TABLE `" + datatableName + "`"); + for (JsonElement column : dropColumns) { parseDatatableColumnForDrop(column.getAsJsonObject(), sqlBuilder); } + + // Remove the first comma, right after ALTER TABLE `datatable` + int indexOfFirstComma = sqlBuilder.indexOf(","); + if (indexOfFirstComma != -1) { + sqlBuilder = sqlBuilder.deleteCharAt(indexOfFirstComma); + } + + this.jdbcTemplate.execute(sqlBuilder.toString()); } - - // Remove the first comma, right after ALTER TABLE `datatable` - int indexOfFirstComma = sqlBuilder.indexOf(","); - if (indexOfFirstComma != -1) { - sqlBuilder = sqlBuilder.deleteCharAt(indexOfFirstComma); + if (addColumns != null) { + + StringBuilder sqlBuilder = new StringBuilder("ALTER TABLE `" + datatableName + "`"); + + for (JsonElement column : addColumns) { + parseDatatableColumnForAdd(column.getAsJsonObject(), sqlBuilder); + } + + // Remove the first comma, right after ALTER TABLE `datatable` + int indexOfFirstComma = sqlBuilder.indexOf(","); + if (indexOfFirstComma != -1) { + sqlBuilder = sqlBuilder.deleteCharAt(indexOfFirstComma); + } + + this.jdbcTemplate.execute(sqlBuilder.toString()); + } + if (changeColumns != null) { + + StringBuilder sqlBuilder = new StringBuilder("ALTER TABLE `" + datatableName + "`"); + + for (JsonElement column : changeColumns) { + parseDatatableColumnForUpdate(column.getAsJsonObject(), mapColumnNameDefinition, sqlBuilder); + } + + // Remove the first comma, right after ALTER TABLE `datatable` + int indexOfFirstComma = sqlBuilder.indexOf(","); + if (indexOfFirstComma != -1) { + sqlBuilder = sqlBuilder.deleteCharAt(indexOfFirstComma); + } + + this.jdbcTemplate.execute(sqlBuilder.toString()); } - this.jdbcTemplate.execute(sqlBuilder.toString()); } catch (SQLGrammarException e) { Throwable realCause = e.getCause(); - if (realCause.getMessage().contains("Unknown column")) { + if (realCause.getMessage().toLowerCase().contains("unknown column")) { throw new PlatformDataIntegrityException( - "error.msg.datatable.column.missing", realCause.getMessage()); + "error.msg.datatable.column.missing.update", realCause.getMessage()); + } else if (realCause.getMessage().toLowerCase().contains("can't drop")) { + throw new PlatformDataIntegrityException( + "error.msg.datatable.column.missing.drop", realCause.getMessage()); + } else if (realCause.getMessage().toLowerCase().contains("duplicate column")) { + throw new PlatformDataIntegrityException( + "error.msg.datatable.duplicate.column", realCause.getMessage()); } } } @@ -535,18 +607,18 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ @Transactional @Override public void deleteDatatable(final String datatableName) { - + try { context.authenticatedUser(); - + validateDatatableName(datatableName); deregisterDatatable(datatableName); - + String sql = "DROP TABLE `" + datatableName + "`"; this.jdbcTemplate.execute(sql); } catch (SQLGrammarException e) { Throwable realCause = e.getCause(); - + if (realCause.getMessage().contains("Unknown table")) { throw new PlatformDataIntegrityException( "error.msg.datatable.missing", realCause.getMessage()); From 86a9badb56e61b8a5ec78417d29ed7d0aed2135a Mon Sep 17 00:00:00 2001 From: Keith Woodlock Date: Tue, 4 Jun 2013 14:13:03 +0100 Subject: [PATCH 24/49] remove unused import warnings and unused variable warnings. --- .../loans/LoanApplicationTestBuilder.java | 6 +++--- .../rule/domain/AccountingTagRule.java | 2 ++ ...ingRuleCommandFromApiJsonDeserializer.java | 14 ++++++------- .../group/data/GroupAccountSummaryData.java | 3 ++- .../savings/domain/SavingsAccount.java | 15 ++++++------- .../service/DocumentStoreFactoryTest.java | 11 ---------- .../core/service/FileDocumentStoreTest.java | 8 ------- .../core/service/S3DocumentStoreTest.java | 21 ------------------- 8 files changed, 21 insertions(+), 59 deletions(-) diff --git a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanApplicationTestBuilder.java b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanApplicationTestBuilder.java index fbb1d8bee..b1b0de92a 100644 --- a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanApplicationTestBuilder.java +++ b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanApplicationTestBuilder.java @@ -31,7 +31,7 @@ public class LoanApplicationTestBuilder { private final String transactionProcessingID = MIFOS_STANDARD_STRATEGY; private String expectedDisbursmentDate = ""; private String submittedOnDate = ""; - private String loanType = "individual"; //default it to individual + private String loanType = "individual"; public String build(final String clientID, final String loanProductId) { @@ -42,7 +42,7 @@ public class LoanApplicationTestBuilder { map.put("productId", loanProductId); map.put("principal", principal); map.put("loanTermFrequency", loanTermFrequency); - map.put("loanTermFrequencyType", loanTermFrequencyType); // FIXME - Should this be using + map.put("loanTermFrequencyType", loanTermFrequencyType); map.put("numberOfRepayments", numberOfRepayment); map.put("repaymentEvery", repaymentPeriod); map.put("repaymentFrequencyType", repaymentFrequencyType); @@ -53,7 +53,7 @@ public class LoanApplicationTestBuilder { map.put("transactionProcessingStrategyId", transactionProcessingID); map.put("expectedDisbursementDate", expectedDisbursmentDate); map.put("submittedOnDate", submittedOnDate); - map.put("loanType", "individual"); + map.put("loanType", loanType); return new Gson().toJson(map); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/domain/AccountingTagRule.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/domain/AccountingTagRule.java index cdc845b28..18bc38349 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/domain/AccountingTagRule.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/domain/AccountingTagRule.java @@ -18,10 +18,12 @@ import org.springframework.data.jpa.domain.AbstractPersistable; @Table(name = "acc_rule_tags") public class AccountingTagRule extends AbstractPersistable { + @SuppressWarnings("unused") @ManyToOne @JoinColumn(name = "acc_rule_id", nullable = false) private AccountingRule accountingRule; + @SuppressWarnings("unused") @ManyToOne @JoinColumn(name = "tag_id", nullable = false) private CodeValue tagId; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/serialization/AccountingRuleCommandFromApiJsonDeserializer.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/serialization/AccountingRuleCommandFromApiJsonDeserializer.java index d74429434..fb1101b94 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/serialization/AccountingRuleCommandFromApiJsonDeserializer.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/serialization/AccountingRuleCommandFromApiJsonDeserializer.java @@ -14,19 +14,13 @@ import org.mifosplatform.accounting.rule.api.AccountingRuleJsonInputParams; import org.mifosplatform.accounting.rule.command.AccountingRuleCommand; import org.mifosplatform.infrastructure.core.exception.InvalidJsonException; import org.mifosplatform.infrastructure.core.serialization.AbstractFromApiJsonDeserializer; -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 AccountingRuleCommandFromApiJsonDeserializer extends AbstractFromApiJsonDeserializer { @@ -55,8 +49,12 @@ public final class AccountingRuleCommandFromApiJsonDeserializer extends Abstract element); final String name = this.fromApiJsonHelper.extractStringNamed(AccountingRuleJsonInputParams.NAME.getValue(), element); final String description = this.fromApiJsonHelper.extractStringNamed(AccountingRuleJsonInputParams.DESCRIPTION.getValue(), element); - final String[] creditTags = this.fromApiJsonHelper.extractArrayNamed(AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue(), element); - final String[] debitTags = this.fromApiJsonHelper.extractArrayNamed(AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue(), element); + + // final String[] creditTags = + this.fromApiJsonHelper.extractArrayNamed(AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue(), element); + // final String[] debitTags = + this.fromApiJsonHelper.extractArrayNamed(AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue(), element); + return new AccountingRuleCommand(id, officeId, accountToDebitId, accountToCreditId, name, description); } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/group/data/GroupAccountSummaryData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/group/data/GroupAccountSummaryData.java index 18493e3c7..d02c746fd 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/group/data/GroupAccountSummaryData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/group/data/GroupAccountSummaryData.java @@ -15,6 +15,7 @@ public class GroupAccountSummaryData { private final Long productId; private final String productName; private final Integer accountStatusId; + @SuppressWarnings("unused") private final String accountNo; public GroupAccountSummaryData(final Long id, final String externalId, final Long productId, final String productName, @@ -46,4 +47,4 @@ public class GroupAccountSummaryData { public Integer getAccountStatusId() { return this.accountStatusId; } -} +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccount.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccount.java index 15b487e54..cb0af6e76 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccount.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccount.java @@ -163,6 +163,7 @@ public class SavingsAccount extends AbstractPersistable { @Column(name = "annual_fee_on_day", nullable = true) private Integer annualFeeOnDay; + @SuppressWarnings("unused") @Temporal(TemporalType.DATE) @Column(name = "annual_fee_next_due_date", nullable = true) private Date annualFeeNextDueDate; @@ -276,8 +277,8 @@ public class SavingsAccount extends AbstractPersistable { return SavingsAccountStatusType.fromInt(this.status).isActive(); } - public void activate(final DateTimeFormatter formatter, final LocalDate activationDate, List existingTransactionIds, - List existingReversedTransactionIds) { + public void activate(final DateTimeFormatter formatter, final LocalDate activationDate, final List existingTransactionIds, + final List existingReversedTransactionIds) { if (isActive()) { final String defaultUserMessage = "Cannot activate account. Account is already active."; @@ -352,8 +353,8 @@ public class SavingsAccount extends AbstractPersistable { return lockedInUntilLocalDate; } - public void postInterest(final LocalDate interestPostingUpToDate, List existingTransactionIds, - List existingReversedTransactionIds) { + public void postInterest(final LocalDate interestPostingUpToDate, final List existingTransactionIds, + final List existingReversedTransactionIds) { final SavingsInterestPostingPeriodType postingPeriodType = SavingsInterestPostingPeriodType.fromInt(this.interestPostingPeriodType); final List postingLocalDates = determineInterestPostingDates(getActivationLocalDate(), interestPostingUpToDate, @@ -756,7 +757,7 @@ public class SavingsAccount extends AbstractPersistable { } public SavingsAccountTransaction deposit(final DateTimeFormatter formatter, final LocalDate transactionDate, - final BigDecimal transactionAmount, List existingTransactionIds, List existingReversedTransactionIds, + final BigDecimal transactionAmount, final List existingTransactionIds, final List existingReversedTransactionIds, final PaymentDetail paymentDetail) { if (isNotActive()) { @@ -818,7 +819,7 @@ public class SavingsAccount extends AbstractPersistable { } public SavingsAccountTransaction withdraw(final DateTimeFormatter formatter, final LocalDate transactionDate, - final BigDecimal transactionAmount, List existingTransactionIds, List existingReversedTransactionIds, + final BigDecimal transactionAmount, final List existingTransactionIds, final List existingReversedTransactionIds, final PaymentDetail paymentDetail) { if (isNotActive()) { @@ -915,7 +916,7 @@ public class SavingsAccount extends AbstractPersistable { } public SavingsAccountTransaction addAnnualFee(final DateTimeFormatter formatter, final LocalDate transactionDate, - List existingTransactionIds, List existingReversedTransactionIds) { + final List existingTransactionIds, final List existingReversedTransactionIds) { if (isNotActive()) { diff --git a/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/DocumentStoreFactoryTest.java b/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/DocumentStoreFactoryTest.java index 29a85f473..fd54df1db 100644 --- a/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/DocumentStoreFactoryTest.java +++ b/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/DocumentStoreFactoryTest.java @@ -1,18 +1,7 @@ package org.mifosplatform.infrastructure.core.service; -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import org.junit.Test; import org.junit.runner.RunWith; -import org.mifosplatform.infrastructure.configuration.domain.ConfigurationDomainService; -import org.mifosplatform.infrastructure.configuration.service.ExternalServicesReadPlatformServiceImpl; -import org.mifosplatform.infrastructure.documentmanagement.contentrepository.ContentRepositoryFactory; -import org.mifosplatform.infrastructure.documentmanagement.contentrepository.FileSystemContentRepository; -import org.mifosplatform.infrastructure.documentmanagement.contentrepository.S3ContentRepository; import org.mockito.runners.MockitoJUnitRunner; -import org.springframework.context.ApplicationContext; @RunWith(MockitoJUnitRunner.class) public class DocumentStoreFactoryTest { diff --git a/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/FileDocumentStoreTest.java b/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/FileDocumentStoreTest.java index 22764aa94..37c8a367a 100644 --- a/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/FileDocumentStoreTest.java +++ b/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/FileDocumentStoreTest.java @@ -1,14 +1,6 @@ package org.mifosplatform.infrastructure.core.service; -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import org.junit.Test; import org.junit.runner.RunWith; -import org.mifosplatform.infrastructure.documentmanagement.contentrepository.FileSystemContentRepository; -import org.mifosplatform.infrastructure.documentmanagement.data.DocumentData; -import org.mifosplatform.infrastructure.documentmanagement.data.FileData; import org.mockito.runners.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) diff --git a/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/S3DocumentStoreTest.java b/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/S3DocumentStoreTest.java index 212f8c49a..8b6e4ab53 100644 --- a/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/S3DocumentStoreTest.java +++ b/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/S3DocumentStoreTest.java @@ -1,34 +1,13 @@ package org.mifosplatform.infrastructure.core.service; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.io.IOException; import java.io.InputStream; -import org.junit.Test; import org.junit.runner.RunWith; import org.mifosplatform.infrastructure.documentmanagement.command.DocumentCommand; -import org.mifosplatform.infrastructure.documentmanagement.contentrepository.S3ContentRepository; import org.mifosplatform.infrastructure.documentmanagement.data.DocumentData; -import org.mifosplatform.infrastructure.documentmanagement.data.FileData; -import org.mifosplatform.infrastructure.documentmanagement.exception.ContentManagementException; -import org.mifosplatform.infrastructure.documentmanagement.exception.DocumentNotFoundException; -import org.mockito.Matchers; import org.mockito.runners.MockitoJUnitRunner; -import com.amazonaws.AmazonClientException; -import com.amazonaws.AmazonServiceException; import com.amazonaws.services.s3.AmazonS3Client; -import com.amazonaws.services.s3.model.DeleteObjectRequest; -import com.amazonaws.services.s3.model.GetObjectRequest; -import com.amazonaws.services.s3.model.PutObjectRequest; -import com.amazonaws.services.s3.model.PutObjectResult; -import com.amazonaws.services.s3.model.S3Object; @RunWith(MockitoJUnitRunner.class) public class S3DocumentStoreTest { From c62ee07d455189fdaa5d7313773e56a29d2ff9d8 Mon Sep 17 00:00:00 2001 From: Vishwas Babu AJ Date: Wed, 5 Jun 2013 14:22:19 +0530 Subject: [PATCH 25/49] tracking transactions that repay loan charges --- ...track-loan-charge-payment-transactions.sql | 14 ++++ ...RepaymentScheduleTransactionProcessor.java | 25 ++++-- .../portfolio/loanaccount/domain/Loan.java | 82 +++++++++++-------- .../loanaccount/domain/LoanCharge.java | 7 +- .../loanaccount/domain/LoanChargePaidBy.java | 62 ++++++++++++++ .../domain/LoanChargePaidByRepository.java | 13 +++ .../loanaccount/domain/LoanTransaction.java | 22 +++++ ...WritePlatformServiceJpaRepositoryImpl.java | 1 + 8 files changed, 183 insertions(+), 43 deletions(-) create mode 100755 mifosng-db/migrations/core_db/V49__track-loan-charge-payment-transactions.sql create mode 100755 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanChargePaidBy.java create mode 100755 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanChargePaidByRepository.java diff --git a/mifosng-db/migrations/core_db/V49__track-loan-charge-payment-transactions.sql b/mifosng-db/migrations/core_db/V49__track-loan-charge-payment-transactions.sql new file mode 100755 index 000000000..206e0f909 --- /dev/null +++ b/mifosng-db/migrations/core_db/V49__track-loan-charge-payment-transactions.sql @@ -0,0 +1,14 @@ +CREATE TABLE `m_loan_charge_paid_by` ( + `id` BIGINT(20) NOT NULL AUTO_INCREMENT, + `loan_transaction_id` BIGINT(20) NOT NULL, + `loan_charge_id` BIGINT(20) NOT NULL, + `amount` DECIMAL(19,6) NOT NULL, + PRIMARY KEY (`id`), + INDEX `FK__m_loan_transaction` (`loan_transaction_id`), + INDEX `FK__m_loan_charge` (`loan_charge_id`), + CONSTRAINT `FK__m_loan_charge` FOREIGN KEY (`loan_charge_id`) REFERENCES `m_loan_charge` (`id`), + CONSTRAINT `FK__m_loan_transaction` FOREIGN KEY (`loan_transaction_id`) REFERENCES `m_loan_transaction` (`id`) +) +COLLATE='utf8_general_ci' +ENGINE=InnoDB; + diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/AbstractLoanRepaymentScheduleTransactionProcessor.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/AbstractLoanRepaymentScheduleTransactionProcessor.java index 7412242bc..a9d4fea52 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/AbstractLoanRepaymentScheduleTransactionProcessor.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/AbstractLoanRepaymentScheduleTransactionProcessor.java @@ -133,12 +133,12 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen if (loanTransaction.isNotWaiver()) { Money feeCharges = loanTransaction.getFeeChargesPortion(currency); if (feeCharges.isGreaterThanZero()) { - updateFeeChargesPaidAmountBy(feeCharges, charges); + updateFeeChargesPaidAmountBy(loanTransaction, feeCharges, charges); } Money penaltyCharges = loanTransaction.getPenaltyChargesPortion(currency); if (penaltyCharges.isGreaterThanZero()) { - updatePenaltyChargesPaidAmountBy(penaltyCharges, charges); + updatePenaltyChargesPaidAmountBy(loanTransaction, penaltyCharges, charges); } } @@ -147,20 +147,27 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen } } - private void updateFeeChargesPaidAmountBy(final Money feeCharges, final Set charges) { + private void updateFeeChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money feeCharges, final Set charges) { Money amountRemaining = feeCharges; for (LoanCharge loanCharge : charges) { if (!loanCharge.isDueAtDisbursement()) { if (loanCharge.isFeeCharge() && loanCharge.isNotFullyPaid() && amountRemaining.isGreaterThanZero()) { final LoanCharge unpaidCharge = findEarliestUnpaidChargeFromUnOrderedSet(charges); - amountRemaining = unpaidCharge.updatePaidAmountBy(amountRemaining); + Money amountPaidTowardsCharge = unpaidCharge.updatePaidAmountBy(amountRemaining); + if (!amountPaidTowardsCharge.isZero()) { + LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(loanTransaction, loanCharge, + amountPaidTowardsCharge.getAmount()); + loanTransaction.getLoanChargesPaid().add(loanChargePaidBy); + amountRemaining = amountRemaining.minus(amountPaidTowardsCharge); + } } } } } - private void updatePenaltyChargesPaidAmountBy(final Money feeCharges, final Set charges) { + private void updatePenaltyChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money feeCharges, + final Set charges) { Money amountRemaining = feeCharges; for (LoanCharge loanCharge : charges) { @@ -168,7 +175,13 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen if (loanCharge.isPenaltyCharge() && amountRemaining.isGreaterThanZero()) { final LoanCharge unpaidCharge = findEarliestUnpaidChargeFromUnOrderedSet(charges); - amountRemaining = unpaidCharge.updatePaidAmountBy(amountRemaining); + Money amountPaidTowardsCharge = unpaidCharge.updatePaidAmountBy(amountRemaining); + if (!amountPaidTowardsCharge.isZero()) { + LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(loanTransaction, loanCharge, + amountPaidTowardsCharge.getAmount()); + loanTransaction.getLoanChargesPaid().add(loanChargePaidBy); + amountRemaining = amountRemaining.minus(amountPaidTowardsCharge); + } } } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java index 19a2ec498..b6df12500 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java @@ -101,7 +101,7 @@ public class Loan extends AbstractPersistable { @SuppressWarnings("unused") @Column(name = "loan_type_enum", nullable = false) private Integer loanType; - + @ManyToOne @JoinColumn(name = "product_id", nullable = false) private LoanProduct loanProduct; @@ -264,8 +264,8 @@ public class Loan extends AbstractPersistable { @Transient private LoanSummaryWrapper loanSummaryWrapper; - 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, + 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, final LoanSchedule loanSchedule, final Set loanCharges, final Set collateral) { final LoanStatus status = null; @@ -304,8 +304,8 @@ public class Loan extends AbstractPersistable { this.client = null; } - private Loan(final String accountNo, final Client client, final Group group, final Integer loanType, final Fund fund, final Staff loanOfficer, - final CodeValue loanPurpose, final LoanTransactionProcessingStrategy transactionProcessingStrategy, + private Loan(final String accountNo, final Client client, final Group group, final Integer loanType, final Fund fund, + final Staff loanOfficer, final CodeValue loanPurpose, final LoanTransactionProcessingStrategy transactionProcessingStrategy, final LoanProduct loanProduct, final LoanProductRelatedDetail loanRepaymentScheduleDetail, final LoanStatus loanStatus, final Set loanCharges, final Set collateral) { if (StringUtils.isBlank(accountNo)) { @@ -677,10 +677,10 @@ public class Loan extends AbstractPersistable { return this.client; } - public LoanProduct loanProduct(){ + public LoanProduct loanProduct() { return this.loanProduct; } - + public LoanProductRelatedDetail repaymentScheduleDetail() { return this.loanRepaymentScheduleDetail; } @@ -822,16 +822,18 @@ public class Loan extends AbstractPersistable { this.externalId = StringUtils.defaultIfEmpty(newValue, null); } - //add clientId, groupId and loanType changes to actual changes - //FIXME: AA - We may require separate api command to move loan from one client to another + // add clientId, groupId and loanType changes to actual changes + // FIXME: AA - We may require separate api command to move loan from one + // client to another final String clientIdParamName = "clientId"; final Long clientId = this.client == null ? null : this.client.getId(); if (command.isChangeInLongParameterNamed(clientIdParamName, clientId)) { final Long newValue = command.longValueOfParameterNamed(clientIdParamName); actualChanges.put(clientIdParamName, newValue); } - - //FIXME: AA - We may require separate api command to move loan from one group to another + + // FIXME: AA - We may require separate api command to move loan from one + // group to another final String groupIdParamName = "groupId"; final Long groupId = this.group == null ? null : this.group.getId(); if (command.isChangeInLongParameterNamed(groupIdParamName, groupId)) { @@ -942,17 +944,19 @@ public class Loan extends AbstractPersistable { final String errorMessage = "The date on which a loan is submitted cannot be in the future."; throw new InvalidLoanStateTransitionException("submittal", "cannot.be.a.future.date", errorMessage, getSubmittedOnDate()); } - - if (!(client == null)){ - if (getSubmittedOnDate().isBefore(client.getActivationLocalDate())) { - final String errorMessage = "The date on which a loan is submitted cannot be earlier than client's activation date."; - throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.client.activation.date", errorMessage, getSubmittedOnDate()); - } - }else if (!(group == null)){ - if (getSubmittedOnDate().isBefore(group.getActivationLocalDate())) { - final String errorMessage = "The date on which a loan is submitted cannot be earlier than groups's activation date."; - throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.group.activation.date", errorMessage, getSubmittedOnDate()); - } + + if (!(client == null)) { + if (getSubmittedOnDate().isBefore(client.getActivationLocalDate())) { + final String errorMessage = "The date on which a loan is submitted cannot be earlier than client's activation date."; + throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.client.activation.date", errorMessage, + getSubmittedOnDate()); + } + } else if (!(group == null)) { + if (getSubmittedOnDate().isBefore(group.getActivationLocalDate())) { + final String errorMessage = "The date on which a loan is submitted cannot be earlier than groups's activation date."; + throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.group.activation.date", errorMessage, + getSubmittedOnDate()); + } } if (getSubmittedOnDate().isAfter(getExpectedDisbursedOnLocalDate())) { @@ -986,7 +990,7 @@ public class Loan extends AbstractPersistable { actualChanges.put(collateralParamName, listOfLoanCollateralData(possiblyModifedLoanCollateralItems)); } } - + return actualChanges; } @@ -1081,16 +1085,18 @@ public class Loan extends AbstractPersistable { throw new InvalidLoanStateTransitionException("submittal", "cannot.be.a.future.date", errorMessage, submittedOn); } - if (!(client == null)){ - if (submittedOn.isBefore(client.getActivationLocalDate())) { - final String errorMessage = "The date on which a loan is submitted cannot be earlier than client's activation date."; - throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.client.activation.date", errorMessage, submittedOn); - } - }else if (!(group == null)){ - if (submittedOn.isBefore(group.getActivationLocalDate())) { - final String errorMessage = "The date on which a loan is submitted cannot be earlier than groups's activation date."; - throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.group.activation.date", errorMessage, submittedOn); - } + if (!(client == null)) { + if (submittedOn.isBefore(client.getActivationLocalDate())) { + final String errorMessage = "The date on which a loan is submitted cannot be earlier than client's activation date."; + throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.client.activation.date", errorMessage, + submittedOn); + } + } else if (!(group == null)) { + if (submittedOn.isBefore(group.getActivationLocalDate())) { + final String errorMessage = "The date on which a loan is submitted cannot be earlier than groups's activation date."; + throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.group.activation.date", errorMessage, + submittedOn); + } } if (submittedOn.isAfter(getExpectedDisbursedOnLocalDate())) { @@ -1405,6 +1411,9 @@ public class Loan extends AbstractPersistable { for (LoanCharge charge : setOfLoanCharges()) { if (charge.isDueAtDisbursement()) { charge.markAsFullyPaid(); + // Add "Loan Charge Paid By" details to this transaction + LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(disbursementTransaction, charge, charge.amount()); + disbursementTransaction.getLoanChargesPaid().add(loanChargePaidBy); } else { handleChargeAppliedTransaction(charge, disbursedOn); } @@ -1419,14 +1428,15 @@ public class Loan extends AbstractPersistable { } if (disbursedOn.isAfter(new LocalDate())) { - final String errorMessage = "The date on which a loan with identifier : " + this.accountNumber + " is disbursed cannot be in the future."; + final String errorMessage = "The date on which a loan with identifier : " + this.accountNumber + + " is disbursed cannot be in the future."; throw new InvalidLoanStateTransitionException("disbursal", "cannot.be.a.future.date", errorMessage, disbursedOn); } LocalDate firstRepaymentDueDate = this.repaymentScheduleInstallments.get(0).getDueDate(); if (disbursedOn.isAfter(firstRepaymentDueDate)) { - final String errorMessage = "The date on which a loan with identifier : " + this.accountNumber + " is disbursed cannot be after the first expected repayment date: " - + firstRepaymentDueDate.toString(); + final String errorMessage = "The date on which a loan with identifier : " + this.accountNumber + + " is disbursed cannot be after the first expected repayment date: " + firstRepaymentDueDate.toString(); throw new InvalidLoanStateTransitionException("disbursal", "cannot.be.after.first.repayment.due.date", errorMessage, this.accountNumber, disbursedOn, firstRepaymentDueDate); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanCharge.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanCharge.java index 1ebc1d960..a1a8ff9e7 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanCharge.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanCharge.java @@ -449,6 +449,11 @@ public class LoanCharge extends AbstractPersistable { return Money.of(currency, this.amountWrittenOff); } + /** + * @param incrementBy + * Amount used to pay off this charge + * @return Actual amount paid on this charge + */ public Money updatePaidAmountBy(final Money incrementBy) { Money amountPaidToDate = Money.of(incrementBy.getCurrency(), this.amountPaid); @@ -471,7 +476,7 @@ public class LoanCharge extends AbstractPersistable { this.paid = determineIfFullyPaid(); - return incrementBy.minus(amountPaidOnThisCharge); + return amountPaidOnThisCharge; } public String name() { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanChargePaidBy.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanChargePaidBy.java new file mode 100755 index 000000000..c2bd409b8 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanChargePaidBy.java @@ -0,0 +1,62 @@ +package org.mifosplatform.portfolio.loanaccount.domain; + +import java.math.BigDecimal; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +import org.springframework.data.jpa.domain.AbstractPersistable; + +@Entity +@Table(name = "m_loan_charge_paid_by") +public class LoanChargePaidBy extends AbstractPersistable { + + @ManyToOne + @JoinColumn(name = "loan_transaction_id", nullable = false) + private LoanTransaction loanTransaction; + + @ManyToOne + @JoinColumn(name = "loan_charge_id", nullable = false) + private LoanCharge loanCharge; + + @Column(name = "amount", scale = 6, precision = 19, nullable = false) + private BigDecimal amount; + + protected LoanChargePaidBy() { + + } + + public LoanChargePaidBy(LoanTransaction loanTransaction, LoanCharge loanCharge, BigDecimal amount) { + this.loanTransaction = loanTransaction; + this.loanCharge = loanCharge; + this.amount = amount; + } + + public LoanTransaction getLoanTransaction() { + return this.loanTransaction; + } + + public void setLoanTransaction(LoanTransaction loanTransaction) { + this.loanTransaction = loanTransaction; + } + + public LoanCharge getLoanCharge() { + return this.loanCharge; + } + + public void setLoanCharge(LoanCharge loanCharge) { + this.loanCharge = loanCharge; + } + + public BigDecimal getAmount() { + return this.amount; + } + + public void setAmount(BigDecimal amount) { + this.amount = amount; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanChargePaidByRepository.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanChargePaidByRepository.java new file mode 100755 index 000000000..5a84a0d49 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanChargePaidByRepository.java @@ -0,0 +1,13 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.loanaccount.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface LoanChargePaidByRepository extends JpaRepository, JpaSpecificationExecutor { + // no added behaviour +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransaction.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransaction.java index 362a8229f..3e6a856a9 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransaction.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransaction.java @@ -7,17 +7,23 @@ package org.mifosplatform.portfolio.loanaccount.domain; import java.math.BigDecimal; import java.util.Date; +import java.util.HashSet; import java.util.LinkedHashMap; 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 org.hibernate.annotations.LazyCollection; +import org.hibernate.annotations.LazyCollectionOption; import org.joda.time.LocalDate; import org.mifosplatform.organisation.monetary.data.CurrencyData; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; @@ -70,6 +76,10 @@ public final class LoanTransaction extends AbstractPersistable { @Column(name = "is_reversed", nullable = false) private boolean reversed; + @LazyCollection(LazyCollectionOption.FALSE) + @OneToMany(cascade = CascadeType.ALL, mappedBy = "loanTransaction", orphanRemoval = true) + private Set loanChargesPaid = new HashSet(); + protected LoanTransaction() { this.loan = null; this.dateOf = null; @@ -250,6 +260,10 @@ public final class LoanTransaction extends AbstractPersistable { return !isReversed(); } + public boolean isAnyTypeOfRepayment() { + return isRepayment() || isRepaymentAtDisbursement() || isRecoveryRepayment(); + } + public boolean isRepayment() { return LoanTransactionType.REPAYMENT.equals(getTypeOf()) && isNotReversed(); } @@ -367,4 +381,12 @@ public final class LoanTransaction extends AbstractPersistable { return this.loan; } + public Set getLoanChargesPaid() { + return this.loanChargesPaid; + } + + public void setLoanChargesPaid(Set loanChargesPaid) { + this.loanChargesPaid = loanChargesPaid; + } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index b6c285350..0102458e8 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -337,6 +337,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf defaultLoanLifecycleStateMachine(), existingTransactionIds, existingReversedTransactionIds); this.loanTransactionRepository.save(newRepaymentTransaction); + /*** * TODO Vishwas Batch save is giving me a * HibernateOptimisticLockingFailureException, looping and saving for From 997fd4d3e23c80d6445481306d770a7cd031e7a1 Mon Sep 17 00:00:00 2001 From: ashok-conflux Date: Wed, 5 Jun 2013 14:57:37 +0530 Subject: [PATCH 26/49] MIFOSX-416 : display loan type for loan in Client's general details --- .../client/data/ClientAccountSummaryData.java | 17 +++++++++++++++++ .../service/ClientReadPlatformServiceImpl.java | 6 ++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/data/ClientAccountSummaryData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/data/ClientAccountSummaryData.java index 6bf022033..944473f03 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/data/ClientAccountSummaryData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/data/ClientAccountSummaryData.java @@ -5,6 +5,7 @@ */ package org.mifosplatform.portfolio.client.data; +import org.mifosplatform.infrastructure.core.data.EnumOptionData; import org.mifosplatform.portfolio.loanaccount.data.LoanStatusEnumData; /** @@ -25,6 +26,8 @@ public class ClientAccountSummaryData { private final Integer accountStatusId; private final LoanStatusEnumData status; + @SuppressWarnings("unused") + private final EnumOptionData loanType; public ClientAccountSummaryData(final Long id, final String externalId, final Long productId, final String loanProductName, final Integer loanStatusId) { @@ -35,6 +38,7 @@ public class ClientAccountSummaryData { this.productName = loanProductName; this.accountStatusId = loanStatusId; this.status = null; + this.loanType = null; } public ClientAccountSummaryData(final Long id, final String accountNo, final String externalId, final Long productId, @@ -46,6 +50,19 @@ public class ClientAccountSummaryData { this.productName = loanProductName; this.accountStatusId = null; this.status = loanStatus; + this.loanType = null; + } + + public ClientAccountSummaryData(final Long id, final String accountNo, final String externalId, final Long productId, + final String loanProductName, final LoanStatusEnumData loanStatus, final EnumOptionData loanType) { + this.id = id; + this.accountNo = accountNo; + this.externalId = externalId; + this.productId = productId; + this.productName = loanProductName; + this.accountStatusId = null; + this.status = loanStatus; + this.loanType = loanType; } public Integer accountStatusId() { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientReadPlatformServiceImpl.java index a0f139038..72f1851e9 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientReadPlatformServiceImpl.java @@ -476,7 +476,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService public String loanAccountSummarySchema() { StringBuilder accountsSummary = new StringBuilder("l.id as id, l.account_no as accountNo, l.external_id as externalId,"); - accountsSummary.append("l.product_id as productId, lp.name as productName,").append("l.loan_status_id as statusId ") + accountsSummary.append("l.product_id as productId, lp.name as productName,").append("l.loan_status_id as statusId, l.loan_type_enum as loanType ") .append("from m_loan l ").append("LEFT JOIN m_product_loan AS lp ON lp.id = l.product_id "); return accountsSummary.toString(); @@ -492,8 +492,10 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService final String loanProductName = rs.getString("productName"); final Integer loanStatusId = JdbcSupport.getInteger(rs, "statusId"); final LoanStatusEnumData loanStatus = LoanEnumerations.status(loanStatusId); + final Integer loanTypeId = JdbcSupport.getInteger(rs, "loanType"); + final EnumOptionData loanType = LoanEnumerations.loanType(loanTypeId); - return new ClientAccountSummaryData(id, accountNo, externalId, productId, loanProductName, loanStatus); + return new ClientAccountSummaryData(id, accountNo, externalId, productId, loanProductName, loanStatus, loanType); } } From 8e0427d63355fd096b97ccf041ed4fd2a7174569 Mon Sep 17 00:00:00 2001 From: Vishwas Babu AJ Date: Wed, 5 Jun 2013 16:15:18 +0530 Subject: [PATCH 27/49] deleting test cases for document upload --- .../ConfigurationDomainServiceJpaTest.java | 46 ------- .../service/DocumentStoreFactoryTest.java | 45 ------- .../core/service/FileDocumentStoreTest.java | 23 ---- .../core/service/S3DocumentStoreTest.java | 121 ------------------ 4 files changed, 235 deletions(-) delete mode 100644 mifosng-provider/src/test/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainServiceJpaTest.java delete mode 100644 mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/DocumentStoreFactoryTest.java delete mode 100644 mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/FileDocumentStoreTest.java delete mode 100644 mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/S3DocumentStoreTest.java diff --git a/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainServiceJpaTest.java b/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainServiceJpaTest.java deleted file mode 100644 index 5fe094e14..000000000 --- a/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainServiceJpaTest.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.mifosplatform.infrastructure.configuration.domain; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mifosplatform.useradministration.domain.Permission; -import org.mifosplatform.useradministration.domain.PermissionRepository; -import org.mockito.runners.MockitoJUnitRunner; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; - -import java.util.List; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -@SuppressWarnings("unused") -@RunWith(MockitoJUnitRunner.class) -public class ConfigurationDomainServiceJpaTest { - - private final String S3 = "amazon-S3"; - private PermissionRepository permissionRepositoryMock = mock(PermissionRepository.class);; - private GlobalConfigurationRepository globalConfigurationRepositoryMock = mock(GlobalConfigurationRepository.class); - - @Test - public void shouldReturnTrueIfAmazonS3ConfigurationIsEnabled(){ - when(globalConfigurationRepositoryMock.findOneByName(S3)).thenReturn(new GlobalConfigurationProperty(S3, true)); - ConfigurationDomainServiceJpa configurationDomainServiceJpa = new ConfigurationDomainServiceJpa(permissionRepositoryMock, globalConfigurationRepositoryMock); - - boolean actualIsS3Enabled = configurationDomainServiceJpa.isAmazonS3Enabled(); - - assertTrue(actualIsS3Enabled); - } - - @Test - public void shouldReturnFalseIfAmazonS3ConfigurationIsDisabled(){ - when(globalConfigurationRepositoryMock.findOneByName(S3)).thenReturn(new GlobalConfigurationProperty(S3, false)); - ConfigurationDomainServiceJpa configurationDomainServiceJpa = new ConfigurationDomainServiceJpa(permissionRepositoryMock, globalConfigurationRepositoryMock); - - boolean actualIsS3Enabled = configurationDomainServiceJpa.isAmazonS3Enabled(); - - assertFalse(actualIsS3Enabled); - } -} diff --git a/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/DocumentStoreFactoryTest.java b/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/DocumentStoreFactoryTest.java deleted file mode 100644 index fd54df1db..000000000 --- a/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/DocumentStoreFactoryTest.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.mifosplatform.infrastructure.core.service; - -import org.junit.runner.RunWith; -import org.mockito.runners.MockitoJUnitRunner; - -@RunWith(MockitoJUnitRunner.class) -public class DocumentStoreFactoryTest { -/** - @Test - public void shouldReturnS3DocumentStoreObjectIfS3Enabled() { - S3ClientFactory s3ClientFactoryMock = mock(S3ClientFactory.class); - when(s3ClientFactoryMock.getS3Client()).thenReturn(null); - - ExternalServicesReadPlatformServiceImpl externalServicesDomainServiceMock = mock(ExternalServicesReadPlatformServiceImpl.class); - when(externalServicesDomainServiceMock.getValue("s3_bucket_name")).thenReturn("bucket_name"); - - ConfigurationDomainService configurationDomainServiceMock = mock(ConfigurationDomainService.class); - when(configurationDomainServiceMock.isAmazonS3Enabled()).thenReturn(true); - - ApplicationContext applicationContextMock = mock(ApplicationContext.class); - - when(applicationContextMock.getBean("configurationDomainServiceJpa", ConfigurationDomainService.class)).thenReturn( - configurationDomainServiceMock); - when(applicationContextMock.getBean("s3ClientFactory", S3ClientFactory.class)).thenReturn(s3ClientFactoryMock); - when(applicationContextMock.getBean("externalServicesDomainService", ExternalServicesReadPlatformServiceImpl.class)).thenReturn( - externalServicesDomainServiceMock); - - ContentRepositoryFactory documentStoreFactory = new ContentRepositoryFactory(applicationContextMock); - assertEquals(documentStoreFactory.getInstanceFromConfiguration().getClass(), S3ContentRepository.class); - } - - @Test - public void shouldReturnFileSystemDocumentStoreObjectIfS3IsDisabled() { - ConfigurationDomainService configurationDomainService = mock(ConfigurationDomainService.class); - when(configurationDomainService.isAmazonS3Enabled()).thenReturn(false); - - ApplicationContext applicationContext = mock(ApplicationContext.class); - when(applicationContext.getBean("configurationDomainServiceJpa", ConfigurationDomainService.class)).thenReturn( - configurationDomainService); - - ContentRepositoryFactory documentStoreFactory = new ContentRepositoryFactory(applicationContext); - assertEquals(documentStoreFactory.getInstanceFromConfiguration().getClass(), FileSystemContentRepository.class); - } -**/ -} diff --git a/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/FileDocumentStoreTest.java b/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/FileDocumentStoreTest.java deleted file mode 100644 index 37c8a367a..000000000 --- a/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/FileDocumentStoreTest.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.mifosplatform.infrastructure.core.service; - -import org.junit.runner.RunWith; -import org.mockito.runners.MockitoJUnitRunner; - -@RunWith(MockitoJUnitRunner.class) -public class FileDocumentStoreTest { -/** - @Test - public void shouldRetrieveDocument() { - FileSystemContentRepository fileSystemDocumentStore = new FileSystemContentRepository(); - DocumentData documentDataMock = mock(DocumentData.class); - when(documentDataMock.fileName()).thenReturn("some_file"); - when(documentDataMock.fileName()).thenReturn("content_type"); - when(documentDataMock.fileLocation()).thenReturn("abcd"); - - FileData fileData = fileSystemDocumentStore.retrieveDocument(documentDataMock); - - assertEquals(fileData.name(), documentDataMock.fileName()); - assertEquals(fileData.contentType(), documentDataMock.contentType()); - } -**/ -} diff --git a/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/S3DocumentStoreTest.java b/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/S3DocumentStoreTest.java deleted file mode 100644 index 8b6e4ab53..000000000 --- a/mifosng-provider/src/test/java/org/mifosplatform/infrastructure/core/service/S3DocumentStoreTest.java +++ /dev/null @@ -1,121 +0,0 @@ -package org.mifosplatform.infrastructure.core.service; - -import java.io.InputStream; - -import org.junit.runner.RunWith; -import org.mifosplatform.infrastructure.documentmanagement.command.DocumentCommand; -import org.mifosplatform.infrastructure.documentmanagement.data.DocumentData; -import org.mockito.runners.MockitoJUnitRunner; - -import com.amazonaws.services.s3.AmazonS3Client; - -@RunWith(MockitoJUnitRunner.class) -public class S3DocumentStoreTest { - - InputStream inputStream; - AmazonS3Client s3ClientMock; - DocumentCommand documentCommand; - DocumentData documentDataMock; -/** - @Test - public void shouldGenerateParentDirectoryFromEntityDetails() { - S3ContentRepository s3DocumentStore = new S3ContentRepository("bucketName", null); - String actualParentDirectory = s3DocumentStore.generateFileParentDirectory("entity_type", 1234L); - assertTrue(actualParentDirectory.startsWith("documents/entity_type/1234")); - } - - @Test - public void shouldGenerateClientImageDirectoryFromResourceId() { - S3ContentRepository s3DocumentStore = new S3ContentRepository("bucketName", null); - String actualParentDirectory = s3DocumentStore.generateClientImageParentDirectory(1234L); - assertTrue(actualParentDirectory.startsWith("images/clients/1234")); - } - - @Test - public void shouldSaveADocumentToS3() throws IOException { - initUpload(); - when(s3ClientMock.putObject(Matchers. anyObject())).thenReturn(new PutObjectResult()); - S3ContentRepository s3DocumentStore = new S3ContentRepository("bucketName", s3ClientMock); - - s3DocumentStore.saveDocument(inputStream, documentCommand); - - verify(s3ClientMock, times(1)).putObject(Matchers. anyObject()); - } - - @Test(expected = DocumentManagementException.class) - public void shouldCatchAmazonServiceExceptionWhenUnableToUploadDocumentToS3() throws IOException { - initUpload(); - when(s3ClientMock.putObject(Matchers. anyObject())).thenThrow(new AmazonServiceException("serviceException")); - - S3ContentRepository s3DocumentStore = new S3ContentRepository("bucketName", s3ClientMock); - - s3DocumentStore.saveDocument(inputStream, documentCommand); - } - - @Test(expected = DocumentManagementException.class) - public void shouldCatchAmazonClientExceptionWhenUnableToUploadDocumentToS3() throws IOException { - initUpload(); - when(s3ClientMock.putObject(Matchers. anyObject())).thenThrow(new AmazonClientException("clientException")); - - S3ContentRepository s3DocumentStore = new S3ContentRepository("bucketName", s3ClientMock); - - s3DocumentStore.saveDocument(inputStream, documentCommand); - } - - @Test - public void shouldRetrieveDocumentFromS3() throws IOException { - initRetrieve(); - when(s3ClientMock.getObject(Matchers. anyObject())).thenReturn(new S3Object()); - S3ContentRepository s3DocumentStore = new S3ContentRepository("bucketName", s3ClientMock); - - FileData fileData = s3DocumentStore.retrieveDocument(documentDataMock); - - verify(s3ClientMock, times(1)).getObject(Matchers. anyObject()); - assertEquals(fileData.name(), "name"); - assertEquals(fileData.contentType(), "type"); - } - - @Test(expected = DocumentNotFoundException.class) - public void shouldCatchAmazonServiceExceptionWhenUnableToRetrieveDocumentFromS3() throws IOException { - initRetrieve(); - when(s3ClientMock.getObject(Matchers. anyObject())).thenThrow(new AmazonServiceException("serviceException")); - S3ContentRepository s3DocumentStore = new S3ContentRepository("bucketName", s3ClientMock); - - s3DocumentStore.retrieveDocument(documentDataMock); - } - - @Test(expected = DocumentNotFoundException.class) - public void shouldCatchAmazonClientExceptionWhenUnableToRetrieveDocumentFromS3() throws IOException { - initRetrieve(); - when(s3ClientMock.getObject(Matchers. anyObject())).thenThrow(new AmazonClientException("clientException")); - S3ContentRepository s3DocumentStore = new S3ContentRepository("bucketName", s3ClientMock); - - s3DocumentStore.retrieveDocument(documentDataMock); - } - - @Test - public void shouldDeleteImageFromS3() throws IOException { - AmazonS3Client s3ClientMock = mock(AmazonS3Client.class); - S3ContentRepository s3DocumentStore = new S3ContentRepository("bucketName", s3ClientMock); - - s3DocumentStore.deleteImage(123L, "imagekey"); - - verify(s3ClientMock, times(1)).deleteObject(Matchers. anyObject()); - } - - private void initRetrieve() { - s3ClientMock = mock(AmazonS3Client.class); - - documentDataMock = mock(DocumentData.class); - when(documentDataMock.fileLocation()).thenReturn("path"); - when(documentDataMock.fileName()).thenReturn("name"); - when(documentDataMock.contentType()).thenReturn("type"); - } - - private void initUpload() { - inputStream = S3DocumentStoreTest.class.getResourceAsStream("DummyString"); - s3ClientMock = mock(AmazonS3Client.class); - documentCommand = new DocumentCommand(null, -1l, "parentEntityType", 1l, null, "someFileName", 3l, null, null, null); - } - **/ -} From efbb1e3b2f6ab5d4b0ad6b6c7e2db84b63f9879c Mon Sep 17 00:00:00 2001 From: Keith Woodlock Date: Wed, 5 Jun 2013 12:10:51 +0100 Subject: [PATCH 28/49] MIFOSX-295: support principal and interest grace along with ability to have interest-free periods. --- api-docs/apiLive.htm | 66 +- ...50__add-grace-settings-to-loan-product.sql | 9 + .../data/GLAccountDataForLookup.java | 5 + .../core/domain/JdbcSupport.java | 13 + .../core}/domain/LocalDateInterval.java | 2 +- .../monetary/domain/ApplicationCurrency.java | 4 + .../organisation/monetary/domain/Money.java | 4 + .../loanaccount/api/LoansApiResource.java | 60 +- .../loanaccount/data/LoanAccountData.java | 216 +++--- .../portfolio/loanaccount/domain/Loan.java | 134 ++-- .../data/LoanSchedulePeriodData.java | 8 - .../domain/AbstractLoanScheduleGenerator.java | 188 +++++ .../AmortizationLoanScheduleGenerator.java | 23 - ...rtizationLoanScheduleGeneratorFactory.java | 31 - ...gBalanceInterestLoanScheduleGenerator.java | 67 ++ ...ingBalanceMethodLoanScheduleGenerator.java | 74 -- .../DefaultLoanScheduleGeneratorFactory.java | 4 +- ...aultPaymentPeriodsInOneYearCalculator.java | 156 ++-- .../domain/DefaultScheduledDateGenerator.java | 121 ++- ...entsAmortizationLoanScheduleGenerator.java | 190 ----- ...ipalAmortizationLoanScheduleGenerator.java | 178 ----- .../domain/FinanicalFunctions.java | 106 +++ .../FlatInterestLoanScheduleGenerator.java | 43 ++ .../FlatMethodLoanScheduleGenerator.java | 178 ----- .../domain/LoanApplicationTerms.java | 691 ++++++++++++++++++ .../loanschedule/domain/LoanSchedule.java | 109 --- .../domain/LoanScheduleGenerator.java | 11 +- .../domain/LoanScheduleGeneratorFactory.java | 1 - .../domain/LoanScheduleModel.java | 85 +++ .../LoanScheduleModelDisbursementPeriod.java | 86 +++ .../domain/LoanScheduleModelPeriod.java | 32 + .../LoanScheduleModelRepaymentPeriod.java | 118 +++ .../PaymentPeriodsInOneYearCalculator.java | 10 +- .../PeriodicInterestRateCalculator.java | 131 ++-- .../loanschedule/domain/PmtCalculator.java | 100 --- .../domain/PrincipalInterest.java | 33 + .../domain/ScheduledDateGenerator.java | 13 +- .../service/LoanScheduleAssembler.java | 124 ++-- ...oanScheduleCalculationPlatformService.java | 4 +- ...cheduleCalculationPlatformServiceImpl.java | 15 +- ...ateLoanScheduleQueryFromApiJsonHelper.java | 5 +- ...anApplicationCommandFromApiJsonHelper.java | 73 +- ...WritePlatformServiceJpaRepositoryImpl.java | 40 +- .../loanaccount/service/LoanAssembler.java | 29 +- .../service/LoanReadPlatformServiceImpl.java | 10 +- ...WritePlatformServiceJpaRepositoryImpl.java | 23 +- .../api/LoanProductsApiResource.java | 15 +- .../loanproduct/data/LoanProductData.java | 45 +- .../loanproduct/domain/LoanProduct.java | 25 +- .../domain/LoanProductRelatedDetail.java | 100 ++- ...zer.java => LoanProductDataValidator.java} | 41 +- .../LoanProductReadPlatformServiceImpl.java | 8 +- ...WritePlatformServiceJpaRepositoryImpl.java | 6 +- .../InterestCompoundingPeriodSummary.java | 1 + .../savings/domain/SavingsAccount.java | 1 + .../domain/SavingsAccountDailyBalance.java | 1 + .../domain/SavingsAccountTransaction.java | 1 + ...avingsAccountStateTransitionException.java | 5 + .../resources/META-INF/spring/scheduler.xml | 7 + ...alanceMethodLoanScheduleGeneratorTest.java | 140 ---- .../FlatMethodLoanScheduleGeneratorTest.java | 163 ----- .../LoanProductRelatedDetailTestHelper.java | 21 +- 62 files changed, 2376 insertions(+), 1827 deletions(-) create mode 100644 mifosng-db/migrations/core_db/V50__add-grace-settings-to-loan-product.sql rename mifosng-provider/src/main/java/org/mifosplatform/{portfolio/savings => infrastructure/core}/domain/LocalDateInterval.java (96%) create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java delete mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AmortizationLoanScheduleGenerator.java delete mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AmortizationLoanScheduleGeneratorFactory.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DecliningBalanceInterestLoanScheduleGenerator.java delete mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DecliningBalanceMethodLoanScheduleGenerator.java delete mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/EqualInstallmentsAmortizationLoanScheduleGenerator.java delete mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/EqualPrincipalAmortizationLoanScheduleGenerator.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/FinanicalFunctions.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/FlatInterestLoanScheduleGenerator.java delete mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/FlatMethodLoanScheduleGenerator.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java delete mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanSchedule.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModel.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelDisbursementPeriod.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelPeriod.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelRepaymentPeriod.java delete mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/PmtCalculator.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/PrincipalInterest.java rename mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/serialization/{LoanProductCommandFromApiJsonDeserializer.java => LoanProductDataValidator.java} (95%) delete mode 100644 mifosng-provider/src/test/java/org/mifosplatform/portfolio/loanaccount/DecliningBalanceMethodLoanScheduleGeneratorTest.java delete mode 100644 mifosng-provider/src/test/java/org/mifosplatform/portfolio/loanaccount/FlatMethodLoanScheduleGeneratorTest.java diff --git a/api-docs/apiLive.htm b/api-docs/apiLive.htm index a6608b6cc..53bee542d 100644 --- a/api-docs/apiLive.htm +++ b/api-docs/apiLive.htm @@ -11,7 +11,7 @@ var result = {}; if (window.location.search) { // split up the query string and store in an associative array - var params = window.location.search.slice(1).split("&"); x + var params = window.location.search.slice(1).split("&"); for ( var i = 0; i < params.length; i++) { var tmp = params[i].split("="); result[tmp[0]] = unescape(tmp[1]); @@ -3821,6 +3821,34 @@ GET https://DomainName/mifosng-provider/api/v1/groups Example Values: 2=Per month, 3=Per year + + graceOnPrincipalPayment + + + Optional: Integer - represents the number of repayment periods that grace should apply to the principal component of a repayment period. + + + + graceOnInterestPayment + + + Optional: Integer - represents the number of repayment periods that grace should apply to the interest component of a repayment period. Interest is still calculated but offset to later repayment periods. + + + + graceOnInterestCharged + + + Optional: Integer - represents the number of repayment periods that should be interest-free. + + + + interestChargedFromDate + + + Optional: Date - The date from with interest is to start being charged. + + expectedDisbursementDate @@ -4849,6 +4877,15 @@ Request Body: expectedDisbursementDate, submittedOnDate, loanType +
+ + + + + + + +
Optional Fields
graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged
@@ -6967,6 +7004,28 @@ Request Body: + + graceOnPrincipalPayment + + + Optional: Integer - represents the number of repayment periods that grace should apply to the principal component of a repayment period. + + + + graceOnInterestPayment + + + Optional: Integer - represents the number of repayment periods that grace should apply to the interest component of a repayment period. Interest is still calculated but offset to later repayment periods. + + + + graceOnInterestCharged + + + Optional: Integer - represents the number of repayment periods that should be interest-free. + + + accountingRule @@ -7524,13 +7583,14 @@ GET https://DomainName/api/v1/loanproducts/{productId} name, currencyCode, digitsAfterDecimal, principal, numberOfRepayments, repaymentEvery, repaymentFrequencyType, interestRatePerPeriod, interestRateFrequencyType, amortizationType, interestType, - interestCalculationPeriodType, transactionProcessingStrategyId, accountingRule + interestCalculationPeriodType, transactionProcessingStrategyId, + accountingRule
Optional Fields
- inArrearsTolerance, charges,paymentChannelToFundSourceMappings + inArrearsTolerance, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, charges, paymentChannelToFundSourceMappings diff --git a/mifosng-db/migrations/core_db/V50__add-grace-settings-to-loan-product.sql b/mifosng-db/migrations/core_db/V50__add-grace-settings-to-loan-product.sql new file mode 100644 index 000000000..bef3665d4 --- /dev/null +++ b/mifosng-db/migrations/core_db/V50__add-grace-settings-to-loan-product.sql @@ -0,0 +1,9 @@ +ALTER TABLE `m_product_loan` +ADD COLUMN `grace_on_principal_periods` SMALLINT(5) NULL DEFAULT NULL AFTER `max_number_of_repayments`, +ADD COLUMN `grace_on_interest_periods` SMALLINT(5) NULL DEFAULT NULL AFTER `grace_on_principal_periods`, +ADD COLUMN `grace_interest_free_periods` SMALLINT(5) NULL DEFAULT NULL AFTER `grace_on_interest_periods`; + +ALTER TABLE `m_loan` +ADD COLUMN `grace_on_principal_periods` SMALLINT(5) NULL DEFAULT NULL AFTER `number_of_repayments`, +ADD COLUMN `grace_on_interest_periods` SMALLINT(5) NULL DEFAULT NULL AFTER `grace_on_principal_periods`, +ADD COLUMN `grace_interest_free_periods` SMALLINT(5) NULL DEFAULT NULL AFTER `grace_on_interest_periods`; \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/data/GLAccountDataForLookup.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/data/GLAccountDataForLookup.java index 751a803f0..7416dee50 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/data/GLAccountDataForLookup.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/data/GLAccountDataForLookup.java @@ -1,3 +1,8 @@ +/** + * 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.glaccount.data; public class GLAccountDataForLookup { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/domain/JdbcSupport.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/domain/JdbcSupport.java index ca8e203a2..45d59db3e 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/domain/JdbcSupport.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/domain/JdbcSupport.java @@ -47,6 +47,19 @@ public class JdbcSupport { return (Integer) JdbcUtils.getResultSetValue(rs, rs.findColumn(columnName), Integer.class); } + public static Integer getIntegerDefaultToNullIfZero(final ResultSet rs, final String columnName) throws SQLException { + Integer value = (Integer) JdbcUtils.getResultSetValue(rs, rs.findColumn(columnName), Integer.class); + return defaultToNullIfZero(value); + } + + private static Integer defaultToNullIfZero(final Integer value) { + Integer result = value; + if (result != null && Integer.valueOf(0).equals(value)) { + result = null; + } + return result; + } + public static BigDecimal getBigDecimalDefaultToZeroIfNull(final ResultSet rs, final String columnName) throws SQLException { final BigDecimal value = rs.getBigDecimal(columnName); return defaultToZeroIfNull(value); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/LocalDateInterval.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/domain/LocalDateInterval.java similarity index 96% rename from mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/LocalDateInterval.java rename to mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/domain/LocalDateInterval.java index 84fef927b..80dd741ff 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/LocalDateInterval.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/domain/LocalDateInterval.java @@ -3,7 +3,7 @@ * 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.savings.domain; +package org.mifosplatform.infrastructure.core.domain; import org.joda.time.Days; import org.joda.time.LocalDate; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/organisation/monetary/domain/ApplicationCurrency.java b/mifosng-provider/src/main/java/org/mifosplatform/organisation/monetary/domain/ApplicationCurrency.java index e7d5e189d..bdf28007d 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/organisation/monetary/domain/ApplicationCurrency.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/organisation/monetary/domain/ApplicationCurrency.java @@ -77,6 +77,10 @@ public class ApplicationCurrency extends AbstractPersistable { return new CurrencyData(this.code, this.name, this.decimalPlaces, this.displaySymbol, this.nameCode); } + public CurrencyData toData(final int digitsAfterDecimalSupported) { + return new CurrencyData(this.code, this.name, digitsAfterDecimalSupported, this.displaySymbol, this.nameCode); + } + public OrganisationCurrency toOrganisationCurrency() { return new OrganisationCurrency(this.code, this.name, this.decimalPlaces, this.nameCode, this.displaySymbol); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/organisation/monetary/domain/Money.java b/mifosng-provider/src/main/java/org/mifosplatform/organisation/monetary/domain/Money.java index 9a66837ee..87768c984 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/organisation/monetary/domain/Money.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/organisation/monetary/domain/Money.java @@ -256,4 +256,8 @@ public class Money implements Comparable { private MonetaryCurrency monetaryCurrency() { return new MonetaryCurrency(this.currencyCode, this.currencyDigitsAfterDecimal); } + + public Money zero() { + return Money.zero(getCurrency()); + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoansApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoansApiResource.java index 3059f5df6..0adbb3507 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoansApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoansApiResource.java @@ -64,6 +64,7 @@ import org.mifosplatform.portfolio.loanaccount.exception.NotSupportedLoanTemplat import org.mifosplatform.portfolio.loanaccount.guarantor.data.GuarantorData; import org.mifosplatform.portfolio.loanaccount.guarantor.service.GuarantorReadPlatformService; import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData; +import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; import org.mifosplatform.portfolio.loanaccount.loanschedule.service.LoanScheduleCalculationPlatformService; import org.mifosplatform.portfolio.loanaccount.service.LoanChargeReadPlatformService; import org.mifosplatform.portfolio.loanaccount.service.LoanReadPlatformService; @@ -88,12 +89,12 @@ public class LoansApiResource { "loanPurposeName", "loanOfficerId", "loanOfficerName", "currency", "principal", "inArrearsTolerance", "termFrequency", "termPeriodFrequencyType", "numberOfRepayments", "repaymentEvery", "interestRatePerPeriod", "annualInterestRate", "repaymentFrequencyType", "transactionProcessingStrategyId", "interestRateFrequencyType", "amortizationType", "interestType", - "interestCalculationPeriodType", "expectedFirstRepaymentOnDate", "interestChargedFromDate", "timeline", - "totalFeeChargesAtDisbursement", "summary", "repaymentSchedule", "transactions", "charges", "collateral", "guarantors","meeting", - "productOptions", "amortizationTypeOptions", "interestTypeOptions", "interestCalculationPeriodTypeOptions", - "repaymentFrequencyTypeOptions", "termFrequencyTypeOptions", "interestRateFrequencyTypeOptions", "fundOptions", - "repaymentStrategyOptions", "chargeOptions", "loanOfficerOptions", "loanPurposeOptions", "loanCollateralOptions", - "chargeTemplate","calendarOptions")); + "interestCalculationPeriodType", "expectedFirstRepaymentOnDate", "graceOnPrincipalPayment", "graceOnInterestPayment", + "graceOnInterestCharged", "interestChargedFromDate", "timeline", "totalFeeChargesAtDisbursement", "summary", + "repaymentSchedule", "transactions", "charges", "collateral", "guarantors", "meeting", "productOptions", + "amortizationTypeOptions", "interestTypeOptions", "interestCalculationPeriodTypeOptions", "repaymentFrequencyTypeOptions", + "termFrequencyTypeOptions", "interestRateFrequencyTypeOptions", "fundOptions", "repaymentStrategyOptions", "chargeOptions", + "loanOfficerOptions", "loanPurposeOptions", "loanCollateralOptions", "chargeTemplate", "calendarOptions")); private final String resourceNameForPermissions = "LOAN"; @@ -126,8 +127,7 @@ public class LoansApiResource { final CollateralReadPlatformService loanCollateralReadPlatformService, final LoanScheduleCalculationPlatformService calculationPlatformService, final StaffReadPlatformService staffReadPlatformService, final GuarantorReadPlatformService guarantorReadPlatformService, - final CodeValueReadPlatformService codeValueReadPlatformService, - final GroupReadPlatformService groupReadPlatformService, + final CodeValueReadPlatformService codeValueReadPlatformService, final GroupReadPlatformService groupReadPlatformService, final DefaultToApiJsonSerializer toApiJsonSerializer, final DefaultToApiJsonSerializer loanScheduleToApiJsonSerializer, final DefaultToApiJsonSerializer loanOfficeTransferToApiJsonSerializer, @@ -161,7 +161,8 @@ public class LoansApiResource { @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) public String template(@QueryParam("clientId") final Long clientId, @QueryParam("groupId") final Long groupId, - @QueryParam("productId") final Long productId, @QueryParam("templateType") final String templateType, @Context final UriInfo uriInfo) { + @QueryParam("productId") final Long productId, @QueryParam("templateType") final String templateType, + @Context final UriInfo uriInfo) { context.authenticatedUser().validateHasReadPermission(resourceNameForPermissions); @@ -174,24 +175,24 @@ public class LoansApiResource { Collection calendarOptions = null; LoanAccountData newLoanAccount = null; Long officeId = null; - + if (productId != null) { newLoanAccount = this.loanReadPlatformService.retrieveLoanProductDetailsTemplate(productId); } - - if(templateType == null){ + + if (templateType == null) { final String errorMsg = "Loan template type must be provided"; throw new LoanTemplateTypeRequiredException(errorMsg); - }else if (templateType.equals("collateral")) { + } else if (templateType.equals("collateral")) { newLoanAccount = LoanAccountData.collateralTemplate(loanCollateralOptions); } else { - //for JLG loan both client and group details are required + // for JLG loan both client and group details are required if (templateType.equals("individual") || templateType.equals("jlg")) { LoanAccountData loanAccountClientDetails = this.loanReadPlatformService.retrieveClientDetailsTemplate(clientId); officeId = loanAccountClientDetails.officeId(); - + newLoanAccount = (newLoanAccount == null) ? loanAccountClientDetails : LoanAccountData.populateClientDefaults( newLoanAccount, loanAccountClientDetails); @@ -211,7 +212,7 @@ public class LoansApiResource { loanAccountGroupData); } else if (templateType.equals("jlgbulk")) { - //get group details along with members in that group + // get group details along with members in that group final LoanAccountData loanAccountGroupData = this.loanReadPlatformService.retrieveGroupAndMembersDetailsTemplate(groupId); officeId = loanAccountGroupData.groupOfficeId(); calendarOptions = this.loanReadPlatformService.retrieveCalendars(groupId); @@ -223,7 +224,8 @@ public class LoansApiResource { } if (officeId != null) allowedLoanOfficers = this.staffReadPlatformService.retrieveAllLoanOfficersByOffice(officeId); - // add product options, allowed loan officers and calendar options (calendar options will be null in individual loan) + // add product options, allowed loan officers and calendar options + // (calendar options will be null in individual loan) newLoanAccount = LoanAccountData.associationsAndTemplate(newLoanAccount, productOptions, allowedLoanOfficers, calendarOptions); } final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters()); @@ -246,7 +248,7 @@ public class LoansApiResource { Collection guarantors = null; Collection collateral = null; CalendarData meeting = null; - + final Set mandatoryResponseParameters = new HashSet(); final Set associationParameters = ApiParameterHelper.extractAssociationsForResponseIfProvided(uriInfo.getQueryParameters()); if (!associationParameters.isEmpty()) { @@ -293,7 +295,7 @@ public class LoansApiResource { collateral = null; } } - + if (associationParameters.contains("meeting")) { mandatoryResponseParameters.add("meeting"); meeting = this.calendarReadPlatformService.retrieveLoanCalendar(loanId); @@ -332,20 +334,20 @@ public class LoansApiResource { final boolean feeChargesOnly = false; chargeOptions = this.chargeReadPlatformService.retrieveLoanApplicableCharges(feeChargesOnly); chargeTemplate = this.loanChargeReadPlatformService.retrieveLoanChargeTemplate(); - - if(loanBasicDetails.officeId() != null){ + + if (loanBasicDetails.officeId() != null) { allowedLoanOfficers = this.staffReadPlatformService.retrieveAllLoanOfficersByOffice(loanBasicDetails.officeId()); - }else if(loanBasicDetails.groupOfficeId() != null){ + } else if (loanBasicDetails.groupOfficeId() != null) { allowedLoanOfficers = this.staffReadPlatformService.retrieveAllLoanOfficersByOffice(loanBasicDetails.groupOfficeId()); } - + loanPurposeOptions = this.codeValueReadPlatformService.retrieveCodeValuesByCode("LoanPurpose"); loanCollateralOptions = this.codeValueReadPlatformService.retrieveCodeValuesByCode("LoanCollateral"); - - if(loanBasicDetails.groupId() != null){ + + if (loanBasicDetails.groupId() != null) { calendarOptions = this.loanReadPlatformService.retrieveCalendars(loanBasicDetails.groupId()); } - + } final LoanAccountData loanAccount = LoanAccountData.associationsAndTemplate(loanBasicDetails, repaymentSchedule, loanRepayments, @@ -376,7 +378,7 @@ public class LoansApiResource { final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters()); return this.toApiJsonSerializer.serialize(settings, loanBasicDetails, LOAN_DATA_PARAMETERS); } - + @POST @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) @@ -388,10 +390,10 @@ public class LoansApiResource { final JsonElement parsedQuery = this.fromJsonHelper.parse(apiRequestBodyAsJson); final JsonQuery query = JsonQuery.from(apiRequestBodyAsJson, parsedQuery, this.fromJsonHelper); - final LoanScheduleData loanSchedule = this.calculationPlatformService.calculateLoanSchedule(query); + final LoanScheduleModel loanSchedule = this.calculationPlatformService.calculateLoanSchedule(query); final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters()); - return this.loanScheduleToApiJsonSerializer.serialize(settings, loanSchedule, new HashSet()); + return this.loanScheduleToApiJsonSerializer.serialize(settings, loanSchedule.toData(), new HashSet()); } final CommandWrapper commandRequest = new CommandWrapperBuilder().createLoanApplication().withJson(apiRequestBodyAsJson).build(); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanAccountData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanAccountData.java index d0a97a4c9..2add7b5da 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanAccountData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanAccountData.java @@ -82,8 +82,11 @@ public class LoanAccountData { private final EnumOptionData interestCalculationPeriodType; private final BigDecimal inArrearsTolerance; private final Long transactionProcessingStrategyId; - private final LocalDate expectedFirstRepaymentOnDate; + private final Integer graceOnPrincipalPayment; + private final Integer graceOnInterestPayment; + private final Integer graceOnInterestCharged; private final LocalDate interestChargedFromDate; + private final LocalDate expectedFirstRepaymentOnDate; // timeline private final LoanApplicationTimelineData timeline; @@ -157,6 +160,9 @@ public class LoanAccountData { final EnumOptionData interestType = null; final EnumOptionData interestCalculationPeriodType = null; final LocalDate expectedFirstRepaymentOnDate = null; + final Integer graceOnPrincipalPayment = null; + final Integer graceOnInterestPayment = null; + final Integer graceOnInterestCharged = null; final LocalDate interestChargedFromDate = null; final LoanApplicationTimelineData timeline = null; final LoanSummaryData summary = null; @@ -188,11 +194,11 @@ public class LoanAccountData { currencyData, principal, inArrearsTolerance, termFrequency, termPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentFrequencyType, transactionProcessingStrategyId, amortizationType, interestRatePerPeriod, interestRateFrequencyType, annualInterestRate, interestType, interestCalculationPeriodType, expectedFirstRepaymentOnDate, - interestChargedFromDate, timeline, summary, feeChargesDueAtDisbursementCharged, repaymentSchedule, transactions, charges, - collateral, guarantors, calendarData, productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, - transactionProcessingStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, - interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, - loanCollateralOptions, calendarOptions); + graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, timeline, summary, + feeChargesDueAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, calendarData, + productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, transactionProcessingStrategyOptions, + interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, + fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions); } /** @@ -233,6 +239,9 @@ public class LoanAccountData { final EnumOptionData interestType = null; final EnumOptionData interestCalculationPeriodType = null; final LocalDate expectedFirstRepaymentOnDate = null; + final Integer graceOnPrincipalPayment = null; + final Integer graceOnInterestPayment = null; + final Integer graceOnInterestCharged = null; final LocalDate interestChargedFromDate = null; final LoanApplicationTimelineData timeline = LoanApplicationTimelineData.templateDefault(expectedDisbursementDate); final LoanSummaryData summary = null; @@ -265,8 +274,9 @@ public class LoanAccountData { currencyData, principal, inArrearsTolerance, termFrequency, termPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentFrequencyType, transactionProcessingStrategyId, amortizationType, interestRatePerPeriod, interestRateFrequencyType, annualInterestRate, interestType, interestCalculationPeriodType, expectedFirstRepaymentOnDate, - interestChargedFromDate, timeline, summary, feeChargesDueAtDisbursementCharged, repaymentSchedule, transactions, charges, - collateral, guarantors, calendarData, productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, repaymentStrategyOptions, + graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, timeline, summary, + feeChargesDueAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, calendarData, + productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, repaymentStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions); } @@ -274,19 +284,20 @@ public class LoanAccountData { public static LoanAccountData populateClientDefaults(final LoanAccountData acc, final LoanAccountData clientAcc) { return new LoanAccountData(acc.id, acc.accountNo, acc.status, acc.externalId, clientAcc.clientId, clientAcc.clientName, - clientAcc.clientOfficeId, acc.group, acc.loanType, acc.loanProductId, acc.loanProductName, acc.loanProductDescription, acc.fundId, - acc.fundName, acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, acc.currency, acc.principal, - acc.inArrearsTolerance, acc.termFrequency, acc.termPeriodFrequencyType, acc.numberOfRepayments, acc.repaymentEvery, - acc.repaymentFrequencyType, acc.transactionProcessingStrategyId, acc.amortizationType, acc.interestRatePerPeriod, - acc.interestRateFrequencyType, acc.annualInterestRate, acc.interestType, acc.interestCalculationPeriodType, - acc.expectedFirstRepaymentOnDate, acc.interestChargedFromDate, clientAcc.timeline, acc.summary, - acc.feeChargesAtDisbursementCharged, acc.repaymentSchedule, acc.transactions, acc.charges, acc.collateral, acc.guarantors, acc.meeting, - acc.productOptions, acc.termFrequencyTypeOptions, acc.repaymentFrequencyTypeOptions, + clientAcc.clientOfficeId, acc.group, acc.loanType, acc.loanProductId, acc.loanProductName, acc.loanProductDescription, + acc.fundId, acc.fundName, acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, acc.currency, + acc.principal, acc.inArrearsTolerance, acc.termFrequency, acc.termPeriodFrequencyType, acc.numberOfRepayments, + acc.repaymentEvery, acc.repaymentFrequencyType, acc.transactionProcessingStrategyId, acc.amortizationType, + acc.interestRatePerPeriod, acc.interestRateFrequencyType, acc.annualInterestRate, acc.interestType, + acc.interestCalculationPeriodType, acc.expectedFirstRepaymentOnDate, acc.graceOnPrincipalPayment, + acc.graceOnInterestPayment, acc.graceOnInterestCharged, acc.interestChargedFromDate, clientAcc.timeline, acc.summary, + acc.feeChargesAtDisbursementCharged, acc.repaymentSchedule, acc.transactions, acc.charges, acc.collateral, acc.guarantors, + acc.meeting, acc.productOptions, acc.termFrequencyTypeOptions, acc.repaymentFrequencyTypeOptions, acc.transactionProcessingStrategyOptions, acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions, acc.interestTypeOptions, acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, acc.loanOfficerOptions, acc.loanPurposeOptions, acc.loanCollateralOptions, acc.calendarOptions); } - + /** * Used to produce a {@link LoanAccountData} with only group information * defaulted. @@ -326,6 +337,9 @@ public class LoanAccountData { final EnumOptionData interestType = null; final EnumOptionData interestCalculationPeriodType = null; final LocalDate expectedFirstRepaymentOnDate = null; + final Integer graceOnPrincipalPayment = null; + final Integer graceOnInterestPayment = null; + final Integer graceOnInterestCharged = null; final LocalDate interestChargedFromDate = null; final LoanApplicationTimelineData timeline = LoanApplicationTimelineData.templateDefault(expectedDisbursementDate); final LoanSummaryData summary = null; @@ -358,28 +372,30 @@ public class LoanAccountData { currencyData, principal, inArrearsTolerance, termFrequency, termPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentFrequencyType, transactionProcessingStrategyId, amortizationType, interestRatePerPeriod, interestRateFrequencyType, annualInterestRate, interestType, interestCalculationPeriodType, expectedFirstRepaymentOnDate, - interestChargedFromDate, timeline, summary, feeChargesDueAtDisbursementCharged, repaymentSchedule, transactions, charges, - collateral, guarantors, calendarData, productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, repaymentStrategyOptions, + graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, timeline, summary, + feeChargesDueAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, calendarData, + productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, repaymentStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions); } public static LoanAccountData populateGroupDefaults(final LoanAccountData acc, final LoanAccountData groupAcc) { - return new LoanAccountData(acc.id, acc.accountNo, acc.status, acc.externalId, acc.clientId, acc.clientName, - acc.clientOfficeId, groupAcc.group, acc.loanType, acc.loanProductId, acc.loanProductName, acc.loanProductDescription, acc.fundId, - acc.fundName, acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, acc.currency, acc.principal, + return new LoanAccountData(acc.id, acc.accountNo, acc.status, acc.externalId, acc.clientId, acc.clientName, acc.clientOfficeId, + groupAcc.group, acc.loanType, acc.loanProductId, acc.loanProductName, acc.loanProductDescription, acc.fundId, acc.fundName, + acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, acc.currency, acc.principal, acc.inArrearsTolerance, acc.termFrequency, acc.termPeriodFrequencyType, acc.numberOfRepayments, acc.repaymentEvery, acc.repaymentFrequencyType, acc.transactionProcessingStrategyId, acc.amortizationType, acc.interestRatePerPeriod, acc.interestRateFrequencyType, acc.annualInterestRate, acc.interestType, acc.interestCalculationPeriodType, - acc.expectedFirstRepaymentOnDate, acc.interestChargedFromDate, groupAcc.timeline, acc.summary, - acc.feeChargesAtDisbursementCharged, acc.repaymentSchedule, acc.transactions, acc.charges, acc.collateral, acc.guarantors, acc.meeting, - acc.productOptions, acc.termFrequencyTypeOptions, acc.repaymentFrequencyTypeOptions, - acc.transactionProcessingStrategyOptions, acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions, - acc.interestTypeOptions, acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, - acc.loanOfficerOptions, acc.loanPurposeOptions, acc.loanCollateralOptions, acc.calendarOptions); + acc.expectedFirstRepaymentOnDate, acc.graceOnPrincipalPayment, acc.graceOnInterestPayment, acc.graceOnInterestCharged, + acc.interestChargedFromDate, groupAcc.timeline, acc.summary, acc.feeChargesAtDisbursementCharged, acc.repaymentSchedule, + acc.transactions, acc.charges, acc.collateral, acc.guarantors, acc.meeting, acc.productOptions, + acc.termFrequencyTypeOptions, acc.repaymentFrequencyTypeOptions, acc.transactionProcessingStrategyOptions, + acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions, acc.interestTypeOptions, + acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, acc.loanOfficerOptions, + acc.loanPurposeOptions, acc.loanCollateralOptions, acc.calendarOptions); } - + public static LoanAccountData loanProductWithTemplateDefaults(final LoanProductData product, final Collection termFrequencyTypeOptions, final Collection repaymentFrequencyTypeOptions, final Collection repaymentStrategyOptions, @@ -387,7 +403,7 @@ public class LoanAccountData { final Collection interestTypeOptions, final Collection interestCalculationPeriodTypeOptions, final Collection fundOptions, final Collection chargeOptions, final Collection loanPurposeOptions, final Collection loanCollateralOptions) { - + final Long id = null; final String accountNo = null; final LoanStatusEnumData status = null; @@ -423,7 +439,7 @@ public class LoanAccountData { final LoanSummaryData summary = null; final BigDecimal feeChargesDueAtDisbursementCharged = null; final ChargeData chargeTemplate = null; - + final LoanScheduleData repaymentSchedule = null; final Collection transactions = null; final Collection collateral = null; @@ -432,7 +448,7 @@ public class LoanAccountData { final Collection productOptions = null; final Collection calendarOptions = null; final Collection loanOfficerOptions = null; - + final Integer termFrequency = product.getNumberOfRepayments() * product.getRepaymentEvery(); final EnumOptionData termPeriodFrequencyType = product.getRepaymentFrequencyType(); @@ -441,20 +457,21 @@ public class LoanAccountData { charges.add(charge.toLoanChargeData()); } - return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, - group, loanType, product.getId(), product.getName(), product.getDescription(), product.getFundId(), product.getFundName(), - loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName, product.getCurrency(), - product.getPrincipal(), product.getInArrearsTolerance(), termFrequency, termPeriodFrequencyType, - product.getNumberOfRepayments(), product.getRepaymentEvery(), product.getRepaymentFrequencyType(), - product.getTransactionProcessingStrategyId(), product.getAmortizationType(), product.getInterestRatePerPeriod(), - product.getInterestRateFrequencyType(), product.getAnnualInterestRate(), product.getInterestType(), - product.getInterestCalculationPeriodType(), expectedFirstRepaymentOnDate, interestChargedFromDate, timeline, - summary, feeChargesDueAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, calendarData, - productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, repaymentStrategyOptions, - interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, - fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions); + return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, group, loanType, + product.getId(), product.getName(), product.getDescription(), product.getFundId(), product.getFundName(), loanPurposeId, + loanPurposeName, loanOfficerId, loanOfficerName, product.getCurrency(), product.getPrincipal(), + product.getInArrearsTolerance(), termFrequency, termPeriodFrequencyType, product.getNumberOfRepayments(), + product.getRepaymentEvery(), product.getRepaymentFrequencyType(), product.getTransactionProcessingStrategyId(), + product.getAmortizationType(), product.getInterestRatePerPeriod(), product.getInterestRateFrequencyType(), + product.getAnnualInterestRate(), product.getInterestType(), product.getInterestCalculationPeriodType(), + expectedFirstRepaymentOnDate, product.getGraceOnPrincipalPayment(), product.getGraceOnInterestPayment(), + product.getGraceOnInterestCharged(), interestChargedFromDate, timeline, summary, feeChargesDueAtDisbursementCharged, + repaymentSchedule, transactions, charges, collateral, guarantors, calendarData, productOptions, termFrequencyTypeOptions, + repaymentFrequencyTypeOptions, repaymentStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, + interestTypeOptions, interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, + loanPurposeOptions, loanCollateralOptions, calendarOptions); } - + public static LoanAccountData populateLoanProductDefaults(final LoanAccountData acc, final LoanProductData product) { final LoanScheduleData repaymentSchedule = null; @@ -477,7 +494,7 @@ public class LoanAccountData { final Collection loanPurposeOptions = null; final Collection loanCollateralOptions = null; final Collection calendarOptions = null; - + final Integer termFrequency = product.getNumberOfRepayments() * product.getRepaymentEvery(); final EnumOptionData termPeriodFrequencyType = product.getRepaymentFrequencyType(); @@ -487,15 +504,16 @@ public class LoanAccountData { } return new LoanAccountData(acc.id, acc.accountNo, acc.status, acc.externalId, acc.clientId, acc.clientName, acc.clientOfficeId, - acc.group, acc.loanType, product.getId(), product.getName(), product.getDescription(), product.getFundId(), product.getFundName(), - acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, product.getCurrency(), - product.getPrincipal(), product.getInArrearsTolerance(), termFrequency, termPeriodFrequencyType, + acc.group, acc.loanType, product.getId(), product.getName(), product.getDescription(), product.getFundId(), + product.getFundName(), acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, + product.getCurrency(), product.getPrincipal(), product.getInArrearsTolerance(), termFrequency, termPeriodFrequencyType, product.getNumberOfRepayments(), product.getRepaymentEvery(), product.getRepaymentFrequencyType(), product.getTransactionProcessingStrategyId(), product.getAmortizationType(), product.getInterestRatePerPeriod(), product.getInterestRateFrequencyType(), product.getAnnualInterestRate(), product.getInterestType(), - product.getInterestCalculationPeriodType(), acc.expectedFirstRepaymentOnDate, acc.interestChargedFromDate, acc.timeline, - acc.summary, acc.feeChargesAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, calendarData, - productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, repaymentStrategyOptions, + product.getInterestCalculationPeriodType(), acc.expectedFirstRepaymentOnDate, product.getGraceOnPrincipalPayment(), + product.getGraceOnInterestPayment(), product.getGraceOnInterestCharged(), acc.interestChargedFromDate, acc.timeline, + acc.summary, acc.feeChargesAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, + calendarData, productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, repaymentStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions); } @@ -506,16 +524,16 @@ public class LoanAccountData { */ public static LoanAccountData basicLoanDetails(final Long id, final String accountNo, final LoanStatusEnumData status, final String externalId, final Long clientId, final String clientName, final Long clientOfficeId, final GroupGeneralData group, - final EnumOptionData loanType, final Long loanProductId, final String loanProductName, final String loanProductDescription, final Long fundId, - final String fundName, final Long loanPurposeId, final String loanPurposeName, final Long loanOfficerId, + final EnumOptionData loanType, final Long loanProductId, final String loanProductName, final String loanProductDescription, + final Long fundId, final String fundName, final Long loanPurposeId, final String loanPurposeName, final Long loanOfficerId, final String loanOfficerName, final CurrencyData currencyData, final BigDecimal principal, final BigDecimal inArrearsTolerance, final Integer termFrequency, final EnumOptionData termPeriodFrequencyType, final Integer numberOfRepayments, final Integer repaymentEvery, final EnumOptionData repaymentFrequencyType, final Long transactionStrategyId, final EnumOptionData amortizationType, final BigDecimal interestRatePerPeriod, final EnumOptionData interestRateFrequencyType, final BigDecimal annualInterestRate, final EnumOptionData interestType, final EnumOptionData interestCalculationPeriodType, - final LocalDate expectedFirstRepaymentOnDate, final LocalDate interestChargedFromDate, - final LoanApplicationTimelineData timeline, final LoanSummaryData loanSummary, - final BigDecimal feeChargesDueAtDisbursementCharged) { + final LocalDate expectedFirstRepaymentOnDate, final Integer graceOnPrincipalPayment, final Integer graceOnInterestPayment, + final Integer graceOnInterestCharged, final LocalDate interestChargedFromDate, final LoanApplicationTimelineData timeline, + final LoanSummaryData loanSummary, final BigDecimal feeChargesDueAtDisbursementCharged) { final LoanScheduleData repaymentSchedule = null; final Collection transactions = null; @@ -538,14 +556,15 @@ public class LoanAccountData { final Collection loanPurposeOptions = null; final Collection loanCollateralOptions = null; final Collection calendarOptions = null; - + return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, group, loanType, loanProductId, loanProductName, loanProductDescription, fundId, fundName, loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName, currencyData, principal, inArrearsTolerance, termFrequency, termPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentFrequencyType, transactionStrategyId, amortizationType, interestRatePerPeriod, interestRateFrequencyType, - annualInterestRate, interestType, interestCalculationPeriodType, expectedFirstRepaymentOnDate, interestChargedFromDate, - timeline, loanSummary, feeChargesDueAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, - guarantors, calendarData, productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, repaymentStrategyOptions, + annualInterestRate, interestType, interestCalculationPeriodType, expectedFirstRepaymentOnDate, graceOnPrincipalPayment, + graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, timeline, loanSummary, + feeChargesDueAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, calendarData, + productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, repaymentStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions); } @@ -566,41 +585,44 @@ public class LoanAccountData { final Collection loanOfficerOptions, final Collection loanPurposeOptions, final Collection loanCollateralOptions, final Collection calendarOptions) { return new LoanAccountData(acc.id, acc.accountNo, acc.status, acc.externalId, acc.clientId, acc.clientName, acc.clientOfficeId, - acc.group, acc.loanType, acc.loanProductId, acc.loanProductName, acc.loanProductDescription, acc.fundId, acc.fundName, acc.loanPurposeId, - acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, acc.currency, acc.principal, acc.inArrearsTolerance, - acc.termFrequency, acc.termPeriodFrequencyType, acc.numberOfRepayments, acc.repaymentEvery, acc.repaymentFrequencyType, - acc.transactionProcessingStrategyId, acc.amortizationType, acc.interestRatePerPeriod, acc.interestRateFrequencyType, - acc.annualInterestRate, acc.interestType, acc.interestCalculationPeriodType, acc.expectedFirstRepaymentOnDate, + acc.group, acc.loanType, acc.loanProductId, acc.loanProductName, acc.loanProductDescription, acc.fundId, acc.fundName, + acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, acc.currency, acc.principal, + acc.inArrearsTolerance, acc.termFrequency, acc.termPeriodFrequencyType, acc.numberOfRepayments, acc.repaymentEvery, + acc.repaymentFrequencyType, acc.transactionProcessingStrategyId, acc.amortizationType, acc.interestRatePerPeriod, + acc.interestRateFrequencyType, acc.annualInterestRate, acc.interestType, acc.interestCalculationPeriodType, + acc.expectedFirstRepaymentOnDate, acc.graceOnPrincipalPayment, acc.graceOnInterestPayment, acc.graceOnInterestCharged, acc.interestChargedFromDate, acc.timeline, acc.summary, acc.feeChargesAtDisbursementCharged, repaymentSchedule, - transactions, charges, collateral, guarantors, calendarData, productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, - transactionProcessingStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, - interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, - loanCollateralOptions, calendarOptions); + transactions, charges, collateral, guarantors, calendarData, productOptions, termFrequencyTypeOptions, + repaymentFrequencyTypeOptions, transactionProcessingStrategyOptions, interestRateFrequencyTypeOptions, + amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, + chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions); } - public static LoanAccountData associationsAndTemplate(LoanAccountData acc, Collection productOptions, - Collection allowedLoanOfficers, final Collection calendarOptions) { - return associationsAndTemplate(acc, acc.repaymentSchedule, acc.transactions, acc.charges, acc.collateral, acc.guarantors, acc.meeting, - productOptions, acc.termFrequencyTypeOptions, acc.repaymentFrequencyTypeOptions, acc.transactionProcessingStrategyOptions, - acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions, acc.interestTypeOptions, - acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, allowedLoanOfficers, - acc.loanPurposeOptions, acc.loanCollateralOptions, calendarOptions); + public static LoanAccountData associationsAndTemplate(final LoanAccountData acc, final Collection productOptions, + final Collection allowedLoanOfficers, final Collection calendarOptions) { + return associationsAndTemplate(acc, acc.repaymentSchedule, acc.transactions, acc.charges, acc.collateral, acc.guarantors, + acc.meeting, productOptions, acc.termFrequencyTypeOptions, acc.repaymentFrequencyTypeOptions, + acc.transactionProcessingStrategyOptions, acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions, + acc.interestTypeOptions, acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, + allowedLoanOfficers, acc.loanPurposeOptions, acc.loanCollateralOptions, calendarOptions); } - + public static LoanAccountData associateGroup(final LoanAccountData acc, final GroupGeneralData group) { return new LoanAccountData(acc.id, acc.accountNo, acc.status, acc.externalId, acc.clientId, acc.clientName, acc.clientOfficeId, - group, acc.loanType, acc.loanProductId, acc.loanProductName, acc.loanProductDescription, acc.fundId, acc.fundName, acc.loanPurposeId, - acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, acc.currency, acc.principal, acc.inArrearsTolerance, - acc.termFrequency, acc.termPeriodFrequencyType, acc.numberOfRepayments, acc.repaymentEvery, acc.repaymentFrequencyType, - acc.transactionProcessingStrategyId, acc.amortizationType, acc.interestRatePerPeriod, acc.interestRateFrequencyType, - acc.annualInterestRate, acc.interestType, acc.interestCalculationPeriodType, acc.expectedFirstRepaymentOnDate, + group, acc.loanType, acc.loanProductId, acc.loanProductName, acc.loanProductDescription, acc.fundId, acc.fundName, + acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, acc.currency, acc.principal, + acc.inArrearsTolerance, acc.termFrequency, acc.termPeriodFrequencyType, acc.numberOfRepayments, acc.repaymentEvery, + acc.repaymentFrequencyType, acc.transactionProcessingStrategyId, acc.amortizationType, acc.interestRatePerPeriod, + acc.interestRateFrequencyType, acc.annualInterestRate, acc.interestType, acc.interestCalculationPeriodType, + acc.expectedFirstRepaymentOnDate, acc.graceOnPrincipalPayment, acc.graceOnInterestPayment, acc.graceOnInterestCharged, acc.interestChargedFromDate, acc.timeline, acc.summary, acc.feeChargesAtDisbursementCharged, acc.repaymentSchedule, - acc.transactions, acc.charges, acc.collateral, acc.guarantors, acc.meeting, acc.productOptions, acc.termFrequencyTypeOptions, acc.repaymentFrequencyTypeOptions, - acc.transactionProcessingStrategyOptions, acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions, acc.interestTypeOptions, - acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, acc.loanOfficerOptions, acc.loanPurposeOptions, - acc.loanCollateralOptions, acc.calendarOptions); + acc.transactions, acc.charges, acc.collateral, acc.guarantors, acc.meeting, acc.productOptions, + acc.termFrequencyTypeOptions, acc.repaymentFrequencyTypeOptions, acc.transactionProcessingStrategyOptions, + acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions, acc.interestTypeOptions, + acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, acc.loanOfficerOptions, + acc.loanPurposeOptions, acc.loanCollateralOptions, acc.calendarOptions); } - + private LoanAccountData( final Long id, // final String accountNo, // @@ -631,6 +653,7 @@ public class LoanAccountData { final Long transactionProcessingStrategyId, final EnumOptionData amortizationType, final BigDecimal interestRatePerPeriod, final EnumOptionData interestRateFrequencyType, final BigDecimal annualInterestRate, final EnumOptionData interestType, final EnumOptionData interestCalculationPeriodType, final LocalDate expectedFirstRepaymentOnDate, + final Integer graceOnPrincipalPayment, final Integer graceOnInterestPayment, final Integer graceOnInterestCharged, final LocalDate interestChargedFromDate, final LoanApplicationTimelineData timeline, final LoanSummaryData summary, final BigDecimal feeChargesDueAtDisbursementCharged, final LoanScheduleData repaymentSchedule, final Collection transactions, final Collection charges, @@ -677,6 +700,9 @@ public class LoanAccountData { this.interestType = interestType; this.interestCalculationPeriodType = interestCalculationPeriodType; this.expectedFirstRepaymentOnDate = expectedFirstRepaymentOnDate; + this.graceOnPrincipalPayment = graceOnPrincipalPayment; + this.graceOnInterestPayment = graceOnInterestPayment; + this.graceOnInterestCharged = graceOnInterestCharged; this.interestChargedFromDate = interestChargedFromDate; this.timeline = timeline; this.feeChargesAtDisbursementCharged = feeChargesDueAtDisbursementCharged; @@ -736,7 +762,7 @@ public class LoanAccountData { } else { this.loanCollateralOptions = loanCollateralOptions; } - + if (CollectionUtils.isEmpty(calendarOptions)) { this.calendarOptions = null; } else { @@ -760,12 +786,12 @@ public class LoanAccountData { public Collection charges() { return this.charges; } - - public Long groupOfficeId(){ - return group == null? null : group.officeId(); + + public Long groupOfficeId() { + return group == null ? null : group.officeId(); } - - public Long groupId(){ - return group == null? null : group.getId(); + + public Long groupId() { + return group == null ? null : group.getId(); } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java index b6df12500..1a59b08fa 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java @@ -6,6 +6,8 @@ package org.mifosplatform.portfolio.loanaccount.domain; import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -61,14 +63,12 @@ import org.mifosplatform.portfolio.loanaccount.exception.InvalidLoanTransactionT import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerAssignmentDateException; import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerAssignmentException; import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerUnassignmentDateException; -import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData; -import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.AprCalculator; -import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.DefaultLoanScheduleGeneratorFactory; -import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanSchedule; +import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator; -import org.mifosplatform.portfolio.loanproduct.domain.AmortizationMethod; -import org.mifosplatform.portfolio.loanproduct.domain.InterestCalculationPeriodMethod; +import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory; +import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; +import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod; import org.mifosplatform.portfolio.loanproduct.domain.InterestMethod; import org.mifosplatform.portfolio.loanproduct.domain.LoanProduct; import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; @@ -266,21 +266,20 @@ public class Loan extends AbstractPersistable { 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, final LoanSchedule loanSchedule, - final Set loanCharges, final Set collateral) { + final LoanTransactionProcessingStrategy transactionProcessingStrategy, + final LoanProductRelatedDetail loanRepaymentScheduleDetail, final Set loanCharges, + final Set collateral) { final LoanStatus status = null; final Group group = null; - LoanProductRelatedDetail loanRepaymentScheduleDetail = loanSchedule.loanProductRelatedDetail(); return new Loan(accountNo, client, group, loanType, fund, officer, loanPurpose, transactionProcessingStrategy, loanProduct, loanRepaymentScheduleDetail, status, loanCharges, collateral); } public static Loan newGroupLoanApplication(final String accountNo, final Group group, final Integer loanType, final LoanProduct loanProduct, final Fund fund, final Staff officer, - final LoanTransactionProcessingStrategy transactionProcessingStrategy, final LoanSchedule loanSchedule, - final Set loanCharges) { + final LoanTransactionProcessingStrategy transactionProcessingStrategy, + final LoanProductRelatedDetail loanRepaymentScheduleDetail, final Set loanCharges) { final LoanStatus status = null; - final LoanProductRelatedDetail loanRepaymentScheduleDetail = loanSchedule.loanProductRelatedDetail(); final CodeValue loanPurpose = null; final Set collateral = null; final Client client = null; @@ -290,10 +289,9 @@ public class Loan extends AbstractPersistable { public static Loan newIndividualLoanApplicationFromGroup(final String accountNo, final Client client, final Group group, final Integer loanType, final LoanProduct loanProduct, final Fund fund, final Staff officer, - final LoanTransactionProcessingStrategy transactionProcessingStrategy, final LoanSchedule loanSchedule, - final Set loanCharges) { + final LoanTransactionProcessingStrategy transactionProcessingStrategy, + final LoanProductRelatedDetail loanRepaymentScheduleDetail, final Set loanCharges) { final LoanStatus status = null; - final LoanProductRelatedDetail loanRepaymentScheduleDetail = loanSchedule.loanProductRelatedDetail(); final CodeValue loanPurpose = null; final Set collateral = null; return new Loan(accountNo, client, group, loanType, fund, officer, loanPurpose, transactionProcessingStrategy, loanProduct, @@ -308,6 +306,10 @@ public class Loan extends AbstractPersistable { final Staff loanOfficer, final CodeValue loanPurpose, final LoanTransactionProcessingStrategy transactionProcessingStrategy, final LoanProduct loanProduct, final LoanProductRelatedDetail loanRepaymentScheduleDetail, final LoanStatus loanStatus, final Set loanCharges, final Set collateral) { + + this.loanRepaymentScheduleDetail = loanRepaymentScheduleDetail; + this.loanRepaymentScheduleDetail.validateRepaymentPeriodWithGraceSettings(); + if (StringUtils.isBlank(accountNo)) { this.accountNumber = new RandomPasswordGenerator(19).generate(); this.accountNumberRequiresAutoGeneration = true; @@ -323,7 +325,6 @@ public class Loan extends AbstractPersistable { this.transactionProcessingStrategy = transactionProcessingStrategy; this.loanProduct = loanProduct; - this.loanRepaymentScheduleDetail = loanRepaymentScheduleDetail; if (loanStatus != null) { this.loanStatus = loanStatus.getValue(); } else { @@ -735,10 +736,10 @@ public class Loan extends AbstractPersistable { this.collateral.addAll(associateWithThisLoan(loanCollateral)); } - public void updateLoanSchedule(final LoanScheduleData modifiedLoanSchedule) { + public void updateLoanSchedule(final LoanScheduleModel modifiedLoanSchedule) { this.repaymentScheduleInstallments.clear(); - for (LoanSchedulePeriodData scheduledLoanInstallment : modifiedLoanSchedule.getPeriods()) { + for (LoanScheduleModelPeriod scheduledLoanInstallment : modifiedLoanSchedule.getPeriods()) { if (scheduledLoanInstallment.isRepaymentPeriod()) { LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(this, @@ -750,24 +751,6 @@ public class Loan extends AbstractPersistable { } } - // if the loan application/contract is modified when repayments are - // already against it - then need to re-process it - - /**** - * TODO Vishwas Currently we do not allow modifying a Loan contract once - * the loan is approved (hence there is no need to adjust any loan - * transactions). - * - * Consider removing this block of code or logically completing it for - * the future by getting the list of affected Transactions - ***/ - final List repaymentsOrWaivers = retreiveListOfTransactionsPostDisbursement(); - - final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory - .determineProcessor(this.transactionProcessingStrategy); - loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(), repaymentsOrWaivers, getCurrency(), - this.repaymentScheduleInstallments, setOfLoanCharges()); - updateLoanScheduleDependentDerivedFields(); updateLoanSummaryDerivedFields(); } @@ -1042,18 +1025,11 @@ public class Loan extends AbstractPersistable { } } - public void loanApplicationSubmittal(final AppUser currentUser, final LoanSchedule loanSchedule, - final LoanLifecycleStateMachine lifecycleStateMachine, final LocalDate submittedOn, final String externalId) { + public void loanApplicationSubmittal(final AppUser currentUser, final LoanScheduleModel loanSchedule, + final LoanApplicationTerms loanApplicationTerms, final LoanLifecycleStateMachine lifecycleStateMachine, + final LocalDate submittedOn, final String externalId) { - final LoanScheduleData loanScheduleData = loanSchedule.generate(); - - // Have to set expectedDisbursementDate to avoid nullPointer so should - // be passed down to updateLoanSchedule method - if (loanSchedule.getDisbursementDate() != null) { - this.expectedDisbursementDate = loanSchedule.getDisbursementDate().toDate(); - } - - updateLoanSchedule(loanScheduleData); + updateLoanSchedule(loanSchedule); LoanStatus from = null; if (this.loanStatus != null) { @@ -1064,22 +1040,16 @@ public class Loan extends AbstractPersistable { this.loanStatus = statusEnum.getValue(); this.externalId = externalId; - - this.termFrequency = loanSchedule.getLoanTermFrequency(); - this.termPeriodFrequencyType = loanSchedule.getLoanTermPeriodFrequencyType().getValue(); - + this.termFrequency = loanApplicationTerms.getLoanTermFrequency(); + this.termPeriodFrequencyType = loanApplicationTerms.getLoanTermPeriodFrequencyType().getValue(); this.submittedOnDate = submittedOn.toDate(); this.submittedBy = currentUser; + this.expectedDisbursementDate = loanApplicationTerms.getExpectedDisbursementDate().toDate(); + this.expectedFirstRepaymentOnDate = loanApplicationTerms.getRepaymentStartFromDate(); + this.interestChargedFromDate = loanApplicationTerms.getInterestChargedFromDate(); + updateLoanScheduleDependentDerivedFields(); - if (loanSchedule.getRepaymentStartFromDate() != null) { - this.expectedFirstRepaymentOnDate = loanSchedule.getRepaymentStartFromDate().toDate(); - } - - if (loanSchedule.getInterestChargedFromDate() != null) { - this.interestChargedFromDate = loanSchedule.getInterestChargedFromDate().toDate(); - } - if (submittedOn.isAfter(DateUtils.getLocalDateOfTenant())) { final String errorMessage = "The date on which a loan is submitted cannot be in the future."; throw new InvalidLoanStateTransitionException("submittal", "cannot.be.a.future.date", errorMessage, submittedOn); @@ -1297,9 +1267,9 @@ public class Loan extends AbstractPersistable { return ids; } - public Map disburse(final AppUser currentUser, final JsonCommand command, final ApplicationCurrency currency, - final List existingTransactionIds, final List existingReversedTransactionIds, - final Map actualChanges, final PaymentDetail paymentDetail) { + public Map disburse(final LoanScheduleGeneratorFactory loanScheduleFactory, final AppUser currentUser, + final JsonCommand command, final ApplicationCurrency currency, final List existingTransactionIds, + final List existingReversedTransactionIds, final Map actualChanges, final PaymentDetail paymentDetail) { updateLoanToPreDisbursalState(); @@ -1325,7 +1295,7 @@ public class Loan extends AbstractPersistable { handleDisbursementTransaction(paymentDetail, actualDisbursementDate); if (isRepaymentScheduleRegenerationRequiredForDisbursement(actualDisbursementDate)) { - regenerateRepaymentSchedule(currency); + regenerateRepaymentSchedule(loanScheduleFactory, currency); } else { updateLoanSummaryDerivedFields(); } @@ -1349,35 +1319,25 @@ public class Loan extends AbstractPersistable { * Ability to regenerate the repayment schedule based on the loans current * details/state. */ - private void regenerateRepaymentSchedule(final ApplicationCurrency applicationCurrency) { + private void regenerateRepaymentSchedule(final LoanScheduleGeneratorFactory loanScheduleFactory, + final ApplicationCurrency applicationCurrency) { final InterestMethod interestMethod = this.loanRepaymentScheduleDetail.getInterestMethod(); - final LoanScheduleGenerator loanScheduleGenerator = new DefaultLoanScheduleGeneratorFactory().create(interestMethod); + final LoanScheduleGenerator loanScheduleGenerator = loanScheduleFactory.create(interestMethod); - final BigDecimal principal = this.loanRepaymentScheduleDetail.getPrincipal().getAmount(); - final BigDecimal inArrearsTolerance = this.loanRepaymentScheduleDetail.getInArrearsTolerance().getAmount(); - final BigDecimal interestRatePerPeriod = this.loanRepaymentScheduleDetail.getNominalInterestRatePerPeriod(); - final PeriodFrequencyType interestRatePeriodFrequencyType = this.loanRepaymentScheduleDetail.getInterestPeriodFrequencyType(); - final BigDecimal defaultAnnualNominalInterestRate = this.loanRepaymentScheduleDetail.getAnnualNominalInterestRate(); + final RoundingMode roundingMode = RoundingMode.HALF_EVEN; + final MathContext mc = new MathContext(8, roundingMode); - final InterestCalculationPeriodMethod interestCalculationPeriodMethod = this.loanRepaymentScheduleDetail - .getInterestCalculationPeriodMethod(); - final Integer repaymentEvery = this.loanRepaymentScheduleDetail.getRepayEvery(); - final PeriodFrequencyType repaymentPeriodFrequencyType = this.loanRepaymentScheduleDetail.getRepaymentPeriodFrequencyType(); - final Integer numberOfRepayments = this.loanRepaymentScheduleDetail.getNumberOfRepayments(); - final AmortizationMethod amortizationMethod = this.loanRepaymentScheduleDetail.getAmortizationMethod(); final Integer loanTermFrequency = this.termFrequency; final PeriodFrequencyType loanTermPeriodFrequencyType = PeriodFrequencyType.fromInt(this.termPeriodFrequencyType); - final LoanSchedule loanSchedule = new LoanSchedule(loanScheduleGenerator, applicationCurrency, principal, interestRatePerPeriod, - interestRatePeriodFrequencyType, defaultAnnualNominalInterestRate, interestMethod, interestCalculationPeriodMethod, - repaymentEvery, repaymentPeriodFrequencyType, numberOfRepayments, amortizationMethod, loanTermFrequency, - loanTermPeriodFrequencyType, setOfLoanCharges(), this.getDisbursementDate(), this.getExpectedFirstRepaymentOnDate(), - this.getInterestChargedFromDate(), inArrearsTolerance); + LoanApplicationTerms loanApplicationTerms = LoanApplicationTerms.assembleFrom(applicationCurrency, loanTermFrequency, + loanTermPeriodFrequencyType, getExpectedDisbursedOnLocalDate(), getExpectedFirstRepaymentOnDate(), getInArrearsTolerance(), + this.loanRepaymentScheduleDetail); - final LoanScheduleData generatedData = loanSchedule.generate(); + LoanScheduleModel loanSchedule = loanScheduleGenerator.generate(mc, applicationCurrency, loanApplicationTerms, this.charges); - updateLoanSchedule(generatedData); + updateLoanSchedule(loanSchedule); } private LoanTransaction handleDisbursementTransaction(final PaymentDetail paymentDetail, final LocalDate disbursedOn) { @@ -1433,14 +1393,6 @@ public class Loan extends AbstractPersistable { throw new InvalidLoanStateTransitionException("disbursal", "cannot.be.a.future.date", errorMessage, disbursedOn); } - LocalDate firstRepaymentDueDate = this.repaymentScheduleInstallments.get(0).getDueDate(); - if (disbursedOn.isAfter(firstRepaymentDueDate)) { - final String errorMessage = "The date on which a loan with identifier : " + this.accountNumber - + " is disbursed cannot be after the first expected repayment date: " + firstRepaymentDueDate.toString(); - throw new InvalidLoanStateTransitionException("disbursal", "cannot.be.after.first.repayment.due.date", errorMessage, - this.accountNumber, disbursedOn, firstRepaymentDueDate); - } - return disbursementTransaction; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java index 17bed9155..6729921d3 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java @@ -286,14 +286,6 @@ public class LoanSchedulePeriodData { return value; } - public boolean isRepaymentPeriod() { - boolean isRepaymentPeriod = false; - if (principalDue != null && this.interestDue != null) { - isRepaymentPeriod = BigDecimal.ZERO.compareTo(this.principalDue) == -1 || BigDecimal.ZERO.compareTo(this.interestDue) == -1; - } - return isRepaymentPeriod; - } - public Integer periodNumber() { return this.period; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java new file mode 100644 index 000000000..8a342c5cd --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java @@ -0,0 +1,188 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import org.joda.time.Days; +import org.joda.time.LocalDate; +import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; +import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; +import org.mifosplatform.organisation.monetary.domain.Money; +import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; + +/** + * + */ +public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGenerator { + + private final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator(); + private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator = new DefaultPaymentPeriodsInOneYearCalculator(); + + @Override + public LoanScheduleModel generate(final MathContext mc, final ApplicationCurrency applicationCurrency, + final LoanApplicationTerms loanApplicationTerms, final Set loanCharges) { + + // 1. generate list of proposed schedule due dates + final List scheduledDates = this.scheduledDateGenerator.generate(loanApplicationTerms); + + // 2. determine the total charges due at time of disbursement + final BigDecimal chargesDueAtTimeOfDisbursement = deriveTotalChargesDueAtTimeOfDisbursement(loanCharges); + + // 3. setup variables for tracking important facts required for loan + // schedule generation. + final Money principalDisbursed = loanApplicationTerms.getPrincipal(); + final MonetaryCurrency currency = principalDisbursed.getCurrency(); + final int numberOfRepayments = loanApplicationTerms.getNumberOfRepayments(); + + // variables for cumulative totals + int loanTermInDays = Integer.valueOf(0); + BigDecimal totalPrincipalExpected = BigDecimal.ZERO; + BigDecimal totalPrincipalPaid = BigDecimal.ZERO; + BigDecimal totalInterestCharged = BigDecimal.ZERO; + BigDecimal totalFeeChargesCharged = chargesDueAtTimeOfDisbursement; + BigDecimal totalPenaltyChargesCharged = BigDecimal.ZERO; + BigDecimal totalRepaymentExpected = chargesDueAtTimeOfDisbursement; + BigDecimal totalOutstanding = BigDecimal.ZERO; + + final Collection periods = createNewLoanScheduleListWithDisbursementDetails(numberOfRepayments, + loanApplicationTerms, chargesDueAtTimeOfDisbursement); + + // 4. Determine the total interest owed over the full loan. + final Money totalInterestChargedForFullLoanTerm = loanApplicationTerms.calculateTotalInterestCharged( + this.paymentPeriodsInOneYearCalculator, mc); + + // 5. loop over the known set of 'scheduled due dates' + LocalDate repaymentPeriodStartDate = this.scheduledDateGenerator.idealDisbursementDateBasedOnFirstRepaymentDate( + loanApplicationTerms.getLoanTermPeriodFrequencyType(), loanApplicationTerms.getRepaymentEvery(), scheduledDates); + + LocalDate startDate = loanApplicationTerms.getExpectedDisbursementDate(); + int periodNumber = 1; + Money totalCumulativePrincipal = principalDisbursed.zero(); + Money totalCumulativeInterest = principalDisbursed.zero(); + Money totalOutstandingInterestPaymentDueToGrace = principalDisbursed.zero(); + Money outstandingBalance = principalDisbursed; + for (LocalDate scheduledDueDate : scheduledDates) { + + final int daysInPeriod = Days.daysBetween(startDate, scheduledDueDate).getDays(); + + double interestCalculationGraceOnRepaymentPeriodFraction = this.paymentPeriodsInOneYearCalculator + .calculatePortionOfRepaymentPeriodInterestChargingGrace(repaymentPeriodStartDate, scheduledDueDate, + loanApplicationTerms.getInterestChargedFromLocalDate(), loanApplicationTerms.getLoanTermPeriodFrequencyType(), + loanApplicationTerms.getRepaymentEvery()); + + // 5 determine principal,interest of repayment period + PrincipalInterest principalInterestForThisPeriod = calculatePrincipalInterestComponentsForPeriod( + this.paymentPeriodsInOneYearCalculator, interestCalculationGraceOnRepaymentPeriodFraction, totalCumulativePrincipal, + totalCumulativeInterest, totalInterestChargedForFullLoanTerm, totalOutstandingInterestPaymentDueToGrace, daysInPeriod, + outstandingBalance, loanApplicationTerms, periodNumber, mc); + + // update cumulative fields for principal & interest + totalCumulativePrincipal = totalCumulativePrincipal.plus(principalInterestForThisPeriod.principal()); + totalCumulativeInterest = totalCumulativeInterest.plus(principalInterestForThisPeriod.interest()); + totalOutstandingInterestPaymentDueToGrace = principalInterestForThisPeriod.interestPaymentDueToGrace(); + + // 6. update outstandingLoanBlance using correct 'principalDue' + outstandingBalance = outstandingBalance.minus(principalInterestForThisPeriod.principal()); + + // 7. determine fees and penalties + final Money feeChargesForInstallment = cumulativeFeeChargesDueWithin(startDate, scheduledDueDate, loanCharges, currency); + final Money penaltyChargesForInstallment = cumulativePenaltyChargesDueWithin(startDate, scheduledDueDate, loanCharges, currency); + + // 8. sum up real totalInstallmentDue from components + final Money totalInstallmentDue = principalInterestForThisPeriod.principal() // + .plus(principalInterestForThisPeriod.interest()) // + .plus(feeChargesForInstallment) // + .plus(penaltyChargesForInstallment); + + // 9. create repayment period from parts + final LoanScheduleModelPeriod installment = LoanScheduleModelRepaymentPeriod.repayment(periodNumber, startDate, + scheduledDueDate, principalInterestForThisPeriod.principal(), outstandingBalance, + principalInterestForThisPeriod.interest(), feeChargesForInstallment, penaltyChargesForInstallment, totalInstallmentDue); + periods.add(installment); + + // handle cumulative fields + loanTermInDays += daysInPeriod; + totalPrincipalExpected = totalPrincipalExpected.add(principalInterestForThisPeriod.principal().getAmount()); + totalInterestCharged = totalInterestCharged.add(principalInterestForThisPeriod.interest().getAmount()); + totalFeeChargesCharged = totalFeeChargesCharged.add(feeChargesForInstallment.getAmount()); + totalPenaltyChargesCharged = totalPenaltyChargesCharged.add(penaltyChargesForInstallment.getAmount()); + totalRepaymentExpected = totalRepaymentExpected.add(totalInstallmentDue.getAmount()); + startDate = scheduledDueDate; + repaymentPeriodStartDate = startDate; + + periodNumber++; + } + + return LoanScheduleModel.from(periods, applicationCurrency, loanTermInDays, principalDisbursed, totalPrincipalExpected, + totalPrincipalPaid, totalInterestCharged, totalFeeChargesCharged, totalPenaltyChargesCharged, totalRepaymentExpected, + totalOutstanding); + } + + public abstract PrincipalInterest calculatePrincipalInterestComponentsForPeriod(PaymentPeriodsInOneYearCalculator calculator, + double interestCalculationGraceOnRepaymentPeriodFraction, Money totalCumulativePrincipal, Money totalCumulativeInterest, + Money totalInterestDueForLoan, Money cumulatingInterestPaymentDueToGrace, int daysInPeriod, Money outstandingBalance, + LoanApplicationTerms loanApplicationTerms, int periodNumber, MathContext mc); + + protected final boolean isLastRepaymentPeriod(final int numberOfRepayments, final int periodNumber) { + return periodNumber == numberOfRepayments; + } + + private BigDecimal deriveTotalChargesDueAtTimeOfDisbursement(final Set loanCharges) { + BigDecimal chargesDueAtTimeOfDisbursement = BigDecimal.ZERO; + for (LoanCharge loanCharge : loanCharges) { + if (loanCharge.isDueAtDisbursement()) { + chargesDueAtTimeOfDisbursement = chargesDueAtTimeOfDisbursement.add(loanCharge.amount()); + } + } + return chargesDueAtTimeOfDisbursement; + } + + private Collection createNewLoanScheduleListWithDisbursementDetails(final int numberOfRepayments, + final LoanApplicationTerms loanApplicationTerms, final BigDecimal chargesDueAtTimeOfDisbursement) { + + final Collection periods = new ArrayList(numberOfRepayments + 1); + + final LoanScheduleModelDisbursementPeriod disbursementPeriod = LoanScheduleModelDisbursementPeriod.disbursement( + loanApplicationTerms, chargesDueAtTimeOfDisbursement); + periods.add(disbursementPeriod); + + return periods; + } + + private Money cumulativeFeeChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, final Set loanCharges, + final MonetaryCurrency monetaryCurrency) { + + Money cumulative = Money.zero(monetaryCurrency); + + for (LoanCharge loanCharge : loanCharges) { + if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd) && loanCharge.isFeeCharge()) { + cumulative = cumulative.plus(loanCharge.amount()); + } + } + + return cumulative; + } + + private Money cumulativePenaltyChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, + final Set loanCharges, final MonetaryCurrency monetaryCurrency) { + + Money cumulative = Money.zero(monetaryCurrency); + + for (LoanCharge loanCharge : loanCharges) { + if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd) && loanCharge.isPenaltyCharge()) { + cumulative = cumulative.plus(loanCharge.amount()); + } + } + + return cumulative; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AmortizationLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AmortizationLoanScheduleGenerator.java deleted file mode 100644 index 4a9f11b0e..000000000 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AmortizationLoanScheduleGenerator.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. - */ -package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; - -import java.math.BigDecimal; -import java.util.List; -import java.util.Set; - -import org.joda.time.LocalDate; -import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; -import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; -import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData; -import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; - -public interface AmortizationLoanScheduleGenerator { - - LoanScheduleData generate(ApplicationCurrency applicationCurrency, LoanProductRelatedDetail loanScheduleInfo, - LocalDate disbursementDate, LocalDate interestCalculatedFrom, BigDecimal periodInterestRateForRepaymentPeriod, - LocalDate idealDisbursementDateBasedOnFirstRepaymentDate, List scheduledDates, Set loanCharges); -} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AmortizationLoanScheduleGeneratorFactory.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AmortizationLoanScheduleGeneratorFactory.java deleted file mode 100644 index f2030fd0b..000000000 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AmortizationLoanScheduleGeneratorFactory.java +++ /dev/null @@ -1,31 +0,0 @@ -/** - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. - */ -package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; - -import org.mifosplatform.portfolio.loanproduct.domain.AmortizationMethod; - -public class AmortizationLoanScheduleGeneratorFactory { - - public AmortizationLoanScheduleGenerator createGenerator(final AmortizationMethod amortizationMethod) { - - AmortizationLoanScheduleGenerator generator = new EqualInstallmentsAmortizationLoanScheduleGenerator(); - - switch (amortizationMethod) { - case INVALID: - generator = new EqualInstallmentsAmortizationLoanScheduleGenerator(); - break; - case EQUAL_INSTALLMENTS: - generator = new EqualInstallmentsAmortizationLoanScheduleGenerator(); - break; - case EQUAL_PRINCIPAL: - generator = new EqualPrincipalAmortizationLoanScheduleGenerator(); - break; - } - - return generator; - } - -} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DecliningBalanceInterestLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DecliningBalanceInterestLoanScheduleGenerator.java new file mode 100644 index 000000000..a6b9e95ac --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DecliningBalanceInterestLoanScheduleGenerator.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.loanaccount.loanschedule.domain; + +import java.math.MathContext; + +import org.mifosplatform.organisation.monetary.domain.Money; +import org.mifosplatform.portfolio.loanproduct.domain.AmortizationMethod; + +/** + *

+ * Declining balance can be amortized (see {@link AmortizationMethod}) in two + * ways at present: + *

    + *
  1. Equal principal payments
  2. + *
  3. Equal installment payments
  4. + *
+ *

+ * + *

+ * When amortized using equal principal payments, the principal + * component of each installment is fixed and interest due is + * calculated from the outstanding principal balance resulting in a + * different total payment due for each installment. + *

+ * + *

+ * When amortized using equal installments, the total payment due + * for each installment is fixed and is calculated using the excel like + * pmt function. The interest due is calculated from the + * outstanding principal balance which results in a principal + * component that is total payment due minus interest due. + *

+ */ +public class DecliningBalanceInterestLoanScheduleGenerator extends AbstractLoanScheduleGenerator { + + @Override + public PrincipalInterest calculatePrincipalInterestComponentsForPeriod(final PaymentPeriodsInOneYearCalculator calculator, + final double interestCalculationGraceOnRepaymentPeriodFraction, final Money totalCumulativePrincipal, + @SuppressWarnings("unused") final Money totalCumulativeInterest, + @SuppressWarnings("unused") final Money totalInterestDueForLoan, final Money cumulatingInterestPaymentDueToGrace, + final int daysInPeriod, final Money outstandingBalance, + final LoanApplicationTerms loanApplicationTerms, final int periodNumber, final MathContext mc) { + + PrincipalInterest result = loanApplicationTerms.calculateTotalInterestForPeriod(calculator, + interestCalculationGraceOnRepaymentPeriodFraction, periodNumber, mc, cumulatingInterestPaymentDueToGrace, daysInPeriod, + outstandingBalance); + + Money interestForThisInstallment = result.interest(); + + Money principalForThisInstallment = loanApplicationTerms.calculateTotalPrincipalForPeriod(calculator, daysInPeriod, + outstandingBalance, periodNumber, mc); + + // update cumulative fields for principal & interest + Money interestBroughtFowardDueToGrace = result.interestPaymentDueToGrace(); + Money totalCumulativePrincipalToDate = totalCumulativePrincipal.plus(principalForThisInstallment); + + // adjust if needed + principalForThisInstallment = loanApplicationTerms.adjustPrincipalIfLastRepaymentPeriod(principalForThisInstallment, + totalCumulativePrincipalToDate, periodNumber); + + return new PrincipalInterest(principalForThisInstallment, interestForThisInstallment, interestBroughtFowardDueToGrace); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DecliningBalanceMethodLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DecliningBalanceMethodLoanScheduleGenerator.java deleted file mode 100644 index f23e11a64..000000000 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DecliningBalanceMethodLoanScheduleGenerator.java +++ /dev/null @@ -1,74 +0,0 @@ -/** - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. - */ -package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; - -import java.math.BigDecimal; -import java.util.List; -import java.util.Set; - -import org.joda.time.LocalDate; -import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; -import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; -import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData; -import org.mifosplatform.portfolio.loanproduct.domain.AmortizationMethod; -import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; -import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType; - -/** - *

- * Declining balance can be amortized (see {@link AmortizationMethod}) in two ways - * at present: - *

    - *
  1. Equal principal payments
  2. - *
  3. Equal installment payments
  4. - *
- *

- * - *

- * When amortized using equal principal payments, the principal - * component of each installment is fixed and interest due is - * calculated from the outstanding principal balance resulting in a - * different total payment due for each installment. - *

- * - *

- * When amortized using equal installments, the total payment due - * for each installment is fixed and is calculated using the excel like - * pmt function. The interest due is calculated from the - * outstanding principal balance which results in a principal - * component that is total payment due minus interest due. - *

- */ -public class DecliningBalanceMethodLoanScheduleGenerator implements LoanScheduleGenerator { - - private final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator(); - private final PeriodicInterestRateCalculator periodicInterestRateCalculator = new PeriodicInterestRateCalculator(); - private final AmortizationLoanScheduleGeneratorFactory amortizationLoanScheduleGeneratorFactory = new AmortizationLoanScheduleGeneratorFactory(); - - @Override - public LoanScheduleData generate(final ApplicationCurrency applicationCurrency, final LoanProductRelatedDetail loanScheduleInfo, - final Integer loanTermFrequency, final PeriodFrequencyType loanTermFrequencyType, final LocalDate disbursementDate, - final LocalDate firstRepaymentDate, final LocalDate interestCalculatedFrom, final Set loanCharges) { - - // 1. generate valid set of 'due dates' based on some of the 'loan - // attributes' - final List scheduledDates = this.scheduledDateGenerator.generate(loanScheduleInfo, disbursementDate, firstRepaymentDate); - - final LocalDate idealDisbursementDateBasedOnFirstRepaymentDate = this.scheduledDateGenerator - .idealDisbursementDateBasedOnFirstRepaymentDate(loanScheduleInfo, scheduledDates); - - // 2. determine the 'periodic' interest rate based on the 'repayment - // periods' so we can use - final BigDecimal periodInterestRateForRepaymentPeriod = this.periodicInterestRateCalculator.calculateFrom(loanScheduleInfo); - - // Determine with 'amortisation' approach to use - final AmortizationLoanScheduleGenerator generator = this.amortizationLoanScheduleGeneratorFactory.createGenerator(loanScheduleInfo - .getAmortizationMethod()); - - return generator.generate(applicationCurrency, loanScheduleInfo, disbursementDate, interestCalculatedFrom, - periodInterestRateForRepaymentPeriod, idealDisbursementDateBasedOnFirstRepaymentDate, scheduledDates, loanCharges); - } -} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultLoanScheduleGeneratorFactory.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultLoanScheduleGeneratorFactory.java index 8bfe8c39a..875fdb0b6 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultLoanScheduleGeneratorFactory.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultLoanScheduleGeneratorFactory.java @@ -18,10 +18,10 @@ public class DefaultLoanScheduleGeneratorFactory implements LoanScheduleGenerato switch (interestMethod) { case FLAT: - loanScheduleGenerator = new FlatMethodLoanScheduleGenerator(); + loanScheduleGenerator = new FlatInterestLoanScheduleGenerator(); break; case DECLINING_BALANCE: - loanScheduleGenerator = new DecliningBalanceMethodLoanScheduleGenerator(); + loanScheduleGenerator = new DecliningBalanceInterestLoanScheduleGenerator(); break; case INVALID: break; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultPaymentPeriodsInOneYearCalculator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultPaymentPeriodsInOneYearCalculator.java index 476450dd8..914697e9f 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultPaymentPeriodsInOneYearCalculator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultPaymentPeriodsInOneYearCalculator.java @@ -5,98 +5,80 @@ */ package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; -import java.util.List; - import org.joda.time.Days; import org.joda.time.LocalDate; +import org.mifosplatform.infrastructure.core.domain.LocalDateInterval; import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType; -public class DefaultPaymentPeriodsInOneYearCalculator implements - PaymentPeriodsInOneYearCalculator { +public class DefaultPaymentPeriodsInOneYearCalculator implements PaymentPeriodsInOneYearCalculator { - @Override - public Integer calculate(final PeriodFrequencyType repaymentFrequencyType) { + @Override + public Integer calculate(final PeriodFrequencyType repaymentFrequencyType) { - Integer paymentPeriodsInOneYear = Integer.valueOf(0); - switch (repaymentFrequencyType) { - case DAYS: - paymentPeriodsInOneYear = Integer.valueOf(365); - break; - case WEEKS: - paymentPeriodsInOneYear = Integer.valueOf(52); - break; - case MONTHS: - paymentPeriodsInOneYear = Integer.valueOf(12); - break; - case YEARS: - paymentPeriodsInOneYear = Integer.valueOf(1); - break; - case INVALID: - paymentPeriodsInOneYear = Integer.valueOf(0); - break; - } - return paymentPeriodsInOneYear; - } + Integer paymentPeriodsInOneYear = Integer.valueOf(0); + switch (repaymentFrequencyType) { + case DAYS: + paymentPeriodsInOneYear = Integer.valueOf(365); + break; + case WEEKS: + paymentPeriodsInOneYear = Integer.valueOf(52); + break; + case MONTHS: + paymentPeriodsInOneYear = Integer.valueOf(12); + break; + case YEARS: + paymentPeriodsInOneYear = Integer.valueOf(1); + break; + case INVALID: + paymentPeriodsInOneYear = Integer.valueOf(0); + break; + } + return paymentPeriodsInOneYear; + } - /** - * calculates the number of grace repayment periods and can be a fraction of a repayment period. - */ - @Override - public double calculateRepaymentPeriodAsAFractionOfDays( - PeriodFrequencyType repaymentPeriodFrequencyType, - Integer every, LocalDate interestCalculatedFrom, - List scheduledDates, LocalDate disbursementDate) { - - Double periodFraction = Double.valueOf("0"); - - if (interestCalculatedFrom != null && !interestCalculatedFrom.isBefore(disbursementDate)) { - Integer repaymentPeriodIndex = 0; - LocalDate lastRepaymentPeriod = disbursementDate; - for (LocalDate repaymentPeriodDueDate : scheduledDates) { - if (interestCalculatedFrom.isBefore(repaymentPeriodDueDate)) { - // fraction is - int numberOfDaysInterestCalculationGraceInPeriod = Days - .daysBetween( - lastRepaymentPeriod.toDateMidnight() - .toDateTime(), - interestCalculatedFrom.toDateMidnight() - .toDateTime()).getDays(); - periodFraction = calculateRepaymentPeriodFraction( - repaymentPeriodFrequencyType, every, - numberOfDaysInterestCalculationGraceInPeriod); - periodFraction = periodFraction - + repaymentPeriodIndex.doubleValue(); - break; - } - repaymentPeriodIndex++; - lastRepaymentPeriod = repaymentPeriodDueDate; - } - } - - return periodFraction; - } - - private double calculateRepaymentPeriodFraction(PeriodFrequencyType repaymentPeriodFrequencyType, Integer every, Integer numberOfDaysInterestCalculationGrace) { - - Double fraction = Double.valueOf("0"); - switch (repaymentPeriodFrequencyType) { - case DAYS: - fraction = numberOfDaysInterestCalculationGrace.doubleValue() * every.doubleValue(); - break; - case WEEKS: - fraction = numberOfDaysInterestCalculationGrace.doubleValue() / (Double.valueOf("7.0") * every.doubleValue()); - break; - case MONTHS: - fraction = numberOfDaysInterestCalculationGrace.doubleValue() / (Double.valueOf("30.0") * every.doubleValue()); - break; - case YEARS: - fraction = numberOfDaysInterestCalculationGrace.doubleValue() / (Double.valueOf("365.0") * every.doubleValue()); - break; - case INVALID: - fraction = Double.valueOf("0"); - break; - } - return fraction; - } + @Override + public double calculatePortionOfRepaymentPeriodInterestChargingGrace(final LocalDate repaymentPeriodStartDate, + final LocalDate scheduledDueDate, final LocalDate interestChargedFromLocalDate, + final PeriodFrequencyType repaymentPeriodFrequencyType, final Integer repaidEvery) { -} + Double periodFraction = Double.valueOf("0.0"); + + LocalDateInterval repaymentPeriod = new LocalDateInterval(repaymentPeriodStartDate, scheduledDueDate); + + if (interestChargedFromLocalDate != null && repaymentPeriod.fallsBefore(interestChargedFromLocalDate.plusDays(1))) { + periodFraction = Double.valueOf("1.0"); + } else if (interestChargedFromLocalDate != null && repaymentPeriod.contains(interestChargedFromLocalDate)) { + + int numberOfDaysInterestCalculationGraceInPeriod = Days.daysBetween(repaymentPeriodStartDate, interestChargedFromLocalDate) + .getDays(); + periodFraction = calculateRepaymentPeriodFraction(repaymentPeriodFrequencyType, repaidEvery, + numberOfDaysInterestCalculationGraceInPeriod); + } + + return periodFraction; + } + + private double calculateRepaymentPeriodFraction(final PeriodFrequencyType repaymentPeriodFrequencyType, final Integer every, + final Integer numberOfDaysInterestCalculationGrace) { + + Double fraction = Double.valueOf("0"); + switch (repaymentPeriodFrequencyType) { + case DAYS: + fraction = numberOfDaysInterestCalculationGrace.doubleValue() * every.doubleValue(); + break; + case WEEKS: + fraction = numberOfDaysInterestCalculationGrace.doubleValue() / (Double.valueOf("7.0") * every.doubleValue()); + break; + case MONTHS: + fraction = numberOfDaysInterestCalculationGrace.doubleValue() / (Double.valueOf("30.0") * every.doubleValue()); + break; + case YEARS: + fraction = numberOfDaysInterestCalculationGrace.doubleValue() / (Double.valueOf("365.0") * every.doubleValue()); + break; + case INVALID: + fraction = Double.valueOf("0"); + break; + } + return fraction; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java index 95cb12b45..575ee2c00 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java @@ -9,46 +9,47 @@ import java.util.ArrayList; import java.util.List; import org.joda.time.LocalDate; -import org.mifosplatform.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail; -import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; +import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType; public class DefaultScheduledDateGenerator implements ScheduledDateGenerator { @Override - public List generate(final LoanProductMinimumRepaymentScheduleRelatedDetail loanScheduleDateInfo, final LocalDate disbursementDate, final LocalDate firstRepaymentPeriodDate) { + public List generate(final LoanApplicationTerms loanApplicationTerms) { - List dueRepaymentPeriodDates = new ArrayList(loanScheduleDateInfo.getNumberOfRepayments()); + final int numberOfRepayments = loanApplicationTerms.getNumberOfRepayments(); - LocalDate startDate = disbursementDate; + final List dueRepaymentPeriodDates = new ArrayList(numberOfRepayments); - for (int repaymentPeriod = 1; repaymentPeriod <= loanScheduleDateInfo.getNumberOfRepayments(); repaymentPeriod++) { + LocalDate startDate = loanApplicationTerms.getExpectedDisbursementDate(); + final LocalDate firstRepaymentPeriodDate = loanApplicationTerms.getRepaymentsStartingFromLocalDate(); + + for (int repaymentPeriod = 1; repaymentPeriod <= numberOfRepayments; repaymentPeriod++) { LocalDate dueRepaymentPeriodDate = startDate; - - if (repaymentPeriod == 1 && firstRepaymentPeriodDate != null) { - dueRepaymentPeriodDate = firstRepaymentPeriodDate; - } else { - switch (loanScheduleDateInfo.getRepaymentPeriodFrequencyType()) { - case DAYS: - dueRepaymentPeriodDate = startDate.plusDays(loanScheduleDateInfo - .getRepayEvery()); - break; - case WEEKS: - dueRepaymentPeriodDate = startDate.plusWeeks(loanScheduleDateInfo - .getRepayEvery()); - break; - case MONTHS: - dueRepaymentPeriodDate = startDate.plusMonths(loanScheduleDateInfo - .getRepayEvery()); - break; - case YEARS: - dueRepaymentPeriodDate = startDate.plusYears(loanScheduleDateInfo - .getRepayEvery()); - break; - case INVALID: - break; - } - } + + if (repaymentPeriod == 1 && firstRepaymentPeriodDate != null) { + dueRepaymentPeriodDate = firstRepaymentPeriodDate; + } else { + + final int repaidEvery = loanApplicationTerms.getRepaymentEvery(); + + switch (loanApplicationTerms.getRepaymentPeriodFrequencyType()) { + case DAYS: + dueRepaymentPeriodDate = startDate.plusDays(repaidEvery); + break; + case WEEKS: + dueRepaymentPeriodDate = startDate.plusWeeks(repaidEvery); + break; + case MONTHS: + dueRepaymentPeriodDate = startDate.plusMonths(repaidEvery); + break; + case YEARS: + dueRepaymentPeriodDate = startDate.plusYears(repaidEvery); + break; + case INVALID: + break; + } + } dueRepaymentPeriodDates.add(dueRepaymentPeriodDate); startDate = dueRepaymentPeriodDate; @@ -57,35 +58,31 @@ public class DefaultScheduledDateGenerator implements ScheduledDateGenerator { return dueRepaymentPeriodDates; } - @Override - public LocalDate idealDisbursementDateBasedOnFirstRepaymentDate( - LoanProductRelatedDetail loanScheduleDateInfo, - List scheduledDates) { - - LocalDate firstRepaymentDate = scheduledDates.get(0); - - LocalDate idealDisbursementDate = null; - - switch (loanScheduleDateInfo.getRepaymentPeriodFrequencyType()) { - case DAYS: - idealDisbursementDate = firstRepaymentDate.minusDays(loanScheduleDateInfo.getRepayEvery()); - break; - case WEEKS: - idealDisbursementDate = firstRepaymentDate.minusWeeks(loanScheduleDateInfo - .getRepayEvery()); - break; - case MONTHS: - idealDisbursementDate = firstRepaymentDate.minusMonths(loanScheduleDateInfo - .getRepayEvery()); - break; - case YEARS: - idealDisbursementDate = firstRepaymentDate.minusYears(loanScheduleDateInfo - .getRepayEvery()); - break; - case INVALID: - break; - } - - return idealDisbursementDate; - } + @Override + public LocalDate idealDisbursementDateBasedOnFirstRepaymentDate(final PeriodFrequencyType repaymentPeriodFrequencyType, + final int repaidEvery, final List scheduledDates) { + + LocalDate firstRepaymentDate = scheduledDates.get(0); + + LocalDate idealDisbursementDate = null; + + switch (repaymentPeriodFrequencyType) { + case DAYS: + idealDisbursementDate = firstRepaymentDate.minusDays(repaidEvery); + break; + case WEEKS: + idealDisbursementDate = firstRepaymentDate.minusWeeks(repaidEvery); + break; + case MONTHS: + idealDisbursementDate = firstRepaymentDate.minusMonths(repaidEvery); + break; + case YEARS: + idealDisbursementDate = firstRepaymentDate.minusYears(repaidEvery); + break; + case INVALID: + break; + } + + return idealDisbursementDate; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/EqualInstallmentsAmortizationLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/EqualInstallmentsAmortizationLoanScheduleGenerator.java deleted file mode 100644 index 2114147fd..000000000 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/EqualInstallmentsAmortizationLoanScheduleGenerator.java +++ /dev/null @@ -1,190 +0,0 @@ -/** - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. - */ -package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; - -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Set; - -import org.joda.time.Days; -import org.joda.time.LocalDate; -import org.mifosplatform.organisation.monetary.data.CurrencyData; -import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; -import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; -import org.mifosplatform.organisation.monetary.domain.Money; -import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; -import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData; -import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; -import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; - -public class EqualInstallmentsAmortizationLoanScheduleGenerator implements AmortizationLoanScheduleGenerator { - - private final PeriodicInterestRateCalculator periodicInterestRateCalculator = new PeriodicInterestRateCalculator(); - private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator = new DefaultPaymentPeriodsInOneYearCalculator(); - private final PmtCalculator pmtCalculator = new PmtCalculator(); - - @Override - public LoanScheduleData generate(final ApplicationCurrency currency, final LoanProductRelatedDetail loanScheduleInfo, - final LocalDate disbursementDate, final LocalDate interestCalculatedFrom, - final BigDecimal periodInterestRateForRepaymentPeriod, final LocalDate idealDisbursementDateBasedOnFirstRepaymentDate, - final List scheduledDates, final Set loanCharges) { - - final Collection periods = new ArrayList(); - - // determine 'total payment' for each repayment based on pmt function - // (and hence the total due overall) - final MonetaryCurrency monetaryCurrency = loanScheduleInfo.getPrincipal().getCurrency(); - final Money totalDuePerInstallment = this.pmtCalculator.calculatePaymentForOnePeriodFrom(loanScheduleInfo, - periodInterestRateForRepaymentPeriod, monetaryCurrency); - final Money totalRepaymentDueForLoanTerm = this.pmtCalculator.calculateTotalRepaymentFrom(loanScheduleInfo, - periodInterestRateForRepaymentPeriod, monetaryCurrency); - - Money totalInterestDue = totalRepaymentDueForLoanTerm.minus(loanScheduleInfo.getPrincipal()); - Money outstandingBalance = loanScheduleInfo.getPrincipal(); - Money principalDisbursed = loanScheduleInfo.getPrincipal(); - Money totalPrincipal = Money.zero(monetaryCurrency); - Money totalInterest = Money.zero(monetaryCurrency); - - double interestCalculationGraceOnRepaymentPeriodFraction = this.paymentPeriodsInOneYearCalculator - .calculateRepaymentPeriodAsAFractionOfDays(loanScheduleInfo.getRepaymentPeriodFrequencyType(), - loanScheduleInfo.getRepayEvery(), interestCalculatedFrom, scheduledDates, - idealDisbursementDateBasedOnFirstRepaymentDate); - - BigDecimal chargesDueAtTimeOfDisbursement = BigDecimal.ZERO; - for (LoanCharge loanCharge : loanCharges) { - if (loanCharge.isDueAtDisbursement()) { - chargesDueAtTimeOfDisbursement = chargesDueAtTimeOfDisbursement.add(loanCharge.amount()); - } - } - - // create entries of disbursement period on loan schedule - final LoanSchedulePeriodData disbursementPeriod = LoanSchedulePeriodData.disbursementOnlyPeriod(disbursementDate, - principalDisbursed.getAmount(), chargesDueAtTimeOfDisbursement, false); - periods.add(disbursementPeriod); - - int loanTermInDays = Integer.valueOf(0); - BigDecimal totalPrincipalDisbursed = principalDisbursed.getAmount(); - BigDecimal totalPrincipalExpected = BigDecimal.ZERO; - BigDecimal totalPrincipalPaid = BigDecimal.ZERO; - BigDecimal totalInterestCharged = BigDecimal.ZERO; - BigDecimal totalFeeChargesCharged = chargesDueAtTimeOfDisbursement; - BigDecimal totalPenaltyChargesCharged = BigDecimal.ZERO; - BigDecimal totalWaived = BigDecimal.ZERO; - BigDecimal totalWrittenOff = BigDecimal.ZERO; - BigDecimal totalRepaymentExpected = chargesDueAtTimeOfDisbursement; - BigDecimal totalRepayment = BigDecimal.ZERO; - BigDecimal totalOutstanding = BigDecimal.ZERO; - - LocalDate startDate = disbursementDate; - int periodNumber = 1; - for (LocalDate scheduledDueDate : scheduledDates) { - - // number of days from startDate to this scheduledDate - int daysInPeriod = Days.daysBetween(startDate, scheduledDueDate).getDays(); - - Money interestForInstallment = this.periodicInterestRateCalculator.calculateInterestOn(outstandingBalance, - periodInterestRateForRepaymentPeriod, daysInPeriod, loanScheduleInfo); - Money principalForInstallment = this.periodicInterestRateCalculator.calculatePrincipalOn(totalDuePerInstallment, - interestForInstallment, loanScheduleInfo); - - if (interestCalculationGraceOnRepaymentPeriodFraction >= Integer.valueOf(1).doubleValue()) { - Money graceOnInterestForRepaymentPeriod = interestForInstallment; - interestForInstallment = interestForInstallment.minus(graceOnInterestForRepaymentPeriod); - totalInterestDue = totalInterestDue.minus(graceOnInterestForRepaymentPeriod); - interestCalculationGraceOnRepaymentPeriodFraction = interestCalculationGraceOnRepaymentPeriodFraction - - Integer.valueOf(1).doubleValue(); - } else if (interestCalculationGraceOnRepaymentPeriodFraction > Double.valueOf("0.25") - && interestCalculationGraceOnRepaymentPeriodFraction < Integer.valueOf(1).doubleValue()) { - Money graceOnInterestForRepaymentPeriod = interestForInstallment - .multipliedBy(interestCalculationGraceOnRepaymentPeriodFraction); - interestForInstallment = interestForInstallment.minus(graceOnInterestForRepaymentPeriod); - totalInterestDue = totalInterestDue.minus(graceOnInterestForRepaymentPeriod); - interestCalculationGraceOnRepaymentPeriodFraction = Double.valueOf("0"); - } - - totalPrincipal = totalPrincipal.plus(principalForInstallment); - totalInterest = totalInterest.plus(interestForInstallment); - - if (periodNumber == loanScheduleInfo.getNumberOfRepayments()) { - Money principalDifference = totalPrincipal.minus(loanScheduleInfo.getPrincipal()); - if (principalDifference.isLessThanZero()) { - principalForInstallment = principalForInstallment.plus(principalDifference.abs()); - } else if (principalDifference.isGreaterThanZero()) { - principalForInstallment = principalForInstallment.minus(principalDifference.abs()); - } - - final Money interestDifference = totalInterest.minus(totalInterestDue); - if (interestDifference.isLessThanZero()) { - interestForInstallment = interestForInstallment.plus(interestDifference.abs()); - } else if (interestDifference.isGreaterThanZero()) { - interestForInstallment = interestForInstallment.minus(interestDifference.abs()); - } - } - - outstandingBalance = outstandingBalance.minus(principalForInstallment); - - final Money feeChargesForInstallment = cumulativeFeeChargesDueWithin(startDate, scheduledDueDate, loanCharges, monetaryCurrency); - final Money penaltyChargesForInstallment = cumulativePenaltyChargesDueWithin(startDate, scheduledDueDate, loanCharges, - monetaryCurrency); - final Money totalInstallmentDue = principalForInstallment.plus(interestForInstallment).plus(feeChargesForInstallment) - .plus(penaltyChargesForInstallment); - - LoanSchedulePeriodData installment = LoanSchedulePeriodData.repaymentOnlyPeriod(periodNumber, startDate, scheduledDueDate, - principalForInstallment.getAmount(), outstandingBalance.getAmount(), interestForInstallment.getAmount(), - feeChargesForInstallment.getAmount(), penaltyChargesForInstallment.getAmount(), totalInstallmentDue.getAmount()); - - periods.add(installment); - - // handle cumulative fields - loanTermInDays += daysInPeriod; - totalPrincipalExpected = totalPrincipalExpected.add(principalForInstallment.getAmount()); - totalInterestCharged = totalInterestCharged.add(interestForInstallment.getAmount()); - totalFeeChargesCharged = totalFeeChargesCharged.add(feeChargesForInstallment.getAmount()); - totalPenaltyChargesCharged = totalPenaltyChargesCharged.add(penaltyChargesForInstallment.getAmount()); - totalRepaymentExpected = totalRepaymentExpected.add(totalInstallmentDue.getAmount()); - startDate = scheduledDueDate; - - periodNumber++; - } - - final CurrencyData currencyData = new CurrencyData(currency.getCode(), currency.getName(), - monetaryCurrency.getDigitsAfterDecimal(), currency.getDisplaySymbol(), currency.getNameCode()); - - return new LoanScheduleData(currencyData, periods, loanTermInDays, totalPrincipalDisbursed, totalPrincipalExpected, - totalPrincipalPaid, totalInterestCharged, totalFeeChargesCharged, totalPenaltyChargesCharged, totalWaived, totalWrittenOff, - totalRepaymentExpected, totalRepayment, totalOutstanding); - } - - private Money cumulativeFeeChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, final Set loanCharges, - final MonetaryCurrency monetaryCurrency) { - - Money cumulative = Money.zero(monetaryCurrency); - - for (LoanCharge loanCharge : loanCharges) { - if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd) && loanCharge.isFeeCharge()) { - cumulative = cumulative.plus(loanCharge.amount()); - } - } - - return cumulative; - } - - private Money cumulativePenaltyChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, - final Set loanCharges, final MonetaryCurrency monetaryCurrency) { - - Money cumulative = Money.zero(monetaryCurrency); - - for (LoanCharge loanCharge : loanCharges) { - if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd) && loanCharge.isPenaltyCharge()) { - cumulative = cumulative.plus(loanCharge.amount()); - } - } - - return cumulative; - } -} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/EqualPrincipalAmortizationLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/EqualPrincipalAmortizationLoanScheduleGenerator.java deleted file mode 100644 index 6766ce2f3..000000000 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/EqualPrincipalAmortizationLoanScheduleGenerator.java +++ /dev/null @@ -1,178 +0,0 @@ -/** - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. - */ -package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; - -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Set; - -import org.joda.time.Days; -import org.joda.time.LocalDate; -import org.mifosplatform.organisation.monetary.data.CurrencyData; -import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; -import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; -import org.mifosplatform.organisation.monetary.domain.Money; -import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; -import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData; -import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; -import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; - -public class EqualPrincipalAmortizationLoanScheduleGenerator implements AmortizationLoanScheduleGenerator { - - private final PeriodicInterestRateCalculator periodicInterestRateCalculator = new PeriodicInterestRateCalculator(); - private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator = new DefaultPaymentPeriodsInOneYearCalculator(); - - @Override - public LoanScheduleData generate(final ApplicationCurrency currency, final LoanProductRelatedDetail loanScheduleInfo, - final LocalDate disbursementDate, final LocalDate interestCalculatedFrom, - final BigDecimal periodInterestRateForRepaymentPeriod, final LocalDate idealDisbursementDateBasedOnFirstRepaymentDate, - final List scheduledDates, final Set loanCharges) { - - final Collection periods = new ArrayList(); - - // 1. we know 'principal' must be equal over repayments so use this to - // calculate interest and thus total repayment due. - final MonetaryCurrency monetaryCurrency = loanScheduleInfo.getPrincipal().getCurrency(); - Money totalDuePerInstallment = Money.zero(monetaryCurrency); - - Money outstandingBalance = loanScheduleInfo.getPrincipal(); - Money principalDisbursed = loanScheduleInfo.getPrincipal(); - Money totalPrincipal = Money.zero(monetaryCurrency); - Money totalInterest = Money.zero(monetaryCurrency); - - double interestCalculationGraceOnRepaymentPeriodFraction = this.paymentPeriodsInOneYearCalculator - .calculateRepaymentPeriodAsAFractionOfDays(loanScheduleInfo.getRepaymentPeriodFrequencyType(), - loanScheduleInfo.getRepayEvery(), interestCalculatedFrom, scheduledDates, - idealDisbursementDateBasedOnFirstRepaymentDate); - - BigDecimal chargesDueAtTimeOfDisbursement = BigDecimal.ZERO; - for (LoanCharge loanCharge : loanCharges) { - if (loanCharge.isDueAtDisbursement()) { - chargesDueAtTimeOfDisbursement = chargesDueAtTimeOfDisbursement.add(loanCharge.amount()); - } - } - - // create entries of disbursement period on loan schedule - final LoanSchedulePeriodData disbursementPeriod = LoanSchedulePeriodData.disbursementOnlyPeriod(disbursementDate, - principalDisbursed.getAmount(), chargesDueAtTimeOfDisbursement, false); - periods.add(disbursementPeriod); - - int loanTermInDays = Integer.valueOf(0); - BigDecimal totalPrincipalDisbursed = principalDisbursed.getAmount(); - BigDecimal totalPrincipalExpected = BigDecimal.ZERO; - BigDecimal totalPrincipalPaid = BigDecimal.ZERO; - BigDecimal totalInterestCharged = BigDecimal.ZERO; - BigDecimal totalFeeChargesCharged = chargesDueAtTimeOfDisbursement; - BigDecimal totalPenaltyChargesCharged = BigDecimal.ZERO; - BigDecimal totalWaived = BigDecimal.ZERO; - BigDecimal totalWrittenOff = BigDecimal.ZERO; - BigDecimal totalRepaymentExpected = chargesDueAtTimeOfDisbursement; - BigDecimal totalRepayment = BigDecimal.ZERO; - BigDecimal totalOutstanding = BigDecimal.ZERO; - - LocalDate startDate = disbursementDate; - int periodNumber = 1; - for (LocalDate scheduledDueDate : scheduledDates) { - - // number of days from startDate to this scheduledDate - int daysInPeriod = Days.daysBetween(startDate.toDateMidnight().toDateTime(), scheduledDueDate.toDateMidnight().toDateTime()) - .getDays(); - - Money interestForInstallment = this.periodicInterestRateCalculator.calculateInterestOn(outstandingBalance, - periodInterestRateForRepaymentPeriod, daysInPeriod, loanScheduleInfo); - Money principalForInstallment = this.periodicInterestRateCalculator.calculatePrincipalOn(totalDuePerInstallment, - interestForInstallment, loanScheduleInfo); - - if (interestCalculationGraceOnRepaymentPeriodFraction >= Integer.valueOf(1).doubleValue()) { - Money graceOnInterestForRepaymentPeriod = interestForInstallment; - interestForInstallment = interestForInstallment.minus(graceOnInterestForRepaymentPeriod); - interestCalculationGraceOnRepaymentPeriodFraction = interestCalculationGraceOnRepaymentPeriodFraction - - Integer.valueOf(1).doubleValue(); - } else if (interestCalculationGraceOnRepaymentPeriodFraction > Double.valueOf("0.25") - && interestCalculationGraceOnRepaymentPeriodFraction < Integer.valueOf(1).doubleValue()) { - Money graceOnInterestForRepaymentPeriod = interestForInstallment - .multipliedBy(interestCalculationGraceOnRepaymentPeriodFraction); - interestForInstallment = interestForInstallment.minus(graceOnInterestForRepaymentPeriod); - interestCalculationGraceOnRepaymentPeriodFraction = Double.valueOf("0"); - } - - totalPrincipal = totalPrincipal.plus(principalForInstallment); - totalInterest = totalInterest.plus(interestForInstallment); - - if (periodNumber == loanScheduleInfo.getNumberOfRepayments()) { - Money principalDifference = totalPrincipal.minus(loanScheduleInfo.getPrincipal()); - if (principalDifference.isLessThanZero()) { - principalForInstallment = principalForInstallment.plus(principalDifference.abs()); - } else if (principalDifference.isGreaterThanZero()) { - principalForInstallment = principalForInstallment.minus(principalDifference.abs()); - } - } - - outstandingBalance = outstandingBalance.minus(principalForInstallment); - - final Money feeChargesForInstallment = cumulativeFeeChargesDueWithin(startDate, scheduledDueDate, loanCharges, monetaryCurrency); - final Money penaltyChargesForInstallment = cumulativePenaltyChargesDueWithin(startDate, scheduledDueDate, loanCharges, - monetaryCurrency); - - final Money totalInstallmentDue = principalForInstallment.plus(interestForInstallment).plus(feeChargesForInstallment) - .plus(penaltyChargesForInstallment); - - LoanSchedulePeriodData installment = LoanSchedulePeriodData.repaymentOnlyPeriod(periodNumber, startDate, scheduledDueDate, - principalForInstallment.getAmount(), outstandingBalance.getAmount(), interestForInstallment.getAmount(), - feeChargesForInstallment.getAmount(), penaltyChargesForInstallment.getAmount(), totalInstallmentDue.getAmount()); - - periods.add(installment); - - // handle cumulative fields - loanTermInDays += daysInPeriod; - totalPrincipalExpected = totalPrincipalExpected.add(principalForInstallment.getAmount()); - totalInterestCharged = totalInterestCharged.add(interestForInstallment.getAmount()); - totalFeeChargesCharged = totalFeeChargesCharged.add(feeChargesForInstallment.getAmount()); - totalPenaltyChargesCharged = totalPenaltyChargesCharged.add(penaltyChargesForInstallment.getAmount()); - totalRepaymentExpected = totalRepaymentExpected.add(totalInstallmentDue.getAmount()); - startDate = scheduledDueDate; - - periodNumber++; - } - - final CurrencyData currencyData = new CurrencyData(currency.getCode(), currency.getName(), - monetaryCurrency.getDigitsAfterDecimal(), currency.getDisplaySymbol(), currency.getNameCode()); - - return new LoanScheduleData(currencyData, periods, loanTermInDays, totalPrincipalDisbursed, totalPrincipalExpected, - totalPrincipalPaid, totalInterestCharged, totalFeeChargesCharged, totalPenaltyChargesCharged, totalWaived, totalWrittenOff, - totalRepaymentExpected, totalRepayment, totalOutstanding); - } - - private Money cumulativeFeeChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, final Set loanCharges, - final MonetaryCurrency monetaryCurrency) { - - Money cumulative = Money.zero(monetaryCurrency); - - for (LoanCharge loanCharge : loanCharges) { - if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd) && loanCharge.isFeeCharge()) { - cumulative = cumulative.plus(loanCharge.amount()); - } - } - - return cumulative; - } - - private Money cumulativePenaltyChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, - final Set loanCharges, final MonetaryCurrency monetaryCurrency) { - - Money cumulative = Money.zero(monetaryCurrency); - - for (LoanCharge loanCharge : loanCharges) { - if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd) && loanCharge.isPenaltyCharge()) { - cumulative = cumulative.plus(loanCharge.amount()); - } - } - - return cumulative; - } -} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/FinanicalFunctions.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/FinanicalFunctions.java new file mode 100644 index 000000000..bad420c4f --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/FinanicalFunctions.java @@ -0,0 +1,106 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; + +import java.math.BigDecimal; + +import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; +import org.mifosplatform.organisation.monetary.domain.Money; +import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; + +public class FinanicalFunctions { + + @Deprecated + public Money calculatePaymentForOnePeriodFrom(final LoanProductRelatedDetail loanScheduleInfo, + final BigDecimal periodInterestRateForRepaymentPeriod, final MonetaryCurrency monetaryCurrency) { + double interestRateFraction = periodInterestRateForRepaymentPeriod.doubleValue(); + + double futureValue = 0; + double numberOfPeriods = loanScheduleInfo.getNumberOfRepayments().doubleValue(); + double principal = loanScheduleInfo.getPrincipal().getAmount().multiply(BigDecimal.valueOf(-1)).doubleValue(); + + // work out the total payment (principal + interest components) for each + // installment due. + // use the period type and interest as expressed e.g. 2% per month or + // 24% per year even is 'daily' interest calculation is selected + double paymentPerInstallment = pmt(interestRateFraction, numberOfPeriods, principal, futureValue, false); + + return Money.of(monetaryCurrency, BigDecimal.valueOf(paymentPerInstallment)); + } + + @Deprecated + public Money calculateTotalRepaymentFrom(final LoanProductRelatedDetail loanScheduleInfo, + final BigDecimal periodInterestRateForRepaymentPeriod, final MonetaryCurrency monetaryCurrency) { + double interestRateFraction = periodInterestRateForRepaymentPeriod.doubleValue(); + + double futureValue = 0; + double numberOfPeriods = loanScheduleInfo.getNumberOfRepayments().doubleValue(); + double principal = loanScheduleInfo.getPrincipal().getAmount().multiply(BigDecimal.valueOf(-1)).doubleValue(); + + // work out the total payment (principal + interest components) for each + // installment due. + // use the period type and interest as expressed e.g. 2% per month or + // 24% per year even is 'daily' interest calculation is selected + double paymentPerInstallment = pmt(interestRateFraction, numberOfPeriods, principal, futureValue, false); + + double totalRepayment = paymentPerInstallment * loanScheduleInfo.getNumberOfRepayments(); + + return Money.of(monetaryCurrency, BigDecimal.valueOf(totalRepayment)); + } + + @Deprecated + public Money calculateTotalRepaymentFrom(final Money principal, final Integer loanTermFrequency, + final BigDecimal periodInterestRateForLoanTermPeriod, final MonetaryCurrency monetaryCurrency) { + + double interestRateFraction = periodInterestRateForLoanTermPeriod.doubleValue(); + + double futureValue = 0; + double numberOfPeriods = loanTermFrequency.doubleValue(); + double principalAmount = principal.getAmount().multiply(BigDecimal.valueOf(-1)).doubleValue(); + + // work out the total payment (principal + interest components) for each + // installment due. + // use the period type and interest as expressed e.g. 2% per month or + // 24% per year even is 'daily' interest calculation is selected + double paymentPerInstallment = pmt(interestRateFraction, numberOfPeriods, principalAmount, futureValue, false); + + double totalRepayment = paymentPerInstallment * loanTermFrequency; + + return Money.of(monetaryCurrency, BigDecimal.valueOf(totalRepayment)); + } + + /** + * PMT calculates a fixed monthly payment to be paid by borrower every + * 'period' to ensure loan is paid off in full (with interest). + * + * This monthly payment c depends upon the monthly interest rate r + * (expressed as a fraction, not a percentage, i.e., divide the quoted + * yearly percentage rate by 100 and by 12 to obtain the monthly interest + * rate), the number of monthly payments N called the loan's term, and the + * amount borrowed P known as the loan's principal; c is given by the + * formula: + * + * c = (r / (1 - (1 + r)^-N))P + * + * @param interestRateFraction + * @param numberOfPayments + * @param principal + * @param futureValue + * @param type + */ + public static double pmt(final double interestRateFraction, final double numberOfPayments, final double principal, + final double futureValue, final boolean type) { + double payment = 0; + if (interestRateFraction == 0) { + payment = -1 * (futureValue + principal) / numberOfPayments; + } else { + double r1 = interestRateFraction + 1; + payment = (futureValue + principal * Math.pow(r1, numberOfPayments)) * interestRateFraction + / ((type ? r1 : 1) * (1 - Math.pow(r1, numberOfPayments))); + } + return payment; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/FlatInterestLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/FlatInterestLoanScheduleGenerator.java new file mode 100644 index 000000000..50ed49518 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/FlatInterestLoanScheduleGenerator.java @@ -0,0 +1,43 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; + +import java.math.MathContext; + +import org.mifosplatform.organisation.monetary.domain.Money; + +public class FlatInterestLoanScheduleGenerator extends AbstractLoanScheduleGenerator { + + @Override + public PrincipalInterest calculatePrincipalInterestComponentsForPeriod(final PaymentPeriodsInOneYearCalculator calculator, + final double interestCalculationGraceOnRepaymentPeriodFraction, final Money totalCumulativePrincipal, + final Money totalCumulativeInterest, final Money totalInterestDueForLoan, final Money cumulatingInterestPaymentDueToGrace, + final int daysInPeriod, final Money outstandingBalance, + final LoanApplicationTerms loanApplicationTerms, final int periodNumber, final MathContext mc) { + + Money principalForThisInstallment = loanApplicationTerms.calculateTotalPrincipalForPeriod(calculator, daysInPeriod, + outstandingBalance, periodNumber, mc); + + PrincipalInterest result = loanApplicationTerms.calculateTotalInterestForPeriod(calculator, + interestCalculationGraceOnRepaymentPeriodFraction, periodNumber, mc, cumulatingInterestPaymentDueToGrace, daysInPeriod, + outstandingBalance); + Money interestForThisInstallment = result.interest(); + + // update cumulative fields for principal & interest + Money interestBroughtForwardDueToGrace = result.interestPaymentDueToGrace(); + Money totalCumulativePrincipalToDate = totalCumulativePrincipal.plus(principalForThisInstallment); + Money totalCumulativeInterestToDate = totalCumulativeInterest.plus(interestForThisInstallment); + + // adjust if needed + principalForThisInstallment = loanApplicationTerms.adjustPrincipalIfLastRepaymentPeriod(principalForThisInstallment, + totalCumulativePrincipalToDate, periodNumber); + + interestForThisInstallment = loanApplicationTerms.adjustInterestIfLastRepaymentPeriod(interestForThisInstallment, + totalCumulativeInterestToDate, totalInterestDueForLoan, periodNumber); + + return new PrincipalInterest(principalForThisInstallment, interestForThisInstallment, interestBroughtForwardDueToGrace); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/FlatMethodLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/FlatMethodLoanScheduleGenerator.java deleted file mode 100644 index 9244aea8c..000000000 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/FlatMethodLoanScheduleGenerator.java +++ /dev/null @@ -1,178 +0,0 @@ -/** - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. - */ -package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; - -import java.math.BigDecimal; -import java.math.MathContext; -import java.math.RoundingMode; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Set; - -import org.joda.time.Days; -import org.joda.time.LocalDate; -import org.mifosplatform.organisation.monetary.data.CurrencyData; -import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; -import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; -import org.mifosplatform.organisation.monetary.domain.Money; -import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; -import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData; -import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; -import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; -import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType; - -public class FlatMethodLoanScheduleGenerator implements LoanScheduleGenerator { - - private final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator(); - private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator = new DefaultPaymentPeriodsInOneYearCalculator(); - - @Override - public LoanScheduleData generate(final ApplicationCurrency currency, final LoanProductRelatedDetail loanScheduleInfo, - final Integer loanTermFrequency, final PeriodFrequencyType loanTermFrequencyType, final LocalDate disbursementDate, - final LocalDate firstRepaymentDate, final LocalDate interestCalculatedFrom, final Set loanCharges) { - - final Collection periods = new ArrayList(); - - final List scheduledDates = this.scheduledDateGenerator.generate(loanScheduleInfo, disbursementDate, firstRepaymentDate); - - MathContext mc = new MathContext(8, RoundingMode.HALF_EVEN); - - BigDecimal loanTermPeriodsInYear = BigDecimal.valueOf(this.paymentPeriodsInOneYearCalculator.calculate(loanTermFrequencyType)); - BigDecimal interestRateForLoanTerm = loanScheduleInfo.getAnnualNominalInterestRate().divide(loanTermPeriodsInYear, mc) - .divide(BigDecimal.valueOf(Double.valueOf("100.0")), mc).multiply(BigDecimal.valueOf(loanTermFrequency)); - - final MonetaryCurrency monetaryCurrency = loanScheduleInfo.getPrincipal().getCurrency(); - Money totalInterestForLoanTerm = loanScheduleInfo.getPrincipal().multiplyRetainScale(interestRateForLoanTerm, - RoundingMode.HALF_EVEN); - - Money interestPerInstallment = totalInterestForLoanTerm.dividedBy(Long.valueOf(loanScheduleInfo.getNumberOfRepayments()), - RoundingMode.HALF_EVEN); - - Money principalPerInstallment = loanScheduleInfo.getPrincipal().dividedBy(loanScheduleInfo.getNumberOfRepayments(), - RoundingMode.HALF_EVEN); - - Money outstandingBalance = loanScheduleInfo.getPrincipal(); - Money principalDisbursed = loanScheduleInfo.getPrincipal(); - Money totalPrincipal = Money.zero(outstandingBalance.getCurrency()); - Money totalInterest = Money.zero(outstandingBalance.getCurrency()); - - BigDecimal chargesDueAtTimeOfDisbursement = BigDecimal.ZERO; - for (LoanCharge loanCharge : loanCharges) { - if (loanCharge.isDueAtDisbursement()) { - chargesDueAtTimeOfDisbursement = chargesDueAtTimeOfDisbursement.add(loanCharge.amount()); - } - } - - // create entries of disbursement period on loan schedule - final LoanSchedulePeriodData disbursementPeriod = LoanSchedulePeriodData.disbursementOnlyPeriod(disbursementDate, - principalDisbursed.getAmount(), chargesDueAtTimeOfDisbursement, false); - periods.add(disbursementPeriod); - - int loanTermInDays = Integer.valueOf(0); - BigDecimal totalPrincipalDisbursed = principalDisbursed.getAmount(); - BigDecimal totalPrincipalExpected = BigDecimal.ZERO; - BigDecimal totalPrincipalPaid = BigDecimal.ZERO; - BigDecimal totalInterestCharged = BigDecimal.ZERO; - BigDecimal totalFeeChargesCharged = chargesDueAtTimeOfDisbursement; - BigDecimal totalPenaltyChargesCharged = BigDecimal.ZERO; - BigDecimal totalWaived = BigDecimal.ZERO; - BigDecimal totalWrittenOff = BigDecimal.ZERO; - BigDecimal totalRepaymentExpected = chargesDueAtTimeOfDisbursement; - BigDecimal totalRepayment = BigDecimal.ZERO; - BigDecimal totalOutstanding = BigDecimal.ZERO; - - LocalDate startDate = disbursementDate; - int periodNumber = 1; - for (LocalDate scheduledDueDate : scheduledDates) { - totalPrincipal = totalPrincipal.plus(principalPerInstallment); - totalInterest = totalInterest.plus(interestPerInstallment); - - // number of days from startDate to this scheduledDate - int daysInPeriod = Days.daysBetween(startDate.toDateMidnight().toDateTime(), scheduledDueDate.toDateMidnight().toDateTime()) - .getDays(); - - if (periodNumber == loanScheduleInfo.getNumberOfRepayments()) { - final Money difference = totalPrincipal.minus(loanScheduleInfo.getPrincipal()); - if (difference.isLessThanZero()) { - principalPerInstallment = principalPerInstallment.plus(difference.abs()); - } else if (difference.isGreaterThanZero()) { - principalPerInstallment = principalPerInstallment.minus(difference.abs()); - } - - final Money interestDifference = totalInterest.minus(totalInterestForLoanTerm); - if (interestDifference.isLessThanZero()) { - interestPerInstallment = interestPerInstallment.plus(interestDifference.abs()); - } else if (interestDifference.isGreaterThanZero()) { - interestPerInstallment = interestPerInstallment.minus(interestDifference.abs()); - } - } - - outstandingBalance = outstandingBalance.minus(principalPerInstallment); - - final Money feeChargesForInstallment = cumulativeFeeChargesDueWithin(startDate, scheduledDueDate, loanCharges, monetaryCurrency); - final Money penaltyChargesForInstallment = cumulativePenaltyChargesDueWithin(startDate, scheduledDueDate, loanCharges, - monetaryCurrency); - - final Money totalInstallmentDue = principalPerInstallment // - .plus(interestPerInstallment) // - .plus(feeChargesForInstallment) // - .plus(penaltyChargesForInstallment); - - LoanSchedulePeriodData installment = LoanSchedulePeriodData.repaymentOnlyPeriod(periodNumber, startDate, scheduledDueDate, - principalPerInstallment.getAmount(), outstandingBalance.getAmount(), interestPerInstallment.getAmount(), - feeChargesForInstallment.getAmount(), penaltyChargesForInstallment.getAmount(), totalInstallmentDue.getAmount()); - - periods.add(installment); - - // handle cumulative fields - loanTermInDays += daysInPeriod; - totalPrincipalExpected = totalPrincipalExpected.add(principalPerInstallment.getAmount()); - totalInterestCharged = totalInterestCharged.add(interestPerInstallment.getAmount()); - totalFeeChargesCharged = totalFeeChargesCharged.add(feeChargesForInstallment.getAmount()); - totalPenaltyChargesCharged = totalPenaltyChargesCharged.add(penaltyChargesForInstallment.getAmount()); - totalRepaymentExpected = totalRepaymentExpected.add(totalInstallmentDue.getAmount()); - startDate = scheduledDueDate; - - periodNumber++; - } - - final CurrencyData currencyData = new CurrencyData(currency.getCode(), currency.getName(), - monetaryCurrency.getDigitsAfterDecimal(), currency.getDisplaySymbol(), currency.getNameCode()); - - return new LoanScheduleData(currencyData, periods, loanTermInDays, totalPrincipalDisbursed, totalPrincipalExpected, - totalPrincipalPaid, totalInterestCharged, totalFeeChargesCharged, totalPenaltyChargesCharged, totalWaived, totalWrittenOff, - totalRepaymentExpected, totalRepayment, totalOutstanding); - } - - private Money cumulativeFeeChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, final Set loanCharges, - final MonetaryCurrency monetaryCurrency) { - - Money cumulative = Money.zero(monetaryCurrency); - - for (LoanCharge loanCharge : loanCharges) { - if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd) && loanCharge.isFeeCharge()) { - cumulative = cumulative.plus(loanCharge.amount()); - } - } - - return cumulative; - } - - private Money cumulativePenaltyChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, - final Set loanCharges, final MonetaryCurrency monetaryCurrency) { - - Money cumulative = Money.zero(monetaryCurrency); - - for (LoanCharge loanCharge : loanCharges) { - if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd) && loanCharge.isPenaltyCharge()) { - cumulative = cumulative.plus(loanCharge.amount()); - } - } - - return cumulative; - } -} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java new file mode 100644 index 000000000..f1711f334 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java @@ -0,0 +1,691 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.util.Date; + +import org.joda.time.LocalDate; +import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; +import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; +import org.mifosplatform.organisation.monetary.domain.Money; +import org.mifosplatform.portfolio.loanproduct.domain.AmortizationMethod; +import org.mifosplatform.portfolio.loanproduct.domain.InterestCalculationPeriodMethod; +import org.mifosplatform.portfolio.loanproduct.domain.InterestMethod; +import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; +import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType; + +public final class LoanApplicationTerms { + + private final ApplicationCurrency currency; + private final Integer loanTermFrequency; + private final PeriodFrequencyType loanTermPeriodFrequencyType; + private final Integer numberOfRepayments; + private final Integer repaymentEvery; + private final PeriodFrequencyType repaymentPeriodFrequencyType; + private final AmortizationMethod amortizationMethod; + + private final InterestMethod interestMethod; + private final BigDecimal interestRatePerPeriod; + private final PeriodFrequencyType interestRatePeriodFrequencyType; + private final BigDecimal annualNominalInterestRate; + private final InterestCalculationPeriodMethod interestCalculationPeriodMethod; + + private final Money principal; + private final LocalDate expectedDisbursementDate; + private final LocalDate repaymentsStartingFromDate; + + /** + * Integer representing the number of 'repayment frequencies' or + * installments where 'grace' should apply to the principal component of a + * loans repayment period (installment). + */ + private final Integer principalGrace; + + /** + * Integer representing the number of 'repayment frequencies' or + * installments where 'grace' should apply to the payment of interest in a + * loans repayment period (installment). + * + * Note: Interest is still calculated taking into account the full + * loan term, the interest is simply offset to a later period. + */ + private final Integer interestPaymentGrace; + + /** + * Integer representing the number of 'repayment frequencies' or + * installments where 'grace' should apply to the charging of interest in a + * loans repayment period (installment). + * + * Note: The loan is interest-free for the period of time + * indicated. + */ + private final Integer interestChargingGrace; + + /** + * Legacy method of support 'grace' on the charging of interest on a loan. + * + *

+ * For the typical structured loan, its reasonable to use an integer to + * indicate the number of 'repayment frequency' periods the 'grace' should + * apply to but for slightly irregular loans where the period between + * disbursement and the date of the 'first repayment period' isnt doest + * match the 'repayment frequency' but can be less (15days instead of 1 + * month) or more (6 weeks instead of 1 month) - The idea was to use a date + * to indicate from whence interest should be charged. + *

+ */ + private final LocalDate interestChargedFromDate; + private final Money inArrearsTolerance; + + public static LoanApplicationTerms assembleFrom(final ApplicationCurrency currency, final Integer loanTermFrequency, + final PeriodFrequencyType loanTermPeriodFrequencyType, final Integer numberOfRepayments, final Integer repaymentEvery, + final PeriodFrequencyType repaymentPeriodFrequencyType, final AmortizationMethod amortizationMethod, + final InterestMethod interestMethod, final BigDecimal interestRatePerPeriod, + final PeriodFrequencyType interestRatePeriodFrequencyType, final BigDecimal annualNominalInterestRate, + final InterestCalculationPeriodMethod interestCalculationPeriodMethod, final Money principalMoney, + final LocalDate expectedDisbursementDate, final LocalDate repaymentsStartingFromDate, final Integer graceOnPrincipalPayment, + final Integer graceOnInterestPayment, final Integer graceOnInterestCharged, final LocalDate interestChargedFromDate, + final Money inArrearsTolerance) { + + return new LoanApplicationTerms(currency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments, repaymentEvery, + repaymentPeriodFrequencyType, amortizationMethod, interestMethod, interestRatePerPeriod, interestRatePeriodFrequencyType, + annualNominalInterestRate, interestCalculationPeriodMethod, principalMoney, expectedDisbursementDate, + repaymentsStartingFromDate, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, + interestChargedFromDate, inArrearsTolerance); + } + + public static LoanApplicationTerms assembleFrom(final ApplicationCurrency applicationCurrency, final Integer loanTermFrequency, + final PeriodFrequencyType loanTermPeriodFrequencyType, final LocalDate expectedDisbursementDate, + final LocalDate repaymentsStartingFromDate, final Money inArrearsTolerance, + final LoanProductRelatedDetail loanProductRelatedDetail) { + + final Integer numberOfRepayments = loanProductRelatedDetail.getNumberOfRepayments(); + final Integer repaymentEvery = loanProductRelatedDetail.getRepayEvery(); + final PeriodFrequencyType repaymentPeriodFrequencyType = loanProductRelatedDetail.getRepaymentPeriodFrequencyType(); + final AmortizationMethod amortizationMethod = loanProductRelatedDetail.getAmortizationMethod(); + final InterestMethod interestMethod = loanProductRelatedDetail.getInterestMethod(); + final BigDecimal interestRatePerPeriod = loanProductRelatedDetail.getNominalInterestRatePerPeriod(); + final PeriodFrequencyType interestRatePeriodFrequencyType = loanProductRelatedDetail.getInterestPeriodFrequencyType(); + final BigDecimal annualNominalInterestRate = loanProductRelatedDetail.getAnnualNominalInterestRate(); + final InterestCalculationPeriodMethod interestCalculationPeriodMethod = loanProductRelatedDetail + .getInterestCalculationPeriodMethod(); + final Money principalMoney = loanProductRelatedDetail.getPrincipal(); + + // + final Integer graceOnPrincipalPayment = 0; + final Integer graceOnInterestPayment = 0; + final Integer graceOnInterestCharged = 0; + final LocalDate interestChargedFromDate = null; + + return new LoanApplicationTerms(applicationCurrency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments, + repaymentEvery, repaymentPeriodFrequencyType, amortizationMethod, interestMethod, interestRatePerPeriod, + interestRatePeriodFrequencyType, annualNominalInterestRate, interestCalculationPeriodMethod, principalMoney, + expectedDisbursementDate, repaymentsStartingFromDate, graceOnPrincipalPayment, graceOnInterestPayment, + graceOnInterestCharged, interestChargedFromDate, inArrearsTolerance); + } + + private LoanApplicationTerms(final ApplicationCurrency currency, final Integer loanTermFrequency, + final PeriodFrequencyType loanTermPeriodFrequencyType, final Integer numberOfRepayments, final Integer repaymentEvery, + final PeriodFrequencyType repaymentPeriodFrequencyType, final AmortizationMethod amortizationMethod, + final InterestMethod interestMethod, final BigDecimal interestRatePerPeriod, + final PeriodFrequencyType interestRatePeriodFrequencyType, final BigDecimal annualNominalInterestRate, + final InterestCalculationPeriodMethod interestCalculationPeriodMethod, final Money principal, + final LocalDate expectedDisbursementDate, final LocalDate repaymentsStartingFromDate, final Integer principalGrace, + final Integer interestPaymentGrace, final Integer interestChargingGrace, final LocalDate interestChargedFromDate, + final Money inArrearsTolerance) { + this.currency = currency; + this.loanTermFrequency = loanTermFrequency; + this.loanTermPeriodFrequencyType = loanTermPeriodFrequencyType; + this.numberOfRepayments = numberOfRepayments; + this.repaymentEvery = repaymentEvery; + this.repaymentPeriodFrequencyType = repaymentPeriodFrequencyType; + this.amortizationMethod = amortizationMethod; + + this.interestMethod = interestMethod; + this.interestRatePerPeriod = interestRatePerPeriod; + this.interestRatePeriodFrequencyType = interestRatePeriodFrequencyType; + this.annualNominalInterestRate = annualNominalInterestRate; + this.interestCalculationPeriodMethod = interestCalculationPeriodMethod; + + this.principal = principal; + this.expectedDisbursementDate = expectedDisbursementDate; + this.repaymentsStartingFromDate = repaymentsStartingFromDate; + + this.principalGrace = principalGrace; + this.interestPaymentGrace = interestPaymentGrace; + this.interestChargingGrace = interestChargingGrace; + this.interestChargedFromDate = interestChargedFromDate; + + this.inArrearsTolerance = inArrearsTolerance; + } + + public Money adjustPrincipalIfLastRepaymentPeriod(final Money principalForPeriod, final Money totalCumulativePrincipalToDate, + final int periodNumber) { + + Money adjusted = principalForPeriod; + + // adjust cumulative fields for principal & interest if needed + if (isLastRepaymentPeriod(this.numberOfRepayments, periodNumber)) { + + final Money difference = totalCumulativePrincipalToDate.minus(this.principal); + if (difference.isLessThanZero()) { + adjusted = principalForPeriod.plus(difference.abs()); + } else if (difference.isGreaterThanZero()) { + adjusted = principalForPeriod.minus(difference.abs()); + } + } + + return adjusted; + } + + public Money adjustInterestIfLastRepaymentPeriod(final Money interestForThisPeriod, final Money totalCumulativeInterestToDate, + final Money totalInterestDueForLoan, final int periodNumber) { + + Money adjusted = interestForThisPeriod; + + if (isLastRepaymentPeriod(this.numberOfRepayments, periodNumber)) { + + final Money interestDifference = totalCumulativeInterestToDate.minus(totalInterestDueForLoan); + if (interestDifference.isLessThanZero()) { + adjusted = interestForThisPeriod.plus(interestDifference.abs()); + } else if (interestDifference.isGreaterThanZero()) { + adjusted = interestForThisPeriod.minus(interestDifference.abs()); + } + } + + return adjusted; + } + + /** + * Calculates the total interest to be charged on loan taking into account + * grace settings. + * + */ + public Money calculateTotalInterestCharged(final PaymentPeriodsInOneYearCalculator calculator, final MathContext mc) { + + Money totalInterestCharged = this.principal.zero(); + + switch (this.interestMethod) { + case FLAT: + final Money totalInterestChargedForLoanTerm = calculateTotalInterestDueWithoutGrace(calculator, mc); + + final Money totalInterestPerInstallment = calculateTotalInterestPerInstallmentWithoutGrace(calculator, mc); + + final Money totalGraceOnInterestCharged = totalInterestPerInstallment.multiplyRetainScale(getInterestChargingGrace(), + mc.getRoundingMode()); + + totalInterestCharged = totalInterestChargedForLoanTerm.minus(totalGraceOnInterestCharged); + break; + case DECLINING_BALANCE: + final BigDecimal periodicInterestRateForRepaymentPeriod = periodicInterestRate(calculator, mc); + + final Money totalRepaymentDueForLoanTerm = calculateTotalDueForEqualInstallmentLoan(periodicInterestRateForRepaymentPeriod); + + totalInterestCharged = totalRepaymentDueForLoanTerm.minus(this.principal); + break; + case INVALID: + break; + } + + return totalInterestCharged; + } + + public Money calculateTotalPrincipalForPeriod(final PaymentPeriodsInOneYearCalculator calculator, final int daysInPeriod, + final Money outstandingBalance, final int periodNumber, final MathContext mc) { + + Money principalForInstallment = this.principal.zero(); + + switch (this.interestMethod) { + case FLAT: + principalForInstallment = calculateTotalPrincipalPerPeriodWithoutGrace(mc); + if (isPrincipalGraceApplicableForThisPeriod(periodNumber)) { + principalForInstallment = Money.zero(principalForInstallment.getCurrency()); + } + break; + case DECLINING_BALANCE: + switch (this.amortizationMethod) { + case EQUAL_INSTALLMENTS: + // equal installments + int periodsElapsed = periodNumber - 1; + final BigDecimal periodicInterestRateForRepaymentPeriod = periodicInterestRate(calculator, mc); + + final Money totalPmtForThisInstallment = calculateTotalDueForEqualInstallmentRepaymentPeriod( + periodicInterestRateForRepaymentPeriod, outstandingBalance, periodsElapsed); + + principalForInstallment = calculatePrincipalDueForInstallment(calculator, mc, periodNumber, + totalPmtForThisInstallment, daysInPeriod, outstandingBalance); + break; + case EQUAL_PRINCIPAL: + principalForInstallment = calculateEqualPrincipalDueForInstallment(mc, periodNumber); + break; + case INVALID: + break; + } + break; + case INVALID: + break; + } + + return principalForInstallment; + } + + public PrincipalInterest calculateTotalInterestForPeriod(final PaymentPeriodsInOneYearCalculator calculator, + final double interestCalculationGraceOnRepaymentPeriodFraction, final int periodNumber, final MathContext mc, + final Money cumulatingInterestPaymentDueToGrace, final int daysInPeriod, final Money outstandingBalance) { + + Money interestForInstallment = this.principal.zero(); + Money interestBroughtForwardDueToGrace = cumulatingInterestPaymentDueToGrace.copy(); + + switch (this.interestMethod) { + case FLAT: + + switch (this.amortizationMethod) { + case EQUAL_INSTALLMENTS: + // average out outstanding interest over remaining + // instalments where interest is applicable + interestForInstallment = calculateTotalFlatInterestForInstallmentAveragingOutGracePeriods(calculator, periodNumber, + mc); + break; + case EQUAL_PRINCIPAL: + // interest follows time-value of money and is brought + // forward to next applicable interest payment period + PrincipalInterest result = calculateTotalFlatInterestForPeriod(calculator, periodNumber, mc, + interestBroughtForwardDueToGrace); + interestForInstallment = result.interest(); + interestBroughtForwardDueToGrace = result.interestPaymentDueToGrace(); + break; + case INVALID: + break; + } + break; + case DECLINING_BALANCE: + + Money interestForThisInstallmentBeforeGrace = calculateDecliningInterestDueForInstallmentBeforeApplyingGrace(calculator, + mc, daysInPeriod, outstandingBalance); + + Money interestForThisInstallmentAfterGrace = calculateDecliningInterestDueForInstallmentAfterApplyingGrace(calculator, + interestCalculationGraceOnRepaymentPeriodFraction, mc, daysInPeriod, outstandingBalance, periodNumber); + + interestForInstallment = interestForThisInstallmentAfterGrace; + if (interestForThisInstallmentAfterGrace.isGreaterThanZero()) { + interestForInstallment = interestBroughtForwardDueToGrace.plus(interestForThisInstallmentAfterGrace); + interestBroughtForwardDueToGrace = interestBroughtForwardDueToGrace.zero(); + } else if (isInterestFreeGracePeriod(periodNumber)) { + interestForInstallment = interestForInstallment.zero(); + } else if (isInterestFreeGracePeriodFromDate(interestCalculationGraceOnRepaymentPeriodFraction)) { + interestForInstallment = interestForThisInstallmentAfterGrace; + } else { + interestBroughtForwardDueToGrace = interestBroughtForwardDueToGrace.plus(interestForThisInstallmentBeforeGrace); + } + break; + case INVALID: + break; + } + + return new PrincipalInterest(null, interestForInstallment, interestBroughtForwardDueToGrace); + } + + private final boolean isLastRepaymentPeriod(final int numberOfRepayments, final int periodNumber) { + return periodNumber == numberOfRepayments; + } + + private Money calculateTotalDueForEqualInstallmentLoan(final BigDecimal periodicInterestRate) { + + final int periodsElapsed = 0; + final double paymentPerRepaymentPeriod = paymentPerPeriod(periodicInterestRate, this.principal, periodsElapsed); + final double totalRepayment = paymentPerRepaymentPeriod * this.numberOfRepayments; + + return Money.of(principal.getCurrency(), BigDecimal.valueOf(totalRepayment)); + } + + /** + * general method to calculate totalInterestDue discounting any grace + * settings + */ + private Money calculateTotalInterestDueWithoutGrace(final PaymentPeriodsInOneYearCalculator calculator, final MathContext mc) { + + Money totalInterestDue = this.principal.zero(); + + switch (this.interestMethod) { + case FLAT: + final BigDecimal interestRateForLoanTerm = calculateFlatInterestRateForLoanTerm(calculator, mc); + + totalInterestDue = this.principal.multiplyRetainScale(interestRateForLoanTerm, mc.getRoundingMode()); + break; + case DECLINING_BALANCE: + break; + case INVALID: + break; + } + + return totalInterestDue; + } + + private BigDecimal calculateFlatInterestRateForLoanTerm(final PaymentPeriodsInOneYearCalculator calculator, final MathContext mc) { + + final BigDecimal divisor = BigDecimal.valueOf(Double.valueOf("100.0")); + + final long loanTermPeriodsInOneYear = calculatePeriodsInOneYear(calculator); + final BigDecimal loanTermPeriodsInYearBigDecimal = BigDecimal.valueOf(loanTermPeriodsInOneYear); + + final BigDecimal loanTermFrequencyBigDecimal = BigDecimal.valueOf(this.loanTermFrequency); + + return this.annualNominalInterestRate.divide(loanTermPeriodsInYearBigDecimal, mc).divide(divisor, mc) + .multiply(loanTermFrequencyBigDecimal); + } + + private Money calculateTotalInterestPerInstallmentWithoutGrace(final PaymentPeriodsInOneYearCalculator calculator, final MathContext mc) { + + final Money totalInterestForLoanTerm = calculateTotalInterestDueWithoutGrace(calculator, mc); + + return totalInterestForLoanTerm.dividedBy(Long.valueOf(this.numberOfRepayments), mc.getRoundingMode()); + } + + private Money calculateTotalPrincipalPerPeriodWithoutGrace(final MathContext mc) { + final int totalRepaymentsWithCapitalPayment = calculateNumberOfRepaymentsWithPrincipalPayment(); + return this.principal.dividedBy(totalRepaymentsWithCapitalPayment, mc.getRoundingMode()); + } + + private PrincipalInterest calculateTotalFlatInterestForPeriod(final PaymentPeriodsInOneYearCalculator calculator, + final int periodNumber, final MathContext mc, final Money cumulatingInterestPaymentDueToGrace) { + + Money interestBroughtForwardDueToGrace = cumulatingInterestPaymentDueToGrace.copy(); + + Money interestForInstallment = calculateTotalInterestPerInstallmentWithoutGrace(calculator, mc); + if (isInterestPaymentGraceApplicableForThisPeriod(periodNumber)) { + interestBroughtForwardDueToGrace = interestBroughtForwardDueToGrace.plus(interestForInstallment); + interestForInstallment = interestForInstallment.zero(); + } else if (isInterestFreeGracePeriod(periodNumber)) { + interestForInstallment = interestForInstallment.zero(); + } else if (isFirstPeriodAfterInterestPaymentGracePeriod(periodNumber)) { + interestForInstallment = cumulatingInterestPaymentDueToGrace.plus(interestForInstallment); + interestBroughtForwardDueToGrace = interestBroughtForwardDueToGrace.zero(); + } + + return new PrincipalInterest(null, interestForInstallment, interestBroughtForwardDueToGrace); + } + + /* + * calculates the interest that should be due for a given scheduled loan + * repayment period. It takes into account GRACE periods and calculates how + * much interest is due per period by averaging the number of periods where + * interest is due and should be paid against the total known interest that + * is due without grace. + */ + private Money calculateTotalFlatInterestForInstallmentAveragingOutGracePeriods(final PaymentPeriodsInOneYearCalculator calculator, + final int periodNumber, final MathContext mc) { + + Money interestForInstallment = calculateTotalInterestPerInstallmentWithoutGrace(calculator, mc); + if (isInterestPaymentGraceApplicableForThisPeriod(periodNumber)) { + interestForInstallment = interestForInstallment.zero(); + } else if (isInterestFreeGracePeriod(periodNumber)) { + interestForInstallment = interestForInstallment.zero(); + } else { + + final Money totalInterestForLoanTerm = calculateTotalInterestDueWithoutGrace(calculator, mc); + + Money interestPerGracePeriod = calculateTotalInterestPerInstallmentWithoutGrace(calculator, mc); + + Money totalInterestFree = interestPerGracePeriod.multipliedBy(getInterestChargingGrace()); + Money realTotalInterestForLoan = totalInterestForLoanTerm.minus(totalInterestFree); + + Integer interestPaymentDuePeriods = calculateNumberOfRepaymentPeriodsWhereInterestPaymentIsDue(this.numberOfRepayments); + + interestForInstallment = realTotalInterestForLoan + .dividedBy(BigDecimal.valueOf(interestPaymentDuePeriods), mc.getRoundingMode()); + } + + return interestForInstallment; + } + + private BigDecimal periodicInterestRate(final PaymentPeriodsInOneYearCalculator calculator, final MathContext mc) { + + final long loanTermPeriodsInOneYear = calculatePeriodsInOneYear(calculator); + + final BigDecimal divisor = BigDecimal.valueOf(Double.valueOf("100.0")); + final BigDecimal loanTermPeriodsInYearBigDecimal = BigDecimal.valueOf(loanTermPeriodsInOneYear); + + final BigDecimal loanTermFrequencyBigDecimal = BigDecimal.valueOf(this.repaymentEvery); + + return this.annualNominalInterestRate.divide(loanTermPeriodsInYearBigDecimal, mc).divide(divisor, mc) + .multiply(loanTermFrequencyBigDecimal); + } + + private long calculatePeriodsInOneYear(final PaymentPeriodsInOneYearCalculator calculator) { + return calculator.calculate(this.repaymentPeriodFrequencyType).longValue(); + } + + private int calculateNumberOfRepaymentsWithPrincipalPayment() { + return this.numberOfRepayments - getPrincipalGrace(); + } + + private Integer calculateNumberOfRepaymentPeriodsWhereInterestPaymentIsDue(final Integer totalNumberOfRepaymentPeriods) { + return totalNumberOfRepaymentPeriods - Math.max(getInterestChargingGrace(), getInterestPaymentGrace()); + } + + private Integer calculateNumberOfPrincipalPaymentPeriods(final Integer totalNumberOfRepaymentPeriods) { + return totalNumberOfRepaymentPeriods - getPrincipalGrace(); + } + + private boolean isPrincipalGraceApplicableForThisPeriod(final int periodNumber) { + return periodNumber > 0 && periodNumber <= getPrincipalGrace(); + } + + private boolean isInterestPaymentGraceApplicableForThisPeriod(final int periodNumber) { + return periodNumber > 0 && periodNumber <= getInterestPaymentGrace(); + } + + private boolean isFirstPeriodAfterInterestPaymentGracePeriod(final int periodNumber) { + return periodNumber > 0 && periodNumber == getInterestPaymentGrace() + 1; + } + + private boolean isInterestFreeGracePeriod(final int periodNumber) { + return periodNumber > 0 && periodNumber <= getInterestChargingGrace(); + } + + private Integer getPrincipalGrace() { + Integer graceOnPrincipalPayments = Integer.valueOf(0); + if (this.principalGrace != null) { + graceOnPrincipalPayments = this.principalGrace; + } + return graceOnPrincipalPayments; + } + + private Integer getInterestPaymentGrace() { + Integer graceOnInterestPayments = Integer.valueOf(0); + if (this.interestPaymentGrace != null) { + graceOnInterestPayments = this.interestPaymentGrace; + } + return graceOnInterestPayments; + } + + private Integer getInterestChargingGrace() { + Integer graceOnInterestCharged = Integer.valueOf(0); + if (this.interestChargingGrace != null) { + graceOnInterestCharged = this.interestChargingGrace; + } + return graceOnInterestCharged; + } + + private double paymentPerPeriod(final BigDecimal periodicInterestRate, final Money balance, final int periodsElapsed) { + final double futureValue = 0; + final double principalDouble = balance.getAmount().multiply(BigDecimal.valueOf(-1)).doubleValue(); + + final Integer periodsRemaining = this.numberOfRepayments - periodsElapsed; + + return FinanicalFunctions.pmt(periodicInterestRate.doubleValue(), periodsRemaining.doubleValue(), principalDouble, futureValue, + false); + } + + private Money calculateDecliningInterestDueForInstallmentBeforeApplyingGrace(final PaymentPeriodsInOneYearCalculator calculator, + final MathContext mc, final int daysInPeriod, final Money outstandingBalance) { + + Money interestDue = Money.zero(outstandingBalance.getCurrency()); + + switch (this.interestCalculationPeriodMethod) { + case DAILY: + BigDecimal dailyInterestRate = this.annualNominalInterestRate.divide(BigDecimal.valueOf(Long.valueOf(365)), mc) + .divide(BigDecimal.valueOf(Double.valueOf("100.0")), mc).multiply(BigDecimal.valueOf(this.repaymentEvery)); + + BigDecimal equivalentInterestRateForPeriod = dailyInterestRate.multiply(BigDecimal.valueOf(Long.valueOf(daysInPeriod))); + + interestDue = outstandingBalance.multiplyRetainScale(equivalentInterestRateForPeriod, mc.getRoundingMode()); + break; + case SAME_AS_REPAYMENT_PERIOD: + final BigDecimal periodicInterestRate = periodicInterestRate(calculator, mc); + interestDue = outstandingBalance.multiplyRetainScale(periodicInterestRate, mc.getRoundingMode()); + break; + case INVALID: + break; + } + + return interestDue; + } + + private Money calculateDecliningInterestDueForInstallmentAfterApplyingGrace(final PaymentPeriodsInOneYearCalculator calculator, + final double interestCalculationGraceOnRepaymentPeriodFraction, final MathContext mc, final int daysInPeriod, + final Money outstandingBalance, final int periodNumber) { + + Money interest = calculateDecliningInterestDueForInstallmentBeforeApplyingGrace(calculator, mc, daysInPeriod, outstandingBalance); + + if (isInterestPaymentGraceApplicableForThisPeriod(periodNumber)) { + interest = interest.zero(); + } + + Double fraction = interestCalculationGraceOnRepaymentPeriodFraction; + + if (isInterestFreeGracePeriod(periodNumber)) { + interest = interest.zero(); + } else if (isInterestFreeGracePeriodFromDate(interestCalculationGraceOnRepaymentPeriodFraction)) { + + if (interestCalculationGraceOnRepaymentPeriodFraction >= Integer.valueOf(1).doubleValue()) { + interest = interest.zero(); + fraction = fraction - Integer.valueOf(1).doubleValue(); + + } else if (interestCalculationGraceOnRepaymentPeriodFraction > Double.valueOf("0.25") + && interestCalculationGraceOnRepaymentPeriodFraction < Integer.valueOf(1).doubleValue()) { + + Money graceOnInterestForRepaymentPeriod = interest.multipliedBy(interestCalculationGraceOnRepaymentPeriodFraction); + interest = interest.minus(graceOnInterestForRepaymentPeriod); + fraction = Double.valueOf("0"); + } + } + + return interest; + } + + private boolean isInterestFreeGracePeriodFromDate(final double interestCalculationGraceOnRepaymentPeriodFraction) { + return this.interestChargedFromDate != null && interestCalculationGraceOnRepaymentPeriodFraction > Double.valueOf("0.0"); + } + + private Money calculateEqualPrincipalDueForInstallment(final MathContext mc, final int periodNumber) { + + final Integer numberOfPrincipalPaymentPeriods = calculateNumberOfPrincipalPaymentPeriods(this.numberOfRepayments); + + Money principal = this.principal.dividedBy(numberOfPrincipalPaymentPeriods, mc.getRoundingMode()); + if (isPrincipalGraceApplicableForThisPeriod(periodNumber)) { + principal = principal.zero(); + } + return principal; + } + + private Money calculatePrincipalDueForInstallment(final PaymentPeriodsInOneYearCalculator calculator, final MathContext mc, + final int periodNumber, final Money totalDuePerInstallment, final int daysInPeriod, final Money outstandingBalance) { + + Money interestForThisInstallmentBeforeGrace = calculateDecliningInterestDueForInstallmentBeforeApplyingGrace(calculator, mc, + daysInPeriod, outstandingBalance); + + Money principal = totalDuePerInstallment.minus(interestForThisInstallmentBeforeGrace); + if (isPrincipalGraceApplicableForThisPeriod(periodNumber)) { + principal = principal.zero(); + } + return principal; + } + + private Money calculateTotalDueForEqualInstallmentRepaymentPeriod(final BigDecimal periodicInterestRate, final Money balance, + final int periodsElapsed) { + + final double paymentPerRepaymentPeriod = paymentPerPeriod(periodicInterestRate, balance, periodsElapsed); + + return Money.of(balance.getCurrency(), BigDecimal.valueOf(paymentPerRepaymentPeriod)); + } + + public LoanProductRelatedDetail toLoanProductRelatedDetail() { + final MonetaryCurrency currency = new MonetaryCurrency(this.currency.getCode(), this.currency.getDecimalPlaces()); + + return LoanProductRelatedDetail.createFrom(currency, this.principal.getAmount(), this.interestRatePerPeriod, + this.interestRatePeriodFrequencyType, this.annualNominalInterestRate, this.interestMethod, + this.interestCalculationPeriodMethod, this.repaymentEvery, this.repaymentPeriodFrequencyType, this.numberOfRepayments, + this.principalGrace, this.interestPaymentGrace, this.interestChargingGrace, this.amortizationMethod, + this.inArrearsTolerance.getAmount()); + } + + public Integer getLoanTermFrequency() { + return this.loanTermFrequency; + } + + public PeriodFrequencyType getLoanTermPeriodFrequencyType() { + return this.loanTermPeriodFrequencyType; + } + + public Integer getRepaymentEvery() { + return this.repaymentEvery; + } + + public PeriodFrequencyType getRepaymentPeriodFrequencyType() { + return this.repaymentPeriodFrequencyType; + } + + public Date getRepaymentStartFromDate() { + Date dateValue = null; + if (this.repaymentsStartingFromDate != null) { + dateValue = this.repaymentsStartingFromDate.toDate(); + } + return dateValue; + } + + public Date getInterestChargedFromDate() { + Date dateValue = null; + if (this.interestChargedFromDate != null) { + dateValue = this.interestChargedFromDate.toDate(); + } + return dateValue; + } + + public LocalDate getInterestChargedFromLocalDate() { + return this.interestChargedFromDate; + } + + public InterestMethod getInterestMethod() { + return this.interestMethod; + } + + public AmortizationMethod getAmortizationMethod() { + return this.amortizationMethod; + } + + public MonetaryCurrency getCurrency() { + return this.principal.getCurrency(); + } + + public Integer getNumberOfRepayments() { + return this.numberOfRepayments; + } + + public LocalDate getExpectedDisbursementDate() { + return this.expectedDisbursementDate; + } + + public LocalDate getRepaymentsStartingFromLocalDate() { + return this.repaymentsStartingFromDate; + } + + public Money getPrincipal() { + return this.principal; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanSchedule.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanSchedule.java deleted file mode 100644 index efa357a64..000000000 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanSchedule.java +++ /dev/null @@ -1,109 +0,0 @@ -/** - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. - */ -package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; - -import java.math.BigDecimal; -import java.util.Set; - -import org.joda.time.LocalDate; -import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; -import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; -import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; -import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData; -import org.mifosplatform.portfolio.loanproduct.domain.AmortizationMethod; -import org.mifosplatform.portfolio.loanproduct.domain.InterestCalculationPeriodMethod; -import org.mifosplatform.portfolio.loanproduct.domain.InterestMethod; -import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; -import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType; - -/** - * Domain representation of a Loan Schedule (not used for persistence) - */ -public final class LoanSchedule { - - private final LoanScheduleGenerator loanScheduleGenerator; - private final ApplicationCurrency applicationCurrency; - private final BigDecimal principal; - private final BigDecimal nominalInterestRatePerPeriod; - private final PeriodFrequencyType interestRatePeriodFrequencyType; - private final BigDecimal nominalAnnualInterestRate; - private final InterestMethod interestMethod; - private final InterestCalculationPeriodMethod interestCalculationPeriodMethod; - private final Integer repaymentEvery; - private final PeriodFrequencyType repaymentPeriodFrequencyType; - private final Integer numberOfRepayments; - private final AmortizationMethod amortizationMethod; - private final Integer loanTermFrequency; - private final PeriodFrequencyType loanTermPeriodFrequencyType; - private final LocalDate disbursementDate; - private final LocalDate repaymentStartFromDate; - private final LocalDate interestChargedFromDate; - private final BigDecimal inArrearsTolerance; - - private final Set loanCharges; - - public LoanSchedule(final LoanScheduleGenerator loanScheduleGenerator, final ApplicationCurrency applicationCurrency, - final BigDecimal principal, final BigDecimal nominalInterestRatePerPeriod, - final PeriodFrequencyType interestRatePeriodFrequencyType, final BigDecimal nominalAnnualInterestRate, - final InterestMethod interestMethod, final InterestCalculationPeriodMethod interestCalculationPeriodMethod, - final Integer repaymentEvery, final PeriodFrequencyType repaymentPeriodFrequencyType, final Integer numberOfRepayments, - final AmortizationMethod amortizationMethod, final Integer loanTermFrequency, - final PeriodFrequencyType loanTermPeriodFrequencyType, final Set loanCharges, final LocalDate disbursementDate, - final LocalDate repaymentStartFromDate, final LocalDate interestChargedFromDate, final BigDecimal inArrearsTolerance) { - this.loanScheduleGenerator = loanScheduleGenerator; - this.applicationCurrency = applicationCurrency; - this.principal = principal; - this.nominalInterestRatePerPeriod = nominalInterestRatePerPeriod; - this.interestRatePeriodFrequencyType = interestRatePeriodFrequencyType; - this.nominalAnnualInterestRate = nominalAnnualInterestRate; - this.interestMethod = interestMethod; - this.interestCalculationPeriodMethod = interestCalculationPeriodMethod; - this.repaymentEvery = repaymentEvery; - this.repaymentPeriodFrequencyType = repaymentPeriodFrequencyType; - this.numberOfRepayments = numberOfRepayments; - this.amortizationMethod = amortizationMethod; - this.loanTermFrequency = loanTermFrequency; - this.loanTermPeriodFrequencyType = loanTermPeriodFrequencyType; - this.loanCharges = loanCharges; - this.disbursementDate = disbursementDate; - this.repaymentStartFromDate = repaymentStartFromDate; - this.interestChargedFromDate = interestChargedFromDate; - this.inArrearsTolerance = inArrearsTolerance; - } - - public LoanScheduleData generate() { - return loanScheduleGenerator.generate(applicationCurrency, loanProductRelatedDetail(), loanTermFrequency, - loanTermPeriodFrequencyType, disbursementDate, repaymentStartFromDate, interestChargedFromDate, loanCharges); - } - - public LoanProductRelatedDetail loanProductRelatedDetail() { - final MonetaryCurrency currency = new MonetaryCurrency(applicationCurrency.getCode(), applicationCurrency.getDecimalPlaces()); - - return LoanProductRelatedDetail.createFrom(currency, principal, nominalInterestRatePerPeriod, interestRatePeriodFrequencyType, - nominalAnnualInterestRate, interestMethod, interestCalculationPeriodMethod, repaymentEvery, repaymentPeriodFrequencyType, - numberOfRepayments, amortizationMethod, this.inArrearsTolerance); - } - - public Integer getLoanTermFrequency() { - return this.loanTermFrequency; - } - - public PeriodFrequencyType getLoanTermPeriodFrequencyType() { - return this.loanTermPeriodFrequencyType; - } - - public LocalDate getDisbursementDate() { - return this.disbursementDate; - } - - public LocalDate getRepaymentStartFromDate() { - return this.repaymentStartFromDate; - } - - public LocalDate getInterestChargedFromDate() { - return this.interestChargedFromDate; - } -} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleGenerator.java index 376296be7..721fad5fb 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleGenerator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleGenerator.java @@ -5,19 +5,14 @@ */ package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; +import java.math.MathContext; import java.util.Set; -import org.joda.time.LocalDate; import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; -import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData; -import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; -import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType; public interface LoanScheduleGenerator { - LoanScheduleData generate(ApplicationCurrency applicationCurrency, LoanProductRelatedDetail loanScheduleInfo, - Integer loanTermFrequency, PeriodFrequencyType loanTermFrequencyType, LocalDate disbursementDate, LocalDate firstRepaymentDate, - LocalDate interestCalculatedFrom, Set loanCharges); - + LoanScheduleModel generate(MathContext mc, ApplicationCurrency applicationCurrency, LoanApplicationTerms loanApplicationTerms, + Set loanCharges); } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorFactory.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorFactory.java index f8e542094..d3e946981 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorFactory.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorFactory.java @@ -10,5 +10,4 @@ import org.mifosplatform.portfolio.loanproduct.domain.InterestMethod; public interface LoanScheduleGeneratorFactory { LoanScheduleGenerator create(InterestMethod interestMethod); - } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModel.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModel.java new file mode 100644 index 000000000..0a8ea9e54 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModel.java @@ -0,0 +1,85 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; + +import org.mifosplatform.organisation.monetary.data.CurrencyData; +import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; +import org.mifosplatform.organisation.monetary.domain.Money; +import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData; +import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; + +/** + * Domain representation of a Loan Schedule (not used for persistence) + */ +public final class LoanScheduleModel { + + private final Collection periods; + private final ApplicationCurrency applicationCurrency; + private final int loanTermInDays; + private final Money totalPrincipalDisbursed; + private final BigDecimal totalPrincipalExpected; + private final BigDecimal totalPrincipalPaid; + private final BigDecimal totalInterestCharged; + private final BigDecimal totalFeeChargesCharged; + private final BigDecimal totalPenaltyChargesCharged; + private final BigDecimal totalRepaymentExpected; + private final BigDecimal totalOutstanding; + + public static LoanScheduleModel from(final Collection periods, final ApplicationCurrency applicationCurrency, + final int loanTermInDays, final Money principalDisbursed, final BigDecimal totalPrincipalExpected, + final BigDecimal totalPrincipalPaid, final BigDecimal totalInterestCharged, final BigDecimal totalFeeChargesCharged, + final BigDecimal totalPenaltyChargesCharged, final BigDecimal totalRepaymentExpected, final BigDecimal totalOutstanding) { + + return new LoanScheduleModel(periods, applicationCurrency, loanTermInDays, principalDisbursed, totalPrincipalExpected, + totalPrincipalPaid, totalInterestCharged, totalFeeChargesCharged, totalPenaltyChargesCharged, totalRepaymentExpected, + totalOutstanding); + } + + private LoanScheduleModel(final Collection periods, final ApplicationCurrency applicationCurrency, + final int loanTermInDays, final Money principalDisbursed, final BigDecimal totalPrincipalExpected, + final BigDecimal totalPrincipalPaid, final BigDecimal totalInterestCharged, final BigDecimal totalFeeChargesCharged, + final BigDecimal totalPenaltyChargesCharged, final BigDecimal totalRepaymentExpected, final BigDecimal totalOutstanding) { + this.periods = periods; + this.applicationCurrency = applicationCurrency; + this.loanTermInDays = loanTermInDays; + this.totalPrincipalDisbursed = principalDisbursed; + this.totalPrincipalExpected = totalPrincipalExpected; + this.totalPrincipalPaid = totalPrincipalPaid; + this.totalInterestCharged = totalInterestCharged; + this.totalFeeChargesCharged = totalFeeChargesCharged; + this.totalPenaltyChargesCharged = totalPenaltyChargesCharged; + this.totalRepaymentExpected = totalRepaymentExpected; + this.totalOutstanding = totalOutstanding; + } + + public LoanScheduleData toData() { + + final int decimalPlaces = this.totalPrincipalDisbursed.getCurrencyDigitsAfterDecimal(); + final CurrencyData currency = applicationCurrency.toData(decimalPlaces); + + final Collection periodsData = new ArrayList(); + for (LoanScheduleModelPeriod modelPeriod : this.periods) { + periodsData.add(modelPeriod.toData()); + } + + BigDecimal totalWaived = null; + BigDecimal totalWrittenOff = null; + BigDecimal totalRepayment = null; + + return new LoanScheduleData(currency, periodsData, this.loanTermInDays, this.totalPrincipalDisbursed.getAmount(), + this.totalPrincipalExpected, this.totalPrincipalPaid, this.totalInterestCharged, this.totalFeeChargesCharged, + this.totalPenaltyChargesCharged, totalWaived, totalWrittenOff, this.totalRepaymentExpected, totalRepayment, + this.totalOutstanding); + } + + public Collection getPeriods() { + return this.periods; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelDisbursementPeriod.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelDisbursementPeriod.java new file mode 100644 index 000000000..61429b3ac --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelDisbursementPeriod.java @@ -0,0 +1,86 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; + +import java.math.BigDecimal; + +import org.joda.time.LocalDate; +import org.mifosplatform.organisation.monetary.domain.Money; +import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; + +/** + * Domain representation of a Loan Schedule Disbursement Period (not used for + * persistence) + */ +public final class LoanScheduleModelDisbursementPeriod implements LoanScheduleModelPeriod { + + @SuppressWarnings("unused") + private final int periodNumber; + private final LocalDate disbursementDate; + private final Money principalDisbursed; + private final BigDecimal chargesDueAtTimeOfDisbursement; + + public static LoanScheduleModelDisbursementPeriod disbursement(final LoanApplicationTerms loanApplicationTerms, + final BigDecimal chargesDueAtTimeOfDisbursement) { + + final int periodNumber = 0; + return new LoanScheduleModelDisbursementPeriod(periodNumber, loanApplicationTerms, chargesDueAtTimeOfDisbursement); + } + + private LoanScheduleModelDisbursementPeriod(final int periodNumber, final LoanApplicationTerms loanApplicationTerms, + final BigDecimal chargesDueAtTimeOfDisbursement) { + this.periodNumber = periodNumber; + this.disbursementDate = loanApplicationTerms.getExpectedDisbursementDate(); + this.principalDisbursed = loanApplicationTerms.getPrincipal(); + this.chargesDueAtTimeOfDisbursement = chargesDueAtTimeOfDisbursement; + } + + @Override + public LoanSchedulePeriodData toData() { + return LoanSchedulePeriodData.disbursementOnlyPeriod(this.disbursementDate, this.principalDisbursed.getAmount(), + this.chargesDueAtTimeOfDisbursement, false); + } + + @Override + public boolean isRepaymentPeriod() { + return false; + } + + @Override + public Integer periodNumber() { + return null; + } + + @Override + public LocalDate periodFromDate() { + return null; + } + + @Override + public LocalDate periodDueDate() { + return null; + } + + @Override + public BigDecimal principalDue() { + return null; + } + + @Override + public BigDecimal interestDue() { + return null; + } + + @Override + public BigDecimal feeChargesDue() { + return null; + } + + @Override + public BigDecimal penaltyChargesDue() { + return null; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelPeriod.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelPeriod.java new file mode 100644 index 000000000..5d7780033 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelPeriod.java @@ -0,0 +1,32 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; + +import java.math.BigDecimal; + +import org.joda.time.LocalDate; +import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; + +public interface LoanScheduleModelPeriod { + + LoanSchedulePeriodData toData(); + + boolean isRepaymentPeriod(); + + Integer periodNumber(); + + LocalDate periodFromDate(); + + LocalDate periodDueDate(); + + BigDecimal principalDue(); + + BigDecimal interestDue(); + + BigDecimal feeChargesDue(); + + BigDecimal penaltyChargesDue(); +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelRepaymentPeriod.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelRepaymentPeriod.java new file mode 100644 index 000000000..87727cb51 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelRepaymentPeriod.java @@ -0,0 +1,118 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; + +import java.math.BigDecimal; + +import org.joda.time.LocalDate; +import org.mifosplatform.organisation.monetary.domain.Money; +import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; + +/** + * Domain representation of a Loan Schedule Repayment Period (not used for + * persistence) + */ +public final class LoanScheduleModelRepaymentPeriod implements LoanScheduleModelPeriod { + + private final int periodNumber; + private final LocalDate fromDate; + private final LocalDate dueDate; + private final Money principalDue; + private final Money outstandingLoanBalance; + private final Money interestDue; + private final Money feeChargesDue; + private final Money penaltyChargesDue; + private final Money totalDue; + + public static LoanScheduleModelRepaymentPeriod repayment(final int periodNumber, final LocalDate startDate, + final LocalDate scheduledDueDate, final Money principalDue, final Money outstandingLoanBalance, final Money interestDue, + final Money feeChargesDue, final Money penaltyChargesDue, final Money totalDue) { + + return new LoanScheduleModelRepaymentPeriod(periodNumber, startDate, scheduledDueDate, principalDue, outstandingLoanBalance, + interestDue, feeChargesDue, penaltyChargesDue, totalDue); + } + + public LoanScheduleModelRepaymentPeriod(final int periodNumber, final LocalDate fromDate, final LocalDate dueDate, + final Money principalDue, final Money outstandingLoanBalance, final Money interestDue, final Money feeChargesDue, + final Money penaltyChargesDue, final Money totalDue) { + this.periodNumber = periodNumber; + this.fromDate = fromDate; + this.dueDate = dueDate; + this.principalDue = principalDue; + this.outstandingLoanBalance = outstandingLoanBalance; + this.interestDue = interestDue; + this.feeChargesDue = feeChargesDue; + this.penaltyChargesDue = penaltyChargesDue; + this.totalDue = totalDue; + } + + @Override + public LoanSchedulePeriodData toData() { + return LoanSchedulePeriodData.repaymentOnlyPeriod(periodNumber, this.fromDate, this.dueDate, this.principalDue.getAmount(), + this.outstandingLoanBalance.getAmount(), this.interestDue.getAmount(), this.feeChargesDue.getAmount(), + this.penaltyChargesDue.getAmount(), this.totalDue.getAmount()); + } + + @Override + public boolean isRepaymentPeriod() { + return true; + } + + @Override + public Integer periodNumber() { + return this.periodNumber; + } + + @Override + public LocalDate periodFromDate() { + return this.fromDate; + } + + @Override + public LocalDate periodDueDate() { + return this.dueDate; + } + + @Override + public BigDecimal principalDue() { + BigDecimal value = null; + if (this.principalDue != null) { + value = this.principalDue.getAmount(); + } + + return value; + } + + @Override + public BigDecimal interestDue() { + BigDecimal value = null; + if (this.interestDue != null) { + value = this.interestDue.getAmount(); + } + + return value; + } + + @Override + public BigDecimal feeChargesDue() { + BigDecimal value = null; + if (this.feeChargesDue != null) { + value = this.feeChargesDue.getAmount(); + } + + return value; + } + + @Override + public BigDecimal penaltyChargesDue() { + BigDecimal value = null; + if (this.penaltyChargesDue != null) { + value = this.penaltyChargesDue.getAmount(); + } + + return value; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/PaymentPeriodsInOneYearCalculator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/PaymentPeriodsInOneYearCalculator.java index a170cd71a..c8b5ae3d0 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/PaymentPeriodsInOneYearCalculator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/PaymentPeriodsInOneYearCalculator.java @@ -5,8 +5,6 @@ */ package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; -import java.util.List; - import org.joda.time.LocalDate; import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType; @@ -14,9 +12,7 @@ public interface PaymentPeriodsInOneYearCalculator { Integer calculate(PeriodFrequencyType repaymentFrequencyType); - double calculateRepaymentPeriodAsAFractionOfDays( - PeriodFrequencyType repaymentPeriodFrequencyType, - Integer every, LocalDate interestCalculatedFrom, - List scheduledDates, LocalDate disbursementDate); + double calculatePortionOfRepaymentPeriodInterestChargingGrace(LocalDate repaymentPeriodStartDate, LocalDate scheduledDueDate, + LocalDate interestChargedFromLocalDate, PeriodFrequencyType repaymentPeriodFrequencyType, Integer repaidEvery); -} +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/PeriodicInterestRateCalculator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/PeriodicInterestRateCalculator.java index 846ad07a4..5c2aa2def 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/PeriodicInterestRateCalculator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/PeriodicInterestRateCalculator.java @@ -13,75 +13,74 @@ import org.mifosplatform.organisation.monetary.domain.Money; import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType; +@Deprecated public class PeriodicInterestRateCalculator { - private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator = new DefaultPaymentPeriodsInOneYearCalculator(); - - public BigDecimal calculateFrom(final PeriodFrequencyType loanTermFrequencyType, final BigDecimal annualNominalInterestRate) { - - MathContext mc = new MathContext(8, RoundingMode.HALF_EVEN); - - Integer paymentPeriodsInOneYear = this.paymentPeriodsInOneYearCalculator.calculate(loanTermFrequencyType); - - BigDecimal divisor = BigDecimal.valueOf(paymentPeriodsInOneYear * 100); - return annualNominalInterestRate.divide(divisor, mc); - } - - public BigDecimal calculateFrom(final LoanProductRelatedDetail loanScheduleInfo) { - - MathContext mc = new MathContext(8, RoundingMode.HALF_EVEN); - - Integer paymentPeriodsInOneYear = this.paymentPeriodsInOneYearCalculator.calculate(loanScheduleInfo.getRepaymentPeriodFrequencyType()); - - BigDecimal divisor = BigDecimal.valueOf(paymentPeriodsInOneYear * 100); - BigDecimal numberOfPeriods = BigDecimal.valueOf(loanScheduleInfo.getRepayEvery()); - - return loanScheduleInfo.getAnnualNominalInterestRate().divide(divisor, mc).multiply(numberOfPeriods); - } + private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator = new DefaultPaymentPeriodsInOneYearCalculator(); - public Money calculateInterestOn( - final Money outstandingBalance, - final BigDecimal periodInterestRateForRepaymentPeriod, - final int daysInPeriod, - final LoanProductRelatedDetail loanScheduleInfo) { + public BigDecimal calculateFrom(final PeriodFrequencyType loanTermFrequencyType, final BigDecimal annualNominalInterestRate) { - MathContext mc = new MathContext(8, RoundingMode.HALF_EVEN); - Money interestDue = Money.zero(outstandingBalance.getCurrency()); - - switch (loanScheduleInfo.getInterestCalculationPeriodMethod()) { - case DAILY: - BigDecimal dailyInterestRate = loanScheduleInfo.getAnnualNominalInterestRate() - .divide(BigDecimal.valueOf(Long.valueOf(365)), mc) - .divide(BigDecimal.valueOf(Double.valueOf("100.0")), mc) - .multiply(BigDecimal.valueOf(loanScheduleInfo.getRepayEvery())); - - BigDecimal equivalentInterestRateForPeriod = dailyInterestRate.multiply(BigDecimal.valueOf(Long.valueOf(daysInPeriod))); - - interestDue = outstandingBalance.multiplyRetainScale(equivalentInterestRateForPeriod, RoundingMode.HALF_EVEN); - break; - default: - interestDue = outstandingBalance.multiplyRetainScale(periodInterestRateForRepaymentPeriod, RoundingMode.HALF_EVEN); - break; - } - - return interestDue; - } + MathContext mc = new MathContext(8, RoundingMode.HALF_EVEN); - public Money calculatePrincipalOn(Money totalDuePerInstallment, Money interestForInstallment, LoanProductRelatedDetail loanScheduleInfo) { - - Money principalDue = Money.zero(totalDuePerInstallment.getCurrency()); - - switch (loanScheduleInfo.getAmortizationMethod()) { - case EQUAL_PRINCIPAL: - principalDue = loanScheduleInfo.getPrincipal().dividedBy(loanScheduleInfo.getNumberOfRepayments(), RoundingMode.HALF_EVEN); - break; - case EQUAL_INSTALLMENTS: - principalDue = totalDuePerInstallment.minus(interestForInstallment); - break; - case INVALID: - break; - } - - return principalDue; - } + Integer paymentPeriodsInOneYear = this.paymentPeriodsInOneYearCalculator.calculate(loanTermFrequencyType); + + BigDecimal divisor = BigDecimal.valueOf(paymentPeriodsInOneYear * 100); + return annualNominalInterestRate.divide(divisor, mc); + } + + public BigDecimal calculateFrom(final LoanProductRelatedDetail loanScheduleInfo) { + + MathContext mc = new MathContext(8, RoundingMode.HALF_EVEN); + + Integer paymentPeriodsInOneYear = this.paymentPeriodsInOneYearCalculator.calculate(loanScheduleInfo + .getRepaymentPeriodFrequencyType()); + + BigDecimal divisor = BigDecimal.valueOf(paymentPeriodsInOneYear * 100); + BigDecimal numberOfPeriods = BigDecimal.valueOf(loanScheduleInfo.getRepayEvery()); + + return loanScheduleInfo.getAnnualNominalInterestRate().divide(divisor, mc).multiply(numberOfPeriods); + } + + public Money calculateInterestOn(final Money outstandingBalance, final BigDecimal periodInterestRateForRepaymentPeriod, + final int daysInPeriod, final LoanProductRelatedDetail loanScheduleInfo) { + + MathContext mc = new MathContext(8, RoundingMode.HALF_EVEN); + Money interestDue = Money.zero(outstandingBalance.getCurrency()); + + switch (loanScheduleInfo.getInterestCalculationPeriodMethod()) { + case DAILY: + BigDecimal dailyInterestRate = loanScheduleInfo.getAnnualNominalInterestRate() + .divide(BigDecimal.valueOf(Long.valueOf(365)), mc).divide(BigDecimal.valueOf(Double.valueOf("100.0")), mc) + .multiply(BigDecimal.valueOf(loanScheduleInfo.getRepayEvery())); + + BigDecimal equivalentInterestRateForPeriod = dailyInterestRate.multiply(BigDecimal.valueOf(Long.valueOf(daysInPeriod))); + + interestDue = outstandingBalance.multiplyRetainScale(equivalentInterestRateForPeriod, RoundingMode.HALF_EVEN); + break; + default: + interestDue = outstandingBalance.multiplyRetainScale(periodInterestRateForRepaymentPeriod, RoundingMode.HALF_EVEN); + break; + } + + return interestDue; + } + + public Money calculatePrincipalOn(final Money totalDuePerInstallment, final Money interestForInstallment, + final LoanProductRelatedDetail loanScheduleInfo) { + + Money principalDue = Money.zero(totalDuePerInstallment.getCurrency()); + + switch (loanScheduleInfo.getAmortizationMethod()) { + case EQUAL_PRINCIPAL: + principalDue = loanScheduleInfo.getPrincipal().dividedBy(loanScheduleInfo.getNumberOfRepayments(), RoundingMode.HALF_EVEN); + break; + case EQUAL_INSTALLMENTS: + principalDue = totalDuePerInstallment.minus(interestForInstallment); + break; + case INVALID: + break; + } + + return principalDue; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/PmtCalculator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/PmtCalculator.java deleted file mode 100644 index 929a36ba0..000000000 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/PmtCalculator.java +++ /dev/null @@ -1,100 +0,0 @@ -/** - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. - */ -package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; - -import java.math.BigDecimal; - -import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; -import org.mifosplatform.organisation.monetary.domain.Money; -import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; - -public class PmtCalculator { - - public Money calculatePaymentForOnePeriodFrom(final LoanProductRelatedDetail loanScheduleInfo, - final BigDecimal periodInterestRateForRepaymentPeriod, - final MonetaryCurrency monetaryCurrency) { - double interestRateFraction = periodInterestRateForRepaymentPeriod.doubleValue(); - - double futureValue = 0; - double numberOfPeriods = loanScheduleInfo.getNumberOfRepayments().doubleValue(); - double principal = loanScheduleInfo.getPrincipal().getAmount().multiply(BigDecimal.valueOf(-1)).doubleValue(); - - // work out the total payment (principal + interest components) for each installment due. - // use the period type and interest as expressed e.g. 2% per month or 24% per year even is 'daily' interest calculation is selected - double paymentPerInstallment = pmt(interestRateFraction, numberOfPeriods, principal, futureValue, false); - - return Money.of(monetaryCurrency, BigDecimal.valueOf(paymentPerInstallment)); - } - - public Money calculateTotalRepaymentFrom(final LoanProductRelatedDetail loanScheduleInfo, - final BigDecimal periodInterestRateForRepaymentPeriod, - final MonetaryCurrency monetaryCurrency) { - double interestRateFraction = periodInterestRateForRepaymentPeriod.doubleValue(); - - double futureValue = 0; - double numberOfPeriods = loanScheduleInfo.getNumberOfRepayments().doubleValue(); - double principal = loanScheduleInfo.getPrincipal().getAmount().multiply(BigDecimal.valueOf(-1)).doubleValue(); - - // work out the total payment (principal + interest components) for each installment due. - // use the period type and interest as expressed e.g. 2% per month or 24% per year even is 'daily' interest calculation is selected - double paymentPerInstallment = pmt(interestRateFraction, numberOfPeriods, principal, futureValue, false); - - double totalRepayment = paymentPerInstallment * loanScheduleInfo.getNumberOfRepayments(); - - return Money.of(monetaryCurrency, BigDecimal.valueOf(totalRepayment)); - } - - public Money calculateTotalRepaymentFrom( - final Money principal, - final Integer loanTermFrequency, - final BigDecimal periodInterestRateForLoanTermPeriod, - final MonetaryCurrency monetaryCurrency) { - - double interestRateFraction = periodInterestRateForLoanTermPeriod.doubleValue(); - - double futureValue = 0; - double numberOfPeriods = loanTermFrequency.doubleValue(); - double principalAmount = principal.getAmount().multiply(BigDecimal.valueOf(-1)).doubleValue(); - - // work out the total payment (principal + interest components) for each installment due. - // use the period type and interest as expressed e.g. 2% per month or 24% per year even is 'daily' interest calculation is selected - double paymentPerInstallment = pmt(interestRateFraction, numberOfPeriods, principalAmount, futureValue, false); - - double totalRepayment = paymentPerInstallment * loanTermFrequency; - - return Money.of(monetaryCurrency, BigDecimal.valueOf(totalRepayment)); - } - - /** - * PMT calculates a fixed monthly payment to be paid by borrower every 'period' to ensure loan is paid off in full (with interest). - * - * This monthly payment c depends upon - * the monthly interest rate r (expressed as a fraction, not a percentage, - * i.e., divide the quoted yearly percentage rate by 100 and by 12 to obtain - * the monthly interest rate), the number of monthly payments N called the - * loan's term, and the amount borrowed P known as the loan's principal; c - * is given by the formula: - * - * c = (r / (1 - (1 + r)^-N))P - * - * @param interestRateFraction - * @param numberOfPayments - * @param principal - * @param futureValue - * @param type - */ - private static double pmt(double interestRateFraction, double numberOfPayments, double principal, double futureValue, boolean type) { - double payment = 0; - if (interestRateFraction == 0) { - payment = -1 * (futureValue + principal) / numberOfPayments; - } else { - double r1 = interestRateFraction + 1; - payment = (futureValue + principal * Math.pow(r1, numberOfPayments)) * interestRateFraction - / ((type ? r1 : 1) * (1 - Math.pow(r1, numberOfPayments))); - } - return payment; - } -} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/PrincipalInterest.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/PrincipalInterest.java new file mode 100644 index 000000000..812263add --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/PrincipalInterest.java @@ -0,0 +1,33 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; + +import org.mifosplatform.organisation.monetary.domain.Money; + +public class PrincipalInterest { + + private final Money principal; + private final Money interest; + private final Money interestPaymentDueToGrace; + + public PrincipalInterest(final Money principal, final Money interest, final Money interestPaymentDueToGrace) { + this.principal = principal; + this.interest = interest; + this.interestPaymentDueToGrace = interestPaymentDueToGrace; + } + + public Money principal() { + return this.principal; + } + + public Money interest() { + return this.interest; + } + + public Money interestPaymentDueToGrace() { + return this.interestPaymentDueToGrace; + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/ScheduledDateGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/ScheduledDateGenerator.java index 2b374933d..ba8722e26 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/ScheduledDateGenerator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/ScheduledDateGenerator.java @@ -8,17 +8,12 @@ package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; import java.util.List; import org.joda.time.LocalDate; -import org.mifosplatform.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail; -import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; +import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType; public interface ScheduledDateGenerator { - List generate( - LoanProductMinimumRepaymentScheduleRelatedDetail loanScheduleDateInfo, - LocalDate disbursementDate, LocalDate firstRepaymentDate); - - LocalDate idealDisbursementDateBasedOnFirstRepaymentDate( - LoanProductRelatedDetail loanScheduleInfo, - List scheduledDates); + List generate(LoanApplicationTerms loanApplicationTerms); + LocalDate idealDisbursementDateBasedOnFirstRepaymentDate(PeriodFrequencyType repaymentPeriodFrequencyType, int repaidEvery, + List scheduledDates); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java index 8a98e7a63..980fe3554 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java @@ -6,6 +6,8 @@ package org.mifosplatform.portfolio.loanaccount.loanschedule.service; import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; import java.util.Set; import org.joda.time.LocalDate; @@ -13,16 +15,19 @@ import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper; import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; import org.mifosplatform.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; +import org.mifosplatform.organisation.monetary.domain.Money; import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.AprCalculator; -import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanSchedule; +import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory; +import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; import org.mifosplatform.portfolio.loanaccount.service.LoanChargeAssembler; import org.mifosplatform.portfolio.loanproduct.domain.AmortizationMethod; import org.mifosplatform.portfolio.loanproduct.domain.InterestCalculationPeriodMethod; import org.mifosplatform.portfolio.loanproduct.domain.InterestMethod; import org.mifosplatform.portfolio.loanproduct.domain.LoanProduct; +import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRepository; import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType; import org.mifosplatform.portfolio.loanproduct.exception.LoanProductNotFoundException; @@ -54,65 +59,96 @@ public class LoanScheduleAssembler { this.loanChargeAssembler = loanChargeAssembler; } - public LoanSchedule fromJson(final String json) { - final JsonElement element = fromApiJsonHelper.parse(json); - return fromJson(element); - } - - public LoanSchedule fromJson(final JsonElement element) { - return fromJson(element, null); - } - - public LoanSchedule fromJson(final JsonElement element, final BigDecimal inArrearsTolerance) { + public LoanApplicationTerms assembleLoanTerms(final JsonElement element) { final Long loanProductId = fromApiJsonHelper.extractLongNamed("productId", element); final LoanProduct loanProduct = this.loanProductRepository.findOne(loanProductId); if (loanProduct == null) { throw new LoanProductNotFoundException(loanProductId); } + return assembleLoanApplicationTermsFrom(element, loanProduct); + } + + private LoanApplicationTerms assembleLoanApplicationTermsFrom(final JsonElement element, final LoanProduct loanProduct) { + final MonetaryCurrency currency = loanProduct.getCurrency(); final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency); - final BigDecimal principal = fromApiJsonHelper.extractBigDecimalWithLocaleNamed("principal", element); - final BigDecimal interestRatePerPeriod = fromApiJsonHelper.extractBigDecimalWithLocaleNamed("interestRatePerPeriod", element); - //final Integer interestRateFrequencyType = fromApiJsonHelper.extractIntegerWithLocaleNamed("interestRateFrequencyType", element); - final Integer interestType = fromApiJsonHelper.extractIntegerWithLocaleNamed("interestType", element); - final Integer interestCalculationPeriodType = fromApiJsonHelper.extractIntegerWithLocaleNamed("interestCalculationPeriodType", - element); - - final InterestMethod interestMethod = InterestMethod.fromInt(interestType); - //final PeriodFrequencyType interestRatePeriodFrequencyType = PeriodFrequencyType.fromInt(interestRateFrequencyType); - - // PeriodFrequencyType is copied from Loan Product - final PeriodFrequencyType interestRatePeriodFrequencyType = loanProduct.loanProductRelatedDetail().getInterestPeriodFrequencyType(); - final InterestCalculationPeriodMethod interestCalculationPeriodMethod = InterestCalculationPeriodMethod - .fromInt(interestCalculationPeriodType); - - final BigDecimal defaultAnnualNominalInterestRate = this.aprCalculator.calculateFrom(interestRatePeriodFrequencyType, - interestRatePerPeriod); - - final Integer repaymentEvery = fromApiJsonHelper.extractIntegerWithLocaleNamed("repaymentEvery", element); - final Integer repaymentFrequencyType = fromApiJsonHelper.extractIntegerWithLocaleNamed("repaymentFrequencyType", element); - final Integer numberOfRepayments = fromApiJsonHelper.extractIntegerWithLocaleNamed("numberOfRepayments", element); - final Integer amortizationType = fromApiJsonHelper.extractIntegerWithLocaleNamed("amortizationType", element); - final PeriodFrequencyType repaymentPeriodFrequencyType = PeriodFrequencyType.fromInt(repaymentFrequencyType); - final AmortizationMethod amortizationMethod = AmortizationMethod.fromInt(amortizationType); - + // loan terms final Integer loanTermFrequency = fromApiJsonHelper.extractIntegerWithLocaleNamed("loanTermFrequency", element); final Integer loanTermFrequencyType = fromApiJsonHelper.extractIntegerWithLocaleNamed("loanTermFrequencyType", element); final PeriodFrequencyType loanTermPeriodFrequencyType = PeriodFrequencyType.fromInt(loanTermFrequencyType); - final LocalDate expectedDisbursementDate = fromApiJsonHelper.extractLocalDateNamed("expectedDisbursementDate", element); - final LocalDate repaymentsStartingFromDate = fromApiJsonHelper.extractLocalDateNamed("repaymentsStartingFromDate", element); + final Integer numberOfRepayments = fromApiJsonHelper.extractIntegerWithLocaleNamed("numberOfRepayments", element); + final Integer repaymentEvery = fromApiJsonHelper.extractIntegerWithLocaleNamed("repaymentEvery", element); + final Integer repaymentFrequencyType = fromApiJsonHelper.extractIntegerWithLocaleNamed("repaymentFrequencyType", element); + final PeriodFrequencyType repaymentPeriodFrequencyType = PeriodFrequencyType.fromInt(repaymentFrequencyType); + + final Integer amortizationType = fromApiJsonHelper.extractIntegerWithLocaleNamed("amortizationType", element); + final AmortizationMethod amortizationMethod = AmortizationMethod.fromInt(amortizationType); + + // interest terms + final Integer interestType = fromApiJsonHelper.extractIntegerWithLocaleNamed("interestType", element); + final InterestMethod interestMethod = InterestMethod.fromInt(interestType); + + final Integer interestCalculationPeriodType = fromApiJsonHelper.extractIntegerWithLocaleNamed("interestCalculationPeriodType", + element); + final InterestCalculationPeriodMethod interestCalculationPeriodMethod = InterestCalculationPeriodMethod + .fromInt(interestCalculationPeriodType); + + final BigDecimal interestRatePerPeriod = fromApiJsonHelper.extractBigDecimalWithLocaleNamed("interestRatePerPeriod", element); + final PeriodFrequencyType interestRatePeriodFrequencyType = loanProduct.getInterestPeriodFrequencyType(); + + final BigDecimal annualNominalInterestRate = this.aprCalculator.calculateFrom(interestRatePeriodFrequencyType, + interestRatePerPeriod); + + // disbursement details + final BigDecimal principal = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("principal", element); + final Money principalMoney = Money.of(currency, principal); + + final LocalDate expectedDisbursementDate = this.fromApiJsonHelper.extractLocalDateNamed("expectedDisbursementDate", element); + final LocalDate repaymentsStartingFromDate = this.fromApiJsonHelper.extractLocalDateNamed("repaymentsStartingFromDate", element); + + // grace details + final Integer graceOnPrincipalPayment = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnPrincipalPayment", element); + final Integer graceOnInterestPayment = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnInterestPayment", element); + final Integer graceOnInterestCharged = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnInterestCharged", element); final LocalDate interestChargedFromDate = fromApiJsonHelper.extractLocalDateNamed("interestChargedFromDate", element); + // other + final BigDecimal inArrearsTolerance = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("inArrearsTolerance", element); + final Money inArrearsToleranceMoney = Money.of(currency, inArrearsTolerance); + + return LoanApplicationTerms.assembleFrom(applicationCurrency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments, + repaymentEvery, repaymentPeriodFrequencyType, amortizationMethod, interestMethod, interestRatePerPeriod, + interestRatePeriodFrequencyType, annualNominalInterestRate, interestCalculationPeriodMethod, principalMoney, + expectedDisbursementDate, repaymentsStartingFromDate, graceOnPrincipalPayment, graceOnInterestPayment, + graceOnInterestCharged, interestChargedFromDate, inArrearsToleranceMoney); + } + + public LoanProductRelatedDetail assembleLoanProductRelatedDetail(final JsonElement element) { + LoanApplicationTerms loanApplicationTerms = assembleLoanTerms(element); + return loanApplicationTerms.toLoanProductRelatedDetail(); + } + + public LoanScheduleModel assembleLoanScheduleFrom(final JsonElement element) { + + final LoanApplicationTerms loanApplicationTerms = assembleLoanTerms(element); + + return assembleLoanScheduleFrom(loanApplicationTerms, element); + } + + public LoanScheduleModel assembleLoanScheduleFrom(final LoanApplicationTerms loanApplicationTerms, final JsonElement element) { + final Set loanCharges = this.loanChargeAssembler.fromParsedJson(element); - final LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory.create(interestMethod); + final LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory.create(loanApplicationTerms.getInterestMethod()); - return new LoanSchedule(loanScheduleGenerator, applicationCurrency, principal, interestRatePerPeriod, - interestRatePeriodFrequencyType, defaultAnnualNominalInterestRate, interestMethod, interestCalculationPeriodMethod, - repaymentEvery, repaymentPeriodFrequencyType, numberOfRepayments, amortizationMethod, loanTermFrequency, - loanTermPeriodFrequencyType, loanCharges, expectedDisbursementDate, repaymentsStartingFromDate, interestChargedFromDate, - inArrearsTolerance); + final RoundingMode roundingMode = RoundingMode.HALF_EVEN; + final MathContext mc = new MathContext(8, roundingMode); + + final MonetaryCurrency currency = loanApplicationTerms.getCurrency(); + final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency); + + return loanScheduleGenerator.generate(mc, applicationCurrency, loanApplicationTerms, loanCharges); } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformService.java index a8cc2b393..e95a84fe5 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformService.java @@ -6,9 +6,9 @@ package org.mifosplatform.portfolio.loanaccount.loanschedule.service; import org.mifosplatform.infrastructure.core.api.JsonQuery; -import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData; +import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; public interface LoanScheduleCalculationPlatformService { - LoanScheduleData calculateLoanSchedule(JsonQuery query); + LoanScheduleModel calculateLoanSchedule(JsonQuery query); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformServiceImpl.java index b43bc59b2..11619237b 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformServiceImpl.java @@ -6,9 +6,7 @@ package org.mifosplatform.portfolio.loanaccount.loanschedule.service; import org.mifosplatform.infrastructure.core.api.JsonQuery; -import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; -import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData; -import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanSchedule; +import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; import org.mifosplatform.portfolio.loanaccount.serialization.CalculateLoanScheduleQueryFromApiJsonHelper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -16,26 +14,21 @@ import org.springframework.stereotype.Service; @Service public class LoanScheduleCalculationPlatformServiceImpl implements LoanScheduleCalculationPlatformService { - private final PlatformSecurityContext context; private final CalculateLoanScheduleQueryFromApiJsonHelper fromApiJsonDeserializer; private final LoanScheduleAssembler loanScheduleAssembler; @Autowired - public LoanScheduleCalculationPlatformServiceImpl(final PlatformSecurityContext context, - final CalculateLoanScheduleQueryFromApiJsonHelper fromApiJsonDeserializer, + public LoanScheduleCalculationPlatformServiceImpl(final CalculateLoanScheduleQueryFromApiJsonHelper fromApiJsonDeserializer, final LoanScheduleAssembler loanScheduleAssembler) { - this.context = context; this.fromApiJsonDeserializer = fromApiJsonDeserializer; this.loanScheduleAssembler = loanScheduleAssembler; } @Override - public LoanScheduleData calculateLoanSchedule(final JsonQuery query) { - context.authenticatedUser(); + public LoanScheduleModel calculateLoanSchedule(final JsonQuery query) { this.fromApiJsonDeserializer.validate(query.json()); - final LoanSchedule loanSchedule = this.loanScheduleAssembler.fromJson(query.parsedJson()); - return loanSchedule.generate(); + return this.loanScheduleAssembler.assembleLoanScheduleFrom(query.parsedJson()); } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/CalculateLoanScheduleQueryFromApiJsonHelper.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/CalculateLoanScheduleQueryFromApiJsonHelper.java index 8d4215b1a..585cf2186 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/CalculateLoanScheduleQueryFromApiJsonHelper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/CalculateLoanScheduleQueryFromApiJsonHelper.java @@ -40,8 +40,9 @@ public final class CalculateLoanScheduleQueryFromApiJsonHelper { "productId", "accountNo", "externalId", "fundId", "loanOfficerId", "loanPurposeId", "transactionProcessingStrategyId", "principal", "inArrearsTolerance", "interestRatePerPeriod", "repaymentEvery", "numberOfRepayments", "loanTermFrequency", "loanTermFrequencyType", "repaymentFrequencyType", "interestRateFrequencyType", "amortizationType", "interestType", - "interestCalculationPeriodType", "expectedDisbursementDate", "repaymentsStartingFromDate", "interestChargedFromDate", - "submittedOnDate", "submittedOnNote", "locale", "dateFormat", "charges", "collateral")); + "interestCalculationPeriodType", "expectedDisbursementDate", "repaymentsStartingFromDate", "graceOnPrincipalPayment", + "graceOnInterestPayment", "graceOnInterestCharged", "interestChargedFromDate", "submittedOnDate", "submittedOnNote", "locale", + "dateFormat", "charges", "collateral")); private final FromJsonHelper fromApiJsonHelper; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java index f31d7b1dd..b6be5dddc 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java @@ -40,8 +40,9 @@ public final class LoanApplicationCommandFromApiJsonHelper { */ final Set supportedParameters = new HashSet(Arrays.asList("dateFormat", "locale", "id", "clientId", "groupId", "loanType", "productId", "principal", "loanTermFrequency", "loanTermFrequencyType", "numberOfRepayments", "repaymentEvery", - "repaymentFrequencyType", "interestRatePerPeriod", "amortizationType", "interestType", - "interestCalculationPeriodType", "expectedDisbursementDate", "repaymentsStartingFromDate", "interestChargedFromDate", + "repaymentFrequencyType", "interestRatePerPeriod", "amortizationType", "interestType", "interestCalculationPeriodType", + "expectedDisbursementDate", "repaymentsStartingFromDate", "graceOnPrincipalPayment", "graceOnInterestPayment", + "graceOnInterestCharged", "interestChargedFromDate", // "submittedOnDate", "submittedOnNote", // "accountNo", "externalId", "fundId", "loanOfficerId", // optional "loanPurposeId", "inArrearsTolerance", "charges", "collateral", // optional @@ -50,7 +51,7 @@ public final class LoanApplicationCommandFromApiJsonHelper { )); private final FromJsonHelper fromApiJsonHelper; - + @Autowired public LoanApplicationCommandFromApiJsonHelper(final FromJsonHelper fromApiJsonHelper) { this.fromApiJsonHelper = fromApiJsonHelper; @@ -70,14 +71,14 @@ public final class LoanApplicationCommandFromApiJsonHelper { final String loanTypeParameterName = "loanType"; final String loanTypeStr = fromApiJsonHelper.extractStringNamed(loanTypeParameterName, element); baseDataValidator.reset().parameter(loanTypeParameterName).value(loanTypeStr).notNull(); - - if(!StringUtils.isBlank(loanTypeStr)){ + + if (!StringUtils.isBlank(loanTypeStr)) { final LoanType loanType = LoanType.fromName(loanTypeStr); baseDataValidator.reset().parameter(loanTypeParameterName).value(loanType.getValue()).inMinMaxRange(1, 3); - + final Long clientId = fromApiJsonHelper.extractLongNamed("clientId", element); final Long groupId = fromApiJsonHelper.extractLongNamed("groupId", element); - if(loanType.isIndividualLoan()){ + if (loanType.isIndividualLoan()) { baseDataValidator.reset().parameter("clientId").value(clientId).notNull().longGreaterThanZero(); baseDataValidator.reset().parameter("groupId").value(groupId).mustBeBlankWhenParameterProvided("clientId", clientId); } @@ -93,7 +94,7 @@ public final class LoanApplicationCommandFromApiJsonHelper { } } - + final Long productId = fromApiJsonHelper.extractLongNamed("productId", element); baseDataValidator.reset().parameter("productId").value(productId).notNull().integerGreaterThanZero(); @@ -176,13 +177,15 @@ public final class LoanApplicationCommandFromApiJsonHelper { final LocalDate expectedDisbursementDate = fromApiJsonHelper.extractLocalDateNamed(expectedDisbursementDateParameterName, element); baseDataValidator.reset().parameter(expectedDisbursementDateParameterName).value(expectedDisbursementDate).notNull(); - final String repaymentsStartingFromDateParameterName = "repaymentsStartingFromDate"; - if (fromApiJsonHelper.parameterExists(repaymentsStartingFromDateParameterName, element)) { - final LocalDate repaymentsStartingFromDate = fromApiJsonHelper.extractLocalDateNamed(repaymentsStartingFromDateParameterName, - element); - baseDataValidator.reset().parameter(repaymentsStartingFromDateParameterName).value(repaymentsStartingFromDate).ignoreIfNull() - .notNull(); - } + // grace validation + final Integer graceOnPrincipalPayment = fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnPrincipalPayment", element); + baseDataValidator.reset().parameter("graceOnPrincipalPayment").value(graceOnPrincipalPayment).zeroOrPositiveAmount(); + + final Integer graceOnInterestPayment = fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnInterestPayment", element); + baseDataValidator.reset().parameter("graceOnInterestPayment").value(graceOnInterestPayment).zeroOrPositiveAmount(); + + final Integer graceOnInterestCharged = fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnInterestCharged", element); + baseDataValidator.reset().parameter("graceOnInterestCharged").value(graceOnInterestCharged).zeroOrPositiveAmount(); final String interestChargedFromDateParameterName = "interestChargedFromDate"; if (fromApiJsonHelper.parameterExists(interestChargedFromDateParameterName, element)) { @@ -192,6 +195,14 @@ public final class LoanApplicationCommandFromApiJsonHelper { .notNull(); } + final String repaymentsStartingFromDateParameterName = "repaymentsStartingFromDate"; + if (fromApiJsonHelper.parameterExists(repaymentsStartingFromDateParameterName, element)) { + final LocalDate repaymentsStartingFromDate = fromApiJsonHelper.extractLocalDateNamed(repaymentsStartingFromDateParameterName, + element); + baseDataValidator.reset().parameter(repaymentsStartingFromDateParameterName).value(repaymentsStartingFromDate).ignoreIfNull() + .notNull(); + } + final String inArrearsToleranceParameterName = "inArrearsTolerance"; if (fromApiJsonHelper.parameterExists(inArrearsToleranceParameterName, element)) { final BigDecimal inArrearsTolerance = fromApiJsonHelper.extractBigDecimalWithLocaleNamed(inArrearsToleranceParameterName, @@ -304,7 +315,7 @@ public final class LoanApplicationCommandFromApiJsonHelper { final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan"); final JsonElement element = fromApiJsonHelper.parse(json); boolean atLeastOneParameterPassedForUpdate = false; - + final String clientIdParameterName = "clientId"; if (fromApiJsonHelper.parameterExists(clientIdParameterName, element)) { atLeastOneParameterPassedForUpdate = true; @@ -459,12 +470,20 @@ public final class LoanApplicationCommandFromApiJsonHelper { baseDataValidator.reset().parameter(expectedDisbursementDateParameterName).value(expectedDisbursementDate).notNull(); } - final String repaymentsStartingFromDateParameterName = "repaymentsStartingFromDate"; - if (fromApiJsonHelper.parameterExists(repaymentsStartingFromDateParameterName, element)) { - atLeastOneParameterPassedForUpdate = true; - final LocalDate repaymentsStartingFromDate = fromApiJsonHelper.extractLocalDateNamed(repaymentsStartingFromDateParameterName, - element); - baseDataValidator.reset().parameter(repaymentsStartingFromDateParameterName).value(repaymentsStartingFromDate).ignoreIfNull(); + // grace validation + if (fromApiJsonHelper.parameterExists("graceOnPrincipalPayment", element)) { + final Integer graceOnPrincipalPayment = fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnPrincipalPayment", element); + baseDataValidator.reset().parameter("graceOnPrincipalPayment").value(graceOnPrincipalPayment).zeroOrPositiveAmount(); + } + + if (fromApiJsonHelper.parameterExists("graceOnInterestPayment", element)) { + final Integer graceOnInterestPayment = fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnInterestPayment", element); + baseDataValidator.reset().parameter("graceOnInterestPayment").value(graceOnInterestPayment).zeroOrPositiveAmount(); + } + + if (fromApiJsonHelper.parameterExists("graceOnInterestCharged", element)) { + final Integer graceOnInterestCharged = fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnInterestCharged", element); + baseDataValidator.reset().parameter("graceOnInterestCharged").value(graceOnInterestCharged).zeroOrPositiveAmount(); } final String interestChargedFromDateParameterName = "interestChargedFromDate"; @@ -475,6 +494,14 @@ public final class LoanApplicationCommandFromApiJsonHelper { baseDataValidator.reset().parameter(interestChargedFromDateParameterName).value(interestChargedFromDate).ignoreIfNull(); } + final String repaymentsStartingFromDateParameterName = "repaymentsStartingFromDate"; + if (fromApiJsonHelper.parameterExists(repaymentsStartingFromDateParameterName, element)) { + atLeastOneParameterPassedForUpdate = true; + final LocalDate repaymentsStartingFromDate = fromApiJsonHelper.extractLocalDateNamed(repaymentsStartingFromDateParameterName, + element); + baseDataValidator.reset().parameter(repaymentsStartingFromDateParameterName).value(repaymentsStartingFromDate).ignoreIfNull(); + } + final String submittedOnDateParameterName = "submittedOnDate"; if (fromApiJsonHelper.parameterExists(submittedOnDateParameterName, element)) { atLeastOneParameterPassedForUpdate = true; @@ -593,7 +620,7 @@ public final class LoanApplicationCommandFromApiJsonHelper { if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", dataValidationErrors); } } - + public void validateMinMaxConstraintValues(final JsonElement element, final LoanProduct loanProduct) { final List dataValidationErrors = new ArrayList(); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java index 34f0e8069..06a634447 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java @@ -51,8 +51,8 @@ import org.mifosplatform.portfolio.loanaccount.exception.LoanApplicationDateExce import org.mifosplatform.portfolio.loanaccount.exception.LoanApplicationNotInSubmittedAndPendingApprovalStateCannotBeDeleted; import org.mifosplatform.portfolio.loanaccount.exception.LoanApplicationNotInSubmittedAndPendingApprovalStateCannotBeModified; import org.mifosplatform.portfolio.loanaccount.exception.LoanNotFoundException; -import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.AprCalculator; +import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; import org.mifosplatform.portfolio.loanaccount.loanschedule.service.LoanScheduleCalculationPlatformService; import org.mifosplatform.portfolio.loanaccount.serialization.LoanApplicationCommandFromApiJsonHelper; import org.mifosplatform.portfolio.loanaccount.serialization.LoanApplicationTransitionApiJsonValidator; @@ -60,7 +60,7 @@ import org.mifosplatform.portfolio.loanproduct.domain.LoanProduct; import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRepository; import org.mifosplatform.portfolio.loanproduct.domain.LoanTransactionProcessingStrategy; import org.mifosplatform.portfolio.loanproduct.exception.LoanProductNotFoundException; -import org.mifosplatform.portfolio.loanproduct.serialization.LoanProductCommandFromApiJsonDeserializer; +import org.mifosplatform.portfolio.loanproduct.serialization.LoanProductDataValidator; import org.mifosplatform.portfolio.note.domain.Note; import org.mifosplatform.portfolio.note.domain.NoteRepository; import org.mifosplatform.useradministration.domain.AppUser; @@ -81,7 +81,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa private final PlatformSecurityContext context; private final FromJsonHelper fromJsonHelper; private final LoanApplicationTransitionApiJsonValidator loanApplicationTransitionApiJsonValidator; - private final LoanProductCommandFromApiJsonDeserializer loanProductCommandFromApiJsonDeserializer; + private final LoanProductDataValidator loanProductCommandFromApiJsonDeserializer; private final LoanApplicationCommandFromApiJsonHelper fromApiJsonDeserializer; private final LoanRepository loanRepository; private final NoteRepository noteRepository; @@ -102,7 +102,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa public LoanApplicationWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context, final FromJsonHelper fromJsonHelper, final LoanApplicationTransitionApiJsonValidator loanApplicationTransitionApiJsonValidator, final LoanApplicationCommandFromApiJsonHelper fromApiJsonDeserializer, - final LoanProductCommandFromApiJsonDeserializer loanProductCommandFromApiJsonDeserializer, final AprCalculator aprCalculator, + final LoanProductDataValidator loanProductCommandFromApiJsonDeserializer, final AprCalculator aprCalculator, final LoanAssembler loanAssembler, final LoanChargeAssembler loanChargeAssembler, final CollateralAssembler loanCollateralAssembler, final LoanRepository loanRepository, final NoteRepository noteRepository, final LoanScheduleCalculationPlatformService calculationPlatformService, final ClientRepositoryWrapper clientRepository, @@ -294,7 +294,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa final JsonElement parsedQuery = this.fromJsonHelper.parse(command.json()); final JsonQuery query = JsonQuery.from(command.json(), parsedQuery, this.fromJsonHelper); - final LoanScheduleData loanSchedule = this.calculationPlatformService.calculateLoanSchedule(query); + final LoanScheduleModel loanSchedule = this.calculationPlatformService.calculateLoanSchedule(query); existingLoanApplication.updateLoanSchedule(loanSchedule); } @@ -536,14 +536,16 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa final LocalDate edDate = command.localDateValueOfParameterNamed("expectedDisbursementDate"); if (edDate != null) { - //Expected disbursement date should not be before Meeting start date - if(edDate.isBefore(calendar.getStartDateLocalDate())){ + // Expected disbursement date should not be before Meeting start + // date + if (edDate.isBefore(calendar.getStartDateLocalDate())) { final String errorMessage = "Expected disbursement date '" + edDate.toString() + "' cannot be before meeting start date '" + calendar.getStartDate().toString() + "' "; - throw new LoanApplicationDateException("disbursement.date.cannot.be.before.meeting.start.date", errorMessage, edDate, calendar.getStartDate()); + throw new LoanApplicationDateException("disbursement.date.cannot.be.before.meeting.start.date", errorMessage, edDate, + calendar.getStartDate()); } - - //Disbursement date should fall on a meeting date + + // Disbursement date should fall on a meeting date if (!CalendarHelper.isValidRedurringDate(calendar.getRecurrence(), calendar.getStartDateLocalDate(), edDate)) { final String errorMessage = "Expected disbursement date '" + edDate.toString() + "' does not fall on a meeting date."; throw new NotValidRecurringDateException("loan.expected.disbursement.date", errorMessage, edDate.toString(), @@ -553,18 +555,20 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa } final LocalDate repStartingDate = command.localDateValueOfParameterNamed("repaymentsStartingFromDate"); - + if (repStartingDate != null) { - //First repayment date should not be before Meeting date - if(repStartingDate.isBefore(calendar.getStartDateLocalDate())){ - final String errorMessage = "First repayment date '" + repStartingDate.toString() + "' cannot be before meeting start date '" - + calendar.getStartDate().toString() + "' "; - throw new LoanApplicationDateException("first.repayment.date.cannot.be.before.meeting.start.date", errorMessage, repStartingDate, calendar.getStartDate()); + // First repayment date should not be before Meeting date + if (repStartingDate.isBefore(calendar.getStartDateLocalDate())) { + final String errorMessage = "First repayment date '" + repStartingDate.toString() + + "' cannot be before meeting start date '" + calendar.getStartDate().toString() + "' "; + throw new LoanApplicationDateException("first.repayment.date.cannot.be.before.meeting.start.date", errorMessage, + repStartingDate, calendar.getStartDate()); } - + if (!CalendarHelper.isValidRedurringDate(calendar.getRecurrence(), calendar.getStartDateLocalDate(), repStartingDate)) { - final String errorMessage = "The First repayment date '" + repStartingDate.toString() + "' does not fall on a meeting date."; + final String errorMessage = "The First repayment date '" + repStartingDate.toString() + + "' does not fall on a meeting date."; throw new NotValidRecurringDateException("loan.first.repayment.date", errorMessage, repStartingDate.toString(), calendar.getTitle()); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanAssembler.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanAssembler.java index 21d649072..b8d500a26 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanAssembler.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanAssembler.java @@ -5,7 +5,6 @@ */ package org.mifosplatform.portfolio.loanaccount.service; -import java.math.BigDecimal; import java.util.Arrays; import java.util.List; import java.util.Set; @@ -40,9 +39,11 @@ import org.mifosplatform.portfolio.loanaccount.domain.LoanStatus; import org.mifosplatform.portfolio.loanaccount.domain.LoanSummaryWrapper; import org.mifosplatform.portfolio.loanaccount.domain.LoanTransactionProcessingStrategyRepository; import org.mifosplatform.portfolio.loanaccount.exception.LoanTransactionProcessingStrategyNotFoundException; -import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanSchedule; +import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; +import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; import org.mifosplatform.portfolio.loanaccount.loanschedule.service.LoanScheduleAssembler; import org.mifosplatform.portfolio.loanproduct.domain.LoanProduct; +import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRepository; import org.mifosplatform.portfolio.loanproduct.domain.LoanTransactionProcessingStrategy; import org.mifosplatform.portfolio.loanproduct.exception.InvalidCurrencyException; @@ -131,22 +132,21 @@ public class LoanAssembler { } } - final BigDecimal inArrearsTolerance = fromApiJsonHelper.extractBigDecimalWithLocaleNamed("inArrearsTolerance", element); - final LoanSchedule loanSchedule = this.loanScheduleAssembler.fromJson(element, inArrearsTolerance); - Loan loanApplication = null; Client client = null; Group group = null; - + + final LoanProductRelatedDetail loanProductRelatedDetail = this.loanScheduleAssembler.assembleLoanProductRelatedDetail(element); + final String loanTypeParameterName = "loanType"; final String loanTypeStr = fromApiJsonHelper.extractStringNamed(loanTypeParameterName, element); final EnumOptionData loanType = LoanEnumerations.loanType(loanTypeStr); - + if (clientId != null) { client = this.clientRepository.findOneWithNotFoundDetection(clientId); loanApplication = Loan.newIndividualLoanApplication(accountNo, client, loanType.getId().intValue(), loanProduct, fund, - loanOfficer, loanPurpose, loanTransactionProcessingStrategy, loanSchedule, loanCharges, collateral); + loanOfficer, loanPurpose, loanTransactionProcessingStrategy, loanProductRelatedDetail, loanCharges, collateral); } if (groupId != null) { @@ -154,15 +154,15 @@ public class LoanAssembler { if (group == null) { throw new GroupNotFoundException(groupId); } loanApplication = Loan.newGroupLoanApplication(accountNo, group, loanType.getId().intValue(), loanProduct, fund, loanOfficer, - loanTransactionProcessingStrategy, loanSchedule, loanCharges); + loanTransactionProcessingStrategy, loanProductRelatedDetail, loanCharges); } if (client != null && group != null) { if (!group.hasClientAsMember(client)) { throw new ClientNotInGroupException(clientId, groupId); } - loanApplication = Loan.newIndividualLoanApplicationFromGroup(accountNo, client, group, loanType.getId().intValue(), loanProduct, fund, loanOfficer, - loanTransactionProcessingStrategy, loanSchedule, loanCharges); + loanApplication = Loan.newIndividualLoanApplicationFromGroup(accountNo, client, group, loanType.getId().intValue(), + loanProduct, fund, loanOfficer, loanTransactionProcessingStrategy, loanProductRelatedDetail, loanCharges); } final String externalId = fromApiJsonHelper.extractStringNamed("externalId", element); @@ -172,8 +172,11 @@ public class LoanAssembler { loanApplication.setHelpers(defaultLoanLifecycleStateMachine(), this.loanSummaryWrapper, this.loanRepaymentScheduleTransactionProcessorFactory); - loanApplication - .loanApplicationSubmittal(currentUser, loanSchedule, defaultLoanLifecycleStateMachine(), submittedOnDate, externalId); + final LoanApplicationTerms loanApplicationTerms = this.loanScheduleAssembler.assembleLoanTerms(element); + final LoanScheduleModel loanScheduleModel = this.loanScheduleAssembler.assembleLoanScheduleFrom(loanApplicationTerms, element); + + loanApplication.loanApplicationSubmittal(currentUser, loanScheduleModel, loanApplicationTerms, defaultLoanLifecycleStateMachine(), + submittedOnDate, externalId); return loanApplication; } 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 1212b2bfc..65cb2f02a 100644 --- 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 @@ -95,7 +95,6 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { private final PaginationHelper paginationHelper = new PaginationHelper(); private final LoanMapper loaanLoanMapper = new LoanMapper(); - @Autowired public LoanReadPlatformServiceImpl(final PlatformSecurityContext context, final LoanRepository loanRepository, final LoanTransactionRepository loanTransactionRepository, @@ -402,6 +401,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { + " l.closedon_date as closedOnDate, cbu.username as closedByUsername, cbu.firstname as closedByFirstname, cbu.lastname as closedByLastname," + " l.expected_firstrepaymenton_date as expectedFirstRepaymentOnDate, l.interest_calculated_from_date as interestChargedFromDate, l.expected_maturedon_date as expectedMaturityDate, " + " l.principal_amount as principal, l.arrearstolerance_amount as inArrearsTolerance, l.number_of_repayments as numberOfRepayments, l.repay_every as repaymentEvery," + + " l.grace_on_principal_periods as graceOnPrincipalPayment, l.grace_on_interest_periods as graceOnInterestPayment, l.grace_interest_free_periods as graceOnInterestCharged," + " l.nominal_interest_rate_per_period as interestRatePerPeriod, l.annual_nominal_interest_rate as annualInterestRate, " + " l.repayment_period_frequency_enum as repaymentFrequencyType, l.interest_period_frequency_enum as interestRateFrequencyType, " + " l.term_frequency as termFrequency, l.term_period_frequency_enum as termPeriodFrequencyType, " @@ -550,6 +550,10 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { final BigDecimal interestRatePerPeriod = rs.getBigDecimal("interestRatePerPeriod"); final BigDecimal annualInterestRate = rs.getBigDecimal("annualInterestRate"); + final Integer graceOnPrincipalPayment = JdbcSupport.getIntegerDefaultToNullIfZero(rs, "graceOnPrincipalPayment"); + final Integer graceOnInterestPayment = JdbcSupport.getIntegerDefaultToNullIfZero(rs, "graceOnInterestPayment"); + final Integer graceOnInterestCharged = JdbcSupport.getIntegerDefaultToNullIfZero(rs, "graceOnInterestCharged"); + final Integer termFrequency = JdbcSupport.getInteger(rs, "termFrequency"); final Integer termPeriodFrequencyTypeInt = JdbcSupport.getInteger(rs, "termPeriodFrequencyType"); final EnumOptionData termPeriodFrequencyType = LoanEnumerations.termFrequencyType(termPeriodFrequencyTypeInt); @@ -645,7 +649,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { loanOfficerId, loanOfficerName, currencyData, principal, inArrearsTolerance, termFrequency, termPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentFrequencyType, transactionStrategyId, amortizationType, interestRatePerPeriod, interestRateFrequencyType, annualInterestRate, interestType, interestCalculationPeriodType, - expectedFirstRepaymentOnDate, interestChargedFromDate, timeline, loanSummary, feeChargesDueAtDisbursementCharged); + expectedFirstRepaymentOnDate, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, + interestChargedFromDate, timeline, loanSummary, feeChargesDueAtDisbursementCharged); } } @@ -864,7 +869,6 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { @Override public LoanAccountData retrieveLoanProductDetailsTemplate(final Long productId) { - context.authenticatedUser(); final LoanProductData loanProduct = this.loanProductReadPlatformService.retrieveLoanProduct(productId); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index 0102458e8..3ad2152e5 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -62,6 +62,7 @@ import org.mifosplatform.portfolio.loanaccount.exception.LoanNotFoundException; import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerAssignmentException; import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerUnassignmentException; import org.mifosplatform.portfolio.loanaccount.exception.LoanTransactionNotFoundException; +import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory; import org.mifosplatform.portfolio.loanaccount.serialization.LoanEventApiJsonValidator; import org.mifosplatform.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer; import org.mifosplatform.portfolio.loanproduct.exception.InvalidCurrencyException; @@ -91,6 +92,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf private final JournalEntryWritePlatformService journalEntryWritePlatformService; private final LoanSummaryWrapper loanSummaryWrapper; private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory; + private final LoanScheduleGeneratorFactory loanScheduleFactory; private final CalendarInstanceRepository calendarInstanceRepository; private final PaymentDetailWritePlatformService paymentDetailWritePlatformService; @@ -103,7 +105,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final LoanChargeRepository loanChargeRepository, final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository, final JournalEntryWritePlatformService journalEntryWritePlatformService, final LoanSummaryWrapper loanSummaryWrapper, final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory, - final CalendarInstanceRepository calendarInstanceRepository, + final LoanScheduleGeneratorFactory loanScheduleFactory, final CalendarInstanceRepository calendarInstanceRepository, final PaymentDetailWritePlatformService paymentDetailWritePlatformService) { this.context = context; this.loanEventApiJsonValidator = loanEventApiJsonValidator; @@ -118,6 +120,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf this.loanUpdateCommandFromApiJsonDeserializer = loanUpdateCommandFromApiJsonDeserializer; this.loanSummaryWrapper = loanSummaryWrapper; this.loanRepaymentScheduleTransactionProcessorFactory = loanRepaymentScheduleTransactionProcessorFactory; + this.loanScheduleFactory = loanScheduleFactory; this.calendarInstanceRepository = calendarInstanceRepository; this.paymentDetailWritePlatformService = paymentDetailWritePlatformService; } @@ -152,8 +155,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf PaymentDetail paymentDetail = paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); - loan.disburse(currentUser, command, applicationCurrency, existingTransactionIds, existingReversedTransactionIds, changes, - paymentDetail); + loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency, existingTransactionIds, + existingReversedTransactionIds, changes, paymentDetail); if (!changes.isEmpty()) { this.loanRepository.save(loan); @@ -178,7 +181,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf .build(); } - private void validateDisbursementDate(LocalDate disbursementDate, Collection calendarInstances) { + private void validateDisbursementDate(final LocalDate disbursementDate, final Collection calendarInstances) { if (!CollectionUtils.isEmpty(calendarInstances)) { Calendar calendar = null; for (CalendarInstance calendarInstance : calendarInstances) { @@ -208,16 +211,14 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } } + /**** + * TODO Vishwas: Pair with Ashok and re-factor collection sheet code-base + *****/ @Transactional @Override public Map bulkLoanDisbursal(final JsonCommand command, final CollectionSheetBulkDisbursalCommand bulkDisbursalCommand) { final AppUser currentUser = context.authenticatedUser(); - /**** - * TODO Vishwas: Pair with Ashok and re-factor collection sheet - * code-base - *****/ - SingleDisbursalCommand[] disbursalCommand = bulkDisbursalCommand.getDisburseTransactions(); Map changes = new LinkedHashMap(); if (disbursalCommand == null) { return changes; } @@ -234,8 +235,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf PaymentDetail paymentDetail = paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); - loan.disburse(currentUser, command, applicationCurrency, existingTransactionIds, existingReversedTransactionIds, changes, - paymentDetail); + loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency, existingTransactionIds, + existingReversedTransactionIds, changes, paymentDetail); if (!changes.isEmpty()) { this.loanRepository.save(loan); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/api/LoanProductsApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/api/LoanProductsApiResource.java index 32f0b7e45..cce082ab1 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/api/LoanProductsApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/api/LoanProductsApiResource.java @@ -61,13 +61,14 @@ public class LoanProductsApiResource { private final Set LOAN_PRODUCT_DATA_PARAMETERS = new HashSet(Arrays.asList("id", "name", "description", "fundId", "fundName", "currency", "principal", "minPrincipal", "maxPrincipal", "numberOfRepayments", "minNumberOfRepayments", - "maxNumberOfRepayments", "repaymentEvery", "repaymentFrequencyType", "interestRatePerPeriod", "minInterestRatePerPeriod", - "maxInterestRatePerPeriod", "interestRateFrequencyType", "annualInterestRate", "amortizationType", "interestType", - "interestCalculationPeriodType", "inArrearsTolerance", "transactionProcessingStrategyId", "transactionProcessingStrategyName", - "charges", "accountingRule", "accountingMappings", "paymentChannelToFundSourceMappings", "fundOptions", "paymentTypeOptions", - "currencyOptions", "repaymentFrequencyTypeOptions", "interestRateFrequencyTypeOptions", "amortizationTypeOptions", - "interestTypeOptions", "interestCalculationPeriodTypeOptions", "transactionProcessingStrategyOptions", "chargeOptions", - "accountingOptions", "accountingRuleOptions", "accountingMappingOptions")); + "maxNumberOfRepayments", "repaymentEvery", "repaymentFrequencyType", "graceOnPrincipalPayment", "graceOnInterestPayment", + "graceOnInterestCharged", "interestRatePerPeriod", "minInterestRatePerPeriod", "maxInterestRatePerPeriod", + "interestRateFrequencyType", "annualInterestRate", "amortizationType", "interestType", "interestCalculationPeriodType", + "inArrearsTolerance", "transactionProcessingStrategyId", "transactionProcessingStrategyName", "charges", "accountingRule", + "accountingMappings", "paymentChannelToFundSourceMappings", "fundOptions", "paymentTypeOptions", "currencyOptions", + "repaymentFrequencyTypeOptions", "interestRateFrequencyTypeOptions", "amortizationTypeOptions", "interestTypeOptions", + "interestCalculationPeriodTypeOptions", "transactionProcessingStrategyOptions", "chargeOptions", "accountingOptions", + "accountingRuleOptions", "accountingMappingOptions")); private final String resourceNameForPermissions = "LOANPRODUCT"; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/data/LoanProductData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/data/LoanProductData.java index 1f91573e9..5791981c5 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/data/LoanProductData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/data/LoanProductData.java @@ -60,6 +60,9 @@ public class LoanProductData { private final BigDecimal inArrearsTolerance; private final Long transactionProcessingStrategyId; private final String transactionProcessingStrategyName; + private final Integer graceOnPrincipalPayment; + private final Integer graceOnInterestPayment; + private final Integer graceOnInterestCharged; // charges private final Collection charges; @@ -112,6 +115,9 @@ public class LoanProductData { final String fundName = null; final Long transactionProcessingStrategyId = null; final String transactionProcessingStrategyName = null; + final Integer graceOnPrincipalPayment = null; + final Integer graceOnInterestPayment = null; + final Integer graceOnInterestCharged = null; final Collection charges = null; final EnumOptionData accountingType = null; @@ -119,7 +125,8 @@ public class LoanProductData { minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, minInterestRatePerPeriod, maxInterestRatePerPeriod, annualInterestRate, repaymentFrequencyType, interestRateFrequencyType, amortizationType, interestType, interestCalculationPeriodType, fundId, fundName, transactionProcessingStrategyId, - transactionProcessingStrategyName, charges, accountingType); + transactionProcessingStrategyName, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, charges, + accountingType); } public static LoanProductData lookupWithCurrency(final Long id, final String name, final CurrencyData currency) { @@ -145,6 +152,10 @@ public class LoanProductData { final String fundName = null; final Long transactionProcessingStrategyId = null; final String transactionProcessingStrategyName = null; + final Integer graceOnPrincipalPayment = null; + final Integer graceOnInterestPayment = null; + final Integer graceOnInterestCharged = null; + final Collection charges = null; final EnumOptionData accountingType = null; @@ -152,7 +163,8 @@ public class LoanProductData { minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, minInterestRatePerPeriod, maxInterestRatePerPeriod, annualInterestRate, repaymentFrequencyType, interestRateFrequencyType, amortizationType, interestType, interestCalculationPeriodType, fundId, fundName, transactionProcessingStrategyId, - transactionProcessingStrategyName, charges, accountingType); + transactionProcessingStrategyName, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, charges, + accountingType); } public static LoanProductData sensibleDefaultsForNewLoanProductCreation() { @@ -167,6 +179,7 @@ public class LoanProductData { final Integer numberOfRepayments = null; final Integer minNumberOfRepayments = null; final Integer maxNumberOfRepayments = null; + final Integer repaymentEvery = null; final BigDecimal interestRatePerPeriod = null; final BigDecimal minInterestRatePerPeriod = null; @@ -182,6 +195,11 @@ public class LoanProductData { final String fundName = null; final Long transactionProcessingStrategyId = null; final String transactionProcessingStrategyName = null; + + final Integer graceOnPrincipalPayment = null; + final Integer graceOnInterestPayment = null; + final Integer graceOnInterestCharged = null; + final Collection charges = null; final EnumOptionData accountingType = AccountingEnumerations.accountingRuleType(AccountingRuleType.NONE); @@ -189,7 +207,8 @@ public class LoanProductData { minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, minInterestRatePerPeriod, maxInterestRatePerPeriod, annualInterestRate, repaymentFrequencyType, interestRateFrequencyType, amortizationType, interestType, interestCalculationPeriodType, fundId, fundName, transactionProcessingStrategyId, - transactionProcessingStrategyName, charges, accountingType); + transactionProcessingStrategyName, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, charges, + accountingType); } public static LoanProductData withAccountingDetails(final LoanProductData productData, final Map accountingMappings, @@ -210,6 +229,7 @@ public class LoanProductData { final EnumOptionData interestRateFrequencyType, final EnumOptionData amortizationType, final EnumOptionData interestType, final EnumOptionData interestCalculationPeriodType, final Long fundId, final String fundName, final Long transactionProcessingStrategyId, final String transactionProcessingStrategyName, + final Integer graceOnPrincipalPayment, final Integer graceOnInterestPayment, final Integer graceOnInterestCharged, final Collection charges, final EnumOptionData accountingType) { this.id = id; this.name = name; @@ -222,6 +242,9 @@ public class LoanProductData { this.numberOfRepayments = numberOfRepayments; this.minNumberOfRepayments = minNumberOfRepayments; this.maxNumberOfRepayments = maxNumberOfRepayments; + this.graceOnPrincipalPayment = graceOnPrincipalPayment; + this.graceOnInterestPayment = graceOnInterestPayment; + this.graceOnInterestCharged = graceOnInterestCharged; this.repaymentEvery = repaymentEvery; this.interestRatePerPeriod = interestRatePerPeriod; this.minInterestRatePerPeriod = minInterestRatePerPeriod; @@ -308,6 +331,10 @@ public class LoanProductData { this.transactionProcessingStrategyName = productData.transactionProcessingStrategyName; } + this.graceOnPrincipalPayment = productData.graceOnPrincipalPayment; + this.graceOnInterestPayment = productData.graceOnInterestPayment; + this.graceOnInterestCharged = productData.graceOnInterestCharged; + this.amortizationTypeOptions = amortizationTypeOptions; this.interestTypeOptions = interestTypeOptions; this.interestCalculationPeriodTypeOptions = interestCalculationPeriodTypeOptions; @@ -418,6 +445,18 @@ public class LoanProductData { return repaymentFrequencyType; } + public Integer getGraceOnPrincipalPayment() { + return this.graceOnPrincipalPayment; + } + + public Integer getGraceOnInterestPayment() { + return this.graceOnInterestPayment; + } + + public Integer getGraceOnInterestCharged() { + return this.graceOnInterestCharged; + } + public EnumOptionData getInterestRateFrequencyType() { return interestRateFrequencyType; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProduct.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProduct.java index 1779a2f47..d9d894631 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProduct.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProduct.java @@ -104,12 +104,19 @@ public class LoanProduct extends AbstractPersistable { final Integer minNumberOfRepayments = command.integerValueOfParameterNamed("minNumberOfRepayments"); final Integer maxNumberOfRepayments = command.integerValueOfParameterNamed("maxNumberOfRepayments"); final BigDecimal inArrearsTolerance = command.bigDecimalValueOfParameterNamed("inArrearsTolerance"); + + // grace details + final Integer graceOnPrincipalPayment = command.integerValueOfParameterNamed("graceOnPrincipalPayment"); + final Integer graceOnInterestPayment = command.integerValueOfParameterNamed("graceOnInterestPayment"); + final Integer graceOnInterestCharged = command.integerValueOfParameterNamed("graceOnInterestCharged"); + final AccountingRuleType accountingRuleType = AccountingRuleType.fromInt(command.integerValueOfParameterNamed("accountingRule")); return new LoanProduct(fund, loanTransactionProcessingStrategy, name, description, currency, principal, minPrincipal, maxPrincipal, interestRatePerPeriod, minInterestRatePerPeriod, maxInterestRatePerPeriod, interestFrequencyType, annualInterestRate, interestMethod, interestCalculationPeriodMethod, repaymentEvery, repaymentFrequencyType, numberOfRepayments, - minNumberOfRepayments, maxNumberOfRepayments, amortizationMethod, inArrearsTolerance, productCharges, accountingRuleType); + minNumberOfRepayments, maxNumberOfRepayments, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, + amortizationMethod, inArrearsTolerance, productCharges, accountingRuleType); } protected LoanProduct() { @@ -126,6 +133,7 @@ public class LoanProduct extends AbstractPersistable { final InterestCalculationPeriodMethod interestCalculationPeriodMethod, final Integer repayEvery, final PeriodFrequencyType repaymentFrequencyType, final Integer defaultNumberOfInstallments, final Integer defaultMinNumberOfInstallments, final Integer defaultMaxNumberOfInstallments, + final Integer graceOnPrincipalPayment, final Integer graceOnInterestPayment, final Integer graceOnInterestCharged, final AmortizationMethod amortizationMethod, final BigDecimal inArrearsTolerance, final List charges, final AccountingRuleType accountingRuleType) { this.fund = fund; @@ -143,7 +151,10 @@ public class LoanProduct extends AbstractPersistable { this.loanProductRelatedDetail = new LoanProductRelatedDetail(currency, defaultPrincipal, defaultNominalInterestRatePerPeriod, interestPeriodFrequencyType, defaultAnnualNominalInterestRate, interestMethod, interestCalculationPeriodMethod, repayEvery, - repaymentFrequencyType, defaultNumberOfInstallments, amortizationMethod, inArrearsTolerance); + repaymentFrequencyType, defaultNumberOfInstallments, graceOnPrincipalPayment, graceOnInterestPayment, + graceOnInterestCharged, amortizationMethod, inArrearsTolerance); + + this.loanProductRelatedDetail.validateRepaymentPeriodWithGraceSettings(); this.loanProductMinMaxConstraints = new LoanProductMinMaxConstraints(defaultMinPrincipal, defaultMaxPrincipal, defaultMinNominalInterestRatePerPeriod, defaultMaxNominalInterestRatePerPeriod, defaultMinNumberOfInstallments, @@ -274,6 +285,10 @@ public class LoanProduct extends AbstractPersistable { return this.loanProductRelatedDetail.getNominalInterestRatePerPeriod(); } + public PeriodFrequencyType getInterestPeriodFrequencyType() { + return this.loanProductRelatedDetail.getInterestPeriodFrequencyType(); + } + public BigDecimal getMinNominalInterestRatePerPeriod() { return this.loanProductMinMaxConstraints().getMinNominalInterestRatePerPeriod(); } @@ -283,7 +298,7 @@ public class LoanProduct extends AbstractPersistable { } public Integer getNumberOfRepayments() { - return this.loanProductRelatedDetail().getNumberOfRepayments(); + return this.loanProductRelatedDetail.getNumberOfRepayments(); } public Integer getMinNumberOfRepayments() { @@ -294,10 +309,6 @@ public class LoanProduct extends AbstractPersistable { return this.loanProductMinMaxConstraints().getMaxNumberOfRepayments(); } - public LoanProductRelatedDetail loanProductRelatedDetail() { - return this.loanProductRelatedDetail; - } - public LoanProductMinMaxConstraints loanProductMinMaxConstraints() { // If all min and max fields are null then loanProductMinMaxConstraints // initialising to null diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProductRelatedDetail.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProductRelatedDetail.java index 94d4a237e..f114bf3fb 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProductRelatedDetail.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProductRelatedDetail.java @@ -6,7 +6,9 @@ package org.mifosplatform.portfolio.loanproduct.domain; import java.math.BigDecimal; +import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import javax.persistence.Column; @@ -16,6 +18,9 @@ import javax.persistence.EnumType; import javax.persistence.Enumerated; import org.mifosplatform.infrastructure.core.api.JsonCommand; +import org.mifosplatform.infrastructure.core.data.ApiParameterError; +import org.mifosplatform.infrastructure.core.data.DataValidatorBuilder; +import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; import org.mifosplatform.organisation.monetary.domain.Money; import org.mifosplatform.portfolio.loanaccount.domain.Loan; @@ -33,7 +38,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche @Column(name = "principal_amount", scale = 6, precision = 19, nullable = false) private BigDecimal principal; - + @Column(name = "nominal_interest_rate_per_period", scale = 6, precision = 19, nullable = false) private BigDecimal nominalInterestRatePerPeriod; @@ -69,7 +74,16 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche @Column(name = "number_of_repayments", nullable = false) private Integer numberOfRepayments; - + + @Column(name = "grace_on_principal_periods", nullable = true) + private Integer graceOnPrincipalPayment; + + @Column(name = "grace_on_interest_periods", nullable = true) + private Integer graceOnInterestPayment; + + @Column(name = "grace_interest_free_periods", nullable = true) + private Integer graceOnInterestCharged; + // FIXME - move away form JPA ordinal use for enums using just integer - // requires sql patch for existing users of software. @Enumerated(EnumType.ORDINAL) @@ -84,11 +98,13 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche final BigDecimal nominalAnnualInterestRate, final InterestMethod interestMethod, final InterestCalculationPeriodMethod interestCalculationPeriodMethod, final Integer repaymentEvery, final PeriodFrequencyType repaymentPeriodFrequencyType, final Integer numberOfRepayments, + final Integer graceOnPrincipalPayment, final Integer graceOnInterestPayment, final Integer graceOnInterestCharged, final AmortizationMethod amortizationMethod, final BigDecimal inArrearsTolerance) { return new LoanProductRelatedDetail(currency, principal, nominalInterestRatePerPeriod, interestRatePeriodFrequencyType, nominalAnnualInterestRate, interestMethod, interestCalculationPeriodMethod, repaymentEvery, repaymentPeriodFrequencyType, - numberOfRepayments, amortizationMethod, inArrearsTolerance); + numberOfRepayments, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, amortizationMethod, + inArrearsTolerance); } protected LoanProductRelatedDetail() { @@ -96,10 +112,11 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche } public LoanProductRelatedDetail(final MonetaryCurrency currency, final BigDecimal defaultPrincipal, - final BigDecimal defaultNominalInterestRatePerPeriod,final PeriodFrequencyType interestPeriodFrequencyType, + final BigDecimal defaultNominalInterestRatePerPeriod, final PeriodFrequencyType interestPeriodFrequencyType, final BigDecimal defaultAnnualNominalInterestRate, final InterestMethod interestMethod, final InterestCalculationPeriodMethod interestCalculationPeriodMethod, final Integer repayEvery, final PeriodFrequencyType repaymentFrequencyType, final Integer defaultNumberOfRepayments, + final Integer graceOnPrincipalPayment, final Integer graceOnInterestPayment, final Integer graceOnInterestCharged, final AmortizationMethod amortizationMethod, final BigDecimal inArrearsTolerance) { this.currency = currency; this.principal = defaultPrincipal; @@ -111,6 +128,9 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche this.repayEvery = repayEvery; this.repaymentPeriodFrequencyType = repaymentFrequencyType; this.numberOfRepayments = defaultNumberOfRepayments; + this.graceOnPrincipalPayment = defaultToNullIfZero(graceOnPrincipalPayment); + this.graceOnInterestPayment = defaultToNullIfZero(graceOnInterestPayment); + this.graceOnInterestCharged = defaultToNullIfZero(graceOnInterestCharged); this.amortizationMethod = amortizationMethod; if (inArrearsTolerance != null && BigDecimal.ZERO.compareTo(inArrearsTolerance) == 0) { this.inArrearsTolerance = null; @@ -119,6 +139,14 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche } } + private Integer defaultToNullIfZero(final Integer value) { + Integer defaultTo = value; + if (value != null && Integer.valueOf(0).equals(value)) { + defaultTo = null; + } + return defaultTo; + } + public MonetaryCurrency getCurrency() { return this.currency.copy(); } @@ -126,7 +154,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche public Money getPrincipal() { return Money.of(this.currency, this.principal); } - + public Money getInArrearsTolerance() { return Money.of(this.currency, this.inArrearsTolerance); } @@ -165,7 +193,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche public Integer getNumberOfRepayments() { return numberOfRepayments; } - + public AmortizationMethod getAmortizationMethod() { return amortizationMethod; } @@ -231,7 +259,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche actualChanges.put("locale", localeAsInput); this.repaymentPeriodFrequencyType = PeriodFrequencyType.fromInt(newValue); } - + final String numberOfRepaymentsParamName = "numberOfRepayments"; if (command.isChangeInIntegerParameterNamed(numberOfRepaymentsParamName, this.numberOfRepayments)) { final Integer newValue = command.integerValueOfParameterNamed(numberOfRepaymentsParamName); @@ -255,7 +283,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche actualChanges.put("locale", localeAsInput); this.inArrearsTolerance = newValue; } - + final String interestRatePerPeriodParamName = "interestRatePerPeriod"; if (command.isChangeInBigDecimalParameterNamed(interestRatePerPeriodParamName, this.nominalInterestRatePerPeriod)) { final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(interestRatePerPeriodParamName); @@ -291,9 +319,65 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche this.interestCalculationPeriodMethod = InterestCalculationPeriodMethod.fromInt(newValue); } + final String graceOnPrincipalPaymentParamName = "graceOnPrincipalPayment"; + if (command.isChangeInIntegerParameterNamed(graceOnPrincipalPaymentParamName, this.graceOnPrincipalPayment)) { + final Integer newValue = command.integerValueOfParameterNamed(graceOnPrincipalPaymentParamName); + actualChanges.put(graceOnPrincipalPaymentParamName, newValue); + actualChanges.put("locale", localeAsInput); + this.graceOnPrincipalPayment = newValue; + } + + final String graceOnInterestPaymentParamName = "graceOnInterestPayment"; + if (command.isChangeInIntegerParameterNamed(graceOnInterestPaymentParamName, this.graceOnInterestPayment)) { + final Integer newValue = command.integerValueOfParameterNamed(graceOnInterestPaymentParamName); + actualChanges.put(graceOnInterestPaymentParamName, newValue); + actualChanges.put("locale", localeAsInput); + this.graceOnInterestPayment = newValue; + } + + final String graceOnInterestChargedParamName = "graceOnInterestCharged"; + if (command.isChangeInIntegerParameterNamed(graceOnInterestChargedParamName, this.graceOnInterestCharged)) { + final Integer newValue = command.integerValueOfParameterNamed(graceOnInterestChargedParamName); + actualChanges.put(graceOnInterestChargedParamName, newValue); + actualChanges.put("locale", localeAsInput); + this.graceOnInterestCharged = newValue; + } + + validateRepaymentPeriodWithGraceSettings(); + return actualChanges; } + public void validateRepaymentPeriodWithGraceSettings() { + final List dataValidationErrors = new ArrayList(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loanproduct"); + + if (this.numberOfRepayments <= defaultToZeroIfNull(this.graceOnPrincipalPayment)) { + baseDataValidator.reset().parameter("graceOnPrincipalPayment").value(graceOnPrincipalPayment) + .failWithCode(".mustBeLessThan.numberOfRepayments"); + } + + if (this.numberOfRepayments <= defaultToZeroIfNull(this.graceOnInterestPayment)) { + baseDataValidator.reset().parameter("graceOnInterestPayment").value(graceOnInterestPayment) + .failWithCode(".mustBeLessThan.numberOfRepayments"); + } + + if (this.numberOfRepayments < defaultToZeroIfNull(this.graceOnInterestCharged)) { + baseDataValidator.reset().parameter("graceOnInterestCharged").value(graceOnInterestCharged) + .failWithCode(".mustBeLessThan.numberOfRepayments"); + } + + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } + } + + private Integer defaultToZeroIfNull(final Integer value) { + Integer result = value; + if (value == null) { + result = Integer.valueOf(0); + } + return result; + } + private void updateInterestRateDerivedFields(final AprCalculator aprCalculator) { this.annualNominalInterestRate = aprCalculator.calculateFrom(this.interestPeriodFrequencyType, this.nominalInterestRatePerPeriod); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/serialization/LoanProductCommandFromApiJsonDeserializer.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/serialization/LoanProductDataValidator.java similarity index 95% rename from mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/serialization/LoanProductCommandFromApiJsonDeserializer.java rename to mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/serialization/LoanProductDataValidator.java index 9cbd5520b..f0a77c00d 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/serialization/LoanProductCommandFromApiJsonDeserializer.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/serialization/LoanProductDataValidator.java @@ -16,8 +16,8 @@ import java.util.Map; import java.util.Set; import org.apache.commons.lang.StringUtils; -import org.mifosplatform.accounting.common.AccountingRuleType; import org.mifosplatform.accounting.common.AccountingConstants.LOAN_PRODUCT_ACCOUNTING_PARAMS; +import org.mifosplatform.accounting.common.AccountingRuleType; import org.mifosplatform.infrastructure.core.data.ApiParameterError; import org.mifosplatform.infrastructure.core.data.DataValidatorBuilder; import org.mifosplatform.infrastructure.core.exception.InvalidJsonException; @@ -33,7 +33,7 @@ import com.google.gson.JsonObject; import com.google.gson.reflect.TypeToken; @Component -public final class LoanProductCommandFromApiJsonDeserializer { +public final class LoanProductDataValidator { /** * The parameters supported for this command. @@ -42,7 +42,8 @@ public final class LoanProductCommandFromApiJsonDeserializer { "currencyCode", "digitsAfterDecimal", "principal", "minPrincipal", "maxPrincipal", "repaymentEvery", "numberOfRepayments", "minNumberOfRepayments", "maxNumberOfRepayments", "repaymentFrequencyType", "interestRatePerPeriod", "minInterestRatePerPeriod", "maxInterestRatePerPeriod", "interestRateFrequencyType", "amortizationType", "interestType", - "interestCalculationPeriodType", "inArrearsTolerance", "transactionProcessingStrategyId", "charges", "accountingRule", + "interestCalculationPeriodType", "inArrearsTolerance", "transactionProcessingStrategyId", "graceOnPrincipalPayment", + "graceOnInterestPayment", "graceOnInterestCharged", "charges", "accountingRule", LOAN_PRODUCT_ACCOUNTING_PARAMS.FEES_RECEIVABLE.getValue(), LOAN_PRODUCT_ACCOUNTING_PARAMS.FUND_SOURCE.getValue(), LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_FROM_FEES.getValue(), LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_FROM_PENALTIES.getValue(), LOAN_PRODUCT_ACCOUNTING_PARAMS.INTEREST_ON_LOANS.getValue(), LOAN_PRODUCT_ACCOUNTING_PARAMS.INTEREST_RECEIVABLE.getValue(), @@ -53,7 +54,7 @@ public final class LoanProductCommandFromApiJsonDeserializer { private final FromJsonHelper fromApiJsonHelper; @Autowired - public LoanProductCommandFromApiJsonDeserializer(final FromJsonHelper fromApiJsonHelper) { + public LoanProductDataValidator(final FromJsonHelper fromApiJsonHelper) { this.fromApiJsonHelper = fromApiJsonHelper; } @@ -218,6 +219,16 @@ public final class LoanProductCommandFromApiJsonDeserializer { baseDataValidator.reset().parameter("transactionProcessingStrategyId").value(transactionProcessingStrategyId).notNull() .integerGreaterThanZero(); + // grace validation + final Integer graceOnPrincipalPayment = fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnPrincipalPayment", element); + baseDataValidator.reset().parameter("graceOnPrincipalPayment").value(graceOnPrincipalPayment).zeroOrPositiveAmount(); + + final Integer graceOnInterestPayment = fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnInterestPayment", element); + baseDataValidator.reset().parameter("graceOnInterestPayment").value(graceOnInterestPayment).zeroOrPositiveAmount(); + + final Integer graceOnInterestCharged = fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnInterestCharged", element); + baseDataValidator.reset().parameter("graceOnInterestCharged").value(graceOnInterestCharged).zeroOrPositiveAmount(); + // accounting related data validation final Integer accountingRuleType = fromApiJsonHelper.extractIntegerNamed("accountingRule", element, Locale.getDefault()); baseDataValidator.reset().parameter("accountingRule").value(accountingRuleType).notNull().inMinMaxRange(1, 3); @@ -396,6 +407,23 @@ public final class LoanProductCommandFromApiJsonDeserializer { .integerGreaterThanZero(); } + // grace validation + if (fromApiJsonHelper.parameterExists("graceOnPrincipalPayment", element)) { + final Integer graceOnPrincipalPayment = fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnPrincipalPayment", element); + baseDataValidator.reset().parameter("graceOnPrincipalPayment").value(graceOnPrincipalPayment).zeroOrPositiveAmount(); + } + + if (fromApiJsonHelper.parameterExists("graceOnInterestPayment", element)) { + final Integer graceOnInterestPayment = fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnInterestPayment", element); + baseDataValidator.reset().parameter("graceOnInterestPayment").value(graceOnInterestPayment).zeroOrPositiveAmount(); + } + + if (fromApiJsonHelper.parameterExists("graceOnInterestCharged", element)) { + final Integer graceOnInterestCharged = fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnInterestCharged", element); + baseDataValidator.reset().parameter("graceOnInterestCharged").value(graceOnInterestCharged).zeroOrPositiveAmount(); + } + + // if (fromApiJsonHelper.parameterExists("interestRateFrequencyType", element)) { final Integer interestRateFrequencyType = fromApiJsonHelper.extractIntegerNamed("interestRateFrequencyType", element, Locale.getDefault()); @@ -473,11 +501,8 @@ public final class LoanProductCommandFromApiJsonDeserializer { throwExceptionIfValidationWarningsExist(dataValidationErrors); } - /** + /* * Validation for advanced accounting options - * - * @param baseDataValidator - * @param element */ private void validatePaymentChannelFundSourceMappings(final DataValidatorBuilder baseDataValidator, final JsonElement element) { if (fromApiJsonHelper.parameterExists(LOAN_PRODUCT_ACCOUNTING_PARAMS.PAYMENT_CHANNEL_FUND_SOURCE_MAPPING.getValue(), element)) { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java index 71dad3c11..f702558f8 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java @@ -101,6 +101,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo + "lp.nominal_interest_rate_per_period as interestRatePerPeriod, lp.min_nominal_interest_rate_per_period as minInterestRatePerPeriod, lp.max_nominal_interest_rate_per_period as maxInterestRatePerPeriod, lp.interest_period_frequency_enum as interestRatePerPeriodFreq, " + "lp.annual_nominal_interest_rate as annualInterestRate, lp.interest_method_enum as interestMethod, lp.interest_calculated_in_period_enum as interestCalculationInPeriodMethod," + "lp.repay_every as repaidEvery, lp.repayment_period_frequency_enum as repaymentPeriodFrequency, lp.number_of_repayments as numberOfRepayments, lp.min_number_of_repayments as minNumberOfRepayments, lp.max_number_of_repayments as maxNumberOfRepayments, " + + "lp.grace_on_principal_periods as graceOnPrincipalPayment, lp.grace_on_interest_periods as graceOnInterestPayment, lp.grace_interest_free_periods as graceOnInterestCharged," + "lp.amortization_method_enum as amortizationMethod, lp.arrearstolerance_amount as tolerance, " + "lp.accounting_type as accountingType, " + "curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, curr.display_symbol as currencyDisplaySymbol " @@ -140,6 +141,10 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo final Integer maxNumberOfRepayments = JdbcSupport.getInteger(rs, "maxNumberOfRepayments"); final Integer repaymentEvery = JdbcSupport.getInteger(rs, "repaidEvery"); + final Integer graceOnPrincipalPayment = JdbcSupport.getIntegerDefaultToNullIfZero(rs, "graceOnPrincipalPayment"); + final Integer graceOnInterestPayment = JdbcSupport.getIntegerDefaultToNullIfZero(rs, "graceOnInterestPayment"); + final Integer graceOnInterestCharged = JdbcSupport.getIntegerDefaultToNullIfZero(rs, "graceOnInterestCharged"); + final Integer accountingRuleId = JdbcSupport.getInteger(rs, "accountingType"); final EnumOptionData accountingRuleType = AccountingEnumerations.accountingRuleType(accountingRuleId); @@ -168,7 +173,8 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, minInterestRatePerPeriod, maxInterestRatePerPeriod, annualInterestRate, repaymentFrequencyType, interestRateFrequencyType, amortizationType, interestType, interestCalculationPeriodType, fundId, fundName, - transactionStrategyId, transactionStrategyName, this.charges, accountingRuleType); + transactionStrategyId, transactionStrategyName, graceOnPrincipalPayment, graceOnInterestPayment, + graceOnInterestCharged, this.charges, accountingRuleType); } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java index 379df3185..75de974fe 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java @@ -28,7 +28,7 @@ import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRepository; import org.mifosplatform.portfolio.loanproduct.domain.LoanTransactionProcessingStrategy; import org.mifosplatform.portfolio.loanproduct.exception.InvalidCurrencyException; import org.mifosplatform.portfolio.loanproduct.exception.LoanProductNotFoundException; -import org.mifosplatform.portfolio.loanproduct.serialization.LoanProductCommandFromApiJsonDeserializer; +import org.mifosplatform.portfolio.loanproduct.serialization.LoanProductDataValidator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -44,7 +44,7 @@ public class LoanProductWritePlatformServiceJpaRepositoryImpl implements LoanPro private final static Logger logger = LoggerFactory.getLogger(LoanProductWritePlatformServiceJpaRepositoryImpl.class); private final PlatformSecurityContext context; - private final LoanProductCommandFromApiJsonDeserializer fromApiJsonDeserializer; + private final LoanProductDataValidator fromApiJsonDeserializer; private final LoanProductRepository loanProductRepository; private final AprCalculator aprCalculator; private final FundRepository fundRepository; @@ -54,7 +54,7 @@ public class LoanProductWritePlatformServiceJpaRepositoryImpl implements LoanPro @Autowired public LoanProductWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context, - final LoanProductCommandFromApiJsonDeserializer fromApiJsonDeserializer, final LoanProductRepository loanProductRepository, + final LoanProductDataValidator fromApiJsonDeserializer, final LoanProductRepository loanProductRepository, final AprCalculator aprCalculator, final FundRepository fundRepository, final LoanTransactionProcessingStrategyRepository loanTransactionProcessingStrategyRepository, final ChargeRepositoryWrapper chargeRepository, diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/InterestCompoundingPeriodSummary.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/InterestCompoundingPeriodSummary.java index eff523c7a..a21253e49 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/InterestCompoundingPeriodSummary.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/InterestCompoundingPeriodSummary.java @@ -9,6 +9,7 @@ import java.math.BigDecimal; import java.math.MathContext; import org.joda.time.LocalDate; +import org.mifosplatform.infrastructure.core.domain.LocalDateInterval; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; import org.mifosplatform.organisation.monetary.domain.Money; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccount.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccount.java index cb0af6e76..f1a3ec3fb 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccount.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccount.java @@ -46,6 +46,7 @@ import org.joda.time.MonthDay; import org.joda.time.format.DateTimeFormatter; import org.mifosplatform.infrastructure.core.api.JsonCommand; import org.mifosplatform.infrastructure.core.data.ApiParameterError; +import org.mifosplatform.infrastructure.core.domain.LocalDateInterval; import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException; import org.mifosplatform.infrastructure.core.service.DateUtils; import org.mifosplatform.infrastructure.security.service.RandomPasswordGenerator; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountDailyBalance.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountDailyBalance.java index de2241e57..96a6b13ee 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountDailyBalance.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountDailyBalance.java @@ -9,6 +9,7 @@ import java.math.BigDecimal; import java.math.MathContext; import java.math.RoundingMode; +import org.mifosplatform.infrastructure.core.domain.LocalDateInterval; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; import org.mifosplatform.organisation.monetary.domain.Money; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountTransaction.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountTransaction.java index d4eff548a..4851d5c8a 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountTransaction.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountTransaction.java @@ -19,6 +19,7 @@ import javax.persistence.Temporal; import javax.persistence.TemporalType; import org.joda.time.LocalDate; +import org.mifosplatform.infrastructure.core.domain.LocalDateInterval; import org.mifosplatform.organisation.monetary.data.CurrencyData; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; import org.mifosplatform.organisation.monetary.domain.Money; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/exception/InvalidSavingsAccountStateTransitionException.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/exception/InvalidSavingsAccountStateTransitionException.java index e6071f8f2..0d10e696e 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/exception/InvalidSavingsAccountStateTransitionException.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/exception/InvalidSavingsAccountStateTransitionException.java @@ -1,3 +1,8 @@ +/** + * 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.savings.exception; import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRuleException; diff --git a/mifosng-provider/src/main/resources/META-INF/spring/scheduler.xml b/mifosng-provider/src/main/resources/META-INF/spring/scheduler.xml index 5afe7d5ff..03411a951 100644 --- a/mifosng-provider/src/main/resources/META-INF/spring/scheduler.xml +++ b/mifosng-provider/src/main/resources/META-INF/spring/scheduler.xml @@ -1,4 +1,11 @@ +