FINERACT-1510-reversal-transaction-posting (#2046)

This commit is contained in:
Manoj
2022-02-23 00:01:16 +05:30
committed by GitHub
parent 8389106a58
commit d7d9a831ec
17 changed files with 384 additions and 9 deletions
@@ -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";
@@ -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";
@@ -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);
@@ -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(
@@ -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<>();
@@ -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);
}
@@ -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;
}
}
@@ -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:
@@ -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;
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
@@ -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) {
@@ -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>
@@ -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>
@@ -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");
}
}
@@ -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");