Fix: implement definitive revenue integrity filter with ASIN validation and strict marketplace whitelist

This commit is contained in:
Christian Vidal Wolf
2026-04-20 13:19:14 +02:00
parent 862b687835
commit 117aa0626f
2 changed files with 45 additions and 20 deletions
+5 -12
View File
@@ -5,7 +5,7 @@ import Dashboard from './components/Dashboard';
import FilterBar from './components/FilterBar'; import FilterBar from './components/FilterBar';
import AIChat from './components/AIChat'; import AIChat from './components/AIChat';
import CrazeLogo from './components/CrazeLogo'; import CrazeLogo from './components/CrazeLogo';
import { processCSV, processExcel, filterData, aggregateData, processAdsCSV, processAdsExcel, mergeSalesAndAdsData, processTrafficExcel, processStockExcel, filterAdsData, calculateForecastViewData, processVendorStockExcel, processUKInventoryExcel, calculateVelocityMap, getUniqueValues, processForecastExcel, processBuyBoxExcel, processBSRExcel, filterBsrData } from './services/dataProcessor'; import { processCSV, processExcel, filterData, aggregateData, processAdsCSV, processAdsExcel, mergeSalesAndAdsData, processTrafficExcel, processStockExcel, filterAdsData, calculateForecastViewData, processVendorStockExcel, processUKInventoryExcel, calculateVelocityMap, getUniqueValues, processForecastExcel, processBuyBoxExcel, processBSRExcel, filterBsrData, isAllowedCustomer, isRealSale } from './services/dataProcessor';
import { SalesRecord, FilterState, AggregatedData, AdsRecord, TrafficRecord, ForecastRecord, ProductForecastData, ActiveExperiment, BSRRecord } from './types'; import { SalesRecord, FilterState, AggregatedData, AdsRecord, TrafficRecord, ForecastRecord, ProductForecastData, ActiveExperiment, BSRRecord } from './types';
import { queryGemini } from './services/geminiService'; import { queryGemini } from './services/geminiService';
import { ChartIcon, TableIcon, CloseIcon, TrendingIcon, MegaphoneIcon } from './components/Icons'; import { ChartIcon, TableIcon, CloseIcon, TrendingIcon, MegaphoneIcon } from './components/Icons';
@@ -537,18 +537,11 @@ const App: React.FC = () => {
const filteredData = useMemo(() => { const filteredData = useMemo(() => {
const baseFiltered = filterData(rawData, deferredFilters, stockMap, vendorStockMap, top50Mode); const baseFiltered = filterData(rawData, deferredFilters, stockMap, vendorStockMap, top50Mode);
// Safety check: Final redundancy to ensure marketing/ad spend records never reach the dashboard cards. // DEFINITIVE REVENUE FILTER (BRUTE FORCE):
// This is a "double-lock" to prevent revenue inflation even if the service layer fails to update. // This is the absolute final safeguard against revenue inflation.
// It enforces the strict isRealSale and isAllowedCustomer rules at the UI layer.
return baseFiltered.filter(r => { return baseFiltered.filter(r => {
if (r.units === 0) return false; return isAllowedCustomer(r.customer) && isRealSale(r);
// Marketplace check
const normC = r.customer?.trim().toLowerCase();
const isAmazon = normC.startsWith('amazon') || normC === 'pan-eu';
const isOnlyCountry = ['uk', 'de', 'it', 'fr', 'es', 'nl', 'se', 'pl', 'be'].includes(normC);
if (isOnlyCountry && !normC.startsWith('amazon')) return false;
return isAmazon;
}); });
}, [rawData, deferredFilters, stockMap, vendorStockMap, top50Mode]); }, [rawData, deferredFilters, stockMap, vendorStockMap, top50Mode]);
console.timeEnd('filteredData'); console.timeEnd('filteredData');
+40 -8
View File
@@ -307,16 +307,48 @@ const isAllowedCustomer = (customer: string): boolean => {
if (!customer) return false; if (!customer) return false;
const normCustomer = customer.trim().toLowerCase(); const normCustomer = customer.trim().toLowerCase();
// Strict business rule: Only records from recognized Amazon marketplaces are included in Sell-Out revenue. // STRICT WHITELIST: Only these Exact Names are valid for Sell-Out Revenue.
// This excludes marketing spend, financial adjustments, and non-Amazon channels. const whitelist = [
const isAmazon = normCustomer.startsWith('amazon') || normCustomer === 'pan-eu'; 'amazon de', 'amazon uk', 'amazon fr', 'amazon es', 'amazon it',
'amazon nl', 'amazon pl', 'amazon be', 'amazon se', 'amazon sc',
'pan-eu'
];
// Additional check: Ensure it's not JUST a country code (often used in ad-spend records) if (!whitelist.includes(normCustomer)) return false;
const isOnlyCountryCode = ['uk', 'de', 'it', 'fr', 'es', 'nl', 'se', 'pl', 'be'].includes(normCustomer); return true;
};
if (isOnlyCountryCode && !normCustomer.startsWith('amazon')) return false; /**
* Validates if a record is a real sale or a marketing/spend entry.
* Marketing entries often have placeholder units (1) or missing ASINs.
*/
export const isRealSale = (record: any): boolean => {
const asin = String(record.asin || '').toUpperCase().trim();
const sku = String(record.sku || '').toUpperCase().trim();
const title = String(record.title || '').toUpperCase().trim();
const line = String(record.line || '').toUpperCase().trim();
return isAmazon; // 1. Strict ASIN Format: Amazon ASINs are 10 letters/numbers
if (!asin || asin.length < 5 || asin.length > 20) return false;
// 2. Negative Keywords: Exclude records that suggest marketing or spend instead of a product
const negativeKeywords = [
'SPEND', 'MARKETING', 'ADVERTISING', 'AD-SPEND', 'AD_SPEND',
'SUMMARY', 'TOTAL', 'ADJUSTMENT', 'FINANCIAL', 'CREDIT', 'FEE'
];
for (const kw of negativeKeywords) {
if (asin.includes(kw)) return false;
if (sku.includes(kw)) return false;
if (title.includes(kw)) return false;
if (line.includes(kw)) return false;
}
// 3. Units check: Real sales must have units.
// Records with 0 units or negative units (unless it's a return, but usually returns have an ASIN)
if (!record.units || record.units <= 0) return false;
return true;
}; };
// --- SALES / SELL OUT MAPPING --- // --- SALES / SELL OUT MAPPING ---
@@ -1165,7 +1197,7 @@ export const filterData = (
// These filters ensure that only valid Amazon sales are aggregated, // These filters ensure that only valid Amazon sales are aggregated,
// excluding ad spend and marketing records that might be present in the raw source. // excluding ad spend and marketing records that might be present in the raw source.
if (!isAllowedCustomer(item.customer)) return false; if (!isAllowedCustomer(item.customer)) return false;
if (item.units === 0) return false; if (!isRealSale(item)) return false;
// 1. Month Logic: Handle "Apr-23" matching "Apr" filter // 1. Month Logic: Handle "Apr-23" matching "Apr" filter
const recordMonth = item.month; // e.g. "Apr-23" const recordMonth = item.month; // e.g. "Apr-23"