Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 249
0.00% covered (danger)
0.00%
0 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
IntegrationService
0.00% covered (danger)
0.00%
0 / 249
0.00% covered (danger)
0.00%
0 / 19
2550
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
 getAdapter
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
90
 getSupportedTypes
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 createIntegration
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
20
 createFromTemplate
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
20
 syncAllDates
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
12
 syncDates
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 activatePending
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
20
 deactivateAll
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
30
 cascadeDelete
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
12
 deleteIntegration
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getAggregatedStatus
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
12
 getStatusCounts
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 getIntegrationsForEvent
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 getIntegration
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 updateIntegrationStatus
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 logError
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getDefaultConfig
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 validateConfig
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace BuyerKiosk\EventManagement\Services;
4
5use PDO;
6use Exception;
7use BuyerKiosk\EventManagement\Models\Event;
8use BuyerKiosk\EventManagement\Models\EventIntegration;
9use BuyerKiosk\EventManagement\Adapters\IntegrationAdapterInterface;
10use BuyerKiosk\EventManagement\Adapters\IntegrationException;
11use BuyerKiosk\EventManagement\Adapters\BackstockAdapter;
12use BuyerKiosk\EventManagement\Adapters\SmsAdapter;
13use BuyerKiosk\EventManagement\Adapters\SignageAdapter;
14use BuyerKiosk\EventManagement\Adapters\ComebackCashAdapter;
15use BuyerKiosk\EventManagement\Adapters\TaskAdapter;
16use BuyerKiosk\EventManagement\Adapters\NoteAdapter;
17
18/**
19 * IntegrationService - Orchestrates all event integration adapters
20 *
21 * This service coordinates operations across all six integration types:
22 * - Backstock (bin pull events)
23 * - SMS (blasts and triggers)
24 * - Signage (digital sign slides)
25 * - Comeback Cash (coupon events)
26 * - Tasks (staff workbook tasks)
27 * - Notes (staff announcements)
28 *
29 * Responsibilities:
30 * - Adapter registry and factory
31 * - Creating integrations from templates
32 * - Cascading date changes to all integrations
33 * - Cascading activation/deactivation
34 * - Cascading deletes
35 * - Aggregating status across integrations
36 *
37 * @package BuyerKiosk\EventManagement\Services
38 */
39class IntegrationService
40{
41    /**
42     * @var PDO Database connection for the store
43     */
44    private PDO $db;
45
46    /**
47     * @var PDO|null Central database connection (for global templates)
48     */
49    private ?PDO $centralDb;
50
51    /**
52     * @var string|null Store timezone
53     */
54    private ?string $timezone;
55
56    /**
57     * @var array<string, IntegrationAdapterInterface> Cached adapter instances
58     */
59    private array $adapters = [];
60
61    /**
62     * Constructor
63     *
64     * @param PDO $db Store database connection
65     * @param PDO|null $centralDb Central database connection (optional)
66     * @param string|null $timezone Store timezone
67     */
68    public function __construct(PDO $db, ?PDO $centralDb = null, ?string $timezone = null)
69    {
70        $this->db = $db;
71        $this->centralDb = $centralDb;
72        $this->timezone = $timezone ?? 'America/Chicago';
73    }
74
75    // =========================================================================
76    // ADAPTER REGISTRY
77    // =========================================================================
78
79    /**
80     * Get an adapter by integration type
81     *
82     * @param string $integrationType One of EventIntegration::TYPE_* constants
83     * @return IntegrationAdapterInterface
84     * @throws IntegrationException If adapter type is unknown
85     */
86    public function getAdapter(string $integrationType): IntegrationAdapterInterface
87    {
88        // Return cached adapter if available
89        if (isset($this->adapters[$integrationType])) {
90            return $this->adapters[$integrationType];
91        }
92
93        // Create and cache the adapter
94        $adapter = match ($integrationType) {
95            EventIntegration::TYPE_BACKSTOCK => new BackstockAdapter($this->db, $this->timezone),
96            EventIntegration::TYPE_SMS_BLAST,
97            EventIntegration::TYPE_SMS_TRIGGER => new SmsAdapter($this->db, $this->timezone),
98            EventIntegration::TYPE_SIGNAGE => new SignageAdapter($this->db, $this->centralDb, $this->timezone),
99            EventIntegration::TYPE_COMEBACK_CASH => new ComebackCashAdapter($this->db, $this->timezone),
100            EventIntegration::TYPE_TASK => new TaskAdapter($this->db, $this->timezone),
101            EventIntegration::TYPE_NOTE => new NoteAdapter($this->db, $this->timezone),
102            default => throw new IntegrationException(
103                "Unknown integration type: {$integrationType}",
104                $integrationType,
105                'getAdapter'
106            ),
107        };
108
109        $this->adapters[$integrationType] = $adapter;
110        return $adapter;
111    }
112
113    /**
114     * Get all supported integration types
115     *
116     * @return array
117     */
118    public function getSupportedTypes(): array
119    {
120        return EventIntegration::getValidTypes();
121    }
122
123    // =========================================================================
124    // CREATE INTEGRATIONS
125    // =========================================================================
126
127    /**
128     * Create a single integration for an event
129     *
130     * @param Event $event The event to create integration for
131     * @param string $integrationType Integration type
132     * @param array $config Configuration for the integration
133     * @return EventIntegration Created integration record
134     * @throws IntegrationException On failure
135     */
136    public function createIntegration(Event $event, string $integrationType, array $config): EventIntegration
137    {
138        $adapter = $this->getAdapter($integrationType);
139
140        // Validate configuration
141        $errors = $adapter->validateConfig($config);
142        if (!empty($errors)) {
143            throw IntegrationException::invalidConfig($integrationType, $errors);
144        }
145
146        $this->db->beginTransaction();
147
148        try {
149            // Create the record in the target system
150            $foreignId = $adapter->create($event, $config);
151
152            // Create the integration link record
153            $integration = new EventIntegration();
154            $integration->eventId = $event->id;
155            $integration->integrationType = $integrationType;
156            $integration->foreignId = $foreignId;
157            $integration->config = $config;
158            $integration->status = EventIntegration::STATUS_PENDING;
159            $integration->relativeDays = $config['relativeDays'] ?? null;
160
161            $sql = "INSERT INTO event_integrations (
162                eventId, integrationType, foreignId, config, status, relativeDays, created_at
163            ) VALUES (
164                :eventId, :integrationType, :foreignId, :config, :status, :relativeDays, NOW()
165            )";
166
167            $stmt = $this->db->prepare($sql);
168            $stmt->execute([
169                ':eventId' => $integration->eventId,
170                ':integrationType' => $integration->integrationType,
171                ':foreignId' => $integration->foreignId,
172                ':config' => json_encode($integration->config),
173                ':status' => $integration->status,
174                ':relativeDays' => $integration->relativeDays,
175            ]);
176
177            $integration->id = (int) $this->db->lastInsertId();
178
179            $this->db->commit();
180
181            return $integration;
182        } catch (Exception $e) {
183            $this->db->rollBack();
184
185            if ($e instanceof IntegrationException) {
186                throw $e;
187            }
188
189            throw IntegrationException::createFailed(
190                $integrationType,
191                $e->getMessage(),
192                ['eventId' => $event->id],
193                $e
194            );
195        }
196    }
197
198    /**
199     * Create multiple integrations from a template
200     *
201     * @param Event $event The event to create integrations for
202     * @param array $templateIntegrations Array of integration configurations from template
203     * @return array ['created' => EventIntegration[], 'failed' => array]
204     */
205    public function createFromTemplate(Event $event, array $templateIntegrations): array
206    {
207        $created = [];
208        $failed = [];
209
210        foreach ($templateIntegrations as $templateInt) {
211            try {
212                $integrationType = $templateInt['integrationType'] ?? $templateInt['type'] ?? null;
213                $config = $templateInt['config'] ?? $templateInt;
214
215                if (!$integrationType) {
216                    $failed[] = [
217                        'config' => $templateInt,
218                        'error' => 'Missing integration type',
219                    ];
220                    continue;
221                }
222
223                $integration = $this->createIntegration($event, $integrationType, $config);
224                $created[] = $integration;
225            } catch (IntegrationException $e) {
226                $failed[] = [
227                    'config' => $templateInt,
228                    'error' => $e->getMessage(),
229                ];
230            }
231        }
232
233        return [
234            'created' => $created,
235            'failed' => $failed,
236        ];
237    }
238
239    // =========================================================================
240    // SYNC DATES (CASCADE)
241    // =========================================================================
242
243    /**
244     * Sync dates for all integrations when event dates change
245     *
246     * @param Event $event The event with updated dates
247     * @return array ['synced' => int, 'failed' => array]
248     */
249    public function syncAllDates(Event $event): array
250    {
251        $integrations = $this->getIntegrationsForEvent($event->id);
252        $synced = 0;
253        $failed = [];
254
255        foreach ($integrations as $integration) {
256            try {
257                $adapter = $this->getAdapter($integration->integrationType);
258                $adapter->syncDates($event, $integration);
259                $synced++;
260            } catch (IntegrationException $e) {
261                $failed[] = [
262                    'integrationId' => $integration->id,
263                    'type' => $integration->integrationType,
264                    'error' => $e->getMessage(),
265                ];
266                $this->logError("Failed to sync dates for integration {$integration->id}", [
267                    'type' => $integration->integrationType,
268                    'error' => $e->getMessage(),
269                ]);
270            }
271        }
272
273        return [
274            'synced' => $synced,
275            'failed' => $failed,
276        ];
277    }
278
279    /**
280     * Sync dates for a single integration
281     *
282     * @param Event $event The event with updated dates
283     * @param EventIntegration $integration The integration to sync
284     * @throws IntegrationException On failure
285     */
286    public function syncDates(Event $event, EventIntegration $integration): void
287    {
288        $adapter = $this->getAdapter($integration->integrationType);
289        $adapter->syncDates($event, $integration);
290    }
291
292    // =========================================================================
293    // ACTIVATE / DEACTIVATE (CASCADE)
294    // =========================================================================
295
296    /**
297     * Activate all pending integrations for an event
298     *
299     * @param Event $event The event being activated
300     * @return array ['activated' => int, 'failed' => array]
301     */
302    public function activatePending(Event $event): array
303    {
304        $integrations = $this->getIntegrationsForEvent($event->id);
305        $activated = 0;
306        $failed = [];
307
308        foreach ($integrations as $integration) {
309            // Only activate pending integrations
310            if ($integration->status !== EventIntegration::STATUS_PENDING) {
311                continue;
312            }
313
314            try {
315                $adapter = $this->getAdapter($integration->integrationType);
316                $adapter->activate($event, $integration);
317
318                // Update integration status
319                $this->updateIntegrationStatus($integration->id, EventIntegration::STATUS_ACTIVE);
320                $activated++;
321            } catch (IntegrationException $e) {
322                // Mark as failed
323                $this->updateIntegrationStatus($integration->id, EventIntegration::STATUS_FAILED);
324                $failed[] = [
325                    'integrationId' => $integration->id,
326                    'type' => $integration->integrationType,
327                    'error' => $e->getMessage(),
328                ];
329                $this->logError("Failed to activate integration {$integration->id}", [
330                    'type' => $integration->integrationType,
331                    'error' => $e->getMessage(),
332                ]);
333            }
334        }
335
336        return [
337            'activated' => $activated,
338            'failed' => $failed,
339        ];
340    }
341
342    /**
343     * Deactivate all active integrations for an event (on cancel)
344     *
345     * @param Event $event The event being cancelled
346     * @return array ['deactivated' => int, 'failed' => array]
347     */
348    public function deactivateAll(Event $event): array
349    {
350        $integrations = $this->getIntegrationsForEvent($event->id);
351        $deactivated = 0;
352        $failed = [];
353
354        foreach ($integrations as $integration) {
355            // Only deactivate pending or active integrations
356            if (!in_array($integration->status, [EventIntegration::STATUS_PENDING, EventIntegration::STATUS_ACTIVE])) {
357                continue;
358            }
359
360            try {
361                $adapter = $this->getAdapter($integration->integrationType);
362                $adapter->deactivate($event, $integration);
363
364                // Update integration status
365                $newStatus = $integration->status === EventIntegration::STATUS_ACTIVE
366                    ? EventIntegration::STATUS_COMPLETED
367                    : EventIntegration::STATUS_FAILED;
368                $this->updateIntegrationStatus($integration->id, $newStatus);
369                $deactivated++;
370            } catch (IntegrationException $e) {
371                $this->updateIntegrationStatus($integration->id, EventIntegration::STATUS_FAILED);
372                $failed[] = [
373                    'integrationId' => $integration->id,
374                    'type' => $integration->integrationType,
375                    'error' => $e->getMessage(),
376                ];
377                $this->logError("Failed to deactivate integration {$integration->id}", [
378                    'type' => $integration->integrationType,
379                    'error' => $e->getMessage(),
380                ]);
381            }
382        }
383
384        return [
385            'deactivated' => $deactivated,
386            'failed' => $failed,
387        ];
388    }
389
390    // =========================================================================
391    // DELETE (CASCADE)
392    // =========================================================================
393
394    /**
395     * Delete all integrations for an event (cascade delete)
396     *
397     * @param int $eventId The event ID
398     * @return array ['deleted' => int, 'failed' => array, 'byType' => array]
399     */
400    public function cascadeDelete(int $eventId): array
401    {
402        $integrations = $this->getIntegrationsForEvent($eventId);
403        $deleted = 0;
404        $failed = [];
405        $byType = [];
406
407        foreach ($integrations as $integration) {
408            try {
409                $adapter = $this->getAdapter($integration->integrationType);
410                $adapter->delete($integration);
411
412                // Delete the integration link record
413                $sql = "DELETE FROM event_integrations WHERE id = :id";
414                $stmt = $this->db->prepare($sql);
415                $stmt->execute([':id' => $integration->id]);
416
417                $deleted++;
418                $byType[$integration->integrationType] = ($byType[$integration->integrationType] ?? 0) + 1;
419            } catch (IntegrationException $e) {
420                $failed[] = [
421                    'integrationId' => $integration->id,
422                    'type' => $integration->integrationType,
423                    'error' => $e->getMessage(),
424                ];
425                $this->logError("Failed to delete integration {$integration->id}", [
426                    'type' => $integration->integrationType,
427                    'error' => $e->getMessage(),
428                ]);
429            }
430        }
431
432        return [
433            'deleted' => $deleted,
434            'failed' => $failed,
435            'byType' => $byType,
436        ];
437    }
438
439    /**
440     * Delete a single integration
441     *
442     * @param EventIntegration $integration The integration to delete
443     * @throws IntegrationException On failure
444     */
445    public function deleteIntegration(EventIntegration $integration): void
446    {
447        $adapter = $this->getAdapter($integration->integrationType);
448        $adapter->delete($integration);
449
450        // Delete the integration link record
451        $sql = "DELETE FROM event_integrations WHERE id = :id";
452        $stmt = $this->db->prepare($sql);
453        $stmt->execute([':id' => $integration->id]);
454    }
455
456    // =========================================================================
457    // STATUS AGGREGATION
458    // =========================================================================
459
460    /**
461     * Get aggregated status for all integrations of an event
462     *
463     * @param int $eventId The event ID
464     * @return array Status summary by integration type
465     */
466    public function getAggregatedStatus(int $eventId): array
467    {
468        $integrations = $this->getIntegrationsForEvent($eventId);
469        $result = [];
470
471        foreach ($integrations as $integration) {
472            try {
473                $adapter = $this->getAdapter($integration->integrationType);
474                $status = $adapter->getStatus($integration);
475
476                $result[$integration->integrationType] = [
477                    'integrationId' => $integration->id,
478                    'foreignId' => $integration->foreignId,
479                    'status' => $integration->status,
480                    'details' => $status,
481                ];
482            } catch (IntegrationException $e) {
483                $result[$integration->integrationType] = [
484                    'integrationId' => $integration->id,
485                    'foreignId' => $integration->foreignId,
486                    'status' => 'error',
487                    'error' => $e->getMessage(),
488                ];
489            }
490        }
491
492        return $result;
493    }
494
495    /**
496     * Get status counts by integration type
497     *
498     * @param int $eventId The event ID
499     * @return array ['pending' => int, 'active' => int, 'completed' => int, 'failed' => int, 'total' => int]
500     */
501    public function getStatusCounts(int $eventId): array
502    {
503        $sql = "SELECT status, COUNT(*) as cnt
504                FROM event_integrations
505                WHERE eventId = :eventId
506                GROUP BY status";
507
508        $stmt = $this->db->prepare($sql);
509        $stmt->execute([':eventId' => $eventId]);
510
511        $counts = [
512            'pending' => 0,
513            'active' => 0,
514            'completed' => 0,
515            'failed' => 0,
516            'total' => 0,
517        ];
518
519        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
520            $counts[$row['status']] = (int) $row['cnt'];
521            $counts['total'] += (int) $row['cnt'];
522        }
523
524        return $counts;
525    }
526
527    // =========================================================================
528    // HELPER METHODS
529    // =========================================================================
530
531    /**
532     * Get all integrations for an event
533     *
534     * @param int $eventId The event ID
535     * @return EventIntegration[]
536     */
537    public function getIntegrationsForEvent(int $eventId): array
538    {
539        $sql = "SELECT * FROM event_integrations WHERE eventId = :eventId ORDER BY integrationType, id";
540        $stmt = $this->db->prepare($sql);
541        $stmt->execute([':eventId' => $eventId]);
542
543        $integrations = [];
544        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
545            $integrations[] = EventIntegration::fromRow($row);
546        }
547
548        return $integrations;
549    }
550
551    /**
552     * Get a single integration by ID
553     *
554     * @param int $integrationId
555     * @return EventIntegration|null
556     */
557    public function getIntegration(int $integrationId): ?EventIntegration
558    {
559        $sql = "SELECT * FROM event_integrations WHERE id = :id LIMIT 1";
560        $stmt = $this->db->prepare($sql);
561        $stmt->execute([':id' => $integrationId]);
562        $row = $stmt->fetch(PDO::FETCH_ASSOC);
563
564        return $row ? EventIntegration::fromRow($row) : null;
565    }
566
567    /**
568     * Update integration status
569     *
570     * @param int $integrationId
571     * @param string $status
572     */
573    private function updateIntegrationStatus(int $integrationId, string $status): void
574    {
575        $sql = "UPDATE event_integrations SET status = :status WHERE id = :id";
576        $stmt = $this->db->prepare($sql);
577        $stmt->execute([
578            ':id' => $integrationId,
579            ':status' => $status,
580        ]);
581    }
582
583    /**
584     * Log an error
585     *
586     * @param string $message
587     * @param array $context
588     */
589    private function logError(string $message, array $context = []): void
590    {
591        $logMessage = "[EventManagement:IntegrationService] {$message}";
592        if (!empty($context)) {
593            $logMessage .= ' ' . json_encode($context);
594        }
595        error_log($logMessage);
596    }
597
598    /**
599     * Get default configuration for an integration type
600     *
601     * @param string $integrationType
602     * @return array
603     */
604    public function getDefaultConfig(string $integrationType): array
605    {
606        $adapter = $this->getAdapter($integrationType);
607        return $adapter->getDefaultConfig();
608    }
609
610    /**
611     * Validate configuration for an integration type
612     *
613     * @param string $integrationType
614     * @param array $config
615     * @return array Validation errors
616     */
617    public function validateConfig(string $integrationType, array $config): array
618    {
619        $adapter = $this->getAdapter($integrationType);
620        return $adapter->validateConfig($config);
621    }
622}