Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 91
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
EventIntegration
0.00% covered (danger)
0.00%
0 / 91
0.00% covered (danger)
0.00%
0 / 11
2450
0.00% covered (danger)
0.00%
0 / 1
 getValidTypes
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 getValidStatuses
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 getTypeLabel
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
90
 getConfigValue
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setConfigValue
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 isTerminal
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 fromRow
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 toArray
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 validate
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
110
 validateTypeSpecific
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
272
 parseJson
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3namespace BuyerKiosk\EventManagement\Models;
4
5/**
6 * EventIntegration - Links events to other system components
7 *
8 * Represents a connection between an event and another system feature
9 * such as backstock categories, SMS campaigns, signage slides,
10 * Comeback Cash events, tasks, or notes.
11 *
12 * Integrations can be configured with:
13 * - Type-specific configuration (stored as JSON)
14 * - Relative timing (days before/after event dates)
15 * - Status tracking for activation/completion
16 *
17 * @package BuyerKiosk\EventManagement\Models
18 */
19class EventIntegration
20{
21    // Integration Types
22    public const TYPE_BACKSTOCK = 'backstock';
23    public const TYPE_SMS_BLAST = 'sms_blast';
24    public const TYPE_SMS_TRIGGER = 'sms_trigger';
25    public const TYPE_SIGNAGE = 'signage';
26    public const TYPE_COMEBACK_CASH = 'comeback_cash';
27    public const TYPE_TASK = 'task';
28    public const TYPE_NOTE = 'note';
29
30    // Status constants
31    public const STATUS_PENDING = 'pending';
32    public const STATUS_ACTIVE = 'active';
33    public const STATUS_COMPLETED = 'completed';
34    public const STATUS_FAILED = 'failed';
35    public const STATUS_DISABLED = 'disabled';
36
37    /**
38     * @var int|null Integration ID
39     */
40    public ?int $id = null;
41
42    /**
43     * @var int Event ID this integration belongs to
44     */
45    public int $eventId;
46
47    /**
48     * @var string Integration type: 'backstock', 'sms_blast', 'sms_trigger', 'signage', 'comeback_cash', 'task', 'note'
49     */
50    public string $integrationType;
51
52    /**
53     * @var int Foreign ID referencing the integrated system's record
54     *
55     * Examples:
56     * - Backstock category ID for TYPE_BACKSTOCK
57     * - SMS template ID for TYPE_SMS_BLAST
58     * - Signage slide ID for TYPE_SIGNAGE
59     * - Comeback Cash event ID for TYPE_COMEBACK_CASH
60     * - Task ID for TYPE_TASK
61     * - Note ID for TYPE_NOTE
62     */
63    public int $foreignId;
64
65    /**
66     * @var array|null Type-specific configuration as JSON
67     *
68     * Configuration varies by integration type:
69     * - Backstock: {"markupAdjustment": 10, "priorityBoost": true}
70     * - SMS Blast: {"sendTime": "10:00", "segment": "vip"}
71     * - Signage: {"displayOrder": 1, "duration": 10}
72     * - Comeback Cash: {"autoActivate": true}
73     * - Task: {"assignTo": "manager", "priority": "high"}
74     * - Note: {"visibility": "staff"}
75     */
76    public ?array $config = null;
77
78    /**
79     * @var string Integration status: 'pending', 'active', 'completed', 'failed'
80     */
81    public string $status = self::STATUS_PENDING;
82
83    /**
84     * @var int|null Days relative to event date for activation
85     *
86     * Positive = after event start
87     * Negative = before event start
88     * null = activate with event
89     */
90    public ?int $relativeDays = null;
91
92    /**
93     * @var string|null Created timestamp
94     */
95    public ?string $createdAt = null;
96
97    /**
98     * Get all valid integration types
99     *
100     * @return array List of valid integration type constants
101     */
102    public static function getValidTypes(): array
103    {
104        return [
105            self::TYPE_BACKSTOCK,
106            self::TYPE_SMS_BLAST,
107            self::TYPE_SMS_TRIGGER,
108            self::TYPE_SIGNAGE,
109            self::TYPE_COMEBACK_CASH,
110            self::TYPE_TASK,
111            self::TYPE_NOTE,
112        ];
113    }
114
115    /**
116     * Get all valid statuses
117     *
118     * @return array List of valid status constants
119     */
120    public static function getValidStatuses(): array
121    {
122        return [
123            self::STATUS_PENDING,
124            self::STATUS_ACTIVE,
125            self::STATUS_COMPLETED,
126            self::STATUS_FAILED,
127            self::STATUS_DISABLED,
128        ];
129    }
130
131    /**
132     * Get a human-readable label for the integration type
133     *
134     * @return string Human-readable type label
135     */
136    public function getTypeLabel(): string
137    {
138        return match ($this->integrationType) {
139            self::TYPE_BACKSTOCK => 'Backstock Category',
140            self::TYPE_SMS_BLAST => 'SMS Blast',
141            self::TYPE_SMS_TRIGGER => 'SMS Trigger',
142            self::TYPE_SIGNAGE => 'Digital Signage',
143            self::TYPE_COMEBACK_CASH => 'Comeback Cash',
144            self::TYPE_TASK => 'Task',
145            self::TYPE_NOTE => 'Note',
146            default => ucfirst(str_replace('_', ' ', $this->integrationType)),
147        };
148    }
149
150    /**
151     * Get a configuration value by key
152     *
153     * @param string $key Configuration key to retrieve
154     * @param mixed $default Default value if key not found
155     * @return mixed Configuration value or default
156     */
157    public function getConfigValue(string $key, mixed $default = null): mixed
158    {
159        return $this->config[$key] ?? $default;
160    }
161
162    /**
163     * Set a configuration value by key
164     *
165     * @param string $key Configuration key to set
166     * @param mixed $value Value to set
167     * @return self For method chaining
168     */
169    public function setConfigValue(string $key, mixed $value): self
170    {
171        if ($this->config === null) {
172            $this->config = [];
173        }
174        $this->config[$key] = $value;
175        return $this;
176    }
177
178    /**
179     * Check if integration is in a terminal state
180     *
181     * @return bool True if integration is completed or failed
182     */
183    public function isTerminal(): bool
184    {
185        return in_array($this->status, [self::STATUS_COMPLETED, self::STATUS_FAILED], true);
186    }
187
188    /**
189     * Create an EventIntegration object from a database row
190     *
191     * Maps camelCase database columns to properties.
192     *
193     * @param array $row Database row from event_integrations table
194     * @return self Hydrated EventIntegration instance
195     */
196    public static function fromRow(array $row): self
197    {
198        $integration = new self();
199
200        $integration->id = isset($row['id']) ? (int) $row['id'] : null;
201        $integration->eventId = (int) ($row['eventId'] ?? 0);
202        $integration->integrationType = $row['integrationType'] ?? self::TYPE_NOTE;
203        $integration->foreignId = (int) ($row['foreignId'] ?? 0);
204        $integration->config = self::parseJson($row['config'] ?? null);
205        $integration->status = $row['status'] ?? self::STATUS_PENDING;
206        $integration->relativeDays = isset($row['relativeDays']) ? (int) $row['relativeDays'] : null;
207        $integration->createdAt = $row['created_at'] ?? null;
208
209        return $integration;
210    }
211
212    /**
213     * Convert integration to array for serialization
214     *
215     * @return array Associative array representation
216     */
217    public function toArray(): array
218    {
219        return [
220            'id' => $this->id,
221            'eventId' => $this->eventId,
222            'integrationType' => $this->integrationType,
223            'typeLabel' => $this->getTypeLabel(),
224            'foreignId' => $this->foreignId,
225            'config' => $this->config,
226            'status' => $this->status,
227            'relativeDays' => $this->relativeDays,
228            'createdAt' => $this->createdAt,
229            'isTerminal' => $this->isTerminal(),
230        ];
231    }
232
233    /**
234     * Validate the integration configuration
235     *
236     * Checks all business rules and constraints for integration validity.
237     *
238     * @return array Array of validation errors (empty if valid)
239     */
240    public function validate(): array
241    {
242        $errors = [];
243
244        // Event ID is required
245        if (!isset($this->eventId) || $this->eventId <= 0) {
246            $errors[] = 'Event ID is required';
247        }
248
249        // Valid integration type
250        if (!in_array($this->integrationType, self::getValidTypes(), true)) {
251            $errors[] = 'Invalid integration type: ' . $this->integrationType;
252        }
253
254        // Foreign ID is required and must be positive
255        if (!isset($this->foreignId) || $this->foreignId <= 0) {
256            $errors[] = 'Foreign ID is required and must be positive';
257        }
258
259        // Valid status
260        if (!in_array($this->status, self::getValidStatuses(), true)) {
261            $errors[] = 'Invalid integration status: ' . $this->status;
262        }
263
264        // Relative days must be reasonable if set (-365 to +365)
265        if ($this->relativeDays !== null) {
266            if ($this->relativeDays < -365 || $this->relativeDays > 365) {
267                $errors[] = 'Relative days must be between -365 and 365';
268            }
269        }
270
271        // Type-specific validation
272        $errors = array_merge($errors, $this->validateTypeSpecific());
273
274        return $errors;
275    }
276
277    /**
278     * Perform type-specific validation
279     *
280     * @return array Array of validation errors specific to integration type
281     */
282    private function validateTypeSpecific(): array
283    {
284        $errors = [];
285
286        switch ($this->integrationType) {
287            case self::TYPE_SMS_BLAST:
288                // SMS blasts should have send time in config
289                if ($this->config !== null && isset($this->config['sendTime'])) {
290                    if (!preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $this->config['sendTime'])) {
291                        $errors[] = 'SMS blast send time must be in HH:MM format';
292                    }
293                }
294                break;
295
296            case self::TYPE_SIGNAGE:
297                // Signage duration should be positive if set
298                if ($this->config !== null && isset($this->config['duration'])) {
299                    if (!is_numeric($this->config['duration']) || $this->config['duration'] <= 0) {
300                        $errors[] = 'Signage duration must be a positive number';
301                    }
302                }
303                break;
304
305            case self::TYPE_BACKSTOCK:
306                // Markup adjustment should be reasonable if set
307                if ($this->config !== null && isset($this->config['markupAdjustment'])) {
308                    $adjustment = $this->config['markupAdjustment'];
309                    if (!is_numeric($adjustment) || $adjustment < -100 || $adjustment > 100) {
310                        $errors[] = 'Backstock markup adjustment must be between -100 and 100';
311                    }
312                }
313                break;
314        }
315
316        return $errors;
317    }
318
319    /**
320     * Parse a JSON string into an array
321     *
322     * @param string|null $value JSON string or null
323     * @return array|null Decoded array or null
324     */
325    private static function parseJson(?string $value): ?array
326    {
327        if ($value === null || $value === '') {
328            return null;
329        }
330
331        $decoded = json_decode($value, true);
332        return is_array($decoded) ? $decoded : null;
333    }
334}