Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
13.49% covered (danger)
13.49%
17 / 126
30.77% covered (danger)
30.77%
8 / 26
CRAP
0.00% covered (danger)
0.00%
0 / 1
StoreAssignment
13.49% covered (danger)
13.49%
17 / 126
30.77% covered (danger)
30.77%
8 / 26
1668.48
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 hydrate
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
10
 find
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 findByUserAndStore
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 findByUser
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 findByStore
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 findActiveUserIdsByStore
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 create
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
56
 upsert
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 save
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
30
 deactivate
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 reactivate
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 delete
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 toArray
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 getId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUserId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTypeNum
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getClockPin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDrsEmployeeId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRole
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isActive
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAssignedAt
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDeactivatedAt
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setClockPin
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setDrsEmployeeId
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setRole
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Store Assignment Model
4 *
5 * Represents a user's assignment to a specific store. Users can be assigned
6 * to multiple stores, and each assignment can have store-specific data like
7 * clock PIN and DRS employee ID.
8 *
9 * @package BuyerKiosk\Auth\Models
10 */
11
12namespace BuyerKiosk\Auth\Models;
13
14class StoreAssignment
15{
16    /**
17     * @var \PDO Database connection
18     */
19    private $db;
20
21    /**
22     * @var int|null Assignment ID
23     */
24    private $id;
25
26    /**
27     * @var int User ID (foreign key to users.id)
28     */
29    private $userId;
30
31    /**
32     * @var string Store identifier (e.g., 'ou00', 'pa00')
33     */
34    private $typeNum;
35
36    /**
37     * @var string|null Clock PIN for time punch
38     */
39    private $clockPin;
40
41    /**
42     * @var string|null External DRS employee ID
43     */
44    private $drsEmployeeId;
45
46    /**
47     * @var int Role level (0 = standard employee)
48     */
49    private $role = 0;
50
51    /**
52     * @var bool Whether assignment is active
53     */
54    private $isActive = true;
55
56    /**
57     * @var string|null Assignment timestamp
58     */
59    private $assignedAt;
60
61    /**
62     * @var string|null Deactivation timestamp
63     */
64    private $deactivatedAt;
65
66    /**
67     * Constructor
68     *
69     * @param \PDO $db Database connection
70     * @param array $properties Optional properties to hydrate
71     */
72    public function __construct(\PDO $db, array $properties = [])
73    {
74        $this->db = $db;
75        $this->hydrate($properties);
76    }
77
78    /**
79     * Hydrate object from array
80     *
81     * @param array $data Property values
82     */
83    private function hydrate(array $data): void
84    {
85        if (isset($data['id'])) $this->id = (int)$data['id'];
86        if (isset($data['userId'])) $this->userId = (int)$data['userId'];
87        if (isset($data['typeNum'])) $this->typeNum = $data['typeNum'];
88        if (isset($data['clockPin'])) $this->clockPin = $data['clockPin'];
89        if (isset($data['drsEmployeeId'])) $this->drsEmployeeId = $data['drsEmployeeId'];
90        if (isset($data['role'])) $this->role = (int)$data['role'];
91        if (isset($data['isActive'])) $this->isActive = (bool)$data['isActive'];
92        if (isset($data['assignedAt'])) $this->assignedAt = $data['assignedAt'];
93        if (isset($data['deactivatedAt'])) $this->deactivatedAt = $data['deactivatedAt'];
94    }
95
96    // =========================================================================
97    // Static Factory Methods
98    // =========================================================================
99
100    /**
101     * Find assignment by ID
102     *
103     * @param \PDO $db Database connection
104     * @param int $id Assignment ID
105     * @return self|null
106     */
107    public static function find(\PDO $db, int $id): ?self
108    {
109        $stmt = $db->prepare("SELECT * FROM userStoreAssignments WHERE id = :id");
110        $stmt->execute(['id' => $id]);
111        $row = $stmt->fetch(\PDO::FETCH_ASSOC);
112
113        return $row ? new self($db, $row) : null;
114    }
115
116    /**
117     * Find assignment by user ID and store
118     *
119     * @param \PDO $db Database connection
120     * @param int $userId User ID
121     * @param string $typeNum Store identifier
122     * @return self|null
123     */
124    public static function findByUserAndStore(\PDO $db, int $userId, string $typeNum): ?self
125    {
126        $stmt = $db->prepare("
127            SELECT * FROM userStoreAssignments
128            WHERE userId = :userId AND typeNum = :typeNum
129        ");
130        $stmt->execute(['userId' => $userId, 'typeNum' => $typeNum]);
131        $row = $stmt->fetch(\PDO::FETCH_ASSOC);
132
133        return $row ? new self($db, $row) : null;
134    }
135
136    /**
137     * Find all assignments for a user
138     *
139     * @param \PDO $db Database connection
140     * @param int $userId User ID
141     * @param bool $activeOnly Only return active assignments
142     * @return self[]
143     */
144    public static function findByUser(\PDO $db, int $userId, bool $activeOnly = false): array
145    {
146        $sql = "SELECT * FROM userStoreAssignments WHERE userId = :userId";
147        if ($activeOnly) {
148            $sql .= " AND isActive = 1";
149        }
150        $sql .= " ORDER BY typeNum";
151
152        $stmt = $db->prepare($sql);
153        $stmt->execute(['userId' => $userId]);
154
155        return array_map(
156            fn($row) => new self($db, $row),
157            $stmt->fetchAll(\PDO::FETCH_ASSOC)
158        );
159    }
160
161    /**
162     * Find all assignments for a store
163     *
164     * @param \PDO $db Database connection
165     * @param string $typeNum Store identifier
166     * @param bool $activeOnly Only return active assignments
167     * @return self[]
168     */
169    public static function findByStore(\PDO $db, string $typeNum, bool $activeOnly = true): array
170    {
171        $sql = "SELECT * FROM userStoreAssignments WHERE typeNum = :typeNum";
172        if ($activeOnly) {
173            $sql .= " AND isActive = 1";
174        }
175        $sql .= " ORDER BY userId";
176
177        $stmt = $db->prepare($sql);
178        $stmt->execute(['typeNum' => $typeNum]);
179
180        return array_map(
181            fn($row) => new self($db, $row),
182            $stmt->fetchAll(\PDO::FETCH_ASSOC)
183        );
184    }
185
186    /**
187     * Find all active user IDs for a store
188     *
189     * @param \PDO $db Database connection
190     * @param string $typeNum Store identifier
191     * @return int[]
192     */
193    public static function findActiveUserIdsByStore(\PDO $db, string $typeNum): array
194    {
195        $stmt = $db->prepare("
196            SELECT userId FROM userStoreAssignments
197            WHERE typeNum = :typeNum AND isActive = 1
198        ");
199        $stmt->execute(['typeNum' => $typeNum]);
200
201        return $stmt->fetchAll(\PDO::FETCH_COLUMN);
202    }
203
204    /**
205     * Create a new store assignment
206     *
207     * If assignment already exists for user+store, returns existing (optionally updates it)
208     *
209     * @param \PDO $db Database connection
210     * @param int $userId User ID
211     * @param string $typeNum Store identifier
212     * @param array $data Additional fields (clockPin, drsEmployeeId, role)
213     * @param bool $updateIfExists Update existing assignment with provided data
214     * @return self
215     */
216    public static function create(\PDO $db, int $userId, string $typeNum, array $data = [], bool $updateIfExists = true): self
217    {
218        // Check for existing assignment
219        $existing = self::findByUserAndStore($db, $userId, $typeNum);
220
221        if ($existing) {
222            if ($updateIfExists) {
223                // Update existing assignment with new data
224                if (isset($data['clockPin'])) $existing->clockPin = $data['clockPin'];
225                if (isset($data['drsEmployeeId'])) $existing->drsEmployeeId = $data['drsEmployeeId'];
226                if (isset($data['role'])) $existing->role = (int)$data['role'];
227
228                // Reactivate if it was deactivated
229                if (!$existing->isActive) {
230                    $existing->reactivate();
231                } else {
232                    $existing->save();
233                }
234            }
235            return $existing;
236        }
237
238        // Create new assignment
239        $assignment = new self($db, [
240            'userId' => $userId,
241            'typeNum' => $typeNum,
242            'clockPin' => $data['clockPin'] ?? null,
243            'drsEmployeeId' => $data['drsEmployeeId'] ?? null,
244            'role' => $data['role'] ?? 0,
245            'isActive' => true,
246        ]);
247
248        $assignment->save();
249
250        return $assignment;
251    }
252
253    /**
254     * Create or update assignment (upsert)
255     *
256     * @param \PDO $db Database connection
257     * @param int $userId User ID
258     * @param string $typeNum Store identifier
259     * @param array $data Fields to set/update
260     * @return self
261     */
262    public static function upsert(\PDO $db, int $userId, string $typeNum, array $data = []): self
263    {
264        return self::create($db, $userId, $typeNum, $data, true);
265    }
266
267    // =========================================================================
268    // Instance Methods
269    // =========================================================================
270
271    /**
272     * Save the assignment to database
273     *
274     * @return bool True if successful
275     */
276    public function save(): bool
277    {
278        if ($this->id) {
279            // Update existing
280            $stmt = $this->db->prepare("
281                UPDATE userStoreAssignments SET
282                    clockPin = :clockPin,
283                    drsEmployeeId = :drsEmployeeId,
284                    role = :role,
285                    isActive = :isActive,
286                    deactivatedAt = :deactivatedAt
287                WHERE id = :id
288            ");
289
290            return $stmt->execute([
291                'clockPin' => $this->clockPin,
292                'drsEmployeeId' => $this->drsEmployeeId,
293                'role' => $this->role,
294                'isActive' => $this->isActive ? 1 : 0,
295                'deactivatedAt' => $this->deactivatedAt,
296                'id' => $this->id,
297            ]);
298        } else {
299            // Insert new
300            $stmt = $this->db->prepare("
301                INSERT INTO userStoreAssignments
302                    (userId, typeNum, clockPin, drsEmployeeId, role, isActive, assignedAt)
303                VALUES
304                    (:userId, :typeNum, :clockPin, :drsEmployeeId, :role, :isActive, NOW())
305            ");
306
307            $result = $stmt->execute([
308                'userId' => $this->userId,
309                'typeNum' => $this->typeNum,
310                'clockPin' => $this->clockPin,
311                'drsEmployeeId' => $this->drsEmployeeId,
312                'role' => $this->role,
313                'isActive' => $this->isActive ? 1 : 0,
314            ]);
315
316            if ($result) {
317                $this->id = (int)$this->db->lastInsertId();
318                $this->assignedAt = date('Y-m-d H:i:s');
319            }
320
321            return $result;
322        }
323    }
324
325    /**
326     * Deactivate this assignment
327     *
328     * @return bool True if successful
329     */
330    public function deactivate(): bool
331    {
332        $this->isActive = false;
333        $this->deactivatedAt = date('Y-m-d H:i:s');
334        return $this->save();
335    }
336
337    /**
338     * Reactivate this assignment
339     *
340     * @return bool True if successful
341     */
342    public function reactivate(): bool
343    {
344        $this->isActive = true;
345        $this->deactivatedAt = null;
346        return $this->save();
347    }
348
349    /**
350     * Delete this assignment permanently
351     *
352     * @return bool True if successful
353     */
354    public function delete(): bool
355    {
356        if (!$this->id) {
357            return false;
358        }
359
360        $stmt = $this->db->prepare("DELETE FROM userStoreAssignments WHERE id = :id");
361        return $stmt->execute(['id' => $this->id]);
362    }
363
364    /**
365     * Convert to array
366     *
367     * @return array
368     */
369    public function toArray(): array
370    {
371        return [
372            'id' => $this->id,
373            'userId' => $this->userId,
374            'typeNum' => $this->typeNum,
375            'clockPin' => $this->clockPin,
376            'drsEmployeeId' => $this->drsEmployeeId,
377            'role' => $this->role,
378            'isActive' => $this->isActive,
379            'assignedAt' => $this->assignedAt,
380            'deactivatedAt' => $this->deactivatedAt,
381        ];
382    }
383
384    // =========================================================================
385    // Getters
386    // =========================================================================
387
388    public function getId(): ?int { return $this->id; }
389    public function getUserId(): int { return $this->userId; }
390    public function getTypeNum(): string { return $this->typeNum; }
391    public function getClockPin(): ?string { return $this->clockPin; }
392    public function getDrsEmployeeId(): ?string { return $this->drsEmployeeId; }
393    public function getRole(): int { return $this->role; }
394    public function isActive(): bool { return $this->isActive; }
395    public function getAssignedAt(): ?string { return $this->assignedAt; }
396    public function getDeactivatedAt(): ?string { return $this->deactivatedAt; }
397
398    // =========================================================================
399    // Setters
400    // =========================================================================
401
402    public function setClockPin(?string $clockPin): self
403    {
404        $this->clockPin = $clockPin;
405        return $this;
406    }
407
408    public function setDrsEmployeeId(?string $drsEmployeeId): self
409    {
410        $this->drsEmployeeId = $drsEmployeeId;
411        return $this;
412    }
413
414    public function setRole(int $role): self
415    {
416        $this->role = $role;
417        return $this;
418    }
419}