Changes for v26

This commit is contained in:
2020-01-20 23:29:35 +01:00
parent 8cec43ee91
commit 95c1c0e7d6
27 changed files with 713 additions and 38 deletions

View File

@@ -0,0 +1,177 @@
package database.common;
import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Date;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
public class V26_0_0__createGrandTotalPeriod extends BaseJavaMigration {
private static class TransactionAccountsContainer {
public Long transactionAmount;
public Long fromAccountId;
public Long toAccountId;
public Long id;
public TransactionAccountsContainer(Long transactionAmount, Long fromAccountId, Long toAccountId, Long id) {
this.transactionAmount = transactionAmount;
this.fromAccountId = fromAccountId;
this.toAccountId = toAccountId;
this.id = id;
}
}
private static final String GET_CONTAINERS_SQL = "SELECT t.amount, t.from_account_id, t.to_account_id, t.id FROM \"transaction\" t";
private static final String ACCOUNT_STATISTIC_EXISTS_SQL = "SELECT id FROM account_statistic WHERE account_id = ? AND period_id = ?";
private static final String ACCOUNT_STATISTIC_UPDATE_FROM_SQL = "UPDATE account_statistic SET spending_total_from = spending_total_from + ?, transaction_count_from = transaction_count_from + 1 WHERE id = ?";
private static final String ACCOUNT_STATISTIC_UPDATE_TO_SQL = "UPDATE account_statistic SET spending_total_to = spending_total_to + ?, transaction_count_to = transaction_count_to + 1 WHERE id = ?";
private static final String ACCOUNT_STATISTIC_INSERT_FROM_SQL = "INSERT INTO account_statistic(account_id, period_id, spending_total_from, transaction_count_from, spending_total_to, transaction_count_to) VALUES(?, ?, ?, 1, 0, 0)";
private static final String ACCOUNT_STATISTIC_INSERT_TO_SQL = "INSERT INTO account_statistic(account_id, period_id, spending_total_from, transaction_count_from, spending_total_to, transaction_count_to) VALUES(?, ?, 0, 0, ?, 1)";
private static final String SELECT_DATE_OLDEST_TRANSACTION_SQL = "SELECT \"date\" FROM \"transaction\" ORDER BY \"date\" ASC";
private static final String INSERT_GRAND_TOTAL_PERIOD_SQL = "INSERT INTO period (type, start) VALUES ('GRAND_TOTAL', ?)";
private static final String SELECT_GRAND_TOTAL_PERIOD_SQL = "SELECT id FROM period WHERE type = 'GRAND_TOTAL'";
private static final String INSERT_LINK_TRANSACTION_PERIOD_SQL = "INSERT INTO link_transaction_period (transaction_id, period_id) VALUES (?, ?)";
@Override
public void migrate(Context context) throws Exception {
final long grandTotalPeriodId = createGrandTotalPeriod(context);
final List<TransactionAccountsContainer> cons = getContainers(context);
for (TransactionAccountsContainer con : cons) {
handleAccount(con, context, grandTotalPeriodId, true);
handleAccount(con, context, grandTotalPeriodId, false);
handleTransaction(con, context, grandTotalPeriodId);
}
}
private long createGrandTotalPeriod(Context context) throws Exception {
// Check whether there are transactions from which we can derive the inception date
// If so, use the date of the oldest transaction to create the GRAND_TOTAL period
// If not, use the current date to create the GRAND_TOTAL period
try (PreparedStatement statement =
context
.getConnection()
.prepareStatement(SELECT_DATE_OLDEST_TRANSACTION_SQL)) {
statement.setMaxRows(1);
final ResultSet rs = statement.executeQuery();
final Date inceptionDate;
// The ResultSet contains only one entry as specified with the setMaxRows(1) call above
if (rs.next()) {
inceptionDate = rs.getDate(1);
}
else {
inceptionDate = new Date(System.currentTimeMillis());
}
try (PreparedStatement statementInsert =
context
.getConnection()
.prepareStatement(INSERT_GRAND_TOTAL_PERIOD_SQL)) {
statementInsert.setDate(1, inceptionDate);
statementInsert.execute();
try (PreparedStatement statementSelect =
context
.getConnection()
.prepareStatement(SELECT_GRAND_TOTAL_PERIOD_SQL)) {
final ResultSet rsSelect = statementSelect.executeQuery();
// We know it's there as we just created it
rsSelect.next();
return rsSelect.getLong(1);
}
}
}
}
private List<TransactionAccountsContainer> getContainers(Context context) throws SQLException {
try (PreparedStatement statement =
context
.getConnection()
.prepareStatement(GET_CONTAINERS_SQL)) {
final ResultSet rs = statement.executeQuery();
final List<TransactionAccountsContainer> cons = new ArrayList<>();
while(rs.next()) {
cons.add(new TransactionAccountsContainer(
rs.getLong(1), // transaction amount
rs.getLong(2), // from account ID
rs.getLong(3), // to account ID
rs.getLong(4) // ID
));
}
return cons;
}
}
private void handleAccount(TransactionAccountsContainer con, Context context, long grandTotalPeriodId, boolean from) throws SQLException {
try (PreparedStatement statement =
context
.getConnection()
.prepareStatement(ACCOUNT_STATISTIC_EXISTS_SQL)) {
statement.setLong(1, from ? con.fromAccountId : con.toAccountId);
statement.setLong(2, grandTotalPeriodId);
final ResultSet rs = statement.executeQuery();
boolean found = false;
// Only one possible entry because of the unique constraint
while(rs.next()) {
found = true;
try (PreparedStatement statementUpdate =
context
.getConnection()
.prepareStatement(from ? ACCOUNT_STATISTIC_UPDATE_FROM_SQL : ACCOUNT_STATISTIC_UPDATE_TO_SQL)) {
statementUpdate.setLong(1, con.transactionAmount);
statementUpdate.setLong(2, rs.getLong(1));
statementUpdate.executeUpdate();
}
}
// We need to create a new account_statistic entry for the tuple {account_id;period_id}
if (!found) {
try (PreparedStatement statementInsert =
context
.getConnection()
.prepareStatement(from ? ACCOUNT_STATISTIC_INSERT_FROM_SQL : ACCOUNT_STATISTIC_INSERT_TO_SQL)) {
statementInsert.setLong(1, from ? con.fromAccountId : con.toAccountId);
statementInsert.setLong(2, grandTotalPeriodId);
statementInsert.setLong(3, con.transactionAmount);
statementInsert.execute();
}
}
}
}
private void handleTransaction(TransactionAccountsContainer con, Context context, long grandTotalPeriodId) throws SQLException {
try (PreparedStatement statementInsert =
context
.getConnection()
.prepareStatement(INSERT_LINK_TRANSACTION_PERIOD_SQL)) {
statementInsert.setLong(1, con.id);
statementInsert.setLong(2, grandTotalPeriodId);
statementInsert.execute();
}
}
}

