PluginProbe
Yatra – Travel Booking & Tour Operator Software / trunk
Yatra – Travel Booking & Tour Operator Software vtrunk
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 2.0.10 All 83 releases
← All changes | includes/helpers.php +253 -16 3.0.14.2trunk View file →
@@ -54,9 +54,15 @@
54 54 * Get booking form configuration
55 55 *
56 56 * @return array
57 57 */
58 -function yatra_get_booking_form_config(): array
58 +/**
59 + * @param int|null $tripId Trip being booked. Pass it from every checkout-side
60 + * caller so per-trip field visibility (Pro) applies to
61 + * rendering, the AJAX re-render and server validation
62 + * alike. Omit it where the whole config is wanted.
63 + */
64 +function yatra_get_booking_form_config(?int $tripId = null): array
59 65 {
60 66 // Check if Dynamic Form Field module is enabled via Pro plugin
61 67 $is_dynamic_enabled = apply_filters('yatra_dynamic_form_field_enabled', false);
62 68
@@ -61,15 +67,16 @@
61 67 $is_dynamic_enabled = apply_filters('yatra_dynamic_form_field_enabled', false);
62 68
63 69 if ($is_dynamic_enabled) {
64 70 // Pro module is active — merged config from options (filtered in SettingsService::getBookingFormConfig)
65 - return SettingsService::getBookingFormConfig();
71 + return SettingsService::getBookingFormConfig($tripId);
66 72 }
67 73
68 74 // Module off: still allow filters to adjust defaults (tests / edge integrations)
69 75 return apply_filters(
70 76 'yatra_booking_form_config',
71 - SettingsService::getDefaultBookingFormConfig()
77 + SettingsService::getDefaultBookingFormConfig(),
78 + $tripId
72 79 );
73 80 }
74 81
75 82 /**
@@ -1305,12 +1312,23 @@
1305 1312 * Core always fired `yatra_booking_status_changed`; Pro modules (Trip Consent, Google Calendar) listen
1306 1313 * on this dedicated action. Call this after any code path that sets a booking to `confirmed` without
1307 1314 * going through {@see \Yatra\Services\BookingService::updateStatus()}.
1308 1315 *
1309 - * @param int $bookingId Booking ID.
1310 - * @param string $previousStatus Booking status in the database immediately before confirming.
1316 + * Async payment-completion paths (gateway webhooks / return handlers, scheduled
1317 + * payments) confirm the booking with a direct DB write, bypassing
1318 + * updateStatus(). Pass $fromDirectConfirm = true from those paths so this
1319 + * function replicates the customer-facing side effects updateStatus() would
1320 + * have run — the "booking confirmed" email AND the `yatra_booking_status_changed`
1321 + * action that status-based listeners (Pro Email Automation, cache invalidation,
1322 + * inventory sync) rely on. The manual / checkout / waitlist paths leave it false
1323 + * because they already run those side effects themselves; passing true there
1324 + * would double-fire them.
1325 + *
1326 + * @param int $bookingId Booking ID.
1327 + * @param string $previousStatus Booking status in the database immediately before confirming.
1328 + * @param bool $fromDirectConfirm True for confirmations that bypassed updateStatus().
1311 1329 */
1312 -function yatra_trigger_booking_confirmed(int $bookingId, string $previousStatus): void
1330 +function yatra_trigger_booking_confirmed(int $bookingId, string $previousStatus, bool $fromDirectConfirm = false): void
1313 1331 {
1314 1332 if ($bookingId < 1 || $previousStatus === 'confirmed') {
1315 1333 return;
1316 1334 }
@@ -1321,8 +1339,17 @@
1321 1339 if (!$booking || ($booking->status ?? '') !== 'confirmed') {
1322 1340 return;
1323 1341 }
1324 1342
1343 + if ($fromDirectConfirm) {
1344 + // Mirror BookingService::updateStatus(): send the confirmation email and
1345 + // fire the generic status-change action for status-based listeners. Only
1346 + // async/direct confirms reach here with true — the manual, checkout and
1347 + // waitlist paths fire these themselves, so this never double-fires.
1348 + (new \Yatra\Services\BookingService())->sendBookingConfirmedEmail($bookingId);
1349 + do_action('yatra_booking_status_changed', $bookingId, $previousStatus, 'confirmed');
1350 + }
1351 +
1325 1352 /**
1326 1353 * Booking reached confirmed status (was not confirmed before this transition).
1327 1354 *
1328 1355 * @param int $bookingId Booking ID.
@@ -1331,17 +1358,150 @@
1331 1358 do_action('yatra_booking_confirmed', $bookingId, $booking);
1332 1359 }
1333 1360
1334 1361 /**
1362 + * Fire `yatra_booking_cancelled` for a booking that has just been cancelled.
1363 + *
1364 + * The action is documented and listened to (Google Calendar removes its event,
1365 + * the Pro webhook `booking.cancelled` and the WhatsApp cancellation template are
1366 + * bound to it) but nothing in the plugin ever fired it: only Channel Manager's
1367 + * OTA ingest did, so an in-app cancellation reached none of those listeners.
1368 + *
1369 + * Call it from the specific transition sites — not from a global
1370 + * `yatra_booking_status_changed` listener — so the OTA path, which already
1371 + * fires this action itself, cannot double-fire.
1372 + *
1373 + * @param int $bookingId Booking ID.
1374 + * @param string $previousStatus Status before the transition.
1375 + */
1376 +function yatra_trigger_booking_cancelled(int $bookingId, string $previousStatus): void
1377 +{
1378 + if ($bookingId < 1 || $previousStatus === 'cancelled') {
1379 + return;
1380 + }
1381 +
1382 + $repo = new \Yatra\Repositories\BookingRepository();
1383 + $booking = $repo->findWithTrip($bookingId);
1384 +
1385 + // Only announce a cancellation that actually stuck.
1386 + if (!$booking || ($booking->status ?? '') !== 'cancelled') {
1387 + return;
1388 + }
1389 +
1390 + /**
1391 + * Booking reached cancelled status (was not cancelled before this transition).
1392 + *
1393 + * @param int $bookingId Booking ID.
1394 + * @param object $booking Row from {@see \Yatra\Repositories\BookingRepository::findWithTrip()}.
1395 + */
1396 + do_action('yatra_booking_cancelled', $bookingId, $booking);
1397 +}
1398 +
1399 +/**
1400 + * Resolve the "Auto-Confirm Bookings" mode.
1401 + *
1402 + * Modes:
1403 + * - 'none' — never auto-confirm; every booking stays pending for manual review.
1404 + * - 'online' — auto-confirm only when a successful ONLINE gateway payment
1405 + * (Stripe, PayPal, Razorpay, …) settles the balance in full.
1406 + * Deposits / partial payments and offline methods (bank transfer,
1407 + * pay-later) stay pending.
1408 + * - 'all' — auto-confirm every booking at checkout, paid or not.
1409 + *
1410 + * No migration is stored: the value is resolved on the fly. When the operator
1411 + * has never chosen a mode (no `yatra_auto_confirm_mode` option), we derive it
1412 + * from the legacy boolean `auto_confirm_bookings` so each site keeps its ACTUAL
1413 + * behaviour from the released (buggy) version:
1414 + * - true → 'all' (it confirmed every booking at checkout)
1415 + * - false → 'online' (online payments auto-confirmed anyway — that was the
1416 + * bug — while offline methods stayed pending)
1417 + * The first time the operator saves the setting, the chosen mode is stored and
1418 + * becomes authoritative. New installs default to 'online' (see the default in
1419 + * SettingsController / SettingsService).
1420 + *
1421 + * @return string One of: none | online | all.
1422 + */
1423 +function yatra_get_auto_confirm_mode(): string
1424 +{
1425 + $raw = get_option('yatra_auto_confirm_mode', null);
1426 + if (is_string($raw)) {
1427 + $mode = strtolower(trim($raw));
1428 + if (in_array($mode, ['none', 'online', 'all'], true)) {
1429 + return $mode;
1430 + }
1431 + }
1432 +
1433 + // Never configured: preserve the site's experienced behaviour.
1434 + return \Yatra\Services\SettingsService::isEnabled('auto_confirm_bookings') ? 'all' : 'online';
1435 +}
1436 +
1437 +/**
1438 + * How far ahead (in months) the storefront lets customers see and book dates.
1439 + *
1440 + * Reads `availability_horizon_months` (Settings → Booking). The default, 12, is
1441 + * the value that was hard-coded before it became configurable, so a site that
1442 + * never touches the setting behaves exactly as before. Anything outside 1–36
1443 + * falls back to 12 rather than blanking the calendar. Callers that pass their
1444 + * own explicit date range (REST `to_date`, the OTA inventory sync, admin
1445 + * previews) are not affected by this at all.
1446 + *
1447 + * Developers can adjust the horizon per request:
1448 + *
1449 + * add_filter('yatra_availability_horizon_months', fn($m) => is_page('summer') ? 6 : $m);
1450 + *
1451 + * @return int Months, 1–36.
1452 + */
1453 +function yatra_get_availability_horizon_months(): int
1454 +{
1455 + $months = (int) \Yatra\Services\SettingsService::getInt('availability_horizon_months', 12);
1456 + if ($months < 1 || $months > 36) {
1457 + $months = 12;
1458 + }
1459 +
1460 + /**
1461 + * Filter the storefront booking horizon.
1462 + *
1463 + * @param int $months Horizon in months (1–36).
1464 + */
1465 + $filtered = (int) apply_filters('yatra_availability_horizon_months', $months);
1466 +
1467 + return ($filtered < 1 || $filtered > 36) ? $months : $filtered;
1468 +}
1469 +
1470 +/**
1471 + * The last date (Y-m-d) the storefront offers: the start date plus the horizon.
1472 + *
1473 + * Mirrors the `date('Y-m-d', strtotime('+12 months'))` expression the callers
1474 + * used before, so with the default setting the result is byte-identical.
1475 + *
1476 + * @param string|null $fromDate Start date (Y-m-d). Defaults to today.
1477 + * @return string Y-m-d.
1478 + */
1479 +function yatra_get_availability_horizon_date(?string $fromDate = null): string
1480 +{
1481 + $base = ($fromDate !== null && $fromDate !== '' && strtotime($fromDate) !== false)
1482 + ? (int) strtotime($fromDate)
1483 + : time();
1484 + $ts = strtotime('+' . yatra_get_availability_horizon_months() . ' months', $base);
1485 +
1486 + return date('Y-m-d', $ts !== false ? $ts : (int) strtotime('+12 months', $base));
1487 +}
1488 +
1489 +/**
1335 1490 * Decide whether a successful payment should auto-confirm the booking.
1336 1491 *
1337 - * A booking auto-confirms on payment only when the operator has enabled
1338 - * "Auto-Confirm Bookings", OR the booking is now fully paid. A deposit /
1339 - * partial payment must NOT confirm the booking while auto-confirm is off — the
1340 - * operator confirms it manually. Previously the synchronous-gateway, Stripe and
1341 - * PayPal completion paths force-confirmed on any payment, so deposit bookings
1342 - * were confirmed immediately regardless of the setting.
1492 + * This runs on the ONLINE payment-completion path. It confirms when the
1493 + * "Auto-Confirm Bookings" mode is 'all', or when the mode is 'online' AND the
1494 + * payment settles the balance in full ($fullyPaid). Mode 'none' — and an
1495 + * 'online' deposit / partial payment — leaves the booking `pending`.
1343 1496 *
1497 + * The $fullyPaid flag is still passed to the `yatra_confirm_booking_on_payment`
1498 + * filter so an operator who wants the older "confirm once fully paid" behaviour
1499 + * can opt back in without touching core:
1500 + *
1501 + * add_filter('yatra_confirm_booking_on_payment',
1502 + * function ($shouldConfirm, $fullyPaid) { return $shouldConfirm || $fullyPaid; }, 10, 2);
1503 + *
1344 1504 * @param bool $fullyPaid Whether the booking's balance is now zero.
1345 1505 * @param int $bookingId Booking ID (passed to the filter for context).
1346 1506 * @return bool True to set the booking to `confirmed`.
1347 1507 */
@@ -1346,20 +1506,97 @@
1346 1506 * @return bool True to set the booking to `confirmed`.
1347 1507 */
1348 1508 function yatra_should_confirm_booking_on_payment(bool $fullyPaid, int $bookingId = 0): bool
1349 1509 {
1350 - $autoConfirm = (bool) \Yatra\Services\SettingsService::isEnabled('auto_confirm_bookings');
1351 - $shouldConfirm = $autoConfirm || $fullyPaid;
1510 + $mode = yatra_get_auto_confirm_mode();
1511 + // 'all' -> always confirm on a successful payment.
1512 + // 'online' -> confirm only when the payment settles the balance in full;
1513 + // a deposit / partial online payment leaves it pending until
1514 + // the balance is paid.
1515 + // 'none' -> never.
1516 + $shouldConfirm = ($mode === 'all') || ($mode === 'online' && $fullyPaid);
1517 + // Backward compatibility for the filter's 4th argument: since 3.0.10 it has
1518 + // been the old on/off toggle's value. The toggle maps on → 'all' and
1519 + // off → 'online', so only 'all' may report true here — a legacy-off site
1520 + // (now 'online') must keep handing existing callbacks `false`. Read the
1521 + // full mode with yatra_get_auto_confirm_mode() instead of this flag.
1522 + $autoConfirm = ($mode === 'all');
1352 1523
1353 1524 /**
1354 1525 * Filter whether a completed payment auto-confirms the booking.
1355 1526 *
1356 - * @param bool $shouldConfirm Default: auto-confirm setting is on OR fully paid.
1527 + * @param bool $shouldConfirm Default: true for mode 'all', or mode 'online' when $fullyPaid.
1357 1528 * @param bool $fullyPaid Whether the balance is now zero.
1358 1529 * @param int $bookingId Booking ID.
1359 - * @param bool $autoConfirm The `auto_confirm_bookings` setting value.
1530 + * @param bool $autoConfirm The old on/off toggle's value — true only for mode
1531 + * 'all' (unchanged meaning for callbacks written
1532 + * against 3.0.10–3.0.14). Use yatra_get_auto_confirm_mode()
1533 + * to distinguish 'online' from 'none'.
1360 1534 */
1361 1535 return (bool) apply_filters('yatra_confirm_booking_on_payment', $shouldConfirm, $fullyPaid, $bookingId, $autoConfirm);
1536 +}
1537 +
1538 +/**
1539 + * Determine whether a `yatra_payment_completed` payment settled the balance in
1540 + * full, from the raw action args.
1541 + *
1542 + * `yatra_payment_completed` fires with either an array payload carrying
1543 + * `booking_id`, or the ($bookingId, $gateway, $txnId, $array) signature — so we
1544 + * sniff the id out of the args, then read the booking's current `amount_due`.
1545 + * Uses the same `amount_due <= 0` test as the transactional payment email and
1546 + * the Email Automation event, so every channel agrees on partial vs full.
1547 + *
1548 + * @param array<int, mixed> $hookArgs Raw args the action passed.
1549 + * @return bool|null True = paid in full, false = partial/deposit, null = unknown.
1550 + */
1551 +function yatra_payment_completed_is_full(array $hookArgs): ?bool
1552 +{
1553 + $bookingId = 0;
1554 + foreach ($hookArgs as $a) {
1555 + if (is_array($a) && (int) ($a['booking_id'] ?? 0) > 0) {
1556 + $bookingId = (int) $a['booking_id'];
1557 + break;
1558 + }
1559 + if ($bookingId === 0 && is_numeric($a)) {
1560 + $bookingId = (int) $a;
1561 + }
1562 + }
1563 +
1564 + if ($bookingId < 1) {
1565 + return null;
1566 + }
1567 +
1568 + $booking = (new \Yatra\Repositories\BookingRepository())->find($bookingId);
1569 + if (!$booking) {
1570 + return null;
1571 + }
1572 +
1573 + return (float) ($booking->amount_due ?? 0) <= 0;
1574 +}
1575 +
1576 +/**
1577 + * Gate for the split payment events (`payment.received` = full,
1578 + * `payment.partial_received` = deposit) which both bind to
1579 + * `yatra_payment_completed`. Returns true when the given event should be
1580 + * delivered for this payment, so notification dispatchers (webhooks, WhatsApp)
1581 + * fire only the matching one. Non-payment events are never gated.
1582 + *
1583 + * @param array<int, mixed> $hookArgs Raw args the action passed.
1584 + */
1585 +function yatra_payment_event_applies(string $eventKey, array $hookArgs): bool
1586 +{
1587 + if ($eventKey !== 'payment.received' && $eventKey !== 'payment.partial_received') {
1588 + return true;
1589 + }
1590 +
1591 + $isFull = yatra_payment_completed_is_full($hookArgs);
1592 + if ($isFull === null) {
1593 + // Can't determine the balance — deliver the "received" (full) event and
1594 + // suppress the partial one, matching the historical default.
1595 + $isFull = true;
1596 + }
1597 +
1598 + return $eventKey === 'payment.received' ? $isFull : !$isFull;
1362 1599 }
1363 1600
1364 1601 /**
1365 1602 * ============================================