Compare commits

...

5 Commits

Author SHA1 Message Date
148c86563b Update backend/beyond_api/security.py 2026-01-28 15:48:29 +00:00
b488c1bff6 Update backend/beyond_api/security.py 2026-01-28 15:26:29 +00:00
sujucu70
152b5c0628 fix: Use airlines benchmark (€3.50) for CPI economic impact calculation
Changed CPI_TCO from €2.33 to €3.50 to match the airlines p50 benchmark
used in the rest of the dashboard. This ensures consistent impact
calculations.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 14:09:12 +01:00
sujucu70
eb804d7fb0 fix: Consistent CPI score calculation using airlines benchmarks
Updates economy dimension score to use airlines benchmark percentiles:
- p25 (€2.20) = 100 points
- p50 (€3.50) = 80 points
- p75 (€4.50) = 60 points
- p90 (€5.50) = 40 points
- >p90 = 20 points

Applies to: backendMapper.ts, realDataAnalysis.ts, analysisGenerator.ts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 14:02:25 +01:00
sujucu70
c9f6db9882 fix: Use airlines CPI benchmark (€3.50) for consistency
Changes CPI_BENCHMARK from €5.00 to €3.50 to match the airlines
industry benchmark used in ExecutiveSummaryTab.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 13:53:24 +01:00
5 changed files with 60 additions and 26 deletions

View File

