Skip to main content

Reciprocity Agreements

Overview

A system for managing inter-club reciprocity arrangements, allowing members of one club to play at partner clubs with special rates and benefits.

Status: Implemented (tracking: #10371 open for closure/rollout)

The reciprocity feature is fully implemented with database schema, services, and pricing integration.

This guide focuses on agreement structure and operational details for clubs and administrators.


Features

Agreement Types

  • Bilateral: Two clubs agree to honor each other's members
  • Network Membership: All-to-all agreements (e.g., SAGA-like networks)
  • Corporate Sponsorship: Company-funded access across multiple clubs
  • Association Tier: Handicap-based access (e.g., low-handicap reciprocity)

Rate Configurations

  • Percentage Discount: X% off standard rate
  • Fixed Amount: Flat discount (e.g., R200 off)
  • Fixed Rate: Set price regardless of standard rate
  • Rate Tier: Map to specific club rate tier (member, affiliate, etc.)

Restrictions

  • Handicap Ranges: Min/max handicap index requirements
  • Time Windows: Specific days/times when reciprocity applies
  • Blackout Dates: Excluded dates (tournaments, peak days)
  • Booking Limits: Max bookings per month/year

Priority System

  • Multiple agreements evaluated in priority order
  • First matching agreement wins
  • Higher priority = lower number

Data Model

ReciprocityAgreement
├── id, tenantId
├── homeClubId (granting club)
├── partnerClubId (receiving club, nullable for networks)
├── agreementType (BILATERAL, NETWORK, CORPORATE, ASSOCIATION)
├── name, description
├── status (DRAFT, ACTIVE, SUSPENDED, EXPIRED)
├── startDate, endDate
├── priority
└── settings (JSON)

ReciprocityRateConfig
├── agreementId
├── discountType (PERCENTAGE, FIXED_AMOUNT, FIXED_RATE, RATE_TIER)
├── discountValue
├── rateTierId (for RATE_TIER type)
└── applicableDays (bitmap: Sun-Sat)

ReciprocityRestrictions
├── agreementId
├── handicapMin, handicapMax
├── timeWindowStart, timeWindowEnd
├── blackoutDates (JSON array)
├── maxBookingsPerMonth
└── maxBookingsPerYear

BenefitMembership
├── playerId
├── benefitProviderId (club or association)
├── membershipTier
├── homeClubId
├── status (ACTIVE, EXPIRED, SUSPENDED)
├── validFrom, validTo
└── verificationData (JSON)

API Endpoints

Admin Endpoints

MethodEndpointDescription
GET/admin/reciprocityList agreements
POST/admin/reciprocityCreate agreement
GET/admin/reciprocity/:idGet agreement
PATCH/admin/reciprocity/:idUpdate agreement
DELETE/admin/reciprocity/:idDelete agreement
POST/admin/reciprocity/:id/activateActivate
POST/admin/reciprocity/:id/suspendSuspend

Player Endpoints

MethodEndpointDescription
GET/v1/players/me/benefitsMy benefits
GET/v1/clubs/:id/reciprocityClub's reciprocity info

Services

ReciprocityService

  • listAgreements(clubId) - All agreements for a club
  • createAgreement(data) - New agreement
  • updateAgreement(id, data) - Modify agreement
  • activateAgreement(id) - Set to ACTIVE
  • suspendAgreement(id) - Temporarily disable
  • checkEligibility(playerId, clubId) - Check if player qualifies

EligibilityService

  • getApplicableAgreements(playerId, clubId, date) - Matching agreements
  • calculateRate(playerId, clubId, slotId) - Best available rate
  • validateRestrictions(agreementId, playerId, date) - Check limits

ReciprocityConflictService

  • detectConflicts(agreementId) - Find overlapping agreements
  • resolveByPriority(agreements) - Order by priority

AgreementExpirySchedulerService

  • expireAgreements() - Scheduled job for expiry
  • notifyExpiringAgreements(daysAhead) - Warning notifications

Pricing Integration

// Price resolution flow
1. Get slot base price
2. Check player benefits/memberships
3. Find applicable reciprocity agreements
4. Apply best rate from:
- Player's home club rate (if member)
- Reciprocity agreement rate
- Visitor rate (fallback)
5. Apply any additional discounts (specials, promotions)

Example Configurations

Bilateral Agreement

{
"agreementType": "BILATERAL",
"homeClubId": "club-a-uuid",
"partnerClubId": "club-b-uuid",
"name": "Pine Valley ↔ Royal Links",
"rateConfig": {
"discountType": "PERCENTAGE",
"discountValue": 50
},
"restrictions": {
"handicapMax": 18,
"maxBookingsPerMonth": 4
}
}

Network Membership

{
"agreementType": "NETWORK",
"homeClubId": "network-provider-uuid",
"partnerClubId": null,
"name": "Premier Golf Network",
"rateConfig": {
"discountType": "RATE_TIER",
"rateTierId": "affiliate-tier-uuid"
},
"restrictions": {
"blackoutDates": ["2025-12-25", "2025-01-01"]
}
}

Integration Points

  • Pricing Engine: Rate resolution with reciprocity discounts
  • Booking Service: Validate eligibility at booking time
  • Benefits System: Membership verification
  • Association Providers: External handicap/membership lookup

Admin UI (Complete)

The admin UI is implemented in libs/ui/benefits-admin/ and integrated into teetime-admin at /benefits:

ComponentFeatures
AgreementsTableList, search, filter, bulk toggle/delete, import, clone, history
AgreementFormDrawerCreate/edit with all fields (type, discount, restrictions, days, time, blackout, handicap)
NetworksManagerAdd/remove/toggle network memberships
HomeClubMapEditorProvider-specific home club mappings
EligibilityPreviewTest eligibility calculations
DiagnosticsViewRun diagnostic checks

Feature Flag: VITE_ENABLE_BENEFITS_ADMIN (enabled by default)


Mobile UX Integration (Complete)

The mobile app displays reciprocity benefits throughout the booking flow:

Components

ComponentLocationFeatures
SlotPricelibs/mobile-booking/src/lib/components/slot/SlotPrice.tsxMember/Reciprocal badges, crossed-out original price, green member rate, "Save R{amount}" indicator
SlotCardlibs/mobile-booking/src/lib/components/slot/SlotCard.tsxEligibility enforcement via canBook, deny reason display with formatted message
SlotSelectionModallibs/mobile-booking/src/lib/components/slot/SlotSelectionModal.tsxToast notifications for booking denials
VenueCardlibs/mobile-booking/src/lib/components/venue/VenueCard.tsxCurrency and eligibility fields propagation

Type Definitions

Eligibility fields defined in libs/mobile-booking/src/lib/types/slot.types.ts:54-68:

// Eligibility/reciprocity fields (from SSE response)
eligibilityPrice?: number; // Final price after discounts
eligibilityRole?: 'VISITOR' | 'RECIPROCAL' | 'MEMBER';
reciprocityEligible?: boolean; // Qualifies for reciprocity pricing
isHomeClub?: boolean; // Booking at home club
canBook?: boolean; // Policy gate
denyReason?: string; // e.g. OUTSIDE_BOOKING_WINDOW
currencyCode?: string; // ISO-4217 currency code

SSE Data Flow

Backend attaches eligibility fields to each slot in the SSE stream:

// From search-engine.service.ts
{
price: 850, // Base visitor price
eligibilityPrice: 650, // Final price after reciprocity
eligibilityRole: 'RECIPROCAL', // VISITOR | RECIPROCAL | MEMBER
canBook: true, // false if outside policy
denyReason: null, // OUTSIDE_BOOKING_WINDOW, etc.
currencyCode: 'ZAR' // For multi-currency display
}

Multi-Currency Support

Currency resolution chain in libs/tee-time-services/src/lib/utils/currency.util.ts:

  1. slot.currencyCode (if present)
  2. club.meta?.currencyCode
  3. club.mcaV1Region → region mapping
  4. club.countryId → country mapping
  5. Default: 'ZAR'

Mobile fallback symbols in libs/mobile-booking/src/lib/utils/currency.ts:

  • ZAR → R, GBP → £, USD → $, EUR → , AUD → A$, CAD → C$

Test Coverage

Backend (tee-time-services)

Test FileCoverage
eligibility.service.spec.tsEligibility calculations, rate resolution
access-policy.service.spec.tscanBook/denyReason logic
search-engine.eligibility.spec.tsSSE eligibility field emission
search-engine.pricing.spec.tsPrice resolution with reciprocity
booking.controller.benefits.spec.tsBenefits application at booking time
booking.confirm.benefits.int.spec.tsIntegration: benefits → booking confirmation
club-visitor.service.spec.tsCandidate player eligibility, currency resolution

Mobile (mobile-booking)

Test FileCoverage
currency.spec.tsformatCurrency, formatCurrencyParts, fallback symbols
pricing.service.spec.tsSSE tee sheet → slot mapping, eligibility field propagation

Note: Zustand store tests require React context (@testing-library/react) - P1 future work.


Future Enhancements

  • Admin UI for agreement management
  • Mobile reciprocity badges and savings display
  • Eligibility enforcement in booking flow
  • Player-visible benefits dashboard (web)
  • Usage reporting and analytics
  • Automated renewal workflows