Skip to main content

Overview

The calendar system consists of three interconnected services that handle appointment scheduling, intent detection, and conversational flows for booking appointments via WhatsApp. Architecture:
  1. GoogleCalendarService - Direct Google Calendar API integration
  2. CalendarIntentService - Detects user intent using OpenAI function calling
  3. CalendarFlowHandler - Manages multi-step appointment booking conversations

GoogleCalendarService

Namespace: App\Services Handles all interactions with the Google Calendar API, including creating events, checking availability, and managing appointments.

Constructor

public function __construct(
    string $accessToken,
    string $calendarId,
    Logger $logger,
    string $timezone = 'America/Bogota',
    string $refreshToken = null,
    string $clientId = null,
    string $clientSecret = null,
    Database $db = null
)
accessToken
string
required
Google OAuth2 access token
calendarId
string
required
Google Calendar ID (usually email address)
logger
Logger
required
Logger instance
timezone
string
default:"America/Bogota"
Timezone for all date/time operations
refreshToken
string
OAuth refresh token for automatic token renewal
clientId
string
OAuth client ID
clientSecret
string
OAuth client secret
db
Database
Database for persisting refreshed tokens
$calendarConfig = CalendarConfigHelper::loadFromDatabase($db);
$credentials = $credentialService->getGoogleOAuthCredentials();

$calendar = new GoogleCalendarService(
    $credentials['access_token'],
    $credentials['calendar_id'],
    $logger,
    $calendarConfig['timezone'],
    $credentials['refresh_token'],
    $credentials['client_id'],
    $credentials['client_secret'],
    $db
);
Features:
  • Automatic token refresh when access token expires
  • Timezone-aware date/time handling
  • Business hours validation
  • Conflict detection

Core Calendar Methods

createEvent

Creates a new Google Calendar event.
public function createEvent(
    string $summary,
    string $description,
    string $startDateTime,
    string $endDateTime,
    string $attendeeEmail = null,
    array $calendarConfig = null
): array
summary
string
required
Event title (e.g., “Cita - Juan Pérez”)
description
string
required
Event description
startDateTime
string
required
Start time in RFC3339 format (e.g., “2026-03-15T14:00:00”)
endDateTime
string
required
End time in RFC3339 format
attendeeEmail
string
Guest email address
calendarConfig
array
Configuration with reminder settings
Returns: Google Calendar event array with id, summary, start, end, etc.
$eventTitle = 'Cita - ' . $contactName;
$description = 'Creado desde WhatsApp por ' . $contactName;
if ($service) {
    $description .= "\nMotivo: " . $service;
}

$startDateTime = new \DateTime("{$date} {$time}", new \DateTimeZone($timezone));
$endDateTime = clone $startDateTime;
$endDateTime->modify("+60 minutes");

$event = $calendar->createEvent(
    $eventTitle,
    $description,
    $startDateTime->format(\DateTime::RFC3339),
    $endDateTime->format(\DateTime::RFC3339),
    null,
    $calendarConfig
);

listUpcomingEvents

Retrieves upcoming events from the calendar.
public function listUpcomingEvents(int $maxResults = 10): array
Returns: Array with items containing event objects

checkEventOverlap

Checks if a time slot has any conflicting events.
public function checkEventOverlap(
    string $date,
    string $startTime,
    string $endTime
): array
Returns: Array with overlap (boolean) and events (array of conflicting events)
$result = $calendar->checkEventOverlap('2026-03-15', '14:00', '15:00');

if ($result['overlap']) {
    echo "Slot is busy. Conflicting events: " . count($result['events']);
} else {
    echo "Slot is available";
}

rescheduleEvent

Moves an existing event to a new date/time.
public function rescheduleEvent(
    string $eventId,
    string $newStart,
    string $newEnd
): array
$eventId = $rescheduleData['event_id'];
$newStart = $date . 'T' . $time . ':00';
$newEnd = $date . 'T' . date('H:i', strtotime($time) + 3600) . ':00';

$calendar->rescheduleEvent($eventId, $newStart, $newEnd);

deleteEvent

Deletes an event from the calendar.
public function deleteEvent(string $eventId): bool

Validation Methods

validateDateNotPast

