module transaction.data_impl_sqlite; import handy_http_primitives : Optional; import std.datetime; import std.typecons; import d2sqlite3; import transaction.model; import transaction.data; import transaction.dto; import util.sqlite; import util.money; import util.pagination; import util.data; class SqliteTransactionVendorRepository : TransactionVendorRepository { private Database db; this(Database db) { this.db = db; } Optional!TransactionVendor findById(ulong id) { return util.sqlite.findById(db, "transaction_vendor", &parseVendor, id); } TransactionVendor[] findAll() { return util.sqlite.findAll( db, "SELECT * FROM transaction_vendor ORDER BY name ASC", &parseVendor ); } bool existsByName(string name) { return util.sqlite.exists(db, "SELECT id FROM transaction_vendor WHERE name = ?", name); } bool existsById(ulong id) { return util.sqlite.exists(db, "SELECT id FROM transaction_vendor WHERE id = ?", id); } TransactionVendor insert(string name, string description) { util.sqlite.update( db, "INSERT INTO transaction_vendor (name, description) VALUES (?, ?)", name, description ); ulong id = db.lastInsertRowid(); return findById(id).orElseThrow(); } void deleteById(ulong id) { util.sqlite.deleteById(db, "transaction_vendor", id); } TransactionVendor updateById(ulong id, string name, string description) { util.sqlite.update( db, "UPDATE transaction_vendor SET name = ?, description = ? WHERE id = ?", name, description, id ); return findById(id).orElseThrow(); } private static TransactionVendor parseVendor(Row row) { return TransactionVendor( row.peek!ulong(0), row.peek!string(1), row.peek!string(2) ); } } class SqliteTransactionCategoryRepository : TransactionCategoryRepository { private Database db; this(Database db) { this.db = db; } Optional!TransactionCategory findById(ulong id) { return util.sqlite.findById(db, "transaction_category", &parseCategory, id); } bool existsById(ulong id) { return util.sqlite.exists(db, "SELECT id FROM transaction_category WHERE id = ?", id); } TransactionCategory[] findAll() { return util.sqlite.findAll( db, "SELECT * FROM transaction_category ORDER BY parent_id, name", &parseCategory ); } TransactionCategory[] findAllByParentId(Optional!ulong parentId) { if (parentId) { return util.sqlite.findAll( db, "SELECT * FROM transaction_category WHERE parent_id = ? ORDER BY name ASC", &parseCategory, parentId.value ); } return util.sqlite.findAll( db, "SELECT * FROM transaction_category WHERE parent_id IS NULL ORDER BY name ASC", &parseCategory ); } TransactionCategory insert(Optional!ulong parentId, string name, string description, string color) { util.sqlite.update( db, "INSERT INTO transaction_category (parent_id, name, description, color) VALUES (?, ?, ?, ?)", toNullable(parentId), name, description, color ); ulong id = db.lastInsertRowid(); return findById(id).orElseThrow(); } void deleteById(ulong id) { util.sqlite.deleteById(db, "transaction_category", id); } TransactionCategory updateById(ulong id, string name, string description, string color) { util.sqlite.update( db, "UPDATE transaction_category SET name = ?, description = ?, color = ? WHERE id = ?", name, description, color, id ); return findById(id).orElseThrow(); } private static TransactionCategory parseCategory(Row row) { import std.typecons; return TransactionCategory( row.peek!ulong(0), toOptional(row.peek!(Nullable!ulong)(1)), row.peek!string(2), row.peek!string(3), row.peek!string(4) ); } } class SqliteTransactionTagRepository : TransactionTagRepository { private Database db; this(Database db) { this.db = db; } string[] findAllByTransactionId(ulong transactionId) { return util.sqlite.findAll( db, "SELECT tag FROM transaction_tag WHERE transaction_id = ? ORDER BY tag", r => r.peek!string(0), transactionId ); } void updateTags(ulong transactionId, in string[] tags) { util.sqlite.update( db, "DELETE FROM transaction_tag WHERE transaction_id = ?", transactionId ); foreach (tag; tags) { util.sqlite.update( db, "INSERT INTO transaction_tag (transaction_id, tag) VALUES (?, ?)", transactionId, tag ); } } string[] findAll() { return util.sqlite.findAll( db, "SELECT DISTINCT tag FROM transaction_tag ORDER BY tag", r => r.peek!string(0) ); } } class SqliteTransactionRepository : TransactionRepository { private const TABLE_NAME = "\"transaction\""; private Database db; this(Database db) { this.db = db; } Page!TransactionsListItem findAll(PageRequest pr) { const BASE_QUERY = import("sql/get_transactions.sql"); // TODO: Implement filtering or something! import std.array; const string countQuery = "SELECT COUNT(ID) FROM " ~ TABLE_NAME; auto sqlBuilder = appender!string; sqlBuilder ~= BASE_QUERY; sqlBuilder ~= " "; sqlBuilder ~= pr.toSql(); string query = sqlBuilder[]; TransactionsListItem[] results = util.sqlite.findAll(db, query, (row) { TransactionsListItem item; item.id = row.peek!ulong(0); item.timestamp = row.peek!string(1); item.addedAt = row.peek!string(2); item.amount = row.peek!ulong(3); item.currency = Currency.ofCode(row.peek!(string, PeekMode.slice)(4)); item.description = row.peek!string(5); Nullable!ulong vendorId = row.peek!(Nullable!ulong)(6); if (!vendorId.isNull) { string vendorName = row.peek!string(7); item.vendor = Optional!(TransactionsListItem.Vendor).of( TransactionsListItem.Vendor(vendorId.get, vendorName)); } Nullable!ulong categoryId = row.peek!(Nullable!ulong)(8); if (!categoryId.isNull) { string categoryName = row.peek!string(9); string categoryColor = row.peek!string(10); item.category = Optional!(TransactionsListItem.Category).of( TransactionsListItem.Category(categoryId.get, categoryName, categoryColor)); } Nullable!ulong creditedAccountId = row.peek!(Nullable!ulong)(11); if (!creditedAccountId.isNull) { ulong id = creditedAccountId.get; string name = row.peek!string(12); string type = row.peek!string(13); string suffix = row.peek!string(14); item.creditedAccount = Optional!(TransactionsListItem.Account).of( TransactionsListItem.Account(id, name, type, suffix)); } Nullable!ulong debitedAccountId = row.peek!(Nullable!ulong)(15); if (!debitedAccountId.isNull) { ulong id = debitedAccountId.get; string name = row.peek!string(16); string type = row.peek!string(17); string suffix = row.peek!string(18); item.debitedAccount = Optional!(TransactionsListItem.Account).of( TransactionsListItem.Account(id, name, type, suffix)); } string tagsStr = row.peek!string(19); if (tagsStr !is null && tagsStr.length > 0) { import std.string : split; item.tags = tagsStr.split(","); } else { item.tags = []; } return item; }); ulong totalCount = util.sqlite.count(db, countQuery); return Page!(TransactionsListItem).of(results, pr, totalCount); } Optional!TransactionDetail findById(ulong id) { Optional!TransactionDetail item = util.sqlite.findOne( db, import("sql/get_transaction.sql"), (row) { TransactionDetail item; item.id = row.peek!ulong(0); item.timestamp = row.peek!string(1); item.addedAt = row.peek!string(2); item.amount = row.peek!ulong(3); item.currency = Currency.ofCode(row.peek!(string, PeekMode.slice)(4)); item.description = row.peek!string(5); Nullable!ulong vendorId = row.peek!(Nullable!ulong)(6); if (!vendorId.isNull) { item.vendor = Optional!(TransactionDetail.Vendor).of( TransactionDetail.Vendor( vendorId.get, row.peek!string(7), row.peek!string(8) )).toNullable; } Nullable!ulong categoryId = row.peek!(Nullable!ulong)(9); if (!categoryId.isNull) { item.category = Optional!(TransactionDetail.Category).of( TransactionDetail.Category( categoryId.get, row.peek!(Nullable!ulong)(10), row.peek!string(11), row.peek!string(12), row.peek!string(13) )).toNullable; } Nullable!ulong creditedAccountId = row.peek!(Nullable!ulong)(14); if (!creditedAccountId.isNull) { item.creditedAccount = Optional!(TransactionDetail.Account).of( TransactionDetail.Account( creditedAccountId.get, row.peek!string(15), row.peek!string(16), row.peek!string(17) )).toNullable; } Nullable!ulong debitedAccountId = row.peek!(Nullable!ulong)(18); if (!debitedAccountId.isNull) { item.debitedAccount = Optional!(TransactionDetail.Account).of( TransactionDetail.Account( debitedAccountId.get, row.peek!string(19), row.peek!string(20), row.peek!string(21) )).toNullable; } string tagsStr = row.peek!string(22); if (tagsStr !is null && tagsStr.length > 0) { import std.string : split; item.tags = tagsStr.split(","); } else { item.tags = []; } return item; }, id ); if (item.isNull) return item; item.value.lineItems = util.sqlite.findAll( db, import("sql/get_line_items.sql"), (row) { TransactionDetail.LineItem li; li.idx = row.peek!uint(0); li.valuePerItem = row.peek!long(1); li.quantity = row.peek!ulong(2); li.description = row.peek!string(3); Nullable!ulong categoryId = row.peek!(Nullable!ulong)(4); if (!categoryId.isNull) { li.category = Optional!(TransactionDetail.Category).of( TransactionDetail.Category( categoryId.get, row.peek!(Nullable!ulong)(5), row.peek!string(6), row.peek!string(7), row.peek!string(8) )).toNullable; } return li; }, id ); return item; } TransactionDetail insert(in AddTransactionPayload data) { util.sqlite.update( db, import("sql/insert_transaction.sql"), data.timestamp, Clock.currTime(UTC()).toISOExtString(), data.amount, data.currencyCode, data.description, data.vendorId, data.categoryId ); ulong transactionId = db.lastInsertRowid(); insertLineItems(transactionId, data); return findById(transactionId).orElseThrow(); } TransactionDetail update(ulong transactionId, in AddTransactionPayload data) { util.sqlite.update( db, import("sql/update_transaction.sql"), data.timestamp, data.amount, data.currencyCode, data.description, data.vendorId, data.categoryId, transactionId ); // Re-write all line items: util.sqlite.update( db, "DELETE FROM transaction_line_item WHERE transaction_id = ?", transactionId ); insertLineItems(transactionId, data); return findById(transactionId).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), toOptional(row.peek!(Nullable!ulong)(6)), toOptional(row.peek!(Nullable!ulong)(7)) ); } private void insertLineItems(ulong transactionId, in AddTransactionPayload data) { foreach (size_t idx, lineItem; data.lineItems) { util.sqlite.update( db, import("sql/insert_line_item.sql"), transactionId, idx, lineItem.valuePerItem, lineItem.quantity, lineItem.description, lineItem.categoryId ); } } }