Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 159
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
UserEmployeePromotion
0.00% covered (danger)
0.00%
0 / 159
0.00% covered (danger)
0.00%
0 / 7
870
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
 promoteEmployee
0.00% covered (danger)
0.00%
0 / 64
0.00% covered (danger)
0.00%
0 / 1
110
 completeInvitation
0.00% covered (danger)
0.00%
0 / 62
0.00% covered (danger)
0.00%
0 / 1
90
 isUsernameAvailable
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 isEmailAvailable
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 validatePassword
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
 getBuyerGroupId
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace BuyerKiosk\UserEmployee;
4
5use PDO;
6use Exception;
7
8/**
9 * Handles promoting employees to full uf_user accounts
10 *
11 * This class manages the process of creating uf_user accounts for employees,
12 * either through admin-initiated promotion or employee self-registration via invitation.
13 */
14class UserEmployeePromotion
15{
16    private PDO $centralDb;
17    private UserEmployeeLinkManager $linkManager;
18    private EmployeeInvitationManager $invitationManager;
19
20    /**
21     * Constructor
22     *
23     * @param PDO $centralDb Connection to kiosk_users database
24     * @param UserEmployeeLinkManager $linkManager Link manager instance
25     * @param EmployeeInvitationManager $invitationManager Invitation manager instance
26     */
27    public function __construct(
28        PDO $centralDb,
29        UserEmployeeLinkManager $linkManager,
30        EmployeeInvitationManager $invitationManager
31    ) {
32        $this->centralDb = $centralDb;
33        $this->linkManager = $linkManager;
34        $this->invitationManager = $invitationManager;
35    }
36
37    /**
38     * Promote an employee to a full uf_user account (admin-initiated)
39     *
40     * @param string $typeNum Store identifier
41     * @param int $employeeId Employee ID to promote
42     * @param string $username Desired username
43     * @param string $email Email address
44     * @param string $password Initial password
45     * @param array $groups Group IDs to assign
46     * @param int $createdBy Admin user ID
47     * @return array Created user data with link info
48     * @throws Exception If validation fails or user creation fails
49     */
50    public function promoteEmployee(
51        string $typeNum,
52        int $employeeId,
53        string $username,
54        string $email,
55        string $password,
56        array $groups,
57        int $createdBy
58    ): array {
59        // Validate inputs
60        if (!$this->isUsernameAvailable($username)) {
61            throw new Exception("Username is already taken");
62        }
63
64        if (!$this->isEmailAvailable($email)) {
65            throw new Exception("Email is already registered");
66        }
67
68        $passwordErrors = $this->validatePassword($password);
69        if (!empty($passwordErrors)) {
70            throw new Exception("Password validation failed: " . implode(', ', $passwordErrors));
71        }
72
73        // Check if employee exists and is not already linked
74        if ($this->linkManager->hasLinkedUser($typeNum, $employeeId)) {
75            throw new Exception("Employee already has a linked user account");
76        }
77
78        // Get employee data
79        $storeDb = dbConnectByName('kiosk_' . $typeNum);
80        $empStmt = $storeDb->prepare(
81            "SELECT * FROM employees WHERE employeeID = :employeeId"
82        );
83        $empStmt->bindValue(':employeeId', $employeeId, PDO::PARAM_INT);
84        $empStmt->execute();
85        $employee = $empStmt->fetch(PDO::FETCH_ASSOC);
86
87        if (!$employee) {
88            throw new Exception("Employee not found");
89        }
90
91        // Begin transaction
92        $this->centralDb->beginTransaction();
93
94        try {
95            // Create uf_user record
96            $displayName = trim($employee['employeeFirstName'] . ' ' . $employee['employeeLastName']);
97
98            // Hash password using existing Authentication class if available
99            if (class_exists('\UserFrosting\Authentication')) {
100                $hashedPassword = \UserFrosting\Authentication::hashPassword($password);
101            } else {
102                // Fallback to password_hash
103                $hashedPassword = password_hash($password, PASSWORD_DEFAULT);
104            }
105
106            $insertStmt = $this->centralDb->prepare(
107                "INSERT INTO uf_user (user_name, display_name, email, password, active, enabled, primary_group_id, sign_up_stamp)
108                 VALUES (:username, :displayName, :email, :password, 1, 1, :primaryGroup, NOW())"
109            );
110            $insertStmt->bindValue(':username', $username, PDO::PARAM_STR);
111            $insertStmt->bindValue(':displayName', $displayName, PDO::PARAM_STR);
112            $insertStmt->bindValue(':email', $email, PDO::PARAM_STR);
113            $insertStmt->bindValue(':password', $hashedPassword, PDO::PARAM_STR);
114
115            // Use first group as primary, or default to a buyer group
116            $primaryGroup = !empty($groups) ? $groups[0] : $this->getBuyerGroupId();
117            $insertStmt->bindValue(':primaryGroup', $primaryGroup, PDO::PARAM_INT);
118            $insertStmt->execute();
119
120            $userId = (int) $this->centralDb->lastInsertId();
121
122            // Assign to groups
123            foreach ($groups as $groupId) {
124                $groupStmt = $this->centralDb->prepare(
125                    "INSERT INTO uf_user_group (user_id, group_id) VALUES (:userId, :groupId)"
126                );
127                $groupStmt->bindValue(':userId', $userId, PDO::PARAM_INT);
128                $groupStmt->bindValue(':groupId', $groupId, PDO::PARAM_INT);
129                $groupStmt->execute();
130            }
131
132            // Create link
133            $linkId = $this->linkManager->createLink(
134                $userId,
135                $typeNum,
136                $employeeId,
137                'promotion',
138                $createdBy
139            );
140
141            // Commit transaction
142            $this->centralDb->commit();
143
144            // Get created user
145            $userStmt = $this->centralDb->prepare(
146                "SELECT * FROM uf_user WHERE id = :id"
147            );
148            $userStmt->bindValue(':id', $userId, PDO::PARAM_INT);
149            $userStmt->execute();
150            $user = $userStmt->fetch(PDO::FETCH_ASSOC);
151
152            return [
153                'user' => $user,
154                'linkId' => $linkId,
155                'employeeId' => $employeeId
156            ];
157
158        } catch (Exception $e) {
159            $this->centralDb->rollBack();
160            throw $e;
161        }
162    }
163
164    /**
165     * Complete self-registration from invitation
166     *
167     * @param string $token Invitation token
168     * @param string $username Desired username
169     * @param string $password Chosen password
170     * @return array Created user data with link info
171     * @throws Exception If token invalid or registration fails
172     */
173    public function completeInvitation(
174        string $token,
175        string $username,
176        string $password
177    ): array {
178        // Validate token
179        $invitationData = $this->invitationManager->validateToken($token);
180        if (!$invitationData) {
181            throw new Exception("Invalid or expired invitation");
182        }
183
184        $invitation = EmployeeInvitation::fromRow($invitationData['invitation']);
185        $employee = $invitationData['employee'];
186        $typeNum = $invitation->getTypeNum();
187        $employeeId = $invitation->getEmployeeId();
188
189        // Validate username and password
190        if (!$this->isUsernameAvailable($username)) {
191            throw new Exception("Username is already taken");
192        }
193
194        $passwordErrors = $this->validatePassword($password);
195        if (!empty($passwordErrors)) {
196            throw new Exception("Password validation failed: " . implode(', ', $passwordErrors));
197        }
198
199        // Check if employee is already linked
200        if ($this->linkManager->hasLinkedUser($typeNum, $employeeId)) {
201            throw new Exception("Employee already has a linked user account");
202        }
203
204        // Begin transaction
205        $this->centralDb->beginTransaction();
206
207        try {
208            // Create uf_user record
209            $displayName = trim($employee['employeeFirstName'] . ' ' . $employee['employeeLastName']);
210
211            // Hash password
212            if (class_exists('\UserFrosting\Authentication')) {
213                $hashedPassword = \UserFrosting\Authentication::hashPassword($password);
214            } else {
215                $hashedPassword = password_hash($password, PASSWORD_DEFAULT);
216            }
217
218            $defaultGroups = $invitation->getDefaultGroupsArray();
219            $primaryGroup = !empty($defaultGroups) ? $defaultGroups[0] : $this->getBuyerGroupId();
220
221            $insertStmt = $this->centralDb->prepare(
222                "INSERT INTO uf_user (user_name, display_name, email, password, active, enabled, primary_group_id, sign_up_stamp)
223                 VALUES (:username, :displayName, :email, :password, 1, 1, :primaryGroup, NOW())"
224            );
225            $insertStmt->bindValue(':username', $username, PDO::PARAM_STR);
226            $insertStmt->bindValue(':displayName', $displayName, PDO::PARAM_STR);
227            $insertStmt->bindValue(':email', $invitation->getEmail(), PDO::PARAM_STR);
228            $insertStmt->bindValue(':password', $hashedPassword, PDO::PARAM_STR);
229            $insertStmt->bindValue(':primaryGroup', $primaryGroup, PDO::PARAM_INT);
230            $insertStmt->execute();
231
232            $userId = (int) $this->centralDb->lastInsertId();
233
234            // Assign to groups
235            foreach ($defaultGroups as $groupId) {
236                $groupStmt = $this->centralDb->prepare(
237                    "INSERT INTO uf_user_group (user_id, group_id) VALUES (:userId, :groupId)"
238                );
239                $groupStmt->bindValue(':userId', $userId, PDO::PARAM_INT);
240                $groupStmt->bindValue(':groupId', $groupId, PDO::PARAM_INT);
241                $groupStmt->execute();
242            }
243
244            // Create link
245            $linkId = $this->linkManager->createLink(
246                $userId,
247                $typeNum,
248                $employeeId,
249                'promotion',
250                null // Self-registration, no linkedBy
251            );
252
253            // Mark invitation as used
254            $this->invitationManager->markUsed($token);
255
256            // Commit transaction
257            $this->centralDb->commit();
258
259            // Get created user
260            $userStmt = $this->centralDb->prepare(
261                "SELECT * FROM uf_user WHERE id = :id"
262            );
263            $userStmt->bindValue(':id', $userId, PDO::PARAM_INT);
264            $userStmt->execute();
265            $user = $userStmt->fetch(PDO::FETCH_ASSOC);
266
267            return [
268                'user' => $user,
269                'linkId' => $linkId,
270                'employeeId' => $employeeId
271            ];
272
273        } catch (Exception $e) {
274            $this->centralDb->rollBack();
275            throw $e;
276        }
277    }
278
279    /**
280     * Check if username is available
281     *
282     * @param string $username Username to check
283     * @return bool True if username is available
284     */
285    public function isUsernameAvailable(string $username): bool
286    {
287        $stmt = $this->centralDb->prepare(
288            "SELECT COUNT(*) as count FROM uf_user WHERE user_name = :username"
289        );
290        $stmt->bindValue(':username', $username, PDO::PARAM_STR);
291        $stmt->execute();
292        $result = $stmt->fetch(PDO::FETCH_ASSOC);
293        return $result['count'] == 0;
294    }
295
296    /**
297     * Check if email is available
298     *
299     * @param string $email Email to check
300     * @return bool True if email is available
301     */
302    public function isEmailAvailable(string $email): bool
303    {
304        $stmt = $this->centralDb->prepare(
305            "SELECT COUNT(*) as count FROM uf_user WHERE email = :email"
306        );
307        $stmt->bindValue(':email', $email, PDO::PARAM_STR);
308        $stmt->execute();
309        $result = $stmt->fetch(PDO::FETCH_ASSOC);
310        return $result['count'] == 0;
311    }
312
313    /**
314     * Validate password strength
315     *
316     * @param string $password Password to validate
317     * @return array Array of error messages (empty if valid)
318     */
319    public function validatePassword(string $password): array
320    {
321        $errors = [];
322
323        if (strlen($password) < 8) {
324            $errors[] = "Password must be at least 8 characters long";
325        }
326
327        if (!preg_match('/[A-Z]/', $password)) {
328            $errors[] = "Password must contain at least one uppercase letter";
329        }
330
331        if (!preg_match('/[a-z]/', $password)) {
332            $errors[] = "Password must contain at least one lowercase letter";
333        }
334
335        if (!preg_match('/[0-9]/', $password)) {
336            $errors[] = "Password must contain at least one number";
337        }
338
339        return $errors;
340    }
341
342    /**
343     * Get the "Buyer" group ID
344     *
345     * @return int Group ID for the buyer group
346     */
347    private function getBuyerGroupId(): int
348    {
349        $stmt = $this->centralDb->prepare(
350            "SELECT id FROM uf_group WHERE name = 'Buyer' LIMIT 1"
351        );
352        $stmt->execute();
353        $result = $stmt->fetch(PDO::FETCH_ASSOC);
354
355        // Return Buyer group ID if found, otherwise return 2 as a safe default
356        return $result ? (int) $result['id'] : 2;
357    }
358}