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 / Repositories / DestinationRepository.php

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

385 lines 12.5 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
13 /**
14 * Destination Repository
15 * Handles database operations for destinations using the new ClassificationsTable
16 */
17 class DestinationRepository extends BaseRepository
18 {
19 /**
20 * Rich text fields specific to destinations
21 */
22 protected array $richTextFields = ['description'];
23
24 /**
25 * Integer fields specific to destinations
26 */
27 protected array $integerFields = ['id', 'created_by', 'updated_by'];
28
29 /**
30 * JSON fields specific to destinations
31 */
32 protected array $jsonFields = ['metadata'];
33
34 /**
35 * Get table name - using the new ClassificationsTable
36 */
37 protected function getTableName(): string
38 {
39 return ClassificationsTable::getTableName();
40 }
41
42 /**
43 * Find by slug - for destinations
44 */
45 public function findBySlug(string $slug): ?\stdClass
46 {
47 $table = esc_sql($this->table);
48 $result = $this->wpdb->get_row(
49 $this->wpdb->prepare(
50 "SELECT * FROM `{$table}` WHERE type = %s AND slug = %s",
51 ClassificationTypes::DESTINATION,
52 $slug
53 )
54 );
55
56 return $result ?: null;
57 }
58
59 /**
60 * Get published destinations
61 */
62 public function getPublished(array $args = []): array
63 {
64 $args['where']['type'] = ClassificationTypes::DESTINATION;
65 $args['where']['status'] = 'publish';
66 return $this->all($args);
67 }
68
69 /**
70 * Get destinations by status
71 */
72 public function getByStatus(string $status, array $args = []): array
73 {
74 $args['where']['type'] = ClassificationTypes::DESTINATION;
75 $args['where']['status'] = $status;
76 return $this->all($args);
77 }
78
79 /**
80 * Get published destinations with attached trip counts.
81 *
82 * This uses the new TripClassificationsTable relation table to count how many
83 * trips are linked to each destination. It returns each destination row
84 * plus a numeric trips_count property.
85 */
86 public function getPublishedWithTripCounts(): array
87 {
88 global $wpdb;
89
90 $destTable = esc_sql($this->table);
91 $relTable = TripClassificationsTable::getTableName();
92 $tripsTable = TripsTable::getTableName();
93 $reviewsTable = ReviewsTable::getTableName();
94
95 // COUNT(DISTINCT tc.trip_id) gives real number of trips per destination.
96 // avg_rating is computed from approved reviews across all those trips.
97 // starting_price is computed in PHP using both regular trip prices and
98 // traveler-based pricing from recurring availability rules.
99 $sql = "SELECT d.*,
100 COUNT(DISTINCT tc.trip_id) AS trips_count,
101 COALESCE(AVG(r.rating), 0) AS avg_rating,
102 GROUP_CONCAT(DISTINCT tc.trip_id) AS trip_ids
103 FROM `{$destTable}` d
104 LEFT JOIN `{$relTable}` tc
105 ON tc.classification_id = d.id
106 AND tc.classification_type = %s
107 LEFT JOIN `{$tripsTable}` t
108 ON t.id = tc.trip_id
109 LEFT JOIN `{$reviewsTable}` r
110 ON r.trip_id = t.id AND r.status = 'approved'
111 WHERE d.type = %s AND d.status = 'publish'
112 GROUP BY d.id";
113
114 $rows = $this->wpdb->get_results($this->wpdb->prepare($sql, ClassificationTypes::DESTINATION, ClassificationTypes::DESTINATION)) ?: [];
115
116 if (empty($rows)) {
117 return [];
118 }
119
120 foreach ($rows as $row) {
121 $row->starting_price = $this->computeStartingPriceForTripIds($row->trip_ids ?? '');
122 }
123
124 return $rows;
125 }
126
127 /**
128 * Compute the minimum effective starting price across a set of trip IDs.
129 *
130 * This looks at both regular trip pricing (sale/discounted/original) and
131 * traveler-based pricing defined in recurring availability rules.
132 */
133 private function computeStartingPriceForTripIds(string $tripIdsCsv): float
134 {
135 if (trim($tripIdsCsv) === '') {
136 return 0.0;
137 }
138
139 $tripIds = array_filter(array_map('intval', explode(',', $tripIdsCsv)));
140 if (empty($tripIds)) {
141 return 0.0;
142 }
143
144 $minPrice = null;
145 foreach ($tripIds as $tripId) {
146 $price = $this->getEffectiveTripBasePrice($tripId);
147 if ($price > 0 && ($minPrice === null || $price < $minPrice)) {
148 $minPrice = $price;
149 }
150 }
151
152 return $minPrice ?? 0.0;
153 }
154
155 /**
156 * Get an effective base price for a single trip.
157 *
158 * Priority:
159 * 1) Trip-level sale/discounted/original price (if any > 0)
160 * 2) Traveler-based pricing from active recurring availability rules
161 * (minimum effective traveler price or rule-level sale/original).
162 */
163 private function getEffectiveTripBasePrice(int $tripId): float
164 {
165 global $wpdb;
166
167 if ($tripId <= 0) {
168 return 0.0;
169 }
170
171 $tripsTable = TripsTable::getTableName();
172
173 $trip = $wpdb->get_row(
174 $wpdb->prepare(
175 "SELECT id, sale_price, discounted_price, original_price FROM `{$tripsTable}` WHERE id = %d",
176 $tripId
177 )
178 );
179
180 if (!$trip) {
181 return 0.0;
182 }
183
184 // Use centralized TripPricingService for trip-level pricing
185 $tripEffective = \Yatra\Services\TripPricingService::getEffectivePrice($trip);
186 if ($tripEffective > 0) {
187 return $tripEffective;
188 }
189
190 $candidates = [];
191
192 // Fallback 1: look at traveler-based pricing from recurring availability rules
193 $rulesRepo = new RecurringAvailabilityRepository();
194 $rules = $rulesRepo->findByTripId($tripId, ['status' => 'active']);
195
196 foreach ($rules as $rule) {
197 // Rule-level sale/original
198 if (!empty($rule->sale_price) && (float) $rule->sale_price > 0) {
199 $candidates[] = (float) $rule->sale_price;
200 }
201 if (!empty($rule->original_price) && (float) $rule->original_price > 0) {
202 $candidates[] = (float) $rule->original_price;
203 }
204
205 // Traveler pricing on the rule itself
206 if (!empty($rule->traveler_pricing) && is_array($rule->traveler_pricing)) {
207 foreach ($rule->traveler_pricing as $pricing) {
208 if (!empty($pricing['effective_price']) && (float) $pricing['effective_price'] > 0) {
209 $candidates[] = (float) $pricing['effective_price'];
210 }
211 }
212 }
213
214 // Traveler pricing nested in time_slots
215 if (!empty($rule->time_slots) && is_array($rule->time_slots)) {
216 foreach ($rule->time_slots as $slot) {
217 if (empty($slot['traveler_pricing']) || !is_array($slot['traveler_pricing'])) {
218 continue;
219 }
220 foreach ($slot['traveler_pricing'] as $pricing) {
221 if (!empty($pricing['effective_price']) && (float) $pricing['effective_price'] > 0) {
222 $candidates[] = (float) $pricing['effective_price'];
223 }
224 }
225 }
226 }
227 }
228
229 if (empty($candidates)) {
230 return 0.0;
231 }
232
233 return (float) min($candidates);
234 }
235
236 /**
237 * Search destinations
238 */
239 public function search(string $search, array $args = []): array
240 {
241 $table = esc_sql($this->table);
242 $where = $this->buildWhereClause($args);
243 $order = $this->buildOrderClause($args);
244 $limit = $this->buildLimitClause($args);
245
246 $search_where = $this->wpdb->prepare(
247 "WHERE type = %s AND (name LIKE %s OR slug LIKE %s OR description LIKE %s)",
248 ClassificationTypes::DESTINATION,
249 '%' . $this->wpdb->esc_like($search) . '%',
250 '%' . $this->wpdb->esc_like($search) . '%',
251 '%' . $this->wpdb->esc_like($search) . '%'
252 );
253
254 if ($where) {
255 $search_where .= ' AND ' . str_replace('WHERE ', '', $where);
256 }
257
258 $query = "SELECT * FROM `{$table}` {$search_where} {$order} {$limit}";
259 return $this->wpdb->get_results($query) ?: [];
260 }
261
262 /**
263 * Get status counts for destinations
264 */
265 public function getStatusCounts(array $args = []): array
266 {
267 $table = esc_sql($this->table);
268
269 // Get counts for each status - only for destinations
270 $results = $this->wpdb->get_results($this->wpdb->prepare("
271 SELECT status, COUNT(*) as count
272 FROM `{$table}`
273 WHERE type = %s
274 GROUP BY status
275 ", ClassificationTypes::DESTINATION), ARRAY_A) ?: [];
276
277 $counts = [
278 'publish' => 0,
279 'draft' => 0,
280 'trash' => 0,
281 'total' => 0
282 ];
283
284 foreach ($results as $row) {
285 $status = $row['status'];
286 $count = (int) $row['count'];
287
288 // Map old status values to new ones
289 if ($status === 'active') {
290 $status = 'publish';
291 } elseif ($status === 'inactive') {
292 $status = 'trash';
293 }
294
295 if (isset($counts[$status])) {
296 $counts[$status] += $count;
297 $counts['total'] += $count;
298 } else {
299 // Handle any unexpected statuses
300 $counts['total'] += $count;
301 }
302 }
303
304 // Ensure we have entries for all main statuses even if count is 0
305 $counts['publish'] = $counts['publish'] ?? 0;
306 $counts['draft'] = $counts['draft'] ?? 0;
307 $counts['trash'] = $counts['trash'] ?? 0;
308
309
310 return $counts;
311 }
312
313 /**
314 * Override base all() method to ensure type filtering
315 */
316 public function all(array $args = []): array
317 {
318 // IMPORTANT: Always filter by type = 'destination' for destinations
319 $args['where']['type'] = ClassificationTypes::DESTINATION;
320 return parent::all($args);
321 }
322
323 /**
324 * Override base count() method to ensure type filtering
325 */
326 public function count(array $args = []): int
327 {
328 // IMPORTANT: Always filter by type = 'destination' for destinations
329 $args['where']['type'] = ClassificationTypes::DESTINATION;
330 return parent::count($args);
331 }
332
333 /**
334 * Get trip count for a destination
335 *
336 * @param int $destinationId Destination ID
337 * @return int Number of trips with this destination
338 */
339 public function getTripCount(int $destinationId): int
340 {
341 global $wpdb;
342 $tripRepository = new \Yatra\Repositories\TripRepository();
343 $tripsTable = $tripRepository->getTableName();
344
345 // Use TripClassificationsTable for trip-destination relationships
346 $tripDestinationsTable = TripClassificationsTable::getTableName();
347
348 return (int) $wpdb->get_var($wpdb->prepare(
349 "SELECT COUNT(DISTINCT t.id)
350 FROM `{$tripsTable}` t
351 INNER JOIN `{$tripDestinationsTable}` td ON td.trip_id = t.id
352 WHERE td.classification_id = %d
353 AND td.classification_type = %s
354 AND t.status != 'trash'",
355 $destinationId,
356 ClassificationTypes::DESTINATION
357 ));
358 }
359
360 /**
361 * Get trip count for destination (direct field method)
362 *
363 * @param int $destinationId Destination ID
364 * @return int Number of trips with this destination
365 */
366 public function getTripCountDirect(int $destinationId): int
367 {
368 global $wpdb;
369 $tripRepository = new \Yatra\Repositories\TripRepository();
370 $tripTable = $tripRepository->getTableName();
371 $tripClassificationsTable = TripClassificationsTable::getTableName();
372
373 return (int) $wpdb->get_var($wpdb->prepare(
374 "SELECT COUNT(DISTINCT t.id)
375 FROM `{$tripTable}` t
376 INNER JOIN `{$tripClassificationsTable}` tc ON tc.trip_id = t.id
377 WHERE tc.classification_id = %d
378 AND tc.classification_type = %s
379 AND t.status != 'trash'",
380 $destinationId,
381 ClassificationTypes::DESTINATION
382 ));
383 }
384 }
385