From 86ee9f8187ca01dee1c5ec21057b4cfab1011d60 Mon Sep 17 00:00:00 2001 From: andrewlalis Date: Sun, 21 Jul 2024 20:25:08 -0400 Subject: [PATCH] Added sample profile generator. --- .../control/ProfilesViewController.java | 11 ++ .../perfin/data/SampleProfileGenerator.java | 186 ++++++++++++++++++ .../perfin/model/ProfileBackups.java | 1 + src/main/resources/profiles-view.fxml | 39 ++-- 4 files changed, 220 insertions(+), 17 deletions(-) create mode 100644 src/main/java/com/andrewlalis/perfin/data/SampleProfileGenerator.java diff --git a/src/main/java/com/andrewlalis/perfin/control/ProfilesViewController.java b/src/main/java/com/andrewlalis/perfin/control/ProfilesViewController.java index a37a964..0172a46 100644 --- a/src/main/java/com/andrewlalis/perfin/control/ProfilesViewController.java +++ b/src/main/java/com/andrewlalis/perfin/control/ProfilesViewController.java @@ -2,6 +2,7 @@ package com.andrewlalis.perfin.control; import com.andrewlalis.perfin.PerfinApp; import com.andrewlalis.perfin.data.ProfileLoadException; +import com.andrewlalis.perfin.data.SampleProfileGenerator; import com.andrewlalis.perfin.data.util.FileUtil; import com.andrewlalis.perfin.model.Profile; import com.andrewlalis.perfin.model.ProfileBackups; @@ -58,6 +59,16 @@ public class ProfilesViewController { } } + @FXML public void createSampleProfile() { + SampleProfileGenerator generator = new SampleProfileGenerator(PerfinApp.profileLoader); + try { + generator.createSampleProfile(); + refreshAvailableProfiles(); + } catch (Exception e) { + Popups.error(profilesVBox, e); + } + } + private void refreshAvailableProfiles() { List profileNames = ProfileLoader.getAvailableProfiles(); String currentProfile = Profile.getCurrent() == null ? null : Profile.getCurrent().name(); diff --git a/src/main/java/com/andrewlalis/perfin/data/SampleProfileGenerator.java b/src/main/java/com/andrewlalis/perfin/data/SampleProfileGenerator.java new file mode 100644 index 0000000..45ef77c --- /dev/null +++ b/src/main/java/com/andrewlalis/perfin/data/SampleProfileGenerator.java @@ -0,0 +1,186 @@ +package com.andrewlalis.perfin.data; + +import com.andrewlalis.perfin.data.pagination.PageRequest; +import com.andrewlalis.perfin.data.util.DateUtil; +import com.andrewlalis.perfin.model.*; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.nio.file.Files; +import java.sql.SQLException; +import java.time.LocalDateTime; +import java.util.*; + +public class SampleProfileGenerator { + private final ProfileLoader profileLoader; + + private final Random random; + + public SampleProfileGenerator(ProfileLoader profileLoader) { + this.profileLoader = profileLoader; + this.random = new Random(); + } + + public Profile createSampleProfile() throws ProfileLoadException, SQLException, IOException { + String name = getNewSampleProfileName(); + Profile profile = profileLoader.load(name); + generateRandomAccounts(profile); + generateBrokerageAccountAssetRecords(profile); + generateRandomTransactions(profile); + return profile; + } + + private String getNewSampleProfileName() { + int i = 1; + while (true) { + String name = "sample-" + i; + if (Files.notExists(Profile.getDir(name))) { + return name; + } + i++; + } + } + + private void generateRandomAccounts(Profile profile) { + final int accountsToCreate = random.nextInt(5, 11); + AccountRepository accountRepo = profile.dataSource().getAccountRepository(); + BalanceRecordRepository balanceRecordRepo = profile.dataSource().getBalanceRecordRepository(); + for (int i = 0; i < accountsToCreate; i++) { + long id = accountRepo.insert( + randomChoice(AccountType.values()), + randomAccountNumber(), + "Sample Account " + i, + randomChoice(Currency.getInstance("USD"), Currency.getInstance("EUR")), + "Description for sample account " + i + "." + ); + Account account = accountRepo.findById(id).orElseThrow(); + BigDecimal initialBalance = randomMoneyValue(account.getCurrency(), 0, 5000, true); + if (account.getType() == AccountType.CREDIT_CARD) { + BigDecimal creditLimit = randomMoneyValue(account.getCurrency(), 200, 10000, false); + accountRepo.saveCreditCardProperties(new CreditCardProperties(id, creditLimit)); + } + balanceRecordRepo.insert(DateUtil.nowAsUTC(), account.id, BalanceRecordType.CASH, initialBalance, account.getCurrency(), Collections.emptyList()); + } + } + + private void generateBrokerageAccountAssetRecords(Profile profile) { + AccountRepository accountRepo = profile.dataSource().getAccountRepository(); + BalanceRecordRepository balanceRecordRepo = profile.dataSource().getBalanceRecordRepository(); + List accounts = accountRepo.findAll(PageRequest.unpaged()).items(); + for (var account : accounts) { + if (account.getType() == AccountType.BROKERAGE) { + LocalDateTime cutoff = account.getCreatedAt().minusYears(5); + LocalDateTime currentTimestamp = account.getCreatedAt().minusDays(random.nextInt(1, 30)); + BigDecimal assetValue = randomMoneyValue(account.getCurrency(), 1000, 1_000_000, true); + while (currentTimestamp.isAfter(cutoff)) { + balanceRecordRepo.insert( + currentTimestamp, + account.id, + BalanceRecordType.ASSETS, + assetValue, + account.getCurrency(), + Collections.emptyList() + ); + double valueAdjustment = random.nextGaussian() * assetValue.doubleValue() / 100.0 - 0.2; + assetValue = assetValue.subtract(BigDecimal.valueOf(valueAdjustment)).setScale(4, RoundingMode.HALF_UP); + currentTimestamp = currentTimestamp.minusDays(random.nextInt(7, 60)); + } + } + } + } + + private void generateRandomTransactions(Profile profile) { + AccountRepository accountRepo = profile.dataSource().getAccountRepository(); + TransactionRepository transactionRepo = profile.dataSource().getTransactionRepository(); + TransactionVendorRepository vendorRepo = profile.dataSource().getTransactionVendorRepository(); + TransactionCategoryRepository categoryRepo = profile.dataSource().getTransactionCategoryRepository(); + final int vendorCount = 50; + for (int i = 0; i < vendorCount; i++) { + vendorRepo.insert("Vendor " + i); + } + List vendors = vendorRepo.findAll().stream().map(TransactionVendor::getName).toList(); + final int tagCount = 10; + List tags = new ArrayList<>(tagCount); + for (int i = 0; i < tagCount; i++) { + tags.add("tag-" + i); + } + List categories = categoryRepo.findAll().stream().map(TransactionCategory::getName).toList(); + + for (var account : accountRepo.findAll(PageRequest.unpaged()).items()) { + LocalDateTime cutoff = account.getCreatedAt().minusMonths(3); + LocalDateTime timestamp = account.getCreatedAt().minusSeconds(random.nextInt(60, 60*60*24)); + while (timestamp.isAfter(cutoff)) { + String vendor = null; + if (randomChance(0.75)) { + vendor = randomChoice(vendors); + } + String category = null; + if (randomChance(0.8)) { + category = randomChoice(categories); + } + Set tagsToUse = new HashSet<>(); + if (randomChance(0.75)) { + for (int i = 0; i < random.nextInt(3); i++) { + tagsToUse.add(randomChoice(tags)); + } + } + BigDecimal transactionAmount = randomMoneyValue(account.getCurrency(), 1, 500, true); + CreditAndDebitAccounts accounts = new CreditAndDebitAccounts(account, null); + if (randomChance(0.1)) { + accounts = new CreditAndDebitAccounts(null, account); + transactionAmount = randomMoneyValue(account.getCurrency(), 500, 2000, true); + } + + transactionRepo.insert( + timestamp, + transactionAmount, + account.getCurrency(), + "Sample transaction description.", + accounts, + vendor, + category, + tagsToUse, + Collections.emptyList(), + Collections.emptyList() + ); + + timestamp = timestamp.minusSeconds(random.nextInt(60, 60*60*24 * 30)); + } + } + } + + private BigDecimal randomMoneyValue(Currency currency, int min, int max, boolean includeDecimals) { + int wholeValue = random.nextInt(min, max + 1); + BigDecimal value = BigDecimal.valueOf(wholeValue * 10000L, 4); + if (includeDecimals && currency.getDefaultFractionDigits() > 0) { + int orderOfMagnitude = (int) Math.pow(10, currency.getDefaultFractionDigits()); + int decimalValue = random.nextInt( orderOfMagnitude + 1); + BigDecimal fractionalValue = BigDecimal.valueOf(decimalValue, currency.getDefaultFractionDigits()); + value = value.add(fractionalValue); + } + return value.setScale(4, RoundingMode.HALF_UP); + } + + private String randomAccountNumber() { + String alphabet = "0123456789"; + StringBuilder sb = new StringBuilder(16); + for (int i = 0; i < 16; i++) { + sb.append(alphabet.charAt(random.nextInt(alphabet.length()))); + } + return sb.toString(); + } + + @SafeVarargs + private T randomChoice(T... items) { + return items[random.nextInt(items.length)]; + } + + private T randomChoice(List items) { + return items.get(random.nextInt(items.size())); + } + + private boolean randomChance(double percentChance) { + return random.nextDouble() <= percentChance; + } +} diff --git a/src/main/java/com/andrewlalis/perfin/model/ProfileBackups.java b/src/main/java/com/andrewlalis/perfin/model/ProfileBackups.java index 9381a3d..80e06fd 100644 --- a/src/main/java/com/andrewlalis/perfin/model/ProfileBackups.java +++ b/src/main/java/com/andrewlalis/perfin/model/ProfileBackups.java @@ -52,6 +52,7 @@ public class ProfileBackups { } public static LocalDateTime getLastBackupTimestamp(String name) { + if (Files.notExists(getBackupDir(name))) return null; try (var files = Files.list(getBackupDir(name))) { return files.map(ProfileBackups::getTimestampFromBackup) .max(LocalDateTime::compareTo) diff --git a/src/main/resources/profiles-view.fxml b/src/main/resources/profiles-view.fxml index a4ed63e..9e516ef 100644 --- a/src/main/resources/profiles-view.fxml +++ b/src/main/resources/profiles-view.fxml @@ -37,22 +37,27 @@ - - - - - -
- - - -
- - -