From 74d92b83b54a24c662ff90d5f864d13a0761e772 Mon Sep 17 00:00:00 2001 From: Christian Vidal Wolf Date: Tue, 10 Mar 2026 14:04:20 +0100 Subject: [PATCH] fix: logic to show BSR improvements (rank drops) in green --- calculate_uk_ad_spend.ts | 118 +++++++++ components/ExperimentsView.tsx | 2 +- debug_all_asins.ts | 100 +++++++ debug_bodyness_experiment.ts | 243 ++++++++++++++++++ debug_inkee.ts | 50 ++++ debug_marketplace.ts | 42 +++ debug_merge.ts | 42 +++ debug_timestamps.ts | 70 +++++ .../Ads_Performance_Export_2026-02-26.xlsx | Bin 0 -> 24466 bytes inspect_ads.ts | 30 +++ inspect_user_ads.ts | 37 +++ test-ads.ts | 57 ++++ test-iso-weeks.ts | 94 +++++++ test_did.ts | 40 +++ 14 files changed, 924 insertions(+), 1 deletion(-) create mode 100644 calculate_uk_ad_spend.ts create mode 100644 debug_all_asins.ts create mode 100644 debug_bodyness_experiment.ts create mode 100644 debug_inkee.ts create mode 100644 debug_marketplace.ts create mode 100644 debug_merge.ts create mode 100644 debug_timestamps.ts create mode 100644 docs/plans/Ads_Performance_Export_2026-02-26.xlsx create mode 100644 inspect_ads.ts create mode 100644 inspect_user_ads.ts create mode 100644 test-ads.ts create mode 100644 test-iso-weeks.ts create mode 100644 test_did.ts diff --git a/calculate_uk_ad_spend.ts b/calculate_uk_ad_spend.ts new file mode 100644 index 0000000..416cd2e --- /dev/null +++ b/calculate_uk_ad_spend.ts @@ -0,0 +1,118 @@ +import XLSX from 'xlsx'; +import { readFileSync, existsSync } from 'fs'; +import Papa from 'papaparse'; + +const ADS_FILE = 'Ads Weekly.xlsx'; +const DATA_FILE = 'dist/Amazon-Sell-Out-2023-2025.csv'; // Need to check if this exists locally or if I need to fetch it. + +async function main() { + console.log('--- AD SPEND BY PRODUCT LINE (UK, 2026) ---'); + + // 1. Build Metadata Map (ASIN -> Line) from Sales Records + const metaMap = new Map(); + + // Check local CSV if it exists, otherwise we might need to fetch it + const dropboxUrl = "https://www.dropbox.com/scl/fi/b9zxn4z5i7sxwfakk5g5y/Amazon-Sell-Out-2023-2025.csv?rlkey=uoto6v0mm99py8nszy8ldtez8&st=pzn1zkrg&dl=1"; + console.log('Fetching Sales data to build ASIN-Line mapping...'); + const csvResponse = await fetch(dropboxUrl); + const csvText = await csvResponse.text(); + + console.log('Parsing CSV...'); + const results = Papa.parse(csvText, { header: true, skipEmptyLines: true }); + const rows = results.data as any[]; + + if (rows.length > 0) { + console.log('CSV Columns:', Object.keys(rows[0])); + } + + rows.forEach(r => { + const asin = String(rowValue(r, ['CUSTOMER REFERENCE', 'ASIN'])).trim().toUpperCase(); + const line = rowValue(r, ['PRODUCT Line', 'Line']) || 'Unassigned'; + if (asin && !metaMap.has(asin)) { + metaMap.set(asin, line); + } + }); + console.log(`Built mapping for ${metaMap.size} unique ASINs.`); + + // Helper to get case-insensitive row value + function rowValue(row: any, aliases: string[]): string { + for (const a of aliases) { + if (row[a]) return row[a]; + const lowerA = a.toLowerCase(); + const found = Object.keys(row).find(k => k.toLowerCase() === lowerA); + if (found) return row[found]; + } + return ''; + } + + // 2. Parse Ads Data + if (!existsSync(ADS_FILE)) { + console.error(`Error: ${ADS_FILE} not found.`); + return; + } + + const buf = readFileSync(ADS_FILE); + const wb = XLSX.read(buf, { type: 'buffer' }); + + const spendByLine = new Map(); + let totalUkSpend = 0; + let foundMatches: any[] = []; + + for (const sheetName of wb.SheetNames) { + const ws = wb.Sheets[sheetName]; + const rows: any[] = XLSX.utils.sheet_to_json(ws, { defval: '' }); + + rows.forEach((r: any) => { + const countryRaw = String(r.Country || '').toUpperCase(); + const asin = String(r.ASIN || '').trim().toUpperCase(); + const cost = parseFloat(r.Cost) || 0; + const line = metaMap.get(asin) || 'Unassigned'; + + // Still track official UK 2026 total for the summary + if (sheetName === '2026' && (countryRaw === 'UK' || countryRaw === 'GB' || countryRaw === 'AMAZON UK' || countryRaw === 'AMAZON GB')) { + spendByLine.set(line, (spendByLine.get(line) || 0) + cost); + totalUkSpend += cost; + } + + // DEBUG: Find ALL "INKEE" records anywhere that might explain the 126 + if (line.toUpperCase() === 'INKEE' && cost > 0) { + foundMatches.push({ + Sheet: sheetName, + Country: countryRaw, + ASIN: asin, + Cost: cost, + Week: r.Week + }); + } + }); + } + + console.log('\n--- ALL INKEE AD RECORDS FOUND (ANY COUNTRY/YEAR) ---'); + console.table(foundMatches.slice(0, 50)); // Showing first 50 + if (foundMatches.length > 50) console.log(`... and ${foundMatches.length - 50} more records.`); + + const totalsByGroup = foundMatches.reduce((acc, curr) => { + const key = `${curr.Sheet} | ${curr.Country}`; + acc[key] = (acc[key] || 0) + curr.Cost; + return acc; + }, {}); + + console.log('\nSummary of INKEE Spend by Sheet and Country:'); + console.table(totalsByGroup); + + // 3. Output results + console.log('\nOfficial Results for Amazon UK (2026 YTD) based on sheet "2026":'); + const sortedLines = Array.from(spendByLine.entries()) + .sort((a, b) => b[1] - a[1]); + + console.log('--------------------------------------------------'); + console.log(`${'Product Line'.padEnd(30)} | ${'Ad Spend (£)'.padStart(15)}`); + console.log('--------------------------------------------------'); + sortedLines.forEach(([line, spend]) => { + console.log(`${line.padEnd(30)} | ${spend.toFixed(2).padStart(15)}`); + }); + console.log('--------------------------------------------------'); + console.log(`${'TOTAL'.padEnd(30)} | ${totalUkSpend.toFixed(2).padStart(15)}`); +} + +main().catch(console.error); diff --git a/components/ExperimentsView.tsx b/components/ExperimentsView.tsx index 1aada7d..a42364c 100644 --- a/components/ExperimentsView.tsx +++ b/components/ExperimentsView.tsx @@ -600,7 +600,7 @@ const ExperimentDetailView: React.FC<{
- = 0 ? '' : 'rotate-180') : 'rotate-180'}`} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}> + = 0 ? '' : 'rotate-180') : (res.lift_percent <= 0 ? '' : 'rotate-180')}`} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}> {res.lift_percent >= 0 ? '+' : ''}{res.lift_percent.toFixed(1)}% diff --git a/debug_all_asins.ts b/debug_all_asins.ts new file mode 100644 index 0000000..c094555 --- /dev/null +++ b/debug_all_asins.ts @@ -0,0 +1,100 @@ +import { createClient } from '@supabase/supabase-js'; +import { readFileSync } from 'fs'; +import { processAdsExcel, mergeSalesAndAdsData } from './services/dataProcessor'; +import { computeDiD } from './services/experimentAnalysis'; +import { CombinedKPIs } from './types'; + +// Load .env.local if it exists +import { config } from 'dotenv'; +config({ path: '.env.local' }); + +async function run() { + const supabase = createClient( + process.env.SUPABASE_URL || '', + process.env.SUPABASE_SERVICE_KEY || '' + ); + + // Get the INKEE DE experiment + const { data: experiments, error } = await supabase + .from('experiments') + .select('*') + .ilike('name', '%INKEE%') + .eq('marketplace', 'DE'); + + if (error) { + console.error('Supabase error:', error); + return; + } + + if (!experiments || experiments.length === 0) { + console.log('No INKEE DE experiments found. Listing all experiments...'); + const { data: all } = await supabase.from('experiments').select('name, marketplace, start_date, end_date, asins').limit(20); + console.log(JSON.stringify(all, null, 2)); + return; + } + + for (const exp of experiments) { + console.log(`\n=== Experiment: "${exp.name}" ===`); + console.log(` Marketplace: ${exp.marketplace}`); + console.log(` Start: ${exp.start_date} → End: ${exp.end_date}`); + console.log(` Baseline: ${exp.baseline_start_date} → ${exp.baseline_end_date}`); + console.log(` ASINs (${exp.asins?.length || 0}):`, exp.asins); + console.log(` Control ASINs (${exp.control_asins?.length || 0}):`, exp.control_asins); + } + + // Now load the Ads data and check all ASINs + console.log('\n=== Loading Ads data ==='); + const buf = readFileSync('/tmp/Ads-Weekly.xlsx').buffer; + const adsData = await processAdsExcel(buf); + const merged = mergeSalesAndAdsData([], adsData) as CombinedKPIs[]; + + const inkeeExp = experiments[0]; + const allAsins = inkeeExp.asins?.map((a: string) => a.trim().toUpperCase()) || []; + + console.log(`\n=== Checking ${allAsins.length} ASINs in Ads data (weeks 7-8 of 2026) ===`); + let totalCostW7W8 = 0; + let totalAdRevW7W8 = 0; + let missingFromAds: string[] = []; + + for (const asin of allAsins) { + const adRecords = merged.filter(r => + r.asin === asin && + r.year === 2026 && + [7, 8].includes(r.week) && + (r.marketplace || r.customer || '').toLowerCase().includes('de') + ); + + if (adRecords.length === 0) { + missingFromAds.push(asin); + } else { + const cost = adRecords.reduce((s, r) => s + (r.cost || 0), 0); + const adRev = adRecords.reduce((s, r) => s + (r.salesAds || 0), 0); + const clicks = adRecords.reduce((s, r) => s + (r.clicks || 0), 0); + if (cost > 0 || adRev > 0 || clicks > 0) { + console.log(` ${asin}: W7+W8 cost=${cost.toFixed(2)} adRev=${adRev.toFixed(2)} clicks=${clicks}`); + totalCostW7W8 += cost; + totalAdRevW7W8 += adRev; + } else { + console.log(` ${asin}: W7+W8 cost=0 adRev=0 clicks=0 (zero activity)`); + } + } + } + + console.log(`\nMissing from Ads file entirely (${missingFromAds.length}):`, missingFromAds); + console.log(`\nTotal W7+W8 2026 cost: €${totalCostW7W8.toFixed(2)}`); + console.log(`Total W7+W8 2026 adRev: €${totalAdRevW7W8.toFixed(2)}`); + + if (totalCostW7W8 > 0) { + console.log(`Implied ACOS: ${(totalCostW7W8 / (totalAdRevW7W8 || 1) * 100).toFixed(1)}%`); + } + + // Run actual DiD + const did = computeDiD(inkeeExp, merged); + console.log('\n=== DiD Result ==='); + console.log('ACOS:', did.metrics['acos']); + console.log('ROAS:', did.metrics['roas']); + console.log('Revenue:', did.metrics['revenue']); + console.log('Units:', did.metrics['units']); +} + +run().catch(console.error); diff --git a/debug_bodyness_experiment.ts b/debug_bodyness_experiment.ts new file mode 100644 index 0000000..856c6da --- /dev/null +++ b/debug_bodyness_experiment.ts @@ -0,0 +1,243 @@ +import { createClient } from '@supabase/supabase-js'; + +interface Experiment { + id: string; + name: string; + start_date: string; + end_date?: string; + marketplace: string; + asins: string[]; + baseline_start_date?: string; + baseline_end_date?: string; +} + +// Configura Supabase con credenciales hardcodeadas para debug +const supabase = createClient( + 'https://qjioywarwdbxmdihyrti.supabase.co', + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InFqaW95d2Fyd2RieG1kaWh5cnRpIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc3MTUxMDU0NCwiZXhwIjoyMDg3MDg2NTQ0fQ.oXxdNBDvMSyfMRaf57oCb8cUS2Bqm73CeyM19v3GJPI' +); + +async function debugBodynessExperiment() { + console.log('=== DEBUG BODYNESS ES EXPERIMENT ===\n'); + + // 1. Obtener el experimento + const { data: expData, error: expError } = await supabase + .from('experiments') + .select('*') + .ilike('name', '%bodyness%') + .single(); + + if (expError || !expData) { + console.log('❌ No se encontró el experimento'); + console.log('Error:', expError); + return; + } + + const experiment: Experiment = expData; + console.log('📋 Experimento:', experiment.name); + console.log('📅 Start:', experiment.start_date); + console.log('📅 End:', experiment.end_date); + console.log('🌍 Marketplace:', experiment.marketplace); + console.log('📦 ASINs:', experiment.asins); + console.log(''); + + // 2. Obtener datos de ventas (sin filtro de año primero) - verificar total + const { count, error: countError } = await supabase + .from('vendor_daily_data') + .select('*', { count: 'exact', head: true }); + + console.log(`📊 Total registros en vendor_daily_data: ${count || 'unknown'}`); + console.log(''); + + // Buscar específicamente los ASINs del experimento + console.log('🔍 Buscando ASINs del experimento en la DB...'); + const { data: asinData, error: asinError } = await supabase + .from('vendor_daily_data') + .select('*') + .in('asin', experiment.asins); + + if (asinError) { + console.log('Error buscando ASINs:', asinError.message); + } else { + console.log(`✅ Registros encontrados para los ASINs del experimento: ${asinData?.length || 0}`); + if (asinData && asinData.length > 0) { + const asinsEncontrados = [...new Set(asinData.map(r => r.asin))]; + console.log('ASINs encontrados:', asinsEncontrados); + const marketsEncontrados = [...new Set(asinData.map(r => r.market))]; + console.log('Markets encontrados:', marketsEncontrados); + } + } + console.log(''); + + // Obtener datos filtrados por los ASINs del experimento (usamos asinData que ya tiene los datos) + const salesData = asinData || []; + console.log(`📊 Total registros para ASINs del experimento: ${salesData.length}`); + console.log(''); + + // Ver muestra de datos + console.log('📋 Muestra de datos (primeros 3 registros - todos los campos):'); + salesData.slice(0, 3).forEach((r: any, i) => { + console.log(` ${i+1}.`, JSON.stringify(r, null, 2)); + }); + console.log(''); + + // Ver rango de fechas + const dates = salesData.map(r => r.date).filter(Boolean).sort(); + if (dates.length > 0) { + console.log(`📅 Rango de fechas: ${dates[0]} → ${dates[dates.length - 1]}`); + } + console.log(''); + + // 4. Ver marketplace en los datos + const marketplaces = [...new Set(salesData.map(r => r.market || 'N/A'))]; + console.log('🌍 Marketplaces en los datos:', marketplaces); + console.log(''); + + // 5. Filtrar por marketplace del experimento (ES) + const mktFilter = (experiment.marketplace || '').toLowerCase(); + console.log(`Filtrando por marketplace: "${experiment.marketplace}"`); + + const filteredByMkt = salesData.filter(r => { + const mkt = (r.market || '').toLowerCase(); + return !mktFilter || mktFilter === 'all' || mkt.includes(mktFilter) || mktFilter.includes(mkt); + }); + + console.log(`✅ Registros después de filtrar por marketplace: ${filteredByMkt.length}`); + console.log(''); + + // 6. Agrupar por semana ISO (usando el campo date) + const weeksMap = new Map(); + filteredByMkt.forEach((r: any) => { + const dateStr = r.date; + if (!dateStr) return; + const date = new Date(dateStr); + const iso = getISOWeek(date); + const key = `${iso.year}-W${String(iso.week).padStart(2, '0')}`; + const existing = weeksMap.get(key) || { units: 0, records: 0, dates: [] as string[] }; + existing.units += (r.units || r.unitsTotal || 0); + existing.records++; + if (!existing.dates.includes(dateStr)) existing.dates.push(dateStr); + weeksMap.set(key, existing); + }); + + console.log('📅 Unidades por semana (TODOS los datos):'); + const sortedWeeks = Array.from(weeksMap.entries()).sort(); + sortedWeeks.forEach(([week, data]) => { + console.log(` ${week}: ${data.units} unidades (${data.records} registros) - fechas: ${data.dates.sort().join(', ')}`); + }); + console.log(''); + + // 7. Ver semanas dentro del período del experimento (Jan 22 - Feb 4, 2026) + const startDate = new Date(experiment.start_date!); + const endDate = new Date(experiment.end_date!); + + console.log('📅 Período del experimento:', startDate.toISOString().split('T')[0], '→', endDate.toISOString().split('T')[0]); + + // Calcular semanas ISO que toca el experimento + const startISO = getISOWeek(startDate); + const endISO = getISOWeek(endDate); + console.log(`📅 Semanas ISO: ${startISO.year}-W${startISO.week} → ${endISO.year}-W${endISO.week}`); + + const afterWeeks: string[] = []; + let currentYear = startISO.year; + let currentWeek = startISO.week; + + while (true) { + const weekKey = `${currentYear}-W${String(currentWeek).padStart(2, '0')}`; + afterWeeks.push(weekKey); + + if (currentYear === endISO.year && currentWeek === endISO.week) { + break; + } + + currentWeek++; + const weeksInYear = getWeeksInYear(currentYear); + if (currentWeek > weeksInYear) { + currentWeek = 1; + currentYear++; + } + } + + console.log(`📅 Semanas incluidas en AFTER: ${afterWeeks.join(', ')}`); + console.log(''); + + // 8. Calcular unidades en esas semanas + let afterUnits = 0; + let afterWeekCount = 0; + + console.log('📊 Unidades en semanas AFTER:'); + sortedWeeks.forEach(([week, data]) => { + if (afterWeeks.includes(week)) { + afterUnits += data.units; + afterWeekCount++; + console.log(` ✅ ${week}: ${data.units} unidades`); + } + }); + + console.log(''); + console.log(`📊 Total unidades en AFTER: ${afterUnits}`); + console.log(`📊 Número de semanas con datos: ${afterWeekCount}`); + console.log(`📊 Media calculada: ${afterUnits} / ${afterWeekCount} = ${(afterUnits / afterWeekCount).toFixed(2)}`); + console.log(''); + + // 9. Ver baseline (3 semanas antes) + const baselineEndTs = getISOWeekMonday(startISO.year, startISO.week); + const baselineStartTs = baselineEndTs - (afterWeeks.length * 7 * 86400000); + + console.log('📅 Baseline período:', new Date(baselineStartTs).toISOString().split('T')[0], '→', new Date(baselineEndTs).toISOString().split('T')[0]); + + // Calcular semanas del baseline + const baselineWeeks: string[] = []; + let baselineDate = new Date(baselineStartTs); + while (baselineDate.getTime() < baselineEndTs) { + const iso = getISOWeek(baselineDate); + const weekKey = `${iso.year}-W${String(iso.week).padStart(2, '0')}`; + if (!baselineWeeks.includes(weekKey)) { + baselineWeeks.push(weekKey); + } + baselineDate.setDate(baselineDate.getDate() + 7); + } + + console.log(`📅 Semanas del BASELINE: ${baselineWeeks.join(', ')}`); + + let beforeUnits = 0; + let beforeWeekCount = 0; + console.log('📊 Unidades en semanas BASELINE:'); + sortedWeeks.forEach(([week, data]) => { + if (baselineWeeks.includes(week)) { + beforeUnits += data.units; + beforeWeekCount++; + console.log(` ✅ ${week}: ${data.units} unidades`); + } + }); + + console.log(''); + console.log(`📊 Total unidades en BASELINE: ${beforeUnits}`); + console.log(`📊 Número de semanas con datos: ${beforeWeekCount}`); + console.log(`📊 Media calculada: ${beforeUnits} / ${beforeWeekCount} = ${(beforeUnits / beforeWeekCount).toFixed(2)}`); +} + +// Funciones helper ISO week +function getISOWeek(date: Date): { year: number; week: number } { + const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); + const dayNum = d.getUTCDay() || 7; + d.setUTCDate(d.getUTCDate() + 4 - dayNum); + const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); + const weekNum = Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7); + return { year: d.getUTCFullYear(), week: weekNum }; +} + +function getWeeksInYear(year: number): number { + const dec28 = new Date(Date.UTC(year, 11, 28)); + const iso = getISOWeek(dec28); + return iso.week; +} + +function getISOWeekMonday(year: number, week: number): number { + const jan4 = new Date(Date.UTC(year, 0, 4)); + const dayOfWeek = jan4.getUTCDay() || 7; + const week1Monday = new Date(Date.UTC(year, 0, 4 - (dayOfWeek - 1))); + return week1Monday.getTime() + (week - 1) * 7 * 86400000; +} + +debugBodynessExperiment().catch(console.error); diff --git a/debug_inkee.ts b/debug_inkee.ts new file mode 100644 index 0000000..6cbec9d --- /dev/null +++ b/debug_inkee.ts @@ -0,0 +1,50 @@ +import XLSX from 'xlsx'; +import { readFileSync } from 'fs'; +import { processAdsExcel } from './services/dataProcessor'; + +async function run() { + const buf = readFileSync('/tmp/Ads-Weekly.xlsx').buffer; + const adsData = await processAdsExcel(buf); + + // Show ALL INKEE DE ads with their cost/salesAds + const inkeeDE = adsData.filter(a => + a.asin.toUpperCase().startsWith('B0CQ') && + a.country === 'Amazon DE' + ); + + console.log('=== ALL INKEE DE ADS RECORDS ==='); + console.log('Count:', inkeeDE.length); + + // Group by ASIN + const byAsin = new Map(); + for (const a of inkeeDE) { + const list = byAsin.get(a.asin) || []; + list.push(a); + byAsin.set(a.asin, list); + } + + for (const [asin, records] of byAsin.entries()) { + const totalCost = records.reduce((s, r) => s + r.cost, 0); + const totalSales = records.reduce((s, r) => s + r.attributedSales30d, 0); + const totalClicks = records.reduce((s, r) => s + r.clicks, 0); + + console.log(`\nASIN: ${asin}`); + console.log(` Records: ${records.length} | TotalCost: ${totalCost.toFixed(2)} | TotalSales: ${totalSales.toFixed(2)} | TotalClicks: ${totalClicks}`); + + for (const r of records) { + console.log(` Year:${r.year} Week:${r.week} cost:${r.cost} salesAds:${r.attributedSales30d} clicks:${r.clicks}`); + } + } + + // Show acos calculation possibility + const withCost = inkeeDE.filter(a => a.cost > 0); + console.log('\nINKEE DE records with cost > 0:', withCost.length); + + // What week numbers are we talking about? + // INKEE DE experiment likely runs in Jan-Feb 2026 or similar + // Week 1 of 2026 = Jan 5-11, Week 6 = Feb 9-15, Week 8 = Feb 23 - Mar 1 + console.log('\nWeek 1 2026 start (Sun):', new Date(2026, 0, 4).toISOString().split('T')[0]); + console.log('Week 8 2026 start (Sun):', new Date(2026, 0, 4 + 7 * 7).toISOString().split('T')[0]); +} + +run().catch(console.error); diff --git a/debug_marketplace.ts b/debug_marketplace.ts new file mode 100644 index 0000000..9d3ce5c --- /dev/null +++ b/debug_marketplace.ts @@ -0,0 +1,42 @@ +import XLSX from 'xlsx'; +import { readFileSync } from 'fs'; +import { processAdsExcel, mergeSalesAndAdsData } from './services/dataProcessor'; + +async function run() { + const buf = readFileSync('/tmp/Ads-Weekly.xlsx').buffer; + const adsData = await processAdsExcel(buf); + + // Simulate with empty sales - ads-only records + const merged = mergeSalesAndAdsData([], adsData); + + console.log('Total merged records (ads-only):', merged.length); + + // Find INKEE DE ads-only records + const inkeeDE = merged.filter(m => + m.asin.startsWith('B0CQ') && + (m.marketplace || m.customer || '').includes('DE') + ); + + console.log('\nINKEE DE in merged:', inkeeDE.length); + for (const r of inkeeDE) { + console.log(` ASIN:${r.asin} year:${r.year} week:${r.week} marketplace:"${r.marketplace}" customer:"${r.customer}" cost:${r.cost} salesAds:${r.salesAds} clicks:${r.clicks}`); + } + + // Now test the aggregation (just for "DE" marketplace filter) + const asinSet = new Set(['B0CQ247T2V', 'B0CQTLZRCV']); + + for (const marketplace of ['DE', 'Amazon DE', 'de', 'amazon de', '']) { + const matching = inkeeDE.filter(r => { + const mkt = (r.marketplace || r.customer || '').toLowerCase(); + if (!marketplace || marketplace === 'All') return true; + return mkt.includes(marketplace.toLowerCase()); + }); + console.log(`\nFilter mkt="${marketplace}": ${matching.length} matching INKEE DE records`); + if (matching.length > 0) { + const totalCost = matching.reduce((s, r) => s + (r.cost || 0), 0); + console.log(` Total cost: ${totalCost.toFixed(2)}`); + } + } +} + +run().catch(console.error); diff --git a/debug_merge.ts b/debug_merge.ts new file mode 100644 index 0000000..60e5410 --- /dev/null +++ b/debug_merge.ts @@ -0,0 +1,42 @@ +import XLSX from 'xlsx'; +import { readFileSync } from 'fs'; +import { processAdsExcel, mergeSalesAndAdsData } from './services/dataProcessor'; + +async function run() { + // Load ads directly + const buf = readFileSync('/tmp/Ads-Weekly.xlsx').buffer; + const adsData = await processAdsExcel(buf); + + console.log('Total ads records:', adsData.length); + + // Find INKEE ads + const inkeeAds = adsData.filter(a => a.asin.includes('B0CQ')); + console.log('INKEE ads rows:', inkeeAds.length); + + if (inkeeAds.length > 0) { + console.log('INKEE ad years:', [...new Set(inkeeAds.map(a => a.year))]); + console.log('INKEE ad weeks:', [...new Set(inkeeAds.map(a => a.week))].sort((a, b) => a - b)); + console.log('INKEE cost sample:', inkeeAds.slice(0, 3).map(a => ({ year: a.year, week: a.week, asin: a.asin, cost: a.cost, country: a.country }))); + + // What does "DE" country map to? + const deAds = inkeeAds.filter(a => a.country.includes('DE')); + console.log('\nINKEE DE ads count:', deAds.length); + if (deAds.length > 0) { + console.log('First DE ad:', deAds[0]); + } + } + + // Now simulate merge with a mock sales record + const mockSales = [ + { + id: 'test', year: 2025, week: 1, asin: 'B0CQTLZRCV', customer: 'Amazon DE', + sellOut: 500, units: 5, month: 'Jan', sku: 'TEST', title: 'INKEE', line: 'Inkee', articleName: 'INKEE' + } + ] as any[]; + + const merged = mergeSalesAndAdsData(mockSales, adsData); + const inkeeRow = merged.find(m => m.asin === 'B0CQTLZRCV' && m.year === 2025); + console.log('\nMerged INKEE row:', inkeeRow ? { cost: inkeeRow.cost, salesAds: inkeeRow.salesAds, clicks: inkeeRow.clicks } : 'NOT FOUND'); +} + +run().catch(console.error); diff --git a/debug_timestamps.ts b/debug_timestamps.ts new file mode 100644 index 0000000..74145c0 --- /dev/null +++ b/debug_timestamps.ts @@ -0,0 +1,70 @@ +import XLSX from 'xlsx'; +import { readFileSync } from 'fs'; +import { processAdsExcel, mergeSalesAndAdsData } from './services/dataProcessor'; +import { computeDiD } from './services/experimentAnalysis'; +import { getExperimentAsins } from './services/experiments'; +import { Experiment, CombinedKPIs } from './types'; + +// Simulate the getWeekStartSunday function from experimentAnalysis.ts +function getWeekStartSunday(year: number, week: number): number { + const jan1 = new Date(year, 0, 1); + const day = jan1.getDay(); + const startYear = new Date(year, 0, 1 - day); + return startYear.getTime() + (week - 1) * 7 * 86400000; +} + +async function run() { + const buf = readFileSync('/tmp/Ads-Weekly.xlsx').buffer; + const adsData = await processAdsExcel(buf); + + const merged = mergeSalesAndAdsData([], adsData); + + // Show the timestamps for INKEE DE weeks + const inkeeDE = merged.filter(m => + m.asin.startsWith('B0CQ') && + (m.marketplace || m.customer || '').includes('DE') + ); + + console.log('=== INKEE DE TIMESTAMP ANALYSIS ==='); + for (const r of inkeeDE.slice(0, 5)) { + const weekStartTs = getWeekStartSunday(r.year, r.week); + const weekStartDate = new Date(weekStartTs).toISOString().split('T')[0]; + console.log(`ASIN:${r.asin} ${r.year}-W${r.week} weekStart:${weekStartDate} cost:${r.cost.toFixed(2)}`); + } + + console.log('\n=== WEEK DATE RANGES ==='); + for (let week = 1; week <= 8; week++) { + const ts2025 = getWeekStartSunday(2025, week); + const ts2026 = getWeekStartSunday(2026, week); + const d2025 = new Date(ts2025).toISOString().split('T')[0]; + const d2026 = new Date(ts2026).toISOString().split('T')[0]; + console.log(`Week ${week}: 2025 → ${d2025} | 2026 → ${d2026}`); + } + + // Now simulate an experiment that spans the existing data range + // The experiment "INKEE DE" - let's test with dates in Jan 2026 (W1-W4) + const testExperiment: Experiment = { + id: 'test', + name: 'INKEE DE Test', + type: 'advertising', + status: 'active', + asins: ['B0CQ247T2V', 'B0CQTLZRCV'], + control_asins: [], + marketplace: 'DE', + start_date: '2026-01-05', // Start of week 2, 2026 + end_date: '2026-02-22', // End of week 8, 2026 + baseline_start_date: '2025-01-06', // Week 2, 2025 + baseline_end_date: '2025-02-23', // Week 8, 2025 + primary_metric: 'acos', + notes: '', + owner: '' + }; + + console.log('\n=== DiD SIMULATION ==='); + const did = computeDiD(testExperiment, merged as CombinedKPIs[]); + console.log('ACOS:', did.metrics['acos']); + console.log('ROAS:', did.metrics['roas']); + console.log('Units:', did.metrics['units']); +} + +run().catch(console.error); diff --git a/docs/plans/Ads_Performance_Export_2026-02-26.xlsx b/docs/plans/Ads_Performance_Export_2026-02-26.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..b04b977b9669dd2d52f094584dc42133262e21d3 GIT binary patch literal 24466 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 literal 0 HcmV?d00001 diff --git a/inspect_ads.ts b/inspect_ads.ts new file mode 100644 index 0000000..583c9a5 --- /dev/null +++ b/inspect_ads.ts @@ -0,0 +1,30 @@ +import XLSX from 'xlsx'; +import { readFileSync } from 'fs'; + +const buf = readFileSync('/tmp/Ads-Weekly.xlsx'); +const wb = XLSX.read(buf, { type: 'buffer' }); + +console.log('=== SHEETS:', wb.SheetNames, '\n'); + +for (const sheetName of wb.SheetNames) { + const ws = wb.Sheets[sheetName]; + const rows: any[] = XLSX.utils.sheet_to_json(ws, { defval: '' }); + + const weeks = [...new Set(rows.map((r: any) => r.Week))].sort((a: any, b: any) => a - b); + console.log(`Sheet ${sheetName}: ${rows.length} rows | weeks ${weeks[0]} to ${weeks[weeks.length - 1]}`); + + const inkeeRows = rows.filter((r: any) => + String(r.ASIN || '').toUpperCase().startsWith('B0CQ') + ); + if (inkeeRows.length > 0) { + console.log(` INKEE rows: ${inkeeRows.length}`); + console.log(' First INKEE row:', JSON.stringify(inkeeRows[0])); + const inkeeWeeks = [...new Set(inkeeRows.map((r: any) => r.Week))].sort((a: any, b: any) => a - b); + console.log(' INKEE weeks:', inkeeWeeks.join(', ')); + } else { + console.log(` No INKEE rows (B0CQ...) in ${sheetName}`); + const deRows = rows.filter((r: any) => String(r.Country || '') === 'DE'); + const uniqueAsins = [...new Set(deRows.map((r: any) => r.ASIN))].slice(0, 8); + console.log(' Some DE ASINs:', uniqueAsins.join(', ')); + } +} diff --git a/inspect_user_ads.ts b/inspect_user_ads.ts new file mode 100644 index 0000000..888e9e2 --- /dev/null +++ b/inspect_user_ads.ts @@ -0,0 +1,37 @@ +import XLSX from 'xlsx'; +import { readFileSync } from 'fs'; + +const buf = readFileSync('/Users/christianvidalwolf/github/CrazeAnalytix/docs/plans/Ads_Performance_Export_2026-02-26.xlsx'); +const wb = XLSX.read(buf, { type: 'buffer' }); + +console.log('Sheets:', wb.SheetNames); + +for (const sheetName of wb.SheetNames) { + const ws = wb.Sheets[sheetName]; + const rows: any[] = XLSX.utils.sheet_to_json(ws, { defval: '' }); + + if (rows.length === 0) { console.log(`Sheet "${sheetName}" is empty`); continue; } + + console.log(`\n=== Sheet: "${sheetName}" (${rows.length} rows) ===`); + const headers = Object.keys(rows[0]); + console.log('Columns:', headers.join(' | ')); + console.log('First 3 rows:'); + rows.slice(0, 3).forEach(r => console.log(' ', JSON.stringify(r).slice(0, 300))); + + // Look for key columns + const weekCol = headers.find(h => /week|woche|semana/i.test(h)); + const asinCol = headers.find(h => /asin/i.test(h)); + const countryCol = headers.find(h => /country|marketplace|portfolio|land|país/i.test(h)); + const costCol = headers.find(h => /^cost$|^spend$|^ausgaben$|^gasto$/i.test(h) || (/cost|spend/i.test(h) && !/acos|of sales/i.test(h))); + const salesCol = headers.find(h => /sales|umsatz|ventas/i.test(h) && !/acos|cost of/i.test(h)); + + console.log(`\nKey columns: week="${weekCol}" | asin="${asinCol}" | country="${countryCol}" | cost="${costCol}" | sales="${salesCol}"`); + + if (weekCol && costCol) { + const weeks = [...new Set(rows.map(r => r[weekCol!]))]; + console.log('Unique weeks:', weeks.sort()); + const totalCost = rows.reduce((s, r) => s + (parseFloat(String(r[costCol!]).replace(',', '.')) || 0), 0); + const totalSales = salesCol ? rows.reduce((s, r) => s + (parseFloat(String(r[salesCol!]).replace(',', '.')) || 0), 0) : 0; + console.log(`Total cost: ${totalCost.toFixed(2)}, Total sales: ${totalSales.toFixed(2)}`); + } +} diff --git a/test-ads.ts b/test-ads.ts new file mode 100644 index 0000000..59f98c4 --- /dev/null +++ b/test-ads.ts @@ -0,0 +1,57 @@ +import fs from 'fs'; +import { processCSV } from './services/dataProcessor'; +import { processAdsExcel, mergeSalesAndAdsData } from './services/dataProcessor'; +import { computeDiD } from './services/experimentAnalysis'; + +async function runTest() { + console.log("Loading sales data..."); + const csvStr = fs.readFileSync('public/Craze analytix 2026.csv', 'utf-8'); + const sales = await processCSV(csvStr); + + console.log("Loading ads data..."); + const excelBuffer = fs.readFileSync('public/ads.xlsx').buffer; + const ads = await processAdsExcel(excelBuffer); + + console.log("Sales rows:", sales.length); + console.log("Ads rows:", ads.length); + + // Check INKEE DE explicitly + const inkeeSales = sales.filter(s => s.asin.includes('B0CQ')) + console.log("INKEE Sales rows:", inkeeSales.length); + + const inkeeAds = ads.filter(a => a.asin.includes('B0CQ')) + console.log("INKEE Ad rows:", inkeeAds.length); + + console.log("Merging..."); + const merged = mergeSalesAndAdsData(sales, ads); + + const mergedInkee = merged.filter(m => m.asin.includes('B0CQ') && m.year === 2024); + + const totalCost = mergedInkee.reduce((sum, r) => sum + (r.cost || 0), 0); + const totalAdSales = mergedInkee.reduce((sum, r) => sum + (r.salesAds || 0), 0); + + console.log("Merged INKEE total cost 2024:", totalCost); + console.log("Merged INKEE total ad sales 2024:", totalAdSales); + + const experiment = { + id: "INKEE_DE", + name: "INKEE DE Test", + status: "active", + type: "advertising", + asins: ["B0CQXNBGBQ"], // Assuming this is INKEE DE + control_asins: [], + marketplace: "Amazon DE", // Matching the UI + start_date: "2024-03-01", + end_date: "2024-04-15", + baseline_start_date: "2024-01-01", + baseline_end_date: "2024-02-28", + primary_metric: "acos" + } as any; + + console.log("Running DiD..."); + const did = computeDiD(experiment, merged); + console.log("DiD Output:", JSON.stringify(did.metrics, null, 2)); + +} + +runTest().catch(console.error); diff --git a/test-iso-weeks.ts b/test-iso-weeks.ts new file mode 100644 index 0000000..cc722b7 --- /dev/null +++ b/test-iso-weeks.ts @@ -0,0 +1,94 @@ +// Verificar fechas ISO para el experimento BODYNESS + +function getISOWeek(date: Date): { year: number; week: number } { + const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); + const dayNum = d.getUTCDay() || 7; + d.setUTCDate(d.getUTCDate() + 4 - dayNum); + const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); + const weekNum = Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7); + return { year: d.getUTCFullYear(), week: weekNum }; +} + +function getISOWeekMonday(year: number, week: number): number { + const jan4 = new Date(Date.UTC(year, 0, 4)); + const dayOfWeek = jan4.getUTCDay() || 7; + const week1Monday = new Date(Date.UTC(year, 0, 4 - (dayOfWeek - 1))); + return week1Monday.getTime() + (week - 1) * 7 * 86400000; +} + +function getWeeksInYear(year: number): number { + const dec28 = new Date(Date.UTC(year, 11, 28)); + const iso = getISOWeek(dec28); + return iso.week; +} + +function calculateISOWeekRange(startDate: Date, endDate: Date): { + numWeeks: number; + expandedStartTs: number; + expandedEndTs: number; + weekKeys: string[]; +} { + const startISO = getISOWeek(startDate); + const endISO = getISOWeek(endDate); + + const weekKeys: string[] = []; + let currentYear = startISO.year; + let currentWeek = startISO.week; + + while (true) { + const weekKey = `${currentYear}-W${String(currentWeek).padStart(2, '0')}`; + weekKeys.push(weekKey); + + if (currentYear === endISO.year && currentWeek === endISO.week) { + break; + } + + currentWeek++; + const weeksInYear = getWeeksInYear(currentYear); + if (currentWeek > weeksInYear) { + currentWeek = 1; + currentYear++; + } + } + + const expandedStartTs = getISOWeekMonday(startISO.year, startISO.week); + const expandedEndTs = getISOWeekMonday(endISO.year, endISO.week) + 7 * 86400000; + + return { + numWeeks: weekKeys.length, + expandedStartTs, + expandedEndTs, + weekKeys, + }; +} + +// Experimento: 22 enero - 4 febrero 2026 +const startDate = new Date('2026-01-22'); +const endDate = new Date('2026-02-04'); + +console.log('=== Experimento BODYNESS ES ==='); +console.log('Start:', startDate.toISOString().split('T')[0], '(day', startDate.getDay(), ')'); +console.log('End:', endDate.toISOString().split('T')[0], '(day', endDate.getDay(), ')'); +console.log(''); + +const startISO = getISOWeek(startDate); +const endISO = getISOWeek(endDate); +console.log('Start ISO:', `W${startISO.week}-${startISO.year}`); +console.log('End ISO:', `W${endISO.week}-${endISO.year}`); +console.log(''); + +const isoRange = calculateISOWeekRange(startDate, endDate); +console.log('ISO Week Range:'); +console.log(' numWeeks:', isoRange.numWeeks); +console.log(' weekKeys:', isoRange.weekKeys); +console.log(' expandedStart:', new Date(isoRange.expandedStartTs).toISOString().split('T')[0]); +console.log(' expandedEnd:', new Date(isoRange.expandedEndTs).toISOString().split('T')[0]); +console.log(''); + +// Verificar cada semana +isoRange.weekKeys.forEach(week => { + const [year, weekNum] = week.split('-W'); + const monday = getISOWeekMonday(Number(year), Number(weekNum)); + const sunday = monday + 6 * 86400000 + 86399999; + console.log(` ${week}: ${new Date(monday).toISOString().split('T')[0]} (Mon) → ${new Date(sunday).toISOString().split('T')[0]} (Sun)`); +}); diff --git a/test_did.ts b/test_did.ts new file mode 100644 index 0000000..df0a776 --- /dev/null +++ b/test_did.ts @@ -0,0 +1,40 @@ +import { computeDiD } from './services/experimentAnalysis'; +import { CombinedKPIs, Experiment } from './types'; + +const salesData: CombinedKPIs[] = [ + { + id: '1', marketplace: 'Amazon DE', customer: 'Amazon DE', month: 'Jan-24', week: 1, year: 2024, + asin: 'B0C123', title: 'Test', line: 'Test', sku: 'T-1', + salesTotal: 1000, unitsTotal: 100, salesAds: 500, unitsAds: 50, cost: 100, + clicks: 100, impressions: 1000, conversions: 5, salesOrganic: 500, unitsOrganic: 50, + paidSalesShare: 50, organicSalesShare: 50, acos: 20, tacos: 10, roas: 5, ctr: 10, cpc: 1, cvrUnits: 5, glanceViews: 1000 + }, + { + id: '2', marketplace: 'Amazon DE', customer: 'Amazon DE', month: 'Feb-24', week: 5, year: 2024, + asin: 'B0C123', title: 'Test', line: 'Test', sku: 'T-1', + salesTotal: 2000, unitsTotal: 200, salesAds: 2000, unitsAds: 200, cost: 200, + clicks: 200, impressions: 2000, conversions: 10, salesOrganic: 0, unitsOrganic: 0, + paidSalesShare: 100, organicSalesShare: 0, acos: 10, tacos: 10, roas: 10, ctr: 10, cpc: 1, cvrUnits: 5, glanceViews: 2000 + } +]; + +const experiment: Experiment = { + id: 'exp1', + name: 'Test Exp', + type: 'advertising', + status: 'active', + asins: ['B0C123'], + control_asins: [], + marketplace: 'DE', + start_date: '2024-01-20', + end_date: '2024-02-28', + baseline_start_date: '2023-12-01', + baseline_end_date: '2024-01-19', + created_at: '', + updated_at: '', + primary_metric: 'acos', + changes: [] +}; + +const result = computeDiD(experiment, salesData); +console.log(JSON.stringify(result.metrics, null, 2));