feat: assign deterministic operation UUIDs

This commit is contained in:
2026-08-19 17:46:50 +03:00
parent 3d08c9e5c6
commit 5c059cbc62
8 changed files with 34 additions and 7 deletions

View File

@@ -133,6 +133,16 @@ const migrations: { name: string; sql: string }[] = [
AND NOT EXISTS (SELECT 1 FROM category_rules LIMIT 1);
`,
},
{
name: '006_transaction_operation_uuid',
sql: `
CREATE EXTENSION IF NOT EXISTS pgcrypto;
ALTER TABLE transactions
ADD COLUMN IF NOT EXISTS operation_id UUID NOT NULL DEFAULT gen_random_uuid();
CREATE UNIQUE INDEX IF NOT EXISTS ux_transactions_account_operation_id
ON transactions(account_id, operation_id);
`,
},
{
name: '005_imports_table',
sql: `

View File

@@ -9,6 +9,7 @@ const TRANSFER_PHRASES = [
'внутри втб',
];
const CASHBACK_KEYWORD = 'зачисление';
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
function computeFingerprint(
accountNumber: string,
@@ -99,6 +100,9 @@ function validateStructure(body: unknown): ValidationError | null {
if (t.sourceId !== undefined && (typeof t.sourceId !== 'string' || !t.sourceId.trim())) {
return { status: 400, error: 'BAD_REQUEST', message: `transactions[${i}].sourceId must be a non-empty string when provided` };
}
if (t.operationId !== undefined && (typeof t.operationId !== 'string' || !UUID_RE.test(t.operationId))) {
return { status: 400, error: 'BAD_REQUEST', message: `transactions[${i}].operationId must be a valid UUID when provided` };
}
if (typeof t.amountSigned !== 'number' || !Number.isInteger(t.amountSigned)) {
return { status: 400, error: 'BAD_REQUEST', message: `transactions[${i}].amountSigned must be an integer` };
}
@@ -212,11 +216,11 @@ export async function importStatement(
const result = await client.query(
`INSERT INTO transactions
(account_id, operation_at, amount_signed, commission, description, direction, fingerprint, category_id, is_category_confirmed, import_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
(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)
ON CONFLICT (account_id, fingerprint) DO NOTHING
RETURNING id`,
[accountId, 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],
);
if (result.rows.length > 0) {