/** * Supplier voucher service. * After an order is placed, splits order items by supplier and sends each * supplier an HTML email voucher listing the items they need to provide. */ import pool from '../db/index.js'; import { sendEmail } from './mailer.js'; import { getHingeCountForDoor } from '../../shared/manufacturing.js'; /** * Resolve which supplier (if any) provides the finish/panel material. */ async function resolveFinishSupplier(finishId) { if (!finishId) return null; // Try formex_materials first (linked via finishes → formex_materials) const { rows } = await pool.query( `SELECT fm.id, fm.formex_code AS code, fm.name, fm.price_chf, s.id AS supplier_id, s.name AS supplier_name, s.email AS supplier_email, s.code AS supplier_code FROM formex_materials fm JOIN suppliers s ON s.id = fm.supplier_id WHERE fm.id = $1 AND fm.active = 1 AND s.active = 1`, [finishId] ); if (rows.length > 0) return { supplier: rows[0], item: rows[0], category: 'panel' }; return null; } /** * Resolve drawer system supplier. */ async function resolveBlumHardware(hardwareId, type) { if (!hardwareId) return null; const { rows } = await pool.query( `SELECT bh.id, bh.code, bh.name, bh.price_chf, bh.type, s.id AS supplier_id, s.name AS supplier_name, s.email AS supplier_email, s.code AS supplier_code FROM blum_hardware bh JOIN suppliers s ON s.id = bh.supplier_id WHERE bh.id = $1 AND bh.active = 1 AND s.active = 1`, [hardwareId] ); if (rows.length > 0) return { supplier: rows[0], item: rows[0], category: type }; return null; } /** * Resolve handle supplier. */ async function resolveHandle(handleId) { if (!handleId) return null; const { rows } = await pool.query( `SELECT hv.id, hv.code, hv.name, hv.price_chf, s.id AS supplier_id, s.name AS supplier_name, s.email AS supplier_email, s.code AS supplier_code FROM handle_variants hv JOIN suppliers s ON s.id = hv.supplier_id WHERE hv.id = $1 AND hv.active = 1 AND s.active = 1`, [handleId] ); if (rows.length > 0) return { supplier: rows[0], item: rows[0], category: 'handle' }; return null; } /** * Resolve hinge supplier. */ async function resolveHinge(hingeId) { if (!hingeId) return null; const { rows } = await pool.query( `SELECT hv.id, hv.code, hv.name, hv.price_chf, s.id AS supplier_id, s.name AS supplier_name, s.email AS supplier_email, s.code AS supplier_code FROM hinge_variants hv JOIN suppliers s ON s.id = hv.supplier_id WHERE hv.id = $1 AND hv.active = 1 AND s.active = 1`, [hingeId] ); if (rows.length > 0) return { supplier: rows[0], item: rows[0], category: 'hinge' }; return null; } /** * Estimate quantity for each item based on the order config. */ function estimateQuantity(category, config) { const { modules = [], blocks = [], doorType, slidingPanelsCount } = config; const itemCount = modules.length || blocks.length || 1; switch (category) { case 'panel': return 1; // quantity expressed in panels by the price engine; we report 1 "lot" case 'drawer_system': return modules.reduce((sum, m) => sum + (m.drawers || 0), 0) || 1; case 'hinge': if (doorType === 'sliding') return 0; return itemCount * getHingeCountForDoor({ heightMm: (config.heightMm || 2400) - (config.plinthHeightMm || 100) - 4 }); case 'handle': return doorType === 'sliding' ? (slidingPanelsCount || 2) : itemCount; case 'runner': case 'lift_system': return 1; default: return 1; } } /** * Build an HTML voucher email body for a single supplier. */ function buildVoucherHtml({ orderId, orderDate, customerName, supplierName, items }) { const formattedDate = new Date(orderDate).toLocaleDateString('fr-CH', { day: '2-digit', month: '2-digit', year: 'numeric' }); const subtotal = items.reduce((sum, it) => sum + (it.unitPrice * it.qty), 0); const rows = items.map(it => `