Introduce expenseNeutral flag

This commit is contained in:
2020-04-10 16:37:06 +02:00
parent aa7168ac3c
commit 3d4fb73424
7 changed files with 20 additions and 252 deletions

View File

@@ -1,74 +0,0 @@
package de.financer.dto;
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
public class SearchTransactionsRequestDto {
private String fromAccountKey;
private String toAccountKey;
private String periodId;
private String limit;
private String order;
private String taxRelevant;
private String hasFile;
public String getFromAccountKey() {
return fromAccountKey;
}
public void setFromAccountKey(String fromAccountKey) {
this.fromAccountKey = fromAccountKey;
}
public String getToAccountKey() {
return toAccountKey;
}
public void setToAccountKey(String toAccountKey) {
this.toAccountKey = toAccountKey;
}
public String getPeriodId() {
return periodId;
}
public void setPeriodId(String periodId) {
this.periodId = periodId;
}
public String getLimit() {
return limit;
}
public void setLimit(String limit) {
this.limit = limit;
}
public String getOrder() {
return order;
}
public void setOrder(String order) {
this.order = order;
}
public String getTaxRelevant() {
return taxRelevant;
}
public void setTaxRelevant(String taxRelevant) {
this.taxRelevant = taxRelevant;
}
@Override
public String toString() {
return ReflectionToStringBuilder.toString(this);
}
public String getHasFile() {
return hasFile;
}
public void setHasFile(String hasFile) {
this.hasFile = hasFile;
}
}

View File

@@ -35,6 +35,10 @@ public class Transaction {
inverseJoinColumns = @JoinColumn(name = "file_id"))
//@formatter:on
private Set<File> files;
// Property is used to exclude transaction from the 'expenses current period',
// the inline expense history chart and the expense/income/liability chart
// No UI to set the flag as its use is only for very special cases
private boolean expenseNeutral;
public Long getId() {
return id;
@@ -111,4 +115,12 @@ public class Transaction {
public void setFiles(Set<File> files) {
this.files = files;
}
public boolean isExpenseNeutral() {
return expenseNeutral;
}
public void setExpenseNeutral(boolean expenseNeutral) {
this.expenseNeutral = expenseNeutral;
}
}

View File

@@ -14,15 +14,15 @@ public interface TransactionRepository extends CrudRepository<Transaction, Long>
@Query("SELECT t FROM Transaction t WHERE t.toAccount = :toAccount OR t.fromAccount = :fromAccount")
Iterable<Transaction> findTransactionsByFromAccountOrToAccount(Account fromAccount, Account toAccount);
@Query("SELECT SUM(t.amount) FROM Transaction t JOIN t.periods p JOIN t.toAccount a WHERE a.type IN :expenseTypes AND p = :period")
@Query("SELECT SUM(t.amount) FROM Transaction t JOIN t.periods p JOIN t.toAccount a WHERE a.type IN :expenseTypes AND p = :period AND t.expenseNeutral = false")
Long getExpensesForPeriod(Period period, AccountType... expenseTypes);
@Query("SELECT SUM(t.amount) FROM Transaction t JOIN t.periods p JOIN t.toAccount a JOIN t.fromAccount f WHERE a.type IN :expenseTypes AND f.type != :startType AND p.type = :type GROUP BY p ORDER BY p.start ASC")
@Query("SELECT SUM(t.amount) FROM Transaction t JOIN t.periods p JOIN t.toAccount a JOIN t.fromAccount f WHERE a.type IN :expenseTypes AND f.type != :startType AND p.type = :type AND t.expenseNeutral = false GROUP BY p ORDER BY p.start ASC")
List<Long> getExpensesForAllPeriods(PeriodType type, AccountType startType, AccountType... expenseTypes);
// The HQL contains a hack because Hibernate can't resolve the alias of the CASE column in the GROUP BY clause
// That's why the generated alias is used directly in the HQL. It will break if the columns in the SELECT clause get reordered
// col_2_0_ instead of AccType
@Query("SELECT new de.financer.dto.ExpensePeriodTotal(p, SUM(t.amount), CASE WHEN fa.type = :incomeType THEN fa.type ELSE ta.type END AS AccType) FROM Transaction t JOIN t.toAccount ta JOIN t.fromAccount fa JOIN t.periods p WHERE ((ta.type IN :expenseTypes AND fa.type <> :startType) OR (fa.type = :incomeType)) AND p IN :periods GROUP BY p, col_2_0_")
@Query("SELECT new de.financer.dto.ExpensePeriodTotal(p, SUM(t.amount), CASE WHEN fa.type = :incomeType THEN fa.type ELSE ta.type END AS AccType) FROM Transaction t JOIN t.toAccount ta JOIN t.fromAccount fa JOIN t.periods p WHERE ((ta.type IN :expenseTypes AND fa.type <> :startType) OR (fa.type = :incomeType)) AND p IN :periods AND t.expenseNeutral = false GROUP BY p, col_2_0_")
List<ExpensePeriodTotal> getAccountExpenseTotals(Iterable<Period> periods, AccountType incomeType, AccountType startType, AccountType... expenseTypes);
}

