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/PaymentGatewayController.php +96 -31 3.0.10 → 3.0.16 View file →
@@ -481,9 +481,12 @@
481 481 $paymentData['return_url'] = add_query_arg('payment', 'success', $this->getConfirmationUrl($reference));
482 482 }
483 483
484 484 $cancelParam = esc_url_raw($request->get_param('cancel_url'));
485 - $paymentData['cancel_url'] = $cancelParam ?: home_url('/book/?payment=cancelled&ref=' . ($paymentData['reference'] ?? $paymentData['booking_id']));
485 + // Fall back to the booking-confirmation page (always a resolvable route) rather
486 + // than `home_url('/book/?...')`, which 404s under a custom booking base/page.
487 + $cancelReference = (string) ($paymentData['reference'] ?? $paymentData['booking_id']);
488 + $paymentData['cancel_url'] = $cancelParam ?: add_query_arg('payment', 'cancelled', $this->getConfirmationUrl($cancelReference));
486 489
487 490 if ($paymentData['amount'] <= 0) {
488 491 return new WP_Error('invalid_amount', __('Invalid payment amount', 'yatra'), ['status' => 400]);
489 492 }
@@ -539,8 +542,22 @@
539 542 if (!$gateway) {
540 543 return new WP_Error('invalid_gateway', __('Gateway not found', 'yatra'), ['status' => 404]);
541 544 }
542 545
546 + // Offline gateways (Bank Transfer, Pay Later) settle out of band and take
547 + // no money at checkout. They must NEVER be auto-completed through this
548 + // endpoint — doing so marks the booking paid + records a "completed"
549 + // payment before any funds have arrived. Confirmation is a manual admin
550 + // action once the operator sees the money. Guard here as well as in the
551 + // gateways' verifyPayment() so a future offline gateway can't regress.
552 + if ($gateway->isOffline()) {
553 + return new WP_Error(
554 + 'offline_gateway_manual',
555 + __('This payment method is settled manually and cannot be confirmed automatically.', 'yatra'),
556 + ['status' => 400]
557 + );
558 + }
559 +
543 560 if ($bookingId <= 0 || $transactionId === '') {
544 561 return new WP_Error('invalid_request', __('booking_id and transaction_id are required.', 'yatra'), ['status' => 400]);
545 562 }
546 563
@@ -646,10 +663,19 @@
646 663 wp_redirect(home_url('/booking-failed/'));
647 664 exit;
648 665 }
649 666
667 + // Offline gateways never redirect here, and must never be auto-completed:
668 + // they settle out of band and are confirmed manually by the operator.
669 + // Bounce a spoofed `?status=success` callback to the confirmation page
670 + // (still pending) rather than recording a payment that never happened.
671 + if ($gateway->isOffline()) {
672 + wp_redirect(yatra_get_booking_confirmation_url($bookingId > 0 ? (string) $bookingId : ''));
673 + exit;
674 + }
675 +
650 676 // Get transaction ID from request (varies by gateway)
651 - $transactionId = $request->get_param('refId')
677 + $transactionId = $request->get_param('refId')
652 678 ?? $request->get_param('pidx')
653 679 ?? $request->get_param('transaction_id')
654 680 ?? '';
655 681
@@ -786,11 +812,11 @@
786 812 }
787 813
788 814 $previousBookingStatus = (string) ($booking->status ?? 'pending');
789 815
790 - // Only auto-confirm when the operator allows it (or the booking is now
791 - // fully paid). A deposit / partial payment leaves the booking pending
792 - // when "Auto-Confirm Bookings" is off, for the operator to confirm.
816 + // Only auto-confirm when "Auto-Confirm Bookings" is on; otherwise the
817 + // booking stays pending for the operator to confirm manually, regardless
818 + // of a successful (full or partial) payment.
793 819 $should_confirm = \yatra_should_confirm_booking_on_payment($new_amount_due <= 0, $bookingId);
794 820
795 821 // Update booking
796 822 $booking_update = [
@@ -803,9 +829,9 @@
803 829 }
804 830 $this->bookingRepository->update($bookingId, $booking_update);
805 831
806 832 if ($should_confirm) {
807 - \yatra_trigger_booking_confirmed($bookingId, $previousBookingStatus);
833 + \yatra_trigger_booking_confirmed($bookingId, $previousBookingStatus, true);
808 834 }
809 835
810 836 // Clear remaining payment session if this was a remaining payment
811 837 if (function_exists('yatra_has_remaining_session') && yatra_has_remaining_session()) {
@@ -842,9 +868,11 @@
842 868 return new WP_Error('payment_not_found', __('Payment not found.', 'yatra'), ['status' => 404]);
843 869 }
844 870
845 871 // Authorisation:
846 - // 1. Administrators can always access (no further checks).
872 + // 1. Staff can always access (no further checks): a WP administrator,
873 + // or a user holding Yatra's yatra_view_bookings capability, which is
874 + // the same audience that already sees every booking in the list.
847 875 // 2. Logged-in owner of the booking can access.
848 876 // 3. Anyone with a valid signed `invoice_token` (HMAC) can access — used on the
849 877 // booking-confirmation page so guest checkouts and post-session views work.
850 878 // 4. Legacy guest path: `booking_token` (active checkout transient) — kept for BC.
@@ -850,9 +878,9 @@
850 878 // 4. Legacy guest path: `booking_token` (active checkout transient) — kept for BC.
851 879 $currentUserId = (int) get_current_user_id();
852 880 $bookingUserId = (int) ($payment->booking_user_id ?? $payment->user_id ?? 0);
853 881 $paymentBookingId = (int) ($payment->booking_id ?? 0);
854 - $isAdmin = current_user_can('manage_options');
882 + $isAdmin = current_user_can('manage_options') || current_user_can('yatra_view_bookings');
855 883 $authorised = false;
856 884
857 885 if ($isAdmin) {
858 886 $authorised = true;
@@ -923,9 +951,11 @@
923 951 $tax_amount += (float) ($tax['amount'] ?? 0);
924 952 $tax_breakdown[] = [
925 953 'name' => $tax['name'] ?? 'Tax',
926 954 'rate' => $tax['rate'] ?? 0,
927 - 'amount' => $tax['amount'] ?? 0
955 + // Pre-formatted like every other invoice figure, so the tax
956 + // rows honour the configured separators and symbol position.
957 + 'amount' => yatra_format_price((float) ($tax['amount'] ?? 0), $currency, false)
928 958 ];
929 959 }
930 960 // Adjust subtotal for tax-exclusive pricing
931 961 if (!empty($payment->tax_inclusive) && $payment->tax_inclusive) {
@@ -936,9 +966,9 @@
936 966 $tax_amount = (float) $payment->tax_amount;
937 967 $tax_breakdown[] = [
938 968 'name' => __('Tax', 'yatra'),
939 969 'rate' => (float) ($payment->tax_rate ?? 0),
940 - 'amount' => $tax_amount
970 + 'amount' => yatra_format_price((float) $tax_amount, $currency, false)
941 971 ];
942 972 // Adjust subtotal for tax-exclusive pricing
943 973 if (!empty($payment->tax_inclusive) && $payment->tax_inclusive) {
944 974 $subtotal = (float) ($payment->subtotal ?? $subtotal);
@@ -949,12 +979,14 @@
949 979
950 980 $templateData = [
951 981 'company_name' => $companyName,
952 982 'company_address' => $companyAddress,
983 + 'company_address_lines' => \Yatra\Helpers\FormatHelper::companyAddressLines(),
953 984 'company_email' => $companyEmail,
954 985 'company_phone' => $companyPhone,
955 986 'customer_name' => trim(($payment->contact_first_name ?? '') . ' ' . ($payment->contact_last_name ?? '')) ?: ($payment->customer_name ?? __('Customer', 'yatra')),
956 987 'customer_email' => $payment->contact_email ?? $payment->customer_email ?? '',
988 + 'customer_address_lines' => FormatHelper::customerAddressLines($payment),
957 989 // The booking_payments table has no `reference` column — the payment
958 990 // reference is a derived value. Mirror PaymentService::formatPayment
959 991 // (`PAY-%06d`, the same string the React account page shows) so the
960 992 // invoice's "Invoice #" is populated and consistent, instead of blank.
@@ -965,19 +997,24 @@
965 997 'payment_date' => $paymentDate,
966 998 'payment_status' => ucfirst($payment->status ?? 'paid'),
967 999 'status_class' => in_array(strtolower((string) ($payment->status ?? '')), ['paid', 'completed', 'success'], true) ? 'paid' : 'pending',
968 1000 'trip_title' => $trip->title ?? $payment->trip_title ?? __('Trip Booking', 'yatra'),
969 - 'payment_method' => ucfirst($payment->gateway ?? $payment->payment_method ?? 'Online'),
970 - 'booking_ref' => $payment->booking_reference ?? $payment->booking_number ?? '',
1001 + 'payment_method' => $this->gatewayLabel(
1002 + $payment->gateway ?? $payment->payment_method ?? null,
1003 + __('Online', 'yatra')
1004 + ),
1005 + // Booking-only fallback chain (never a payment identifier) so the
1006 + // invoice number always resolves to the booking reference.
1007 + 'booking_ref' => $payment->booking_reference ?? $payment->booking_number ?? (string) ($payment->booking_id ?? ''),
971 1008 'travel_date' => $travelDate,
972 1009 'currency_symbol' => $currencySymbol,
973 - 'amount' => number_format((float) ($payment->amount ?? 0), 2),
974 - 'booking_total' => number_format((float) ($payment->booking_total_amount ?? $payment->amount ?? 0), 2),
975 - 'amount_paid' => number_format((float) ($payment->booking_amount_paid ?? $payment->amount ?? 0), 2),
976 - 'amount_due' => number_format((float) ($payment->booking_amount_due ?? 0), 2),
1010 + 'amount' => yatra_format_price((float) ($payment->amount ?? 0), $currency, false),
1011 + 'booking_total' => yatra_format_price((float) ($payment->booking_total_amount ?? $payment->amount ?? 0), $currency, false),
1012 + 'amount_paid' => yatra_format_price((float) ($payment->booking_amount_paid ?? $payment->amount ?? 0), $currency, false),
1013 + 'amount_due' => yatra_format_price((float) ($payment->booking_amount_due ?? 0), $currency, false),
977 1014 'tax_breakdown' => $tax_breakdown,
978 - 'tax_amount' => number_format($tax_amount, 2),
979 - 'subtotal' => number_format($subtotal, 2),
1015 + 'tax_amount' => yatra_format_price((float) $tax_amount, $currency, false),
1016 + 'subtotal' => yatra_format_price((float) $subtotal, $currency, false),
980 1017 ];
981 1018
982 1019 $pdfBinary = $pdfService->renderTemplateToPdfSafely('pdf/invoice.php', $templateData, [
983 1020 'paper' => 'A4',
@@ -999,8 +1036,23 @@
999 1036 }
1000 1037 }
1001 1038
1002 1039 /**
1040 + * Customer-facing label for a gateway id on a document.
1041 + *
1042 + * Uses the same title the checkout shows (operator's custom title, else the
1043 + * gateway's translated one). Falls back to the prettified id — the previous
1044 + * behaviour — when the gateway is not registered any more, so an invoice for
1045 + * a payment taken through a since-removed gateway still reads sensibly.
1046 + */
1047 + private function gatewayLabel(?string $gatewayId, string $fallback = 'Online'): string
1048 + {
1049 + return function_exists('yatra_payment_gateway_label')
1050 + ? yatra_payment_gateway_label($gatewayId, $fallback)
1051 + : ($gatewayId ? ucwords(str_replace(['_', '-'], ' ', $gatewayId)) : $fallback);
1052 + }
1053 +
1054 + /**
1003 1055 * Download a PRO-FORMA invoice for a booking that has no payment yet
1004 1056 * (offline gateways such as Bank Transfer). Shows the amount due and any
1005 1057 * gateway-supplied payment instructions (via yatra_invoice_payment_instructions)
1006 1058 * so the customer knows how to pay. Renders the same pdf/invoice.php template.
@@ -1021,14 +1073,17 @@
1021 1073 if (!$booking) {
1022 1074 return new WP_Error('booking_not_found', __('Booking not found.', 'yatra'), ['status' => 404]);
1023 1075 }
1024 1076
1025 - // Authorisation mirrors download_invoice: admin -> owner -> signed
1077 + // Authorisation mirrors download_invoice: staff -> owner -> signed
1026 1078 // booking-scoped invoice_token (paymentId 0) -> guest booking_token.
1079 + // Staff means a WP admin OR a user holding Yatra's booking-view
1080 + // capability, so Pro Team roles (which deliberately don't carry
1081 + // manage_options) can use the admin "Download invoice" action.
1027 1082 $currentUserId = (int) get_current_user_id();
1028 1083 $bookingUserId = (int) ($booking->user_id ?? 0);
1029 1084 $authorised = false;
1030 - if (current_user_can('manage_options')) {
1085 + if (current_user_can('manage_options') || current_user_can('yatra_view_bookings')) {
1031 1086 $authorised = true;
1032 1087 } elseif ($currentUserId && $bookingUserId && $currentUserId === $bookingUserId) {
1033 1088 $authorised = true;
1034 1089 } elseif ($invoiceToken !== '' && self::verifyInvoiceToken($invoiceToken, 0, $bookingId)) {
@@ -1067,12 +1122,14 @@
1067 1122
1068 1123 $templateData = [
1069 1124 'company_name' => SettingsService::get('company_name', get_bloginfo('name')),
1070 1125 'company_address' => SettingsService::get('company_address', ''),
1126 + 'company_address_lines' => \Yatra\Helpers\FormatHelper::companyAddressLines(),
1071 1127 'company_email' => SettingsService::get('company_email', get_option('admin_email')),
1072 1128 'company_phone' => SettingsService::get('company_phone', ''),
1073 1129 'customer_name' => trim(($booking->contact_first_name ?? '') . ' ' . ($booking->contact_last_name ?? '')) ?: __('Customer', 'yatra'),
1074 1130 'customer_email' => $booking->contact_email ?? '',
1131 + 'customer_address_lines' => FormatHelper::customerAddressLines($booking),
1075 1132 'payment_ref' => $bookingRef,
1076 1133 'payment_date' => !empty($booking->created_at) ? date_i18n(get_option('date_format'), strtotime((string) $booking->created_at)) : '',
1077 1134 // Reflect the booking's real payment state rather than a fixed
1078 1135 // "Payment Pending" — a deposit-paid booking is Partially Paid.
@@ -1080,19 +1137,22 @@
1080 1137 ? __('Paid', 'yatra')
1081 1138 : ($paid > 0.0 ? __('Partially Paid', 'yatra') : __('Payment Pending', 'yatra')),
1082 1139 'status_class' => $due <= 0.0 ? 'paid' : ($paid > 0.0 ? 'partial' : 'pending'),
1083 1140 'trip_title' => $trip->title ?? $booking->trip_title ?? __('Trip Booking', 'yatra'),
1084 - 'payment_method' => ucwords(str_replace('_', ' ', (string) ($booking->payment_gateway ?? 'offline'))),
1141 + 'payment_method' => $this->gatewayLabel(
1142 + $booking->payment_gateway ?? null,
1143 + __('Offline', 'yatra')
1144 + ),
1085 1145 'booking_ref' => $bookingRef,
1086 1146 'travel_date' => $travelDate,
1087 1147 'currency_symbol' => $currencySymbol,
1088 - 'amount' => number_format($due, 2),
1089 - 'booking_total' => number_format($total, 2),
1090 - 'amount_paid' => number_format($paid, 2),
1091 - 'amount_due' => number_format($due, 2),
1148 + 'amount' => yatra_format_price((float) $due, $currency, false),
1149 + 'booking_total' => yatra_format_price((float) $total, $currency, false),
1150 + 'amount_paid' => yatra_format_price((float) $paid, $currency, false),
1151 + 'amount_due' => yatra_format_price((float) $due, $currency, false),
1092 1152 'tax_breakdown' => [],
1093 - 'tax_amount' => number_format(0, 2),
1094 - 'subtotal' => number_format($total, 2),
1153 + 'tax_amount' => yatra_format_price(0.0, $currency, false),
1154 + 'subtotal' => yatra_format_price((float) $total, $currency, false),
1095 1155 'payment_instructions' => is_array($paymentInstructions) ? $paymentInstructions : [],
1096 1156 ];
1097 1157
1098 1158 $pdfBinary = $pdfService->renderTemplateToPdfSafely('pdf/invoice.php', $templateData, [
@@ -1199,12 +1259,14 @@
1199 1259
1200 1260 $templateData = [
1201 1261 'company_name' => $companyName,
1202 1262 'company_address' => $companyAddress,
1263 + 'company_address_lines' => \Yatra\Helpers\FormatHelper::companyAddressLines(),
1203 1264 'company_email' => $companyEmail,
1204 1265 'company_phone' => $companyPhone,
1205 1266 'customer_name' => trim(($payment->contact_first_name ?? '') . ' ' . ($payment->contact_last_name ?? '')) ?: ($payment->customer_name ?? __('Customer', 'yatra')),
1206 1267 'customer_email' => $payment->contact_email ?? $payment->customer_email ?? '',
1268 + 'customer_address_lines' => FormatHelper::customerAddressLines($payment),
1207 1269 'booking_ref' => $bookingRef,
1208 1270 'booking_date' => $bookingDate,
1209 1271 'booking_status' => ucfirst($payment->status ?? 'confirmed'),
1210 1272 'status_class' => in_array(strtolower((string) ($payment->status ?? '')), ['confirmed', 'completed', 'success'], true) ? 'confirmed' :
@@ -1216,9 +1278,12 @@
1216 1278 'trip_duration' => yatra_format_duration(
1217 1279 (int) ($payment->trip_duration_days ?? ($trip->duration_days ?? 0)),
1218 1280 isset($payment->trip_duration_nights)
1219 1281 ? (int) $payment->trip_duration_nights
1220 - : (isset($trip->duration_nights) ? (int) $trip->duration_nights : null)
1282 + : (isset($trip->duration_nights) ? (int) $trip->duration_nights : null),
1283 + // Hour-based day tours: hours come from the loaded trip (the payment
1284 + // join carries only days/nights); a soft-deleted trip falls back to days.
1285 + (int) ($trip->duration_hours ?? 0)
1221 1286 ),
1222 1287 'trip_difficulty' => $trip ? ($trip->difficulty_name ?? '') : '',
1223 1288 'departure_location' => $trip ? ($trip->departure_location ?? '') : '',
1224 1289 'destination' => $trip ? ($trip->destination ?? $payment->destination ?? '') : ($payment->destination ?? ''),
@@ -1224,11 +1289,11 @@
1224 1289 'destination' => $trip ? ($trip->destination ?? $payment->destination ?? '') : ($payment->destination ?? ''),
1225 1290 'travel_date' => $travelDate,
1226 1291 'return_date' => $returnDate,
1227 1292 'currency_symbol' => $currencySymbol,
1228 - 'total_amount' => number_format((float) ($payment->booking_total_amount ?? $payment->amount ?? 0), 2),
1229 - 'amount_paid' => number_format((float) ($payment->booking_amount_paid ?? $payment->amount ?? 0), 2),
1230 - 'amount_due' => number_format((float) ($payment->booking_amount_due ?? 0), 2),
1293 + 'total_amount' => yatra_format_price((float) ($payment->booking_total_amount ?? $payment->amount ?? 0), $currency, false),
1294 + 'amount_paid' => yatra_format_price((float) ($payment->booking_amount_paid ?? $payment->amount ?? 0), $currency, false),
1295 + 'amount_due' => yatra_format_price((float) ($payment->booking_amount_due ?? 0), $currency, false),
1231 1296 'traveler_count' => (int) ($payment->traveler_count ?? 1),
1232 1297 ];
1233 1298
1234 1299 $pdfBinary = $pdfService->renderTemplateToPdfSafely('pdf/voucher.php', $templateData, [