Add various delete methods

Add delete methods for transactions and recurring transactions. Also add
 open/close methods for accounts. Add unit tests for all new methods.
 Also JAXB is no longer a provided dependency for build-war.
This commit is contained in:
2019-03-15 20:24:47 +01:00
parent ab7fb15254
commit e9774b3b35
14 changed files with 567 additions and 8 deletions

View File

@@ -166,11 +166,6 @@
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</profile>
</profiles>

View File

@@ -25,7 +25,11 @@ public enum ResponseReason {
INVALID_LAST_OCCURRENCE_FORMAT(HttpStatus.INTERNAL_SERVER_ERROR),
MISSING_RECURRING_TRANSACTION_ID(HttpStatus.INTERNAL_SERVER_ERROR),
INVALID_RECURRING_TRANSACTION_ID(HttpStatus.INTERNAL_SERVER_ERROR),
RECURRING_TRANSACTION_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR);
RECURRING_TRANSACTION_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR),
MISSING_TRANSACTION_ID(HttpStatus.INTERNAL_SERVER_ERROR),
INVALID_TRANSACTION_ID(HttpStatus.INTERNAL_SERVER_ERROR),
TRANSACTION_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR),
ACCOUNT_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR);
private HttpStatus httpStatus;

View File

@@ -53,4 +53,34 @@ public class AccountController {
return responseReason.toResponseEntity();
}
@RequestMapping("closeAccount")
public ResponseEntity closeAccount(String key) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("/accounts/closeAccount got parameters: %s", key));
}
final ResponseReason responseReason = this.accountService.closeAccount(key);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("/accounts/closeAccount returns with %s", responseReason.name()));
}
return responseReason.toResponseEntity();
}
@RequestMapping("openAccount")
public ResponseEntity openAccount(String key) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("/accounts/openAccount got parameters: %s", key));
}
final ResponseReason responseReason = this.accountService.openAccount(key);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("/accounts/openAccount returns with %s", responseReason.name()));
}
return responseReason.toResponseEntity();
}
}

View File

@@ -80,4 +80,23 @@ public class RecurringTransactionController {
return responseReason.toResponseEntity();
}
@RequestMapping("deleteRecurringTransaction")
public ResponseEntity deleteRecurringTransaction(String recurringTransactionId) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String
.format("/recurringTransactions/deleteRecurringTransaction got parameters: %s",
recurringTransactionId));
}
final ResponseReason responseReason = this.recurringTransactionService
.deleteRecurringTransaction(recurringTransactionId);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String
.format("/recurringTransactions/deleteRecurringTransaction returns with %s", responseReason.name()));
}
return responseReason.toResponseEntity();
}
}

View File

@@ -47,4 +47,23 @@ public class TransactionController {
return responseReason.toResponseEntity();
}
@RequestMapping("deleteTransaction")
public ResponseEntity deleteTransaction(String transactionId) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String
.format("/transactions/deleteTransaction got parameters: %s",
transactionId));
}
final ResponseReason responseReason = this.transactionService
.deleteTransaction(transactionId);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String
.format("/transactions/deleteTransaction returns with %s", responseReason.name()));
}
return responseReason.toResponseEntity();
}
}

View File

@@ -1,8 +1,20 @@
package de.financer.model;
import java.util.Arrays;
public enum AccountStatus {
/** Indicates that the account is open for bookings */
OPEN,
/** Indicates that the account is closed and bookings to it are forbidden */
CLOSED
CLOSED;
/**
* This method validates whether the given string represents a valid account status.
*
* @param status to check
* @return whether the given status represents a valid account status
*/
public static boolean isValidType(String status) {
return Arrays.stream(AccountStatus.values()).anyMatch((accountStatus) -> accountStatus.name().equals(status));
}
}

View File

@@ -106,4 +106,40 @@ public class AccountService {
return ResponseReason.OK;
}
@Transactional(propagation = Propagation.REQUIRED)
public ResponseReason closeAccount(String key) {
return setAccountStatus(key, AccountStatus.CLOSED);
}
@Transactional(propagation = Propagation.REQUIRED)
public ResponseReason openAccount(String key) {
return setAccountStatus(key, AccountStatus.OPEN);
}
// Visible for unit tests
/* package */ ResponseReason setAccountStatus(String key, AccountStatus accountStatus) {
if (!StringUtils.startsWith(key, "accounts.")) {
return ResponseReason.INVALID_ACCOUNT_KEY;
}
final Account account = this.accountRepository.findByKey(key);
if (account == null) {
return ResponseReason.ACCOUNT_NOT_FOUND;
}
account.setStatus(accountStatus);
try {
this.accountRepository.save(account);
}
catch (Exception e) {
LOGGER.error(String.format("Could not update account status %s|%s", key, accountStatus.name()), e);
return ResponseReason.UNKNOWN_ERROR;
}
return ResponseReason.OK;
}
}

