# API Integrations

## Overview

The EventBookings Shopify app integrates with two primary APIs:
1. **EventBookings API** - For event data and authentication
2. **Shopify Admin API** - For store integration and webhooks

This document provides comprehensive implementation details for both integrations.

## EventBookings API Integration

### Authentication Flow

The EventBookings API uses OAuth 2.0 Client Credentials flow for authentication.

#### Endpoints
- **Identity Server**: `https://identity.eventbookings.com`
- **API Server**: `https://api-rto.eventbookings.com/v3`

#### Token Generation

**PHP Implementation**:
```php
class EventBookingsApi
{
    private const IDENTITY_URL = 'https://identity.eventbookings.com';
    private const API_URL = 'https://api-rto.eventbookings.com/v3';

    private $httpClient;

    public function __construct()
    {
        $this->httpClient = new \GuzzleHttp\Client([
            'timeout' => 30,
            'verify' => true, // SSL verification
        ]);
    }

    /**
     * Generate access token from client credentials
     * Based on Django function: generate_token_from_credentials
     */
    public function generateToken(string $clientId, string $clientSecret): array
    {
        if (empty($clientId) || empty($clientSecret)) {
            throw new \InvalidArgumentException('Client ID and Client Secret are required');
        }

        try {
            $response = $this->httpClient->post(self::IDENTITY_URL . '/connect/token', [
                'form_params' => [
                    'client_id' => $clientId,
                    'client_secret' => $clientSecret,
                    'grant_type' => 'client_credentials'
                ],
                'headers' => [
                    'Content-Type' => 'application/x-www-form-urlencoded'
                ]
            ]);

            $tokenData = json_decode($response->getBody()->getContents(), true);

            if (!isset($tokenData['access_token'])) {
                throw new \RuntimeException('No access token in response');
            }

            $expiresIn = $tokenData['expires_in'] ?? 3600;
            $expiresAt = now()->addSeconds($expiresIn);

            return [
                'access_token' => $tokenData['access_token'],
                'expires_at' => $expiresAt,
                'expires_in' => $expiresIn
            ];

        } catch (\GuzzleHttp\Exception\ClientException $e) {
            $response = $e->getResponse();
            $errorData = json_decode($response->getBody()->getContents(), true);

            $errorMessage = $errorData['error_description'] ??
                           $errorData['error'] ??
                           'Token request failed: ' . $response->getStatusCode();

            throw new \RuntimeException($errorMessage);

        } catch (\GuzzleHttp\Exception\RequestException $e) {
            throw new \RuntimeException('Network error: ' . $e->getMessage());
        }
    }

    /**
     * Extract organization UUID from JWT token
     * Based on Django function: extract_org_uuid_from_token
     */
    public function extractOrgUuid(string $accessToken): ?string
    {
        try {
            $parts = explode('.', $accessToken);
            if (count($parts) !== 3) {
                return null;
            }

            // Decode the payload (second part)
            $payload = $parts[1];

            // Add padding if necessary
            $missingPadding = strlen($payload) % 4;
            if ($missingPadding) {
                $payload .= str_repeat('=', 4 - $missingPadding);
            }

            // Decode base64
            $decodedPayload = base64_decode($payload, true);
            if ($decodedPayload === false) {
                return null;
            }

            $payloadData = json_decode($decodedPayload, true);
            if (!$payloadData) {
                return null;
            }

            // Extract org_uuid (check different possible field names)
            return $payloadData['org_uuid'] ??
                   $payloadData['organization_uuid'] ??
                   $payloadData['client_OrgId'] ??
                   null;

        } catch (\Exception $e) {
            \Log::warning('Error extracting org_uuid from token', [
                'error' => $e->getMessage()
            ]);
            return null;
        }
    }

    /**
     * Fetch events from EventBookings API
     * Based on Django function: fetch_events_from_api
     */
    public function fetchEvents(string $accessToken, string $orgUuid, array $queryParams = []): array
    {
        if (empty($accessToken) || empty($orgUuid)) {
            throw new \InvalidArgumentException('Access token and organization UUID are required');
        }

        try {
            $url = self::API_URL . "/orgs/{$orgUuid}/events";

            $params = [];
            if (isset($queryParams['page'])) {
                $params['page'] = $queryParams['page'];
            }
            if (isset($queryParams['per_page'])) {
                $params['per_page'] = $queryParams['per_page'];
            }
            if (isset($queryParams['status'])) {
                $params['status'] = $queryParams['status'];
            }

            $response = $this->httpClient->get($url, [
                'query' => $params,
                'headers' => [
                    'Authorization' => 'Bearer ' . $accessToken,
                    'Accept' => 'application/json',
                    'Content-Type' => 'application/json'
                ]
            ]);

            return json_decode($response->getBody()->getContents(), true);

        } catch (\GuzzleHttp\Exception\ClientException $e) {
            if ($e->getResponse()->getStatusCode() === 401) {
                throw new \RuntimeException('Token expired or invalid');
            }

            $response = $e->getResponse();
            $errorData = json_decode($response->getBody()->getContents(), true);
            $errorMessage = $errorData['message'] ?? 'API request failed: ' . $response->getStatusCode();

            throw new \RuntimeException($errorMessage);

        } catch (\GuzzleHttp\Exception\RequestException $e) {
            throw new \RuntimeException('Network error: ' . $e->getMessage());
        }
    }

    /**
     * Fetch detailed data for a specific event
     * Based on Django function: fetch_event_details_from_api
     */
    public function fetchEventDetails(string $accessToken, string $orgUuid, string $eventUuid): array
    {
        if (empty($accessToken) || empty($orgUuid) || empty($eventUuid)) {
            throw new \InvalidArgumentException('Access token, organization UUID, and event UUID are required');
        }

        try {
            $url = self::API_URL . "/orgs/{$orgUuid}/events/{$eventUuid}";

            $response = $this->httpClient->get($url, [
                'headers' => [
                    'Authorization' => 'Bearer ' . $accessToken,
                    'Accept' => 'application/json',
                    'Content-Type' => 'application/json'
                ]
            ]);

            $eventData = json_decode($response->getBody()->getContents(), true);

            // Ensure all required fields are present for EventDetails component
            $detailedEvent = array_merge($eventData, [
                'booking_starts_on' => $eventData['booking_starts_on'] ?? $eventData['created_at'] ?? null,
                'booking_ends_on' => $eventData['booking_ends_on'] ?? $eventData['starts_on'] ?? null,
                'location' => $eventData['location'] ?? 'Location TBD',
                'organizer' => $eventData['organizer'] ?? 'Event Organizer',
                'timezone' => $eventData['timezone'] ?? 'UTC',
                'booking_url' => "https://eventbookings.com/book/{$eventData['uuid']}",
                'max_capacity' => $eventData['max_capacity'] ?? 100,
                'available_tickets' => ($eventData['max_capacity'] ?? 100) - ($eventData['tickets_sold'] ?? 0),
                'currency' => $eventData['currency'] ?? 'USD',
                'status' => $eventData['status'] ?? 'published'
            ]);

            return $detailedEvent;

        } catch (\GuzzleHttp\Exception\ClientException $e) {
            if ($e->getResponse()->getStatusCode() === 404) {
                throw new \RuntimeException('Event not found');
            }
            if ($e->getResponse()->getStatusCode() === 401) {
                throw new \RuntimeException('Token expired or invalid');
            }

            throw new \RuntimeException('API request failed: ' . $e->getResponse()->getStatusCode());

        } catch (\GuzzleHttp\Exception\RequestException $e) {
            throw new \RuntimeException('Network error: ' . $e->getMessage());
        }
    }

    /**
     * Test API connection with credentials
     */
    public function testConnection(string $clientId, string $clientSecret): array
    {
        try {
            // Generate token
            $tokenResult = $this->generateToken($clientId, $clientSecret);

            // Extract org_uuid
            $orgUuid = $this->extractOrgUuid($tokenResult['access_token']);
            if (!$orgUuid) {
                return [
                    'success' => false,
                    'message' => 'Could not extract organization UUID from token',
                    'data' => null
                ];
            }

            // Test API call
            $eventsResult = $this->fetchEvents(
                $tokenResult['access_token'],
                $orgUuid,
                ['per_page' => 1]
            );

            return [
                'success' => true,
                'message' => 'Connection successful',
                'data' => [
                    'access_token' => $tokenResult['access_token'],
                    'org_uuid' => $orgUuid,
                    'expires_at' => $tokenResult['expires_at']
                ]
            ];

        } catch (\Exception $e) {
            return [
                'success' => false,
                'message' => 'Connection failed: ' . $e->getMessage(),
                'data' => null
            ];
        }
    }
}
```

