diff --git a/financer-common/src/main/java/de/financer/dto/SearchTransactionsRequestDto.java b/financer-common/src/main/java/de/financer/dto/SearchTransactionsRequestDto.java deleted file mode 100644 index e387e95..0000000 --- a/financer-common/src/main/java/de/financer/dto/SearchTransactionsRequestDto.java +++ /dev/null @@ -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; - } -} diff --git a/financer-common/src/main/java/de/financer/model/Transaction.java b/financer-common/src/main/java/de/financer/model/Transaction.java index c57e2b4..c44942a 100644 --- a/financer-common/src/main/java/de/financer/model/Transaction.java +++ b/financer-common/src/main/java/de/financer/model/Transaction.java @@ -35,6 +35,10 @@ public class Transaction { inverseJoinColumns = @JoinColumn(name = "file_id")) //@formatter:on private Set 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 files) { this.files = files; } + + public boolean isExpenseNeutral() { + return expenseNeutral; + } + + public void setExpenseNeutral(boolean expenseNeutral) { + this.expenseNeutral = expenseNeutral; + } } diff --git a/financer-server/src/main/java/de/financer/dba/TransactionRepository.java b/financer-server/src/main/java/de/financer/dba/TransactionRepository.java index 16a85a5..a9be9cc 100644 --- a/financer-server/src/main/java/de/financer/dba/TransactionRepository.java +++ b/financer-server/src/main/java/de/financer/dba/TransactionRepository.java @@ -14,15 +14,15 @@ public interface TransactionRepository extends CrudRepository @Query("SELECT t FROM Transaction t WHERE t.toAccount = :toAccount OR t.fromAccount = :fromAccount") Iterable 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 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 getAccountExpenseTotals(Iterable periods, AccountType incomeType, AccountType startType, AccountType... expenseTypes); } \ No newline at end of file diff --git a/financer-server/src/main/resources/database/hsqldb/V29_0_0__expenseNeutral.sql b/financer-server/src/main/resources/database/hsqldb/V29_0_0__expenseNeutral.sql new file mode 100644 index 0000000..38ad730 --- /dev/null +++ b/financer-server/src/main/resources/database/hsqldb/V29_0_0__expenseNeutral.sql @@ -0,0 +1,2 @@ +ALTER TABLE "transaction" + ADD COLUMN expense_neutral BOOLEAN DEFAULT FALSE NOT NULL; \ No newline at end of file diff --git a/financer-server/src/main/resources/database/postgres/V29_0_0__expenseNeutral.sql b/financer-server/src/main/resources/database/postgres/V29_0_0__expenseNeutral.sql new file mode 100644 index 0000000..a268832 --- /dev/null +++ b/financer-server/src/main/resources/database/postgres/V29_0_0__expenseNeutral.sql @@ -0,0 +1,2 @@ +ALTER TABLE "transaction" + ADD COLUMN expense_neutral BOOLEAN DEFAULT 'FALSE' NOT NULL; \ No newline at end of file diff --git a/financer-server/src/test/java/de/financer/service/TransactionService_createTransactionTest.java b/financer-server/src/test/java/de/financer/service/TransactionService_createTransactionTest.java deleted file mode 100644 index 5f72bd8..0000000 --- a/financer-server/src/test/java/de/financer/service/TransactionService_createTransactionTest.java +++ /dev/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; - } -} diff --git a/financer-web-client/src/main/resources/templates/transaction/transactionList.html b/financer-web-client/src/main/resources/templates/transaction/transactionList.html index e80ddc6..916e303 100644 --- a/financer-web-client/src/main/resources/templates/transaction/transactionList.html +++ b/financer-web-client/src/main/resources/templates/transaction/transactionList.html @@ -9,7 +9,7 @@ - +