View File

@@ -0,0 +1,200 @@
package database.common;
import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;
import java.sql.*;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
public class V26_0_1__createExpenseYearPeriod_2019 extends BaseJavaMigration {
private static class TransactionAccountsContainer {
public Long transactionAmount;
public Long fromAccountId;
public Long toAccountId;
public Long id;
public TransactionAccountsContainer(Long transactionAmount, Long fromAccountId, Long toAccountId, Long id) {
this.transactionAmount = transactionAmount;
this.fromAccountId = fromAccountId;
this.toAccountId = toAccountId;
this.id = id;
}
}
private static final String GET_CONTAINERS_SQL = "SELECT t.amount, t.from_account_id, t.to_account_id, t.id FROM \"transaction\" t INNER JOIN link_transaction_period ltp ON ltp.transaction_id = t.id INNER JOIN period p ON p.id = ltp.period_id WHERE p.type = 'EXPENSE' AND t.\"date\" >= ? AND t.\"date\" < ?";
private static final String ACCOUNT_STATISTIC_EXISTS_SQL = "SELECT id FROM account_statistic WHERE account_id = ? AND period_id = ?";
private static final String ACCOUNT_STATISTIC_UPDATE_FROM_SQL = "UPDATE account_statistic SET spending_total_from = spending_total_from + ?, transaction_count_from = transaction_count_from + 1 WHERE id = ?";
private static final String ACCOUNT_STATISTIC_UPDATE_TO_SQL = "UPDATE account_statistic SET spending_total_to = spending_total_to + ?, transaction_count_to = transaction_count_to + 1 WHERE id = ?";
private static final String ACCOUNT_STATISTIC_INSERT_FROM_SQL = "INSERT INTO account_statistic(account_id, period_id, spending_total_from, transaction_count_from, spending_total_to, transaction_count_to) VALUES(?, ?, ?, 1, 0, 0)";
private static final String ACCOUNT_STATISTIC_INSERT_TO_SQL = "INSERT INTO account_statistic(account_id, period_id, spending_total_from, transaction_count_from, spending_total_to, transaction_count_to) VALUES(?, ?, 0, 0, ?, 1)";
private static final String SELECT_FIRST_PERIOD_SQL = "SELECT start FROM period WHERE type = 'EXPENSE' AND start BETWEEN '2019-01-01' AND '2019-12-31' ORDER BY start ASC";
private static final String SELECT_LAST_PERIOD_SQL = "SELECT \"end\" FROM period WHERE type = 'EXPENSE' AND start BETWEEN '2019-01-01' AND '2019-12-31' ORDER BY start DESC";
private static final String INSERT_EXPENSE_YEAR_PERIOD_SQL = "INSERT INTO period (type, start, \"end\") VALUES ('EXPENSE_YEAR', ?, ?)";
private static final String SELECT_EXPENSE_YEAR_PERIOD_SQL = "SELECT id FROM period WHERE type = 'EXPENSE_YEAR'";
private static final String INSERT_LINK_TRANSACTION_PERIOD_SQL = "INSERT INTO link_transaction_period (transaction_id, period_id) VALUES (?, ?)";
private Timestamp firstPeriodStart;
private Timestamp lastPeriodEnd;
@Override
public void migrate(Context context) throws Exception {
final long expenseYearPeriodId = createExpenseYearPeriod(context);
if (expenseYearPeriodId == -1) {
return;
}
final List<TransactionAccountsContainer> cons = getContainers(context);
for (TransactionAccountsContainer con : cons) {
handleAccount(con, context, expenseYearPeriodId, true);
handleAccount(con, context, expenseYearPeriodId, false);
handleTransaction(con, context, expenseYearPeriodId);
}
}
private long createExpenseYearPeriod(Context context) throws Exception {
try (PreparedStatement statement =
context
.getConnection()
.prepareStatement(SELECT_FIRST_PERIOD_SQL)) {
statement.setMaxRows(1);
final ResultSet rs = statement.executeQuery();
// The ResultSet contains only one entry as specified with the setMaxRows(1) call above
if (!rs.next()) {
return -1; // it may happen that no period for 2019 exists
}
firstPeriodStart = rs.getTimestamp(1);
try (PreparedStatement statementLast =
context
.getConnection()
.prepareStatement(SELECT_LAST_PERIOD_SQL)) {
statementLast.setMaxRows(1);
final ResultSet rsLast = statementLast.executeQuery();
// The ResultSet contains only one entry as specified with the setMaxRows(1) call above
rsLast.next();
lastPeriodEnd = rsLast.getTimestamp(1);
try (PreparedStatement statementInsert =
context
.getConnection()
.prepareStatement(INSERT_EXPENSE_YEAR_PERIOD_SQL)) {
statementInsert.setTimestamp(1, firstPeriodStart);
statementInsert.setTimestamp(2, lastPeriodEnd);
statementInsert.execute();
try (PreparedStatement statementSelect =
context
.getConnection()
.prepareStatement(SELECT_EXPENSE_YEAR_PERIOD_SQL)) {
final ResultSet rsSelect = statementSelect.executeQuery();
// We know it's there as we just created it
rsSelect.next();
return rsSelect.getLong(1);
}
}
}
}
}
private List<TransactionAccountsContainer> getContainers(Context context) throws SQLException {
try (PreparedStatement statement =
context
.getConnection()
.prepareStatement(GET_CONTAINERS_SQL)) {
statement.setDate(1, new Date(firstPeriodStart.toLocalDateTime().toInstant(ZoneOffset.UTC).truncatedTo(ChronoUnit.DAYS).toEpochMilli()));
statement.setDate(2, new Date(lastPeriodEnd.toLocalDateTime().toInstant(ZoneOffset.UTC).truncatedTo(ChronoUnit.DAYS).toEpochMilli()));
final ResultSet rs = statement.executeQuery();
final List<TransactionAccountsContainer> cons = new ArrayList<>();
while(rs.next()) {
cons.add(new TransactionAccountsContainer(
rs.getLong(1), // transaction amount
rs.getLong(2), // from account ID
rs.getLong(3), // to account ID
rs.getLong(4) // ID
));
}
return cons;
}
}
private void handleAccount(TransactionAccountsContainer con, Context context, long expenseYearPeriodId, boolean from) throws SQLException {
try (PreparedStatement statement =
context
.getConnection()
.prepareStatement(ACCOUNT_STATISTIC_EXISTS_SQL)) {
statement.setLong(1, from ? con.fromAccountId : con.toAccountId);
statement.setLong(2, expenseYearPeriodId);
final ResultSet rs = statement.executeQuery();
boolean found = false;
// Only one possible entry because of the unique constraint
while(rs.next()) {
found = true;
try (PreparedStatement statementUpdate =
context
.getConnection()
.prepareStatement(from ? ACCOUNT_STATISTIC_UPDATE_FROM_SQL : ACCOUNT_STATISTIC_UPDATE_TO_SQL)) {
statementUpdate.setLong(1, con.transactionAmount);
statementUpdate.setLong(2, rs.getLong(1));
statementUpdate.executeUpdate();
}
}
// We need to create a new account_statistic entry for the tuple {account_id;period_id}
if (!found) {
try (PreparedStatement statementInsert =
context
.getConnection()
.prepareStatement(from ? ACCOUNT_STATISTIC_INSERT_FROM_SQL : ACCOUNT_STATISTIC_INSERT_TO_SQL)) {
statementInsert.setLong(1, from ? con.fromAccountId : con.toAccountId);
statementInsert.setLong(2, expenseYearPeriodId);
statementInsert.setLong(3, con.transactionAmount);
statementInsert.execute();
}
}
}
}
private void handleTransaction(TransactionAccountsContainer con, Context context, long expenseYearPeriodId) throws SQLException {
try (PreparedStatement statementInsert =
context
.getConnection()
.prepareStatement(INSERT_LINK_TRANSACTION_PERIOD_SQL)) {
statementInsert.setLong(1, con.id);
statementInsert.setLong(2, expenseYearPeriodId);
statementInsert.execute();
}
}
}

