Rene Fichtmueller aa977abc97 feat(v0.2.0): Sales Intelligence Engine — Phase 0+A
New API routes:
- GET /api/finder — Switch→Flexoptix transceiver finder with FlexBox coding
- GET /api/competitor-alerts — Competitor intelligence (price changes, new products, stock)
- GET /api/forecast/:technology — Sales forecast 3/9/12/18 months + buy/wait/hold signal
- POST /api/transport/plan — Transport system planner (city→city BOM with fiber providers)

New MCP tools:
- find_flexoptix_for_switch — Customer switch → Flexoptix products
- get_competitor_alerts — Competitor monitoring
- plan_transport — Network transport planning
- forecast_sales — Volume/revenue prediction
- generate_blog — Enhanced blog generation

New DB tables (migration 013):
- competitor_alerts, price_changes, flexoptix_product_map
- sales_forecasts, fiber_providers, fiber_routes, cities
- generated_datasheets, blog_series
- Views: v_price_coverage, v_image_coverage, v_switch_flexoptix_finder

Seed data (migration 014):
- 25 European cities with IX/DC locations + coordinates
- 15 fiber providers (euNetworks, Telia, DTAG, Colt, Zayo, etc.)
- 16 fiber routes with pricing (Germany focus)

Infrastructure:
- Scraper scheduler: 2h Flexoptix, 4h FS.com/Optcore (was 6-8h)
- Change detector for competitor price/stock monitoring
- Image downloader utility with coverage tracking
2026-03-31 08:51:22 +02:00

202 lines
8.1 KiB
TypeScript