#### Token Management Service

```php
class TokenManager
{
    private $eventBookingsApi;

    public function __construct(EventBookingsApi $eventBookingsApi)
    {
        $this->eventBookingsApi = $eventBookingsApi;
    }

    /**
     * Check if token needs refresh and refresh if necessary
     * Based on Django function: refresh_token_if_needed
     */
    public function refreshTokenIfNeeded(StoreSettings $storeSettings): array
    {
        if (!$storeSettings->hasValidCredentials()) {
            return [
                'success' => false,
                'message' => 'Missing credentials'
            ];
        }

        // Check if token is expired or will expire soon (within 5 minutes)
        if ($storeSettings->token_expires_at &&
            $storeSettings->token_expires_at->diffInMinutes(now()) > 5) {
            return [
                'success' => true,
                'message' => 'Token is still valid'
            ];
        }

        // Token expired or will expire soon, refresh it
        try {
            $tokenResult = $this->eventBookingsApi->generateToken(
                $storeSettings->client_id,
                $storeSettings->client_secret
            );

            // Update store settings with new token
            $storeSettings->update([
                'access_token' => $tokenResult['access_token'],
                'token_expires_at' => $tokenResult['expires_at'],
                'is_connected' => true,
                'connection_error' => ''
            ]);

            // Extract org_uuid if not already set
            if (empty($storeSettings->org_uuid)) {
                $orgUuid = $this->eventBookingsApi->extractOrgUuid($tokenResult['access_token']);
                if ($orgUuid) {
                    $storeSettings->update(['org_uuid' => $orgUuid]);
                }
            }

            return [
                'success' => true,
                'message' => 'Token refreshed successfully'
            ];

        } catch (\Exception $e) {
            $storeSettings->update([
                'is_connected' => false,
                'connection_error' => 'Token refresh failed: ' . $e->getMessage()
            ]);

            return [
                'success' => false,
                'message' => 'Token refresh failed: ' . $e->getMessage()
            ];
        }
    }

    /**
     * Fetch events with automatic token refresh on 401 errors
     * WordPress plugin pattern implementation
     */
    public function fetchEventsWithRetry(StoreSettings $storeSettings, array $queryParams = []): array
    {
        if (!$storeSettings->access_token || !$storeSettings->org_uuid) {
            return [
                'success' => false,
                'message' => 'No API credentials available',
                'data' => null
            ];
        }

        try {
            // First attempt with current token
            $eventsData = $this->eventBookingsApi->fetchEvents(
                $storeSettings->access_token,
                $storeSettings->org_uuid,
                $queryParams
            );

            return [
                'success' => true,
                'message' => 'Events fetched successfully',
                'data' => $eventsData
            ];

        } catch (\RuntimeException $e) {
            if (str_contains($e->getMessage(), 'Token expired or invalid')) {
                // Attempt token refresh
                $refreshResult = $this->refreshTokenIfNeeded($storeSettings);

                if ($refreshResult['success']) {
                    // Retry with refreshed token
                    try {
                        $storeSettings->refresh(); // Reload from database
                        $eventsData = $this->eventBookingsApi->fetchEvents(
                            $storeSettings->access_token,
                            $storeSettings->org_uuid,
                            $queryParams
                        );

                        return [
                            'success' => true,
                            'message' => 'Events fetched successfully after token refresh',
                            'data' => $eventsData
                        ];

                    } catch (\RuntimeException $retryException) {
                        if (str_contains($retryException->getMessage(), 'Token expired or invalid')) {
                            return [
                                'success' => false,
                                'message' => 'Authentication failed. Please reconnect EventBookings.',
                                'data' => null
                            ];
                        }
                        throw $retryException;
                    }
                } else {
                    return [
                        'success' => false,
                        'message' => 'Token refresh failed: ' . $refreshResult['message'],
                        'data' => null
                    ];
                }
            }

            return [
                'success' => false,
                'message' => $e->getMessage(),
                'data' => null
            ];
        }
    }

    /**
     * Fetch event details with retry logic
     */
    public function fetchEventDetailsWithRetry(StoreSettings $storeSettings, string $eventUuid): array
    {
        if (!$storeSettings->access_token || !$storeSettings->org_uuid) {
            return [
                'success' => false,
                'message' => 'No API credentials available',
                'data' => null
            ];
        }

        try {
            // First attempt with current token
            $eventData = $this->eventBookingsApi->fetchEventDetails(
                $storeSettings->access_token,
                $storeSettings->org_uuid,
                $eventUuid
            );

            return [
                'success' => true,
                'message' => 'Event details fetched successfully',
                'data' => $eventData
            ];

        } catch (\RuntimeException $e) {
            if (str_contains($e->getMessage(), 'Token expired or invalid')) {
                // Attempt token refresh and retry
                $refreshResult = $this->refreshTokenIfNeeded($storeSettings);

                if ($refreshResult['success']) {
                    try {
                        $storeSettings->refresh();
                        $eventData = $this->eventBookingsApi->fetchEventDetails(
                            $storeSettings->access_token,
                            $storeSettings->org_uuid,
                            $eventUuid
                        );

                        return [
                            'success' => true,
                            'message' => 'Event details fetched successfully after token refresh',
                            'data' => $eventData
                        ];

                    } catch (\RuntimeException $retryException) {
                        if (str_contains($retryException->getMessage(), 'Token expired or invalid')) {
                            return [
                                'success' => false,
                                'message' => 'Authentication failed. Please reconnect EventBookings.',
                                'data' => null
                            ];
                        }
                        throw $retryException;
                    }
                }
            }

            return [
                'success' => false,
                'message' => $e->getMessage(),
                'data' => null
            ];
        }
    }
}
```

