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
yatra / app / Services / TravelerCategoryService.php

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

488 lines 17.4 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 // "Charge for additional groups" needs a maximum group size to know
150 // how big one group (one priced block) is.
151 if (
152 ($data['group_overflow'] ?? 'block') === 'per_block' &&
153 (!isset($data['max_pax']) || $data['max_pax'] === '' || $data['max_pax'] === null || (int) $data['max_pax'] <= 0)
154 ) {
155 throw new \InvalidArgumentException('Maximum group size is required to charge for additional groups');
156 }
157 }
158 }
159
160 /**
161 * Process before create
162 */
163 protected function processBeforeCreate(array $data): array
164 {
165 // Handle field mapping for backward compatibility
166 if (isset($data['label']) && !isset($data['name'])) {
167 $data['name'] = $data['label'];
168 // Remove the old label field to prevent database errors
169 unset($data['label']);
170 }
171
172 // Sanitize name
173 if (isset($data['name'])) {
174 $data['name'] = sanitize_text_field($data['name']);
175 }
176
177 // Always auto-generate slug from name (backend ensures uniqueness)
178 if (!empty($data['name'])) {
179 $data['slug'] = SlugHelper::generateUniqueFromDatabase(
180 $data['name'],
181 'yatra_classifications',
182 'slug'
183 );
184 } elseif (isset($data['slug'])) {
185 // If name is empty but slug is provided, sanitize it
186 $data['slug'] = SlugHelper::generate($data['slug']);
187 }
188
189 // Sanitize description
190 if (isset($data['description'])) {
191 $data['description'] = sanitize_textarea_field($data['description']);
192 }
193
194 // Sanitize status
195 if (isset($data['status'])) {
196 $allowed_statuses = ['draft', 'publish', 'trash'];
197 $data['status'] = in_array($data['status'], $allowed_statuses, true)
198 ? $data['status']
199 : 'draft';
200 }
201
202 // Prepare metadata for traveler type
203 $metadata = [];
204
205 if (isset($data['age_min']) && $data['age_min'] !== '' && $data['age_min'] !== null) {
206 $metadata['age_min'] = (int) $data['age_min'];
207 }
208
209 if (isset($data['age_max']) && $data['age_max'] !== '' && $data['age_max'] !== null) {
210 $metadata['age_max'] = (int) $data['age_max'];
211 }
212
213 if (isset($data['pricing_mode'])) {
214 $metadata['pricing_mode'] = sanitize_text_field($data['pricing_mode']);
215 }
216
217 if (isset($data['min_pax']) && $data['min_pax'] !== '' && $data['min_pax'] !== null) {
218 $metadata['min_pax'] = (int) $data['min_pax'];
219 }
220
221 if (isset($data['max_pax']) && $data['max_pax'] !== '' && $data['max_pax'] !== null) {
222 $metadata['max_pax'] = (int) $data['max_pax'];
223 }
224
225 if (isset($data['group_overflow'])) {
226 $metadata['group_overflow'] = in_array($data['group_overflow'], ['block', 'per_block'], true)
227 ? $data['group_overflow']
228 : 'block';
229 }
230
231 // Set the type for traveler categories
232 $data['type'] = 'traveler_type';
233
234 // Store metadata as JSON
235 $data['metadata'] = wp_json_encode($metadata);
236
237 // Remove old field names that don't exist in ClassificationsTable
238 unset($data['age_min'], $data['age_max'], $data['pricing_mode'], $data['min_pax'], $data['max_pax'], $data['group_overflow']);
239
240 // Set created_by and updated_by to current user
241 $current_user_id = get_current_user_id();
242 $data['created_by'] = absint($current_user_id);
243 $data['updated_by'] = absint($current_user_id);
244
245 // Sanitize and serialize icon if it's an array
246 if (isset($data['icon'])) {
247 if (is_array($data['icon'])) {
248 $data['icon'] = yatra_normalize_icon_picker_for_storage($data['icon']);
249 $data['icon'] = maybe_serialize($data['icon']);
250 } elseif (is_string($data['icon'])) {
251 // If it's already a string, sanitize it
252 $data['icon'] = sanitize_text_field($data['icon']);
253 }
254 }
255
256 return $data;
257 }
258
259 /**
260 * Process before update
261 */
262 protected function processBeforeUpdate(int $id, array $data): array
263 {
264 // Handle field mapping for backward compatibility
265 if (isset($data['label']) && !isset($data['name'])) {
266 $data['name'] = $data['label'];
267 // Remove the old label field to prevent database errors
268 unset($data['label']);
269 }
270
271 // Sanitize name
272 if (isset($data['name'])) {
273 $data['name'] = sanitize_text_field($data['name']);
274 }
275
276 // Handle slug: preserve manually edited slug, otherwise auto-generate from name
277 $preserveSlug = isset($data['preserve_slug']) && $data['preserve_slug'] === true;
278 unset($data['preserve_slug']); // Remove flag from data array
279
280 if ($preserveSlug && isset($data['slug']) && !empty($data['slug'])) {
281 // Slug was manually edited - preserve it but ensure uniqueness
282 $data['slug'] = SlugHelper::generateUniqueFromDatabase(
283 $data['slug'],
284 'yatra_classifications',
285 'slug',
286 $id // Exclude current record when checking uniqueness
287 );
288 } elseif (!empty($data['name'])) {
289 // Auto-generate slug from name if name is provided and slug not manually edited
290 $data['slug'] = SlugHelper::generateUniqueFromDatabase(
291 $data['name'],
292 'yatra_classifications',
293 'slug',
294 $id // Exclude current record when checking uniqueness
295 );
296 } elseif (isset($data['slug'])) {
297 // If name is not provided but slug is, sanitize the slug
298 $data['slug'] = SlugHelper::generate($data['slug']);
299 }
300
301 // Sanitize description
302 if (isset($data['description'])) {
303 $data['description'] = sanitize_textarea_field($data['description']);
304 }
305
306 // Sanitize status
307 if (isset($data['status'])) {
308 $allowed_statuses = ['draft', 'publish', 'trash'];
309 $data['status'] = in_array($data['status'], $allowed_statuses, true)
310 ? $data['status']
311 : 'draft';
312 }
313
314 // Get existing metadata to merge with new data
315 $existing = $this->repository->find($id);
316 $metadata = [];
317 if ($existing && !empty($existing->metadata)) {
318 $metadata = json_decode($existing->metadata, true) ?: [];
319 }
320
321 // Update metadata fields
322 if (isset($data['age_min'])) {
323 if ($data['age_min'] === '' || $data['age_min'] === null) {
324 unset($metadata['age_min']);
325 } else {
326 $metadata['age_min'] = absint($data['age_min']);
327 }
328 }
329
330 if (isset($data['age_max'])) {
331 if ($data['age_max'] === '' || $data['age_max'] === null) {
332 unset($metadata['age_max']);
333 } else {
334 $metadata['age_max'] = absint($data['age_max']);
335 }
336 }
337
338 if (isset($data['pricing_mode'])) {
339 $allowed_pricing_modes = ['per_person', 'per_group'];
340 if (in_array($data['pricing_mode'], $allowed_pricing_modes, true)) {
341 $metadata['pricing_mode'] = $data['pricing_mode'];
342 } else {
343 $metadata['pricing_mode'] = 'per_person';
344 }
345 }
346
347 if (isset($data['min_pax'])) {
348 if ($data['min_pax'] === '' || $data['min_pax'] === null) {
349 unset($metadata['min_pax']);
350 } else {
351 $metadata['min_pax'] = absint($data['min_pax']);
352 }
353 }
354
355 if (isset($data['max_pax'])) {
356 if ($data['max_pax'] === '' || $data['max_pax'] === null) {
357 unset($metadata['max_pax']);
358 } else {
359 $metadata['max_pax'] = absint($data['max_pax']);
360 }
361 }
362
363 if (isset($data['group_overflow'])) {
364 $metadata['group_overflow'] = in_array($data['group_overflow'], ['block', 'per_block'], true)
365 ? $data['group_overflow']
366 : 'block';
367 }
368
369 // Store updated metadata as JSON (existing metadata read above, so
370 // untouched keys are preserved — only the keys present in $data change).
371 $data['metadata'] = wp_json_encode($metadata);
372
373 // Remove old field names that don't exist in ClassificationsTable
374 unset($data['age_min'], $data['age_max'], $data['pricing_mode'], $data['min_pax'], $data['max_pax'], $data['group_overflow']);
375
376 // Sanitize and serialize icon if it's an array
377 if (isset($data['icon'])) {
378 if (is_array($data['icon'])) {
379 if ($data['icon']['type'] === 'image' && !empty($data['icon']['value'])) {
380 $value = $data['icon']['value'];
381 if (filter_var($value, FILTER_VALIDATE_URL)) {
382 $attachment_id = attachment_url_to_postid($value);
383 if ($attachment_id) {
384 $data['icon']['value'] = $attachment_id;
385 }
386 } elseif (is_numeric($value)) {
387 $data['icon']['value'] = (int) $value;
388 }
389 }
390 $data['icon'] = yatra_normalize_icon_picker_for_storage($data['icon']);
391 $data['icon'] = maybe_serialize($data['icon']);
392 } elseif (is_string($data['icon'])) {
393 $data['icon'] = sanitize_text_field($data['icon']);
394 }
395 }
396
397 // Set updated_by to current user
398 $data['updated_by'] = absint(get_current_user_id());
399
400 return $data;
401 }
402
403 /**
404 * Get status counts for admin list views
405 *
406 * Provides stable counts for All / Published / Draft / Trash that
407 * do not change when filters (status/search) are applied in the UI.
408 */
409 public function getStatusCounts(): array
410 {
411 // All statuses combined (no status filter)
412 $all = $this->count([]);
413
414 // Individual statuses
415 $publish = $this->count(['status' => 'publish']);
416 $draft = $this->count(['status' => 'draft']);
417 $trash = $this->count(['status' => 'trash']);
418
419 return [
420 'all' => (int) $all,
421 'publish' => (int) $publish,
422 'draft' => (int) $draft,
423 'trash' => (int) $trash,
424 ];
425 }
426
427 /**
428 * Get all items with search and filters
429 */
430 public function getAll(array $args = []): array
431 {
432 // Sanitize and handle search
433 if (!empty($args['search'])) {
434 $search = sanitize_text_field($args['search']);
435 return $this->repository->search($search, $args);
436 }
437
438 // Sanitize and handle status filter
439 if (!empty($args['status']) && $args['status'] !== 'all') {
440 $allowed_statuses = ['draft', 'publish', 'trash'];
441 $status = in_array($args['status'], $allowed_statuses, true)
442 ? $args['status']
443 : null;
444 if ($status) {
445 $args['where']['status'] = $status;
446 }
447 }
448
449 return $this->repository->all($args);
450 }
451
452 /**
453 * Get published categories
454 */
455 public function getPublished(array $args = []): array
456 {
457 $args['where']['status'] = 'publish';
458 return $this->repository->all($args);
459 }
460
461 /**
462 * Count items
463 */
464 public function count(array $args = []): int
465 {
466 // Sanitize and handle search
467 if (!empty($args['search'])) {
468 $search = sanitize_text_field($args['search']);
469 $items = $this->repository->search($search, $args);
470 return count($items);
471 }
472
473 // Sanitize and handle status filter
474 if (!empty($args['status']) && $args['status'] !== 'all') {
475 $allowed_statuses = ['draft', 'publish', 'trash'];
476 $status = in_array($args['status'], $allowed_statuses, true)
477 ? $args['status']
478 : null;
479 if ($status) {
480 $args['where']['status'] = $status;
481 }
482 }
483
484 return $this->repository->count($args);
485 }
486 }
487
488