Add FQL and rework /transaction endpoint

This commit is contained in:
2020-03-14 23:44:43 +01:00
parent 1cc7fdf052
commit d4fef75903
85 changed files with 2257 additions and 472 deletions

View File

@@ -0,0 +1,103 @@
grammar FQL;
/*
* Parser rules
*/
query
: (orExpression | orderByExpression EOF)* ;
orExpression
: andExpression (OR andExpression)* ;
andExpression
: expression (AND expression)* ;
expression
: (regularExpression | betweenExpression | periodExpression)
| parenthesisExpression ;
parenthesisExpression
: L_PAREN expr=orExpression R_PAREN ;
orderByExpression
: ORDER_BY field=IDENTIFIER order=(DESC | ASC ) ;
// regular expressions - NOT regex
regularExpression
: (stringExpression | intExpression | booleanExpression | dateExpression) ;
stringExpression
: field=IDENTIFIER operator=STRING_OPERATOR value=STRING_VALUE ;
intExpression
: field=IDENTIFIER operator=(INT_OPERATOR | STRING_OPERATOR) value=INT_VALUE ;
booleanExpression
: field=IDENTIFIER operator=STRING_OPERATOR value=(TRUE | FALSE) ;
dateExpression
: field=IDENTIFIER operator=STRING_OPERATOR value=DATE_VALUE ;
// BETWEEN expressions
betweenExpression
: (betweenStringExpression | betweenIntExpression ) ;
betweenStringExpression
: field=IDENTIFIER BETWEEN left=STRING_VALUE AND right=STRING_VALUE ;
betweenIntExpression
: field=IDENTIFIER BETWEEN left=INT_VALUE AND right=INT_VALUE ;
// period expressions
periodExpression
: periodConstExpression ;
periodConstExpression
: field=IDENTIFIER operator=STRING_OPERATOR value=(CURRENT |
LAST |
CURRENT_YEAR |
LAST_YEAR |
GRAND_TOTAL) ;
/*
* Lexer rules
*/
fragment B : ('B' | 'b') ;
fragment E : ('E' | 'e') ;
fragment T : ('T' | 't') ;
fragment W : ('W' | 'w') ;
fragment N : ('N' | 'n') ;
fragment A : ('A' | 'a') ;
fragment D : ('D' | 'd') ;
fragment C : ('C' | 'c') ;
fragment U : ('U' | 'u') ;
fragment R : ('R' | 'r') ;
fragment L : ('L' | 'l') ;
fragment S : ('S' | 's') ;
fragment Y : ('Y' | 'y') ;
fragment G : ('G' | 'g') ;
fragment O : ('O' | 'o') ;
fragment F : ('F' | 'f') ;
fragment SPACE : ' ' ;
// Keywords
BETWEEN : B E T W E E N ;
AND : A N D ;
OR : O R ;
ORDER_BY : O R D E R SPACE B Y ;
DESC : D E S C ;
ASC : A S C ;
TRUE : T R U E ;
FALSE : F A L S E ;
// Constant values
CURRENT : C U R R E N T ;
LAST : L A S T ;
YEAR : Y E A R ;
GRAND_TOTAL : G R A N D '_' T O T A L ;
CURRENT_YEAR : CURRENT '_' YEAR ;
LAST_YEAR : LAST '_' YEAR ;
STRING_OPERATOR : '=' ;
INT_OPERATOR : '>' | '<' | '>=' | '<=' | '!=' ;
L_PAREN : '(' ;
R_PAREN : ')' ;
IDENTIFIER : [a-zA-Z]+ ;
INT_VALUE : [0-9]+ ;
STRING_VALUE : '\'' [a-zA-Z0-9- ]+ '\'' ;
DATE_VALUE : [0-9-]+ ;
NEWLINE : ('\r'? '\n' | '\r')+ ;
WHITESPACE : ' ' -> skip;

View File

