From 86ccb0b52cdebd8fa94e40894f3b07a3da1a768b Mon Sep 17 00:00:00 2001 From: MK130591 Date: Fri, 15 Feb 2019 21:30:10 +0100 Subject: [PATCH 01/65] Initial commit for financer --- doc/README | 13 ++ pom.xml | 92 +++++++++++ .../java/de/financer/FinancerApplication.java | 11 ++ src/main/java/de/financer/ResponseReason.java | 29 ++++ .../controller/AccountController.java | 41 +++++ .../controller/TransactionController.java | 30 ++++ .../de/financer/dba/AccountRepository.java | 11 ++ .../dba/RecurringTransactionRepository.java | 7 + .../financer/dba/TransactionRepository.java | 12 ++ src/main/java/de/financer/model/Account.java | 53 +++++++ .../java/de/financer/model/AccountStatus.java | 8 + .../java/de/financer/model/AccountType.java | 33 ++++ .../de/financer/model/HolidayWeekendType.java | 28 ++++ .../java/de/financer/model/IntervalType.java | 30 ++++ .../financer/model/RecurringTransaction.java | 93 +++++++++++ .../java/de/financer/model/Transaction.java | 65 ++++++++ .../de/financer/service/AccountService.java | 104 ++++++++++++ .../java/de/financer/service/RuleService.java | 114 ++++++++++++++ .../financer/service/TransactionService.java | 149 ++++++++++++++++++ .../config/application-dev.properties | 2 + .../config/application-hsqldb.properties | 6 + .../config/application-postgres.properties | 1 + .../resources/config/application.properties | 15 ++ .../database/hsqldb/V1_0_0__init.sql | 55 +++++++ 24 files changed, 1002 insertions(+) create mode 100644 doc/README create mode 100644 pom.xml create mode 100644 src/main/java/de/financer/FinancerApplication.java create mode 100644 src/main/java/de/financer/ResponseReason.java create mode 100644 src/main/java/de/financer/controller/AccountController.java create mode 100644 src/main/java/de/financer/controller/TransactionController.java create mode 100644 src/main/java/de/financer/dba/AccountRepository.java create mode 100644 src/main/java/de/financer/dba/RecurringTransactionRepository.java create mode 100644 src/main/java/de/financer/dba/TransactionRepository.java create mode 100644 src/main/java/de/financer/model/Account.java create mode 100644 src/main/java/de/financer/model/AccountStatus.java create mode 100644 src/main/java/de/financer/model/AccountType.java create mode 100644 src/main/java/de/financer/model/HolidayWeekendType.java create mode 100644 src/main/java/de/financer/model/IntervalType.java create mode 100644 src/main/java/de/financer/model/RecurringTransaction.java create mode 100644 src/main/java/de/financer/model/Transaction.java create mode 100644 src/main/java/de/financer/service/AccountService.java create mode 100644 src/main/java/de/financer/service/RuleService.java create mode 100644 src/main/java/de/financer/service/TransactionService.java create mode 100644 src/main/resources/config/application-dev.properties create mode 100644 src/main/resources/config/application-hsqldb.properties create mode 100644 src/main/resources/config/application-postgres.properties create mode 100644 src/main/resources/config/application.properties create mode 100644 src/main/resources/database/hsqldb/V1_0_0__init.sql diff --git a/doc/README b/doc/README new file mode 100644 index 0000000..c04e884 --- /dev/null +++ b/doc/README @@ -0,0 +1,13 @@ + ___ _ + / __(_)_ __ __ _ _ __ ___ ___ _ __ + / _\ | | '_ \ / _` | '_ \ / __/ _ \ '__| + / / | | | | | (_| | | | | (_| __/ | + \/ |_|_| |_|\__,_|_| |_|\___\___|_| + + + 1. About + 2. Content + 3. Overview + 4. Architectural overview + 5. Account types + 6. Booking rules \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..5ff5359 --- /dev/null +++ b/pom.xml @@ -0,0 +1,92 @@ + + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.1.2.RELEASE + + + + de.77zzcx7 + financer + 1.0-SNAPSHOT + jar + + financer + + + UTF-8 + 1.8 + 1.8 + 1.8 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-mail + + + org.springframework.boot + spring-boot-starter-web + + + org.flywaydb + flyway-core + + + + org.apache.commons + commons-lang3 + 3.8.1 + + + + + org.hsqldb + hsqldb + runtime + + + org.postgresql + postgresql + runtime + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.restdocs + spring-restdocs-mockmvc + test + + + junit + junit + 4.11 + test + + + + + financer + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/src/main/java/de/financer/FinancerApplication.java b/src/main/java/de/financer/FinancerApplication.java new file mode 100644 index 0000000..e3cace7 --- /dev/null +++ b/src/main/java/de/financer/FinancerApplication.java @@ -0,0 +1,11 @@ +package de.financer; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class FinancerApplication { + public static void main(String[] args) { + SpringApplication.run(FinancerApplication.class); + } +} diff --git a/src/main/java/de/financer/ResponseReason.java b/src/main/java/de/financer/ResponseReason.java new file mode 100644 index 0000000..fd57146 --- /dev/null +++ b/src/main/java/de/financer/ResponseReason.java @@ -0,0 +1,29 @@ +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), + INVALID_ACCOUNT_KEY(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); + + private HttpStatus httpStatus; + + ResponseReason(HttpStatus httpStatus) { + this.httpStatus = httpStatus; + } + + public ResponseEntity toResponseEntity() { + return new ResponseEntity(this.name(), this.httpStatus); + } +} diff --git a/src/main/java/de/financer/controller/AccountController.java b/src/main/java/de/financer/controller/AccountController.java new file mode 100644 index 0000000..144efaa --- /dev/null +++ b/src/main/java/de/financer/controller/AccountController.java @@ -0,0 +1,41 @@ +package de.financer.controller; + +import de.financer.model.Account; +import de.financer.service.AccountService; +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 { + + @Autowired + private AccountService accountService; + + @RequestMapping("getByKey") + public Account getAccountByKey(String key) { + return this.accountService.getAccountByKey(key); + } + + @RequestMapping("getAll") + public Iterable getAll() { + return this.accountService.getAll(); + } + + @RequestMapping("getAccountTypes") + public Iterable getAccountTypes() { + return this.accountService.getAccountTypes(); + } + + @RequestMapping("getAccountStatus") + public Iterable getAccountStatus() { + return this.accountService.getAccountStatus(); + } + + @RequestMapping("createAccount") + public ResponseEntity createAccount(String key, String type) { + return this.accountService.createAccount(key, type).toResponseEntity(); + } +} diff --git a/src/main/java/de/financer/controller/TransactionController.java b/src/main/java/de/financer/controller/TransactionController.java new file mode 100644 index 0000000..aba4b6a --- /dev/null +++ b/src/main/java/de/financer/controller/TransactionController.java @@ -0,0 +1,30 @@ +package de.financer.controller; + +import de.financer.model.Transaction; +import de.financer.service.TransactionService; +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("transactions") +public class TransactionController { + @Autowired + private TransactionService transactionService; + + @RequestMapping("getAll") + public Iterable getAll() { + return this.transactionService.getAll(); + } + + @RequestMapping("getAllForAccount") + public Iterable getAllForAccount(String accountKey) { + return this.transactionService.getAllForAccount(accountKey); + } + + @RequestMapping("createTransaction") + public ResponseEntity createTransaction(String fromAccountKey, String toAccountKey, Long amount, String date, String description) { + return this.transactionService.createTransaction(fromAccountKey, toAccountKey, amount, date, description).toResponseEntity(); + } +} diff --git a/src/main/java/de/financer/dba/AccountRepository.java b/src/main/java/de/financer/dba/AccountRepository.java new file mode 100644 index 0000000..13cdf21 --- /dev/null +++ b/src/main/java/de/financer/dba/AccountRepository.java @@ -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 findByKey(String key); +} diff --git a/src/main/java/de/financer/dba/RecurringTransactionRepository.java b/src/main/java/de/financer/dba/RecurringTransactionRepository.java new file mode 100644 index 0000000..430cd06 --- /dev/null +++ b/src/main/java/de/financer/dba/RecurringTransactionRepository.java @@ -0,0 +1,7 @@ +package de.financer.dba; + +import de.financer.model.RecurringTransaction; +import org.springframework.data.repository.CrudRepository; + +public interface RecurringTransactionRepository extends CrudRepository { +} diff --git a/src/main/java/de/financer/dba/TransactionRepository.java b/src/main/java/de/financer/dba/TransactionRepository.java new file mode 100644 index 0000000..f62e74c --- /dev/null +++ b/src/main/java/de/financer/dba/TransactionRepository.java @@ -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 { + Iterable findTransactionsByFromAccountOrToAccount(Account fromAccount, Account toAccount); +} diff --git a/src/main/java/de/financer/model/Account.java b/src/main/java/de/financer/model/Account.java new file mode 100644 index 0000000..b9c8eea --- /dev/null +++ b/src/main/java/de/financer/model/Account.java @@ -0,0 +1,53 @@ +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; + + 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; + } +} diff --git a/src/main/java/de/financer/model/AccountStatus.java b/src/main/java/de/financer/model/AccountStatus.java new file mode 100644 index 0000000..60ced16 --- /dev/null +++ b/src/main/java/de/financer/model/AccountStatus.java @@ -0,0 +1,8 @@ +package de.financer.model; + +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; +} diff --git a/src/main/java/de/financer/model/AccountType.java b/src/main/java/de/financer/model/AccountType.java new file mode 100644 index 0000000..b4e20d2 --- /dev/null +++ b/src/main/java/de/financer/model/AccountType.java @@ -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.asList(AccountType.values()).stream().anyMatch((accountType) -> accountType.name().equals(type)); + } +} diff --git a/src/main/java/de/financer/model/HolidayWeekendType.java b/src/main/java/de/financer/model/HolidayWeekendType.java new file mode 100644 index 0000000..cd2cf20 --- /dev/null +++ b/src/main/java/de/financer/model/HolidayWeekendType.java @@ -0,0 +1,28 @@ +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, + + /** Indicates that the action should be deferred to the next workday */ + NEXT_WORKDAY, + + /** Indicates that the action should be dated back to the previous day */ + 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.asList(HolidayWeekendType.values()).stream().anyMatch((holidayWeekendType) -> holidayWeekendType.name().equals(type)); + } +} diff --git a/src/main/java/de/financer/model/IntervalType.java b/src/main/java/de/financer/model/IntervalType.java new file mode 100644 index 0000000..2d64948 --- /dev/null +++ b/src/main/java/de/financer/model/IntervalType.java @@ -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.asList(IntervalType.values()).stream().anyMatch((intervalType) -> intervalType.name().equals(type)); + } +} diff --git a/src/main/java/de/financer/model/RecurringTransaction.java b/src/main/java/de/financer/model/RecurringTransaction.java new file mode 100644 index 0000000..97954cc --- /dev/null +++ b/src/main/java/de/financer/model/RecurringTransaction.java @@ -0,0 +1,93 @@ +package de.financer.model; + +import javax.persistence.*; +import java.util.Date; + +@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; + @Temporal(TemporalType.DATE) + private Date firstOccurrence; + @Temporal(TemporalType.DATE) + private Date lastOccurrence; + @Enumerated(EnumType.STRING) + private HolidayWeekendType holidayWeekendType; + + 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 Date getLastOccurrence() { + return lastOccurrence; + } + + public void setLastOccurrence(Date lastOccurrence) { + this.lastOccurrence = lastOccurrence; + } + + public Date getFirstOccurrence() { + return firstOccurrence; + } + + public void setFirstOccurrence(Date firstOccurrence) { + this.firstOccurrence = firstOccurrence; + } + + public IntervalType getIntervalType() { + return intervalType; + } + + public void setIntervalType(IntervalType intervalType) { + this.intervalType = intervalType; + } +} diff --git a/src/main/java/de/financer/model/Transaction.java b/src/main/java/de/financer/model/Transaction.java new file mode 100644 index 0000000..06eff5d --- /dev/null +++ b/src/main/java/de/financer/model/Transaction.java @@ -0,0 +1,65 @@ +package de.financer.model; + +import javax.persistence.*; +import java.util.Date; + +@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; + @Temporal(TemporalType.DATE) + @Column(name = "\"date\"") + private Date date; + private String description; + private Long amount; + + 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 Date getDate() { + return date; + } + + public void setDate(Date 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; + } +} diff --git a/src/main/java/de/financer/service/AccountService.java b/src/main/java/de/financer/service/AccountService.java new file mode 100644 index 0000000..694988e --- /dev/null +++ b/src/main/java/de/financer/service/AccountService.java @@ -0,0 +1,104 @@ +package de.financer.service; + +import de.financer.ResponseReason; +import de.financer.dba.AccountRepository; +import de.financer.model.Account; +import de.financer.model.AccountStatus; +import de.financer.model.AccountType; +import org.apache.commons.lang3.StringUtils; +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.util.Arrays; +import java.util.stream.Collectors; + +@Service +public class AccountService { + @Autowired + private AccountRepository accountRepository; + + /** + * This method returns the account identified by the given key. + * + * @param key the key to get the account for + * @return the account or null 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 getAll() { + return this.accountRepository.findAll(); + } + + /** + * @return all possible account types as specified by the {@link AccountType} enumeration, never null + */ + public Iterable getAccountTypes() { + return Arrays.asList(AccountType.values()).stream().map(AccountType::name).collect(Collectors.toList()); + } + + /** + * @return all possible account status as specified by the {@link AccountStatus} enumeration, never null + */ + public Iterable getAccountStatus() { + return Arrays.asList(AccountStatus.values()).stream().map(AccountStatus::name).collect(Collectors.toList()); + } + + /** + * This method saves the given account. It either updates the account if it already exists or inserts + * it if it's new. + * + * @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 0. + * + * @param key the key of the new account. Must begin with account. + * @param type the type of the new account. Must be one of {@link AccountType}. + * @return {@link ResponseReason#INVALID_ACCOUNT_TYPE} if the given type is not a valid {@link AccountType}, + * {@link ResponseReason#INVALID_ACCOUNT_KEY} if the given key does not conform to the format specification, + * {@link ResponseReason#UNKNOWN_ERROR} if an unexpected error occurs and + * {@link ResponseReason#OK} if the operation completed successfully. Never returns null. + */ + @Transactional(propagation = Propagation.REQUIRED) + public ResponseReason createAccount(String key, String type) { + if (!AccountType.isValidType(type)) { + return ResponseReason.INVALID_ACCOUNT_TYPE; + } + + if (!StringUtils.startsWith(key, "accounts.")) { + return ResponseReason.INVALID_ACCOUNT_KEY; + } + + final Account account = new Account(); + + 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 (Exception e) { + // TODO log and check for unique constraint exception so we can return a more specific error + return ResponseReason.UNKNOWN_ERROR; + } + + return ResponseReason.OK; + } +} diff --git a/src/main/java/de/financer/service/RuleService.java b/src/main/java/de/financer/service/RuleService.java new file mode 100644 index 0000000..adb0705 --- /dev/null +++ b/src/main/java/de/financer/service/RuleService.java @@ -0,0 +1,114 @@ +package de.financer.service; + +import de.financer.model.Account; +import de.financer.model.AccountType; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.stereotype.Service; + +import java.util.*; + +import static de.financer.model.AccountType.*; + +/** + * This service encapsulates methods that form business logic rules. + * While most of the logic could be placed elsewhere this service provides + * centralized access to these rules. + */ +@Service +public class RuleService implements InitializingBean { + + private Map> bookingRules; + + @Override + public void afterPropertiesSet() { + initBookingRules(); + } + + 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(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. + * + * 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 1 or -1 + */ + 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 + + if (INCOME.equals(fromAccount)) { + return 1L; + } + else if (BANK.equals(fromAccount)) { + return -1L; + } + else if (CASH.equals(fromAccount)) { + return -1L; + } + else if (LIABILITY.equals(fromAccount)) { + return 1L; + } + else if (START.equals(fromAccount)) { + return 1L; + } + + return 1L; + } + + /** + * This method returns the multiplier for the given to account. + * + * 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 1 or -1 + */ + public long getMultiplierToAccount(Account toAccount) { + // There are no multipliers for INCOME and START accounts + // because they are not valid to account types + + if (BANK.equals(toAccount)) { + return 1L; + } + else if (CASH.equals(toAccount)) { + return 1L; + } + else if (LIABILITY.equals(toAccount)) { + return -1L; + } + else if (EXPENSE.equals(toAccount)) { + return 1L; + } + + return -1L; + } + + /** + * This method validates whether the booking from fromAccount to toAccount + * 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 true if the from->to relationship of the given accounts is valid, false otherwise + */ + public boolean isValidBooking(Account fromAccount, Account toAccount) { + return this.bookingRules.get(fromAccount.getType()).contains(toAccount.getType()); + } +} diff --git a/src/main/java/de/financer/service/TransactionService.java b/src/main/java/de/financer/service/TransactionService.java new file mode 100644 index 0000000..61554e1 --- /dev/null +++ b/src/main/java/de/financer/service/TransactionService.java @@ -0,0 +1,149 @@ +package de.financer.service; + +import de.financer.ResponseReason; +import de.financer.dba.TransactionRepository; +import de.financer.model.Account; +import de.financer.model.Transaction; +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.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Collections; + +@Service +public class TransactionService { + public static final String DATE_FORMAT = "dd.MM.yyyy"; + + @Autowired + private AccountService accountService; + + @Autowired + private RuleService ruleService; + + @Autowired + private TransactionRepository transactionRepository; + + /** + * @return all transactions, for all accounts and all time + */ + public Iterable 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 getAllForAccount(String accountKey) { + final Account account = this.accountService.getAccountByKey(accountKey); + + if (account == null) { + return Collections.emptyList(); + } + + // As we want all transactions of the given account use it as from and to account + return this.transactionRepository.findTransactionsByFromAccountOrToAccount(account, account); + } + + @Transactional(propagation = Propagation.REQUIRED) + public ResponseReason createTransaction(String fromAccountKey, String toAccountKey, Long amount, String date, String description) { + 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); + + fromAccount.setCurrentBalance(fromAccount.getCurrentBalance() + (this.ruleService.getMultiplierFromAccount(fromAccount) * amount)); + 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(ParseException e) { + // TODO log + + response = ResponseReason.INVALID_DATE_FORMAT; + } + catch (Exception e) { + // TODO log + + 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 + * @return the build {@link Transaction} instance + * @throws ParseException if the given date string cannot be parsed into a {@link java.util.Date} instance + */ + private Transaction buildTransaction(Account fromAccount, Account toAccount, Long amount, String description, String date) throws ParseException { + final Transaction transaction = new Transaction(); + + transaction.setFromAccount(fromAccount); + transaction.setToAccount(toAccount); + transaction.setAmount(amount); + transaction.setDescription(description); + transaction.setDate(new SimpleDateFormat(DATE_FORMAT).parse(date)); + + 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 null 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 (date == null) { + response = ResponseReason.MISSING_DATE; + } + + return response; + } +} diff --git a/src/main/resources/config/application-dev.properties b/src/main/resources/config/application-dev.properties new file mode 100644 index 0000000..000f956 --- /dev/null +++ b/src/main/resources/config/application-dev.properties @@ -0,0 +1,2 @@ +# Hibernate +spring.jpa.show-sql=true \ No newline at end of file diff --git a/src/main/resources/config/application-hsqldb.properties b/src/main/resources/config/application-hsqldb.properties new file mode 100644 index 0000000..e2f0f22 --- /dev/null +++ b/src/main/resources/config/application-hsqldb.properties @@ -0,0 +1,6 @@ +spring.flyway.locations=classpath:/database/hsqldb + +# DataSource +#spring.datasource.url=jdbc:hsqldb:file:/tmp/financer +spring.datasource.url=jdbc:hsqldb:mem:. +spring.datasource.username=sa \ No newline at end of file diff --git a/src/main/resources/config/application-postgres.properties b/src/main/resources/config/application-postgres.properties new file mode 100644 index 0000000..944068b --- /dev/null +++ b/src/main/resources/config/application-postgres.properties @@ -0,0 +1 @@ +spring.flyway.locations=classpath:/database/postgres \ No newline at end of file diff --git a/src/main/resources/config/application.properties b/src/main/resources/config/application.properties new file mode 100644 index 0000000..0c43199 --- /dev/null +++ b/src/main/resources/config/application.properties @@ -0,0 +1,15 @@ +### +### This is the main configuration file of the application +### + +spring.profiles.active=hsqldb,dev + +server.servlet.context-path=/financer + +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@ \ No newline at end of file diff --git a/src/main/resources/database/hsqldb/V1_0_0__init.sql b/src/main/resources/database/hsqldb/V1_0_0__init.sql new file mode 100644 index 0000000..7f16c26 --- /dev/null +++ b/src/main/resources/database/hsqldb/V1_0_0__init.sql @@ -0,0 +1,55 @@ +-- +-- This file contains the basic initialization of the financer schema and init data +-- + +-- Account table and init data +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 c_u_name_key UNIQUE ("key") +); + +INSERT INTO account ("key", type, status, current_balance) +VALUES ('accounts.checkaccount', 'BANK', 'OPEN', 0); + +INSERT INTO account ("key", type, status, current_balance) +VALUES ('accounts.income', 'INCOME', 'OPEN', 0); + +INSERT INTO account ("key", type, status, current_balance) +VALUES ('accounts.cash', 'CASH', 'OPEN', 0); + +INSERT INTO account ("key", type, status, current_balance) +VALUES ('accounts.start', 'START', 'OPEN', 0); + +-- 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, + + CONSTRAINT fk_from_account FOREIGN KEY (from_account_id) REFERENCES account (id), + CONSTRAINT fk_to_account FOREIGN KEY (to_account_id) REFERENCES account (id) +); + +-- 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, + + CONSTRAINT fk_from_account FOREIGN KEY (from_account_id) REFERENCES account (id), + CONSTRAINT fk_to_account FOREIGN KEY (to_account_id) REFERENCES account (id) +); \ No newline at end of file From dfe1e08dc7570de1d8a6d3b9168a0ea73748d978 Mon Sep 17 00:00:00 2001 From: MK13 Date: Sun, 17 Feb 2019 23:42:24 +0100 Subject: [PATCH 02/65] - Add reference from Transaction to the RecurringTransaction that created the transaction - Add BANK as valid booking target of a BANK account - Remove EXPENSE as valid booking target of a LIABILITY account - Fix a bug in the getMultiplierFromAccount and getMultiplierToAccount methods that caused the result to always be the default case - Adjust init SQL script to reflect the changes done - Add a sample integration test - Add unit tests for the RuleService - Configure surefire plugin - Add a profile for integration test running --- pom.xml | 40 ++- .../java/de/financer/model/Transaction.java | 10 + .../java/de/financer/service/RuleService.java | 26 +- .../database/hsqldb/V1_0_0__init.sql | 36 +-- .../AccountControllerIntegrationTest.java | 47 ++++ ...eService_getMultiplierFromAccountTest.java | 71 ++++++ ...uleService_getMultiplierToAccountTest.java | 71 ++++++ .../RuleService_isValidBookingTest.java | 233 ++++++++++++++++++ .../application-integrationtest.properties | 17 ++ 9 files changed, 522 insertions(+), 29 deletions(-) create mode 100644 src/test/java/de/financer/controller/integration/AccountControllerIntegrationTest.java create mode 100644 src/test/java/de/financer/service/RuleService_getMultiplierFromAccountTest.java create mode 100644 src/test/java/de/financer/service/RuleService_getMultiplierToAccountTest.java create mode 100644 src/test/java/de/financer/service/RuleService_isValidBookingTest.java create mode 100644 src/test/resources/application-integrationtest.properties diff --git a/pom.xml b/pom.xml index 5ff5359..af9934f 100644 --- a/pom.xml +++ b/pom.xml @@ -75,7 +75,7 @@ junit junit - 4.11 + 4.12 test @@ -87,6 +87,44 @@ org.springframework.boot spring-boot-maven-plugin + + org.apache.maven.plugins + maven-surefire-plugin + + + **/*IntegrationTest + + + + + + + integration-tests + + + + maven-surefire-plugin + + + integration-test + + test + + + + none + + + **/*IntegrationTest + + + + + + + + + diff --git a/src/main/java/de/financer/model/Transaction.java b/src/main/java/de/financer/model/Transaction.java index 06eff5d..e4297f8 100644 --- a/src/main/java/de/financer/model/Transaction.java +++ b/src/main/java/de/financer/model/Transaction.java @@ -18,6 +18,8 @@ public class Transaction { private Date date; private String description; private Long amount; + @OneToOne(fetch = FetchType.EAGER) + private RecurringTransaction recurringTransaction; public Long getId() { return id; @@ -62,4 +64,12 @@ public class Transaction { public void setAmount(Long amount) { this.amount = amount; } + + public RecurringTransaction getRecurringTransaction() { + return recurringTransaction; + } + + public void setRecurringTransaction(RecurringTransaction recurringTransaction) { + this.recurringTransaction = recurringTransaction; + } } diff --git a/src/main/java/de/financer/service/RuleService.java b/src/main/java/de/financer/service/RuleService.java index adb0705..5819b49 100644 --- a/src/main/java/de/financer/service/RuleService.java +++ b/src/main/java/de/financer/service/RuleService.java @@ -31,10 +31,10 @@ public class RuleService implements InitializingBean { // 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(CASH, EXPENSE, LIABILITY)); + 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(LIABILITY, Arrays.asList(BANK, CASH)); this.bookingRules.put(START, Arrays.asList(BANK, CASH, LIABILITY)); } @@ -51,19 +51,21 @@ public class RuleService implements InitializingBean { // There is no multiplier if the from account is an EXPENSE account because // it's not a valid from account type - if (INCOME.equals(fromAccount)) { + final AccountType accountType = fromAccount.getType(); + + if (INCOME.equals(accountType)) { return 1L; } - else if (BANK.equals(fromAccount)) { + else if (BANK.equals(accountType)) { return -1L; } - else if (CASH.equals(fromAccount)) { + else if (CASH.equals(accountType)) { return -1L; } - else if (LIABILITY.equals(fromAccount)) { + else if (LIABILITY.equals(accountType)) { return 1L; } - else if (START.equals(fromAccount)) { + else if (START.equals(accountType)) { return 1L; } @@ -83,16 +85,18 @@ public class RuleService implements InitializingBean { // There are no multipliers for INCOME and START accounts // because they are not valid to account types - if (BANK.equals(toAccount)) { + final AccountType accountType = toAccount.getType(); + + if (BANK.equals(accountType)) { return 1L; } - else if (CASH.equals(toAccount)) { + else if (CASH.equals(accountType)) { return 1L; } - else if (LIABILITY.equals(toAccount)) { + else if (LIABILITY.equals(accountType)) { return -1L; } - else if (EXPENSE.equals(toAccount)) { + else if (EXPENSE.equals(accountType)) { return 1L; } diff --git a/src/main/resources/database/hsqldb/V1_0_0__init.sql b/src/main/resources/database/hsqldb/V1_0_0__init.sql index 7f16c26..16070f2 100644 --- a/src/main/resources/database/hsqldb/V1_0_0__init.sql +++ b/src/main/resources/database/hsqldb/V1_0_0__init.sql @@ -10,7 +10,7 @@ CREATE TABLE account ( status VARCHAR(255) NOT NULL, current_balance BIGINT NOT NULL, - CONSTRAINT c_u_name_key UNIQUE ("key") + CONSTRAINT un_account_name_key UNIQUE ("key") ); INSERT INTO account ("key", type, status, current_balance) @@ -25,20 +25,7 @@ VALUES ('accounts.cash', 'CASH', 'OPEN', 0); INSERT INTO account ("key", type, status, current_balance) VALUES ('accounts.start', 'START', 'OPEN', 0); --- 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, - - CONSTRAINT fk_from_account FOREIGN KEY (from_account_id) REFERENCES account (id), - CONSTRAINT fk_to_account FOREIGN KEY (to_account_id) REFERENCES account (id) -); - --- Transaction table +-- Recurring transaction table CREATE TABLE recurring_transaction ( id BIGINT NOT NULL PRIMARY KEY IDENTITY, from_account_id BIGINT NOT NULL, @@ -50,6 +37,21 @@ CREATE TABLE recurring_transaction ( last_occurrence DATE, holiday_weekend_type VARCHAR(255) NOT NULL, - CONSTRAINT fk_from_account FOREIGN KEY (from_account_id) REFERENCES account (id), - CONSTRAINT fk_to_account FOREIGN KEY (to_account_id) REFERENCES account (id) + 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 NOT NULL, + + 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) ); \ No newline at end of file diff --git a/src/test/java/de/financer/controller/integration/AccountControllerIntegrationTest.java b/src/test/java/de/financer/controller/integration/AccountControllerIntegrationTest.java new file mode 100644 index 0000000..6fdf02e --- /dev/null +++ b/src/test/java/de/financer/controller/integration/AccountControllerIntegrationTest.java @@ -0,0 +1,47 @@ +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 AccountControllerIntegrationTest { + + @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 allAccounts = this.objectMapper.readValue(mvcResult.getResponse().getContentAsByteArray(), new TypeReference>(){}); + + Assert.assertEquals(4, allAccounts.size()); + } +} diff --git a/src/test/java/de/financer/service/RuleService_getMultiplierFromAccountTest.java b/src/test/java/de/financer/service/RuleService_getMultiplierFromAccountTest.java new file mode 100644 index 0000000..d2ddc3a --- /dev/null +++ b/src/test/java/de/financer/service/RuleService_getMultiplierFromAccountTest.java @@ -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; + } +} diff --git a/src/test/java/de/financer/service/RuleService_getMultiplierToAccountTest.java b/src/test/java/de/financer/service/RuleService_getMultiplierToAccountTest.java new file mode 100644 index 0000000..3224e30 --- /dev/null +++ b/src/test/java/de/financer/service/RuleService_getMultiplierToAccountTest.java @@ -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; + } +} diff --git a/src/test/java/de/financer/service/RuleService_isValidBookingTest.java b/src/test/java/de/financer/service/RuleService_isValidBookingTest.java new file mode 100644 index 0000000..45bfb54 --- /dev/null +++ b/src/test/java/de/financer/service/RuleService_isValidBookingTest.java @@ -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, false); + } + + @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; + } +} diff --git a/src/test/resources/application-integrationtest.properties b/src/test/resources/application-integrationtest.properties new file mode 100644 index 0000000..c7c7a35 --- /dev/null +++ b/src/test/resources/application-integrationtest.properties @@ -0,0 +1,17 @@ +### +### This is the main configuration file of the application +### + +server.servlet.context-path=/financer + +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@ + +spring.datasource.url=jdbc:hsqldb:mem:. +spring.datasource.username=sa +spring.flyway.locations=classpath:/database/hsqldb \ No newline at end of file From d2d5d7f38a1f2f5501e75f93d583c92c6f8fb690 Mon Sep 17 00:00:00 2001 From: MK13 Date: Mon, 18 Feb 2019 21:10:13 +0100 Subject: [PATCH 03/65] Add a basic startup test of the app --- .../financer/FinancerApplicationBootTest.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/test/java/de/financer/FinancerApplicationBootTest.java diff --git a/src/test/java/de/financer/FinancerApplicationBootTest.java b/src/test/java/de/financer/FinancerApplicationBootTest.java new file mode 100644 index 0000000..21ec49f --- /dev/null +++ b/src/test/java/de/financer/FinancerApplicationBootTest.java @@ -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); + } +} From f48657092063d27c73e2fc36a8bdb4d943d99163 Mon Sep 17 00:00:00 2001 From: MK13 Date: Mon, 18 Feb 2019 21:13:01 +0100 Subject: [PATCH 04/65] Re-add EXPENSE as a valid booking target of a LIABILITY account (e.g. an instalment purchase can be handled this way) --- src/main/java/de/financer/service/RuleService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/financer/service/RuleService.java b/src/main/java/de/financer/service/RuleService.java index 5819b49..220fe79 100644 --- a/src/main/java/de/financer/service/RuleService.java +++ b/src/main/java/de/financer/service/RuleService.java @@ -34,7 +34,7 @@ public class RuleService implements InitializingBean { 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)); + this.bookingRules.put(LIABILITY, Arrays.asList(BANK, CASH, EXPENSE)); this.bookingRules.put(START, Arrays.asList(BANK, CASH, LIABILITY)); } From a02a5635a0d64ac06ed60780cdaf004977907bd4 Mon Sep 17 00:00:00 2001 From: MK13 Date: Mon, 18 Feb 2019 21:17:23 +0100 Subject: [PATCH 05/65] Cleanup integration test properties --- src/test/resources/application-integrationtest.properties | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/test/resources/application-integrationtest.properties b/src/test/resources/application-integrationtest.properties index c7c7a35..d9b9c6a 100644 --- a/src/test/resources/application-integrationtest.properties +++ b/src/test/resources/application-integrationtest.properties @@ -1,17 +1,11 @@ ### -### This is the main configuration file of the application +### This is the main configuration file for integration tests ### server.servlet.context-path=/financer 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@ - spring.datasource.url=jdbc:hsqldb:mem:. spring.datasource.username=sa spring.flyway.locations=classpath:/database/hsqldb \ No newline at end of file From fe5380a86592036754613a6965ac054ef84a6356 Mon Sep 17 00:00:00 2001 From: MK13 Date: Mon, 18 Feb 2019 21:22:02 +0100 Subject: [PATCH 06/65] Fix broken test (LIABILITY->EXPENSE is now valid) and adjust mapping of RecurringTransaction in Transaction to better reflect the actual relationship --- src/main/java/de/financer/model/Transaction.java | 2 +- .../de/financer/service/RuleService_isValidBookingTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/financer/model/Transaction.java b/src/main/java/de/financer/model/Transaction.java index e4297f8..b42ca7d 100644 --- a/src/main/java/de/financer/model/Transaction.java +++ b/src/main/java/de/financer/model/Transaction.java @@ -18,7 +18,7 @@ public class Transaction { private Date date; private String description; private Long amount; - @OneToOne(fetch = FetchType.EAGER) + @ManyToOne(fetch = FetchType.EAGER) private RecurringTransaction recurringTransaction; public Long getId() { diff --git a/src/test/java/de/financer/service/RuleService_isValidBookingTest.java b/src/test/java/de/financer/service/RuleService_isValidBookingTest.java index 45bfb54..4fb39fa 100644 --- a/src/test/java/de/financer/service/RuleService_isValidBookingTest.java +++ b/src/test/java/de/financer/service/RuleService_isValidBookingTest.java @@ -166,7 +166,7 @@ public class RuleService_isValidBookingTest { @Test public void test_isValidBooking_LIABILITY_EXPENSE() { - doTest(AccountType.LIABILITY, AccountType.EXPENSE, false); + doTest(AccountType.LIABILITY, AccountType.EXPENSE, true); } @Test From 35902afe431b31710c4b60b7f14b31af3396764c Mon Sep 17 00:00:00 2001 From: MK13 Date: Fri, 1 Mar 2019 20:39:31 +0100 Subject: [PATCH 07/65] Various stuff all over the tree - Increase Java version to 1.9 - Add commons-collection and Jollyday dependencies - Add JavaDoc plugin - Add country and state configuration for Jollyday library - Add WIP implementation of the recurring transaction feature - Improve JavaDoc - Use Java 8 date API - Reformatting - Add special Flyway migration version for test data - Add and improve unit tests --- pom.xml | 31 +++- .../de/financer/config/FinancerConfig.java | 53 ++++++ .../RecurringTransactionController.java | 43 +++++ .../controller/TransactionController.java | 8 +- .../dba/RecurringTransactionRepository.java | 5 + .../de/financer/model/HolidayWeekendType.java | 41 ++++- .../financer/model/RecurringTransaction.java | 16 +- .../java/de/financer/model/Transaction.java | 9 +- .../java/de/financer/model/package-info.java | 10 ++ .../service/RecurringTransactionService.java | 163 ++++++++++++++++++ .../java/de/financer/service/RuleService.java | 90 +++++++--- .../financer/service/TransactionService.java | 55 +++--- .../de/financer/service/package-info.java | 9 + .../resources/config/application.properties | 11 +- .../database/hsqldb/V1_0_0__init.sql | 16 +- .../AccountControllerIntegrationTest.java | 12 +- ...getAllDueToday_DAILY_NEXT_WORKDAYTest.java | 134 ++++++++++++++ ...tAllDueToday_MONTHLY_NEXT_WORKDAYTest.java | 156 +++++++++++++++++ ...nsactionService_createTransactionTest.java | 158 +++++++++++++++++ .../application-integrationtest.properties | 10 +- .../integration/V999_99_00__testdata.sql | 22 +++ 21 files changed, 950 insertions(+), 102 deletions(-) create mode 100644 src/main/java/de/financer/config/FinancerConfig.java create mode 100644 src/main/java/de/financer/controller/RecurringTransactionController.java create mode 100644 src/main/java/de/financer/model/package-info.java create mode 100644 src/main/java/de/financer/service/RecurringTransactionService.java create mode 100644 src/main/java/de/financer/service/package-info.java create mode 100644 src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest.java create mode 100644 src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest.java create mode 100644 src/test/java/de/financer/service/TransactionService_createTransactionTest.java create mode 100644 src/test/resources/database/hsqldb/integration/V999_99_00__testdata.sql diff --git a/pom.xml b/pom.xml index af9934f..8fb3e96 100644 --- a/pom.xml +++ b/pom.xml @@ -20,9 +20,9 @@ UTF-8 - 1.8 - 1.8 - 1.8 + 1.9 + 1.9 + 1.9 @@ -46,7 +46,17 @@ org.apache.commons commons-lang3 - 3.8.1 + + + org.apache.commons + commons-collections4 + 4.3 + + + + de.jollyday + jollyday + 0.5.7 @@ -99,6 +109,19 @@ + + + + org.apache.maven.plugins + maven-javadoc-plugin + + private + /usr/bin/javadoc + + + + + integration-tests diff --git a/src/main/java/de/financer/config/FinancerConfig.java b/src/main/java/de/financer/config/FinancerConfig.java new file mode 100644 index 0000000..a3a549a --- /dev/null +++ b/src/main/java/de/financer/config/FinancerConfig.java @@ -0,0 +1,53 @@ +package de.financer.config; + +import de.jollyday.HolidayCalendar; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.Arrays; +import java.util.Optional; + +@Configuration +@ConfigurationProperties(prefix = "financer") +public class FinancerConfig { + private String countryCode; + private String state; + + /** + * @return the raw country code, mostly an uppercase ISO 3166 2-letter code + */ + public String getCountryCode() { + return countryCode; + } + + /** + * @return the state + */ + public String getState() { + return state; + } + + /** + * @return the {@link HolidayCalendar} used to calculate the holidays. Internally uses the country code + * specified via {@link FinancerConfig#getCountryCode}. + */ + public HolidayCalendar getHolidayCalendar() { + final Optional optionalHoliday = Arrays.asList(HolidayCalendar.values()).stream() + .filter((hc) -> hc.getId().equals(this.countryCode)) + .findFirst(); + + if (!optionalHoliday.isPresent()) { + // TODO log info about default DE + } + + return optionalHoliday.orElse(HolidayCalendar.GERMANY); + } + + public void setState(String state) { + this.state = state; + } + + public void setCountryCode(String countryCode) { + this.countryCode = countryCode; + } +} diff --git a/src/main/java/de/financer/controller/RecurringTransactionController.java b/src/main/java/de/financer/controller/RecurringTransactionController.java new file mode 100644 index 0000000..5c4a72b --- /dev/null +++ b/src/main/java/de/financer/controller/RecurringTransactionController.java @@ -0,0 +1,43 @@ +package de.financer.controller; + +import de.financer.model.RecurringTransaction; +import de.financer.service.RecurringTransactionService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("recurringTransactions") +public class RecurringTransactionController { + + @Autowired + private RecurringTransactionService recurringTransactionService; + + @RequestMapping("getAll") + public Iterable getAll() { + return this.recurringTransactionService.getAll(); + } + + @RequestMapping("getAllForAccount") + public Iterable getAllForAccount(String accountKey) { + return this.recurringTransactionService.getAllForAccount(accountKey); + } + + @RequestMapping("getAllDueToday") + public Iterable getAllDueToday() { + return this.recurringTransactionService.getAllDueToday(); + } + + @RequestMapping("createRecurringTransaction") + public ResponseEntity createRecurringTransaction(String fromAccountKey, String toAccountKey, Long amount, + String description, String holidayWeekendType, + String intervalType, String firstOccurrence, + String lastOccurrence) { + return this.recurringTransactionService.createRecurringTransaction(fromAccountKey, toAccountKey, amount, + description, holidayWeekendType, + intervalType, firstOccurrence, + lastOccurrence) + .toResponseEntity(); + } +} diff --git a/src/main/java/de/financer/controller/TransactionController.java b/src/main/java/de/financer/controller/TransactionController.java index aba4b6a..bb14c44 100644 --- a/src/main/java/de/financer/controller/TransactionController.java +++ b/src/main/java/de/financer/controller/TransactionController.java @@ -23,8 +23,10 @@ public class TransactionController { return this.transactionService.getAllForAccount(accountKey); } - @RequestMapping("createTransaction") - public ResponseEntity createTransaction(String fromAccountKey, String toAccountKey, Long amount, String date, String description) { - return this.transactionService.createTransaction(fromAccountKey, toAccountKey, amount, date, description).toResponseEntity(); + @RequestMapping(value = "createTransaction") + public ResponseEntity createTransaction(String fromAccountKey, String toAccountKey, Long amount, String date, + String description) { + return this.transactionService.createTransaction(fromAccountKey, toAccountKey, amount, date, description) + .toResponseEntity(); } } diff --git a/src/main/java/de/financer/dba/RecurringTransactionRepository.java b/src/main/java/de/financer/dba/RecurringTransactionRepository.java index 430cd06..dffe6d5 100644 --- a/src/main/java/de/financer/dba/RecurringTransactionRepository.java +++ b/src/main/java/de/financer/dba/RecurringTransactionRepository.java @@ -1,7 +1,12 @@ package de.financer.dba; +import de.financer.model.Account; import de.financer.model.RecurringTransaction; import org.springframework.data.repository.CrudRepository; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +@Transactional(propagation = Propagation.REQUIRED) public interface RecurringTransactionRepository extends CrudRepository { + Iterable findRecurringTransactionsByFromAccountOrToAccount(Account fromAccount, Account toAccount); } diff --git a/src/main/java/de/financer/model/HolidayWeekendType.java b/src/main/java/de/financer/model/HolidayWeekendType.java index cd2cf20..3de89b0 100644 --- a/src/main/java/de/financer/model/HolidayWeekendType.java +++ b/src/main/java/de/financer/model/HolidayWeekendType.java @@ -10,10 +10,47 @@ public enum HolidayWeekendType { /** Indicates that the action should be done on the specified day regardless whether it's a holiday or a weekend */ SAME_DAY, - /** Indicates that the action should be deferred to the next workday */ + /** + *

+ * Indicates that the action should be deferred to the next workday. + *

+ *
+     *     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
+     * 
+ *
+     *     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
+     * 
+ * + */ NEXT_WORKDAY, - /** Indicates that the action should be dated back to the previous day */ + /** + *

+ * Indicates that the action should be made earlier at the previous day + *

+ *
+     *     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
+     * 
+ *
+     *     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
+     * 
+ */ PREVIOUS_WORKDAY; /** diff --git a/src/main/java/de/financer/model/RecurringTransaction.java b/src/main/java/de/financer/model/RecurringTransaction.java index 97954cc..1a9d69e 100644 --- a/src/main/java/de/financer/model/RecurringTransaction.java +++ b/src/main/java/de/financer/model/RecurringTransaction.java @@ -1,7 +1,7 @@ package de.financer.model; import javax.persistence.*; -import java.util.Date; +import java.time.LocalDate; @Entity public class RecurringTransaction { @@ -16,10 +16,8 @@ public class RecurringTransaction { private Long amount; @Enumerated(EnumType.STRING) private IntervalType intervalType; - @Temporal(TemporalType.DATE) - private Date firstOccurrence; - @Temporal(TemporalType.DATE) - private Date lastOccurrence; + private LocalDate firstOccurrence; + private LocalDate lastOccurrence; @Enumerated(EnumType.STRING) private HolidayWeekendType holidayWeekendType; @@ -67,19 +65,19 @@ public class RecurringTransaction { this.holidayWeekendType = holidayWeekendType; } - public Date getLastOccurrence() { + public LocalDate getLastOccurrence() { return lastOccurrence; } - public void setLastOccurrence(Date lastOccurrence) { + public void setLastOccurrence(LocalDate lastOccurrence) { this.lastOccurrence = lastOccurrence; } - public Date getFirstOccurrence() { + public LocalDate getFirstOccurrence() { return firstOccurrence; } - public void setFirstOccurrence(Date firstOccurrence) { + public void setFirstOccurrence(LocalDate firstOccurrence) { this.firstOccurrence = firstOccurrence; } diff --git a/src/main/java/de/financer/model/Transaction.java b/src/main/java/de/financer/model/Transaction.java index b42ca7d..0b33788 100644 --- a/src/main/java/de/financer/model/Transaction.java +++ b/src/main/java/de/financer/model/Transaction.java @@ -1,7 +1,7 @@ package de.financer.model; import javax.persistence.*; -import java.util.Date; +import java.time.LocalDate; @Entity @Table(name = "\"transaction\"") @@ -13,9 +13,8 @@ public class Transaction { private Account fromAccount; @OneToOne(fetch = FetchType.EAGER) private Account toAccount; - @Temporal(TemporalType.DATE) @Column(name = "\"date\"") - private Date date; + private LocalDate date; private String description; private Long amount; @ManyToOne(fetch = FetchType.EAGER) @@ -41,11 +40,11 @@ public class Transaction { this.toAccount = toAccount; } - public Date getDate() { + public LocalDate getDate() { return date; } - public void setDate(Date date) { + public void setDate(LocalDate date) { this.date = date; } diff --git a/src/main/java/de/financer/model/package-info.java b/src/main/java/de/financer/model/package-info.java new file mode 100644 index 0000000..421aaf9 --- /dev/null +++ b/src/main/java/de/financer/model/package-info.java @@ -0,0 +1,10 @@ +/** + *

+ * This package contains the main model for the financer application. + * In the DDD (Domain Driven Design) 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. + *

+ */ +package de.financer.model; \ No newline at end of file diff --git a/src/main/java/de/financer/service/RecurringTransactionService.java b/src/main/java/de/financer/service/RecurringTransactionService.java new file mode 100644 index 0000000..f03cf76 --- /dev/null +++ b/src/main/java/de/financer/service/RecurringTransactionService.java @@ -0,0 +1,163 @@ +package de.financer.service; + +import de.financer.ResponseReason; +import de.financer.dba.RecurringTransactionRepository; +import de.financer.model.Account; +import de.financer.model.HolidayWeekendType; +import de.financer.model.RecurringTransaction; +import org.apache.commons.collections4.IterableUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.util.Collections; +import java.util.stream.Collectors; + +@Service +public class RecurringTransactionService { + + @Autowired + private RecurringTransactionRepository recurringTransactionRepository; + + @Autowired + private AccountService accountService; + + @Autowired + private RuleService ruleService; + + public Iterable getAll() { + return this.recurringTransactionRepository.findAll(); + } + + public Iterable getAllForAccount(String accountKey) { + final Account account = this.accountService.getAccountByKey(accountKey); + + if (account == null) { + return Collections.emptyList(); + } + + // As we want all transactions of the given account use it as from and to account + return this.recurringTransactionRepository.findRecurringTransactionsByFromAccountOrToAccount(account, account); + } + + /** + * This method gets all recurring transactions that are due today. Whether a recurring transaction is due today + * depends on today's date and the configured {@link RecurringTransaction#getIntervalType() interval type} + * and {@link RecurringTransaction#getHolidayWeekendType() holiday weekend type}. + * + * @return all recurring transactions that are due today + */ + public Iterable getAllDueToday() { + return this.getAllDueToday(LocalDate.now()); + } + + // Visible for unit tests + /* package */ Iterable getAllDueToday(LocalDate now) { + // TODO filter for lastOccurrence not in the past + final Iterable allRecurringTransactions = this.recurringTransactionRepository.findAll(); + + //@formatter:off + return IterableUtils.toList(allRecurringTransactions).stream() + .filter((rt) -> checkRecurringTransactionDueToday(rt, now) || + checkRecurringTransactionDuePast(rt, now)) + // TODO checkRecurringTransactionDueFuture for HolidayWeekendType.PREVIOUS_WORKDAY + .collect(Collectors.toList()); + //@formatter:on + } + + /** + * This method checks whether the given {@link RecurringTransaction} is due today. + * A recurring transaction is due if the current {@link LocalDate date} is a multiple of the + * {@link RecurringTransaction#getFirstOccurrence() first occurrence} of the recurring transaction and the + * {@link RecurringTransaction#getIntervalType() interval type}. If today is a + * {@link RuleService#isHoliday(LocalDate) holiday} or a + * {@link RuleService#isWeekend(LocalDate) weekend day} the + * {@link HolidayWeekendType holiday weekend type} + * is taken into account to decide whether the recurring transaction should be deferred. + * + * @param recurringTransaction to check whether it is due today + * @param now today's date + * @return true if the recurring transaction is due today, false otherwise + */ + private boolean checkRecurringTransactionDueToday(RecurringTransaction recurringTransaction, LocalDate now) { + final boolean holiday = this.ruleService.isHoliday(now); + + final boolean dueToday = recurringTransaction.getFirstOccurrence() + // This calculates all dates between the first occurrence of the + // recurring transaction and tomorrow for the interval specified + // by the recurring transaction. We need to use tomorrow as + // upper bound of the interval because the upper bound is exclusive + // in the datesUntil method. + .datesUntil(now.plusDays(1), this.ruleService + .getPeriodForInterval(recurringTransaction + .getIntervalType())) + // Then we check whether today is a date in the calculated range. + // If so the recurring transaction is due today + .anyMatch((d) -> d.equals(now)); + final boolean weekend = this.ruleService.isWeekend(now); + boolean defer = false; + + + if (holiday || weekend) { + defer = recurringTransaction.getHolidayWeekendType() == HolidayWeekendType.NEXT_WORKDAY; + } + + return !defer && dueToday; + } + + /** + * This method checks whether the given {@link RecurringTransaction} was actually due in the close past + * but has been deferred to maybe 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 true if the recurring transaction is due today, false otherwise + */ + private boolean checkRecurringTransactionDuePast(RecurringTransaction recurringTransaction, LocalDate now) { + boolean weekend; + boolean holiday; + LocalDate yesterday = now; + boolean due = false; + + // Go back in time until we hit the first non-holiday, non-weekend day + // and check for every day in between if the given recurring transaction was due on this day + do { + yesterday = yesterday.minusDays(1); + holiday = this.ruleService.isHoliday(yesterday); + weekend = this.ruleService.isWeekend(yesterday); + + if (holiday || weekend) { + // Lambdas require final local variables + final LocalDate finalYesterday = yesterday; + + // For an explanation of the expression see the ...DueToday method + due = recurringTransaction.getFirstOccurrence() + .datesUntil(yesterday.plusDays(1), this.ruleService + .getPeriodForInterval(recurringTransaction + .getIntervalType())) + .anyMatch((d) -> d.equals(finalYesterday)); + + if (due) { + break; + } + } + } + while (holiday || weekend); + + return due; + } + + public ResponseReason createRecurringTransaction(String fromAccountKey, String toAccountKey, Long amount, + String description, String holidayWeekendType, + String intervalType, String firstOccurrence, + String lastOccurrence) { + return null; + } +} diff --git a/src/main/java/de/financer/service/RuleService.java b/src/main/java/de/financer/service/RuleService.java index 220fe79..7dee141 100644 --- a/src/main/java/de/financer/service/RuleService.java +++ b/src/main/java/de/financer/service/RuleService.java @@ -1,27 +1,51 @@ package de.financer.service; +import de.financer.config.FinancerConfig; import de.financer.model.Account; import de.financer.model.AccountType; +import de.financer.model.IntervalType; +import de.jollyday.HolidayManager; +import de.jollyday.ManagerParameters; import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.Period; import java.util.*; import static de.financer.model.AccountType.*; /** - * This service encapsulates methods that form business logic rules. + * This service encapsulates methods that form basic logic rules. * While most of the logic could be placed elsewhere this service provides - * centralized access to these rules. + * centralized access to these rules. Placing them in here also enables easy + * unit testing. */ @Service public class RuleService implements InitializingBean { + @Autowired + private FinancerConfig financerConfig; + private Map> bookingRules; + private Map intervalPeriods; @Override public void afterPropertiesSet() { initBookingRules(); + initIntervalValues(); + } + + private void initIntervalValues() { + this.intervalPeriods = new EnumMap<>(IntervalType.class); + + this.intervalPeriods.put(IntervalType.DAILY, Period.ofDays(1)); + this.intervalPeriods.put(IntervalType.WEEKLY, Period.ofWeeks(1)); + this.intervalPeriods.put(IntervalType.MONTHLY, Period.ofMonths(1)); + this.intervalPeriods.put(IntervalType.QUARTERLY, Period.ofMonths(3)); + this.intervalPeriods.put(IntervalType.YEARLY, Period.ofYears(1)); } private void initBookingRules() { @@ -40,7 +64,7 @@ public class RuleService implements InitializingBean { /** * This method returns the multiplier for the given from account. - * + *

* The multiplier controls whether the current amount of the given from account is increased or * decreased depending on the {@link AccountType} of the given account. * @@ -55,17 +79,13 @@ public class RuleService implements InitializingBean { if (INCOME.equals(accountType)) { return 1L; - } - else if (BANK.equals(accountType)) { + } else if (BANK.equals(accountType)) { return -1L; - } - else if (CASH.equals(accountType)) { + } else if (CASH.equals(accountType)) { return -1L; - } - else if (LIABILITY.equals(accountType)) { + } else if (LIABILITY.equals(accountType)) { return 1L; - } - else if (START.equals(accountType)) { + } else if (START.equals(accountType)) { return 1L; } @@ -74,7 +94,7 @@ public class RuleService implements InitializingBean { /** * This method returns the multiplier for the given to account. - * + *

* The multiplier controls whether the current amount of the given to account is increased or * decreased depending on the {@link AccountType} of the given account. * @@ -89,14 +109,11 @@ public class RuleService implements InitializingBean { if (BANK.equals(accountType)) { return 1L; - } - else if (CASH.equals(accountType)) { + } else if (CASH.equals(accountType)) { return 1L; - } - else if (LIABILITY.equals(accountType)) { + } else if (LIABILITY.equals(accountType)) { return -1L; - } - else if (EXPENSE.equals(accountType)) { + } else if (EXPENSE.equals(accountType)) { return 1L; } @@ -109,10 +126,43 @@ public class RuleService implements InitializingBean { * account does not make sense and is declared as invalid. * * @param fromAccount the account to subtract the money from - * @param toAccount the account to add the money to - * @return true if the from->to relationship of the given accounts is valid, false otherwise + * @param toAccount the account to add the money to + * @return true if the from->to relationship of the given accounts is valid, false 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 true if the given date is a holiday, false otherwise + */ + public boolean isHoliday(LocalDate now) { + return HolidayManager.getInstance(ManagerParameters.create(this.financerConfig.getHolidayCalendar())) + .isHoliday(now, this.financerConfig.getState()); + } + + /** + * This method checks whether the given date is a weekend day, i.e. whether it's a + * {@link DayOfWeek#SATURDAY} or {@link DayOfWeek#SUNDAY}. + * + * @param now the date to check + * @return true if the given date is a weekend day, false otherwise + */ + public boolean isWeekend(LocalDate now) { + return EnumSet.of(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY).contains(now.getDayOfWeek()); + } } diff --git a/src/main/java/de/financer/service/TransactionService.java b/src/main/java/de/financer/service/TransactionService.java index 61554e1..7e3cdca 100644 --- a/src/main/java/de/financer/service/TransactionService.java +++ b/src/main/java/de/financer/service/TransactionService.java @@ -9,8 +9,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; -import java.text.ParseException; -import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.Collections; @Service @@ -36,7 +37,7 @@ public class TransactionService { /** * @param accountKey the key of the account to get the transactions for * @return all transactions for the given account, for all time. Returns an empty list if the given key does not - * match any account. + * match any account. */ public Iterable getAllForAccount(String accountKey) { final Account account = this.accountService.getAccountByKey(accountKey); @@ -63,8 +64,10 @@ public class TransactionService { try { final Transaction transaction = buildTransaction(fromAccount, toAccount, amount, description, date); - fromAccount.setCurrentBalance(fromAccount.getCurrentBalance() + (this.ruleService.getMultiplierFromAccount(fromAccount) * amount)); - toAccount.setCurrentBalance(toAccount.getCurrentBalance() + (this.ruleService.getMultiplierToAccount(toAccount) * amount)); + fromAccount.setCurrentBalance(fromAccount.getCurrentBalance() + (this.ruleService + .getMultiplierFromAccount(fromAccount) * amount)); + toAccount.setCurrentBalance(toAccount.getCurrentBalance() + (this.ruleService + .getMultiplierToAccount(toAccount) * amount)); this.transactionRepository.save(transaction); @@ -72,13 +75,11 @@ public class TransactionService { this.accountService.saveAccount(toAccount); response = ResponseReason.OK; - } - catch(ParseException e) { + } catch (DateTimeParseException e) { // TODO log response = ResponseReason.INVALID_DATE_FORMAT; - } - catch (Exception e) { + } catch (Exception e) { // TODO log response = ResponseReason.UNKNOWN_ERROR; @@ -91,21 +92,21 @@ public class TransactionService { * This method builds the actual transaction object with the given values. * * @param fromAccount the from account - * @param toAccount the to account - * @param amount the transaction amount + * @param toAccount the to account + * @param amount the transaction amount * @param description the description of the transaction - * @param date the date of the transaction + * @param date the date of the transaction * @return the build {@link Transaction} instance - * @throws ParseException if the given date string cannot be parsed into a {@link java.util.Date} instance + * @throws DateTimeParseException if the given date string cannot be parsed into a {@link java.time.LocalDate} instance */ - private Transaction buildTransaction(Account fromAccount, Account toAccount, Long amount, String description, String date) throws ParseException { + private Transaction buildTransaction(Account fromAccount, Account toAccount, Long amount, String description, String date) throws DateTimeParseException { final Transaction transaction = new Transaction(); transaction.setFromAccount(fromAccount); transaction.setToAccount(toAccount); transaction.setAmount(amount); transaction.setDescription(description); - transaction.setDate(new SimpleDateFormat(DATE_FORMAT).parse(date)); + transaction.setDate(LocalDate.parse(date, DateTimeFormatter.ofPattern(DATE_FORMAT))); return transaction; } @@ -114,9 +115,9 @@ public class TransactionService { * This method checks whether the parameters for creating a transaction are valid. * * @param fromAccount the from account - * @param toAccount the to account - * @param amount the transaction amount - * @param date the transaction date + * @param toAccount the to account + * @param amount the transaction amount + * @param date the transaction date * @return the first error found or null if all parameters are valid */ private ResponseReason validateParameters(Account fromAccount, Account toAccount, Long amount, String date) { @@ -124,23 +125,17 @@ public class TransactionService { if (fromAccount == null && toAccount == null) { response = ResponseReason.FROM_AND_TO_ACCOUNT_NOT_FOUND; - } - else if (toAccount == null) { + } else if (toAccount == null) { response = ResponseReason.TO_ACCOUNT_NOT_FOUND; - } - else if (fromAccount == null) { + } else if (fromAccount == null) { response = ResponseReason.FROM_ACCOUNT_NOT_FOUND; - } - else if (!this.ruleService.isValidBooking(fromAccount, toAccount)) { + } else if (!this.ruleService.isValidBooking(fromAccount, toAccount)) { response = ResponseReason.INVALID_BOOKING_ACCOUNTS; - } - else if (amount == null) { + } else if (amount == null) { response = ResponseReason.MISSING_AMOUNT; - } - else if (amount == 0l) { + } else if (amount == 0l) { response = ResponseReason.AMOUNT_ZERO; - } - else if (date == null) { + } else if (date == null) { response = ResponseReason.MISSING_DATE; } diff --git a/src/main/java/de/financer/service/package-info.java b/src/main/java/de/financer/service/package-info.java new file mode 100644 index 0000000..d440c80 --- /dev/null +++ b/src/main/java/de/financer/service/package-info.java @@ -0,0 +1,9 @@ +/** + *

+ * 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. + *

+ */ +package de.financer.service; \ No newline at end of file diff --git a/src/main/resources/config/application.properties b/src/main/resources/config/application.properties index 0c43199..00c8e39 100644 --- a/src/main/resources/config/application.properties +++ b/src/main/resources/config/application.properties @@ -12,4 +12,13 @@ info.app.name=Financer info.app.description=A simple server for personal finance administration info.build.group=@project.groupId@ info.build.artifact=@project.artifactId@ -info.build.version=@project.version@ \ No newline at end of file +info.build.version=@project.version@ + +# Country code for holiday checks +# Mostly an uppercase ISO 3166 2-letter code +# For a complete list of the supported codes see https://github.com/svendiedrichsen/jollyday/blob/master/src/main/java/de/jollyday/HolidayCalendar.java +financer.countryCode=DE + +# The state used for holiday checks +# For a complete list of the supported states see e.g. https://github.com/svendiedrichsen/jollyday/blob/master/src/main/resources/holidays/Holidays_de.xml +financer.state=sl \ No newline at end of file diff --git a/src/main/resources/database/hsqldb/V1_0_0__init.sql b/src/main/resources/database/hsqldb/V1_0_0__init.sql index 16070f2..1cfbe37 100644 --- a/src/main/resources/database/hsqldb/V1_0_0__init.sql +++ b/src/main/resources/database/hsqldb/V1_0_0__init.sql @@ -1,8 +1,8 @@ -- --- This file contains the basic initialization of the financer schema and init data +-- This file contains the basic initialization of the financer schema -- --- Account table and init data +-- Account table CREATE TABLE account ( id BIGINT NOT NULL PRIMARY KEY IDENTITY, "key" VARCHAR(1000) NOT NULL, --escape keyword "key" @@ -13,18 +13,6 @@ CREATE TABLE account ( CONSTRAINT un_account_name_key UNIQUE ("key") ); -INSERT INTO account ("key", type, status, current_balance) -VALUES ('accounts.checkaccount', 'BANK', 'OPEN', 0); - -INSERT INTO account ("key", type, status, current_balance) -VALUES ('accounts.income', 'INCOME', 'OPEN', 0); - -INSERT INTO account ("key", type, status, current_balance) -VALUES ('accounts.cash', 'CASH', 'OPEN', 0); - -INSERT INTO account ("key", type, status, current_balance) -VALUES ('accounts.start', 'START', 'OPEN', 0); - -- Recurring transaction table CREATE TABLE recurring_transaction ( id BIGINT NOT NULL PRIMARY KEY IDENTITY, diff --git a/src/test/java/de/financer/controller/integration/AccountControllerIntegrationTest.java b/src/test/java/de/financer/controller/integration/AccountControllerIntegrationTest.java index 6fdf02e..910b4c3 100644 --- a/src/test/java/de/financer/controller/integration/AccountControllerIntegrationTest.java +++ b/src/test/java/de/financer/controller/integration/AccountControllerIntegrationTest.java @@ -36,12 +36,14 @@ public class AccountControllerIntegrationTest { @Test public void test_getAll() throws Exception { - final MvcResult mvcResult = this.mockMvc.perform(get("/accounts/getAll").contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andReturn(); + final MvcResult mvcResult = this.mockMvc + .perform(get("/accounts/getAll").contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn(); - final List allAccounts = this.objectMapper.readValue(mvcResult.getResponse().getContentAsByteArray(), new TypeReference>(){}); + final List allAccounts = this.objectMapper + .readValue(mvcResult.getResponse().getContentAsByteArray(), new TypeReference>() {}); - Assert.assertEquals(4, allAccounts.size()); + Assert.assertEquals(5, allAccounts.size()); } } diff --git a/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest.java b/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest.java new file mode 100644 index 0000000..a978bf5 --- /dev/null +++ b/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest.java @@ -0,0 +1,134 @@ +package de.financer.service; + +import de.financer.dba.RecurringTransactionRepository; +import de.financer.model.HolidayWeekendType; +import de.financer.model.IntervalType; +import de.financer.model.RecurringTransaction; +import org.apache.commons.collections4.IterableUtils; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import java.time.LocalDate; +import java.time.Period; +import java.util.Collections; + +/** + * This class contains tests for the {@link RecurringTransactionService}, specifically for + * {@link RecurringTransaction}s that have {@link IntervalType#DAILY} and {@link HolidayWeekendType#NEXT_WORKDAY}. + * Due to these restrictions this class does not contain any tests for recurring transactions due in the close past + * that have been deferred, because recurring transactions with interval type daily get executed on the next workday + * anyway, regardless whether they have been deferred. This means that some executions of a recurring transaction with + * daily/next workday get ignored if they are on a holiday or a weekend day - they do not get executed multiple + * times on the next workday. While this is somehow unfortunate it is not in the current requirements and + * therefore left out for the sake of simplicity. If such a behavior is required daily/same day should do the trick, + * even though with slightly different semantics (execution even on holidays or weekends). + */ +@RunWith(MockitoJUnitRunner.class) +public class RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest { + @InjectMocks + private RecurringTransactionService classUnderTest; + + @Mock + private RecurringTransactionRepository recurringTransactionRepository; + + @Mock + private RuleService ruleService; + + @Before + public void setUp() { + Mockito.when(this.ruleService.getPeriodForInterval(IntervalType.DAILY)).thenReturn(Period.ofDays(1)); + } + + /** + * This method tests whether a recurring transaction with firstOccurrence = three days ago, intervalType = daily and + * holidayWeekendType = next_workday is due on a non-holiday, non-weekend day + */ + @Test + public void test_getAllDueToday_dueToday() { + // Arrange + // Implicitly: ruleService.isHoliday().return(false) and ruleService.isWeekend().return(false) + Mockito.when(this.recurringTransactionRepository.findAll()).thenReturn(Collections.singletonList(createRecurringTransaction(-3))); + final LocalDate now = LocalDate.now(); + + // Act + final Iterable 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 not due on a holiday, non-weekend day + */ + @Test + public void test_getAllDueToday_dueToday_holiday() { + // Arrange + // Implicitly: ruleService.isWeekend().return(false) + Mockito.when(this.recurringTransactionRepository.findAll()).thenReturn(Collections.singletonList(createRecurringTransaction(0))); + // Today is a holiday, but yesterday was not + Mockito.when(this.ruleService.isHoliday(Mockito.any())).thenReturn(Boolean.TRUE, Boolean.FALSE); + final LocalDate now = LocalDate.now(); + + // Act + final Iterable 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 not due on a non-holiday, weekend day + */ + @Test + public void test_getAllDueToday_dueToday_weekend() { + // Arrange + // Implicitly: ruleService.isHoliday().return(false) + Mockito.when(this.recurringTransactionRepository.findAll()).thenReturn(Collections.singletonList(createRecurringTransaction(0))); + // Today is a weekend day, but yesterday was not + Mockito.when(this.ruleService.isWeekend(Mockito.any())).thenReturn(Boolean.TRUE, Boolean.FALSE); + final LocalDate now = LocalDate.now(); + + // Act + final Iterable 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 not due today + */ + @Test + public void test_getAllDueToday_dueToday_tomorrow() { + // Arrange + // Implicitly: ruleService.isHoliday().return(false) and ruleService.isWeekend().return(false) + Mockito.when(this.recurringTransactionRepository.findAll()).thenReturn(Collections.singletonList(createRecurringTransaction(1))); + final LocalDate now = LocalDate.now(); + + // Act + final Iterable recurringDueToday = this.classUnderTest.getAllDueToday(now); + + // Assert + Assert.assertEquals(0, IterableUtils.size(recurringDueToday)); + } + + private RecurringTransaction createRecurringTransaction(int days) { + final RecurringTransaction recurringTransaction = new RecurringTransaction(); + + recurringTransaction.setFirstOccurrence(LocalDate.now().plusDays(days)); + + recurringTransaction.setHolidayWeekendType(HolidayWeekendType.NEXT_WORKDAY); + recurringTransaction.setIntervalType(IntervalType.DAILY); + + return recurringTransaction; + } +} diff --git a/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest.java b/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest.java new file mode 100644 index 0000000..9299dea --- /dev/null +++ b/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest.java @@ -0,0 +1,156 @@ +package de.financer.service; + +import de.financer.dba.RecurringTransactionRepository; +import de.financer.model.HolidayWeekendType; +import de.financer.model.IntervalType; +import de.financer.model.RecurringTransaction; +import org.apache.commons.collections4.IterableUtils; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import java.time.LocalDate; +import java.time.Period; +import java.util.Collections; + +@RunWith(MockitoJUnitRunner.class) +public class RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest { + @InjectMocks + private RecurringTransactionService classUnderTest; + + @Mock + private RecurringTransactionRepository recurringTransactionRepository; + + @Mock + private RuleService ruleService; + + @Before + public void setUp() { + Mockito.when(this.ruleService.getPeriodForInterval(IntervalType.MONTHLY)).thenReturn(Period.ofMonths(1)); + } + + /** + * This method tests whether a recurring transaction with firstOccurrence = one month and one day ago + * (and thus was actually due yesterday), intervalType = monthly and holidayWeekendType = next_workday is due today, + * if yesterday was a holiday but today is not + */ + @Test + public void test_getAllDueToday_duePast_holiday() { + // Arrange + Mockito.when(this.recurringTransactionRepository.findAll()) + .thenReturn(Collections.singletonList(createRecurringTransaction(-1))); + // Today is not a holiday but yesterday was + Mockito.when(this.ruleService.isHoliday(Mockito.any())).thenReturn(Boolean.FALSE, Boolean.TRUE); + final LocalDate now = LocalDate.now(); + + // Act + final Iterable recurringDueToday = this.classUnderTest.getAllDueToday(now); + + // Assert + Assert.assertEquals(1, IterableUtils.size(recurringDueToday)); + } + + /** + * This method tests whether a recurring transaction with firstOccurrence = last friday one month ago + * (and thus was actually due last friday), intervalType = monthly and holidayWeekendType = next_workday is due + * today (monday), if friday was holiday + */ + @Test + public void test_getAllDueToday_duePast_weekend_friday_holiday() { + //@formatter:off + // MO TU WE TH FR SA SU -> Weekdays + // 1 2 3 4 5 6 7 -> Ordinal + // H WE WE -> Holiday/WeekEnd + // X -> Scheduled recurring transaction + // O -> now + // + // So now - (ordinal +- offset) + // now - (3 - 1) = previous MO + // now - 3 = previous SU + // now - (3 + 2) = previous FR + //@formatter:on + + // Arrange + final LocalDate now = LocalDate.now(); + final LocalDate monday = now.minusDays(now.getDayOfWeek().getValue() - 1); + // The transaction occurs on a friday + Mockito.when(this.recurringTransactionRepository.findAll()) + .thenReturn(Collections.singletonList(createRecurringTransaction(-(now.getDayOfWeek().getValue() + 2)))); + // First False for the dueToday check, 2x True for actual weekend, second False for Friday + Mockito.when(this.ruleService.isWeekend(Mockito.any())) + .thenReturn(Boolean.FALSE, Boolean.TRUE, Boolean.TRUE, Boolean.FALSE); + // First False for the dueToday check, 2x False for actual weekend, True for Friday + Mockito.when(this.ruleService.isHoliday(Mockito.any())) + .thenReturn(Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, Boolean.TRUE); + + // Act + final Iterable recurringDueToday = this.classUnderTest.getAllDueToday(monday); + + // Assert + Assert.assertEquals(1, IterableUtils.size(recurringDueToday)); + } + + /** + * This method tests whether a recurring transaction with firstOccurrence = last sunday a month ago + * (and thus was actually due last sunday/yesterday), intervalType = monthly and holidayWeekendType = next_workday + * is due today (monday) + */ + @Test + public void test_getAllDueToday_duePast_weekend_sunday() { + // Arrange + final LocalDate now = LocalDate.now(); + final LocalDate monday = now.minusDays(now.getDayOfWeek().getValue() - 1); + // The transaction occurs on a sunday + Mockito.when(this.recurringTransactionRepository.findAll()) + .thenReturn(Collections.singletonList(createRecurringTransaction(-now.getDayOfWeek().getValue()))); + // First False for the dueToday check, 2x True for actual weekend, second False for Friday + Mockito.when(this.ruleService.isWeekend(Mockito.any())) + .thenReturn(Boolean.FALSE, Boolean.TRUE, Boolean.TRUE, Boolean.FALSE); + + // Act + final Iterable recurringDueToday = this.classUnderTest.getAllDueToday(monday); + + // Assert + Assert.assertEquals(1, IterableUtils.size(recurringDueToday)); + } + + /** + * This method tests whether a recurring transaction with firstOccurrence = saturday a month ago + * (and thus was actually due last saturday/two days ago), intervalType = monthly and + * holidayWeekendType = next_workday is due today (monday) + */ + @Test + public void test_getAllDueToday_duePast_weekend_saturday() { + // Arrange + final LocalDate now = LocalDate.now(); + final LocalDate monday = now.minusDays(now.getDayOfWeek().getValue() - 1); + // The transaction occurs on a saturday + Mockito.when(this.recurringTransactionRepository.findAll()) + .thenReturn(Collections.singletonList(createRecurringTransaction(-(now.getDayOfWeek().getValue() + 1)))); + // First False for the dueToday check, 2x True for actual weekend, second False for Friday + Mockito.when(this.ruleService.isWeekend(Mockito.any())) + .thenReturn(Boolean.FALSE, Boolean.TRUE, Boolean.TRUE, Boolean.FALSE); + + // Act + final Iterable recurringDueToday = this.classUnderTest.getAllDueToday(monday); + + // Assert + Assert.assertEquals(1, IterableUtils.size(recurringDueToday)); + } + + private RecurringTransaction createRecurringTransaction(int days) { + final RecurringTransaction recurringTransaction = new RecurringTransaction(); + + recurringTransaction.setFirstOccurrence(LocalDate.now().plusDays(days).minusMonths(1)); + + recurringTransaction.setHolidayWeekendType(HolidayWeekendType.NEXT_WORKDAY); + recurringTransaction.setIntervalType(IntervalType.MONTHLY); + + return recurringTransaction; + } +} diff --git a/src/test/java/de/financer/service/TransactionService_createTransactionTest.java b/src/test/java/de/financer/service/TransactionService_createTransactionTest.java new file mode 100644 index 0000000..5c2fd07 --- /dev/null +++ b/src/test/java/de/financer/service/TransactionService_createTransactionTest.java @@ -0,0 +1,158 @@ +package de.financer.service; + +import de.financer.ResponseReason; +import de.financer.dba.TransactionRepository; +import de.financer.model.Account; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class TransactionService_createTransactionTest { + @InjectMocks + private TransactionService classUnderTest; + + @Mock + private AccountService accountService; + + @Mock + private RuleService ruleService; + + @Mock + private TransactionRepository transactionRepository; + + @Test + public void test_createTransaction_FROM_AND_TO_ACCOUNT_NOT_FOUND() { + // Arrange + // Nothing to do, if we do not instruct the account service instance to return anything the accounts + // will not be found. + + // Act + final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.invalid", Long.valueOf(150l), "24.02.2019", "XXX"); + + // Assert + Assert.assertEquals(ResponseReason.FROM_AND_TO_ACCOUNT_NOT_FOUND, response); + } + + @Test + public void test_createTransaction_TO_ACCOUNT_NOT_FOUND() { + // Arrange + Mockito.when(this.accountService.getAccountByKey(Mockito.anyString())).thenReturn(createAccount(), null); + + // Act + final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.invalid", Long.valueOf(150l), "24.02.2019", "XXX"); + + // Assert + Assert.assertEquals(ResponseReason.TO_ACCOUNT_NOT_FOUND, response); + } + + @Test + public void test_createTransaction_FROM_ACCOUNT_NOT_FOUND() { + // Arrange + Mockito.when(this.accountService.getAccountByKey(Mockito.anyString())).thenReturn(null, createAccount()); + + // Act + final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.invalid", Long.valueOf(150l), "24.02.2019", "XXX"); + + // Assert + Assert.assertEquals(ResponseReason.FROM_ACCOUNT_NOT_FOUND, response); + } + + @Test + public void test_createTransaction_INVALID_BOOKING_ACCOUNTS() { + // Arrange + Mockito.when(this.accountService.getAccountByKey(Mockito.anyString())).thenReturn(createAccount(), createAccount()); + Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.FALSE); + + // Act + final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.invalid", Long.valueOf(150l), "24.02.2019", "XXX"); + + // Assert + Assert.assertEquals(ResponseReason.INVALID_BOOKING_ACCOUNTS, response); + } + + @Test + public void test_createTransaction_MISSING_AMOUNT() { + // Arrange + Mockito.when(this.accountService.getAccountByKey(Mockito.anyString())).thenReturn(createAccount(), createAccount()); + Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.TRUE); + + // Act + final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.invalid", null, "24.02.2019", "XXX"); + + // Assert + Assert.assertEquals(ResponseReason.MISSING_AMOUNT, response); + } + + @Test + public void test_createTransaction_AMOUNT_ZERO() { + // Arrange + Mockito.when(this.accountService.getAccountByKey(Mockito.anyString())).thenReturn(createAccount(), createAccount()); + Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.TRUE); + + // Act + final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.invalid", Long.valueOf(0l), "24.02.2019", "XXX"); + + // Assert + Assert.assertEquals(ResponseReason.AMOUNT_ZERO, response); + } + + @Test + public void test_createTransaction_MISSING_DATE() { + // Arrange + Mockito.when(this.accountService.getAccountByKey(Mockito.anyString())).thenReturn(createAccount(), createAccount()); + Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.TRUE); + + // Act + final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.invalid", Long.valueOf(125l), null, "XXX"); + + // Assert + Assert.assertEquals(ResponseReason.MISSING_DATE, response); + } + + @Test + public void test_createTransaction_INVALID_DATE_FORMAT() { + // Arrange + Mockito.when(this.accountService.getAccountByKey(Mockito.anyString())).thenReturn(createAccount(), createAccount()); + Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.TRUE); + + // Act + final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.invalid", Long.valueOf(125l), "2019-01-01", "XXX"); + + // Assert + Assert.assertEquals(ResponseReason.INVALID_DATE_FORMAT, response); + } + + @Test + public void test_createTransaction_OK() { + // Arrange + final Account fromAccount = Mockito.mock(Account.class); + final Account toAccount = Mockito.mock(Account.class); + Mockito.when(this.accountService.getAccountByKey(Mockito.anyString())).thenReturn(fromAccount, toAccount); + Mockito.when(this.ruleService.isValidBooking(Mockito.any(), Mockito.any())).thenReturn(Boolean.TRUE); + Mockito.when(this.ruleService.getMultiplierFromAccount(Mockito.any())).thenReturn(Long.valueOf(-1l)); + Mockito.when(this.ruleService.getMultiplierToAccount(Mockito.any())).thenReturn(Long.valueOf(1l)); + Mockito.when(fromAccount.getCurrentBalance()).thenReturn(Long.valueOf(0l)); + Mockito.when(toAccount.getCurrentBalance()).thenReturn(Long.valueOf(0l)); + + // Act + final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.invalid", Long.valueOf(125l), "24.02.2019", "XXX"); + + // Assert + Assert.assertEquals(ResponseReason.OK, response); + Mockito.verify(fromAccount, Mockito.times(1)).setCurrentBalance(Long.valueOf(-125)); + Mockito.verify(toAccount, Mockito.times(1)).setCurrentBalance(Long.valueOf(125)); + } + + public Account createAccount() { + final Account account = new Account(); + + account.setCurrentBalance(Long.valueOf(0l)); + + return account; + } +} diff --git a/src/test/resources/application-integrationtest.properties b/src/test/resources/application-integrationtest.properties index d9b9c6a..b75a1d2 100644 --- a/src/test/resources/application-integrationtest.properties +++ b/src/test/resources/application-integrationtest.properties @@ -1,11 +1,3 @@ -### -### This is the main configuration file for integration tests -### - -server.servlet.context-path=/financer - -spring.jpa.hibernate.ddl-auto=validate - spring.datasource.url=jdbc:hsqldb:mem:. spring.datasource.username=sa -spring.flyway.locations=classpath:/database/hsqldb \ No newline at end of file +spring.flyway.locations=classpath:/database/hsqldb,classpath:/database/hsqldb/integration diff --git a/src/test/resources/database/hsqldb/integration/V999_99_00__testdata.sql b/src/test/resources/database/hsqldb/integration/V999_99_00__testdata.sql new file mode 100644 index 0000000..468578d --- /dev/null +++ b/src/test/resources/database/hsqldb/integration/V999_99_00__testdata.sql @@ -0,0 +1,22 @@ +-- Accounts +INSERT INTO account (id, "key", type, status, current_balance) +VALUES (1, 'accounts.checkaccount', 'BANK', 'OPEN', 0); -- insert first with ID 1 so we get predictable numbering + +INSERT INTO account ("key", type, status, current_balance) +VALUES ('accounts.income', 'INCOME', 'OPEN', 0); + +INSERT INTO account ("key", type, status, current_balance) +VALUES ('accounts.cash', 'CASH', 'OPEN', 0); + +INSERT INTO account ("key", type, status, current_balance) +VALUES ('accounts.start', 'START', 'OPEN', 0); + +INSERT INTO account ("key", type, status, current_balance) +VALUES ('accounts.convenience', 'EXPENSE', 'OPEN', 0); + +--Recurring transactions +INSERT INTO recurring_transaction (from_account_id, to_account_id, description, amount, interval_type, first_occurrence, holiday_weekend_type) +VALUES (2, 1, 'Pay', 250000, 'MONTHLY', '2019-01-15', 'NEXT_WORKDAY'); + +INSERT INTO recurring_transaction (from_account_id, to_account_id, description, amount, interval_type, first_occurrence, holiday_weekend_type) +VALUES (3, 5, 'Pretzel', 170, 'DAILY', '2019-02-20', 'SAME_DAY'); \ No newline at end of file From 449e8fb216ce6b19f809fb70848c32747a7e85ae Mon Sep 17 00:00:00 2001 From: MK13 Date: Tue, 5 Mar 2019 23:13:26 +0100 Subject: [PATCH 08/65] Add implementation to create a recurring transaction - Therefore extracted the date format into the financer config - Simplify date testing, also for plain transaction service - Add test for duePast MONTHLY SAME_DAY --- src/main/java/de/financer/ResponseReason.java | 9 +- .../de/financer/config/FinancerConfig.java | 27 +++ .../service/RecurringTransactionService.java | 178 +++++++++++++++--- .../financer/service/TransactionService.java | 35 ++-- .../resources/config/application.properties | 5 +- ...e_getAllDueToday_MONTHLY_SAME_DAYTest.java | 68 +++++++ 6 files changed, 282 insertions(+), 40 deletions(-) create mode 100644 src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_SAME_DAYTest.java diff --git a/src/main/java/de/financer/ResponseReason.java b/src/main/java/de/financer/ResponseReason.java index fd57146..e631e2b 100644 --- a/src/main/java/de/financer/ResponseReason.java +++ b/src/main/java/de/financer/ResponseReason.java @@ -15,7 +15,14 @@ public enum ResponseReason { 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); + 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); private HttpStatus httpStatus; diff --git a/src/main/java/de/financer/config/FinancerConfig.java b/src/main/java/de/financer/config/FinancerConfig.java index a3a549a..3041b2e 100644 --- a/src/main/java/de/financer/config/FinancerConfig.java +++ b/src/main/java/de/financer/config/FinancerConfig.java @@ -4,6 +4,7 @@ import de.jollyday.HolidayCalendar; 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.Optional; @@ -12,6 +13,7 @@ import java.util.Optional; public class FinancerConfig { private String countryCode; private String state; + private String dateFormat; /** * @return the raw country code, mostly an uppercase ISO 3166 2-letter code @@ -50,4 +52,29 @@ public class FinancerConfig { 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) { + // TODO log info about default dd.MM.yyyy + + dateFormat = "dd.MM.yyyy"; + } + + this.dateFormat = dateFormat; + } } diff --git a/src/main/java/de/financer/service/RecurringTransactionService.java b/src/main/java/de/financer/service/RecurringTransactionService.java index f03cf76..6bd4ce8 100644 --- a/src/main/java/de/financer/service/RecurringTransactionService.java +++ b/src/main/java/de/financer/service/RecurringTransactionService.java @@ -1,15 +1,21 @@ 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.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.stream.Collectors; @@ -25,6 +31,9 @@ public class RecurringTransactionService { @Autowired private RuleService ruleService; + @Autowired + private FinancerConfig financerConfig; + public Iterable getAll() { return this.recurringTransactionRepository.findAll(); } @@ -42,8 +51,8 @@ public class RecurringTransactionService { /** * 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}. + * 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 */ @@ -66,17 +75,16 @@ public class RecurringTransactionService { } /** - * 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. + * 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 + * @param now today's date + * * @return true if the recurring transaction is due today, false otherwise */ private boolean checkRecurringTransactionDueToday(RecurringTransaction recurringTransaction, LocalDate now) { @@ -106,21 +114,26 @@ public class RecurringTransactionService { } /** - * This method checks whether the given {@link RecurringTransaction} was actually due in the close past - * but has been deferred to maybe 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. + * This method checks whether the given {@link RecurringTransaction} was actually due in the close past but has been + * deferred to maybe 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 + * @param now today's date + * * @return true if the recurring transaction is due today, false 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())) { + return false; // early return + } + boolean weekend; boolean holiday; LocalDate yesterday = now; @@ -154,10 +167,129 @@ public class RecurringTransactionService { 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) { - return null; + String lastOccurrence + ) { + final Account fromAccount = this.accountService.getAccountByKey(fromAccountKey); + final Account toAccount = this.accountService.getAccountByKey(toAccountKey); + ResponseReason response = validateParameters(fromAccount, toAccount, amount, holidayWeekendType, intervalType, + firstOccurrence, lastOccurrence); + + // 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); + + this.recurringTransactionRepository.save(transaction); + + response = ResponseReason.OK; + } catch (Exception e) { + // TODO log + + 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 null + * + * @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 + ) { + 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()))); + 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 null + * + * @return the first error found or null 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 (holidayWeekendType == null) { + response = ResponseReason.MISSING_HOLIDAY_WEEKEND_TYPE; + } else if (!HolidayWeekendType.isValidType(holidayWeekendType)) { + response = ResponseReason.INVALID_HOLIDAY_WEEKEND_TYPE; + } else if (intervalType == null) { + response = ResponseReason.MISSING_INTERVAL_TYPE; + } else if (!IntervalType.isValidType(intervalType)) { + response = ResponseReason.INVALID_INTERVAL_TYPE; + } else if (firstOccurrence == null) { + response = ResponseReason.MISSING_FIRST_OCCURRENCE; + } + + try { + LocalDate.parse(firstOccurrence, DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat())); + } catch (DateTimeParseException e) { + response = ResponseReason.INVALID_FIRST_OCCURRENCE_FORMAT; + } + + if (lastOccurrence != null) { + try { + LocalDate.parse(lastOccurrence, DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat())); + } catch (DateTimeParseException e) { + response = ResponseReason.INVALID_LAST_OCCURRENCE_FORMAT; + } + } + + return response; } } diff --git a/src/main/java/de/financer/service/TransactionService.java b/src/main/java/de/financer/service/TransactionService.java index 7e3cdca..345c394 100644 --- a/src/main/java/de/financer/service/TransactionService.java +++ b/src/main/java/de/financer/service/TransactionService.java @@ -1,6 +1,7 @@ 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.Transaction; @@ -16,8 +17,6 @@ import java.util.Collections; @Service public class TransactionService { - public static final String DATE_FORMAT = "dd.MM.yyyy"; - @Autowired private AccountService accountService; @@ -27,6 +26,9 @@ public class TransactionService { @Autowired private TransactionRepository transactionRepository; + @Autowired + private FinancerConfig financerConfig; + /** * @return all transactions, for all accounts and all time */ @@ -75,10 +77,6 @@ public class TransactionService { this.accountService.saveAccount(toAccount); response = ResponseReason.OK; - } catch (DateTimeParseException e) { - // TODO log - - response = ResponseReason.INVALID_DATE_FORMAT; } catch (Exception e) { // TODO log @@ -92,21 +90,20 @@ public class TransactionService { * This method builds the actual transaction object with the given values. * * @param fromAccount the from account - * @param toAccount the to account - * @param amount the transaction amount + * @param toAccount the to account + * @param amount the transaction amount * @param description the description of the transaction - * @param date the date of the transaction + * @param date the date of the transaction * @return the build {@link Transaction} instance - * @throws DateTimeParseException if the given date string cannot be parsed into a {@link java.time.LocalDate} instance */ - private Transaction buildTransaction(Account fromAccount, Account toAccount, Long amount, String description, String date) throws DateTimeParseException { + private Transaction buildTransaction(Account fromAccount, Account toAccount, Long amount, String description, String date) { final Transaction transaction = new Transaction(); transaction.setFromAccount(fromAccount); transaction.setToAccount(toAccount); transaction.setAmount(amount); transaction.setDescription(description); - transaction.setDate(LocalDate.parse(date, DateTimeFormatter.ofPattern(DATE_FORMAT))); + transaction.setDate(LocalDate.parse(date, DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat()))); return transaction; } @@ -115,9 +112,9 @@ public class TransactionService { * This method checks whether the parameters for creating a transaction are valid. * * @param fromAccount the from account - * @param toAccount the to account - * @param amount the transaction amount - * @param date the transaction date + * @param toAccount the to account + * @param amount the transaction amount + * @param date the transaction date * @return the first error found or null if all parameters are valid */ private ResponseReason validateParameters(Account fromAccount, Account toAccount, Long amount, String date) { @@ -139,6 +136,14 @@ public class TransactionService { response = ResponseReason.MISSING_DATE; } + try { + LocalDate.parse(date, DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat())); + } + catch (DateTimeParseException e) { + response = ResponseReason.INVALID_DATE_FORMAT; + } + + return response; } } diff --git a/src/main/resources/config/application.properties b/src/main/resources/config/application.properties index 00c8e39..9f05903 100644 --- a/src/main/resources/config/application.properties +++ b/src/main/resources/config/application.properties @@ -21,4 +21,7 @@ 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 \ No newline at end of file +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 \ No newline at end of file diff --git a/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_SAME_DAYTest.java b/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_SAME_DAYTest.java new file mode 100644 index 0000000..f787688 --- /dev/null +++ b/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_SAME_DAYTest.java @@ -0,0 +1,68 @@ +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.findAll()) + .thenReturn(Collections.singletonList(createRecurringTransaction(-1))); + // Today is not a holiday but yesterday was + Mockito.when(this.ruleService.isHoliday(Mockito.any())).thenReturn(Boolean.FALSE, Boolean.TRUE); + final LocalDate now = LocalDate.now(); + + // Act + final Iterable 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); + + return recurringTransaction; + } +} From b6318c65d48a6891c5845767b8cc667fdc93e166 Mon Sep 17 00:00:00 2001 From: MK13 Date: Thu, 7 Mar 2019 23:48:47 +0100 Subject: [PATCH 09/65] Various stuff all over the tree again - RecurringTransactions can now be used to create Transactions - Improve JavaDoc - Add unit tests and fix various small bugs because of them - Add integration test --- src/main/java/de/financer/ResponseReason.java | 5 +- .../RecurringTransactionController.java | 5 + .../de/financer/model/HolidayWeekendType.java | 2 +- .../service/RecurringTransactionService.java | 108 +++++- .../financer/service/TransactionService.java | 17 +- ...ountControllerIntegration_getAllTest.java} | 4 +- ...ration_createRecurringTransactionTest.java | 59 +++ ...ervice_createRecurringTransactionTest.java | 346 ++++++++++++++++++ ...DueToday_MONTHLY_PREVIOUS_WORKDAYTest.java | 68 ++++ ...nsactionService_createTransactionTest.java | 28 +- .../integration/V999_99_00__testdata.sql | 3 + 11 files changed, 615 insertions(+), 30 deletions(-) rename src/test/java/de/financer/controller/integration/{AccountControllerIntegrationTest.java => AccountControllerIntegration_getAllTest.java} (94%) create mode 100644 src/test/java/de/financer/controller/integration/RecurringTransactionServiceIntegration_createRecurringTransactionTest.java create mode 100644 src/test/java/de/financer/service/RecurringTransactionService_createRecurringTransactionTest.java create mode 100644 src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_PREVIOUS_WORKDAYTest.java diff --git a/src/main/java/de/financer/ResponseReason.java b/src/main/java/de/financer/ResponseReason.java index e631e2b..8da8a29 100644 --- a/src/main/java/de/financer/ResponseReason.java +++ b/src/main/java/de/financer/ResponseReason.java @@ -22,7 +22,10 @@ public enum ResponseReason { 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); + 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); private HttpStatus httpStatus; diff --git a/src/main/java/de/financer/controller/RecurringTransactionController.java b/src/main/java/de/financer/controller/RecurringTransactionController.java index 5c4a72b..5b19f5d 100644 --- a/src/main/java/de/financer/controller/RecurringTransactionController.java +++ b/src/main/java/de/financer/controller/RecurringTransactionController.java @@ -40,4 +40,9 @@ public class RecurringTransactionController { lastOccurrence) .toResponseEntity(); } + + @RequestMapping("createTransaction") + public ResponseEntity createTransaction(String recurringTransactionId) { + return this.recurringTransactionService.createTransaction(recurringTransactionId).toResponseEntity(); + } } diff --git a/src/main/java/de/financer/model/HolidayWeekendType.java b/src/main/java/de/financer/model/HolidayWeekendType.java index 3de89b0..6c1b883 100644 --- a/src/main/java/de/financer/model/HolidayWeekendType.java +++ b/src/main/java/de/financer/model/HolidayWeekendType.java @@ -34,7 +34,7 @@ public enum HolidayWeekendType { /** *

- * Indicates that the action should be made earlier at the previous day + * Indicates that the action should preponed to the previous day *

*
      *     Example 1:
diff --git a/src/main/java/de/financer/service/RecurringTransactionService.java b/src/main/java/de/financer/service/RecurringTransactionService.java
index 6bd4ce8..53cd68d 100644
--- a/src/main/java/de/financer/service/RecurringTransactionService.java
+++ b/src/main/java/de/financer/service/RecurringTransactionService.java
@@ -8,6 +8,8 @@ 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.StringUtils;
+import org.apache.commons.lang3.math.NumberUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Propagation;
@@ -17,6 +19,7 @@ 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
@@ -34,6 +37,9 @@ public class RecurringTransactionService {
     @Autowired
     private FinancerConfig financerConfig;
 
+    @Autowired
+    private TransactionService transactionService;
+
     public Iterable getAll() {
         return this.recurringTransactionRepository.findAll();
     }
@@ -68,8 +74,8 @@ public class RecurringTransactionService {
         //@formatter:off
         return IterableUtils.toList(allRecurringTransactions).stream()
                             .filter((rt) -> checkRecurringTransactionDueToday(rt, now) ||
-                                            checkRecurringTransactionDuePast(rt, now))
-                                            // TODO checkRecurringTransactionDueFuture for HolidayWeekendType.PREVIOUS_WORKDAY
+                                            checkRecurringTransactionDuePast(rt, now) ||
+                                            checkRecurringTransactionDueFuture(rt, now))
                             .collect(Collectors.toList());
         //@formatter:on
     }
@@ -167,6 +173,59 @@ public class RecurringTransactionService {
         return due;
     }
 
+    /**
+     * This method checks whether the given {@link RecurringTransaction} will actually be due in the close future will
+     * be preponed to maybe 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 true if the recurring transaction is due today, false 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())) {
+            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);
+
+        return due;
+    }
+
     @Transactional(propagation = Propagation.REQUIRED)
     public ResponseReason createRecurringTransaction(String fromAccountKey, String toAccountKey, Long amount,
                                                      String description, String holidayWeekendType,
@@ -192,6 +251,7 @@ public class RecurringTransactionService {
             response = ResponseReason.OK;
         } catch (Exception e) {
             // TODO log
+            e.printStackTrace();
 
             response = ResponseReason.UNKNOWN_ERROR;
         }
@@ -227,8 +287,12 @@ public class RecurringTransactionService {
         recurringTransaction.setIntervalType(IntervalType.valueOf(intervalType));
         recurringTransaction.setFirstOccurrence(LocalDate
                 .parse(firstOccurrence, DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat())));
-        recurringTransaction.setLastOccurrence(LocalDate
-                .parse(lastOccurrence, DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat())));
+
+        // lastOccurrence is optional
+        if (StringUtils.isNotEmpty(lastOccurrence)) {
+            recurringTransaction.setLastOccurrence(LocalDate
+                    .parse(lastOccurrence, DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat())));
+        }
 
         return recurringTransaction;
     }
@@ -276,13 +340,15 @@ public class RecurringTransactionService {
             response = ResponseReason.MISSING_FIRST_OCCURRENCE;
         }
 
-        try {
-            LocalDate.parse(firstOccurrence, DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat()));
-        } catch (DateTimeParseException e) {
-            response = ResponseReason.INVALID_FIRST_OCCURRENCE_FORMAT;
+        if (response == null && firstOccurrence != null) {
+            try {
+                LocalDate.parse(firstOccurrence, DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat()));
+            } catch (DateTimeParseException e) {
+                response = ResponseReason.INVALID_FIRST_OCCURRENCE_FORMAT;
+            }
         }
 
-        if (lastOccurrence != null) {
+        if (response == null && lastOccurrence != null) {
             try {
                 LocalDate.parse(lastOccurrence, DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat()));
             } catch (DateTimeParseException e) {
@@ -292,4 +358,28 @@ public class RecurringTransactionService {
 
         return response;
     }
+
+    @Transactional(propagation = Propagation.REQUIRED)
+    public ResponseReason createTransaction(String recurringTransactionId) {
+        if (recurringTransactionId == null) {
+            return ResponseReason.MISSING_RECURRING_TRANSACTION_ID;
+        } else if (!NumberUtils.isCreatable(recurringTransactionId)) {
+            return ResponseReason.INVALID_RECURRING_TRANSACTION_ID;
+        }
+
+        final Optional 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(),
+                recurringTransaction.getAmount(),
+                LocalDate.now().format(DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat())),
+                recurringTransaction.getDescription());
+    }
 }
diff --git a/src/main/java/de/financer/service/TransactionService.java b/src/main/java/de/financer/service/TransactionService.java
index 345c394..7c5012e 100644
--- a/src/main/java/de/financer/service/TransactionService.java
+++ b/src/main/java/de/financer/service/TransactionService.java
@@ -38,6 +38,7 @@ public class TransactionService {
 
     /**
      * @param accountKey the key of the account to get the transactions for
+     *
      * @return all transactions for the given account, for all time. Returns an empty list if the given key does not
      * match any account.
      */
@@ -94,6 +95,7 @@ public class TransactionService {
      * @param amount the transaction amount
      * @param description the description of the transaction
      * @param date the date of the transaction
+     *
      * @return the build {@link Transaction} instance
      */
     private Transaction buildTransaction(Account fromAccount, Account toAccount, Long amount, String description, String date) {
@@ -115,6 +117,7 @@ public class TransactionService {
      * @param toAccount the to account
      * @param amount the transaction amount
      * @param date the transaction date
+     *
      * @return the first error found or null if all parameters are valid
      */
     private ResponseReason validateParameters(Account fromAccount, Account toAccount, Long amount, String date) {
@@ -134,16 +137,14 @@ public class TransactionService {
             response = ResponseReason.AMOUNT_ZERO;
         } else if (date == null) {
             response = ResponseReason.MISSING_DATE;
+        } else if (date != null) {
+            try {
+                LocalDate.parse(date, DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat()));
+            } catch (DateTimeParseException e) {
+                response = ResponseReason.INVALID_DATE_FORMAT;
+            }
         }
 
-        try {
-            LocalDate.parse(date, DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat()));
-        }
-        catch (DateTimeParseException e) {
-            response = ResponseReason.INVALID_DATE_FORMAT;
-        }
-
-
         return response;
     }
 }
diff --git a/src/test/java/de/financer/controller/integration/AccountControllerIntegrationTest.java b/src/test/java/de/financer/controller/integration/AccountControllerIntegration_getAllTest.java
similarity index 94%
rename from src/test/java/de/financer/controller/integration/AccountControllerIntegrationTest.java
rename to src/test/java/de/financer/controller/integration/AccountControllerIntegration_getAllTest.java
index 910b4c3..f34631e 100644
--- a/src/test/java/de/financer/controller/integration/AccountControllerIntegrationTest.java
+++ b/src/test/java/de/financer/controller/integration/AccountControllerIntegration_getAllTest.java
@@ -26,7 +26,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
 @AutoConfigureMockMvc
 @TestPropertySource(
         locations = "classpath:application-integrationtest.properties")
-public class AccountControllerIntegrationTest {
+public class AccountControllerIntegration_getAllTest {
 
     @Autowired
     private MockMvc mockMvc;
@@ -44,6 +44,6 @@ public class AccountControllerIntegrationTest {
         final List allAccounts = this.objectMapper
                 .readValue(mvcResult.getResponse().getContentAsByteArray(), new TypeReference>() {});
 
-        Assert.assertEquals(5, allAccounts.size());
+        Assert.assertEquals(6, allAccounts.size());
     }
 }
diff --git a/src/test/java/de/financer/controller/integration/RecurringTransactionServiceIntegration_createRecurringTransactionTest.java b/src/test/java/de/financer/controller/integration/RecurringTransactionServiceIntegration_createRecurringTransactionTest.java
new file mode 100644
index 0000000..436e07c
--- /dev/null
+++ b/src/test/java/de/financer/controller/integration/RecurringTransactionServiceIntegration_createRecurringTransactionTest.java
@@ -0,0 +1,59 @@
+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 RecurringTransactionServiceIntegration_createRecurringTransactionTest {
+    @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", "accounts.income")
+                                                            .param("toAccountKey", "accounts.checkaccount")
+                                                            .param("amount", "250000")
+                                                            .param("description", "Monthly rent")
+                                                            .param("holidayWeekendType", "SAME_DAY")
+                                                            .param("intervalType", "MONTHLY")
+                                                            .param("firstOccurrence", "07.03.2019"))
+                                                 .andExpect(status().isOk())
+                                                 .andReturn();
+
+        final MvcResult mvcResult = this.mockMvc.perform(get("/recurringTransactions/getAll")
+                                                            .contentType(MediaType.APPLICATION_JSON))
+                                                .andExpect(status().isOk())
+                                                .andReturn();
+
+        final List allRecurringTransaction = this.objectMapper
+                .readValue(mvcResult.getResponse().getContentAsByteArray(), new TypeReference>() {});
+
+        Assert.assertEquals(3, allRecurringTransaction.size());
+    }
+}
diff --git a/src/test/java/de/financer/service/RecurringTransactionService_createRecurringTransactionTest.java b/src/test/java/de/financer/service/RecurringTransactionService_createRecurringTransactionTest.java
new file mode 100644
index 0000000..c551797
--- /dev/null
+++ b/src/test/java/de/financer/service/RecurringTransactionService_createRecurringTransactionTest.java
@@ -0,0 +1,346 @@
+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");
+
+        // 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");
+
+        // 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");
+
+        // 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");
+
+        // 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");
+
+        // 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");
+
+        // 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");
+
+        // 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");
+
+        // 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");
+
+        // 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");
+
+        // 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");
+
+        // 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");
+
+        // 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");
+
+        // 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);
+
+        // 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);
+
+        // Assert
+        Assert.assertEquals(ResponseReason.OK, response);
+    }
+
+    private Account createAccount() {
+        final Account account = new Account();
+
+        account.setCurrentBalance(Long.valueOf(0l));
+
+        return account;
+    }
+}
diff --git a/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_PREVIOUS_WORKDAYTest.java b/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_PREVIOUS_WORKDAYTest.java
new file mode 100644
index 0000000..5a88e9f
--- /dev/null
+++ b/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_PREVIOUS_WORKDAYTest.java
@@ -0,0 +1,68 @@
+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.findAll())
+               .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 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.PREVIOUS_WORKDAY);
+        recurringTransaction.setIntervalType(IntervalType.MONTHLY);
+
+        return recurringTransaction;
+    }
+}
diff --git a/src/test/java/de/financer/service/TransactionService_createTransactionTest.java b/src/test/java/de/financer/service/TransactionService_createTransactionTest.java
index 5c2fd07..24ca33d 100644
--- a/src/test/java/de/financer/service/TransactionService_createTransactionTest.java
+++ b/src/test/java/de/financer/service/TransactionService_createTransactionTest.java
@@ -1,9 +1,11 @@
 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;
@@ -25,6 +27,14 @@ public class TransactionService_createTransactionTest {
     @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
@@ -44,7 +54,7 @@ public class TransactionService_createTransactionTest {
         Mockito.when(this.accountService.getAccountByKey(Mockito.anyString())).thenReturn(createAccount(), null);
 
         // Act
-        final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.invalid", Long.valueOf(150l), "24.02.2019", "XXX");
+        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);
@@ -56,7 +66,7 @@ public class TransactionService_createTransactionTest {
         Mockito.when(this.accountService.getAccountByKey(Mockito.anyString())).thenReturn(null, createAccount());
 
         // Act
-        final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.invalid", Long.valueOf(150l), "24.02.2019", "XXX");
+        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);
@@ -69,7 +79,7 @@ public class TransactionService_createTransactionTest {
         Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.FALSE);
 
         // Act
-        final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.invalid", Long.valueOf(150l), "24.02.2019", "XXX");
+        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);
@@ -82,7 +92,7 @@ public class TransactionService_createTransactionTest {
         Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.TRUE);
 
         // Act
-        final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.invalid", null, "24.02.2019", "XXX");
+        final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", null, "24.02.2019", "XXX");
 
         // Assert
         Assert.assertEquals(ResponseReason.MISSING_AMOUNT, response);
@@ -95,7 +105,7 @@ public class TransactionService_createTransactionTest {
         Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.TRUE);
 
         // Act
-        final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.invalid", Long.valueOf(0l), "24.02.2019", "XXX");
+        final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", Long.valueOf(0l), "24.02.2019", "XXX");
 
         // Assert
         Assert.assertEquals(ResponseReason.AMOUNT_ZERO, response);
@@ -108,7 +118,7 @@ public class TransactionService_createTransactionTest {
         Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.TRUE);
 
         // Act
-        final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.invalid", Long.valueOf(125l), null, "XXX");
+        final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", Long.valueOf(125l), null, "XXX");
 
         // Assert
         Assert.assertEquals(ResponseReason.MISSING_DATE, response);
@@ -121,7 +131,7 @@ public class TransactionService_createTransactionTest {
         Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.TRUE);
 
         // Act
-        final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.invalid", Long.valueOf(125l), "2019-01-01", "XXX");
+        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);
@@ -140,7 +150,7 @@ public class TransactionService_createTransactionTest {
         Mockito.when(toAccount.getCurrentBalance()).thenReturn(Long.valueOf(0l));
 
         // Act
-        final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.invalid", Long.valueOf(125l), "24.02.2019", "XXX");
+        final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", Long.valueOf(125l), "24.02.2019", "XXX");
 
         // Assert
         Assert.assertEquals(ResponseReason.OK, response);
@@ -148,7 +158,7 @@ public class TransactionService_createTransactionTest {
         Mockito.verify(toAccount, Mockito.times(1)).setCurrentBalance(Long.valueOf(125));
     }
 
-    public Account createAccount() {
+    private Account createAccount() {
         final Account account = new Account();
 
         account.setCurrentBalance(Long.valueOf(0l));
diff --git a/src/test/resources/database/hsqldb/integration/V999_99_00__testdata.sql b/src/test/resources/database/hsqldb/integration/V999_99_00__testdata.sql
index 468578d..2000b85 100644
--- a/src/test/resources/database/hsqldb/integration/V999_99_00__testdata.sql
+++ b/src/test/resources/database/hsqldb/integration/V999_99_00__testdata.sql
@@ -14,6 +14,9 @@ VALUES ('accounts.start', 'START', 'OPEN', 0);
 INSERT INTO account ("key", type, status, current_balance)
 VALUES ('accounts.convenience', 'EXPENSE', 'OPEN', 0);
 
+INSERT INTO account ("key", type, status, current_balance)
+VALUES ('accounts.rent', 'EXPENSE', 'OPEN', 0);
+
 --Recurring transactions
 INSERT INTO recurring_transaction (from_account_id, to_account_id, description, amount, interval_type, first_occurrence, holiday_weekend_type)
 VALUES (2, 1, 'Pay', 250000, 'MONTHLY', '2019-01-15', 'NEXT_WORKDAY');

From 24e9dcda357a68ae979fafb72d4e23cb1849585a Mon Sep 17 00:00:00 2001
From: MK13 
Date: Fri, 8 Mar 2019 19:37:06 +0100
Subject: [PATCH 10/65] Adjust reference from transaction to recurring
 transaction

- Make the reference nullable
- Add the reference when a transaction gets created by a recurring transaction
- Add overload method createTransaction as the reference is optional
- Fix integration tests name so they get picked up by surefire
- Add a integration test for the recurringTransaction -> transaction creation
---
 .../service/RecurringTransactionService.java  |  3 +-
 .../financer/service/TransactionService.java  | 21 +++++-
 .../database/hsqldb/V1_0_0__init.sql          |  2 +-
 ...ountController_getAllIntegrationTest.java} |  2 +-
 ...eRecurringTransactionIntegrationTest.java} |  2 +-
 ...vice_createTransactionIntegrationTest.java | 68 +++++++++++++++++++
 6 files changed, 91 insertions(+), 7 deletions(-)
 rename src/test/java/de/financer/controller/integration/{AccountControllerIntegration_getAllTest.java => AccountController_getAllIntegrationTest.java} (97%)
 rename src/test/java/de/financer/controller/integration/{RecurringTransactionServiceIntegration_createRecurringTransactionTest.java => RecurringTransactionService_createRecurringTransactionIntegrationTest.java} (97%)
 create mode 100644 src/test/java/de/financer/controller/integration/RecurringTransactionService_createTransactionIntegrationTest.java

diff --git a/src/main/java/de/financer/service/RecurringTransactionService.java b/src/main/java/de/financer/service/RecurringTransactionService.java
index 53cd68d..fd9a477 100644
--- a/src/main/java/de/financer/service/RecurringTransactionService.java
+++ b/src/main/java/de/financer/service/RecurringTransactionService.java
@@ -380,6 +380,7 @@ public class RecurringTransactionService {
                 recurringTransaction.getToAccount().getKey(),
                 recurringTransaction.getAmount(),
                 LocalDate.now().format(DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat())),
-                recurringTransaction.getDescription());
+                recurringTransaction.getDescription(),
+                recurringTransaction);
     }
 }
diff --git a/src/main/java/de/financer/service/TransactionService.java b/src/main/java/de/financer/service/TransactionService.java
index 7c5012e..ff49233 100644
--- a/src/main/java/de/financer/service/TransactionService.java
+++ b/src/main/java/de/financer/service/TransactionService.java
@@ -4,6 +4,7 @@ import de.financer.ResponseReason;
 import de.financer.config.FinancerConfig;
 import de.financer.dba.TransactionRepository;
 import de.financer.model.Account;
+import de.financer.model.RecurringTransaction;
 import de.financer.model.Transaction;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
@@ -54,7 +55,16 @@ public class TransactionService {
     }
 
     @Transactional(propagation = Propagation.REQUIRED)
-    public ResponseReason createTransaction(String fromAccountKey, String toAccountKey, Long amount, String date, String description) {
+    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);
@@ -65,7 +75,7 @@ public class TransactionService {
         }
 
         try {
-            final Transaction transaction = buildTransaction(fromAccount, toAccount, amount, description, date);
+            final Transaction transaction = buildTransaction(fromAccount, toAccount, amount, description, date, recurringTransaction);
 
             fromAccount.setCurrentBalance(fromAccount.getCurrentBalance() + (this.ruleService
                     .getMultiplierFromAccount(fromAccount) * amount));
@@ -95,10 +105,14 @@ public class TransactionService {
      * @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
+     * null
      *
      * @return the build {@link Transaction} instance
      */
-    private Transaction buildTransaction(Account fromAccount, Account toAccount, Long amount, String description, String date) {
+    private Transaction buildTransaction(Account fromAccount, Account toAccount, Long amount, String description,
+                                         String date, RecurringTransaction recurringTransaction
+    ) {
         final Transaction transaction = new Transaction();
 
         transaction.setFromAccount(fromAccount);
@@ -106,6 +120,7 @@ public class TransactionService {
         transaction.setAmount(amount);
         transaction.setDescription(description);
         transaction.setDate(LocalDate.parse(date, DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat())));
+        transaction.setRecurringTransaction(recurringTransaction);
 
         return transaction;
     }
diff --git a/src/main/resources/database/hsqldb/V1_0_0__init.sql b/src/main/resources/database/hsqldb/V1_0_0__init.sql
index 1cfbe37..88b281a 100644
--- a/src/main/resources/database/hsqldb/V1_0_0__init.sql
+++ b/src/main/resources/database/hsqldb/V1_0_0__init.sql
@@ -37,7 +37,7 @@ CREATE TABLE "transaction" ( --escape keyword "transaction"
     "date" DATE NOT NULL, --escape keyword "date"
     description VARCHAR(1000),
     amount BIGINT NOT NULL,
-    recurring_transaction_id 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),
diff --git a/src/test/java/de/financer/controller/integration/AccountControllerIntegration_getAllTest.java b/src/test/java/de/financer/controller/integration/AccountController_getAllIntegrationTest.java
similarity index 97%
rename from src/test/java/de/financer/controller/integration/AccountControllerIntegration_getAllTest.java
rename to src/test/java/de/financer/controller/integration/AccountController_getAllIntegrationTest.java
index f34631e..cc1390c 100644
--- a/src/test/java/de/financer/controller/integration/AccountControllerIntegration_getAllTest.java
+++ b/src/test/java/de/financer/controller/integration/AccountController_getAllIntegrationTest.java
@@ -26,7 +26,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
 @AutoConfigureMockMvc
 @TestPropertySource(
         locations = "classpath:application-integrationtest.properties")
-public class AccountControllerIntegration_getAllTest {
+public class AccountController_getAllIntegrationTest {
 
     @Autowired
     private MockMvc mockMvc;
diff --git a/src/test/java/de/financer/controller/integration/RecurringTransactionServiceIntegration_createRecurringTransactionTest.java b/src/test/java/de/financer/controller/integration/RecurringTransactionService_createRecurringTransactionIntegrationTest.java
similarity index 97%
rename from src/test/java/de/financer/controller/integration/RecurringTransactionServiceIntegration_createRecurringTransactionTest.java
rename to src/test/java/de/financer/controller/integration/RecurringTransactionService_createRecurringTransactionIntegrationTest.java
index 436e07c..4cb8119 100644
--- a/src/test/java/de/financer/controller/integration/RecurringTransactionServiceIntegration_createRecurringTransactionTest.java
+++ b/src/test/java/de/financer/controller/integration/RecurringTransactionService_createRecurringTransactionIntegrationTest.java
@@ -26,7 +26,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
 @AutoConfigureMockMvc
 @TestPropertySource(
         locations = "classpath:application-integrationtest.properties")
-public class RecurringTransactionServiceIntegration_createRecurringTransactionTest {
+public class RecurringTransactionService_createRecurringTransactionIntegrationTest {
     @Autowired
     private MockMvc mockMvc;
 
diff --git a/src/test/java/de/financer/controller/integration/RecurringTransactionService_createTransactionIntegrationTest.java b/src/test/java/de/financer/controller/integration/RecurringTransactionService_createTransactionIntegrationTest.java
new file mode 100644
index 0000000..c19d396
--- /dev/null
+++ b/src/test/java/de/financer/controller/integration/RecurringTransactionService_createTransactionIntegrationTest.java
@@ -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 allRecurringTransactions = this.objectMapper
+                .readValue(mvcResultAll.getResponse().getContentAsByteArray(), new TypeReference>() {});
+        final Optional 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 allTransactions = this.objectMapper
+                .readValue(mvcResultAllTransactions.getResponse().getContentAsByteArray(), new TypeReference>() {});
+
+        Assert.assertEquals(1, allTransactions.size());
+    }
+}

From 297d7a80fdf4eeae9216148ddfffa0b86d5f2553 Mon Sep 17 00:00:00 2001
From: MK13 
Date: Sun, 10 Mar 2019 23:13:55 +0100
Subject: [PATCH 11/65] Add basic mail reminder for recurring transactions

Also add some basic accounts to the DB init. Further add dependency to JAXB as it was missing.
Add an optional parameter amount to the recurringTransactions/createTransaction method that can be used to overwrite the amount of the recurring transaction.
---
 pom.xml                                       |  5 ++
 .../java/de/financer/FinancerApplication.java |  2 +
 .../de/financer/config/FinancerConfig.java    | 25 +++++++
 .../RecurringTransactionController.java       |  6 +-
 .../service/RecurringTransactionService.java  |  4 +-
 .../SendRecurringTransactionReminderTask.java | 69 ++++++++++++++++++
 .../resources/config/application.properties   | 13 +++-
 .../database/hsqldb/V1_0_0__init.sql          | 20 +++++-
 ...dRecurringTransactionReminderTaskTest.java | 72 +++++++++++++++++++
 .../integration/V999_99_00__testdata.sql      | 15 ----
 10 files changed, 209 insertions(+), 22 deletions(-)
 create mode 100644 src/main/java/de/financer/task/SendRecurringTransactionReminderTask.java
 create mode 100644 src/test/java/de/financer/task/SendRecurringTransactionReminderTaskTest.java

diff --git a/pom.xml b/pom.xml
index 8fb3e96..790b37d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -58,6 +58,11 @@
             jollyday
             0.5.7
         
+        
+        
+            org.glassfish.jaxb
+            jaxb-runtime
+        
 
         
         
diff --git a/src/main/java/de/financer/FinancerApplication.java b/src/main/java/de/financer/FinancerApplication.java
index e3cace7..670d579 100644
--- a/src/main/java/de/financer/FinancerApplication.java
+++ b/src/main/java/de/financer/FinancerApplication.java
@@ -2,8 +2,10 @@ package de.financer;
 
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableScheduling;
 
 @SpringBootApplication
+@EnableScheduling
 public class FinancerApplication {
     public static void main(String[] args) {
         SpringApplication.run(FinancerApplication.class);
diff --git a/src/main/java/de/financer/config/FinancerConfig.java b/src/main/java/de/financer/config/FinancerConfig.java
index 3041b2e..f08e8a1 100644
--- a/src/main/java/de/financer/config/FinancerConfig.java
+++ b/src/main/java/de/financer/config/FinancerConfig.java
@@ -6,6 +6,7 @@ import org.springframework.context.annotation.Configuration;
 
 import java.time.format.DateTimeFormatter;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Optional;
 
 @Configuration
@@ -14,6 +15,8 @@ public class FinancerConfig {
     private String countryCode;
     private String state;
     private String dateFormat;
+    private Collection mailRecipients;
+    private String fromAddress;
 
     /**
      * @return the raw country code, mostly an uppercase ISO 3166 2-letter code
@@ -77,4 +80,26 @@ public class FinancerConfig {
 
         this.dateFormat = dateFormat;
     }
+
+    /**
+     * @return a collection of email addresses that should receive mails from financer
+     */
+    public Collection getMailRecipients() {
+        return mailRecipients;
+    }
+
+    public void setMailRecipients(Collection 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;
+    }
 }
diff --git a/src/main/java/de/financer/controller/RecurringTransactionController.java b/src/main/java/de/financer/controller/RecurringTransactionController.java
index 5b19f5d..792ea56 100644
--- a/src/main/java/de/financer/controller/RecurringTransactionController.java
+++ b/src/main/java/de/financer/controller/RecurringTransactionController.java
@@ -7,6 +7,8 @@ import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
+import java.util.Optional;
+
 @RestController
 @RequestMapping("recurringTransactions")
 public class RecurringTransactionController {
@@ -42,7 +44,7 @@ public class RecurringTransactionController {
     }
 
     @RequestMapping("createTransaction")
-    public ResponseEntity createTransaction(String recurringTransactionId) {
-        return this.recurringTransactionService.createTransaction(recurringTransactionId).toResponseEntity();
+    public ResponseEntity createTransaction(String recurringTransactionId, Long amount) {
+        return this.recurringTransactionService.createTransaction(recurringTransactionId, Optional.of(amount)).toResponseEntity();
     }
 }
diff --git a/src/main/java/de/financer/service/RecurringTransactionService.java b/src/main/java/de/financer/service/RecurringTransactionService.java
index fd9a477..1e6b00e 100644
--- a/src/main/java/de/financer/service/RecurringTransactionService.java
+++ b/src/main/java/de/financer/service/RecurringTransactionService.java
@@ -360,7 +360,7 @@ public class RecurringTransactionService {
     }
 
     @Transactional(propagation = Propagation.REQUIRED)
-    public ResponseReason createTransaction(String recurringTransactionId) {
+    public ResponseReason createTransaction(String recurringTransactionId, Optional amount) {
         if (recurringTransactionId == null) {
             return ResponseReason.MISSING_RECURRING_TRANSACTION_ID;
         } else if (!NumberUtils.isCreatable(recurringTransactionId)) {
@@ -378,7 +378,7 @@ public class RecurringTransactionService {
 
         return this.transactionService.createTransaction(recurringTransaction.getFromAccount().getKey(),
                 recurringTransaction.getToAccount().getKey(),
-                recurringTransaction.getAmount(),
+                amount.orElseGet(() -> recurringTransaction.getAmount()),
                 LocalDate.now().format(DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat())),
                 recurringTransaction.getDescription(),
                 recurringTransaction);
diff --git a/src/main/java/de/financer/task/SendRecurringTransactionReminderTask.java b/src/main/java/de/financer/task/SendRecurringTransactionReminderTask.java
new file mode 100644
index 0000000..124379b
--- /dev/null
+++ b/src/main/java/de/financer/task/SendRecurringTransactionReminderTask.java
@@ -0,0 +1,69 @@
+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.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;
+
+@Component
+public class SendRecurringTransactionReminderTask {
+
+    @Autowired
+    private RecurringTransactionService recurringTransactionService;
+
+    @Autowired
+    private FinancerConfig financerConfig;
+
+    @Autowired
+    private JavaMailSender mailSender;
+
+    @Scheduled(cron = "0 30 0 * * *")
+    public void sendReminder() {
+        final Iterable recurringTransactions = this.recurringTransactionService.getAllDueToday();
+
+        // If no recurring transaction is due today we don't need to send a reminder
+        if (IterableUtils.isEmpty(recurringTransactions)) {
+            return; // early return
+        }
+
+        final StringBuilder reminderBuilder = new StringBuilder();
+
+        reminderBuilder.append("The following recurring transactions are due today:")
+                       .append(System.lineSeparator())
+                       .append(System.lineSeparator());
+
+        IterableUtils.toList(recurringTransactions).stream().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) {
+            // TODO log
+        }
+    }
+}
diff --git a/src/main/resources/config/application.properties b/src/main/resources/config/application.properties
index 9f05903..c4701ea 100644
--- a/src/main/resources/config/application.properties
+++ b/src/main/resources/config/application.properties
@@ -24,4 +24,15 @@ financer.countryCode=DE
 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
\ No newline at end of file
+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=
diff --git a/src/main/resources/database/hsqldb/V1_0_0__init.sql b/src/main/resources/database/hsqldb/V1_0_0__init.sql
index 88b281a..a8cca05 100644
--- a/src/main/resources/database/hsqldb/V1_0_0__init.sql
+++ b/src/main/resources/database/hsqldb/V1_0_0__init.sql
@@ -1,5 +1,5 @@
 --
--- This file contains the basic initialization of the financer schema
+-- This file contains the basic initialization of the financer schema and basic init data
 --
 
 -- Account table
@@ -42,4 +42,20 @@ CREATE TABLE "transaction" ( --escape keyword "transaction"
     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)
-);
\ No newline at end of file
+);
+
+-- Accounts
+INSERT INTO account (id, "key", type, status, current_balance)
+VALUES (1, 'accounts.checkaccount', 'BANK', 'OPEN', 0); -- insert first with ID 1 so we get predictable numbering
+
+INSERT INTO account ("key", type, status, current_balance)
+VALUES ('accounts.income', 'INCOME', 'OPEN', 0);
+
+INSERT INTO account ("key", type, status, current_balance)
+VALUES ('accounts.cash', 'CASH', 'OPEN', 0);
+
+INSERT INTO account ("key", type, status, current_balance)
+VALUES ('accounts.start', 'START', 'OPEN', 0);
+
+INSERT INTO account ("key", type, status, current_balance)
+VALUES ('accounts.rent', 'EXPENSE', 'OPEN', 0);
\ No newline at end of file
diff --git a/src/test/java/de/financer/task/SendRecurringTransactionReminderTaskTest.java b/src/test/java/de/financer/task/SendRecurringTransactionReminderTaskTest.java
new file mode 100644
index 0000000..7af9de9
--- /dev/null
+++ b/src/test/java/de/financer/task/SendRecurringTransactionReminderTaskTest.java
@@ -0,0 +1,72 @@
+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 recurringTransactions = Arrays.asList(
+          createRecurringTransaction("Test booking 1", "accounts.income", "accounts.bank", Long.valueOf(250000)),
+          createRecurringTransaction("Test booking 2", "accounts.bank", "accounts.rent", Long.valueOf(41500)),
+          createRecurringTransaction("Test booking 3", "accounts.bank", "accounts.cash", Long.valueOf(5000))
+        );
+
+        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) {
+        final RecurringTransaction recurringTransaction = new RecurringTransaction();
+
+        recurringTransaction.setDescription(description);
+        recurringTransaction.setFromAccount(createAccount(fromAccountKey));
+        recurringTransaction.setToAccount(createAccount(toAccountKey));
+        recurringTransaction.setAmount(amount);
+
+        return recurringTransaction;
+    }
+
+    private Account createAccount(String key) {
+        final Account account = new Account();
+
+        account.setKey(key);
+
+        return account;
+    }
+}
diff --git a/src/test/resources/database/hsqldb/integration/V999_99_00__testdata.sql b/src/test/resources/database/hsqldb/integration/V999_99_00__testdata.sql
index 2000b85..18c37f1 100644
--- a/src/test/resources/database/hsqldb/integration/V999_99_00__testdata.sql
+++ b/src/test/resources/database/hsqldb/integration/V999_99_00__testdata.sql
@@ -1,22 +1,7 @@
 -- Accounts
-INSERT INTO account (id, "key", type, status, current_balance)
-VALUES (1, 'accounts.checkaccount', 'BANK', 'OPEN', 0); -- insert first with ID 1 so we get predictable numbering
-
-INSERT INTO account ("key", type, status, current_balance)
-VALUES ('accounts.income', 'INCOME', 'OPEN', 0);
-
-INSERT INTO account ("key", type, status, current_balance)
-VALUES ('accounts.cash', 'CASH', 'OPEN', 0);
-
-INSERT INTO account ("key", type, status, current_balance)
-VALUES ('accounts.start', 'START', 'OPEN', 0);
-
 INSERT INTO account ("key", type, status, current_balance)
 VALUES ('accounts.convenience', 'EXPENSE', 'OPEN', 0);
 
-INSERT INTO account ("key", type, status, current_balance)
-VALUES ('accounts.rent', 'EXPENSE', 'OPEN', 0);
-
 --Recurring transactions
 INSERT INTO recurring_transaction (from_account_id, to_account_id, description, amount, interval_type, first_occurrence, holiday_weekend_type)
 VALUES (2, 1, 'Pay', 250000, 'MONTHLY', '2019-01-15', 'NEXT_WORKDAY');

From a0884e1f21cc68cac07f17fdf30fd486622a7dad Mon Sep 17 00:00:00 2001
From: MK13 
Date: Mon, 11 Mar 2019 20:32:43 +0100
Subject: [PATCH 12/65] Fix NPE in
 RecurringTransactionController.createTransaction

As the amount parameter is optional it may be null - use the appropriate
Optional method ofNullable()
---
 .../de/financer/controller/RecurringTransactionController.java | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/main/java/de/financer/controller/RecurringTransactionController.java b/src/main/java/de/financer/controller/RecurringTransactionController.java
index 792ea56..62feb32 100644
--- a/src/main/java/de/financer/controller/RecurringTransactionController.java
+++ b/src/main/java/de/financer/controller/RecurringTransactionController.java
@@ -45,6 +45,7 @@ public class RecurringTransactionController {
 
     @RequestMapping("createTransaction")
     public ResponseEntity createTransaction(String recurringTransactionId, Long amount) {
-        return this.recurringTransactionService.createTransaction(recurringTransactionId, Optional.of(amount)).toResponseEntity();
+        return this.recurringTransactionService.createTransaction(recurringTransactionId, Optional.ofNullable(amount))
+                                               .toResponseEntity();
     }
 }

From a27d26596b8a692a8e6cd5af1f3abc4c83a4962a Mon Sep 17 00:00:00 2001
From: MK13 
Date: Mon, 11 Mar 2019 21:25:22 +0100
Subject: [PATCH 13/65] Add basic logging

Add basic logging of exceptions, warnings, info and debug statements.
The implementation used is Logback.
---
 .../de/financer/config/FinancerConfig.java    | 31 ++++++-----
 .../controller/AccountController.java         | 17 +++++-
 .../RecurringTransactionController.java       | 48 ++++++++++++++---
 .../controller/TransactionController.java     | 24 +++++++--
 .../de/financer/service/AccountService.java   |  7 ++-
 .../service/RecurringTransactionService.java  |  8 ++-
 .../java/de/financer/service/RuleService.java | 52 +++++++++++++------
 .../financer/service/TransactionService.java  |  8 ++-
 .../SendRecurringTransactionReminderTask.java | 19 ++++++-
 .../resources/config/application.properties   |  2 +
 10 files changed, 170 insertions(+), 46 deletions(-)

diff --git a/src/main/java/de/financer/config/FinancerConfig.java b/src/main/java/de/financer/config/FinancerConfig.java
index f08e8a1..a9371cd 100644
--- a/src/main/java/de/financer/config/FinancerConfig.java
+++ b/src/main/java/de/financer/config/FinancerConfig.java
@@ -1,6 +1,8 @@
 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;
 
@@ -12,6 +14,8 @@ 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;
@@ -33,8 +37,8 @@ public class FinancerConfig {
     }
 
     /**
-     * @return the {@link HolidayCalendar} used to calculate the holidays. Internally uses the country code
-     * specified via {@link FinancerConfig#getCountryCode}.
+     * @return the {@link HolidayCalendar} used to calculate the holidays. Internally uses the country code specified
+     * via {@link FinancerConfig#getCountryCode}.
      */
     public HolidayCalendar getHolidayCalendar() {
         final Optional optionalHoliday = Arrays.asList(HolidayCalendar.values()).stream()
@@ -42,7 +46,10 @@ public class FinancerConfig {
                                                                 .findFirst();
 
         if (!optionalHoliday.isPresent()) {
-            // TODO log info about default DE
+            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);
@@ -57,12 +64,11 @@ public class FinancerConfig {
     }
 
     /**
-     * @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
+     * @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;
@@ -71,9 +77,10 @@ public class FinancerConfig {
     public void setDateFormat(String dateFormat) {
         try {
             DateTimeFormatter.ofPattern(dateFormat);
-        }
-        catch (IllegalArgumentException e) {
-            // TODO log info about default dd.MM.yyyy
+        } 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";
         }
diff --git a/src/main/java/de/financer/controller/AccountController.java b/src/main/java/de/financer/controller/AccountController.java
index 144efaa..202aeef 100644
--- a/src/main/java/de/financer/controller/AccountController.java
+++ b/src/main/java/de/financer/controller/AccountController.java
@@ -1,7 +1,10 @@
 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;
@@ -11,6 +14,8 @@ import org.springframework.web.bind.annotation.RestController;
 @RequestMapping("accounts")
 public class AccountController {
 
+    private static final Logger LOGGER = LoggerFactory.getLogger(AccountController.class);
+
     @Autowired
     private AccountService accountService;
 
@@ -36,6 +41,16 @@ public class AccountController {
 
     @RequestMapping("createAccount")
     public ResponseEntity createAccount(String key, String type) {
-        return this.accountService.createAccount(key, type).toResponseEntity();
+        if (LOGGER.isDebugEnabled()) {
+            LOGGER.debug(String.format("/accounts/createAccount got parameters: %s, %s", key, type));
+        }
+
+        final ResponseReason responseReason = this.accountService.createAccount(key, type);
+
+        if (LOGGER.isDebugEnabled()) {
+            LOGGER.debug(String.format("/accounts/createAccount returns with %s", responseReason.name()));
+        }
+
+        return responseReason.toResponseEntity();
     }
 }
diff --git a/src/main/java/de/financer/controller/RecurringTransactionController.java b/src/main/java/de/financer/controller/RecurringTransactionController.java
index 62feb32..cd78932 100644
--- a/src/main/java/de/financer/controller/RecurringTransactionController.java
+++ b/src/main/java/de/financer/controller/RecurringTransactionController.java
@@ -1,7 +1,10 @@
 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;
@@ -13,6 +16,8 @@ import java.util.Optional;
 @RequestMapping("recurringTransactions")
 public class RecurringTransactionController {
 
+    private static final Logger LOGGER = LoggerFactory.getLogger(RecurringTransactionController.class);
+
     @Autowired
     private RecurringTransactionService recurringTransactionService;
 
@@ -35,17 +40,44 @@ public class RecurringTransactionController {
     public ResponseEntity createRecurringTransaction(String fromAccountKey, String toAccountKey, Long amount,
                                                      String description, String holidayWeekendType,
                                                      String intervalType, String firstOccurrence,
-                                                     String lastOccurrence) {
-        return this.recurringTransactionService.createRecurringTransaction(fromAccountKey, toAccountKey, amount,
-                                                                           description, holidayWeekendType,
-                                                                           intervalType, firstOccurrence,
-                                                                           lastOccurrence)
-                                               .toResponseEntity();
+                                                     String lastOccurrence
+    ) {
+        if (LOGGER.isDebugEnabled()) {
+            LOGGER.debug(String
+                    .format("/recurringTransactions/createRecurringTransaction got parameters: %s, %s, %s, %s, %s, " +
+                                    "%s, %s, %s", fromAccountKey, toAccountKey, amount, description, holidayWeekendType,
+                            intervalType, firstOccurrence, lastOccurrence));
+        }
+
+        final ResponseReason responseReason = this.recurringTransactionService
+                .createRecurringTransaction(fromAccountKey, toAccountKey, amount, description, holidayWeekendType,
+                        intervalType, firstOccurrence, lastOccurrence);
+
+        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) {
-        return this.recurringTransactionService.createTransaction(recurringTransactionId, Optional.ofNullable(amount))
-                                               .toResponseEntity();
+        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();
     }
 }
diff --git a/src/main/java/de/financer/controller/TransactionController.java b/src/main/java/de/financer/controller/TransactionController.java
index bb14c44..8ddbb58 100644
--- a/src/main/java/de/financer/controller/TransactionController.java
+++ b/src/main/java/de/financer/controller/TransactionController.java
@@ -1,7 +1,10 @@
 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;
@@ -10,6 +13,8 @@ import org.springframework.web.bind.annotation.RestController;
 @RestController
 @RequestMapping("transactions")
 public class TransactionController {
+    private static final Logger LOGGER = LoggerFactory.getLogger(TransactionController.class);
+
     @Autowired
     private TransactionService transactionService;
 
@@ -25,8 +30,21 @@ public class TransactionController {
 
     @RequestMapping(value = "createTransaction")
     public ResponseEntity createTransaction(String fromAccountKey, String toAccountKey, Long amount, String date,
-                                            String description) {
-        return this.transactionService.createTransaction(fromAccountKey, toAccountKey, amount, date, description)
-                                      .toResponseEntity();
+                                            String description
+    ) {
+        if (LOGGER.isDebugEnabled()) {
+            LOGGER.debug(String
+                    .format("/transactions/createTransaction got parameters: %s, %s, %s, %s, %s",
+                            fromAccountKey, toAccountKey, amount, date, description));
+        }
+
+        final ResponseReason responseReason = this.transactionService
+                .createTransaction(fromAccountKey, toAccountKey, amount, date, description);
+
+        if (LOGGER.isDebugEnabled()) {
+            LOGGER.debug(String.format("/transactions/createTransaction returns with %s", responseReason.name()));
+        }
+
+        return responseReason.toResponseEntity();
     }
 }
diff --git a/src/main/java/de/financer/service/AccountService.java b/src/main/java/de/financer/service/AccountService.java
index 694988e..7f05956 100644
--- a/src/main/java/de/financer/service/AccountService.java
+++ b/src/main/java/de/financer/service/AccountService.java
@@ -6,6 +6,8 @@ import de.financer.model.Account;
 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.stereotype.Service;
 import org.springframework.transaction.annotation.Propagation;
@@ -16,6 +18,8 @@ import java.util.stream.Collectors;
 
 @Service
 public class AccountService {
+    private static final Logger LOGGER = LoggerFactory.getLogger(AccountService.class);
+
     @Autowired
     private AccountRepository accountRepository;
 
@@ -95,7 +99,8 @@ public class AccountService {
             this.accountRepository.save(account);
         }
         catch (Exception e) {
-            // TODO log and check for unique constraint exception so we can return a more specific error
+            LOGGER.error(String.format("Could not save account %s|%s", key, type), e);
+
             return ResponseReason.UNKNOWN_ERROR;
         }
 
diff --git a/src/main/java/de/financer/service/RecurringTransactionService.java b/src/main/java/de/financer/service/RecurringTransactionService.java
index 1e6b00e..6eb27ce 100644
--- a/src/main/java/de/financer/service/RecurringTransactionService.java
+++ b/src/main/java/de/financer/service/RecurringTransactionService.java
@@ -10,6 +10,8 @@ import de.financer.model.RecurringTransaction;
 import org.apache.commons.collections4.IterableUtils;
 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;
@@ -24,6 +26,7 @@ import java.util.stream.Collectors;
 
 @Service
 public class RecurringTransactionService {
+    private static final Logger LOGGER = LoggerFactory.getLogger(RecurringTransactionService.class);
 
     @Autowired
     private RecurringTransactionRepository recurringTransactionRepository;
@@ -48,6 +51,8 @@ public class RecurringTransactionService {
         final Account account = this.accountService.getAccountByKey(accountKey);
 
         if (account == null) {
+            LOGGER.warn(String.format("Account with key %s not found!", accountKey));
+
             return Collections.emptyList();
         }
 
@@ -250,8 +255,7 @@ public class RecurringTransactionService {
 
             response = ResponseReason.OK;
         } catch (Exception e) {
-            // TODO log
-            e.printStackTrace();
+            LOGGER.error("Could not create recurring transaction!", e);
 
             response = ResponseReason.UNKNOWN_ERROR;
         }
diff --git a/src/main/java/de/financer/service/RuleService.java b/src/main/java/de/financer/service/RuleService.java
index 7dee141..35436bc 100644
--- a/src/main/java/de/financer/service/RuleService.java
+++ b/src/main/java/de/financer/service/RuleService.java
@@ -6,6 +6,8 @@ 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;
@@ -18,13 +20,12 @@ 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.
+ * 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;
@@ -65,10 +66,11 @@ public class RuleService implements InitializingBean {
     /**
      * This method returns the multiplier for the given from account.
      * 

- * 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. + * 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 1 or -1 */ public long getMultiplierFromAccount(Account fromAccount) { @@ -89,16 +91,20 @@ public class RuleService implements InitializingBean { 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. *

- * 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. + * 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 1 or -1 */ public long getMultiplierToAccount(Account toAccount) { @@ -117,27 +123,33 @@ public class RuleService implements InitializingBean { 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 fromAccount to toAccount - * 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. + * This method validates whether the booking from fromAccount to toAccount 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 true if the from->to relationship of the given accounts is valid, false otherwise + * @param toAccount the account to add the money to + * + * @return true if the from->to relationship of the given accounts is valid, false + * 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}. + * 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) { @@ -148,18 +160,24 @@ public class RuleService implements InitializingBean { * This method checks whether the given date is a holiday in the configured country and state. * * @param now the date to check + * * @return true if the given date is a holiday, false 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}. + * 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 true if the given date is a weekend day, false otherwise */ public boolean isWeekend(LocalDate now) { diff --git a/src/main/java/de/financer/service/TransactionService.java b/src/main/java/de/financer/service/TransactionService.java index ff49233..84ad9aa 100644 --- a/src/main/java/de/financer/service/TransactionService.java +++ b/src/main/java/de/financer/service/TransactionService.java @@ -6,6 +6,8 @@ import de.financer.dba.TransactionRepository; import de.financer.model.Account; import de.financer.model.RecurringTransaction; import de.financer.model.Transaction; +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; @@ -18,6 +20,8 @@ import java.util.Collections; @Service public class TransactionService { + private static final Logger LOGGER = LoggerFactory.getLogger(TransactionService.class); + @Autowired private AccountService accountService; @@ -47,6 +51,8 @@ public class TransactionService { final Account account = this.accountService.getAccountByKey(accountKey); if (account == null) { + LOGGER.warn(String.format("Account with key %s not found!", accountKey)); + return Collections.emptyList(); } @@ -89,7 +95,7 @@ public class TransactionService { response = ResponseReason.OK; } catch (Exception e) { - // TODO log + LOGGER.error("Could not create transaction!", e); response = ResponseReason.UNKNOWN_ERROR; } diff --git a/src/main/java/de/financer/task/SendRecurringTransactionReminderTask.java b/src/main/java/de/financer/task/SendRecurringTransactionReminderTask.java index 124379b..4e003d8 100644 --- a/src/main/java/de/financer/task/SendRecurringTransactionReminderTask.java +++ b/src/main/java/de/financer/task/SendRecurringTransactionReminderTask.java @@ -4,6 +4,8 @@ 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; @@ -14,6 +16,8 @@ import org.springframework.stereotype.Component; @Component public class SendRecurringTransactionReminderTask { + private static final Logger LOGGER = LoggerFactory.getLogger(SendRecurringTransactionReminderTask.class); + @Autowired private RecurringTransactionService recurringTransactionService; @@ -25,13 +29,23 @@ public class SendRecurringTransactionReminderTask { @Scheduled(cron = "0 30 0 * * *") public void sendReminder() { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Enter recurring transaction reminder task"); + } + final Iterable 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 } + 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:") @@ -63,7 +77,10 @@ public class SendRecurringTransactionReminderTask { try { this.mailSender.send(msg); } catch (MailException e) { - // TODO log + LOGGER.error("Could not send recurring transaction email reminder!", e); + + LOGGER.info("Dumb email reminder content because the sending failed"); + LOGGER.info(reminderBuilder.toString()); } } } diff --git a/src/main/resources/config/application.properties b/src/main/resources/config/application.properties index c4701ea..cfaaa4c 100644 --- a/src/main/resources/config/application.properties +++ b/src/main/resources/config/application.properties @@ -14,6 +14,8 @@ info.build.group=@project.groupId@ info.build.artifact=@project.artifactId@ info.build.version=@project.version@ +logging.level.de.financer=DEBUG + # 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 From 0d83ea75b2b9589c4a94f091205c4c4678aa5be4 Mon Sep 17 00:00:00 2001 From: MK13 Date: Mon, 11 Mar 2019 21:58:33 +0100 Subject: [PATCH 14/65] Add build-war maven profile The profile builds a .war file with no embedded tomcat, so it can be deployed on a standalone tomcat instance --- pom.xml | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 790b37d..54c048d 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ de.77zzcx7 financer 1.0-SNAPSHOT - jar + ${packaging.type} financer @@ -23,6 +23,7 @@ 1.9 1.9 1.9 + jar @@ -58,7 +59,6 @@ jollyday 0.5.7 - org.glassfish.jaxb jaxb-runtime @@ -154,5 +154,24 @@ + + + build-war + + war + + + + org.springframework.boot + spring-boot-starter-tomcat + provided + + + org.glassfish.jaxb + jaxb-runtime + provided + + + From 07a454e2270b7bb71b22dc3471fac9b273da5940 Mon Sep 17 00:00:00 2001 From: MK13 Date: Mon, 11 Mar 2019 22:11:24 +0100 Subject: [PATCH 15/65] Various cleanups according to code check --- src/main/java/de/financer/ResponseReason.java | 2 +- src/main/java/de/financer/config/FinancerConfig.java | 2 +- src/main/java/de/financer/model/AccountStatus.java | 2 +- src/main/java/de/financer/model/AccountType.java | 2 +- src/main/java/de/financer/model/HolidayWeekendType.java | 2 +- src/main/java/de/financer/model/IntervalType.java | 2 +- src/main/java/de/financer/service/AccountService.java | 6 +++--- .../de/financer/service/RecurringTransactionService.java | 4 ++-- src/main/java/de/financer/service/TransactionService.java | 2 +- .../financer/task/SendRecurringTransactionReminderTask.java | 2 +- 10 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main/java/de/financer/ResponseReason.java b/src/main/java/de/financer/ResponseReason.java index 8da8a29..35d12e4 100644 --- a/src/main/java/de/financer/ResponseReason.java +++ b/src/main/java/de/financer/ResponseReason.java @@ -34,6 +34,6 @@ public enum ResponseReason { } public ResponseEntity toResponseEntity() { - return new ResponseEntity(this.name(), this.httpStatus); + return new ResponseEntity<>(this.name(), this.httpStatus); } } diff --git a/src/main/java/de/financer/config/FinancerConfig.java b/src/main/java/de/financer/config/FinancerConfig.java index a9371cd..3e8a5f6 100644 --- a/src/main/java/de/financer/config/FinancerConfig.java +++ b/src/main/java/de/financer/config/FinancerConfig.java @@ -41,7 +41,7 @@ public class FinancerConfig { * via {@link FinancerConfig#getCountryCode}. */ public HolidayCalendar getHolidayCalendar() { - final Optional optionalHoliday = Arrays.asList(HolidayCalendar.values()).stream() + final Optional optionalHoliday = Arrays.stream(HolidayCalendar.values()) .filter((hc) -> hc.getId().equals(this.countryCode)) .findFirst(); diff --git a/src/main/java/de/financer/model/AccountStatus.java b/src/main/java/de/financer/model/AccountStatus.java index 60ced16..a60151f 100644 --- a/src/main/java/de/financer/model/AccountStatus.java +++ b/src/main/java/de/financer/model/AccountStatus.java @@ -4,5 +4,5 @@ public enum AccountStatus { /** Indicates that the account is open for bookings */ OPEN, /** Indicates that the account is closed and bookings to it are forbidden */ - CLOSED; + CLOSED } diff --git a/src/main/java/de/financer/model/AccountType.java b/src/main/java/de/financer/model/AccountType.java index b4e20d2..007146b 100644 --- a/src/main/java/de/financer/model/AccountType.java +++ b/src/main/java/de/financer/model/AccountType.java @@ -28,6 +28,6 @@ public enum AccountType { * @return whether the given type represents a valid account type */ public static boolean isValidType(String type) { - return Arrays.asList(AccountType.values()).stream().anyMatch((accountType) -> accountType.name().equals(type)); + return Arrays.stream(AccountType.values()).anyMatch((accountType) -> accountType.name().equals(type)); } } diff --git a/src/main/java/de/financer/model/HolidayWeekendType.java b/src/main/java/de/financer/model/HolidayWeekendType.java index 6c1b883..b25cbdb 100644 --- a/src/main/java/de/financer/model/HolidayWeekendType.java +++ b/src/main/java/de/financer/model/HolidayWeekendType.java @@ -60,6 +60,6 @@ public enum HolidayWeekendType { * @return whether the given type represents a valid holiday weekend type */ public static boolean isValidType(String type) { - return Arrays.asList(HolidayWeekendType.values()).stream().anyMatch((holidayWeekendType) -> holidayWeekendType.name().equals(type)); + return Arrays.stream(HolidayWeekendType.values()).anyMatch((holidayWeekendType) -> holidayWeekendType.name().equals(type)); } } diff --git a/src/main/java/de/financer/model/IntervalType.java b/src/main/java/de/financer/model/IntervalType.java index 2d64948..952f9b8 100644 --- a/src/main/java/de/financer/model/IntervalType.java +++ b/src/main/java/de/financer/model/IntervalType.java @@ -25,6 +25,6 @@ public enum IntervalType { * @return whether the given type represents a valid interval type */ public static boolean isValidType(String type) { - return Arrays.asList(IntervalType.values()).stream().anyMatch((intervalType) -> intervalType.name().equals(type)); + return Arrays.stream(IntervalType.values()).anyMatch((intervalType) -> intervalType.name().equals(type)); } } diff --git a/src/main/java/de/financer/service/AccountService.java b/src/main/java/de/financer/service/AccountService.java index 7f05956..511dec0 100644 --- a/src/main/java/de/financer/service/AccountService.java +++ b/src/main/java/de/financer/service/AccountService.java @@ -44,14 +44,14 @@ public class AccountService { * @return all possible account types as specified by the {@link AccountType} enumeration, never null */ public Iterable getAccountTypes() { - return Arrays.asList(AccountType.values()).stream().map(AccountType::name).collect(Collectors.toList()); + return Arrays.stream(AccountType.values()).map(AccountType::name).collect(Collectors.toList()); } /** * @return all possible account status as specified by the {@link AccountStatus} enumeration, never null */ public Iterable getAccountStatus() { - return Arrays.asList(AccountStatus.values()).stream().map(AccountStatus::name).collect(Collectors.toList()); + return Arrays.stream(AccountStatus.values()).map(AccountStatus::name).collect(Collectors.toList()); } /** @@ -93,7 +93,7 @@ public class AccountService { // 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)); + account.setCurrentBalance(Long.valueOf(0L)); try { this.accountRepository.save(account); diff --git a/src/main/java/de/financer/service/RecurringTransactionService.java b/src/main/java/de/financer/service/RecurringTransactionService.java index 6eb27ce..4daf71a 100644 --- a/src/main/java/de/financer/service/RecurringTransactionService.java +++ b/src/main/java/de/financer/service/RecurringTransactionService.java @@ -330,7 +330,7 @@ public class RecurringTransactionService { response = ResponseReason.INVALID_BOOKING_ACCOUNTS; } else if (amount == null) { response = ResponseReason.MISSING_AMOUNT; - } else if (amount == 0l) { + } else if (amount == 0L) { response = ResponseReason.AMOUNT_ZERO; } else if (holidayWeekendType == null) { response = ResponseReason.MISSING_HOLIDAY_WEEKEND_TYPE; @@ -382,7 +382,7 @@ public class RecurringTransactionService { return this.transactionService.createTransaction(recurringTransaction.getFromAccount().getKey(), recurringTransaction.getToAccount().getKey(), - amount.orElseGet(() -> recurringTransaction.getAmount()), + amount.orElseGet(recurringTransaction::getAmount), LocalDate.now().format(DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat())), recurringTransaction.getDescription(), recurringTransaction); diff --git a/src/main/java/de/financer/service/TransactionService.java b/src/main/java/de/financer/service/TransactionService.java index 84ad9aa..6e8865a 100644 --- a/src/main/java/de/financer/service/TransactionService.java +++ b/src/main/java/de/financer/service/TransactionService.java @@ -154,7 +154,7 @@ public class TransactionService { response = ResponseReason.INVALID_BOOKING_ACCOUNTS; } else if (amount == null) { response = ResponseReason.MISSING_AMOUNT; - } else if (amount == 0l) { + } else if (amount == 0L) { response = ResponseReason.AMOUNT_ZERO; } else if (date == null) { response = ResponseReason.MISSING_DATE; diff --git a/src/main/java/de/financer/task/SendRecurringTransactionReminderTask.java b/src/main/java/de/financer/task/SendRecurringTransactionReminderTask.java index 4e003d8..f534e7c 100644 --- a/src/main/java/de/financer/task/SendRecurringTransactionReminderTask.java +++ b/src/main/java/de/financer/task/SendRecurringTransactionReminderTask.java @@ -52,7 +52,7 @@ public class SendRecurringTransactionReminderTask { .append(System.lineSeparator()) .append(System.lineSeparator()); - IterableUtils.toList(recurringTransactions).stream().forEach((rt) -> { + IterableUtils.toList(recurringTransactions).forEach((rt) -> { reminderBuilder.append(rt.getId()) .append("|") .append(rt.getDescription()) From bae50081c2cca81644706d8fda737ae859509985 Mon Sep 17 00:00:00 2001 From: MK13 Date: Tue, 12 Mar 2019 22:28:01 +0100 Subject: [PATCH 16/65] Make financer deployable on a standalone servlet container --- src/main/java/de/financer/FinancerApplication.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/financer/FinancerApplication.java b/src/main/java/de/financer/FinancerApplication.java index 670d579..e084a5c 100644 --- a/src/main/java/de/financer/FinancerApplication.java +++ b/src/main/java/de/financer/FinancerApplication.java @@ -2,12 +2,19 @@ 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 { +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); + } } From a8d4d6682c43fe4b90fdbd16e19679fb174499c3 Mon Sep 17 00:00:00 2001 From: MK13 Date: Tue, 12 Mar 2019 22:36:09 +0100 Subject: [PATCH 17/65] Enable file logging and add .gitignore file --- .gitignore | 2 ++ src/main/resources/config/application.properties | 1 + 2 files changed, 3 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1d934d0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +financer.log +.attach* \ No newline at end of file diff --git a/src/main/resources/config/application.properties b/src/main/resources/config/application.properties index cfaaa4c..3c0c7cc 100644 --- a/src/main/resources/config/application.properties +++ b/src/main/resources/config/application.properties @@ -15,6 +15,7 @@ info.build.artifact=@project.artifactId@ info.build.version=@project.version@ logging.level.de.financer=DEBUG +logging.file=financer.log # Country code for holiday checks # Mostly an uppercase ISO 3166 2-letter code From ab7fb1525498b50547423aa2522f6f196acfa083 Mon Sep 17 00:00:00 2001 From: MK13 Date: Wed, 13 Mar 2019 22:43:21 +0100 Subject: [PATCH 18/65] Add integration test for RecurringTransactionController.getAll() Also adjust .gitignore to ignore financer.log* because of rotation --- .gitignore | 2 +- ...nsactionService_getAllIntegrationTest.java | 49 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 src/test/java/de/financer/controller/integration/RecurringTransactionService_getAllIntegrationTest.java diff --git a/.gitignore b/.gitignore index 1d934d0..51d50b8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -financer.log +financer.log* .attach* \ No newline at end of file diff --git a/src/test/java/de/financer/controller/integration/RecurringTransactionService_getAllIntegrationTest.java b/src/test/java/de/financer/controller/integration/RecurringTransactionService_getAllIntegrationTest.java new file mode 100644 index 0000000..021ad2e --- /dev/null +++ b/src/test/java/de/financer/controller/integration/RecurringTransactionService_getAllIntegrationTest.java @@ -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 allRecurringTransactions = this.objectMapper + .readValue(mvcResult.getResponse().getContentAsByteArray(), new TypeReference>() {}); + + Assert.assertEquals(2, allRecurringTransactions.size()); + } + +} From e9774b3b35a024647bdff120350f69dff34f8390 Mon Sep 17 00:00:00 2001 From: MK13 Date: Fri, 15 Mar 2019 20:24:47 +0100 Subject: [PATCH 19/65] Add various delete methods Add delete methods for transactions and recurring transactions. Also add open/close methods for accounts. Add unit tests for all new methods. Also JAXB is no longer a provided dependency for build-war. --- pom.xml | 5 - src/main/java/de/financer/ResponseReason.java | 6 +- .../controller/AccountController.java | 30 ++++ .../RecurringTransactionController.java | 19 +++ .../controller/TransactionController.java | 19 +++ .../java/de/financer/model/AccountStatus.java | 14 +- .../de/financer/service/AccountService.java | 36 +++++ .../service/RecurringTransactionService.java | 29 ++++ .../financer/service/TransactionService.java | 46 +++++++ ...nsactionService_getAllIntegrationTest.java | 2 +- .../AccountService_createAccountTest.java | 72 ++++++++++ .../AccountService_setAccountStatusTest.java | 74 ++++++++++ ...ervice_deleteRecurringTransactionTest.java | 94 +++++++++++++ ...nsactionService_deleteTransactionTest.java | 129 ++++++++++++++++++ 14 files changed, 567 insertions(+), 8 deletions(-) create mode 100644 src/test/java/de/financer/service/AccountService_createAccountTest.java create mode 100644 src/test/java/de/financer/service/AccountService_setAccountStatusTest.java create mode 100644 src/test/java/de/financer/service/RecurringTransactionService_deleteRecurringTransactionTest.java create mode 100644 src/test/java/de/financer/service/TransactionService_deleteTransactionTest.java diff --git a/pom.xml b/pom.xml index 54c048d..e38c2cd 100644 --- a/pom.xml +++ b/pom.xml @@ -166,11 +166,6 @@ spring-boot-starter-tomcat provided - - org.glassfish.jaxb - jaxb-runtime - provided - diff --git a/src/main/java/de/financer/ResponseReason.java b/src/main/java/de/financer/ResponseReason.java index 35d12e4..d531686 100644 --- a/src/main/java/de/financer/ResponseReason.java +++ b/src/main/java/de/financer/ResponseReason.java @@ -25,7 +25,11 @@ public enum ResponseReason { INVALID_LAST_OCCURRENCE_FORMAT(HttpStatus.INTERNAL_SERVER_ERROR), MISSING_RECURRING_TRANSACTION_ID(HttpStatus.INTERNAL_SERVER_ERROR), INVALID_RECURRING_TRANSACTION_ID(HttpStatus.INTERNAL_SERVER_ERROR), - RECURRING_TRANSACTION_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR); + RECURRING_TRANSACTION_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR), + MISSING_TRANSACTION_ID(HttpStatus.INTERNAL_SERVER_ERROR), + INVALID_TRANSACTION_ID(HttpStatus.INTERNAL_SERVER_ERROR), + TRANSACTION_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR), + ACCOUNT_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR); private HttpStatus httpStatus; diff --git a/src/main/java/de/financer/controller/AccountController.java b/src/main/java/de/financer/controller/AccountController.java index 202aeef..22103d2 100644 --- a/src/main/java/de/financer/controller/AccountController.java +++ b/src/main/java/de/financer/controller/AccountController.java @@ -53,4 +53,34 @@ public class AccountController { return responseReason.toResponseEntity(); } + + @RequestMapping("closeAccount") + public ResponseEntity closeAccount(String key) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug(String.format("/accounts/closeAccount got parameters: %s", key)); + } + + final ResponseReason responseReason = this.accountService.closeAccount(key); + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug(String.format("/accounts/closeAccount returns with %s", responseReason.name())); + } + + return responseReason.toResponseEntity(); + } + + @RequestMapping("openAccount") + public ResponseEntity openAccount(String key) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug(String.format("/accounts/openAccount got parameters: %s", key)); + } + + final ResponseReason responseReason = this.accountService.openAccount(key); + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug(String.format("/accounts/openAccount returns with %s", responseReason.name())); + } + + return responseReason.toResponseEntity(); + } } diff --git a/src/main/java/de/financer/controller/RecurringTransactionController.java b/src/main/java/de/financer/controller/RecurringTransactionController.java index cd78932..c63a27f 100644 --- a/src/main/java/de/financer/controller/RecurringTransactionController.java +++ b/src/main/java/de/financer/controller/RecurringTransactionController.java @@ -80,4 +80,23 @@ public class RecurringTransactionController { return responseReason.toResponseEntity(); } + + @RequestMapping("deleteRecurringTransaction") + public ResponseEntity deleteRecurringTransaction(String recurringTransactionId) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug(String + .format("/recurringTransactions/deleteRecurringTransaction got parameters: %s", + recurringTransactionId)); + } + + final ResponseReason responseReason = this.recurringTransactionService + .deleteRecurringTransaction(recurringTransactionId); + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug(String + .format("/recurringTransactions/deleteRecurringTransaction returns with %s", responseReason.name())); + } + + return responseReason.toResponseEntity(); + } } diff --git a/src/main/java/de/financer/controller/TransactionController.java b/src/main/java/de/financer/controller/TransactionController.java index 8ddbb58..276a3f4 100644 --- a/src/main/java/de/financer/controller/TransactionController.java +++ b/src/main/java/de/financer/controller/TransactionController.java @@ -47,4 +47,23 @@ public class TransactionController { return responseReason.toResponseEntity(); } + + @RequestMapping("deleteTransaction") + public ResponseEntity deleteTransaction(String transactionId) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug(String + .format("/transactions/deleteTransaction got parameters: %s", + transactionId)); + } + + final ResponseReason responseReason = this.transactionService + .deleteTransaction(transactionId); + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug(String + .format("/transactions/deleteTransaction returns with %s", responseReason.name())); + } + + return responseReason.toResponseEntity(); + } } diff --git a/src/main/java/de/financer/model/AccountStatus.java b/src/main/java/de/financer/model/AccountStatus.java index a60151f..9298e31 100644 --- a/src/main/java/de/financer/model/AccountStatus.java +++ b/src/main/java/de/financer/model/AccountStatus.java @@ -1,8 +1,20 @@ package de.financer.model; +import java.util.Arrays; + public enum AccountStatus { /** Indicates that the account is open for bookings */ OPEN, /** Indicates that the account is closed and bookings to it are forbidden */ - CLOSED + CLOSED; + + /** + * This method validates whether the given string represents a valid account status. + * + * @param status to check + * @return whether the given status represents a valid account status + */ + public static boolean isValidType(String status) { + return Arrays.stream(AccountStatus.values()).anyMatch((accountStatus) -> accountStatus.name().equals(status)); + } } diff --git a/src/main/java/de/financer/service/AccountService.java b/src/main/java/de/financer/service/AccountService.java index 511dec0..58864c3 100644 --- a/src/main/java/de/financer/service/AccountService.java +++ b/src/main/java/de/financer/service/AccountService.java @@ -106,4 +106,40 @@ public class AccountService { return ResponseReason.OK; } + + @Transactional(propagation = Propagation.REQUIRED) + public ResponseReason closeAccount(String key) { + return setAccountStatus(key, AccountStatus.CLOSED); + } + + @Transactional(propagation = Propagation.REQUIRED) + public ResponseReason openAccount(String key) { + return setAccountStatus(key, AccountStatus.OPEN); + } + + // Visible for unit tests + /* package */ ResponseReason setAccountStatus(String key, AccountStatus accountStatus) { + if (!StringUtils.startsWith(key, "accounts.")) { + return ResponseReason.INVALID_ACCOUNT_KEY; + } + + final Account account = this.accountRepository.findByKey(key); + + if (account == null) { + return ResponseReason.ACCOUNT_NOT_FOUND; + } + + account.setStatus(accountStatus); + + try { + this.accountRepository.save(account); + } + catch (Exception e) { + LOGGER.error(String.format("Could not update account status %s|%s", key, accountStatus.name()), e); + + return ResponseReason.UNKNOWN_ERROR; + } + + return ResponseReason.OK; + } } diff --git a/src/main/java/de/financer/service/RecurringTransactionService.java b/src/main/java/de/financer/service/RecurringTransactionService.java index 4daf71a..4d35cce 100644 --- a/src/main/java/de/financer/service/RecurringTransactionService.java +++ b/src/main/java/de/financer/service/RecurringTransactionService.java @@ -387,4 +387,33 @@ public class RecurringTransactionService { recurringTransaction.getDescription(), recurringTransaction); } + + @Transactional(propagation = Propagation.REQUIRED) + public ResponseReason deleteRecurringTransaction(String recurringTransactionId) { + ResponseReason response = ResponseReason.OK; + + if (recurringTransactionId == null) { + return ResponseReason.MISSING_RECURRING_TRANSACTION_ID; + } else if (!NumberUtils.isCreatable(recurringTransactionId)) { + return ResponseReason.INVALID_RECURRING_TRANSACTION_ID; + } + + final Optional optionalRecurringTransaction = this.recurringTransactionRepository + .findById(Long.valueOf(recurringTransactionId)); + + if (!optionalRecurringTransaction.isPresent()) { + return ResponseReason.RECURRING_TRANSACTION_NOT_FOUND; + } + + try { + this.recurringTransactionRepository.deleteById(Long.valueOf(recurringTransactionId)); + } + catch (Exception e) { + LOGGER.error("Could not delete recurring transaction!", e); + + response = ResponseReason.UNKNOWN_ERROR; + } + + return response; + } } diff --git a/src/main/java/de/financer/service/TransactionService.java b/src/main/java/de/financer/service/TransactionService.java index 6e8865a..70da78a 100644 --- a/src/main/java/de/financer/service/TransactionService.java +++ b/src/main/java/de/financer/service/TransactionService.java @@ -6,6 +6,7 @@ import de.financer.dba.TransactionRepository; import de.financer.model.Account; import de.financer.model.RecurringTransaction; import de.financer.model.Transaction; +import org.apache.commons.lang3.math.NumberUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -17,6 +18,7 @@ import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.Collections; +import java.util.Optional; @Service public class TransactionService { @@ -168,4 +170,48 @@ public class TransactionService { return response; } + + @Transactional(propagation = Propagation.REQUIRED) + public ResponseReason deleteTransaction(String transactionId) { + ResponseReason response = ResponseReason.OK; + + if (transactionId == null) { + return ResponseReason.MISSING_TRANSACTION_ID; + } else if (!NumberUtils.isCreatable(transactionId)) { + return ResponseReason.INVALID_TRANSACTION_ID; + } + + final Optional 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; + } } diff --git a/src/test/java/de/financer/controller/integration/RecurringTransactionService_getAllIntegrationTest.java b/src/test/java/de/financer/controller/integration/RecurringTransactionService_getAllIntegrationTest.java index 021ad2e..984d673 100644 --- a/src/test/java/de/financer/controller/integration/RecurringTransactionService_getAllIntegrationTest.java +++ b/src/test/java/de/financer/controller/integration/RecurringTransactionService_getAllIntegrationTest.java @@ -43,7 +43,7 @@ public class RecurringTransactionService_getAllIntegrationTest { final List allRecurringTransactions = this.objectMapper .readValue(mvcResult.getResponse().getContentAsByteArray(), new TypeReference>() {}); - Assert.assertEquals(2, allRecurringTransactions.size()); + Assert.assertEquals(3, allRecurringTransactions.size()); } } diff --git a/src/test/java/de/financer/service/AccountService_createAccountTest.java b/src/test/java/de/financer/service/AccountService_createAccountTest.java new file mode 100644 index 0000000..38daec8 --- /dev/null +++ b/src/test/java/de/financer/service/AccountService_createAccountTest.java @@ -0,0 +1,72 @@ +package de.financer.service; + +import de.financer.ResponseReason; +import de.financer.dba.AccountRepository; +import de.financer.model.Account; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentMatchers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class AccountService_createAccountTest { + @InjectMocks + private AccountService classUnderTest; + + @Mock + private AccountRepository accountRepository; + + @Test + public void test_createAccount_INVALID_ACCOUNT_TYPE() { + // Arrange + // Nothing to do + + // Act + ResponseReason response = this.classUnderTest.createAccount(null, null); + + // Assert + Assert.assertEquals(ResponseReason.INVALID_ACCOUNT_TYPE, response); + } + + @Test + public void test_createAccount_INVALID_ACCOUNT_KEY() { + // Arrange + // Nothing to do + + // Act + ResponseReason response = this.classUnderTest.createAccount(null, "BANK"); + + // Assert + Assert.assertEquals(ResponseReason.INVALID_ACCOUNT_KEY, response); + } + + @Test + public void test_createAccount_UNKNOWN_ERROR() { + // Arrange + Mockito.doThrow(new NullPointerException()).when(this.accountRepository).save(Mockito.any(Account.class)); + + // Act + ResponseReason response = this.classUnderTest.createAccount("accounts.test", "BANK"); + + // Assert + Assert.assertEquals(ResponseReason.UNKNOWN_ERROR, response); + } + + @Test + public void test_createAccount_OK() { + // Arrange + // Nothing to do + + // Act + ResponseReason response = this.classUnderTest.createAccount("accounts.test", "BANK"); + + // Assert + Assert.assertEquals(ResponseReason.OK, response); + Mockito.verify(this.accountRepository, Mockito.times(1)) + .save(ArgumentMatchers.argThat((acc) -> "accounts.test".equals(acc.getKey()))); + } +} diff --git a/src/test/java/de/financer/service/AccountService_setAccountStatusTest.java b/src/test/java/de/financer/service/AccountService_setAccountStatusTest.java new file mode 100644 index 0000000..880fff8 --- /dev/null +++ b/src/test/java/de/financer/service/AccountService_setAccountStatusTest.java @@ -0,0 +1,74 @@ +package de.financer.service; + +import de.financer.ResponseReason; +import de.financer.dba.AccountRepository; +import de.financer.model.Account; +import de.financer.model.AccountStatus; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentMatchers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class AccountService_setAccountStatusTest { + @InjectMocks + private AccountService classUnderTest; + + @Mock + private AccountRepository accountRepository; + + @Test + public void test_setAccountStatus_INVALID_ACCOUNT_KEY() { + // Arrange + // Nothing to do + + // Act + ResponseReason response = this.classUnderTest.setAccountStatus(null, AccountStatus.CLOSED); + + // Assert + Assert.assertEquals(ResponseReason.INVALID_ACCOUNT_KEY, response); + } + + @Test + public void test_setAccountStatus_ACCOUNT_NOT_FOUND() { + // Arrange + // Nothing to do + + // Act + ResponseReason response = this.classUnderTest.setAccountStatus("accounts.test", AccountStatus.CLOSED); + + // Assert + Assert.assertEquals(ResponseReason.ACCOUNT_NOT_FOUND, response); + } + + @Test + public void test_setAccountStatus_UNKNOWN_ERROR() { + // Arrange + Mockito.when(this.accountRepository.findByKey(Mockito.anyString())).thenReturn(new Account()); + Mockito.doThrow(new NullPointerException()).when(this.accountRepository).save(Mockito.any(Account.class)); + + // Act + ResponseReason response = this.classUnderTest.setAccountStatus("accounts.test", AccountStatus.CLOSED); + + // Assert + Assert.assertEquals(ResponseReason.UNKNOWN_ERROR, response); + } + + @Test + public void test_setAccountStatus_OK() { + // Arrange + Mockito.when(this.accountRepository.findByKey(Mockito.anyString())).thenReturn(new Account()); + + // Act + ResponseReason response = this.classUnderTest.setAccountStatus("accounts.test", AccountStatus.CLOSED); + + // Assert + Assert.assertEquals(ResponseReason.OK, response); + Mockito.verify(this.accountRepository, Mockito.times(1)) + .save(ArgumentMatchers.argThat((acc) -> AccountStatus.CLOSED.equals(acc.getStatus()))); + } +} diff --git a/src/test/java/de/financer/service/RecurringTransactionService_deleteRecurringTransactionTest.java b/src/test/java/de/financer/service/RecurringTransactionService_deleteRecurringTransactionTest.java new file mode 100644 index 0000000..635d1a9 --- /dev/null +++ b/src/test/java/de/financer/service/RecurringTransactionService_deleteRecurringTransactionTest.java @@ -0,0 +1,94 @@ +package de.financer.service; + +import de.financer.ResponseReason; +import de.financer.config.FinancerConfig; +import de.financer.dba.RecurringTransactionRepository; +import de.financer.model.RecurringTransaction; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Optional; + +@RunWith(MockitoJUnitRunner.class) +public class RecurringTransactionService_deleteRecurringTransactionTest { + @InjectMocks + private RecurringTransactionService classUnderTest; + + @Mock + private AccountService accountService; + + @Mock + private RuleService ruleService; + + @Mock + private RecurringTransactionRepository recurringTransactionRepository; + + @Mock + private FinancerConfig financerConfig; + + @Test + public void test_deleteRecurringTransaction_MISSING_RECURRING_TRANSACTION_ID() { + // Arrange + // Nothing to do + + // Act + final ResponseReason response = this.classUnderTest.deleteRecurringTransaction(null); + + // Assert + Assert.assertEquals(ResponseReason.MISSING_RECURRING_TRANSACTION_ID, response); + } + + @Test + public void test_deleteRecurringTransaction_INVALID_RECURRING_TRANSACTION_ID() { + // Arrange + // Nothing to do + + // Act + final ResponseReason response = this.classUnderTest.deleteRecurringTransaction("invalid"); + + // Assert + Assert.assertEquals(ResponseReason.INVALID_RECURRING_TRANSACTION_ID, response); + } + + @Test + public void test_deleteRecurringTransaction_RECURRING_TRANSACTION_NOT_FOUND() { + // Arrange + Mockito.when(this.recurringTransactionRepository.findById(Mockito.anyLong())).thenReturn(Optional.empty()); + + // Act + final ResponseReason response = this.classUnderTest.deleteRecurringTransaction("123"); + + // Assert + Assert.assertEquals(ResponseReason.RECURRING_TRANSACTION_NOT_FOUND, response); + } + + @Test + public void test_deleteRecurringTransaction_UNKNOWN_ERROR() { + // Arrange + Mockito.when(this.recurringTransactionRepository.findById(Mockito.anyLong())).thenReturn(Optional.of(new RecurringTransaction())); + Mockito.doThrow(new NullPointerException()).when(this.recurringTransactionRepository).deleteById(Mockito.anyLong()); + + // Act + final ResponseReason response = this.classUnderTest.deleteRecurringTransaction("123"); + + // Assert + Assert.assertEquals(ResponseReason.UNKNOWN_ERROR, response); + } + + @Test + public void test_deleteRecurringTransaction_OK() { + // Arrange + Mockito.when(this.recurringTransactionRepository.findById(Mockito.anyLong())).thenReturn(Optional.of(new RecurringTransaction())); + + // Act + final ResponseReason response = this.classUnderTest.deleteRecurringTransaction("123"); + + // Assert + Assert.assertEquals(ResponseReason.OK, response); + } +} diff --git a/src/test/java/de/financer/service/TransactionService_deleteTransactionTest.java b/src/test/java/de/financer/service/TransactionService_deleteTransactionTest.java new file mode 100644 index 0000000..f6febd5 --- /dev/null +++ b/src/test/java/de/financer/service/TransactionService_deleteTransactionTest.java @@ -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; + } +} From aa92718e747b4316049c437bda641c0f6de72957 Mon Sep 17 00:00:00 2001 From: MK13 Date: Sat, 16 Mar 2019 00:04:13 +0100 Subject: [PATCH 20/65] Add PostgreSQL support Therefore add migration scripts and default connection settings. Also add a draft chapter in the documentation about how to setup the database . Make HSQLDB the default for integration tests. --- doc/README | 19 +++++++- .../config/application-hsqldb.properties | 2 +- .../config/application-postgres.properties | 8 +++- .../database/common/V1_0_1__initData.sql | 15 +++++++ .../database/hsqldb/V1_0_0__init.sql | 18 +------- .../database/postgres/V1_0_0__init.sql | 45 +++++++++++++++++++ .../application-integrationtest.properties | 4 +- 7 files changed, 90 insertions(+), 21 deletions(-) create mode 100644 src/main/resources/database/common/V1_0_1__initData.sql create mode 100644 src/main/resources/database/postgres/V1_0_0__init.sql diff --git a/doc/README b/doc/README index c04e884..83330b0 100644 --- a/doc/README +++ b/doc/README @@ -10,4 +10,21 @@ 3. Overview 4. Architectural overview 5. Account types - 6. Booking rules \ No newline at end of file + 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 --interactive + Enter 'financer' as role name and make it superuser. Then create the actual database: + createdb financer + Naming both, the user and the database, 'financer' is the expected default. If you want any other names you need + to adjust the database connection settings of the financer application. \ No newline at end of file diff --git a/src/main/resources/config/application-hsqldb.properties b/src/main/resources/config/application-hsqldb.properties index e2f0f22..3976d92 100644 --- a/src/main/resources/config/application-hsqldb.properties +++ b/src/main/resources/config/application-hsqldb.properties @@ -1,4 +1,4 @@ -spring.flyway.locations=classpath:/database/hsqldb +spring.flyway.locations=classpath:/database/hsqldb,classpath:/database/common # DataSource #spring.datasource.url=jdbc:hsqldb:file:/tmp/financer diff --git a/src/main/resources/config/application-postgres.properties b/src/main/resources/config/application-postgres.properties index 944068b..7082fd1 100644 --- a/src/main/resources/config/application-postgres.properties +++ b/src/main/resources/config/application-postgres.properties @@ -1 +1,7 @@ -spring.flyway.locations=classpath:/database/postgres \ No newline at end of file +spring.flyway.locations=classpath:/database/postgres,classpath:/database/common + +spring.datasource.url=jdbc:postgresql://localhost/financer +spring.datasource.username=financer + +# See https://github.com/spring-projects/spring-boot/issues/12007 +spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true \ No newline at end of file diff --git a/src/main/resources/database/common/V1_0_1__initData.sql b/src/main/resources/database/common/V1_0_1__initData.sql new file mode 100644 index 0000000..084e8fc --- /dev/null +++ b/src/main/resources/database/common/V1_0_1__initData.sql @@ -0,0 +1,15 @@ +-- 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); \ No newline at end of file diff --git a/src/main/resources/database/hsqldb/V1_0_0__init.sql b/src/main/resources/database/hsqldb/V1_0_0__init.sql index a8cca05..0737352 100644 --- a/src/main/resources/database/hsqldb/V1_0_0__init.sql +++ b/src/main/resources/database/hsqldb/V1_0_0__init.sql @@ -42,20 +42,4 @@ CREATE TABLE "transaction" ( --escape keyword "transaction" 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) -); - --- Accounts -INSERT INTO account (id, "key", type, status, current_balance) -VALUES (1, 'accounts.checkaccount', 'BANK', 'OPEN', 0); -- insert first with ID 1 so we get predictable numbering - -INSERT INTO account ("key", type, status, current_balance) -VALUES ('accounts.income', 'INCOME', 'OPEN', 0); - -INSERT INTO account ("key", type, status, current_balance) -VALUES ('accounts.cash', 'CASH', 'OPEN', 0); - -INSERT INTO account ("key", type, status, current_balance) -VALUES ('accounts.start', 'START', 'OPEN', 0); - -INSERT INTO account ("key", type, status, current_balance) -VALUES ('accounts.rent', 'EXPENSE', 'OPEN', 0); \ No newline at end of file +); \ No newline at end of file diff --git a/src/main/resources/database/postgres/V1_0_0__init.sql b/src/main/resources/database/postgres/V1_0_0__init.sql new file mode 100644 index 0000000..52158af --- /dev/null +++ b/src/main/resources/database/postgres/V1_0_0__init.sql @@ -0,0 +1,45 @@ +-- +-- 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, + + 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) +); \ No newline at end of file diff --git a/src/test/resources/application-integrationtest.properties b/src/test/resources/application-integrationtest.properties index b75a1d2..d6974b3 100644 --- a/src/test/resources/application-integrationtest.properties +++ b/src/test/resources/application-integrationtest.properties @@ -1,3 +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 +spring.flyway.locations=classpath:/database/hsqldb,classpath:/database/hsqldb/integration,classpath:/database/common From 57643f5b5b623267546b80cdf8a3d79aee855f06 Mon Sep 17 00:00:00 2001 From: MK13 Date: Sat, 16 Mar 2019 22:27:02 +0100 Subject: [PATCH 21/65] Various stuff build-war maven profile now builds a tomcat parallel deployment compatible war file. Fix test data and integrations tests. Add a bunch of accounts to the init data. --- pom.xml | 5 +- .../database/common/V1_0_1__initData.sql | 53 ++++++++++++++++++- ...countController_getAllIntegrationTest.java | 2 +- .../integration/V999_99_00__testdata.sql | 4 +- 4 files changed, 59 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index e38c2cd..0f05163 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ de.77zzcx7 financer - 1.0-SNAPSHOT + 1-SNAPSHOT ${packaging.type} financer @@ -160,6 +160,9 @@ war + + financer##${project.version} + org.springframework.boot diff --git a/src/main/resources/database/common/V1_0_1__initData.sql b/src/main/resources/database/common/V1_0_1__initData.sql index 084e8fc..e675048 100644 --- a/src/main/resources/database/common/V1_0_1__initData.sql +++ b/src/main/resources/database/common/V1_0_1__initData.sql @@ -12,4 +12,55 @@ 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); \ No newline at end of file +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); \ No newline at end of file diff --git a/src/test/java/de/financer/controller/integration/AccountController_getAllIntegrationTest.java b/src/test/java/de/financer/controller/integration/AccountController_getAllIntegrationTest.java index cc1390c..082d37f 100644 --- a/src/test/java/de/financer/controller/integration/AccountController_getAllIntegrationTest.java +++ b/src/test/java/de/financer/controller/integration/AccountController_getAllIntegrationTest.java @@ -44,6 +44,6 @@ public class AccountController_getAllIntegrationTest { final List allAccounts = this.objectMapper .readValue(mvcResult.getResponse().getContentAsByteArray(), new TypeReference>() {}); - Assert.assertEquals(6, allAccounts.size()); + Assert.assertEquals(23, allAccounts.size()); } } diff --git a/src/test/resources/database/hsqldb/integration/V999_99_00__testdata.sql b/src/test/resources/database/hsqldb/integration/V999_99_00__testdata.sql index 18c37f1..797e1a2 100644 --- a/src/test/resources/database/hsqldb/integration/V999_99_00__testdata.sql +++ b/src/test/resources/database/hsqldb/integration/V999_99_00__testdata.sql @@ -4,7 +4,7 @@ VALUES ('accounts.convenience', 'EXPENSE', 'OPEN', 0); --Recurring transactions INSERT INTO recurring_transaction (from_account_id, to_account_id, description, amount, interval_type, first_occurrence, holiday_weekend_type) -VALUES (2, 1, 'Pay', 250000, 'MONTHLY', '2019-01-15', 'NEXT_WORKDAY'); +VALUES ((SELECT ID FROM account WHERE "key" = 'accounts.income'), (SELECT ID FROM account WHERE "key" = 'accounts.checkaccount'), 'Pay', 250000, 'MONTHLY', '2019-01-15', 'NEXT_WORKDAY'); INSERT INTO recurring_transaction (from_account_id, to_account_id, description, amount, interval_type, first_occurrence, holiday_weekend_type) -VALUES (3, 5, 'Pretzel', 170, 'DAILY', '2019-02-20', 'SAME_DAY'); \ No newline at end of file +VALUES ((SELECT ID FROM account WHERE "key" = 'accounts.cash'), (SELECT ID FROM account WHERE "key" = 'accounts.convenience'), 'Pretzel', 170, 'DAILY', '2019-02-20', 'SAME_DAY'); \ No newline at end of file From 44af29c7a75b0ebcb1add5d5a613f1584c2b34e3 Mon Sep 17 00:00:00 2001 From: MK13 Date: Sat, 16 Mar 2019 22:52:13 +0100 Subject: [PATCH 22/65] Select active Spring profile based on Maven profile build-war uses the Spring postgres profile, default is hsqldb and dev --- pom.xml | 2 ++ src/main/resources/config/application.properties | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 0f05163..2771336 100644 --- a/pom.xml +++ b/pom.xml @@ -24,6 +24,7 @@ 1.9 1.9 jar + hsqldb,dev @@ -159,6 +160,7 @@ build-war war + postgres financer##${project.version} diff --git a/src/main/resources/config/application.properties b/src/main/resources/config/application.properties index 3c0c7cc..b44c9c2 100644 --- a/src/main/resources/config/application.properties +++ b/src/main/resources/config/application.properties @@ -1,8 +1,9 @@ ### -### This is the main configuration file of the application -### +### 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=hsqldb,dev +spring.profiles.active=@activeProfiles@ server.servlet.context-path=/financer From 0468094f3c122dae45a386a651c8b4bcf73866c4 Mon Sep 17 00:00:00 2001 From: MK13 Date: Sun, 17 Mar 2019 15:33:43 +0100 Subject: [PATCH 23/65] Rename from financer to financer-server In fact this is the server part of the financer app, so the new name is more fitting. Also adjust the group ID accordingly, so -server and -client can reside in the same group. Also add property parallelDeploymentVersion as the String version comparison of the context version is a bit quirky on Tomcat. --- pom.xml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 2771336..dbca90c 100644 --- a/pom.xml +++ b/pom.xml @@ -11,12 +11,12 @@ - de.77zzcx7 - financer + de.77zzcx7.financer + financer-server 1-SNAPSHOT ${packaging.type} - - financer + The server part of the financer application - a simple app to manage your personal finances + financer-server UTF-8 @@ -25,6 +25,10 @@ 1.9 jar hsqldb,dev + + 000001 @@ -97,7 +101,7 @@ - financer + ${project.artifactId} org.springframework.boot @@ -163,7 +167,7 @@ postgres - financer##${project.version} + ${project.artifactId}##${parallelDeploymentVersion} From d6572e26f1e77b805f05d6d19e137b827f8a3d9b Mon Sep 17 00:00:00 2001 From: MK13 Date: Sun, 17 Mar 2019 15:46:20 +0100 Subject: [PATCH 24/65] Also rename log file and embedded tomcat context path --- .gitignore | 2 +- src/main/resources/config/application.properties | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 51d50b8..9f363e8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -financer.log* +financer-server.log* .attach* \ No newline at end of file diff --git a/src/main/resources/config/application.properties b/src/main/resources/config/application.properties index b44c9c2..b430a7a 100644 --- a/src/main/resources/config/application.properties +++ b/src/main/resources/config/application.properties @@ -5,7 +5,7 @@ spring.profiles.active=@activeProfiles@ -server.servlet.context-path=/financer +server.servlet.context-path=/financer-server spring.jpa.hibernate.ddl-auto=validate @@ -16,7 +16,7 @@ info.build.artifact=@project.artifactId@ info.build.version=@project.version@ logging.level.de.financer=DEBUG -logging.file=financer.log +logging.file=financer-server.log # Country code for holiday checks # Mostly an uppercase ISO 3166 2-letter code From f9b448f24e331fea37882fd5eed35754f3f9fe3e Mon Sep 17 00:00:00 2001 From: MK13 Date: Sun, 24 Mar 2019 23:20:02 +0100 Subject: [PATCH 25/65] Various fixes and additions all over the tree Add a method to obtain all active recurring transactions. Add and adjust unit and integration tests. --- .../controller/AccountController.java | 10 ---- .../RecurringTransactionController.java | 5 ++ .../dba/RecurringTransactionRepository.java | 4 ++ .../de/financer/service/AccountService.java | 14 ------ .../service/RecurringTransactionService.java | 22 +++++---- .../financer/service/TransactionService.java | 19 +++++-- .../resources/config/application.properties | 1 + ...teRecurringTransactionIntegrationTest.java | 2 +- ...onService_getAllActiveIntegrationTest.java | 49 +++++++++++++++++++ ...nsactionService_getAllIntegrationTest.java | 2 +- ...getAllDueToday_DAILY_NEXT_WORKDAYTest.java | 34 ++++++++----- ...tAllDueToday_MONTHLY_NEXT_WORKDAYTest.java | 36 ++++++++------ ...DueToday_MONTHLY_PREVIOUS_WORKDAYTest.java | 7 +-- ...e_getAllDueToday_MONTHLY_SAME_DAYTest.java | 9 ++-- .../integration/V999_99_00__testdata.sql | 5 +- 15 files changed, 143 insertions(+), 76 deletions(-) create mode 100644 src/test/java/de/financer/controller/integration/RecurringTransactionService_getAllActiveIntegrationTest.java diff --git a/src/main/java/de/financer/controller/AccountController.java b/src/main/java/de/financer/controller/AccountController.java index 22103d2..9cff5f9 100644 --- a/src/main/java/de/financer/controller/AccountController.java +++ b/src/main/java/de/financer/controller/AccountController.java @@ -29,16 +29,6 @@ public class AccountController { return this.accountService.getAll(); } - @RequestMapping("getAccountTypes") - public Iterable getAccountTypes() { - return this.accountService.getAccountTypes(); - } - - @RequestMapping("getAccountStatus") - public Iterable getAccountStatus() { - return this.accountService.getAccountStatus(); - } - @RequestMapping("createAccount") public ResponseEntity createAccount(String key, String type) { if (LOGGER.isDebugEnabled()) { diff --git a/src/main/java/de/financer/controller/RecurringTransactionController.java b/src/main/java/de/financer/controller/RecurringTransactionController.java index c63a27f..2d495d8 100644 --- a/src/main/java/de/financer/controller/RecurringTransactionController.java +++ b/src/main/java/de/financer/controller/RecurringTransactionController.java @@ -26,6 +26,11 @@ public class RecurringTransactionController { return this.recurringTransactionService.getAll(); } + @RequestMapping("getAllActive") + public Iterable getAllActive() { + return this.recurringTransactionService.getAllActive(); + } + @RequestMapping("getAllForAccount") public Iterable getAllForAccount(String accountKey) { return this.recurringTransactionService.getAllForAccount(accountKey); diff --git a/src/main/java/de/financer/dba/RecurringTransactionRepository.java b/src/main/java/de/financer/dba/RecurringTransactionRepository.java index dffe6d5..e55bf38 100644 --- a/src/main/java/de/financer/dba/RecurringTransactionRepository.java +++ b/src/main/java/de/financer/dba/RecurringTransactionRepository.java @@ -6,7 +6,11 @@ import org.springframework.data.repository.CrudRepository; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDate; + @Transactional(propagation = Propagation.REQUIRED) public interface RecurringTransactionRepository extends CrudRepository { Iterable findRecurringTransactionsByFromAccountOrToAccount(Account fromAccount, Account toAccount); + + Iterable findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(LocalDate lastOccurrence); } diff --git a/src/main/java/de/financer/service/AccountService.java b/src/main/java/de/financer/service/AccountService.java index 58864c3..fba36b4 100644 --- a/src/main/java/de/financer/service/AccountService.java +++ b/src/main/java/de/financer/service/AccountService.java @@ -40,20 +40,6 @@ public class AccountService { return this.accountRepository.findAll(); } - /** - * @return all possible account types as specified by the {@link AccountType} enumeration, never null - */ - public Iterable getAccountTypes() { - return Arrays.stream(AccountType.values()).map(AccountType::name).collect(Collectors.toList()); - } - - /** - * @return all possible account status as specified by the {@link AccountStatus} enumeration, never null - */ - public Iterable getAccountStatus() { - return Arrays.stream(AccountStatus.values()).map(AccountStatus::name).collect(Collectors.toList()); - } - /** * This method saves the given account. It either updates the account if it already exists or inserts * it if it's new. diff --git a/src/main/java/de/financer/service/RecurringTransactionService.java b/src/main/java/de/financer/service/RecurringTransactionService.java index 4d35cce..777f015 100644 --- a/src/main/java/de/financer/service/RecurringTransactionService.java +++ b/src/main/java/de/financer/service/RecurringTransactionService.java @@ -47,6 +47,11 @@ public class RecurringTransactionService { return this.recurringTransactionRepository.findAll(); } + public Iterable getAllActive() { + return this.recurringTransactionRepository + .findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(LocalDate.now()); + } + public Iterable getAllForAccount(String accountKey) { final Account account = this.accountService.getAccountByKey(accountKey); @@ -73,8 +78,8 @@ public class RecurringTransactionService { // Visible for unit tests /* package */ Iterable getAllDueToday(LocalDate now) { - // TODO filter for lastOccurrence not in the past - final Iterable allRecurringTransactions = this.recurringTransactionRepository.findAll(); + final Iterable allRecurringTransactions = this.recurringTransactionRepository + .findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(now); //@formatter:off return IterableUtils.toList(allRecurringTransactions).stream() @@ -332,19 +337,19 @@ public class RecurringTransactionService { response = ResponseReason.MISSING_AMOUNT; } else if (amount == 0L) { response = ResponseReason.AMOUNT_ZERO; - } else if (holidayWeekendType == null) { + } else if (StringUtils.isEmpty(holidayWeekendType)) { response = ResponseReason.MISSING_HOLIDAY_WEEKEND_TYPE; } else if (!HolidayWeekendType.isValidType(holidayWeekendType)) { response = ResponseReason.INVALID_HOLIDAY_WEEKEND_TYPE; - } else if (intervalType == null) { + } else if (StringUtils.isEmpty(intervalType)) { response = ResponseReason.MISSING_INTERVAL_TYPE; } else if (!IntervalType.isValidType(intervalType)) { response = ResponseReason.INVALID_INTERVAL_TYPE; - } else if (firstOccurrence == null) { + } else if (StringUtils.isEmpty(firstOccurrence)) { response = ResponseReason.MISSING_FIRST_OCCURRENCE; } - if (response == null && firstOccurrence != null) { + if (response == null && StringUtils.isNotEmpty(firstOccurrence)) { try { LocalDate.parse(firstOccurrence, DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat())); } catch (DateTimeParseException e) { @@ -352,7 +357,7 @@ public class RecurringTransactionService { } } - if (response == null && lastOccurrence != null) { + if (response == null && StringUtils.isNotEmpty(lastOccurrence)) { try { LocalDate.parse(lastOccurrence, DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat())); } catch (DateTimeParseException e) { @@ -407,8 +412,7 @@ public class RecurringTransactionService { try { this.recurringTransactionRepository.deleteById(Long.valueOf(recurringTransactionId)); - } - catch (Exception e) { + } catch (Exception e) { LOGGER.error("Could not delete recurring transaction!", e); response = ResponseReason.UNKNOWN_ERROR; diff --git a/src/main/java/de/financer/service/TransactionService.java b/src/main/java/de/financer/service/TransactionService.java index 70da78a..1cfa3cc 100644 --- a/src/main/java/de/financer/service/TransactionService.java +++ b/src/main/java/de/financer/service/TransactionService.java @@ -4,8 +4,10 @@ import de.financer.ResponseReason; import de.financer.config.FinancerConfig; import de.financer.dba.TransactionRepository; import de.financer.model.Account; +import de.financer.model.AccountType; import de.financer.model.RecurringTransaction; import de.financer.model.Transaction; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.NumberUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -87,8 +89,17 @@ public class TransactionService { fromAccount.setCurrentBalance(fromAccount.getCurrentBalance() + (this.ruleService .getMultiplierFromAccount(fromAccount) * amount)); - toAccount.setCurrentBalance(toAccount.getCurrentBalance() + (this.ruleService - .getMultiplierToAccount(toAccount) * amount)); + + // Special case: if we do the initial bookings, and the booking is to introduce a liability, + // the balance of the liability account must increase + if (AccountType.START.equals(fromAccount.getType()) && AccountType.LIABILITY.equals(toAccount.getType())) { + toAccount.setCurrentBalance(toAccount.getCurrentBalance() + (this.ruleService + .getMultiplierToAccount(toAccount) * amount * -1)); + } + else { + toAccount.setCurrentBalance(toAccount.getCurrentBalance() + (this.ruleService + .getMultiplierToAccount(toAccount) * amount)); + } this.transactionRepository.save(transaction); @@ -158,9 +169,9 @@ public class TransactionService { response = ResponseReason.MISSING_AMOUNT; } else if (amount == 0L) { response = ResponseReason.AMOUNT_ZERO; - } else if (date == null) { + } else if (StringUtils.isEmpty(date)) { response = ResponseReason.MISSING_DATE; - } else if (date != null) { + } else if (StringUtils.isNotEmpty(date)) { try { LocalDate.parse(date, DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat())); } catch (DateTimeParseException e) { diff --git a/src/main/resources/config/application.properties b/src/main/resources/config/application.properties index b430a7a..658522c 100644 --- a/src/main/resources/config/application.properties +++ b/src/main/resources/config/application.properties @@ -6,6 +6,7 @@ spring.profiles.active=@activeProfiles@ server.servlet.context-path=/financer-server +server.port=8089 spring.jpa.hibernate.ddl-auto=validate diff --git a/src/test/java/de/financer/controller/integration/RecurringTransactionService_createRecurringTransactionIntegrationTest.java b/src/test/java/de/financer/controller/integration/RecurringTransactionService_createRecurringTransactionIntegrationTest.java index 4cb8119..6f8fc25 100644 --- a/src/test/java/de/financer/controller/integration/RecurringTransactionService_createRecurringTransactionIntegrationTest.java +++ b/src/test/java/de/financer/controller/integration/RecurringTransactionService_createRecurringTransactionIntegrationTest.java @@ -54,6 +54,6 @@ public class RecurringTransactionService_createRecurringTransactionIntegrationTe final List allRecurringTransaction = this.objectMapper .readValue(mvcResult.getResponse().getContentAsByteArray(), new TypeReference>() {}); - Assert.assertEquals(3, allRecurringTransaction.size()); + Assert.assertEquals(4, allRecurringTransaction.size()); } } diff --git a/src/test/java/de/financer/controller/integration/RecurringTransactionService_getAllActiveIntegrationTest.java b/src/test/java/de/financer/controller/integration/RecurringTransactionService_getAllActiveIntegrationTest.java new file mode 100644 index 0000000..f170a6f --- /dev/null +++ b/src/test/java/de/financer/controller/integration/RecurringTransactionService_getAllActiveIntegrationTest.java @@ -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 allRecurringTransactions = this.objectMapper + .readValue(mvcResult.getResponse().getContentAsByteArray(), new TypeReference>() {}); + + Assert.assertEquals(3, allRecurringTransactions.size()); + } + +} diff --git a/src/test/java/de/financer/controller/integration/RecurringTransactionService_getAllIntegrationTest.java b/src/test/java/de/financer/controller/integration/RecurringTransactionService_getAllIntegrationTest.java index 984d673..abea31f 100644 --- a/src/test/java/de/financer/controller/integration/RecurringTransactionService_getAllIntegrationTest.java +++ b/src/test/java/de/financer/controller/integration/RecurringTransactionService_getAllIntegrationTest.java @@ -43,7 +43,7 @@ public class RecurringTransactionService_getAllIntegrationTest { final List allRecurringTransactions = this.objectMapper .readValue(mvcResult.getResponse().getContentAsByteArray(), new TypeReference>() {}); - Assert.assertEquals(3, allRecurringTransactions.size()); + Assert.assertEquals(4, allRecurringTransactions.size()); } } diff --git a/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest.java b/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest.java index a978bf5..d01b7f6 100644 --- a/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest.java +++ b/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest.java @@ -19,15 +19,15 @@ import java.time.Period; import java.util.Collections; /** - * This class contains tests for the {@link RecurringTransactionService}, specifically for - * {@link RecurringTransaction}s that have {@link IntervalType#DAILY} and {@link HolidayWeekendType#NEXT_WORKDAY}. - * Due to these restrictions this class does not contain any tests for recurring transactions due in the close past - * that have been deferred, because recurring transactions with interval type daily get executed on the next workday - * anyway, regardless whether they have been deferred. This means that some executions of a recurring transaction with - * daily/next workday get ignored if they are on a holiday or a weekend day - they do not get executed multiple - * times on the next workday. While this is somehow unfortunate it is not in the current requirements and - * therefore left out for the sake of simplicity. If such a behavior is required daily/same day should do the trick, - * even though with slightly different semantics (execution even on holidays or weekends). + * This class contains tests for the {@link RecurringTransactionService}, specifically for {@link RecurringTransaction}s + * that have {@link IntervalType#DAILY} and {@link HolidayWeekendType#NEXT_WORKDAY}. Due to these restrictions this + * class does not contain any tests for recurring transactions due in the close past that have been deferred, because + * recurring transactions with interval type daily get executed on the next workday anyway, regardless whether they have + * been deferred. This means that some executions of a recurring transaction with daily/next workday get ignored if they + * are on a holiday or a weekend day - they do not get executed multiple times on the next workday. While this is + * somehow unfortunate it is not in the current requirements and therefore left out for the sake of simplicity. + * If such a behavior is required daily/same day should do the trick, even though with slightly different semantics + * (execution even on holidays or weekends). */ @RunWith(MockitoJUnitRunner.class) public class RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest { @@ -53,7 +53,9 @@ public class RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest { public void test_getAllDueToday_dueToday() { // Arrange // Implicitly: ruleService.isHoliday().return(false) and ruleService.isWeekend().return(false) - Mockito.when(this.recurringTransactionRepository.findAll()).thenReturn(Collections.singletonList(createRecurringTransaction(-3))); + Mockito.when(this.recurringTransactionRepository + .findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any())) + .thenReturn(Collections.singletonList(createRecurringTransaction(-3))); final LocalDate now = LocalDate.now(); // Act @@ -71,7 +73,9 @@ public class RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest { public void test_getAllDueToday_dueToday_holiday() { // Arrange // Implicitly: ruleService.isWeekend().return(false) - Mockito.when(this.recurringTransactionRepository.findAll()).thenReturn(Collections.singletonList(createRecurringTransaction(0))); + Mockito.when(this.recurringTransactionRepository + .findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any())) + .thenReturn(Collections.singletonList(createRecurringTransaction(0))); // Today is a holiday, but yesterday was not Mockito.when(this.ruleService.isHoliday(Mockito.any())).thenReturn(Boolean.TRUE, Boolean.FALSE); final LocalDate now = LocalDate.now(); @@ -91,7 +95,9 @@ public class RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest { public void test_getAllDueToday_dueToday_weekend() { // Arrange // Implicitly: ruleService.isHoliday().return(false) - Mockito.when(this.recurringTransactionRepository.findAll()).thenReturn(Collections.singletonList(createRecurringTransaction(0))); + Mockito.when(this.recurringTransactionRepository + .findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any())) + .thenReturn(Collections.singletonList(createRecurringTransaction(0))); // Today is a weekend day, but yesterday was not Mockito.when(this.ruleService.isWeekend(Mockito.any())).thenReturn(Boolean.TRUE, Boolean.FALSE); final LocalDate now = LocalDate.now(); @@ -111,7 +117,9 @@ public class RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest { public void test_getAllDueToday_dueToday_tomorrow() { // Arrange // Implicitly: ruleService.isHoliday().return(false) and ruleService.isWeekend().return(false) - Mockito.when(this.recurringTransactionRepository.findAll()).thenReturn(Collections.singletonList(createRecurringTransaction(1))); + Mockito.when(this.recurringTransactionRepository + .findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any())) + .thenReturn(Collections.singletonList(createRecurringTransaction(1))); final LocalDate now = LocalDate.now(); // Act diff --git a/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest.java b/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest.java index 9299dea..420c171 100644 --- a/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest.java +++ b/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest.java @@ -35,14 +35,15 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest } /** - * This method tests whether a recurring transaction with firstOccurrence = one month and one day ago - * (and thus was actually due yesterday), intervalType = monthly and holidayWeekendType = next_workday is due today, - * if yesterday was a holiday but today is not + * This method tests whether a recurring transaction with firstOccurrence = one month and one day ago (and thus was + * actually due yesterday), intervalType = monthly and holidayWeekendType = next_workday is due today, if yesterday + * was a holiday but today is not */ @Test public void test_getAllDueToday_duePast_holiday() { // Arrange - Mockito.when(this.recurringTransactionRepository.findAll()) + Mockito.when(this.recurringTransactionRepository + .findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any())) .thenReturn(Collections.singletonList(createRecurringTransaction(-1))); // Today is not a holiday but yesterday was Mockito.when(this.ruleService.isHoliday(Mockito.any())).thenReturn(Boolean.FALSE, Boolean.TRUE); @@ -56,9 +57,9 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest } /** - * This method tests whether a recurring transaction with firstOccurrence = last friday one month ago - * (and thus was actually due last friday), intervalType = monthly and holidayWeekendType = next_workday is due - * today (monday), if friday was holiday + * This method tests whether a recurring transaction with firstOccurrence = last friday one month ago (and thus was + * actually due last friday), intervalType = monthly and holidayWeekendType = next_workday is due today (monday), if + * friday was holiday */ @Test public void test_getAllDueToday_duePast_weekend_friday_holiday() { @@ -79,7 +80,8 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest final LocalDate now = LocalDate.now(); final LocalDate monday = now.minusDays(now.getDayOfWeek().getValue() - 1); // The transaction occurs on a friday - Mockito.when(this.recurringTransactionRepository.findAll()) + Mockito.when(this.recurringTransactionRepository + .findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any())) .thenReturn(Collections.singletonList(createRecurringTransaction(-(now.getDayOfWeek().getValue() + 2)))); // First False for the dueToday check, 2x True for actual weekend, second False for Friday Mockito.when(this.ruleService.isWeekend(Mockito.any())) @@ -96,9 +98,9 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest } /** - * This method tests whether a recurring transaction with firstOccurrence = last sunday a month ago - * (and thus was actually due last sunday/yesterday), intervalType = monthly and holidayWeekendType = next_workday - * is due today (monday) + * This method tests whether a recurring transaction with firstOccurrence = last sunday a month ago (and thus was + * actually due last sunday/yesterday), intervalType = monthly and holidayWeekendType = next_workday is due today + * (monday) */ @Test public void test_getAllDueToday_duePast_weekend_sunday() { @@ -106,7 +108,8 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest final LocalDate now = LocalDate.now(); final LocalDate monday = now.minusDays(now.getDayOfWeek().getValue() - 1); // The transaction occurs on a sunday - Mockito.when(this.recurringTransactionRepository.findAll()) + Mockito.when(this.recurringTransactionRepository + .findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any())) .thenReturn(Collections.singletonList(createRecurringTransaction(-now.getDayOfWeek().getValue()))); // First False for the dueToday check, 2x True for actual weekend, second False for Friday Mockito.when(this.ruleService.isWeekend(Mockito.any())) @@ -120,9 +123,9 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest } /** - * This method tests whether a recurring transaction with firstOccurrence = saturday a month ago - * (and thus was actually due last saturday/two days ago), intervalType = monthly and - * holidayWeekendType = next_workday is due today (monday) + * This method tests whether a recurring transaction with firstOccurrence = saturday a month ago (and thus was + * actually due last saturday/two days ago), intervalType = monthly and holidayWeekendType = next_workday is due + * today (monday) */ @Test public void test_getAllDueToday_duePast_weekend_saturday() { @@ -130,7 +133,8 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest final LocalDate now = LocalDate.now(); final LocalDate monday = now.minusDays(now.getDayOfWeek().getValue() - 1); // The transaction occurs on a saturday - Mockito.when(this.recurringTransactionRepository.findAll()) + Mockito.when(this.recurringTransactionRepository + .findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any())) .thenReturn(Collections.singletonList(createRecurringTransaction(-(now.getDayOfWeek().getValue() + 1)))); // First False for the dueToday check, 2x True for actual weekend, second False for Friday Mockito.when(this.ruleService.isWeekend(Mockito.any())) diff --git a/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_PREVIOUS_WORKDAYTest.java b/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_PREVIOUS_WORKDAYTest.java index 5a88e9f..113aef3 100644 --- a/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_PREVIOUS_WORKDAYTest.java +++ b/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_PREVIOUS_WORKDAYTest.java @@ -35,14 +35,15 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_PREVIOUS_WORKDAY } /** - * This method tests whether a recurring transaction with firstOccurrence = one month plus one day (and thus - * will actually be due tomorrow), intervalType = monthly and holidayWeekendType = previous_workday is due today, if + * This method tests whether a recurring transaction with firstOccurrence = one month plus one day (and thus will + * actually be due tomorrow), intervalType = monthly and holidayWeekendType = previous_workday is due today, if * tomorrow will be a holiday but today is not */ @Test public void test_getAllDueToday_dueFuture_holiday() { // Arrange - Mockito.when(this.recurringTransactionRepository.findAll()) + Mockito.when(this.recurringTransactionRepository + .findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any())) .thenReturn(Collections.singletonList(createRecurringTransaction(1))); // Today is not a holiday but tomorrow is Mockito.when(this.ruleService.isHoliday(Mockito.any())).thenReturn(Boolean.FALSE, Boolean.TRUE); diff --git a/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_SAME_DAYTest.java b/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_SAME_DAYTest.java index f787688..8d5cf19 100644 --- a/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_SAME_DAYTest.java +++ b/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_SAME_DAYTest.java @@ -35,14 +35,15 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_SAME_DAYTest { } /** - * This method tests whether a recurring transaction with firstOccurrence = one month and one day ago - * (and thus was actually due yesterday), intervalType = monthly and holidayWeekendType = same_day is not due today, - * if yesterday was a holiday but today is not + * This method tests whether a recurring transaction with firstOccurrence = one month and one day ago (and thus was + * actually due yesterday), intervalType = monthly and holidayWeekendType = same_day is not due today, if yesterday + * was a holiday but today is not */ @Test public void test_getAllDueToday_duePast_holiday() { // Arrange - Mockito.when(this.recurringTransactionRepository.findAll()) + Mockito.when(this.recurringTransactionRepository + .findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any())) .thenReturn(Collections.singletonList(createRecurringTransaction(-1))); // Today is not a holiday but yesterday was Mockito.when(this.ruleService.isHoliday(Mockito.any())).thenReturn(Boolean.FALSE, Boolean.TRUE); diff --git a/src/test/resources/database/hsqldb/integration/V999_99_00__testdata.sql b/src/test/resources/database/hsqldb/integration/V999_99_00__testdata.sql index 797e1a2..c55b95e 100644 --- a/src/test/resources/database/hsqldb/integration/V999_99_00__testdata.sql +++ b/src/test/resources/database/hsqldb/integration/V999_99_00__testdata.sql @@ -7,4 +7,7 @@ INSERT INTO recurring_transaction (from_account_id, to_account_id, description, VALUES ((SELECT ID FROM account WHERE "key" = 'accounts.income'), (SELECT ID FROM account WHERE "key" = 'accounts.checkaccount'), 'Pay', 250000, 'MONTHLY', '2019-01-15', 'NEXT_WORKDAY'); INSERT INTO recurring_transaction (from_account_id, to_account_id, description, amount, interval_type, first_occurrence, holiday_weekend_type) -VALUES ((SELECT ID FROM account WHERE "key" = 'accounts.cash'), (SELECT ID FROM account WHERE "key" = 'accounts.convenience'), 'Pretzel', 170, 'DAILY', '2019-02-20', 'SAME_DAY'); \ No newline at end of file +VALUES ((SELECT ID FROM account WHERE "key" = 'accounts.cash'), (SELECT ID FROM account WHERE "key" = 'accounts.convenience'), 'Pretzel', 170, 'DAILY', '2019-02-20', 'SAME_DAY'); + +INSERT INTO recurring_transaction (from_account_id, to_account_id, description, amount, interval_type, first_occurrence, last_occurrence, holiday_weekend_type) +VALUES ((SELECT ID FROM account WHERE "key" = 'accounts.cash'), (SELECT ID FROM account WHERE "key" = 'accounts.foodexternal'), 'McDonalds Happy Meal', 399, 'WEEKLY', '2019-02-20', '2019-03-20', 'SAME_DAY'); \ No newline at end of file From 52f36ebdddba86d5d7ab50a1dccc50075373c070 Mon Sep 17 00:00:00 2001 From: MK13 Date: Mon, 22 Apr 2019 21:25:23 +0200 Subject: [PATCH 26/65] Test commit for permissions handling --- doc/README | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/README b/doc/README index 83330b0..f46af59 100644 --- a/doc/README +++ b/doc/README @@ -27,4 +27,4 @@ Enter 'financer' as role name and make it superuser. Then create the actual database: createdb financer Naming both, the user and the database, 'financer' is the expected default. If you want any other names you need - to adjust the database connection settings of the financer application. \ No newline at end of file + to adjust the database connection settings of the financer application. From c88bf838347c4be8268285747370e288bd0a58e3 Mon Sep 17 00:00:00 2001 From: MK13 Date: Mon, 22 Apr 2019 21:40:02 +0200 Subject: [PATCH 27/65] Another test commit for permissions handling --- doc/README | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/README b/doc/README index f46af59..83330b0 100644 --- a/doc/README +++ b/doc/README @@ -27,4 +27,4 @@ Enter 'financer' as role name and make it superuser. Then create the actual database: createdb financer Naming both, the user and the database, 'financer' is the expected default. If you want any other names you need - to adjust the database connection settings of the financer application. + to adjust the database connection settings of the financer application. \ No newline at end of file From 78f4dea907743101e920bb35d35ae3cecf53db51 Mon Sep 17 00:00:00 2001 From: MK13 Date: Mon, 22 Apr 2019 21:49:59 +0200 Subject: [PATCH 28/65] Yet another test commit for permissions handling --- doc/README | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/README b/doc/README index 83330b0..f46af59 100644 --- a/doc/README +++ b/doc/README @@ -27,4 +27,4 @@ Enter 'financer' as role name and make it superuser. Then create the actual database: createdb financer Naming both, the user and the database, 'financer' is the expected default. If you want any other names you need - to adjust the database connection settings of the financer application. \ No newline at end of file + to adjust the database connection settings of the financer application. From ab5d0e447aa03f2f5a3a8126dfe4d0214be456c6 Mon Sep 17 00:00:00 2001 From: MK13 Date: Mon, 22 Apr 2019 21:51:38 +0200 Subject: [PATCH 29/65] Yet yet another test commit for permissions handling --- doc/README | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/README b/doc/README index f46af59..83330b0 100644 --- a/doc/README +++ b/doc/README @@ -27,4 +27,4 @@ Enter 'financer' as role name and make it superuser. Then create the actual database: createdb financer Naming both, the user and the database, 'financer' is the expected default. If you want any other names you need - to adjust the database connection settings of the financer application. + to adjust the database connection settings of the financer application. \ No newline at end of file From 2d780267effa635c46fec72e950c406c501dbcd4 Mon Sep 17 00:00:00 2001 From: MK13 Date: Mon, 22 Apr 2019 21:55:14 +0200 Subject: [PATCH 30/65] Yet yet yet another test commit for permissions handling --- doc/README | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/README b/doc/README index 83330b0..f46af59 100644 --- a/doc/README +++ b/doc/README @@ -27,4 +27,4 @@ Enter 'financer' as role name and make it superuser. Then create the actual database: createdb financer Naming both, the user and the database, 'financer' is the expected default. If you want any other names you need - to adjust the database connection settings of the financer application. \ No newline at end of file + to adjust the database connection settings of the financer application. From 9b82084f21b3af2506f69163bc8823731a892438 Mon Sep 17 00:00:00 2001 From: MK13 Date: Sun, 5 May 2019 02:01:12 +0200 Subject: [PATCH 31/65] Introduce 'deleted' flag to RecurringTransaction Hard-deleting recurring transaction cannot be done reliable because a transaction may have a foreign key reference to a recurring transaction of this transaction has been created from a recurring transaction. Thus hard-deleting a recurring transaction may lead to data inconsistencies. --- .../dba/RecurringTransactionRepository.java | 4 +++- .../financer/model/RecurringTransaction.java | 9 +++++++++ .../service/RecurringTransactionService.java | 12 +++++++---- .../database/hsqldb/V1_0_0__init.sql | 1 + .../database/postgres/V1_0_0__init.sql | 1 + ...ervice_deleteRecurringTransactionTest.java | 2 +- ...getAllDueToday_DAILY_NEXT_WORKDAYTest.java | 9 +++++---- ...tAllDueToday_MONTHLY_NEXT_WORKDAYTest.java | 20 ++++++++++++++----- ...DueToday_MONTHLY_PREVIOUS_WORKDAYTest.java | 2 +- ...e_getAllDueToday_MONTHLY_SAME_DAYTest.java | 3 ++- 10 files changed, 46 insertions(+), 17 deletions(-) diff --git a/src/main/java/de/financer/dba/RecurringTransactionRepository.java b/src/main/java/de/financer/dba/RecurringTransactionRepository.java index e55bf38..5226f92 100644 --- a/src/main/java/de/financer/dba/RecurringTransactionRepository.java +++ b/src/main/java/de/financer/dba/RecurringTransactionRepository.java @@ -12,5 +12,7 @@ import java.time.LocalDate; public interface RecurringTransactionRepository extends CrudRepository { Iterable findRecurringTransactionsByFromAccountOrToAccount(Account fromAccount, Account toAccount); - Iterable findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(LocalDate lastOccurrence); + Iterable findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(LocalDate lastOccurrence); + + Iterable findByDeletedFalse(); } diff --git a/src/main/java/de/financer/model/RecurringTransaction.java b/src/main/java/de/financer/model/RecurringTransaction.java index 1a9d69e..3df99c8 100644 --- a/src/main/java/de/financer/model/RecurringTransaction.java +++ b/src/main/java/de/financer/model/RecurringTransaction.java @@ -20,6 +20,7 @@ public class RecurringTransaction { private LocalDate lastOccurrence; @Enumerated(EnumType.STRING) private HolidayWeekendType holidayWeekendType; + private boolean deleted; public Long getId() { return id; @@ -88,4 +89,12 @@ public class RecurringTransaction { public void setIntervalType(IntervalType intervalType) { this.intervalType = intervalType; } + + public boolean isDeleted() { + return deleted; + } + + public void setDeleted(boolean deleted) { + this.deleted = deleted; + } } diff --git a/src/main/java/de/financer/service/RecurringTransactionService.java b/src/main/java/de/financer/service/RecurringTransactionService.java index 777f015..4af4ce9 100644 --- a/src/main/java/de/financer/service/RecurringTransactionService.java +++ b/src/main/java/de/financer/service/RecurringTransactionService.java @@ -44,12 +44,12 @@ public class RecurringTransactionService { private TransactionService transactionService; public Iterable getAll() { - return this.recurringTransactionRepository.findAll(); + return this.recurringTransactionRepository.findByDeletedFalse(); } public Iterable getAllActive() { return this.recurringTransactionRepository - .findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(LocalDate.now()); + .findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(LocalDate.now()); } public Iterable getAllForAccount(String accountKey) { @@ -79,7 +79,7 @@ public class RecurringTransactionService { // Visible for unit tests /* package */ Iterable getAllDueToday(LocalDate now) { final Iterable allRecurringTransactions = this.recurringTransactionRepository - .findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(now); + .findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(now); //@formatter:off return IterableUtils.toList(allRecurringTransactions).stream() @@ -411,7 +411,11 @@ public class RecurringTransactionService { } try { - this.recurringTransactionRepository.deleteById(Long.valueOf(recurringTransactionId)); + RecurringTransaction recurringTransaction = optionalRecurringTransaction.get(); + + recurringTransaction.setDeleted(true); + + this.recurringTransactionRepository.save(recurringTransaction); } catch (Exception e) { LOGGER.error("Could not delete recurring transaction!", e); diff --git a/src/main/resources/database/hsqldb/V1_0_0__init.sql b/src/main/resources/database/hsqldb/V1_0_0__init.sql index 0737352..b1a900b 100644 --- a/src/main/resources/database/hsqldb/V1_0_0__init.sql +++ b/src/main/resources/database/hsqldb/V1_0_0__init.sql @@ -24,6 +24,7 @@ CREATE TABLE recurring_transaction ( 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) diff --git a/src/main/resources/database/postgres/V1_0_0__init.sql b/src/main/resources/database/postgres/V1_0_0__init.sql index 52158af..5d6221e 100644 --- a/src/main/resources/database/postgres/V1_0_0__init.sql +++ b/src/main/resources/database/postgres/V1_0_0__init.sql @@ -24,6 +24,7 @@ CREATE TABLE recurring_transaction ( 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) diff --git a/src/test/java/de/financer/service/RecurringTransactionService_deleteRecurringTransactionTest.java b/src/test/java/de/financer/service/RecurringTransactionService_deleteRecurringTransactionTest.java index 635d1a9..8a44a1c 100644 --- a/src/test/java/de/financer/service/RecurringTransactionService_deleteRecurringTransactionTest.java +++ b/src/test/java/de/financer/service/RecurringTransactionService_deleteRecurringTransactionTest.java @@ -71,7 +71,7 @@ public class RecurringTransactionService_deleteRecurringTransactionTest { public void test_deleteRecurringTransaction_UNKNOWN_ERROR() { // Arrange Mockito.when(this.recurringTransactionRepository.findById(Mockito.anyLong())).thenReturn(Optional.of(new RecurringTransaction())); - Mockito.doThrow(new NullPointerException()).when(this.recurringTransactionRepository).deleteById(Mockito.anyLong()); + Mockito.doThrow(new NullPointerException()).when(this.recurringTransactionRepository).save(Mockito.any()); // Act final ResponseReason response = this.classUnderTest.deleteRecurringTransaction("123"); diff --git a/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest.java b/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest.java index d01b7f6..0471644 100644 --- a/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest.java +++ b/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest.java @@ -54,7 +54,7 @@ public class RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest { // Arrange // Implicitly: ruleService.isHoliday().return(false) and ruleService.isWeekend().return(false) Mockito.when(this.recurringTransactionRepository - .findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any())) + .findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any())) .thenReturn(Collections.singletonList(createRecurringTransaction(-3))); final LocalDate now = LocalDate.now(); @@ -74,7 +74,7 @@ public class RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest { // Arrange // Implicitly: ruleService.isWeekend().return(false) Mockito.when(this.recurringTransactionRepository - .findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any())) + .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); @@ -96,7 +96,7 @@ public class RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest { // Arrange // Implicitly: ruleService.isHoliday().return(false) Mockito.when(this.recurringTransactionRepository - .findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any())) + .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); @@ -118,7 +118,7 @@ public class RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest { // Arrange // Implicitly: ruleService.isHoliday().return(false) and ruleService.isWeekend().return(false) Mockito.when(this.recurringTransactionRepository - .findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any())) + .findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any())) .thenReturn(Collections.singletonList(createRecurringTransaction(1))); final LocalDate now = LocalDate.now(); @@ -136,6 +136,7 @@ public class RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest { recurringTransaction.setHolidayWeekendType(HolidayWeekendType.NEXT_WORKDAY); recurringTransaction.setIntervalType(IntervalType.DAILY); + recurringTransaction.setDeleted(false); return recurringTransaction; } diff --git a/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest.java b/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest.java index 420c171..db5f517 100644 --- a/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest.java +++ b/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest.java @@ -7,6 +7,7 @@ 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; @@ -43,7 +44,7 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest public void test_getAllDueToday_duePast_holiday() { // Arrange Mockito.when(this.recurringTransactionRepository - .findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any())) + .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); @@ -81,7 +82,7 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest final LocalDate monday = now.minusDays(now.getDayOfWeek().getValue() - 1); // The transaction occurs on a friday Mockito.when(this.recurringTransactionRepository - .findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any())) + .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())) @@ -103,14 +104,20 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest * (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 - .findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any())) - .thenReturn(Collections.singletonList(createRecurringTransaction(-now.getDayOfWeek().getValue()))); + .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); @@ -128,13 +135,15 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest * 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 - .findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any())) + .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())) @@ -154,6 +163,7 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest recurringTransaction.setHolidayWeekendType(HolidayWeekendType.NEXT_WORKDAY); recurringTransaction.setIntervalType(IntervalType.MONTHLY); + recurringTransaction.setDeleted(false); return recurringTransaction; } diff --git a/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_PREVIOUS_WORKDAYTest.java b/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_PREVIOUS_WORKDAYTest.java index 113aef3..2a17604 100644 --- a/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_PREVIOUS_WORKDAYTest.java +++ b/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_PREVIOUS_WORKDAYTest.java @@ -43,7 +43,7 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_PREVIOUS_WORKDAY public void test_getAllDueToday_dueFuture_holiday() { // Arrange Mockito.when(this.recurringTransactionRepository - .findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any())) + .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); diff --git a/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_SAME_DAYTest.java b/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_SAME_DAYTest.java index 8d5cf19..e04ef6a 100644 --- a/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_SAME_DAYTest.java +++ b/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_SAME_DAYTest.java @@ -43,7 +43,7 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_SAME_DAYTest { public void test_getAllDueToday_duePast_holiday() { // Arrange Mockito.when(this.recurringTransactionRepository - .findByLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any())) + .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); @@ -63,6 +63,7 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_SAME_DAYTest { recurringTransaction.setHolidayWeekendType(HolidayWeekendType.SAME_DAY); recurringTransaction.setIntervalType(IntervalType.MONTHLY); + recurringTransaction.setDeleted(false); return recurringTransaction; } From 762c373104412cdb36978c304a0a24c3d8ef8c1c Mon Sep 17 00:00:00 2001 From: MK13 Date: Sun, 5 May 2019 02:08:33 +0200 Subject: [PATCH 32/65] financer release version 1 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index dbca90c..1f96950 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ de.77zzcx7.financer financer-server - 1-SNAPSHOT + 1 ${packaging.type} The server part of the financer application - a simple app to manage your personal finances financer-server From ab3f08356e6e1c2705d8c1d8cc74a59fc3bd663e Mon Sep 17 00:00:00 2001 From: MK13 Date: Sun, 5 May 2019 02:12:11 +0200 Subject: [PATCH 33/65] Prepare next development iteration for v2 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 1f96950..78e81df 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ de.77zzcx7.financer financer-server - 1 + 2-SNAPSHOT ${packaging.type} The server part of the financer application - a simple app to manage your personal finances financer-server From 6c7a418828458e810db1bb90c65509ffc1f793d3 Mon Sep 17 00:00:00 2001 From: MK13 Date: Sun, 5 May 2019 11:58:09 +0200 Subject: [PATCH 34/65] Update database setup guide and introduce password for DB connection --- doc/README | 14 ++++++++++---- .../config/application-postgres.properties | 1 + 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/doc/README b/doc/README index f46af59..b860396 100644 --- a/doc/README +++ b/doc/README @@ -23,8 +23,14 @@ ------------------ First install PostgreSQL. Then create a user for financer: sudo -iu postgres - createuser --interactive - Enter 'financer' as role name and make it superuser. Then create the actual database: + 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 - Naming both, the user and the database, 'financer' is the expected default. If you want any other names you need - to adjust the database connection settings of the financer application. + 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 \ No newline at end of file diff --git a/src/main/resources/config/application-postgres.properties b/src/main/resources/config/application-postgres.properties index 7082fd1..1eeea1c 100644 --- a/src/main/resources/config/application-postgres.properties +++ b/src/main/resources/config/application-postgres.properties @@ -2,6 +2,7 @@ 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 \ No newline at end of file From f830586a4fa03fbd564621d8f2affb9214f3ff95 Mon Sep 17 00:00:00 2001 From: MK13 Date: Sun, 5 May 2019 12:00:28 +0200 Subject: [PATCH 35/65] financer release version 2 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 78e81df..228f990 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ de.77zzcx7.financer financer-server - 2-SNAPSHOT + 2 ${packaging.type} The server part of the financer application - a simple app to manage your personal finances financer-server From 137369ab58defb1ae433fa67e0f3409fb4f4383a Mon Sep 17 00:00:00 2001 From: MK13 Date: Sun, 5 May 2019 12:01:43 +0200 Subject: [PATCH 36/65] Prepare next development iteration for v3 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 228f990..1cafac3 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ de.77zzcx7.financer financer-server - 2 + 3-SNAPSHOT ${packaging.type} The server part of the financer application - a simple app to manage your personal finances financer-server From b288397b7f43f5cf7fcf9347448f7e6ee37e89ed Mon Sep 17 00:00:00 2001 From: MK13 Date: Sun, 5 May 2019 12:07:58 +0200 Subject: [PATCH 37/65] Increase parallel deployment version to match the project version --- pom.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 1cafac3..307beab 100644 --- a/pom.xml +++ b/pom.xml @@ -13,6 +13,7 @@ de.77zzcx7.financer financer-server + 3-SNAPSHOT ${packaging.type} The server part of the financer application - a simple app to manage your personal finances @@ -28,7 +29,7 @@ - 000001 + 000003 From 4d192e30fa5594bc06718ae6339d45c5216d7ee7 Mon Sep 17 00:00:00 2001 From: MK13 Date: Thu, 9 May 2019 21:25:07 +0200 Subject: [PATCH 38/65] Disable JMX to enable seamless parallel deployments --- src/main/resources/config/application.properties | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/resources/config/application.properties b/src/main/resources/config/application.properties index 658522c..c2d60b9 100644 --- a/src/main/resources/config/application.properties +++ b/src/main/resources/config/application.properties @@ -41,3 +41,7 @@ financer.fromAddress=financer@77zzcx7.de 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 \ No newline at end of file From 4e29ecff6ade55b695f995c4baab538efac216bc Mon Sep 17 00:00:00 2001 From: MK13 Date: Sat, 11 May 2019 23:44:48 +0200 Subject: [PATCH 39/65] URL decode account key to prepare for account keys with spaces Also extend DEBUG logging in controllers --- .../controller/AccountController.java | 29 ++++++++++++++----- .../RecurringTransactionController.java | 17 +++++++++-- .../controller/TransactionController.java | 18 ++++++++++-- 3 files changed, 51 insertions(+), 13 deletions(-) diff --git a/src/main/java/de/financer/controller/AccountController.java b/src/main/java/de/financer/controller/AccountController.java index 9cff5f9..12e318f 100644 --- a/src/main/java/de/financer/controller/AccountController.java +++ b/src/main/java/de/financer/controller/AccountController.java @@ -10,6 +10,9 @@ 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("accounts") public class AccountController { @@ -21,7 +24,13 @@ public class AccountController { @RequestMapping("getByKey") public Account getAccountByKey(String key) { - return this.accountService.getAccountByKey(key); + final String decoded = URLDecoder.decode(key, StandardCharsets.UTF_8); + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug(String.format("/accounts/getAccountByKey got parameter: %s", decoded)); + } + + return this.accountService.getAccountByKey(decoded); } @RequestMapping("getAll") @@ -31,11 +40,13 @@ public class AccountController { @RequestMapping("createAccount") public ResponseEntity createAccount(String key, String type) { + final String decoded = URLDecoder.decode(key, StandardCharsets.UTF_8); + if (LOGGER.isDebugEnabled()) { - LOGGER.debug(String.format("/accounts/createAccount got parameters: %s, %s", key, type)); + LOGGER.debug(String.format("/accounts/createAccount got parameters: %s, %s", decoded, type)); } - final ResponseReason responseReason = this.accountService.createAccount(key, type); + final ResponseReason responseReason = this.accountService.createAccount(decoded, type); if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format("/accounts/createAccount returns with %s", responseReason.name())); @@ -46,11 +57,13 @@ public class AccountController { @RequestMapping("closeAccount") public ResponseEntity closeAccount(String key) { + final String decoded = URLDecoder.decode(key, StandardCharsets.UTF_8); + if (LOGGER.isDebugEnabled()) { - LOGGER.debug(String.format("/accounts/closeAccount got parameters: %s", key)); + LOGGER.debug(String.format("/accounts/closeAccount got parameters: %s", decoded)); } - final ResponseReason responseReason = this.accountService.closeAccount(key); + final ResponseReason responseReason = this.accountService.closeAccount(decoded); if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format("/accounts/closeAccount returns with %s", responseReason.name())); @@ -61,11 +74,13 @@ public class AccountController { @RequestMapping("openAccount") public ResponseEntity openAccount(String key) { + final String decoded = URLDecoder.decode(key, StandardCharsets.UTF_8); + if (LOGGER.isDebugEnabled()) { - LOGGER.debug(String.format("/accounts/openAccount got parameters: %s", key)); + LOGGER.debug(String.format("/accounts/openAccount got parameters: %s", decoded)); } - final ResponseReason responseReason = this.accountService.openAccount(key); + final ResponseReason responseReason = this.accountService.openAccount(decoded); if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format("/accounts/openAccount returns with %s", responseReason.name())); diff --git a/src/main/java/de/financer/controller/RecurringTransactionController.java b/src/main/java/de/financer/controller/RecurringTransactionController.java index 2d495d8..dc8ba26 100644 --- a/src/main/java/de/financer/controller/RecurringTransactionController.java +++ b/src/main/java/de/financer/controller/RecurringTransactionController.java @@ -10,6 +10,8 @@ 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 @@ -33,7 +35,13 @@ public class RecurringTransactionController { @RequestMapping("getAllForAccount") public Iterable getAllForAccount(String accountKey) { - return this.recurringTransactionService.getAllForAccount(accountKey); + final String decoded = URLDecoder.decode(accountKey, StandardCharsets.UTF_8); + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug(String.format("/recurringTransactions/getAllForAccount got parameter: %s", decoded)); + } + + return this.recurringTransactionService.getAllForAccount(decoded); } @RequestMapping("getAllDueToday") @@ -47,15 +55,18 @@ public class RecurringTransactionController { String intervalType, String firstOccurrence, String lastOccurrence ) { + final String decodedFrom = URLDecoder.decode(fromAccountKey, StandardCharsets.UTF_8); + final String decodedTo = URLDecoder.decode(toAccountKey, StandardCharsets.UTF_8); + if (LOGGER.isDebugEnabled()) { LOGGER.debug(String .format("/recurringTransactions/createRecurringTransaction got parameters: %s, %s, %s, %s, %s, " + - "%s, %s, %s", fromAccountKey, toAccountKey, amount, description, holidayWeekendType, + "%s, %s, %s", decodedFrom, decodedTo, amount, description, holidayWeekendType, intervalType, firstOccurrence, lastOccurrence)); } final ResponseReason responseReason = this.recurringTransactionService - .createRecurringTransaction(fromAccountKey, toAccountKey, amount, description, holidayWeekendType, + .createRecurringTransaction(decodedFrom, decodedTo, amount, description, holidayWeekendType, intervalType, firstOccurrence, lastOccurrence); if (LOGGER.isDebugEnabled()) { diff --git a/src/main/java/de/financer/controller/TransactionController.java b/src/main/java/de/financer/controller/TransactionController.java index 276a3f4..bce51d3 100644 --- a/src/main/java/de/financer/controller/TransactionController.java +++ b/src/main/java/de/financer/controller/TransactionController.java @@ -10,6 +10,9 @@ 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 { @@ -25,21 +28,30 @@ public class TransactionController { @RequestMapping("getAllForAccount") public Iterable getAllForAccount(String accountKey) { - return this.transactionService.getAllForAccount(accountKey); + final String decoded = URLDecoder.decode(accountKey, StandardCharsets.UTF_8); + + 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 = URLDecoder.decode(fromAccountKey, StandardCharsets.UTF_8); + final String decodedTo = URLDecoder.decode(toAccountKey, StandardCharsets.UTF_8); + if (LOGGER.isDebugEnabled()) { LOGGER.debug(String .format("/transactions/createTransaction got parameters: %s, %s, %s, %s, %s", - fromAccountKey, toAccountKey, amount, date, description)); + decodedFrom, decodedTo, amount, date, description)); } final ResponseReason responseReason = this.transactionService - .createTransaction(fromAccountKey, toAccountKey, amount, date, description); + .createTransaction(decodedFrom, decodedTo, amount, date, description); if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format("/transactions/createTransaction returns with %s", responseReason.name())); From c96a36f3253234aa00e4063a724e358faa017d48 Mon Sep 17 00:00:00 2001 From: MK13 Date: Sun, 12 May 2019 00:43:28 +0200 Subject: [PATCH 40/65] Remove artificial restriction that account keys need to start with 'accounts.' Rework URL decoding into own util method because the previously used method is only available since Java 10 (current runtime is 1.9 however). Further adjust unit tests and test data to reflect the change. Also add a migration script for already installed instances. --- src/main/java/de/financer/ResponseReason.java | 1 - .../controller/AccountController.java | 11 +-- .../financer/controller/ControllerUtil.java | 22 +++++ .../RecurringTransactionController.java | 6 +- .../controller/TransactionController.java | 6 +- .../de/financer/service/AccountService.java | 11 +-- .../database/common/V3_0_0__accountRename.sql | 88 +++++++++++++++++++ ...teRecurringTransactionIntegrationTest.java | 4 +- .../AccountService_createAccountTest.java | 18 +--- .../AccountService_setAccountStatusTest.java | 18 +--- ...dRecurringTransactionReminderTaskTest.java | 6 +- .../integration/V999_99_00__testdata.sql | 8 +- 12 files changed, 136 insertions(+), 63 deletions(-) create mode 100644 src/main/java/de/financer/controller/ControllerUtil.java create mode 100644 src/main/resources/database/common/V3_0_0__accountRename.sql diff --git a/src/main/java/de/financer/ResponseReason.java b/src/main/java/de/financer/ResponseReason.java index d531686..8e87790 100644 --- a/src/main/java/de/financer/ResponseReason.java +++ b/src/main/java/de/financer/ResponseReason.java @@ -7,7 +7,6 @@ public enum ResponseReason { OK(HttpStatus.OK), UNKNOWN_ERROR(HttpStatus.INTERNAL_SERVER_ERROR), INVALID_ACCOUNT_TYPE(HttpStatus.INTERNAL_SERVER_ERROR), - INVALID_ACCOUNT_KEY(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), diff --git a/src/main/java/de/financer/controller/AccountController.java b/src/main/java/de/financer/controller/AccountController.java index 12e318f..9c37805 100644 --- a/src/main/java/de/financer/controller/AccountController.java +++ b/src/main/java/de/financer/controller/AccountController.java @@ -10,9 +10,6 @@ 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("accounts") public class AccountController { @@ -24,7 +21,7 @@ public class AccountController { @RequestMapping("getByKey") public Account getAccountByKey(String key) { - final String decoded = URLDecoder.decode(key, StandardCharsets.UTF_8); + final String decoded = ControllerUtil.urlDecode(key); if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format("/accounts/getAccountByKey got parameter: %s", decoded)); @@ -40,7 +37,7 @@ public class AccountController { @RequestMapping("createAccount") public ResponseEntity createAccount(String key, String type) { - final String decoded = URLDecoder.decode(key, StandardCharsets.UTF_8); + final String decoded = ControllerUtil.urlDecode(key); if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format("/accounts/createAccount got parameters: %s, %s", decoded, type)); @@ -57,7 +54,7 @@ public class AccountController { @RequestMapping("closeAccount") public ResponseEntity closeAccount(String key) { - final String decoded = URLDecoder.decode(key, StandardCharsets.UTF_8); + final String decoded = ControllerUtil.urlDecode(key); if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format("/accounts/closeAccount got parameters: %s", decoded)); @@ -74,7 +71,7 @@ public class AccountController { @RequestMapping("openAccount") public ResponseEntity openAccount(String key) { - final String decoded = URLDecoder.decode(key, StandardCharsets.UTF_8); + final String decoded = ControllerUtil.urlDecode(key); if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format("/accounts/openAccount got parameters: %s", decoded)); diff --git a/src/main/java/de/financer/controller/ControllerUtil.java b/src/main/java/de/financer/controller/ControllerUtil.java new file mode 100644 index 0000000..bf5711e --- /dev/null +++ b/src/main/java/de/financer/controller/ControllerUtil.java @@ -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 %20 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; + } + } +} diff --git a/src/main/java/de/financer/controller/RecurringTransactionController.java b/src/main/java/de/financer/controller/RecurringTransactionController.java index dc8ba26..623d6e1 100644 --- a/src/main/java/de/financer/controller/RecurringTransactionController.java +++ b/src/main/java/de/financer/controller/RecurringTransactionController.java @@ -35,7 +35,7 @@ public class RecurringTransactionController { @RequestMapping("getAllForAccount") public Iterable getAllForAccount(String accountKey) { - final String decoded = URLDecoder.decode(accountKey, StandardCharsets.UTF_8); + final String decoded = ControllerUtil.urlDecode(accountKey); if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format("/recurringTransactions/getAllForAccount got parameter: %s", decoded)); @@ -55,8 +55,8 @@ public class RecurringTransactionController { String intervalType, String firstOccurrence, String lastOccurrence ) { - final String decodedFrom = URLDecoder.decode(fromAccountKey, StandardCharsets.UTF_8); - final String decodedTo = URLDecoder.decode(toAccountKey, StandardCharsets.UTF_8); + final String decodedFrom = ControllerUtil.urlDecode(fromAccountKey); + final String decodedTo = ControllerUtil.urlDecode(toAccountKey); if (LOGGER.isDebugEnabled()) { LOGGER.debug(String diff --git a/src/main/java/de/financer/controller/TransactionController.java b/src/main/java/de/financer/controller/TransactionController.java index bce51d3..61ab109 100644 --- a/src/main/java/de/financer/controller/TransactionController.java +++ b/src/main/java/de/financer/controller/TransactionController.java @@ -28,7 +28,7 @@ public class TransactionController { @RequestMapping("getAllForAccount") public Iterable getAllForAccount(String accountKey) { - final String decoded = URLDecoder.decode(accountKey, StandardCharsets.UTF_8); + final String decoded = ControllerUtil.urlDecode(accountKey); if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format("/transactions/getAllForAccount got parameter: %s", decoded)); @@ -41,8 +41,8 @@ public class TransactionController { public ResponseEntity createTransaction(String fromAccountKey, String toAccountKey, Long amount, String date, String description ) { - final String decodedFrom = URLDecoder.decode(fromAccountKey, StandardCharsets.UTF_8); - final String decodedTo = URLDecoder.decode(toAccountKey, StandardCharsets.UTF_8); + final String decodedFrom = ControllerUtil.urlDecode(fromAccountKey); + final String decodedTo = ControllerUtil.urlDecode(toAccountKey); if (LOGGER.isDebugEnabled()) { LOGGER.debug(String diff --git a/src/main/java/de/financer/service/AccountService.java b/src/main/java/de/financer/service/AccountService.java index fba36b4..bcea422 100644 --- a/src/main/java/de/financer/service/AccountService.java +++ b/src/main/java/de/financer/service/AccountService.java @@ -55,10 +55,9 @@ public class AccountService { * This method creates new account with the given key and type. The account has status {@link AccountStatus#OPEN OPEN} * and a current balance of 0. * - * @param key the key of the new account. Must begin with account. + * @param key the key of the new account * @param type the type of the new account. Must be one of {@link AccountType}. * @return {@link ResponseReason#INVALID_ACCOUNT_TYPE} if the given type is not a valid {@link AccountType}, - * {@link ResponseReason#INVALID_ACCOUNT_KEY} if the given key does not conform to the format specification, * {@link ResponseReason#UNKNOWN_ERROR} if an unexpected error occurs and * {@link ResponseReason#OK} if the operation completed successfully. Never returns null. */ @@ -68,10 +67,6 @@ public class AccountService { return ResponseReason.INVALID_ACCOUNT_TYPE; } - if (!StringUtils.startsWith(key, "accounts.")) { - return ResponseReason.INVALID_ACCOUNT_KEY; - } - final Account account = new Account(); account.setKey(key); @@ -105,10 +100,6 @@ public class AccountService { // Visible for unit tests /* package */ ResponseReason setAccountStatus(String key, AccountStatus accountStatus) { - if (!StringUtils.startsWith(key, "accounts.")) { - return ResponseReason.INVALID_ACCOUNT_KEY; - } - final Account account = this.accountRepository.findByKey(key); if (account == null) { diff --git a/src/main/resources/database/common/V3_0_0__accountRename.sql b/src/main/resources/database/common/V3_0_0__accountRename.sql new file mode 100644 index 0000000..12652c7 --- /dev/null +++ b/src/main/resources/database/common/V3_0_0__accountRename.sql @@ -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'; \ No newline at end of file diff --git a/src/test/java/de/financer/controller/integration/RecurringTransactionService_createRecurringTransactionIntegrationTest.java b/src/test/java/de/financer/controller/integration/RecurringTransactionService_createRecurringTransactionIntegrationTest.java index 6f8fc25..c64e6ae 100644 --- a/src/test/java/de/financer/controller/integration/RecurringTransactionService_createRecurringTransactionIntegrationTest.java +++ b/src/test/java/de/financer/controller/integration/RecurringTransactionService_createRecurringTransactionIntegrationTest.java @@ -36,8 +36,8 @@ public class RecurringTransactionService_createRecurringTransactionIntegrationTe @Test public void test_createRecurringTransaction() throws Exception { final MvcResult mvcRequest = this.mockMvc.perform(get("/recurringTransactions/createRecurringTransaction") - .param("fromAccountKey", "accounts.income") - .param("toAccountKey", "accounts.checkaccount") + .param("fromAccountKey", "Income") + .param("toAccountKey", "Check account") .param("amount", "250000") .param("description", "Monthly rent") .param("holidayWeekendType", "SAME_DAY") diff --git a/src/test/java/de/financer/service/AccountService_createAccountTest.java b/src/test/java/de/financer/service/AccountService_createAccountTest.java index 38daec8..ed4e007 100644 --- a/src/test/java/de/financer/service/AccountService_createAccountTest.java +++ b/src/test/java/de/financer/service/AccountService_createAccountTest.java @@ -32,25 +32,13 @@ public class AccountService_createAccountTest { Assert.assertEquals(ResponseReason.INVALID_ACCOUNT_TYPE, response); } - @Test - public void test_createAccount_INVALID_ACCOUNT_KEY() { - // Arrange - // Nothing to do - - // Act - ResponseReason response = this.classUnderTest.createAccount(null, "BANK"); - - // Assert - Assert.assertEquals(ResponseReason.INVALID_ACCOUNT_KEY, response); - } - @Test public void test_createAccount_UNKNOWN_ERROR() { // Arrange Mockito.doThrow(new NullPointerException()).when(this.accountRepository).save(Mockito.any(Account.class)); // Act - ResponseReason response = this.classUnderTest.createAccount("accounts.test", "BANK"); + ResponseReason response = this.classUnderTest.createAccount("Test", "BANK"); // Assert Assert.assertEquals(ResponseReason.UNKNOWN_ERROR, response); @@ -62,11 +50,11 @@ public class AccountService_createAccountTest { // Nothing to do // Act - ResponseReason response = this.classUnderTest.createAccount("accounts.test", "BANK"); + ResponseReason response = this.classUnderTest.createAccount("Test", "BANK"); // Assert Assert.assertEquals(ResponseReason.OK, response); Mockito.verify(this.accountRepository, Mockito.times(1)) - .save(ArgumentMatchers.argThat((acc) -> "accounts.test".equals(acc.getKey()))); + .save(ArgumentMatchers.argThat((acc) -> "Test".equals(acc.getKey()))); } } diff --git a/src/test/java/de/financer/service/AccountService_setAccountStatusTest.java b/src/test/java/de/financer/service/AccountService_setAccountStatusTest.java index 880fff8..6c0ce2c 100644 --- a/src/test/java/de/financer/service/AccountService_setAccountStatusTest.java +++ b/src/test/java/de/financer/service/AccountService_setAccountStatusTest.java @@ -21,25 +21,13 @@ public class AccountService_setAccountStatusTest { @Mock private AccountRepository accountRepository; - @Test - public void test_setAccountStatus_INVALID_ACCOUNT_KEY() { - // Arrange - // Nothing to do - - // Act - ResponseReason response = this.classUnderTest.setAccountStatus(null, AccountStatus.CLOSED); - - // Assert - Assert.assertEquals(ResponseReason.INVALID_ACCOUNT_KEY, response); - } - @Test public void test_setAccountStatus_ACCOUNT_NOT_FOUND() { // Arrange // Nothing to do // Act - ResponseReason response = this.classUnderTest.setAccountStatus("accounts.test", AccountStatus.CLOSED); + ResponseReason response = this.classUnderTest.setAccountStatus("Test", AccountStatus.CLOSED); // Assert Assert.assertEquals(ResponseReason.ACCOUNT_NOT_FOUND, response); @@ -52,7 +40,7 @@ public class AccountService_setAccountStatusTest { Mockito.doThrow(new NullPointerException()).when(this.accountRepository).save(Mockito.any(Account.class)); // Act - ResponseReason response = this.classUnderTest.setAccountStatus("accounts.test", AccountStatus.CLOSED); + ResponseReason response = this.classUnderTest.setAccountStatus("Test", AccountStatus.CLOSED); // Assert Assert.assertEquals(ResponseReason.UNKNOWN_ERROR, response); @@ -64,7 +52,7 @@ public class AccountService_setAccountStatusTest { Mockito.when(this.accountRepository.findByKey(Mockito.anyString())).thenReturn(new Account()); // Act - ResponseReason response = this.classUnderTest.setAccountStatus("accounts.test", AccountStatus.CLOSED); + ResponseReason response = this.classUnderTest.setAccountStatus("Test", AccountStatus.CLOSED); // Assert Assert.assertEquals(ResponseReason.OK, response); diff --git a/src/test/java/de/financer/task/SendRecurringTransactionReminderTaskTest.java b/src/test/java/de/financer/task/SendRecurringTransactionReminderTaskTest.java index 7af9de9..844178d 100644 --- a/src/test/java/de/financer/task/SendRecurringTransactionReminderTaskTest.java +++ b/src/test/java/de/financer/task/SendRecurringTransactionReminderTaskTest.java @@ -36,9 +36,9 @@ public class SendRecurringTransactionReminderTaskTest { public void test_sendReminder() { // Arrange final Collection recurringTransactions = Arrays.asList( - createRecurringTransaction("Test booking 1", "accounts.income", "accounts.bank", Long.valueOf(250000)), - createRecurringTransaction("Test booking 2", "accounts.bank", "accounts.rent", Long.valueOf(41500)), - createRecurringTransaction("Test booking 3", "accounts.bank", "accounts.cash", Long.valueOf(5000)) + createRecurringTransaction("Test booking 1", "Income", "accounts.bank", Long.valueOf(250000)), + createRecurringTransaction("Test booking 2", "Bank", "accounts.rent", Long.valueOf(41500)), + createRecurringTransaction("Test booking 3", "Bank", "accounts.cash", Long.valueOf(5000)) ); Mockito.when(this.recurringTransactionService.getAllDueToday()).thenReturn(recurringTransactions); diff --git a/src/test/resources/database/hsqldb/integration/V999_99_00__testdata.sql b/src/test/resources/database/hsqldb/integration/V999_99_00__testdata.sql index c55b95e..ef84e78 100644 --- a/src/test/resources/database/hsqldb/integration/V999_99_00__testdata.sql +++ b/src/test/resources/database/hsqldb/integration/V999_99_00__testdata.sql @@ -1,13 +1,13 @@ -- Accounts INSERT INTO account ("key", type, status, current_balance) -VALUES ('accounts.convenience', 'EXPENSE', 'OPEN', 0); +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" = 'accounts.income'), (SELECT ID FROM account WHERE "key" = 'accounts.checkaccount'), 'Pay', 250000, 'MONTHLY', '2019-01-15', 'NEXT_WORKDAY'); +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" = 'accounts.cash'), (SELECT ID FROM account WHERE "key" = 'accounts.convenience'), 'Pretzel', 170, 'DAILY', '2019-02-20', 'SAME_DAY'); +VALUES ((SELECT ID FROM account WHERE "key" = '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" = 'accounts.cash'), (SELECT ID FROM account WHERE "key" = 'accounts.foodexternal'), 'McDonalds Happy Meal', 399, 'WEEKLY', '2019-02-20', '2019-03-20', 'SAME_DAY'); \ No newline at end of file +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'); \ No newline at end of file From 8f605a17e4995005aa9ef50b94618374d5bf5ff3 Mon Sep 17 00:00:00 2001 From: MK13 Date: Sun, 12 May 2019 01:05:37 +0200 Subject: [PATCH 41/65] Adjust log file settings Retain max seven days of logs and change max file size to 50MB --- src/main/resources/config/application.properties | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/resources/config/application.properties b/src/main/resources/config/application.properties index c2d60b9..bc847d2 100644 --- a/src/main/resources/config/application.properties +++ b/src/main/resources/config/application.properties @@ -18,6 +18,8 @@ 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 From a78c0f217b7cc227d427c83af9d6208dd7586c3a Mon Sep 17 00:00:00 2001 From: MK13 Date: Sun, 12 May 2019 01:06:49 +0200 Subject: [PATCH 42/65] financer release version 3 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 307beab..799c334 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ de.77zzcx7.financer financer-server - 3-SNAPSHOT + 3 ${packaging.type} The server part of the financer application - a simple app to manage your personal finances financer-server From 2827297d01080a6297736d107cba688f7384b7b9 Mon Sep 17 00:00:00 2001 From: MK13 Date: Sun, 12 May 2019 01:07:51 +0200 Subject: [PATCH 43/65] Prepare next development iteration for v4 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 799c334..423cdcc 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ de.77zzcx7.financer financer-server - 3 + 4-SNAPSHOT ${packaging.type} The server part of the financer application - a simple app to manage your personal finances financer-server @@ -29,7 +29,7 @@ - 000003 + 000004 From c830a8e0b21cf7f0ffe0de7ccba2fdffdb0dce78 Mon Sep 17 00:00:00 2001 From: MK13 Date: Sun, 12 May 2019 01:17:21 +0200 Subject: [PATCH 44/65] Fix rename script --- src/main/resources/database/common/V3_0_0__accountRename.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/database/common/V3_0_0__accountRename.sql b/src/main/resources/database/common/V3_0_0__accountRename.sql index 12652c7..4bdd4e8 100644 --- a/src/main/resources/database/common/V3_0_0__accountRename.sql +++ b/src/main/resources/database/common/V3_0_0__accountRename.sql @@ -5,7 +5,7 @@ WHERE "key" = 'accounts.checkaccount'; UPDATE account SET "key" = 'Income' -WHERE "key" = 'accounts.income' +WHERE "key" = 'accounts.income'; UPDATE account SET "key" = 'Cash' From 148a1a44215400e4fd6f08b16e5ddf2f0b6ca7f7 Mon Sep 17 00:00:00 2001 From: MK13 Date: Sun, 12 May 2019 11:13:22 +0200 Subject: [PATCH 45/65] Handle duplicate account key issue gracefully Change transaction propagation to SUPPORTS for that as otherwise an UnexpectedRollbackException gets thrown --- src/main/java/de/financer/ResponseReason.java | 3 ++- src/main/java/de/financer/service/AccountService.java | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/financer/ResponseReason.java b/src/main/java/de/financer/ResponseReason.java index 8e87790..bd1b02e 100644 --- a/src/main/java/de/financer/ResponseReason.java +++ b/src/main/java/de/financer/ResponseReason.java @@ -28,7 +28,8 @@ public enum ResponseReason { 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); + ACCOUNT_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR), + DUPLICATE_ACCOUNT_KEY(HttpStatus.INTERNAL_SERVER_ERROR); private HttpStatus httpStatus; diff --git a/src/main/java/de/financer/service/AccountService.java b/src/main/java/de/financer/service/AccountService.java index bcea422..67deea5 100644 --- a/src/main/java/de/financer/service/AccountService.java +++ b/src/main/java/de/financer/service/AccountService.java @@ -9,6 +9,7 @@ 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; @@ -61,7 +62,7 @@ public class AccountService { * {@link ResponseReason#UNKNOWN_ERROR} if an unexpected error occurs and * {@link ResponseReason#OK} if the operation completed successfully. Never returns null. */ - @Transactional(propagation = Propagation.REQUIRED) + @Transactional(propagation = Propagation.SUPPORTS) public ResponseReason createAccount(String key, String type) { if (!AccountType.isValidType(type)) { return ResponseReason.INVALID_ACCOUNT_TYPE; @@ -79,6 +80,11 @@ public class AccountService { try { this.accountRepository.save(account); } + catch (DataIntegrityViolationException dive) { + LOGGER.error(String.format("Duplicate key! %s|%s", key, type), dive); + + return ResponseReason.DUPLICATE_ACCOUNT_KEY; + } catch (Exception e) { LOGGER.error(String.format("Could not save account %s|%s", key, type), e); From 2208348cd58ca43d9edb1febc82c4c0e39c036e6 Mon Sep 17 00:00:00 2001 From: MK13 Date: Tue, 14 May 2019 20:18:17 +0200 Subject: [PATCH 46/65] Various fixes Use URLdecode also for the descriptions of (recurring) transactions. Further fix a bug with recurring transactions that have a firstOccurrence date in the future. --- .../RecurringTransactionController.java | 5 +++-- .../controller/TransactionController.java | 5 +++-- .../service/RecurringTransactionService.java | 19 +++++++++++++++++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/main/java/de/financer/controller/RecurringTransactionController.java b/src/main/java/de/financer/controller/RecurringTransactionController.java index 623d6e1..6c36465 100644 --- a/src/main/java/de/financer/controller/RecurringTransactionController.java +++ b/src/main/java/de/financer/controller/RecurringTransactionController.java @@ -57,16 +57,17 @@ public class RecurringTransactionController { ) { 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", decodedFrom, decodedTo, amount, description, holidayWeekendType, + "%s, %s, %s", decodedFrom, decodedTo, amount, decodedDesc, holidayWeekendType, intervalType, firstOccurrence, lastOccurrence)); } final ResponseReason responseReason = this.recurringTransactionService - .createRecurringTransaction(decodedFrom, decodedTo, amount, description, holidayWeekendType, + .createRecurringTransaction(decodedFrom, decodedTo, amount, decodedDesc, holidayWeekendType, intervalType, firstOccurrence, lastOccurrence); if (LOGGER.isDebugEnabled()) { diff --git a/src/main/java/de/financer/controller/TransactionController.java b/src/main/java/de/financer/controller/TransactionController.java index 61ab109..71de55a 100644 --- a/src/main/java/de/financer/controller/TransactionController.java +++ b/src/main/java/de/financer/controller/TransactionController.java @@ -43,15 +43,16 @@ public class TransactionController { ) { 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, description)); + decodedFrom, decodedTo, amount, date, decodedDesc)); } final ResponseReason responseReason = this.transactionService - .createTransaction(decodedFrom, decodedTo, amount, date, description); + .createTransaction(decodedFrom, decodedTo, amount, date, decodedDesc); if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format("/transactions/createTransaction returns with %s", responseReason.name())); diff --git a/src/main/java/de/financer/service/RecurringTransactionService.java b/src/main/java/de/financer/service/RecurringTransactionService.java index 4af4ce9..4c5a7bb 100644 --- a/src/main/java/de/financer/service/RecurringTransactionService.java +++ b/src/main/java/de/financer/service/RecurringTransactionService.java @@ -104,6 +104,19 @@ public class RecurringTransactionService { * @return true if the recurring transaction is due today, false 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)) { + return false; // early return + } + final boolean holiday = this.ruleService.isHoliday(now); final boolean dueToday = recurringTransaction.getFirstOccurrence() @@ -150,6 +163,12 @@ public class RecurringTransactionService { 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)) { + return false; // early return + } + boolean weekend; boolean holiday; LocalDate yesterday = now; From 31faf60da1a0e016954050056368e855411bcb1d Mon Sep 17 00:00:00 2001 From: MK13 Date: Tue, 14 May 2019 20:19:37 +0200 Subject: [PATCH 47/65] financer release version 4 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 423cdcc..73c1a81 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ de.77zzcx7.financer financer-server - 4-SNAPSHOT + 4 ${packaging.type} The server part of the financer application - a simple app to manage your personal finances financer-server From 7d1f915dd8c8d892491d0aeccf3274b21365ff69 Mon Sep 17 00:00:00 2001 From: MK13 Date: Tue, 14 May 2019 20:20:11 +0200 Subject: [PATCH 48/65] Prepare next development iteration for v5 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 73c1a81..b71ea44 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ de.77zzcx7.financer financer-server - 4 + 5-SNAPSHOT ${packaging.type} The server part of the financer application - a simple app to manage your personal finances financer-server @@ -29,7 +29,7 @@ - 000004 + 000005 From 8a62bb8bea3ad12d31c4e0216d6f20ab3d633204 Mon Sep 17 00:00:00 2001 From: MK13 Date: Sun, 19 May 2019 11:57:57 +0200 Subject: [PATCH 49/65] Fix a bug in handling of recurring transactions with HWT NEXT_WORKDAY If a recurring transaction has its firstOccurrence on a saturday it was falsely recognized as due today, if today was a sunday --- .../service/RecurringTransactionService.java | 6 +++ ...tAllDueToday_MONTHLY_NEXT_WORKDAYTest.java | 42 +++++++++++++++++-- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/main/java/de/financer/service/RecurringTransactionService.java b/src/main/java/de/financer/service/RecurringTransactionService.java index 4c5a7bb..b921365 100644 --- a/src/main/java/de/financer/service/RecurringTransactionService.java +++ b/src/main/java/de/financer/service/RecurringTransactionService.java @@ -169,6 +169,12 @@ public class RecurringTransactionService { 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)) { + return false; // early return + } + boolean weekend; boolean holiday; LocalDate yesterday = now; diff --git a/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest.java b/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest.java index db5f517..85d0d4b 100644 --- a/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest.java +++ b/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest.java @@ -47,7 +47,7 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest .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); + Mockito.when(this.ruleService.isHoliday(Mockito.any())).thenReturn(Boolean.FALSE, Boolean.FALSE, Boolean.TRUE); final LocalDate now = LocalDate.now(); // Act @@ -86,10 +86,10 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest .thenReturn(Collections.singletonList(createRecurringTransaction(-(now.getDayOfWeek().getValue() + 2)))); // First False for the dueToday check, 2x True for actual weekend, second False for Friday Mockito.when(this.ruleService.isWeekend(Mockito.any())) - .thenReturn(Boolean.FALSE, Boolean.TRUE, Boolean.TRUE, Boolean.FALSE); + .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.TRUE); + .thenReturn(Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, Boolean.TRUE); // Act final Iterable recurringDueToday = this.classUnderTest.getAllDueToday(monday); @@ -156,6 +156,31 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest Assert.assertEquals(1, IterableUtils.size(recurringDueToday)); } + /** + * This method tests whether a recurring transaction with firstOccurrence yesterday (a saturday) is not 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 recurringDueToday = this.classUnderTest.getAllDueToday(now); + + // Assert + Assert.assertEquals(0, IterableUtils.size(recurringDueToday)); + } + private RecurringTransaction createRecurringTransaction(int days) { final RecurringTransaction recurringTransaction = new RecurringTransaction(); @@ -167,4 +192,15 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest 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; + } } From 18440694ed4b378975f8f3dca97bcc23e7e398e3 Mon Sep 17 00:00:00 2001 From: MK13 Date: Sun, 19 May 2019 11:58:39 +0200 Subject: [PATCH 50/65] financer release version 5 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b71ea44..ed0f00f 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ de.77zzcx7.financer financer-server - 5-SNAPSHOT + 5 ${packaging.type} The server part of the financer application - a simple app to manage your personal finances financer-server From 4ae49df145b9307a7584177736fa335a3e36bb41 Mon Sep 17 00:00:00 2001 From: MK13 Date: Sun, 19 May 2019 11:59:30 +0200 Subject: [PATCH 51/65] Prepare next development iteration for v6 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index ed0f00f..7749fcc 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ de.77zzcx7.financer financer-server - 5 + 6-SNAPSHOT ${packaging.type} The server part of the financer application - a simple app to manage your personal finances financer-server @@ -29,7 +29,7 @@ - 000005 + 000006 From 52525ff0d1db88f114494025fa90fd71f5aee041 Mon Sep 17 00:00:00 2001 From: MK13 Date: Mon, 10 Jun 2019 02:29:43 +0200 Subject: [PATCH 52/65] Add flag to control whether to remind about maturity of rec. transaction --- .gitignore | 2 - .../RecurringTransactionController.java | 8 ++-- .../financer/model/RecurringTransaction.java | 9 ++++ .../service/RecurringTransactionService.java | 15 +++++-- .../SendRecurringTransactionReminderTask.java | 10 ++++- ...V6_0_0__remindFlagRecurringTransaction.sql | 4 ++ ...V6_0_0__remindFlagRecurringTransaction.sql | 4 ++ .../database/postgres/readme_V1_0_0__init.txt | 25 +++++++++++ ...teRecurringTransactionIntegrationTest.java | 3 +- ...ervice_createRecurringTransactionTest.java | 45 ++++++++++++------- ...dRecurringTransactionReminderTaskTest.java | 10 +++-- 11 files changed, 104 insertions(+), 31 deletions(-) delete mode 100644 .gitignore create mode 100644 src/main/resources/database/hsqldb/V6_0_0__remindFlagRecurringTransaction.sql create mode 100644 src/main/resources/database/postgres/V6_0_0__remindFlagRecurringTransaction.sql create mode 100644 src/main/resources/database/postgres/readme_V1_0_0__init.txt diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 9f363e8..0000000 --- a/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -financer-server.log* -.attach* \ No newline at end of file diff --git a/src/main/java/de/financer/controller/RecurringTransactionController.java b/src/main/java/de/financer/controller/RecurringTransactionController.java index 6c36465..91738b7 100644 --- a/src/main/java/de/financer/controller/RecurringTransactionController.java +++ b/src/main/java/de/financer/controller/RecurringTransactionController.java @@ -53,7 +53,7 @@ public class RecurringTransactionController { public ResponseEntity createRecurringTransaction(String fromAccountKey, String toAccountKey, Long amount, String description, String holidayWeekendType, String intervalType, String firstOccurrence, - String lastOccurrence + String lastOccurrence, Boolean remind ) { final String decodedFrom = ControllerUtil.urlDecode(fromAccountKey); final String decodedTo = ControllerUtil.urlDecode(toAccountKey); @@ -62,13 +62,13 @@ public class RecurringTransactionController { if (LOGGER.isDebugEnabled()) { LOGGER.debug(String .format("/recurringTransactions/createRecurringTransaction got parameters: %s, %s, %s, %s, %s, " + - "%s, %s, %s", decodedFrom, decodedTo, amount, decodedDesc, holidayWeekendType, - intervalType, firstOccurrence, lastOccurrence)); + "%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); + intervalType, firstOccurrence, lastOccurrence, remind); if (LOGGER.isDebugEnabled()) { LOGGER.debug(String diff --git a/src/main/java/de/financer/model/RecurringTransaction.java b/src/main/java/de/financer/model/RecurringTransaction.java index 3df99c8..135fabe 100644 --- a/src/main/java/de/financer/model/RecurringTransaction.java +++ b/src/main/java/de/financer/model/RecurringTransaction.java @@ -21,6 +21,7 @@ public class RecurringTransaction { @Enumerated(EnumType.STRING) private HolidayWeekendType holidayWeekendType; private boolean deleted; + private boolean remind; public Long getId() { return id; @@ -97,4 +98,12 @@ public class RecurringTransaction { public void setDeleted(boolean deleted) { this.deleted = deleted; } + + public boolean isRemind() { + return remind; + } + + public void setRemind(boolean remind) { + this.remind = remind; + } } diff --git a/src/main/java/de/financer/service/RecurringTransactionService.java b/src/main/java/de/financer/service/RecurringTransactionService.java index b921365..38fd3ad 100644 --- a/src/main/java/de/financer/service/RecurringTransactionService.java +++ b/src/main/java/de/financer/service/RecurringTransactionService.java @@ -8,6 +8,7 @@ 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.math.NumberUtils; import org.slf4j.Logger; @@ -261,16 +262,18 @@ public class RecurringTransactionService { 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 + 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); + 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) { @@ -279,7 +282,7 @@ public class RecurringTransactionService { try { final RecurringTransaction transaction = buildRecurringTransaction(fromAccount, toAccount, amount, - description, holidayWeekendType, intervalType, firstOccurrence, lastOccurrence); + description, holidayWeekendType, intervalType, firstOccurrence, lastOccurrence, remind); this.recurringTransactionRepository.save(transaction); @@ -303,13 +306,14 @@ public class RecurringTransactionService { * @param intervalType the interval type * @param firstOccurrence the first occurrence * @param lastOccurrence the last occurrence, may be null + * @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 + String lastOccurrence, Boolean remind ) { final RecurringTransaction recurringTransaction = new RecurringTransaction(); @@ -321,6 +325,9 @@ public class RecurringTransactionService { 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)) { diff --git a/src/main/java/de/financer/task/SendRecurringTransactionReminderTask.java b/src/main/java/de/financer/task/SendRecurringTransactionReminderTask.java index f534e7c..48cf729 100644 --- a/src/main/java/de/financer/task/SendRecurringTransactionReminderTask.java +++ b/src/main/java/de/financer/task/SendRecurringTransactionReminderTask.java @@ -13,6 +13,8 @@ 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 { @@ -33,7 +35,7 @@ public class SendRecurringTransactionReminderTask { LOGGER.debug("Enter recurring transaction reminder task"); } - final Iterable recurringTransactions = this.recurringTransactionService.getAllDueToday(); + Iterable recurringTransactions = this.recurringTransactionService.getAllDueToday(); // If no recurring transaction is due today we don't need to send a reminder if (IterableUtils.isEmpty(recurringTransactions)) { @@ -42,6 +44,12 @@ public class SendRecurringTransactionReminderTask { 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))); diff --git a/src/main/resources/database/hsqldb/V6_0_0__remindFlagRecurringTransaction.sql b/src/main/resources/database/hsqldb/V6_0_0__remindFlagRecurringTransaction.sql new file mode 100644 index 0000000..a9410ad --- /dev/null +++ b/src/main/resources/database/hsqldb/V6_0_0__remindFlagRecurringTransaction.sql @@ -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; \ No newline at end of file diff --git a/src/main/resources/database/postgres/V6_0_0__remindFlagRecurringTransaction.sql b/src/main/resources/database/postgres/V6_0_0__remindFlagRecurringTransaction.sql new file mode 100644 index 0000000..aa29189 --- /dev/null +++ b/src/main/resources/database/postgres/V6_0_0__remindFlagRecurringTransaction.sql @@ -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 \ No newline at end of file diff --git a/src/main/resources/database/postgres/readme_V1_0_0__init.txt b/src/main/resources/database/postgres/readme_V1_0_0__init.txt new file mode 100644 index 0000000..8d853b0 --- /dev/null +++ b/src/main/resources/database/postgres/readme_V1_0_0__init.txt @@ -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() \ No newline at end of file diff --git a/src/test/java/de/financer/controller/integration/RecurringTransactionService_createRecurringTransactionIntegrationTest.java b/src/test/java/de/financer/controller/integration/RecurringTransactionService_createRecurringTransactionIntegrationTest.java index c64e6ae..1b7cf20 100644 --- a/src/test/java/de/financer/controller/integration/RecurringTransactionService_createRecurringTransactionIntegrationTest.java +++ b/src/test/java/de/financer/controller/integration/RecurringTransactionService_createRecurringTransactionIntegrationTest.java @@ -42,7 +42,8 @@ public class RecurringTransactionService_createRecurringTransactionIntegrationTe .param("description", "Monthly rent") .param("holidayWeekendType", "SAME_DAY") .param("intervalType", "MONTHLY") - .param("firstOccurrence", "07.03.2019")) + .param("firstOccurrence", "07.03.2019") + .param("remind", "true")) .andExpect(status().isOk()) .andReturn(); diff --git a/src/test/java/de/financer/service/RecurringTransactionService_createRecurringTransactionTest.java b/src/test/java/de/financer/service/RecurringTransactionService_createRecurringTransactionTest.java index c551797..c34cf0d 100644 --- a/src/test/java/de/financer/service/RecurringTransactionService_createRecurringTransactionTest.java +++ b/src/test/java/de/financer/service/RecurringTransactionService_createRecurringTransactionTest.java @@ -51,7 +51,8 @@ public class RecurringTransactionService_createRecurringTransactionTest { "HOLIDAY_WEEKEND_TYPE", "INTERVAL_TYPE", "FIRST_OCCURRENCE", - "LAST_OCCURRENCE"); + "LAST_OCCURRENCE", + Boolean.TRUE); // Assert Assert.assertEquals(ResponseReason.FROM_AND_TO_ACCOUNT_NOT_FOUND, response); @@ -70,7 +71,8 @@ public class RecurringTransactionService_createRecurringTransactionTest { "HOLIDAY_WEEKEND_TYPE", "INTERVAL_TYPE", "FIRST_OCCURRENCE", - "LAST_OCCURRENCE"); + "LAST_OCCURRENCE", + Boolean.TRUE); // Assert Assert.assertEquals(ResponseReason.TO_ACCOUNT_NOT_FOUND, response); @@ -89,7 +91,8 @@ public class RecurringTransactionService_createRecurringTransactionTest { "HOLIDAY_WEEKEND_TYPE", "INTERVAL_TYPE", "FIRST_OCCURRENCE", - "LAST_OCCURRENCE"); + "LAST_OCCURRENCE", + Boolean.TRUE); // Assert Assert.assertEquals(ResponseReason.FROM_ACCOUNT_NOT_FOUND, response); @@ -109,7 +112,8 @@ public class RecurringTransactionService_createRecurringTransactionTest { "HOLIDAY_WEEKEND_TYPE", "INTERVAL_TYPE", "FIRST_OCCURRENCE", - "LAST_OCCURRENCE"); + "LAST_OCCURRENCE", + Boolean.TRUE); // Assert Assert.assertEquals(ResponseReason.INVALID_BOOKING_ACCOUNTS, response); @@ -129,7 +133,8 @@ public class RecurringTransactionService_createRecurringTransactionTest { "HOLIDAY_WEEKEND_TYPE", "INTERVAL_TYPE", "FIRST_OCCURRENCE", - "LAST_OCCURRENCE"); + "LAST_OCCURRENCE", + Boolean.TRUE); // Assert Assert.assertEquals(ResponseReason.MISSING_AMOUNT, response); @@ -149,7 +154,8 @@ public class RecurringTransactionService_createRecurringTransactionTest { "HOLIDAY_WEEKEND_TYPE", "INTERVAL_TYPE", "FIRST_OCCURRENCE", - "LAST_OCCURRENCE"); + "LAST_OCCURRENCE", + Boolean.TRUE); // Assert Assert.assertEquals(ResponseReason.AMOUNT_ZERO, response); @@ -169,7 +175,8 @@ public class RecurringTransactionService_createRecurringTransactionTest { null, "INTERVAL_TYPE", "FIRST_OCCURRENCE", - "LAST_OCCURRENCE"); + "LAST_OCCURRENCE", + Boolean.TRUE); // Assert Assert.assertEquals(ResponseReason.MISSING_HOLIDAY_WEEKEND_TYPE, response); @@ -189,7 +196,8 @@ public class RecurringTransactionService_createRecurringTransactionTest { "HOLIDAY_WEEKEND_TYPE", "INTERVAL_TYPE", "FIRST_OCCURRENCE", - "LAST_OCCURRENCE"); + "LAST_OCCURRENCE", + Boolean.TRUE); // Assert Assert.assertEquals(ResponseReason.INVALID_HOLIDAY_WEEKEND_TYPE, response); @@ -209,7 +217,8 @@ public class RecurringTransactionService_createRecurringTransactionTest { HolidayWeekendType.SAME_DAY.name(), null, "FIRST_OCCURRENCE", - "LAST_OCCURRENCE"); + "LAST_OCCURRENCE", + Boolean.TRUE); // Assert Assert.assertEquals(ResponseReason.MISSING_INTERVAL_TYPE, response); @@ -229,7 +238,8 @@ public class RecurringTransactionService_createRecurringTransactionTest { HolidayWeekendType.SAME_DAY.name(), "INTERVAL_TYPE", "FIRST_OCCURRENCE", - "LAST_OCCURRENCE"); + "LAST_OCCURRENCE", + Boolean.TRUE); // Assert Assert.assertEquals(ResponseReason.INVALID_INTERVAL_TYPE, response); @@ -249,7 +259,8 @@ public class RecurringTransactionService_createRecurringTransactionTest { HolidayWeekendType.SAME_DAY.name(), IntervalType.DAILY.name(), null, - "LAST_OCCURRENCE"); + "LAST_OCCURRENCE", + Boolean.TRUE); // Assert Assert.assertEquals(ResponseReason.MISSING_FIRST_OCCURRENCE, response); @@ -269,7 +280,8 @@ public class RecurringTransactionService_createRecurringTransactionTest { HolidayWeekendType.SAME_DAY.name(), IntervalType.DAILY.name(), "FIRST_OCCURRENCE", - "LAST_OCCURRENCE"); + "LAST_OCCURRENCE", + Boolean.TRUE); // Assert Assert.assertEquals(ResponseReason.INVALID_FIRST_OCCURRENCE_FORMAT, response); @@ -289,7 +301,8 @@ public class RecurringTransactionService_createRecurringTransactionTest { HolidayWeekendType.SAME_DAY.name(), IntervalType.DAILY.name(), "07.03.2019", - "LAST_OCCURRENCE"); + "LAST_OCCURRENCE", + Boolean.TRUE); // Assert Assert.assertEquals(ResponseReason.INVALID_LAST_OCCURRENCE_FORMAT, response); @@ -310,7 +323,8 @@ public class RecurringTransactionService_createRecurringTransactionTest { HolidayWeekendType.SAME_DAY.name(), IntervalType.DAILY.name(), "07.03.2019", - null); + null, + Boolean.TRUE); // Assert Assert.assertEquals(ResponseReason.UNKNOWN_ERROR, response); @@ -330,7 +344,8 @@ public class RecurringTransactionService_createRecurringTransactionTest { HolidayWeekendType.SAME_DAY.name(), IntervalType.DAILY.name(), "07.03.2019", - null); + null, + Boolean.TRUE); // Assert Assert.assertEquals(ResponseReason.OK, response); diff --git a/src/test/java/de/financer/task/SendRecurringTransactionReminderTaskTest.java b/src/test/java/de/financer/task/SendRecurringTransactionReminderTaskTest.java index 844178d..215f2fc 100644 --- a/src/test/java/de/financer/task/SendRecurringTransactionReminderTaskTest.java +++ b/src/test/java/de/financer/task/SendRecurringTransactionReminderTaskTest.java @@ -36,9 +36,10 @@ public class SendRecurringTransactionReminderTaskTest { public void test_sendReminder() { // Arrange final Collection recurringTransactions = Arrays.asList( - createRecurringTransaction("Test booking 1", "Income", "accounts.bank", Long.valueOf(250000)), - createRecurringTransaction("Test booking 2", "Bank", "accounts.rent", Long.valueOf(41500)), - createRecurringTransaction("Test booking 3", "Bank", "accounts.cash", Long.valueOf(5000)) + 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); @@ -51,13 +52,14 @@ public class SendRecurringTransactionReminderTaskTest { Mockito.verify(this.mailSender, Mockito.times(1)).send(Mockito.any(SimpleMailMessage.class)); } - private RecurringTransaction createRecurringTransaction(String description, String fromAccountKey, String toAccountKey, Long amount) { + 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; } From adf6573429d863ebb3d92c76443e7cc7684ef468 Mon Sep 17 00:00:00 2001 From: MK13 Date: Mon, 10 Jun 2019 12:40:49 +0200 Subject: [PATCH 53/65] Add parent module financer-parent Rework POM structure: - Move general profiles to new parent - Add dependency management for project dependencies - Move general properties to new parent - Alo remove version numbers from child modules, so versioning is now done via parent --- pom.xml | 66 ++++----------------------------------------------------- 1 file changed, 4 insertions(+), 62 deletions(-) diff --git a/pom.xml b/pom.xml index 7749fcc..d0c9888 100644 --- a/pom.xml +++ b/pom.xml @@ -5,31 +5,21 @@ 4.0.0 - org.springframework.boot - spring-boot-starter-parent - 2.1.2.RELEASE - + de.77zzcx7.financer + financer-parent + 6-SNAPSHOT + ../financer-parent de.77zzcx7.financer financer-server - - 6-SNAPSHOT ${packaging.type} The server part of the financer application - a simple app to manage your personal finances financer-server - UTF-8 - 1.9 - 1.9 - 1.9 jar hsqldb,dev - - 000006 @@ -57,7 +47,6 @@ org.apache.commons commons-collections4 - 4.3 @@ -96,30 +85,10 @@ junit junit - 4.12 test - - ${project.artifactId} - - - org.springframework.boot - spring-boot-maven-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - - - **/*IntegrationTest - - - - - - @@ -134,33 +103,6 @@ - - integration-tests - - - - maven-surefire-plugin - - - integration-test - - test - - - - none - - - **/*IntegrationTest - - - - - - - - - build-war From 781f0e0ac9ab4c0c24194afa3f2b2b798cf92acb Mon Sep 17 00:00:00 2001 From: MK13 Date: Mon, 10 Jun 2019 21:25:54 +0200 Subject: [PATCH 54/65] Re-add .gitignore file --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e831c8c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +financer-server.log* +.attach* +*.iml \ No newline at end of file From 6b748489dcc134e19b9c2af4c3a7e70cfa7d9bc3 Mon Sep 17 00:00:00 2001 From: MK13 Date: Mon, 10 Jun 2019 22:02:30 +0200 Subject: [PATCH 55/65] Use JPA query to query candidates for recurring transactions due today Previously the query was autogenerated but the WHERE clause was wrong, because the braces where at the wrong places and thus the query matched wrong data. --- .../java/de/financer/dba/RecurringTransactionRepository.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/de/financer/dba/RecurringTransactionRepository.java b/src/main/java/de/financer/dba/RecurringTransactionRepository.java index 5226f92..712bff2 100644 --- a/src/main/java/de/financer/dba/RecurringTransactionRepository.java +++ b/src/main/java/de/financer/dba/RecurringTransactionRepository.java @@ -2,7 +2,9 @@ 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; @@ -12,6 +14,7 @@ import java.time.LocalDate; public interface RecurringTransactionRepository extends CrudRepository { Iterable 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 findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(LocalDate lastOccurrence); Iterable findByDeletedFalse(); From 2fb63d757ebd2829006c45c25b8dc4e8e5932214 Mon Sep 17 00:00:00 2001 From: MK13 Date: Mon, 10 Jun 2019 22:21:59 +0200 Subject: [PATCH 56/65] financer release version 6 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d0c9888..d55dee7 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ de.77zzcx7.financer financer-parent - 6-SNAPSHOT + 6 ../financer-parent From 735a83107f36871e674a07d507bd8017d1939b5f Mon Sep 17 00:00:00 2001 From: MK13 Date: Mon, 10 Jun 2019 22:23:47 +0200 Subject: [PATCH 57/65] Prepare next development iteration for v7 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d55dee7..6891380 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ de.77zzcx7.financer financer-parent - 6 + 7-SNAPSHOT ../financer-parent From 5e12dbeb2b03d51b499ad98979956dd7f33c8b51 Mon Sep 17 00:00:00 2001 From: MK13 Date: Sat, 15 Jun 2019 23:42:16 +0200 Subject: [PATCH 58/65] Bug fix for recurring transactions with HWT 'PREVIOUS_WORKDAY' Fix the actual bug and add a test case (test case also contains a more detailed description) --- .../service/RecurringTransactionService.java | 3 +- ...DueToday_MONTHLY_PREVIOUS_WORKDAYTest.java | 28 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/financer/service/RecurringTransactionService.java b/src/main/java/de/financer/service/RecurringTransactionService.java index 38fd3ad..d41bbc9 100644 --- a/src/main/java/de/financer/service/RecurringTransactionService.java +++ b/src/main/java/de/financer/service/RecurringTransactionService.java @@ -137,7 +137,8 @@ public class RecurringTransactionService { if (holiday || weekend) { - defer = recurringTransaction.getHolidayWeekendType() == HolidayWeekendType.NEXT_WORKDAY; + defer = recurringTransaction.getHolidayWeekendType() == HolidayWeekendType.NEXT_WORKDAY + || recurringTransaction.getHolidayWeekendType() == HolidayWeekendType.PREVIOUS_WORKDAY; } return !defer && dueToday; diff --git a/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_PREVIOUS_WORKDAYTest.java b/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_PREVIOUS_WORKDAYTest.java index 2a17604..cc8609e 100644 --- a/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_PREVIOUS_WORKDAYTest.java +++ b/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_PREVIOUS_WORKDAYTest.java @@ -56,6 +56,34 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_PREVIOUS_WORKDAY 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 recurringDueToday = this.classUnderTest.getAllDueToday(now); + + // Assert + Assert.assertEquals(0, IterableUtils.size(recurringDueToday)); + } + private RecurringTransaction createRecurringTransaction(int days) { final RecurringTransaction recurringTransaction = new RecurringTransaction(); From f5fac223472bc21f3e27b99200b9d8ca85893172 Mon Sep 17 00:00:00 2001 From: MK13 Date: Sun, 16 Jun 2019 01:34:55 +0200 Subject: [PATCH 59/65] Introduce new feature 'Account groups' Account groups are a simple way to group accounts. Currently the design of the group is very sparse and supports only a name. However, as this feature is a stepping stone for the much bigger 'Reports' feature this will most likely be subject to change. With this change new accounts are also required to get assigned to a group. --- src/main/java/de/financer/ResponseReason.java | 4 +- .../controller/AccountController.java | 9 +-- .../controller/AccountGroupController.java | 53 ++++++++++++++ .../financer/dba/AccountGroupRepository.java | 11 +++ src/main/java/de/financer/model/Account.java | 10 +++ .../java/de/financer/model/AccountGroup.java | 23 +++++++ .../financer/service/AccountGroupService.java | 69 +++++++++++++++++++ .../de/financer/service/AccountService.java | 27 ++++++-- .../common/V7_0_1__initAccountGroups.sql | 17 +++++ .../database/hsqldb/V7_0_0__accountGroup.sql | 16 +++++ .../postgres/V7_0_0__accountGroup.sql | 16 +++++ ...ntGroupService_createAccountGroupTest.java | 62 +++++++++++++++++ .../AccountService_createAccountTest.java | 36 +++++++++- 13 files changed, 340 insertions(+), 13 deletions(-) create mode 100644 src/main/java/de/financer/controller/AccountGroupController.java create mode 100644 src/main/java/de/financer/dba/AccountGroupRepository.java create mode 100644 src/main/java/de/financer/model/AccountGroup.java create mode 100644 src/main/java/de/financer/service/AccountGroupService.java create mode 100644 src/main/resources/database/common/V7_0_1__initAccountGroups.sql create mode 100644 src/main/resources/database/hsqldb/V7_0_0__accountGroup.sql create mode 100644 src/main/resources/database/postgres/V7_0_0__accountGroup.sql create mode 100644 src/test/java/de/financer/service/AccountGroupService_createAccountGroupTest.java diff --git a/src/main/java/de/financer/ResponseReason.java b/src/main/java/de/financer/ResponseReason.java index bd1b02e..e5877cb 100644 --- a/src/main/java/de/financer/ResponseReason.java +++ b/src/main/java/de/financer/ResponseReason.java @@ -29,7 +29,9 @@ public enum ResponseReason { 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_KEY(HttpStatus.INTERNAL_SERVER_ERROR), + DUPLICATE_ACCOUNT_GROUP_NAME(HttpStatus.INTERNAL_SERVER_ERROR), + ACCOUNT_GROUP_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR); private HttpStatus httpStatus; diff --git a/src/main/java/de/financer/controller/AccountController.java b/src/main/java/de/financer/controller/AccountController.java index 9c37805..46b0b04 100644 --- a/src/main/java/de/financer/controller/AccountController.java +++ b/src/main/java/de/financer/controller/AccountController.java @@ -24,7 +24,7 @@ public class AccountController { final String decoded = ControllerUtil.urlDecode(key); if (LOGGER.isDebugEnabled()) { - LOGGER.debug(String.format("/accounts/getAccountByKey got parameter: %s", decoded)); + LOGGER.debug(String.format("/accounts/getByKey got parameter: %s", decoded)); } return this.accountService.getAccountByKey(decoded); @@ -36,14 +36,15 @@ public class AccountController { } @RequestMapping("createAccount") - public ResponseEntity createAccount(String key, String type) { + 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", decoded, type)); + LOGGER.debug(String.format("/accounts/createAccount got parameters: %s, %s, %s", decoded, type, decodedGroup)); } - final ResponseReason responseReason = this.accountService.createAccount(decoded, type); + final ResponseReason responseReason = this.accountService.createAccount(decoded, type, decodedGroup); if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format("/accounts/createAccount returns with %s", responseReason.name())); diff --git a/src/main/java/de/financer/controller/AccountGroupController.java b/src/main/java/de/financer/controller/AccountGroupController.java new file mode 100644 index 0000000..109f9ce --- /dev/null +++ b/src/main/java/de/financer/controller/AccountGroupController.java @@ -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 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(); + } +} diff --git a/src/main/java/de/financer/dba/AccountGroupRepository.java b/src/main/java/de/financer/dba/AccountGroupRepository.java new file mode 100644 index 0000000..144cb66 --- /dev/null +++ b/src/main/java/de/financer/dba/AccountGroupRepository.java @@ -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 findByName(String name); +} diff --git a/src/main/java/de/financer/model/Account.java b/src/main/java/de/financer/model/Account.java index b9c8eea..732b947 100644 --- a/src/main/java/de/financer/model/Account.java +++ b/src/main/java/de/financer/model/Account.java @@ -14,6 +14,8 @@ public class Account { @Enumerated(EnumType.STRING) private AccountStatus status; private Long currentBalance; + @ManyToOne + private AccountGroup accountGroup; public Long getId() { return id; @@ -50,4 +52,12 @@ public class Account { public void setCurrentBalance(Long currentBalance) { this.currentBalance = currentBalance; } + + public AccountGroup getAccountGroup() { + return accountGroup; + } + + public void setAccountGroup(AccountGroup accountGroup) { + this.accountGroup = accountGroup; + } } diff --git a/src/main/java/de/financer/model/AccountGroup.java b/src/main/java/de/financer/model/AccountGroup.java new file mode 100644 index 0000000..1ec6d15 --- /dev/null +++ b/src/main/java/de/financer/model/AccountGroup.java @@ -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; + } +} diff --git a/src/main/java/de/financer/service/AccountGroupService.java b/src/main/java/de/financer/service/AccountGroupService.java new file mode 100644 index 0000000..1b41de9 --- /dev/null +++ b/src/main/java/de/financer/service/AccountGroupService.java @@ -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 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 null 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 null. + */ + @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; + } +} diff --git a/src/main/java/de/financer/service/AccountService.java b/src/main/java/de/financer/service/AccountService.java index 67deea5..9e4559a 100644 --- a/src/main/java/de/financer/service/AccountService.java +++ b/src/main/java/de/financer/service/AccountService.java @@ -3,6 +3,7 @@ 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; @@ -23,6 +24,8 @@ public class AccountService { @Autowired private AccountRepository accountRepository; + @Autowired + private AccountGroupService accountGroupService; /** * This method returns the account identified by the given key. @@ -58,18 +61,32 @@ public class AccountService { * * @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 null * @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 and - * {@link ResponseReason#OK} if the operation completed successfully. Never returns null. + * {@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 + * accountGroupName does not identify a valid account group. Never returns null. */ @Transactional(propagation = Propagation.SUPPORTS) - public ResponseReason createAccount(String key, String type) { + 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 @@ -81,12 +98,12 @@ public class AccountService { this.accountRepository.save(account); } catch (DataIntegrityViolationException dive) { - LOGGER.error(String.format("Duplicate key! %s|%s", key, type), 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", key, type), e); + LOGGER.error(String.format("Could not save account %s|%s|%s", key, type, accountGroupName), e); return ResponseReason.UNKNOWN_ERROR; } diff --git a/src/main/resources/database/common/V7_0_1__initAccountGroups.sql b/src/main/resources/database/common/V7_0_1__initAccountGroups.sql new file mode 100644 index 0000000..ac3fa07 --- /dev/null +++ b/src/main/resources/database/common/V7_0_1__initAccountGroups.sql @@ -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'); \ No newline at end of file diff --git a/src/main/resources/database/hsqldb/V7_0_0__accountGroup.sql b/src/main/resources/database/hsqldb/V7_0_0__accountGroup.sql new file mode 100644 index 0000000..769184b --- /dev/null +++ b/src/main/resources/database/hsqldb/V7_0_0__accountGroup.sql @@ -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); \ No newline at end of file diff --git a/src/main/resources/database/postgres/V7_0_0__accountGroup.sql b/src/main/resources/database/postgres/V7_0_0__accountGroup.sql new file mode 100644 index 0000000..5d34e0f --- /dev/null +++ b/src/main/resources/database/postgres/V7_0_0__accountGroup.sql @@ -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); \ No newline at end of file diff --git a/src/test/java/de/financer/service/AccountGroupService_createAccountGroupTest.java b/src/test/java/de/financer/service/AccountGroupService_createAccountGroupTest.java new file mode 100644 index 0000000..9f097db --- /dev/null +++ b/src/test/java/de/financer/service/AccountGroupService_createAccountGroupTest.java @@ -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); + } +} diff --git a/src/test/java/de/financer/service/AccountService_createAccountTest.java b/src/test/java/de/financer/service/AccountService_createAccountTest.java index ed4e007..400b77a 100644 --- a/src/test/java/de/financer/service/AccountService_createAccountTest.java +++ b/src/test/java/de/financer/service/AccountService_createAccountTest.java @@ -3,6 +3,7 @@ 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; @@ -11,12 +12,16 @@ 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; @@ -26,7 +31,7 @@ public class AccountService_createAccountTest { // Nothing to do // Act - ResponseReason response = this.classUnderTest.createAccount(null, null); + ResponseReason response = this.classUnderTest.createAccount(null, null, null); // Assert Assert.assertEquals(ResponseReason.INVALID_ACCOUNT_TYPE, response); @@ -38,7 +43,7 @@ public class AccountService_createAccountTest { Mockito.doThrow(new NullPointerException()).when(this.accountRepository).save(Mockito.any(Account.class)); // Act - ResponseReason response = this.classUnderTest.createAccount("Test", "BANK"); + ResponseReason response = this.classUnderTest.createAccount("Test", "BANK", null); // Assert Assert.assertEquals(ResponseReason.UNKNOWN_ERROR, response); @@ -50,11 +55,36 @@ public class AccountService_createAccountTest { // Nothing to do // Act - ResponseReason response = this.classUnderTest.createAccount("Test", "BANK"); + 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); + } } From f08f987fd0dfcb81f10ae4913bf0a1fe78e8e5bd Mon Sep 17 00:00:00 2001 From: MK13 Date: Sun, 16 Jun 2019 01:43:54 +0200 Subject: [PATCH 60/65] financer release version 7 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6891380..9807a52 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ de.77zzcx7.financer financer-parent - 7-SNAPSHOT + 7 ../financer-parent From 5356bb209fcd44a535fbc5ce5b855abd5e6bc61a Mon Sep 17 00:00:00 2001 From: MK13 Date: Sun, 16 Jun 2019 01:44:37 +0200 Subject: [PATCH 61/65] Prepare next development iteration for v8 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 9807a52..2c5f4bc 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ de.77zzcx7.financer financer-parent - 7 + 8-SNAPSHOT ../financer-parent From 4182c38656198f954a9fd797a0245e888bce5c8c Mon Sep 17 00:00:00 2001 From: MK13 Date: Sun, 16 Jun 2019 11:47:45 +0200 Subject: [PATCH 62/65] Fix a bug in the rec. trx. service if it has HWT 'NW' and LO on a weekend Adjust it so that recurring transactions that have their last occurrence (LO) on a weekend or a holiday in the near past and HWT NEXT_WORKDAY (NW) 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)'. Also add a unit test case for this. Further add better logging. --- .../service/RecurringTransactionService.java | 40 ++++++++++++++++++- ...tAllDueToday_MONTHLY_NEXT_WORKDAYTest.java | 26 ++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/financer/service/RecurringTransactionService.java b/src/main/java/de/financer/service/RecurringTransactionService.java index d41bbc9..a9bb2b3 100644 --- a/src/main/java/de/financer/service/RecurringTransactionService.java +++ b/src/main/java/de/financer/service/RecurringTransactionService.java @@ -10,6 +10,7 @@ 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; @@ -79,8 +80,15 @@ public class RecurringTransactionService { // Visible for unit tests /* package */ Iterable 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 allRecurringTransactions = this.recurringTransactionRepository - .findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(now); + .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() @@ -115,6 +123,10 @@ public class RecurringTransactionService { // 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 } @@ -141,6 +153,9 @@ public class RecurringTransactionService { || 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; } @@ -162,18 +177,31 @@ public class RecurringTransactionService { 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 } @@ -207,6 +235,9 @@ public class RecurringTransactionService { } while (holiday || weekend); + LOGGER.debug(String.format("Recurring transaction %s is due in the past? %s", + ReflectionToStringBuilder.toString(recurringTransaction), due)); + return due; } @@ -227,6 +258,10 @@ public class RecurringTransactionService { 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 } @@ -260,6 +295,9 @@ public class RecurringTransactionService { } while (holiday || weekend); + LOGGER.debug(String.format("Recurring transaction %s is due in the future? %s", + ReflectionToStringBuilder.toString(recurringTransaction), due)); + return due; } diff --git a/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest.java b/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest.java index 85d0d4b..83733ba 100644 --- a/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest.java +++ b/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest.java @@ -181,6 +181,32 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest 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 recurringDueToday = this.classUnderTest.getAllDueToday(now); + + // Assert + Assert.assertEquals(1, IterableUtils.size(recurringDueToday)); + } + private RecurringTransaction createRecurringTransaction(int days) { final RecurringTransaction recurringTransaction = new RecurringTransaction(); From d55ddb080725a982ba6ec2b3ac1da8fec8cb899d Mon Sep 17 00:00:00 2001 From: MK13 Date: Sun, 16 Jun 2019 20:25:59 +0200 Subject: [PATCH 63/65] financer release version 8 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 2c5f4bc..fcb5bef 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ de.77zzcx7.financer financer-parent - 8-SNAPSHOT + 8 ../financer-parent From c5734f38c290597f49ba1c8b6b54fbd7f67f92e0 Mon Sep 17 00:00:00 2001 From: MK13 Date: Sun, 16 Jun 2019 20:26:28 +0200 Subject: [PATCH 64/65] Prepare next development iteration for v9 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index fcb5bef..b68ab8c 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ de.77zzcx7.financer financer-parent - 8 + 9-SNAPSHOT ../financer-parent From ff2ea0c68deaa28e8bf33f3edc2c40cf2b0c5f57 Mon Sep 17 00:00:00 2001 From: MK13 Date: Thu, 20 Jun 2019 14:29:33 +0200 Subject: [PATCH 65/65] Move content to financer-server/ subfolder as preparation for repository merge --- .gitignore => financer-server/.gitignore | 0 {doc => financer-server/doc}/README | 0 pom.xml => financer-server/pom.xml | 0 .../src}/main/java/de/financer/FinancerApplication.java | 0 .../src}/main/java/de/financer/ResponseReason.java | 0 .../src}/main/java/de/financer/config/FinancerConfig.java | 0 .../src}/main/java/de/financer/controller/AccountController.java | 0 .../main/java/de/financer/controller/AccountGroupController.java | 0 .../src}/main/java/de/financer/controller/ControllerUtil.java | 0 .../de/financer/controller/RecurringTransactionController.java | 0 .../main/java/de/financer/controller/TransactionController.java | 0 .../src}/main/java/de/financer/dba/AccountGroupRepository.java | 0 .../src}/main/java/de/financer/dba/AccountRepository.java | 0 .../main/java/de/financer/dba/RecurringTransactionRepository.java | 0 .../src}/main/java/de/financer/dba/TransactionRepository.java | 0 .../src}/main/java/de/financer/model/Account.java | 0 .../src}/main/java/de/financer/model/AccountGroup.java | 0 .../src}/main/java/de/financer/model/AccountStatus.java | 0 .../src}/main/java/de/financer/model/AccountType.java | 0 .../src}/main/java/de/financer/model/HolidayWeekendType.java | 0 .../src}/main/java/de/financer/model/IntervalType.java | 0 .../src}/main/java/de/financer/model/RecurringTransaction.java | 0 .../src}/main/java/de/financer/model/Transaction.java | 0 .../src}/main/java/de/financer/model/package-info.java | 0 .../src}/main/java/de/financer/service/AccountGroupService.java | 0 .../src}/main/java/de/financer/service/AccountService.java | 0 .../java/de/financer/service/RecurringTransactionService.java | 0 .../src}/main/java/de/financer/service/RuleService.java | 0 .../src}/main/java/de/financer/service/TransactionService.java | 0 .../src}/main/java/de/financer/service/package-info.java | 0 .../de/financer/task/SendRecurringTransactionReminderTask.java | 0 .../src}/main/resources/config/application-dev.properties | 0 .../src}/main/resources/config/application-hsqldb.properties | 0 .../src}/main/resources/config/application-postgres.properties | 0 .../src}/main/resources/config/application.properties | 0 .../src}/main/resources/database/common/V1_0_1__initData.sql | 0 .../src}/main/resources/database/common/V3_0_0__accountRename.sql | 0 .../main/resources/database/common/V7_0_1__initAccountGroups.sql | 0 .../src}/main/resources/database/hsqldb/V1_0_0__init.sql | 0 .../database/hsqldb/V6_0_0__remindFlagRecurringTransaction.sql | 0 .../src}/main/resources/database/hsqldb/V7_0_0__accountGroup.sql | 0 .../src}/main/resources/database/postgres/V1_0_0__init.sql | 0 .../database/postgres/V6_0_0__remindFlagRecurringTransaction.sql | 0 .../main/resources/database/postgres/V7_0_0__accountGroup.sql | 0 .../src}/main/resources/database/postgres/readme_V1_0_0__init.txt | 0 .../src}/test/java/de/financer/FinancerApplicationBootTest.java | 0 .../integration/AccountController_getAllIntegrationTest.java | 0 ...nsactionService_createRecurringTransactionIntegrationTest.java | 0 ...urringTransactionService_createTransactionIntegrationTest.java | 0 .../RecurringTransactionService_getAllActiveIntegrationTest.java | 0 .../RecurringTransactionService_getAllIntegrationTest.java | 0 .../service/AccountGroupService_createAccountGroupTest.java | 0 .../de/financer/service/AccountService_createAccountTest.java | 0 .../de/financer/service/AccountService_setAccountStatusTest.java | 0 ...ecurringTransactionService_createRecurringTransactionTest.java | 0 ...ecurringTransactionService_deleteRecurringTransactionTest.java | 0 ...gTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest.java | 0 ...ransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest.java | 0 ...actionService_getAllDueToday_MONTHLY_PREVIOUS_WORKDAYTest.java | 0 ...ingTransactionService_getAllDueToday_MONTHLY_SAME_DAYTest.java | 0 .../service/RuleService_getMultiplierFromAccountTest.java | 0 .../financer/service/RuleService_getMultiplierToAccountTest.java | 0 .../java/de/financer/service/RuleService_isValidBookingTest.java | 0 .../service/TransactionService_createTransactionTest.java | 0 .../service/TransactionService_deleteTransactionTest.java | 0 .../financer/task/SendRecurringTransactionReminderTaskTest.java | 0 .../src}/test/resources/application-integrationtest.properties | 0 .../database/hsqldb/integration/V999_99_00__testdata.sql | 0 68 files changed, 0 insertions(+), 0 deletions(-) rename .gitignore => financer-server/.gitignore (100%) rename {doc => financer-server/doc}/README (100%) rename pom.xml => financer-server/pom.xml (100%) rename {src => financer-server/src}/main/java/de/financer/FinancerApplication.java (100%) rename {src => financer-server/src}/main/java/de/financer/ResponseReason.java (100%) rename {src => financer-server/src}/main/java/de/financer/config/FinancerConfig.java (100%) rename {src => financer-server/src}/main/java/de/financer/controller/AccountController.java (100%) rename {src => financer-server/src}/main/java/de/financer/controller/AccountGroupController.java (100%) rename {src => financer-server/src}/main/java/de/financer/controller/ControllerUtil.java (100%) rename {src => financer-server/src}/main/java/de/financer/controller/RecurringTransactionController.java (100%) rename {src => financer-server/src}/main/java/de/financer/controller/TransactionController.java (100%) rename {src => financer-server/src}/main/java/de/financer/dba/AccountGroupRepository.java (100%) rename {src => financer-server/src}/main/java/de/financer/dba/AccountRepository.java (100%) rename {src => financer-server/src}/main/java/de/financer/dba/RecurringTransactionRepository.java (100%) rename {src => financer-server/src}/main/java/de/financer/dba/TransactionRepository.java (100%) rename {src => financer-server/src}/main/java/de/financer/model/Account.java (100%) rename {src => financer-server/src}/main/java/de/financer/model/AccountGroup.java (100%) rename {src => financer-server/src}/main/java/de/financer/model/AccountStatus.java (100%) rename {src => financer-server/src}/main/java/de/financer/model/AccountType.java (100%) rename {src => financer-server/src}/main/java/de/financer/model/HolidayWeekendType.java (100%) rename {src => financer-server/src}/main/java/de/financer/model/IntervalType.java (100%) rename {src => financer-server/src}/main/java/de/financer/model/RecurringTransaction.java (100%) rename {src => financer-server/src}/main/java/de/financer/model/Transaction.java (100%) rename {src => financer-server/src}/main/java/de/financer/model/package-info.java (100%) rename {src => financer-server/src}/main/java/de/financer/service/AccountGroupService.java (100%) rename {src => financer-server/src}/main/java/de/financer/service/AccountService.java (100%) rename {src => financer-server/src}/main/java/de/financer/service/RecurringTransactionService.java (100%) rename {src => financer-server/src}/main/java/de/financer/service/RuleService.java (100%) rename {src => financer-server/src}/main/java/de/financer/service/TransactionService.java (100%) rename {src => financer-server/src}/main/java/de/financer/service/package-info.java (100%) rename {src => financer-server/src}/main/java/de/financer/task/SendRecurringTransactionReminderTask.java (100%) rename {src => financer-server/src}/main/resources/config/application-dev.properties (100%) rename {src => financer-server/src}/main/resources/config/application-hsqldb.properties (100%) rename {src => financer-server/src}/main/resources/config/application-postgres.properties (100%) rename {src => financer-server/src}/main/resources/config/application.properties (100%) rename {src => financer-server/src}/main/resources/database/common/V1_0_1__initData.sql (100%) rename {src => financer-server/src}/main/resources/database/common/V3_0_0__accountRename.sql (100%) rename {src => financer-server/src}/main/resources/database/common/V7_0_1__initAccountGroups.sql (100%) rename {src => financer-server/src}/main/resources/database/hsqldb/V1_0_0__init.sql (100%) rename {src => financer-server/src}/main/resources/database/hsqldb/V6_0_0__remindFlagRecurringTransaction.sql (100%) rename {src => financer-server/src}/main/resources/database/hsqldb/V7_0_0__accountGroup.sql (100%) rename {src => financer-server/src}/main/resources/database/postgres/V1_0_0__init.sql (100%) rename {src => financer-server/src}/main/resources/database/postgres/V6_0_0__remindFlagRecurringTransaction.sql (100%) rename {src => financer-server/src}/main/resources/database/postgres/V7_0_0__accountGroup.sql (100%) rename {src => financer-server/src}/main/resources/database/postgres/readme_V1_0_0__init.txt (100%) rename {src => financer-server/src}/test/java/de/financer/FinancerApplicationBootTest.java (100%) rename {src => financer-server/src}/test/java/de/financer/controller/integration/AccountController_getAllIntegrationTest.java (100%) rename {src => financer-server/src}/test/java/de/financer/controller/integration/RecurringTransactionService_createRecurringTransactionIntegrationTest.java (100%) rename {src => financer-server/src}/test/java/de/financer/controller/integration/RecurringTransactionService_createTransactionIntegrationTest.java (100%) rename {src => financer-server/src}/test/java/de/financer/controller/integration/RecurringTransactionService_getAllActiveIntegrationTest.java (100%) rename {src => financer-server/src}/test/java/de/financer/controller/integration/RecurringTransactionService_getAllIntegrationTest.java (100%) rename {src => financer-server/src}/test/java/de/financer/service/AccountGroupService_createAccountGroupTest.java (100%) rename {src => financer-server/src}/test/java/de/financer/service/AccountService_createAccountTest.java (100%) rename {src => financer-server/src}/test/java/de/financer/service/AccountService_setAccountStatusTest.java (100%) rename {src => financer-server/src}/test/java/de/financer/service/RecurringTransactionService_createRecurringTransactionTest.java (100%) rename {src => financer-server/src}/test/java/de/financer/service/RecurringTransactionService_deleteRecurringTransactionTest.java (100%) rename {src => financer-server/src}/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest.java (100%) rename {src => financer-server/src}/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest.java (100%) rename {src => financer-server/src}/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_PREVIOUS_WORKDAYTest.java (100%) rename {src => financer-server/src}/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_SAME_DAYTest.java (100%) rename {src => financer-server/src}/test/java/de/financer/service/RuleService_getMultiplierFromAccountTest.java (100%) rename {src => financer-server/src}/test/java/de/financer/service/RuleService_getMultiplierToAccountTest.java (100%) rename {src => financer-server/src}/test/java/de/financer/service/RuleService_isValidBookingTest.java (100%) rename {src => financer-server/src}/test/java/de/financer/service/TransactionService_createTransactionTest.java (100%) rename {src => financer-server/src}/test/java/de/financer/service/TransactionService_deleteTransactionTest.java (100%) rename {src => financer-server/src}/test/java/de/financer/task/SendRecurringTransactionReminderTaskTest.java (100%) rename {src => financer-server/src}/test/resources/application-integrationtest.properties (100%) rename {src => financer-server/src}/test/resources/database/hsqldb/integration/V999_99_00__testdata.sql (100%) diff --git a/.gitignore b/financer-server/.gitignore similarity index 100% rename from .gitignore rename to financer-server/.gitignore diff --git a/doc/README b/financer-server/doc/README similarity index 100% rename from doc/README rename to financer-server/doc/README diff --git a/pom.xml b/financer-server/pom.xml similarity index 100% rename from pom.xml rename to financer-server/pom.xml diff --git a/src/main/java/de/financer/FinancerApplication.java b/financer-server/src/main/java/de/financer/FinancerApplication.java similarity index 100% rename from src/main/java/de/financer/FinancerApplication.java rename to financer-server/src/main/java/de/financer/FinancerApplication.java diff --git a/src/main/java/de/financer/ResponseReason.java b/financer-server/src/main/java/de/financer/ResponseReason.java similarity index 100% rename from src/main/java/de/financer/ResponseReason.java rename to financer-server/src/main/java/de/financer/ResponseReason.java diff --git a/src/main/java/de/financer/config/FinancerConfig.java b/financer-server/src/main/java/de/financer/config/FinancerConfig.java similarity index 100% rename from src/main/java/de/financer/config/FinancerConfig.java rename to financer-server/src/main/java/de/financer/config/FinancerConfig.java diff --git a/src/main/java/de/financer/controller/AccountController.java b/financer-server/src/main/java/de/financer/controller/AccountController.java similarity index 100% rename from src/main/java/de/financer/controller/AccountController.java rename to financer-server/src/main/java/de/financer/controller/AccountController.java diff --git a/src/main/java/de/financer/controller/AccountGroupController.java b/financer-server/src/main/java/de/financer/controller/AccountGroupController.java similarity index 100% rename from src/main/java/de/financer/controller/AccountGroupController.java rename to financer-server/src/main/java/de/financer/controller/AccountGroupController.java diff --git a/src/main/java/de/financer/controller/ControllerUtil.java b/financer-server/src/main/java/de/financer/controller/ControllerUtil.java similarity index 100% rename from src/main/java/de/financer/controller/ControllerUtil.java rename to financer-server/src/main/java/de/financer/controller/ControllerUtil.java diff --git a/src/main/java/de/financer/controller/RecurringTransactionController.java b/financer-server/src/main/java/de/financer/controller/RecurringTransactionController.java similarity index 100% rename from src/main/java/de/financer/controller/RecurringTransactionController.java rename to financer-server/src/main/java/de/financer/controller/RecurringTransactionController.java diff --git a/src/main/java/de/financer/controller/TransactionController.java b/financer-server/src/main/java/de/financer/controller/TransactionController.java similarity index 100% rename from src/main/java/de/financer/controller/TransactionController.java rename to financer-server/src/main/java/de/financer/controller/TransactionController.java diff --git a/src/main/java/de/financer/dba/AccountGroupRepository.java b/financer-server/src/main/java/de/financer/dba/AccountGroupRepository.java similarity index 100% rename from src/main/java/de/financer/dba/AccountGroupRepository.java rename to financer-server/src/main/java/de/financer/dba/AccountGroupRepository.java diff --git a/src/main/java/de/financer/dba/AccountRepository.java b/financer-server/src/main/java/de/financer/dba/AccountRepository.java similarity index 100% rename from src/main/java/de/financer/dba/AccountRepository.java rename to financer-server/src/main/java/de/financer/dba/AccountRepository.java diff --git a/src/main/java/de/financer/dba/RecurringTransactionRepository.java b/financer-server/src/main/java/de/financer/dba/RecurringTransactionRepository.java similarity index 100% rename from src/main/java/de/financer/dba/RecurringTransactionRepository.java rename to financer-server/src/main/java/de/financer/dba/RecurringTransactionRepository.java diff --git a/src/main/java/de/financer/dba/TransactionRepository.java b/financer-server/src/main/java/de/financer/dba/TransactionRepository.java similarity index 100% rename from src/main/java/de/financer/dba/TransactionRepository.java rename to financer-server/src/main/java/de/financer/dba/TransactionRepository.java diff --git a/src/main/java/de/financer/model/Account.java b/financer-server/src/main/java/de/financer/model/Account.java similarity index 100% rename from src/main/java/de/financer/model/Account.java rename to financer-server/src/main/java/de/financer/model/Account.java diff --git a/src/main/java/de/financer/model/AccountGroup.java b/financer-server/src/main/java/de/financer/model/AccountGroup.java similarity index 100% rename from src/main/java/de/financer/model/AccountGroup.java rename to financer-server/src/main/java/de/financer/model/AccountGroup.java diff --git a/src/main/java/de/financer/model/AccountStatus.java b/financer-server/src/main/java/de/financer/model/AccountStatus.java similarity index 100% rename from src/main/java/de/financer/model/AccountStatus.java rename to financer-server/src/main/java/de/financer/model/AccountStatus.java diff --git a/src/main/java/de/financer/model/AccountType.java b/financer-server/src/main/java/de/financer/model/AccountType.java similarity index 100% rename from src/main/java/de/financer/model/AccountType.java rename to financer-server/src/main/java/de/financer/model/AccountType.java diff --git a/src/main/java/de/financer/model/HolidayWeekendType.java b/financer-server/src/main/java/de/financer/model/HolidayWeekendType.java similarity index 100% rename from src/main/java/de/financer/model/HolidayWeekendType.java rename to financer-server/src/main/java/de/financer/model/HolidayWeekendType.java diff --git a/src/main/java/de/financer/model/IntervalType.java b/financer-server/src/main/java/de/financer/model/IntervalType.java similarity index 100% rename from src/main/java/de/financer/model/IntervalType.java rename to financer-server/src/main/java/de/financer/model/IntervalType.java diff --git a/src/main/java/de/financer/model/RecurringTransaction.java b/financer-server/src/main/java/de/financer/model/RecurringTransaction.java similarity index 100% rename from src/main/java/de/financer/model/RecurringTransaction.java rename to financer-server/src/main/java/de/financer/model/RecurringTransaction.java diff --git a/src/main/java/de/financer/model/Transaction.java b/financer-server/src/main/java/de/financer/model/Transaction.java similarity index 100% rename from src/main/java/de/financer/model/Transaction.java rename to financer-server/src/main/java/de/financer/model/Transaction.java diff --git a/src/main/java/de/financer/model/package-info.java b/financer-server/src/main/java/de/financer/model/package-info.java similarity index 100% rename from src/main/java/de/financer/model/package-info.java rename to financer-server/src/main/java/de/financer/model/package-info.java diff --git a/src/main/java/de/financer/service/AccountGroupService.java b/financer-server/src/main/java/de/financer/service/AccountGroupService.java similarity index 100% rename from src/main/java/de/financer/service/AccountGroupService.java rename to financer-server/src/main/java/de/financer/service/AccountGroupService.java diff --git a/src/main/java/de/financer/service/AccountService.java b/financer-server/src/main/java/de/financer/service/AccountService.java similarity index 100% rename from src/main/java/de/financer/service/AccountService.java rename to financer-server/src/main/java/de/financer/service/AccountService.java diff --git a/src/main/java/de/financer/service/RecurringTransactionService.java b/financer-server/src/main/java/de/financer/service/RecurringTransactionService.java similarity index 100% rename from src/main/java/de/financer/service/RecurringTransactionService.java rename to financer-server/src/main/java/de/financer/service/RecurringTransactionService.java diff --git a/src/main/java/de/financer/service/RuleService.java b/financer-server/src/main/java/de/financer/service/RuleService.java similarity index 100% rename from src/main/java/de/financer/service/RuleService.java rename to financer-server/src/main/java/de/financer/service/RuleService.java diff --git a/src/main/java/de/financer/service/TransactionService.java b/financer-server/src/main/java/de/financer/service/TransactionService.java similarity index 100% rename from src/main/java/de/financer/service/TransactionService.java rename to financer-server/src/main/java/de/financer/service/TransactionService.java diff --git a/src/main/java/de/financer/service/package-info.java b/financer-server/src/main/java/de/financer/service/package-info.java similarity index 100% rename from src/main/java/de/financer/service/package-info.java rename to financer-server/src/main/java/de/financer/service/package-info.java diff --git a/src/main/java/de/financer/task/SendRecurringTransactionReminderTask.java b/financer-server/src/main/java/de/financer/task/SendRecurringTransactionReminderTask.java similarity index 100% rename from src/main/java/de/financer/task/SendRecurringTransactionReminderTask.java rename to financer-server/src/main/java/de/financer/task/SendRecurringTransactionReminderTask.java diff --git a/src/main/resources/config/application-dev.properties b/financer-server/src/main/resources/config/application-dev.properties similarity index 100% rename from src/main/resources/config/application-dev.properties rename to financer-server/src/main/resources/config/application-dev.properties diff --git a/src/main/resources/config/application-hsqldb.properties b/financer-server/src/main/resources/config/application-hsqldb.properties similarity index 100% rename from src/main/resources/config/application-hsqldb.properties rename to financer-server/src/main/resources/config/application-hsqldb.properties diff --git a/src/main/resources/config/application-postgres.properties b/financer-server/src/main/resources/config/application-postgres.properties similarity index 100% rename from src/main/resources/config/application-postgres.properties rename to financer-server/src/main/resources/config/application-postgres.properties diff --git a/src/main/resources/config/application.properties b/financer-server/src/main/resources/config/application.properties similarity index 100% rename from src/main/resources/config/application.properties rename to financer-server/src/main/resources/config/application.properties diff --git a/src/main/resources/database/common/V1_0_1__initData.sql b/financer-server/src/main/resources/database/common/V1_0_1__initData.sql similarity index 100% rename from src/main/resources/database/common/V1_0_1__initData.sql rename to financer-server/src/main/resources/database/common/V1_0_1__initData.sql diff --git a/src/main/resources/database/common/V3_0_0__accountRename.sql b/financer-server/src/main/resources/database/common/V3_0_0__accountRename.sql similarity index 100% rename from src/main/resources/database/common/V3_0_0__accountRename.sql rename to financer-server/src/main/resources/database/common/V3_0_0__accountRename.sql diff --git a/src/main/resources/database/common/V7_0_1__initAccountGroups.sql b/financer-server/src/main/resources/database/common/V7_0_1__initAccountGroups.sql similarity index 100% rename from src/main/resources/database/common/V7_0_1__initAccountGroups.sql rename to financer-server/src/main/resources/database/common/V7_0_1__initAccountGroups.sql diff --git a/src/main/resources/database/hsqldb/V1_0_0__init.sql b/financer-server/src/main/resources/database/hsqldb/V1_0_0__init.sql similarity index 100% rename from src/main/resources/database/hsqldb/V1_0_0__init.sql rename to financer-server/src/main/resources/database/hsqldb/V1_0_0__init.sql diff --git a/src/main/resources/database/hsqldb/V6_0_0__remindFlagRecurringTransaction.sql b/financer-server/src/main/resources/database/hsqldb/V6_0_0__remindFlagRecurringTransaction.sql similarity index 100% rename from src/main/resources/database/hsqldb/V6_0_0__remindFlagRecurringTransaction.sql rename to financer-server/src/main/resources/database/hsqldb/V6_0_0__remindFlagRecurringTransaction.sql diff --git a/src/main/resources/database/hsqldb/V7_0_0__accountGroup.sql b/financer-server/src/main/resources/database/hsqldb/V7_0_0__accountGroup.sql similarity index 100% rename from src/main/resources/database/hsqldb/V7_0_0__accountGroup.sql rename to financer-server/src/main/resources/database/hsqldb/V7_0_0__accountGroup.sql diff --git a/src/main/resources/database/postgres/V1_0_0__init.sql b/financer-server/src/main/resources/database/postgres/V1_0_0__init.sql similarity index 100% rename from src/main/resources/database/postgres/V1_0_0__init.sql rename to financer-server/src/main/resources/database/postgres/V1_0_0__init.sql diff --git a/src/main/resources/database/postgres/V6_0_0__remindFlagRecurringTransaction.sql b/financer-server/src/main/resources/database/postgres/V6_0_0__remindFlagRecurringTransaction.sql similarity index 100% rename from src/main/resources/database/postgres/V6_0_0__remindFlagRecurringTransaction.sql rename to financer-server/src/main/resources/database/postgres/V6_0_0__remindFlagRecurringTransaction.sql diff --git a/src/main/resources/database/postgres/V7_0_0__accountGroup.sql b/financer-server/src/main/resources/database/postgres/V7_0_0__accountGroup.sql similarity index 100% rename from src/main/resources/database/postgres/V7_0_0__accountGroup.sql rename to financer-server/src/main/resources/database/postgres/V7_0_0__accountGroup.sql diff --git a/src/main/resources/database/postgres/readme_V1_0_0__init.txt b/financer-server/src/main/resources/database/postgres/readme_V1_0_0__init.txt similarity index 100% rename from src/main/resources/database/postgres/readme_V1_0_0__init.txt rename to financer-server/src/main/resources/database/postgres/readme_V1_0_0__init.txt diff --git a/src/test/java/de/financer/FinancerApplicationBootTest.java b/financer-server/src/test/java/de/financer/FinancerApplicationBootTest.java similarity index 100% rename from src/test/java/de/financer/FinancerApplicationBootTest.java rename to financer-server/src/test/java/de/financer/FinancerApplicationBootTest.java diff --git a/src/test/java/de/financer/controller/integration/AccountController_getAllIntegrationTest.java b/financer-server/src/test/java/de/financer/controller/integration/AccountController_getAllIntegrationTest.java similarity index 100% rename from src/test/java/de/financer/controller/integration/AccountController_getAllIntegrationTest.java rename to financer-server/src/test/java/de/financer/controller/integration/AccountController_getAllIntegrationTest.java diff --git a/src/test/java/de/financer/controller/integration/RecurringTransactionService_createRecurringTransactionIntegrationTest.java b/financer-server/src/test/java/de/financer/controller/integration/RecurringTransactionService_createRecurringTransactionIntegrationTest.java similarity index 100% rename from src/test/java/de/financer/controller/integration/RecurringTransactionService_createRecurringTransactionIntegrationTest.java rename to financer-server/src/test/java/de/financer/controller/integration/RecurringTransactionService_createRecurringTransactionIntegrationTest.java diff --git a/src/test/java/de/financer/controller/integration/RecurringTransactionService_createTransactionIntegrationTest.java b/financer-server/src/test/java/de/financer/controller/integration/RecurringTransactionService_createTransactionIntegrationTest.java similarity index 100% rename from src/test/java/de/financer/controller/integration/RecurringTransactionService_createTransactionIntegrationTest.java rename to financer-server/src/test/java/de/financer/controller/integration/RecurringTransactionService_createTransactionIntegrationTest.java diff --git a/src/test/java/de/financer/controller/integration/RecurringTransactionService_getAllActiveIntegrationTest.java b/financer-server/src/test/java/de/financer/controller/integration/RecurringTransactionService_getAllActiveIntegrationTest.java similarity index 100% rename from src/test/java/de/financer/controller/integration/RecurringTransactionService_getAllActiveIntegrationTest.java rename to financer-server/src/test/java/de/financer/controller/integration/RecurringTransactionService_getAllActiveIntegrationTest.java diff --git a/src/test/java/de/financer/controller/integration/RecurringTransactionService_getAllIntegrationTest.java b/financer-server/src/test/java/de/financer/controller/integration/RecurringTransactionService_getAllIntegrationTest.java similarity index 100% rename from src/test/java/de/financer/controller/integration/RecurringTransactionService_getAllIntegrationTest.java rename to financer-server/src/test/java/de/financer/controller/integration/RecurringTransactionService_getAllIntegrationTest.java diff --git a/src/test/java/de/financer/service/AccountGroupService_createAccountGroupTest.java b/financer-server/src/test/java/de/financer/service/AccountGroupService_createAccountGroupTest.java similarity index 100% rename from src/test/java/de/financer/service/AccountGroupService_createAccountGroupTest.java rename to financer-server/src/test/java/de/financer/service/AccountGroupService_createAccountGroupTest.java diff --git a/src/test/java/de/financer/service/AccountService_createAccountTest.java b/financer-server/src/test/java/de/financer/service/AccountService_createAccountTest.java similarity index 100% rename from src/test/java/de/financer/service/AccountService_createAccountTest.java rename to financer-server/src/test/java/de/financer/service/AccountService_createAccountTest.java diff --git a/src/test/java/de/financer/service/AccountService_setAccountStatusTest.java b/financer-server/src/test/java/de/financer/service/AccountService_setAccountStatusTest.java similarity index 100% rename from src/test/java/de/financer/service/AccountService_setAccountStatusTest.java rename to financer-server/src/test/java/de/financer/service/AccountService_setAccountStatusTest.java diff --git a/src/test/java/de/financer/service/RecurringTransactionService_createRecurringTransactionTest.java b/financer-server/src/test/java/de/financer/service/RecurringTransactionService_createRecurringTransactionTest.java similarity index 100% rename from src/test/java/de/financer/service/RecurringTransactionService_createRecurringTransactionTest.java rename to financer-server/src/test/java/de/financer/service/RecurringTransactionService_createRecurringTransactionTest.java diff --git a/src/test/java/de/financer/service/RecurringTransactionService_deleteRecurringTransactionTest.java b/financer-server/src/test/java/de/financer/service/RecurringTransactionService_deleteRecurringTransactionTest.java similarity index 100% rename from src/test/java/de/financer/service/RecurringTransactionService_deleteRecurringTransactionTest.java rename to financer-server/src/test/java/de/financer/service/RecurringTransactionService_deleteRecurringTransactionTest.java diff --git a/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest.java b/financer-server/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest.java similarity index 100% rename from src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest.java rename to financer-server/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest.java diff --git a/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest.java b/financer-server/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest.java similarity index 100% rename from src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest.java rename to financer-server/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest.java diff --git a/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_PREVIOUS_WORKDAYTest.java b/financer-server/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_PREVIOUS_WORKDAYTest.java similarity index 100% rename from src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_PREVIOUS_WORKDAYTest.java rename to financer-server/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_PREVIOUS_WORKDAYTest.java diff --git a/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_SAME_DAYTest.java b/financer-server/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_SAME_DAYTest.java similarity index 100% rename from src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_SAME_DAYTest.java rename to financer-server/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_SAME_DAYTest.java diff --git a/src/test/java/de/financer/service/RuleService_getMultiplierFromAccountTest.java b/financer-server/src/test/java/de/financer/service/RuleService_getMultiplierFromAccountTest.java similarity index 100% rename from src/test/java/de/financer/service/RuleService_getMultiplierFromAccountTest.java rename to financer-server/src/test/java/de/financer/service/RuleService_getMultiplierFromAccountTest.java diff --git a/src/test/java/de/financer/service/RuleService_getMultiplierToAccountTest.java b/financer-server/src/test/java/de/financer/service/RuleService_getMultiplierToAccountTest.java similarity index 100% rename from src/test/java/de/financer/service/RuleService_getMultiplierToAccountTest.java rename to financer-server/src/test/java/de/financer/service/RuleService_getMultiplierToAccountTest.java diff --git a/src/test/java/de/financer/service/RuleService_isValidBookingTest.java b/financer-server/src/test/java/de/financer/service/RuleService_isValidBookingTest.java similarity index 100% rename from src/test/java/de/financer/service/RuleService_isValidBookingTest.java rename to financer-server/src/test/java/de/financer/service/RuleService_isValidBookingTest.java diff --git a/src/test/java/de/financer/service/TransactionService_createTransactionTest.java b/financer-server/src/test/java/de/financer/service/TransactionService_createTransactionTest.java similarity index 100% rename from src/test/java/de/financer/service/TransactionService_createTransactionTest.java rename to financer-server/src/test/java/de/financer/service/TransactionService_createTransactionTest.java diff --git a/src/test/java/de/financer/service/TransactionService_deleteTransactionTest.java b/financer-server/src/test/java/de/financer/service/TransactionService_deleteTransactionTest.java similarity index 100% rename from src/test/java/de/financer/service/TransactionService_deleteTransactionTest.java rename to financer-server/src/test/java/de/financer/service/TransactionService_deleteTransactionTest.java diff --git a/src/test/java/de/financer/task/SendRecurringTransactionReminderTaskTest.java b/financer-server/src/test/java/de/financer/task/SendRecurringTransactionReminderTaskTest.java similarity index 100% rename from src/test/java/de/financer/task/SendRecurringTransactionReminderTaskTest.java rename to financer-server/src/test/java/de/financer/task/SendRecurringTransactionReminderTaskTest.java diff --git a/src/test/resources/application-integrationtest.properties b/financer-server/src/test/resources/application-integrationtest.properties similarity index 100% rename from src/test/resources/application-integrationtest.properties rename to financer-server/src/test/resources/application-integrationtest.properties diff --git a/src/test/resources/database/hsqldb/integration/V999_99_00__testdata.sql b/financer-server/src/test/resources/database/hsqldb/integration/V999_99_00__testdata.sql similarity index 100% rename from src/test/resources/database/hsqldb/integration/V999_99_00__testdata.sql rename to financer-server/src/test/resources/database/hsqldb/integration/V999_99_00__testdata.sql