@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.
- package/dist/index.d.ts +57 -24
- package/dist/index.js +345 -136
- 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
|
|
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
|
|
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
|
-
*
|
|
729
|
-
*
|
|
730
|
-
*
|
|
731
|
-
*
|
|
732
|
-
*
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
895
|
-
*
|
|
896
|
-
*
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
'
|
|
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
|
-
*
|
|
502
|
-
*
|
|
503
|
-
*
|
|
504
|
-
*
|
|
505
|
-
*
|
|
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
|
-
|
|
629
|
-
|
|
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 =
|
|
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 =
|
|
737
|
+
result.ignore_result = group === ResultGroup.NO_MATCH || group === ResultGroup.EXCLUDED;
|
|
641
738
|
return result;
|
|
642
739
|
});
|
|
643
740
|
}
|
|
644
741
|
/**
|
|
645
|
-
* Calculate
|
|
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
|
-
|
|
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
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
return
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
|
|
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
|
|
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 = [
|
|
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 = [
|
|
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 = [
|
|
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 = [
|
|
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 = [
|
|
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 = [
|
|
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 = [
|
|
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(
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
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
|
-
*
|
|
2299
|
-
*
|
|
2300
|
-
*
|
|
2301
|
-
*
|
|
2302
|
-
*
|
|
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(
|
|
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
|
-
|
|
2309
|
-
|
|
2310
|
-
if (
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
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
|
}
|