@factorypure/client-helpers 1.1.27 → 1.1.29

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.
package/dist/index.d.ts CHANGED
@@ -385,6 +385,7 @@ export declare const scrapeResultsSchema: z.ZodObject<{
385
385
  parsed_results: z.ZodOptional<z.ZodNull>;
386
386
  lowest_price: z.ZodOptional<z.ZodNull>;
387
387
  highest_price: z.ZodOptional<z.ZodNull>;
388
+ listing_hash: z.ZodOptional<z.ZodNull>;
388
389
  latest_source_count: z.ZodOptional<z.ZodNull>;
389
390
  largest_source_count: z.ZodOptional<z.ZodNull>;
390
391
  is_immersive: z.ZodOptional<z.ZodNull>;
@@ -575,6 +576,8 @@ export declare const variantScrapeOptionsSchema: z.ZodObject<{
575
576
  key_parts: z.ZodString;
576
577
  }, z.core.$strip>>>;
577
578
  day_window: z.ZodNullable<z.ZodString>;
579
+ latest_margin_email_sent_at: z.ZodNullable<z.ZodString>;
580
+ latest_map_email_sent_at: z.ZodNullable<z.ZodString>;
578
581
  }, z.core.$strip>;
579
582
  export type VariantScrapeOptionsType = z.infer<typeof variantScrapeOptionsSchema>;
