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