store-scrapper-js-common 1.0.219 → 1.0.222

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.
@@ -3,9 +3,13 @@ export type ChannelFilter = {
3
3
  categories?: string[];
4
4
  brands?: string[];
5
5
  stores?: string[];
6
+ sellers?: string[];
6
7
  minDiscountPct?: number;
7
8
  minPrice?: number;
8
9
  maxPrice?: number;
9
10
  words?: string[];
11
+ excludeWords?: string[];
12
+ excludeSellers?: string[];
13
+ excludeMarketplace?: boolean;
10
14
  };
11
15
  export declare function evalFilter(product: Product, filter: ChannelFilter): boolean;
@@ -18,15 +18,19 @@ 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, _d, _e, _f;
22
22
  const categories = cleanSet(filter.categories, true);
23
23
  const brands = cleanSet(filter.brands, true);
24
24
  const stores = cleanSet(filter.stores, false);
25
+ const sellers = cleanSet(filter.sellers, true);
25
26
  const words = cleanSet(filter.words, true);
26
27
  const minDiscountPct = finite(filter.minDiscountPct) && filter.minDiscountPct > 0 ? filter.minDiscountPct : null;
27
28
  const minPrice = finite(filter.minPrice) ? filter.minPrice : null;
28
29
  const maxPrice = finite(filter.maxPrice) ? filter.maxPrice : null;
29
- if (!categories && !brands && !stores && !words
30
+ const excludeWords = cleanSet(filter.excludeWords, true);
31
+ const excludeSellers = cleanSet(filter.excludeSellers, true);
32
+ const excludeMarketplace = filter.excludeMarketplace === true;
33
+ if (!categories && !brands && !stores && !sellers && !words
30
34
  && minDiscountPct === null && minPrice === null && maxPrice === null) {
31
35
  return false;
32
36
  }
@@ -58,11 +62,28 @@ function evalFilter(product, filter) {
58
62
  if (maxPrice !== null && currentPrice > maxPrice)
59
63
  return false;
60
64
  }
65
+ if (sellers) {
66
+ const s = (_c = product.sellerName) === null || _c === void 0 ? void 0 : _c.toLowerCase();
67
+ if (!s || !sellers.includes(s))
68
+ return false;
69
+ }
61
70
  if (words) {
62
- const name = (_c = product.name) === null || _c === void 0 ? void 0 : _c.toLowerCase();
71
+ const name = (_d = product.name) === null || _d === void 0 ? void 0 : _d.toLowerCase();
63
72
  if (!name || !words.some((w) => name.includes(w)))
64
73
  return false;
65
74
  }
75
+ if (excludeMarketplace && product.isMarketplace === true)
76
+ return false;
77
+ if (excludeWords) {
78
+ const name = (_e = product.name) === null || _e === void 0 ? void 0 : _e.toLowerCase();
79
+ if (name && excludeWords.some((w) => name.includes(w)))
80
+ return false;
81
+ }
82
+ if (excludeSellers) {
83
+ const s = (_f = product.sellerName) === null || _f === void 0 ? void 0 : _f.toLowerCase();
84
+ if (s && excludeSellers.includes(s))
85
+ return false;
86
+ }
66
87
  return true;
67
88
  }
68
89
  //# sourceMappingURL=eval-filter.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"eval-filter.js","sourceRoot":"/","sources":["utils/eval-filter.ts"],"names":[],"mappings":"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","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"]}
