From 4d7836540a604d770b38779a6b2e56a5e9a3c2e7 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Wed, 15 Apr 2026 19:06:32 +0200 Subject: [PATCH] feat(pitch-deck): add admin migration endpoint for finanzplan tables POST /api/admin/migrate creates all fp_* tables on production DB. Admin-only, creates tables with IF NOT EXISTS for safe re-runs. Co-Authored-By: Claude Opus 4.6 (1M context) --- pitch-deck/app/api/admin/migrate/route.ts | 128 ++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 pitch-deck/app/api/admin/migrate/route.ts diff --git a/pitch-deck/app/api/admin/migrate/route.ts b/pitch-deck/app/api/admin/migrate/route.ts new file mode 100644 index 0000000..149cdd1 --- /dev/null +++ b/pitch-deck/app/api/admin/migrate/route.ts @@ -0,0 +1,128 @@ +import { NextRequest, NextResponse } from 'next/server' +import { requireAdmin } from '@/lib/admin-auth' +import pool from '@/lib/db' + +export async function POST(request: NextRequest) { + const guard = await requireAdmin(request) + if (guard.kind === 'response') return guard.response + + const results: string[] = [] + + // Finanzplan tables — the ones missing on production + const statements = [ + `CREATE TABLE IF NOT EXISTS fp_scenarios ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL DEFAULT 'Base Case', + description TEXT, + is_default BOOLEAN DEFAULT false, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() + )`, + `INSERT INTO fp_scenarios (name, description, is_default) + SELECT 'Base Case', 'Basisdaten aus Excel-Import', true + WHERE NOT EXISTS (SELECT 1 FROM fp_scenarios WHERE is_default = true)`, + `CREATE TABLE IF NOT EXISTS fp_kunden ( + id SERIAL PRIMARY KEY, scenario_id UUID REFERENCES fp_scenarios(id) ON DELETE CASCADE, + segment_name TEXT NOT NULL, segment_index INT NOT NULL, row_label TEXT NOT NULL, row_index INT NOT NULL, + percentage NUMERIC(5,3), formula_type TEXT, is_editable BOOLEAN DEFAULT false, + values JSONB NOT NULL DEFAULT '{}', excel_row INT, sort_order INT NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() + )`, + `CREATE TABLE IF NOT EXISTS fp_kunden_summary ( + id SERIAL PRIMARY KEY, scenario_id UUID REFERENCES fp_scenarios(id) ON DELETE CASCADE, + row_label TEXT NOT NULL, row_index INT NOT NULL, values JSONB NOT NULL DEFAULT '{}', + excel_row INT, sort_order INT NOT NULL + )`, + `CREATE TABLE IF NOT EXISTS fp_umsatzerloese ( + id SERIAL PRIMARY KEY, scenario_id UUID REFERENCES fp_scenarios(id) ON DELETE CASCADE, + section TEXT NOT NULL, row_label TEXT NOT NULL, row_index INT NOT NULL, + is_editable BOOLEAN DEFAULT false, values JSONB NOT NULL DEFAULT '{}', + excel_row INT, sort_order INT NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() + )`, + `CREATE TABLE IF NOT EXISTS fp_materialaufwand ( + id SERIAL PRIMARY KEY, scenario_id UUID REFERENCES fp_scenarios(id) ON DELETE CASCADE, + section TEXT NOT NULL, row_label TEXT NOT NULL, row_index INT NOT NULL, + is_editable BOOLEAN DEFAULT false, values JSONB NOT NULL DEFAULT '{}', + excel_row INT, sort_order INT NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() + )`, + `CREATE TABLE IF NOT EXISTS fp_personalkosten ( + id SERIAL PRIMARY KEY, scenario_id UUID REFERENCES fp_scenarios(id) ON DELETE CASCADE, + person_name TEXT NOT NULL, person_nr TEXT, position TEXT, + start_date DATE, end_date DATE, brutto_monthly NUMERIC(10,2), + annual_raise_pct NUMERIC(5,2) DEFAULT 3.0, ag_sozial_pct NUMERIC(5,2) DEFAULT 20.425, + is_editable BOOLEAN DEFAULT true, + values_brutto JSONB NOT NULL DEFAULT '{}', values_sozial JSONB NOT NULL DEFAULT '{}', + values_total JSONB NOT NULL DEFAULT '{}', + excel_row INT, sort_order INT NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() + )`, + `CREATE TABLE IF NOT EXISTS fp_betriebliche_aufwendungen ( + id SERIAL PRIMARY KEY, scenario_id UUID REFERENCES fp_scenarios(id) ON DELETE CASCADE, + category TEXT NOT NULL, row_label TEXT NOT NULL, row_index INT NOT NULL, + is_editable BOOLEAN DEFAULT true, is_sum_row BOOLEAN DEFAULT false, formula_desc TEXT, + values JSONB NOT NULL DEFAULT '{}', excel_row INT, sort_order INT NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() + )`, + `CREATE TABLE IF NOT EXISTS fp_investitionen ( + id SERIAL PRIMARY KEY, scenario_id UUID REFERENCES fp_scenarios(id) ON DELETE CASCADE, + item_name TEXT NOT NULL, category TEXT, purchase_amount NUMERIC(12,2) NOT NULL, + purchase_date DATE, afa_years INT, afa_end_date DATE, is_editable BOOLEAN DEFAULT true, + values_invest JSONB NOT NULL DEFAULT '{}', values_afa JSONB NOT NULL DEFAULT '{}', + excel_row INT, sort_order INT NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() + )`, + `CREATE TABLE IF NOT EXISTS fp_sonst_ertraege ( + id SERIAL PRIMARY KEY, scenario_id UUID REFERENCES fp_scenarios(id) ON DELETE CASCADE, + category TEXT NOT NULL, row_label TEXT, row_index INT NOT NULL, + is_editable BOOLEAN DEFAULT true, is_sum_row BOOLEAN DEFAULT false, + values JSONB NOT NULL DEFAULT '{}', excel_row INT, sort_order INT NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() + )`, + `CREATE TABLE IF NOT EXISTS fp_liquiditaet ( + id SERIAL PRIMARY KEY, scenario_id UUID REFERENCES fp_scenarios(id) ON DELETE CASCADE, + row_label TEXT NOT NULL, row_type TEXT NOT NULL, + is_editable BOOLEAN DEFAULT false, formula_desc TEXT, + values JSONB NOT NULL DEFAULT '{}', excel_row INT, sort_order INT NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() + )`, + `CREATE TABLE IF NOT EXISTS fp_guv ( + id SERIAL PRIMARY KEY, scenario_id UUID REFERENCES fp_scenarios(id) ON DELETE CASCADE, + row_label TEXT NOT NULL, row_index INT NOT NULL, + is_sum_row BOOLEAN DEFAULT false, formula_desc TEXT, + values JSONB NOT NULL DEFAULT '{}', excel_row INT, sort_order INT NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() + )`, + `CREATE TABLE IF NOT EXISTS fp_cell_overrides ( + id SERIAL PRIMARY KEY, scenario_id UUID REFERENCES fp_scenarios(id) ON DELETE CASCADE, + sheet_name TEXT NOT NULL, row_id INT NOT NULL, month_key TEXT NOT NULL, + override_value NUMERIC, created_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(scenario_id, sheet_name, row_id, month_key) + )`, + `CREATE INDEX IF NOT EXISTS idx_fp_kunden_scenario ON fp_kunden(scenario_id)`, + `CREATE INDEX IF NOT EXISTS idx_fp_kunden_summary_scenario ON fp_kunden_summary(scenario_id)`, + `CREATE INDEX IF NOT EXISTS idx_fp_umsatz_scenario ON fp_umsatzerloese(scenario_id)`, + `CREATE INDEX IF NOT EXISTS idx_fp_material_scenario ON fp_materialaufwand(scenario_id)`, + `CREATE INDEX IF NOT EXISTS idx_fp_personal_scenario ON fp_personalkosten(scenario_id)`, + `CREATE INDEX IF NOT EXISTS idx_fp_betrieb_scenario ON fp_betriebliche_aufwendungen(scenario_id)`, + `CREATE INDEX IF NOT EXISTS idx_fp_invest_scenario ON fp_investitionen(scenario_id)`, + `CREATE INDEX IF NOT EXISTS idx_fp_sonst_scenario ON fp_sonst_ertraege(scenario_id)`, + `CREATE INDEX IF NOT EXISTS idx_fp_liquid_scenario ON fp_liquiditaet(scenario_id)`, + `CREATE INDEX IF NOT EXISTS idx_fp_guv_scenario ON fp_guv(scenario_id)`, + `CREATE INDEX IF NOT EXISTS idx_fp_overrides_lookup ON fp_cell_overrides(scenario_id, sheet_name, row_id)`, + ] + + for (const sql of statements) { + try { + await pool.query(sql) + const label = sql.substring(0, 60).replace(/\s+/g, ' ') + results.push(`OK: ${label}...`) + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + results.push(`ERROR: ${msg}`) + } + } + + return NextResponse.json({ success: true, results }) +}