FINERACT-1510-reversal-transaction-posting (#2046)
This commit is contained in:
+11
@@ -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";
|
||||
|
||||
+1
@@ -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";
|
||||
|
||||
+6
-2
@@ -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);
|
||||
|
||||
+23
-5
@@ -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(
|
||||
|
||||
+47
@@ -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<>();
|
||||
|
||||
+3
@@ -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);
|
||||
}
|
||||
|
||||
+51
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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:
|
||||
|
||||
+23
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+50
@@ -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);
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -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);
|
||||
|
||||
+43
@@ -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>
|
||||
|
||||
+49
@@ -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>
|
||||
+55
@@ -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");
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+14
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user