Ensures a date is not in the past.
public function validateDateNotPast(string $dateString): array
Returns:
[
    'valid' => false,
    'message' => 'Esa fecha ya pasó. Por favor indica una fecha futura válida.'
]

validateMinAdvanceHours

Ensures appointments are scheduled with minimum advance notice.
public function validateMinAdvanceHours(
    string $date,
    string $time,
    int $minAdvanceHours
): array
$result = $calendar->validateMinAdvanceHours('2026-03-06', '15:00', 24);

if (!$result['valid']) {
    echo $result['message'];
    // "Las citas requieren al menos 24 hora(s) de antelación..."
}

validateBusinessHours

Checks if a date/time falls within business hours.
public function validateBusinessHours(
    string $date,
    string $time,
    array $businessHours
): array
businessHours
array
required
Array with keys: monday, tuesday, etc., each containing ['start' => '09:00', 'end' => '18:00']
$businessHours = [
    'monday' => ['start' => '09:00', 'end' => '18:00'],
    'tuesday' => ['start' => '09:00', 'end' => '18:00'],
    'saturday' => null,  // Closed
    'sunday' => null     // Closed
];

$result = $calendar->validateBusinessHours('2026-03-15', '20:00', $businessHours);

if (!$result['valid']) {
    echo $result['reason'];
    // "Horario fuera de atención. Atendemos de 09:00 a 18:00"
}

validateDateFormat

Parses natural language dates into ISO format.
public function validateDateFormat(string $dateText): ?string
Supports:
  • “mañana” → tomorrow
  • “hoy” → today
  • “pasado mañana” → day after tomorrow
  • “15/03/2026” → 2026-03-15
  • “15 de marzo” → current/next year
  • “15 de marzo del 2026” → 2026-03-15
$date = $calendar->validateDateFormat('mañana');
echo $date;  // "2026-03-07"

$date = $calendar->validateDateFormat('15 de marzo del 2026');
echo $date;  // "2026-03-15"

CalendarIntentService

Namespace: App\Services Uses OpenAI function calling to detect user intent related to calendar operations.

Constructor

public function __construct(
    OpenAIService $openai,
    Logger $logger
)

detectIntent

Analyzes user message to determine calendar-related intent.
public function detectIntent(
    string $message,
    array $conversationHistory,
    string $systemPrompt
): array
Returns:
[
    'intent' => 'schedule',  // schedule, cancel, reschedule, list, check_availability, none
    'extracted_data' => [
        'date_preference' => 'mañana',
        'time_preference' => '3pm',
        'service_type' => 'consulta médica',
        'is_confirmed' => false
    ],
    'confidence' => 'high',  // high, low
    'original_response' => '...'
]
Detected Intents:
  • schedule - User wants to book an appointment
  • cancel - User wants to cancel an appointment
  • reschedule - User wants to change appointment time
  • list - User wants to see their appointments
  • check_availability - User asking about available slots
  • none - No calendar-related intent
$calendarIntent = new CalendarIntentService($openai, $logger);

$result = $calendarIntent->detectIntent(
    $messageData['text'],
    $conversationHistory,
    $systemPrompt
);

if ($result['intent'] === 'schedule' && $result['confidence'] === 'high') {
    // Start scheduling flow
    $flowHandler->startFlow(
        'schedule',
        $result['extracted_data'],
        $conversation,
        $messageData
    );
}

CalendarFlowHandler

Namespace: App\Handlers Manages multi-step conversational flows for appointment scheduling, cancellation, and rescheduling.

Constructor

public function __construct(
    Database $db,
    Logger $logger,
    GoogleCalendarService $calendar,
    OpenAIService $openai,
    array $calendarConfig,
    ConversationService $conversationService
)

Flow States

The handler manages these conversation steps:
StepDescription
expecting_dateWaiting for appointment date
expecting_timeWaiting for appointment time
expecting_serviceWaiting for appointment reason/type
expecting_confirmationWaiting for final confirmation
cancel_selectUser selecting which appointment to cancel
cancel_confirmConfirming cancellation
reschedule_selectUser selecting which appointment to reschedule
reschedule_reasonAsking why they’re rescheduling

Main Methods

