Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 1011
0.00% covered (danger)
0.00%
0 / 49
CRAP
0.00% covered (danger)
0.00%
0 / 1
EventApiController
0.00% covered (danger)
0.00%
0 / 1011
0.00% covered (danger)
0.00%
0 / 49
69960
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getIntegrationService
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 checkReadAuth
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 checkWriteAuth
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 list
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
72
 get
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 create
0.00% covered (danger)
0.00%
0 / 66
0.00% covered (danger)
0.00%
0 / 1
272
 update
0.00% covered (danger)
0.00%
0 / 55
0.00% covered (danger)
0.00%
0 / 1
462
 delete
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
30
 activate
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
42
 cancel
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
42
 duplicate
0.00% covered (danger)
0.00%
0 / 51
0.00% covered (danger)
0.00%
0 / 1
156
 archive
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
42
 unarchive
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
56
 permanentDelete
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
42
 checkConflicts
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
132
 getEventsFromDatabase
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
20
 getEventById
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 saveEventToDatabase
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
6
 updateEventInDatabase
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
6
 deleteEventFromDatabase
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 findOverlappingEvents
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 transformIntegrationsForDatabase
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
20
 saveEventIntegrations
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 updateEventIntegrations
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 deleteEventIntegrations
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 duplicateEventIntegrations
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 retryIntegration
0.00% covered (danger)
0.00%
0 / 54
0.00% covered (danger)
0.00%
0 / 1
156
 getRetryCount
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 setRetryCount
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 getRetrySessionKey
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getExistingIntegration
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 deleteIntegrationById
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 createIntegration
0.00% covered (danger)
0.00%
0 / 65
0.00% covered (danger)
0.00%
0 / 1
182
 getIntegrationByTypeForEvent
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 getIntegration
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
42
 updateIntegration
0.00% covered (danger)
0.00%
0 / 57
0.00% covered (danger)
0.00%
0 / 1
272
 updateIntegrationConfig
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 updateIntegrationForeignId
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 updateIntegrationStatusById
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 checkIntegrationConflicts
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
30
 getIntegrationStats
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
42
 getIntegrationRecordCounts
