Added sample profile generator.
This commit is contained in:
parent
36c29e0d06
commit
86ee9f8187
|
@ -2,6 +2,7 @@ package com.andrewlalis.perfin.control;
|
||||||
|
|
||||||
import com.andrewlalis.perfin.PerfinApp;
|
import com.andrewlalis.perfin.PerfinApp;
|
||||||
import com.andrewlalis.perfin.data.ProfileLoadException;
|
import com.andrewlalis.perfin.data.ProfileLoadException;
|
||||||
|
import com.andrewlalis.perfin.data.SampleProfileGenerator;
|
||||||
import com.andrewlalis.perfin.data.util.FileUtil;
|
import com.andrewlalis.perfin.data.util.FileUtil;
|
||||||
import com.andrewlalis.perfin.model.Profile;
|
import com.andrewlalis.perfin.model.Profile;
|
||||||
import com.andrewlalis.perfin.model.ProfileBackups;
|
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() {
|
private void refreshAvailableProfiles() {
|
||||||
List<String> profileNames = ProfileLoader.getAvailableProfiles();
|
List<String> profileNames = ProfileLoader.getAvailableProfiles();
|
||||||
String currentProfile = Profile.getCurrent() == null ? null : Profile.getCurrent().name();
|
String currentProfile = Profile.getCurrent() == null ? null : Profile.getCurrent().name();
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -52,6 +52,7 @@ public class ProfileBackups {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LocalDateTime getLastBackupTimestamp(String name) {
|
public static LocalDateTime getLastBackupTimestamp(String name) {
|
||||||
|
if (Files.notExists(getBackupDir(name))) return null;
|
||||||
try (var files = Files.list(getBackupDir(name))) {
|
try (var files = Files.list(getBackupDir(name))) {
|
||||||
return files.map(ProfileBackups::getTimestampFromBackup)
|
return files.map(ProfileBackups::getTimestampFromBackup)
|
||||||
.max(LocalDateTime::compareTo)
|
.max(LocalDateTime::compareTo)
|
||||||
|
|
|
@ -37,22 +37,27 @@
|
||||||
</ScrollPane>
|
</ScrollPane>
|
||||||
</center>
|
</center>
|
||||||
<bottom>
|
<bottom>
|
||||||
<BorderPane>
|
<VBox>
|
||||||
<left>
|
<BorderPane>
|
||||||
<AnchorPane styleClass="std-padding">
|
<left>
|
||||||
<Label text="Add New Profile" styleClass="bold-text" AnchorPane.leftAnchor="0" AnchorPane.topAnchor="0" AnchorPane.bottomAnchor="0"/>
|
<AnchorPane styleClass="std-padding">
|
||||||
</AnchorPane>
|
<Label text="Add New Profile" styleClass="bold-text" AnchorPane.leftAnchor="0" AnchorPane.topAnchor="0" AnchorPane.bottomAnchor="0"/>
|
||||||
</left>
|
</AnchorPane>
|
||||||
<center>
|
</left>
|
||||||
<VBox styleClass="std-padding">
|
<center>
|
||||||
<TextField fx:id="newProfileNameField" style="-fx-min-width: 50px; -fx-pref-width: 50px;"/>
|
<VBox styleClass="std-padding">
|
||||||
</VBox>
|
<TextField fx:id="newProfileNameField" style="-fx-min-width: 50px; -fx-pref-width: 50px;"/>
|
||||||
</center>
|
</VBox>
|
||||||
<right>
|
</center>
|
||||||
<VBox styleClass="std-padding">
|
<right>
|
||||||
<Button text="Add" onAction="#addProfile" fx:id="addProfileButton"/>
|
<VBox styleClass="std-padding">
|
||||||
</VBox>
|
<Button text="Add" onAction="#addProfile" fx:id="addProfileButton"/>
|
||||||
</right>
|
</VBox>
|
||||||
</BorderPane>
|
</right>
|
||||||
|
</BorderPane>
|
||||||
|
<HBox styleClass="std-spacing,std-padding">
|
||||||
|
<Button text="Create Sample Profile" styleClass="small-font" onAction="#createSampleProfile"/>
|
||||||
|
</HBox>
|
||||||
|
</VBox>
|
||||||
</bottom>
|
</bottom>
|
||||||
</BorderPane>
|
</BorderPane>
|
||||||
|
|
Loading…
Reference in New Issue