#20 Add transaction type

This commit is contained in:
2021-08-08 23:38:41 +02:00
parent 7ffc597727
commit 0bb534c8b4
19 changed files with 373 additions and 104 deletions

View File

@@ -23,7 +23,7 @@ orderByExpression
regularExpression
: (stringExpression | intExpression | booleanExpression | dateExpression) ;
stringExpression
: field=IDENTIFIER operator=STRING_OPERATOR value=(STRING_VALUE | ACCOUNT_TYPE_VALUE) ;
: field=IDENTIFIER operator=STRING_OPERATOR value=STRING_VALUE ;
intExpression
: field=IDENTIFIER operator=(INT_OPERATOR | STRING_OPERATOR) value=INT_VALUE ;
@@ -122,7 +122,7 @@ L_PAREN : '(' ;
R_PAREN : ')' ;
IDENTIFIER : [a-zA-Z]+ ;
INT_VALUE : [0-9]+ ;
STRING_VALUE : '\'' [a-zA-Z0-9\-/.&%@$ ]+ '\'' ;
STRING_VALUE : '\'' [a-zA-Z0-9\-/.&%@$_ ]+ '\'' ;
DATE_VALUE : [0-9][0-9][0-9][0-9][-][0-9][0-9][-][0-9][0-9] ; // ANTLR does not support regex quantifiers
NEWLINE : ('\r'? '\n' | '\r')+ ;

View File

@@ -0,0 +1,133 @@
package database.common;
import de.financer.model.AccountType;
import de.financer.model.TransactionType;
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.SQLException;
import java.util.*;
import static de.financer.model.AccountType.*;
import static de.financer.model.AccountType.BANK;
public class V48_0_1__calculateTransactionType extends BaseJavaMigration {
private static class TransactionContainer {
public final Long transactionId;
public final AccountType fromAccountType;
public final AccountType toAccountType;
public TransactionContainer(Long transactionId, AccountType fromAccountType, AccountType toAccountType) {
this.transactionId = transactionId;
this.fromAccountType = fromAccountType;
this.toAccountType = toAccountType;
}
}
private static final String GET_CONTAINERS_SQL = "SELECT t.id, fa.type, ta.type FROM \"transaction\" t INNER JOIN account fa ON fa.id = t.from_account_id INNER JOIN account ta ON ta.id = t.to_account_id";
private static final String UPDATE_TRANSACTION_SQL = "UPDATE \"transaction\" SET transaction_type = ? WHERE id = ?";
private Map<TransactionTypeDefinition, TransactionType> transactionTypeDefinitions;
@Override
public void migrate(Context context) throws Exception {
initTransactionTypes();
final List<TransactionContainer> cons = getContainers(context);
TransactionType transactionType;
for (TransactionContainer con : cons) {
transactionType = getTransactionType(con.fromAccountType, con.toAccountType);
updateTransaction(context, con.transactionId, transactionType.name());
}
}
private void updateTransaction(Context context, Long transactionId, String transactionType) throws SQLException {
try (PreparedStatement statementUpdate =
context
.getConnection()
.prepareStatement(UPDATE_TRANSACTION_SQL)) {
statementUpdate.setString(1, transactionType);
statementUpdate.setLong(2, transactionId);
statementUpdate.executeUpdate();
}
}
private List<TransactionContainer> getContainers(Context context) throws SQLException {
try (PreparedStatement statement =
context
.getConnection()
.prepareStatement(GET_CONTAINERS_SQL)) {
final ResultSet rs = statement.executeQuery();
final List<TransactionContainer> cons = new ArrayList<>();
while(rs.next()) {
cons.add(new TransactionContainer(
rs.getLong(1), // transaction id
AccountType.valueOf(rs.getString(2)), // from account type
AccountType.valueOf(rs.getString(3)) // to account type
));
}
return cons;
}
}
private void initTransactionTypes() {
this.transactionTypeDefinitions = new HashMap<>();
this.transactionTypeDefinitions
.put(TransactionTypeDefinition.withFrom(CASH, BANK)
.withTo(CASH, BANK), TransactionType.ASSET_SWAP);
this.transactionTypeDefinitions
.put(TransactionTypeDefinition.withFrom(CASH, BANK, LIABILITY)
.withTo(EXPENSE), TransactionType.EXPENSE);
this.transactionTypeDefinitions
.put(TransactionTypeDefinition.withFrom(CASH, BANK)
.withTo(LIABILITY), TransactionType.LIABILITY);
this.transactionTypeDefinitions
.put(TransactionTypeDefinition.withFrom(START, INCOME, LIABILITY)
.withTo(CASH, BANK), TransactionType.INCOME);
this.transactionTypeDefinitions
.put(TransactionTypeDefinition.withFrom(START)
.withTo(LIABILITY), TransactionType.START_LIABILITY);
}
private TransactionType getTransactionType(AccountType fromAccountType, AccountType toAccountType) {
return this.transactionTypeDefinitions.entrySet().stream()
.filter(ttdE -> ttdE.getKey().matches(fromAccountType, toAccountType))
.findFirst().map(ttd -> ttd.getValue()).orElse(null);
}
private static class TransactionTypeDefinition {
private Collection<AccountType> fromAccountTypes = new ArrayList<>();
private Collection<AccountType> toAccountTypes = new ArrayList<>();
public static TransactionTypeDefinition withFrom(AccountType... fromAccountTypes) {
TransactionTypeDefinition def = new TransactionTypeDefinition();
def.fromAccountTypes.addAll(Arrays.asList(fromAccountTypes));
return def;
}
public TransactionTypeDefinition withTo(AccountType... toAccountTypes) {
this.toAccountTypes.addAll(Arrays.asList(toAccountTypes));
return this;
}
public boolean matches(AccountType fromAccountType, AccountType toAccountType) {
return this.fromAccountTypes.contains(fromAccountType) && this.toAccountTypes.contains(toAccountType);
}
}
}

