692 lines
21 KiB
TypeScript
692 lines
21 KiB
TypeScript
import { Injectable, Logger } from '@nestjs/common';
|
||
import { InjectRepository } from '@nestjs/typeorm';
|
||
import { Repository } from 'typeorm';
|
||
import { PolicyRule } from '../entities/policy-rule.entity';
|
||
import { RuleCondition } from '../entities/rule-condition.entity';
|
||
import { LogicalOperator } from '../entities/policy.enums';
|
||
import { ConditionField } from '../../master-data/entities/condition-field.entity';
|
||
import { Operator } from '../../master-data/entities/operator.entity';
|
||
import { IncidentContext } from './master-value-resolver.service';
|
||
|
||
export interface ConditionDiagnostic {
|
||
field: string;
|
||
actual: any;
|
||
expected: any;
|
||
operator: string;
|
||
matched: boolean;
|
||
reason: string;
|
||
}
|
||
|
||
export interface RuleEvaluationResult {
|
||
ruleId: string;
|
||
category?: string;
|
||
priority: number;
|
||
matched: boolean;
|
||
conditions: ConditionDiagnostic[];
|
||
reason?: string;
|
||
}
|
||
|
||
@Injectable()
|
||
export class RuleEvaluationService {
|
||
private readonly logger = new Logger(RuleEvaluationService.name);
|
||
|
||
constructor(
|
||
@InjectRepository(ConditionField)
|
||
private readonly fieldRepo: Repository<ConditionField>,
|
||
@InjectRepository(Operator)
|
||
private readonly operatorRepo: Repository<Operator>,
|
||
) {}
|
||
|
||
async evaluateRule(
|
||
rule: PolicyRule,
|
||
ctx: IncidentContext,
|
||
): Promise<RuleEvaluationResult> {
|
||
this.logger.log(`--- Evaluating Rule ID: ${rule.id} (Priority: ${rule.priority}) ---`);
|
||
|
||
const fields = await this.fieldRepo.find();
|
||
const operators = await this.operatorRepo.find();
|
||
|
||
const sortedConditions = (rule.conditions || []).sort(
|
||
(a, b) => (a.sequence || 0) - (b.sequence || 0),
|
||
);
|
||
|
||
if (sortedConditions.length === 0) {
|
||
this.logger.warn(
|
||
`>>> [RULE NOT MATCHED ❌] Rule ID: ${rule.id} (Priority: ${rule.priority}) | Reason: No conditions configured for rule.`,
|
||
);
|
||
return {
|
||
ruleId: rule.id,
|
||
category: rule.ruleCategoryId,
|
||
priority: rule.priority || 1,
|
||
matched: false,
|
||
conditions: [],
|
||
reason: 'Rule has no configured conditions or evaluation criteria.',
|
||
};
|
||
}
|
||
|
||
const diagnostics: ConditionDiagnostic[] = [];
|
||
let isRuleMatched = true;
|
||
|
||
for (let i = 0; i < sortedConditions.length; i++) {
|
||
const cond = sortedConditions[i];
|
||
const fieldDef = fields.find((f) => f.id === cond.fieldId);
|
||
const opDef = operators.find((o) => o.id === cond.operatorId);
|
||
|
||
const fieldCode = (fieldDef?.code || 'unknown').toLowerCase();
|
||
const opCode = (opDef?.code || opDef?.symbol || 'equals').toLowerCase();
|
||
|
||
const actualValue = this.extractActualValue(fieldCode, ctx);
|
||
const expectedValue = cond.valueText;
|
||
|
||
const { matched: isCondMatched, reason } = this.evaluateConditionWithReason(
|
||
actualValue,
|
||
expectedValue,
|
||
opCode,
|
||
fieldCode,
|
||
);
|
||
|
||
if (isCondMatched) {
|
||
this.logger.log(
|
||
` [CONDITION MATCHED ✅] Field: "${fieldCode}" | Operator: "${opCode}" | Actual: ${JSON.stringify(actualValue)} | Expected: ${JSON.stringify(expectedValue)} | REASON: ${reason}`,
|
||
);
|
||
} else {
|
||
this.logger.warn(
|
||
` [CONDITION FAILED ❌] Field: "${fieldCode}" | Operator: "${opCode}" | Actual: ${JSON.stringify(actualValue)} | Expected: ${JSON.stringify(expectedValue)} | REASON: ${reason}`,
|
||
);
|
||
}
|
||
|
||
diagnostics.push({
|
||
field: fieldCode,
|
||
actual: actualValue,
|
||
expected: expectedValue,
|
||
operator: opCode,
|
||
matched: isCondMatched,
|
||
reason,
|
||
});
|
||
|
||
const logicalOp = cond.logicalOperator || LogicalOperator.AND;
|
||
if (i === 0) {
|
||
isRuleMatched = isCondMatched;
|
||
} else {
|
||
if (logicalOp === LogicalOperator.OR) {
|
||
isRuleMatched = isRuleMatched || isCondMatched;
|
||
} else {
|
||
isRuleMatched = isRuleMatched && isCondMatched;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (isRuleMatched) {
|
||
this.logger.log(
|
||
`>>> [RULE MATCHED ✅] Rule ID: ${rule.id} (Priority: ${rule.priority}) | Actions Configured: ${rule.actions?.length || 0}`,
|
||
);
|
||
} else {
|
||
const failedConds = diagnostics.filter((d) => !d.matched);
|
||
this.logger.warn(
|
||
`>>> [RULE NOT MATCHED ❌] Rule ID: ${rule.id} (Priority: ${rule.priority}) | REASON: Failed condition(s): [${failedConds
|
||
.map((c) => `Field "${c.field}": ${c.reason}`)
|
||
.join('; ')}]`,
|
||
);
|
||
}
|
||
|
||
return {
|
||
ruleId: rule.id,
|
||
category: rule.ruleCategoryId,
|
||
priority: rule.priority || 1,
|
||
matched: isRuleMatched,
|
||
conditions: diagnostics,
|
||
reason: isRuleMatched
|
||
? 'All required conditions matched successfully.'
|
||
: `Failed condition(s): ${diagnostics.filter((d) => !d.matched).map((d) => `${d.field} (${d.reason})`).join(', ')}`,
|
||
};
|
||
}
|
||
|
||
private normalizeOperatorCode(opCode: string): string {
|
||
const raw = (opCode || '').toLowerCase().trim();
|
||
if (!raw) return 'eq';
|
||
|
||
if (
|
||
raw === '=' ||
|
||
raw === '==' ||
|
||
raw === '===' ||
|
||
raw === 'eq' ||
|
||
raw === 'equal' ||
|
||
raw === 'equals' ||
|
||
raw === 'is' ||
|
||
raw === 'same'
|
||
) {
|
||
return 'eq';
|
||
}
|
||
if (
|
||
raw === '!=' ||
|
||
raw === '!==' ||
|
||
raw === '<>' ||
|
||
raw === 'ne' ||
|
||
raw === 'neq' ||
|
||
raw === 'not_equal' ||
|
||
raw === 'not_equals' ||
|
||
raw === 'not equal' ||
|
||
raw === 'not equals' ||
|
||
raw === 'is_not' ||
|
||
raw === 'is not'
|
||
) {
|
||
return 'neq';
|
||
}
|
||
if (
|
||
raw === '>' ||
|
||
raw === 'gt' ||
|
||
raw === 'greater_than' ||
|
||
raw === 'greater than' ||
|
||
raw === 'more_than' ||
|
||
raw === 'more than' ||
|
||
raw === 'above'
|
||
) {
|
||
return 'gt';
|
||
}
|
||
if (
|
||
raw === '>=' ||
|
||
raw === 'gte' ||
|
||
raw === 'greater_than_or_equal' ||
|
||
raw === 'greater than or equal' ||
|
||
raw === 'greater_than_or_equals' ||
|
||
raw === 'greater than or equals' ||
|
||
raw === 'at_least' ||
|
||
raw === 'at least' ||
|
||
raw === 'min' ||
|
||
raw === 'minimum'
|
||
) {
|
||
return 'gte';
|
||
}
|
||
if (
|
||
raw === '<' ||
|
||
raw === 'lt' ||
|
||
raw === 'less_than' ||
|
||
raw === 'less than' ||
|
||
raw === 'under' ||
|
||
raw === 'below' ||
|
||
raw === 'fewer_than' ||
|
||
raw === 'fewer than'
|
||
) {
|
||
return 'lt';
|
||
}
|
||
if (
|
||
raw === '<=' ||
|
||
raw === 'lte' ||
|
||
raw === 'less_than_or_equal' ||
|
||
raw === 'less than or equal' ||
|
||
raw === 'less_than_or_equals' ||
|
||
raw === 'less than or equals' ||
|
||
raw === 'at_most' ||
|
||
raw === 'at most' ||
|
||
raw === 'max' ||
|
||
raw === 'maximum'
|
||
) {
|
||
return 'lte';
|
||
}
|
||
if (
|
||
raw === 'contains' ||
|
||
raw === 'contain' ||
|
||
raw === 'includes' ||
|
||
raw === 'include' ||
|
||
raw === 'like' ||
|
||
raw === 'ilike' ||
|
||
raw === 'has' ||
|
||
raw === 'have'
|
||
) {
|
||
return 'contains';
|
||
}
|
||
if (
|
||
raw === 'not_contains' ||
|
||
raw === 'not contains' ||
|
||
raw === 'does_not_contain' ||
|
||
raw === 'does not contain' ||
|
||
raw === 'not_includes' ||
|
||
raw === 'not includes' ||
|
||
raw === 'excludes' ||
|
||
raw === 'exclude' ||
|
||
raw === 'without' ||
|
||
raw === 'not_like' ||
|
||
raw === 'not like'
|
||
) {
|
||
return 'not_contains';
|
||
}
|
||
if (
|
||
raw === 'in' ||
|
||
raw === 'inside' ||
|
||
raw === 'any_of' ||
|
||
raw === 'any of' ||
|
||
raw === 'one_of' ||
|
||
raw === 'one of'
|
||
) {
|
||
return 'in';
|
||
}
|
||
if (
|
||
raw === 'not_in' ||
|
||
raw === 'not in' ||
|
||
raw === 'not_inside' ||
|
||
raw === 'not inside' ||
|
||
raw === 'none_of' ||
|
||
raw === 'none of'
|
||
) {
|
||
return 'not_in';
|
||
}
|
||
if (
|
||
raw === 'starts_with' ||
|
||
raw === 'starts with' ||
|
||
raw === 'startswith' ||
|
||
raw === 'beginning_with'
|
||
) {
|
||
return 'starts_with';
|
||
}
|
||
if (
|
||
raw === 'ends_with' ||
|
||
raw === 'ends with' ||
|
||
raw === 'endswith' ||
|
||
raw === 'ending_with'
|
||
) {
|
||
return 'ends_with';
|
||
}
|
||
if (
|
||
raw === 'is_empty' ||
|
||
raw === 'is empty' ||
|
||
raw === 'empty' ||
|
||
raw === 'is_null' ||
|
||
raw === 'is null' ||
|
||
raw === 'null'
|
||
) {
|
||
return 'is_empty';
|
||
}
|
||
if (
|
||
raw === 'is_not_empty' ||
|
||
raw === 'is not empty' ||
|
||
raw === 'not_empty' ||
|
||
raw === 'not empty' ||
|
||
raw === 'is_not_null' ||
|
||
raw === 'is not null' ||
|
||
raw === 'not_null' ||
|
||
raw === 'not null'
|
||
) {
|
||
return 'is_not_empty';
|
||
}
|
||
|
||
return raw;
|
||
}
|
||
|
||
private evaluateConditionWithReason(
|
||
actual: any,
|
||
expected: any,
|
||
opCode: string,
|
||
fieldCode: string,
|
||
): { matched: boolean; reason: string } {
|
||
const normalizedOp = this.normalizeOperatorCode(opCode);
|
||
|
||
if (normalizedOp === 'is_empty') {
|
||
const isEmpty = actual === undefined || actual === null || String(actual).trim() === '';
|
||
return {
|
||
matched: isEmpty,
|
||
reason: isEmpty
|
||
? `Field "${fieldCode}" is empty or null.`
|
||
: `Field "${fieldCode}" has value "${actual}" and is NOT empty.`,
|
||
};
|
||
}
|
||
if (normalizedOp === 'is_not_empty') {
|
||
const isNotEmpty = actual !== undefined && actual !== null && String(actual).trim() !== '';
|
||
return {
|
||
matched: isNotEmpty,
|
||
reason: isNotEmpty
|
||
? `Field "${fieldCode}" has value "${actual}" and is not empty.`
|
||
: `Field "${fieldCode}" is empty or null.`,
|
||
};
|
||
}
|
||
|
||
if (actual === undefined || actual === null) {
|
||
return {
|
||
matched: false,
|
||
reason: `Incident context property "${fieldCode}" is missing, null, or undefined in the disruption payload.`,
|
||
};
|
||
}
|
||
|
||
const matched = this.compareValues(actual, expected, opCode);
|
||
|
||
if (matched) {
|
||
return {
|
||
matched: true,
|
||
reason: `Actual value "${actual}" satisfies operator "${opCode}" against expected value "${expected}".`,
|
||
};
|
||
} else {
|
||
const actStr = String(actual);
|
||
const expStr = String(expected || '');
|
||
const actNum = Number(actual);
|
||
const expNum = Number(expected);
|
||
const isNumeric = !isNaN(actNum) && !isNaN(expNum) && expStr !== '';
|
||
|
||
if (normalizedOp === 'neq') {
|
||
return {
|
||
matched: false,
|
||
reason: `Actual value "${actStr}" IS EQUAL to expected value "${expStr}", failing "not equal" operator "${opCode}".`,
|
||
};
|
||
} else if (isNumeric) {
|
||
return {
|
||
matched: false,
|
||
reason: `Actual numeric value (${actNum}) DOES NOT satisfy operator "${opCode}" against expected required numeric value (${expNum}).`,
|
||
};
|
||
} else {
|
||
return {
|
||
matched: false,
|
||
reason: `Actual text value "${actStr}" DOES NOT match expected value "${expStr}" under operator "${opCode}".`,
|
||
};
|
||
}
|
||
}
|
||
}
|
||
|
||
private extractActualValue(fieldCode: string, ctx: IncidentContext): any {
|
||
const fc = (fieldCode || '').toLowerCase().trim();
|
||
const raw = ctx.rawIncident || {};
|
||
|
||
switch (fc) {
|
||
// FLIGHT
|
||
case 'flight_type':
|
||
case 'flighttype':
|
||
return (
|
||
(raw as any).flightType ||
|
||
(ctx.origin && ctx.destination
|
||
? ctx.origin === ctx.destination
|
||
? 'domestic'
|
||
: 'international'
|
||
: undefined)
|
||
);
|
||
case 'origin':
|
||
case 'origin_airport':
|
||
case 'originairport':
|
||
return ctx.origin || (raw as any).origin;
|
||
case 'destination':
|
||
case 'destination_airport':
|
||
case 'destinationairport':
|
||
case 'airport':
|
||
return ctx.destination || (raw as any).destination;
|
||
case 'operating_carrier':
|
||
case 'operatingcarrier':
|
||
return (raw as any).operatingCarrier || (raw as any).carrier;
|
||
case 'marketing_carrier':
|
||
case 'marketingcarrier':
|
||
return (raw as any).marketingCarrier || (raw as any).carrier;
|
||
case 'flight_distance':
|
||
case 'flightdistance':
|
||
return (raw as any).flightDistance !== undefined
|
||
? Number((raw as any).flightDistance)
|
||
: undefined;
|
||
case 'flight_number':
|
||
case 'flightnumber':
|
||
return ctx.flightNumber || (raw as any).flightNumber;
|
||
|
||
// DISRUPTION
|
||
case 'delay_duration':
|
||
case 'delayduration':
|
||
case 'delay':
|
||
return ctx.delayDuration !== undefined ? ctx.delayDuration : (raw as any).delayDuration;
|
||
case 'delay_reason':
|
||
case 'delayreason':
|
||
return (raw as any).delayReason || (raw as any).reason;
|
||
case 'cancellation_reason':
|
||
case 'cancellationreason':
|
||
return (raw as any).cancellationReason || (raw as any).reason;
|
||
case 'diversion_reason':
|
||
case 'diversionreason':
|
||
return (raw as any).diversionReason || (raw as any).reason;
|
||
case 'weather_condition':
|
||
case 'weathercondition':
|
||
return (raw as any).weatherCondition;
|
||
case 'atc_restriction':
|
||
case 'atcrestriction':
|
||
return (raw as any).atcRestriction;
|
||
case 'technical_fault':
|
||
case 'technicalfault':
|
||
return (raw as any).technicalFault;
|
||
case 'extraordinary_circumstance':
|
||
case 'extraordinarycircumstance':
|
||
return (raw as any).extraordinaryCircumstance;
|
||
case 'airline_responsibility':
|
||
case 'airlineresponsibility':
|
||
return (raw as any).airlineResponsibility;
|
||
|
||
// PASSENGER
|
||
case 'passenger_type':
|
||
case 'passengertype':
|
||
return ctx.passengerTypeId || ctx.passengerTypeName || (raw as any).passengerType;
|
||
case 'cabin_class':
|
||
case 'cabinclass':
|
||
return ctx.cabinClassId || ctx.cabinClassName || (raw as any).cabinClass;
|
||
case 'membership_tier':
|
||
case 'membershiptier':
|
||
case 'loyalty_tier':
|
||
case 'loyaltytier':
|
||
return ctx.loyaltyTierId || ctx.loyaltyTierName || (raw as any).loyaltyTier;
|
||
case 'customer_value':
|
||
case 'customervalue':
|
||
return (raw as any).customerValue;
|
||
case 'corporate_customer':
|
||
case 'corporatecustomer':
|
||
return (raw as any).corporateCustomer;
|
||
case 'group_booking':
|
||
case 'groupbooking':
|
||
return (raw as any).groupBooking;
|
||
case 'special_assistance':
|
||
case 'specialassistance':
|
||
return ctx.specialAssistance || (raw as any).specialAssistance;
|
||
|
||
// JOURNEY
|
||
case 'journey_type':
|
||
case 'journeytype':
|
||
return (raw as any).journeyType;
|
||
case 'protected_connection':
|
||
case 'protectedconnection':
|
||
return (raw as any).protectedConnection;
|
||
case 'self_transfer':
|
||
case 'selftransfer':
|
||
return (raw as any).selfTransfer;
|
||
case 'number_of_segments':
|
||
case 'numberofsegments':
|
||
return (raw as any).numberOfSegments !== undefined
|
||
? Number((raw as any).numberOfSegments)
|
||
: undefined;
|
||
case 'final_destination_delay':
|
||
case 'finaldestinationdelay':
|
||
return (raw as any).finalDestinationDelay !== undefined
|
||
? Number((raw as any).finalDestinationDelay)
|
||
: ctx.delayDuration;
|
||
|
||
// BOOKING
|
||
case 'booking_channel':
|
||
case 'bookingchannel':
|
||
return (raw as any).bookingChannel;
|
||
case 'refundable_ticket':
|
||
case 'refundableticket':
|
||
return (raw as any).refundableTicket;
|
||
case 'fare_flexibility':
|
||
case 'fareflexibility':
|
||
return (raw as any).fareFlexibility;
|
||
case 'ticket_value':
|
||
case 'ticketvalue':
|
||
return (raw as any).ticketValue !== undefined
|
||
? Number((raw as any).ticketValue)
|
||
: (raw as any).value;
|
||
case 'ancillary_purchased':
|
||
case 'ancillarypurchased':
|
||
return (raw as any).ancillaryPurchased;
|
||
|
||
// BAGGAGE
|
||
case 'bag_status':
|
||
case 'bagstatus':
|
||
return (raw as any).bagStatus;
|
||
case 'bag_type':
|
||
case 'bagtype':
|
||
return (raw as any).bagType;
|
||
case 'bag_delay':
|
||
case 'bagdelay':
|
||
return (raw as any).bagDelay !== undefined ? Number((raw as any).bagDelay) : undefined;
|
||
case 'bag_value':
|
||
case 'bagvalue':
|
||
return (raw as any).bagValue !== undefined ? Number((raw as any).bagValue) : undefined;
|
||
case 'pir_created':
|
||
case 'pircreated':
|
||
return (raw as any).pirCreated;
|
||
|
||
// CABIN
|
||
case 'original_cabin':
|
||
case 'originalcabin':
|
||
return ctx.originalCabinId || ctx.originalCabinName || (raw as any).originalCabin;
|
||
case 'assigned_cabin':
|
||
case 'assignedcabin':
|
||
case 'actual_cabin':
|
||
case 'actualcabin':
|
||
return ctx.actualCabinId || ctx.actualCabinName || (raw as any).actualCabin;
|
||
case 'downgrade_level':
|
||
case 'downgradelevel':
|
||
return (raw as any).downgradeLevel !== undefined
|
||
? Number((raw as any).downgradeLevel)
|
||
: undefined;
|
||
case 'seat_type':
|
||
case 'seattype':
|
||
return (raw as any).seatType;
|
||
|
||
// ANCILLARY
|
||
case 'ancillary_type':
|
||
case 'ancillarytype':
|
||
return (raw as any).ancillaryType;
|
||
case 'ancillary_delivered':
|
||
case 'ancillarydelivered':
|
||
return (raw as any).ancillaryDelivered;
|
||
case 'service_value':
|
||
case 'servicevalue':
|
||
return (raw as any).serviceValue !== undefined
|
||
? Number((raw as any).serviceValue)
|
||
: undefined;
|
||
|
||
// GENERAL / COMMON
|
||
case 'scenario':
|
||
return ctx.scenario || (raw as any).scenario;
|
||
case 'category':
|
||
return ctx.category || (raw as any).category;
|
||
case 'jurisdiction':
|
||
return ctx.jurisdictionId || ctx.jurisdictionCode || (raw as any).jurisdiction;
|
||
case 'nationality':
|
||
return ctx.nationality || (raw as any).nationality;
|
||
|
||
default:
|
||
const camelCase = fc.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
||
return (
|
||
(ctx as any)[fc] ??
|
||
(ctx as any)[camelCase] ??
|
||
(raw as any)[fc] ??
|
||
(raw as any)[camelCase]
|
||
);
|
||
}
|
||
}
|
||
|
||
private compareValues(actual: any, expected: any, rawOpCode: string): boolean {
|
||
const op = this.normalizeOperatorCode(rawOpCode);
|
||
|
||
if (op === 'is_empty') {
|
||
return actual === undefined || actual === null || String(actual).trim() === '';
|
||
}
|
||
if (op === 'is_not_empty') {
|
||
return actual !== undefined && actual !== null && String(actual).trim() !== '';
|
||
}
|
||
|
||
if (actual === undefined || actual === null) return false;
|
||
|
||
const actStr = String(actual).toLowerCase().trim();
|
||
const expStr = String(expected || '').toLowerCase().trim();
|
||
|
||
// Check if numeric comparison
|
||
const actNum = Number(actual);
|
||
const expNum = Number(expected);
|
||
const isNumeric = !isNaN(actNum) && !isNaN(expNum) && expStr !== '';
|
||
|
||
if (isNumeric) {
|
||
switch (op) {
|
||
case 'eq':
|
||
return actNum === expNum;
|
||
case 'neq':
|
||
return actNum !== expNum;
|
||
case 'gt':
|
||
return actNum > expNum;
|
||
case 'gte':
|
||
return actNum >= expNum;
|
||
case 'lt':
|
||
return actNum < expNum;
|
||
case 'lte':
|
||
return actNum <= expNum;
|
||
case 'in':
|
||
const numInList = expStr.split(',').map((s) => Number(s.trim())).filter((n) => !isNaN(n));
|
||
return numInList.includes(actNum);
|
||
case 'not_in':
|
||
const numNotInList = expStr.split(',').map((s) => Number(s.trim())).filter((n) => !isNaN(n));
|
||
return !numNotInList.includes(actNum);
|
||
default:
|
||
return actNum === expNum;
|
||
}
|
||
}
|
||
|
||
// Smart String Matcher for Airport Codes & Master Data Dropdown Labels (e.g. "JFK" vs "JFK - New York JFK")
|
||
const isStringMatch = (a: string, e: string): boolean => {
|
||
if (!a && !e) return true;
|
||
if (!a || !e) return false;
|
||
if (a === e) return true;
|
||
if (a.includes(e) || e.includes(a)) return true;
|
||
|
||
// Extract IATA / token prefix before dash or space (e.g. "JFK - New York JFK" -> "jfk")
|
||
const eTokens = e.split(/[\s\-–_]+/);
|
||
const aTokens = a.split(/[\s\-–_]+/);
|
||
|
||
if (eTokens.some((t) => t && t.length >= 2 && a.includes(t))) return true;
|
||
if (aTokens.some((t) => t && t.length >= 2 && e.includes(t))) return true;
|
||
|
||
return false;
|
||
};
|
||
|
||
switch (op) {
|
||
case 'eq':
|
||
return isStringMatch(actStr, expStr);
|
||
|
||
case 'neq':
|
||
return !isStringMatch(actStr, expStr);
|
||
|
||
case 'gt':
|
||
return actStr > expStr;
|
||
|
||
case 'gte':
|
||
return actStr >= expStr;
|
||
|
||
case 'lt':
|
||
return actStr < expStr;
|
||
|
||
case 'lte':
|
||
return actStr <= expStr;
|
||
|
||
case 'contains':
|
||
return isStringMatch(actStr, expStr);
|
||
|
||
case 'not_contains':
|
||
return !isStringMatch(actStr, expStr);
|
||
|
||
case 'starts_with':
|
||
return actStr.startsWith(expStr);
|
||
|
||
case 'ends_with':
|
||
return actStr.endsWith(expStr);
|
||
|
||
case 'in':
|
||
const inList = expStr.split(',').map((s) => s.trim());
|
||
return inList.some((item) => isStringMatch(actStr, item));
|
||
|
||
case 'not_in':
|
||
const notInList = expStr.split(',').map((s) => s.trim());
|
||
return !notInList.some((item) => isStringMatch(actStr, item));
|
||
|
||
default:
|
||
return isStringMatch(actStr, expStr);
|
||
}
|
||
}
|
||
}
|