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/Controllers/BookingSessionController.php +127 -25 3.0.10 → 3.0.16 View file →
@@ -351,11 +351,19 @@
351 351 $total_paid = $paymentRepository->getTotalPaidForBooking($booking_id);
352 352 $total_amount = (float) $booking->total_amount;
353 353
354 354 if ($total_paid >= $total_amount) {
355 + // Fully paid. Respect the Auto-Confirm mode (same as every
356 + // other payment-completion path) — only confirm when the
357 + // mode is 'online' or 'all'; otherwise record the payment
358 + // and leave the booking pending for manual confirmation.
355 359 $prevStatus = (string) ($booking->status ?? 'pending');
356 - $bookingRepository->update($booking_id, ['status' => 'confirmed', 'payment_status' => 'paid']);
357 - \yatra_trigger_booking_confirmed((int) $booking_id, $prevStatus);
360 + if (\yatra_should_confirm_booking_on_payment(true, (int) $booking_id)) {
361 + $bookingRepository->update($booking_id, ['status' => 'confirmed', 'payment_status' => 'paid']);
362 + \yatra_trigger_booking_confirmed((int) $booking_id, $prevStatus, true);
363 + } else {
364 + $bookingRepository->update($booking_id, ['payment_status' => 'paid']);
365 + }
358 366 } else {
359 367 $bookingRepository->update($booking_id, ['payment_status' => 'partial']);
360 368 }
361 369 }
@@ -888,8 +896,11 @@
888 896 'slug' => $trip->slug,
889 897 'featured_image' => $trip->featured_image,
890 898 'duration_days' => (int) $trip->duration_days,
891 899 'duration_nights' => (int) $trip->duration_nights,
900 + // Hour-based day tours (0 on every day-based trip). Additive field:
901 + // existing consumers keep reading duration_days/duration_nights.
902 + 'duration_hours' => (int) ($trip->duration_hours ?? 0),
892 903 'difficulty_level' => $trip->difficulty_level,
893 904 'min_travelers' => (int) ($trip->min_travelers ?: 1),
894 905 'max_travelers' => (int) ($trip->max_travelers ?: 20),
895 906 'original_price' => (float) $trip->original_price,
@@ -1279,9 +1290,9 @@
1279 1290 // GET BOOKING SETTINGS
1280 1291 // ========================================
1281 1292 $settings = [
1282 1293 'booking_confirmation' => \Yatra\Services\SettingsService::get('booking_confirmation', true),
1283 - 'auto_confirm_bookings' => \Yatra\Services\SettingsService::get('auto_confirm_bookings', false),
1294 + 'auto_confirm_mode' => \yatra_get_auto_confirm_mode(),
1284 1295 'require_login' => \Yatra\Services\SettingsService::get('require_login', false),
1285 1296 'allow_guest_checkout' => \Yatra\Services\SettingsService::get('allow_guest_checkout', true),
1286 1297 'booking_expiry_hours' => (int) \Yatra\Services\SettingsService::get('booking_expiry_hours', 24),
1287 1298 'auto_confirm_pay_later' => \Yatra\Services\SettingsService::get('auto_confirm_pay_later', true),
@@ -1364,9 +1375,13 @@
1364 1375 // Which booking-form sections are enabled (Pro Dynamic Form module).
1365 1376 // The default config has every section enabled, so on existing/un-customised
1366 1377 // sites $contact_enabled and $traveler_enabled are both true and the logic
1367 1378 // below behaves exactly as before — only disabled sections change anything.
1368 - $form_config = function_exists('yatra_get_booking_form_config') ? yatra_get_booking_form_config() : [];
1379 + // Scoped to the trip being booked — the same config the checkout
1380 + // rendered, so a field hidden for this trip is never treated as required.
1381 + $form_config = function_exists('yatra_get_booking_form_config')
1382 + ? yatra_get_booking_form_config($trip_id > 0 ? (int) $trip_id : null)
1383 + : [];
1369 1384 $contact_enabled = !isset($form_config['contact_form']['enabled']) || (bool) $form_config['contact_form']['enabled'];
1370 1385 $traveler_enabled = !isset($form_config['traveler_form']['enabled']) || (bool) $form_config['traveler_form']['enabled'];
1371 1386
1372 1387 // Get contact email - handle both flat and nested formats
@@ -1727,9 +1742,9 @@
1727 1742 $isWaitlistCheckout = false;
1728 1743
1729 1744 if ($resolvedAvailabilityForWaitlist !== null) {
1730 1745 $availStatus = (string) ($resolvedAvailabilityForWaitlist->status ?? 'available');
1731 - if (in_array($availStatus, ['blocked', 'closed', 'cancelled'], true)) {
1746 + if (in_array($availStatus, ['blocked', 'closed', 'cancelled', 'unavailable'], true)) {
1732 1747 return new WP_REST_Response([
1733 1748 'success' => false,
1734 1749 'message' => __('This departure is not open for booking.', 'yatra'),
1735 1750 'code' => 'date_blocked',
@@ -2429,21 +2444,29 @@
2429 2444
2430 2445 // ========================================
2431 2446 // DETERMINE BOOKING STATUS
2432 2447 // ========================================
2433 - // Priority:
2434 - // 1. auto_confirm_bookings setting (confirms ALL bookings automatically)
2435 - // 2. For pay_later: auto_confirm_pay_later setting
2436 - // 3. For bank_transfer: always pending until verified
2437 -
2448 + // Priority (Auto-Confirm mode: none | online | all):
2449 + // - 'all' → confirm every booking here at checkout.
2450 + // - 'online' → confirm nothing at checkout; only a successful online
2451 + // gateway payment confirms later (offline stays pending).
2452 + // - 'none' → per-method: pay_later uses auto_confirm_pay_later,
2453 + // bank_transfer stays pending, everything else pending.
2454 +
2438 2455 $booking_status = 'pending';
2439 2456 $status_message = __('Booking received!', 'yatra');
2440 -
2441 - // Check if auto-confirm all bookings is enabled
2442 - if ($settings['auto_confirm_bookings']) {
2443 - // Auto-confirm is enabled - confirm immediately regardless of payment
2457 +
2458 + $auto_confirm_mode = $settings['auto_confirm_mode'] ?? 'none';
2459 + if ($auto_confirm_mode === 'all') {
2460 + // Confirm every booking immediately, regardless of payment.
2444 2461 $booking_status = 'confirmed';
2445 2462 $status_message = __('Booking confirmed!', 'yatra');
2463 + } elseif ($auto_confirm_mode === 'online') {
2464 + // Only successful online payments auto-confirm (at payment
2465 + // completion). Leave the booking pending at checkout; offline
2466 + // methods (bank transfer, pay-later) stay pending for the operator.
2467 + $booking_status = 'pending';
2468 + $status_message = __('Booking received!', 'yatra');
2446 2469 } elseif ($payment_gateway === 'pay_later') {
2447 2470 // Pay Later: Check the specific pay_later auto-confirm setting
2448 2471 if ($settings['auto_confirm_pay_later']) {
2449 2472 $booking_status = 'confirmed';
@@ -2863,11 +2886,15 @@
2863 2886 // Default return_url to the configured booking confirmation URL so redirect gateways
2864 2887 // (e.g. PayPal Advanced, Mollie, Paystack) do not fall back to wrong paths; gateways
2865 2888 // may still append their own query args on top of this URL.
2866 2889 $ref = isset($params['reference']) ? trim((string) $params['reference']) : '';
2890 + // Cancel returns must land on the booking-confirmation page (always resolvable);
2891 + // `home_url('/book/?...')` 404s under a custom booking base/page. Use the reference,
2892 + // falling back to the booking id so the confirmation route always has a token.
2893 + $cancelRef = $ref !== '' ? $ref : (string) ($params['booking_id'] ?? '');
2867 2894 $paymentData = array_merge($params, [
2868 2895 'description' => $params['trip_title'] ?? '',
2869 - 'cancel_url' => home_url('/book/?payment=cancelled&ref=' . ($params['reference'] ?? '')),
2896 + 'cancel_url' => add_query_arg('payment', 'cancelled', $this->getConfirmationUrl($cancelRef)),
2870 2897 'metadata' => [
2871 2898 'booking_id' => $params['booking_id'],
2872 2899 'reference' => $params['reference'] ?? ''
2873 2900 ]
@@ -2916,10 +2943,12 @@
2916 2943 ];
2917 2944 }
2918 2945
2919 2946 // For offline gateways or successful direct payments without redirect
2947 + $this->recordOfflinePendingPayment($params, $result, $gatewayId);
2948 +
2920 2949 return [
2921 - 'success' => true,
2950 + 'success' => true,
2922 2951 'redirect_url' => $this->getConfirmationUrl($params['reference'] ?? '')
2923 2952 ];
2924 2953 }
2925 2954
@@ -2944,8 +2973,75 @@
2944 2973 /**
2945 2974 * Record payment from gateway result
2946 2975 * Matches Stripe's completePayment behavior
2947 2976 */
2977 + /**
2978 + * Record the awaited payment for an offline gateway (bank transfer, cash on
2979 + * arrival, pay later) as a PENDING ledger row.
2980 + *
2981 + * These gateways take no money at checkout, and previously wrote no payment
2982 + * row at all — so when the transfer finally landed there was nothing in the
2983 + * Payments screen for the operator to mark as received. The booking's own
2984 + * fields were the only record, and marking those by hand left the invoice
2985 + * reporting "Payment Pending" with nothing paid.
2986 + *
2987 + * The row is deliberately `pending`: no money has arrived yet, and
2988 + * getTotalPaidForBooking() counts only `completed`, so booking financials and
2989 + * every report are untouched until the operator confirms it.
2990 + */
2991 + private function recordOfflinePendingPayment(array $params, array $result, string $gatewayId): void
2992 + {
2993 + try {
2994 + $bookingId = (int) ($params['booking_id'] ?? 0);
2995 + $amount = (float) ($params['amount'] ?? 0);
2996 +
2997 + if ($bookingId <= 0 || $amount <= 0) {
2998 + return;
2999 + }
3000 +
3001 + // Only for gateways that settle out of band. Anything reporting a
3002 + // completed/succeeded status already records its own row.
3003 + $status = strtolower((string) ($result['status'] ?? ''));
3004 + if (!in_array($status, ['', 'pending', 'pending_verification'], true)) {
3005 + return;
3006 + }
3007 +
3008 + $booking = $this->bookingRepository->find($bookingId);
3009 + if (!$booking || ($booking->payment_status ?? '') === 'paid') {
3010 + return;
3011 + }
3012 +
3013 + $paymentRepository = new \Yatra\Repositories\PaymentRepository();
3014 +
3015 + // Idempotency: a retried checkout must not stack up duplicate rows.
3016 + foreach ($paymentRepository->findByBookingId($bookingId) as $existing) {
3017 + if ((string) ($existing->gateway ?? '') === $gatewayId
3018 + && in_array((string) ($existing->status ?? ''), ['pending', 'completed'], true)
3019 + ) {
3020 + return;
3021 + }
3022 + }
3023 +
3024 + $paymentRepository->create([
3025 + 'booking_id' => $bookingId,
3026 + 'amount' => $amount,
3027 + 'currency' => $params['currency'] ?? \Yatra\Services\SettingsService::getCurrency(),
3028 + 'gateway' => $gatewayId,
3029 + 'status' => 'pending',
3030 + 'customer_id' => !empty($booking->customer_id) ? (int) $booking->customer_id : null,
3031 + 'notes' => __('Awaiting payment — mark as completed once received.', 'yatra'),
3032 + 'created_at' => current_time('mysql'),
3033 + ]);
3034 + } catch (\Throwable $e) {
3035 + // Never break a successful checkout over a bookkeeping row.
3036 + \Yatra\Utils\Logger::warning('Could not record pending offline payment', [
3037 + 'booking_id' => $params['booking_id'] ?? 0,
3038 + 'gateway' => $gatewayId,
3039 + 'error' => $e->getMessage(),
3040 + ]);
3041 + }
3042 + }
3043 +
2948 3044 private function recordGatewayPayment(array $params, array $result, string $gatewayId): void
2949 3045 {
2950 3046 global $wpdb;
2951 3047
@@ -2999,17 +3095,18 @@
2999 3095 // (Square, Authorize.Net) reach this generic path but previously left
3000 3096 // the booking at pending/pending — only the payment row was written.
3001 3097 // This now matches handle_successful_payment(): accumulate amount_paid,
3002 3098 // recompute amount_due, set payment_status (paid vs partial), and
3003 - // confirm the booking (a deposit confirms too, consistent with Stripe).
3099 + // confirm the booking only when "Auto-Confirm Bookings" is on
3100 + // (consistent with every gateway).
3004 3101 $newAmountPaid = (float) ($booking->amount_paid ?? 0) + $amount;
3005 3102 $newAmountDue = max(0.0, (float) ($booking->total_amount ?? 0) - $newAmountPaid);
3006 3103 $paymentStatus = $newAmountDue > 0.0 ? 'partial' : 'paid';
3007 3104 $previousStatus = (string) ($booking->status ?? 'pending');
3008 3105
3009 - // Only auto-confirm when the operator allows it (or fully paid). A
3010 - // deposit / partial payment must not confirm when "Auto-Confirm
3011 - // Bookings" is off.
3106 + // Only auto-confirm when "Auto-Confirm Bookings" is on; otherwise the
3107 + // booking stays pending for the operator to confirm manually,
3108 + // regardless of a successful (full or partial) payment.
3012 3109 $shouldConfirm = \yatra_should_confirm_booking_on_payment($newAmountDue <= 0.0, $bookingId);
3013 3110
3014 3111 $bookingUpdate = [
3015 3112 'amount_paid' => $newAmountPaid,
@@ -3021,9 +3118,9 @@
3021 3118 }
3022 3119 $this->bookingRepository->update($bookingId, $bookingUpdate);
3023 3120
3024 3121 if ($shouldConfirm && function_exists('yatra_trigger_booking_confirmed')) {
3025 - \yatra_trigger_booking_confirmed($bookingId, $previousStatus);
3122 + \yatra_trigger_booking_confirmed($bookingId, $previousStatus, true);
3026 3123 }
3027 3124
3028 3125 // Fire payment completed action
3029 3126 do_action('yatra_payment_completed', [
@@ -3091,9 +3188,9 @@
3091 3188 'description' => $params['trip_title'],
3092 3189 ]],
3093 3190 'application_context' => [
3094 3191 'return_url' => add_query_arg('payment', 'success', $this->getConfirmationUrl($params['reference'])),
3095 - 'cancel_url' => home_url('/book/?payment=cancelled&ref=' . $params['reference']),
3192 + 'cancel_url' => add_query_arg('payment', 'cancelled', $this->getConfirmationUrl($params['reference'])),
3096 3193 ],
3097 3194 ]),
3098 3195 ]);
3099 3196
@@ -3212,9 +3309,12 @@
3212 3309 'su' => add_query_arg(
3213 3310 ['payment' => 'success', 'gateway' => 'esewa'],
3214 3311 $this->getConfirmationUrl($params['reference'])
3215 3312 ),
3216 - 'fu' => home_url('/book/?payment=failed&ref=' . $params['reference']),
3313 + 'fu' => add_query_arg(
3314 + ['payment' => 'failed', 'gateway' => 'esewa'],
3315 + $this->getConfirmationUrl($params['reference'])
3316 + ),
3217 3317 ], $base_url);
3218 3318
3219 3319 return ['success' => true, 'payment_url' => $payment_url];
3220 3320 }
@@ -3366,9 +3466,9 @@
3366 3466 <p style="margin:0 0 8px;"><strong><?php esc_html_e('Booking reference', 'yatra'); ?>:</strong> <?php echo esc_html($reference); ?></p>
3367 3467 <p style="margin:0 0 8px;"><strong><?php esc_html_e('Trip', 'yatra'); ?>:</strong> <?php echo esc_html($trip->title); ?></p>
3368 3468 <p style="margin:0 0 8px;"><strong><?php esc_html_e('Travel date', 'yatra'); ?>:</strong> <?php echo esc_html(date_i18n(get_option('date_format'), strtotime($travel_date))); ?></p>
3369 3469 <p style="margin:0 0 8px;"><strong><?php esc_html_e('Duration', 'yatra'); ?>:</strong> <?php /* translators: 1: number of days, 2: number of nights. */
3370 -echo esc_html(sprintf(__('%1$d days / %2$d nights', 'yatra'), (int) $trip->duration_days, (int) $trip->duration_nights)); ?></p>
3470 +echo esc_html(yatra_format_duration((int) $trip->duration_days, (int) $trip->duration_nights, (int) ($trip->duration_hours ?? 0))); ?></p>
3371 3471 <p style="margin:0;"><strong><?php esc_html_e('Travelers', 'yatra'); ?>:</strong> <?php echo esc_html((string) count($travelers)); ?></p>
3372 3472 </div>
3373 3473 <h3 style="font-size:16px;"><?php esc_html_e('Payment details', 'yatra'); ?></h3>
3374 3474 <p><?php /* translators: %s: total amount (formatted). */
@@ -4442,9 +4542,11 @@
4442 4542 // Pro can already override per-trip via trip.deposit_percentage), then
4443 4543 // hand off to `yatra_calculate_amount_due` so Pro can apply absolute
4444 4544 // overrides too (e.g. trip.deposit_amount as a fixed cap). Doing both
4445 4545 // keeps the math consistent with CalculationService::calculatePaymentAmounts().
4446 - $context = ['trip_id' => $trip_id];
4546 + // Tour start → Pro can force full payment when the tour is within the
4547 + // balance-due window (tour-anchored scheduled payments).
4548 + $context = ['trip_id' => $trip_id, 'travel_date' => (string) ($travel_date ?? '')];
4447 4549 $flexible_payments_enabled = apply_filters('yatra_flexible_payments_enabled', false);
4448 4550 $deposit_percentage = (int) apply_filters('yatra_deposit_percentage', 20, $context);
4449 4551 $partial_percentage = (int) apply_filters('yatra_partial_payment_percentage', 30, $context);
4450 4552