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 | app/Controllers/BookingSessionController.php +151 -26 3.0.9trunk 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,
@@ -1205,8 +1216,23 @@
1205 1216 global $wpdb;
1206 1217
1207 1218 $data = $request->get_json_params();
1208 1219
1220 + // reCAPTCHA v3 — no-op unless the booking form is explicitly protected in
1221 + // settings (off by default so payment flows are never gated unless the
1222 + // operator opts in).
1223 + $recaptcha = \Yatra\Services\RecaptchaService::verifyForm(
1224 + 'booking',
1225 + (string) (($data['recaptcha_token'] ?? '') ?: ''),
1226 + $_SERVER['REMOTE_ADDR'] ?? null
1227 + );
1228 + if (empty($recaptcha['success'])) {
1229 + return new WP_REST_Response([
1230 + 'success' => false,
1231 + 'message' => $recaptcha['message'] ?? __('reCAPTCHA verification failed.', 'yatra'),
1232 + ], 400);
1233 + }
1234 +
1209 1235 // ========================================
1210 1236 // CSRF — booking-scoped action nonce
1211 1237 // ========================================
1212 1238 // The public_permission_callback on this route intentionally
@@ -1264,9 +1290,9 @@
1264 1290 // GET BOOKING SETTINGS
1265 1291 // ========================================
1266 1292 $settings = [
1267 1293 'booking_confirmation' => \Yatra\Services\SettingsService::get('booking_confirmation', true),
1268 - 'auto_confirm_bookings' => \Yatra\Services\SettingsService::get('auto_confirm_bookings', false),
1294 + 'auto_confirm_mode' => \yatra_get_auto_confirm_mode(),
1269 1295 'require_login' => \Yatra\Services\SettingsService::get('require_login', false),
1270 1296 'allow_guest_checkout' => \Yatra\Services\SettingsService::get('allow_guest_checkout', true),
1271 1297 'booking_expiry_hours' => (int) \Yatra\Services\SettingsService::get('booking_expiry_hours', 24),
1272 1298 'auto_confirm_pay_later' => \Yatra\Services\SettingsService::get('auto_confirm_pay_later', true),
@@ -1349,9 +1375,13 @@
1349 1375 // Which booking-form sections are enabled (Pro Dynamic Form module).
1350 1376 // The default config has every section enabled, so on existing/un-customised
1351 1377 // sites $contact_enabled and $traveler_enabled are both true and the logic
1352 1378 // below behaves exactly as before — only disabled sections change anything.
1353 - $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 + : [];
1354 1384 $contact_enabled = !isset($form_config['contact_form']['enabled']) || (bool) $form_config['contact_form']['enabled'];
1355 1385 $traveler_enabled = !isset($form_config['traveler_form']['enabled']) || (bool) $form_config['traveler_form']['enabled'];
1356 1386
1357 1387 // Get contact email - handle both flat and nested formats
@@ -1712,9 +1742,9 @@
1712 1742 $isWaitlistCheckout = false;
1713 1743
1714 1744 if ($resolvedAvailabilityForWaitlist !== null) {
1715 1745 $availStatus = (string) ($resolvedAvailabilityForWaitlist->status ?? 'available');
1716 - if (in_array($availStatus, ['blocked', 'closed', 'cancelled'], true)) {
1746 + if (in_array($availStatus, ['blocked', 'closed', 'cancelled', 'unavailable'], true)) {
1717 1747 return new WP_REST_Response([
1718 1748 'success' => false,
1719 1749 'message' => __('This departure is not open for booking.', 'yatra'),
1720 1750 'code' => 'date_blocked',
@@ -2414,21 +2444,29 @@
2414 2444
2415 2445 // ========================================
2416 2446 // DETERMINE BOOKING STATUS
2417 2447 // ========================================
2418 - // Priority:
2419 - // 1. auto_confirm_bookings setting (confirms ALL bookings automatically)
2420 - // 2. For pay_later: auto_confirm_pay_later setting
2421 - // 3. For bank_transfer: always pending until verified
2422 -
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 +
2423 2455 $booking_status = 'pending';
2424 2456 $status_message = __('Booking received!', 'yatra');
2425 -
2426 - // Check if auto-confirm all bookings is enabled
2427 - if ($settings['auto_confirm_bookings']) {
2428 - // 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.
2429 2461 $booking_status = 'confirmed';
2430 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');
2431 2469 } elseif ($payment_gateway === 'pay_later') {
2432 2470 // Pay Later: Check the specific pay_later auto-confirm setting
2433 2471 if ($settings['auto_confirm_pay_later']) {
2434 2472 $booking_status = 'confirmed';
@@ -2848,11 +2886,15 @@
2848 2886 // Default return_url to the configured booking confirmation URL so redirect gateways
2849 2887 // (e.g. PayPal Advanced, Mollie, Paystack) do not fall back to wrong paths; gateways
2850 2888 // may still append their own query args on top of this URL.
2851 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'] ?? '');
2852 2894 $paymentData = array_merge($params, [
2853 2895 'description' => $params['trip_title'] ?? '',
2854 - 'cancel_url' => home_url('/book/?payment=cancelled&ref=' . ($params['reference'] ?? '')),
2896 + 'cancel_url' => add_query_arg('payment', 'cancelled', $this->getConfirmationUrl($cancelRef)),
2855 2897 'metadata' => [
2856 2898 'booking_id' => $params['booking_id'],
2857 2899 'reference' => $params['reference'] ?? ''
2858 2900 ]
@@ -2901,10 +2943,12 @@
2901 2943 ];
2902 2944 }
2903 2945
2904 2946 // For offline gateways or successful direct payments without redirect
2947 + $this->recordOfflinePendingPayment($params, $result, $gatewayId);
2948 +
2905 2949 return [
2906 - 'success' => true,
2950 + 'success' => true,
2907 2951 'redirect_url' => $this->getConfirmationUrl($params['reference'] ?? '')
2908 2952 ];
2909 2953 }
2910 2954
@@ -2929,8 +2973,75 @@
2929 2973 /**
2930 2974 * Record payment from gateway result
2931 2975 * Matches Stripe's completePayment behavior
2932 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 +
2933 3044 private function recordGatewayPayment(array $params, array $result, string $gatewayId): void
2934 3045 {
2935 3046 global $wpdb;
2936 3047
@@ -2984,23 +3095,32 @@
2984 3095 // (Square, Authorize.Net) reach this generic path but previously left
2985 3096 // the booking at pending/pending — only the payment row was written.
2986 3097 // This now matches handle_successful_payment(): accumulate amount_paid,
2987 3098 // recompute amount_due, set payment_status (paid vs partial), and
2988 - // 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).
2989 3101 $newAmountPaid = (float) ($booking->amount_paid ?? 0) + $amount;
2990 3102 $newAmountDue = max(0.0, (float) ($booking->total_amount ?? 0) - $newAmountPaid);
2991 3103 $paymentStatus = $newAmountDue > 0.0 ? 'partial' : 'paid';
2992 3104 $previousStatus = (string) ($booking->status ?? 'pending');
2993 3105
2994 - $this->bookingRepository->update($bookingId, [
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.
3109 + $shouldConfirm = \yatra_should_confirm_booking_on_payment($newAmountDue <= 0.0, $bookingId);
3110 +
3111 + $bookingUpdate = [
2995 3112 'amount_paid' => $newAmountPaid,
2996 3113 'amount_due' => $newAmountDue,
2997 3114 'payment_status' => $paymentStatus,
2998 - 'status' => 'confirmed',
2999 - ]);
3115 + ];
3116 + if ($shouldConfirm) {
3117 + $bookingUpdate['status'] = 'confirmed';
3118 + }
3119 + $this->bookingRepository->update($bookingId, $bookingUpdate);
3000 3120
3001 - if (function_exists('yatra_trigger_booking_confirmed')) {
3002 - \yatra_trigger_booking_confirmed($bookingId, $previousStatus);
3121 + if ($shouldConfirm && function_exists('yatra_trigger_booking_confirmed')) {
3122 + \yatra_trigger_booking_confirmed($bookingId, $previousStatus, true);
3003 3123 }
3004 3124
3005 3125 // Fire payment completed action
3006 3126 do_action('yatra_payment_completed', [
@@ -3068,9 +3188,9 @@
3068 3188 'description' => $params['trip_title'],
3069 3189 ]],
3070 3190 'application_context' => [
3071 3191 'return_url' => add_query_arg('payment', 'success', $this->getConfirmationUrl($params['reference'])),
3072 - 'cancel_url' => home_url('/book/?payment=cancelled&ref=' . $params['reference']),
3192 + 'cancel_url' => add_query_arg('payment', 'cancelled', $this->getConfirmationUrl($params['reference'])),
3073 3193 ],
3074 3194 ]),
3075 3195 ]);
3076 3196
@@ -3189,9 +3309,12 @@
3189 3309 'su' => add_query_arg(
3190 3310 ['payment' => 'success', 'gateway' => 'esewa'],
3191 3311 $this->getConfirmationUrl($params['reference'])
3192 3312 ),
3193 - '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 + ),
3194 3317 ], $base_url);
3195 3318
3196 3319 return ['success' => true, 'payment_url' => $payment_url];
3197 3320 }
@@ -3343,9 +3466,9 @@
3343 3466 <p style="margin:0 0 8px;"><strong><?php esc_html_e('Booking reference', 'yatra'); ?>:</strong> <?php echo esc_html($reference); ?></p>
3344 3467 <p style="margin:0 0 8px;"><strong><?php esc_html_e('Trip', 'yatra'); ?>:</strong> <?php echo esc_html($trip->title); ?></p>
3345 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>
3346 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. */
3347 -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>
3348 3471 <p style="margin:0;"><strong><?php esc_html_e('Travelers', 'yatra'); ?>:</strong> <?php echo esc_html((string) count($travelers)); ?></p>
3349 3472 </div>
3350 3473 <h3 style="font-size:16px;"><?php esc_html_e('Payment details', 'yatra'); ?></h3>
3351 3474 <p><?php /* translators: %s: total amount (formatted). */
@@ -4419,9 +4542,11 @@
4419 4542 // Pro can already override per-trip via trip.deposit_percentage), then
4420 4543 // hand off to `yatra_calculate_amount_due` so Pro can apply absolute
4421 4544 // overrides too (e.g. trip.deposit_amount as a fixed cap). Doing both
4422 4545 // keeps the math consistent with CalculationService::calculatePaymentAmounts().
4423 - $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 ?? '')];
4424 4549 $flexible_payments_enabled = apply_filters('yatra_flexible_payments_enabled', false);
4425 4550 $deposit_percentage = (int) apply_filters('yatra_deposit_percentage', 20, $context);
4426 4551 $partial_percentage = (int) apply_filters('yatra_partial_payment_percentage', 30, $context);
4427 4552