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/BookingService.php +165 -18 3.0.13 → 3.0.16 View file →
@@ -386,9 +386,14 @@
386 386 'yatra_calculate_amount_due',
387 387 $bs_total - $bs_paid,
388 388 $bs_total,
389 389 $bs_payment_method,
390 - ['trip_id' => (int) ($data['trip_id'] ?? 0)]
390 + [
391 + 'trip_id' => (int) ($data['trip_id'] ?? 0),
392 + // Tour start lets Pro enforce "pay in full when the tour is
393 + // within the balance-due window" (tour-anchored payments).
394 + 'travel_date' => (string) ($data['travel_date'] ?? ($data['start_date'] ?? '')),
395 + ]
391 396 );
392 397 $data['amount_due'] = max(0.0, round($bs_due_now, 2));
393 398
394 399 // Create booking
@@ -566,8 +571,9 @@
566 571 }
567 572
568 573 $oldStatus = (string) ($booking->status ?? '');
569 574 $oldPaymentStatus = (string) ($booking->payment_status ?? '');
575 + $oldTripId = (int) ($booking->trip_id ?? 0);
570 576
571 577 // Update booking
572 578 $updated = $this->bookingRepository->update($id, $data);
573 579
@@ -588,14 +594,36 @@
588 594 if (!empty($data['departure_time']) && is_string($data['departure_time'])) {
589 595 $departureTimeForUpdate = trim($data['departure_time']) !== '' ? trim($data['departure_time']) : null;
590 596 }
591 597
592 - if (($dateChanged || $departureTimeForUpdate !== null) && !empty($data['start_date']) && !empty($data['end_date'])) {
598 + // Changing the tour (trip_id) also has to move the departure: the booking
599 + // now belongs to a different trip, so its seat has to be released from the
600 + // old trip's departure and taken on the new trip's departure. Previously
601 + // only a date/time change triggered the re-link, so switching Tour A -> Tour B
602 + // left the booking counted against Tour A's departure (and absent from
603 + // Tour B's) — over-selling A and under-selling B.
604 + $tripChanged = isset($data['trip_id'])
605 + && (int) $data['trip_id'] > 0
606 + && (int) $data['trip_id'] !== $oldTripId;
607 +
608 + // handleBookingDateChange() re-reads the booking (already saved with the new
609 + // trip_id above) and needs the effective travel dates. When only the tour
610 + // changed, the date fields aren't in $data, so fall back to the booking's
611 + // current dates rather than skipping the re-link.
612 + $effectiveStart = !empty($data['start_date'])
613 + ? $data['start_date']
614 + : ($booking->start_date ?? $booking->travel_date ?? null);
615 + $effectiveEnd = !empty($data['end_date'])
616 + ? $data['end_date']
617 + : ($booking->end_date ?? $effectiveStart);
618 +
619 + if (($dateChanged || $tripChanged || $departureTimeForUpdate !== null)
620 + && !empty($effectiveStart) && !empty($effectiveEnd)) {
593 621 try {
594 622 $this->departureService->handleBookingDateChange(
595 623 $id,
596 - $data['start_date'],
597 - $data['end_date'],
624 + $effectiveStart,
625 + $effectiveEnd,
598 626 $departureTimeForUpdate
599 627 );
600 628 } catch (\Exception $e) {
601 629 // Log error but don't fail the update
@@ -647,8 +675,13 @@
647 675 if ($newStatus === 'confirmed' && $oldStatus !== 'confirmed'
648 676 && function_exists('yatra_trigger_booking_confirmed')) {
649 677 yatra_trigger_booking_confirmed($id, $oldStatus);
650 678 }
679 +
680 + if ($newStatus === 'cancelled' && $oldStatus !== 'cancelled'
681 + && function_exists('yatra_trigger_booking_cancelled')) {
682 + yatra_trigger_booking_cancelled($id, $oldStatus);
683 + }
651 684 }
652 685
653 686 // Return the fresh booking so the REST controller's `$result['data']`
654 687 // is defined (previously absent → "Undefined array key data" warning).
@@ -847,10 +880,17 @@
847 880 $startDate = $booking->start_date ?? $booking->travel_date;
848 881 $endDate = $booking->end_date ?? $this->calculateEndDate($startDate, (int) $booking->trip_id);
849 882
850 883 $trip = $this->tripRepository->find((int) $booking->trip_id);
851 - $maxCapacity = $trip ? ($trip->max_capacity ?? 9999) : 9999;
852 -
884 + // Resolve capacity from the trip's real column (`max_travelers`).
885 + // The old `$trip->max_capacity` does not exist on the trips table,
886 + // so this always fell back to 9999 — seeding a junk "unlimited"
887 + // sentinel that then showed as a huge number on the Departures page
888 + // but as 0 on the dashboard. Pass null when unset so
889 + // findOrCreateForBooking resolves via Availability, then its own
890 + // trip-default fallback, exactly like the primary creation path.
891 + $maxCapacity = ($trip && !empty($trip->max_travelers)) ? (int) $trip->max_travelers : null;
892 +
853 893 $departure = $this->departureService->findOrCreateForBooking(
854 894 (int) $booking->trip_id,
855 895 $startDate,
856 896 $endDate,
@@ -882,8 +922,12 @@
882 922 if ($status === 'confirmed' && $oldStatus !== 'confirmed') {
883 923 \yatra_trigger_booking_confirmed($id, $oldStatus);
884 924 }
885 925
926 + if ($status === 'cancelled' && $oldStatus !== 'cancelled') {
927 + \yatra_trigger_booking_cancelled($id, $oldStatus);
928 + }
929 +
886 930 return [
887 931 'success' => true,
888 932 'message' => sprintf(
889 933 /* translators: %s: new booking status. */
@@ -1218,10 +1262,39 @@
1218 1262 );
1219 1263 }
1220 1264
1221 1265 /**
1266 + * Send the "your booking has been confirmed" transactional email.
1267 + *
1268 + * Shared by the manual / programmatic status-change path ({@see updateStatus()}
1269 + * → sendStatusChangeNotification()) and the asynchronous payment-completion
1270 + * paths (via {@see yatra_trigger_booking_confirmed()}), which set a booking to
1271 + * `confirmed` with a direct DB write and never pass through updateStatus().
1272 + * Gated by the template's enabled flag (`sendIfEnabled`).
1273 + *
1274 + * @param int $bookingId Booking ID.
1275 + */
1276 + public function sendBookingConfirmedEmail(int $bookingId): void
1277 + {
1278 + $booking = $this->bookingRepository->findWithTrip($bookingId);
1279 +
1280 + if (!$booking || empty($booking->contact_email)) {
1281 + return;
1282 + }
1283 +
1284 + $vars = TransactionalEmailTemplateService::variablesFromBooking($booking);
1285 + $vars['intro_paragraph'] = __('Your booking has been confirmed! Here are your details:', 'yatra');
1286 + $vars['transactional_context'] = 'status_confirmed';
1287 + TransactionalEmailTemplateService::sendIfEnabled(
1288 + TransactionalEmailTemplateService::TYPE_BOOKING_CONFIRMATION,
1289 + $booking->contact_email,
1290 + $vars
1291 + );
1292 + }
1293 +
1294 + /**
1222 1295 * Send status change notification
1223 - *
1296 + *
1224 1297 * @param int $bookingId Booking ID
1225 1298 * @param string $oldStatus Previous status
1226 1299 * @param string $newStatus New status
1227 1300 */
@@ -1252,16 +1325,9 @@
1252 1325 return;
1253 1326 }
1254 1327
1255 1328 if ($newStatus === 'confirmed') {
1256 - $vars = TransactionalEmailTemplateService::variablesFromBooking($booking);
1257 - $vars['intro_paragraph'] = __('Your booking has been confirmed! Here are your details:', 'yatra');
1258 - $vars['transactional_context'] = 'status_confirmed';
1259 - TransactionalEmailTemplateService::sendIfEnabled(
1260 - TransactionalEmailTemplateService::TYPE_BOOKING_CONFIRMATION,
1261 - $booking->contact_email,
1262 - $vars
1263 - );
1329 + $this->sendBookingConfirmedEmail($bookingId);
1264 1330
1265 1331 return;
1266 1332 }
1267 1333
@@ -1335,15 +1401,30 @@
1335 1401 if (!$booking) {
1336 1402 return ['success' => false, 'message' => __('Booking not found.', 'yatra')];
1337 1403 }
1338 1404
1339 - if (empty($booking->contact_email)) {
1340 - return ['success' => false, 'message' => __('No email address found.', 'yatra')];
1405 + // Customer-facing emails need a recipient; admin notifications go to the
1406 + // store admin address, so they don't require the booking's contact email.
1407 + $customerEmail = (string) ($booking->contact_email ?? '');
1408 + $customerTypes = ['confirmation', 'reminder', 'cancellation', 'completed', 'payment_confirmation'];
1409 + if (in_array($emailType, $customerTypes, true) && $customerEmail === '') {
1410 + return ['success' => false, 'message' => __('No customer email address on this booking.', 'yatra')];
1341 1411 }
1342 1412
1343 1413 switch ($emailType) {
1344 1414 case 'confirmation':
1345 - $this->sendBookingConfirmationEmail($bookingId);
1415 + // Resend the email that matches the booking's CURRENT state so it
1416 + // is identical to the automated one. A confirmed booking (or a
1417 + // completed one — it was confirmed before it travelled) gets the
1418 + // "booking confirmed" email (status_confirmed context → the
1419 + // `booking_confirmed` / booking.confirmed template); anything
1420 + // still pending gets the initial "booking received" email
1421 + // (booking_created context → the `booking_confirmation` template).
1422 + if (in_array((string) ($booking->status ?? ''), ['confirmed', 'completed'], true)) {
1423 + $this->sendBookingConfirmedEmail($bookingId);
1424 + } else {
1425 + $this->sendBookingConfirmationEmail($bookingId);
1426 + }
1346 1427 break;
1347 1428
1348 1429 case 'reminder':
1349 1430 $this->sendBookingReminderEmail($booking);
@@ -1348,8 +1429,50 @@
1348 1429 case 'reminder':
1349 1430 $this->sendBookingReminderEmail($booking);
1350 1431 break;
1351 1432
1433 + case 'cancellation':
1434 + // Mirrors sendStatusChangeNotification() so the resent email is
1435 + // identical to the automated cancellation email.
1436 + $vars = TransactionalEmailTemplateService::variablesFromBooking($booking);
1437 + $vars['cancellation_reason'] = (string) ($booking->cancellation_reason ?? '');
1438 + TransactionalEmailTemplateService::sendIfEnabled(
1439 + TransactionalEmailTemplateService::TYPE_BOOKING_CANCELLATION,
1440 + $customerEmail,
1441 + $vars
1442 + );
1443 + break;
1444 +
1445 + case 'completed':
1446 + $vars = TransactionalEmailTemplateService::variablesFromBooking($booking);
1447 + $vars['completion_date'] = date_i18n(get_option('date_format'));
1448 + TransactionalEmailTemplateService::sendIfEnabled(
1449 + TransactionalEmailTemplateService::TYPE_BOOKING_COMPLETED,
1450 + $customerEmail,
1451 + $vars
1452 + );
1453 + break;
1454 +
1455 + case 'payment_confirmation':
1456 + $paymentData = $this->buildPaymentDataForResend($booking);
1457 + if ($paymentData === null) {
1458 + return ['success' => false, 'message' => __('No recorded payment to resend for this booking.', 'yatra')];
1459 + }
1460 + \Yatra\Services\NotificationService::resendCustomerPaymentEmail($paymentData);
1461 + break;
1462 +
1463 + case 'admin_new_booking':
1464 + \Yatra\Services\NotificationService::sendBookingCreatedNotification($bookingId, (array) $booking);
1465 + break;
1466 +
1467 + case 'admin_payment_received':
1468 + $paymentData = $this->buildPaymentDataForResend($booking);
1469 + if ($paymentData === null) {
1470 + return ['success' => false, 'message' => __('No recorded payment to resend for this booking.', 'yatra')];
1471 + }
1472 + \Yatra\Services\NotificationService::resendAdminPaymentEmail($paymentData);
1473 + break;
1474 +
1352 1475 default:
1353 1476 return ['success' => false, 'message' => __('Unknown email type.', 'yatra')];
1354 1477 }
1355 1478
@@ -1355,8 +1478,32 @@
1355 1478
1356 1479 return [
1357 1480 'success' => true,
1358 1481 'message' => __('Email sent successfully.', 'yatra'),
1482 + ];
1483 + }
1484 +
1485 + /**
1486 + * Reconstruct the payment-notification payload for a resend from the latest
1487 + * payment on the booking (falling back to the booking's own amount_paid /
1488 + * gateway when no ledger row exists). Returns null when nothing has been
1489 + * paid, so there is no payment to acknowledge.
1490 + */
1491 + private function buildPaymentDataForResend(object $booking): ?array
1492 + {
1493 + $bookingId = (int) ($booking->id ?? 0);
1494 + $payment = $this->paymentRepository->findLatestByBookingId($bookingId);
1495 +
1496 + $amount = (float) ($payment->amount ?? $booking->amount_paid ?? 0);
1497 + if ($amount <= 0) {
1498 + return null;
1499 + }
1500 +
1501 + return [
1502 + 'booking_id' => $bookingId,
1503 + 'amount' => $amount,
1504 + 'payment_method' => (string) ($payment->gateway ?? $booking->payment_gateway ?? ''),
1505 + 'transaction_id' => (string) ($payment->transaction_id ?? ''),
1359 1506 ];
1360 1507 }
1361 1508
1362 1509 /**