PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.7
Yatra – Travel Booking & Tour Operator Software v3.0.7
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 / Repositories / ActivityRepository.php

ActivityRepository.php in Yatra – Travel Booking & Tour Operator Software 3.0.7, at app/Repositories/ActivityRepository.php

402 lines 13.7 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\Repositories;
6
7 use Yatra\Constants\ClassificationTypes;
8 use Yatra\Database\Tables\ClassificationsTable;
9 use Yatra\Database\Tables\TripClassificationsTable;
10 use Yatra\Database\Tables\TripsTable;
11 use Yatra\Database\Tables\ReviewsTable;
12 use Yatra\Utils\Cache;
13 use Yatra\Utils\QueryCache;
14
15 /**
16 * Activity Repository
17 * Handles database operations for activities using the new ClassificationsTable
18 */
19 class ActivityRepository extends BaseRepository
20 {
21 /**
22 * Rich text fields specific to activities
23 */
24 protected array $richTextFields = ['description'];
25
26 /**
27 * Integer fields specific to activities
28 */
29 protected array $integerFields = ['id', 'created_by', 'updated_by'];
30
31 /**
32 * JSON fields specific to activities
33 */
34 protected array $jsonFields = ['metadata'];
35
36 /**
37 * Get table name - using the new ClassificationsTable
38 */
39 protected function getTableName(): string
40 {
41 return ClassificationsTable::getTableName();
42 }
43
44
45 /**
46 * Find by slug - for activities
47 */
48 public function findBySlug(string $slug): ?\stdClass
49 {
50 $table = esc_sql($this->table);
51 $result = $this->wpdb->get_row(
52 $this->wpdb->prepare(
53 "SELECT * FROM `{$table}` WHERE type = %s AND slug = %s",
54 ClassificationTypes::ACTIVITY,
55 $slug
56 )
57 );
58
59 return $result ?: null;
60 }
61
62 /**
63 * Get published activities (visible in listings and search).
64 *
65 * Accepts both `active` and `publish` status — some sites/data use either.
66 */
67 public function getPublished(array $args = []): array
68 {
69 $statuses = apply_filters('yatra_activity_published_statuses', ['active', 'publish']);
70 if (!is_array($statuses) || $statuses === []) {
71 $statuses = ['active', 'publish'];
72 }
73 $args['where']['type'] = ClassificationTypes::ACTIVITY;
74 $args['where']['status'] = $statuses;
75
76 return $this->all($args);
77 }
78
79 /**
80 * Get activities by status
81 */
82 public function getByStatus(string $status, array $args = []): array
83 {
84 $args['where']['type'] = ClassificationTypes::ACTIVITY;
85 $args['where']['status'] = $status;
86 return $this->all($args);
87 }
88
89 /**
90 * Search activities
91 */
92 public function search(string $search, array $args = []): array
93 {
94 $table = esc_sql($this->table);
95 $where = $this->buildWhereClause($args);
96 $order = $this->buildOrderClause($args);
97 $limit = $this->buildLimitClause($args);
98
99 $search_where = $this->wpdb->prepare(
100 "WHERE type = %s AND (name LIKE %s OR slug LIKE %s OR description LIKE %s)",
101 ClassificationTypes::ACTIVITY,
102 '%' . $this->wpdb->esc_like($search) . '%',
103 '%' . $this->wpdb->esc_like($search) . '%',
104 '%' . $this->wpdb->esc_like($search) . '%'
105 );
106
107 if ($where) {
108 $search_where .= ' AND ' . str_replace('WHERE ', '', $where);
109 }
110
111 $query = "SELECT * FROM `{$table}` {$search_where} {$order} {$limit}";
112
113 return $this->wpdb->get_results($query) ?: [];
114 }
115
116 /**
117 * Bulk update status for multiple activities
118 */
119 public function bulkUpdateStatus(array $ids, string $status): bool
120 {
121 if (empty($ids)) {
122 return false;
123 }
124
125 $table = esc_sql($this->table);
126 $placeholders = implode(',', array_fill(0, count($ids), '%d'));
127 $sql = "UPDATE `{$table}` SET status = %s, updated_at = %s WHERE type = %s AND id IN ({$placeholders})";
128 $params = array_merge([$status, current_time('mysql'), ClassificationTypes::ACTIVITY], $ids);
129
130 $prepared = $this->wpdb->prepare($sql, $params);
131 return $this->wpdb->query($prepared) !== false;
132 }
133
134 /**
135 * Permanently delete multiple activities
136 */
137 public function bulkDelete(array $ids): bool
138 {
139 if (empty($ids)) {
140 return false;
141 }
142
143 $table = esc_sql($this->table);
144 $placeholders = implode(',', array_fill(0, count($ids), '%d'));
145 $sql = "DELETE FROM `{$table}` WHERE type = %s AND id IN ({$placeholders})";
146
147 $prepared = $this->wpdb->prepare($sql, array_merge([ClassificationTypes::ACTIVITY], $ids));
148 return $this->wpdb->query($prepared) !== false;
149 }
150
151 /**
152 * Get published activities with trip counts and stats
153 * Uses the new ClassificationsTable and TripClassificationsTable
154 */
155 /**
156 * Wipe listing caches whenever an activity row is created /
157 * updated / deleted so {@see self::getPublishedWithTripCounts()}
158 * never serves stale aggregates. See the matching override in
159 * {@see DestinationRepository::afterWrite()} for the full reasoning
160 * — the existing `yatra_activity_*` CacheHooks listeners only fire
161 * when an action of that name is dispatched, and no caller does.
162 */
163 protected function afterWrite(string $operation, int $id, array $context = []): void
164 {
165 Cache::invalidateListingCaches();
166 }
167
168 public function getPublishedWithTripCounts(): array
169 {
170 // Cache the aggregate so repeat visits skip the GROUP BY +
171 // per-activity MIN-price subquery. Cache key sits behind the
172 // `activity_listing_` prefix wiped by
173 // {@see \Yatra\Utils\Cache::invalidateListingCaches()}, which
174 // runs whenever an activity/trip row is written (via this
175 // class's afterWrite or {@see \Yatra\Hooks\CacheHooks} on trip
176 // writes). Stale data is impossible after admin edits.
177 return $this->cacheQueryResult(
178 'activity_listing_with_trip_counts_v2',
179 function (): array {
180 return $this->fetchPublishedWithTripCounts();
181 },
182 Cache::DURATION_ACTIVITY_DATA
183 );
184 }
185
186 /**
187 * Uncached worker for {@see self::getPublishedWithTripCounts()}.
188 *
189 * @return array<int, \stdClass>
190 */
191 private function fetchPublishedWithTripCounts(): array
192 {
193 $actTable = esc_sql($this->table);
194 $relTable = TripClassificationsTable::getTableName();
195 $tripsTable = TripsTable::getTableName();
196 $reviewsTable = ReviewsTable::getTableName();
197
198 // COUNT(DISTINCT tc.trip_id) gives real number of trips per activity.
199 // avg_rating is computed from approved reviews across all those trips.
200 // starting_price is computed in PHP using both regular trip prices and
201 // traveler-based pricing from recurring availability rules.
202 // Status must match {@see getPublished()} — many sites use `publish`, not only `active`.
203 $statuses = apply_filters('yatra_activity_published_statuses', ['active', 'publish']);
204 if (!is_array($statuses) || $statuses === []) {
205 $statuses = ['active', 'publish'];
206 }
207 $statuses = array_values(array_intersect($statuses, ['active', 'publish']));
208 if ($statuses === []) {
209 $statuses = ['active', 'publish'];
210 }
211 $statusIn = implode(',', array_fill(0, count($statuses), '%s'));
212
213 $sql = "SELECT a.*,
214 COUNT(DISTINCT tc.trip_id) AS trips_count,
215 COALESCE(AVG(r.rating), 0) AS avg_rating,
216 GROUP_CONCAT(DISTINCT tc.trip_id) AS trip_ids
217 FROM `{$actTable}` a
218 LEFT JOIN `{$relTable}` tc
219 ON tc.classification_id = a.id
220 AND tc.classification_type = %s
221 LEFT JOIN `{$tripsTable}` t
222 ON t.id = tc.trip_id
223 LEFT JOIN `{$reviewsTable}` r
224 ON r.trip_id = t.id AND r.status = 'approved'
225 WHERE a.type = %s AND a.status IN ({$statusIn})
226 GROUP BY a.id";
227
228 $params = array_merge(
229 [ClassificationTypes::ACTIVITY, ClassificationTypes::ACTIVITY],
230 $statuses
231 );
232 $rows = $this->wpdb->get_results($this->wpdb->prepare($sql, ...$params)) ?: [];
233
234 if (empty($rows)) {
235 return [];
236 }
237
238 foreach ($rows as $row) {
239 $row->starting_price = $this->computeStartingPriceForTripIds($row->trip_ids ?? '');
240 }
241
242 return $rows;
243 }
244
245 /**
246 * Compute starting price for given trip IDs
247 * This method calculates the lowest price from all trips associated with an activity
248 */
249 private function computeStartingPriceForTripIds(string $tripIds): float
250 {
251 if (empty($tripIds)) {
252 return 0.0;
253 }
254
255 global $wpdb;
256 $tripIdsArray = array_filter(array_map('intval', explode(',', $tripIds)));
257
258 if (empty($tripIdsArray)) {
259 return 0.0;
260 }
261
262 $placeholders = implode(',', array_fill(0, count($tripIdsArray), '%d'));
263 $tripsTable = TripsTable::getTableName();
264
265 // Get the minimum original price from trips
266 $sql = "SELECT MIN(CAST(original_price AS DECIMAL(10,2))) as min_price
267 FROM `{$tripsTable}`
268 WHERE id IN ({$placeholders}) AND original_price > 0";
269
270 $prepared = $this->wpdb->prepare($sql, $tripIdsArray);
271 $result = $this->wpdb->get_var($prepared);
272
273 return $result ? (float) $result : 0.0;
274 }
275
276 /**
277 * Get status counts for activities
278 */
279 public function getStatusCounts(array $args = []): array
280 {
281 $table = esc_sql($this->table);
282
283 $debug_sql = "SELECT id, type, status, name FROM `{$table}` ORDER BY id";
284 $debug_results = $this->wpdb->get_results($debug_sql);
285
286 // Get counts for each status - only for activities
287 $sql = "SELECT status, COUNT(*) as count
288 FROM `{$table}`
289 WHERE type = %s
290 GROUP BY status";
291
292 $results = $this->wpdb->get_results($this->wpdb->prepare($sql, ClassificationTypes::ACTIVITY));
293 $counts = [
294 'publish' => 0,
295 'draft' => 0,
296 'trash' => 0,
297 'total' => 0
298 ];
299
300 foreach ($results as $row) {
301 $status = $row->status;
302 $count = (int) $row->count;
303
304 // Map old status values to new ones if needed
305 if ($status === 'active') {
306 $status = 'publish';
307 } elseif ($status === 'inactive') {
308 $status = 'trash';
309 }
310
311 if (isset($counts[$status])) {
312 $counts[$status] += $count;
313 $counts['total'] += $count;
314 } else {
315 // Handle any unexpected statuses
316 $counts['total'] += $count;
317 }
318 }
319
320
321 return $counts;
322 }
323
324 /**
325 * Override base all() method to ensure type filtering
326 */
327 public function all(array $args = []): array
328 {
329 // IMPORTANT: Always filter by type = 'activity' for activities
330 $args['where']['type'] = ClassificationTypes::ACTIVITY;
331 return parent::all($args);
332 }
333
334 /**
335 * Override base count() method to ensure type filtering
336 */
337 public function count(array $args = []): int
338 {
339 // IMPORTANT: Always filter by type = 'activity' for activities
340 $args['where']['type'] = ClassificationTypes::ACTIVITY;
341 return parent::count($args);
342 }
343
344 /**
345 * Get trip count for an activity
346 *
347 * @param int $activityId Activity ID
348 * @return int Number of trips with this activity
349 */
350 public function getTripCount(int $activityId): int
351 {
352 // Use QueryCache for caching activity trip counts
353 $cacheKey = Cache::KEY_ACTIVITY_TRIP_COUNT . '_' . $activityId;
354
355 // Cache backends often return strings for scalars; force int on read + write.
356 return (int) $this->cacheQueryResult($cacheKey, function () use ($activityId): int {
357 global $wpdb;
358 $tripRepository = new \Yatra\Repositories\TripRepository();
359 $tripsTable = $tripRepository->getTableName();
360
361 // Using hardcoded table name since there's no dedicated repository for trip activities
362 $tripActivitiesTable = TripClassificationsTable::getTableName();
363
364 return (int) ($wpdb->get_var($wpdb->prepare(
365 "SELECT COUNT(DISTINCT t.id)
366 FROM `{$tripsTable}` t
367 INNER JOIN `{$tripActivitiesTable}` ta ON ta.trip_id = t.id
368 WHERE ta.classification_id = %d AND ta.classification_type = %s
369 AND t.status != 'trash'",
370 $activityId,
371 ClassificationTypes::ACTIVITY
372 )) ?? 0);
373 }, Cache::DURATION_COUNTS); // Cache for 30 minutes
374 }
375
376 /**
377 * Get trip count for activity (direct field method)
378 *
379 * @param int $activityId Activity ID
380 * @return int Number of trips with this activity
381 */
382 public function getTripCountDirect(int $activityId): int
383 {
384 // Use QueryCache for caching activity trip counts
385 $cacheKey = Cache::KEY_ACTIVITY_TRIP_COUNT_DIRECT . '_' . $activityId;
386
387 // Cache backends often return strings for scalars; force int on read + write.
388 return (int) $this->cacheQueryResult($cacheKey, function () use ($activityId): int {
389 global $wpdb;
390 $tripRepository = new \Yatra\Repositories\TripRepository();
391 $tripTable = $tripRepository->getTableName();
392
393 return (int) ($wpdb->get_var($wpdb->prepare(
394 "SELECT COUNT(*)
395 FROM `{$tripTable}` t
396 WHERE t.activity_id = %d",
397 $activityId
398 )) ?? 0);
399 }, Cache::DURATION_COUNTS); // Cache for 30 minutes
400 }
401 }
402