View File

@@ -0,0 +1,183 @@
package database.common;
import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;
import java.sql.*;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalField;
import java.util.ArrayList;
import java.util.List;
public class V26_0_2__createExpenseYearPeriod_2020 extends BaseJavaMigration {
private static class TransactionAccountsContainer {
public Long transactionAmount;
public Long fromAccountId;
public Long toAccountId;
public Long id;
public TransactionAccountsContainer(Long transactionAmount, Long fromAccountId, Long toAccountId, Long id) {
this.transactionAmount = transactionAmount;
this.fromAccountId = fromAccountId;
this.toAccountId = toAccountId;
this.id = id;
}
}
private static final String GET_CONTAINERS_SQL = "SELECT t.amount, t.from_account_id, t.to_account_id, t.id FROM \"transaction\" t INNER JOIN link_transaction_period ltp ON ltp.transaction_id = t.id INNER JOIN period p ON p.id = ltp.period_id WHERE p.type = 'EXPENSE' AND t.\"date\" >= ?";
private static final String ACCOUNT_STATISTIC_EXISTS_SQL = "SELECT id FROM account_statistic WHERE account_id = ? AND period_id = ?";
private static final String ACCOUNT_STATISTIC_UPDATE_FROM_SQL = "UPDATE account_statistic SET spending_total_from = spending_total_from + ?, transaction_count_from = transaction_count_from + 1 WHERE id = ?";
private static final String ACCOUNT_STATISTIC_UPDATE_TO_SQL = "UPDATE account_statistic SET spending_total_to = spending_total_to + ?, transaction_count_to = transaction_count_to + 1 WHERE id = ?";
private static final String ACCOUNT_STATISTIC_INSERT_FROM_SQL = "INSERT INTO account_statistic(account_id, period_id, spending_total_from, transaction_count_from, spending_total_to, transaction_count_to) VALUES(?, ?, ?, 1, 0, 0)";
private static final String ACCOUNT_STATISTIC_INSERT_TO_SQL = "INSERT INTO account_statistic(account_id, period_id, spending_total_from, transaction_count_from, spending_total_to, transaction_count_to) VALUES(?, ?, 0, 0, ?, 1)";
private static final String SELECT_FIRST_PERIOD_SQL = "SELECT start FROM period WHERE type = 'EXPENSE' AND start > '2020-01-01' ORDER BY start ASC";
private static final String INSERT_EXPENSE_YEAR_PERIOD_SQL = "INSERT INTO period (type, start) VALUES ('EXPENSE_YEAR', ?)";
private static final String SELECT_EXPENSE_YEAR_PERIOD_SQL = "SELECT id FROM period WHERE type = 'EXPENSE_YEAR' AND start BETWEEN '2020-01-01' AND '2020-12-31'";
private static final String INSERT_LINK_TRANSACTION_PERIOD_SQL = "INSERT INTO link_transaction_period (transaction_id, period_id) VALUES (?, ?)";
private Timestamp firstPeriodStart;
@Override
public void migrate(Context context) throws Exception {
final long expenseYearPeriodId = createExpenseYearPeriod(context);
if (expenseYearPeriodId == -1) {
return;
}
final List<TransactionAccountsContainer> cons = getContainers(context);
for (TransactionAccountsContainer con : cons) {
handleAccount(con, context, expenseYearPeriodId, true);
handleAccount(con, context, expenseYearPeriodId, false);
handleTransaction(con, context, expenseYearPeriodId);
}
}
private long createExpenseYearPeriod(Context context) throws Exception {
try (PreparedStatement statement =
context
.getConnection()
.prepareStatement(SELECT_FIRST_PERIOD_SQL)) {
statement.setMaxRows(1);
final ResultSet rs = statement.executeQuery();
// The ResultSet contains only one entry as specified with the setMaxRows(1) call above
if (!rs.next()) {
return -1; // it may happen that no period for 2020 exists
}
firstPeriodStart = rs.getTimestamp(1);
try (PreparedStatement statementInsert =
context
.getConnection()
.prepareStatement(INSERT_EXPENSE_YEAR_PERIOD_SQL)) {
statementInsert.setTimestamp(1, firstPeriodStart);
statementInsert.execute();
try (PreparedStatement statementSelect =
context
.getConnection()
.prepareStatement(SELECT_EXPENSE_YEAR_PERIOD_SQL)) {
final ResultSet rsSelect = statementSelect.executeQuery();
// We know it's there as we just created it
rsSelect.next();
return rsSelect.getLong(1);
}
}
}
}
private List<TransactionAccountsContainer> getContainers(Context context) throws SQLException {
try (PreparedStatement statement =
context
.getConnection()
.prepareStatement(GET_CONTAINERS_SQL)) {
statement.setDate(1, new Date(firstPeriodStart.toLocalDateTime().toInstant(ZoneOffset.UTC).truncatedTo(ChronoUnit.DAYS).toEpochMilli()));
final ResultSet rs = statement.executeQuery();
final List<TransactionAccountsContainer> cons = new ArrayList<>();
while(rs.next()) {
cons.add(new TransactionAccountsContainer(
rs.getLong(1), // transaction amount
rs.getLong(2), // from account ID
rs.getLong(3), // to account ID
rs.getLong(4) // ID
));
}
return cons;
}
}
private void handleAccount(TransactionAccountsContainer con, Context context, long expenseYearPeriodId, boolean from) throws SQLException {
try (PreparedStatement statement =
context
.getConnection()
.prepareStatement(ACCOUNT_STATISTIC_EXISTS_SQL)) {
statement.setLong(1, from ? con.fromAccountId : con.toAccountId);
statement.setLong(2, expenseYearPeriodId);
final ResultSet rs = statement.executeQuery();
boolean found = false;
// Only one possible entry because of the unique constraint
while(rs.next()) {
found = true;
try (PreparedStatement statementUpdate =
context
.getConnection()
.prepareStatement(from ? ACCOUNT_STATISTIC_UPDATE_FROM_SQL : ACCOUNT_STATISTIC_UPDATE_TO_SQL)) {
statementUpdate.setLong(1, con.transactionAmount);
statementUpdate.setLong(2, rs.getLong(1));
statementUpdate.executeUpdate();
}
}
// We need to create a new account_statistic entry for the tuple {account_id;period_id}
if (!found) {
try (PreparedStatement statementInsert =
context
.getConnection()
.prepareStatement(from ? ACCOUNT_STATISTIC_INSERT_FROM_SQL : ACCOUNT_STATISTIC_INSERT_TO_SQL)) {
statementInsert.setLong(1, from ? con.fromAccountId : con.toAccountId);
statementInsert.setLong(2, expenseYearPeriodId);
statementInsert.setLong(3, con.transactionAmount);
statementInsert.execute();
}
}
}
}
private void handleTransaction(TransactionAccountsContainer con, Context context, long expenseYearPeriodId) throws SQLException {
try (PreparedStatement statementInsert =
context
.getConnection()
.prepareStatement(INSERT_LINK_TRANSACTION_PERIOD_SQL)) {
statementInsert.setLong(1, con.id);
statementInsert.setLong(2, expenseYearPeriodId);
statementInsert.execute();
}
}
}

