module util.sample_data; import slf4d; import handy_http_primitives : Optional, mapIfPresent; import auth; import profile; import account; import transaction; import transaction.dto; import util.money; import util.data; import std.random; import std.conv; import std.array; import std.datetime; import std.typecons; import std.file; import std.algorithm; import std.csv; void generateSampleData() { UserRepository userRepo = new FileSystemUserRepository; // Remove all existing user data. foreach (User user; userRepo.findAll()) { userRepo.deleteByUsername(user.username); } const int userCount = uniform(5, 10); for (int i = 0; i < userCount; i++) { try { generateRandomUser(i, userRepo); } catch (Throwable t) { import std.stdio; stderr.writeln(t); throw t; } } info("Random sample data generation complete."); } void generateRandomUser(int idx, UserRepository userRepo) { string username = "testuser" ~ idx.to!string; string password = "testpass"; infoF!"Generating random user %s, password: %s."(username, password); User user = createNewUser(userRepo, username, password); ProfileRepository profileRepo = new FileSystemProfileRepository(username); const int profileCount = uniform(1, 3); for (int i = 0; i < profileCount; i++) { generateRandomProfile(i, profileRepo); } } void generateRandomProfile(int idx, ProfileRepository profileRepo) { string profileName = "test-profile-" ~ idx.to!string; infoF!" Generating random profile %s."(profileName); Profile profile = profileRepo.createProfile(profileName); ProfileDataSource ds = profileRepo.getDataSource(profile); ds.getPropertiesRepository().setProperty("sample-data-idx", idx.to!string); Currency preferredCurrency = Currencies.USD; const int accountCount = uniform(3, 8); for (int i = 0; i < accountCount; i++) { generateRandomAccount(i, ds, preferredCurrency); } ds.doTransaction(() { generateVendors(ds); generateCategories(ds); }); generateRandomTransactions(ds); } void generateVendors(ProfileDataSource ds) { auto vendorRepo = ds.getTransactionVendorRepository(); string vendorsCsv = readText("sample-data/vendors.csv"); uint vendorCount = 0; foreach (record; csvReader!(Tuple!(string, string))(vendorsCsv)) { vendorRepo.insert(record[0], record[1]); vendorCount++; } infoF!" Generated %d vendors."(vendorCount); } void generateCategories(ProfileDataSource ds) { auto categoryRepo = ds.getTransactionCategoryRepository(); string categoriesCsv = readText("sample-data/categories.csv"); uint categoryCount = 0; foreach (record; csvReader!(Tuple!(string, string, string, string))(categoriesCsv)) { string parentName = record[3]; Optional!ulong parentId = Optional!ulong.empty; if (parentName !is null && parentName.length > 0) { parentId = categoryRepo.findByName(parentName).mapIfPresent!(c => c.id); } categoryRepo.insert(parentId, record[0], record[2], record[1][1..$]); } infoF!" Generated %d categories."(categoryCount); } void generateRandomAccount(int idx, ProfileDataSource ds, Currency preferredCurrency) { AccountRepository accountRepo = ds.getAccountRepository(); AccountValueRecordRepository valueRecordRepo = ds.getAccountValueRecordRepository(); string idxStr = idx.to!string; string numberSuffix = "0".replicate(4 - idxStr.length) ~ idxStr; string name = "Test Account " ~ idxStr; AccountType type = choice(ALL_ACCOUNT_TYPES); Currency currency = preferredCurrency; if (uniform01() < 0.1) { currency = choice(ALL_CURRENCIES); } string description = "This is a testing account generated by util.sample_data.generateRandomAccount()."; Account account = accountRepo.insert( type, numberSuffix, name, currency, description ); long balance = uniform(-1_000_000, 1_000_000); ulong accountAge = uniform(5, 365*10); SysTime accountOpenedAt = Clock.currTime(UTC()) - days(accountAge); valueRecordRepo.insert( accountOpenedAt, account.id, AccountValueRecordType.BALANCE, balance, account.currency ); infoF!" Generated random account: %s, #%s, %s"(name, numberSuffix, currency.code); } void generateRandomTransactions(ProfileDataSource ds) { const TransactionVendor[] vendors = ds.getTransactionVendorRepository.findAll(); const TransactionCategory[] categories = ds.getTransactionCategoryRepository() .findAllByParentId(Optional!ulong.empty); const Account[] accounts = ds.getAccountRepository().findAll(); SysTime timestamp = Clock.currTime(UTC()) - seconds(1); for (int i = 0; i < 100; i++) { AddTransactionPayload data; data.timestamp = timestamp.toISOExtString(); if (uniform01() < 0.7) { data.vendorId = Optional!ulong.of(choice(vendors).id).toNullable; } if (uniform01() < 0.8) { data.categoryId = Optional!ulong.of(choice(categories).id).toNullable; } // Randomly choose an account to credit / debit the transaction to. Account primaryAccount = choice(accounts); data.currencyCode = primaryAccount.currency.code.idup; Optional!ulong secondaryAccountId; if (uniform01() < 0.25) { foreach (acc; accounts) { if (acc.id != primaryAccount.id && acc.currency == primaryAccount.currency) { secondaryAccountId = Optional!ulong.of(acc.id); break; } } } if (uniform01() < 0.5) { data.creditedAccountId = Optional!ulong.of(primaryAccount.id).toNullable; if (secondaryAccountId) data.debitedAccountId = secondaryAccountId.toNullable; } else { data.debitedAccountId = Optional!ulong.of(primaryAccount.id).toNullable; if (secondaryAccountId) data.creditedAccountId = secondaryAccountId.toNullable; } // Randomly choose some tags to add. string[] tags; foreach (n; 1..10) { if (uniform01 < 0.25) { tags ~= "tag-" ~ n.to!string; } } data.tags = tags; data.amount = uniform(0, 1_000_000); data.description = "This is a sample transaction which was generated as part of sample data."; // Generate random line items: if (uniform01 < 0.5) { long lineItemTotal = 0; foreach (n; 1..uniform(1, 20)) { AddTransactionPayload.LineItem item; item.valuePerItem = uniform(1, 10_000); item.quantity = uniform(1, 5); lineItemTotal += item.quantity * item.valuePerItem; item.description = "Sample item " ~ n.to!string; if (uniform01 < 0.5) { TransactionCategory category = choice(categories); item.categoryId = category.id; } data.lineItems ~= item; } long diff = data.amount - lineItemTotal; // Add one final line item that adds up to the transaction total. if (diff != 0) { data.lineItems ~= AddTransactionPayload.LineItem( diff, 1, "Last item which reconciles line items total with transaction amount.", Nullable!ulong.init ); } } // Add attachments: MultipartFile[] attachments; if (uniform01 < 0.5) { for (int k = 0; k < uniform(1, 3); k++) { if (k == 0) { attachments ~= MultipartFile( "1.txt", "text/plain", cast(ubyte[]) std.file.read("sample-data/attachments/1.txt") ); } else if (k == 1) { attachments ~= MultipartFile( "2.jpg", "image/jpg", cast(ubyte[]) std.file.read("sample-data/attachments/2.jpg") ); } else if (k == 2) { attachments ~= MultipartFile( "3.png", "image/png", cast(ubyte[]) std.file.read("sample-data/attachments/3.png") ); } } } auto txn = addTransaction(ds, data, attachments); infoF!" Generated transaction %d"(txn.id); timestamp -= seconds(uniform(10, 1_000_000)); } }