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/BookingsController.php +121 -109 3.0.3 → 3.0.16 View file →
@@ -66,20 +66,20 @@
66 66 // =====================
67 67 // BOOKINGS ROUTES
68 68 // =====================
69 69
70 - // List bookings
70 + // List bookings — view cap.
71 71 register_rest_route($this->namespace, '/bookings', [
72 72 'methods' => 'GET',
73 73 'callback' => [$this, 'getBookings'],
74 - 'permission_callback' => [$this, 'checkAdminPermission'],
74 + 'permission_callback' => [$this, 'checkCanView'],
75 75 ]);
76 76
77 - // Get single booking
77 + // Get single booking — view cap.
78 78 register_rest_route($this->namespace, '/bookings/(?P<id>\d+)', [
79 79 'methods' => 'GET',
80 80 'callback' => [$this, 'getBooking'],
81 - 'permission_callback' => [$this, 'checkAdminPermission'],
81 + 'permission_callback' => [$this, 'checkCanView'],
82 82 'args' => [
83 83 'id' => [
84 84 'required' => true,
85 85 'type' => 'integer',
@@ -87,66 +87,75 @@
87 87 ],
88 88 ],
89 89 ]);
90 90
91 - // Create booking
91 + // Create booking — create cap.
92 92 register_rest_route($this->namespace, '/bookings', [
93 93 'methods' => 'POST',
94 94 'callback' => [$this, 'createBooking'],
95 - 'permission_callback' => [$this, 'checkAdminPermission'],
95 + 'permission_callback' => [$this, 'checkCanCreate'],
96 96 ]);
97 97
98 - // Update booking
98 + // Update booking — edit cap.
99 99 register_rest_route($this->namespace, '/bookings/(?P<id>\d+)', [
100 100 'methods' => 'PUT',
101 101 'callback' => [$this, 'updateBooking'],
102 - 'permission_callback' => [$this, 'checkAdminPermission'],
102 + 'permission_callback' => [$this, 'checkCanEdit'],
103 103 ]);
104 104
105 - // Delete booking
105 + // Delete booking — critical-sensitivity delete cap. Only
106 + // Owner role gets this by default.
106 107 register_rest_route($this->namespace, '/bookings/(?P<id>\d+)', [
107 108 'methods' => 'DELETE',
108 109 'callback' => [$this, 'deleteBooking'],
109 - 'permission_callback' => [$this, 'checkAdminPermission'],
110 + 'permission_callback' => [$this, 'checkCanDelete'],
110 111 ]);
111 112
112 - // Update booking status
113 + // Update booking status — dedicated change-status cap so
114 + // Front Desk (who has this cap but NOT edit) can flip
115 + // confirmed → checked-in without being able to mutate other
116 + // fields.
113 117 register_rest_route($this->namespace, '/bookings/(?P<id>\d+)/status', [
114 118 'methods' => 'PUT',
115 119 'callback' => [$this, 'updateBookingStatus'],
116 - 'permission_callback' => [$this, 'checkAdminPermission'],
120 + 'permission_callback' => [$this, 'checkCanChangeStatus'],
117 121 ]);
118 122
119 - // Get booking statistics
123 + // Get booking statistics — view cap (aggregates only).
120 124 register_rest_route($this->namespace, '/bookings/stats', [
121 125 'methods' => 'GET',
122 126 'callback' => [$this, 'getBookingStats'],
123 - 'permission_callback' => [$this, 'checkAdminPermission'],
127 + 'permission_callback' => [$this, 'checkCanView'],
124 128 ]);
125 129
126 - // Send booking email
130 + // Send booking email — edit cap. Sending a transactional
131 + // re-confirmation is a write-side operation against the
132 + // customer's record.
127 133 register_rest_route($this->namespace, '/bookings/(?P<id>\d+)/send-email', [
128 134 'methods' => 'POST',
129 135 'callback' => [$this, 'sendBookingEmail'],
130 - 'permission_callback' => [$this, 'checkAdminPermission'],
136 + 'permission_callback' => [$this, 'checkCanEdit'],
131 137 ]);
132 138
133 139 // =====================
134 140 // PAYMENTS ROUTES
135 141 // =====================
136 -
137 - // Get booking payments
142 +
143 + // Get booking payments — view cap.
138 144 register_rest_route($this->namespace, '/bookings/(?P<id>\d+)/payments', [
139 145 'methods' => 'GET',
140 146 'callback' => [$this, 'getBookingPayments'],
141 - 'permission_callback' => [$this, 'checkAdminPermission'],
147 + 'permission_callback' => [$this, 'checkCanView'],
142 148 ]);
143 149
144 - // Add payment to booking
150 + // Add payment to booking — edit cap (modifies the booking's
151 + // payment state). Refunds + payment deletion live on the
152 + // dedicated PaymentController with their own high-sensitivity
153 + // caps.
145 154 register_rest_route($this->namespace, '/bookings/(?P<id>\d+)/payments', [
146 155 'methods' => 'POST',
147 156 'callback' => [$this, 'addPayment'],
148 - 'permission_callback' => [$this, 'checkAdminPermission'],
157 + 'permission_callback' => [$this, 'checkCanEdit'],
149 158 ]);
150 159
151 160 // NOTE: Payment CRUD operations moved to PaymentController
152 161 // This keeps BookingsController focused on booking operations only
@@ -153,20 +162,21 @@
153 162
154 163 // =====================
155 164 // TRAVELERS ROUTES
156 165 // =====================
157 -
166 +
167 + // Travelers list — view cap.
158 168 register_rest_route($this->namespace, '/travelers', [
159 169 'methods' => 'GET',
160 170 'callback' => [$this, 'getTravelers'],
161 - 'permission_callback' => [$this, 'checkAdminPermission'],
171 + 'permission_callback' => [$this, 'checkCanView'],
162 172 ]);
163 173
164 - // Traveler bulk actions
174 + // Traveler bulk actions — edit cap.
165 175 register_rest_route($this->namespace, '/travelers/bulk', [
166 176 'methods' => 'PUT',
167 177 'callback' => [$this, 'bulkTravelers'],
168 - 'permission_callback' => [$this, 'checkAdminPermission'],
178 + 'permission_callback' => [$this, 'checkCanEdit'],
169 179 ]);
170 180
171 181 // Download travel voucher for a booking
172 182 register_rest_route($this->namespace, '/bookings/(?P<id>\d+)/voucher', [
@@ -183,17 +193,53 @@
183 193 ]);
184 194 }
185 195
186 196 /**
187 - * Check admin permission
197 + * Granular permission checks — one per operation. WP administrators
198 + * pass every cap via the Team module's admin-fallback filter
199 + * (priority 7 / 8), so an explicit `manage_options` check isn't
200 + * needed at this layer — the cap covers it.
188 201 */
202 + public function checkCanView(): bool
203 + {
204 + return current_user_can('yatra_view_bookings');
205 + }
206 +
207 + public function checkCanCreate(): bool
208 + {
209 + return current_user_can('yatra_create_bookings');
210 + }
211 +
212 + public function checkCanEdit(): bool
213 + {
214 + return current_user_can('yatra_edit_bookings');
215 + }
216 +
217 + public function checkCanDelete(): bool
218 + {
219 + // Critical-sensitivity cap. By default only the Owner role
220 + // holds this — Manager, Sales Agent, Front Desk, etc. cannot
221 + // delete bookings even when they can edit them.
222 + return current_user_can('yatra_delete_bookings');
223 + }
224 +
225 + public function checkCanChangeStatus(): bool
226 + {
227 + // Separate from edit — Front Desk has this without the
228 + // broader edit cap so they can confirm/check-in bookings
229 + // without being able to mutate other fields.
230 + return current_user_can('yatra_change_booking_status');
231 + }
232 +
233 + /**
234 + * @deprecated Kept for any external code (snippet, integration)
235 + * that referenced the old method name. Routes to the view-only
236 + * cap — safer than the old `view OR manage_options` shorthand,
237 + * and admin users still pass via the admin-fallback layer.
238 + */
189 239 public function checkAdminPermission(): bool
190 240 {
191 - // Allow custom booking capability or fallback to manage_options
192 - if (current_user_can('yatra_view_bookings')) {
193 - return true;
194 - }
195 - return current_user_can('manage_options');
241 + return $this->checkCanView();
196 242 }
197 243
198 244 // =========================================================================
199 245 // BOOKING ENDPOINTS
@@ -213,8 +259,11 @@
213 259 'trip_id' => (int) $request->get_param('trip_id'),
214 260 'search' => $request->get_param('search') ?: '',
215 261 'date_from' => $request->get_param('date_from') ?: '',
216 262 'date_to' => $request->get_param('date_to') ?: '',
263 + // Column sorting from the table headers — whitelisted in the repository.
264 + 'orderby' => $request->get_param('orderby') ?: '',
265 + 'order' => $request->get_param('order') ?: '',
217 266 ];
218 267
219 268 // Delegate to service
220 269 $result = $this->bookingService->getBookings($filters);
@@ -313,9 +362,9 @@
313 362 return $this->error_response($result['message'] ?? 'Failed to update booking', 400);
314 363 }
315 364
316 365 Logger::info("Booking updated successfully", ['booking_id' => $id]);
317 - return $this->success_response($result['data']);
366 + return $this->success_response($result['data'] ?? null);
318 367
319 368 } catch (\Exception $e) {
320 369 Logger::error("Failed to update booking", ['booking_id' => $id ?? 0, 'data' => $data ?? [], 'error' => $e->getMessage()]);
321 370 return $this->handle_exception($e);
@@ -732,11 +781,21 @@
732 781 $bookingDate = !empty($createdAt) ? date_i18n(get_option('date_format'), strtotime((string) $createdAt)) : '';
733 782 $travelDateRaw = $booking['travel_date'] ?? '';
734 783 $travelDate = !empty($travelDateRaw) ? date_i18n(get_option('date_format'), strtotime((string) $travelDateRaw)) : '';
735 784
785 + // Return date. Prefer the booking's STORED end_date — the actual booked
786 + // return (accounts for a flexible window or a trip duration changed after
787 + // booking). Fall back to the trip duration only when no end is stored:
788 + // duration_days is INCLUSIVE, so the return is travel_date + (days - 1)
789 + // (matches BookingRepository::calculateEndDate; a bare "+ duration_days"
790 + // was one day too far and implied an extra night — see ItineraryPdfBuilder).
736 791 $returnDate = '';
737 - if (!empty($travelDateRaw) && $trip && !empty($trip->duration)) {
738 - $returnTimestamp = strtotime((string) $travelDateRaw . ' +' . (int) $trip->duration . ' days');
792 + $storedEnd = (string) ($booking['end_date'] ?? '');
793 + if ($storedEnd !== '' && ($travelDateRaw === '' || $storedEnd >= $travelDateRaw)) {
794 + $returnDate = date_i18n(get_option('date_format'), strtotime($storedEnd));
795 + } elseif (!empty($travelDateRaw) && $trip && !empty($trip->duration_days)) {
796 + $returnOffset = max(0, (int) $trip->duration_days - 1);
797 + $returnTimestamp = strtotime((string) $travelDateRaw . ' +' . $returnOffset . ' days');
739 798 $returnDate = date_i18n(get_option('date_format'), $returnTimestamp);
740 799 }
741 800
742 801 $statusRaw = (string) ($booking['booking_status'] ?? $booking['status'] ?? '');
@@ -749,12 +808,14 @@
749 808
750 809 $templateData = [
751 810 'company_name' => $companyName,
752 811 'company_address' => $companyAddress,
812 + 'company_address_lines' => \Yatra\Helpers\FormatHelper::companyAddressLines(),
753 813 'company_email' => $companyEmail,
754 814 'company_phone' => $companyPhone,
755 815 'customer_name' => $customerName,
756 816 'customer_email' => (string) ($booking['contact_email'] ?? $booking['customer_email'] ?? ''),
817 + 'customer_address_lines' => FormatHelper::customerAddressLines($booking),
757 818 'booking_ref' => $bookingRef,
758 819 'booking_date' => $bookingDate,
759 820 'booking_status' => ucfirst($statusRaw ?: 'pending'),
760 821 'status_class' => in_array(strtolower($statusRaw), ['confirmed', 'completed', 'success'], true) ? 'confirmed' :
@@ -759,9 +820,19 @@
759 820 'booking_status' => ucfirst($statusRaw ?: 'pending'),
760 821 'status_class' => in_array(strtolower($statusRaw), ['confirmed', 'completed', 'success'], true) ? 'confirmed' :
761 822 (in_array(strtolower($statusRaw), ['cancelled'], true) ? 'cancelled' : 'pending'),
762 823 'trip_title' => $trip ? ($trip->title ?? $booking['trip_title'] ?? __('Trip Booking', 'yatra')) : ($booking['trip_title'] ?? __('Trip Booking', 'yatra')),
763 - 'trip_duration' => $trip && $trip->duration ? sprintf(__('%d days', 'yatra'), (int) $trip->duration) : '',
824 + // Trip duration comes from duration_days/duration_nights (there is no
825 + // `duration` column — accessing it caused a blank value + PHP notice).
826 + 'trip_duration' => $trip
827 + ? yatra_format_duration(
828 + (int) ($trip->duration_days ?? 0),
829 + isset($trip->duration_nights) ? (int) $trip->duration_nights : null,
830 + // Hour-based day tours: "8 hours" instead of "1 day". Absent
831 + // or NULL on every day-based trip, which keeps its wording.
832 + (int) ($trip->duration_hours ?? 0)
833 + )
834 + : '',
764 835 'trip_difficulty' => $trip ? ($trip->difficulty_name ?? '') : '',
765 836 'departure_location' => $trip ? ($trip->departure_location ?? '') : '',
766 837 'destination' => $trip ? ($trip->destination ?? '') : '',
767 838 'travel_date' => $travelDate,
@@ -766,11 +837,11 @@
766 837 'destination' => $trip ? ($trip->destination ?? '') : '',
767 838 'travel_date' => $travelDate,
768 839 'return_date' => $returnDate,
769 840 'currency_symbol' => $currencySymbol,
770 - 'total_amount' => number_format((float) ($booking['total_amount'] ?? 0), 2),
771 - 'amount_paid' => number_format((float) ($booking['amount_paid'] ?? 0), 2),
772 - 'amount_due' => number_format((float) ($booking['amount_due'] ?? 0), 2),
841 + 'total_amount' => yatra_format_price((float) ($booking['total_amount'] ?? 0), $currency, false),
842 + 'amount_paid' => yatra_format_price((float) ($booking['amount_paid'] ?? 0), $currency, false),
843 + 'amount_due' => yatra_format_price((float) ($booking['amount_due'] ?? 0), $currency, false),
773 844 'traveler_count' => (int) ($booking['travelers_count'] ?? $booking['travelers'] ?? 1),
774 845 ];
775 846
776 847 $pdfService = new PdfService();
@@ -806,43 +877,10 @@
806 877 * @param array<string,mixed> $booking From BookingService::getBooking()
807 878 */
808 879 private function renderItineraryFromBookingData(array $booking, bool $isPreview)
809 880 {
810 - $tripRepository = new TripRepository();
811 - $trip = null;
812 - $tripId = (int) ($booking['trip_id'] ?? 0);
813 - if ($tripId > 0) {
814 - $trip = $tripRepository->find($tripId);
815 - }
816 -
817 - $companyName = SettingsService::get('company_name', get_bloginfo('name'));
818 - $companyAddress = SettingsService::get('company_address', '');
819 - $companyEmail = SettingsService::get('company_email', get_option('admin_email'));
820 - $companyPhone = SettingsService::get('company_phone', '');
821 - $currency = SettingsService::getCurrency();
822 - $currencySymbol = FormatHelper::getCurrencySymbol($currency);
823 -
824 - $createdAt = $booking['created_at'] ?? $booking['booking_date'] ?? '';
825 - $bookingDate = !empty($createdAt) ? date_i18n(get_option('date_format'), strtotime((string) $createdAt)) : '';
826 - $travelDateRaw = $booking['travel_date'] ?? '';
827 - $travelDate = !empty($travelDateRaw) ? date_i18n(get_option('date_format'), strtotime((string) $travelDateRaw)) : '';
828 -
829 - $returnDate = '';
830 - if (!empty($travelDateRaw) && $trip && !empty($trip->duration)) {
831 - $returnTimestamp = strtotime((string) $travelDateRaw . ' +' . (int) $trip->duration . ' days');
832 - $returnDate = date_i18n(get_option('date_format'), $returnTimestamp);
833 - }
834 -
835 - $statusRaw = (string) ($booking['booking_status'] ?? $booking['status'] ?? '');
836 - $bookingId = (int) ($booking['id'] ?? 0);
837 - $bookingRef = 'YTR-' . strtoupper(str_pad((string) $bookingId, 8, '0', STR_PAD_LEFT));
838 -
839 - $customerName = trim(
840 - (string) ($booking['contact_first_name'] ?? '') . ' ' . (string) ($booking['contact_last_name'] ?? '')
841 - ) ?: (string) ($booking['customer_name'] ?? __('Customer', 'yatra'));
842 -
843 - $pdfService = new PdfService();
844 - if (!$pdfService->isAvailable()) {
881 + $builder = new \Yatra\Services\ItineraryPdfBuilder();
882 + if (!$builder->pdfService()->isAvailable()) {
845 883 return new WP_Error(
846 884 'pdf_engine_missing',
847 885 __('Itinerary PDF generator is not installed. Please run composer install to install dompdf/dompdf.', 'yatra'),
848 886 ['status' => 500]
@@ -848,46 +886,20 @@
848 886 ['status' => 500]
849 887 );
850 888 }
851 889
890 + $bookingId = (int) ($booking['id'] ?? 0);
891 + $bookingRef = $bookingId > 0
892 + ? 'YTR-' . strtoupper(str_pad((string) $bookingId, 8, '0', STR_PAD_LEFT))
893 + : 'PENDING';
852 894 $filename = 'Travel-Itinerary-' . $bookingRef . '.pdf';
853 895
854 - $templateData = [
855 - 'company_name' => $companyName,
856 - 'company_address' => $companyAddress,
857 - 'company_email' => $companyEmail,
858 - 'company_phone' => $companyPhone,
859 - 'customer_name' => $customerName,
860 - 'customer_email' => (string) ($booking['contact_email'] ?? $booking['customer_email'] ?? ''),
861 - 'booking_ref' => $bookingRef,
862 - 'booking_date' => $bookingDate,
863 - 'booking_status' => ucfirst($statusRaw ?: 'pending'),
864 - 'status_class' => in_array(strtolower($statusRaw), ['confirmed', 'completed', 'success'], true) ? 'confirmed' :
865 - (in_array(strtolower($statusRaw), ['cancelled'], true) ? 'cancelled' : 'pending'),
866 - 'trip_title' => $trip ? ($trip->title ?? $booking['trip_title'] ?? __('Trip Booking', 'yatra')) : ($booking['trip_title'] ?? __('Trip Booking', 'yatra')),
867 - 'trip_description' => $trip ? ($trip->description ?? $trip->content ?? '') : '',
868 - 'trip_duration' => $trip && $trip->duration ? sprintf(__('%d days', 'yatra'), (int) $trip->duration) : '',
869 - 'trip_difficulty' => $trip ? ($trip->difficulty_name ?? '') : '',
870 - 'trip_highlights' => $trip ? ($trip->highlights ?? $trip->trip_highlights ?? '') : '',
871 - 'trip_includes' => $trip ? ($trip->includes ?? $trip->trip_includes ?? '') : '',
872 - 'trip_excludes' => $trip ? ($trip->excludes ?? $trip->trip_excludes ?? '') : '',
873 - 'departure_location' => $trip ? ($trip->departure_location ?? '') : '',
874 - 'destination' => $trip ? ($trip->destination ?? '') : '',
875 - 'travel_date' => $travelDate,
876 - 'return_date' => $returnDate,
877 - 'currency_symbol' => $currencySymbol,
878 - 'total_amount' => number_format((float) ($booking['total_amount'] ?? 0), 2),
879 - 'amount_paid' => number_format((float) ($booking['amount_paid'] ?? 0), 2),
880 - 'amount_due' => number_format((float) ($booking['amount_due'] ?? 0), 2),
881 - 'traveler_count' => (int) ($booking['travelers_count'] ?? $booking['travelers'] ?? 1),
882 - ];
896 + // The builder accepts the booking array shape directly — just
897 + // forward `id` as `booking_id` so the reference resolves the
898 + // same as the legacy code, and let it normalise everything else.
899 + $source = $booking + ['booking_id' => $bookingId];
900 + $pdfBinary = $builder->build($source);
883 901
884 - $pdfBinary = $pdfService->renderTemplateToPdfSafely('pdf/itinerary.php', $templateData, [
885 - 'paper' => 'A4',
886 - 'orientation' => 'portrait',
887 - 'default_font' => 'DejaVu Sans',
888 - ]);
889 -
890 902 if ($isPreview) {
891 903 return new WP_REST_Response([
892 904 'success' => true,
893 905 'pdf_data' => base64_encode($pdfBinary),
@@ -894,8 +906,8 @@
894 906 'filename' => $filename,
895 907 ]);
896 908 }
897 909
898 - $pdfService->outputPdfDownload($pdfBinary, $filename);
910 + $builder->pdfService()->outputPdfDownload($pdfBinary, $filename);
899 911 exit;
900 912 }
901 913 }