store-scrapper-js-common 1.0.215 → 1.0.219
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -18,7 +18,7 @@ function cleanSet(arr, lower) {
|
|
|
18
18
|
}
|
|
19
19
|
const finite = (n) => typeof n === 'number' && Number.isFinite(n);
|
|
20
20
|
function evalFilter(product, filter) {
|
|
21
|
-
var _a, _b, _c
|
|
21
|
+
var _a, _b, _c;
|
|
22
22
|
const categories = cleanSet(filter.categories, true);
|
|
23
23
|
const brands = cleanSet(filter.brands, true);
|
|
24
24
|
const stores = cleanSet(filter.stores, false);
|
|
@@ -31,12 +31,12 @@ function evalFilter(product, filter) {
|
|
|
31
31
|
return false;
|
|
32
32
|
}
|
|
33
33
|
if (categories) {
|
|
34
|
-
const
|
|
35
|
-
if (!
|
|
34
|
+
const productCats = cleanSet(product.inferredCategories, true);
|
|
35
|
+
if (!productCats || !productCats.some((pc) => categories.includes(pc)))
|
|
36
36
|
return false;
|
|
37
37
|
}
|
|
38
38
|
if (brands) {
|
|
39
|
-
const b = (
|
|
39
|
+
const b = (_a = product.brandName) === null || _a === void 0 ? void 0 : _a.toLowerCase();
|
|
40
40
|
if (!b || !brands.includes(b))
|
|
41
41
|
return false;
|
|
42
42
|
}
|
|
@@ -45,7 +45,7 @@ function evalFilter(product, filter) {
|
|
|
45
45
|
return false;
|
|
46
46
|
}
|
|
47
47
|
if (minDiscountPct !== null) {
|
|
48
|
-
const pct = (
|
|
48
|
+
const pct = (_b = product.priceStats) === null || _b === void 0 ? void 0 : _b.currentPricePrevPriceDiffPct;
|
|
49
49
|
if (pct === undefined || pct === null || pct < minDiscountPct)
|
|
50
50
|
return false;
|
|
51
51
|
}
|
|
@@ -59,7 +59,7 @@ function evalFilter(product, filter) {
|
|
|
59
59
|
return false;
|
|
60
60
|
}
|
|
61
61
|
if (words) {
|
|
62
|
-
const name = (
|
|
62
|
+
const name = (_c = product.name) === null || _c === void 0 ? void 0 : _c.toLowerCase();
|
|
63
63
|
if (!name || !words.some((w) => name.includes(w)))
|
|
64
64
|
return false;
|
|
65
65
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eval-filter.js","sourceRoot":"/","sources":["utils/eval-filter.ts"],"names":[],"mappings":";;AAmDA,
|
|
1
|
+
{"version":3,"file":"eval-filter.js","sourceRoot":"/","sources":["utils/eval-filter.ts"],"names":[],"mappings":";;AAmDA,gCAmDC;AArGD,+CAA4C;AAyB5C,SAAS,QAAQ,CAAC,GAAyB,EAAE,KAAc;IACzD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACrC,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,KAAK,MAAM,CAAC,IAAI,GAAG,EAAE,CAAC;QACpB,IAAI,OAAO,CAAC,KAAK,QAAQ;YAAE,SAAS;QACpC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;QACnB,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QAC7B,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACxC,CAAC;IACD,OAAO,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;AACrC,CAAC;AAED,MAAM,MAAM,GAAG,CAAC,CAAqB,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;AAanG,SAAgB,UAAU,CAAC,OAAgB,EAAE,MAAqB;;IAChE,MAAM,UAAU,GAAG,QAAQ,CAAC,MAAM,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;IACrD,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAC7C,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC9C,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IAE3C,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,IAAI,MAAM,CAAC,cAAc,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,CAAC,IAAI,CAAC;IACjH,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC;IAClE,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC;IAGlE,IAAI,CAAC,UAAU,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,IAAI,CAAC,KAAK;WAC1C,cAAc,KAAK,IAAI,IAAI,QAAQ,KAAK,IAAI,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACvE,OAAO,KAAK,CAAC;IACf,CAAC;IAED,IAAI,UAAU,EAAE,CAAC;QAIf,MAAM,WAAW,GAAG,QAAQ,CAAC,OAAO,CAAC,kBAAkB,EAAE,IAAI,CAAC,CAAC;QAC/D,IAAI,CAAC,WAAW,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;YAAE,OAAO,KAAK,CAAC;IACvF,CAAC;IAED,IAAI,MAAM,EAAE,CAAC;QACX,MAAM,CAAC,GAAG,MAAA,OAAO,CAAC,SAAS,0CAAE,WAAW,EAAE,CAAC;QAC3C,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;YAAE,OAAO,KAAK,CAAC;IAC9C,CAAC;IAED,IAAI,MAAM,EAAE,CAAC;QACX,IAAI,CAAC,OAAO,CAAC,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC;YAAE,OAAO,KAAK,CAAC;IAC5E,CAAC;IAED,IAAI,cAAc,KAAK,IAAI,EAAE,CAAC;QAC5B,MAAM,GAAG,GAAG,MAAA,OAAO,CAAC,UAAU,0CAAE,4BAA4B,CAAC;QAC7D,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,GAAG,cAAc;YAAE,OAAO,KAAK,CAAC;IAC9E,CAAC;IAED,IAAI,QAAQ,KAAK,IAAI,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QAC3C,MAAM,YAAY,GAAG,IAAA,yBAAW,EAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QAChD,IAAI,YAAY,KAAK,IAAI,IAAI,YAAY,KAAK,SAAS;YAAE,OAAO,KAAK,CAAC;QACtE,IAAI,QAAQ,KAAK,IAAI,IAAI,YAAY,GAAG,QAAQ;YAAE,OAAO,KAAK,CAAC;QAC/D,IAAI,QAAQ,KAAK,IAAI,IAAI,YAAY,GAAG,QAAQ;YAAE,OAAO,KAAK,CAAC;IACjE,CAAC;IAED,IAAI,KAAK,EAAE,CAAC;QACV,MAAM,IAAI,GAAG,MAAA,OAAO,CAAC,IAAI,0CAAE,WAAW,EAAE,CAAC;QACzC,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;YAAE,OAAO,KAAK,CAAC;IAClE,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["import { Product } from '../entities/product';\nimport { getMinPrice } from './price-utils';\n\n/**\n * User-defined filter for an alert channel.\n * Every present field is AND-ed; an absent (or empty/blank) field is ignored.\n * A filter with NO effective fields returns false (safety floor).\n */\nexport type ChannelFilter = {\n /** Match if any of the product's AI-normalized `inferredCategories` is any of these (case-insensitive). */\n categories?: string[];\n /** Match if the product's brandName is any of these (case-insensitive). */\n brands?: string[];\n /** Match if the product's storeRef is any of these (exact). */\n stores?: string[];\n /** Match if product.priceStats.currentPricePrevPriceDiffPct >= this (>0 to be active). */\n minDiscountPct?: number;\n /** Match if the product's current min price >= this value. */\n minPrice?: number;\n /** Match if the product's current min price <= this value. */\n maxPrice?: number;\n /** Match if any of these words appears (case-insensitive substring) in product.name. */\n words?: string[];\n};\n\n/** Trimmed, non-empty string elements (optionally lowercased), or null if none. */\nfunction cleanSet(arr: string[] | undefined, lower: boolean): string[] | null {\n if (!Array.isArray(arr)) return null;\n const out: string[] = [];\n for (const v of arr) {\n if (typeof v !== 'string') continue;\n const t = v.trim();\n if (t.length === 0) continue;\n out.push(lower ? t.toLowerCase() : t);\n }\n return out.length > 0 ? out : null;\n}\n\nconst finite = (n: number | undefined): n is number => typeof n === 'number' && Number.isFinite(n);\n\n/**\n * Pure, allocation-light predicate: true when the product matches every effective\n * field of the filter. Defensive against untrusted JSON filters (null/empty arrays,\n * null elements, blank words, non-numeric bounds are all ignored, never throw).\n *\n * Returns false when the filter has no effective fields (safety floor).\n *\n * NB: price bounds use getMinPrice(product.price), which (by existing lib behavior)\n * does not treat a validated offerPrice of 0 (isFree) as the min — free products are\n * not reliably caught by maxPrice:0.\n */\nexport function evalFilter(product: Product, filter: ChannelFilter): boolean {\n const categories = cleanSet(filter.categories, true);\n const brands = cleanSet(filter.brands, true);\n const stores = cleanSet(filter.stores, false); // storeRef is an id — exact, no lowercase\n const words = cleanSet(filter.words, true);\n // minDiscountPct of 0 (or negative/NaN) is \"no floor\" => inactive\n const minDiscountPct = finite(filter.minDiscountPct) && filter.minDiscountPct > 0 ? filter.minDiscountPct : null;\n const minPrice = finite(filter.minPrice) ? filter.minPrice : null;\n const maxPrice = finite(filter.maxPrice) ? filter.maxPrice : null;\n\n // Safety floor: a filter with no effective field must not match anything.\n if (!categories && !brands && !stores && !words\n && minDiscountPct === null && minPrice === null && maxPrice === null) {\n return false;\n }\n\n if (categories) {\n // Match against the AI-normalized `inferredCategories` (the mapped keys the\n // UI offers), NOT the raw per-store `product.category`. Any-of: the product\n // matches if any of its inferred categories is in the filter's set.\n const productCats = cleanSet(product.inferredCategories, true);\n if (!productCats || !productCats.some((pc) => categories.includes(pc))) return false;\n }\n\n if (brands) {\n const b = product.brandName?.toLowerCase();\n if (!b || !brands.includes(b)) return false;\n }\n\n if (stores) {\n if (!product.storeRef || !stores.includes(product.storeRef)) return false;\n }\n\n if (minDiscountPct !== null) {\n const pct = product.priceStats?.currentPricePrevPriceDiffPct;\n if (pct === undefined || pct === null || pct < minDiscountPct) return false;\n }\n\n if (minPrice !== null || maxPrice !== null) {\n const currentPrice = getMinPrice(product.price);\n if (currentPrice === null || currentPrice === undefined) return false;\n if (minPrice !== null && currentPrice < minPrice) return false;\n if (maxPrice !== null && currentPrice > maxPrice) return false;\n }\n\n if (words) {\n const name = product.name?.toLowerCase();\n if (!name || !words.some((w) => name.includes(w))) return false;\n }\n\n return true;\n}\n"]}
|
|
@@ -27,7 +27,8 @@ function makeProduct(overrides = {}) {
|
|
|
27
27
|
name: 'Samsung Galaxy S24 Ultra 256GB',
|
|
28
28
|
storeRef: '5f27437dabd4b000086cd698',
|
|
29
29
|
brandName: 'Samsung',
|
|
30
|
-
category: '
|
|
30
|
+
category: 'Celulares y Telefonía',
|
|
31
|
+
inferredCategories: ['Smartphones'],
|
|
31
32
|
price,
|
|
32
33
|
priceStats,
|
|
33
34
|
};
|
|
@@ -37,21 +38,29 @@ describe('evalFilter', () => {
|
|
|
37
38
|
it('returns false when filter has no fields set', () => {
|
|
38
39
|
expect((0, eval_filter_1.evalFilter)(makeProduct(), {})).toBe(false);
|
|
39
40
|
});
|
|
40
|
-
it('matches when
|
|
41
|
+
it('matches when an inferredCategory is in filter.categories (case-insensitive)', () => {
|
|
41
42
|
const filter = { categories: ['smartphones'] };
|
|
42
43
|
expect((0, eval_filter_1.evalFilter)(makeProduct(), filter)).toBe(true);
|
|
43
44
|
});
|
|
44
|
-
it('matches when
|
|
45
|
+
it('matches when an inferredCategory is in filter.categories (mixed case)', () => {
|
|
45
46
|
const filter = { categories: ['SMARTPHONES', 'Laptops'] };
|
|
46
47
|
expect((0, eval_filter_1.evalFilter)(makeProduct(), filter)).toBe(true);
|
|
47
48
|
});
|
|
48
|
-
it('
|
|
49
|
+
it('matches any-of across multiple inferredCategories', () => {
|
|
50
|
+
const filter = { categories: ['electronics'] };
|
|
51
|
+
expect((0, eval_filter_1.evalFilter)(makeProduct({ inferredCategories: ['Smartphones', 'Electronics'] }), filter)).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
it('matches against inferredCategories, NOT the raw product.category', () => {
|
|
54
|
+
const filter = { categories: ['celulares y telefonía'] };
|
|
55
|
+
expect((0, eval_filter_1.evalFilter)(makeProduct(), filter)).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
it('misses when none of the inferredCategories are in filter.categories', () => {
|
|
49
58
|
const filter = { categories: ['Laptops', 'Tablets'] };
|
|
50
59
|
expect((0, eval_filter_1.evalFilter)(makeProduct(), filter)).toBe(false);
|
|
51
60
|
});
|
|
52
|
-
it('misses when product has no
|
|
61
|
+
it('misses when product has no inferredCategories and filter.categories is set', () => {
|
|
53
62
|
const filter = { categories: ['Smartphones'] };
|
|
54
|
-
expect((0, eval_filter_1.evalFilter)(makeProduct({
|
|
63
|
+
expect((0, eval_filter_1.evalFilter)(makeProduct({ inferredCategories: undefined }), filter)).toBe(false);
|
|
55
64
|
});
|
|
56
65
|
it('matches when product brandName is in filter.brands (case-insensitive)', () => {
|
|
57
66
|
const filter = { brands: ['samsung'] };
|
|
@@ -177,7 +186,8 @@ describe('evalFilter — robustness over untrusted JSON filters', () => {
|
|
|
177
186
|
it('does not throw on a product missing priceStats/price/name/category/brand', () => {
|
|
178
187
|
const bare = makeProduct({
|
|
179
188
|
priceStats: undefined, price: undefined, name: undefined,
|
|
180
|
-
category: undefined,
|
|
189
|
+
category: undefined, inferredCategories: undefined,
|
|
190
|
+
brandName: undefined, storeRef: undefined,
|
|
181
191
|
});
|
|
182
192
|
expect((0, eval_filter_1.evalFilter)(bare, { minDiscountPct: 30 })).toBe(false);
|
|
183
193
|
expect((0, eval_filter_1.evalFilter)(bare, { minPrice: 1 })).toBe(false);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eval-filter.spec.js","sourceRoot":"/","sources":["utils/eval-filter.spec.ts"],"names":[],"mappings":"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","sourcesContent":["import { Product } from '../entities/product';\nimport { Price } from '../entities/price';\nimport { PricesStats } from '../entities/prices-stats';\nimport { evalFilter, ChannelFilter } from './eval-filter';\n\n// ---------------------------------------------------------------------------\n// Minimal Product fixture (only fields used by evalFilter)\n// ---------------------------------------------------------------------------\nfunction makeProduct(overrides: Partial<Product> = {}): Product {\n const price = {\n productRef: 'prod-1',\n offerPrice: 50000,\n normalPrice: 80000,\n } as Price;\n\n const priceStats = {\n minPriceSum: 0,\n maxPriceSum: 0,\n avgPriceSum: 0,\n pricesCount: 1,\n avgMinPrice: 0,\n avgAvgPrice: 0,\n avgMaxPrice: 0,\n currentMinPrice: 50000,\n currentMaxPrice: 80000,\n currentPriceAvgMinPriceDiffPct: 0,\n currentPriceBestPriceDiffPct: 0,\n currentPricePrevPriceDiffPct: 37.5, // 37.5 % off vs previous price\n currentMinPriceMaxPriceDiffPct: 37.5,\n } as PricesStats;\n\n const base: Partial<Product> = {\n name: 'Samsung Galaxy S24 Ultra 256GB',\n storeRef: '5f27437dabd4b000086cd698', // Falabella store ref\n brandName: 'Samsung',\n category: 'Smartphones',\n price,\n priceStats,\n };\n\n return Object.assign(new Product(), base, overrides);\n}\n\n// ---------------------------------------------------------------------------\n// Tests\n// ---------------------------------------------------------------------------\n\ndescribe('evalFilter', () => {\n // --- empty filter guard ------------------------------------------------\n\n it('returns false when filter has no fields set', () => {\n expect(evalFilter(makeProduct(), {})).toBe(false);\n });\n\n // --- categories --------------------------------------------------------\n\n it('matches when product category is in filter.categories (exact, case-insensitive)', () => {\n const filter: ChannelFilter = { categories: ['smartphones'] };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n\n it('matches when product category is in filter.categories (mixed case)', () => {\n const filter: ChannelFilter = { categories: ['SMARTPHONES', 'Laptops'] };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n\n it('misses when product category is NOT in filter.categories', () => {\n const filter: ChannelFilter = { categories: ['Laptops', 'Tablets'] };\n expect(evalFilter(makeProduct(), filter)).toBe(false);\n });\n\n it('misses when product has no category and filter.categories is set', () => {\n const filter: ChannelFilter = { categories: ['Smartphones'] };\n expect(evalFilter(makeProduct({ category: undefined }), filter)).toBe(false);\n });\n\n // --- brands ------------------------------------------------------------\n\n it('matches when product brandName is in filter.brands (case-insensitive)', () => {\n const filter: ChannelFilter = { brands: ['samsung'] };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n\n it('misses when product brandName is NOT in filter.brands', () => {\n const filter: ChannelFilter = { brands: ['Apple', 'LG'] };\n expect(evalFilter(makeProduct(), filter)).toBe(false);\n });\n\n // --- stores ------------------------------------------------------------\n\n it('matches when product storeRef is in filter.stores', () => {\n const filter: ChannelFilter = { stores: ['5f27437dabd4b000086cd698'] };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n\n it('misses when product storeRef is NOT in filter.stores', () => {\n const filter: ChannelFilter = { stores: ['other-store-id'] };\n expect(evalFilter(makeProduct(), filter)).toBe(false);\n });\n\n // --- minDiscountPct ----------------------------------------------------\n\n it('matches when product discount equals minDiscountPct (boundary >=)', () => {\n // currentPricePrevPriceDiffPct is 37.5 in fixture\n const filter: ChannelFilter = { minDiscountPct: 37.5 };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n\n it('matches when product discount is above minDiscountPct', () => {\n const filter: ChannelFilter = { minDiscountPct: 30 };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n\n it('misses when product discount is below minDiscountPct', () => {\n const filter: ChannelFilter = { minDiscountPct: 50 };\n expect(evalFilter(makeProduct(), filter)).toBe(false);\n });\n\n it('misses when product has no priceStats and minDiscountPct is set', () => {\n const filter: ChannelFilter = { minDiscountPct: 10 };\n expect(evalFilter(makeProduct({ priceStats: undefined }), filter)).toBe(false);\n });\n\n // --- minPrice / maxPrice -----------------------------------------------\n\n it('matches when product min price equals minPrice (boundary >=)', () => {\n // getMinPrice(price) = min(50000, 80000) = 50000\n const filter: ChannelFilter = { minPrice: 50000 };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n\n it('misses when product min price is below minPrice', () => {\n const filter: ChannelFilter = { minPrice: 60000 };\n expect(evalFilter(makeProduct(), filter)).toBe(false);\n });\n\n it('matches when product min price equals maxPrice (boundary <=)', () => {\n const filter: ChannelFilter = { maxPrice: 50000 };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n\n it('misses when product min price is above maxPrice', () => {\n const filter: ChannelFilter = { maxPrice: 49999 };\n expect(evalFilter(makeProduct(), filter)).toBe(false);\n });\n\n it('matches when product price is within [minPrice, maxPrice] range', () => {\n const filter: ChannelFilter = { minPrice: 40000, maxPrice: 60000 };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n\n it('misses when product has no price and a price bound is set', () => {\n const filter: ChannelFilter = { minPrice: 1000 };\n expect(evalFilter(makeProduct({ price: undefined }), filter)).toBe(false);\n });\n\n // --- words -------------------------------------------------------------\n\n it('matches when any word appears (case-insensitive substring) in product name', () => {\n const filter: ChannelFilter = { words: ['galaxy'] };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n\n it('matches when second of two words appears in product name', () => {\n const filter: ChannelFilter = { words: ['iphone', 'ultra'] };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n\n it('misses when none of the words appear in product name', () => {\n const filter: ChannelFilter = { words: ['iphone', 'pixel'] };\n expect(evalFilter(makeProduct(), filter)).toBe(false);\n });\n\n it('misses when product has no name and words filter is set', () => {\n const filter: ChannelFilter = { words: ['galaxy'] };\n expect(evalFilter(makeProduct({ name: undefined }), filter)).toBe(false);\n });\n\n // --- combined fields (AND logic) ---------------------------------------\n\n it('matches when all present fields pass', () => {\n const filter: ChannelFilter = {\n categories: ['smartphones'],\n brands: ['samsung'],\n stores: ['5f27437dabd4b000086cd698'],\n minDiscountPct: 30,\n minPrice: 40000,\n maxPrice: 60000,\n words: ['galaxy'],\n };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n\n it('misses when one of multiple present fields fails', () => {\n const filter: ChannelFilter = {\n categories: ['smartphones'],\n brands: ['Apple'], // <-- this will fail\n minDiscountPct: 30,\n };\n expect(evalFilter(makeProduct(), filter)).toBe(false);\n });\n\n it('ignores absent optional fields (product with only storeRef filter)', () => {\n const filter: ChannelFilter = { stores: ['5f27437dabd4b000086cd698'] };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n});\n\ndescribe('evalFilter — robustness over untrusted JSON filters', () => {\n const p = makeProduct(); // Samsung / Smartphones / store ref / \"Galaxy\" / 37.5% off\n\n it('ignores null/non-string array elements (no throw)', () => {\n expect(evalFilter(p, { brands: ['Samsung', null as never] })).toBe(true);\n expect(evalFilter(p, { categories: [null as never, 'Smartphones'] })).toBe(true);\n expect(evalFilter(p, { words: ['galaxy', null as never] })).toBe(true);\n });\n\n it('treats a null field as absent (no throw)', () => {\n expect(evalFilter(p, { stores: null as never, brands: ['Samsung'] })).toBe(true);\n expect(evalFilter(p, { categories: null as never })).toBe(false); // no effective field\n });\n\n it('treats an empty array as absent, not match-nothing', () => {\n expect(evalFilter(p, { categories: [] })).toBe(false); // only field, empty => floor\n expect(evalFilter(p, { categories: [], brands: ['Samsung'] })).toBe(true); // empty ignored, brand matches\n });\n\n it('ignores empty/whitespace word tokens (does not match everything)', () => {\n expect(evalFilter(p, { words: [''] })).toBe(false);\n expect(evalFilter(p, { words: [' '] })).toBe(false);\n expect(evalFilter(p, { words: [' galaxy '] })).toBe(true); // trimmed\n });\n\n it('minDiscountPct of 0 is no floor (inactive)', () => {\n expect(evalFilter(p, { minDiscountPct: 0 })).toBe(false); // only field => floor\n expect(evalFilter(p, { minDiscountPct: 0, brands: ['Samsung'] })).toBe(true);\n });\n\n it('does not throw on a product missing priceStats/price/name/category/brand', () => {\n const bare = makeProduct({\n priceStats: undefined as never, price: undefined as never, name: undefined as never,\n category: undefined as never, brandName: undefined as never, storeRef: undefined as never,\n });\n expect(evalFilter(bare, { minDiscountPct: 30 })).toBe(false);\n expect(evalFilter(bare, { minPrice: 1 })).toBe(false);\n expect(evalFilter(bare, { words: ['x'] })).toBe(false);\n expect(() => evalFilter(bare, {\n brands: ['Samsung'], categories: ['x'], stores: ['y'], words: ['z'], minPrice: 1, maxPrice: 9, minDiscountPct: 5,\n })).not.toThrow();\n });\n});\n"]}
|
|
1
|
+
{"version":3,"file":"eval-filter.spec.js","sourceRoot":"/","sources":["utils/eval-filter.spec.ts"],"names":[],"mappings":"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","sourcesContent":["import { Product } from '../entities/product';\nimport { Price } from '../entities/price';\nimport { PricesStats } from '../entities/prices-stats';\nimport { evalFilter, ChannelFilter } from './eval-filter';\n\n// ---------------------------------------------------------------------------\n// Minimal Product fixture (only fields used by evalFilter)\n// ---------------------------------------------------------------------------\nfunction makeProduct(overrides: Partial<Product> = {}): Product {\n const price = {\n productRef: 'prod-1',\n offerPrice: 50000,\n normalPrice: 80000,\n } as Price;\n\n const priceStats = {\n minPriceSum: 0,\n maxPriceSum: 0,\n avgPriceSum: 0,\n pricesCount: 1,\n avgMinPrice: 0,\n avgAvgPrice: 0,\n avgMaxPrice: 0,\n currentMinPrice: 50000,\n currentMaxPrice: 80000,\n currentPriceAvgMinPriceDiffPct: 0,\n currentPriceBestPriceDiffPct: 0,\n currentPricePrevPriceDiffPct: 37.5, // 37.5 % off vs previous price\n currentMinPriceMaxPriceDiffPct: 37.5,\n } as PricesStats;\n\n const base: Partial<Product> = {\n name: 'Samsung Galaxy S24 Ultra 256GB',\n storeRef: '5f27437dabd4b000086cd698', // Falabella store ref\n brandName: 'Samsung',\n category: 'Celulares y Telefonía', // raw per-store category (NOT matched)\n inferredCategories: ['Smartphones'], // AI-normalized keys (matched by evalFilter)\n price,\n priceStats,\n };\n\n return Object.assign(new Product(), base, overrides);\n}\n\n// ---------------------------------------------------------------------------\n// Tests\n// ---------------------------------------------------------------------------\n\ndescribe('evalFilter', () => {\n // --- empty filter guard ------------------------------------------------\n\n it('returns false when filter has no fields set', () => {\n expect(evalFilter(makeProduct(), {})).toBe(false);\n });\n\n // --- categories --------------------------------------------------------\n\n it('matches when an inferredCategory is in filter.categories (case-insensitive)', () => {\n const filter: ChannelFilter = { categories: ['smartphones'] };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n\n it('matches when an inferredCategory is in filter.categories (mixed case)', () => {\n const filter: ChannelFilter = { categories: ['SMARTPHONES', 'Laptops'] };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n\n it('matches any-of across multiple inferredCategories', () => {\n const filter: ChannelFilter = { categories: ['electronics'] };\n expect(evalFilter(makeProduct({ inferredCategories: ['Smartphones', 'Electronics'] }), filter)).toBe(true);\n });\n\n it('matches against inferredCategories, NOT the raw product.category', () => {\n // raw category is 'Celulares y Telefonía'; matching it must NOT work.\n const filter: ChannelFilter = { categories: ['celulares y telefonía'] };\n expect(evalFilter(makeProduct(), filter)).toBe(false);\n });\n\n it('misses when none of the inferredCategories are in filter.categories', () => {\n const filter: ChannelFilter = { categories: ['Laptops', 'Tablets'] };\n expect(evalFilter(makeProduct(), filter)).toBe(false);\n });\n\n it('misses when product has no inferredCategories and filter.categories is set', () => {\n const filter: ChannelFilter = { categories: ['Smartphones'] };\n expect(evalFilter(makeProduct({ inferredCategories: undefined }), filter)).toBe(false);\n });\n\n // --- brands ------------------------------------------------------------\n\n it('matches when product brandName is in filter.brands (case-insensitive)', () => {\n const filter: ChannelFilter = { brands: ['samsung'] };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n\n it('misses when product brandName is NOT in filter.brands', () => {\n const filter: ChannelFilter = { brands: ['Apple', 'LG'] };\n expect(evalFilter(makeProduct(), filter)).toBe(false);\n });\n\n // --- stores ------------------------------------------------------------\n\n it('matches when product storeRef is in filter.stores', () => {\n const filter: ChannelFilter = { stores: ['5f27437dabd4b000086cd698'] };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n\n it('misses when product storeRef is NOT in filter.stores', () => {\n const filter: ChannelFilter = { stores: ['other-store-id'] };\n expect(evalFilter(makeProduct(), filter)).toBe(false);\n });\n\n // --- minDiscountPct ----------------------------------------------------\n\n it('matches when product discount equals minDiscountPct (boundary >=)', () => {\n // currentPricePrevPriceDiffPct is 37.5 in fixture\n const filter: ChannelFilter = { minDiscountPct: 37.5 };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n\n it('matches when product discount is above minDiscountPct', () => {\n const filter: ChannelFilter = { minDiscountPct: 30 };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n\n it('misses when product discount is below minDiscountPct', () => {\n const filter: ChannelFilter = { minDiscountPct: 50 };\n expect(evalFilter(makeProduct(), filter)).toBe(false);\n });\n\n it('misses when product has no priceStats and minDiscountPct is set', () => {\n const filter: ChannelFilter = { minDiscountPct: 10 };\n expect(evalFilter(makeProduct({ priceStats: undefined }), filter)).toBe(false);\n });\n\n // --- minPrice / maxPrice -----------------------------------------------\n\n it('matches when product min price equals minPrice (boundary >=)', () => {\n // getMinPrice(price) = min(50000, 80000) = 50000\n const filter: ChannelFilter = { minPrice: 50000 };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n\n it('misses when product min price is below minPrice', () => {\n const filter: ChannelFilter = { minPrice: 60000 };\n expect(evalFilter(makeProduct(), filter)).toBe(false);\n });\n\n it('matches when product min price equals maxPrice (boundary <=)', () => {\n const filter: ChannelFilter = { maxPrice: 50000 };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n\n it('misses when product min price is above maxPrice', () => {\n const filter: ChannelFilter = { maxPrice: 49999 };\n expect(evalFilter(makeProduct(), filter)).toBe(false);\n });\n\n it('matches when product price is within [minPrice, maxPrice] range', () => {\n const filter: ChannelFilter = { minPrice: 40000, maxPrice: 60000 };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n\n it('misses when product has no price and a price bound is set', () => {\n const filter: ChannelFilter = { minPrice: 1000 };\n expect(evalFilter(makeProduct({ price: undefined }), filter)).toBe(false);\n });\n\n // --- words -------------------------------------------------------------\n\n it('matches when any word appears (case-insensitive substring) in product name', () => {\n const filter: ChannelFilter = { words: ['galaxy'] };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n\n it('matches when second of two words appears in product name', () => {\n const filter: ChannelFilter = { words: ['iphone', 'ultra'] };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n\n it('misses when none of the words appear in product name', () => {\n const filter: ChannelFilter = { words: ['iphone', 'pixel'] };\n expect(evalFilter(makeProduct(), filter)).toBe(false);\n });\n\n it('misses when product has no name and words filter is set', () => {\n const filter: ChannelFilter = { words: ['galaxy'] };\n expect(evalFilter(makeProduct({ name: undefined }), filter)).toBe(false);\n });\n\n // --- combined fields (AND logic) ---------------------------------------\n\n it('matches when all present fields pass', () => {\n const filter: ChannelFilter = {\n categories: ['smartphones'],\n brands: ['samsung'],\n stores: ['5f27437dabd4b000086cd698'],\n minDiscountPct: 30,\n minPrice: 40000,\n maxPrice: 60000,\n words: ['galaxy'],\n };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n\n it('misses when one of multiple present fields fails', () => {\n const filter: ChannelFilter = {\n categories: ['smartphones'],\n brands: ['Apple'], // <-- this will fail\n minDiscountPct: 30,\n };\n expect(evalFilter(makeProduct(), filter)).toBe(false);\n });\n\n it('ignores absent optional fields (product with only storeRef filter)', () => {\n const filter: ChannelFilter = { stores: ['5f27437dabd4b000086cd698'] };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n});\n\ndescribe('evalFilter — robustness over untrusted JSON filters', () => {\n const p = makeProduct(); // Samsung / Smartphones / store ref / \"Galaxy\" / 37.5% off\n\n it('ignores null/non-string array elements (no throw)', () => {\n expect(evalFilter(p, { brands: ['Samsung', null as never] })).toBe(true);\n expect(evalFilter(p, { categories: [null as never, 'Smartphones'] })).toBe(true);\n expect(evalFilter(p, { words: ['galaxy', null as never] })).toBe(true);\n });\n\n it('treats a null field as absent (no throw)', () => {\n expect(evalFilter(p, { stores: null as never, brands: ['Samsung'] })).toBe(true);\n expect(evalFilter(p, { categories: null as never })).toBe(false); // no effective field\n });\n\n it('treats an empty array as absent, not match-nothing', () => {\n expect(evalFilter(p, { categories: [] })).toBe(false); // only field, empty => floor\n expect(evalFilter(p, { categories: [], brands: ['Samsung'] })).toBe(true); // empty ignored, brand matches\n });\n\n it('ignores empty/whitespace word tokens (does not match everything)', () => {\n expect(evalFilter(p, { words: [''] })).toBe(false);\n expect(evalFilter(p, { words: [' '] })).toBe(false);\n expect(evalFilter(p, { words: [' galaxy '] })).toBe(true); // trimmed\n });\n\n it('minDiscountPct of 0 is no floor (inactive)', () => {\n expect(evalFilter(p, { minDiscountPct: 0 })).toBe(false); // only field => floor\n expect(evalFilter(p, { minDiscountPct: 0, brands: ['Samsung'] })).toBe(true);\n });\n\n it('does not throw on a product missing priceStats/price/name/category/brand', () => {\n const bare = makeProduct({\n priceStats: undefined as never, price: undefined as never, name: undefined as never,\n category: undefined as never, inferredCategories: undefined as never,\n brandName: undefined as never, storeRef: undefined as never,\n });\n expect(evalFilter(bare, { minDiscountPct: 30 })).toBe(false);\n expect(evalFilter(bare, { minPrice: 1 })).toBe(false);\n expect(evalFilter(bare, { words: ['x'] })).toBe(false);\n expect(() => evalFilter(bare, {\n brands: ['Samsung'], categories: ['x'], stores: ['y'], words: ['z'], minPrice: 1, maxPrice: 9, minDiscountPct: 5,\n })).not.toThrow();\n });\n});\n"]}
|
package/package.json
CHANGED
|
@@ -33,7 +33,8 @@ function makeProduct(overrides: Partial<Product> = {}): Product {
|
|
|
33
33
|
name: 'Samsung Galaxy S24 Ultra 256GB',
|
|
34
34
|
storeRef: '5f27437dabd4b000086cd698', // Falabella store ref
|
|
35
35
|
brandName: 'Samsung',
|
|
36
|
-
category: '
|
|
36
|
+
category: 'Celulares y Telefonía', // raw per-store category (NOT matched)
|
|
37
|
+
inferredCategories: ['Smartphones'], // AI-normalized keys (matched by evalFilter)
|
|
37
38
|
price,
|
|
38
39
|
priceStats,
|
|
39
40
|
};
|
|
@@ -54,24 +55,35 @@ describe('evalFilter', () => {
|
|
|
54
55
|
|
|
55
56
|
// --- categories --------------------------------------------------------
|
|
56
57
|
|
|
57
|
-
it('matches when
|
|
58
|
+
it('matches when an inferredCategory is in filter.categories (case-insensitive)', () => {
|
|
58
59
|
const filter: ChannelFilter = { categories: ['smartphones'] };
|
|
59
60
|
expect(evalFilter(makeProduct(), filter)).toBe(true);
|
|
60
61
|
});
|
|
61
62
|
|
|
62
|
-
it('matches when
|
|
63
|
+
it('matches when an inferredCategory is in filter.categories (mixed case)', () => {
|
|
63
64
|
const filter: ChannelFilter = { categories: ['SMARTPHONES', 'Laptops'] };
|
|
64
65
|
expect(evalFilter(makeProduct(), filter)).toBe(true);
|
|
65
66
|
});
|
|
66
67
|
|
|
67
|
-
it('
|
|
68
|
+
it('matches any-of across multiple inferredCategories', () => {
|
|
69
|
+
const filter: ChannelFilter = { categories: ['electronics'] };
|
|
70
|
+
expect(evalFilter(makeProduct({ inferredCategories: ['Smartphones', 'Electronics'] }), filter)).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('matches against inferredCategories, NOT the raw product.category', () => {
|
|
74
|
+
// raw category is 'Celulares y Telefonía'; matching it must NOT work.
|
|
75
|
+
const filter: ChannelFilter = { categories: ['celulares y telefonía'] };
|
|
76
|
+
expect(evalFilter(makeProduct(), filter)).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('misses when none of the inferredCategories are in filter.categories', () => {
|
|
68
80
|
const filter: ChannelFilter = { categories: ['Laptops', 'Tablets'] };
|
|
69
81
|
expect(evalFilter(makeProduct(), filter)).toBe(false);
|
|
70
82
|
});
|
|
71
83
|
|
|
72
|
-
it('misses when product has no
|
|
84
|
+
it('misses when product has no inferredCategories and filter.categories is set', () => {
|
|
73
85
|
const filter: ChannelFilter = { categories: ['Smartphones'] };
|
|
74
|
-
expect(evalFilter(makeProduct({
|
|
86
|
+
expect(evalFilter(makeProduct({ inferredCategories: undefined }), filter)).toBe(false);
|
|
75
87
|
});
|
|
76
88
|
|
|
77
89
|
// --- brands ------------------------------------------------------------
|
|
@@ -239,7 +251,8 @@ describe('evalFilter — robustness over untrusted JSON filters', () => {
|
|
|
239
251
|
it('does not throw on a product missing priceStats/price/name/category/brand', () => {
|
|
240
252
|
const bare = makeProduct({
|
|
241
253
|
priceStats: undefined as never, price: undefined as never, name: undefined as never,
|
|
242
|
-
category: undefined as never,
|
|
254
|
+
category: undefined as never, inferredCategories: undefined as never,
|
|
255
|
+
brandName: undefined as never, storeRef: undefined as never,
|
|
243
256
|
});
|
|
244
257
|
expect(evalFilter(bare, { minDiscountPct: 30 })).toBe(false);
|
|
245
258
|
expect(evalFilter(bare, { minPrice: 1 })).toBe(false);
|
package/src/utils/eval-filter.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { getMinPrice } from './price-utils';
|
|
|
7
7
|
* A filter with NO effective fields returns false (safety floor).
|
|
8
8
|
*/
|
|
9
9
|
export type ChannelFilter = {
|
|
10
|
-
/** Match if the product's
|
|
10
|
+
/** Match if any of the product's AI-normalized `inferredCategories` is any of these (case-insensitive). */
|
|
11
11
|
categories?: string[];
|
|
12
12
|
/** Match if the product's brandName is any of these (case-insensitive). */
|
|
13
13
|
brands?: string[];
|
|
@@ -66,8 +66,11 @@ export function evalFilter(product: Product, filter: ChannelFilter): boolean {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
if (categories) {
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
// Match against the AI-normalized `inferredCategories` (the mapped keys the
|
|
70
|
+
// UI offers), NOT the raw per-store `product.category`. Any-of: the product
|
|
71
|
+
// matches if any of its inferred categories is in the filter's set.
|
|
72
|
+
const productCats = cleanSet(product.inferredCategories, true);
|
|
73
|
+
if (!productCats || !productCats.some((pc) => categories.includes(pc))) return false;
|
|
71
74
|
}
|
|
72
75
|
|
|
73
76
|
if (brands) {
|