PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.5
Yatra – Travel Booking & Tour Operator Software v3.0.5
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 / SampleDataRepository.php

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

871 lines 34.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Yatra\Repositories;
4
5 use Yatra\Database\Tables\TripsTable;
6 use Yatra\Database\Tables\ClassificationsTable;
7 use Yatra\Database\Tables\TripClassificationsTable;
8 use Yatra\Database\Tables\DiscountsTable;
9 use Yatra\Database\Tables\TripAvailabilityDatesTable;
10 use Yatra\Database\Tables\TripAvailabilityRulesTable;
11 use Yatra\Database\Tables\TripItineraryDaysTable;
12 use Yatra\Database\Tables\TripItineraryDayEntryTable;
13 use Yatra\Database\Tables\TripContentTable;
14
15 /**
16 * Sample Data Repository
17 *
18 * Handles database operations for sample data import.
19 * Inserts data into all required tables matching the exact structure
20 * that the real UI/API/Service layer uses.
21 */
22 class SampleDataRepository
23 {
24 private $wpdb;
25
26 private $trips_table;
27 private $classifications_table;
28 private $trip_classifications_table;
29 private $discounts_table;
30 private $availability_dates_table;
31 private $availability_rules_table;
32 private $itinerary_days_table;
33 private $itinerary_entries_table;
34 private $trip_content_table;
35
36 /**
37 * Tracks inserted classification type:slug => id mapping.
38 * Uses composite key to avoid slug collisions across types.
39 */
40 private $classification_ids = [];
41
42 /**
43 * Simple slug => id mapping (last wins) for backward compat.
44 */
45 private $slug_to_id = [];
46
47 public function __construct()
48 {
49 global $wpdb;
50 $this->wpdb = $wpdb;
51
52 $this->trips_table = TripsTable::getTableName();
53 $this->classifications_table = ClassificationsTable::getTableName();
54 $this->trip_classifications_table = TripClassificationsTable::getTableName();
55 $this->discounts_table = DiscountsTable::getTableName();
56 $this->availability_dates_table = TripAvailabilityDatesTable::getTableName();
57 $this->availability_rules_table = TripAvailabilityRulesTable::getTableName();
58 $this->itinerary_days_table = TripItineraryDaysTable::getTableName();
59 $this->itinerary_entries_table = TripItineraryDayEntryTable::getTableName();
60 $this->trip_content_table = TripContentTable::getTableName();
61 }
62
63 /**
64 * Get sample data directory path
65 */
66 private function get_sample_data_dir()
67 {
68 return YATRA_ABSPATH . 'sample-data/';
69 }
70
71 /**
72 * Read JSON file
73 */
74 private function read_json_file($filename)
75 {
76 $file_path = $this->get_sample_data_dir() . $filename;
77
78 if (!file_exists($file_path)) {
79 return [];
80 }
81
82 $json_content = file_get_contents($file_path);
83 $data = json_decode($json_content, true);
84
85 if (json_last_error() !== JSON_ERROR_NONE) {
86 return [];
87 }
88
89 return $data;
90 }
91
92 /**
93 * Get all sample data from JSON files
94 */
95 public function get_all_sample_data()
96 {
97 return [
98 'categories' => $this->read_json_file('categories.json'),
99 'activities' => $this->read_json_file('activities.json'),
100 'destinations' => $this->read_json_file('destinations.json'),
101 'difficulty_levels' => $this->read_json_file('difficulty-levels.json'),
102 'attributes' => $this->read_json_file('attributes.json'),
103 'item_types' => $this->read_json_file('item-types.json'),
104 'traveler_categories' => $this->read_json_file('traveler-categories.json'),
105 'items' => $this->read_json_file('items.json'),
106 'discounts' => $this->read_json_file('discounts.json'),
107 'trips' => $this->read_json_file('trips.json'),
108 'trip_classifications' => $this->read_json_file('trip-classifications.json'),
109 'availability_dates' => $this->read_json_file('availability-dates.json'),
110 'availability_rules' => $this->read_json_file('availability-rules.json'),
111 'itinerary_days' => $this->read_json_file('itinerary-days.json'),
112 'itinerary_entries' => $this->read_json_file('itinerary-entries.json'),
113 ];
114 }
115
116 /**
117 * Get the tracked classification IDs (slug => id)
118 */
119 public function get_classification_ids()
120 {
121 return $this->classification_ids;
122 }
123
124 /**
125 * Insert classifications and track their IDs by slug.
126 * Returns the count of inserted records.
127 */
128 public function insert_classifications($data)
129 {
130 $inserted = 0;
131 $now = current_time('mysql');
132 $uid = get_current_user_id();
133
134 foreach ($data as $item) {
135 // Track the slug before inserting
136 $slug = $item['slug'] ?? '';
137
138 // Set defaults for required fields
139 $item['created_at'] = $item['created_at'] ?? $now;
140 $item['updated_at'] = $item['updated_at'] ?? $now;
141 $item['created_by'] = $item['created_by'] ?? $uid;
142 $item['updated_by'] = $item['updated_by'] ?? $uid;
143 $item['parent_id'] = $item['parent_id'] ?? null;
144 $item['level'] = $item['level'] ?? 0;
145 $item['sorting'] = $item['sorting'] ?? 0;
146 $item['is_featured'] = $item['is_featured'] ?? 0;
147
148 // Convert metadata to JSON if it's an array
149 if (isset($item['metadata']) && is_array($item['metadata'])) {
150 $item['metadata'] = json_encode($item['metadata']);
151 }
152
153 $result = $this->wpdb->insert($this->classifications_table, $item);
154
155 if ($result && $this->wpdb->insert_id) {
156 $inserted++;
157 $id = $this->wpdb->insert_id;
158 $type = $item['type'] ?? '';
159 // Track by composite key (type:slug) and simple slug
160 if ($slug) {
161 $this->classification_ids[$type . ':' . $slug] = $id;
162 $this->slug_to_id[$slug] = $id;
163 }
164 } else {
165 }
166 }
167
168 return $inserted;
169 }
170
171 /**
172 * Insert items (type='item') with parent_id resolved from item_type slugs.
173 * Items use parent_slug to reference their item_type.
174 */
175 public function insert_items($data)
176 {
177 $inserted = 0;
178 $now = current_time('mysql');
179 $uid = get_current_user_id();
180
181 foreach ($data as $item) {
182 $slug = $item['slug'] ?? '';
183 $parent_slug = $item['parent_slug'] ?? '';
184 unset($item['parent_slug']);
185
186 // Resolve parent_id from the item_type slug using composite key
187 $parent_key = 'item_type:' . $parent_slug;
188 if ($parent_slug && isset($this->classification_ids[$parent_key])) {
189 $item['parent_id'] = $this->classification_ids[$parent_key];
190 } else {
191 $item['parent_id'] = null;
192 if ($parent_slug) {
193 }
194 }
195
196 $item['created_at'] = $item['created_at'] ?? $now;
197 $item['updated_at'] = $item['updated_at'] ?? $now;
198 $item['created_by'] = $item['created_by'] ?? $uid;
199 $item['updated_by'] = $item['updated_by'] ?? $uid;
200 $item['level'] = $item['level'] ?? 0;
201 $item['sorting'] = $item['sorting'] ?? 0;
202 $item['is_featured'] = $item['is_featured'] ?? 0;
203
204 if (isset($item['metadata']) && is_array($item['metadata'])) {
205 $item['metadata'] = json_encode($item['metadata']);
206 }
207
208 $result = $this->wpdb->insert($this->classifications_table, $item);
209
210 if ($result && $this->wpdb->insert_id) {
211 $inserted++;
212 $id = $this->wpdb->insert_id;
213 if ($slug) {
214 $this->classification_ids['item:' . $slug] = $id;
215 $this->slug_to_id[$slug] = $id;
216 }
217 }
218 }
219
220 return $inserted;
221 }
222
223 /**
224 * Insert trip-classification pivot records.
225 * Links trips to their categories, activities, destinations, difficulty, traveler_types.
226 */
227 public function insert_trip_classifications($data, $trip_ids)
228 {
229 $inserted = 0;
230 $now = current_time('mysql');
231
232 foreach ($data as $mapping) {
233 $trip_slug = $mapping['trip_slug'] ?? '';
234 if (!isset($trip_ids[$trip_slug])) {
235 continue;
236 }
237 $trip_id = $trip_ids[$trip_slug];
238
239 $classifications = $mapping['classifications'] ?? [];
240
241 // Track index per classification type so first of each type gets 'primary'
242 // This matches how TripRepository::saveDestinations/saveActivities works
243 $type_counters = [];
244
245 foreach ($classifications as $classification) {
246 $cls_slug = $classification['slug'] ?? '';
247 $cls_type = $classification['type'] ?? '';
248
249 if (!$cls_slug || !$cls_type) {
250 continue;
251 }
252
253 // Look up the classification ID using composite key (type:slug)
254 $composite_key = $cls_type . ':' . $cls_slug;
255 if (!isset($this->classification_ids[$composite_key])) {
256 // Try to find it in DB if not tracked (might already exist)
257 $existing = $this->wpdb->get_var($this->wpdb->prepare(
258 "SELECT id FROM {$this->classifications_table} WHERE slug = %s AND type = %s LIMIT 1",
259 $cls_slug, $cls_type
260 ));
261 if ($existing) {
262 $this->classification_ids[$composite_key] = (int) $existing;
263 } else {
264 continue;
265 }
266 }
267
268 $classification_id = $this->classification_ids[$composite_key];
269
270 // Per-type index for relationship_type and sort_order
271 if (!isset($type_counters[$cls_type])) {
272 $type_counters[$cls_type] = 0;
273 }
274 $type_index = $type_counters[$cls_type];
275 $type_counters[$cls_type]++;
276
277 $metadata_json = null;
278 if ($cls_type === 'attribute') {
279 if (!empty($classification['metadata']) && is_array($classification['metadata'])) {
280 $metadata_json = wp_json_encode($classification['metadata']);
281 } elseif (array_key_exists('value', $classification) || !empty($classification['field_type'])) {
282 $metadata_json = wp_json_encode([
283 'field_type' => $classification['field_type'] ?? 'text_field',
284 'value' => $classification['value'] ?? '',
285 ]);
286 }
287 } elseif (!empty($classification['metadata'])) {
288 $meta = $classification['metadata'];
289 $metadata_json = is_string($meta) ? $meta : wp_json_encode($meta);
290 }
291
292 $result = $this->wpdb->insert(
293 $this->trip_classifications_table,
294 [
295 'trip_id' => $trip_id,
296 'classification_id' => $classification_id,
297 'classification_type' => $cls_type,
298 'relationship_type' => $type_index === 0 ? 'primary' : 'secondary',
299 'metadata' => $metadata_json,
300 'sort_order' => $type_index,
301 'is_featured' => $type_index === 0 ? 1 : 0,
302 'is_active' => 1,
303 'created_at' => $now,
304 'updated_at' => $now,
305 ],
306 ['%d', '%d', '%s', '%s', '%s', '%d', '%d', '%d', '%s', '%s']
307 );
308
309 if ($result) {
310 $inserted++;
311 }
312 }
313 }
314
315 return $inserted;
316 }
317
318 /**
319 * Insert discounts
320 */
321 public function insert_discounts($data)
322 {
323 $inserted = 0;
324 $now = current_time('mysql');
325 $future = date('Y-m-d H:i:s', strtotime('+6 months'));
326 $uid = get_current_user_id();
327
328 foreach ($data as $item) {
329 $item['created_at'] = $item['created_at'] ?? $now;
330 $item['updated_at'] = $item['updated_at'] ?? $now;
331 $item['created_by'] = $item['created_by'] ?? $uid;
332 $item['updated_by'] = $item['updated_by'] ?? $uid;
333 $item['usage_count'] = $item['usage_count'] ?? 0;
334 $item['usage_limit_per_customer'] = $item['usage_limit_per_customer'] ?? 0;
335 $item['applicable_to'] = $item['applicable_to'] ?? 'all';
336 $item['first_time_customer_only'] = $item['first_time_customer_only'] ?? 0;
337 $item['is_group_discount'] = $item['is_group_discount'] ?? 0;
338 $item['discount_mode'] = $item['discount_mode'] ?? 'both';
339 $item['valid_from'] = $item['valid_from'] ?? $now;
340 $item['expiry_date'] = $item['expiry_date'] ?? $future;
341
342 $result = $this->wpdb->insert($this->discounts_table, $item);
343 if ($result) {
344 $inserted++;
345 }
346 }
347
348 return $inserted;
349 }
350
351 /**
352 * Insert trips. Returns slug => id mapping.
353 */
354 public function insert_trips($data)
355 {
356 $trip_ids = [];
357 $now = current_time('mysql');
358 $uid = get_current_user_id();
359
360 foreach ($data as $item) {
361 $trip_slug = $item['slug'];
362
363 $item['created_at'] = $item['created_at'] ?? $now;
364 $item['updated_at'] = $item['updated_at'] ?? $now;
365 $item['created_by'] = $item['created_by'] ?? $uid;
366 $item['updated_by'] = $item['updated_by'] ?? $uid;
367
368 // Resolve price_types: convert traveler_category slugs to category_id
369 // and map field names (price→original_price, sale_price→discounted_price)
370 if (!empty($item['price_types'])) {
371 $price_types_raw = $item['price_types'];
372
373 // Decode if it's a JSON string
374 if (is_string($price_types_raw)) {
375 $price_types_raw = json_decode($price_types_raw, true);
376 }
377
378 if (is_array($price_types_raw) && !empty($price_types_raw)) {
379 $resolved_price_types = [];
380 foreach ($price_types_raw as $pt) {
381 $resolved = [];
382
383 // Resolve traveler_category slug to numeric category_id
384 // Composite key uses 'traveler_type:' (matches type field in classifications table)
385 if (isset($pt['traveler_category'])) {
386 $slug = $pt['traveler_category'];
387 $composite_key = 'traveler_type:' . $slug;
388 if (isset($this->classification_ids[$composite_key])) {
389 $resolved['category_id'] = $this->classification_ids[$composite_key];
390 } elseif (isset($this->slug_to_id[$slug])) {
391 $resolved['category_id'] = $this->slug_to_id[$slug];
392 } else {
393 // Keep slug as label fallback
394 $resolved['category_id'] = null;
395 $resolved['label'] = ucfirst(str_replace('-', ' ', $slug));
396 }
397 } elseif (isset($pt['category_id'])) {
398 $resolved['category_id'] = $pt['category_id'];
399 }
400
401 // Map field names: price → original_price, sale_price → discounted_price
402 $resolved['original_price'] = (float) ($pt['original_price'] ?? $pt['price'] ?? 0);
403 $resolved['discounted_price'] = (float) ($pt['discounted_price'] ?? $pt['sale_price'] ?? 0);
404
405 if (isset($resolved['label'])) {
406 // Keep label for fallback display
407 }
408
409 $resolved_price_types[] = $resolved;
410 }
411
412 $item['price_types'] = wp_json_encode($resolved_price_types);
413
414 // Auto-set pricing_type to traveler_based when price_types exist
415 if (empty($item['pricing_type']) || $item['pricing_type'] === 'regular') {
416 $item['pricing_type'] = 'traveler_based';
417 }
418 } else {
419 $item['price_types'] = null;
420 }
421 }
422
423 // Ensure other JSON fields are properly encoded
424 foreach (['included_items', 'excluded_items', 'frontend_tabs', 'custom_fields'] as $jsonField) {
425 if (isset($item[$jsonField]) && is_array($item[$jsonField])) {
426 $item[$jsonField] = wp_json_encode($item[$jsonField]);
427 }
428 }
429
430 $result = $this->wpdb->insert($this->trips_table, $item);
431
432 if ($result && $this->wpdb->insert_id) {
433 $trip_ids[$trip_slug] = $this->wpdb->insert_id;
434 } else {
435 }
436 }
437
438 return $trip_ids;
439 }
440
441 /**
442 * Insert availability dates
443 */
444 public function insert_availability_dates($data, $trip_ids)
445 {
446 $inserted = 0;
447
448 foreach ($data as $item) {
449 $trip_slug = $item['trip_slug'];
450 unset($item['trip_slug']);
451
452 if (!isset($trip_ids[$trip_slug])) {
453 continue;
454 }
455 $item['trip_id'] = $trip_ids[$trip_slug];
456
457 if (empty($item['return_date']) && !empty($item['arrival_date'])) {
458 $item['return_date'] = $item['arrival_date'];
459 }
460
461 $result = $this->wpdb->insert($this->availability_dates_table, $item);
462 if ($result) {
463 $inserted++;
464 }
465 }
466
467 return $inserted;
468 }
469
470 /**
471 * Insert availability rules.
472 *
473 * Sample JSON only knows the legacy columns (`recurrence_type`,
474 * `capacity_value`, `interval`, `day_of_month`). The new admin React UI
475 * binds to a parallel set (`rule_type`, `seats_total`, `interval_days`,
476 * `interval_start_date`). We populate both so a freshly imported sample
477 * dataset is immediately editable in the new UI without needing the
478 * idempotent {@see InstallerService::maybeNormalizeAvailabilityRulesLegacyData()}
479 * heal-step to fix it on next admin_init.
480 */
481 public function insert_availability_rules($data, $trip_ids)
482 {
483 $inserted = 0;
484
485 foreach ($data as $item) {
486 $trip_slug = $item['trip_slug'];
487 unset($item['trip_slug']);
488
489 if (!isset($trip_ids[$trip_slug])) {
490 continue;
491 }
492 $item['trip_id'] = $trip_ids[$trip_slug];
493
494 $item = $this->mapLegacyAvailabilityRuleToNewSchema($item);
495
496 // Convert arrays to JSON
497 if (isset($item['days_of_week']) && is_array($item['days_of_week'])) {
498 $item['days_of_week'] = json_encode($item['days_of_week']);
499 }
500 if (isset($item['recurrence_pattern']) && is_array($item['recurrence_pattern'])) {
501 $item['recurrence_pattern'] = json_encode($item['recurrence_pattern']);
502 }
503
504 $result = $this->wpdb->insert($this->availability_rules_table, $item);
505 if ($result) {
506 $inserted++;
507 }
508 }
509
510 return $inserted;
511 }
512
513 /**
514 * Map a sample-data row written against the legacy availability-rule
515 * schema onto the new-schema columns the admin UI reads.
516 *
517 * Mirrors the heal logic in
518 * {@see InstallerService::maybeNormalizeAvailabilityRulesLegacyData()}
519 * so write-time and read-time-repair stay in lock-step.
520 *
521 * @param array<string, mixed> $item
522 * @return array<string, mixed>
523 */
524 private function mapLegacyAvailabilityRuleToNewSchema(array $item): array
525 {
526 $recurrence = isset($item['recurrence_type']) ? (string) $item['recurrence_type'] : 'weekly';
527 $intervalRaw = isset($item['interval']) ? (int) $item['interval'] : 1;
528 $intervalNorm = $intervalRaw > 0 ? $intervalRaw : 1;
529
530 if (!isset($item['rule_type']) || $item['rule_type'] === '' || $item['rule_type'] === null) {
531 switch ($recurrence) {
532 case 'daily':
533 $item['rule_type'] = 'interval';
534 if (!isset($item['interval_days'])) {
535 $item['interval_days'] = $intervalNorm;
536 }
537 if (!isset($item['interval_start_date']) && !empty($item['start_date'])) {
538 $item['interval_start_date'] = $item['start_date'];
539 }
540 break;
541 case 'monthly':
542 case 'yearly':
543 $item['rule_type'] = 'monthly';
544 break;
545 case 'custom':
546 $item['rule_type'] = 'interval';
547 if (!isset($item['interval_days'])) {
548 $item['interval_days'] = $intervalNorm;
549 }
550 if (!isset($item['interval_start_date']) && !empty($item['start_date'])) {
551 $item['interval_start_date'] = $item['start_date'];
552 }
553 break;
554 case 'weekly':
555 default:
556 $item['rule_type'] = 'weekly';
557 break;
558 }
559 }
560
561 // seats_total mirrors capacity_value when capacity is fixed (the
562 // sample dataset doesn't model percentage capacity). CapacityService
563 // and the React table read seats_total directly.
564 $capacityType = $item['capacity_type'] ?? 'fixed';
565 if (
566 !isset($item['seats_total'])
567 && isset($item['capacity_value'])
568 && (int) $item['capacity_value'] > 0
569 && $capacityType === 'fixed'
570 ) {
571 $item['seats_total'] = (int) $item['capacity_value'];
572 }
573
574 return $item;
575 }
576
577 /**
578 * Insert itinerary days. Returns composite key => day_id mapping.
579 */
580 public function insert_itinerary_days($data, $trip_ids)
581 {
582 $day_ids = [];
583 $now = current_time('mysql');
584 $uid = get_current_user_id();
585
586 foreach ($data as $item) {
587 $trip_slug = $item['trip_slug'];
588 unset($item['trip_slug']);
589
590 if (!isset($trip_ids[$trip_slug])) {
591 continue;
592 }
593 $item['trip_id'] = $trip_ids[$trip_slug];
594 $item['created_at'] = $item['created_at'] ?? $now;
595 $item['updated_at'] = $item['updated_at'] ?? $now;
596 $item['created_by'] = $item['created_by'] ?? $uid;
597 $item['updated_by'] = $item['updated_by'] ?? $uid;
598
599 $result = $this->wpdb->insert($this->itinerary_days_table, $item);
600
601 if ($result && $this->wpdb->insert_id) {
602 $key = $trip_slug . '_day_' . $item['day_number'];
603 $day_ids[$key] = $this->wpdb->insert_id;
604 }
605 }
606
607 return $day_ids;
608 }
609
610 /**
611 * Insert itinerary entries with proper item_type_id and item_id resolution.
612 * Uses item_type slug from JSON to look up real classification IDs.
613 */
614 public function insert_itinerary_entries($data, $trip_ids, $day_ids)
615 {
616 $inserted = 0;
617 $now = current_time('mysql');
618 $uid = get_current_user_id();
619
620 // Build a map of item_type slug => {id, name, icon} from classifications
621 $item_type_map = $this->build_item_type_map();
622 // Build a map of item slug => {id, name, icon, parent_id} from classifications
623 $item_map = $this->build_item_map();
624
625 foreach ($data as $item) {
626 $trip_slug = $item['trip_slug'];
627 $day_number = $item['day_number'];
628 unset($item['trip_slug'], $item['day_number']);
629
630 if (!isset($trip_ids[$trip_slug])) {
631 continue;
632 }
633 $day_key = $trip_slug . '_day_' . $day_number;
634 if (!isset($day_ids[$day_key])) {
635 continue;
636 }
637
638 $item['trip_id'] = $trip_ids[$trip_slug];
639 $item['day_id'] = $day_ids[$day_key];
640
641 // Resolve item_type field to real item_type_id, item_name, item_icon
642 // Note: item_type is also a real DB column (varchar(50)) so we keep it
643 $item_type_slug = $item['item_type'] ?? null;
644 if ($item_type_slug && isset($item_type_map[$item_type_slug])) {
645 $typeInfo = $item_type_map[$item_type_slug];
646 $item['item_type_id'] = $typeInfo['id'];
647 $item['item_name'] = $item['item_name'] ?? $typeInfo['name'];
648 $item['item_icon'] = $item['item_icon'] ?? $typeInfo['icon'];
649
650 // Try to find a matching item for this item_type
651 $matched_item = $this->find_best_item_match($item_type_slug, $item['title'] ?? '', $item_map);
652 if ($matched_item) {
653 $item['item_id'] = $matched_item['id'];
654 $item['item_name'] = $matched_item['name'];
655 $item['item_icon'] = $matched_item['icon'] ?: ($item['item_icon'] ?? null);
656 }
657 }
658
659 $item['created_at'] = $item['created_at'] ?? $now;
660 $item['updated_at'] = $item['updated_at'] ?? $now;
661 $item['created_by'] = $item['created_by'] ?? $uid;
662 $item['updated_by'] = $item['updated_by'] ?? $uid;
663
664 // Convert JSON fields if arrays
665 foreach (['included_items', 'excluded_items', 'gallery'] as $jsonField) {
666 if (isset($item[$jsonField]) && is_array($item[$jsonField])) {
667 $item[$jsonField] = json_encode($item[$jsonField]);
668 }
669 }
670
671 $result = $this->wpdb->insert($this->itinerary_entries_table, $item);
672 if ($result) {
673 $inserted++;
674 } else {
675 }
676 }
677
678 return $inserted;
679 }
680
681 /**
682 * Build item_type slug => info map from DB
683 */
684 private function build_item_type_map()
685 {
686 $map = [];
687 $rows = $this->wpdb->get_results(
688 "SELECT id, slug, name, icon FROM {$this->classifications_table} WHERE type = 'item_type'"
689 );
690 foreach ($rows as $row) {
691 $map[$row->slug] = [
692 'id' => (int) $row->id,
693 'name' => $row->name,
694 'icon' => $row->icon,
695 ];
696 }
697 return $map;
698 }
699
700 /**
701 * Build item slug => info map from DB (type='item')
702 */
703 private function build_item_map()
704 {
705 $map = [];
706 $rows = $this->wpdb->get_results(
707 "SELECT id, slug, name, icon, parent_id FROM {$this->classifications_table} WHERE type = 'item'"
708 );
709 foreach ($rows as $row) {
710 $map[$row->slug] = [
711 'id' => (int) $row->id,
712 'name' => $row->name,
713 'icon' => $row->icon,
714 'parent_id' => (int) $row->parent_id,
715 ];
716 }
717 return $map;
718 }
719
720 /**
721 * Find the best matching item for an itinerary entry based on item_type and title
722 */
723 private function find_best_item_match($item_type_slug, $entry_title, $item_map)
724 {
725 // Get the item_type_id for this slug using composite key
726 $item_type_id = $this->classification_ids['item_type:' . $item_type_slug] ?? null;
727 if (!$item_type_id) {
728 return null;
729 }
730
731 // Filter items that belong to this item_type (via parent_id)
732 $candidates = [];
733 foreach ($item_map as $slug => $info) {
734 if ($info['parent_id'] === $item_type_id) {
735 $candidates[$slug] = $info;
736 }
737 }
738
739 if (empty($candidates)) {
740 return null;
741 }
742
743 // Simple keyword matching
744 $title_lower = strtolower($entry_title);
745 foreach ($candidates as $slug => $info) {
746 $name_lower = strtolower($info['name']);
747 $slug_words = explode('-', $slug);
748
749 // Check if any word from slug appears in the entry title
750 foreach ($slug_words as $word) {
751 if (strlen($word) > 2 && strpos($title_lower, $word) !== false) {
752 return $info;
753 }
754 }
755 // Check if item name appears in title
756 if (strpos($title_lower, $name_lower) !== false) {
757 return $info;
758 }
759 }
760
761 // Return first candidate as fallback
762 return reset($candidates);
763 }
764
765 /**
766 * Cleanup all sample data
767 */
768 public function cleanup_sample_data()
769 {
770 $results = [];
771
772 // Get trip IDs first for cleaning pivot tables
773 $trip_slugs = [
774 'swiss-alps-mountain-trek', 'maldives-beach-escape', 'kyoto-cultural-journey',
775 'serengeti-wildlife-safari', 'paris-city-explorer', 'bali-island-adventure',
776 'iceland-northern-lights', 'new-zealand-adventure', 'peru-machu-picchu-trek',
777 'norway-fjords-cruise', 'grand-canyon-day-tour', 'paris-city-highlights-tour',
778 'nyc-helicopter-liberty-tour', 'tokyo-cultural-food-tour'
779 ];
780 $slugs_string = "'" . implode("','", $trip_slugs) . "'";
781
782 // Get trip IDs before deleting
783 $trip_ids = $this->wpdb->get_col(
784 "SELECT id FROM {$this->trips_table} WHERE slug IN ({$slugs_string})"
785 );
786
787 if (!empty($trip_ids)) {
788 $trip_ids_string = implode(',', array_map('intval', $trip_ids));
789
790 // Clean pivot table
791 $this->wpdb->query(
792 "DELETE FROM {$this->trip_classifications_table} WHERE trip_id IN ({$trip_ids_string})"
793 );
794
795 // Clean itinerary entries
796 $this->wpdb->query(
797 "DELETE FROM {$this->itinerary_entries_table} WHERE trip_id IN ({$trip_ids_string})"
798 );
799
800 // Clean itinerary days
801 $this->wpdb->query(
802 "DELETE FROM {$this->itinerary_days_table} WHERE trip_id IN ({$trip_ids_string})"
803 );
804
805 // Clean availability dates
806 $this->wpdb->query(
807 "DELETE FROM {$this->availability_dates_table} WHERE trip_id IN ({$trip_ids_string})"
808 );
809
810 // Clean availability rules
811 $this->wpdb->query(
812 "DELETE FROM {$this->availability_rules_table} WHERE trip_id IN ({$trip_ids_string})"
813 );
814
815 // Clean trip content
816 $this->wpdb->query(
817 "DELETE FROM {$this->trip_content_table} WHERE trip_id IN ({$trip_ids_string})"
818 );
819 }
820
821 // Delete trips
822 $result = $this->wpdb->query(
823 "DELETE FROM {$this->trips_table} WHERE slug IN ({$slugs_string})"
824 );
825 $results['trips'] = $result !== false;
826
827 // All classification slugs including items
828 $classification_slugs = [
829 'adventure-tours', 'beach-island', 'cultural-tours', 'wildlife-safari',
830 'city-tours', 'trekking-hiking', 'cruise-tours', 'food-wine',
831 'hiking', 'snorkeling', 'city-walking-tour', 'wildlife-viewing',
832 'kayaking', 'photography', 'camping', 'rock-climbing', 'cycling', 'cooking-class',
833 'swiss-alps', 'maldives', 'kyoto-japan', 'serengeti-tanzania', 'paris-france',
834 'bali-indonesia', 'iceland', 'new-zealand', 'peru', 'norway',
835 'easy', 'moderate', 'challenging', 'difficult', 'extreme', 'expert',
836 'group-size', 'age-restriction', 'fitness-level', 'accommodation-type',
837 'meal-plan', 'transportation', 'guide-language', 'season',
838 'accommodation', 'activity', 'meal', 'sightseeing', 'free-time',
839 'adult', 'child', 'infant', 'student', 'senior', 'group-leader', 'family', 'solo-traveler',
840 'mountain-hut', '5-star-hotel', 'beach-resort', 'safari-lodge', 'tented-camp',
841 'guided-hiking', 'snorkeling-trip', 'temple-visit', 'game-drive',
842 'city-walking-tour-item', 'cooking-class-item',
843 'breakfast', 'lunch', 'dinner', 'welcome-dinner',
844 'airport-transfer', 'coach-transfer', 'boat-transfer',
845 'landmark-visit', 'scenic-viewpoint',
846 'rest-and-explore', 'shopping-time'
847 ];
848
849 $class_slugs_string = "'" . implode("','", $classification_slugs) . "'";
850 $result = $this->wpdb->query(
851 "DELETE FROM {$this->classifications_table} WHERE slug IN ({$class_slugs_string})"
852 );
853 $results['classifications'] = $result !== false;
854
855 // Clean discounts
856 $discount_codes = ['SUMMER2024', 'EARLYBIRD', 'GROUP10', 'WELCOME50',
857 'FAMILY20', 'LASTMINUTE', 'LOYAL100', 'WINTER2024'];
858 $codes_string = "'" . implode("','", $discount_codes) . "'";
859 $result = $this->wpdb->query(
860 "DELETE FROM {$this->discounts_table} WHERE code IN ({$codes_string})"
861 );
862 $results['discounts'] = $result !== false;
863
864 // Clear import flags
865 delete_option('yatra_sample_data_imported');
866 delete_option('yatra_sample_data_import_date');
867
868 return $results;
869 }
870 }
871