| @@ -85,10 +85,10 @@ | ||
| 85 | 85 | } |
| 86 | 86 | |
| 87 | 87 | // Validate slug format |
| 88 | 88 | if (isset($data['slug']) && !empty($data['slug'])) { |
| 89 | - if (!preg_match('/^[a-z0-9-]+$/', $data['slug'])) { | |
| 90 | - $errors['slug'][] = __('Slug can only contain lowercase letters, numbers, and hyphens', 'yatra'); | |
| 89 | + if (!preg_match('/^[\pL\pN-]+$/u', $data['slug'])) { | |
| 90 | + $errors['slug'][] = __('Slug can only contain letters, numbers, and hyphens', 'yatra'); | |
| 91 | 91 | } |
| 92 | 92 | } |
| 93 | 93 | |
| 94 | 94 | if (!empty($errors)) { |
| @@ -120,8 +120,24 @@ | ||
| 120 | 120 | if (isset($data['sale_price']) && (!is_numeric($data['sale_price']) || (float)$data['sale_price'] < 0)) { |
| 121 | 121 | $errors['sale_price'][] = __('Sale price must be a valid positive number', 'yatra'); |
| 122 | 122 | } |
| 123 | 123 | |
| 124 | + // Deposit fields (FlexiblePayments Pro feature — schema lives in free, used by Pro) | |
| 125 | + if (isset($data['deposit_amount']) && $data['deposit_amount'] !== '' && (!is_numeric($data['deposit_amount']) || (float)$data['deposit_amount'] < 0)) { | |
| 126 | + $errors['deposit_amount'][] = __('Deposit amount must be a valid positive number', 'yatra'); | |
| 127 | + } | |
| 128 | + | |
| 129 | + if (isset($data['deposit_percentage']) && $data['deposit_percentage'] !== '') { | |
| 130 | + if (!is_numeric($data['deposit_percentage'])) { | |
| 131 | + $errors['deposit_percentage'][] = __('Deposit percentage must be a valid number', 'yatra'); | |
| 132 | + } else { | |
| 133 | + $pct = (float)$data['deposit_percentage']; | |
| 134 | + if ($pct < 0 || $pct > 100) { | |
| 135 | + $errors['deposit_percentage'][] = __('Deposit percentage must be between 0 and 100', 'yatra'); | |
| 136 | + } | |
| 137 | + } | |
| 138 | + } | |
| 139 | + | |
| 124 | 140 | if (isset($data['duration_days']) && (!is_numeric($data['duration_days']) || (int)$data['duration_days'] < 0)) { |
| 125 | 141 | $errors['duration_days'][] = __('Duration days must be a valid positive number', 'yatra'); |
| 126 | 142 | } |
| 127 | 143 | |
| @@ -146,10 +162,10 @@ | ||
| 146 | 162 | $errors['max_group_size'][] = __('Maximum group size must be greater than or equal to minimum group size', 'yatra'); |
| 147 | 163 | } |
| 148 | 164 | } |
| 149 | 165 | |
| 150 | - if (isset($data['slug']) && !empty($data['slug']) && !preg_match('/^[a-z0-9-]+$/', $data['slug'])) { | |
| 151 | - $errors['slug'][] = __('Slug can only contain lowercase letters, numbers, and hyphens', 'yatra'); | |
| 166 | + if (isset($data['slug']) && !empty($data['slug']) && !preg_match('/^[\pL\pN-]+$/u', $data['slug'])) { | |
| 167 | + $errors['slug'][] = __('Slug can only contain letters, numbers, and hyphens', 'yatra'); | |
| 152 | 168 | } |
| 153 | 169 | |
| 154 | 170 | if (!empty($errors)) { |
| 155 | 171 | throw new ValidationException('Trip validation failed', $errors); |
| @@ -168,9 +184,9 @@ | ||
| 168 | 184 | $sanitized['title'] = sanitize_text_field($data['title']); |
| 169 | 185 | } |
| 170 | 186 | |
| 171 | 187 | if (isset($data['slug'])) { |
| 172 | - $sanitized['slug'] = sanitize_title($data['slug']); | |
| 188 | + $sanitized['slug'] = \Yatra\Helpers\SlugHelper::generate($data['slug']); | |
| 173 | 189 | } |
| 174 | 190 | |
| 175 | 191 | if (isset($data['description'])) { |
| 176 | 192 | $sanitized['description'] = wp_kses_post($data['description']); |
| @@ -271,8 +287,24 @@ | ||
| 271 | 287 | if (isset($data['payment_terms'])) { |
| 272 | 288 | $sanitized['payment_terms'] = wp_kses_post($data['payment_terms']); |
| 273 | 289 | } |
| 274 | 290 | |
| 291 | + // Deposit fields — DB columns are decimal(10,2) / decimal(5,2). | |
| 292 | + // Cast empty string to null so the column resets cleanly when the admin | |
| 293 | + // clears the field (cast to float would coerce '' → 0.0 which is a | |
| 294 | + // semantically-different "fixed $0 deposit"). | |
| 295 | + if (array_key_exists('deposit_amount', $data)) { | |
| 296 | + $sanitized['deposit_amount'] = ($data['deposit_amount'] === '' || $data['deposit_amount'] === null) | |
| 297 | + ? null | |
| 298 | + : (float)$data['deposit_amount']; | |
| 299 | + } | |
| 300 | + | |
| 301 | + if (array_key_exists('deposit_percentage', $data)) { | |
| 302 | + $sanitized['deposit_percentage'] = ($data['deposit_percentage'] === '' || $data['deposit_percentage'] === null) | |
| 303 | + ? null | |
| 304 | + : (float)$data['deposit_percentage']; | |
| 305 | + } | |
| 306 | + | |
| 275 | 307 | if (isset($data['cancellation_policy'])) { |
| 276 | 308 | $sanitized['cancellation_policy'] = wp_kses_post($data['cancellation_policy']); |
| 277 | 309 | } |
| 278 | 310 | |
| @@ -299,15 +331,23 @@ | ||
| 299 | 331 | if (isset($data['meta_keywords'])) { |
| 300 | 332 | $sanitized['meta_keywords'] = sanitize_text_field($data['meta_keywords']); |
| 301 | 333 | } |
| 302 | 334 | |
| 303 | - // Numeric fields | |
| 304 | - if (isset($data['original_price'])) { | |
| 305 | - $sanitized['original_price'] = (float)$data['original_price']; | |
| 335 | + // Numeric fields. | |
| 336 | + // Use array_key_exists (not isset) so an explicitly-sent null/empty | |
| 337 | + // price is written as SQL NULL — letting admins CLEAR a price. With | |
| 338 | + // isset(), a null was dropped and the old value lingered, so prices | |
| 339 | + // could never be emptied from the UI. | |
| 340 | + if (array_key_exists('original_price', $data)) { | |
| 341 | + $sanitized['original_price'] = ($data['original_price'] === null || $data['original_price'] === '') | |
| 342 | + ? null | |
| 343 | + : (float)$data['original_price']; | |
| 306 | 344 | } |
| 307 | 345 | |
| 308 | - if (isset($data['discounted_price'])) { | |
| 309 | - $sanitized['discounted_price'] = (float)$data['discounted_price']; | |
| 346 | + if (array_key_exists('discounted_price', $data)) { | |
| 347 | + $sanitized['discounted_price'] = ($data['discounted_price'] === null || $data['discounted_price'] === '') | |
| 348 | + ? null | |
| 349 | + : (float)$data['discounted_price']; | |
| 310 | 350 | } |
| 311 | 351 | |
| 312 | 352 | if (isset($data['duration_days'])) { |
| 313 | 353 | $sanitized['duration_days'] = (int)$data['duration_days']; |
| @@ -481,8 +521,30 @@ | ||
| 481 | 521 | if (isset($data['custom_fields'])) { |
| 482 | 522 | $sanitized['custom_fields'] = is_array($data['custom_fields']) |
| 483 | 523 | ? $data['custom_fields'] |
| 484 | 524 | : (array) $data['custom_fields']; |
| 525 | + } | |
| 526 | + | |
| 527 | + // Hour-based duration only applies to single-day tours. Clamp to a sane | |
| 528 | + // day-length range, and never let a multi-day / flexible trip carry | |
| 529 | + // hours — otherwise the front end would show "8 hours" for a multi-day | |
| 530 | + // trip and the Google Calendar module would build a short timed event | |
| 531 | + // instead of the correct multi-day span. | |
| 532 | + // | |
| 533 | + // `trip_type` settles it when the payload carries it (the trip form | |
| 534 | + // always sends both). A partial update that omits `trip_type` is caught | |
| 535 | + // by the duration_days fallback below, so hours can never be stored | |
| 536 | + // against a multi-day span. | |
| 537 | + if (array_key_exists('duration_hours', $sanitized)) { | |
| 538 | + $sanitized['duration_hours'] = max(0, min(24, (int) $sanitized['duration_hours'])); | |
| 539 | + $isMultiDayType = array_key_exists('trip_type', $sanitized) | |
| 540 | + && $sanitized['trip_type'] !== 'single_day'; | |
| 541 | + $isMultiDaySpan = array_key_exists('duration_days', $sanitized) | |
| 542 | + && (int) $sanitized['duration_days'] > 1; | |
| 543 | + | |
| 544 | + if ($isMultiDayType || $isMultiDaySpan) { | |
| 545 | + $sanitized['duration_hours'] = 0; | |
| 546 | + } | |
| 485 | 547 | } |
| 486 | 548 | |
| 487 | 549 | return apply_filters('yatra_trip_sanitize_data', $sanitized, $data); |
| 488 | 550 | } |