diff --git a/pom.xml b/pom.xml
index 44a2705..dc999e1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -49,6 +49,11 @@
h2
2.2.224
+
+ com.github.f4b6a3
+ ulid-creator
+ 5.2.2
+
diff --git a/src/main/java/com/andrewlalis/perfin/control/CreateTransactionController.java b/src/main/java/com/andrewlalis/perfin/control/CreateTransactionController.java
index 5e27e9a..8056ee5 100644
--- a/src/main/java/com/andrewlalis/perfin/control/CreateTransactionController.java
+++ b/src/main/java/com/andrewlalis/perfin/control/CreateTransactionController.java
@@ -2,8 +2,9 @@ package com.andrewlalis.perfin.control;
import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
import com.andrewlalis.perfin.data.DateUtil;
-import com.andrewlalis.perfin.data.FileUtil;
-import com.andrewlalis.perfin.model.*;
+import com.andrewlalis.perfin.model.Account;
+import com.andrewlalis.perfin.model.CreditAndDebitAccounts;
+import com.andrewlalis.perfin.model.Profile;
import com.andrewlalis.perfin.view.AccountComboBoxCellFactory;
import com.andrewlalis.perfin.view.BindingUtil;
import javafx.application.Platform;
@@ -17,14 +18,15 @@ import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import java.io.File;
-import java.io.IOException;
import java.math.BigDecimal;
-import java.nio.file.Files;
import java.nio.file.Path;
import java.time.DateTimeException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.Currency;
+import java.util.List;
import java.util.stream.Collectors;
import static com.andrewlalis.perfin.PerfinApp.router;
@@ -129,36 +131,21 @@ public class CreateTransactionController implements RouteSelectionListener {
);
alert.show();
} else {
- LocalDateTime timestamp = parseTimestamp();
+ LocalDateTime utcTimestamp = DateUtil.localToUTC(parseTimestamp());
BigDecimal amount = new BigDecimal(amountField.getText());
Currency currency = currencyChoiceBox.getValue();
String description = descriptionField.getText() == null ? null : descriptionField.getText().strip();
- Map affectedAccounts = getSelectedAccounts();
- List attachments = selectedAttachmentFiles.stream()
- .map(file -> {
- String filename = file.getName();
- String filetypeSuffix = filename.substring(filename.lastIndexOf('.'));
- String mimeType = FileUtil.MIMETYPES.get(filetypeSuffix);
- return new TransactionAttachment(filename, mimeType);
- }).toList();
- Transaction transaction = new Transaction(timestamp, amount, currency, description);
+ CreditAndDebitAccounts linkedAccounts = getSelectedAccounts();
+ List attachments = selectedAttachmentFiles.stream().map(File::toPath).toList();
Profile.getCurrent().getDataSource().useTransactionRepository(repo -> {
- long txId = repo.insert(transaction, affectedAccounts);
- repo.addAttachments(txId, attachments);
- // Copy the actual attachment files to their new locations.
- for (var attachment : repo.findAttachments(txId)) {
- Path filePath = attachment.getPath();
- Path dirPath = filePath.getParent();
- Path originalFilePath = selectedAttachmentFiles.stream()
- .filter(file -> file.getName().equals(attachment.getFilename()))
- .findFirst().orElseThrow().toPath();
- try {
- Files.createDirectories(dirPath);
- Files.copy(originalFilePath, filePath);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
+ repo.insert(
+ utcTimestamp,
+ amount,
+ currency,
+ description,
+ linkedAccounts,
+ attachments
+ );
});
router.navigateBackAndClear();
}
@@ -191,13 +178,11 @@ public class CreateTransactionController implements RouteSelectionListener {
});
}
- private Map getSelectedAccounts() {
- Account debitAccount = linkDebitAccountComboBox.getValue();
- Account creditAccount = linkCreditAccountComboBox.getValue();
- Map accountsMap = new HashMap<>();
- if (debitAccount != null) accountsMap.put(debitAccount.getId(), AccountEntry.Type.DEBIT);
- if (creditAccount != null) accountsMap.put(creditAccount.getId(), AccountEntry.Type.CREDIT);
- return accountsMap;
+ private CreditAndDebitAccounts getSelectedAccounts() {
+ return new CreditAndDebitAccounts(
+ linkCreditAccountComboBox.getValue(),
+ linkDebitAccountComboBox.getValue()
+ );
}
private LocalDateTime parseTimestamp() {
diff --git a/src/main/java/com/andrewlalis/perfin/control/EditAccountController.java b/src/main/java/com/andrewlalis/perfin/control/EditAccountController.java
index eff6b4b..145eb23 100644
--- a/src/main/java/com/andrewlalis/perfin/control/EditAccountController.java
+++ b/src/main/java/com/andrewlalis/perfin/control/EditAccountController.java
@@ -3,7 +3,6 @@ package com.andrewlalis.perfin.control;
import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
import com.andrewlalis.perfin.model.Account;
import com.andrewlalis.perfin.model.AccountType;
-import com.andrewlalis.perfin.model.BalanceRecord;
import com.andrewlalis.perfin.model.Profile;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
@@ -12,6 +11,9 @@ import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import java.math.BigDecimal;
+import java.nio.file.Path;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
import java.util.*;
import java.util.stream.Stream;
@@ -84,6 +86,7 @@ public class EditAccountController implements RouteSelectionListener {
AccountType type = accountTypeChoiceBox.getValue();
Currency currency = accountCurrencyComboBox.getValue();
BigDecimal initialBalance = new BigDecimal(initialBalanceField.getText().strip());
+ List attachments = Collections.emptyList();
Alert confirm = new Alert(
Alert.AlertType.CONFIRMATION,
@@ -92,14 +95,13 @@ public class EditAccountController implements RouteSelectionListener {
Optional result = confirm.showAndWait();
boolean success = result.isPresent() && result.get().equals(ButtonType.OK);
if (success) {
- Account newAccount = new Account(type, number, name, currency);
- long id = accountRepo.insert(newAccount);
- Account savedAccount = accountRepo.findById(id).orElseThrow();
- balanceRepo.insert(new BalanceRecord(id, initialBalance, savedAccount.getCurrency()));
+ long id = accountRepo.insert(type, number, name, currency);
+ balanceRepo.insert(LocalDateTime.now(ZoneOffset.UTC), id, initialBalance, currency, attachments);
// Once we create the new account, go to the account.
+ Account newAccount = accountRepo.findById(id).orElseThrow();
router.getHistory().clear();
- router.navigate("account", savedAccount);
+ router.navigate("account", newAccount);
}
} else {
System.out.println("Updating account " + account.getName());
@@ -113,7 +115,7 @@ public class EditAccountController implements RouteSelectionListener {
router.navigate("account", updatedAccount);
}
} catch (Exception e) {
- e.printStackTrace();
+ e.printStackTrace(System.err);
}
}
diff --git a/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java b/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java
index bde5bfd..fdc3f17 100644
--- a/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java
+++ b/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java
@@ -2,10 +2,10 @@ package com.andrewlalis.perfin.control;
import com.andrewlalis.perfin.data.CurrencyUtil;
import com.andrewlalis.perfin.data.DateUtil;
+import com.andrewlalis.perfin.model.Attachment;
import com.andrewlalis.perfin.model.CreditAndDebitAccounts;
import com.andrewlalis.perfin.model.Profile;
import com.andrewlalis.perfin.model.Transaction;
-import com.andrewlalis.perfin.model.TransactionAttachment;
import com.andrewlalis.perfin.view.BindingUtil;
import com.andrewlalis.perfin.view.component.AttachmentPreview;
import javafx.application.Platform;
@@ -36,7 +36,7 @@ public class TransactionViewController {
@FXML public VBox attachmentsContainer;
@FXML public HBox attachmentsHBox;
- private final ObservableList attachmentsList = FXCollections.observableArrayList();
+ private final ObservableList attachmentsList = FXCollections.observableArrayList();
public void setTransaction(Transaction transaction) {
this.transaction = transaction;
@@ -67,7 +67,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.getId());
Platform.runLater(() -> attachmentsList.setAll(attachments));
});
});
diff --git a/src/main/java/com/andrewlalis/perfin/data/AccountEntryRepository.java b/src/main/java/com/andrewlalis/perfin/data/AccountEntryRepository.java
index 18fc8d9..8472b16 100644
--- a/src/main/java/com/andrewlalis/perfin/data/AccountEntryRepository.java
+++ b/src/main/java/com/andrewlalis/perfin/data/AccountEntryRepository.java
@@ -2,8 +2,19 @@ package com.andrewlalis.perfin.data;
import com.andrewlalis.perfin.model.AccountEntry;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.Currency;
import java.util.List;
public interface AccountEntryRepository extends AutoCloseable {
+ long insert(
+ LocalDateTime timestamp,
+ long accountId,
+ long transactionId,
+ BigDecimal amount,
+ AccountEntry.Type type,
+ Currency currency
+ );
List findAllByAccountId(long accountId);
}
diff --git a/src/main/java/com/andrewlalis/perfin/data/AccountHistoryItemRepository.java b/src/main/java/com/andrewlalis/perfin/data/AccountHistoryItemRepository.java
new file mode 100644
index 0000000..0413e07
--- /dev/null
+++ b/src/main/java/com/andrewlalis/perfin/data/AccountHistoryItemRepository.java
@@ -0,0 +1,9 @@
+package com.andrewlalis.perfin.data;
+
+import java.time.LocalDateTime;
+
+public interface AccountHistoryItemRepository extends 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);
+}
diff --git a/src/main/java/com/andrewlalis/perfin/data/AccountRepository.java b/src/main/java/com/andrewlalis/perfin/data/AccountRepository.java
index 2c25beb..fd19b8e 100644
--- a/src/main/java/com/andrewlalis/perfin/data/AccountRepository.java
+++ b/src/main/java/com/andrewlalis/perfin/data/AccountRepository.java
@@ -3,6 +3,7 @@ package com.andrewlalis.perfin.data;
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 java.math.BigDecimal;
import java.time.Clock;
@@ -13,7 +14,7 @@ import java.util.Optional;
import java.util.Set;
public interface AccountRepository extends AutoCloseable {
- long insert(Account account);
+ long insert(AccountType type, String accountNumber, String name, Currency currency);
Page findAll(PageRequest pagination);
List findAllByCurrency(Currency currency);
Optional findById(long id);
diff --git a/src/main/java/com/andrewlalis/perfin/data/AttachmentRepository.java b/src/main/java/com/andrewlalis/perfin/data/AttachmentRepository.java
new file mode 100644
index 0000000..69cc2b4
--- /dev/null
+++ b/src/main/java/com/andrewlalis/perfin/data/AttachmentRepository.java
@@ -0,0 +1,13 @@
+package com.andrewlalis.perfin.data;
+
+import com.andrewlalis.perfin.model.Attachment;
+
+import java.nio.file.Path;
+import java.util.Optional;
+
+public interface AttachmentRepository extends AutoCloseable {
+ Attachment insert(Path sourcePath);
+ Optional findById(long attachmentId);
+ Optional findByIdentifier(String identifier);
+ void deleteById(long attachmentId);
+}
diff --git a/src/main/java/com/andrewlalis/perfin/data/BalanceRecordRepository.java b/src/main/java/com/andrewlalis/perfin/data/BalanceRecordRepository.java
index 19d73ca..82db27a 100644
--- a/src/main/java/com/andrewlalis/perfin/data/BalanceRecordRepository.java
+++ b/src/main/java/com/andrewlalis/perfin/data/BalanceRecordRepository.java
@@ -2,7 +2,13 @@ package com.andrewlalis.perfin.data;
import com.andrewlalis.perfin.model.BalanceRecord;
+import java.math.BigDecimal;
+import java.nio.file.Path;
+import java.time.LocalDateTime;
+import java.util.Currency;
+import java.util.List;
+
public interface BalanceRecordRepository extends AutoCloseable {
- long insert(BalanceRecord record);
+ long insert(LocalDateTime utcTimestamp, long accountId, BigDecimal balance, Currency currency, List attachments);
BalanceRecord findLatestByAccountId(long accountId);
}
diff --git a/src/main/java/com/andrewlalis/perfin/data/DataSource.java b/src/main/java/com/andrewlalis/perfin/data/DataSource.java
index 0447835..aa67bcd 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.model.Account;
import javafx.application.Platform;
import java.math.BigDecimal;
+import java.nio.file.Path;
import java.util.Currency;
import java.util.HashMap;
import java.util.List;
@@ -12,6 +13,8 @@ import java.util.Map;
import java.util.function.Consumer;
public interface DataSource {
+ Path getContentDir();
+
AccountRepository getAccountRepository();
default void useAccountRepository(ThrowableConsumer repoConsumer) {
DbUtil.useClosable(this::getAccountRepository, repoConsumer);
@@ -27,6 +30,13 @@ public interface DataSource {
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) {
diff --git a/src/main/java/com/andrewlalis/perfin/data/DateUtil.java b/src/main/java/com/andrewlalis/perfin/data/DateUtil.java
index 60b7e2b..245764a 100644
--- a/src/main/java/com/andrewlalis/perfin/data/DateUtil.java
+++ b/src/main/java/com/andrewlalis/perfin/data/DateUtil.java
@@ -17,4 +17,16 @@ public class DateUtil {
.atZoneSameInstant(ZoneId.systemDefault())
.format(DEFAULT_DATETIME_FORMAT_WITH_ZONE);
}
+
+ public static LocalDateTime localToUTC(LocalDateTime localTime, ZoneId localZone) {
+ return localTime.atZone(localZone).withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime();
+ }
+
+ public static LocalDateTime localToUTC(LocalDateTime localTime) {
+ return localToUTC(localTime, ZoneId.systemDefault());
+ }
+
+ public static LocalDateTime nowAsUTC() {
+ return LocalDateTime.now(ZoneOffset.UTC);
+ }
}
diff --git a/src/main/java/com/andrewlalis/perfin/data/DbUtil.java b/src/main/java/com/andrewlalis/perfin/data/DbUtil.java
index 402d571..e58a634 100644
--- a/src/main/java/com/andrewlalis/perfin/data/DbUtil.java
+++ b/src/main/java/com/andrewlalis/perfin/data/DbUtil.java
@@ -27,6 +27,10 @@ public final class DbUtil {
}
}
+ public static void setArgs(PreparedStatement stmt, Object... args) {
+ setArgs(stmt, List.of(args));
+ }
+
public static List findAll(Connection conn, String query, List