## Shopify Admin API Integration

### Authentication

The Shopify Admin API uses OAuth 2.0 for app-based authentication.

#### OAuth Flow Implementation

```php
class ShopifyAuth
{
    private const API_VERSION = '2023-07';

    private $clientId;
    private $clientSecret;
    private $redirectUri;

    public function __construct()
    {
        $this->clientId = config('shopify.client_id');
        $this->clientSecret = config('shopify.client_secret');
        $this->redirectUri = config('shopify.redirect_uri');
    }

    /**
     * Generate OAuth authorization URL
     */
    public function getAuthUrl(string $shop, array $scopes = []): string
    {
        $shop = $this->normalizeShopDomain($shop);

        $params = [
            'client_id' => $this->clientId,
            'scope' => implode(',', $scopes),
            'redirect_uri' => $this->redirectUri,
            'state' => csrf_token(),
        ];

        return "https://{$shop}/admin/oauth/authorize?" . http_build_query($params);
    }

    /**
     * Exchange authorization code for access token
     */
    public function getAccessToken(string $shop, string $code): string
    {
        $shop = $this->normalizeShopDomain($shop);

        $client = new \GuzzleHttp\Client();

        try {
            $response = $client->post("https://{$shop}/admin/oauth/access_token", [
                'form_params' => [
                    'client_id' => $this->clientId,
                    'client_secret' => $this->clientSecret,
                    'code' => $code,
                ]
            ]);

            $data = json_decode($response->getBody()->getContents(), true);

            if (!isset($data['access_token'])) {
                throw new \RuntimeException('No access token in response');
            }

            return $data['access_token'];

        } catch (\GuzzleHttp\Exception\RequestException $e) {
            throw new \RuntimeException('Failed to get access token: ' . $e->getMessage());
        }
    }

    /**
     * Verify webhook authenticity
     */
    public function verifyWebhook(string $data, string $hmacHeader): bool
    {
        $calculatedHmac = base64_encode(hash_hmac('sha256', $data, $this->clientSecret, true));

        return hash_equals($hmacHeader, $calculatedHmac);
    }

    /**
     * Normalize shop domain format
     */
    private function normalizeShopDomain(string $shop): string
    {
        $shop = strtolower(trim($shop));

        // Remove protocol if present
        $shop = preg_replace('/^https?:\/\//', '', $shop);

        // Add .myshopify.com if not present
        if (!str_ends_with($shop, '.myshopify.com')) {
            $shop .= '.myshopify.com';
        }

        return $shop;
    }
}
```

