PluginProbe
Yatra – Travel Booking & Tour Operator Software / trunk
Yatra – Travel Booking & Tour Operator Software vtrunk
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 2.0.10 All 83 releases
yatra / app / Migrations / BookingMigration.php

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

572 lines 24.6 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\Database\Tables\BookingsTable;
6 use Yatra\Database\Tables\BookingPaymentsTable;
7 use Yatra\Database\Tables\CustomersTable;
8 use Yatra\Migration\MigrationProgress;
9 use Yatra\Utils\Logger;
10
11 /**
12 * BookingMigration - Migrates bookings from old Yatra CPTs to new custom tables.
13 *
14 * Old system:
15 * CPT 'yatra-booking' in wp_posts
16 * - post_status: yatra-pending, yatra-processing, yatra-on-hold, yatra-completed, yatra-cancelled, yatra-failed
17 * - post_meta 'yatra_booking_meta': array of tour booking data per tour
18 * Each entry: tour_id, tour_name, selected_date, pricing, pricing_type,
19 * duration_days, duration_nights, country, yatra_currency,
20 * number_of_person, total_tour_price, total_tour_final_price
21 * - post_meta 'yatra_booking_meta_params': booking parameters
22 * Keys: total_booking_price, yatra_currency, booking_date,
23 * yatra_tour_customer_info (fullname, email, phone, country),
24 * booking_code, total_booking_gross_price, total_booking_net_price,
25 * coupon (code, type, value, discount_amount), tax_rate, tax_amount
26 * - post_meta 'yatra_customer_id': old customer post ID
27 * - post_meta 'yatra_user_id': WordPress user ID
28 *
29 * CPT 'yatra-payment' in wp_posts
30 * - post_status: processing, publish (completed), hold, refunded, failed
31 * - post_meta: booking_id, payment_gateway, total_amount, currency_code,
32 * paid_amount, payable_amount, due_amount, payment_type,
33 * installment, transaction_id, booking_details
34 *
35 * New system:
36 * wp_yatra_bookings + wp_yatra_booking_payments
37 */
38 class BookingMigration extends BaseMigration
39 {
40 public function __construct(MigrationProgress $service)
41 {
42 parent::__construct($service);
43 }
44
45 public function run(): array
46 {
47 $migrated = 0;
48 $skipped = 0;
49 $failed = 0;
50
51 // Check if old booking post type exists at all
52 $postTypeCheck = $this->wpdb->get_var(
53 "SELECT COUNT(*) FROM {$this->wpdb->posts} WHERE post_type = 'yatra-booking'"
54 );
55
56 // Get old bookings
57 $oldBookings = $this->wpdb->get_results(
58 "SELECT ID, post_title, post_status, post_date, post_content, post_modified
59 FROM {$this->wpdb->posts}
60 WHERE post_type = 'yatra-booking'
61 AND post_status NOT IN ('trash', 'auto-draft')
62 ORDER BY ID ASC"
63 );
64
65 $total = count($oldBookings);
66
67 foreach ($oldBookings as $oldBooking) {
68 try {
69 // Check if already migrated
70 $migratedId = $this->getRawPostMeta($oldBooking->ID, '_migrated_to_booking_id');
71 if ($migratedId && !$this->isForceMigration()) {
72 $skipped++;
73 $this->updateProgress('bookings', 'running', $migrated, $skipped, $failed, $total, null, null);
74 continue;
75 }
76
77 $meta = $this->getPostMeta($oldBooking->ID);
78
79 // Parse booking meta arrays
80 $bookingMeta = maybe_unserialize($meta['yatra_booking_meta'] ?? '');
81 $bookingParams = maybe_unserialize($meta['yatra_booking_meta_params'] ?? '');
82
83 if (!is_array($bookingMeta)) {
84 $bookingMeta = [];
85 }
86 if (!is_array($bookingParams)) {
87 $bookingParams = [];
88 }
89
90 // Get the first tour entry from booking meta (most bookings have one tour)
91 $firstTour = !empty($bookingMeta) ? reset($bookingMeta) : [];
92 $oldTourId = (int) ($firstTour['yatra_tour_id'] ?? 0);
93
94 // Validate old tour ID
95 if ($oldTourId <= 0) {
96 $failed++;
97 Logger::warning("Booking {$oldBooking->ID}: Invalid tour ID", [
98 'source' => 'migration',
99 'data_type' => 'bookings',
100 'booking_id' => $oldBooking->ID,
101 'tour_id' => $oldTourId
102 ]);
103 $this->updateProgress('bookings', 'running', $migrated, $skipped, $failed, $total, null, null);
104 continue;
105 }
106
107 // Extract booking code early for error logging.
108 // Old plugin stores booking_code with a '#' prefix (e.g. '#abc1234567').
109 // Strip it so the new 'reference' column stays clean.
110 $rawBookingCode = $bookingParams['booking_code'] ?? ('YTR-' . $oldBooking->ID);
111 $bookingCode = ltrim($rawBookingCode, '#');
112
113 // Map old trip ID to new trip ID
114 $newTripId = $oldTourId > 0 ? $this->getMigratedTripId($oldTourId) : null;
115
116 // If initial mapping failed, try fallback methods
117 if (!$newTripId) {
118 // Try to find trip by title if tour_id mapping doesn't exist
119 $tourName = $firstTour['yatra_tour_name'] ?? '';
120
121 if (!empty($tourName)) {
122 $newTripId = $this->wpdb->get_var($this->wpdb->prepare(
123 "SELECT id FROM " . \Yatra\Database\Tables\TripsTable::getTableName() . " WHERE title = %s LIMIT 1",
124 $tourName
125 ));
126 }
127
128 // If still not found, try by slug
129 if (!$newTripId && !empty($tourName)) {
130 $slug = sanitize_title($tourName);
131 $newTripId = $this->wpdb->get_var($this->wpdb->prepare(
132 "SELECT id FROM " . \Yatra\Database\Tables\TripsTable::getTableName() . " WHERE slug = %s LIMIT 1",
133 $slug
134 ));
135 }
136
137 // Last resort: check if old tour post still exists and get its title
138 if (!$newTripId && $oldTourId > 0) {
139 $oldTourPost = $this->wpdb->get_row($this->wpdb->prepare(
140 "SELECT post_title FROM {$this->wpdb->posts} WHERE ID = %d AND post_type = 'tour'",
141 $oldTourId
142 ));
143
144 if ($oldTourPost && !empty($oldTourPost->post_title)) {
145 $newTripId = $this->wpdb->get_var($this->wpdb->prepare(
146 "SELECT id FROM " . \Yatra\Database\Tables\TripsTable::getTableName() . " WHERE title = %s LIMIT 1",
147 $oldTourPost->post_title
148 ));
149 }
150 }
151 }
152
153 // If still no trip found, fail this booking
154 if (!$newTripId) {
155 $failed++;
156 Logger::warning("Booking {$oldBooking->ID}: No matching new trip found", [
157 'source' => 'migration',
158 'data_type' => 'bookings',
159 'old_tour_id' => $oldTourId,
160 'tour_name' => $firstTour['yatra_tour_name'] ?? 'N/A',
161 'booking_code' => $bookingCode,
162 ]);
163 $this->updateProgress('bookings', 'running', $migrated, $skipped, $failed, $total, null, null);
164 continue;
165 }
166
167 // Start database transaction for data integrity
168 $this->wpdb->query('START TRANSACTION');
169
170 try {
171 // Map old customer to new customer
172 $oldCustomerId = (int) ($meta['yatra_customer_id'] ?? 0);
173 $newCustomerId = null;
174 $wpUserId = $meta['yatra_user_id'] ?? null;
175
176 if ($oldCustomerId > 0) {
177 $newCustomerId = (int) ($this->getRawPostMeta($oldCustomerId, '_migrated_to_customer_id') ?: 0) ?: null;
178 }
179
180 // If no migrated customer found, try to look up by email
181 $customerInfo = $bookingParams['yatra_tour_customer_info'] ?? [];
182 if (is_string($customerInfo)) {
183 $customerInfo = maybe_unserialize($customerInfo);
184 }
185 if (!is_array($customerInfo)) {
186 $customerInfo = [];
187 }
188
189 $contactEmail = $customerInfo['email'] ?? '';
190 $contactPhone = $customerInfo['phone'] ?? $customerInfo['phone_number'] ?? '';
191 $contactCountry = $customerInfo['country'] ?? $firstTour['country'] ?? '';
192 $nameParts = $this->parseFullName($customerInfo['fullname'] ?? $customerInfo['full_name'] ?? '');
193
194 // Map old booking status to new status
195 $newStatus = $this->mapBookingStatus($oldBooking->post_status);
196
197 // Extract pricing info
198 $totalAmount = (float) ($bookingParams['total_booking_price'] ?? $bookingParams['total_booking_net_price'] ?? 0);
199 $grossAmount = (float) ($bookingParams['total_booking_gross_price'] ?? $totalAmount);
200 $currency = $bookingParams['yatra_currency'] ?? 'USD';
201 $bookingDate = $bookingParams['booking_date'] ?? $oldBooking->post_date;
202
203 // Calculate total travelers across all tours in this booking
204 $totalTravelers = 0;
205 foreach ($bookingMeta as $tourEntry) {
206 $pax = $tourEntry['number_of_person'] ?? 1;
207 $totalTravelers += is_array($pax) ? array_sum($pax) : (int) $pax;
208 }
209 if ($totalTravelers < 1) {
210 $totalTravelers = 1;
211 }
212
213 // Get travel date from first tour
214 $travelDate = $firstTour['yatra_selected_date'] ?? null;
215 if ($travelDate) {
216 // Normalize date format
217 $parsedDate = strtotime($travelDate);
218 if ($parsedDate) {
219 $travelDate = date('Y-m-d', $parsedDate);
220 } else {
221 $travelDate = date('Y-m-d');
222 }
223 } else {
224 $travelDate = date('Y-m-d');
225 }
226
227 // Extract coupon/discount info
228 $couponData = $bookingParams['coupon'] ?? [];
229 if (is_string($couponData)) {
230 $couponData = maybe_unserialize($couponData);
231 }
232 if (!is_array($couponData)) {
233 $couponData = [];
234 }
235 $discountCode = $couponData['code'] ?? null;
236 $discountAmount = (float) ($couponData['discount_amount'] ?? 0);
237
238 // Extract tax info
239 $taxRate = (float) ($bookingParams['tax_rate'] ?? 0);
240 $taxAmount = (float) ($bookingParams['tax_amount'] ?? 0);
241
242 // Calculate paid and due amounts from old payments
243 $paidAmount = $this->getOldPaidAmount($oldBooking->ID);
244 $dueAmount = ($totalAmount - $paidAmount) > 0 ? ($totalAmount - $paidAmount) : 0;
245
246 // Determine payment status
247 $paymentStatus = 'pending';
248 if ($paidAmount >= $totalAmount && $totalAmount > 0) {
249 $paymentStatus = 'paid';
250 } elseif ($paidAmount > 0) {
251 $paymentStatus = 'partial';
252 }
253
254 // Get payment gateway from old payments
255 $paymentGateway = $this->getOldPaymentGateway($oldBooking->ID);
256
257 $bookingData = [
258 'reference' => $bookingCode,
259 'trip_id' => $newTripId,
260 'customer_id' => $newCustomerId,
261 'user_id' => !empty($wpUserId) ? (int) $wpUserId : null,
262 'contact_first_name' => $nameParts['first_name'],
263 'contact_last_name' => $nameParts['last_name'],
264 'contact_email' => $contactEmail,
265 // phone_number is the actual checkout form field key in old plugin;
266 // always prefer it — phone is a legacy fallback only.
267 'contact_phone' => $customerInfo['phone_number'] ?? $customerInfo['phone'] ?? $contactPhone,
268 'contact_country' => $contactCountry,
269 'travel_date' => $travelDate,
270 'travelers_count' => $totalTravelers,
271 'total_amount' => $totalAmount,
272 'amount_paid' => $paidAmount,
273 'amount_due' => $dueAmount,
274 'currency' => $currency,
275 'discount_amount' => $discountAmount,
276 'discount_code' => $discountCode,
277 'subtotal' => $grossAmount,
278 'tax_amount' => $taxAmount,
279 'tax_rate' => $taxRate,
280 'payment_gateway' => $paymentGateway ?: 'pay_later',
281 'payment_status' => $paymentStatus,
282 'status' => $newStatus,
283 'created_at' => $oldBooking->post_date,
284 // post_modified is now included in the SELECT query — was NULL before.
285 'updated_at' => $oldBooking->post_modified ?: $oldBooking->post_date,
286 ];
287
288 // Check if booking with same reference exists
289 $existingBookingId = $this->wpdb->get_var($this->wpdb->prepare(
290 "SELECT id FROM " . BookingsTable::getTableName() . " WHERE reference = %s",
291 $bookingCode
292 ));
293
294 // Initialize to null — will be set on successful insert or update.
295 $newBookingId = null;
296
297 if ($existingBookingId && !$this->isForceMigration()) {
298 // Update existing booking
299 $updateData = $bookingData;
300 unset($updateData['created_at']);
301 unset($updateData['reference']);
302
303 $this->wpdb->update(
304 BookingsTable::getTableName(),
305 $updateData,
306 ['id' => $existingBookingId]
307 );
308 $newBookingId = (int) $existingBookingId;
309 } else {
310 // For force migration, ensure unique reference
311 if ($this->isForceMigration() && $existingBookingId) {
312 $bookingData['reference'] = $bookingCode . '-' . time();
313 }
314
315 $inserted = $this->wpdb->insert(
316 BookingsTable::getTableName(),
317 $bookingData
318 );
319
320 if ($inserted) {
321 $newBookingId = (int) $this->wpdb->insert_id;
322 } else {
323 $failed++;
324 $errorDetails = [
325 'source' => 'migration',
326 'data_type' => 'bookings',
327 'booking_id' => $oldBooking->ID,
328 'booking_code' => $bookingCode,
329 'error' => $this->wpdb->last_error,
330 'trip_id' => $newTripId,
331 'customer_id' => $newCustomerId
332 ];
333
334 // Check for specific error types
335 if (strpos($this->wpdb->last_error, 'Duplicate entry') !== false) {
336 $errorDetails['error_type'] = 'duplicate_reference';
337 Logger::warning("Booking failed (duplicate reference): {$oldBooking->ID} - {$bookingCode}", $errorDetails);
338 } elseif (strpos($this->wpdb->last_error, 'Cannot add or update a child row') !== false) {
339 $errorDetails['error_type'] = 'foreign_key_constraint';
340 Logger::warning("Booking failed (foreign key): {$oldBooking->ID} - trip_id: {$newTripId}, customer_id: {$newCustomerId}", $errorDetails);
341 } else {
342 $errorDetails['error_type'] = 'database_error';
343 Logger::error("Failed to insert booking ID {$oldBooking->ID}: {$this->wpdb->last_error}", $errorDetails);
344 }
345
346 $this->updateProgress('bookings', 'running', $migrated, $skipped, $failed, $total, null, null);
347 continue;
348 }
349 }
350
351 // Guard: only persist migration marker and payments if we have a valid new ID.
352 if ($newBookingId === null) {
353 $failed++;
354 Logger::error("Booking {$oldBooking->ID}: no new booking ID — skipping marker and payments.", [
355 'source' => 'migration',
356 'data_type' => 'bookings',
357 'booking_id' => $oldBooking->ID,
358 ]);
359 $this->updateProgress('bookings', 'running', $migrated, $skipped, $failed, $total, null, null);
360 continue;
361 }
362
363 // Mark as migrated
364 $this->setRawPostMeta($oldBooking->ID, '_migrated_to_booking_id', (string) $newBookingId);
365
366 // Migrate associated payments
367 $this->migrateBookingPayments($oldBooking->ID, $newBookingId, $newCustomerId);
368
369 // Commit transaction
370 $this->wpdb->query('COMMIT');
371 $migrated++;
372 Logger::info("Migrated booking ID {$oldBooking->ID} → new booking ID {$newBookingId}.", [
373 'source' => 'migration',
374 'data_type' => 'bookings',
375 'booking_id' => $oldBooking->ID,
376 'new_id' => $newBookingId,
377 ]);
378
379 } catch (\Exception $e) {
380 // Rollback transaction on error
381 $this->wpdb->query('ROLLBACK');
382 throw $e;
383 }
384
385 } catch (\Exception $e) {
386 $failed++;
387 Logger::error("Exception migrating booking ID {$oldBooking->ID}: {$e->getMessage()}", [
388 'source' => 'migration',
389 'data_type' => 'bookings',
390 'booking_id' => $oldBooking->ID,
391 'error' => $e->getMessage(),
392 ]);
393 $this->updateProgress('bookings', 'running', $migrated, $skipped, $failed, $total, null, null);
394 }
395
396 }
397
398 return compact('migrated', 'skipped', 'failed');
399 }
400
401 /**
402 * Map old booking post_status to new booking status
403 */
404 private function mapBookingStatus(string $oldStatus): string
405 {
406 $statusMap = [
407 'yatra-pending' => 'pending',
408 'yatra-processing' => 'processing',
409 'yatra-on-hold' => 'on_hold',
410 'yatra-completed' => 'completed',
411 'yatra-cancelled' => 'cancelled',
412 'yatra-failed' => 'failed',
413 'publish' => 'confirmed',
414 'draft' => 'pending',
415 ];
416
417 return $statusMap[$oldStatus] ?? 'pending';
418 }
419
420 /**
421 * Get total paid amount for an old booking from yatra-payment CPT
422 */
423 private function getOldPaidAmount(int $oldBookingId): float
424 {
425 $totalPaid = 0.0;
426
427 $payments = $this->wpdb->get_results($this->wpdb->prepare(
428 "SELECT p.ID, p.post_status FROM {$this->wpdb->posts} p
429 INNER JOIN {$this->wpdb->postmeta} pm ON p.ID = pm.post_id
430 WHERE p.post_type = 'yatra-payment'
431 AND pm.meta_key = 'booking_id'
432 AND pm.meta_value = %s
433 AND p.post_status = 'publish'",
434 $oldBookingId
435 ));
436
437 foreach ($payments as $payment) {
438 $paidAmount = (float) ($this->getRawPostMeta($payment->ID, 'paid_amount') ?: 0);
439 $totalPaid += $paidAmount;
440 }
441
442 return $totalPaid;
443 }
444
445 /**
446 * Get payment gateway used for an old booking
447 */
448 private function getOldPaymentGateway(int $oldBookingId): string
449 {
450 $payment = $this->wpdb->get_row($this->wpdb->prepare(
451 "SELECT p.ID FROM {$this->wpdb->posts} p
452 INNER JOIN {$this->wpdb->postmeta} pm ON p.ID = pm.post_id
453 WHERE p.post_type = 'yatra-payment'
454 AND pm.meta_key = 'booking_id'
455 AND pm.meta_value = %s
456 ORDER BY p.post_date DESC
457 LIMIT 1",
458 $oldBookingId
459 ));
460
461 if ($payment) {
462 return $this->getRawPostMeta($payment->ID, 'payment_gateway') ?: '';
463 }
464
465 return '';
466 }
467
468 /**
469 * Migrate payment records associated with a booking
470 */
471 private function migrateBookingPayments(int $oldBookingId, int $newBookingId, ?int $newCustomerId): void
472 {
473 $oldPayments = $this->wpdb->get_results($this->wpdb->prepare(
474 "SELECT p.* FROM {$this->wpdb->posts} p
475 INNER JOIN {$this->wpdb->postmeta} pm ON p.ID = pm.post_id
476 WHERE p.post_type = 'yatra-payment'
477 AND pm.meta_key = 'booking_id'
478 AND pm.meta_value = %s
479 ORDER BY p.post_date ASC",
480 $oldBookingId
481 ));
482
483 if (empty($oldPayments)) {
484 return;
485 }
486
487 foreach ($oldPayments as $oldPayment) {
488 // Check if already migrated
489 $alreadyMigrated = $this->getRawPostMeta($oldPayment->ID, '_migrated_to_payment_id');
490 if ($alreadyMigrated && !$this->isForceMigration()) {
491 continue;
492 }
493
494 $paymentMeta = $this->getPostMeta($oldPayment->ID);
495 $gateway = $paymentMeta['payment_gateway'] ?? 'unknown';
496 $amount = (float) ($paymentMeta['paid_amount'] ?? 0);
497 $currency = $paymentMeta['currency_code'] ?? 'USD';
498 $transactionId = $paymentMeta['transaction_id'] ?? null;
499 $paymentType = $paymentMeta['payment_type'] ?? 'full';
500 $installment = (int) ($paymentMeta['installment'] ?? 0);
501
502 // Map old payment status to new status
503 $oldPaymentStatus = $oldPayment->post_status;
504 $newPaymentStatus = $this->mapPaymentStatus($oldPaymentStatus);
505
506 // Map payment type
507 $newPaymentType = 'initial';
508 if ($paymentType === 'partial' || $installment > 1) {
509 $newPaymentType = 'partial';
510 }
511
512 $paymentData = [
513 'booking_id' => $newBookingId,
514 'customer_id' => $newCustomerId,
515 'transaction_id' => $transactionId,
516 'gateway' => $gateway,
517 'amount' => $amount,
518 'currency' => $currency,
519 'status' => $newPaymentStatus,
520 'payment_type' => $newPaymentType,
521 'notes' => "Migrated from old payment #{$oldPayment->ID}",
522 'processed_at' => $newPaymentStatus === 'completed' ? $oldPayment->post_date : null,
523 'created_at' => $oldPayment->post_date,
524 ];
525
526 $inserted = $this->wpdb->insert(
527 BookingPaymentsTable::getTableName(),
528 $paymentData
529 );
530
531 if ($inserted) {
532 $this->setRawPostMeta($oldPayment->ID, '_migrated_to_payment_id', (string) $this->wpdb->insert_id);
533 }
534 }
535 }
536
537 /**
538 * Map old payment post_status to new payment status
539 */
540 private function mapPaymentStatus(string $oldStatus): string
541 {
542 $statusMap = [
543 'processing' => 'pending',
544 'publish' => 'completed',
545 'hold' => 'pending',
546 'refunded' => 'refunded',
547 'failed' => 'failed',
548 ];
549
550 return $statusMap[$oldStatus] ?? 'pending';
551 }
552
553 /**
554 * Parse a full name string into first and last name parts
555 */
556 private function parseFullName(string $fullname): array
557 {
558 $fullname = trim($fullname);
559
560 if (empty($fullname)) {
561 return ['first_name' => '', 'last_name' => ''];
562 }
563
564 $parts = preg_split('/\s+/', $fullname, 2);
565
566 return [
567 'first_name' => $parts[0] ?? '',
568 'last_name' => $parts[1] ?? '',
569 ];
570 }
571 }
572