Various stuff all over the tree

- Increase Java version to 1.9
- Add commons-collection and Jollyday dependencies
- Add JavaDoc plugin
- Add country and state configuration for Jollyday library
- Add WIP implementation of the recurring transaction feature
- Improve JavaDoc
- Use Java 8 date API
- Reformatting
- Add special Flyway migration version for test data
- Add and improve unit tests
This commit is contained in:
2019-03-01 20:39:31 +01:00
parent fe5380a865
commit 35902afe43
21 changed files with 950 additions and 102 deletions

31
pom.xml
View File

@@ -20,9 +20,9 @@
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.source>1.9</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target> <maven.compiler.target>1.9</maven.compiler.target>
<java.version>1.8</java.version> <java.version>1.9</java.version>
</properties> </properties>
<dependencies> <dependencies>
@@ -46,7 +46,17 @@
<dependency> <dependency>
<groupId>org.apache.commons</groupId> <groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId> <artifactId>commons-lang3</artifactId>
<version>3.8.1</version> </dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.3</version>
</dependency>
<!-- Misc dependencies -->
<dependency>
<groupId>de.jollyday</groupId>
<artifactId>jollyday</artifactId>
<version>0.5.7</version>
</dependency> </dependency>
<!-- Runtime dependencies --> <!-- Runtime dependencies -->
@@ -99,6 +109,19 @@
</plugins> </plugins>
</build> </build>
<reporting>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<configuration>
<show>private</show>
<javadocExecutable>/usr/bin/javadoc</javadocExecutable>
</configuration>
</plugin>
</plugins>
</reporting>
<profiles> <profiles>
<profile> <profile>
<id>integration-tests</id> <id>integration-tests</id>

View File

@@ -0,0 +1,53 @@
package de.financer.config;
import de.jollyday.HolidayCalendar;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.Arrays;
import java.util.Optional;
@Configuration
@ConfigurationProperties(prefix = "financer")
public class FinancerConfig {
private String countryCode;
private String state;
/**
* @return the raw country code, mostly an uppercase ISO 3166 2-letter code
*/
public String getCountryCode() {
return countryCode;
}
/**
* @return the state
*/
public String getState() {
return state;
}
/**
* @return the {@link HolidayCalendar} used to calculate the holidays. Internally uses the country code
* specified via {@link FinancerConfig#getCountryCode}.
*/
public HolidayCalendar getHolidayCalendar() {
final Optional<HolidayCalendar> optionalHoliday = Arrays.asList(HolidayCalendar.values()).stream()
.filter((hc) -> hc.getId().equals(this.countryCode))
.findFirst();
if (!optionalHoliday.isPresent()) {
// TODO log info about default DE
}
return optionalHoliday.orElse(HolidayCalendar.GERMANY);
}
public void setState(String state) {
this.state = state;
}
public void setCountryCode(String countryCode) {
this.countryCode = countryCode;
}
}

View File

@@ -0,0 +1,43 @@
package de.financer.controller;
import de.financer.model.RecurringTransaction;
import de.financer.service.RecurringTransactionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("recurringTransactions")
public class RecurringTransactionController {
@Autowired
private RecurringTransactionService recurringTransactionService;
@RequestMapping("getAll")
public Iterable<RecurringTransaction> getAll() {
return this.recurringTransactionService.getAll();
}
@RequestMapping("getAllForAccount")
public Iterable<RecurringTransaction> getAllForAccount(String accountKey) {
return this.recurringTransactionService.getAllForAccount(accountKey);
}
@RequestMapping("getAllDueToday")
public Iterable<RecurringTransaction> getAllDueToday() {
return this.recurringTransactionService.getAllDueToday();
}
@RequestMapping("createRecurringTransaction")
public ResponseEntity createRecurringTransaction(String fromAccountKey, String toAccountKey, Long amount,
String description, String holidayWeekendType,
String intervalType, String firstOccurrence,
String lastOccurrence) {
return this.recurringTransactionService.createRecurringTransaction(fromAccountKey, toAccountKey, amount,
description, holidayWeekendType,
intervalType, firstOccurrence,
lastOccurrence)
.toResponseEntity();
}
}

View File

@@ -23,8 +23,10 @@ public class TransactionController {
return this.transactionService.getAllForAccount(accountKey); return this.transactionService.getAllForAccount(accountKey);
} }
@RequestMapping("createTransaction") @RequestMapping(value = "createTransaction")
public ResponseEntity createTransaction(String fromAccountKey, String toAccountKey, Long amount, String date, String description) { public ResponseEntity createTransaction(String fromAccountKey, String toAccountKey, Long amount, String date,
return this.transactionService.createTransaction(fromAccountKey, toAccountKey, amount, date, description).toResponseEntity(); String description) {
return this.transactionService.createTransaction(fromAccountKey, toAccountKey, amount, date, description)
.toResponseEntity();
} }
} }

View File

@@ -1,7 +1,12 @@
package de.financer.dba; package de.financer.dba;
import de.financer.model.Account;
import de.financer.model.RecurringTransaction; import de.financer.model.RecurringTransaction;
import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.CrudRepository;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Transactional(propagation = Propagation.REQUIRED)
public interface RecurringTransactionRepository extends CrudRepository<RecurringTransaction, Long> { public interface RecurringTransactionRepository extends CrudRepository<RecurringTransaction, Long> {
Iterable<RecurringTransaction> findRecurringTransactionsByFromAccountOrToAccount(Account fromAccount, Account toAccount);
} }

View File

