Various fixes and additions all over the tree

Add a method to obtain all active recurring transactions. Add and adjust
unit and integration tests.
This commit is contained in:
2019-03-24 23:20:02 +01:00
parent d6572e26f1
commit f9b448f24e
15 changed files with 143 additions and 76 deletions

View File

@@ -29,16 +29,6 @@ public class AccountController {
return this.accountService.getAll();
}
@RequestMapping("getAccountTypes")
public Iterable<String> getAccountTypes() {
return this.accountService.getAccountTypes();
}
@RequestMapping("getAccountStatus")
public Iterable<String> getAccountStatus() {
return this.accountService.getAccountStatus();
}
@RequestMapping("createAccount")
public ResponseEntity createAccount(String key, String type) {
if (LOGGER.isDebugEnabled()) {

View File

@@ -26,6 +26,11 @@ public class RecurringTransactionController {
return this.recurringTransactionService.getAll();
}
@RequestMapping("getAllActive")
public Iterable<RecurringTransaction> getAllActive() {
return this.recurringTransactionService.getAllActive();
}
@RequestMapping("getAllForAccount")
public Iterable<RecurringTransaction> getAllForAccount(String accountKey) {
return this.recurringTransactionService.getAllForAccount(accountKey);

View File

@@ -6,7 +6,11 @@ import org.springframework.data.repository.CrudRepository;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
@Transactional(propagation = Propagation.REQUIRED)
public interface RecurringTransactionRepository extends CrudRepository<RecurringTransaction, Long> {
Iterable<RecurringTransaction> findRecurringTransactionsByFromAccountOrToAccount(Account fromAccount, Account toAccount);
Iterable<RecurringTransaction> findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(LocalDate lastOccurrence);
}

View File

@@ -40,20 +40,6 @@ public class AccountService {
return this.accountRepository.findAll();
}
/**
* @return all possible account types as specified by the {@link AccountType} enumeration, never <code>null</code>
*/
public Iterable<String> getAccountTypes() {
return Arrays.stream(AccountType.values()).map(AccountType::name).collect(Collectors.toList());
}
/**
* @return all possible account status as specified by the {@link AccountStatus} enumeration, never <code>null</code>
*/
public Iterable<String> getAccountStatus() {
return Arrays.stream(AccountStatus.values()).map(AccountStatus::name).collect(Collectors.toList());
}
/**
* This method saves the given account. It either updates the account if it already exists or inserts
* it if it's new.

View File

@@ -47,6 +47,11 @@ public class RecurringTransactionService {
return this.recurringTransactionRepository.findAll();
}
public Iterable<RecurringTransaction> getAllActive() {
return this.recurringTransactionRepository
.findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(LocalDate.now());
}
public Iterable<RecurringTransaction> getAllForAccount(String accountKey) {
final Account account = this.accountService.getAccountByKey(accountKey);
@@ -73,8 +78,8 @@ public class RecurringTransactionService {
// Visible for unit tests
/* package */ Iterable<RecurringTransaction> getAllDueToday(LocalDate now) {
// TODO filter for lastOccurrence not in the past
final Iterable<RecurringTransaction> allRecurringTransactions = this.recurringTransactionRepository.findAll();
final Iterable<RecurringTransaction> allRecurringTransactions = this.recurringTransactionRepository
.findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(now);
//@formatter:off
return IterableUtils.toList(allRecurringTransactions).stream()
@@ -332,19 +337,19 @@ public class RecurringTransactionService {
response = ResponseReason.MISSING_AMOUNT;
} else if (amount == 0L) {
response = ResponseReason.AMOUNT_ZERO;
} else if (holidayWeekendType == null) {
} else if (StringUtils.isEmpty(holidayWeekendType)) {
response = ResponseReason.MISSING_HOLIDAY_WEEKEND_TYPE;
} else if (!HolidayWeekendType.isValidType(holidayWeekendType)) {
response = ResponseReason.INVALID_HOLIDAY_WEEKEND_TYPE;
} else if (intervalType == null) {
} else if (StringUtils.isEmpty(intervalType)) {
response = ResponseReason.MISSING_INTERVAL_TYPE;
} else if (!IntervalType.isValidType(intervalType)) {
response = ResponseReason.INVALID_INTERVAL_TYPE;
} else if (firstOccurrence == null) {
} else if (StringUtils.isEmpty(firstOccurrence)) {
response = ResponseReason.MISSING_FIRST_OCCURRENCE;
}
if (response == null && firstOccurrence != null) {
if (response == null && StringUtils.isNotEmpty(firstOccurrence)) {
try {
LocalDate.parse(firstOccurrence, DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat()));
} catch (DateTimeParseException e) {
@@ -352,7 +357,7 @@ public class RecurringTransactionService {
}
}
if (response == null && lastOccurrence != null) {
if (response == null && StringUtils.isNotEmpty(lastOccurrence)) {
try {
LocalDate.parse(lastOccurrence, DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat()));
} catch (DateTimeParseException e) {
@@ -407,8 +412,7 @@ public class RecurringTransactionService {
try {
this.recurringTransactionRepository.deleteById(Long.valueOf(recurringTransactionId));
}
catch (Exception e) {
} catch (Exception e) {
LOGGER.error("Could not delete recurring transaction!", e);
response = ResponseReason.UNKNOWN_ERROR;

View File

@@ -4,8 +4,10 @@ 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.RecurringTransaction;
import de.financer.model.Transaction;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -87,8 +89,17 @@ public class TransactionService {
fromAccount.setCurrentBalance(fromAccount.getCurrentBalance() + (this.ruleService
.getMultiplierFromAccount(fromAccount) * amount));
toAccount.setCurrentBalance(toAccount.getCurrentBalance() + (this.ruleService
.getMultiplierToAccount(toAccount) * amount));
// Special case: if we do the initial bookings, and the booking is to introduce a liability,
// the balance of the liability account must increase
if (AccountType.START.equals(fromAccount.getType()) && AccountType.LIABILITY.equals(toAccount.getType())) {
toAccount.setCurrentBalance(toAccount.getCurrentBalance() + (this.ruleService
.getMultiplierToAccount(toAccount) * amount * -1));
}
else {
toAccount.setCurrentBalance(toAccount.getCurrentBalance() + (this.ruleService
.getMultiplierToAccount(toAccount) * amount));
}
this.transactionRepository.save(transaction);
@@ -158,9 +169,9 @@ public class TransactionService {
response = ResponseReason.MISSING_AMOUNT;
} else if (amount == 0L) {
response = ResponseReason.AMOUNT_ZERO;
} else if (date == null) {
} else if (StringUtils.isEmpty(date)) {
response = ResponseReason.MISSING_DATE;
} else if (date != null) {
} else if (StringUtils.isNotEmpty(date)) {
try {
LocalDate.parse(date, DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat()));
} catch (DateTimeParseException e) {

View File

@@ -6,6 +6,7 @@
spring.profiles.active=@activeProfiles@
server.servlet.context-path=/financer-server
server.port=8089
spring.jpa.hibernate.ddl-auto=validate

View File

@@ -54,6 +54,6 @@ public class RecurringTransactionService_createRecurringTransactionIntegrationTe
final List<RecurringTransaction> allRecurringTransaction = this.objectMapper
.readValue(mvcResult.getResponse().getContentAsByteArray(), new TypeReference<List<RecurringTransaction>>() {});
Assert.assertEquals(3, allRecurringTransaction.size());
Assert.assertEquals(4, allRecurringTransaction.size());
}
}

View File

@@ -0,0 +1,49 @@
package de.financer.controller.integration;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.financer.FinancerApplication;
import de.financer.model.RecurringTransaction;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import java.util.List;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = FinancerApplication.class)
@AutoConfigureMockMvc
@TestPropertySource(
locations = "classpath:application-integrationtest.properties")
public class RecurringTransactionService_getAllActiveIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Test
public void test_getAll() throws Exception {
final MvcResult mvcResult = this.mockMvc
.perform(get("/recurringTransactions/getAllActive").contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andReturn();
final List<RecurringTransaction> allRecurringTransactions = this.objectMapper
.readValue(mvcResult.getResponse().getContentAsByteArray(), new TypeReference<List<RecurringTransaction>>() {});
Assert.assertEquals(3, allRecurringTransactions.size());
}
}

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(3, allRecurringTransactions.size());
Assert.assertEquals(4, allRecurringTransactions.size());
}
}

View File

@@ -19,15 +19,15 @@ import java.time.Period;
import java.util.Collections;
/**
* This class contains tests for the {@link RecurringTransactionService}, specifically for
* {@link RecurringTransaction}s that have {@link IntervalType#DAILY} and {@link HolidayWeekendType#NEXT_WORKDAY}.
* Due to these restrictions this class does not contain any tests for recurring transactions due in the close past
* that have been deferred, because recurring transactions with interval type daily get executed on the next workday
* anyway, regardless whether they have been deferred. This means that some executions of a recurring transaction with
* daily/next workday get ignored if they are on a holiday or a weekend day - they do <b>not</b> get executed multiple
* times on the next workday. While this is somehow unfortunate it is <b>not</b> in the current requirements and
* therefore left out for the sake of simplicity. If such a behavior is required daily/same day should do the trick,
* even though with slightly different semantics (execution even on holidays or weekends).
* This class contains tests for the {@link RecurringTransactionService}, specifically for {@link RecurringTransaction}s
* that have {@link IntervalType#DAILY} and {@link HolidayWeekendType#NEXT_WORKDAY}. Due to these restrictions this
* class does not contain any tests for recurring transactions due in the close past that have been deferred, because
* recurring transactions with interval type daily get executed on the next workday anyway, regardless whether they have
* been deferred. This means that some executions of a recurring transaction with daily/next workday get ignored if they
* are on a holiday or a weekend day - they do <b>not</b> get executed multiple times on the next workday. While this is
* somehow unfortunate it is <b>not</b> in the current requirements and therefore left out for the sake of simplicity.
* If such a behavior is required daily/same day should do the trick, even though with slightly different semantics
* (execution even on holidays or weekends).
*/
@RunWith(MockitoJUnitRunner.class)
public class RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest {
@@ -53,7 +53,9 @@ public class RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest {
public void test_getAllDueToday_dueToday() {
// Arrange
// Implicitly: ruleService.isHoliday().return(false) and ruleService.isWeekend().return(false)
Mockito.when(this.recurringTransactionRepository.findAll()).thenReturn(Collections.singletonList(createRecurringTransaction(-3)));
Mockito.when(this.recurringTransactionRepository
.findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any()))
.thenReturn(Collections.singletonList(createRecurringTransaction(-3)));
final LocalDate now = LocalDate.now();
// Act
@@ -71,7 +73,9 @@ public class RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest {
public void test_getAllDueToday_dueToday_holiday() {
// Arrange
// Implicitly: ruleService.isWeekend().return(false)
Mockito.when(this.recurringTransactionRepository.findAll()).thenReturn(Collections.singletonList(createRecurringTransaction(0)));
Mockito.when(this.recurringTransactionRepository
.findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any()))
.thenReturn(Collections.singletonList(createRecurringTransaction(0)));
// Today is a holiday, but yesterday was not
Mockito.when(this.ruleService.isHoliday(Mockito.any())).thenReturn(Boolean.TRUE, Boolean.FALSE);
final LocalDate now = LocalDate.now();
@@ -91,7 +95,9 @@ public class RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest {
public void test_getAllDueToday_dueToday_weekend() {
// Arrange
// Implicitly: ruleService.isHoliday().return(false)
Mockito.when(this.recurringTransactionRepository.findAll()).thenReturn(Collections.singletonList(createRecurringTransaction(0)));
Mockito.when(this.recurringTransactionRepository
.findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any()))
.thenReturn(Collections.singletonList(createRecurringTransaction(0)));
// Today is a weekend day, but yesterday was not
Mockito.when(this.ruleService.isWeekend(Mockito.any())).thenReturn(Boolean.TRUE, Boolean.FALSE);
final LocalDate now = LocalDate.now();
@@ -111,7 +117,9 @@ public class RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest {
public void test_getAllDueToday_dueToday_tomorrow() {
// Arrange
// Implicitly: ruleService.isHoliday().return(false) and ruleService.isWeekend().return(false)
Mockito.when(this.recurringTransactionRepository.findAll()).thenReturn(Collections.singletonList(createRecurringTransaction(1)));
Mockito.when(this.recurringTransactionRepository
.findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any()))
.thenReturn(Collections.singletonList(createRecurringTransaction(1)));
final LocalDate now = LocalDate.now();
// Act

View File

@@ -35,14 +35,15 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest
}
/**
* This method tests whether a recurring transaction with firstOccurrence = one month and one day ago
* (and thus was actually due yesterday), intervalType = monthly and holidayWeekendType = next_workday is due today,
* if yesterday was a holiday but today is not
* This method tests whether a recurring transaction with firstOccurrence = one month and one day ago (and thus was
* actually due yesterday), intervalType = monthly and holidayWeekendType = next_workday is due today, if yesterday
* was a holiday but today is not
*/
@Test
public void test_getAllDueToday_duePast_holiday() {
// Arrange
Mockito.when(this.recurringTransactionRepository.findAll())
Mockito.when(this.recurringTransactionRepository
.findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any()))
.thenReturn(Collections.singletonList(createRecurringTransaction(-1)));
// Today is not a holiday but yesterday was
Mockito.when(this.ruleService.isHoliday(Mockito.any())).thenReturn(Boolean.FALSE, Boolean.TRUE);
@@ -56,9 +57,9 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest
}
/**
* This method tests whether a recurring transaction with firstOccurrence = last friday one month ago
* (and thus was actually due last friday), intervalType = monthly and holidayWeekendType = next_workday is due
* today (monday), if friday was holiday
* This method tests whether a recurring transaction with firstOccurrence = last friday one month ago (and thus was
* actually due last friday), intervalType = monthly and holidayWeekendType = next_workday is due today (monday), if
* friday was holiday
*/
@Test
public void test_getAllDueToday_duePast_weekend_friday_holiday() {
@@ -79,7 +80,8 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest
final LocalDate now = LocalDate.now();
final LocalDate monday = now.minusDays(now.getDayOfWeek().getValue() - 1);
// The transaction occurs on a friday
Mockito.when(this.recurringTransactionRepository.findAll())
Mockito.when(this.recurringTransactionRepository
.findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any()))
.thenReturn(Collections.singletonList(createRecurringTransaction(-(now.getDayOfWeek().getValue() + 2))));
// First False for the dueToday check, 2x True for actual weekend, second False for Friday
Mockito.when(this.ruleService.isWeekend(Mockito.any()))
@@ -96,9 +98,9 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest
}
/**
* This method tests whether a recurring transaction with firstOccurrence = last sunday a month ago
* (and thus was actually due last sunday/yesterday), intervalType = monthly and holidayWeekendType = next_workday
* is due today (monday)
* This method tests whether a recurring transaction with firstOccurrence = last sunday a month ago (and thus was
* actually due last sunday/yesterday), intervalType = monthly and holidayWeekendType = next_workday is due today
* (monday)
*/
@Test
public void test_getAllDueToday_duePast_weekend_sunday() {
@@ -106,7 +108,8 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest
final LocalDate now = LocalDate.now();
final LocalDate monday = now.minusDays(now.getDayOfWeek().getValue() - 1);
// The transaction occurs on a sunday
Mockito.when(this.recurringTransactionRepository.findAll())
Mockito.when(this.recurringTransactionRepository
.findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any()))
.thenReturn(Collections.singletonList(createRecurringTransaction(-now.getDayOfWeek().getValue())));
// First False for the dueToday check, 2x True for actual weekend, second False for Friday
Mockito.when(this.ruleService.isWeekend(Mockito.any()))
@@ -120,9 +123,9 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest
}
/**
* This method tests whether a recurring transaction with firstOccurrence = saturday a month ago
* (and thus was actually due last saturday/two days ago), intervalType = monthly and
* holidayWeekendType = next_workday is due today (monday)
* This method tests whether a recurring transaction with firstOccurrence = saturday a month ago (and thus was
* actually due last saturday/two days ago), intervalType = monthly and holidayWeekendType = next_workday is due
* today (monday)
*/
@Test
public void test_getAllDueToday_duePast_weekend_saturday() {
@@ -130,7 +133,8 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest
final LocalDate now = LocalDate.now();
final LocalDate monday = now.minusDays(now.getDayOfWeek().getValue() - 1);
// The transaction occurs on a saturday
Mockito.when(this.recurringTransactionRepository.findAll())
Mockito.when(this.recurringTransactionRepository
.findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any()))
.thenReturn(Collections.singletonList(createRecurringTransaction(-(now.getDayOfWeek().getValue() + 1))));
// First False for the dueToday check, 2x True for actual weekend, second False for Friday
Mockito.when(this.ruleService.isWeekend(Mockito.any()))

View File

@@ -35,14 +35,15 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_PREVIOUS_WORKDAY
}
/**
* This method tests whether a recurring transaction with firstOccurrence = one month plus one day (and thus
* will actually be due tomorrow), intervalType = monthly and holidayWeekendType = previous_workday is due today, if
* This method tests whether a recurring transaction with firstOccurrence = one month plus one day (and thus will
* actually be due tomorrow), intervalType = monthly and holidayWeekendType = previous_workday is due today, if
* tomorrow will be a holiday but today is not
*/
@Test
public void test_getAllDueToday_dueFuture_holiday() {
// Arrange
Mockito.when(this.recurringTransactionRepository.findAll())
Mockito.when(this.recurringTransactionRepository
.findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any()))
.thenReturn(Collections.singletonList(createRecurringTransaction(1)));
// Today is not a holiday but tomorrow is
Mockito.when(this.ruleService.isHoliday(Mockito.any())).thenReturn(Boolean.FALSE, Boolean.TRUE);

View File

@@ -35,14 +35,15 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_SAME_DAYTest {
}
/**
* This method tests whether a recurring transaction with firstOccurrence = one month and one day ago
* (and thus was actually due yesterday), intervalType = monthly and holidayWeekendType = same_day is not due today,
* if yesterday was a holiday but today is not
* This method tests whether a recurring transaction with firstOccurrence = one month and one day ago (and thus was
* actually due yesterday), intervalType = monthly and holidayWeekendType = same_day is not due today, if yesterday
* was a holiday but today is not
*/
@Test
public void test_getAllDueToday_duePast_holiday() {
// Arrange
Mockito.when(this.recurringTransactionRepository.findAll())
Mockito.when(this.recurringTransactionRepository
.findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any()))
.thenReturn(Collections.singletonList(createRecurringTransaction(-1)));
// Today is not a holiday but yesterday was
Mockito.when(this.ruleService.isHoliday(Mockito.any())).thenReturn(Boolean.FALSE, Boolean.TRUE);

View File

@@ -7,4 +7,7 @@ INSERT INTO recurring_transaction (from_account_id, to_account_id, description,
VALUES ((SELECT ID FROM account WHERE "key" = 'accounts.income'), (SELECT ID FROM account WHERE "key" = 'accounts.checkaccount'), 'Pay', 250000, 'MONTHLY', '2019-01-15', 'NEXT_WORKDAY');
INSERT INTO recurring_transaction (from_account_id, to_account_id, description, amount, interval_type, first_occurrence, holiday_weekend_type)
VALUES ((SELECT ID FROM account WHERE "key" = 'accounts.cash'), (SELECT ID FROM account WHERE "key" = 'accounts.convenience'), 'Pretzel', 170, 'DAILY', '2019-02-20', 'SAME_DAY');
VALUES ((SELECT ID FROM account WHERE "key" = 'accounts.cash'), (SELECT ID FROM account WHERE "key" = 'accounts.convenience'), 'Pretzel', 170, 'DAILY', '2019-02-20', 'SAME_DAY');
INSERT INTO recurring_transaction (from_account_id, to_account_id, description, amount, interval_type, first_occurrence, last_occurrence, holiday_weekend_type)
VALUES ((SELECT ID FROM account WHERE "key" = 'accounts.cash'), (SELECT ID FROM account WHERE "key" = 'accounts.foodexternal'), 'McDonalds Happy Meal', 399, 'WEEKLY', '2019-02-20', '2019-03-20', 'SAME_DAY');