import { createClient } from '@supabase/supabase-js'; import { Experiment, ExperimentCreateInput, ExperimentListItem, ActiveExperiment, ExperimentType, ExperimentStatus, } from '../types'; import { SalesRecord } from '../types'; const supabaseUrl = process.env.SUPABASE_URL || ''; const supabaseServiceKey = process.env.SUPABASE_SERVICE_KEY || ''; const getSupabase = () => { if (!supabaseUrl || !supabaseServiceKey) { throw new Error('Supabase credentials not configured'); } return createClient(supabaseUrl, supabaseServiceKey); }; // ============ CRUD Operations ============ export const createExperiment = async (input: ExperimentCreateInput): Promise => { const supabase = getSupabase(); const { data, error } = await supabase .from('experiments') .insert([{ ...input, status: 'planned', created_at: new Date().toISOString(), updated_at: new Date().toISOString(), }]) .select() .single(); if (error) throw error; return data as Experiment; }; export const updateExperiment = async ( id: string, updates: Partial ): Promise => { const supabase = getSupabase(); const { data, error } = await supabase .from('experiments') .update({ ...updates, updated_at: new Date().toISOString() }) .eq('id', id) .select() .single(); if (error) throw error; return data as Experiment; }; export const deleteExperiment = async (id: string): Promise => { const supabase = getSupabase(); const { error } = await supabase .from('experiments') .delete() .eq('id', id); if (error) throw error; }; export const getExperiment = async (id: string): Promise => { const supabase = getSupabase(); const { data, error } = await supabase .from('experiments') .select('*') .eq('id', id) .single(); if (error) return null; return data as Experiment; }; export const listExperiments = async ( filters?: { status?: ExperimentStatus[]; type?: ExperimentType[]; marketplace?: string[]; asin?: string; dateFrom?: string; dateTo?: string; } ): Promise => { const supabase = getSupabase(); let query = supabase.from('experiments').select('*'); if (filters?.status?.length) { query = query.in('status', filters.status); } if (filters?.type?.length) { query = query.in('type', filters.type); } if (filters?.marketplace?.length) { query = query.in('marketplace', filters.marketplace); } if (filters?.asin) { query = query.contains('asins', [filters.asin]); } if (filters?.dateFrom) { query = query.gte('start_date', filters.dateFrom); } if (filters?.dateTo) { query = query.lte('end_date', filters.dateTo); } const { data, error } = await query.order('created_at', { ascending: false }); if (error) throw error; // 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, marketplace: exp.marketplace, start_date: exp.start_date, end_date: exp.end_date, progress_percent: Math.round(progressPercent), actual_lift_percent: exp.actual_lift_percent, owner: exp.owner, }; }); }; export const getActiveExperimentsForASINs = async ( asins: string[] ): Promise> => { const supabase = getSupabase(); const today = new Date().toISOString().split('T')[0]; const { data, error } = await supabase .from('experiments') .select('*') .eq('status', 'active') .lte('start_date', today) .or(`end_date.is.null,end_date.gte.${today}`); if (error) throw error; const map = new Map(); for (const asin of asins) { const experiments = (data || []) .filter((exp: any) => exp.asins?.includes(asin)) .map((exp: any) => { 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; return { asin, 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, } as ActiveExperiment; }); if (experiments.length > 0) { map.set(asin, experiments); } } return map; }; // ============ Performance Calculations ============ export const calculateExperimentPerformance = async ( experiment: Experiment, salesData: SalesRecord[] ): Promise<{ baseline_units: number; baseline_revenue: number; experiment_units: number; experiment_revenue: number; actual_lift_percent: number; }> => { const startDate = new Date(experiment.start_date); const endDate = experiment.end_date ? new Date(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 = new Set(experiment.asins.map(a => a.toUpperCase())); const baselineData = salesData.filter(r => { const recordDate = new Date(`${r.year}-${getMonthNumber(r.month)}-01`); return asinSet.has(r.asin.toUpperCase()) && recordDate >= baselineStart && recordDate < baselineEnd; }); const experimentData = salesData.filter(r => { const recordDate = new Date(`${r.year}-${getMonthNumber(r.month)}-01`); return asinSet.has(r.asin.toUpperCase()) && recordDate >= startDate && recordDate <= endDate; }); const baseline_units = baselineData.reduce((sum, r) => sum + r.units, 0); const baseline_revenue = baselineData.reduce((sum, r) => sum + r.sellOut, 0); const experiment_units = experimentData.reduce((sum, r) => sum + r.units, 0); const experiment_revenue = experimentData.reduce((sum, r) => sum + r.sellOut, 0); const actual_lift_percent = baseline_units > 0 ? ((experiment_units - baseline_units) / baseline_units) * 100 : 0; return { baseline_units, baseline_revenue, experiment_units, experiment_revenue, actual_lift_percent: Math.round(actual_lift_percent * 10) / 10, }; }; export const updateExperimentResults = async ( experimentId: string, salesData: SalesRecord[] ): 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 '๐Ÿงช'; } };