test(analytics): cover net category semantics
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@family-budget/backend",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/app.ts",
|
||||
@@ -8,6 +8,7 @@
|
||||
"start": "node dist/app.js",
|
||||
"migrate": "tsx src/db/migrate.ts",
|
||||
"migrate:prod": "node dist/db/migrate.js",
|
||||
"test:analytics": "tsx src/services/analyticsSemantics.test.ts",
|
||||
"test:llm": "tsx src/scripts/testLlm.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { pool } from '../db/pool';
|
||||
import { effectiveAmountCase } from './analyticsSemantics';
|
||||
import type {
|
||||
AnalyticsSummaryResponse,
|
||||
TopCategory,
|
||||
@@ -14,14 +15,7 @@ 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`;
|
||||
const effectiveAmount = effectiveAmountCase('t');
|
||||
|
||||
function analyticsTransactions(where: string): string {
|
||||
return `
|
||||
|
||||
25
backend/src/services/analyticsSemantics.test.ts
Normal file
25
backend/src/services/analyticsSemantics.test.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { getEffectiveAmount, getNetAmount } from './analyticsSemantics';
|
||||
|
||||
assert.equal(getNetAmount('expense', [
|
||||
{ amountSigned: -10_000, commission: 0 },
|
||||
{ amountSigned: 4_000, commission: 0 },
|
||||
]), 6_000, 'возврат уменьшает расход своей категории');
|
||||
|
||||
assert.equal(getNetAmount('income', [
|
||||
{ amountSigned: 20_000, commission: 0 },
|
||||
{ amountSigned: -5_000, commission: 0 },
|
||||
]), 15_000, 'корректировка уменьшает доход');
|
||||
|
||||
assert.equal(getNetAmount('transfer', [
|
||||
{ amountSigned: -10_000, commission: 0 },
|
||||
{ amountSigned: 10_000, commission: 0 },
|
||||
]), 0, 'перевод не является доходом или расходом');
|
||||
|
||||
assert.equal(
|
||||
getEffectiveAmount({ amountSigned: 0, commission: 500, description: 'Зачисление кэшбэка' }),
|
||||
500,
|
||||
'кэшбэк учитывается как поступление',
|
||||
);
|
||||
|
||||
console.log('analytics semantics: OK');
|
||||
32
backend/src/services/analyticsSemantics.ts
Normal file
32
backend/src/services/analyticsSemantics.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
export type AnalyticType = 'expense' | 'income' | 'transfer';
|
||||
|
||||
interface AmountInput {
|
||||
amountSigned: number;
|
||||
commission: number;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export function getEffectiveAmount({ amountSigned, commission, description = '' }: AmountInput): number {
|
||||
if (amountSigned === 0 && commission > 0 && description.toLowerCase().includes('зачисление')) {
|
||||
return commission;
|
||||
}
|
||||
return amountSigned < 0 ? amountSigned - commission : amountSigned + commission;
|
||||
}
|
||||
|
||||
export function getNetAmount(type: AnalyticType, amounts: AmountInput[]): number {
|
||||
const amount = amounts.reduce((sum, item) => sum + getEffectiveAmount(item), 0);
|
||||
if (type === 'expense') return Math.max(-amount, 0);
|
||||
if (type === 'income') return Math.max(amount, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function effectiveAmountCase(alias: string): string {
|
||||
return `
|
||||
CASE
|
||||
WHEN ${alias}.amount_signed = 0 AND ${alias}.commission > 0
|
||||
AND ${alias}.description ILIKE '%зачисление%'
|
||||
THEN ${alias}.commission
|
||||
WHEN ${alias}.amount_signed < 0 THEN ${alias}.amount_signed - ${alias}.commission
|
||||
ELSE ${alias}.amount_signed + ${alias}.commission
|
||||
END`;
|
||||
}
|
||||
Reference in New Issue
Block a user