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:
| File | Purpose |
|---|---|
rules-engine.ts | Main rule evaluation logic |
rules-engine.helpers.ts | Utility functions |
types/rules-engine.types.ts | Type definitions |
dynamic-price.ts | Dynamic pricing resolution |
price-resolver.service.ts | Multi-source price resolver (MCA/SCL) |
special-rate-rule.service.ts | Promotional pricing management |
policy/visibility-pricing.policy.ts | Visibility gate helpers |
Rule Types
| Type | Value | Purpose |
|---|---|---|
| Rate | 0 | Standard pricing rules |
| Special | 1 | Promotional/special pricing |
| Exclusion | 2 | Block/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:
- Active rules with
visibleForVisitors !== false - Date range check (18 or 9-hole specific)
- Holiday fallback (if holiday, use
applyToPublicHolidayrules) - Ball count filter (
appliesTo1Ball,appliesTo2Ball, etc.) - Sort by
order(descending = highest priority first)
For Members:
RulesEngine.getMemberRateRules(
allMemberRules,
sheetDate,
isPublicHoliday,
nineHoles,
dayOfWeek
);
Filters applied:
- Active rules with
visibleForMembers !== falseANDvisibleForPhoneBookings !== false - Date range check
- Holiday fallback
- Sort by order (descending)
Tee-Time Level Selection
For Visitors:
RulesEngine.getVisitorTeeTimeRate(
possibleRules,
teeTime,
player?,
nineHoles,
isPublicHoliday
);
Additional filters:
- Final date window check
- Time range validation per rule
- Visibility window check:
visibleBeforeHoursgates when slot becomes visible - Gender filter: Checks player gender against rule's
genderfield - Age filter: Checks player age against
minimumAge/maximumAge - Return highest-priority rule (max
order)
For Members:
RulesEngine.getMemberTeeTimeRate(
possibleRules,
teeTime,
membership,
nineHoles,
golferClassification,
isPublicHoliday
);
Additional filters:
- Final date window check
- Time range validation
- Visibility window check
- Member status filter: Match
golferClassificationsorstatuses - Return highest-priority rule
Eligibility Matrix
| Check | Visitor | Member | Fields Used |
|---|---|---|---|
| Active | ✓ | ✓ | rule.active |
| Visibility | visibleForVisitors | visibleForMembers + visibleForPhoneBookings | |
| Date Range | ✓ | ✓ | startDate/endDate or startDate9/endDate9 |
| Time Range | ✓ | ✓ | startTime/endTime |
| Day of Week | ✓ | ✓ | days[].day |
| Public Holiday | ✓ | ✓ | applyToPublicHoliday |
| Visibility Window | ✓ | ✓ | days[].visibleBeforeHours |
| Ball Count | ✓ | ✗ | appliesTo1Ball/2Ball/3Ball/4Ball |
| Gender | ✓ | ✗ | gender field |
| Age | ✓ | ✗ | minimumAge/maximumAge |
| Membership Status | ✗ | ✓ | statuses[].clubMemberStatus.statusCode |
| Golfer Classification | ✗ | ✓ | golferClassifications[].classificationCode |
Dynamic Pricing
Resolution Flow
- Apply the highest-priority matching rule → get base
rateorrate9Holes - Check if dynamic pricing enabled on the club (
dynamicPricingMode) - 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
- 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
| Endpoint | Method | Description |
|---|---|---|
/admin/specials | GET | List specials (with filtering) |
/admin/specials | POST | Create a new special |
/admin/specials/:id | PUT | Update an existing special |
/admin/specials/:id | DELETE | Delete a special |
/admin/specials/:id/toggle | PUT | Toggle active state |
/admin/specials/course/:courseId/active | GET | Get 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=trueapply - Pricing uses
publicRate/publicRate9Holeswhen applicable
Helper Functions
| Function | Purpose |
|---|---|
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/