Cleaned up entities, balance computation logic.
This commit is contained in:
parent
087242396d
commit
4899d5e8b5
|
@ -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<AccountHistoryItem> historyItems = historyRepo.findMostRecentForAccount(
|
||||
account.getId(),
|
||||
account.id,
|
||||
loadHistoryFrom,
|
||||
historyLoadSize
|
||||
);
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Attachment> attachments = repo.findAttachments(transaction.getId());
|
||||
List<Attachment> 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");
|
||||
});
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
|
|
|
@ -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<Currency> findAllUsedCurrencies();
|
||||
}
|
||||
|
|
|
@ -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<AccountRepository> repoConsumer) {
|
||||
DbUtil.useClosable(this::getAccountRepository, repoConsumer);
|
||||
}
|
||||
|
||||
BalanceRecordRepository getBalanceRecordRepository();
|
||||
default void useBalanceRecordRepository(ThrowableConsumer<BalanceRecordRepository> repoConsumer) {
|
||||
DbUtil.useClosable(this::getBalanceRecordRepository, repoConsumer);
|
||||
}
|
||||
|
||||
TransactionRepository getTransactionRepository();
|
||||
default void useTransactionRepository(ThrowableConsumer<TransactionRepository> repoConsumer) {
|
||||
DbUtil.useClosable(this::getTransactionRepository, repoConsumer);
|
||||
}
|
||||
|
||||
AttachmentRepository getAttachmentRepository();
|
||||
default void useAttachmentRepository(ThrowableConsumer<AttachmentRepository> repoConsumer) {
|
||||
DbUtil.useClosable(this::getAttachmentRepository, repoConsumer);
|
||||
}
|
||||
|
||||
AccountHistoryItemRepository getAccountHistoryItemRepository();
|
||||
|
||||
// Utility methods:
|
||||
|
||||
default void getAccountBalanceText(Account account, Consumer<String> 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<Currency, BigDecimal> 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) {
|
||||
|
|
|
@ -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.");
|
||||
}
|
||||
}
|
|
@ -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<BalanceRecord> 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<AccountEntry> accountEntries = DbUtil.findAll(
|
||||
List<AccountEntry> 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<BalanceRecord> 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<AccountEntry> accountEntries = DbUtil.findAll(
|
||||
List<AccountEntry> 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<AccountEntry> 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<AccountEntry> entriesBeforeRecord) {
|
||||
BigDecimal balance = balanceRecord.getBalance();
|
||||
for (AccountEntry entry : entriesBeforeRecord) {
|
||||
balance = balance.subtract(entry.getEffectiveValue(account.getType()));
|
||||
}
|
||||
return balance;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -30,13 +30,12 @@ import java.util.Currency;
|
|||
* all those extra accounts would be a burden to casual users.
|
||||
* </p>
|
||||
*/
|
||||
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;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -107,7 +107,7 @@ public class TransactionTile extends BorderPane {
|
|||
CompletableFuture<CreditAndDebitAccounts> 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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
|
@ -1,3 +0,0 @@
|
|||
module com.andrewlalis.perfin_test {
|
||||
requires org.junit.jupiter.api;
|
||||
}
|
Loading…
Reference in New Issue