1
+ {"version":3,"file":"eval-filter.js","sourceRoot":"/","sources":["utils/eval-filter.ts"],"names":[],"mappings":"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","sourcesContent":["import { Product } from '../entities/product';\nimport { getMinPrice } from './price-utils';\n\n/**\n * User-defined filter for an alert channel.\n * POSITIVE fields are AND-ed; an absent (or empty/blank) field is ignored.\n * EXCLUSION fields only narrow (they reject matches) and do NOT count toward the\n * safety floor. A filter with NO positive field returns false (anti-spam floor) —\n * so an exclusion-only filter (e.g. just `excludeMarketplace`) matches nothing,\n * never \"everything except X\".\n */\nexport type ChannelFilter = {\n // ── Positive (inclusion) — AND-ed; at least one is required to match anything ──\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 the product's sellerName is any of these (case-insensitive exact). */\n sellers?: 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 // ── Exclusion (negative) — reject a match; do NOT satisfy the safety floor ──\n /** Skip if any of these words appears (case-insensitive substring) in product.name. */\n excludeWords?: string[];\n /** Skip if the product's sellerName is any of these (case-insensitive exact). */\n excludeSellers?: string[];\n /** Skip if the product is a marketplace listing (product.isMarketplace === true). */\n excludeMarketplace?: boolean;\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 sellers = cleanSet(filter.sellers, true);\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 // Exclusion fields — only narrow; they do NOT satisfy the safety floor below.\n const excludeWords = cleanSet(filter.excludeWords, true);\n const excludeSellers = cleanSet(filter.excludeSellers, true);\n const excludeMarketplace = filter.excludeMarketplace === true;\n\n // Safety floor (anti-spam): require at least one POSITIVE field. Exclusion-only\n // filters (excludeWords/excludeSellers/excludeMarketplace) match nothing — never\n // \"everything except X\".\n if (!categories && !brands && !stores && !sellers && !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 (sellers) {\n const s = product.sellerName?.toLowerCase();\n if (!s || !sellers.includes(s)) 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 // ── Exclusions (applied after the positive AND passes; any hit rejects) ──\n if (excludeMarketplace && product.isMarketplace === true) return false;\n\n if (excludeWords) {\n const name = product.name?.toLowerCase();\n if (name && excludeWords.some((w) => name.includes(w))) return false;\n }\n\n if (excludeSellers) {\n const s = product.sellerName?.toLowerCase();\n if (s && excludeSellers.includes(s)) return false;\n }\n\n return true;\n}\n"]}
@@ -196,5 +196,48 @@ describe('evalFilter — robustness over untrusted JSON filters', () => {
196
196
  brands: ['Samsung'], categories: ['x'], stores: ['y'], words: ['z'], minPrice: 1, maxPrice: 9, minDiscountPct: 5,
197
197
  })).not.toThrow();
198
198
  });
199
+ describe('sellers (positive)', () => {
200
+ it('matches when sellerName is in filter.sellers (case-insensitive)', () => {
201
+ expect((0, eval_filter_1.evalFilter)(makeProduct({ sellerName: 'Falabella' }), { sellers: ['falabella'] })).toBe(true);
202
+ });
203
+ it('misses when sellerName is NOT in filter.sellers', () => {
204
+ expect((0, eval_filter_1.evalFilter)(makeProduct({ sellerName: 'OtherSeller' }), { sellers: ['falabella'] })).toBe(false);
205
+ });
206
+ it('misses when product has no sellerName but filter.sellers is set', () => {
207
+ expect((0, eval_filter_1.evalFilter)(makeProduct({ sellerName: undefined }), { sellers: ['falabella'] })).toBe(false);
208
+ });
209
+ it('counts as a positive field for the safety floor', () => {
210
+ expect((0, eval_filter_1.evalFilter)(makeProduct({ sellerName: 'Falabella' }), { sellers: ['falabella'] })).toBe(true);
211
+ });
212
+ });
213
+ describe('exclusions', () => {
214
+ it('excludeWords: rejects when name contains an excluded word (substring, ci)', () => {
215
+ expect((0, eval_filter_1.evalFilter)(makeProduct(), { words: ['galaxy'], excludeWords: ['ultra'] })).toBe(false);
216
+ });
217
+ it('excludeWords: passes when name contains none of the excluded words', () => {
218
+ expect((0, eval_filter_1.evalFilter)(makeProduct(), { words: ['galaxy'], excludeWords: ['refurbished'] })).toBe(true);
219
+ });
220
+ it('excludeMarketplace: rejects a marketplace product', () => {
221
+ expect((0, eval_filter_1.evalFilter)(makeProduct({ isMarketplace: true }), { brands: ['Samsung'], excludeMarketplace: true })).toBe(false);
222
+ });
223
+ it('excludeMarketplace: passes a non-marketplace product', () => {
224
+ expect((0, eval_filter_1.evalFilter)(makeProduct({ isMarketplace: false }), { brands: ['Samsung'], excludeMarketplace: true })).toBe(true);
225
+ });
226
+ it('excludeSellers: rejects when sellerName is in the deny-list (ci)', () => {
227
+ expect((0, eval_filter_1.evalFilter)(makeProduct({ sellerName: 'BadSeller' }), { brands: ['Samsung'], excludeSellers: ['badseller'] })).toBe(false);
228
+ });
229
+ it('excludeSellers: passes when sellerName is not in the deny-list', () => {
230
+ expect((0, eval_filter_1.evalFilter)(makeProduct({ sellerName: 'GoodSeller' }), { brands: ['Samsung'], excludeSellers: ['badseller'] })).toBe(true);
231
+ });
232
+ it('excludeMarketplace-only filter matches nothing (no positive field)', () => {
233
+ expect((0, eval_filter_1.evalFilter)(makeProduct({ isMarketplace: false }), { excludeMarketplace: true })).toBe(false);
234
+ });
235
+ it('excludeWords-only filter matches nothing (no positive field)', () => {
236
+ expect((0, eval_filter_1.evalFilter)(makeProduct(), { excludeWords: ['refurbished'] })).toBe(false);
237
+ });
238
+ it('excludeSellers-only filter matches nothing (no positive field)', () => {
239
+ expect((0, eval_filter_1.evalFilter)(makeProduct({ sellerName: 'GoodSeller' }), { excludeSellers: ['badseller'] })).toBe(false);
240
+ });
241
+ });
199
242
  });
