test: cover portfolio import idempotency
This commit is contained in:
@@ -1,5 +1,11 @@
|
|||||||
# Changelog
|
# 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
|
## [Backend 0.8.1] - 2026-08-20
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@family-budget/backend",
|
"name": "@family-budget/backend",
|
||||||
"version": "0.8.1",
|
"version": "0.8.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tsx watch src/app.ts",
|
"dev": "tsx watch src/app.ts",
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
"migrate:prod": "node dist/db/migrate.js",
|
"migrate:prod": "node dist/db/migrate.js",
|
||||||
"test:analytics": "tsx src/services/analyticsSemantics.test.ts",
|
"test:analytics": "tsx src/services/analyticsSemantics.test.ts",
|
||||||
"test:portfolio": "tsx src/services/portfolio.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: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:import:db": "NODE_ENV=test tsx src/services/import.integration.test.ts",
|
||||||
"test:llm": "tsx src/scripts/testLlm.ts"
|
"test:llm": "tsx src/scripts/testLlm.ts"
|
||||||
|
|||||||
29
backend/src/services/portfolio.integration.test.ts
Normal file
29
backend/src/services/portfolio.integration.test.ts
Normal 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();
|
||||||
@@ -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);
|
validatePortfolio(body);
|
||||||
const data = body;
|
const data = body;
|
||||||
const sourceHash = crypto.createHash('sha256').update(JSON.stringify(data)).digest('hex');
|
const sourceHash = crypto.createHash('sha256').update(JSON.stringify(data)).digest('hex');
|
||||||
const client = await pool.connect();
|
const client = await db.connect();
|
||||||
try {
|
try {
|
||||||
await client.query('BEGIN');
|
await client.query('BEGIN');
|
||||||
const accountResult = await client.query(
|
const accountResult = await client.query(
|
||||||
|
|||||||
Reference in New Issue
Block a user