(int)$data['max_group_size']) { $errors['max_group_size'][] = __('Maximum group size must be greater than or equal to minimum group size', 'yatra'); } } // Validate slug format if (isset($data['slug']) && !empty($data['slug'])) { if (!preg_match('/^[\pL\pN-]+$/u', $data['slug'])) { $errors['slug'][] = __('Slug can only contain letters, numbers, and hyphens', 'yatra'); } } if (!empty($errors)) { throw new ValidationException('Trip validation failed', $errors); } } /** * Validate trip update data */ public static function validateUpdate(array $data, int $tripId): void { $errors = []; // ID validation if ($tripId <= 0) { $errors['id'][] = __('Invalid trip ID', 'yatra'); } // Optional field validation (same rules as create, but fields are optional) if (isset($data['status']) && !in_array($data['status'], ['draft', 'publish', 'private', 'trash'])) { $errors['status'][] = __('Invalid status value', 'yatra'); } if (isset($data['original_price']) && (!is_numeric($data['original_price']) || (float)$data['original_price'] < 0)) { $errors['original_price'][] = __('Original price must be a valid positive number', 'yatra'); } if (isset($data['sale_price']) && (!is_numeric($data['sale_price']) || (float)$data['sale_price'] < 0)) { $errors['sale_price'][] = __('Sale price must be a valid positive number', 'yatra'); } // Deposit fields (FlexiblePayments Pro feature — schema lives in free, used by Pro) if (isset($data['deposit_amount']) && $data['deposit_amount'] !== '' && (!is_numeric($data['deposit_amount']) || (float)$data['deposit_amount'] < 0)) { $errors['deposit_amount'][] = __('Deposit amount must be a valid positive number', 'yatra'); } if (isset($data['deposit_percentage']) && $data['deposit_percentage'] !== '') { if (!is_numeric($data['deposit_percentage'])) { $errors['deposit_percentage'][] = __('Deposit percentage must be a valid number', 'yatra'); } else { $pct = (float)$data['deposit_percentage']; if ($pct < 0 || $pct > 100) { $errors['deposit_percentage'][] = __('Deposit percentage must be between 0 and 100', 'yatra'); } } } if (isset($data['duration_days']) && (!is_numeric($data['duration_days']) || (int)$data['duration_days'] < 0)) { $errors['duration_days'][] = __('Duration days must be a valid positive number', 'yatra'); } if (isset($data['duration_nights']) && (!is_numeric($data['duration_nights']) || (int)$data['duration_nights'] < 0)) { $errors['duration_nights'][] = __('Duration nights must be a valid positive number', 'yatra'); } if (isset($data['difficulty_level']) && (!is_numeric($data['difficulty_level']) || (int)$data['difficulty_level'] <= 0)) { $errors['difficulty_level'][] = __('Difficulty level must be a valid positive integer', 'yatra'); } if (isset($data['min_group_size']) && (!is_numeric($data['min_group_size']) || (int)$data['min_group_size'] < 1)) { $errors['min_group_size'][] = __('Minimum group size must be at least 1', 'yatra'); } if (isset($data['max_group_size']) && (!is_numeric($data['max_group_size']) || (int)$data['max_group_size'] < 1)) { $errors['max_group_size'][] = __('Maximum group size must be at least 1', 'yatra'); } if (isset($data['min_group_size']) && isset($data['max_group_size'])) { if ((int)$data['min_group_size'] > (int)$data['max_group_size']) { $errors['max_group_size'][] = __('Maximum group size must be greater than or equal to minimum group size', 'yatra'); } } if (isset($data['slug']) && !empty($data['slug']) && !preg_match('/^[\pL\pN-]+$/u', $data['slug'])) { $errors['slug'][] = __('Slug can only contain letters, numbers, and hyphens', 'yatra'); } if (!empty($errors)) { throw new ValidationException('Trip validation failed', $errors); } } /** * Sanitize trip data */ public static function sanitize(array $data): array { $sanitized = []; // Text fields if (isset($data['title'])) { $sanitized['title'] = sanitize_text_field($data['title']); } if (isset($data['slug'])) { $sanitized['slug'] = \Yatra\Helpers\SlugHelper::generate($data['slug']); } if (isset($data['description'])) { $sanitized['description'] = wp_kses_post($data['description']); } if (isset($data['short_description'])) { $sanitized['short_description'] = wp_kses_post($data['short_description']); } if (isset($data['trip_details'])) { $sanitized['trip_details'] = wp_kses_post($data['trip_details']); } if (isset($data['what_makes_special'])) { $sanitized['what_makes_special'] = wp_kses_post($data['what_makes_special']); } if (isset($data['trip_story'])) { $sanitized['trip_story'] = wp_kses_post($data['trip_story']); } if (isset($data['video_url'])) { $sanitized['video_url'] = esc_url_raw($data['video_url']); } if (isset($data['virtual_tour_url'])) { $sanitized['virtual_tour_url'] = esc_url_raw($data['virtual_tour_url']); } if (isset($data['starting_location'])) { $sanitized['starting_location'] = sanitize_text_field($data['starting_location']); } if (isset($data['ending_location'])) { $sanitized['ending_location'] = sanitize_text_field($data['ending_location']); } if (isset($data['starting_latitude'])) { $sanitized['starting_latitude'] = is_numeric($data['starting_latitude']) ? (float) $data['starting_latitude'] : null; } if (isset($data['starting_longitude'])) { $sanitized['starting_longitude'] = is_numeric($data['starting_longitude']) ? (float) $data['starting_longitude'] : null; } if (isset($data['ending_latitude'])) { $sanitized['ending_latitude'] = is_numeric($data['ending_latitude']) ? (float) $data['ending_latitude'] : null; } if (isset($data['ending_longitude'])) { $sanitized['ending_longitude'] = is_numeric($data['ending_longitude']) ? (float) $data['ending_longitude'] : null; } if (isset($data['seasonal_availability'])) { $sanitized['seasonal_availability'] = sanitize_text_field($data['seasonal_availability']); } if (isset($data['best_season'])) { $sanitized['best_season'] = sanitize_text_field($data['best_season']); } if (isset($data['peak_season'])) { $sanitized['peak_season'] = sanitize_text_field($data['peak_season']); } if (isset($data['off_season'])) { $sanitized['off_season'] = sanitize_text_field($data['off_season']); } if (isset($data['accommodation_type'])) { $sanitized['accommodation_type'] = sanitize_text_field($data['accommodation_type']); } if (isset($data['meal_plan'])) { $sanitized['meal_plan'] = sanitize_text_field($data['meal_plan']); } if (isset($data['accommodation_details'])) { $sanitized['accommodation_details'] = wp_kses_post($data['accommodation_details']); } if (isset($data['pickup_location'])) { $sanitized['pickup_location'] = sanitize_text_field($data['pickup_location']); } if (isset($data['dropoff_location'])) { $sanitized['dropoff_location'] = sanitize_text_field($data['dropoff_location']); } if (isset($data['transportation_details'])) { $sanitized['transportation_details'] = wp_kses_post($data['transportation_details']); } if (isset($data['transportation_included'])) { $sanitized['transportation_included'] = (bool)$data['transportation_included']; } if (isset($data['payment_terms'])) { $sanitized['payment_terms'] = wp_kses_post($data['payment_terms']); } // Deposit fields — DB columns are decimal(10,2) / decimal(5,2). // Cast empty string to null so the column resets cleanly when the admin // clears the field (cast to float would coerce '' → 0.0 which is a // semantically-different "fixed $0 deposit"). if (array_key_exists('deposit_amount', $data)) { $sanitized['deposit_amount'] = ($data['deposit_amount'] === '' || $data['deposit_amount'] === null) ? null : (float)$data['deposit_amount']; } if (array_key_exists('deposit_percentage', $data)) { $sanitized['deposit_percentage'] = ($data['deposit_percentage'] === '' || $data['deposit_percentage'] === null) ? null : (float)$data['deposit_percentage']; } if (isset($data['cancellation_policy'])) { $sanitized['cancellation_policy'] = wp_kses_post($data['cancellation_policy']); } if (isset($data['physical_requirements'])) { $sanitized['physical_requirements'] = wp_kses_post($data['physical_requirements']); } if (isset($data['visa_requirements'])) { $sanitized['visa_requirements'] = wp_kses_post($data['visa_requirements']); } if (isset($data['vaccination_requirements'])) { $sanitized['vaccination_requirements'] = wp_kses_post($data['vaccination_requirements']); } if (isset($data['meta_title'])) { $sanitized['meta_title'] = sanitize_text_field($data['meta_title']); } if (isset($data['meta_description'])) { $sanitized['meta_description'] = sanitize_textarea_field($data['meta_description']); } if (isset($data['meta_keywords'])) { $sanitized['meta_keywords'] = sanitize_text_field($data['meta_keywords']); } // Numeric fields. // Use array_key_exists (not isset) so an explicitly-sent null/empty // price is written as SQL NULL — letting admins CLEAR a price. With // isset(), a null was dropped and the old value lingered, so prices // could never be emptied from the UI. if (array_key_exists('original_price', $data)) { $sanitized['original_price'] = ($data['original_price'] === null || $data['original_price'] === '') ? null : (float)$data['original_price']; } if (array_key_exists('discounted_price', $data)) { $sanitized['discounted_price'] = ($data['discounted_price'] === null || $data['discounted_price'] === '') ? null : (float)$data['discounted_price']; } if (isset($data['duration_days'])) { $sanitized['duration_days'] = (int)$data['duration_days']; } if (isset($data['duration_nights'])) { $sanitized['duration_nights'] = (int)$data['duration_nights']; } if (isset($data['duration_hours'])) { $sanitized['duration_hours'] = (int)$data['duration_hours']; } if (isset($data['available_from'])) { $sanitized['available_from'] = sanitize_text_field($data['available_from']); } if (isset($data['available_to'])) { $sanitized['available_to'] = sanitize_text_field($data['available_to']); } if (isset($data['age_min'])) { $sanitized['age_min'] = (int)$data['age_min']; } if (isset($data['age_max'])) { $sanitized['age_max'] = (int)$data['age_max']; } if (isset($data['version'])) { $sanitized['version'] = (int)$data['version']; } if (isset($data['updated_by'])) { $sanitized['updated_by'] = (int)$data['updated_by']; } if (isset($data['updated_at'])) { $sanitized['updated_at'] = sanitize_text_field($data['updated_at']); } if (isset($data['difficulty_level'])) { $sanitized['difficulty_level'] = (int)$data['difficulty_level']; } if (isset($data['min_group_size'])) { $sanitized['min_group_size'] = (int)$data['min_group_size']; } if (isset($data['max_group_size'])) { $sanitized['max_group_size'] = (int)$data['max_group_size']; } // Booking fields if (isset($data['min_travelers'])) { $sanitized['min_travelers'] = (int)$data['min_travelers']; } if (isset($data['max_travelers'])) { $sanitized['max_travelers'] = (int)$data['max_travelers']; } if (isset($data['booking_window_days'])) { $sanitized['booking_window_days'] = (int)$data['booking_window_days']; } if (isset($data['booking_deadline_hours'])) { $sanitized['booking_deadline_hours'] = (int)$data['booking_deadline_hours']; } // Time slot fields if (isset($data['has_default_time_slots'])) { $sanitized['has_default_time_slots'] = (bool)$data['has_default_time_slots']; } if (isset($data['default_time_slots'])) { // Keep as string (already JSON encoded from controller) $sanitized['default_time_slots'] = $data['default_time_slots']; } if (isset($data['departure_time'])) { $sanitized['departure_time'] = sanitize_text_field($data['departure_time']); } // Enum fields if (isset($data['status'])) { $sanitized['status'] = in_array($data['status'], ['draft', 'publish', 'private', 'trash']) ? $data['status'] : 'draft'; } if (isset($data['trip_type'])) { $sanitized['trip_type'] = in_array($data['trip_type'], ['single_day', 'multi_day', 'flexible']) ? $data['trip_type'] : 'multi_day'; } if (isset($data['pricing_type'])) { $sanitized['pricing_type'] = in_array($data['pricing_type'], ['regular', 'traveler_based', 'dynamic', 'custom']) ? $data['pricing_type'] : 'regular'; } if (isset($data['featured_priority'])) { $sanitized['featured_priority'] = in_array($data['featured_priority'], ['none', 'featured', 'new', 'limited', 'bestseller']) ? $data['featured_priority'] : 'none'; } if (isset($data['currency'])) { $sanitized['currency'] = sanitize_text_field($data['currency']); } // Always include featured_image if it exists in the data, even if null if (array_key_exists('featured_image', $data)) { // Allow null or empty string to remove featured image $sanitized['featured_image'] = empty($data['featured_image']) ? null : sanitize_text_field($data['featured_image']); } // JSON fields if (isset($data['testimonials'])) { $sanitized['testimonials'] = is_array($data['testimonials']) ? maybe_serialize($data['testimonials']) : $data['testimonials']; } if (isset($data['countries'])) { $sanitized['countries'] = is_array($data['countries']) ? maybe_serialize($data['countries']) : $data['countries']; } if (isset($data['regions'])) { $sanitized['regions'] = is_array($data['regions']) ? maybe_serialize($data['regions']) : $data['regions']; } if (isset($data['landmarks'])) { $sanitized['landmarks'] = is_array($data['landmarks']) ? maybe_serialize($data['landmarks']) : $data['landmarks']; } if (isset($data['tags'])) { $sanitized['tags'] = is_array($data['tags']) ? maybe_serialize($data['tags']) : $data['tags']; } if (isset($data['included_items'])) { $sanitized['included_items'] = is_array($data['included_items']) ? maybe_serialize($data['included_items']) : $data['included_items']; } if (isset($data['excluded_items'])) { $sanitized['excluded_items'] = is_array($data['excluded_items']) ? maybe_serialize($data['excluded_items']) : $data['excluded_items']; } if (isset($data['frontend_tabs'])) { $sanitized['frontend_tabs'] = is_array($data['frontend_tabs']) ? maybe_serialize($data['frontend_tabs']) : $data['frontend_tabs']; } if (isset($data['testimonial_review_ids'])) { $sanitized['testimonial_review_ids'] = is_array($data['testimonial_review_ids']) ? json_encode($data['testimonial_review_ids']) : $data['testimonial_review_ids']; } if (isset($data['features'])) { $sanitized['features'] = is_array($data['features']) ? json_encode($data['features']) : $data['features']; } if (isset($data['includes'])) { $sanitized['includes'] = is_array($data['includes']) ? json_encode($data['includes']) : $data['includes']; } if (isset($data['excludes'])) { $sanitized['excludes'] = is_array($data['excludes']) ? json_encode($data['excludes']) : $data['excludes']; } // custom_fields is a JSON column — pass the array through so TripService can // merge it with the existing stored data before serialising. if (isset($data['custom_fields'])) { $sanitized['custom_fields'] = is_array($data['custom_fields']) ? $data['custom_fields'] : (array) $data['custom_fields']; } // Hour-based duration only applies to single-day tours. Clamp to a sane // day-length range, and never let a multi-day / flexible trip carry // hours — otherwise the front end would show "8 hours" for a multi-day // trip and the Google Calendar module would build a short timed event // instead of the correct multi-day span. // // `trip_type` settles it when the payload carries it (the trip form // always sends both). A partial update that omits `trip_type` is caught // by the duration_days fallback below, so hours can never be stored // against a multi-day span. if (array_key_exists('duration_hours', $sanitized)) { $sanitized['duration_hours'] = max(0, min(24, (int) $sanitized['duration_hours'])); $isMultiDayType = array_key_exists('trip_type', $sanitized) && $sanitized['trip_type'] !== 'single_day'; $isMultiDaySpan = array_key_exists('duration_days', $sanitized) && (int) $sanitized['duration_days'] > 1; if ($isMultiDayType || $isMultiDaySpan) { $sanitized['duration_hours'] = 0; } } return apply_filters('yatra_trip_sanitize_data', $sanitized, $data); } }