Files
iistwin/server/finance-di2/override-tables.ts
Ильяс Султанов 254950f151 DI2 внедрение, таск 1: серверная интеграция Data-Insight2 (server/finance-di2)
- Копия DI2-сервера в server/finance-di2/ с правками: schema/db-client/cache/table-config/override-tables/google-auth/audit-agent/routes
- db-client — обёртка над существующим пулом server/finance/db-client (второй пул не создаётся, добавлен экспорт isConnectionError)
- ai-config.ts — чистое IO конфигов из ai-agent.ts без Telegram-поллинга; роуты /api/ai/toggle и /api/ai/status удалены, без compression/startAuditScheduler/autoStartIfEnabled
- Обёртка registerDi2Routes с auth-gate (authenticateToken + finance.manage), регистрация строго перед registerFinanceRoutes
- Статика /di2 из dist/public-di2 перед веткой vite/static (dev и prod)
- Фикс предсуществующего бага DI2: buildGlobalExclusionConditions без cats в /api/profitability
2026-09-06 19:27:43 +03:00

233 lines
8.7 KiB
TypeScript

import { query } from "./db-client";
import { loadTableConfig } from "./table-config";
import fs from "fs";
import path from "path";
export const INCOME_OVERRIDE_COLS = [
"zakazchik", "dogovorzakazchika", "otvetstvenniipokontraktu",
"statya", "dochka", "vnuchka", "naznachenieplatezha",
"kontragent", "dogovorkontragenta", "stavkands",
"kommentarii", "statyadop", "schetnaoplatu", "nomervhodyaschegodokumenta",
];
export const EXPENSE_OVERRIDE_COLS = [
"zakazchik", "dogovorzakazchika", "otvetstvenniipokontraktu",
"statya", "dochka", "vnuchka", "naznachenieplatezha",
"kontragent", "dogovorkontragenta", "stavkands",
"kommentarii", "statyadop", "ssilkanomervhodyaschegodokumenta",
];
async function createOverrideTables(): Promise<void> {
await query(`CREATE TABLE IF NOT EXISTS opdohodi_overrides (
id TEXT PRIMARY KEY,
category_id TEXT,
zakazchik TEXT, dogovorzakazchika TEXT, otvetstvenniipokontraktu TEXT,
statya TEXT, dochka TEXT, vnuchka TEXT, naznachenieplatezha TEXT,
kontragent TEXT, dogovorkontragenta TEXT, stavkands TEXT,
kommentarii TEXT, statyadop TEXT, schetnaoplatu TEXT,
nomervhodyaschegodokumenta TEXT,
created_at TIMESTAMP DEFAULT NOW()
)`);
await query(`CREATE TABLE IF NOT EXISTS oprashodi_overrides (
id TEXT PRIMARY KEY,
category_id TEXT,
zakazchik TEXT, dogovorzakazchika TEXT, otvetstvenniipokontraktu TEXT,
statya TEXT, dochka TEXT, vnuchka TEXT, naznachenieplatezha TEXT,
kontragent TEXT, dogovorkontragenta TEXT, stavkands TEXT,
kommentarii TEXT, statyadop TEXT, ssilkanomervhodyaschegodokumenta TEXT,
created_at TIMESTAMP DEFAULT NOW()
)`);
}
async function createEffectiveView(
sourceTable: string,
overrideTable: string,
viewName: string,
overrideCols: string[]
): Promise<void> {
const schemaResult = await query(
`SELECT column_name FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = $1
ORDER BY ordinal_position`,
[sourceTable]
);
const allCols: string[] = schemaResult.rows.map((r: any) => r.column_name);
if (allCols.length === 0) {
console.warn(`[override-tables] Table ${sourceTable} not found in schema, skipping view ${viewName}`);
return;
}
const overrideColSet = new Set(overrideCols);
const selectExprs = allCols.map(col => {
if (overrideColSet.has(col)) {
return `COALESCE(ov."${col}", o."${col}") AS "${col}"`;
}
return `o."${col}"`;
});
selectExprs.push(`ov.category_id AS "_override_category_id"`);
const viewSQL = `
CREATE OR REPLACE VIEW "${viewName}" AS
SELECT ${selectExprs.join(",\n ")}
FROM "${sourceTable}" o
LEFT JOIN "${overrideTable}" ov ON o.id = ov.id
`;
await query(viewSQL);
console.log(`[override-tables] View ${viewName} created/updated`);
}
export async function initOverrideTables(): Promise<void> {
try {
const cfg = loadTableConfig();
await createOverrideTables();
await Promise.all([
createEffectiveView(cfg.incomeTable, "opdohodi_overrides", "vw_income_effective", INCOME_OVERRIDE_COLS),
createEffectiveView(cfg.expenseTable, "oprashodi_overrides", "vw_expense_effective", EXPENSE_OVERRIDE_COLS),
]);
console.log("[override-tables] Initialization complete");
} catch (err) {
console.error("[override-tables] Init failed:", err);
}
}
export async function recreateEffectiveViews(): Promise<void> {
const cfg = loadTableConfig();
await Promise.all([
createEffectiveView(cfg.incomeTable, "opdohodi_overrides", "vw_income_effective", INCOME_OVERRIDE_COLS),
createEffectiveView(cfg.expenseTable, "oprashodi_overrides", "vw_expense_effective", EXPENSE_OVERRIDE_COLS),
]);
}
export async function saveIncomeOverride(
recordId: string,
colValues: Record<string, string>,
categoryId: string
): Promise<void> {
const params: unknown[] = [recordId, categoryId];
for (const col of INCOME_OVERRIDE_COLS) {
params.push(colValues[col] ?? null);
}
const colDefs = `"id", "category_id", ${INCOME_OVERRIDE_COLS.map(c => `"${c}"`).join(", ")}`;
const valuePhs = params.map((_, i) => `$${i + 1}`).join(", ");
const updateSets = [`"category_id" = $2`, ...INCOME_OVERRIDE_COLS.map((col, i) => `"${col}" = $${i + 3}`)].join(", ");
await query(
`INSERT INTO opdohodi_overrides (${colDefs}) VALUES (${valuePhs})
ON CONFLICT (id) DO UPDATE SET ${updateSets}`,
params
);
}
export async function saveExpenseOverride(
recordId: string,
colValues: Record<string, string>,
categoryId: string
): Promise<void> {
const params: unknown[] = [recordId, categoryId];
for (const col of EXPENSE_OVERRIDE_COLS) {
params.push(colValues[col] ?? null);
}
const colDefs = `"id", "category_id", ${EXPENSE_OVERRIDE_COLS.map(c => `"${c}"`).join(", ")}`;
const valuePhs = params.map((_, i) => `$${i + 1}`).join(", ");
const updateSets = [`"category_id" = $2`, ...EXPENSE_OVERRIDE_COLS.map((col, i) => `"${col}" = $${i + 3}`)].join(", ");
await query(
`INSERT INTO oprashodi_overrides (${colDefs}) VALUES (${valuePhs})
ON CONFLICT (id) DO UPDATE SET ${updateSets}`,
params
);
}
export async function deleteIncomeOverride(recordId: string): Promise<void> {
await query(`DELETE FROM opdohodi_overrides WHERE id = $1`, [recordId]);
}
export async function deleteExpenseOverride(recordId: string): Promise<void> {
await query(`DELETE FROM oprashodi_overrides WHERE id = $1`, [recordId]);
}
export async function getAllIncomeOverrides(): Promise<Array<{ id: string; categoryId: string }>> {
const result = await query(`SELECT id, category_id FROM opdohodi_overrides WHERE category_id IS NOT NULL`);
return result.rows.map((r: any) => ({ id: String(r.id), categoryId: String(r.category_id) }));
}
export async function getAllExpenseOverrides(): Promise<Array<{ id: string; categoryId: string }>> {
const result = await query(`SELECT id, category_id FROM oprashodi_overrides WHERE category_id IS NOT NULL`);
return result.rows.map((r: any) => ({ id: String(r.id), categoryId: String(r.category_id) }));
}
export function extractColumnValuesFromCategoryPath(
path: Array<{ rules: Array<{ column: string; operator: string; value: string }> }>,
allowedCols: string[]
): Record<string, string> {
const allowed = new Set(allowedCols);
const result: Record<string, string> = {};
for (const cat of path) {
for (const rule of cat.rules) {
if (rule.operator === "equals" && allowed.has(rule.column)) {
result[rule.column] = rule.value;
}
}
}
return result;
}
export async function migrateJsonOverrides(
jsonFilePath: string,
findIncomeCategory: (id: string) => { cat: any; path: any[] } | null,
findExpenseCategory: (id: string) => { cat: any; path: any[] } | null
): Promise<void> {
if (!fs.existsSync(jsonFilePath)) return;
const migratedPath = jsonFilePath + ".migrated";
if (fs.existsSync(migratedPath)) return;
try {
const incomeCount = await query(`SELECT COUNT(*) as cnt FROM opdohodi_overrides`);
const expenseCount = await query(`SELECT COUNT(*) as cnt FROM oprashodi_overrides`);
if (Number(incomeCount.rows[0].cnt) > 0 || Number(expenseCount.rows[0].cnt) > 0) {
console.log("[override-tables] Override tables already have data, skipping migration");
fs.renameSync(jsonFilePath, migratedPath);
return;
}
const raw = fs.readFileSync(jsonFilePath, "utf-8");
const parsed = JSON.parse(raw);
const income: Record<string, string> = parsed.income || {};
const expense: Record<string, string> = parsed.expense || {};
let migratedIncome = 0;
let migratedExpense = 0;
for (const [recordId, categoryId] of Object.entries(income)) {
try {
const found = findIncomeCategory(categoryId);
if (!found) continue;
const colValues = extractColumnValuesFromCategoryPath(found.path, INCOME_OVERRIDE_COLS);
await saveIncomeOverride(recordId, colValues, categoryId);
migratedIncome++;
} catch (err) {
console.warn(`[override-tables] Migration failed for income record ${recordId}:`, err);
}
}
for (const [recordId, categoryId] of Object.entries(expense)) {
try {
const found = findExpenseCategory(categoryId);
if (!found) continue;
const colValues = extractColumnValuesFromCategoryPath(found.path, EXPENSE_OVERRIDE_COLS);
await saveExpenseOverride(recordId, colValues, categoryId);
migratedExpense++;
} catch (err) {
console.warn(`[override-tables] Migration failed for expense record ${recordId}:`, err);
}
}
fs.renameSync(jsonFilePath, migratedPath);
console.log(`[override-tables] Migration complete: ${migratedIncome} income, ${migratedExpense} expense overrides migrated`);
} catch (err) {
console.error("[override-tables] Migration error:", err);
}
}