View File

@@ -387,4 +387,33 @@ public class RecurringTransactionService {
recurringTransaction.getDescription(),
recurringTransaction);
}
@Transactional(propagation = Propagation.REQUIRED)
public ResponseReason deleteRecurringTransaction(String recurringTransactionId) {
ResponseReason response = ResponseReason.OK;
if (recurringTransactionId == null) {
return ResponseReason.MISSING_RECURRING_TRANSACTION_ID;
} else if (!NumberUtils.isCreatable(recurringTransactionId)) {
return ResponseReason.INVALID_RECURRING_TRANSACTION_ID;
}
final Optional<RecurringTransaction> optionalRecurringTransaction = this.recurringTransactionRepository
.findById(Long.valueOf(recurringTransactionId));
if (!optionalRecurringTransaction.isPresent()) {
return ResponseReason.RECURRING_TRANSACTION_NOT_FOUND;
}
try {
this.recurringTransactionRepository.deleteById(Long.valueOf(recurringTransactionId));
}
catch (Exception e) {
LOGGER.error("Could not delete recurring transaction!", e);
response = ResponseReason.UNKNOWN_ERROR;
}
return response;
}
}

View File

@@ -6,6 +6,7 @@ import de.financer.dba.TransactionRepository;
import de.financer.model.Account;
import de.financer.model.RecurringTransaction;
import de.financer.model.Transaction;
import org.apache.commons.lang3.math.NumberUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -17,6 +18,7 @@ import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Collections;
import java.util.Optional;
@Service
public class TransactionService {
@@ -168,4 +170,48 @@ public class TransactionService {
return response;
}
@Transactional(propagation = Propagation.REQUIRED)
public ResponseReason deleteTransaction(String transactionId) {
ResponseReason response = ResponseReason.OK;
if (transactionId == null) {
return ResponseReason.MISSING_TRANSACTION_ID;
} else if (!NumberUtils.isCreatable(transactionId)) {
return ResponseReason.INVALID_TRANSACTION_ID;
}
final Optional<Transaction> optionalTransaction = this.transactionRepository
.findById(Long.valueOf(transactionId));
if (!optionalTransaction.isPresent()) {
return ResponseReason.TRANSACTION_NOT_FOUND;
}
final Transaction transaction = optionalTransaction.get();
final Account fromAccount = transaction.getFromAccount();
final Account toAccount = transaction.getToAccount();
final Long amount = transaction.getAmount();
// Invert the actual multiplier by multiplying with -1
// If we delete a transaction we do the inverse of the original transaction
fromAccount.setCurrentBalance(fromAccount.getCurrentBalance() + (this.ruleService
.getMultiplierFromAccount(fromAccount) * amount * -1));
toAccount.setCurrentBalance(toAccount.getCurrentBalance() + (this.ruleService
.getMultiplierToAccount(toAccount) * amount * -1));
try {
this.transactionRepository.deleteById(Long.valueOf(transactionId));
this.accountService.saveAccount(fromAccount);
this.accountService.saveAccount(toAccount);
}
catch (Exception e) {
LOGGER.error("Could not delete transaction!", e);
response = ResponseReason.UNKNOWN_ERROR;
}
return response;
}
}

View File

@@ -43,7 +43,7 @@ public class RecurringTransactionService_getAllIntegrationTest {
final List<RecurringTransaction> allRecurringTransactions = this.objectMapper
.readValue(mvcResult.getResponse().getContentAsByteArray(), new TypeReference<List<RecurringTransaction>>() {});
Assert.assertEquals(2, allRecurringTransactions.size());
Assert.assertEquals(3, allRecurringTransactions.size());
}
}

View File

@@ -0,0 +1,72 @@
package de.financer.service;
import de.financer.ResponseReason;
import de.financer.dba.AccountRepository;
import de.financer.model.Account;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class AccountService_createAccountTest {
@InjectMocks
private AccountService classUnderTest;
@Mock
private AccountRepository accountRepository;
@Test
public void test_createAccount_INVALID_ACCOUNT_TYPE() {
// Arrange
// Nothing to do
// Act
ResponseReason response = this.classUnderTest.createAccount(null, null);
// Assert
Assert.assertEquals(ResponseReason.INVALID_ACCOUNT_TYPE, response);
}
@Test
public void test_createAccount_INVALID_ACCOUNT_KEY() {
// Arrange
// Nothing to do
// Act
ResponseReason response = this.classUnderTest.createAccount(null, "BANK");
// Assert
Assert.assertEquals(ResponseReason.INVALID_ACCOUNT_KEY, response);
}
@Test
public void test_createAccount_UNKNOWN_ERROR() {
// Arrange
Mockito.doThrow(new NullPointerException()).when(this.accountRepository).save(Mockito.any(Account.class));
// Act
ResponseReason response = this.classUnderTest.createAccount("accounts.test", "BANK");
// Assert
Assert.assertEquals(ResponseReason.UNKNOWN_ERROR, response);
}
@Test
public void test_createAccount_OK() {
// Arrange
// Nothing to do
// Act
ResponseReason response = this.classUnderTest.createAccount("accounts.test", "BANK");
// Assert
Assert.assertEquals(ResponseReason.OK, response);
Mockito.verify(this.accountRepository, Mockito.times(1))
.save(ArgumentMatchers.argThat((acc) -> "accounts.test".equals(acc.getKey())));
}
}

