Added credit limit and general purpose credit card properties to only credit card accounts.
This commit is contained in:
parent
2abbd6ca43
commit
b74119a233
|
@ -36,6 +36,7 @@ public class AccountViewController implements RouteSelectionListener {
|
|||
private final ObservableValue<Boolean> accountArchived = accountProperty.map(a -> a != null && a.isArchived());
|
||||
private final StringProperty balanceTextProperty = new SimpleStringProperty(null);
|
||||
private final StringProperty assetValueTextProperty = new SimpleStringProperty(null);
|
||||
private final StringProperty creditLimitTextProperty = new SimpleStringProperty(null);
|
||||
|
||||
@FXML public Label titleLabel;
|
||||
@FXML public Label accountNameLabel;
|
||||
|
@ -45,6 +46,8 @@ public class AccountViewController implements RouteSelectionListener {
|
|||
@FXML public Label accountBalanceLabel;
|
||||
@FXML public PropertiesPane assetValuePane;
|
||||
@FXML public Label latestAssetsValueLabel;
|
||||
@FXML public PropertiesPane creditCardPropertiesPane;
|
||||
@FXML public Label creditLimitLabel;
|
||||
@FXML public PropertiesPane descriptionPane;
|
||||
@FXML public Text accountDescriptionText;
|
||||
|
||||
|
@ -65,10 +68,15 @@ public class AccountViewController implements RouteSelectionListener {
|
|||
var hasDescription = accountProperty.map(a -> a.getDescription() != null);
|
||||
BindingUtil.bindManagedAndVisible(descriptionPane, hasDescription);
|
||||
accountBalanceLabel.textProperty().bind(balanceTextProperty);
|
||||
|
||||
var isBrokerageAccount = accountProperty.map(a -> a.getType() == AccountType.BROKERAGE);
|
||||
BindingUtil.bindManagedAndVisible(assetValuePane, isBrokerageAccount);
|
||||
latestAssetsValueLabel.textProperty().bind(assetValueTextProperty);
|
||||
|
||||
var isCreditCardAccount = accountProperty.map(a -> a.getType() == AccountType.CREDIT_CARD);
|
||||
BindingUtil.bindManagedAndVisible(creditCardPropertiesPane, isCreditCardAccount);
|
||||
creditLimitLabel.textProperty().bind(creditLimitTextProperty);
|
||||
|
||||
actionsBox.getChildren().forEach(node -> {
|
||||
Button button = (Button) node;
|
||||
ObservableValue<Boolean> buttonDisabled = accountArchived;
|
||||
|
@ -126,6 +134,22 @@ public class AccountViewController implements RouteSelectionListener {
|
|||
repo -> repo.getNearestAssetValue(account.id)
|
||||
).thenApply(value -> CurrencyUtil.formatMoney(new MoneyValue(value, account.getCurrency())))
|
||||
.thenAccept(text -> Platform.runLater(() -> assetValueTextProperty.set(text)));
|
||||
} else if (account.getType() == AccountType.CREDIT_CARD) {
|
||||
Profile.getCurrent().dataSource().mapRepoAsync(
|
||||
AccountRepository.class,
|
||||
repo -> repo.getCreditCardProperties(account.id)
|
||||
).thenAccept(props -> Platform.runLater(() -> {
|
||||
if (props == null) {
|
||||
creditLimitTextProperty.set("No credit card info.");
|
||||
return;
|
||||
}
|
||||
if (props.creditLimit() == null) {
|
||||
creditLimitTextProperty.set("No credit limit set.");
|
||||
} else {
|
||||
MoneyValue money = new MoneyValue(props.creditLimit(), account.getCurrency());
|
||||
creditLimitTextProperty.set(CurrencyUtil.formatMoney(money));
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
package com.andrewlalis.perfin.control;
|
||||
|
||||
import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
|
||||
import com.andrewlalis.perfin.data.AccountRepository;
|
||||
import com.andrewlalis.perfin.data.util.CurrencyUtil;
|
||||
import com.andrewlalis.perfin.model.*;
|
||||
import com.andrewlalis.perfin.view.BindingUtil;
|
||||
import com.andrewlalis.perfin.view.component.PropertiesPane;
|
||||
import com.andrewlalis.perfin.view.component.validation.ValidationApplier;
|
||||
import com.andrewlalis.perfin.view.component.validation.validators.CurrencyAmountValidator;
|
||||
import com.andrewlalis.perfin.view.component.validation.validators.PredicateValidator;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.BooleanExpression;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
|
@ -35,6 +38,7 @@ public class EditAccountController implements RouteSelectionListener {
|
|||
@FXML public TextField accountNumberField;
|
||||
@FXML public ComboBox<Currency> accountCurrencyComboBox;
|
||||
@FXML public ChoiceBox<AccountType> accountTypeChoiceBox;
|
||||
@FXML public TextField creditLimitField;
|
||||
@FXML public TextArea descriptionField;
|
||||
@FXML public PropertiesPane initialBalanceContent;
|
||||
@FXML public TextField initialBalanceField;
|
||||
|
@ -57,12 +61,25 @@ public class EditAccountController implements RouteSelectionListener {
|
|||
new CurrencyAmountValidator(() -> accountCurrencyComboBox.getValue(), true, false)
|
||||
).attachToTextField(initialBalanceField, accountCurrencyComboBox.valueProperty());
|
||||
|
||||
var isEditingCreditCardAccount = accountTypeChoiceBox.valueProperty().isEqualTo(AccountType.CREDIT_CARD);
|
||||
BindingUtil.bindManagedAndVisible(creditLimitField, isEditingCreditCardAccount);
|
||||
var creditLimitValid = new ValidationApplier<>(new CurrencyAmountValidator(
|
||||
() -> accountCurrencyComboBox.getValue(),
|
||||
false,
|
||||
true
|
||||
)).validatedInitially().attachToTextField(creditLimitField)
|
||||
.or(isEditingCreditCardAccount.not());
|
||||
|
||||
var descriptionValid = new ValidationApplier<>(new PredicateValidator<String>()
|
||||
.addPredicate(s -> s == null || s.strip().length() <= Account.DESCRIPTION_MAX_LENGTH, "Description is too long.")
|
||||
).attach(descriptionField, descriptionField.textProperty());
|
||||
|
||||
// Combine validity of all fields for an expression that determines if the whole form is valid.
|
||||
BooleanExpression formValid = nameValid.and(numberValid).and(balanceValid.or(creatingNewAccount.not())).and(descriptionValid);
|
||||
BooleanExpression formValid = nameValid
|
||||
.and(numberValid)
|
||||
.and(balanceValid.or(creatingNewAccount.not()))
|
||||
.and(descriptionValid)
|
||||
.and(creditLimitValid);
|
||||
saveButton.disableProperty().bind(formValid.not());
|
||||
|
||||
List<Currency> priorityCurrencies = Stream.of("USD", "EUR", "GBP", "CAD", "AUD")
|
||||
|
@ -111,6 +128,10 @@ public class EditAccountController implements RouteSelectionListener {
|
|||
description = description.strip();
|
||||
if (description.isBlank()) description = null;
|
||||
}
|
||||
BigDecimal creditLimit = null;
|
||||
if (type == AccountType.CREDIT_CARD && creditLimitField.getText() != null && !creditLimitField.getText().isBlank()) {
|
||||
creditLimit = new BigDecimal(creditLimitField.getText());
|
||||
}
|
||||
try (
|
||||
var accountRepo = Profile.getCurrent().dataSource().getAccountRepository();
|
||||
var balanceRepo = Profile.getCurrent().dataSource().getBalanceRecordRepository()
|
||||
|
@ -130,12 +151,18 @@ public class EditAccountController implements RouteSelectionListener {
|
|||
if (success) {
|
||||
long id = accountRepo.insert(type, number, name, currency, description);
|
||||
balanceRepo.insert(LocalDateTime.now(ZoneOffset.UTC), id, BalanceRecordType.CASH, initialBalance, currency, attachments);
|
||||
if (type == AccountType.CREDIT_CARD && creditLimit != null) {
|
||||
accountRepo.saveCreditCardProperties(new CreditCardProperties(id, creditLimit));
|
||||
}
|
||||
// Once we create the new account, go to the account.
|
||||
Account newAccount = accountRepo.findById(id).orElseThrow();
|
||||
router.replace("account", newAccount);
|
||||
}
|
||||
} else {
|
||||
accountRepo.update(account.id, type, number, name, currency, description);
|
||||
if (type == AccountType.CREDIT_CARD) {
|
||||
accountRepo.saveCreditCardProperties(new CreditCardProperties(account.id, creditLimit));
|
||||
}
|
||||
Account updatedAccount = accountRepo.findById(account.id).orElseThrow();
|
||||
router.replace("account", updatedAccount);
|
||||
}
|
||||
|
@ -158,12 +185,28 @@ public class EditAccountController implements RouteSelectionListener {
|
|||
accountCurrencyComboBox.getSelectionModel().select(Currency.getInstance("USD"));
|
||||
initialBalanceField.setText(String.format("%.02f", 0f));
|
||||
descriptionField.setText(null);
|
||||
|
||||
creditLimitField.setText(null);
|
||||
} else {
|
||||
accountNameField.setText(account.getName());
|
||||
accountNumberField.setText(account.getAccountNumber());
|
||||
accountTypeChoiceBox.getSelectionModel().select(account.getType());
|
||||
accountCurrencyComboBox.getSelectionModel().select(account.getCurrency());
|
||||
descriptionField.setText(account.getDescription());
|
||||
|
||||
// Fetch the account's credit limit if it's a credit card account.
|
||||
if (account.getType() == AccountType.CREDIT_CARD) {
|
||||
Profile.getCurrent().dataSource().mapRepoAsync(
|
||||
AccountRepository.class,
|
||||
repo -> repo.getCreditCardProperties(account.id)
|
||||
).thenAccept(props -> Platform.runLater(() -> {
|
||||
if (props != null && props.creditLimit() != null) {
|
||||
creditLimitField.setText(CurrencyUtil.formatMoneyAsBasicNumber(new MoneyValue(props.creditLimit(), account.getCurrency())));
|
||||
} else {
|
||||
creditLimitField.setText(null);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import com.andrewlalis.perfin.data.pagination.Page;
|
|||
import com.andrewlalis.perfin.data.pagination.PageRequest;
|
||||
import com.andrewlalis.perfin.model.Account;
|
||||
import com.andrewlalis.perfin.model.AccountType;
|
||||
import com.andrewlalis.perfin.model.CreditCardProperties;
|
||||
import com.andrewlalis.perfin.model.Timestamped;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
@ -23,6 +24,8 @@ public interface AccountRepository extends Repository, AutoCloseable {
|
|||
List<Account> findTopNRecentlyActive(int n, int daysSinceLastActive);
|
||||
List<Account> findAllByCurrency(Currency currency);
|
||||
Optional<Account> findById(long id);
|
||||
CreditCardProperties getCreditCardProperties(long id);
|
||||
void saveCreditCardProperties(CreditCardProperties properties);
|
||||
void update(long accountId, AccountType type, String accountNumber, String name, Currency currency, String description);
|
||||
void delete(Account account);
|
||||
void archive(long accountId);
|
||||
|
|
|
@ -25,7 +25,6 @@ public record JdbcAccountRepository(Connection conn, Path contentDir) implements
|
|||
@Override
|
||||
public long insert(AccountType type, String accountNumber, String name, Currency currency, String description) {
|
||||
return DbUtil.doTransaction(conn, () -> {
|
||||
|
||||
long accountId = DbUtil.insertOne(
|
||||
conn,
|
||||
"INSERT INTO account (created_at, account_type, account_number, name, currency, description) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
|
@ -36,6 +35,10 @@ public record JdbcAccountRepository(Connection conn, Path contentDir) implements
|
|||
currency.getCurrencyCode(),
|
||||
description
|
||||
);
|
||||
// If it's a credit card account, preemptively create a credit card properties record.
|
||||
if (type == AccountType.CREDIT_CARD) {
|
||||
saveCreditCardProperties(new CreditCardProperties(accountId, null));
|
||||
}
|
||||
// Insert a history item indicating the creation of the account.
|
||||
HistoryRepository historyRepo = new JdbcHistoryRepository(conn);
|
||||
long historyId = historyRepo.getOrCreateHistoryForAccount(accountId);
|
||||
|
@ -113,6 +116,48 @@ public record JdbcAccountRepository(Connection conn, Path contentDir) implements
|
|||
return DbUtil.findById(conn, "SELECT * FROM account WHERE id = ?", id, JdbcAccountRepository::parseAccount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CreditCardProperties getCreditCardProperties(long id) {
|
||||
AccountType accountType = getAccountType(id);
|
||||
if (accountType != AccountType.CREDIT_CARD) return null;
|
||||
Optional<CreditCardProperties> optionalProperties = DbUtil.findOne(
|
||||
conn,
|
||||
"SELECT * FROM credit_card_account_properties WHERE account_id = ?",
|
||||
List.of(id),
|
||||
JdbcAccountRepository::parseCreditCardProperties
|
||||
);
|
||||
if (optionalProperties.isPresent()) return optionalProperties.get();
|
||||
// No properties were found for the credit card account, so create an empty properties.
|
||||
CreditCardProperties defaultProperties = new CreditCardProperties(id, null);
|
||||
saveCreditCardProperties(defaultProperties);
|
||||
return defaultProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveCreditCardProperties(CreditCardProperties properties) {
|
||||
AccountType accountType = getAccountType(properties.accountId());
|
||||
if (accountType != AccountType.CREDIT_CARD) return;
|
||||
CreditCardProperties existingProperties = DbUtil.findOne(
|
||||
conn,
|
||||
"SELECT * FROM credit_card_account_properties WHERE account_id = ?",
|
||||
List.of(properties.accountId()),
|
||||
JdbcAccountRepository::parseCreditCardProperties
|
||||
).orElse(null);
|
||||
if (existingProperties != null) {
|
||||
DbUtil.updateOne(
|
||||
conn,
|
||||
"UPDATE credit_card_account_properties SET credit_limit = ? WHERE account_id = ?",
|
||||
properties.creditLimit(), properties.accountId()
|
||||
);
|
||||
} else {
|
||||
DbUtil.updateOne(
|
||||
conn,
|
||||
"INSERT INTO credit_card_account_properties (account_id, credit_limit) VALUES (?, ?)",
|
||||
properties.accountId(), properties.creditLimit()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal deriveCashBalance(long accountId, Instant timestamp) {
|
||||
// First find the account itself, since its properties influence the balance.
|
||||
|
@ -254,7 +299,10 @@ public record JdbcAccountRepository(Connection conn, Path contentDir) implements
|
|||
|
||||
@Override
|
||||
public void delete(Account account) {
|
||||
DbUtil.updateOne(conn, "DELETE FROM account WHERE id = ?", List.of(account.id));
|
||||
DbUtil.doTransaction(conn, () -> {
|
||||
DbUtil.update(conn, "DELETE FROM credit_card_account_properties WHERE account_id = ?", account.id);
|
||||
DbUtil.updateOne(conn, "DELETE FROM account WHERE id = ?", account.id);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -289,6 +337,12 @@ public record JdbcAccountRepository(Connection conn, Path contentDir) implements
|
|||
return new Account(id, createdAt, archived, type, accountNumber, name, currency, description);
|
||||
}
|
||||
|
||||
public static CreditCardProperties parseCreditCardProperties(ResultSet rs) throws SQLException {
|
||||
long accountId = rs.getLong("account_id");
|
||||
BigDecimal creditLimit = rs.getBigDecimal("credit_limit");
|
||||
return new CreditCardProperties(accountId, creditLimit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
conn.close();
|
||||
|
@ -310,4 +364,11 @@ public record JdbcAccountRepository(Connection conn, Path contentDir) implements
|
|||
}
|
||||
return balance;
|
||||
}
|
||||
|
||||
private AccountType getAccountType(long id) {
|
||||
String accountTypeStr = DbUtil.findOne(conn, "SELECT account_type FROM account WHERE id = ?", List.of(id), rs -> rs.getString(1))
|
||||
.orElse(null);
|
||||
if (accountTypeStr == null) return null;
|
||||
return AccountType.valueOf(accountTypeStr.toUpperCase());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ public class JdbcDataSourceFactory implements DataSourceFactory {
|
|||
* This value should be one higher than the
|
||||
* </p>
|
||||
*/
|
||||
public static final int SCHEMA_VERSION = 5;
|
||||
public static final int SCHEMA_VERSION = 6;
|
||||
|
||||
public DataSource getDataSource(String profileName) throws ProfileLoadException {
|
||||
final boolean dbExists = Files.exists(getDatabaseFile(profileName));
|
||||
|
|
|
@ -20,6 +20,7 @@ public class Migrations {
|
|||
migrations.put(2, new PlainSQLMigration("/sql/migration/M002_RefactorHistories.sql"));
|
||||
migrations.put(3, new PlainSQLMigration("/sql/migration/M003_AddLineItemCategoryAndAccountDescription.sql"));
|
||||
migrations.put(4, new PlainSQLMigration("/sql/migration/M004_AddBrokerageValueRecords.sql"));
|
||||
migrations.put(5, new PlainSQLMigration("/sql/migration/M005_AddCreditCardLimit.sql"));
|
||||
return migrations;
|
||||
}
|
||||
|
||||
|
|
|
@ -113,8 +113,13 @@ public final class DbUtil {
|
|||
}
|
||||
|
||||
public static void updateOne(Connection conn, String query, List<Object> args) {
|
||||
Object[] argsArray = args.toArray();
|
||||
updateOne(conn, query, argsArray);
|
||||
try (var stmt = conn.prepareStatement(query)) {
|
||||
setArgs(stmt, args);
|
||||
int updateCount = stmt.executeUpdate();
|
||||
if (updateCount != 1) throw new UncheckedSqlException("Update count is " + updateCount + "; expected 1.");
|
||||
} catch (SQLException e) {
|
||||
throw new UncheckedSqlException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void updateOne(Connection conn, String query, Object... args) {
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
package com.andrewlalis.perfin.model;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public record CreditCardProperties(
|
||||
long accountId,
|
||||
BigDecimal creditLimit
|
||||
) {}
|
|
@ -49,6 +49,11 @@
|
|||
</VBox>
|
||||
</PropertiesPane>
|
||||
|
||||
<PropertiesPane vgap="5" hgap="5" fx:id="creditCardPropertiesPane" styleClass="std-padding,std-spacing">
|
||||
<Label text="Credit Limit" styleClass="bold-text" labelFor="${creditLimitLabel}"/>
|
||||
<Label fx:id="creditLimitLabel" styleClass="mono-font"/>
|
||||
</PropertiesPane>
|
||||
|
||||
<PropertiesPane vgap="5" hgap="5" fx:id="descriptionPane" styleClass="std-padding,std-spacing">
|
||||
<Label text="Description" styleClass="bold-text" labelFor="${accountDescriptionText}"/>
|
||||
<TextFlow maxWidth="500"><Text fx:id="accountDescriptionText"/></TextFlow>
|
||||
|
|
|
@ -33,6 +33,9 @@
|
|||
<Label text="Account Type" styleClass="bold-text"/>
|
||||
<ChoiceBox fx:id="accountTypeChoiceBox"/>
|
||||
|
||||
<Label text="Credit Limit" styleClass="bold-text" managed="${creditLimitField.managed}" visible="${creditLimitField.visible}"/>
|
||||
<TextField fx:id="creditLimitField" styleClass="mono-font"/>
|
||||
|
||||
<Label text="Description" styleClass="bold-text"/>
|
||||
<TextArea
|
||||
fx:id="descriptionField"
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
This migration adds a new table for credit-card specific properties.
|
||||
*/
|
||||
|
||||
CREATE TABLE credit_card_account_properties (
|
||||
account_id BIGINT PRIMARY KEY,
|
||||
credit_limit NUMERIC(12, 4) NULL,
|
||||
CONSTRAINT fk_credit_card_account_properties_account
|
||||
FOREIGN KEY (account_id) REFERENCES account(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE
|
||||
);
|
|
@ -17,6 +17,14 @@ CREATE TABLE account (
|
|||
description VARCHAR(255) DEFAULT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE credit_card_account_properties (
|
||||
account_id BIGINT PRIMARY KEY,
|
||||
credit_limit NUMERIC(12, 4) NULL,
|
||||
CONSTRAINT fk_credit_card_account_properties_account
|
||||
FOREIGN KEY (account_id) REFERENCES account(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE attachment (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
uploaded_at TIMESTAMP NOT NULL,
|
||||
|
|
Loading…
Reference in New Issue