@@ -11,10 +11,10 @@ import java.util.List;
public class V22_0_1__calculateAccountStatistics extends BaseJavaMigration {
private static class TransactionPeriodAccountsContainer {
public Long transactionAmount;
public Long expensePeriodId;
public Long fromAccountId;
public Long toAccountId;
public final Long transactionAmount;
public final Long expensePeriodId;
public final Long fromAccountId;
public final Long toAccountId;
public TransactionPeriodAccountsContainer(Long transactionAmount, Long expensePeriodId, Long fromAccountId, Long toAccountId) {
this.transactionAmount = transactionAmount;

View File

@@ -12,10 +12,10 @@ 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 final Long transactionAmount;
public final Long fromAccountId;
public final Long toAccountId;
public final Long id;
public TransactionAccountsContainer(Long transactionAmount, Long fromAccountId, Long toAccountId, Long id) {
this.transactionAmount = transactionAmount;

View File

@@ -14,10 +14,10 @@ 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 final Long transactionAmount;
public final Long fromAccountId;
public final Long toAccountId;
public final Long id;
public TransactionAccountsContainer(Long transactionAmount, Long fromAccountId, Long toAccountId, Long id) {
this.transactionAmount = transactionAmount;
this.fromAccountId = fromAccountId;

View File

@@ -6,7 +6,6 @@ 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;
@@ -14,10 +13,10 @@ 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 final Long transactionAmount;
public final Long fromAccountId;
public final Long toAccountId;
public final Long id;
public TransactionAccountsContainer(Long transactionAmount, Long fromAccountId, Long toAccountId, Long id) {
this.transactionAmount = transactionAmount;
this.fromAccountId = fromAccountId;

View File

@@ -1,45 +0,0 @@
package de.financer;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
public enum ResponseReason {
OK(HttpStatus.OK),
UNKNOWN_ERROR(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;
ResponseReason(HttpStatus httpStatus) {
this.httpStatus = httpStatus;
}
public ResponseEntity toResponseEntity() {
return new ResponseEntity<>(this.name(), this.httpStatus);
}
}

View File

@@ -6,7 +6,6 @@ import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import javax.management.ReflectionException;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Collection;
@@ -68,10 +67,10 @@ public class FinancerConfig {
/**
* @return the date format used in e.g. the {@link de.financer.service.TransactionService#createTransaction(String,
* String, Long, String, String) TransactionService#createTransaction} or {@link
* String, Long, String, String, Boolean)} TransactionService#createTransaction} or {@link
* de.financer.service.RecurringTransactionService#createRecurringTransaction(String, String, Long, String, String,
* String, String, String) RecurringTransactionService#createRecurringTransaction} methods. Used to parse the
* client-supplied date string to proper {@link java.time.LocalDate LocalDate} objects
* String, String, String, Boolean, Boolean)} RecurringTransactionService#createRecurringTransaction} methods. Used
* to parse the client-supplied date string to proper {@link java.time.LocalDate LocalDate} objects
*/
public String getDateFormat() {
return dateFormat;

View File

@@ -11,7 +11,7 @@ public class ControllerUtil {
* @param toDecode the string to decode
* @return the decoded string in UTF-8 or, if UTF-8 is not available for whatever reason, the encoded string
*/
public static final String urlDecode(String toDecode) {
public static String urlDecode(String toDecode) {
try {
return URLDecoder.decode(toDecode, StandardCharsets.UTF_8.name());
}

View File

@@ -10,8 +10,6 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
@RestController

View File

@@ -2,125 +2,160 @@ package de.financer.controller;
import de.financer.ResponseReason;
import de.financer.dto.ExpensePeriodTotal;
import de.financer.model.Transaction;
import de.financer.dto.SaveTransactionRequestDto;
import de.financer.dto.SearchTransactionsResponseDto;
import de.financer.service.TransactionService;
import de.financer.service.parameter.SearchTransactionsParameter;
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;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("transactions")
public class TransactionController {
private static final Logger LOGGER = LoggerFactory.getLogger(TransactionController.class);
@Autowired
private TransactionService transactionService;
@RequestMapping("getAll")
public Iterable<Transaction> getAll() {
return this.transactionService.getAll();
}
@RequestMapping("getAllForAccount")
public Iterable<Transaction> getAllForAccount(String accountKey) {
final String decoded = ControllerUtil.urlDecode(accountKey);
@GetMapping("/transactionsByFql")
public Iterable<SearchTransactionsResponseDto> searchTransactions(String fql) {
final String fqlDecoded = ControllerUtil.urlDecode(fql);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("/transactions/getAllForAccount got parameter: %s", decoded));
LOGGER.debug(String.format("GET /transactionsByFql/ got parameter: %s", fqlDecoded));
}
return this.transactionService.getAllForAccount(decoded);
}
@RequestMapping(value = "createTransaction")
public ResponseEntity createTransaction(String fromAccountKey, String toAccountKey, Long amount, String date,
String description, Boolean taxRelevant
) {
final String decodedFrom = ControllerUtil.urlDecode(fromAccountKey);
final String decodedTo = ControllerUtil.urlDecode(toAccountKey);
final String decodedDesc = ControllerUtil.urlDecode(description);
final Iterable<SearchTransactionsResponseDto> transactionSearchResponse =
this.transactionService.searchTransactions(fqlDecoded);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String
.format("/transactions/createTransaction got parameters: %s, %s, %s, %s, %s, %s",
decodedFrom, decodedTo, amount, date, decodedDesc, taxRelevant));
LOGGER.debug(String.format("GET /transactionsByFql returns with %s", transactionSearchResponse));
}
return transactionSearchResponse;
}
@GetMapping("/transactions")
public Iterable<SearchTransactionsResponseDto> searchTransactions(String fromAccountKey,
String toAccountKey,
String periodId,
String limit,
String order,
String taxRelevant,
String accountsAnd) {
final String fromDecoded = ControllerUtil.urlDecode(fromAccountKey);
final String toDecoded = ControllerUtil.urlDecode(toAccountKey);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("GET /transactions/ got parameter: %s, %s, %s, %s, %s, %s - with order %s",
fromDecoded, toDecoded, periodId, limit, taxRelevant, accountsAnd, order));
}
// Wrap the url parameters into a proper POJO so the service parameter list wont get polluted
// There is no validation taking place here, only the wrapping. Validation happens in the service.
final SearchTransactionsParameter parameter = new SearchTransactionsParameter();
parameter.setFromAccountKey(fromDecoded);
parameter.setToAccountKey(toDecoded);
parameter.setLimit(limit);
parameter.setPeriodId(periodId);
parameter.setOrder(order);
parameter.setTaxRelevant(taxRelevant);
parameter.setAccountsAnd(accountsAnd);
final Iterable<SearchTransactionsResponseDto> transactionSearchResponse =
this.transactionService.searchTransactions(parameter);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("GET /transactions returns with %s", transactionSearchResponse));
}
return transactionSearchResponse;
}
@PostMapping(value = "/transactions")
public ResponseEntity createTransaction(@RequestBody SaveTransactionRequestDto requestDto) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("POST /transactions got parameters: %s", requestDto));
}
final ResponseReason responseReason = this.transactionService
.createTransaction(decodedFrom, decodedTo, amount, date, decodedDesc, taxRelevant);
.createTransaction(requestDto.getFromAccountKey(),
requestDto.getToAccountKey(),
//TODO Conversion should be moved to
//TODO service and cause a proper response
//TODO if the value is not numeric
Long.valueOf(requestDto.getAmount()),
requestDto.getDate(),
requestDto.getDescription(),
requestDto.getTaxRelevant());
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("/transactions/createTransaction returns with %s", responseReason.name()));
LOGGER.debug(String.format("POST /transactions returns with %s", responseReason.name()));
}
return responseReason.toResponseEntity();
}
@RequestMapping("deleteTransaction")
public ResponseEntity deleteTransaction(String transactionId) {
@DeleteMapping("/transactions/{transactionId}")
public ResponseEntity deleteTransaction(@PathVariable String transactionId) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String
.format("/transactions/deleteTransaction got parameters: %s",
transactionId));
LOGGER.debug(String.format("DELETE /transactions/{transactionId} got parameters: %s", transactionId));
}
final ResponseReason responseReason = this.transactionService
.deleteTransaction(transactionId);
final ResponseReason responseReason = this.transactionService.deleteTransaction(transactionId);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String
.format("/transactions/deleteTransaction returns with %s", responseReason.name()));
LOGGER.debug(String.format("DELETE /transactions/{transactionId} returns with %s", responseReason.name()));
}
return responseReason.toResponseEntity();
}
@RequestMapping("getExpensesCurrentPeriod")
@GetMapping("/transactions/getExpensesCurrentPeriod")
public Long getExpensesCurrentPeriod() {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("/transactions/getExpensesCurrentPeriod called"));
LOGGER.debug(String.format("GET /transactions/getExpensesCurrentPeriod called"));
}
final Long response = this.transactionService.getExpensesCurrentPeriod();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("/transactions/getExpensesCurrentPeriod returns with %s", response));
LOGGER.debug(String.format("GET /transactions/getExpensesCurrentPeriod returns with %s", response));
}
return response;
}
@RequestMapping("getExpensePeriodTotals")
@GetMapping("/transactions/getExpensePeriodTotals")
public Iterable<ExpensePeriodTotal> getExpensePeriodTotals(Integer year) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("/transactions/getExpensePeriodTotals got parameters: %s", year));
LOGGER.debug(String.format("GET /transactions/getExpensePeriodTotals got parameters: %s", year));
}
final Iterable<ExpensePeriodTotal> expensePeriodTotals = this.transactionService
.getExpensePeriodTotals(year);
final Iterable<ExpensePeriodTotal> expensePeriodTotals = this.transactionService.getExpensePeriodTotals(year);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("/transactions/getExpensePeriodTotals returns with %s", expensePeriodTotals));
LOGGER.debug(String.format("GET /transactions/getExpensePeriodTotals returns with %s", expensePeriodTotals));
}
return expensePeriodTotals;
}
@RequestMapping("getExpensesAllPeriods")
@GetMapping("/transactions/getExpensesAllPeriods")
public List<Long> getExpensesAllPeriods() {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("/transactions/getExpensesAllPeriods called"));
LOGGER.debug(String.format("GET /transactions/getExpensesAllPeriods called"));
}
final List<Long> response = this.transactionService.getExpensesAllPeriods();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("/transactions/getExpensesAllPeriods returns with %s", response));
LOGGER.debug(String.format("GET /transactions/getExpensesAllPeriods returns with %s", response));
}
return response;

View File

@@ -7,7 +7,6 @@ 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)

View File

@@ -4,7 +4,6 @@ import de.financer.model.Account;
import de.financer.model.RecurringTransaction;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

View File

