diff --git a/api-docs/apiLive.htm b/api-docs/apiLive.htm index 75227bd99..46bb779c5 100644 --- a/api-docs/apiLive.htm +++ b/api-docs/apiLive.htm @@ -11862,7 +11862,7 @@ Content-Type: application/json No Request Body: { "locale": "en", - "nominalAnnualInterestRate": "5.65", + "nominalAnnualInterestRate": "5.9999999999", } @@ -12053,6 +12053,31 @@ No Request Body: + +   +
+
+

Undo transaction

+

This command reverses the given transaction.

+
+
+ POST https://Domain Name/api/v1/savingsaccounts/{accountsId}/transactions/{transactionId}?command=undo + POST savingsaccounts/1/transactions/1?command=undo +Content-Type: application/json +No Request Body: +{ +} + + +{ + "officeId": 1, + "clientId": 2, + "savingsId": 1, + "resourceId": 1 +} + +
+
@@ -17218,6 +17243,14 @@ GET https://DomainName/api/v1/search?query=000000111 + + + + savingsaccounts/{accountId}/transactions/{transactionId}?command=undo + Undo transaction + + + diff --git a/mifosng-db/migrations/core_db/V81__savings_related_changes.sql b/mifosng-db/migrations/core_db/V81__savings_related_changes.sql index 5cf68d763..ee81e1a56 100644 --- a/mifosng-db/migrations/core_db/V81__savings_related_changes.sql +++ b/mifosng-db/migrations/core_db/V81__savings_related_changes.sql @@ -35,7 +35,7 @@ SET `approvedon_date`=`activatedon_date`; -INSERT INTO `mifostenant-default`.`m_permission` +INSERT INTO `m_permission` (`grouping`,`code`,`entity_name`,`action_name`,`can_maker_checker`) VALUES ('transaction_savings', 'APPROVE_SAVINGSACCOUNT', 'SAVINGSACCOUNT', 'APPROVE', 1), @@ -44,7 +44,7 @@ VALUES ('transaction_savings', 'APPROVALUNDO_SAVINGSACCOUNT', 'SAVINGSACCOUNT', 'APPROVALUNDO', 1), ('transaction_savings', 'CLOSE_SAVINGSACCOUNT', 'SAVINGSACCOUNT', 'CLOSE', 1); -INSERT INTO `mifostenant-default`.`m_permission` +INSERT INTO `m_permission` (`grouping`,`code`,`entity_name`,`action_name`,`can_maker_checker`) VALUES ('transaction_savings', 'APPROVE_SAVINGSACCOUNT_CHECKER', 'SAVINGSACCOUNT', 'APPROVE', 0), diff --git a/mifosng-db/migrations/core_db/V84__undo_savings_transaction_permission.sql b/mifosng-db/migrations/core_db/V84__undo_savings_transaction_permission.sql new file mode 100644 index 000000000..16860a41c --- /dev/null +++ b/mifosng-db/migrations/core_db/V84__undo_savings_transaction_permission.sql @@ -0,0 +1,5 @@ +INSERT INTO `m_permission` +(`grouping`,`code`,`entity_name`,`action_name`,`can_maker_checker`) +VALUES +('transaction_savings', 'UNDOTRANSACTION_SAVINGSACCOUNT', 'SAVINGSACCOUNT', 'UNDOTRANSACTION', 1), +('transaction_savings', 'UNDOTRANSACTION_SAVINGSACCOUNT_CHECKER', 'SAVINGSACCOUNT', 'UNDOTRANSACTION', 0); \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java b/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java index 17b2ca473..7406539d2 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java @@ -459,6 +459,10 @@ public class CommandWrapper { return this.actionName.equalsIgnoreCase("POSTINTEREST") && this.entityName.equalsIgnoreCase("SAVINGSACCOUNT"); } + public boolean isSavingsAccountUndoTransaction() { + return this.actionName.equalsIgnoreCase("UNDOTRANSACTION") && this.entityName.equalsIgnoreCase("SAVINGSACCOUNT"); + } + public boolean isCalendarResource() { return this.entityName.equalsIgnoreCase("CALENDAR"); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java index 45db68066..c32081ad0 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java @@ -820,6 +820,17 @@ public class CommandWrapperBuilder { return this; } + public CommandWrapperBuilder undoSavingsAccountTransaction(final Long accountId, final Long transactionId) { + this.actionName = "UNDOTRANSACTION"; + this.entityName = "SAVINGSACCOUNT"; + this.savingsId = accountId; + this.entityId = accountId; + this.subentityId = transactionId; + this.transactionId = transactionId.toString(); + this.href = "/savingsaccounts/" + accountId + "/transactions/" + transactionId + "?command=undo"; + return this; + } + public CommandWrapperBuilder savingsAccountInterestCalculation(final Long accountId) { this.actionName = "CALCULATEINTEREST"; this.entityName = "SAVINGSACCOUNT"; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java index 68c98a282..0e007a2c2 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java @@ -401,6 +401,8 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer handler = this.applicationContext.getBean("calculateInterestSavingsAccountCommandHandler", NewCommandSourceHandler.class); } else if (wrapper.isSavingsAccountInterestPosting()) { handler = this.applicationContext.getBean("postInterestSavingsAccountCommandHandler", NewCommandSourceHandler.class); + } else if (wrapper.isSavingsAccountUndoTransaction()) { + handler = this.applicationContext.getBean("undoTransactionSavingsAccountCommandHandler", NewCommandSourceHandler.class); } else { throw new UnsupportedCommandException(wrapper.commandName()); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/SavingsAccountTransactionType.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/SavingsAccountTransactionType.java index fb22b500a..e660f11ca 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/SavingsAccountTransactionType.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/SavingsAccountTransactionType.java @@ -80,4 +80,12 @@ public enum SavingsAccountTransactionType { public boolean isAnnualFee() { return this.value.equals(SavingsAccountTransactionType.ANNUAL_FEE.getValue()); } + + public boolean isDebit() { + return isWithdrawal() || isWithdrawalFee() || isAnnualFee(); + } + + public boolean isCredit() { + return isDeposit() || isInterestPosting(); + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/SavingsApiConstants.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/SavingsApiConstants.java index baa0f9d04..d8f136f29 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/SavingsApiConstants.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/SavingsApiConstants.java @@ -28,6 +28,7 @@ public class SavingsApiConstants { public static String activateAction = ".activate"; public static String modifyApplicationAction = ".modify"; public static String deleteApplicationAction = ".delete"; + public static String undoTransactionAction = ".undotransaction"; // general public static final String localeParamName = "locale"; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/api/SavingsAccountTransactionsApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/api/SavingsAccountTransactionsApiResource.java index 47ecd2bb1..f7a6820f6 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/api/SavingsAccountTransactionsApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/api/SavingsAccountTransactionsApiResource.java @@ -109,8 +109,36 @@ public class SavingsAccountTransactionsApiResource { } if (result == null) { - // FIXME - KW - handle blank command param use case - throw new UnrecognizedQueryParamException("command", commandParam); + // + throw new UnrecognizedQueryParamException("command", commandParam, new Object[] { "deposit", "withdrawal" }); + } + + return this.toApiJsonSerializer.serialize(result); + } + + @POST + @Path("{transactionId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String transaction(@PathParam("savingsId") final Long savingsId, @PathParam("transactionId") final Long transactionId, + @QueryParam("command") final String commandParam, final String apiRequestBodyAsJson) { + + String jsonApiRequest = apiRequestBodyAsJson; + if (StringUtils.isBlank(jsonApiRequest)) { + jsonApiRequest = "{}"; + } + + final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(jsonApiRequest); + + CommandProcessingResult result = null; + if (is(commandParam, "undo")) { + final CommandWrapper commandRequest = builder.undoSavingsAccountTransaction(savingsId, transactionId).build(); + result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + } + + if (result == null) { + // + throw new UnrecognizedQueryParamException("command", commandParam, new Object[] { "undo" }); } return this.toApiJsonSerializer.serialize(result); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/api/SavingsAccountsApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/api/SavingsAccountsApiResource.java index 14f7dab0b..a890c7879 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/api/SavingsAccountsApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/api/SavingsAccountsApiResource.java @@ -193,7 +193,12 @@ public class SavingsAccountsApiResource { public String handleCommands(@PathParam("accountId") final Long accountId, @QueryParam("command") final String commandParam, final String apiRequestBodyAsJson) { - final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson); + String jsonApiRequest = apiRequestBodyAsJson; + if (StringUtils.isBlank(jsonApiRequest)) { + jsonApiRequest = "{}"; + } + + final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(jsonApiRequest); CommandProcessingResult result = null; if (is(commandParam, "reject")) { @@ -212,7 +217,7 @@ public class SavingsAccountsApiResource { final CommandWrapper commandRequest = builder.savingsAccountActivation(accountId).build(); result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); } else if (is(commandParam, "calculateInterest")) { - final CommandWrapper commandRequest = builder.savingsAccountInterestCalculation(accountId).build(); + final CommandWrapper commandRequest = builder.withJson(null).savingsAccountInterestCalculation(accountId).build(); result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); } else if (is(commandParam, "postInterest")) { final CommandWrapper commandRequest = builder.savingsAccountInterestPosting(accountId).build(); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/SavingsAccountTransactionData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/SavingsAccountTransactionData.java index 200e84e1f..28547379e 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/SavingsAccountTransactionData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/SavingsAccountTransactionData.java @@ -27,16 +27,18 @@ public class SavingsAccountTransactionData { private final CurrencyData currency; private final PaymentDetailData paymentDetailData; private final BigDecimal amount; + private final BigDecimal runningBalance; + private final boolean reversed; // templates final Collection paymentTypeOptions; public static SavingsAccountTransactionData create(final Long id, final SavingsAccountTransactionEnumData transactionType, final PaymentDetailData paymentDetailData, final Long savingsId, final String savingsAccountNo, final LocalDate date, - final CurrencyData currency, final BigDecimal amount) { + final CurrencyData currency, final BigDecimal amount, final BigDecimal runningBalance, final boolean reversed) { final Collection paymentTypeOptions = null; return new SavingsAccountTransactionData(id, transactionType, paymentDetailData, savingsId, savingsAccountNo, date, currency, - amount, paymentTypeOptions); + amount, runningBalance, reversed, paymentTypeOptions); } public static SavingsAccountTransactionData template(final Long savingsId, final String savingsAccountNo, @@ -44,10 +46,12 @@ public class SavingsAccountTransactionData { final Long id = null; final SavingsAccountTransactionEnumData transactionType = null; final BigDecimal amount = null; + final BigDecimal runningBalance = null; + final boolean reversed = false; final PaymentDetailData paymentDetailData = null; final Collection paymentTypeOptions = null; return new SavingsAccountTransactionData(id, transactionType, paymentDetailData, savingsId, savingsAccountNo, defaultLocalDate, - currency, amount, null); + currency, amount, runningBalance, reversed, null); } public static SavingsAccountTransactionData templateOnTop(final SavingsAccountTransactionData savingsAccountTransactionData, @@ -55,12 +59,14 @@ public class SavingsAccountTransactionData { return new SavingsAccountTransactionData(savingsAccountTransactionData.id, savingsAccountTransactionData.transactionType, savingsAccountTransactionData.paymentDetailData, savingsAccountTransactionData.accountId, savingsAccountTransactionData.accountNo, savingsAccountTransactionData.date, savingsAccountTransactionData.currency, - savingsAccountTransactionData.amount, paymentTypeOptions); + savingsAccountTransactionData.amount, savingsAccountTransactionData.runningBalance, savingsAccountTransactionData.reversed, + paymentTypeOptions); } private SavingsAccountTransactionData(final Long id, final SavingsAccountTransactionEnumData transactionType, final PaymentDetailData paymentDetailData, final Long savingsId, final String savingsAccountNo, final LocalDate date, - final CurrencyData currency, final BigDecimal amount, final Collection paymentTypeOptions) { + final CurrencyData currency, final BigDecimal amount, final BigDecimal runningBalance, final boolean reversed, + final Collection paymentTypeOptions) { this.id = id; this.transactionType = transactionType; this.paymentDetailData = paymentDetailData; @@ -69,6 +75,8 @@ public class SavingsAccountTransactionData { this.date = date; this.currency = currency; this.amount = amount; + this.runningBalance = runningBalance; + this.reversed = reversed; this.paymentTypeOptions = paymentTypeOptions; } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccount.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccount.java index 380a0d114..dc08d3320 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccount.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccount.java @@ -247,6 +247,8 @@ public class SavingsAccount extends AbstractPersistable { private boolean accountNumberRequiresAutoGeneration = false; @Transient private SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper; + @Transient + private SavingsHelper savingsHelper; protected SavingsAccount() { // @@ -330,8 +332,10 @@ public class SavingsAccount extends AbstractPersistable { * helper services/components used for update summary details after * events/transactions on a {@link SavingsAccount}. */ - public void setHelpers(final SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper) { + public void setHelpers(final SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper, + final SavingsHelper savingsHelper) { this.savingsAccountTransactionSummaryWrapper = savingsAccountTransactionSummaryWrapper; + this.savingsHelper = savingsHelper; } public boolean isNotActive() { @@ -350,10 +354,10 @@ public class SavingsAccount extends AbstractPersistable { return SavingsAccountStatusType.fromInt(this.status).isSubmittedAndPendingApproval(); } - public void postInterest(final SavingsHelper savingsHelper, final MathContext mc, final LocalDate interestPostingUpToDate, - final List existingTransactionIds, final List existingReversedTransactionIds) { + public void postInterest(final MathContext mc, final LocalDate interestPostingUpToDate, final List existingTransactionIds, + final List existingReversedTransactionIds) { - final List postingPeriods = calculateInterestUsing(savingsHelper, mc, interestPostingUpToDate); + final List postingPeriods = calculateInterestUsing(mc, interestPostingUpToDate); Money interestPostedToDate = Money.zero(this.currency); @@ -431,8 +435,7 @@ public class SavingsAccount extends AbstractPersistable { * period check if an existing 'interest posting' transaction exists for * date and matches the amount posted */ - public List calculateInterestUsing(final SavingsHelper savingsHelper, final MathContext mc, - final LocalDate upToInterestCalculationDate) { + public List calculateInterestUsing(final MathContext mc, final LocalDate upToInterestCalculationDate) { // no openingBalance concept supported yet but probably will to allow // for migrations. @@ -732,9 +735,32 @@ public class SavingsAccount extends AbstractPersistable { } } - public SavingsAccountTransaction addAnnualFee(final SavingsHelper savingsHelper, final MathContext mc, - final DateTimeFormatter formatter, final LocalDate transactionDate, final List existingTransactionIds, - final List existingReversedTransactionIds) { + private void validateAccountBalanceDoesNotBecomeNegativeDueToUndoAction(final List transactionsSortedByDate) { + + Money runningBalance = Money.zero(this.currency); + + for (SavingsAccountTransaction transaction : transactionsSortedByDate) { + if (transaction.isNotReversed() && transaction.isDeposit() || transaction.isInterestPosting()) { + runningBalance = runningBalance.plus(transaction.getAmount(this.currency)); + } else if (transaction.isNotReversed() && transaction.isWithdrawal()) { + runningBalance = runningBalance.minus(transaction.getAmount(this.currency)); + } + + if (runningBalance.isLessThanZero()) { + // + final List dataValidationErrors = new ArrayList(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(SAVINGS_ACCOUNT_RESOURCE_NAME + SavingsApiConstants.undoTransactionAction); + + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("results.in.balance.going.negative"); + + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } + } + } + } + + public SavingsAccountTransaction addAnnualFee(final MathContext mc, final DateTimeFormatter formatter, final LocalDate transactionDate, + final List existingTransactionIds, final List existingReversedTransactionIds) { if (isNotActive()) { @@ -795,7 +821,7 @@ public class SavingsAccount extends AbstractPersistable { this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); final LocalDate today = DateUtils.getLocalDateOfTenant(); - calculateInterestUsing(savingsHelper, mc, today); + calculateInterestUsing(mc, today); updateAnnualFeeNextDueDate(); @@ -1345,6 +1371,29 @@ public class SavingsAccount extends AbstractPersistable { return actualChanges; } + public void undoTransaction(final Long transactionId, final List reversedTransactionIds, final MathContext mc, + final LocalDate upToInterestCalculationDate) { + + SavingsAccountTransaction transactionToUndo = null; + for (SavingsAccountTransaction transaction : this.transactions) { + if (transaction.isIdentifiedBy(transactionId)) { + transactionToUndo = transaction; + } + } + + if (transactionToUndo == null) { + // throw non found exception + + } else { + transactionToUndo.reverse(); + reversedTransactionIds.add(transactionId); + + calculateInterestUsing(mc, upToInterestCalculationDate); + + validateAccountBalanceDoesNotBecomeNegativeDueToUndoAction(retreiveListOfTransactions()); + } + } + public Map rejectApplication(final AppUser currentUser, final JsonCommand command, final LocalDate tenantsTodayDate) { final Map actualChanges = new LinkedHashMap(); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountAssembler.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountAssembler.java index 2e954d19c..0b0826f29 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountAssembler.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountAssembler.java @@ -54,6 +54,7 @@ import com.google.gson.JsonElement; public class SavingsAccountAssembler { private final SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper; + private final SavingsHelper savingsHelper = new SavingsHelper(); private final ClientRepositoryWrapper clientRepository; private final GroupRepositoryWrapper groupRepository; private final StaffRepositoryWrapper staffRepository; @@ -213,7 +214,7 @@ public class SavingsAccountAssembler { submittedOnDate, interestRate, interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, withdrawalFeeAmount, withdrawalFeeType, annualFeeAmount, monthDayOfAnnualFee); - account.setHelpers(this.savingsAccountTransactionSummaryWrapper); + account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper); account.validateNewApplicationState(DateUtils.getLocalDateOfTenant()); @@ -222,7 +223,7 @@ public class SavingsAccountAssembler { public SavingsAccount assembleFrom(final Long savingsId) { SavingsAccount account = this.savingsAccountRepository.findOneWithNotFoundDetection(savingsId); - account.setHelpers(this.savingsAccountTransactionSummaryWrapper); + account.setHelpers(this.savingsAccountTransactionSummaryWrapper, savingsHelper); return account; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountRepositoryWrapper.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountRepositoryWrapper.java index 711ca69a9..c7d651f8d 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountRepositoryWrapper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountRepositoryWrapper.java @@ -25,21 +25,15 @@ import org.springframework.stereotype.Service; public class SavingsAccountRepositoryWrapper { private final SavingsAccountRepository repository; - private final SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper; @Autowired - public SavingsAccountRepositoryWrapper(final SavingsAccountRepository repository, - final SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper) { + public SavingsAccountRepositoryWrapper(final SavingsAccountRepository repository) { this.repository = repository; - this.savingsAccountTransactionSummaryWrapper = savingsAccountTransactionSummaryWrapper; } public SavingsAccount findOneWithNotFoundDetection(final Long savingsId) { final SavingsAccount account = this.repository.findOne(savingsId); if (account == null) { throw new SavingsAccountNotFoundException(savingsId); } - - account.setHelpers(this.savingsAccountTransactionSummaryWrapper); - return account; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountTransaction.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountTransaction.java index 687014b9c..d133d76b1 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountTransaction.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountTransaction.java @@ -325,4 +325,8 @@ public final class SavingsAccountTransaction extends AbstractPersistable { LocalDateInterval balanceInterval = LocalDateInterval.create(getTransactionLocalDate(), getEndOfBalanceLocalDate()); return balanceInterval.containsPortionOf(periodInterval); } + + public boolean isIdentifiedBy(final Long transactionId) { + return this.getId().equals(transactionId); + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountTransactionRepository.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountTransactionRepository.java index edd2f8a58..745814a4c 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountTransactionRepository.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountTransactionRepository.java @@ -11,4 +11,6 @@ import org.springframework.data.jpa.repository.JpaSpecificationExecutor; public interface SavingsAccountTransactionRepository extends JpaRepository, JpaSpecificationExecutor { + SavingsAccountTransaction findOneByIdAndSavingsAccountId(Long transactionId, Long savingsId); + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/handler/UndoTransactionSavingsAccountCommandHandler.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/handler/UndoTransactionSavingsAccountCommandHandler.java new file mode 100644 index 000000000..a942e2be5 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/handler/UndoTransactionSavingsAccountCommandHandler.java @@ -0,0 +1,32 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.savings.handler; + +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 UndoTransactionSavingsAccountCommandHandler implements NewCommandSourceHandler { + + private final SavingsAccountWritePlatformService writePlatformService; + + @Autowired + public UndoTransactionSavingsAccountCommandHandler(final SavingsAccountWritePlatformService writePlatformService) { + this.writePlatformService = writePlatformService; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + final Long transactionId = Long.valueOf(command.getTransactionId()); + return this.writePlatformService.undoTransaction(command.getSavingsId(), transactionId); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java index 481794968..b54dff26d 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java @@ -489,8 +489,7 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead @Override public Collection retrieveAllTransactions(final Long savingsId) { - final String sql = "select " + this.transactionsMapper.schema() - + " where sa.id = ? and tr.is_reversed=0 order by tr.transaction_date DESC, tr.id DESC"; + final String sql = "select " + this.transactionsMapper.schema() + " where sa.id = ? order by tr.transaction_date DESC, tr.id DESC"; return this.jdbcTemplate.query(sql, this.transactionsMapper, new Object[] { savingsId }); } @@ -504,7 +503,8 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead final StringBuilder sqlBuilder = new StringBuilder(400); sqlBuilder.append("tr.id as transactionId, tr.transaction_type_enum as transactionType, "); sqlBuilder.append("tr.transaction_date as transactionDate, tr.amount as transactionAmount,"); - sqlBuilder.append("sa.id as savingsId, sa.account_no as accountNo, "); + sqlBuilder.append("tr.running_balance_derived as runningBalance, tr.is_reversed as reversed,"); + sqlBuilder.append("sa.id as savingsId, sa.account_no as accountNo,"); sqlBuilder.append("pd.payment_type_cv_id as paymentType,pd.account_number as accountNumber,pd.check_number as checkNumber, "); sqlBuilder.append("pd.receipt_number as receiptNumber, pd.bank_number as bankNumber,pd.routing_code as routingCode, "); sqlBuilder.append("sa.currency_code as currencyCode, sa.currency_digits as currencyDigits, "); @@ -514,7 +514,7 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead sqlBuilder.append("from m_savings_account sa "); sqlBuilder.append("join m_savings_account_transaction tr on tr.savings_account_id = sa.id "); sqlBuilder.append("join m_currency curr on curr.code = sa.currency_code "); - sqlBuilder.append("left JOIN m_payment_detail pd ON tr.payment_detail_id = pd.id "); + sqlBuilder.append("left join m_payment_detail pd on tr.payment_detail_id = pd.id "); sqlBuilder.append("left join m_code_value cv on pd.payment_type_cv_id = cv.id "); this.schemaSql = sqlBuilder.toString(); @@ -532,6 +532,8 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead final LocalDate date = JdbcSupport.getLocalDate(rs, "transactionDate"); final BigDecimal amount = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "transactionAmount"); + final BigDecimal runningBalance = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "runningBalance"); + final boolean reversed = rs.getBoolean("reversed"); final Long savingsId = rs.getLong("savingsId"); final String accountNo = rs.getString("accountNo"); @@ -561,7 +563,7 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead currencyNameCode); return SavingsAccountTransactionData.create(id, transactionType, paymentDetailData, savingsId, accountNo, date, currency, - amount); + amount, runningBalance, reversed); } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountWritePlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountWritePlatformService.java index d6b3b50d6..fc45e279c 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountWritePlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountWritePlatformService.java @@ -21,4 +21,6 @@ public interface SavingsAccountWritePlatformService { CommandProcessingResult calculateInterest(Long savingsId); CommandProcessingResult postInterest(Long savingsId); + + CommandProcessingResult undoTransaction(Long savingsId, Long transactionId); } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java index dac0e508b..2b973dd7d 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java @@ -34,7 +34,6 @@ import org.mifosplatform.portfolio.savings.domain.SavingsAccountAssembler; import org.mifosplatform.portfolio.savings.domain.SavingsAccountRepositoryWrapper; import org.mifosplatform.portfolio.savings.domain.SavingsAccountTransaction; import org.mifosplatform.portfolio.savings.domain.SavingsAccountTransactionRepository; -import org.mifosplatform.portfolio.savings.domain.SavingsHelper; import org.mifosplatform.useradministration.domain.AppUser; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -52,8 +51,6 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper; private final JournalEntryWritePlatformService journalEntryWritePlatformService; - private final SavingsHelper savingsHelper = new SavingsHelper(); - @Autowired public SavingsAccountWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context, final SavingsAccountRepositoryWrapper savingAccountRepository, @@ -131,10 +128,10 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi if (account.isBeforeLastPostingPeriod(transactionDate)) { final LocalDate today = DateUtils.getLocalDateOfTenant(); - account.postInterest(this.savingsHelper, mc, today, existingTransactionIds, existingReversedTransactionIds); + account.postInterest(mc, today, existingTransactionIds, existingReversedTransactionIds); } else { final LocalDate today = DateUtils.getLocalDateOfTenant(); - account.calculateInterestUsing(this.savingsHelper, mc, today); + account.calculateInterestUsing(mc, today); } final Long transactionId = saveTransactionToGenerateTransactionId(deposit); @@ -185,10 +182,10 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi if (account.isBeforeLastPostingPeriod(transactionDate)) { final LocalDate today = DateUtils.getLocalDateOfTenant(); - account.postInterest(this.savingsHelper, mc, today, existingTransactionIds, existingReversedTransactionIds); + account.postInterest(mc, today, existingTransactionIds, existingReversedTransactionIds); } else { final LocalDate today = DateUtils.getLocalDateOfTenant(); - account.calculateInterestUsing(this.savingsHelper, mc, today); + account.calculateInterestUsing(mc, today); } final Long transactionId = saveTransactionToGenerateTransactionId(withdrawal); @@ -220,7 +217,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi final List existingReversedTransactionIds = new ArrayList(); final MathContext mc = new MathContext(10, RoundingMode.HALF_EVEN); - final SavingsAccountTransaction annualFee = account.addAnnualFee(this.savingsHelper, mc, fmt, transactionDate, + final SavingsAccountTransaction annualFee = account.addAnnualFee(mc, fmt, transactionDate, existingTransactionIds, existingReversedTransactionIds); final Long transactionId = saveTransactionToGenerateTransactionId(annualFee); this.savingAccountRepository.save(account); @@ -239,15 +236,13 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi @Transactional @Override public CommandProcessingResult calculateInterest(final Long savingsId) { - this.context.authenticatedUser(); final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId); final LocalDate today = DateUtils.getLocalDateOfTenant(); - final MathContext mc = new MathContext(15, RoundingMode.HALF_EVEN); - account.calculateInterestUsing(this.savingsHelper, mc, today); + account.calculateInterestUsing(mc, today); this.savingAccountRepository.save(account); @@ -263,7 +258,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi @Transactional @Override public CommandProcessingResult postInterest(final Long savingsId) { - this.context.authenticatedUser(); + final List existingTransactionIds = new ArrayList(); final List existingReversedTransactionIds = new ArrayList(); @@ -272,7 +267,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi final LocalDate today = DateUtils.getLocalDateOfTenant(); final MathContext mc = new MathContext(10, RoundingMode.HALF_EVEN); - account.postInterest(this.savingsHelper, mc, today, existingTransactionIds, existingReversedTransactionIds); + account.postInterest(mc, today, existingTransactionIds, existingReversedTransactionIds); this.savingAccountRepository.save(account); postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); @@ -286,6 +281,35 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi .build(); } + @Override + public CommandProcessingResult undoTransaction(final Long savingsId, final Long transactionId) { + + final List newTransactionIds = new ArrayList(); + final List reversedTransactionIds = new ArrayList(); + + SavingsAccountTransaction savingsAccountTransaction = this.savingsAccountTransactionRepository.findOneByIdAndSavingsAccountId( + transactionId, savingsId); + if (savingsAccountTransaction == null) { + // throw new not found exception + } + + final LocalDate today = DateUtils.getLocalDateOfTenant(); + final MathContext mc = new MathContext(15, RoundingMode.HALF_EVEN); + + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId); + account.undoTransaction(transactionId, reversedTransactionIds, mc, today); + + postJournalEntries(account, newTransactionIds, reversedTransactionIds); + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .build(); + } + private void postJournalEntries(final SavingsAccount savingsAccount, final List existingTransactionIds, final List existingReversedTransactionIds) { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java index 213580cd3..e20e89165 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java @@ -169,7 +169,7 @@ public class SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl impl final Map changes = new LinkedHashMap(20); - final SavingsAccount account = this.savingAccountRepository.findOneWithNotFoundDetection(savingsId); + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId); account.modifyApplication(command, changes); account.validateNewApplicationState(DateUtils.getLocalDateOfTenant()); @@ -236,7 +236,7 @@ public class SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl impl @Override public CommandProcessingResult deleteApplication(final Long savingsId) { - final SavingsAccount account = this.savingAccountRepository.findOneWithNotFoundDetection(savingsId); + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId); if (account.isNotSubmittedAndPendingApproval()) { final List dataValidationErrors = new ArrayList();