Merge financer-server into financer-parent
This commit is contained in:
3
financer-server/.gitignore
vendored
Normal file
3
financer-server/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
financer-server.log*
|
||||
.attach*
|
||||
*.iml
|
||||
36
financer-server/doc/README
Normal file
36
financer-server/doc/README
Normal file
@@ -0,0 +1,36 @@
|
||||
___ _
|
||||
/ __(_)_ __ __ _ _ __ ___ ___ _ __
|
||||
/ _\ | | '_ \ / _` | '_ \ / __/ _ \ '__|
|
||||
/ / | | | | | (_| | | | | (_| __/ |
|
||||
\/ |_|_| |_|\__,_|_| |_|\___\___|_|
|
||||
|
||||
|
||||
1. About
|
||||
2. Content
|
||||
3. Overview
|
||||
4. Architectural overview
|
||||
5. Account types
|
||||
6. Booking rules
|
||||
7. Setup
|
||||
|
||||
|
||||
7. Setup
|
||||
========
|
||||
This chapter explains how to setup a financer instance. It requires PostgreSQL as a database backend and a Java
|
||||
Servlet Container (e.g. Apache Tomcat) as a runtime environment.
|
||||
|
||||
7.1 Database setup
|
||||
------------------
|
||||
First install PostgreSQL. Then create a user for financer:
|
||||
sudo -iu postgres
|
||||
createuser -P -s -e financer
|
||||
This creates a user named 'financer' and prompts for the creation of a password for this user. The expected default
|
||||
password is 'financer'. Then create the actual database:
|
||||
createdb financer
|
||||
Using 'financer' for the name of the user, its password and the database name is the expected default. If you want
|
||||
any other values you need to adjust the database connection settings of the financer application.
|
||||
Then you need to grant the created user permission to the created database:
|
||||
psql
|
||||
GRANT ALL PRIVILEGES ON DATABASE "financer" to financer;
|
||||
\q
|
||||
exit
|
||||
124
financer-server/pom.xml
Normal file
124
financer-server/pom.xml
Normal file
@@ -0,0 +1,124 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>de.77zzcx7.financer</groupId>
|
||||
<artifactId>financer-parent</artifactId>
|
||||
<version>9-SNAPSHOT</version>
|
||||
<relativePath>../financer-parent</relativePath>
|
||||
</parent>
|
||||
|
||||
<groupId>de.77zzcx7.financer</groupId>
|
||||
<artifactId>financer-server</artifactId>
|
||||
<packaging>${packaging.type}</packaging>
|
||||
<description>The server part of the financer application - a simple app to manage your personal finances</description>
|
||||
<name>financer-server</name>
|
||||
|
||||
<properties>
|
||||
<packaging.type>jar</packaging.type>
|
||||
<activeProfiles>hsqldb,dev</activeProfiles>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-mail</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-core</artifactId>
|
||||
</dependency>
|
||||
<!-- Apache commons -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-collections4</artifactId>
|
||||
</dependency>
|
||||
<!-- Misc dependencies -->
|
||||
<dependency>
|
||||
<groupId>de.jollyday</groupId>
|
||||
<artifactId>jollyday</artifactId>
|
||||
<version>0.5.7</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jaxb</groupId>
|
||||
<artifactId>jaxb-runtime</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Runtime dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.hsqldb</groupId>
|
||||
<artifactId>hsqldb</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Test dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.restdocs</groupId>
|
||||
<artifactId>spring-restdocs-mockmvc</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<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>build-war</id>
|
||||
<properties>
|
||||
<packaging.type>war</packaging.type>
|
||||
<activeProfiles>postgres</activeProfiles>
|
||||
</properties>
|
||||
<build>
|
||||
<finalName>${project.artifactId}##${parallelDeploymentVersion}</finalName>
|
||||
</build>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-tomcat</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
@@ -0,0 +1,20 @@
|
||||
package de.financer;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableScheduling
|
||||
public class FinancerApplication extends SpringBootServletInitializer {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(FinancerApplication.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
|
||||
return application.sources(FinancerApplication.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package de.financer;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
public enum ResponseReason {
|
||||
OK(HttpStatus.OK),
|
||||
UNKNOWN_ERROR(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
INVALID_ACCOUNT_TYPE(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
FROM_ACCOUNT_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
TO_ACCOUNT_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
FROM_AND_TO_ACCOUNT_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
INVALID_DATE_FORMAT(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
MISSING_DATE(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
AMOUNT_ZERO(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
MISSING_AMOUNT(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
INVALID_BOOKING_ACCOUNTS(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
MISSING_HOLIDAY_WEEKEND_TYPE(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
INVALID_HOLIDAY_WEEKEND_TYPE(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
MISSING_INTERVAL_TYPE(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
INVALID_INTERVAL_TYPE(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
MISSING_FIRST_OCCURRENCE(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
INVALID_FIRST_OCCURRENCE_FORMAT(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
INVALID_LAST_OCCURRENCE_FORMAT(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
MISSING_RECURRING_TRANSACTION_ID(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
INVALID_RECURRING_TRANSACTION_ID(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
RECURRING_TRANSACTION_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
MISSING_TRANSACTION_ID(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
INVALID_TRANSACTION_ID(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
TRANSACTION_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
ACCOUNT_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
DUPLICATE_ACCOUNT_KEY(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
DUPLICATE_ACCOUNT_GROUP_NAME(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
ACCOUNT_GROUP_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
|
||||
private HttpStatus httpStatus;
|
||||
|
||||
ResponseReason(HttpStatus httpStatus) {
|
||||
this.httpStatus = httpStatus;
|
||||
}
|
||||
|
||||
public ResponseEntity toResponseEntity() {
|
||||
return new ResponseEntity<>(this.name(), this.httpStatus);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package de.financer.config;
|
||||
|
||||
import de.jollyday.HolidayCalendar;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "financer")
|
||||
public class FinancerConfig {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(FinancerConfig.class);
|
||||
|
||||
private String countryCode;
|
||||
private String state;
|
||||
private String dateFormat;
|
||||
private Collection<String> mailRecipients;
|
||||
private String fromAddress;
|
||||
|
||||
/**
|
||||
* @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.stream(HolidayCalendar.values())
|
||||
.filter((hc) -> hc.getId().equals(this.countryCode))
|
||||
.findFirst();
|
||||
|
||||
if (!optionalHoliday.isPresent()) {
|
||||
LOGGER.warn(String
|
||||
.format("Use Germany as fallback country for holiday calculations. Configured country code is: %s. " +
|
||||
"This does not match any valid country code as specified by Jollyday",
|
||||
this.countryCode));
|
||||
}
|
||||
|
||||
return optionalHoliday.orElse(HolidayCalendar.GERMANY);
|
||||
}
|
||||
|
||||
public void setState(String state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public void setCountryCode(String countryCode) {
|
||||
this.countryCode = countryCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the date format used in e.g. the {@link de.financer.service.TransactionService#createTransaction(String,
|
||||
* String, Long, String, String) TransactionService#createTransaction} or {@link
|
||||
* de.financer.service.RecurringTransactionService#createRecurringTransaction(String, String, Long, String, String,
|
||||
* String, String, String) RecurringTransactionService#createRecurringTransaction} methods. Used to parse the
|
||||
* client-supplied date string to proper {@link java.time.LocalDate LocalDate} objects
|
||||
*/
|
||||
public String getDateFormat() {
|
||||
return dateFormat;
|
||||
}
|
||||
|
||||
public void setDateFormat(String dateFormat) {
|
||||
try {
|
||||
DateTimeFormatter.ofPattern(dateFormat);
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOGGER.warn(String
|
||||
.format("Use 'dd.MM.yyyy' as fallback for the date format because the configured format '%s' " +
|
||||
"cannot be parsed!", dateFormat), e);
|
||||
|
||||
dateFormat = "dd.MM.yyyy";
|
||||
}
|
||||
|
||||
this.dateFormat = dateFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a collection of email addresses that should receive mails from financer
|
||||
*/
|
||||
public Collection<String> getMailRecipients() {
|
||||
return mailRecipients;
|
||||
}
|
||||
|
||||
public void setMailRecipients(Collection<String> mailRecipients) {
|
||||
this.mailRecipients = mailRecipients;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the from address used in emails send by financer
|
||||
*/
|
||||
public String getFromAddress() {
|
||||
return fromAddress;
|
||||
}
|
||||
|
||||
public void setFromAddress(String fromAddress) {
|
||||
this.fromAddress = fromAddress;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package de.financer.controller;
|
||||
|
||||
import de.financer.ResponseReason;
|
||||
import de.financer.model.Account;
|
||||
import de.financer.service.AccountService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
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("accounts")
|
||||
public class AccountController {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(AccountController.class);
|
||||
|
||||
@Autowired
|
||||
private AccountService accountService;
|
||||
|
||||
@RequestMapping("getByKey")
|
||||
public Account getAccountByKey(String key) {
|
||||
final String decoded = ControllerUtil.urlDecode(key);
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String.format("/accounts/getByKey got parameter: %s", decoded));
|
||||
}
|
||||
|
||||
return this.accountService.getAccountByKey(decoded);
|
||||
}
|
||||
|
||||
@RequestMapping("getAll")
|
||||
public Iterable<Account> getAll() {
|
||||
return this.accountService.getAll();
|
||||
}
|
||||
|
||||
@RequestMapping("createAccount")
|
||||
public ResponseEntity createAccount(String key, String type, String accountGroupName) {
|
||||
final String decoded = ControllerUtil.urlDecode(key);
|
||||
final String decodedGroup = ControllerUtil.urlDecode(accountGroupName);
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String.format("/accounts/createAccount got parameters: %s, %s, %s", decoded, type, decodedGroup));
|
||||
}
|
||||
|
||||
final ResponseReason responseReason = this.accountService.createAccount(decoded, type, decodedGroup);
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String.format("/accounts/createAccount returns with %s", responseReason.name()));
|
||||
}
|
||||
|
||||
return responseReason.toResponseEntity();
|
||||
}
|
||||
|
||||
@RequestMapping("closeAccount")
|
||||
public ResponseEntity closeAccount(String key) {
|
||||
final String decoded = ControllerUtil.urlDecode(key);
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String.format("/accounts/closeAccount got parameters: %s", decoded));
|
||||
}
|
||||
|
||||
final ResponseReason responseReason = this.accountService.closeAccount(decoded);
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String.format("/accounts/closeAccount returns with %s", responseReason.name()));
|
||||
}
|
||||
|
||||
return responseReason.toResponseEntity();
|
||||
}
|
||||
|
||||
@RequestMapping("openAccount")
|
||||
public ResponseEntity openAccount(String key) {
|
||||
final String decoded = ControllerUtil.urlDecode(key);
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String.format("/accounts/openAccount got parameters: %s", decoded));
|
||||
}
|
||||
|
||||
final ResponseReason responseReason = this.accountService.openAccount(decoded);
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String.format("/accounts/openAccount returns with %s", responseReason.name()));
|
||||
}
|
||||
|
||||
return responseReason.toResponseEntity();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package de.financer.controller;
|
||||
|
||||
import de.financer.ResponseReason;
|
||||
import de.financer.model.AccountGroup;
|
||||
import de.financer.service.AccountGroupService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
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("accountGroups")
|
||||
public class AccountGroupController {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(AccountGroupController.class);
|
||||
|
||||
@Autowired
|
||||
private AccountGroupService accountGroupService;
|
||||
|
||||
@RequestMapping("getByName")
|
||||
public AccountGroup getAccountGroupByName(String name) {
|
||||
final String decoded = ControllerUtil.urlDecode(name);
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String.format("/accountGroups/getByName got parameter: %s", decoded));
|
||||
}
|
||||
|
||||
return this.accountGroupService.getAccountGroupByName(decoded);
|
||||
}
|
||||
|
||||
@RequestMapping("getAll")
|
||||
public Iterable<AccountGroup> getAll() {
|
||||
return this.accountGroupService.getAll();
|
||||
}
|
||||
|
||||
@RequestMapping("createAccountGroup")
|
||||
public ResponseEntity createAccountGroup(String name) {
|
||||
final String decoded = ControllerUtil.urlDecode(name);
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String.format("/accountGroups/createAccountGroup got parameter: %s", decoded));
|
||||
}
|
||||
|
||||
final ResponseReason responseReason = this.accountGroupService.createAccountGroup(decoded);
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String.format("/accountGroups/createAccountGroup returns with %s", responseReason.name()));
|
||||
}
|
||||
|
||||
return responseReason.toResponseEntity();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package de.financer.controller;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class ControllerUtil {
|
||||
/**
|
||||
* This method decodes the given URL encoded string, e.g. replaces <code>%20</code> with a space.
|
||||
*
|
||||
* @param toDecode the string to decode
|
||||
* @return the decoded string in UTF-8 or, if UTF-8 is not available for whatever reason, the encoded string
|
||||
*/
|
||||
public static final String urlDecode(String toDecode) {
|
||||
try {
|
||||
return URLDecoder.decode(toDecode, StandardCharsets.UTF_8.name());
|
||||
}
|
||||
catch (UnsupportedEncodingException e) {
|
||||
return toDecode;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package de.financer.controller;
|
||||
|
||||
import de.financer.ResponseReason;
|
||||
import de.financer.model.RecurringTransaction;
|
||||
import de.financer.service.RecurringTransactionService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
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;
|
||||
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Optional;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("recurringTransactions")
|
||||
public class RecurringTransactionController {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(RecurringTransactionController.class);
|
||||
|
||||
@Autowired
|
||||
private RecurringTransactionService recurringTransactionService;
|
||||
|
||||
@RequestMapping("getAll")
|
||||
public Iterable<RecurringTransaction> getAll() {
|
||||
return this.recurringTransactionService.getAll();
|
||||
}
|
||||
|
||||
@RequestMapping("getAllActive")
|
||||
public Iterable<RecurringTransaction> getAllActive() {
|
||||
return this.recurringTransactionService.getAllActive();
|
||||
}
|
||||
|
||||
@RequestMapping("getAllForAccount")
|
||||
public Iterable<RecurringTransaction> getAllForAccount(String accountKey) {
|
||||
final String decoded = ControllerUtil.urlDecode(accountKey);
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String.format("/recurringTransactions/getAllForAccount got parameter: %s", decoded));
|
||||
}
|
||||
|
||||
return this.recurringTransactionService.getAllForAccount(decoded);
|
||||
}
|
||||
|
||||
@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, Boolean remind
|
||||
) {
|
||||
final String decodedFrom = ControllerUtil.urlDecode(fromAccountKey);
|
||||
final String decodedTo = ControllerUtil.urlDecode(toAccountKey);
|
||||
final String decodedDesc = ControllerUtil.urlDecode(description);
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String
|
||||
.format("/recurringTransactions/createRecurringTransaction got parameters: %s, %s, %s, %s, %s, " +
|
||||
"%s, %s, %s, %s", decodedFrom, decodedTo, amount, decodedDesc, holidayWeekendType,
|
||||
intervalType, firstOccurrence, lastOccurrence, remind));
|
||||
}
|
||||
|
||||
final ResponseReason responseReason = this.recurringTransactionService
|
||||
.createRecurringTransaction(decodedFrom, decodedTo, amount, decodedDesc, holidayWeekendType,
|
||||
intervalType, firstOccurrence, lastOccurrence, remind);
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String
|
||||
.format("/recurringTransactions/createRecurringTransaction returns with %s", responseReason
|
||||
.name()));
|
||||
}
|
||||
|
||||
return responseReason.toResponseEntity();
|
||||
}
|
||||
|
||||
@RequestMapping("createTransaction")
|
||||
public ResponseEntity createTransaction(String recurringTransactionId, Long amount) {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String
|
||||
.format("/recurringTransactions/createTransaction got parameters: %s, %s",
|
||||
recurringTransactionId, amount));
|
||||
}
|
||||
|
||||
final ResponseReason responseReason = this.recurringTransactionService
|
||||
.createTransaction(recurringTransactionId, Optional.ofNullable(amount));
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String
|
||||
.format("/recurringTransactions/createTransaction returns with %s", responseReason.name()));
|
||||
}
|
||||
|
||||
return responseReason.toResponseEntity();
|
||||
}
|
||||
|
||||
@RequestMapping("deleteRecurringTransaction")
|
||||
public ResponseEntity deleteRecurringTransaction(String recurringTransactionId) {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String
|
||||
.format("/recurringTransactions/deleteRecurringTransaction got parameters: %s",
|
||||
recurringTransactionId));
|
||||
}
|
||||
|
||||
final ResponseReason responseReason = this.recurringTransactionService
|
||||
.deleteRecurringTransaction(recurringTransactionId);
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String
|
||||
.format("/recurringTransactions/deleteRecurringTransaction returns with %s", responseReason.name()));
|
||||
}
|
||||
|
||||
return responseReason.toResponseEntity();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package de.financer.controller;
|
||||
|
||||
import de.financer.ResponseReason;
|
||||
import de.financer.model.Transaction;
|
||||
import de.financer.service.TransactionService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
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;
|
||||
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("transactions")
|
||||
public class TransactionController {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(TransactionController.class);
|
||||
|
||||
@Autowired
|
||||
private TransactionService transactionService;
|
||||
|
||||
@RequestMapping("getAll")
|
||||
public Iterable<Transaction> getAll() {
|
||||
return this.transactionService.getAll();
|
||||
}
|
||||
|
||||
@RequestMapping("getAllForAccount")
|
||||
public Iterable<Transaction> getAllForAccount(String accountKey) {
|
||||
final String decoded = ControllerUtil.urlDecode(accountKey);
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String.format("/transactions/getAllForAccount got parameter: %s", decoded));
|
||||
}
|
||||
|
||||
return this.transactionService.getAllForAccount(decoded);
|
||||
}
|
||||
|
||||
@RequestMapping(value = "createTransaction")
|
||||
public ResponseEntity createTransaction(String fromAccountKey, String toAccountKey, Long amount, String date,
|
||||
String description
|
||||
) {
|
||||
final String decodedFrom = ControllerUtil.urlDecode(fromAccountKey);
|
||||
final String decodedTo = ControllerUtil.urlDecode(toAccountKey);
|
||||
final String decodedDesc = ControllerUtil.urlDecode(description);
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String
|
||||
.format("/transactions/createTransaction got parameters: %s, %s, %s, %s, %s",
|
||||
decodedFrom, decodedTo, amount, date, decodedDesc));
|
||||
}
|
||||
|
||||
final ResponseReason responseReason = this.transactionService
|
||||
.createTransaction(decodedFrom, decodedTo, amount, date, decodedDesc);
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String.format("/transactions/createTransaction returns with %s", responseReason.name()));
|
||||
}
|
||||
|
||||
return responseReason.toResponseEntity();
|
||||
}
|
||||
|
||||
@RequestMapping("deleteTransaction")
|
||||
public ResponseEntity deleteTransaction(String transactionId) {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String
|
||||
.format("/transactions/deleteTransaction got parameters: %s",
|
||||
transactionId));
|
||||
}
|
||||
|
||||
final ResponseReason responseReason = this.transactionService
|
||||
.deleteTransaction(transactionId);
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String
|
||||
.format("/transactions/deleteTransaction returns with %s", responseReason.name()));
|
||||
}
|
||||
|
||||
return responseReason.toResponseEntity();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package de.financer.dba;
|
||||
|
||||
import de.financer.model.AccountGroup;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Transactional(propagation = Propagation.REQUIRED)
|
||||
public interface AccountGroupRepository extends CrudRepository<AccountGroup, Long> {
|
||||
AccountGroup findByName(String name);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package de.financer.dba;
|
||||
|
||||
import de.financer.model.Account;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Transactional(propagation = Propagation.REQUIRED)
|
||||
public interface AccountRepository extends CrudRepository<Account, Long> {
|
||||
Account findByKey(String key);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package de.financer.dba;
|
||||
|
||||
import de.financer.model.Account;
|
||||
import de.financer.model.RecurringTransaction;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Transactional(propagation = Propagation.REQUIRED)
|
||||
public interface RecurringTransactionRepository extends CrudRepository<RecurringTransaction, Long> {
|
||||
Iterable<RecurringTransaction> findRecurringTransactionsByFromAccountOrToAccount(Account fromAccount, Account toAccount);
|
||||
|
||||
@Query("SELECT rt FROM RecurringTransaction rt WHERE rt.deleted = false AND (rt.lastOccurrence IS NULL OR rt.lastOccurrence >= :lastOccurrence)")
|
||||
Iterable<RecurringTransaction> findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(LocalDate lastOccurrence);
|
||||
|
||||
Iterable<RecurringTransaction> findByDeletedFalse();
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package de.financer.dba;
|
||||
|
||||
import de.financer.model.Account;
|
||||
import de.financer.model.Transaction;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Transactional(propagation = Propagation.REQUIRED)
|
||||
public interface TransactionRepository extends CrudRepository<Transaction, Long> {
|
||||
Iterable<Transaction> findTransactionsByFromAccountOrToAccount(Account fromAccount, Account toAccount);
|
||||
}
|
||||
63
financer-server/src/main/java/de/financer/model/Account.java
Normal file
63
financer-server/src/main/java/de/financer/model/Account.java
Normal file
@@ -0,0 +1,63 @@
|
||||
package de.financer.model;
|
||||
|
||||
import javax.persistence.*;
|
||||
|
||||
@Entity
|
||||
public class Account {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
@Column(name = "\"key\"") // we need to escape the keyword "key"
|
||||
private String key;
|
||||
@Enumerated(EnumType.STRING)
|
||||
private AccountType type;
|
||||
@Enumerated(EnumType.STRING)
|
||||
private AccountStatus status;
|
||||
private Long currentBalance;
|
||||
@ManyToOne
|
||||
private AccountGroup accountGroup;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public void setKey(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public AccountType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(AccountType type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public AccountStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(AccountStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public Long getCurrentBalance() {
|
||||
return currentBalance;
|
||||
}
|
||||
|
||||
public void setCurrentBalance(Long currentBalance) {
|
||||
this.currentBalance = currentBalance;
|
||||
}
|
||||
|
||||
public AccountGroup getAccountGroup() {
|
||||
return accountGroup;
|
||||
}
|
||||
|
||||
public void setAccountGroup(AccountGroup accountGroup) {
|
||||
this.accountGroup = accountGroup;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package de.financer.model;
|
||||
|
||||
import javax.persistence.*;
|
||||
|
||||
@Entity
|
||||
public class AccountGroup {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
private String name;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package de.financer.model;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public enum AccountStatus {
|
||||
/** Indicates that the account is open for bookings */
|
||||
OPEN,
|
||||
/** Indicates that the account is closed and bookings to it are forbidden */
|
||||
CLOSED;
|
||||
|
||||
/**
|
||||
* This method validates whether the given string represents a valid account status.
|
||||
*
|
||||
* @param status to check
|
||||
* @return whether the given status represents a valid account status
|
||||
*/
|
||||
public static boolean isValidType(String status) {
|
||||
return Arrays.stream(AccountStatus.values()).anyMatch((accountStatus) -> accountStatus.name().equals(status));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package de.financer.model;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public enum AccountType {
|
||||
/** Used to mark an account that acts as a source of money, e.g. monthly wage */
|
||||
INCOME,
|
||||
|
||||
/** Indicates a real account at a bank, e.g. a check payment account */
|
||||
BANK,
|
||||
|
||||
/** Marks an account as physical cash, e.g. the money currently in the purse */
|
||||
CASH,
|
||||
|
||||
/** Used to mark an account that acts as a destination of money, e.g. through buying goods */
|
||||
EXPENSE,
|
||||
|
||||
/** Marks an account as a liability from a third party, e.g. credit card or loan */
|
||||
LIABILITY,
|
||||
|
||||
/** Marks the start account that is to be used to book all the opening balances for the different accounts */
|
||||
START;
|
||||
|
||||
/**
|
||||
* This method validates whether the given string represents a valid account type.
|
||||
*
|
||||
* @param type to check
|
||||
* @return whether the given type represents a valid account type
|
||||
*/
|
||||
public static boolean isValidType(String type) {
|
||||
return Arrays.stream(AccountType.values()).anyMatch((accountType) -> accountType.name().equals(type));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package de.financer.model;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* This enum specifies constants that control how actions should be handled that would fall on a holiday
|
||||
* or weekday (where usually are no bookings done by e.g. banks)
|
||||
*/
|
||||
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,
|
||||
|
||||
/**
|
||||
* <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,
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Indicates that the action should preponed to 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;
|
||||
|
||||
/**
|
||||
* This method validates whether the given string represents a valid holiday weekend type.
|
||||
*
|
||||
* @param type to check
|
||||
* @return whether the given type represents a valid holiday weekend type
|
||||
*/
|
||||
public static boolean isValidType(String type) {
|
||||
return Arrays.stream(HolidayWeekendType.values()).anyMatch((holidayWeekendType) -> holidayWeekendType.name().equals(type));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package de.financer.model;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public enum IntervalType {
|
||||
/** Indicates that an action should be executed every day */
|
||||
DAILY,
|
||||
|
||||
/** Indicates that an action should be executed once a week */
|
||||
WEEKLY,
|
||||
|
||||
/** Indicates that an action should be executed once a month */
|
||||
MONTHLY,
|
||||
|
||||
/** Indicates that an action should be executed once a quarter */
|
||||
QUARTERLY,
|
||||
|
||||
/** Indicates that an action should be executed once a year */
|
||||
YEARLY;
|
||||
|
||||
/**
|
||||
* This method validates whether the given string represents a valid interval type.
|
||||
*
|
||||
* @param type to check
|
||||
* @return whether the given type represents a valid interval type
|
||||
*/
|
||||
public static boolean isValidType(String type) {
|
||||
return Arrays.stream(IntervalType.values()).anyMatch((intervalType) -> intervalType.name().equals(type));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package de.financer.model;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Entity
|
||||
public class RecurringTransaction {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
@OneToOne(fetch = FetchType.EAGER)
|
||||
private Account fromAccount;
|
||||
@OneToOne(fetch = FetchType.EAGER)
|
||||
private Account toAccount;
|
||||
private String description;
|
||||
private Long amount;
|
||||
@Enumerated(EnumType.STRING)
|
||||
private IntervalType intervalType;
|
||||
private LocalDate firstOccurrence;
|
||||
private LocalDate lastOccurrence;
|
||||
@Enumerated(EnumType.STRING)
|
||||
private HolidayWeekendType holidayWeekendType;
|
||||
private boolean deleted;
|
||||
private boolean remind;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public Account getFromAccount() {
|
||||
return fromAccount;
|
||||
}
|
||||
|
||||
public void setFromAccount(Account fromAccount) {
|
||||
this.fromAccount = fromAccount;
|
||||
}
|
||||
|
||||
public Account getToAccount() {
|
||||
return toAccount;
|
||||
}
|
||||
|
||||
public void setToAccount(Account toAccount) {
|
||||
this.toAccount = toAccount;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public Long getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public void setAmount(Long amount) {
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
public HolidayWeekendType getHolidayWeekendType() {
|
||||
return holidayWeekendType;
|
||||
}
|
||||
|
||||
public void setHolidayWeekendType(HolidayWeekendType holidayWeekendType) {
|
||||
this.holidayWeekendType = holidayWeekendType;
|
||||
}
|
||||
|
||||
public LocalDate getLastOccurrence() {
|
||||
return lastOccurrence;
|
||||
}
|
||||
|
||||
public void setLastOccurrence(LocalDate lastOccurrence) {
|
||||
this.lastOccurrence = lastOccurrence;
|
||||
}
|
||||
|
||||
public LocalDate getFirstOccurrence() {
|
||||
return firstOccurrence;
|
||||
}
|
||||
|
||||
public void setFirstOccurrence(LocalDate firstOccurrence) {
|
||||
this.firstOccurrence = firstOccurrence;
|
||||
}
|
||||
|
||||
public IntervalType getIntervalType() {
|
||||
return intervalType;
|
||||
}
|
||||
|
||||
public void setIntervalType(IntervalType intervalType) {
|
||||
this.intervalType = intervalType;
|
||||
}
|
||||
|
||||
public boolean isDeleted() {
|
||||
return deleted;
|
||||
}
|
||||
|
||||
public void setDeleted(boolean deleted) {
|
||||
this.deleted = deleted;
|
||||
}
|
||||
|
||||
public boolean isRemind() {
|
||||
return remind;
|
||||
}
|
||||
|
||||
public void setRemind(boolean remind) {
|
||||
this.remind = remind;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package de.financer.model;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Entity
|
||||
@Table(name = "\"transaction\"")
|
||||
public class Transaction {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
@OneToOne(fetch = FetchType.EAGER)
|
||||
private Account fromAccount;
|
||||
@OneToOne(fetch = FetchType.EAGER)
|
||||
private Account toAccount;
|
||||
@Column(name = "\"date\"")
|
||||
private LocalDate date;
|
||||
private String description;
|
||||
private Long amount;
|
||||
@ManyToOne(fetch = FetchType.EAGER)
|
||||
private RecurringTransaction recurringTransaction;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public Account getFromAccount() {
|
||||
return fromAccount;
|
||||
}
|
||||
|
||||
public void setFromAccount(Account fromAccount) {
|
||||
this.fromAccount = fromAccount;
|
||||
}
|
||||
|
||||
public Account getToAccount() {
|
||||
return toAccount;
|
||||
}
|
||||
|
||||
public void setToAccount(Account toAccount) {
|
||||
this.toAccount = toAccount;
|
||||
}
|
||||
|
||||
public LocalDate getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public void setDate(LocalDate date) {
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public Long getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public void setAmount(Long amount) {
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
public RecurringTransaction getRecurringTransaction() {
|
||||
return recurringTransaction;
|
||||
}
|
||||
|
||||
public void setRecurringTransaction(RecurringTransaction recurringTransaction) {
|
||||
this.recurringTransaction = recurringTransaction;
|
||||
}
|
||||
}
|
||||
@@ -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,69 @@
|
||||
package de.financer.service;
|
||||
|
||||
import de.financer.ResponseReason;
|
||||
import de.financer.dba.AccountGroupRepository;
|
||||
import de.financer.model.AccountGroup;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
public class AccountGroupService {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(AccountGroupService.class);
|
||||
|
||||
@Autowired
|
||||
private AccountGroupRepository accountGroupRepository;
|
||||
|
||||
/**
|
||||
* @return all existing account groups
|
||||
*/
|
||||
public Iterable<AccountGroup> getAll() {
|
||||
return this.accountGroupRepository.findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns the account group with the given name.
|
||||
*
|
||||
* @param name the name to get the account group for
|
||||
* @return the account group or <code>null</code> if no account group with the given name can be found
|
||||
*/
|
||||
public AccountGroup getAccountGroupByName(String name) {
|
||||
return this.accountGroupRepository.findByName(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method creates a new account group with the given name.
|
||||
*
|
||||
* @param name the name of the new account group
|
||||
* @return {@link ResponseReason#DUPLICATE_ACCOUNT_GROUP_NAME} if an account group with the given name already exists,
|
||||
* {@link ResponseReason#UNKNOWN_ERROR} if an unknown error occurs,
|
||||
* {@link ResponseReason#OK} if the operation completed successfully.
|
||||
* Never returns <code>null</code>.
|
||||
*/
|
||||
@Transactional(propagation = Propagation.SUPPORTS)
|
||||
public ResponseReason createAccountGroup(String name) {
|
||||
final AccountGroup accountGroup = new AccountGroup();
|
||||
|
||||
accountGroup.setName(name);
|
||||
|
||||
try {
|
||||
this.accountGroupRepository.save(accountGroup);
|
||||
}
|
||||
catch (DataIntegrityViolationException dive) {
|
||||
LOGGER.error(String.format("Duplicate account group name! %s", name), dive);
|
||||
|
||||
return ResponseReason.DUPLICATE_ACCOUNT_GROUP_NAME;
|
||||
}
|
||||
catch (Exception e) {
|
||||
LOGGER.error(String.format("Could not save account group %s", name), e);
|
||||
|
||||
return ResponseReason.UNKNOWN_ERROR;
|
||||
}
|
||||
|
||||
return ResponseReason.OK;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package de.financer.service;
|
||||
|
||||
import de.financer.ResponseReason;
|
||||
import de.financer.dba.AccountRepository;
|
||||
import de.financer.model.Account;
|
||||
import de.financer.model.AccountGroup;
|
||||
import de.financer.model.AccountStatus;
|
||||
import de.financer.model.AccountType;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class AccountService {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(AccountService.class);
|
||||
|
||||
@Autowired
|
||||
private AccountRepository accountRepository;
|
||||
@Autowired
|
||||
private AccountGroupService accountGroupService;
|
||||
|
||||
/**
|
||||
* This method returns the account identified by the given key.
|
||||
*
|
||||
* @param key the key to get the account for
|
||||
* @return the account or <code>null</code> if no account with the given key can be found
|
||||
*/
|
||||
public Account getAccountByKey(String key) {
|
||||
return this.accountRepository.findByKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return all existing accounts, regardless of their type or status. This explicitly covers accounts in {@link AccountStatus#CLOSED CLOSED} as well.
|
||||
*/
|
||||
public Iterable<Account> getAll() {
|
||||
return this.accountRepository.findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method saves the given account. It either updates the account if it already exists or inserts
|
||||
* it if it's new.
|
||||
*
|
||||
* @param account the account to save
|
||||
*/
|
||||
@Transactional(propagation = Propagation.REQUIRED)
|
||||
public void saveAccount(Account account) {
|
||||
this.accountRepository.save(account);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method creates new account with the given key and type. The account has status {@link AccountStatus#OPEN OPEN}
|
||||
* and a current balance of <code>0</code>.
|
||||
*
|
||||
* @param key the key of the new account
|
||||
* @param type the type of the new account. Must be one of {@link AccountType}.
|
||||
* @param accountGroupName the name of the account group to use, can be <code>null</code>
|
||||
* @return {@link ResponseReason#INVALID_ACCOUNT_TYPE} if the given type is not a valid {@link AccountType},
|
||||
* {@link ResponseReason#UNKNOWN_ERROR} if an unexpected error occurs,
|
||||
* {@link ResponseReason#OK} if the operation completed successfully,
|
||||
* {@link ResponseReason#DUPLICATE_ACCOUNT_KEY} if an account with the given key
|
||||
* already exists and {@link ResponseReason#ACCOUNT_GROUP_NOT_FOUND} if the optional parameter
|
||||
* <code>accountGroupName</code> does not identify a valid account group. Never returns <code>null</code>.
|
||||
*/
|
||||
@Transactional(propagation = Propagation.SUPPORTS)
|
||||
public ResponseReason createAccount(String key, String type, String accountGroupName) {
|
||||
if (!AccountType.isValidType(type)) {
|
||||
return ResponseReason.INVALID_ACCOUNT_TYPE;
|
||||
}
|
||||
|
||||
final Account account = new Account();
|
||||
|
||||
if (StringUtils.isNotEmpty(accountGroupName)) {
|
||||
final AccountGroup accountGroup = this.accountGroupService.getAccountGroupByName(accountGroupName);
|
||||
|
||||
if (accountGroup == null) {
|
||||
return ResponseReason.ACCOUNT_GROUP_NOT_FOUND; // early return
|
||||
}
|
||||
|
||||
account.setAccountGroup(accountGroup);
|
||||
}
|
||||
|
||||
account.setKey(key);
|
||||
account.setType(AccountType.valueOf(type));
|
||||
// If we create an account it's implicitly open
|
||||
account.setStatus(AccountStatus.OPEN);
|
||||
// and has a current balance of 0
|
||||
account.setCurrentBalance(Long.valueOf(0L));
|
||||
|
||||
try {
|
||||
this.accountRepository.save(account);
|
||||
}
|
||||
catch (DataIntegrityViolationException dive) {
|
||||
LOGGER.error(String.format("Duplicate key! %s|%s|%s", key, type, accountGroupName), dive);
|
||||
|
||||
return ResponseReason.DUPLICATE_ACCOUNT_KEY;
|
||||
}
|
||||
catch (Exception e) {
|
||||
LOGGER.error(String.format("Could not save account %s|%s|%s", key, type, accountGroupName), e);
|
||||
|
||||
return ResponseReason.UNKNOWN_ERROR;
|
||||
}
|
||||
|
||||
return ResponseReason.OK;
|
||||
}
|
||||
|
||||
@Transactional(propagation = Propagation.REQUIRED)
|
||||
public ResponseReason closeAccount(String key) {
|
||||
return setAccountStatus(key, AccountStatus.CLOSED);
|
||||
}
|
||||
|
||||
@Transactional(propagation = Propagation.REQUIRED)
|
||||
public ResponseReason openAccount(String key) {
|
||||
return setAccountStatus(key, AccountStatus.OPEN);
|
||||
}
|
||||
|
||||
// Visible for unit tests
|
||||
/* package */ ResponseReason setAccountStatus(String key, AccountStatus accountStatus) {
|
||||
final Account account = this.accountRepository.findByKey(key);
|
||||
|
||||
if (account == null) {
|
||||
return ResponseReason.ACCOUNT_NOT_FOUND;
|
||||
}
|
||||
|
||||
account.setStatus(accountStatus);
|
||||
|
||||
try {
|
||||
this.accountRepository.save(account);
|
||||
}
|
||||
catch (Exception e) {
|
||||
LOGGER.error(String.format("Could not update account status %s|%s", key, accountStatus.name()), e);
|
||||
|
||||
return ResponseReason.UNKNOWN_ERROR;
|
||||
}
|
||||
|
||||
return ResponseReason.OK;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,498 @@
|
||||
package de.financer.service;
|
||||
|
||||
import de.financer.ResponseReason;
|
||||
import de.financer.config.FinancerConfig;
|
||||
import de.financer.dba.RecurringTransactionRepository;
|
||||
import de.financer.model.Account;
|
||||
import de.financer.model.HolidayWeekendType;
|
||||
import de.financer.model.IntervalType;
|
||||
import de.financer.model.RecurringTransaction;
|
||||
import org.apache.commons.collections4.IterableUtils;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
|
||||
import org.apache.commons.lang3.math.NumberUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class RecurringTransactionService {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(RecurringTransactionService.class);
|
||||
|
||||
@Autowired
|
||||
private RecurringTransactionRepository recurringTransactionRepository;
|
||||
|
||||
@Autowired
|
||||
private AccountService accountService;
|
||||
|
||||
@Autowired
|
||||
private RuleService ruleService;
|
||||
|
||||
@Autowired
|
||||
private FinancerConfig financerConfig;
|
||||
|
||||
@Autowired
|
||||
private TransactionService transactionService;
|
||||
|
||||
public Iterable<RecurringTransaction> getAll() {
|
||||
return this.recurringTransactionRepository.findByDeletedFalse();
|
||||
}
|
||||
|
||||
public Iterable<RecurringTransaction> getAllActive() {
|
||||
return this.recurringTransactionRepository
|
||||
.findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(LocalDate.now());
|
||||
}
|
||||
|
||||
public Iterable<RecurringTransaction> getAllForAccount(String accountKey) {
|
||||
final Account account = this.accountService.getAccountByKey(accountKey);
|
||||
|
||||
if (account == null) {
|
||||
LOGGER.warn(String.format("Account with key %s not found!", accountKey));
|
||||
|
||||
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) {
|
||||
// Subtract one week/seven days from the current date so that recurring transactions that have their last
|
||||
// occurrence on a weekend or a holiday in the near past and HWT NEXT_WORKDAY are also grabbed. Otherwise
|
||||
// there would never be a reminder about them. On the actual due date the reminder is deferred because of the
|
||||
// HWT and for later runs it's not grabbed because of the condition '...LastOccurrenceGreaterThanEqual(now)'
|
||||
final Iterable<RecurringTransaction> allRecurringTransactions = this.recurringTransactionRepository
|
||||
.findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(now.minusDays(7));
|
||||
|
||||
LOGGER.debug(String.format("Found %s candidate recurring transactions. Checking which are due",
|
||||
IterableUtils.size(allRecurringTransactions)));
|
||||
|
||||
//@formatter:off
|
||||
return IterableUtils.toList(allRecurringTransactions).stream()
|
||||
.filter((rt) -> checkRecurringTransactionDueToday(rt, now) ||
|
||||
checkRecurringTransactionDuePast(rt, now) ||
|
||||
checkRecurringTransactionDueFuture(rt, now))
|
||||
.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) {
|
||||
// If a recurring transactions first occurrence is in the future it can never be relevant for this
|
||||
// method. This case will be handled in the checkRecurringTransactionDueFuture method if the recurring
|
||||
// transaction also has HolidayWeekendType#PREVIOUS_WORKDAY.
|
||||
// If this check is not done the datesUntil(...) call will fail as it expects that the callees date is lower
|
||||
// or equal the first parameter which is not the case for the following example:
|
||||
// callee.firstOccurrence = 2019-05-27
|
||||
// now = 2019-05-14
|
||||
// now.plusDays(1) = 2019-05-15
|
||||
// => IllegalArgumentException: 2019-05-15 < 2019-05-27
|
||||
if (recurringTransaction.getFirstOccurrence().isAfter(now)) {
|
||||
LOGGER.debug(String.format("Recurring transaction %s has its first occurrence in the future and thus " +
|
||||
"cannot be due today",
|
||||
ReflectionToStringBuilder.toString(recurringTransaction)));
|
||||
|
||||
return false; // early return
|
||||
}
|
||||
|
||||
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
|
||||
|| recurringTransaction.getHolidayWeekendType() == HolidayWeekendType.PREVIOUS_WORKDAY;
|
||||
}
|
||||
|
||||
LOGGER.debug(String.format("Recurring transaction %s due today? %s (defer=%s, dueToday=%s)",
|
||||
ReflectionToStringBuilder.toString(recurringTransaction), (!defer && dueToday), defer, dueToday));
|
||||
|
||||
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) {
|
||||
// Recurring transactions with holiday weekend type SAME_DAY or PREVIOUS_WORKDAY can't be due in the past
|
||||
if (!HolidayWeekendType.NEXT_WORKDAY.equals(recurringTransaction.getHolidayWeekendType())) {
|
||||
LOGGER.debug(String.format("Recurring transaction %s has HWT %s and thus cannot be due in the past",
|
||||
ReflectionToStringBuilder.toString(recurringTransaction),
|
||||
recurringTransaction.getHolidayWeekendType()));
|
||||
|
||||
return false; // early return
|
||||
}
|
||||
|
||||
// If a recurring transactions first occurrence is in the future it can never be relevant for this
|
||||
// method, as this method handles recurring transactions due in the past.
|
||||
if (recurringTransaction.getFirstOccurrence().isAfter(now)) {
|
||||
LOGGER.debug(String.format("Recurring transaction %s has its first occurrence in the future and thus " +
|
||||
"cannot be due in the past",
|
||||
ReflectionToStringBuilder.toString(recurringTransaction)));
|
||||
|
||||
return false; // early return
|
||||
}
|
||||
|
||||
// If today is a weekend day or holiday the recurring transaction cannot be due today, because the
|
||||
// holiday weekend type says NEXT_WORKDAY.
|
||||
if (this.ruleService.isHoliday(now) || this.ruleService.isWeekend(now)) {
|
||||
LOGGER.debug(String.format("Recurring transaction %s has HWT %s and today is either a holiday or weekend," +
|
||||
" thus it cannot be due in the past",
|
||||
ReflectionToStringBuilder.toString(recurringTransaction),
|
||||
recurringTransaction.getHolidayWeekendType()));
|
||||
|
||||
return false; // early return
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
LOGGER.debug(String.format("Recurring transaction %s is due in the past? %s",
|
||||
ReflectionToStringBuilder.toString(recurringTransaction), due));
|
||||
|
||||
return due;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method checks whether the given {@link RecurringTransaction} will actually be due in the close future will
|
||||
* be preponed to <i>maybe</i> today because the actual due day will be a holiday or weekend day and the {@link
|
||||
* RecurringTransaction#getHolidayWeekendType() holiday weekend type} is {@link
|
||||
* HolidayWeekendType#PREVIOUS_WORKDAY}. The period this method considers starts with today and ends with the next
|
||||
* 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 will due at the next workday day
|
||||
* it does not need to be preponed.
|
||||
*
|
||||
* @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 checkRecurringTransactionDueFuture(RecurringTransaction recurringTransaction, LocalDate now) {
|
||||
// Recurring transactions with holiday weekend type SAME_DAY or PREVIOUS_WORKDAY can't be due in the future
|
||||
if (!HolidayWeekendType.PREVIOUS_WORKDAY.equals(recurringTransaction.getHolidayWeekendType())) {
|
||||
LOGGER.debug(String.format("Recurring transaction %s has HWT %s and thus cannot be due in the future",
|
||||
ReflectionToStringBuilder.toString(recurringTransaction),
|
||||
recurringTransaction.getHolidayWeekendType()));
|
||||
|
||||
return false; // early return
|
||||
}
|
||||
|
||||
boolean weekend;
|
||||
boolean holiday;
|
||||
LocalDate tomorrow = now;
|
||||
boolean due = false;
|
||||
|
||||
// Go forth in time until we hit the first non-holiday, non-weekend day
|
||||
// and check for every day in between if the given recurring transaction will be due on this day
|
||||
do {
|
||||
tomorrow = tomorrow.plusDays(1);
|
||||
holiday = this.ruleService.isHoliday(tomorrow);
|
||||
weekend = this.ruleService.isWeekend(tomorrow);
|
||||
|
||||
if (holiday || weekend) {
|
||||
// Lambdas require final local variables
|
||||
final LocalDate finalTomorrow = tomorrow;
|
||||
|
||||
// For an explanation of the expression see the ...DueToday method
|
||||
due = recurringTransaction.getFirstOccurrence()
|
||||
.datesUntil(tomorrow.plusDays(1), this.ruleService
|
||||
.getPeriodForInterval(recurringTransaction
|
||||
.getIntervalType()))
|
||||
.anyMatch((d) -> d.equals(finalTomorrow));
|
||||
|
||||
if (due) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
while (holiday || weekend);
|
||||
|
||||
LOGGER.debug(String.format("Recurring transaction %s is due in the future? %s",
|
||||
ReflectionToStringBuilder.toString(recurringTransaction), due));
|
||||
|
||||
return due;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Transactional(propagation = Propagation.REQUIRED)
|
||||
public ResponseReason createRecurringTransaction(String fromAccountKey, String toAccountKey, Long amount,
|
||||
String description, String holidayWeekendType,
|
||||
String intervalType, String firstOccurrence,
|
||||
String lastOccurrence, Boolean remind
|
||||
) {
|
||||
final Account fromAccount = this.accountService.getAccountByKey(fromAccountKey);
|
||||
final Account toAccount = this.accountService.getAccountByKey(toAccountKey);
|
||||
ResponseReason response = validateParameters(fromAccount, toAccount, amount, holidayWeekendType, intervalType,
|
||||
firstOccurrence, lastOccurrence); // no validation of 'remind' as it's completely optional
|
||||
|
||||
// If we detected an issue with the given parameters return the first error found to the caller
|
||||
if (response != null) {
|
||||
return response; // early return
|
||||
}
|
||||
|
||||
try {
|
||||
final RecurringTransaction transaction = buildRecurringTransaction(fromAccount, toAccount, amount,
|
||||
description, holidayWeekendType, intervalType, firstOccurrence, lastOccurrence, remind);
|
||||
|
||||
this.recurringTransactionRepository.save(transaction);
|
||||
|
||||
response = ResponseReason.OK;
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Could not create recurring transaction!", e);
|
||||
|
||||
response = ResponseReason.UNKNOWN_ERROR;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method builds the actual recurring transaction object with the given values.
|
||||
*
|
||||
* @param fromAccount the from account
|
||||
* @param toAccount the to account
|
||||
* @param amount the transaction amount
|
||||
* @param holidayWeekendType the holiday weekend type
|
||||
* @param intervalType the interval type
|
||||
* @param firstOccurrence the first occurrence
|
||||
* @param lastOccurrence the last occurrence, may be <code>null</code>
|
||||
* @param remind the remind flag
|
||||
*
|
||||
* @return the build {@link RecurringTransaction} instance
|
||||
*/
|
||||
private RecurringTransaction buildRecurringTransaction(Account fromAccount, Account toAccount, Long amount,
|
||||
String description, String holidayWeekendType,
|
||||
String intervalType, String firstOccurrence,
|
||||
String lastOccurrence, Boolean remind
|
||||
) {
|
||||
final RecurringTransaction recurringTransaction = new RecurringTransaction();
|
||||
|
||||
recurringTransaction.setFromAccount(fromAccount);
|
||||
recurringTransaction.setToAccount(toAccount);
|
||||
recurringTransaction.setAmount(amount);
|
||||
recurringTransaction.setDescription(description);
|
||||
recurringTransaction.setHolidayWeekendType(HolidayWeekendType.valueOf(holidayWeekendType));
|
||||
recurringTransaction.setIntervalType(IntervalType.valueOf(intervalType));
|
||||
recurringTransaction.setFirstOccurrence(LocalDate
|
||||
.parse(firstOccurrence, DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat())));
|
||||
// See 'resources/database/postgres/readme_V1_0_0__init.txt'
|
||||
recurringTransaction.setDeleted(false);
|
||||
recurringTransaction.setRemind(BooleanUtils.toBooleanDefaultIfNull(remind, true));
|
||||
|
||||
// lastOccurrence is optional
|
||||
if (StringUtils.isNotEmpty(lastOccurrence)) {
|
||||
recurringTransaction.setLastOccurrence(LocalDate
|
||||
.parse(lastOccurrence, DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat())));
|
||||
}
|
||||
|
||||
return recurringTransaction;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 holidayWeekendType the holiday weekend type
|
||||
* @param intervalType the interval type
|
||||
* @param firstOccurrence the first occurrence
|
||||
* @param lastOccurrence the last occurrence, may be <code>null</code>
|
||||
*
|
||||
* @return the first error found or <code>null</code> if all parameters are valid
|
||||
*/
|
||||
private ResponseReason validateParameters(Account fromAccount, Account toAccount, Long amount,
|
||||
String holidayWeekendType, String intervalType, String firstOccurrence,
|
||||
String lastOccurrence
|
||||
) {
|
||||
ResponseReason response = null;
|
||||
|
||||
if (fromAccount == null && toAccount == null) {
|
||||
response = ResponseReason.FROM_AND_TO_ACCOUNT_NOT_FOUND;
|
||||
} else if (toAccount == null) {
|
||||
response = ResponseReason.TO_ACCOUNT_NOT_FOUND;
|
||||
} else if (fromAccount == null) {
|
||||
response = ResponseReason.FROM_ACCOUNT_NOT_FOUND;
|
||||
} else if (!this.ruleService.isValidBooking(fromAccount, toAccount)) {
|
||||
response = ResponseReason.INVALID_BOOKING_ACCOUNTS;
|
||||
} else if (amount == null) {
|
||||
response = ResponseReason.MISSING_AMOUNT;
|
||||
} else if (amount == 0L) {
|
||||
response = ResponseReason.AMOUNT_ZERO;
|
||||
} else if (StringUtils.isEmpty(holidayWeekendType)) {
|
||||
response = ResponseReason.MISSING_HOLIDAY_WEEKEND_TYPE;
|
||||
} else if (!HolidayWeekendType.isValidType(holidayWeekendType)) {
|
||||
response = ResponseReason.INVALID_HOLIDAY_WEEKEND_TYPE;
|
||||
} else if (StringUtils.isEmpty(intervalType)) {
|
||||
response = ResponseReason.MISSING_INTERVAL_TYPE;
|
||||
} else if (!IntervalType.isValidType(intervalType)) {
|
||||
response = ResponseReason.INVALID_INTERVAL_TYPE;
|
||||
} else if (StringUtils.isEmpty(firstOccurrence)) {
|
||||
response = ResponseReason.MISSING_FIRST_OCCURRENCE;
|
||||
}
|
||||
|
||||
if (response == null && StringUtils.isNotEmpty(firstOccurrence)) {
|
||||
try {
|
||||
LocalDate.parse(firstOccurrence, DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat()));
|
||||
} catch (DateTimeParseException e) {
|
||||
response = ResponseReason.INVALID_FIRST_OCCURRENCE_FORMAT;
|
||||
}
|
||||
}
|
||||
|
||||
if (response == null && StringUtils.isNotEmpty(lastOccurrence)) {
|
||||
try {
|
||||
LocalDate.parse(lastOccurrence, DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat()));
|
||||
} catch (DateTimeParseException e) {
|
||||
response = ResponseReason.INVALID_LAST_OCCURRENCE_FORMAT;
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@Transactional(propagation = Propagation.REQUIRED)
|
||||
public ResponseReason createTransaction(String recurringTransactionId, Optional<Long> amount) {
|
||||
if (recurringTransactionId == null) {
|
||||
return ResponseReason.MISSING_RECURRING_TRANSACTION_ID;
|
||||
} else if (!NumberUtils.isCreatable(recurringTransactionId)) {
|
||||
return ResponseReason.INVALID_RECURRING_TRANSACTION_ID;
|
||||
}
|
||||
|
||||
final Optional<RecurringTransaction> optionalRecurringTransaction = this.recurringTransactionRepository
|
||||
.findById(Long.valueOf(recurringTransactionId));
|
||||
|
||||
if (!optionalRecurringTransaction.isPresent()) {
|
||||
return ResponseReason.RECURRING_TRANSACTION_NOT_FOUND;
|
||||
}
|
||||
|
||||
final RecurringTransaction recurringTransaction = optionalRecurringTransaction.get();
|
||||
|
||||
return this.transactionService.createTransaction(recurringTransaction.getFromAccount().getKey(),
|
||||
recurringTransaction.getToAccount().getKey(),
|
||||
amount.orElseGet(recurringTransaction::getAmount),
|
||||
LocalDate.now().format(DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat())),
|
||||
recurringTransaction.getDescription(),
|
||||
recurringTransaction);
|
||||
}
|
||||
|
||||
@Transactional(propagation = Propagation.REQUIRED)
|
||||
public ResponseReason deleteRecurringTransaction(String recurringTransactionId) {
|
||||
ResponseReason response = ResponseReason.OK;
|
||||
|
||||
if (recurringTransactionId == null) {
|
||||
return ResponseReason.MISSING_RECURRING_TRANSACTION_ID;
|
||||
} else if (!NumberUtils.isCreatable(recurringTransactionId)) {
|
||||
return ResponseReason.INVALID_RECURRING_TRANSACTION_ID;
|
||||
}
|
||||
|
||||
final Optional<RecurringTransaction> optionalRecurringTransaction = this.recurringTransactionRepository
|
||||
.findById(Long.valueOf(recurringTransactionId));
|
||||
|
||||
if (!optionalRecurringTransaction.isPresent()) {
|
||||
return ResponseReason.RECURRING_TRANSACTION_NOT_FOUND;
|
||||
}
|
||||
|
||||
try {
|
||||
RecurringTransaction recurringTransaction = optionalRecurringTransaction.get();
|
||||
|
||||
recurringTransaction.setDeleted(true);
|
||||
|
||||
this.recurringTransactionRepository.save(recurringTransaction);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Could not delete recurring transaction!", e);
|
||||
|
||||
response = ResponseReason.UNKNOWN_ERROR;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
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.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
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 basic logic rules. While most of the logic could be placed elsewhere this
|
||||
* service provides centralized access to these rules. Placing them in here also enables easy unit testing.
|
||||
*/
|
||||
@Service
|
||||
public class RuleService implements InitializingBean {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(RuleService.class);
|
||||
|
||||
@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() {
|
||||
this.bookingRules = new EnumMap<>(AccountType.class);
|
||||
|
||||
// This map contains valid booking constellations
|
||||
// The key is the from account and the value is a list of valid
|
||||
// to accounts for this from account
|
||||
this.bookingRules.put(INCOME, Arrays.asList(BANK, CASH));
|
||||
this.bookingRules.put(BANK, Arrays.asList(BANK, CASH, EXPENSE, LIABILITY));
|
||||
this.bookingRules.put(CASH, Arrays.asList(BANK, EXPENSE, LIABILITY));
|
||||
this.bookingRules.put(EXPENSE, Collections.emptyList());
|
||||
this.bookingRules.put(LIABILITY, Arrays.asList(BANK, CASH, EXPENSE));
|
||||
this.bookingRules.put(START, Arrays.asList(BANK, CASH, LIABILITY));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param fromAccount the from account to get the multiplier for
|
||||
*
|
||||
* @return the multiplier, either <code>1</code> or <code>-1</code>
|
||||
*/
|
||||
public long getMultiplierFromAccount(Account fromAccount) {
|
||||
// There is no multiplier if the from account is an EXPENSE account because
|
||||
// it's not a valid from account type
|
||||
|
||||
final AccountType accountType = fromAccount.getType();
|
||||
|
||||
if (INCOME.equals(accountType)) {
|
||||
return 1L;
|
||||
} else if (BANK.equals(accountType)) {
|
||||
return -1L;
|
||||
} else if (CASH.equals(accountType)) {
|
||||
return -1L;
|
||||
} else if (LIABILITY.equals(accountType)) {
|
||||
return 1L;
|
||||
} else if (START.equals(accountType)) {
|
||||
return 1L;
|
||||
}
|
||||
|
||||
LOGGER.warn(String
|
||||
.format("Unknown or invalid account type in getMultiplierFromAccount: %s", accountType.name()));
|
||||
|
||||
return 1L;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param toAccount the to account to get the multiplier for
|
||||
*
|
||||
* @return the multiplier, either <code>1</code> or <code>-1</code>
|
||||
*/
|
||||
public long getMultiplierToAccount(Account toAccount) {
|
||||
// There are no multipliers for INCOME and START accounts
|
||||
// because they are not valid to account types
|
||||
|
||||
final AccountType accountType = toAccount.getType();
|
||||
|
||||
if (BANK.equals(accountType)) {
|
||||
return 1L;
|
||||
} else if (CASH.equals(accountType)) {
|
||||
return 1L;
|
||||
} else if (LIABILITY.equals(accountType)) {
|
||||
return -1L;
|
||||
} else if (EXPENSE.equals(accountType)) {
|
||||
return 1L;
|
||||
}
|
||||
|
||||
LOGGER.warn(String
|
||||
.format("Unknown or invalid account type in getMultiplierToAccount: %s", accountType.name()));
|
||||
|
||||
return -1L;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method validates whether the booking from <code>fromAccount</code> to <code>toAccount</code> is valid, e.g.
|
||||
* booking directly from an {@link AccountType#INCOME INCOME} to an {@link AccountType#EXPENSE EXPENSE} 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
|
||||
*/
|
||||
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) {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String.format("Use state '%s' for holiday calculation", this.financerConfig.getState()));
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
package de.financer.service;
|
||||
|
||||
import de.financer.ResponseReason;
|
||||
import de.financer.config.FinancerConfig;
|
||||
import de.financer.dba.TransactionRepository;
|
||||
import de.financer.model.Account;
|
||||
import de.financer.model.AccountType;
|
||||
import de.financer.model.RecurringTransaction;
|
||||
import de.financer.model.Transaction;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.math.NumberUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
public class TransactionService {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(TransactionService.class);
|
||||
|
||||
@Autowired
|
||||
private AccountService accountService;
|
||||
|
||||
@Autowired
|
||||
private RuleService ruleService;
|
||||
|
||||
@Autowired
|
||||
private TransactionRepository transactionRepository;
|
||||
|
||||
@Autowired
|
||||
private FinancerConfig financerConfig;
|
||||
|
||||
/**
|
||||
* @return all transactions, for all accounts and all time
|
||||
*/
|
||||
public Iterable<Transaction> getAll() {
|
||||
return this.transactionRepository.findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
public Iterable<Transaction> getAllForAccount(String accountKey) {
|
||||
final Account account = this.accountService.getAccountByKey(accountKey);
|
||||
|
||||
if (account == null) {
|
||||
LOGGER.warn(String.format("Account with key %s not found!", accountKey));
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// As we want all transactions of the given account use it as from and to account
|
||||
return this.transactionRepository.findTransactionsByFromAccountOrToAccount(account, account);
|
||||
}
|
||||
|
||||
@Transactional(propagation = Propagation.REQUIRED)
|
||||
public ResponseReason createTransaction(String fromAccountKey, String toAccountKey, Long amount, String date,
|
||||
String description)
|
||||
{
|
||||
return this.createTransaction(fromAccountKey, toAccountKey, amount, date, description, null);
|
||||
}
|
||||
|
||||
@Transactional(propagation = Propagation.REQUIRED)
|
||||
public ResponseReason createTransaction(String fromAccountKey, String toAccountKey, Long amount, String date,
|
||||
String description, RecurringTransaction recurringTransaction
|
||||
) {
|
||||
final Account fromAccount = this.accountService.getAccountByKey(fromAccountKey);
|
||||
final Account toAccount = this.accountService.getAccountByKey(toAccountKey);
|
||||
ResponseReason response = validateParameters(fromAccount, toAccount, amount, date);
|
||||
|
||||
// If we detected an issue with the given parameters return the first error found to the caller
|
||||
if (response != null) {
|
||||
return response; // early return
|
||||
}
|
||||
|
||||
try {
|
||||
final Transaction transaction = buildTransaction(fromAccount, toAccount, amount, description, date, recurringTransaction);
|
||||
|
||||
fromAccount.setCurrentBalance(fromAccount.getCurrentBalance() + (this.ruleService
|
||||
.getMultiplierFromAccount(fromAccount) * amount));
|
||||
|
||||
// Special case: if we do the initial bookings, and the booking is to introduce a liability,
|
||||
// the balance of the liability account must increase
|
||||
if (AccountType.START.equals(fromAccount.getType()) && AccountType.LIABILITY.equals(toAccount.getType())) {
|
||||
toAccount.setCurrentBalance(toAccount.getCurrentBalance() + (this.ruleService
|
||||
.getMultiplierToAccount(toAccount) * amount * -1));
|
||||
}
|
||||
else {
|
||||
toAccount.setCurrentBalance(toAccount.getCurrentBalance() + (this.ruleService
|
||||
.getMultiplierToAccount(toAccount) * amount));
|
||||
}
|
||||
|
||||
this.transactionRepository.save(transaction);
|
||||
|
||||
this.accountService.saveAccount(fromAccount);
|
||||
this.accountService.saveAccount(toAccount);
|
||||
|
||||
response = ResponseReason.OK;
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Could not create transaction!", e);
|
||||
|
||||
response = ResponseReason.UNKNOWN_ERROR;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 description the description of the transaction
|
||||
* @param date the date of the transaction
|
||||
* @param recurringTransaction the recurring transaction that caused the creation of this transaction, may be
|
||||
* <code>null</code>
|
||||
*
|
||||
* @return the build {@link Transaction} instance
|
||||
*/
|
||||
private Transaction buildTransaction(Account fromAccount, Account toAccount, Long amount, String description,
|
||||
String date, RecurringTransaction recurringTransaction
|
||||
) {
|
||||
final Transaction transaction = new Transaction();
|
||||
|
||||
transaction.setFromAccount(fromAccount);
|
||||
transaction.setToAccount(toAccount);
|
||||
transaction.setAmount(amount);
|
||||
transaction.setDescription(description);
|
||||
transaction.setDate(LocalDate.parse(date, DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat())));
|
||||
transaction.setRecurringTransaction(recurringTransaction);
|
||||
|
||||
return transaction;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @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) {
|
||||
ResponseReason response = null;
|
||||
|
||||
if (fromAccount == null && toAccount == null) {
|
||||
response = ResponseReason.FROM_AND_TO_ACCOUNT_NOT_FOUND;
|
||||
} else if (toAccount == null) {
|
||||
response = ResponseReason.TO_ACCOUNT_NOT_FOUND;
|
||||
} else if (fromAccount == null) {
|
||||
response = ResponseReason.FROM_ACCOUNT_NOT_FOUND;
|
||||
} else if (!this.ruleService.isValidBooking(fromAccount, toAccount)) {
|
||||
response = ResponseReason.INVALID_BOOKING_ACCOUNTS;
|
||||
} else if (amount == null) {
|
||||
response = ResponseReason.MISSING_AMOUNT;
|
||||
} else if (amount == 0L) {
|
||||
response = ResponseReason.AMOUNT_ZERO;
|
||||
} else if (StringUtils.isEmpty(date)) {
|
||||
response = ResponseReason.MISSING_DATE;
|
||||
} else if (StringUtils.isNotEmpty(date)) {
|
||||
try {
|
||||
LocalDate.parse(date, DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat()));
|
||||
} catch (DateTimeParseException e) {
|
||||
response = ResponseReason.INVALID_DATE_FORMAT;
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@Transactional(propagation = Propagation.REQUIRED)
|
||||
public ResponseReason deleteTransaction(String transactionId) {
|
||||
ResponseReason response = ResponseReason.OK;
|
||||
|
||||
if (transactionId == null) {
|
||||
return ResponseReason.MISSING_TRANSACTION_ID;
|
||||
} else if (!NumberUtils.isCreatable(transactionId)) {
|
||||
return ResponseReason.INVALID_TRANSACTION_ID;
|
||||
}
|
||||
|
||||
final Optional<Transaction> optionalTransaction = this.transactionRepository
|
||||
.findById(Long.valueOf(transactionId));
|
||||
|
||||
if (!optionalTransaction.isPresent()) {
|
||||
return ResponseReason.TRANSACTION_NOT_FOUND;
|
||||
}
|
||||
|
||||
final Transaction transaction = optionalTransaction.get();
|
||||
final Account fromAccount = transaction.getFromAccount();
|
||||
final Account toAccount = transaction.getToAccount();
|
||||
final Long amount = transaction.getAmount();
|
||||
|
||||
// Invert the actual multiplier by multiplying with -1
|
||||
// If we delete a transaction we do the inverse of the original transaction
|
||||
fromAccount.setCurrentBalance(fromAccount.getCurrentBalance() + (this.ruleService
|
||||
.getMultiplierFromAccount(fromAccount) * amount * -1));
|
||||
toAccount.setCurrentBalance(toAccount.getCurrentBalance() + (this.ruleService
|
||||
.getMultiplierToAccount(toAccount) * amount * -1));
|
||||
|
||||
try {
|
||||
this.transactionRepository.deleteById(Long.valueOf(transactionId));
|
||||
|
||||
this.accountService.saveAccount(fromAccount);
|
||||
this.accountService.saveAccount(toAccount);
|
||||
}
|
||||
catch (Exception e) {
|
||||
LOGGER.error("Could not delete transaction!", e);
|
||||
|
||||
response = ResponseReason.UNKNOWN_ERROR;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,94 @@
|
||||
package de.financer.task;
|
||||
|
||||
import de.financer.config.FinancerConfig;
|
||||
import de.financer.model.RecurringTransaction;
|
||||
import de.financer.service.RecurringTransactionService;
|
||||
import org.apache.commons.collections4.IterableUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.mail.MailException;
|
||||
import org.springframework.mail.SimpleMailMessage;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Component
|
||||
public class SendRecurringTransactionReminderTask {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(SendRecurringTransactionReminderTask.class);
|
||||
|
||||
@Autowired
|
||||
private RecurringTransactionService recurringTransactionService;
|
||||
|
||||
@Autowired
|
||||
private FinancerConfig financerConfig;
|
||||
|
||||
@Autowired
|
||||
private JavaMailSender mailSender;
|
||||
|
||||
@Scheduled(cron = "0 30 0 * * *")
|
||||
public void sendReminder() {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("Enter recurring transaction reminder task");
|
||||
}
|
||||
|
||||
Iterable<RecurringTransaction> recurringTransactions = this.recurringTransactionService.getAllDueToday();
|
||||
|
||||
// If no recurring transaction is due today we don't need to send a reminder
|
||||
if (IterableUtils.isEmpty(recurringTransactions)) {
|
||||
LOGGER.info("No recurring transactions due today!");
|
||||
|
||||
return; // early return
|
||||
}
|
||||
|
||||
// TODO Filtering currently happens in memory but should be done via SQL
|
||||
recurringTransactions = IterableUtils.toList(recurringTransactions)
|
||||
.stream()
|
||||
.filter((rt) -> rt.isRemind())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
LOGGER.info(String
|
||||
.format("%s recurring transaction are due today and are about to be included in the reminder email",
|
||||
IterableUtils.size(recurringTransactions)));
|
||||
|
||||
final StringBuilder reminderBuilder = new StringBuilder();
|
||||
|
||||
reminderBuilder.append("The following recurring transactions are due today:")
|
||||
.append(System.lineSeparator())
|
||||
.append(System.lineSeparator());
|
||||
|
||||
IterableUtils.toList(recurringTransactions).forEach((rt) -> {
|
||||
reminderBuilder.append(rt.getId())
|
||||
.append("|")
|
||||
.append(rt.getDescription())
|
||||
.append(System.lineSeparator())
|
||||
.append("From ")
|
||||
.append(rt.getFromAccount().getKey())
|
||||
.append(" to ")
|
||||
.append(rt.getToAccount().getKey())
|
||||
.append(": ")
|
||||
.append(rt.getAmount().toString())
|
||||
.append(System.lineSeparator())
|
||||
.append(System.lineSeparator());
|
||||
});
|
||||
|
||||
final SimpleMailMessage msg = new SimpleMailMessage();
|
||||
|
||||
msg.setTo(this.financerConfig.getMailRecipients().toArray(new String[]{}));
|
||||
msg.setFrom(this.financerConfig.getFromAddress());
|
||||
msg.setSubject("[Financer] Recurring transactions reminder");
|
||||
msg.setText(reminderBuilder.toString());
|
||||
|
||||
try {
|
||||
this.mailSender.send(msg);
|
||||
} catch (MailException e) {
|
||||
LOGGER.error("Could not send recurring transaction email reminder!", e);
|
||||
|
||||
LOGGER.info("Dumb email reminder content because the sending failed");
|
||||
LOGGER.info(reminderBuilder.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
# Hibernate
|
||||
spring.jpa.show-sql=true
|
||||
@@ -0,0 +1,6 @@
|
||||
spring.flyway.locations=classpath:/database/hsqldb,classpath:/database/common
|
||||
|
||||
# DataSource
|
||||
#spring.datasource.url=jdbc:hsqldb:file:/tmp/financer
|
||||
spring.datasource.url=jdbc:hsqldb:mem:.
|
||||
spring.datasource.username=sa
|
||||
@@ -0,0 +1,8 @@
|
||||
spring.flyway.locations=classpath:/database/postgres,classpath:/database/common
|
||||
|
||||
spring.datasource.url=jdbc:postgresql://localhost/financer
|
||||
spring.datasource.username=financer
|
||||
spring.datasource.password=financer
|
||||
|
||||
# See https://github.com/spring-projects/spring-boot/issues/12007
|
||||
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
|
||||
@@ -0,0 +1,49 @@
|
||||
###
|
||||
### This is the main configuration file of the application.
|
||||
### Filtering of the @...@ values happens via the maven-resource-plugin. The execution of the plugin is configured in
|
||||
### the Spring Boot parent POM.
|
||||
|
||||
spring.profiles.active=@activeProfiles@
|
||||
|
||||
server.servlet.context-path=/financer-server
|
||||
server.port=8089
|
||||
|
||||
spring.jpa.hibernate.ddl-auto=validate
|
||||
|
||||
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@
|
||||
|
||||
logging.level.de.financer=DEBUG
|
||||
logging.file=financer-server.log
|
||||
logging.file.max-history=7
|
||||
logging.file.max-size=50MB
|
||||
|
||||
# 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
|
||||
|
||||
# The date format of the client-supplied date string, used to parse the string into a proper object
|
||||
financer.dateFormat=dd.MM.yyyy
|
||||
|
||||
# A collection of email addresses that should receive mails from financer
|
||||
financer.mailRecipients[0]=marius@kleberonline.de
|
||||
|
||||
# The from address used in emails send by financer
|
||||
financer.fromAddress=financer@77zzcx7.de
|
||||
|
||||
# Mail configuration
|
||||
spring.mail.host=localhost
|
||||
#spring.mail.username=
|
||||
#spring.mail.password=
|
||||
|
||||
# Disable JMX as we don't need it and it blocks parallel deployment on Tomcat
|
||||
# because the connection pool cannot shutdown properly
|
||||
spring.jmx.enabled=false
|
||||
@@ -0,0 +1,66 @@
|
||||
-- Accounts
|
||||
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);
|
||||
|
||||
INSERT INTO account ("key", type, status, current_balance)
|
||||
VALUES ('accounts.rent', 'EXPENSE', 'OPEN', 0);
|
||||
|
||||
INSERT INTO account ("key", type, status, current_balance)
|
||||
VALUES ('accounts.fvs', 'EXPENSE', 'OPEN', 0);
|
||||
|
||||
INSERT INTO account ("key", type, status, current_balance)
|
||||
VALUES ('accounts.car', 'EXPENSE', 'OPEN', 0);
|
||||
|
||||
INSERT INTO account ("key", type, status, current_balance)
|
||||
VALUES ('accounts.gas', 'EXPENSE', 'OPEN', 0);
|
||||
|
||||
INSERT INTO account ("key", type, status, current_balance)
|
||||
VALUES ('accounts.alimony', 'EXPENSE', 'OPEN', 0);
|
||||
|
||||
INSERT INTO account ("key", type, status, current_balance)
|
||||
VALUES ('accounts.electricitywater', 'EXPENSE', 'OPEN', 0);
|
||||
|
||||
INSERT INTO account ("key", type, status, current_balance)
|
||||
VALUES ('accounts.mobile', 'EXPENSE', 'OPEN', 0);
|
||||
|
||||
INSERT INTO account ("key", type, status, current_balance)
|
||||
VALUES ('accounts.internet', 'EXPENSE', 'OPEN', 0);
|
||||
|
||||
INSERT INTO account ("key", type, status, current_balance)
|
||||
VALUES ('accounts.legalinsurance', 'EXPENSE', 'OPEN', 0);
|
||||
|
||||
INSERT INTO account ("key", type, status, current_balance)
|
||||
VALUES ('accounts.netflix', 'EXPENSE', 'OPEN', 0);
|
||||
|
||||
INSERT INTO account ("key", type, status, current_balance)
|
||||
VALUES ('accounts.hetzner', 'EXPENSE', 'OPEN', 0);
|
||||
|
||||
INSERT INTO account ("key", type, status, current_balance)
|
||||
VALUES ('accounts.fees', 'EXPENSE', 'OPEN', 0);
|
||||
|
||||
INSERT INTO account ("key", type, status, current_balance)
|
||||
VALUES ('accounts.food', 'EXPENSE', 'OPEN', 0);
|
||||
|
||||
INSERT INTO account ("key", type, status, current_balance)
|
||||
VALUES ('accounts.foodexternal', 'EXPENSE', 'OPEN', 0);
|
||||
|
||||
INSERT INTO account ("key", type, status, current_balance)
|
||||
VALUES ('accounts.child', 'EXPENSE', 'OPEN', 0);
|
||||
|
||||
INSERT INTO account ("key", type, status, current_balance)
|
||||
VALUES ('accounts.creditcard', 'LIABILITY', 'OPEN', 0);
|
||||
|
||||
INSERT INTO account ("key", type, status, current_balance)
|
||||
VALUES ('accounts.studentloan', 'LIABILITY', 'OPEN', 0);
|
||||
|
||||
INSERT INTO account ("key", type, status, current_balance)
|
||||
VALUES ('accounts.bed', 'LIABILITY', 'OPEN', 0);
|
||||
@@ -0,0 +1,88 @@
|
||||
-- Rename all accounts to proper names instead of the artificial 'accounts.' names
|
||||
UPDATE account
|
||||
SET "key" = 'Check account'
|
||||
WHERE "key" = 'accounts.checkaccount';
|
||||
|
||||
UPDATE account
|
||||
SET "key" = 'Income'
|
||||
WHERE "key" = 'accounts.income';
|
||||
|
||||
UPDATE account
|
||||
SET "key" = 'Cash'
|
||||
WHERE "key" = 'accounts.cash';
|
||||
|
||||
UPDATE account
|
||||
SET "key" = 'Start'
|
||||
WHERE "key" = 'accounts.start';
|
||||
|
||||
UPDATE account
|
||||
SET "key" = 'Rent'
|
||||
WHERE "key" = 'accounts.rent';
|
||||
|
||||
UPDATE account
|
||||
SET "key" = 'FVS'
|
||||
WHERE "key" = 'accounts.fvs';
|
||||
|
||||
UPDATE account
|
||||
SET "key" = 'Car'
|
||||
WHERE "key" = 'accounts.car';
|
||||
|
||||
UPDATE account
|
||||
SET "key" = 'Gas'
|
||||
WHERE "key" = 'accounts.gas';
|
||||
|
||||
UPDATE account
|
||||
SET "key" = 'Alimony'
|
||||
WHERE "key" = 'accounts.alimony';
|
||||
|
||||
UPDATE account
|
||||
SET "key" = 'Electricity/Water'
|
||||
WHERE "key" = 'accounts.electricitywater';
|
||||
|
||||
UPDATE account
|
||||
SET "key" = 'Mobile'
|
||||
WHERE "key" = 'accounts.mobile';
|
||||
|
||||
UPDATE account
|
||||
SET "key" = 'Internet'
|
||||
WHERE "key" = 'accounts.internet';
|
||||
|
||||
UPDATE account
|
||||
SET "key" = 'Legal insurance'
|
||||
WHERE "key" = 'accounts.legalinsurance';
|
||||
|
||||
UPDATE account
|
||||
SET "key" = 'Netflix'
|
||||
WHERE "key" = 'accounts.netflix';
|
||||
|
||||
UPDATE account
|
||||
SET "key" = 'Hetzner'
|
||||
WHERE "key" = 'accounts.hetzner';
|
||||
|
||||
UPDATE account
|
||||
SET "key" = 'Fees'
|
||||
WHERE "key" = 'accounts.fees';
|
||||
|
||||
UPDATE account
|
||||
SET "key" = 'Food'
|
||||
WHERE "key" = 'accounts.food';
|
||||
|
||||
UPDATE account
|
||||
SET "key" = 'Food (external)'
|
||||
WHERE "key" = 'accounts.foodexternal';
|
||||
|
||||
UPDATE account
|
||||
SET "key" = 'Child'
|
||||
WHERE "key" = 'accounts.child';
|
||||
|
||||
UPDATE account
|
||||
SET "key" = 'Credit card'
|
||||
WHERE "key" = 'accounts.creditcard';
|
||||
|
||||
UPDATE account
|
||||
SET "key" = 'Student loan'
|
||||
WHERE "key" = 'accounts.studentloan';
|
||||
|
||||
UPDATE account
|
||||
SET "key" = 'Bed'
|
||||
WHERE "key" = 'accounts.bed';
|
||||
@@ -0,0 +1,17 @@
|
||||
INSERT INTO account_group (name)
|
||||
VALUES ('Miscellaneous');
|
||||
|
||||
INSERT INTO account_group (name)
|
||||
VALUES ('Car');
|
||||
|
||||
INSERT INTO account_group (name)
|
||||
VALUES ('Housing');
|
||||
|
||||
INSERT INTO account_group (name)
|
||||
VALUES ('Child');
|
||||
|
||||
INSERT INTO account_group (name)
|
||||
VALUES ('Insurance');
|
||||
|
||||
INSERT INTO account_group (name)
|
||||
VALUES ('Entertainment');
|
||||
@@ -0,0 +1,46 @@
|
||||
--
|
||||
-- This file contains the basic initialization of the financer schema and basic init data
|
||||
--
|
||||
|
||||
-- Account table
|
||||
CREATE TABLE account (
|
||||
id BIGINT NOT NULL PRIMARY KEY IDENTITY,
|
||||
"key" VARCHAR(1000) NOT NULL, --escape keyword "key"
|
||||
type VARCHAR(255) NOT NULL,
|
||||
status VARCHAR(255) NOT NULL,
|
||||
current_balance BIGINT NOT NULL,
|
||||
|
||||
CONSTRAINT un_account_name_key UNIQUE ("key")
|
||||
);
|
||||
|
||||
-- Recurring transaction table
|
||||
CREATE TABLE recurring_transaction (
|
||||
id BIGINT NOT NULL PRIMARY KEY IDENTITY,
|
||||
from_account_id BIGINT NOT NULL,
|
||||
to_account_id BIGINT NOT NULL,
|
||||
description VARCHAR(1000),
|
||||
amount BIGINT NOT NULL,
|
||||
interval_type VARCHAR(255) NOT NULL,
|
||||
first_occurrence DATE NOT NULL,
|
||||
last_occurrence DATE,
|
||||
holiday_weekend_type VARCHAR(255) NOT NULL,
|
||||
deleted BOOLEAN DEFAULT FALSE NOT NULL,
|
||||
|
||||
CONSTRAINT fk_recurring_transaction_from_account FOREIGN KEY (from_account_id) REFERENCES account (id),
|
||||
CONSTRAINT fk_recurring_transaction_to_account FOREIGN KEY (to_account_id) REFERENCES account (id)
|
||||
);
|
||||
|
||||
-- Transaction table
|
||||
CREATE TABLE "transaction" ( --escape keyword "transaction"
|
||||
id BIGINT NOT NULL PRIMARY KEY IDENTITY,
|
||||
from_account_id BIGINT NOT NULL,
|
||||
to_account_id BIGINT NOT NULL,
|
||||
"date" DATE NOT NULL, --escape keyword "date"
|
||||
description VARCHAR(1000),
|
||||
amount BIGINT NOT NULL,
|
||||
recurring_transaction_id BIGINT,
|
||||
|
||||
CONSTRAINT fk_transaction_from_account FOREIGN KEY (from_account_id) REFERENCES account (id),
|
||||
CONSTRAINT fk_transaction_to_account FOREIGN KEY (to_account_id) REFERENCES account (id),
|
||||
CONSTRAINT fk_transaction_recurring_transaction FOREIGN KEY (recurring_transaction_id) REFERENCES recurring_transaction (id)
|
||||
);
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Add a new column to the recurring transaction table that controls whether
|
||||
-- a reminder about the maturity should be send
|
||||
ALTER TABLE recurring_transaction
|
||||
ADD COLUMN remind BOOLEAN DEFAULT TRUE NOT NULL;
|
||||
@@ -0,0 +1,16 @@
|
||||
-- Account group table
|
||||
CREATE TABLE account_group (
|
||||
id BIGINT NOT NULL PRIMARY KEY IDENTITY,
|
||||
name VARCHAR(1000) NOT NULL,
|
||||
|
||||
CONSTRAINT un_account_group_name_key UNIQUE (name)
|
||||
);
|
||||
|
||||
-- Add a column for the Account group
|
||||
ALTER TABLE account
|
||||
ADD COLUMN account_group_id BIGINT;
|
||||
|
||||
-- Add a foreign key column to the Account table referencing an Account group
|
||||
ALTER TABLE account
|
||||
ADD CONSTRAINT fk_account_account_group
|
||||
FOREIGN KEY (account_group_id) REFERENCES account_group (id);
|
||||
@@ -0,0 +1,46 @@
|
||||
--
|
||||
-- This file contains the basic initialization of the financer schema and basic init data
|
||||
--
|
||||
|
||||
-- Account table
|
||||
CREATE TABLE account (
|
||||
id BIGINT NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
"key" VARCHAR(1000) NOT NULL, --escape keyword "key"
|
||||
type VARCHAR(255) NOT NULL,
|
||||
status VARCHAR(255) NOT NULL,
|
||||
current_balance BIGINT NOT NULL,
|
||||
|
||||
CONSTRAINT un_account_name_key UNIQUE ("key")
|
||||
);
|
||||
|
||||
-- Recurring transaction table
|
||||
CREATE TABLE recurring_transaction (
|
||||
id BIGINT NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
from_account_id BIGINT NOT NULL,
|
||||
to_account_id BIGINT NOT NULL,
|
||||
description VARCHAR(1000),
|
||||
amount BIGINT NOT NULL,
|
||||
interval_type VARCHAR(255) NOT NULL,
|
||||
first_occurrence DATE NOT NULL,
|
||||
last_occurrence DATE,
|
||||
holiday_weekend_type VARCHAR(255) NOT NULL,
|
||||
deleted BOOLEAN DEFAULT 'TRUE' NOT NULL,
|
||||
|
||||
CONSTRAINT fk_recurring_transaction_from_account FOREIGN KEY (from_account_id) REFERENCES account (id),
|
||||
CONSTRAINT fk_recurring_transaction_to_account FOREIGN KEY (to_account_id) REFERENCES account (id)
|
||||
);
|
||||
|
||||
-- Transaction table
|
||||
CREATE TABLE "transaction" ( --escape keyword "transaction"
|
||||
id BIGINT NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
from_account_id BIGINT NOT NULL,
|
||||
to_account_id BIGINT NOT NULL,
|
||||
"date" DATE NOT NULL, --escape keyword "date"
|
||||
description VARCHAR(1000),
|
||||
amount BIGINT NOT NULL,
|
||||
recurring_transaction_id BIGINT,
|
||||
|
||||
CONSTRAINT fk_transaction_from_account FOREIGN KEY (from_account_id) REFERENCES account (id),
|
||||
CONSTRAINT fk_transaction_to_account FOREIGN KEY (to_account_id) REFERENCES account (id),
|
||||
CONSTRAINT fk_transaction_recurring_transaction FOREIGN KEY (recurring_transaction_id) REFERENCES recurring_transaction (id)
|
||||
);
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Add a new column to the recurring transaction table that controls whether
|
||||
-- a reminder about the maturity should be send
|
||||
ALTER TABLE recurring_transaction
|
||||
ADD COLUMN remind BOOLEAN DEFAULT 'TRUE' NOT NULL
|
||||
@@ -0,0 +1,16 @@
|
||||
-- Account group table
|
||||
CREATE TABLE account_group (
|
||||
id BIGINT NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
name VARCHAR(1000) NOT NULL,
|
||||
|
||||
CONSTRAINT un_account_group_name_key UNIQUE (name)
|
||||
);
|
||||
|
||||
-- Add a column for the Account group
|
||||
ALTER TABLE account
|
||||
ADD COLUMN account_group_id BIGINT;
|
||||
|
||||
-- Add a foreign key column to the Account table referencing an Account group
|
||||
ALTER TABLE account
|
||||
ADD CONSTRAINT fk_account_account_group
|
||||
FOREIGN KEY (account_group_id) REFERENCES account_group (id);
|
||||
@@ -0,0 +1,25 @@
|
||||
The recurring transaction table is defined like this (at least for postgres):
|
||||
-- Recurring transaction table
|
||||
CREATE TABLE recurring_transaction (
|
||||
id BIGINT NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
from_account_id BIGINT NOT NULL,
|
||||
to_account_id BIGINT NOT NULL,
|
||||
description VARCHAR(1000),
|
||||
amount BIGINT NOT NULL,
|
||||
interval_type VARCHAR(255) NOT NULL,
|
||||
first_occurrence DATE NOT NULL,
|
||||
last_occurrence DATE,
|
||||
holiday_weekend_type VARCHAR(255) NOT NULL,
|
||||
deleted BOOLEAN DEFAULT 'TRUE' NOT NULL,
|
||||
|
||||
CONSTRAINT fk_recurring_transaction_from_account FOREIGN KEY (from_account_id) REFERENCES account (id),
|
||||
CONSTRAINT fk_recurring_transaction_to_account FOREIGN KEY (to_account_id) REFERENCES account (id)
|
||||
);
|
||||
|
||||
Note the
|
||||
deleted BOOLEAN DEFAULT 'TRUE' NOT NULL,
|
||||
column definition. Not sure why the default is TRUE here is it doesn't make sense.
|
||||
It was probably a mistake, however fixing it here _WILL_ break existing installations
|
||||
as Flyway uses a checksum for scripts. So there is no easy fix, except for effectively
|
||||
overwriting this default in Java code when creating a new recurring transaction.
|
||||
See RecurringTransactionService.createRecurringTransaction()
|
||||
@@ -0,0 +1,31 @@
|
||||
package de.financer;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest(classes = FinancerApplication.class)
|
||||
@AutoConfigureMockMvc
|
||||
@TestPropertySource(
|
||||
locations = "classpath:application-integrationtest.properties")
|
||||
public class FinancerApplicationBootTest {
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Test
|
||||
public void test_appBoots() {
|
||||
// Nothing to do in this test as we just want to startup the app
|
||||
// to make sure that spring, flyway and hibernate all work
|
||||
// as expected even after changes
|
||||
// While this slightly increases build time it's an easy and safe
|
||||
// way to ensure that the app can start
|
||||
Assert.assertTrue(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package de.financer.controller.integration;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import de.financer.FinancerApplication;
|
||||
import de.financer.model.Account;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest(classes = FinancerApplication.class)
|
||||
@AutoConfigureMockMvc
|
||||
@TestPropertySource(
|
||||
locations = "classpath:application-integrationtest.properties")
|
||||
public class AccountController_getAllIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@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 List<Account> allAccounts = this.objectMapper
|
||||
.readValue(mvcResult.getResponse().getContentAsByteArray(), new TypeReference<List<Account>>() {});
|
||||
|
||||
Assert.assertEquals(23, allAccounts.size());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package de.financer.controller.integration;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import de.financer.FinancerApplication;
|
||||
import de.financer.model.RecurringTransaction;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest(classes = FinancerApplication.class)
|
||||
@AutoConfigureMockMvc
|
||||
@TestPropertySource(
|
||||
locations = "classpath:application-integrationtest.properties")
|
||||
public class RecurringTransactionService_createRecurringTransactionIntegrationTest {
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Test
|
||||
public void test_createRecurringTransaction() throws Exception {
|
||||
final MvcResult mvcRequest = this.mockMvc.perform(get("/recurringTransactions/createRecurringTransaction")
|
||||
.param("fromAccountKey", "Income")
|
||||
.param("toAccountKey", "Check account")
|
||||
.param("amount", "250000")
|
||||
.param("description", "Monthly rent")
|
||||
.param("holidayWeekendType", "SAME_DAY")
|
||||
.param("intervalType", "MONTHLY")
|
||||
.param("firstOccurrence", "07.03.2019")
|
||||
.param("remind", "true"))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
|
||||
final MvcResult mvcResult = this.mockMvc.perform(get("/recurringTransactions/getAll")
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
|
||||
final List<RecurringTransaction> allRecurringTransaction = this.objectMapper
|
||||
.readValue(mvcResult.getResponse().getContentAsByteArray(), new TypeReference<List<RecurringTransaction>>() {});
|
||||
|
||||
Assert.assertEquals(4, allRecurringTransaction.size());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package de.financer.controller.integration;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import de.financer.FinancerApplication;
|
||||
import de.financer.model.RecurringTransaction;
|
||||
import de.financer.model.Transaction;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest(classes = FinancerApplication.class)
|
||||
@AutoConfigureMockMvc
|
||||
@TestPropertySource(
|
||||
locations = "classpath:application-integrationtest.properties")
|
||||
public class RecurringTransactionService_createTransactionIntegrationTest {
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Test
|
||||
public void test_createTransaction() throws Exception {
|
||||
final MvcResult mvcResultAll = this.mockMvc.perform(get("/recurringTransactions/getAll")
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
|
||||
final List<RecurringTransaction> allRecurringTransactions = this.objectMapper
|
||||
.readValue(mvcResultAll.getResponse().getContentAsByteArray(), new TypeReference<List<RecurringTransaction>>() {});
|
||||
final Optional<RecurringTransaction> optionalRecurringTransaction = allRecurringTransactions.stream().findFirst();
|
||||
|
||||
if (!optionalRecurringTransaction.isPresent()) {
|
||||
Assert.fail("No recurring transaction found!");
|
||||
}
|
||||
|
||||
this.mockMvc.perform(get("/recurringTransactions/createTransaction")
|
||||
.param("recurringTransactionId", optionalRecurringTransaction.get().getId().toString()))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
|
||||
final MvcResult mvcResultAllTransactions = this.mockMvc.perform(get("/transactions/getAll")
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
|
||||
final List<Transaction> allTransactions = this.objectMapper
|
||||
.readValue(mvcResultAllTransactions.getResponse().getContentAsByteArray(), new TypeReference<List<Transaction>>() {});
|
||||
|
||||
Assert.assertEquals(1, allTransactions.size());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package de.financer.controller.integration;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import de.financer.FinancerApplication;
|
||||
import de.financer.model.RecurringTransaction;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest(classes = FinancerApplication.class)
|
||||
@AutoConfigureMockMvc
|
||||
@TestPropertySource(
|
||||
locations = "classpath:application-integrationtest.properties")
|
||||
public class RecurringTransactionService_getAllActiveIntegrationTest {
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Test
|
||||
public void test_getAll() throws Exception {
|
||||
final MvcResult mvcResult = this.mockMvc
|
||||
.perform(get("/recurringTransactions/getAllActive").contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
|
||||
final List<RecurringTransaction> allRecurringTransactions = this.objectMapper
|
||||
.readValue(mvcResult.getResponse().getContentAsByteArray(), new TypeReference<List<RecurringTransaction>>() {});
|
||||
|
||||
Assert.assertEquals(3, allRecurringTransactions.size());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package de.financer.controller.integration;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import de.financer.FinancerApplication;
|
||||
import de.financer.model.RecurringTransaction;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest(classes = FinancerApplication.class)
|
||||
@AutoConfigureMockMvc
|
||||
@TestPropertySource(
|
||||
locations = "classpath:application-integrationtest.properties")
|
||||
public class RecurringTransactionService_getAllIntegrationTest {
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Test
|
||||
public void test_getAll() throws Exception {
|
||||
final MvcResult mvcResult = this.mockMvc
|
||||
.perform(get("/recurringTransactions/getAll").contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
|
||||
final List<RecurringTransaction> allRecurringTransactions = this.objectMapper
|
||||
.readValue(mvcResult.getResponse().getContentAsByteArray(), new TypeReference<List<RecurringTransaction>>() {});
|
||||
|
||||
Assert.assertEquals(4, allRecurringTransactions.size());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package de.financer.service;
|
||||
|
||||
import de.financer.ResponseReason;
|
||||
import de.financer.dba.AccountGroupRepository;
|
||||
import de.financer.model.AccountGroup;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class AccountGroupService_createAccountGroupTest {
|
||||
|
||||
@InjectMocks
|
||||
private AccountGroupService classUnderTest;
|
||||
|
||||
@Mock
|
||||
private AccountGroupRepository accountGroupRepository;
|
||||
|
||||
@Test
|
||||
public void test_createAccount_UNKNOWN_ERROR() {
|
||||
// Arrange
|
||||
Mockito.doThrow(new NullPointerException()).when(this.accountGroupRepository).save(Mockito.any(AccountGroup.class));
|
||||
|
||||
// Act
|
||||
ResponseReason response = this.classUnderTest.createAccountGroup("Test");
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.UNKNOWN_ERROR, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_createAccount_OK() {
|
||||
// Arrange
|
||||
// Nothing to do
|
||||
|
||||
// Act
|
||||
ResponseReason response = this.classUnderTest.createAccountGroup("Test");
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.OK, response);
|
||||
Mockito.verify(this.accountGroupRepository, Mockito.times(1))
|
||||
.save(ArgumentMatchers.argThat((ag) -> "Test".equals(ag.getName())));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_createAccount_DUPLICATE_ACCOUNT_GROUP_NAME() {
|
||||
// Arrange
|
||||
Mockito.doThrow(new DataIntegrityViolationException("DIVE")).when(this.accountGroupRepository).save(Mockito.any(AccountGroup.class));
|
||||
|
||||
// Act
|
||||
ResponseReason response = this.classUnderTest.createAccountGroup("Test");
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.DUPLICATE_ACCOUNT_GROUP_NAME, response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package de.financer.service;
|
||||
|
||||
import de.financer.ResponseReason;
|
||||
import de.financer.dba.AccountRepository;
|
||||
import de.financer.model.Account;
|
||||
import de.financer.model.AccountGroup;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class AccountService_createAccountTest {
|
||||
@InjectMocks
|
||||
private AccountService classUnderTest;
|
||||
|
||||
@Mock
|
||||
private AccountGroupService accountGroupService;
|
||||
|
||||
@Mock
|
||||
private AccountRepository accountRepository;
|
||||
|
||||
@Test
|
||||
public void test_createAccount_INVALID_ACCOUNT_TYPE() {
|
||||
// Arrange
|
||||
// Nothing to do
|
||||
|
||||
// Act
|
||||
ResponseReason response = this.classUnderTest.createAccount(null, null, null);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.INVALID_ACCOUNT_TYPE, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_createAccount_UNKNOWN_ERROR() {
|
||||
// Arrange
|
||||
Mockito.doThrow(new NullPointerException()).when(this.accountRepository).save(Mockito.any(Account.class));
|
||||
|
||||
// Act
|
||||
ResponseReason response = this.classUnderTest.createAccount("Test", "BANK", null);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.UNKNOWN_ERROR, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_createAccount_OK() {
|
||||
// Arrange
|
||||
// Nothing to do
|
||||
|
||||
// Act
|
||||
ResponseReason response = this.classUnderTest.createAccount("Test", "BANK", null);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.OK, response);
|
||||
Mockito.verify(this.accountRepository, Mockito.times(1))
|
||||
.save(ArgumentMatchers.argThat((acc) -> "Test".equals(acc.getKey())));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_createAccount_ACCOUNT_GROUP_NOT_FOUND() {
|
||||
// Arrange
|
||||
Mockito.when(this.accountGroupService.getAccountGroupByName(Mockito.anyString()))
|
||||
.thenReturn(null);
|
||||
|
||||
// Act
|
||||
ResponseReason response = this.classUnderTest.createAccount("Test", "BANK", "Group1");
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.ACCOUNT_GROUP_NOT_FOUND, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_createAccount_DUPLICATE_ACCOUNT_KEY() {
|
||||
// Arrange
|
||||
Mockito.doThrow(new DataIntegrityViolationException("DIVE")).when(this.accountRepository).save(Mockito.any(Account.class));
|
||||
|
||||
// Act
|
||||
ResponseReason response = this.classUnderTest.createAccount("Test", "BANK", null);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.DUPLICATE_ACCOUNT_KEY, response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package de.financer.service;
|
||||
|
||||
import de.financer.ResponseReason;
|
||||
import de.financer.dba.AccountRepository;
|
||||
import de.financer.model.Account;
|
||||
import de.financer.model.AccountStatus;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class AccountService_setAccountStatusTest {
|
||||
@InjectMocks
|
||||
private AccountService classUnderTest;
|
||||
|
||||
@Mock
|
||||
private AccountRepository accountRepository;
|
||||
|
||||
@Test
|
||||
public void test_setAccountStatus_ACCOUNT_NOT_FOUND() {
|
||||
// Arrange
|
||||
// Nothing to do
|
||||
|
||||
// Act
|
||||
ResponseReason response = this.classUnderTest.setAccountStatus("Test", AccountStatus.CLOSED);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.ACCOUNT_NOT_FOUND, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_setAccountStatus_UNKNOWN_ERROR() {
|
||||
// Arrange
|
||||
Mockito.when(this.accountRepository.findByKey(Mockito.anyString())).thenReturn(new Account());
|
||||
Mockito.doThrow(new NullPointerException()).when(this.accountRepository).save(Mockito.any(Account.class));
|
||||
|
||||
// Act
|
||||
ResponseReason response = this.classUnderTest.setAccountStatus("Test", AccountStatus.CLOSED);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.UNKNOWN_ERROR, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_setAccountStatus_OK() {
|
||||
// Arrange
|
||||
Mockito.when(this.accountRepository.findByKey(Mockito.anyString())).thenReturn(new Account());
|
||||
|
||||
// Act
|
||||
ResponseReason response = this.classUnderTest.setAccountStatus("Test", AccountStatus.CLOSED);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.OK, response);
|
||||
Mockito.verify(this.accountRepository, Mockito.times(1))
|
||||
.save(ArgumentMatchers.argThat((acc) -> AccountStatus.CLOSED.equals(acc.getStatus())));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
package de.financer.service;
|
||||
|
||||
import de.financer.ResponseReason;
|
||||
import de.financer.config.FinancerConfig;
|
||||
import de.financer.dba.RecurringTransactionRepository;
|
||||
import de.financer.model.Account;
|
||||
import de.financer.model.HolidayWeekendType;
|
||||
import de.financer.model.IntervalType;
|
||||
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;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class RecurringTransactionService_createRecurringTransactionTest {
|
||||
@InjectMocks
|
||||
private RecurringTransactionService classUnderTest;
|
||||
|
||||
@Mock
|
||||
private AccountService accountService;
|
||||
|
||||
@Mock
|
||||
private RuleService ruleService;
|
||||
|
||||
@Mock
|
||||
private RecurringTransactionRepository recurringTransactionRepository;
|
||||
|
||||
@Mock
|
||||
private FinancerConfig financerConfig;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
Mockito.when(this.financerConfig.getDateFormat()).thenReturn("dd.MM.yyyy");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_createRecurringTransaction_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.createRecurringTransaction("account.invalid",
|
||||
"account.invalid",
|
||||
Long.valueOf(150l),
|
||||
"DESCRIPTION",
|
||||
"HOLIDAY_WEEKEND_TYPE",
|
||||
"INTERVAL_TYPE",
|
||||
"FIRST_OCCURRENCE",
|
||||
"LAST_OCCURRENCE",
|
||||
Boolean.TRUE);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.FROM_AND_TO_ACCOUNT_NOT_FOUND, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_createRecurringTransaction_TO_ACCOUNT_NOT_FOUND() {
|
||||
// Arrange
|
||||
Mockito.when(this.accountService.getAccountByKey(Mockito.anyString())).thenReturn(createAccount(), null);
|
||||
|
||||
// Act
|
||||
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
|
||||
"account.invalid",
|
||||
Long.valueOf(150l),
|
||||
"DESCRIPTION",
|
||||
"HOLIDAY_WEEKEND_TYPE",
|
||||
"INTERVAL_TYPE",
|
||||
"FIRST_OCCURRENCE",
|
||||
"LAST_OCCURRENCE",
|
||||
Boolean.TRUE);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.TO_ACCOUNT_NOT_FOUND, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_createRecurringTransaction_FROM_ACCOUNT_NOT_FOUND() {
|
||||
// Arrange
|
||||
Mockito.when(this.accountService.getAccountByKey(Mockito.anyString())).thenReturn(null, createAccount());
|
||||
|
||||
// Act
|
||||
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.invalid",
|
||||
"account.to",
|
||||
Long.valueOf(150l),
|
||||
"DESCRIPTION",
|
||||
"HOLIDAY_WEEKEND_TYPE",
|
||||
"INTERVAL_TYPE",
|
||||
"FIRST_OCCURRENCE",
|
||||
"LAST_OCCURRENCE",
|
||||
Boolean.TRUE);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.FROM_ACCOUNT_NOT_FOUND, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_createRecurringTransaction_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.createRecurringTransaction("account.from",
|
||||
"account.to",
|
||||
Long.valueOf(150l),
|
||||
"DESCRIPTION",
|
||||
"HOLIDAY_WEEKEND_TYPE",
|
||||
"INTERVAL_TYPE",
|
||||
"FIRST_OCCURRENCE",
|
||||
"LAST_OCCURRENCE",
|
||||
Boolean.TRUE);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.INVALID_BOOKING_ACCOUNTS, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_createRecurringTransaction_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.createRecurringTransaction("account.from",
|
||||
"account.to",
|
||||
null,
|
||||
"DESCRIPTION",
|
||||
"HOLIDAY_WEEKEND_TYPE",
|
||||
"INTERVAL_TYPE",
|
||||
"FIRST_OCCURRENCE",
|
||||
"LAST_OCCURRENCE",
|
||||
Boolean.TRUE);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.MISSING_AMOUNT, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_createRecurringTransaction_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.createRecurringTransaction("account.from",
|
||||
"account.to",
|
||||
Long.valueOf(0l),
|
||||
"DESCRIPTION",
|
||||
"HOLIDAY_WEEKEND_TYPE",
|
||||
"INTERVAL_TYPE",
|
||||
"FIRST_OCCURRENCE",
|
||||
"LAST_OCCURRENCE",
|
||||
Boolean.TRUE);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.AMOUNT_ZERO, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_createRecurringTransaction_MISSING_HOLIDAY_WEEKEND_TYPE() {
|
||||
// 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.createRecurringTransaction("account.from",
|
||||
"account.to",
|
||||
Long.valueOf(150l),
|
||||
"DESCRIPTION",
|
||||
null,
|
||||
"INTERVAL_TYPE",
|
||||
"FIRST_OCCURRENCE",
|
||||
"LAST_OCCURRENCE",
|
||||
Boolean.TRUE);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.MISSING_HOLIDAY_WEEKEND_TYPE, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_createRecurringTransaction_INVALID_HOLIDAY_WEEKEND_TYPE() {
|
||||
// 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.createRecurringTransaction("account.from",
|
||||
"account.to",
|
||||
Long.valueOf(150l),
|
||||
"DESCRIPTION",
|
||||
"HOLIDAY_WEEKEND_TYPE",
|
||||
"INTERVAL_TYPE",
|
||||
"FIRST_OCCURRENCE",
|
||||
"LAST_OCCURRENCE",
|
||||
Boolean.TRUE);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.INVALID_HOLIDAY_WEEKEND_TYPE, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_createRecurringTransaction_MISSING_INTERVAL_TYPE() {
|
||||
// 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.createRecurringTransaction("account.from",
|
||||
"account.to",
|
||||
Long.valueOf(150l),
|
||||
"DESCRIPTION",
|
||||
HolidayWeekendType.SAME_DAY.name(),
|
||||
null,
|
||||
"FIRST_OCCURRENCE",
|
||||
"LAST_OCCURRENCE",
|
||||
Boolean.TRUE);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.MISSING_INTERVAL_TYPE, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_createRecurringTransaction_INVALID_INTERVAL_TYPE() {
|
||||
// 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.createRecurringTransaction("account.from",
|
||||
"account.to",
|
||||
Long.valueOf(150l),
|
||||
"DESCRIPTION",
|
||||
HolidayWeekendType.SAME_DAY.name(),
|
||||
"INTERVAL_TYPE",
|
||||
"FIRST_OCCURRENCE",
|
||||
"LAST_OCCURRENCE",
|
||||
Boolean.TRUE);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.INVALID_INTERVAL_TYPE, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_createRecurringTransaction_MISSING_FIRST_OCCURRENCE() {
|
||||
// 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.createRecurringTransaction("account.from",
|
||||
"account.to",
|
||||
Long.valueOf(150l),
|
||||
"DESCRIPTION",
|
||||
HolidayWeekendType.SAME_DAY.name(),
|
||||
IntervalType.DAILY.name(),
|
||||
null,
|
||||
"LAST_OCCURRENCE",
|
||||
Boolean.TRUE);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.MISSING_FIRST_OCCURRENCE, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_createRecurringTransaction_INVALID_FIRST_OCCURRENCE_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.createRecurringTransaction("account.from",
|
||||
"account.to",
|
||||
Long.valueOf(150l),
|
||||
"DESCRIPTION",
|
||||
HolidayWeekendType.SAME_DAY.name(),
|
||||
IntervalType.DAILY.name(),
|
||||
"FIRST_OCCURRENCE",
|
||||
"LAST_OCCURRENCE",
|
||||
Boolean.TRUE);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.INVALID_FIRST_OCCURRENCE_FORMAT, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_createRecurringTransaction_INVALID_LAST_OCCURRENCE_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.createRecurringTransaction("account.from",
|
||||
"account.to",
|
||||
Long.valueOf(150l),
|
||||
"DESCRIPTION",
|
||||
HolidayWeekendType.SAME_DAY.name(),
|
||||
IntervalType.DAILY.name(),
|
||||
"07.03.2019",
|
||||
"LAST_OCCURRENCE",
|
||||
Boolean.TRUE);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.INVALID_LAST_OCCURRENCE_FORMAT, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_createRecurringTransaction_UNKNOWN_ERROR() {
|
||||
// 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);
|
||||
Mockito.when(this.recurringTransactionRepository.save(Mockito.any())).thenThrow(new NullPointerException());
|
||||
|
||||
// Act
|
||||
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
|
||||
"account.to",
|
||||
Long.valueOf(150l),
|
||||
"DESCRIPTION",
|
||||
HolidayWeekendType.SAME_DAY.name(),
|
||||
IntervalType.DAILY.name(),
|
||||
"07.03.2019",
|
||||
null,
|
||||
Boolean.TRUE);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.UNKNOWN_ERROR, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_createRecurringTransaction_OK() {
|
||||
// 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.createRecurringTransaction("account.from",
|
||||
"account.to",
|
||||
Long.valueOf(150l),
|
||||
"DESCRIPTION",
|
||||
HolidayWeekendType.SAME_DAY.name(),
|
||||
IntervalType.DAILY.name(),
|
||||
"07.03.2019",
|
||||
null,
|
||||
Boolean.TRUE);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.OK, response);
|
||||
}
|
||||
|
||||
private Account createAccount() {
|
||||
final Account account = new Account();
|
||||
|
||||
account.setCurrentBalance(Long.valueOf(0l));
|
||||
|
||||
return account;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package de.financer.service;
|
||||
|
||||
import de.financer.ResponseReason;
|
||||
import de.financer.config.FinancerConfig;
|
||||
import de.financer.dba.RecurringTransactionRepository;
|
||||
import de.financer.model.RecurringTransaction;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class RecurringTransactionService_deleteRecurringTransactionTest {
|
||||
@InjectMocks
|
||||
private RecurringTransactionService classUnderTest;
|
||||
|
||||
@Mock
|
||||
private AccountService accountService;
|
||||
|
||||
@Mock
|
||||
private RuleService ruleService;
|
||||
|
||||
@Mock
|
||||
private RecurringTransactionRepository recurringTransactionRepository;
|
||||
|
||||
@Mock
|
||||
private FinancerConfig financerConfig;
|
||||
|
||||
@Test
|
||||
public void test_deleteRecurringTransaction_MISSING_RECURRING_TRANSACTION_ID() {
|
||||
// Arrange
|
||||
// Nothing to do
|
||||
|
||||
// Act
|
||||
final ResponseReason response = this.classUnderTest.deleteRecurringTransaction(null);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.MISSING_RECURRING_TRANSACTION_ID, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_deleteRecurringTransaction_INVALID_RECURRING_TRANSACTION_ID() {
|
||||
// Arrange
|
||||
// Nothing to do
|
||||
|
||||
// Act
|
||||
final ResponseReason response = this.classUnderTest.deleteRecurringTransaction("invalid");
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.INVALID_RECURRING_TRANSACTION_ID, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_deleteRecurringTransaction_RECURRING_TRANSACTION_NOT_FOUND() {
|
||||
// Arrange
|
||||
Mockito.when(this.recurringTransactionRepository.findById(Mockito.anyLong())).thenReturn(Optional.empty());
|
||||
|
||||
// Act
|
||||
final ResponseReason response = this.classUnderTest.deleteRecurringTransaction("123");
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.RECURRING_TRANSACTION_NOT_FOUND, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_deleteRecurringTransaction_UNKNOWN_ERROR() {
|
||||
// Arrange
|
||||
Mockito.when(this.recurringTransactionRepository.findById(Mockito.anyLong())).thenReturn(Optional.of(new RecurringTransaction()));
|
||||
Mockito.doThrow(new NullPointerException()).when(this.recurringTransactionRepository).save(Mockito.any());
|
||||
|
||||
// Act
|
||||
final ResponseReason response = this.classUnderTest.deleteRecurringTransaction("123");
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.UNKNOWN_ERROR, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_deleteRecurringTransaction_OK() {
|
||||
// Arrange
|
||||
Mockito.when(this.recurringTransactionRepository.findById(Mockito.anyLong())).thenReturn(Optional.of(new RecurringTransaction()));
|
||||
|
||||
// Act
|
||||
final ResponseReason response = this.classUnderTest.deleteRecurringTransaction("123");
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.OK, response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
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
|
||||
.findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any()))
|
||||
.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
|
||||
.findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any()))
|
||||
.thenReturn(Collections.singletonList(createRecurringTransaction(0)));
|
||||
// Today is a holiday, but yesterday was not
|
||||
Mockito.when(this.ruleService.isHoliday(Mockito.any())).thenReturn(Boolean.TRUE, Boolean.FALSE);
|
||||
final LocalDate now = LocalDate.now();
|
||||
|
||||
// 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
|
||||
.findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any()))
|
||||
.thenReturn(Collections.singletonList(createRecurringTransaction(0)));
|
||||
// Today is a weekend day, but yesterday was not
|
||||
Mockito.when(this.ruleService.isWeekend(Mockito.any())).thenReturn(Boolean.TRUE, Boolean.FALSE);
|
||||
final LocalDate now = LocalDate.now();
|
||||
|
||||
// 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
|
||||
.findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any()))
|
||||
.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);
|
||||
recurringTransaction.setDeleted(false);
|
||||
|
||||
return recurringTransaction;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
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.Ignore;
|
||||
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
|
||||
.findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any()))
|
||||
.thenReturn(Collections.singletonList(createRecurringTransaction(-1)));
|
||||
// Today is not a holiday but yesterday was
|
||||
Mockito.when(this.ruleService.isHoliday(Mockito.any())).thenReturn(Boolean.FALSE, Boolean.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
|
||||
.findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any()))
|
||||
.thenReturn(Collections.singletonList(createRecurringTransaction(-(now.getDayOfWeek().getValue() + 2))));
|
||||
// First False for the dueToday check, 2x True for actual weekend, second False for Friday
|
||||
Mockito.when(this.ruleService.isWeekend(Mockito.any()))
|
||||
.thenReturn(Boolean.FALSE, 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.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
|
||||
@Ignore
|
||||
// This test does not work as expected: if go back to the last sunday and then again one month back, we do
|
||||
// not necessarily end up on on a date that causes the transaction to be due on monday
|
||||
// e.g. 01.04.19 -> monday, 31.03.19 -> sunday, minus one month -> 28.02.19
|
||||
// whereas the resulting 28.02.19 would be the first occurrence of the transaction. The next due dates would
|
||||
// be 28.03.19 and 28.04.19 and not the 01.04.19 as expected
|
||||
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
|
||||
.findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any()))
|
||||
.thenReturn(Collections.singletonList(createRecurringTransaction(-(now.getDayOfWeek().getValue()))));
|
||||
// First False for the dueToday check, 2x True for actual weekend, second False for Friday
|
||||
Mockito.when(this.ruleService.isWeekend(Mockito.any()))
|
||||
.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
|
||||
@Ignore
|
||||
// Same as with the _sunday test -> does not work as expected
|
||||
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
|
||||
.findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any()))
|
||||
.thenReturn(Collections.singletonList(createRecurringTransaction(-(now.getDayOfWeek().getValue() + 1))));
|
||||
// First False for the dueToday check, 2x True for actual weekend, second False for Friday
|
||||
Mockito.when(this.ruleService.isWeekend(Mockito.any()))
|
||||
.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 yesterday (a saturday) is <b>not</b> due
|
||||
* today (a sunday).
|
||||
*
|
||||
* relates to: test_getAllDueToday_duePast_weekend_sunday
|
||||
*/
|
||||
@Test
|
||||
public void test_getAllDueToday_duePast_weekend_not_due_on_sunday() {
|
||||
// Arrange
|
||||
final LocalDate now = LocalDate.of(2019, 5, 19); // A sunday
|
||||
Mockito.when(this.recurringTransactionRepository
|
||||
.findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any()))
|
||||
.thenReturn(Collections.singletonList(createRecurringTransaction(LocalDate.of(2019, 5, 18))));
|
||||
// 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(now);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(0, IterableUtils.size(recurringDueToday));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_() {
|
||||
// Arrange
|
||||
final LocalDate now = LocalDate.of(2019, 6, 17); // A monday
|
||||
final RecurringTransaction recurringTransaction = new RecurringTransaction();
|
||||
|
||||
recurringTransaction.setLastOccurrence(LocalDate.of(2019, 6, 15)); // a saturday
|
||||
recurringTransaction.setFirstOccurrence(LocalDate.of(2019, 5, 15)); // a wednesday
|
||||
recurringTransaction.setHolidayWeekendType(HolidayWeekendType.NEXT_WORKDAY);
|
||||
recurringTransaction.setIntervalType(IntervalType.MONTHLY);
|
||||
|
||||
Mockito.when(this.recurringTransactionRepository
|
||||
.findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any()))
|
||||
.thenReturn(Collections.singletonList(recurringTransaction));
|
||||
Mockito.when(this.ruleService.isWeekend(Mockito.any()))
|
||||
.thenReturn(Boolean.FALSE, Boolean.FALSE, Boolean.TRUE, Boolean.TRUE, Boolean.FALSE);
|
||||
Mockito.when(this.ruleService.isHoliday(Mockito.any()))
|
||||
.thenReturn(Boolean.FALSE);
|
||||
|
||||
// Act
|
||||
final Iterable<RecurringTransaction> recurringDueToday = this.classUnderTest.getAllDueToday(now);
|
||||
|
||||
// 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);
|
||||
recurringTransaction.setDeleted(false);
|
||||
|
||||
return recurringTransaction;
|
||||
}
|
||||
|
||||
private RecurringTransaction createRecurringTransaction(LocalDate firstOccurrence) {
|
||||
final RecurringTransaction recurringTransaction = new RecurringTransaction();
|
||||
|
||||
recurringTransaction.setFirstOccurrence(firstOccurrence);
|
||||
recurringTransaction.setHolidayWeekendType(HolidayWeekendType.NEXT_WORKDAY);
|
||||
recurringTransaction.setIntervalType(IntervalType.MONTHLY);
|
||||
recurringTransaction.setDeleted(false);
|
||||
|
||||
return recurringTransaction;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
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_PREVIOUS_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 plus one day (and thus will
|
||||
* actually be due tomorrow), intervalType = monthly and holidayWeekendType = previous_workday is due today, if
|
||||
* tomorrow will be a holiday but today is not
|
||||
*/
|
||||
@Test
|
||||
public void test_getAllDueToday_dueFuture_holiday() {
|
||||
// Arrange
|
||||
Mockito.when(this.recurringTransactionRepository
|
||||
.findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any()))
|
||||
.thenReturn(Collections.singletonList(createRecurringTransaction(1)));
|
||||
// Today is not a holiday but tomorrow is
|
||||
Mockito.when(this.ruleService.isHoliday(Mockito.any())).thenReturn(Boolean.FALSE, Boolean.TRUE);
|
||||
final LocalDate now = LocalDate.now();
|
||||
|
||||
// Act
|
||||
final Iterable<RecurringTransaction> recurringDueToday = this.classUnderTest.getAllDueToday(now);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(1, IterableUtils.size(recurringDueToday));
|
||||
}
|
||||
|
||||
/**
|
||||
* Negative test case for the following: recurringTransaction firstOccurrence = saturday the 15th,
|
||||
* intervalType = monthly and holidayWeekendType = previous_workday => should not be due today if today is the 15th,
|
||||
* as it was actually due yesterday.
|
||||
*/
|
||||
@Test
|
||||
public void test_getAllDueToday_PreviousWorkday_weekend_notDue() {
|
||||
// Arrange
|
||||
final RecurringTransaction recurringTransaction = new RecurringTransaction();
|
||||
|
||||
recurringTransaction.setFirstOccurrence(LocalDate.of(2019, 6, 15));
|
||||
recurringTransaction.setHolidayWeekendType(HolidayWeekendType.PREVIOUS_WORKDAY);
|
||||
recurringTransaction.setIntervalType(IntervalType.MONTHLY);
|
||||
|
||||
Mockito.when(this.recurringTransactionRepository
|
||||
.findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any()))
|
||||
.thenReturn(Collections.singletonList(recurringTransaction));
|
||||
Mockito.when(this.ruleService.isHoliday(Mockito.any())).thenReturn(Boolean.FALSE);
|
||||
Mockito.when(this.ruleService.isWeekend(Mockito.any())).thenReturn(Boolean.TRUE, Boolean.FALSE);
|
||||
final LocalDate now = LocalDate.of(2019, 6, 15);
|
||||
|
||||
// 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).minusMonths(1));
|
||||
|
||||
recurringTransaction.setHolidayWeekendType(HolidayWeekendType.PREVIOUS_WORKDAY);
|
||||
recurringTransaction.setIntervalType(IntervalType.MONTHLY);
|
||||
|
||||
return recurringTransaction;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
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_SAME_DAYTest {
|
||||
@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 = same_day is not due today, if yesterday
|
||||
* was a holiday but today is not
|
||||
*/
|
||||
@Test
|
||||
public void test_getAllDueToday_duePast_holiday() {
|
||||
// Arrange
|
||||
Mockito.when(this.recurringTransactionRepository
|
||||
.findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any()))
|
||||
.thenReturn(Collections.singletonList(createRecurringTransaction(-1)));
|
||||
// Today is not a holiday but yesterday was
|
||||
Mockito.when(this.ruleService.isHoliday(Mockito.any())).thenReturn(Boolean.FALSE, Boolean.TRUE);
|
||||
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).minusMonths(1));
|
||||
|
||||
recurringTransaction.setHolidayWeekendType(HolidayWeekendType.SAME_DAY);
|
||||
recurringTransaction.setIntervalType(IntervalType.MONTHLY);
|
||||
recurringTransaction.setDeleted(false);
|
||||
|
||||
return recurringTransaction;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package de.financer.service;
|
||||
|
||||
import de.financer.model.Account;
|
||||
import de.financer.model.AccountType;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
@RunWith(JUnit4.class)
|
||||
public class RuleService_getMultiplierFromAccountTest {
|
||||
|
||||
private RuleService classUnderTest;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
this.classUnderTest = new RuleService();
|
||||
|
||||
this.classUnderTest.afterPropertiesSet();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_getMultiplierFromAccount_INCOME() {
|
||||
doTest(AccountType.INCOME, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_getMultiplierFromAccount_BANK() {
|
||||
doTest(AccountType.BANK, -1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_getMultiplierFromAccount_CASH() {
|
||||
doTest(AccountType.CASH, -1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_getMultiplierFromAccount_EXPENSE() {
|
||||
doTest(AccountType.EXPENSE, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_getMultiplierFromAccount_LIABILITY() {
|
||||
doTest(AccountType.LIABILITY, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_getMultiplierFromAccount_START() {
|
||||
doTest(AccountType.START, 1);
|
||||
}
|
||||
|
||||
public void doTest(AccountType accountType, long expected) {
|
||||
// Arrange
|
||||
final Account fromAccount = createAccount(accountType);
|
||||
|
||||
// Act
|
||||
final long multiplier = this.classUnderTest.getMultiplierFromAccount(fromAccount);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(expected, multiplier);
|
||||
}
|
||||
|
||||
private Account createAccount(AccountType accountType) {
|
||||
final Account account = new Account();
|
||||
|
||||
account.setType(accountType);
|
||||
|
||||
return account;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package de.financer.service;
|
||||
|
||||
import de.financer.model.Account;
|
||||
import de.financer.model.AccountType;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
@RunWith(JUnit4.class)
|
||||
public class RuleService_getMultiplierToAccountTest {
|
||||
|
||||
private RuleService classUnderTest;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
this.classUnderTest = new RuleService();
|
||||
|
||||
this.classUnderTest.afterPropertiesSet();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_getMultiplierToAccount_INCOME() {
|
||||
doTest(AccountType.INCOME, -1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_getMultiplierToAccount_BANK() {
|
||||
doTest(AccountType.BANK, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_getMultiplierToAccount_CASH() {
|
||||
doTest(AccountType.CASH, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_getMultiplierToAccount_EXPENSE() {
|
||||
doTest(AccountType.EXPENSE, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_getMultiplierToAccount_LIABILITY() {
|
||||
doTest(AccountType.LIABILITY, -1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_getMultiplierToAccount_START() {
|
||||
doTest(AccountType.START, -1);
|
||||
}
|
||||
|
||||
public void doTest(AccountType accountType, long expected) {
|
||||
// Arrange
|
||||
final Account fromAccount = createAccount(accountType);
|
||||
|
||||
// Act
|
||||
final long multiplier = this.classUnderTest.getMultiplierToAccount(fromAccount);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(expected, multiplier);
|
||||
}
|
||||
|
||||
private Account createAccount(AccountType accountType) {
|
||||
final Account account = new Account();
|
||||
|
||||
account.setType(accountType);
|
||||
|
||||
return account;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
package de.financer.service;
|
||||
|
||||
import de.financer.model.Account;
|
||||
import de.financer.model.AccountType;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
@RunWith(JUnit4.class)
|
||||
public class RuleService_isValidBookingTest {
|
||||
private RuleService classUnderTest;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
this.classUnderTest = new RuleService();
|
||||
|
||||
this.classUnderTest.afterPropertiesSet();
|
||||
}
|
||||
|
||||
// from INCOME
|
||||
|
||||
@Test
|
||||
public void test_isValidBooking_INCOME_INCOME() {
|
||||
doTest(AccountType.INCOME, AccountType.INCOME, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_isValidBooking_INCOME_BANK() {
|
||||
doTest(AccountType.INCOME, AccountType.BANK, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_isValidBooking_INCOME_CASH() {
|
||||
doTest(AccountType.INCOME, AccountType.CASH, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_isValidBooking_INCOME_EXPENSE() {
|
||||
doTest(AccountType.INCOME, AccountType.EXPENSE, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_isValidBooking_INCOME_LIABILITY() {
|
||||
doTest(AccountType.INCOME, AccountType.LIABILITY, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_isValidBooking_INCOME_START() {
|
||||
doTest(AccountType.INCOME, AccountType.START, false);
|
||||
}
|
||||
|
||||
// from BANK
|
||||
|
||||
@Test
|
||||
public void test_isValidBooking_BANK_INCOME() {
|
||||
doTest(AccountType.BANK, AccountType.INCOME, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_isValidBooking_BANK_BANK() {
|
||||
doTest(AccountType.BANK, AccountType.BANK, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_isValidBooking_BANK_CASH() {
|
||||
doTest(AccountType.BANK, AccountType.CASH, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_isValidBooking_BANK_EXPENSE() {
|
||||
doTest(AccountType.BANK, AccountType.EXPENSE, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_isValidBooking_BANK_LIABILITY() {
|
||||
doTest(AccountType.BANK, AccountType.LIABILITY, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_isValidBooking_BANK_START() {
|
||||
doTest(AccountType.BANK, AccountType.START, false);
|
||||
}
|
||||
|
||||
// from CASH
|
||||
|
||||
@Test
|
||||
public void test_isValidBooking_CASH_INCOME() {
|
||||
doTest(AccountType.CASH, AccountType.INCOME, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_isValidBooking_CASH_BANK() {
|
||||
doTest(AccountType.CASH, AccountType.BANK, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_isValidBooking_CASH_CASH() {
|
||||
doTest(AccountType.CASH, AccountType.CASH, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_isValidBooking_CASH_EXPENSE() {
|
||||
doTest(AccountType.CASH, AccountType.EXPENSE, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_isValidBooking_CASH_LIABILITY() {
|
||||
doTest(AccountType.CASH, AccountType.LIABILITY, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_isValidBooking_CASH_START() {
|
||||
doTest(AccountType.CASH, AccountType.START, false);
|
||||
}
|
||||
|
||||
// from EXPENSE
|
||||
|
||||
@Test
|
||||
public void test_isValidBooking_EXPENSE_INCOME() {
|
||||
doTest(AccountType.EXPENSE, AccountType.INCOME, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_isValidBooking_EXPENSE_BANK() {
|
||||
doTest(AccountType.EXPENSE, AccountType.BANK, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_isValidBooking_EXPENSE_CASH() {
|
||||
doTest(AccountType.EXPENSE, AccountType.CASH, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_isValidBooking_EXPENSE_EXPENSE() {
|
||||
doTest(AccountType.EXPENSE, AccountType.EXPENSE, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_isValidBooking_EXPENSE_LIABILITY() {
|
||||
doTest(AccountType.EXPENSE, AccountType.LIABILITY, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_isValidBooking_EXPENSE_START() {
|
||||
doTest(AccountType.EXPENSE, AccountType.START, false);
|
||||
}
|
||||
|
||||
// from LIABILITY
|
||||
|
||||
@Test
|
||||
public void test_isValidBooking_LIABILITY_INCOME() {
|
||||
doTest(AccountType.LIABILITY, AccountType.INCOME, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_isValidBooking_LIABILITY_BANK() {
|
||||
doTest(AccountType.LIABILITY, AccountType.BANK, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_isValidBooking_LIABILITY_CASH() {
|
||||
doTest(AccountType.LIABILITY, AccountType.CASH, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_isValidBooking_LIABILITY_EXPENSE() {
|
||||
doTest(AccountType.LIABILITY, AccountType.EXPENSE, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_isValidBooking_LIABILITY_LIABILITY() {
|
||||
doTest(AccountType.LIABILITY, AccountType.LIABILITY, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_isValidBooking_LIABILITY_START() {
|
||||
doTest(AccountType.LIABILITY, AccountType.START, false);
|
||||
}
|
||||
|
||||
// from START
|
||||
|
||||
@Test
|
||||
public void test_isValidBooking_START_INCOME() {
|
||||
doTest(AccountType.START, AccountType.INCOME, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_isValidBooking_START_BANK() {
|
||||
doTest(AccountType.START, AccountType.BANK, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_isValidBooking_START_CASH() {
|
||||
doTest(AccountType.START, AccountType.CASH, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_isValidBooking_START_EXPENSE() {
|
||||
doTest(AccountType.START, AccountType.EXPENSE, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_isValidBooking_START_LIABILITY() {
|
||||
doTest(AccountType.START, AccountType.LIABILITY, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_isValidBooking_START_START() {
|
||||
doTest(AccountType.START, AccountType.START, false);
|
||||
}
|
||||
|
||||
private void doTest(AccountType fromAccountType, AccountType toAccountType, boolean expected) {
|
||||
// Arrange
|
||||
final Account fromAccount = createAccount(fromAccountType);
|
||||
final Account toAccount = createAccount(toAccountType);
|
||||
|
||||
// Act
|
||||
final boolean isValid = this.classUnderTest.isValidBooking(fromAccount, toAccount);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(expected, isValid);
|
||||
}
|
||||
|
||||
private Account createAccount(AccountType accountType) {
|
||||
final Account account = new Account();
|
||||
|
||||
account.setType(accountType);
|
||||
|
||||
return account;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package de.financer.service;
|
||||
|
||||
import de.financer.ResponseReason;
|
||||
import de.financer.config.FinancerConfig;
|
||||
import de.financer.dba.TransactionRepository;
|
||||
import de.financer.model.Account;
|
||||
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;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class TransactionService_createTransactionTest {
|
||||
@InjectMocks
|
||||
private TransactionService classUnderTest;
|
||||
|
||||
@Mock
|
||||
private AccountService accountService;
|
||||
|
||||
@Mock
|
||||
private RuleService ruleService;
|
||||
|
||||
@Mock
|
||||
private TransactionRepository transactionRepository;
|
||||
|
||||
@Mock
|
||||
private FinancerConfig financerConfig;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
Mockito.when(this.financerConfig.getDateFormat()).thenReturn("dd.MM.yyyy");
|
||||
}
|
||||
|
||||
@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.from", "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.to", 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.from", "account.to", 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.from", "account.to", 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.from", "account.to", 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.from", "account.to", 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.from", "account.to", 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.from", "account.to", 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));
|
||||
}
|
||||
|
||||
private Account createAccount() {
|
||||
final Account account = new Account();
|
||||
|
||||
account.setCurrentBalance(Long.valueOf(0l));
|
||||
|
||||
return account;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package de.financer.service;
|
||||
|
||||
import de.financer.ResponseReason;
|
||||
import de.financer.config.FinancerConfig;
|
||||
import de.financer.dba.TransactionRepository;
|
||||
import de.financer.model.Account;
|
||||
import de.financer.model.AccountType;
|
||||
import de.financer.model.Transaction;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.*;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class TransactionService_deleteTransactionTest {
|
||||
@InjectMocks
|
||||
private TransactionService classUnderTest;
|
||||
|
||||
@Mock
|
||||
private AccountService accountService;
|
||||
|
||||
@Mock
|
||||
private RuleService ruleService;
|
||||
|
||||
@Mock
|
||||
private TransactionRepository transactionRepository;
|
||||
|
||||
@Mock
|
||||
private FinancerConfig financerConfig;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
this.ruleService.afterPropertiesSet();
|
||||
|
||||
Mockito.when(this.ruleService.getMultiplierFromAccount(Mockito.any())).thenCallRealMethod();
|
||||
Mockito.when(this.ruleService.getMultiplierToAccount(Mockito.any())).thenCallRealMethod();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_deleteRecurringTransaction_MISSING_TRANSACTION_ID() {
|
||||
// Arrange
|
||||
// Nothing to do
|
||||
|
||||
// Act
|
||||
final ResponseReason response = this.classUnderTest.deleteTransaction(null);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.MISSING_TRANSACTION_ID, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_deleteRecurringTransaction_INVALID_TRANSACTION_ID() {
|
||||
// Arrange
|
||||
// Nothing to do
|
||||
|
||||
// Act
|
||||
final ResponseReason response = this.classUnderTest.deleteTransaction("invalid");
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.INVALID_TRANSACTION_ID, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_deleteRecurringTransaction_TRANSACTION_NOT_FOUND() {
|
||||
// Arrange
|
||||
Mockito.when(this.transactionRepository.findById(Mockito.anyLong())).thenReturn(Optional.empty());
|
||||
|
||||
// Act
|
||||
final ResponseReason response = this.classUnderTest.deleteTransaction("123");
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.TRANSACTION_NOT_FOUND, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_deleteRecurringTransaction_UNKNOWN_ERROR() {
|
||||
// Arrange
|
||||
Mockito.when(this.transactionRepository.findById(Mockito.anyLong()))
|
||||
.thenReturn(Optional.of(createTransaction(AccountType.BANK, AccountType.EXPENSE)));
|
||||
Mockito.doThrow(new NullPointerException()).when(this.transactionRepository).deleteById(Mockito.anyLong());
|
||||
|
||||
// Act
|
||||
final ResponseReason response = this.classUnderTest.deleteTransaction("123");
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.UNKNOWN_ERROR, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_deleteRecurringTransaction_OK() {
|
||||
// Arrange
|
||||
Mockito.when(this.transactionRepository.findById(Mockito.anyLong()))
|
||||
.thenReturn(Optional.of(createTransaction(AccountType.BANK, AccountType.EXPENSE)));
|
||||
|
||||
// Act
|
||||
final ResponseReason response = this.classUnderTest.deleteTransaction("123");
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.OK, response);
|
||||
|
||||
final InOrder inOrder = Mockito.inOrder(this.accountService);
|
||||
|
||||
inOrder.verify(this.accountService).saveAccount(ArgumentMatchers.argThat((Account arg) -> Long.valueOf(50000L).equals(arg.getCurrentBalance())));
|
||||
inOrder.verify(this.accountService).saveAccount(ArgumentMatchers.argThat((Account arg) -> Long.valueOf(5000L).equals(arg.getCurrentBalance())));
|
||||
}
|
||||
|
||||
private Transaction createTransaction(AccountType fromType, AccountType toType) {
|
||||
final Transaction transaction = new Transaction();
|
||||
final Account fromAccount = new Account();
|
||||
final Account toAccount = new Account();
|
||||
|
||||
transaction.setFromAccount(fromAccount);
|
||||
transaction.setToAccount(toAccount);
|
||||
transaction.setAmount(Long.valueOf(10000L));
|
||||
|
||||
fromAccount.setCurrentBalance(Long.valueOf(40000L));
|
||||
toAccount.setCurrentBalance(Long.valueOf(15000L));
|
||||
|
||||
fromAccount.setType(fromType);
|
||||
toAccount.setType(toType);
|
||||
|
||||
return transaction;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package de.financer.task;
|
||||
|
||||
import de.financer.config.FinancerConfig;
|
||||
import de.financer.model.Account;
|
||||
import de.financer.model.RecurringTransaction;
|
||||
import de.financer.service.RecurringTransactionService;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import org.springframework.mail.SimpleMailMessage;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class SendRecurringTransactionReminderTaskTest {
|
||||
@InjectMocks
|
||||
private SendRecurringTransactionReminderTask classUnderTest;
|
||||
|
||||
@Mock
|
||||
private RecurringTransactionService recurringTransactionService;
|
||||
|
||||
@Mock
|
||||
private JavaMailSender mailSender;
|
||||
|
||||
@Mock
|
||||
private FinancerConfig financerConfig;
|
||||
|
||||
@Test
|
||||
public void test_sendReminder() {
|
||||
// Arrange
|
||||
final Collection<RecurringTransaction> recurringTransactions = Arrays.asList(
|
||||
createRecurringTransaction("Test booking 1", "Income", "accounts.bank", Long.valueOf(250000), true),
|
||||
createRecurringTransaction("Test booking 2", "Bank", "accounts.rent", Long.valueOf(41500), true),
|
||||
createRecurringTransaction("Test booking 3", "Bank", "accounts.cash", Long.valueOf(5000), true),
|
||||
createRecurringTransaction("Test booking 4", "Car", "accounts.car", Long.valueOf(1234), false)
|
||||
);
|
||||
|
||||
Mockito.when(this.recurringTransactionService.getAllDueToday()).thenReturn(recurringTransactions);
|
||||
Mockito.when(this.financerConfig.getMailRecipients()).thenReturn(Collections.singletonList("test@test.com"));
|
||||
|
||||
// Act
|
||||
this.classUnderTest.sendReminder();
|
||||
|
||||
// Assert
|
||||
Mockito.verify(this.mailSender, Mockito.times(1)).send(Mockito.any(SimpleMailMessage.class));
|
||||
}
|
||||
|
||||
private RecurringTransaction createRecurringTransaction(String description, String fromAccountKey, String toAccountKey, Long amount, boolean remind) {
|
||||
final RecurringTransaction recurringTransaction = new RecurringTransaction();
|
||||
|
||||
recurringTransaction.setDescription(description);
|
||||
recurringTransaction.setFromAccount(createAccount(fromAccountKey));
|
||||
recurringTransaction.setToAccount(createAccount(toAccountKey));
|
||||
recurringTransaction.setAmount(amount);
|
||||
recurringTransaction.setRemind(remind);
|
||||
|
||||
return recurringTransaction;
|
||||
}
|
||||
|
||||
private Account createAccount(String key) {
|
||||
final Account account = new Account();
|
||||
|
||||
account.setKey(key);
|
||||
|
||||
return account;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
spring.profiles.active=hsqldb,dev
|
||||
|
||||
spring.datasource.url=jdbc:hsqldb:mem:.
|
||||
spring.datasource.username=sa
|
||||
spring.flyway.locations=classpath:/database/hsqldb,classpath:/database/hsqldb/integration,classpath:/database/common
|
||||
@@ -0,0 +1,13 @@
|
||||
-- Accounts
|
||||
INSERT INTO account ("key", type, status, current_balance)
|
||||
VALUES ('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 ((SELECT ID FROM account WHERE "key" = 'Income'), (SELECT ID FROM account WHERE "key" = 'Check account'), 'Pay', 250000, 'MONTHLY', '2019-01-15', 'NEXT_WORKDAY');
|
||||
|
||||
INSERT INTO recurring_transaction (from_account_id, to_account_id, description, amount, interval_type, first_occurrence, holiday_weekend_type)
|
||||
VALUES ((SELECT ID FROM account WHERE "key" = 'Cash'), (SELECT ID FROM account WHERE "key" = 'Convenience'), 'Pretzel', 170, 'DAILY', '2019-02-20', 'SAME_DAY');
|
||||
|
||||
INSERT INTO recurring_transaction (from_account_id, to_account_id, description, amount, interval_type, first_occurrence, last_occurrence, holiday_weekend_type)
|
||||
VALUES ((SELECT ID FROM account WHERE "key" = 'Cash'), (SELECT ID FROM account WHERE "key" = 'Food (external)'), 'McDonalds Happy Meal', 399, 'WEEKLY', '2019-02-20', '2019-03-20', 'SAME_DAY');
|
||||
Reference in New Issue
Block a user