Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 100 |
|
0.00% |
0 / 12 |
CRAP | |
0.00% |
0 / 1 |
| TimePunchAuditRepository | |
0.00% |
0 / 100 |
|
0.00% |
0 / 12 |
930 | |
0.00% |
0 / 1 |
| __construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| logCreate | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| logEdit | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| logDelete | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| logManagerOverride | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
| log | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
12 | |||
| findByPunchId | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 | |||
| findByDateRange | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
| findModifications | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
| findManagerOverrides | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
| getWeeklyModificationsByEmployee | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
| getActionSummary | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
30 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace BuyerKiosk\Scheduling\Repositories; |
| 4 | |
| 5 | use DateTime; |
| 6 | use DateTimeZone; |
| 7 | use PDO; |
| 8 | |
| 9 | /** |
| 10 | * TimePunchAuditRepository |
| 11 | * |
| 12 | * Data access layer for time punch audit trail. |
| 13 | * Tracks all changes made to time punches for compliance and payroll accuracy. |
| 14 | * |
| 15 | * @package BuyerKiosk\Scheduling\Repositories |
| 16 | */ |
| 17 | class TimePunchAuditRepository |
| 18 | { |
| 19 | private PDO $db; |
| 20 | |
| 21 | public function __construct(PDO $db) |
| 22 | { |
| 23 | $this->db = $db; |
| 24 | } |
| 25 | |
| 26 | /** |
| 27 | * Log a punch creation |
| 28 | * |
| 29 | * @param int $punchId The created punch ID |
| 30 | * @param int $userId User who created the punch |
| 31 | * @param array $newValues The punch data |
| 32 | * @param string|null $reason Optional reason for the punch |
| 33 | * @return int Audit record ID |
| 34 | */ |
| 35 | public function logCreate(int $punchId, int $userId, array $newValues, ?string $reason = null): int |
| 36 | { |
| 37 | return $this->log($punchId, 'create', $userId, null, $newValues, $reason); |
| 38 | } |
| 39 | |
| 40 | /** |
| 41 | * Log a punch edit |
| 42 | * |
| 43 | * @param int $punchId The edited punch ID |
| 44 | * @param int $userId User who made the edit |
| 45 | * @param array $oldValues Previous values |
| 46 | * @param array $newValues New values |
| 47 | * @param string $reason Required reason for the edit |
| 48 | * @return int Audit record ID |
| 49 | */ |
| 50 | public function logEdit(int $punchId, int $userId, array $oldValues, array $newValues, string $reason): int |
| 51 | { |
| 52 | // Table schema stores action as enum('create','update','delete') |
| 53 | return $this->log($punchId, 'update', $userId, $oldValues, $newValues, $reason); |
| 54 | } |
| 55 | |
| 56 | /** |
| 57 | * Log a punch deletion |
| 58 | * |
| 59 | * @param int $punchId The deleted punch ID |
| 60 | * @param int $userId User who deleted the punch |
| 61 | * @param array $oldValues The punch data at time of deletion |
| 62 | * @param string $reason Required reason for deletion |
| 63 | * @return int Audit record ID |
| 64 | */ |
| 65 | public function logDelete(int $punchId, int $userId, array $oldValues, string $reason): int |
| 66 | { |
| 67 | return $this->log($punchId, 'delete', $userId, $oldValues, null, $reason); |
| 68 | } |
| 69 | |
| 70 | /** |
| 71 | * Log a manager override |
| 72 | * |
| 73 | * @param int $punchId The punch ID |
| 74 | * @param int $managerId Manager who performed the override |
| 75 | * @param array $punchValues Current punch values |
| 76 | * @param string $overrideType Type of override (e.g., 'clock_window', 'unscheduled') |
| 77 | * @param string $reason Reason for override |
| 78 | * @return int Audit record ID |
| 79 | */ |
| 80 | public function logManagerOverride( |
| 81 | int $punchId, |
| 82 | int $managerId, |
| 83 | array $punchValues, |
| 84 | string $overrideType, |
| 85 | string $reason |
| 86 | ): int { |
| 87 | $newValues = $punchValues; |
| 88 | $newValues['overrideType'] = $overrideType; |
| 89 | |
| 90 | // Stored as an 'update' with a note prefix so it can be filtered. |
| 91 | return $this->log( |
| 92 | $punchId, |
| 93 | 'update', |
| 94 | $managerId, |
| 95 | null, |
| 96 | $newValues, |
| 97 | "MANAGER_OVERRIDE: {$overrideType}: {$reason}" |
| 98 | ); |
| 99 | } |
| 100 | |
| 101 | /** |
| 102 | * Internal log method |
| 103 | * |
| 104 | * @param int $punchId Punch ID |
| 105 | * @param string $action Action type |
| 106 | * @param int $userId User who performed the action |
| 107 | * @param array|null $oldValues Previous values |
| 108 | * @param array|null $newValues New values |
| 109 | * @param string|null $note Note/reason for the action |
| 110 | * @return int Audit record ID |
| 111 | */ |
| 112 | private function log( |
| 113 | int $punchId, |
| 114 | string $action, |
| 115 | int $userId, |
| 116 | ?array $oldValues, |
| 117 | ?array $newValues, |
| 118 | ?string $note |
| 119 | ): int { |
| 120 | $occurredAtUtc = new DateTime('now', new DateTimeZone('UTC')); |
| 121 | |
| 122 | $stmt = $this->db->prepare(" |
| 123 | INSERT INTO scheduleTimePunchAudit |
| 124 | (punchId, action, actorUserId, occurredAt, oldValueJson, newValueJson, note) |
| 125 | VALUES |
| 126 | (:punchId, :action, :actorUserId, :occurredAt, :oldValueJson, :newValueJson, :note) |
| 127 | "); |
| 128 | |
| 129 | $stmt->bindValue(':punchId', $punchId, PDO::PARAM_INT); |
| 130 | $stmt->bindValue(':action', $action); |
| 131 | $stmt->bindValue(':actorUserId', $userId, PDO::PARAM_INT); |
| 132 | $stmt->bindValue(':occurredAt', $occurredAtUtc->format('Y-m-d H:i:s')); |
| 133 | $stmt->bindValue(':oldValueJson', $oldValues !== null ? json_encode($oldValues) : null); |
| 134 | $stmt->bindValue(':newValueJson', $newValues !== null ? json_encode($newValues) : null); |
| 135 | $stmt->bindValue(':note', $note); |
| 136 | |
| 137 | $stmt->execute(); |
| 138 | |
| 139 | return (int)$this->db->lastInsertId(); |
| 140 | } |
| 141 | |
| 142 | /** |
| 143 | * Get audit history for a specific punch |
| 144 | * |
| 145 | * @param int $punchId Punch ID |
| 146 | * @return array Audit records |
| 147 | */ |
| 148 | public function findByPunchId(int $punchId): array |
| 149 | { |
| 150 | $stmt = $this->db->prepare(" |
| 151 | SELECT |
| 152 | tpa.*, |
| 153 | CONCAT(u.first_name, ' ', u.last_name) as userName |
| 154 | FROM scheduleTimePunchAudit tpa |
| 155 | LEFT JOIN kiosk_users.users u ON tpa.actorUserId = u.id |
| 156 | WHERE tpa.punchId = :punchId |
| 157 | ORDER BY tpa.occurredAt DESC |
| 158 | "); |
| 159 | $stmt->bindValue(':punchId', $punchId, PDO::PARAM_INT); |
| 160 | $stmt->execute(); |
| 161 | |
| 162 | $records = []; |
| 163 | while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { |
| 164 | $row['oldValueJson'] = $row['oldValueJson'] ? json_decode($row['oldValueJson'], true) : null; |
| 165 | $row['newValueJson'] = $row['newValueJson'] ? json_decode($row['newValueJson'], true) : null; |
| 166 | $records[] = $row; |
| 167 | } |
| 168 | |
| 169 | return $records; |
| 170 | } |
| 171 | |
| 172 | /** |
| 173 | * Get all audit records within a date range |
| 174 | * |
| 175 | * @param DateTime $start Range start |
| 176 | * @param DateTime $end Range end |
| 177 | * @return array Audit records |
| 178 | */ |
| 179 | public function findByDateRange(DateTime $start, DateTime $end): array |
| 180 | { |
| 181 | $stmt = $this->db->prepare(" |
| 182 | SELECT |
| 183 | tpa.*, |
| 184 | CONCAT(u.first_name, ' ', u.last_name) as userName |
| 185 | FROM scheduleTimePunchAudit tpa |
| 186 | LEFT JOIN kiosk_users.users u ON tpa.actorUserId = u.id |
| 187 | WHERE tpa.occurredAt >= :start |
| 188 | AND tpa.occurredAt < :end |
| 189 | ORDER BY tpa.occurredAt DESC |
| 190 | "); |
| 191 | $stmt->bindValue(':start', $start->format('Y-m-d H:i:s')); |
| 192 | $stmt->bindValue(':end', $end->format('Y-m-d H:i:s')); |
| 193 | $stmt->execute(); |
| 194 | |
| 195 | $records = []; |
| 196 | while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { |
| 197 | $row['oldValueJson'] = $row['oldValueJson'] ? json_decode($row['oldValueJson'], true) : null; |
| 198 | $row['newValueJson'] = $row['newValueJson'] ? json_decode($row['newValueJson'], true) : null; |
| 199 | $records[] = $row; |
| 200 | } |
| 201 | |
| 202 | return $records; |
| 203 | } |
| 204 | |
| 205 | /** |
| 206 | * Get only edit/delete actions (for payroll review) |
| 207 | * |
| 208 | * @param DateTime $start Range start |
| 209 | * @param DateTime $end Range end |
| 210 | * @return array Audit records with modifications only |
| 211 | */ |
| 212 | public function findModifications(DateTime $start, DateTime $end): array |
| 213 | { |
| 214 | $stmt = $this->db->prepare(" |
| 215 | SELECT |
| 216 | tpa.*, |
| 217 | CONCAT(u.first_name, ' ', u.last_name) as userName |
| 218 | FROM scheduleTimePunchAudit tpa |
| 219 | LEFT JOIN kiosk_users.users u ON tpa.actorUserId = u.id |
| 220 | WHERE tpa.occurredAt >= :start |
| 221 | AND tpa.occurredAt < :end |
| 222 | AND tpa.action IN ('update', 'delete') |
| 223 | ORDER BY tpa.occurredAt DESC |
| 224 | "); |
| 225 | $stmt->bindValue(':start', $start->format('Y-m-d H:i:s')); |
| 226 | $stmt->bindValue(':end', $end->format('Y-m-d H:i:s')); |
| 227 | $stmt->execute(); |
| 228 | |
| 229 | $records = []; |
| 230 | while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { |
| 231 | $row['oldValueJson'] = $row['oldValueJson'] ? json_decode($row['oldValueJson'], true) : null; |
| 232 | $row['newValueJson'] = $row['newValueJson'] ? json_decode($row['newValueJson'], true) : null; |
| 233 | $records[] = $row; |
| 234 | } |
| 235 | |
| 236 | return $records; |
| 237 | } |
| 238 | |
| 239 | /** |
| 240 | * Get manager override records |
| 241 | * |
| 242 | * @param DateTime $start Range start |
| 243 | * @param DateTime $end Range end |
| 244 | * @return array Override records |
| 245 | */ |
| 246 | public function findManagerOverrides(DateTime $start, DateTime $end): array |
| 247 | { |
| 248 | $stmt = $this->db->prepare(" |
| 249 | SELECT |
| 250 | tpa.*, |
| 251 | CONCAT(u.first_name, ' ', u.last_name) as userName |
| 252 | FROM scheduleTimePunchAudit tpa |
| 253 | LEFT JOIN kiosk_users.users u ON tpa.actorUserId = u.id |
| 254 | WHERE tpa.occurredAt >= :start |
| 255 | AND tpa.occurredAt < :end |
| 256 | AND tpa.action = 'update' |
| 257 | AND tpa.note LIKE 'MANAGER_OVERRIDE:%' |
| 258 | ORDER BY tpa.occurredAt DESC |
| 259 | "); |
| 260 | $stmt->bindValue(':start', $start->format('Y-m-d H:i:s')); |
| 261 | $stmt->bindValue(':end', $end->format('Y-m-d H:i:s')); |
| 262 | $stmt->execute(); |
| 263 | |
| 264 | $records = []; |
| 265 | while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { |
| 266 | $row['oldValueJson'] = $row['oldValueJson'] ? json_decode($row['oldValueJson'], true) : null; |
| 267 | $row['newValueJson'] = $row['newValueJson'] ? json_decode($row['newValueJson'], true) : null; |
| 268 | $records[] = $row; |
| 269 | } |
| 270 | |
| 271 | return $records; |
| 272 | } |
| 273 | |
| 274 | /** |
| 275 | * Get summary of punch modifications by employee |
| 276 | * |
| 277 | * @param DateTime $weekStart Start of week |
| 278 | * @return array Employee modification counts |
| 279 | */ |
| 280 | public function getWeeklyModificationsByEmployee(DateTime $weekStart): array |
| 281 | { |
| 282 | $weekEnd = (clone $weekStart)->modify('+7 days'); |
| 283 | |
| 284 | $stmt = $this->db->prepare(" |
| 285 | SELECT |
| 286 | tp.employeeId, |
| 287 | u.firstName AS employeeFirstName, |
| 288 | u.lastName AS employeeLastName, |
| 289 | COUNT(*) as modificationCount |
| 290 | FROM scheduleTimePunchAudit tpa |
| 291 | INNER JOIN scheduleTimePunches tp ON tpa.punchId = tp.punchId |
| 292 | INNER JOIN kiosk_users.users u ON tp.employeeId = u.id |
| 293 | WHERE tpa.occurredAt >= :start |
| 294 | AND tpa.occurredAt < :end |
| 295 | AND tpa.action IN ('update', 'delete') |
| 296 | GROUP BY tp.employeeId, u.firstName, u.lastName |
| 297 | ORDER BY modificationCount DESC |
| 298 | "); |
| 299 | $stmt->bindValue(':start', $weekStart->format('Y-m-d H:i:s')); |
| 300 | $stmt->bindValue(':end', $weekEnd->format('Y-m-d H:i:s')); |
| 301 | $stmt->execute(); |
| 302 | |
| 303 | return $stmt->fetchAll(PDO::FETCH_ASSOC); |
| 304 | } |
| 305 | |
| 306 | /** |
| 307 | * Get audit summary for compliance reporting |
| 308 | * |
| 309 | * @param DateTime $start Range start |
| 310 | * @param DateTime $end Range end |
| 311 | * @return array{creates: int, edits: int, deletes: int, managerOverrides: int} |
| 312 | */ |
| 313 | public function getActionSummary(DateTime $start, DateTime $end): array |
| 314 | { |
| 315 | $stmt = $this->db->prepare(" |
| 316 | SELECT |
| 317 | action, |
| 318 | COUNT(*) as count, |
| 319 | SUM(CASE WHEN action = 'update' AND note LIKE 'MANAGER_OVERRIDE:%' THEN 1 ELSE 0 END) as managerOverrideCount |
| 320 | FROM scheduleTimePunchAudit |
| 321 | WHERE occurredAt >= :start |
| 322 | AND occurredAt < :end |
| 323 | GROUP BY action |
| 324 | "); |
| 325 | $stmt->bindValue(':start', $start->format('Y-m-d H:i:s')); |
| 326 | $stmt->bindValue(':end', $end->format('Y-m-d H:i:s')); |
| 327 | $stmt->execute(); |
| 328 | |
| 329 | $summary = [ |
| 330 | 'creates' => 0, |
| 331 | 'edits' => 0, |
| 332 | 'deletes' => 0, |
| 333 | 'managerOverrides' => 0, |
| 334 | ]; |
| 335 | |
| 336 | while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { |
| 337 | switch ($row['action']) { |
| 338 | case 'create': |
| 339 | $summary['creates'] = (int)$row['count']; |
| 340 | break; |
| 341 | case 'update': |
| 342 | $summary['edits'] = (int)$row['count']; |
| 343 | $summary['managerOverrides'] = (int)($row['managerOverrideCount'] ?? 0); |
| 344 | break; |
| 345 | case 'delete': |
| 346 | $summary['deletes'] = (int)$row['count']; |
| 347 | break; |
| 348 | } |
| 349 | } |
| 350 | |
| 351 | return $summary; |
| 352 | } |
| 353 | } |