@@ -7,11 +7,10 @@ import org.springframework.data.repository.CrudRepository;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.util.List;
@Transactional(propagation = Propagation.REQUIRED)
public interface TransactionRepository extends CrudRepository<Transaction, Long> {
public interface TransactionRepository extends CrudRepository<Transaction, Long>, TransactionRepositoryCustom {
@Query("SELECT t FROM Transaction t WHERE t.toAccount = :toAccount OR t.fromAccount = :fromAccount")
Iterable<Transaction> findTransactionsByFromAccountOrToAccount(Account fromAccount, Account toAccount);

View File

@@ -0,0 +1,18 @@
package de.financer.dba;
import de.financer.dto.Order;
import de.financer.dto.SearchTransactionsResponseDto;
import de.financer.model.Account;
import de.financer.model.Period;
public interface TransactionRepositoryCustom {
Iterable<SearchTransactionsResponseDto> searchTransactions(Period period,
Account fromAccount,
Account toAccount,
Integer limit,
Order order,
Boolean taxRelevant,
boolean accountsAnd);
Iterable<SearchTransactionsResponseDto> searchTransactions(String fql);
}

View File

@@ -0,0 +1,117 @@
package de.financer.dba.impl;
import de.financer.dba.TransactionRepositoryCustom;
import de.financer.dto.Order;
import de.financer.dto.SearchTransactionsResponseDto;
import de.financer.fql.FQLLexer;
import de.financer.fql.FQLParser;
import de.financer.fql.FQLVisitorImpl;
import de.financer.model.*;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.springframework.stereotype.Repository;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.*;
import java.util.ArrayList;
import java.util.List;
@Repository
public class TransactionRepositoryCustomImpl implements TransactionRepositoryCustom {
@PersistenceContext
private EntityManager entityManager;
@Override
public Iterable<SearchTransactionsResponseDto> searchTransactions(Period period,
Account fromAccount,
Account toAccount,
Integer limit,
Order order,
Boolean taxRelevant,
boolean accountsAnd) {
final CriteriaBuilder criteriaBuilder = this.entityManager.getCriteriaBuilder();
final CriteriaQuery<SearchTransactionsResponseDto> criteriaQuery = criteriaBuilder
.createQuery(SearchTransactionsResponseDto.class);
final Root<Transaction> fromTransaction = criteriaQuery.from(Transaction.class);
final List<Predicate> predicates = new ArrayList<>();
Predicate fromAccountPredicate = null;
Predicate toAccountPredicate = null;
if (taxRelevant != null) {
predicates.add(criteriaBuilder.equal(fromTransaction.get(Transaction_.taxRelevant), taxRelevant));
}
if (fromAccount != null) {
fromAccountPredicate = criteriaBuilder.equal(fromTransaction.get(Transaction_.fromAccount), fromAccount);
}
if (toAccount != null) {
toAccountPredicate = criteriaBuilder.equal(fromTransaction.get(Transaction_.toAccount), toAccount);
}
if (period != null) {
final SetJoin<Transaction, Period> periodJoin = fromTransaction.join(Transaction_.periods);
predicates.add(criteriaBuilder.equal(periodJoin.get(Period_.id), period.getId()));
}
if (fromAccountPredicate != null && toAccountPredicate != null) {
if (accountsAnd) {
predicates.add(criteriaBuilder.and(fromAccountPredicate, toAccountPredicate));
}
else {
predicates.add(criteriaBuilder.or(fromAccountPredicate, toAccountPredicate));
}
}
else if (fromAccountPredicate != null) {
predicates.add(fromAccountPredicate);
}
else if (toAccountPredicate != null) {
predicates.add(toAccountPredicate);
}
// else: both null, nothing to do
criteriaQuery.where(predicates.toArray(new Predicate[]{}));
switch(order) {
case TRANSACTIONS_BY_DATE_DESC:
// either leave case as last before default if new cases arrive
// or copy the expression
default:
criteriaQuery.orderBy(criteriaBuilder.desc(fromTransaction.get(Transaction_.date)));
}
criteriaQuery.select(criteriaBuilder.construct(SearchTransactionsResponseDto.class,
fromTransaction.get(Transaction_.id),
fromTransaction.get(Transaction_.fromAccount),
fromTransaction.get(Transaction_.toAccount),
fromTransaction.get(Transaction_.date),
fromTransaction.get(Transaction_.description),
fromTransaction.get(Transaction_.amount),
fromTransaction.get(Transaction_.taxRelevant),
criteriaBuilder.isNotNull(fromTransaction.get(Transaction_.recurringTransaction))));
final TypedQuery<SearchTransactionsResponseDto> query = this.entityManager.createQuery(criteriaQuery);
if (limit != null) {
query.setMaxResults(limit);
}
return query.getResultList();
}
@Override
public Iterable<SearchTransactionsResponseDto> searchTransactions(String fql) {
final FQLLexer lexer = new FQLLexer(CharStreams.fromString(fql));
final FQLParser parser = new FQLParser(new CommonTokenStream(lexer));
final CriteriaBuilder criteriaBuilder = this.entityManager.getCriteriaBuilder();
final FQLVisitorImpl visitor = new FQLVisitorImpl(criteriaBuilder);
visitor.visitQuery(parser.query());
return this.entityManager.createQuery(visitor.getCriteriaQuery()).getResultList();
}
}

View File

@@ -0,0 +1,230 @@
package de.financer.fql;
import de.financer.dto.SearchTransactionsResponseDto;
import de.financer.fql.field_handler.BetweenIntHandler;
import de.financer.fql.field_handler.BetweenStringHandler;
import de.financer.fql.field_handler.PeriodConstHandler;
import de.financer.fql.join_handler.JoinKey;
import de.financer.fql.field_handler.IntHandler;
import de.financer.model.*;
import org.antlr.v4.runtime.tree.ParseTree;
import org.apache.commons.lang3.StringUtils;
import javax.persistence.criteria.*;
import java.util.*;
public class FQLVisitorImpl extends FQLBaseVisitor<Predicate> {
private final CriteriaBuilder criteriaBuilder;
private final CriteriaQuery<SearchTransactionsResponseDto> criteriaQuery;
private final Root<Transaction> criteriaRoot;
private final Map<JoinKey, From<?, ?>> froms;
public FQLVisitorImpl(CriteriaBuilder criteriaBuilder) {
this.criteriaBuilder = criteriaBuilder;
this.criteriaQuery = criteriaBuilder.createQuery(SearchTransactionsResponseDto.class);
this.criteriaRoot = criteriaQuery.from(Transaction.class);
this.froms = new HashMap<>();
this.froms.put(JoinKey.of(Transaction.class), this.criteriaRoot);
}
public CriteriaQuery<SearchTransactionsResponseDto> getCriteriaQuery() {
return this.criteriaQuery;
}
@Override
public Predicate visitQuery(FQLParser.QueryContext ctx) {
buildSelect();
final Predicate predicateTree = ctx.getChild(0).accept(this);
// ORDER BY
if (ctx.getChildCount() > 1) {
ctx.getChild(1).accept(this);
}
this.criteriaQuery.where(predicateTree);
return predicateTree;
}
private void buildSelect() {
criteriaQuery.select(criteriaBuilder.construct(SearchTransactionsResponseDto.class,
this.criteriaRoot.get(Transaction_.id),
this.criteriaRoot.get(Transaction_.fromAccount),
this.criteriaRoot.get(Transaction_.toAccount),
this.criteriaRoot.get(Transaction_.date),
this.criteriaRoot.get(Transaction_.description),
this.criteriaRoot.get(Transaction_.amount),
this.criteriaRoot.get(Transaction_.taxRelevant),
criteriaBuilder.isNotNull(this.criteriaRoot.get(Transaction_.recurringTransaction))));
}
@Override
public Predicate visitOrExpression(FQLParser.OrExpressionContext ctx) {
if (ctx.getChildCount() == 1) {
return ctx.getChild(0).accept(this);
} else {
final List<Predicate> predicates = new ArrayList<>();
for (int i = 0; i < ctx.getChildCount(); i++) {
ParseTree c = ctx.getChild(i);
Predicate p = c.accept(this);
if (p != null) {
predicates.add(p);
}
}
return this.criteriaBuilder.or(predicates.toArray(new Predicate[]{}));
}
}
@Override
public Predicate visitAndExpression(FQLParser.AndExpressionContext ctx) {
if (ctx.getChildCount() == 1) {
return ctx.getChild(0).accept(this);
} else {
final List<Predicate> predicates = new ArrayList<>();
for (int i = 0; i < ctx.getChildCount(); i++) {
ParseTree c = ctx.getChild(i);
Predicate p = c.accept(this);
if (p != null) {
predicates.add(p);
}
}
return this.criteriaBuilder.and(predicates.toArray(new Predicate[]{}));
}
}
@Override
public Predicate visitExpression(FQLParser.ExpressionContext ctx) {
return ctx.getChild(0).accept(this);
}
@Override
public Predicate visitParenthesisExpression(FQLParser.ParenthesisExpressionContext ctx) {
return ctx.expr.accept(this);
}
@Override
public Predicate visitRegularExpression(FQLParser.RegularExpressionContext ctx) {
return ctx.getChild(0).accept(this);
}
@Override
public Predicate visitBooleanExpression(FQLParser.BooleanExpressionContext ctx) {
final FieldMapping fieldMapping = FieldMapping.findByFieldName(ctx.field.getText());
final Boolean value = Boolean.parseBoolean(ctx.value.getText());
fieldMapping.getJoinHandler().apply(this.froms, fieldMapping);
return fieldMapping.getFieldHandler().apply(fieldMapping, this.froms, this.criteriaBuilder, value);
}
@Override
public Predicate visitStringExpression(FQLParser.StringExpressionContext ctx) {
final FieldMapping fieldMapping = FieldMapping.findByFieldName(ctx.field.getText());
fieldMapping.getJoinHandler().apply(this.froms, fieldMapping);
return fieldMapping.getFieldHandler()
.apply(fieldMapping, this.froms, this.criteriaBuilder, ctx.value.getText());
}
@Override
public Predicate visitIntExpression(FQLParser.IntExpressionContext ctx) {
final FieldMapping fieldMapping = FieldMapping.findByFieldName(ctx.field.getText());
final IntHandler.IntHandlerParameterContainer con = IntHandler.IntHandlerParameterContainer
.of(ctx.operator.getText(), ctx.value.getText());
fieldMapping.getJoinHandler().apply(this.froms, fieldMapping);
return fieldMapping.getFieldHandler().apply(fieldMapping, this.froms, this.criteriaBuilder, con);
}
@Override
public Predicate visitDateExpression(FQLParser.DateExpressionContext ctx) {
final FieldMapping fieldMapping = FieldMapping.findByFieldName(ctx.field.getText());
fieldMapping.getJoinHandler().apply(this.froms, fieldMapping);
return fieldMapping.getFieldHandler()
.apply(fieldMapping, this.froms, this.criteriaBuilder, ctx.value.getText());
}
@Override
public Predicate visitBetweenExpression(FQLParser.BetweenExpressionContext ctx) {
return ctx.getChild(0).accept(this);
}
@Override
public Predicate visitBetweenStringExpression(FQLParser.BetweenStringExpressionContext ctx) {
final FieldMapping fieldMapping = FieldMapping.findByFieldName(ctx.field.getText());
final BetweenStringHandler.BetweenStringHandlerParameterContainer con = BetweenStringHandler.BetweenStringHandlerParameterContainer
.of(ctx.left.getText(), ctx.right.getText());
fieldMapping.getJoinHandler().apply(this.froms, fieldMapping);
// Overwrite the actual field handler of the field
return new BetweenStringHandler().apply(fieldMapping, this.froms, this.criteriaBuilder, con);
}
@Override
public Predicate visitBetweenIntExpression(FQLParser.BetweenIntExpressionContext ctx) {
final FieldMapping fieldMapping = FieldMapping.findByFieldName(ctx.field.getText());
final BetweenIntHandler.BetweenIntHandlerParameterContainer con = BetweenIntHandler.BetweenIntHandlerParameterContainer
.of(ctx.left.getText(), ctx.right.getText());
fieldMapping.getJoinHandler().apply(this.froms, fieldMapping);
// Overwrite the actual field handler of the field
return new BetweenIntHandler().apply(fieldMapping, this.froms, this.criteriaBuilder, con);
}
@Override
public Predicate visitPeriodExpression(FQLParser.PeriodExpressionContext ctx) {
return ctx.getChild(0).accept(this);
}
@Override
public Predicate visitPeriodConstExpression(FQLParser.PeriodConstExpressionContext ctx) {
final FieldMapping fieldMapping = FieldMapping.findByFieldName(ctx.field.getText());
final PeriodConstHandler.PeriodConstHandlerParameterContainer con = PeriodConstHandler.PeriodConstHandlerParameterContainer
.of(ctx.value.getText(), this.criteriaQuery);
fieldMapping.getJoinHandler().apply(this.froms, fieldMapping);
return fieldMapping.getFieldHandler().apply(fieldMapping, this.froms, this.criteriaBuilder, con);
}
@Override
public Predicate visitOrderByExpression(FQLParser.OrderByExpressionContext ctx) {
final FieldMapping fieldMapping = FieldMapping.findByFieldName(ctx.field.getText());
final String orderValue = StringUtils.upperCase(ctx.order.getText());
Order order;
fieldMapping.getJoinHandler().apply(this.froms, fieldMapping);
switch (orderValue) {
case "DESC":
order = this.criteriaBuilder
.desc(this.froms.get(fieldMapping.getJoinKey()).get(fieldMapping.getAttributeName()));
break;
case "ASC":
order = this.criteriaBuilder
.asc(this.froms.get(fieldMapping.getJoinKey()).get(fieldMapping.getAttributeName()));
break;
default:
throw new IllegalArgumentException(String.format("Unknown order value: %s", ctx.order.getText()));
}
this.criteriaQuery.orderBy(order);
return null;
}
}

View File

@@ -0,0 +1,119 @@
package de.financer.fql;
import de.financer.fql.field_handler.*;
import de.financer.fql.join_handler.*;
import de.financer.model.*;
import java.util.Arrays;
public enum FieldMapping {
AMOUNT("amount", Transaction_.AMOUNT, Transaction.class, NoopJoinHandler.class, JoinKey
.of(Transaction.class), null, IntHandler.class),
PERIOD("period", Period_.ID, Period.class, PeriodJoinHandler.class, JoinKey.of(Period.class), null, PeriodConstHandler.class),
FROM_ACCOUNT("fromAccount", Account_.KEY, Account.class, FromAccountJoinHandler.class, JoinKey
.of(Account.class, "FROM"), null, StringHandler.class),
TO_ACCOUNT("toAccount", Account_.KEY, Account.class, ToAccountJoinHandler.class, JoinKey
.of(Account.class, "TO"), null, StringHandler.class),
FROM_ACCOUNT_GROUP("fromAccountGroup", AccountGroup_.NAME, AccountGroup.class,
AccountGroupJoinHandler.class, JoinKey.of(AccountGroup.class, "FROM"), FROM_ACCOUNT, StringHandler.class),
TO_ACCOUNT_GROUP("toAccountGroup", AccountGroup_.NAME, AccountGroup.class,
AccountGroupJoinHandler.class, JoinKey.of(AccountGroup.class, "TO"), TO_ACCOUNT, StringHandler.class),
DATE("date", Transaction_.DATE, Transaction.class, NoopJoinHandler.class, JoinKey.of(Transaction.class), null, DateHandler.class),
RECURRING("recurring", Transaction_.RECURRING_TRANSACTION, Transaction.class, NoopJoinHandler.class,
JoinKey.of(Transaction.class), null, NotNullSyntheticHandler.class),
TAX_RELEVANT("taxRelevant", Transaction_.TAX_RELEVANT, Transaction.class, NoopJoinHandler.class,
JoinKey.of(Transaction.class), null, BooleanHandler.class);
/**
* The name of the field as used in FQL
*/
private final String fieldName;
/**
* The name of the ORM field
*/
private final String attributeName;
/**
* The entity this field belongs to
*/
private final Class<?> entityClass;
/**
* The join handler responsible for constructing the join if the field is not part of the root entity
*/
private final Class<? extends JoinHandler> joinHandlerClass;
/**
* A synthetic key to uniquely identify the join required for this field
*/
private final JoinKey joinKey;
/**
* The field mapping this field depends on if the entity is not directly reachable from the root entity
*/
private final FieldMapping dependingJoin;
/**
* The handler for this FQL field handling the translation to a JPA criteria
*/
private final Class<? extends FieldHandler> fieldHandlerClass;
FieldMapping(String fieldName, String attributeName, Class<?> entityClass,
Class<? extends JoinHandler> joinHandlerClass, JoinKey joinKey,
FieldMapping dependingJoin, Class<? extends FieldHandler> fieldHandlerClass
) {
this.fieldName = fieldName;
this.attributeName = attributeName;
this.entityClass = entityClass;
this.joinHandlerClass = joinHandlerClass;
this.joinKey = joinKey;
this.dependingJoin = dependingJoin;
this.fieldHandlerClass = fieldHandlerClass;
}
public static FieldMapping findByFieldName(String fqlFieldName) {
return Arrays.stream(values()).filter(fm -> fm.fieldName.equalsIgnoreCase(fqlFieldName)).findFirst()
.orElseThrow(() -> new IllegalArgumentException("Unknown field: " + fqlFieldName));
}
public String getAttributeName() {
return attributeName;
}
public Class<?> getEntityClass() {
return entityClass;
}
public JoinHandler getJoinHandler() {
try {
return this.joinHandlerClass.getDeclaredConstructor().newInstance();
} catch (ReflectiveOperationException e) {
throw new IllegalStateException("No public, parameterless constructor found!");
}
}
public FieldHandler getFieldHandler() {
try {
return this.fieldHandlerClass.getDeclaredConstructor().newInstance();
} catch (ReflectiveOperationException e) {
throw new IllegalStateException("No public, parameterless constructor found!");
}
}
public JoinKey getJoinKey() {
return this.joinKey;
}
public FieldMapping getDependingJoin() {
return this.dependingJoin;
}
}

View File

@@ -0,0 +1,32 @@
package de.financer.fql.field_handler;
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.util.Map;
public class BetweenIntHandler implements FieldHandler<BetweenIntHandler.BetweenIntHandlerParameterContainer> {
public static class BetweenIntHandlerParameterContainer {
private Long left;
private Long right;
public static BetweenIntHandlerParameterContainer of(String left, String right) {
final BetweenIntHandlerParameterContainer con = new BetweenIntHandlerParameterContainer();
con.left = Long.valueOf(left);
con.right = Long.valueOf(right);
return con;
}
}
@Override
public Predicate apply(FieldMapping fieldMapping, Map<JoinKey, From<?, ?>> froms, CriteriaBuilder criteriaBuilder, BetweenIntHandlerParameterContainer con) {
return criteriaBuilder
.between(froms.get(fieldMapping.getJoinKey()).get(fieldMapping.getAttributeName()), con.left, con.right);
}
}

View File

@@ -0,0 +1,38 @@
package de.financer.fql.field_handler;
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.util.Map;
public class BetweenStringHandler implements FieldHandler<BetweenStringHandler.BetweenStringHandlerParameterContainer> {
public static class BetweenStringHandlerParameterContainer {
private String left;
private String right;
public static BetweenStringHandlerParameterContainer of(String left, String right) {
final BetweenStringHandlerParameterContainer con = new BetweenStringHandlerParameterContainer();
con.left = left;
con.right = right;
return con;
}
}
@Override
public Predicate apply(FieldMapping fieldMapping, Map<JoinKey, From<?, ?>> froms, CriteriaBuilder criteriaBuilder, BetweenStringHandlerParameterContainer con) {
return criteriaBuilder
.between(froms.get(fieldMapping.getJoinKey()).get(fieldMapping.getAttributeName()),
removeQuotes(con.left),
removeQuotes(con.right));
}
private String removeQuotes(String value) {
return value.replaceAll("'", "");
}
}

View File

@@ -0,0 +1,16 @@
package de.financer.fql.field_handler;
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.util.Map;
public class BooleanHandler implements FieldHandler<Boolean> {
@Override
public Predicate apply(FieldMapping fieldMapping, Map<JoinKey, From<?, ?>> froms, CriteriaBuilder criteriaBuilder, Boolean value) {
return criteriaBuilder.equal(froms.get(fieldMapping.getJoinKey()).get(fieldMapping.getAttributeName()), value);
}
}

View File

@@ -0,0 +1,21 @@
package de.financer.fql.field_handler;
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.time.LocalDate;
import java.util.Map;
public class DateHandler 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()), parseDate(value));
}
private LocalDate parseDate(String value) {
return LocalDate.parse(value);
}
}

