Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 108
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
TemplateApiController
0.00% covered (danger)
0.00%
0 / 108
0.00% covered (danger)
0.00%
0 / 12
1122
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
 checkAuth
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 / 24
0.00% covered (danger)
0.00%
0 / 1
56
 get
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
30
 getTemplatesForStore
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
 getTemplateById
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 getTemplateIntegrations
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 isTemplateAccessible
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 getStoreType
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 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
1<?php
2
3namespace BuyerKiosk\EventManagement\Controllers;
4
5use Exception;
6use BuyerKiosk\EventManagement\Models\EventTemplate;
7
8/**
9 * TemplateApiController - REST API for Event Template operations
10 *
11 * Provides read-only endpoints for accessing event templates:
12 *
13 * 1. GET /api/:typeNum/event-templates              - List available templates
14 * 2. GET /api/:typeNum/event-templates/:templateId  - Get single template
15 *
16 * Templates are used to create events from predefined configurations.
17 * Templates can be scoped at different levels: global, franchise, store.
18 *
19 * Authentication: All endpoints require session authentication + uri_events permission
20 *
21 * @package BuyerKiosk\EventManagement\Controllers
22 */
23class TemplateApiController
24{
25    /**
26     * @var \Slim\Slim Slim application instance
27     */
28    private $app;
29
30    /**
31     * @var \Store Store object
32     */
33    private $store;
34
35    /**
36     * @var string Store type number
37     */
38    private string $typeNum;
39
40    /**
41     * Constructor
42     *
43     * @param \Slim\Slim $app Slim application instance
44     * @param \Store $store Store object (validated)
45     */
46    public function __construct($app, \Store $store)
47    {
48        $this->app = $app;
49        $this->store = $store;
50        $this->typeNum = $store->getTypeNum();
51    }
52
53    // =========================================================================
54    // AUTHENTICATION & PERMISSION CHECK
55    // =========================================================================
56
57    /**
58     * Check session authentication and uri_events permission
59     *
60     * @return bool True if authorized, false otherwise (response already sent)
61     */
62    private function checkAuth(): bool
63    {
64        if (!isset($this->app->user) || !$this->app->user) {
65            $this->sendErrorResponse('Authentication required', 401, 'UNAUTHORIZED');
66            return false;
67        }
68
69        if (!$this->app->user->checkAccess('uri_events')) {
70            $this->sendErrorResponse('Access denied. Requires uri_events permission', 403, 'FORBIDDEN');
71            return false;
72        }
73
74        return true;
75    }
76
77    // =========================================================================
78    // TEMPLATE ENDPOINTS
79    // =========================================================================
80
81    /**
82     * GET /api/:typeNum/event-templates
83     *
84     * List available event templates filtered by scope.
85     * Returns templates that apply to this store based on scope hierarchy.
86     *
87     * Query Parameters:
88     * - scope: string (optional) - Filter by scope: 'global', 'franchise', 'store', or 'all'
89     *   Default returns all applicable templates for the store
90     *
91     * Response:
92     * {
93     *   "success": true,
94     *   "templates": EventTemplate[],
95     *   "total": int,
96     *   "scope": string
97     * }
98     */
99    public function list(): void
100    {
101        $this->setJsonContentType();
102
103        if (!$this->checkAuth()) {
104            return;
105        }
106
107        try {
108            $scope = $this->app->request->get('scope');
109
110            // Validate scope if provided
111            if (!empty($scope) && $scope !== 'all') {
112                if (!in_array($scope, EventTemplate::getValidScopes(), true)) {
113                    $this->sendErrorResponse(
114                        'Invalid scope. Must be one of: ' . implode(', ', EventTemplate::getValidScopes()) . ', or "all"',
115                        400,
116                        'VALIDATION_ERROR'
117                    );
118                    return;
119                }
120            }
121
122            // Get templates
123            $templates = $this->getTemplatesForStore($scope);
124
125            // Convert templates to array format
126            $templatesArray = array_map(fn(EventTemplate $t) => $t->toArray(), $templates);
127
128            $response = [
129                'success' => true,
130                'templates' => $templatesArray,
131                'total' => count($templatesArray),
132                'scope' => $scope ?: 'all',
133            ];
134
135            $this->sendJsonResponse($response);
136
137        } catch (Exception $e) {
138            error_log("TemplateApiController::list error: " . $e->getMessage());
139            $this->sendErrorResponse('Failed to retrieve templates', 500);
140        }
141    }
142
143    /**
144     * GET /api/:typeNum/event-templates/:templateId
145     *
146     * Get a single template by ID.
147     * Only returns the template if it's accessible by this store.
148     *
149     * @param int $templateId Template ID
150     *
151     * Response:
152     * {
153     *   "success": true,
154     *   "template": EventTemplate
155     * }
156     */
157    public function get(int $templateId): void
158    {
159        $this->setJsonContentType();
160
161        if (!$this->checkAuth()) {
162            return;
163        }
164
165        try {
166            $template = $this->getTemplateById($templateId);
167
168            if (!$template) {
169                $this->sendErrorResponse('Template not found', 404, 'NOT_FOUND');
170                return;
171            }
172
173            // Verify template is accessible to this store
174            if (!$this->isTemplateAccessible($template)) {
175                $this->sendErrorResponse('Template not found', 404, 'NOT_FOUND');
176                return;
177            }
178
179            // Load integrations for the template
180            $template->integrations = $this->getTemplateIntegrations($templateId);
181
182            $response = [
183                'success' => true,
184                'template' => $template->toArray(),
185            ];
186
187            $this->sendJsonResponse($response);
188
189        } catch (Exception $e) {
190            error_log("TemplateApiController::get error: " . $e->getMessage());
191            $this->sendErrorResponse('Failed to retrieve template', 500);
192        }
193    }
194
195    // =========================================================================
196    // DATABASE OPERATIONS
197    // =========================================================================
198
199    /**
200     * Get templates available for the current store
201     *
202     * Templates are returned based on scope hierarchy:
203     * - Global templates (available to all stores)
204     * - Franchise templates (if store belongs to a franchise)
205     * - Store-specific templates
206     *
207     * @param string|null $scope Optional scope filter
208     * @return EventTemplate[] Array of templates
209     */
210    private function getTemplatesForStore(?string $scope): array
211    {
212        // Templates are stored in central database (kiosk_buykiosk)
213        $centralDb = dbConnectByName('kiosk_buykiosk');
214
215        // Build query based on scope
216        $sql = "SELECT * FROM eventTemplates WHERE isActive = 1";
217        $params = [];
218
219        if (!empty($scope) && $scope !== 'all') {
220            $sql .= " AND scope = :scope";
221            $params[':scope'] = $scope;
222        } else {
223            // Get all applicable templates for this store
224            // Global templates + franchise templates (if applicable) + store templates
225            $sql .= " AND (
226                scope = 'global'
227                OR (scope = 'franchise' AND storeType = :storeType)
228                OR (scope = 'store' AND storeType = :storeType)
229            )";
230            $params[':storeType'] = $this->getStoreType();
231        }
232
233        $sql .= " ORDER BY scope ASC, name ASC";
234
235        $stmt = $centralDb->prepare($sql);
236        $stmt->execute($params);
237        $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
238
239        $templates = array_map(fn($row) => EventTemplate::fromRow($row), $rows);
240
241        return $templates;
242    }
243
244    /**
245     * Get a single template by ID
246     *
247     * @param int $templateId Template ID
248     * @return EventTemplate|null Template or null if not found
249     */
250    private function getTemplateById(int $templateId): ?EventTemplate
251    {
252        $db = dbConnectByName('kiosk_buykiosk');
253
254        $sql = "SELECT * FROM eventTemplates WHERE id = :id AND isActive = 1";
255        $stmt = $db->prepare($sql);
256        $stmt->execute([':id' => $templateId]);
257        $row = $stmt->fetch(\PDO::FETCH_ASSOC);
258
259        if (!$row) {
260            return null;
261        }
262
263        return EventTemplate::fromRow($row);
264    }
265
266    /**
267     * Get integrations for a template
268     *
269     * @param int $templateId Template ID
270     * @return array Array of integration data
271     */
272    private function getTemplateIntegrations(int $templateId): array
273    {
274        $db = dbConnectByName('kiosk_buykiosk');
275
276        $sql = "SELECT * FROM eventTemplate_Integrations WHERE templateId = :templateId";
277        $stmt = $db->prepare($sql);
278        $stmt->execute([':templateId' => $templateId]);
279        $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
280
281        return array_map(function ($row) {
282            return [
283                'id' => (int) $row['id'],
284                'type' => $row['integrationType'],
285                'config' => json_decode($row['config'] ?? '{}', true),
286                'relativeDays' => $row['relativeDays'] ?? null,
287                'isOptional' => (bool) ($row['isOptional'] ?? false),
288            ];
289        }, $rows);
290    }
291
292    /**
293     * Check if a template is accessible to the current store
294     *
295     * @param EventTemplate $template Template to check
296     * @return bool True if accessible
297     */
298    private function isTemplateAccessible(EventTemplate $template): bool
299    {
300        // Global templates are accessible to all stores
301        if ($template->scope === EventTemplate::SCOPE_GLOBAL) {
302            return true;
303        }
304
305        // Check store type match for franchise and store-scoped templates
306        if ($template->scope === EventTemplate::SCOPE_FRANCHISE || $template->scope === EventTemplate::SCOPE_STORE) {
307            return $template->appliesToStoreType($this->getStoreType());
308        }
309
310        return false;
311    }
312
313    /**
314     * Get the store type for the current store
315     *
316     * @return string|null Store type or null
317     */
318    private function getStoreType(): ?string
319    {
320        // Extract store type from typeNum (e.g., 'ou' from 'ou00')
321        if (strlen($this->typeNum) >= 2) {
322            return substr($this->typeNum, 0, 2);
323        }
324        return null;
325    }
326
327    // =========================================================================
328    // RESPONSE HELPERS
329    // =========================================================================
330
331    /**
332     * Set JSON content type header
333     */
334    private function setJsonContentType(): void
335    {
336        $this->app->response->headers->set('Content-Type', 'application/json');
337    }
338
339    /**
340     * Send JSON response body
341     *
342     * @param array $data Response data
343     */
344    private function sendJsonResponse(array $data): void
345    {
346        $this->app->response->setBody(json_encode($data));
347    }
348
349    /**
350     * Send error response
351     *
352     * @param string $message Error message
353     * @param int $httpStatus HTTP status code
354     * @param string|null $errorCode Application error code
355     */
356    private function sendErrorResponse(string $message, int $httpStatus, ?string $errorCode = null): void
357    {
358        $response = [
359            'success' => false,
360            'error' => $message,
361        ];
362
363        if ($errorCode !== null) {
364            $response['code'] = $errorCode;
365        }
366
367        $this->app->response->setStatus($httpStatus);
368        $this->sendJsonResponse($response);
369    }
370}