@factorypure/client-helpers 1.1.28 → 1.2.0

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.
Files changed (2) hide show
  1. package/dist/index.js +73 -40
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -480,7 +480,6 @@ export const ABSOLUTE_BLOCK_RULE_IDS = [
480
480
  'duplicate',
481
481
  'high_price_outlier',
482
482
  'low_price_outlier',
483
- 'skip_sku',
484
483
  ];
485
484
  /**
486
485
  * Rule IDs that can be overridden when the matching-engine confidence is high enough.
@@ -496,6 +495,7 @@ export const CONFIDENCE_OVERRIDABLE_RULE_IDS = [
496
495
  'competitor_exclusion',
497
496
  'date_outlier',
498
497
  'critical_spec_mismatch',
498
+ 'skip_sku',
499
499
  ];
500
500
  /**
501
501
  * Evaluates whether a result should be surfaced given its filter state and matching confidence.
@@ -524,7 +524,6 @@ export function evaluateConfidenceOverride(filterResults, visibilityState, confi
524
524
  return 'show_with_override';
525
525
  return 'hide';
526
526
  }
527
- // const wattages = Array.from({ length: 41 }, (_, i) => (5000 + i * 500).toString())
528
527
  // ===============================
529
528
  // Filter Rule Registry
530
529
  // ===============================
@@ -908,6 +907,7 @@ class SearchExclusionRule {
908
907
  return null;
909
908
  }
910
909
  }
910
+ const skipSkuRegexCache = new Map();
911
911
  /**
912
912
  * Rule for SKU skip filtering
913
913
  */