580
583
  declare const matchConfirmationSchema: z.ZodObject<{
package/dist/index.js CHANGED
@@ -279,6 +279,7 @@ export const scrapeResultsSchema = z.object({
279
279
  parsed_results: z.null().optional(),
280
280
  lowest_price: z.null().optional(),
281
281
  highest_price: z.null().optional(),
282
+ listing_hash: z.null().optional(),
282
283
  latest_source_count: z.null().optional(),
283
284
  largest_source_count: z.null().optional(),
284
285
  is_immersive: z.null().optional(),
@@ -380,6 +381,8 @@ export const variantScrapeOptionsSchema = z.object({
380
381
  found_product_ids: z.array(z.string()),
381
382
  result_ignore_keys: z.array(ResultIgnoreSchema).nullable(),
382
383
  day_window: z.string().nullable(),
384
+ latest_margin_email_sent_at: z.string().nullable(),
385
+ latest_map_email_sent_at: z.string().nullable(),
383
386
  });
384
387
  const matchConfirmationSchema = z.object({
385
388
  confirmation_source: z.string(),
@@ -905,6 +908,7 @@ class SearchExclusionRule {
905
908
  return null;
906
909
  }
907
910
  }
911
+ const skipSkuRegexCache = new Map();
908
912
  /**
909
913
  * Rule for SKU skip filtering
910
914
  */
@@ -918,28 +922,32 @@ class SkipSkuRule {
918
922
  canBeOverridden = true;
919
923
  overridableBy = ['sku_match', 'calculated_sku_match', 'alt_sku_match'];
920
924
  evaluate(context) {
921
- const nots = [];
922
- const formatted = context.vendorScrapeOptions.default_skip_skus
923
- .filter((s) => s.toLowerCase() !== context.variant.sku.toLowerCase())
924
- .map((s) => `${s}`);
925
- nots.push(...formatted);
926
- nots.push(...context.vendorScrapeOptions.vendor_skip_skus.filter((s) => s.toLowerCase() !== context.variant.sku.toLowerCase()));
927
- // Get SKU alternates for comparison
925
+ const variantSkuLower = context.variant.sku.toLowerCase();
926
+ const allSkus = [
927
+ ...context.vendorScrapeOptions.default_skip_skus,
928
+ ...context.vendorScrapeOptions.vendor_skip_skus,
929
+ ];
930
+ const nots = allSkus.filter((s) => s.toLowerCase() !== variantSkuLower);
931
+ if (nots.length === 0)
932
+ return null;
928
933
  const skuAlternates = context.variantScrapeOptions.sku_alternates || [];
929
- const skuAlternatesLower = skuAlternates.map((alt) => alt.toLowerCase());
930
- const hasExclusion = nots.some((sku) => {
931
- const escapedSku = sku.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
932
- const skuRegex = new RegExp(`(?: |\/|\&|\=|"|'|\`)${escapedSku}(?: |\/|\&|\=|"|'|\`)`, 'gi');
933
- const skuMatches = context.result.title?.match(skuRegex);
934
- // If this skip SKU is found in the title
935
- if (skuMatches && skuMatches.length > 0) {
936
- // Check if this skip SKU is actually one of the variant's alternates
937
- const isAlternate = skuAlternatesLower.includes(sku.toLowerCase());
938
- // Only count as exclusion if it's NOT an alternate
939
- return !isAlternate;
940
- }
941
- return false;
942
- });
934
+ const cacheKey = variantSkuLower + '\x00' + nots.join('\x00') + '\x01' + skuAlternates.join('\x00');
935
+ let cached = skipSkuRegexCache.get(cacheKey);
936
+ if (!cached) {
937
+ const alternatesSet = new Set(skuAlternates.map((a) => a.toLowerCase()));
938
+ // Only build regex for SKUs that are not alternates (alternates can never be exclusions)
939
+ const nonAlternateNots = nots.filter((s) => !alternatesSet.has(s.toLowerCase()));
940
+ const regex = nonAlternateNots.length > 0
941
+ ? new RegExp(`(?: |\/|\&|\=|"|'|\`)(?:${nonAlternateNots
942
+ .map((s) => s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'))
943
+ .join('|')})(?: |\/|\&|\=|"|'|\`)`, 'i')
944
+ : null;
945
+ cached = { regex: regex, alternatesSet };
946
+ skipSkuRegexCache.set(cacheKey, cached);
947
+ }
948
+ if (!cached.regex)
949
+ return null;
950
+ const hasExclusion = cached.regex.test(context.result.title ?? '');
943
951
  if (hasExclusion) {
944
952
  return {
945
953
  ruleId: this.id,
@@ -952,6 +960,7 @@ class SkipSkuRule {
952
960
  return null;
953
961
  }
954
962
  }
963
+ const vendorExclusionRegexCache = new Map();
955
964
  /**
956
965
  * Rule for vendor exclusions
957
966
  */
@@ -964,18 +973,42 @@ class VendorExclusionRule {
964
973
  enabled = true;
965
974
  canBeOverridden = true;
966
975
  overridableBy = ['sku_match', 'calculated_sku_match', 'alt_sku_match', 'match_confirmation_override'];
976
+ // evaluate(context: FilterContext): FilterResult | null {
977
+ // const nots: string[] = []
978
+ // const formatted = context.vendorScrapeOptions.default_skip_vendors
979
+ // .filter((s) => s.toLowerCase() !== context.variant.vendor.toLowerCase())
980
+ // .map((s) => `${s}`)
981
+ // nots.push(...formatted)
982
+ // const hasExclusion = nots.some((vendor) => {
983
+ // const escaped = vendor.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')
984
+ // const matchRegex = new RegExp(`(?: |\/|\&|\=|"|'|\`)${escaped}(?: |\/|\&|\=|"|'|\`)`, 'gi')
985
+ // const matches = context.result.title?.match(matchRegex)
986
+ // return matches && matches.length > 0
987
+ // })
988
+ // if (hasExclusion) {
989
+ // return {
990
+ // ruleId: this.id,
991
+ // severity: this.severity,
992
+ // message: HIDE_REASONS.VENDOR_EXCLUSION,
993
+ // metadata: { title: context.result.title },
994
+ // timestamp: new Date().toISOString(),
995
+ // }
996
+ // }
997
+ // return null
998
+ // }
967
999
  evaluate(context) {
968
- const nots = [];
969
- const formatted = context.vendorScrapeOptions.default_skip_vendors
970
- .filter((s) => s.toLowerCase() !== context.variant.vendor.toLowerCase())
971
- .map((s) => `${s}`);
972
- nots.push(...formatted);
973
- const hasExclusion = nots.some((vendor) => {
974
- const escaped = vendor.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
975
- const matchRegex = new RegExp(`(?: |\/|\&|\=|"|'|\`)${escaped}(?: |\/|\&|\=|"|'|\`)`, 'gi');
976
- const matches = context.result.title?.match(matchRegex);
977
- return matches && matches.length > 0;
978
- });
1000
+ const variantVendorLower = context.variant.vendor.toLowerCase();
1001
+ const vendors = context.vendorScrapeOptions.default_skip_vendors.filter((s) => s.toLowerCase() !== variantVendorLower);
1002
+ if (vendors.length === 0)
1003
+ return null;
1004
+ const cacheKey = variantVendorLower + '\x00' + vendors.join('\x00');
1005
+ let matchRegex = vendorExclusionRegexCache.get(cacheKey);
1006
+ if (!matchRegex) {
1007
+ const combinedPattern = vendors.map((s) => s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')).join('|');
1008
+ matchRegex = new RegExp(`(?: |\/|\&|\=|"|'|\`)(?:${combinedPattern})(?: |\/|\&|\=|"|'|\`)`, 'i');
1009
+ vendorExclusionRegexCache.set(cacheKey, matchRegex);
1010
+ }
1011
+ const hasExclusion = matchRegex.test(context.result.title ?? '');
979
1012
  if (hasExclusion) {
980
1013
  return {
981
1014
  ruleId: this.id,
@@ -1252,8 +1285,9 @@ class RefurbishedUsedRule {
1252
1285
  evaluate(context) {
1253
1286
  const refurbishedKeywords = getRefurbishedKeywords(context.variant);
1254
1287
  const normalizedTitle = context.result.title?.toLowerCase().replace(/[^a-z0-9-]/g, ' ') ?? '';
1288
+ const titleWords = normalizedTitle.split(/\s+/);
1255
1289
  const details = context.result.details_and_offers;
1256
- const isRefurbished = refurbishedKeywords.some((keyword) => normalizedTitle.split(/\s+/).includes(keyword)) ||
1290
+ const isRefurbished = refurbishedKeywords.some((keyword) => titleWords.includes(keyword)) ||
1257
1291
  (details && details.includes('Pre-owned'));
1258
1292
  if (isRefurbished) {
1259
1293
  return {
@@ -1621,6 +1655,9 @@ export function createCustomFilterRule(config) {
1621
1655
  * This provides more flexibility and better extensibility than the legacy filterScrapeResults
1622
1656
  */
1623
1657
  export const filterScrapeResultsV2 = ({ scrapeResults, variant, variantScrapeOptions, vendorScrapeOptions, globalScrapeOptions, filterConfig, customRules, comparisonMap, matchConfirmationMap, }) => {
1658
+ if (scrapeResults.length === 0 || !scrapeResults) {
1659
+ return [];
1660
+ }
1624
1661
  const registry = createDefaultFilterRegistry();
1625
1662
  // Register custom rules if provided
1626
1663
  if (customRules) {
@@ -1644,8 +1681,10 @@ export const filterScrapeResultsV2 = ({ scrapeResults, variant, variantScrapeOpt
1644
1681
  function handleDuplicatesV2(results) {
1645
1682
  const filteredUniqueResultsMap = {};
1646
1683
  results.forEach((item) => {
1647
- // @ts-ignore
1648
1684
  const key = item.listing_hash;
1685
+ if (!key) {
1686
+ return;
1687
+ }
1649
1688
  if (!filteredUniqueResultsMap[key]) {
1650
1689
  filteredUniqueResultsMap[key] = item;
1651
1690
  }
@@ -2019,11 +2058,9 @@ export const getRefurbishedKeywords = (variant) => {
2019
2058
  ];
2020
2059
  if (variant?.sku) {
2021
2060
  const skuBasedKeywords = [
2061
+ `${variant.sku.toLowerCase()}-r`,
2062
+ `${variant.sku.toUpperCase()}-R`,
2022
2063
  `${variant.sku}-R`,
2023
- `${variant.sku}-R`.toLowerCase(),
2024
- `${variant.sku}-R`.toUpperCase(),
2025
- `${variant.sku?.toLowerCase()}-R`,
2026
- `${variant.sku?.toUpperCase()}-R`,
2027
2064
  ];
2028
2065
  refurbishedKeywords.push(...skuBasedKeywords);
2029
2066
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@factorypure/client-helpers",
3
- "version": "1.1.27",
3
+ "version": "1.1.29",
4
4
  "description": "",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",