View File

@@ -0,0 +1,13 @@
package de.financer.fql.field_handler;
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.util.Map;
public interface FieldHandler<T> {
Predicate apply(FieldMapping fieldMapping, Map<JoinKey, From<?, ?>> froms, CriteriaBuilder criteriaBuilder, T value);
}

View File

@@ -0,0 +1,55 @@
package de.financer.fql.field_handler;
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.Path;
import javax.persistence.criteria.Predicate;
import java.util.Map;
public class IntHandler implements FieldHandler<IntHandler.IntHandlerParameterContainer> {
public static class IntHandlerParameterContainer {
private String operator;
private Long value;
public static IntHandlerParameterContainer of(String operator, String value) {
IntHandlerParameterContainer con = new IntHandlerParameterContainer();
con.operator = operator;
con.value = Long.valueOf(value);
return con;
}
}
@Override
public Predicate apply(FieldMapping fieldMapping, Map<JoinKey, From<?, ?>> froms, CriteriaBuilder criteriaBuilder, IntHandlerParameterContainer con) {
final Path<Long> path = froms.get(fieldMapping.getJoinKey()).get(fieldMapping.getAttributeName());
Predicate retVal = null;
switch (con.operator) {
case "=":
retVal = criteriaBuilder.equal(path, con.value);
break;
case ">":
retVal = criteriaBuilder.gt(path, con.value);
break;
case "<":
retVal = criteriaBuilder.lt(path, con.value);
break;
case ">=":
retVal = criteriaBuilder.greaterThanOrEqualTo(path, con.value);
break;
case "<=":
retVal = criteriaBuilder.lessThanOrEqualTo(path, con.value);
break;
case "!=":
retVal = criteriaBuilder.notEqual(path, con.value);
break;
}
return retVal;
}
}

