From b6318c65d48a6891c5845767b8cc667fdc93e166 Mon Sep 17 00:00:00 2001 From: MK13 Date: Thu, 7 Mar 2019 23:48:47 +0100 Subject: [PATCH] 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');