Added sample profile generator.

This commit is contained in:
Andrew Lalis 2024-07-21 20:25:08 -04:00
parent 36c29e0d06
commit 86ee9f8187
4 changed files with 220 additions and 17 deletions

View File

@ -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<String> profileNames = ProfileLoader.getAvailableProfiles();
String currentProfile = Profile.getCurrent() == null ? null : Profile.getCurrent().name();

View File

@ -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<Account> 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<String> vendors = vendorRepo.findAll().stream().map(TransactionVendor::getName).toList();
final int tagCount = 10;
List<String> tags = new ArrayList<>(tagCount);
for (int i = 0; i < tagCount; i++) {
tags.add("tag-" + i);
}
List<String> 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<String> 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> T randomChoice(T... items) {
return items[random.nextInt(items.length)];
}
private <T> T randomChoice(List<T> items) {
return items.get(random.nextInt(items.size()));
}
private boolean randomChance(double percentChance) {
return random.nextDouble() <= percentChance;
}
}

View File

@ -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)

View File

@ -37,22 +37,27 @@
</ScrollPane>
</center>
<bottom>
<BorderPane>
<left>
<AnchorPane styleClass="std-padding">
<Label text="Add New Profile" styleClass="bold-text" AnchorPane.leftAnchor="0" AnchorPane.topAnchor="0" AnchorPane.bottomAnchor="0"/>
</AnchorPane>
</left>
<center>
<VBox styleClass="std-padding">
<TextField fx:id="newProfileNameField" style="-fx-min-width: 50px; -fx-pref-width: 50px;"/>
</VBox>
</center>
<right>
<VBox styleClass="std-padding">
<Button text="Add" onAction="#addProfile" fx:id="addProfileButton"/>
</VBox>
</right>
</BorderPane>
<VBox>
<BorderPane>
<left>
<AnchorPane styleClass="std-padding">
<Label text="Add New Profile" styleClass="bold-text" AnchorPane.leftAnchor="0" AnchorPane.topAnchor="0" AnchorPane.bottomAnchor="0"/>
</AnchorPane>
</left>
<center>
<VBox styleClass="std-padding">
<TextField fx:id="newProfileNameField" style="-fx-min-width: 50px; -fx-pref-width: 50px;"/>
</VBox>
</center>
<right>
<VBox styleClass="std-padding">
<Button text="Add" onAction="#addProfile" fx:id="addProfileButton"/>
</VBox>
</right>
</BorderPane>
<HBox styleClass="std-spacing,std-padding">
<Button text="Create Sample Profile" styleClass="small-font" onAction="#createSampleProfile"/>
</HBox>
</VBox>
</bottom>
</BorderPane>