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

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

579 lines 21.3 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\Constants\ClassificationTypes;
6 use Yatra\Database\Tables\ClassificationsTable;
7 use Yatra\Database\Tables\TripsTable;
8 use Yatra\Utils\Logger;
9
10 /**
11 * Migration for traveler categories (multiple pricing options)
12 *
13 * Migrates multiple pricing options from old Yatra to traveler categories system
14 * (ClassificationsTable with type='traveler_type') and assigns pricing to trips
15 * via the price_types JSON column on TripsTable.
16 */
17 class TravelerCategoriesMigration extends BaseMigration
18 {
19 /**
20 * Run the traveler categories migration
21 */
22 public function run(): array
23 {
24 $migrated = 0;
25 $skipped = 0;
26 $failed = 0;
27
28 // First, create traveler categories from multiple pricing options
29 $categoryResult = $this->createTravelerCategories();
30 $migrated += $categoryResult['migrated'];
31 $skipped += $categoryResult['skipped'];
32 $failed += $categoryResult['failed'];
33
34 // Then, assign categories to trips
35 $assignmentResult = $this->assignCategoriesToTrips();
36 $migrated += $assignmentResult['migrated'];
37 $skipped += $assignmentResult['skipped'];
38 $failed += $assignmentResult['failed'];
39
40 return [
41 'success' => true,
42 'data_type' => 'traveler_categories',
43 'migrated' => $migrated,
44 'skipped' => $skipped,
45 'failed' => $failed,
46 'details' => [
47 'categories_created' => $categoryResult['migrated'],
48 'assignments_created' => $assignmentResult['migrated']
49 ]
50 ];
51 }
52
53 /**
54 * Create traveler categories from multiple pricing options
55 */
56 private function createTravelerCategories(): array
57 {
58 $migrated = 0;
59 $skipped = 0;
60 $failed = 0;
61
62 // Get all unique pricing options from old tours
63 $uniquePricingOptions = $this->getUniquePricingOptions();
64 $total = count($uniquePricingOptions);
65
66 Logger::info("Traveler Categories Migration: Starting category creation", [
67 'total_options_found' => $total,
68 'force_migration' => $this->service->isForceMigration()
69 ]);
70
71 // During force migration, we preserve ALL existing data and only add new categories
72 // If a category with the same label exists, we create a new one with a unique slug
73 // This ensures NO data is lost during re-migration
74
75 foreach ($uniquePricingOptions as $pricingOption) {
76 try {
77 Logger::info("Processing pricing option for category creation", [
78 'label' => $pricingOption['label'],
79 'description' => $pricingOption['description'] ?? ''
80 ]);
81
82 // Check if category already exists
83 $exists = $this->categoryExists($pricingOption['label']);
84 Logger::info("Category existence check", [
85 'label' => $pricingOption['label'],
86 'exists' => $exists,
87 'force_migration' => $this->service->isForceMigration()
88 ]);
89
90 // During normal migration: skip existing categories
91 if ($exists && !$this->service->isForceMigration()) {
92 $skipped++;
93 Logger::info("Category already exists, skipping (normal migration)", [
94 'label' => $pricingOption['label']
95 ]);
96 continue;
97 }
98
99 // During force migration: ALWAYS create new category to preserve existing data
100 // The generateUniqueSlug() method will handle slug conflicts automatically
101 if ($exists && $this->service->isForceMigration()) {
102 Logger::info("Category already exists but force migration enabled - creating new category with unique slug", [
103 'label' => $pricingOption['label'],
104 'note' => 'Existing category will be preserved, new category will have unique slug'
105 ]);
106 }
107
108 // Create traveler category (will handle slug conflicts automatically)
109 $categoryId = $this->createCategory([
110 'label' => $pricingOption['label'],
111 'slug' => sanitize_title($pricingOption['label']),
112 'description' => $pricingOption['description'] ?? '',
113 'is_active' => 1
114 ]);
115
116 if ($categoryId) {
117 $migrated++;
118 Logger::info("Traveler category created successfully", [
119 'category_id' => $categoryId,
120 'label' => $pricingOption['label'],
121 'migration_type' => $this->service->isForceMigration() ? 'force' : 'normal'
122 ]);
123 } else {
124 $failed++;
125 Logger::error("Failed to create traveler category - no ID returned", [
126 'label' => $pricingOption['label']
127 ]);
128 }
129 } catch (\Exception $e) {
130 $failed++;
131 Logger::error("Failed to create traveler category - exception", [
132 'label' => $pricingOption['label'],
133 'error' => $e->getMessage(),
134 'trace' => $e->getTraceAsString()
135 ]);
136 }
137 }
138
139 Logger::info("Traveler Categories Migration: Category creation completed", [
140 'migrated' => $migrated,
141 'skipped' => $skipped,
142 'failed' => $failed,
143 'total' => $total,
144 'force_migration' => $this->service->isForceMigration(),
145 'note' => 'NO existing data was deleted during this migration'
146 ]);
147
148 return compact('migrated', 'skipped', 'failed', 'total');
149 }
150
151 /**
152 * Assign categories to trips based on multiple pricing
153 */
154 private function assignCategoriesToTrips(): array
155 {
156 $migrated = 0;
157 $skipped = 0;
158 $failed = 0;
159
160 // Get all tours with multiple pricing
161 $toursWithMultiplePricing = $this->getToursWithMultiplePricing();
162 $total = count($toursWithMultiplePricing);
163
164 Logger::info("Traveler Categories Migration: Starting trip assignments", [
165 'total_tours_to_process' => $total
166 ]);
167
168 foreach ($toursWithMultiplePricing as $tour) {
169 try {
170 $oldTourId = $tour->ID;
171
172 Logger::info("Processing tour for assignment", [
173 'old_tour_id' => $oldTourId,
174 'tour_title' => $tour->post_title
175 ]);
176
177 // Check if trip was migrated
178 $newTripId = $this->getMigratedTripId($oldTourId);
179 Logger::info("Trip migration check", [
180 'old_tour_id' => $oldTourId,
181 'new_trip_id' => $newTripId,
182 'is_migrated' => !empty($newTripId)
183 ]);
184
185 if (!$newTripId) {
186 $skipped++;
187 Logger::info("Trip not migrated, skipping", [
188 'old_tour_id' => $oldTourId
189 ]);
190 continue;
191 }
192
193 // Get multiple pricing for this tour
194 $multiplePricing = $this->getRawPostMeta($oldTourId, 'yatra_multiple_pricing');
195 $multiplePricing = maybe_unserialize($multiplePricing);
196 Logger::info("Multiple pricing data", [
197 'old_tour_id' => $oldTourId,
198 'pricing_data' => $multiplePricing,
199 'is_array' => is_array($multiplePricing)
200 ]);
201
202 if (!is_array($multiplePricing)) {
203 $skipped++;
204 Logger::info("No valid multiple pricing data, skipping", [
205 'old_tour_id' => $oldTourId
206 ]);
207 continue;
208 }
209
210 // Assign each pricing option as a category
211 $assignmentsCreated = 0;
212 foreach ($multiplePricing as $pricingId => $pricing) {
213 Logger::info("Processing pricing assignment", [
214 'old_tour_id' => $oldTourId,
215 'new_trip_id' => $newTripId,
216 'pricing_id' => $pricingId,
217 'pricing_data' => $pricing
218 ]);
219
220 if (empty($pricing['pricing_label'])) {
221 Logger::info("Empty pricing label, skipping", [
222 'pricing_id' => $pricingId
223 ]);
224 continue;
225 }
226
227 $categoryId = $this->getCategoryIdByLabel($pricing['pricing_label']);
228 Logger::info("Category ID lookup", [
229 'label' => $pricing['pricing_label'],
230 'category_id' => $categoryId
231 ]);
232
233 if (!$categoryId) {
234 Logger::warning("Category not found for label", [
235 'label' => $pricing['pricing_label']
236 ]);
237 continue;
238 }
239
240 // Create price type assignment
241 $priceTypeId = $this->createPriceType([
242 'trip_id' => $newTripId,
243 'category_id' => $categoryId,
244 'label' => $pricing['pricing_label'],
245 'description' => $pricing['pricing_description'] ?? '',
246 'regular_price' => (float) ($pricing['regular_price'] ?? 0),
247 'sales_price' => (float) ($pricing['sales_price'] ?? 0),
248 'minimum_pax' => (int) ($pricing['minimum_pax'] ?? 1),
249 'maximum_pax' => (int) ($pricing['maximum_pax'] ?? 0),
250 'group_size' => (int) ($pricing['group_size'] ?? 1),
251 'pricing_per' => $pricing['pricing_per'] ?? 'person',
252 'is_active' => 1
253 ]);
254
255 Logger::info("Price type creation result", [
256 'trip_id' => $newTripId,
257 'category_id' => $categoryId,
258 'price_type_id' => $priceTypeId,
259 'success' => !empty($priceTypeId)
260 ]);
261
262 if ($priceTypeId) {
263 $assignmentsCreated++;
264 }
265 }
266
267 if ($assignmentsCreated > 0) {
268 $migrated += $assignmentsCreated;
269 Logger::info("Price types assigned to trip successfully", [
270 'trip_id' => $newTripId,
271 'old_tour_id' => $oldTourId,
272 'assignments' => $assignmentsCreated
273 ]);
274 } else {
275 $skipped++;
276 Logger::info("No price types created for tour", [
277 'old_tour_id' => $oldTourId,
278 'new_trip_id' => $newTripId
279 ]);
280 }
281 } catch (\Exception $e) {
282 $failed++;
283 Logger::error("Failed to assign categories to trip", [
284 'tour_id' => $tour->ID,
285 'error' => $e->getMessage(),
286 'trace' => $e->getTraceAsString()
287 ]);
288 }
289 }
290
291 Logger::info("Traveler Categories Migration: Trip assignments completed", [
292 'migrated' => $migrated,
293 'skipped' => $skipped,
294 'failed' => $failed,
295 'total' => $total
296 ]);
297
298 return compact('migrated', 'skipped', 'failed', 'total');
299 }
300
301 /**
302 * Get unique pricing options from all tours
303 */
304 private function getUniquePricingOptions(): array
305 {
306 $sql = "
307 SELECT DISTINCT
308 pm.meta_value as pricing_data,
309 pm.post_id
310 FROM {$this->wpdb->postmeta} pm
311 INNER JOIN {$this->wpdb->posts} p ON pm.post_id = p.ID
312 WHERE pm.meta_key = 'yatra_multiple_pricing'
313 AND p.post_type = 'tour'
314 AND pm.meta_value != ''
315 AND pm.meta_value != 'a:0:{}'
316 ";
317
318 $results = $this->wpdb->get_results($sql);
319 $uniqueOptions = [];
320
321 Logger::info("Traveler Categories Migration: Found tours with multiple pricing", [
322 'total_tours_found' => count($results)
323 ]);
324
325 foreach ($results as $result) {
326 $pricingData = maybe_unserialize($result->pricing_data);
327 Logger::info("Processing tour for pricing options", [
328 'post_id' => $result->post_id,
329 'pricing_data_serialized' => $result->pricing_data,
330 'pricing_data_unserialized' => $pricingData,
331 'is_array' => is_array($pricingData)
332 ]);
333
334 if (is_array($pricingData)) {
335 foreach ($pricingData as $pricingId => $pricing) {
336 Logger::info("Processing pricing option", [
337 'post_id' => $result->post_id,
338 'pricing_id' => $pricingId,
339 'pricing_data' => $pricing,
340 'has_label' => !empty($pricing['pricing_label'])
341 ]);
342
343 if (!empty($pricing['pricing_label'])) {
344 $label = $pricing['pricing_label'];
345 if (!isset($uniqueOptions[$label])) {
346 $uniqueOptions[$label] = [
347 'label' => $label,
348 'description' => $pricing['pricing_description'] ?? ''
349 ];
350 Logger::info("Added unique pricing option", [
351 'label' => $label,
352 'description' => $pricing['pricing_description'] ?? ''
353 ]);
354 }
355 }
356 }
357 }
358 }
359
360 Logger::info("Traveler Categories Migration: Final unique pricing options", [
361 'total_unique_options' => count($uniqueOptions),
362 'options' => array_values($uniqueOptions)
363 ]);
364
365 return array_values($uniqueOptions);
366 }
367
368 /**
369 * Get tours that have multiple pricing
370 */
371 private function getToursWithMultiplePricing(): array
372 {
373 $sql = "
374 SELECT p.ID, p.post_title, pm.meta_value as pricing_data
375 FROM {$this->wpdb->posts} p
376 INNER JOIN {$this->wpdb->postmeta} pm ON p.ID = pm.post_id
377 WHERE p.post_type = 'tour'
378 AND pm.meta_key = 'yatra_multiple_pricing'
379 AND pm.meta_value != ''
380 AND pm.meta_value != 'a:0:{}'
381 AND p.post_status != 'trash'
382 ";
383
384 $tours = $this->wpdb->get_results($sql);
385
386 Logger::info("Traveler Categories Migration: Found tours for assignment", [
387 'total_tours' => count($tours)
388 ]);
389
390 foreach ($tours as $tour) {
391 $pricingData = maybe_unserialize($tour->pricing_data);
392 Logger::info("Tour assignment data", [
393 'tour_id' => $tour->ID,
394 'tour_title' => $tour->post_title,
395 'pricing_data' => $pricingData,
396 'is_array' => is_array($pricingData),
397 'pricing_count' => is_array($pricingData) ? count($pricingData) : 0
398 ]);
399 }
400
401 return $tours;
402 }
403
404 /**
405 * Check if traveler category exists by label in ClassificationsTable
406 */
407 private function categoryExists(string $label): bool
408 {
409 $table = ClassificationsTable::getTableName();
410 $type = ClassificationTypes::TRAVELER_TYPE;
411
412 return (int) $this->wpdb->get_var($this->wpdb->prepare(
413 "SELECT COUNT(*) FROM {$table} WHERE type = %s AND name = %s",
414 $type,
415 $label
416 )) > 0;
417 }
418
419 /**
420 * Get traveler category ID by label from ClassificationsTable
421 */
422 private function getCategoryIdByLabel(string $label): ?int
423 {
424 $table = ClassificationsTable::getTableName();
425 $type = ClassificationTypes::TRAVELER_TYPE;
426
427 $id = $this->wpdb->get_var($this->wpdb->prepare(
428 "SELECT id FROM {$table} WHERE type = %s AND name = %s LIMIT 1",
429 $type,
430 $label
431 ));
432
433 return $id ? (int) $id : null;
434 }
435
436 /**
437 * Create traveler category in ClassificationsTable with type='traveler_type'
438 */
439 private function createCategory(array $data): ?int
440 {
441 $table = ClassificationsTable::getTableName();
442 $type = ClassificationTypes::TRAVELER_TYPE;
443
444 // Generate unique slug scoped to traveler_type
445 $baseSlug = sanitize_title($data['label']);
446 $slug = $baseSlug;
447 $counter = 1;
448 while ($this->slugExists($slug)) {
449 $slug = $baseSlug . '-' . $counter;
450 $counter++;
451 }
452
453 $currentUserId = get_current_user_id() ?: 1;
454
455 $result = $this->wpdb->insert(
456 $table,
457 [
458 'type' => $type,
459 'name' => $data['label'],
460 'slug' => $slug,
461 'description' => $data['description'] ?? '',
462 'status' => !empty($data['is_active']) ? 'publish' : 'draft',
463 'created_at' => current_time('mysql'),
464 'updated_at' => current_time('mysql'),
465 'created_by' => $currentUserId,
466 'updated_by' => $currentUserId,
467 ],
468 ['%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%d']
469 );
470
471 return $result ? (int) $this->wpdb->insert_id : null;
472 }
473
474 /**
475 * Check if slug exists for traveler_type in ClassificationsTable
476 */
477 private function slugExists(string $slug): bool
478 {
479 $table = ClassificationsTable::getTableName();
480 $type = ClassificationTypes::TRAVELER_TYPE;
481
482 return (int) $this->wpdb->get_var($this->wpdb->prepare(
483 "SELECT COUNT(*) FROM {$table} WHERE type = %s AND slug = %s",
484 $type,
485 $slug
486 )) > 0;
487 }
488
489 /**
490 * Create/update price type assignment for a trip.
491 *
492 * Appends a pricing entry to the trip's price_types JSON column in TripsTable.
493 * Each entry links a traveler category (from ClassificationsTable) with pricing.
494 */
495 private function createPriceType(array $data): ?int
496 {
497 $tripsTable = TripsTable::getTableName();
498 $tripId = (int) $data['trip_id'];
499
500 // Read existing price_types JSON
501 $existingJson = $this->wpdb->get_var($this->wpdb->prepare(
502 "SELECT price_types FROM {$tripsTable} WHERE id = %d",
503 $tripId
504 ));
505
506 $priceTypes = [];
507 if (!empty($existingJson)) {
508 $decoded = json_decode($existingJson, true);
509 if (is_array($decoded)) {
510 $priceTypes = $decoded;
511 }
512 }
513
514 // Check for duplicate (same category_id)
515 foreach ($priceTypes as $existing) {
516 if (($existing['category_id'] ?? null) == $data['category_id']) {
517 return (int) $data['category_id']; // Already exists
518 }
519 }
520
521 // Canonical keys must match TripMigration::migrateTripPricing() and TripController
522 // (original_price / discounted_price / pricing_mode / min_travelers / max_travelers).
523 $regular = (float) ($data['regular_price'] ?? 0);
524 $sales = (float) ($data['sales_price'] ?? 0);
525 $discounted = ($sales > 0 && $sales < $regular) ? $sales : null;
526 $pricingPer = $data['pricing_per'] ?? 'person';
527 $pricingMode = ($pricingPer === 'group') ? 'per_group' : 'per_person';
528 $minPax = max(1, (int) ($data['minimum_pax'] ?? 1));
529 $maxPax = (int) ($data['maximum_pax'] ?? 0);
530 if ($maxPax < $minPax) {
531 $maxPax = $minPax;
532 }
533
534 $entry = [
535 'category_id' => (int) $data['category_id'],
536 'label' => $data['label'] ?? '',
537 'original_price' => $regular,
538 'discounted_price' => $discounted,
539 'pricing_mode' => $pricingMode,
540 'min_travelers' => $minPax,
541 'max_travelers' => $maxPax,
542 ];
543 if (!empty($data['description'])) {
544 $entry['description'] = $data['description'];
545 }
546 if ($pricingMode === 'per_group') {
547 $entry['group_size'] = max(1, (int) ($data['group_size'] ?? 1));
548 }
549
550 $priceTypes[] = $entry;
551
552 $updated = $this->wpdb->update(
553 $tripsTable,
554 ['price_types' => json_encode($priceTypes)],
555 ['id' => $tripId],
556 ['%s'],
557 ['%d']
558 );
559
560 if ($updated !== false) {
561 // Also update pricing_type to 'traveler_based' on the trip
562 $this->wpdb->update(
563 $tripsTable,
564 ['pricing_type' => 'traveler_based'],
565 ['id' => $tripId],
566 ['%s'],
567 ['%d']
568 );
569 return (int) $data['category_id'];
570 }
571
572 Logger::error("Failed to update price_types for trip", [
573 'trip_id' => $tripId,
574 'error' => $this->wpdb->last_error
575 ]);
576 return null;
577 }
578 }
579