Refactored account history.
This commit is contained in:
parent
3493003588
commit
8f36380e21
|
@ -1,12 +1,12 @@
|
|||
package com.andrewlalis.perfin.control;
|
||||
|
||||
import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
|
||||
import com.andrewlalis.perfin.data.AccountHistoryItemRepository;
|
||||
import com.andrewlalis.perfin.data.AccountRepository;
|
||||
import com.andrewlalis.perfin.data.HistoryRepository;
|
||||
import com.andrewlalis.perfin.data.util.DateUtil;
|
||||
import com.andrewlalis.perfin.model.Account;
|
||||
import com.andrewlalis.perfin.model.Profile;
|
||||
import com.andrewlalis.perfin.model.history.AccountHistoryItem;
|
||||
import com.andrewlalis.perfin.model.history.HistoryItem;
|
||||
import com.andrewlalis.perfin.view.component.AccountHistoryItemTile;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.BooleanExpression;
|
||||
|
@ -131,20 +131,15 @@ public class AccountViewController implements RouteSelectionListener {
|
|||
}
|
||||
|
||||
@FXML public void loadMoreHistory() {
|
||||
Profile.getCurrent().dataSource().useRepoAsync(AccountHistoryItemRepository.class, repo -> {
|
||||
List<AccountHistoryItem> historyItems = repo.findMostRecentForAccount(
|
||||
account.id,
|
||||
loadHistoryFrom,
|
||||
historyLoadSize
|
||||
);
|
||||
if (historyItems.size() < historyLoadSize) {
|
||||
Profile.getCurrent().dataSource().useRepoAsync(HistoryRepository.class, repo -> {
|
||||
long historyId = repo.getOrCreateHistoryForAccount(account.id);
|
||||
List<HistoryItem> items = repo.getNItemsBefore(historyId, historyLoadSize, loadHistoryFrom);
|
||||
if (items.size() < historyLoadSize) {
|
||||
Platform.runLater(() -> loadMoreHistoryButton.setDisable(true));
|
||||
} else {
|
||||
loadHistoryFrom = historyItems.getLast().getTimestamp();
|
||||
loadHistoryFrom = items.getLast().getTimestamp();
|
||||
}
|
||||
List<? extends Node> nodes = historyItems.stream()
|
||||
.map(item -> AccountHistoryItemTile.forItem(item, repo, this))
|
||||
.toList();
|
||||
List<? extends Node> nodes = items.stream().map(AccountHistoryItemTile::forItem).toList();
|
||||
Platform.runLater(() -> historyItemsVBox.getChildren().addAll(nodes));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
package com.andrewlalis.perfin.data;
|
||||
|
||||
import com.andrewlalis.perfin.data.util.DateUtil;
|
||||
import com.andrewlalis.perfin.model.AccountEntry;
|
||||
import com.andrewlalis.perfin.model.BalanceRecord;
|
||||
import com.andrewlalis.perfin.model.history.AccountHistoryItem;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface AccountHistoryItemRepository extends Repository, AutoCloseable {
|
||||
void recordAccountEntry(LocalDateTime timestamp, long accountId, long entryId);
|
||||
void recordBalanceRecord(LocalDateTime timestamp, long accountId, long recordId);
|
||||
void recordText(LocalDateTime timestamp, long accountId, String text);
|
||||
List<AccountHistoryItem> findMostRecentForAccount(long accountId, LocalDateTime utcTimestamp, int count);
|
||||
default Optional<AccountHistoryItem> getMostRecentForAccount(long accountId) {
|
||||
var items = findMostRecentForAccount(accountId, DateUtil.nowAsUTC(), 1);
|
||||
if (items.isEmpty()) return Optional.empty();
|
||||
return Optional.of(items.getFirst());
|
||||
}
|
||||
String getTextItem(long itemId);
|
||||
AccountEntry getAccountEntryItem(long itemId);
|
||||
BalanceRecord getBalanceRecordItem(long itemId);
|
||||
}
|
|
@ -33,7 +33,7 @@ public interface DataSource {
|
|||
TransactionVendorRepository getTransactionVendorRepository();
|
||||
TransactionCategoryRepository getTransactionCategoryRepository();
|
||||
AttachmentRepository getAttachmentRepository();
|
||||
AccountHistoryItemRepository getAccountHistoryItemRepository();
|
||||
HistoryRepository getHistoryRepository();
|
||||
|
||||
// Repository helper methods:
|
||||
|
||||
|
@ -86,7 +86,7 @@ public interface DataSource {
|
|||
TransactionVendorRepository.class, this::getTransactionVendorRepository,
|
||||
TransactionCategoryRepository.class, this::getTransactionCategoryRepository,
|
||||
AttachmentRepository.class, this::getAttachmentRepository,
|
||||
AccountHistoryItemRepository.class, this::getAccountHistoryItemRepository
|
||||
HistoryRepository.class, this::getHistoryRepository
|
||||
);
|
||||
return (Supplier<R>) repoSuppliers.get(type);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
package com.andrewlalis.perfin.data;
|
||||
|
||||
import com.andrewlalis.perfin.data.pagination.Page;
|
||||
import com.andrewlalis.perfin.data.pagination.PageRequest;
|
||||
import com.andrewlalis.perfin.data.util.DateUtil;
|
||||
import com.andrewlalis.perfin.model.history.HistoryItem;
|
||||
import com.andrewlalis.perfin.model.history.HistoryTextItem;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
|
||||
public interface HistoryRepository extends Repository, AutoCloseable {
|
||||
long getOrCreateHistoryForAccount(long accountId);
|
||||
long getOrCreateHistoryForTransaction(long transactionId);
|
||||
void deleteHistoryForAccount(long accountId);
|
||||
void deleteHistoryForTransaction(long transactionId);
|
||||
|
||||
HistoryTextItem addTextItem(long historyId, LocalDateTime utcTimestamp, String description);
|
||||
default HistoryTextItem addTextItem(long historyId, String description) {
|
||||
return addTextItem(historyId, DateUtil.nowAsUTC(), description);
|
||||
}
|
||||
Page<HistoryItem> getItems(long historyId, PageRequest pagination);
|
||||
List<HistoryItem> getNItemsBefore(long historyId, int n, LocalDateTime timestamp);
|
||||
default List<HistoryItem> getNItemsBeforeNow(long historyId, int n) {
|
||||
return getNItemsBefore(historyId, n, LocalDateTime.now(ZoneOffset.UTC));
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
package com.andrewlalis.perfin.data.impl;
|
||||
|
||||
import com.andrewlalis.perfin.data.AccountEntryRepository;
|
||||
import com.andrewlalis.perfin.data.AccountHistoryItemRepository;
|
||||
import com.andrewlalis.perfin.data.HistoryRepository;
|
||||
import com.andrewlalis.perfin.data.util.DbUtil;
|
||||
import com.andrewlalis.perfin.model.AccountEntry;
|
||||
|
||||
|
@ -31,8 +31,9 @@ public record JdbcAccountEntryRepository(Connection conn) implements AccountEntr
|
|||
)
|
||||
);
|
||||
// Insert an entry into the account's history.
|
||||
AccountHistoryItemRepository historyRepo = new JdbcAccountHistoryItemRepository(conn);
|
||||
historyRepo.recordAccountEntry(timestamp, accountId, entryId);
|
||||
HistoryRepository historyRepo = new JdbcHistoryRepository(conn);
|
||||
long historyId = historyRepo.getOrCreateHistoryForAccount(accountId);
|
||||
historyRepo.addTextItem(historyId, timestamp, "Entry #" + entryId + " added as a " + type.name() + " from Transaction #" + transactionId + ".");
|
||||
return entryId;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,120 +0,0 @@
|
|||
package com.andrewlalis.perfin.data.impl;
|
||||
|
||||
import com.andrewlalis.perfin.data.AccountHistoryItemRepository;
|
||||
import com.andrewlalis.perfin.data.util.DbUtil;
|
||||
import com.andrewlalis.perfin.model.AccountEntry;
|
||||
import com.andrewlalis.perfin.model.BalanceRecord;
|
||||
import com.andrewlalis.perfin.model.history.AccountHistoryItem;
|
||||
import com.andrewlalis.perfin.model.history.AccountHistoryItemType;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public record JdbcAccountHistoryItemRepository(Connection conn) implements AccountHistoryItemRepository {
|
||||
@Override
|
||||
public void recordAccountEntry(LocalDateTime timestamp, long accountId, long entryId) {
|
||||
long itemId = insertHistoryItem(timestamp, accountId, AccountHistoryItemType.ACCOUNT_ENTRY);
|
||||
DbUtil.insertOne(
|
||||
conn,
|
||||
"INSERT INTO account_history_item_account_entry (item_id, entry_id) VALUES (?, ?)",
|
||||
List.of(itemId, entryId)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordBalanceRecord(LocalDateTime timestamp, long accountId, long recordId) {
|
||||
long itemId = insertHistoryItem(timestamp, accountId, AccountHistoryItemType.BALANCE_RECORD);
|
||||
DbUtil.insertOne(
|
||||
conn,
|
||||
"INSERT INTO account_history_item_balance_record (item_id, record_id) VALUES (?, ?)",
|
||||
List.of(itemId, recordId)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordText(LocalDateTime timestamp, long accountId, String text) {
|
||||
long itemId = insertHistoryItem(timestamp, accountId, AccountHistoryItemType.TEXT);
|
||||
DbUtil.insertOne(
|
||||
conn,
|
||||
"INSERT INTO account_history_item_text (item_id, description) VALUES (?, ?)",
|
||||
List.of(itemId, text)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AccountHistoryItem> findMostRecentForAccount(long accountId, LocalDateTime utcTimestamp, int count) {
|
||||
return DbUtil.findAll(
|
||||
conn,
|
||||
"SELECT * FROM account_history_item WHERE account_id = ? AND timestamp < ? ORDER BY timestamp DESC LIMIT " + count,
|
||||
List.of(accountId, DbUtil.timestampFromUtcLDT(utcTimestamp)),
|
||||
JdbcAccountHistoryItemRepository::parseHistoryItem
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTextItem(long itemId) {
|
||||
return DbUtil.findOne(
|
||||
conn,
|
||||
"SELECT description FROM account_history_item_text WHERE item_id = ?",
|
||||
List.of(itemId),
|
||||
rs -> rs.getString(1)
|
||||
).orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccountEntry getAccountEntryItem(long itemId) {
|
||||
return DbUtil.findOne(
|
||||
conn,
|
||||
"""
|
||||
SELECT *
|
||||
FROM account_entry
|
||||
LEFT JOIN account_history_item_account_entry h ON h.entry_id = account_entry.id
|
||||
WHERE h.item_id = ?""",
|
||||
List.of(itemId),
|
||||
JdbcAccountEntryRepository::parse
|
||||
).orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BalanceRecord getBalanceRecordItem(long itemId) {
|
||||
return DbUtil.findOne(
|
||||
conn,
|
||||
"""
|
||||
SELECT *
|
||||
FROM balance_record
|
||||
LEFT JOIN account_history_item_balance_record h ON h.record_id = balance_record.id
|
||||
WHERE h.item_id = ?""",
|
||||
List.of(itemId),
|
||||
JdbcBalanceRecordRepository::parse
|
||||
).orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
conn.close();
|
||||
}
|
||||
|
||||
public static AccountHistoryItem parseHistoryItem(ResultSet rs) throws SQLException {
|
||||
return new AccountHistoryItem(
|
||||
rs.getLong("id"),
|
||||
DbUtil.utcLDTFromTimestamp(rs.getTimestamp("timestamp")),
|
||||
rs.getLong("account_id"),
|
||||
AccountHistoryItemType.valueOf(rs.getString("type"))
|
||||
);
|
||||
}
|
||||
|
||||
private long insertHistoryItem(LocalDateTime timestamp, long accountId, AccountHistoryItemType type) {
|
||||
return DbUtil.insertOne(
|
||||
conn,
|
||||
"INSERT INTO account_history_item (timestamp, account_id, type) VALUES (?, ?, ?)",
|
||||
List.of(
|
||||
DbUtil.timestampFromUtcLDT(timestamp),
|
||||
accountId,
|
||||
type.name()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,12 +1,8 @@
|
|||
package com.andrewlalis.perfin.data.impl;
|
||||
|
||||
import com.andrewlalis.perfin.data.AccountEntryRepository;
|
||||
import com.andrewlalis.perfin.data.AccountRepository;
|
||||
import com.andrewlalis.perfin.data.BalanceRecordRepository;
|
||||
import com.andrewlalis.perfin.data.EntityNotFoundException;
|
||||
import com.andrewlalis.perfin.data.*;
|
||||
import com.andrewlalis.perfin.data.pagination.Page;
|
||||
import com.andrewlalis.perfin.data.pagination.PageRequest;
|
||||
import com.andrewlalis.perfin.data.util.DateUtil;
|
||||
import com.andrewlalis.perfin.data.util.DbUtil;
|
||||
import com.andrewlalis.perfin.model.Account;
|
||||
import com.andrewlalis.perfin.model.AccountEntry;
|
||||
|
@ -43,8 +39,9 @@ public record JdbcAccountRepository(Connection conn, Path contentDir) implements
|
|||
)
|
||||
);
|
||||
// Insert a history item indicating the creation of the account.
|
||||
var historyRepo = new JdbcAccountHistoryItemRepository(conn);
|
||||
historyRepo.recordText(DateUtil.nowAsUTC(), accountId, "Account added to your Perfin profile.");
|
||||
HistoryRepository historyRepo = new JdbcHistoryRepository(conn);
|
||||
long historyId = historyRepo.getOrCreateHistoryForAccount(accountId);
|
||||
historyRepo.addTextItem(historyId, "Account added to your Perfin profile.");
|
||||
return accountId;
|
||||
});
|
||||
}
|
||||
|
@ -59,11 +56,12 @@ public record JdbcAccountRepository(Connection conn, Path contentDir) implements
|
|||
return DbUtil.findAll(
|
||||
conn,
|
||||
"""
|
||||
SELECT DISTINCT ON (account.id) account.*, ahi.timestamp AS _
|
||||
SELECT DISTINCT ON (account.id) account.*, hi.timestamp AS _
|
||||
FROM account
|
||||
LEFT OUTER JOIN account_history_item ahi ON ahi.account_id = account.id
|
||||
LEFT OUTER JOIN history_account ha ON ha.account_id = account.id
|
||||
LEFT OUTER JOIN history_item hi ON hi.history_id = ha.history_id
|
||||
WHERE NOT account.archived
|
||||
ORDER BY ahi.timestamp DESC, account.created_at DESC""",
|
||||
ORDER BY hi.timestamp DESC, account.created_at DESC""",
|
||||
JdbcAccountRepository::parseAccount
|
||||
);
|
||||
}
|
||||
|
@ -160,7 +158,9 @@ public record JdbcAccountRepository(Connection conn, Path contentDir) implements
|
|||
public void archive(long accountId) {
|
||||
DbUtil.doTransaction(conn, () -> {
|
||||
DbUtil.updateOne(conn, "UPDATE account SET archived = TRUE WHERE id = ?", List.of(accountId));
|
||||
new JdbcAccountHistoryItemRepository(conn).recordText(DateUtil.nowAsUTC(), accountId, "Account has been archived.");
|
||||
HistoryRepository historyRepo = new JdbcHistoryRepository(conn);
|
||||
long historyId = historyRepo.getOrCreateHistoryForAccount(accountId);
|
||||
historyRepo.addTextItem(historyId, "Account has been archived.");
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -168,7 +168,9 @@ public record JdbcAccountRepository(Connection conn, Path contentDir) implements
|
|||
public void unarchive(long accountId) {
|
||||
DbUtil.doTransaction(conn, () -> {
|
||||
DbUtil.updateOne(conn, "UPDATE account SET archived = FALSE WHERE id = ?", List.of(accountId));
|
||||
new JdbcAccountHistoryItemRepository(conn).recordText(DateUtil.nowAsUTC(), accountId, "Account has been unarchived.");
|
||||
HistoryRepository historyRepo = new JdbcHistoryRepository(conn);
|
||||
long historyId = historyRepo.getOrCreateHistoryForAccount(accountId);
|
||||
historyRepo.addTextItem(historyId, "Account has been unarchived.");
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
package com.andrewlalis.perfin.data.impl;
|
||||
|
||||
import com.andrewlalis.perfin.data.AccountHistoryItemRepository;
|
||||
import com.andrewlalis.perfin.data.AttachmentRepository;
|
||||
import com.andrewlalis.perfin.data.BalanceRecordRepository;
|
||||
import com.andrewlalis.perfin.data.HistoryRepository;
|
||||
import com.andrewlalis.perfin.data.util.CurrencyUtil;
|
||||
import com.andrewlalis.perfin.data.util.DbUtil;
|
||||
import com.andrewlalis.perfin.model.Attachment;
|
||||
import com.andrewlalis.perfin.model.BalanceRecord;
|
||||
import com.andrewlalis.perfin.model.MoneyValue;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.file.Path;
|
||||
|
@ -36,8 +38,9 @@ public record JdbcBalanceRecordRepository(Connection conn, Path contentDir) impl
|
|||
}
|
||||
}
|
||||
// Add a history item entry.
|
||||
AccountHistoryItemRepository historyRepo = new JdbcAccountHistoryItemRepository(conn);
|
||||
historyRepo.recordBalanceRecord(utcTimestamp, accountId, recordId);
|
||||
HistoryRepository historyRepo = new JdbcHistoryRepository(conn);
|
||||
long historyId = historyRepo.getOrCreateHistoryForAccount(accountId);
|
||||
historyRepo.addTextItem(historyId, utcTimestamp, "Balance Record #" + recordId + " added with a value of " + CurrencyUtil.formatMoneyWithCurrencyPrefix(new MoneyValue(balance, currency)));
|
||||
return recordId;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -65,7 +65,7 @@ public class JdbcDataSource implements DataSource {
|
|||
}
|
||||
|
||||
@Override
|
||||
public AccountHistoryItemRepository getAccountHistoryItemRepository() {
|
||||
return new JdbcAccountHistoryItemRepository(getConnection());
|
||||
public HistoryRepository getHistoryRepository() {
|
||||
return new JdbcHistoryRepository(getConnection());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ public class JdbcDataSourceFactory implements DataSourceFactory {
|
|||
* the profile has a newer schema version, we'll exit and prompt the user
|
||||
* to update their app.
|
||||
*/
|
||||
public static final int SCHEMA_VERSION = 2;
|
||||
public static final int SCHEMA_VERSION = 3;
|
||||
|
||||
public DataSource getDataSource(String profileName) throws ProfileLoadException {
|
||||
final boolean dbExists = Files.exists(getDatabaseFile(profileName));
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
package com.andrewlalis.perfin.data.impl;
|
||||
|
||||
import com.andrewlalis.perfin.data.HistoryRepository;
|
||||
import com.andrewlalis.perfin.data.pagination.Page;
|
||||
import com.andrewlalis.perfin.data.pagination.PageRequest;
|
||||
import com.andrewlalis.perfin.data.util.DateUtil;
|
||||
import com.andrewlalis.perfin.data.util.DbUtil;
|
||||
import com.andrewlalis.perfin.model.history.HistoryItem;
|
||||
import com.andrewlalis.perfin.model.history.HistoryTextItem;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public record JdbcHistoryRepository(Connection conn) implements HistoryRepository {
|
||||
@Override
|
||||
public long getOrCreateHistoryForAccount(long accountId) {
|
||||
return getOrCreateHistoryForEntity(accountId, "history_account", "account_id");
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getOrCreateHistoryForTransaction(long transactionId) {
|
||||
return getOrCreateHistoryForEntity(transactionId, "history_transaction", "transaction_id");
|
||||
}
|
||||
|
||||
private long getOrCreateHistoryForEntity(long entityId, String joinTableName, String joinColumn) {
|
||||
String selectQuery = "SELECT history_id FROM " + joinTableName + " WHERE " + joinColumn + " = ?";
|
||||
var optionalHistoryId = DbUtil.findById(conn, selectQuery, entityId, rs -> rs.getLong(1));
|
||||
if (optionalHistoryId.isPresent()) return optionalHistoryId.get();
|
||||
long historyId = DbUtil.insertOne(conn, "INSERT INTO history () VALUES ()");
|
||||
String insertQuery = "INSERT INTO " + joinTableName + " (" + joinColumn + ", history_id) VALUES (?, ?)";
|
||||
DbUtil.updateOne(conn, insertQuery, entityId, historyId);
|
||||
return historyId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteHistoryForAccount(long accountId) {
|
||||
deleteHistoryForEntity(accountId, "history_account", "account_id");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteHistoryForTransaction(long transactionId) {
|
||||
deleteHistoryForEntity(transactionId, "history_transaction", "transaction_id");
|
||||
}
|
||||
|
||||
private void deleteHistoryForEntity(long entityId, String joinTableName, String joinColumn) {
|
||||
String selectQuery = "SELECT history_id FROM " + joinTableName + " WHERE " + joinColumn + " = ?";
|
||||
var optionalHistoryId = DbUtil.findById(conn, selectQuery, entityId, rs -> rs.getLong(1));
|
||||
if (optionalHistoryId.isPresent()) {
|
||||
long historyId = optionalHistoryId.get();
|
||||
DbUtil.updateOne(conn, "DELETE FROM history WHERE id = ?", historyId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public HistoryTextItem addTextItem(long historyId, LocalDateTime utcTimestamp, String description) {
|
||||
long itemId = insertHistoryItem(historyId, utcTimestamp, HistoryItem.TYPE_TEXT);
|
||||
DbUtil.updateOne(
|
||||
conn,
|
||||
"INSERT INTO history_item_text (id, description) VALUES (?, ?)",
|
||||
itemId,
|
||||
description
|
||||
);
|
||||
return new HistoryTextItem(itemId, historyId, utcTimestamp, description);
|
||||
}
|
||||
|
||||
private long insertHistoryItem(long historyId, LocalDateTime timestamp, String type) {
|
||||
return DbUtil.insertOne(
|
||||
conn,
|
||||
"INSERT INTO history_item (history_id, timestamp, type) VALUES (?, ?, ?)",
|
||||
historyId,
|
||||
DbUtil.timestampFromUtcLDT(timestamp),
|
||||
type
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<HistoryItem> getItems(long historyId, PageRequest pagination) {
|
||||
return DbUtil.findAll(
|
||||
conn,
|
||||
"SELECT * FROM history_item WHERE history_id = ?",
|
||||
pagination,
|
||||
List.of(historyId),
|
||||
JdbcHistoryRepository::parseItem
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<HistoryItem> getNItemsBefore(long historyId, int n, LocalDateTime timestamp) {
|
||||
return DbUtil.findAll(
|
||||
conn,
|
||||
"""
|
||||
SELECT *
|
||||
FROM history_item
|
||||
WHERE history_id = ? AND timestamp <= ?
|
||||
ORDER BY timestamp DESC""",
|
||||
List.of(historyId, DbUtil.timestampFromUtcLDT(timestamp)),
|
||||
JdbcHistoryRepository::parseItem
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
conn.close();
|
||||
}
|
||||
|
||||
public static HistoryItem parseItem(ResultSet rs) throws SQLException {
|
||||
long id = rs.getLong(1);
|
||||
long historyId = rs.getLong(2);
|
||||
LocalDateTime timestamp = DbUtil.utcLDTFromTimestamp(rs.getTimestamp(3));
|
||||
String type = rs.getString(4);
|
||||
if (type.equalsIgnoreCase(HistoryItem.TYPE_TEXT)) {
|
||||
String description = DbUtil.findOne(
|
||||
rs.getStatement().getConnection(),
|
||||
"SELECT description FROM history_item_text WHERE id = ?",
|
||||
List.of(id),
|
||||
r -> r.getString(1)
|
||||
).orElseThrow();
|
||||
return new HistoryTextItem(id, historyId, timestamp, description);
|
||||
}
|
||||
throw new SQLException("Unknown history item type: " + type);
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ package com.andrewlalis.perfin.data.impl;
|
|||
|
||||
import com.andrewlalis.perfin.data.AccountEntryRepository;
|
||||
import com.andrewlalis.perfin.data.AttachmentRepository;
|
||||
import com.andrewlalis.perfin.data.HistoryRepository;
|
||||
import com.andrewlalis.perfin.data.TransactionRepository;
|
||||
import com.andrewlalis.perfin.data.pagination.Page;
|
||||
import com.andrewlalis.perfin.data.pagination.PageRequest;
|
||||
|
@ -386,9 +387,9 @@ public record JdbcTransactionRepository(Connection conn, Path contentDir) implem
|
|||
|
||||
// Add a text history item to any linked accounts detailing the changes.
|
||||
String updateMessageStr = "Transaction #" + tx.id + " was updated:\n" + String.join("\n", updateMessages);
|
||||
var historyRepo = new JdbcAccountHistoryItemRepository(conn);
|
||||
linkedAccounts.ifCredit(acc -> historyRepo.recordText(DateUtil.nowAsUTC(), acc.id, updateMessageStr));
|
||||
linkedAccounts.ifDebit(acc -> historyRepo.recordText(DateUtil.nowAsUTC(), acc.id, updateMessageStr));
|
||||
HistoryRepository historyRepo = new JdbcHistoryRepository(conn);
|
||||
long historyId = historyRepo.getOrCreateHistoryForTransaction(id);
|
||||
historyRepo.addTextItem(historyId, updateMessageStr);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ public class Migrations {
|
|||
public static Map<Integer, Migration> getMigrations() {
|
||||
final Map<Integer, Migration> migrations = new HashMap<>();
|
||||
migrations.put(1, new PlainSQLMigration("/sql/migration/M001_AddTransactionProperties.sql"));
|
||||
migrations.put(2, new PlainSQLMigration("/sql/migration/M002_RefactorHistories.sql"));
|
||||
return migrations;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
package com.andrewlalis.perfin.model.history;
|
||||
|
||||
import com.andrewlalis.perfin.model.IdEntity;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* The base class representing account history items, a read-only record of an
|
||||
* account's data and changes over time. The type of history item determines
|
||||
* what exactly it means, and could be something like an account entry, balance
|
||||
* record, or modifications to the account's properties.
|
||||
*/
|
||||
public class AccountHistoryItem extends IdEntity {
|
||||
private final LocalDateTime timestamp;
|
||||
private final long accountId;
|
||||
private final AccountHistoryItemType type;
|
||||
|
||||
public AccountHistoryItem(long id, LocalDateTime timestamp, long accountId, AccountHistoryItemType type) {
|
||||
super(id);
|
||||
this.timestamp = timestamp;
|
||||
this.accountId = accountId;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public LocalDateTime getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public long getAccountId() {
|
||||
return accountId;
|
||||
}
|
||||
|
||||
public AccountHistoryItemType getType() {
|
||||
return type;
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package com.andrewlalis.perfin.model.history;
|
||||
|
||||
public enum AccountHistoryItemType {
|
||||
TEXT,
|
||||
ACCOUNT_ENTRY,
|
||||
BALANCE_RECORD
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package com.andrewlalis.perfin.model.history;
|
||||
|
||||
import com.andrewlalis.perfin.model.IdEntity;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Represents a single polymorphic history item. The item's "type" attribute
|
||||
* tells where to find additional type-specific data.
|
||||
*/
|
||||
public abstract class HistoryItem extends IdEntity {
|
||||
public static final String TYPE_TEXT = "TEXT";
|
||||
|
||||
private final long historyId;
|
||||
private final LocalDateTime timestamp;
|
||||
private final String type;
|
||||
|
||||
public HistoryItem(long id, long historyId, LocalDateTime timestamp, String type) {
|
||||
super(id);
|
||||
this.historyId = historyId;
|
||||
this.timestamp = timestamp;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public long getHistoryId() {
|
||||
return historyId;
|
||||
}
|
||||
|
||||
public LocalDateTime getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package com.andrewlalis.perfin.model.history;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class HistoryTextItem extends HistoryItem {
|
||||
private final String description;
|
||||
|
||||
public HistoryTextItem(long id, long historyId, LocalDateTime timestamp, String description) {
|
||||
super(id, historyId, timestamp, HistoryItem.TYPE_TEXT);
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
package com.andrewlalis.perfin.view.component;
|
||||
|
||||
import com.andrewlalis.perfin.control.TransactionsViewController;
|
||||
import com.andrewlalis.perfin.data.AccountHistoryItemRepository;
|
||||
import com.andrewlalis.perfin.data.util.CurrencyUtil;
|
||||
import com.andrewlalis.perfin.model.AccountEntry;
|
||||
import com.andrewlalis.perfin.model.history.AccountHistoryItem;
|
||||
import javafx.scene.control.Hyperlink;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
|
||||
import static com.andrewlalis.perfin.PerfinApp.router;
|
||||
|
||||
public class AccountHistoryAccountEntryTile extends AccountHistoryItemTile {
|
||||
public AccountHistoryAccountEntryTile(AccountHistoryItem item, AccountHistoryItemRepository repo) {
|
||||
super(item);
|
||||
AccountEntry entry = repo.getAccountEntryItem(item.id);
|
||||
if (entry == null) {
|
||||
setCenter(new TextFlow(new Text("Deleted account entry because of deleted transaction.")));
|
||||
return;
|
||||
}
|
||||
|
||||
Text amountText = new Text(CurrencyUtil.formatMoneyWithCurrencyPrefix(entry.getMoneyValue()));
|
||||
Hyperlink transactionLink = new Hyperlink("Transaction #" + entry.getTransactionId());
|
||||
transactionLink.setOnAction(event -> router.navigate(
|
||||
"transactions",
|
||||
new TransactionsViewController.RouteContext(entry.getTransactionId())
|
||||
));
|
||||
var text = new TextFlow(
|
||||
transactionLink,
|
||||
new Text("posted as a " + entry.getType().name().toLowerCase() + " to this account, with a value of "),
|
||||
amountText
|
||||
);
|
||||
setCenter(text);
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
package com.andrewlalis.perfin.view.component;
|
||||
|
||||
import com.andrewlalis.perfin.control.AccountViewController;
|
||||
import com.andrewlalis.perfin.data.AccountHistoryItemRepository;
|
||||
import com.andrewlalis.perfin.data.util.CurrencyUtil;
|
||||
import com.andrewlalis.perfin.model.BalanceRecord;
|
||||
import com.andrewlalis.perfin.model.history.AccountHistoryItem;
|
||||
import javafx.scene.control.Hyperlink;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
|
||||
import static com.andrewlalis.perfin.PerfinApp.router;
|
||||
|
||||
public class AccountHistoryBalanceRecordTile extends AccountHistoryItemTile {
|
||||
public AccountHistoryBalanceRecordTile(AccountHistoryItem item, AccountHistoryItemRepository repo, AccountViewController controller) {
|
||||
super(item);
|
||||
BalanceRecord balanceRecord = repo.getBalanceRecordItem(item.id);
|
||||
if (balanceRecord == null) {
|
||||
setCenter(new TextFlow(new Text("Deleted balance record was added.")));
|
||||
return;
|
||||
}
|
||||
|
||||
Text amountText = new Text(CurrencyUtil.formatMoneyWithCurrencyPrefix(balanceRecord.getMoneyAmount()));
|
||||
var text = new TextFlow(new Text("Balance record #" + balanceRecord.id + " added with value of "), amountText);
|
||||
setCenter(text);
|
||||
|
||||
Hyperlink viewLink = new Hyperlink("View this balance record");
|
||||
viewLink.setOnAction(event -> router.navigate("balance-record", balanceRecord));
|
||||
setBottom(viewLink);
|
||||
}
|
||||
}
|
|
@ -1,9 +1,8 @@
|
|||
package com.andrewlalis.perfin.view.component;
|
||||
|
||||
import com.andrewlalis.perfin.control.AccountViewController;
|
||||
import com.andrewlalis.perfin.data.AccountHistoryItemRepository;
|
||||
import com.andrewlalis.perfin.data.util.DateUtil;
|
||||
import com.andrewlalis.perfin.model.history.AccountHistoryItem;
|
||||
import com.andrewlalis.perfin.model.history.HistoryItem;
|
||||
import com.andrewlalis.perfin.model.history.HistoryTextItem;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
|
||||
|
@ -11,7 +10,7 @@ import javafx.scene.layout.BorderPane;
|
|||
* A tile that shows a brief bit of information about an account history item.
|
||||
*/
|
||||
public abstract class AccountHistoryItemTile extends BorderPane {
|
||||
public AccountHistoryItemTile(AccountHistoryItem item) {
|
||||
public AccountHistoryItemTile(HistoryItem item) {
|
||||
getStyleClass().add("tile");
|
||||
|
||||
Label timestampLabel = new Label(DateUtil.formatUTCAsLocalWithZone(item.getTimestamp()));
|
||||
|
@ -20,14 +19,11 @@ public abstract class AccountHistoryItemTile extends BorderPane {
|
|||
}
|
||||
|
||||
public static AccountHistoryItemTile forItem(
|
||||
AccountHistoryItem item,
|
||||
AccountHistoryItemRepository repo,
|
||||
AccountViewController controller
|
||||
HistoryItem item
|
||||
) {
|
||||
return switch (item.getType()) {
|
||||
case TEXT -> new AccountHistoryTextTile(item, repo);
|
||||
case ACCOUNT_ENTRY -> new AccountHistoryAccountEntryTile(item, repo);
|
||||
case BALANCE_RECORD -> new AccountHistoryBalanceRecordTile(item, repo, controller);
|
||||
};
|
||||
if (item instanceof HistoryTextItem t) {
|
||||
return new AccountHistoryTextTile(t);
|
||||
}
|
||||
throw new RuntimeException("Unsupported history item type: " + item.getType());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
package com.andrewlalis.perfin.view.component;
|
||||
|
||||
import com.andrewlalis.perfin.data.AccountHistoryItemRepository;
|
||||
import com.andrewlalis.perfin.model.history.AccountHistoryItem;
|
||||
import com.andrewlalis.perfin.model.history.HistoryTextItem;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
|
||||
public class AccountHistoryTextTile extends AccountHistoryItemTile {
|
||||
public AccountHistoryTextTile(AccountHistoryItem item, AccountHistoryItemRepository repo) {
|
||||
public AccountHistoryTextTile(HistoryTextItem item) {
|
||||
super(item);
|
||||
String text = repo.getTextItem(item.id);
|
||||
setCenter(new TextFlow(new Text(text)));
|
||||
setCenter(new TextFlow(new Text(item.getDescription())));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,4 +19,5 @@ module com.andrewlalis.perfin {
|
|||
opens com.andrewlalis.perfin.view to javafx.fxml;
|
||||
opens com.andrewlalis.perfin.view.component to javafx.fxml;
|
||||
opens com.andrewlalis.perfin.view.component.validation to javafx.fxml;
|
||||
exports com.andrewlalis.perfin.model.history to javafx.graphics;
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
Migration to clean up history entities so that they are easier to work with, and
|
||||
less prone to errors.
|
||||
|
||||
- Removes existing account history items.
|
||||
- Adds a generic history table and history items that are linked to a history.
|
||||
- Adds history links to accounts and transactions.
|
||||
*/
|
||||
|
||||
CREATE TABLE history (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT
|
||||
);
|
||||
|
||||
CREATE TABLE history_item (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
history_id BIGINT NOT NULL,
|
||||
timestamp TIMESTAMP NOT NULL,
|
||||
type VARCHAR(63) NOT NULL,
|
||||
CONSTRAINT fk_history_item_history
|
||||
FOREIGN KEY (history_id) REFERENCES history(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE history_item_text (
|
||||
id BIGINT NOT NULL,
|
||||
description VARCHAR(255) NOT NULL,
|
||||
CONSTRAINT fk_history_item_text_pk
|
||||
FOREIGN KEY (id) REFERENCES history_item(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE history_account (
|
||||
account_id BIGINT NOT NULL,
|
||||
history_id BIGINT NOT NULL,
|
||||
PRIMARY KEY (account_id, history_id),
|
||||
CONSTRAINT fk_history_account_account
|
||||
FOREIGN KEY (account_id) REFERENCES account(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT fk_history_account_history
|
||||
FOREIGN KEY (history_id) REFERENCES history(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE history_transaction (
|
||||
transaction_id BIGINT NOT NULL,
|
||||
history_id BIGINT NOT NULL,
|
||||
PRIMARY KEY (transaction_id, history_id),
|
||||
CONSTRAINT fk_history_transaction_transaction
|
||||
FOREIGN KEY (transaction_id) REFERENCES transaction(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT fk_history_transaction_history
|
||||
FOREIGN KEY (history_id) REFERENCES history(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE
|
||||
);
|
||||
|
||||
DROP TABLE IF EXISTS account_history_item_text;
|
||||
DROP TABLE IF EXISTS account_history_item_account_entry;
|
||||
DROP TABLE IF EXISTS account_history_item_balance_record;
|
||||
DROP TABLE IF EXISTS account_history_item;
|
||||
|
||||
|
|
@ -134,42 +134,49 @@ CREATE TABLE balance_record_attachment (
|
|||
ON UPDATE CASCADE ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE account_history_item (
|
||||
/* HISTORY */
|
||||
CREATE TABLE history (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT
|
||||
);
|
||||
|
||||
CREATE TABLE history_item (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
history_id BIGINT NOT NULL,
|
||||
timestamp TIMESTAMP NOT NULL,
|
||||
account_id BIGINT NOT NULL,
|
||||
type VARCHAR(63) NOT NULL,
|
||||
CONSTRAINT fk_account_history_item_account
|
||||
FOREIGN KEY (account_id) REFERENCES account(id)
|
||||
CONSTRAINT fk_history_item_history
|
||||
FOREIGN KEY (history_id) REFERENCES history(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE account_history_item_text (
|
||||
item_id BIGINT NOT NULL PRIMARY KEY,
|
||||
CREATE TABLE history_item_text (
|
||||
id BIGINT PRIMARY KEY,
|
||||
description VARCHAR(255) NOT NULL,
|
||||
CONSTRAINT fk_account_history_item_text_pk
|
||||
FOREIGN KEY (item_id) REFERENCES account_history_item(id)
|
||||
CONSTRAINT fk_history_item_text_pk
|
||||
FOREIGN KEY (id) REFERENCES history_item(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE account_history_item_account_entry (
|
||||
item_id BIGINT NOT NULL PRIMARY KEY,
|
||||
entry_id BIGINT NOT NULL,
|
||||
CONSTRAINT fk_account_history_item_account_entry_pk
|
||||
FOREIGN KEY (item_id) REFERENCES account_history_item(id)
|
||||
CREATE TABLE history_account (
|
||||
account_id BIGINT NOT NULL,
|
||||
history_id BIGINT NOT NULL,
|
||||
PRIMARY KEY (account_id, history_id),
|
||||
CONSTRAINT fk_history_account_account
|
||||
FOREIGN KEY (account_id) REFERENCES account(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT fk_account_history_item_account_entry
|
||||
FOREIGN KEY (entry_id) REFERENCES account_entry(id)
|
||||
CONSTRAINT fk_history_account_history
|
||||
FOREIGN KEY (history_id) REFERENCES history(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE account_history_item_balance_record (
|
||||
item_id BIGINT NOT NULL PRIMARY KEY,
|
||||
record_id BIGINT NOT NULL,
|
||||
CONSTRAINT fk_account_history_item_balance_record_pk
|
||||
FOREIGN KEY (item_id) REFERENCES account_history_item(id)
|
||||
CREATE TABLE history_transaction (
|
||||
transaction_id BIGINT NOT NULL,
|
||||
history_id BIGINT NOT NULL,
|
||||
PRIMARY KEY (transaction_id, history_id),
|
||||
CONSTRAINT fk_history_transaction_transaction
|
||||
FOREIGN KEY (transaction_id) REFERENCES transaction(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT fk_account_history_item_balance_record
|
||||
FOREIGN KEY (record_id) REFERENCES balance_record(id)
|
||||
CONSTRAINT fk_history_transaction_history
|
||||
FOREIGN KEY (history_id) REFERENCES history(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue