Skip to content

Commit

Permalink
#460 - new page to show still active recurring transactions
Browse files Browse the repository at this point in the history
  • Loading branch information
deadlocker8 committed Jan 7, 2023
1 parent a11e227 commit 4ae5fb1
Show file tree
Hide file tree
Showing 9 changed files with 304 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

import com.google.gson.annotations.Expose;
import de.deadlocker8.budgetmaster.repeating.endoption.RepeatingEnd;
import de.deadlocker8.budgetmaster.repeating.endoption.RepeatingEndAfterXTimes;
import de.deadlocker8.budgetmaster.repeating.endoption.RepeatingEndDate;
import de.deadlocker8.budgetmaster.repeating.endoption.RepeatingEndNever;
import de.deadlocker8.budgetmaster.repeating.modifier.RepeatingModifier;
import de.deadlocker8.budgetmaster.transactions.Transaction;
import org.springframework.format.annotation.DateTimeFormat;
Expand Down Expand Up @@ -132,6 +135,35 @@ public List<LocalDate> getRepeatingDates(LocalDate dateFetchLimit)
return dates;
}

/***
* Returns whether this repeating option has ended before the given date.
*/
public boolean hasEndedBefore(LocalDate date)
{
if(endOption instanceof RepeatingEndNever)
{
return false;
}

if(endOption instanceof RepeatingEndDate)
{
final LocalDate endDate = (LocalDate) endOption.getValue();
return endDate.isBefore(date);
}

if(endOption instanceof RepeatingEndAfterXTimes)
{
// Use a date fetch limit far into future to really calculate all dates. The date calculation will finish
// as soon as the number of repetitions is reached and therefore never calculate dates until the year 3000.
final List<LocalDate> repeatingDates = getRepeatingDates(LocalDate.of(3000, 1, 1));
final LocalDate lastDate = repeatingDates.get(repeatingDates.size() - 1);

return lastDate.isBefore(date);
}

throw new UnsupportedOperationException("Unknown repeating end option type");
}

@Override
public String toString()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

@Service
Expand Down Expand Up @@ -53,4 +54,24 @@ private boolean containsDate(List<Transaction> transactions, LocalDate date)

return false;
}

/**
* Returns all repeating transactions that have not ended before the given date.
*/
public List<Transaction> getActiveRepeatingTransactionsAfter(LocalDate date)
{
final List<RepeatingOption> repeatingOptions = repeatingOptionRepository.findAllByOrderByStartDateAsc();
final List<RepeatingOption> activeRepeatingOptions = repeatingOptions.stream()
.filter(repeatingOption -> !repeatingOption.hasEndedBefore(date))
.toList();

final List<Transaction> activeTransactions = new ArrayList<>();
for(RepeatingOption repeatingOption : activeRepeatingOptions)
{
final List<Transaction> transactions = transactionService.getRepository().findAllByRepeatingOption(repeatingOption);
activeTransactions.add(transactions.get(0));
}

return activeTransactions;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ private static class ReturnValues
public static final String REDIRECT_NEW_TRANSACTION = "redirect:/transactions/newTransaction/normal";
public static final String NEW_TRANSACTION = "transactions/newTransactionNormal";
public static final String CHANGE_TYPE = "transactions/changeTypeModal";
public static final String RECURRING_OVERVIEW = "transactions/recurringOverview";
}

private static final String CONTINUE = "continue";
Expand Down Expand Up @@ -468,4 +469,12 @@ public String editFutureRepetitions(Model model, @PathVariable("ID") Integer ID,
}
return ReturnValues.NEW_TRANSACTION;
}