200
243
  //# sourceMappingURL=eval-filter.spec.js.map
@@ -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: '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"]}
1
+ {"version":3,"file":"eval-filter.spec.js","sourceRoot":"/","sources":["utils/eval-filter.spec.ts"],"names":[],"mappings":";;AAAA,iDAA8C;AAG9C,+CAA0D;AAK1D,SAAS,WAAW,CAAC,YAA8B,EAAE;IACnD,MAAM,KAAK,GAAG;QACZ,UAAU,EAAE,QAAQ;QACpB,UAAU,EAAE,KAAK;QACjB,WAAW,EAAE,KAAK;KACV,CAAC;IAEX,MAAM,UAAU,GAAG;QACjB,WAAW,EAAE,CAAC;QACd,WAAW,EAAE,CAAC;QACd,WAAW,EAAE,CAAC;QACd,WAAW,EAAE,CAAC;QACd,WAAW,EAAE,CAAC;QACd,WAAW,EAAE,CAAC;QACd,WAAW,EAAE,CAAC;QACd,eAAe,EAAE,KAAK;QACtB,eAAe,EAAE,KAAK;QACtB,8BAA8B,EAAE,CAAC;QACjC,4BAA4B,EAAE,CAAC;QAC/B,4BAA4B,EAAE,IAAI;QAClC,8BAA8B,EAAE,IAAI;KACtB,CAAC;IAEjB,MAAM,IAAI,GAAqB;QAC7B,IAAI,EAAE,gCAAgC;QACtC,QAAQ,EAAE,0BAA0B;QACpC,SAAS,EAAE,SAAS;QACpB,QAAQ,EAAE,uBAAuB;QACjC,kBAAkB,EAAE,CAAC,aAAa,CAAC;QACnC,KAAK;QACL,UAAU;KACX,CAAC;IAEF,OAAO,MAAM,CAAC,MAAM,CAAC,IAAI,iBAAO,EAAE,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC;AACvD,CAAC;AAMD,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;IAG1B,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;IAIH,EAAE,CAAC,6EAA6E,EAAE,GAAG,EAAE;QACrF,MAAM,MAAM,GAAkB,EAAE,UAAU,EAAE,CAAC,aAAa,CAAC,EAAE,CAAC;QAC9D,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uEAAuE,EAAE,GAAG,EAAE;QAC/E,MAAM,MAAM,GAAkB,EAAE,UAAU,EAAE,CAAC,aAAa,EAAE,SAAS,CAAC,EAAE,CAAC;QACzE,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mDAAmD,EAAE,GAAG,EAAE;QAC3D,MAAM,MAAM,GAAkB,EAAE,UAAU,EAAE,CAAC,aAAa,CAAC,EAAE,CAAC;QAC9D,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,CAAC,EAAE,kBAAkB,EAAE,CAAC,aAAa,EAAE,aAAa,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC7G,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kEAAkE,EAAE,GAAG,EAAE;QAE1E,MAAM,MAAM,GAAkB,EAAE,UAAU,EAAE,CAAC,uBAAuB,CAAC,EAAE,CAAC;QACxE,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qEAAqE,EAAE,GAAG,EAAE;QAC7E,MAAM,MAAM,GAAkB,EAAE,UAAU,EAAE,CAAC,SAAS,EAAE,SAAS,CAAC,EAAE,CAAC;QACrE,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4EAA4E,EAAE,GAAG,EAAE;QACpF,MAAM,MAAM,GAAkB,EAAE,UAAU,EAAE,CAAC,aAAa,CAAC,EAAE,CAAC;QAC9D,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,CAAC,EAAE,kBAAkB,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACzF,CAAC,CAAC,CAAC;IAIH,EAAE,CAAC,uEAAuE,EAAE,GAAG,EAAE;QAC/E,MAAM,MAAM,GAAkB,EAAE,MAAM,EAAE,CAAC,SAAS,CAAC,EAAE,CAAC;QACtD,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uDAAuD,EAAE,GAAG,EAAE;QAC/D,MAAM,MAAM,GAAkB,EAAE,MAAM,EAAE,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC;QAC1D,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAIH,EAAE,CAAC,mDAAmD,EAAE,GAAG,EAAE;QAC3D,MAAM,MAAM,GAAkB,EAAE,MAAM,EAAE,CAAC,0BAA0B,CAAC,EAAE,CAAC;QACvE,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sDAAsD,EAAE,GAAG,EAAE;QAC9D,MAAM,MAAM,GAAkB,EAAE,MAAM,EAAE,CAAC,gBAAgB,CAAC,EAAE,CAAC;QAC7D,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAIH,EAAE,CAAC,mEAAmE,EAAE,GAAG,EAAE;QAE3E,MAAM,MAAM,GAAkB,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC;QACvD,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uDAAuD,EAAE,GAAG,EAAE;QAC/D,MAAM,MAAM,GAAkB,EAAE,cAAc,EAAE,EAAE,EAAE,CAAC;QACrD,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sDAAsD,EAAE,GAAG,EAAE;QAC9D,MAAM,MAAM,GAAkB,EAAE,cAAc,EAAE,EAAE,EAAE,CAAC;QACrD,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iEAAiE,EAAE,GAAG,EAAE;QACzE,MAAM,MAAM,GAAkB,EAAE,cAAc,EAAE,EAAE,EAAE,CAAC;QACrD,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,CAAC,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACjF,CAAC,CAAC,CAAC;IAIH,EAAE,CAAC,8DAA8D,EAAE,GAAG,EAAE;QAEtE,MAAM,MAAM,GAAkB,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;QAClD,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;QACzD,MAAM,MAAM,GAAkB,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;QAClD,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8DAA8D,EAAE,GAAG,EAAE;QACtE,MAAM,MAAM,GAAkB,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;QAClD,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;QACzD,MAAM,MAAM,GAAkB,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;QAClD,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iEAAiE,EAAE,GAAG,EAAE;QACzE,MAAM,MAAM,GAAkB,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;QACnE,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2DAA2D,EAAE,GAAG,EAAE;QACnE,MAAM,MAAM,GAAkB,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;QACjD,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC5E,CAAC,CAAC,CAAC;IAIH,EAAE,CAAC,4EAA4E,EAAE,GAAG,EAAE;QACpF,MAAM,MAAM,GAAkB,EAAE,KAAK,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC;QACpD,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0DAA0D,EAAE,GAAG,EAAE;QAClE,MAAM,MAAM,GAAkB,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,OAAO,CAAC,EAAE,CAAC;QAC7D,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sDAAsD,EAAE,GAAG,EAAE;QAC9D,MAAM,MAAM,GAAkB,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,OAAO,CAAC,EAAE,CAAC;QAC7D,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yDAAyD,EAAE,GAAG,EAAE;QACjE,MAAM,MAAM,GAAkB,EAAE,KAAK,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC;QACpD,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC3E,CAAC,CAAC,CAAC;IAIH,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,MAAM,GAAkB;YAC5B,UAAU,EAAE,CAAC,aAAa,CAAC;YAC3B,MAAM,EAAE,CAAC,SAAS,CAAC;YACnB,MAAM,EAAE,CAAC,0BAA0B,CAAC;YACpC,cAAc,EAAE,EAAE;YAClB,QAAQ,EAAE,KAAK;YACf,QAAQ,EAAE,KAAK;YACf,KAAK,EAAE,CAAC,QAAQ,CAAC;SAClB,CAAC;QACF,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,MAAM,MAAM,GAAkB;YAC5B,UAAU,EAAE,CAAC,aAAa,CAAC;YAC3B,MAAM,EAAE,CAAC,OAAO,CAAC;YACjB,cAAc,EAAE,EAAE;SACnB,CAAC;QACF,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oEAAoE,EAAE,GAAG,EAAE;QAC5E,MAAM,MAAM,GAAkB,EAAE,MAAM,EAAE,CAAC,0BAA0B,CAAC,EAAE,CAAC;QACvE,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,qDAAqD,EAAE,GAAG,EAAE;IACnE,MAAM,CAAC,GAAG,WAAW,EAAE,CAAC;IAExB,EAAE,CAAC,mDAAmD,EAAE,GAAG,EAAE;QAC3D,MAAM,CAAC,IAAA,wBAAU,EAAC,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,SAAS,EAAE,IAAa,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACzE,MAAM,CAAC,IAAA,wBAAU,EAAC,CAAC,EAAE,EAAE,UAAU,EAAE,CAAC,IAAa,EAAE,aAAa,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACjF,MAAM,CAAC,IAAA,wBAAU,EAAC,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,IAAa,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACzE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QAClD,MAAM,CAAC,IAAA,wBAAU,EAAC,CAAC,EAAE,EAAE,MAAM,EAAE,IAAa,EAAE,MAAM,EAAE,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACjF,MAAM,CAAC,IAAA,wBAAU,EAAC,CAAC,EAAE,EAAE,UAAU,EAAE,IAAa,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACnE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oDAAoD,EAAE,GAAG,EAAE;QAC5D,MAAM,CAAC,IAAA,wBAAU,EAAC,CAAC,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACtD,MAAM,CAAC,IAAA,wBAAU,EAAC,CAAC,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kEAAkE,EAAE,GAAG,EAAE;QAC1E,MAAM,CAAC,IAAA,wBAAU,EAAC,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACnD,MAAM,CAAC,IAAA,wBAAU,EAAC,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACtD,MAAM,CAAC,IAAA,wBAAU,EAAC,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC9D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;QACpD,MAAM,CAAC,IAAA,wBAAU,EAAC,CAAC,EAAE,EAAE,cAAc,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACzD,MAAM,CAAC,IAAA,wBAAU,EAAC,CAAC,EAAE,EAAE,cAAc,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC/E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0EAA0E,EAAE,GAAG,EAAE;QAClF,MAAM,IAAI,GAAG,WAAW,CAAC;YACvB,UAAU,EAAE,SAAkB,EAAE,KAAK,EAAE,SAAkB,EAAE,IAAI,EAAE,SAAkB;YACnF,QAAQ,EAAE,SAAkB,EAAE,kBAAkB,EAAE,SAAkB;YACpE,SAAS,EAAE,SAAkB,EAAE,QAAQ,EAAE,SAAkB;SAC5D,CAAC,CAAC;QACH,MAAM,CAAC,IAAA,wBAAU,EAAC,IAAI,EAAE,EAAE,cAAc,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC7D,MAAM,CAAC,IAAA,wBAAU,EAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACtD,MAAM,CAAC,IAAA,wBAAU,EAAC,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACvD,MAAM,CAAC,GAAG,EAAE,CAAC,IAAA,wBAAU,EAAC,IAAI,EAAE;YAC5B,MAAM,EAAE,CAAC,SAAS,CAAC,EAAE,UAAU,EAAE,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,cAAc,EAAE,CAAC;SACjH,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;IACpB,CAAC,CAAC,CAAC;IAGH,QAAQ,CAAC,oBAAoB,EAAE,GAAG,EAAE;QAClC,EAAE,CAAC,iEAAiE,EAAE,GAAG,EAAE;YACzE,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,CAAC,EAAE,UAAU,EAAE,WAAW,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtG,CAAC,CAAC,CAAC;QACH,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;YACzD,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,CAAC,EAAE,UAAU,EAAE,aAAa,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACzG,CAAC,CAAC,CAAC;QACH,EAAE,CAAC,iEAAiE,EAAE,GAAG,EAAE;YACzE,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,CAAC,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACrG,CAAC,CAAC,CAAC;QACH,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;YAEzD,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,CAAC,EAAE,UAAU,EAAE,WAAW,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtG,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAGH,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;QAC1B,EAAE,CAAC,2EAA2E,EAAE,GAAG,EAAE;YAEnF,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,QAAQ,CAAC,EAAE,YAAY,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAChG,CAAC,CAAC,CAAC;QACH,EAAE,CAAC,oEAAoE,EAAE,GAAG,EAAE;YAC5E,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,QAAQ,CAAC,EAAE,YAAY,EAAE,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrG,CAAC,CAAC,CAAC;QACH,EAAE,CAAC,mDAAmD,EAAE,GAAG,EAAE;YAC3D,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,CAAC,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,SAAS,CAAC,EAAE,kBAAkB,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC1H,CAAC,CAAC,CAAC;QACH,EAAE,CAAC,sDAAsD,EAAE,GAAG,EAAE;YAC9D,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,CAAC,EAAE,aAAa,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,SAAS,CAAC,EAAE,kBAAkB,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC1H,CAAC,CAAC,CAAC;QACH,EAAE,CAAC,kEAAkE,EAAE,GAAG,EAAE;YAC1E,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,CAAC,EAAE,UAAU,EAAE,WAAW,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,SAAS,CAAC,EAAE,cAAc,EAAE,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACnI,CAAC,CAAC,CAAC;QACH,EAAE,CAAC,gEAAgE,EAAE,GAAG,EAAE;YACxE,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,CAAC,EAAE,UAAU,EAAE,YAAY,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,SAAS,CAAC,EAAE,cAAc,EAAE,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnI,CAAC,CAAC,CAAC;QAGH,EAAE,CAAC,oEAAoE,EAAE,GAAG,EAAE;YAC5E,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,CAAC,EAAE,aAAa,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,kBAAkB,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACtG,CAAC,CAAC,CAAC;QACH,EAAE,CAAC,8DAA8D,EAAE,GAAG,EAAE;YACtE,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,EAAE,EAAE,EAAE,YAAY,EAAE,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACnF,CAAC,CAAC,CAAC;QACH,EAAE,CAAC,gEAAgE,EAAE,GAAG,EAAE;YACxE,MAAM,CAAC,IAAA,wBAAU,EAAC,WAAW,CAAC,EAAE,UAAU,EAAE,YAAY,EAAE,CAAC,EAAE,EAAE,cAAc,EAAE,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC/G,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","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 // --- sellers (positive allow-list) -------------------------------------\n describe('sellers (positive)', () => {\n it('matches when sellerName is in filter.sellers (case-insensitive)', () => {\n expect(evalFilter(makeProduct({ sellerName: 'Falabella' }), { sellers: ['falabella'] })).toBe(true);\n });\n it('misses when sellerName is NOT in filter.sellers', () => {\n expect(evalFilter(makeProduct({ sellerName: 'OtherSeller' }), { sellers: ['falabella'] })).toBe(false);\n });\n it('misses when product has no sellerName but filter.sellers is set', () => {\n expect(evalFilter(makeProduct({ sellerName: undefined }), { sellers: ['falabella'] })).toBe(false);\n });\n it('counts as a positive field for the safety floor', () => {\n // sellers-only filter is allowed (not exclusion-only)\n expect(evalFilter(makeProduct({ sellerName: 'Falabella' }), { sellers: ['falabella'] })).toBe(true);\n });\n });\n\n // --- excludeWords / excludeSellers / excludeMarketplace (negative) -----\n describe('exclusions', () => {\n it('excludeWords: rejects when name contains an excluded word (substring, ci)', () => {\n // positive word matches, but excludeWords kicks it out\n expect(evalFilter(makeProduct(), { words: ['galaxy'], excludeWords: ['ultra'] })).toBe(false);\n });\n it('excludeWords: passes when name contains none of the excluded words', () => {\n expect(evalFilter(makeProduct(), { words: ['galaxy'], excludeWords: ['refurbished'] })).toBe(true);\n });\n it('excludeMarketplace: rejects a marketplace product', () => {\n expect(evalFilter(makeProduct({ isMarketplace: true }), { brands: ['Samsung'], excludeMarketplace: true })).toBe(false);\n });\n it('excludeMarketplace: passes a non-marketplace product', () => {\n expect(evalFilter(makeProduct({ isMarketplace: false }), { brands: ['Samsung'], excludeMarketplace: true })).toBe(true);\n });\n it('excludeSellers: rejects when sellerName is in the deny-list (ci)', () => {\n expect(evalFilter(makeProduct({ sellerName: 'BadSeller' }), { brands: ['Samsung'], excludeSellers: ['badseller'] })).toBe(false);\n });\n it('excludeSellers: passes when sellerName is not in the deny-list', () => {\n expect(evalFilter(makeProduct({ sellerName: 'GoodSeller' }), { brands: ['Samsung'], excludeSellers: ['badseller'] })).toBe(true);\n });\n\n // anti-spam floor: exclusion-only filters match NOTHING\n it('excludeMarketplace-only filter matches nothing (no positive field)', () => {\n expect(evalFilter(makeProduct({ isMarketplace: false }), { excludeMarketplace: true })).toBe(false);\n });\n it('excludeWords-only filter matches nothing (no positive field)', () => {\n expect(evalFilter(makeProduct(), { excludeWords: ['refurbished'] })).toBe(false);\n });\n it('excludeSellers-only filter matches nothing (no positive field)', () => {\n expect(evalFilter(makeProduct({ sellerName: 'GoodSeller' }), { excludeSellers: ['badseller'] })).toBe(false);\n });\n });\n});\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "store-scrapper-js-common",
3
- "version": "1.0.219",
3
+ "version": "1.0.222",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -261,4 +261,55 @@ describe('evalFilter — robustness over untrusted JSON filters', () => {
261
261
  brands: ['Samsung'], categories: ['x'], stores: ['y'], words: ['z'], minPrice: 1, maxPrice: 9, minDiscountPct: 5,
262
262
  })).not.toThrow();
