Tee-time Search Engine
The SearchEngineService is the core component for tee-time discovery, providing visitor and member flows with visibility rules, pricing resolution, duplicate suppression, and a 5-minute tee-sheet freshness window.
Architecture
Location: libs/tee-time-services/src/lib/search/
| File | Purpose |
|---|---|
search-engine.service.ts | Main service (phone-booking compliant) |
multi-club-search.service.ts | Multi-club search with SSE streaming |
club-pricing.service.ts | Club pricing wrapper |
visitor-availability.service.ts | Visitor availability checking |
caching.policy.ts | Tee sheet caching and fetching logic |
tee-time-scheduler.ts | Conflict-free tee time suggestions |
tee-mapping.strategies.ts | Tee time mapping and transformation |
filters/ | Date and member rule filters |
metrics/ | Capacity metrics tracking |
Key Methods
Primary Search Methods
| Method | Purpose |
|---|---|
getTeeTimes() | Unified entry point for visitor/member tee times |
getVisitorTeeTimes() | Fetch visitor-accessible tee times with pricing |
getMemberTeeTimes() | Fetch member-accessible tee times |
suggestTeeTimes() | Return conflict-free tee time subset |
getTeeSheet() | Raw tee sheet retrieval with optional enrichment |
getClubPricing() | Get club pricing in MCA format |
Visitor Flow Example
const result = await searchEngine.getVisitorTeeTimes(
course, // Course entity
date, // Date for tee sheet
nineHoles, // true for 9-hole rounds
numPlayers, // Party size (1-4)
undefined, // rateOverride
false, // isAdmin
true, // filterOutPast
false, // showPlayers
eligibilityContext, // Optional eligibility
player // Optional player for age/gender filters
);
Tee Time Filtering Chain
The visitor flow applies filters in this order:
- Fetch tee sheet (with optional enrichment)
- Apply visibility & pricing rules (RulesEngine)
- Sort and filter by capacity + cutoff time
- Duplicate suppression (consecutive empty slots)
- HideTee filtering (per-tee visibility rules)
- Access policy evaluation (eligibility/roles)
- Slot status mapping (AVAILABLE, BOOKED, BLOCKED, HELD)
- Dynamic pricing resolution (MCA/SCL)
- Specials enrichment check
- Return filtered TeeSheetResult
Integration Points
SearchEngineService
├── TeeSheetBridgeService (fetch raw data)
├── PriceResolverService (dynamic pricing)
│ ├── MCA PricingService
│ ├── MCA DynamicClassService
│ └── SCL PriceScheduleRepository
├── TeeTimeEnrichmentService (weather/specials)
├── AccessPolicyService (role-based filtering)
├── EligibilityService (member validation)
├── TeeSheetService (course settings)
└── RulesEngine (static rule matching)
Course Settings Enforcement
The search engine enforces course-level settings configured in the tee-sheet admin. This ensures public booking flows respect operational constraints.
How It Works
-
Config Resolution:
resolveInternalConfig()maps the MCA course ID to the internal teetime DB UUID viaCourseRepository.findById(), then fetches the course tee-sheet config. -
Config Lookup:
TeeSheetService.getCourseTeeSheetConfig()uses an overlapping date window query:WHERE courseId = ? AND startDate <= ? AND endDate >= ?
ORDER BY startDate DESCThis finds the most specific config covering the requested date.
-
Enforcement: Applied in both
getVisitorTeeTimes()andgetMemberTeeTimes()before any other filtering.
Settings Enforced
| Setting | Behavior |
|---|---|
| Booking Channels | If bookingChannels.online === false, returns empty result with offlineReason |
| Operating Days | If date's weekday not in operatingDays, returns empty with offlineReason |
| Operating Hours | filterTeeTimesByWindow() filters tee times to startTime–endTime range |
| Number of Tees | Multi-club search filters tee <= numberOfTees |
Admin Bypass
The isAdmin parameter bypasses the online channel check only:
if (!onlineEnabled && !isAdmin) {
return { ...out, offlineReason: 'Online bookings disabled' };
}
Operating days, hours, and tee limits are always enforced.
Config Shape
interface CourseTeeSheetConfig {
startTime?: string; // "06:00"
endTime?: string; // "18:00"
numberOfTees?: number; // 1 or 2
bookingChannels: {
online: boolean;
phone: boolean;
walkIn: boolean;
};
operatingDays: string[]; // ["monday", "tuesday", ...]
offlineReason?: string;
intervalMinutes: number;
crossoverBreak: number;
weatherThresholds?: {...};
}
Multi-Club Search
MultiClubSearchService also enforces settings via applyInternalConfig():
// In getManyClubs() and streamManyClubs()
const internal = await this.resolveInternalConfig(code, date);
const filtered = this.applyInternalConfig(sheet, internal.config, internal.tz);
This ensures multi-club availability searches respect per-course settings.
Configuration
Club-Level Settings
interface ClubConfig {
enableDynamicPricing: boolean; // Activate SCL or MCA pricing
enableMemberTeeTimePayments: boolean; // Members pay vs. pay at club
dynamicPricingMode?: 'mca' | 'scl' | 'dual'; // Pricing source
}
Pricing Source Modes
| Mode | Behavior |
|---|---|
'off' | Static rule-based pricing only |
'mca' | MCA dynamic class-based pricing |
'scl' | SCL schedule-based pricing |
'dual' | Both (SCL preferred if available) |
Caching Policy
- Tee sheets cached in memory per course
- 5-minute TTL (freshAgeMs >= 5 * 60_000)
- Falls back to cached if bridge fetch fails
- Force refresh with
fetchNew=true
Multi-Club Search
For searching across multiple clubs simultaneously:
// Parallel fetch, returns all results after completion
const results = await multiClubSearch.getManyClubs(clubs, date, preferences);
// SSE streaming, emits as each club completes
const stream$ = multiClubSearch.streamManyClubs(clubs, date, preferences);
// Full pricing via SearchEngineService (concurrency=4)
const pricedStream$ = multiClubSearch.streamManyClubsWithPricing(
clubs, date, preferences, eligibilityContext, player
);
Concurrency Settings:
- Raw sheets: concurrency=8
- Pricing: concurrency=4
Result Format
interface TeeSheetResult {
date: Date;
clubTz: string; // Club timezone
clubcode: string;
courseCode: string;
isNineHoles: boolean;
isMember: boolean;
isPublicHoliday: boolean;
teeTimes: TeeTimeResult[];
intervalMinutes: number;
crossoverBreak: number;
offlineReason?: string; // If sheet unavailable
}
Capacity Rules
Filter rules by ball count:
appliesTo1Ball,appliesTo2Ball,appliesTo3Ball,appliesTo4Ball
Capacity sourced from:
- Slot array with capacity/available properties
availableSpotsnumeric fallback- Filtered out if no capacity info
Eligibility & Access Control
- Role Computation: VISITOR → MEMBER/RECIPROCAL/CORPORATE via EligibilityService
- Access Decisions: Blocked by club blackout windows, member-only windows, visitor disabled policy
- Per-Slot Eligibility: Stamps
canBook,denyReason,eligibilityPriceon results
Metrics & Observability
| Metric | Description |
|---|---|
searchLatency | Timing for visitor/member flows |
searchRequests | Counter by kind (visitor/member) |
policyDenied | Access policy denials by reason |
tee_capacity_path_total | Capacity evaluation method tracking |
tee_capacity_value | Capacity values observed |
Error Handling
- TeeSheetBridgeError: Bridge fetch failures (uses cache if available)
- Graceful degradation: Returns cached sheet on provider errors
- Logging: Capacity rejections, eligibility failures, bridge errors