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.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();
|
||||
|
|
|
@ -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) {
|
||||
if (Files.notExists(getBackupDir(name))) return null;
|
||||
try (var files = Files.list(getBackupDir(name))) {
|
||||
return files.map(ProfileBackups::getTimestampFromBackup)
|
||||
.max(LocalDateTime::compareTo)
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue