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
This commit is contained in:
2019-03-05 23:13:26 +01:00
parent 35902afe43
commit 449e8fb216
6 changed files with 282 additions and 40 deletions

View File

@@ -15,7 +15,14 @@ public enum ResponseReason {
MISSING_DATE(HttpStatus.INTERNAL_SERVER_ERROR), MISSING_DATE(HttpStatus.INTERNAL_SERVER_ERROR),
AMOUNT_ZERO(HttpStatus.INTERNAL_SERVER_ERROR), AMOUNT_ZERO(HttpStatus.INTERNAL_SERVER_ERROR),
MISSING_AMOUNT(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; private HttpStatus httpStatus;

View File

@@ -4,6 +4,7 @@ import de.jollyday.HolidayCalendar;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import java.time.format.DateTimeFormatter;
import java.util.Arrays; import java.util.Arrays;
import java.util.Optional; import java.util.Optional;
@@ -12,6 +13,7 @@ import java.util.Optional;
public class FinancerConfig { public class FinancerConfig {
private String countryCode; private String countryCode;
private String state; private String state;
private String dateFormat;
/** /**
* @return the raw country code, mostly an uppercase ISO 3166 2-letter code * @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) { public void setCountryCode(String countryCode) {
this.countryCode = 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;
}
} }

View File

@@ -1,15 +1,21 @@
package de.financer.service; package de.financer.service;
import de.financer.ResponseReason; import de.financer.ResponseReason;
import de.financer.config.FinancerConfig;
import de.financer.dba.RecurringTransactionRepository; import de.financer.dba.RecurringTransactionRepository;
import de.financer.model.Account; import de.financer.model.Account;
import de.financer.model.HolidayWeekendType; import de.financer.model.HolidayWeekendType;
import de.financer.model.IntervalType;
import de.financer.model.RecurringTransaction; import de.financer.model.RecurringTransaction;
import org.apache.commons.collections4.IterableUtils; import org.apache.commons.collections4.IterableUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Collections; import java.util.Collections;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -25,6 +31,9 @@ public class RecurringTransactionService {
@Autowired @Autowired
private RuleService ruleService; private RuleService ruleService;
@Autowired
private FinancerConfig financerConfig;
public Iterable<RecurringTransaction> getAll() { public Iterable<RecurringTransaction> getAll() {
return this.recurringTransactionRepository.findAll(); 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 * 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} * depends on today's date and the configured {@link RecurringTransaction#getIntervalType() interval type} and
* and {@link RecurringTransaction#getHolidayWeekendType() holiday weekend type}. * {@link RecurringTransaction#getHolidayWeekendType() holiday weekend type}.
* *
* @return all recurring transactions that are due today * @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. * This method checks whether the given {@link RecurringTransaction} is due today. A recurring transaction is due if
* A recurring transaction is due if the current {@link LocalDate date} is a multiple of the * the current {@link LocalDate date} is a multiple of the {@link RecurringTransaction#getFirstOccurrence() first
* {@link RecurringTransaction#getFirstOccurrence() first occurrence} of the recurring transaction and the * occurrence} of the recurring transaction and the {@link RecurringTransaction#getIntervalType() interval type}. If
* {@link RecurringTransaction#getIntervalType() interval type}. If today is a * today is a {@link RuleService#isHoliday(LocalDate) holiday} or a {@link RuleService#isWeekend(LocalDate) weekend
* {@link RuleService#isHoliday(LocalDate) holiday} or a * day} the {@link HolidayWeekendType holiday weekend type} is taken into account to decide whether the recurring
* {@link RuleService#isWeekend(LocalDate) weekend day} the * transaction should be deferred.
* {@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 recurringTransaction to check whether it is due today
* @param now today's date * @param now today's date
*
* @return <code>true</code> if the recurring transaction is due today, <code>false</code> otherwise * @return <code>true</code> if the recurring transaction is due today, <code>false</code> otherwise
*/ */
private boolean checkRecurringTransactionDueToday(RecurringTransaction recurringTransaction, LocalDate now) { 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 * This method checks whether the given {@link RecurringTransaction} was actually due in the close past but has been
* but has been deferred to <i>maybe</i> today because the actual due day has been a holiday or weekend day and the * deferred to <i>maybe</i> today because the actual due day has been a holiday or weekend day and the {@link
* {@link RecurringTransaction#getHolidayWeekendType() holiday weekend type} was * RecurringTransaction#getHolidayWeekendType() holiday weekend type} was {@link HolidayWeekendType#NEXT_WORKDAY}.
* {@link HolidayWeekendType#NEXT_WORKDAY}. Note that the recurring transaction may get deferred again if today * Note that the recurring transaction may get deferred again if today again is a holiday or a weekend day. The
* again is a holiday or a weekend day. * period this method considers starts with today and ends with the last workday (no {@link
* The period this method considers starts with today and ends with the last workday (no * RuleService#isHoliday(LocalDate) holiday}, not a {@link RuleService#isWeekend(LocalDate) weekend day}) whereas
* {@link RuleService#isHoliday(LocalDate) holiday}, not a {@link RuleService#isWeekend(LocalDate) weekend day}) * the end is exclusive, because if the recurring transaction would have been due at the last workday day it
* whereas the end is exclusive, because if the recurring transaction would have been due at the last workday day * wouldn't has been deferred.
* it wouldn't has been deferred.
* *
* @param recurringTransaction to check whether it is due today * @param recurringTransaction to check whether it is due today
* @param now today's date * @param now today's date
*
* @return <code>true</code> if the recurring transaction is due today, <code>false</code> otherwise * @return <code>true</code> if the recurring transaction is due today, <code>false</code> otherwise
*/ */
private boolean checkRecurringTransactionDuePast(RecurringTransaction recurringTransaction, LocalDate now) { 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 weekend;
boolean holiday; boolean holiday;
LocalDate yesterday = now; LocalDate yesterday = now;
@@ -154,10 +167,129 @@ public class RecurringTransactionService {
return due; return due;
} }
@Transactional(propagation = Propagation.REQUIRED)
public ResponseReason createRecurringTransaction(String fromAccountKey, String toAccountKey, Long amount, public ResponseReason createRecurringTransaction(String fromAccountKey, String toAccountKey, Long amount,
String description, String holidayWeekendType, String description, String holidayWeekendType,
String intervalType, String firstOccurrence, String intervalType, String firstOccurrence,
String lastOccurrence) { String lastOccurrence
return null; ) {
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 <code>null</code>
*
* @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 <code>null</code>
*
* @return the first error found or <code>null</code> if all parameters are valid
*/
private ResponseReason validateParameters(Account fromAccount, Account toAccount, Long amount,
String holidayWeekendType, String intervalType, String firstOccurrence,
String lastOccurrence
) {
ResponseReason response = null;
if (fromAccount == null && toAccount == null) {
response = ResponseReason.FROM_AND_TO_ACCOUNT_NOT_FOUND;
} else if (toAccount == null) {
response = ResponseReason.TO_ACCOUNT_NOT_FOUND;
} else if (fromAccount == null) {
response = ResponseReason.FROM_ACCOUNT_NOT_FOUND;
} else if (!this.ruleService.isValidBooking(fromAccount, toAccount)) {
response = ResponseReason.INVALID_BOOKING_ACCOUNTS;
} else if (amount == null) {
response = ResponseReason.MISSING_AMOUNT;
} else if (amount == 0l) {
response = ResponseReason.AMOUNT_ZERO;
} else if (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;
} }
} }

View File

@@ -1,6 +1,7 @@
package de.financer.service; package de.financer.service;
import de.financer.ResponseReason; import de.financer.ResponseReason;
import de.financer.config.FinancerConfig;
import de.financer.dba.TransactionRepository; import de.financer.dba.TransactionRepository;
import de.financer.model.Account; import de.financer.model.Account;
import de.financer.model.Transaction; import de.financer.model.Transaction;
@@ -16,8 +17,6 @@ import java.util.Collections;
@Service @Service
public class TransactionService { public class TransactionService {
public static final String DATE_FORMAT = "dd.MM.yyyy";
@Autowired @Autowired
private AccountService accountService; private AccountService accountService;
@@ -27,6 +26,9 @@ public class TransactionService {
@Autowired @Autowired
private TransactionRepository transactionRepository; private TransactionRepository transactionRepository;
@Autowired
private FinancerConfig financerConfig;
/** /**
* @return all transactions, for all accounts and all time * @return all transactions, for all accounts and all time
*/ */
@@ -75,10 +77,6 @@ public class TransactionService {
this.accountService.saveAccount(toAccount); this.accountService.saveAccount(toAccount);
response = ResponseReason.OK; response = ResponseReason.OK;
} catch (DateTimeParseException e) {
// TODO log
response = ResponseReason.INVALID_DATE_FORMAT;
} catch (Exception e) { } catch (Exception e) {
// TODO log // TODO log
@@ -92,21 +90,20 @@ 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 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(); 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(LocalDate.parse(date, DateTimeFormatter.ofPattern(DATE_FORMAT))); transaction.setDate(LocalDate.parse(date, DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat())));
return transaction; return transaction;
} }
@@ -115,9 +112,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) {
@@ -139,6 +136,14 @@ public class TransactionService {
response = ResponseReason.MISSING_DATE; response = ResponseReason.MISSING_DATE;
} }
try {
LocalDate.parse(date, DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat()));
}
catch (DateTimeParseException e) {
response = ResponseReason.INVALID_DATE_FORMAT;
}
return response; return response;
} }
} }

View File

@@ -21,4 +21,7 @@ financer.countryCode=DE
# The state used for holiday checks # 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 # 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 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

View File

@@ -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<RecurringTransaction> recurringDueToday = this.classUnderTest.getAllDueToday(now);
// Assert
Assert.assertEquals(0, IterableUtils.size(recurringDueToday));
}
private RecurringTransaction createRecurringTransaction(int days) {
final RecurringTransaction recurringTransaction = new RecurringTransaction();
recurringTransaction.setFirstOccurrence(LocalDate.now().plusDays(days).minusMonths(1));
recurringTransaction.setHolidayWeekendType(HolidayWeekendType.SAME_DAY);
recurringTransaction.setIntervalType(IntervalType.MONTHLY);
return recurringTransaction;
}
}