fix(dataProcessor): parse DD/M/YY dates from column C in sell-out CSV

Add support for European day-first date format (e.g. "23/2/26" = 23 Feb 2026)
in the sell-out CSV pipeline so months and years are correctly extracted from
column C of Amazon Sell Out 2023-2025.csv.

- normalizeMonth: detect DD/MM/YY when first part > 12, return "Mon-YY"
- mapRowToRecord: add 'C', 'Date', 'Fecha', 'DATA' to month column aliases
- validateSellOutHeaders: accept date columns as substitute for YEAR + MONTH

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Christian Vidal Wolf
2026-03-03 13:07:59 +01:00
co-authored by Claude Sonnet 4.6
parent ad1695d360
commit 2eed94b94b
10 changed files with 112 additions and 747 deletions
+28 -5
View File
@@ -193,9 +193,30 @@ const normalizeMonth = (rawMonth: string): string => {
}
// 2. Handle numeric months "01", "1", "01-2023"
// If it's a full date string like "2023-04-01" or "01/04/2023"
// If it's a full date string like "2023-04-01", "01/04/2023", or "23/2/26" (DD/M/YY)
if (m.includes('/') || m.includes('-')) {
// Try parsing standard date
// Handle DD/M/YY or DD/MM/YY (European day-first format, e.g. "23/2/26" = 23 Feb 2026)
const parts = m.split(m.includes('/') ? '/' : '-');
if (parts.length === 3) {
const [a, b, c] = parts.map(p => parseInt(p, 10));
if (!isNaN(a) && !isNaN(b) && !isNaN(c)) {
// If first part > 12: definitely day-first → DD/MM/YY
if (a > 12 && b >= 1 && b <= 12) {
const yearShort = c < 100 ? String(c).padStart(2, '0') : String(c).slice(2);
const result = `${MONTH_ORDER[b - 1]}-${yearShort}`;
monthCache[rawMonth] = result;
return result;
}
// YYYY/MM/DD or YYYY-MM-DD (ISO-like, year is 4 digits in first position)
if (a > 31 && b >= 1 && b <= 12) {
const yearShort = String(a).slice(2);
const result = `${MONTH_ORDER[b - 1]}-${yearShort}`;
monthCache[rawMonth] = result;
return result;
}
}
}
// Try parsing standard date as fallback
const date = new Date(m);
if (!isNaN(date.getTime())) {
const monthIdx = date.getMonth();
@@ -289,8 +310,10 @@ const isAllowedCustomer = (customer: string): boolean => {
export const validateSellOutHeaders = (headers: string[]) => {
const normHeaders = headers.map(h => String(h).trim().toLowerCase());
const hasYear = normHeaders.includes('year');
const hasTime = normHeaders.includes('month') || normHeaders.includes('week');
// A date column (e.g. column named "C", "Date", "Fecha") can provide both year and month
const hasDateCol = ['c', 'date', 'fecha', 'data'].some(d => normHeaders.includes(d));
const hasYear = normHeaders.includes('year') || hasDateCol;
const hasTime = normHeaders.includes('month') || normHeaders.includes('week') || hasDateCol;
const hasCustomerRef = normHeaders.includes('customer reference') || normHeaders.includes('asin');
const hasEan = normHeaders.includes('ean');
const hasUnits = normHeaders.includes('units');
@@ -315,7 +338,7 @@ const mapRowToRecord = (row: any, index: number): SalesRecord => {
const yearStr = getColumnValue(row, ['YEAR', 'Year', 'D']);
// Sanitize year string before parsing (remove commas/dots e.g. "2,023")
let year = parseInt(yearStr.replace(/[,.]/g, '')) || 0;
const monthStr = getColumnValue(row, ['MONTH', 'Month', 'Period']);
const monthStr = getColumnValue(row, ['MONTH', 'Month', 'Period', 'C', 'Date', 'DATE', 'Fecha', 'FECHA', 'Data', 'DATA']);
const month = normalizeMonth(monthStr);
// BACKFILL YEAR if missing but present in Month (e.g. "Apr-23")