PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.3
Yatra – Travel Booking & Tour Operator Software v3.0.3
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 2.0.11 All 82 releases
yatra / app / Services / ActivityService.php

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

445 lines 15.2 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\ActivityRepository;
8 use Yatra\Helpers\SlugHelper;
9 use Yatra\Helpers\FormatHelper;
10 use Yatra\Database\Tables\ClassificationsTable;
11
12 /**
13 * Activity Service
14 * Contains business logic for activities
15 */
16 class ActivityService extends BaseService
17 {
18 /**
19 * @var ActivityRepository
20 */
21 private ActivityRepository $repository;
22
23 /**
24 * Constructor
25 */
26 public function __construct()
27 {
28 $this->repository = new ActivityRepository();
29 }
30
31 /**
32 * Get repository
33 */
34 protected function getRepository(): ActivityRepository
35 {
36 return $this->repository;
37 }
38
39 /**
40 * Validate activity data
41 */
42 protected function validate(array $data, ?int $id = null): void
43 {
44 if (empty($data['name'])) {
45 throw new \InvalidArgumentException('Activity name is required');
46 }
47
48 // Slug will be auto-generated from name, so we don't need to validate it here
49 // The SlugHelper will ensure uniqueness
50
51 // Validate status - accept both old and new values for backward compatibility
52 $allowed_statuses = ['draft', 'active', 'inactive', 'publish', 'trash'];
53 if (isset($data['status']) && !in_array($data['status'], $allowed_statuses, true)) {
54 throw new \InvalidArgumentException('Invalid status. Must be one of: ' . implode(', ', $allowed_statuses));
55 }
56 }
57
58 /**
59 * Process before create
60 */
61 protected function processBeforeCreate(array $data): array
62 {
63 // Set the type to 'activity' for the ClassificationsTable
64 $data['type'] = 'activity';
65
66 // Sanitize name
67 if (isset($data['name'])) {
68 $data['name'] = sanitize_text_field($data['name']);
69 }
70
71 // Always auto-generate slug from name (backend ensures uniqueness)
72 if (!empty($data['name'])) {
73 $data['slug'] = SlugHelper::generateUniqueFromDatabase(
74 $data['name'],
75 'yatra_new_classifications',
76 'slug'
77 );
78 } elseif (isset($data['slug'])) {
79 // If name is empty but slug is provided, sanitize it
80 $data['slug'] = SlugHelper::generate($data['slug']);
81 }
82
83 // Sanitize Quill HTML description
84 if (isset($data['description'])) {
85 $data['description'] = FormatHelper::sanitizeQuillHtml($data['description']);
86 }
87
88 // Sanitize status
89 if (isset($data['status'])) {
90 // Validate status
91 $allowed_statuses = ['draft', 'publish', 'trash'];
92 $data['status'] = in_array($data['status'], $allowed_statuses, true)
93 ? $data['status']
94 : 'draft';
95 } else {
96 $data['status'] = 'draft';
97 }
98
99 // Set created_by and updated_by to current user
100 $current_user_id = get_current_user_id();
101 $data['created_by'] = absint($current_user_id);
102 $data['updated_by'] = absint($current_user_id);
103
104 // Sanitize and serialize icon if it's an array
105 if (isset($data['icon'])) {
106 if (is_array($data['icon'])) {
107 // Convert URL back to attachment ID if possible
108 if ($data['icon']['type'] === 'image' && !empty($data['icon']['value'])) {
109 $value = $data['icon']['value'];
110
111 // If it's a URL, try to find the attachment ID
112 if (filter_var($value, FILTER_VALIDATE_URL)) {
113 $attachment_id = attachment_url_to_postid($value);
114 if ($attachment_id) {
115 $data['icon']['value'] = $attachment_id;
116 }
117 }
118 // If it's already numeric, keep it as is
119 elseif (is_numeric($value)) {
120 $data['icon']['value'] = (int) $value;
121 }
122 }
123
124 $data['icon'] = yatra_normalize_icon_picker_for_storage($data['icon']);
125 $data['icon'] = maybe_serialize($data['icon']);
126 } elseif (is_string($data['icon'])) {
127 // If it's already a string, sanitize it
128 $data['icon'] = sanitize_text_field($data['icon']);
129 }
130 }
131
132 // Handle SEO metadata
133 if (isset($data['seo_title']) || isset($data['seo_description']) || isset($data['seo_keywords'])) {
134
135 $existing_metadata = [];
136 if (isset($data['metadata']) && is_array($data['metadata'])) {
137 $existing_metadata = $data['metadata'];
138 } elseif (isset($data['metadata'])) {
139 $existing_metadata = maybe_unserialize($data['metadata']);
140 }
141
142 // Add SEO fields to metadata
143 if (isset($data['seo_title'])) {
144 $existing_metadata['seo_title'] = sanitize_text_field($data['seo_title']);
145 unset($data['seo_title']); // Remove from main data
146 }
147 if (isset($data['seo_description'])) {
148 $existing_metadata['seo_description'] = sanitize_textarea_field($data['seo_description']);
149 unset($data['seo_description']); // Remove from main data
150 }
151 if (isset($data['seo_keywords'])) {
152 $existing_metadata['seo_keywords'] = sanitize_text_field($data['seo_keywords']);
153 unset($data['seo_keywords']); // Remove from main data
154 }
155
156 $data['metadata'] = $existing_metadata;
157 }
158
159 return $data;
160 }
161
162 /**
163 * Process before update
164 */
165 protected function processBeforeUpdate(int $id, array $data): array
166 {
167
168 // Remove preserve_slug flag if sent from frontend (not a database column)
169 unset($data['preserve_slug']);
170
171 // Sanitize name
172 if (isset($data['name'])) {
173 $data['name'] = sanitize_text_field($data['name']);
174 }
175
176 // Handle slug in EDIT mode: Only update if explicitly provided
177 // Do NOT auto-generate from name in edit mode
178 if (isset($data['slug']) && !empty($data['slug'])) {
179 // Slug was explicitly provided - ensure uniqueness
180 $data['slug'] = SlugHelper::generateUniqueFromDatabase(
181 $data['slug'],
182 ClassificationsTable::getTableName(),
183 'slug',
184 $id // Exclude current record when checking uniqueness
185 );
186 }
187 // If slug is not provided, don't modify it (keep existing slug)
188
189 // Sanitize Quill HTML description
190 if (isset($data['description'])) {
191 $data['description'] = FormatHelper::sanitizeQuillHtml($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 // Set updated_by to current user
203 $data['updated_by'] = absint(get_current_user_id());
204
205 // Sanitize and serialize icon if it's an array
206 if (isset($data['icon'])) {
207 if (is_array($data['icon'])) {
208 // Convert URL back to attachment ID if possible
209 if ($data['icon']['type'] === 'image' && !empty($data['icon']['value'])) {
210 $value = $data['icon']['value'];
211
212 // If it's a URL, try to find the attachment ID
213 if (filter_var($value, FILTER_VALIDATE_URL)) {
214 $attachment_id = attachment_url_to_postid($value);
215 if ($attachment_id) {
216 $data['icon']['value'] = $attachment_id;
217 }
218 }
219 // If it's already numeric, keep it as is
220 elseif (is_numeric($value)) {
221 $data['icon']['value'] = (int) $value;
222 }
223 }
224
225 $data['icon'] = yatra_normalize_icon_picker_for_storage($data['icon']);
226 $data['icon'] = maybe_serialize($data['icon']);
227 } elseif (is_string($data['icon'])) {
228 // If it's already a string, sanitize it
229 $data['icon'] = sanitize_text_field($data['icon']);
230 }
231 }
232
233 // Handle SEO metadata
234 if (isset($data['seo_title']) || isset($data['seo_description']) || isset($data['seo_keywords'])) {
235 // Get existing metadata
236 $existing_metadata = [];
237 if (isset($data['metadata']) && is_array($data['metadata'])) {
238 $existing_metadata = $data['metadata'];
239 } elseif (isset($data['metadata'])) {
240 $existing_metadata = maybe_unserialize($data['metadata']);
241 } else {
242 // Get existing metadata from database
243 $existing = $this->repository->find($id);
244 if ($existing && isset($existing->metadata)) {
245 $existing_metadata = maybe_unserialize($existing->metadata);
246 }
247 }
248
249 // Add/update SEO fields in metadata
250 if (isset($data['seo_title'])) {
251 $existing_metadata['seo_title'] = sanitize_text_field($data['seo_title']);
252 unset($data['seo_title']); // Remove from main data
253 }
254 if (isset($data['seo_description'])) {
255 $existing_metadata['seo_description'] = sanitize_textarea_field($data['seo_description']);
256 unset($data['seo_description']); // Remove from main data
257 }
258 if (isset($data['seo_keywords'])) {
259 $existing_metadata['seo_keywords'] = sanitize_text_field($data['seo_keywords']);
260 unset($data['seo_keywords']); // Remove from main data
261 }
262
263 $data['metadata'] = $existing_metadata;
264 }
265
266 return $data;
267 }
268
269 /**
270 * Get all items with search and filters
271 */
272 public function getAll(array $args = []): array
273 {
274 // IMPORTANT: Always filter by type = 'activity' for activities
275 $args['where']['type'] = 'activity';
276
277 // Sanitize and handle search
278 if (!empty($args['search'])) {
279 $search = sanitize_text_field($args['search']);
280 return $this->repository->search($search, $args);
281 }
282
283 // Sanitize and handle status filter
284 if (!empty($args['status']) && $args['status'] !== 'all') {
285 $allowed_statuses = ['draft', 'publish', 'trash'];
286 $status = in_array($args['status'], $allowed_statuses, true)
287 ? $args['status']
288 : null;
289 if ($status) {
290 $args['where']['status'] = $status;
291 }
292 }
293
294 return $this->repository->all($args);
295 }
296
297 /**
298 * Get published activities
299 */
300 public function getPublished(array $args = []): array
301 {
302 return $this->repository->getPublished($args);
303 }
304
305 /**
306 * Get published activities along with aggregated stats such as
307 * trips_count derived from related trip records.
308 */
309 public function getPublishedWithStats(): array
310 {
311 // For now this simply delegates to a repository method that attaches
312 // trips_count using the yatra_trip_activities relation table.
313 return $this->repository->getPublishedWithTripCounts();
314 }
315
316 /**
317 * Count items
318 */
319 public function count(array $args = []): int
320 {
321 // IMPORTANT: Always filter by type = 'activity' for activities
322 $args['where']['type'] = 'activity';
323
324 // Sanitize and handle search
325 if (!empty($args['search'])) {
326 $search = sanitize_text_field($args['search']);
327 $items = $this->repository->search($search, $args);
328 return count($items);
329 }
330
331 // Sanitize and handle status filter
332 if (!empty($args['status']) && $args['status'] !== 'all') {
333 $allowed_statuses = ['draft', 'publish', 'trash'];
334 $status = in_array($args['status'], $allowed_statuses, true)
335 ? $args['status']
336 : null;
337 if ($status) {
338 $args['where']['status'] = $status;
339 }
340 }
341
342 return $this->repository->count($args);
343 }
344
345 /**
346 * Bulk update status
347 */
348 public function bulkUpdateStatus(array $ids, string $status): array
349 {
350 $ids = array_filter(array_map('absint', $ids));
351 if (empty($ids)) {
352 throw new \InvalidArgumentException(__('No activities selected.', 'yatra'));
353 }
354
355 $updated = 0;
356 foreach ($ids as $id) {
357 if ($this->repository->update($id, ['status' => $status])) {
358 $updated++;
359 }
360 }
361
362 return [
363 'updated' => $updated,
364 'total' => count($ids),
365 'message' => sprintf(
366 /* translators: %d number of activities */
367 _n('%d activity updated.', '%d activities updated.', $updated, 'yatra'),
368 $updated
369 )
370 ];
371 }
372
373 /**
374 * Bulk delete permanently
375 */
376 public function bulkDelete(array $ids): array
377 {
378 $ids = array_filter(array_map('absint', $ids));
379 if (empty($ids)) {
380 throw new \InvalidArgumentException(__('No activities selected.', 'yatra'));
381 }
382
383 $result = $this->repository->bulkDelete($ids);
384 if (!$result) {
385 throw new \Exception(__('Failed to delete activities.', 'yatra'));
386 }
387
388 return [
389 'message' => sprintf(
390 /* translators: %d number of activities */
391 _n('%d activity deleted.', '%d activities deleted.', count($ids), 'yatra'),
392 count($ids)
393 ),
394 ];
395 }
396
397 /**
398 * Get status counts for list views
399 */
400 public function getStatusCounts(): array
401 {
402 $counts = $this->repository->getStatusCounts();
403
404 // Debug: Log the repository counts
405 $publish = $counts['publish'] ?? 0;
406 $draft = $counts['draft'] ?? 0;
407 $trash = $counts['trash'] ?? 0;
408
409 // Calculate total from all statuses, not just the main three
410 $all = $counts['total'] ?? 0;
411
412 $result = [
413 'all' => (int) $all,
414 'publish' => (int) $publish,
415 'draft' => (int) $draft,
416 'trash' => (int) $trash,
417 ];
418
419 // Add legacy status keys for backward compatibility
420 $result['active'] = $result['publish'];
421 $result['inactive'] = $result['trash'];
422
423 return $result;
424 }
425
426 /**
427 * Get trip count for an activity
428 */
429 public function getTripCount(int $activityId): int
430 {
431 $activityRepository = new \Yatra\Repositories\ActivityRepository();
432 return $activityRepository->getTripCount($activityId);
433 }
434
435 /**
436 * Get trip count for activity (direct field method)
437 */
438 public function getTripCountDirect(int $activityId): int
439 {
440 $activityRepository = new \Yatra\Repositories\ActivityRepository();
441 return $activityRepository->getTripCountDirect($activityId);
442 }
443 }
444
445