diff --git a/api/experiments.ts b/api/experiments.ts new file mode 100644 index 0000000..dec671e --- /dev/null +++ b/api/experiments.ts @@ -0,0 +1,112 @@ +import type { VercelRequest, VercelResponse } from '@vercel/node'; +import { createClient } from '@supabase/supabase-js'; +import { Experiment, ExperimentCreateInput } from '../types'; + +const supabase = createClient( + process.env.SUPABASE_URL || '', + process.env.SUPABASE_SERVICE_KEY || '' +); + +export default async function handler(req: VercelRequest, res: VercelResponse) { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + + if (req.method === 'OPTIONS') return res.status(200).end(); + + try { + // GET - List experiments or get single experiment + if (req.method === 'GET') { + const { id, status, type, marketplace, asin, dateFrom, dateTo } = req.query; + + let query = supabase.from('experiments').select('*'); + + if (id) { + const { data, error } = await query.eq('id', id).single(); + if (error) throw error; + return res.status(200).json(data); + } + + // List with filters + if (status) { + const statuses = Array.isArray(status) ? status : [status]; + query = query.in('status', statuses); + } + if (type) { + const types = Array.isArray(type) ? type : [type]; + query = query.in('type', types); + } + if (marketplace) { + const markets = Array.isArray(marketplace) ? marketplace : [marketplace]; + query = query.in('marketplace', markets); + } + if (asin) { + query = query.contains('asins', [asin]); + } + if (dateFrom) { + query = query.gte('start_date', dateFrom as string); + } + if (dateTo) { + query = query.lte('end_date', dateTo as string); + } + + const { data, error } = await query.order('created_at', { ascending: false }); + if (error) throw error; + + return res.status(200).json(data || []); + } + + // POST - Create experiment + if (req.method === 'POST') { + const input: ExperimentCreateInput = req.body; + + 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 res.status(201).json(data); + } + + // PUT - Update experiment + if (req.method === 'PUT') { + const { id } = req.query; + const updates = req.body; + + const { data, error } = await supabase + .from('experiments') + .update({ ...updates, updated_at: new Date().toISOString() }) + .eq('id', id) + .select() + .single(); + + if (error) throw error; + return res.status(200).json(data); + } + + // DELETE - Delete experiment + if (req.method === 'DELETE') { + const { id } = req.query; + + const { error } = await supabase + .from('experiments') + .delete() + .eq('id', id); + + if (error) throw error; + return res.status(200).json({ success: true }); + } + + return res.status(405).json({ error: 'Method not allowed' }); + } catch (error: any) { + console.error('Experiments API error:', error); + return res.status(500).json({ error: error.message || 'Internal server error' }); + } +} diff --git a/services/experiments.ts b/services/experiments.ts index 1a5a3fb..47a20cd 100644 --- a/services/experiments.ts +++ b/services/experiments.ts @@ -1,4 +1,3 @@ -import { createClient } from '@supabase/supabase-js'; import { Experiment, ExperimentCreateInput, @@ -9,75 +8,60 @@ import { } 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); -}; +const API_BASE = '/api/experiments'; // ============ 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(); + const response = await fetch(API_BASE, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(input), + }); - if (error) throw error; - return data as Experiment; + 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 supabase = getSupabase(); - - const { data, error } = await supabase - .from('experiments') - .update({ ...updates, updated_at: new Date().toISOString() }) - .eq('id', id) - .select() - .single(); + const response = await fetch(`${API_BASE}?id=${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(updates), + }); - if (error) throw error; - return data as Experiment; + 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 supabase = getSupabase(); - - const { error } = await supabase - .from('experiments') - .delete() - .eq('id', id); + const response = await fetch(`${API_BASE}?id=${id}`, { + method: 'DELETE', + }); - if (error) throw error; + 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 supabase = getSupabase(); - - const { data, error } = await supabase - .from('experiments') - .select('*') - .eq('id', id) - .single(); + const response = await fetch(`${API_BASE}?id=${id}`); - if (error) return null; - return data as Experiment; + if (!response.ok) return null; + + return response.json(); }; export const listExperiments = async ( @@ -90,35 +74,38 @@ export const listExperiments = async ( dateTo?: string; } ): Promise => { - const supabase = getSupabase(); - - let query = supabase.from('experiments').select('*'); + const params = new URLSearchParams(); if (filters?.status?.length) { - query = query.in('status', filters.status); + filters.status.forEach(s => params.append('status', s)); } if (filters?.type?.length) { - query = query.in('type', filters.type); + filters.type.forEach(t => params.append('type', t)); } if (filters?.marketplace?.length) { - query = query.in('marketplace', filters.marketplace); + filters.marketplace.forEach(m => params.append('marketplace', m)); } if (filters?.asin) { - query = query.contains('asins', [filters.asin]); + params.append('asin', filters.asin); } if (filters?.dateFrom) { - query = query.gte('start_date', filters.dateFrom); + params.append('dateFrom', filters.dateFrom); } if (filters?.dateTo) { - query = query.lte('end_date', filters.dateTo); + params.append('dateTo', filters.dateTo); } - const { data, error } = await query.order('created_at', { ascending: false }); + const response = await fetch(`${API_BASE}?${params.toString()}`); - if (error) throw error; + 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) => { + 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; @@ -149,24 +136,31 @@ export const listExperiments = async ( export const getActiveExperimentsForASINs = async ( asins: string[] ): Promise> => { - const supabase = getSupabase(); + const params = new URLSearchParams(); + asins.forEach(asin => params.append('asin', asin)); + const response = await fetch(`${API_BASE}?${params.toString()}&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]; - 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; + // 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 asin of asins) { - const experiments = (data || []) - .filter((exp: any) => Array.isArray(exp.asins) && exp.asins.includes(asin)) + const experiments = activeExperiments + .filter((exp: any) => exp.asins?.includes(asin)) .map((exp: any) => { const endDate = exp.end_date ? new Date(exp.end_date) : null; const daysRemaining = endDate