Initial commit for financer
This commit is contained in:
11
src/main/java/de/financer/FinancerApplication.java
Normal file
11
src/main/java/de/financer/FinancerApplication.java
Normal file
@@ -0,0 +1,11 @@
|
||||
package de.financer;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class FinancerApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(FinancerApplication.class);
|
||||
}
|
||||
}
|
||||
29
src/main/java/de/financer/ResponseReason.java
Normal file
29
src/main/java/de/financer/ResponseReason.java
Normal file
@@ -0,0 +1,29 @@
|
||||
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.INTERNAL_SERVER_ERROR),
|
||||
INVALID_ACCOUNT_KEY(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);
|
||||
|
||||
private HttpStatus httpStatus;
|
||||
|
||||
ResponseReason(HttpStatus httpStatus) {
|
||||
this.httpStatus = httpStatus;
|
||||
}
|
||||
|
||||
public ResponseEntity toResponseEntity() {
|
||||
return new ResponseEntity(this.name(), this.httpStatus);
|
||||
}
|
||||
}
|
||||
41
src/main/java/de/financer/controller/AccountController.java
Normal file
41
src/main/java/de/financer/controller/AccountController.java
Normal file
@@ -0,0 +1,41 @@
|
||||
package de.financer.controller;
|
||||
|
||||
import de.financer.model.Account;
|
||||
import de.financer.service.AccountService;
|
||||
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("accounts")
|
||||
public class AccountController {
|
||||
|
||||
@Autowired
|
||||
private AccountService accountService;
|
||||
|
||||
@RequestMapping("getByKey")
|
||||
public Account getAccountByKey(String key) {
|
||||
return this.accountService.getAccountByKey(key);
|
||||
}
|
||||
|
||||
@RequestMapping("getAll")
|
||||
public Iterable<Account> getAll() {
|
||||
return this.accountService.getAll();
|
||||
}
|
||||
|
||||
@RequestMapping("getAccountTypes")
|
||||
public Iterable<String> getAccountTypes() {
|
||||
return this.accountService.getAccountTypes();
|
||||
}
|
||||
|
||||
@RequestMapping("getAccountStatus")
|
||||
public Iterable<String> getAccountStatus() {
|
||||
return this.accountService.getAccountStatus();
|
||||
}
|
||||
|
||||
@RequestMapping("createAccount")
|
||||
public ResponseEntity createAccount(String key, String type) {
|
||||
return this.accountService.createAccount(key, type).toResponseEntity();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package de.financer.controller;
|
||||
|
||||
import de.financer.model.Transaction;
|
||||
import de.financer.service.TransactionService;
|
||||
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("transactions")
|
||||
public class TransactionController {
|
||||
@Autowired
|
||||
private TransactionService transactionService;
|
||||
|
||||
@RequestMapping("getAll")
|
||||
public Iterable<Transaction> getAll() {
|
||||
return this.transactionService.getAll();
|
||||
}
|
||||
|
||||
@RequestMapping("getAllForAccount")
|
||||
public Iterable<Transaction> getAllForAccount(String accountKey) {
|
||||
return this.transactionService.getAllForAccount(accountKey);
|
||||
}
|
||||
|
||||
@RequestMapping("createTransaction")
|
||||
public ResponseEntity createTransaction(String fromAccountKey, String toAccountKey, Long amount, String date, String description) {
|
||||
return this.transactionService.createTransaction(fromAccountKey, toAccountKey, amount, date, description).toResponseEntity();
|
||||
}
|
||||
}
|
||||
11
src/main/java/de/financer/dba/AccountRepository.java
Normal file
11
src/main/java/de/financer/dba/AccountRepository.java
Normal file
@@ -0,0 +1,11 @@
|
||||
package de.financer.dba;
|
||||
|
||||
import de.financer.model.Account;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Transactional(propagation = Propagation.REQUIRED)
|
||||
public interface AccountRepository extends CrudRepository<Account, Long> {
|
||||
Account findByKey(String key);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package de.financer.dba;
|
||||
|
||||
import de.financer.model.RecurringTransaction;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
|
||||
public interface RecurringTransactionRepository extends CrudRepository<RecurringTransaction, Long> {
|
||||
}
|
||||
12
src/main/java/de/financer/dba/TransactionRepository.java
Normal file
12
src/main/java/de/financer/dba/TransactionRepository.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package de.financer.dba;
|
||||
|
||||
import de.financer.model.Account;
|
||||
import de.financer.model.Transaction;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Transactional(propagation = Propagation.REQUIRED)
|
||||
public interface TransactionRepository extends CrudRepository<Transaction, Long> {
|
||||
Iterable<Transaction> findTransactionsByFromAccountOrToAccount(Account fromAccount, Account toAccount);
|
||||
}
|
||||
53
src/main/java/de/financer/model/Account.java
Normal file
53
src/main/java/de/financer/model/Account.java
Normal file
@@ -0,0 +1,53 @@
|
||||
package de.financer.model;
|
||||
|
||||
import javax.persistence.*;
|
||||
|
||||
@Entity
|
||||
public class Account {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
@Column(name = "\"key\"") // we need to escape the keyword "key"
|
||||
private String key;
|
||||
@Enumerated(EnumType.STRING)
|
||||
private AccountType type;
|
||||
@Enumerated(EnumType.STRING)
|
||||
private AccountStatus status;
|
||||
private Long currentBalance;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public void setKey(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public AccountType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(AccountType type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public AccountStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(AccountStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public Long getCurrentBalance() {
|
||||
return currentBalance;
|
||||
}
|
||||
|
||||
public void setCurrentBalance(Long currentBalance) {
|
||||
this.currentBalance = currentBalance;
|
||||
}
|
||||
}
|
||||
8
src/main/java/de/financer/model/AccountStatus.java
Normal file
8
src/main/java/de/financer/model/AccountStatus.java
Normal file
@@ -0,0 +1,8 @@
|
||||
package de.financer.model;
|
||||
|
||||
public enum AccountStatus {
|
||||
/** Indicates that the account is open for bookings */
|
||||
OPEN,
|
||||
/** Indicates that the account is closed and bookings to it are forbidden */
|
||||
CLOSED;
|
||||
}
|
||||
33
src/main/java/de/financer/model/AccountType.java
Normal file
33
src/main/java/de/financer/model/AccountType.java
Normal file
@@ -0,0 +1,33 @@
|
||||
package de.financer.model;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public enum AccountType {
|
||||
/** Used to mark an account that acts as a source of money, e.g. monthly wage */
|
||||
INCOME,
|
||||
|
||||
/** Indicates a real account at a bank, e.g. a check payment account */
|
||||
BANK,
|
||||
|
||||
/** Marks an account as physical cash, e.g. the money currently in the purse */
|
||||
CASH,
|
||||
|
||||
/** Used to mark an account that acts as a destination of money, e.g. through buying goods */
|
||||
EXPENSE,
|
||||
|
||||
/** Marks an account as a liability from a third party, e.g. credit card or loan */
|
||||
LIABILITY,
|
||||
|
||||
/** Marks the start account that is to be used to book all the opening balances for the different accounts */
|
||||
START;
|
||||
|
||||
/**
|
||||
* This method validates whether the given string represents a valid account type.
|
||||
*
|
||||
* @param type to check
|
||||
* @return whether the given type represents a valid account type
|
||||
*/
|
||||
public static boolean isValidType(String type) {
|
||||
return Arrays.asList(AccountType.values()).stream().anyMatch((accountType) -> accountType.name().equals(type));
|
||||
}
|
||||
}
|
||||
28
src/main/java/de/financer/model/HolidayWeekendType.java
Normal file
28
src/main/java/de/financer/model/HolidayWeekendType.java
Normal file
@@ -0,0 +1,28 @@
|
||||
package de.financer.model;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* This enum specifies constants that control how actions should be handled that would fall on a holiday
|
||||
* or weekday (where usually are no bookings done by e.g. banks)
|
||||
*/
|
||||
public enum HolidayWeekendType {
|
||||
/** Indicates that the action should be done on the specified day regardless whether it's a holiday or a weekend */
|
||||
SAME_DAY,
|
||||
|
||||
/** Indicates that the action should be deferred to the next workday */
|
||||
NEXT_WORKDAY,
|
||||
|
||||
/** Indicates that the action should be dated back to the previous day */
|
||||
PREVIOUS_WORKDAY;
|
||||
|
||||
/**
|
||||
* This method validates whether the given string represents a valid holiday weekend type.
|
||||
*
|
||||
* @param type to check
|
||||
* @return whether the given type represents a valid holiday weekend type
|
||||
*/
|
||||
public static boolean isValidType(String type) {
|
||||
return Arrays.asList(HolidayWeekendType.values()).stream().anyMatch((holidayWeekendType) -> holidayWeekendType.name().equals(type));
|
||||
}
|
||||
}
|
||||
30
src/main/java/de/financer/model/IntervalType.java
Normal file
30
src/main/java/de/financer/model/IntervalType.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package de.financer.model;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public enum IntervalType {
|
||||
/** Indicates that an action should be executed every day */
|
||||
DAILY,
|
||||
|
||||
/** Indicates that an action should be executed once a week */
|
||||
WEEKLY,
|
||||
|
||||
/** Indicates that an action should be executed once a month */
|
||||
MONTHLY,
|
||||
|
||||
/** Indicates that an action should be executed once a quarter */
|
||||
QUARTERLY,
|
||||
|
||||
/** Indicates that an action should be executed once a year */
|
||||
YEARLY;
|
||||
|
||||
/**
|
||||
* This method validates whether the given string represents a valid interval type.
|
||||
*
|
||||
* @param type to check
|
||||
* @return whether the given type represents a valid interval type
|
||||
*/
|
||||
public static boolean isValidType(String type) {
|
||||
return Arrays.asList(IntervalType.values()).stream().anyMatch((intervalType) -> intervalType.name().equals(type));
|
||||
}
|
||||
}
|
||||
93
src/main/java/de/financer/model/RecurringTransaction.java
Normal file
93
src/main/java/de/financer/model/RecurringTransaction.java
Normal file
@@ -0,0 +1,93 @@
|
||||
package de.financer.model;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.util.Date;
|
||||
|
||||
@Entity
|
||||
public class RecurringTransaction {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
@OneToOne(fetch = FetchType.EAGER)
|
||||
private Account fromAccount;
|
||||
@OneToOne(fetch = FetchType.EAGER)
|
||||
private Account toAccount;
|
||||
private String description;
|
||||
private Long amount;
|
||||
@Enumerated(EnumType.STRING)
|
||||
private IntervalType intervalType;
|
||||
@Temporal(TemporalType.DATE)
|
||||
private Date firstOccurrence;
|
||||
@Temporal(TemporalType.DATE)
|
||||
private Date lastOccurrence;
|
||||
@Enumerated(EnumType.STRING)
|
||||
private HolidayWeekendType holidayWeekendType;
|
||||
|
||||
public Long getId() {
|
||||
return 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 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 HolidayWeekendType getHolidayWeekendType() {
|
||||
return holidayWeekendType;
|
||||
}
|
||||
|
||||
public void setHolidayWeekendType(HolidayWeekendType holidayWeekendType) {
|
||||
this.holidayWeekendType = holidayWeekendType;
|
||||
}
|
||||
|
||||
public Date getLastOccurrence() {
|
||||
return lastOccurrence;
|
||||
}
|
||||
|
||||
public void setLastOccurrence(Date lastOccurrence) {
|
||||
this.lastOccurrence = lastOccurrence;
|
||||
}
|
||||
|
||||
public Date getFirstOccurrence() {
|
||||
return firstOccurrence;
|
||||
}
|
||||
|
||||
public void setFirstOccurrence(Date firstOccurrence) {
|
||||
this.firstOccurrence = firstOccurrence;
|
||||
}
|
||||
|
||||
public IntervalType getIntervalType() {
|
||||
return intervalType;
|
||||
}
|
||||
|
||||
public void setIntervalType(IntervalType intervalType) {
|
||||
this.intervalType = intervalType;
|
||||
}
|
||||
}
|
||||
65
src/main/java/de/financer/model/Transaction.java
Normal file
65
src/main/java/de/financer/model/Transaction.java
Normal file
@@ -0,0 +1,65 @@
|
||||
package de.financer.model;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.util.Date;
|
||||
|
||||
@Entity
|
||||
@Table(name = "\"transaction\"")
|
||||
public class Transaction {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
@OneToOne(fetch = FetchType.EAGER)
|
||||
private Account fromAccount;
|
||||
@OneToOne(fetch = FetchType.EAGER)
|
||||
private Account toAccount;
|
||||
@Temporal(TemporalType.DATE)
|
||||
@Column(name = "\"date\"")
|
||||
private Date date;
|
||||
private String description;
|
||||
private Long amount;
|
||||
|
||||
public Long getId() {
|
||||
return 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 Date getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public void setDate(Date 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;
|
||||
}
|
||||
}
|
||||
104
src/main/java/de/financer/service/AccountService.java
Normal file
104
src/main/java/de/financer/service/AccountService.java
Normal file
@@ -0,0 +1,104 @@
|
||||
package de.financer.service;
|
||||
|
||||
import de.financer.ResponseReason;
|
||||
import de.financer.dba.AccountRepository;
|
||||
import de.financer.model.Account;
|
||||
import de.financer.model.AccountStatus;
|
||||
import de.financer.model.AccountType;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
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.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class AccountService {
|
||||
@Autowired
|
||||
private AccountRepository accountRepository;
|
||||
|
||||
/**
|
||||
* This method returns the account identified by the given key.
|
||||
*
|
||||
* @param key the key to get the account for
|
||||
* @return the account or <code>null</code> if no account with the given key can be found
|
||||
*/
|
||||
public Account getAccountByKey(String key) {
|
||||
return this.accountRepository.findByKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return all existing accounts, regardless of their type or status. This explicitly covers accounts in {@link AccountStatus#CLOSED CLOSED} as well.
|
||||
*/
|
||||
public Iterable<Account> getAll() {
|
||||
return this.accountRepository.findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return all possible account types as specified by the {@link AccountType} enumeration, never <code>null</code>
|
||||
*/
|
||||
public Iterable<String> getAccountTypes() {
|
||||
return Arrays.asList(AccountType.values()).stream().map(AccountType::name).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return all possible account status as specified by the {@link AccountStatus} enumeration, never <code>null</code>
|
||||
*/
|
||||
public Iterable<String> getAccountStatus() {
|
||||
return Arrays.asList(AccountStatus.values()).stream().map(AccountStatus::name).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* This method saves the given account. It either updates the account if it already exists or inserts
|
||||
* it if it's new.
|
||||
*
|
||||
* @param account the account to save
|
||||
*/
|
||||
@Transactional(propagation = Propagation.REQUIRED)
|
||||
public void saveAccount(Account account) {
|
||||
this.accountRepository.save(account);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method creates new account with the given key and type. The account has status {@link AccountStatus#OPEN OPEN}
|
||||
* and a current balance of <code>0</code>.
|
||||
*
|
||||
* @param key the key of the new account. Must begin with <b>account.</b>
|
||||
* @param type the type of the new account. Must be one of {@link AccountType}.
|
||||
* @return {@link ResponseReason#INVALID_ACCOUNT_TYPE} if the given type is not a valid {@link AccountType},
|
||||
* {@link ResponseReason#INVALID_ACCOUNT_KEY} if the given key does not conform to the format specification,
|
||||
* {@link ResponseReason#UNKNOWN_ERROR} if an unexpected error occurs and
|
||||
* {@link ResponseReason#OK} if the operation completed successfully. Never returns <code>null</code>.
|
||||
*/
|
||||
@Transactional(propagation = Propagation.REQUIRED)
|
||||
public ResponseReason createAccount(String key, String type) {
|
||||
if (!AccountType.isValidType(type)) {
|
||||
return ResponseReason.INVALID_ACCOUNT_TYPE;
|
||||
}
|
||||
|
||||
if (!StringUtils.startsWith(key, "accounts.")) {
|
||||
return ResponseReason.INVALID_ACCOUNT_KEY;
|
||||
}
|
||||
|
||||
final Account account = new Account();
|
||||
|
||||
account.setKey(key);
|
||||
account.setType(AccountType.valueOf(type));
|
||||
// If we create an account it's implicitly open
|
||||
account.setStatus(AccountStatus.OPEN);
|
||||
// and has a current balance of 0
|
||||
account.setCurrentBalance(Long.valueOf(0l));
|
||||
|
||||
try {
|
||||
this.accountRepository.save(account);
|
||||
}
|
||||
catch (Exception e) {
|
||||
// TODO log and check for unique constraint exception so we can return a more specific error
|
||||
return ResponseReason.UNKNOWN_ERROR;
|
||||
}
|
||||
|
||||
return ResponseReason.OK;
|
||||
}
|
||||
}
|
||||
114
src/main/java/de/financer/service/RuleService.java
Normal file
114
src/main/java/de/financer/service/RuleService.java
Normal file
@@ -0,0 +1,114 @@
|
||||
package de.financer.service;
|
||||
|
||||
import de.financer.model.Account;
|
||||
import de.financer.model.AccountType;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static de.financer.model.AccountType.*;
|
||||
|
||||
/**
|
||||
* This service encapsulates methods that form business logic rules.
|
||||
* While most of the logic could be placed elsewhere this service provides
|
||||
* centralized access to these rules.
|
||||
*/
|
||||
@Service
|
||||
public class RuleService implements InitializingBean {
|
||||
|
||||
private Map<AccountType, Collection<AccountType>> bookingRules;
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
initBookingRules();
|
||||
}
|
||||
|
||||
private void initBookingRules() {
|
||||
this.bookingRules = new EnumMap<>(AccountType.class);
|
||||
|
||||
// This map contains valid booking constellations
|
||||
// The key is the from account and the value is a list of valid
|
||||
// to accounts for this from account
|
||||
this.bookingRules.put(INCOME, Arrays.asList(BANK, CASH));
|
||||
this.bookingRules.put(BANK, Arrays.asList(CASH, EXPENSE, LIABILITY));
|
||||
this.bookingRules.put(CASH, Arrays.asList(BANK, EXPENSE, LIABILITY));
|
||||
this.bookingRules.put(EXPENSE, Collections.emptyList());
|
||||
this.bookingRules.put(LIABILITY, Arrays.asList(BANK, CASH, EXPENSE));
|
||||
this.bookingRules.put(START, Arrays.asList(BANK, CASH, LIABILITY));
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns the multiplier for the given from account.
|
||||
*
|
||||
* The multiplier controls whether the current amount of the given from account is increased or
|
||||
* decreased depending on the {@link AccountType} of the given account.
|
||||
*
|
||||
* @param fromAccount the from account to get the multiplier for
|
||||
* @return the multiplier, either <code>1</code> or <code>-1</code>
|
||||
*/
|
||||
public long getMultiplierFromAccount(Account fromAccount) {
|
||||
// There is no multiplier if the from account is an EXPENSE account because
|
||||
// it's not a valid from account type
|
||||
|
||||
if (INCOME.equals(fromAccount)) {
|
||||
return 1L;
|
||||
}
|
||||
else if (BANK.equals(fromAccount)) {
|
||||
return -1L;
|
||||
}
|
||||
else if (CASH.equals(fromAccount)) {
|
||||
return -1L;
|
||||
}
|
||||
else if (LIABILITY.equals(fromAccount)) {
|
||||
return 1L;
|
||||
}
|
||||
else if (START.equals(fromAccount)) {
|
||||
return 1L;
|
||||
}
|
||||
|
||||
return 1L;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns the multiplier for the given to account.
|
||||
*
|
||||
* The multiplier controls whether the current amount of the given to account is increased or
|
||||
* decreased depending on the {@link AccountType} of the given account.
|
||||
*
|
||||
* @param toAccount the to account to get the multiplier for
|
||||
* @return the multiplier, either <code>1</code> or <code>-1</code>
|
||||
*/
|
||||
public long getMultiplierToAccount(Account toAccount) {
|
||||
// There are no multipliers for INCOME and START accounts
|
||||
// because they are not valid to account types
|
||||
|
||||
if (BANK.equals(toAccount)) {
|
||||
return 1L;
|
||||
}
|
||||
else if (CASH.equals(toAccount)) {
|
||||
return 1L;
|
||||
}
|
||||
else if (LIABILITY.equals(toAccount)) {
|
||||
return -1L;
|
||||
}
|
||||
else if (EXPENSE.equals(toAccount)) {
|
||||
return 1L;
|
||||
}
|
||||
|
||||
return -1L;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method validates whether the booking from <code>fromAccount</code> to <code>toAccount</code>
|
||||
* is valid, e.g. booking directly from an {@link AccountType#INCOME INCOME} to an {@link AccountType#EXPENSE EXPENSE}
|
||||
* account does not make sense and is declared as invalid.
|
||||
*
|
||||
* @param fromAccount the account to subtract the money from
|
||||
* @param toAccount the account to add the money to
|
||||
* @return <code>true</code> if the from->to relationship of the given accounts is valid, <code>false</code> otherwise
|
||||
*/
|
||||
public boolean isValidBooking(Account fromAccount, Account toAccount) {
|
||||
return this.bookingRules.get(fromAccount.getType()).contains(toAccount.getType());
|
||||
}
|
||||
}
|
||||
149
src/main/java/de/financer/service/TransactionService.java
Normal file
149
src/main/java/de/financer/service/TransactionService.java
Normal file
@@ -0,0 +1,149 @@
|
||||
package de.financer.service;
|
||||
|
||||
import de.financer.ResponseReason;
|
||||
import de.financer.dba.TransactionRepository;
|
||||
import de.financer.model.Account;
|
||||
import de.financer.model.Transaction;
|
||||
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.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Collections;
|
||||
|
||||
@Service
|
||||
public class TransactionService {
|
||||
public static final String DATE_FORMAT = "dd.MM.yyyy";
|
||||
|
||||
@Autowired
|
||||
private AccountService accountService;
|
||||
|
||||
@Autowired
|
||||
private RuleService ruleService;
|
||||
|
||||
@Autowired
|
||||
private TransactionRepository transactionRepository;
|
||||
|
||||
/**
|
||||
* @return all transactions, for all accounts and all time
|
||||
*/
|
||||
public Iterable<Transaction> getAll() {
|
||||
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) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// As we want all transactions of the given account use it as from and to account
|
||||
return this.transactionRepository.findTransactionsByFromAccountOrToAccount(account, account);
|
||||
}
|
||||
|
||||
@Transactional(propagation = Propagation.REQUIRED)
|
||||
public ResponseReason createTransaction(String fromAccountKey, String toAccountKey, Long amount, String date, String description) {
|
||||
final Account fromAccount = this.accountService.getAccountByKey(fromAccountKey);
|
||||
final Account toAccount = this.accountService.getAccountByKey(toAccountKey);
|
||||
ResponseReason response = validateParameters(fromAccount, toAccount, amount, date);
|
||||
|
||||
// If we detected an issue with the given parameters return the first error found to the caller
|
||||
if (response != null) {
|
||||
return response; // early return
|
||||
}
|
||||
|
||||
try {
|
||||
final Transaction transaction = buildTransaction(fromAccount, toAccount, amount, description, date);
|
||||
|
||||
fromAccount.setCurrentBalance(fromAccount.getCurrentBalance() + (this.ruleService.getMultiplierFromAccount(fromAccount) * amount));
|
||||
toAccount.setCurrentBalance(toAccount.getCurrentBalance() + (this.ruleService.getMultiplierToAccount(toAccount) * amount));
|
||||
|
||||
this.transactionRepository.save(transaction);
|
||||
|
||||
this.accountService.saveAccount(fromAccount);
|
||||
this.accountService.saveAccount(toAccount);
|
||||
|
||||
response = ResponseReason.OK;
|
||||
}
|
||||
catch(ParseException e) {
|
||||
// TODO log
|
||||
|
||||
response = ResponseReason.INVALID_DATE_FORMAT;
|
||||
}
|
||||
catch (Exception e) {
|
||||
// TODO log
|
||||
|
||||
response = ResponseReason.UNKNOWN_ERROR;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method builds the actual transaction object with the given values.
|
||||
*
|
||||
* @param fromAccount the from account
|
||||
* @param toAccount the to account
|
||||
* @param amount the transaction amount
|
||||
* @param description the description of the transaction
|
||||
* @param date the date of the transaction
|
||||
* @return the build {@link Transaction} instance
|
||||
* @throws ParseException if the given date string cannot be parsed into a {@link java.util.Date} instance
|
||||
*/
|
||||
private Transaction buildTransaction(Account fromAccount, Account toAccount, Long amount, String description, String date) throws ParseException {
|
||||
final Transaction transaction = new Transaction();
|
||||
|
||||
transaction.setFromAccount(fromAccount);
|
||||
transaction.setToAccount(toAccount);
|
||||
transaction.setAmount(amount);
|
||||
transaction.setDescription(description);
|
||||
transaction.setDate(new SimpleDateFormat(DATE_FORMAT).parse(date));
|
||||
|
||||
return transaction;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method checks whether the parameters for creating a transaction are valid.
|
||||
*
|
||||
* @param fromAccount the from account
|
||||
* @param toAccount the to account
|
||||
* @param amount the transaction amount
|
||||
* @param date the transaction date
|
||||
* @return the first error found or <code>null</code> if all parameters are valid
|
||||
*/
|
||||
private ResponseReason validateParameters(Account fromAccount, Account toAccount, Long amount, String date) {
|
||||
ResponseReason response = null;
|
||||
|
||||
if (fromAccount == null && toAccount == null) {
|
||||
response = ResponseReason.FROM_AND_TO_ACCOUNT_NOT_FOUND;
|
||||
}
|
||||
else if (toAccount == null) {
|
||||
response = ResponseReason.TO_ACCOUNT_NOT_FOUND;
|
||||
}
|
||||
else if (fromAccount == null) {
|
||||
response = ResponseReason.FROM_ACCOUNT_NOT_FOUND;
|
||||
}
|
||||
else if (!this.ruleService.isValidBooking(fromAccount, toAccount)) {
|
||||
response = ResponseReason.INVALID_BOOKING_ACCOUNTS;
|
||||
}
|
||||
else if (amount == null) {
|
||||
response = ResponseReason.MISSING_AMOUNT;
|
||||
}
|
||||
else if (amount == 0l) {
|
||||
response = ResponseReason.AMOUNT_ZERO;
|
||||
}
|
||||
else if (date == null) {
|
||||
response = ResponseReason.MISSING_DATE;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
2
src/main/resources/config/application-dev.properties
Normal file
2
src/main/resources/config/application-dev.properties
Normal file
@@ -0,0 +1,2 @@
|
||||
# Hibernate
|
||||
spring.jpa.show-sql=true
|
||||
6
src/main/resources/config/application-hsqldb.properties
Normal file
6
src/main/resources/config/application-hsqldb.properties
Normal file
@@ -0,0 +1,6 @@
|
||||
spring.flyway.locations=classpath:/database/hsqldb
|
||||
|
||||
# DataSource
|
||||
#spring.datasource.url=jdbc:hsqldb:file:/tmp/financer
|
||||
spring.datasource.url=jdbc:hsqldb:mem:.
|
||||
spring.datasource.username=sa
|
||||
@@ -0,0 +1 @@
|
||||
spring.flyway.locations=classpath:/database/postgres
|
||||
15
src/main/resources/config/application.properties
Normal file
15
src/main/resources/config/application.properties
Normal file
@@ -0,0 +1,15 @@
|
||||
###
|
||||
### This is the main configuration file of the application
|
||||
###
|
||||
|
||||
spring.profiles.active=hsqldb,dev
|
||||
|
||||
server.servlet.context-path=/financer
|
||||
|
||||
spring.jpa.hibernate.ddl-auto=validate
|
||||
|
||||
info.app.name=Financer
|
||||
info.app.description=A simple server for personal finance administration
|
||||
info.build.group=@project.groupId@
|
||||
info.build.artifact=@project.artifactId@
|
||||
info.build.version=@project.version@
|
||||
55
src/main/resources/database/hsqldb/V1_0_0__init.sql
Normal file
55
src/main/resources/database/hsqldb/V1_0_0__init.sql
Normal file
@@ -0,0 +1,55 @@
|
||||
--
|
||||
-- This file contains the basic initialization of the financer schema and init data
|
||||
--
|
||||
|
||||
-- Account table and init data
|
||||
CREATE TABLE account (
|
||||
id BIGINT NOT NULL PRIMARY KEY IDENTITY,
|
||||
"key" VARCHAR(1000) NOT NULL, --escape keyword "key"
|
||||
type VARCHAR(255) NOT NULL,
|
||||
status VARCHAR(255) NOT NULL,
|
||||
current_balance BIGINT NOT NULL,
|
||||
|
||||
CONSTRAINT c_u_name_key UNIQUE ("key")
|
||||
);
|
||||
|
||||
INSERT INTO account ("key", type, status, current_balance)
|
||||
VALUES ('accounts.checkaccount', 'BANK', 'OPEN', 0);
|
||||
|
||||
INSERT INTO account ("key", type, status, current_balance)
|
||||
VALUES ('accounts.income', 'INCOME', 'OPEN', 0);
|
||||
|
||||
INSERT INTO account ("key", type, status, current_balance)
|
||||
VALUES ('accounts.cash', 'CASH', 'OPEN', 0);
|
||||
|
||||
INSERT INTO account ("key", type, status, current_balance)
|
||||
VALUES ('accounts.start', 'START', 'OPEN', 0);
|
||||
|
||||
-- Transaction table
|
||||
CREATE TABLE "transaction" ( --escape keyword "transaction"
|
||||
id BIGINT NOT NULL PRIMARY KEY IDENTITY,
|
||||
from_account_id BIGINT NOT NULL,
|
||||
to_account_id BIGINT NOT NULL,
|
||||
"date" DATE NOT NULL, --escape keyword "date"
|
||||
description VARCHAR(1000),
|
||||
amount BIGINT NOT NULL,
|
||||
|
||||
CONSTRAINT fk_from_account FOREIGN KEY (from_account_id) REFERENCES account (id),
|
||||
CONSTRAINT fk_to_account FOREIGN KEY (to_account_id) REFERENCES account (id)
|
||||
);
|
||||
|
||||
-- Transaction table
|
||||
CREATE TABLE recurring_transaction (
|
||||
id BIGINT NOT NULL PRIMARY KEY IDENTITY,
|
||||
from_account_id BIGINT NOT NULL,
|
||||
to_account_id BIGINT NOT NULL,
|
||||
description VARCHAR(1000),
|
||||
amount BIGINT NOT NULL,
|
||||
interval_type VARCHAR(255) NOT NULL,
|
||||
first_occurrence DATE NOT NULL,
|
||||
last_occurrence DATE,
|
||||
holiday_weekend_type VARCHAR(255) NOT NULL,
|
||||
|
||||
CONSTRAINT fk_from_account FOREIGN KEY (from_account_id) REFERENCES account (id),
|
||||
CONSTRAINT fk_to_account FOREIGN KEY (to_account_id) REFERENCES account (id)
|
||||
);
|
||||
Reference in New Issue
Block a user