View File

@@ -0,0 +1,20 @@
package de.financer.fql.field_handler;
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.util.Map;
public class NotNullSyntheticHandler implements FieldHandler<Boolean> {
@Override
public Predicate apply(FieldMapping fieldMapping, Map<JoinKey, From<?, ?>> froms, CriteriaBuilder criteriaBuilder, Boolean value) {
if (value) {
return criteriaBuilder.isNotNull(froms.get(fieldMapping.getJoinKey()).get(fieldMapping.getAttributeName()));
} else {
return criteriaBuilder.isNull(froms.get(fieldMapping.getJoinKey()).get(fieldMapping.getAttributeName()));
}
}
}

View File

@@ -0,0 +1,73 @@
package de.financer.fql.field_handler;
import de.financer.dto.SearchTransactionsResponseDto;
import de.financer.fql.FieldMapping;
import de.financer.fql.join_handler.JoinKey;
import de.financer.model.Period;
import de.financer.model.PeriodType;
import de.financer.model.Period_;
import org.apache.commons.lang3.StringUtils;
import javax.persistence.criteria.*;
import java.util.Map;
public class PeriodConstHandler implements FieldHandler<PeriodConstHandler.PeriodConstHandlerParameterContainer> {
public static class PeriodConstHandlerParameterContainer {
private String value;
private CriteriaQuery<SearchTransactionsResponseDto> criteriaQuery;
public static PeriodConstHandlerParameterContainer of(String value, CriteriaQuery<SearchTransactionsResponseDto> criteriaQuery) {
final PeriodConstHandlerParameterContainer con = new PeriodConstHandlerParameterContainer();
con.value = value;
con.criteriaQuery = criteriaQuery;
return con;
}
}
@Override
public Predicate apply(FieldMapping fieldMapping, Map<JoinKey, From<?, ?>> froms, CriteriaBuilder criteriaBuilder, PeriodConstHandlerParameterContainer con) {
final Subquery<Long> subquery = con.criteriaQuery.subquery(Long.class);
final Root<Period> subFrom = subquery.from(Period.class);
final String upperValue = StringUtils.upperCase(con.value);
subquery.select(subFrom.get(fieldMapping.getAttributeName()));
// Generate subselect for each period value
switch (upperValue) {
case "CURRENT":
subquery.where(criteriaBuilder.and(criteriaBuilder
.equal(subFrom.get(Period_.TYPE), PeriodType.EXPENSE), criteriaBuilder
.isNull(subFrom.get(Period_.END))));
break;
case "LAST":
subquery.where(criteriaBuilder.and(criteriaBuilder
.equal(subFrom.get(Period_.TYPE), PeriodType.EXPENSE), criteriaBuilder
.isNotNull(subFrom.get(Period_.END))));
// subquery.orderBy -> orderBy not supported by JPA criteria
// So we need to workaround this by misusing the autoincremented ID
subquery.select(criteriaBuilder.max(subFrom.get(fieldMapping.getAttributeName())));
break;
case "CURRENT_YEAR":
subquery.where(criteriaBuilder.and(criteriaBuilder
.equal(subFrom.get(Period_.TYPE), PeriodType.EXPENSE_YEAR), criteriaBuilder
.isNull(subFrom.get(Period_.END))));
break;
case "LAST_YEAR":
subquery.where(criteriaBuilder.and(criteriaBuilder
.equal(subFrom.get(Period_.TYPE), PeriodType.EXPENSE_YEAR), criteriaBuilder
.isNotNull(subFrom.get(Period_.END))));
subquery.select(criteriaBuilder.max(subFrom.get(fieldMapping.getAttributeName())));
break;
case "GRAND_TOTAL":
subquery.where(criteriaBuilder.equal(subFrom.get(Period_.TYPE), PeriodType.GRAND_TOTAL));
break;
}
return criteriaBuilder.equal(froms.get(fieldMapping.getJoinKey())
.get(fieldMapping.getAttributeName()), subquery);
}
}

View File

@@ -0,0 +1,21 @@
package de.financer.fql.field_handler;
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.util.Map;
public class StringHandler 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()), removeQuotes(value));
}
private String removeQuotes(String value) {
return value.replaceAll("'", "");
}
}

View File

@@ -0,0 +1,32 @@
package de.financer.fql.join_handler;
import de.financer.fql.FieldMapping;
import de.financer.model.Account;
import de.financer.model.AccountGroup;
import de.financer.model.Account_;
import de.financer.model.Transaction;
import javax.persistence.criteria.From;
import javax.persistence.criteria.Join;
import java.util.Map;
public class AccountGroupJoinHandler implements JoinHandler {
@Override
public Void apply(Map<JoinKey, From<?, ?>> joinKeyFromMap, FieldMapping fieldMapping) {
final FieldMapping dependingJoin = fieldMapping.getDependingJoin();
if (dependingJoin != null) {
dependingJoin.getJoinHandler().apply(joinKeyFromMap, dependingJoin);
}
if (joinKeyFromMap.get(fieldMapping.getJoinKey()) == null) {
final Join<Account, AccountGroup> accountGroupJoin = ((Join<Transaction, Account>) joinKeyFromMap
.get(dependingJoin.getJoinKey()))
.join(Account_.accountGroup);
joinKeyFromMap.put(fieldMapping.getJoinKey(), accountGroupJoin);
}
return null;
}
}

View File

