diff --git a/pom.xml b/pom.xml
index 8fb3e96..790b37d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -58,6 +58,11 @@
jollyday
0.5.7
+
+
+ org.glassfish.jaxb
+ jaxb-runtime
+
diff --git a/src/main/java/de/financer/FinancerApplication.java b/src/main/java/de/financer/FinancerApplication.java
index e3cace7..670d579 100644
--- a/src/main/java/de/financer/FinancerApplication.java
+++ b/src/main/java/de/financer/FinancerApplication.java
@@ -2,8 +2,10 @@ package de.financer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
+@EnableScheduling
public class FinancerApplication {
public static void main(String[] args) {
SpringApplication.run(FinancerApplication.class);
diff --git a/src/main/java/de/financer/config/FinancerConfig.java b/src/main/java/de/financer/config/FinancerConfig.java
index 3041b2e..f08e8a1 100644
--- a/src/main/java/de/financer/config/FinancerConfig.java
+++ b/src/main/java/de/financer/config/FinancerConfig.java
@@ -6,6 +6,7 @@ import org.springframework.context.annotation.Configuration;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
+import java.util.Collection;
import java.util.Optional;
@Configuration
@@ -14,6 +15,8 @@ public class FinancerConfig {
private String countryCode;
private String state;
private String dateFormat;
+ private Collection mailRecipients;
+ private String fromAddress;
/**
* @return the raw country code, mostly an uppercase ISO 3166 2-letter code
@@ -77,4 +80,26 @@ public class FinancerConfig {
this.dateFormat = dateFormat;
}
+
+ /**
+ * @return a collection of email addresses that should receive mails from financer
+ */
+ public Collection getMailRecipients() {
+ return mailRecipients;
+ }
+
+ public void setMailRecipients(Collection mailRecipients) {
+ this.mailRecipients = mailRecipients;
+ }
+
+ /**
+ * @return the from address used in emails send by financer
+ */
+ public String getFromAddress() {
+ return fromAddress;
+ }
+
+ public void setFromAddress(String fromAddress) {
+ this.fromAddress = fromAddress;
+ }
}
diff --git a/src/main/java/de/financer/controller/RecurringTransactionController.java b/src/main/java/de/financer/controller/RecurringTransactionController.java
index 5b19f5d..792ea56 100644
--- a/src/main/java/de/financer/controller/RecurringTransactionController.java
+++ b/src/main/java/de/financer/controller/RecurringTransactionController.java
@@ -7,6 +7,8 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
+import java.util.Optional;
+
@RestController
@RequestMapping("recurringTransactions")
public class RecurringTransactionController {
@@ -42,7 +44,7 @@ public class RecurringTransactionController {
}
@RequestMapping("createTransaction")
- public ResponseEntity createTransaction(String recurringTransactionId) {
- return this.recurringTransactionService.createTransaction(recurringTransactionId).toResponseEntity();
+ public ResponseEntity createTransaction(String recurringTransactionId, Long amount) {
+ return this.recurringTransactionService.createTransaction(recurringTransactionId, Optional.of(amount)).toResponseEntity();
}
}
diff --git a/src/main/java/de/financer/service/RecurringTransactionService.java b/src/main/java/de/financer/service/RecurringTransactionService.java
index fd9a477..1e6b00e 100644
--- a/src/main/java/de/financer/service/RecurringTransactionService.java
+++ b/src/main/java/de/financer/service/RecurringTransactionService.java
@@ -360,7 +360,7 @@ public class RecurringTransactionService {
}
@Transactional(propagation = Propagation.REQUIRED)
- public ResponseReason createTransaction(String recurringTransactionId) {
+ public ResponseReason createTransaction(String recurringTransactionId, Optional amount) {
if (recurringTransactionId == null) {
return ResponseReason.MISSING_RECURRING_TRANSACTION_ID;
} else if (!NumberUtils.isCreatable(recurringTransactionId)) {
@@ -378,7 +378,7 @@ public class RecurringTransactionService {
return this.transactionService.createTransaction(recurringTransaction.getFromAccount().getKey(),
recurringTransaction.getToAccount().getKey(),
- recurringTransaction.getAmount(),
+ amount.orElseGet(() -> recurringTransaction.getAmount()),
LocalDate.now().format(DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat())),
recurringTransaction.getDescription(),
recurringTransaction);
diff --git a/src/main/java/de/financer/task/SendRecurringTransactionReminderTask.java b/src/main/java/de/financer/task/SendRecurringTransactionReminderTask.java
new file mode 100644
index 0000000..124379b
--- /dev/null
+++ b/src/main/java/de/financer/task/SendRecurringTransactionReminderTask.java
@@ -0,0 +1,69 @@
+package de.financer.task;
+
+import de.financer.config.FinancerConfig;
+import de.financer.model.RecurringTransaction;
+import de.financer.service.RecurringTransactionService;
+import org.apache.commons.collections4.IterableUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.mail.MailException;
+import org.springframework.mail.SimpleMailMessage;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+@Component
+public class SendRecurringTransactionReminderTask {
+
+ @Autowired
+ private RecurringTransactionService recurringTransactionService;
+
+ @Autowired
+ private FinancerConfig financerConfig;
+
+ @Autowired
+ private JavaMailSender mailSender;
+
+ @Scheduled(cron = "0 30 0 * * *")
+ public void sendReminder() {
+ final Iterable recurringTransactions = this.recurringTransactionService.getAllDueToday();
+
+ // If no recurring transaction is due today we don't need to send a reminder
+ if (IterableUtils.isEmpty(recurringTransactions)) {
+ return; // early return
+ }
+
+ final StringBuilder reminderBuilder = new StringBuilder();
+
+ reminderBuilder.append("The following recurring transactions are due today:")
+ .append(System.lineSeparator())
+ .append(System.lineSeparator());
+
+ IterableUtils.toList(recurringTransactions).stream().forEach((rt) -> {
+ reminderBuilder.append(rt.getId())
+ .append("|")
+ .append(rt.getDescription())
+ .append(System.lineSeparator())
+ .append("From ")
+ .append(rt.getFromAccount().getKey())
+ .append(" to ")
+ .append(rt.getToAccount().getKey())
+ .append(": ")
+ .append(rt.getAmount().toString())
+ .append(System.lineSeparator())
+ .append(System.lineSeparator());
+ });
+
+ final SimpleMailMessage msg = new SimpleMailMessage();
+
+ msg.setTo(this.financerConfig.getMailRecipients().toArray(new String[]{}));
+ msg.setFrom(this.financerConfig.getFromAddress());
+ msg.setSubject("[Financer] Recurring transactions reminder");
+ msg.setText(reminderBuilder.toString());
+
+ try {
+ this.mailSender.send(msg);
+ } catch (MailException e) {
+ // TODO log
+ }
+ }
+}
diff --git a/src/main/resources/config/application.properties b/src/main/resources/config/application.properties
index 9f05903..c4701ea 100644
--- a/src/main/resources/config/application.properties
+++ b/src/main/resources/config/application.properties
@@ -24,4 +24,15 @@ financer.countryCode=DE
financer.state=sl
# The date format of the client-supplied date string, used to parse the string into a proper object
-financer.dateFormat=dd.MM.yyyy
\ No newline at end of file
+financer.dateFormat=dd.MM.yyyy
+
+# A collection of email addresses that should receive mails from financer
+financer.mailRecipients[0]=marius@kleberonline.de
+
+# The from address used in emails send by financer
+financer.fromAddress=financer@77zzcx7.de
+
+# Mail configuration
+spring.mail.host=localhost
+#spring.mail.username=
+#spring.mail.password=
diff --git a/src/main/resources/database/hsqldb/V1_0_0__init.sql b/src/main/resources/database/hsqldb/V1_0_0__init.sql
index 88b281a..a8cca05 100644
--- a/src/main/resources/database/hsqldb/V1_0_0__init.sql
+++ b/src/main/resources/database/hsqldb/V1_0_0__init.sql
@@ -1,5 +1,5 @@
--
--- This file contains the basic initialization of the financer schema
+-- This file contains the basic initialization of the financer schema and basic init data
--
-- Account table
@@ -42,4 +42,20 @@ CREATE TABLE "transaction" ( --escape keyword "transaction"
CONSTRAINT fk_transaction_from_account FOREIGN KEY (from_account_id) REFERENCES account (id),
CONSTRAINT fk_transaction_to_account FOREIGN KEY (to_account_id) REFERENCES account (id),
CONSTRAINT fk_transaction_recurring_transaction FOREIGN KEY (recurring_transaction_id) REFERENCES recurring_transaction (id)
-);
\ No newline at end of file
+);
+
+-- Accounts
+INSERT INTO account (id, "key", type, status, current_balance)
+VALUES (1, 'accounts.checkaccount', 'BANK', 'OPEN', 0); -- insert first with ID 1 so we get predictable numbering
+
+INSERT INTO account ("key", type, status, current_balance)
+VALUES ('accounts.income', 'INCOME', 'OPEN', 0);
+
+INSERT INTO account ("key", type, status, current_balance)
+VALUES ('accounts.cash', 'CASH', 'OPEN', 0);
+
+INSERT INTO account ("key", type, status, current_balance)
+VALUES ('accounts.start', 'START', 'OPEN', 0);
+
+INSERT INTO account ("key", type, status, current_balance)
+VALUES ('accounts.rent', 'EXPENSE', 'OPEN', 0);
\ No newline at end of file
diff --git a/src/test/java/de/financer/task/SendRecurringTransactionReminderTaskTest.java b/src/test/java/de/financer/task/SendRecurringTransactionReminderTaskTest.java
new file mode 100644
index 0000000..7af9de9
--- /dev/null
+++ b/src/test/java/de/financer/task/SendRecurringTransactionReminderTaskTest.java
@@ -0,0 +1,72 @@
+package de.financer.task;
+
+import de.financer.config.FinancerConfig;
+import de.financer.model.Account;
+import de.financer.model.RecurringTransaction;
+import de.financer.service.RecurringTransactionService;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.springframework.mail.SimpleMailMessage;
+import org.springframework.mail.javamail.JavaMailSender;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+
+@RunWith(MockitoJUnitRunner.class)
+public class SendRecurringTransactionReminderTaskTest {
+ @InjectMocks
+ private SendRecurringTransactionReminderTask classUnderTest;
+
+ @Mock
+ private RecurringTransactionService recurringTransactionService;
+
+ @Mock
+ private JavaMailSender mailSender;
+
+ @Mock
+ private FinancerConfig financerConfig;
+
+ @Test
+ public void test_sendReminder() {
+ // Arrange
+ final Collection recurringTransactions = Arrays.asList(
+ createRecurringTransaction("Test booking 1", "accounts.income", "accounts.bank", Long.valueOf(250000)),
+ createRecurringTransaction("Test booking 2", "accounts.bank", "accounts.rent", Long.valueOf(41500)),
+ createRecurringTransaction("Test booking 3", "accounts.bank", "accounts.cash", Long.valueOf(5000))
+ );
+
+ Mockito.when(this.recurringTransactionService.getAllDueToday()).thenReturn(recurringTransactions);
+ Mockito.when(this.financerConfig.getMailRecipients()).thenReturn(Collections.singletonList("test@test.com"));
+
+ // Act
+ this.classUnderTest.sendReminder();
+
+ // Assert
+ Mockito.verify(this.mailSender, Mockito.times(1)).send(Mockito.any(SimpleMailMessage.class));
+ }
+
+ private RecurringTransaction createRecurringTransaction(String description, String fromAccountKey, String toAccountKey, Long amount) {
+ final RecurringTransaction recurringTransaction = new RecurringTransaction();
+
+ recurringTransaction.setDescription(description);
+ recurringTransaction.setFromAccount(createAccount(fromAccountKey));
+ recurringTransaction.setToAccount(createAccount(toAccountKey));
+ recurringTransaction.setAmount(amount);
+
+ return recurringTransaction;
+ }
+
+ private Account createAccount(String key) {
+ final Account account = new Account();
+
+ account.setKey(key);
+
+ return account;
+ }
+}
diff --git a/src/test/resources/database/hsqldb/integration/V999_99_00__testdata.sql b/src/test/resources/database/hsqldb/integration/V999_99_00__testdata.sql
index 2000b85..18c37f1 100644
--- a/src/test/resources/database/hsqldb/integration/V999_99_00__testdata.sql
+++ b/src/test/resources/database/hsqldb/integration/V999_99_00__testdata.sql
@@ -1,22 +1,7 @@
-- Accounts
-INSERT INTO account (id, "key", type, status, current_balance)
-VALUES (1, 'accounts.checkaccount', 'BANK', 'OPEN', 0); -- insert first with ID 1 so we get predictable numbering
-
-INSERT INTO account ("key", type, status, current_balance)
-VALUES ('accounts.income', 'INCOME', 'OPEN', 0);
-
-INSERT INTO account ("key", type, status, current_balance)
-VALUES ('accounts.cash', 'CASH', 'OPEN', 0);
-
-INSERT INTO account ("key", type, status, current_balance)
-VALUES ('accounts.start', 'START', 'OPEN', 0);
-
INSERT INTO account ("key", type, status, current_balance)
VALUES ('accounts.convenience', 'EXPENSE', 'OPEN', 0);
-INSERT INTO account ("key", type, status, current_balance)
-VALUES ('accounts.rent', 'EXPENSE', 'OPEN', 0);
-
--Recurring transactions
INSERT INTO recurring_transaction (from_account_id, to_account_id, description, amount, interval_type, first_occurrence, holiday_weekend_type)
VALUES (2, 1, 'Pay', 250000, 'MONTHLY', '2019-01-15', 'NEXT_WORKDAY');