Add FQL and rework /transaction endpoint
This commit is contained in:
@@ -56,6 +56,10 @@
|
|||||||
<groupId>com.fasterxml.jackson.core</groupId>
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
<artifactId>jackson-annotations</artifactId>
|
<artifactId>jackson-annotations</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework</groupId>
|
||||||
|
<artifactId>spring-web</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import org.springframework.http.ResponseEntity;
|
|||||||
|
|
||||||
public enum ResponseReason {
|
public enum ResponseReason {
|
||||||
OK(HttpStatus.OK),
|
OK(HttpStatus.OK),
|
||||||
|
CREATED(HttpStatus.CREATED),
|
||||||
UNKNOWN_ERROR(HttpStatus.INTERNAL_SERVER_ERROR),
|
UNKNOWN_ERROR(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||||
INVALID_ACCOUNT_TYPE(HttpStatus.INTERNAL_SERVER_ERROR),
|
INVALID_ACCOUNT_TYPE(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||||
FROM_ACCOUNT_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR),
|
FROM_ACCOUNT_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||||
@@ -31,9 +32,15 @@ public enum ResponseReason {
|
|||||||
ACCOUNT_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR),
|
ACCOUNT_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||||
DUPLICATE_ACCOUNT_KEY(HttpStatus.INTERNAL_SERVER_ERROR),
|
DUPLICATE_ACCOUNT_KEY(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||||
DUPLICATE_ACCOUNT_GROUP_NAME(HttpStatus.INTERNAL_SERVER_ERROR),
|
DUPLICATE_ACCOUNT_GROUP_NAME(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||||
ACCOUNT_GROUP_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR);
|
ACCOUNT_GROUP_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||||
|
PERIOD_ID_NOT_NUMERIC(HttpStatus.BAD_REQUEST),
|
||||||
|
PERIOD_NOT_FOUND(HttpStatus.BAD_REQUEST),
|
||||||
|
LIMIT_NOT_NUMERIC(HttpStatus.BAD_REQUEST),
|
||||||
|
INVALID_TAX_RELEVANT_VALUE(HttpStatus.BAD_REQUEST),
|
||||||
|
INVALID_ACCOUNTS_AND_VALUE(HttpStatus.BAD_REQUEST),
|
||||||
|
FQL_MALFORMED(HttpStatus.BAD_REQUEST);
|
||||||
|
|
||||||
private HttpStatus httpStatus;
|
private final HttpStatus httpStatus;
|
||||||
|
|
||||||
ResponseReason(HttpStatus httpStatus) {
|
ResponseReason(HttpStatus httpStatus) {
|
||||||
this.httpStatus = httpStatus;
|
this.httpStatus = httpStatus;
|
||||||
@@ -43,6 +50,10 @@ public enum ResponseReason {
|
|||||||
return new ResponseEntity<>(this.name(), this.httpStatus);
|
return new ResponseEntity<>(this.name(), this.httpStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public HttpStatus getHttpStatus() {
|
||||||
|
return this.httpStatus;
|
||||||
|
}
|
||||||
|
|
||||||
public static ResponseReason fromResponseEntity(ResponseEntity<String> entity) {
|
public static ResponseReason fromResponseEntity(ResponseEntity<String> entity) {
|
||||||
for (ResponseReason reason : values()) {
|
for (ResponseReason reason : values()) {
|
||||||
if (reason.name().equals(entity.getBody())) {
|
if (reason.name().equals(entity.getBody())) {
|
||||||
30
financer-common/src/main/java/de/financer/dto/Order.java
Normal file
30
financer-common/src/main/java/de/financer/dto/Order.java
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package de.financer.dto;
|
||||||
|
|
||||||
|
import de.financer.dto.comparator.TransactionsByDateByIdDescComparator;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public enum Order {
|
||||||
|
TRANSACTIONS_BY_DATE_DESC(TransactionsByDateByIdDescComparator.class);
|
||||||
|
|
||||||
|
private Comparator comparator;
|
||||||
|
|
||||||
|
Order(Class<? extends Comparator> comparatorClass) {
|
||||||
|
try {
|
||||||
|
this.comparator = comparatorClass.getDeclaredConstructor().newInstance();
|
||||||
|
}
|
||||||
|
catch (ReflectiveOperationException e) {
|
||||||
|
// Comparators are required by contract to have a default constructor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Comparator getComparator() {
|
||||||
|
return this.comparator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<Order> getByName(String name) {
|
||||||
|
return Arrays.stream(values()).filter(o -> o.name().equals(name)).findFirst();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package de.financer.dto;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
|
||||||
|
|
||||||
|
public class SaveTransactionRequestDto {
|
||||||
|
private String fromAccountKey;
|
||||||
|
private String toAccountKey;
|
||||||
|
private String amount;
|
||||||
|
private String date;
|
||||||
|
private String description;
|
||||||
|
private Boolean taxRelevant;
|
||||||
|
|
||||||
|
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 getAmount() {
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAmount(String amount) {
|
||||||
|
this.amount = amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDate() {
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDate(String date) {
|
||||||
|
this.date = date;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDescription(String description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getTaxRelevant() {
|
||||||
|
return taxRelevant;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTaxRelevant(Boolean taxRelevant) {
|
||||||
|
this.taxRelevant = taxRelevant;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return ReflectionToStringBuilder.toString(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package de.financer.dto;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
|
||||||
|
|
||||||
|
public class SearchTransactionsRequestDto {
|
||||||
|
private String fromAccountKey;
|
||||||
|
private String toAccountKey;
|
||||||
|
private String periodId;
|
||||||
|
private String limit;
|
||||||
|
private String order;
|
||||||
|
private String taxRelevant;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return ReflectionToStringBuilder.toString(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package de.financer.dto;
|
||||||
|
|
||||||
|
import de.financer.model.Account;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
public class SearchTransactionsResponseDto {
|
||||||
|
private Long id;
|
||||||
|
private Account fromAccount;
|
||||||
|
private Account toAccount;
|
||||||
|
private LocalDate date;
|
||||||
|
private String description;
|
||||||
|
private long amount;
|
||||||
|
private boolean taxRelevant;
|
||||||
|
private boolean recurring;
|
||||||
|
|
||||||
|
public SearchTransactionsResponseDto() {
|
||||||
|
// No-arg constructor for Jackson
|
||||||
|
}
|
||||||
|
|
||||||
|
public SearchTransactionsResponseDto(Long id,
|
||||||
|
Account fromAccount,
|
||||||
|
Account toAccount,
|
||||||
|
LocalDate date,
|
||||||
|
String description,
|
||||||
|
long amount,
|
||||||
|
boolean taxRelevant,
|
||||||
|
boolean recurring) {
|
||||||
|
this.id = id;
|
||||||
|
this.fromAccount = fromAccount;
|
||||||
|
this.toAccount = toAccount;
|
||||||
|
this.date = date;
|
||||||
|
this.description = description;
|
||||||
|
this.amount = amount;
|
||||||
|
this.taxRelevant = taxRelevant;
|
||||||
|
this.recurring = recurring;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Account getFromAccount() {
|
||||||
|
return fromAccount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFromAccount(Account fromAccount) {
|
||||||
|
this.fromAccount = fromAccount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Account getToAccount() {
|
||||||
|
return toAccount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setToAccount(Account toAccount) {
|
||||||
|
this.toAccount = toAccount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getDate() {
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDate(LocalDate date) {
|
||||||
|
this.date = date;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDescription(String description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getAmount() {
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAmount(long amount) {
|
||||||
|
this.amount = amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTaxRelevant() {
|
||||||
|
return taxRelevant;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTaxRelevant(boolean taxRelevant) {
|
||||||
|
this.taxRelevant = taxRelevant;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRecurring() {
|
||||||
|
return recurring;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRecurring(boolean recurring) {
|
||||||
|
this.recurring = recurring;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package de.financer.dto.comparator;
|
||||||
|
|
||||||
|
import de.financer.model.Transaction;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
|
||||||
|
public class TransactionsByDateByIdDescComparator implements Comparator<Transaction> {
|
||||||
|
@Override
|
||||||
|
public int compare(Transaction o1, Transaction o2) {
|
||||||
|
int dateOrder = o1.getDate().compareTo(o2.getDate()) * -1;
|
||||||
|
|
||||||
|
return dateOrder != 0 ? dateOrder : o1.getId().compareTo(o2.getId()) * -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
/**
|
||||||
|
* All implementations need to declare an (implicit) parameterless default constructor
|
||||||
|
*/
|
||||||
|
package de.financer.dto.comparator;
|
||||||
@@ -17,6 +17,7 @@ public class Account {
|
|||||||
private Long currentBalance;
|
private Long currentBalance;
|
||||||
@ManyToOne
|
@ManyToOne
|
||||||
private AccountGroup accountGroup;
|
private AccountGroup accountGroup;
|
||||||
|
// TODO need to be lazy and disabled via Jackson View
|
||||||
@OneToMany(fetch = FetchType.EAGER)
|
@OneToMany(fetch = FetchType.EAGER)
|
||||||
@JoinColumn(name = "account_id")
|
@JoinColumn(name = "account_id")
|
||||||
private Set<AccountStatistic> accountStatistics;
|
private Set<AccountStatistic> accountStatistics;
|
||||||
|
|||||||
@@ -57,6 +57,11 @@
|
|||||||
<groupId>org.glassfish.jaxb</groupId>
|
<groupId>org.glassfish.jaxb</groupId>
|
||||||
<artifactId>jaxb-runtime</artifactId>
|
<artifactId>jaxb-runtime</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.antlr</groupId>
|
||||||
|
<artifactId>antlr4-runtime</artifactId>
|
||||||
|
<version>4.8-1</version>
|
||||||
|
</dependency>
|
||||||
<!-- Financer dependencies -->
|
<!-- Financer dependencies -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>de.77zzcx7.financer</groupId>
|
<groupId>de.77zzcx7.financer</groupId>
|
||||||
@@ -93,6 +98,28 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.antlr</groupId>
|
||||||
|
<artifactId>antlr4-maven-plugin</artifactId>
|
||||||
|
<version>4.8-1</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>antlr</id>
|
||||||
|
<goals>
|
||||||
|
<goal>antlr4</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<visitor>true</visitor>
|
||||||
|
<listener>false</listener>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
<reporting>
|
<reporting>
|
||||||
<plugins>
|
<plugins>
|
||||||
<plugin>
|
<plugin>
|
||||||
|
|||||||
103
financer-server/src/main/antlr4/de/financer/fql/FQL.g4
Normal file
103
financer-server/src/main/antlr4/de/financer/fql/FQL.g4
Normal 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;
|
||||||
@@ -11,10 +11,10 @@ import java.util.List;
|
|||||||
|
|
||||||
public class V22_0_1__calculateAccountStatistics extends BaseJavaMigration {
|
public class V22_0_1__calculateAccountStatistics extends BaseJavaMigration {
|
||||||
private static class TransactionPeriodAccountsContainer {
|
private static class TransactionPeriodAccountsContainer {
|
||||||
public Long transactionAmount;
|
public final Long transactionAmount;
|
||||||
public Long expensePeriodId;
|
public final Long expensePeriodId;
|
||||||
public Long fromAccountId;
|
public final Long fromAccountId;
|
||||||
public Long toAccountId;
|
public final Long toAccountId;
|
||||||
|
|
||||||
public TransactionPeriodAccountsContainer(Long transactionAmount, Long expensePeriodId, Long fromAccountId, Long toAccountId) {
|
public TransactionPeriodAccountsContainer(Long transactionAmount, Long expensePeriodId, Long fromAccountId, Long toAccountId) {
|
||||||
this.transactionAmount = transactionAmount;
|
this.transactionAmount = transactionAmount;
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ import java.util.List;
|
|||||||
|
|
||||||
public class V26_0_0__createGrandTotalPeriod extends BaseJavaMigration {
|
public class V26_0_0__createGrandTotalPeriod extends BaseJavaMigration {
|
||||||
private static class TransactionAccountsContainer {
|
private static class TransactionAccountsContainer {
|
||||||
public Long transactionAmount;
|
public final Long transactionAmount;
|
||||||
public Long fromAccountId;
|
public final Long fromAccountId;
|
||||||
public Long toAccountId;
|
public final Long toAccountId;
|
||||||
public Long id;
|
public final Long id;
|
||||||
|
|
||||||
public TransactionAccountsContainer(Long transactionAmount, Long fromAccountId, Long toAccountId, Long id) {
|
public TransactionAccountsContainer(Long transactionAmount, Long fromAccountId, Long toAccountId, Long id) {
|
||||||
this.transactionAmount = transactionAmount;
|
this.transactionAmount = transactionAmount;
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ public class V26_0_1__createExpenseYearPeriod_2019 extends BaseJavaMigration {
|
|||||||
private static class TransactionAccountsContainer {
|
private static class TransactionAccountsContainer {
|
||||||
|
|
||||||
|
|
||||||
public Long transactionAmount;
|
public final Long transactionAmount;
|
||||||
public Long fromAccountId;
|
public final Long fromAccountId;
|
||||||
public Long toAccountId;
|
public final Long toAccountId;
|
||||||
public Long id;
|
public final Long id;
|
||||||
public TransactionAccountsContainer(Long transactionAmount, Long fromAccountId, Long toAccountId, Long id) {
|
public TransactionAccountsContainer(Long transactionAmount, Long fromAccountId, Long toAccountId, Long id) {
|
||||||
this.transactionAmount = transactionAmount;
|
this.transactionAmount = transactionAmount;
|
||||||
this.fromAccountId = fromAccountId;
|
this.fromAccountId = fromAccountId;
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import org.flywaydb.core.api.migration.Context;
|
|||||||
import java.sql.*;
|
import java.sql.*;
|
||||||
import java.time.ZoneOffset;
|
import java.time.ZoneOffset;
|
||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.time.temporal.TemporalField;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -14,10 +13,10 @@ public class V26_0_2__createExpenseYearPeriod_2020 extends BaseJavaMigration {
|
|||||||
private static class TransactionAccountsContainer {
|
private static class TransactionAccountsContainer {
|
||||||
|
|
||||||
|
|
||||||
public Long transactionAmount;
|
public final Long transactionAmount;
|
||||||
public Long fromAccountId;
|
public final Long fromAccountId;
|
||||||
public Long toAccountId;
|
public final Long toAccountId;
|
||||||
public Long id;
|
public final Long id;
|
||||||
public TransactionAccountsContainer(Long transactionAmount, Long fromAccountId, Long toAccountId, Long id) {
|
public TransactionAccountsContainer(Long transactionAmount, Long fromAccountId, Long toAccountId, Long id) {
|
||||||
this.transactionAmount = transactionAmount;
|
this.transactionAmount = transactionAmount;
|
||||||
this.fromAccountId = fromAccountId;
|
this.fromAccountId = fromAccountId;
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,6 @@ import org.slf4j.LoggerFactory;
|
|||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
import javax.management.ReflectionException;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
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,
|
* @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,
|
* de.financer.service.RecurringTransactionService#createRecurringTransaction(String, String, Long, String, String,
|
||||||
* String, String, String) RecurringTransactionService#createRecurringTransaction} methods. Used to parse the
|
* String, String, String, Boolean, Boolean)} RecurringTransactionService#createRecurringTransaction} methods. Used
|
||||||
* client-supplied date string to proper {@link java.time.LocalDate LocalDate} objects
|
* to parse the client-supplied date string to proper {@link java.time.LocalDate LocalDate} objects
|
||||||
*/
|
*/
|
||||||
public String getDateFormat() {
|
public String getDateFormat() {
|
||||||
return dateFormat;
|
return dateFormat;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ public class ControllerUtil {
|
|||||||
* @param toDecode the string to decode
|
* @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
|
* @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 {
|
try {
|
||||||
return URLDecoder.decode(toDecode, StandardCharsets.UTF_8.name());
|
return URLDecoder.decode(toDecode, StandardCharsets.UTF_8.name());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ import org.springframework.http.ResponseEntity;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import java.net.URLDecoder;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
|||||||
@@ -2,125 +2,160 @@ package de.financer.controller;
|
|||||||
|
|
||||||
import de.financer.ResponseReason;
|
import de.financer.ResponseReason;
|
||||||
import de.financer.dto.ExpensePeriodTotal;
|
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.TransactionService;
|
||||||
|
import de.financer.service.parameter.SearchTransactionsParameter;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("transactions")
|
|
||||||
public class TransactionController {
|
public class TransactionController {
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(TransactionController.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(TransactionController.class);
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private TransactionService transactionService;
|
private TransactionService transactionService;
|
||||||
|
|
||||||
@RequestMapping("getAll")
|
@GetMapping("/transactionsByFql")
|
||||||
public Iterable<Transaction> getAll() {
|
public Iterable<SearchTransactionsResponseDto> searchTransactions(String fql) {
|
||||||
return this.transactionService.getAll();
|
final String fqlDecoded = ControllerUtil.urlDecode(fql);
|
||||||
}
|
|
||||||
|
|
||||||
@RequestMapping("getAllForAccount")
|
|
||||||
public Iterable<Transaction> getAllForAccount(String accountKey) {
|
|
||||||
final String decoded = ControllerUtil.urlDecode(accountKey);
|
|
||||||
|
|
||||||
if (LOGGER.isDebugEnabled()) {
|
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);
|
final Iterable<SearchTransactionsResponseDto> transactionSearchResponse =
|
||||||
}
|
this.transactionService.searchTransactions(fqlDecoded);
|
||||||
|
|
||||||
@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);
|
|
||||||
|
|
||||||
if (LOGGER.isDebugEnabled()) {
|
if (LOGGER.isDebugEnabled()) {
|
||||||
LOGGER.debug(String
|
LOGGER.debug(String.format("GET /transactionsByFql returns with %s", transactionSearchResponse));
|
||||||
.format("/transactions/createTransaction got parameters: %s, %s, %s, %s, %s, %s",
|
}
|
||||||
decodedFrom, decodedTo, amount, date, decodedDesc, taxRelevant));
|
|
||||||
|
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
|
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()) {
|
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();
|
return responseReason.toResponseEntity();
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping("deleteTransaction")
|
@DeleteMapping("/transactions/{transactionId}")
|
||||||
public ResponseEntity deleteTransaction(String transactionId) {
|
public ResponseEntity deleteTransaction(@PathVariable String transactionId) {
|
||||||
if (LOGGER.isDebugEnabled()) {
|
if (LOGGER.isDebugEnabled()) {
|
||||||
LOGGER.debug(String
|
LOGGER.debug(String.format("DELETE /transactions/{transactionId} got parameters: %s", transactionId));
|
||||||
.format("/transactions/deleteTransaction got parameters: %s",
|
|
||||||
transactionId));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final ResponseReason responseReason = this.transactionService
|
final ResponseReason responseReason = this.transactionService.deleteTransaction(transactionId);
|
||||||
.deleteTransaction(transactionId);
|
|
||||||
|
|
||||||
if (LOGGER.isDebugEnabled()) {
|
if (LOGGER.isDebugEnabled()) {
|
||||||
LOGGER.debug(String
|
LOGGER.debug(String.format("DELETE /transactions/{transactionId} returns with %s", responseReason.name()));
|
||||||
.format("/transactions/deleteTransaction returns with %s", responseReason.name()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return responseReason.toResponseEntity();
|
return responseReason.toResponseEntity();
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping("getExpensesCurrentPeriod")
|
@GetMapping("/transactions/getExpensesCurrentPeriod")
|
||||||
public Long getExpensesCurrentPeriod() {
|
public Long getExpensesCurrentPeriod() {
|
||||||
if (LOGGER.isDebugEnabled()) {
|
if (LOGGER.isDebugEnabled()) {
|
||||||
LOGGER.debug(String.format("/transactions/getExpensesCurrentPeriod called"));
|
LOGGER.debug(String.format("GET /transactions/getExpensesCurrentPeriod called"));
|
||||||
}
|
}
|
||||||
|
|
||||||
final Long response = this.transactionService.getExpensesCurrentPeriod();
|
final Long response = this.transactionService.getExpensesCurrentPeriod();
|
||||||
|
|
||||||
if (LOGGER.isDebugEnabled()) {
|
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;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping("getExpensePeriodTotals")
|
@GetMapping("/transactions/getExpensePeriodTotals")
|
||||||
public Iterable<ExpensePeriodTotal> getExpensePeriodTotals(Integer year) {
|
public Iterable<ExpensePeriodTotal> getExpensePeriodTotals(Integer year) {
|
||||||
if (LOGGER.isDebugEnabled()) {
|
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
|
final Iterable<ExpensePeriodTotal> expensePeriodTotals = this.transactionService.getExpensePeriodTotals(year);
|
||||||
.getExpensePeriodTotals(year);
|
|
||||||
|
|
||||||
if (LOGGER.isDebugEnabled()) {
|
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;
|
return expensePeriodTotals;
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping("getExpensesAllPeriods")
|
@GetMapping("/transactions/getExpensesAllPeriods")
|
||||||
public List<Long> getExpensesAllPeriods() {
|
public List<Long> getExpensesAllPeriods() {
|
||||||
if (LOGGER.isDebugEnabled()) {
|
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();
|
final List<Long> response = this.transactionService.getExpensesAllPeriods();
|
||||||
|
|
||||||
if (LOGGER.isDebugEnabled()) {
|
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;
|
return response;
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import org.springframework.data.repository.CrudRepository;
|
|||||||
import org.springframework.transaction.annotation.Propagation;
|
import org.springframework.transaction.annotation.Propagation;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Transactional(propagation = Propagation.REQUIRED)
|
@Transactional(propagation = Propagation.REQUIRED)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import de.financer.model.Account;
|
|||||||
import de.financer.model.RecurringTransaction;
|
import de.financer.model.RecurringTransaction;
|
||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.jpa.repository.Query;
|
||||||
import org.springframework.data.repository.CrudRepository;
|
import org.springframework.data.repository.CrudRepository;
|
||||||
import org.springframework.data.repository.query.Param;
|
|
||||||
import org.springframework.transaction.annotation.Propagation;
|
import org.springframework.transaction.annotation.Propagation;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,10 @@ import org.springframework.data.repository.CrudRepository;
|
|||||||
import org.springframework.transaction.annotation.Propagation;
|
import org.springframework.transaction.annotation.Propagation;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Transactional(propagation = Propagation.REQUIRED)
|
@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")
|
@Query("SELECT t FROM Transaction t WHERE t.toAccount = :toAccount OR t.fromAccount = :fromAccount")
|
||||||
Iterable<Transaction> findTransactionsByFromAccountOrToAccount(Account fromAccount, Account toAccount);
|
Iterable<Transaction> findTransactionsByFromAccountOrToAccount(Account fromAccount, Account toAccount);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
119
financer-server/src/main/java/de/financer/fql/FieldMapping.java
Normal file
119
financer-server/src/main/java/de/financer/fql/FieldMapping.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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("'", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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("'", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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> {
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,6 +43,7 @@ public class AccountService {
|
|||||||
* @return the account or <code>null</code> if no account with the given key can be found
|
* @return the account or <code>null</code> if no account with the given key can be found
|
||||||
*/
|
*/
|
||||||
public Account getAccountByKey(String key) {
|
public Account getAccountByKey(String key) {
|
||||||
|
// TODO change return type of repository to Optional<Account>
|
||||||
return this.accountRepository.findByKey(key);
|
return this.accountRepository.findByKey(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +103,7 @@ public class AccountService {
|
|||||||
// If we create an account it's implicitly open
|
// If we create an account it's implicitly open
|
||||||
account.setStatus(AccountStatus.OPEN);
|
account.setStatus(AccountStatus.OPEN);
|
||||||
// and has a current balance of 0
|
// and has a current balance of 0
|
||||||
account.setCurrentBalance(Long.valueOf(0L));
|
account.setCurrentBalance(0L);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.accountRepository.save(account);
|
this.accountRepository.save(account);
|
||||||
|
|||||||
@@ -12,10 +12,9 @@ import org.slf4j.LoggerFactory;
|
|||||||
import org.springframework.transaction.annotation.Propagation;
|
import org.springframework.transaction.annotation.Propagation;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.LocalTime;
|
import java.time.LocalTime;
|
||||||
import java.time.Month;
|
import java.util.Optional;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class PeriodService {
|
public class PeriodService {
|
||||||
@@ -132,4 +131,8 @@ public class PeriodService {
|
|||||||
|
|
||||||
return period;
|
return period;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Optional<Period> getPeriodById(Long id) {
|
||||||
|
return this.periodRepository.findById(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ import de.financer.ResponseReason;
|
|||||||
import de.financer.config.FinancerConfig;
|
import de.financer.config.FinancerConfig;
|
||||||
import de.financer.dba.TransactionRepository;
|
import de.financer.dba.TransactionRepository;
|
||||||
import de.financer.dto.ExpensePeriodTotal;
|
import de.financer.dto.ExpensePeriodTotal;
|
||||||
|
import de.financer.dto.Order;
|
||||||
|
import de.financer.dto.SearchTransactionsResponseDto;
|
||||||
import de.financer.model.*;
|
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.StringUtils;
|
||||||
import org.apache.commons.lang3.math.NumberUtils;
|
import org.apache.commons.lang3.math.NumberUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -48,25 +53,6 @@ public class TransactionService {
|
|||||||
return this.transactionRepository.findAll();
|
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)
|
@Transactional(propagation = Propagation.REQUIRED)
|
||||||
public ResponseReason createTransaction(String fromAccountKey, String toAccountKey, Long amount, String date,
|
public ResponseReason createTransaction(String fromAccountKey, String toAccountKey, Long amount, String date,
|
||||||
String description, Boolean taxRelevant
|
String description, Boolean taxRelevant
|
||||||
@@ -114,7 +100,7 @@ public class TransactionService {
|
|||||||
this.accountService.saveAccount(fromAccount);
|
this.accountService.saveAccount(fromAccount);
|
||||||
this.accountService.saveAccount(toAccount);
|
this.accountService.saveAccount(toAccount);
|
||||||
|
|
||||||
response = ResponseReason.OK;
|
response = ResponseReason.CREATED;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOGGER.error("Could not create transaction!", e);
|
LOGGER.error("Could not create transaction!", e);
|
||||||
|
|
||||||
@@ -261,7 +247,7 @@ public class TransactionService {
|
|||||||
final Long expensesCurrentPeriod = this.transactionRepository
|
final Long expensesCurrentPeriod = this.transactionRepository
|
||||||
.getExpensesForPeriod(expensePeriod, AccountType.EXPENSE, AccountType.LIABILITY);
|
.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
|
return this.transactionRepository
|
||||||
.getAccountExpenseTotals(periods, AccountType.INCOME, AccountType.START, AccountType.EXPENSE, AccountType.LIABILITY);
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,6 @@ import org.springframework.mail.javamail.JavaMailSender;
|
|||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@@ -52,7 +51,7 @@ public class SendRecurringTransactionReminderTask {
|
|||||||
// TODO Filtering currently happens in memory but should be done via SQL
|
// TODO Filtering currently happens in memory but should be done via SQL
|
||||||
recurringTransactions = IterableUtils.toList(recurringTransactions)
|
recurringTransactions = IterableUtils.toList(recurringTransactions)
|
||||||
.stream()
|
.stream()
|
||||||
.filter((rt) -> rt.isRemind())
|
.filter(RecurringTransaction::isRemind)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
LOGGER.info(String
|
LOGGER.info(String
|
||||||
@@ -65,20 +64,18 @@ public class SendRecurringTransactionReminderTask {
|
|||||||
.append(System.lineSeparator())
|
.append(System.lineSeparator())
|
||||||
.append(System.lineSeparator());
|
.append(System.lineSeparator());
|
||||||
|
|
||||||
IterableUtils.toList(recurringTransactions).forEach((rt) -> {
|
IterableUtils.toList(recurringTransactions).forEach((rt) -> reminderBuilder.append(rt.getId())
|
||||||
reminderBuilder.append(rt.getId())
|
.append("|")
|
||||||
.append("|")
|
.append(rt.getDescription())
|
||||||
.append(rt.getDescription())
|
.append(System.lineSeparator())
|
||||||
.append(System.lineSeparator())
|
.append(this.getMessage("financer.recurring-transaction.email.reminder.from"))
|
||||||
.append(this.getMessage("financer.recurring-transaction.email.reminder.from"))
|
.append(rt.getFromAccount().getKey())
|
||||||
.append(rt.getFromAccount().getKey())
|
.append(this.getMessage("financer.recurring-transaction.email.reminder.to"))
|
||||||
.append(this.getMessage("financer.recurring-transaction.email.reminder.to"))
|
.append(rt.getToAccount().getKey())
|
||||||
.append(rt.getToAccount().getKey())
|
.append(": ")
|
||||||
.append(": ")
|
.append(rt.getAmount().toString())
|
||||||
.append(rt.getAmount().toString())
|
.append(System.lineSeparator())
|
||||||
.append(System.lineSeparator())
|
.append(System.lineSeparator()));
|
||||||
.append(System.lineSeparator());
|
|
||||||
});
|
|
||||||
|
|
||||||
final SimpleMailMessage msg = new SimpleMailMessage();
|
final SimpleMailMessage msg = new SimpleMailMessage();
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package de.financer.service;
|
|||||||
import de.financer.ResponseReason;
|
import de.financer.ResponseReason;
|
||||||
import de.financer.dba.AccountRepository;
|
import de.financer.dba.AccountRepository;
|
||||||
import de.financer.model.Account;
|
import de.financer.model.Account;
|
||||||
import de.financer.model.AccountGroup;
|
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ public class RecurringTransactionService_createRecurringTransactionTest {
|
|||||||
// Act
|
// Act
|
||||||
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.invalid",
|
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.invalid",
|
||||||
"account.invalid",
|
"account.invalid",
|
||||||
Long.valueOf(150l),
|
150L,
|
||||||
"DESCRIPTION",
|
"DESCRIPTION",
|
||||||
"HOLIDAY_WEEKEND_TYPE",
|
"HOLIDAY_WEEKEND_TYPE",
|
||||||
"INTERVAL_TYPE",
|
"INTERVAL_TYPE",
|
||||||
@@ -67,7 +67,7 @@ public class RecurringTransactionService_createRecurringTransactionTest {
|
|||||||
// Act
|
// Act
|
||||||
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
|
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
|
||||||
"account.invalid",
|
"account.invalid",
|
||||||
Long.valueOf(150l),
|
150L,
|
||||||
"DESCRIPTION",
|
"DESCRIPTION",
|
||||||
"HOLIDAY_WEEKEND_TYPE",
|
"HOLIDAY_WEEKEND_TYPE",
|
||||||
"INTERVAL_TYPE",
|
"INTERVAL_TYPE",
|
||||||
@@ -88,7 +88,7 @@ public class RecurringTransactionService_createRecurringTransactionTest {
|
|||||||
// Act
|
// Act
|
||||||
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.invalid",
|
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.invalid",
|
||||||
"account.to",
|
"account.to",
|
||||||
Long.valueOf(150l),
|
150L,
|
||||||
"DESCRIPTION",
|
"DESCRIPTION",
|
||||||
"HOLIDAY_WEEKEND_TYPE",
|
"HOLIDAY_WEEKEND_TYPE",
|
||||||
"INTERVAL_TYPE",
|
"INTERVAL_TYPE",
|
||||||
@@ -110,7 +110,7 @@ public class RecurringTransactionService_createRecurringTransactionTest {
|
|||||||
// Act
|
// Act
|
||||||
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
|
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
|
||||||
"account.to",
|
"account.to",
|
||||||
Long.valueOf(150l),
|
150L,
|
||||||
"DESCRIPTION",
|
"DESCRIPTION",
|
||||||
"HOLIDAY_WEEKEND_TYPE",
|
"HOLIDAY_WEEKEND_TYPE",
|
||||||
"INTERVAL_TYPE",
|
"INTERVAL_TYPE",
|
||||||
@@ -154,7 +154,7 @@ public class RecurringTransactionService_createRecurringTransactionTest {
|
|||||||
// Act
|
// Act
|
||||||
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
|
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
|
||||||
"account.to",
|
"account.to",
|
||||||
Long.valueOf(0l),
|
0L,
|
||||||
"DESCRIPTION",
|
"DESCRIPTION",
|
||||||
"HOLIDAY_WEEKEND_TYPE",
|
"HOLIDAY_WEEKEND_TYPE",
|
||||||
"INTERVAL_TYPE",
|
"INTERVAL_TYPE",
|
||||||
@@ -176,7 +176,7 @@ public class RecurringTransactionService_createRecurringTransactionTest {
|
|||||||
// Act
|
// Act
|
||||||
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
|
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
|
||||||
"account.to",
|
"account.to",
|
||||||
Long.valueOf(150l),
|
150L,
|
||||||
"DESCRIPTION",
|
"DESCRIPTION",
|
||||||
null,
|
null,
|
||||||
"INTERVAL_TYPE",
|
"INTERVAL_TYPE",
|
||||||
@@ -198,7 +198,7 @@ public class RecurringTransactionService_createRecurringTransactionTest {
|
|||||||
// Act
|
// Act
|
||||||
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
|
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
|
||||||
"account.to",
|
"account.to",
|
||||||
Long.valueOf(150l),
|
150L,
|
||||||
"DESCRIPTION",
|
"DESCRIPTION",
|
||||||
"HOLIDAY_WEEKEND_TYPE",
|
"HOLIDAY_WEEKEND_TYPE",
|
||||||
"INTERVAL_TYPE",
|
"INTERVAL_TYPE",
|
||||||
@@ -220,7 +220,7 @@ public class RecurringTransactionService_createRecurringTransactionTest {
|
|||||||
// Act
|
// Act
|
||||||
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
|
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
|
||||||
"account.to",
|
"account.to",
|
||||||
Long.valueOf(150l),
|
150L,
|
||||||
"DESCRIPTION",
|
"DESCRIPTION",
|
||||||
HolidayWeekendType.SAME_DAY.name(),
|
HolidayWeekendType.SAME_DAY.name(),
|
||||||
null,
|
null,
|
||||||
@@ -242,7 +242,7 @@ public class RecurringTransactionService_createRecurringTransactionTest {
|
|||||||
// Act
|
// Act
|
||||||
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
|
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
|
||||||
"account.to",
|
"account.to",
|
||||||
Long.valueOf(150l),
|
150L,
|
||||||
"DESCRIPTION",
|
"DESCRIPTION",
|
||||||
HolidayWeekendType.SAME_DAY.name(),
|
HolidayWeekendType.SAME_DAY.name(),
|
||||||
"INTERVAL_TYPE",
|
"INTERVAL_TYPE",
|
||||||
@@ -264,7 +264,7 @@ public class RecurringTransactionService_createRecurringTransactionTest {
|
|||||||
// Act
|
// Act
|
||||||
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
|
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
|
||||||
"account.to",
|
"account.to",
|
||||||
Long.valueOf(150l),
|
150L,
|
||||||
"DESCRIPTION",
|
"DESCRIPTION",
|
||||||
HolidayWeekendType.SAME_DAY.name(),
|
HolidayWeekendType.SAME_DAY.name(),
|
||||||
IntervalType.DAILY.name(),
|
IntervalType.DAILY.name(),
|
||||||
@@ -286,7 +286,7 @@ public class RecurringTransactionService_createRecurringTransactionTest {
|
|||||||
// Act
|
// Act
|
||||||
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
|
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
|
||||||
"account.to",
|
"account.to",
|
||||||
Long.valueOf(150l),
|
150L,
|
||||||
"DESCRIPTION",
|
"DESCRIPTION",
|
||||||
HolidayWeekendType.SAME_DAY.name(),
|
HolidayWeekendType.SAME_DAY.name(),
|
||||||
IntervalType.DAILY.name(),
|
IntervalType.DAILY.name(),
|
||||||
@@ -308,7 +308,7 @@ public class RecurringTransactionService_createRecurringTransactionTest {
|
|||||||
// Act
|
// Act
|
||||||
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
|
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
|
||||||
"account.to",
|
"account.to",
|
||||||
Long.valueOf(150l),
|
150L,
|
||||||
"DESCRIPTION",
|
"DESCRIPTION",
|
||||||
HolidayWeekendType.SAME_DAY.name(),
|
HolidayWeekendType.SAME_DAY.name(),
|
||||||
IntervalType.DAILY.name(),
|
IntervalType.DAILY.name(),
|
||||||
@@ -331,7 +331,7 @@ public class RecurringTransactionService_createRecurringTransactionTest {
|
|||||||
// Act
|
// Act
|
||||||
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
|
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
|
||||||
"account.to",
|
"account.to",
|
||||||
Long.valueOf(150l),
|
150L,
|
||||||
"DESCRIPTION",
|
"DESCRIPTION",
|
||||||
HolidayWeekendType.SAME_DAY.name(),
|
HolidayWeekendType.SAME_DAY.name(),
|
||||||
IntervalType.DAILY.name(),
|
IntervalType.DAILY.name(),
|
||||||
@@ -353,7 +353,7 @@ public class RecurringTransactionService_createRecurringTransactionTest {
|
|||||||
// Act
|
// Act
|
||||||
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
|
final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from",
|
||||||
"account.to",
|
"account.to",
|
||||||
Long.valueOf(150l),
|
150L,
|
||||||
"DESCRIPTION",
|
"DESCRIPTION",
|
||||||
HolidayWeekendType.SAME_DAY.name(),
|
HolidayWeekendType.SAME_DAY.name(),
|
||||||
IntervalType.DAILY.name(),
|
IntervalType.DAILY.name(),
|
||||||
@@ -369,7 +369,7 @@ public class RecurringTransactionService_createRecurringTransactionTest {
|
|||||||
private Account createAccount() {
|
private Account createAccount() {
|
||||||
final Account account = new Account();
|
final Account account = new Account();
|
||||||
|
|
||||||
account.setCurrentBalance(Long.valueOf(0l));
|
account.setCurrentBalance(0L);
|
||||||
|
|
||||||
return account;
|
return account;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ public class TransactionService_createTransactionTest {
|
|||||||
// will not be found.
|
// will not be found.
|
||||||
|
|
||||||
// Act
|
// 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
|
||||||
Assert.assertEquals(ResponseReason.FROM_AND_TO_ACCOUNT_NOT_FOUND, response);
|
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);
|
Mockito.when(this.accountService.getAccountByKey(Mockito.anyString())).thenReturn(createAccount(), null);
|
||||||
|
|
||||||
// Act
|
// 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
|
||||||
Assert.assertEquals(ResponseReason.TO_ACCOUNT_NOT_FOUND, response);
|
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());
|
Mockito.when(this.accountService.getAccountByKey(Mockito.anyString())).thenReturn(null, createAccount());
|
||||||
|
|
||||||
// Act
|
// 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
|
||||||
Assert.assertEquals(ResponseReason.FROM_ACCOUNT_NOT_FOUND, response);
|
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);
|
Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.FALSE);
|
||||||
|
|
||||||
// Act
|
// 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
|
||||||
Assert.assertEquals(ResponseReason.INVALID_BOOKING_ACCOUNTS, response);
|
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);
|
Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.TRUE);
|
||||||
|
|
||||||
// Act
|
// 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
|
||||||
Assert.assertEquals(ResponseReason.AMOUNT_ZERO, response);
|
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);
|
Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.TRUE);
|
||||||
|
|
||||||
// Act
|
// 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
|
||||||
Assert.assertEquals(ResponseReason.MISSING_DATE, response);
|
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);
|
Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.TRUE);
|
||||||
|
|
||||||
// Act
|
// 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
|
||||||
Assert.assertEquals(ResponseReason.INVALID_DATE_FORMAT, response);
|
Assert.assertEquals(ResponseReason.INVALID_DATE_FORMAT, response);
|
||||||
@@ -150,24 +150,24 @@ public class TransactionService_createTransactionTest {
|
|||||||
final Account toAccount = Mockito.mock(Account.class);
|
final Account toAccount = Mockito.mock(Account.class);
|
||||||
Mockito.when(this.accountService.getAccountByKey(Mockito.anyString())).thenReturn(fromAccount, toAccount);
|
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.isValidBooking(Mockito.any(), Mockito.any())).thenReturn(Boolean.TRUE);
|
||||||
Mockito.when(this.ruleService.getMultiplierFromAccount(Mockito.any())).thenReturn(Long.valueOf(-1l));
|
Mockito.when(this.ruleService.getMultiplierFromAccount(Mockito.any())).thenReturn(-1L);
|
||||||
Mockito.when(this.ruleService.getMultiplierToAccount(Mockito.any())).thenReturn(Long.valueOf(1l));
|
Mockito.when(this.ruleService.getMultiplierToAccount(Mockito.any())).thenReturn(1L);
|
||||||
Mockito.when(fromAccount.getCurrentBalance()).thenReturn(Long.valueOf(0l));
|
Mockito.when(fromAccount.getCurrentBalance()).thenReturn(0L);
|
||||||
Mockito.when(toAccount.getCurrentBalance()).thenReturn(Long.valueOf(0l));
|
Mockito.when(toAccount.getCurrentBalance()).thenReturn(0L);
|
||||||
|
|
||||||
// Act
|
// 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
|
||||||
Assert.assertEquals(ResponseReason.OK, response);
|
Assert.assertEquals(ResponseReason.CREATED, response);
|
||||||
Mockito.verify(fromAccount, Mockito.times(1)).setCurrentBalance(Long.valueOf(-125));
|
Mockito.verify(fromAccount, Mockito.times(1)).setCurrentBalance((long) -125);
|
||||||
Mockito.verify(toAccount, Mockito.times(1)).setCurrentBalance(Long.valueOf(125));
|
Mockito.verify(toAccount, Mockito.times(1)).setCurrentBalance(125L);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Account createAccount() {
|
private Account createAccount() {
|
||||||
final Account account = new Account();
|
final Account account = new Account();
|
||||||
|
|
||||||
account.setCurrentBalance(Long.valueOf(0l));
|
account.setCurrentBalance(0L);
|
||||||
|
|
||||||
return account;
|
return account;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import org.junit.runner.RunWith;
|
|||||||
import org.mockito.*;
|
import org.mockito.*;
|
||||||
import org.mockito.junit.MockitoJUnitRunner;
|
import org.mockito.junit.MockitoJUnitRunner;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@RunWith(MockitoJUnitRunner.class)
|
@RunWith(MockitoJUnitRunner.class)
|
||||||
@@ -122,10 +121,10 @@ public class TransactionService_deleteTransactionTest {
|
|||||||
|
|
||||||
transaction.setFromAccount(fromAccount);
|
transaction.setFromAccount(fromAccount);
|
||||||
transaction.setToAccount(toAccount);
|
transaction.setToAccount(toAccount);
|
||||||
transaction.setAmount(Long.valueOf(10000L));
|
transaction.setAmount(10000L);
|
||||||
|
|
||||||
fromAccount.setCurrentBalance(Long.valueOf(40000L));
|
fromAccount.setCurrentBalance(40000L);
|
||||||
toAccount.setCurrentBalance(Long.valueOf(15000L));
|
toAccount.setCurrentBalance(15000L);
|
||||||
|
|
||||||
fromAccount.setType(fromType);
|
fromAccount.setType(fromType);
|
||||||
toAccount.setType(toType);
|
toAccount.setType(toType);
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import de.financer.config.FinancerConfig;
|
|||||||
import de.financer.model.Account;
|
import de.financer.model.Account;
|
||||||
import de.financer.model.RecurringTransaction;
|
import de.financer.model.RecurringTransaction;
|
||||||
import de.financer.service.RecurringTransactionService;
|
import de.financer.service.RecurringTransactionService;
|
||||||
import org.junit.Assert;
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
@@ -40,10 +39,10 @@ public class SendRecurringTransactionReminderTaskTest {
|
|||||||
public void test_sendReminder() {
|
public void test_sendReminder() {
|
||||||
// Arrange
|
// Arrange
|
||||||
final Collection<RecurringTransaction> recurringTransactions = Arrays.asList(
|
final Collection<RecurringTransaction> recurringTransactions = Arrays.asList(
|
||||||
createRecurringTransaction("Test booking 1", "Income", "accounts.bank", Long.valueOf(250000), true),
|
createRecurringTransaction("Test booking 1", "Income", "accounts.bank", 250000L, true),
|
||||||
createRecurringTransaction("Test booking 2", "Bank", "accounts.rent", Long.valueOf(41500), true),
|
createRecurringTransaction("Test booking 2", "Bank", "accounts.rent", 41500L, true),
|
||||||
createRecurringTransaction("Test booking 3", "Bank", "accounts.cash", Long.valueOf(5000), true),
|
createRecurringTransaction("Test booking 3", "Bank", "accounts.cash", 5000L, true),
|
||||||
createRecurringTransaction("Test booking 4", "Car", "accounts.car", Long.valueOf(1234), false)
|
createRecurringTransaction("Test booking 4", "Car", "accounts.car", 1234L, false)
|
||||||
);
|
);
|
||||||
|
|
||||||
Mockito.when(this.recurringTransactionService.getAllDueToday()).thenReturn(recurringTransactions);
|
Mockito.when(this.recurringTransactionService.getAllDueToday()).thenReturn(recurringTransactions);
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import org.springframework.boot.SpringApplication;
|
|||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||||
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
|
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
|
||||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
|
||||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
public class FinancerApplication extends SpringBootServletInitializer {
|
public class FinancerApplication extends SpringBootServletInitializer {
|
||||||
|
|||||||
@@ -12,18 +12,18 @@ public enum ChartType {
|
|||||||
EXPENSE_PERIOD_TOTALS_CURRENT_YEAR(false),
|
EXPENSE_PERIOD_TOTALS_CURRENT_YEAR(false),
|
||||||
EXPENSES_ALL_PERIODS_INLINE(true);
|
EXPENSES_ALL_PERIODS_INLINE(true);
|
||||||
|
|
||||||
private boolean inline;
|
private final boolean inline;
|
||||||
|
|
||||||
ChartType(boolean inline) {
|
ChartType(boolean inline) {
|
||||||
this.inline = inline;
|
this.inline = inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<String> valueList(boolean filterInline) {
|
public static List<String> valueList(boolean filterInline) {
|
||||||
return Arrays.stream(ChartType.values()).filter((ct) -> filterInline ? !ct.inline : true).map(ChartType::name)
|
return Arrays.stream(ChartType.values()).filter((ct) -> !filterInline || !ct.inline).map(ChartType::name)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ChartType getByValue(String value) {
|
public static ChartType getByValue(String value) {
|
||||||
return Arrays.asList(values()).stream().filter((ct) -> ct.name().equals(value)).findFirst().get();
|
return Arrays.stream(values()).filter((ct) -> ct.name().equals(value)).findFirst().get();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ package de.financer.chart.impl.expense;
|
|||||||
|
|
||||||
import de.financer.config.FinancerConfig;
|
import de.financer.config.FinancerConfig;
|
||||||
import de.financer.dto.AccountExpense;
|
import de.financer.dto.AccountExpense;
|
||||||
import de.financer.dto.AccountGroupExpense;
|
|
||||||
import de.financer.template.GetAccountExpensesCurrentExpensePeriodTemplate;
|
import de.financer.template.GetAccountExpensesCurrentExpensePeriodTemplate;
|
||||||
import de.financer.template.GetAccountExpensesTemplate;
|
import de.financer.template.GetAccountExpensesTemplate;
|
||||||
import de.financer.template.GetAccountGroupExpensesTemplate;
|
|
||||||
import org.apache.commons.collections4.IterableUtils;
|
import org.apache.commons.collections4.IterableUtils;
|
||||||
import org.jfree.data.general.DefaultPieDataset;
|
import org.jfree.data.general.DefaultPieDataset;
|
||||||
import org.jfree.data.general.PieDataset;
|
import org.jfree.data.general.PieDataset;
|
||||||
@@ -24,7 +22,7 @@ public class AccountExpensesGenerator extends AbstractExpensesGenerator {
|
|||||||
|
|
||||||
final DefaultPieDataset dataSet = new DefaultPieDataset();
|
final DefaultPieDataset dataSet = new DefaultPieDataset();
|
||||||
|
|
||||||
IterableUtils.toList(expenses).stream()
|
IterableUtils.toList(expenses)
|
||||||
.forEach((ex) -> dataSet.setValue(ex.getAccount().getKey(), (ex.getExpense() / 100D)));
|
.forEach((ex) -> dataSet.setValue(ex.getAccount().getKey(), (ex.getExpense() / 100D)));
|
||||||
|
|
||||||
return dataSet;
|
return dataSet;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public class AccountGroupExpensesGenerator extends AbstractExpensesGenerator {
|
|||||||
|
|
||||||
final DefaultPieDataset dataSet = new DefaultPieDataset();
|
final DefaultPieDataset dataSet = new DefaultPieDataset();
|
||||||
|
|
||||||
IterableUtils.toList(expenses).stream()
|
IterableUtils.toList(expenses)
|
||||||
.forEach((ex) -> dataSet.setValue(ex.getAccountGroup().getName(), (ex.getExpense() / 100D)));
|
.forEach((ex) -> dataSet.setValue(ex.getAccountGroup().getName(), (ex.getExpense() / 100D)));
|
||||||
|
|
||||||
return dataSet;
|
return dataSet;
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ public class ExpensesAllPeriodsGenerator extends AbstractChartGenerator<EmptyPar
|
|||||||
final List<Long> totalData = new GetExpensesAllPeriodsTemplate().exchange(this.getFinancerConfig()).getBody();
|
final List<Long> totalData = new GetExpensesAllPeriodsTemplate().exchange(this.getFinancerConfig()).getBody();
|
||||||
final AtomicInteger counter = new AtomicInteger();
|
final AtomicInteger counter = new AtomicInteger();
|
||||||
|
|
||||||
totalData.stream().forEach((l) -> result.addValue(l, "c", "r" + counter.incrementAndGet()));
|
totalData.forEach((l) -> result.addValue(l, "c", "r" + counter.incrementAndGet()));
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ public class PeriodTotalGenerator extends AbstractChartGenerator<PeriodTotalPara
|
|||||||
|
|
||||||
IterableUtils.toList(totalData).stream()
|
IterableUtils.toList(totalData).stream()
|
||||||
.sorted(Comparator.comparing((ExpensePeriodTotal ept) -> ept.getPeriod().getStart())
|
.sorted(Comparator.comparing((ExpensePeriodTotal ept) -> ept.getPeriod().getStart())
|
||||||
.thenComparing((ExpensePeriodTotal ept) -> ept.getType()))
|
.thenComparing(ExpensePeriodTotal::getType))
|
||||||
.forEach((ept) -> result.addValue((ept.getTotal() / 100D),
|
.forEach((ept) -> result.addValue((ept.getTotal() / 100D),
|
||||||
this.getMessage("financer.account-type." + ept.getType()),
|
this.getMessage("financer.account-type." + ept.getType()),
|
||||||
formatDateY(ept.getPeriod())));
|
formatDateY(ept.getPeriod())));
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ package de.financer.controller;
|
|||||||
import de.financer.ResponseReason;
|
import de.financer.ResponseReason;
|
||||||
import de.financer.config.FinancerConfig;
|
import de.financer.config.FinancerConfig;
|
||||||
import de.financer.decorator.AccountDecorator;
|
import de.financer.decorator.AccountDecorator;
|
||||||
import de.financer.template.*;
|
import de.financer.dto.Order;
|
||||||
|
import de.financer.dto.SearchTransactionsResponseDto;
|
||||||
import de.financer.form.NewAccountForm;
|
import de.financer.form.NewAccountForm;
|
||||||
import de.financer.model.*;
|
import de.financer.model.*;
|
||||||
|
import de.financer.template.*;
|
||||||
|
import de.financer.template.exception.FinancerRestException;
|
||||||
import de.financer.util.ControllerUtils;
|
import de.financer.util.ControllerUtils;
|
||||||
import de.financer.util.TransactionUtils;
|
import de.financer.util.TransactionUtils;
|
||||||
import de.financer.util.comparator.TransactionByDateByIdDescComparator;
|
|
||||||
import org.apache.commons.collections4.IterableUtils;
|
import org.apache.commons.collections4.IterableUtils;
|
||||||
import org.apache.commons.lang3.BooleanUtils;
|
import org.apache.commons.lang3.BooleanUtils;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -57,7 +59,7 @@ public class AccountController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Iterable<AccountDecorator> decorateAccounts(List<Account> accounts) {
|
private Iterable<AccountDecorator> decorateAccounts(List<Account> accounts) {
|
||||||
return accounts.stream().map((a) -> new AccountDecorator(a)).collect(Collectors.toList());
|
return accounts.stream().map(AccountDecorator::new).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/newAccount")
|
@GetMapping("/newAccount")
|
||||||
@@ -102,21 +104,40 @@ public class AccountController {
|
|||||||
return "redirect:/accountOverview";
|
return "redirect:/accountOverview";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void _accountDetails(String key, Model model, FinancerConfig financerConfig) {
|
||||||
|
try {
|
||||||
|
final Iterable<SearchTransactionsResponseDto> response =
|
||||||
|
new SearchTransactionsTemplate()
|
||||||
|
.exchangeGet(financerConfig, key, key, null, null,
|
||||||
|
Order.TRANSACTIONS_BY_DATE_DESC, null, false);
|
||||||
|
|
||||||
|
final List<SearchTransactionsResponseDto> transactions = IterableUtils.toList(response);
|
||||||
|
final Account account = IterableUtils.toList(response).stream()
|
||||||
|
.filter(t -> t.getFromAccount().getKey().equals(key))
|
||||||
|
.findFirst()
|
||||||
|
.map(SearchTransactionsResponseDto::getFromAccount)
|
||||||
|
.orElseGet(() -> transactions
|
||||||
|
.stream()
|
||||||
|
.filter(t -> t.getToAccount().getKey().equals(key))
|
||||||
|
.findFirst()
|
||||||
|
.map(SearchTransactionsResponseDto::getToAccount).get());
|
||||||
|
|
||||||
|
transactions.forEach((t) -> TransactionUtils.adjustAmount(t, account));
|
||||||
|
|
||||||
|
model.addAttribute("account", account);
|
||||||
|
model.addAttribute("transactions", transactions);
|
||||||
|
model.addAttribute("showActions", true);
|
||||||
|
model.addAttribute("isClosed", AccountStatus.CLOSED.equals(account.getStatus()));
|
||||||
|
}
|
||||||
|
catch(FinancerRestException e) {
|
||||||
|
// TODO probably leads to follow-up exceptions during thymeleaf parsing as relevant information are missing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/accountDetails")
|
@GetMapping("/accountDetails")
|
||||||
public String accountDetails(String key, Model model) {
|
public String accountDetails(String key, Model model) {
|
||||||
final ResponseEntity<Account> response = new GetAccountByKeyTemplate().exchange(this.financerConfig, key);
|
_accountDetails(key, model, this.financerConfig);
|
||||||
final Account account = response.getBody();
|
|
||||||
final ResponseEntity<Iterable<Transaction>> transactionResponse = new GetAllTransactionsForAccountTemplate()
|
|
||||||
.exchange(this.financerConfig, account.getKey());
|
|
||||||
|
|
||||||
List<Transaction> transactions = IterableUtils.toList(transactionResponse.getBody());
|
|
||||||
|
|
||||||
transactions.sort(new TransactionByDateByIdDescComparator());
|
|
||||||
transactions.stream().forEach((t) -> TransactionUtils.adjustAmount(t, account));
|
|
||||||
|
|
||||||
model.addAttribute("account", account);
|
|
||||||
model.addAttribute("transactions", transactions);
|
|
||||||
model.addAttribute("isClosed", AccountStatus.CLOSED.equals(account.getStatus()));
|
|
||||||
ControllerUtils.addVersionAttribute(model, this.financerConfig);
|
ControllerUtils.addVersionAttribute(model, this.financerConfig);
|
||||||
ControllerUtils.addCurrencySymbol(model, this.financerConfig);
|
ControllerUtils.addCurrencySymbol(model, this.financerConfig);
|
||||||
|
|
||||||
@@ -134,19 +155,8 @@ public class AccountController {
|
|||||||
final ResponseReason responseReason = ResponseReason.fromResponseEntity(closeResponse);
|
final ResponseReason responseReason = ResponseReason.fromResponseEntity(closeResponse);
|
||||||
|
|
||||||
if (!ResponseReason.OK.equals(responseReason)) {
|
if (!ResponseReason.OK.equals(responseReason)) {
|
||||||
final ResponseEntity<Account> response = new GetAccountByKeyTemplate().exchange(this.financerConfig, key);
|
_accountDetails(key, model, this.financerConfig);
|
||||||
final Account account = response.getBody();
|
|
||||||
final ResponseEntity<Iterable<Transaction>> transactionResponse = new GetAllTransactionsForAccountTemplate()
|
|
||||||
.exchange(this.financerConfig, account.getKey());
|
|
||||||
|
|
||||||
List<Transaction> transactions = IterableUtils.toList(transactionResponse.getBody());
|
|
||||||
|
|
||||||
transactions.sort(new TransactionByDateByIdDescComparator());
|
|
||||||
transactions.stream().forEach((t) -> TransactionUtils.adjustAmount(t, account));
|
|
||||||
|
|
||||||
model.addAttribute("account", account);
|
|
||||||
model.addAttribute("transactions", transactions);
|
|
||||||
model.addAttribute("isClosed", AccountStatus.CLOSED.equals(account.getStatus()));
|
|
||||||
model.addAttribute("errorMessage", responseReason.name());
|
model.addAttribute("errorMessage", responseReason.name());
|
||||||
ControllerUtils.addVersionAttribute(model, this.financerConfig);
|
ControllerUtils.addVersionAttribute(model, this.financerConfig);
|
||||||
ControllerUtils.addCurrencySymbol(model, this.financerConfig);
|
ControllerUtils.addCurrencySymbol(model, this.financerConfig);
|
||||||
@@ -168,19 +178,8 @@ public class AccountController {
|
|||||||
final ResponseReason responseReason = ResponseReason.fromResponseEntity(closeResponse);
|
final ResponseReason responseReason = ResponseReason.fromResponseEntity(closeResponse);
|
||||||
|
|
||||||
if (!ResponseReason.OK.equals(responseReason)) {
|
if (!ResponseReason.OK.equals(responseReason)) {
|
||||||
final ResponseEntity<Account> response = new GetAccountByKeyTemplate().exchange(this.financerConfig, key);
|
_accountDetails(key, model, this.financerConfig);
|
||||||
final Account account = response.getBody();
|
|
||||||
final ResponseEntity<Iterable<Transaction>> transactionResponse = new GetAllTransactionsForAccountTemplate()
|
|
||||||
.exchange(this.financerConfig, account.getKey());
|
|
||||||
|
|
||||||
List<Transaction> transactions = IterableUtils.toList(transactionResponse.getBody());
|
|
||||||
|
|
||||||
transactions.sort(new TransactionByDateByIdDescComparator());
|
|
||||||
transactions.stream().forEach((t) -> TransactionUtils.adjustAmount(t, account));
|
|
||||||
|
|
||||||
model.addAttribute("account", account);
|
|
||||||
model.addAttribute("transactions", transactions);
|
|
||||||
model.addAttribute("isClosed", AccountStatus.CLOSED.equals(account.getStatus()));
|
|
||||||
model.addAttribute("errorMessage", responseReason.name());
|
model.addAttribute("errorMessage", responseReason.name());
|
||||||
ControllerUtils.addVersionAttribute(model, this.financerConfig);
|
ControllerUtils.addVersionAttribute(model, this.financerConfig);
|
||||||
ControllerUtils.addCurrencySymbol(model, this.financerConfig);
|
ControllerUtils.addCurrencySymbol(model, this.financerConfig);
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ public enum Function {
|
|||||||
ACC_GP_GET_ACC_GP_EXPENSES("accountGroups/getAccountGroupExpenses"),
|
ACC_GP_GET_ACC_GP_EXPENSES("accountGroups/getAccountGroupExpenses"),
|
||||||
ACC_GP_GET_ACC_GP_EXPENSES_CURRENT_EXPENSE_PERIOD("accountGroups/getAccountGroupExpensesCurrentExpensePeriod"),
|
ACC_GP_GET_ACC_GP_EXPENSES_CURRENT_EXPENSE_PERIOD("accountGroups/getAccountGroupExpensesCurrentExpensePeriod"),
|
||||||
|
|
||||||
TR_GET_ALL("transactions/getAll"),
|
TR_SEARCH("transactions"),
|
||||||
TR_GET_ALL_FOR_ACCOUNT("transactions/getAllForAccount"),
|
TR_SEARCH_BY_FQL("transactionsByFql"),
|
||||||
TR_CREATE_TRANSACTION("transactions/createTransaction"),
|
TR_CREATE_TRANSACTION("transactions"),
|
||||||
TR_DELETE_TRANSACTION("transactions/deleteTransaction"),
|
TR_DELETE_TRANSACTION("transactions/"),
|
||||||
TR_EXPENSES_CURRENT_PERIOD("transactions/getExpensesCurrentPeriod"),
|
TR_EXPENSES_CURRENT_PERIOD("transactions/getExpensesCurrentPeriod"),
|
||||||
TR_EXPENSE_PERIOD_TOTALS("transactions/getExpensePeriodTotals"),
|
TR_EXPENSE_PERIOD_TOTALS("transactions/getExpensePeriodTotals"),
|
||||||
TR_EXPENSES_ALL_PERIODS("transactions/getExpensesAllPeriods"),
|
TR_EXPENSES_ALL_PERIODS("transactions/getExpensesAllPeriods"),
|
||||||
@@ -34,7 +34,7 @@ public enum Function {
|
|||||||
P_GET_CURRENT_EXPENSE_PERIOD("periods/getCurrentExpensePeriod"),
|
P_GET_CURRENT_EXPENSE_PERIOD("periods/getCurrentExpensePeriod"),
|
||||||
P_CLOSE_CURRENT_EXPENSE_PERIOD("periods/closeCurrentExpensePeriod");
|
P_CLOSE_CURRENT_EXPENSE_PERIOD("periods/closeCurrentExpensePeriod");
|
||||||
|
|
||||||
private String path;
|
private final String path;
|
||||||
|
|
||||||
Function(String path) {
|
Function(String path) {
|
||||||
this.path = path;
|
this.path = path;
|
||||||
|
|||||||
@@ -2,21 +2,17 @@ package de.financer.controller;
|
|||||||
|
|
||||||
import de.financer.ResponseReason;
|
import de.financer.ResponseReason;
|
||||||
import de.financer.config.FinancerConfig;
|
import de.financer.config.FinancerConfig;
|
||||||
|
import de.financer.dto.SaveTransactionRequestDto;
|
||||||
|
import de.financer.dto.SearchTransactionsResponseDto;
|
||||||
|
import de.financer.form.SearchTransactionsForm;
|
||||||
import de.financer.model.AccountType;
|
import de.financer.model.AccountType;
|
||||||
import de.financer.template.GetAccountByKeyTemplate;
|
import de.financer.template.*;
|
||||||
import de.financer.template.GetAllAccountsTemplate;
|
|
||||||
import de.financer.template.GetAllTransactionsForAccountTemplate;
|
|
||||||
import de.financer.template.StringTemplate;
|
|
||||||
import de.financer.form.NewTransactionForm;
|
import de.financer.form.NewTransactionForm;
|
||||||
import de.financer.model.Account;
|
import de.financer.model.Account;
|
||||||
import de.financer.model.AccountStatus;
|
import de.financer.template.exception.FinancerRestException;
|
||||||
import de.financer.model.Transaction;
|
|
||||||
import de.financer.util.ControllerUtils;
|
import de.financer.util.ControllerUtils;
|
||||||
import de.financer.util.TransactionUtils;
|
|
||||||
import de.financer.util.comparator.TransactionByDateByIdDescComparator;
|
|
||||||
import org.apache.commons.collections4.IterableUtils;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.core.ParameterizedTypeReference;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
@@ -24,6 +20,7 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.util.UriComponentsBuilder;
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@@ -32,20 +29,77 @@ public class TransactionController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private FinancerConfig financerConfig;
|
private FinancerConfig financerConfig;
|
||||||
|
|
||||||
|
@GetMapping("/searchTransactions")
|
||||||
|
public String searchTransaction(Model model) {
|
||||||
|
model.addAttribute("form", new SearchTransactionsForm());
|
||||||
|
model.addAttribute("showActions", false);
|
||||||
|
ControllerUtils.addVersionAttribute(model, this.financerConfig);
|
||||||
|
ControllerUtils.addCurrencySymbol(model, this.financerConfig);
|
||||||
|
|
||||||
|
return "transaction/searchTransactions";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/searchTransactions")
|
||||||
|
public String searchTransactions(SearchTransactionsForm form, Model model) {
|
||||||
|
try {
|
||||||
|
final UriComponentsBuilder transactionBuilder = UriComponentsBuilder
|
||||||
|
.fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.TR_SEARCH_BY_FQL));
|
||||||
|
|
||||||
|
transactionBuilder.queryParam("fql", form.getFql());
|
||||||
|
|
||||||
|
final Iterable<SearchTransactionsResponseDto> trxs = FinancerRestTemplate.exchangeGet(transactionBuilder,
|
||||||
|
new ParameterizedTypeReference<Iterable<SearchTransactionsResponseDto>>() {
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
model.addAttribute("transactions", trxs);
|
||||||
|
}
|
||||||
|
catch(FinancerRestException e) {
|
||||||
|
// TODO
|
||||||
|
model.addAttribute("errorMessage", e.getResponseReason().name());
|
||||||
|
}
|
||||||
|
|
||||||
|
model.addAttribute("form", form);
|
||||||
|
model.addAttribute("showActions", false);
|
||||||
|
ControllerUtils.addVersionAttribute(model, this.financerConfig);
|
||||||
|
ControllerUtils.addCurrencySymbol(model, this.financerConfig);
|
||||||
|
|
||||||
|
return "transaction/searchTransactions";
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/newTransaction")
|
@GetMapping("/newTransaction")
|
||||||
public String newTransaction(Model model) {
|
public String newTransaction(Model model) {
|
||||||
final ResponseEntity<Iterable<Account>> response = new GetAllAccountsTemplate().exchange(this.financerConfig);
|
return _newTransaction(model, new NewTransactionForm(), Optional.empty());
|
||||||
final List<Account> fromAccounts = ControllerUtils.filterAndSortAccounts(response.getBody()).stream()
|
}
|
||||||
.filter((a) -> a.getType() != AccountType.EXPENSE)
|
|
||||||
.collect(Collectors.toList());
|
private String _newTransaction(Model model, NewTransactionForm form, Optional<ResponseReason> responseReason) {
|
||||||
final List<Account> toAccounts = ControllerUtils.filterAndSortAccounts(response.getBody()).stream()
|
try {
|
||||||
.filter((a) -> a.getType() != AccountType.INCOME && a
|
final Iterable<Account> allAccounts =
|
||||||
.getType() != AccountType.START)
|
FinancerRestTemplate.exchangeGet(this.financerConfig,
|
||||||
.collect(Collectors.toList());
|
Function.ACC_GET_ALL,
|
||||||
|
new ParameterizedTypeReference<Iterable<Account>>() {});
|
||||||
|
|
||||||
|
final List<Account> fromAccounts = ControllerUtils.filterAndSortAccounts(allAccounts).stream()
|
||||||
|
.filter((a) -> a.getType() != AccountType.EXPENSE)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
final List<Account> toAccounts = ControllerUtils.filterAndSortAccounts(allAccounts).stream()
|
||||||
|
.filter((a) -> a.getType() != AccountType.INCOME && a
|
||||||
|
.getType() != AccountType.START)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
model.addAttribute("fromAccounts", fromAccounts);
|
||||||
|
model.addAttribute("toAccounts", toAccounts);
|
||||||
|
}
|
||||||
|
catch(FinancerRestException e) {
|
||||||
|
// Nothing to do
|
||||||
|
// This is very unlikely to happen and if it happens the account selection stays empty, so the user
|
||||||
|
// cannot create a transaction anyway and is forced to reload the page or navigate back
|
||||||
|
}
|
||||||
|
|
||||||
|
model.addAttribute("form", form);
|
||||||
|
|
||||||
|
responseReason.ifPresent(rr -> model.addAttribute("errorMessage", rr.name()));
|
||||||
|
|
||||||
model.addAttribute("fromAccounts", fromAccounts);
|
|
||||||
model.addAttribute("toAccounts", toAccounts);
|
|
||||||
model.addAttribute("form", new NewTransactionForm());
|
|
||||||
ControllerUtils.addVersionAttribute(model, this.financerConfig);
|
ControllerUtils.addVersionAttribute(model, this.financerConfig);
|
||||||
ControllerUtils.addCurrencySymbol(model, this.financerConfig);
|
ControllerUtils.addCurrencySymbol(model, this.financerConfig);
|
||||||
|
|
||||||
@@ -54,36 +108,20 @@ public class TransactionController {
|
|||||||
|
|
||||||
@PostMapping("/saveTransaction")
|
@PostMapping("/saveTransaction")
|
||||||
public String saveTransaction(NewTransactionForm form, Model model) {
|
public String saveTransaction(NewTransactionForm form, Model model) {
|
||||||
final UriComponentsBuilder builder = UriComponentsBuilder
|
final SaveTransactionRequestDto requestDto = new SaveTransactionRequestDto();
|
||||||
.fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.TR_CREATE_TRANSACTION))
|
|
||||||
.queryParam("fromAccountKey", form.getFromAccountKey())
|
|
||||||
.queryParam("toAccountKey", form.getToAccountKey())
|
|
||||||
.queryParam("amount", form.getAmount())
|
|
||||||
.queryParam("date", ControllerUtils.formatDate(this.financerConfig, form.getDate()))
|
|
||||||
.queryParam("description", form.getDescription())
|
|
||||||
.queryParam("taxRelevant", form.getTaxRelevant());
|
|
||||||
|
|
||||||
final ResponseEntity<String> response = new StringTemplate().exchange(builder);
|
requestDto.setFromAccountKey(form.getFromAccountKey());
|
||||||
final ResponseReason responseReason = ResponseReason.fromResponseEntity(response);
|
requestDto.setToAccountKey(form.getToAccountKey());
|
||||||
|
requestDto.setAmount(form.getAmount());
|
||||||
|
requestDto.setDate(ControllerUtils.formatDate(this.financerConfig, form.getDate()));
|
||||||
|
requestDto.setDescription(form.getDescription());
|
||||||
|
requestDto.setTaxRelevant(form.getTaxRelevant());
|
||||||
|
|
||||||
if (!ResponseReason.OK.equals(responseReason)) {
|
final ResponseReason responseReason = FinancerRestTemplate
|
||||||
final ResponseEntity<Iterable<Account>> accRes = new GetAllAccountsTemplate().exchange(this.financerConfig);
|
.exchangePost(this.financerConfig, Function.TR_CREATE_TRANSACTION, requestDto);
|
||||||
final List<Account> fromAccounts = ControllerUtils.filterAndSortAccounts(accRes.getBody()).stream()
|
|
||||||
.filter((a) -> a.getType() != AccountType.EXPENSE)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
final List<Account> toAccounts = ControllerUtils.filterAndSortAccounts(accRes.getBody()).stream()
|
|
||||||
.filter((a) -> a.getType() != AccountType.INCOME && a
|
|
||||||
.getType() != AccountType.START)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
model.addAttribute("fromAccounts", fromAccounts);
|
if (!ResponseReason.CREATED.equals(responseReason)) {
|
||||||
model.addAttribute("toAccounts", toAccounts);
|
return _newTransaction(model, form, Optional.of(responseReason));
|
||||||
model.addAttribute("form", form);
|
|
||||||
model.addAttribute("errorMessage", responseReason.name());
|
|
||||||
ControllerUtils.addVersionAttribute(model, this.financerConfig);
|
|
||||||
ControllerUtils.addCurrencySymbol(model, this.financerConfig);
|
|
||||||
|
|
||||||
return "transaction/newTransaction";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return "redirect:/accountOverview";
|
return "redirect:/accountOverview";
|
||||||
@@ -91,34 +129,16 @@ public class TransactionController {
|
|||||||
|
|
||||||
@GetMapping("/deleteTransaction")
|
@GetMapping("/deleteTransaction")
|
||||||
public String deleteTransaction(String transactionId, String accountKey, Model model) {
|
public String deleteTransaction(String transactionId, String accountKey, Model model) {
|
||||||
final UriComponentsBuilder builder = UriComponentsBuilder
|
final ResponseReason responseReason = FinancerRestTemplate
|
||||||
.fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.TR_DELETE_TRANSACTION))
|
.exchangeDelete(this.financerConfig, Function.TR_DELETE_TRANSACTION, transactionId);
|
||||||
.queryParam("transactionId", transactionId);
|
|
||||||
|
|
||||||
final ResponseEntity<String> response = new StringTemplate().exchange(builder);
|
AccountController._accountDetails(accountKey, model, this.financerConfig);
|
||||||
final ResponseReason responseReason = ResponseReason.fromResponseEntity(response);
|
|
||||||
|
|
||||||
final ResponseEntity<Account> accountResponse = new GetAccountByKeyTemplate()
|
|
||||||
.exchange(this.financerConfig, accountKey);
|
|
||||||
final Account account = accountResponse.getBody();
|
|
||||||
final ResponseEntity<Iterable<Transaction>> transactionResponse = new GetAllTransactionsForAccountTemplate()
|
|
||||||
.exchange(this.financerConfig, account.getKey());
|
|
||||||
|
|
||||||
List<Transaction> transactions = IterableUtils.toList(transactionResponse.getBody());
|
|
||||||
|
|
||||||
transactions.sort(new TransactionByDateByIdDescComparator());
|
|
||||||
transactions.stream().forEach((t) -> TransactionUtils.adjustAmount(t, account));
|
|
||||||
|
|
||||||
model.addAttribute("account", account);
|
|
||||||
model.addAttribute("transactions", transactions);
|
|
||||||
model.addAttribute("isClosed", AccountStatus.CLOSED.equals(account.getStatus()));
|
|
||||||
ControllerUtils.addVersionAttribute(model, this.financerConfig);
|
ControllerUtils.addVersionAttribute(model, this.financerConfig);
|
||||||
ControllerUtils.addCurrencySymbol(model, this.financerConfig);
|
ControllerUtils.addCurrencySymbol(model, this.financerConfig);
|
||||||
|
|
||||||
if (!ResponseReason.OK.equals(responseReason)) {
|
if (!ResponseReason.OK.equals(responseReason)) {
|
||||||
model.addAttribute("errorMessage", responseReason.name());
|
model.addAttribute("errorMessage", responseReason.name());
|
||||||
|
|
||||||
return "account/accountDetails";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return "account/accountDetails";
|
return "account/accountDetails";
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
package de.financer.controller.handler;
|
|
||||||
|
|
||||||
import org.springframework.http.client.ClientHttpResponse;
|
|
||||||
import org.springframework.web.client.ResponseErrorHandler;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
public class NoExceptionResponseErrorHandler implements ResponseErrorHandler {
|
|
||||||
@Override
|
|
||||||
public boolean hasError(ClientHttpResponse response) throws IOException {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleError(ClientHttpResponse response) throws IOException {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -41,7 +41,7 @@ public class AccountDecorator {
|
|||||||
.findFirst()
|
.findFirst()
|
||||||
.orElseGet(() -> {
|
.orElseGet(() -> {
|
||||||
AccountStatistic as = new AccountStatistic();
|
AccountStatistic as = new AccountStatistic();
|
||||||
as.setTransactionCountTo(0l);
|
as.setTransactionCountTo(0L);
|
||||||
return as;
|
return as;
|
||||||
})
|
})
|
||||||
.getSpendingTotalTo();
|
.getSpendingTotalTo();
|
||||||
@@ -55,9 +55,9 @@ public class AccountDecorator {
|
|||||||
return Math.round(this.account.getAccountStatistics().stream()
|
return Math.round(this.account.getAccountStatistics().stream()
|
||||||
.filter((as) -> as.getPeriod().getType().equals(PeriodType.EXPENSE) && as.getPeriod()
|
.filter((as) -> as.getPeriod().getType().equals(PeriodType.EXPENSE) && as.getPeriod()
|
||||||
.getEnd() != null)
|
.getEnd() != null)
|
||||||
.mapToLong((as) -> as.getSpendingTotalTo())
|
.mapToLong(AccountStatistic::getSpendingTotalTo)
|
||||||
.average()
|
.average()
|
||||||
.orElseGet(() -> Double.valueOf(0d)));
|
.orElseGet(() -> 0d));
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package de.financer.form;
|
||||||
|
|
||||||
|
public class SearchTransactionsForm {
|
||||||
|
private String fql;
|
||||||
|
|
||||||
|
public String getFql() {
|
||||||
|
return fql;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFql(String fql) {
|
||||||
|
this.fql = fql;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,133 @@
|
|||||||
package de.financer.template;
|
package de.financer.template;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
import de.financer.ResponseReason;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import de.financer.config.FinancerConfig;
|
||||||
import de.financer.controller.handler.NoExceptionResponseErrorHandler;
|
import de.financer.controller.Function;
|
||||||
|
import de.financer.template.exception.FinancerRestException;
|
||||||
|
import de.financer.util.ControllerUtils;
|
||||||
import org.springframework.core.ParameterizedTypeReference;
|
import org.springframework.core.ParameterizedTypeReference;
|
||||||
|
import org.springframework.http.HttpEntity;
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
import org.springframework.web.client.HttpStatusCodeException;
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
|
||||||
public class FinancerRestTemplate<T> {
|
public class FinancerRestTemplate<T> {
|
||||||
public ResponseEntity<T> exchange(String url, ParameterizedTypeReference<T> type) {
|
// Requests that result in the return of a String allow simplified error handling
|
||||||
|
|
||||||
|
private static <R, B> ResponseEntity<R> _exchangePost(FinancerConfig financerConfig,
|
||||||
|
Function endpoint,
|
||||||
|
B body,
|
||||||
|
Class<R> type) throws FinancerRestException {
|
||||||
|
final UriComponentsBuilder builder = UriComponentsBuilder
|
||||||
|
.fromHttpUrl(ControllerUtils.buildUrl(financerConfig, endpoint));
|
||||||
final RestTemplate restTemplate = new RestTemplate();
|
final RestTemplate restTemplate = new RestTemplate();
|
||||||
|
|
||||||
restTemplate.setErrorHandler(new NoExceptionResponseErrorHandler());
|
try {
|
||||||
|
return restTemplate.exchange(builder.toUriString(), HttpMethod.POST, new HttpEntity<>(body), type);
|
||||||
|
} catch (HttpStatusCodeException e) {
|
||||||
|
final ResponseEntity<String> exceptionResponse = new ResponseEntity<>(e.getResponseBodyAsString(), e
|
||||||
|
.getStatusCode());
|
||||||
|
|
||||||
|
throw new FinancerRestException(ResponseReason.fromResponseEntity(exceptionResponse));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <R, B> R exchangePost(FinancerConfig financerConfig,
|
||||||
|
Function endpoint,
|
||||||
|
B body,
|
||||||
|
Class<R> type) throws FinancerRestException {
|
||||||
|
return FinancerRestTemplate._exchangePost(financerConfig, endpoint, body, type).getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <B> ResponseReason exchangePost(FinancerConfig financerConfig,
|
||||||
|
Function endpoint,
|
||||||
|
B body) {
|
||||||
|
ResponseReason response;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final ResponseEntity<String> responseEntity = FinancerRestTemplate
|
||||||
|
._exchangePost(financerConfig, endpoint, body, String.class);
|
||||||
|
|
||||||
|
response = ResponseReason.fromResponseEntity(responseEntity);
|
||||||
|
} catch (FinancerRestException e) {
|
||||||
|
response = e.getResponseReason();
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ResponseReason exchangeDelete(FinancerConfig financerConfig,
|
||||||
|
Function endpoint,
|
||||||
|
String id
|
||||||
|
) {
|
||||||
|
final UriComponentsBuilder builder = UriComponentsBuilder
|
||||||
|
.fromHttpUrl(ControllerUtils.buildUrl(financerConfig, endpoint, id));
|
||||||
|
final RestTemplate restTemplate = new RestTemplate();
|
||||||
|
ResponseEntity<String> response;
|
||||||
|
|
||||||
|
try {
|
||||||
|
response = restTemplate.exchange(builder.toUriString(), HttpMethod.DELETE, null, String.class);
|
||||||
|
} catch (HttpStatusCodeException e) {
|
||||||
|
response = new ResponseEntity<>(e.getResponseBodyAsString(), e.getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseReason.fromResponseEntity(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <R> R exchangeGet(FinancerConfig financerConfig,
|
||||||
|
Function endpoint,
|
||||||
|
Class<R> type) throws FinancerRestException {
|
||||||
|
final UriComponentsBuilder builder = UriComponentsBuilder
|
||||||
|
.fromHttpUrl(ControllerUtils.buildUrl(financerConfig, endpoint));
|
||||||
|
final RestTemplate restTemplate = new RestTemplate();
|
||||||
|
|
||||||
|
try {
|
||||||
|
return restTemplate.exchange(builder.toUriString(), HttpMethod.GET, null, type).getBody();
|
||||||
|
} catch (HttpStatusCodeException e) {
|
||||||
|
final ResponseEntity<String> exceptionResponse = new ResponseEntity<>(e.getResponseBodyAsString(), e
|
||||||
|
.getStatusCode());
|
||||||
|
|
||||||
|
throw new FinancerRestException(ResponseReason.fromResponseEntity(exceptionResponse));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <R> R exchangeGet(FinancerConfig financerConfig,
|
||||||
|
Function endpoint,
|
||||||
|
ParameterizedTypeReference<R> type) throws FinancerRestException {
|
||||||
|
final UriComponentsBuilder builder = UriComponentsBuilder
|
||||||
|
.fromHttpUrl(ControllerUtils.buildUrl(financerConfig, endpoint));
|
||||||
|
final RestTemplate restTemplate = new RestTemplate();
|
||||||
|
|
||||||
|
try {
|
||||||
|
return restTemplate.exchange(builder.toUriString(), HttpMethod.GET, null, type).getBody();
|
||||||
|
} catch (HttpStatusCodeException e) {
|
||||||
|
final ResponseEntity<String> exceptionResponse = new ResponseEntity<>(e.getResponseBodyAsString(), e
|
||||||
|
.getStatusCode());
|
||||||
|
|
||||||
|
throw new FinancerRestException(ResponseReason.fromResponseEntity(exceptionResponse));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <R> R exchangeGet(UriComponentsBuilder builder,
|
||||||
|
ParameterizedTypeReference<R> type) throws FinancerRestException {
|
||||||
|
final RestTemplate restTemplate = new RestTemplate();
|
||||||
|
|
||||||
|
try {
|
||||||
|
return restTemplate.exchange(builder.toUriString(), HttpMethod.GET, null, type).getBody();
|
||||||
|
} catch (HttpStatusCodeException e) {
|
||||||
|
final ResponseEntity<String> exceptionResponse = new ResponseEntity<>(e.getResponseBodyAsString(), e
|
||||||
|
.getStatusCode());
|
||||||
|
|
||||||
|
throw new FinancerRestException(ResponseReason.fromResponseEntity(exceptionResponse));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------- LEGACY
|
||||||
|
|
||||||
|
public ResponseEntity<T> exchange(String url, ParameterizedTypeReference<T> type) {
|
||||||
|
final RestTemplate restTemplate = new RestTemplate();
|
||||||
|
|
||||||
return restTemplate.exchange(url, HttpMethod.GET, null, type);
|
return restTemplate.exchange(url, HttpMethod.GET, null, type);
|
||||||
}
|
}
|
||||||
@@ -21,22 +135,6 @@ public class FinancerRestTemplate<T> {
|
|||||||
public ResponseEntity<T> exchange(String url, Class<T> type) {
|
public ResponseEntity<T> exchange(String url, Class<T> type) {
|
||||||
final RestTemplate restTemplate = new RestTemplate();
|
final RestTemplate restTemplate = new RestTemplate();
|
||||||
|
|
||||||
restTemplate.setErrorHandler(new NoExceptionResponseErrorHandler());
|
|
||||||
|
|
||||||
return restTemplate.exchange(url, HttpMethod.GET, null, type);
|
return restTemplate.exchange(url, HttpMethod.GET, null, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The spring.jackson. properties are not picked up by the RestTemplate and its converter,
|
|
||||||
// so we need to overwrite it here
|
|
||||||
private MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
|
|
||||||
MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
|
|
||||||
ObjectMapper objectMapper = new ObjectMapper();
|
|
||||||
|
|
||||||
objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
|
|
||||||
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
|
||||||
|
|
||||||
jsonConverter.setObjectMapper(objectMapper);
|
|
||||||
|
|
||||||
return jsonConverter;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package de.financer.template;
|
|||||||
import de.financer.config.FinancerConfig;
|
import de.financer.config.FinancerConfig;
|
||||||
import de.financer.controller.Function;
|
import de.financer.controller.Function;
|
||||||
import de.financer.dto.AccountExpense;
|
import de.financer.dto.AccountExpense;
|
||||||
import de.financer.dto.AccountGroupExpense;
|
|
||||||
import de.financer.util.ControllerUtils;
|
import de.financer.util.ControllerUtils;
|
||||||
import org.springframework.core.ParameterizedTypeReference;
|
import org.springframework.core.ParameterizedTypeReference;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ package de.financer.template;
|
|||||||
import de.financer.config.FinancerConfig;
|
import de.financer.config.FinancerConfig;
|
||||||
import de.financer.controller.Function;
|
import de.financer.controller.Function;
|
||||||
import de.financer.dto.AccountGroupExpense;
|
import de.financer.dto.AccountGroupExpense;
|
||||||
import de.financer.model.Account;
|
|
||||||
import de.financer.model.Transaction;
|
|
||||||
import de.financer.util.ControllerUtils;
|
import de.financer.util.ControllerUtils;
|
||||||
import org.springframework.core.ParameterizedTypeReference;
|
import org.springframework.core.ParameterizedTypeReference;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
package de.financer.template;
|
|
||||||
|
|
||||||
import de.financer.config.FinancerConfig;
|
|
||||||
import de.financer.controller.Function;
|
|
||||||
import de.financer.model.Transaction;
|
|
||||||
import de.financer.util.ControllerUtils;
|
|
||||||
import org.springframework.core.ParameterizedTypeReference;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.util.UriComponentsBuilder;
|
|
||||||
|
|
||||||
public class GetAllTransactionsForAccountTemplate {
|
|
||||||
public ResponseEntity<Iterable<Transaction>> exchange(FinancerConfig financerConfig, String accountKey) {
|
|
||||||
final UriComponentsBuilder transactionBuilder = UriComponentsBuilder
|
|
||||||
.fromHttpUrl(ControllerUtils.buildUrl(financerConfig, Function.TR_GET_ALL_FOR_ACCOUNT))
|
|
||||||
.queryParam("accountKey", accountKey);
|
|
||||||
|
|
||||||
return new FinancerRestTemplate<Iterable<Transaction>>()
|
|
||||||
.exchange(transactionBuilder.toUriString(), new ParameterizedTypeReference<Iterable<Transaction>>() {
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ package de.financer.template;
|
|||||||
|
|
||||||
import de.financer.config.FinancerConfig;
|
import de.financer.config.FinancerConfig;
|
||||||
import de.financer.controller.Function;
|
import de.financer.controller.Function;
|
||||||
import de.financer.dto.ExpensePeriodTotal;
|
|
||||||
import de.financer.util.ControllerUtils;
|
import de.financer.util.ControllerUtils;
|
||||||
import org.springframework.core.ParameterizedTypeReference;
|
import org.springframework.core.ParameterizedTypeReference;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package de.financer.template;
|
||||||
|
|
||||||
|
import de.financer.config.FinancerConfig;
|
||||||
|
import de.financer.controller.Function;
|
||||||
|
import de.financer.dto.Order;
|
||||||
|
import de.financer.dto.SearchTransactionsResponseDto;
|
||||||
|
import de.financer.template.exception.FinancerRestException;
|
||||||
|
import de.financer.util.ControllerUtils;
|
||||||
|
import org.springframework.core.ParameterizedTypeReference;
|
||||||
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
|
||||||
|
public class SearchTransactionsTemplate {
|
||||||
|
public Iterable<SearchTransactionsResponseDto> exchangeGet(FinancerConfig financerConfig,
|
||||||
|
String fromAccountKey,
|
||||||
|
String toAccountKey,
|
||||||
|
Long periodId,
|
||||||
|
Integer limit,
|
||||||
|
Order order,
|
||||||
|
Boolean taxRelevant,
|
||||||
|
boolean accountsAnd) throws FinancerRestException {
|
||||||
|
final UriComponentsBuilder transactionBuilder = UriComponentsBuilder
|
||||||
|
.fromHttpUrl(ControllerUtils.buildUrl(financerConfig, Function.TR_SEARCH));
|
||||||
|
|
||||||
|
if (fromAccountKey != null) {
|
||||||
|
transactionBuilder.queryParam("fromAccountKey", fromAccountKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromAccountKey != null) {
|
||||||
|
transactionBuilder.queryParam("toAccountKey", toAccountKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (periodId != null) {
|
||||||
|
transactionBuilder.queryParam("periodId", periodId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (limit != null) {
|
||||||
|
transactionBuilder.queryParam("limit", limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (order != null) {
|
||||||
|
transactionBuilder.queryParam("order", order.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (taxRelevant != null) {
|
||||||
|
transactionBuilder.queryParam("taxRelevant", taxRelevant);
|
||||||
|
}
|
||||||
|
|
||||||
|
transactionBuilder.queryParam("accountsAnd", accountsAnd);
|
||||||
|
|
||||||
|
return FinancerRestTemplate.exchangeGet(transactionBuilder, new ParameterizedTypeReference<Iterable<SearchTransactionsResponseDto>>() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package de.financer.template.exception;
|
||||||
|
|
||||||
|
import de.financer.ResponseReason;
|
||||||
|
|
||||||
|
public class FinancerRestException extends Exception {
|
||||||
|
private final ResponseReason responseReason;
|
||||||
|
|
||||||
|
public FinancerRestException(ResponseReason responseReason) {
|
||||||
|
this.responseReason = responseReason;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResponseReason getResponseReason() {
|
||||||
|
return this.responseReason;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,10 @@ public class ControllerUtils {
|
|||||||
return String.format("%s%s", financerConfig.getServerUrl(), function.getPath());
|
return String.format("%s%s", financerConfig.getServerUrl(), function.getPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String buildUrl(FinancerConfig financerConfig, Function function, String id) {
|
||||||
|
return String.format("%s%s%s", financerConfig.getServerUrl(), function.getPath(), id);
|
||||||
|
}
|
||||||
|
|
||||||
public static List<Account> filterAndSortAccounts(Iterable<Account> accounts) {
|
public static List<Account> filterAndSortAccounts(Iterable<Account> accounts) {
|
||||||
return filterAndSortAccounts(accounts, false);
|
return filterAndSortAccounts(accounts, false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package de.financer.util;
|
package de.financer.util;
|
||||||
|
|
||||||
|
import de.financer.dto.SearchTransactionsResponseDto;
|
||||||
import de.financer.model.Account;
|
import de.financer.model.Account;
|
||||||
import de.financer.model.AccountType;
|
import de.financer.model.AccountType;
|
||||||
import de.financer.model.Transaction;
|
|
||||||
|
|
||||||
import static de.financer.model.AccountType.*;
|
import static de.financer.model.AccountType.*;
|
||||||
|
|
||||||
public class TransactionUtils {
|
public class TransactionUtils {
|
||||||
public static final void adjustAmount(Transaction t, Account account) {
|
public static void adjustAmount(SearchTransactionsResponseDto t, Account account) {
|
||||||
boolean isFrom = t.getFromAccount().getKey().equals(account.getKey());
|
boolean isFrom = t.getFromAccount().getKey().equals(account.getKey());
|
||||||
|
|
||||||
if (AccountType.START.equals(t.getFromAccount().getType()) && AccountType.LIABILITY.equals(t.getToAccount().getType())) {
|
if (AccountType.START.equals(t.getFromAccount().getType()) && AccountType.LIABILITY.equals(t.getToAccount().getType())) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ financer.account-overview.available-actions.show-closed=Show closed accounts
|
|||||||
financer.account-overview.available-actions.hide-closed=Hide closed accounts
|
financer.account-overview.available-actions.hide-closed=Hide closed accounts
|
||||||
financer.account-overview.available-actions.create-account=Create new account
|
financer.account-overview.available-actions.create-account=Create new account
|
||||||
financer.account-overview.available-actions.create-transaction=Create new transaction
|
financer.account-overview.available-actions.create-transaction=Create new transaction
|
||||||
|
financer.account-overview.available-actions.search-transactions=Search transactions
|
||||||
financer.account-overview.available-actions.create-recurring-transaction=Create new recurring transaction
|
financer.account-overview.available-actions.create-recurring-transaction=Create new recurring transaction
|
||||||
financer.account-overview.available-actions.recurring-transaction-all=Show all recurring transactions
|
financer.account-overview.available-actions.recurring-transaction-all=Show all recurring transactions
|
||||||
financer.account-overview.available-actions.create-account-group=Create new account group
|
financer.account-overview.available-actions.create-account-group=Create new account group
|
||||||
@@ -98,28 +99,34 @@ financer.account-details.available-actions.close-account=Close account
|
|||||||
financer.account-details.available-actions.open-account=Open account
|
financer.account-details.available-actions.open-account=Open account
|
||||||
financer.account-details.available-actions.back-to-overview=Back to overview
|
financer.account-details.available-actions.back-to-overview=Back to overview
|
||||||
financer.account-details.available-actions.create-transaction=Create new transaction
|
financer.account-details.available-actions.create-transaction=Create new transaction
|
||||||
financer.account-details.table-header.id=ID
|
financer.transaction-list.table-header.id=ID
|
||||||
financer.account-details.table-header.fromAccount=From account
|
financer.transaction-list.table-header.fromAccount=From account
|
||||||
financer.account-details.table-header.toAccount=To account
|
financer.transaction-list.table-header.toAccount=To account
|
||||||
financer.account-details.table-header.date=Date
|
financer.transaction-list.table-header.date=Date
|
||||||
financer.account-details.table-header.amount=Amount
|
financer.transaction-list.table-header.amount=Amount
|
||||||
financer.account-details.table-header.description=Description
|
financer.transaction-list.table-header.description=Description
|
||||||
financer.account-details.table-header.byRecurring=Recurring
|
financer.transaction-list.table-header.byRecurring=Recurring
|
||||||
financer.account-details.table-header.taxRelevant=Tax relevant
|
financer.transaction-list.table-header.taxRelevant=Tax relevant
|
||||||
financer.account-details.details.type=Type\:
|
financer.account-details.details.type=Type\:
|
||||||
financer.account-details.details.balance=Current balance\:
|
financer.account-details.details.balance=Current balance\:
|
||||||
financer.account-details.details.group=Group\:
|
financer.account-details.details.group=Group\:
|
||||||
financer.account-details.table-header.actions=Actions
|
financer.transaction-list.table-header.actions=Actions
|
||||||
financer.account-details.table.actions.deleteTransaction=Delete
|
financer.transaction-list.table.actions.deleteTransaction=Delete
|
||||||
financer.account-details.table.recurring.yes=Yes
|
financer.transaction-list.table.recurring.yes=Yes
|
||||||
financer.account-details.table.recurring.no=No
|
financer.transaction-list.table.recurring.no=No
|
||||||
financer.account-details.table.taxRelevant.true=Yes
|
financer.transaction-list.table.taxRelevant.true=Yes
|
||||||
financer.account-details.table.taxRelevant.false=No
|
financer.transaction-list.table.taxRelevant.false=No
|
||||||
|
|
||||||
financer.recurring-to-transaction-with-amount.title=financer\: create transaction from recurring with amount
|
financer.recurring-to-transaction-with-amount.title=financer\: create transaction from recurring with amount
|
||||||
financer.recurring-to-transaction-with-amount.label.amount=Amount\:
|
financer.recurring-to-transaction-with-amount.label.amount=Amount\:
|
||||||
financer.recurring-to-transaction-with-amount.submit=Create
|
financer.recurring-to-transaction-with-amount.submit=Create
|
||||||
|
|
||||||
|
financer.search-transactions.title=financer\: search transactions
|
||||||
|
financer.search-transactions.back-to-overview=Back to overview
|
||||||
|
financer.search-transactions.label.fql=FQL expression\:
|
||||||
|
financer.search-transactions.submit=Search
|
||||||
|
financer.search-transactions.show-query-options=Show query options
|
||||||
|
|
||||||
financer.chart-select.title=Select a chart to generate
|
financer.chart-select.title=Select a chart to generate
|
||||||
financer.chart-select.submit=Select
|
financer.chart-select.submit=Select
|
||||||
|
|
||||||
@@ -166,6 +173,7 @@ financer.heading.recurring-to-transaction-with-amount=financer\: create transact
|
|||||||
financer.heading.chart-select=financer\: select a chart to generate
|
financer.heading.chart-select=financer\: select a chart to generate
|
||||||
financer.heading.chart-config-account-group-expenses-for-period=financer\: configure account group expenses for period chart
|
financer.heading.chart-config-account-group-expenses-for-period=financer\: configure account group expenses for period chart
|
||||||
financer.heading.chart-config-account-expenses-for-period=financer\: configure account expenses for period chart
|
financer.heading.chart-config-account-expenses-for-period=financer\: configure account expenses for period chart
|
||||||
|
financer.heading.search-transactions=financer\: search transactions
|
||||||
|
|
||||||
financer.cancel-back-to-overview=Cancel and back to overview
|
financer.cancel-back-to-overview=Cancel and back to overview
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ financer.account-overview.available-actions.show-closed=Zeige auch geschlossene
|
|||||||
financer.account-overview.available-actions.hide-closed=Verstecke geschlossene Konten
|
financer.account-overview.available-actions.hide-closed=Verstecke geschlossene Konten
|
||||||
financer.account-overview.available-actions.create-account=Neues Konto erstellen
|
financer.account-overview.available-actions.create-account=Neues Konto erstellen
|
||||||
financer.account-overview.available-actions.create-transaction=Neue Buchung erstellen
|
financer.account-overview.available-actions.create-transaction=Neue Buchung erstellen
|
||||||
|
financer.account-overview.available-actions.search-transactions=Buchungen suchen
|
||||||
financer.account-overview.available-actions.create-recurring-transaction=Neue wiederkehrende Buchung erstellen
|
financer.account-overview.available-actions.create-recurring-transaction=Neue wiederkehrende Buchung erstellen
|
||||||
financer.account-overview.available-actions.recurring-transaction-all=Zeige alle wiederkehrende Buchungen
|
financer.account-overview.available-actions.recurring-transaction-all=Zeige alle wiederkehrende Buchungen
|
||||||
financer.account-overview.available-actions.create-account-group=Neue Konto-Gruppe erstellen
|
financer.account-overview.available-actions.create-account-group=Neue Konto-Gruppe erstellen
|
||||||
@@ -18,6 +19,8 @@ financer.account-overview.table-header.id=ID
|
|||||||
financer.account-overview.table-header.key=Konto
|
financer.account-overview.table-header.key=Konto
|
||||||
financer.account-overview.table-header.group=Gruppe
|
financer.account-overview.table-header.group=Gruppe
|
||||||
financer.account-overview.table-header.balance=Kontostand
|
financer.account-overview.table-header.balance=Kontostand
|
||||||
|
financer.account-overview.table-header.spending-current-period=Ausgaben aktuelle Periode
|
||||||
|
financer.account-overview.table-header.average-spending-period=Durchschnittliche Ausgaben
|
||||||
financer.account-overview.table-header.type=Typ
|
financer.account-overview.table-header.type=Typ
|
||||||
financer.account-overview.table-header.status=Status
|
financer.account-overview.table-header.status=Status
|
||||||
financer.account-overview.tooltip.status.current-expenses=Periode ab {0}. Durch Klicken des Betrags kann eine grafische \u00DCbersicht über die Ausgaben gruppiert nach Konto-Gruppe angezeigt werden
|
financer.account-overview.tooltip.status.current-expenses=Periode ab {0}. Durch Klicken des Betrags kann eine grafische \u00DCbersicht über die Ausgaben gruppiert nach Konto-Gruppe angezeigt werden
|
||||||
@@ -96,28 +99,34 @@ financer.account-details.available-actions.close-account=Konto schlie\u00DFen
|
|||||||
financer.account-details.available-actions.open-account=Konto \u00F6ffnen
|
financer.account-details.available-actions.open-account=Konto \u00F6ffnen
|
||||||
financer.account-details.available-actions.back-to-overview=Zur\u00FCck zur \u00DCbersicht
|
financer.account-details.available-actions.back-to-overview=Zur\u00FCck zur \u00DCbersicht
|
||||||
financer.account-details.available-actions.create-transaction=Neue Buchung erstellen
|
financer.account-details.available-actions.create-transaction=Neue Buchung erstellen
|
||||||
financer.account-details.table-header.id=ID
|
financer.transaction-list.table-header.id=ID
|
||||||
financer.account-details.table-header.fromAccount=Von Konto
|
financer.transaction-list.table-header.fromAccount=Von Konto
|
||||||
financer.account-details.table-header.toAccount=An Konto
|
financer.transaction-list.table-header.toAccount=An Konto
|
||||||
financer.account-details.table-header.date=Datum
|
financer.transaction-list.table-header.date=Datum
|
||||||
financer.account-details.table-header.amount=Betrag
|
financer.transaction-list.table-header.amount=Betrag
|
||||||
financer.account-details.table-header.description=Beschreibung
|
financer.transaction-list.table-header.description=Beschreibung
|
||||||
financer.account-details.table-header.byRecurring=Wiederkehrend
|
financer.transaction-list.table-header.byRecurring=Wiederkehrend
|
||||||
financer.account-details.table-header.taxRelevant=Relevant f\u00FCr Steuererkl\u00E4rung
|
financer.transaction-list.table-header.taxRelevant=Relevant f\u00FCr Steuererkl\u00E4rung
|
||||||
financer.account-details.details.type=Typ\:
|
financer.account-details.details.type=Typ\:
|
||||||
financer.account-details.details.balance=Kontostand\:
|
financer.account-details.details.balance=Kontostand\:
|
||||||
financer.account-details.details.group=Gruppe\:
|
financer.account-details.details.group=Gruppe\:
|
||||||
financer.account-details.table-header.actions=Aktionen
|
financer.transaction-list.table-header.actions=Aktionen
|
||||||
financer.account-details.table.actions.deleteTransaction=L\u00F6schen
|
financer.transaction-list.table.actions.deleteTransaction=L\u00F6schen
|
||||||
financer.account-details.table.recurring.yes=Ja
|
financer.transaction-list.table.recurring.yes=Ja
|
||||||
financer.account-details.table.recurring.no=Nein
|
financer.transaction-list.table.recurring.no=Nein
|
||||||
financer.account-details.table.taxRelevant.true=Ja
|
financer.transaction-list.table.taxRelevant.true=Ja
|
||||||
financer.account-details.table.taxRelevant.false=Nein
|
financer.transaction-list.table.taxRelevant.false=Nein
|
||||||
|
|
||||||
financer.recurring-to-transaction-with-amount.title=financer\: Buchung mit Betrag aus wiederkehrender Buchung erstellen
|
financer.recurring-to-transaction-with-amount.title=financer\: Buchung mit Betrag aus wiederkehrender Buchung erstellen
|
||||||
financer.recurring-to-transaction-with-amount.label.amount=Betrag\:
|
financer.recurring-to-transaction-with-amount.label.amount=Betrag\:
|
||||||
financer.recurring-to-transaction-with-amount.submit=Erstellen
|
financer.recurring-to-transaction-with-amount.submit=Erstellen
|
||||||
|
|
||||||
|
financer.search-transactions.title=financer\: Buchungen suchen
|
||||||
|
financer.search-transactions.back-to-overview=Zur\u00FCck zur \u00DCbersicht
|
||||||
|
financer.search-transactions.label.fql=FQL Ausdruck\:
|
||||||
|
financer.search-transactions.submit=Suchen
|
||||||
|
financer.search-transactions.show-query-options=Suchoptionen anzeigen
|
||||||
|
|
||||||
financer.chart-select.title=Ein Diagramm zum Erzeugen ausw\u00E4hlen
|
financer.chart-select.title=Ein Diagramm zum Erzeugen ausw\u00E4hlen
|
||||||
financer.chart-select.submit=Ausw\u00E4hlen
|
financer.chart-select.submit=Ausw\u00E4hlen
|
||||||
|
|
||||||
@@ -157,12 +166,13 @@ financer.heading.account-new=financer\: Neues Konto erstellen
|
|||||||
financer.heading.account-group-new=financer\: Neue Konto-Gruppe erstellen
|
financer.heading.account-group-new=financer\: Neue Konto-Gruppe erstellen
|
||||||
financer.heading.account-overview=financer\: \u00DCbersicht
|
financer.heading.account-overview=financer\: \u00DCbersicht
|
||||||
financer.heading.account-details=financer\: Kontodetails f\u00FCr {0}
|
financer.heading.account-details=financer\: Kontodetails f\u00FCr {0}
|
||||||
financer.heading.recurring-transaction-list.dueToday=financer\: wiederkehrende Buchungen heute f\u00E4llig
|
financer.heading.recurring-transaction-list.dueToday=financer\: Wiederkehrende Buchungen heute f\u00E4llig
|
||||||
financer.heading.recurring-transaction-list.active=financer\: aktive wiederkehrende Buchungen
|
financer.heading.recurring-transaction-list.active=financer\: Aktive wiederkehrende Buchungen
|
||||||
financer.heading.recurring-transaction-list.all=financer\: alle wiederkehrende Buchungen
|
financer.heading.recurring-transaction-list.all=financer\: Alle wiederkehrende Buchungen
|
||||||
financer.heading.recurring-to-transaction-with-amount=financer\: Buchung mit Betrag aus wiederkehrender Buchung erstellen
|
financer.heading.recurring-to-transaction-with-amount=financer\: Buchung mit Betrag aus wiederkehrender Buchung erstellen
|
||||||
financer.heading.chart-select=financer\: ein Diagramm zum Erzeugen ausw\u00E4hlen
|
financer.heading.chart-select=financer\: Ein Diagramm zum Erzeugen ausw\u00E4hlen
|
||||||
financer.heading.chart-config-account-group-expenses-for-period=financer\: konfigurieren von Ausgaben f\u00FCr Periode gruppiert nach Konto-Gruppe Diagramm
|
financer.heading.chart-config-account-group-expenses-for-period=financer\: Konfigurieren von Ausgaben f\u00FCr Periode gruppiert nach Konto-Gruppe Diagramm
|
||||||
|
financer.heading.search-transactions=financer\: Buchungen suchen
|
||||||
|
|
||||||
financer.cancel-back-to-overview=Abbrechen und zur \u00DCbersicht
|
financer.cancel-back-to-overview=Abbrechen und zur \u00DCbersicht
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ v26 -> v27:
|
|||||||
- Changed sort order of accounts in overview page. The accounts are now sorted by the account type first (BCILES), then
|
- Changed sort order of accounts in overview page. The accounts are now sorted by the account type first (BCILES), then
|
||||||
by the account group name and then by the account ID, leading to an overall more organic order of accounts
|
by the account group name and then by the account ID, leading to an overall more organic order of accounts
|
||||||
- Add tax relevance flag to transaction and recurring transaction creation. This flag denotes whether a transaction or
|
- Add tax relevance flag to transaction and recurring transaction creation. This flag denotes whether a transaction or
|
||||||
the instances of a recurring transaction are relevant for a tax declaration. This is preparation for extended reports.
|
the instances of a recurring transaction are relevant for a tax declaration. This is preparation for extended reports
|
||||||
|
- Add searching of transactions via FQL (Financer Query Language)
|
||||||
|
- Rework /transaction end point to better adhere to REST API requirements (proper HTTP return codes and HTTP method
|
||||||
|
usage)
|
||||||
|
|
||||||
v25 -> v26:
|
v25 -> v26:
|
||||||
- Close of the current expense period now creates null statistic entries for accounts that have not been used in
|
- Close of the current expense period now creates null statistic entries for accounts that have not been used in
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
/* --------------------- */
|
/* --------------------- */
|
||||||
|
|
||||||
#account-overview-table,
|
#account-overview-table,
|
||||||
#account-transaction-table,
|
#transaction-table,
|
||||||
#recurring-transaction-list-table {
|
#recurring-transaction-list-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
@@ -23,8 +23,8 @@
|
|||||||
|
|
||||||
#account-overview-table th,
|
#account-overview-table th,
|
||||||
#account-overview-table td,
|
#account-overview-table td,
|
||||||
#account-transaction-table th,
|
#transaction-table th,
|
||||||
#account-transaction-table td,
|
#transaction-table td,
|
||||||
#recurring-transaction-list-table th,
|
#recurring-transaction-list-table th,
|
||||||
#recurring-transaction-list-table td {
|
#recurring-transaction-list-table td {
|
||||||
border-bottom: 1px solid #ddd;
|
border-bottom: 1px solid #ddd;
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#account-overview-table th,
|
#account-overview-table th,
|
||||||
#account-transaction-table th,
|
#transaction-table th,
|
||||||
#recurring-transaction-list-table th {
|
#recurring-transaction-list-table th {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
@@ -107,13 +107,18 @@ tr:hover {
|
|||||||
#recurring-to-transaction-with-amount-form *,
|
#recurring-to-transaction-with-amount-form *,
|
||||||
#new-account-group-form *,
|
#new-account-group-form *,
|
||||||
#chart-config-account-group-expenses-for-period-form *,
|
#chart-config-account-group-expenses-for-period-form *,
|
||||||
#chart-config-account-expenses-for-period-form * {
|
#chart-config-account-expenses-for-period-form *,
|
||||||
|
#search-transactions-form * {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
width: 20em;
|
width: 20em;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#search-transactions-form > input[type=text] {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
#chart-select-form div {
|
#chart-select-form div {
|
||||||
width: 20em;
|
width: 20em;
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
@@ -188,3 +193,14 @@ input[type=submit] {
|
|||||||
width: var(--type-row-width);
|
width: var(--type-row-width);
|
||||||
padding: 0px !important
|
padding: 0px !important
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#search-transactions-fql-detail {
|
||||||
|
border: 1px solid grey;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
padding-inline: 0.3em;
|
||||||
|
background-color: lightgrey;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-transactions-fql-detail > * {
|
||||||
|
font-size: 0.7em;
|
||||||
|
}
|
||||||
@@ -14,8 +14,9 @@
|
|||||||
7. Transactions
|
7. Transactions
|
||||||
8. Recurring transactions
|
8. Recurring transactions
|
||||||
9. Reporting
|
9. Reporting
|
||||||
10. Setup
|
10. FQL
|
||||||
11. Planned features
|
11. Setup
|
||||||
|
12. Planned features
|
||||||
|
|
||||||
1. About
|
1. About
|
||||||
========
|
========
|
||||||
@@ -170,12 +171,15 @@
|
|||||||
9. Reporting
|
9. Reporting
|
||||||
============
|
============
|
||||||
|
|
||||||
10. Setup
|
10. FQL
|
||||||
|
=======
|
||||||
|
|
||||||
|
11. Setup
|
||||||
=========
|
=========
|
||||||
This chapter explains how to setup a financer instance. It requires PostgreSQL as a database backend and a Java
|
This chapter explains how to setup a financer instance. It requires PostgreSQL as a database backend and a Java
|
||||||
Servlet Container (e.g. Apache Tomcat) as a runtime environment.
|
Servlet Container (e.g. Apache Tomcat) as a runtime environment.
|
||||||
|
|
||||||
10.1 Database setup
|
11.1 Database setup
|
||||||
-------------------
|
-------------------
|
||||||
First install PostgreSQL. Then create a user for financer:
|
First install PostgreSQL. Then create a user for financer:
|
||||||
sudo -iu postgres
|
sudo -iu postgres
|
||||||
@@ -191,7 +195,7 @@
|
|||||||
\q
|
\q
|
||||||
exit
|
exit
|
||||||
|
|
||||||
11. Planned features
|
12. Planned features
|
||||||
====================
|
====================
|
||||||
This chapter lists planned features. The list is in no particular order:
|
This chapter lists planned features. The list is in no particular order:
|
||||||
- Transaction import from online banking (file based)
|
- Transaction import from online banking (file based)
|
||||||
|
|||||||
@@ -35,36 +35,7 @@
|
|||||||
<a th:href="@{/accountOverview}"
|
<a th:href="@{/accountOverview}"
|
||||||
th:text="#{financer.account-details.available-actions.back-to-overview}"/>
|
th:text="#{financer.account-details.available-actions.back-to-overview}"/>
|
||||||
</div>
|
</div>
|
||||||
<table id="account-transaction-table">
|
<div th:replace="transaction/transactionList :: transaction-list"/>
|
||||||
<tr>
|
|
||||||
<th class="hideable-column" th:text="#{financer.account-details.table-header.id}"/>
|
|
||||||
<th th:text="#{financer.account-details.table-header.fromAccount}"/>
|
|
||||||
<th th:text="#{financer.account-details.table-header.toAccount}"/>
|
|
||||||
<th th:text="#{financer.account-details.table-header.date}"/>
|
|
||||||
<th th:text="#{financer.account-details.table-header.amount}"/>
|
|
||||||
<th th:text="#{financer.account-details.table-header.description}"/>
|
|
||||||
<th th:text="#{financer.account-details.table-header.byRecurring}"/>
|
|
||||||
<th th:text="#{financer.account-details.table-header.taxRelevant}"/>
|
|
||||||
<th th:text="#{financer.account-details.table-header.actions}"/>
|
|
||||||
</tr>
|
|
||||||
<tr th:each="transaction : ${transactions}">
|
|
||||||
<td class="hideable-column" th:text="${transaction.id}"/>
|
|
||||||
<td th:text="${transaction.fromAccount.key}" />
|
|
||||||
<td th:text="${transaction.toAccount.key}" />
|
|
||||||
<td th:text="${#temporals.format(transaction.date)}" />
|
|
||||||
<td th:text="${#numbers.formatDecimal(transaction.amount/100D, 1, 'DEFAULT', 2, 'DEFAULT') + currencySymbol}"/>
|
|
||||||
<td th:text="${transaction.description}" />
|
|
||||||
<td th:if="${transaction.recurringTransaction != null}" th:text="#{financer.account-details.table.recurring.yes}" />
|
|
||||||
<td th:if="${transaction.recurringTransaction == null}" th:text="#{financer.account-details.table.recurring.no}" />
|
|
||||||
<td th:text="#{'financer.account-details.table.taxRelevant.' + ${transaction.taxRelevant}}" />
|
|
||||||
<td>
|
|
||||||
<div id="account-transaction-table-actions-container">
|
|
||||||
<a th:href="@{/deleteTransaction(transactionId=${transaction.id}, accountKey=${account.key})}"
|
|
||||||
th:text="#{financer.account-details.table.actions.deleteTransaction}"/>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<div th:replace="includes/footer :: footer"/>
|
<div th:replace="includes/footer :: footer"/>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -49,6 +49,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="action-container-sub-transactions">
|
<div id="action-container-sub-transactions">
|
||||||
<a th:href="@{/newTransaction}" th:text="#{financer.account-overview.available-actions.create-transaction}"/>
|
<a th:href="@{/newTransaction}" th:text="#{financer.account-overview.available-actions.create-transaction}"/>
|
||||||
|
<a th:href="@{/searchTransactions}" th:text="#{financer.account-overview.available-actions.search-transactions}"/>
|
||||||
</div>
|
</div>
|
||||||
<div id="action-container-sub-recurring-transactions">
|
<div id="action-container-sub-recurring-transactions">
|
||||||
<a th:href="@{/newRecurringTransaction}"
|
<a th:href="@{/newRecurringTransaction}"
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<title th:text="#{financer.search-transactions.title}"/>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" th:href="@{/css/main.css}">
|
||||||
|
<link rel="shortcut icon" th:href="@{/favicon.ico}" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1 th:text="#{financer.heading.search-transactions}" />
|
||||||
|
<span class="errorMessage" th:if="${errorMessage != null}" th:text="#{'financer.error-message.' + ${errorMessage}}"/>
|
||||||
|
<a th:href="@{/accountOverview}" th:text="#{financer.search-transactions.back-to-overview}"/>
|
||||||
|
<form id="search-transactions-form" action="#" th:action="@{/searchTransactions}" th:object="${form}"
|
||||||
|
method="post">
|
||||||
|
<label for="inputFql" th:text="#{financer.search-transactions.label.fql}"/>
|
||||||
|
<input type="text" id="inputFql" th:field="*{fql}"/>
|
||||||
|
<input type="submit" th:value="#{financer.search-transactions.submit}"/>
|
||||||
|
</form>
|
||||||
|
<details>
|
||||||
|
<summary th:text="#{financer.search-transactions.show-query-options}"/>
|
||||||
|
<div id="search-transactions-fql-detail">
|
||||||
|
<p>
|
||||||
|
<div>Available fields:</div>
|
||||||
|
<ul>
|
||||||
|
<li>amount: the amount of a transaction</li>
|
||||||
|
<li>fromAccount: the key of the from account</li>
|
||||||
|
<li>toAccount: the key of the to account</li>
|
||||||
|
<li>fromAccountGroup: the name of the account group of the from account</li>
|
||||||
|
<li>toAccountGroup: the name of the account group of the to account</li>
|
||||||
|
<li>date: the date of the transaction in the format yyyy-mm-dd</li>
|
||||||
|
<li>recurring: whether the transaction has been created from a recurring transaction</li>
|
||||||
|
<li>taxRelevant: whether the transaction is relevant for tax declaration</li>
|
||||||
|
<li>period: the period the transaction is assigned to
|
||||||
|
<ul>
|
||||||
|
<li>CURRENT: denotes the current expense period</li>
|
||||||
|
<li>LAST: denotes the last expense period</li>
|
||||||
|
<li>CURRENT_YEAR: denotes the current year period</li>
|
||||||
|
<li>LAST_YEAR: denotes the last year period</li>
|
||||||
|
<li>GRAND_TOTAL: denotes the grand total period</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<div>General</div>
|
||||||
|
<ul>
|
||||||
|
<li>Conjunctions with OR, AND</li>
|
||||||
|
<li>Grouping of expressions with parenthesis</li>
|
||||||
|
<li>Strings enclosed in single quotes</li>
|
||||||
|
<li>Case-insensitive</li>
|
||||||
|
<li>Possible operators =, >, >=, <, <=, !=</li>
|
||||||
|
<li>Ordering for a single field ASC, DESC via ORDER BY</li>
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
<div th:replace="transaction/transactionList :: transaction-list"/>
|
||||||
|
<div th:replace="includes/footer :: footer"/>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<div id="transaction-list-container" th:fragment="transaction-list">
|
||||||
|
<table id="transaction-table">
|
||||||
|
<tr>
|
||||||
|
<th class="hideable-column" th:text="#{financer.transaction-list.table-header.id}"/>
|
||||||
|
<th th:text="#{financer.transaction-list.table-header.fromAccount}"/>
|
||||||
|
<th th:text="#{financer.transaction-list.table-header.toAccount}"/>
|
||||||
|
<th th:text="#{financer.transaction-list.table-header.date}"/>
|
||||||
|
<th th:text="#{financer.transaction-list.table-header.amount}"/>
|
||||||
|
<th th:text="#{financer.transaction-list.table-header.description}"/>
|
||||||
|
<th th:text="#{financer.transaction-list.table-header.byRecurring}"/>
|
||||||
|
<th th:text="#{financer.transaction-list.table-header.taxRelevant}"/>
|
||||||
|
<th th:if="${showActions}" th:text="#{financer.transaction-list.table-header.actions}"/>
|
||||||
|
</tr>
|
||||||
|
<tr th:each="transaction : ${transactions}">
|
||||||
|
<td class="hideable-column" th:text="${transaction.id}"/>
|
||||||
|
<td th:text="${transaction.fromAccount.key}" />
|
||||||
|
<td th:text="${transaction.toAccount.key}" />
|
||||||
|
<td th:text="${#temporals.format(transaction.date)}" />
|
||||||
|
<td th:text="${#numbers.formatDecimal(transaction.amount/100D, 1, 'DEFAULT', 2, 'DEFAULT') + currencySymbol}"/>
|
||||||
|
<td th:text="${transaction.description}" />
|
||||||
|
<td th:if="${transaction.recurring}" th:text="#{financer.transaction-list.table.recurring.yes}" />
|
||||||
|
<td th:if="${!transaction.recurring}" th:text="#{financer.transaction-list.table.recurring.no}" />
|
||||||
|
<td th:text="#{'financer.transaction-list.table.taxRelevant.' + ${transaction.taxRelevant}}" />
|
||||||
|
<td th:if="${showActions}">
|
||||||
|
<div id="account-transaction-table-actions-container">
|
||||||
|
<a th:href="@{/deleteTransaction(transactionId=${transaction.id}, accountKey=${account.key})}"
|
||||||
|
th:text="#{financer.transaction-list.table.actions.deleteTransaction}"/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user