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 / RecurringAvailabilityRepository.php

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

437 lines 13.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\Repositories;
6
7 use Yatra\Database\Tables\TripAvailabilityRulesTable;
8
9 /**
10 * Recurring Availability Repository
11 * Handles database operations for recurring availability rules
12 */
13 class RecurringAvailabilityRepository extends BaseRepository
14 {
15 private function sanitizeCoordinate($value): ?string
16 {
17 if ($value === null || $value === '') {
18 return null;
19 }
20 if (is_numeric($value)) {
21 return (string) $value;
22 }
23
24 return null;
25 }
26
27 /**
28 * Get table name
29 */
30 protected function getTableName(): string
31 {
32 return TripAvailabilityRulesTable::getTableName();
33 }
34
35 /**
36 * Find all rules by trip ID
37 */
38 public function findByTripId(int $tripId, array $filters = []): array
39 {
40 $table = esc_sql($this->table);
41 $where = ['trip_id = %d'];
42 $params = [$tripId];
43
44 // Status filter
45 if (!empty($filters['status']) && $filters['status'] !== 'all') {
46 $where[] = 'status = %s';
47 $params[] = $filters['status'];
48 }
49
50 // Rule type filter
51 if (!empty($filters['rule_type']) && $filters['rule_type'] !== 'all') {
52 $where[] = 'rule_type = %s';
53 $params[] = $filters['rule_type'];
54 }
55
56 // Search filter
57 if (!empty($filters['search'])) {
58 $where[] = '(name LIKE %s OR from_location LIKE %s OR to_location LIKE %s)';
59 $search = '%' . $this->wpdb->esc_like($filters['search']) . '%';
60 $params[] = $search;
61 $params[] = $search;
62 $params[] = $search;
63 }
64
65 $query = "SELECT * FROM `{$table}` WHERE " . implode(' AND ', $where);
66 $query .= " ORDER BY priority DESC, created_at DESC";
67
68 if (!empty($filters['per_page'])) {
69 $perPage = (int) $filters['per_page'];
70 $page = max(1, (int) ($filters['page'] ?? 1));
71 $offset = ($page - 1) * $perPage;
72 $query .= $this->wpdb->prepare(" LIMIT %d OFFSET %d", $perPage, $offset);
73 }
74
75 $results = $this->wpdb->get_results(
76 $this->wpdb->prepare($query, ...$params)
77 );
78
79 return array_map([$this, 'hydrateRule'], $results ?: []);
80 }
81
82 /**
83 * Count rules by trip ID
84 */
85 public function countByTripId(int $tripId, array $filters = []): int
86 {
87 $table = esc_sql($this->table);
88 $where = ['trip_id = %d'];
89 $params = [$tripId];
90
91 if (!empty($filters['status']) && $filters['status'] !== 'all') {
92 $where[] = 'status = %s';
93 $params[] = $filters['status'];
94 }
95
96 if (!empty($filters['rule_type']) && $filters['rule_type'] !== 'all') {
97 $where[] = 'rule_type = %s';
98 $params[] = $filters['rule_type'];
99 }
100
101 $query = "SELECT COUNT(*) FROM `{$table}` WHERE " . implode(' AND ', $where);
102
103 return (int) $this->wpdb->get_var(
104 $this->wpdb->prepare($query, ...$params)
105 );
106 }
107
108 /**
109 * Get status counts for recurring rules by trip ID
110 */
111 public function getStatusCounts(array $args = []): array
112 {
113 // Extract trip ID from args for backward compatibility
114 $tripId = $args['trip_id'] ?? null;
115
116 if (!$tripId) {
117 throw new \InvalidArgumentException('Trip ID is required for RecurringAvailability status counts');
118 }
119
120 $table = esc_sql($this->table);
121
122 $query = $this->wpdb->prepare(
123 "SELECT
124 COUNT(*) as all_count,
125 SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active,
126 SUM(CASE WHEN status = 'inactive' THEN 1 ELSE 0 END) as inactive
127 FROM `{$table}`
128 WHERE trip_id = %d",
129 $tripId
130 );
131
132 $result = $this->wpdb->get_row($query, ARRAY_A);
133
134 return [
135 'all' => (int) ($result['all_count'] ?? 0),
136 'active' => (int) ($result['active'] ?? 0),
137 'inactive' => (int) ($result['inactive'] ?? 0),
138 ];
139 }
140
141 /**
142 * Get active rules for a trip within a date range
143 */
144 public function getActiveRulesForDateRange(int $tripId, string $fromDate, string $toDate): array
145 {
146 $table = esc_sql($this->table);
147
148 $query = $this->wpdb->prepare(
149 "SELECT * FROM `{$table}`
150 WHERE trip_id = %d
151 AND status = 'active'
152 AND start_date <= %s
153 AND (end_date IS NULL OR end_date >= %s)
154 ORDER BY priority DESC",
155 $tripId,
156 $toDate,
157 $fromDate
158 );
159
160 $results = $this->wpdb->get_results($query);
161
162 return array_map([$this, 'hydrateRule'], $results ?: []);
163 }
164
165 /**
166 * Find active rules that apply to a specific date
167 *
168 * @param int $tripId Trip ID
169 * @param string $date Date in YYYY-MM-DD format
170 * @return array Array of matching rules ordered by priority
171 */
172 public function findActiveRulesForDate(int $tripId, string $date): array
173 {
174 $table = esc_sql($this->table);
175
176 $query = $this->wpdb->prepare(
177 "SELECT * FROM `{$table}`
178 WHERE trip_id = %d
179 AND status = 'active'
180 AND start_date <= %s
181 AND (end_date IS NULL OR end_date >= %s)
182 ORDER BY priority DESC",
183 $tripId,
184 $date,
185 $date
186 );
187
188 $results = $this->wpdb->get_results($query);
189
190 return array_map([$this, 'hydrateRule'], $results ?: []);
191 }
192
193 /**
194 * Create a new rule
195 */
196 public function create(array $data): int
197 {
198 $prepared = $this->prepareData($data);
199
200 $result = $this->wpdb->insert($this->table, $prepared);
201
202 if ($result === false) {
203 throw new \RuntimeException('Failed to create recurring rule: ' . $this->wpdb->last_error);
204 }
205
206 return (int) $this->wpdb->insert_id;
207 }
208
209 /**
210 * Update a rule
211 */
212 public function update(int $id, array $data): bool
213 {
214 $prepared = $this->prepareData($data);
215 $prepared['updated_at'] = current_time('mysql');
216
217 $result = $this->wpdb->update(
218 $this->table,
219 $prepared,
220 ['id' => $id]
221 );
222
223 return $result !== false;
224 }
225
226 /**
227 * Delete a rule
228 */
229 public function delete(int $id): bool
230 {
231 $result = $this->wpdb->delete($this->table, ['id' => $id]);
232 return $result !== false;
233 }
234
235 /**
236 * Find a rule by ID (override to hydrate data)
237 */
238 public function find(int $id, bool $includeDeleted = false): ?\stdClass
239 {
240 $result = parent::find($id, $includeDeleted);
241
242 if ($result) {
243 return $this->hydrateRule($result);
244 }
245
246 return null;
247 }
248
249 /**
250 * Prepare data for database
251 */
252 private function prepareData(array $data): array
253 {
254 $allowed = [
255 'trip_id', 'name', 'rule_type', 'days_of_week', 'week_of_month',
256 'day_of_week', 'interval_days', 'interval_start_date', 'start_date',
257 'end_date', 'excluded_dates', 'months', 'time_slots', 'original_price',
258 'sale_price', 'traveler_pricing', 'seats_total', 'alert_threshold',
259 'departure_time', 'arrival_time', 'from_location', 'to_location',
260 'from_latitude', 'from_longitude', 'to_latitude', 'to_longitude',
261 'cutoff_hours', 'advance_booking_days', 'day_overrides', 'status', 'priority',
262 ];
263
264 $prepared = [];
265
266 // Map API pricing_type to schema column price_type (enum fixed|percentage)
267 if (array_key_exists('pricing_type', $data)) {
268 $pt = $data['pricing_type'];
269 $prepared['price_type'] = ($pt === 'percentage' || $pt === 'percent') ? 'percentage' : 'fixed';
270 }
271
272 foreach ($allowed as $field) {
273 if (array_key_exists($field, $data)) {
274 $value = $data[$field];
275
276 // JSON encode array fields
277 if (in_array($field, ['excluded_dates', 'months', 'time_slots', 'day_overrides', 'traveler_pricing'], true)) {
278 if (is_array($value)) {
279 $value = wp_json_encode($value);
280 }
281 }
282
283 // Handle empty values
284 if ($value === '' || $value === null) {
285 if (in_array($field, ['end_date', 'interval_start_date', 'departure_time', 'arrival_time', 'advance_booking_days'], true)) {
286 $value = null;
287 }
288 }
289
290 if (in_array($field, ['from_latitude', 'from_longitude', 'to_latitude', 'to_longitude'], true)) {
291 $value = $this->sanitizeCoordinate($value);
292 }
293
294 $prepared[$field] = $value;
295 }
296 }
297
298 return $prepared;
299 }
300
301 /**
302 * Hydrate rule data (decode JSON fields)
303 */
304 private function hydrateRule(object $rule): object
305 {
306 // Decode JSON fields
307 if (!empty($rule->excluded_dates)) {
308 $rule->excluded_dates = json_decode($rule->excluded_dates, true) ?: [];
309 } else {
310 $rule->excluded_dates = [];
311 }
312
313 if (!empty($rule->time_slots)) {
314 $rule->time_slots = json_decode($rule->time_slots, true) ?: [];
315 } else {
316 $rule->time_slots = [];
317 }
318
319 if (!empty($rule->day_overrides)) {
320 $rule->day_overrides = json_decode($rule->day_overrides, true) ?: [];
321 } else {
322 $rule->day_overrides = [];
323 }
324
325 if (!empty($rule->traveler_pricing)) {
326 $rule->traveler_pricing = json_decode($rule->traveler_pricing, true) ?: [];
327 // Enrich traveler pricing with category labels
328 $rule->traveler_pricing = $this->enrichTravelerPricing($rule->traveler_pricing);
329 } else {
330 $rule->traveler_pricing = [];
331 }
332
333 // CapacityService reads seats_total; fall back to capacity_value when fixed capacity
334 if (empty($rule->seats_total) && !empty($rule->capacity_value)) {
335 $capType = $rule->capacity_type ?? 'fixed';
336 if ($capType === 'fixed') {
337 $rule->seats_total = (int) $rule->capacity_value;
338 }
339 }
340
341 // Also enrich time_slots traveler_pricing
342 if (!empty($rule->time_slots)) {
343 foreach ($rule->time_slots as &$slot) {
344 if (!empty($slot['traveler_pricing'])) {
345 $slot['traveler_pricing'] = $this->enrichTravelerPricing($slot['traveler_pricing']);
346 }
347 }
348 }
349
350 // Convert days_of_week to array (JSON array from DB, or legacy comma-separated)
351 if (!empty($rule->days_of_week)) {
352 $dow = $rule->days_of_week;
353 if (is_string($dow)) {
354 $decoded = json_decode($dow, true);
355 if (is_array($decoded)) {
356 $rule->days_of_week_array = array_map('intval', $decoded);
357 } else {
358 $rule->days_of_week_array = array_map('intval', explode(',', $dow));
359 }
360 } elseif (is_array($dow)) {
361 $rule->days_of_week_array = array_map('intval', $dow);
362 } else {
363 $rule->days_of_week_array = [];
364 }
365 } else {
366 $rule->days_of_week_array = [];
367 }
368
369 // Decode months (JSON column or longtext)
370 if (!empty($rule->months)) {
371 if (is_string($rule->months)) {
372 $rule->months = json_decode($rule->months, true) ?: [];
373 } elseif (!is_array($rule->months)) {
374 $rule->months = [];
375 }
376 } else {
377 $rule->months = [];
378 }
379
380 return $rule;
381 }
382
383 /**
384 * Enrich traveler pricing with category labels from database
385 */
386 private function enrichTravelerPricing(array $pricing): array
387 {
388 if (empty($pricing)) {
389 return [];
390 }
391
392 // Get all category IDs
393 $categoryIds = array_filter(array_map(function($p) {
394 return isset($p['category_id']) ? (int) $p['category_id'] : null;
395 }, $pricing));
396
397 if (empty($categoryIds)) {
398 return $pricing;
399 }
400
401 // Fetch category details
402 // Using hardcoded table name since there's no dedicated repository for this table
403 $categories_table = $this->wpdb->prefix . 'yatra_traveler_categories';
404 $placeholders = implode(',', array_fill(0, count($categoryIds), '%d'));
405 $sql = $this->wpdb->prepare(
406 "SELECT id, label, slug, description, age_min, age_max
407 FROM {$categories_table}
408 WHERE id IN ({$placeholders})",
409 ...$categoryIds
410 );
411 $categories = $this->wpdb->get_results($sql);
412
413 // Index by ID
414 $categoryIndex = [];
415 foreach ($categories as $cat) {
416 $categoryIndex[(int) $cat->id] = $cat;
417 }
418
419 // Enrich pricing with category info
420 foreach ($pricing as &$p) {
421 $catId = isset($p['category_id']) ? (int) $p['category_id'] : null;
422 if ($catId && isset($categoryIndex[$catId])) {
423 $cat = $categoryIndex[$catId];
424 $p['category_label'] = $cat->label;
425 $p['category_slug'] = $cat->slug;
426 $p['age_min'] = $cat->age_min ? (int) $cat->age_min : null;
427 $p['age_max'] = $cat->age_max ? (int) $cat->age_max : null;
428 // Calculate effective price
429 $p['effective_price'] = \Yatra\Services\TripPricingService::resolveCategoryEffectivePrice($p);
430 }
431 }
432
433 return $pricing;
434 }
435 }
436
437