module account.data_impl_sqlite; import std.datetime; import d2sqlite3; import handy_httpd.components.optional; import account.data; import account.model; import history.model; import util.sqlite; import util.money; class SqliteAccountRepository : AccountRepository { private Database db; this(Database db) { this.db = db; } Optional!Account findById(ulong id) { return findOne(db, "SELECT * FROM account WHERE id = ?", &parseAccount, id); } Account insert(AccountType type, string numberSuffix, string name, Currency currency, string description) { util.sqlite.update( db, "INSERT INTO account (created_at, type, number_suffix, name, currency, description) VALUES (?, ?, ?, ?, ?, ?)", Clock.currTime(UTC()).toISOExtString(), type.id, numberSuffix, name, currency.code, description ); ulong accountId = db.lastInsertRowid(); return findById(accountId).orElseThrow("Couldn't find account!"); } void setArchived(ulong id, bool archived) { util.sqlite.update( db, "UPDATE account SET archived = ? WHERE id = ?", archived ? 1 : 0, id ); } Account update(ulong id, in Account newData) { return doTransaction(db, () { Account oldAccount = this.findById(id).orElseThrow("Account doesn't exist."); bool typeDiff = oldAccount.type != newData.type; bool numberSuffixDiff = oldAccount.numberSuffix != newData.numberSuffix; bool nameDiff = oldAccount.name != newData.name; bool currencyDiff = oldAccount.currency != newData.currency; bool descriptionDiff = oldAccount.description != newData.description; util.sqlite.update( db, q"SQL UPDATE account SET type = ?, number_suffix = ?, name = ?, currency = ?, description = ? WHERE id = ? SQL", newData.type.id, newData.numberSuffix, newData.name, newData.currency.code, newData.description ); return this.findById(id).orElseThrow("Account doesn't exist"); }); } void deleteById(ulong id) { doTransaction(db, () { util.sqlite.update( db, "DELETE FROM history WHERE id IN ( SELECT history_id FROM account_history WHERE account_id = ? )", id ); util.sqlite.update(db, "DELETE FROM account WHERE id = ?", id); }); } Account[] findAll() { return util.sqlite.findAll(db, "SELECT * FROM account", &parseAccount); } AccountCreditCardProperties getCreditCardProperties(ulong id) { Account account = findById(id).orElseThrow("Account doesn't exist."); if (account.type != AccountTypes.CREDIT_CARD) throw new Exception("Account is not credit card."); auto optionalProps = findOne( db, "SELECT * FROM account_credit_card_properties WHERE account_id = ?", &parseCreditCardProperties, id ); if (!optionalProps.isNull) return optionalProps.value; // No properties exist, so set them and return the new data. const props = AccountCreditCardProperties(account.id, -1); util.sqlite.update( db, "INSERT INTO account_credit_card_properties (account_id, credit_limit) VALUES (?, ?)", props.account_id, props.creditLimit ); return props; } void setCreditCardProperties(ulong id, in AccountCreditCardProperties props) { bool hasProps = exists(db, "SELECT * FROM account_credit_card_properties WHERE account_id = ?", id); if (hasProps) { util.sqlite.update( db, "UPDATE account_credit_card_properties SET credit_limit = ? WHERE account_id = ?", props.creditLimit, id ); } else { util.sqlite.update( db, "INSERT INTO account_credit_card_properties (account_id, credit_limit) VALUES (?, ?)", id, props.creditLimit ); } } History getHistory(ulong id) { if (!exists(db, "SELECT id FROM account WHERE id = ?", id)) { throw new Exception("Account doesn't exist."); } Optional!History history = findOne( db, q"SQL SELECT * FROM history LEFT JOIN account_history ah ON ah.history_id = history.id WHERE ah.account_id = ? SQL", r => History(r.peek!ulong(0)), id ); if (!history.empty) { return history.value; } // No history exists yet, so add it. ulong historyId = doTransaction(db, () { util.sqlite.update(db, "INSERT INTO history DEFAULT VALUES"); ulong historyId = db.lastInsertRowid(); util.sqlite.update(db, "INSERT INTO account_history (account_id, history_id) VALUES (?, ?)", id, historyId); return historyId; }); return History(historyId); } static Account parseAccount(Row row) { return Account( row.peek!ulong(0), SysTime.fromISOExtString(row.peek!string(1)), row.peek!bool(2), AccountType.fromId(row.peek!string(3)), row.peek!string(4), row.peek!string(5), Currency.ofCode(row.peek!string(6)), row.peek!string(7) ); } static AccountCreditCardProperties parseCreditCardProperties(Row row) { import std.typecons : Nullable; ulong accountId = row.peek!ulong(0); Nullable!ulong creditLimit = row.peek!ulong(1); return AccountCreditCardProperties( accountId, creditLimit.isNull ? -1 : creditLimit.get() ); } } 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)) ); } }