View File

@@ -50,7 +50,10 @@ public enum FieldMapping {
JoinKey.of(File.class), null, NotNullSyntheticHandler.class),
DESCRIPTION("description", Transaction_.DESCRIPTION, Transaction.class, NoopJoinHandler.class,
JoinKey.of(Transaction.class), null, StringHandler.class);
JoinKey.of(Transaction.class), null, StringHandler.class),
TRANSACTION_TYPE("transactionType", Transaction_.TRANSACTION_TYPE, Transaction.class, NoopJoinHandler.class,
JoinKey.of(Transaction.class), null, TransactionTypeHandler.class);
private final String fieldName;

View File

@@ -0,0 +1,38 @@
package de.financer.fql.field_handler;
import de.financer.fql.FQLException;
import de.financer.fql.FieldMapping;
import de.financer.fql.join_handler.JoinKey;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.From;
import javax.persistence.criteria.Predicate;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
public abstract class AbstractEnumTypeHandler<T extends Enum<?>> implements FieldHandler<String> {
private Class<T> clazz;
protected AbstractEnumTypeHandler(Class<T> clazz) {
this.clazz = clazz;
}
@Override
public Predicate apply(FieldMapping fieldMapping, Map<JoinKey, From<?, ?>> froms, CriteriaBuilder criteriaBuilder, String value) {
return criteriaBuilder
.equal(froms.get(fieldMapping.getJoinKey()).get(fieldMapping.getAttributeName()), toEnumType(FieldHandlerUtils.removeQuotes(value)));
}
private T toEnumType(String value) {
if (value == null) {
throw new FQLException(String.format("NULL cannot be resolved to %s!", this.clazz.getName()));
}
try {
return (T) this.clazz.getMethod("valueOf", String.class).invoke(null, value.toUpperCase());
}
catch (IllegalArgumentException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
throw new FQLException(e);
}
}
}

View File

@@ -1,32 +1,9 @@
package de.financer.fql.field_handler;
import de.financer.fql.FQLException;
import de.financer.fql.FieldMapping;
import de.financer.fql.join_handler.JoinKey;
import de.financer.model.AccountType;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.From;
import javax.persistence.criteria.Predicate;
import java.util.Map;
public class AccountTypeHandler implements FieldHandler<String> {
@Override
public Predicate apply(FieldMapping fieldMapping, Map<JoinKey, From<?, ?>> froms, CriteriaBuilder criteriaBuilder, String value) {
return criteriaBuilder
.equal(froms.get(fieldMapping.getJoinKey()).get(fieldMapping.getAttributeName()), toAccountType(FieldHandlerUtils.removeQuotes(value)));
}
private static AccountType toAccountType(String value) {
if (value == null) {
throw new FQLException("NULL cannot be solved to AccountType!");
}
try {
return AccountType.valueOf(value.toUpperCase());
}
catch (IllegalArgumentException e) {
throw new FQLException(e);
}
public class AccountTypeHandler extends AbstractEnumTypeHandler<AccountType> {
public AccountTypeHandler() {
super(AccountType.class);
}
}

View File

@@ -0,0 +1,9 @@
package de.financer.fql.field_handler;
import de.financer.model.TransactionType;
public class TransactionTypeHandler extends AbstractEnumTypeHandler<TransactionType> {
public TransactionTypeHandler() {
super(TransactionType.class);
}
}

View File

@@ -1,9 +1,7 @@
package de.financer.service;
import de.financer.config.FinancerConfig;
import de.financer.model.Account;
import de.financer.model.AccountType;
import de.financer.model.IntervalType;
import de.financer.model.*;
import de.jollyday.HolidayManager;
import de.jollyday.ManagerParameters;
import org.slf4j.Logger;
@@ -32,11 +30,13 @@ public class RuleService implements InitializingBean {
private Map<AccountType, Collection<AccountType>> bookingRules;
private Map<IntervalType, Period> intervalPeriods;
private Map<TransactionTypeDefinition, TransactionType> transactionTypeDefinitions;
@Override
public void afterPropertiesSet() {
initBookingRules();
initIntervalValues();
initTransactionTypes();
}
private void initIntervalValues() {
@@ -63,6 +63,30 @@ public class RuleService implements InitializingBean {
this.bookingRules.put(START, Arrays.asList(BANK, CASH, LIABILITY));
}
private void initTransactionTypes() {
this.transactionTypeDefinitions = new HashMap<>();
this.transactionTypeDefinitions
.put(TransactionTypeDefinition.withFrom(CASH, BANK)
.withTo(CASH, BANK), TransactionType.ASSET_SWAP);
this.transactionTypeDefinitions
.put(TransactionTypeDefinition.withFrom(CASH, BANK, LIABILITY)
.withTo(EXPENSE), TransactionType.EXPENSE);
this.transactionTypeDefinitions
.put(TransactionTypeDefinition.withFrom(CASH, BANK)
.withTo(LIABILITY), TransactionType.LIABILITY);
this.transactionTypeDefinitions
.put(TransactionTypeDefinition.withFrom(START, INCOME, LIABILITY)
.withTo(CASH, BANK), TransactionType.INCOME);
this.transactionTypeDefinitions
.put(TransactionTypeDefinition.withFrom(START)
.withTo(LIABILITY), TransactionType.START_LIABILITY);
}
/**
* This method returns the multiplier for the given from account.
* <p>
@@ -185,4 +209,42 @@ public class RuleService implements InitializingBean {
public boolean isWeekend(LocalDate now) {
return EnumSet.of(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY).contains(now.getDayOfWeek());
}
/**
* @param fromAccountType the from account
* @param toAccountType the to account
* @return the {@link TransactionType} matching the given from and to accounts or <code>null</code>
* if no matching transaction type was found
*/
public TransactionType getTransactionType(AccountType fromAccountType, AccountType toAccountType) {
return this.transactionTypeDefinitions.entrySet().stream()
.filter(ttdE -> ttdE.getKey().matches(fromAccountType, toAccountType))
.findFirst().map(ttd -> ttd.getValue()).orElse(null);
}
/**
* Helper class
*/
private static class TransactionTypeDefinition {
private Collection<AccountType> fromAccountTypes = new ArrayList<>();
private Collection<AccountType> toAccountTypes = new ArrayList<>();
public static TransactionTypeDefinition withFrom(AccountType... fromAccountTypes) {
TransactionTypeDefinition def = new TransactionTypeDefinition();
def.fromAccountTypes.addAll(Arrays.asList(fromAccountTypes));
return def;
}
public TransactionTypeDefinition withTo(AccountType... toAccountTypes) {
this.toAccountTypes.addAll(Arrays.asList(toAccountTypes));
return this;
}
public boolean matches(AccountType fromAccountType, AccountType toAccountType) {
return this.fromAccountTypes.contains(fromAccountType) && this.toAccountTypes.contains(toAccountType);
}
}
}

View File

@@ -105,6 +105,8 @@ public class TransactionService {
transaction.setExpenseNeutral(true);
}
transaction.setTransactionType(this.ruleService.getTransactionType(fromAccount.getType(), toAccount.getType()));
this.transactionRepository.save(transaction);
this.accountStatisticService.calculateStatistics(transaction);

View File

@@ -0,0 +1,2 @@
ALTER TABLE "transaction" --escape keyword "transaction"
ADD COLUMN transaction_type VARCHAR(255);

View File

@@ -0,0 +1,2 @@
ALTER TABLE "transaction" --escape keyword "transaction"
ALTER COLUMN transaction_type SET NOT NULL;

View File

@@ -1,41 +1,12 @@
WITH income AS (
-- Regular income based on INCOME accounts
SELECT p.ID, SUM(asIncome.spending_total_from) AS incomeSum
FROM period p
INNER JOIN account_statistic asIncome ON asIncome.period_id = p.id
INNER JOIN account aIncome ON aIncome.id = asIncome.account_id AND aIncome.type = 'INCOME'
SELECT p.id, SUM(amount) AS incomeSum
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 1 = 1
AND t.transaction_type = 'INCOME'
GROUP BY p.id, p.type, p.start, p."end"
),
incomeCredit as (
-- Special case for credits that can be booked from a LIABILITY account to a BANK/CASH account
-- Need to be counted as income as the money is used for expenses and those expenses will be counted
-- as expense, so to make it even we need to count it here as well
SELECT p2.id, SUM(amount) AS incomeCreditSum
FROM "transaction" t
INNER JOIN account a on a.id = t.from_account_id
INNER JOIN account a2 on a2.id = t.to_account_id
INNER JOIN link_transaction_period ltp on ltp.transaction_id = t.id
INNER JOIN period p2 on p2.id = ltp.period_id
WHERE 1 = 1
AND a.type in ('LIABILITY')
AND a2.type in ('BANK', 'CASH')
GROUP BY p2.id, p2.type, p2.start, p2."end"
),
incomeStart as (
-- Special case for money that was there at the starting time of a financer instance
-- Will be counted as income as this money is used for expanses, so to make it even
-- we need to count it here as well
SELECT p2.id, SUM(amount) AS incomeStartSum
FROM "transaction" t
INNER JOIN account a on a.id = t.from_account_id
INNER JOIN account a2 on a2.id = t.to_account_id
INNER JOIN link_transaction_period ltp on ltp.transaction_id = t.id
INNER JOIN period p2 on p2.id = ltp.period_id
WHERE 1 = 1
AND a.type in ('START')
AND a2.type in ('BANK', 'CASH')
GROUP BY p2.id, p2.type, p2.start, p2."end"
),
expense AS (
-- Expense booking - NOT counted is the case LIABILITY -> EXPENSE even though that is a
-- valid booking in the app. This is because we would count the expense once here and a second time
@@ -43,28 +14,21 @@ expense AS (
SELECT p2.id, SUM(amount) AS expenseSum
FROM "transaction" t
INNER JOIN account a on a.id = t.from_account_id
INNER JOIN account a2 on a2.id = t.to_account_id
INNER JOIN link_transaction_period ltp on ltp.transaction_id = t.id
INNER JOIN period p2 on p2.id = ltp.period_id
WHERE 1 = 1
AND t.transaction_type = 'EXPENSE'
AND a.type in ('BANK', 'CASH')
AND a2.type in ('EXPENSE')
GROUP BY p2.id, p2.type, p2.start, p2."end"
),
liability AS (
-- Excluded is the special case for start bookings, START -> LIABILITY
-- as the actual expense for that was some time in the past before the starting
-- of the financer instance
SELECT p2.id, SUM(amount) AS liabilitySum
FROM "transaction" t
INNER JOIN account a on a.id = t.from_account_id
INNER JOIN account a2 on a2.id = t.to_account_id
INNER JOIN link_transaction_period ltp on ltp.transaction_id = t.id
INNER JOIN period p2 on p2.id = ltp.period_id
WHERE 1 = 1
AND a.type in ('BANK', 'CASH')
AND a2.type in ('LIABILITY')
GROUP BY p2.id, p2.type, p2.start, p2."end"
SELECT p3.id, SUM(amount) AS liabilitySum
FROM "transaction" t
INNER JOIN link_transaction_period ltp on ltp.transaction_id = t.id
INNER JOIN period p3 on p3.id = ltp.period_id
WHERE 1 = 1
AND t.transaction_type = 'LIABILITY'
GROUP BY p3.id, p3.type, p3.start, p3."end"
),
assets AS (
-- Returns only the assets for closed periods
@@ -86,16 +50,9 @@ SELECT
p.start PERIOD_START,
p."end" PERIOD_END,
CASE
-- 2^3 possible cases
WHEN i.incomeSum IS NULL AND ic.incomeCreditSum IS NULL AND "is".incomeStartSum IS NULL THEN 0
WHEN i.incomeSum IS NOT NULL AND ic.incomeCreditSum IS NULL AND "is".incomeStartSum IS NULL THEN i.incomeSum
WHEN i.incomeSum IS NULL AND ic.incomeCreditSum IS NOT NULL AND "is".incomeStartSum IS NULL THEN ic.incomeCreditSum
WHEN i.incomeSum IS NOT NULL AND ic.incomeCreditSum IS NOT NULL AND "is".incomeStartSum IS NULL THEN (i.incomeSum + ic.incomeCreditSum)
WHEN i.incomeSum IS NULL AND ic.incomeCreditSum IS NULL AND "is".incomeStartSum IS NOT NULL THEN "is".incomeStartSum
WHEN i.incomeSum IS NOT NULL AND ic.incomeCreditSum IS NULL AND "is".incomeStartSum IS NOT NULL THEN (i.incomeSum + "is".incomeStartSum)
WHEN i.incomeSum IS NULL AND ic.incomeCreditSum IS NOT NULL AND "is".incomeStartSum IS NOT NULL THEN (ic.incomeCreditSum + "is".incomeStartSum)
WHEN i.incomeSum IS NOT NULL AND ic.incomeCreditSum IS NOT NULL AND "is".incomeStartSum IS NOT NULL THEN (i.incomeSum + ic.incomeCreditSum + "is".incomeStartSum)
END INCOME_SUM,
WHEN i.incomeSum IS NULL THEN 0
WHEN i.incomeSum IS NOT NULL THEN i.incomeSum
END INCOME_SUM,
CASE
WHEN e.expenseSum IS NULL THEN 0
WHEN e.expenseSum IS NOT NULL THEN e.expenseSum
@@ -125,8 +82,6 @@ SELECT
FROM
period p
LEFT JOIN income i ON i.ID = p.ID
LEFT JOIN incomeCredit ic ON ic.ID = p.ID
LEFT JOIN incomeStart "is" ON "is".ID = p.ID
LEFT JOIN expense e ON e.ID = p.ID
LEFT JOIN assets a ON a.ID = p.ID
LEFT JOIN liability l ON l.ID = p.ID