| @@ -56,9 +56,10 @@ | ||
| 56 | 56 | // Calculate capacity based on the date if not provided |
| 57 | 57 | if (empty($data['max_capacity'])) { |
| 58 | 58 | $data['max_capacity'] = $this->capacityService->getCapacityForDate( |
| 59 | 59 | (int) $data['trip_id'], |
| 60 | - $startDate | |
| 60 | + $startDate, | |
| 61 | + $data['time'] ?? null | |
| 61 | 62 | ); |
| 62 | 63 | |
| 63 | 64 | // If still no capacity, throw an error |
| 64 | 65 | if ($data['max_capacity'] <= 0) { |
| @@ -155,16 +156,53 @@ | ||
| 155 | 156 | throw new \InvalidArgumentException('A departure already exists for this trip and date'); |
| 156 | 157 | } |
| 157 | 158 | } |
| 158 | 159 | |
| 160 | + // Detect a tour (trip) reassignment: the operator is moving this | |
| 161 | + // departure from one trip to another. The DB layer already persists | |
| 162 | + // trip_id, but the move must be validated and kept consistent: | |
| 163 | + // 1. the target trip must exist, | |
| 164 | + // 2. it must not collide with an existing departure on the target | |
| 165 | + // trip for the same date/time, and | |
| 166 | + // 3. the departure's bookings must move to the new trip too, so a | |
| 167 | + // booking never ends up pointing at a departure that belongs to a | |
| 168 | + // different trip (the invariant the booking edit path also keeps). | |
| 169 | + $newTripId = isset($data['trip_id']) ? (int) $data['trip_id'] : 0; | |
| 170 | + $tripReassigned = $newTripId > 0 && $newTripId !== (int) $departure->trip_id; | |
| 171 | + | |
| 172 | + if ($tripReassigned) { | |
| 173 | + $targetTrip = $this->tripRepository->find($newTripId); | |
| 174 | + if (!$targetTrip) { | |
| 175 | + throw new \InvalidArgumentException('Target tour not found'); | |
| 176 | + } | |
| 177 | + | |
| 178 | + $checkDate = $data['start_date'] ?? $data['date'] ?? ($departure->start_date ?: $departure->date); | |
| 179 | + $checkTime = $data['time'] ?? $departure->time; | |
| 180 | + $existing = $this->repository->findByTripIdAndStartDate($newTripId, (string) $checkDate, $checkTime); | |
| 181 | + if ($existing && (int) $existing->id !== $id) { | |
| 182 | + throw new \InvalidArgumentException( | |
| 183 | + 'A departure already exists for the selected tour on this date' | |
| 184 | + ); | |
| 185 | + } | |
| 186 | + } | |
| 187 | + | |
| 159 | 188 | // Mark as manually edited by admin |
| 160 | 189 | if (!isset($data['source'])) { |
| 161 | 190 | $data['source'] = 'manual'; // Admin edits mark as manual |
| 162 | 191 | } |
| 163 | - | |
| 192 | + | |
| 164 | 193 | // Update departure |
| 165 | 194 | $result = $this->repository->update($id, $data); |
| 166 | - | |
| 195 | + | |
| 196 | + // Move the departure's bookings onto the new trip so booking.trip_id and | |
| 197 | + // departure.trip_id stay in sync after a manual reassignment. | |
| 198 | + if ($result && $tripReassigned) { | |
| 199 | + $bookingIds = $this->bookingDepartureRepository->getBookingIdsForDeparture($id); | |
| 200 | + foreach ($bookingIds as $bookingId) { | |
| 201 | + $this->bookingRepository->update((int) $bookingId, ['trip_id' => $newTripId]); | |
| 202 | + } | |
| 203 | + } | |
| 204 | + | |
| 167 | 205 | // Trigger hook to sync capacity from availability |
| 168 | 206 | if ($result) { |
| 169 | 207 | do_action('yatra_departure_saved', $id); |
| 170 | 208 | } |
| @@ -304,8 +342,27 @@ | ||
| 304 | 342 | return $this->repository->findByTripId($tripId, $filters); |
| 305 | 343 | } |
| 306 | 344 | |
| 307 | 345 | /** |
| 346 | + * Count departures across all trips for the same filters as | |
| 347 | + * getAllDepartures() (pagination keys ignored) — the true total behind a | |
| 348 | + * paginated list. | |
| 349 | + */ | |
| 350 | + public function countAllDepartures(array $filters = []): int | |
| 351 | + { | |
| 352 | + return $this->repository->countAll($filters); | |
| 353 | + } | |
| 354 | + | |
| 355 | + /** | |
| 356 | + * Count departures for one trip for the same filters as getByTripId() | |
| 357 | + * (pagination keys ignored). | |
| 358 | + */ | |
| 359 | + public function countByTripId(int $tripId, array $filters = []): int | |
| 360 | + { | |
| 361 | + return $this->repository->countByTripId($tripId, $filters); | |
| 362 | + } | |
| 363 | + | |
| 364 | + /** | |
| 308 | 365 | * Get past departures by trip ID |
| 309 | 366 | */ |
| 310 | 367 | public function getPastByTripId(int $tripId, array $filters = []): array |
| 311 | 368 | { |
| @@ -325,15 +382,18 @@ | ||
| 325 | 382 | * Combines manual departures and dynamically generated recurring rule dates |
| 326 | 383 | * |
| 327 | 384 | * @param int $tripId Trip ID |
| 328 | 385 | * @param string $fromDate Start date (default: today) |
| 329 | - * @param string $toDate End date (default: +12 months) | |
| 386 | + * @param string $toDate End date (default: today + the configurable booking horizon, 12 months unless changed) | |
| 330 | 387 | * @return array Available dates with pricing and capacity info |
| 331 | 388 | */ |
| 332 | 389 | public function getAvailableDates(int $tripId, ?string $fromDate = null, ?string $toDate = null): array |
| 333 | 390 | { |
| 334 | 391 | $fromDate = $fromDate ?? date('Y-m-d'); |
| 335 | - $toDate = $toDate ?? date('Y-m-d', strtotime('+12 months')); | |
| 392 | + // Default counted from today (not $fromDate), exactly as the previous | |
| 393 | + // hard-coded '+12 months' was, so callers passing only a start date | |
| 394 | + // keep the same window. | |
| 395 | + $toDate = $toDate ?? yatra_get_availability_horizon_date(); | |
| 336 | 396 | |
| 337 | 397 | // Get all manual departures |
| 338 | 398 | $manualDepartures = $this->repository->findByTripId($tripId, [ |
| 339 | 399 | 'date_from' => $fromDate, |
| @@ -419,9 +479,9 @@ | ||
| 419 | 479 | */ |
| 420 | 480 | public function findOrCreateForBooking(int $tripId, string $startDate, string $endDate, int $travelersCount = 0, ?int $defaultMaxCapacity = null, ?string $time = null): Departure |
| 421 | 481 | { |
| 422 | 482 | // Get capacity based on priority |
| 423 | - $maxCapacity = $this->capacityService->getCapacityForDate($tripId, $startDate); | |
| 483 | + $maxCapacity = $this->capacityService->getCapacityForDate($tripId, $startDate, $time); | |
| 424 | 484 | |
| 425 | 485 | // If no capacity found from availability or rules, use the provided default |
| 426 | 486 | if ($maxCapacity <= 0 && $defaultMaxCapacity !== null) { |
| 427 | 487 | $maxCapacity = $defaultMaxCapacity; |
| @@ -602,9 +662,14 @@ | ||
| 602 | 662 | * @param string $newStartDate New start date |
| 603 | 663 | * @param string $newEndDate New end date |
| 604 | 664 | * @return array {success: bool, new_departure_id: int, old_departure_id: int|null} |
| 605 | 665 | */ |
| 606 | - public function handleBookingDateChange(int $bookingId, string $newStartDate, string $newEndDate): array | |
| 666 | + /** | |
| 667 | + * @param string|null $newDepartureTime Departure time (HH:MM/HH:MM:SS) when the trip | |
| 668 | + * runs several departures a day. Optional, so | |
| 669 | + * existing callers keep their behaviour. | |
| 670 | + */ | |
| 671 | + public function handleBookingDateChange(int $bookingId, string $newStartDate, string $newEndDate, ?string $newDepartureTime = null): array | |
| 607 | 672 | { |
| 608 | 673 | // Get booking to find trip_id |
| 609 | 674 | $booking = $this->bookingRepository->find($bookingId); |
| 610 | 675 | if (!$booking) { |
| @@ -621,10 +686,19 @@ | ||
| 621 | 686 | if ($trip && !empty($trip->max_travelers)) { |
| 622 | 687 | $maxCapacity = (int) $trip->max_travelers; |
| 623 | 688 | } |
| 624 | 689 | |
| 625 | - // Find or create new departure | |
| 626 | - $newDeparture = $this->findOrCreateForBooking($tripId, $newStartDate, $newEndDate, 0, $maxCapacity); | |
| 690 | + // Find or create new departure. The time matters when a trip runs several | |
| 691 | + // departures a day: without it the booking lands on whichever departure | |
| 692 | + // matches the date alone, so it never occupies the slot it was booked for. | |
| 693 | + $newDeparture = $this->findOrCreateForBooking( | |
| 694 | + $tripId, | |
| 695 | + $newStartDate, | |
| 696 | + $newEndDate, | |
| 697 | + 0, | |
| 698 | + $maxCapacity, | |
| 699 | + ($newDepartureTime !== null && trim($newDepartureTime) !== '') ? trim($newDepartureTime) : null | |
| 700 | + ); | |
| 627 | 701 | |
| 628 | 702 | // Link booking to new departure |
| 629 | 703 | $this->bookingDepartureRepository->updateDepartureForBooking($bookingId, $newDeparture->id); |
| 630 | 704 | |