PluginProbe
Yatra – Travel Booking & Tour Operator Software / trunk
Yatra – Travel Booking & Tour Operator Software vtrunk
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 trunk, at app/Services/ActivityService.php

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