diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index ee6ddc948..b21dfa86b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -1466,6 +1466,17 @@ public class CommandWrapperBuilder { return this; } + public CommandWrapperBuilder reverseSavingsAccountTransaction(final Long accountId, final Long transactionId) { + this.actionName = "REVERSETRANSACTION"; + this.entityName = "SAVINGSACCOUNT"; + this.savingsId = accountId; + this.entityId = accountId; + this.subentityId = transactionId; + this.transactionId = transactionId.toString(); + this.href = "/savingsaccounts/" + accountId + "/transactions/" + transactionId + "?command=reverse"; + return this; + } + public CommandWrapperBuilder adjustSavingsAccountTransaction(final Long accountId, final Long transactionId) { this.actionName = "ADJUSTTRANSACTION"; this.entityName = "SAVINGSACCOUNT"; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/SavingsApiConstants.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/SavingsApiConstants.java index f02ad81cd..08086dcf4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/SavingsApiConstants.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/SavingsApiConstants.java @@ -52,6 +52,7 @@ public class SavingsApiConstants { // command public static final String COMMAND_UNDO_TRANSACTION = "undo"; + public static final String COMMAND_REVERSE_TRANSACTION = "reverse"; public static final String COMMAND_ADJUST_TRANSACTION = "modify"; public static final String COMMAND_WAIVE_CHARGE = "waive"; public static final String COMMAND_PAY_CHARGE = "paycharge"; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResource.java index 19f421da6..79a767caa 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResource.java @@ -188,6 +188,9 @@ public class SavingsAccountTransactionsApiResource { if (is(commandParam, SavingsApiConstants.COMMAND_UNDO_TRANSACTION)) { final CommandWrapper commandRequest = builder.undoSavingsAccountTransaction(savingsId, transactionId).build(); result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, SavingsApiConstants.COMMAND_REVERSE_TRANSACTION)) { + final CommandWrapper commandRequest = builder.reverseSavingsAccountTransaction(savingsId, transactionId).build(); + result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); } else if (is(commandParam, SavingsApiConstants.COMMAND_ADJUST_TRANSACTION)) { final CommandWrapper commandRequest = builder.adjustSavingsAccountTransaction(savingsId, transactionId).build(); result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); @@ -198,8 +201,9 @@ public class SavingsAccountTransactionsApiResource { if (result == null) { // - throw new UnrecognizedQueryParamException("command", commandParam, new Object[] { SavingsApiConstants.COMMAND_UNDO_TRANSACTION, - SavingsApiConstants.COMMAND_ADJUST_TRANSACTION, SavingsApiConstants.COMMAND_RELEASE_AMOUNT }); + throw new UnrecognizedQueryParamException("command", commandParam, + new Object[] { SavingsApiConstants.COMMAND_UNDO_TRANSACTION, SavingsApiConstants.COMMAND_ADJUST_TRANSACTION, + SavingsApiConstants.COMMAND_RELEASE_AMOUNT, SavingsApiConstants.COMMAND_REVERSE_TRANSACTION }); } return this.toApiJsonSerializer.serialize(result); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionData.java index 3ff8c0692..acf5fd928 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionData.java @@ -71,6 +71,8 @@ public final class SavingsAccountTransactionData implements Serializable { private final String submittedByUsername; private final String note; private final boolean isManualTransaction; + private final Boolean isReversal; + private final Long originalTransactionId; private Set chargesPaidByData = new HashSet<>(); // templates @@ -343,6 +345,8 @@ public final class SavingsAccountTransactionData implements Serializable { this.paymentTypeOptions = null; this.submittedByUsername = null; this.note = null; + this.isReversal = null; + this.originalTransactionId = null; } public boolean isChargeTransaction() { @@ -538,6 +542,9 @@ public final class SavingsAccountTransactionData implements Serializable { this.submittedByUsername = null; this.note = null; this.isManualTransaction = isManualTransaction; + this.isReversal = null; + this.originalTransactionId = null; + } private SavingsAccountTransactionData(BigDecimal transactionAmount, LocalDate transactionDate, Long paymentTypeId, String accountNumber, @@ -573,6 +580,8 @@ public final class SavingsAccountTransactionData implements Serializable { this.submittedByUsername = null; this.note = null; this.isManualTransaction = false; + this.isReversal = null; + this.originalTransactionId = null; } private SavingsAccountTransactionData(Integer id, BigDecimal transactionAmount, LocalDate transactionDate, Long paymentTypeId, @@ -610,6 +619,8 @@ public final class SavingsAccountTransactionData implements Serializable { this.cumulativeBalance = cumulativeBalance; this.transfer = null; this.isManualTransaction = false; + this.isReversal = null; + this.originalTransactionId = null; } public Integer getRowIndex() { @@ -740,6 +751,8 @@ public final class SavingsAccountTransactionData implements Serializable { this.note = null; this.balanceEndDate = balanceEndDate; this.isManualTransaction = false; + this.isReversal = null; + this.originalTransactionId = null; } public static SavingsAccountTransactionData create(final Long id, final SavingsAccountTransactionEnumData transactionType, @@ -757,17 +770,17 @@ public final class SavingsAccountTransactionData implements Serializable { final PaymentDetailData paymentDetailData, final Long savingsId, final String savingsAccountNo, final LocalDate date, final CurrencyData currency, final BigDecimal amount, final BigDecimal outstandingChargeAmount, final BigDecimal runningBalance, final boolean reversed, final AccountTransferData transfer, final LocalDate submittedOnDate, final boolean interestedPostedAsOn, - final String submittedByUsername, final String note) { + final String submittedByUsername, final String note, final Boolean isReversal, final Long originalTransactionId) { final Collection paymentTypeOptions = null; return new SavingsAccountTransactionData(id, transactionType, paymentDetailData, savingsId, savingsAccountNo, date, currency, amount, outstandingChargeAmount, runningBalance, reversed, transfer, paymentTypeOptions, submittedOnDate, - interestedPostedAsOn, submittedByUsername, note); + interestedPostedAsOn, submittedByUsername, note, isReversal, originalTransactionId); } public static SavingsAccountTransactionData create(final Long id) { final Collection paymentTypeOptions = null; return new SavingsAccountTransactionData(id, null, null, null, null, null, null, null, null, null, false, null, paymentTypeOptions, - null, false, null, null); + null, false, null, null, null, null); } public static SavingsAccountTransactionData template(final Long savingsId, final String savingsAccountNo, @@ -806,14 +819,15 @@ public final class SavingsAccountTransactionData implements Serializable { final boolean interestedPostedAsOn, final String submittedByUsername, final String note) { this(id, transactionType, paymentDetailData, savingsId, savingsAccountNo, date, currency, amount, outstandingChargeAmount, - runningBalance, reversed, transfer, paymentTypeOptions, date, interestedPostedAsOn, submittedByUsername, note); + runningBalance, reversed, transfer, paymentTypeOptions, date, interestedPostedAsOn, submittedByUsername, note, null, null); } 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 BigDecimal outstandingChargeAmount, final BigDecimal runningBalance, final boolean reversed, final AccountTransferData transfer, final Collection paymentTypeOptions, - final LocalDate submittedOnDate, final boolean interestedPostedAsOn, final String submittedByUsername, final String note) { + final LocalDate submittedOnDate, final boolean interestedPostedAsOn, final String submittedByUsername, final String note, + final Boolean isReversal, final Long originalTransactionId) { this.id = id; this.transactionType = transactionType; this.paymentDetailData = paymentDetailData; @@ -837,6 +851,8 @@ public final class SavingsAccountTransactionData implements Serializable { this.submittedByUsername = submittedByUsername; this.note = note; this.isManualTransaction = false; + this.isReversal = isReversal; + this.originalTransactionId = originalTransactionId; } private SavingsAccountTransactionData(final Long id, final SavingsAccountTransactionEnumData transactionType, @@ -868,6 +884,8 @@ public final class SavingsAccountTransactionData implements Serializable { this.note = null; this.cumulativeBalance = cumulativeBalance; this.isManualTransaction = false; + this.isReversal = null; + this.originalTransactionId = null; } public static SavingsAccountTransactionData withWithDrawalTransactionDetails( diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java index 024394306..3bf5809f7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java @@ -2378,6 +2378,29 @@ public class SavingsAccount extends AbstractPersistableCustom { } } + public void undoTransaction(final SavingsAccountTransaction transactionToUndo) { + + if (transactionToUndo.isReversed()) { + throw new SavingsAccountTransactionNotFoundException(this.getId(), transactionToUndo.getId()); + } + + validateAttemptToUndoTransferRelatedTransactions(transactionToUndo); + validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_UNDO_TRANSACTION, transactionToUndo.transactionLocalDate()); + transactionToUndo.reverse(); + if (transactionToUndo.isChargeTransaction() || transactionToUndo.isWaiveCharge()) { + // undo charge + final Set chargesPaidBy = transactionToUndo.getSavingsAccountChargesPaid(); + for (final SavingsAccountChargePaidBy savingsAccountChargePaidBy : chargesPaidBy) { + final SavingsAccountCharge chargeToUndo = savingsAccountChargePaidBy.getSavingsAccountCharge(); + if (transactionToUndo.isChargeTransaction()) { + chargeToUndo.undoPayment(this.getCurrency(), transactionToUndo.getAmount(this.getCurrency())); + } else if (transactionToUndo.isWaiveCharge()) { + chargeToUndo.undoWaiver(this.getCurrency(), transactionToUndo.getAmount(this.getCurrency())); + } + } + } + } + private Date findLatestAnnualFeeTransactionDueDate() { Date nextDueDate = null; @@ -2400,6 +2423,30 @@ public class SavingsAccount extends AbstractPersistableCustom { return nextDueDate; } + public void validateAccountBalanceDoesNotBecomeNegativeMinimal(final BigDecimal transactionAmount, final boolean isException) { + // final List transactionsSortedByDate = retreiveListOfTransactions(); + Money runningBalance = this.summary.getAccountBalance(getCurrency()); + Money minRequiredBalance = minRequiredBalanceDerived(getCurrency()); + org.joda.time.LocalDate lastSavingsDate = null; + final BigDecimal withdrawalFee = null; + + // check last txn date + + // In overdraft cases, minRequiredBalance can be in violation after interest posting + // and should be checked after processing all transactions + if (!isOverdraft()) { + if (runningBalance.minus(minRequiredBalance).isLessThanZero()) { + throw new InsufficientAccountBalanceException("transactionAmount", getAccountBalance(), withdrawalFee, transactionAmount); + } + } + + if (this.getSavingsHoldAmount().compareTo(BigDecimal.ZERO) > 0) { + if (runningBalance.minus(this.getSavingsHoldAmount()).minus(minRequiredBalance).isLessThanZero()) { + throw new InsufficientAccountBalanceException("transactionAmount", getAccountBalance(), withdrawalFee, transactionAmount); + } + } + } + public Map rejectApplication(final AppUser currentUser, final JsonCommand command, final LocalDate tenantsTodayDate) { final Map actualChanges = new LinkedHashMap<>(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountDomainService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountDomainService.java index 8b656858d..ae1124690 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountDomainService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountDomainService.java @@ -40,4 +40,7 @@ public interface SavingsAccountDomainService { SavingsAccountTransaction handleDividendPayout(SavingsAccount account, LocalDate transactionDate, BigDecimal transactionAmount, boolean backdatedTxnsAllowedTill); + + SavingsAccountTransaction handleReversal(SavingsAccount account, SavingsAccountTransaction savingsAccountTransaction, + boolean backdatedTxnsAllowedTill); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountDomainServiceJpa.java index 8b784894f..6612b4e50 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountDomainServiceJpa.java @@ -22,6 +22,7 @@ import java.math.BigDecimal; import java.math.MathContext; import java.time.LocalDate; import java.time.format.DateTimeFormatter; +import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -35,6 +36,7 @@ import org.apache.fineract.infrastructure.security.service.PlatformSecurityConte import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency; import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.domain.MoneyHelper; import org.apache.fineract.portfolio.common.BusinessEventNotificationConstants.BusinessEntity; import org.apache.fineract.portfolio.common.BusinessEventNotificationConstants.BusinessEvents; import org.apache.fineract.portfolio.common.service.BusinessEventNotifierService; @@ -275,4 +277,53 @@ public class SavingsAccountDomainServiceJpa implements SavingsAccountDomainServi map.put(entityEvent, entity); return map; } + + @Override + public SavingsAccountTransaction handleReversal(SavingsAccount account, SavingsAccountTransaction savingsAccountTransaction, + boolean backdatedTxnsAllowedTill) { + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + + final Set chargePaidBySet = savingsAccountTransaction.getSavingsAccountChargesPaid(); + + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + + if (backdatedTxnsAllowedTill) { + updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds); + } else { + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + } + + SavingsAccountTransaction reversal = SavingsAccountTransaction.reversal(savingsAccountTransaction); + reversal.getSavingsAccountChargesPaid().addAll(chargePaidBySet); + account.undoTransaction(savingsAccountTransaction); + + boolean isInterestTransfer = false; + LocalDate postInterestOnDate = null; + final LocalDate today = DateUtils.getLocalDateOfTenant(); + final MathContext mc = new MathContext(15, MoneyHelper.getRoundingMode()); + + if (savingsAccountTransaction.isPostInterestCalculationRequired() + && account.isBeforeLastPostingPeriod(savingsAccountTransaction.transactionLocalDate(), backdatedTxnsAllowedTill)) { + account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, + postInterestOnDate, backdatedTxnsAllowedTill); + } else { + account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill); + } + + List newTransactions = new ArrayList<>(); + newTransactions.add(reversal); + + account.validateAccountBalanceDoesNotBecomeNegativeMinimal(savingsAccountTransaction.getAmount(), false); + account.activateAccountBasedOnBalance(); + this.savingsAccountRepository.save(account); + this.savingsAccountTransactionRepository.saveAll(newTransactions); + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false, backdatedTxnsAllowedTill); + + return reversal; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountSummary.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountSummary.java index 67f191322..92c358e43 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountSummary.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountSummary.java @@ -123,7 +123,7 @@ public final class SavingsAccountSummary { public void updateSummaryWithPivotConfig(final MonetaryCurrency currency, final SavingsAccountTransactionSummaryWrapper wrapper, final SavingsAccountTransaction transaction, final List savingsAccountTransactions) { - if (transaction != null) { + if (transaction != null && !transaction.isReversalTransaction()) { Money transactionAmount = Money.of(currency, transaction.getAmount()); switch (SavingsAccountTransactionType.fromInt(transaction.getTypeOf())) { case DEPOSIT: diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransaction.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransaction.java index c190ef528..241253f3a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransaction.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransaction.java @@ -132,6 +132,12 @@ public final class SavingsAccountTransaction extends AbstractPersistableCustom { @JoinColumn(name = "savings_account_transaction_id", referencedColumnName = "id") private List notes = new ArrayList<>(); + @Column(name = "is_reversal", nullable = false) + private boolean reversalTransaction; + + @Column(name = "original_transaction_id") + private Long originalTxnId; + SavingsAccountTransaction() { this.dateOf = null; this.typeOf = null; @@ -342,6 +348,15 @@ public final class SavingsAccountTransaction extends AbstractPersistableCustom { accountTransaction.reversed, appUser, accountTransaction.isManualTransaction); } + public static SavingsAccountTransaction reversal(SavingsAccountTransaction accountTransaction) { + SavingsAccountTransaction sat = copyTransaction(accountTransaction); + sat.reversed = false; + sat.setReversalTransaction(true); + sat.originalTxnId = accountTransaction.getId(); + return sat; + + } + public LocalDate transactionLocalDate() { return LocalDate.ofInstant(this.dateOf.toInstant(), DateUtils.getDateTimeZoneOfTenant()); } @@ -863,4 +878,12 @@ public final class SavingsAccountTransaction extends AbstractPersistableCustom { public boolean getIsManualTransaction() { return this.isManualTransaction; } + + public void setReversalTransaction(boolean reversalTransaction) { + this.reversalTransaction = reversalTransaction; + } + + public boolean isReversalTransaction() { + return reversalTransaction; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/ReverseTransactionSavingsAccountCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/ReverseTransactionSavingsAccountCommandHandler.java new file mode 100644 index 000000000..1932791ee --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/ReverseTransactionSavingsAccountCommandHandler.java @@ -0,0 +1,50 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.savings.handler; + +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author manoj + */ +@Service +@CommandType(entity = "SAVINGSACCOUNT", action = "REVERSETRANSACTION") +public class ReverseTransactionSavingsAccountCommandHandler implements NewCommandSourceHandler { + + private final SavingsAccountWritePlatformService writePlatformService; + + @Autowired + public ReverseTransactionSavingsAccountCommandHandler(final SavingsAccountWritePlatformService writePlatformService) { + this.writePlatformService = writePlatformService; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + final Long transactionId = Long.valueOf(command.getTransactionId()); + return this.writePlatformService.reverseTransaction(command.getSavingsId(), transactionId, false); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java index 1e5fca901..62db2e9de 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java @@ -1317,6 +1317,7 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead sqlBuilder.append(" au.username as submittedByUsername, "); sqlBuilder.append(" nt.note as transactionNote, "); sqlBuilder.append("tr.running_balance_derived as runningBalance, tr.is_reversed as reversed,"); + sqlBuilder.append("tr.is_reversal as isReversal, tr.original_transaction_id as originalTransactionId,"); sqlBuilder.append("fromtran.id as fromTransferId, fromtran.is_reversed as fromTransferReversed,"); sqlBuilder.append("fromtran.transaction_date as fromTransferDate, fromtran.amount as fromTransferAmount,"); sqlBuilder.append("fromtran.description as fromTransferDescription,"); @@ -1360,6 +1361,8 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead final BigDecimal outstandingChargeAmount = null; final BigDecimal runningBalance = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "runningBalance"); final boolean reversed = rs.getBoolean("reversed"); + final boolean isReversal = rs.getBoolean("isReversal"); + final Long originalTransactionId = rs.getLong("originalTransactionId"); final Long savingsId = rs.getLong("savingsId"); final String accountNo = rs.getString("accountNo"); @@ -1414,7 +1417,7 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead final String note = rs.getString("transactionNote"); return SavingsAccountTransactionData.create(id, transactionType, paymentDetailData, savingsId, accountNo, date, currency, amount, outstandingChargeAmount, runningBalance, reversed, transfer, submittedOnDate, postInterestAsOn, - submittedByUsername, note); + submittedByUsername, note, isReversal, originalTransactionId); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformService.java index 134460eb1..2036b53d1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformService.java @@ -41,6 +41,8 @@ public interface SavingsAccountWritePlatformService { CommandProcessingResult calculateInterest(Long savingsId); + CommandProcessingResult reverseTransaction(Long savingsId, Long transactionId, boolean allowAccountTransferModification); + CommandProcessingResult undoTransaction(Long savingsId, Long transactionId, boolean allowAccountTransferModification); CommandProcessingResult adjustSavingsTransaction(Long savingsId, Long transactionId, JsonCommand command); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java index eac3a8a97..93c83ef05 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java @@ -679,6 +679,49 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi return savingsAccountData; } + @Override + public CommandProcessingResult reverseTransaction(final Long savingsId, final Long transactionId, + final boolean allowAccountTransferModification) { + + final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill); + + final SavingsAccountTransaction savingsAccountTransaction = this.savingsAccountTransactionRepository + .findOneByIdAndSavingsAccountId(transactionId, savingsId); + if (savingsAccountTransaction == null) { + throw new SavingsAccountTransactionNotFoundException(savingsId, transactionId); + } + + if (!allowAccountTransferModification + && this.accountTransfersReadPlatformService.isAccountTransfer(transactionId, PortfolioAccountType.SAVINGS)) { + throw new PlatformServiceUnavailableException("error.msg.saving.account.transfer.transaction.update.not.allowed", + "Savings account transaction:" + transactionId + " update not allowed as it involves in account transfer", + transactionId); + } + + if (!account.allowModify()) { + throw new PlatformServiceUnavailableException("error.msg.saving.account.transaction.update.not.allowed", + "Savings account transaction:" + transactionId + " update not allowed for this savings type", transactionId); + } + + if (account.isNotActive()) { + throwValidationForActiveStatus(SavingsApiConstants.undoTransactionAction); + } + + checkClientOrGroupActive(account); + + final SavingsAccountTransaction reversal = this.savingsAccountDomainService.handleReversal(account, savingsAccountTransaction, + backdatedTxnsAllowedTill); + + return new CommandProcessingResultBuilder() // + .withEntityId(reversal.getId()) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .build(); + } + @Override public CommandProcessingResult undoTransaction(final Long savingsId, final Long transactionId, final boolean allowAccountTransferModification) { diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index 0248acf8d..1afbbe245 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -24,4 +24,5 @@ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd"> + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0005_savings_transaction_reversal.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0005_savings_transaction_reversal.xml new file mode 100644 index 000000000..b54e66d43 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0005_savings_transaction_reversal.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientSavingsIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientSavingsIntegrationTest.java index 96802a353..88465822d 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientSavingsIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientSavingsIntegrationTest.java @@ -2392,4 +2392,59 @@ public class ClientSavingsIntegrationTest { HashMap summaryTwo = this.savingsAccountHelper.getSavingsSummary(savingsId); assertEquals(balanceAfterChargeTwo, summaryTwo.get("accountBalance"), "Verifying Balance after withdrawal charge two "); } + + /** + * Test Transaction reversal feature, here a new reversal transaction is posted when a savings transaction is + * reversed + */ + + @Test + public void testAccountBalanceAfterSavingsTransactionReversalPosting() { + this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec); + + final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec); + ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientID); + ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientID); + // Assertions.assertNotNull(clientID); + final String minBalanceForInterestCalculation = null; + final String minRequiredBalance = "0"; + final String enforceMinRequiredBalance = "false"; + final boolean allowOverdraft = true; + final String MINIMUM_OPENING_BALANCE = "0"; + + final Integer savingsProductID = createSavingsProduct(this.requestSpec, this.responseSpec, MINIMUM_OPENING_BALANCE, + minBalanceForInterestCalculation, minRequiredBalance, enforceMinRequiredBalance, allowOverdraft); + Assertions.assertNotNull(savingsProductID); + + final Integer savingsId = this.savingsAccountHelper.applyForSavingsApplication(clientID, savingsProductID, ACCOUNT_TYPE_INDIVIDUAL); + Assertions.assertNotNull(savingsId); + + HashMap savingsStatusHashMap = this.savingsAccountHelper.approveSavings(savingsId); + SavingsStatusChecker.verifySavingsIsApproved(savingsStatusHashMap); + + savingsStatusHashMap = this.savingsAccountHelper.activateSavings(savingsId); + SavingsStatusChecker.verifySavingsIsActive(savingsStatusHashMap); + + Integer depositTransactionId = (Integer) this.savingsAccountHelper.depositToSavingsAccount(savingsId, "500", + SavingsAccountHelper.TRANSACTION_DATE, CommonConstants.RESPONSE_RESOURCE_ID); + + this.savingsAccountHelper.reverseSavingsAccountTransaction(savingsId, depositTransactionId); + + HashMap reversedDepositTransaction = this.savingsAccountHelper.getSavingsTransaction(savingsId, depositTransactionId); + + Assertions.assertTrue((Boolean) reversedDepositTransaction.get("reversed")); + + List transactions = this.savingsAccountHelper.getSavingsTransactions(savingsId); + + HashMap reversalDepositTransaction = transactions.get(0); + + Assertions.assertTrue((Boolean) reversalDepositTransaction.get("isReversal")); + + HashMap summary = this.savingsAccountHelper.getSavingsSummary(savingsId); + + Float balance = Float.parseFloat("0.0"); + + assertEquals(balance, summary.get("accountBalance"), "Verifying opening Balance is 500"); + + } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java index a2b36ddea..733cf33b7 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java @@ -18,6 +18,7 @@ */ package org.apache.fineract.integrationtests.common.savings; +import com.google.common.reflect.TypeToken; import com.google.gson.Gson; import io.restassured.specification.RequestSpecification; import io.restassured.specification.ResponseSpecification; @@ -69,6 +70,7 @@ public class SavingsAccountHelper { private static final String GSIM_DEPOSIT_SAVINGS_COMMAND = "gsimDeposit"; private static final String MODIFY_TRASACTION_COMMAND = "modify"; private static final String UNDO_TRASACTION_COMMAND = "undo"; + private static final String REVERSE_TRASACTION_COMMAND = "reverse"; private static final String BLOCK_SAVINGS_COMMAND = "block"; private static final String UNBLOCK_SAVINGS_COMMAND = "unblock"; @@ -272,6 +274,12 @@ public class SavingsAccountHelper { getSavingsTransactionJSON("0", LAST_TRANSACTION_DATE), CommonConstants.RESPONSE_RESOURCE_ID); } + public Integer reverseSavingsAccountTransaction(final Integer savingsId, final Integer transactionId) { + LOG.info("\n--------------------------------- REVERSE SAVINGS TRANSACTION --------------------------------"); + return (Integer) performSavingActions(createAdjustTransactionURL(REVERSE_TRASACTION_COMMAND, savingsId, transactionId), + getSavingsTransactionJSON("0", LAST_TRANSACTION_DATE), CommonConstants.RESPONSE_RESOURCE_ID); + } + public void calculateInterestForSavings(final Integer savingsId) { LOG.info("--------------------------------- CALCULATING INTEREST FOR SAVINGS --------------------------------"); performSavingActions(createSavingsCalculateInterestURL(CALCULATE_INTEREST_SAVINGS_COMMAND, savingsId), @@ -598,6 +606,12 @@ public class SavingsAccountHelper { return Utils.performServerGet(requestSpec, responseSpec, URL, ""); } + public List getSavingsTransactions(final Integer savingsID) { + final Object get = getSavingsCollectionAttribute(savingsID, "transactions"); + final String json = new Gson().toJson(get); + return new Gson().fromJson(json, new TypeToken>() {}.getType()); + } + public Object getSavingsInterest(final Integer savingsID) { final String URL = SAVINGS_ACCOUNT_URL + "/" + savingsID + "?associations=summary&" + Utils.TENANT_IDENTIFIER; final HashMap response = Utils.performServerGet(requestSpec, responseSpec, URL, "summary");