Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 64
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
StoreHoursRepository
0.00% covered (danger)
0.00%
0 / 64
0.00% covered (danger)
0.00%
0 / 7
342
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
 findByTypeNum
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 findByDay
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 upsertByTypeNum
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
42
 deleteByTypeNum
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 formatRow
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
 getDayName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace BuyerKiosk\StoreConfig\Repositories;
4
5use PDO;
6
7/**
8 * StoreHoursRepository
9 *
10 * Data access layer for per-day store operating hours (storeOperatingHours table).
11 * These are day-of-week overrides that take precedence over default store hours.
12 *
13 * @package BuyerKiosk\StoreConfig\Repositories
14 */
15class StoreHoursRepository
16{
17    private PDO $db;
18
19    /**
20     * Day name mapping for convenience methods
21     */
22    private const DAY_NAMES = [
23        0 => 'Sunday',
24        1 => 'Monday',
25        2 => 'Tuesday',
26        3 => 'Wednesday',
27        4 => 'Thursday',
28        5 => 'Friday',
29        6 => 'Saturday',
30    ];
31
32    public function __construct(PDO $db)
33    {
34        $this->db = $db;
35    }
36
37    /**
38     * Find all operating hours for a store
39     *
40     * Returns an array indexed by dayOfWeek (0-6) with the hours configuration.
41     * Days without overrides will not be included in the result.
42     *
43     * @param string $typeNum Store identifier
44     * @return array<int, array{id: int, typeNum: string, dayOfWeek: int, dayName: string, openTime: string|null, closeTime: string|null, isClosed: bool, usesDefault: bool}>
45     */
46    public function findByTypeNum(string $typeNum): array
47    {
48        $stmt = $this->db->prepare("
49            SELECT id, typeNum, dayOfWeek, openTime, closeTime, isClosed, created_at, updated_at
50            FROM storeOperatingHours
51            WHERE typeNum = :typeNum
52            ORDER BY dayOfWeek ASC
53        ");
54        $stmt->bindValue(':typeNum', $typeNum);
55        $stmt->execute();
56
57        $hours = [];
58        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
59            $dayOfWeek = (int) $row['dayOfWeek'];
60            $hours[$dayOfWeek] = $this->formatRow($row);
61        }
62
63        return $hours;
64    }
65
66    /**
67     * Find operating hours for a specific day of week
68     *
69     * @param string $typeNum Store identifier
70     * @param int $dayOfWeek Day of week (0=Sunday, 6=Saturday)
71     * @return array{id: int, typeNum: string, dayOfWeek: int, dayName: string, openTime: string|null, closeTime: string|null, isClosed: bool, usesDefault: bool}|null
72     */
73    public function findByDay(string $typeNum, int $dayOfWeek): ?array
74    {
75        if ($dayOfWeek < 0 || $dayOfWeek > 6) {
76            throw new \InvalidArgumentException('dayOfWeek must be between 0 and 6');
77        }
78
79        $stmt = $this->db->prepare("
80            SELECT id, typeNum, dayOfWeek, openTime, closeTime, isClosed, created_at, updated_at
81            FROM storeOperatingHours
82            WHERE typeNum = :typeNum AND dayOfWeek = :dayOfWeek
83            LIMIT 1
84        ");
85        $stmt->bindValue(':typeNum', $typeNum);
86        $stmt->bindValue(':dayOfWeek', $dayOfWeek, PDO::PARAM_INT);
87        $stmt->execute();
88
89        $row = $stmt->fetch(PDO::FETCH_ASSOC);
90        if (!$row) {
91            return null;
92        }
93
94        return $this->formatRow($row);
95    }
96
97    /**
98     * Upsert operating hours for a store (full replacement)
99     *
100     * This method accepts an array of day configurations and performs a full
101     * replacement of all operating hours for the store. Days not included
102     * will be deleted.
103     *
104     * @param string $typeNum Store identifier
105     * @param array<int, array{openTime: string|null, closeTime: string|null, isClosed: bool}> $dayHours Indexed by dayOfWeek (0-6)
106     * @return void
107     */
108    public function upsertByTypeNum(string $typeNum, array $dayHours): void
109    {
110        // Validate input
111        foreach ($dayHours as $dayOfWeek => $config) {
112            if ($dayOfWeek < 0 || $dayOfWeek > 6) {
113                throw new \InvalidArgumentException("Invalid dayOfWeek: {$dayOfWeek}");
114            }
115        }
116
117        // Delete existing hours for days not in the new configuration
118        $providedDays = array_keys($dayHours);
119        if (empty($providedDays)) {
120            // Delete all hours for this store
121            $deleteStmt = $this->db->prepare("DELETE FROM storeOperatingHours WHERE typeNum = :typeNum");
122            $deleteStmt->bindValue(':typeNum', $typeNum);
123            $deleteStmt->execute();
124            return;
125        }
126
127        // Delete days not in the new configuration
128        $placeholders = implode(',', array_fill(0, count($providedDays), '?'));
129        $deleteStmt = $this->db->prepare("
130            DELETE FROM storeOperatingHours
131            WHERE typeNum = ? AND dayOfWeek NOT IN ({$placeholders})
132        ");
133        $deleteStmt->execute(array_merge([$typeNum], $providedDays));
134
135        // Upsert each day
136        $upsertStmt = $this->db->prepare("
137            INSERT INTO storeOperatingHours (typeNum, dayOfWeek, openTime, closeTime, isClosed)
138            VALUES (:typeNum, :dayOfWeek, :openTime, :closeTime, :isClosed)
139            ON DUPLICATE KEY UPDATE
140                openTime = VALUES(openTime),
141                closeTime = VALUES(closeTime),
142                isClosed = VALUES(isClosed)
143        ");
144
145        foreach ($dayHours as $dayOfWeek => $config) {
146            $upsertStmt->bindValue(':typeNum', $typeNum);
147            $upsertStmt->bindValue(':dayOfWeek', $dayOfWeek, PDO::PARAM_INT);
148            $upsertStmt->bindValue(':openTime', $config['openTime'] ?? null);
149            $upsertStmt->bindValue(':closeTime', $config['closeTime'] ?? null);
150            $upsertStmt->bindValue(':isClosed', (int) ($config['isClosed'] ?? false), PDO::PARAM_INT);
151            $upsertStmt->execute();
152        }
153    }
154
155    /**
156     * Delete all operating hours for a store
157     *
158     * @param string $typeNum Store identifier
159     * @return int Number of rows deleted
160     */
161    public function deleteByTypeNum(string $typeNum): int
162    {
163        $stmt = $this->db->prepare("DELETE FROM storeOperatingHours WHERE typeNum = :typeNum");
164        $stmt->bindValue(':typeNum', $typeNum);
165        $stmt->execute();
166        return $stmt->rowCount();
167    }
168
169    /**
170     * Format a database row into the standard return format
171     *
172     * @param array $row Database row
173     * @return array{id: int, typeNum: string, dayOfWeek: int, dayName: string, openTime: string|null, closeTime: string|null, isClosed: bool, usesDefault: bool}
174     */
175    private function formatRow(array $row): array
176    {
177        $dayOfWeek = (int) $row['dayOfWeek'];
178        $openTime = $row['openTime'];
179        $closeTime = $row['closeTime'];
180        $isClosed = (bool) $row['isClosed'];
181
182        // usesDefault is true if:
183        // - Not closed AND
184        // - Both openTime and closeTime are null
185        $usesDefault = !$isClosed && $openTime === null && $closeTime === null;
186
187        return [
188            'id' => (int) $row['id'],
189            'typeNum' => $row['typeNum'],
190            'dayOfWeek' => $dayOfWeek,
191            'dayName' => self::DAY_NAMES[$dayOfWeek],
192            'openTime' => $openTime,
193            'closeTime' => $closeTime,
194            'isClosed' => $isClosed,
195            'usesDefault' => $usesDefault,
196        ];
197    }
198
199    /**
200     * Get day name from day of week number
201     *
202     * @param int $dayOfWeek Day of week (0-6)
203     * @return string Day name
204     */
205    public static function getDayName(int $dayOfWeek): string
206    {
207        return self::DAY_NAMES[$dayOfWeek] ?? 'Unknown';
208    }
209}