PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.9
Yatra – Travel Booking & Tour Operator Software v3.0.2.9
3.0.16 3.0.15 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 All 84 releases
yatra / app / Migrations / TripMigration.php

TripMigration.php in Yatra – Travel Booking & Tour Operator Software 3.0.2.9, at app/Migrations/TripMigration.php

1,129 lines 45.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Yatra\Migration;
4
5 use Yatra\Utils\Logger;
6 use Yatra\Migration\MigrationProgress;
7 use Yatra\Database\Tables\TripsTable;
8 use Yatra\Database\Tables\TripContentTable;
9 use Yatra\Database\Tables\TripAvailabilityDatesTable;
10
11 class TripMigration extends BaseMigration
12 {
13 public function __construct(MigrationProgress $service)
14 {
15 parent::__construct($service);
16 }
17
18 public function run(): array
19 {
20 $migrated = 0;
21 $skipped = 0;
22 $failed = 0;
23
24 // Get all tours from old system (including all statuses except trash)
25 $oldTrips = $this->wpdb->get_results(
26 "SELECT * FROM {$this->wpdb->posts}
27 WHERE post_type = 'tour' AND post_status != 'trash'"
28 );
29
30 $total = count($oldTrips);
31
32 // Count existing trips in new system
33 $existingTripsCount = $this->wpdb->get_var(
34 "SELECT COUNT(*) FROM " . TripsTable::getTableName()
35 );
36
37 foreach ($oldTrips as $oldTrip) {
38 try {
39 // Always re-migrate trips on every run - no skip logic
40 $meta = $this->getPostMeta($oldTrip->ID);
41
42 $status = ($oldTrip->post_status === 'publish') ? 'publish' : 'draft';
43
44 $baseSlug = $oldTrip->post_name ?: sanitize_title($oldTrip->post_title);
45 $slug = $baseSlug;
46 $existingTripId = null;
47
48 if ($this->isForceMigration()) {
49 // Force migration: Always insert new (create duplicates)
50 $slug = $this->generateUniqueSlug($baseSlug, 'yatra_new_trips');
51 } else {
52 // Regular migration: Check if already exists
53 $existingTripId = $this->wpdb->get_var($this->wpdb->prepare(
54 "SELECT id FROM " . TripsTable::getTableName() . " WHERE slug = %s",
55 $baseSlug
56 ));
57
58 if (!$existingTripId) {
59 // Generate unique slug for new insert
60 $slug = $this->generateUniqueSlug($baseSlug, 'yatra_new_trips');
61 }
62 }
63
64 // Raw SQL: get_post_thumbnail_id internally calls get_post_meta for _thumbnail_id
65 $featuredImageId = $this->getRawPostMeta($oldTrip->ID, '_thumbnail_id');
66 $featuredImageId = $featuredImageId ? (int) $featuredImageId : null;
67 $createdBy = intval($oldTrip->post_author);
68
69 $regularPrice = $this->getLegacyMetaValue($meta, [
70 'yatra_tour_meta_regular_price',
71 'yatra_tour_meta_tour_price',
72 'yatra_tour_meta_price',
73 'yatra_tour_meta_price_per_person',
74 ]);
75
76 $salePrice = $this->getLegacyMetaValue($meta, [
77 'yatra_tour_meta_sales_price',
78 'yatra_tour_meta_sale_price',
79 'yatra_tour_meta_tour_sale_price',
80 ]);
81
82 $maxTravelersMeta = $this->getLegacyMetaValue($meta, [
83 'yatra_tour_meta_tour_group_size',
84 'yatra_tour_meta_group_size',
85 'yatra_tour_maximum_number_of_traveller',
86 'yatra_tour_meta_group_max',
87 ]);
88
89 $regularPriceFloat = $regularPrice !== null ? (float) $regularPrice : 0.0;
90 $salePriceFloat = $salePrice !== null && $salePrice !== ''
91 ? (float) $salePrice
92 : null;
93
94 if ($salePriceFloat !== null && $salePriceFloat <= 0) {
95 $salePriceFloat = null;
96 }
97
98 if ($salePriceFloat !== null && $salePriceFloat >= $regularPriceFloat) {
99 $salePriceFloat = null;
100 }
101
102 $maxTravelers = $maxTravelersMeta !== null && $maxTravelersMeta !== ''
103 ? (int) $maxTravelersMeta
104 : 10;
105
106 $minTravelersMeta = $this->getLegacyMetaValue($meta, [
107 'yatra_tour_minimum_pax',
108 ]);
109 $minTravelers = $minTravelersMeta !== null && $minTravelersMeta !== ''
110 ? max(1, (int) $minTravelersMeta)
111 : 1;
112
113 $durationDays = intval($meta['yatra_tour_meta_tour_duration_days'] ?? 1);
114 $durationNights = intval($meta['yatra_tour_meta_tour_duration_nights'] ?? 0);
115 $tripType = ($durationDays <= 1 && $durationNights < 1) ? 'single_day' : 'multi_day';
116
117 $isFeatured = !empty($meta['yatra_tour_meta_tour_featured']) ? 1 : 0;
118
119 $legacyTourType = $meta['yatra_tour_meta_tour_type'] ?? 'regular';
120 $customFields = [];
121 if (!empty($meta['yatra_tour_meta_disable_booking'])) {
122 $customFields['legacy_disable_booking'] = true;
123 }
124 if ($legacyTourType === 'external') {
125 $extUrl = $meta['yatra_tour_meta_tour_external_url'] ?? '';
126 if ($extUrl !== '') {
127 $customFields['external_booking_url'] = esc_url_raw((string) $extUrl);
128 }
129 $extBtn = $meta['yatra_tour_meta_tour_external_button_text'] ?? '';
130 if ($extBtn !== '') {
131 $customFields['external_booking_button_text'] = sanitize_text_field((string) $extBtn);
132 }
133 }
134
135 $tripData = [
136 'title' => $oldTrip->post_title,
137 'slug' => $slug,
138 'description' => $oldTrip->post_content,
139 'short_description' => $oldTrip->post_excerpt,
140 'trip_details' => $oldTrip->post_content,
141 'trip_type' => $tripType,
142 'duration_days' => $durationDays,
143 'duration_nights' => $durationNights,
144 'min_travelers' => $minTravelers,
145 'max_travelers' => $maxTravelers > 0 ? $maxTravelers : 10,
146 'original_price' => $regularPriceFloat,
147 'sale_price' => $salePriceFloat,
148 'discounted_price' => $salePriceFloat,
149 'featured_image' => $featuredImageId ?: null,
150 'is_featured' => $isFeatured,
151 'status' => $status,
152 'created_at' => $oldTrip->post_date,
153 'updated_at' => $oldTrip->post_modified,
154 'created_by' => $createdBy,
155 'updated_by' => $createdBy,
156 ];
157 if (!empty($customFields)) {
158 $tripData['custom_fields'] = wp_json_encode($customFields);
159 }
160
161 if ($existingTripId && !$this->isForceMigration()) {
162 // Regular migration: Update existing trip
163 $updateData = $tripData;
164 unset($updateData['created_at']); // Don't update created_at
165
166 $updated = $this->wpdb->update(
167 TripsTable::getTableName(),
168 $updateData,
169 ['id' => $existingTripId]
170 );
171
172 if ($updated !== false) {
173 $newTripId = $existingTripId;
174 $this->deleteTripRelationships($newTripId);
175 $migrated++;
176 } else {
177 $failed++;
178 Logger::error("Failed to update trip ID {$oldTrip->ID}: {$this->wpdb->last_error}", [
179 'source' => 'migration',
180 'data_type' => 'trips',
181 'trip_id' => $oldTrip->ID,
182 'trip_title' => $oldTrip->post_title,
183 'db_error' => $this->wpdb->last_error
184 ]);
185 $this->updateProgress('trips', 'running', $migrated, $skipped, $failed, $total, null, null);
186 continue;
187 }
188 } else {
189 // Force migration OR new trip: Insert new
190 $mode = $this->isForceMigration() ? 'Force mode' : 'Regular mode (new trip)';
191 $inserted = $this->wpdb->insert(
192 TripsTable::getTableName(),
193 $tripData
194 );
195
196 if ($inserted) {
197 $newTripId = $this->wpdb->insert_id;
198 $migrated++;
199 } else {
200 $failed++;
201 Logger::error("Failed to insert trip ID {$oldTrip->ID} into database", [
202 'source' => 'migration',
203 'data_type' => 'trips',
204 'trip_id' => $oldTrip->ID,
205 'trip_title' => $oldTrip->post_title,
206 'db_error' => $this->wpdb->last_error
207 ]);
208 $this->updateProgress('trips', 'running', $migrated, $skipped, $failed, $total, null, null);
209 continue;
210 }
211 }
212
213 $this->setRawPostMeta($oldTrip->ID, '_migrated_to_trip_id', (string) $newTripId);
214
215 // Migrate trip classifications (destinations, activities, categories, attributes)
216 $this->migrateTripDestinations($oldTrip->ID, $newTripId);
217 $this->migrateTripActivities($oldTrip->ID, $newTripId);
218 $this->migrateTripCategories($oldTrip->ID, $newTripId);
219 $this->migrateTripAttributes($oldTrip->ID, $newTripId);
220
221 // Migrate trip content (gallery, highlights, FAQs)
222 $this->migrateTripGallery($oldTrip->ID, $newTripId, $meta);
223 $this->migrateTripHighlights($oldTrip->ID, $newTripId, $meta);
224 $this->migrateTripFAQs($oldTrip->ID, $newTripId, $meta);
225
226 // Migrate pricing and availability
227 $this->migrateTripPricing($oldTrip->ID, $newTripId, $meta);
228 $this->migrateTripAvailabilityDates($oldTrip->ID, $newTripId, $meta, $tripData);
229
230 $this->updateProgress('trips', 'running', $migrated, $skipped, $failed, $total, null, null);
231
232 usleep(100000);
233 } catch (\Exception $e) {
234 $failed++;
235 Logger::error("Exception migrating trip ID {$oldTrip->ID}: {$e->getMessage()}", [
236 'source' => 'migration',
237 'data_type' => 'trips',
238 'trip_id' => $oldTrip->ID,
239 'trip_title' => $oldTrip->post_title,
240 'error' => $e->getMessage()
241 ]);
242
243 $this->updateProgress('trips', 'running', $migrated, $skipped, $failed, $total, null, null);
244 }
245 }
246
247 return compact('migrated', 'skipped', 'failed');
248 }
249
250 /**
251 * Migrate trip gallery images from old tour system
252 */
253 private function migrateTripGallery(int $oldTourId, int $newTripId, array $meta): void
254 {
255 global $wpdb;
256 $table = TripContentTable::getTableName();
257
258 // Clear existing gallery images for this trip (unless force migration)
259 if (!$this->isForceMigration()) {
260 $deleted = $wpdb->delete($table, ['trip_id' => $newTripId, 'content_type' => 'image'], ['%d', '%s']);
261 if ($deleted) {
262 }
263 }
264
265 // Direct database query to find ALL gallery-related meta
266 $allGalleryMeta = $wpdb->get_results($wpdb->prepare(
267 "SELECT meta_key, meta_value FROM {$wpdb->postmeta}
268 WHERE post_id = %d
269 AND (meta_key LIKE '%%gallery%%' OR meta_key LIKE '%%image%%' OR meta_key LIKE '%%photo%%')
270 ORDER BY meta_key",
271 $oldTourId
272 ));
273
274 if (!empty($allGalleryMeta)) {
275 foreach ($allGalleryMeta as $metaRow) {
276 }
277 } else {
278 }
279
280 $galleryImages = [];
281
282 // The primary gallery meta key from old Yatra system
283 $galleryKeys = [
284 'yatra_tour_meta_gallery', // Primary: comma-separated attachment IDs
285 'yatra_tour_slider_items', // Slider items: comma-separated attachment IDs
286 'yatra_tour_gallery',
287 'yatra_tour_meta_gallery_images',
288 'yatra_gallery',
289 'tour_gallery',
290 'yatra_tour_images',
291 'yatra_tour_meta_images',
292 'yatra_images',
293 'tour_images',
294 'yatra_tour_photos',
295 'yatra_tour_meta_photos',
296 'yatra_photos',
297 'tour_photos',
298 '_yatra_gallery',
299 '_tour_gallery'
300 ];
301
302 foreach ($galleryKeys as $key) {
303 if (isset($meta[$key]) && !empty($meta[$key])) {
304 $galleryData = $meta[$key];
305 // Handle comma-separated attachment IDs (e.g., "0,27,26,25,24,23,22,19,18")
306 if (is_string($galleryData) && strpos($galleryData, ',') !== false) {
307 $attachmentIds = array_filter(array_map('intval', explode(',', $galleryData)));
308 if (!empty($attachmentIds)) {
309 $galleryImages = $attachmentIds;
310 break;
311 }
312 }
313 // Handle single attachment ID
314 elseif (is_numeric($galleryData) && intval($galleryData) > 0) {
315 $galleryImages = [intval($galleryData)];
316 break;
317 }
318 // Handle serialized data
319 elseif (is_string($galleryData)) {
320 $unserialized = maybe_unserialize($galleryData);
321 if (is_array($unserialized) && !empty($unserialized)) {
322 $galleryImages = $unserialized;
323 break;
324 }
325 }
326 // Handle array data
327 elseif (is_array($galleryData) && !empty($galleryData)) {
328 $galleryImages = $galleryData;
329 break;
330 }
331 }
332 }
333
334 // Log what keys were checked
335 // If no gallery meta found, try to get attached images to the tour post
336 if (empty($galleryImages)) {
337 // Raw SQL: get_attached_media won't reliably work for unregistered CPT parents
338 $attachedImages = $this->wpdb->get_results($this->wpdb->prepare(
339 "SELECT ID, post_excerpt FROM {$this->wpdb->posts}
340 WHERE post_parent = %d AND post_type = 'attachment' AND post_mime_type LIKE 'image%%'
341 ORDER BY menu_order ASC",
342 $oldTourId
343 ));
344 if (!empty($attachedImages)) {
345 foreach ($attachedImages as $attachment) {
346 $galleryImages[] = [
347 'id' => $attachment->ID,
348 'url' => wp_get_attachment_url($attachment->ID),
349 'thumbnail_url' => wp_get_attachment_thumb_url($attachment->ID),
350 'alt_text' => $this->getRawPostMeta($attachment->ID, '_wp_attachment_image_alt') ?? '',
351 'caption' => $attachment->post_excerpt
352 ];
353 }
354 } else {
355 }
356 }
357
358 // Migrate gallery images to new format
359 if (empty($galleryImages)) {
360 return;
361 }
362
363 $order = 0;
364 $migrated = 0;
365 $failed = 0;
366
367 foreach ($galleryImages as $index => $image) {
368 $imageUrl = '';
369 $thumbnailUrl = '';
370 $altText = '';
371 $caption = '';
372 $imageId = null;
373
374 // Handle different image data formats
375 if (is_numeric($image)) {
376 // Simple attachment ID (most common format from old Yatra)
377 $imageId = (int) $image;
378
379 // Skip invalid IDs (like 0)
380 if ($imageId <= 0) {
381 continue;
382 }
383
384 $imageUrl = wp_get_attachment_url($imageId);
385 if (!$imageUrl) {
386 continue;
387 }
388
389 $thumbnailUrl = wp_get_attachment_thumb_url($imageId) ?: $imageUrl;
390 $altText = $this->getRawPostMeta($imageId, '_wp_attachment_image_alt') ?? '';
391 $attachment = get_post($imageId);
392 if ($attachment) {
393 $caption = $attachment->post_excerpt ?: '';
394 }
395
396 } elseif (is_string($image)) {
397 // URL string
398 $imageUrl = $image;
399 $thumbnailUrl = $imageUrl;
400 } elseif (is_array($image)) {
401 // Array format
402 $imageId = !empty($image['id']) ? (int) $image['id'] : null;
403 $imageUrl = $image['url'] ?? $image['image_url'] ?? $image['src'] ?? '';
404 $thumbnailUrl = $image['thumbnail_url'] ?? $image['thumb'] ?? $imageUrl;
405 $altText = $image['alt_text'] ?? $image['alt'] ?? '';
406 $caption = $image['caption'] ?? $image['description'] ?? '';
407 }
408
409 if (!empty($imageUrl)) {
410 $imageMetadata = [];
411 if ($imageId) {
412 $imageMetadata['image_id'] = $imageId;
413 }
414 if (!empty($altText)) {
415 $imageMetadata['alt_text'] = $altText;
416 }
417
418 $result = $wpdb->insert(
419 $table,
420 [
421 'trip_id' => $newTripId,
422 'content_type' => 'image',
423 'title' => !empty($altText) ? $altText : (!empty($caption) ? $caption : null),
424 'description' => !empty($caption) ? $caption : null,
425 'content_url' => $imageUrl,
426 'thumbnail_url' => $thumbnailUrl,
427 'metadata' => !empty($imageMetadata) ? json_encode($imageMetadata) : null,
428 'sort_order' => $order,
429 'is_featured' => $order === 0 ? 1 : 0,
430 'created_at' => current_time('mysql')
431 ],
432 ['%d', '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%d', '%s']
433 );
434
435 if ($result) {
436 $migrated++;
437 $order++;
438 } else {
439 $failed++;
440 }
441 } else {
442 }
443 }
444
445 }
446
447 /**
448 * Migrate trip highlights from old tour system
449 */
450 private function migrateTripHighlights(int $oldTourId, int $newTripId, array $meta): void
451 {
452 global $wpdb;
453 $table = TripContentTable::getTableName();
454
455 // Clear existing highlights for this trip
456 $wpdb->delete($table, ['trip_id' => $newTripId, 'content_type' => 'highlight'], ['%d', '%s']);
457
458 $highlights = [];
459
460 // Try different possible highlights meta keys
461 $highlightsKeys = [
462 'yatra_tour_highlights',
463 'yatra_tour_meta_highlights',
464 'yatra_highlights',
465 'tour_highlights',
466 'yatra_tour_features',
467 'yatra_tour_meta_features',
468 'yatra_features',
469 'tour_features'
470 ];
471
472 foreach ($highlightsKeys as $key) {
473 if (!empty($meta[$key])) {
474 $highlightsData = $meta[$key];
475 if (is_string($highlightsData)) {
476 $highlightsData = maybe_unserialize($highlightsData);
477 }
478
479 if (is_array($highlightsData) && !empty($highlightsData)) {
480 $highlights = $highlightsData;
481 break;
482 }
483 }
484 }
485
486 // Migrate highlights
487 if (!empty($highlights)) {
488 $order = 0;
489 foreach ($highlights as $highlight) {
490 $title = '';
491 $description = '';
492 $icon = '';
493
494 if (is_string($highlight)) {
495 // Simple string - treat as title
496 $title = $highlight;
497 } elseif (is_array($highlight)) {
498 $title = $highlight['title'] ?? $highlight['name'] ?? $highlight['text'] ?? '';
499 $description = $highlight['description'] ?? $highlight['content'] ?? '';
500 $icon = $highlight['icon'] ?? $highlight['icon_class'] ?? '';
501 }
502
503 if (!empty($title)) {
504 $highlightMetadata = [];
505 if (!empty($icon)) {
506 $highlightMetadata['icon'] = $icon;
507 }
508 if (!empty($description)) {
509 $highlightMetadata['description'] = $description;
510 }
511
512 $wpdb->insert(
513 $table,
514 [
515 'trip_id' => $newTripId,
516 'content_type' => 'highlight',
517 'title' => $title,
518 'description' => !empty($description) ? $description : null,
519 'metadata' => !empty($highlightMetadata) ? json_encode($highlightMetadata) : null,
520 'sort_order' => $order,
521 'created_at' => current_time('mysql')
522 ],
523 ['%d', '%s', '%s', '%s', '%s', '%d', '%s']
524 );
525 $order++;
526 }
527 }
528
529 }
530 }
531
532 /**
533 * Migrate trip FAQs from old tour system
534 */
535 private function migrateTripFAQs(int $oldTourId, int $newTripId, array $meta): void
536 {
537 global $wpdb;
538 $table = TripContentTable::getTableName();
539
540 // Clear existing FAQs for this trip (unless force migration)
541 if (!$this->isForceMigration()) {
542 $deleted = $wpdb->delete($table, ['trip_id' => $newTripId, 'content_type' => 'faq'], ['%d', '%s']);
543 if ($deleted) {
544 }
545 }
546
547 $faqs = [];
548
549 // The primary FAQ meta key from old Yatra system
550 $faqKeys = [
551 'faq_repeator', // Primary: serialized array with faq_heading and faq_description
552 'yatra_tour_faqs',
553 'yatra_tour_meta_faqs',
554 'yatra_faqs',
555 'tour_faqs',
556 'yatra_tour_faq',
557 'yatra_tour_meta_faq',
558 'yatra_faq',
559 'tour_faq'
560 ];
561
562 foreach ($faqKeys as $key) {
563 if (isset($meta[$key]) && !empty($meta[$key])) {
564 $faqData = $meta[$key];
565 if (is_string($faqData)) {
566 $faqData = maybe_unserialize($faqData);
567 }
568
569 if (is_array($faqData) && !empty($faqData)) {
570 // Check if this is the faq_repeator format with faq_heading and faq_description
571 if (isset($faqData['faq_heading']) && isset($faqData['faq_description'])) {
572 $headings = $faqData['faq_heading'];
573 $descriptions = $faqData['faq_description'];
574
575 if (is_array($headings) && is_array($descriptions)) {
576 // Convert to standard format
577 $faqs = [];
578 $count = min(count($headings), count($descriptions));
579 for ($i = 0; $i < $count; $i++) {
580 if (!empty($headings[$i]) && !empty($descriptions[$i])) {
581 $faqs[] = [
582 'question' => $headings[$i],
583 'answer' => $descriptions[$i]
584 ];
585 }
586 }
587 break;
588 }
589 } else {
590 // Standard array format
591 $faqs = $faqData;
592 break;
593 }
594 }
595 }
596 }
597
598 // Migrate FAQs
599 if (empty($faqs)) {
600 return;
601 }
602
603 $order = 0;
604 $migrated = 0;
605 $failed = 0;
606
607 foreach ($faqs as $faq) {
608 $question = '';
609 $answer = '';
610
611 if (is_array($faq)) {
612 $question = $faq['question'] ?? $faq['q'] ?? $faq['title'] ?? '';
613 $answer = $faq['answer'] ?? $faq['a'] ?? $faq['content'] ?? $faq['description'] ?? '';
614 }
615
616 if (!empty($question) && !empty($answer)) {
617 $result = $wpdb->insert(
618 $table,
619 [
620 'trip_id' => $newTripId,
621 'content_type' => 'faq',
622 'title' => wp_kses_post($question),
623 'description' => wp_kses_post($answer),
624 'sort_order' => $order,
625 'created_at' => current_time('mysql')
626 ],
627 ['%d', '%s', '%s', '%s', '%d', '%s']
628 );
629
630 if ($result) {
631 $migrated++;
632 $order++;
633 } else {
634 $failed++;
635 }
636 } else {
637 }
638 }
639
640 }
641
642 /**
643 * Migrate trip categories relationship
644 */
645 private function migrateTripCategories(int $oldTripId, int $newTripId): void
646 {
647 global $wpdb;
648
649 // Categories: legacy sites used `trip_category` (3.x) or `tour_category` (some 2.x builds).
650 $categories = $wpdb->get_results($wpdb->prepare(
651 "SELECT t.term_id, t.name, t.slug, tt.taxonomy AS source_taxonomy
652 FROM {$wpdb->terms} t
653 INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
654 INNER JOIN {$wpdb->term_relationships} tr ON tt.term_taxonomy_id = tr.term_taxonomy_id
655 WHERE tr.object_id = %d AND tt.taxonomy IN ('trip_category', 'tour_category')",
656 $oldTripId
657 ));
658
659 if (empty($categories)) {
660 return;
661 }
662
663 $classificationsTable = \Yatra\Database\Tables\ClassificationsTable::getTableName();
664 $tripClassificationsTable = \Yatra\Database\Tables\TripClassificationsTable::getTableName();
665
666 foreach ($categories as $index => $category) {
667 // Find the migrated category in ClassificationsTable
668 $newCategoryId = $wpdb->get_var($wpdb->prepare(
669 "SELECT id FROM {$classificationsTable} WHERE slug = %s AND type = %s",
670 $category->slug,
671 \Yatra\Constants\ClassificationTypes::CATEGORY
672 ));
673
674 if (!$newCategoryId) {
675 // Term meta from {@see BaseMigration::migrateTaxonomy}: _yatra_migrated_{taxonomy}_id
676 $sourceTax = isset($category->source_taxonomy) && is_string($category->source_taxonomy)
677 ? $category->source_taxonomy
678 : 'trip_category';
679 $mappedId = $this->getRawTermMeta(
680 (int) $category->term_id,
681 sprintf('_yatra_migrated_%s_id', $sourceTax)
682 );
683 if (!$mappedId && $sourceTax === 'trip_category') {
684 $mappedId = $this->getRawTermMeta((int) $category->term_id, '_yatra_migrated_tour_category_id');
685 } elseif (!$mappedId && $sourceTax === 'tour_category') {
686 $mappedId = $this->getRawTermMeta((int) $category->term_id, '_yatra_migrated_trip_category_id');
687 }
688 if (!$mappedId) {
689 $mappedId = $this->getRawTermMeta((int) $category->term_id, '_yatra_migrated_category_id');
690 }
691 if ($mappedId) {
692 $newCategoryId = (int) $mappedId;
693 }
694 }
695
696 if ($newCategoryId) {
697 // Check if relationship already exists
698 $exists = $wpdb->get_var($wpdb->prepare(
699 "SELECT id FROM {$tripClassificationsTable}
700 WHERE trip_id = %d AND classification_id = %d AND classification_type = %s",
701 $newTripId,
702 $newCategoryId,
703 \Yatra\Constants\ClassificationTypes::CATEGORY
704 ));
705
706 if (!$exists) {
707 $wpdb->insert(
708 $tripClassificationsTable,
709 [
710 'trip_id' => $newTripId,
711 'classification_id' => $newCategoryId,
712 'classification_type' => \Yatra\Constants\ClassificationTypes::CATEGORY,
713 'relationship_type' => 'primary',
714 'sort_order' => $index,
715 'is_active' => 1,
716 'created_at' => current_time('mysql'),
717 'updated_at' => current_time('mysql'),
718 ]
719 );
720 }
721 }
722 }
723 }
724
725 /**
726 * Migrate trip attributes with values
727 */
728 private function migrateTripAttributes(int $oldTripId, int $newTripId): void
729 {
730 global $wpdb;
731
732 // Get custom attributes from post meta
733 $attributesData = $this->getRawPostMeta($oldTripId, 'tour_meta_custom_attributes');
734
735 if (empty($attributesData)) {
736 return;
737 }
738
739 // Unserialize if needed
740 if (is_string($attributesData)) {
741 $attributesData = maybe_unserialize($attributesData);
742 }
743
744 if (!is_array($attributesData) || empty($attributesData)) {
745 return;
746 }
747
748 $classificationsTable = \Yatra\Database\Tables\ClassificationsTable::getTableName();
749 $tripClassificationsTable = \Yatra\Database\Tables\TripClassificationsTable::getTableName();
750
751 $order = 0;
752 foreach ($attributesData as $termId => $attributeValue) {
753 // Get the old attribute term
754 $attribute = $wpdb->get_row($wpdb->prepare(
755 "SELECT t.term_id, t.name, t.slug
756 FROM {$wpdb->terms} t
757 INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
758 WHERE t.term_id = %d AND tt.taxonomy = 'attributes'",
759 $termId
760 ));
761
762 if (!$attribute) {
763 continue;
764 }
765
766 // Find the migrated attribute in ClassificationsTable
767 $newAttributeId = $wpdb->get_var($wpdb->prepare(
768 "SELECT id FROM {$classificationsTable} WHERE slug = %s AND type = %s",
769 $attribute->slug,
770 \Yatra\Constants\ClassificationTypes::ATTRIBUTE
771 ));
772
773 if (!$newAttributeId) {
774 // Try looking up by term meta mapping
775 $mappedId = $this->getRawTermMeta((int) $attribute->term_id, '_yatra_migrated_attributes_id');
776 if ($mappedId) {
777 $newAttributeId = (int) $mappedId;
778 }
779 }
780
781 if ($newAttributeId) {
782 // Check if relationship already exists
783 $exists = $wpdb->get_var($wpdb->prepare(
784 "SELECT id FROM {$tripClassificationsTable}
785 WHERE trip_id = %d AND classification_id = %d AND classification_type = %s",
786 $newTripId,
787 $newAttributeId,
788 \Yatra\Constants\ClassificationTypes::ATTRIBUTE
789 ));
790
791 if (!$exists) {
792 // Store attribute value in metadata JSON
793 $metadata = json_encode(['value' => $attributeValue]);
794
795 $wpdb->insert(
796 $tripClassificationsTable,
797 [
798 'trip_id' => $newTripId,
799 'classification_id' => $newAttributeId,
800 'classification_type' => \Yatra\Constants\ClassificationTypes::ATTRIBUTE,
801 'relationship_type' => 'primary',
802 'sort_order' => $order,
803 'metadata' => $metadata,
804 'is_active' => 1,
805 'created_at' => current_time('mysql'),
806 'updated_at' => current_time('mysql'),
807 ]
808 );
809 $order++;
810 }
811 }
812 }
813 }
814
815 /**
816 * Migrate trip pricing (traveler categories with pricing values)
817 *
818 * Old Plugin Structure:
819 * - Single Pricing: yatra_tour_meta_regular_price, yatra_tour_meta_sales_price
820 * - Multiple Pricing: yatra_multiple_pricing (serialized array)
821 * - Fields: pricing_label, regular_price, sales_price, pricing_per, group_size, minimum_pax, maximum_pax
822 *
823 * New Plugin Structure:
824 * - pricing_type: 'regular' or 'traveler_based'
825 * - price_types: JSON array with category_id, original_price, discounted_price, pricing_mode
826 */
827 private function migrateTripPricing(int $oldTripId, int $newTripId, array $meta): void
828 {
829 global $wpdb;
830
831 $classificationsTable = \Yatra\Database\Tables\ClassificationsTable::getTableName();
832 $tripsTable = \Yatra\Database\Tables\TripsTable::getTableName();
833
834 // Get multiple pricing data from old plugin (stored in yatra_multiple_pricing meta key)
835 $multiplePricing = $meta['yatra_multiple_pricing'] ?? null;
836
837 if (is_string($multiplePricing)) {
838 $multiplePricing = maybe_unserialize($multiplePricing);
839 }
840
841 // Check if we have valid multiple pricing data
842 if (empty($multiplePricing) || !is_array($multiplePricing)) {
843 // No multiple pricing, trip will use regular pricing (already migrated in main trip data)
844 return;
845 }
846
847 // Build price_types array for traveler-based pricing
848 $priceTypes = [];
849
850 foreach ($multiplePricing as $pricingId => $pricing) {
851 if (!is_array($pricing)) {
852 continue;
853 }
854
855 // Old plugin uses 'pricing_label' field for category name
856 $label = trim($pricing['pricing_label'] ?? '');
857 if (empty($label)) {
858 continue;
859 }
860
861 // Find the traveler category by label (case-insensitive)
862 $categoryId = $wpdb->get_var($wpdb->prepare(
863 "SELECT id FROM {$classificationsTable} WHERE LOWER(name) = LOWER(%s) AND type = %s",
864 $label,
865 \Yatra\Constants\ClassificationTypes::TRAVELER_TYPE
866 ));
867
868 if (!$categoryId) {
869 // Category doesn't exist in new system, skip this pricing entry
870 continue;
871 }
872
873 // Extract pricing values - old plugin uses 'regular_price' and 'sales_price'
874 $regularPrice = floatval($pricing['regular_price'] ?? 0);
875 $salePrice = floatval($pricing['sales_price'] ?? 0);
876
877 // Get pricing mode from old plugin (person/group)
878 $pricingPer = $pricing['pricing_per'] ?? 'person';
879 // Map old pricing_per to new pricing_mode
880 $pricingMode = ($pricingPer === 'group') ? 'per_group' : 'per_person';
881
882 // Get group size if pricing is per group
883 $groupSize = intval($pricing['group_size'] ?? 1);
884
885 // Get min/max travelers (pax)
886 $minPax = intval($pricing['minimum_pax'] ?? 1);
887 $maxPax = intval($pricing['maximum_pax'] ?? 1);
888
889 // Ensure min is at least 1
890 if ($minPax < 1) {
891 $minPax = 1;
892 }
893
894 // Ensure max is at least equal to min
895 if ($maxPax < $minPax) {
896 $maxPax = $minPax;
897 }
898
899 // Only add if regular price is valid
900 if ($regularPrice > 0) {
901 $priceType = [
902 'category_id' => (int) $categoryId,
903 'label' => $label,
904 'original_price' => $regularPrice,
905 'discounted_price' => ($salePrice > 0 && $salePrice < $regularPrice) ? $salePrice : null,
906 'pricing_mode' => $pricingMode,
907 'min_travelers' => $minPax,
908 'max_travelers' => $maxPax,
909 ];
910
911 // Add group_size if pricing is per group
912 if ($pricingMode === 'per_group' && $groupSize > 0) {
913 $priceType['group_size'] = $groupSize;
914 }
915
916 $priceTypes[] = $priceType;
917 }
918 }
919
920 // If we have valid price types, update the trip
921 if (!empty($priceTypes)) {
922 // Encode price_types as JSON
923 $priceTypesJson = json_encode($priceTypes);
924
925 // Update trip with price_types JSON AND set pricing_type to 'traveler_based'
926 $wpdb->update(
927 $tripsTable,
928 [
929 'price_types' => $priceTypesJson,
930 'pricing_type' => 'traveler_based'
931 ],
932 ['id' => $newTripId],
933 ['%s', '%s'],
934 ['%d']
935 );
936 }
937 }
938
939 /**
940 * Migrate availability date ranges from old tour system
941 */
942 private function migrateTripAvailabilityDates(int $oldTourId, int $newTripId, array $meta, array $tripData): void
943 {
944 global $wpdb;
945 $table = TripAvailabilityDatesTable::getTableName();
946
947 // Clear existing availability dates for this trip (for re-migration)
948 if ($this->isForceMigration()) {
949 // In force mode, don't delete - allow duplicates
950 } else {
951 // In regular mode, clear existing to avoid duplicates
952 $wpdb->delete($table, ['trip_id' => $newTripId], ['%d']);
953 }
954
955 // Try different possible availability date meta keys from old Yatra
956 $availabilityKeys = [
957 'yatra_tour_meta_availability_date_ranges', // Most common format from old system
958 'yatra_tour_availability_date_ranges',
959 'yatra_tour_meta_availability_dates',
960 'yatra_tour_availability_dates',
961 'yatra_availability_dates',
962 'tour_availability_dates',
963 'yatra_tour_fixed_departure_dates',
964 'yatra_tour_meta_fixed_departure',
965 'yatra_fixed_departure_dates',
966 ];
967
968 $availabilityData = null;
969 $foundKey = null;
970
971 // Also try direct database query to ensure we're not missing data
972 $directMetaQuery = $wpdb->get_results($wpdb->prepare(
973 "SELECT meta_key, meta_value FROM {$wpdb->prefix}postmeta WHERE post_id = %d AND meta_key LIKE %s",
974 $oldTourId,
975 '%availability%'
976 ));
977
978 if (!empty($directMetaQuery)) {
979 foreach ($directMetaQuery as $metaRow) {
980 }
981 }
982
983 foreach ($availabilityKeys as $key) {
984 if (isset($meta[$key])) {
985 $availabilityData = $meta[$key];
986 if (is_string($availabilityData)) {
987 $availabilityData = maybe_unserialize($availabilityData);
988 if (is_array($availabilityData) || is_object($availabilityData)) {
989 }
990 }
991
992 // Check if it's an empty array string like '[]'
993 if (is_string($availabilityData) && trim($availabilityData) === '[]') {
994 continue;
995 }
996
997 if (!empty($availabilityData)) {
998 $foundKey = $key;
999 break;
1000 }
1001 }
1002 }
1003
1004 if (empty($availabilityData)) {
1005 return;
1006 }
1007
1008 // Parse availability date ranges
1009 $dateRanges = [];
1010
1011 // First, try to decode as JSON if it's a string (common format in database)
1012 if (is_string($availabilityData)) {
1013 $jsonDecoded = json_decode($availabilityData, true);
1014 if (json_last_error() === JSON_ERROR_NONE && is_array($jsonDecoded)) {
1015 $availabilityData = $jsonDecoded;
1016 }
1017 }
1018
1019 if (is_array($availabilityData)) {
1020 // Handle array format
1021 foreach ($availabilityData as $dateRange) {
1022 if (is_array($dateRange) || is_object($dateRange)) {
1023 // Convert object to array if needed
1024 $dateRange = (array) $dateRange;
1025
1026 // Format: ['start' => '2025-12-25', 'end' => '2026-01-18']
1027 $start = $dateRange['start'] ?? $dateRange['start_date'] ?? $dateRange['from'] ?? null;
1028 $end = $dateRange['end'] ?? $dateRange['end_date'] ?? $dateRange['to'] ?? null;
1029
1030 if ($start && $end) {
1031 $dateRanges[] = ['start' => $start, 'end' => $end];
1032 }
1033 } elseif (is_string($dateRange)) {
1034 // Format: "2025-12-25 - 2026-01-18"
1035 $parts = preg_split('/\s*[-–—]\s*/', $dateRange, 2);
1036 if (count($parts) === 2) {
1037 $dateRanges[] = ['start' => trim($parts[0]), 'end' => trim($parts[1])];
1038 }
1039 }
1040 }
1041 } elseif (is_string($availabilityData)) {
1042 // Handle plain string format with multiple ranges separated by comma or newline
1043 $lines = preg_split('/[,\n\r]+/', $availabilityData);
1044 foreach ($lines as $line) {
1045 $line = trim($line);
1046 if (empty($line)) continue;
1047
1048 // Format: "2025-12-25 - 2026-01-18"
1049 $parts = preg_split('/\s*[-–—]\s*/', $line, 2);
1050 if (count($parts) === 2) {
1051 $dateRanges[] = ['start' => trim($parts[0]), 'end' => trim($parts[1])];
1052 }
1053 }
1054 }
1055
1056 if (empty($dateRanges)) {
1057 return;
1058 }
1059
1060 // Create availability dates for each range
1061 $created = 0;
1062 foreach ($dateRanges as $range) {
1063 $startDate = $range['start'];
1064 $endDate = $range['end'];
1065
1066 // Validate dates
1067 if (!strtotime($startDate) || !strtotime($endDate)) {
1068 continue;
1069 }
1070
1071 // Calculate duration
1072 $start = new \DateTime($startDate);
1073 $end = new \DateTime($endDate);
1074 $duration = $start->diff($end)->days;
1075
1076 // Determine if single day or multi-day trip
1077 $isSingleDay = $duration === 0 || ($tripData['duration_days'] ?? 1) === 1;
1078
1079 // Create availability date entry
1080 $availabilityEntry = [
1081 'trip_id' => $newTripId,
1082 'departure_date' => $startDate,
1083 'arrival_date' => $isSingleDay ? $startDate : $endDate,
1084 'return_date' => $endDate,
1085 'departure_time' => null,
1086 'arrival_time' => null,
1087 'seats_total' => $tripData['max_travelers'] ?? 10,
1088 'seats_available' => $tripData['max_travelers'] ?? 10,
1089 'seats_reserved' => 0,
1090 'seats_waitlist' => 0,
1091 'pricing_type' => 'regular',
1092 'original_price' => $tripData['original_price'] ?? null,
1093 'discounted_price' => $tripData['sale_price'] ?? null,
1094 'discount_percentage' => null,
1095 'price_types' => null,
1096 'status' => 'available',
1097 'from_location' => null,
1098 'to_location' => null,
1099 'from_latitude' => null,
1100 'from_longitude' => null,
1101 'to_latitude' => null,
1102 'to_longitude' => null,
1103 'special_notes' => null,
1104 'cutoff_date' => null,
1105 'cutoff_hours' => 24,
1106 'created_at' => current_time('mysql'),
1107 'updated_at' => current_time('mysql'),
1108 ];
1109
1110 $inserted = $wpdb->insert(
1111 $table,
1112 $availabilityEntry,
1113 [
1114 '%d', '%s', '%s', '%s', '%s', '%s', '%d', '%d', '%d', '%d',
1115 '%s', '%f', '%f', '%f', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s',
1116 ]
1117 );
1118
1119 if ($inserted) {
1120 $created++;
1121 } else {
1122 }
1123 }
1124
1125 if ($created > 0) {
1126 }
1127 }
1128 }
1129