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

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

466 lines 16.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\Services;
6
7 use Yatra\Repositories\TravelerCategoryRepository;
8 use Yatra\Helpers\SlugHelper;
9
10 /**
11 * Traveler Category Service
12 * Contains business logic for traveler categories
13 */
14 class TravelerCategoryService extends BaseService
15 {
16 /**
17 * @var TravelerCategoryRepository
18 */
19 private TravelerCategoryRepository $repository;
20
21 /**
22 * Constructor
23 */
24 public function __construct()
25 {
26 $this->repository = new TravelerCategoryRepository();
27 }
28
29 /**
30 * Get repository
31 */
32 protected function getRepository(): TravelerCategoryRepository
33 {
34 return $this->repository;
35 }
36
37 /**
38 * Validate data for creation with field mapping
39 */
40 public function validateCreate(array $data): void
41 {
42 // Handle field mapping for backward compatibility BEFORE validation
43 if (isset($data['label']) && !isset($data['name'])) {
44 $data['name'] = $data['label'];
45 // Remove the old label field to prevent database errors
46 unset($data['label']);
47 }
48
49 // Call parent validation with mapped data
50 parent::validateCreate($data);
51 }
52
53 /**
54 * Validate data for update with field mapping
55 */
56 public function validateUpdate(int $id, array $data): void
57 {
58 // Handle field mapping for backward compatibility BEFORE validation
59 if (isset($data['label']) && !isset($data['name'])) {
60 $data['name'] = $data['label'];
61 // Remove the old label field to prevent database errors
62 unset($data['label']);
63 }
64
65 // Call parent validation with mapped data
66 parent::validateUpdate($id, $data);
67 }
68
69 /**
70 * Validate traveler category data
71 */
72 protected function validate(array $data, ?int $id = null): void
73 {
74 if (empty($data['name'])) {
75 throw new \InvalidArgumentException('Category name is required');
76 }
77
78 // Slug will be auto-generated from name, so we don't need to validate it here
79 // The SlugHelper will ensure uniqueness
80
81 // Validate status
82 $allowed_statuses = ['draft', 'publish', 'trash'];
83 if (isset($data['status']) && !in_array($data['status'], $allowed_statuses, true)) {
84 throw new \InvalidArgumentException('Invalid status. Must be one of: ' . implode(', ', $allowed_statuses));
85 }
86
87 // Validate age range
88 if (isset($data['age_min']) && $data['age_min'] !== '' && $data['age_min'] !== null) {
89 $age_min = (int) $data['age_min'];
90 if ($age_min < 0) {
91 throw new \InvalidArgumentException('Minimum age cannot be negative');
92 }
93 }
94
95 if (isset($data['age_max']) && $data['age_max'] !== '' && $data['age_max'] !== null) {
96 $age_max = (int) $data['age_max'];
97 if ($age_max < 0) {
98 throw new \InvalidArgumentException('Maximum age cannot be negative');
99 }
100 }
101
102 // Validate age range logic
103 if (isset($data['age_min']) && isset($data['age_max'])
104 && $data['age_min'] !== '' && $data['age_max'] !== ''
105 && $data['age_min'] !== null && $data['age_max'] !== null) {
106 $age_min = (int) $data['age_min'];
107 $age_max = (int) $data['age_max'];
108 if ($age_min >= $age_max) {
109 throw new \InvalidArgumentException('Maximum age must be greater than minimum age');
110 }
111 }
112
113 // Validate pricing mode
114 if (isset($data['pricing_mode']) && $data['pricing_mode'] !== '') {
115 $allowed_pricing_modes = ['per_person', 'per_group'];
116 if (!in_array($data['pricing_mode'], $allowed_pricing_modes, true)) {
117 throw new \InvalidArgumentException('Invalid pricing mode. Must be per_person or per_group');
118 }
119 }
120
121 // Validate group size when pricing per group
122 if (($data['pricing_mode'] ?? 'per_person') === 'per_group') {
123 if (isset($data['min_pax']) && $data['min_pax'] !== '' && $data['min_pax'] !== null) {
124 $min_pax = (int) $data['min_pax'];
125 if ($min_pax <= 0) {
126 throw new \InvalidArgumentException('Minimum group size must be greater than zero');
127 }
128 }
129
130 if (isset($data['max_pax']) && $data['max_pax'] !== '' && $data['max_pax'] !== null) {
131 $max_pax = (int) $data['max_pax'];
132 if ($max_pax <= 0) {
133 throw new \InvalidArgumentException('Maximum group size must be greater than zero');
134 }
135 }
136
137 if (
138 isset($data['min_pax'], $data['max_pax']) &&
139 $data['min_pax'] !== '' && $data['max_pax'] !== '' &&
140 $data['min_pax'] !== null && $data['max_pax'] !== null
141 ) {
142 $min_pax = (int) $data['min_pax'];
143 $max_pax = (int) $data['max_pax'];
144 if ($min_pax > $max_pax) {
145 throw new \InvalidArgumentException('Maximum group size must be greater than or equal to minimum group size');
146 }
147 }
148 }
149 }
150
151 /**
152 * Process before create
153 */
154 protected function processBeforeCreate(array $data): array
155 {
156 // Handle field mapping for backward compatibility
157 if (isset($data['label']) && !isset($data['name'])) {
158 $data['name'] = $data['label'];
159 // Remove the old label field to prevent database errors
160 unset($data['label']);
161 }
162
163 // Sanitize name
164 if (isset($data['name'])) {
165 $data['name'] = sanitize_text_field($data['name']);
166 }
167
168 // Always auto-generate slug from name (backend ensures uniqueness)
169 if (!empty($data['name'])) {
170 $data['slug'] = SlugHelper::generateUniqueFromDatabase(
171 $data['name'],
172 'yatra_classifications',
173 'slug'
174 );
175 } elseif (isset($data['slug'])) {
176 // If name is empty but slug is provided, sanitize it
177 $data['slug'] = SlugHelper::generate($data['slug']);
178 }
179
180 // Sanitize description
181 if (isset($data['description'])) {
182 $data['description'] = sanitize_textarea_field($data['description']);
183 }
184
185 // Sanitize status
186 if (isset($data['status'])) {
187 $allowed_statuses = ['draft', 'publish', 'trash'];
188 $data['status'] = in_array($data['status'], $allowed_statuses, true)
189 ? $data['status']
190 : 'draft';
191 }
192
193 // Prepare metadata for traveler type
194 $metadata = [];
195
196 if (isset($data['age_min']) && $data['age_min'] !== '' && $data['age_min'] !== null) {
197 $metadata['age_min'] = (int) $data['age_min'];
198 }
199
200 if (isset($data['age_max']) && $data['age_max'] !== '' && $data['age_max'] !== null) {
201 $metadata['age_max'] = (int) $data['age_max'];
202 }
203
204 if (isset($data['pricing_mode'])) {
205 $metadata['pricing_mode'] = sanitize_text_field($data['pricing_mode']);
206 }
207
208 if (isset($data['min_pax']) && $data['min_pax'] !== '' && $data['min_pax'] !== null) {
209 $metadata['min_pax'] = (int) $data['min_pax'];
210 }
211
212 if (isset($data['max_pax']) && $data['max_pax'] !== '' && $data['max_pax'] !== null) {
213 $metadata['max_pax'] = (int) $data['max_pax'];
214 }
215
216 // Set the type for traveler categories
217 $data['type'] = 'traveler_type';
218
219 // Store metadata as JSON
220 $data['metadata'] = wp_json_encode($metadata);
221
222 // Remove old field names that don't exist in ClassificationsTable
223 unset($data['age_min'], $data['age_max'], $data['pricing_mode'], $data['min_pax'], $data['max_pax']);
224
225 // Set created_by and updated_by to current user
226 $current_user_id = get_current_user_id();
227 $data['created_by'] = absint($current_user_id);
228 $data['updated_by'] = absint($current_user_id);
229
230 // Sanitize and serialize icon if it's an array
231 if (isset($data['icon'])) {
232 if (is_array($data['icon'])) {
233 $data['icon'] = yatra_normalize_icon_picker_for_storage($data['icon']);
234 $data['icon'] = maybe_serialize($data['icon']);
235 } elseif (is_string($data['icon'])) {
236 // If it's already a string, sanitize it
237 $data['icon'] = sanitize_text_field($data['icon']);
238 }
239 }
240
241 return $data;
242 }
243
244 /**
245 * Process before update
246 */
247 protected function processBeforeUpdate(int $id, array $data): array
248 {
249 // Handle field mapping for backward compatibility
250 if (isset($data['label']) && !isset($data['name'])) {
251 $data['name'] = $data['label'];
252 // Remove the old label field to prevent database errors
253 unset($data['label']);
254 }
255
256 // Sanitize name
257 if (isset($data['name'])) {
258 $data['name'] = sanitize_text_field($data['name']);
259 }
260
261 // Handle slug: preserve manually edited slug, otherwise auto-generate from name
262 $preserveSlug = isset($data['preserve_slug']) && $data['preserve_slug'] === true;
263 unset($data['preserve_slug']); // Remove flag from data array
264
265 if ($preserveSlug && isset($data['slug']) && !empty($data['slug'])) {
266 // Slug was manually edited - preserve it but ensure uniqueness
267 $data['slug'] = SlugHelper::generateUniqueFromDatabase(
268 $data['slug'],
269 'yatra_classifications',
270 'slug',
271 $id // Exclude current record when checking uniqueness
272 );
273 } elseif (!empty($data['name'])) {
274 // Auto-generate slug from name if name is provided and slug not manually edited
275 $data['slug'] = SlugHelper::generateUniqueFromDatabase(
276 $data['name'],
277 'yatra_classifications',
278 'slug',
279 $id // Exclude current record when checking uniqueness
280 );
281 } elseif (isset($data['slug'])) {
282 // If name is not provided but slug is, sanitize the slug
283 $data['slug'] = SlugHelper::generate($data['slug']);
284 }
285
286 // Sanitize description
287 if (isset($data['description'])) {
288 $data['description'] = sanitize_textarea_field($data['description']);
289 }
290
291 // Sanitize status
292 if (isset($data['status'])) {
293 $allowed_statuses = ['draft', 'publish', 'trash'];
294 $data['status'] = in_array($data['status'], $allowed_statuses, true)
295 ? $data['status']
296 : 'draft';
297 }
298
299 // Get existing metadata to merge with new data
300 $existing = $this->repository->find($id);
301 $metadata = [];
302 if ($existing && !empty($existing->metadata)) {
303 $metadata = json_decode($existing->metadata, true) ?: [];
304 }
305
306 // Update metadata fields
307 if (isset($data['age_min'])) {
308 if ($data['age_min'] === '' || $data['age_min'] === null) {
309 unset($metadata['age_min']);
310 } else {
311 $metadata['age_min'] = absint($data['age_min']);
312 }
313 }
314
315 if (isset($data['age_max'])) {
316 if ($data['age_max'] === '' || $data['age_max'] === null) {
317 unset($metadata['age_max']);
318 } else {
319 $metadata['age_max'] = absint($data['age_max']);
320 }
321 }
322
323 if (isset($data['pricing_mode'])) {
324 $allowed_pricing_modes = ['per_person', 'per_group'];
325 if (in_array($data['pricing_mode'], $allowed_pricing_modes, true)) {
326 $metadata['pricing_mode'] = $data['pricing_mode'];
327 } else {
328 $metadata['pricing_mode'] = 'per_person';
329 }
330 }
331
332 if (isset($data['min_pax'])) {
333 if ($data['min_pax'] === '' || $data['min_pax'] === null) {
334 unset($metadata['min_pax']);
335 } else {
336 $metadata['min_pax'] = absint($data['min_pax']);
337 }
338 }
339
340 if (isset($data['max_pax'])) {
341 if ($data['max_pax'] === '' || $data['max_pax'] === null) {
342 unset($metadata['max_pax']);
343 } else {
344 $metadata['max_pax'] = absint($data['max_pax']);
345 }
346 }
347
348 // Store updated metadata as JSON
349 $data['metadata'] = wp_json_encode($metadata);
350
351 // Remove old field names that don't exist in ClassificationsTable
352 unset($data['age_min'], $data['age_max'], $data['pricing_mode'], $data['min_pax'], $data['max_pax']);
353
354 // Sanitize and serialize icon if it's an array
355 if (isset($data['icon'])) {
356 if (is_array($data['icon'])) {
357 if ($data['icon']['type'] === 'image' && !empty($data['icon']['value'])) {
358 $value = $data['icon']['value'];
359 if (filter_var($value, FILTER_VALIDATE_URL)) {
360 $attachment_id = attachment_url_to_postid($value);
361 if ($attachment_id) {
362 $data['icon']['value'] = $attachment_id;
363 }
364 } elseif (is_numeric($value)) {
365 $data['icon']['value'] = (int) $value;
366 }
367 }
368 $data['icon'] = yatra_normalize_icon_picker_for_storage($data['icon']);
369 $data['icon'] = maybe_serialize($data['icon']);
370 } elseif (is_string($data['icon'])) {
371 $data['icon'] = sanitize_text_field($data['icon']);
372 }
373 }
374
375 // Set updated_by to current user
376 $data['updated_by'] = absint(get_current_user_id());
377
378 return $data;
379 }
380
381 /**
382 * Get status counts for admin list views
383 *
384 * Provides stable counts for All / Published / Draft / Trash that
385 * do not change when filters (status/search) are applied in the UI.
386 */
387 public function getStatusCounts(): array
388 {
389 // All statuses combined (no status filter)
390 $all = $this->count([]);
391
392 // Individual statuses
393 $publish = $this->count(['status' => 'publish']);
394 $draft = $this->count(['status' => 'draft']);
395 $trash = $this->count(['status' => 'trash']);
396
397 return [
398 'all' => (int) $all,
399 'publish' => (int) $publish,
400 'draft' => (int) $draft,
401 'trash' => (int) $trash,
402 ];
403 }
404
405 /**
406 * Get all items with search and filters
407 */
408 public function getAll(array $args = []): array
409 {
410 // Sanitize and handle search
411 if (!empty($args['search'])) {
412 $search = sanitize_text_field($args['search']);
413 return $this->repository->search($search, $args);
414 }
415
416 // Sanitize and handle status filter
417 if (!empty($args['status']) && $args['status'] !== 'all') {
418 $allowed_statuses = ['draft', 'publish', 'trash'];
419 $status = in_array($args['status'], $allowed_statuses, true)
420 ? $args['status']
421 : null;
422 if ($status) {
423 $args['where']['status'] = $status;
424 }
425 }
426
427 return $this->repository->all($args);
428 }
429
430 /**
431 * Get published categories
432 */
433 public function getPublished(array $args = []): array
434 {
435 $args['where']['status'] = 'publish';
436 return $this->repository->all($args);
437 }
438
439 /**
440 * Count items
441 */
442 public function count(array $args = []): int
443 {
444 // Sanitize and handle search
445 if (!empty($args['search'])) {
446 $search = sanitize_text_field($args['search']);
447 $items = $this->repository->search($search, $args);
448 return count($items);
449 }
450
451 // Sanitize and handle status filter
452 if (!empty($args['status']) && $args['status'] !== 'all') {
453 $allowed_statuses = ['draft', 'publish', 'trash'];
454 $status = in_array($args['status'], $allowed_statuses, true)
455 ? $args['status']
456 : null;
457 if ($status) {
458 $args['where']['status'] = $status;
459 }
460 }
461
462 return $this->repository->count($args);
463 }
464 }
465
466