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