feat: preserve transaction source order

This commit is contained in:
2026-08-20 23:20:56 +03:00
parent 4172b0c8e4
commit ab42caae37
5 changed files with 24 additions and 6 deletions

View File

@@ -1,5 +1,11 @@
# Changelog # 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 ## [Backend 0.8.3] - 2026-08-20
### Fixed ### Fixed

View File

@@ -1,6 +1,6 @@
{ {
"name": "@family-budget/backend", "name": "@family-budget/backend",
"version": "0.8.3", "version": "0.9.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "tsx watch src/app.ts", "dev": "tsx watch src/app.ts",

View File

@@ -287,6 +287,15 @@ const migrations: { name: string; sql: string }[] = [
ON portfolio_trades(account_id, concluded_at DESC); 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<void> { export async function runMigrations(): Promise<void> {

View File

@@ -219,7 +219,7 @@ export async function importStatement(
// Insert transactions // Insert transactions
const insertedIds: number[] = []; 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 fp = computeFingerprint(data.statement.accountNumber, tx);
const isCashbackCommissionImport = const isCashbackCommissionImport =
tx.amountSigned === 0 && tx.amountSigned === 0 &&
@@ -235,11 +235,11 @@ export async function importStatement(
const result = await client.query( const result = await client.query(
`INSERT INTO transactions `INSERT INTO transactions
(account_id, operation_id, operation_at, amount_signed, commission, description, direction, fingerprint, category_id, is_category_confirmed, import_id) (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) VALUES ($1, COALESCE($2::uuid, gen_random_uuid()), $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
ON CONFLICT DO NOTHING ON CONFLICT DO NOTHING
RETURNING id`, 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) { if (result.rows.length > 0) {

View File

@@ -14,6 +14,9 @@ export async function getTransactions(
const pageSize = [10, 50, 100].includes(params.pageSize ?? 50) ? (params.pageSize ?? 50) : 50; 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 sortBy = params.sortBy === 'amount' ? 't.amount_signed' : 't.operation_at';
const sortOrder = params.sortOrder === 'asc' ? 'ASC' : 'DESC'; 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 conditions: string[] = [];
const values: unknown[] = []; const values: unknown[] = [];
@@ -84,7 +87,7 @@ export async function getTransactions(
JOIN accounts a ON a.id = t.account_id JOIN accounts a ON a.id = t.account_id
LEFT JOIN categories c ON c.id = t.category_id LEFT JOIN categories c ON c.id = t.category_id
${where} ${where}
ORDER BY ${sortBy} ${sortOrder} ORDER BY ${orderBy}
LIMIT $${idx++} OFFSET $${idx++}`, LIMIT $${idx++} OFFSET $${idx++}`,
[...values, pageSize, offset], [...values, pageSize, offset],
); );