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");
}
public boolean isSavingsAccountApplyAnnualFee() {
return this.actionName.equalsIgnoreCase("APPLYANNUALFEE") && this.entityName.equalsIgnoreCase("SAVINGSACCOUNT");
}
public boolean isSavingsAccountUndoTransaction() {
return this.actionName.equalsIgnoreCase("UNDOTRANSACTION") && this.entityName.equalsIgnoreCase("SAVINGSACCOUNT");
}
@@ -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;
}
}
@@ -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 {
@@ -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) {
@@ -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";
@@ -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;
@@ -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) {
@@ -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;
}
public Long id() {
return this.id;
}
public Long clientId() {
return this.clientId;
}
@@ -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)) {
@@ -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)
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<Long> {
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<Long> {
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<Long> {
List<SavingsAccountTransaction> orderedNonInterestPostingTransactions = new ArrayList<SavingsAccountTransaction>();
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<Long> {
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<Long> {
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<Long> {
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<Long> {
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<Long> {
}
}
public SavingsAccountTransaction addAnnualFee(final MathContext mc, final DateTimeFormatter formatter, final LocalDate transactionDate,
final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds) {
public SavingsAccountTransaction addAnnualFee(final MathContext mc, final DateTimeFormatter formatter,
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()) {
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<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
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<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
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<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
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<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
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<Long> {
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<Long> {
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<Long> {
this.annualFeeOnMonth = null;
}
if (this.annualFeeOnDay != null && this.annualFeeOnMonth != null) {
updateAnnualFeeNextDueDate();
}
validateLockinDetails();
validateWithdrawalFeeDetails();
validateAnnualFeeDetails();
@@ -1388,12 +1415,45 @@ public class SavingsAccount extends AbstractPersistable<Long> {
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<String, Object> rejectApplication(final AppUser currentUser, final JsonCommand command, final LocalDate tenantsTodayDate) {
final Map<String, Object> actualChanges = new LinkedHashMap<String, Object>();
@@ -1548,6 +1608,10 @@ public class SavingsAccount extends AbstractPersistable<Long> {
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());
@@ -146,16 +146,20 @@ public final class SavingsAccountTransaction extends AbstractPersistable<Long> {
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<Long> {
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<Long> {
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<Long> {
} 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<Long> {
public boolean isIdentifiedBy(final Long 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());
if (comparsion == 0) {
// equal transaction dates
if (o1.isInterestPosting() && !o2.isInterestPosting()) {
if (o1.isInterestPostingAndNotReversed() && !o2.isInterestPostingAndNotReversed()) {
compareResult = -1;
} else if (!o1.isInterestPosting() && o2.isInterestPosting()) {
} else if (!o1.isInterestPostingAndNotReversed() && o2.isInterestPostingAndNotReversed()) {
compareResult = 1;
} else {
compareResult = 0;
@@ -42,7 +42,7 @@ public final class SavingsAccountTransactionSummaryWrapper {
public BigDecimal calculateTotalInterestPosted(final MonetaryCurrency currency, final List<SavingsAccountTransaction> 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<SavingsAccountTransaction> 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<SavingsAccountTransaction> 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));
}
}
@@ -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.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<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.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<SavingsAccountData> paginationHelper = new PaginationHelper<SavingsAccountData>();
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<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 final String schemaSql;
@@ -731,4 +745,32 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead
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;
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);
@@ -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<Long> existingTransactionIds = new ArrayList<Long>();
final List<Long> existingReversedTransactionIds = new ArrayList<Long>();
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);
@@ -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);
}
@@ -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<SavingsAccountAnnualFeeData> 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<Long> 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());
}
}