@factorypure/client-helpers 1.1.29 → 1.2.1

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 (3) hide show
  1. package/dist/index.d.ts +57 -24
  2. package/dist/index.js +345 -136
  3. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -257,6 +257,19 @@ export declare enum VisibilityState {
257
257
  VISIBLE_WITH_WARNINGS = "VISIBLE_WITH_WARNINGS",
258
258
  VISIBLE = "VISIBLE"
259
259
  }
260
+ /**
261
+ * Result groups that replace VisibilityState with richer classification.
262
+ * Each result is assigned a group after all filter rules run:
263
+ * MATCH — no active blocks (shown; undercut determination is a separate downstream step)
264
+ * LOW_LIKELIHOOD — medium confidence only (50–79); shown in "needs review" if cheaper
265
+ * NO_MATCH — low confidence or has an active block rule (hidden)
266
+ */
267
+ export declare enum ResultGroup {
268
+ MATCH = "match",
269
+ LOW_LIKELIHOOD = "low_likelihood",
270
+ NO_MATCH = "no_match",
271
+ EXCLUDED = "excluded"
272
+ }
260
273
  /**
261
274
  * Structured result from a filter rule evaluation
262
275
  */
@@ -310,7 +323,7 @@ export declare const scrapeResultsSchema: z.ZodObject<{
310
323
  metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
311
324
  timestamp: z.ZodString;
312
325
  }, z.core.$strip>>>;
