Various stuff all over the tree
- Increase Java version to 1.9 - Add commons-collection and Jollyday dependencies - Add JavaDoc plugin - Add country and state configuration for Jollyday library - Add WIP implementation of the recurring transaction feature - Improve JavaDoc - Use Java 8 date API - Reformatting - Add special Flyway migration version for test data - Add and improve unit tests
This commit is contained in:
31
pom.xml
31
pom.xml
@@ -20,9 +20,9 @@
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<maven.compiler.target>1.8</maven.compiler.target>
|
||||
<java.version>1.8</java.version>
|
||||
<maven.compiler.source>1.9</maven.compiler.source>
|
||||
<maven.compiler.target>1.9</maven.compiler.target>
|
||||
<java.version>1.9</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -46,7 +46,17 @@
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>3.8.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-collections4</artifactId>
|
||||
<version>4.3</version>
|
||||
</dependency>
|
||||
<!-- Misc dependencies -->
|
||||
<dependency>
|
||||
<groupId>de.jollyday</groupId>
|
||||
<artifactId>jollyday</artifactId>
|
||||
<version>0.5.7</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Runtime dependencies -->
|
||||
@@ -99,6 +109,19 @@
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<reporting>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<configuration>
|
||||
<show>private</show>
|
||||
<javadocExecutable>/usr/bin/javadoc</javadocExecutable>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</reporting>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>integration-tests</id>
|
||||
|
||||
53
src/main/java/de/financer/config/FinancerConfig.java
Normal file
53
src/main/java/de/financer/config/FinancerConfig.java
Normal file
@@ -0,0 +1,53 @@
|
||||
package de.financer.config;
|
||||
|
||||
import de.jollyday.HolidayCalendar;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "financer")
|
||||
public class FinancerConfig {
|
||||
private String countryCode;
|
||||
private String state;
|
||||
|
||||
/**
|
||||
* @return the raw country code, mostly an uppercase ISO 3166 2-letter code
|
||||
*/
|
||||
public String getCountryCode() {
|
||||
return countryCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the state
|
||||
*/
|
||||
public String getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the {@link HolidayCalendar} used to calculate the holidays. Internally uses the country code
|
||||
* specified via {@link FinancerConfig#getCountryCode}.
|
||||
*/
|
||||
public HolidayCalendar getHolidayCalendar() {
|
||||
final Optional<HolidayCalendar> optionalHoliday = Arrays.asList(HolidayCalendar.values()).stream()
|
||||
.filter((hc) -> hc.getId().equals(this.countryCode))
|
||||
.findFirst();
|
||||
|
||||
if (!optionalHoliday.isPresent()) {
|
||||
// TODO log info about default DE
|
||||
}
|
||||
|
||||
return optionalHoliday.orElse(HolidayCalendar.GERMANY);
|
||||
}
|
||||
|
||||
public void setState(String state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public void setCountryCode(String countryCode) {
|
||||
this.countryCode = countryCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package de.financer.controller;
|
||||
|
||||
import de.financer.model.RecurringTransaction;
|
||||
import de.financer.service.RecurringTransactionService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("recurringTransactions")
|
||||
public class RecurringTransactionController {
|
||||
|
||||
@Autowired
|
||||
private RecurringTransactionService recurringTransactionService;
|
||||
|
||||
@RequestMapping("getAll")
|
||||
public Iterable<RecurringTransaction> getAll() {
|
||||
return this.recurringTransactionService.getAll();
|
||||
}
|
||||
|
||||
@RequestMapping("getAllForAccount")
|
||||
public Iterable<RecurringTransaction> getAllForAccount(String accountKey) {
|
||||
return this.recurringTransactionService.getAllForAccount(accountKey);
|
||||
}
|
||||
|
||||
@RequestMapping("getAllDueToday")
|
||||
public Iterable<RecurringTransaction> getAllDueToday() {
|
||||
return this.recurringTransactionService.getAllDueToday();
|
||||
}
|
||||
|
||||
@RequestMapping("createRecurringTransaction")
|
||||
public ResponseEntity createRecurringTransaction(String fromAccountKey, String toAccountKey, Long amount,
|
||||
String description, String holidayWeekendType,
|
||||
String intervalType, String firstOccurrence,
|
||||
String lastOccurrence) {
|
||||
return this.recurringTransactionService.createRecurringTransaction(fromAccountKey, toAccountKey, amount,
|
||||
description, holidayWeekendType,
|
||||
intervalType, firstOccurrence,
|
||||
lastOccurrence)
|
||||
.toResponseEntity();
|
||||
}
|
||||
}
|
||||
@@ -23,8 +23,10 @@ public class TransactionController {
|
||||
return this.transactionService.getAllForAccount(accountKey);
|
||||
}
|
||||
|
||||
@RequestMapping("createTransaction")
|
||||
public ResponseEntity createTransaction(String fromAccountKey, String toAccountKey, Long amount, String date, String description) {
|
||||
return this.transactionService.createTransaction(fromAccountKey, toAccountKey, amount, date, description).toResponseEntity();
|
||||
@RequestMapping(value = "createTransaction")
|
||||
public ResponseEntity createTransaction(String fromAccountKey, String toAccountKey, Long amount, String date,
|
||||
String description) {
|
||||
return this.transactionService.createTransaction(fromAccountKey, toAccountKey, amount, date, description)
|
||||
.toResponseEntity();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
package de.financer.dba;
|
||||
|
||||
import de.financer.model.Account;
|
||||
import de.financer.model.RecurringTransaction;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Transactional(propagation = Propagation.REQUIRED)
|
||||
public interface RecurringTransactionRepository extends CrudRepository<RecurringTransaction, Long> {
|
||||
Iterable<RecurringTransaction> findRecurringTransactionsByFromAccountOrToAccount(Account fromAccount, Account toAccount);
|
||||
}
|
||||
|
||||
@@ -10,10 +10,47 @@ public enum HolidayWeekendType {
|
||||
/** Indicates that the action should be done on the specified day regardless whether it's a holiday or a weekend */
|
||||
SAME_DAY,
|
||||
|
||||
/** Indicates that the action should be deferred to the next workday */
|
||||
/**
|
||||
* <p>
|
||||
* Indicates that the action should be deferred to the next workday.
|
||||
* </p>
|
||||
* <pre>
|
||||
* Example 1:
|
||||
* MO TU WE TH FR SA SO
|
||||
* H WE WE -> Holiday/WeekEnd
|
||||
* X -> Due date of action
|
||||
* X' -> Deferred, effective due date of action
|
||||
* </pre>
|
||||
* <pre>
|
||||
* Example 2:
|
||||
* TU WE TH FR SA SO MO
|
||||
* H WE WE -> Holiday/WeekEnd
|
||||
* X -> Due date of action
|
||||
* X' -> Deferred, effective due date of action
|
||||
* </pre>
|
||||
*
|
||||
*/
|
||||
NEXT_WORKDAY,
|
||||
|
||||
/** Indicates that the action should be dated back to the previous day */
|
||||
/**
|
||||
* <p>
|
||||
* Indicates that the action should be made earlier at the previous day
|
||||
* </p>
|
||||
* <pre>
|
||||
* Example 1:
|
||||
* MO TU WE TH FR SA SO
|
||||
* H WE WE -> Holiday/WeekEnd
|
||||
* X -> Due date of action
|
||||
* X' -> Earlier, effective due date of action
|
||||
* </pre>
|
||||
* <pre>
|
||||
* Example 2:
|
||||
* MO TU WE TH FR SA SO
|
||||
* H WE WE -> Holiday/WeekEnd
|
||||
* X -> Due date of action
|
||||
* X' -> Earlier, effective due date of action
|
||||
* </pre>
|
||||
*/
|
||||
PREVIOUS_WORKDAY;
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package de.financer.model;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.util.Date;
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Entity
|
||||
public class RecurringTransaction {
|
||||
@@ -16,10 +16,8 @@ public class RecurringTransaction {
|
||||
private Long amount;
|
||||
@Enumerated(EnumType.STRING)
|
||||
private IntervalType intervalType;
|
||||
@Temporal(TemporalType.DATE)
|
||||
private Date firstOccurrence;
|
||||
@Temporal(TemporalType.DATE)
|
||||
private Date lastOccurrence;
|
||||
private LocalDate firstOccurrence;
|
||||
private LocalDate lastOccurrence;
|
||||
@Enumerated(EnumType.STRING)
|
||||
private HolidayWeekendType holidayWeekendType;
|
||||
|
||||
@@ -67,19 +65,19 @@ public class RecurringTransaction {
|
||||
this.holidayWeekendType = holidayWeekendType;
|
||||
}
|
||||
|
||||
public Date getLastOccurrence() {
|
||||
public LocalDate getLastOccurrence() {
|
||||
return lastOccurrence;
|
||||
}
|
||||
|
||||
public void setLastOccurrence(Date lastOccurrence) {
|
||||
public void setLastOccurrence(LocalDate lastOccurrence) {
|
||||
this.lastOccurrence = lastOccurrence;
|
||||
}
|
||||
|
||||
public Date getFirstOccurrence() {
|
||||
public LocalDate getFirstOccurrence() {
|
||||
return firstOccurrence;
|
||||
}
|
||||
|
||||
public void setFirstOccurrence(Date firstOccurrence) {
|
||||
public void setFirstOccurrence(LocalDate firstOccurrence) {
|
||||
this.firstOccurrence = firstOccurrence;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package de.financer.model;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.util.Date;
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Entity
|
||||
@Table(name = "\"transaction\"")
|
||||
@@ -13,9 +13,8 @@ public class Transaction {
|
||||
private Account fromAccount;
|
||||
@OneToOne(fetch = FetchType.EAGER)
|
||||
private Account toAccount;
|
||||
@Temporal(TemporalType.DATE)
|
||||
@Column(name = "\"date\"")
|
||||
private Date date;
|
||||
private LocalDate date;
|
||||
private String description;
|
||||
private Long amount;
|
||||
@ManyToOne(fetch = FetchType.EAGER)
|
||||
@@ -41,11 +40,11 @@ public class Transaction {
|
||||
this.toAccount = toAccount;
|
||||
}
|
||||
|
||||
public Date getDate() {
|
||||
public LocalDate getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public void setDate(Date date) {
|
||||
public void setDate(LocalDate date) {
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
|
||||
10
src/main/java/de/financer/model/package-info.java
Normal file
10
src/main/java/de/financer/model/package-info.java
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* <p>
|
||||
* This package contains the main model for the financer application.
|
||||
* In the DDD (<i>Domain Driven Design</i>) sense the models are anemic
|
||||
* as they contain no logic themselves but act as mere POJOs with additional
|
||||
* Hibernate annotations. The (business) logic is located in the services of the
|
||||
* {@link de.financer.service} package.
|
||||
* </p>
|
||||
*/
|
||||
package de.financer.model;
|
||||
@@ -0,0 +1,163 @@
|
||||
package de.financer.service;
|
||||
|
||||
import de.financer.ResponseReason;
|
||||
import de.financer.dba.RecurringTransactionRepository;
|
||||
import de.financer.model.Account;
|
||||
import de.financer.model.HolidayWeekendType;
|
||||
import de.financer.model.RecurringTransaction;
|
||||
import org.apache.commons.collections4.IterableUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Collections;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class RecurringTransactionService {
|
||||
|
||||
@Autowired
|
||||
private RecurringTransactionRepository recurringTransactionRepository;
|
||||
|
||||
@Autowired
|
||||
private AccountService accountService;
|
||||
|
||||
@Autowired
|
||||
private RuleService ruleService;
|
||||
|
||||
public Iterable<RecurringTransaction> getAll() {
|
||||
return this.recurringTransactionRepository.findAll();
|
||||
}
|
||||
|
||||
public Iterable<RecurringTransaction> getAllForAccount(String accountKey) {
|
||||
final Account account = this.accountService.getAccountByKey(accountKey);
|
||||
|
||||
if (account == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// As we want all transactions of the given account use it as from and to account
|
||||
return this.recurringTransactionRepository.findRecurringTransactionsByFromAccountOrToAccount(account, account);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method gets all recurring transactions that are due today. Whether a recurring transaction is due today
|
||||
* depends on today's date and the configured {@link RecurringTransaction#getIntervalType() interval type}
|
||||
* and {@link RecurringTransaction#getHolidayWeekendType() holiday weekend type}.
|
||||
*
|
||||
* @return all recurring transactions that are due today
|
||||
*/
|
||||
public Iterable<RecurringTransaction> getAllDueToday() {
|
||||
return this.getAllDueToday(LocalDate.now());
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
||||
//@formatter:off
|
||||
return IterableUtils.toList(allRecurringTransactions).stream()
|
||||
.filter((rt) -> checkRecurringTransactionDueToday(rt, now) ||
|
||||
checkRecurringTransactionDuePast(rt, now))
|
||||
// TODO checkRecurringTransactionDueFuture for HolidayWeekendType.PREVIOUS_WORKDAY
|
||||
.collect(Collectors.toList());
|
||||
//@formatter:on
|
||||
}
|
||||
|
||||
/**
|
||||
* This method checks whether the given {@link RecurringTransaction} is due today.
|
||||
* A recurring transaction is due if the current {@link LocalDate date} is a multiple of the
|
||||
* {@link RecurringTransaction#getFirstOccurrence() first occurrence} of the recurring transaction and the
|
||||
* {@link RecurringTransaction#getIntervalType() interval type}. If today is a
|
||||
* {@link RuleService#isHoliday(LocalDate) holiday} or a
|
||||
* {@link RuleService#isWeekend(LocalDate) weekend day} the
|
||||
* {@link HolidayWeekendType holiday weekend type}
|
||||
* is taken into account to decide whether the recurring transaction should be deferred.
|
||||
*
|
||||
* @param recurringTransaction to check whether it is due today
|
||||
* @param now today's date
|
||||
* @return <code>true</code> if the recurring transaction is due today, <code>false</code> otherwise
|
||||
*/
|
||||
private boolean checkRecurringTransactionDueToday(RecurringTransaction recurringTransaction, LocalDate now) {
|
||||
final boolean holiday = this.ruleService.isHoliday(now);
|
||||
|
||||
final boolean dueToday = recurringTransaction.getFirstOccurrence()
|
||||
// This calculates all dates between the first occurrence of the
|
||||
// recurring transaction and tomorrow for the interval specified
|
||||
// by the recurring transaction. We need to use tomorrow as
|
||||
// upper bound of the interval because the upper bound is exclusive
|
||||
// in the datesUntil method.
|
||||
.datesUntil(now.plusDays(1), this.ruleService
|
||||
.getPeriodForInterval(recurringTransaction
|
||||
.getIntervalType()))
|
||||
// Then we check whether today is a date in the calculated range.
|
||||
// If so the recurring transaction is due today
|
||||
.anyMatch((d) -> d.equals(now));
|
||||
final boolean weekend = this.ruleService.isWeekend(now);
|
||||
boolean defer = false;
|
||||
|
||||
|
||||
if (holiday || weekend) {
|
||||
defer = recurringTransaction.getHolidayWeekendType() == HolidayWeekendType.NEXT_WORKDAY;
|
||||
}
|
||||
|
||||
return !defer && dueToday;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method checks whether the given {@link RecurringTransaction} was actually due in the close past
|
||||
* but has been deferred to <i>maybe</i> today because the actual due day has been a holiday or weekend day and the
|
||||
* {@link RecurringTransaction#getHolidayWeekendType() holiday weekend type} was
|
||||
* {@link HolidayWeekendType#NEXT_WORKDAY}. Note that the recurring transaction may get deferred again if today
|
||||
* again is a holiday or a weekend day.
|
||||
* The period this method considers starts with today and ends with the last workday (no
|
||||
* {@link RuleService#isHoliday(LocalDate) holiday}, not a {@link RuleService#isWeekend(LocalDate) weekend day})
|
||||
* whereas the end is exclusive, because if the recurring transaction would have been due at the last workday day
|
||||
* it wouldn't has been deferred.
|
||||
*
|
||||
* @param recurringTransaction to check whether it is due today
|
||||
* @param now today's date
|
||||
* @return <code>true</code> if the recurring transaction is due today, <code>false</code> otherwise
|
||||
*/
|
||||
private boolean checkRecurringTransactionDuePast(RecurringTransaction recurringTransaction, LocalDate now) {
|
||||
boolean weekend;
|
||||
boolean holiday;
|
||||
LocalDate yesterday = now;
|
||||
boolean due = false;
|
||||
|
||||
// Go back in time until we hit the first non-holiday, non-weekend day
|
||||
// and check for every day in between if the given recurring transaction was due on this day
|
||||
do {
|
||||
yesterday = yesterday.minusDays(1);
|
||||
holiday = this.ruleService.isHoliday(yesterday);
|
||||
weekend = this.ruleService.isWeekend(yesterday);
|
||||
|
||||
if (holiday || weekend) {
|
||||
// Lambdas require final local variables
|
||||
final LocalDate finalYesterday = yesterday;
|
||||
|
||||
// For an explanation of the expression see the ...DueToday method
|
||||
due = recurringTransaction.getFirstOccurrence()
|
||||
.datesUntil(yesterday.plusDays(1), this.ruleService
|
||||
.getPeriodForInterval(recurringTransaction
|
||||
.getIntervalType()))
|
||||
.anyMatch((d) -> d.equals(finalYesterday));
|
||||
|
||||
if (due) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
while (holiday || weekend);
|
||||
|
||||
return due;
|
||||
}
|
||||
|
||||
public ResponseReason createRecurringTransaction(String fromAccountKey, String toAccountKey, Long amount,
|
||||
String description, String holidayWeekendType,
|
||||
String intervalType, String firstOccurrence,
|
||||
String lastOccurrence) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,51 @@
|
||||
package de.financer.service;
|
||||
|
||||
import de.financer.config.FinancerConfig;
|
||||
import de.financer.model.Account;
|
||||
import de.financer.model.AccountType;
|
||||
import de.financer.model.IntervalType;
|
||||
import de.jollyday.HolidayManager;
|
||||
import de.jollyday.ManagerParameters;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.DayOfWeek;
|
||||
import java.time.LocalDate;
|
||||
import java.time.Period;
|
||||
import java.util.*;
|
||||
|
||||
import static de.financer.model.AccountType.*;
|
||||
|
||||
/**
|
||||
* This service encapsulates methods that form business logic rules.
|
||||
* This service encapsulates methods that form basic logic rules.
|
||||
* While most of the logic could be placed elsewhere this service provides
|
||||
* centralized access to these rules.
|
||||
* centralized access to these rules. Placing them in here also enables easy
|
||||
* unit testing.
|
||||
*/
|
||||
@Service
|
||||
public class RuleService implements InitializingBean {
|
||||
|
||||
@Autowired
|
||||
private FinancerConfig financerConfig;
|
||||
|
||||
private Map<AccountType, Collection<AccountType>> bookingRules;
|
||||
private Map<IntervalType, Period> intervalPeriods;
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
initBookingRules();
|
||||
initIntervalValues();
|
||||
}
|
||||
|
||||
private void initIntervalValues() {
|
||||
this.intervalPeriods = new EnumMap<>(IntervalType.class);
|
||||
|
||||
this.intervalPeriods.put(IntervalType.DAILY, Period.ofDays(1));
|
||||
this.intervalPeriods.put(IntervalType.WEEKLY, Period.ofWeeks(1));
|
||||
this.intervalPeriods.put(IntervalType.MONTHLY, Period.ofMonths(1));
|
||||
this.intervalPeriods.put(IntervalType.QUARTERLY, Period.ofMonths(3));
|
||||
this.intervalPeriods.put(IntervalType.YEARLY, Period.ofYears(1));
|
||||
}
|
||||
|
||||
private void initBookingRules() {
|
||||
@@ -40,7 +64,7 @@ public class RuleService implements InitializingBean {
|
||||
|
||||
/**
|
||||
* This method returns the multiplier for the given from account.
|
||||
*
|
||||
* <p>
|
||||
* The multiplier controls whether the current amount of the given from account is increased or
|
||||
* decreased depending on the {@link AccountType} of the given account.
|
||||
*
|
||||
@@ -55,17 +79,13 @@ public class RuleService implements InitializingBean {
|
||||
|
||||
if (INCOME.equals(accountType)) {
|
||||
return 1L;
|
||||
}
|
||||
else if (BANK.equals(accountType)) {
|
||||
} else if (BANK.equals(accountType)) {
|
||||
return -1L;
|
||||
}
|
||||
else if (CASH.equals(accountType)) {
|
||||
} else if (CASH.equals(accountType)) {
|
||||
return -1L;
|
||||
}
|
||||
else if (LIABILITY.equals(accountType)) {
|
||||
} else if (LIABILITY.equals(accountType)) {
|
||||
return 1L;
|
||||
}
|
||||
else if (START.equals(accountType)) {
|
||||
} else if (START.equals(accountType)) {
|
||||
return 1L;
|
||||
}
|
||||
|
||||
@@ -74,7 +94,7 @@ public class RuleService implements InitializingBean {
|
||||
|
||||
/**
|
||||
* This method returns the multiplier for the given to account.
|
||||
*
|
||||
* <p>
|
||||
* The multiplier controls whether the current amount of the given to account is increased or
|
||||
* decreased depending on the {@link AccountType} of the given account.
|
||||
*
|
||||
@@ -89,14 +109,11 @@ public class RuleService implements InitializingBean {
|
||||
|
||||
if (BANK.equals(accountType)) {
|
||||
return 1L;
|
||||
}
|
||||
else if (CASH.equals(accountType)) {
|
||||
} else if (CASH.equals(accountType)) {
|
||||
return 1L;
|
||||
}
|
||||
else if (LIABILITY.equals(accountType)) {
|
||||
} else if (LIABILITY.equals(accountType)) {
|
||||
return -1L;
|
||||
}
|
||||
else if (EXPENSE.equals(accountType)) {
|
||||
} else if (EXPENSE.equals(accountType)) {
|
||||
return 1L;
|
||||
}
|
||||
|
||||
@@ -109,10 +126,43 @@ public class RuleService implements InitializingBean {
|
||||
* account does not make sense and is declared as invalid.
|
||||
*
|
||||
* @param fromAccount the account to subtract the money from
|
||||
* @param toAccount the account to add the money to
|
||||
* @return <code>true</code> if the from->to relationship of the given accounts is valid, <code>false</code> otherwise
|
||||
* @param toAccount the account to add the money to
|
||||
* @return <code>true</code> if the from->to relationship of the given accounts is valid, <code>false</code> otherwise
|
||||
*/
|
||||
public boolean isValidBooking(Account fromAccount, Account toAccount) {
|
||||
return this.bookingRules.get(fromAccount.getType()).contains(toAccount.getType());
|
||||
}
|
||||
|
||||
/**
|
||||
* This method gets the {@link Period} for the given {@link IntervalType},
|
||||
* e.g. a period of three months for {@link IntervalType#QUARTERLY}.
|
||||
*
|
||||
* @param intervalType to get the period for
|
||||
* @return the period matching the interval type
|
||||
*/
|
||||
public Period getPeriodForInterval(IntervalType intervalType) {
|
||||
return this.intervalPeriods.get(intervalType);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method checks whether the given date is a holiday in the configured country and state.
|
||||
*
|
||||
* @param now the date to check
|
||||
* @return <code>true</code> if the given date is a holiday, <code>false</code> otherwise
|
||||
*/
|
||||
public boolean isHoliday(LocalDate now) {
|
||||
return HolidayManager.getInstance(ManagerParameters.create(this.financerConfig.getHolidayCalendar()))
|
||||
.isHoliday(now, this.financerConfig.getState());
|
||||
}
|
||||
|
||||
/**
|
||||
* This method checks whether the given date is a weekend day, i.e. whether it's a
|
||||
* {@link DayOfWeek#SATURDAY} or {@link DayOfWeek#SUNDAY}.
|
||||
*
|
||||
* @param now the date to check
|
||||
* @return <code>true</code> if the given date is a weekend day, <code>false</code> otherwise
|
||||
*/
|
||||
public boolean isWeekend(LocalDate now) {
|
||||
return EnumSet.of(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY).contains(now.getDayOfWeek());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,9 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.Collections;
|
||||
|
||||
@Service
|
||||
@@ -36,7 +37,7 @@ public class TransactionService {
|
||||
/**
|
||||
* @param accountKey the key of the account to get the transactions for
|
||||
* @return all transactions for the given account, for all time. Returns an empty list if the given key does not
|
||||
* match any account.
|
||||
* match any account.
|
||||
*/
|
||||
public Iterable<Transaction> getAllForAccount(String accountKey) {
|
||||
final Account account = this.accountService.getAccountByKey(accountKey);
|
||||
@@ -63,8 +64,10 @@ public class TransactionService {
|
||||
try {
|
||||
final Transaction transaction = buildTransaction(fromAccount, toAccount, amount, description, date);
|
||||
|
||||
fromAccount.setCurrentBalance(fromAccount.getCurrentBalance() + (this.ruleService.getMultiplierFromAccount(fromAccount) * amount));
|
||||
toAccount.setCurrentBalance(toAccount.getCurrentBalance() + (this.ruleService.getMultiplierToAccount(toAccount) * amount));
|
||||
fromAccount.setCurrentBalance(fromAccount.getCurrentBalance() + (this.ruleService
|
||||
.getMultiplierFromAccount(fromAccount) * amount));
|
||||
toAccount.setCurrentBalance(toAccount.getCurrentBalance() + (this.ruleService
|
||||
.getMultiplierToAccount(toAccount) * amount));
|
||||
|
||||
this.transactionRepository.save(transaction);
|
||||
|
||||
@@ -72,13 +75,11 @@ public class TransactionService {
|
||||
this.accountService.saveAccount(toAccount);
|
||||
|
||||
response = ResponseReason.OK;
|
||||
}
|
||||
catch(ParseException e) {
|
||||
} catch (DateTimeParseException e) {
|
||||
// TODO log
|
||||
|
||||
response = ResponseReason.INVALID_DATE_FORMAT;
|
||||
}
|
||||
catch (Exception e) {
|
||||
} catch (Exception e) {
|
||||
// TODO log
|
||||
|
||||
response = ResponseReason.UNKNOWN_ERROR;
|
||||
@@ -91,21 +92,21 @@ public class TransactionService {
|
||||
* This method builds the actual transaction object with the given values.
|
||||
*
|
||||
* @param fromAccount the from account
|
||||
* @param toAccount the to account
|
||||
* @param amount the transaction amount
|
||||
* @param toAccount the to account
|
||||
* @param amount the transaction amount
|
||||
* @param description the description of the transaction
|
||||
* @param date the date of the transaction
|
||||
* @param date the date of the transaction
|
||||
* @return the build {@link Transaction} instance
|
||||
* @throws ParseException if the given date string cannot be parsed into a {@link java.util.Date} instance
|
||||
* @throws DateTimeParseException if the given date string cannot be parsed into a {@link java.time.LocalDate} instance
|
||||
*/
|
||||
private Transaction buildTransaction(Account fromAccount, Account toAccount, Long amount, String description, String date) throws ParseException {
|
||||
private Transaction buildTransaction(Account fromAccount, Account toAccount, Long amount, String description, String date) throws DateTimeParseException {
|
||||
final Transaction transaction = new Transaction();
|
||||
|
||||
transaction.setFromAccount(fromAccount);
|
||||
transaction.setToAccount(toAccount);
|
||||
transaction.setAmount(amount);
|
||||
transaction.setDescription(description);
|
||||
transaction.setDate(new SimpleDateFormat(DATE_FORMAT).parse(date));
|
||||
transaction.setDate(LocalDate.parse(date, DateTimeFormatter.ofPattern(DATE_FORMAT)));
|
||||
|
||||
return transaction;
|
||||
}
|
||||
@@ -114,9 +115,9 @@ public class TransactionService {
|
||||
* This method checks whether the parameters for creating a transaction are valid.
|
||||
*
|
||||
* @param fromAccount the from account
|
||||
* @param toAccount the to account
|
||||
* @param amount the transaction amount
|
||||
* @param date the transaction date
|
||||
* @param toAccount the to account
|
||||
* @param amount the transaction amount
|
||||
* @param date the transaction date
|
||||
* @return the first error found or <code>null</code> if all parameters are valid
|
||||
*/
|
||||
private ResponseReason validateParameters(Account fromAccount, Account toAccount, Long amount, String date) {
|
||||
@@ -124,23 +125,17 @@ public class TransactionService {
|
||||
|
||||
if (fromAccount == null && toAccount == null) {
|
||||
response = ResponseReason.FROM_AND_TO_ACCOUNT_NOT_FOUND;
|
||||
}
|
||||
else if (toAccount == null) {
|
||||
} else if (toAccount == null) {
|
||||
response = ResponseReason.TO_ACCOUNT_NOT_FOUND;
|
||||
}
|
||||
else if (fromAccount == null) {
|
||||
} else if (fromAccount == null) {
|
||||
response = ResponseReason.FROM_ACCOUNT_NOT_FOUND;
|
||||
}
|
||||
else if (!this.ruleService.isValidBooking(fromAccount, toAccount)) {
|
||||
} else if (!this.ruleService.isValidBooking(fromAccount, toAccount)) {
|
||||
response = ResponseReason.INVALID_BOOKING_ACCOUNTS;
|
||||
}
|
||||
else if (amount == null) {
|
||||
} else if (amount == null) {
|
||||
response = ResponseReason.MISSING_AMOUNT;
|
||||
}
|
||||
else if (amount == 0l) {
|
||||
} else if (amount == 0l) {
|
||||
response = ResponseReason.AMOUNT_ZERO;
|
||||
}
|
||||
else if (date == null) {
|
||||
} else if (date == null) {
|
||||
response = ResponseReason.MISSING_DATE;
|
||||
}
|
||||
|
||||
|
||||
9
src/main/java/de/financer/service/package-info.java
Normal file
9
src/main/java/de/financer/service/package-info.java
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* <p>
|
||||
* This package contains the actual business logic services of the financer application.
|
||||
* They get called by the {@link de.financer.controller controller} layer. Also they call each other,
|
||||
* e.g. the {@link de.financer.service.TransactionService} internally uses the {@link de.financer.service.RuleService}.
|
||||
* Data access is done via the {@link de.financer.dba DBA} layer.
|
||||
* </p>
|
||||
*/
|
||||
package de.financer.service;
|
||||
@@ -12,4 +12,13 @@ info.app.name=Financer
|
||||
info.app.description=A simple server for personal finance administration
|
||||
info.build.group=@project.groupId@
|
||||
info.build.artifact=@project.artifactId@
|
||||
info.build.version=@project.version@
|
||||
info.build.version=@project.version@
|
||||
|
||||
# Country code for holiday checks
|
||||
# Mostly an uppercase ISO 3166 2-letter code
|
||||
# For a complete list of the supported codes see https://github.com/svendiedrichsen/jollyday/blob/master/src/main/java/de/jollyday/HolidayCalendar.java
|
||||
financer.countryCode=DE
|
||||
|
||||
# The state used for holiday checks
|
||||
# For a complete list of the supported states see e.g. https://github.com/svendiedrichsen/jollyday/blob/master/src/main/resources/holidays/Holidays_de.xml
|
||||
financer.state=sl
|
||||
@@ -1,8 +1,8 @@
|
||||
--
|
||||
-- This file contains the basic initialization of the financer schema and init data
|
||||
-- This file contains the basic initialization of the financer schema
|
||||
--
|
||||
|
||||
-- Account table and init data
|
||||
-- Account table
|
||||
CREATE TABLE account (
|
||||
id BIGINT NOT NULL PRIMARY KEY IDENTITY,
|
||||
"key" VARCHAR(1000) NOT NULL, --escape keyword "key"
|
||||
@@ -13,18 +13,6 @@ CREATE TABLE account (
|
||||
CONSTRAINT un_account_name_key UNIQUE ("key")
|
||||
);
|
||||
|
||||
INSERT INTO account ("key", type, status, current_balance)
|
||||
VALUES ('accounts.checkaccount', 'BANK', 'OPEN', 0);
|
||||
|
||||
INSERT INTO account ("key", type, status, current_balance)
|
||||
VALUES ('accounts.income', 'INCOME', 'OPEN', 0);
|
||||
|
||||
INSERT INTO account ("key", type, status, current_balance)
|
||||
VALUES ('accounts.cash', 'CASH', 'OPEN', 0);
|
||||
|
||||
INSERT INTO account ("key", type, status, current_balance)
|
||||
VALUES ('accounts.start', 'START', 'OPEN', 0);
|
||||
|
||||
-- Recurring transaction table
|
||||
CREATE TABLE recurring_transaction (
|
||||
id BIGINT NOT NULL PRIMARY KEY IDENTITY,
|
||||
|
||||
@@ -36,12 +36,14 @@ public class AccountControllerIntegrationTest {
|
||||
|
||||
@Test
|
||||
public void test_getAll() throws Exception {
|
||||
final MvcResult mvcResult = this.mockMvc.perform(get("/accounts/getAll").contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
final MvcResult mvcResult = this.mockMvc
|
||||
.perform(get("/accounts/getAll").contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
|
||||
final List<Account> allAccounts = this.objectMapper.readValue(mvcResult.getResponse().getContentAsByteArray(), new TypeReference<List<Account>>(){});
|
||||
final List<Account> allAccounts = this.objectMapper
|
||||
.readValue(mvcResult.getResponse().getContentAsByteArray(), new TypeReference<List<Account>>() {});
|
||||
|
||||
Assert.assertEquals(4, allAccounts.size());
|
||||
Assert.assertEquals(5, allAccounts.size());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
package de.financer.service;
|
||||
|
||||
import de.financer.dba.RecurringTransactionRepository;
|
||||
import de.financer.model.HolidayWeekendType;
|
||||
import de.financer.model.IntervalType;
|
||||
import de.financer.model.RecurringTransaction;
|
||||
import org.apache.commons.collections4.IterableUtils;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
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.time.LocalDate;
|
||||
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).
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest {
|
||||
@InjectMocks
|
||||
private RecurringTransactionService classUnderTest;
|
||||
|
||||
@Mock
|
||||
private RecurringTransactionRepository recurringTransactionRepository;
|
||||
|
||||
@Mock
|
||||
private RuleService ruleService;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
Mockito.when(this.ruleService.getPeriodForInterval(IntervalType.DAILY)).thenReturn(Period.ofDays(1));
|
||||
}
|
||||
|
||||
/**
|
||||
* This method tests whether a recurring transaction with firstOccurrence = three days ago, intervalType = daily and
|
||||
* holidayWeekendType = next_workday is due on a non-holiday, non-weekend day
|
||||
*/
|
||||
@Test
|
||||
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)));
|
||||
final LocalDate now = LocalDate.now();
|
||||
|
||||
// Act
|
||||
final Iterable<RecurringTransaction> recurringDueToday = this.classUnderTest.getAllDueToday(now);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(1, IterableUtils.size(recurringDueToday));
|
||||
}
|
||||
|
||||
/**
|
||||
* This method tests whether a recurring transaction with firstOccurrence = today, intervalType = daily and
|
||||
* holidayWeekendType = next_workday is <b>not</b> due on a holiday, non-weekend day
|
||||
*/
|
||||
@Test
|
||||
public void test_getAllDueToday_dueToday_holiday() {
|
||||
// Arrange
|
||||
// Implicitly: ruleService.isWeekend().return(false)
|
||||
Mockito.when(this.recurringTransactionRepository.findAll()).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();
|
||||
|
||||
// Act
|
||||
final Iterable<RecurringTransaction> recurringDueToday = this.classUnderTest.getAllDueToday(now);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(0, IterableUtils.size(recurringDueToday));
|
||||
}
|
||||
|
||||
/**
|
||||
* This method tests whether a recurring transaction with firstOccurrence = today, intervalType = daily and
|
||||
* holidayWeekendType = next_workday is <b>not</b> due on a non-holiday, weekend day
|
||||
*/
|
||||
@Test
|
||||
public void test_getAllDueToday_dueToday_weekend() {
|
||||
// Arrange
|
||||
// Implicitly: ruleService.isHoliday().return(false)
|
||||
Mockito.when(this.recurringTransactionRepository.findAll()).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();
|
||||
|
||||
// Act
|
||||
final Iterable<RecurringTransaction> recurringDueToday = this.classUnderTest.getAllDueToday(now);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(0, IterableUtils.size(recurringDueToday));
|
||||
}
|
||||
|
||||
/**
|
||||
* This method tests whether a recurring transaction with firstOccurrence = tomorrow, intervalType = daily and
|
||||
* holidayWeekendType = next_workday is <b>not</b> due today
|
||||
*/
|
||||
@Test
|
||||
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)));
|
||||
final LocalDate now = LocalDate.now();
|
||||
|
||||
// Act
|
||||
final Iterable<RecurringTransaction> recurringDueToday = this.classUnderTest.getAllDueToday(now);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(0, IterableUtils.size(recurringDueToday));
|
||||
}
|
||||
|
||||
private RecurringTransaction createRecurringTransaction(int days) {
|
||||
final RecurringTransaction recurringTransaction = new RecurringTransaction();
|
||||
|
||||
recurringTransaction.setFirstOccurrence(LocalDate.now().plusDays(days));
|
||||
|
||||
recurringTransaction.setHolidayWeekendType(HolidayWeekendType.NEXT_WORKDAY);
|
||||
recurringTransaction.setIntervalType(IntervalType.DAILY);
|
||||
|
||||
return recurringTransaction;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package de.financer.service;
|
||||
|
||||
import de.financer.dba.RecurringTransactionRepository;
|
||||
import de.financer.model.HolidayWeekendType;
|
||||
import de.financer.model.IntervalType;
|
||||
import de.financer.model.RecurringTransaction;
|
||||
import org.apache.commons.collections4.IterableUtils;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
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.time.LocalDate;
|
||||
import java.time.Period;
|
||||
import java.util.Collections;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest {
|
||||
@InjectMocks
|
||||
private RecurringTransactionService classUnderTest;
|
||||
|
||||
@Mock
|
||||
private RecurringTransactionRepository recurringTransactionRepository;
|
||||
|
||||
@Mock
|
||||
private RuleService ruleService;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
Mockito.when(this.ruleService.getPeriodForInterval(IntervalType.MONTHLY)).thenReturn(Period.ofMonths(1));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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())
|
||||
.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);
|
||||
final LocalDate now = LocalDate.now();
|
||||
|
||||
// Act
|
||||
final Iterable<RecurringTransaction> recurringDueToday = this.classUnderTest.getAllDueToday(now);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(1, IterableUtils.size(recurringDueToday));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
//@formatter:off
|
||||
// MO TU WE TH FR SA SU -> Weekdays
|
||||
// 1 2 3 4 5 6 7 -> Ordinal
|
||||
// H WE WE -> Holiday/WeekEnd
|
||||
// X -> Scheduled recurring transaction
|
||||
// O -> now
|
||||
//
|
||||
// So now - (ordinal +- offset)
|
||||
// now - (3 - 1) = previous MO
|
||||
// now - 3 = previous SU
|
||||
// now - (3 + 2) = previous FR
|
||||
//@formatter:on
|
||||
|
||||
// Arrange
|
||||
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())
|
||||
.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()))
|
||||
.thenReturn(Boolean.FALSE, Boolean.TRUE, Boolean.TRUE, Boolean.FALSE);
|
||||
// First False for the dueToday check, 2x False for actual weekend, True for Friday
|
||||
Mockito.when(this.ruleService.isHoliday(Mockito.any()))
|
||||
.thenReturn(Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, Boolean.TRUE);
|
||||
|
||||
// Act
|
||||
final Iterable<RecurringTransaction> recurringDueToday = this.classUnderTest.getAllDueToday(monday);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(1, IterableUtils.size(recurringDueToday));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
// Arrange
|
||||
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())
|
||||
.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()))
|
||||
.thenReturn(Boolean.FALSE, Boolean.TRUE, Boolean.TRUE, Boolean.FALSE);
|
||||
|
||||
// Act
|
||||
final Iterable<RecurringTransaction> recurringDueToday = this.classUnderTest.getAllDueToday(monday);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(1, IterableUtils.size(recurringDueToday));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
// Arrange
|
||||
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())
|
||||
.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()))
|
||||
.thenReturn(Boolean.FALSE, Boolean.TRUE, Boolean.TRUE, Boolean.FALSE);
|
||||
|
||||
// Act
|
||||
final Iterable<RecurringTransaction> recurringDueToday = this.classUnderTest.getAllDueToday(monday);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(1, IterableUtils.size(recurringDueToday));
|
||||
}
|
||||
|
||||
private RecurringTransaction createRecurringTransaction(int days) {
|
||||
final RecurringTransaction recurringTransaction = new RecurringTransaction();
|
||||
|
||||
recurringTransaction.setFirstOccurrence(LocalDate.now().plusDays(days).minusMonths(1));
|
||||
|
||||
recurringTransaction.setHolidayWeekendType(HolidayWeekendType.NEXT_WORKDAY);
|
||||
recurringTransaction.setIntervalType(IntervalType.MONTHLY);
|
||||
|
||||
return recurringTransaction;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package de.financer.service;
|
||||
|
||||
import de.financer.ResponseReason;
|
||||
import de.financer.dba.TransactionRepository;
|
||||
import de.financer.model.Account;
|
||||
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;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class TransactionService_createTransactionTest {
|
||||
@InjectMocks
|
||||
private TransactionService classUnderTest;
|
||||
|
||||
@Mock
|
||||
private AccountService accountService;
|
||||
|
||||
@Mock
|
||||
private RuleService ruleService;
|
||||
|
||||
@Mock
|
||||
private TransactionRepository transactionRepository;
|
||||
|
||||
@Test
|
||||
public void test_createTransaction_FROM_AND_TO_ACCOUNT_NOT_FOUND() {
|
||||
// Arrange
|
||||
// Nothing to do, if we do not instruct the account service instance to return anything the accounts
|
||||
// will not be found.
|
||||
|
||||
// Act
|
||||
final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.invalid", Long.valueOf(150l), "24.02.2019", "XXX");
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.FROM_AND_TO_ACCOUNT_NOT_FOUND, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_createTransaction_TO_ACCOUNT_NOT_FOUND() {
|
||||
// Arrange
|
||||
Mockito.when(this.accountService.getAccountByKey(Mockito.anyString())).thenReturn(createAccount(), null);
|
||||
|
||||
// Act
|
||||
final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.invalid", Long.valueOf(150l), "24.02.2019", "XXX");
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.TO_ACCOUNT_NOT_FOUND, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_createTransaction_FROM_ACCOUNT_NOT_FOUND() {
|
||||
// Arrange
|
||||
Mockito.when(this.accountService.getAccountByKey(Mockito.anyString())).thenReturn(null, createAccount());
|
||||
|
||||
// Act
|
||||
final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.invalid", Long.valueOf(150l), "24.02.2019", "XXX");
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.FROM_ACCOUNT_NOT_FOUND, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_createTransaction_INVALID_BOOKING_ACCOUNTS() {
|
||||
// Arrange
|
||||
Mockito.when(this.accountService.getAccountByKey(Mockito.anyString())).thenReturn(createAccount(), createAccount());
|
||||
Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.FALSE);
|
||||
|
||||
// Act
|
||||
final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.invalid", Long.valueOf(150l), "24.02.2019", "XXX");
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.INVALID_BOOKING_ACCOUNTS, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_createTransaction_MISSING_AMOUNT() {
|
||||
// Arrange
|
||||
Mockito.when(this.accountService.getAccountByKey(Mockito.anyString())).thenReturn(createAccount(), createAccount());
|
||||
Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.TRUE);
|
||||
|
||||
// Act
|
||||
final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.invalid", null, "24.02.2019", "XXX");
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.MISSING_AMOUNT, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_createTransaction_AMOUNT_ZERO() {
|
||||
// Arrange
|
||||
Mockito.when(this.accountService.getAccountByKey(Mockito.anyString())).thenReturn(createAccount(), createAccount());
|
||||
Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.TRUE);
|
||||
|
||||
// Act
|
||||
final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.invalid", Long.valueOf(0l), "24.02.2019", "XXX");
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.AMOUNT_ZERO, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_createTransaction_MISSING_DATE() {
|
||||
// Arrange
|
||||
Mockito.when(this.accountService.getAccountByKey(Mockito.anyString())).thenReturn(createAccount(), createAccount());
|
||||
Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.TRUE);
|
||||
|
||||
// Act
|
||||
final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.invalid", Long.valueOf(125l), null, "XXX");
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.MISSING_DATE, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_createTransaction_INVALID_DATE_FORMAT() {
|
||||
// Arrange
|
||||
Mockito.when(this.accountService.getAccountByKey(Mockito.anyString())).thenReturn(createAccount(), createAccount());
|
||||
Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.TRUE);
|
||||
|
||||
// Act
|
||||
final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.invalid", Long.valueOf(125l), "2019-01-01", "XXX");
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.INVALID_DATE_FORMAT, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_createTransaction_OK() {
|
||||
// Arrange
|
||||
final Account fromAccount = Mockito.mock(Account.class);
|
||||
final Account toAccount = Mockito.mock(Account.class);
|
||||
Mockito.when(this.accountService.getAccountByKey(Mockito.anyString())).thenReturn(fromAccount, toAccount);
|
||||
Mockito.when(this.ruleService.isValidBooking(Mockito.any(), Mockito.any())).thenReturn(Boolean.TRUE);
|
||||
Mockito.when(this.ruleService.getMultiplierFromAccount(Mockito.any())).thenReturn(Long.valueOf(-1l));
|
||||
Mockito.when(this.ruleService.getMultiplierToAccount(Mockito.any())).thenReturn(Long.valueOf(1l));
|
||||
Mockito.when(fromAccount.getCurrentBalance()).thenReturn(Long.valueOf(0l));
|
||||
Mockito.when(toAccount.getCurrentBalance()).thenReturn(Long.valueOf(0l));
|
||||
|
||||
// Act
|
||||
final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.invalid", Long.valueOf(125l), "24.02.2019", "XXX");
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.OK, response);
|
||||
Mockito.verify(fromAccount, Mockito.times(1)).setCurrentBalance(Long.valueOf(-125));
|
||||
Mockito.verify(toAccount, Mockito.times(1)).setCurrentBalance(Long.valueOf(125));
|
||||
}
|
||||
|
||||
public Account createAccount() {
|
||||
final Account account = new Account();
|
||||
|
||||
account.setCurrentBalance(Long.valueOf(0l));
|
||||
|
||||
return account;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,3 @@
|
||||
###
|
||||
### This is the main configuration file for integration tests
|
||||
###
|
||||
|
||||
server.servlet.context-path=/financer
|
||||
|
||||
spring.jpa.hibernate.ddl-auto=validate
|
||||
|
||||
spring.datasource.url=jdbc:hsqldb:mem:.
|
||||
spring.datasource.username=sa
|
||||
spring.flyway.locations=classpath:/database/hsqldb
|
||||
spring.flyway.locations=classpath:/database/hsqldb,classpath:/database/hsqldb/integration
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
-- Accounts
|
||||
INSERT INTO account (id, "key", type, status, current_balance)
|
||||
VALUES (1, 'accounts.checkaccount', 'BANK', 'OPEN', 0); -- insert first with ID 1 so we get predictable numbering
|
||||
|
||||
INSERT INTO account ("key", type, status, current_balance)
|
||||
VALUES ('accounts.income', 'INCOME', 'OPEN', 0);
|
||||
|
||||
INSERT INTO account ("key", type, status, current_balance)
|
||||
VALUES ('accounts.cash', 'CASH', 'OPEN', 0);
|
||||
|
||||
INSERT INTO account ("key", type, status, current_balance)
|
||||
VALUES ('accounts.start', 'START', 'OPEN', 0);
|
||||
|
||||
INSERT INTO account ("key", type, status, current_balance)
|
||||
VALUES ('accounts.convenience', 'EXPENSE', 'OPEN', 0);
|
||||
|
||||
--Recurring transactions
|
||||
INSERT INTO recurring_transaction (from_account_id, to_account_id, description, amount, interval_type, first_occurrence, holiday_weekend_type)
|
||||
VALUES (2, 1, '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 (3, 5, 'Pretzel', 170, 'DAILY', '2019-02-20', 'SAME_DAY');
|
||||
Reference in New Issue
Block a user