@GetMapping("/recurringOverview")
public String recurringOverview(Model model)
{
final List<Transaction> activeRepeatingTransactions = repeatingTransactionUpdater.getActiveRepeatingTransactionsAfter(LocalDate.now());
model.addAttribute(TransactionModelAttributes.ALL_ENTITIES, activeRepeatingTransactions);
return ReturnValues.RECURRING_OVERVIEW;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,8 @@ transaction.new.label.account=Konto
transaction.new.label.transfer.account=Zielkonto
transaction.new.label.repeating=Wiederholung
transaction.new.label.repeating.all=Alle
transactions.recurring.headline=Aktive wiederholende Buchungen
transactions.recurring.placeholder=Keine aktiven wiederholenden Buchungen

repeating.button.add=Wiederholung hinzufügen
repeating.button.remove=Wiederholung entfernen
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,8 @@ transaction.new.label.account=Account
transaction.new.label.transfer.account=Destination Account
transaction.new.label.repeating=Repeating
transaction.new.label.repeating.all=Every
transactions.recurring.headline=Active Recurring Transactions
transactions.recurring.placeholder=No active recurring transactions

repeating.button.add=Add repetition
repeating.button.remove=Remove repetition
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@
<#if activeID == "transactions" || activeID == "templates" || activeID == "recurring">
<li class="sub-menu <#if activeID == "transactions">active</#if>"><a href="<@s.url '${link}'/>" class="waves-effect no-padding"><div class="stripe ${activeColor}"></div><i class="material-icons">${icon}</i>${text}</a></li>
<li class="sub-menu sub-menu-entry <#if activeID == "templates">active</#if>"><a href="<@s.url '/templates'/>" class="waves-effect no-padding"><div class="stripe ${activeColor}"></div><i class="material-icons">${entityType.TEMPLATE.getIcon()}</i>${locale.getString("menu.transactions.templates")}</a></li>
<li class="sub-menu sub-menu-entry <#if activeID == "recurring">active</#if>"><a href="<@s.url '/templates'/>" class="waves-effect no-padding"><div class="stripe ${activeColor}"></div><i class="material-icons">${entityType.RECURRING_TRANSACTIONS.getIcon()}</i>${locale.getString("menu.transactions.recurring")}</a></li>
<li class="sub-menu sub-menu-entry <#if activeID == "recurring">active</#if>"><a href="<@s.url '/transactions/recurringOverview'/>" class="waves-effect no-padding"><div class="stripe ${activeColor}"></div><i class="material-icons">${entityType.RECURRING_TRANSACTIONS.getIcon()}</i>${locale.getString("menu.transactions.recurring")}</a></li>
<#else>
<li><a href="<@s.url '${link}'/>" class="waves-effect"><i class="material-icons">${icon}</i>${text}</a></li>
</#if>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<html>
<head>
<#import "../helpers/header.ftl" as header>
<@header.globals/>
<@header.header "BudgetMaster - ${locale.getString('transactions.recurring.headline')}"/>
<@header.style "transactions"/>
<@header.style "search"/>
<#import "/spring.ftl" as s>
</head>
<@header.body>
<#import "../helpers/navbar.ftl" as navbar>
<@navbar.navbar "recurring" settings/>

<#import "../search/searchMacros.ftl" as searchMacros>

<main>
<div class="card main-card background-color">
<div class="container">
<div class="section center-align">
<div class="headline">${locale.getString("transactions.recurring.headline")}</div>
</div>
</div>

<@header.content>
<#if transactions?has_content>
<@searchMacros.renderTransactions transactions=transactions openLinksInNewTab=false/>
<#else>
<#-- show placeholde text if there are no active recurring transactions -->
<br>
<div class="row">
<div class="col s12">
<div class="headline-small center-align">${locale.getString("transactions.recurring.placeholder")}</div>
</div>
</div>
</#if>
</@header.content>
</div>
</main>

<!-- Scripts-->
<#import "../helpers/scripts.ftl" as scripts>
<@scripts.scripts/>
</@header.body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -200,4 +200,73 @@ void test_GetRepeatingDates_EveryYear_EndNever()
assertThat(repeatingOption.getRepeatingDates(dateFetchLimit))
.isEqualTo(expected);
}

// test hasEndedBefore()

@Test
void test_HasEndedBefore_EndNever()
{
LocalDate startDate = LocalDate.of(2018, 4, 22);
RepeatingOption repeatingOption = new RepeatingOption(startDate,
new RepeatingModifierDays(3),
new RepeatingEndNever());

LocalDate date = LocalDate.of(2018, 5, 2);

assertThat(repeatingOption.hasEndedBefore(date)).isFalse();
}