View File

@@ -0,0 +1,2 @@
ALTER TABLE "transaction"
ADD COLUMN expense_neutral BOOLEAN DEFAULT FALSE NOT NULL;

View File

@@ -0,0 +1,2 @@
ALTER TABLE "transaction"
ADD COLUMN expense_neutral BOOLEAN DEFAULT 'FALSE' NOT NULL;

View File

@@ -1,174 +0,0 @@
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;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class TransactionService_createTransactionTest {
@InjectMocks
private TransactionService classUnderTest;
@Mock
private AccountService accountService;
@Mock
private RuleService ruleService;
@Mock
private PeriodService periodService;
@Mock
private AccountStatisticService accountStatisticService;
@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
// 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.createTransaction("account.invalid", "account.invalid", 150L, "24.02.2019", "XXX", false, null, null);
// Assert
Assert.assertEquals(ResponseReason.FROM_AND_TO_ACCOUNT_NOT_FOUND, response);
}
@Test
public void test_createTransaction_TO_ACCOUNT_NOT_FOUND() {
// Arrange
Mockito.when(this.accountService.getAccountByKey(Mockito.anyString())).thenReturn(createAccount(), null);
// Act
final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.invalid", 150L, "24.02.2019", "XXX", false, null, null);
// Assert
Assert.assertEquals(ResponseReason.TO_ACCOUNT_NOT_FOUND, response);
}
@Test
public void test_createTransaction_FROM_ACCOUNT_NOT_FOUND() {
// Arrange
Mockito.when(this.accountService.getAccountByKey(Mockito.anyString())).thenReturn(null, createAccount());
// Act
final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.to", 150L, "24.02.2019", "XXX", false, null, null);
// Assert
Assert.assertEquals(ResponseReason.FROM_ACCOUNT_NOT_FOUND, response);
}
@Test
public void test_createTransaction_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.createTransaction("account.from", "account.to", 150L, "24.02.2019", "XXX", false, null, null);
// Assert
Assert.assertEquals(ResponseReason.INVALID_BOOKING_ACCOUNTS, response);
}
@Test
public void test_createTransaction_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.createTransaction("account.from", "account.to", null, "24.02.2019", "XXX", false, null, null);
// Assert
Assert.assertEquals(ResponseReason.MISSING_AMOUNT, response);
}
@Test
public void test_createTransaction_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.createTransaction("account.from", "account.to", 0L, "24.02.2019", "XXX", false, null, null);
// Assert
Assert.assertEquals(ResponseReason.AMOUNT_ZERO, response);
}
@Test
public void test_createTransaction_MISSING_DATE() {
// 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.createTransaction("account.from", "account.to", 125L, null, "XXX", false, null, null);
// Assert
Assert.assertEquals(ResponseReason.MISSING_DATE, response);
}
@Test
public void test_createTransaction_INVALID_DATE_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.createTransaction("account.from", "account.to", 125L, "2019-01-01", "XXX", false, null, null);
// Assert
Assert.assertEquals(ResponseReason.INVALID_DATE_FORMAT, response);
}
@Test
public void test_createTransaction_OK() {
// Arrange
final Account fromAccount = Mockito.mock(Account.class);
final Account toAccount = Mockito.mock(Account.class);
Mockito.when(this.accountService.getAccountByKey(Mockito.anyString())).thenReturn(fromAccount, toAccount);
Mockito.when(this.ruleService.isValidBooking(Mockito.any(), Mockito.any())).thenReturn(Boolean.TRUE);
Mockito.when(this.ruleService.getMultiplierFromAccount(Mockito.any())).thenReturn(-1L);
Mockito.when(this.ruleService.getMultiplierToAccount(Mockito.any())).thenReturn(1L);
Mockito.when(fromAccount.getCurrentBalance()).thenReturn(0L);
Mockito.when(toAccount.getCurrentBalance()).thenReturn(0L);
// Act
final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", 125L, "24.02.2019", "XXX", false, null, null);
// Assert
Assert.assertEquals(ResponseReason.CREATED, response);
Mockito.verify(fromAccount, Mockito.times(1)).setCurrentBalance((long) -125);
Mockito.verify(toAccount, Mockito.times(1)).setCurrentBalance(125L);
}
private Account createAccount() {
final Account account = new Account();
account.setCurrentBalance(0L);
return account;
}
}

View File

@@ -9,7 +9,7 @@
<th th:text="#{financer.transaction-list.table-header.description}"/>
<th th:text="#{financer.transaction-list.table-header.byRecurring}"/>
<th th:text="#{financer.transaction-list.table-header.taxRelevant}"/>
<th th:if="${showActions}" th:text="#{financer.transaction-list.table-header.actions}"/>
<th th:text="#{financer.transaction-list.table-header.actions}"/>
</tr>
<tr th:each="transaction : ${transactions}">
<td class="hideable-column" th:text="${transaction.id}"/>