Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 79
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
EventTemplate
0.00% covered (danger)
0.00%
0 / 79
0.00% covered (danger)
0.00%
0 / 6
420
0.00% covered (danger)
0.00%
0 / 1
 getValidScopes
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 appliesToStoreType
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 createEvent
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
 fromRow
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
20
 toArray
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
6
 validate
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
110
1<?php
2
3namespace BuyerKiosk\EventManagement\Models;
4
5/**
6 * EventTemplate - Reusable event configuration template
7 *
8 * Templates allow stores to create events from predefined configurations.
9 * Templates can be scoped at different levels:
10 * - Global: Available to all stores (e.g., major holidays)
11 * - Franchise: Available to stores in a franchise group
12 * - Store: Custom templates for a specific store
13 *
14 * Templates define default values for build-up/wind-down periods,
15 * display styling (color/icon), and can have pre-configured integrations.
16 *
17 * @package BuyerKiosk\EventManagement\Models
18 */
19class EventTemplate
20{
21    // Scope constants
22    public const SCOPE_GLOBAL = 'global';
23    public const SCOPE_FRANCHISE = 'franchise';
24    public const SCOPE_STORE = 'store';
25
26    /**
27     * @var int|null Template ID
28     */
29    public ?int $id = null;
30
31    /**
32     * @var string Template name for display
33     */
34    public string $name = '';
35
36    /**
37     * @var string|null Template description
38     */
39    public ?string $description = null;
40
41    /**
42     * @var string Event type: 'season', 'holiday', 'sale', 'custom'
43     */
44    public string $eventType = Event::TYPE_CUSTOM;
45
46    /**
47     * @var string Scope: 'global', 'franchise', 'store'
48     */
49    public string $scope = self::SCOPE_GLOBAL;
50
51    /**
52     * @var string|null Store type this template applies to (null = all types)
53     */
54    public ?string $storeType = null;
55
56    /**
57     * @var int Default number of days before event start for build-up phase
58     */
59    public int $defaultBuildUpDays = 14;
60
61    /**
62     * @var int Default number of days after event end for wind-down phase
63     */
64    public int $defaultWindDownDays = 7;
65
66    /**
67     * @var string|null Default display color (hex code)
68     */
69    public ?string $color = null;
70
71    /**
72     * @var string|null Default icon identifier for display
73     */
74    public ?string $icon = null;
75
76    /**
77     * @var bool Whether this template is active and available for use
78     */
79    public bool $isActive = true;
80
81    /**
82     * @var string|null Created timestamp
83     */
84    public ?string $createdAt = null;
85
86    /**
87     * @var string|null Updated timestamp
88     */
89    public ?string $updatedAt = null;
90
91    /**
92     * @var array Loaded integrations (not stored directly in template table)
93     *
94     * This property is populated when loading template with integrations.
95     * Contains EventIntegration objects or arrays representing template integrations.
96     */
97    public array $integrations = [];
98
99    /**
100     * Get all valid scopes
101     *
102     * @return array List of valid scope constants
103     */
104    public static function getValidScopes(): array
105    {
106        return [
107            self::SCOPE_GLOBAL,
108            self::SCOPE_FRANCHISE,
109            self::SCOPE_STORE,
110        ];
111    }
112
113    /**
114     * Check if this template applies to a given store type
115     *
116     * @param string|null $storeType The store type to check
117     * @return bool True if template applies to the store type
118     */
119    public function appliesToStoreType(?string $storeType): bool
120    {
121        // If no store type restriction, applies to all
122        if ($this->storeType === null) {
123            return true;
124        }
125
126        // Check if store type matches
127        return $this->storeType === $storeType;
128    }
129
130    /**
131     * Create an Event from this template
132     *
133     * Creates a new Event instance with default values from the template.
134     * Dates must be set separately as templates don't define specific dates.
135     *
136     * @param int $year The year for the event
137     * @return Event New Event instance based on template
138     */
139    public function createEvent(int $year): Event
140    {
141        $event = new Event();
142
143        $event->templateId = $this->id;
144        $event->name = $this->name;
145        $event->description = $this->description;
146        $event->eventType = $this->eventType;
147        $event->year = $year;
148        $event->buildUpDays = $this->defaultBuildUpDays;
149        $event->windDownDays = $this->defaultWindDownDays;
150        $event->color = $this->color;
151        $event->icon = $this->icon;
152        $event->status = Event::STATUS_DRAFT;
153        $event->phase = Event::PHASE_UPCOMING;
154
155        return $event;
156    }
157
158    /**
159     * Create an EventTemplate object from a database row
160     *
161     * Maps camelCase database columns to properties.
162     *
163     * @param array $row Database row from eventTemplates table
164     * @return self Hydrated EventTemplate instance
165     */
166    public static function fromRow(array $row): self
167    {
168        $template = new self();
169
170        $template->id = isset($row['id']) ? (int) $row['id'] : null;
171        $template->name = $row['name'] ?? '';
172        $template->description = $row['description'] ?? null;
173        $template->eventType = $row['eventType'] ?? Event::TYPE_CUSTOM;
174        $template->scope = $row['scope'] ?? self::SCOPE_GLOBAL;
175        $template->storeType = $row['storeType'] ?? null;
176        $template->defaultBuildUpDays = isset($row['defaultBuildUpDays'])
177            ? (int) $row['defaultBuildUpDays']
178            : 14;
179        $template->defaultWindDownDays = isset($row['defaultWindDownDays'])
180            ? (int) $row['defaultWindDownDays']
181            : 7;
182        $template->color = $row['color'] ?? null;
183        $template->icon = $row['icon'] ?? null;
184        $template->isActive = (bool) ($row['isActive'] ?? true);
185        $template->createdAt = $row['created_at'] ?? null;
186        $template->updatedAt = $row['updated_at'] ?? null;
187
188        return $template;
189    }
190
191    /**
192     * Convert template to array for serialization
193     *
194     * @return array Associative array representation
195     */
196    public function toArray(): array
197    {
198        return [
199            'id' => $this->id,
200            'name' => $this->name,
201            'description' => $this->description,
202            'eventType' => $this->eventType,
203            'scope' => $this->scope,
204            'storeType' => $this->storeType,
205            'defaultBuildUpDays' => $this->defaultBuildUpDays,
206            'defaultWindDownDays' => $this->defaultWindDownDays,
207            'color' => $this->color,
208            'icon' => $this->icon,
209            'isActive' => $this->isActive,
210            'createdAt' => $this->createdAt,
211            'updatedAt' => $this->updatedAt,
212            'integrations' => array_map(
213                fn($integration) => $integration instanceof EventIntegration
214                    ? $integration->toArray()
215                    : $integration,
216                $this->integrations
217            ),
218        ];
219    }
220
221    /**
222     * Validate the template configuration
223     *
224     * Checks all business rules and constraints for template validity.
225     *
226     * @return array Array of validation errors (empty if valid)
227     */
228    public function validate(): array
229    {
230        $errors = [];
231
232        // Name is required
233        if (empty(trim($this->name))) {
234            $errors[] = 'Template name is required';
235        }
236
237        // Valid event type
238        if (!in_array($this->eventType, Event::getValidTypes(), true)) {
239            $errors[] = 'Invalid event type: ' . $this->eventType;
240        }
241
242        // Valid scope
243        if (!in_array($this->scope, self::getValidScopes(), true)) {
244            $errors[] = 'Invalid template scope: ' . $this->scope;
245        }
246
247        // Default build-up days must be non-negative
248        if ($this->defaultBuildUpDays < 0) {
249            $errors[] = 'Default build-up days cannot be negative';
250        }
251
252        // Default wind-down days must be non-negative
253        if ($this->defaultWindDownDays < 0) {
254            $errors[] = 'Default wind-down days cannot be negative';
255        }
256
257        // Reasonable limits on build-up/wind-down (max 90 days each)
258        if ($this->defaultBuildUpDays > 90) {
259            $errors[] = 'Default build-up days cannot exceed 90 days';
260        }
261
262        if ($this->defaultWindDownDays > 90) {
263            $errors[] = 'Default wind-down days cannot exceed 90 days';
264        }
265
266        // Color must be valid hex if provided
267        if ($this->color !== null && !preg_match('/^#[0-9A-Fa-f]{6}$/', $this->color)) {
268            $errors[] = 'Color must be a valid hex color code (e.g., #FF5733)';
269        }
270
271        // Store-scoped templates should have a store type or be validated at service layer
272        // (Not enforced here as store type might be set during save)
273
274        return $errors;
275    }
276}