223 lines
13 KiB
TypeScript
223 lines
13 KiB
TypeScript
import { pool } from './pool';
|
||
|
||
const migrations: { name: string; sql: string }[] = [
|
||
{
|
||
name: '001_create_tables',
|
||
sql: `
|
||
CREATE TABLE IF NOT EXISTS accounts (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
bank TEXT NOT NULL,
|
||
account_number TEXT NOT NULL,
|
||
currency TEXT NOT NULL,
|
||
alias TEXT
|
||
);
|
||
CREATE UNIQUE INDEX IF NOT EXISTS ux_accounts_bank_number
|
||
ON accounts(bank, account_number);
|
||
|
||
CREATE TABLE IF NOT EXISTS categories (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
name TEXT NOT NULL,
|
||
type TEXT NOT NULL,
|
||
is_active BOOLEAN NOT NULL DEFAULT TRUE
|
||
);
|
||
ALTER TABLE categories DROP CONSTRAINT IF EXISTS chk_categories_type;
|
||
ALTER TABLE categories
|
||
ADD CONSTRAINT chk_categories_type
|
||
CHECK (type IN ('expense', 'income', 'transfer'));
|
||
|
||
CREATE TABLE IF NOT EXISTS transactions (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
account_id BIGINT NOT NULL REFERENCES accounts(id),
|
||
operation_at TIMESTAMPTZ NOT NULL,
|
||
amount_signed BIGINT NOT NULL,
|
||
commission BIGINT NOT NULL,
|
||
description TEXT NOT NULL,
|
||
direction TEXT NOT NULL,
|
||
fingerprint TEXT NOT NULL,
|
||
category_id BIGINT REFERENCES categories(id),
|
||
is_category_confirmed BOOLEAN NOT NULL DEFAULT FALSE,
|
||
comment TEXT,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
CREATE UNIQUE INDEX IF NOT EXISTS ux_transactions_account_fingerprint
|
||
ON transactions(account_id, fingerprint);
|
||
ALTER TABLE transactions DROP CONSTRAINT IF EXISTS chk_transactions_direction;
|
||
ALTER TABLE transactions
|
||
ADD CONSTRAINT chk_transactions_direction
|
||
CHECK (direction IN ('income', 'expense', 'transfer'));
|
||
|
||
CREATE TABLE IF NOT EXISTS category_rules (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
pattern TEXT NOT NULL,
|
||
match_type TEXT NOT NULL,
|
||
category_id BIGINT NOT NULL REFERENCES categories(id),
|
||
priority INT NOT NULL DEFAULT 0,
|
||
requires_confirmation BOOLEAN NOT NULL DEFAULT FALSE,
|
||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS sessions (
|
||
id TEXT PRIMARY KEY,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
last_activity_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
is_active BOOLEAN NOT NULL DEFAULT TRUE
|
||
);
|
||
`,
|
||
},
|
||
{
|
||
name: '002_seed_categories',
|
||
sql: `
|
||
INSERT INTO categories (name, type) VALUES
|
||
('Продукты', 'expense'),
|
||
('Авто', 'expense'),
|
||
('Здоровье', 'expense'),
|
||
('Арчи', 'expense'),
|
||
('ЖКХ', 'expense'),
|
||
('Дом', 'expense'),
|
||
('Проезд', 'expense'),
|
||
('Одежда', 'expense'),
|
||
('Химия', 'expense'),
|
||
('Косметика', 'expense'),
|
||
('Инвестиции', 'transfer'),
|
||
('Развлечения', 'expense'),
|
||
('Общепит', 'expense'),
|
||
('Штрафы', 'expense'),
|
||
('Налоги', 'expense'),
|
||
('Подписки', 'expense'),
|
||
('Перевод', 'transfer'),
|
||
('Наличные', 'expense'),
|
||
('Подарки', 'expense'),
|
||
('Спорт', 'expense'),
|
||
('Отпуск', 'expense'),
|
||
('Техника', 'expense'),
|
||
('Поступления', 'income')
|
||
ON CONFLICT DO NOTHING;
|
||
`,
|
||
},
|
||
{
|
||
name: '003_seed_category_rules',
|
||
sql: `
|
||
INSERT INTO category_rules (pattern, match_type, category_id, priority, requires_confirmation)
|
||
SELECT pattern, match_type, category_id, priority, requires_confirmation
|
||
FROM (VALUES
|
||
('PYATEROCHK', 'contains', (SELECT id FROM categories WHERE name = 'Продукты' AND type = 'expense' LIMIT 1), 20, false),
|
||
('ПЯТЕРОЧКА', 'contains', (SELECT id FROM categories WHERE name = 'Продукты' AND type = 'expense' LIMIT 1), 20, false),
|
||
('AUCHAN', 'contains', (SELECT id FROM categories WHERE name = 'Продукты' AND type = 'expense' LIMIT 1), 20, false),
|
||
('DOSTAVKA IZ PYATEROCHK', 'contains', (SELECT id FROM categories WHERE name = 'Продукты' AND type = 'expense' LIMIT 1), 25, false),
|
||
('МЕТРО', 'contains', (SELECT id FROM categories WHERE name = 'Продукты' AND type = 'expense' LIMIT 1), 20, false),
|
||
('PEREKRESTOK', 'contains', (SELECT id FROM categories WHERE name = 'Продукты' AND type = 'expense' LIMIT 1), 20, false),
|
||
('MAGNIT', 'contains', (SELECT id FROM categories WHERE name = 'Продукты' AND type = 'expense' LIMIT 1), 20, false),
|
||
('LENTA', 'contains', (SELECT id FROM categories WHERE name = 'Продукты' AND type = 'expense' LIMIT 1), 20, false),
|
||
('SPORTMASTER', 'contains', (SELECT id FROM categories WHERE name = 'Спорт' AND type = 'expense' LIMIT 1), 20, false),
|
||
('DECATHLON', 'contains', (SELECT id FROM categories WHERE name = 'Спорт' AND type = 'expense' LIMIT 1), 20, false),
|
||
('PAYPARKING', 'contains', (SELECT id FROM categories WHERE name = 'Авто' AND type = 'expense' LIMIT 1), 20, false),
|
||
('PARKING', 'contains', (SELECT id FROM categories WHERE name = 'Авто' AND type = 'expense' LIMIT 1), 15, false),
|
||
('AZS', 'contains', (SELECT id FROM categories WHERE name = 'Авто' AND type = 'expense' LIMIT 1), 20, false),
|
||
('ЯНДЕКС.ЕДА', 'contains', (SELECT id FROM categories WHERE name = 'Общепит' AND type = 'expense' LIMIT 1), 25, false),
|
||
('MCDONALD', 'contains', (SELECT id FROM categories WHERE name = 'Общепит' AND type = 'expense' LIMIT 1), 20, false),
|
||
('KFC', 'contains', (SELECT id FROM categories WHERE name = 'Общепит' AND type = 'expense' LIMIT 1), 20, false),
|
||
('STARBUCKS', 'contains', (SELECT id FROM categories WHERE name = 'Общепит' AND type = 'expense' LIMIT 1), 20, false),
|
||
('tapper.ru', 'contains', (SELECT id FROM categories WHERE name = 'Развлечения' AND type = 'expense' LIMIT 1), 20, false),
|
||
('АПТЕКА', 'contains', (SELECT id FROM categories WHERE name = 'Здоровье' AND type = 'expense' LIMIT 1), 20, false),
|
||
('РИГЛА', 'contains', (SELECT id FROM categories WHERE name = 'Здоровье' AND type = 'expense' LIMIT 1), 20, false),
|
||
('OZON', 'contains', (SELECT id FROM categories WHERE name = 'Дом' AND type = 'expense' LIMIT 1), 15, false),
|
||
('WILDBERRIES', 'contains', (SELECT id FROM categories WHERE name = 'Дом' AND type = 'expense' LIMIT 1), 15, false),
|
||
('ЯНДЕКС ПЛЮС', 'contains', (SELECT id FROM categories WHERE name = 'Подписки' AND type = 'expense' LIMIT 1), 20, false),
|
||
('SPOTIFY', 'contains', (SELECT id FROM categories WHERE name = 'Подписки' AND type = 'expense' LIMIT 1), 20, false),
|
||
('НЕТФЛИКС', 'contains', (SELECT id FROM categories WHERE name = 'Подписки' AND type = 'expense' LIMIT 1), 20, false)
|
||
) AS v(pattern, match_type, category_id, priority, requires_confirmation)
|
||
WHERE category_id IS NOT NULL
|
||
AND EXISTS (SELECT 1 FROM categories LIMIT 1)
|
||
AND NOT EXISTS (SELECT 1 FROM category_rules LIMIT 1);
|
||
`,
|
||
},
|
||
{
|
||
name: '004_seed_category_rules_extended',
|
||
sql: `
|
||
INSERT INTO category_rules (pattern, match_type, category_id, priority, requires_confirmation)
|
||
SELECT pattern, match_type, category_id, priority, requires_confirmation
|
||
FROM (VALUES
|
||
('STOLOVAYA', 'contains', (SELECT id FROM categories WHERE name = 'Общепит' LIMIT 1), 25, false),
|
||
('KOFEJNYA', 'contains', (SELECT id FROM categories WHERE name = 'Общепит' LIMIT 1), 25, false),
|
||
('JEFFREY S COFFEESHOP', 'contains', (SELECT id FROM categories WHERE name = 'Общепит' LIMIT 1), 25, false),
|
||
('TA TORRRO GRIL', 'contains', (SELECT id FROM categories WHERE name = 'Общепит' LIMIT 1), 25, false),
|
||
('OUSHEN FRENDS', 'contains', (SELECT id FROM categories WHERE name = 'Общепит' LIMIT 1), 25, false),
|
||
('mkad', 'contains', (SELECT id FROM categories WHERE name = 'Авто' LIMIT 1), 20, false),
|
||
('RNAZK', 'contains', (SELECT id FROM categories WHERE name = 'Авто' LIMIT 1), 20, false),
|
||
('IP SHEVELEV', 'contains', (SELECT id FROM categories WHERE name = 'Авто' LIMIT 1), 20, false),
|
||
('PAVELETSKAYA', 'contains', (SELECT id FROM categories WHERE name = 'Проезд' LIMIT 1), 25, false),
|
||
('CPPK-', 'contains', (SELECT id FROM categories WHERE name = 'Проезд' LIMIT 1), 25, false),
|
||
('MOS.TRANSPORT', 'contains', (SELECT id FROM categories WHERE name = 'Проезд' LIMIT 1), 25, false),
|
||
('Lab4uru', 'contains', (SELECT id FROM categories WHERE name = 'Здоровье' LIMIT 1), 25, false),
|
||
('APTEKA', 'contains', (SELECT id FROM categories WHERE name = 'Здоровье' LIMIT 1), 20, false),
|
||
('IP SHARAFETDINOV', 'contains', (SELECT id FROM categories WHERE name = 'Арчи' LIMIT 1), 25, false),
|
||
('ZOOMAGAZIN CHETYRE LAP', 'contains', (SELECT id FROM categories WHERE name = 'Арчи' LIMIT 1), 25, false),
|
||
('VKUSVILL', 'contains', (SELECT id FROM categories WHERE name = 'Продукты' LIMIT 1), 20, false),
|
||
('GLOBUS', 'contains', (SELECT id FROM categories WHERE name = 'Продукты' LIMIT 1), 20, false),
|
||
('VERNYJ', 'contains', (SELECT id FROM categories WHERE name = 'Продукты' LIMIT 1), 20, false),
|
||
('GOLD APPLE', 'contains', (SELECT id FROM categories WHERE name = 'Косметика' LIMIT 1), 25, false),
|
||
('SPIRITFIT', 'contains', (SELECT id FROM categories WHERE name = 'Спорт' LIMIT 1), 25, false),
|
||
('insanity', 'contains', (SELECT id FROM categories WHERE name = 'Спорт' LIMIT 1), 25, false),
|
||
('anta-sport', 'contains', (SELECT id FROM categories WHERE name = 'Спорт' LIMIT 1), 25, false),
|
||
('VANYAVPN', 'contains', (SELECT id FROM categories WHERE name = 'Подписки' LIMIT 1), 25, false),
|
||
('ГРАНЛАЙН', 'contains', (SELECT id FROM categories WHERE name = 'Подписки' LIMIT 1), 20, false),
|
||
('Мобильная связь', 'contains', (SELECT id FROM categories WHERE name = 'Подписки' LIMIT 1), 25, false),
|
||
('OSTROVOK', 'contains', (SELECT id FROM categories WHERE name = 'Отпуск' LIMIT 1), 25, false),
|
||
('sutochno', 'contains', (SELECT id FROM categories WHERE name = 'Отпуск' LIMIT 1), 25, false),
|
||
('УФК', 'contains', (SELECT id FROM categories WHERE name = 'Штрафы' LIMIT 1), 30, false),
|
||
('ГИБДД', 'contains', (SELECT id FROM categories WHERE name = 'Штрафы' LIMIT 1), 30, false),
|
||
('Поступление заработной платы', 'contains', (SELECT id FROM categories WHERE name = 'Поступления' LIMIT 1), 30, false),
|
||
('avito', 'contains', (SELECT id FROM categories WHERE name = 'Поступления' LIMIT 1), 25, false),
|
||
('Init payout', 'contains', (SELECT id FROM categories WHERE name = 'Поступления' LIMIT 1), 25, false)
|
||
) AS v(pattern, match_type, category_id, priority, requires_confirmation)
|
||
WHERE category_id IS NOT NULL
|
||
AND EXISTS (SELECT 1 FROM categories LIMIT 1);
|
||
`,
|
||
},
|
||
];
|
||
|
||
export async function runMigrations(): Promise<void> {
|
||
await pool.query(`
|
||
CREATE TABLE IF NOT EXISTS _migrations (
|
||
name TEXT PRIMARY KEY,
|
||
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
)
|
||
`);
|
||
|
||
for (const m of migrations) {
|
||
const { rows } = await pool.query(
|
||
'SELECT 1 FROM _migrations WHERE name = $1',
|
||
[m.name],
|
||
);
|
||
if (rows.length > 0) continue;
|
||
|
||
const client = await pool.connect();
|
||
try {
|
||
await client.query('BEGIN');
|
||
await client.query(m.sql);
|
||
await client.query('INSERT INTO _migrations (name) VALUES ($1)', [m.name]);
|
||
await client.query('COMMIT');
|
||
console.log(`Migration applied: ${m.name}`);
|
||
} catch (err) {
|
||
await client.query('ROLLBACK');
|
||
throw err;
|
||
} finally {
|
||
client.release();
|
||
}
|
||
}
|
||
}
|
||
|
||
if (require.main === module) {
|
||
runMigrations()
|
||
.then(() => {
|
||
console.log('All migrations applied');
|
||
process.exit(0);
|
||
})
|
||
.catch((err) => {
|
||
console.error('Migration failed:', err);
|
||
process.exit(1);
|
||
});
|
||
}
|