/** * WS3: Transport System Planner * * "Berlin to Darmstadt, 100G" → complete BOM with switches, fiber providers, Flexoptix transceivers */ import { Router } from "express"; import { pool } from "../db/client"; export const transportRouter = Router(); // Haversine distance calculation function haversineKm(lat1: number, lon1: number, lat2: number, lon2: number): number { const R = 6371; const dLat = (lat2 - lat1) * Math.PI / 180; const dLon = (lon2 - lon1) * Math.PI / 180; const a = Math.sin(dLat/2)**2 + Math.cos(lat1*Math.PI/180) * Math.cos(lat2*Math.PI/180) * Math.sin(dLon/2)**2; return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); } /** * POST /api/transport/plan * Body: { from, to, bandwidth_gbps, redundancy?, budget_preference? } */ transportRouter.post("/plan", async (req, res) => { try { const { from, to, bandwidth_gbps = 100, redundancy = false, budget_preference = "balanced" } = req.body; if (!from || !to) { return res.status(400).json({ error: "Parameters 'from' and 'to' are required" }); } // 1. Resolve cities const cityA = await pool.query(`SELECT * FROM cities WHERE name ILIKE $1 LIMIT 1`, [from]); const cityB = await pool.query(`SELECT * FROM cities WHERE name ILIKE $1 LIMIT 1`, [to]); if (!cityA.rows[0] || !cityB.rows[0]) { const allCities = await pool.query(`SELECT name, country FROM cities ORDER BY name`); return res.status(404).json({ error: `City not found: ${!cityA.rows[0] ? from : to}`, available_cities: allCities.rows.map(c => `${c.name} (${c.country})`), }); } const a = cityA.rows[0]; const b = cityB.rows[0]; // 2. Calculate distance const straightKm = haversineKm(parseFloat(a.lat), parseFloat(a.lon), parseFloat(b.lat), parseFloat(b.lon)); const fiberKm = Math.round(straightKm * 1.4); // fiber route multiplier // 3. Determine transceiver requirements based on distance const transceiverOptions = determineTransceiverOptions(fiberKm, bandwidth_gbps); // 4. Find fiber providers for this route const providers = await pool.query( `SELECT fp.name, fp.website, fp.type, fp.products, fr.product_type, fr.monthly_price_eur, fr.setup_fee_eur, fr.min_contract_months FROM fiber_routes fr JOIN fiber_providers fp ON fr.provider_id = fp.id WHERE (fr.city_a ILIKE $1 AND fr.city_b ILIKE $2) OR (fr.city_a ILIKE $2 AND fr.city_b ILIKE $1) OR (fr.city_a ILIKE $1 AND fr.city_b ILIKE 'Frankfurt%') OR (fr.city_a ILIKE 'Frankfurt%' AND fr.city_b ILIKE $2) ORDER BY fr.monthly_price_eur ASC NULLS LAST`, [from, to] ); // 5. Find matching switches const switchOptions = await pool.query( `SELECT sw.id, sw.model, sw.series, sw.max_speed_gbps, sw.switching_capacity_tbps, sw.ports_config, sw.msrp_usd, v.name AS vendor FROM switches sw JOIN vendors v ON sw.vendor_id = v.id WHERE sw.max_speed_gbps >= $1 AND sw.lifecycle_status NOT IN ('End-of-Life', 'End-of-Sale') ORDER BY sw.msrp_usd ASC NULLS LAST, sw.max_speed_gbps DESC LIMIT 10`, [bandwidth_gbps] ); // 6. Find Flexoptix transceivers for each option const options = []; for (const tcvrOpt of transceiverOptions) { const flexoptix = await pool.query( `SELECT t.id, t.slug, t.form_factor, t.speed_gbps, t.reach_label, t.reach_meters, t.fiber_type, t.connector, t.image_url, (SELECT po.price FROM price_observations po WHERE po.transceiver_id = t.id ORDER BY po.time DESC LIMIT 1) AS price, (SELECT po.currency FROM price_observations po WHERE po.transceiver_id = t.id ORDER BY po.time DESC LIMIT 1) AS currency FROM transceivers t JOIN vendors v ON t.vendor_id = v.id WHERE t.speed_gbps >= $1 AND t.reach_meters >= $2 AND t.fiber_type = 'SMF' AND v.slug = 'flexoptix' ORDER BY t.speed_gbps ASC, t.reach_meters ASC LIMIT 5`, [tcvrOpt.speed_gbps, tcvrOpt.min_reach_m] ); // If no Flexoptix match, find any compatible transceiver const anyMatch = flexoptix.rows.length > 0 ? flexoptix.rows : (await pool.query( `SELECT t.id, t.slug, t.form_factor, t.speed_gbps, t.reach_label, t.reach_meters, t.fiber_type, t.connector, t.image_url, v.name AS vendor, (SELECT po.price FROM price_observations po WHERE po.transceiver_id = t.id ORDER BY po.time DESC LIMIT 1) AS price FROM transceivers t JOIN vendors v ON t.vendor_id = v.id WHERE t.speed_gbps >= $1 AND t.reach_meters >= $2 AND t.fiber_type = 'SMF' ORDER BY t.speed_gbps ASC LIMIT 5`, [tcvrOpt.speed_gbps, tcvrOpt.min_reach_m] )).rows; const spanCount = Math.ceil(fiberKm * 1000 / tcvrOpt.max_span_m); const tcvrCount = redundancy ? spanCount * 4 : spanCount * 2; // 2 per span (both ends), x2 for redundancy const tcvrPrice = anyMatch[0]?.price ? parseFloat(anyMatch[0].price) : tcvrOpt.est_price_eur; const totalTcvrCost = tcvrCount * tcvrPrice; options.push({ name: tcvrOpt.name, description: tcvrOpt.description, transceiver: { type: `${tcvrOpt.speed_gbps}G ${tcvrOpt.reach_label}`, form_factor: tcvrOpt.form_factor, spans_needed: spanCount, units_needed: tcvrCount, unit_price_est: tcvrPrice, total_cost_est: totalTcvrCost, flexoptix_products: anyMatch.map(m => ({ slug: m.slug, speed: m.speed_gbps + 'G', reach: m.reach_label, price: m.price ? parseFloat(m.price) : null, buy_url: `https://www.flexoptix.net/en/catalogsearch/result/?q=${encodeURIComponent(m.form_factor + ' ' + m.speed_gbps + 'G ' + m.reach_label)}`, })), }, switches: switchOptions.rows.slice(0, 3).map(sw => ({ model: sw.model, vendor: sw.vendor, max_speed: sw.max_speed_gbps + 'G', price_est: sw.msrp_usd ? parseFloat(sw.msrp_usd) : null, })), fiber_providers: providers.rows.length > 0 ? providers.rows : [ { name: "Contact local fiber providers", note: `No pre-seeded routes for ${from}↔${to}. Check euNetworks, Telia, DTAG.` } ], }); } res.json({ route: { from: a.name, to: b.name, straight_line_km: Math.round(straightKm), estimated_fiber_km: fiberKm, bandwidth_requested: bandwidth_gbps + 'G', redundancy, }, options, note: "Prices are estimates. Contact Flexoptix sales for volume pricing.", }); } catch (err) { console.error("Transport planner error:", err); res.status(500).json({ error: "Internal server error" }); } }); function determineTransceiverOptions(fiberKm: number, bandwidthGbps: number) { const options = []; if (fiberKm <= 2) { options.push({ name: `${bandwidthGbps}G FR (2km)`, description: `Short reach — single span, no amplification needed`, speed_gbps: bandwidthGbps, reach_label: 'FR', form_factor: bandwidthGbps >= 400 ? 'QSFP-DD' : 'QSFP28', min_reach_m: 2000, max_span_m: 2000, est_price_eur: bandwidthGbps >= 400 ? 200 : 80, }); } if (fiberKm <= 10) { options.push({ name: `${bandwidthGbps}G LR4 (10km)`, description: `Metro reach — ${Math.ceil(fiberKm / 10)} span(s)`, speed_gbps: bandwidthGbps, reach_label: 'LR4', form_factor: bandwidthGbps >= 400 ? 'QSFP-DD' : 'QSFP28', min_reach_m: 10000, max_span_m: 10000, est_price_eur: bandwidthGbps >= 400 ? 400 : 120, }); } if (fiberKm <= 40) { options.push({ name: `${bandwidthGbps}G ER4 (40km)`, description: `Extended reach — ${Math.ceil(fiberKm / 40)} span(s)`, speed_gbps: bandwidthGbps, reach_label: 'ER4', form_factor: bandwidthGbps >= 400 ? 'QSFP-DD' : 'QSFP28', min_reach_m: 40000, max_span_m: 40000, est_price_eur: bandwidthGbps >= 400 ? 1500 : 400, }); } // ZR is always an option for long distances if (fiberKm > 10) { options.push({ name: `${Math.min(bandwidthGbps, 400)}G ZR Coherent (80km/span)`, description: `Coherent DWDM — ${Math.ceil(fiberKm / 80)} span(s), OIF 400ZR`, speed_gbps: Math.min(bandwidthGbps, 400), reach_label: 'ZR', form_factor: 'QSFP-DD', min_reach_m: 80000, max_span_m: 80000, est_price_eur: 2500, }); } // Carrier wavelength option options.push({ name: `Carrier Wavelength Service (${bandwidthGbps}G)`, description: `Managed service — provider handles fiber + amplification. You only need LR4 transceivers at each end.`, speed_gbps: bandwidthGbps, reach_label: 'LR4', form_factor: bandwidthGbps >= 400 ? 'QSFP-DD' : 'QSFP28', min_reach_m: 10000, max_span_m: 999000, est_price_eur: bandwidthGbps >= 400 ? 400 : 120, }); return options; } /** * GET /api/transport/cities */ transportRouter.get("/cities", async (_req, res) => { try { const result = await pool.query(`SELECT name, country, has_ix, ix_names, has_datacenter FROM cities ORDER BY name`); res.json({ cities: result.rows, total: result.rowCount }); } catch (err) { res.status(500).json({ error: "Internal server error" }); } }); /** * GET /api/transport/providers */ transportRouter.get("/providers", async (_req, res) => { try { const result = await pool.query(`SELECT * FROM fiber_providers ORDER BY name`); res.json({ providers: result.rows, total: result.rowCount }); } catch (err) { res.status(500).json({ error: "Internal server error" }); } });