Skip to main content

Rate Rules Admin API

The rate rules admin system provides comprehensive pricing management with CRUD operations, conflict detection, and pricing preview.

Architecture

┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐
│ Admin API │────▶│ RateRulesAdmin │────▶│ RateRule │
│ /admin/rules │ │ Controller │ │ Repository │
└─────────────────┘ └──────────────────┘ └─────────────────┘


┌──────────────────┐
│ Conflict │
│ Detection │
└──────────────────┘

Source: apps/teetime/teetime-backend/src/admin/course-rate-rules-admin.controller.ts

Rule Types

TypeValueDescription
Rate0Standard visitor pricing
Special1Promotional pricing (with label/description)
Exclusion2Block/exclude tee times

Rule Structure

interface RateRule {
id: string;
courseId: string;
name: string;
ruleType: 0 | 1 | 2;
active: boolean;
order: number; // Priority (higher wins)

// Pricing
rate: number; // 18-hole price
rate9Holes: number; // 9-hole price
cartRate?: number; // Cart price
cartRate9Holes?: number; // 9-hole cart price
publicRate?: number; // Public holiday price
publicRate9Holes?: number; // 9-hole holiday price

// Date/Time Filters
startDate: string; // ISO date
endDate: string;
startTime?: string; // HH:mm
endTime?: string;

// Ball Count Filters
appliesTo1Ball: boolean;
appliesTo2Ball: boolean;
appliesTo3Ball: boolean;
appliesTo4Ball: boolean;

// Special Pricing (ruleType=1)
specialLabel?: string;
specialDescription?: string;

// Options
applyToPublicHoliday: boolean;
visibleForPhoneBookings: boolean;
includeCart: boolean;

// Nested Relations
ruleDays: RuleDay[];
ruleTees: RuleTee[];
ruleAssociations: RuleAssociation[];
rateClubs: RuleClub[];
}

Nested Relations

Rule Days

Day-of-week visibility with advance booking control:

interface RuleDay {
day: number; // 0=Sunday, 1=Monday, ...6=Saturday
visibleBeforeHours: number; // Hours before tee time rule becomes visible
}

Rule Tees

Per-tee visibility:

interface RuleTee {
tee: number; // Tee number (1, 10, etc.)
hideTee: boolean; // Hide this tee from rule
}

Rule Associations

Required membership associations:

interface RuleAssociation {
associationId: string; // SAGA, GolfRSA, etc.
}

Rule Clubs

Applicable clubs (visitor rules):

interface RuleClub {
clubId: string;
}

API Endpoints

List Rules

GET /admin/courses/:courseId/rules/rate?active=true&ruleType=0

// Response
[
{
"id": "rule-123",
"name": "Weekday Standard",
"rate": 450,
"rate9Holes": 250,
"ruleType": 0,
"order": 100,
"ruleDays": [
{ "day": 1, "visibleBeforeHours": 168 },
{ "day": 2, "visibleBeforeHours": 168 }
]
}
]

Create Rule

POST /admin/courses/:courseId/rules/rate

// Request
{
"name": "Weekend Special",
"ruleType": 1,
"rate": 550,
"rate9Holes": 300,
"startDate": "2025-01-01",
"endDate": "2025-12-31",
"appliesTo1Ball": true,
"appliesTo2Ball": true,
"appliesTo3Ball": true,
"appliesTo4Ball": true,
"specialLabel": "Weekend Deal",
"specialDescription": "Special weekend pricing",
"order": 150,
"ruleDays": [
{ "day": 6, "visibleBeforeHours": 168 },
{ "day": 0, "visibleBeforeHours": 168 }
]
}

Update Rule

PUT /admin/courses/:courseId/rules/rate/:ruleId

// Request (partial update)
{
"rate": 500,
"specialLabel": "New Weekend Deal"
}

Delete Rule

DELETE /admin/courses/:courseId/rules/rate/:ruleId

Pricing Preview

Test rule matching for specific scenarios:

POST /admin/courses/:courseId/rules/rate/preview

// Request
{
"date": "2025-01-15",
"time": "08:30",
"playerType": "visitor",
"ballCount": 2,
"nineHoles": false,
"isPublicHoliday": false,
"gender": "M",
"age": 35,
"tee": 1
}

