Skip to main content

Pricing & Rules Engine

Centralized rule evaluation for visibility and pricing across visitor and member flows. The RulesEngine mirrors the original C# DigiWedge.TeeTimes.Rules.RulesEngine for parity.

Architecture

Core Files:

FilePurpose
rules-engine.tsMain rule evaluation logic
rules-engine.helpers.tsUtility functions
types/rules-engine.types.tsType definitions
dynamic-price.tsDynamic pricing resolution
price-resolver.service.tsMulti-source price resolver (MCA/SCL)
special-rate-rule.service.tsPromotional pricing management
policy/visibility-pricing.policy.tsVisibility gate helpers

Rule Types

TypeValuePurpose
Rate0Standard pricing rules
Special1Promotional/special pricing
Exclusion2Block/hide tee times

Rule Structure

CourseRateRule (Visitor Rules)

interface CourseRateRule {
id: number;
name: string;
ruleType: 'Rate' | 'Special' | 'Exclusion';
active: boolean;
order: number; // higher = higher priority
billingItemId?: number; // SCL integration

// Date/time ranges (separate for 9/18 holes)
startDate: Date;
endDate: Date;
startDate9: Date;
endDate9: Date;
startTime: string; // HH:mm format
endTime: string;

// Rates (in cents)
rate: number;
rate9Holes: number;
publicRate: number; // public holiday
publicRate9Holes: number;

// Day-of-week with visibility windows
days: RuleDay[]; // {day: DayOfWeek, visibleBeforeHours: number}

// Ball count filters
appliesTo1Ball?: boolean;
appliesTo2Ball?: boolean;
appliesTo3Ball?: boolean;
appliesTo4Ball?: boolean;

// Visibility
visibleForVisitors?: boolean;
visibleForPhoneBookings?: boolean;

// Eligibility
gender?: string; // "M", "F", or both
minimumAge?: number;
maximumAge?: number;

// Dynamic pricing
dynamicClasses?: { dynamicClassId: number }[];

// Per-tee visibility
tees?: TeeVisibility[];

// Special-specific
specialLabel?: string;
specialDescription?: string;
}

CourseMemberRule (Member Rules)

Similar to CourseRateRule, with additional:

interface CourseMemberRule extends CourseRateRule {
golferClassifications: GolferClassificationRef[];
statuses: MemberStatusRef[]; // e.g., "FULL", "JUNIOR"
visibleForMembers?: boolean;
}

Rule Evaluation Flow

Sheet-Level (Tee Sheet Date)

For Visitors:

RulesEngine.getVisitorRateRules(
allRateRules,
sheetDate,
isPublicHoliday,
nineHoles,
dayOfWeek,
numPlayers
);

Filters applied in order:

  1. Active rules with visibleForVisitors !== false
  2. Date range check (18 or 9-hole specific)
  3. Holiday fallback (if holiday, use applyToPublicHoliday rules)
  4. Ball count filter (appliesTo1Ball, appliesTo2Ball, etc.)
  5. Sort by order (descending = highest priority first)

For Members:

RulesEngine.getMemberRateRules(
allMemberRules,
sheetDate,
isPublicHoliday,
nineHoles,
dayOfWeek
);

Filters applied:

  1. Active rules with visibleForMembers !== false AND visibleForPhoneBookings !== false
  2. Date range check
  3. Holiday fallback
  4. Sort by order (descending)

Tee-Time Level Selection

For Visitors:

RulesEngine.getVisitorTeeTimeRate(
possibleRules,
teeTime,
player?,
nineHoles,
isPublicHoliday
);

Additional filters:

  1. Final date window check
  2. Time range validation per rule
  3. Visibility window check: visibleBeforeHours gates when slot becomes visible
  4. Gender filter: Checks player gender against rule's gender field
  5. Age filter: Checks player age against minimumAge/maximumAge
  6. Return highest-priority rule (max order)

For Members:

