// Commission calculation service. Single source of truth used by all provision endpoints. // // Hierarchy for shop commission (highest priority wins): // 1. Tariff-specific rule (provisionsregeln, active, gueltig_ab <= schaltungs_datum) // 2. Global provider rule (anbieter_provisionsregeln, active, gueltig_ab <= schaltungs_datum) // 3. Direct tariff amount (tarife.provision_euro) // // For type='prozent' the rule's basis_euro field is the base amount the percentage // applies to. If basis_euro is missing on a percent rule, the contribution is 0 // (rule is effectively unusable - caller should validate at write time). // // Tarifoptionen are added on top (signed: rebates are negative). // Hardware logic: when hardware is set, shop = shop - hardware_ek + kundenverkaufspreis_netto. // // Employee commission follows the same hierarchy but uses verkaeufer_* fields. // For type='prozent' the base is the computed shop commission. function num(value) { const n = parseFloat(value); return Number.isFinite(n) ? n : 0; } function pickRule(context) { // Returns the active rule that should apply, or null. // context: { tarifRule, anbieterRule, tarif } if (context.tarifRule) { return { source: 'Tarif-spezifische Regel', rule: context.tarifRule }; } if (context.anbieterRule) { return { source: 'Globale Anbieter-Regel', rule: context.anbieterRule }; } if (context.tarif && context.tarif.provision_euro != null && num(context.tarif.provision_euro) !== 0) { return { source: 'Direkte Tarif-Provision', rule: { provision_typ: 'fix', betrag: num(context.tarif.provision_euro), basis_euro: null, beschreibung: context.tarif.beschreibung || null, verkaeufer_provision_typ: null, verkaeufer_provision_betrag: null, verkaeufer_provision_beschreibung: null, }, }; } return null; } function applyShopRule(rule) { if (!rule) return 0; if (rule.provision_typ === 'fix') { return num(rule.betrag); } if (rule.provision_typ === 'prozent') { const basis = num(rule.basis_euro); return (num(rule.betrag) / 100) * basis; } return 0; } function applyEmployeeRule(rule, shopProvision) { if (!rule) return 0; if (rule.verkaeufer_provision_typ === 'fix') { return num(rule.verkaeufer_provision_betrag); } if (rule.verkaeufer_provision_typ === 'prozent') { return (num(rule.verkaeufer_provision_betrag) / 100) * shopProvision; } return 0; } function sumTarifoptionen(tarifoptionen) { if (!Array.isArray(tarifoptionen)) return { sum: 0, names: [] }; let sum = 0; const names = []; for (const opt of tarifoptionen) { const v = num(opt.provision_euro); if (v !== 0) { sum += v; names.push(opt.name); } } return { sum, names }; } // Main entry. Inputs are plain data (already loaded from DB by the route). // `schaltung` is the schaltungen row (joined with tarif basics). // `tarif` is the full tarif row. // `tarifRule` is the active provisionsregeln row (or null). // `anbieterRule` is the active anbieter_provisionsregeln row (or null). // `tarifoptionen` is an array of tarifoption rows linked to this schaltung. // `mitarbeiterProzentsatz` is a number (0-100) — used only when there is no // employee-specific rule and the tariff defines mitarbeiter_provision. function calculate({ schaltung, tarif, tarifRule, anbieterRule, tarifoptionen, mitarbeiterProzentsatz }) { const picked = pickRule({ tarifRule, anbieterRule, tarif }); const ruleSource = picked ? picked.source : 'Keine Shop-Provision definiert'; const rule = picked ? picked.rule : null; let shopProvision = applyShopRule(rule); const { sum: tarifoptionenSum, names: tarifoptionenNames } = sumTarifoptionen(tarifoptionen); shopProvision += tarifoptionenSum; let hardwareGewinn = 0; if (schaltung && schaltung.hardware && String(schaltung.hardware).trim() !== '') { hardwareGewinn = num(schaltung.kundenverkaufspreis_netto) - num(schaltung.hardware_ek); shopProvision += hardwareGewinn; } let employeeProvision = applyEmployeeRule(rule, shopProvision); let employeeSource = picked ? `${picked.source} (Verkäufer)` : 'Keine Mitarbeiter-Provision definiert'; // Fallback: if no rule defines an employee commission, use the per-anbieter // mitarbeiter_prozentsatz against the computed shop commission. if (employeeProvision === 0 && (!rule || !rule.verkaeufer_provision_typ)) { const p = num(mitarbeiterProzentsatz); if (p !== 0) { employeeProvision = (p / 100) * shopProvision; employeeSource = `${p}% Mitarbeiter-Anteil von ${shopProvision.toFixed(2)}€`; } } return { shop: { betrag: round2(shopProvision), quelle: ruleSource, typ: rule ? rule.provision_typ : null, regel_betrag: rule ? num(rule.betrag) : null, basis_euro: rule ? (rule.basis_euro != null ? num(rule.basis_euro) : null) : null, beschreibung: rule ? rule.beschreibung : null, }, mitarbeiter: { betrag: round2(employeeProvision), quelle: employeeSource, typ: rule ? rule.verkaeufer_provision_typ : null, regel_betrag: rule ? (rule.verkaeufer_provision_betrag != null ? num(rule.verkaeufer_provision_betrag) : null) : null, beschreibung: rule ? rule.verkaeufer_provision_beschreibung : null, prozentsatz: num(mitarbeiterProzentsatz), }, tarifoptionen: { betrag: round2(tarifoptionenSum), namen: tarifoptionenNames, }, hardware_gewinn: round2(hardwareGewinn), }; } function round2(n) { return Math.round(n * 100) / 100; } module.exports = { calculate, num, round2 };