View File

@@ -0,0 +1,74 @@
package de.financer.service;
import de.financer.ResponseReason;
import de.financer.dba.AccountRepository;
import de.financer.model.Account;
import de.financer.model.AccountStatus;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class AccountService_setAccountStatusTest {
@InjectMocks
private AccountService classUnderTest;
@Mock
private AccountRepository accountRepository;
@Test
public void test_setAccountStatus_INVALID_ACCOUNT_KEY() {
// Arrange
// Nothing to do
// Act
ResponseReason response = this.classUnderTest.setAccountStatus(null, AccountStatus.CLOSED);
// Assert
Assert.assertEquals(ResponseReason.INVALID_ACCOUNT_KEY, response);
}
@Test
public void test_setAccountStatus_ACCOUNT_NOT_FOUND() {
// Arrange
// Nothing to do
// Act
ResponseReason response = this.classUnderTest.setAccountStatus("accounts.test", AccountStatus.CLOSED);
// Assert
Assert.assertEquals(ResponseReason.ACCOUNT_NOT_FOUND, response);
}
@Test
public void test_setAccountStatus_UNKNOWN_ERROR() {
// Arrange
Mockito.when(this.accountRepository.findByKey(Mockito.anyString())).thenReturn(new Account());
Mockito.doThrow(new NullPointerException()).when(this.accountRepository).save(Mockito.any(Account.class));
// Act
ResponseReason response = this.classUnderTest.setAccountStatus("accounts.test", AccountStatus.CLOSED);
// Assert
Assert.assertEquals(ResponseReason.UNKNOWN_ERROR, response);
}
@Test
public void test_setAccountStatus_OK() {
// Arrange
Mockito.when(this.accountRepository.findByKey(Mockito.anyString())).thenReturn(new Account());
// Act
ResponseReason response = this.classUnderTest.setAccountStatus("accounts.test", AccountStatus.CLOSED);
// Assert
Assert.assertEquals(ResponseReason.OK, response);
Mockito.verify(this.accountRepository, Mockito.times(1))
.save(ArgumentMatchers.argThat((acc) -> AccountStatus.CLOSED.equals(acc.getStatus())));
}
}

View File

@@ -0,0 +1,94 @@
package de.financer.service;
import de.financer.ResponseReason;
import de.financer.config.FinancerConfig;
import de.financer.dba.RecurringTransactionRepository;
import de.financer.model.RecurringTransaction;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.Optional;
@RunWith(MockitoJUnitRunner.class)
public class RecurringTransactionService_deleteRecurringTransactionTest {
@InjectMocks
private RecurringTransactionService classUnderTest;
@Mock
private AccountService accountService;
@Mock
private RuleService ruleService;
@Mock
private RecurringTransactionRepository recurringTransactionRepository;
@Mock
private FinancerConfig financerConfig;
@Test
public void test_deleteRecurringTransaction_MISSING_RECURRING_TRANSACTION_ID() {
// Arrange
// Nothing to do
// Act
final ResponseReason response = this.classUnderTest.deleteRecurringTransaction(null);
// Assert
Assert.assertEquals(ResponseReason.MISSING_RECURRING_TRANSACTION_ID, response);
}
@Test
public void test_deleteRecurringTransaction_INVALID_RECURRING_TRANSACTION_ID() {
// Arrange
// Nothing to do
// Act
final ResponseReason response = this.classUnderTest.deleteRecurringTransaction("invalid");
// Assert
Assert.assertEquals(ResponseReason.INVALID_RECURRING_TRANSACTION_ID, response);
}
@Test
public void test_deleteRecurringTransaction_RECURRING_TRANSACTION_NOT_FOUND() {
// Arrange
Mockito.when(this.recurringTransactionRepository.findById(Mockito.anyLong())).thenReturn(Optional.empty());
// Act
final ResponseReason response = this.classUnderTest.deleteRecurringTransaction("123");
// Assert
Assert.assertEquals(ResponseReason.RECURRING_TRANSACTION_NOT_FOUND, response);
}
@Test
public void test_deleteRecurringTransaction_UNKNOWN_ERROR() {
// Arrange
Mockito.when(this.recurringTransactionRepository.findById(Mockito.anyLong())).thenReturn(Optional.of(new RecurringTransaction()));
Mockito.doThrow(new NullPointerException()).when(this.recurringTransactionRepository).deleteById(Mockito.anyLong());
// Act
final ResponseReason response = this.classUnderTest.deleteRecurringTransaction("123");
// Assert
Assert.assertEquals(ResponseReason.UNKNOWN_ERROR, response);
}
@Test
public void test_deleteRecurringTransaction_OK() {
// Arrange
Mockito.when(this.recurringTransactionRepository.findById(Mockito.anyLong())).thenReturn(Optional.of(new RecurringTransaction()));
// Act
final ResponseReason response = this.classUnderTest.deleteRecurringTransaction("123");
// Assert
Assert.assertEquals(ResponseReason.OK, response);
}
}