0.00% covered (danger)
0.00%
0 / 54
0.00% covered (danger)
0.00%
0 / 1
306
 disableIntegration
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 1
56
 validateCreateRequest
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
380
 setJsonContentType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 sendJsonResponse
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 sendErrorResponse
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 getJsonRequestBody
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3namespace BuyerKiosk\EventManagement\Controllers;
4
5use Exception;
6use BuyerKiosk\EventManagement\Models\Event;
7use BuyerKiosk\EventManagement\Models\IntegrationResult;
8use BuyerKiosk\EventManagement\Services\IntegrationService;
9use BuyerKiosk\EventManagement\Adapters\IntegrationException;
10
11/**
12 * EventApiController - REST API for Event Management operations
13 *
14 * Provides endpoints for managing store events including:
15 *
16 * Event CRUD:
17 * 1. GET    /api/:typeNum/events                    - List events with filtering
18 * 2. GET    /api/:typeNum/events/:eventId           - Get single event
19 * 3. POST   /api/:typeNum/events                    - Create event
20 * 4. PUT    /api/:typeNum/events/:eventId           - Update event
21 * 5. DELETE /api/:typeNum/events/:eventId           - Delete event (draft only)
22 *
23 * Event Actions:
24 * 6. POST   /api/:typeNum/events/:eventId/activate   - Activate event
25 * 7. POST   /api/:typeNum/events/:eventId/cancel     - Cancel event
26 * 8. POST   /api/:typeNum/events/:eventId/duplicate  - Duplicate event
27 * 9. POST   /api/:typeNum/events/:eventId/archive    - Archive completed/cancelled event
28 * 10. POST  /api/:typeNum/events/:eventId/unarchive  - Unarchive event
29 * 11. DELETE /api/:typeNum/events/:eventId/permanent - Permanently delete event
30 *
31 * Integration Management:
32 * 12. GET   /api/:typeNum/events/:eventId/integrations/:integrationId - Get single integration
33 * 13. PUT   /api/:typeNum/events/:eventId/integrations/:integrationId - Update integration config
34 * 14. POST  /api/:typeNum/events/:eventId/integrations/:type/retry    - Retry failed integration
35 *
36 * Conflict Detection:
37 * 15. POST  /api/:typeNum/events/check-conflicts     - Check for scheduling conflicts
38 *
39 * Authentication: All endpoints require session authentication + uri_events permission
40 *
41 * @package BuyerKiosk\EventManagement\Controllers
42 */
43class EventApiController
44{
45    /**
46     * @var \Slim\Slim Slim application instance
47     */
48    private $app;
49
50    /**
51     * @var \Store Store object
52     */
53    private $store;
54
55    /**
56     * @var string Store type number
57     */
58    private string $typeNum;
59
60    /**
61     * @var IntegrationService|null Integration service instance
62     */
63    private ?IntegrationService $integrationService = null;
64
65    /**
66     * Constructor
67     *
68     * @param \Slim\Slim $app Slim application instance
69     * @param \Store $store Store object (validated)
70     */
71    public function __construct($app, \Store $store)
72    {
73        $this->app = $app;
74        $this->store = $store;
75        $this->typeNum = $store->getTypeNum();
76    }
77
78    /**
79     * Get or create the IntegrationService instance
80     *
81     * Lazy initialization to avoid connecting to the database until needed.
82     *
83     * @return IntegrationService
84     */
85    private function getIntegrationService(): IntegrationService
86    {
87        if ($this->integrationService === null) {
88            $db = dbConnectByName($this->store->getDbName());
89            $centralDb = dbConnectByName('kiosk_buykiosk');
90            $this->integrationService = new IntegrationService($db, $centralDb, $this->store->getTimezone());
91        }
92        return $this->integrationService;
93    }
94
95    // =========================================================================
96    // AUTHENTICATION & PERMISSION CHECKS
97    // =========================================================================
98
99    /**
100     * Check session authentication and uri_events permission (read access)
101     *
102     * @return bool True if authorized, false otherwise (response already sent)
103     */
104    private function checkReadAuth(): bool
105    {
106        if (!isset($this->app->user) || !$this->app->user) {
107            $this->sendErrorResponse('Authentication required', 401, 'UNAUTHORIZED');
108            return false;
109        }
110
111        if (!$this->app->user->checkAccess('uri_events')) {
112            $this->sendErrorResponse('Access denied. Requires uri_events permission', 403, 'FORBIDDEN');
113            return false;
114        }
115
116        return true;
117    }
118
119    /**
120     * Check session authentication and uri_events_manage permission (write access)
121     *
122     * @return bool True if authorized, false otherwise (response already sent)
123     */
124    private function checkWriteAuth(): bool
125    {
126        if (!isset($this->app->user) || !$this->app->user) {
127            $this->sendErrorResponse('Authentication required', 401, 'UNAUTHORIZED');
128            return false;
129        }
130
131        if (!$this->app->user->checkAccess('uri_events_manage')) {
132            $this->sendErrorResponse('Access denied. Requires uri_events_manage permission', 403, 'FORBIDDEN');
133            return false;
134        }
135
136        return true;
137    }
138
139    // =========================================================================
140    // EVENT CRUD ENDPOINTS
141    // =========================================================================
142
143    /**
144     * GET /api/:typeNum/events
145     *
146     * List events with optional filtering by year, status, and eventType.
147     *
148     * Query Parameters:
149     * - year: int (optional) - Filter by event year
150     * - status: string (optional) - Filter by status (draft, scheduled, active, etc.)
151     * - eventType: string (optional) - Filter by type (season, holiday, sale, custom)
152     *
153     * Response:
154     * {
155     *   "success": true,
156     *   "events": Event[],
157     *   "total": int,
158     *   "filters": { year, status, eventType }
159     * }
160     */
161    public function list(): void
162    {
163        $this->setJsonContentType();
164
165        if (!$this->checkReadAuth()) {
166            return;
167        }
168
169        try {
170            // Build filters from query parameters
171            $filters = [];
172            $year = $this->app->request->get('year');
173            $status = $this->app->request->get('status');
174            $eventType = $this->app->request->get('eventType');
175
176            if (!empty($year)) {
177                $filters['year'] = (int) $year;
178            }
179            if (!empty($status)) {
180                if (!in_array($status, Event::getValidStatuses(), true)) {
181                    $this->sendErrorResponse('Invalid status filter', 400, 'VALIDATION_ERROR');
182                    return;
183                }
184                $filters['status'] = $status;
185            }
186            if (!empty($eventType)) {
187                if (!in_array($eventType, Event::getValidTypes(), true)) {
188                    $this->sendErrorResponse('Invalid eventType filter', 400, 'VALIDATION_ERROR');
189                    return;
190                }
191                $filters['eventType'] = $eventType;
192            }
193
194            // Get events from database
195            $events = $this->getEventsFromDatabase($filters);
196
197            // Convert events to array format
198            $eventsArray = array_map(fn(Event $e) => $e->toArray(), $events);
199
200            $response = [
201                'success' => true,
202                'events' => $eventsArray,
203                'total' => count($eventsArray),
204                'filters' => $filters,
205            ];
206
207            $this->sendJsonResponse($response);
208
209        } catch (Exception $e) {
210            error_log("EventApiController::list error: " . $e->getMessage());
211            $this->sendErrorResponse('Failed to retrieve events', 500);
212        }
213    }
214
215    /**
216     * GET /api/:typeNum/events/:eventId
217     *
218     * Get a single event by ID.
219     *
220     * @param int $eventId Event ID
221     *
222     * Response:
223     * {
224     *   "success": true,
225     *   "event": Event
226     * }
227     */
228    public function get(int $eventId): void
229    {
230        $this->setJsonContentType();
231
232        if (!$this->checkReadAuth()) {
233            return;
234        }
235
236        try {
237            $event = $this->getEventById($eventId);
238
239            if (!$event) {
240                $this->sendErrorResponse('Event not found', 404, 'NOT_FOUND');
241                return;
242            }
243
244            $response = [
245                'success' => true,
246                'event' => $event->toArray(),
247            ];
248
249            $this->sendJsonResponse($response);
250
251        } catch (Exception $e) {
252            error_log("EventApiController::get error: " . $e->getMessage());
253            $this->sendErrorResponse('Failed to retrieve event', 500);
254        }
255    }
256
257    /**
258     * POST /api/:typeNum/events
259     *
260     * Create a new event.
261     *
262     * Request Body:
263     * - name: string (required)
264     * - eventType: string (required) - 'season', 'holiday', 'sale', 'custom'
265     * - startDate: string (required) - ISO date format
266     * - endDate: string (required) - ISO date format
267     * - description: string (optional)
268     * - buildUpDays: int (optional, default 14)
269     * - windDownDays: int (optional, default 7)
270     * - color: string (optional) - Hex color code
271     * - icon: string (optional)
272     * - isRecurring: bool (optional, default false)
273     * - integrations: array (optional)
274     *
275     * Response (201):
276     * {
277     *   "success": true,
278     *   "event": Event,
279     *   "integrationResults": IntegrationResult[],
280     *   "summary": { "total_enabled": int, "created": int, "failed": int, "skipped": int }
281     * }
282     */
283    public function create(): void
284    {
285        $this->setJsonContentType();
286
287        if (!$this->checkWriteAuth()) {
288            return;
289        }
290
291        try {
292            $data = $this->getJsonRequestBody();
293            if ($data === null) {
294                $this->sendErrorResponse('Invalid JSON in request body', 400, 'VALIDATION_ERROR');
295                return;
296            }
297
298            // Validate required fields
299            $validation = $this->validateCreateRequest($data);
300            if ($validation !== null) {
301                $this->sendErrorResponse($validation['error'], $validation['status'], 'VALIDATION_ERROR');
302                return;
303            }
304
305            // Create event object
306            $event = new Event();
307            $event->name = trim($data['name']);
308            $event->eventType = $data['eventType'];
309            $event->startDate = new \DateTime($data['startDate']);
310            $event->endDate = new \DateTime($data['endDate']);
311            $event->description = $data['description'] ?? null;
312            $event->buildUpDays = isset($data['buildUpDays']) ? (int) $data['buildUpDays'] : 14;
313            $event->windDownDays = isset($data['windDownDays']) ? (int) $data['windDownDays'] : 7;
314            $event->color = $data['color'] ?? null;
315            $event->icon = $data['icon'] ?? null;
316            $event->isRecurring = (bool) ($data['isRecurring'] ?? false);
317            $event->year = (int) $event->startDate->format('Y');
318            $event->status = Event::STATUS_DRAFT;
319            $event->phase = $event->calculatePhase();
320            $event->createdBy = $this->app->user->id ?? 0;
321
322            // Validate the event
323            $validationErrors = $event->validate();
324            if (!empty($validationErrors)) {
325                $this->sendErrorResponse(implode(', ', $validationErrors), 400, 'VALIDATION_ERROR');
326                return;
327            }
328
329            // Save to database
330            $eventId = $this->saveEventToDatabase($event);
331            $event->id = $eventId;
332
333            // Process integrations and collect results
334            $integrationResults = [];
335            $summary = ['total_enabled' => 0, 'created' => 0, 'failed' => 0, 'skipped' => 0];
336
337            if (!empty($data['integrations']) && is_array($data['integrations'])) {
338                $integrationService = $this->getIntegrationService();
339
340                foreach ($data['integrations'] as $type => $config) {
341                    // Skip if not enabled
342                    if (empty($config['enabled'])) {
343                        continue;
344                    }
345
346                    $summary['total_enabled']++;
347
348                    // Map frontend key to database type
349                    $dbType = self::INTEGRATION_TYPE_MAP[$type] ?? null;
350                    if (!$dbType) {
351                        $integrationResults[] = IntegrationResult::skipped($type, "Unknown integration type: {$type}");
352                        $summary['skipped']++;
353                        continue;
354                    }
355
356                    try {
357                        $integration = $integrationService->createIntegration($event, $dbType, $config);
358                        $integrationResults[] = IntegrationResult::created($dbType, $integration->id);
359                        $summary['created']++;
360                    } catch (IntegrationException $e) {
361                        $integrationResults[] = IntegrationResult::failed($e->getIntegrationType(), $e->getMessage());
362                        $summary['failed']++;
363                        error_log("EventApiController::create integration error: " . $e->getDetailedMessage());
364                    }
365                }
366            }
367
368            // Fetch the saved event with integrations
369            $savedEvent = $this->getEventById($eventId);
370
371            $response = [
372                'success' => true,
373                'event' => $savedEvent ? $savedEvent->toArray() : null,
374                'integrationResults' => array_map(fn($r) => $r->toArray(), $integrationResults),
375                'summary' => $summary,
376            ];
377
378            $this->app->response->setStatus(201);
379            $this->sendJsonResponse($response);
380
381        } catch (\InvalidArgumentException $e) {
382            $this->sendErrorResponse($e->getMessage(), 400, 'VALIDATION_ERROR');
383        } catch (Exception $e) {
384            error_log("EventApiController::create error: " . $e->getMessage() . " | Trace: " . $e->getTraceAsString());
385            $this->sendErrorResponse('Failed to create event: ' . $e->getMessage(), 500);
386        }
387    }
388
389    /**
390     * PUT /api/:typeNum/events/:eventId
391     *
392     * Update an existing event.
393     *
394     * @param int $eventId Event ID
395     *
396     * Request Body: Same as create, all fields optional
397     *
398     * Response:
399     * {
400     *   "success": true,
401     *   "event": Event
402     * }
403     */
404    public function update(int $eventId): void
405    {
406        $this->setJsonContentType();
407
408        if (!$this->checkWriteAuth()) {
409            return;
410        }
411
412        try {
413            $data = $this->getJsonRequestBody();
414            if ($data === null) {
415                $this->sendErrorResponse('Invalid JSON in request body', 400, 'VALIDATION_ERROR');
416                return;
417            }
418
419            $event = $this->getEventById($eventId);
420            if (!$event) {
421                $this->sendErrorResponse('Event not found', 404, 'NOT_FOUND');
422                return;
423            }
424
425            // Update fields if provided
426            if (isset($data['name'])) {
427                $event->name = trim($data['name']);
428            }
429            if (isset($data['eventType'])) {
430                if (!in_array($data['eventType'], Event::getValidTypes(), true)) {
431                    $this->sendErrorResponse('Invalid event type', 400, 'VALIDATION_ERROR');
432                    return;
433                }
434                $event->eventType = $data['eventType'];
435            }
436            if (isset($data['startDate'])) {
437                $event->startDate = new \DateTime($data['startDate']);
438                $event->year = (int) $event->startDate->format('Y');
439            }
440            if (isset($data['endDate'])) {
441                $event->endDate = new \DateTime($data['endDate']);
442            }
443            if (array_key_exists('description', $data)) {
444                $event->description = $data['description'];
445            }
446            if (isset($data['buildUpDays'])) {
447                $event->buildUpDays = (int) $data['buildUpDays'];
448            }
449            if (isset($data['windDownDays'])) {
450                $event->windDownDays = (int) $data['windDownDays'];
451            }
452            if (array_key_exists('color', $data)) {
453                $event->color = $data['color'];
454            }
455            if (array_key_exists('icon', $data)) {
456                $event->icon = $data['icon'];
457            }
458            if (isset($data['isRecurring'])) {
459                $event->isRecurring = (bool) $data['isRecurring'];
460            }
461
462            // Recalculate phase
463            $event->phase = $event->calculatePhase();
464
465            // Validate the event
466            $validationErrors = $event->validate();
467            if (!empty($validationErrors)) {
468                $this->sendErrorResponse(implode(', ', $validationErrors), 400, 'VALIDATION_ERROR');
469                return;
470            }
471
472            // Update in database
473            $this->updateEventInDatabase($event);
474
475            // Handle integrations if provided
476            if (isset($data['integrations']) && is_array($data['integrations'])) {
477                $transformedIntegrations = $this->transformIntegrationsForDatabase($data['integrations']);
478                $this->updateEventIntegrations($eventId, $transformedIntegrations);
479            }
480
481            // Fetch the updated event
482            $updatedEvent = $this->getEventById($eventId);
483
484            $response = [
485                'success' => true,
486                'event' => $updatedEvent ? $updatedEvent->toArray() : null,
487            ];
488
489            $this->sendJsonResponse($response);
490
491        } catch (\InvalidArgumentException $e) {
492            $this->sendErrorResponse($e->getMessage(), 400, 'VALIDATION_ERROR');
493        } catch (Exception $e) {
494            error_log("EventApiController::update error: " . $e->getMessage());
495            $this->sendErrorResponse('Failed to update event', 500);
496        }
497    }
498
499    /**
500     * DELETE /api/:typeNum/events/:eventId
501     *
502     * Delete an event. Only draft events can be deleted with this endpoint.
503     * Use /permanent for force deletion.
504     *
505     * @param int $eventId Event ID
506     *
507     * Response:
508     * {
509     *   "success": true
510     * }
511     */
512    public function delete(int $eventId): void
513    {
514        $this->setJsonContentType();
515
516        if (!$this->checkWriteAuth()) {
517            return;
518        }
519
520        try {
521            $event = $this->getEventById($eventId);
522            if (!$event) {
523                $this->sendErrorResponse('Event not found', 404, 'NOT_FOUND');
524                return;
525            }
526
527            // Only draft events can be deleted normally
528            if ($event->status !== Event::STATUS_DRAFT) {
529                $this->sendErrorResponse(
530                    'Only draft events can be deleted. Use archive for completed events or cancel for scheduled events.',
531                    400,
532                    'INVALID_STATUS'
533                );
534                return;
535            }
536
537            $this->deleteEventFromDatabase($eventId);
538
539            $response = [
540                'success' => true,
541            ];
542
543            $this->sendJsonResponse($response);
544
545        } catch (Exception $e) {
546            error_log("EventApiController::delete error: " . $e->getMessage());
547            $this->sendErrorResponse('Failed to delete event', 500);
548        }
549    }
550
551    // =========================================================================
552    // EVENT ACTION ENDPOINTS
553    // =========================================================================
554
555    /**
556     * POST /api/:typeNum/events/:eventId/activate
557     *
558     * Activate a scheduled event.
559     *
560     * @param int $eventId Event ID
561     *
562     * Response:
563     * {
564     *   "success": true,
565     *   "event": Event
566     * }
567     */
568    public function activate(int $eventId): void
569    {
570        $this->setJsonContentType();
571
572        if (!$this->checkWriteAuth()) {
573            return;
574        }
575
576        try {
577            $event = $this->getEventById($eventId);
578            if (!$event) {
579                $this->sendErrorResponse('Event not found', 404, 'NOT_FOUND');
580                return;
581            }
582
583            // Check valid transition
584            if (!$event->canTransitionTo(Event::STATUS_ACTIVE)) {
585                $this->sendErrorResponse(
586                    "Cannot activate event with status '{$event->status}'. Event must be scheduled first.",
587                    400,
588                    'INVALID_TRANSITION'
589                );
590                return;
591            }
592
593            // Update status
594            $event->previousStatus = $event->status;
595            $event->status = Event::STATUS_ACTIVE;
596            $event->phase = Event::PHASE_ACTIVE;
597
598            $this->updateEventInDatabase($event);
599
600            $updatedEvent = $this->getEventById($eventId);
601
602            $response = [
603                'success' => true,
604                'event' => $updatedEvent ? $updatedEvent->toArray() : null,
605            ];
606
607            $this->sendJsonResponse($response);
608
609        } catch (Exception $e) {
610            error_log("EventApiController::activate error: " . $e->getMessage());
611            $this->sendErrorResponse('Failed to activate event', 500);
612        }
613    }
614
615    /**
616     * POST /api/:typeNum/events/:eventId/cancel
617     *
618     * Cancel an event (draft, scheduled, or active).
619     *
620     * @param int $eventId Event ID
621     *
622     * Response:
623     * {
624     *   "success": true,
625     *   "event": Event
626     * }
627     */
628    public function cancel(int $eventId): void
629    {
630        $this->setJsonContentType();
631
632        if (!$this->checkWriteAuth()) {
633            return;
634        }
635
636        try {
637            $event = $this->getEventById($eventId);
638            if (!$event) {
639                $this->sendErrorResponse('Event not found', 404, 'NOT_FOUND');
640                return;
641            }
642
643            // Check valid transition
644            if (!$event->canTransitionTo(Event::STATUS_CANCELLED)) {
645                $this->sendErrorResponse(
646                    "Cannot cancel event with status '{$event->status}'.",
647                    400,
648                    'INVALID_TRANSITION'
649                );
650                return;
651            }
652
653            // Update status
654            $event->previousStatus = $event->status;
655            $event->status = Event::STATUS_CANCELLED;
656
657            $this->updateEventInDatabase($event);
658
659            $updatedEvent = $this->getEventById($eventId);
660
661            $response = [
662                'success' => true,
663                'event' => $updatedEvent ? $updatedEvent->toArray() : null,
664            ];
665
666            $this->sendJsonResponse($response);
667
668        } catch (Exception $e) {
669            error_log("EventApiController::cancel error: " . $e->getMessage());
670            $this->sendErrorResponse('Failed to cancel event', 500);
671        }
672    }
673
674    /**
675     * POST /api/:typeNum/events/:eventId/duplicate
676     *
677     * Duplicate an event with new dates.
678     *
679     * @param int $eventId Event ID to duplicate
680     *
681     * Request Body:
682     * - name: string (optional) - New name for duplicated event
683     * - startDate: string (required) - ISO date format
684     * - endDate: string (required) - ISO date format
685     * - includeIntegrations: bool (optional, default true)
686     *
687     * Response (201):
688     * {
689     *   "success": true,
690     *   "event": Event (the new duplicated event)
691     * }
692     */
693    public function duplicate(int $eventId): void
694    {
695        $this->setJsonContentType();
696
697        if (!$this->checkWriteAuth()) {
698            return;
699        }
700
701        try {
702            $data = $this->getJsonRequestBody();
703            if ($data === null) {
704                $this->sendErrorResponse('Invalid JSON in request body', 400, 'VALIDATION_ERROR');
705                return;
706            }
707
708            // Validate required fields
709            if (empty($data['startDate']) || empty($data['endDate'])) {
710                $this->sendErrorResponse('startDate and endDate are required', 400, 'VALIDATION_ERROR');
711                return;
712            }
713
714            $sourceEvent = $this->getEventById($eventId);
715            if (!$sourceEvent) {
716                $this->sendErrorResponse('Source event not found', 404, 'NOT_FOUND');
717                return;
718            }
719
720            // Create new event based on source
721            $newEvent = new Event();
722            $newEvent->sourceEventId = $sourceEvent->id;
723            $newEvent->templateId = $sourceEvent->templateId;
724            $newEvent->name = isset($data['name']) ? trim($data['name']) : $sourceEvent->name . ' (Copy)';
725            $newEvent->description = $sourceEvent->description;
726            $newEvent->eventType = $sourceEvent->eventType;
727            $newEvent->startDate = new \DateTime($data['startDate']);
728            $newEvent->endDate = new \DateTime($data['endDate']);
729            $newEvent->year = (int) $newEvent->startDate->format('Y');
730            $newEvent->buildUpDays = $sourceEvent->buildUpDays;
731            $newEvent->windDownDays = $sourceEvent->windDownDays;
732            $newEvent->color = $sourceEvent->color;
733            $newEvent->icon = $sourceEvent->icon;
734            $newEvent->isRecurring = $sourceEvent->isRecurring;
735            $newEvent->status = Event::STATUS_DRAFT;
736            $newEvent->phase = $newEvent->calculatePhase();
737            $newEvent->createdBy = $this->app->user->id ?? 0;
738
739            // Validate the new event
740            $validationErrors = $newEvent->validate();
741            if (!empty($validationErrors)) {
742                $this->sendErrorResponse(implode(', ', $validationErrors), 400, 'VALIDATION_ERROR');
743                return;
744            }
745
746            // Save to database
747            $newEventId = $this->saveEventToDatabase($newEvent);
748
749            // Duplicate integrations if requested
750            $includeIntegrations = $data['includeIntegrations'] ?? true;
751            if ($includeIntegrations) {
752                $this->duplicateEventIntegrations($eventId, $newEventId);
753            }
754
755            // Fetch the saved event
756            $savedEvent = $this->getEventById($newEventId);
757
758            $response = [
759                'success' => true,
760                'event' => $savedEvent ? $savedEvent->toArray() : null,
761            ];
762
763            $this->app->response->setStatus(201);
764            $this->sendJsonResponse($response);
765
766        } catch (\InvalidArgumentException $e) {
767            $this->sendErrorResponse($e->getMessage(), 400, 'VALIDATION_ERROR');
768        } catch (Exception $e) {
769            error_log("EventApiController::duplicate error: " . $e->getMessage());
770            $this->sendErrorResponse('Failed to duplicate event', 500);
771        }
772    }
773
774    /**
775     * POST /api/:typeNum/events/:eventId/archive
776     *
777     * Archive a completed or cancelled event.
778     *
779     * @param int $eventId Event ID
780     *
781     * Response:
782     * {
783     *   "success": true,
784     *   "event": Event
785     * }
786     */
787    public function archive(int $eventId): void
788    {
789        $this->setJsonContentType();
790
791        if (!$this->checkWriteAuth()) {
792            return;
793        }
794
795        try {
796            $event = $this->getEventById($eventId);
797            if (!$event) {
798                $this->sendErrorResponse('Event not found', 404, 'NOT_FOUND');
799                return;
800            }
801
802            // Check valid transition
803            if (!$event->canTransitionTo(Event::STATUS_ARCHIVED)) {
804                $this->sendErrorResponse(
805                    "Cannot archive event with status '{$event->status}'. Only completed or cancelled events can be archived.",
806                    400,
807                    'INVALID_TRANSITION'
808                );
809                return;
810            }
811
812            // Update status
813            $event->previousStatus = $event->status;
814            $event->status = Event::STATUS_ARCHIVED;
815            $event->archivedAt = new \DateTime();
816
817            $this->updateEventInDatabase($event);
818
819            $updatedEvent = $this->getEventById($eventId);
820
821            $response = [
822                'success' => true,
823                'event' => $updatedEvent ? $updatedEvent->toArray() : null,
824            ];
825
826            $this->sendJsonResponse($response);
827
828        } catch (Exception $e) {
829            error_log("EventApiController::archive error: " . $e->getMessage());
830            $this->sendErrorResponse('Failed to archive event', 500);
831        }
832    }
833
834    /**
835     * POST /api/:typeNum/events/:eventId/unarchive
836     *
837     * Restore an archived event to its previous status.
838     *
839     * @param int $eventId Event ID
840     *
841     * Response:
842     * {
843     *   "success": true,
844     *   "event": Event
845     * }
846     */
847    public function unarchive(int $eventId): void
848    {
849        $this->setJsonContentType();
850
851        if (!$this->checkWriteAuth()) {
852            return;
853        }
854
855        try {
856            $event = $this->getEventById($eventId);
857            if (!$event) {
858                $this->sendErrorResponse('Event not found', 404, 'NOT_FOUND');
859                return;
860            }
861
862            // Only archived events can be unarchived
863            if ($event->status !== Event::STATUS_ARCHIVED) {
864                $this->sendErrorResponse(
865                    "Cannot unarchive event with status '{$event->status}'. Only archived events can be unarchived.",
866                    400,
867                    'INVALID_TRANSITION'
868                );
869                return;
870            }
871
872            // Restore to previous status or completed
873            $newStatus = $event->previousStatus ?? Event::STATUS_COMPLETED;
874
875            // Validate the restoration status
876            if (!in_array($newStatus, [Event::STATUS_COMPLETED, Event::STATUS_CANCELLED], true)) {
877                $newStatus = Event::STATUS_COMPLETED;
878            }
879
880            $event->previousStatus = Event::STATUS_ARCHIVED;
881            $event->status = $newStatus;
882            $event->archivedAt = null;
883
884            $this->updateEventInDatabase($event);
885
886            $updatedEvent = $this->getEventById($eventId);
887
888            $response = [
889                'success' => true,
890                'event' => $updatedEvent ? $updatedEvent->toArray() : null,
891            ];
892
893            $this->sendJsonResponse($response);
894
895        } catch (Exception $e) {
896            error_log("EventApiController::unarchive error: " . $e->getMessage());
897            $this->sendErrorResponse('Failed to unarchive event', 500);
898        }
899    }
900
901    /**
902     * DELETE /api/:typeNum/events/:eventId/permanent
903     *
904     * Permanently delete an event and all associated data.
905     * Requires explicit confirmation.
906     *
907     * @param int $eventId Event ID
908     *
909     * Request Body:
910     * - confirm: bool (required, must be true)
911     *
912     * Response:
913     * {
914     *   "success": true
915     * }
916     */
917    public function permanentDelete(int $eventId): void
918    {
919        $this->setJsonContentType();
920
921        if (!$this->checkWriteAuth()) {
922            return;
923        }
924
925        try {
926            $data = $this->getJsonRequestBody();
927
928            // Require explicit confirmation
929            if (!isset($data['confirm']) || $data['confirm'] !== true) {
930                $this->sendErrorResponse(
931                    'Permanent deletion requires explicit confirmation. Set confirm: true in request body.',
932                    400,
933                    'CONFIRMATION_REQUIRED'
934                );
935                return;
936            }
937
938            $event = $this->getEventById($eventId);
939            if (!$event) {
940                $this->sendErrorResponse('Event not found', 404, 'NOT_FOUND');
941                return;
942            }
943
944            // Delete all associated data and the event
945            $this->deleteEventIntegrations($eventId);
946            $this->deleteEventFromDatabase($eventId);
947
948            $response = [
949                'success' => true,
950            ];
951
952            $this->sendJsonResponse($response);
953
954        } catch (Exception $e) {
955            error_log("EventApiController::permanentDelete error: " . $e->getMessage());
956            $this->sendErrorResponse('Failed to permanently delete event', 500);
957        }
958    }
959
960    // =========================================================================
961    // CONFLICT DETECTION ENDPOINT
962    // =========================================================================
963
964    /**
965     * POST /api/:typeNum/events/check-conflicts
966     *
967     * Check for scheduling conflicts with existing events.
968     *
969     * Request Body:
970     * - startDate: string (required) - ISO date format
971     * - endDate: string (required) - ISO date format
972     * - eventType: string (required)
973     * - eventId: int (optional) - Exclude this event from conflict check (for updates)
974     * - integrations: array (optional) - Check integration-specific conflicts
975     *
976     * Response:
977     * {
978     *   "success": true,
979     *   "hasConflicts": bool,
980     *   "conflicts": [
981     *     {
982     *       "type": "date_overlap" | "integration_conflict",
983     *       "event": Event,
984     *       "message": string
985     *     }
986     *   ]
987     * }
988     */
989    public function checkConflicts(): void
990    {
991        $this->setJsonContentType();
992
993        if (!$this->checkReadAuth()) {
994            return;
995        }
996
997        try {
998            $data = $this->getJsonRequestBody();
999            if ($data === null) {
1000                $this->sendErrorResponse('Invalid JSON in request body', 400, 'VALIDATION_ERROR');
1001                return;
1002            }
1003
1004            // Validate required fields
1005            if (empty($data['startDate']) || empty($data['endDate']) || empty($data['eventType'])) {
1006                $this->sendErrorResponse('startDate, endDate, and eventType are required', 400, 'VALIDATION_ERROR');
1007                return;
1008            }
1009
1010            $startDate = new \DateTime($data['startDate']);
1011            $endDate = new \DateTime($data['endDate']);
1012            $eventType = $data['eventType'];
1013            $excludeEventId = isset($data['eventId']) ? (int) $data['eventId'] : null;
1014            $integrations = $data['integrations'] ?? [];
1015
1016            $conflicts = [];
1017
1018            // Check for date overlaps with existing events of the same type
1019            $overlappingEvents = $this->findOverlappingEvents($startDate, $endDate, $eventType, $excludeEventId);
1020
1021            foreach ($overlappingEvents as $overlappingEvent) {
1022                $conflicts[] = [
1023                    'type' => 'date_overlap',
1024                    'event' => $overlappingEvent->toArray(),
1025                    'message' => "Date range overlaps with '{$overlappingEvent->name}' ({$overlappingEvent->startDate->format('Y-m-d')} - {$overlappingEvent->endDate->format('Y-m-d')})",
1026                ];
1027            }
1028
1029            // Check for integration-specific conflicts if integrations provided
1030            if (!empty($integrations)) {
1031                $integrationConflicts = $this->checkIntegrationConflicts($startDate, $endDate, $integrations, $excludeEventId);
1032                $conflicts = array_merge($conflicts, $integrationConflicts);
1033            }
1034
1035            $response = [
1036                'success' => true,
1037                'hasConflicts' => !empty($conflicts),
1038                'conflicts' => $conflicts,
1039            ];
1040
1041            $this->sendJsonResponse($response);
1042
1043        } catch (\InvalidArgumentException $e) {
1044            $this->sendErrorResponse($e->getMessage(), 400, 'VALIDATION_ERROR');
1045        } catch (Exception $e) {
1046            error_log("EventApiController::checkConflicts error: " . $e->getMessage());
1047            $this->sendErrorResponse('Failed to check conflicts', 500);
1048        }
1049    }
1050
1051    // =========================================================================
1052    // DATABASE OPERATIONS
1053    // =========================================================================
1054
1055    /**
1056     * Get events from database with filters
1057     *
1058     * @param array $filters Filter criteria
1059     * @return Event[] Array of Event objects
1060     */
1061    private function getEventsFromDatabase(array $filters): array
1062    {
1063        $db = dbConnectByName($this->store->getDbName());
1064
1065        $sql = "SELECT * FROM events WHERE 1=1";
1066        $params = [];
1067
1068        if (isset($filters['year'])) {
1069            $sql .= " AND year = :year";
1070            $params[':year'] = $filters['year'];
1071        }
1072
1073        if (isset($filters['status'])) {
1074            $sql .= " AND status = :status";
1075            $params[':status'] = $filters['status'];
1076        }
1077
1078        if (isset($filters['eventType'])) {
1079            $sql .= " AND eventType = :eventType";
1080            $params[':eventType'] = $filters['eventType'];
1081        }
1082
1083        $sql .= " ORDER BY startDate ASC, name ASC";
1084
1085        $stmt = $db->prepare($sql);
1086        $stmt->execute($params);
1087        $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
1088
1089        return array_map(fn($row) => Event::fromRow($row), $rows);
1090    }
1091
1092    /**
1093     * Get a single event by ID
1094     *
1095     * @param int $eventId Event ID
1096     * @return Event|null Event object or null if not found
1097     */
1098    private function getEventById(int $eventId): ?Event
1099    {
1100        $db = dbConnectByName($this->store->getDbName());
1101
1102        $sql = "SELECT * FROM events WHERE id = :id";
1103        $stmt = $db->prepare($sql);
1104        $stmt->execute([':id' => $eventId]);
1105        $row = $stmt->fetch(\PDO::FETCH_ASSOC);
1106
1107        if (!$row) {
1108            return null;
1109        }
1110
1111        return Event::fromRow($row);
1112    }
1113
1114    /**
1115     * Save a new event to the database
1116     *
1117     * @param Event $event Event to save
1118     * @return int New event ID
1119     */
1120    private function saveEventToDatabase(Event $event): int
1121    {
1122        $db = dbConnectByName($this->store->getDbName());
1123
1124        $sql = "INSERT INTO events (
1125            templateId, sourceEventId, name, description, eventType, year,
1126            startDate, endDate, buildUpDays, windDownDays,
1127            status, previousStatus, phase, color, icon, isRecurring,
1128            createdBy, created_at, updated_at
1129        ) VALUES (
1130            :templateId, :sourceEventId, :name, :description, :eventType, :year,
1131            :startDate, :endDate, :buildUpDays, :windDownDays,
1132            :status, :previousStatus, :phase, :color, :icon, :isRecurring,
1133            :createdBy, NOW(), NOW()
1134        )";
1135
1136        $stmt = $db->prepare($sql);
1137        $stmt->execute([
1138            ':templateId' => $event->templateId,
1139            ':sourceEventId' => $event->sourceEventId,
1140            ':name' => $event->name,
1141            ':description' => $event->description,
1142            ':eventType' => $event->eventType,
1143            ':year' => $event->year,
1144            ':startDate' => $event->startDate?->format('Y-m-d'),
1145            ':endDate' => $event->endDate?->format('Y-m-d'),
1146            ':buildUpDays' => $event->buildUpDays,
1147            ':windDownDays' => $event->windDownDays,
1148            ':status' => $event->status,
1149            ':previousStatus' => $event->previousStatus,
1150            ':phase' => $event->phase,
1151            ':color' => $event->color,
1152            ':icon' => $event->icon,
1153            ':isRecurring' => $event->isRecurring ? 1 : 0,
1154            ':createdBy' => $event->createdBy,
1155        ]);
1156
1157        return (int) $db->lastInsertId();
1158    }
1159
1160    /**
1161     * Update an existing event in the database
1162     *
1163     * @param Event $event Event to update
1164     */
1165    private function updateEventInDatabase(Event $event): void
1166    {
1167        $db = dbConnectByName($this->store->getDbName());
1168
1169        $sql = "UPDATE events SET
1170            name = :name,
1171            description = :description,
1172            eventType = :eventType,
1173            year = :year,
1174            startDate = :startDate,
1175            endDate = :endDate,
1176            buildUpDays = :buildUpDays,
1177            windDownDays = :windDownDays,
1178            status = :status,
1179            previousStatus = :previousStatus,
1180            phase = :phase,
1181            color = :color,
1182            icon = :icon,
1183            isRecurring = :isRecurring,
1184            archivedAt = :archivedAt,
1185            updated_at = NOW()
1186        WHERE id = :id";
1187
1188        $stmt = $db->prepare($sql);
1189        $stmt->execute([
1190            ':id' => $event->id,
1191            ':name' => $event->name,
1192            ':description' => $event->description,
1193            ':eventType' => $event->eventType,
1194            ':year' => $event->year,
1195            ':startDate' => $event->startDate?->format('Y-m-d'),
1196            ':endDate' => $event->endDate?->format('Y-m-d'),
1197            ':buildUpDays' => $event->buildUpDays,
1198            ':windDownDays' => $event->windDownDays,
1199            ':status' => $event->status,
1200            ':previousStatus' => $event->previousStatus,
1201            ':phase' => $event->phase,
1202            ':color' => $event->color,
1203            ':icon' => $event->icon,
1204            ':isRecurring' => $event->isRecurring ? 1 : 0,
1205            ':archivedAt' => $event->archivedAt?->format('Y-m-d H:i:s'),
1206        ]);
1207    }
1208
1209    /**
1210     * Delete an event from the database
1211     *
1212     * @param int $eventId Event ID to delete
1213     */
1214    private function deleteEventFromDatabase(int $eventId): void
1215    {
1216        $db = dbConnectByName($this->store->getDbName());
1217
1218        $sql = "DELETE FROM events WHERE id = :id";
1219        $stmt = $db->prepare($sql);
1220        $stmt->execute([':id' => $eventId]);
1221    }
1222
1223    /**
1224     * Find events that overlap with a given date range
1225     *
1226     * @param \DateTime $startDate Start date
1227     * @param \DateTime $endDate End date
1228     * @param string $eventType Event type to check
1229     * @param int|null $excludeEventId Event ID to exclude
1230     * @return Event[] Overlapping events
1231     */
1232    private function findOverlappingEvents(
1233        \DateTime $startDate,
1234        \DateTime $endDate,
1235        string $eventType,
1236        ?int $excludeEventId
1237    ): array {
1238        $db = dbConnectByName($this->store->getDbName());
1239
1240        $sql = "SELECT * FROM events
1241                WHERE eventType = :eventType
1242                AND status NOT IN ('cancelled', 'archived')
1243                AND (
1244                    (startDate <= :endDate AND endDate >= :startDate)
1245                )";
1246        $params = [
1247            ':eventType' => $eventType,
1248            ':startDate' => $startDate->format('Y-m-d'),
1249            ':endDate' => $endDate->format('Y-m-d'),
1250        ];
1251
1252        if ($excludeEventId !== null) {
1253            $sql .= " AND id != :exclude_id";
1254            $params[':exclude_id'] = $excludeEventId;
1255        }
1256
1257        $stmt = $db->prepare($sql);
1258        $stmt->execute($params);
1259        $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
1260
1261        return array_map(fn($row) => Event::fromRow($row), $rows);
1262    }
1263
1264    // =========================================================================
1265    // INTEGRATION OPERATIONS
1266    // =========================================================================
1267
1268    /**
1269     * Map frontend integration keys to database ENUM values
1270     */
1271    private const INTEGRATION_TYPE_MAP = [
1272        'comebackCash' => 'comeback_cash',
1273        'signage' => 'signage',
1274        'sms' => 'sms_blast',  // Default to sms_blast, sms_trigger is handled separately
1275        'tasks' => 'task',
1276        'notes' => 'note',
1277        'backstock' => 'backstock',
1278    ];
1279
1280    /**
1281     * Transform frontend integration format to database format
1282     *
1283     * Frontend sends: { "comebackCash": { "enabled": true, ... }, "signage": { "enabled": false, ... } }
1284     * Database expects: [ { "type": "comeback_cash", "config": {...} }, ... ]
1285     *
1286     * @param array $frontendIntegrations Frontend integrations object
1287     * @return array Array of integrations ready for database
1288     */
1289    private function transformIntegrationsForDatabase(array $frontendIntegrations): array
1290    {
1291        $dbIntegrations = [];
1292
1293        foreach ($frontendIntegrations as $key => $config) {
1294            // Skip if not enabled
1295            if (empty($config['enabled'])) {
1296                continue;
1297            }
1298
1299            // Map frontend key to database enum value
1300            $dbType = self::INTEGRATION_TYPE_MAP[$key] ?? null;
1301            if (!$dbType) {
1302                error_log("Unknown integration type: $key");
1303                continue;
1304            }
1305
1306            // Build the integration record
1307            $integration = [
1308                'type' => $dbType,
1309                'foreignId' => $config['foreignId'] ?? 0,
1310                'config' => $config,
1311                'status' => 'pending',
1312                'relativeDays' => $config['relativeDays'] ?? null,
1313            ];
1314
1315            $dbIntegrations[] = $integration;
1316        }
1317
1318        return $dbIntegrations;
1319    }
1320
1321    /**
1322     * Save event integrations
1323     *
1324     * @param int $eventId Event ID
1325     * @param array $integrations Integrations data
1326     */
1327    private function saveEventIntegrations(int $eventId, array $integrations): void
1328    {
1329        $db = dbConnectByName($this->store->getDbName());
1330
1331        foreach ($integrations as $integration) {
1332            $sql = "INSERT INTO event_integrations (
1333                eventId, integrationType, foreignId, config, status, relativeDays, created_at
1334            ) VALUES (
1335                :eventId, :integrationType, :foreignId, :config, :status, :relativeDays, NOW()
1336            )";
1337
1338            $stmt = $db->prepare($sql);
1339            $stmt->execute([
1340                ':eventId' => $eventId,
1341                ':integrationType' => $integration['type'] ?? '',
1342                ':foreignId' => $integration['foreignId'] ?? 0,
1343                ':config' => json_encode($integration['config'] ?? []),
1344                ':status' => $integration['status'] ?? 'pending',
1345                ':relativeDays' => $integration['relativeDays'] ?? null,
1346            ]);
1347        }
1348    }
1349
1350    /**
1351     * Update event integrations (replace all)
1352     *
1353     * @param int $eventId Event ID
1354     * @param array $integrations New integrations data
1355     */
1356    private function updateEventIntegrations(int $eventId, array $integrations): void
1357    {
1358        // Delete existing integrations
1359        $this->deleteEventIntegrations($eventId);
1360
1361        // Add new integrations
1362        $this->saveEventIntegrations($eventId, $integrations);
1363    }
1364
1365    /**
1366     * Delete all integrations for an event
1367     *
1368     * @param int $eventId Event ID
1369     */
1370    private function deleteEventIntegrations(int $eventId): void
1371    {
1372        $db = dbConnectByName($this->store->getDbName());
1373
1374        $sql = "DELETE FROM event_integrations WHERE eventId = :eventId";
1375        $stmt = $db->prepare($sql);
1376        $stmt->execute([':eventId' => $eventId]);
1377    }
1378
1379    /**
1380     * Duplicate integrations from one event to another
1381     *
1382     * @param int $sourceEventId Source event ID
1383     * @param int $targetEventId Target event ID
1384     */
1385    private function duplicateEventIntegrations(int $sourceEventId, int $targetEventId): void
1386    {
1387        $db = dbConnectByName($this->store->getDbName());
1388
1389        $sql = "INSERT INTO event_integrations (eventId, integrationType, foreignId, config, status, relativeDays, created_at)
1390                SELECT :targetId, integrationType, foreignId, config, 'pending', relativeDays, NOW()
1391                FROM event_integrations
1392                WHERE eventId = :sourceId";
1393
1394        $stmt = $db->prepare($sql);
1395        $stmt->execute([
1396            ':sourceId' => $sourceEventId,
1397            ':targetId' => $targetEventId,
1398        ]);
1399    }
1400
1401    // =========================================================================
1402    // INTEGRATION RETRY ENDPOINT
1403    // =========================================================================
1404
1405    /**
1406     * Maximum number of retry attempts allowed per integration per session
1407     */
1408    private const MAX_RETRY_ATTEMPTS = 3;
1409
1410    /**
1411     * POST /api/:typeNum/events/:eventId/integrations/:type/retry
1412     *
1413     * Retry a failed integration creation.
1414     *
1415     * @param int $eventId Event ID
1416     * @param string $type Integration type (e.g., 'comeback_cash', 'sms_blast')
1417     *
1418     * Request Body:
1419     * - config: object (optional) - Use existing config if not provided
1420     *
1421     * Response:
1422     * {
1423     *   "success": true,
1424     *   "integration": {
1425     *     "type": string,
1426     *     "status": "created" | "failed",
1427     *     "foreignId": int | null,
1428     *     "error": string | null
1429     *   },
1430     *   "retryCount": int (1-3)
1431     * }
1432     *
1433     * Business Rules:
1434     * - Maximum 3 retry attempts per integration per event creation session
1435     * - After 3 failures, show "Configure Manually" option
1436     */
1437    public function retryIntegration(int $eventId, string $type): void
1438    {
1439        $this->setJsonContentType();
1440
1441        if (!$this->checkWriteAuth()) {
1442            return;
1443        }
1444
1445        try {
1446            // Validate the event exists
1447            $event = $this->getEventById($eventId);
1448            if (!$event) {
1449                $this->sendErrorResponse('Event not found', 404, 'NOT_FOUND');
1450                return;
1451            }
1452
1453            // Validate integration type
1454            $validTypes = \BuyerKiosk\EventManagement\Models\EventIntegration::getValidTypes();
1455            if (!in_array($type, $validTypes, true)) {
1456                $this->sendErrorResponse(
1457                    'Invalid integration type: ' . $type . '. Valid types: ' . implode(', ', $validTypes),
1458                    400,
1459                    'VALIDATION_ERROR'
1460                );
1461                return;
1462            }
1463
1464            // Get retry count from session
1465            $retryCount = $this->getRetryCount($eventId, $type);
1466
1467            // Check if max retries exceeded
1468            if ($retryCount >= self::MAX_RETRY_ATTEMPTS) {
1469                $this->sendErrorResponse(
1470                    'Maximum retry attempts (' . self::MAX_RETRY_ATTEMPTS . ') exceeded for ' . $type . ' integration. Please configure manually.',
1471                    400,
1472                    'MAX_RETRIES_EXCEEDED'
1473                );
1474                return;
1475            }
1476
1477            // Increment retry count
1478            $retryCount++;
1479            $this->setRetryCount($eventId, $type, $retryCount);
1480
1481            // Get request body for optional config override
1482            $data = $this->getJsonRequestBody() ?? [];
1483
1484            // Get existing integration record if exists
1485            $existingIntegration = $this->getExistingIntegration($eventId, $type);
1486
1487            // Determine config to use
1488            $config = $data['config'] ?? null;
1489            if ($config === null && $existingIntegration) {
1490                $config = $existingIntegration->config ?? [];
1491            }
1492            if ($config === null) {
1493                $config = [];
1494            }
1495
1496            // Ensure config has 'enabled' flag
1497            $config['enabled'] = true;
1498
1499            // Try to create the integration
1500            $integrationService = $this->getIntegrationService();
1501
1502            try {
1503                $integration = $integrationService->createIntegration($event, $type, $config);
1504
1505                // Delete the old failed integration record if it exists
1506                if ($existingIntegration && $existingIntegration->id) {
1507                    $this->deleteIntegrationById($existingIntegration->id);
1508                }
1509
1510                $response = [
1511                    'success' => true,
1512                    'integration' => IntegrationResult::created($type, $integration->id)->toArray(),
1513                    'retryCount' => $retryCount,
1514                ];
1515
1516                $this->sendJsonResponse($response);
1517
1518            } catch (IntegrationException $e) {
1519                // Integration creation failed
1520                $response = [
1521                    'success' => true, // API call succeeded, but integration failed
1522                    'integration' => IntegrationResult::failed($type, $e->getMessage())->toArray(),
1523                    'retryCount' => $retryCount,
1524                ];
1525
1526                error_log("EventApiController::retryIntegration failed: " . $e->getDetailedMessage());
1527
1528                $this->sendJsonResponse($response);
1529            }
1530
1531        } catch (Exception $e) {
1532            error_log("EventApiController::retryIntegration error: " . $e->getMessage() . " | Trace: " . $e->getTraceAsString());
1533            $this->sendErrorResponse('Failed to retry integration: ' . $e->getMessage(), 500);
1534        }
1535    }
1536
1537    /**
1538     * Get the retry count for a specific event/integration combination from session
1539     *
1540     * @param int $eventId Event ID
1541     * @param string $type Integration type
1542     * @return int Current retry count
1543     */
1544    private function getRetryCount(int $eventId, string $type): int
1545    {
1546        $key = $this->getRetrySessionKey($eventId, $type);
1547
1548        if (!isset($_SESSION)) {
1549            return 0;
1550        }
1551
1552        return $_SESSION[$key] ?? 0;
1553    }
1554
1555    /**
1556     * Set the retry count for a specific event/integration combination in session
1557     *
1558     * @param int $eventId Event ID
1559     * @param string $type Integration type
1560     * @param int $count New retry count
1561     */
1562    private function setRetryCount(int $eventId, string $type, int $count): void
1563    {
1564        $key = $this->getRetrySessionKey($eventId, $type);
1565
1566        if (!isset($_SESSION)) {
1567            // Session not started, try to start it
1568            if (session_status() === PHP_SESSION_NONE) {
1569                @session_start();
1570            }
1571        }
1572
1573        if (isset($_SESSION)) {
1574            $_SESSION[$key] = $count;
1575        }
1576    }
1577
1578    /**
1579     * Generate session key for retry count tracking
1580     *
1581     * @param int $eventId Event ID
1582     * @param string $type Integration type
1583     * @return string Session key
1584     */
1585    private function getRetrySessionKey(int $eventId, string $type): string
1586    {
1587        return "event_integration_retry_{$this->typeNum}_{$eventId}_{$type}";
1588    }
1589
1590    /**
1591     * Get existing integration record for an event and type
1592     *
1593     * @param int $eventId Event ID
1594     * @param string $type Integration type
1595     * @return \BuyerKiosk\EventManagement\Models\EventIntegration|null
1596     */
1597    private function getExistingIntegration(int $eventId, string $type): ?\BuyerKiosk\EventManagement\Models\EventIntegration
1598    {
1599        $db = dbConnectByName($this->store->getDbName());
1600
1601        $sql = "SELECT * FROM event_integrations WHERE eventId = :eventId AND integrationType = :type LIMIT 1";
1602        $stmt = $db->prepare($sql);
1603        $stmt->execute([
1604            ':eventId' => $eventId,
1605            ':type' => $type,
1606        ]);
1607        $row = $stmt->fetch(\PDO::FETCH_ASSOC);
1608
1609        if (!$row) {
1610            return null;
1611        }
1612
1613        return \BuyerKiosk\EventManagement\Models\EventIntegration::fromRow($row);
1614    }
1615
1616    /**
1617     * Delete an integration record by ID
1618     *
1619     * @param int $integrationId Integration ID
1620     */
1621    private function deleteIntegrationById(int $integrationId): void
1622    {
1623        $db = dbConnectByName($this->store->getDbName());
1624
1625        $sql = "DELETE FROM event_integrations WHERE id = :id";
1626        $stmt = $db->prepare($sql);
1627        $stmt->execute([':id' => $integrationId]);
1628    }
1629
1630    // =========================================================================
1631    // SINGLE INTEGRATION ENDPOINTS
1632    // =========================================================================
1633
1634    /**
1635     * POST /api/:typeNum/events/:eventId/integrations
1636     *
1637     * Create a new integration for an existing event.
1638     *
1639     * @param int $eventId Event ID
1640     *
1641     * Request Body:
1642     * {
1643     *   "integrationType": "comeback_cash",
1644     *   "config": { ... optional initial configuration ... }
1645     * }
1646     *
1647     * Response:
1648     * {
1649     *   "success": true,
1650     *   "integration": {
1651     *     "id": 123,
1652     *     "integrationType": "comeback_cash",
1653     *     "status": "pending",
1654     *     "config": { ... },
1655     *     "foreignId": null
1656     *   }
1657     * }
1658     */
1659    public function createIntegration(int $eventId): void
1660    {
1661        $this->setJsonContentType();
1662
1663        if (!$this->checkWriteAuth()) {
1664            return;
1665        }
1666
1667        try {
1668            // Parse request body
1669            $data = $this->getJsonRequestBody();
1670            if ($data === null) {
1671                $this->sendErrorResponse('Invalid JSON in request body', 400, 'VALIDATION_ERROR');
1672                return;
1673            }
1674
1675            // Validate required fields
1676            if (!isset($data['integrationType']) || empty($data['integrationType'])) {
1677                $this->sendErrorResponse('integrationType is required', 400, 'VALIDATION_ERROR');
1678                return;
1679            }
1680
1681            $integrationType = $data['integrationType'];
1682            $config = $data['config'] ?? [];
1683
1684            // Validate integration type
1685            $validTypes = [
1686                \BuyerKiosk\EventManagement\Models\EventIntegration::TYPE_COMEBACK_CASH,
1687                \BuyerKiosk\EventManagement\Models\EventIntegration::TYPE_SIGNAGE,
1688                \BuyerKiosk\EventManagement\Models\EventIntegration::TYPE_SMS_BLAST,
1689                \BuyerKiosk\EventManagement\Models\EventIntegration::TYPE_SMS_TRIGGER,
1690                \BuyerKiosk\EventManagement\Models\EventIntegration::TYPE_TASK,
1691                \BuyerKiosk\EventManagement\Models\EventIntegration::TYPE_NOTE,
1692                \BuyerKiosk\EventManagement\Models\EventIntegration::TYPE_BACKSTOCK,
1693            ];
1694
1695            if (!in_array($integrationType, $validTypes)) {
1696                $this->sendErrorResponse('Invalid integrationType: ' . $integrationType, 400, 'VALIDATION_ERROR');
1697                return;
1698            }
1699
1700            // Validate the event exists
1701            $event = $this->getEventById($eventId);
1702            if (!$event) {
1703                $this->sendErrorResponse('Event not found', 404, 'NOT_FOUND');
1704                return;
1705            }
1706
1707            // Check if event can accept new integrations (not completed or cancelled)
1708            if (in_array($event->status, ['completed', 'cancelled'])) {
1709                $this->sendErrorResponse('Cannot add integrations to a ' . $event->status . ' event', 400, 'INVALID_STATUS');
1710                return;
1711            }
1712
1713            // Check if this integration type already exists for this event
1714            $existingIntegration = $this->getIntegrationByTypeForEvent($eventId, $integrationType);
1715            if ($existingIntegration) {
1716                $this->sendErrorResponse(
1717                    'An integration of type "' . $integrationType . '" already exists for this event',
1718                    400,
1719                    'DUPLICATE_INTEGRATION'
1720                );
1721                return;
1722            }
1723
1724            // Ensure config has enabled flag
1725            if (!isset($config['enabled'])) {
1726                $config['enabled'] = true;
1727            }
1728
1729            // Insert the integration record
1730            $db = dbConnectByName($this->store->getDbName());
1731            $sql = "INSERT INTO event_integrations
1732                    (eventId, integrationType, status, config, created_at, updated_at)
1733                    VALUES (:eventId, :integrationType, :status, :config, NOW(), NOW())";
1734            $stmt = $db->prepare($sql);
1735            $stmt->execute([
1736                ':eventId' => $eventId,
1737                ':integrationType' => $integrationType,
1738                ':status' => \BuyerKiosk\EventManagement\Models\EventIntegration::STATUS_PENDING,
1739                ':config' => json_encode($config),
1740            ]);
1741
1742            $integrationId = (int) $db->lastInsertId();
1743
1744            // Fetch the created integration
1745            $integrationService = $this->getIntegrationService();
1746            $integration = $integrationService->getIntegration($integrationId);
1747
1748            $response = [
1749                'success' => true,
1750                'integration' => $integration ? $integration->toArray() : null,
1751            ];
1752
1753            $this->sendJsonResponse($response, 201);
1754
1755        } catch (IntegrationException $e) {
1756            error_log("EventApiController::createIntegration error: " . $e->getDetailedMessage());
1757            $this->sendErrorResponse('Failed to create integration: ' . $e->getMessage(), 500);
1758        } catch (Exception $e) {
1759            error_log("EventApiController::createIntegration error: " . $e->getMessage());
1760            $this->sendErrorResponse('Failed to create integration', 500);
1761        }
1762    }
1763
1764    /**
1765     * Check if an integration of a specific type already exists for an event
1766     *
1767     * @param int $eventId Event ID
1768     * @param string $integrationType Integration type
1769     * @return bool True if exists
1770     */
1771    private function getIntegrationByTypeForEvent(int $eventId, string $integrationType): bool
1772    {
1773        $db = dbConnectByName($this->store->getDbName());
1774
1775        $sql = "SELECT id FROM event_integrations WHERE eventId = :eventId AND integrationType = :integrationType LIMIT 1";
1776        $stmt = $db->prepare($sql);
1777        $stmt->execute([
1778            ':eventId' => $eventId,
1779            ':integrationType' => $integrationType,
1780        ]);
1781
1782        return $stmt->fetch() !== false;
1783    }
1784
1785    /**
1786     * GET /api/:typeNum/events/:eventId/integrations/:integrationId
1787     *
1788     * Get a single integration by ID for wizard pre-population.
1789     *
1790     * @param int $eventId Event ID
1791     * @param int $integrationId Integration ID
1792     *
1793     * Response:
1794     * {
1795     *   "id": 123,
1796     *   "integrationType": "comeback_cash",
1797     *   "status": "pending",
1798     *   "config": { ... full config ... },
1799     *   "foreignId": 456,
1800     *   "createdAt": "2025-12-06T10:00:00Z",
1801     *   "updatedAt": "2025-12-06T10:30:00Z"
1802     * }
1803     */
1804    public function getIntegration(int $eventId, int $integrationId): void
1805    {
1806        $this->setJsonContentType();
1807
1808        if (!$this->checkReadAuth()) {
1809            return;
1810        }
1811
1812        try {
1813            // Validate the event exists
1814            $event = $this->getEventById($eventId);
1815            if (!$event) {
1816                $this->sendErrorResponse('Event not found', 404, 'NOT_FOUND');
1817                return;
1818            }
1819
1820            // Get the integration
1821            $integrationService = $this->getIntegrationService();
1822            $integration = $integrationService->getIntegration($integrationId);
1823
1824            if (!$integration) {
1825                $this->sendErrorResponse('Integration not found', 404, 'NOT_FOUND');
1826                return;
1827            }
1828
1829            // Verify integration belongs to the specified event
1830            if ($integration->eventId !== $eventId) {
1831                $this->sendErrorResponse('Integration does not belong to this event', 404, 'NOT_FOUND');
1832                return;
1833            }
1834
1835            // Return the integration data
1836            $response = $integration->toArray();
1837            $this->sendJsonResponse($response);
1838
1839        } catch (Exception $e) {
1840            error_log("EventApiController::getIntegration error: " . $e->getMessage());
1841            $this->sendErrorResponse('Failed to retrieve integration', 500);
1842        }
1843    }
1844
1845    /**
1846     * PUT /api/:typeNum/events/:eventId/integrations/:integrationId
1847     *
1848     * Update an integration configuration.
1849     *
1850     * @param int $eventId Event ID
1851     * @param int $integrationId Integration ID
1852     *
1853     * Request Body:
1854     * {
1855     *   "config": { ... full configuration object ... }
1856     * }
1857     *
1858     * Response:
1859     * {
1860     *   "success": true,
1861     *   "integration": {
1862     *     "id": 123,
1863     *     "integrationType": "comeback_cash",
1864     *     "status": "pending",
1865     *     "config": { ... },
1866     *     "foreignId": 456
1867     *   }
1868     * }
1869     */
1870    public function updateIntegration(int $eventId, int $integrationId): void
1871    {
1872        $this->setJsonContentType();
1873
1874        if (!$this->checkWriteAuth()) {
1875            return;
1876        }
1877
1878        try {
1879            // Parse request body
1880            $data = $this->getJsonRequestBody();
1881            if ($data === null) {
1882                $this->sendErrorResponse('Invalid JSON in request body', 400, 'VALIDATION_ERROR');
1883                return;
1884            }
1885
1886            // Validate config is provided
1887            if (!isset($data['config']) || !is_array($data['config'])) {
1888                $this->sendErrorResponse('config is required and must be an object', 400, 'VALIDATION_ERROR');
1889                return;
1890            }
1891
1892            // Validate the event exists
1893            $event = $this->getEventById($eventId);
1894            if (!$event) {
1895                $this->sendErrorResponse('Event not found', 404, 'NOT_FOUND');
1896                return;
1897            }
1898
1899            // Get the integration
1900            $integrationService = $this->getIntegrationService();
1901            $integration = $integrationService->getIntegration($integrationId);
1902
1903            if (!$integration) {
1904                $this->sendErrorResponse('Integration not found', 404, 'NOT_FOUND');
1905                return;
1906            }
1907
1908            // Verify integration belongs to the specified event
1909            if ($integration->eventId !== $eventId) {
1910                $this->sendErrorResponse('Integration does not belong to this event', 404, 'NOT_FOUND');
1911                return;
1912            }
1913
1914            // Get the adapter and validate the new config
1915            $adapter = $integrationService->getAdapter($integration->integrationType);
1916            $config = $data['config'];
1917
1918            // Ensure config has enabled flag for validation
1919            if (!isset($config['enabled'])) {
1920                $config['enabled'] = true;
1921            }
1922
1923            $errors = $adapter->validateConfig($config);
1924            if (!empty($errors)) {
1925                $this->sendErrorResponse(
1926                    'Invalid configuration: ' . implode(', ', $errors),
1927                    400,
1928                    'VALIDATION_ERROR'
1929                );
1930                return;
1931            }
1932
1933            // Update the integration record in the database
1934            $this->updateIntegrationConfig($integrationId, $config);
1935
1936            // If integration has a foreignId, update in target system via adapter
1937            if ($integration->foreignId > 0) {
1938                try {
1939                    // Check if adapter has an update method
1940                    if (method_exists($adapter, 'update')) {
1941                        $adapter->update($event, $integration, $config);
1942                    } else {
1943                        // Fallback: delete and recreate
1944                        // First, delete the old record in the target system
1945                        $adapter->delete($integration);
1946
1947                        // Create new record with updated config
1948                        $newForeignId = $adapter->create($event, $config);
1949
1950                        // Update the foreignId in the integration record
1951                        $this->updateIntegrationForeignId($integrationId, $newForeignId);
1952                        $integration->foreignId = $newForeignId;
1953                    }
1954                } catch (IntegrationException $e) {
1955                    // Log but don't fail the whole operation - config was saved
1956                    error_log("EventApiController::updateIntegration adapter update failed: " . $e->getMessage());
1957                    // Update status to indicate sync issue
1958                    $this->updateIntegrationStatusById($integrationId, \BuyerKiosk\EventManagement\Models\EventIntegration::STATUS_PENDING);
1959                }
1960            }
1961
1962            // Fetch the updated integration
1963            $updatedIntegration = $integrationService->getIntegration($integrationId);
1964
1965            $response = [
1966                'success' => true,
1967                'integration' => $updatedIntegration ? $updatedIntegration->toArray() : null,
1968            ];
1969
1970            $this->sendJsonResponse($response);
1971
1972        } catch (IntegrationException $e) {
1973            error_log("EventApiController::updateIntegration error: " . $e->getDetailedMessage());
1974            $this->sendErrorResponse('Failed to update integration: ' . $e->getMessage(), 500);
1975        } catch (Exception $e) {
1976            error_log("EventApiController::updateIntegration error: " . $e->getMessage());
1977            $this->sendErrorResponse('Failed to update integration', 500);
1978        }
1979    }
1980
1981    /**
1982     * Update integration configuration in the database
1983     *
1984     * @param int $integrationId Integration ID
1985     * @param array $config New configuration
1986     */
1987    private function updateIntegrationConfig(int $integrationId, array $config): void
1988    {
1989        $db = dbConnectByName($this->store->getDbName());
1990
1991        $sql = "UPDATE event_integrations SET config = :config WHERE id = :id";
1992        $stmt = $db->prepare($sql);
1993        $stmt->execute([
1994            ':id' => $integrationId,
1995            ':config' => json_encode($config),
1996        ]);
1997    }
1998
1999    /**
2000     * Update integration foreignId in the database
2001     *
2002     * @param int $integrationId Integration ID
2003     * @param int $foreignId New foreign ID
2004     */
2005    private function updateIntegrationForeignId(int $integrationId, int $foreignId): void
2006    {
2007        $db = dbConnectByName($this->store->getDbName());
2008
2009        $sql = "UPDATE event_integrations SET foreignId = :foreignId WHERE id = :id";
2010        $stmt = $db->prepare($sql);
2011        $stmt->execute([
2012            ':id' => $integrationId,
2013            ':foreignId' => $foreignId,
2014        ]);
2015    }
2016
2017    /**
2018     * Update integration status in the database
2019     *
2020     * @param int $integrationId Integration ID
2021     * @param string $status New status
2022     */
2023    private function updateIntegrationStatusById(int $integrationId, string $status): void
2024    {
2025        $db = dbConnectByName($this->store->getDbName());
2026
2027        $sql = "UPDATE event_integrations SET status = :status WHERE id = :id";
2028        $stmt = $db->prepare($sql);
2029        $stmt->execute([
2030            ':id' => $integrationId,
2031            ':status' => $status,
2032        ]);
2033    }
2034
2035    /**
2036     * Check for integration-specific conflicts
2037     *
2038     * @param \DateTime $startDate Start date
2039     * @param \DateTime $endDate End date
2040     * @param array $integrations Integrations to check
2041     * @param int|null $excludeEventId Event ID to exclude
2042     * @return array Conflict information
2043     */
2044    private function checkIntegrationConflicts(
2045        \DateTime $startDate,
2046        \DateTime $endDate,
2047        array $integrations,
2048        ?int $excludeEventId
2049    ): array {
2050        $conflicts = [];
2051        $db = dbConnectByName($this->store->getDbName());
2052
2053        foreach ($integrations as $integration) {
2054            $integrationType = $integration['type'] ?? '';
2055            if (empty($integrationType)) {
2056                continue;
2057            }
2058
2059            // Find events with the same integration type that overlap
2060            $sql = "SELECT e.* FROM events e
2061                    JOIN event_integrations ei ON e.id = ei.eventId
2062                    WHERE ei.integrationType = :integrationType
2063                    AND ei.status NOT IN ('failed')
2064                    AND e.status NOT IN ('cancelled', 'archived')
2065                    AND (e.startDate <= :endDate AND e.endDate >= :startDate)";
2066            $params = [
2067                ':integrationType' => $integrationType,
2068                ':startDate' => $startDate->format('Y-m-d'),
2069                ':endDate' => $endDate->format('Y-m-d'),
2070            ];
2071
2072            if ($excludeEventId !== null) {
2073                $sql .= " AND e.id != :exclude_id";
2074                $params[':exclude_id'] = $excludeEventId;
2075            }
2076
2077            $stmt = $db->prepare($sql);
2078            $stmt->execute($params);
2079            $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
2080
2081            foreach ($rows as $row) {
2082                $event = Event::fromRow($row);
2083                $conflicts[] = [
2084                    'type' => 'integration_conflict',
2085                    'integrationtype' => $integrationType,
2086                    'event' => $event->toArray(),
2087                    'message' => "Integration '{$integrationType}' conflicts with '{$event->name}'",
2088                ];
2089            }
2090        }
2091
2092        return $conflicts;
2093    }
2094
2095    // =========================================================================
2096    // INTEGRATION STATS AND DISABLE
2097    // =========================================================================
2098
2099    /**
2100     * GET /api/:typeNum/events/:eventId/integrations/:integrationId/stats
2101     *
2102     * Get statistics for an integration (record counts for associated data).
2103     *
2104     * @param int $eventId Event ID
2105     * @param int $integrationId Integration ID
2106     */
2107    public function getIntegrationStats(int $eventId, int $integrationId): void
2108    {
2109        $this->setJsonContentType();
2110
2111        if (!$this->checkReadAuth()) {
2112            return;
2113        }
2114
2115        try {
2116            // Validate the event exists
2117            $event = $this->getEventById($eventId);
2118            if (!$event) {
2119                $this->sendErrorResponse('Event not found', 404, 'NOT_FOUND');
2120                return;
2121            }
2122
2123            // Get the integration
2124            $integrationService = $this->getIntegrationService();
2125            $integration = $integrationService->getIntegration($integrationId);
2126
2127            if (!$integration) {
2128                $this->sendErrorResponse('Integration not found', 404, 'NOT_FOUND');
2129                return;
2130            }
2131
2132            // Verify integration belongs to the specified event
2133            if ($integration->eventId !== $eventId) {
2134                $this->sendErrorResponse('Integration does not belong to this event', 404, 'NOT_FOUND');
2135                return;
2136            }
2137
2138            // Get stats based on integration type
2139            $stats = $this->getIntegrationRecordCounts($integration);
2140
2141            $response = [
2142                'success' => true,
2143                'stats' => $stats,
2144            ];
2145
2146            $this->sendJsonResponse($response);
2147
2148        } catch (Exception $e) {
2149            error_log("EventApiController::getIntegrationStats error: " . $e->getMessage());
2150            $this->sendErrorResponse('Failed to retrieve integration stats', 500);
2151        }
2152    }
2153
2154    /**
2155     * Get record counts for an integration based on its type
2156     *
2157     * @param \BuyerKiosk\EventManagement\Models\EventIntegration $integration
2158     * @return array Stats with recordCount and details
2159     */
2160    private function getIntegrationRecordCounts($integration): array
2161    {
2162        $db = dbConnectByName($this->store->getDbName());
2163        $recordCount = 0;
2164        $details = [];
2165
2166        switch ($integration->integrationType) {
2167            case \BuyerKiosk\EventManagement\Models\EventIntegration::TYPE_COMEBACK_CASH:
2168                // Count coupons issued via this integration
2169                if ($integration->foreignId > 0) {
2170                    // Check if there's a comeback_cash table with event reference
2171                    try {
2172                        $sql = "SELECT COUNT(*) as count FROM comeback_cash_coupons WHERE promotionId = :foreignId";
2173                        $stmt = $db->prepare($sql);
2174                        $stmt->execute([':foreignId' => $integration->foreignId]);
2175                        $row = $stmt->fetch(\PDO::FETCH_ASSOC);
2176                        $details['Coupons Issued'] = $row ? (int) $row['count'] : 0;
2177                        $recordCount = $details['Coupons Issued'];
2178                    } catch (\PDOException $e) {
2179                        // Table might not exist, set 0
2180                        $details['Coupons Issued'] = 0;
2181                    }
2182                }
2183                break;
2184
2185            case \BuyerKiosk\EventManagement\Models\EventIntegration::TYPE_TASK:
2186                // Count tasks created via this integration
2187                try {
2188                    $sql = "SELECT COUNT(*) as count FROM event_tasks WHERE integrationId = :integrationId";
2189                    $stmt = $db->prepare($sql);
2190                    $stmt->execute([':integrationId' => $integration->id]);
2191                    $row = $stmt->fetch(\PDO::FETCH_ASSOC);
2192                    $details['Tasks Created'] = $row ? (int) $row['count'] : 0;
2193                    $recordCount = $details['Tasks Created'];
2194                } catch (\PDOException $e) {
2195                    $details['Tasks Created'] = 0;
2196                }
2197                break;
2198
2199            case \BuyerKiosk\EventManagement\Models\EventIntegration::TYPE_NOTE:
2200                // Count notes created via this integration
2201                try {
2202                    $sql = "SELECT COUNT(*) as count FROM event_notes WHERE integrationId = :integrationId";
2203                    $stmt = $db->prepare($sql);
2204                    $stmt->execute([':integrationId' => $integration->id]);
2205                    $row = $stmt->fetch(\PDO::FETCH_ASSOC);
2206                    $details['Notes Created'] = $row ? (int) $row['count'] : 0;
2207                    $recordCount = $details['Notes Created'];
2208                } catch (\PDOException $e) {
2209                    $details['Notes Created'] = 0;
2210                }
2211                break;
2212
2213            case \BuyerKiosk\EventManagement\Models\EventIntegration::TYPE_SMS_BLAST:
2214            case \BuyerKiosk\EventManagement\Models\EventIntegration::TYPE_SMS_TRIGGER:
2215                // Count SMS messages sent via this integration
2216                try {
2217                    $sql = "SELECT COUNT(*) as count FROM sms_log WHERE eventIntegrationId = :integrationId";
2218                    $stmt = $db->prepare($sql);
2219                    $stmt->execute([':integrationId' => $integration->id]);
2220                    $row = $stmt->fetch(\PDO::FETCH_ASSOC);
2221                    $details['Messages Sent'] = $row ? (int) $row['count'] : 0;
2222                    $recordCount = $details['Messages Sent'];
2223                } catch (\PDOException $e) {
2224                    $details['Messages Sent'] = 0;
2225                }
2226                break;
2227
2228            case \BuyerKiosk\EventManagement\Models\EventIntegration::TYPE_SIGNAGE:
2229            case \BuyerKiosk\EventManagement\Models\EventIntegration::TYPE_BACKSTOCK:
2230                // These typically don't have associated records to count
2231                $recordCount = 0;
2232                break;
2233        }
2234
2235        return [
2236            'recordCount' => $recordCount,
2237            'details' => $details,
2238        ];
2239    }
2240
2241    /**
2242     * POST /api/:typeNum/events/:eventId/integrations/:integrationId/disable
2243     *
2244     * Disable an integration (soft-delete - marks as disabled, preserves data).
2245     *
2246     * @param int $eventId Event ID
2247     * @param int $integrationId Integration ID
2248     */
2249    public function disableIntegration(int $eventId, int $integrationId): void
2250    {
2251        $this->setJsonContentType();
2252
2253        if (!$this->checkWriteAuth()) {
2254            return;
2255        }
2256
2257        try {
2258            // Validate the event exists
2259            $event = $this->getEventById($eventId);
2260            if (!$event) {
2261                $this->sendErrorResponse('Event not found', 404, 'NOT_FOUND');
2262                return;
2263            }
2264
2265            // Check if event allows disabling integrations
2266            if (in_array($event->status, ['active', 'completed'])) {
2267                $this->sendErrorResponse(
2268                    'Cannot disable integrations on an ' . $event->status . ' event',
2269                    400,
2270                    'INVALID_STATUS'
2271                );
2272                return;
2273            }
2274
2275            // Get the integration
2276            $integrationService = $this->getIntegrationService();
2277            $integration = $integrationService->getIntegration($integrationId);
2278
2279            if (!$integration) {
2280                $this->sendErrorResponse('Integration not found', 404, 'NOT_FOUND');
2281                return;
2282            }
2283
2284            // Verify integration belongs to the specified event
2285            if ($integration->eventId !== $eventId) {
2286                $this->sendErrorResponse('Integration does not belong to this event', 404, 'NOT_FOUND');
2287                return;
2288            }
2289
2290            // Soft-disable: update config to mark as disabled and set status
2291            $config = $integration->configuration ?? [];
2292            $config['enabled'] = false;
2293            $config['disabledAt'] = date('c');
2294            $config['disabledBy'] = $this->app->user->id ?? null;
2295
2296            // Update the integration
2297            $db = dbConnectByName($this->store->getDbName());
2298            $sql = "UPDATE event_integrations
2299                    SET config = :config,
2300                        status = :status,
2301                        updated_at = NOW()
2302                    WHERE id = :id";
2303            $stmt = $db->prepare($sql);
2304            $stmt->execute([
2305                ':id' => $integrationId,
2306                ':config' => json_encode($config),
2307                ':status' => \BuyerKiosk\EventManagement\Models\EventIntegration::STATUS_DISABLED,
2308            ]);
2309
2310            $response = [
2311                'success' => true,
2312                'message' => 'Integration has been disabled',
2313            ];
2314
2315            $this->sendJsonResponse($response);
2316
2317        } catch (Exception $e) {
2318            error_log("EventApiController::disableIntegration error: " . $e->getMessage());
2319            $this->sendErrorResponse('Failed to disable integration', 500);
2320        }
2321    }
2322
2323    // =========================================================================
2324    // VALIDATION HELPERS
2325    // =========================================================================
2326
2327    /**
2328     * Validate create request data
2329     *
2330     * @param array $data Request data
2331     * @return array|null Validation error or null if valid
2332     */
2333    private function validateCreateRequest(array $data): ?array
2334    {
2335        // Required: name
2336        if (!isset($data['name']) || empty(trim($data['name']))) {
2337            return ['error' => 'name is required', 'status' => 400];
2338        }
2339
2340        // Required: eventType
2341        if (!isset($data['eventType']) || empty($data['eventType'])) {
2342            return ['error' => 'eventType is required', 'status' => 400];
2343        }
2344
2345        if (!in_array($data['eventType'], Event::getValidTypes(), true)) {
2346            return ['error' => 'Invalid eventType. Must be one of: ' . implode(', ', Event::getValidTypes()), 'status' => 400];
2347        }
2348
2349        // Required: startDate
2350        if (!isset($data['startDate']) || empty($data['startDate'])) {
2351            return ['error' => 'startDate is required', 'status' => 400];
2352        }
2353
2354        // Required: endDate
2355        if (!isset($data['endDate']) || empty($data['endDate'])) {
2356            return ['error' => 'endDate is required', 'status' => 400];
2357        }
2358
2359        // Validate date formats
2360        try {
2361            $startDate = new \DateTime($data['startDate']);
2362            $endDate = new \DateTime($data['endDate']);
2363
2364            if ($endDate < $startDate) {
2365                return ['error' => 'endDate must be after startDate', 'status' => 400];
2366            }
2367        } catch (\Exception $e) {
2368            return ['error' => 'Invalid date format. Use ISO 8601 format (e.g., 2024-12-25)', 'status' => 400];
2369        }
2370
2371        // Optional validation
2372        if (isset($data['color']) && $data['color'] !== null) {
2373            if (!preg_match('/^#[0-9A-Fa-f]{6}$/', $data['color'])) {
2374                return ['error' => 'color must be a valid hex color code (e.g., #FF5733)', 'status' => 400];
2375            }
2376        }
2377
2378        if (isset($data['buildUpDays']) && (int) $data['buildUpDays'] < 0) {
2379            return ['error' => 'buildUpDays cannot be negative', 'status' => 400];
2380        }
2381
2382        if (isset($data['windDownDays']) && (int) $data['windDownDays'] < 0) {
2383            return ['error' => 'windDownDays cannot be negative', 'status' => 400];
2384        }
2385
2386        return null;
2387    }
2388
2389    // =========================================================================
2390    // RESPONSE HELPERS
2391    // =========================================================================
2392
2393    /**
2394     * Set JSON content type header
2395     */
2396    private function setJsonContentType(): void
2397    {
2398        $this->app->response->headers->set('Content-Type', 'application/json');
2399    }
2400
2401    /**
2402     * Send JSON response body
2403     *
2404     * @param array $data Response data
2405     */
2406    private function sendJsonResponse(array $data): void
2407    {
2408        $this->app->response->setBody(json_encode($data));
2409    }
2410
2411    /**
2412     * Send error response
2413     *
2414     * @param string $message Error message
2415     * @param int $httpStatus HTTP status code
2416     * @param string|null $errorCode Application error code
2417     */
2418    private function sendErrorResponse(string $message, int $httpStatus, ?string $errorCode = null): void
2419    {
2420        $response = [
2421            'success' => false,
2422            'error' => $message,
2423        ];
2424
2425        if ($errorCode !== null) {
2426            $response['code'] = $errorCode;
2427        }
2428
2429        $this->app->response->setStatus($httpStatus);
2430        $this->sendJsonResponse($response);
2431    }
2432
2433    /**
2434     * Parse JSON request body
2435     *
2436     * @return array|null Parsed data or null on error
2437     */
2438    private function getJsonRequestBody(): ?array
2439    {
2440        $body = $this->app->request->getBody();
2441
2442        if (empty($body)) {
2443            // Try POST parameters as fallback
2444            $post = $this->app->request->post();
2445            if (!empty($post)) {
2446                return $post;
2447            }
2448            return null;
2449        }
2450
2451        $data = json_decode($body, true);
2452
2453        if (json_last_error() !== JSON_ERROR_NONE) {
2454            return null;
2455        }
2456
2457        return $data;
2458    }
2459}