@factorypure/client-helpers 1.2.0 → 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 +344 -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,10 +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',
|
|
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',
|
|
483
518
|
];
|
|
484
519
|
/**
|
|
485
520
|
* Rule IDs that can be overridden when the matching-engine confidence is high enough.
|
|
@@ -498,17 +533,11 @@ export const CONFIDENCE_OVERRIDABLE_RULE_IDS = [
|
|
|
498
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
|
|
@@ -610,7 +639,21 @@ export class FilterEngine {
|
|
|
610
639
|
/**
|
|
611
640
|
* Evaluate rules for a batch of results
|
|
612
641
|
*/
|
|
613
|
-
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
|
+
}
|
|
614
657
|
return results.map((result) => {
|
|
615
658
|
const context = {
|
|
616
659
|
result,
|
|
@@ -624,11 +667,66 @@ export class FilterEngine {
|
|
|
624
667
|
const filterResults = this.evaluateResult(context);
|
|
625
668
|
// Merge with existing filter_results from batch processing (duplicates, search exclusions, etc.)
|
|
626
669
|
const existingFilterResults = result.filter_results || [];
|
|
627
|
-
|
|
628
|
-
|
|
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');
|
|
629
725
|
// Populate new fields
|
|
630
726
|
result.filter_results = allFilterResults;
|
|
631
|
-
result.visibility_state =
|
|
727
|
+
result.visibility_state = group;
|
|
728
|
+
result.confidence_override_active =
|
|
729
|
+
hadBlocksBefore && group !== ResultGroup.NO_MATCH && group !== ResultGroup.EXCLUDED ? true : undefined;
|
|
632
730
|
// Maintain backward compatibility with old fields
|
|
633
731
|
result.hide_reasons = allFilterResults
|
|
634
732
|
.filter((fr) => fr.severity === FilterSeverity.BLOCK || fr.severity === FilterSeverity.WARNING)
|
|
@@ -636,67 +734,41 @@ export class FilterEngine {
|
|
|
636
734
|
result.hide_override_reasons = allFilterResults
|
|
637
735
|
.filter((fr) => fr.metadata?.isOverride)
|
|
638
736
|
.map((fr) => fr.message);
|
|
639
|
-
result.ignore_result =
|
|
737
|
+
result.ignore_result = group === ResultGroup.NO_MATCH || group === ResultGroup.EXCLUDED;
|
|
640
738
|
return result;
|
|
641
739
|
});
|
|
642
740
|
}
|
|
643
741
|
/**
|
|
644
|
-
* 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.
|
|
645
750
|
*/
|
|
646
|
-
|
|
647
|
-
if (filterResults.length === 0) {
|
|
648
|
-
return VisibilityState.VISIBLE;
|
|
649
|
-
}
|
|
650
|
-
// Check for override results
|
|
751
|
+
calculateGroup(filterResults, resultPrice, variantPrice) {
|
|
651
752
|
const overrideResults = filterResults.filter((fr) => fr.metadata?.isOverride);
|
|
652
753
|
const blockResults = filterResults.filter((fr) => fr.severity === FilterSeverity.BLOCK);
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
return
|
|
665
|
-
});
|
|
666
|
-
if (nonOverridableBlocks.length > 0) {
|
|
667
|
-
return VisibilityState.HIDDEN;
|
|
668
|
-
}
|
|
669
|
-
// Check for overridable blocks
|
|
670
|
-
const overridableBlocks = blockResults.filter((br) => {
|
|
671
|
-
const rule = this.registry.getRule(br.ruleId);
|
|
672
|
-
if (!rule || !rule.canBeOverridden)
|
|
673
|
-
return false;
|
|
674
|
-
const canBeOverridden = overrideResults.some((or) => {
|
|
675
|
-
const overrideRule = this.registry.getRule(or.ruleId);
|
|
676
|
-
return overrideRule && rule.overridableBy.includes(overrideRule.id);
|
|
677
|
-
});
|
|
678
|
-
return !canBeOverridden; // Still blocked if no override present
|
|
679
|
-
});
|
|
680
|
-
if (overridableBlocks.length > 0) {
|
|
681
|
-
return VisibilityState.HIDDEN;
|
|
682
|
-
}
|
|
683
|
-
// Check for warnings that haven't been overridden
|
|
684
|
-
const nonOverriddenWarnings = warningResults.filter((wr) => {
|
|
685
|
-
const rule = this.registry.getRule(wr.ruleId);
|
|
686
|
-
if (!rule || !rule.canBeOverridden)
|
|
687
|
-
return true;
|
|
688
|
-
// Check if any override can override this warning
|
|
689
|
-
const canBeOverridden = overrideResults.some((or) => {
|
|
690
|
-
const overrideRule = this.registry.getRule(or.ruleId);
|
|
691
|
-
return overrideRule && rule.overridableBy.includes(overrideRule.id);
|
|
692
|
-
});
|
|
693
|
-
return !canBeOverridden; // Still a warning if no override present
|
|
694
|
-
});
|
|
695
|
-
// If we have non-overridden warnings, show with warnings
|
|
696
|
-
if (nonOverriddenWarnings.length > 0) {
|
|
697
|
-
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;
|
|
698
766
|
}
|
|
699
|
-
|
|
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;
|
|
770
|
+
}
|
|
771
|
+
return ResultGroup.NO_MATCH;
|
|
700
772
|
}
|
|
701
773
|
}
|
|
702
774
|
// ===============================
|
|
@@ -716,7 +788,7 @@ class HighPriceOutlierRule {
|
|
|
716
788
|
overridableBy = [];
|
|
717
789
|
evaluate(context) {
|
|
718
790
|
const multiplier = TOO_EXPENSIVE_MULTIPLIER;
|
|
719
|
-
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;
|
|
720
792
|
if (isMoreExpensive) {
|
|
721
793
|
return {
|
|
722
794
|
ruleId: this.id,
|
|
@@ -724,7 +796,7 @@ class HighPriceOutlierRule {
|
|
|
724
796
|
message: HIDE_REASONS.HIGH_PRICE_OUTLIER,
|
|
725
797
|
metadata: {
|
|
726
798
|
variantPrice: context.variant.price,
|
|
727
|
-
resultPrice: context.result.extracted_price,
|
|
799
|
+
resultPrice: context.result.extracted_total ?? context.result.extracted_price ?? 0,
|
|
728
800
|
threshold: multiplier,
|
|
729
801
|
},
|
|
730
802
|
timestamp: new Date().toISOString(),
|
|
@@ -764,6 +836,72 @@ class LowPriceOutlierRule {
|
|
|
764
836
|
return null;
|
|
765
837
|
}
|
|
766
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
|
+
}
|
|
767
905
|
/**
|
|
768
906
|
* Rule for filtering outdated listings
|
|
769
907
|
*/
|
|
@@ -807,7 +945,7 @@ class CompetitorExclusionRule {
|
|
|
807
945
|
priority = 80;
|
|
808
946
|
enabled = true;
|
|
809
947
|
canBeOverridden = true;
|
|
810
|
-
overridableBy = ['match_confirmation_override'];
|
|
948
|
+
overridableBy = ['match_confirmation_override', 'high_confidence_override'];
|
|
811
949
|
evaluate(context) {
|
|
812
950
|
const lowerSource = context.result.source.toLowerCase();
|
|
813
951
|
const isExcluded = context.variantScrapeOptions.competitor_exclusions.some((exclusion) => exclusion && lowerSource === exclusion.toLowerCase());
|
|
@@ -834,7 +972,13 @@ class AIComparisonMismatchRule {
|
|
|
834
972
|
priority = 85;
|
|
835
973
|
enabled = true;
|
|
836
974
|
canBeOverridden = true;
|
|
837
|
-
overridableBy = [
|
|
975
|
+
overridableBy = [
|
|
976
|
+
'sku_match',
|
|
977
|
+
'calculated_sku_match',
|
|
978
|
+
'alt_sku_match',
|
|
979
|
+
'match_confirmation_override',
|
|
980
|
+
'high_confidence_override',
|
|
981
|
+
];
|
|
838
982
|
evaluate(context) {
|
|
839
983
|
// Only applies to results with listing_hash (primarily immersive results)
|
|
840
984
|
const result = context.result;
|
|
@@ -901,7 +1045,13 @@ class SearchExclusionRule {
|
|
|
901
1045
|
priority = 60;
|
|
902
1046
|
enabled = true;
|
|
903
1047
|
canBeOverridden = true;
|
|
904
|
-
overridableBy = [
|
|
1048
|
+
overridableBy = [
|
|
1049
|
+
'sku_match',
|
|
1050
|
+
'calculated_sku_match',
|
|
1051
|
+
'alt_sku_match',
|
|
1052
|
+
'match_confirmation_override',
|
|
1053
|
+
'high_confidence_override',
|
|
1054
|
+
];
|
|
905
1055
|
evaluate(context) {
|
|
906
1056
|
// This is handled in batch processing due to FlexSearch requirement
|
|
907
1057
|
return null;
|
|
@@ -919,7 +1069,7 @@ class SkipSkuRule {
|
|
|
919
1069
|
priority = 50;
|
|
920
1070
|
enabled = true;
|
|
921
1071
|
canBeOverridden = true;
|
|
922
|
-
overridableBy = ['sku_match', 'calculated_sku_match', 'alt_sku_match'];
|
|
1072
|
+
overridableBy = ['sku_match', 'calculated_sku_match', 'alt_sku_match', 'high_confidence_override'];
|
|
923
1073
|
evaluate(context) {
|
|
924
1074
|
const variantSkuLower = context.variant.sku.toLowerCase();
|
|
925
1075
|
const allSkus = [
|
|
@@ -971,7 +1121,13 @@ class VendorExclusionRule {
|
|
|
971
1121
|
priority = 50;
|
|
972
1122
|
enabled = true;
|
|
973
1123
|
canBeOverridden = true;
|
|
974
|
-
overridableBy = [
|
|
1124
|
+
overridableBy = [
|
|
1125
|
+
'sku_match',
|
|
1126
|
+
'calculated_sku_match',
|
|
1127
|
+
'alt_sku_match',
|
|
1128
|
+
'match_confirmation_override',
|
|
1129
|
+
'high_confidence_override',
|
|
1130
|
+
];
|
|
975
1131
|
// evaluate(context: FilterContext): FilterResult | null {
|
|
976
1132
|
// const nots: string[] = []
|
|
977
1133
|
// const formatted = context.vendorScrapeOptions.default_skip_vendors
|
|
@@ -1102,7 +1258,13 @@ class BrandMismatchRule {
|
|
|
1102
1258
|
priority = 40;
|
|
1103
1259
|
enabled = true;
|
|
1104
1260
|
canBeOverridden = true;
|
|
1105
|
-
overridableBy = [
|
|
1261
|
+
overridableBy = [
|
|
1262
|
+
'sku_match',
|
|
1263
|
+
'calculated_sku_match',
|
|
1264
|
+
'alt_sku_match',
|
|
1265
|
+
'match_confirmation_override',
|
|
1266
|
+
'high_confidence_override',
|
|
1267
|
+
];
|
|
1106
1268
|
evaluate(context) {
|
|
1107
1269
|
if (!context.result.brand) {
|
|
1108
1270
|
return null;
|
|
@@ -1165,7 +1327,13 @@ class CalculatedSkuMismatchRule {
|
|
|
1165
1327
|
priority = 30;
|
|
1166
1328
|
enabled = true;
|
|
1167
1329
|
canBeOverridden = true;
|
|
1168
|
-
overridableBy = [
|
|
1330
|
+
overridableBy = [
|
|
1331
|
+
'sku_match',
|
|
1332
|
+
'calculated_sku_match',
|
|
1333
|
+
'alt_sku_match',
|
|
1334
|
+
'match_confirmation_override',
|
|
1335
|
+
'high_confidence_override',
|
|
1336
|
+
];
|
|
1169
1337
|
evaluate(context) {
|
|
1170
1338
|
const result = context.result;
|
|
1171
1339
|
if (!result.calculated_sku) {
|
|
@@ -1208,7 +1376,13 @@ class AISkuMismatchRule {
|
|
|
1208
1376
|
priority = 30;
|
|
1209
1377
|
enabled = true;
|
|
1210
1378
|
canBeOverridden = true;
|
|
1211
|
-
overridableBy = [
|
|
1379
|
+
overridableBy = [
|
|
1380
|
+
'sku_match',
|
|
1381
|
+
'calculated_sku_match',
|
|
1382
|
+
'alt_sku_match',
|
|
1383
|
+
'match_confirmation_override',
|
|
1384
|
+
'high_confidence_override',
|
|
1385
|
+
];
|
|
1212
1386
|
evaluate(context) {
|
|
1213
1387
|
const result = context.result;
|
|
1214
1388
|
if (!result.ai_extracted_sku) {
|
|
@@ -1251,7 +1425,13 @@ class CriticalSpecMismatchRule {
|
|
|
1251
1425
|
priority = 35;
|
|
1252
1426
|
enabled = true;
|
|
1253
1427
|
canBeOverridden = true;
|
|
1254
|
-
overridableBy = [
|
|
1428
|
+
overridableBy = [
|
|
1429
|
+
'sku_match',
|
|
1430
|
+
'calculated_sku_match',
|
|
1431
|
+
'alt_sku_match',
|
|
1432
|
+
'match_confirmation_override',
|
|
1433
|
+
'high_confidence_override',
|
|
1434
|
+
];
|
|
1255
1435
|
evaluate(context) {
|
|
1256
1436
|
if (!context.result.regexUnitResults || !context.variant.regexUnitResults) {
|
|
1257
1437
|
return null;
|
|
@@ -1529,6 +1709,45 @@ class MatchConfirmationOverrideRule {
|
|
|
1529
1709
|
};
|
|
1530
1710
|
}
|
|
1531
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
|
+
}
|
|
1532
1751
|
// ===============================
|
|
1533
1752
|
// Registry Factory
|
|
1534
1753
|
// ===============================
|
|
@@ -1540,6 +1759,8 @@ export function createDefaultFilterRegistry() {
|
|
|
1540
1759
|
// Register all filter rules
|
|
1541
1760
|
registry.registerRule(new HighPriceOutlierRule());
|
|
1542
1761
|
registry.registerRule(new LowPriceOutlierRule());
|
|
1762
|
+
registry.registerRule(new AbsoluteLowPriceOutlierRule());
|
|
1763
|
+
registry.registerRule(new AbsoluteHighPriceOutlierRule());
|
|
1543
1764
|
registry.registerRule(new DateOutlierRule());
|
|
1544
1765
|
registry.registerRule(new CompetitorExclusionRule());
|
|
1545
1766
|
registry.registerRule(new AIComparisonMismatchRule());
|
|
@@ -1562,6 +1783,10 @@ export function createDefaultFilterRegistry() {
|
|
|
1562
1783
|
registry.registerRule(new AltSkuMatchOverrideRule());
|
|
1563
1784
|
registry.registerRule(new ProductIdMatchOverrideRule());
|
|
1564
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());
|
|
1565
1790
|
return registry;
|
|
1566
1791
|
}
|
|
1567
1792
|
/**
|
|
@@ -1653,7 +1878,7 @@ export function createCustomFilterRule(config) {
|
|
|
1653
1878
|
* Filter scrape results using the new rule-based engine
|
|
1654
1879
|
* This provides more flexibility and better extensibility than the legacy filterScrapeResults
|
|
1655
1880
|
*/
|
|
1656
|
-
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, }) => {
|
|
1657
1882
|
if (scrapeResults.length === 0 || !scrapeResults) {
|
|
1658
1883
|
return [];
|
|
1659
1884
|
}
|
|
@@ -1671,7 +1896,7 @@ export const filterScrapeResultsV2 = ({ scrapeResults, variant, variantScrapeOpt
|
|
|
1671
1896
|
let results = handleDuplicatesV2(scrapeResults);
|
|
1672
1897
|
results = handleSearchExclusionsV2(results, variantScrapeOptions, variant, registry);
|
|
1673
1898
|
// Evaluate all other rules
|
|
1674
|
-
results = engine.evaluateBatch(results, variant, variantScrapeOptions, vendorScrapeOptions, globalScrapeOptions, comparisonMap, matchConfirmationMap);
|
|
1899
|
+
results = engine.evaluateBatch(results, variant, variantScrapeOptions, vendorScrapeOptions, globalScrapeOptions, comparisonMap, matchConfirmationMap, matchResult);
|
|
1675
1900
|
return results;
|
|
1676
1901
|
};
|
|
1677
1902
|
/**
|
|
@@ -2265,67 +2490,50 @@ export function findUnitVariations(data) {
|
|
|
2265
2490
|
* confidence is at or above overrideThreshold.
|
|
2266
2491
|
* Results are sorted by price ascending by default; pass sortDirection to override.
|
|
2267
2492
|
*/
|
|
2268
|
-
export function getShownResults(
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
if (candidate.confidence >= minConfidence) {
|
|
2273
|
-
if (!foundResult)
|
|
2274
|
-
return;
|
|
2275
|
-
foundResult.candidate_result = candidate;
|
|
2276
|
-
const decision = evaluateConfidenceOverride(foundResult.filter_results ?? [], foundResult.visibility_state ?? VisibilityState.VISIBLE, candidate.confidence, overrideThreshold);
|
|
2277
|
-
if (decision === 'hide')
|
|
2278
|
-
return;
|
|
2279
|
-
if (decision === 'show_with_override') {
|
|
2280
|
-
foundResult.confidence_override_active = true;
|
|
2281
|
-
}
|
|
2282
|
-
shownResults.push(foundResult);
|
|
2283
|
-
}
|
|
2284
|
-
else if (foundResult &&
|
|
2285
|
-
foundResult.filter_results?.some((fr) => fr?.metadata?.isOverride === true)
|
|
2286
|
-
// foundResult.filter_results?.map((fr: FilterResult) => fr.ruleId).includes('match_confirmation_override')
|
|
2287
|
-
) {
|
|
2288
|
-
shownResults.push(foundResult);
|
|
2289
|
-
}
|
|
2290
|
-
});
|
|
2291
|
-
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) => {
|
|
2292
2497
|
const aPrice = a.extracted_total || a.extracted_price || 0;
|
|
2293
2498
|
const bPrice = b.extracted_total || b.extracted_price || 0;
|
|
2294
2499
|
return sortDirection === 'asc' ? aPrice - bPrice : bPrice - aPrice;
|
|
2295
2500
|
});
|
|
2296
|
-
return shownResults.filter((r) => r.source !== 'FactoryPure');
|
|
2297
2501
|
}
|
|
2298
2502
|
/**
|
|
2299
|
-
*
|
|
2300
|
-
*
|
|
2301
|
-
*
|
|
2302
|
-
*
|
|
2303
|
-
*
|
|
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.
|
|
2304
2524
|
* Results are sorted by price ascending.
|
|
2305
2525
|
*/
|
|
2306
|
-
export function getNeedsReviewResults(
|
|
2526
|
+
export function getNeedsReviewResults(sortedResults, shownResults, variantPrice) {
|
|
2307
2527
|
const firstValidResult = shownResults[0];
|
|
2308
2528
|
const cheapestShownPrice = firstValidResult?.extracted_total ?? firstValidResult?.extracted_price ?? Infinity;
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
if (
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
if (priceTotal < variantPrice && priceTotal < cheapestShownPrice) {
|
|
2320
|
-
foundResult.candidate_result = candidate;
|
|
2321
|
-
needsReviewResults.push(foundResult);
|
|
2322
|
-
}
|
|
2323
|
-
}
|
|
2324
|
-
});
|
|
2325
|
-
needsReviewResults.sort((a, b) => {
|
|
2326
|
-
const aPrice = a.extracted_total || a.extracted_price || 0;
|
|
2327
|
-
const bPrice = b.extracted_total || b.extracted_price || 0;
|
|
2328
|
-
return aPrice - bPrice;
|
|
2329
|
-
});
|
|
2330
|
-
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));
|
|
2331
2539
|
}
|