Introduce periods (and other smaller stuff)
This commit is contained in:
@@ -6,32 +6,32 @@ import org.springframework.http.ResponseEntity;
|
||||
public enum ResponseReason {
|
||||
OK(HttpStatus.OK),
|
||||
UNKNOWN_ERROR(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
INVALID_ACCOUNT_TYPE(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
FROM_ACCOUNT_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
TO_ACCOUNT_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
FROM_AND_TO_ACCOUNT_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
INVALID_DATE_FORMAT(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
MISSING_DATE(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
AMOUNT_ZERO(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
MISSING_AMOUNT(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
INVALID_BOOKING_ACCOUNTS(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
MISSING_HOLIDAY_WEEKEND_TYPE(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
INVALID_HOLIDAY_WEEKEND_TYPE(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
MISSING_INTERVAL_TYPE(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
INVALID_INTERVAL_TYPE(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
MISSING_FIRST_OCCURRENCE(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
INVALID_FIRST_OCCURRENCE_FORMAT(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
INVALID_LAST_OCCURRENCE_FORMAT(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
MISSING_RECURRING_TRANSACTION_ID(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
INVALID_RECURRING_TRANSACTION_ID(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
RECURRING_TRANSACTION_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
MISSING_TRANSACTION_ID(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
INVALID_TRANSACTION_ID(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
TRANSACTION_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
ACCOUNT_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
DUPLICATE_ACCOUNT_KEY(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
DUPLICATE_ACCOUNT_GROUP_NAME(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
ACCOUNT_GROUP_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
INVALID_ACCOUNT_TYPE(HttpStatus.BAD_REQUEST),
|
||||
FROM_ACCOUNT_NOT_FOUND(HttpStatus.BAD_REQUEST),
|
||||
TO_ACCOUNT_NOT_FOUND(HttpStatus.BAD_REQUEST),
|
||||
FROM_AND_TO_ACCOUNT_NOT_FOUND(HttpStatus.BAD_REQUEST),
|
||||
INVALID_DATE_FORMAT(HttpStatus.BAD_REQUEST),
|
||||
MISSING_DATE(HttpStatus.BAD_REQUEST),
|
||||
AMOUNT_ZERO(HttpStatus.BAD_REQUEST),
|
||||
MISSING_AMOUNT(HttpStatus.BAD_REQUEST),
|
||||
INVALID_BOOKING_ACCOUNTS(HttpStatus.BAD_REQUEST),
|
||||
MISSING_HOLIDAY_WEEKEND_TYPE(HttpStatus.BAD_REQUEST),
|
||||
INVALID_HOLIDAY_WEEKEND_TYPE(HttpStatus.BAD_REQUEST),
|
||||
MISSING_INTERVAL_TYPE(HttpStatus.BAD_REQUEST),
|
||||
INVALID_INTERVAL_TYPE(HttpStatus.BAD_REQUEST),
|
||||
MISSING_FIRST_OCCURRENCE(HttpStatus.BAD_REQUEST),
|
||||
INVALID_FIRST_OCCURRENCE_FORMAT(HttpStatus.BAD_REQUEST),
|
||||
INVALID_LAST_OCCURRENCE_FORMAT(HttpStatus.BAD_REQUEST),
|
||||
MISSING_RECURRING_TRANSACTION_ID(HttpStatus.BAD_REQUEST),
|
||||
INVALID_RECURRING_TRANSACTION_ID(HttpStatus.BAD_REQUEST),
|
||||
RECURRING_TRANSACTION_NOT_FOUND(HttpStatus.BAD_REQUEST),
|
||||
MISSING_TRANSACTION_ID(HttpStatus.BAD_REQUEST),
|
||||
INVALID_TRANSACTION_ID(HttpStatus.BAD_REQUEST),
|
||||
TRANSACTION_NOT_FOUND(HttpStatus.BAD_REQUEST),
|
||||
ACCOUNT_NOT_FOUND(HttpStatus.BAD_REQUEST),
|
||||
DUPLICATE_ACCOUNT_KEY(HttpStatus.BAD_REQUEST),
|
||||
DUPLICATE_ACCOUNT_GROUP_NAME(HttpStatus.BAD_REQUEST),
|
||||
ACCOUNT_GROUP_NOT_FOUND(HttpStatus.BAD_REQUEST);
|
||||
|
||||
private HttpStatus httpStatus;
|
||||
|
||||
|
||||
@@ -111,4 +111,13 @@ public class AccountController {
|
||||
|
||||
return this.accountService.getAccountExpenses(periodStart, periodEnd);
|
||||
}
|
||||
|
||||
@RequestMapping("getAccountExpensesCurrentExpensePeriod")
|
||||
public Iterable<AccountExpense> getAccountExpensesCurrentExpensePeriod() {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("/accounts/getAccountExpensesCurrentExpensePeriod called");
|
||||
}
|
||||
|
||||
return this.accountService.getAccountExpensesCurrentExpensePeriod();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,4 +60,13 @@ public class AccountGroupController {
|
||||
|
||||
return this.accountGroupService.getAccountGroupExpenses(periodStart, periodEnd);
|
||||
}
|
||||
|
||||
@RequestMapping("getAccountGroupExpensesCurrentExpensePeriod")
|
||||
public Iterable<AccountGroupExpense> getAccountGroupExpensesCurrentExpensePeriod() {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("/accountGroups/getAccountGroupExpenses called");
|
||||
}
|
||||
|
||||
return this.accountGroupService.getAccountGroupExpensesCurrentExpensePeriod();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package de.financer.controller;
|
||||
|
||||
import de.financer.model.Period;
|
||||
import de.financer.service.PeriodService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("periods")
|
||||
public class PeriodController {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(PeriodController.class);
|
||||
|
||||
@Autowired
|
||||
private PeriodService periodService;
|
||||
|
||||
@RequestMapping("closeCurrentExpensePeriod")
|
||||
public ResponseEntity closeCurrentExpensePeriod() {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("/periods/closeCurrentExpensePeriod called");
|
||||
}
|
||||
|
||||
final ResponseEntity responseEntity = this.periodService.closeCurrentExpensePeriod().toResponseEntity();
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String.format("/periods/closeCurrentExpensePeriod returns with %s", responseEntity));
|
||||
}
|
||||
|
||||
return responseEntity;
|
||||
}
|
||||
|
||||
@RequestMapping("getCurrentExpensePeriod")
|
||||
public Period getCurrentExpensePeriod() {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("/periods/getCurrentExpensePeriod called");
|
||||
}
|
||||
|
||||
final Period currentExpensePeriod = this.periodService.getCurrentExpensePeriod();
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String.format("/periods/getCurrentExpensePeriod returns with %s", currentExpensePeriod));
|
||||
}
|
||||
|
||||
return currentExpensePeriod;
|
||||
}
|
||||
}
|
||||
@@ -79,12 +79,12 @@ public class TransactionController {
|
||||
}
|
||||
|
||||
@RequestMapping("getExpensesCurrentPeriod")
|
||||
public Long getExpensesCurrentPeriod(Integer monthPeriodStartDay) {
|
||||
public Long getExpensesCurrentPeriod() {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String.format("/transactions/getExpensesCurrentPeriod got parameters: %s", monthPeriodStartDay));
|
||||
LOGGER.debug(String.format("/transactions/getExpensesCurrentPeriod called"));
|
||||
}
|
||||
|
||||
final Long response = this.transactionService.getExpensesCurrentPeriod(monthPeriodStartDay);
|
||||
final Long response = this.transactionService.getExpensesCurrentPeriod();
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String.format("/transactions/getExpensesCurrentPeriod returns with %s", response));
|
||||
@@ -94,13 +94,13 @@ public class TransactionController {
|
||||
}
|
||||
|
||||
@RequestMapping("getExpensePeriodTotals")
|
||||
public Iterable<ExpensePeriodTotal> getExpensePeriodTotals(Integer monthPeriodStartDay, Integer year) {
|
||||
public Iterable<ExpensePeriodTotal> getExpensePeriodTotals(Integer year) {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String.format("/transactions/getExpensePeriodTotals got parameters: %s, %s", monthPeriodStartDay, year));
|
||||
LOGGER.debug(String.format("/transactions/getExpensePeriodTotals got parameters: %s", year));
|
||||
}
|
||||
|
||||
final Iterable<ExpensePeriodTotal> expensePeriodTotals = this.transactionService
|
||||
.getExpensePeriodTotals(monthPeriodStartDay, year);
|
||||
.getExpensePeriodTotals(year);
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String.format("/transactions/getExpensePeriodTotals returns with %s", expensePeriodTotals));
|
||||
|
||||
@@ -3,6 +3,7 @@ package de.financer.dba;
|
||||
import de.financer.dto.AccountGroupExpense;
|
||||
import de.financer.model.AccountGroup;
|
||||
import de.financer.model.AccountType;
|
||||
import de.financer.model.Period;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
@@ -16,4 +17,7 @@ public interface AccountGroupRepository extends CrudRepository<AccountGroup, Lon
|
||||
|
||||
@Query("SELECT new de.financer.dto.AccountGroupExpense(ag, SUM(t.amount)) FROM Transaction t JOIN t.toAccount a JOIN a.accountGroup ag WHERE a.type in :expenseTypes AND t.date BETWEEN :startDate AND :startEnd GROUP BY ag")
|
||||
Iterable<AccountGroupExpense> getAccountGroupExpenses(LocalDate startDate, LocalDate startEnd, AccountType... expenseTypes);
|
||||
|
||||
@Query("SELECT new de.financer.dto.AccountGroupExpense(ag, SUM(t.amount)) FROM Transaction t JOIN t.toAccount a JOIN a.accountGroup ag JOIN t.periods p WHERE a.type in :expenseTypes AND p = :period GROUP BY ag")
|
||||
Iterable<AccountGroupExpense> getAccountGroupExpensesCurrentExpensePeriod(Period period, AccountType... expenseTypes);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package de.financer.dba;
|
||||
import de.financer.dto.AccountExpense;
|
||||
import de.financer.model.Account;
|
||||
import de.financer.model.AccountType;
|
||||
import de.financer.model.Period;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
@@ -19,4 +20,7 @@ public interface AccountRepository extends CrudRepository<Account, Long> {
|
||||
|
||||
@Query("SELECT new de.financer.dto.AccountExpense(a, SUM(t.amount)) FROM Transaction t JOIN t.toAccount a WHERE a.type in :expenseTypes AND t.date BETWEEN :startDate AND :startEnd GROUP BY a")
|
||||
Iterable<AccountExpense> getAccountExpenses(LocalDate startDate, LocalDate startEnd, AccountType... expenseTypes);
|
||||
|
||||
@Query("SELECT new de.financer.dto.AccountExpense(a, SUM(t.amount)) FROM Transaction t JOIN t.toAccount a JOIN t.periods p WHERE a.type in :expenseTypes AND p = :period GROUP BY a")
|
||||
Iterable<AccountExpense> getAccountExpenses(Period period, AccountType... expenseTypes);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package de.financer.dba;
|
||||
|
||||
import de.financer.model.Account;
|
||||
import de.financer.model.AccountStatistic;
|
||||
import de.financer.model.Period;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Transactional(propagation = Propagation.REQUIRED)
|
||||
public interface AccountStatisticRepository extends CrudRepository<AccountStatistic, Long> {
|
||||
@Query("SELECT accStat FROM AccountStatistic accStat WHERE accStat.account = :account AND accStat.period = :period")
|
||||
AccountStatistic findForAccountAndPeriod(Account account, Period period);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package de.financer.dba;
|
||||
|
||||
import de.financer.model.Period;
|
||||
import de.financer.model.PeriodType;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Transactional(propagation = Propagation.REQUIRED)
|
||||
public interface PeriodRepository extends CrudRepository<Period, Long> {
|
||||
@Query("SELECT p FROM Period p WHERE p.type = :type AND p.end IS NULL")
|
||||
Period findCurrentExpensePeriod(PeriodType type);
|
||||
|
||||
@Query("SELECT p FROM Period p WHERE p.type = :type AND YEAR(p.start) = :year")
|
||||
Iterable<Period> getAllPeriodsForYear(PeriodType type, Integer year);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package de.financer.dba;
|
||||
import de.financer.dto.ExpensePeriodTotal;
|
||||
import de.financer.model.Account;
|
||||
import de.financer.model.AccountType;
|
||||
import de.financer.model.Period;
|
||||
import de.financer.model.Transaction;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
@@ -16,13 +17,12 @@ import java.util.List;
|
||||
public interface TransactionRepository extends CrudRepository<Transaction, Long> {
|
||||
Iterable<Transaction> findTransactionsByFromAccountOrToAccount(Account fromAccount, Account toAccount);
|
||||
|
||||
@Query("SELECT SUM(t.amount) FROM Transaction t JOIN t.toAccount a WHERE t.date BETWEEN :periodStart AND :periodEnd AND a.type IN :expenseTypes")
|
||||
Long getExpensesCurrentPeriod(LocalDate periodStart, LocalDate periodEnd, AccountType... expenseTypes);
|
||||
@Query("SELECT SUM(t.amount) FROM Transaction t JOIN t.periods p JOIN t.toAccount a WHERE a.type IN :expenseTypes AND p = :period")
|
||||
Long getExpensesForPeriod(Period period, 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_0_0_ instead of periodShortCode
|
||||
// col_2_0_ instead of AccType
|
||||
@Query("SELECT new de.financer.dto.ExpensePeriodTotal(CASE WHEN EXTRACT(DAY FROM t.date) >= :startDay THEN CONCAT(CAST(EXTRACT(YEAR FROM t.date) AS string), '/', CAST(EXTRACT(MONTH FROM t.date) AS string)) ELSE CASE WHEN EXTRACT(MONTH FROM t.date) = 1 THEN CONCAT(CAST(EXTRACT(YEAR FROM t.date) - 1 AS string), '/12') ELSE CONCAT(CAST(EXTRACT(YEAR FROM t.date) AS string), '/', CAST(EXTRACT(MONTH FROM t.date) - 1 AS string)) END END AS periodShortCode, 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 WHERE ((ta.type IN :expenseTypes AND fa.type <> :startType) OR (fa.type = :incomeType)) AND t.date BETWEEN :lowerBound AND :upperBound GROUP BY col_0_0_, col_2_0_ ORDER BY periodShortCode, AccType ASC")
|
||||
List<ExpensePeriodTotal> getAccountExpenseTotals(int startDay, LocalDate lowerBound, LocalDate upperBound, AccountType incomeType, AccountType startType, AccountType... expenseTypes);
|
||||
@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_")
|
||||
List<ExpensePeriodTotal> getAccountExpenseTotals(Iterable<Period> periods, AccountType incomeType, AccountType startType, AccountType... expenseTypes);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import de.financer.dba.AccountGroupRepository;
|
||||
import de.financer.dto.AccountGroupExpense;
|
||||
import de.financer.model.AccountGroup;
|
||||
import de.financer.model.AccountType;
|
||||
import de.financer.model.Period;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -26,6 +27,9 @@ public class AccountGroupService {
|
||||
@Autowired
|
||||
private AccountGroupRepository accountGroupRepository;
|
||||
|
||||
@Autowired
|
||||
private PeriodService periodService;
|
||||
|
||||
@Autowired
|
||||
private FinancerConfig financerConfig;
|
||||
|
||||
@@ -94,4 +98,10 @@ public class AccountGroupService {
|
||||
|
||||
return this.accountGroupRepository.getAccountGroupExpenses(startDate, endDate, AccountType.LIABILITY, AccountType.EXPENSE);
|
||||
}
|
||||
|
||||
public Iterable<AccountGroupExpense> getAccountGroupExpensesCurrentExpensePeriod() {
|
||||
final Period period = this.periodService.getCurrentExpensePeriod();
|
||||
|
||||
return this.accountGroupRepository.getAccountGroupExpensesCurrentExpensePeriod(period, AccountType.LIABILITY, AccountType.EXPENSE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,7 @@ import de.financer.ResponseReason;
|
||||
import de.financer.config.FinancerConfig;
|
||||
import de.financer.dba.AccountRepository;
|
||||
import de.financer.dto.AccountExpense;
|
||||
import de.financer.model.Account;
|
||||
import de.financer.model.AccountGroup;
|
||||
import de.financer.model.AccountStatus;
|
||||
import de.financer.model.AccountType;
|
||||
import de.financer.model.*;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -32,6 +29,9 @@ public class AccountService {
|
||||
@Autowired
|
||||
private AccountGroupService accountGroupService;
|
||||
|
||||
@Autowired
|
||||
private PeriodService periodService;
|
||||
|
||||
@Autowired
|
||||
private FinancerConfig financerConfig;
|
||||
|
||||
@@ -161,6 +161,13 @@ public class AccountService {
|
||||
return this.accountRepository.getCurrentAssets(AccountType.BANK, AccountType.CASH);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method calculates the expenses per account in the given arbitrary period.
|
||||
*
|
||||
* @param periodStart the start of the arbitrary period
|
||||
* @param periodEnd the end of the arbitrary period
|
||||
* @return a mapping of {@link Account}<->its expenses in the given period
|
||||
*/
|
||||
public Iterable<AccountExpense> getAccountExpenses(String periodStart, String periodEnd) {
|
||||
LocalDate startDate;
|
||||
LocalDate endDate;
|
||||
@@ -178,4 +185,15 @@ public class AccountService {
|
||||
|
||||
return this.accountRepository.getAccountExpenses(startDate, endDate, AccountType.LIABILITY, AccountType.EXPENSE);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method calculates the expenses per account in the current expense period.
|
||||
*
|
||||
* @return a mapping of {@link Account}<->its expenses in the current expense period
|
||||
*/
|
||||
public Iterable<AccountExpense> getAccountExpensesCurrentExpensePeriod() {
|
||||
final Period period = this.periodService.getCurrentExpensePeriod();
|
||||
|
||||
return this.accountRepository.getAccountExpenses(period, AccountType.LIABILITY, AccountType.EXPENSE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package de.financer.service;
|
||||
|
||||
import de.financer.dba.AccountStatisticRepository;
|
||||
import de.financer.model.Account;
|
||||
import de.financer.model.AccountStatistic;
|
||||
import de.financer.model.Period;
|
||||
import de.financer.model.Transaction;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class AccountStatisticService {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(PeriodService.class);
|
||||
|
||||
@Autowired
|
||||
private AccountStatisticRepository accountStatisticRepository;
|
||||
|
||||
@Transactional(propagation = Propagation.REQUIRED)
|
||||
public void calculateStatistics(Transaction transaction) {
|
||||
final Account fromAccount = transaction.getFromAccount();
|
||||
final Account toAccount = transaction.getToAccount();
|
||||
final long amount = transaction.getAmount();
|
||||
final List<AccountStatistic> resultList = new ArrayList<>();
|
||||
|
||||
for (final Period period : transaction.getPeriods()) {
|
||||
resultList.add(calculateInternal(fromAccount, period, amount, true, 1));
|
||||
resultList.add(calculateInternal(toAccount, period, amount, false, 1));
|
||||
}
|
||||
|
||||
this.accountStatisticRepository.saveAll(resultList);
|
||||
}
|
||||
|
||||
@Transactional(propagation = Propagation.REQUIRED)
|
||||
public void revertStatistics(Transaction transaction) {
|
||||
final Account fromAccount = transaction.getFromAccount();
|
||||
final Account toAccount = transaction.getToAccount();
|
||||
final long amount = transaction.getAmount();
|
||||
final List<AccountStatistic> resultList = new ArrayList<>();
|
||||
|
||||
for (final Period period : transaction.getPeriods()) {
|
||||
resultList.add(calculateInternal(fromAccount, period, amount, true, -1));
|
||||
resultList.add(calculateInternal(toAccount, period, amount, false, -1));
|
||||
}
|
||||
|
||||
this.accountStatisticRepository.saveAll(resultList);
|
||||
}
|
||||
|
||||
private AccountStatistic calculateInternal(Account account, Period period, long amount, boolean from, int multiplier) {
|
||||
AccountStatistic accountStatistic = this.accountStatisticRepository
|
||||
.findForAccountAndPeriod(account, period);
|
||||
|
||||
if (accountStatistic == null) {
|
||||
accountStatistic = new AccountStatistic();
|
||||
|
||||
accountStatistic.setAccount(account);
|
||||
accountStatistic.setPeriod(period);
|
||||
}
|
||||
|
||||
if (from) {
|
||||
accountStatistic.setSpendingTotalFrom(accountStatistic.getSpendingTotalFrom() + amount * multiplier);
|
||||
accountStatistic.setTransactionCountFrom(accountStatistic.getTransactionCountFrom() + multiplier);
|
||||
}
|
||||
else {
|
||||
accountStatistic.setSpendingTotalTo(accountStatistic.getSpendingTotalTo() + amount * multiplier);
|
||||
accountStatistic.setTransactionCountTo(accountStatistic.getTransactionCountTo() + multiplier);
|
||||
}
|
||||
|
||||
return accountStatistic;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package de.financer.service;
|
||||
|
||||
import de.financer.ResponseReason;
|
||||
import de.financer.dba.PeriodRepository;
|
||||
import de.financer.model.Period;
|
||||
import de.financer.model.PeriodType;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Service
|
||||
public class PeriodService {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(PeriodService.class);
|
||||
|
||||
@Autowired
|
||||
private PeriodRepository periodRepository;
|
||||
|
||||
/**
|
||||
* @return the currently open expense period
|
||||
*/
|
||||
public Period getCurrentExpensePeriod() {
|
||||
return this.periodRepository.findCurrentExpensePeriod(PeriodType.EXPENSE);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method closes the current expense period and opens a new one.
|
||||
*
|
||||
* @return {@link ResponseReason#OK} if the operation succeeded, {@link ResponseReason#OK}
|
||||
*/
|
||||
@Transactional(propagation = Propagation.REQUIRED)
|
||||
public ResponseReason closeCurrentExpensePeriod() {
|
||||
final Period currentPeriod = this.getCurrentExpensePeriod();
|
||||
final Period nextPeriod = new Period();
|
||||
final LocalDateTime now = LocalDateTime.now();
|
||||
ResponseReason response;
|
||||
|
||||
currentPeriod.setEnd(now);
|
||||
|
||||
nextPeriod.setStart(now.plusSeconds(1));
|
||||
nextPeriod.setType(PeriodType.EXPENSE);
|
||||
|
||||
try {
|
||||
this.periodRepository.save(currentPeriod);
|
||||
this.periodRepository.save(nextPeriod);
|
||||
|
||||
response = ResponseReason.OK;
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Could not close current expense period!", e);
|
||||
|
||||
response = ResponseReason.UNKNOWN_ERROR;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public Iterable<Period> getAllExpensePeriodsForYear(Integer year) {
|
||||
return this.periodRepository.getAllPeriodsForYear(PeriodType.EXPENSE, year);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,10 @@
|
||||
package de.financer.service;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import de.financer.ResponseReason;
|
||||
import de.financer.config.FinancerConfig;
|
||||
import de.financer.dba.TransactionRepository;
|
||||
import de.financer.dto.ExpensePeriodTotal;
|
||||
import de.financer.model.Account;
|
||||
import de.financer.model.AccountType;
|
||||
import de.financer.model.RecurringTransaction;
|
||||
import de.financer.model.Transaction;
|
||||
import de.financer.util.ExpensePeriod;
|
||||
import de.financer.model.*;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.math.NumberUtils;
|
||||
import org.slf4j.Logger;
|
||||
@@ -23,7 +18,6 @@ import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@@ -36,6 +30,12 @@ public class TransactionService {
|
||||
@Autowired
|
||||
private RuleService ruleService;
|
||||
|
||||
@Autowired
|
||||
private PeriodService periodService;
|
||||
|
||||
@Autowired
|
||||
private AccountStatisticService accountStatisticService;
|
||||
|
||||
@Autowired
|
||||
private TransactionRepository transactionRepository;
|
||||
|
||||
@@ -91,6 +91,8 @@ public class TransactionService {
|
||||
try {
|
||||
final Transaction transaction = buildTransaction(fromAccount, toAccount, amount, description, date, recurringTransaction);
|
||||
|
||||
transaction.setPeriods(Collections.singleton(this.periodService.getCurrentExpensePeriod()));
|
||||
|
||||
fromAccount.setCurrentBalance(fromAccount.getCurrentBalance() + (this.ruleService
|
||||
.getMultiplierFromAccount(fromAccount) * amount));
|
||||
|
||||
@@ -106,6 +108,8 @@ public class TransactionService {
|
||||
|
||||
this.transactionRepository.save(transaction);
|
||||
|
||||
this.accountStatisticService.calculateStatistics(transaction);
|
||||
|
||||
this.accountService.saveAccount(fromAccount);
|
||||
this.accountService.saveAccount(toAccount);
|
||||
|
||||
@@ -215,7 +219,9 @@ public class TransactionService {
|
||||
.getMultiplierToAccount(toAccount) * amount * -1));
|
||||
|
||||
try {
|
||||
this.transactionRepository.deleteById(Long.valueOf(transactionId));
|
||||
this.transactionRepository.deleteById(transaction.getId());
|
||||
|
||||
this.accountStatisticService.revertStatistics(transaction);
|
||||
|
||||
this.accountService.saveAccount(fromAccount);
|
||||
this.accountService.saveAccount(toAccount);
|
||||
@@ -228,31 +234,19 @@ public class TransactionService {
|
||||
return response;
|
||||
}
|
||||
|
||||
public Long getExpensesCurrentPeriod(Integer monthPeriodStartDay) {
|
||||
if (monthPeriodStartDay == null) {
|
||||
return Long.valueOf(-1l);
|
||||
}
|
||||
|
||||
final ExpensePeriod expensePeriod = ExpensePeriod
|
||||
.getCurrentExpensePeriod(monthPeriodStartDay);
|
||||
public Long getExpensesCurrentPeriod() {
|
||||
final Period expensePeriod = this.periodService.getCurrentExpensePeriod();
|
||||
|
||||
final Long expensesCurrentPeriod = this.transactionRepository
|
||||
.getExpensesCurrentPeriod(expensePeriod.getStart(), expensePeriod
|
||||
.getEnd(), AccountType.EXPENSE, AccountType.LIABILITY);
|
||||
.getExpensesForPeriod(expensePeriod, AccountType.EXPENSE, AccountType.LIABILITY);
|
||||
|
||||
return Optional.ofNullable(expensesCurrentPeriod).orElse(Long.valueOf(0l));
|
||||
}
|
||||
|
||||
public Iterable<ExpensePeriodTotal> getExpensePeriodTotals(Integer monthPeriodStartDay, Integer year) {
|
||||
final List<ExpensePeriod> expensePeriods = ExpensePeriod.generateExpensePeriodsForYear(monthPeriodStartDay, year);
|
||||
public Iterable<ExpensePeriodTotal> getExpensePeriodTotals(Integer year) {
|
||||
final Iterable<Period> periods = this.periodService.getAllExpensePeriodsForYear(year);
|
||||
|
||||
final ExpensePeriod lowerBound = Iterables.get(expensePeriods, 0);
|
||||
final ExpensePeriod upperBound = Iterables.getLast(expensePeriods);
|
||||
|
||||
final List<ExpensePeriodTotal> expensePeriodTotals = this.transactionRepository
|
||||
.getAccountExpenseTotals(monthPeriodStartDay, lowerBound.getStart(), upperBound
|
||||
.getEnd(), AccountType.INCOME, AccountType.START, AccountType.EXPENSE, AccountType.LIABILITY);
|
||||
|
||||
return expensePeriodTotals;
|
||||
return this.transactionRepository
|
||||
.getAccountExpenseTotals(periods, AccountType.INCOME, AccountType.START, AccountType.EXPENSE, AccountType.LIABILITY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
# Hibernate
|
||||
spring.jpa.show-sql=true
|
||||
spring.jpa.show-sql=true
|
||||
#logging.level.org.hibernate.type=trace
|
||||
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE account ALTER COLUMN "key" NVARCHAR(1000);
|
||||
ALTER TABLE recurring_transaction ALTER COLUMN description NVARCHAR(1000);
|
||||
ALTER TABLE "transaction" ALTER COLUMN description NVARCHAR(1000);
|
||||
ALTER TABLE account_group ALTER COLUMN name NVARCHAR(1000);
|
||||
@@ -0,0 +1,19 @@
|
||||
-- Account group table
|
||||
CREATE TABLE period (
|
||||
id BIGINT NOT NULL PRIMARY KEY IDENTITY,
|
||||
type VARCHAR(255) NOT NULL,
|
||||
start DATETIME NOT NULL,
|
||||
"end" DATETIME
|
||||
);
|
||||
|
||||
CREATE TABLE link_transaction_period (
|
||||
id BIGINT NOT NULL PRIMARY KEY IDENTITY,
|
||||
transaction_id BIGINT NOT NULL,
|
||||
period_id BIGINT NOT NULL,
|
||||
|
||||
CONSTRAINT fk_link_transaction_period_transaction FOREIGN KEY (transaction_id) REFERENCES "transaction" (id),
|
||||
CONSTRAINT fk_link_transaction_period_period FOREIGN KEY (period_id) REFERENCES period (id)
|
||||
);
|
||||
|
||||
INSERT INTO period (type, start)
|
||||
VALUES ('EXPENSE', NOW());
|
||||
@@ -0,0 +1,13 @@
|
||||
-- Account statistic table
|
||||
CREATE TABLE account_statistic (
|
||||
id BIGINT NOT NULL PRIMARY KEY IDENTITY,
|
||||
account_id BIGINT NOT NULL,
|
||||
period_id BIGINT NOT NULL,
|
||||
spending_total_from BIGINT NOT NULL,
|
||||
transaction_count_from BIGINT NOT NULL,
|
||||
spending_total_to BIGINT NOT NULL,
|
||||
transaction_count_to BIGINT NOT NULL,
|
||||
|
||||
CONSTRAINT fk_account_statistic_account FOREIGN KEY (account_id) REFERENCES account (id),
|
||||
CONSTRAINT fk_account_statistic_period FOREIGN KEY (period_id) REFERENCES period (id)
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
-- Account group table
|
||||
CREATE TABLE period (
|
||||
id BIGINT NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
type VARCHAR(255) NOT NULL,
|
||||
start DATETIME NOT NULL,
|
||||
"end" DATETIME
|
||||
);
|
||||
|
||||
CREATE TABLE link_transaction_period (
|
||||
id BIGINT NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
transaction_id BIGINT NOT NULL,
|
||||
period_id BIGINT NOT NULL,
|
||||
|
||||
CONSTRAINT fk_link_transaction_period_transaction FOREIGN KEY (transaction_id) REFERENCES "transaction" (id),
|
||||
CONSTRAINT fk_link_transaction_period_period FOREIGN KEY (period_id) REFERENCES period (id)
|
||||
);
|
||||
|
||||
INSERT INTO period (type, start)
|
||||
VALUES ('EXPENSE', NOW());
|
||||
@@ -0,0 +1,13 @@
|
||||
-- Account statistic table
|
||||
CREATE TABLE account_statistic (
|
||||
id BIGINT NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
account_id BIGINT NOT NULL,
|
||||
period_id BIGINT NOT NULL,
|
||||
spending_total_from BIGINT NOT NULL,
|
||||
transaction_count_from BIGINT NOT NULL,
|
||||
spending_total_to BIGINT NOT NULL,
|
||||
transaction_count_to BIGINT NOT NULL,
|
||||
|
||||
CONSTRAINT fk_account_statistic_account FOREIGN KEY (account_id) REFERENCES account (id),
|
||||
CONSTRAINT fk_account_statistic_period FOREIGN KEY (period_id) REFERENCES period (id)
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
The VARCHAR type on PostgreSQL is already multibyte capable so nothing to do here
|
||||
@@ -18,7 +18,7 @@ The recurring transaction table is defined like this (at least for postgres):
|
||||
|
||||
Note the
|
||||
deleted BOOLEAN DEFAULT 'TRUE' NOT NULL,
|
||||
column definition. Not sure why the default is TRUE here is it doesn't make sense.
|
||||
column definition. Not sure why the default is TRUE here as it doesn't make sense.
|
||||
It was probably a mistake, however fixing it here _WILL_ break existing installations
|
||||
as Flyway uses a checksum for scripts. So there is no easy fix, except for effectively
|
||||
overwriting this default in Java code when creating a new recurring transaction.
|
||||
|
||||
@@ -24,6 +24,12 @@ public class TransactionService_createTransactionTest {
|
||||
@Mock
|
||||
private RuleService ruleService;
|
||||
|
||||
@Mock
|
||||
private PeriodService periodService;
|
||||
|
||||
@Mock
|
||||
private AccountStatisticService accountStatisticService;
|
||||
|
||||
@Mock
|
||||
private TransactionRepository transactionRepository;
|
||||
|
||||
|
||||
@@ -27,6 +27,9 @@ public class TransactionService_deleteTransactionTest {
|
||||
@Mock
|
||||
private RuleService ruleService;
|
||||
|
||||
@Mock
|
||||
private AccountStatisticService accountStatisticService;
|
||||
|
||||
@Mock
|
||||
private TransactionRepository transactionRepository;
|
||||
|
||||
@@ -82,7 +85,7 @@ public class TransactionService_deleteTransactionTest {
|
||||
// Arrange
|
||||
Mockito.when(this.transactionRepository.findById(Mockito.anyLong()))
|
||||
.thenReturn(Optional.of(createTransaction(AccountType.BANK, AccountType.EXPENSE)));
|
||||
Mockito.doThrow(new NullPointerException()).when(this.transactionRepository).deleteById(Mockito.anyLong());
|
||||
Mockito.doThrow(new NullPointerException()).when(this.transactionRepository).deleteById(Mockito.any());
|
||||
|
||||
// Act
|
||||
final ResponseReason response = this.classUnderTest.deleteTransaction("123");
|
||||
@@ -94,8 +97,10 @@ public class TransactionService_deleteTransactionTest {
|
||||
@Test
|
||||
public void test_deleteRecurringTransaction_OK() {
|
||||
// Arrange
|
||||
final Transaction transaction = createTransaction(AccountType.BANK, AccountType.EXPENSE);
|
||||
|
||||
Mockito.when(this.transactionRepository.findById(Mockito.anyLong()))
|
||||
.thenReturn(Optional.of(createTransaction(AccountType.BANK, AccountType.EXPENSE)));
|
||||
.thenReturn(Optional.of(transaction));
|
||||
|
||||
// Act
|
||||
final ResponseReason response = this.classUnderTest.deleteTransaction("123");
|
||||
@@ -103,10 +108,11 @@ public class TransactionService_deleteTransactionTest {
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.OK, response);
|
||||
|
||||
final InOrder inOrder = Mockito.inOrder(this.accountService);
|
||||
final InOrder inOrder = Mockito.inOrder(this.accountStatisticService, this.accountService);
|
||||
|
||||
inOrder.verify(this.accountService).saveAccount(ArgumentMatchers.argThat((Account arg) -> Long.valueOf(50000L).equals(arg.getCurrentBalance())));
|
||||
inOrder.verify(this.accountService).saveAccount(ArgumentMatchers.argThat((Account arg) -> Long.valueOf(5000L).equals(arg.getCurrentBalance())));
|
||||
inOrder.verify(this.accountStatisticService).revertStatistics(ArgumentMatchers.eq(transaction));
|
||||
inOrder.verify(this.accountService).saveAccount(ArgumentMatchers.argThat((arg) -> Long.valueOf(50000L).equals(arg.getCurrentBalance())));
|
||||
inOrder.verify(this.accountService).saveAccount(ArgumentMatchers.argThat((arg) -> Long.valueOf(5000L).equals(arg.getCurrentBalance())));
|
||||
}
|
||||
|
||||
private Transaction createTransaction(AccountType fromType, AccountType toType) {
|
||||
|
||||
Reference in New Issue
Block a user