const sqlite3 = require('sqlite3').verbose(); const path = require('path'); const dbPath = path.join(__dirname, 'provision.db'); let dbInstance = null; function getDatabase() { if (!dbInstance) { dbInstance = new sqlite3.Database(dbPath, (err) => { if (err) { console.error('Error opening database:', err.message); dbInstance = null; throw err; } console.log('Connected to SQLite database'); }); dbInstance.configure('busyTimeout', 3000); } return dbInstance; } function enableForeignKeys(db) { db.run('PRAGMA foreign_keys = ON', (err) => { if (err) { console.error('Error enabling foreign keys:', err.message); } }); } function closeDatabase() { if (dbInstance) { dbInstance.close((err) => { if (err) { console.error('Error closing database:', err.message); } else { console.log('Database connection closed'); } dbInstance = null; }); } } // Best-effort ALTER TABLE: ignores "duplicate column" errors but logs others function addColumnIfMissing(db, table, columnDef) { db.run(`ALTER TABLE ${table} ADD COLUMN ${columnDef}`, (err) => { if (err && !/duplicate column name/i.test(err.message)) { console.error(`ALTER TABLE ${table} ADD ${columnDef}:`, err.message); } }); } function initDatabase() { const db = getDatabase(); db.serialize(() => { // Anbieter (Providers). default_vo_id picks which of this provider's // VOs is preselected in the schaltung wizard. db.run(`CREATE TABLE IF NOT EXISTS anbieter ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, default_vo_id INTEGER, created_at DATETIME DEFAULT CURRENT_TIMESTAMP )`); addColumnIfMissing(db, 'anbieter', 'default_vo_id INTEGER'); // Mitarbeiter-Prozentsätze pro Anbieter db.run(`CREATE TABLE IF NOT EXISTS mitarbeiter_prozentsaetze ( id INTEGER PRIMARY KEY AUTOINCREMENT, anbieter_id INTEGER NOT NULL, prozentsatz DECIMAL(5,2) NOT NULL DEFAULT 0, gueltig_ab DATE DEFAULT CURRENT_DATE, beschreibung TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (anbieter_id) REFERENCES anbieter (id), UNIQUE(anbieter_id, gueltig_ab) )`); // Vertriebspartner-Nummern (VOs) per Anbieter. Each tariff is assigned to // exactly one VO (nullable for legacy rows that pre-date this column). db.run(`CREATE TABLE IF NOT EXISTS vos ( id INTEGER PRIMARY KEY AUTOINCREMENT, anbieter_id INTEGER NOT NULL, nummer TEXT NOT NULL, bezeichnung TEXT, aktiv INTEGER NOT NULL DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (anbieter_id) REFERENCES anbieter (id), UNIQUE(anbieter_id, nummer) )`); // Tarife db.run(`CREATE TABLE IF NOT EXISTS tarife ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, anbieter_id INTEGER, vo_id INTEGER, provision_euro DECIMAL(10,2) DEFAULT 0, mitarbeiter_provision DECIMAL(10,2) DEFAULT 0, beschreibung TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (anbieter_id) REFERENCES anbieter (id), FOREIGN KEY (vo_id) REFERENCES vos (id), UNIQUE(name, anbieter_id) )`); addColumnIfMissing(db, 'tarife', 'mitarbeiter_provision DECIMAL(10,2) DEFAULT 0'); addColumnIfMissing(db, 'tarife', 'vo_id INTEGER'); // Tariff-specific commission rules. basis_euro: base amount for percent calculation. db.run(`CREATE TABLE IF NOT EXISTS provisionsregeln ( id INTEGER PRIMARY KEY AUTOINCREMENT, tarif_id INTEGER NOT NULL, provision_typ TEXT NOT NULL CHECK(provision_typ IN ('fix', 'prozent')), betrag DECIMAL(10,2) NOT NULL, basis_euro DECIMAL(10,2), beschreibung TEXT, verkaeufer_provision_typ TEXT CHECK(verkaeufer_provision_typ IN ('fix', 'prozent')), verkaeufer_provision_betrag DECIMAL(10,2), verkaeufer_provision_beschreibung TEXT, gueltig_ab DATE DEFAULT CURRENT_DATE, aktiv BOOLEAN DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (tarif_id) REFERENCES tarife (id) )`); addColumnIfMissing(db, 'provisionsregeln', 'basis_euro DECIMAL(10,2)'); // Global provider commission rules db.run(`CREATE TABLE IF NOT EXISTS anbieter_provisionsregeln ( id INTEGER PRIMARY KEY AUTOINCREMENT, anbieter_id INTEGER NOT NULL, provision_typ TEXT NOT NULL CHECK(provision_typ IN ('fix', 'prozent')), betrag DECIMAL(10,2) NOT NULL, basis_euro DECIMAL(10,2), beschreibung TEXT, verkaeufer_provision_typ TEXT CHECK(verkaeufer_provision_typ IN ('fix', 'prozent')), verkaeufer_provision_betrag DECIMAL(10,2), verkaeufer_provision_beschreibung TEXT, gueltig_ab DATE DEFAULT CURRENT_DATE, aktiv BOOLEAN DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (anbieter_id) REFERENCES anbieter (id) )`); addColumnIfMissing(db, 'anbieter_provisionsregeln', 'basis_euro DECIMAL(10,2)'); // Schaltungen. rufnummer is nullable on new installs (the application // also accepts NULL on existing DBs with NOT NULL columns since we just // pass empty string when missing). vo_id captures which VO was used. db.run(`CREATE TABLE IF NOT EXISTS schaltungen ( id INTEGER PRIMARY KEY AUTOINCREMENT, schaltungs_id TEXT NOT NULL UNIQUE, eingabe_datum DATE NOT NULL, schaltungs_datum DATE NOT NULL, vorname TEXT NOT NULL, nachname TEXT NOT NULL, firma TEXT, rufnummer TEXT, anbieter_id INTEGER NOT NULL, vo_id INTEGER, tarif_id INTEGER NOT NULL, hardware TEXT, hardware_ek DECIMAL(10,2), kundenverkaufspreis_netto DECIMAL(10,2), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (anbieter_id) REFERENCES anbieter (id), FOREIGN KEY (tarif_id) REFERENCES tarife (id) )`); addColumnIfMissing(db, 'schaltungen', 'hardware_ek DECIMAL(10,2)'); addColumnIfMissing(db, 'schaltungen', 'kundenverkaufspreis_netto DECIMAL(10,2)'); addColumnIfMissing(db, 'schaltungen', 'vo_id INTEGER'); // Dokumente (note: ON DELETE CASCADE only applied to new DBs - existing DBs // handled via explicit DELETE in server.js) db.run(`CREATE TABLE IF NOT EXISTS dokumente ( id INTEGER PRIMARY KEY AUTOINCREMENT, schaltung_id INTEGER NOT NULL, filename TEXT NOT NULL, original_filename TEXT NOT NULL, filepath TEXT NOT NULL, filesize INTEGER, mimetype TEXT, uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (schaltung_id) REFERENCES schaltungen (id) ON DELETE CASCADE )`); // Soft-deleted schaltungen db.run(`CREATE TABLE IF NOT EXISTS geloeschte_schaltungen ( id INTEGER PRIMARY KEY AUTOINCREMENT, original_id INTEGER NOT NULL, schaltungs_id TEXT NOT NULL, eingabe_datum DATE NOT NULL, schaltungs_datum DATE NOT NULL, vorname TEXT NOT NULL, nachname TEXT NOT NULL, firma TEXT, rufnummer TEXT NOT NULL, anbieter_id INTEGER NOT NULL, tarif_id INTEGER NOT NULL, hardware TEXT, anbieter_name TEXT NOT NULL, tarif_name TEXT NOT NULL, loeschgrund TEXT NOT NULL, geloescht_am DATETIME DEFAULT CURRENT_TIMESTAMP, geloescht_von TEXT DEFAULT 'System' )`); // Tarifoptionen - schema matches production: anbieter_id required, tarif_id optional // (NULL = anbieter-wide), unique per (anbieter_id, tarif_id, name). db.run(`CREATE TABLE IF NOT EXISTS tarifoptionen ( id INTEGER PRIMARY KEY AUTOINCREMENT, anbieter_id INTEGER NOT NULL, tarif_id INTEGER, name TEXT NOT NULL, code TEXT, provision_euro DECIMAL(10,2) DEFAULT 0, beschreibung TEXT, aktiv BOOLEAN DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (anbieter_id) REFERENCES anbieter (id), FOREIGN KEY (tarif_id) REFERENCES tarife (id), UNIQUE(anbieter_id, tarif_id, name) )`); // M:N variant kept for the /multi endpoints db.run(`CREATE TABLE IF NOT EXISTS tarifoptionen_new ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, code TEXT, provision_euro DECIMAL(10,2) DEFAULT 0, beschreibung TEXT, aktiv BOOLEAN DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP )`); db.run(`CREATE TABLE IF NOT EXISTS tarifoptionen_tarife ( id INTEGER PRIMARY KEY AUTOINCREMENT, tarifoption_id INTEGER NOT NULL, tarif_id INTEGER NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (tarifoption_id) REFERENCES tarifoptionen_new (id) ON DELETE CASCADE, FOREIGN KEY (tarif_id) REFERENCES tarife (id) ON DELETE CASCADE, UNIQUE(tarifoption_id, tarif_id) )`); // Link table: which tarifoptionen are assigned to which schaltung db.run(`CREATE TABLE IF NOT EXISTS schaltungen_tarifoptionen ( id INTEGER PRIMARY KEY AUTOINCREMENT, schaltung_id INTEGER NOT NULL, tarifoption_id INTEGER NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (schaltung_id) REFERENCES schaltungen (id) ON DELETE CASCADE, FOREIGN KEY (tarifoption_id) REFERENCES tarifoptionen (id), UNIQUE(schaltung_id, tarifoption_id) )`); // Users for authentication. uuid is the stable external identifier exposed // through the API so usernames can be renamed without breaking references. db.run(`CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, uuid TEXT UNIQUE, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, display_name TEXT, is_admin INTEGER NOT NULL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP )`); addColumnIfMissing(db, 'users', 'uuid TEXT'); // Backfill UUIDs for rows that pre-date the column. db.all('SELECT id FROM users WHERE uuid IS NULL OR uuid = ""', (err, rows) => { if (err) { console.error('Backfill uuid lookup failed:', err.message); return; } if (!rows || rows.length === 0) return; const { randomUUID } = require('crypto'); for (const row of rows) { db.run('UPDATE users SET uuid = ? WHERE id = ?', [randomUUID(), row.id], (uerr) => { if (uerr) console.error('Backfill uuid update failed:', uerr.message); }); } }); // Seed data (idempotent) db.run(`INSERT OR IGNORE INTO anbieter (name) VALUES ('Telekom'), ('Vodafone'), ('O2'), ('1&1')`); db.run(`INSERT OR IGNORE INTO tarife (name, anbieter_id, provision_euro, beschreibung) VALUES ('MagentaMobil S', 1, 150.00, 'Telekom Basis-Tarif'), ('MagentaMobil M', 1, 200.00, 'Telekom Standard-Tarif'), ('MagentaMobil L', 1, 250.00, 'Telekom Premium-Tarif'), ('VF green LTE 10GB mit Smartphone 5', 2, 190.00, 'Vodafone Standard-Paket'), ('Red S', 2, 120.00, 'Vodafone Einsteiger-Tarif'), ('Red M', 2, 160.00, 'Vodafone Standard-Tarif'), ('Red L', 2, 200.00, 'Vodafone Premium-Tarif'), ('o2 Free S', 3, 100.00, 'O2 Basis-Tarif'), ('o2 Free M', 3, 140.00, 'O2 Standard-Tarif'), ('o2 Free L', 3, 180.00, 'O2 Premium-Tarif')`); db.run(`INSERT OR IGNORE INTO mitarbeiter_prozentsaetze (anbieter_id, prozentsatz, beschreibung) VALUES (1, 8.00, 'Telekom Mitarbeiter-Provision'), (2, 10.00, 'Vodafone/Freenet Mitarbeiter-Provision'), (3, 7.50, 'O2 Mitarbeiter-Provision'), (4, 9.00, '1&1 Mitarbeiter-Provision')`); db.run(`INSERT OR IGNORE INTO anbieter_provisionsregeln (anbieter_id, provision_typ, betrag, beschreibung) VALUES (3, 'fix', 20.00, 'Standard O2 Provision'), (4, 'prozent', 12.00, '12% Standard 1&1 Provision')`); // Seed tarifoptionen (correct schema: anbieter_id required) db.run(`INSERT OR IGNORE INTO tarifoptionen (anbieter_id, tarif_id, name, code, provision_euro, beschreibung) VALUES (1, 1, 'Magenta Premium Option', 'MAG-PREM', 50.00, 'Premium-Zusatz für Telekom'), (1, 1, 'Aktionscode Neukunde', 'NEU2024', 30.00, 'Neukunden-Bonus'), (2, 4, '24 x 17€ MGP Rabatt GK A3958457', 'A3958457', -120.00, 'Vodafone Rabatt-Aktion'), (2, 5, 'Red M Aktion Q1', 'RED-M-Q1', 25.00, 'Quartals-Aktion'), (2, 6, 'Red L Premium Paket', 'RED-L-PREM', 35.00, 'Premium-Paket')`); db.run(`INSERT OR IGNORE INTO provisionsregeln (tarif_id, provision_typ, betrag, beschreibung) VALUES (1, 'fix', 25.00, 'Feste Provision für MagentaMobil S'), (2, 'fix', 35.00, 'Feste Provision für MagentaMobil M'), (3, 'fix', 45.00, 'Feste Provision für MagentaMobil L')`); }); enableForeignKeys(db); } module.exports = { initDatabase, getDatabase, closeDatabase };