View File

@@ -0,0 +1,129 @@
package de.financer.service;
import de.financer.ResponseReason;
import de.financer.config.FinancerConfig;
import de.financer.dba.TransactionRepository;
import de.financer.model.Account;
import de.financer.model.AccountType;
import de.financer.model.Transaction;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.*;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.List;
import java.util.Optional;
@RunWith(MockitoJUnitRunner.class)
public class TransactionService_deleteTransactionTest {
@InjectMocks
private TransactionService classUnderTest;
@Mock
private AccountService accountService;
@Mock
private RuleService ruleService;
@Mock
private TransactionRepository transactionRepository;
@Mock
private FinancerConfig financerConfig;
@Before
public void setUp() {
this.ruleService.afterPropertiesSet();
Mockito.when(this.ruleService.getMultiplierFromAccount(Mockito.any())).thenCallRealMethod();
Mockito.when(this.ruleService.getMultiplierToAccount(Mockito.any())).thenCallRealMethod();
}
@Test
public void test_deleteRecurringTransaction_MISSING_TRANSACTION_ID() {
// Arrange
// Nothing to do
// Act
final ResponseReason response = this.classUnderTest.deleteTransaction(null);
// Assert
Assert.assertEquals(ResponseReason.MISSING_TRANSACTION_ID, response);
}
@Test
public void test_deleteRecurringTransaction_INVALID_TRANSACTION_ID() {
// Arrange
// Nothing to do
// Act
final ResponseReason response = this.classUnderTest.deleteTransaction("invalid");
// Assert
Assert.assertEquals(ResponseReason.INVALID_TRANSACTION_ID, response);
}
@Test
public void test_deleteRecurringTransaction_TRANSACTION_NOT_FOUND() {
// Arrange
Mockito.when(this.transactionRepository.findById(Mockito.anyLong())).thenReturn(Optional.empty());
// Act
final ResponseReason response = this.classUnderTest.deleteTransaction("123");
// Assert
Assert.assertEquals(ResponseReason.TRANSACTION_NOT_FOUND, response);
}
@Test
public void test_deleteRecurringTransaction_UNKNOWN_ERROR() {
// Arrange
Mockito.when(this.transactionRepository.findById(Mockito.anyLong()))
.thenReturn(Optional.of(createTransaction(AccountType.BANK, AccountType.EXPENSE)));
Mockito.doThrow(new NullPointerException()).when(this.transactionRepository).deleteById(Mockito.anyLong());
// Act
final ResponseReason response = this.classUnderTest.deleteTransaction("123");
// Assert
Assert.assertEquals(ResponseReason.UNKNOWN_ERROR, response);
}
@Test
public void test_deleteRecurringTransaction_OK() {
// Arrange
Mockito.when(this.transactionRepository.findById(Mockito.anyLong()))
.thenReturn(Optional.of(createTransaction(AccountType.BANK, AccountType.EXPENSE)));
// Act
final ResponseReason response = this.classUnderTest.deleteTransaction("123");
// Assert
Assert.assertEquals(ResponseReason.OK, response);
final InOrder inOrder = Mockito.inOrder(this.accountService);
inOrder.verify(this.accountService).saveAccount(ArgumentMatchers.argThat((Account arg) -> Long.valueOf(50000L).equals(arg.getCurrentBalance())));
inOrder.verify(this.accountService).saveAccount(ArgumentMatchers.argThat((Account arg) -> Long.valueOf(5000L).equals(arg.getCurrentBalance())));
}
private Transaction createTransaction(AccountType fromType, AccountType toType) {
final Transaction transaction = new Transaction();
final Account fromAccount = new Account();
final Account toAccount = new Account();
transaction.setFromAccount(fromAccount);
transaction.setToAccount(toAccount);
transaction.setAmount(Long.valueOf(10000L));
fromAccount.setCurrentBalance(Long.valueOf(40000L));
toAccount.setCurrentBalance(Long.valueOf(15000L));
fromAccount.setType(fromType);
toAccount.setType(toType);
return transaction;
}
}