// Response
{
"matchingRule": {
"id": "rule-123",
"name": "Weekday Standard",
"rate": 450,
"rate9Holes": 250,
"includeCart": false,
"ruleType": "Rate",
"order": 100
},
"evaluatedRules": [
{
"id": "rule-123",
"name": "Weekday Standard",
"matches": true,
"reason": null,
"order": 100
},
{
"id": "rule-456",
"name": "Weekend Special",
"matches": false,
"reason": "Day mismatch",
"order": 150
}
],
"request": { /* echo */ }
}

Preview Parameters

ParamTypeDescription
datestringISO date
timestringHH:mm format
playerTypestringvisitor or member
ballCountnumber1-4
nineHolesboolean9-hole round
isPublicHolidaybooleanHoliday pricing
genderstringM or F
agenumberPlayer age
classificationstringMember classification
membershipStatusstringMember status code
teenumberTee number

Mismatch Reasons

ReasonDescription
Day mismatchDay not in ruleDays
Date range mismatchOutside startDate/endDate
Time window mismatchOutside startTime/endTime
Ball count mismatchBall count not applicable
Gender filter mismatchGender doesn't match
Age filter mismatchAge outside range
Tee hiddenTee marked as hidden
Classification mismatchMember classification doesn't match
Status mismatchMember status doesn't match

Priority System

Order Field

  • Higher order value = higher priority
  • Default: 100
  • When multiple rules match, highest order wins
// Sort rules by priority
rules.sort((a, b) => b.order - a.order); // Descending

Public Holiday Logic

if (isPublicHoliday && rulesWithHolidayFlag.length > 0) {
// Use ONLY holiday-specific rules
return rulesWithHolidayFlag;
} else {
// Use day-of-week based rules
return dayBasedRules;
}

Conflict Detection

Before creating/updating rules, conflicts are detected:

Conflict Criteria

  1. Same course
  2. Both rules active
  3. Date range overlap
  4. Day-of-week overlap (if specified)
  5. Tee overlap (if specified)

Conflict Response

// ConflictException thrown with details
{
"statusCode": 409,
"message": "Rule conflicts with existing rules",
"conflicts": [
{ "id": "rule-existing", "name": "Weekday Standard" }
]
}

Member Rules

Separate endpoint for member-specific pricing:

// Base: /admin/courses/:courseId/rules/member

// Additional fields for member rules:
{
"golferClassifications": ["A", "B"], // Classification filter
"statuses": ["FULL", "ASSOCIATE"] // Status filter
}

Visibility Controls

Advance Booking Window

// Rule visible if:
currentTime >= (teeTime - visibleBeforeHours)

// Example: visibleBeforeHours=168 (7 days)
// Rule visible starting 7 days before tee time

Phone Booking Flag

{
"visibleForPhoneBookings": true // Shows in phone booking search
}

Data Model (Prisma)

model CourseRateRule {
id String @id
courseId String
name String
ruleType Int @default(0)
active Boolean @default(true)
order Int @default(100)

rate Decimal
rate9Holes Decimal
cartRate Decimal?
cartRate9Holes Decimal?
publicRate Decimal?
publicRate9Holes Decimal?

startDate DateTime
endDate DateTime
startTime String?
endTime String?

appliesTo1Ball Boolean @default(true)
appliesTo2Ball Boolean @default(true)
appliesTo3Ball Boolean @default(true)
appliesTo4Ball Boolean @default(true)

applyToPublicHoliday Boolean @default(false)
visibleForPhoneBookings Boolean @default(true)
includeCart Boolean @default(false)

specialLabel String?
specialDescription String?

ruleDays CourseRateRuleDay[]
ruleTees CourseRateRuleTee[]
ruleAssociations CourseRateRuleAssociation[]
rateClubs CourseRateRuleClub[]
}

Authentication

All endpoints require:

  • JwtAuthGuard
  • AudienceGuard('teetime-admin')

Troubleshooting

Rule Not Matching

  1. Check date range includes target date
  2. Verify day-of-week in ruleDays
  3. Check ball count applicability
  4. Verify time window if specified
  5. Check visibleBeforeHours vs current time
  6. Review preview endpoint for mismatch reasons

Conflict Errors

  1. Review existing rules for date overlap
  2. Check day-of-week overlap
  3. Consider tee-specific rules
  4. Adjust order instead of dates if possible

Priority Issues

  1. Increase order for preferred rule
  2. Check holiday vs regular day logic
  3. Review rule activation status