Initial commit for financer

This commit is contained in:
2019-02-15 21:30:10 +01:00
commit 86ccb0b52c
24 changed files with 1002 additions and 0 deletions

View 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);
}
}

View 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);
}
}

View 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();
}
}

View File

@@ -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();
}
}

View 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);
}

View File

@@ -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> {
}

View 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);
}

View 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;
}
}

View 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;
}

View 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));
}
}

View 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));
}
}

View 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));
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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());
}
}

View 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;
}
}

View File

@@ -0,0 +1,2 @@
# Hibernate
spring.jpa.show-sql=true

View 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

View File

@@ -0,0 +1 @@
spring.flyway.locations=classpath:/database/postgres

View 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@

View 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)
);