fix: complete analytics edge cases

This commit is contained in:
2026-08-21 06:13:23 +03:00
parent d86624b9ef
commit 7154e8f2ea
9 changed files with 56 additions and 8 deletions

View File

@@ -8,7 +8,7 @@ const router = Router();
router.get(
'/summary',
asyncHandler(async (req, res) => {
const { from, to, accountId, onlyConfirmed } = req.query;
const { from, to, accountId, categoryId, onlyConfirmed } = req.query;
if (!from || !to) {
res.status(400).json({ error: 'BAD_REQUEST', message: 'from and to are required' });
return;
@@ -18,6 +18,7 @@ router.get(
from: from as string,
to: to as string,
accountId: accountId ? Number(accountId) : undefined,
categoryId: categoryId !== undefined ? Number(categoryId) : undefined,
onlyConfirmed: onlyConfirmed === 'true',
});
res.json(result);
@@ -27,7 +28,7 @@ router.get(
router.get(
'/by-category',
asyncHandler(async (req, res) => {
const { from, to, accountId, onlyConfirmed } = req.query;
const { from, to, accountId, categoryId, onlyConfirmed } = req.query;
if (!from || !to) {
res.status(400).json({ error: 'BAD_REQUEST', message: 'from and to are required' });
return;
@@ -37,6 +38,7 @@ router.get(
from: from as string,
to: to as string,
accountId: accountId ? Number(accountId) : undefined,
categoryId: categoryId !== undefined ? Number(categoryId) : undefined,
onlyConfirmed: onlyConfirmed === 'true',
});
res.json(result);

View File

@@ -28,15 +28,26 @@ async function testQueries(): Promise<void> {
);
await insert(otherAccountId, '2026-07-01T12:00:00+03:00', -99_000, categoryId.expense, 'analytics-test-6');
await insert(accountId, '2026-08-01T12:00:00+03:00', -7_000, categoryId.expense, 'analytics-test-7');
await client.query(
"INSERT INTO transactions (account_id, operation_at, amount_signed, commission, description, direction, fingerprint, category_id, is_category_confirmed) VALUES ($1, '2026-07-07T12:00:00+03:00', -2_500, 0, 'uncategorized', 'expense', 'analytics-test-uncategorized', NULL, FALSE)",
[accountId],
);
const params = { from: '2026-07-01', to: '2026-07-31', accountId, onlyConfirmed: true };
const summary = await getSummary(params, client);
assert.deepEqual({ expense: summary.totalExpense, income: summary.totalIncome, net: summary.net, transferOut: summary.transferOutflow, interest: summary.interestIncome }, { expense: 6_000, income: 15_000, net: 9_000, transferOut: 3_000, interest: 1_500 });
const categorySummary = await getSummary({ ...params, categoryId: categoryId.expense }, client);
assert.deepEqual({ expense: categorySummary.totalExpense, income: categorySummary.totalIncome }, { expense: 6_000, income: 0 });
const uncategorized = await getSummary({ ...params, categoryId: 0, onlyConfirmed: false }, client);
assert.equal(uncategorized.totalExpense, 2_500);
assert.equal((await getSummary({ ...params, from: '2026-08-01', to: '2026-08-31' }, client)).totalExpense, 7_000);
assert.deepEqual((await getByCategory(params, client)).map((item) => item.amount), [6_000]);
const timeseries = await getTimeseries({ ...params, granularity: 'month' }, client);
assert.equal(timeseries[0].expenseAmount, 6_000);
assert.equal(timeseries[0].incomeAmount, 15_000);
const partial = await getTimeseries({ ...params, from: '2026-07-02', to: '2026-07-03', granularity: 'month' }, client);
assert.equal(partial[0].expenseAmount, 0);
assert.equal(partial[0].incomeAmount, 20_000);
} finally {
await client.query('ROLLBACK');
client.release();

View File

@@ -14,6 +14,7 @@ interface BaseParams {
from: string;
to: string;
accountId?: number;
categoryId?: number;
onlyConfirmed?: boolean;
}
@@ -61,6 +62,13 @@ function buildBaseConditions(
values.push(params.accountId);
idx++;
}
if (params.categoryId != null) {
conditions.push(params.categoryId === 0 ? 't.category_id IS NULL' : `t.category_id = $${idx}`);
if (params.categoryId !== 0) {
values.push(params.categoryId);
idx++;
}
}
if (params.onlyConfirmed) {
conditions.push('t.is_category_confirmed = TRUE');
}
@@ -196,6 +204,8 @@ export async function getTimeseries(
}
const txConditions: string[] = [
't.operation_at::date >= $1::date',
't.operation_at::date <= $2::date',
't.operation_at::date >= p.period_start',
't.operation_at::date <= p.period_end',
];
@@ -207,8 +217,11 @@ export async function getTimeseries(
values.push(params.accountId);
}
if (params.categoryId != null) {
txConditions.push(`t.category_id = $${idx++}`);
values.push(params.categoryId);
if (params.categoryId === 0) txConditions.push('t.category_id IS NULL');
else {
txConditions.push(`t.category_id = $${idx++}`);
values.push(params.categoryId);
}
}
if (params.onlyConfirmed) {
txConditions.push('t.is_category_confirmed = TRUE');