MIFOSX-397: support apply of annual fees on savings account through manual and scheduled job approach.

This commit is contained in:
Keith Woodlock
2013-07-25 00:57:25 +01:00
parent 4b392a830c
commit b48bc62691
23 changed files with 372 additions and 111 deletions
@@ -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);
@@ -466,6 +466,10 @@ public class CommandWrapper {
return this.actionName.equalsIgnoreCase("POSTINTEREST") && this.entityName.equalsIgnoreCase("SAVINGSACCOUNT"); return this.actionName.equalsIgnoreCase("POSTINTEREST") && this.entityName.equalsIgnoreCase("SAVINGSACCOUNT");
} }
public boolean isSavingsAccountApplyAnnualFee() {
return this.actionName.equalsIgnoreCase("APPLYANNUALFEE") && this.entityName.equalsIgnoreCase("SAVINGSACCOUNT");
}
public boolean isSavingsAccountUndoTransaction() { public boolean isSavingsAccountUndoTransaction() {
return this.actionName.equalsIgnoreCase("UNDOTRANSACTION") && this.entityName.equalsIgnoreCase("SAVINGSACCOUNT"); return this.actionName.equalsIgnoreCase("UNDOTRANSACTION") && this.entityName.equalsIgnoreCase("SAVINGSACCOUNT");
} }
@@ -47,6 +47,11 @@ public class CommandWrapperBuilder {
return this; return this;
} }
public CommandWrapperBuilder withNoJsonBody() {
this.json = null;
return this;
}
public CommandWrapperBuilder updateGlobalConfiguration() { public CommandWrapperBuilder updateGlobalConfiguration() {
this.actionName = "UPDATE"; this.actionName = "UPDATE";
this.entityName = "CONFIGURATION"; this.entityName = "CONFIGURATION";
@@ -850,6 +855,15 @@ public class CommandWrapperBuilder {
return this; 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) { public CommandWrapperBuilder createCalendar(final String supportedEntityType, final Long supportedEntityId) {
this.actionName = "CREATE"; this.actionName = "CREATE";
this.entityName = "CALENDAR"; this.entityName = "CALENDAR";
@@ -1168,5 +1182,4 @@ public class CommandWrapperBuilder {
this.href = "/updateJobDetail/" + jobId+"/updateJobDetail"; this.href = "/updateJobDetail/" + jobId+"/updateJobDetail";
return this; return this;
} }
} }
@@ -401,6 +401,8 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
handler = this.applicationContext.getBean("calculateInterestSavingsAccountCommandHandler", NewCommandSourceHandler.class); handler = this.applicationContext.getBean("calculateInterestSavingsAccountCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isSavingsAccountInterestPosting()) { } else if (wrapper.isSavingsAccountInterestPosting()) {
handler = this.applicationContext.getBean("postInterestSavingsAccountCommandHandler", NewCommandSourceHandler.class); handler = this.applicationContext.getBean("postInterestSavingsAccountCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isSavingsAccountApplyAnnualFee()) {
handler = this.applicationContext.getBean("applyAnnualFeeSavingsAccountCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isSavingsAccountUndoTransaction()) { } else if (wrapper.isSavingsAccountUndoTransaction()) {
handler = this.applicationContext.getBean("undoTransactionSavingsAccountCommandHandler", NewCommandSourceHandler.class); handler = this.applicationContext.getBean("undoTransactionSavingsAccountCommandHandler", NewCommandSourceHandler.class);
} else { } else {
@@ -97,7 +97,13 @@ public class FromJsonHelper {
} }
public JsonElement parse(final String json) { 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) { public boolean parameterExists(final String parameterName, final JsonElement element) {
@@ -29,6 +29,7 @@ public class SavingsApiConstants {
public static String modifyApplicationAction = ".modify"; public static String modifyApplicationAction = ".modify";
public static String deleteApplicationAction = ".delete"; public static String deleteApplicationAction = ".delete";
public static String undoTransactionAction = ".undotransaction"; public static String undoTransactionAction = ".undotransaction";
public static String applyAnnualFeeTransactionAction = ".applyannualfee";
// general // general
public static final String localeParamName = "locale"; public static final String localeParamName = "locale";
@@ -120,7 +120,7 @@ public class SavingsAccountTransactionsApiResource {
@Path("{transactionId}") @Path("{transactionId}")
@Consumes({ MediaType.APPLICATION_JSON }) @Consumes({ MediaType.APPLICATION_JSON })
@Produces({ 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) { @QueryParam("command") final String commandParam, final String apiRequestBodyAsJson) {
String jsonApiRequest = apiRequestBodyAsJson; String jsonApiRequest = apiRequestBodyAsJson;
@@ -164,7 +164,6 @@ public class SavingsAccountsApiResource {
SavingsAccountData templateData = null; SavingsAccountData templateData = null;
final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters()); final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters());
if (settings.isTemplate()) { if (settings.isTemplate()) {
templateData = this.savingsAccountReadPlatformService.retrieveTemplate(savingsAccount.clientId(), savingsAccount.groupId(), templateData = this.savingsAccountReadPlatformService.retrieveTemplate(savingsAccount.clientId(), savingsAccount.groupId(),
savingsAccount.productId(), staffInSelectedOfficeOnly); savingsAccount.productId(), staffInSelectedOfficeOnly);
} }
@@ -217,11 +216,14 @@ public class SavingsAccountsApiResource {
final CommandWrapper commandRequest = builder.savingsAccountActivation(accountId).build(); final CommandWrapper commandRequest = builder.savingsAccountActivation(accountId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, "calculateInterest")) { } 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); result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, "postInterest")) { } else if (is(commandParam, "postInterest")) {
final CommandWrapper commandRequest = builder.savingsAccountInterestPosting(accountId).build(); final CommandWrapper commandRequest = builder.savingsAccountInterestPosting(accountId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); 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) { if (result == null) {
@@ -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;
}
}
@@ -248,6 +248,10 @@ public class SavingsAccountData {
this.withdrawalFeeTypeOptions = withdrawalFeeTypeOptions; this.withdrawalFeeTypeOptions = withdrawalFeeTypeOptions;
} }
public Long id() {
return this.id;
}
public Long clientId() { public Long clientId() {
return this.clientId; return this.clientId;
} }
@@ -92,7 +92,7 @@ public class SavingsAccountDataValidator {
if (fromApiJsonHelper.parameterExists(fieldOfficerIdParamName, element)) { if (fromApiJsonHelper.parameterExists(fieldOfficerIdParamName, element)) {
final Long fieldOfficerId = fromApiJsonHelper.extractLongNamed(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); final LocalDate submittedOnDate = fromApiJsonHelper.extractLocalDateNamed(submittedOnDateParamName, element);
@@ -276,7 +276,7 @@ public class SavingsAccountDataValidator {
if (fromApiJsonHelper.parameterExists(fieldOfficerIdParamName, element)) { if (fromApiJsonHelper.parameterExists(fieldOfficerIdParamName, element)) {
final Long fieldOfficerId = fromApiJsonHelper.extractLongNamed(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)) { if (fromApiJsonHelper.parameterExists(submittedOnDateParamName, element)) {
@@ -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;
}
}
@@ -230,7 +230,6 @@ public class SavingsAccount extends AbstractPersistable<Long> {
@Column(name = "annual_fee_on_day", nullable = true) @Column(name = "annual_fee_on_day", nullable = true)
private Integer annualFeeOnDay; private Integer annualFeeOnDay;
@SuppressWarnings("unused")
@Temporal(TemporalType.DATE) @Temporal(TemporalType.DATE)
@Column(name = "annual_fee_next_due_date", nullable = true) @Column(name = "annual_fee_next_due_date", nullable = true)
private Date annualFeeNextDueDate; private Date annualFeeNextDueDate;
@@ -311,22 +310,11 @@ public class SavingsAccount extends AbstractPersistable<Long> {
if (annualFeeOnMonthDay != null) { if (annualFeeOnMonthDay != null) {
this.annualFeeOnMonth = annualFeeOnMonthDay.getMonthOfYear(); this.annualFeeOnMonth = annualFeeOnMonthDay.getMonthOfYear();
this.annualFeeOnDay = annualFeeOnMonthDay.getDayOfMonth(); this.annualFeeOnDay = annualFeeOnMonthDay.getDayOfMonth();
updateAnnualFeeNextDueDate();
} }
this.summary = new SavingsAccountSummary(); 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 * Used after fetching/hydrating a {@link SavingsAccount} object to inject
* helper services/components used for update summary details after * helper services/components used for update summary details after
@@ -412,7 +400,7 @@ public class SavingsAccount extends AbstractPersistable<Long> {
SavingsAccountTransaction postingTransation = null; SavingsAccountTransaction postingTransation = null;
for (SavingsAccountTransaction transaction : this.transactions) { for (SavingsAccountTransaction transaction : this.transactions) {
if (transaction.isInterestPosting() && transaction.occursOn(postingDate)) { if (transaction.isInterestPostingAndNotReversed() && transaction.occursOn(postingDate)) {
postingTransation = transaction; postingTransation = transaction;
break; break;
} }
@@ -492,7 +480,7 @@ public class SavingsAccount extends AbstractPersistable<Long> {
List<SavingsAccountTransaction> orderedNonInterestPostingTransactions = new ArrayList<SavingsAccountTransaction>(); List<SavingsAccountTransaction> orderedNonInterestPostingTransactions = new ArrayList<SavingsAccountTransaction>();
for (SavingsAccountTransaction transaction : listOfTransactionsSorted) { for (SavingsAccountTransaction transaction : listOfTransactionsSorted) {
if (!transaction.isInterestPosting() && transaction.isNotReversed()) { if (!transaction.isInterestPostingAndNotReversed() && transaction.isNotReversed()) {
orderedNonInterestPostingTransactions.add(transaction); orderedNonInterestPostingTransactions.add(transaction);
} }
} }
@@ -523,12 +511,10 @@ public class SavingsAccount extends AbstractPersistable<Long> {
transaction.zeroBalanceFields(); transaction.zeroBalanceFields();
} else { } else {
Money transactionAmount = Money.zero(this.currency); Money transactionAmount = Money.zero(this.currency);
if (transaction.isDeposit()) { if (transaction.isCredit()) {
transactionAmount = transactionAmount.plus(transaction.getAmount(this.currency)); transactionAmount = transactionAmount.plus(transaction.getAmount(this.currency));
} else if (transaction.isWithdrawal()) { } else if (transaction.isDebit()) {
transactionAmount = transactionAmount.minus(transaction.getAmount(this.currency)); transactionAmount = transactionAmount.minus(transaction.getAmount(this.currency));
} else if (transaction.isInterestPosting()) {
transactionAmount = transactionAmount.plus(transaction.getAmount(this.currency));
} }
runningBalance = runningBalance.plus(transactionAmount); runningBalance = runningBalance.plus(transactionAmount);
@@ -705,7 +691,7 @@ public class SavingsAccount extends AbstractPersistable<Long> {
boolean transactionBeforeLastInterestPosting = false; boolean transactionBeforeLastInterestPosting = false;
for (SavingsAccountTransaction transaction : retreiveListOfTransactions()) { for (SavingsAccountTransaction transaction : retreiveListOfTransactions()) {
if (transaction.isNotReversed() && transaction.isInterestPosting() && transaction.isAfter(transactionDate)) { if (transaction.isNotReversed() && transaction.isInterestPostingAndNotReversed() && transaction.isAfter(transactionDate)) {
transactionBeforeLastInterestPosting = true; transactionBeforeLastInterestPosting = true;
break; break;
} }
@@ -720,7 +706,7 @@ public class SavingsAccount extends AbstractPersistable<Long> {
Money runningBalance = Money.zero(this.currency); Money runningBalance = Money.zero(this.currency);
for (SavingsAccountTransaction transaction : transactionsSortedByDate) { 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)); runningBalance = runningBalance.plus(transaction.getAmount(this.currency));
} else if (transaction.isNotReversed() && transaction.isWithdrawal()) { } else if (transaction.isNotReversed() && transaction.isWithdrawal()) {
runningBalance = runningBalance.minus(transaction.getAmount(this.currency)); runningBalance = runningBalance.minus(transaction.getAmount(this.currency));
@@ -740,7 +726,7 @@ public class SavingsAccount extends AbstractPersistable<Long> {
Money runningBalance = Money.zero(this.currency); Money runningBalance = Money.zero(this.currency);
for (SavingsAccountTransaction transaction : transactionsSortedByDate) { 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)); runningBalance = runningBalance.plus(transaction.getAmount(this.currency));
} else if (transaction.isNotReversed() && transaction.isWithdrawal()) { } else if (transaction.isNotReversed() && transaction.isWithdrawal()) {
runningBalance = runningBalance.minus(transaction.getAmount(this.currency)); runningBalance = runningBalance.minus(transaction.getAmount(this.currency));
@@ -759,54 +745,51 @@ public class SavingsAccount extends AbstractPersistable<Long> {
} }
} }
public SavingsAccountTransaction addAnnualFee(final MathContext mc, final DateTimeFormatter formatter, final LocalDate transactionDate, public SavingsAccountTransaction addAnnualFee(final MathContext mc, final DateTimeFormatter formatter,
final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds) { final LocalDate annualFeeTransactionDate, final LocalDate today, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds) {
final List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(SAVINGS_ACCOUNT_RESOURCE_NAME + SavingsApiConstants.applyAnnualFeeTransactionAction);
if (isNotActive()) { if (isNotActive()) {
baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.is.not.active");
final String defaultUserMessage = "Transaction is not allowed. Account is not active."; if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); }
final ApiParameterError error = ApiParameterError.parameterError("error.msg.savingsaccount.transaction.account.is.not.active", }
defaultUserMessage, "transactionDate", transactionDate.toString(formatter));
final List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>(); LocalDate nextAnnualFeeDueDate = getNextAnnualFeeDueDate();
dataValidationErrors.add(error); if (nextAnnualFeeDueDate == null || annualFeeSettingsNotSet()) {
baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("no.annualfee.settings");
throw new PlatformApiDataValidationException(dataValidationErrors); throw new PlatformApiDataValidationException(dataValidationErrors);
} }
if (isDateInTheFuture(transactionDate)) { if (nextAnnualFeeDueDate.isBefore(getActivationLocalDate())) {
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));
final List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>(); baseDataValidator.reset().parameter("annualFeeTransactionDate").value(getActivationLocalDate().toString(formatter))
dataValidationErrors.add(error); .failWithCodeNoParameterAddedToErrorCode("before.activationDate");
throw new PlatformApiDataValidationException(dataValidationErrors); throw new PlatformApiDataValidationException(dataValidationErrors);
} }
if (transactionDate.isBefore(getActivationLocalDate())) { if (isDateInTheFuture(annualFeeTransactionDate)) {
final Object[] defaultUserArgs = Arrays.asList(transactionDate.toString(formatter), baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("transaction.in.the.future");
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<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
dataValidationErrors.add(error);
throw new PlatformApiDataValidationException(dataValidationErrors); throw new PlatformApiDataValidationException(dataValidationErrors);
} }
if (isAccountLocked(transactionDate)) { if (isNotValidAnnualFeeTransactionDate(annualFeeTransactionDate, today)) {
final String defaultUserMessage = "Withdrawal is not allowed. No withdrawals are allowed until after " baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("invalid.date");
+ 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));
final List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>(); throw new PlatformApiDataValidationException(dataValidationErrors);
dataValidationErrors.add(error); }
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); throw new PlatformApiDataValidationException(dataValidationErrors);
} }
@@ -815,19 +798,67 @@ public class SavingsAccount extends AbstractPersistable<Long> {
existingReversedTransactionIds.addAll(findExistingReversedTransactionIds()); existingReversedTransactionIds.addAll(findExistingReversedTransactionIds());
final Money annualFee = Money.of(this.currency, this.annualFeeAmount); 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.transactions.add(annualFeeTransaction);
this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions);
final LocalDate today = DateUtils.getLocalDateOfTenant();
calculateInterestUsing(mc, today); 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; 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() { private boolean isAutomaticWithdrawalFee() {
return this.withdrawalFeeType != null; return this.withdrawalFeeType != null;
} }
@@ -1004,22 +1035,22 @@ public class SavingsAccount extends AbstractPersistable<Long> {
this.annualFeeAmount = newValue; this.annualFeeAmount = newValue;
} }
if (command.isChangeInIntegerParameterNamedDefaultingZeroToNull(annualFeeOnMonthDayParamName, this.annualFeeOnDay)) { if (command.hasParameter(annualFeeOnMonthDayParamName)) {
final MonthDay monthDay = command.extractMonthDayNamed(annualFeeOnMonthDayParamName); final MonthDay monthDay = command.extractMonthDayNamed(annualFeeOnMonthDayParamName);
final String actualValueEntered = command.stringValueOfParameterNamed(annualFeeOnMonthDayParamName); final String actualValueEntered = command.stringValueOfParameterNamed(annualFeeOnMonthDayParamName);
final Integer newValue = monthDay.getDayOfMonth(); final Integer dayOfMonthValue = monthDay.getDayOfMonth();
actualChanges.put(annualFeeOnMonthDayParamName, actualValueEntered); if (this.annualFeeOnDay != dayOfMonthValue) {
actualChanges.put(localeParamName, localeAsInput); actualChanges.put(annualFeeOnMonthDayParamName, actualValueEntered);
this.annualFeeOnDay = newValue; actualChanges.put(localeParamName, localeAsInput);
} this.annualFeeOnDay = dayOfMonthValue;
}
if (command.isChangeInIntegerParameterNamedDefaultingZeroToNull(annualFeeOnMonthDayParamName, this.annualFeeOnMonth)) { final Integer monthOfYear = monthDay.getMonthOfYear();
final MonthDay monthDay = command.extractMonthDayNamed(annualFeeOnMonthDayParamName); if (this.annualFeeOnMonth != monthOfYear) {
final String actualValueEntered = command.stringValueOfParameterNamed(annualFeeOnMonthDayParamName); actualChanges.put(annualFeeOnMonthDayParamName, actualValueEntered);
final Integer newValue = monthDay.getMonthOfYear(); actualChanges.put(localeParamName, localeAsInput);
actualChanges.put(annualFeeOnMonthDayParamName, actualValueEntered); this.annualFeeOnMonth = monthOfYear;
actualChanges.put(localeParamName, localeAsInput); }
this.annualFeeOnMonth = newValue;
} }
// set period type to null if frequency is null // set period type to null if frequency is null
@@ -1028,10 +1059,6 @@ public class SavingsAccount extends AbstractPersistable<Long> {
this.annualFeeOnMonth = null; this.annualFeeOnMonth = null;
} }
if (this.annualFeeOnDay != null && this.annualFeeOnMonth != null) {
updateAnnualFeeNextDueDate();
}
validateLockinDetails(); validateLockinDetails();
validateWithdrawalFeeDetails(); validateWithdrawalFeeDetails();
validateAnnualFeeDetails(); validateAnnualFeeDetails();
@@ -1388,12 +1415,45 @@ public class SavingsAccount extends AbstractPersistable<Long> {
transactionToUndo.reverse(); transactionToUndo.reverse();
reversedTransactionIds.add(transactionId); 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); calculateInterestUsing(mc, upToInterestCalculationDate);
validateAccountBalanceDoesNotBecomeNegativeDueToUndoAction(retreiveListOfTransactions()); 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<String, Object> rejectApplication(final AppUser currentUser, final JsonCommand command, final LocalDate tenantsTodayDate) { public Map<String, Object> rejectApplication(final AppUser currentUser, final JsonCommand command, final LocalDate tenantsTodayDate) {
final Map<String, Object> actualChanges = new LinkedHashMap<String, Object>(); final Map<String, Object> actualChanges = new LinkedHashMap<String, Object>();
@@ -1548,6 +1608,10 @@ public class SavingsAccount extends AbstractPersistable<Long> {
this.activatedBy = currentUser; this.activatedBy = currentUser;
this.lockedInUntilDate = calculateDateAccountIsLockedUntil(getActivationLocalDate()); this.lockedInUntilDate = calculateDateAccountIsLockedUntil(getActivationLocalDate());
if (annualFeeSettingsSet()) {
updateToNextAnnualFeeDueDateFrom(getActivationLocalDate());
}
if (this.client.isActivatedAfter(activationDate)) { if (this.client.isActivatedAfter(activationDate)) {
final DateTimeFormatter formatter = DateTimeFormat.forPattern(command.dateFormat()).withLocale(command.extractLocale()); final DateTimeFormatter formatter = DateTimeFormat.forPattern(command.dateFormat()).withLocale(command.extractLocale());
@@ -146,16 +146,20 @@ public final class SavingsAccountTransaction extends AbstractPersistable<Long> {
return SavingsAccountTransactionType.fromInt(this.typeOf).isWithdrawal(); return SavingsAccountTransactionType.fromInt(this.typeOf).isWithdrawal();
} }
public boolean isInterestPosting() { public boolean isInterestPostingAndNotReversed() {
return SavingsAccountTransactionType.fromInt(this.typeOf).isInterestPosting() && isNotReversed(); return SavingsAccountTransactionType.fromInt(this.typeOf).isInterestPosting() && isNotReversed();
} }
public boolean isWithdrawalFee() { public boolean isWithdrawalFeeAndNotReversed() {
return SavingsAccountTransactionType.fromInt(this.typeOf).isWithdrawalFee() && isNotReversed(); return SavingsAccountTransactionType.fromInt(this.typeOf).isWithdrawalFee() && isNotReversed();
} }
public boolean isAnnualFeeAndNotReversed() {
return isAnnualFee() && isNotReversed();
}
public boolean isAnnualFee() { public boolean isAnnualFee() {
return SavingsAccountTransactionType.fromInt(this.typeOf).isAnnualFee() && isNotReversed(); return SavingsAccountTransactionType.fromInt(this.typeOf).isAnnualFee();
} }
public boolean isNotReversed() { public boolean isNotReversed() {
@@ -258,7 +262,7 @@ public final class SavingsAccountTransaction extends AbstractPersistable<Long> {
Money endOfDayBalance = openingBalance.copy(); Money endOfDayBalance = openingBalance.copy();
if (isDeposit()) { if (isDeposit()) {
endOfDayBalance = openingBalance.plus(getAmount(currency)); endOfDayBalance = openingBalance.plus(getAmount(currency));
} else if (isWithdrawal() || isWithdrawalFee()) { } else if (isWithdrawal() || isWithdrawalFeeAndNotReversed()) {
endOfDayBalance = openingBalance.minus(getAmount(currency)); endOfDayBalance = openingBalance.minus(getAmount(currency));
} }
@@ -274,7 +278,7 @@ public final class SavingsAccountTransaction extends AbstractPersistable<Long> {
Money endOfDayBalance = openingBalance.copy(); Money endOfDayBalance = openingBalance.copy();
if (isDeposit()) { if (isDeposit()) {
endOfDayBalance = openingBalance.plus(getAmount(currency)); endOfDayBalance = openingBalance.plus(getAmount(currency));
} else if (isWithdrawal() || isWithdrawalFee()) { } else if (isWithdrawal() || isWithdrawalFeeAndNotReversed()) {
endOfDayBalance = openingBalance.minus(getAmount(currency)); endOfDayBalance = openingBalance.minus(getAmount(currency));
} }
@@ -298,7 +302,7 @@ public final class SavingsAccountTransaction extends AbstractPersistable<Long> {
} else { } else {
if (isDeposit()) { if (isDeposit()) {
endOfDayBalance = openingBalance.plus(getAmount(currency)); endOfDayBalance = openingBalance.plus(getAmount(currency));
} else if (isWithdrawal() || isWithdrawalFee()) { } else if (isWithdrawal() || isWithdrawalFeeAndNotReversed()) {
endOfDayBalance = openingBalance.minus(getAmount(currency)); endOfDayBalance = openingBalance.minus(getAmount(currency));
} }
} }
@@ -329,4 +333,12 @@ public final class SavingsAccountTransaction extends AbstractPersistable<Long> {
public boolean isIdentifiedBy(final Long transactionId) { public boolean isIdentifiedBy(final Long transactionId) {
return this.getId().equals(transactionId); return this.getId().equals(transactionId);
} }
public boolean isCredit() {
return isDeposit() || isInterestPostingAndNotReversed();
}
public boolean isDebit() {
return isWithdrawal() || isWithdrawalFeeAndNotReversed() || isAnnualFeeAndNotReversed();
}
} }
@@ -19,9 +19,9 @@ public class SavingsAccountTransactionComparator implements Comparator<SavingsAc
final int comparsion = o1.transactionLocalDate().compareTo(o2.transactionLocalDate()); final int comparsion = o1.transactionLocalDate().compareTo(o2.transactionLocalDate());
if (comparsion == 0) { if (comparsion == 0) {
// equal transaction dates // equal transaction dates
if (o1.isInterestPosting() && !o2.isInterestPosting()) { if (o1.isInterestPostingAndNotReversed() && !o2.isInterestPostingAndNotReversed()) {
compareResult = -1; compareResult = -1;
} else if (!o1.isInterestPosting() && o2.isInterestPosting()) { } else if (!o1.isInterestPostingAndNotReversed() && o2.isInterestPostingAndNotReversed()) {
compareResult = 1; compareResult = 1;
} else { } else {
compareResult = 0; compareResult = 0;
@@ -42,7 +42,7 @@ public final class SavingsAccountTransactionSummaryWrapper {
public BigDecimal calculateTotalInterestPosted(final MonetaryCurrency currency, final List<SavingsAccountTransaction> transactions) { public BigDecimal calculateTotalInterestPosted(final MonetaryCurrency currency, final List<SavingsAccountTransaction> transactions) {
Money total = Money.zero(currency); Money total = Money.zero(currency);
for (SavingsAccountTransaction transaction : transactions) { for (SavingsAccountTransaction transaction : transactions) {
if (transaction.isInterestPosting() && transaction.isNotReversed()) { if (transaction.isInterestPostingAndNotReversed() && transaction.isNotReversed()) {
total = total.plus(transaction.getAmount(currency)); total = total.plus(transaction.getAmount(currency));
} }
} }
@@ -52,7 +52,7 @@ public final class SavingsAccountTransactionSummaryWrapper {
public BigDecimal calculateTotalWithdrawalFees(final MonetaryCurrency currency, final List<SavingsAccountTransaction> transactions) { public BigDecimal calculateTotalWithdrawalFees(final MonetaryCurrency currency, final List<SavingsAccountTransaction> transactions) {
Money total = Money.zero(currency); Money total = Money.zero(currency);
for (SavingsAccountTransaction transaction : transactions) { for (SavingsAccountTransaction transaction : transactions) {
if (transaction.isWithdrawalFee() && transaction.isNotReversed()) { if (transaction.isWithdrawalFeeAndNotReversed() && transaction.isNotReversed()) {
total = total.plus(transaction.getAmount(currency)); total = total.plus(transaction.getAmount(currency));
} }
} }
@@ -62,7 +62,7 @@ public final class SavingsAccountTransactionSummaryWrapper {
public BigDecimal calculateTotalAnnualFees(final MonetaryCurrency currency, final List<SavingsAccountTransaction> transactions) { public BigDecimal calculateTotalAnnualFees(final MonetaryCurrency currency, final List<SavingsAccountTransaction> transactions) {
Money total = Money.zero(currency); Money total = Money.zero(currency);
for (SavingsAccountTransaction transaction : transactions) { for (SavingsAccountTransaction transaction : transactions) {
if (transaction.isAnnualFee() && transaction.isNotReversed()) { if (transaction.isAnnualFeeAndNotReversed() && transaction.isNotReversed()) {
total = total.plus(transaction.getAmount(currency)); total = total.plus(transaction.getAmount(currency));
} }
} }
@@ -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);
}
}
@@ -9,6 +9,7 @@ import java.util.Collection;
import org.mifosplatform.infrastructure.core.service.Page; import org.mifosplatform.infrastructure.core.service.Page;
import org.mifosplatform.portfolio.group.service.SearchParameters; 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.SavingsAccountData;
import org.mifosplatform.portfolio.savings.data.SavingsAccountTransactionData; import org.mifosplatform.portfolio.savings.data.SavingsAccountTransactionData;
@@ -23,4 +24,6 @@ public interface SavingsAccountReadPlatformService {
SavingsAccountTransactionData retrieveDepositTransactionTemplate(Long savingsId); SavingsAccountTransactionData retrieveDepositTransactionTemplate(Long savingsId);
Collection<SavingsAccountTransactionData> retrieveAllTransactions(Long savingsId); Collection<SavingsAccountTransactionData> retrieveAllTransactions(Long savingsId);
Collection<SavingsAccountAnnualFeeData> retrieveAccountsWithAnnualFeeDue();
} }
@@ -37,6 +37,7 @@ import org.mifosplatform.portfolio.savings.SavingsInterestCalculationDaysInYearT
import org.mifosplatform.portfolio.savings.SavingsInterestCalculationType; import org.mifosplatform.portfolio.savings.SavingsInterestCalculationType;
import org.mifosplatform.portfolio.savings.SavingsPeriodFrequencyType; import org.mifosplatform.portfolio.savings.SavingsPeriodFrequencyType;
import org.mifosplatform.portfolio.savings.SavingsPostingInterestPeriodType; 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.SavingsAccountApplicationTimelineData;
import org.mifosplatform.portfolio.savings.data.SavingsAccountData; import org.mifosplatform.portfolio.savings.data.SavingsAccountData;
import org.mifosplatform.portfolio.savings.data.SavingsAccountStatusEnumData; import org.mifosplatform.portfolio.savings.data.SavingsAccountStatusEnumData;
@@ -63,11 +64,16 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead
private final SavingsProductReadPlatformService savingsProductReadPlatformService; private final SavingsProductReadPlatformService savingsProductReadPlatformService;
private final StaffReadPlatformService staffReadPlatformService; private final StaffReadPlatformService staffReadPlatformService;
private final SavingsDropdownReadPlatformService dropdownReadPlatformService; private final SavingsDropdownReadPlatformService dropdownReadPlatformService;
// mappers
private final SavingsAccountTransactionTemplateMapper transactionTemplateMapper; private final SavingsAccountTransactionTemplateMapper transactionTemplateMapper;
private final SavingsAccountTransactionsMapper transactionsMapper; private final SavingsAccountTransactionsMapper transactionsMapper;
private final SavingAccountMapper savingAccountMapper;
private final SavingsAccountAnnualFeeMapper annualFeeMapper;
// pagination
private final PaginationHelper<SavingsAccountData> paginationHelper = new PaginationHelper<SavingsAccountData>(); private final PaginationHelper<SavingsAccountData> paginationHelper = new PaginationHelper<SavingsAccountData>();
private final SavingAccountMapper savingAccountMapper = new SavingAccountMapper();
@Autowired @Autowired
public SavingsAccountReadPlatformServiceImpl(final PlatformSecurityContext context, final RoutingDataSource dataSource, public SavingsAccountReadPlatformServiceImpl(final PlatformSecurityContext context, final RoutingDataSource dataSource,
@@ -83,6 +89,8 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead
this.dropdownReadPlatformService = dropdownReadPlatformService; this.dropdownReadPlatformService = dropdownReadPlatformService;
this.transactionTemplateMapper = new SavingsAccountTransactionTemplateMapper(); this.transactionTemplateMapper = new SavingsAccountTransactionTemplateMapper();
this.transactionsMapper = new SavingsAccountTransactionsMapper(); this.transactionsMapper = new SavingsAccountTransactionsMapper();
this.savingAccountMapper = new SavingAccountMapper();
this.annualFeeMapper = new SavingsAccountAnnualFeeMapper();
} }
@Override @Override
@@ -476,8 +484,6 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead
public SavingsAccountTransactionData retrieveDepositTransactionTemplate(final Long savingsId) { public SavingsAccountTransactionData retrieveDepositTransactionTemplate(final Long savingsId) {
try { try {
this.context.authenticatedUser();
final String sql = "select " + transactionTemplateMapper.schema() + " where sa.id = ?"; final String sql = "select " + transactionTemplateMapper.schema() + " where sa.id = ?";
return this.jdbcTemplate.queryForObject(sql, transactionTemplateMapper, new Object[] { savingsId }); 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 }); return this.jdbcTemplate.query(sql, this.transactionsMapper, new Object[] { savingsId });
} }
@Override
public Collection<SavingsAccountAnnualFeeData> 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<SavingsAccountTransactionData> { private static final class SavingsAccountTransactionsMapper implements RowMapper<SavingsAccountTransactionData> {
private final String schemaSql; private final String schemaSql;
@@ -731,4 +745,32 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead
annualFeeOnMonthDay, annualFeeNextDueDate, summary); annualFeeOnMonthDay, annualFeeNextDueDate, summary);
} }
} }
private static final class SavingsAccountAnnualFeeMapper implements RowMapper<SavingsAccountAnnualFeeData> {
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);
}
}
} }
@@ -5,6 +5,7 @@
*/ */
package org.mifosplatform.portfolio.savings.service; package org.mifosplatform.portfolio.savings.service;
import org.joda.time.LocalDate;
import org.mifosplatform.infrastructure.core.api.JsonCommand; import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; import org.mifosplatform.infrastructure.core.data.CommandProcessingResult;
@@ -16,7 +17,7 @@ public interface SavingsAccountWritePlatformService {
CommandProcessingResult withdrawal(Long savingsId, JsonCommand command); CommandProcessingResult withdrawal(Long savingsId, JsonCommand command);
CommandProcessingResult applyAnnualFee(Long savingsId); CommandProcessingResult applyAnnualFee(Long savingsId, LocalDate annualFeeTransactionDate);
CommandProcessingResult calculateInterest(Long savingsId); CommandProcessingResult calculateInterest(Long savingsId);
@@ -205,20 +205,20 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
@Transactional @Transactional
@Override @Override
public CommandProcessingResult applyAnnualFee(final Long savingsId) { public CommandProcessingResult applyAnnualFee(final Long savingsId, final LocalDate annualFeeTransactionDate) {
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId); final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId);
final LocalDate transactionDate = LocalDate.now();
final DateTimeFormatter fmt = DateTimeFormat.forPattern("dd MM yyyy"); final DateTimeFormatter fmt = DateTimeFormat.forPattern("dd MM yyyy");
final List<Long> existingTransactionIds = new ArrayList<Long>(); final List<Long> existingTransactionIds = new ArrayList<Long>();
final List<Long> existingReversedTransactionIds = new ArrayList<Long>(); final List<Long> existingReversedTransactionIds = new ArrayList<Long>();
final MathContext mc = new MathContext(10, RoundingMode.HALF_EVEN); final MathContext mc = MathContext.DECIMAL64;
final SavingsAccountTransaction annualFee = account.addAnnualFee(mc, fmt, transactionDate, final LocalDate today = DateUtils.getLocalDateOfTenant();
existingTransactionIds, existingReversedTransactionIds);
final SavingsAccountTransaction annualFee = account.addAnnualFee(mc, fmt, annualFeeTransactionDate, today, existingTransactionIds,
existingReversedTransactionIds);
final Long transactionId = saveTransactionToGenerateTransactionId(annualFee); final Long transactionId = saveTransactionToGenerateTransactionId(annualFee);
this.savingAccountRepository.save(account); this.savingAccountRepository.save(account);
@@ -209,8 +209,12 @@ public class SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
if (changes.containsKey(SavingsApiConstants.fieldOfficerIdParamName)) { if (changes.containsKey(SavingsApiConstants.fieldOfficerIdParamName)) {
final Long fieldOfficerId = command.longValueOfParameterNamed(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); account.update(fieldOfficer);
} }
@@ -5,12 +5,14 @@
*/ */
package org.mifosplatform.scheduledjobs.service; 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.RoutingDataSourceServiceFactory;
import org.mifosplatform.infrastructure.core.service.ThreadLocalContextUtil; import org.mifosplatform.infrastructure.core.service.ThreadLocalContextUtil;
import org.mifosplatform.infrastructure.jobs.annotation.CronTarget; import org.mifosplatform.infrastructure.jobs.annotation.CronTarget;
import org.mifosplatform.infrastructure.jobs.service.JobName; 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.mifosplatform.portfolio.savings.service.SavingsAccountWritePlatformService;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -23,14 +25,18 @@ import org.springframework.transaction.annotation.Transactional;
public class ScheduledJobRunnerServiceImpl implements ScheduledJobRunnerService { public class ScheduledJobRunnerServiceImpl implements ScheduledJobRunnerService {
private final static Logger logger = LoggerFactory.getLogger(ScheduledJobRunnerServiceImpl.class); private final static Logger logger = LoggerFactory.getLogger(ScheduledJobRunnerServiceImpl.class);
private final RoutingDataSourceServiceFactory dataSourceServiceFactory; private final RoutingDataSourceServiceFactory dataSourceServiceFactory;
private final SavingsAccountWritePlatformService savingsAccountWritePlatformService; private final SavingsAccountWritePlatformService savingsAccountWritePlatformService;
private final SavingsAccountReadPlatformService savingsAccountReadPlatformService;
@Autowired @Autowired
public ScheduledJobRunnerServiceImpl(final RoutingDataSourceServiceFactory dataSourceServiceFactory, public ScheduledJobRunnerServiceImpl(final RoutingDataSourceServiceFactory dataSourceServiceFactory,
final SavingsAccountWritePlatformService savingsAccountWritePlatformService) { final SavingsAccountWritePlatformService savingsAccountWritePlatformService,
final SavingsAccountReadPlatformService savingsAccountReadPlatformService) {
this.dataSourceServiceFactory = dataSourceServiceFactory; this.dataSourceServiceFactory = dataSourceServiceFactory;
this.savingsAccountWritePlatformService = savingsAccountWritePlatformService; this.savingsAccountWritePlatformService = savingsAccountWritePlatformService;
this.savingsAccountReadPlatformService = savingsAccountReadPlatformService;
} }
@Transactional @Transactional
@@ -194,15 +200,14 @@ public class ScheduledJobRunnerServiceImpl implements ScheduledJobRunnerService
@CronTarget(jobName = JobName.APPLY_ANNUAL_FEE_FOR_SAVINGS) @CronTarget(jobName = JobName.APPLY_ANNUAL_FEE_FOR_SAVINGS)
public void applyAnnualFeeForSavings() { public void applyAnnualFeeForSavings() {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSourceServiceFactory.determineDataSourceService().retrieveDataSource()); final Collection<SavingsAccountAnnualFeeData> annualFeeData = this.savingsAccountReadPlatformService
.retrieveAccountsWithAnnualFeeDue();
String sql = "SELECT ms.id as savingsAccountId FROM m_savings_account ms WHERE ms.annual_fee_next_due_date <= NOW() "; for (SavingsAccountAnnualFeeData savingsAccountReference : annualFeeData) {
List<Long> savingsIds = jdbcTemplate.queryForList(sql, Long.class); this.savingsAccountWritePlatformService.applyAnnualFee(savingsAccountReference.getId(),
savingsAccountReference.getNextAnnualFeeDueDate());
for (Long savingsId : savingsIds) {
this.savingsAccountWritePlatformService.applyAnnualFee(savingsId);
} }
logger.info(ThreadLocalContextUtil.getTenant().getName() + ": Results affected by update: " + savingsIds.size()); logger.info(ThreadLocalContextUtil.getTenant().getName() + ": Savings accounts affected by update: " + annualFeeData.size());
} }
} }