mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 11:45:23 +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> }>> => {
|
||||
try {
|
||||
const arrayBuffer = fileOrBuffer instanceof File
|
||||
? await fileOrBuffer.arrayBuffer()
|
||||
: fileOrBuffer;
|
||||
const arrayBuffer = fileOrBuffer instanceof File ? await fileOrBuffer.arrayBuffer() : fileOrBuffer;
|
||||
const workbook = XLSX.read(arrayBuffer, { type: 'array' });
|
||||
|
||||
// Map: ASIN -> { countries: [], reasons: {} }
|
||||
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(', '));
|
||||
|
||||
for (const config of BB_SHEET_CONFIG) {
|
||||
// Find sheet case-insensitively and with flexible separators (BB_ES, BB-ES, BB ES, ES BB)
|
||||
const sheetName = workbook.SheetNames.find(name => {
|
||||
const n = name.toUpperCase().replace(/[-_ ]/g, '');
|
||||
const target = config.sheet.toUpperCase().replace(/[-_ ]/g, '');
|
||||
const country = config.country.toUpperCase();
|
||||
// Match if:
|
||||
// 1. Exact pattern (BB_ES)
|
||||
// 2. Just the country code (ES)
|
||||
// 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')));
|
||||
});
|
||||
// Detect country code from sheet name (flexible matching)
|
||||
const detectCountry = (name: string): string | null => {
|
||||
const n = name.toUpperCase().replace(/[_\- ]/g, '');
|
||||
if (n.includes('FRANCE') || n.includes('FRANC') || n.includes('BBFR') || n === 'FR') return 'FR';
|
||||
if (n.includes('UNITEDKINGDOM') || n.includes('REINOUNIDO') || n.includes('BBUK') || n === 'UK' || n === 'GB') return 'UK';
|
||||
if (n.includes('GERMANY') || n.includes('DEUTSCH') || n.includes('ALEMAN') || n.includes('BBDE') || n === 'DE') return 'DE';
|
||||
if (n.includes('ITALY') || n.includes('ITALIA') || n.includes('BBIT') || n === 'IT') return 'IT';
|
||||
if (n.includes('SPAIN') || n.includes('ESPAÑA') || n.includes('ESPANA') || n.includes('BBES') || n === 'ES') return 'ES';
|
||||
return null;
|
||||
};
|
||||
|
||||
if (!sheetName) {
|
||||
console.warn(`[BuyBox] Sheet matching ${config.sheet} not found, skipping...`);
|
||||
for (const sheetName of workbook.SheetNames) {
|
||||
const country = detectCountry(sheetName);
|
||||
if (!country) {
|
||||
console.log(`[BuyBox] Sheet "${sheetName}" — no country detected, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const worksheet = workbook.Sheets[sheetName];
|
||||
const jsonData: any[][] = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
|
||||
if (jsonData.length === 0) continue;
|
||||
const rows: any[][] = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: '' });
|
||||
if (!rows.length) continue;
|
||||
|
||||
// Find the header row (first row with ASIN or similar)
|
||||
let headerRowIdx = 0;
|
||||
let headers: any[] = jsonData[0] || [];
|
||||
for (let r = 0; r < Math.min(jsonData.length, 10); r++) {
|
||||
const rowData = jsonData[r];
|
||||
if (!rowData) continue;
|
||||
const isHeader = rowData.some(cell => {
|
||||
const s = String(cell || '').toUpperCase().trim();
|
||||
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}")`);
|
||||
// Detect ASIN column by scanning first 100 rows for cells matching ASIN pattern
|
||||
const asinHits: number[] = [];
|
||||
const scanLimit = Math.min(rows.length, 100);
|
||||
for (let r = 0; r < scanLimit; r++) {
|
||||
const row = rows[r] || [];
|
||||
for (let c = 0; c < row.length; c++) {
|
||||
if (ASIN_REGEX.test(String(row[c] || '').trim().toUpperCase())) {
|
||||
asinHits[c] = (asinHits[c] || 0) + 1;
|
||||
}
|
||||
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;
|
||||
} catch (error) {
|
||||
console.error("Error processing Buy Box Excel:", error);
|
||||
console.error('[BuyBox] Error processing Excel:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user