diff --git a/financer-common/pom.xml b/financer-common/pom.xml index 8cfbeaa..cea9a1e 100644 --- a/financer-common/pom.xml +++ b/financer-common/pom.xml @@ -28,6 +28,34 @@ org.apache.commons commons-lang3 + + org.hibernate + hibernate-core + + + xml-apis + xml-apis + + + + + + org.hibernate + hibernate-jpamodelgen + + + org.glassfish.jaxb + jaxb-runtime + + + javax.annotation + javax.annotation-api + + + + com.fasterxml.jackson.core + jackson-annotations + diff --git a/financer-common/src/main/java/de/financer/dto/ExpensePeriodTotal.java b/financer-common/src/main/java/de/financer/dto/ExpensePeriodTotal.java index a18c060..8d4c200 100644 --- a/financer-common/src/main/java/de/financer/dto/ExpensePeriodTotal.java +++ b/financer-common/src/main/java/de/financer/dto/ExpensePeriodTotal.java @@ -1,19 +1,20 @@ package de.financer.dto; import de.financer.model.AccountType; +import de.financer.model.Period; public class ExpensePeriodTotal { private Long total; - private String periodShortCode; + private Period period; private AccountType type; public ExpensePeriodTotal() { // nothing to do } - public ExpensePeriodTotal(String periodShortCode, Long total, AccountType type) { + public ExpensePeriodTotal(Period period, Long total, AccountType type) { this.total = total; - this.periodShortCode = periodShortCode; + this.period = period; this.type = type; } @@ -25,12 +26,12 @@ public class ExpensePeriodTotal { this.total = total; } - public String getPeriodShortCode() { - return periodShortCode; + public Period getPeriod() { + return period; } - public void setPeriodShortCode(String periodShortCode) { - this.periodShortCode = periodShortCode; + public void setPeriod(Period period) { + this.period = period; } public AccountType getType() { diff --git a/financer-common/src/main/java/de/financer/model/Account.java b/financer-common/src/main/java/de/financer/model/Account.java index 732b947..a1d8bd0 100644 --- a/financer-common/src/main/java/de/financer/model/Account.java +++ b/financer-common/src/main/java/de/financer/model/Account.java @@ -1,6 +1,7 @@ package de.financer.model; import javax.persistence.*; +import java.util.Set; @Entity public class Account { @@ -16,6 +17,9 @@ public class Account { private Long currentBalance; @ManyToOne private AccountGroup accountGroup; + @OneToMany(fetch = FetchType.EAGER) + @JoinColumn(name = "account_id") + private Set accountStatistics; public Long getId() { return id; @@ -60,4 +64,12 @@ public class Account { public void setAccountGroup(AccountGroup accountGroup) { this.accountGroup = accountGroup; } + + public Set getAccountStatistics() { + return accountStatistics; + } + + public void setAccountStatistics(Set accountStatistics) { + this.accountStatistics = accountStatistics; + } } diff --git a/financer-common/src/main/java/de/financer/model/AccountStatistic.java b/financer-common/src/main/java/de/financer/model/AccountStatistic.java new file mode 100644 index 0000000..1a4d253 --- /dev/null +++ b/financer-common/src/main/java/de/financer/model/AccountStatistic.java @@ -0,0 +1,76 @@ +package de.financer.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import javax.persistence.*; + +@Entity +public class AccountStatistic { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @OneToOne(fetch = FetchType.LAZY) + // We need to ignore this field during serialization as otherwise + // the stack will overflow because of the circular dependency + // between Account and AccountStatistic + @JsonIgnore + private Account account; + @OneToOne(fetch = FetchType.EAGER) + private Period period; + private long spendingTotalFrom; + private long transactionCountFrom; + private long spendingTotalTo; + private long transactionCountTo; + + public Long getId() { + return id; + } + + public Account getAccount() { + return account; + } + + public void setAccount(Account account) { + this.account = account; + } + + public Period getPeriod() { + return period; + } + + public void setPeriod(Period period) { + this.period = period; + } + + public long getSpendingTotalFrom() { + return spendingTotalFrom; + } + + public void setSpendingTotalFrom(long spendingTotalFrom) { + this.spendingTotalFrom = spendingTotalFrom; + } + + public long getTransactionCountFrom() { + return transactionCountFrom; + } + + public void setTransactionCountFrom(long transactionCountFrom) { + this.transactionCountFrom = transactionCountFrom; + } + + public long getSpendingTotalTo() { + return spendingTotalTo; + } + + public void setSpendingTotalTo(long spendingTotalTo) { + this.spendingTotalTo = spendingTotalTo; + } + + public long getTransactionCountTo() { + return transactionCountTo; + } + + public void setTransactionCountTo(long transactionCountTo) { + this.transactionCountTo = transactionCountTo; + } +} diff --git a/financer-common/src/main/java/de/financer/model/Period.java b/financer-common/src/main/java/de/financer/model/Period.java new file mode 100644 index 0000000..6833d6c --- /dev/null +++ b/financer-common/src/main/java/de/financer/model/Period.java @@ -0,0 +1,44 @@ +package de.financer.model; + +import javax.persistence.*; +import java.time.LocalDateTime; + +@Entity +public class Period { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Enumerated(EnumType.STRING) + private PeriodType type; + private LocalDateTime start; + @Column(name = "\"end\"") + private LocalDateTime end; + + public Long getId() { + return id; + } + + public PeriodType getType() { + return type; + } + + public void setType(PeriodType type) { + this.type = type; + } + + public LocalDateTime getStart() { + return start; + } + + public void setStart(LocalDateTime start) { + this.start = start; + } + + public LocalDateTime getEnd() { + return end; + } + + public void setEnd(LocalDateTime end) { + this.end = end; + } +} diff --git a/financer-common/src/main/java/de/financer/model/PeriodType.java b/financer-common/src/main/java/de/financer/model/PeriodType.java new file mode 100644 index 0000000..5f4f24d --- /dev/null +++ b/financer-common/src/main/java/de/financer/model/PeriodType.java @@ -0,0 +1,24 @@ +package de.financer.model; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public enum PeriodType { + /** User defined start and end */ + EXPENSE; + + /** + * This method validates whether the given string represents a valid period type. + * + * @param type to check + * @return whether the given type represents a valid period type + */ + public static boolean isValidType(String type) { + return Arrays.stream(PeriodType.values()).anyMatch((periodType) -> periodType.name().equals(type)); + } + + public static List valueList() { + return Arrays.stream(PeriodType.values()).map(PeriodType::name).collect(Collectors.toList()); + } +} diff --git a/financer-common/src/main/java/de/financer/model/Transaction.java b/financer-common/src/main/java/de/financer/model/Transaction.java index 0b33788..3299792 100644 --- a/financer-common/src/main/java/de/financer/model/Transaction.java +++ b/financer-common/src/main/java/de/financer/model/Transaction.java @@ -2,6 +2,7 @@ package de.financer.model; import javax.persistence.*; import java.time.LocalDate; +import java.util.Set; @Entity @Table(name = "\"transaction\"") @@ -16,9 +17,16 @@ public class Transaction { @Column(name = "\"date\"") private LocalDate date; private String description; - private Long amount; + private long amount; @ManyToOne(fetch = FetchType.EAGER) private RecurringTransaction recurringTransaction; + @OneToMany(cascade = CascadeType.PERSIST, fetch = FetchType.EAGER) + //@formatter:off + @JoinTable(name = "link_transaction_period", + joinColumns = @JoinColumn(name = "transaction_id"), + inverseJoinColumns = @JoinColumn(name = "period_id")) + //@formatter:on + private Set periods; public Long getId() { return id; @@ -56,11 +64,11 @@ public class Transaction { this.description = description; } - public Long getAmount() { + public long getAmount() { return amount; } - public void setAmount(Long amount) { + public void setAmount(long amount) { this.amount = amount; } @@ -71,4 +79,12 @@ public class Transaction { public void setRecurringTransaction(RecurringTransaction recurringTransaction) { this.recurringTransaction = recurringTransaction; } + + public Set getPeriods() { + return periods; + } + + public void setPeriods(Set periods) { + this.periods = periods; + } } diff --git a/financer-common/src/main/java/de/financer/util/ExpensePeriod.java b/financer-common/src/main/java/de/financer/util/ExpensePeriod.java deleted file mode 100644 index 4379bb7..0000000 --- a/financer-common/src/main/java/de/financer/util/ExpensePeriod.java +++ /dev/null @@ -1,100 +0,0 @@ -package de.financer.util; - -import com.google.common.collect.Streams; - -import java.time.LocalDate; -import java.time.Month; -import java.time.Period; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public class ExpensePeriod { - private LocalDate start; - private LocalDate end; - - private ExpensePeriod(LocalDate start, LocalDate end) { - this.start = start; - this.end = end; - } - - public static final ExpensePeriod getCurrentExpensePeriod(int periodStartDay) { - LocalDate periodStart; - LocalDate periodEnd; - - if (LocalDate.now().getDayOfMonth() < periodStartDay) { - // If the current day of month is less than the configured period start day - // we need to go back one month in time and then set the day to the configured - // start day. For example: - // configured start day of month for period = 15 - // now = 2019-06-10 - // => 10 < 15 - // now - one month = 2019-05-10 - // set the day = 2019-05-15 = start - // end = start + one month - one day = 2019-06-14 - // Period from 2019-05-15 to 2019-06-14 - periodStart = LocalDate.now().minusMonths(1).withDayOfMonth(periodStartDay); - periodEnd = periodStart.plusMonths(1).minusDays(1); - } else { - // Else, the current day of month is greater or equals the configured period - // start day, just reset the day of month to the configured day, for example: - // configured start day of month for period = 15 - // now = 2019-06-26 - // => 26 > 15 - // set the day = 2019-06-15 = start - // end = start + one month - one day = 2019-07-14 - // Period from 2019-06-15 to 2019-07-14 - periodStart = LocalDate.now().withDayOfMonth(periodStartDay); - periodEnd = periodStart.plusMonths(1).minusDays(1); - } - - return new ExpensePeriod(periodStart, periodEnd); - } - - public static final List generateExpensePeriodsForYear(int periodStartDay, int year) { - Stream localDateStreamStart; - Stream localDateStreamEnd; - - if (periodStartDay == 1) { - localDateStreamStart = LocalDate.of(year, Month.JANUARY, 1) - .datesUntil(LocalDate.of(year, Month.DECEMBER, 2), Period - .ofMonths(1)); - - localDateStreamEnd = LocalDate.of(year, Month.JANUARY, 31) - .datesUntil(LocalDate.of(year + 1, Month.JANUARY, 1), Period - .ofMonths(1)); - } - // If the start of the period is not the 1st we need to look to the previous year as well - // because the period in January actually started in December last year - else { - localDateStreamStart = LocalDate.of(year, Month.JANUARY, periodStartDay).minusMonths(1) - .datesUntil(LocalDate.of(year, Month.DECEMBER, periodStartDay + 1).plusDays(1), Period - .ofMonths(1)); - - localDateStreamEnd = LocalDate.of(year, Month.JANUARY, periodStartDay - 1) - .datesUntil(LocalDate.of(year, Month.DECEMBER, periodStartDay + 1).plusDays(1), Period - .ofMonths(1)); - } - - return Streams - .zip(localDateStreamStart, localDateStreamEnd, (start, end) -> new ExpensePeriod(start, end)) - .collect(Collectors.toList()); - } - - public String generatePeriodShortCode() { - return String.format("%s/%s", this.start.getYear(), this.start.getMonthValue()); - } - - @Override - public String toString() { - return String.format("Start[%s], End[%s], ShortCode[%s]", this.start, this.end, this.generatePeriodShortCode()); - } - - public LocalDate getStart() { - return start; - } - - public LocalDate getEnd() { - return end; - } -} diff --git a/financer-server/pom.xml b/financer-server/pom.xml index 2531cce..502e4b1 100644 --- a/financer-server/pom.xml +++ b/financer-server/pom.xml @@ -12,7 +12,8 @@ financer-server ${packaging.type} - The server part of the financer application - a simple app to manage your personal finances + The server part of the financer application - a simple app to manage your personal finances + financer-server diff --git a/financer-server/src/main/java/de/financer/ResponseReason.java b/financer-server/src/main/java/de/financer/ResponseReason.java index e5877cb..264d41f 100644 --- a/financer-server/src/main/java/de/financer/ResponseReason.java +++ b/financer-server/src/main/java/de/financer/ResponseReason.java @@ -6,32 +6,32 @@ import org.springframework.http.ResponseEntity; public enum ResponseReason { OK(HttpStatus.OK), UNKNOWN_ERROR(HttpStatus.INTERNAL_SERVER_ERROR), - INVALID_ACCOUNT_TYPE(HttpStatus.INTERNAL_SERVER_ERROR), - FROM_ACCOUNT_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR), - TO_ACCOUNT_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR), - FROM_AND_TO_ACCOUNT_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR), - INVALID_DATE_FORMAT(HttpStatus.INTERNAL_SERVER_ERROR), - MISSING_DATE(HttpStatus.INTERNAL_SERVER_ERROR), - AMOUNT_ZERO(HttpStatus.INTERNAL_SERVER_ERROR), - MISSING_AMOUNT(HttpStatus.INTERNAL_SERVER_ERROR), - INVALID_BOOKING_ACCOUNTS(HttpStatus.INTERNAL_SERVER_ERROR), - MISSING_HOLIDAY_WEEKEND_TYPE(HttpStatus.INTERNAL_SERVER_ERROR), - INVALID_HOLIDAY_WEEKEND_TYPE(HttpStatus.INTERNAL_SERVER_ERROR), - MISSING_INTERVAL_TYPE(HttpStatus.INTERNAL_SERVER_ERROR), - INVALID_INTERVAL_TYPE(HttpStatus.INTERNAL_SERVER_ERROR), - MISSING_FIRST_OCCURRENCE(HttpStatus.INTERNAL_SERVER_ERROR), - INVALID_FIRST_OCCURRENCE_FORMAT(HttpStatus.INTERNAL_SERVER_ERROR), - INVALID_LAST_OCCURRENCE_FORMAT(HttpStatus.INTERNAL_SERVER_ERROR), - MISSING_RECURRING_TRANSACTION_ID(HttpStatus.INTERNAL_SERVER_ERROR), - INVALID_RECURRING_TRANSACTION_ID(HttpStatus.INTERNAL_SERVER_ERROR), - RECURRING_TRANSACTION_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR), - MISSING_TRANSACTION_ID(HttpStatus.INTERNAL_SERVER_ERROR), - INVALID_TRANSACTION_ID(HttpStatus.INTERNAL_SERVER_ERROR), - TRANSACTION_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR), - ACCOUNT_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR), - DUPLICATE_ACCOUNT_KEY(HttpStatus.INTERNAL_SERVER_ERROR), - DUPLICATE_ACCOUNT_GROUP_NAME(HttpStatus.INTERNAL_SERVER_ERROR), - ACCOUNT_GROUP_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR); + INVALID_ACCOUNT_TYPE(HttpStatus.BAD_REQUEST), + FROM_ACCOUNT_NOT_FOUND(HttpStatus.BAD_REQUEST), + TO_ACCOUNT_NOT_FOUND(HttpStatus.BAD_REQUEST), + FROM_AND_TO_ACCOUNT_NOT_FOUND(HttpStatus.BAD_REQUEST), + INVALID_DATE_FORMAT(HttpStatus.BAD_REQUEST), + MISSING_DATE(HttpStatus.BAD_REQUEST), + AMOUNT_ZERO(HttpStatus.BAD_REQUEST), + MISSING_AMOUNT(HttpStatus.BAD_REQUEST), + INVALID_BOOKING_ACCOUNTS(HttpStatus.BAD_REQUEST), + MISSING_HOLIDAY_WEEKEND_TYPE(HttpStatus.BAD_REQUEST), + INVALID_HOLIDAY_WEEKEND_TYPE(HttpStatus.BAD_REQUEST), + MISSING_INTERVAL_TYPE(HttpStatus.BAD_REQUEST), + INVALID_INTERVAL_TYPE(HttpStatus.BAD_REQUEST), + MISSING_FIRST_OCCURRENCE(HttpStatus.BAD_REQUEST), + INVALID_FIRST_OCCURRENCE_FORMAT(HttpStatus.BAD_REQUEST), + INVALID_LAST_OCCURRENCE_FORMAT(HttpStatus.BAD_REQUEST), + MISSING_RECURRING_TRANSACTION_ID(HttpStatus.BAD_REQUEST), + INVALID_RECURRING_TRANSACTION_ID(HttpStatus.BAD_REQUEST), + RECURRING_TRANSACTION_NOT_FOUND(HttpStatus.BAD_REQUEST), + MISSING_TRANSACTION_ID(HttpStatus.BAD_REQUEST), + INVALID_TRANSACTION_ID(HttpStatus.BAD_REQUEST), + TRANSACTION_NOT_FOUND(HttpStatus.BAD_REQUEST), + ACCOUNT_NOT_FOUND(HttpStatus.BAD_REQUEST), + DUPLICATE_ACCOUNT_KEY(HttpStatus.BAD_REQUEST), + DUPLICATE_ACCOUNT_GROUP_NAME(HttpStatus.BAD_REQUEST), + ACCOUNT_GROUP_NOT_FOUND(HttpStatus.BAD_REQUEST); private HttpStatus httpStatus; diff --git a/financer-server/src/main/java/de/financer/controller/AccountController.java b/financer-server/src/main/java/de/financer/controller/AccountController.java index 5b9d473..5acd8d8 100644 --- a/financer-server/src/main/java/de/financer/controller/AccountController.java +++ b/financer-server/src/main/java/de/financer/controller/AccountController.java @@ -111,4 +111,13 @@ public class AccountController { return this.accountService.getAccountExpenses(periodStart, periodEnd); } + + @RequestMapping("getAccountExpensesCurrentExpensePeriod") + public Iterable getAccountExpensesCurrentExpensePeriod() { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("/accounts/getAccountExpensesCurrentExpensePeriod called"); + } + + return this.accountService.getAccountExpensesCurrentExpensePeriod(); + } } diff --git a/financer-server/src/main/java/de/financer/controller/AccountGroupController.java b/financer-server/src/main/java/de/financer/controller/AccountGroupController.java index 8a00135..7a927e7 100644 --- a/financer-server/src/main/java/de/financer/controller/AccountGroupController.java +++ b/financer-server/src/main/java/de/financer/controller/AccountGroupController.java @@ -60,4 +60,13 @@ public class AccountGroupController { return this.accountGroupService.getAccountGroupExpenses(periodStart, periodEnd); } + + @RequestMapping("getAccountGroupExpensesCurrentExpensePeriod") + public Iterable getAccountGroupExpensesCurrentExpensePeriod() { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("/accountGroups/getAccountGroupExpenses called"); + } + + return this.accountGroupService.getAccountGroupExpensesCurrentExpensePeriod(); + } } diff --git a/financer-server/src/main/java/de/financer/controller/PeriodController.java b/financer-server/src/main/java/de/financer/controller/PeriodController.java new file mode 100644 index 0000000..1cf6b1f --- /dev/null +++ b/financer-server/src/main/java/de/financer/controller/PeriodController.java @@ -0,0 +1,49 @@ +package de.financer.controller; + +import de.financer.model.Period; +import de.financer.service.PeriodService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("periods") +public class PeriodController { + private static final Logger LOGGER = LoggerFactory.getLogger(PeriodController.class); + + @Autowired + private PeriodService periodService; + + @RequestMapping("closeCurrentExpensePeriod") + public ResponseEntity closeCurrentExpensePeriod() { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("/periods/closeCurrentExpensePeriod called"); + } + + final ResponseEntity responseEntity = this.periodService.closeCurrentExpensePeriod().toResponseEntity(); + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug(String.format("/periods/closeCurrentExpensePeriod returns with %s", responseEntity)); + } + + return responseEntity; + } + + @RequestMapping("getCurrentExpensePeriod") + public Period getCurrentExpensePeriod() { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("/periods/getCurrentExpensePeriod called"); + } + + final Period currentExpensePeriod = this.periodService.getCurrentExpensePeriod(); + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug(String.format("/periods/getCurrentExpensePeriod returns with %s", currentExpensePeriod)); + } + + return currentExpensePeriod; + } +} diff --git a/financer-server/src/main/java/de/financer/controller/TransactionController.java b/financer-server/src/main/java/de/financer/controller/TransactionController.java index b5e15aa..065da82 100644 --- a/financer-server/src/main/java/de/financer/controller/TransactionController.java +++ b/financer-server/src/main/java/de/financer/controller/TransactionController.java @@ -79,12 +79,12 @@ public class TransactionController { } @RequestMapping("getExpensesCurrentPeriod") - public Long getExpensesCurrentPeriod(Integer monthPeriodStartDay) { + public Long getExpensesCurrentPeriod() { if (LOGGER.isDebugEnabled()) { - LOGGER.debug(String.format("/transactions/getExpensesCurrentPeriod got parameters: %s", monthPeriodStartDay)); + LOGGER.debug(String.format("/transactions/getExpensesCurrentPeriod called")); } - final Long response = this.transactionService.getExpensesCurrentPeriod(monthPeriodStartDay); + final Long response = this.transactionService.getExpensesCurrentPeriod(); if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format("/transactions/getExpensesCurrentPeriod returns with %s", response)); @@ -94,13 +94,13 @@ public class TransactionController { } @RequestMapping("getExpensePeriodTotals") - public Iterable getExpensePeriodTotals(Integer monthPeriodStartDay, Integer year) { + public Iterable getExpensePeriodTotals(Integer year) { if (LOGGER.isDebugEnabled()) { - LOGGER.debug(String.format("/transactions/getExpensePeriodTotals got parameters: %s, %s", monthPeriodStartDay, year)); + LOGGER.debug(String.format("/transactions/getExpensePeriodTotals got parameters: %s", year)); } final Iterable expensePeriodTotals = this.transactionService - .getExpensePeriodTotals(monthPeriodStartDay, year); + .getExpensePeriodTotals(year); if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format("/transactions/getExpensePeriodTotals returns with %s", expensePeriodTotals)); diff --git a/financer-server/src/main/java/de/financer/dba/AccountGroupRepository.java b/financer-server/src/main/java/de/financer/dba/AccountGroupRepository.java index 4013ab1..db398c1 100644 --- a/financer-server/src/main/java/de/financer/dba/AccountGroupRepository.java +++ b/financer-server/src/main/java/de/financer/dba/AccountGroupRepository.java @@ -3,6 +3,7 @@ package de.financer.dba; import de.financer.dto.AccountGroupExpense; import de.financer.model.AccountGroup; import de.financer.model.AccountType; +import de.financer.model.Period; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; import org.springframework.transaction.annotation.Propagation; @@ -16,4 +17,7 @@ public interface AccountGroupRepository extends CrudRepository getAccountGroupExpenses(LocalDate startDate, LocalDate startEnd, AccountType... expenseTypes); + + @Query("SELECT new de.financer.dto.AccountGroupExpense(ag, SUM(t.amount)) FROM Transaction t JOIN t.toAccount a JOIN a.accountGroup ag JOIN t.periods p WHERE a.type in :expenseTypes AND p = :period GROUP BY ag") + Iterable getAccountGroupExpensesCurrentExpensePeriod(Period period, AccountType... expenseTypes); } diff --git a/financer-server/src/main/java/de/financer/dba/AccountRepository.java b/financer-server/src/main/java/de/financer/dba/AccountRepository.java index 6fc0e52..71175c6 100644 --- a/financer-server/src/main/java/de/financer/dba/AccountRepository.java +++ b/financer-server/src/main/java/de/financer/dba/AccountRepository.java @@ -3,6 +3,7 @@ package de.financer.dba; import de.financer.dto.AccountExpense; import de.financer.model.Account; import de.financer.model.AccountType; +import de.financer.model.Period; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; import org.springframework.transaction.annotation.Propagation; @@ -19,4 +20,7 @@ public interface AccountRepository extends CrudRepository { @Query("SELECT new de.financer.dto.AccountExpense(a, SUM(t.amount)) FROM Transaction t JOIN t.toAccount a WHERE a.type in :expenseTypes AND t.date BETWEEN :startDate AND :startEnd GROUP BY a") Iterable getAccountExpenses(LocalDate startDate, LocalDate startEnd, AccountType... expenseTypes); + + @Query("SELECT new de.financer.dto.AccountExpense(a, SUM(t.amount)) FROM Transaction t JOIN t.toAccount a JOIN t.periods p WHERE a.type in :expenseTypes AND p = :period GROUP BY a") + Iterable getAccountExpenses(Period period, AccountType... expenseTypes); } diff --git a/financer-server/src/main/java/de/financer/dba/AccountStatisticRepository.java b/financer-server/src/main/java/de/financer/dba/AccountStatisticRepository.java new file mode 100644 index 0000000..5dc3272 --- /dev/null +++ b/financer-server/src/main/java/de/financer/dba/AccountStatisticRepository.java @@ -0,0 +1,15 @@ +package de.financer.dba; + +import de.financer.model.Account; +import de.financer.model.AccountStatistic; +import de.financer.model.Period; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +@Transactional(propagation = Propagation.REQUIRED) +public interface AccountStatisticRepository extends CrudRepository { + @Query("SELECT accStat FROM AccountStatistic accStat WHERE accStat.account = :account AND accStat.period = :period") + AccountStatistic findForAccountAndPeriod(Account account, Period period); +} diff --git a/financer-server/src/main/java/de/financer/dba/PeriodRepository.java b/financer-server/src/main/java/de/financer/dba/PeriodRepository.java new file mode 100644 index 0000000..ac2a107 --- /dev/null +++ b/financer-server/src/main/java/de/financer/dba/PeriodRepository.java @@ -0,0 +1,17 @@ +package de.financer.dba; + +import de.financer.model.Period; +import de.financer.model.PeriodType; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +@Transactional(propagation = Propagation.REQUIRED) +public interface PeriodRepository extends CrudRepository { + @Query("SELECT p FROM Period p WHERE p.type = :type AND p.end IS NULL") + Period findCurrentExpensePeriod(PeriodType type); + + @Query("SELECT p FROM Period p WHERE p.type = :type AND YEAR(p.start) = :year") + Iterable getAllPeriodsForYear(PeriodType type, Integer year); +} diff --git a/financer-server/src/main/java/de/financer/dba/TransactionRepository.java b/financer-server/src/main/java/de/financer/dba/TransactionRepository.java index 5691f2d..f070d2c 100644 --- a/financer-server/src/main/java/de/financer/dba/TransactionRepository.java +++ b/financer-server/src/main/java/de/financer/dba/TransactionRepository.java @@ -3,6 +3,7 @@ package de.financer.dba; import de.financer.dto.ExpensePeriodTotal; import de.financer.model.Account; import de.financer.model.AccountType; +import de.financer.model.Period; import de.financer.model.Transaction; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; @@ -16,13 +17,12 @@ import java.util.List; public interface TransactionRepository extends CrudRepository { Iterable findTransactionsByFromAccountOrToAccount(Account fromAccount, Account toAccount); - @Query("SELECT SUM(t.amount) FROM Transaction t JOIN t.toAccount a WHERE t.date BETWEEN :periodStart AND :periodEnd AND a.type IN :expenseTypes") - Long getExpensesCurrentPeriod(LocalDate periodStart, LocalDate periodEnd, AccountType... expenseTypes); + @Query("SELECT SUM(t.amount) FROM Transaction t JOIN t.periods p JOIN t.toAccount a WHERE a.type IN :expenseTypes AND p = :period") + Long getExpensesForPeriod(Period period, AccountType... expenseTypes); // The HQL contains a hack because Hibernate can't resolve the alias of the CASE column in the GROUP BY clause // That's why the generated alias is used directly in the HQL. It will break if the columns in the SELECT clause get reordered - // col_0_0_ instead of periodShortCode // col_2_0_ instead of AccType - @Query("SELECT new de.financer.dto.ExpensePeriodTotal(CASE WHEN EXTRACT(DAY FROM t.date) >= :startDay THEN CONCAT(CAST(EXTRACT(YEAR FROM t.date) AS string), '/', CAST(EXTRACT(MONTH FROM t.date) AS string)) ELSE CASE WHEN EXTRACT(MONTH FROM t.date) = 1 THEN CONCAT(CAST(EXTRACT(YEAR FROM t.date) - 1 AS string), '/12') ELSE CONCAT(CAST(EXTRACT(YEAR FROM t.date) AS string), '/', CAST(EXTRACT(MONTH FROM t.date) - 1 AS string)) END END AS periodShortCode, SUM(t.amount), CASE WHEN fa.type = :incomeType THEN fa.type ELSE ta.type END AS AccType) FROM Transaction t JOIN t.toAccount ta JOIN t.fromAccount fa WHERE ((ta.type IN :expenseTypes AND fa.type <> :startType) OR (fa.type = :incomeType)) AND t.date BETWEEN :lowerBound AND :upperBound GROUP BY col_0_0_, col_2_0_ ORDER BY periodShortCode, AccType ASC") - List getAccountExpenseTotals(int startDay, LocalDate lowerBound, LocalDate upperBound, AccountType incomeType, AccountType startType, AccountType... expenseTypes); + @Query("SELECT new de.financer.dto.ExpensePeriodTotal(p, SUM(t.amount), CASE WHEN fa.type = :incomeType THEN fa.type ELSE ta.type END AS AccType) FROM Transaction t JOIN t.toAccount ta JOIN t.fromAccount fa JOIN t.periods p WHERE ((ta.type IN :expenseTypes AND fa.type <> :startType) OR (fa.type = :incomeType)) AND p IN :periods GROUP BY p, col_2_0_") + List getAccountExpenseTotals(Iterable periods, AccountType incomeType, AccountType startType, AccountType... expenseTypes); } \ No newline at end of file diff --git a/financer-server/src/main/java/de/financer/service/AccountGroupService.java b/financer-server/src/main/java/de/financer/service/AccountGroupService.java index 4a4886a..14a58ea 100644 --- a/financer-server/src/main/java/de/financer/service/AccountGroupService.java +++ b/financer-server/src/main/java/de/financer/service/AccountGroupService.java @@ -6,6 +6,7 @@ import de.financer.dba.AccountGroupRepository; import de.financer.dto.AccountGroupExpense; import de.financer.model.AccountGroup; import de.financer.model.AccountType; +import de.financer.model.Period; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -26,6 +27,9 @@ public class AccountGroupService { @Autowired private AccountGroupRepository accountGroupRepository; + @Autowired + private PeriodService periodService; + @Autowired private FinancerConfig financerConfig; @@ -94,4 +98,10 @@ public class AccountGroupService { return this.accountGroupRepository.getAccountGroupExpenses(startDate, endDate, AccountType.LIABILITY, AccountType.EXPENSE); } + + public Iterable getAccountGroupExpensesCurrentExpensePeriod() { + final Period period = this.periodService.getCurrentExpensePeriod(); + + return this.accountGroupRepository.getAccountGroupExpensesCurrentExpensePeriod(period, AccountType.LIABILITY, AccountType.EXPENSE); + } } diff --git a/financer-server/src/main/java/de/financer/service/AccountService.java b/financer-server/src/main/java/de/financer/service/AccountService.java index 3b5b499..ede5fa9 100644 --- a/financer-server/src/main/java/de/financer/service/AccountService.java +++ b/financer-server/src/main/java/de/financer/service/AccountService.java @@ -4,10 +4,7 @@ import de.financer.ResponseReason; import de.financer.config.FinancerConfig; import de.financer.dba.AccountRepository; import de.financer.dto.AccountExpense; -import de.financer.model.Account; -import de.financer.model.AccountGroup; -import de.financer.model.AccountStatus; -import de.financer.model.AccountType; +import de.financer.model.*; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,6 +29,9 @@ public class AccountService { @Autowired private AccountGroupService accountGroupService; + @Autowired + private PeriodService periodService; + @Autowired private FinancerConfig financerConfig; @@ -161,6 +161,13 @@ public class AccountService { return this.accountRepository.getCurrentAssets(AccountType.BANK, AccountType.CASH); } + /** + * This method calculates the expenses per account in the given arbitrary period. + * + * @param periodStart the start of the arbitrary period + * @param periodEnd the end of the arbitrary period + * @return a mapping of {@link Account}<->its expenses in the given period + */ public Iterable getAccountExpenses(String periodStart, String periodEnd) { LocalDate startDate; LocalDate endDate; @@ -178,4 +185,15 @@ public class AccountService { return this.accountRepository.getAccountExpenses(startDate, endDate, AccountType.LIABILITY, AccountType.EXPENSE); } + + /** + * This method calculates the expenses per account in the current expense period. + * + * @return a mapping of {@link Account}<->its expenses in the current expense period + */ + public Iterable getAccountExpensesCurrentExpensePeriod() { + final Period period = this.periodService.getCurrentExpensePeriod(); + + return this.accountRepository.getAccountExpenses(period, AccountType.LIABILITY, AccountType.EXPENSE); + } } diff --git a/financer-server/src/main/java/de/financer/service/AccountStatisticService.java b/financer-server/src/main/java/de/financer/service/AccountStatisticService.java new file mode 100644 index 0000000..b1b8858 --- /dev/null +++ b/financer-server/src/main/java/de/financer/service/AccountStatisticService.java @@ -0,0 +1,77 @@ +package de.financer.service; + +import de.financer.dba.AccountStatisticRepository; +import de.financer.model.Account; +import de.financer.model.AccountStatistic; +import de.financer.model.Period; +import de.financer.model.Transaction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +@Service +public class AccountStatisticService { + private static final Logger LOGGER = LoggerFactory.getLogger(PeriodService.class); + + @Autowired + private AccountStatisticRepository accountStatisticRepository; + + @Transactional(propagation = Propagation.REQUIRED) + public void calculateStatistics(Transaction transaction) { + final Account fromAccount = transaction.getFromAccount(); + final Account toAccount = transaction.getToAccount(); + final long amount = transaction.getAmount(); + final List resultList = new ArrayList<>(); + + for (final Period period : transaction.getPeriods()) { + resultList.add(calculateInternal(fromAccount, period, amount, true, 1)); + resultList.add(calculateInternal(toAccount, period, amount, false, 1)); + } + + this.accountStatisticRepository.saveAll(resultList); + } + + @Transactional(propagation = Propagation.REQUIRED) + public void revertStatistics(Transaction transaction) { + final Account fromAccount = transaction.getFromAccount(); + final Account toAccount = transaction.getToAccount(); + final long amount = transaction.getAmount(); + final List resultList = new ArrayList<>(); + + for (final Period period : transaction.getPeriods()) { + resultList.add(calculateInternal(fromAccount, period, amount, true, -1)); + resultList.add(calculateInternal(toAccount, period, amount, false, -1)); + } + + this.accountStatisticRepository.saveAll(resultList); + } + + private AccountStatistic calculateInternal(Account account, Period period, long amount, boolean from, int multiplier) { + AccountStatistic accountStatistic = this.accountStatisticRepository + .findForAccountAndPeriod(account, period); + + if (accountStatistic == null) { + accountStatistic = new AccountStatistic(); + + accountStatistic.setAccount(account); + accountStatistic.setPeriod(period); + } + + if (from) { + accountStatistic.setSpendingTotalFrom(accountStatistic.getSpendingTotalFrom() + amount * multiplier); + accountStatistic.setTransactionCountFrom(accountStatistic.getTransactionCountFrom() + multiplier); + } + else { + accountStatistic.setSpendingTotalTo(accountStatistic.getSpendingTotalTo() + amount * multiplier); + accountStatistic.setTransactionCountTo(accountStatistic.getTransactionCountTo() + multiplier); + } + + return accountStatistic; + } +} diff --git a/financer-server/src/main/java/de/financer/service/PeriodService.java b/financer-server/src/main/java/de/financer/service/PeriodService.java new file mode 100644 index 0000000..8bbd59f --- /dev/null +++ b/financer-server/src/main/java/de/financer/service/PeriodService.java @@ -0,0 +1,64 @@ +package de.financer.service; + +import de.financer.ResponseReason; +import de.financer.dba.PeriodRepository; +import de.financer.model.Period; +import de.financer.model.PeriodType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +@Service +public class PeriodService { + private static final Logger LOGGER = LoggerFactory.getLogger(PeriodService.class); + + @Autowired + private PeriodRepository periodRepository; + + /** + * @return the currently open expense period + */ + public Period getCurrentExpensePeriod() { + return this.periodRepository.findCurrentExpensePeriod(PeriodType.EXPENSE); + } + + /** + * This method closes the current expense period and opens a new one. + * + * @return {@link ResponseReason#OK} if the operation succeeded, {@link ResponseReason#OK} + */ + @Transactional(propagation = Propagation.REQUIRED) + public ResponseReason closeCurrentExpensePeriod() { + final Period currentPeriod = this.getCurrentExpensePeriod(); + final Period nextPeriod = new Period(); + final LocalDateTime now = LocalDateTime.now(); + ResponseReason response; + + currentPeriod.setEnd(now); + + nextPeriod.setStart(now.plusSeconds(1)); + nextPeriod.setType(PeriodType.EXPENSE); + + try { + this.periodRepository.save(currentPeriod); + this.periodRepository.save(nextPeriod); + + response = ResponseReason.OK; + } catch (Exception e) { + LOGGER.error("Could not close current expense period!", e); + + response = ResponseReason.UNKNOWN_ERROR; + } + + return response; + } + + public Iterable getAllExpensePeriodsForYear(Integer year) { + return this.periodRepository.getAllPeriodsForYear(PeriodType.EXPENSE, year); + } +} diff --git a/financer-server/src/main/java/de/financer/service/TransactionService.java b/financer-server/src/main/java/de/financer/service/TransactionService.java index 721ee71..f252f53 100644 --- a/financer-server/src/main/java/de/financer/service/TransactionService.java +++ b/financer-server/src/main/java/de/financer/service/TransactionService.java @@ -1,15 +1,10 @@ package de.financer.service; -import com.google.common.collect.Iterables; import de.financer.ResponseReason; import de.financer.config.FinancerConfig; import de.financer.dba.TransactionRepository; import de.financer.dto.ExpensePeriodTotal; -import de.financer.model.Account; -import de.financer.model.AccountType; -import de.financer.model.RecurringTransaction; -import de.financer.model.Transaction; -import de.financer.util.ExpensePeriod; +import de.financer.model.*; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.NumberUtils; import org.slf4j.Logger; @@ -23,7 +18,6 @@ import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.Collections; -import java.util.List; import java.util.Optional; @Service @@ -36,6 +30,12 @@ public class TransactionService { @Autowired private RuleService ruleService; + @Autowired + private PeriodService periodService; + + @Autowired + private AccountStatisticService accountStatisticService; + @Autowired private TransactionRepository transactionRepository; @@ -91,6 +91,8 @@ public class TransactionService { try { final Transaction transaction = buildTransaction(fromAccount, toAccount, amount, description, date, recurringTransaction); + transaction.setPeriods(Collections.singleton(this.periodService.getCurrentExpensePeriod())); + fromAccount.setCurrentBalance(fromAccount.getCurrentBalance() + (this.ruleService .getMultiplierFromAccount(fromAccount) * amount)); @@ -106,6 +108,8 @@ public class TransactionService { this.transactionRepository.save(transaction); + this.accountStatisticService.calculateStatistics(transaction); + this.accountService.saveAccount(fromAccount); this.accountService.saveAccount(toAccount); @@ -215,7 +219,9 @@ public class TransactionService { .getMultiplierToAccount(toAccount) * amount * -1)); try { - this.transactionRepository.deleteById(Long.valueOf(transactionId)); + this.transactionRepository.deleteById(transaction.getId()); + + this.accountStatisticService.revertStatistics(transaction); this.accountService.saveAccount(fromAccount); this.accountService.saveAccount(toAccount); @@ -228,31 +234,19 @@ public class TransactionService { return response; } - public Long getExpensesCurrentPeriod(Integer monthPeriodStartDay) { - if (monthPeriodStartDay == null) { - return Long.valueOf(-1l); - } - - final ExpensePeriod expensePeriod = ExpensePeriod - .getCurrentExpensePeriod(monthPeriodStartDay); + public Long getExpensesCurrentPeriod() { + final Period expensePeriod = this.periodService.getCurrentExpensePeriod(); final Long expensesCurrentPeriod = this.transactionRepository - .getExpensesCurrentPeriod(expensePeriod.getStart(), expensePeriod - .getEnd(), AccountType.EXPENSE, AccountType.LIABILITY); + .getExpensesForPeriod(expensePeriod, AccountType.EXPENSE, AccountType.LIABILITY); return Optional.ofNullable(expensesCurrentPeriod).orElse(Long.valueOf(0l)); } - public Iterable getExpensePeriodTotals(Integer monthPeriodStartDay, Integer year) { - final List expensePeriods = ExpensePeriod.generateExpensePeriodsForYear(monthPeriodStartDay, year); + public Iterable getExpensePeriodTotals(Integer year) { + final Iterable periods = this.periodService.getAllExpensePeriodsForYear(year); - final ExpensePeriod lowerBound = Iterables.get(expensePeriods, 0); - final ExpensePeriod upperBound = Iterables.getLast(expensePeriods); - - final List expensePeriodTotals = this.transactionRepository - .getAccountExpenseTotals(monthPeriodStartDay, lowerBound.getStart(), upperBound - .getEnd(), AccountType.INCOME, AccountType.START, AccountType.EXPENSE, AccountType.LIABILITY); - - return expensePeriodTotals; + return this.transactionRepository + .getAccountExpenseTotals(periods, AccountType.INCOME, AccountType.START, AccountType.EXPENSE, AccountType.LIABILITY); } } diff --git a/financer-server/src/main/resources/config/application-dev.properties b/financer-server/src/main/resources/config/application-dev.properties index 000f956..e72794a 100644 --- a/financer-server/src/main/resources/config/application-dev.properties +++ b/financer-server/src/main/resources/config/application-dev.properties @@ -1,2 +1,3 @@ # Hibernate -spring.jpa.show-sql=true \ No newline at end of file +spring.jpa.show-sql=true +#logging.level.org.hibernate.type=trace \ No newline at end of file diff --git a/financer-server/src/main/resources/database/hsqldb/V19_0_0__varcharToNvarchar.sql b/financer-server/src/main/resources/database/hsqldb/V19_0_0__varcharToNvarchar.sql new file mode 100644 index 0000000..9225bb5 --- /dev/null +++ b/financer-server/src/main/resources/database/hsqldb/V19_0_0__varcharToNvarchar.sql @@ -0,0 +1,4 @@ +ALTER TABLE account ALTER COLUMN "key" NVARCHAR(1000); +ALTER TABLE recurring_transaction ALTER COLUMN description NVARCHAR(1000); +ALTER TABLE "transaction" ALTER COLUMN description NVARCHAR(1000); +ALTER TABLE account_group ALTER COLUMN name NVARCHAR(1000); \ No newline at end of file diff --git a/financer-server/src/main/resources/database/hsqldb/V19_0_1__period.sql b/financer-server/src/main/resources/database/hsqldb/V19_0_1__period.sql new file mode 100644 index 0000000..39fa41d --- /dev/null +++ b/financer-server/src/main/resources/database/hsqldb/V19_0_1__period.sql @@ -0,0 +1,19 @@ +-- Account group table +CREATE TABLE period ( + id BIGINT NOT NULL PRIMARY KEY IDENTITY, + type VARCHAR(255) NOT NULL, + start DATETIME NOT NULL, + "end" DATETIME +); + +CREATE TABLE link_transaction_period ( + id BIGINT NOT NULL PRIMARY KEY IDENTITY, + transaction_id BIGINT NOT NULL, + period_id BIGINT NOT NULL, + + CONSTRAINT fk_link_transaction_period_transaction FOREIGN KEY (transaction_id) REFERENCES "transaction" (id), + CONSTRAINT fk_link_transaction_period_period FOREIGN KEY (period_id) REFERENCES period (id) +); + +INSERT INTO period (type, start) +VALUES ('EXPENSE', NOW()); diff --git a/financer-server/src/main/resources/database/hsqldb/V19_0_2__accountStatistic.sql b/financer-server/src/main/resources/database/hsqldb/V19_0_2__accountStatistic.sql new file mode 100644 index 0000000..09d6407 --- /dev/null +++ b/financer-server/src/main/resources/database/hsqldb/V19_0_2__accountStatistic.sql @@ -0,0 +1,13 @@ +-- Account statistic table +CREATE TABLE account_statistic ( + id BIGINT NOT NULL PRIMARY KEY IDENTITY, + account_id BIGINT NOT NULL, + period_id BIGINT NOT NULL, + spending_total_from BIGINT NOT NULL, + transaction_count_from BIGINT NOT NULL, + spending_total_to BIGINT NOT NULL, + transaction_count_to BIGINT NOT NULL, + + CONSTRAINT fk_account_statistic_account FOREIGN KEY (account_id) REFERENCES account (id), + CONSTRAINT fk_account_statistic_period FOREIGN KEY (period_id) REFERENCES period (id) +) \ No newline at end of file diff --git a/financer-server/src/main/resources/database/postgres/V19_0_1__period.sql b/financer-server/src/main/resources/database/postgres/V19_0_1__period.sql new file mode 100644 index 0000000..33fa73d --- /dev/null +++ b/financer-server/src/main/resources/database/postgres/V19_0_1__period.sql @@ -0,0 +1,19 @@ +-- Account group table +CREATE TABLE period ( + id BIGINT NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + type VARCHAR(255) NOT NULL, + start DATETIME NOT NULL, + "end" DATETIME +); + +CREATE TABLE link_transaction_period ( + id BIGINT NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + transaction_id BIGINT NOT NULL, + period_id BIGINT NOT NULL, + + CONSTRAINT fk_link_transaction_period_transaction FOREIGN KEY (transaction_id) REFERENCES "transaction" (id), + CONSTRAINT fk_link_transaction_period_period FOREIGN KEY (period_id) REFERENCES period (id) +); + +INSERT INTO period (type, start) +VALUES ('EXPENSE', NOW()); diff --git a/financer-server/src/main/resources/database/postgres/V19_0_2__accountStatistic.sql b/financer-server/src/main/resources/database/postgres/V19_0_2__accountStatistic.sql new file mode 100644 index 0000000..29af9c4 --- /dev/null +++ b/financer-server/src/main/resources/database/postgres/V19_0_2__accountStatistic.sql @@ -0,0 +1,13 @@ +-- Account statistic table +CREATE TABLE account_statistic ( + id BIGINT NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + account_id BIGINT NOT NULL, + period_id BIGINT NOT NULL, + spending_total_from BIGINT NOT NULL, + transaction_count_from BIGINT NOT NULL, + spending_total_to BIGINT NOT NULL, + transaction_count_to BIGINT NOT NULL, + + CONSTRAINT fk_account_statistic_account FOREIGN KEY (account_id) REFERENCES account (id), + CONSTRAINT fk_account_statistic_period FOREIGN KEY (period_id) REFERENCES period (id) +) \ No newline at end of file diff --git a/financer-server/src/main/resources/database/postgres/readme_V19_0_0__varcharToNvarchar.txt b/financer-server/src/main/resources/database/postgres/readme_V19_0_0__varcharToNvarchar.txt new file mode 100644 index 0000000..b5207b0 --- /dev/null +++ b/financer-server/src/main/resources/database/postgres/readme_V19_0_0__varcharToNvarchar.txt @@ -0,0 +1 @@ +The VARCHAR type on PostgreSQL is already multibyte capable so nothing to do here \ No newline at end of file diff --git a/financer-server/src/main/resources/database/postgres/readme_V1_0_0__init.txt b/financer-server/src/main/resources/database/postgres/readme_V1_0_0__init.txt index 8d853b0..4cc8514 100644 --- a/financer-server/src/main/resources/database/postgres/readme_V1_0_0__init.txt +++ b/financer-server/src/main/resources/database/postgres/readme_V1_0_0__init.txt @@ -18,7 +18,7 @@ The recurring transaction table is defined like this (at least for postgres): Note the deleted BOOLEAN DEFAULT 'TRUE' NOT NULL, -column definition. Not sure why the default is TRUE here is it doesn't make sense. +column definition. Not sure why the default is TRUE here as it doesn't make sense. It was probably a mistake, however fixing it here _WILL_ break existing installations as Flyway uses a checksum for scripts. So there is no easy fix, except for effectively overwriting this default in Java code when creating a new recurring transaction. diff --git a/financer-server/src/test/java/de/financer/service/TransactionService_createTransactionTest.java b/financer-server/src/test/java/de/financer/service/TransactionService_createTransactionTest.java index 24ca33d..916edac 100644 --- a/financer-server/src/test/java/de/financer/service/TransactionService_createTransactionTest.java +++ b/financer-server/src/test/java/de/financer/service/TransactionService_createTransactionTest.java @@ -24,6 +24,12 @@ public class TransactionService_createTransactionTest { @Mock private RuleService ruleService; + @Mock + private PeriodService periodService; + + @Mock + private AccountStatisticService accountStatisticService; + @Mock private TransactionRepository transactionRepository; diff --git a/financer-server/src/test/java/de/financer/service/TransactionService_deleteTransactionTest.java b/financer-server/src/test/java/de/financer/service/TransactionService_deleteTransactionTest.java index f6febd5..a45c96f 100644 --- a/financer-server/src/test/java/de/financer/service/TransactionService_deleteTransactionTest.java +++ b/financer-server/src/test/java/de/financer/service/TransactionService_deleteTransactionTest.java @@ -27,6 +27,9 @@ public class TransactionService_deleteTransactionTest { @Mock private RuleService ruleService; + @Mock + private AccountStatisticService accountStatisticService; + @Mock private TransactionRepository transactionRepository; @@ -82,7 +85,7 @@ public class TransactionService_deleteTransactionTest { // Arrange Mockito.when(this.transactionRepository.findById(Mockito.anyLong())) .thenReturn(Optional.of(createTransaction(AccountType.BANK, AccountType.EXPENSE))); - Mockito.doThrow(new NullPointerException()).when(this.transactionRepository).deleteById(Mockito.anyLong()); + Mockito.doThrow(new NullPointerException()).when(this.transactionRepository).deleteById(Mockito.any()); // Act final ResponseReason response = this.classUnderTest.deleteTransaction("123"); @@ -94,8 +97,10 @@ public class TransactionService_deleteTransactionTest { @Test public void test_deleteRecurringTransaction_OK() { // Arrange + final Transaction transaction = createTransaction(AccountType.BANK, AccountType.EXPENSE); + Mockito.when(this.transactionRepository.findById(Mockito.anyLong())) - .thenReturn(Optional.of(createTransaction(AccountType.BANK, AccountType.EXPENSE))); + .thenReturn(Optional.of(transaction)); // Act final ResponseReason response = this.classUnderTest.deleteTransaction("123"); @@ -103,10 +108,11 @@ public class TransactionService_deleteTransactionTest { // Assert Assert.assertEquals(ResponseReason.OK, response); - final InOrder inOrder = Mockito.inOrder(this.accountService); + final InOrder inOrder = Mockito.inOrder(this.accountStatisticService, this.accountService); - inOrder.verify(this.accountService).saveAccount(ArgumentMatchers.argThat((Account arg) -> Long.valueOf(50000L).equals(arg.getCurrentBalance()))); - inOrder.verify(this.accountService).saveAccount(ArgumentMatchers.argThat((Account arg) -> Long.valueOf(5000L).equals(arg.getCurrentBalance()))); + inOrder.verify(this.accountStatisticService).revertStatistics(ArgumentMatchers.eq(transaction)); + inOrder.verify(this.accountService).saveAccount(ArgumentMatchers.argThat((arg) -> Long.valueOf(50000L).equals(arg.getCurrentBalance()))); + inOrder.verify(this.accountService).saveAccount(ArgumentMatchers.argThat((arg) -> Long.valueOf(5000L).equals(arg.getCurrentBalance()))); } private Transaction createTransaction(AccountType fromType, AccountType toType) { diff --git a/financer-web-client/src/main/java/de/financer/chart/impl/expense/AccountExpensesGenerator.java b/financer-web-client/src/main/java/de/financer/chart/impl/expense/AccountExpensesGenerator.java index 44a01b9..f1f581f 100644 --- a/financer-web-client/src/main/java/de/financer/chart/impl/expense/AccountExpensesGenerator.java +++ b/financer-web-client/src/main/java/de/financer/chart/impl/expense/AccountExpensesGenerator.java @@ -3,6 +3,7 @@ package de.financer.chart.impl.expense; import de.financer.config.FinancerConfig; import de.financer.dto.AccountExpense; import de.financer.dto.AccountGroupExpense; +import de.financer.template.GetAccountExpensesCurrentExpensePeriodTemplate; import de.financer.template.GetAccountExpensesTemplate; import de.financer.template.GetAccountGroupExpensesTemplate; import org.apache.commons.collections4.IterableUtils; @@ -12,8 +13,14 @@ import org.jfree.data.general.PieDataset; public class AccountExpensesGenerator extends AbstractExpensesGenerator { @Override protected PieDataset getDataset(String start, String end, FinancerConfig financerConfig) { - final Iterable expenses = new GetAccountExpensesTemplate() - .exchange(financerConfig, start, end).getBody(); + Iterable expenses; + + if (start == null && end == null) { + expenses = new GetAccountExpensesCurrentExpensePeriodTemplate().exchange(financerConfig).getBody(); + } + else { + expenses = new GetAccountExpensesTemplate().exchange(financerConfig, start, end).getBody(); + } final DefaultPieDataset dataSet = new DefaultPieDataset(); diff --git a/financer-web-client/src/main/java/de/financer/chart/impl/expense/AccountGroupExpensesGenerator.java b/financer-web-client/src/main/java/de/financer/chart/impl/expense/AccountGroupExpensesGenerator.java index 29d1527..03101a1 100644 --- a/financer-web-client/src/main/java/de/financer/chart/impl/expense/AccountGroupExpensesGenerator.java +++ b/financer-web-client/src/main/java/de/financer/chart/impl/expense/AccountGroupExpensesGenerator.java @@ -2,6 +2,7 @@ package de.financer.chart.impl.expense; import de.financer.config.FinancerConfig; import de.financer.dto.AccountGroupExpense; +import de.financer.template.GetAccountGroupExpensesCurrentExpensePeriodTemplate; import de.financer.template.GetAccountGroupExpensesTemplate; import org.apache.commons.collections4.IterableUtils; import org.jfree.data.general.DefaultPieDataset; @@ -10,8 +11,14 @@ import org.jfree.data.general.PieDataset; public class AccountGroupExpensesGenerator extends AbstractExpensesGenerator { @Override protected PieDataset getDataset(String start, String end, FinancerConfig financerConfig) { - final Iterable expenses = new GetAccountGroupExpensesTemplate() - .exchange(financerConfig, start, end).getBody(); + Iterable expenses; + + if (start == null && end == null) { + expenses = new GetAccountGroupExpensesCurrentExpensePeriodTemplate().exchange(financerConfig).getBody(); + } + else { + expenses = new GetAccountGroupExpensesTemplate().exchange(financerConfig, start, end).getBody(); + } final DefaultPieDataset dataSet = new DefaultPieDataset(); diff --git a/financer-web-client/src/main/java/de/financer/chart/impl/total/PeriodTotalGenerator.java b/financer-web-client/src/main/java/de/financer/chart/impl/total/PeriodTotalGenerator.java index e65bb7c..9c3ead9 100644 --- a/financer-web-client/src/main/java/de/financer/chart/impl/total/PeriodTotalGenerator.java +++ b/financer-web-client/src/main/java/de/financer/chart/impl/total/PeriodTotalGenerator.java @@ -2,9 +2,8 @@ package de.financer.chart.impl.total; import de.financer.chart.AbstractChartGenerator; import de.financer.dto.ExpensePeriodTotal; +import de.financer.model.Period; import de.financer.template.GetExpensePeriodTotalsTemplate; -import de.financer.util.ControllerUtils; -import de.financer.util.ExpensePeriod; import org.apache.commons.collections4.IterableUtils; import org.jfree.chart.ChartFactory; import org.jfree.chart.JFreeChart; @@ -17,7 +16,7 @@ import org.springframework.context.i18n.LocaleContextHolder; import java.text.NumberFormat; import java.time.format.DateTimeFormatter; import java.time.format.FormatStyle; -import java.util.List; +import java.util.Comparator; public class PeriodTotalGenerator extends AbstractChartGenerator { @Override @@ -45,30 +44,36 @@ public class PeriodTotalGenerator extends AbstractChartGenerator expensePeriods = ExpensePeriod - .generateExpensePeriodsForYear(this.getFinancerConfig().getMonthPeriodStartDay(), parameter.getYear()); final Iterable totalData = new GetExpensePeriodTotalsTemplate() .exchange(this.getFinancerConfig(), parameter.getYear()).getBody(); IterableUtils.toList(totalData).stream() + .sorted(Comparator.comparing((ExpensePeriodTotal ept) -> ept.getPeriod().getStart()) + .thenComparing((ExpensePeriodTotal ept) -> ept.getType())) .forEach((ept) -> result.addValue((ept.getTotal() / 100D), this.getMessage("financer.account-type." + ept.getType()), - expensePeriods.stream() - .filter((ep) -> ep.generatePeriodShortCode() - .equals(ept.getPeriodShortCode())) - .map((ep) -> formatDateY(ep)) - .findFirst().get())); + formatDateY(ept.getPeriod()))); return result; } - private String formatDateY(ExpensePeriod ep) { - return String.format("%s - %s", - ep.getStart().format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) - .withLocale(LocaleContextHolder.getLocale())), - ep.getEnd().format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) - .withLocale(LocaleContextHolder.getLocale()))); + private String formatDateY(Period ep) { + final String start = ep.getStart().format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) + .withLocale(LocaleContextHolder.getLocale())); + + String end; + + if (ep.getEnd() == null) { + end = this.getMessage("financer.chart.expense-period-totals-current-year.open-period"); + } + else { + end = ep.getEnd().format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) + .withLocale(LocaleContextHolder.getLocale())); + } + + return String.format("%s - %s", start, end); + } } diff --git a/financer-web-client/src/main/java/de/financer/controller/AccountController.java b/financer-web-client/src/main/java/de/financer/controller/AccountController.java index 656015f..ab3cf89 100644 --- a/financer-web-client/src/main/java/de/financer/controller/AccountController.java +++ b/financer-web-client/src/main/java/de/financer/controller/AccountController.java @@ -2,11 +2,11 @@ package de.financer.controller; import de.financer.ResponseReason; import de.financer.config.FinancerConfig; +import de.financer.decorator.AccountDecorator; import de.financer.template.*; import de.financer.form.NewAccountForm; import de.financer.model.*; import de.financer.util.ControllerUtils; -import de.financer.util.ExpensePeriod; import de.financer.util.TransactionUtils; import de.financer.util.comparator.TransactionByDateByIdDescComparator; import org.apache.commons.collections4.IterableUtils; @@ -20,6 +20,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.util.UriComponentsBuilder; import java.util.List; +import java.util.stream.Collectors; @Controller public class AccountController { @@ -34,18 +35,20 @@ public class AccountController { final ResponseEntity> rtAllActRes = new GetAllActiveRecurringTransactionsTemplate() .exchange(this.financerConfig); final ResponseEntity currentAssets = new GetCurrentAssetsTemplate().exchange(this.financerConfig); - final ResponseEntity currentExpenses = new GetExpensesCurrentPeriodTemplate().exchange(this.financerConfig); + final ResponseEntity currentExpenses = new GetExpensesCurrentPeriodTemplate() + .exchange(this.financerConfig); final boolean showClosedBoolean = BooleanUtils.toBoolean(showClosed); - final ExpensePeriod expensePeriod = ExpensePeriod - .getCurrentExpensePeriod(this.financerConfig.getMonthPeriodStartDay()); + final ResponseEntity expensePeriodRes = new GetCurrentExpensePeriodTemplate() + .exchange(this.financerConfig); + final Period expensePeriod = expensePeriodRes.getBody(); - model.addAttribute("accounts", ControllerUtils.filterAndSortAccounts(response.getBody(), showClosedBoolean)); + model.addAttribute("accounts", decorateAccounts(ControllerUtils + .filterAndSortAccounts(response.getBody(), showClosedBoolean))); model.addAttribute("rtDueTodayCount", IterableUtils.size(rtDtRes.getBody())); model.addAttribute("rtAllActiveCount", IterableUtils.size(rtAllActRes.getBody())); model.addAttribute("currentAssets", currentAssets.getBody()); model.addAttribute("currentExpenses", currentExpenses.getBody()); model.addAttribute("periodStart", expensePeriod.getStart()); - model.addAttribute("periodEnd", expensePeriod.getEnd()); model.addAttribute("showClosed", showClosedBoolean); ControllerUtils.addVersionAttribute(model, this.financerConfig); ControllerUtils.addCurrencySymbol(model, this.financerConfig); @@ -53,6 +56,10 @@ public class AccountController { return "account/accountOverview"; } + private Iterable decorateAccounts(List accounts) { + return accounts.stream().map((a) -> new AccountDecorator(a)).collect(Collectors.toList()); + } + @GetMapping("/newAccount") public String newAccount(Model model) { final ResponseEntity> accountGroupResponse = new GetAllAccountGroupsTemplate() diff --git a/financer-web-client/src/main/java/de/financer/controller/ChartController.java b/financer-web-client/src/main/java/de/financer/controller/ChartController.java index 4802bc4..b9d161e 100644 --- a/financer-web-client/src/main/java/de/financer/controller/ChartController.java +++ b/financer-web-client/src/main/java/de/financer/controller/ChartController.java @@ -1,15 +1,14 @@ package de.financer.controller; -import de.financer.chart.impl.expense.ExpensesParameter; import de.financer.chart.ChartGenerator; import de.financer.chart.ChartType; import de.financer.chart.FinancerChartFactory; +import de.financer.chart.impl.expense.ExpensesParameter; import de.financer.chart.impl.total.PeriodTotalParameter; import de.financer.config.FinancerConfig; import de.financer.form.ConfigAccountExpenseForPeriodForm; import de.financer.form.ConfigAccountGroupExpenseForPeriodForm; import de.financer.util.ControllerUtils; -import de.financer.util.ExpensePeriod; import org.jfree.chart.ChartUtils; import org.jfree.chart.JFreeChart; import org.springframework.beans.factory.annotation.Autowired; @@ -32,12 +31,8 @@ public class ChartController { @GetMapping("/getAccountGroupExpensesCurrentPeriod") public void getAccountGroupExpensesCurrentPeriod(HttpServletResponse response) { - final ExpensePeriod period = ExpensePeriod - .getCurrentExpensePeriod(this.financerConfig.getMonthPeriodStartDay()); final ExpensesParameter parameter = new ExpensesParameter(); - parameter.setPeriodStart(period.getStart()); - parameter.setPeriodEnd(period.getEnd()); parameter.setTitle("financer.chart.account-group-expenses-current-period.title"); final ChartGenerator generator = @@ -70,12 +65,8 @@ public class ChartController { @GetMapping("/getAccountExpensesCurrentPeriod") public void getAccountExpensesCurrentPeriod(HttpServletResponse response) { - final ExpensePeriod period = ExpensePeriod - .getCurrentExpensePeriod(this.financerConfig.getMonthPeriodStartDay()); final ExpensesParameter parameter = new ExpensesParameter(); - parameter.setPeriodStart(period.getStart()); - parameter.setPeriodEnd(period.getEnd()); parameter.setTitle("financer.chart.account-expenses-current-period.title"); final ChartGenerator generator = diff --git a/financer-web-client/src/main/java/de/financer/controller/Function.java b/financer-web-client/src/main/java/de/financer/controller/Function.java index 899fbc9..52aeea3 100644 --- a/financer-web-client/src/main/java/de/financer/controller/Function.java +++ b/financer-web-client/src/main/java/de/financer/controller/Function.java @@ -8,10 +8,12 @@ public enum Function { ACC_OPEN_ACCOUNT("accounts/openAccount"), ACC_CURRENT_ASSETS("accounts/getCurrentAssets"), ACC_GET_ACC_EXPENSES("accounts/getAccountExpenses"), + ACC_GET_ACC_EXPENSES_CURRENT_EXPENSE_PERIOD("accounts/getAccountExpensesCurrentExpensePeriod"), ACC_GP_CREATE_ACCOUNT_GROUP("accountGroups/createAccountGroup"), ACC_GP_GET_ALL("accountGroups/getAll"), ACC_GP_GET_ACC_GP_EXPENSES("accountGroups/getAccountGroupExpenses"), + ACC_GP_GET_ACC_GP_EXPENSES_CURRENT_EXPENSE_PERIOD("accountGroups/getAccountGroupExpensesCurrentExpensePeriod"), TR_GET_ALL("transactions/getAll"), TR_GET_ALL_FOR_ACCOUNT("transactions/getAllForAccount"), @@ -26,7 +28,10 @@ public enum Function { RT_GET_ALL_DUE_TODAY("recurringTransactions/getAllDueToday"), RT_CREATE_RECURRING_TRANSACTION("recurringTransactions/createRecurringTransaction"), RT_DELETE_RECURRING_TRANSACTION("recurringTransactions/deleteRecurringTransaction"), - RT_CREATE_TRANSACTION("recurringTransactions/createTransaction"); + RT_CREATE_TRANSACTION("recurringTransactions/createTransaction"), + + P_GET_CURRENT_EXPENSE_PERIOD("periods/getCurrentExpensePeriod"), + P_CLOSE_CURRENT_EXPENSE_PERIOD("periods/closeCurrentExpensePeriod"); private String path; diff --git a/financer-web-client/src/main/java/de/financer/controller/PeriodController.java b/financer-web-client/src/main/java/de/financer/controller/PeriodController.java new file mode 100644 index 0000000..dfa9938 --- /dev/null +++ b/financer-web-client/src/main/java/de/financer/controller/PeriodController.java @@ -0,0 +1,25 @@ +package de.financer.controller; + +import de.financer.config.FinancerConfig; +import de.financer.template.StringTemplate; +import de.financer.util.ControllerUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.util.UriComponentsBuilder; + +@Controller +public class PeriodController { + @Autowired + private FinancerConfig financerConfig; + + @GetMapping("/closePeriod") + public String closePeriod() { + final UriComponentsBuilder builder = UriComponentsBuilder + .fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.P_CLOSE_CURRENT_EXPENSE_PERIOD)); + + new StringTemplate().exchange(builder); + + return "redirect:/accountOverview"; + } +} diff --git a/financer-web-client/src/main/java/de/financer/controller/RecurringTransactionController.java b/financer-web-client/src/main/java/de/financer/controller/RecurringTransactionController.java index fc371f2..0a99747 100644 --- a/financer-web-client/src/main/java/de/financer/controller/RecurringTransactionController.java +++ b/financer-web-client/src/main/java/de/financer/controller/RecurringTransactionController.java @@ -2,13 +2,10 @@ package de.financer.controller; import de.financer.ResponseReason; import de.financer.config.FinancerConfig; +import de.financer.model.*; import de.financer.template.*; import de.financer.form.NewRecurringTransactionForm; import de.financer.form.RecurringToTransactionWithAmountForm; -import de.financer.model.Account; -import de.financer.model.HolidayWeekendType; -import de.financer.model.IntervalType; -import de.financer.model.RecurringTransaction; import de.financer.util.ControllerUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; @@ -19,6 +16,8 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.util.UriComponentsBuilder; import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; @Controller public class RecurringTransactionController { @@ -30,10 +29,18 @@ public class RecurringTransactionController { public String newRecurringTransaction(Model model) { final ResponseEntity> response = new GetAllAccountsTemplate().exchange(this.financerConfig); final NewRecurringTransactionForm form = new NewRecurringTransactionForm(); + final List fromAccounts = ControllerUtils.filterAndSortAccounts(response.getBody()).stream() + .filter((a) -> a.getType() != AccountType.EXPENSE) + .collect(Collectors.toList()); + final List toAccounts = ControllerUtils.filterAndSortAccounts(response.getBody()).stream() + .filter((a) -> a.getType() != AccountType.INCOME && a + .getType() != AccountType.START) + .collect(Collectors.toList()); form.setRemind(Boolean.TRUE); - model.addAttribute("accounts", ControllerUtils.filterAndSortAccounts(response.getBody())); + model.addAttribute("fromAccounts", fromAccounts); + model.addAttribute("toAccounts", toAccounts); model.addAttribute("intervalTypes", IntervalType.valueList()); model.addAttribute("holidayWeekendTypes", HolidayWeekendType.valueList()); model.addAttribute("form", form); @@ -64,8 +71,16 @@ public class RecurringTransactionController { if (!ResponseReason.OK.equals(responseReason)) { final ResponseEntity> getAllResponse = new GetAllAccountsTemplate() .exchange(this.financerConfig); + final List fromAccounts = ControllerUtils.filterAndSortAccounts(getAllResponse.getBody()).stream() + .filter((a) -> a.getType() != AccountType.EXPENSE) + .collect(Collectors.toList()); + final List toAccounts = ControllerUtils.filterAndSortAccounts(getAllResponse.getBody()).stream() + .filter((a) -> a.getType() != AccountType.INCOME && a + .getType() != AccountType.START) + .collect(Collectors.toList()); - model.addAttribute("accounts", ControllerUtils.filterAndSortAccounts(getAllResponse.getBody())); + model.addAttribute("fromAccounts", fromAccounts); + model.addAttribute("toAccounts", toAccounts); model.addAttribute("intervalTypes", IntervalType.valueList()); model.addAttribute("holidayWeekendTypes", HolidayWeekendType.valueList()); model.addAttribute("form", form); diff --git a/financer-web-client/src/main/java/de/financer/controller/TransactionController.java b/financer-web-client/src/main/java/de/financer/controller/TransactionController.java index cff418b..5b5da8b 100644 --- a/financer-web-client/src/main/java/de/financer/controller/TransactionController.java +++ b/financer-web-client/src/main/java/de/financer/controller/TransactionController.java @@ -2,6 +2,7 @@ package de.financer.controller; import de.financer.ResponseReason; import de.financer.config.FinancerConfig; +import de.financer.model.AccountType; import de.financer.template.GetAccountByKeyTemplate; import de.financer.template.GetAllAccountsTemplate; import de.financer.template.GetAllTransactionsForAccountTemplate; @@ -23,6 +24,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.util.UriComponentsBuilder; import java.util.List; +import java.util.stream.Collectors; @Controller public class TransactionController { @@ -33,8 +35,16 @@ public class TransactionController { @GetMapping("/newTransaction") public String newTransaction(Model model) { final ResponseEntity> response = new GetAllAccountsTemplate().exchange(this.financerConfig); + final List fromAccounts = ControllerUtils.filterAndSortAccounts(response.getBody()).stream() + .filter((a) -> a.getType() != AccountType.EXPENSE) + .collect(Collectors.toList()); + final List toAccounts = ControllerUtils.filterAndSortAccounts(response.getBody()).stream() + .filter((a) -> a.getType() != AccountType.INCOME && a + .getType() != AccountType.START) + .collect(Collectors.toList()); - model.addAttribute("accounts", ControllerUtils.filterAndSortAccounts(response.getBody())); + model.addAttribute("fromAccounts", fromAccounts); + model.addAttribute("toAccounts", toAccounts); model.addAttribute("form", new NewTransactionForm()); ControllerUtils.addVersionAttribute(model, this.financerConfig); ControllerUtils.addCurrencySymbol(model, this.financerConfig); @@ -57,8 +67,16 @@ public class TransactionController { if (!ResponseReason.OK.equals(responseReason)) { final ResponseEntity> accRes = new GetAllAccountsTemplate().exchange(this.financerConfig); + final List fromAccounts = ControllerUtils.filterAndSortAccounts(accRes.getBody()).stream() + .filter((a) -> a.getType() != AccountType.EXPENSE) + .collect(Collectors.toList()); + final List toAccounts = ControllerUtils.filterAndSortAccounts(accRes.getBody()).stream() + .filter((a) -> a.getType() != AccountType.INCOME && a + .getType() != AccountType.START) + .collect(Collectors.toList()); - model.addAttribute("accounts", ControllerUtils.filterAndSortAccounts(accRes.getBody())); + model.addAttribute("fromAccounts", fromAccounts); + model.addAttribute("toAccounts", toAccounts); model.addAttribute("form", form); model.addAttribute("errorMessage", responseReason.name()); ControllerUtils.addVersionAttribute(model, this.financerConfig); @@ -79,7 +97,8 @@ public class TransactionController { final ResponseEntity response = new StringTemplate().exchange(builder); final ResponseReason responseReason = ResponseReason.fromResponseEntity(response); - final ResponseEntity accountResponse = new GetAccountByKeyTemplate().exchange(this.financerConfig, accountKey); + final ResponseEntity accountResponse = new GetAccountByKeyTemplate() + .exchange(this.financerConfig, accountKey); final Account account = accountResponse.getBody(); final ResponseEntity> transactionResponse = new GetAllTransactionsForAccountTemplate() .exchange(this.financerConfig, account.getKey()); diff --git a/financer-web-client/src/main/java/de/financer/decorator/AccountDecorator.java b/financer-web-client/src/main/java/de/financer/decorator/AccountDecorator.java new file mode 100644 index 0000000..6563d53 --- /dev/null +++ b/financer-web-client/src/main/java/de/financer/decorator/AccountDecorator.java @@ -0,0 +1,65 @@ +package de.financer.decorator; + +import de.financer.model.*; + +public class AccountDecorator { + private final Account account; + + public Long getId() { + return account.getId(); + } + + public String getKey() { + return account.getKey(); + } + + public AccountType getType() { + return account.getType(); + } + + public AccountStatus getStatus() { + return account.getStatus(); + } + + public Long getCurrentBalance() { + return account.getCurrentBalance(); + } + + public AccountGroup getAccountGroup() { + return account.getAccountGroup(); + } + + public AccountDecorator(Account account) { + this.account = account; + } + + public Long getSpendingCurrentExpensePeriod() { + if (this.account.getType() == AccountType.EXPENSE || this.account.getType() == AccountType.LIABILITY) { + return this.account.getAccountStatistics().stream() + .filter((as) -> as.getPeriod().getType().equals(PeriodType.EXPENSE) && as.getPeriod() + .getEnd() == null) + .findFirst() + .orElseGet(() -> { + AccountStatistic as = new AccountStatistic(); + as.setTransactionCountTo(0l); + return as; + }) + .getSpendingTotalTo(); + } + + return null; + } + + public Long getAverageSpendingExpensePeriod() { + if (this.account.getType() == AccountType.EXPENSE || this.account.getType() == AccountType.LIABILITY) { + return Math.round(this.account.getAccountStatistics().stream() + .filter((as) -> as.getPeriod().getType().equals(PeriodType.EXPENSE) && as.getPeriod() + .getEnd() != null) + .mapToLong((as) -> as.getSpendingTotalTo()) + .average() + .orElseGet(() -> Double.valueOf(0d))); + } + + return null; + } +} diff --git a/financer-web-client/src/main/java/de/financer/template/GetAccountExpensesCurrentExpensePeriodTemplate.java b/financer-web-client/src/main/java/de/financer/template/GetAccountExpensesCurrentExpensePeriodTemplate.java new file mode 100644 index 0000000..b9f7727 --- /dev/null +++ b/financer-web-client/src/main/java/de/financer/template/GetAccountExpensesCurrentExpensePeriodTemplate.java @@ -0,0 +1,20 @@ +package de.financer.template; + +import de.financer.config.FinancerConfig; +import de.financer.controller.Function; +import de.financer.dto.AccountExpense; +import de.financer.util.ControllerUtils; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.ResponseEntity; +import org.springframework.web.util.UriComponentsBuilder; + +public class GetAccountExpensesCurrentExpensePeriodTemplate { + public ResponseEntity> exchange(FinancerConfig financerConfig) { + final UriComponentsBuilder expensesBuilder = UriComponentsBuilder + .fromHttpUrl(ControllerUtils.buildUrl(financerConfig, Function.ACC_GET_ACC_EXPENSES_CURRENT_EXPENSE_PERIOD)); + + return new FinancerRestTemplate>() + .exchange(expensesBuilder.toUriString(), new ParameterizedTypeReference>() { + }); + } +} diff --git a/financer-web-client/src/main/java/de/financer/template/GetAccountGroupExpensesCurrentExpensePeriodTemplate.java b/financer-web-client/src/main/java/de/financer/template/GetAccountGroupExpensesCurrentExpensePeriodTemplate.java new file mode 100644 index 0000000..b53eda2 --- /dev/null +++ b/financer-web-client/src/main/java/de/financer/template/GetAccountGroupExpensesCurrentExpensePeriodTemplate.java @@ -0,0 +1,20 @@ +package de.financer.template; + +import de.financer.config.FinancerConfig; +import de.financer.controller.Function; +import de.financer.dto.AccountGroupExpense; +import de.financer.util.ControllerUtils; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.ResponseEntity; +import org.springframework.web.util.UriComponentsBuilder; + +public class GetAccountGroupExpensesCurrentExpensePeriodTemplate { + public ResponseEntity> exchange(FinancerConfig financerConfig) { + final UriComponentsBuilder expensesBuilder = UriComponentsBuilder + .fromHttpUrl(ControllerUtils.buildUrl(financerConfig, Function.ACC_GP_GET_ACC_GP_EXPENSES_CURRENT_EXPENSE_PERIOD)); + + return new FinancerRestTemplate>() + .exchange(expensesBuilder.toUriString(), new ParameterizedTypeReference>() { + }); + } +} diff --git a/financer-web-client/src/main/java/de/financer/template/GetCurrentExpensePeriodTemplate.java b/financer-web-client/src/main/java/de/financer/template/GetCurrentExpensePeriodTemplate.java new file mode 100644 index 0000000..1c8ec62 --- /dev/null +++ b/financer-web-client/src/main/java/de/financer/template/GetCurrentExpensePeriodTemplate.java @@ -0,0 +1,20 @@ +package de.financer.template; + +import de.financer.config.FinancerConfig; +import de.financer.controller.Function; +import de.financer.model.Period; +import de.financer.util.ControllerUtils; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.ResponseEntity; +import org.springframework.web.util.UriComponentsBuilder; + +public class GetCurrentExpensePeriodTemplate { + public ResponseEntity exchange(FinancerConfig financerConfig) { + final UriComponentsBuilder expensesBuilder = UriComponentsBuilder + .fromHttpUrl(ControllerUtils.buildUrl(financerConfig, Function.P_GET_CURRENT_EXPENSE_PERIOD)); + + return new FinancerRestTemplate() + .exchange(expensesBuilder.toUriString(), new ParameterizedTypeReference() { + }); + } +} diff --git a/financer-web-client/src/main/java/de/financer/template/GetExpensePeriodTotalsTemplate.java b/financer-web-client/src/main/java/de/financer/template/GetExpensePeriodTotalsTemplate.java index 3c4795f..60daec7 100644 --- a/financer-web-client/src/main/java/de/financer/template/GetExpensePeriodTotalsTemplate.java +++ b/financer-web-client/src/main/java/de/financer/template/GetExpensePeriodTotalsTemplate.java @@ -12,7 +12,6 @@ public class GetExpensePeriodTotalsTemplate { public ResponseEntity> exchange(FinancerConfig financerConfig, int year) { final UriComponentsBuilder builder = UriComponentsBuilder .fromHttpUrl(ControllerUtils.buildUrl(financerConfig, Function.TR_EXPENSE_PERIOD_TOTALS)) - .queryParam("monthPeriodStartDay", financerConfig.getMonthPeriodStartDay()) .queryParam("year", year); return new FinancerRestTemplate>() diff --git a/financer-web-client/src/main/java/de/financer/template/GetExpensesCurrentPeriodTemplate.java b/financer-web-client/src/main/java/de/financer/template/GetExpensesCurrentPeriodTemplate.java index 4f10b25..3ee803b 100644 --- a/financer-web-client/src/main/java/de/financer/template/GetExpensesCurrentPeriodTemplate.java +++ b/financer-web-client/src/main/java/de/financer/template/GetExpensesCurrentPeriodTemplate.java @@ -10,8 +10,7 @@ import org.springframework.web.util.UriComponentsBuilder; public class GetExpensesCurrentPeriodTemplate { public ResponseEntity exchange(FinancerConfig financerConfig) { final UriComponentsBuilder transactionBuilder = UriComponentsBuilder - .fromHttpUrl(ControllerUtils.buildUrl(financerConfig, Function.TR_EXPENSES_CURRENT_PERIOD)) - .queryParam("monthPeriodStartDay", financerConfig.getMonthPeriodStartDay()); + .fromHttpUrl(ControllerUtils.buildUrl(financerConfig, Function.TR_EXPENSES_CURRENT_PERIOD)); return new FinancerRestTemplate() .exchange(transactionBuilder.toUriString(), new ParameterizedTypeReference() { diff --git a/financer-web-client/src/main/java/de/financer/util/ControllerUtils.java b/financer-web-client/src/main/java/de/financer/util/ControllerUtils.java index 52ece98..6c5ebd4 100644 --- a/financer-web-client/src/main/java/de/financer/util/ControllerUtils.java +++ b/financer-web-client/src/main/java/de/financer/util/ControllerUtils.java @@ -44,6 +44,10 @@ public class ControllerUtils { } public static String formatDate(FinancerConfig financerConfig, LocalDate date) { + if (date == null) { + return null; + } + return date.format(DateTimeFormatter.ofPattern(financerConfig.getDateFormat())); } diff --git a/financer-web-client/src/main/resources/i18n/message.properties b/financer-web-client/src/main/resources/i18n/message.properties index 6a00e8b..6e0d56a 100644 --- a/financer-web-client/src/main/resources/i18n/message.properties +++ b/financer-web-client/src/main/resources/i18n/message.properties @@ -8,6 +8,7 @@ financer.account-overview.available-actions.create-recurring-transaction=Create 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.select-chart=Generate a chart +financer.account-overview.available-actions.close-current-period=Close the current expense period financer.account-overview.status=Status\: financer.account-overview.status.recurring-transaction-due-today=Recurring transactions due today\: financer.account-overview.status.recurring-transaction-active=Active recurring transactions\: @@ -16,10 +17,12 @@ financer.account-overview.status.current-expenses=Expenses in the current period financer.account-overview.table-header.id=ID financer.account-overview.table-header.key=Account financer.account-overview.table-header.group=Group -financer.account-overview.table-header.balance=Current Balance +financer.account-overview.table-header.balance=Balance +financer.account-overview.table-header.spending-current-period=Spending current period +financer.account-overview.table-header.average-spending-period=Average spending financer.account-overview.table-header.type=Type financer.account-overview.table-header.status=Status -financer.account-overview.tooltip.status.current-expenses=Period from {0} to {1}. Clicking the amount will open a graphical overview about the expenses grouped by account group +financer.account-overview.tooltip.status.current-expenses=Period starting at {0}. Clicking the amount will open a graphical overview about the expenses grouped by account group financer.account-overview.tooltip.status.current-assets=Assets available at short-notice financer.account-new.title=financer\: create new account @@ -88,6 +91,7 @@ financer.account-details.available-actions=Available actions\: financer.account-details.available-actions.close-account=Close 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.create-transaction=Create new transaction financer.account-details.table-header.id=ID financer.account-details.table-header.fromAccount=From account financer.account-details.table-header.toAccount=To account @@ -163,6 +167,7 @@ financer.chart.account-expenses-for-period.title=Expenses for period from {0} to financer.chart.expense-period-totals-current-year.title=Expense period totals for the current year financer.chart.expense-period-totals-current-year.x=Amount financer.chart.expense-period-totals-current-year.y=Period +financer.chart.expense-period-totals-current-year.open-period=OPEN financer.chart.name.ACCOUNT_GROUP_EXPENSES_CURRENT_PERIOD=Expenses of the current period grouped by account group (pie chart) financer.chart.name.ACCOUNT_GROUP_EXPENSES_FOR_PERIOD=Expenses for a configurable period grouped by account group (pie chart) diff --git a/financer-web-client/src/main/resources/i18n/message_de_DE.properties b/financer-web-client/src/main/resources/i18n/message_de_DE.properties index fee2ccd..7af956e 100644 --- a/financer-web-client/src/main/resources/i18n/message_de_DE.properties +++ b/financer-web-client/src/main/resources/i18n/message_de_DE.properties @@ -8,6 +8,7 @@ financer.account-overview.available-actions.create-recurring-transaction=Neue wi 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.select-chart=Ein Diagramm erzeugen +financer.account-overview.available-actions.close-current-period=Aktuelle Periode schlie\u00DFen financer.account-overview.status=Status\: financer.account-overview.status.recurring-transaction-due-today=Wiederkehrende Buchungen heute f\u00E4llig\: financer.account-overview.status.recurring-transaction-active=Aktive wiederkehrende Buchungen\: @@ -19,7 +20,7 @@ financer.account-overview.table-header.group=Gruppe financer.account-overview.table-header.balance=Kontostand financer.account-overview.table-header.type=Typ financer.account-overview.table-header.status=Status -financer.account-overview.tooltip.status.current-expenses=Periode ab {0} bis {1}. 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 financer.account-overview.tooltip.status.current-assets=Kurzfristig verf\u00FCgbares Verm\u00F6gen financer.account-new.title=financer\: Neues Konto erstellen @@ -88,6 +89,7 @@ financer.account-details.available-actions=Verf\u00FCgbare Aktionen\: financer.account-details.available-actions.close-account=Konto schlie\u00DFen 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.create-transaction=Neue Buchung erstellen financer.account-details.table-header.id=ID financer.account-details.table-header.fromAccount=Von Konto financer.account-details.table-header.toAccount=An Konto @@ -162,6 +164,7 @@ financer.chart.account-expenses-for-period.title=Ausgaben in der Periode vom {0} financer.chart.expense-period-totals-current-year.title=Gesamtbetr\u00E4ge gruppiert nach Periode für das aktuelle Jahr financer.chart.expense-period-totals-current-year.x=Betrag financer.chart.expense-period-totals-current-year.y=Periode +financer.chart.expense-period-totals-current-year.open-period=OFFEN financer.chart.name.ACCOUNT_GROUP_EXPENSES_CURRENT_PERIOD=Ausgaben in der aktuellen Periode gruppiert nach Konto-Gruppe (Kuchendiagramm) financer.chart.name.ACCOUNT_GROUP_EXPENSES_FOR_PERIOD=Ausgaben f\u00FCr eine konfigurierbare Periode gruppiert nach Konto-Gruppe (Kuchendiagramm) diff --git a/financer-web-client/src/main/resources/static/changelog.txt b/financer-web-client/src/main/resources/static/changelog.txt index eeba98f..54a2c1b 100644 --- a/financer-web-client/src/main/resources/static/changelog.txt +++ b/financer-web-client/src/main/resources/static/changelog.txt @@ -1,3 +1,15 @@ +v18 -> v19: +- Fix a bug in recurring transaction handling that caused a recurring transaction with Holiday/Weekend type + 'previous workday' and a planned occurrence on a Sunday to be due on Friday and Saturday +- Filter from and to account lists when creating a transaction or a recurring transaction by account types that + are allowed in from and to +- Add the create new transaction action to the account detail view +- Rework periods (e.g. expense) so that the underlying technical structures are better defined. This is preparation for + more reports and budgeting functions +- Add action to close the current expense period manually. This also opens a new one +- Add spending in current expense period per account to the account overview +- Add average spending in expense period per account to the account overview + v17 -> v18: - Add readme to the footer - Translate error messages to German diff --git a/financer-web-client/src/main/resources/templates/account/accountDetails.html b/financer-web-client/src/main/resources/templates/account/accountDetails.html index ea9219c..a2657dc 100644 --- a/financer-web-client/src/main/resources/templates/account/accountDetails.html +++ b/financer-web-client/src/main/resources/templates/account/accountDetails.html @@ -18,9 +18,9 @@ -
+
- +
@@ -29,6 +29,8 @@ th:text="#{financer.account-details.available-actions.close-account}"/> +
diff --git a/financer-web-client/src/main/resources/templates/account/accountOverview.html b/financer-web-client/src/main/resources/templates/account/accountOverview.html index a2dbc17..62c099e 100644 --- a/financer-web-client/src/main/resources/templates/account/accountOverview.html +++ b/financer-web-client/src/main/resources/templates/account/accountOverview.html @@ -8,13 +8,14 @@

+