Skip to main content

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/

FilePurpose
search-engine.service.tsMain service (phone-booking compliant)
multi-club-search.service.tsMulti-club search with SSE streaming
club-pricing.service.tsClub pricing wrapper
visitor-availability.service.tsVisitor availability checking
caching.policy.tsTee sheet caching and fetching logic
tee-time-scheduler.tsConflict-free tee time suggestions
tee-mapping.strategies.tsTee time mapping and transformation
filters/Date and member rule filters
metrics/Capacity metrics tracking

Key Methods

Primary Search Methods

MethodPurpose
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:

  1. Fetch tee sheet (with optional enrichment)
  2. Apply visibility & pricing rules (RulesEngine)
  3. Sort and filter by capacity + cutoff time
  4. Duplicate suppression (consecutive empty slots)
  5. HideTee filtering (per-tee visibility rules)
  6. Access policy evaluation (eligibility/roles)
  7. Slot status mapping (AVAILABLE, BOOKED, BLOCKED, HELD)
  8. Dynamic pricing resolution (MCA/SCL)
  9. Specials enrichment check
  10. 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

  1. Config Resolution: resolveInternalConfig() maps the MCA course ID to the internal teetime DB UUID via CourseRepository.findById(), then fetches the course tee-sheet config.

  2. Config Lookup: TeeSheetService.getCourseTeeSheetConfig() uses an overlapping date window query:

    WHERE courseId = ? AND startDate <= ? AND endDate >= ?
    ORDER BY startDate DESC

    This finds the most specific config covering the requested date.

  3. Enforcement: Applied in both getVisitorTeeTimes() and getMemberTeeTimes() before any other filtering.

Settings Enforced

SettingBehavior
Booking ChannelsIf bookingChannels.online === false, returns empty result with offlineReason
Operating DaysIf date's weekday not in operatingDays, returns empty with offlineReason
Operating HoursfilterTeeTimesByWindow() filters tee times to startTimeendTime range
Number of TeesMulti-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?: {...};
}

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

ModeBehavior
'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:

  1. Slot array with capacity/available properties
  2. availableSpots numeric fallback
  3. 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, eligibilityPrice on results

Metrics & Observability

MetricDescription
searchLatencyTiming for visitor/member flows
searchRequestsCounter by kind (visitor/member)
policyDeniedAccess policy denials by reason
tee_capacity_path_totalCapacity evaluation method tracking
tee_capacity_valueCapacity 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