PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.5.1
Yatra – Travel Booking & Tour Operator Software v3.0.5.1
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
yatra / app / Validators / BookingValidator.php

BookingValidator.php in Yatra – Travel Booking & Tour Operator Software 3.0.5.1, at app/Validators/BookingValidator.php

406 lines 15.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 namespace Yatra\Validators;
6
7 use Yatra\Exceptions\ValidationException;
8
9 /**
10 * Booking Validator
11 *
12 * Comprehensive validation for booking data
13 */
14 class BookingValidator
15 {
16 /**
17 * Statuses a booking row may legally hold.
18 *
19 * `pending_verification` is the holding state used when
20 * `require_guest_email_verification` is on: the row is created but is not
21 * actionable until the guest clicks the verification link, at which
22 * point {@see BookingSessionController::confirmEmailVerifiedBooking()}
23 * (the `pending_verification` → `pending` transition) takes over.
24 *
25 * Keep this list as the single source of truth — both validate* and
26 * sanitize use it, so adding a new status anywhere in the booking
27 * lifecycle just needs one edit here.
28 */
29 private const VALID_BOOKING_STATUSES = [
30 'pending',
31 'pending_verification',
32 'confirmed',
33 'cancelled',
34 'completed',
35 'refunded',
36 'waitlist',
37 ];
38
39 /**
40 * Validate booking creation data
41 */
42 public static function validateCreate(array $data): void
43 {
44 $errors = [];
45
46 // Required fields
47 if (empty($data['trip_id'])) {
48 $errors['trip_id'][] = __('Trip ID is required', 'yatra');
49 } elseif (!is_numeric($data['trip_id']) || (int)$data['trip_id'] <= 0) {
50 $errors['trip_id'][] = __('Trip ID must be a valid positive integer', 'yatra');
51 }
52
53 // Customer can be guest, so customer_id is optional (if provided, validate)
54 if (isset($data['customer_id']) && $data['customer_id'] !== '') {
55 if (!is_numeric($data['customer_id']) || (int)$data['customer_id'] <= 0) {
56 $errors['customer_id'][] = __('Customer ID must be a valid positive integer', 'yatra');
57 }
58 }
59
60 // Accept either departure_date or travel_date
61 $departureDate = $data['departure_date'] ?? $data['travel_date'] ?? null;
62 if (empty($departureDate)) {
63 $errors['departure_date'][] = __('Departure date is required', 'yatra');
64 } elseif (!self::isValidDate($departureDate)) {
65 $errors['departure_date'][] = __('Departure date must be a valid date', 'yatra');
66 } elseif (strtotime($departureDate) < strtotime('today')) {
67 $errors['departure_date'][] = __('Departure date cannot be in the past', 'yatra');
68 }
69
70 // Validate status
71 if (isset($data['status'])) {
72 if (!in_array($data['status'], self::VALID_BOOKING_STATUSES, true)) {
73 $errors['status'][] = __('Invalid booking status', 'yatra');
74 }
75 }
76
77 // Validate pricing
78 if (isset($data['total_amount'])) {
79 if (!is_numeric($data['total_amount']) || (float)$data['total_amount'] < 0) {
80 $errors['total_amount'][] = __('Total amount must be a valid positive number', 'yatra');
81 }
82 }
83
84 if (isset($data['paid_amount'])) {
85 if (!is_numeric($data['paid_amount']) || (float)$data['paid_amount'] < 0) {
86 $errors['paid_amount'][] = __('Paid amount must be a valid positive number', 'yatra');
87 }
88 }
89
90 // Validate traveler count
91 $travelerCount = $data['total_travelers'] ?? $data['travelers_count'] ?? null;
92 if ($travelerCount !== null) {
93 if (!is_numeric($travelerCount) || (int)$travelerCount < 1) {
94 $errors['total_travelers'][] = __('Total travelers must be at least 1', 'yatra');
95 }
96 }
97
98 // Payment: booking "amount type" (full / deposit / partial) vs gateway (processor).
99 // Checkout sends both; Pro Flexible Payments uses payment_method=deposit|partial|full.
100 $bookingAmountMethods = ['full', 'partial', 'deposit'];
101 $gatewayIds = apply_filters('yatra_valid_booking_payment_gateway_ids', [
102 'cash',
103 'bank_transfer',
104 'credit_card',
105 'paypal',
106 'stripe',
107 'razorpay',
108 'pay_later',
109 'paystack',
110 'mollie',
111 'square',
112 'authorize_net',
113 'esewa',
114 'khalti',
115 ]);
116
117 if (isset($data['payment_method']) && $data['payment_method'] !== '') {
118 if (!in_array($data['payment_method'], $bookingAmountMethods, true)) {
119 // Legacy: some clients put the gateway id in payment_method only
120 if (!in_array($data['payment_method'], $gatewayIds, true)) {
121 $errors['payment_method'][] = __('Invalid payment method', 'yatra');
122 }
123 }
124 }
125
126 if (isset($data['payment_gateway']) && $data['payment_gateway'] !== '') {
127 if (!in_array($data['payment_gateway'], $gatewayIds, true)) {
128 $errors['payment_gateway'][] = __('Invalid payment gateway', 'yatra');
129 }
130 }
131
132 // Validate email format
133 if (isset($data['customer_email']) && !empty($data['customer_email'])) {
134 if (!is_email($data['customer_email'])) {
135 $errors['customer_email'][] = __('Invalid email format', 'yatra');
136 }
137 }
138
139 if (!empty($errors)) {
140 throw new ValidationException('Booking validation failed', $errors);
141 }
142 }
143
144 /**
145 * Validate booking update data
146 */
147 public static function validateUpdate(array $data, int $bookingId): void
148 {
149 $errors = [];
150
151 // ID validation
152 if ($bookingId <= 0) {
153 $errors['id'][] = __('Invalid booking ID', 'yatra');
154 }
155
156 // Optional field validation
157 if (isset($data['trip_id']) && (!is_numeric($data['trip_id']) || (int)$data['trip_id'] <= 0)) {
158 $errors['trip_id'][] = __('Trip ID must be a valid positive integer', 'yatra');
159 }
160
161 if (isset($data['customer_id']) && (!is_numeric($data['customer_id']) || (int)$data['customer_id'] <= 0)) {
162 $errors['customer_id'][] = __('Customer ID must be a valid positive integer', 'yatra');
163 }
164
165 if (isset($data['departure_date'])) {
166 if (!self::isValidDate($data['departure_date'])) {
167 $errors['departure_date'][] = __('Departure date must be a valid date', 'yatra');
168 }
169 }
170
171 if (isset($data['status'])) {
172 if (!in_array($data['status'], self::VALID_BOOKING_STATUSES, true)) {
173 $errors['status'][] = __('Invalid booking status', 'yatra');
174 }
175 }
176
177 if (isset($data['total_amount']) && (!is_numeric($data['total_amount']) || (float)$data['total_amount'] < 0)) {
178 $errors['total_amount'][] = __('Total amount must be a valid positive number', 'yatra');
179 }
180
181 if (isset($data['paid_amount']) && (!is_numeric($data['paid_amount']) || (float)$data['paid_amount'] < 0)) {
182 $errors['paid_amount'][] = __('Paid amount must be a valid positive number', 'yatra');
183 }
184
185 if (isset($data['total_travelers']) && (!is_numeric($data['total_travelers']) || (int)$data['total_travelers'] < 1)) {
186 $errors['total_travelers'][] = __('Total travelers must be at least 1', 'yatra');
187 }
188
189 if (isset($data['payment_method'])) {
190 $validMethods = ['cash', 'bank_transfer', 'credit_card', 'paypal', 'stripe', 'razorpay'];
191 if (!in_array($data['payment_method'], $validMethods)) {
192 $errors['payment_method'][] = __('Invalid payment method', 'yatra');
193 }
194 }
195
196 if (isset($data['customer_email']) && !empty($data['customer_email']) && !is_email($data['customer_email'])) {
197 $errors['customer_email'][] = __('Invalid email format', 'yatra');
198 }
199
200 if (!empty($errors)) {
201 throw new ValidationException('Booking validation failed', $errors);
202 }
203 }
204
205 /**
206 * Sanitize booking data
207 */
208 public static function sanitize(array $data): array
209 {
210 $sanitized = [];
211
212 // Integer fields
213 if (isset($data['trip_id'])) {
214 $sanitized['trip_id'] = (int)$data['trip_id'];
215 }
216
217 if (isset($data['customer_id'])) {
218 $sanitized['customer_id'] = (int)$data['customer_id'];
219 }
220
221 if (isset($data['total_travelers'])) {
222 $sanitized['total_travelers'] = (int)$data['total_travelers'];
223 }
224 if (isset($data['travelers_count'])) {
225 $sanitized['travelers_count'] = (int)$data['travelers_count'];
226 }
227
228 // Float fields
229 if (isset($data['total_amount'])) {
230 $sanitized['total_amount'] = (float)$data['total_amount'];
231 }
232
233 if (isset($data['paid_amount'])) {
234 $sanitized['paid_amount'] = (float)$data['paid_amount'];
235 }
236
237 // Date fields
238 if (isset($data['departure_date'])) {
239 $sanitized['departure_date'] = sanitize_text_field($data['departure_date']);
240 }
241 if (isset($data['travel_date'])) {
242 $sanitized['travel_date'] = sanitize_text_field($data['travel_date']);
243 }
244
245 if (isset($data['booking_date'])) {
246 $sanitized['booking_date'] = sanitize_text_field($data['booking_date']);
247 }
248
249 // Text fields
250 if (isset($data['customer_name'])) {
251 $sanitized['customer_name'] = sanitize_text_field($data['customer_name']);
252 }
253
254 if (isset($data['customer_email'])) {
255 $sanitized['customer_email'] = sanitize_email($data['customer_email']);
256 }
257
258 if (isset($data['customer_phone'])) {
259 $sanitized['customer_phone'] = sanitize_text_field($data['customer_phone']);
260 }
261
262 if (isset($data['notes'])) {
263 $sanitized['notes'] = wp_kses_post($data['notes']);
264 }
265
266 // Enum fields
267 if (isset($data['status'])) {
268 $sanitized['status'] = in_array($data['status'], self::VALID_BOOKING_STATUSES, true) ? $data['status'] : 'pending';
269 }
270
271 if (isset($data['payment_method'])) {
272 // Align with allowed frontend values (full/partial or gateway handles)
273 $validMethods = [
274 'full',
275 'partial',
276 'cash',
277 'bank_transfer',
278 'credit_card',
279 'paypal',
280 'stripe',
281 'razorpay',
282 'pay_later',
283 'paystack',
284 'mollie',
285 'square',
286 'authorize_net',
287 'esewa',
288 'khalti',
289 ];
290 $sanitized['payment_method'] = in_array($data['payment_method'], $validMethods, true)
291 ? $data['payment_method']
292 : $data['payment_method']; // keep original so validation can report exact value
293 }
294 if (isset($data['payment_gateway'])) {
295 $sanitized['payment_gateway'] = sanitize_text_field($data['payment_gateway']);
296 }
297
298 if (isset($data['payment_status'])) {
299 $validStatuses = ['pending', 'paid', 'partial', 'refunded', 'failed'];
300 $sanitized['payment_status'] = in_array($data['payment_status'], $validStatuses) ? $data['payment_status'] : 'pending';
301 }
302
303 // Tax fields
304 if (isset($data['subtotal'])) {
305 $sanitized['subtotal'] = (float)$data['subtotal'];
306 }
307 if (isset($data['tax_amount'])) {
308 $sanitized['tax_amount'] = (float)$data['tax_amount'];
309 }
310 if (isset($data['tax_rate'])) {
311 $sanitized['tax_rate'] = (float)$data['tax_rate'];
312 }
313 if (isset($data['tax_inclusive'])) {
314 $sanitized['tax_inclusive'] = (bool)$data['tax_inclusive'];
315 }
316 if (isset($data['tax_details'])) {
317 $sanitized['tax_details'] = $data['tax_details']; // Already JSON encoded
318 }
319
320 // Other booking fields
321 if (isset($data['currency'])) {
322 $sanitized['currency'] = sanitize_text_field($data['currency']);
323 }
324 if (isset($data['amount_due'])) {
325 $sanitized['amount_due'] = (float)$data['amount_due'];
326 }
327 if (isset($data['amount_paid'])) {
328 $sanitized['amount_paid'] = (float)$data['amount_paid'];
329 }
330 if (isset($data['discount_amount'])) {
331 $sanitized['discount_amount'] = (float)$data['discount_amount'];
332 }
333 if (isset($data['discount_code'])) {
334 $sanitized['discount_code'] = sanitize_text_field($data['discount_code']);
335 }
336 if (isset($data['reference'])) {
337 $sanitized['reference'] = sanitize_text_field($data['reference']);
338 }
339 if (isset($data['contact_first_name'])) {
340 $sanitized['contact_first_name'] = sanitize_text_field($data['contact_first_name']);
341 }
342 if (isset($data['contact_last_name'])) {
343 $sanitized['contact_last_name'] = sanitize_text_field($data['contact_last_name']);
344 }
345 if (isset($data['contact_email'])) {
346 $sanitized['contact_email'] = sanitize_email($data['contact_email']);
347 }
348 if (isset($data['contact_phone'])) {
349 $sanitized['contact_phone'] = sanitize_text_field($data['contact_phone']);
350 }
351 if (isset($data['contact_country'])) {
352 $sanitized['contact_country'] = sanitize_text_field($data['contact_country']);
353 }
354 if (isset($data['contact_data'])) {
355 $sanitized['contact_data'] = $data['contact_data']; // Already JSON encoded
356 }
357 if (isset($data['emergency_contact'])) {
358 $sanitized['emergency_contact'] = $data['emergency_contact']; // Already JSON encoded
359 }
360 if (isset($data['availability_id'])) {
361 $sanitized['availability_id'] = !empty($data['availability_id']) ? (int)$data['availability_id'] : null;
362 }
363 if (isset($data['user_id'])) {
364 $sanitized['user_id'] = !empty($data['user_id']) ? (int)$data['user_id'] : null;
365 }
366 if (isset($data['special_requests'])) {
367 $sanitized['special_requests'] = sanitize_textarea_field($data['special_requests']);
368 }
369 if (isset($data['newsletter_optin'])) {
370 $sanitized['newsletter_optin'] = (int)(bool)$data['newsletter_optin'];
371 }
372 if (isset($data['ip_address'])) {
373 $sanitized['ip_address'] = sanitize_text_field($data['ip_address']);
374 }
375 if (isset($data['created_at'])) {
376 $sanitized['created_at'] = sanitize_text_field($data['created_at']);
377 }
378 if (isset($data['updated_at'])) {
379 $sanitized['updated_at'] = sanitize_text_field($data['updated_at']);
380 }
381
382 // Itinerary costs fields
383 if (isset($data['itinerary_costs'])) {
384 $sanitized['itinerary_costs'] = $data['itinerary_costs']; // Already JSON encoded
385 }
386 if (isset($data['itinerary_costs_total'])) {
387 $sanitized['itinerary_costs_total'] = (float)$data['itinerary_costs_total'];
388 }
389 if (isset($data['departure_time'])) {
390 $t = trim((string) $data['departure_time']);
391 $sanitized['departure_time'] = $t !== '' ? sanitize_text_field($t) : '';
392 }
393
394 return $sanitized;
395 }
396
397 /**
398 * Check if date is valid
399 */
400 private static function isValidDate(string $date): bool
401 {
402 $d = \DateTime::createFromFormat('Y-m-d', $date);
403 return $d && $d->format('Y-m-d') === $date;
404 }
405 }
406