import { Experiment, ExperimentCreateInput, ExperimentListItem, ActiveExperiment, ExperimentType, ExperimentStatus, } from '../types'; import { CombinedKPIs } from '../types'; const API_BASE = '/api/experiments'; // ============ Helper Functions ============ export const getExperimentAsins = (experimentAsins: string[], salesData: CombinedKPIs[]): Set => { const explicitAsins = new Set(); const lines = new Set(); (experimentAsins || []).forEach(a => { const val = (a || '').trim().toUpperCase(); if (val.startsWith('LINE:')) { lines.add(val.substring(5).trim()); } else if (val) { explicitAsins.add(val); } }); const asinSet = new Set(explicitAsins); if (lines.size > 0 && salesData) { salesData.forEach(r => { const line = (r.line || '').trim().toUpperCase(); if (line && lines.has(line)) { if (r.asin) asinSet.add(r.asin.toUpperCase()); } }); } return asinSet; }; // ============ CRUD Operations ============ export const createExperiment = async (input: ExperimentCreateInput): Promise => { const response = await fetch(API_BASE, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to create experiment'); } return response.json(); }; export const updateExperiment = async ( id: string, updates: Partial ): Promise => { const response = await fetch(`${API_BASE}?id=${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updates), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to update experiment'); } return response.json(); }; export const deleteExperiment = async (id: string): Promise => { const response = await fetch(`${API_BASE}?id=${id}`, { method: 'DELETE', }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to delete experiment'); } }; export const getExperiment = async (id: string): Promise => { const response = await fetch(`${API_BASE}?id=${id}`); if (!response.ok) return null; return response.json(); }; export const listExperiments = async ( filters?: { status?: ExperimentStatus[]; type?: ExperimentType[]; marketplace?: string[]; asin?: string; dateFrom?: string; dateTo?: string; } ): Promise => { const params = new URLSearchParams(); if (filters?.status?.length) { filters.status.forEach(s => params.append('status', s)); } if (filters?.type?.length) { filters.type.forEach(t => params.append('type', t)); } if (filters?.marketplace?.length) { filters.marketplace.forEach(m => params.append('marketplace', m)); } if (filters?.asin) { params.append('asin', filters.asin); } if (filters?.dateFrom) { params.append('dateFrom', filters.dateFrom); } if (filters?.dateTo) { params.append('dateTo', filters.dateTo); } const response = await fetch(`${API_BASE}?${params.toString()}`); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to list experiments'); } const data = await response.json(); // Transform to ExperimentListItem return data.map((exp: any) => { const today = new Date(); const startDate = new Date(exp.start_date); const endDate = exp.end_date ? new Date(exp.end_date) : null; let progressPercent = 0; if (endDate) { const totalDays = endDate.getTime() - startDate.getTime(); const elapsedDays = today.getTime() - startDate.getTime(); progressPercent = Math.min(100, Math.max(0, (elapsedDays / totalDays) * 100)); } return { id: exp.id, name: exp.name, type: exp.type, status: exp.status, asin_count: exp.asins?.length || 0, asins: exp.asins || [], marketplace: exp.marketplace, start_date: exp.start_date, end_date: exp.end_date, progress_percent: Math.round(progressPercent), primary_metric: exp.primary_metric, actual_lift_percent: exp.actual_lift_percent, owner: exp.owner, }; }); }; export const getActiveExperiments = async ( salesData: CombinedKPIs[] ): Promise> => { const response = await fetch(`${API_BASE}?status=active`); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to fetch active experiments'); } const data = await response.json(); const today = new Date().toISOString().split('T')[0]; // Filter experiments that are currently active const activeExperiments = data.filter((exp: any) => exp.status === 'active' && exp.start_date <= today && (!exp.end_date || exp.end_date >= today) ); const map = new Map(); for (const exp of activeExperiments) { const expAsins = getExperimentAsins(exp.asins || [], salesData); const endDate = exp.end_date ? new Date(exp.end_date) : null; const daysRemaining = endDate ? Math.ceil((endDate.getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24)) : undefined; const activeExp: ActiveExperiment = { asin: '', // placeholder experiment_id: exp.id, experiment_name: exp.name, type: exp.type, status: exp.status, start_date: exp.start_date, end_date: exp.end_date, days_remaining: daysRemaining, }; expAsins.forEach(asin => { const asinList = map.get(asin) || []; asinList.push({ ...activeExp, asin }); map.set(asin, asinList); }); } return map; }; // ============ Performance Calculations ============ export const calculateExperimentPerformance = async ( experiment: Experiment, salesData: CombinedKPIs[] ): Promise<{ baseline_units: number; baseline_revenue: number; baseline_gv: number; baseline_cvr: number; baseline_acos: number; experiment_units: number; experiment_revenue: number; experiment_gv: number; experiment_cvr: number; experiment_acos: number; actual_lift_percent: number; }> => { const parseLocalDate = (dateStr: string) => { const [y, m, d] = dateStr.split('T')[0].split('-'); return new Date(Number(y), Number(m) - 1, Number(d)); }; const startDate = parseLocalDate(experiment.start_date); const endDate = experiment.end_date ? parseLocalDate(experiment.end_date) : new Date(); // Calculate baseline period (same duration before experiment) const durationMs = endDate.getTime() - startDate.getTime(); const baselineStart = new Date(startDate.getTime() - durationMs); const baselineEnd = startDate; // Filter sales data for experiment ASINs const asinSet = getExperimentAsins(experiment.asins, salesData); const baselineData = salesData.filter(r => { const recordDate = new Date(r.year, 0, 1 + (r.week - 1) * 7); return asinSet.has(r.asin.toUpperCase()) && recordDate >= baselineStart && recordDate < baselineEnd; }); const experimentData = salesData.filter(r => { const recordDate = new Date(r.year, 0, 1 + (r.week - 1) * 7); return asinSet.has(r.asin.toUpperCase()) && recordDate >= startDate && recordDate <= endDate; }); const baseline_units = baselineData.reduce((sum, r) => sum + r.unitsTotal, 0); const baseline_revenue = baselineData.reduce((sum, r) => sum + r.salesTotal, 0); const baseline_gv = baselineData.reduce((sum, r) => sum + (r.glanceViews || 0), 0); const baseline_cvr = baseline_gv > 0 ? (baseline_units / baseline_gv) * 100 : 0; const baseline_spend = baselineData.reduce((sum, r) => sum + (r.cost || 0), 0); const baseline_acos = baseline_revenue > 0 ? (baseline_spend / baseline_revenue) * 100 : 0; const experiment_units = experimentData.reduce((sum, r) => sum + r.unitsTotal, 0); const experiment_revenue = experimentData.reduce((sum, r) => sum + r.salesTotal, 0); const experiment_gv = experimentData.reduce((sum, r) => sum + (r.glanceViews || 0), 0); const experiment_cvr = experiment_gv > 0 ? (experiment_units / experiment_gv) * 100 : 0; const experiment_spend = experimentData.reduce((sum, r) => sum + (r.cost || 0), 0); const experiment_acos = experiment_revenue > 0 ? (experiment_spend / experiment_revenue) * 100 : 0; let actual_lift_percent = 0; if (experiment.primary_metric === 'cvr' && baseline_cvr > 0) { actual_lift_percent = ((experiment_cvr - baseline_cvr) / baseline_cvr) * 100; } else if (experiment.primary_metric === 'revenue' && baseline_revenue > 0) { actual_lift_percent = ((experiment_revenue - baseline_revenue) / baseline_revenue) * 100; } else if (experiment.primary_metric === 'acos' && baseline_acos > 0) { actual_lift_percent = ((baseline_acos - experiment_acos) / Math.max(0.1, baseline_acos)) * 100; // inverted, lower is better } else if (baseline_units > 0) { actual_lift_percent = ((experiment_units - baseline_units) / baseline_units) * 100; } return { baseline_units, baseline_revenue, baseline_gv, baseline_cvr: Math.round(baseline_cvr * 10) / 10, baseline_acos: Math.round(baseline_acos * 10) / 10, experiment_units, experiment_revenue, experiment_gv, experiment_cvr: Math.round(experiment_cvr * 10) / 10, experiment_acos: Math.round(experiment_acos * 10) / 10, actual_lift_percent: Math.round(actual_lift_percent * 10) / 10, }; }; export const updateExperimentResults = async ( experimentId: string, salesData: CombinedKPIs[] ): Promise => { const experiment = await getExperiment(experimentId); if (!experiment) throw new Error('Experiment not found'); const performance = await calculateExperimentPerformance(experiment, salesData); return updateExperiment(experimentId, { ...performance, }); }; // ============ Helper Functions ============ function getMonthNumber(monthStr: string): number { const months: Record = { 'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6, 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12 }; return months[monthStr.split('-')[0]] || 1; } export const getExperimentStatusColor = (status: ExperimentStatus): string => { switch (status) { case 'active': return 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30'; case 'planned': return 'bg-amber-500/20 text-amber-400 border-amber-500/30'; case 'completed': return 'bg-slate-500/20 text-slate-400 border-slate-500/30'; case 'paused': return 'bg-red-500/20 text-red-400 border-red-500/30'; default: return 'bg-slate-500/20 text-slate-400'; } }; export const getExperimentTypeColor = (type: ExperimentType): string => { switch (type) { case 'pricing': return 'bg-blue-500/20 text-blue-400 border-blue-500/30'; case 'advertising': return 'bg-purple-500/20 text-purple-400 border-purple-500/30'; case 'content': return 'bg-pink-500/20 text-pink-400 border-pink-500/30'; case 'promotion': return 'bg-orange-500/20 text-orange-400 border-orange-500/30'; default: return 'bg-slate-500/20 text-slate-400'; } }; export const getExperimentIcon = (type: ExperimentType): string => { switch (type) { case 'pricing': return '๐Ÿ’ฐ'; case 'advertising': return '๐Ÿ“ข'; case 'content': return '๐Ÿ“'; case 'promotion': return '๐Ÿท๏ธ'; default: return '๐Ÿงช'; } };