@Test
void test_HasEndedBefore_EndDate_NotEnded()
{
LocalDate startDate = LocalDate.of(2018, 4, 30);
LocalDate endDate = LocalDate.of(2019, 9, 28);
RepeatingOption repeatingOption = new RepeatingOption(startDate,
new RepeatingModifierYears(1),
new RepeatingEndDate(endDate));

LocalDate date = LocalDate.of(2018, 5, 2);

assertThat(repeatingOption.hasEndedBefore(date)).isFalse();
}

@Test
void test_HasEndedBefore_EndDate_HasEnded()
{
LocalDate startDate = LocalDate.of(2018, 4, 30);
LocalDate endDate = LocalDate.of(2019, 9, 28);
RepeatingOption repeatingOption = new RepeatingOption(startDate,
new RepeatingModifierYears(1),
new RepeatingEndDate(endDate));

LocalDate date = LocalDate.of(2019, 9, 29);

assertThat(repeatingOption.hasEndedBefore(date)).isTrue();
}

@Test
void test_HasEndedBefore_EndAfterXTimes_NotEnded()
{
LocalDate startDate = LocalDate.of(2018, 4, 30);
RepeatingOption repeatingOption = new RepeatingOption(startDate,
new RepeatingModifierYears(1),
new RepeatingEndAfterXTimes(2));

LocalDate date = LocalDate.of(2020, 4, 29);

assertThat(repeatingOption.hasEndedBefore(date)).isFalse();
}

