feat(analytics): add net cashflow reporting
This commit is contained in:
@@ -14,6 +14,31 @@ interface BaseParams {
|
||||
onlyConfirmed?: boolean;
|
||||
}
|
||||
|
||||
const effectiveAmount = `
|
||||
CASE
|
||||
WHEN t.amount_signed = 0 AND t.commission > 0
|
||||
AND t.description ILIKE '%зачисление%'
|
||||
THEN t.commission
|
||||
WHEN t.amount_signed < 0 THEN t.amount_signed - t.commission
|
||||
ELSE t.amount_signed + t.commission
|
||||
END`;
|
||||
|
||||
function analyticsTransactions(where: string): string {
|
||||
return `
|
||||
WITH analytics_transactions AS (
|
||||
SELECT
|
||||
t.id,
|
||||
t.operation_at,
|
||||
COALESCE(c.type, t.direction) AS analytic_type,
|
||||
COALESCE(t.category_id, 0) AS category_id,
|
||||
COALESCE(c.name, 'Без категории') AS category_name,
|
||||
${effectiveAmount} AS effective_amount
|
||||
FROM transactions t
|
||||
LEFT JOIN categories c ON c.id = t.category_id
|
||||
${where}
|
||||
)`;
|
||||
}
|
||||
|
||||
function buildBaseConditions(
|
||||
params: BaseParams,
|
||||
startIdx: number,
|
||||
@@ -47,91 +72,37 @@ export async function getSummary(params: BaseParams): Promise<AnalyticsSummaryRe
|
||||
const where = 'WHERE ' + conditions.join(' AND ');
|
||||
|
||||
const totalsResult = await pool.query(
|
||||
`WITH investment_ref AS (
|
||||
SELECT (
|
||||
SELECT id
|
||||
FROM categories
|
||||
WHERE name = 'Инвестиции' AND type = 'transfer'
|
||||
ORDER BY id ASC
|
||||
LIMIT 1
|
||||
) AS investment_category_id
|
||||
`${analyticsTransactions(where)},
|
||||
category_net AS (
|
||||
SELECT category_id, category_name, analytic_type, SUM(effective_amount)::bigint AS amount
|
||||
FROM analytics_transactions
|
||||
GROUP BY category_id, category_name, analytic_type
|
||||
)
|
||||
SELECT
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
WHEN (t.direction = 'expense' OR (t.direction = 'transfer' AND t.amount_signed < 0))
|
||||
AND (
|
||||
ir.investment_category_id IS NULL
|
||||
OR t.category_id IS DISTINCT FROM ir.investment_category_id
|
||||
)
|
||||
THEN ABS(t.amount_signed) + t.commission
|
||||
ELSE 0
|
||||
END
|
||||
), 0)::bigint AS total_expense,
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
WHEN t.direction = 'income'
|
||||
AND (
|
||||
ir.investment_category_id IS NULL
|
||||
OR t.category_id IS DISTINCT FROM ir.investment_category_id
|
||||
)
|
||||
THEN t.amount_signed + t.commission
|
||||
ELSE 0
|
||||
END
|
||||
), 0)::bigint AS total_income,
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
WHEN t.amount_signed < 0
|
||||
AND ir.investment_category_id IS NOT NULL
|
||||
AND t.category_id = ir.investment_category_id
|
||||
THEN ABS(t.amount_signed) + t.commission
|
||||
ELSE 0
|
||||
END
|
||||
), 0)::bigint AS investment_outflow,
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
WHEN t.amount_signed > 0
|
||||
AND ir.investment_category_id IS NOT NULL
|
||||
AND t.category_id = ir.investment_category_id
|
||||
THEN t.amount_signed + t.commission
|
||||
ELSE 0
|
||||
END
|
||||
), 0)::bigint AS investment_income_excluded
|
||||
FROM transactions t
|
||||
CROSS JOIN investment_ref ir
|
||||
${where}`,
|
||||
COALESCE(SUM(GREATEST(-amount, 0)) FILTER (WHERE analytic_type = 'expense'), 0)::bigint AS total_expense,
|
||||
COALESCE(SUM(GREATEST(amount, 0)) FILTER (WHERE analytic_type = 'income'), 0)::bigint AS total_income,
|
||||
COALESCE((SELECT SUM(GREATEST(effective_amount, 0)) FROM analytics_transactions), 0)::bigint AS cash_inflow,
|
||||
COALESCE((SELECT SUM(GREATEST(-effective_amount, 0)) FROM analytics_transactions), 0)::bigint AS cash_outflow,
|
||||
COALESCE((SELECT SUM(GREATEST(effective_amount, 0)) FROM analytics_transactions WHERE analytic_type = 'transfer'), 0)::bigint AS transfer_inflow,
|
||||
COALESCE((SELECT SUM(GREATEST(-effective_amount, 0)) FROM analytics_transactions WHERE analytic_type = 'transfer'), 0)::bigint AS transfer_outflow
|
||||
FROM category_net`,
|
||||
values,
|
||||
);
|
||||
|
||||
const totalExpense = Number(totalsResult.rows[0].total_expense);
|
||||
const totalIncome = Number(totalsResult.rows[0].total_income);
|
||||
const investmentOutflow = Number(totalsResult.rows[0].investment_outflow);
|
||||
const investmentIncomeExcluded = Number(totalsResult.rows[0].investment_income_excluded);
|
||||
const cashInflow = Number(totalsResult.rows[0].cash_inflow);
|
||||
const cashOutflow = Number(totalsResult.rows[0].cash_outflow);
|
||||
const transferInflow = Number(totalsResult.rows[0].transfer_inflow);
|
||||
const transferOutflow = Number(totalsResult.rows[0].transfer_outflow);
|
||||
|
||||
const topResult = await pool.query(
|
||||
`WITH investment_ref AS (
|
||||
SELECT (
|
||||
SELECT id
|
||||
FROM categories
|
||||
WHERE name = 'Инвестиции' AND type = 'transfer'
|
||||
ORDER BY id ASC
|
||||
LIMIT 1
|
||||
) AS investment_category_id
|
||||
)
|
||||
SELECT
|
||||
COALESCE(t.category_id, 0)::bigint AS category_id,
|
||||
COALESCE(c.name, 'Без категории') AS category_name,
|
||||
SUM(ABS(t.amount_signed) + t.commission)::bigint AS amount
|
||||
FROM transactions t
|
||||
CROSS JOIN investment_ref ir
|
||||
LEFT JOIN categories c ON c.id = t.category_id
|
||||
${where}
|
||||
AND (t.direction = 'expense' OR (t.direction = 'transfer' AND t.amount_signed < 0))
|
||||
AND (
|
||||
ir.investment_category_id IS NULL
|
||||
OR t.category_id IS DISTINCT FROM ir.investment_category_id
|
||||
)
|
||||
GROUP BY COALESCE(t.category_id, 0), COALESCE(c.name, 'Без категории')
|
||||
`${analyticsTransactions(where)}
|
||||
SELECT category_id::bigint, category_name, GREATEST(-SUM(effective_amount), 0)::bigint AS amount
|
||||
FROM analytics_transactions
|
||||
WHERE analytic_type = 'expense'
|
||||
GROUP BY category_id, category_name
|
||||
HAVING SUM(effective_amount) < 0
|
||||
ORDER BY amount DESC
|
||||
LIMIT 5`,
|
||||
values,
|
||||
@@ -148,8 +119,11 @@ export async function getSummary(params: BaseParams): Promise<AnalyticsSummaryRe
|
||||
totalExpense,
|
||||
totalIncome,
|
||||
net: totalIncome - totalExpense,
|
||||
investmentOutflow,
|
||||
investmentIncomeExcluded,
|
||||
cashInflow,
|
||||
cashOutflow,
|
||||
transferInflow,
|
||||
transferOutflow,
|
||||
cashNet: cashInflow - cashOutflow,
|
||||
topCategories,
|
||||
};
|
||||
}
|
||||
@@ -158,58 +132,23 @@ export async function getByCategory(params: BaseParams): Promise<ByCategoryItem[
|
||||
const { conditions, values } = buildBaseConditions(params, 1);
|
||||
const where = 'WHERE ' + conditions.join(' AND ');
|
||||
|
||||
const totalResult = await pool.query(
|
||||
`WITH investment_ref AS (
|
||||
SELECT (
|
||||
SELECT id
|
||||
FROM categories
|
||||
WHERE name = 'Инвестиции' AND type = 'transfer'
|
||||
ORDER BY id ASC
|
||||
LIMIT 1
|
||||
) AS investment_category_id
|
||||
)
|
||||
SELECT COALESCE(SUM(ABS(t.amount_signed) + t.commission), 0)::bigint AS total
|
||||
FROM transactions t
|
||||
CROSS JOIN investment_ref ir
|
||||
${where}
|
||||
AND (t.direction = 'expense' OR (t.direction = 'transfer' AND t.amount_signed < 0))
|
||||
AND (
|
||||
ir.investment_category_id IS NULL
|
||||
OR t.category_id IS DISTINCT FROM ir.investment_category_id
|
||||
)`,
|
||||
values,
|
||||
);
|
||||
const total = Number(totalResult.rows[0].total);
|
||||
|
||||
const { rows } = await pool.query(
|
||||
`WITH investment_ref AS (
|
||||
SELECT (
|
||||
SELECT id
|
||||
FROM categories
|
||||
WHERE name = 'Инвестиции' AND type = 'transfer'
|
||||
ORDER BY id ASC
|
||||
LIMIT 1
|
||||
) AS investment_category_id
|
||||
)
|
||||
`${analyticsTransactions(where)}
|
||||
SELECT
|
||||
COALESCE(t.category_id, 0)::bigint AS category_id,
|
||||
COALESCE(c.name, 'Без категории') AS category_name,
|
||||
SUM(ABS(t.amount_signed) + t.commission)::bigint AS amount,
|
||||
category_id::bigint,
|
||||
category_name,
|
||||
GREATEST(-SUM(effective_amount), 0)::bigint AS amount,
|
||||
COUNT(*)::int AS tx_count
|
||||
FROM transactions t
|
||||
CROSS JOIN investment_ref ir
|
||||
LEFT JOIN categories c ON c.id = t.category_id
|
||||
${where}
|
||||
AND (t.direction = 'expense' OR (t.direction = 'transfer' AND t.amount_signed < 0))
|
||||
AND (
|
||||
ir.investment_category_id IS NULL
|
||||
OR t.category_id IS DISTINCT FROM ir.investment_category_id
|
||||
)
|
||||
GROUP BY COALESCE(t.category_id, 0), COALESCE(c.name, 'Без категории')
|
||||
FROM analytics_transactions
|
||||
WHERE analytic_type = 'expense'
|
||||
GROUP BY category_id, category_name
|
||||
HAVING SUM(effective_amount) < 0
|
||||
ORDER BY amount DESC`,
|
||||
values,
|
||||
);
|
||||
|
||||
const total = rows.reduce((sum, row) => sum + Number(row.amount), 0);
|
||||
|
||||
return rows.map((r) => ({
|
||||
categoryId: r.category_id != null ? Number(r.category_id) : 0,
|
||||
categoryName: r.category_name ?? 'Без категории',
|
||||
@@ -272,54 +211,31 @@ export async function getTimeseries(
|
||||
${periodEndExpr} AS period_end
|
||||
FROM generate_series(${truncExpr}, $2::date, '${intervalStr}'::interval) gs
|
||||
),
|
||||
investment_ref AS (
|
||||
SELECT (
|
||||
SELECT id
|
||||
FROM categories
|
||||
WHERE name = 'Инвестиции' AND type = 'transfer'
|
||||
ORDER BY id ASC
|
||||
LIMIT 1
|
||||
) AS investment_category_id
|
||||
period_transactions AS (
|
||||
SELECT
|
||||
p.period_start,
|
||||
p.period_end,
|
||||
COALESCE(c.type, t.direction) AS analytic_type,
|
||||
COALESCE(t.category_id, 0) AS category_id,
|
||||
${effectiveAmount} AS effective_amount
|
||||
FROM periods p
|
||||
LEFT JOIN transactions t ON ${txWhere}
|
||||
LEFT JOIN categories c ON c.id = t.category_id
|
||||
),
|
||||
period_category_net AS (
|
||||
SELECT period_start, period_end, analytic_type, category_id, SUM(effective_amount)::bigint AS amount
|
||||
FROM period_transactions
|
||||
GROUP BY period_start, period_end, analytic_type, category_id
|
||||
)
|
||||
SELECT
|
||||
p.period_start,
|
||||
p.period_end,
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
WHEN (t.direction = 'expense' OR (t.direction = 'transfer' AND t.amount_signed < 0))
|
||||
AND (
|
||||
ir.investment_category_id IS NULL
|
||||
OR t.category_id IS DISTINCT FROM ir.investment_category_id
|
||||
)
|
||||
THEN ABS(t.amount_signed) + t.commission
|
||||
ELSE 0
|
||||
END
|
||||
), 0)::bigint AS expense_amount,
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
WHEN t.direction = 'income'
|
||||
AND (
|
||||
ir.investment_category_id IS NULL
|
||||
OR t.category_id IS DISTINCT FROM ir.investment_category_id
|
||||
)
|
||||
THEN t.amount_signed + t.commission
|
||||
ELSE 0
|
||||
END
|
||||
), 0)::bigint AS income_amount,
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
WHEN t.amount_signed < 0
|
||||
AND ir.investment_category_id IS NOT NULL
|
||||
AND t.category_id = ir.investment_category_id
|
||||
THEN ABS(t.amount_signed) + t.commission
|
||||
ELSE 0
|
||||
END
|
||||
), 0)::bigint AS investment_outflow
|
||||
FROM periods p
|
||||
CROSS JOIN investment_ref ir
|
||||
LEFT JOIN transactions t ON ${txWhere}
|
||||
GROUP BY p.period_start, p.period_end
|
||||
ORDER BY p.period_start`,
|
||||
period_start,
|
||||
period_end,
|
||||
COALESCE(SUM(GREATEST(-amount, 0)) FILTER (WHERE analytic_type = 'expense'), 0)::bigint AS expense_amount,
|
||||
COALESCE(SUM(GREATEST(amount, 0)) FILTER (WHERE analytic_type = 'income'), 0)::bigint AS income_amount,
|
||||
COALESCE(SUM(GREATEST(-amount, 0)) FILTER (WHERE analytic_type = 'transfer'), 0)::bigint AS transfer_outflow
|
||||
FROM period_category_net
|
||||
GROUP BY period_start, period_end
|
||||
ORDER BY period_start`,
|
||||
values,
|
||||
);
|
||||
|
||||
@@ -328,6 +244,6 @@ export async function getTimeseries(
|
||||
periodEnd: r.period_end.toISOString().slice(0, 10),
|
||||
expenseAmount: Number(r.expense_amount),
|
||||
incomeAmount: Number(r.income_amount),
|
||||
investmentOutflow: Number(r.investment_outflow),
|
||||
transferOutflow: Number(r.transfer_outflow),
|
||||
}));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user