@@ -10,10 +10,47 @@ public enum HolidayWeekendType {
/** Indicates that the action should be done on the specified day regardless whether it's a holiday or a weekend */ /** Indicates that the action should be done on the specified day regardless whether it's a holiday or a weekend */
SAME_DAY, SAME_DAY,
/** Indicates that the action should be deferred to the next workday */ /**
* <p>
* Indicates that the action should be deferred to the next workday.
* </p>
* <pre>
* Example 1:
* MO TU WE TH FR SA SO
* H WE WE -&gt; Holiday/WeekEnd
* X -&gt; Due date of action
* X' -&gt; Deferred, effective due date of action
* </pre>
* <pre>
* Example 2:
* TU WE TH FR SA SO MO
* H WE WE -&gt; Holiday/WeekEnd
* X -&gt; Due date of action
* X' -&gt; Deferred, effective due date of action
* </pre>
*
*/
NEXT_WORKDAY, NEXT_WORKDAY,
/** Indicates that the action should be dated back to the previous day */ /**
* <p>
* Indicates that the action should be made earlier at the previous day
* </p>
* <pre>
* Example 1:
* MO TU WE TH FR SA SO
* H WE WE -&gt; Holiday/WeekEnd
* X -&gt; Due date of action
* X' -&gt; Earlier, effective due date of action
* </pre>
* <pre>
* Example 2:
* MO TU WE TH FR SA SO
* H WE WE -&gt; Holiday/WeekEnd
* X -&gt; Due date of action
* X' -&gt; Earlier, effective due date of action
* </pre>
*/
PREVIOUS_WORKDAY; PREVIOUS_WORKDAY;
/** /**

View File

@@ -1,7 +1,7 @@
package de.financer.model; package de.financer.model;
import javax.persistence.*; import javax.persistence.*;
import java.util.Date; import java.time.LocalDate;
@Entity @Entity
public class RecurringTransaction { public class RecurringTransaction {
@@ -16,10 +16,8 @@ public class RecurringTransaction {
private Long amount; private Long amount;
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
private IntervalType intervalType; private IntervalType intervalType;
@Temporal(TemporalType.DATE) private LocalDate firstOccurrence;
private Date firstOccurrence; private LocalDate lastOccurrence;
@Temporal(TemporalType.DATE)
private Date lastOccurrence;
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
private HolidayWeekendType holidayWeekendType; private HolidayWeekendType holidayWeekendType;
@@ -67,19 +65,19 @@ public class RecurringTransaction {
this.holidayWeekendType = holidayWeekendType; this.holidayWeekendType = holidayWeekendType;
} }
public Date getLastOccurrence() { public LocalDate getLastOccurrence() {
return lastOccurrence; return lastOccurrence;
} }
public void setLastOccurrence(Date lastOccurrence) { public void setLastOccurrence(LocalDate lastOccurrence) {
this.lastOccurrence = lastOccurrence; this.lastOccurrence = lastOccurrence;
} }
public Date getFirstOccurrence() { public LocalDate getFirstOccurrence() {
return firstOccurrence; return firstOccurrence;
} }
public void setFirstOccurrence(Date firstOccurrence) { public void setFirstOccurrence(LocalDate firstOccurrence) {
this.firstOccurrence = firstOccurrence; this.firstOccurrence = firstOccurrence;
} }

View File

@@ -1,7 +1,7 @@
package de.financer.model; package de.financer.model;
import javax.persistence.*; import javax.persistence.*;
import java.util.Date; import java.time.LocalDate;
@Entity @Entity
@Table(name = "\"transaction\"") @Table(name = "\"transaction\"")
@@ -13,9 +13,8 @@ public class Transaction {
private Account fromAccount; private Account fromAccount;
@OneToOne(fetch = FetchType.EAGER) @OneToOne(fetch = FetchType.EAGER)
private Account toAccount; private Account toAccount;
@Temporal(TemporalType.DATE)
@Column(name = "\"date\"") @Column(name = "\"date\"")
private Date date; private LocalDate date;
private String description; private String description;
private Long amount; private Long amount;
@ManyToOne(fetch = FetchType.EAGER) @ManyToOne(fetch = FetchType.EAGER)
@@ -41,11 +40,11 @@ public class Transaction {
this.toAccount = toAccount; this.toAccount = toAccount;
} }
public Date getDate() { public LocalDate getDate() {
return date; return date;
} }
public void setDate(Date date) { public void setDate(LocalDate date) {
this.date = date; this.date = date;
} }

View File

@@ -0,0 +1,10 @@
/**
* <p>
* This package contains the main model for the financer application.
* In the DDD (<i>Domain Driven Design</i>) sense the models are anemic
* as they contain no logic themselves but act as mere POJOs with additional
* Hibernate annotations. The (business) logic is located in the services of the
* {@link de.financer.service} package.
* </p>
*/
package de.financer.model;

View File

@@ -0,0 +1,163 @@
package de.financer.service;
import de.financer.ResponseReason;
import de.financer.dba.RecurringTransactionRepository;
import de.financer.model.Account;
import de.financer.model.HolidayWeekendType;
import de.financer.model.RecurringTransaction;
import org.apache.commons.collections4.IterableUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.Collections;
import java.util.stream.Collectors;
@Service
public class RecurringTransactionService {
@Autowired
private RecurringTransactionRepository recurringTransactionRepository;
@Autowired
private AccountService accountService;
@Autowired
private RuleService ruleService;
public Iterable<RecurringTransaction> getAll() {
return this.recurringTransactionRepository.findAll();
}
public Iterable<RecurringTransaction> getAllForAccount(String accountKey) {
final Account account = this.accountService.getAccountByKey(accountKey);
if (account == null) {
return Collections.emptyList();
}
// As we want all transactions of the given account use it as from and to account
return this.recurringTransactionRepository.findRecurringTransactionsByFromAccountOrToAccount(account, account);
}
/**
* This method gets all recurring transactions that are due today. Whether a recurring transaction is due today
* depends on today's date and the configured {@link RecurringTransaction#getIntervalType() interval type}
* and {@link RecurringTransaction#getHolidayWeekendType() holiday weekend type}.
*
* @return all recurring transactions that are due today
*/
public Iterable<RecurringTransaction> getAllDueToday() {
return this.getAllDueToday(LocalDate.now());
}
// Visible for unit tests
/* package */ Iterable<RecurringTransaction> getAllDueToday(LocalDate now) {
// TODO filter for lastOccurrence not in the past
final Iterable<RecurringTransaction> allRecurringTransactions = this.recurringTransactionRepository.findAll();
//@formatter:off
return IterableUtils.toList(allRecurringTransactions).stream()
.filter((rt) -> checkRecurringTransactionDueToday(rt, now) ||
checkRecurringTransactionDuePast(rt, now))
// TODO checkRecurringTransactionDueFuture for HolidayWeekendType.PREVIOUS_WORKDAY
.collect(Collectors.toList());
//@formatter:on
}
/**
* This method checks whether the given {@link RecurringTransaction} is due today.
* A recurring transaction is due if the current {@link LocalDate date} is a multiple of the
* {@link RecurringTransaction#getFirstOccurrence() first occurrence} of the recurring transaction and the
* {@link RecurringTransaction#getIntervalType() interval type}. If today is a
* {@link RuleService#isHoliday(LocalDate) holiday} or a
* {@link RuleService#isWeekend(LocalDate) weekend day} the
* {@link HolidayWeekendType holiday weekend type}
* is taken into account to decide whether the recurring transaction should be deferred.
*
* @param recurringTransaction to check whether it is due today
* @param now today's date
* @return <code>true</code> if the recurring transaction is due today, <code>false</code> otherwise
*/
private boolean checkRecurringTransactionDueToday(RecurringTransaction recurringTransaction, LocalDate now) {
final boolean holiday = this.ruleService.isHoliday(now);
final boolean dueToday = recurringTransaction.getFirstOccurrence()
// This calculates all dates between the first occurrence of the
// recurring transaction and tomorrow for the interval specified
// by the recurring transaction. We need to use tomorrow as
// upper bound of the interval because the upper bound is exclusive
// in the datesUntil method.
.datesUntil(now.plusDays(1), this.ruleService
.getPeriodForInterval(recurringTransaction
.getIntervalType()))
// Then we check whether today is a date in the calculated range.
// If so the recurring transaction is due today
.anyMatch((d) -> d.equals(now));
final boolean weekend = this.ruleService.isWeekend(now);
boolean defer = false;
if (holiday || weekend) {
defer = recurringTransaction.getHolidayWeekendType() == HolidayWeekendType.NEXT_WORKDAY;
}
return !defer && dueToday;
}
/**
* This method checks whether the given {@link RecurringTransaction} was actually due in the close past
* but has been deferred to <i>maybe</i> today because the actual due day has been a holiday or weekend day and the
* {@link RecurringTransaction#getHolidayWeekendType() holiday weekend type} was
* {@link HolidayWeekendType#NEXT_WORKDAY}. Note that the recurring transaction may get deferred again if today
* again is a holiday or a weekend day.
* The period this method considers starts with today and ends with the last workday (no
* {@link RuleService#isHoliday(LocalDate) holiday}, not a {@link RuleService#isWeekend(LocalDate) weekend day})
* whereas the end is exclusive, because if the recurring transaction would have been due at the last workday day
* it wouldn't has been deferred.
*
* @param recurringTransaction to check whether it is due today
* @param now today's date
* @return <code>true</code> if the recurring transaction is due today, <code>false</code> otherwise
*/
private boolean checkRecurringTransactionDuePast(RecurringTransaction recurringTransaction, LocalDate now) {
boolean weekend;
boolean holiday;
LocalDate yesterday = now;
boolean due = false;
// Go back in time until we hit the first non-holiday, non-weekend day
// and check for every day in between if the given recurring transaction was due on this day
do {
yesterday = yesterday.minusDays(1);
holiday = this.ruleService.isHoliday(yesterday);
weekend = this.ruleService.isWeekend(yesterday);
if (holiday || weekend) {
// Lambdas require final local variables
final LocalDate finalYesterday = yesterday;
// For an explanation of the expression see the ...DueToday method
due = recurringTransaction.getFirstOccurrence()
.datesUntil(yesterday.plusDays(1), this.ruleService
.getPeriodForInterval(recurringTransaction
.getIntervalType()))
.anyMatch((d) -> d.equals(finalYesterday));
if (due) {
break;
}
}
}
while (holiday || weekend);
return due;
}
public ResponseReason createRecurringTransaction(String fromAccountKey, String toAccountKey, Long amount,
String description, String holidayWeekendType,
String intervalType, String firstOccurrence,
String lastOccurrence) {
return null;
}
}

View File

@@ -1,27 +1,51 @@
package de.financer.service; package de.financer.service;
import de.financer.config.FinancerConfig;
import de.financer.model.Account; import de.financer.model.Account;
import de.financer.model.AccountType; import de.financer.model.AccountType;
import de.financer.model.IntervalType;
import de.jollyday.HolidayManager;
import de.jollyday.ManagerParameters;
import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.Period;
import java.util.*; import java.util.*;
import static de.financer.model.AccountType.*; import static de.financer.model.AccountType.*;
/** /**
* This service encapsulates methods that form business logic rules. * This service encapsulates methods that form basic logic rules.
* While most of the logic could be placed elsewhere this service provides * While most of the logic could be placed elsewhere this service provides
* centralized access to these rules. * centralized access to these rules. Placing them in here also enables easy
* unit testing.
*/ */
@Service @Service
public class RuleService implements InitializingBean { public class RuleService implements InitializingBean {
@Autowired
private FinancerConfig financerConfig;
private Map<AccountType, Collection<AccountType>> bookingRules; private Map<AccountType, Collection<AccountType>> bookingRules;
private Map<IntervalType, Period> intervalPeriods;
@Override @Override
public void afterPropertiesSet() { public void afterPropertiesSet() {
initBookingRules(); initBookingRules();
initIntervalValues();
}
private void initIntervalValues() {
this.intervalPeriods = new EnumMap<>(IntervalType.class);
this.intervalPeriods.put(IntervalType.DAILY, Period.ofDays(1));
this.intervalPeriods.put(IntervalType.WEEKLY, Period.ofWeeks(1));
this.intervalPeriods.put(IntervalType.MONTHLY, Period.ofMonths(1));
this.intervalPeriods.put(IntervalType.QUARTERLY, Period.ofMonths(3));
this.intervalPeriods.put(IntervalType.YEARLY, Period.ofYears(1));
} }
private void initBookingRules() { private void initBookingRules() {
@@ -40,7 +64,7 @@ public class RuleService implements InitializingBean {
/** /**
* This method returns the multiplier for the given from account. * This method returns the multiplier for the given from account.
* * <p>
* The multiplier controls whether the current amount of the given from account is increased or * The multiplier controls whether the current amount of the given from account is increased or
* decreased depending on the {@link AccountType} of the given account. * decreased depending on the {@link AccountType} of the given account.
* *
@@ -55,17 +79,13 @@ public class RuleService implements InitializingBean {
if (INCOME.equals(accountType)) { if (INCOME.equals(accountType)) {
return 1L; return 1L;
} } else if (BANK.equals(accountType)) {
else if (BANK.equals(accountType)) {
return -1L; return -1L;
} } else if (CASH.equals(accountType)) {
else if (CASH.equals(accountType)) {
return -1L; return -1L;
} } else if (LIABILITY.equals(accountType)) {
else if (LIABILITY.equals(accountType)) {
return 1L; return 1L;
} } else if (START.equals(accountType)) {
else if (START.equals(accountType)) {
return 1L; return 1L;
} }
@@ -74,7 +94,7 @@ public class RuleService implements InitializingBean {
/** /**
* This method returns the multiplier for the given to account. * This method returns the multiplier for the given to account.
* * <p>
* The multiplier controls whether the current amount of the given to account is increased or * The multiplier controls whether the current amount of the given to account is increased or
* decreased depending on the {@link AccountType} of the given account. * decreased depending on the {@link AccountType} of the given account.
* *
@@ -89,14 +109,11 @@ public class RuleService implements InitializingBean {
if (BANK.equals(accountType)) { if (BANK.equals(accountType)) {
return 1L; return 1L;
} } else if (CASH.equals(accountType)) {
else if (CASH.equals(accountType)) {
return 1L; return 1L;
} } else if (LIABILITY.equals(accountType)) {
else if (LIABILITY.equals(accountType)) {
return -1L; return -1L;
} } else if (EXPENSE.equals(accountType)) {
else if (EXPENSE.equals(accountType)) {
return 1L; return 1L;
} }
@@ -109,10 +126,43 @@ public class RuleService implements InitializingBean {
* account does not make sense and is declared as invalid. * account does not make sense and is declared as invalid.
* *
* @param fromAccount the account to subtract the money from * @param fromAccount the account to subtract the money from
* @param toAccount the account to add the money to * @param toAccount the account to add the money to
* @return <code>true</code> if the from->to relationship of the given accounts is valid, <code>false</code> otherwise * @return <code>true</code> if the from-&gt;to relationship of the given accounts is valid, <code>false</code> otherwise
*/ */
public boolean isValidBooking(Account fromAccount, Account toAccount) { public boolean isValidBooking(Account fromAccount, Account toAccount) {
return this.bookingRules.get(fromAccount.getType()).contains(toAccount.getType()); return this.bookingRules.get(fromAccount.getType()).contains(toAccount.getType());
} }
/**
* This method gets the {@link Period} for the given {@link IntervalType},
* e.g. a period of three months for {@link IntervalType#QUARTERLY}.
*
* @param intervalType to get the period for
* @return the period matching the interval type
*/
public Period getPeriodForInterval(IntervalType intervalType) {
return this.intervalPeriods.get(intervalType);
}
/**
* This method checks whether the given date is a holiday in the configured country and state.
*
* @param now the date to check
* @return <code>true</code> if the given date is a holiday, <code>false</code> otherwise
*/
public boolean isHoliday(LocalDate now) {
return HolidayManager.getInstance(ManagerParameters.create(this.financerConfig.getHolidayCalendar()))
.isHoliday(now, this.financerConfig.getState());
}
/**
* This method checks whether the given date is a weekend day, i.e. whether it's a
* {@link DayOfWeek#SATURDAY} or {@link DayOfWeek#SUNDAY}.
*
* @param now the date to check
* @return <code>true</code> if the given date is a weekend day, <code>false</code> otherwise
*/
public boolean isWeekend(LocalDate now) {
return EnumSet.of(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY).contains(now.getDayOfWeek());
}
} }

View File

@@ -9,8 +9,9 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.text.ParseException; import java.time.LocalDate;
import java.text.SimpleDateFormat; import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Collections; import java.util.Collections;
@Service @Service
@@ -36,7 +37,7 @@ public class TransactionService {
/** /**
* @param accountKey the key of the account to get the transactions for * @param accountKey the key of the account to get the transactions for
* @return all transactions for the given account, for all time. Returns an empty list if the given key does not * @return all transactions for the given account, for all time. Returns an empty list if the given key does not
* match any account. * match any account.
*/ */
public Iterable<Transaction> getAllForAccount(String accountKey) { public Iterable<Transaction> getAllForAccount(String accountKey) {
final Account account = this.accountService.getAccountByKey(accountKey); final Account account = this.accountService.getAccountByKey(accountKey);
@@ -63,8 +64,10 @@ public class TransactionService {
try { try {
final Transaction transaction = buildTransaction(fromAccount, toAccount, amount, description, date); final Transaction transaction = buildTransaction(fromAccount, toAccount, amount, description, date);
fromAccount.setCurrentBalance(fromAccount.getCurrentBalance() + (this.ruleService.getMultiplierFromAccount(fromAccount) * amount)); fromAccount.setCurrentBalance(fromAccount.getCurrentBalance() + (this.ruleService
toAccount.setCurrentBalance(toAccount.getCurrentBalance() + (this.ruleService.getMultiplierToAccount(toAccount) * amount)); .getMultiplierFromAccount(fromAccount) * amount));
toAccount.setCurrentBalance(toAccount.getCurrentBalance() + (this.ruleService
.getMultiplierToAccount(toAccount) * amount));
this.transactionRepository.save(transaction); this.transactionRepository.save(transaction);
@@ -72,13 +75,11 @@ public class TransactionService {
this.accountService.saveAccount(toAccount); this.accountService.saveAccount(toAccount);
response = ResponseReason.OK; response = ResponseReason.OK;
} } catch (DateTimeParseException e) {
catch(ParseException e) {
// TODO log // TODO log
response = ResponseReason.INVALID_DATE_FORMAT; response = ResponseReason.INVALID_DATE_FORMAT;
} } catch (Exception e) {
catch (Exception e) {
// TODO log // TODO log
response = ResponseReason.UNKNOWN_ERROR; response = ResponseReason.UNKNOWN_ERROR;
@@ -91,21 +92,21 @@ public class TransactionService {
* This method builds the actual transaction object with the given values. * This method builds the actual transaction object with the given values.
* *
* @param fromAccount the from account * @param fromAccount the from account
* @param toAccount the to account * @param toAccount the to account
* @param amount the transaction amount * @param amount the transaction amount
* @param description the description of the transaction * @param description the description of the transaction
* @param date the date of the transaction * @param date the date of the transaction
* @return the build {@link Transaction} instance * @return the build {@link Transaction} instance
* @throws ParseException if the given date string cannot be parsed into a {@link java.util.Date} instance * @throws DateTimeParseException if the given date string cannot be parsed into a {@link java.time.LocalDate} instance
*/ */
private Transaction buildTransaction(Account fromAccount, Account toAccount, Long amount, String description, String date) throws ParseException { private Transaction buildTransaction(Account fromAccount, Account toAccount, Long amount, String description, String date) throws DateTimeParseException {
final Transaction transaction = new Transaction(); final Transaction transaction = new Transaction();
transaction.setFromAccount(fromAccount); transaction.setFromAccount(fromAccount);
transaction.setToAccount(toAccount); transaction.setToAccount(toAccount);
transaction.setAmount(amount); transaction.setAmount(amount);
transaction.setDescription(description); transaction.setDescription(description);
transaction.setDate(new SimpleDateFormat(DATE_FORMAT).parse(date)); transaction.setDate(LocalDate.parse(date, DateTimeFormatter.ofPattern(DATE_FORMAT)));
return transaction; return transaction;
} }
@@ -114,9 +115,9 @@ public class TransactionService {
* This method checks whether the parameters for creating a transaction are valid. * This method checks whether the parameters for creating a transaction are valid.
* *
* @param fromAccount the from account * @param fromAccount the from account
* @param toAccount the to account * @param toAccount the to account
* @param amount the transaction amount * @param amount the transaction amount
* @param date the transaction date * @param date the transaction date
* @return the first error found or <code>null</code> if all parameters are valid * @return the first error found or <code>null</code> if all parameters are valid
*/ */
private ResponseReason validateParameters(Account fromAccount, Account toAccount, Long amount, String date) { private ResponseReason validateParameters(Account fromAccount, Account toAccount, Long amount, String date) {
@@ -124,23 +125,17 @@ public class TransactionService {
if (fromAccount == null && toAccount == null) { if (fromAccount == null && toAccount == null) {
response = ResponseReason.FROM_AND_TO_ACCOUNT_NOT_FOUND; response = ResponseReason.FROM_AND_TO_ACCOUNT_NOT_FOUND;
} } else if (toAccount == null) {
else if (toAccount == null) {
response = ResponseReason.TO_ACCOUNT_NOT_FOUND; response = ResponseReason.TO_ACCOUNT_NOT_FOUND;
} } else if (fromAccount == null) {
else if (fromAccount == null) {
response = ResponseReason.FROM_ACCOUNT_NOT_FOUND; response = ResponseReason.FROM_ACCOUNT_NOT_FOUND;
} } else if (!this.ruleService.isValidBooking(fromAccount, toAccount)) {
else if (!this.ruleService.isValidBooking(fromAccount, toAccount)) {
response = ResponseReason.INVALID_BOOKING_ACCOUNTS; response = ResponseReason.INVALID_BOOKING_ACCOUNTS;
} } else if (amount == null) {
else if (amount == null) {
response = ResponseReason.MISSING_AMOUNT; response = ResponseReason.MISSING_AMOUNT;
} } else if (amount == 0l) {
else if (amount == 0l) {
response = ResponseReason.AMOUNT_ZERO; response = ResponseReason.AMOUNT_ZERO;
} } else if (date == null) {
else if (date == null) {
response = ResponseReason.MISSING_DATE; response = ResponseReason.MISSING_DATE;
} }

View File

@@ -0,0 +1,9 @@
/**
* <p>
* This package contains the actual business logic services of the financer application.
* They get called by the {@link de.financer.controller controller} layer. Also they call each other,
* e.g. the {@link de.financer.service.TransactionService} internally uses the {@link de.financer.service.RuleService}.
* Data access is done via the {@link de.financer.dba DBA} layer.
* </p>
*/
package de.financer.service;

View File

@@ -12,4 +12,13 @@ info.app.name=Financer
info.app.description=A simple server for personal finance administration info.app.description=A simple server for personal finance administration
info.build.group=@project.groupId@ info.build.group=@project.groupId@
info.build.artifact=@project.artifactId@ info.build.artifact=@project.artifactId@
info.build.version=@project.version@ info.build.version=@project.version@
# Country code for holiday checks
# Mostly an uppercase ISO 3166 2-letter code
# For a complete list of the supported codes see https://github.com/svendiedrichsen/jollyday/blob/master/src/main/java/de/jollyday/HolidayCalendar.java
financer.countryCode=DE
# The state used for holiday checks
# For a complete list of the supported states see e.g. https://github.com/svendiedrichsen/jollyday/blob/master/src/main/resources/holidays/Holidays_de.xml
financer.state=sl

View File

@@ -1,8 +1,8 @@
-- --
-- This file contains the basic initialization of the financer schema and init data -- This file contains the basic initialization of the financer schema
-- --
-- Account table and init data -- Account table
CREATE TABLE account ( CREATE TABLE account (
id BIGINT NOT NULL PRIMARY KEY IDENTITY, id BIGINT NOT NULL PRIMARY KEY IDENTITY,
"key" VARCHAR(1000) NOT NULL, --escape keyword "key" "key" VARCHAR(1000) NOT NULL, --escape keyword "key"
@@ -13,18 +13,6 @@ CREATE TABLE account (
CONSTRAINT un_account_name_key UNIQUE ("key") CONSTRAINT un_account_name_key UNIQUE ("key")
); );
INSERT INTO account ("key", type, status, current_balance)
VALUES ('accounts.checkaccount', 'BANK', 'OPEN', 0);
INSERT INTO account ("key", type, status, current_balance)
VALUES ('accounts.income', 'INCOME', 'OPEN', 0);
INSERT INTO account ("key", type, status, current_balance)
VALUES ('accounts.cash', 'CASH', 'OPEN', 0);
INSERT INTO account ("key", type, status, current_balance)
VALUES ('accounts.start', 'START', 'OPEN', 0);
-- Recurring transaction table -- Recurring transaction table
CREATE TABLE recurring_transaction ( CREATE TABLE recurring_transaction (
id BIGINT NOT NULL PRIMARY KEY IDENTITY, id BIGINT NOT NULL PRIMARY KEY IDENTITY,

View File

@@ -36,12 +36,14 @@ public class AccountControllerIntegrationTest {
@Test @Test
public void test_getAll() throws Exception { public void test_getAll() throws Exception {
final MvcResult mvcResult = this.mockMvc.perform(get("/accounts/getAll").contentType(MediaType.APPLICATION_JSON)) final MvcResult mvcResult = this.mockMvc
.andExpect(status().isOk()) .perform(get("/accounts/getAll").contentType(MediaType.APPLICATION_JSON))
.andReturn(); .andExpect(status().isOk())
.andReturn();
final List<Account> allAccounts = this.objectMapper.readValue(mvcResult.getResponse().getContentAsByteArray(), new TypeReference<List<Account>>(){}); final List<Account> allAccounts = this.objectMapper
.readValue(mvcResult.getResponse().getContentAsByteArray(), new TypeReference<List<Account>>() {});
Assert.assertEquals(4, allAccounts.size()); Assert.assertEquals(5, allAccounts.size());
} }
} }

View File

@@ -0,0 +1,134 @@
package de.financer.service;
import de.financer.dba.RecurringTransactionRepository;
import de.financer.model.HolidayWeekendType;
import de.financer.model.IntervalType;
import de.financer.model.RecurringTransaction;
import org.apache.commons.collections4.IterableUtils;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import java.time.LocalDate;
import java.time.Period;
import java.util.Collections;
/**
* This class contains tests for the {@link RecurringTransactionService}, specifically for
* {@link RecurringTransaction}s that have {@link IntervalType#DAILY} and {@link HolidayWeekendType#NEXT_WORKDAY}.
* Due to these restrictions this class does not contain any tests for recurring transactions due in the close past
* that have been deferred, because recurring transactions with interval type daily get executed on the next workday
* anyway, regardless whether they have been deferred. This means that some executions of a recurring transaction with
* daily/next workday get ignored if they are on a holiday or a weekend day - they do <b>not</b> get executed multiple
* times on the next workday. While this is somehow unfortunate it is <b>not</b> in the current requirements and
* therefore left out for the sake of simplicity. If such a behavior is required daily/same day should do the trick,
* even though with slightly different semantics (execution even on holidays or weekends).
*/
@RunWith(MockitoJUnitRunner.class)
public class RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest {
@InjectMocks
private RecurringTransactionService classUnderTest;
@Mock
private RecurringTransactionRepository recurringTransactionRepository;
@Mock
private RuleService ruleService;
@Before
public void setUp() {
Mockito.when(this.ruleService.getPeriodForInterval(IntervalType.DAILY)).thenReturn(Period.ofDays(1));
}
/**
* This method tests whether a recurring transaction with firstOccurrence = three days ago, intervalType = daily and
* holidayWeekendType = next_workday is due on a non-holiday, non-weekend day
*/
@Test
public void test_getAllDueToday_dueToday() {
// Arrange
// Implicitly: ruleService.isHoliday().return(false) and ruleService.isWeekend().return(false)
Mockito.when(this.recurringTransactionRepository.findAll()).thenReturn(Collections.singletonList(createRecurringTransaction(-3)));
final LocalDate now = LocalDate.now();
// Act
final Iterable<RecurringTransaction> recurringDueToday = this.classUnderTest.getAllDueToday(now);
// Assert
Assert.assertEquals(1, IterableUtils.size(recurringDueToday));
}
/**
* This method tests whether a recurring transaction with firstOccurrence = today, intervalType = daily and
* holidayWeekendType = next_workday is <b>not</b> due on a holiday, non-weekend day
*/
@Test
public void test_getAllDueToday_dueToday_holiday() {
// Arrange
// Implicitly: ruleService.isWeekend().return(false)
Mockito.when(this.recurringTransactionRepository.findAll()).thenReturn(Collections.singletonList(createRecurringTransaction(0)));
// Today is a holiday, but yesterday was not
Mockito.when(this.ruleService.isHoliday(Mockito.any())).thenReturn(Boolean.TRUE, Boolean.FALSE);
final LocalDate now = LocalDate.now();
// Act
final Iterable<RecurringTransaction> recurringDueToday = this.classUnderTest.getAllDueToday(now);
// Assert
Assert.assertEquals(0, IterableUtils.size(recurringDueToday));
}
/**
* This method tests whether a recurring transaction with firstOccurrence = today, intervalType = daily and
* holidayWeekendType = next_workday is <b>not</b> due on a non-holiday, weekend day
*/
@Test
public void test_getAllDueToday_dueToday_weekend() {
// Arrange
// Implicitly: ruleService.isHoliday().return(false)
Mockito.when(this.recurringTransactionRepository.findAll()).thenReturn(Collections.singletonList(createRecurringTransaction(0)));
// Today is a weekend day, but yesterday was not
Mockito.when(this.ruleService.isWeekend(Mockito.any())).thenReturn(Boolean.TRUE, Boolean.FALSE);
final LocalDate now = LocalDate.now();
// Act
final Iterable<RecurringTransaction> recurringDueToday = this.classUnderTest.getAllDueToday(now);
// Assert
Assert.assertEquals(0, IterableUtils.size(recurringDueToday));
}
/**
* This method tests whether a recurring transaction with firstOccurrence = tomorrow, intervalType = daily and
* holidayWeekendType = next_workday is <b>not</b> due today
*/
@Test
public void test_getAllDueToday_dueToday_tomorrow() {
// Arrange
// Implicitly: ruleService.isHoliday().return(false) and ruleService.isWeekend().return(false)
Mockito.when(this.recurringTransactionRepository.findAll()).thenReturn(Collections.singletonList(createRecurringTransaction(1)));
final LocalDate now = LocalDate.now();
// Act
final Iterable<RecurringTransaction> recurringDueToday = this.classUnderTest.getAllDueToday(now);
// Assert
Assert.assertEquals(0, IterableUtils.size(recurringDueToday));
}
private RecurringTransaction createRecurringTransaction(int days) {
final RecurringTransaction recurringTransaction = new RecurringTransaction();
recurringTransaction.setFirstOccurrence(LocalDate.now().plusDays(days));
recurringTransaction.setHolidayWeekendType(HolidayWeekendType.NEXT_WORKDAY);
recurringTransaction.setIntervalType(IntervalType.DAILY);
return recurringTransaction;
}
}

View File

@@ -0,0 +1,156 @@
package de.financer.service;
import de.financer.dba.RecurringTransactionRepository;
import de.financer.model.HolidayWeekendType;
import de.financer.model.IntervalType;
import de.financer.model.RecurringTransaction;
import org.apache.commons.collections4.IterableUtils;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import java.time.LocalDate;
import java.time.Period;
import java.util.Collections;
@RunWith(MockitoJUnitRunner.class)
public class RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest {
@InjectMocks
private RecurringTransactionService classUnderTest;
@Mock
private RecurringTransactionRepository recurringTransactionRepository;
@Mock
private RuleService ruleService;
@Before
public void setUp() {
Mockito.when(this.ruleService.getPeriodForInterval(IntervalType.MONTHLY)).thenReturn(Period.ofMonths(1));
}
/**
* This method tests whether a recurring transaction with firstOccurrence = one month and one day ago
* (and thus was actually due yesterday), intervalType = monthly and holidayWeekendType = next_workday is due today,
* if yesterday was a holiday but today is not
*/
@Test
public void test_getAllDueToday_duePast_holiday() {
// Arrange
Mockito.when(this.recurringTransactionRepository.findAll())
.thenReturn(Collections.singletonList(createRecurringTransaction(-1)));
// Today is not a holiday but yesterday was
Mockito.when(this.ruleService.isHoliday(Mockito.any())).thenReturn(Boolean.FALSE, Boolean.TRUE);
final LocalDate now = LocalDate.now();
// Act
final Iterable<RecurringTransaction> recurringDueToday = this.classUnderTest.getAllDueToday(now);
// Assert
Assert.assertEquals(1, IterableUtils.size(recurringDueToday));
}
/**
* This method tests whether a recurring transaction with firstOccurrence = last friday one month ago
* (and thus was actually due last friday), intervalType = monthly and holidayWeekendType = next_workday is due
* today (monday), if friday was holiday
*/
@Test
public void test_getAllDueToday_duePast_weekend_friday_holiday() {
//@formatter:off
// MO TU WE TH FR SA SU -> Weekdays
// 1 2 3 4 5 6 7 -> Ordinal
// H WE WE -> Holiday/WeekEnd
// X -> Scheduled recurring transaction
// O -> now
//
// So now - (ordinal +- offset)
// now - (3 - 1) = previous MO
// now - 3 = previous SU
// now - (3 + 2) = previous FR
//@formatter:on
// Arrange
final LocalDate now = LocalDate.now();
final LocalDate monday = now.minusDays(now.getDayOfWeek().getValue() - 1);
// The transaction occurs on a friday
Mockito.when(this.recurringTransactionRepository.findAll())
.thenReturn(Collections.singletonList(createRecurringTransaction(-(now.getDayOfWeek().getValue() + 2))));
// First False for the dueToday check, 2x True for actual weekend, second False for Friday
Mockito.when(this.ruleService.isWeekend(Mockito.any()))
.thenReturn(Boolean.FALSE, Boolean.TRUE, Boolean.TRUE, Boolean.FALSE);
// First False for the dueToday check, 2x False for actual weekend, True for Friday
Mockito.when(this.ruleService.isHoliday(Mockito.any()))
.thenReturn(Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, Boolean.TRUE);
// Act
final Iterable<RecurringTransaction> recurringDueToday = this.classUnderTest.getAllDueToday(monday);
// Assert
Assert.assertEquals(1, IterableUtils.size(recurringDueToday));
}
/**
* This method tests whether a recurring transaction with firstOccurrence = last sunday a month ago
* (and thus was actually due last sunday/yesterday), intervalType = monthly and holidayWeekendType = next_workday
* is due today (monday)
*/
@Test
public void test_getAllDueToday_duePast_weekend_sunday() {
// Arrange
final LocalDate now = LocalDate.now();
final LocalDate monday = now.minusDays(now.getDayOfWeek().getValue() - 1);
// The transaction occurs on a sunday
Mockito.when(this.recurringTransactionRepository.findAll())
.thenReturn(Collections.singletonList(createRecurringTransaction(-now.getDayOfWeek().getValue())));
// First False for the dueToday check, 2x True for actual weekend, second False for Friday
Mockito.when(this.ruleService.isWeekend(Mockito.any()))
.thenReturn(Boolean.FALSE, Boolean.TRUE, Boolean.TRUE, Boolean.FALSE);
// Act
final Iterable<RecurringTransaction> recurringDueToday = this.classUnderTest.getAllDueToday(monday);
// Assert
Assert.assertEquals(1, IterableUtils.size(recurringDueToday));
}
/**
* This method tests whether a recurring transaction with firstOccurrence = saturday a month ago
* (and thus was actually due last saturday/two days ago), intervalType = monthly and
* holidayWeekendType = next_workday is due today (monday)
*/
@Test
public void test_getAllDueToday_duePast_weekend_saturday() {
// Arrange
final LocalDate now = LocalDate.now();
final LocalDate monday = now.minusDays(now.getDayOfWeek().getValue() - 1);
// The transaction occurs on a saturday
Mockito.when(this.recurringTransactionRepository.findAll())
.thenReturn(Collections.singletonList(createRecurringTransaction(-(now.getDayOfWeek().getValue() + 1))));
// First False for the dueToday check, 2x True for actual weekend, second False for Friday
Mockito.when(this.ruleService.isWeekend(Mockito.any()))
.thenReturn(Boolean.FALSE, Boolean.TRUE, Boolean.TRUE, Boolean.FALSE);
// Act
final Iterable<RecurringTransaction> recurringDueToday = this.classUnderTest.getAllDueToday(monday);
// Assert
Assert.assertEquals(1, IterableUtils.size(recurringDueToday));
}
private RecurringTransaction createRecurringTransaction(int days) {
final RecurringTransaction recurringTransaction = new RecurringTransaction();
recurringTransaction.setFirstOccurrence(LocalDate.now().plusDays(days).minusMonths(1));
recurringTransaction.setHolidayWeekendType(HolidayWeekendType.NEXT_WORKDAY);
recurringTransaction.setIntervalType(IntervalType.MONTHLY);
return recurringTransaction;
}
}

View File

@@ -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;
}
}

View File

@@ -1,11 +1,3 @@
###
### This is the main configuration file for integration tests
###
server.servlet.context-path=/financer
spring.jpa.hibernate.ddl-auto=validate
spring.datasource.url=jdbc:hsqldb:mem:. spring.datasource.url=jdbc:hsqldb:mem:.
spring.datasource.username=sa spring.datasource.username=sa
spring.flyway.locations=classpath:/database/hsqldb spring.flyway.locations=classpath:/database/hsqldb,classpath:/database/hsqldb/integration

View File

@@ -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');