Add balance record "type" attribute, for cash and assets.
This commit is contained in:
parent
d4bd5cc6ec
commit
feda2e1897
|
@ -6,6 +6,7 @@ import com.andrewlalis.perfin.data.BalanceRecordRepository;
|
|||
import com.andrewlalis.perfin.data.util.CurrencyUtil;
|
||||
import com.andrewlalis.perfin.data.util.DateUtil;
|
||||
import com.andrewlalis.perfin.model.Account;
|
||||
import com.andrewlalis.perfin.model.BalanceRecordType;
|
||||
import com.andrewlalis.perfin.model.MoneyValue;
|
||||
import com.andrewlalis.perfin.model.Profile;
|
||||
import com.andrewlalis.perfin.view.component.FileSelectionArea;
|
||||
|
@ -29,6 +30,10 @@ import java.time.format.DateTimeParseException;
|
|||
|
||||
import static com.andrewlalis.perfin.PerfinApp.router;
|
||||
|
||||
/**
|
||||
* Controller for the page where users can create a balance record for an
|
||||
* account.
|
||||
*/
|
||||
public class CreateBalanceRecordController implements RouteSelectionListener {
|
||||
@FXML public TextField timestampField;
|
||||
@FXML public TextField balanceField;
|
||||
|
@ -102,6 +107,7 @@ public class CreateBalanceRecordController implements RouteSelectionListener {
|
|||
repo.insert(
|
||||
DateUtil.localToUTC(localTimestamp),
|
||||
account.id,
|
||||
BalanceRecordType.CASH,
|
||||
reportedBalance,
|
||||
account.getCurrency(),
|
||||
attachmentSelectionArea.getSelectedPaths()
|
||||
|
|
|
@ -2,10 +2,7 @@ package com.andrewlalis.perfin.control;
|
|||
|
||||
import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
|
||||
import com.andrewlalis.perfin.data.util.CurrencyUtil;
|
||||
import com.andrewlalis.perfin.model.Account;
|
||||
import com.andrewlalis.perfin.model.AccountType;
|
||||
import com.andrewlalis.perfin.model.MoneyValue;
|
||||
import com.andrewlalis.perfin.model.Profile;
|
||||
import com.andrewlalis.perfin.model.*;
|
||||
import com.andrewlalis.perfin.view.component.PropertiesPane;
|
||||
import com.andrewlalis.perfin.view.component.validation.ValidationApplier;
|
||||
import com.andrewlalis.perfin.view.component.validation.validators.CurrencyAmountValidator;
|
||||
|
@ -132,7 +129,7 @@ public class EditAccountController implements RouteSelectionListener {
|
|||
boolean success = Popups.confirm(accountNameField, prompt);
|
||||
if (success) {
|
||||
long id = accountRepo.insert(type, number, name, currency, description);
|
||||
balanceRepo.insert(LocalDateTime.now(ZoneOffset.UTC), id, initialBalance, currency, attachments);
|
||||
balanceRepo.insert(LocalDateTime.now(ZoneOffset.UTC), id, BalanceRecordType.CASH, initialBalance, currency, attachments);
|
||||
// Once we create the new account, go to the account.
|
||||
Account newAccount = accountRepo.findById(id).orElseThrow();
|
||||
router.replace("account", newAccount);
|
||||
|
|
|
@ -2,6 +2,7 @@ package com.andrewlalis.perfin.data;
|
|||
|
||||
import com.andrewlalis.perfin.model.Attachment;
|
||||
import com.andrewlalis.perfin.model.BalanceRecord;
|
||||
import com.andrewlalis.perfin.model.BalanceRecordType;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.file.Path;
|
||||
|
@ -11,11 +12,11 @@ import java.util.List;
|
|||
import java.util.Optional;
|
||||
|
||||
public interface BalanceRecordRepository extends Repository, AutoCloseable {
|
||||
long insert(LocalDateTime utcTimestamp, long accountId, BigDecimal balance, Currency currency, List<Path> attachments);
|
||||
BalanceRecord findLatestByAccountId(long accountId);
|
||||
long insert(LocalDateTime utcTimestamp, long accountId, BalanceRecordType type, BigDecimal balance, Currency currency, List<Path> attachments);
|
||||
BalanceRecord findLatestByAccountId(long accountId, BalanceRecordType type);
|
||||
Optional<BalanceRecord> findById(long id);
|
||||
Optional<BalanceRecord> findClosestBefore(long accountId, LocalDateTime utcTimestamp);
|
||||
Optional<BalanceRecord> findClosestAfter(long accountId, LocalDateTime utcTimestamp);
|
||||
Optional<BalanceRecord> findClosestBefore(long accountId, BalanceRecordType type, LocalDateTime utcTimestamp);
|
||||
Optional<BalanceRecord> findClosestAfter(long accountId, BalanceRecordType type, LocalDateTime utcTimestamp);
|
||||
List<Attachment> findAttachments(long recordId);
|
||||
void deleteById(long id);
|
||||
}
|
||||
|
|
|
@ -122,7 +122,7 @@ public record JdbcAccountRepository(Connection conn, Path contentDir) implements
|
|||
BalanceRecordRepository balanceRecordRepo = new JdbcBalanceRecordRepository(conn, contentDir);
|
||||
AccountEntryRepository accountEntryRepo = new JdbcAccountEntryRepository(conn);
|
||||
// Find the most recent balance record before timestamp.
|
||||
Optional<BalanceRecord> closestPastRecord = balanceRecordRepo.findClosestBefore(account.id, utcTimestamp);
|
||||
Optional<BalanceRecord> closestPastRecord = balanceRecordRepo.findClosestBefore(account.id, BalanceRecordType.CASH, utcTimestamp);
|
||||
if (closestPastRecord.isPresent()) {
|
||||
// Then find any entries on the account since that balance record and the timestamp.
|
||||
List<AccountEntry> entriesBetweenRecentRecordAndNow = accountEntryRepo.findAllByAccountIdBetween(
|
||||
|
@ -133,7 +133,7 @@ public record JdbcAccountRepository(Connection conn, Path contentDir) implements
|
|||
return computeBalanceWithEntries(account.getType(), closestPastRecord.get(), entriesBetweenRecentRecordAndNow);
|
||||
} else {
|
||||
// There is no balance record present before the given timestamp. Try and find the closest one after.
|
||||
Optional<BalanceRecord> closestFutureRecord = balanceRecordRepo.findClosestAfter(account.id, utcTimestamp);
|
||||
Optional<BalanceRecord> closestFutureRecord = balanceRecordRepo.findClosestAfter(account.id, BalanceRecordType.CASH, utcTimestamp);
|
||||
if (closestFutureRecord.isPresent()) {
|
||||
// Now find any entries on the account from the timestamp until that balance record.
|
||||
List<AccountEntry> entriesBetweenNowAndFutureRecord = accountEntryRepo.findAllByAccountIdBetween(
|
||||
|
@ -145,7 +145,7 @@ public record JdbcAccountRepository(Connection conn, Path contentDir) implements
|
|||
} else {
|
||||
// No balance records exist for the account! Assume balance of 0 when the account was created.
|
||||
log.warn("No balance record exists for account {}! Assuming balance was 0 at account creation.", account.getShortName());
|
||||
BalanceRecord placeholder = new BalanceRecord(-1, account.getCreatedAt(), account.id, BigDecimal.ZERO, account.getCurrency());
|
||||
BalanceRecord placeholder = new BalanceRecord(-1, account.getCreatedAt(), account.id, BalanceRecordType.CASH, BigDecimal.ZERO, account.getCurrency());
|
||||
List<AccountEntry> entriesSinceAccountCreated = accountEntryRepo.findAllByAccountIdBetween(account.id, account.getCreatedAt(), utcTimestamp);
|
||||
return computeBalanceWithEntries(account.getType(), placeholder, entriesSinceAccountCreated);
|
||||
}
|
||||
|
@ -177,7 +177,7 @@ public record JdbcAccountRepository(Connection conn, Path contentDir) implements
|
|||
LEFT JOIN history_account ha ON history_item.history_id = ha.history_id
|
||||
UNION ALL
|
||||
SELECT id, timestamp, 'BALANCE_RECORD' AS type, account_id
|
||||
FROM balance_record
|
||||
FROM balance_record WHERE type = 'CASH'
|
||||
)
|
||||
WHERE account_id = ? AND timestamp < ?
|
||||
ORDER BY timestamp DESC
|
||||
|
|
|
@ -5,6 +5,7 @@ import com.andrewlalis.perfin.data.BalanceRecordRepository;
|
|||
import com.andrewlalis.perfin.data.util.DbUtil;
|
||||
import com.andrewlalis.perfin.model.Attachment;
|
||||
import com.andrewlalis.perfin.model.BalanceRecord;
|
||||
import com.andrewlalis.perfin.model.BalanceRecordType;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.file.Path;
|
||||
|
@ -18,12 +19,12 @@ import java.util.Optional;
|
|||
|
||||
public record JdbcBalanceRecordRepository(Connection conn, Path contentDir) implements BalanceRecordRepository {
|
||||
@Override
|
||||
public long insert(LocalDateTime utcTimestamp, long accountId, BigDecimal balance, Currency currency, List<Path> attachments) {
|
||||
public long insert(LocalDateTime utcTimestamp, long accountId, BalanceRecordType type, BigDecimal balance, Currency currency, List<Path> attachments) {
|
||||
return DbUtil.doTransaction(conn, () -> {
|
||||
long recordId = DbUtil.insertOne(
|
||||
conn,
|
||||
"INSERT INTO balance_record (timestamp, account_id, balance, currency) VALUES (?, ?, ?, ?)",
|
||||
List.of(DbUtil.timestampFromUtcLDT(utcTimestamp), accountId, balance, currency.getCurrencyCode())
|
||||
"INSERT INTO balance_record (timestamp, account_id, type, balance, currency) VALUES (?, ?, ?, ?, ?)",
|
||||
List.of(DbUtil.timestampFromUtcLDT(utcTimestamp), accountId, type.name(), balance, currency.getCurrencyCode())
|
||||
);
|
||||
// Insert attachments.
|
||||
AttachmentRepository attachmentRepo = new JdbcAttachmentRepository(conn, contentDir);
|
||||
|
@ -39,11 +40,11 @@ public record JdbcBalanceRecordRepository(Connection conn, Path contentDir) impl
|
|||
}
|
||||
|
||||
@Override
|
||||
public BalanceRecord findLatestByAccountId(long accountId) {
|
||||
public BalanceRecord findLatestByAccountId(long accountId, BalanceRecordType type) {
|
||||
return DbUtil.findOne(
|
||||
conn,
|
||||
"SELECT * FROM balance_record WHERE account_id = ? ORDER BY timestamp DESC LIMIT 1",
|
||||
List.of(accountId),
|
||||
"SELECT * FROM balance_record WHERE account_id = ? AND type = ? ORDER BY timestamp DESC LIMIT 1",
|
||||
List.of(accountId, type.name()),
|
||||
JdbcBalanceRecordRepository::parse
|
||||
).orElse(null);
|
||||
}
|
||||
|
@ -59,21 +60,21 @@ public record JdbcBalanceRecordRepository(Connection conn, Path contentDir) impl
|
|||
}
|
||||
|
||||
@Override
|
||||
public Optional<BalanceRecord> findClosestBefore(long accountId, LocalDateTime utcTimestamp) {
|
||||
public Optional<BalanceRecord> findClosestBefore(long accountId, BalanceRecordType type, LocalDateTime utcTimestamp) {
|
||||
return DbUtil.findOne(
|
||||
conn,
|
||||
"SELECT * FROM balance_record WHERE account_id = ? AND timestamp <= ? ORDER BY timestamp DESC LIMIT 1",
|
||||
List.of(accountId, DbUtil.timestampFromUtcLDT(utcTimestamp)),
|
||||
"SELECT * FROM balance_record WHERE account_id = ? AND type = ? AND timestamp <= ? ORDER BY timestamp DESC LIMIT 1",
|
||||
List.of(accountId, type.name(), DbUtil.timestampFromUtcLDT(utcTimestamp)),
|
||||
JdbcBalanceRecordRepository::parse
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<BalanceRecord> findClosestAfter(long accountId, LocalDateTime utcTimestamp) {
|
||||
public Optional<BalanceRecord> findClosestAfter(long accountId, BalanceRecordType type, LocalDateTime utcTimestamp) {
|
||||
return DbUtil.findOne(
|
||||
conn,
|
||||
"SELECT * FROM balance_record WHERE account_id = ? AND timestamp >= ? ORDER BY timestamp ASC LIMIT 1",
|
||||
List.of(accountId, DbUtil.timestampFromUtcLDT(utcTimestamp)),
|
||||
"SELECT * FROM balance_record WHERE account_id = ? AND type = ? AND timestamp >= ? ORDER BY timestamp ASC LIMIT 1",
|
||||
List.of(accountId, type.name(), DbUtil.timestampFromUtcLDT(utcTimestamp)),
|
||||
JdbcBalanceRecordRepository::parse
|
||||
);
|
||||
}
|
||||
|
@ -108,6 +109,7 @@ public record JdbcBalanceRecordRepository(Connection conn, Path contentDir) impl
|
|||
rs.getLong("id"),
|
||||
DbUtil.utcLDTFromTimestamp(rs.getTimestamp("timestamp")),
|
||||
rs.getLong("account_id"),
|
||||
BalanceRecordType.valueOf(rs.getString("type").toUpperCase()),
|
||||
rs.getBigDecimal("balance"),
|
||||
Currency.getInstance(rs.getString("currency"))
|
||||
);
|
||||
|
|
|
@ -34,8 +34,11 @@ public class JdbcDataSourceFactory implements DataSourceFactory {
|
|||
* loaded with an old schema version, then we'll migrate to the latest. If
|
||||
* the profile has a newer schema version, we'll exit and prompt the user
|
||||
* to update their app.
|
||||
* <p>
|
||||
* This value should be one higher than the
|
||||
* </p>
|
||||
*/
|
||||
public static final int SCHEMA_VERSION = 4;
|
||||
public static final int SCHEMA_VERSION = 5;
|
||||
|
||||
public DataSource getDataSource(String profileName) throws ProfileLoadException {
|
||||
final boolean dbExists = Files.exists(getDatabaseFile(profileName));
|
||||
|
|
|
@ -19,6 +19,7 @@ public class Migrations {
|
|||
migrations.put(1, new PlainSQLMigration("/sql/migration/M001_AddTransactionProperties.sql"));
|
||||
migrations.put(2, new PlainSQLMigration("/sql/migration/M002_RefactorHistories.sql"));
|
||||
migrations.put(3, new PlainSQLMigration("/sql/migration/M003_AddLineItemCategoryAndAccountDescription.sql"));
|
||||
migrations.put(4, new PlainSQLMigration("/sql/migration/M004_AddBrokerageValueRecords.sql"));
|
||||
return migrations;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,20 +5,20 @@ import java.time.LocalDateTime;
|
|||
import java.util.Currency;
|
||||
|
||||
/**
|
||||
* A recording of an account's real reported balance at a given point in time,
|
||||
* used as a sanity check for ensuring that an account's entries add up to the
|
||||
* correct balance.
|
||||
* A recording of an account's real reported balance at a given point in time.
|
||||
*/
|
||||
public class BalanceRecord extends IdEntity implements Timestamped {
|
||||
private final LocalDateTime timestamp;
|
||||
private final long accountId;
|
||||
private final BalanceRecordType type;
|
||||
private final BigDecimal balance;
|
||||
private final Currency currency;
|
||||
|
||||
public BalanceRecord(long id, LocalDateTime timestamp, long accountId, BigDecimal balance, Currency currency) {
|
||||
public BalanceRecord(long id, LocalDateTime timestamp, long accountId, BalanceRecordType type, BigDecimal balance, Currency currency) {
|
||||
super(id);
|
||||
this.timestamp = timestamp;
|
||||
this.accountId = accountId;
|
||||
this.type = type;
|
||||
this.balance = balance;
|
||||
this.currency = currency;
|
||||
}
|
||||
|
@ -31,6 +31,10 @@ public class BalanceRecord extends IdEntity implements Timestamped {
|
|||
return accountId;
|
||||
}
|
||||
|
||||
public BalanceRecordType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public BigDecimal getBalance() {
|
||||
return balance;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
package com.andrewlalis.perfin.model;
|
||||
|
||||
public enum BalanceRecordType {
|
||||
CASH("Cash"),
|
||||
ASSETS("Assets");
|
||||
|
||||
private final String name;
|
||||
|
||||
BalanceRecordType(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
This migration adds a new entity specifically for brokerage accounts: the asset
|
||||
value record. This records the approximate value of the brokerage account assets
|
||||
excluding cash (which is already recorded).
|
||||
|
||||
This allows users to include their brokerage/investment assets in their Perfin
|
||||
profile for analysis, and paves the way for adding integrations with brokerage
|
||||
APIs to automate asset value record fetching.
|
||||
|
||||
Note that at the moment, asset value records only make sense for brokerage
|
||||
accounts, but in the future more account types might be added for which this
|
||||
would make sense.
|
||||
*/
|
||||
|
||||
ALTER TABLE balance_record
|
||||
ADD COLUMN type ENUM('CASH', 'ASSETS') NOT NULL DEFAULT 'CASH' AFTER account_id;
|
|
@ -128,6 +128,7 @@ CREATE TABLE balance_record (
|
|||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
timestamp TIMESTAMP NOT NULL,
|
||||
account_id BIGINT NOT NULL,
|
||||
type ENUM('CASH', 'ASSETS') NOT NULL DEFAULT 'CASH',
|
||||
balance NUMERIC(12, 4) NOT NULL,
|
||||
currency VARCHAR(3) NOT NULL,
|
||||
CONSTRAINT fk_balance_record_account
|
||||
|
|
Loading…
Reference in New Issue