Added transaction generation to sample data generation in the API.
This commit is contained in:
parent
f5c75ccf35
commit
3fa2938f48
|
@ -141,9 +141,9 @@ CREATE TABLE account_value_record (
|
||||||
timestamp TEXT NOT NULL,
|
timestamp TEXT NOT NULL,
|
||||||
account_id INTEGER NOT NULL,
|
account_id INTEGER NOT NULL,
|
||||||
type TEXT NOT NULL DEFAULT 'BALANCE',
|
type TEXT NOT NULL DEFAULT 'BALANCE',
|
||||||
balance INTEGER NOT NULL,
|
value INTEGER NOT NULL,
|
||||||
currency TEXT NOT NULL,
|
currency TEXT NOT NULL,
|
||||||
CONSTRAINT fk_balance_record_account
|
CONSTRAINT fk_account_value_record_account
|
||||||
FOREIGN KEY (account_id) REFERENCES account(id)
|
FOREIGN KEY (account_id) REFERENCES account(id)
|
||||||
ON UPDATE CASCADE ON DELETE CASCADE
|
ON UPDATE CASCADE ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,6 +6,8 @@ import account.model;
|
||||||
import util.money;
|
import util.money;
|
||||||
import history.model;
|
import history.model;
|
||||||
|
|
||||||
|
import std.datetime : SysTime;
|
||||||
|
|
||||||
interface AccountRepository {
|
interface AccountRepository {
|
||||||
Optional!Account findById(ulong id);
|
Optional!Account findById(ulong id);
|
||||||
Account insert(AccountType type, string numberSuffix, string name, Currency currency, string description);
|
Account insert(AccountType type, string numberSuffix, string name, Currency currency, string description);
|
||||||
|
@ -17,3 +19,16 @@ interface AccountRepository {
|
||||||
void setCreditCardProperties(ulong id, in AccountCreditCardProperties props);
|
void setCreditCardProperties(ulong id, in AccountCreditCardProperties props);
|
||||||
History getHistory(ulong id);
|
History getHistory(ulong id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AccountJournalEntryRepository {
|
||||||
|
Optional!AccountJournalEntry findById(ulong id);
|
||||||
|
AccountJournalEntry insert(
|
||||||
|
SysTime timestamp,
|
||||||
|
ulong accountId,
|
||||||
|
ulong transactionId,
|
||||||
|
ulong amount,
|
||||||
|
AccountJournalEntryType type,
|
||||||
|
Currency currency
|
||||||
|
);
|
||||||
|
void deleteById(ulong id);
|
||||||
|
}
|
||||||
|
|
|
@ -185,3 +185,63 @@ SQL",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SqliteAccountJournalEntryRepository : AccountJournalEntryRepository {
|
||||||
|
private Database db;
|
||||||
|
this(Database db) {
|
||||||
|
this.db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional!AccountJournalEntry findById(ulong id) {
|
||||||
|
return util.sqlite.findById(db, "account_journal_entry", &parseEntry, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
AccountJournalEntry insert(
|
||||||
|
SysTime timestamp,
|
||||||
|
ulong accountId,
|
||||||
|
ulong transactionId,
|
||||||
|
ulong amount,
|
||||||
|
AccountJournalEntryType type,
|
||||||
|
Currency currency
|
||||||
|
) {
|
||||||
|
util.sqlite.update(
|
||||||
|
db,
|
||||||
|
"INSERT INTO account_journal_entry
|
||||||
|
(timestamp, account_id, transaction_id, amount, type, currency)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)",
|
||||||
|
timestamp.toISOExtString(),
|
||||||
|
accountId,
|
||||||
|
transactionId,
|
||||||
|
amount,
|
||||||
|
type,
|
||||||
|
currency.code
|
||||||
|
);
|
||||||
|
ulong id = db.lastInsertRowid();
|
||||||
|
return findById(id).orElseThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
void deleteById(ulong id) {
|
||||||
|
util.sqlite.deleteById(db, "account_journal_entry", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
static AccountJournalEntry parseEntry(Row row) {
|
||||||
|
string typeStr = row.peek!(string, PeekMode.slice)(5);
|
||||||
|
AccountJournalEntryType type;
|
||||||
|
if (typeStr == "CREDIT") {
|
||||||
|
type = AccountJournalEntryType.CREDIT;
|
||||||
|
} else if (typeStr == "DEBIT") {
|
||||||
|
type = AccountJournalEntryType.DEBIT;
|
||||||
|
} else {
|
||||||
|
throw new Exception("Invalid account journal entry type: " ~ typeStr);
|
||||||
|
}
|
||||||
|
return AccountJournalEntry(
|
||||||
|
row.peek!ulong(0),
|
||||||
|
SysTime.fromISOExtString(row.peek!string(1)),
|
||||||
|
row.peek!ulong(2),
|
||||||
|
row.peek!ulong(3),
|
||||||
|
row.peek!ulong(4),
|
||||||
|
type,
|
||||||
|
Currency.ofCode(row.peek!(string, PeekMode.slice)(6))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,9 +6,9 @@ import std.traits : EnumMembers;
|
||||||
import util.money;
|
import util.money;
|
||||||
|
|
||||||
struct AccountType {
|
struct AccountType {
|
||||||
const string id;
|
immutable string id;
|
||||||
const string name;
|
immutable string name;
|
||||||
const bool debitsPositive;
|
immutable bool debitsPositive;
|
||||||
|
|
||||||
static AccountType fromId(string id) {
|
static AccountType fromId(string id) {
|
||||||
static foreach (t; ALL_ACCOUNT_TYPES) {
|
static foreach (t; ALL_ACCOUNT_TYPES) {
|
||||||
|
@ -28,17 +28,45 @@ enum AccountTypes : AccountType {
|
||||||
immutable(AccountType[]) ALL_ACCOUNT_TYPES = cast(AccountType[]) [ EnumMembers!AccountTypes ];
|
immutable(AccountType[]) ALL_ACCOUNT_TYPES = cast(AccountType[]) [ EnumMembers!AccountTypes ];
|
||||||
|
|
||||||
struct Account {
|
struct Account {
|
||||||
const ulong id;
|
immutable ulong id;
|
||||||
const SysTime createdAt;
|
immutable SysTime createdAt;
|
||||||
const bool archived;
|
immutable bool archived;
|
||||||
const AccountType type;
|
immutable AccountType type;
|
||||||
const string numberSuffix;
|
immutable string numberSuffix;
|
||||||
const string name;
|
immutable string name;
|
||||||
const Currency currency;
|
immutable Currency currency;
|
||||||
const string description;
|
immutable string description;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AccountCreditCardProperties {
|
struct AccountCreditCardProperties {
|
||||||
const ulong account_id;
|
immutable ulong account_id;
|
||||||
const long creditLimit;
|
immutable long creditLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AccountJournalEntryType : string {
|
||||||
|
CREDIT = "CREDIT",
|
||||||
|
DEBIT = "DEBIT"
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AccountJournalEntry {
|
||||||
|
immutable ulong id;
|
||||||
|
immutable SysTime timestamp;
|
||||||
|
immutable ulong accountId;
|
||||||
|
immutable ulong transactionId;
|
||||||
|
immutable ulong amount;
|
||||||
|
immutable AccountJournalEntryType type;
|
||||||
|
immutable Currency currency;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AccountValueRecordType : string {
|
||||||
|
BALANCE = "BALANCE"
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AccountValueRecord {
|
||||||
|
immutable ulong id;
|
||||||
|
immutable SysTime timestamp;
|
||||||
|
immutable ulong accountId;
|
||||||
|
immutable AccountValueRecordType type;
|
||||||
|
immutable long value;
|
||||||
|
immutable Currency currency;
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,11 +58,11 @@ SQL",
|
||||||
static Attachment parseAttachment(Row row) {
|
static Attachment parseAttachment(Row row) {
|
||||||
return Attachment(
|
return Attachment(
|
||||||
row.peek!ulong(0),
|
row.peek!ulong(0),
|
||||||
SysTime.fromISOExtString(row.peek!string(1), UTC()),
|
parseISOTimestamp(row, 1),
|
||||||
row.peek!string(2),
|
row.peek!string(2),
|
||||||
row.peek!string(3),
|
row.peek!string(3),
|
||||||
row.peek!ulong(4),
|
row.peek!ulong(4),
|
||||||
row.peek!(ubyte[])(5)
|
parseBlob(row, 5)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,10 @@ module attachment.model;
|
||||||
import std.datetime;
|
import std.datetime;
|
||||||
|
|
||||||
struct Attachment {
|
struct Attachment {
|
||||||
ulong id;
|
immutable ulong id;
|
||||||
SysTime uploadedAt;
|
immutable SysTime uploadedAt;
|
||||||
string filename;
|
immutable string filename;
|
||||||
string contentType;
|
immutable string contentType;
|
||||||
ulong size;
|
immutable ulong size;
|
||||||
ubyte[] content;
|
immutable ubyte[] content;
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,8 +25,18 @@ interface PropertiesRepository {
|
||||||
* gateway for all data access operations for a profile.
|
* gateway for all data access operations for a profile.
|
||||||
*/
|
*/
|
||||||
interface ProfileDataSource {
|
interface ProfileDataSource {
|
||||||
import account.data : AccountRepository;
|
import account.data;
|
||||||
|
import transaction.data;
|
||||||
|
|
||||||
PropertiesRepository getPropertiesRepository();
|
PropertiesRepository getPropertiesRepository();
|
||||||
|
|
||||||
AccountRepository getAccountRepository();
|
AccountRepository getAccountRepository();
|
||||||
|
AccountJournalEntryRepository getAccountJournalEntryRepository();
|
||||||
|
|
||||||
|
TransactionVendorRepository getTransactionVendorRepository();
|
||||||
|
TransactionCategoryRepository getTransactionCategoryRepository();
|
||||||
|
TransactionTagRepository getTransactionTagRepository();
|
||||||
|
TransactionRepository getTransactionRepository();
|
||||||
|
|
||||||
|
void doTransaction(void delegate () dg);
|
||||||
}
|
}
|
||||||
|
|
|
@ -141,6 +141,8 @@ class SqlitePropertiesRepository : PropertiesRepository {
|
||||||
class SqliteProfileDataSource : ProfileDataSource {
|
class SqliteProfileDataSource : ProfileDataSource {
|
||||||
import account.data;
|
import account.data;
|
||||||
import account.data_impl_sqlite;
|
import account.data_impl_sqlite;
|
||||||
|
import transaction.data;
|
||||||
|
import transaction.data_impl_sqlite;
|
||||||
|
|
||||||
const SCHEMA = import("schema.sql");
|
const SCHEMA = import("schema.sql");
|
||||||
private const string dbPath;
|
private const string dbPath;
|
||||||
|
@ -164,4 +166,28 @@ class SqliteProfileDataSource : ProfileDataSource {
|
||||||
AccountRepository getAccountRepository() {
|
AccountRepository getAccountRepository() {
|
||||||
return new SqliteAccountRepository(db);
|
return new SqliteAccountRepository(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AccountJournalEntryRepository getAccountJournalEntryRepository() {
|
||||||
|
return new SqliteAccountJournalEntryRepository(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
TransactionVendorRepository getTransactionVendorRepository() {
|
||||||
|
return new SqliteTransactionVendorRepository(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
TransactionCategoryRepository getTransactionCategoryRepository() {
|
||||||
|
return new SqliteTransactionCategoryRepository(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
TransactionTagRepository getTransactionTagRepository() {
|
||||||
|
return new SqliteTransactionTagRepository(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
TransactionRepository getTransactionRepository() {
|
||||||
|
return new SqliteTransactionRepository(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
void doTransaction(void delegate () dg) {
|
||||||
|
util.sqlite.doTransaction(db, dg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -179,3 +179,59 @@ class SqliteTransactionTagRepository : TransactionTagRepository {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SqliteTransactionRepository : TransactionRepository {
|
||||||
|
private const TABLE_NAME = "\"transaction\"";
|
||||||
|
private Database db;
|
||||||
|
this(Database db) {
|
||||||
|
this.db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional!Transaction findById(ulong id) {
|
||||||
|
return util.sqlite.findById(db, TABLE_NAME, &parseTransaction, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Transaction insert(
|
||||||
|
SysTime timestamp,
|
||||||
|
SysTime addedAt,
|
||||||
|
ulong amount,
|
||||||
|
Currency currency,
|
||||||
|
string description,
|
||||||
|
Optional!ulong vendorId,
|
||||||
|
Optional!ulong categoryId
|
||||||
|
) {
|
||||||
|
util.sqlite.update(
|
||||||
|
db,
|
||||||
|
"INSERT INTO " ~ TABLE_NAME ~ "
|
||||||
|
(timestamp, added_at, amount, currency, description, vendor_id, category_id)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
timestamp.toISOExtString(),
|
||||||
|
addedAt.toISOExtString(),
|
||||||
|
amount,
|
||||||
|
currency.code,
|
||||||
|
description,
|
||||||
|
vendorId.asNullable,
|
||||||
|
categoryId.asNullable
|
||||||
|
);
|
||||||
|
ulong id = db.lastInsertRowid();
|
||||||
|
return findById(id).orElseThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
void deleteById(ulong id) {
|
||||||
|
util.sqlite.deleteById(db, TABLE_NAME, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Transaction parseTransaction(Row row) {
|
||||||
|
import std.typecons : Nullable;
|
||||||
|
return Transaction(
|
||||||
|
row.peek!ulong(0),
|
||||||
|
SysTime.fromISOExtString(row.peek!string(1)),
|
||||||
|
SysTime.fromISOExtString(row.peek!string(2)),
|
||||||
|
row.peek!ulong(3),
|
||||||
|
Currency.ofCode(row.peek!(string, PeekMode.slice)(4)),
|
||||||
|
row.peek!string(5),
|
||||||
|
Optional!(ulong).of(row.peek!(Nullable!ulong)(6)),
|
||||||
|
Optional!(ulong).of(row.peek!(Nullable!ulong)(7))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,43 +6,43 @@ import std.datetime;
|
||||||
import util.money;
|
import util.money;
|
||||||
|
|
||||||
struct TransactionVendor {
|
struct TransactionVendor {
|
||||||
const ulong id;
|
immutable ulong id;
|
||||||
const string name;
|
immutable string name;
|
||||||
const string description;
|
immutable string description;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TransactionCategory {
|
struct TransactionCategory {
|
||||||
const ulong id;
|
immutable ulong id;
|
||||||
const Optional!ulong parentId;
|
immutable Optional!ulong parentId;
|
||||||
const string name;
|
immutable string name;
|
||||||
const string description;
|
immutable string description;
|
||||||
const string color;
|
immutable string color;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TransactionTag {
|
struct TransactionTag {
|
||||||
const ulong id;
|
immutable ulong id;
|
||||||
const string name;
|
immutable string name;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Transaction {
|
struct Transaction {
|
||||||
const ulong id;
|
immutable ulong id;
|
||||||
/// The time at which the transaction happened.
|
/// The time at which the transaction happened.
|
||||||
const SysTime timestamp;
|
immutable SysTime timestamp;
|
||||||
/// The time at which the transaction entity was saved.
|
/// The time at which the transaction entity was saved.
|
||||||
const SysTime addedAt;
|
immutable SysTime addedAt;
|
||||||
const ulong amount;
|
immutable ulong amount;
|
||||||
const Currency currency;
|
immutable Currency currency;
|
||||||
const string description;
|
immutable string description;
|
||||||
const Optional!ulong vendorId;
|
immutable Optional!ulong vendorId;
|
||||||
const Optional!ulong categoryId;
|
immutable Optional!ulong categoryId;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TransactionLineItem {
|
struct TransactionLineItem {
|
||||||
const ulong id;
|
immutable ulong id;
|
||||||
const ulong transactionId;
|
immutable ulong transactionId;
|
||||||
const long valuePerItem;
|
immutable long valuePerItem;
|
||||||
const ulong quantity;
|
immutable ulong quantity;
|
||||||
const uint idx;
|
immutable uint idx;
|
||||||
const string description;
|
immutable string description;
|
||||||
const Optional!ulong categoryId;
|
immutable Optional!ulong categoryId;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,3 +2,4 @@ module transaction;
|
||||||
|
|
||||||
public import transaction.data;
|
public import transaction.data;
|
||||||
public import transaction.model;
|
public import transaction.model;
|
||||||
|
public import transaction.service;
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
module transaction.service;
|
||||||
|
|
||||||
|
import handy_httpd.components.optional;
|
||||||
|
import std.datetime;
|
||||||
|
|
||||||
|
import transaction.model;
|
||||||
|
import transaction.data;
|
||||||
|
import profile.data;
|
||||||
|
import util.money;
|
||||||
|
import account.model;
|
||||||
|
|
||||||
|
void addTransaction(
|
||||||
|
ProfileDataSource ds,
|
||||||
|
SysTime timestamp,
|
||||||
|
SysTime addedAt,
|
||||||
|
ulong amount,
|
||||||
|
Currency currency,
|
||||||
|
string description,
|
||||||
|
Optional!ulong vendorId,
|
||||||
|
Optional!ulong categoryId,
|
||||||
|
Optional!ulong creditedAccountId,
|
||||||
|
Optional!ulong debitedAccountId,
|
||||||
|
TransactionLineItem[] lineItems
|
||||||
|
// TODO: Add attachments and tags!
|
||||||
|
) {
|
||||||
|
if (creditedAccountId.isNull && debitedAccountId.isNull) {
|
||||||
|
throw new Exception("At least one account must be linked to a transaction.");
|
||||||
|
}
|
||||||
|
ds.doTransaction(() {
|
||||||
|
auto journalEntryRepo = ds.getAccountJournalEntryRepository();
|
||||||
|
auto txRepo = ds.getTransactionRepository();
|
||||||
|
Transaction tx = txRepo.insert(
|
||||||
|
timestamp,
|
||||||
|
addedAt,
|
||||||
|
amount,
|
||||||
|
currency,
|
||||||
|
description,
|
||||||
|
vendorId,
|
||||||
|
categoryId
|
||||||
|
);
|
||||||
|
if (creditedAccountId) {
|
||||||
|
journalEntryRepo.insert(
|
||||||
|
timestamp,
|
||||||
|
creditedAccountId.value,
|
||||||
|
tx.id,
|
||||||
|
amount,
|
||||||
|
AccountJournalEntryType.CREDIT,
|
||||||
|
currency
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (debitedAccountId) {
|
||||||
|
journalEntryRepo.insert(
|
||||||
|
timestamp,
|
||||||
|
debitedAccountId.value,
|
||||||
|
tx.id,
|
||||||
|
amount,
|
||||||
|
AccountJournalEntryType.DEBIT,
|
||||||
|
currency
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -8,11 +8,11 @@ import std.traits : isSomeString, EnumMembers;
|
||||||
*/
|
*/
|
||||||
struct Currency {
|
struct Currency {
|
||||||
/// The common 3-character code for the currency, like "USD".
|
/// The common 3-character code for the currency, like "USD".
|
||||||
const char[3] code;
|
immutable char[3] code;
|
||||||
/// The number of digits after the decimal place that the currency supports.
|
/// The number of digits after the decimal place that the currency supports.
|
||||||
const ubyte fractionalDigits;
|
immutable ubyte fractionalDigits;
|
||||||
/// The ISO 4217 numeric code for the currency.
|
/// The ISO 4217 numeric code for the currency.
|
||||||
const ushort numericCode;
|
immutable ushort numericCode;
|
||||||
|
|
||||||
static Currency ofCode(S)(S code) if (isSomeString!S) {
|
static Currency ofCode(S)(S code) if (isSomeString!S) {
|
||||||
if (code.length != 3) {
|
if (code.length != 3) {
|
||||||
|
@ -50,8 +50,8 @@ unittest {
|
||||||
* so for example, with USD currency, a value of 123 indicates $1.23.
|
* so for example, with USD currency, a value of 123 indicates $1.23.
|
||||||
*/
|
*/
|
||||||
struct MoneyValue {
|
struct MoneyValue {
|
||||||
const Currency currency;
|
immutable Currency currency;
|
||||||
const long value;
|
immutable long value;
|
||||||
|
|
||||||
int opCmp(in MoneyValue other) const {
|
int opCmp(in MoneyValue other) const {
|
||||||
if (other.currency != this.currency) return 0;
|
if (other.currency != this.currency) return 0;
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
module util.sample_data;
|
module util.sample_data;
|
||||||
|
|
||||||
import slf4d;
|
import slf4d;
|
||||||
|
import handy_httpd.components.optional;
|
||||||
|
|
||||||
import auth;
|
import auth;
|
||||||
import profile;
|
import profile;
|
||||||
import account;
|
import account;
|
||||||
|
import transaction;
|
||||||
import util.money;
|
import util.money;
|
||||||
|
|
||||||
import std.random;
|
import std.random;
|
||||||
import std.conv;
|
import std.conv;
|
||||||
import std.array;
|
import std.array;
|
||||||
|
import std.datetime;
|
||||||
|
|
||||||
void generateSampleData() {
|
void generateSampleData() {
|
||||||
UserRepository userRepo = new FileSystemUserRepository;
|
UserRepository userRepo = new FileSystemUserRepository;
|
||||||
|
@ -20,7 +23,13 @@ void generateSampleData() {
|
||||||
|
|
||||||
const int userCount = uniform(5, 10);
|
const int userCount = uniform(5, 10);
|
||||||
for (int i = 0; i < userCount; i++) {
|
for (int i = 0; i < userCount; i++) {
|
||||||
generateRandomUser(i, userRepo);
|
try {
|
||||||
|
generateRandomUser(i, userRepo);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
import std.stdio;
|
||||||
|
stderr.writeln(t);
|
||||||
|
throw t;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
info("Random sample data generation complete.");
|
info("Random sample data generation complete.");
|
||||||
}
|
}
|
||||||
|
@ -48,6 +57,29 @@ void generateRandomProfile(int idx, ProfileRepository profileRepo) {
|
||||||
for (int i = 0; i < accountCount; i++) {
|
for (int i = 0; i < accountCount; i++) {
|
||||||
generateRandomAccount(i, ds);
|
generateRandomAccount(i, ds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto vendorRepo = ds.getTransactionVendorRepository();
|
||||||
|
const int vendorCount = uniform(5, 30);
|
||||||
|
for (int i = 0; i < vendorCount; i++) {
|
||||||
|
vendorRepo.insert("Test Vendor " ~ to!string(i), "Testing vendor for sample data.");
|
||||||
|
}
|
||||||
|
infoF!" Generated %d random vendors."(vendorCount);
|
||||||
|
|
||||||
|
auto tagRepo = ds.getTransactionTagRepository();
|
||||||
|
const int tagCount = uniform(5, 30);
|
||||||
|
for (int i = 0; i < tagCount; i++) {
|
||||||
|
tagRepo.insert("test-tag-" ~ to!string(i));
|
||||||
|
}
|
||||||
|
infoF!" Generated %d random tags."(tagCount);
|
||||||
|
|
||||||
|
auto categoryRepo = ds.getTransactionCategoryRepository();
|
||||||
|
const int categoryCount = uniform(5, 30);
|
||||||
|
for (int i = 0; i < categoryCount; i++) {
|
||||||
|
categoryRepo.insert(Optional!ulong.empty, "Test Category " ~ to!string(i), "Testing category.", "FFFFFF");
|
||||||
|
}
|
||||||
|
infoF!" Generated %d random categories."(categoryCount);
|
||||||
|
|
||||||
|
generateRandomTransactions(ds);
|
||||||
}
|
}
|
||||||
|
|
||||||
void generateRandomAccount(int idx, ProfileDataSource ds) {
|
void generateRandomAccount(int idx, ProfileDataSource ds) {
|
||||||
|
@ -58,7 +90,6 @@ void generateRandomAccount(int idx, ProfileDataSource ds) {
|
||||||
AccountType type = choice(ALL_ACCOUNT_TYPES);
|
AccountType type = choice(ALL_ACCOUNT_TYPES);
|
||||||
Currency currency = choice(ALL_CURRENCIES);
|
Currency currency = choice(ALL_CURRENCIES);
|
||||||
string description = "This is a testing account generated by util.sample_data.generateRandomAccount().";
|
string description = "This is a testing account generated by util.sample_data.generateRandomAccount().";
|
||||||
infoF!" Generating random account: %s, ...%s"(name, numberSuffix);
|
|
||||||
Account account = accountRepo.insert(
|
Account account = accountRepo.insert(
|
||||||
type,
|
type,
|
||||||
numberSuffix,
|
numberSuffix,
|
||||||
|
@ -66,4 +97,65 @@ void generateRandomAccount(int idx, ProfileDataSource ds) {
|
||||||
currency,
|
currency,
|
||||||
description
|
description
|
||||||
);
|
);
|
||||||
|
infoF!" Generated random account: %s, #%s"(name, numberSuffix);
|
||||||
|
}
|
||||||
|
|
||||||
|
void generateRandomTransactions(ProfileDataSource ds) {
|
||||||
|
const bool hasVendor = uniform01() > 0.3;
|
||||||
|
const bool hasCategory = uniform01() > 0.2;
|
||||||
|
const TransactionVendor[] vendors = ds.getTransactionVendorRepository.findAll();
|
||||||
|
const TransactionCategory[] categories = ds.getTransactionCategoryRepository()
|
||||||
|
.findAllByParentId(Optional!ulong.empty);
|
||||||
|
const TransactionTag[] tags = ds.getTransactionTagRepository().findAll();
|
||||||
|
const Account[] accounts = ds.getAccountRepository().findAll();
|
||||||
|
|
||||||
|
SysTime now = Clock.currTime(UTC());
|
||||||
|
SysTime timestamp = Clock.currTime(UTC()) - seconds(1);
|
||||||
|
|
||||||
|
for (int i = 0; i < 1000; i++) {
|
||||||
|
Optional!ulong vendorId;
|
||||||
|
if (hasVendor) {
|
||||||
|
vendorId = Optional!ulong.of(choice(vendors).id);
|
||||||
|
}
|
||||||
|
Optional!ulong categoryId;
|
||||||
|
if (hasCategory) {
|
||||||
|
categoryId = Optional!ulong.of(choice(categories).id);
|
||||||
|
}
|
||||||
|
Optional!ulong creditedAccountId;
|
||||||
|
Optional!ulong debitedAccountId;
|
||||||
|
Account primaryAccount = choice(accounts);
|
||||||
|
Optional!ulong secondaryAccount;
|
||||||
|
if (uniform01() < 0.25) {
|
||||||
|
foreach (acc; accounts) {
|
||||||
|
if (acc.id != primaryAccount.id && acc.currency == primaryAccount.currency) {
|
||||||
|
secondaryAccount.value = acc.id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (uniform01() > 0.5) {
|
||||||
|
creditedAccountId = Optional!ulong.of(primaryAccount.id);
|
||||||
|
if (secondaryAccount) debitedAccountId = secondaryAccount;
|
||||||
|
} else {
|
||||||
|
debitedAccountId = Optional!ulong.of(primaryAccount.id);
|
||||||
|
if (secondaryAccount) creditedAccountId = secondaryAccount;
|
||||||
|
}
|
||||||
|
ulong value = uniform(0, 1_000_000);
|
||||||
|
|
||||||
|
addTransaction(
|
||||||
|
ds,
|
||||||
|
timestamp,
|
||||||
|
now,
|
||||||
|
value,
|
||||||
|
primaryAccount.currency,
|
||||||
|
"Test transaction " ~ to!string(i),
|
||||||
|
vendorId,
|
||||||
|
categoryId,
|
||||||
|
creditedAccountId,
|
||||||
|
debitedAccountId,
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
infoF!" Generated transaction %d"(i);
|
||||||
|
timestamp -= seconds(uniform(10, 1_000_000));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
module util.sqlite;
|
module util.sqlite;
|
||||||
|
|
||||||
|
import std.datetime;
|
||||||
|
|
||||||
import slf4d;
|
import slf4d;
|
||||||
import handy_httpd.components.optional;
|
import handy_httpd.components.optional;
|
||||||
import d2sqlite3;
|
import d2sqlite3;
|
||||||
|
@ -31,7 +33,7 @@ Optional!T findOne(T, Args...)(Database db, string query, T function(Row) result
|
||||||
* Returns: An optional result.
|
* Returns: An optional result.
|
||||||
*/
|
*/
|
||||||
Optional!T findById(T)(Database db, string table, T function(Row) resultMapper, ulong id) {
|
Optional!T findById(T)(Database db, string table, T function(Row) resultMapper, ulong id) {
|
||||||
Statement stmt = db.prepare("SELECT * FROM " ~ table);
|
Statement stmt = db.prepare("SELECT * FROM " ~ table ~ " WHERE id = ?");
|
||||||
stmt.bind(1, id);
|
stmt.bind(1, id);
|
||||||
ResultRange result = stmt.execute();
|
ResultRange result = stmt.execute();
|
||||||
if (result.empty) return Optional!T.empty;
|
if (result.empty) return Optional!T.empty;
|
||||||
|
@ -123,3 +125,44 @@ T doTransaction(T)(Database db, T delegate() dg) {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a "SELECT COUNT..." query on a database.
|
||||||
|
* Params:
|
||||||
|
* db = The database to use.
|
||||||
|
* query = The query to use, which must return an integer as its sole result.
|
||||||
|
* args = Arguments to provide to the query.
|
||||||
|
* Returns: The count returned by the query.
|
||||||
|
*/
|
||||||
|
ulong count(Args...)(Database db, string query, Args args) {
|
||||||
|
Statement stmt = db.prepare(query);
|
||||||
|
stmt.bindAll(args);
|
||||||
|
ResultRange result = stmt.execute();
|
||||||
|
if (result.empty) return 0;
|
||||||
|
return result.front.peek!ulong(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads an ISO-8601 UTC timestamp from a result row.
|
||||||
|
* Params:
|
||||||
|
* row = The row to read from.
|
||||||
|
* idx = The column index in the row to read.
|
||||||
|
* Returns: The timestamp that was read.
|
||||||
|
*/
|
||||||
|
SysTime parseISOTimestamp(Row row, size_t idx) {
|
||||||
|
return SysTime.fromISOExtString(
|
||||||
|
row.peek!(string, PeekMode.slice)(idx),
|
||||||
|
UTC()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a set of bytes from a result row.
|
||||||
|
* Params:
|
||||||
|
* row = The row to read from.
|
||||||
|
* idx = The column index in the row to read.
|
||||||
|
* Returns: The blob data that was read.
|
||||||
|
*/
|
||||||
|
immutable(ubyte[]) parseBlob(Row row, size_t idx) {
|
||||||
|
return row.peek!(ubyte[], PeekMode.slice)(idx).idup;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue