diff --git a/finnow-api/source/transaction/data_impl_sqlite.d b/finnow-api/source/transaction/data_impl_sqlite.d index 30b38f6..44e9293 100644 --- a/finnow-api/source/transaction/data_impl_sqlite.d +++ b/finnow-api/source/transaction/data_impl_sqlite.d @@ -223,9 +223,8 @@ class SqliteTransactionRepository : TransactionRepository { // 1. Get the total count of transactions that match the search filters. qb.select("COUNT (DISTINCT txn.id)"); - applyFilters(qb, request); + applyFilters(qb, request, new SqliteTransactionCategoryRepository(db)); string countQuery = qb.build(); - // writeln(countQuery); Statement countStmt = db.prepare(countQuery); qb.applyArgBindings(countStmt); auto countResult = countStmt.execute(); diff --git a/finnow-api/source/transaction/search_filters.d b/finnow-api/source/transaction/search_filters.d index 6883d25..1e9f735 100644 --- a/finnow-api/source/transaction/search_filters.d +++ b/finnow-api/source/transaction/search_filters.d @@ -14,17 +14,23 @@ import std.uri; import std.uni; import util.sqlite; +import transaction.data; /** * Applies a set of filters to a query builder for searching over transactions. * Params: * qb = The query builder to add WHERE clauses and argument bindings to. * request = The request to get filter options from. + * categoryRepo = Repository for fetching category data, which might be + * needed if the user is filtering by a parent category. */ -void applyFilters(ref QueryBuilder qb, in ServerHttpRequest request) { +void applyFilters( + ref QueryBuilder qb, + in ServerHttpRequest request, + TransactionCategoryRepository categoryRepo +) { applyPropertyInFilter!string(qb, request, "tags.tag", "tag"); applyPropertyInFilter!ulong(qb, request, "vendor.id", "vendor"); - applyPropertyInFilter!ulong(qb, request, "category.id", "category"); applyPropertyInFilter!string(qb, request, "txn.currency", "currency"); applyPropertyInFilter!ulong(qb, request, "account_credit.id", "credited-account"); applyPropertyInFilter!ulong(qb, request, "account_debit.id", "debited-account"); @@ -45,6 +51,25 @@ void applyFilters(ref QueryBuilder qb, in ServerHttpRequest request) { }); } + // Separate filter that handles the hierarchical category relationship, so + // if a parent category is filtered, all children are also included. + if (request.hasParam("category")) { + ulong[] categoryIds = request.getParamValues!ulong("category"); + auto app = appender!(ulong[]); + foreach (id; categoryIds) { + app ~= getAllPossibleCategoryIds(id, categoryRepo); + } + ulong[] allPossibleIds = app[]; + if (allPossibleIds.length > 0) { + qb.where("(" ~ "txn.category_id = ?".repeat(allPossibleIds.length).join(" OR ") ~ ")"); + qb.withArgBinding((ref stmt, ref idx) { + foreach (value; allPossibleIds) { + stmt.bind(idx++, value); + } + }); + } + } + if (request.hasParam("min-amount")) { ulong[] values = request.getParamValues!ulong("min-amount"); if (values.length > 0) { @@ -140,3 +165,13 @@ private T[] getParamValues(T = string)(in ServerHttpRequest request, string key) } return []; } + +private ulong[] getAllPossibleCategoryIds(ulong parentId, TransactionCategoryRepository categoryRepo) { + auto app = appender!(ulong[]); + app ~= parentId; + foreach (child; categoryRepo.findAllByParentId(Optional!ulong.of(parentId))) { + app ~= child.id; + app ~= getAllPossibleCategoryIds(child.id, categoryRepo); + } + return app[]; +}