@Test
void test_HasEndedBefore_EndAfterXTimes_HasEnded()
{
LocalDate startDate = LocalDate.of(2018, 4, 30);
RepeatingOption repeatingOption = new RepeatingOption(startDate,
new RepeatingModifierYears(1),
new RepeatingEndAfterXTimes(2));

LocalDate date = LocalDate.of(2022, 9, 29);

assertThat(repeatingOption.hasEndedBefore(date)).isTrue();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package de.deadlocker8.budgetmaster.unit.repeating;

import de.deadlocker8.budgetmaster.accounts.Account;
import de.deadlocker8.budgetmaster.accounts.AccountType;
import de.deadlocker8.budgetmaster.repeating.RepeatingOption;
import de.deadlocker8.budgetmaster.repeating.RepeatingOptionRepository;
import de.deadlocker8.budgetmaster.repeating.RepeatingTransactionUpdater;
import de.deadlocker8.budgetmaster.repeating.endoption.RepeatingEndAfterXTimes;
import de.deadlocker8.budgetmaster.repeating.endoption.RepeatingEndDate;
import de.deadlocker8.budgetmaster.repeating.endoption.RepeatingEndNever;
import de.deadlocker8.budgetmaster.repeating.modifier.RepeatingModifierDays;
import de.deadlocker8.budgetmaster.repeating.modifier.RepeatingModifierMonths;
import de.deadlocker8.budgetmaster.repeating.modifier.RepeatingModifierYears;
import de.deadlocker8.budgetmaster.transactions.Transaction;
import de.deadlocker8.budgetmaster.transactions.TransactionRepository;
import de.deadlocker8.budgetmaster.transactions.TransactionService;
import de.deadlocker8.budgetmaster.unit.helpers.LocalizedTest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.time.LocalDate;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;


@ExtendWith(SpringExtension.class)
@LocalizedTest
class RepeatingTransactionUpdaterTest
{
@Mock
private TransactionService transactionService;

@Mock
private TransactionRepository transactionRepository;

@Mock
private RepeatingOptionRepository repeatingOptionRepository;

private Transaction TRANSACTION_1;
private Transaction TRANSACTION_2;
private Transaction TRANSACTION_3;
private Transaction TRANSACTION_4;

private RepeatingTransactionUpdater repeatingTransactionUpdater;

@BeforeEach
void beforeEach()
{
final RepeatingOption REPEATING_OPTION_END_NEVER = new RepeatingOption(LocalDate.of(2022, 12, 1),
new RepeatingModifierDays(3),
new RepeatingEndNever());

final RepeatingOption REPEATING_OPTION_END_DATE = new RepeatingOption(LocalDate.of(2022, 10, 24),
new RepeatingModifierMonths(1),
new RepeatingEndDate(LocalDate.of(2023, 2, 15)));

final RepeatingOption REPEATING_OPTION_END_AFTER_X_TIMES = new RepeatingOption(LocalDate.of(2018, 4, 30),
new RepeatingModifierYears(1),
new RepeatingEndAfterXTimes(2));

final Account account = new Account("Account", AccountType.CUSTOM);

TRANSACTION_1 = new Transaction();
TRANSACTION_1.setName("abc");
TRANSACTION_1.setAmount(700);
TRANSACTION_1.setAccount(account);
TRANSACTION_1.setIsExpenditure(true);
TRANSACTION_1.setRepeatingOption(REPEATING_OPTION_END_NEVER);
Mockito.when(transactionRepository.findAllByRepeatingOption(REPEATING_OPTION_END_NEVER)).thenReturn(List.of(TRANSACTION_1));

TRANSACTION_2 = new Transaction();
TRANSACTION_2.setName("Lorem");
TRANSACTION_2.setAmount(200);
TRANSACTION_2.setAccount(account);
TRANSACTION_2.setIsExpenditure(true);
TRANSACTION_2.setRepeatingOption(REPEATING_OPTION_END_DATE);

TRANSACTION_3 = new Transaction();
TRANSACTION_3.setName("Ipsum");
TRANSACTION_3.setAmount(75);
TRANSACTION_3.setAccount(account);
TRANSACTION_3.setIsExpenditure(true);
TRANSACTION_3.setRepeatingOption(REPEATING_OPTION_END_AFTER_X_TIMES);
Mockito.when(transactionRepository.findAllByRepeatingOption(REPEATING_OPTION_END_DATE)).thenReturn(List.of(TRANSACTION_2, TRANSACTION_3));

TRANSACTION_4 = new Transaction();
TRANSACTION_4.setName("dolor");
TRANSACTION_4.setAmount(50);
TRANSACTION_4.setAccount(account);
TRANSACTION_4.setIsExpenditure(true);
TRANSACTION_4.setRepeatingOption(REPEATING_OPTION_END_AFTER_X_TIMES);
Mockito.when(transactionRepository.findAllByRepeatingOption(REPEATING_OPTION_END_AFTER_X_TIMES)).thenReturn(List.of(TRANSACTION_4));

Mockito.when(transactionService.getRepository()).thenReturn(transactionRepository);
Mockito.when(repeatingOptionRepository.findAllByOrderByStartDateAsc()).thenReturn(List.of(REPEATING_OPTION_END_NEVER, REPEATING_OPTION_END_DATE, REPEATING_OPTION_END_AFTER_X_TIMES));
repeatingTransactionUpdater = new RepeatingTransactionUpdater(transactionService, repeatingOptionRepository);
}

@Test
void test_getActiveRepeatingTransactionsAfter()
{
assertThat(repeatingTransactionUpdater.getActiveRepeatingTransactionsAfter(LocalDate.of(2023, 1, 20)))
.containsExactly(TRANSACTION_1, TRANSACTION_2);
}

@Test
void test_getActiveRepeatingTransactionsAfter_2()
{
assertThat(repeatingTransactionUpdater.getActiveRepeatingTransactionsAfter(LocalDate.of(2017, 1, 20)))
.containsExactly(TRANSACTION_1, TRANSACTION_2, TRANSACTION_4);
}

@Test
void test_getActiveRepeatingTransactionsAfter_3()
{
assertThat(repeatingTransactionUpdater.getActiveRepeatingTransactionsAfter(LocalDate.of(2023, 10, 1)))
.containsExactly(TRANSACTION_1);
}
}

0 comments on commit 4ae5fb1

Please sign in to comment.