| @@ -28,8 +28,17 @@ | ||
| 28 | 28 | return null; |
| 29 | 29 | } |
| 30 | 30 | |
| 31 | 31 | /** |
| 32 | + * Clamp an alert threshold to the column's range (smallint unsigned: 0–65535) | |
| 33 | + * so out-of-range input can't abort the write under MySQL strict mode. | |
| 34 | + */ | |
| 35 | + private function clampAlertThreshold($value): int | |
| 36 | + { | |
| 37 | + return max(0, min(65535, (int) $value)); | |
| 38 | + } | |
| 39 | + | |
| 40 | + /** | |
| 32 | 41 | * Get table name |
| 33 | 42 | */ |
| 34 | 43 | protected function getTableName(): string |
| 35 | 44 | { |
| @@ -63,9 +72,15 @@ | ||
| 63 | 72 | */ |
| 64 | 73 | public function findByTripIdAndDate(int $tripId, string $departureDate): ?object |
| 65 | 74 | { |
| 66 | 75 | $table = esc_sql($this->table); |
| 67 | - | |
| 76 | + | |
| 77 | + // departure_date is a DATE column — strip any time component so a datetime | |
| 78 | + // input still matches (avoids date-vs-datetime string-compare misses). | |
| 79 | + if (preg_match('/^(\d{4}-\d{2}-\d{2})/', $departureDate, $m)) { | |
| 80 | + $departureDate = $m[1]; | |
| 81 | + } | |
| 82 | + | |
| 68 | 83 | $result = $this->wpdb->get_row($this->wpdb->prepare( |
| 69 | 84 | "SELECT * FROM `{$table}` |
| 70 | 85 | WHERE trip_id = %d |
| 71 | 86 | AND departure_date = %s |
| @@ -169,21 +184,25 @@ | ||
| 169 | 184 | |
| 170 | 185 | return $results ?: []; |
| 171 | 186 | } |
| 172 | 187 | |
| 173 | - public function existsForTripDateTime(int $tripId, string $departureDate, ?string $departureTime): bool | |
| 188 | + public function existsForTripDateTime(int $tripId, string $departureDate, ?string $departureTime, ?int $excludeId = null): bool | |
| 174 | 189 | { |
| 175 | 190 | $table = esc_sql($this->table); |
| 176 | 191 | |
| 192 | + // Optional so update() can ignore the row it is editing; omitted, behaviour | |
| 193 | + // is exactly as before for existing callers. | |
| 194 | + $exclude = $excludeId !== null ? $this->wpdb->prepare(' AND id <> %d', $excludeId) : ''; | |
| 195 | + | |
| 177 | 196 | if ($departureTime === null || $departureTime === '') { |
| 178 | 197 | $count = (int) $this->wpdb->get_var($this->wpdb->prepare( |
| 179 | - "SELECT COUNT(*) FROM `{$table}` WHERE trip_id = %d AND departure_date = %s AND departure_time IS NULL", | |
| 198 | + "SELECT COUNT(*) FROM `{$table}` WHERE trip_id = %d AND departure_date = %s AND departure_time IS NULL{$exclude}", | |
| 180 | 199 | $tripId, |
| 181 | 200 | $departureDate |
| 182 | 201 | )); |
| 183 | 202 | } else { |
| 184 | 203 | $count = (int) $this->wpdb->get_var($this->wpdb->prepare( |
| 185 | - "SELECT COUNT(*) FROM `{$table}` WHERE trip_id = %d AND departure_date = %s AND departure_time = %s", | |
| 204 | + "SELECT COUNT(*) FROM `{$table}` WHERE trip_id = %d AND departure_date = %s AND departure_time = %s{$exclude}", | |
| 186 | 205 | $tripId, |
| 187 | 206 | $departureDate, |
| 188 | 207 | $departureTime |
| 189 | 208 | )); |
| @@ -317,21 +336,36 @@ | ||
| 317 | 336 | 'to_longitude' => $this->sanitizeCoordinate($data['to_longitude'] ?? null), |
| 318 | 337 | 'special_notes' => !empty($data['special_notes']) ? sanitize_textarea_field($data['special_notes']) : null, |
| 319 | 338 | 'cutoff_date' => !empty($data['cutoff_date']) ? sanitize_text_field($data['cutoff_date']) : null, |
| 320 | 339 | 'cutoff_hours' => (int) ($data['cutoff_hours'] ?? 24), |
| 340 | + 'is_blocked' => !empty($data['is_blocked']) ? 1 : 0, | |
| 341 | + 'block_reason' => !empty($data['block_reason']) ? mb_substr(sanitize_textarea_field($data['block_reason']), 0, 255) : null, | |
| 342 | + 'alert_threshold' => (isset($data['alert_threshold']) && $data['alert_threshold'] !== '' && $data['alert_threshold'] !== null) ? $this->clampAlertThreshold($data['alert_threshold']) : null, | |
| 321 | 343 | ]; |
| 322 | - | |
| 344 | + | |
| 323 | 345 | // Calculate discount percentage if not provided |
| 324 | 346 | if (!empty($insertData['original_price']) && !empty($insertData['discounted_price']) && empty($data['discount_percentage'])) { |
| 325 | 347 | $insertData['discount_percentage'] = round((($insertData['original_price'] - $insertData['discounted_price']) / $insertData['original_price']) * 100, 2); |
| 326 | 348 | } |
| 327 | - | |
| 328 | - $this->wpdb->insert($table, $insertData, [ | |
| 349 | + | |
| 350 | + $inserted = $this->wpdb->insert($table, $insertData, [ | |
| 329 | 351 | '%d', '%s', '%s', '%s', '%s', '%s', '%d', '%d', '%d', '%d', |
| 330 | 352 | '%s', '%f', '%f', '%f', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d', |
| 353 | + '%d', '%s', '%d', | |
| 331 | 354 | ]); |
| 332 | - | |
| 333 | - return $this->wpdb->insert_id; | |
| 355 | + | |
| 356 | + // A rejected insert used to be swallowed: insert_id stays 0, the caller | |
| 357 | + // looks up row 0, gets null, and trips its own return type with a fatal | |
| 358 | + // TypeError. Fail loudly instead so the caller can report something useful. | |
| 359 | + if ($inserted === false) { | |
| 360 | + throw new \RuntimeException( | |
| 361 | + $this->wpdb->last_error !== '' | |
| 362 | + ? $this->wpdb->last_error | |
| 363 | + : 'Could not save the availability date.' | |
| 364 | + ); | |
| 365 | + } | |
| 366 | + | |
| 367 | + return (int) $this->wpdb->insert_id; | |
| 334 | 368 | } |
| 335 | 369 | |
| 336 | 370 | /** |
| 337 | 371 | * Update availability date |
| @@ -374,9 +408,15 @@ | ||
| 374 | 408 | } |
| 375 | 409 | if (isset($data['special_notes'])) $updateData['special_notes'] = !empty($data['special_notes']) ? sanitize_textarea_field($data['special_notes']) : null; |
| 376 | 410 | if (isset($data['cutoff_date'])) $updateData['cutoff_date'] = !empty($data['cutoff_date']) ? sanitize_text_field($data['cutoff_date']) : null; |
| 377 | 411 | if (isset($data['cutoff_hours'])) $updateData['cutoff_hours'] = (int) $data['cutoff_hours']; |
| 378 | - | |
| 412 | + // array_key_exists (not isset) so an explicit null from the form — e.g. | |
| 413 | + // clearing the block reason / threshold when a date is unblocked — is | |
| 414 | + // honored instead of silently skipped (isset(null) === false). | |
| 415 | + if (array_key_exists('is_blocked', $data)) $updateData['is_blocked'] = !empty($data['is_blocked']) ? 1 : 0; | |
| 416 | + if (array_key_exists('block_reason', $data)) $updateData['block_reason'] = !empty($data['block_reason']) ? mb_substr(sanitize_textarea_field($data['block_reason']), 0, 255) : null; | |
| 417 | + if (array_key_exists('alert_threshold', $data)) $updateData['alert_threshold'] = ($data['alert_threshold'] !== '' && $data['alert_threshold'] !== null) ? $this->clampAlertThreshold($data['alert_threshold']) : null; | |
| 418 | + | |
| 379 | 419 | // Calculate discount percentage if not provided |
| 380 | 420 | if (!empty($updateData['original_price']) && !empty($updateData['discounted_price']) && empty($updateData['discount_percentage'])) { |
| 381 | 421 | $updateData['discount_percentage'] = round((($updateData['original_price'] - $updateData['discounted_price']) / $updateData['original_price']) * 100, 2); |
| 382 | 422 | } |