Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 361
0.00% covered (danger)
0.00%
0 / 104
CRAP
0.00% covered (danger)
0.00%
0 / 1
UnifiedUser
0.00% covered (danger)
0.00%
0 / 361
0.00% covered (danger)
0.00%
0 / 104
34782
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 find
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 findByUsername
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 findByEmail
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 findByExternalId
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 hydrate
0.00% covered (danger)
0.00%
0 / 86
0.00% covered (danger)
0.00%
0 / 1
1980
 save
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 insert
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 update
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 getInsertParams
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
72
 setPassword
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getStoreAssignments
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 hasStoreAccess
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 getStoreTypeNums
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getGroups
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 getStoreGroups
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getPermissions
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
30
 getPermissionHooks
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 checkAccess
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
42
 isGuest
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isLocked
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 canAuthenticate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
20
 isOnLeave
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 isTerminated
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 clearCache
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getFullName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDisplayNameOrUsername
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 toArray
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
6
 getId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUsername
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getEmail
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPasswordHash
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDisplayName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getFirstName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLastName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPhone
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPhotoUrl
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 hasAvatarOverride
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPosition
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getHourlyRate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getHireDate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTerminationDate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLeaveStartDate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLeaveEndDate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getEmergencyContactName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getEmergencyContactPhone
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSource
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getExternalId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLastSyncedAt
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 canLogin
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getAccountType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isEnabled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isActive
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getActivationToken
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getActivationTokenExpiresAt
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPasswordResetToken
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPasswordResetExpiresAt
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 hasMfaEnabled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMfaSecret
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMfaBackupCodes
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMfaVerifiedAt
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLocale
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 wantsDailyReport
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTimezone
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCreatedAt
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUpdatedAt
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLastLoginAt
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLastLoginIp
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getFailedLoginAttempts
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLockedUntil
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setUsername
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setEmail
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setDisplayName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setFirstName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setLastName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setPhone
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setPhotoUrl
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setAvatarOverride
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setPosition
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setHourlyRate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setHireDate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setTerminationDate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setLeaveStartDate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setLeaveEndDate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setEmergencyContactName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setEmergencyContactPhone
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setSource
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setExternalId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setLastSyncedAt
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setCanLogin
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setAccountType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setEnabled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setActive
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setActivationToken
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setActivationTokenExpiresAt
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setPasswordResetToken
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setPasswordResetExpiresAt
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setMfaEnabled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setMfaSecret
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setMfaBackupCodes
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setMfaVerifiedAt
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setLocale
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setDailyReport
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setTimezone
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * UnifiedUser Model
4 *
5 * Domain model representing a user in the unified authentication system.
6 * Maps to the `users` table in the central kiosk_users database.
7 *
8 * @package BuyerKiosk\Auth\Models
9 */
10
11namespace BuyerKiosk\Auth\Models;
12
13class UnifiedUser
14{
15    // =========================================================================
16    // Core Identity Properties
17    // =========================================================================
18
19    /** @var int|null User ID (null for new users) */
20    private ?int $id = null;
21
22    /** @var string|null Login username */
23    private ?string $username = null;
24
25    /** @var string|null Email address */
26    private ?string $email = null;
27
28    /** @var string|null Hashed password (Argon2id preferred) */
29    private ?string $password = null;
30
31    // =========================================================================
32    // Profile Properties
33    // =========================================================================
34
35    /** @var string|null Display name shown in UI */
36    private ?string $displayName = null;
37
38    /** @var string|null First name */
39    private ?string $firstName = null;
40
41    /** @var string|null Last name */
42    private ?string $lastName = null;
43
44    /** @var string|null Phone number */
45    private ?string $phone = null;
46
47    /** @var string|null Profile photo URL */
48    private ?string $photoUrl = null;
49
50    /** @var bool Allow custom avatar vs synced photo */
51    private bool $avatarOverride = false;
52
53    // =========================================================================
54    // Employment Properties
55    // =========================================================================
56
57    /** @var string|null Job title/position */
58    private ?string $position = null;
59
60    /** @var string|null Hourly rate as string to preserve precision */
61    private ?string $hourlyRate = null;
62
63    /** @var string|null Hire date (Y-m-d format) */
64    private ?string $hireDate = null;
65
66    /** @var string|null Termination date (Y-m-d format) */
67    private ?string $terminationDate = null;
68
69    /** @var string|null Leave start date (Y-m-d format) */
70    private ?string $leaveStartDate = null;
71
72    /** @var string|null Leave end date (Y-m-d format) */
73    private ?string $leaveEndDate = null;
74
75    /** @var string|null Emergency contact name */
76    private ?string $emergencyContactName = null;
77
78    /** @var string|null Emergency contact phone */
79    private ?string $emergencyContactPhone = null;
80
81    // =========================================================================
82    // External Sync Properties
83    // =========================================================================
84
85    /** @var string Data source: homegrown, wheniwork, homebase, system */
86    private string $source = 'system';
87
88    /** @var string|null ID in external system */
89    private ?string $externalId = null;
90
91    /** @var string|null Last sync timestamp from external provider */
92    private ?string $lastSyncedAt = null;
93
94    // =========================================================================
95    // Access Control Properties
96    // =========================================================================
97
98    /** @var bool Whether user can authenticate */
99    private bool $canLogin = false;
100
101    /** @var string Account type: employee, user, admin, system */
102    private string $accountType = 'employee';
103
104    /** @var bool Account enabled flag */
105    private bool $enabled = true;
106
107    /** @var bool Account activated flag */
108    private bool $active = false;
109
110    /** @var string|null Account activation token */
111    private ?string $activationToken = null;
112
113    /** @var string|null Activation token expiration timestamp */
114    private ?string $activationTokenExpiresAt = null;
115
116    /** @var string|null Password reset token */
117    private ?string $passwordResetToken = null;
118
119    /** @var string|null Password reset token expiration timestamp */
120    private ?string $passwordResetExpiresAt = null;
121
122    // =========================================================================
123    // MFA Properties
124    // =========================================================================
125
126    /** @var bool Whether MFA is enabled */
127    private bool $mfaEnabled = false;
128
129    /** @var string|null Encrypted TOTP secret */
130    private ?string $mfaSecret = null;
131
132    /** @var array|null Hashed backup codes */
133    private ?array $mfaBackupCodes = null;
134
135    /** @var string|null MFA verification timestamp */
136    private ?string $mfaVerifiedAt = null;
137
138    // =========================================================================
139    // Preferences Properties
140    // =========================================================================
141
142    /** @var string Locale setting */
143    private string $locale = 'en_US';
144
145    /** @var bool Receive daily report emails */
146    private bool $dailyReport = false;
147
148    /** @var string Timezone setting */
149    private string $timezone = 'America/Los_Angeles';
150
151    // =========================================================================
152    // Audit Properties
153    // =========================================================================
154
155    /** @var string|null Creation timestamp */
156    private ?string $createdAt = null;
157
158    /** @var string|null Last update timestamp */
159    private ?string $updatedAt = null;
160
161    /** @var string|null Last login timestamp */
162    private ?string $lastLoginAt = null;
163
164    /** @var string|null Last login IP address */
165    private ?string $lastLoginIp = null;
166
167    /** @var int Failed login attempt count */
168    private int $failedLoginAttempts = 0;
169
170    /** @var string|null Account lockout expiration */
171    private ?string $lockedUntil = null;
172
173    // =========================================================================
174    // Database Connection & Lazy-loaded Relations
175    // =========================================================================
176
177    /** @var \PDO Database connection */
178    private \PDO $db;
179
180    /** @var array|null Cached store assignments */
181    private ?array $storeAssignments = null;
182
183    /** @var array|null Cached groups */
184    private ?array $groups = null;
185
186    /** @var array|null Cached permissions */
187    private ?array $permissions = null;
188
189    // =========================================================================
190    // Constructor & Factory Methods
191    // =========================================================================
192
193    /**
194     * Constructor
195     *
196     * @param \PDO $db Database connection
197     * @param array $properties Optional initial property values
198     */
199    public function __construct(\PDO $db, array $properties = [])
200    {
201        $this->db = $db;
202        $this->hydrate($properties);
203    }
204
205    /**
206     * Find a user by ID
207     *
208     * @param \PDO $db Database connection
209     * @param int $id User ID
210     * @return self|null User instance or null if not found
211     */
212    public static function find(\PDO $db, int $id): ?self
213    {
214        $stmt = $db->prepare("SELECT * FROM users WHERE id = :id LIMIT 1");
215        $stmt->execute(['id' => $id]);
216        $row = $stmt->fetch(\PDO::FETCH_ASSOC);
217
218        if (!$row) {
219            return null;
220        }
221
222        return new self($db, $row);
223    }
224
225    /**
226     * Find a user by username
227     *
228     * @param \PDO $db Database connection
229     * @param string $username Username
230     * @return self|null User instance or null if not found
231     */
232    public static function findByUsername(\PDO $db, string $username): ?self
233    {
234        $stmt = $db->prepare("SELECT * FROM users WHERE username = :username LIMIT 1");
235        $stmt->execute(['username' => $username]);
236        $row = $stmt->fetch(\PDO::FETCH_ASSOC);
237
238        if (!$row) {
239            return null;
240        }
241
242        return new self($db, $row);
243    }
244
245    /**
246     * Find a user by email
247     *
248     * @param \PDO $db Database connection
249     * @param string $email Email address
250     * @return self|null User instance or null if not found
251     */
252    public static function findByEmail(\PDO $db, string $email): ?self
253    {
254        $stmt = $db->prepare("SELECT * FROM users WHERE email = :email LIMIT 1");
255        $stmt->execute(['email' => $email]);
256        $row = $stmt->fetch(\PDO::FETCH_ASSOC);
257
258        if (!$row) {
259            return null;
260        }
261
262        return new self($db, $row);
263    }
264
265    /**
266     * Find a user by external ID and source
267     *
268     * @param \PDO $db Database connection
269     * @param string $source External source (wheniwork, homebase, etc.)
270     * @param string $externalId External ID
271     * @return self|null User instance or null if not found
272     */
273    public static function findByExternalId(\PDO $db, string $source, string $externalId): ?self
274    {
275        $stmt = $db->prepare("SELECT * FROM users WHERE source = :source AND externalId = :externalId LIMIT 1");
276        $stmt->execute(['source' => $source, 'externalId' => $externalId]);
277        $row = $stmt->fetch(\PDO::FETCH_ASSOC);
278
279        if (!$row) {
280            return null;
281        }
282
283        return new self($db, $row);
284    }
285
286    // =========================================================================
287    // Hydration & Persistence
288    // =========================================================================
289
290    /**
291     * Hydrate the model from an array of properties
292     *
293     * @param array $properties Property values from database or input
294     */
295    private function hydrate(array $properties): void
296    {
297        // Core identity
298        if (isset($properties['id'])) {
299            $this->id = (int)$properties['id'];
300        }
301        if (array_key_exists('username', $properties)) {
302            $this->username = $properties['username'];
303        }
304        if (array_key_exists('email', $properties)) {
305            $this->email = $properties['email'];
306        }
307        if (array_key_exists('password', $properties)) {
308            $this->password = $properties['password'];
309        }
310
311        // Profile
312        if (array_key_exists('displayName', $properties)) {
313            $this->displayName = $properties['displayName'];
314        }
315        if (array_key_exists('firstName', $properties)) {
316            $this->firstName = $properties['firstName'];
317        }
318        if (array_key_exists('lastName', $properties)) {
319            $this->lastName = $properties['lastName'];
320        }
321        if (array_key_exists('phone', $properties)) {
322            $this->phone = $properties['phone'];
323        }
324        if (array_key_exists('photoUrl', $properties)) {
325            $this->photoUrl = $properties['photoUrl'];
326        }
327        if (isset($properties['avatarOverride'])) {
328            $this->avatarOverride = (bool)$properties['avatarOverride'];
329        }
330
331        // Employment
332        if (array_key_exists('position', $properties)) {
333            $this->position = $properties['position'];
334        }
335        if (array_key_exists('hourlyRate', $properties)) {
336            $this->hourlyRate = $properties['hourlyRate'];
337        }
338        if (array_key_exists('hireDate', $properties)) {
339            $this->hireDate = $properties['hireDate'];
340        }
341        if (array_key_exists('terminationDate', $properties)) {
342            $this->terminationDate = $properties['terminationDate'];
343        }
344        if (array_key_exists('leaveStartDate', $properties)) {
345            $this->leaveStartDate = $properties['leaveStartDate'];
346        }
347        if (array_key_exists('leaveEndDate', $properties)) {
348            $this->leaveEndDate = $properties['leaveEndDate'];
349        }
350        if (array_key_exists('emergencyContactName', $properties)) {
351            $this->emergencyContactName = $properties['emergencyContactName'];
352        }
353        if (array_key_exists('emergencyContactPhone', $properties)) {
354            $this->emergencyContactPhone = $properties['emergencyContactPhone'];
355        }
356
357        // External sync
358        if (isset($properties['source'])) {
359            $this->source = $properties['source'];
360        }
361        if (array_key_exists('externalId', $properties)) {
362            $this->externalId = $properties['externalId'];
363        }
364        if (array_key_exists('lastSyncedAt', $properties)) {
365            $this->lastSyncedAt = $properties['lastSyncedAt'];
366        }
367
368        // Access control
369        if (isset($properties['canLogin'])) {
370            $this->canLogin = (bool)$properties['canLogin'];
371        }
372        if (isset($properties['accountType'])) {
373            $this->accountType = $properties['accountType'];
374        }
375        if (isset($properties['enabled'])) {
376            $this->enabled = (bool)$properties['enabled'];
377        }
378        if (isset($properties['active'])) {
379            $this->active = (bool)$properties['active'];
380        }
381        if (array_key_exists('activationToken', $properties)) {
382            $this->activationToken = $properties['activationToken'];
383        }
384        if (array_key_exists('activationTokenExpiresAt', $properties)) {
385            $this->activationTokenExpiresAt = $properties['activationTokenExpiresAt'];
386        }
387        if (array_key_exists('passwordResetToken', $properties)) {
388            $this->passwordResetToken = $properties['passwordResetToken'];
389        }
390        if (array_key_exists('passwordResetExpiresAt', $properties)) {
391            $this->passwordResetExpiresAt = $properties['passwordResetExpiresAt'];
392        }
393
394        // MFA
395        if (isset($properties['mfaEnabled'])) {
396            $this->mfaEnabled = (bool)$properties['mfaEnabled'];
397        }
398        if (array_key_exists('mfaSecret', $properties)) {
399            $this->mfaSecret = $properties['mfaSecret'];
400        }
401        if (array_key_exists('mfaBackupCodes', $properties)) {
402            $this->mfaBackupCodes = is_string($properties['mfaBackupCodes'])
403                ? json_decode($properties['mfaBackupCodes'], true)
404                : $properties['mfaBackupCodes'];
405        }
406        if (array_key_exists('mfaVerifiedAt', $properties)) {
407            $this->mfaVerifiedAt = $properties['mfaVerifiedAt'];
408        }
409
410        // Preferences
411        if (isset($properties['locale'])) {
412            $this->locale = $properties['locale'];
413        }
414        if (isset($properties['dailyReport'])) {
415            $this->dailyReport = (bool)$properties['dailyReport'];
416        }
417        if (isset($properties['timezone'])) {
418            $this->timezone = $properties['timezone'];
419        }
420
421        // Audit
422        if (array_key_exists('createdAt', $properties)) {
423            $this->createdAt = $properties['createdAt'];
424        }
425        if (array_key_exists('updatedAt', $properties)) {
426            $this->updatedAt = $properties['updatedAt'];
427        }
428        if (array_key_exists('lastLoginAt', $properties)) {
429            $this->lastLoginAt = $properties['lastLoginAt'];
430        }
431        if (array_key_exists('lastLoginIp', $properties)) {
432            $this->lastLoginIp = $properties['lastLoginIp'];
433        }
434        if (isset($properties['failedLoginAttempts'])) {
435            $this->failedLoginAttempts = (int)$properties['failedLoginAttempts'];
436        }
437        if (array_key_exists('lockedUntil', $properties)) {
438            $this->lockedUntil = $properties['lockedUntil'];
439        }
440    }
441
442    /**
443     * Save the user to the database (insert or update)
444     *
445     * @return int The user ID
446     */
447    public function save(): int
448    {
449        if ($this->id === null) {
450            return $this->insert();
451        }
452        return $this->update();
453    }
454
455    /**
456     * Insert a new user
457     *
458     * @return int The new user ID
459     */
460    private function insert(): int
461    {
462        $stmt = $this->db->prepare("
463            INSERT INTO users (
464                username, email, password, displayName, firstName, lastName, phone, photoUrl, avatarOverride,
465                position, hourlyRate, hireDate, terminationDate, leaveStartDate, leaveEndDate,
466                emergencyContactName, emergencyContactPhone,
467                source, externalId, lastSyncedAt,
468                canLogin, accountType, enabled, active,
469                activationToken, activationTokenExpiresAt, passwordResetToken, passwordResetExpiresAt,
470                mfaEnabled, mfaSecret, mfaBackupCodes, mfaVerifiedAt,
471                locale, dailyReport, timezone
472            ) VALUES (
473                :username, :email, :password, :displayName, :firstName, :lastName, :phone, :photoUrl, :avatarOverride,
474                :position, :hourlyRate, :hireDate, :terminationDate, :leaveStartDate, :leaveEndDate,
475                :emergencyContactName, :emergencyContactPhone,
476                :source, :externalId, :lastSyncedAt,
477                :canLogin, :accountType, :enabled, :active,
478                :activationToken, :activationTokenExpiresAt, :passwordResetToken, :passwordResetExpiresAt,
479                :mfaEnabled, :mfaSecret, :mfaBackupCodes, :mfaVerifiedAt,
480                :locale, :dailyReport, :timezone
481            )
482        ");
483
484        $stmt->execute($this->getInsertParams());
485        $this->id = (int)$this->db->lastInsertId();
486
487        return $this->id;
488    }
489
490    /**
491     * Update an existing user
492     *
493     * @return int The user ID
494     */
495    private function update(): int
496    {
497        $stmt = $this->db->prepare("
498            UPDATE users SET
499                username = :username, email = :email, password = :password,
500                displayName = :displayName, firstName = :firstName, lastName = :lastName,
501                phone = :phone, photoUrl = :photoUrl, avatarOverride = :avatarOverride,
502                position = :position, hourlyRate = :hourlyRate,
503                hireDate = :hireDate, terminationDate = :terminationDate,
504                leaveStartDate = :leaveStartDate, leaveEndDate = :leaveEndDate,
505                emergencyContactName = :emergencyContactName, emergencyContactPhone = :emergencyContactPhone,
506                source = :source, externalId = :externalId, lastSyncedAt = :lastSyncedAt,
507                canLogin = :canLogin, accountType = :accountType, enabled = :enabled, active = :active,
508                activationToken = :activationToken, activationTokenExpiresAt = :activationTokenExpiresAt,
509                passwordResetToken = :passwordResetToken, passwordResetExpiresAt = :passwordResetExpiresAt,
510                mfaEnabled = :mfaEnabled, mfaSecret = :mfaSecret, mfaBackupCodes = :mfaBackupCodes, mfaVerifiedAt = :mfaVerifiedAt,
511                locale = :locale, dailyReport = :dailyReport, timezone = :timezone
512            WHERE id = :id
513        ");
514
515        $params = $this->getInsertParams();
516        $params['id'] = $this->id;
517        $stmt->execute($params);
518
519        return $this->id;
520    }
521
522    /**
523     * Get parameters for insert/update statements
524     *
525     * @return array Parameter values
526     */
527    private function getInsertParams(): array
528    {
529        return [
530            'username' => $this->username,
531            'email' => $this->email,
532            'password' => $this->password,
533            'displayName' => $this->displayName,
534            'firstName' => $this->firstName,
535            'lastName' => $this->lastName,
536            'phone' => $this->phone,
537            'photoUrl' => $this->photoUrl,
538            'avatarOverride' => $this->avatarOverride ? 1 : 0,
539            'position' => $this->position,
540            'hourlyRate' => $this->hourlyRate,
541            'hireDate' => $this->hireDate,
542            'terminationDate' => $this->terminationDate,
543            'leaveStartDate' => $this->leaveStartDate,
544            'leaveEndDate' => $this->leaveEndDate,
545            'emergencyContactName' => $this->emergencyContactName,
546            'emergencyContactPhone' => $this->emergencyContactPhone,
547            'source' => $this->source,
548            'externalId' => $this->externalId,
549            'lastSyncedAt' => $this->lastSyncedAt,
550            'canLogin' => $this->canLogin ? 1 : 0,
551            'accountType' => $this->accountType,
552            'enabled' => $this->enabled ? 1 : 0,
553            'active' => $this->active ? 1 : 0,
554            'activationToken' => $this->activationToken,
555            'activationTokenExpiresAt' => $this->activationTokenExpiresAt,
556            'passwordResetToken' => $this->passwordResetToken,
557            'passwordResetExpiresAt' => $this->passwordResetExpiresAt,
558            'mfaEnabled' => $this->mfaEnabled ? 1 : 0,
559            'mfaSecret' => $this->mfaSecret,
560            'mfaBackupCodes' => $this->mfaBackupCodes !== null ? json_encode($this->mfaBackupCodes) : null,
561            'mfaVerifiedAt' => $this->mfaVerifiedAt,
562            'locale' => $this->locale,
563            'dailyReport' => $this->dailyReport ? 1 : 0,
564            'timezone' => $this->timezone,
565        ];
566    }
567
568    // =========================================================================
569    // Password Management
570    // =========================================================================
571
572    /**
573     * Set a new password (hashes it using Argon2id)
574     *
575     * Note: Password verification is handled by AuthService for consistency
576     * with rate limiting and audit logging.
577     *
578     * @param string $password Plain text password
579     */
580    public function setPassword(string $password): void
581    {
582        $this->password = password_hash($password, PASSWORD_ARGON2ID, [
583            'memory_cost' => 65536,
584            'time_cost' => 3,
585            'threads' => 4
586        ]);
587    }
588
589    // =========================================================================
590    // Store Access Methods
591    // =========================================================================
592
593    /**
594     * Get all store assignments for this user
595     *
596     * @return array List of store assignments with typeNum, role, clockPin, etc.
597     */
598    public function getStoreAssignments(): array
599    {
600        if ($this->storeAssignments !== null) {
601            return $this->storeAssignments;
602        }
603
604        if ($this->id === null) {
605            return [];
606        }
607
608        $stmt = $this->db->prepare("
609            SELECT typeNum, clockPin, drsEmployeeId, role, isActive, assignedAt, deactivatedAt
610            FROM userStoreAssignments
611            WHERE userId = :userId AND isActive = 1
612            ORDER BY typeNum
613        ");
614        $stmt->execute(['userId' => $this->id]);
615        $this->storeAssignments = $stmt->fetchAll(\PDO::FETCH_ASSOC);
616
617        return $this->storeAssignments;
618    }
619
620    /**
621     * Check if user has access to a specific store
622     *
623     * @param string $typeNum Store identifier
624     * @return bool True if user has access
625     */
626    public function hasStoreAccess(string $typeNum): bool
627    {
628        // Check for all_stores permission (super admin)
629        if ($this->checkAccess('all_stores')) {
630            return true;
631        }
632
633        $assignments = $this->getStoreAssignments();
634        foreach ($assignments as $assignment) {
635            if ($assignment['typeNum'] === $typeNum) {
636                return true;
637            }
638        }
639
640        return false;
641    }
642
643    /**
644     * Get store typeNums as a simple array
645     *
646     * @return array List of typeNum strings
647     */
648    public function getStoreTypeNums(): array
649    {
650        return array_column($this->getStoreAssignments(), 'typeNum');
651    }
652
653    // =========================================================================
654    // Group Methods
655    // =========================================================================
656
657    /**
658     * Get all groups this user belongs to
659     *
660     * @return array List of group data with id, name, etc.
661     */
662    public function getGroups(): array
663    {
664        if ($this->groups !== null) {
665            return $this->groups;
666        }
667
668        if ($this->id === null) {
669            return [];
670        }
671
672        $stmt = $this->db->prepare("
673            SELECT g.id, g.name, g.is_default, g.can_delete, g.theme, g.landing_page, g.new_user_title, g.icon
674            FROM userGroups ug
675            JOIN uf_group g ON ug.groupId = g.id
676            WHERE ug.userId = :userId
677            ORDER BY g.name
678        ");
679        $stmt->execute(['userId' => $this->id]);
680        $this->groups = $stmt->fetchAll(\PDO::FETCH_ASSOC);
681
682        return $this->groups;
683    }
684
685    /**
686     * Get store groups (groups matching typeNum pattern)
687     *
688     * @return array List of store groups with typeNum
689     */
690    public function getStoreGroups(): array
691    {
692        $groups = $this->getGroups();
693        $storeGroups = [];
694
695        foreach ($groups as $group) {
696            if (preg_match('/^[a-z]{2}\d{2,}$/', $group['name'])) {
697                $storeGroups[] = ['TypeNum' => $group['name']];
698            }
699        }
700
701        return $storeGroups;
702    }
703
704    // =========================================================================
705    // Permission Methods
706    // =========================================================================
707
708    /**
709     * Get all permissions for this user (from groups + direct permissions)
710     *
711     * @return array List of permission hooks with conditions
712     */
713    public function getPermissions(): array
714    {
715        if ($this->permissions !== null) {
716            return $this->permissions;
717        }
718
719        if ($this->id === null) {
720            return [];
721        }
722
723        // Get permissions from user's groups
724        $groupPermsStmt = $this->db->prepare("
725            SELECT DISTINCT ag.hook, ag.conditions
726            FROM userGroups ug
727            JOIN uf_authorize_group ag ON ug.groupId = ag.group_id
728            WHERE ug.userId = :userId
729        ");
730        $groupPermsStmt->execute(['userId' => $this->id]);
731        $groupPerms = $groupPermsStmt->fetchAll(\PDO::FETCH_ASSOC);
732
733        // Get direct user permissions
734        $userPermsStmt = $this->db->prepare("
735            SELECT hook, conditions FROM userPermissions WHERE userId = :userId
736        ");
737        $userPermsStmt->execute(['userId' => $this->id]);
738        $userPerms = $userPermsStmt->fetchAll(\PDO::FETCH_ASSOC);
739
740        // Merge permissions (user permissions override group permissions for same hook)
741        $permissionMap = [];
742        foreach ($groupPerms as $perm) {
743            $permissionMap[$perm['hook']] = $perm;
744        }
745        foreach ($userPerms as $perm) {
746            $permissionMap[$perm['hook']] = $perm;
747        }
748
749        $this->permissions = array_values($permissionMap);
750        return $this->permissions;
751    }
752
753    /**
754     * Get permission hooks as a simple array
755     *
756     * @return array List of permission hook strings
757     */
758    public function getPermissionHooks(): array
759    {
760        return array_column($this->getPermissions(), 'hook');
761    }
762
763    /**
764     * Check if user has access to a specific permission hook
765     *
766     * @param string $hook Permission hook to check
767     * @param array $params Optional parameters for condition evaluation
768     * @return bool True if user has access
769     */
770    public function checkAccess(string $hook, array $params = []): bool
771    {
772        // System accounts always have full access
773        if ($this->accountType === 'system') {
774            return true;
775        }
776
777        $permissions = $this->getPermissions();
778
779        foreach ($permissions as $permission) {
780            if ($permission['hook'] === $hook) {
781                // If no conditions, permission is granted
782                if (empty($permission['conditions']) || $permission['conditions'] === 'always()') {
783                    return true;
784                }
785
786                // TODO: Evaluate conditions using AccessConditionExpression
787                // For now, grant access if the hook matches
788                return true;
789            }
790        }
791
792        return false;
793    }
794
795    // =========================================================================
796    // Status Checks
797    // =========================================================================
798
799    /**
800     * Check if user is a guest (no ID set)
801     *
802     * @return bool True if guest
803     */
804    public function isGuest(): bool
805    {
806        return $this->id === null;
807    }
808
809    /**
810     * Check if account is currently locked
811     *
812     * @return bool True if locked
813     */
814    public function isLocked(): bool
815    {
816        if ($this->lockedUntil === null) {
817            return false;
818        }
819        return strtotime($this->lockedUntil) > time();
820    }
821
822    /**
823     * Check if user can currently authenticate
824     *
825     * @return bool True if user can login
826     */
827    public function canAuthenticate(): bool
828    {
829        return $this->canLogin && $this->enabled && $this->active && !$this->isLocked();
830    }
831
832    /**
833     * Check if user is on leave
834     *
835     * @return bool True if currently on leave
836     */
837    public function isOnLeave(): bool
838    {
839        if ($this->leaveStartDate === null) {
840            return false;
841        }
842
843        $now = time();
844        $start = strtotime($this->leaveStartDate);
845
846        if ($start > $now) {
847            return false;
848        }
849
850        if ($this->leaveEndDate === null) {
851            return true;
852        }
853
854        return strtotime($this->leaveEndDate) >= $now;
855    }
856
857    /**
858     * Check if user is terminated
859     *
860     * @return bool True if terminated
861     */
862    public function isTerminated(): bool
863    {
864        return $this->terminationDate !== null && strtotime($this->terminationDate) <= time();
865    }
866
867    // =========================================================================
868    // Utility Methods
869    // =========================================================================
870
871    /**
872     * Clear cached relations (call after modifying assignments/groups/permissions)
873     */
874    public function clearCache(): void
875    {
876        $this->storeAssignments = null;
877        $this->groups = null;
878        $this->permissions = null;
879    }
880
881    /**
882     * Get full name (firstName + lastName)
883     *
884     * @return string Full name
885     */
886    public function getFullName(): string
887    {
888        return trim(($this->firstName ?? '') . ' ' . ($this->lastName ?? ''));
889    }
890
891    /**
892     * Get display name with fallback to username
893     *
894     * @return string Display name
895     */
896    public function getDisplayNameOrUsername(): string
897    {
898        return $this->displayName ?? $this->username ?? 'Unknown';
899    }
900
901    /**
902     * Convert user to array for JSON serialization
903     *
904     * @param bool $includePrivate Include sensitive fields
905     * @return array User data
906     */
907    public function toArray(bool $includePrivate = false): array
908    {
909        $data = [
910            'id' => $this->id,
911            'username' => $this->username,
912            'email' => $this->email,
913            'displayName' => $this->displayName,
914            'firstName' => $this->firstName,
915            'lastName' => $this->lastName,
916            'phone' => $this->phone,
917            'photoUrl' => $this->photoUrl,
918            'avatarOverride' => $this->avatarOverride,
919            'position' => $this->position,
920            'source' => $this->source,
921            'canLogin' => $this->canLogin,
922            'accountType' => $this->accountType,
923            'enabled' => $this->enabled,
924            'active' => $this->active,
925            'mfaEnabled' => $this->mfaEnabled,
926            'locale' => $this->locale,
927            'dailyReport' => $this->dailyReport,
928            'timezone' => $this->timezone,
929            'createdAt' => $this->createdAt,
930            'lastLoginAt' => $this->lastLoginAt,
931        ];
932
933        if ($includePrivate) {
934            $data['hourlyRate'] = $this->hourlyRate;
935            $data['hireDate'] = $this->hireDate;
936            $data['terminationDate'] = $this->terminationDate;
937            $data['leaveStartDate'] = $this->leaveStartDate;
938            $data['leaveEndDate'] = $this->leaveEndDate;
939            $data['emergencyContactName'] = $this->emergencyContactName;
940            $data['emergencyContactPhone'] = $this->emergencyContactPhone;
941            $data['externalId'] = $this->externalId;
942            $data['lastSyncedAt'] = $this->lastSyncedAt;
943            $data['failedLoginAttempts'] = $this->failedLoginAttempts;
944            $data['lockedUntil'] = $this->lockedUntil;
945        }
946
947        return $data;
948    }
949
950    // =========================================================================
951    // Getters
952    // =========================================================================
953
954    public function getId(): ?int
955    {
956        return $this->id;
957    }
958
959    public function getUsername(): ?string
960    {
961        return $this->username;
962    }
963
964    public function getEmail(): ?string
965    {
966        return $this->email;
967    }
968
969    public function getPasswordHash(): ?string
970    {
971        return $this->password;
972    }
973
974    public function getDisplayName(): ?string
975    {
976        return $this->displayName;
977    }
978
979    public function getFirstName(): ?string
980    {
981        return $this->firstName;
982    }
983
984    public function getLastName(): ?string
985    {
986        return $this->lastName;
987    }
988
989    public function getPhone(): ?string
990    {
991        return $this->phone;
992    }
993
994    public function getPhotoUrl(): ?string
995    {
996        return $this->photoUrl;
997    }
998
999    public function hasAvatarOverride(): bool
1000    {
1001        return $this->avatarOverride;
1002    }
1003
1004    public function getPosition(): ?string
1005    {
1006        return $this->position;
1007    }
1008
1009    public function getHourlyRate(): ?string
1010    {
1011        return $this->hourlyRate;
1012    }
1013
1014    public function getHireDate(): ?string
1015    {
1016        return $this->hireDate;
1017    }
1018
1019    public function getTerminationDate(): ?string
1020    {
1021        return $this->terminationDate;
1022    }
1023
1024    public function getLeaveStartDate(): ?string
1025    {
1026        return $this->leaveStartDate;
1027    }
1028
1029    public function getLeaveEndDate(): ?string
1030    {
1031        return $this->leaveEndDate;
1032    }
1033
1034    public function getEmergencyContactName(): ?string
1035    {
1036        return $this->emergencyContactName;
1037    }
1038
1039    public function getEmergencyContactPhone(): ?string
1040    {
1041        return $this->emergencyContactPhone;
1042    }
1043
1044    public function getSource(): string
1045    {
1046        return $this->source;
1047    }
1048
1049    public function getExternalId(): ?string
1050    {
1051        return $this->externalId;
1052    }
1053
1054    public function getLastSyncedAt(): ?string
1055    {
1056        return $this->lastSyncedAt;
1057    }
1058
1059    public function canLogin(): bool
1060    {
1061        return $this->canLogin;
1062    }
1063
1064    public function getAccountType(): string
1065    {
1066        return $this->accountType;
1067    }
1068
1069    public function isEnabled(): bool
1070    {
1071        return $this->enabled;
1072    }
1073
1074    public function isActive(): bool
1075    {
1076        return $this->active;
1077    }
1078
1079    public function getActivationToken(): ?string
1080    {
1081        return $this->activationToken;
1082    }
1083
1084    public function getActivationTokenExpiresAt(): ?string
1085    {
1086        return $this->activationTokenExpiresAt;
1087    }
1088
1089    public function getPasswordResetToken(): ?string
1090    {
1091        return $this->passwordResetToken;
1092    }
1093
1094    public function getPasswordResetExpiresAt(): ?string
1095    {
1096        return $this->passwordResetExpiresAt;
1097    }
1098
1099    public function hasMfaEnabled(): bool
1100    {
1101        return $this->mfaEnabled;
1102    }
1103
1104    public function getMfaSecret(): ?string
1105    {
1106        return $this->mfaSecret;
1107    }
1108
1109    public function getMfaBackupCodes(): ?array
1110    {
1111        return $this->mfaBackupCodes;
1112    }
1113
1114    public function getMfaVerifiedAt(): ?string
1115    {
1116        return $this->mfaVerifiedAt;
1117    }
1118
1119    public function getLocale(): string
1120    {
1121        return $this->locale;
1122    }
1123
1124    public function wantsDailyReport(): bool
1125    {
1126        return $this->dailyReport;
1127    }
1128
1129    public function getTimezone(): string
1130    {
1131        return $this->timezone;
1132    }
1133
1134    public function getCreatedAt(): ?string
1135    {
1136        return $this->createdAt;
1137    }
1138
1139    public function getUpdatedAt(): ?string
1140    {
1141        return $this->updatedAt;
1142    }
1143
1144    public function getLastLoginAt(): ?string
1145    {
1146        return $this->lastLoginAt;
1147    }
1148
1149    public function getLastLoginIp(): ?string
1150    {
1151        return $this->lastLoginIp;
1152    }
1153
1154    public function getFailedLoginAttempts(): int
1155    {
1156        return $this->failedLoginAttempts;
1157    }
1158
1159    public function getLockedUntil(): ?string
1160    {
1161        return $this->lockedUntil;
1162    }
1163
1164    // =========================================================================
1165    // Setters
1166    // =========================================================================
1167
1168    public function setUsername(?string $username): void
1169    {
1170        $this->username = $username;
1171    }
1172
1173    public function setEmail(?string $email): void
1174    {
1175        $this->email = $email;
1176    }
1177
1178    public function setDisplayName(?string $displayName): void
1179    {
1180        $this->displayName = $displayName;
1181    }
1182
1183    public function setFirstName(?string $firstName): void
1184    {
1185        $this->firstName = $firstName;
1186    }
1187
1188    public function setLastName(?string $lastName): void
1189    {
1190        $this->lastName = $lastName;
1191    }
1192
1193    public function setPhone(?string $phone): void
1194    {
1195        $this->phone = $phone;
1196    }
1197
1198    public function setPhotoUrl(?string $photoUrl): void
1199    {
1200        $this->photoUrl = $photoUrl;
1201    }
1202
1203    public function setAvatarOverride(bool $avatarOverride): void
1204    {
1205        $this->avatarOverride = $avatarOverride;
1206    }
1207
1208    public function setPosition(?string $position): void
1209    {
1210        $this->position = $position;
1211    }
1212
1213    public function setHourlyRate(?string $hourlyRate): void
1214    {
1215        $this->hourlyRate = $hourlyRate;
1216    }
1217
1218    public function setHireDate(?string $hireDate): void
1219    {
1220        $this->hireDate = $hireDate;
1221    }
1222
1223    public function setTerminationDate(?string $terminationDate): void
1224    {
1225        $this->terminationDate = $terminationDate;
1226    }
1227
1228    public function setLeaveStartDate(?string $leaveStartDate): void
1229    {
1230        $this->leaveStartDate = $leaveStartDate;
1231    }
1232
1233    public function setLeaveEndDate(?string $leaveEndDate): void
1234    {
1235        $this->leaveEndDate = $leaveEndDate;
1236    }
1237
1238    public function setEmergencyContactName(?string $emergencyContactName): void
1239    {
1240        $this->emergencyContactName = $emergencyContactName;
1241    }
1242
1243    public function setEmergencyContactPhone(?string $emergencyContactPhone): void
1244    {
1245        $this->emergencyContactPhone = $emergencyContactPhone;
1246    }
1247
1248    public function setSource(string $source): void
1249    {
1250        $this->source = $source;
1251    }
1252
1253    public function setExternalId(?string $externalId): void
1254    {
1255        $this->externalId = $externalId;
1256    }
1257
1258    public function setLastSyncedAt(?string $lastSyncedAt): void
1259    {
1260        $this->lastSyncedAt = $lastSyncedAt;
1261    }
1262
1263    public function setCanLogin(bool $canLogin): void
1264    {
1265        $this->canLogin = $canLogin;
1266    }
1267
1268    public function setAccountType(string $accountType): void
1269    {
1270        $this->accountType = $accountType;
1271    }
1272
1273    public function setEnabled(bool $enabled): void
1274    {
1275        $this->enabled = $enabled;
1276    }
1277
1278    public function setActive(bool $active): void
1279    {
1280        $this->active = $active;
1281    }
1282
1283    public function setActivationToken(?string $activationToken): void
1284    {
1285        $this->activationToken = $activationToken;
1286    }
1287
1288    public function setActivationTokenExpiresAt(?string $activationTokenExpiresAt): void
1289    {
1290        $this->activationTokenExpiresAt = $activationTokenExpiresAt;
1291    }
1292
1293    public function setPasswordResetToken(?string $passwordResetToken): void
1294    {
1295        $this->passwordResetToken = $passwordResetToken;
1296    }
1297
1298    public function setPasswordResetExpiresAt(?string $passwordResetExpiresAt): void
1299    {
1300        $this->passwordResetExpiresAt = $passwordResetExpiresAt;
1301    }
1302
1303    public function setMfaEnabled(bool $mfaEnabled): void
1304    {
1305        $this->mfaEnabled = $mfaEnabled;
1306    }
1307
1308    public function setMfaSecret(?string $mfaSecret): void
1309    {
1310        $this->mfaSecret = $mfaSecret;
1311    }
1312
1313    public function setMfaBackupCodes(?array $mfaBackupCodes): void
1314    {
1315        $this->mfaBackupCodes = $mfaBackupCodes;
1316    }
1317
1318    public function setMfaVerifiedAt(?string $mfaVerifiedAt): void
1319    {
1320        $this->mfaVerifiedAt = $mfaVerifiedAt;
1321    }
1322
1323    public function setLocale(string $locale): void
1324    {
1325        $this->locale = $locale;
1326    }
1327
1328    public function setDailyReport(bool $dailyReport): void
1329    {
1330        $this->dailyReport = $dailyReport;
1331    }
1332
1333    public function setTimezone(string $timezone): void
1334    {
1335        $this->timezone = $timezone;
1336    }
1337}