You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fineract.apache.org by av...@apache.org on 2022/02/22 18:33:28 UTC

[fineract] branch develop updated: FINERACT-1510-reversal-transaction-posting (#2046)

This is an automated email from the ASF dual-hosted git repository.

avikg pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git


The following commit(s) were added to refs/heads/develop by this push:
     new d7d9a83  FINERACT-1510-reversal-transaction-posting (#2046)
d7d9a83 is described below

commit d7d9a831eccbc1950c17ab430b4b89539f35286b
Author: Manoj <56...@users.noreply.github.com>
AuthorDate: Wed Feb 23 00:01:16 2022 +0530

    FINERACT-1510-reversal-transaction-posting (#2046)
---
 .../commands/service/CommandWrapperBuilder.java    | 11 +++++
 .../portfolio/savings/SavingsApiConstants.java     |  1 +
 .../api/SavingsAccountTransactionsApiResource.java |  8 +++-
 .../data/SavingsAccountTransactionData.java        | 28 +++++++++--
 .../portfolio/savings/domain/SavingsAccount.java   | 47 ++++++++++++++++++
 .../domain/SavingsAccountDomainService.java        |  3 ++
 .../domain/SavingsAccountDomainServiceJpa.java     | 51 ++++++++++++++++++++
 .../savings/domain/SavingsAccountSummary.java      |  2 +-
 .../savings/domain/SavingsAccountTransaction.java  | 23 +++++++++
 ...rseTransactionSavingsAccountCommandHandler.java | 50 ++++++++++++++++++++
 .../SavingsAccountReadPlatformServiceImpl.java     |  5 +-
 .../SavingsAccountWritePlatformService.java        |  2 +
 ...countWritePlatformServiceJpaRepositoryImpl.java | 43 +++++++++++++++++
 .../db/changelog/tenant/changelog-tenant.xml       |  1 +
 .../parts/0005_savings_transaction_reversal.xml    | 49 +++++++++++++++++++
 .../ClientSavingsIntegrationTest.java              | 55 ++++++++++++++++++++++
 .../common/savings/SavingsAccountHelper.java       | 14 ++++++
 17 files changed, 384 insertions(+), 9 deletions(-)

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 ee6ddc9..b21dfa8 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 f02ad81..08086dc 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 19f421d..79a767c 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 3ff8c06..acf5fd9 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<SavingsAccountChargesPaidByData> 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<PaymentTypeData> 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<PaymentTypeData> 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<PaymentTypeData> 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 0243943..3bf5809 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<SavingsAccountChargePaidBy> 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<SavingsAccountTransaction> 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<String, Object> rejectApplication(final AppUser currentUser, final JsonCommand command, final LocalDate tenantsTodayDate) {
 
         final Map<String, Object> 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 8b65685..ae11246 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 8b78489..6612b4e 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<SavingsAccountChargePaidBy> chargePaidBySet = savingsAccountTransaction.getSavingsAccountChargesPaid();
+
+        final Set<Long> existingTransactionIds = new HashSet<>();
+        final Set<Long> 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<SavingsAccountTransaction> 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 67f1913..92c358e 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<SavingsAccountTransaction> 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 c190ef5..241253f 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<Note> 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 0000000..1932791
--- /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 1e5fca9..62db2e9 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 134460e..2036b53 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 eac3a8a..93c83ef 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
@@ -680,6 +680,49 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
     }
 
     @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 0248acf..1afbbe2 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">
     <include file="parts/0003_postgresql_specific_initial_data.xml" relativeToChangelogFile="true"/>
     <include file="parts/0004_camelcase_column_renaming.xml" relativeToChangelogFile="true"/>
+    <include file="parts/0005_savings_transaction_reversal.xml" relativeToChangelogFile="true"/>
 </databaseChangeLog>
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 0000000..b54e66d
--- /dev/null
+++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0005_savings_transaction_reversal.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    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.
+
+-->
+<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
+                   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
+    <changeSet author="fineract" id="1">
+        <addColumn tableName="m_savings_account_transaction">
+            <column name="original_transaction_id" type="BIGINT"/>
+            <column name="is_reversal" type="TINYINT" defaultValueNumeric="0">
+                <constraints nullable="false"/>
+            </column>
+        </addColumn>
+    </changeSet>
+    <changeSet author="fineract" id="2">
+        <insert tableName="m_permission">
+            <column name="grouping" value="transaction_savings"/>
+            <column name="code" value="REVERSETRANSACTION_SAVINGSACCOUNT"/>
+            <column name="entity_name" value="SAVINGSACCOUNT"/>
+            <column name="action_name" value="REVERSETRANSACTION"/>
+            <column name="can_maker_checker" valueBoolean="false"/>
+        </insert>
+        <insert tableName="m_permission">
+            <column name="grouping" value="transaction_savings"/>
+            <column name="code" value="REVERSETRANSACTION_SAVINGSACCOUNT_CHECKER"/>
+            <column name="entity_name" value="SAVINGSACCOUNT"/>
+            <column name="action_name" value="REVERSETRANSACTION_CHECKER"/>
+            <column name="can_maker_checker" valueBoolean="false"/>
+        </insert>
+    </changeSet>
+</databaseChangeLog>
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 96802a3..8846582 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<HashMap> 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 a2b36dd..733cf33 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<HashMap> getSavingsTransactions(final Integer savingsID) {
+        final Object get = getSavingsCollectionAttribute(savingsID, "transactions");
+        final String json = new Gson().toJson(get);
+        return new Gson().fromJson(json, new TypeToken<ArrayList<HashMap>>() {}.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");