263
263
  });
264
+
265
+ // --- sellers (positive allow-list) -------------------------------------
266
+ describe('sellers (positive)', () => {
267
+ it('matches when sellerName is in filter.sellers (case-insensitive)', () => {
268
+ expect(evalFilter(makeProduct({ sellerName: 'Falabella' }), { sellers: ['falabella'] })).toBe(true);
269
+ });
270
+ it('misses when sellerName is NOT in filter.sellers', () => {
271
+ expect(evalFilter(makeProduct({ sellerName: 'OtherSeller' }), { sellers: ['falabella'] })).toBe(false);
272
+ });
273
+ it('misses when product has no sellerName but filter.sellers is set', () => {
274
+ expect(evalFilter(makeProduct({ sellerName: undefined }), { sellers: ['falabella'] })).toBe(false);
275
+ });
276
+ it('counts as a positive field for the safety floor', () => {
277
+ // sellers-only filter is allowed (not exclusion-only)
278
+ expect(evalFilter(makeProduct({ sellerName: 'Falabella' }), { sellers: ['falabella'] })).toBe(true);
279
+ });
280
+ });
281
+
282
+ // --- excludeWords / excludeSellers / excludeMarketplace (negative) -----
283
+ describe('exclusions', () => {
284
+ it('excludeWords: rejects when name contains an excluded word (substring, ci)', () => {
285
+ // positive word matches, but excludeWords kicks it out
286
+ expect(evalFilter(makeProduct(), { words: ['galaxy'], excludeWords: ['ultra'] })).toBe(false);
287
+ });
288
+ it('excludeWords: passes when name contains none of the excluded words', () => {
289
+ expect(evalFilter(makeProduct(), { words: ['galaxy'], excludeWords: ['refurbished'] })).toBe(true);
290
+ });
291
+ it('excludeMarketplace: rejects a marketplace product', () => {
292
+ expect(evalFilter(makeProduct({ isMarketplace: true }), { brands: ['Samsung'], excludeMarketplace: true })).toBe(false);
293
+ });
294
+ it('excludeMarketplace: passes a non-marketplace product', () => {
295
+ expect(evalFilter(makeProduct({ isMarketplace: false }), { brands: ['Samsung'], excludeMarketplace: true })).toBe(true);
296
+ });
297
+ it('excludeSellers: rejects when sellerName is in the deny-list (ci)', () => {
298
+ expect(evalFilter(makeProduct({ sellerName: 'BadSeller' }), { brands: ['Samsung'], excludeSellers: ['badseller'] })).toBe(false);
299
+ });
300
+ it('excludeSellers: passes when sellerName is not in the deny-list', () => {
301
+ expect(evalFilter(makeProduct({ sellerName: 'GoodSeller' }), { brands: ['Samsung'], excludeSellers: ['badseller'] })).toBe(true);
302
+ });
303
+
304
+ // anti-spam floor: exclusion-only filters match NOTHING
305
+ it('excludeMarketplace-only filter matches nothing (no positive field)', () => {
306
+ expect(evalFilter(makeProduct({ isMarketplace: false }), { excludeMarketplace: true })).toBe(false);
307
+ });
308
+ it('excludeWords-only filter matches nothing (no positive field)', () => {
309
+ expect(evalFilter(makeProduct(), { excludeWords: ['refurbished'] })).toBe(false);
310
+ });
311
+ it('excludeSellers-only filter matches nothing (no positive field)', () => {
312
+ expect(evalFilter(makeProduct({ sellerName: 'GoodSeller' }), { excludeSellers: ['badseller'] })).toBe(false);
313
+ });
314
+ });
264
315
  });