#### Shopify API Service

```php
class ShopifyApi
{
    private const API_VERSION = '2023-07';

    private $httpClient;

    public function __construct()
    {
        $this->httpClient = new \GuzzleHttp\Client([
            'timeout' => 30,
        ]);
    }

    /**
     * Make authenticated API request to Shopify
     */
    public function makeRequest(string $shop, string $accessToken, string $endpoint, string $method = 'GET', array $data = []): array
    {
        $shop = $this->normalizeShopDomain($shop);
        $url = "https://{$shop}/admin/api/" . self::API_VERSION . "/{$endpoint}";

        $options = [
            'headers' => [
                'X-Shopify-Access-Token' => $accessToken,
                'Content-Type' => 'application/json',
            ]
        ];

        if (!empty($data) && in_array($method, ['POST', 'PUT', 'PATCH'])) {
            $options['json'] = $data;
        }

        try {
            $response = $this->httpClient->request($method, $url, $options);
            return json_decode($response->getBody()->getContents(), true);

        } catch (\GuzzleHttp\Exception\RequestException $e) {
            throw new \RuntimeException('Shopify API request failed: ' . $e->getMessage());
        }
    }

    /**
     * Get shop information
     */
    public function getShop(string $shop, string $accessToken): array
    {
        return $this->makeRequest($shop, $accessToken, 'shop.json');
    }

    /**
     * Create a webhook
     */
    public function createWebhook(string $shop, string $accessToken, array $webhookData): array
    {
        return $this->makeRequest($shop, $accessToken, 'webhooks.json', 'POST', ['webhook' => $webhookData]);
    }

    /**
     * Get all webhooks
     */
    public function getWebhooks(string $shop, string $accessToken): array
    {
        return $this->makeRequest($shop, $accessToken, 'webhooks.json');
    }

    /**
     * Delete a webhook
     */
    public function deleteWebhook(string $shop, string $accessToken, string $webhookId): array
    {
        return $this->makeRequest($shop, $accessToken, "webhooks/{$webhookId}.json", 'DELETE');
    }

    private function normalizeShopDomain(string $shop): string
    {
        $shop = strtolower(trim($shop));
        $shop = preg_replace('/^https?:\/\//', '', $shop);

        if (!str_ends_with($shop, '.myshopify.com')) {
            $shop .= '.myshopify.com';
        }

        return $shop;
    }
}
```