@@ -0,0 +1,26 @@
package de.financer.fql.join_handler;
import de.financer.fql.FieldMapping;
import de.financer.model.Account;
import de.financer.model.Transaction;
import de.financer.model.Transaction_;
import javax.persistence.criteria.From;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.Root;
import java.util.Map;
public class FromAccountJoinHandler implements JoinHandler {
@Override
public Void apply(Map<JoinKey, From<?, ?>> joinKeyFromMap, FieldMapping fieldMapping) {
if (joinKeyFromMap.get(fieldMapping.getJoinKey()) == null) {
final Join<Transaction, Account> accountJoin = ((Root<Transaction>) joinKeyFromMap
.get(JoinKey.of(Transaction.class)))
.join(Transaction_.fromAccount);
joinKeyFromMap.put(fieldMapping.getJoinKey(), accountJoin);
}
return null;
}
}

View File

@@ -0,0 +1,10 @@
package de.financer.fql.join_handler;
import de.financer.fql.FieldMapping;
import javax.persistence.criteria.From;
import java.util.Map;
import java.util.function.BiFunction;
public interface JoinHandler extends BiFunction<Map<JoinKey, From<?, ?>>, FieldMapping, Void> {
}

View File

@@ -0,0 +1,36 @@
package de.financer.fql.join_handler;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
public class JoinKey {
private Class<?> clazz;
private String key;
public static JoinKey of(Class<?> clazz, String key) {
JoinKey jk = new JoinKey();
jk.clazz = clazz;
jk.key = key;
return jk;
}
public static JoinKey of(Class<?> clazz) {
JoinKey jk = new JoinKey();
jk.clazz = clazz;
return jk;
}
@Override
public boolean equals(Object obj) {
return EqualsBuilder.reflectionEquals(this, obj, false);
}
@Override
public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this, false);
}
}

View File

@@ -0,0 +1,13 @@
package de.financer.fql.join_handler;
import de.financer.fql.FieldMapping;
import javax.persistence.criteria.From;
import java.util.Map;
public class NoopJoinHandler implements JoinHandler {
@Override
public Void apply(Map<JoinKey, From<?, ?>> joinKeyFromMap, FieldMapping fieldMapping) {
return null;
}
}

View File

@@ -0,0 +1,28 @@
package de.financer.fql.join_handler;
import de.financer.fql.FieldMapping;
import de.financer.model.Period;
import de.financer.model.Transaction;
import de.financer.model.Transaction_;
import javax.persistence.criteria.From;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.SetJoin;
import java.util.Map;
public class PeriodJoinHandler implements JoinHandler {
@Override
public Void apply(Map<JoinKey, From<?, ?>> joinKeyFromMap, FieldMapping fieldMapping) {
if (joinKeyFromMap.get(fieldMapping.getJoinKey()) == null) {
final SetJoin<Transaction, Period> periodJoin = ((Root<Transaction>) joinKeyFromMap
.get(JoinKey.of(Transaction.class)))
.join(Transaction_.periods);
joinKeyFromMap.put(fieldMapping.getJoinKey(), periodJoin);
}
return null;
}
}

View File

@@ -0,0 +1,26 @@
package de.financer.fql.join_handler;
import de.financer.fql.FieldMapping;
import de.financer.model.Account;
import de.financer.model.Transaction;
import de.financer.model.Transaction_;
import javax.persistence.criteria.From;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.Root;
import java.util.Map;
public class ToAccountJoinHandler implements JoinHandler {
@Override
public Void apply(Map<JoinKey, From<?, ?>> joinKeyFromMap, FieldMapping fieldMapping) {
if (joinKeyFromMap.get(fieldMapping.getJoinKey()) == null) {
final Join<Transaction, Account> accountJoin = ((Root<Transaction>) joinKeyFromMap
.get(JoinKey.of(Transaction.class)))
.join(Transaction_.toAccount);
joinKeyFromMap.put(fieldMapping.getJoinKey(), accountJoin);
}
return null;
}
}

View File

