From 4899d5e8b593b478969a6e6b0a478ff024babb36 Mon Sep 17 00:00:00 2001 From: andrewlalis Date: Thu, 4 Jan 2024 09:54:06 -0500 Subject: [PATCH] Cleaned up entities, balance computation logic. --- .../perfin/control/AccountViewController.java | 4 +- .../CreateBalanceRecordController.java | 4 +- .../control/CreateTransactionController.java | 4 +- .../perfin/control/EditAccountController.java | 11 +++- .../control/TransactionViewController.java | 8 +-- .../control/TransactionsViewController.java | 2 +- .../perfin/data/AccountRepository.java | 6 +- .../andrewlalis/perfin/data/DataSource.java | 17 +++--- .../perfin/data/EntityNotFoundException.java | 11 ++++ .../data/impl/JdbcAccountRepository.java | 55 +++++++++++-------- .../impl/JdbcBalanceRecordRepository.java | 2 +- .../data/impl/JdbcTransactionRepository.java | 6 +- .../com/andrewlalis/perfin/model/Account.java | 9 +-- .../perfin/model/AccountEntry.java | 25 +++++---- .../andrewlalis/perfin/model/Attachment.java | 9 +-- .../perfin/model/BalanceRecord.java | 10 +--- .../andrewlalis/perfin/model/IdEntity.java | 30 ++++++++++ .../andrewlalis/perfin/model/Transaction.java | 15 +---- .../AccountHistoryBalanceRecordTile.java | 4 +- .../view/component/TransactionTile.java | 2 +- .../migration/M1_AddBalanceRecordDeleted.sql | 5 -- src/test/java/module-info.java | 3 - 22 files changed, 135 insertions(+), 107 deletions(-) create mode 100644 src/main/java/com/andrewlalis/perfin/data/EntityNotFoundException.java create mode 100644 src/main/java/com/andrewlalis/perfin/model/IdEntity.java delete mode 100644 src/main/resources/sql/migration/M1_AddBalanceRecordDeleted.sql delete mode 100644 src/test/java/module-info.java diff --git a/src/main/java/com/andrewlalis/perfin/control/AccountViewController.java b/src/main/java/com/andrewlalis/perfin/control/AccountViewController.java index 508844f..c437454 100644 --- a/src/main/java/com/andrewlalis/perfin/control/AccountViewController.java +++ b/src/main/java/com/andrewlalis/perfin/control/AccountViewController.java @@ -35,7 +35,7 @@ public class AccountViewController implements RouteSelectionListener { @Override public void onRouteSelected(Object context) { account = (Account) context; - titleLabel.setText("Account #" + account.getId()); + titleLabel.setText("Account #" + account.id); accountNameField.setText(account.getName()); accountNumberField.setText(account.getAccountNumber()); @@ -100,7 +100,7 @@ public class AccountViewController implements RouteSelectionListener { Thread.ofVirtual().start(() -> { try (var historyRepo = Profile.getCurrent().getDataSource().getAccountHistoryItemRepository()) { List historyItems = historyRepo.findMostRecentForAccount( - account.getId(), + account.id, loadHistoryFrom, historyLoadSize ); diff --git a/src/main/java/com/andrewlalis/perfin/control/CreateBalanceRecordController.java b/src/main/java/com/andrewlalis/perfin/control/CreateBalanceRecordController.java index c94a13d..56f8d17 100644 --- a/src/main/java/com/andrewlalis/perfin/control/CreateBalanceRecordController.java +++ b/src/main/java/com/andrewlalis/perfin/control/CreateBalanceRecordController.java @@ -38,7 +38,7 @@ public class CreateBalanceRecordController implements RouteSelectionListener { timestampField.setText(LocalDateTime.now().format(DateUtil.DEFAULT_DATETIME_FORMAT)); Thread.ofVirtual().start(() -> { Profile.getCurrent().getDataSource().useAccountRepository(repo -> { - BigDecimal value = repo.deriveCurrentBalance(account.getId()); + BigDecimal value = repo.deriveCurrentBalance(account.id); Platform.runLater(() -> balanceField.setText( CurrencyUtil.formatMoneyAsBasicNumber(new MoneyValue(value, account.getCurrency())) )); @@ -54,7 +54,7 @@ public class CreateBalanceRecordController implements RouteSelectionListener { BigDecimal reportedBalance = new BigDecimal(balanceField.getText()); repo.insert( DateUtil.localToUTC(localTimestamp), - account.getId(), + account.id, reportedBalance, account.getCurrency(), attachmentSelectionArea.getSelectedFiles() diff --git a/src/main/java/com/andrewlalis/perfin/control/CreateTransactionController.java b/src/main/java/com/andrewlalis/perfin/control/CreateTransactionController.java index c0a4ba6..3f78824 100644 --- a/src/main/java/com/andrewlalis/perfin/control/CreateTransactionController.java +++ b/src/main/java/com/andrewlalis/perfin/control/CreateTransactionController.java @@ -197,7 +197,7 @@ public class CreateTransactionController implements RouteSelectionListener { Account creditAccount = linkCreditAccountComboBox.getValue(); if (debitAccount == null && creditAccount == null) { linkedAccountsErrorLabel.setText("At least one credit or debit account must be linked to the transaction for it to have any effect."); - } else if (debitAccount != null && creditAccount != null && debitAccount.getId() == creditAccount.getId()) { + } else if (debitAccount != null && debitAccount.equals(creditAccount)) { linkedAccountsErrorLabel.setText("Cannot link the same account to both credit and debit."); } else { linkedAccountsErrorLabel.setText(null); @@ -223,7 +223,7 @@ public class CreateTransactionController implements RouteSelectionListener { if (debitAccount == null && creditAccount == null) { errorMessages.add("At least one account must be linked to this transaction."); } - if (debitAccount != null && creditAccount != null && debitAccount.getId() == creditAccount.getId()) { + if (debitAccount != null && debitAccount.equals(creditAccount)) { errorMessages.add("Credit and debit accounts cannot be the same."); } return errorMessages; diff --git a/src/main/java/com/andrewlalis/perfin/control/EditAccountController.java b/src/main/java/com/andrewlalis/perfin/control/EditAccountController.java index 8fd18f9..f6c572d 100644 --- a/src/main/java/com/andrewlalis/perfin/control/EditAccountController.java +++ b/src/main/java/com/andrewlalis/perfin/control/EditAccountController.java @@ -12,6 +12,8 @@ import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.TextField; import javafx.scene.layout.VBox; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.math.BigDecimal; import java.nio.file.Path; @@ -23,6 +25,8 @@ import java.util.stream.Stream; import static com.andrewlalis.perfin.PerfinApp.router; public class EditAccountController implements RouteSelectionListener { + private static final Logger log = LoggerFactory.getLogger(EditAccountController.class); + private Account account; private final BooleanProperty creatingNewAccount = new SimpleBooleanProperty(false); @@ -102,18 +106,19 @@ public class EditAccountController implements RouteSelectionListener { router.navigate("account", newAccount); } } else { - System.out.println("Updating account " + account.getName()); + log.debug("Updating account {}", account.id); account.setName(accountNameField.getText().strip()); account.setAccountNumber(accountNumberField.getText().strip()); account.setType(accountTypeChoiceBox.getValue()); account.setCurrency(accountCurrencyComboBox.getValue()); accountRepo.update(account); - Account updatedAccount = accountRepo.findById(account.getId()).orElseThrow(); + Account updatedAccount = accountRepo.findById(account.id).orElseThrow(); router.getHistory().clear(); router.navigate("account", updatedAccount); } } catch (Exception e) { - e.printStackTrace(System.err); + log.error("Failed to save (or update) account " + account.id, e); + Popups.error("Failed to save the account: " + e.getMessage()); } } diff --git a/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java b/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java index d7b738c..4b56611 100644 --- a/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java +++ b/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java @@ -43,7 +43,7 @@ public class TransactionViewController { public void setTransaction(Transaction transaction) { this.transaction = transaction; if (transaction == null) return; - titleLabel.setText("Transaction #" + transaction.getId()); + titleLabel.setText("Transaction #" + transaction.id); amountLabel.setText(CurrencyUtil.formatMoney(transaction.getMoneyAmount())); timestampLabel.setText(DateUtil.formatUTCAsLocalWithZone(transaction.getTimestamp())); descriptionLabel.setText(transaction.getDescription()); @@ -52,7 +52,7 @@ public class TransactionViewController { configureAccountLinkBindings(creditAccountLink); Thread.ofVirtual().start(() -> { Profile.getCurrent().getDataSource().useTransactionRepository(repo -> { - CreditAndDebitAccounts accounts = repo.findLinkedAccounts(transaction.getId()); + CreditAndDebitAccounts accounts = repo.findLinkedAccounts(transaction.id); Platform.runLater(() -> { accounts.ifDebit(acc -> { debitAccountLink.setText(acc.getShortName()); @@ -70,7 +70,7 @@ public class TransactionViewController { attachmentsContainer.visibleProperty().bind(new SimpleListProperty<>(attachmentsList).emptyProperty().not()); Thread.ofVirtual().start(() -> { Profile.getCurrent().getDataSource().useTransactionRepository(repo -> { - List attachments = repo.findAttachments(transaction.getId()); + List attachments = repo.findAttachments(transaction.id); Platform.runLater(() -> attachmentsList.setAll(attachments)); }); }); @@ -94,7 +94,7 @@ public class TransactionViewController { if (confirm) { Profile.getCurrent().getDataSource().useTransactionRepository(repo -> { // TODO: Delete attachments first! - repo.delete(transaction.getId()); + repo.delete(transaction.id); router.getHistory().clear(); router.navigate("transactions"); }); diff --git a/src/main/java/com/andrewlalis/perfin/control/TransactionsViewController.java b/src/main/java/com/andrewlalis/perfin/control/TransactionsViewController.java index ead3f71..d77b7ad 100644 --- a/src/main/java/com/andrewlalis/perfin/control/TransactionsViewController.java +++ b/src/main/java/com/andrewlalis/perfin/control/TransactionsViewController.java @@ -100,7 +100,7 @@ public class TransactionsViewController implements RouteSelectionListener { Thread.ofVirtual().start(() -> { Profile.getCurrent().getDataSource().useTransactionRepository(repo -> { repo.findById(ctx.selectedTransactionId).ifPresent(tx -> { - long offset = repo.countAllAfter(tx.getId()); + long offset = repo.countAllAfter(tx.id); int pageNumber = (int) (offset / DEFAULT_ITEMS_PER_PAGE) + 1; paginationControls.setPage(pageNumber).thenRun(() -> selectedTransaction.set(tx)); }); diff --git a/src/main/java/com/andrewlalis/perfin/data/AccountRepository.java b/src/main/java/com/andrewlalis/perfin/data/AccountRepository.java index 078ca34..fffa253 100644 --- a/src/main/java/com/andrewlalis/perfin/data/AccountRepository.java +++ b/src/main/java/com/andrewlalis/perfin/data/AccountRepository.java @@ -23,9 +23,9 @@ public interface AccountRepository extends AutoCloseable { void delete(Account account); void archive(Account account); - BigDecimal deriveBalance(long id, Instant timestamp); - default BigDecimal deriveCurrentBalance(long id) { - return deriveBalance(id, Instant.now(Clock.systemUTC())); + BigDecimal deriveBalance(long accountId, Instant timestamp); + default BigDecimal deriveCurrentBalance(long accountId) { + return deriveBalance(accountId, Instant.now(Clock.systemUTC())); } Set findAllUsedCurrencies(); } diff --git a/src/main/java/com/andrewlalis/perfin/data/DataSource.java b/src/main/java/com/andrewlalis/perfin/data/DataSource.java index c35366f..f2351e1 100644 --- a/src/main/java/com/andrewlalis/perfin/data/DataSource.java +++ b/src/main/java/com/andrewlalis/perfin/data/DataSource.java @@ -5,6 +5,7 @@ import com.andrewlalis.perfin.data.util.CurrencyUtil; import com.andrewlalis.perfin.data.util.DbUtil; import com.andrewlalis.perfin.data.util.ThrowableConsumer; import com.andrewlalis.perfin.model.Account; +import com.andrewlalis.perfin.model.AccountType; import com.andrewlalis.perfin.model.MoneyValue; import javafx.application.Platform; @@ -29,32 +30,32 @@ public interface DataSource { Path getContentDir(); AccountRepository getAccountRepository(); + BalanceRecordRepository getBalanceRecordRepository(); + TransactionRepository getTransactionRepository(); + AttachmentRepository getAttachmentRepository(); + AccountHistoryItemRepository getAccountHistoryItemRepository(); + default void useAccountRepository(ThrowableConsumer repoConsumer) { DbUtil.useClosable(this::getAccountRepository, repoConsumer); } - BalanceRecordRepository getBalanceRecordRepository(); default void useBalanceRecordRepository(ThrowableConsumer repoConsumer) { DbUtil.useClosable(this::getBalanceRecordRepository, repoConsumer); } - TransactionRepository getTransactionRepository(); default void useTransactionRepository(ThrowableConsumer repoConsumer) { DbUtil.useClosable(this::getTransactionRepository, repoConsumer); } - AttachmentRepository getAttachmentRepository(); default void useAttachmentRepository(ThrowableConsumer repoConsumer) { DbUtil.useClosable(this::getAttachmentRepository, repoConsumer); } - AccountHistoryItemRepository getAccountHistoryItemRepository(); - // Utility methods: default void getAccountBalanceText(Account account, Consumer balanceConsumer) { Thread.ofVirtual().start(() -> useAccountRepository(repo -> { - BigDecimal balance = repo.deriveCurrentBalance(account.getId()); + BigDecimal balance = repo.deriveCurrentBalance(account.id); MoneyValue money = new MoneyValue(balance, account.getCurrency()); Platform.runLater(() -> balanceConsumer.accept(CurrencyUtil.formatMoney(money))); })); @@ -66,7 +67,9 @@ public interface DataSource { Map totals = new HashMap<>(); for (var account : accounts) { BigDecimal currencyTotal = totals.computeIfAbsent(account.getCurrency(), c -> BigDecimal.ZERO); - totals.put(account.getCurrency(), currencyTotal.add(accountRepo.deriveCurrentBalance(account.getId()))); + BigDecimal accountBalance = accountRepo.deriveCurrentBalance(account.id); + if (account.getType() == AccountType.CREDIT_CARD) accountBalance = accountBalance.negate(); + totals.put(account.getCurrency(), currencyTotal.add(accountBalance)); } return totals; } catch (Exception e) { diff --git a/src/main/java/com/andrewlalis/perfin/data/EntityNotFoundException.java b/src/main/java/com/andrewlalis/perfin/data/EntityNotFoundException.java new file mode 100644 index 0000000..45b5571 --- /dev/null +++ b/src/main/java/com/andrewlalis/perfin/data/EntityNotFoundException.java @@ -0,0 +1,11 @@ +package com.andrewlalis.perfin.data; + +/** + * Exception that's thrown when an entity of a certain type is not found + * when it was expected that it would be. + */ +public class EntityNotFoundException extends RuntimeException { + public EntityNotFoundException(Class type, Object id) { + super("Entity of type " + type.getName() + " with id " + id + " was not found."); + } +} diff --git a/src/main/java/com/andrewlalis/perfin/data/impl/JdbcAccountRepository.java b/src/main/java/com/andrewlalis/perfin/data/impl/JdbcAccountRepository.java index 17cda4f..031895d 100644 --- a/src/main/java/com/andrewlalis/perfin/data/impl/JdbcAccountRepository.java +++ b/src/main/java/com/andrewlalis/perfin/data/impl/JdbcAccountRepository.java @@ -1,6 +1,7 @@ package com.andrewlalis.perfin.data.impl; import com.andrewlalis.perfin.data.AccountRepository; +import com.andrewlalis.perfin.data.EntityNotFoundException; import com.andrewlalis.perfin.data.pagination.Page; import com.andrewlalis.perfin.data.pagination.PageRequest; import com.andrewlalis.perfin.data.util.DateUtil; @@ -66,59 +67,53 @@ public record JdbcAccountRepository(Connection conn) implements AccountRepositor } @Override - public BigDecimal deriveBalance(long id, Instant timestamp) { + public BigDecimal deriveBalance(long accountId, Instant timestamp) { + // First find the account itself, since its properties influence the balance. + Account account = findById(accountId).orElse(null); + if (account == null) throw new EntityNotFoundException(Account.class, accountId); // Find the most recent balance record before timestamp. Optional closestPastRecord = DbUtil.findOne( conn, "SELECT * FROM balance_record WHERE account_id = ? AND timestamp <= ? ORDER BY timestamp DESC LIMIT 1", - List.of(id, DbUtil.timestampFromInstant(timestamp)), + List.of(accountId, DbUtil.timestampFromInstant(timestamp)), JdbcBalanceRecordRepository::parse ); if (closestPastRecord.isPresent()) { // Then find any entries on the account since that balance record and the timestamp. - List accountEntries = DbUtil.findAll( + List entriesAfterRecord = DbUtil.findAll( conn, "SELECT * FROM account_entry WHERE account_id = ? AND timestamp >= ? AND timestamp <= ? ORDER BY timestamp ASC", List.of( - id, + accountId, DbUtil.timestampFromUtcLDT(closestPastRecord.get().getTimestamp()), DbUtil.timestampFromInstant(timestamp) ), JdbcAccountEntryRepository::parse ); - // Apply all entries to the most recent known balance to obtain the balance at this point. - BigDecimal currentBalance = closestPastRecord.get().getBalance(); - for (var entry : accountEntries) { - currentBalance = currentBalance.add(entry.getSignedAmount()); - } - return currentBalance; + return computeBalanceWithEntriesAfter(account, closestPastRecord.get(), entriesAfterRecord); } else { // There is no balance record present before the given timestamp. Try and find the closest one after. Optional closestFutureRecord = DbUtil.findOne( conn, "SELECT * FROM balance_record WHERE account_id = ? AND timestamp >= ? ORDER BY timestamp ASC LIMIT 1", - List.of(id, DbUtil.timestampFromInstant(timestamp)), + List.of(accountId, DbUtil.timestampFromInstant(timestamp)), JdbcBalanceRecordRepository::parse ); if (closestFutureRecord.isEmpty()) { - throw new IllegalStateException("No balance record exists for account " + id); + throw new IllegalStateException("No balance record exists for account " + accountId); } // Now find any entries on the account from the timestamp until that balance record. - List accountEntries = DbUtil.findAll( + List entriesBeforeRecord = DbUtil.findAll( conn, "SELECT * FROM account_entry WHERE account_id = ? AND timestamp <= ? AND timestamp >= ? ORDER BY timestamp DESC", List.of( - id, + accountId, DbUtil.timestampFromUtcLDT(closestFutureRecord.get().getTimestamp()), DbUtil.timestampFromInstant(timestamp) ), JdbcAccountEntryRepository::parse ); - BigDecimal currentBalance = closestFutureRecord.get().getBalance(); - for (var entry : accountEntries) { - currentBalance = currentBalance.subtract(entry.getSignedAmount()); - } - return currentBalance; + return computeBalanceWithEntriesBefore(account, closestFutureRecord.get(), entriesBeforeRecord); } } @@ -141,19 +136,19 @@ public record JdbcAccountRepository(Connection conn) implements AccountRepositor account.getAccountNumber(), account.getCurrency().getCurrencyCode(), account.getType().name(), - account.getId() + account.id ) ); } @Override public void delete(Account account) { - DbUtil.updateOne(conn, "DELETE FROM account WHERE id = ?", List.of(account.getId())); + DbUtil.updateOne(conn, "DELETE FROM account WHERE id = ?", List.of(account.id)); } @Override public void archive(Account account) { - DbUtil.updateOne(conn, "UPDATE account SET archived = TRUE WHERE id = ?", List.of(account.getId())); + DbUtil.updateOne(conn, "UPDATE account SET archived = TRUE WHERE id = ?", List.of(account.id)); } public static Account parseAccount(ResultSet rs) throws SQLException { @@ -171,4 +166,20 @@ public record JdbcAccountRepository(Connection conn) implements AccountRepositor public void close() throws Exception { conn.close(); } + + private BigDecimal computeBalanceWithEntriesAfter(Account account, BalanceRecord balanceRecord, List entriesAfterRecord) { + BigDecimal balance = balanceRecord.getBalance(); + for (AccountEntry entry : entriesAfterRecord) { + balance = balance.add(entry.getEffectiveValue(account.getType())); + } + return balance; + } + + private BigDecimal computeBalanceWithEntriesBefore(Account account, BalanceRecord balanceRecord, List entriesBeforeRecord) { + BigDecimal balance = balanceRecord.getBalance(); + for (AccountEntry entry : entriesBeforeRecord) { + balance = balance.subtract(entry.getEffectiveValue(account.getType())); + } + return balance; + } } diff --git a/src/main/java/com/andrewlalis/perfin/data/impl/JdbcBalanceRecordRepository.java b/src/main/java/com/andrewlalis/perfin/data/impl/JdbcBalanceRecordRepository.java index 6ded9ba..4a69161 100644 --- a/src/main/java/com/andrewlalis/perfin/data/impl/JdbcBalanceRecordRepository.java +++ b/src/main/java/com/andrewlalis/perfin/data/impl/JdbcBalanceRecordRepository.java @@ -30,7 +30,7 @@ public record JdbcBalanceRecordRepository(Connection conn, Path contentDir) impl try (var stmt = conn.prepareStatement("INSERT INTO balance_record_attachment(balance_record_id, attachment_id) VALUES (?, ?)")) { for (var attachmentPath : attachments) { Attachment attachment = attachmentRepo.insert(attachmentPath); - DbUtil.setArgs(stmt, recordId, attachment.getId()); + DbUtil.setArgs(stmt, recordId, attachment.id); stmt.executeUpdate(); } } diff --git a/src/main/java/com/andrewlalis/perfin/data/impl/JdbcTransactionRepository.java b/src/main/java/com/andrewlalis/perfin/data/impl/JdbcTransactionRepository.java index ed3ddd1..0bbe25e 100644 --- a/src/main/java/com/andrewlalis/perfin/data/impl/JdbcTransactionRepository.java +++ b/src/main/java/com/andrewlalis/perfin/data/impl/JdbcTransactionRepository.java @@ -36,15 +36,15 @@ public record JdbcTransactionRepository(Connection conn, Path contentDir) implem ); // 2. Insert linked account entries. AccountEntryRepository accountEntryRepository = new JdbcAccountEntryRepository(conn); - linkedAccounts.ifDebit(acc -> accountEntryRepository.insert(utcTimestamp, acc.getId(), txId, amount, AccountEntry.Type.DEBIT, currency)); - linkedAccounts.ifCredit(acc -> accountEntryRepository.insert(utcTimestamp, acc.getId(), txId, amount, AccountEntry.Type.CREDIT, currency)); + linkedAccounts.ifDebit(acc -> accountEntryRepository.insert(utcTimestamp, acc.id, txId, amount, AccountEntry.Type.DEBIT, currency)); + linkedAccounts.ifCredit(acc -> accountEntryRepository.insert(utcTimestamp, acc.id, txId, amount, AccountEntry.Type.CREDIT, currency)); // 3. Add attachments. AttachmentRepository attachmentRepo = new JdbcAttachmentRepository(conn, contentDir); try (var stmt = conn.prepareStatement("INSERT INTO transaction_attachment (transaction_id, attachment_id) VALUES (?, ?)")) { for (var attachmentPath : attachments) { Attachment attachment = attachmentRepo.insert(attachmentPath); // Insert the link-table entry. - DbUtil.setArgs(stmt, txId, attachment.getId()); + DbUtil.setArgs(stmt, txId, attachment.id); stmt.executeUpdate(); } } diff --git a/src/main/java/com/andrewlalis/perfin/model/Account.java b/src/main/java/com/andrewlalis/perfin/model/Account.java index f974117..83146a4 100644 --- a/src/main/java/com/andrewlalis/perfin/model/Account.java +++ b/src/main/java/com/andrewlalis/perfin/model/Account.java @@ -7,8 +7,7 @@ import java.util.Currency; * The representation of a physical account of some sort (checking, savings, * credit-card, etc.). */ -public class Account { - private final long id; +public class Account extends IdEntity { private final LocalDateTime createdAt; private final boolean archived; @@ -18,7 +17,7 @@ public class Account { private Currency currency; public Account(long id, LocalDateTime createdAt, boolean archived, AccountType type, String accountNumber, String name, Currency currency) { - this.id = id; + super(id); this.createdAt = createdAt; this.archived = archived; this.type = type; @@ -73,10 +72,6 @@ public class Account { return createdAt; } - public long getId() { - return id; - } - public boolean isArchived() { return archived; } diff --git a/src/main/java/com/andrewlalis/perfin/model/AccountEntry.java b/src/main/java/com/andrewlalis/perfin/model/AccountEntry.java index 74d3001..80b0ac2 100644 --- a/src/main/java/com/andrewlalis/perfin/model/AccountEntry.java +++ b/src/main/java/com/andrewlalis/perfin/model/AccountEntry.java @@ -30,13 +30,12 @@ import java.util.Currency; * all those extra accounts would be a burden to casual users. *

*/ -public class AccountEntry { +public class AccountEntry extends IdEntity { public enum Type { CREDIT, DEBIT } - private final long id; private final LocalDateTime timestamp; private final long accountId; private final long transactionId; @@ -45,7 +44,7 @@ public class AccountEntry { private final Currency currency; public AccountEntry(long id, LocalDateTime timestamp, long accountId, long transactionId, BigDecimal amount, Type type, Currency currency) { - this.id = id; + super(id); this.timestamp = timestamp; this.accountId = accountId; this.transactionId = transactionId; @@ -54,10 +53,6 @@ public class AccountEntry { this.currency = currency; } - public long getId() { - return id; - } - public LocalDateTime getTimestamp() { return timestamp; } @@ -82,11 +77,19 @@ public class AccountEntry { return currency; } - public BigDecimal getSignedAmount() { - return type == Type.DEBIT ? amount : amount.negate(); - } - public MoneyValue getMoneyValue() { return new MoneyValue(amount, currency); } + + /** + * Gets the effective value of this entry for the account's type. + * @param accountType The type of the account. + * @return The effective value of this entry, either positive or negative. + */ + public BigDecimal getEffectiveValue(AccountType accountType) { + return switch (accountType) { + case CHECKING, SAVINGS -> type == Type.DEBIT ? amount : amount.negate(); + case CREDIT_CARD -> type == Type.DEBIT ? amount.negate() : amount; + }; + } } diff --git a/src/main/java/com/andrewlalis/perfin/model/Attachment.java b/src/main/java/com/andrewlalis/perfin/model/Attachment.java index 282e86e..774490f 100644 --- a/src/main/java/com/andrewlalis/perfin/model/Attachment.java +++ b/src/main/java/com/andrewlalis/perfin/model/Attachment.java @@ -11,25 +11,20 @@ import java.time.format.DateTimeFormatter; * entity, like a receipt attached to a transaction, or a bank statement to an * account balance record. */ -public class Attachment { - private final long id; +public class Attachment extends IdEntity { private final LocalDateTime timestamp; private final String identifier; private final String filename; private final String contentType; public Attachment(long id, LocalDateTime timestamp, String identifier, String filename, String contentType) { - this.id = id; + super(id); this.timestamp = timestamp; this.identifier = identifier; this.filename = filename; this.contentType = contentType; } - public long getId() { - return id; - } - public LocalDateTime getTimestamp() { return timestamp; } diff --git a/src/main/java/com/andrewlalis/perfin/model/BalanceRecord.java b/src/main/java/com/andrewlalis/perfin/model/BalanceRecord.java index ed3b6fe..38e4841 100644 --- a/src/main/java/com/andrewlalis/perfin/model/BalanceRecord.java +++ b/src/main/java/com/andrewlalis/perfin/model/BalanceRecord.java @@ -9,26 +9,20 @@ import java.util.Currency; * used as a sanity check for ensuring that an account's entries add up to the * correct balance. */ -public class BalanceRecord { - private final long id; +public class BalanceRecord extends IdEntity { private final LocalDateTime timestamp; - private final long accountId; private final BigDecimal balance; private final Currency currency; public BalanceRecord(long id, LocalDateTime timestamp, long accountId, BigDecimal balance, Currency currency) { - this.id = id; + super(id); this.timestamp = timestamp; this.accountId = accountId; this.balance = balance; this.currency = currency; } - public long getId() { - return id; - } - public LocalDateTime getTimestamp() { return timestamp; } diff --git a/src/main/java/com/andrewlalis/perfin/model/IdEntity.java b/src/main/java/com/andrewlalis/perfin/model/IdEntity.java new file mode 100644 index 0000000..a39c8a7 --- /dev/null +++ b/src/main/java/com/andrewlalis/perfin/model/IdEntity.java @@ -0,0 +1,30 @@ +package com.andrewlalis.perfin.model; + +/** + * Base class for all entities that are identified by an id. + */ +public abstract class IdEntity { + /** + * The unique identifier for this entity. It distinguishes this entity from + * all others of its type. + */ + public final long id; + + /** + * Constructs the entity with a given id. + * @param id The id to use. + */ + protected IdEntity(long id) { + this.id = id; + } + + @Override + public boolean equals(Object other) { + return other instanceof IdEntity e && e.id == this.id; + } + + @Override + public int hashCode() { + return Long.hashCode(id); + } +} diff --git a/src/main/java/com/andrewlalis/perfin/model/Transaction.java b/src/main/java/com/andrewlalis/perfin/model/Transaction.java index 78627f8..2b451d9 100644 --- a/src/main/java/com/andrewlalis/perfin/model/Transaction.java +++ b/src/main/java/com/andrewlalis/perfin/model/Transaction.java @@ -10,26 +10,20 @@ import java.util.Currency; * actual positive/negative effect is determined by the associated account * entries that apply this transaction's amount to one or more accounts. */ -public class Transaction { - private final long id; +public class Transaction extends IdEntity { private final LocalDateTime timestamp; - private final BigDecimal amount; private final Currency currency; private final String description; public Transaction(long id, LocalDateTime timestamp, BigDecimal amount, Currency currency, String description) { - this.id = id; + super(id); this.timestamp = timestamp; this.amount = amount; this.currency = currency; this.description = description; } - public long getId() { - return id; - } - public LocalDateTime getTimestamp() { return timestamp; } @@ -49,9 +43,4 @@ public class Transaction { public MoneyValue getMoneyAmount() { return new MoneyValue(amount, currency); } - - @Override - public boolean equals(Object other) { - return other instanceof Transaction tx && id == tx.id; - } } diff --git a/src/main/java/com/andrewlalis/perfin/view/component/AccountHistoryBalanceRecordTile.java b/src/main/java/com/andrewlalis/perfin/view/component/AccountHistoryBalanceRecordTile.java index 11cb5fa..8d11d31 100644 --- a/src/main/java/com/andrewlalis/perfin/view/component/AccountHistoryBalanceRecordTile.java +++ b/src/main/java/com/andrewlalis/perfin/view/component/AccountHistoryBalanceRecordTile.java @@ -22,7 +22,7 @@ public class AccountHistoryBalanceRecordTile extends AccountHistoryItemTile { } Text amountText = new Text(CurrencyUtil.formatMoneyWithCurrencyPrefix(balanceRecord.getMoneyAmount())); - var text = new TextFlow(new Text("Balance record #" + balanceRecord.getId() + " added with value of "), amountText); + var text = new TextFlow(new Text("Balance record #" + balanceRecord.id + " added with value of "), amountText); setCenter(text); Hyperlink deleteLink = new Hyperlink("Delete this balance record"); @@ -30,7 +30,7 @@ public class AccountHistoryBalanceRecordTile extends AccountHistoryItemTile { boolean confirm = Popups.confirm("Are you sure you want to delete this balance record? It will be removed permanently, and cannot be undone."); if (confirm) { Profile.getCurrent().getDataSource().useBalanceRecordRepository(balanceRecordRepo -> { - balanceRecordRepo.deleteById(balanceRecord.getId()); + balanceRecordRepo.deleteById(balanceRecord.id); Platform.runLater(controller::reloadHistory); }); } diff --git a/src/main/java/com/andrewlalis/perfin/view/component/TransactionTile.java b/src/main/java/com/andrewlalis/perfin/view/component/TransactionTile.java index 9978d0a..baf6fcb 100644 --- a/src/main/java/com/andrewlalis/perfin/view/component/TransactionTile.java +++ b/src/main/java/com/andrewlalis/perfin/view/component/TransactionTile.java @@ -107,7 +107,7 @@ public class TransactionTile extends BorderPane { CompletableFuture cf = new CompletableFuture<>(); Thread.ofVirtual().start(() -> { Profile.getCurrent().getDataSource().useTransactionRepository(repo -> { - CreditAndDebitAccounts accounts = repo.findLinkedAccounts(transaction.getId()); + CreditAndDebitAccounts accounts = repo.findLinkedAccounts(transaction.id); cf.complete(accounts); }); }); diff --git a/src/main/resources/sql/migration/M1_AddBalanceRecordDeleted.sql b/src/main/resources/sql/migration/M1_AddBalanceRecordDeleted.sql deleted file mode 100644 index 5740723..0000000 --- a/src/main/resources/sql/migration/M1_AddBalanceRecordDeleted.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE balance_record - ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE AFTER currency; - -ALTER TABLE account_entry - ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE AFTER currency; diff --git a/src/test/java/module-info.java b/src/test/java/module-info.java deleted file mode 100644 index 1121bfd..0000000 --- a/src/test/java/module-info.java +++ /dev/null @@ -1,3 +0,0 @@ -module com.andrewlalis.perfin_test { - requires org.junit.jupiter.api; -} \ No newline at end of file