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>
|
<properties>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<maven.compiler.source>1.8</maven.compiler.source>
|
<maven.compiler.source>1.9</maven.compiler.source>
|
||||||
<maven.compiler.target>1.8</maven.compiler.target>
|
<maven.compiler.target>1.9</maven.compiler.target>
|
||||||
<java.version>1.8</java.version>
|
<java.version>1.9</java.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
@@ -46,7 +46,17 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.commons</groupId>
|
<groupId>org.apache.commons</groupId>
|
||||||
<artifactId>commons-lang3</artifactId>
|
<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>
|
</dependency>
|
||||||
|
|
||||||
<!-- Runtime dependencies -->
|
<!-- Runtime dependencies -->
|
||||||
@@ -99,6 +109,19 @@
|
|||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</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>
|
<profiles>
|
||||||
<profile>
|
<profile>
|
||||||
<id>integration-tests</id>
|
<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);
|
return this.transactionService.getAllForAccount(accountKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping("createTransaction")
|
@RequestMapping(value = "createTransaction")
|
||||||
public ResponseEntity createTransaction(String fromAccountKey, String toAccountKey, Long amount, String date, String description) {
|
public ResponseEntity createTransaction(String fromAccountKey, String toAccountKey, Long amount, String date,
|
||||||
return this.transactionService.createTransaction(fromAccountKey, toAccountKey, amount, date, description).toResponseEntity();
|
String description) {
|
||||||
|
return this.transactionService.createTransaction(fromAccountKey, toAccountKey, amount, date, description)
|
||||||
|
.toResponseEntity();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
package de.financer.dba;
|
package de.financer.dba;
|
||||||
|
|
||||||
|
import de.financer.model.Account;
|
||||||
import de.financer.model.RecurringTransaction;
|
import de.financer.model.RecurringTransaction;
|
||||||
import org.springframework.data.repository.CrudRepository;
|
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> {
|
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 */
|
/** Indicates that the action should be done on the specified day regardless whether it's a holiday or a weekend */
|
||||||
SAME_DAY,
|
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,
|
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;
|
PREVIOUS_WORKDAY;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package de.financer.model;
|
package de.financer.model;
|
||||||
|
|
||||||
import javax.persistence.*;
|
import javax.persistence.*;
|
||||||
import java.util.Date;
|
import java.time.LocalDate;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
public class RecurringTransaction {
|
public class RecurringTransaction {
|
||||||
@@ -16,10 +16,8 @@ public class RecurringTransaction {
|
|||||||
private Long amount;
|
private Long amount;
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
private IntervalType intervalType;
|
private IntervalType intervalType;
|
||||||
@Temporal(TemporalType.DATE)
|
private LocalDate firstOccurrence;
|
||||||
private Date firstOccurrence;
|
private LocalDate lastOccurrence;
|
||||||
@Temporal(TemporalType.DATE)
|
|
||||||
private Date lastOccurrence;
|
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
private HolidayWeekendType holidayWeekendType;
|
private HolidayWeekendType holidayWeekendType;
|
||||||
|
|
||||||
@@ -67,19 +65,19 @@ public class RecurringTransaction {
|
|||||||
this.holidayWeekendType = holidayWeekendType;
|
this.holidayWeekendType = holidayWeekendType;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Date getLastOccurrence() {
|
public LocalDate getLastOccurrence() {
|
||||||
return lastOccurrence;
|
return lastOccurrence;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setLastOccurrence(Date lastOccurrence) {
|
public void setLastOccurrence(LocalDate lastOccurrence) {
|
||||||
this.lastOccurrence = lastOccurrence;
|
this.lastOccurrence = lastOccurrence;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Date getFirstOccurrence() {
|
public LocalDate getFirstOccurrence() {
|
||||||
return firstOccurrence;
|
return firstOccurrence;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setFirstOccurrence(Date firstOccurrence) {
|
public void setFirstOccurrence(LocalDate firstOccurrence) {
|
||||||
this.firstOccurrence = firstOccurrence;
|
this.firstOccurrence = firstOccurrence;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package de.financer.model;
|
package de.financer.model;
|
||||||
|
|
||||||
import javax.persistence.*;
|
import javax.persistence.*;
|
||||||
import java.util.Date;
|
import java.time.LocalDate;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "\"transaction\"")
|
@Table(name = "\"transaction\"")
|
||||||
@@ -13,9 +13,8 @@ public class Transaction {
|
|||||||
private Account fromAccount;
|
private Account fromAccount;
|
||||||
@OneToOne(fetch = FetchType.EAGER)
|
@OneToOne(fetch = FetchType.EAGER)
|
||||||
private Account toAccount;
|
private Account toAccount;
|
||||||
@Temporal(TemporalType.DATE)
|
|
||||||
@Column(name = "\"date\"")
|
@Column(name = "\"date\"")
|
||||||
private Date date;
|
private LocalDate date;
|
||||||
private String description;
|
private String description;
|
||||||
private Long amount;
|
private Long amount;
|
||||||
@ManyToOne(fetch = FetchType.EAGER)
|
@ManyToOne(fetch = FetchType.EAGER)
|
||||||
@@ -41,11 +40,11 @@ public class Transaction {
|
|||||||
this.toAccount = toAccount;
|
this.toAccount = toAccount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Date getDate() {
|
public LocalDate getDate() {
|
||||||
return date;
|
return date;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDate(Date date) {
|
public void setDate(LocalDate date) {
|
||||||
this.date = 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;
|
package de.financer.service;
|
||||||
|
|
||||||
|
import de.financer.config.FinancerConfig;
|
||||||
import de.financer.model.Account;
|
import de.financer.model.Account;
|
||||||
import de.financer.model.AccountType;
|
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.InitializingBean;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.DayOfWeek;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.Period;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
import static de.financer.model.AccountType.*;
|
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
|
* 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
|
@Service
|
||||||
public class RuleService implements InitializingBean {
|
public class RuleService implements InitializingBean {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private FinancerConfig financerConfig;
|
||||||
|
|
||||||
private Map<AccountType, Collection<AccountType>> bookingRules;
|
private Map<AccountType, Collection<AccountType>> bookingRules;
|
||||||
|
private Map<IntervalType, Period> intervalPeriods;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterPropertiesSet() {
|
public void afterPropertiesSet() {
|
||||||
initBookingRules();
|
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() {
|
private void initBookingRules() {
|
||||||
@@ -40,7 +64,7 @@ public class RuleService implements InitializingBean {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This method returns the multiplier for the given from account.
|
* 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
|
* 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.
|
* decreased depending on the {@link AccountType} of the given account.
|
||||||
*
|
*
|
||||||
@@ -55,17 +79,13 @@ public class RuleService implements InitializingBean {
|
|||||||
|
|
||||||
if (INCOME.equals(accountType)) {
|
if (INCOME.equals(accountType)) {
|
||||||
return 1L;
|
return 1L;
|
||||||
}
|
} else if (BANK.equals(accountType)) {
|
||||||
else if (BANK.equals(accountType)) {
|
|
||||||
return -1L;
|
return -1L;
|
||||||
}
|
} else if (CASH.equals(accountType)) {
|
||||||
else if (CASH.equals(accountType)) {
|
|
||||||
return -1L;
|
return -1L;
|
||||||
}
|
} else if (LIABILITY.equals(accountType)) {
|
||||||
else if (LIABILITY.equals(accountType)) {
|
|
||||||
return 1L;
|
return 1L;
|
||||||
}
|
} else if (START.equals(accountType)) {
|
||||||
else if (START.equals(accountType)) {
|
|
||||||
return 1L;
|
return 1L;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +94,7 @@ public class RuleService implements InitializingBean {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This method returns the multiplier for the given to account.
|
* 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
|
* 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.
|
* decreased depending on the {@link AccountType} of the given account.
|
||||||
*
|
*
|
||||||
@@ -89,14 +109,11 @@ public class RuleService implements InitializingBean {
|
|||||||
|
|
||||||
if (BANK.equals(accountType)) {
|
if (BANK.equals(accountType)) {
|
||||||
return 1L;
|
return 1L;
|
||||||
}
|
} else if (CASH.equals(accountType)) {
|
||||||
else if (CASH.equals(accountType)) {
|
|
||||||
return 1L;
|
return 1L;
|
||||||
}
|
} else if (LIABILITY.equals(accountType)) {
|
||||||
else if (LIABILITY.equals(accountType)) {
|
|
||||||
return -1L;
|
return -1L;
|
||||||
}
|
} else if (EXPENSE.equals(accountType)) {
|
||||||
else if (EXPENSE.equals(accountType)) {
|
|
||||||
return 1L;
|
return 1L;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,10 +126,43 @@ public class RuleService implements InitializingBean {
|
|||||||
* account does not make sense and is declared as invalid.
|
* account does not make sense and is declared as invalid.
|
||||||
*
|
*
|
||||||
* @param fromAccount the account to subtract the money from
|
* @param fromAccount the account to subtract the money from
|
||||||
* @param toAccount the account to add the money to
|
* @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
|
* @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) {
|
public boolean isValidBooking(Account fromAccount, Account toAccount) {
|
||||||
return this.bookingRules.get(fromAccount.getType()).contains(toAccount.getType());
|
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.Propagation;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.text.ParseException;
|
import java.time.LocalDate;
|
||||||
import java.text.SimpleDateFormat;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.format.DateTimeParseException;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -36,7 +37,7 @@ public class TransactionService {
|
|||||||
/**
|
/**
|
||||||
* @param accountKey the key of the account to get the transactions for
|
* @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
|
* @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) {
|
public Iterable<Transaction> getAllForAccount(String accountKey) {
|
||||||
final Account account = this.accountService.getAccountByKey(accountKey);
|
final Account account = this.accountService.getAccountByKey(accountKey);
|
||||||
@@ -63,8 +64,10 @@ public class TransactionService {
|
|||||||
try {
|
try {
|
||||||
final Transaction transaction = buildTransaction(fromAccount, toAccount, amount, description, date);
|
final Transaction transaction = buildTransaction(fromAccount, toAccount, amount, description, date);
|
||||||
|
|
||||||
fromAccount.setCurrentBalance(fromAccount.getCurrentBalance() + (this.ruleService.getMultiplierFromAccount(fromAccount) * amount));
|
fromAccount.setCurrentBalance(fromAccount.getCurrentBalance() + (this.ruleService
|
||||||
toAccount.setCurrentBalance(toAccount.getCurrentBalance() + (this.ruleService.getMultiplierToAccount(toAccount) * amount));
|
.getMultiplierFromAccount(fromAccount) * amount));
|
||||||
|
toAccount.setCurrentBalance(toAccount.getCurrentBalance() + (this.ruleService
|
||||||
|
.getMultiplierToAccount(toAccount) * amount));
|
||||||
|
|
||||||
this.transactionRepository.save(transaction);
|
this.transactionRepository.save(transaction);
|
||||||
|
|
||||||
@@ -72,13 +75,11 @@ public class TransactionService {
|
|||||||
this.accountService.saveAccount(toAccount);
|
this.accountService.saveAccount(toAccount);
|
||||||
|
|
||||||
response = ResponseReason.OK;
|
response = ResponseReason.OK;
|
||||||
}
|
} catch (DateTimeParseException e) {
|
||||||
catch(ParseException e) {
|
|
||||||
// TODO log
|
// TODO log
|
||||||
|
|
||||||
response = ResponseReason.INVALID_DATE_FORMAT;
|
response = ResponseReason.INVALID_DATE_FORMAT;
|
||||||
}
|
} catch (Exception e) {
|
||||||
catch (Exception e) {
|
|
||||||
// TODO log
|
// TODO log
|
||||||
|
|
||||||
response = ResponseReason.UNKNOWN_ERROR;
|
response = ResponseReason.UNKNOWN_ERROR;
|
||||||
@@ -91,21 +92,21 @@ public class TransactionService {
|
|||||||
* This method builds the actual transaction object with the given values.
|
* This method builds the actual transaction object with the given values.
|
||||||
*
|
*
|
||||||
* @param fromAccount the from account
|
* @param fromAccount the from account
|
||||||
* @param toAccount the to account
|
* @param toAccount the to account
|
||||||
* @param amount the transaction amount
|
* @param amount the transaction amount
|
||||||
* @param description the description of the transaction
|
* @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
|
* @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();
|
final Transaction transaction = new Transaction();
|
||||||
|
|
||||||
transaction.setFromAccount(fromAccount);
|
transaction.setFromAccount(fromAccount);
|
||||||
transaction.setToAccount(toAccount);
|
transaction.setToAccount(toAccount);
|
||||||
transaction.setAmount(amount);
|
transaction.setAmount(amount);
|
||||||
transaction.setDescription(description);
|
transaction.setDescription(description);
|
||||||
transaction.setDate(new SimpleDateFormat(DATE_FORMAT).parse(date));
|
transaction.setDate(LocalDate.parse(date, DateTimeFormatter.ofPattern(DATE_FORMAT)));
|
||||||
|
|
||||||
return transaction;
|
return transaction;
|
||||||
}
|
}
|
||||||
@@ -114,9 +115,9 @@ public class TransactionService {
|
|||||||
* This method checks whether the parameters for creating a transaction are valid.
|
* This method checks whether the parameters for creating a transaction are valid.
|
||||||
*
|
*
|
||||||
* @param fromAccount the from account
|
* @param fromAccount the from account
|
||||||
* @param toAccount the to account
|
* @param toAccount the to account
|
||||||
* @param amount the transaction amount
|
* @param amount the transaction amount
|
||||||
* @param date the transaction date
|
* @param date the transaction date
|
||||||
* @return the first error found or <code>null</code> if all parameters are valid
|
* @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) {
|
private ResponseReason validateParameters(Account fromAccount, Account toAccount, Long amount, String date) {
|
||||||
@@ -124,23 +125,17 @@ public class TransactionService {
|
|||||||
|
|
||||||
if (fromAccount == null && toAccount == null) {
|
if (fromAccount == null && toAccount == null) {
|
||||||
response = ResponseReason.FROM_AND_TO_ACCOUNT_NOT_FOUND;
|
response = ResponseReason.FROM_AND_TO_ACCOUNT_NOT_FOUND;
|
||||||
}
|
} else if (toAccount == null) {
|
||||||
else if (toAccount == null) {
|
|
||||||
response = ResponseReason.TO_ACCOUNT_NOT_FOUND;
|
response = ResponseReason.TO_ACCOUNT_NOT_FOUND;
|
||||||
}
|
} else if (fromAccount == null) {
|
||||||
else if (fromAccount == null) {
|
|
||||||
response = ResponseReason.FROM_ACCOUNT_NOT_FOUND;
|
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;
|
response = ResponseReason.INVALID_BOOKING_ACCOUNTS;
|
||||||
}
|
} else if (amount == null) {
|
||||||
else if (amount == null) {
|
|
||||||
response = ResponseReason.MISSING_AMOUNT;
|
response = ResponseReason.MISSING_AMOUNT;
|
||||||
}
|
} else if (amount == 0l) {
|
||||||
else if (amount == 0l) {
|
|
||||||
response = ResponseReason.AMOUNT_ZERO;
|
response = ResponseReason.AMOUNT_ZERO;
|
||||||
}
|
} else if (date == null) {
|
||||||
else if (date == null) {
|
|
||||||
response = ResponseReason.MISSING_DATE;
|
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.app.description=A simple server for personal finance administration
|
||||||
info.build.group=@project.groupId@
|
info.build.group=@project.groupId@
|
||||||
info.build.artifact=@project.artifactId@
|
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 (
|
CREATE TABLE account (
|
||||||
id BIGINT NOT NULL PRIMARY KEY IDENTITY,
|
id BIGINT NOT NULL PRIMARY KEY IDENTITY,
|
||||||
"key" VARCHAR(1000) NOT NULL, --escape keyword "key"
|
"key" VARCHAR(1000) NOT NULL, --escape keyword "key"
|
||||||
@@ -13,18 +13,6 @@ CREATE TABLE account (
|
|||||||
CONSTRAINT un_account_name_key UNIQUE ("key")
|
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
|
-- Recurring transaction table
|
||||||
CREATE TABLE recurring_transaction (
|
CREATE TABLE recurring_transaction (
|
||||||
id BIGINT NOT NULL PRIMARY KEY IDENTITY,
|
id BIGINT NOT NULL PRIMARY KEY IDENTITY,
|
||||||
|
|||||||
@@ -36,12 +36,14 @@ public class AccountControllerIntegrationTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void test_getAll() throws Exception {
|
public void test_getAll() throws Exception {
|
||||||
final MvcResult mvcResult = this.mockMvc.perform(get("/accounts/getAll").contentType(MediaType.APPLICATION_JSON))
|
final MvcResult mvcResult = this.mockMvc
|
||||||
.andExpect(status().isOk())
|
.perform(get("/accounts/getAll").contentType(MediaType.APPLICATION_JSON))
|
||||||
.andReturn();
|
.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.url=jdbc:hsqldb:mem:.
|
||||||
spring.datasource.username=sa
|
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