From b48bc6269178fe8936808c19fc92fe7613edf550 Mon Sep 17 00:00:00 2001 From: Keith Woodlock Date: Thu, 25 Jul 2013 00:57:25 +0100 Subject: [PATCH] MIFOSX-397: support apply of annual fees on savings account through manual and scheduled job approach. --- .../V91__apply_annual_fees_permission.sql | 5 + .../commands/domain/CommandWrapper.java | 4 + .../service/CommandWrapperBuilder.java | 15 +- .../SynchronousCommandProcessingService.java | 2 + .../core/serialization/FromJsonHelper.java | 8 +- .../savings/SavingsApiConstants.java | 1 + ...SavingsAccountTransactionsApiResource.java | 2 +- .../api/SavingsAccountsApiResource.java | 6 +- .../data/SavingsAccountAnnualFeeData.java | 37 ++++ .../savings/data/SavingsAccountData.java | 4 + .../data/SavingsAccountDataValidator.java | 4 +- .../savings/data/TransactionDateData.java | 21 ++ .../savings/domain/SavingsAccount.java | 206 ++++++++++++------ .../domain/SavingsAccountTransaction.java | 24 +- .../SavingsAccountTransactionComparator.java | 4 +- ...vingsAccountTransactionSummaryWrapper.java | 6 +- ...AnnualFeeSavingsAccountCommandHandler.java | 35 +++ .../SavingsAccountReadPlatformService.java | 3 + ...SavingsAccountReadPlatformServiceImpl.java | 48 +++- .../SavingsAccountWritePlatformService.java | 3 +- ...WritePlatformServiceJpaRepositoryImpl.java | 12 +- ...WritePlatformServiceJpaRepositoryImpl.java | 8 +- .../ScheduledJobRunnerServiceImpl.java | 25 ++- 23 files changed, 372 insertions(+), 111 deletions(-) create mode 100644 mifosng-db/migrations/core_db/V91__apply_annual_fees_permission.sql create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/SavingsAccountAnnualFeeData.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/TransactionDateData.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/handler/ApplyAnnualFeeSavingsAccountCommandHandler.java diff --git a/mifosng-db/migrations/core_db/V91__apply_annual_fees_permission.sql b/mifosng-db/migrations/core_db/V91__apply_annual_fees_permission.sql new file mode 100644 index 000000000..c82bc74c7 --- /dev/null +++ b/mifosng-db/migrations/core_db/V91__apply_annual_fees_permission.sql @@ -0,0 +1,5 @@ +INSERT INTO `m_permission` +(`grouping`,`code`,`entity_name`,`action_name`,`can_maker_checker`) +VALUES +('transaction_savings', 'APPLYANNUALFEE_SAVINGSACCOUNT', 'SAVINGSACCOUNT', 'APPLYANNUALFEE', 1), +('transaction_savings', 'APPLYANNUALFEE_SAVINGSACCOUNT_CHECKER', 'SAVINGSACCOUNT', 'APPLYANNUALFEE', 0); \ No newline at end of file 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 efb0c3e93..483bc88ff 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 @@ -466,6 +466,10 @@ public class CommandWrapper { return this.actionName.equalsIgnoreCase("POSTINTEREST") && this.entityName.equalsIgnoreCase("SAVINGSACCOUNT"); } + public boolean isSavingsAccountApplyAnnualFee() { + return this.actionName.equalsIgnoreCase("APPLYANNUALFEE") && this.entityName.equalsIgnoreCase("SAVINGSACCOUNT"); + } + public boolean isSavingsAccountUndoTransaction() { return this.actionName.equalsIgnoreCase("UNDOTRANSACTION") && this.entityName.equalsIgnoreCase("SAVINGSACCOUNT"); } 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 cf95a1c4e..282259cd5 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 @@ -47,6 +47,11 @@ public class CommandWrapperBuilder { return this; } + public CommandWrapperBuilder withNoJsonBody() { + this.json = null; + return this; + } + public CommandWrapperBuilder updateGlobalConfiguration() { this.actionName = "UPDATE"; this.entityName = "CONFIGURATION"; @@ -850,6 +855,15 @@ public class CommandWrapperBuilder { return this; } + public CommandWrapperBuilder savingsAccountApplyAnnualFees(final Long accountId) { + this.actionName = "APPLYANNUALFEE"; + this.entityName = "SAVINGSACCOUNT"; + this.savingsId = accountId; + this.entityId = accountId; + this.href = "/savingsaccounts/" + accountId + "?command=applyAnnualFees"; + return this; + } + public CommandWrapperBuilder createCalendar(final String supportedEntityType, final Long supportedEntityId) { this.actionName = "CREATE"; this.entityName = "CALENDAR"; @@ -1168,5 +1182,4 @@ public class CommandWrapperBuilder { this.href = "/updateJobDetail/" + jobId+"/updateJobDetail"; return this; } - } \ No newline at end of file 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 0542e942b..80df9e704 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 @@ -401,6 +401,8 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer handler = this.applicationContext.getBean("calculateInterestSavingsAccountCommandHandler", NewCommandSourceHandler.class); } else if (wrapper.isSavingsAccountInterestPosting()) { handler = this.applicationContext.getBean("postInterestSavingsAccountCommandHandler", NewCommandSourceHandler.class); + } else if (wrapper.isSavingsAccountApplyAnnualFee()) { + handler = this.applicationContext.getBean("applyAnnualFeeSavingsAccountCommandHandler", NewCommandSourceHandler.class); } else if (wrapper.isSavingsAccountUndoTransaction()) { handler = this.applicationContext.getBean("undoTransactionSavingsAccountCommandHandler", NewCommandSourceHandler.class); } else { 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 23d2f2eba..b88500db2 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 @@ -97,7 +97,13 @@ public class FromJsonHelper { } public JsonElement parse(final String json) { - return parser.parse(json); + + JsonElement parsedElement = null; + if (StringUtils.isNotBlank(json)) { + parsedElement = parser.parse(json); + } + + return parsedElement; } public boolean parameterExists(final String parameterName, final JsonElement element) { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/SavingsApiConstants.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/SavingsApiConstants.java index d8f136f29..b2ba454b1 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/SavingsApiConstants.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/SavingsApiConstants.java @@ -29,6 +29,7 @@ public class SavingsApiConstants { public static String modifyApplicationAction = ".modify"; public static String deleteApplicationAction = ".delete"; public static String undoTransactionAction = ".undotransaction"; + public static String applyAnnualFeeTransactionAction = ".applyannualfee"; // general public static final String localeParamName = "locale"; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/api/SavingsAccountTransactionsApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/api/SavingsAccountTransactionsApiResource.java index f7a6820f6..d02f65cad 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/api/SavingsAccountTransactionsApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/api/SavingsAccountTransactionsApiResource.java @@ -120,7 +120,7 @@ public class SavingsAccountTransactionsApiResource { @Path("{transactionId}") @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) - public String transaction(@PathParam("savingsId") final Long savingsId, @PathParam("transactionId") final Long transactionId, + public String undoTransaction(@PathParam("savingsId") final Long savingsId, @PathParam("transactionId") final Long transactionId, @QueryParam("command") final String commandParam, final String apiRequestBodyAsJson) { String jsonApiRequest = apiRequestBodyAsJson; 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 a890c7879..6a5239575 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 @@ -164,7 +164,6 @@ public class SavingsAccountsApiResource { SavingsAccountData templateData = null; final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters()); if (settings.isTemplate()) { - templateData = this.savingsAccountReadPlatformService.retrieveTemplate(savingsAccount.clientId(), savingsAccount.groupId(), savingsAccount.productId(), staffInSelectedOfficeOnly); } @@ -217,11 +216,14 @@ public class SavingsAccountsApiResource { final CommandWrapper commandRequest = builder.savingsAccountActivation(accountId).build(); result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); } else if (is(commandParam, "calculateInterest")) { - final CommandWrapper commandRequest = builder.withJson(null).savingsAccountInterestCalculation(accountId).build(); + final CommandWrapper commandRequest = builder.withNoJsonBody().savingsAccountInterestCalculation(accountId).build(); result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); } else if (is(commandParam, "postInterest")) { final CommandWrapper commandRequest = builder.savingsAccountInterestPosting(accountId).build(); result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "applyAnnualFees")) { + final CommandWrapper commandRequest = builder.savingsAccountApplyAnnualFees(accountId).build(); + result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); } if (result == null) { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/SavingsAccountAnnualFeeData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/SavingsAccountAnnualFeeData.java new file mode 100644 index 000000000..6aed6e0e9 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/SavingsAccountAnnualFeeData.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.portfolio.savings.data; + +import org.joda.time.LocalDate; + +public class SavingsAccountAnnualFeeData { + + private final Long id; + private final String accountNo; + private final LocalDate nextAnnualFeeDueDate; + + public static SavingsAccountAnnualFeeData instance(final Long id, final String accountNo, final LocalDate nextAnnualFeeDueDate) { + return new SavingsAccountAnnualFeeData(id, accountNo, nextAnnualFeeDueDate); + } + + private SavingsAccountAnnualFeeData(final Long id, final String accountNo, final LocalDate nextAnnualFeeDueDate) { + this.id = id; + this.accountNo = accountNo; + this.nextAnnualFeeDueDate = nextAnnualFeeDueDate; + } + + public Long getId() { + return this.id; + } + + public String getAccountNo() { + return this.accountNo; + } + + public LocalDate getNextAnnualFeeDueDate() { + return this.nextAnnualFeeDueDate; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/SavingsAccountData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/SavingsAccountData.java index 796d4c271..e9be89150 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/SavingsAccountData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/SavingsAccountData.java @@ -248,6 +248,10 @@ public class SavingsAccountData { this.withdrawalFeeTypeOptions = withdrawalFeeTypeOptions; } + public Long id() { + return this.id; + } + public Long clientId() { return this.clientId; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/SavingsAccountDataValidator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/SavingsAccountDataValidator.java index ca1ac947c..9fa0104fd 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/SavingsAccountDataValidator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/SavingsAccountDataValidator.java @@ -92,7 +92,7 @@ public class SavingsAccountDataValidator { if (fromApiJsonHelper.parameterExists(fieldOfficerIdParamName, element)) { final Long fieldOfficerId = fromApiJsonHelper.extractLongNamed(fieldOfficerIdParamName, element); - baseDataValidator.reset().parameter(fieldOfficerIdParamName).value(fieldOfficerId).notNull().integerGreaterThanZero(); + baseDataValidator.reset().parameter(fieldOfficerIdParamName).value(fieldOfficerId).ignoreIfNull().integerGreaterThanZero(); } final LocalDate submittedOnDate = fromApiJsonHelper.extractLocalDateNamed(submittedOnDateParamName, element); @@ -276,7 +276,7 @@ public class SavingsAccountDataValidator { if (fromApiJsonHelper.parameterExists(fieldOfficerIdParamName, element)) { final Long fieldOfficerId = fromApiJsonHelper.extractLongNamed(fieldOfficerIdParamName, element); - baseDataValidator.reset().parameter(fieldOfficerIdParamName).value(fieldOfficerId).notNull().integerGreaterThanZero(); + baseDataValidator.reset().parameter(fieldOfficerIdParamName).value(fieldOfficerId).ignoreIfNull().integerGreaterThanZero(); } if (fromApiJsonHelper.parameterExists(submittedOnDateParamName, element)) { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/TransactionDateData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/TransactionDateData.java new file mode 100644 index 000000000..0ca1b21be --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/TransactionDateData.java @@ -0,0 +1,21 @@ +/** + * 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.data; + +import org.joda.time.LocalDate; + +/** + * Immutable data object representing date + */ +@SuppressWarnings("unused") +public class TransactionDateData { + + private final LocalDate date; + + public TransactionDateData(final LocalDate date) { + this.date = date; + } +} \ 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 dc08d3320..4810811b0 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 @@ -230,7 +230,6 @@ 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; @@ -311,22 +310,11 @@ public class SavingsAccount extends AbstractPersistable { if (annualFeeOnMonthDay != null) { this.annualFeeOnMonth = annualFeeOnMonthDay.getMonthOfYear(); this.annualFeeOnDay = annualFeeOnMonthDay.getDayOfMonth(); - - updateAnnualFeeNextDueDate(); } this.summary = new SavingsAccountSummary(); } - private void updateAnnualFeeNextDueDate() { - LocalDate nextDueLocalDate = new LocalDate().withMonthOfYear(this.annualFeeOnMonth).withDayOfMonth(this.annualFeeOnDay); - if (nextDueLocalDate.isBefore(DateUtils.getLocalDateOfTenant())) { - nextDueLocalDate = nextDueLocalDate.plusYears(1); - } - - this.annualFeeNextDueDate = nextDueLocalDate.toDate(); - } - /** * Used after fetching/hydrating a {@link SavingsAccount} object to inject * helper services/components used for update summary details after @@ -412,7 +400,7 @@ public class SavingsAccount extends AbstractPersistable { SavingsAccountTransaction postingTransation = null; for (SavingsAccountTransaction transaction : this.transactions) { - if (transaction.isInterestPosting() && transaction.occursOn(postingDate)) { + if (transaction.isInterestPostingAndNotReversed() && transaction.occursOn(postingDate)) { postingTransation = transaction; break; } @@ -492,7 +480,7 @@ public class SavingsAccount extends AbstractPersistable { List orderedNonInterestPostingTransactions = new ArrayList(); for (SavingsAccountTransaction transaction : listOfTransactionsSorted) { - if (!transaction.isInterestPosting() && transaction.isNotReversed()) { + if (!transaction.isInterestPostingAndNotReversed() && transaction.isNotReversed()) { orderedNonInterestPostingTransactions.add(transaction); } } @@ -523,12 +511,10 @@ public class SavingsAccount extends AbstractPersistable { transaction.zeroBalanceFields(); } else { Money transactionAmount = Money.zero(this.currency); - if (transaction.isDeposit()) { + if (transaction.isCredit()) { transactionAmount = transactionAmount.plus(transaction.getAmount(this.currency)); - } else if (transaction.isWithdrawal()) { + } else if (transaction.isDebit()) { transactionAmount = transactionAmount.minus(transaction.getAmount(this.currency)); - } else if (transaction.isInterestPosting()) { - transactionAmount = transactionAmount.plus(transaction.getAmount(this.currency)); } runningBalance = runningBalance.plus(transactionAmount); @@ -705,7 +691,7 @@ public class SavingsAccount extends AbstractPersistable { boolean transactionBeforeLastInterestPosting = false; for (SavingsAccountTransaction transaction : retreiveListOfTransactions()) { - if (transaction.isNotReversed() && transaction.isInterestPosting() && transaction.isAfter(transactionDate)) { + if (transaction.isNotReversed() && transaction.isInterestPostingAndNotReversed() && transaction.isAfter(transactionDate)) { transactionBeforeLastInterestPosting = true; break; } @@ -720,7 +706,7 @@ public class SavingsAccount extends AbstractPersistable { Money runningBalance = Money.zero(this.currency); for (SavingsAccountTransaction transaction : transactionsSortedByDate) { - if (transaction.isNotReversed() && transaction.isDeposit() || transaction.isInterestPosting()) { + if (transaction.isNotReversed() && transaction.isDeposit() || transaction.isInterestPostingAndNotReversed()) { runningBalance = runningBalance.plus(transaction.getAmount(this.currency)); } else if (transaction.isNotReversed() && transaction.isWithdrawal()) { runningBalance = runningBalance.minus(transaction.getAmount(this.currency)); @@ -740,7 +726,7 @@ public class SavingsAccount extends AbstractPersistable { Money runningBalance = Money.zero(this.currency); for (SavingsAccountTransaction transaction : transactionsSortedByDate) { - if (transaction.isNotReversed() && transaction.isDeposit() || transaction.isInterestPosting()) { + if (transaction.isNotReversed() && transaction.isDeposit() || transaction.isInterestPostingAndNotReversed()) { runningBalance = runningBalance.plus(transaction.getAmount(this.currency)); } else if (transaction.isNotReversed() && transaction.isWithdrawal()) { runningBalance = runningBalance.minus(transaction.getAmount(this.currency)); @@ -759,54 +745,51 @@ public class SavingsAccount extends AbstractPersistable { } } - public SavingsAccountTransaction addAnnualFee(final MathContext mc, final DateTimeFormatter formatter, final LocalDate transactionDate, - final List existingTransactionIds, final List existingReversedTransactionIds) { + public SavingsAccountTransaction addAnnualFee(final MathContext mc, final DateTimeFormatter formatter, + final LocalDate annualFeeTransactionDate, final LocalDate today, final List existingTransactionIds, + final List existingReversedTransactionIds) { + + final List dataValidationErrors = new ArrayList(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(SAVINGS_ACCOUNT_RESOURCE_NAME + SavingsApiConstants.applyAnnualFeeTransactionAction); if (isNotActive()) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.is.not.active"); - final String defaultUserMessage = "Transaction is not allowed. Account is not active."; - final ApiParameterError error = ApiParameterError.parameterError("error.msg.savingsaccount.transaction.account.is.not.active", - defaultUserMessage, "transactionDate", transactionDate.toString(formatter)); + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } + } - final List dataValidationErrors = new ArrayList(); - dataValidationErrors.add(error); + LocalDate nextAnnualFeeDueDate = getNextAnnualFeeDueDate(); + if (nextAnnualFeeDueDate == null || annualFeeSettingsNotSet()) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("no.annualfee.settings"); throw new PlatformApiDataValidationException(dataValidationErrors); } - if (isDateInTheFuture(transactionDate)) { - final String defaultUserMessage = "Transaction date cannot be in the future."; - final ApiParameterError error = ApiParameterError.parameterError("error.msg.savingsaccount.transaction.in.the.future", - defaultUserMessage, "transactionDate", transactionDate.toString(formatter)); + if (nextAnnualFeeDueDate.isBefore(getActivationLocalDate())) { - final List dataValidationErrors = new ArrayList(); - dataValidationErrors.add(error); + baseDataValidator.reset().parameter("annualFeeTransactionDate").value(getActivationLocalDate().toString(formatter)) + .failWithCodeNoParameterAddedToErrorCode("before.activationDate"); throw new PlatformApiDataValidationException(dataValidationErrors); } - if (transactionDate.isBefore(getActivationLocalDate())) { - final Object[] defaultUserArgs = Arrays.asList(transactionDate.toString(formatter), - getActivationLocalDate().toString(formatter)).toArray(); - final String defaultUserMessage = "Transaction date cannot be before accounts activation date."; - final ApiParameterError error = ApiParameterError.parameterError("error.msg.savingsaccount.transaction.before.activation.date", - defaultUserMessage, "transactionDate", defaultUserArgs); - - final List dataValidationErrors = new ArrayList(); - dataValidationErrors.add(error); + if (isDateInTheFuture(annualFeeTransactionDate)) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("transaction.in.the.future"); throw new PlatformApiDataValidationException(dataValidationErrors); } - if (isAccountLocked(transactionDate)) { - final String defaultUserMessage = "Withdrawal is not allowed. No withdrawals are allowed until after " - + getLockedInUntilLocalDate().toString(formatter); - final ApiParameterError error = ApiParameterError.parameterError( - "error.msg.savingsaccount.transaction.withdrawals.blocked.during.lockin.period", defaultUserMessage, "transactionDate", - transactionDate.toString(formatter), getLockedInUntilLocalDate().toString(formatter)); + if (isNotValidAnnualFeeTransactionDate(annualFeeTransactionDate, today)) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("invalid.date"); - final List dataValidationErrors = new ArrayList(); - dataValidationErrors.add(error); + throw new PlatformApiDataValidationException(dataValidationErrors); + } + + Date currentAnnualFeeNextDueDate = findLatestAnnualFeeTransactionDueDate(); + if (currentAnnualFeeNextDueDate != null && new LocalDate(currentAnnualFeeNextDueDate).isEqual(annualFeeTransactionDate)) { + baseDataValidator.reset().parameter("annualFeeTransactionDate").value(annualFeeTransactionDate.toString(formatter)) + .failWithCodeNoParameterAddedToErrorCode("transaction.exists.on.date"); throw new PlatformApiDataValidationException(dataValidationErrors); } @@ -815,19 +798,67 @@ public class SavingsAccount extends AbstractPersistable { existingReversedTransactionIds.addAll(findExistingReversedTransactionIds()); final Money annualFee = Money.of(this.currency, this.annualFeeAmount); - SavingsAccountTransaction annualFeeTransaction = SavingsAccountTransaction.annualFee(this, transactionDate, annualFee); + SavingsAccountTransaction annualFeeTransaction = SavingsAccountTransaction.annualFee(this, annualFeeTransactionDate, annualFee); this.transactions.add(annualFeeTransaction); this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); - final LocalDate today = DateUtils.getLocalDateOfTenant(); calculateInterestUsing(mc, today); - updateAnnualFeeNextDueDate(); + currentAnnualFeeNextDueDate = findLatestAnnualFeeTransactionDueDate(); + if (currentAnnualFeeNextDueDate != null) { + LocalDate newAnnualFeeNextDueDate = new LocalDate(currentAnnualFeeNextDueDate).withMonthOfYear(this.annualFeeOnMonth) + .withDayOfMonth(this.annualFeeOnDay).plusYears(1); + this.annualFeeNextDueDate = newAnnualFeeNextDueDate.toDate(); + } else { + updateToNextAnnualFeeDueDateFrom(getActivationLocalDate()); + } return annualFeeTransaction; } + private boolean isNotValidAnnualFeeTransactionDate(final LocalDate annualFeeTransactionDate, final LocalDate today) { + return !isValidAnnualFeeTransactionDate(annualFeeTransactionDate, today); + } + + private boolean isValidAnnualFeeTransactionDate(final LocalDate annualFeeTransactionDate, final LocalDate today) { + + LocalDate startingDate = getActivationLocalDate(); + boolean isValid = false; + while (!startingDate.isAfter(today) && !isValid) { + LocalDate nextDueLocalDate = startingDate.withMonthOfYear(this.annualFeeOnMonth).withDayOfMonth(this.annualFeeOnDay); + if (startingDate.isAfter(nextDueLocalDate)) { + nextDueLocalDate = nextDueLocalDate.plusYears(1); + } + isValid = nextDueLocalDate.isEqual(annualFeeTransactionDate); + + startingDate = nextDueLocalDate.plusYears(1); + } + + return isValid; + } + + private void updateToNextAnnualFeeDueDateFrom(final LocalDate startingDate) { + LocalDate nextDueLocalDate = startingDate.withMonthOfYear(this.annualFeeOnMonth).withDayOfMonth(this.annualFeeOnDay); + this.annualFeeNextDueDate = nextDueLocalDate.toDate(); + } + + private LocalDate getNextAnnualFeeDueDate() { + LocalDate nextAnnualFeeDueDate = null; + if (this.annualFeeNextDueDate != null) { + nextAnnualFeeDueDate = new LocalDate(this.annualFeeNextDueDate); + } + return nextAnnualFeeDueDate; + } + + private boolean annualFeeSettingsNotSet() { + return !annualFeeSettingsSet(); + } + + private boolean annualFeeSettingsSet() { + return this.annualFeeOnDay != null && this.annualFeeOnMonth != null; + } + private boolean isAutomaticWithdrawalFee() { return this.withdrawalFeeType != null; } @@ -1004,22 +1035,22 @@ public class SavingsAccount extends AbstractPersistable { this.annualFeeAmount = newValue; } - if (command.isChangeInIntegerParameterNamedDefaultingZeroToNull(annualFeeOnMonthDayParamName, this.annualFeeOnDay)) { + if (command.hasParameter(annualFeeOnMonthDayParamName)) { final MonthDay monthDay = command.extractMonthDayNamed(annualFeeOnMonthDayParamName); final String actualValueEntered = command.stringValueOfParameterNamed(annualFeeOnMonthDayParamName); - final Integer newValue = monthDay.getDayOfMonth(); - actualChanges.put(annualFeeOnMonthDayParamName, actualValueEntered); - actualChanges.put(localeParamName, localeAsInput); - this.annualFeeOnDay = newValue; - } + final Integer dayOfMonthValue = monthDay.getDayOfMonth(); + if (this.annualFeeOnDay != dayOfMonthValue) { + actualChanges.put(annualFeeOnMonthDayParamName, actualValueEntered); + actualChanges.put(localeParamName, localeAsInput); + this.annualFeeOnDay = dayOfMonthValue; + } - if (command.isChangeInIntegerParameterNamedDefaultingZeroToNull(annualFeeOnMonthDayParamName, this.annualFeeOnMonth)) { - final MonthDay monthDay = command.extractMonthDayNamed(annualFeeOnMonthDayParamName); - final String actualValueEntered = command.stringValueOfParameterNamed(annualFeeOnMonthDayParamName); - final Integer newValue = monthDay.getMonthOfYear(); - actualChanges.put(annualFeeOnMonthDayParamName, actualValueEntered); - actualChanges.put(localeParamName, localeAsInput); - this.annualFeeOnMonth = newValue; + final Integer monthOfYear = monthDay.getMonthOfYear(); + if (this.annualFeeOnMonth != monthOfYear) { + actualChanges.put(annualFeeOnMonthDayParamName, actualValueEntered); + actualChanges.put(localeParamName, localeAsInput); + this.annualFeeOnMonth = monthOfYear; + } } // set period type to null if frequency is null @@ -1028,10 +1059,6 @@ public class SavingsAccount extends AbstractPersistable { this.annualFeeOnMonth = null; } - if (this.annualFeeOnDay != null && this.annualFeeOnMonth != null) { - updateAnnualFeeNextDueDate(); - } - validateLockinDetails(); validateWithdrawalFeeDetails(); validateAnnualFeeDetails(); @@ -1388,12 +1415,45 @@ public class SavingsAccount extends AbstractPersistable { transactionToUndo.reverse(); reversedTransactionIds.add(transactionId); + if (transactionToUndo.isAnnualFee()) { + this.annualFeeNextDueDate = findLatestAnnualFeeTransactionDueDate(); + if (this.annualFeeNextDueDate == null) { + updateToNextAnnualFeeDueDateFrom(getActivationLocalDate()); + } else { + LocalDate newAnnualFeeNextDueDate = new LocalDate(this.annualFeeNextDueDate).withMonthOfYear(this.annualFeeOnMonth) + .withDayOfMonth(this.annualFeeOnDay).plusYears(1); + this.annualFeeNextDueDate = newAnnualFeeNextDueDate.toDate(); + } + } + calculateInterestUsing(mc, upToInterestCalculationDate); validateAccountBalanceDoesNotBecomeNegativeDueToUndoAction(retreiveListOfTransactions()); } } + private Date findLatestAnnualFeeTransactionDueDate() { + + Date nextDueDate = null; + + LocalDate lastAnnualFeeTransactionDate = null; + for (SavingsAccountTransaction transaction : retreiveOrderedListOfTransactions()) { + if (transaction.isAnnualFeeAndNotReversed()) { + if (lastAnnualFeeTransactionDate == null) { + lastAnnualFeeTransactionDate = transaction.transactionLocalDate(); + nextDueDate = lastAnnualFeeTransactionDate.toDate(); + } + + if (transaction.transactionLocalDate().isAfter(lastAnnualFeeTransactionDate)) { + lastAnnualFeeTransactionDate = transaction.transactionLocalDate(); + nextDueDate = lastAnnualFeeTransactionDate.toDate(); + } + } + } + + return nextDueDate; + } + public Map rejectApplication(final AppUser currentUser, final JsonCommand command, final LocalDate tenantsTodayDate) { final Map actualChanges = new LinkedHashMap(); @@ -1548,6 +1608,10 @@ public class SavingsAccount extends AbstractPersistable { this.activatedBy = currentUser; this.lockedInUntilDate = calculateDateAccountIsLockedUntil(getActivationLocalDate()); + if (annualFeeSettingsSet()) { + updateToNextAnnualFeeDueDateFrom(getActivationLocalDate()); + } + if (this.client.isActivatedAfter(activationDate)) { final DateTimeFormatter formatter = DateTimeFormat.forPattern(command.dateFormat()).withLocale(command.extractLocale()); 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 d133d76b1..e9a27c633 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 @@ -146,16 +146,20 @@ public final class SavingsAccountTransaction extends AbstractPersistable { return SavingsAccountTransactionType.fromInt(this.typeOf).isWithdrawal(); } - public boolean isInterestPosting() { + public boolean isInterestPostingAndNotReversed() { return SavingsAccountTransactionType.fromInt(this.typeOf).isInterestPosting() && isNotReversed(); } - public boolean isWithdrawalFee() { + public boolean isWithdrawalFeeAndNotReversed() { return SavingsAccountTransactionType.fromInt(this.typeOf).isWithdrawalFee() && isNotReversed(); } + public boolean isAnnualFeeAndNotReversed() { + return isAnnualFee() && isNotReversed(); + } + public boolean isAnnualFee() { - return SavingsAccountTransactionType.fromInt(this.typeOf).isAnnualFee() && isNotReversed(); + return SavingsAccountTransactionType.fromInt(this.typeOf).isAnnualFee(); } public boolean isNotReversed() { @@ -258,7 +262,7 @@ public final class SavingsAccountTransaction extends AbstractPersistable { Money endOfDayBalance = openingBalance.copy(); if (isDeposit()) { endOfDayBalance = openingBalance.plus(getAmount(currency)); - } else if (isWithdrawal() || isWithdrawalFee()) { + } else if (isWithdrawal() || isWithdrawalFeeAndNotReversed()) { endOfDayBalance = openingBalance.minus(getAmount(currency)); } @@ -274,7 +278,7 @@ public final class SavingsAccountTransaction extends AbstractPersistable { Money endOfDayBalance = openingBalance.copy(); if (isDeposit()) { endOfDayBalance = openingBalance.plus(getAmount(currency)); - } else if (isWithdrawal() || isWithdrawalFee()) { + } else if (isWithdrawal() || isWithdrawalFeeAndNotReversed()) { endOfDayBalance = openingBalance.minus(getAmount(currency)); } @@ -298,7 +302,7 @@ public final class SavingsAccountTransaction extends AbstractPersistable { } else { if (isDeposit()) { endOfDayBalance = openingBalance.plus(getAmount(currency)); - } else if (isWithdrawal() || isWithdrawalFee()) { + } else if (isWithdrawal() || isWithdrawalFeeAndNotReversed()) { endOfDayBalance = openingBalance.minus(getAmount(currency)); } } @@ -329,4 +333,12 @@ public final class SavingsAccountTransaction extends AbstractPersistable { public boolean isIdentifiedBy(final Long transactionId) { return this.getId().equals(transactionId); } + + public boolean isCredit() { + return isDeposit() || isInterestPostingAndNotReversed(); + } + + public boolean isDebit() { + return isWithdrawal() || isWithdrawalFeeAndNotReversed() || isAnnualFeeAndNotReversed(); + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountTransactionComparator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountTransactionComparator.java index 840f48750..0d592f6ba 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountTransactionComparator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountTransactionComparator.java @@ -19,9 +19,9 @@ public class SavingsAccountTransactionComparator implements Comparator transactions) { Money total = Money.zero(currency); for (SavingsAccountTransaction transaction : transactions) { - if (transaction.isInterestPosting() && transaction.isNotReversed()) { + if (transaction.isInterestPostingAndNotReversed() && transaction.isNotReversed()) { total = total.plus(transaction.getAmount(currency)); } } @@ -52,7 +52,7 @@ public final class SavingsAccountTransactionSummaryWrapper { public BigDecimal calculateTotalWithdrawalFees(final MonetaryCurrency currency, final List transactions) { Money total = Money.zero(currency); for (SavingsAccountTransaction transaction : transactions) { - if (transaction.isWithdrawalFee() && transaction.isNotReversed()) { + if (transaction.isWithdrawalFeeAndNotReversed() && transaction.isNotReversed()) { total = total.plus(transaction.getAmount(currency)); } } @@ -62,7 +62,7 @@ public final class SavingsAccountTransactionSummaryWrapper { public BigDecimal calculateTotalAnnualFees(final MonetaryCurrency currency, final List transactions) { Money total = Money.zero(currency); for (SavingsAccountTransaction transaction : transactions) { - if (transaction.isAnnualFee() && transaction.isNotReversed()) { + if (transaction.isAnnualFeeAndNotReversed() && transaction.isNotReversed()) { total = total.plus(transaction.getAmount(currency)); } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/handler/ApplyAnnualFeeSavingsAccountCommandHandler.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/handler/ApplyAnnualFeeSavingsAccountCommandHandler.java new file mode 100644 index 000000000..226e97814 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/handler/ApplyAnnualFeeSavingsAccountCommandHandler.java @@ -0,0 +1,35 @@ +/** + * 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.handler; + +import org.joda.time.LocalDate; +import org.mifosplatform.commands.handler.NewCommandSourceHandler; +import org.mifosplatform.infrastructure.core.api.JsonCommand; +import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; +import org.mifosplatform.portfolio.savings.service.SavingsAccountWritePlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class ApplyAnnualFeeSavingsAccountCommandHandler implements NewCommandSourceHandler { + + private final SavingsAccountWritePlatformService writePlatformService; + + @Autowired + public ApplyAnnualFeeSavingsAccountCommandHandler(final SavingsAccountWritePlatformService writePlatformService) { + this.writePlatformService = writePlatformService; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + LocalDate annualFeeTransactionDate = command.localDateValueOfParameterNamed("annualFeeTransactionDate"); + + return this.writePlatformService.applyAnnualFee(command.getSavingsId(), annualFeeTransactionDate); + } +} \ No newline at end of file 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 240acf10c..677d97f4b 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 @@ -9,6 +9,7 @@ import java.util.Collection; import org.mifosplatform.infrastructure.core.service.Page; import org.mifosplatform.portfolio.group.service.SearchParameters; +import org.mifosplatform.portfolio.savings.data.SavingsAccountAnnualFeeData; import org.mifosplatform.portfolio.savings.data.SavingsAccountData; import org.mifosplatform.portfolio.savings.data.SavingsAccountTransactionData; @@ -23,4 +24,6 @@ public interface SavingsAccountReadPlatformService { SavingsAccountTransactionData retrieveDepositTransactionTemplate(Long savingsId); Collection retrieveAllTransactions(Long savingsId); + + Collection retrieveAccountsWithAnnualFeeDue(); } \ No newline at end of file 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 b54dff26d..c71b96f72 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 @@ -37,6 +37,7 @@ import org.mifosplatform.portfolio.savings.SavingsInterestCalculationDaysInYearT import org.mifosplatform.portfolio.savings.SavingsInterestCalculationType; import org.mifosplatform.portfolio.savings.SavingsPeriodFrequencyType; import org.mifosplatform.portfolio.savings.SavingsPostingInterestPeriodType; +import org.mifosplatform.portfolio.savings.data.SavingsAccountAnnualFeeData; import org.mifosplatform.portfolio.savings.data.SavingsAccountApplicationTimelineData; import org.mifosplatform.portfolio.savings.data.SavingsAccountData; import org.mifosplatform.portfolio.savings.data.SavingsAccountStatusEnumData; @@ -63,11 +64,16 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead private final SavingsProductReadPlatformService savingsProductReadPlatformService; private final StaffReadPlatformService staffReadPlatformService; private final SavingsDropdownReadPlatformService dropdownReadPlatformService; + + // mappers private final SavingsAccountTransactionTemplateMapper transactionTemplateMapper; private final SavingsAccountTransactionsMapper transactionsMapper; + private final SavingAccountMapper savingAccountMapper; + private final SavingsAccountAnnualFeeMapper annualFeeMapper; + // pagination private final PaginationHelper paginationHelper = new PaginationHelper(); - private final SavingAccountMapper savingAccountMapper = new SavingAccountMapper(); + @Autowired public SavingsAccountReadPlatformServiceImpl(final PlatformSecurityContext context, final RoutingDataSource dataSource, @@ -83,6 +89,8 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead this.dropdownReadPlatformService = dropdownReadPlatformService; this.transactionTemplateMapper = new SavingsAccountTransactionTemplateMapper(); this.transactionsMapper = new SavingsAccountTransactionsMapper(); + this.savingAccountMapper = new SavingAccountMapper(); + this.annualFeeMapper = new SavingsAccountAnnualFeeMapper(); } @Override @@ -476,8 +484,6 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead public SavingsAccountTransactionData retrieveDepositTransactionTemplate(final Long savingsId) { try { - this.context.authenticatedUser(); - final String sql = "select " + transactionTemplateMapper.schema() + " where sa.id = ?"; return this.jdbcTemplate.queryForObject(sql, transactionTemplateMapper, new Object[] { savingsId }); @@ -494,6 +500,14 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead return this.jdbcTemplate.query(sql, this.transactionsMapper, new Object[] { savingsId }); } + @Override + public Collection retrieveAccountsWithAnnualFeeDue() { + final String sql = "select " + annualFeeMapper.schema() + + " where sa.annual_fee_next_due_date not null and sa.annual_fee_next_due_date <= NOW()"; + + return this.jdbcTemplate.query(sql, this.annualFeeMapper, new Object[] {}); + } + private static final class SavingsAccountTransactionsMapper implements RowMapper { private final String schemaSql; @@ -731,4 +745,32 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead annualFeeOnMonthDay, annualFeeNextDueDate, summary); } } + + private static final class SavingsAccountAnnualFeeMapper implements RowMapper { + + private final String schemaSql; + + public SavingsAccountAnnualFeeMapper() { + final StringBuilder sqlBuilder = new StringBuilder(200); + sqlBuilder.append("sa.id as id, sa.account_no as accountNo, "); + sqlBuilder.append("sa.annual_fee_next_due_date as annualFeeNextDueDate, "); + sqlBuilder.append("from m_savings_account sa "); + + this.schemaSql = sqlBuilder.toString(); + } + + public String schema() { + return this.schemaSql; + } + + @Override + public SavingsAccountAnnualFeeData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + + final Long id = rs.getLong("id"); + final String accountNo = rs.getString("accountNo"); + final LocalDate annualFeeNextDueDate = JdbcSupport.getLocalDate(rs, "annualFeeNextDueDate"); + + return SavingsAccountAnnualFeeData.instance(id, accountNo, annualFeeNextDueDate); + } + } } \ No newline at end of file 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 fc45e279c..42ffbcc9b 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 @@ -5,6 +5,7 @@ */ package org.mifosplatform.portfolio.savings.service; +import org.joda.time.LocalDate; import org.mifosplatform.infrastructure.core.api.JsonCommand; import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; @@ -16,7 +17,7 @@ public interface SavingsAccountWritePlatformService { CommandProcessingResult withdrawal(Long savingsId, JsonCommand command); - CommandProcessingResult applyAnnualFee(Long savingsId); + CommandProcessingResult applyAnnualFee(Long savingsId, LocalDate annualFeeTransactionDate); CommandProcessingResult calculateInterest(Long savingsId); 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 2b973dd7d..27cc1959b 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 @@ -205,20 +205,20 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi @Transactional @Override - public CommandProcessingResult applyAnnualFee(final Long savingsId) { + public CommandProcessingResult applyAnnualFee(final Long savingsId, final LocalDate annualFeeTransactionDate) { final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId); - final LocalDate transactionDate = LocalDate.now(); - final DateTimeFormatter fmt = DateTimeFormat.forPattern("dd MM yyyy"); final List existingTransactionIds = new ArrayList(); final List existingReversedTransactionIds = new ArrayList(); - final MathContext mc = new MathContext(10, RoundingMode.HALF_EVEN); - final SavingsAccountTransaction annualFee = account.addAnnualFee(mc, fmt, transactionDate, - existingTransactionIds, existingReversedTransactionIds); + final MathContext mc = MathContext.DECIMAL64; + final LocalDate today = DateUtils.getLocalDateOfTenant(); + + final SavingsAccountTransaction annualFee = account.addAnnualFee(mc, fmt, annualFeeTransactionDate, today, existingTransactionIds, + existingReversedTransactionIds); final Long transactionId = saveTransactionToGenerateTransactionId(annualFee); this.savingAccountRepository.save(account); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java index e20e89165..540aaac19 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java @@ -209,8 +209,12 @@ public class SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl impl if (changes.containsKey(SavingsApiConstants.fieldOfficerIdParamName)) { final Long fieldOfficerId = command.longValueOfParameterNamed(SavingsApiConstants.fieldOfficerIdParamName); - final Staff fieldOfficer = this.staffRepository.findOneWithNotFoundDetection(fieldOfficerId); - + Staff fieldOfficer = null; + if (fieldOfficerId != null) { + fieldOfficer = this.staffRepository.findOneWithNotFoundDetection(fieldOfficerId); + } else { + changes.put(SavingsApiConstants.fieldOfficerIdParamName, ""); + } account.update(fieldOfficer); } 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 29caa53b3..00a37e310 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,12 +5,14 @@ */ package org.mifosplatform.scheduledjobs.service; -import java.util.List; +import java.util.Collection; import org.mifosplatform.infrastructure.core.service.RoutingDataSourceServiceFactory; import org.mifosplatform.infrastructure.core.service.ThreadLocalContextUtil; import org.mifosplatform.infrastructure.jobs.annotation.CronTarget; import org.mifosplatform.infrastructure.jobs.service.JobName; +import org.mifosplatform.portfolio.savings.data.SavingsAccountAnnualFeeData; +import org.mifosplatform.portfolio.savings.service.SavingsAccountReadPlatformService; import org.mifosplatform.portfolio.savings.service.SavingsAccountWritePlatformService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -23,14 +25,18 @@ import org.springframework.transaction.annotation.Transactional; public class ScheduledJobRunnerServiceImpl implements ScheduledJobRunnerService { private final static Logger logger = LoggerFactory.getLogger(ScheduledJobRunnerServiceImpl.class); + private final RoutingDataSourceServiceFactory dataSourceServiceFactory; private final SavingsAccountWritePlatformService savingsAccountWritePlatformService; + private final SavingsAccountReadPlatformService savingsAccountReadPlatformService; @Autowired public ScheduledJobRunnerServiceImpl(final RoutingDataSourceServiceFactory dataSourceServiceFactory, - final SavingsAccountWritePlatformService savingsAccountWritePlatformService) { + final SavingsAccountWritePlatformService savingsAccountWritePlatformService, + final SavingsAccountReadPlatformService savingsAccountReadPlatformService) { this.dataSourceServiceFactory = dataSourceServiceFactory; this.savingsAccountWritePlatformService = savingsAccountWritePlatformService; + this.savingsAccountReadPlatformService = savingsAccountReadPlatformService; } @Transactional @@ -193,16 +199,15 @@ public class ScheduledJobRunnerServiceImpl implements ScheduledJobRunnerService @Override @CronTarget(jobName = JobName.APPLY_ANNUAL_FEE_FOR_SAVINGS) public void applyAnnualFeeForSavings() { + + final Collection annualFeeData = this.savingsAccountReadPlatformService + .retrieveAccountsWithAnnualFeeDue(); - JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSourceServiceFactory.determineDataSourceService().retrieveDataSource()); - - 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.applyAnnualFee(savingsId); + for (SavingsAccountAnnualFeeData savingsAccountReference : annualFeeData) { + this.savingsAccountWritePlatformService.applyAnnualFee(savingsAccountReference.getId(), + savingsAccountReference.getNextAnnualFeeDueDate()); } - logger.info(ThreadLocalContextUtil.getTenant().getName() + ": Results affected by update: " + savingsIds.size()); + logger.info(ThreadLocalContextUtil.getTenant().getName() + ": Savings accounts affected by update: " + annualFeeData.size()); } } \ No newline at end of file