@@ -43,6 +43,7 @@ public class AccountService {
* @return the account or <code>null</code> if no account with the given key can be found
*/
public Account getAccountByKey(String key) {
// TODO change return type of repository to Optional<Account>
return this.accountRepository.findByKey(key);
}
@@ -102,7 +103,7 @@ public class AccountService {
// If we create an account it's implicitly open
account.setStatus(AccountStatus.OPEN);
// and has a current balance of 0
account.setCurrentBalance(Long.valueOf(0L));
account.setCurrentBalance(0L);
try {
this.accountRepository.save(account);

View File

@@ -12,10 +12,9 @@ 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;
import java.util.Optional;
@Service
public class PeriodService {
@@ -132,4 +131,8 @@ public class PeriodService {
return period;
}
public Optional<Period> getPeriodById(Long id) {
return this.periodRepository.findById(id);
}
}

View File

@@ -4,7 +4,12 @@ import de.financer.ResponseReason;
import de.financer.config.FinancerConfig;
import de.financer.dba.TransactionRepository;
import de.financer.dto.ExpensePeriodTotal;
import de.financer.dto.Order;
import de.financer.dto.SearchTransactionsResponseDto;
import de.financer.model.*;
import de.financer.service.exception.FinancerServiceException;
import de.financer.service.parameter.SearchTransactionsParameter;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.slf4j.Logger;
@@ -48,25 +53,6 @@ public class TransactionService {
return this.transactionRepository.findAll();
}
/**
* @param accountKey the key of the account to get the transactions for
*
* @return all transactions for the given account, for all time. Returns an empty list if the given key does not
* match any account.
*/
public Iterable<Transaction> getAllForAccount(String accountKey) {
final Account account = this.accountService.getAccountByKey(accountKey);
if (account == null) {
LOGGER.warn(String.format("Account with key %s not found!", accountKey));
return Collections.emptyList();
}
// As we want all transactions of the given account use it as from and to account
return this.transactionRepository.findTransactionsByFromAccountOrToAccount(account, account);
}
@Transactional(propagation = Propagation.REQUIRED)
public ResponseReason createTransaction(String fromAccountKey, String toAccountKey, Long amount, String date,
String description, Boolean taxRelevant
@@ -114,7 +100,7 @@ public class TransactionService {
this.accountService.saveAccount(fromAccount);
this.accountService.saveAccount(toAccount);
response = ResponseReason.OK;
response = ResponseReason.CREATED;
} catch (Exception e) {
LOGGER.error("Could not create transaction!", e);
@@ -261,7 +247,7 @@ public class TransactionService {
final Long expensesCurrentPeriod = this.transactionRepository
.getExpensesForPeriod(expensePeriod, AccountType.EXPENSE, AccountType.LIABILITY);
return Optional.ofNullable(expensesCurrentPeriod).orElse(Long.valueOf(0l));
return Optional.ofNullable(expensesCurrentPeriod).orElse(0L);
}
/**
@@ -290,4 +276,93 @@ public class TransactionService {
return this.transactionRepository
.getAccountExpenseTotals(periods, AccountType.INCOME, AccountType.START, AccountType.EXPENSE, AccountType.LIABILITY);
}
/**
* This method searches for transactions as specified with the given search parameters. If no parameters at all are
* given the search returns the top 500 transactions for all accounts ordered by the transaction date descending.
*
* @param parameter the encapsulated search parameters
* @return the list of transactions matching the given parameters or the top 500 transactions. Never <code>null</code>
*/
public Iterable<SearchTransactionsResponseDto> searchTransactions(SearchTransactionsParameter parameter) {
// Check that at least one bounding parameter is set so the resulting list won't be too large
if (StringUtils.isEmpty(parameter.getPeriodId()) && StringUtils.isEmpty(parameter.getLimit())) {
// 500 is pretty conservative, could probably be increased if required to at least 1000
parameter.setLimit("500");
}
final Order order;
Period period = null;
Integer limit = null;
Account fromAccount = null;
Account toAccount = null;
Boolean taxRelevant = null;
Boolean accountsAnd = Boolean.FALSE; // account values are OR conjunct by default
if (StringUtils.isNotEmpty(parameter.getPeriodId())) {
if (!NumberUtils.isCreatable(parameter.getPeriodId())) {
throw new FinancerServiceException(ResponseReason.PERIOD_ID_NOT_NUMERIC);
}
final Optional<Period> optionalPeriod = this.periodService.getPeriodById(Long.valueOf(parameter.getPeriodId()));
period = optionalPeriod.orElseThrow(() -> new FinancerServiceException(ResponseReason.PERIOD_NOT_FOUND));
}
if (StringUtils.isNotEmpty(parameter.getLimit())) {
if (!NumberUtils.isCreatable(parameter.getLimit())) {
throw new FinancerServiceException(ResponseReason.LIMIT_NOT_NUMERIC);
}
limit = Integer.valueOf(parameter.getLimit());
}
order = Order.getByName(parameter.getOrder()).orElse(Order.TRANSACTIONS_BY_DATE_DESC);
if (StringUtils.isNotEmpty(parameter.getFromAccountKey())) {
fromAccount = this.accountService.getAccountByKey(parameter.getFromAccountKey());
if (fromAccount == null) {
throw new FinancerServiceException(ResponseReason.FROM_ACCOUNT_NOT_FOUND);
}
}
if (StringUtils.isNotEmpty(parameter.getToAccountKey())) {
toAccount = this.accountService.getAccountByKey(parameter.getToAccountKey());
if (toAccount == null) {
throw new FinancerServiceException(ResponseReason.TO_ACCOUNT_NOT_FOUND);
}
}
if (StringUtils.isNotEmpty(parameter.getTaxRelevant())) {
taxRelevant = BooleanUtils.toBooleanObject(parameter.getTaxRelevant());
if (taxRelevant == null) {
throw new FinancerServiceException(ResponseReason.INVALID_TAX_RELEVANT_VALUE);
}
}
if (StringUtils.isNotEmpty(parameter.getAccountsAnd())) {
accountsAnd = BooleanUtils.toBooleanObject(parameter.getAccountsAnd());
if (accountsAnd == null) {
throw new FinancerServiceException(ResponseReason.INVALID_ACCOUNTS_AND_VALUE);
}
}
return this.transactionRepository.searchTransactions(period, fromAccount, toAccount, limit, order, taxRelevant,
accountsAnd);
}
public Iterable<SearchTransactionsResponseDto> searchTransactions(String fql) {
try {
return this.transactionRepository.searchTransactions(fql);
}
catch(IllegalArgumentException iae) {
LOGGER.error("Error while parsing FQL", iae);
throw new FinancerServiceException(ResponseReason.FQL_MALFORMED);
}
}
}

View File

@@ -0,0 +1,15 @@
package de.financer.service.exception;
import de.financer.ResponseReason;
public class FinancerServiceException extends RuntimeException {
private final ResponseReason responseReason;
public FinancerServiceException(ResponseReason responseReason) {
this.responseReason = responseReason;
}
public ResponseReason getResponseReason() {
return this.responseReason;
}
}

View File

@@ -0,0 +1,17 @@
package de.financer.service.exception;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@ControllerAdvice
public class RestControllerExceptionAdvisor extends ResponseEntityExceptionHandler {
@ExceptionHandler(value = {FinancerServiceException.class})
public ResponseEntity<String> handleFinancerServiceException(FinancerServiceException ex, WebRequest request) {
return ex.getResponseReason().toResponseEntity();
}
}

View File

@@ -0,0 +1,67 @@
package de.financer.service.parameter;
public class SearchTransactionsParameter {
private String fromAccountKey;
private String toAccountKey;
private String periodId;
private String limit;
private String order;
private String taxRelevant;
private String accountsAnd;
public String getFromAccountKey() {
return fromAccountKey;
}
public void setFromAccountKey(String fromAccountKey) {
this.fromAccountKey = fromAccountKey;
}
public String getToAccountKey() {
return toAccountKey;
}
public void setToAccountKey(String toAccountKey) {
this.toAccountKey = toAccountKey;
}
public String getPeriodId() {
return periodId;
}
public void setPeriodId(String periodId) {
this.periodId = periodId;
}
public String getLimit() {
return limit;
}
public void setLimit(String limit) {
this.limit = limit;
}
public String getOrder() {
return order;
}
public void setOrder(String order) {
this.order = order;
}
public String getTaxRelevant() {
return taxRelevant;
}
public void setTaxRelevant(String taxRelevant) {
this.taxRelevant = taxRelevant;
}
public String getAccountsAnd() {
return accountsAnd;
}
public void setAccountsAnd(String accountsAnd) {
this.accountsAnd = accountsAnd;
}
}

View File

@@ -14,7 +14,6 @@ import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Locale;
import java.util.stream.Collectors;
@Component
@@ -52,7 +51,7 @@ public class SendRecurringTransactionReminderTask {
// TODO Filtering currently happens in memory but should be done via SQL
recurringTransactions = IterableUtils.toList(recurringTransactions)
.stream()
.filter((rt) -> rt.isRemind())
.filter(RecurringTransaction::isRemind)
.collect(Collectors.toList());
LOGGER.info(String
@@ -65,20 +64,18 @@ public class SendRecurringTransactionReminderTask {
.append(System.lineSeparator())
.append(System.lineSeparator());
IterableUtils.toList(recurringTransactions).forEach((rt) -> {
reminderBuilder.append(rt.getId())
.append("|")
.append(rt.getDescription())
.append(System.lineSeparator())
.append(this.getMessage("financer.recurring-transaction.email.reminder.from"))
.append(rt.getFromAccount().getKey())
.append(this.getMessage("financer.recurring-transaction.email.reminder.to"))
.append(rt.getToAccount().getKey())
.append(": ")
.append(rt.getAmount().toString())
.append(System.lineSeparator())
.append(System.lineSeparator());
});
IterableUtils.toList(recurringTransactions).forEach((rt) -> reminderBuilder.append(rt.getId())
.append("|")
.append(rt.getDescription())
.append(System.lineSeparator())
.append(this.getMessage("financer.recurring-transaction.email.reminder.from"))
.append(rt.getFromAccount().getKey())
.append(this.getMessage("financer.recurring-transaction.email.reminder.to"))
.append(rt.getToAccount().getKey())
.append(": ")
.append(rt.getAmount().toString())
.append(System.lineSeparator())
.append(System.lineSeparator()));
final SimpleMailMessage msg = new SimpleMailMessage();

View File

@@ -3,7 +3,6 @@ package de.financer.service;
import de.financer.ResponseReason;
import de.financer.dba.AccountRepository;
import de.financer.model.Account;
import de.financer.model.AccountGroup;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;

View File

@@ -46,7 +46,7 @@ public class RecurringTransactionService_createRecurringTransactionTest {
// Act
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.invalid",
"account.invalid",
Long.valueOf(150l),
150L,
"DESCRIPTION",
"HOLIDAY_WEEKEND_TYPE",
"INTERVAL_TYPE",
@@ -67,7 +67,7 @@ public class RecurringTransactionService_createRecurringTransactionTest {
// Act
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
"account.invalid",
Long.valueOf(150l),
150L,
"DESCRIPTION",
"HOLIDAY_WEEKEND_TYPE",
"INTERVAL_TYPE",
@@ -88,7 +88,7 @@ public class RecurringTransactionService_createRecurringTransactionTest {
// Act
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.invalid",
"account.to",
Long.valueOf(150l),
150L,
"DESCRIPTION",
"HOLIDAY_WEEKEND_TYPE",
"INTERVAL_TYPE",
@@ -110,7 +110,7 @@ public class RecurringTransactionService_createRecurringTransactionTest {
// Act
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
"account.to",
Long.valueOf(150l),
150L,
"DESCRIPTION",
"HOLIDAY_WEEKEND_TYPE",
"INTERVAL_TYPE",
@@ -154,7 +154,7 @@ public class RecurringTransactionService_createRecurringTransactionTest {
// Act
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
"account.to",
Long.valueOf(0l),
0L,
"DESCRIPTION",
"HOLIDAY_WEEKEND_TYPE",
"INTERVAL_TYPE",
@@ -176,7 +176,7 @@ public class RecurringTransactionService_createRecurringTransactionTest {
// Act
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
"account.to",
Long.valueOf(150l),
150L,
"DESCRIPTION",
null,
"INTERVAL_TYPE",
@@ -198,7 +198,7 @@ public class RecurringTransactionService_createRecurringTransactionTest {
// Act
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
"account.to",
Long.valueOf(150l),
150L,
"DESCRIPTION",
"HOLIDAY_WEEKEND_TYPE",
"INTERVAL_TYPE",
@@ -220,7 +220,7 @@ public class RecurringTransactionService_createRecurringTransactionTest {
// Act
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
"account.to",
Long.valueOf(150l),
150L,
"DESCRIPTION",
HolidayWeekendType.SAME_DAY.name(),
null,
@@ -242,7 +242,7 @@ public class RecurringTransactionService_createRecurringTransactionTest {
// Act
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
"account.to",
Long.valueOf(150l),
150L,
"DESCRIPTION",
HolidayWeekendType.SAME_DAY.name(),
"INTERVAL_TYPE",
@@ -264,7 +264,7 @@ public class RecurringTransactionService_createRecurringTransactionTest {
// Act
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
"account.to",
Long.valueOf(150l),
150L,
"DESCRIPTION",
HolidayWeekendType.SAME_DAY.name(),
IntervalType.DAILY.name(),
@@ -286,7 +286,7 @@ public class RecurringTransactionService_createRecurringTransactionTest {
// Act
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
"account.to",
Long.valueOf(150l),
150L,
"DESCRIPTION",
HolidayWeekendType.SAME_DAY.name(),
IntervalType.DAILY.name(),
@@ -308,7 +308,7 @@ public class RecurringTransactionService_createRecurringTransactionTest {
// Act
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
"account.to",
Long.valueOf(150l),
150L,
"DESCRIPTION",
HolidayWeekendType.SAME_DAY.name(),
IntervalType.DAILY.name(),
@@ -331,7 +331,7 @@ public class RecurringTransactionService_createRecurringTransactionTest {
// Act
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
"account.to",
Long.valueOf(150l),
150L,
"DESCRIPTION",
HolidayWeekendType.SAME_DAY.name(),
IntervalType.DAILY.name(),
@@ -353,7 +353,7 @@ public class RecurringTransactionService_createRecurringTransactionTest {
// Act
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
"account.to",
Long.valueOf(150l),
150L,
"DESCRIPTION",
HolidayWeekendType.SAME_DAY.name(),
IntervalType.DAILY.name(),
@@ -369,7 +369,7 @@ public class RecurringTransactionService_createRecurringTransactionTest {
private Account createAccount() {
final Account account = new Account();
account.setCurrentBalance(Long.valueOf(0l));
account.setCurrentBalance(0L);
return account;
}

View File

@@ -48,7 +48,7 @@ public class TransactionService_createTransactionTest {
// will not be found.
// Act
final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.invalid", Long.valueOf(150l), "24.02.2019", "XXX", false);
final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.invalid", 150L, "24.02.2019", "XXX", false);
// Assert
Assert.assertEquals(ResponseReason.FROM_AND_TO_ACCOUNT_NOT_FOUND, response);
@@ -60,7 +60,7 @@ public class TransactionService_createTransactionTest {
Mockito.when(this.accountService.getAccountByKey(Mockito.anyString())).thenReturn(createAccount(), null);
// Act
final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.invalid", Long.valueOf(150l), "24.02.2019", "XXX", false);
final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.invalid", 150L, "24.02.2019", "XXX", false);
// Assert
Assert.assertEquals(ResponseReason.TO_ACCOUNT_NOT_FOUND, response);
@@ -72,7 +72,7 @@ public class TransactionService_createTransactionTest {
Mockito.when(this.accountService.getAccountByKey(Mockito.anyString())).thenReturn(null, createAccount());
// Act
final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.to", Long.valueOf(150l), "24.02.2019", "XXX", false);
final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.to", 150L, "24.02.2019", "XXX", false);
// Assert
Assert.assertEquals(ResponseReason.FROM_ACCOUNT_NOT_FOUND, response);
@@ -85,7 +85,7 @@ public class TransactionService_createTransactionTest {
Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.FALSE);
// Act
final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", Long.valueOf(150l), "24.02.2019", "XXX", false);
final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", 150L, "24.02.2019", "XXX", false);
// Assert
Assert.assertEquals(ResponseReason.INVALID_BOOKING_ACCOUNTS, response);
@@ -111,7 +111,7 @@ public class TransactionService_createTransactionTest {
Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.TRUE);
// Act
final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", Long.valueOf(0l), "24.02.2019", "XXX", false);
final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", 0L, "24.02.2019", "XXX", false);
// Assert
Assert.assertEquals(ResponseReason.AMOUNT_ZERO, response);
@@ -124,7 +124,7 @@ public class TransactionService_createTransactionTest {
Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.TRUE);
// Act
final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", Long.valueOf(125l), null, "XXX", false);
final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", 125L, null, "XXX", false);
// Assert
Assert.assertEquals(ResponseReason.MISSING_DATE, response);
@@ -137,7 +137,7 @@ public class TransactionService_createTransactionTest {
Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.TRUE);
// Act
final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", Long.valueOf(125l), "2019-01-01", "XXX", false);
final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", 125L, "2019-01-01", "XXX", false);
// Assert
Assert.assertEquals(ResponseReason.INVALID_DATE_FORMAT, response);
@@ -150,24 +150,24 @@ public class TransactionService_createTransactionTest {
final Account toAccount = Mockito.mock(Account.class);
Mockito.when(this.accountService.getAccountByKey(Mockito.anyString())).thenReturn(fromAccount, toAccount);
Mockito.when(this.ruleService.isValidBooking(Mockito.any(), Mockito.any())).thenReturn(Boolean.TRUE);
Mockito.when(this.ruleService.getMultiplierFromAccount(Mockito.any())).thenReturn(Long.valueOf(-1l));
Mockito.when(this.ruleService.getMultiplierToAccount(Mockito.any())).thenReturn(Long.valueOf(1l));
Mockito.when(fromAccount.getCurrentBalance()).thenReturn(Long.valueOf(0l));
Mockito.when(toAccount.getCurrentBalance()).thenReturn(Long.valueOf(0l));
Mockito.when(this.ruleService.getMultiplierFromAccount(Mockito.any())).thenReturn(-1L);
Mockito.when(this.ruleService.getMultiplierToAccount(Mockito.any())).thenReturn(1L);
Mockito.when(fromAccount.getCurrentBalance()).thenReturn(0L);
Mockito.when(toAccount.getCurrentBalance()).thenReturn(0L);
// Act
final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", Long.valueOf(125l), "24.02.2019", "XXX", false);
final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", 125L, "24.02.2019", "XXX", false);
// Assert
Assert.assertEquals(ResponseReason.OK, response);
Mockito.verify(fromAccount, Mockito.times(1)).setCurrentBalance(Long.valueOf(-125));
Mockito.verify(toAccount, Mockito.times(1)).setCurrentBalance(Long.valueOf(125));
Assert.assertEquals(ResponseReason.CREATED, response);
Mockito.verify(fromAccount, Mockito.times(1)).setCurrentBalance((long) -125);
Mockito.verify(toAccount, Mockito.times(1)).setCurrentBalance(125L);
}
private Account createAccount() {
final Account account = new Account();
account.setCurrentBalance(Long.valueOf(0l));
account.setCurrentBalance(0L);
return account;
}

View File

@@ -13,7 +13,6 @@ import org.junit.runner.RunWith;
import org.mockito.*;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.List;
import java.util.Optional;
@RunWith(MockitoJUnitRunner.class)
@@ -122,10 +121,10 @@ public class TransactionService_deleteTransactionTest {
transaction.setFromAccount(fromAccount);
transaction.setToAccount(toAccount);
transaction.setAmount(Long.valueOf(10000L));
transaction.setAmount(10000L);
fromAccount.setCurrentBalance(Long.valueOf(40000L));
toAccount.setCurrentBalance(Long.valueOf(15000L));
fromAccount.setCurrentBalance(40000L);
toAccount.setCurrentBalance(15000L);
fromAccount.setType(fromType);
toAccount.setType(toType);

View File

@@ -4,7 +4,6 @@ import de.financer.config.FinancerConfig;
import de.financer.model.Account;
import de.financer.model.RecurringTransaction;
import de.financer.service.RecurringTransactionService;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
@@ -40,10 +39,10 @@ public class SendRecurringTransactionReminderTaskTest {
public void test_sendReminder() {
// Arrange
final Collection<RecurringTransaction> recurringTransactions = Arrays.asList(
createRecurringTransaction("Test booking 1", "Income", "accounts.bank", Long.valueOf(250000), true),
createRecurringTransaction("Test booking 2", "Bank", "accounts.rent", Long.valueOf(41500), true),
createRecurringTransaction("Test booking 3", "Bank", "accounts.cash", Long.valueOf(5000), true),
createRecurringTransaction("Test booking 4", "Car", "accounts.car", Long.valueOf(1234), false)
createRecurringTransaction("Test booking 1", "Income", "accounts.bank", 250000L, true),
createRecurringTransaction("Test booking 2", "Bank", "accounts.rent", 41500L, true),
createRecurringTransaction("Test booking 3", "Bank", "accounts.cash", 5000L, true),
createRecurringTransaction("Test booking 4", "Car", "accounts.car", 1234L, false)
);
Mockito.when(this.recurringTransactionService.getAllDueToday()).thenReturn(recurringTransactions);