Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 302
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
EmployeeInvitationController
0.00% covered (danger)
0.00%
0 / 302
0.00% covered (danger)
0.00%
0 / 9
2070
0.00% covered (danger)
0.00%
0 / 1
 getPendingInvitations
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
42
 createInvitation
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 1
90
 inviteEmployee
0.00% covered (danger)
0.00%
0 / 52
0.00% covered (danger)
0.00%
0 / 1
72
 resendInvitation
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
30
 revokeInvitation
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
20
 showInvitationPage
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
20
 validateInvitation
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
12
 completeRegistration
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
20
 checkUsernameAvailability
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace BuyerKiosk\Core\Controllers;
4
5use BuyerKiosk\UserEmployee\EmployeeInvitationManager;
6use BuyerKiosk\UserEmployee\UserEmployeeLinkManager;
7use BuyerKiosk\UserEmployee\UserEmployeePromotion;
8use Exception;
9
10/**
11 * Employee Invitation API Controller
12 *
13 * Handles REST API endpoints for employee invitation management,
14 * including both authenticated admin endpoints and public invitation acceptance.
15 *
16 * @package UserFrosting
17 */
18class EmployeeInvitationController extends BaseController
19{
20    /**
21     * GET /:typeNum/api/employee-invitations
22     * Get all pending invitations (admin only)
23     *
24     * @param string $typeNum Store identifier
25     */
26    public function getPendingInvitations($typeNum)
27    {
28        // Check permissions
29        if (!$this->_app->user->checkStoreGroup($typeNum) ||
30            !$this->_app->user->checkAccess('create_user')) {
31            $this->_app->response->setStatus(403);
32            $this->_app->response->headers->set('Content-Type', 'application/json');
33            $this->_app->response->setBody(json_encode([
34                'success' => false,
35                'error' => 'Access denied'
36            ]));
37            return;
38        }
39
40        try {
41            $centralDb = dbConnectByName('kiosk_users');
42            $invitationManager = new EmployeeInvitationManager($centralDb);
43
44            $invitations = $invitationManager->getPendingInvitations($typeNum);
45
46            // Convert to array format and include employee data
47            $invitationData = [];
48            $storeDb = dbConnectByName('kiosk_' . $typeNum);
49
50            foreach ($invitations as $invitation) {
51                $invData = $invitation->toArray();
52
53                // Get employee info
54                $empStmt = $storeDb->prepare(
55                    "SELECT employeeFirstName, employeeLastName FROM employees WHERE employeeID = :id"
56                );
57                $empStmt->bindValue(':id', $invitation->getEmployeeId(), \PDO::PARAM_INT);
58                $empStmt->execute();
59                $employee = $empStmt->fetch(\PDO::FETCH_ASSOC);
60
61                if ($employee) {
62                    $invData['employeeName'] = trim($employee['employeeFirstName'] . ' ' . $employee['employeeLastName']);
63                }
64
65                $invitationData[] = $invData;
66            }
67
68            $this->_app->response->headers->set('Content-Type', 'application/json');
69            $this->_app->response->setBody(json_encode([
70                'success' => true,
71                'invitations' => $invitationData
72            ]));
73        } catch (Exception $e) {
74            error_log("EmployeeInvitationController::getPendingInvitations error: " . $e->getMessage());
75            $this->_app->response->setStatus(500);
76            $this->_app->response->headers->set('Content-Type', 'application/json');
77            $this->_app->response->setBody(json_encode([
78                'success' => false,
79                'error' => 'Failed to retrieve invitations'
80            ]));
81        }
82    }
83
84    /**
85     * POST /:typeNum/api/employee-invitations
86     * Create invitation for employee
87     *
88     * @param string $typeNum Store identifier
89     */
90    public function createInvitation($typeNum)
91    {
92        // Check permissions
93        if (!$this->_app->user->checkStoreGroup($typeNum) ||
94            !$this->_app->user->checkAccess('create_user')) {
95            $this->_app->response->setStatus(403);
96            $this->_app->response->headers->set('Content-Type', 'application/json');
97            $this->_app->response->setBody(json_encode([
98                'success' => false,
99                'error' => 'Access denied'
100            ]));
101            return;
102        }
103
104        // Check CSRF token
105        if (!\NoCSRF::check('csrf_token', $this->_app->request->post())) {
106            $this->_app->response->setStatus(403);
107            $this->_app->response->headers->set('Content-Type', 'application/json');
108            $this->_app->response->setBody(json_encode([
109                'success' => false,
110                'error' => 'Invalid or missing CSRF token'
111            ]));
112            return;
113        }
114
115        try {
116            $data = $this->_app->request->post();
117
118            $employeeId = isset($data['employeeId']) ? (int) $data['employeeId'] : null;
119            $email = $data['email'] ?? '';
120            $groups = $data['groups'] ?? [];
121            $expiresInHours = isset($data['expiresInHours']) ? (int) $data['expiresInHours'] : 72;
122
123            if (!$employeeId || empty($email)) {
124                throw new Exception("Employee ID and email are required");
125            }
126
127            $centralDb = dbConnectByName('kiosk_users');
128            $invitationManager = new EmployeeInvitationManager($centralDb);
129
130            $result = $invitationManager->createInvitation(
131                $typeNum,
132                $employeeId,
133                $email,
134                $this->_app->user->id,
135                $groups,
136                $expiresInHours
137            );
138
139            $this->_app->response->setStatus(201);
140            $this->_app->response->headers->set('Content-Type', 'application/json');
141            $this->_app->response->setBody(json_encode([
142                'success' => true,
143                'invitation' => $result['invitation']->toArray(),
144                'url' => $result['url']
145            ]));
146        } catch (Exception $e) {
147            error_log("EmployeeInvitationController::createInvitation error: " . $e->getMessage());
148            $this->_app->response->setStatus(400);
149            $this->_app->response->headers->set('Content-Type', 'application/json');
150            $this->_app->response->setBody(json_encode([
151                'success' => false,
152                'error' => $e->getMessage()
153            ]));
154        }
155    }
156
157    /**
158     * POST /:typeNum/api/employees/:id/invite
159     * Shorthand to create invitation for specific employee
160     *
161     * @param string $typeNum Store identifier
162     * @param int $employeeId Employee ID
163     */
164    public function inviteEmployee($typeNum, $employeeId)
165    {
166        // Check permissions
167        if (!$this->_app->user->checkStoreGroup($typeNum) ||
168            !$this->_app->user->checkAccess('create_user')) {
169            $this->_app->response->setStatus(403);
170            $this->_app->response->headers->set('Content-Type', 'application/json');
171            $this->_app->response->setBody(json_encode([
172                'success' => false,
173                'error' => 'Access denied'
174            ]));
175            return;
176        }
177
178        // Check CSRF token
179        if (!\NoCSRF::check('csrf_token', $this->_app->request->post())) {
180            $this->_app->response->setStatus(403);
181            $this->_app->response->headers->set('Content-Type', 'application/json');
182            $this->_app->response->setBody(json_encode([
183                'success' => false,
184                'error' => 'Invalid or missing CSRF token'
185            ]));
186            return;
187        }
188
189        try {
190            // Get employee email
191            $storeDb = dbConnectByName('kiosk_' . $typeNum);
192            $empStmt = $storeDb->prepare("SELECT email FROM employees WHERE employeeID = :id");
193            $empStmt->bindValue(':id', $employeeId, \PDO::PARAM_INT);
194            $empStmt->execute();
195            $employee = $empStmt->fetch(\PDO::FETCH_ASSOC);
196
197            if (!$employee || empty($employee['email'])) {
198                throw new Exception("Employee not found or has no email address");
199            }
200
201            $data = $this->_app->request->post();
202            $groups = $data['groups'] ?? [];
203            $expiresInHours = isset($data['expiresInHours']) ? (int) $data['expiresInHours'] : 72;
204
205            $centralDb = dbConnectByName('kiosk_users');
206            $invitationManager = new EmployeeInvitationManager($centralDb);
207
208            $result = $invitationManager->createInvitation(
209                $typeNum,
210                $employeeId,
211                $employee['email'],
212                $this->_app->user->id,
213                $groups,
214                $expiresInHours
215            );
216
217            $this->_app->response->setStatus(201);
218            $this->_app->response->headers->set('Content-Type', 'application/json');
219            $this->_app->response->setBody(json_encode([
220                'success' => true,
221                'invitation' => $result['invitation']->toArray(),
222                'url' => $result['url']
223            ]));
224        } catch (Exception $e) {
225            error_log("EmployeeInvitationController::inviteEmployee error: " . $e->getMessage());
226            $this->_app->response->setStatus(400);
227            $this->_app->response->headers->set('Content-Type', 'application/json');
228            $this->_app->response->setBody(json_encode([
229                'success' => false,
230                'error' => $e->getMessage()
231            ]));
232        }
233    }
234
235    /**
236     * POST /:typeNum/api/employee-invitations/:id/resend
237     * Resend invitation
238     *
239     * @param string $typeNum Store identifier
240     * @param int $invitationId Invitation ID
241     */
242    public function resendInvitation($typeNum, $invitationId)
243    {
244        // Check permissions
245        if (!$this->_app->user->checkStoreGroup($typeNum) ||
246            !$this->_app->user->checkAccess('create_user')) {
247            $this->_app->response->setStatus(403);
248            $this->_app->response->headers->set('Content-Type', 'application/json');
249            $this->_app->response->setBody(json_encode([
250                'success' => false,
251                'error' => 'Access denied'
252            ]));
253            return;
254        }
255
256        // Check CSRF token
257        if (!\NoCSRF::check('csrf_token', $this->_app->request->post())) {
258            $this->_app->response->setStatus(403);
259            $this->_app->response->headers->set('Content-Type', 'application/json');
260            $this->_app->response->setBody(json_encode([
261                'success' => false,
262                'error' => 'Invalid or missing CSRF token'
263            ]));
264            return;
265        }
266
267        try {
268            $centralDb = dbConnectByName('kiosk_users');
269            $invitationManager = new EmployeeInvitationManager($centralDb);
270
271            $success = $invitationManager->resendInvitation($invitationId);
272
273            $this->_app->response->headers->set('Content-Type', 'application/json');
274            $this->_app->response->setBody(json_encode([
275                'success' => $success,
276                'message' => 'Invitation resent successfully'
277            ]));
278        } catch (Exception $e) {
279            error_log("EmployeeInvitationController::resendInvitation error: " . $e->getMessage());
280            $this->_app->response->setStatus(400);
281            $this->_app->response->headers->set('Content-Type', 'application/json');
282            $this->_app->response->setBody(json_encode([
283                'success' => false,
284                'error' => $e->getMessage()
285            ]));
286        }
287    }
288
289    /**
290     * DELETE /:typeNum/api/employee-invitations/:id
291     * Revoke invitation
292     *
293     * @param string $typeNum Store identifier
294     * @param int $invitationId Invitation ID
295     */
296    public function revokeInvitation($typeNum, $invitationId)
297    {
298        // Check permissions
299        if (!$this->_app->user->checkStoreGroup($typeNum) ||
300            !$this->_app->user->checkAccess('create_user')) {
301            $this->_app->response->setStatus(403);
302            $this->_app->response->headers->set('Content-Type', 'application/json');
303            $this->_app->response->setBody(json_encode([
304                'success' => false,
305                'error' => 'Access denied'
306            ]));
307            return;
308        }
309
310        try {
311            $centralDb = dbConnectByName('kiosk_users');
312            $invitationManager = new EmployeeInvitationManager($centralDb);
313
314            $success = $invitationManager->revokeInvitation($invitationId);
315
316            $this->_app->response->headers->set('Content-Type', 'application/json');
317            $this->_app->response->setBody(json_encode([
318                'success' => $success,
319                'message' => 'Invitation revoked successfully'
320            ]));
321        } catch (Exception $e) {
322            error_log("EmployeeInvitationController::revokeInvitation error: " . $e->getMessage());
323            $this->_app->response->setStatus(400);
324            $this->_app->response->headers->set('Content-Type', 'application/json');
325            $this->_app->response->setBody(json_encode([
326                'success' => false,
327                'error' => $e->getMessage()
328            ]));
329        }
330    }
331
332    /**
333     * GET /invite/:token
334     * Show invitation page (public, no auth required)
335     *
336     * @param string $token Invitation token
337     */
338    public function showInvitationPage($token)
339    {
340        try {
341            $centralDb = dbConnectByName('kiosk_users');
342            $invitationManager = new EmployeeInvitationManager($centralDb);
343
344            $data = $invitationManager->validateToken($token);
345
346            if (!$data) {
347                // Invalid or expired token - show error page
348                $this->_app->render('common/invite-invalid.html', [
349                    'page' => [
350                        'title' => 'Invalid Invitation'
351                    ]
352                ]);
353                return;
354            }
355
356            $invitation = $data['invitation'];
357            $employee = $data['employee'];
358            $store = $data['store'];
359
360            $storeName = $store ? $store['storeType'] . ' #' . $store['storeNum'] : $invitation['typeNum'];
361
362            // Show invitation acceptance page
363            $this->_app->render('common/invite.html', [
364                'page' => [
365                    'title' => 'Complete Your Account Setup'
366                ],
367                'token' => $token,
368                'employee' => [
369                    'fullName' => trim($employee['employeeFirstName'] . ' ' . $employee['employeeLastName']),
370                    'firstName' => $employee['employeeFirstName']
371                ],
372                'storeName' => $storeName,
373                'invitation' => $invitation
374            ]);
375
376        } catch (Exception $e) {
377            error_log("EmployeeInvitationController::showInvitationPage error: " . $e->getMessage());
378            $this->_app->render('common/invite-invalid.html', [
379                'page' => [
380                    'title' => 'Error'
381                ],
382                'error' => 'An error occurred while loading the invitation'
383            ]);
384        }
385    }
386
387    /**
388     * GET /api/invite/:token/validate
389     * Validate invitation token via API (public, no auth required)
390     *
391     * @param string $token Invitation token
392     */
393    public function validateInvitation($token)
394    {
395        try {
396            $centralDb = dbConnectByName('kiosk_users');
397            $invitationManager = new EmployeeInvitationManager($centralDb);
398
399            $data = $invitationManager->validateToken($token);
400
401            if (!$data) {
402                $this->_app->response->setStatus(404);
403                $this->_app->response->headers->set('Content-Type', 'application/json');
404                $this->_app->response->setBody(json_encode([
405                    'success' => false,
406                    'error' => 'Invalid or expired invitation'
407                ]));
408                return;
409            }
410
411            $this->_app->response->headers->set('Content-Type', 'application/json');
412            $this->_app->response->setBody(json_encode([
413                'success' => true,
414                'data' => $data
415            ]));
416        } catch (Exception $e) {
417            error_log("EmployeeInvitationController::validateInvitation error: " . $e->getMessage());
418            $this->_app->response->setStatus(500);
419            $this->_app->response->headers->set('Content-Type', 'application/json');
420            $this->_app->response->setBody(json_encode([
421                'success' => false,
422                'error' => 'Validation failed'
423            ]));
424        }
425    }
426
427    /**
428     * POST /invite/:token
429     * Complete registration (public, no auth required)
430     *
431     * @param string $token Invitation token
432     */
433    public function completeRegistration($token)
434    {
435        try {
436            $data = $this->_app->request->post();
437
438            $username = $data['username'] ?? '';
439            $password = $data['password'] ?? '';
440
441            if (empty($username) || empty($password)) {
442                throw new Exception("Username and password are required");
443            }
444
445            $centralDb = dbConnectByName('kiosk_users');
446            $linkManager = new UserEmployeeLinkManager($centralDb);
447            $invitationManager = new EmployeeInvitationManager($centralDb);
448            $promotion = new UserEmployeePromotion($centralDb, $linkManager, $invitationManager);
449
450            $result = $promotion->completeInvitation($token, $username, $password);
451
452            $this->_app->response->setStatus(201);
453            $this->_app->response->headers->set('Content-Type', 'application/json');
454            $this->_app->response->setBody(json_encode([
455                'success' => true,
456                'user' => [
457                    'id' => $result['user']['id'],
458                    'username' => $result['user']['user_name'],
459                    'displayName' => $result['user']['display_name']
460                ],
461                'message' => 'Account created successfully'
462            ]));
463        } catch (Exception $e) {
464            error_log("EmployeeInvitationController::completeRegistration error: " . $e->getMessage());
465            $this->_app->response->setStatus(400);
466            $this->_app->response->headers->set('Content-Type', 'application/json');
467            $this->_app->response->setBody(json_encode([
468                'success' => false,
469                'error' => $e->getMessage()
470            ]));
471        }
472    }
473
474    /**
475     * GET /api/check-username/:username
476     * Check if username is available (public, no auth required)
477     *
478     * @param string $username Username to check
479     */
480    public function checkUsernameAvailability($username)
481    {
482        try {
483            $centralDb = dbConnectByName('kiosk_users');
484            $linkManager = new UserEmployeeLinkManager($centralDb);
485            $invitationManager = new EmployeeInvitationManager($centralDb);
486            $promotion = new UserEmployeePromotion($centralDb, $linkManager, $invitationManager);
487
488            $available = $promotion->isUsernameAvailable($username);
489
490            $this->_app->response->headers->set('Content-Type', 'application/json');
491            $this->_app->response->setBody(json_encode([
492                'success' => true,
493                'available' => $available
494            ]));
495        } catch (Exception $e) {
496            error_log("EmployeeInvitationController::checkUsernameAvailability error: " . $e->getMessage());
497            $this->_app->response->setStatus(500);
498            $this->_app->response->headers->set('Content-Type', 'application/json');
499            $this->_app->response->setBody(json_encode([
500                'success' => false,
501                'error' => 'Failed to check username'
502            ]));
503        }
504    }
505}