PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.16
Yatra – Travel Booking & Tour Operator Software v3.0.16
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
← All changes | app/Services/DepartureService.php +105 -19 3.0.10 → 3.0.16 View file →
@@ -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 }
@@ -184,26 +222,38 @@
184 222 return $result;
185 223 }
186 224
187 225 /**
188 - * Delete a departure
189 - * Only allowed if source is recurring_generated and booked_count is 0
226 + * Delete a departure.
227 + *
228 + * Allowed once the departure no longer has any booking attached, whether it
229 + * was created automatically or by hand — an operator who adds a departure by
230 + * mistake must be able to remove it again.
190 231 */
191 232 public function delete(int $id): bool
192 233 {
193 234 $departure = $this->repository->findModel($id);
194 -
235 +
195 236 if (!$departure) {
196 237 throw new \InvalidArgumentException('Departure not found');
197 238 }
198 -
199 - // Only allow deletion of recurring_generated departures with no bookings
200 - if ($departure->source === 'recurring_generated' && $departure->booked_count === 0) {
201 - return $this->repository->delete($id);
239 +
240 + // Bookings themselves are the source of truth here, not `booked_count`:
241 + // that counter can drift upwards (a cancelled or expired booking does not
242 + // always decrement it), which would otherwise leave a departure
243 + // permanently undeletable long after its last booking went away.
244 + //
245 + // This is the ONLY thing standing between a departure and deletion. The
246 + // previous guard also required source === 'recurring_generated', a value
247 + // this plugin never writes (departures are `booking_created` or `manual`,
248 + // see Departure::$source), so no departure was ever deletable at all.
249 + if (!empty($this->bookingRepository->findByDepartureId($id))) {
250 + throw new \InvalidArgumentException(
251 + __('Cannot delete departure: it still has bookings attached.', 'yatra')
252 + );
202 253 }
203 -
204 - // Manual departures or departures with bookings cannot be deleted
205 - throw new \InvalidArgumentException('Cannot delete departure: Manual departures or departures with bookings cannot be deleted');
254 +
255 + return $this->repository->delete($id);
206 256 }
207 257
208 258 /**
209 259 * Increment booked count (when booking is created).
@@ -292,8 +342,27 @@
292 342 return $this->repository->findByTripId($tripId, $filters);
293 343 }
294 344
295 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 + /**
296 365 * Get past departures by trip ID
297 366 */
298 367 public function getPastByTripId(int $tripId, array $filters = []): array
299 368 {
@@ -313,15 +382,18 @@
313 382 * Combines manual departures and dynamically generated recurring rule dates
314 383 *
315 384 * @param int $tripId Trip ID
316 385 * @param string $fromDate Start date (default: today)
317 - * @param string $toDate End date (default: +12 months)
386 + * @param string $toDate End date (default: today + the configurable booking horizon, 12 months unless changed)
318 387 * @return array Available dates with pricing and capacity info
319 388 */
320 389 public function getAvailableDates(int $tripId, ?string $fromDate = null, ?string $toDate = null): array
321 390 {
322 391 $fromDate = $fromDate ?? date('Y-m-d');
323 - $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();
324 396
325 397 // Get all manual departures
326 398 $manualDepartures = $this->repository->findByTripId($tripId, [
327 399 'date_from' => $fromDate,
@@ -407,9 +479,9 @@
407 479 */
408 480 public function findOrCreateForBooking(int $tripId, string $startDate, string $endDate, int $travelersCount = 0, ?int $defaultMaxCapacity = null, ?string $time = null): Departure
409 481 {
410 482 // Get capacity based on priority
411 - $maxCapacity = $this->capacityService->getCapacityForDate($tripId, $startDate);
483 + $maxCapacity = $this->capacityService->getCapacityForDate($tripId, $startDate, $time);
412 484
413 485 // If no capacity found from availability or rules, use the provided default
414 486 if ($maxCapacity <= 0 && $defaultMaxCapacity !== null) {
415 487 $maxCapacity = $defaultMaxCapacity;
@@ -590,9 +662,14 @@
590 662 * @param string $newStartDate New start date
591 663 * @param string $newEndDate New end date
592 664 * @return array {success: bool, new_departure_id: int, old_departure_id: int|null}
593 665 */
594 - 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
595 672 {
596 673 // Get booking to find trip_id
597 674 $booking = $this->bookingRepository->find($bookingId);
598 675 if (!$booking) {
@@ -609,10 +686,19 @@
609 686 if ($trip && !empty($trip->max_travelers)) {
610 687 $maxCapacity = (int) $trip->max_travelers;
611 688 }
612 689
613 - // Find or create new departure
614 - $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 + );
615 701
616 702 // Link booking to new departure
617 703 $this->bookingDepartureRepository->updateDepartureForBooking($bookingId, $newDeparture->id);
618 704