test(analytics): cover net category semantics
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@family-budget/backend",
|
"name": "@family-budget/backend",
|
||||||
"version": "0.6.0",
|
"version": "0.6.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tsx watch src/app.ts",
|
"dev": "tsx watch src/app.ts",
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
"start": "node dist/app.js",
|
"start": "node dist/app.js",
|
||||||
"migrate": "tsx src/db/migrate.ts",
|
"migrate": "tsx src/db/migrate.ts",
|
||||||
"migrate:prod": "node dist/db/migrate.js",
|
"migrate:prod": "node dist/db/migrate.js",
|
||||||
|
"test:analytics": "tsx src/services/analyticsSemantics.test.ts",
|
||||||
"test:llm": "tsx src/scripts/testLlm.ts"
|
"test:llm": "tsx src/scripts/testLlm.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { pool } from '../db/pool';
|
import { pool } from '../db/pool';
|
||||||
|
import { effectiveAmountCase } from './analyticsSemantics';
|
||||||
import type {
|
import type {
|
||||||
AnalyticsSummaryResponse,
|
AnalyticsSummaryResponse,
|
||||||
TopCategory,
|
TopCategory,
|
||||||
@@ -14,14 +15,7 @@ interface BaseParams {
|
|||||||
onlyConfirmed?: boolean;
|
onlyConfirmed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const effectiveAmount = `
|
const effectiveAmount = effectiveAmountCase('t');
|
||||||
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 {
|
function analyticsTransactions(where: string): string {
|
||||||
return `
|
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