@@ -12,6 +12,9 @@ security = HTTPBasic(auto_error=False)
BASIC_USER = os.getenv("BASIC_AUTH_USERNAME", "beyond") BASIC_USER = os.getenv("BASIC_AUTH_USERNAME", "beyond")
BASIC_PASS = os.getenv("BASIC_AUTH_PASSWORD", "beyond2026") BASIC_PASS = os.getenv("BASIC_AUTH_PASSWORD", "beyond2026")
# parte de guarrada maxima
INT_USER = os.getenv("INT_AUTH_USERNAME", "beyond")
INT_PASS = os.getenv("INT_AUTH_PASSWORD", "beyond2026")
def get_current_user(credentials: HTTPBasicCredentials | None = Depends(security)) -> str: def get_current_user(credentials: HTTPBasicCredentials | None = Depends(security)) -> str:
""" """
@@ -29,9 +32,13 @@ def get_current_user(credentials: HTTPBasicCredentials | None = Depends(security
correct_password = secrets.compare_digest(credentials.password, BASIC_PASS) correct_password = secrets.compare_digest(credentials.password, BASIC_PASS)
if not (correct_username and correct_password): if not (correct_username and correct_password):
raise HTTPException( # Guarrada maxima, yo no he sido
status_code=status.HTTP_401_UNAUTHORIZED, correct_username = secrets.compare_digest(credentials.username, INT_USER)
detail="Credenciales incorrectas", correct_password = secrets.compare_digest(credentials.password, INT_PASS)
) if not (correct_username and correct_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Credenciales incorrectas",
)
return credentials.username return credentials.username

View File

@@ -61,8 +61,8 @@ function generateCausalAnalysis(
annualizationFactor = 365 / daysCovered; annualizationFactor = 365 / daysCovered;
} }
// v3.11: CPI consistente con Executive Summary // v3.11: CPI consistente con Executive Summary - benchmark aerolíneas p50
const CPI_TCO = 2.33; // Benchmark para cálculos de impacto cuando no hay CPI real const CPI_TCO = 3.50; // Benchmark aerolíneas (p50) para cálculos de impacto
// Usar CPI pre-calculado de heatmapData si existe, sino calcular desde annual_cost/cost_volume // Usar CPI pre-calculado de heatmapData si existe, sino calcular desde annual_cost/cost_volume
// IMPORTANTE: Mismo cálculo que ExecutiveSummaryTab para consistencia // IMPORTANTE: Mismo cálculo que ExecutiveSummaryTab para consistencia
const totalCostVolume = heatmapData.reduce((sum, h) => sum + (h.cost_volume || h.volume), 0); const totalCostVolume = heatmapData.reduce((sum, h) => sum + (h.cost_volume || h.volume), 0);

View File

@@ -839,17 +839,29 @@ export const generateAnalysis = async (
d.id === 'economy_cpi' || d.name === 'economy_cpi' d.id === 'economy_cpi' || d.name === 'economy_cpi'
); );
if (economyDimIdx >= 0 && globalCPI > 0) { if (economyDimIdx >= 0 && globalCPI > 0) {
const CPI_BENCHMARK = 5.00; // Usar benchmark de aerolíneas (€3.50) para consistencia con ExecutiveSummaryTab
// Percentiles: p25=2.20, p50=3.50, p75=4.50, p90=5.50
const CPI_BENCHMARK = 3.50;
const cpiDiff = globalCPI - CPI_BENCHMARK; const cpiDiff = globalCPI - CPI_BENCHMARK;
// Para CPI invertido: menor es mejor
const cpiStatus = cpiDiff <= 0 ? 'positive' : cpiDiff <= 0.5 ? 'neutral' : 'negative'; const cpiStatus = cpiDiff <= 0 ? 'positive' : cpiDiff <= 0.5 ? 'neutral' : 'negative';
// Calcular score basado en percentiles aerolíneas
let newScore: number;
if (globalCPI <= 2.20) newScore = 100;
else if (globalCPI <= 3.50) newScore = 80;
else if (globalCPI <= 4.50) newScore = 60;
else if (globalCPI <= 5.50) newScore = 40;
else newScore = 20;
mapped.dimensions[economyDimIdx].score = newScore;
mapped.dimensions[economyDimIdx].kpi = { mapped.dimensions[economyDimIdx].kpi = {
label: 'Coste por Interacción', label: 'Coste por Interacción',
value: `${globalCPI.toFixed(2)}`, value: `${globalCPI.toFixed(2)}`,
change: `vs benchmark €${CPI_BENCHMARK.toFixed(2)}`, change: `vs benchmark €${CPI_BENCHMARK.toFixed(2)}`,
changeType: cpiStatus as 'positive' | 'neutral' | 'negative' changeType: cpiStatus as 'positive' | 'neutral' | 'negative'
}; };
console.log(`💰 CPI sincronizado: €${globalCPI.toFixed(2)} (desde heatmapData, consistente con Executive Summary)`); console.log(`💰 CPI sincronizado: €${globalCPI.toFixed(2)}, score: ${newScore}`);
} }
} }
@@ -1104,10 +1116,22 @@ export const generateAnalysisFromCache = async (
const oldKpi = mapped.dimensions[economyDimIdx].kpi; const oldKpi = mapped.dimensions[economyDimIdx].kpi;
console.log(' - OLD KPI value:', oldKpi?.value); console.log(' - OLD KPI value:', oldKpi?.value);
const CPI_BENCHMARK = 5.00; // Usar benchmark de aerolíneas (€3.50) para consistencia con ExecutiveSummaryTab
// Percentiles: p25=2.20, p50=3.50, p75=4.50, p90=5.50
const CPI_BENCHMARK = 3.50;
const cpiDiff = globalCPI - CPI_BENCHMARK; const cpiDiff = globalCPI - CPI_BENCHMARK;
// Para CPI invertido: menor es mejor
const cpiStatus = cpiDiff <= 0 ? 'positive' : cpiDiff <= 0.5 ? 'neutral' : 'negative'; const cpiStatus = cpiDiff <= 0 ? 'positive' : cpiDiff <= 0.5 ? 'neutral' : 'negative';
// Calcular score basado en percentiles aerolíneas
let newScore: number;
if (globalCPI <= 2.20) newScore = 100;
else if (globalCPI <= 3.50) newScore = 80;
else if (globalCPI <= 4.50) newScore = 60;
else if (globalCPI <= 5.50) newScore = 40;
else newScore = 20;
mapped.dimensions[economyDimIdx].score = newScore;
mapped.dimensions[economyDimIdx].kpi = { mapped.dimensions[economyDimIdx].kpi = {
label: 'Coste por Interacción', label: 'Coste por Interacción',
value: `${globalCPI.toFixed(2)}`, value: `${globalCPI.toFixed(2)}`,
@@ -1115,6 +1139,7 @@ export const generateAnalysisFromCache = async (
changeType: cpiStatus as 'positive' | 'neutral' | 'negative' changeType: cpiStatus as 'positive' | 'neutral' | 'negative'
}; };
console.log(' - NEW KPI value:', mapped.dimensions[economyDimIdx].kpi.value); console.log(' - NEW KPI value:', mapped.dimensions[economyDimIdx].kpi.value);
console.log(' - NEW score:', newScore);
console.log(`💰 CPI sincronizado (cache): €${globalCPI.toFixed(2)}`); console.log(`💰 CPI sincronizado (cache): €${globalCPI.toFixed(2)}`);
} else { } else {
console.warn('⚠️ CPI sync skipped: economyDimIdx=', economyDimIdx, 'globalCPI=', globalCPI); console.warn('⚠️ CPI sync skipped: economyDimIdx=', economyDimIdx, 'globalCPI=', globalCPI);

View File

@@ -637,8 +637,9 @@ function buildEconomyDimension(
const op = raw?.operational_performance; const op = raw?.operational_performance;
const totalAnnual = safeNumber(econ?.cost_breakdown?.total_annual, 0); const totalAnnual = safeNumber(econ?.cost_breakdown?.total_annual, 0);
// Benchmark CPI sector contact center (Fuente: Gartner Contact Center Cost Benchmark 2024) // Benchmark CPI aerolíneas (consistente con ExecutiveSummaryTab)
const CPI_BENCHMARK = 5.00; // p25: 2.20, p50: 3.50, p75: 4.50, p90: 5.50
const CPI_BENCHMARK = 3.50; // p50 aerolíneas
if (totalAnnual <= 0 || totalInteractions <= 0) { if (totalAnnual <= 0 || totalInteractions <= 0) {
return undefined; return undefined;
@@ -651,20 +652,20 @@ function buildEconomyDimension(
// Calcular CPI usando cost_volume (non-abandoned) como denominador // Calcular CPI usando cost_volume (non-abandoned) como denominador
const cpi = costVolume > 0 ? totalAnnual / costVolume : totalAnnual / totalInteractions; const cpi = costVolume > 0 ? totalAnnual / costVolume : totalAnnual / totalInteractions;
// Score basado en comparación con benchmark (€5.00) // Score basado en percentiles de aerolíneas (CPI invertido: menor = mejor)
// CPI <= 4.00 = 100pts (excelente) // CPI <= 2.20 (p25) = 100pts (excelente, top 25%)
// CPI 4.00-5.00 = 80pts (en benchmark) // CPI 2.20-3.50 (p25-p50) = 80pts (bueno, top 50%)
// CPI 5.00-6.00 = 60pts (por encima) // CPI 3.50-4.50 (p50-p75) = 60pts (promedio)
// CPI 6.00-7.00 = 40pts (alto) // CPI 4.50-5.50 (p75-p90) = 40pts (por debajo)
// CPI > 7.00 = 20pts (crítico) // CPI > 5.50 (>p90) = 20pts (crítico)
let score: number; let score: number;
if (cpi <= 4.00) { if (cpi <= 2.20) {
score = 100; score = 100;
} else if (cpi <= 5.00) { } else if (cpi <= 3.50) {
score = 80; score = 80;
} else if (cpi <= 6.00) { } else if (cpi <= 4.50) {
score = 60; score = 60;
} else if (cpi <= 7.00) { } else if (cpi <= 5.50) {
score = 40; score = 40;
} else { } else {
score = 20; score = 20;
@@ -676,7 +677,7 @@ function buildEconomyDimension(
let summary = `Coste por interacción: €${cpi.toFixed(2)} vs benchmark €${CPI_BENCHMARK.toFixed(2)}. `; let summary = `Coste por interacción: €${cpi.toFixed(2)} vs benchmark €${CPI_BENCHMARK.toFixed(2)}. `;
if (cpi <= CPI_BENCHMARK) { if (cpi <= CPI_BENCHMARK) {
summary += 'Eficiencia de costes óptima, por debajo del benchmark del sector.'; summary += 'Eficiencia de costes óptima, por debajo del benchmark del sector.';
} else if (cpi <= 6.00) { } else if (cpi <= 4.50) {
summary += 'Coste ligeramente por encima del benchmark, oportunidad de optimización.'; summary += 'Coste ligeramente por encima del benchmark, oportunidad de optimización.';
} else { } else {
summary += 'Coste elevado respecto al sector. Priorizar iniciativas de eficiencia.'; summary += 'Coste elevado respecto al sector. Priorizar iniciativas de eficiencia.';

View File

@@ -1363,14 +1363,15 @@ function generateDimensionsFromRealData(
kpi: { label: 'CSAT', value: avgCsat > 0 ? `${Math.round(avgCsat)}/100` : 'N/A' }, kpi: { label: 'CSAT', value: avgCsat > 0 ? `${Math.round(avgCsat)}/100` : 'N/A' },
icon: Smile icon: Smile
}, },
// 6. ECONOMÍA - CPI // 6. ECONOMÍA - CPI (benchmark aerolíneas: p25=2.20, p50=3.50, p75=4.50, p90=5.50)
{ {
id: 'economy_cpi', id: 'economy_cpi',
name: 'economy_cpi', name: 'economy_cpi',
title: 'Economía Operacional', title: 'Economía Operacional',
score: costPerInteraction < 4 ? 85 : costPerInteraction < 5 ? 70 : costPerInteraction < 6 ? 55 : 40, // Score basado en percentiles aerolíneas (CPI invertido: menor = mejor)
percentile: costPerInteraction < 4.5 ? 70 : costPerInteraction < 5.5 ? 50 : 30, score: costPerInteraction <= 2.20 ? 100 : costPerInteraction <= 3.50 ? 80 : costPerInteraction <= 4.50 ? 60 : costPerInteraction <= 5.50 ? 40 : 20,
summary: `CPI: €${costPerInteraction.toFixed(2)} por interacción. Coste anual: €${totalCost.toLocaleString('es-ES')}. Benchmark sector: €5.00 (Fuente: Gartner 2024).`, percentile: costPerInteraction <= 2.20 ? 90 : costPerInteraction <= 3.50 ? 70 : costPerInteraction <= 4.50 ? 50 : costPerInteraction <= 5.50 ? 25 : 10,
summary: `CPI: €${costPerInteraction.toFixed(2)} por interacción. Coste anual: €${totalCost.toLocaleString('es-ES')}. Benchmark sector aerolíneas: €3.50.`,
kpi: { label: 'Coste/Interacción', value: `${costPerInteraction.toFixed(2)}` }, kpi: { label: 'Coste/Interacción', value: `${costPerInteraction.toFixed(2)}` },
icon: DollarSign icon: DollarSign
}, },