Introduce periods (and other smaller stuff)

This commit is contained in:
2019-09-28 21:47:07 +02:00
parent f6cdec638e
commit e052a5a98a
58 changed files with 953 additions and 246 deletions

View File

@@ -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;

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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));

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -1,2 +1,3 @@
# Hibernate
spring.jpa.show-sql=true
spring.jpa.show-sql=true
#logging.level.org.hibernate.type=trace

View File

@@ -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);

View File

@@ -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());

View File

@@ -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)
)

View File

@@ -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());

View File

@@ -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)
)

View File

@@ -0,0 +1 @@
The VARCHAR type on PostgreSQL is already multibyte capable so nothing to do here

View File

@@ -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.

View File

@@ -24,6 +24,12 @@ public class TransactionService_createTransactionTest {
@Mock
private RuleService ruleService;
@Mock
private PeriodService periodService;
@Mock
private AccountStatisticService accountStatisticService;
@Mock
private TransactionRepository transactionRepository;

View File

@@ -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) {