@@ -921,28 +921,32 @@ class SkipSkuRule {
921
921
  canBeOverridden = true;
922
922
  overridableBy = ['sku_match', 'calculated_sku_match', 'alt_sku_match'];
923
923
  evaluate(context) {
924
- const nots = [];
925
- const formatted = context.vendorScrapeOptions.default_skip_skus
926
- .filter((s) => s.toLowerCase() !== context.variant.sku.toLowerCase())
927
- .map((s) => `${s}`);
928
- nots.push(...formatted);
929
- nots.push(...context.vendorScrapeOptions.vendor_skip_skus.filter((s) => s.toLowerCase() !== context.variant.sku.toLowerCase()));
930
- // Get SKU alternates for comparison
924
+ const variantSkuLower = context.variant.sku.toLowerCase();
925
+ const allSkus = [
926
+ ...context.vendorScrapeOptions.default_skip_skus,
927
+ ...context.vendorScrapeOptions.vendor_skip_skus,
928
+ ];
929
+ const nots = allSkus.filter((s) => s.toLowerCase() !== variantSkuLower);
930
+ if (nots.length === 0)
931
+ return null;
931
932
  const skuAlternates = context.variantScrapeOptions.sku_alternates || [];
932
- const skuAlternatesLower = skuAlternates.map((alt) => alt.toLowerCase());
933
- const hasExclusion = nots.some((sku) => {
934
- const escapedSku = sku.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
935
- const skuRegex = new RegExp(`(?: |\/|\&|\=|"|'|\`)${escapedSku}(?: |\/|\&|\=|"|'|\`)`, 'gi');
936
- const skuMatches = context.result.title?.match(skuRegex);
937
- // If this skip SKU is found in the title
938
- if (skuMatches && skuMatches.length > 0) {
939
- // Check if this skip SKU is actually one of the variant's alternates
940
- const isAlternate = skuAlternatesLower.includes(sku.toLowerCase());
941
- // Only count as exclusion if it's NOT an alternate
942
- return !isAlternate;
943
- }
944
- return false;
945
- });
933
+ const cacheKey = variantSkuLower + '\x00' + nots.join('\x00') + '\x01' + skuAlternates.join('\x00');
934
+ let cached = skipSkuRegexCache.get(cacheKey);
935
+ if (!cached) {
936
+ const alternatesSet = new Set(skuAlternates.map((a) => a.toLowerCase()));
937
+ // Only build regex for SKUs that are not alternates (alternates can never be exclusions)
938
+ const nonAlternateNots = nots.filter((s) => !alternatesSet.has(s.toLowerCase()));
939
+ const regex = nonAlternateNots.length > 0
940
+ ? new RegExp(`(?: |\/|\&|\=|"|'|\`)(?:${nonAlternateNots
941
+ .map((s) => s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'))
942
+ .join('|')})(?: |\/|\&|\=|"|'|\`)`, 'i')
943
+ : null;
944
+ cached = { regex: regex, alternatesSet };
945
+ skipSkuRegexCache.set(cacheKey, cached);
946
+ }
947
+ if (!cached.regex)
948
+ return null;
949
+ const hasExclusion = cached.regex.test(context.result.title ?? '');
946
950
  if (hasExclusion) {
947
951
  return {
948
952
  ruleId: this.id,
@@ -955,6 +959,7 @@ class SkipSkuRule {
955
959
  return null;
956
960
  }
957
961
  }
962
+ const vendorExclusionRegexCache = new Map();
958
963
  /**
959
964
  * Rule for vendor exclusions
960
965
  */
@@ -967,18 +972,42 @@ class VendorExclusionRule {
967
972
  enabled = true;
968
973
  canBeOverridden = true;
969
974
  overridableBy = ['sku_match', 'calculated_sku_match', 'alt_sku_match', 'match_confirmation_override'];
975
+ // evaluate(context: FilterContext): FilterResult | null {
976
+ // const nots: string[] = []
977
+ // const formatted = context.vendorScrapeOptions.default_skip_vendors
978
+ // .filter((s) => s.toLowerCase() !== context.variant.vendor.toLowerCase())
979
+ // .map((s) => `${s}`)
980
+ // nots.push(...formatted)
981
+ // const hasExclusion = nots.some((vendor) => {
982
+ // const escaped = vendor.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')
983
+ // const matchRegex = new RegExp(`(?: |\/|\&|\=|"|'|\`)${escaped}(?: |\/|\&|\=|"|'|\`)`, 'gi')
984
+ // const matches = context.result.title?.match(matchRegex)
985
+ // return matches && matches.length > 0
986
+ // })
987
+ // if (hasExclusion) {
988
+ // return {
989
+ // ruleId: this.id,
990
+ // severity: this.severity,
991
+ // message: HIDE_REASONS.VENDOR_EXCLUSION,
992
+ // metadata: { title: context.result.title },
993
+ // timestamp: new Date().toISOString(),
994
+ // }
995
+ // }
996
+ // return null
997
+ // }
970
998
  evaluate(context) {
971
- const nots = [];
972
- const formatted = context.vendorScrapeOptions.default_skip_vendors
973
- .filter((s) => s.toLowerCase() !== context.variant.vendor.toLowerCase())
974
- .map((s) => `${s}`);
975
- nots.push(...formatted);
976
- const hasExclusion = nots.some((vendor) => {
977
- const escaped = vendor.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
978
- const matchRegex = new RegExp(`(?: |\/|\&|\=|"|'|\`)${escaped}(?: |\/|\&|\=|"|'|\`)`, 'gi');
979
- const matches = context.result.title?.match(matchRegex);
980
- return matches && matches.length > 0;
981
- });
999
+ const variantVendorLower = context.variant.vendor.toLowerCase();
1000
+ const vendors = context.vendorScrapeOptions.default_skip_vendors.filter((s) => s.toLowerCase() !== variantVendorLower);
1001
+ if (vendors.length === 0)
1002
+ return null;
1003
+ const cacheKey = variantVendorLower + '\x00' + vendors.join('\x00');
1004
+ let matchRegex = vendorExclusionRegexCache.get(cacheKey);
1005
+ if (!matchRegex) {
1006
+ const combinedPattern = vendors.map((s) => s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')).join('|');
1007
+ matchRegex = new RegExp(`(?: |\/|\&|\=|"|'|\`)(?:${combinedPattern})(?: |\/|\&|\=|"|'|\`)`, 'i');
1008
+ vendorExclusionRegexCache.set(cacheKey, matchRegex);
1009
+ }
1010
+ const hasExclusion = matchRegex.test(context.result.title ?? '');
982
1011
  if (hasExclusion) {
983
1012
  return {
984
1013
  ruleId: this.id,
@@ -1255,8 +1284,9 @@ class RefurbishedUsedRule {
1255
1284
  evaluate(context) {
1256
1285
  const refurbishedKeywords = getRefurbishedKeywords(context.variant);
1257
1286
  const normalizedTitle = context.result.title?.toLowerCase().replace(/[^a-z0-9-]/g, ' ') ?? '';
1287
+ const titleWords = normalizedTitle.split(/\s+/);
1258
1288
  const details = context.result.details_and_offers;
1259
- const isRefurbished = refurbishedKeywords.some((keyword) => normalizedTitle.split(/\s+/).includes(keyword)) ||
1289
+ const isRefurbished = refurbishedKeywords.some((keyword) => titleWords.includes(keyword)) ||
1260
1290
  (details && details.includes('Pre-owned'));
1261
1291
  if (isRefurbished) {
1262
1292
  return {
@@ -1624,6 +1654,9 @@ export function createCustomFilterRule(config) {
1624
1654
  * This provides more flexibility and better extensibility than the legacy filterScrapeResults
1625
1655
  */
1626
1656
  export const filterScrapeResultsV2 = ({ scrapeResults, variant, variantScrapeOptions, vendorScrapeOptions, globalScrapeOptions, filterConfig, customRules, comparisonMap, matchConfirmationMap, }) => {
1657
+ if (scrapeResults.length === 0 || !scrapeResults) {
1658
+ return [];
1659
+ }
1627
1660
  const registry = createDefaultFilterRegistry();
1628
1661
  // Register custom rules if provided
1629
1662
  if (customRules) {
@@ -2024,11 +2057,9 @@ export const getRefurbishedKeywords = (variant) => {
2024
2057
  ];
2025
2058
  if (variant?.sku) {
2026
2059
  const skuBasedKeywords = [
2060
+ `${variant.sku.toLowerCase()}-r`,
2061
+ `${variant.sku.toUpperCase()}-R`,
2027
2062
  `${variant.sku}-R`,
2028
- `${variant.sku}-R`.toLowerCase(),
2029
- `${variant.sku}-R`.toUpperCase(),
2030
- `${variant.sku?.toLowerCase()}-R`,
2031
- `${variant.sku?.toUpperCase()}-R`,
2032
2063
  ];
2033
2064
  refurbishedKeywords.push(...skuBasedKeywords);
2034
2065
  }
@@ -2251,7 +2282,9 @@ export function getShownResults(matchResult, sortedResults, minConfidence, sortD
2251
2282
  shownResults.push(foundResult);
2252
2283
  }
2253
2284
  else if (foundResult &&
2254
- foundResult.filter_results?.map((fr) => fr.ruleId).includes('match_confirmation_override')) {
2285
+ foundResult.filter_results?.some((fr) => fr?.metadata?.isOverride === true)
2286
+ // foundResult.filter_results?.map((fr: FilterResult) => fr.ruleId).includes('match_confirmation_override')
2287
+ ) {
2255
2288
  shownResults.push(foundResult);
2256
2289
  }
2257
2290
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@factorypure/client-helpers",
3
- "version": "1.1.28",
3
+ "version": "1.2.0",
4
4
  "description": "",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",