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"))
|
inverseJoinColumns = @JoinColumn(name = "file_id"))
|
||||||
//@formatter:on
|
//@formatter:on
|
||||||
private Set<File> files;
|
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() {
|
public Long getId() {
|
||||||
return id;
|
return id;
|
||||||
@@ -111,4 +115,12 @@ public class Transaction {
|
|||||||
public void setFiles(Set<File> files) {
|
public void setFiles(Set<File> files) {
|
||||||
this.files = 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")
|
@Query("SELECT t FROM Transaction t WHERE t.toAccount = :toAccount OR t.fromAccount = :fromAccount")
|
||||||
Iterable<Transaction> findTransactionsByFromAccountOrToAccount(Account fromAccount, Account toAccount);
|
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);
|
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);
|
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
|
// 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
|
// 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
|
// 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);
|
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.description}"/>
|
||||||
<th th:text="#{financer.transaction-list.table-header.byRecurring}"/>
|
<th th:text="#{financer.transaction-list.table-header.byRecurring}"/>
|
||||||
<th th:text="#{financer.transaction-list.table-header.taxRelevant}"/>
|
<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>
|
||||||
<tr th:each="transaction : ${transactions}">
|
<tr th:each="transaction : ${transactions}">
|
||||||
<td class="hideable-column" th:text="${transaction.id}"/>
|
<td class="hideable-column" th:text="${transaction.id}"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user