Introduce expenseNeutral flag
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "transaction"
|
||||
ADD COLUMN expense_neutral BOOLEAN DEFAULT FALSE NOT NULL;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "transaction"
|
||||
ADD COLUMN expense_neutral BOOLEAN DEFAULT 'FALSE' NOT NULL;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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}"/>
|
||||
|
||||
Reference in New Issue
Block a user