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
| Type | Value | Description |
|---|---|---|
Rate | 0 | Standard visitor pricing |
Special | 1 | Promotional pricing (with label/description) |
Exclusion | 2 | Block/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
| Param | Type | Description |
|---|---|---|
date | string | ISO date |
time | string | HH:mm format |
playerType | string | visitor or member |
ballCount | number | 1-4 |
nineHoles | boolean | 9-hole round |
isPublicHoliday | boolean | Holiday pricing |
gender | string | M or F |
age | number | Player age |
classification | string | Member classification |
membershipStatus | string | Member status code |
tee | number | Tee number |
Mismatch Reasons
| Reason | Description |
|---|---|
Day mismatch | Day not in ruleDays |
Date range mismatch | Outside startDate/endDate |
Time window mismatch | Outside startTime/endTime |
Ball count mismatch | Ball count not applicable |
Gender filter mismatch | Gender doesn't match |
Age filter mismatch | Age outside range |
Tee hidden | Tee marked as hidden |
Classification mismatch | Member classification doesn't match |
Status mismatch | Member status doesn't match |
Priority System
Order Field
- Higher
ordervalue = 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
- Same course
- Both rules active
- Date range overlap
- Day-of-week overlap (if specified)
- 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:
JwtAuthGuardAudienceGuard('teetime-admin')
Troubleshooting
Rule Not Matching
- Check date range includes target date
- Verify day-of-week in ruleDays
- Check ball count applicability
- Verify time window if specified
- Check
visibleBeforeHoursvs current time - Review preview endpoint for mismatch reasons
Conflict Errors
- Review existing rules for date overlap
- Check day-of-week overlap
- Consider tee-specific rules
- Adjust order instead of dates if possible
Priority Issues
- Increase
orderfor preferred rule - Check holiday vs regular day logic
- Review rule activation status