## Error Handling and Retry Logic

### Comprehensive Error Handler

```php
class ApiErrorHandler
{
    private $logger;
    private $maxRetries;
    private $baseDelay;

    public function __construct($maxRetries = 3, $baseDelay = 1000)
    {
        $this->logger = \Log::channel('api');
        $this->maxRetries = $maxRetries;
        $this->baseDelay = $baseDelay; // milliseconds
    }

    /**
     * Execute API call with retry logic
     */
    public function executeWithRetry(callable $apiCall, string $operation = 'API call'): mixed
    {
        $lastException = null;

        for ($attempt = 1; $attempt <= $this->maxRetries; $attempt++) {
            try {
                return $apiCall();

            } catch (\GuzzleHttp\Exception\ConnectException $e) {
                // Network connectivity issues
                $lastException = $e;
                $this->logger->warning("Network error on {$operation} (attempt {$attempt})", [
                    'error' => $e->getMessage(),
                    'attempt' => $attempt
                ]);

            } catch (\GuzzleHttp\Exception\ServerException $e) {
                // 5xx server errors
                $lastException = $e;
                $this->logger->warning("Server error on {$operation} (attempt {$attempt})", [
                    'status_code' => $e->getResponse()->getStatusCode(),
                    'error' => $e->getMessage(),
                    'attempt' => $attempt
                ]);

            } catch (\GuzzleHttp\Exception\ClientException $e) {
                // 4xx client errors - usually don't retry except for rate limiting
                $statusCode = $e->getResponse()->getStatusCode();

                if ($statusCode === 429) { // Rate limited
                    $lastException = $e;
                    $this->logger->warning("Rate limited on {$operation} (attempt {$attempt})", [
                        'attempt' => $attempt
                    ]);
                } else {
                    // Don't retry other client errors
                    throw $e;
                }

            } catch (\Exception $e) {
                // Other exceptions - log and rethrow
                $this->logger->error("Unexpected error on {$operation}", [
                    'error' => $e->getMessage(),
                    'attempt' => $attempt
                ]);
                throw $e;
            }

            // Wait before retry (exponential backoff)
            if ($attempt < $this->maxRetries) {
                $delay = $this->baseDelay * pow(2, $attempt - 1);
                $this->logger->info("Retrying {$operation} in {$delay}ms");
                usleep($delay * 1000); // Convert to microseconds
            }
        }

        // All retries exhausted
        $this->logger->error("All retries exhausted for {$operation}", [
            'max_retries' => $this->maxRetries,
            'last_error' => $lastException->getMessage()
        ]);

        throw new \RuntimeException(
            "API operation failed after {$this->maxRetries} attempts: " . $lastException->getMessage(),
            0,
            $lastException
        );
    }
}
```

