mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 14:05:24 +02:00
fix(buybox): rewrite processBuyBoxExcel with ASIN regex detection
Replace fragile header/column-index approach with pattern-based ASIN detection:
- Scan all cells for Amazon ASIN pattern (B[0-9A-Z]{9}) to auto-detect the ASIN column regardless of headers or position
- Detect country from sheet name with flexible matching (no hardcoded sheet name list)
- Detect reason column from header keywords, no hardcoded column indices
- Remove all hardcoded fallback indices (col 13, 18, 9, etc.) that caused empty results
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
036e7b9a0a
commit
249fba5390
+89
-134
@@ -2513,160 +2513,115 @@ export const processStockExcel = async (fileOrBuffer: File | ArrayBuffer): Promi
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const BB_SHEET_CONFIG: { sheet: string; country: string }[] = [
|
|
||||||
{ sheet: 'BB_FR', country: 'FR' },
|
|
||||||
{ sheet: 'BB_UK', country: 'UK' },
|
|
||||||
{ sheet: 'BB_DE', country: 'DE' },
|
|
||||||
{ sheet: 'BB_IT', country: 'IT' },
|
|
||||||
{ sheet: 'BB_ES', country: 'ES' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const processBuyBoxExcel = async (fileOrBuffer: File | ArrayBuffer): Promise<Map<string, { countries: string[]; reasons: Record<string, string> }>> => {
|
export const processBuyBoxExcel = async (fileOrBuffer: File | ArrayBuffer): Promise<Map<string, { countries: string[]; reasons: Record<string, string> }>> => {
|
||||||
try {
|
try {
|
||||||
const arrayBuffer = fileOrBuffer instanceof File
|
const arrayBuffer = fileOrBuffer instanceof File ? await fileOrBuffer.arrayBuffer() : fileOrBuffer;
|
||||||
? await fileOrBuffer.arrayBuffer()
|
|
||||||
: fileOrBuffer;
|
|
||||||
const workbook = XLSX.read(arrayBuffer, { type: 'array' });
|
const workbook = XLSX.read(arrayBuffer, { type: 'array' });
|
||||||
|
|
||||||
// Map: ASIN -> { countries: [], reasons: {} }
|
|
||||||
const buyBoxMap = new Map<string, { countries: string[]; reasons: Record<string, string> }>();
|
const buyBoxMap = new Map<string, { countries: string[]; reasons: Record<string, string> }>();
|
||||||
|
|
||||||
// Log available sheets for debugging
|
// Amazon ASIN pattern: B followed by exactly 9 alphanumeric chars
|
||||||
|
const ASIN_REGEX = /^B[0-9A-Z]{9}$/;
|
||||||
|
const RESOLVED = new Set(['fixed', 'ok', 'hecho', 'solucionado', 'corrected', 'resolved', 'resuelto', 'done', 'listo']);
|
||||||
|
|
||||||
console.log('[BuyBox] Available sheets:', workbook.SheetNames.join(', '));
|
console.log('[BuyBox] Available sheets:', workbook.SheetNames.join(', '));
|
||||||
|
|
||||||
for (const config of BB_SHEET_CONFIG) {
|
// Detect country code from sheet name (flexible matching)
|
||||||
// Find sheet case-insensitively and with flexible separators (BB_ES, BB-ES, BB ES, ES BB)
|
const detectCountry = (name: string): string | null => {
|
||||||
const sheetName = workbook.SheetNames.find(name => {
|
const n = name.toUpperCase().replace(/[_\- ]/g, '');
|
||||||
const n = name.toUpperCase().replace(/[-_ ]/g, '');
|
if (n.includes('FRANCE') || n.includes('FRANC') || n.includes('BBFR') || n === 'FR') return 'FR';
|
||||||
const target = config.sheet.toUpperCase().replace(/[-_ ]/g, '');
|
if (n.includes('UNITEDKINGDOM') || n.includes('REINOUNIDO') || n.includes('BBUK') || n === 'UK' || n === 'GB') return 'UK';
|
||||||
const country = config.country.toUpperCase();
|
if (n.includes('GERMANY') || n.includes('DEUTSCH') || n.includes('ALEMAN') || n.includes('BBDE') || n === 'DE') return 'DE';
|
||||||
// Match if:
|
if (n.includes('ITALY') || n.includes('ITALIA') || n.includes('BBIT') || n === 'IT') return 'IT';
|
||||||
// 1. Exact pattern (BB_ES)
|
if (n.includes('SPAIN') || n.includes('ESPAÑA') || n.includes('ESPANA') || n.includes('BBES') || n === 'ES') return 'ES';
|
||||||
// 2. Just the country code (ES)
|
return null;
|
||||||
// 3. Contains 'BB' and the country code
|
};
|
||||||
// 4. Special cases for Spain (SPAIN, ESPAÑA)
|
|
||||||
return n === target ||
|
|
||||||
n === country ||
|
|
||||||
(n.includes('BB') && n.includes(country)) ||
|
|
||||||
(country === 'ES' && (n.includes('SPAIN') || n.includes('ESPAÑA') || n.includes('ESPANA')));
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!sheetName) {
|
for (const sheetName of workbook.SheetNames) {
|
||||||
console.warn(`[BuyBox] Sheet matching ${config.sheet} not found, skipping...`);
|
const country = detectCountry(sheetName);
|
||||||
|
if (!country) {
|
||||||
|
console.log(`[BuyBox] Sheet "${sheetName}" — no country detected, skipping`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const worksheet = workbook.Sheets[sheetName];
|
const worksheet = workbook.Sheets[sheetName];
|
||||||
const jsonData: any[][] = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
|
const rows: any[][] = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: '' });
|
||||||
if (jsonData.length === 0) continue;
|
if (!rows.length) continue;
|
||||||
|
|
||||||
// Find the header row (first row with ASIN or similar)
|
// Detect ASIN column by scanning first 100 rows for cells matching ASIN pattern
|
||||||
let headerRowIdx = 0;
|
const asinHits: number[] = [];
|
||||||
let headers: any[] = jsonData[0] || [];
|
const scanLimit = Math.min(rows.length, 100);
|
||||||
for (let r = 0; r < Math.min(jsonData.length, 10); r++) {
|
for (let r = 0; r < scanLimit; r++) {
|
||||||
const rowData = jsonData[r];
|
const row = rows[r] || [];
|
||||||
if (!rowData) continue;
|
for (let c = 0; c < row.length; c++) {
|
||||||
const isHeader = rowData.some(cell => {
|
if (ASIN_REGEX.test(String(row[c] || '').trim().toUpperCase())) {
|
||||||
const s = String(cell || '').toUpperCase().trim();
|
asinHits[c] = (asinHits[c] || 0) + 1;
|
||||||
return s === 'ASIN' || s.includes('AMAZON ASIN') || s.includes('PRODUCT ID') || s.includes('SKU');
|
|
||||||
});
|
|
||||||
if (isHeader) {
|
|
||||||
headerRowIdx = r;
|
|
||||||
headers = rowData;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let finalAsinIdx = headers.findIndex(h => {
|
|
||||||
const s = String(h || '').toUpperCase().replace(/[^A-Z]/g, '');
|
|
||||||
return s === 'ASIN' || s.includes('AMAZONASIN') || s.includes('CHILDASIN');
|
|
||||||
});
|
|
||||||
|
|
||||||
if (finalAsinIdx === -1) {
|
|
||||||
finalAsinIdx = headers.findIndex(h => {
|
|
||||||
const s = String(h || '').toUpperCase().replace(/[^A-Z]/g, '');
|
|
||||||
return s.includes('IDENTIFIER') || s.includes('PRODUCTID') || s.includes('SKU');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (finalAsinIdx === -1) finalAsinIdx = 1; // Default to Col B if not found
|
|
||||||
|
|
||||||
// Dynamically find the "Issue Type" or "Reason" column
|
|
||||||
let reasonIdx = headers.findIndex(h => {
|
|
||||||
const s = String(h || '').toUpperCase();
|
|
||||||
return s.includes('ISSUE TYPE') || s.includes('REASON') || s.includes('BUY BOX STATUS') || s.includes('LBB REASON') || s.includes('ESTADO BB') || s.includes('COMENTARIO') || s.includes('MOTIVO') || s.includes('CAUSA') || s.includes('OBSERVACIONES') || s.includes('DETALLES') || s.includes('JUSTIFICACIÓN') || s.includes('SITUACIÓN') || s.includes('STATUS');
|
|
||||||
});
|
|
||||||
|
|
||||||
// If still not found, check for exact matches of common Spanish headers
|
|
||||||
if (reasonIdx === -1) {
|
|
||||||
reasonIdx = headers.findIndex(h => {
|
|
||||||
const s = String(h || '').toUpperCase().trim();
|
|
||||||
return s === 'COMENTARIOS' || s === 'OBSERVACIONES' || s === 'MOTIVO' || s === 'ESTADO';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to previous hardcoded indices if header search fails or for specific known sheet structures
|
|
||||||
if (reasonIdx === -1 || (config.country === 'FR' && reasonIdx !== 18) || (config.country === 'UK' && reasonIdx !== 9) || (config.country === 'DE' && reasonIdx !== 13) || (config.country === 'ES' && reasonIdx !== 13)) {
|
|
||||||
if (config.country === 'FR') reasonIdx = 18; // Force Column S for FR (Index 18)
|
|
||||||
else if (config.country === 'UK') reasonIdx = 9; // Force Column J for UK (Index 9)
|
|
||||||
else if (config.country === 'DE') reasonIdx = 13; // Force Column N for DE (Index 13)
|
|
||||||
else if (config.country === 'ES') reasonIdx = 13; // Force Column N for ES (Index 13)
|
|
||||||
else if (reasonIdx === -1) {
|
|
||||||
if (config.country === 'IT') reasonIdx = 12;
|
|
||||||
else reasonIdx = 13;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also force ASIN column for DE if not correctly detected
|
|
||||||
if (config.country === 'DE') finalAsinIdx = 1; // Force Column B for Germany (Index 1)
|
|
||||||
if (config.country === 'ES') {
|
|
||||||
console.log(`[BuyBox Debug] ES Headers found:`, headers);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip header row and all rows above it
|
|
||||||
for (let i = headerRowIdx + 1; i < jsonData.length; i++) {
|
|
||||||
const row = jsonData[i];
|
|
||||||
if (!row || row.length <= Math.max(finalAsinIdx, reasonIdx)) continue;
|
|
||||||
|
|
||||||
const rawAsin = String(row[finalAsinIdx] || '').trim().toUpperCase();
|
|
||||||
if (!rawAsin || rawAsin.length < 5) continue;
|
|
||||||
|
|
||||||
const rawReason = String(row[reasonIdx] || '').trim() || 'BB Lost';
|
|
||||||
|
|
||||||
// Debug specific ASIN reported by user
|
|
||||||
const isTargetAsin = rawAsin === 'B0D3874WSD' || rawAsin === 'B095K8948Z';
|
|
||||||
if (isTargetAsin) {
|
|
||||||
console.log(`[BuyBox Debug] Found ASIN ${rawAsin} in ${config.country}. Row data at reasonIdx (${reasonIdx}): "${row[reasonIdx]}", Processed Reason: "${rawReason}"`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only consider as BB lost if not marked as resolved
|
|
||||||
const lowReason = rawReason.toLowerCase();
|
|
||||||
if (lowReason === 'fixed' || lowReason === 'ok' || lowReason === 'hecho' || lowReason === 'solucionado' || lowReason === 'corrected') {
|
|
||||||
if (isTargetAsin) {
|
|
||||||
console.log(`[BuyBox Debug] SKIPPING ASIN ${rawAsin} in ${config.country} because reason matches resolved status ("${rawReason}")`);
|
|
||||||
}
|
}
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let entry = buyBoxMap.get(rawAsin);
|
|
||||||
if (!entry) {
|
|
||||||
entry = { countries: [], reasons: {} };
|
|
||||||
buyBoxMap.set(rawAsin, entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!entry.countries.includes(config.country)) {
|
|
||||||
entry.countries.push(config.country);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect reason for this country
|
|
||||||
entry.reasons[config.country] = rawReason;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Column with most ASIN-pattern matches wins
|
||||||
|
let asinColIdx = -1;
|
||||||
|
let maxHits = 0;
|
||||||
|
asinHits.forEach((count, idx) => {
|
||||||
|
if (count > maxHits) { maxHits = count; asinColIdx = idx; }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fallback: look for a header cell containing "ASIN"
|
||||||
|
if (asinColIdx === -1) {
|
||||||
|
for (let r = 0; r < Math.min(rows.length, 15); r++) {
|
||||||
|
const idx = (rows[r] || []).findIndex((cell: any) =>
|
||||||
|
String(cell || '').toUpperCase().includes('ASIN'));
|
||||||
|
if (idx !== -1) { asinColIdx = idx; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (asinColIdx === -1) {
|
||||||
|
console.warn(`[BuyBox] Sheet "${sheetName}" (${country}): ASIN column not found, skipping`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect reason column from header row keywords
|
||||||
|
let reasonColIdx = -1;
|
||||||
|
for (let r = 0; r < Math.min(rows.length, 15); r++) {
|
||||||
|
const idx = (rows[r] || []).findIndex((cell: any) => {
|
||||||
|
const s = String(cell || '').toUpperCase();
|
||||||
|
return s.includes('ISSUE') || s.includes('REASON') || s.includes('STATUS') ||
|
||||||
|
s.includes('MOTIVO') || s.includes('CAUSA') || s.includes('ESTADO') ||
|
||||||
|
s.includes('COMMENT') || s.includes('OBSERV') || s.includes('SITUAC') ||
|
||||||
|
s.includes('LBB') || s.includes('PROBLEMA') || s.includes('NOTE') ||
|
||||||
|
s.includes('JUSTIF') || s.includes('DETALL');
|
||||||
|
});
|
||||||
|
if (idx !== -1) { reasonColIdx = idx; break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[BuyBox] Sheet "${sheetName}" → ${country}: asinCol=${asinColIdx} (${maxHits} ASINs found), reasonCol=${reasonColIdx}`);
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
for (const row of rows) {
|
||||||
|
if (!row || row.length <= asinColIdx) continue;
|
||||||
|
const rawAsin = String(row[asinColIdx] || '').trim().toUpperCase();
|
||||||
|
if (!ASIN_REGEX.test(rawAsin)) continue;
|
||||||
|
|
||||||
|
const rawReason = reasonColIdx !== -1 && row.length > reasonColIdx
|
||||||
|
? String(row[reasonColIdx] || '').trim()
|
||||||
|
: '';
|
||||||
|
if (RESOLVED.has(rawReason.toLowerCase())) continue;
|
||||||
|
|
||||||
|
const reason = rawReason || 'BB Lost';
|
||||||
|
let entry = buyBoxMap.get(rawAsin);
|
||||||
|
if (!entry) { entry = { countries: [], reasons: {} }; buyBoxMap.set(rawAsin, entry); }
|
||||||
|
if (!entry.countries.includes(country)) entry.countries.push(country);
|
||||||
|
entry.reasons[country] = reason;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[BuyBox] Sheet "${sheetName}": ${count} BB lost entries for ${country}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[BuyBox] Processed ${buyBoxMap.size} total ASINs with BB lost across all countries`);
|
console.log(`[BuyBox] Total: ${buyBoxMap.size} ASINs with BB issues`);
|
||||||
return buyBoxMap;
|
return buyBoxMap;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error processing Buy Box Excel:", error);
|
console.error('[BuyBox] Error processing Excel:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user