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 +252 -30 3.0.8 → 3.0.16 View file →
@@ -144,8 +144,19 @@
144 144 'callback' => [$this, 'download_voucher'],
145 145 'permission_callback' => '__return_true', // Auth checked inside callback
146 146 ],
147 147 ]);
148 +
149 + // Download a pro-forma invoice for a booking that has no payment yet
150 + // (offline gateways, e.g. Bank Transfer). Includes payment instructions
151 + // so the customer knows how to pay. Auth checked inside the callback.
152 + register_rest_route($namespace, '/booking/(?P<booking_id>[\d]+)/invoice', [
153 + [
154 + 'methods' => \WP_REST_Server::READABLE,
155 + 'callback' => [$this, 'download_booking_invoice'],
156 + 'permission_callback' => '__return_true',
157 + ],
158 + ]);
148 159 }
149 160
150 161 /**
151 162 * Payment gateway config — critical-sensitivity cap. By default
@@ -470,9 +481,12 @@
470 481 $paymentData['return_url'] = add_query_arg('payment', 'success', $this->getConfirmationUrl($reference));
471 482 }
472 483
473 484 $cancelParam = esc_url_raw($request->get_param('cancel_url'));
474 - $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));
475 489
476 490 if ($paymentData['amount'] <= 0) {
477 491 return new WP_Error('invalid_amount', __('Invalid payment amount', 'yatra'), ['status' => 400]);
478 492 }
@@ -528,8 +542,22 @@
528 542 if (!$gateway) {
529 543 return new WP_Error('invalid_gateway', __('Gateway not found', 'yatra'), ['status' => 404]);
530 544 }
531 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 +
532 560 if ($bookingId <= 0 || $transactionId === '') {
533 561 return new WP_Error('invalid_request', __('booking_id and transaction_id are required.', 'yatra'), ['status' => 400]);
534 562 }
535 563
@@ -635,10 +663,19 @@
635 663 wp_redirect(home_url('/booking-failed/'));
636 664 exit;
637 665 }
638 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 +
639 676 // Get transaction ID from request (varies by gateway)
640 - $transactionId = $request->get_param('refId')
677 + $transactionId = $request->get_param('refId')
641 678 ?? $request->get_param('pidx')
642 679 ?? $request->get_param('transaction_id')
643 680 ?? '';
644 681
@@ -775,17 +812,27 @@
775 812 }
776 813
777 814 $previousBookingStatus = (string) ($booking->status ?? 'pending');
778 815
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.
819 + $should_confirm = \yatra_should_confirm_booking_on_payment($new_amount_due <= 0, $bookingId);
820 +
779 821 // Update booking
780 - $this->bookingRepository->update($bookingId, [
822 + $booking_update = [
781 823 'amount_paid' => $new_amount_paid,
782 824 'amount_due' => $new_amount_due,
783 825 'payment_status' => $payment_status,
784 - 'status' => 'confirmed',
785 - ]);
826 + ];
827 + if ($should_confirm) {
828 + $booking_update['status'] = 'confirmed';
829 + }
830 + $this->bookingRepository->update($bookingId, $booking_update);
786 831
787 - \yatra_trigger_booking_confirmed($bookingId, $previousBookingStatus);
832 + if ($should_confirm) {
833 + \yatra_trigger_booking_confirmed($bookingId, $previousBookingStatus, true);
834 + }
788 835
789 836 // Clear remaining payment session if this was a remaining payment
790 837 if (function_exists('yatra_has_remaining_session') && yatra_has_remaining_session()) {
791 838 yatra_clear_remaining_session();
@@ -821,9 +868,11 @@
821 868 return new WP_Error('payment_not_found', __('Payment not found.', 'yatra'), ['status' => 404]);
822 869 }
823 870
824 871 // Authorisation:
825 - // 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.
826 875 // 2. Logged-in owner of the booking can access.
827 876 // 3. Anyone with a valid signed `invoice_token` (HMAC) can access — used on the
828 877 // booking-confirmation page so guest checkouts and post-session views work.
829 878 // 4. Legacy guest path: `booking_token` (active checkout transient) — kept for BC.
@@ -829,9 +878,9 @@
829 878 // 4. Legacy guest path: `booking_token` (active checkout transient) — kept for BC.
830 879 $currentUserId = (int) get_current_user_id();
831 880 $bookingUserId = (int) ($payment->booking_user_id ?? $payment->user_id ?? 0);
832 881 $paymentBookingId = (int) ($payment->booking_id ?? 0);
833 - $isAdmin = current_user_can('manage_options');
882 + $isAdmin = current_user_can('manage_options') || current_user_can('yatra_view_bookings');
834 883 $authorised = false;
835 884
836 885 if ($isAdmin) {
837 886 $authorised = true;
@@ -902,9 +951,11 @@
902 951 $tax_amount += (float) ($tax['amount'] ?? 0);
903 952 $tax_breakdown[] = [
904 953 'name' => $tax['name'] ?? 'Tax',
905 954 'rate' => $tax['rate'] ?? 0,
906 - '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)
907 958 ];
908 959 }
909 960 // Adjust subtotal for tax-exclusive pricing
910 961 if (!empty($payment->tax_inclusive) && $payment->tax_inclusive) {
@@ -915,9 +966,9 @@
915 966 $tax_amount = (float) $payment->tax_amount;
916 967 $tax_breakdown[] = [
917 968 'name' => __('Tax', 'yatra'),
918 969 'rate' => (float) ($payment->tax_rate ?? 0),
919 - 'amount' => $tax_amount
970 + 'amount' => yatra_format_price((float) $tax_amount, $currency, false)
920 971 ];
921 972 // Adjust subtotal for tax-exclusive pricing
922 973 if (!empty($payment->tax_inclusive) && $payment->tax_inclusive) {
923 974 $subtotal = (float) ($payment->subtotal ?? $subtotal);
@@ -928,28 +979,42 @@
928 979
929 980 $templateData = [
930 981 'company_name' => $companyName,
931 982 'company_address' => $companyAddress,
983 + 'company_address_lines' => \Yatra\Helpers\FormatHelper::companyAddressLines(),
932 984 'company_email' => $companyEmail,
933 985 'company_phone' => $companyPhone,
934 986 'customer_name' => trim(($payment->contact_first_name ?? '') . ' ' . ($payment->contact_last_name ?? '')) ?: ($payment->customer_name ?? __('Customer', 'yatra')),
935 987 'customer_email' => $payment->contact_email ?? $payment->customer_email ?? '',
936 - 'payment_ref' => $payment->reference ?? '',
988 + 'customer_address_lines' => FormatHelper::customerAddressLines($payment),
989 + // The booking_payments table has no `reference` column — the payment
990 + // reference is a derived value. Mirror PaymentService::formatPayment
991 + // (`PAY-%06d`, the same string the React account page shows) so the
992 + // invoice's "Invoice #" is populated and consistent, instead of blank.
993 + // A real stored reference (if a future join ever provides one) still wins.
994 + 'payment_ref' => (isset($payment->reference) && (string) $payment->reference !== '')
995 + ? (string) $payment->reference
996 + : sprintf('PAY-%06d', (int) ($payment->id ?? 0)),
937 997 'payment_date' => $paymentDate,
938 998 'payment_status' => ucfirst($payment->status ?? 'paid'),
939 999 'status_class' => in_array(strtolower((string) ($payment->status ?? '')), ['paid', 'completed', 'success'], true) ? 'paid' : 'pending',
940 1000 'trip_title' => $trip->title ?? $payment->trip_title ?? __('Trip Booking', 'yatra'),
941 - 'payment_method' => ucfirst($payment->gateway ?? $payment->payment_method ?? 'Online'),
942 - '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 ?? ''),
943 1008 'travel_date' => $travelDate,
944 1009 'currency_symbol' => $currencySymbol,
945 - 'amount' => number_format((float) ($payment->amount ?? 0), 2),
946 - 'booking_total' => number_format((float) ($payment->booking_total_amount ?? $payment->amount ?? 0), 2),
947 - 'amount_paid' => number_format((float) ($payment->booking_amount_paid ?? $payment->amount ?? 0), 2),
948 - '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),
949 1014 'tax_breakdown' => $tax_breakdown,
950 - 'tax_amount' => number_format($tax_amount, 2),
951 - '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),
952 1017 ];
953 1018
954 1019 $pdfBinary = $pdfService->renderTemplateToPdfSafely('pdf/invoice.php', $templateData, [
955 1020 'paper' => 'A4',
@@ -971,8 +1036,144 @@
971 1036 }
972 1037 }
973 1038
974 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 + /**
1055 + * Download a PRO-FORMA invoice for a booking that has no payment yet
1056 + * (offline gateways such as Bank Transfer). Shows the amount due and any
1057 + * gateway-supplied payment instructions (via yatra_invoice_payment_instructions)
1058 + * so the customer knows how to pay. Renders the same pdf/invoice.php template.
1059 + */
1060 + public function download_booking_invoice(WP_REST_Request $request)
1061 + {
1062 + $bookingId = (int) $request->get_param('booking_id');
1063 + $isPreview = $request->get_param('preview') === '1';
1064 + $bookingToken = sanitize_text_field((string) ($request->get_param('booking_token') ?? ''));
1065 + $invoiceToken = sanitize_text_field((string) ($request->get_param('invoice_token') ?? ''));
1066 +
1067 + if ($bookingId <= 0) {
1068 + return new WP_Error('invalid_booking', __('Invalid booking ID.', 'yatra'), ['status' => 400]);
1069 + }
1070 +
1071 + $bookingRepository = new \Yatra\Repositories\BookingRepository();
1072 + $booking = $bookingRepository->find($bookingId);
1073 + if (!$booking) {
1074 + return new WP_Error('booking_not_found', __('Booking not found.', 'yatra'), ['status' => 404]);
1075 + }
1076 +
1077 + // Authorisation mirrors download_invoice: staff -> owner -> signed
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.
1082 + $currentUserId = (int) get_current_user_id();
1083 + $bookingUserId = (int) ($booking->user_id ?? 0);
1084 + $authorised = false;
1085 + if (current_user_can('manage_options') || current_user_can('yatra_view_bookings')) {
1086 + $authorised = true;
1087 + } elseif ($currentUserId && $bookingUserId && $currentUserId === $bookingUserId) {
1088 + $authorised = true;
1089 + } elseif ($invoiceToken !== '' && self::verifyInvoiceToken($invoiceToken, 0, $bookingId)) {
1090 + $authorised = true;
1091 + } elseif ($bookingToken !== '' && (bool) SettingsService::get('allow_guest_checkout', true)) {
1092 + $session = get_transient($bookingToken);
1093 + if (is_array($session) && (int) ($session['booking_id'] ?? 0) === $bookingId) {
1094 + $authorised = true;
1095 + }
1096 + }
1097 + if (!$authorised) {
1098 + return $currentUserId
1099 + ? new WP_Error('forbidden', __('You do not have permission to access this invoice.', 'yatra'), ['status' => 403])
1100 + : new WP_Error('unauthorized', __('You must be logged in to download invoices.', 'yatra'), ['status' => 401]);
1101 + }
1102 +
1103 + $pdfService = new PdfService();
1104 + if (!$pdfService->isAvailable()) {
1105 + return new WP_Error('pdf_engine_missing', __('Invoice PDF generator is not installed. Please run composer install to install dompdf/dompdf.', 'yatra'), ['status' => 500]);
1106 + }
1107 +
1108 + $trip = !empty($booking->trip_id) ? $this->tripRepository->find((int) $booking->trip_id) : null;
1109 +
1110 + $currency = SettingsService::getCurrency();
1111 + $currencySymbol = FormatHelper::getCurrencySymbol($currency);
1112 + $bookingRef = (string) ($booking->reference ?? $booking->booking_number ?? (string) $bookingId);
1113 + $filename = 'Invoice #' . $bookingRef . '.pdf';
1114 + $travelDate = !empty($booking->travel_date) ? date_i18n(get_option('date_format'), strtotime((string) $booking->travel_date)) : '';
1115 +
1116 + $total = (float) ($booking->total_amount ?? 0);
1117 + $paid = (float) ($booking->amount_paid ?? 0);
1118 + $due = (float) ($booking->amount_due ?? max(0.0, $total - $paid));
1119 +
1120 + // Gateway-supplied payment instructions (Bank Transfer fills this in Pro).
1121 + $paymentInstructions = apply_filters('yatra_invoice_payment_instructions', [], $booking);
1122 +
1123 + $templateData = [
1124 + 'company_name' => SettingsService::get('company_name', get_bloginfo('name')),
1125 + 'company_address' => SettingsService::get('company_address', ''),
1126 + 'company_address_lines' => \Yatra\Helpers\FormatHelper::companyAddressLines(),
1127 + 'company_email' => SettingsService::get('company_email', get_option('admin_email')),
1128 + 'company_phone' => SettingsService::get('company_phone', ''),
1129 + 'customer_name' => trim(($booking->contact_first_name ?? '') . ' ' . ($booking->contact_last_name ?? '')) ?: __('Customer', 'yatra'),
1130 + 'customer_email' => $booking->contact_email ?? '',
1131 + 'customer_address_lines' => FormatHelper::customerAddressLines($booking),
1132 + 'payment_ref' => $bookingRef,
1133 + 'payment_date' => !empty($booking->created_at) ? date_i18n(get_option('date_format'), strtotime((string) $booking->created_at)) : '',
1134 + // Reflect the booking's real payment state rather than a fixed
1135 + // "Payment Pending" — a deposit-paid booking is Partially Paid.
1136 + 'payment_status' => $due <= 0.0
1137 + ? __('Paid', 'yatra')
1138 + : ($paid > 0.0 ? __('Partially Paid', 'yatra') : __('Payment Pending', 'yatra')),
1139 + 'status_class' => $due <= 0.0 ? 'paid' : ($paid > 0.0 ? 'partial' : 'pending'),
1140 + 'trip_title' => $trip->title ?? $booking->trip_title ?? __('Trip Booking', 'yatra'),
1141 + 'payment_method' => $this->gatewayLabel(
1142 + $booking->payment_gateway ?? null,
1143 + __('Offline', 'yatra')
1144 + ),
1145 + 'booking_ref' => $bookingRef,
1146 + 'travel_date' => $travelDate,
1147 + 'currency_symbol' => $currencySymbol,
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),
1152 + 'tax_breakdown' => [],
1153 + 'tax_amount' => yatra_format_price(0.0, $currency, false),
1154 + 'subtotal' => yatra_format_price((float) $total, $currency, false),
1155 + 'payment_instructions' => is_array($paymentInstructions) ? $paymentInstructions : [],
1156 + ];
1157 +
1158 + $pdfBinary = $pdfService->renderTemplateToPdfSafely('pdf/invoice.php', $templateData, [
1159 + 'paper' => 'A4',
1160 + 'orientation' => 'portrait',
1161 + 'default_font' => 'DejaVu Sans',
1162 + ]);
1163 +
1164 + if ($isPreview) {
1165 + return new WP_REST_Response([
1166 + 'success' => true,
1167 + 'pdf_data' => base64_encode($pdfBinary),
1168 + 'filename' => $filename,
1169 + ]);
1170 + }
1171 + $pdfService->outputPdfDownload($pdfBinary, $filename);
1172 + exit;
1173 + }
1174 +
1175 + /**
975 1176 * Download travel voucher PDF for a booking
976 1177 */
977 1178 public function download_voucher(WP_REST_Request $request)
978 1179 {
@@ -1022,14 +1223,27 @@
1022 1223 // Format dates
1023 1224 $bookingDate = !empty($payment->created_at) ? date_i18n(get_option('date_format'), strtotime($payment->created_at)) : '';
1024 1225 $travelDate = !empty($payment->travel_date) ? date_i18n(get_option('date_format'), strtotime($payment->travel_date)) : '';
1025 1226
1026 - // Calculate return date if duration is available (duration_days column).
1227 + // Return date. Prefer the booking's STORED end_date — that is the actual
1228 + // booked return (it already accounts for a flexible window or a trip
1229 + // duration that changed after the booking was made). Only when no end is
1230 + // stored do we derive it from the trip duration: duration_days is
1231 + // INCLUSIVE, so the offset is (days - 1) — matching
1232 + // BookingRepository::calculateEndDate. A bare "+ duration_days" was one
1233 + // day too far (see ItineraryPdfBuilder).
1027 1234 $returnDate = '';
1028 - $durationDaysForReturn = (int) ($payment->trip_duration_days ?? ($trip->duration_days ?? 0));
1029 - if (!empty($payment->travel_date) && $durationDaysForReturn > 0) {
1030 - $returnTimestamp = strtotime($payment->travel_date . ' +' . $durationDaysForReturn . ' days');
1031 - $returnDate = date_i18n(get_option('date_format'), $returnTimestamp);
1235 + $storedEnd = isset($payment->booking_end_date) ? (string) $payment->booking_end_date : '';
1236 + $travelStart = (string) ($payment->travel_date ?? '');
1237 + if ($storedEnd !== '' && ($travelStart === '' || $storedEnd >= $travelStart)) {
1238 + $returnDate = date_i18n(get_option('date_format'), strtotime($storedEnd));
1239 + } else {
1240 + $durationDaysForReturn = (int) ($payment->trip_duration_days ?? ($trip->duration_days ?? 0));
1241 + if (!empty($payment->travel_date) && $durationDaysForReturn > 0) {
1242 + $returnOffset = max(0, $durationDaysForReturn - 1);
1243 + $returnTimestamp = strtotime($payment->travel_date . ' +' . $returnOffset . ' days');
1244 + $returnDate = date_i18n(get_option('date_format'), $returnTimestamp);
1245 + }
1032 1246 }
1033 1247
1034 1248 $bookingRef = (string) ($payment->booking_reference ?? $payment->booking_number ?? $payment->reference ?? (string) $paymentId);
1035 1249 $filename = 'Travel Voucher #' . $bookingRef . '.pdf';
@@ -1045,12 +1259,14 @@
1045 1259
1046 1260 $templateData = [
1047 1261 'company_name' => $companyName,
1048 1262 'company_address' => $companyAddress,
1263 + 'company_address_lines' => \Yatra\Helpers\FormatHelper::companyAddressLines(),
1049 1264 'company_email' => $companyEmail,
1050 1265 'company_phone' => $companyPhone,
1051 1266 'customer_name' => trim(($payment->contact_first_name ?? '') . ' ' . ($payment->contact_last_name ?? '')) ?: ($payment->customer_name ?? __('Customer', 'yatra')),
1052 1267 'customer_email' => $payment->contact_email ?? $payment->customer_email ?? '',
1268 + 'customer_address_lines' => FormatHelper::customerAddressLines($payment),
1053 1269 'booking_ref' => $bookingRef,
1054 1270 'booking_date' => $bookingDate,
1055 1271 'booking_status' => ucfirst($payment->status ?? 'confirmed'),
1056 1272 'status_class' => in_array(strtolower((string) ($payment->status ?? '')), ['confirmed', 'completed', 'success'], true) ? 'confirmed' :
@@ -1062,9 +1278,12 @@
1062 1278 'trip_duration' => yatra_format_duration(
1063 1279 (int) ($payment->trip_duration_days ?? ($trip->duration_days ?? 0)),
1064 1280 isset($payment->trip_duration_nights)
1065 1281 ? (int) $payment->trip_duration_nights
1066 - : (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)
1067 1286 ),
1068 1287 'trip_difficulty' => $trip ? ($trip->difficulty_name ?? '') : '',
1069 1288 'departure_location' => $trip ? ($trip->departure_location ?? '') : '',
1070 1289 'destination' => $trip ? ($trip->destination ?? $payment->destination ?? '') : ($payment->destination ?? ''),
@@ -1070,11 +1289,11 @@
1070 1289 'destination' => $trip ? ($trip->destination ?? $payment->destination ?? '') : ($payment->destination ?? ''),
1071 1290 'travel_date' => $travelDate,
1072 1291 'return_date' => $returnDate,
1073 1292 'currency_symbol' => $currencySymbol,
1074 - 'total_amount' => number_format((float) ($payment->booking_total_amount ?? $payment->amount ?? 0), 2),
1075 - 'amount_paid' => number_format((float) ($payment->booking_amount_paid ?? $payment->amount ?? 0), 2),
1076 - '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),
1077 1296 'traveler_count' => (int) ($payment->traveler_count ?? 1),
1078 1297 ];
1079 1298
1080 1299 $pdfBinary = $pdfService->renderTemplateToPdfSafely('pdf/voucher.php', $templateData, [
@@ -1192,9 +1411,11 @@
1192 1411 * download their invoice without a session.
1193 1412 */
1194 1413 public static function issueInvoiceToken(int $paymentId, int $bookingId): string
1195 1414 {
1196 - if ($paymentId <= 0 || $bookingId <= 0) {
1415 + // $paymentId === 0 denotes a booking-scoped (pro-forma) invoice token —
1416 + // used for offline/unpaid bookings that have no payment row yet.
1417 + if ($paymentId < 0 || $bookingId <= 0) {
1197 1418 return '';
1198 1419 }
1199 1420 $iat = time();
1200 1421 $hmac = hash_hmac(
@@ -1217,9 +1438,10 @@
1217 1438 * them now would break existing customer bookmarks.
1218 1439 */
1219 1440 public static function verifyInvoiceToken(string $token, int $paymentId, int $bookingId): bool
1220 1441 {
1221 - if ($token === '' || $paymentId <= 0 || $bookingId <= 0) {
1442 + // $paymentId === 0 = booking-scoped (pro-forma) token; see issueInvoiceToken().
1443 + if ($token === '' || $paymentId < 0 || $bookingId <= 0) {
1222 1444 return false;
1223 1445 }
1224 1446
1225 1447 // v2 path — token starts with the version prefix.