View File

@@ -7,11 +7,23 @@ import org.springframework.data.repository.CrudRepository;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.time.LocalDateTime;
@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")
Period findGrandTotalPeriod(PeriodType type);
@Query("SELECT p FROM Period p WHERE p.type = :type AND YEAR(p.start) = :year")
Period findExpenseYearPeriod(PeriodType type, Integer year);
@Query("SELECT p FROM Period p WHERE p.type = :type AND YEAR(p.start) = :year")
Iterable<Period> getAllPeriodsForYear(PeriodType type, Integer year);
@Query("SELECT p FROM Period p WHERE p.type = :type AND p.start <= :date AND p.end >= :date")
Period findPeriodForDate(PeriodType type, LocalDateTime date);
}

View File

@@ -15,7 +15,7 @@ public interface RecurringTransactionRepository extends CrudRepository<Recurring
Iterable<RecurringTransaction> findRecurringTransactionsByFromAccountOrToAccount(Account fromAccount, Account toAccount);
@Query("SELECT rt FROM RecurringTransaction rt WHERE rt.deleted = false AND (rt.lastOccurrence IS NULL OR rt.lastOccurrence >= :lastOccurrence)")
Iterable<RecurringTransaction> findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(LocalDate lastOccurrence);
Iterable<RecurringTransaction> findAllActive(LocalDate lastOccurrence);
Iterable<RecurringTransaction> findByDeletedFalse();
}