startFlow

Initiates a calendar flow based on detected intent.
public function startFlow(
    string $intent,
    array $extractedData,
    array $conversation,
    array $messageData
): array
Returns:
[
    'handled' => true,
    'response' => 'Con gusto te agendo una cita. ¿Qué fecha prefieres?...',
    'status' => 'expecting_date'
]

handleActiveFlow

Processes user input when a flow is in progress.
public function handleActiveFlow(
    array $flowState,
    string $message,
    array $conversation,
    array $messageData
): array
$flowState = $flowHandler->getFlowState($messageData['from']);

if ($flowState) {
    $result = $flowHandler->handleActiveFlow(
        $flowState,
        $messageData['text'],
        $conversation,
        $messageData
    );
    
    if ($result['handled']) {
        $whatsapp->sendMessage($messageData['from'], $result['response']);
        exit;
    }
}

clearFlowState

Ends the flow and cleans up session data.
public function clearFlowState(string $phone): void

Complete Scheduling Flow

// User: "Quiero agendar una cita para mañana a las 3pm"

// 1. Intent detection
$intent = $calendarIntent->detectIntent($message, $history, $systemPrompt);
// Returns: ['intent' => 'schedule', 'extracted_data' => ['date_preference' => 'mañana', ...]]

// 2. Start flow
$result = $flowHandler->startFlow(
    'schedule',
    $intent['extracted_data'],
    $conversation,
    $messageData
);
// Returns: ['handled' => true, 'response' => 'Perfecto, mañana. ¿A qué hora?', ...]

// 3. User: "3pm"
$result = $flowHandler->handleActiveFlow($flowState, '3pm', $conversation, $messageData);
// Validates time, checks availability, asks for service type

// 4. User: "consulta médica"
$result = $flowHandler->handleActiveFlow($flowState, 'consulta médica', $conversation, $messageData);
// Asks for confirmation

// 5. User: "sí"
$result = $flowHandler->handleActiveFlow($flowState, 'sí', $conversation, $messageData);
// Creates event, sends confirmation
// Returns: ['handled' => true, 'response' => '✅ ¡Cita confirmada! 📅 07/03/2026 a las 15:00', ...]

Database Schema

calendar_flow_state Table

ColumnTypeDescription
user_phoneVARCHARPrimary key - user’s phone
conversation_idINTForeign key to conversations
current_stepVARCHARCurrent flow step
extracted_dateDATESelected date
extracted_timeTIMESelected time
extracted_serviceVARCHARAppointment reason
event_titleVARCHAREvent title to create
cancel_events_jsonTEXTJSON array of events for cancel/reschedule
attemptsINTFailed validation attempts
expires_atTIMESTAMPSession expiration

Configuration

$calendarConfig = [
    'enabled' => true,
    'timezone' => 'America/Bogota',
    'default_duration_minutes' => 60,
    'min_advance_hours' => 24,
    'business_hours' => [
        'monday' => ['start' => '09:00', 'end' => '18:00'],
        'tuesday' => ['start' => '09:00', 'end' => '18:00'],
        'wednesday' => ['start' => '09:00', 'end' => '18:00'],
        'thursday' => ['start' => '09:00', 'end' => '18:00'],
        'friday' => ['start' => '09:00', 'end' => '18:00'],
        'saturday' => null,
        'sunday' => null
    ],
    'reminders' => [
        'email' => ['enabled' => true, 'minutes_before' => 1440],
        'popup' => ['enabled' => true, 'minutes_before' => 30]
    ]
];

Best Practices

Always validate appointments using the three-step validation:
  1. Date not in past
  2. Within business hours
  3. Minimum advance notice met
Flow expiration: Calendar flows expire after 30 minutes of inactivity. Always check isFlowExpired before processing.
Token refresh: The GoogleCalendarService automatically refreshes expired access tokens using the refresh token, and persists the new token to the database.

Source Code

  • GoogleCalendarService: src/Services/GoogleCalendarService.php:1-490
  • CalendarIntentService: src/Services/CalendarIntentService.php:1-127
  • CalendarFlowHandler: src/Handlers/CalendarFlowHandler.php:1-1300+

Build docs developers (and LLMs) love