Transfers move money between your own accounts; at 433 of 3,996 rows (~11%) they crowd out the rows that represent actual spending. getTransactions gains `exclude_categories`, opt-in per caller and deliberately not defaulted in queries.ts: the rules preview and the bulk rule-apply path both read candidate rows through getTransactions, and a default exclusion there would silently shrink what a rule can see and reach — invisibly, since a rule that matches nothing looks the same as a rule with nothing to do. Two behaviours the filter needs, both tested: - An explicit category pick beats the exclusion. Selecting "Transfers" while the default is on subtracts it from the hidden list instead of returning zero rows and reading as "you have no transfers". - COALESCE the effective category to '' before `<> ALL`. NULL <> ALL(...) is NULL, not true, so an uncategorised row would disappear from a filter that never named its category — the trap EXCLUDE_NON_SPEND already documents. The default is off when the view is scoped to a statement: that is a reconciliation view, the row count has to match the statement, and a credit-card payment is the row you went there to check.
This commit is contained in:
@@ -16,6 +16,7 @@ interface TransactionFilters {
|
||||
from?: string;
|
||||
to?: string;
|
||||
categories?: string[];
|
||||
exclude_categories?: string[];
|
||||
bank_names?: string[];
|
||||
tag_ids?: string[];
|
||||
transaction_types?: string[];
|
||||
|
||||
@@ -117,6 +117,13 @@ interface TransactionFilters {
|
||||
from?: string;
|
||||
to?: string;
|
||||
categories?: string[];
|
||||
/**
|
||||
* Categories to hide. Opt-in per caller and never defaulted here — the rules
|
||||
* preview and the bulk rule-apply path both go through getTransactions, and a
|
||||
* default exclusion would silently shrink what a rule can see and reach. The
|
||||
* transactions view sets this; nothing else does.
|
||||
*/
|
||||
exclude_categories?: string[];
|
||||
bank_names?: string[];
|
||||
tag_ids?: string[];
|
||||
transaction_types?: string[];
|
||||
@@ -152,6 +159,19 @@ export async function getTransactions(ownerId: number, filters: TransactionFilte
|
||||
conditions.push(`COALESCE(o.category_override, t.category) = ANY($${paramIdx++}::text[])`);
|
||||
params.push(filters.categories);
|
||||
}
|
||||
if (filters.exclude_categories?.length) {
|
||||
// Picking a category explicitly beats hiding it. Without this, selecting
|
||||
// "Transfers" while the hide-transfers default is on returns zero rows and
|
||||
// reads as "you have no transfers".
|
||||
const hidden = filters.exclude_categories.filter((c) => !filters.categories?.includes(c));
|
||||
if (hidden.length) {
|
||||
// COALESCE to '' rather than leaving it NULL: `NULL <> ALL(...)` is NULL,
|
||||
// not true, so an uncategorised row would be filtered out by a hide rule
|
||||
// that never named it. Same trap EXCLUDE_NON_SPEND documents.
|
||||
conditions.push(`COALESCE(o.category_override, t.category, '') <> ALL($${paramIdx++}::text[])`);
|
||||
params.push(hidden);
|
||||
}
|
||||
}
|
||||
if (filters.bank_names?.length) {
|
||||
// "Manual" and "Gift Card" are not banks — they are the two shapes a
|
||||
// statement-less row can take, and bankLabel() decides which. The filter
|
||||
|
||||
Reference in New Issue
Block a user