View File

@@ -1,10 +1,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 de.financer.model.*;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.transaction.annotation.Propagation;
@@ -15,13 +12,14 @@ import java.util.List;
@Transactional(propagation = Propagation.REQUIRED)
public interface TransactionRepository extends CrudRepository<Transaction, Long> {
@Query("SELECT t FROM Transaction t WHERE t.toAccount = :toAccount OR t.fromAccount = :fromAccount")
Iterable<Transaction> findTransactionsByFromAccountOrToAccount(Account fromAccount, Account toAccount);
@Query("SELECT SUM(t.amount) FROM Transaction t JOIN t.periods p JOIN t.toAccount a WHERE a.type IN :expenseTypes AND p = :period")
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 GROUP BY p ORDER BY p.start ASC")
List<Long> getExpensesForAllPeriods(AccountType startType, 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")
List<Long> getExpensesForAllPeriods(PeriodType type, AccountType startType, AccountType... expenseTypes);
// The HQL contains a hack because Hibernate can't resolve the alias of the CASE column in the GROUP BY clause
// That's why the generated alias is used directly in the HQL. It will break if the columns in the SELECT clause get reordered

View File

@@ -4,6 +4,7 @@ import de.financer.ResponseReason;
import de.financer.dba.PeriodRepository;
import de.financer.model.Period;
import de.financer.model.PeriodType;
import de.financer.model.Transaction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.slf4j.Logger;
@@ -11,7 +12,10 @@ import org.slf4j.LoggerFactory;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Month;
@Service
public class PeriodService {
@@ -24,16 +28,24 @@ public class PeriodService {
private AccountStatisticService accountStatisticService;
/**
* @return the currently open expense period
* @return the currently open {@link PeriodType#EXPENSE expense} period
*/
public Period getCurrentExpensePeriod() {
return this.periodRepository.findCurrentExpensePeriod(PeriodType.EXPENSE);
}
/**
* @return the {@link PeriodType#GRAND_TOTAL grand total} period
*/
public Period getGrandTotalPeriod() {
return this.periodRepository.findGrandTotalPeriod(PeriodType.GRAND_TOTAL);
}
/**
* This method closes the current expense period and opens a new one.
*
* @return {@link ResponseReason#OK} if the operation succeeded, {@link ResponseReason#OK}
* @return {@link ResponseReason#OK} if the operation succeeded, {@link ResponseReason#UNKNOWN_ERROR} if an
* unexpected exception occurred.
*/
@Transactional(propagation = Propagation.REQUIRED)
public ResponseReason closeCurrentExpensePeriod() {
@@ -51,6 +63,8 @@ public class PeriodService {
this.periodRepository.save(currentPeriod);
this.periodRepository.save(nextPeriod);
handleExpenseYearPeriod(currentPeriod, nextPeriod);
this.accountStatisticService.generateNullStatisticsForUnusedAccounts(currentPeriod);
response = ResponseReason.OK;
@@ -60,10 +74,62 @@ public class PeriodService {
response = ResponseReason.UNKNOWN_ERROR;
}
return response;
return response;
}
private void handleExpenseYearPeriod(Period currentPeriod, Period nextPeriod) {
final Period expenseYearPeriod = this.periodRepository
.findExpenseYearPeriod(PeriodType.EXPENSE_YEAR, currentPeriod.getStart().getYear());
if (expenseYearPeriod.getStart().getYear() != nextPeriod.getStart().getYear()) {
expenseYearPeriod.setEnd(currentPeriod.getEnd());
final Period nextExpenseYearPeriod = new Period();
nextExpenseYearPeriod.setType(PeriodType.EXPENSE_YEAR);
nextExpenseYearPeriod.setStart(nextPeriod.getStart());
this.periodRepository.save(expenseYearPeriod);
this.periodRepository.save(nextExpenseYearPeriod);
}
}
/**
* @return the {@link PeriodType#EXPENSE_YEAR expense year} period for the given expense period
*/
public Period getExpenseYearPeriod(Period expensePeriod) {
return this.periodRepository
.findExpenseYearPeriod(PeriodType.EXPENSE_YEAR, expensePeriod.getStart().getYear());
}
public Iterable<Period> getAllExpensePeriodsForYear(Integer year) {
return this.periodRepository.getAllPeriodsForYear(PeriodType.EXPENSE, year);
}
/**
* <p>
* This method gets the {@link Period} for the date of the given transaction. It is intended to handle the edge case
* of <i>late booking</i> a transaction after the corresponding period has already been closed. So most of the time
* it returns the {@link PeriodService#getCurrentExpensePeriod() current expense period}.
* </p>
* <p>
* It is important to note this method also returns the current expense period for transactions which have a date in
* the future.
* </p>
*
* @param transaction to get the period for
*
* @return either the current expense period or one of the past
*/
public Period getExpensePeriodForTransaction(Transaction transaction) {
// Using LocalTime.now() here is kinda hacky. We know that we are only called from transaction creation.
Period period = this.periodRepository
.findPeriodForDate(PeriodType.EXPENSE, LocalDateTime.of(transaction.getDate(), LocalTime.now()));
if (period == null) {
period = this.getCurrentExpensePeriod();
}
return period;
}
}

View File

@@ -3,10 +3,7 @@ package de.financer.service;
import de.financer.ResponseReason;
import de.financer.config.FinancerConfig;
import de.financer.dba.RecurringTransactionRepository;
import de.financer.model.Account;
import de.financer.model.HolidayWeekendType;
import de.financer.model.IntervalType;
import de.financer.model.RecurringTransaction;
import de.financer.model.*;
import org.apache.commons.collections4.IterableUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
@@ -45,13 +42,15 @@ public class RecurringTransactionService {
@Autowired
private TransactionService transactionService;
@Autowired
private PeriodService periodService;
public Iterable<RecurringTransaction> getAll() {
return this.recurringTransactionRepository.findByDeletedFalse();
}
public Iterable<RecurringTransaction> getAllActive() {
return this.recurringTransactionRepository
.findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(LocalDate.now());
return this.recurringTransactionRepository.findAllActive(LocalDate.now());
}
public Iterable<RecurringTransaction> getAllForAccount(String accountKey) {
@@ -85,7 +84,7 @@ public class RecurringTransactionService {
// there would never be a reminder about them. On the actual due date the reminder is deferred because of the
// HWT and for later runs it's not grabbed because of the condition '...LastOccurrenceGreaterThanEqual(now)'
final Iterable<RecurringTransaction> allRecurringTransactions = this.recurringTransactionRepository
.findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(now.minusDays(7));
.findAllActive(now.minusDays(7));
LOGGER.debug(String.format("Found %s candidate recurring transactions. Checking which are due",
IterableUtils.size(allRecurringTransactions)));

View File

@@ -17,9 +17,7 @@ import org.springframework.transaction.annotation.Transactional;
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;
import java.util.*;
@Service
public class TransactionService {
@@ -92,7 +90,7 @@ public class TransactionService {
try {
final Transaction transaction = buildTransaction(fromAccount, toAccount, amount, description, date, recurringTransaction);
transaction.setPeriods(Collections.singleton(this.periodService.getCurrentExpensePeriod()));
transaction.setPeriods(getRelevantPeriods(transaction));
fromAccount.setCurrentBalance(fromAccount.getCurrentBalance() + (this.ruleService
.getMultiplierFromAccount(fromAccount) * amount));
@@ -124,6 +122,17 @@ public class TransactionService {
return response;
}
private Set<Period> getRelevantPeriods(Transaction transaction) {
final Set<Period> periods = new HashSet<>();
final Period expensePeriod = this.periodService.getExpensePeriodForTransaction(transaction);
periods.add(expensePeriod);
periods.add(this.periodService.getExpenseYearPeriod(expensePeriod));
periods.add(this.periodService.getGrandTotalPeriod());
return periods;
}
/**
* This method builds the actual transaction object with the given values.
*
@@ -252,13 +261,14 @@ public class TransactionService {
}
/**
* This method gets all sums of expenses of all periods, ordered from oldest to newest.
* This method gets all sums of expenses of all expense periods, ordered from oldest to newest.
*
* @return the sums of expenses - may be empty
* @see TransactionService#getExpensesCurrentPeriod()
*/
public List<Long> getExpensesAllPeriods() {
return this.transactionRepository.getExpensesForAllPeriods(AccountType.START, AccountType.EXPENSE, AccountType.LIABILITY);
return this.transactionRepository
.getExpensesForAllPeriods(PeriodType.EXPENSE, AccountType.START, AccountType.EXPENSE, AccountType.LIABILITY);
}
/**