/**
* WS5 + WS6: Sales Forecast Engine + Price Trajectory
*/
import { Router } from "express";
import { pool } from "../db/client";
import { computeHypeCycle, findTechnology, TECH_GENERATIONS } from "../hype-cycle/norton-bass";
export const forecastRouter = Router();
/**
* GET /api/forecast/:technology
*
* Returns sales forecast for 3/9/12/18 months + price trajectory + buy signal
*/
forecastRouter.get("/:technology", async (req, res) => {
try {
const techQuery = req.params.technology;
const currentYear = new Date().getFullYear();
// Find technology in Norton-Bass model
const tech = findTechnology(techQuery);
if (!tech) {
return res.status(404).json({
error: "Technology not found",
available: TECH_GENERATIONS.map(t => t.name),
});
}
// Compute hype cycle
const hype = computeHypeCycle(tech, currentYear);
// Get price data from DB
const priceHistory = await pool.query(`
SELECT
date_trunc('week', po.time) AS week,
AVG(po.price) AS avg_price,
MIN(po.price) AS min_price,
MAX(po.price) AS max_price,
COUNT(*) AS observations,
po.currency
FROM price_observations po
JOIN transceivers t ON po.transceiver_id = t.id
WHERE t.speed_gbps = $1
GROUP BY week, po.currency
ORDER BY week DESC
LIMIT 52
`, [tech.speedGbps]);
// Compute price trajectory based on hype cycle phase
const currentPrices = priceHistory.rows.length > 0
? priceHistory.rows.map(r => parseFloat(r.avg_price))
: [];
const currentASP = currentPrices.length > 0 ? currentPrices[0]! : tech.speedGbps * 0.5; // rough estimate
// Price decline model based on phase
const phaseDeclineRates: Record<string, number> = {
'INNOVATION_TRIGGER': 0.05,
'PEAK_OF_INFLATED_EXPECTATIONS': 0.12,
'TROUGH_OF_DISILLUSIONMENT': 0.25,
'SLOPE_OF_ENLIGHTENMENT': 0.15,
'PLATEAU_OF_PRODUCTIVITY': 0.05,
'LEGACY_DECLINE': 0.03,
};
const annualDecline = phaseDeclineRates[hype.phase] ?? 0.10;
const monthlyDecline = 1 - Math.pow(1 - annualDecline, 1/12);
const asp3m = currentASP * Math.pow(1 - monthlyDecline, 3);
const asp9m = currentASP * Math.pow(1 - monthlyDecline, 9);
const asp12m = currentASP * Math.pow(1 - monthlyDecline, 12);
const asp18m = currentASP * Math.pow(1 - monthlyDecline, 18);
// Price floor estimate (based on mature technology pricing patterns)
// Typically 15-25% of peak price at full maturity
const priceFloor = currentASP * 0.20;
const monthsToFloor = annualDecline > 0
? Math.ceil(Math.log(priceFloor / currentASP) / Math.log(1 - monthlyDecline))
: 999;
// Volume forecast based on adoption curve
const adoptionNow = hype.adoptionPct / 100;
const adoption3m = Math.min(1, adoptionNow + (hype.forecast?.[0]?.adoptionPct ?? 0) / 100 * 0.25);
const adoption9m = Math.min(1, adoptionNow + (hype.forecast?.[0]?.adoptionPct ?? 0) / 100 * 0.75);
const adoption12m = Math.min(1, adoptionNow + (hype.forecast?.[1]?.adoptionPct ?? 0) / 100);
const adoption18m = Math.min(1, adoptionNow + (hype.forecast?.[2]?.adoptionPct ?? 0) / 100);
const totalMarketPorts = tech.m * 1000000; // market potential in units
const marketShare = 0.03; // estimated Flexoptix-addressable share
const units3m = Math.round(totalMarketPorts * adoption3m * marketShare * 0.25);
const units9m = Math.round(totalMarketPorts * adoption9m * marketShare * 0.75);
const units12m = Math.round(totalMarketPorts * adoption12m * marketShare);
const units18m = Math.round(totalMarketPorts * adoption18m * marketShare * 1.5);
// Confidence decreases with forecast horizon
const conf3m = Math.min(0.95, 0.85 + (priceHistory.rows.length / 100));
const conf9m = conf3m * 0.78;
const conf12m = conf3m * 0.65;
const conf18m = conf3m * 0.50;
// Buy signal
let buySignal: string;
let signalReason: string;
if (hype.phase === 'SLOPE_OF_ENLIGHTENMENT' || hype.phase === 'PLATEAU_OF_PRODUCTIVITY') {
buySignal = 'BUY_NOW';
signalReason = `${tech.name} is in ${hype.phase.replace(/_/g, ' ').toLowerCase()} — prices near floor, volume growing, stable supply chain.`;
} else if (hype.phase === 'TROUGH_OF_DISILLUSIONMENT') {
buySignal = 'WAIT';
signalReason = `${tech.name} prices dropping >10%/quarter. Wait for trough bottom (estimated ${Math.ceil(monthsToFloor * 0.3)} months).`;
} else if (hype.phase === 'PEAK_OF_INFLATED_EXPECTATIONS') {
buySignal = 'WAIT';
signalReason = `${tech.name} is at peak hype — prices will drop significantly. Only buy if urgent.`;
} else if (hype.phase === 'INNOVATION_TRIGGER') {
buySignal = 'HOLD';
signalReason = `${tech.name} is early-stage — limited availability, premium pricing. Wait unless you need bleeding-edge.`;
} else {
buySignal = 'HOLD';
signalReason = `${tech.name} is in legacy/decline — consider migrating to next generation.`;
}
// Store forecast in DB
await pool.query(`
INSERT INTO sales_forecasts (
technology, speed_gbps, form_factor,
forecast_3m_units, forecast_3m_revenue, forecast_9m_units, forecast_9m_revenue,
forecast_12m_units, forecast_12m_revenue, forecast_18m_units, forecast_18m_revenue,
current_asp, asp_3m, asp_12m, price_floor, months_to_floor,
confidence_3m, confidence_9m, confidence_12m, confidence_18m,
buy_signal, signal_reason, data_points
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23)
`, [
tech.name, tech.speedGbps, tech.formFactor,
units3m, units3m * asp3m, units9m, units9m * asp9m,
units12m, units12m * asp12m, units18m, units18m * asp18m,
currentASP, asp3m, asp12m, priceFloor, monthsToFloor,
conf3m, conf9m, conf12m, conf18m,
buySignal, signalReason, priceHistory.rows.length,
]).catch(() => {}); // Non-critical
res.json({
technology: tech.name,
speed_gbps: tech.speedGbps,
form_factor: tech.formFactor,
hype_cycle: {
phase: hype.phase,
position_pct: hype.positionPct,
adoption_pct: hype.adoptionPct,
},
forecasts: {
"3_months": { units: units3m, revenue_eur: Math.round(units3m * asp3m), confidence: Math.round(conf3m * 100) / 100 },
"9_months": { units: units9m, revenue_eur: Math.round(units9m * asp9m), confidence: Math.round(conf9m * 100) / 100 },
"12_months": { units: units12m, revenue_eur: Math.round(units12m * asp12m), confidence: Math.round(conf12m * 100) / 100 },
"18_months": { units: units18m, revenue_eur: Math.round(units18m * asp18m), confidence: Math.round(conf18m * 100) / 100 },
},
price_trajectory: {
current_asp: Math.round(currentASP * 100) / 100,
asp_3m: Math.round(asp3m * 100) / 100,
asp_9m: Math.round(asp9m * 100) / 100,
asp_12m: Math.round(asp12m * 100) / 100,
asp_18m: Math.round(asp18m * 100) / 100,
price_floor: Math.round(priceFloor * 100) / 100,
months_to_floor: Math.max(0, monthsToFloor),
annual_decline_pct: Math.round(annualDecline * 100),
},
buy_signal: {
signal: buySignal,
reason: signalReason,
},
price_history: priceHistory.rows.slice(0, 12),
model: "Norton-Bass Multigenerational Diffusion v1",
});
} catch (err) {
console.error("Forecast error:", err);
res.status(500).json({ error: "Internal server error" });
}
});
/**
* GET /api/forecast
*
* Overview of all technology forecasts
*/
forecastRouter.get("/", async (_req, res) => {
try {
const currentYear = new Date().getFullYear();
const results = TECH_GENERATIONS.map(tech => {
const hype = computeHypeCycle(tech, currentYear);
return {
technology: tech.name,
speed_gbps: tech.speedGbps,
form_factor: tech.formFactor,
phase: hype.phase,
adoption_pct: hype.adoptionPct,
position_pct: hype.positionPct,
};
});
res.json({ technologies: results });
} catch (err) {
res.status(500).json({ error: "Internal server error" });
}
});