RulesEngine.getMemberTeeTimeRate(
possibleRules,
teeTime,
membership,
nineHoles,
golferClassification,
isPublicHoliday
);

Additional filters:

  1. Final date window check
  2. Time range validation
  3. Visibility window check
  4. Member status filter: Match golferClassifications or statuses
  5. Return highest-priority rule

Eligibility Matrix

CheckVisitorMemberFields Used
Activerule.active
VisibilityvisibleForVisitorsvisibleForMembers + visibleForPhoneBookings
Date RangestartDate/endDate or startDate9/endDate9
Time RangestartTime/endTime
Day of Weekdays[].day
Public HolidayapplyToPublicHoliday
Visibility Windowdays[].visibleBeforeHours
Ball CountappliesTo1Ball/2Ball/3Ball/4Ball
Gendergender field
AgeminimumAge/maximumAge
Membership Statusstatuses[].clubMemberStatus.statusCode
Golfer ClassificationgolferClassifications[].classificationCode

Dynamic Pricing

Resolution Flow

  1. Apply the highest-priority matching rule → get base rate or rate9Holes
  2. Check if dynamic pricing enabled on the club (dynamicPricingMode)
  3. If rule has dynamicClasses:
    • Load dynamic classes by ID
    • Query dynamic price for each class until one returns a non-zero price
    • Use that dynamic price instead of static rate
  4. Return final price

Price Resolver Service

Supports dual-source pricing:

// Resolution modes
'off' // No dynamic pricing, return static rule rate
'scl' // Use SCL (price schedules) only
'mca' // Use MCA (dynamic classes) only
'dual' // Both (SCL preferred if available)

SCL Integration: Queries PriceScheduleRepository via billingItemId MCA Integration: Queries via dynamic classes and PricingService

Special Rules (Promotions)

Special rules enable promotional pricing with:

  • Marketing labels: specialLabel, specialDescription
  • Separate date/time ranges from standard rates
  • All same eligibility checks (gender, age, ball count)
  • Can have dynamic pricing linked

Special Detection

RulesEngine.findMatchingSpecial(
possibleRules,
teeTime,
player?,
nineHoles,
isPublicHoliday
);
  • Filters to only ruleType === 'Special'
  • Returns first matching special or null
  • Used for enrichment: marks tee times as "special" even if not the winning rule

Visibility Policy Gates

VisibilityPricingPolicy.canApplyVisitorRule(rule);
// Returns: rule.visibleForVisitors !== false

VisibilityPricingPolicy.canApplyMemberRule(rule);
// Returns: rule.visibleForMembers !== false &&
// rule.visibleForPhoneBookings !== false

VisibilityPricingPolicy.membersPay(club);
// Returns: club.clubConfig.enableMemberTeeTimePayments

Admin API for Specials

EndpointMethodDescription
/admin/specialsGETList specials (with filtering)
/admin/specialsPOSTCreate a new special
/admin/specials/:idPUTUpdate an existing special
/admin/specials/:idDELETEDelete a special
/admin/specials/:id/togglePUTToggle active state
/admin/specials/course/:courseId/activeGETGet active specials for date

Public Holiday Behavior

  • Sheet-level rule filtering considers public holidays
  • A helper isPublicHoliday(repo, course, date) returns holiday status
  • When holiday, rules with applyToPublicHoliday=true apply
  • Pricing uses publicRate/publicRate9Holes when applicable

Helper Functions

FunctionPurpose
addDaysEndOfDay()Snap date to 23:59:59.999
numericDowToString()Convert JS day (0-6) to name
filterByHolidayOrFallback()Apply holiday rules or fallback
filterByBallCount()Filter rules by player count
getTimeRangeForRule()Parse time strings to Date
computeAgeFromDob()Calculate age from DOB

Testing

Test suites cover:

  • Visitor gender/age/visibility scenarios
  • Member status/classification matching
  • Dynamic pricing resolution
  • Duplicate suppression
  • Hole count preference matching

Location: libs/tee-time-services/src/lib/__tests__/rules-engine/