Buddy Invites
The buddy system enables players to build and manage a golf buddy list, with support for inviting contacts via email or phone.
Architecture
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Player App │────▶│ BuddyService │────▶│ PlayerBuddy │
│ /players/me │ │ │ │ Repository │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│
▼
┌──────────────────┐
│ BuddyInvite │
│ Notification │──▶ WhatsApp
└──────────────────┘
Sources:
libs/tee-time-services/src/lib/buddy.service.tslibs/tee-time-services/src/lib/buddy-invite-notification.service.ts
Invite Workflow
Step 1: Player Sends Invite
POST /players/me/buddies/invite
// Request
{
"email": "friend@example.com",
"phone": "+27821234567",
"name": "John Smith" // Optional display name
}
// Response (existing player found)
{
"status": "added",
"buddyId": "player-123"
}
// Response (no existing player)
{
"status": "pending",
"inviteId": "invite-456"
}
Step 2: Resolution Flow
Input: email/phone
│
▼
┌─────────────────┐
│ Check phone │──► Player found? ──► Create buddy ──► Return "added"
│ (E.164 format) │
└─────────────────┘
│ Not found
▼
┌─────────────────┐
│ Check email │──► Player found? ──► Create buddy ──► Return "added"
│ (case-insensitive)│
└─────────────────┘
│ Not found
▼
┌─────────────────┐
│ Create pending │──► Send WhatsApp ──► Return "pending"
│ invite │
└─────── ──────────┘
Step 3: Invite Acceptance
When invited contact signs up:
- System discovers pending invites by email/phone
- Auto-creates buddy relationships
- Marks invites as accepted
- Sends confirmation to inviters via WhatsApp
Phone Normalization
All phone numbers normalized to E.164 format:
| Input | Output |
|---|---|
0821234567 | +27821234567 |
082 123 4567 | +27821234567 |
27821234567 | +27821234567 |
+27821234567 | +27821234567 |
0027821234567 | +27821234567 |
+1 415 555 0000 | +14155550000 |
Default country code: 27 (South Africa)
WhatsApp Integration
Invite Notification (Phase 1 - Text)
// Message template
"Hi! {inviterName} has added you as a golf buddy on TeeTime.
Download the app to connect: {downloadUrl}"
Invite Notification (Phase 2 - Template)
// Template: buddy_invite_v7
{
header: "TeeTime Buddy Request",
body: "You were added as a golf buddy in TeeTime.",
button: "Review Buddy Request" → deep link
}
Acceptance Notification
// Template: buddy_accepted_v7
{
header: "Buddy List Updated",
body: "{buddyName} has been added as your buddy.",
button: "View Buddy List"
}
Configuration
interface BuddyInviteNotificationOptions {
downloadUrl?: string; // Default: 'https://teetime.app'
locale?: string; // Default: 'en-US'
enableWhatsApp?: boolean; // Default: true
useTemplateMessages?: boolean; // Default: false
}
API Endpoints
Player Endpoints (Authenticated)
| Method | Endpoint | Description |
|---|---|---|
GET | /players/me/buddies | List my buddies |
POST | /players/me/buddies | Add buddy by ID |
POST | /players/me/buddies/invite | Invite by email/phone |
DELETE | /players/me/buddies/:buddyId | Remove buddy |
GET | /players/me/buddies/groups | Generate booking groups |
Public Endpoints (No Auth)
| Method | Endpoint | Description |
|---|---|---|
GET | /buddy-invites/:token | Get invite details |
Admin Endpoints
| Method | Endpoint | Description |
|---|---|---|
GET | /admin/players/:playerId/buddies | List player's buddies |
POST | /admin/players/:playerId/buddies | Add buddy for player |
DELETE | /admin/players/:playerId/buddies/:buddyId | Remove buddy |
Invite Details
Get invite status without authentication:
GET /buddy-invites/:token
// Response
{
"token": "invite-456",
"status": "pending", // or "accepted"
"inviterName": "Jane Doe",
"createdAt": "2025-12-15T08:00:00Z"
}
Buddy List Management
List Buddies
GET /players/me/buddies
// Response
[
{
"id": "buddy-rel-123",
"playerId": "my-player-id",
"buddyId": "player-456",
"createdAt": "2025-12-15T08:00:00Z",
"buddy": {
"id": "player-456",
"firstName": "John",
"lastName": "Smith",
"fullName": "John Smith",
"email": "john@example.com",
"phoneNumber": "+27821234567"
}
}
]
Add Buddy (Direct)
POST /players/me/buddies
// Request
{
"buddyId": "player-456"
}
Remove Buddy
DELETE /players/me/buddies/:buddyId
Buddy Groups
Generate groups for booking:
GET /players/me/buddies/groups?size=4&pad=OPEN
// Response
[
{
"group": [
{ "id": "player-1", "name": "John Smith" },
{ "id": "player-2", "name": "Jane Doe" },
{ "id": "player-3", "name": "Bob Wilson" },
{ "type": "OPEN" } // Padded slot
]
}
]
Data Model
PlayerBuddy
interface PlayerBuddy {
id: string;
playerId: string;
buddyId: string;
createdAt: Date;
}
PendingBuddyInvite
interface PendingBuddyInvite {
id: string; // Also serves as token
inviterPlayerId: string;
inviteeEmail?: string;
inviteePhone?: string;
inviteeName?: string;
status: 'pending' | 'accepted';
acceptedAt?: Date;
createdAt: Date;
}
Error Handling
| Error | Cause |
|---|---|
BadRequestException | Neither email nor phone provided |
NotFoundException | Player not found (for direct add) |
ConflictException | Already buddies |
Notification Delivery
Fire-and-Forget Pattern
- WhatsApp sends don't block API responses
- Errors logged but non-fatal
- Graceful degradation if messaging unavailable
Delivery Events
| Event | Notification |
|---|---|
| Invite created | WhatsApp to invitee (if phone provided) |
| Invite accepted | WhatsApp to inviter |
Access Control
Player Endpoints
- JWT authentication required
- Identity resolved from token (sub, userId, email, phone)
- Auto-creates player on first use
Admin Endpoints
- JWT authentication required
teetime-adminaudience required- Can manage any player's buddies
Troubleshooting
Invite Not Matching on Signup
- Check email case sensitivity
- Verify phone normalization format
- Ensure invite is still pending (not accepted)
WhatsApp Not Sending
- Check
enableWhatsAppconfiguration - Verify phone number format
- Review WhatsApp messenger logs
- Check template approval status (Phase 2)
Duplicate Buddy Relationships
- Service handles duplicates gracefully
- Returns existing relationship instead of error
- Check database constraint on (playerId, buddyId)