## Rate Limiting and Caching

### API Rate Limiter

```php
class ApiRateLimiter
{
    private $cache;
    private $limits;

    public function __construct()
    {
        $this->cache = \Cache::store('redis');
        $this->limits = [
            'eventbookings' => ['requests' => 100, 'window' => 60], // 100 requests per minute
            'shopify' => ['requests' => 40, 'window' => 1], // 40 requests per second
        ];
    }

    /**
     * Check if API call is allowed
     */
    public function isAllowed(string $api, string $identifier): bool
    {
        $key = "rate_limit:{$api}:{$identifier}";
        $limit = $this->limits[$api] ?? ['requests' => 60, 'window' => 60];

        $current = $this->cache->get($key, 0);

        if ($current >= $limit['requests']) {
            return false;
        }

        $this->cache->put($key, $current + 1, $limit['window']);
        return true;
    }

    /**
     * Get remaining requests
     */
    public function getRemainingRequests(string $api, string $identifier): int
    {
        $key = "rate_limit:{$api}:{$identifier}";
        $limit = $this->limits[$api] ?? ['requests' => 60, 'window' => 60];

        $current = $this->cache->get($key, 0);
        return max(0, $limit['requests'] - $current);
    }
}
```

### Response Caching

```php
class ApiResponseCache
{
    private $cache;
    private $defaultTtl;

    public function __construct($defaultTtl = 300) // 5 minutes
    {
        $this->cache = \Cache::store('redis');
        $this->defaultTtl = $defaultTtl;
    }

    /**
     * Get cached response or execute API call
     */
    public function remember(string $key, callable $apiCall, int $ttl = null): mixed
    {
        $ttl = $ttl ?? $this->defaultTtl;

        return $this->cache->remember($key, $ttl, $apiCall);
    }

    /**
     * Generate cache key for API requests
     */
    public function generateKey(string $api, string $endpoint, array $params = []): string
    {
        $paramsHash = md5(serialize($params));
        return "api_cache:{$api}:" . md5($endpoint) . ":{$paramsHash}";
    }

    /**
     * Clear cache for specific API
     */
    public function clearApiCache(string $api): void
    {
        $pattern = "api_cache:{$api}:*";

        // Note: This requires a cache implementation that supports pattern deletion
        $this->cache->deletePattern($pattern);
    }
}
```

This comprehensive API integration documentation provides all the necessary patterns and implementations for connecting with both EventBookings and Shopify APIs, including proper error handling, retry logic, rate limiting, and caching strategies.