test: cover portfolio import idempotency

This commit is contained in:
2026-08-20 15:02:47 +03:00
parent e0da7e86ad
commit 6db1312fb1
4 changed files with 39 additions and 3 deletions

View File

@@ -1,5 +1,11 @@
# Changelog
## [Backend 0.8.2] - 2026-08-20
### Added
- Added a database integration test for repeated portfolio imports and trade deduplication.
## [Backend 0.8.1] - 2026-08-20
### Fixed

View File

@@ -1,6 +1,6 @@
{
"name": "@family-budget/backend",
"version": "0.8.1",
"version": "0.8.2",
"private": true,
"scripts": {
"dev": "tsx watch src/app.ts",
@@ -10,6 +10,7 @@
"migrate:prod": "node dist/db/migrate.js",
"test:analytics": "tsx src/services/analyticsSemantics.test.ts",
"test:portfolio": "tsx src/services/portfolio.test.ts",
"test:portfolio:db": "NODE_ENV=test tsx src/services/portfolio.integration.test.ts",
"test:analytics:db": "NODE_ENV=test tsx src/services/analytics.integration.test.ts",
"test:import:db": "NODE_ENV=test tsx src/services/import.integration.test.ts",
"test:llm": "tsx src/scripts/testLlm.ts"

View File

@@ -0,0 +1,29 @@
import assert from 'node:assert/strict';
import { pool } from '../db/pool';
import { importPortfolio } from './portfolio';
const payload = {
schemaVersion: 'broker-portfolio-1.0', bank: 'PORTFOLIO_TEST', accountNumber: 'portfolio-test',
reportPeriod: { from: '2026-08-01', to: '2026-08-20' }, reportedAt: '2026-08-20T00:00:00+03:00',
positions: [{ instrument: 'Test Bond', isin: 'RU0000000001', quantity: '1.000', price: '100.123456', valuation: '100.123456' }],
trades: [{ instrument: 'Test Bond', isin: 'RU0000000001', concludedAt: '2026-08-10T10:00:00+03:00', side: 'Покупка', quantity: '1.000', settlementAmount: '100.123456' }],
};
async function run(): Promise<void> {
try {
const first = await importPortfolio(payload);
const second = await importPortfolio(payload);
assert.equal(first.importedTrades, 1);
assert.equal(second.duplicateTrades, 1);
const rows = await pool.query('SELECT COUNT(*)::int AS count FROM portfolio_trades WHERE account_id = $1', [first.accountId]);
assert.equal(rows.rows[0].count, 1);
console.log('portfolio import SQL: OK');
} finally {
await pool.query("DELETE FROM portfolio_trades WHERE account_id IN (SELECT id FROM accounts WHERE bank = 'PORTFOLIO_TEST' AND account_number = 'portfolio-test')");
await pool.query("DELETE FROM portfolio_reports WHERE account_id IN (SELECT id FROM accounts WHERE bank = 'PORTFOLIO_TEST' AND account_number = 'portfolio-test')");
await pool.query("DELETE FROM accounts WHERE bank = 'PORTFOLIO_TEST' AND account_number = 'portfolio-test'");
await pool.end();
}
}
run();

View File

@@ -43,11 +43,11 @@ export function validatePortfolio(body: unknown): asserts body is PortfolioFile
}
}
export async function importPortfolio(body: unknown): Promise<ImportPortfolioResponse> {
export async function importPortfolio(body: unknown, db: Pick<typeof pool, 'connect'> = pool): Promise<ImportPortfolioResponse> {
validatePortfolio(body);
const data = body;
const sourceHash = crypto.createHash('sha256').update(JSON.stringify(data)).digest('hex');
const client = await pool.connect();
const client = await db.connect();
try {
await client.query('BEGIN');
const accountResult = await client.query(