| @@ -102,8 +102,29 @@ | ||
| 102 | 102 | if ($recurring !== null) { |
| 103 | 103 | return $this->buildAvailabilityObject($trip, $recurring, 'recurring_rule'); |
| 104 | 104 | } |
| 105 | 105 | |
| 106 | + // Priority 2.5: A date the operator explicitly excluded on a recurring | |
| 107 | + // rule — a holiday, or a period such as a business vacation. | |
| 108 | + // | |
| 109 | + // Without this the date matches no rule, falls through to the trip | |
| 110 | + // default below and reports free seats, so the booking guard allows a | |
| 111 | + // booking on a day the operator has closed. Same failure mode the | |
| 112 | + // blocked/sold_out short-circuits above already guard against. | |
| 113 | + // | |
| 114 | + // Deliberately narrow: it only suppresses the trip-default fallback. An | |
| 115 | + // explicit availability row or another rule that does generate this date | |
| 116 | + // has already returned above, so this can never close a date the | |
| 117 | + // operator opened by other means. | |
| 118 | + if ($this->recurringAvailabilityService->isDateExcludedForTrip($tripId, $date)) { | |
| 119 | + $excluded = $this->buildAvailabilityObject($trip, null, 'trip_default'); | |
| 120 | + $excluded->status = 'blocked'; | |
| 121 | + $excluded->seats_available = 0; | |
| 122 | + $excluded->seats_total = 0; | |
| 123 | + | |
| 124 | + return $excluded; | |
| 125 | + } | |
| 126 | + | |
| 106 | 127 | // Priority 3: Trip default (flexible booking / no configured calendar) |
| 107 | 128 | return $this->buildAvailabilityObject($trip, null, 'trip_default'); |
| 108 | 129 | } |
| 109 | 130 | |