Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 70
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
ShiftAuditRepository
0.00% covered (danger)
0.00%
0 / 70
0.00% covered (danger)
0.00%
0 / 9
600
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 logCreate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 logUpdate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 logDelete
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 log
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 findByShiftId
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 findByDateRange
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 findByUserId
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 getActionSummary
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2
3namespace BuyerKiosk\Scheduling\Repositories;
4
5use DateTime;
6use DateTimeZone;
7use PDO;
8
9/**
10 * ShiftAuditRepository
11 *
12 * Data access layer for shift audit trail.
13 * Tracks all changes made to shifts for compliance and debugging.
14 *
15 * @package BuyerKiosk\Scheduling\Repositories
16 */
17class ShiftAuditRepository
18{
19    private PDO $db;
20
21    public function __construct(PDO $db)
22    {
23        $this->db = $db;
24    }
25
26    /**
27     * Log a shift creation
28     *
29     * @param int $shiftId The created shift ID
30     * @param int $userId User who created the shift
31     * @param array $newValues The shift data
32     * @return int Audit record ID
33     */
34    public function logCreate(int $shiftId, int $userId, array $newValues): int
35    {
36        return $this->log($shiftId, 'create', $userId, null, $newValues);
37    }
38
39    /**
40     * Log a shift update
41     *
42     * @param int $shiftId The updated shift ID
43     * @param int $userId User who made the update
44     * @param array $oldValues Previous values
45     * @param array $newValues New values
46     * @return int Audit record ID
47     */
48    public function logUpdate(int $shiftId, int $userId, array $oldValues, array $newValues): int
49    {
50        return $this->log($shiftId, 'update', $userId, $oldValues, $newValues);
51    }
52
53    /**
54     * Log a shift deletion
55     *
56     * @param int $shiftId The deleted shift ID
57     * @param int $userId User who deleted the shift
58     * @param array $oldValues The shift data at time of deletion
59     * @return int Audit record ID
60     */
61    public function logDelete(int $shiftId, int $userId, array $oldValues): int
62    {
63        return $this->log($shiftId, 'delete', $userId, $oldValues, null);
64    }
65
66    /**
67     * Internal log method
68     *
69     * @param int $shiftId Shift ID
70     * @param string $action Action type (create, update, delete)
71     * @param int $userId User who performed the action
72     * @param array|null $oldValues Previous values
73     * @param array|null $newValues New values
74     * @return int Audit record ID
75     */
76    private function log(
77        int $shiftId,
78        string $action,
79        int $userId,
80        ?array $oldValues,
81        ?array $newValues
82    ): int {
83        $occurredAtUtc = new DateTime('now', new DateTimeZone('UTC'));
84
85        $stmt = $this->db->prepare("
86            INSERT INTO scheduleShiftAudit
87                (shiftId, action, actorUserId, occurredAt, oldValueJson, newValueJson, note)
88            VALUES
89                (:shiftId, :action, :actorUserId, :occurredAt, :oldValueJson, :newValueJson, :note)
90        ");
91
92        $stmt->bindValue(':shiftId', $shiftId, PDO::PARAM_INT);
93        $stmt->bindValue(':action', $action);
94        $stmt->bindValue(':actorUserId', $userId, PDO::PARAM_INT);
95        $stmt->bindValue(':occurredAt', $occurredAtUtc->format('Y-m-d H:i:s'));
96        $stmt->bindValue(':oldValueJson', $oldValues !== null ? json_encode($oldValues) : null);
97        $stmt->bindValue(':newValueJson', $newValues !== null ? json_encode($newValues) : null);
98        $stmt->bindValue(':note', null);
99
100        $stmt->execute();
101
102        return (int)$this->db->lastInsertId();
103    }
104
105    /**
106     * Get audit history for a specific shift
107     *
108     * @param int $shiftId Shift ID
109     * @return array Audit records
110     */
111    public function findByShiftId(int $shiftId): array
112    {
113        $stmt = $this->db->prepare("
114            SELECT
115                sa.*,
116                CONCAT(u.first_name, ' ', u.last_name) as userName
117            FROM scheduleShiftAudit sa
118            LEFT JOIN kiosk_users.users u ON sa.actorUserId = u.id
119            WHERE sa.shiftId = :shiftId
120            ORDER BY sa.occurredAt DESC
121        ");
122        $stmt->bindValue(':shiftId', $shiftId, PDO::PARAM_INT);
123        $stmt->execute();
124
125        $records = [];
126        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
127            $row['oldValueJson'] = $row['oldValueJson'] ? json_decode($row['oldValueJson'], true) : null;
128            $row['newValueJson'] = $row['newValueJson'] ? json_decode($row['newValueJson'], true) : null;
129            $records[] = $row;
130        }
131
132        return $records;
133    }
134
135    /**
136     * Get all audit records within a date range
137     *
138     * @param DateTime $start Range start
139     * @param DateTime $end Range end
140     * @return array Audit records
141     */
142    public function findByDateRange(DateTime $start, DateTime $end): array
143    {
144        $stmt = $this->db->prepare("
145            SELECT
146                sa.*,
147                CONCAT(u.first_name, ' ', u.last_name) as userName
148            FROM scheduleShiftAudit sa
149            LEFT JOIN kiosk_users.users u ON sa.actorUserId = u.id
150            WHERE sa.occurredAt >= :start
151              AND sa.occurredAt < :end
152            ORDER BY sa.occurredAt DESC
153        ");
154        $stmt->bindValue(':start', $start->format('Y-m-d H:i:s'));
155        $stmt->bindValue(':end', $end->format('Y-m-d H:i:s'));
156        $stmt->execute();
157
158        $records = [];
159        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
160            $row['oldValueJson'] = $row['oldValueJson'] ? json_decode($row['oldValueJson'], true) : null;
161            $row['newValueJson'] = $row['newValueJson'] ? json_decode($row['newValueJson'], true) : null;
162            $records[] = $row;
163        }
164
165        return $records;
166    }
167
168    /**
169     * Get audit records by user
170     *
171     * @param int $userId User ID
172     * @param int $limit Maximum records to return
173     * @return array Audit records
174     */
175    public function findByUserId(int $userId, int $limit = 100): array
176    {
177        $stmt = $this->db->prepare("
178            SELECT
179                sa.*,
180                CONCAT(u.first_name, ' ', u.last_name) as userName
181            FROM scheduleShiftAudit sa
182            LEFT JOIN kiosk_users.users u ON sa.actorUserId = u.id
183            WHERE sa.actorUserId = :userId
184            ORDER BY sa.occurredAt DESC
185            LIMIT :limit
186        ");
187        $stmt->bindValue(':userId', $userId, PDO::PARAM_INT);
188        $stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
189        $stmt->execute();
190
191        $records = [];
192        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
193            $row['oldValueJson'] = $row['oldValueJson'] ? json_decode($row['oldValueJson'], true) : null;
194            $row['newValueJson'] = $row['newValueJson'] ? json_decode($row['newValueJson'], true) : null;
195            $records[] = $row;
196        }
197
198        return $records;
199    }
200
201    /**
202     * Get summary of actions in a date range
203     *
204     * @param DateTime $start Range start
205     * @param DateTime $end Range end
206     * @return array{creates: int, updates: int, deletes: int}
207     */
208    public function getActionSummary(DateTime $start, DateTime $end): array
209    {
210        $stmt = $this->db->prepare("
211            SELECT
212                action,
213                COUNT(*) as count
214            FROM scheduleShiftAudit
215            WHERE occurredAt >= :start
216              AND occurredAt < :end
217            GROUP BY action
218        ");
219        $stmt->bindValue(':start', $start->format('Y-m-d H:i:s'));
220        $stmt->bindValue(':end', $end->format('Y-m-d H:i:s'));
221        $stmt->execute();
222
223        $summary = [
224            'creates' => 0,
225            'updates' => 0,
226            'deletes' => 0,
227        ];
228
229        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
230            switch ($row['action']) {
231                case 'create':
232                    $summary['creates'] = (int)$row['count'];
233                    break;
234                case 'update':
235                    $summary['updates'] = (int)$row['count'];
236                    break;
237                case 'delete':
238                    $summary['deletes'] = (int)$row['count'];
239                    break;
240            }
241        }
242
243        return $summary;
244    }
245}