From ab42caae3770c39b3e5a406fc76261e5790724f8 Mon Sep 17 00:00:00 2001 From: admin Date: Thu, 20 Aug 2026 23:20:56 +0300 Subject: [PATCH] feat: preserve transaction source order --- CHANGELOG.md | 6 ++++++ backend/package.json | 2 +- backend/src/db/migrate.ts | 9 +++++++++ backend/src/services/import.ts | 8 ++++---- backend/src/services/transactions.ts | 5 ++++- 5 files changed, 24 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index faec18f..2eecf93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Backend 0.9.0] - 2026-08-20 + +### Added + +- Preserved source-array transaction positions and used them as stable tie-breakers for same-date history sorting. + ## [Backend 0.8.3] - 2026-08-20 ### Fixed diff --git a/backend/package.json b/backend/package.json index 7dbc83a..e18aaec 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "@family-budget/backend", - "version": "0.8.3", + "version": "0.9.0", "private": true, "scripts": { "dev": "tsx watch src/app.ts", diff --git a/backend/src/db/migrate.ts b/backend/src/db/migrate.ts index 2085a3c..1488415 100644 --- a/backend/src/db/migrate.ts +++ b/backend/src/db/migrate.ts @@ -287,6 +287,15 @@ const migrations: { name: string; sql: string }[] = [ ON portfolio_trades(account_id, concluded_at DESC); `, }, + { + name: '009_transaction_source_position', + sql: ` + ALTER TABLE transactions ADD COLUMN IF NOT EXISTS source_position BIGINT NOT NULL DEFAULT 0; + UPDATE transactions SET source_position = id WHERE source_position = 0; + CREATE INDEX IF NOT EXISTS ix_transactions_date_position + ON transactions(operation_at, source_position, id); + `, + }, ]; export async function runMigrations(): Promise { diff --git a/backend/src/services/import.ts b/backend/src/services/import.ts index e9cde1f..b15c5f9 100644 --- a/backend/src/services/import.ts +++ b/backend/src/services/import.ts @@ -219,7 +219,7 @@ export async function importStatement( // Insert transactions const insertedIds: number[] = []; - for (const tx of data.transactions) { + for (const [sourcePosition, tx] of data.transactions.entries()) { const fp = computeFingerprint(data.statement.accountNumber, tx); const isCashbackCommissionImport = tx.amountSigned === 0 && @@ -235,11 +235,11 @@ export async function importStatement( const result = await client.query( `INSERT INTO transactions - (account_id, operation_id, operation_at, amount_signed, commission, description, direction, fingerprint, category_id, is_category_confirmed, import_id) - VALUES ($1, COALESCE($2::uuid, gen_random_uuid()), $3, $4, $5, $6, $7, $8, $9, $10, $11) + (account_id, operation_id, operation_at, amount_signed, commission, description, direction, fingerprint, category_id, is_category_confirmed, import_id, source_position) + VALUES ($1, COALESCE($2::uuid, gen_random_uuid()), $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) ON CONFLICT DO NOTHING RETURNING id`, - [accountId, tx.operationId ?? null, tx.operationAt, tx.amountSigned, tx.commission, tx.description, dir, fp, categoryId, isCategoryConfirmed, importId], + [accountId, tx.operationId ?? null, tx.operationAt, tx.amountSigned, tx.commission, tx.description, dir, fp, categoryId, isCategoryConfirmed, importId, sourcePosition], ); if (result.rows.length > 0) { diff --git a/backend/src/services/transactions.ts b/backend/src/services/transactions.ts index a142199..2d32a91 100644 --- a/backend/src/services/transactions.ts +++ b/backend/src/services/transactions.ts @@ -14,6 +14,9 @@ export async function getTransactions( const pageSize = [10, 50, 100].includes(params.pageSize ?? 50) ? (params.pageSize ?? 50) : 50; const sortBy = params.sortBy === 'amount' ? 't.amount_signed' : 't.operation_at'; const sortOrder = params.sortOrder === 'asc' ? 'ASC' : 'DESC'; + const orderBy = params.sortBy === 'amount' + ? `${sortBy} ${sortOrder}, t.operation_at DESC, t.source_position ASC, t.id DESC` + : `t.operation_at ${sortOrder}, t.source_position ${sortOrder === 'ASC' ? 'DESC' : 'ASC'}, t.id ${sortOrder === 'ASC' ? 'DESC' : 'ASC'}`; const conditions: string[] = []; const values: unknown[] = []; @@ -84,7 +87,7 @@ export async function getTransactions( JOIN accounts a ON a.id = t.account_id LEFT JOIN categories c ON c.id = t.category_id ${where} - ORDER BY ${sortBy} ${sortOrder} + ORDER BY ${orderBy} LIMIT $${idx++} OFFSET $${idx++}`, [...values, pageSize, offset], );