finnow/finnow-api/source/transaction/data_impl_sqlite.d

424 lines
15 KiB
D

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
);
}
}
}