313
- visibility_state: z.ZodOptional<z.ZodEnum<typeof VisibilityState>>;
326
+ visibility_state: z.ZodOptional<z.ZodEnum<typeof ResultGroup>>;
314
327
  regexUnitResults: z.ZodNullable<z.ZodObject<{
315
328
  inch: z.ZodArray<z.ZodObject<{
316
329
  value: z.ZodString;
@@ -445,7 +458,7 @@ export declare const immersiveScrapeResultsSchema: z.ZodObject<{
445
458
  metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
446
459
  timestamp: z.ZodString;
447
460
  }, z.core.$strip>>>;
448
- visibility_state: z.ZodOptional<z.ZodEnum<typeof VisibilityState>>;
461
+ visibility_state: z.ZodOptional<z.ZodEnum<typeof ResultGroup>>;
449
462
  brand: z.ZodNullable<z.ZodString>;
450
463
  company_id: z.ZodNullable<z.ZodNumber>;
451
464
  regexUnitResults: z.ZodNullable<z.ZodObject<{
@@ -702,6 +715,8 @@ export declare const HIDE_REASONS: {
702
715
  CALCULATED_BRAND_MISMATCH: string;
703
716
  SCAM_SOURCE_EXCLUSION: string;
704
717
  AI_COMPARISON_MISMATCH: string;
718
+ ABSOLUTE_LOW_PRICE_OUTLIER: string;
719
+ ABSOLUTE_HIGH_PRICE_OUTLIER: string;
705
720
  };
706
721
  export declare const HIDE_OVERRIDE_REASONS: {
707
722
  SKU_MATCH: string;
@@ -719,23 +734,23 @@ export declare const TOO_EXPENSIVE_MULTIPLIER = 1.15;
719
734
  * These represent explicit user intent or unambiguously bad data.
720
735
  */
721
736
  export declare const ABSOLUTE_BLOCK_RULE_IDS: readonly string[];
737
+ /**
738
+ * Rule IDs that represent explicit admin/security decisions.
739
+ * These are the ONLY blocks respected when a confidence override or match confirmation is present.
740
+ * Operational blocks (out_of_stock, price outliers, etc.) are superseded by overrides.
741
+ */
742
+ export declare const MANUAL_EXCLUSION_RULE_IDS: readonly string[];
722
743
  /**
723
744
  * Rule IDs that can be overridden when the matching-engine confidence is high enough.
724
745
  * These represent heuristics that may be wrong when identifier/title matching is strong.
725
746
  */
726
747
  export declare const CONFIDENCE_OVERRIDABLE_RULE_IDS: readonly string[];
727
748
  /**
728
- * Evaluates whether a result should be surfaced given its filter state and matching confidence.
729
- *
730
- * Returns:
731
- * - `'hide'` — result must not be shown
732
- * - `'show'` — result passes all filters normally
733
- * - `'show_with_override'`— result failed soft filters but confidence is high enough to surface it anyway
734
- *
735
- * @param filterResults The filter_results array from the filter engine
736
- * @param visibilityState The visibility_state computed by the filter engine
737
- * @param confidence Matching-engine confidence score (0–100)
738
- * @param overrideThreshold Minimum confidence to override a soft BLOCK rule (default 80)
749
+ * @deprecated Confidence overrides are now handled by the filter engine.
750
+ * Pass a `confidenceMap` to `filterScrapeResultsV2` — the engine injects a synthetic
751
+ * `high_confidence_override` rule result when confidence ≥ 80, which is listed in the
752
+ * `overridableBy` array of all soft block/warning rules. `visibility_state` on each result
753
+ * is then accurate and final before `getShownResults` is called.
739
754
  */
740
755
  export declare function evaluateConfidenceOverride(filterResults: FilterResult[], visibilityState: VisibilityState, confidence: number, overrideThreshold?: number): 'show' | 'show_with_override' | 'hide';
741
756
  /**
@@ -782,11 +797,18 @@ export declare class FilterEngine {
782
797
  /**
783
798
  * Evaluate rules for a batch of results
784
799
  */
785
- evaluateBatch<T extends ScrapeResultsType | ImmersiveScrapeResultsType>(results: T[], variant: FilterContext<T>['variant'], variantScrapeOptions: VariantScrapeOptionsType, vendorScrapeOptions: VendorScrapeOptionsType, globalScrapeOptions: GlobalScrapeOptionsType, comparisonMap?: ComparisonMapType, matchConfirmationMap?: MatchConfirmationMapType): T[];
800
+ evaluateBatch<T extends ScrapeResultsType | ImmersiveScrapeResultsType>(results: T[], variant: FilterContext<T>['variant'], variantScrapeOptions: VariantScrapeOptionsType, vendorScrapeOptions: VendorScrapeOptionsType, globalScrapeOptions: GlobalScrapeOptionsType, comparisonMap?: ComparisonMapType, matchConfirmationMap?: MatchConfirmationMapType, matchResult?: MatchResult): T[];
786
801
  /**
787
- * Calculate visibility state based on filter results
802
+ * Calculate which result group a scrape result belongs to.
803
+ * MATCH — no manual blocks, and either an override is present or no blocks at all
804
+ * LOW_LIKELIHOOD — no overrides, medium_confidence is the only block (worth reviewing if cheaper)
805
+ * NO_MATCH — manual/security block present, or no overrides and more than one active block
806
+ *
807
+ * Key rule: any override (high_confidence_override, match_confirmation_override, sku_match, etc.)
808
+ * supersedes ALL operational blocks (out_of_stock, price outliers, mismatch rules, etc.).
809
+ * Only explicit admin decisions in MANUAL_EXCLUSION_RULE_IDS can block an overridden result.
788
810
  */
789
- calculateVisibility(filterResults: FilterResult[]): VisibilityState;
811
+ calculateGroup(filterResults: FilterResult[], resultPrice: number, variantPrice: number): ResultGroup;
790
812
  }
791
813
  /**
792
814
  * Create and populate a registry with all built-in rules
@@ -829,7 +851,7 @@ export declare function createCustomFilterRule(config: {
829
851
  * Filter scrape results using the new rule-based engine
830
852
  * This provides more flexibility and better extensibility than the legacy filterScrapeResults
831
853
  */
832
- export declare const filterScrapeResultsV2: <T extends ScrapeResultsType | ImmersiveScrapeResultsType>({ scrapeResults, variant, variantScrapeOptions, vendorScrapeOptions, globalScrapeOptions, filterConfig, customRules, comparisonMap, matchConfirmationMap, }: {
854
+ export declare const filterScrapeResultsV2: <T extends ScrapeResultsType | ImmersiveScrapeResultsType>({ scrapeResults, variant, variantScrapeOptions, vendorScrapeOptions, globalScrapeOptions, filterConfig, customRules, comparisonMap, matchConfirmationMap, matchResult, }: {
833
855
  scrapeResults: T[];
834
856
  variant: {
835
857
  id: number;
@@ -847,6 +869,7 @@ export declare const filterScrapeResultsV2: <T extends ScrapeResultsType | Immer
847
869
  customRules?: FilterRule[];
848
870
  comparisonMap?: ComparisonMapType;
849
871
  matchConfirmationMap?: MatchConfirmationMapType;
872
+ matchResult?: MatchResult;
850
873
  }) => T[];
851
874
  export declare const filterScrapeResults: <T extends ScrapeResultsType | ImmersiveScrapeResultsType>({ scrapeResults, variant, variantScrapeOptions, vendorScrapeOptions, globalScrapeOptions, }: {
852
875
  scrapeResults: T[];
@@ -889,14 +912,24 @@ export declare function findUnitVariations(data: any): any;
889
912
  * confidence is at or above overrideThreshold.
890
913
  * Results are sorted by price ascending by default; pass sortDirection to override.
891
914
  */
892
- export declare function getShownResults(matchResult: MatchResult, sortedResults: (ScrapeResultsType | ImmersiveScrapeResultsType)[], minConfidence: number, sortDirection?: 'asc' | 'desc', overrideThreshold?: number): (ScrapeResultsType | ImmersiveScrapeResultsType)[];
915
+ export declare function getShownResults(sortedResults: (ScrapeResultsType | ImmersiveScrapeResultsType)[], sortDirection?: 'asc' | 'desc'): (ScrapeResultsType | ImmersiveScrapeResultsType)[];
916
+ /**
917
+ * Extracts undercut results from the MATCH group — competitors selling strictly cheaper
918
+ * than the variant price (price < variantPrice).
919
+ *
920
+ * This is the downstream step that divides getShownResults output into two categories:
921
+ * - Undercuts: price < variantPrice (competitors beating our price)
922
+ * - Other matches: price ≥ variantPrice (competitors at or above our price)
923
+ *
924
+ * @param shownResults Output of getShownResults (already filtered to MATCH group)
925
+ * @param variantPrice The variant's current selling price
926
+ */
927
+ export declare function getUndercutResults(shownResults: (ScrapeResultsType | ImmersiveScrapeResultsType)[], variantPrice: number): (ScrapeResultsType | ImmersiveScrapeResultsType)[];
893
928
  /**
894
- * Derives the needs-review results from a MatchResult.
895
- * Includes medium-confidence candidates (reviewConfidenceMin <= confidence < minConfidence)
896
- * that are cheaper than both the variant price and the cheapest shown result.
897
- * Also includes high-confidence candidates whose visibility_state is not VISIBLE but meet
898
- * the same price conditions.
929
+ * Derives the needs-review results from sortedResults.
930
+ * Returns LOW_LIKELIHOOD results (medium confidence, no other active blocks) that are
931
+ * cheaper than both the variant price and the cheapest shown result.
899
932
  * Results are sorted by price ascending.
900
933
  */
901
- export declare function getNeedsReviewResults(matchResult: MatchResult, sortedResults: (ScrapeResultsType | ImmersiveScrapeResultsType)[], shownResults: (ScrapeResultsType | ImmersiveScrapeResultsType)[], variantPrice: number, minConfidence: number, reviewConfidenceMin: number): (ScrapeResultsType | ImmersiveScrapeResultsType)[];
934
+ export declare function getNeedsReviewResults(sortedResults: (ScrapeResultsType | ImmersiveScrapeResultsType)[], shownResults: (ScrapeResultsType | ImmersiveScrapeResultsType)[], variantPrice: number): (ScrapeResultsType | ImmersiveScrapeResultsType)[];
902
935
  export {};
package/dist/index.js CHANGED
@@ -217,6 +217,20 @@ export var VisibilityState;
217
217
  VisibilityState["VISIBLE_WITH_WARNINGS"] = "VISIBLE_WITH_WARNINGS";
218
218
  VisibilityState["VISIBLE"] = "VISIBLE";
219
219
  })(VisibilityState || (VisibilityState = {}));
220
+ /**
221
+ * Result groups that replace VisibilityState with richer classification.
222
+ * Each result is assigned a group after all filter rules run:
223
+ * MATCH — no active blocks (shown; undercut determination is a separate downstream step)
224
+ * LOW_LIKELIHOOD — medium confidence only (50–79); shown in "needs review" if cheaper
225
+ * NO_MATCH — low confidence or has an active block rule (hidden)
226
+ */
227
+ export var ResultGroup;
228
+ (function (ResultGroup) {
229
+ ResultGroup["MATCH"] = "match";
230
+ ResultGroup["LOW_LIKELIHOOD"] = "low_likelihood";
231
+ ResultGroup["NO_MATCH"] = "no_match";
232
+ ResultGroup["EXCLUDED"] = "excluded";
233
+ })(ResultGroup || (ResultGroup = {}));
220
234
  /**
221
235
  * Structured result from a filter rule evaluation
222
236
  */
@@ -264,7 +278,7 @@ export const scrapeResultsSchema = z.object({
264
278
  hide_override_reasons: z.array(z.string()),
265
279
  // New filter system fields
266
280
  filter_results: z.array(filterResultSchema).optional(),
267
- visibility_state: z.nativeEnum(VisibilityState).optional(),
281
+ visibility_state: z.nativeEnum(ResultGroup).optional(),
268
282
  regexUnitResults: z.nullable(regexUnitResultsSchema),
269
283
  // add these properties as null to make ts play nice with the ImmersiveScrapeResultsType union
270
284
  google_product_link: z.null().optional(),
@@ -335,7 +349,7 @@ export const immersiveScrapeResultsSchema = z.object({
335
349
  hide_override_reasons: z.array(z.string()),
336
350
  // New filter system fields
337
351
  filter_results: z.array(filterResultSchema).optional(),
338
- visibility_state: z.nativeEnum(VisibilityState).optional(),
352
+ visibility_state: z.nativeEnum(ResultGroup).optional(),
339
353
  brand: z.string().nullable(),
340
354
  company_id: z.number().nullable(),
341
355
  regexUnitResults: z.nullable(regexUnitResultsSchema),
@@ -439,6 +453,8 @@ export const HIDE_REASONS = {
439
453
  CALCULATED_BRAND_MISMATCH: 'Calculated Brand Mismatch',
440
454
  SCAM_SOURCE_EXCLUSION: 'Scam Source Exclusion',
441
455
  AI_COMPARISON_MISMATCH: 'AI Comparison Mismatch',
456
+ ABSOLUTE_LOW_PRICE_OUTLIER: 'Extreme Price Outlier',
457
+ ABSOLUTE_HIGH_PRICE_OUTLIER: 'Extreme High Price',
442
458
  };
443
459
  export const HIDE_OVERRIDE_REASONS = {
444
460
  SKU_MATCH: 'SKU Match',
@@ -467,6 +483,8 @@ const HIDE_ALWAYS_MAP = {
467
483
  [HIDE_REASONS.VENDOR_EXCLUSION]: false,
468
484
  [HIDE_REASONS.SCAM_SOURCE_EXCLUSION]: true,
469
485
  [HIDE_REASONS.AI_COMPARISON_MISMATCH]: false,
486
+ [HIDE_REASONS.ABSOLUTE_LOW_PRICE_OUTLIER]: true,
487
+ [HIDE_REASONS.ABSOLUTE_HIGH_PRICE_OUTLIER]: true,
470
488
  };
471
489
  export const TOO_CHEAP_MULTIPLIER = 0.75;
472
490
  export const TOO_EXPENSIVE_MULTIPLIER = 1.15;
@@ -476,11 +494,27 @@ export const TOO_EXPENSIVE_MULTIPLIER = 1.15;
476
494
  */
477
495
  export const ABSOLUTE_BLOCK_RULE_IDS = [
478
496
  'manually_ignored',
497
+ 'manually_excluded',
498
+ 'out_of_stock',
479
499
  'scam_source_exclusion',
480
500
  'duplicate',
481
501
  'high_price_outlier',
482
502
  'low_price_outlier',
483
- 'skip_sku',
503
+ 'absolute_low_price_outlier',
504
+ 'absolute_high_price_outlier',
505
+ ];
506
+ /**
507
+ * Rule IDs that represent explicit admin/security decisions.
508
+ * These are the ONLY blocks respected when a confidence override or match confirmation is present.
509
+ * Operational blocks (out_of_stock, price outliers, etc.) are superseded by overrides.
510
+ */
511
+ export const MANUAL_EXCLUSION_RULE_IDS = [
512
+ 'manually_ignored',
513
+ 'manually_excluded',
514
+ 'scam_source_exclusion',
515
+ 'duplicate',
516
+ 'absolute_low_price_outlier',
517
+ 'absolute_high_price_outlier',
484
518
  ];
485
519
  /**
486
520
  * Rule IDs that can be overridden when the matching-engine confidence is high enough.
@@ -496,19 +530,14 @@ export const CONFIDENCE_OVERRIDABLE_RULE_IDS = [
496
530
  'competitor_exclusion',
497
531
  'date_outlier',
498
532
  'critical_spec_mismatch',
533
+ 'skip_sku',
499
534
  ];
500
535
  /**
501
- * Evaluates whether a result should be surfaced given its filter state and matching confidence.
502
- *
503
- * Returns:
504
- * - `'hide'` — result must not be shown
505
- * - `'show'` — result passes all filters normally
506
- * - `'show_with_override'`— result failed soft filters but confidence is high enough to surface it anyway
507
- *
508
- * @param filterResults The filter_results array from the filter engine
509
- * @param visibilityState The visibility_state computed by the filter engine
510
- * @param confidence Matching-engine confidence score (0–100)
511
- * @param overrideThreshold Minimum confidence to override a soft BLOCK rule (default 80)
536
+ * @deprecated Confidence overrides are now handled by the filter engine.
537
+ * Pass a `confidenceMap` to `filterScrapeResultsV2` — the engine injects a synthetic
538
+ * `high_confidence_override` rule result when confidence ≥ 80, which is listed in the
539
+ * `overridableBy` array of all soft block/warning rules. `visibility_state` on each result
540
+ * is then accurate and final before `getShownResults` is called.
512
541
  */
513
542
  export function evaluateConfidenceOverride(filterResults, visibilityState, confidence, overrideThreshold = 80) {
514
543
  // Absolute blocks can never be overridden regardless of confidence
@@ -524,7 +553,6 @@ export function evaluateConfidenceOverride(filterResults, visibilityState, confi
524
553
  return 'show_with_override';
525
554
  return 'hide';
526
555
  }
527
- // const wattages = Array.from({ length: 41 }, (_, i) => (5000 + i * 500).toString())
528
556
  // ===============================
529
557
  // Filter Rule Registry
530
558
  // ===============================
@@ -611,7 +639,21 @@ export class FilterEngine {
611
639
  /**
612
640
  * Evaluate rules for a batch of results
613
641
  */
614
- evaluateBatch(results, variant, variantScrapeOptions, vendorScrapeOptions, globalScrapeOptions, comparisonMap, matchConfirmationMap) {
642
+ evaluateBatch(results, variant, variantScrapeOptions, vendorScrapeOptions, globalScrapeOptions, comparisonMap, matchConfirmationMap, matchResult) {
643
+ // Build a map of result id → best matching candidate so we can look up confidence
644
+ // and attach candidate_result to each filtered result.
645
+ const candidateMap = new Map();
646
+ for (const c of matchResult?.candidates ?? []) {
647
+ candidateMap.set(c.candidateId.toString(), c);
648
+ }
649
+ for (const c of matchResult?.insufficientDataCandidates ?? []) {
650
+ if (!candidateMap.has(c.candidateId.toString()))
651
+ candidateMap.set(c.candidateId.toString(), c);
652
+ }
653
+ for (const c of matchResult?.nonMatchCandidates ?? []) {
654
+ if (!candidateMap.has(c.candidateId.toString()))
655
+ candidateMap.set(c.candidateId.toString(), c);
656
+ }
615
657
  return results.map((result) => {
616
658
  const context = {
617
659
  result,
@@ -625,11 +667,66 @@ export class FilterEngine {
625
667
  const filterResults = this.evaluateResult(context);
626
668
  // Merge with existing filter_results from batch processing (duplicates, search exclusions, etc.)
627
669
  const existingFilterResults = result.filter_results || [];
628
- const allFilterResults = [...existingFilterResults, ...filterResults];
629
- const visibilityState = this.calculateVisibility(allFilterResults);
670
+ let allFilterResults = [...existingFilterResults, ...filterResults];
671
+ // Look up the matching-engine candidate for this result and attach it so downstream
672
+ // consumers have access to match metadata (confidence, scores, reasons, etc.).
673
+ const candidate = candidateMap.get(result.id.toString());
674
+ if (candidate)
675
+ result.candidate_result = candidate;
676
+ const confidence = candidate?.confidence;
677
+ // Inject a confidence-tier filter_result so calculateVisibility is the single source
678
+ // of truth for visibility — no confidence gate is needed in getShownResults:
679
+ // ≥ 80 → high_confidence_override (INFO, isOverride: true) — cancels soft blocks
680
+ // listed in each rule's overridableBy (e.g. ai_comparison_mismatch)
681
+ // 50–79 → medium_confidence (BLOCK) — hidden; overridable by match_confirmation_override
682
+ // < 50 or not evaluated → low_confidence (BLOCK) — hidden; overridable by match_confirmation_override
683
+ if (confidence !== undefined && confidence >= 80) {
684
+ allFilterResults = [
685
+ ...allFilterResults,
686
+ {
687
+ ruleId: 'high_confidence_override',
688
+ severity: FilterSeverity.INFO,
689
+ message: 'High Confidence Match',
690
+ metadata: { isOverride: true, confidence },
691
+ timestamp: new Date().toISOString(),
692
+ },
693
+ ];
694
+ }
695
+ else if (confidence !== undefined && confidence >= 50) {
696
+ allFilterResults = [
697
+ ...allFilterResults,
698
+ {
699
+ ruleId: 'medium_confidence',
700
+ severity: FilterSeverity.BLOCK,
701
+ message: 'Medium Confidence Match',
702
+ metadata: { confidence },
703
+ timestamp: new Date().toISOString(),
704
+ },
705
+ ];
706
+ }
707
+ else {
708
+ allFilterResults = [
709
+ ...allFilterResults,
710
+ {
711
+ ruleId: 'low_confidence',
712
+ severity: FilterSeverity.BLOCK,
713
+ message: 'Low Confidence Match',
714
+ metadata: { confidence },
715
+ timestamp: new Date().toISOString(),
716
+ },
717
+ ];
718
+ }
719
+ const resultPrice = result.extracted_total || result.extracted_price || 0;
720
+ const group = this.calculateGroup(allFilterResults, resultPrice, variant.price);
721
+ // confidence_override_active: true when high confidence cancelled a soft block on this result
722
+ const hadBlocksBefore = confidence !== undefined &&
723
+ confidence >= 80 &&
724
+ allFilterResults.some((fr) => fr.severity === FilterSeverity.BLOCK && fr.ruleId !== 'high_confidence_override');
630
725
  // Populate new fields
631
726
  result.filter_results = allFilterResults;
632
- result.visibility_state = visibilityState;
727
+ result.visibility_state = group;
728
+ result.confidence_override_active =
729
+ hadBlocksBefore && group !== ResultGroup.NO_MATCH && group !== ResultGroup.EXCLUDED ? true : undefined;
633
730
  // Maintain backward compatibility with old fields
634
731
  result.hide_reasons = allFilterResults
635
732
  .filter((fr) => fr.severity === FilterSeverity.BLOCK || fr.severity === FilterSeverity.WARNING)
@@ -637,67 +734,41 @@ export class FilterEngine {
637
734
  result.hide_override_reasons = allFilterResults
638
735
  .filter((fr) => fr.metadata?.isOverride)
639
736
  .map((fr) => fr.message);
640
- result.ignore_result = visibilityState === VisibilityState.HIDDEN;
737
+ result.ignore_result = group === ResultGroup.NO_MATCH || group === ResultGroup.EXCLUDED;
641
738
  return result;
642
739
  });
643
740
  }
644
741
  /**
645
- * Calculate visibility state based on filter results
742
+ * Calculate which result group a scrape result belongs to.
743
+ * MATCH — no manual blocks, and either an override is present or no blocks at all
744
+ * LOW_LIKELIHOOD — no overrides, medium_confidence is the only block (worth reviewing if cheaper)
745
+ * NO_MATCH — manual/security block present, or no overrides and more than one active block
746
+ *
747
+ * Key rule: any override (high_confidence_override, match_confirmation_override, sku_match, etc.)
748
+ * supersedes ALL operational blocks (out_of_stock, price outliers, mismatch rules, etc.).
749
+ * Only explicit admin decisions in MANUAL_EXCLUSION_RULE_IDS can block an overridden result.
646
750
  */
647
- calculateVisibility(filterResults) {
648
- if (filterResults.length === 0) {
649
- return VisibilityState.VISIBLE;
650
- }
651
- // Check for override results
751
+ calculateGroup(filterResults, resultPrice, variantPrice) {
652
752
  const overrideResults = filterResults.filter((fr) => fr.metadata?.isOverride);
653
753
  const blockResults = filterResults.filter((fr) => fr.severity === FilterSeverity.BLOCK);
654
- const warningResults = filterResults.filter((fr) => fr.severity === FilterSeverity.WARNING);
655
- // Block results that cannot be overridden
656
- const nonOverridableBlocks = blockResults.filter((br) => {
657
- const rule = this.registry.getRule(br.ruleId);
658
- if (!rule || !rule.canBeOverridden)
659
- return true;
660
- // Check if any override can override this block
661
- const canBeOverridden = overrideResults.some((or) => {
662
- const overrideRule = this.registry.getRule(or.ruleId);
663
- return overrideRule && rule.overridableBy.includes(overrideRule.id);
664
- });
665
- return !canBeOverridden;
666
- });
667
- if (nonOverridableBlocks.length > 0) {
668
- return VisibilityState.HIDDEN;
669
- }
670
- // Check for overridable blocks
671
- const overridableBlocks = blockResults.filter((br) => {
672
- const rule = this.registry.getRule(br.ruleId);
673
- if (!rule || !rule.canBeOverridden)
674
- return false;
675
- const canBeOverridden = overrideResults.some((or) => {
676
- const overrideRule = this.registry.getRule(or.ruleId);
677
- return overrideRule && rule.overridableBy.includes(overrideRule.id);
678
- });
679
- return !canBeOverridden; // Still blocked if no override present
680
- });
681
- if (overridableBlocks.length > 0) {
682
- return VisibilityState.HIDDEN;
683
- }
684
- // Check for warnings that haven't been overridden
685
- const nonOverriddenWarnings = warningResults.filter((wr) => {
686
- const rule = this.registry.getRule(wr.ruleId);
687
- if (!rule || !rule.canBeOverridden)
688
- return true;
689
- // Check if any override can override this warning
690
- const canBeOverridden = overrideResults.some((or) => {
691
- const overrideRule = this.registry.getRule(or.ruleId);
692
- return overrideRule && rule.overridableBy.includes(overrideRule.id);
693
- });
694
- return !canBeOverridden; // Still a warning if no override present
695
- });
696
- // If we have non-overridden warnings, show with warnings
697
- if (nonOverriddenWarnings.length > 0) {
698
- return VisibilityState.VISIBLE_WITH_WARNINGS;
754
+ // Manual and security exclusions are always respected, regardless of confidence or overrides.
755
+ const hasManualBlock = blockResults.some((br) => MANUAL_EXCLUSION_RULE_IDS.includes(br.ruleId));
756
+ if (hasManualBlock)
757
+ return ResultGroup.EXCLUDED;
758
+ // Any override (high_confidence_override, match_confirmation_override, sku_match, etc.)
759
+ // supersedes all remaining operational blocks — high-confidence / confirmed results always show.
760
+ if (overrideResults.length > 0) {
761
+ return ResultGroup.MATCH;
762
+ }
763
+ // No overrides present — with nothing to cancel them, all remaining blocks are active.
764
+ if (blockResults.length === 0) {
765
+ return ResultGroup.MATCH;
766
+ }
767
+ // Medium confidence is the only block → low likelihood (worth reviewing if cheaper)
768
+ if (blockResults.length === 1 && blockResults[0].ruleId === 'medium_confidence') {
769
+ return ResultGroup.LOW_LIKELIHOOD;
699
770
  }
700
- return VisibilityState.VISIBLE;
771
+ return ResultGroup.NO_MATCH;
701
772
  }
702
773
  }
703
774
  // ===============================
@@ -717,7 +788,7 @@ class HighPriceOutlierRule {
717
788
  overridableBy = [];
718
789
  evaluate(context) {
719
790
  const multiplier = TOO_EXPENSIVE_MULTIPLIER;
720
- const isMoreExpensive = context.result.extracted_price > context.variant.price * multiplier;
791
+ const isMoreExpensive = (context.result.extracted_total ?? context.result.extracted_price ?? 0) > context.variant.price * multiplier;
721
792
  if (isMoreExpensive) {
722
793
  return {
723
794
  ruleId: this.id,
@@ -725,7 +796,7 @@ class HighPriceOutlierRule {
725
796
  message: HIDE_REASONS.HIGH_PRICE_OUTLIER,
726
797
  metadata: {
727
798
  variantPrice: context.variant.price,
728
- resultPrice: context.result.extracted_price,
799
+ resultPrice: context.result.extracted_total ?? context.result.extracted_price ?? 0,
729
800
  threshold: multiplier,
730
801
  },
731
802
  timestamp: new Date().toISOString(),
@@ -765,6 +836,72 @@ class LowPriceOutlierRule {
765
836
  return null;
766
837
  }
767
838
  }
839
+ /**
840
+ * Rule for filtering results that are below an absolute price floor (50% of variant price).
841
+ * This rule cannot be overridden by confidence or any override — it always results in NO_MATCH.
842
+ */
843
+ class AbsoluteLowPriceOutlierRule {
844
+ id = 'absolute_low_price_outlier';
845
+ name = 'Absolute Low Price Outlier';
846
+ description = 'Filters results below 50% of the variant price — this floor cannot be overridden';
847
+ severity = FilterSeverity.BLOCK;
848
+ priority = 99;
849
+ enabled = true;
850
+ canBeOverridden = false;
851
+ overridableBy = [];
852
+ evaluate(context) {
853
+ const ABSOLUTE_CUTOFF = 0.5;
854
+ const resultPrice = context.result.extracted_total ?? context.result.extracted_price ?? 0;
855
+ const isBelowAbsoluteFloor = resultPrice > 0 && resultPrice < context.variant.price * ABSOLUTE_CUTOFF;
856
+ if (isBelowAbsoluteFloor) {
857
+ return {
858
+ ruleId: this.id,
859
+ severity: this.severity,
860
+ message: HIDE_REASONS.ABSOLUTE_LOW_PRICE_OUTLIER,
861
+ metadata: {
862
+ variantPrice: context.variant.price,
863
+ resultPrice,
864
+ threshold: ABSOLUTE_CUTOFF,
865
+ },
866
+ timestamp: new Date().toISOString(),
867
+ };
868
+ }
869
+ return null;
870
+ }
871
+ }
872
+ /**
873
+ * Rule for filtering results above an absolute price ceiling (200% of variant price).
874
+ * This rule cannot be overridden by confidence or any override — it always results in NO_MATCH.
875
+ */
876
+ class AbsoluteHighPriceOutlierRule {
877
+ id = 'absolute_high_price_outlier';
878
+ name = 'Absolute High Price Outlier';
879
+ description = 'Filters results above 200% of the variant price — this ceiling cannot be overridden';
880
+ severity = FilterSeverity.BLOCK;
881
+ priority = 99;
882
+ enabled = true;
883
+ canBeOverridden = false;
884
+ overridableBy = [];
885
+ evaluate(context) {
886
+ const ABSOLUTE_CUTOFF = 2.0;
887
+ const resultPrice = context.result.extracted_total ?? context.result.extracted_price ?? 0;
888
+ const isAbsoluteCeiling = resultPrice > 0 && resultPrice > context.variant.price * ABSOLUTE_CUTOFF;
889
+ if (isAbsoluteCeiling) {
890
+ return {
891
+ ruleId: this.id,
892
+ severity: this.severity,
893
+ message: HIDE_REASONS.ABSOLUTE_HIGH_PRICE_OUTLIER,
894
+ metadata: {
895
+ variantPrice: context.variant.price,
896
+ resultPrice,
897
+ threshold: ABSOLUTE_CUTOFF,
898
+ },
899
+ timestamp: new Date().toISOString(),
900
+ };
901
+ }
902
+ return null;
903
+ }
904
+ }
768
905
  /**
769
906
  * Rule for filtering outdated listings
770
907
  */
@@ -808,7 +945,7 @@ class CompetitorExclusionRule {
808
945
  priority = 80;
809
946
  enabled = true;
810
947
  canBeOverridden = true;
811
- overridableBy = ['match_confirmation_override'];
948
+ overridableBy = ['match_confirmation_override', 'high_confidence_override'];
812
949
  evaluate(context) {
813
950
  const lowerSource = context.result.source.toLowerCase();
814
951
  const isExcluded = context.variantScrapeOptions.competitor_exclusions.some((exclusion) => exclusion && lowerSource === exclusion.toLowerCase());
@@ -835,7 +972,13 @@ class AIComparisonMismatchRule {
835
972
  priority = 85;
836
973
  enabled = true;
837
974
  canBeOverridden = true;
838
- overridableBy = ['sku_match', 'calculated_sku_match', 'alt_sku_match', 'match_confirmation_override'];
975
+ overridableBy = [
976
+ 'sku_match',
977
+ 'calculated_sku_match',
978
+ 'alt_sku_match',
979
+ 'match_confirmation_override',
980
+ 'high_confidence_override',
981
+ ];
839
982
  evaluate(context) {
840
983
  // Only applies to results with listing_hash (primarily immersive results)
841
984
  const result = context.result;
@@ -902,7 +1045,13 @@ class SearchExclusionRule {
902
1045
  priority = 60;
903
1046
  enabled = true;
904
1047
  canBeOverridden = true;
905
- overridableBy = ['sku_match', 'calculated_sku_match', 'alt_sku_match', 'match_confirmation_override'];
1048
+ overridableBy = [
1049
+ 'sku_match',
1050
+ 'calculated_sku_match',
1051
+ 'alt_sku_match',
1052
+ 'match_confirmation_override',
1053
+ 'high_confidence_override',
1054
+ ];
906
1055
  evaluate(context) {
907
1056
  // This is handled in batch processing due to FlexSearch requirement
908
1057
  return null;
@@ -920,7 +1069,7 @@ class SkipSkuRule {
920
1069
  priority = 50;
921
1070
  enabled = true;
922
1071
  canBeOverridden = true;
923
- overridableBy = ['sku_match', 'calculated_sku_match', 'alt_sku_match'];
1072
+ overridableBy = ['sku_match', 'calculated_sku_match', 'alt_sku_match', 'high_confidence_override'];
924
1073
  evaluate(context) {
925
1074
  const variantSkuLower = context.variant.sku.toLowerCase();
926
1075
  const allSkus = [
@@ -972,7 +1121,13 @@ class VendorExclusionRule {
972
1121
  priority = 50;
973
1122
  enabled = true;
974
1123
  canBeOverridden = true;
975
- overridableBy = ['sku_match', 'calculated_sku_match', 'alt_sku_match', 'match_confirmation_override'];
1124
+ overridableBy = [
1125
+ 'sku_match',
1126
+ 'calculated_sku_match',
1127
+ 'alt_sku_match',
1128
+ 'match_confirmation_override',
1129
+ 'high_confidence_override',
1130
+ ];
976
1131
  // evaluate(context: FilterContext): FilterResult | null {
977
1132
  // const nots: string[] = []
978
1133
  // const formatted = context.vendorScrapeOptions.default_skip_vendors
@@ -1103,7 +1258,13 @@ class BrandMismatchRule {
1103
1258
  priority = 40;
1104
1259
  enabled = true;
1105
1260
  canBeOverridden = true;
1106
- overridableBy = ['sku_match', 'calculated_sku_match', 'alt_sku_match', 'match_confirmation_override'];
1261
+ overridableBy = [
1262
+ 'sku_match',
1263
+ 'calculated_sku_match',
1264
+ 'alt_sku_match',
1265
+ 'match_confirmation_override',
1266
+ 'high_confidence_override',
1267
+ ];
1107
1268
  evaluate(context) {
1108
1269
  if (!context.result.brand) {
1109
1270
  return null;
@@ -1166,7 +1327,13 @@ class CalculatedSkuMismatchRule {
1166
1327
  priority = 30;
1167
1328
  enabled = true;
1168
1329
  canBeOverridden = true;
1169
- overridableBy = ['sku_match', 'calculated_sku_match', 'alt_sku_match', 'match_confirmation_override'];
1330
+ overridableBy = [
1331
+ 'sku_match',
1332
+ 'calculated_sku_match',
1333
+ 'alt_sku_match',
1334
+ 'match_confirmation_override',
1335
+ 'high_confidence_override',
1336
+ ];
1170
1337
  evaluate(context) {
1171
1338
  const result = context.result;
1172
1339
  if (!result.calculated_sku) {
@@ -1209,7 +1376,13 @@ class AISkuMismatchRule {
1209
1376
  priority = 30;
1210
1377
  enabled = true;
1211
1378
  canBeOverridden = true;
1212
- overridableBy = ['sku_match', 'calculated_sku_match', 'alt_sku_match', 'match_confirmation_override'];
1379
+ overridableBy = [
1380
+ 'sku_match',
1381
+ 'calculated_sku_match',
1382
+ 'alt_sku_match',
1383
+ 'match_confirmation_override',
1384
+ 'high_confidence_override',
1385
+ ];
1213
1386
  evaluate(context) {
1214
1387
  const result = context.result;
1215
1388
  if (!result.ai_extracted_sku) {
@@ -1252,7 +1425,13 @@ class CriticalSpecMismatchRule {
1252
1425
  priority = 35;
1253
1426
  enabled = true;
1254
1427
  canBeOverridden = true;
1255
- overridableBy = ['sku_match', 'calculated_sku_match', 'alt_sku_match', 'match_confirmation_override'];
1428
+ overridableBy = [
1429
+ 'sku_match',
1430
+ 'calculated_sku_match',
1431
+ 'alt_sku_match',
1432
+ 'match_confirmation_override',
1433
+ 'high_confidence_override',
1434
+ ];
1256
1435
  evaluate(context) {
1257
1436
  if (!context.result.regexUnitResults || !context.variant.regexUnitResults) {
1258
1437
  return null;
@@ -1530,6 +1709,45 @@ class MatchConfirmationOverrideRule {
1530
1709
  };
1531
1710
  }
1532
1711
  }
1712
+ class LowConfidenceRule {
1713
+ id = 'low_confidence';
1714
+ name = 'Low Confidence Match';
1715
+ description = 'Result did not meet the minimum matching confidence threshold';
1716
+ severity = FilterSeverity.BLOCK;
1717
+ priority = 200;
1718
+ enabled = true;
1719
+ canBeOverridden = true;
1720
+ overridableBy = ['match_confirmation_override'];
1721
+ evaluate() {
1722
+ return null; // injected synthetically by FilterEngine.evaluateBatch
1723
+ }
1724
+ }
1725
+ class MediumConfidenceRule {
1726
+ id = 'medium_confidence';
1727
+ name = 'Medium Confidence Match';
1728
+ description = 'Result meets the review confidence threshold but not the display threshold';
1729
+ severity = FilterSeverity.BLOCK;
1730
+ priority = 200;
1731
+ enabled = true;
1732
+ canBeOverridden = true;
1733
+ overridableBy = ['match_confirmation_override'];
1734
+ evaluate() {
1735
+ return null; // injected synthetically by FilterEngine.evaluateBatch
1736
+ }
1737
+ }
1738
+ class HighConfidenceOverrideRule {
1739
+ id = 'high_confidence_override';
1740
+ name = 'High Confidence Override';
1741
+ description = 'Injected when matching-engine confidence meets the display threshold; cancels soft blocks via overridableBy';
1742
+ severity = FilterSeverity.INFO;
1743
+ priority = 200;
1744
+ enabled = true;
1745
+ canBeOverridden = false;
1746
+ overridableBy = [];
1747
+ evaluate() {
1748
+ return null; // injected synthetically by FilterEngine.evaluateBatch
1749
+ }
1750
+ }
1533
1751
  // ===============================
1534
1752
  // Registry Factory
1535
1753
  // ===============================
@@ -1541,6 +1759,8 @@ export function createDefaultFilterRegistry() {
1541
1759
  // Register all filter rules
1542
1760
  registry.registerRule(new HighPriceOutlierRule());
1543
1761
  registry.registerRule(new LowPriceOutlierRule());
1762
+ registry.registerRule(new AbsoluteLowPriceOutlierRule());
1763
+ registry.registerRule(new AbsoluteHighPriceOutlierRule());
1544
1764
  registry.registerRule(new DateOutlierRule());
1545
1765
  registry.registerRule(new CompetitorExclusionRule());
1546
1766
  registry.registerRule(new AIComparisonMismatchRule());
@@ -1563,6 +1783,10 @@ export function createDefaultFilterRegistry() {
1563
1783
  registry.registerRule(new AltSkuMatchOverrideRule());
1564
1784
  registry.registerRule(new ProductIdMatchOverrideRule());
1565
1785
  registry.registerRule(new MatchConfirmationOverrideRule());
1786
+ // Register confidence-tier rules (injected synthetically in FilterEngine.evaluateBatch)
1787
+ registry.registerRule(new LowConfidenceRule());
1788
+ registry.registerRule(new MediumConfidenceRule());
1789
+ registry.registerRule(new HighConfidenceOverrideRule());
1566
1790
  return registry;
1567
1791
  }
1568
1792
  /**
@@ -1654,7 +1878,7 @@ export function createCustomFilterRule(config) {
1654
1878
  * Filter scrape results using the new rule-based engine
1655
1879
  * This provides more flexibility and better extensibility than the legacy filterScrapeResults
1656
1880
  */
1657
- export const filterScrapeResultsV2 = ({ scrapeResults, variant, variantScrapeOptions, vendorScrapeOptions, globalScrapeOptions, filterConfig, customRules, comparisonMap, matchConfirmationMap, }) => {
1881
+ export const filterScrapeResultsV2 = ({ scrapeResults, variant, variantScrapeOptions, vendorScrapeOptions, globalScrapeOptions, filterConfig, customRules, comparisonMap, matchConfirmationMap, matchResult, }) => {
1658
1882
  if (scrapeResults.length === 0 || !scrapeResults) {
1659
1883
  return [];
1660
1884
  }
@@ -1672,7 +1896,7 @@ export const filterScrapeResultsV2 = ({ scrapeResults, variant, variantScrapeOpt
1672
1896
  let results = handleDuplicatesV2(scrapeResults);
1673
1897
  results = handleSearchExclusionsV2(results, variantScrapeOptions, variant, registry);
1674
1898
  // Evaluate all other rules
1675
- results = engine.evaluateBatch(results, variant, variantScrapeOptions, vendorScrapeOptions, globalScrapeOptions, comparisonMap, matchConfirmationMap);
1899
+ results = engine.evaluateBatch(results, variant, variantScrapeOptions, vendorScrapeOptions, globalScrapeOptions, comparisonMap, matchConfirmationMap, matchResult);
1676
1900
  return results;
1677
1901
  };
1678
1902
  /**
@@ -2266,65 +2490,50 @@ export function findUnitVariations(data) {
2266
2490
  * confidence is at or above overrideThreshold.
2267
2491
  * Results are sorted by price ascending by default; pass sortDirection to override.
2268
2492
  */
2269
- export function getShownResults(matchResult, sortedResults, minConfidence, sortDirection = 'asc', overrideThreshold = 80) {
2270
- const shownResults = [];
2271
- matchResult.candidates.forEach((candidate) => {
2272
- const foundResult = sortedResults.find((res) => res.id.toString() === candidate.candidateId.toString());
2273
- if (candidate.confidence >= minConfidence) {
2274
- if (!foundResult)
2275
- return;
2276
- foundResult.candidate_result = candidate;
2277
- const decision = evaluateConfidenceOverride(foundResult.filter_results ?? [], foundResult.visibility_state ?? VisibilityState.VISIBLE, candidate.confidence, overrideThreshold);
2278
- if (decision === 'hide')
2279
- return;
2280
- if (decision === 'show_with_override') {
2281
- foundResult.confidence_override_active = true;
2282
- }
2283
- shownResults.push(foundResult);
2284
- }
2285
- else if (foundResult &&
2286
- foundResult.filter_results?.map((fr) => fr.ruleId).includes('match_confirmation_override')) {
2287
- shownResults.push(foundResult);
2288
- }
2289
- });
2290
- shownResults.sort((a, b) => {
2493
+ export function getShownResults(sortedResults, sortDirection = 'asc') {
2494
+ return [...sortedResults]
2495
+ .filter((r) => r.visibility_state === ResultGroup.MATCH)
2496
+ .sort((a, b) => {
2291
2497
  const aPrice = a.extracted_total || a.extracted_price || 0;
2292
2498
  const bPrice = b.extracted_total || b.extracted_price || 0;
2293
2499
  return sortDirection === 'asc' ? aPrice - bPrice : bPrice - aPrice;
2294
2500
  });
2295
- return shownResults.filter((r) => r.source !== 'FactoryPure');
2296
2501
  }
2297
2502
  /**
2298
- * Derives the needs-review results from a MatchResult.
2299
- * Includes medium-confidence candidates (reviewConfidenceMin <= confidence < minConfidence)
2300
- * that are cheaper than both the variant price and the cheapest shown result.
2301
- * Also includes high-confidence candidates whose visibility_state is not VISIBLE but meet
2302
- * the same price conditions.
2503
+ * Extracts undercut results from the MATCH group — competitors selling strictly cheaper
2504
+ * than the variant price (price < variantPrice).
2505
+ *
2506
+ * This is the downstream step that divides getShownResults output into two categories:
2507
+ * - Undercuts: price < variantPrice (competitors beating our price)
2508
+ * - Other matches: price ≥ variantPrice (competitors at or above our price)
2509
+ *
2510
+ * @param shownResults Output of getShownResults (already filtered to MATCH group)
2511
+ * @param variantPrice The variant's current selling price
2512
+ */
2513
+ export function getUndercutResults(shownResults, variantPrice) {
2514
+ return shownResults.filter((r) => {
2515
+ const price = r.extracted_total || r.extracted_price || 0;
2516
+ const isOutOfStock = r.filter_results?.some((fr) => fr.ruleId === 'out_of_stock');
2517
+ return price > 0 && r.source !== 'FactoryPure' && price <= variantPrice && !isOutOfStock;
2518
+ });
2519
+ }
2520
+ /**
2521
+ * Derives the needs-review results from sortedResults.
2522
+ * Returns LOW_LIKELIHOOD results (medium confidence, no other active blocks) that are
2523
+ * cheaper than both the variant price and the cheapest shown result.
2303
2524
  * Results are sorted by price ascending.
2304
2525
  */
2305
- export function getNeedsReviewResults(matchResult, sortedResults, shownResults, variantPrice, minConfidence, reviewConfidenceMin) {
2526
+ export function getNeedsReviewResults(sortedResults, shownResults, variantPrice) {
2306
2527
  const firstValidResult = shownResults[0];
2307
2528
  const cheapestShownPrice = firstValidResult?.extracted_total ?? firstValidResult?.extracted_price ?? Infinity;
2308
- const needsReviewResults = [];
2309
- matchResult.candidates.forEach((candidate) => {
2310
- if (candidate.confidence >= reviewConfidenceMin && candidate.confidence < minConfidence) {
2311
- const foundResult = sortedResults.find((res) => res.id.toString() === candidate.candidateId.toString());
2312
- if (!foundResult)
2313
- return;
2314
- const hasAbsoluteBlock = (foundResult.filter_results ?? []).some((fr) => ABSOLUTE_BLOCK_RULE_IDS.includes(fr.ruleId));
2315
- if (hasAbsoluteBlock)
2316
- return;
2317
- const priceTotal = foundResult.extracted_total || foundResult.extracted_price || 0;
2318
- if (priceTotal < variantPrice && priceTotal < cheapestShownPrice) {
2319
- foundResult.candidate_result = candidate;
2320
- needsReviewResults.push(foundResult);
2321
- }
2322
- }
2323
- });
2324
- needsReviewResults.sort((a, b) => {
2325
- const aPrice = a.extracted_total || a.extracted_price || 0;
2326
- const bPrice = b.extracted_total || b.extracted_price || 0;
2327
- return aPrice - bPrice;
2328
- });
2329
- return needsReviewResults.filter((r) => r.source !== 'FactoryPure');
2529
+ return sortedResults
2530
+ .filter((r) => {
2531
+ if (r.visibility_state !== ResultGroup.LOW_LIKELIHOOD)
2532
+ return false;
2533
+ if (r.source === 'FactoryPure')
2534
+ return false;
2535
+ const price = r.extracted_total || r.extracted_price || 0;
2536
+ return price > 0 && r.source !== 'FactoryPure' && price <= variantPrice && price <= cheapestShownPrice;
2537
+ })
2538
+ .sort((a, b) => (a.extracted_total || a.extracted_price || 0) - (b.extracted_total || b.extracted_price || 0));
2330
2539
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@factorypure/client-helpers",
3
- "version": "1.1.29",
3
+ "version": "1.2.1",
4
4
  "description": "",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",