| @@ -126,9 +126,29 @@ | ||
| 126 | 126 | */ |
| 127 | 127 | public function create(array $data): Availability |
| 128 | 128 | { |
| 129 | 129 | $this->validate($data); |
| 130 | - | |
| 130 | + | |
| 131 | + // The table carries a UNIQUE KEY on (trip_id, departure_date, departure_time). | |
| 132 | + // Without this check the insert is rejected by the database, the repository | |
| 133 | + // hands back id 0, findModel(0) returns null and the declared return type | |
| 134 | + // raises a TypeError — which is an Error, not an Exception, so it escaped | |
| 135 | + // the controller's catch blocks and surfaced as a fatal 500. Rejecting it | |
| 136 | + // here gives the operator the same clear message duplicate() already gives. | |
| 137 | + $departureTime = !empty($data['departure_time']) | |
| 138 | + ? $this->normalizeTimeFormat((string) $data['departure_time']) | |
| 139 | + : null; | |
| 140 | + if ($departureTime === false) { | |
| 141 | + throw new \InvalidArgumentException('Invalid departure time format. Use HH:MM'); | |
| 142 | + } | |
| 143 | + if ($this->repository->existsForTripDateTime( | |
| 144 | + (int) $data['trip_id'], | |
| 145 | + (string) $data['departure_date'], | |
| 146 | + $departureTime | |
| 147 | + )) { | |
| 148 | + throw new \InvalidArgumentException('Availability date already exists for the selected departure'); | |
| 149 | + } | |
| 150 | + | |
| 131 | 151 | // Set default seats_available if not provided |
| 132 | 152 | if (!isset($data['seats_available'])) { |
| 133 | 153 | $data['seats_available'] = $data['seats_total'] ?? 0; |
| 134 | 154 | } |
| @@ -163,8 +183,30 @@ | ||
| 163 | 183 | |
| 164 | 184 | // Merge with existing data for validation |
| 165 | 185 | $mergedData = array_merge($existing->toArray(), $data); |
| 166 | 186 | $this->validate($mergedData, $id); |
| 187 | + | |
| 188 | + // Moving a date onto a slot another row already holds is rejected by the | |
| 189 | + // same UNIQUE KEY. wpdb::update() then returns false, which this method | |
| 190 | + // used to ignore — it re-read the untouched row and returned 200, so the | |
| 191 | + // operator was told the change saved when nothing had changed. | |
| 192 | + if (array_key_exists('departure_date', $data) || array_key_exists('departure_time', $data)) { | |
| 193 | + $targetDate = (string) ($data['departure_date'] ?? $existing->departure_date); | |
| 194 | + if (array_key_exists('departure_time', $data)) { | |
| 195 | + $targetTime = !empty($data['departure_time']) | |
| 196 | + ? $this->normalizeTimeFormat((string) $data['departure_time']) | |
| 197 | + : null; | |
| 198 | + if ($targetTime === false) { | |
| 199 | + throw new \InvalidArgumentException('Invalid departure time format. Use HH:MM'); | |
| 200 | + } | |
| 201 | + } else { | |
| 202 | + $targetTime = $existing->departure_time; | |
| 203 | + } | |
| 204 | + | |
| 205 | + if ($this->repository->existsForTripDateTime((int) $existing->trip_id, $targetDate, $targetTime, $id)) { | |
| 206 | + throw new \InvalidArgumentException('Availability date already exists for the selected departure'); | |
| 207 | + } | |
| 208 | + } | |
| 167 | 209 | |
| 168 | 210 | // Auto-update status based on availability |
| 169 | 211 | if (isset($data['seats_available']) || isset($data['seats_total'])) { |
| 170 | 212 | $seatsAvailable = (int) ($data['seats_available'] ?? $existing->seats_available); |