@@ -3,16 +3,22 @@ import { getMinPrice } from './price-utils';
3
3
 
4
4
  /**
5
5
  * User-defined filter for an alert channel.
6
- * Every present field is AND-ed; an absent (or empty/blank) field is ignored.
7
- * A filter with NO effective fields returns false (safety floor).
6
+ * POSITIVE fields are AND-ed; an absent (or empty/blank) field is ignored.
7
+ * EXCLUSION fields only narrow (they reject matches) and do NOT count toward the
8
+ * safety floor. A filter with NO positive field returns false (anti-spam floor) —
9
+ * so an exclusion-only filter (e.g. just `excludeMarketplace`) matches nothing,
10
+ * never "everything except X".
8
11
  */
9
12
  export type ChannelFilter = {
13
+ // ── Positive (inclusion) — AND-ed; at least one is required to match anything ──
10
14
  /** Match if any of the product's AI-normalized `inferredCategories` is any of these (case-insensitive). */
11
15
  categories?: string[];
12
16
  /** Match if the product's brandName is any of these (case-insensitive). */
13
17
  brands?: string[];
14
18
  /** Match if the product's storeRef is any of these (exact). */
15
19
  stores?: string[];
20
+ /** Match if the product's sellerName is any of these (case-insensitive exact). */
21
+ sellers?: string[];
16
22
  /** Match if product.priceStats.currentPricePrevPriceDiffPct >= this (>0 to be active). */
17
23
  minDiscountPct?: number;
18
24
  /** Match if the product's current min price >= this value. */
@@ -21,6 +27,14 @@ export type ChannelFilter = {
21
27
  maxPrice?: number;
22
28
  /** Match if any of these words appears (case-insensitive substring) in product.name. */
23
29
  words?: string[];
30
+
31
+ // ── Exclusion (negative) — reject a match; do NOT satisfy the safety floor ──
32
+ /** Skip if any of these words appears (case-insensitive substring) in product.name. */
33
+ excludeWords?: string[];
34
+ /** Skip if the product's sellerName is any of these (case-insensitive exact). */
35
+ excludeSellers?: string[];
36
+ /** Skip if the product is a marketplace listing (product.isMarketplace === true). */
37
+ excludeMarketplace?: boolean;
24
38
  };
25
39
 
26
40
  /** Trimmed, non-empty string elements (optionally lowercased), or null if none. */
@@ -53,14 +67,22 @@ export function evalFilter(product: Product, filter: ChannelFilter): boolean {
53
67
  const categories = cleanSet(filter.categories, true);
54
68
  const brands = cleanSet(filter.brands, true);
55
69
  const stores = cleanSet(filter.stores, false); // storeRef is an id — exact, no lowercase
70
+ const sellers = cleanSet(filter.sellers, true);
56
71
  const words = cleanSet(filter.words, true);
57
72
  // minDiscountPct of 0 (or negative/NaN) is "no floor" => inactive
58
73
  const minDiscountPct = finite(filter.minDiscountPct) && filter.minDiscountPct > 0 ? filter.minDiscountPct : null;
59
74
  const minPrice = finite(filter.minPrice) ? filter.minPrice : null;
60
75
  const maxPrice = finite(filter.maxPrice) ? filter.maxPrice : null;
61
76
 
62
- // Safety floor: a filter with no effective field must not match anything.
63
- if (!categories && !brands && !stores && !words
77
+ // Exclusion fields only narrow; they do NOT satisfy the safety floor below.
78
+ const excludeWords = cleanSet(filter.excludeWords, true);
79
+ const excludeSellers = cleanSet(filter.excludeSellers, true);
80
+ const excludeMarketplace = filter.excludeMarketplace === true;
81
+
82
+ // Safety floor (anti-spam): require at least one POSITIVE field. Exclusion-only
83
+ // filters (excludeWords/excludeSellers/excludeMarketplace) match nothing — never
84
+ // "everything except X".
85
+ if (!categories && !brands && !stores && !sellers && !words
64
86
  && minDiscountPct === null && minPrice === null && maxPrice === null) {
65
87
  return false;
66
88
  }
@@ -94,10 +116,28 @@ export function evalFilter(product: Product, filter: ChannelFilter): boolean {
94
116
  if (maxPrice !== null && currentPrice > maxPrice) return false;
95
117
  }
96
118
 
119
+ if (sellers) {
120
+ const s = product.sellerName?.toLowerCase();
121
+ if (!s || !sellers.includes(s)) return false;
122
+ }
123
+
97
124
  if (words) {
98
125
  const name = product.name?.toLowerCase();
99
126
  if (!name || !words.some((w) => name.includes(w))) return false;
100
127
  }
101
128
 
129
+ // ── Exclusions (applied after the positive AND passes; any hit rejects) ──
130
+ if (excludeMarketplace && product.isMarketplace === true) return false;
131
+
132
+ if (excludeWords) {
133
+ const name = product.name?.toLowerCase();
134
+ if (name && excludeWords.some((w) => name.includes(w))) return false;
135
+ }
136
+
137
+ if (excludeSellers) {
138
+ const s = product.sellerName?.toLowerCase();
139
+ if (s && excludeSellers.includes(s)) return false;
140
+ }
141
+
102
142
  return true;
103
143
  }