PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.5
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.5
1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 trunk All 48 releases
fluent-cart / app / Modules / WooCommerceMigrator / Services / OrderMigrationService.php

OrderMigrationService.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.6.5, at app/Modules/WooCommerceMigrator/Services/OrderMigrationService.php

883 lines 32.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Modules\WooCommerceMigrator\Services;
4
5 use FluentCart\App\Modules\WooCommerceMigrator\Contracts\MigrationServiceInterface;
6 use FluentCart\Framework\Support\Arr;
7
8 /**
9 * OrderMigrationService - Migrates WooCommerce orders to FluentCart
10 *
11 * Handles:
12 * - Order headers (wp_wc_orders -> wp_fct_orders)
13 * - Order items (wp_woocommerce_order_items -> wp_fct_order_items)
14 * - Order addresses (wp_wc_order_addresses -> wp_fct_order_addresses)
15 * - Applied coupons (coupon line items -> wp_fct_applied_coupons)
16 * - Fee items (fee line items -> custom handling)
17 * - Order metadata (wp_wc_orders_meta -> wp_fct_order_meta)
18 */
19 class OrderMigrationService extends BaseMigrationService implements MigrationServiceInterface
20 {
21 protected $entityName = 'orders';
22 protected $batchSize = 50; // Lower batch size for complex order data
23 protected $migrated = 0;
24
25 /**
26 * Set batch size for migration
27 */
28 public function setBatchSize(int $size): void
29 {
30 $this->batchSize = max(1, min($size, 100)); // Min 1, Max 100
31 }
32
33 /**
34 * Add an error message
35 */
36 protected function addError(string $message): void
37 {
38 $this->errors[] = $message;
39 }
40
41 /**
42 * Get all error messages
43 */
44 public function getErrors(): array
45 {
46 return $this->errors;
47 }
48
49 /**
50 * Increment migrated counter
51 */
52 protected function incrementMigrated(): void
53 {
54 $this->migrated++;
55 }
56
57 /**
58 * Check if migration can proceed
59 */
60 public function canMigrate(): bool
61 {
62 if (!$this->isWooCommerceActive()) {
63 $this->addError('WooCommerce is not active');
64 return false;
65 }
66
67 // Check if HPOS is active
68 if (!$this->isHPOSActive()) {
69 $this->addError('WooCommerce HPOS (High-Performance Order Storage) is not active');
70 return false;
71 }
72
73 // Check dependencies - customers and products must be migrated first
74 if (!$this->areDependenciesMigrated()) {
75 $this->addError('Dependencies not satisfied. Please migrate customers and products first.');
76 return false;
77 }
78
79 return true;
80 }
81
82 /**
83 * Get total count of orders to migrate
84 */
85 public function getTotalCount(): int
86 {
87 global $wpdb;
88 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
89 return (int) $wpdb->get_var("
90 SELECT COUNT(*)
91 FROM {$wpdb->prefix}wc_orders
92 WHERE type = 'shop_order'
93 ");
94 }
95
96 /**
97 * Get count of already migrated orders
98 */
99 public function getMigratedCount(): int
100 {
101 global $wpdb;
102 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
103 return (int) $wpdb->get_var("
104 SELECT COUNT(*)
105 FROM {$wpdb->prefix}fct_orders
106 WHERE invoice_no LIKE 'WC-%'
107 ");
108 }
109
110 /**
111 * Discover orders to migrate
112 */
113 public function discoverItems(int $offset = 0, ?int $limit = null): array
114 {
115 global $wpdb;
116
117 $limit = $limit ?: $this->batchSize;
118 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
119 $orders = $wpdb->get_results($wpdb->prepare("
120 SELECT o.*,
121 c.user_id as woo_customer_user_id,
122 fc.id as fluent_customer_id
123 FROM {$wpdb->prefix}wc_orders o
124 LEFT JOIN {$wpdb->prefix}wc_customer_lookup c ON o.customer_id = c.customer_id
125 LEFT JOIN {$wpdb->prefix}fct_customers fc ON c.user_id = fc.user_id
126 WHERE o.type = 'shop_order'
127 AND NOT EXISTS (
128 SELECT 1 FROM {$wpdb->prefix}fct_orders fo
129 WHERE fo.invoice_no = CONCAT('WC-', o.id)
130 )
131 ORDER BY o.id ASC
132 LIMIT %d OFFSET %d
133 ", $limit, $offset));
134
135 return $orders;
136 }
137
138 /**
139 * Migrate a single order
140 */
141 public function migrateSingle($wooOrder): bool
142 {
143 global $wpdb;
144
145 try {
146 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
147 $wpdb->query('START TRANSACTION');
148
149 // 1. Migrate main order record
150 $fluentOrderId = $this->migrateOrderRecord($wooOrder);
151 if (!$fluentOrderId) {
152 throw new \Exception("Failed to migrate order record for WooCommerce order {$wooOrder->id}");
153 }
154
155 // 2. Migrate order addresses
156 $this->migrateOrderAddresses($wooOrder->id, $fluentOrderId);
157
158 // 3. Migrate order items (products, shipping, fees, coupons)
159 $this->migrateOrderItems($wooOrder->id, $fluentOrderId, $wooOrder->customer_id);
160
161 // 4. Migrate order metadata
162 $this->migrateOrderMeta($wooOrder->id, $fluentOrderId);
163
164 // 5. Update order totals and validate
165 $this->updateOrderTotals($fluentOrderId);
166
167 // 6. Update customer purchase statistics
168 if ($wooOrder->fluent_customer_id) {
169 $this->updateCustomerPurchaseStats($wooOrder->fluent_customer_id);
170 }
171 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
172 $wpdb->query('COMMIT');
173
174 $this->incrementMigrated();
175 return true;
176
177 } catch (\Exception $e) {
178 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
179 $wpdb->query('ROLLBACK');
180 $this->addError("Order {$wooOrder->id}: " . $e->getMessage());
181 return false;
182 }
183 }
184
185 /**
186 * Migrate main order record
187 */
188 private function migrateOrderRecord($wooOrder): ?int
189 {
190 global $wpdb;
191
192 // Convert status
193 $status = $this->convertOrderStatus($wooOrder->status);
194 $paymentStatus = $this->convertPaymentStatus($wooOrder->status);
195
196 // Convert amounts to cents
197 $subtotal = $this->convertToCents($wooOrder->total_amount - $wooOrder->tax_amount - $wooOrder->shipping_amount);
198 $taxTotal = $this->convertToCents($wooOrder->tax_amount);
199 $shippingTotal = $this->convertToCents($wooOrder->shipping_amount);
200 $totalAmount = $this->convertToCents($wooOrder->total_amount);
201 $discountTotal = $this->convertToCents($wooOrder->discount_amount);
202
203 $orderData = [
204 'parent_id' => 0,
205 'customer_id' => $wooOrder->fluent_customer_id,
206 'status' => $status,
207 'payment_status' => $paymentStatus,
208 'fulfillment_type' => 'physical', // Default, will be updated based on items
209 'type' => 'checkout',
210 'mode' => 'live',
211 'payment_method' => $wooOrder->payment_method ?: 'unknown',
212 'payment_method_title' => $wooOrder->payment_method_title ?: 'Unknown',
213 'currency' => $wooOrder->currency ?: 'USD',
214 'subtotal' => $subtotal,
215 'tax_total' => $taxTotal,
216 'shipping_total' => $shippingTotal,
217 'manual_discount_total' => 0,
218 'coupon_discount_total' => $discountTotal, // Same as discount for now
219 'total_amount' => $totalAmount,
220 'total_paid' => $paymentStatus === 'paid' ? $totalAmount : 0,
221 'shipping_status' => 'unshipped',
222 'rate' => 1.0000,
223 'uuid' => wp_generate_uuid4(),
224 'invoice_no' => 'WC-' . $wooOrder->id,
225 'created_at' => $wooOrder->date_created_gmt ?: current_time('mysql', true),
226 'updated_at' => $wooOrder->date_modified_gmt ?: current_time('mysql', true),
227 ];
228
229 // Remove null customer_id if guest order
230 if (!$orderData['customer_id']) {
231 unset($orderData['customer_id']);
232 }
233 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
234 $result = $wpdb->insert($wpdb->prefix . 'fct_orders', $orderData);
235
236 if ($result === false) {
237 throw new \Exception(
238 sprintf(
239 'Failed to insert order record: %s',
240 esc_html(sanitize_text_field($wpdb->last_error))
241 )
242 );
243 }
244
245 return $wpdb->insert_id;
246 }
247
248 /**
249 * Migrate order addresses
250 */
251 private function migrateOrderAddresses(int $wooOrderId, int $fluentOrderId): void
252 {
253 global $wpdb;
254 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
255 $addresses = $wpdb->get_results($wpdb->prepare("
256 SELECT * FROM {$wpdb->prefix}wc_order_addresses
257 WHERE order_id = %d
258 ", $wooOrderId));
259
260 foreach ($addresses as $address) {
261 $addressData = [
262 'order_id' => $fluentOrderId,
263 'type' => $address->address_type ?: 'billing',
264 'name' => trim(($address->first_name ?: '') . ' ' . ($address->last_name ?: '')),
265 'address_1' => $address->address_1 ?: '',
266 'address_2' => $address->address_2 ?: '',
267 'city' => $address->city ?: '',
268 'state' => $address->state ?: '',
269 'postcode' => $address->postcode ?: '',
270 'country' => $address->country ?: '',
271 'created_at' => current_time('mysql', true),
272 'updated_at' => current_time('mysql', true),
273 ];
274 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
275 $wpdb->insert($wpdb->prefix . 'fct_order_addresses', $addressData);
276 }
277 }
278
279 /**
280 * Migrate order items (products, shipping, fees, coupons)
281 */
282 private function migrateOrderItems(int $wooOrderId, int $fluentOrderId, ?int $customerId): void
283 {
284 global $wpdb;
285 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
286 $items = $wpdb->get_results($wpdb->prepare("
287 SELECT oi.*, oim.meta_key, oim.meta_value
288 FROM {$wpdb->prefix}woocommerce_order_items oi
289 LEFT JOIN {$wpdb->prefix}woocommerce_order_itemmeta oim ON oi.order_item_id = oim.order_item_id
290 WHERE oi.order_id = %d
291 ORDER BY oi.order_item_id, oim.meta_id
292 ", $wooOrderId));
293
294 // Group items by ID and type
295 $groupedItems = [];
296 foreach ($items as $item) {
297 if (!isset($groupedItems[$item->order_item_id])) {
298 $groupedItems[$item->order_item_id] = [
299 'item' => $item,
300 'meta' => []
301 ];
302 }
303 if ($item->meta_key) {
304 $groupedItems[$item->order_item_id]['meta'][$item->meta_key] = $item->meta_value;
305 }
306 }
307
308 // Process each item type
309 foreach ($groupedItems as $itemData) {
310 $item = $itemData['item'];
311 $meta = $itemData['meta'];
312
313 switch ($item->order_item_type) {
314 case 'line_item':
315 $this->migrateProductItem($item, $meta, $fluentOrderId);
316 break;
317
318 case 'coupon':
319 $this->migrateCouponItem($item, $meta, $fluentOrderId, $customerId);
320 break;
321
322 case 'fee':
323 $this->migrateFeeItem($item, $meta, $fluentOrderId);
324 break;
325
326 case 'shipping':
327 // Shipping is handled in order totals, but we can store method info
328 $this->migrateShippingMeta($item, $meta, $fluentOrderId);
329 break;
330 }
331 }
332 }
333
334 /**
335 * Migrate product line item
336 */
337 private function migrateProductItem($item, array $meta, int $fluentOrderId): void
338 {
339 global $wpdb;
340
341 $productId = (int) Arr::get($meta, '_product_id', 0);
342 $variationId = (int) Arr::get($meta, '_variation_id', 0);
343 $quantity = (int) Arr::get($meta, '_qty', 1);
344
345 // Handle variation ID (0 means simple product)
346 $objectId = $variationId > 0 ? $variationId : null;
347
348 // Convert amounts
349 $lineTotal = $this->convertToCents(Arr::get($meta, '_line_total', 0));
350 $subtotal = $this->convertToCents(Arr::get($meta, '_line_subtotal', 0));
351 $unitPrice = $quantity > 0 ? intval($lineTotal / $quantity) : 0;
352 $discountTotal = $subtotal - $lineTotal;
353
354 $itemData = [
355 'order_id' => $fluentOrderId,
356 'post_id' => $productId,
357 'object_id' => $objectId,
358 'post_title' => $item->order_item_name,
359 'title' => $item->order_item_name,
360 'quantity' => $quantity,
361 'unit_price' => $unitPrice,
362 'subtotal' => $subtotal,
363 'line_total' => $lineTotal,
364 'discount_total' => $discountTotal,
365 'tax_amount' => $this->convertToCents(Arr::get($meta, '_line_tax', 0)),
366 'fulfillment_type' => 'physical', // Default
367 'payment_type' => 'onetime', // Default
368 'cart_index' => 1,
369 'cost' => 0,
370 'shipping_charge' => 0,
371 'refund_total' => 0,
372 'rate' => 1,
373 'fulfilled_quantity' => 0,
374 'created_at' => current_time('mysql', true),
375 'updated_at' => current_time('mysql', true),
376 ];
377 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
378 $wpdb->insert($wpdb->prefix . 'fct_order_items', $itemData);
379 }
380
381 /**
382 * Migrate coupon line item to FluentCart coupon system
383 */
384 private function migrateCouponItem($item, array $meta, int $fluentOrderId, ?int $customerId): void
385 {
386 global $wpdb;
387
388 $couponCode = $item->order_item_name;
389 $discountAmount = $this->convertToCents(Arr::get($meta, 'discount_amount', 0));
390
391 if (empty($couponCode) || $discountAmount <= 0) {
392 return; // Skip invalid coupons
393 }
394
395 // Create coupon if it doesn't exist
396 $couponId = $this->getOrCreateCoupon($couponCode, $discountAmount);
397
398 // Insert applied coupon
399 $appliedCouponData = [
400 'order_id' => $fluentOrderId,
401 'coupon_id' => $couponId,
402 'customer_id' => $customerId,
403 'code' => $couponCode,
404 'amount' => $discountAmount,
405 'created_at' => current_time('mysql', true),
406 'updated_at' => current_time('mysql', true),
407 ];
408 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
409 $wpdb->insert($wpdb->prefix . 'fct_applied_coupons', $appliedCouponData);
410 }
411
412 /**
413 * Migrate fee line item as custom order item
414 */
415 private function migrateFeeItem($item, array $meta, int $fluentOrderId): void
416 {
417 global $wpdb;
418
419 $feeAmount = $this->convertToCents(Arr::get($meta, '_fee_amount', 0));
420 $feeTax = $this->convertToCents(Arr::get($meta, '_line_tax', 0));
421 $feeTotal = $feeAmount + $feeTax;
422
423 if ($feeTotal <= 0) {
424 return; // Skip zero fees
425 }
426
427 $itemData = [
428 'order_id' => $fluentOrderId,
429 'post_id' => 0,
430 'object_id' => null,
431 'post_title' => $item->order_item_name,
432 'title' => $item->order_item_name,
433 'quantity' => 1,
434 'unit_price' => $feeTotal,
435 'subtotal' => $feeAmount,
436 'line_total' => $feeTotal,
437 'discount_total' => 0,
438 'tax_amount' => $feeTax,
439 'fulfillment_type' => 'digital', // Fees are typically service-based
440 'payment_type' => 'onetime',
441 'cart_index' => 999, // Put fees at the end
442 'cost' => 0,
443 'shipping_charge' => 0,
444 'refund_total' => 0,
445 'rate' => 1,
446 'fulfilled_quantity' => 1, // Fees are immediately fulfilled
447 'created_at' => current_time('mysql', true),
448 'updated_at' => current_time('mysql', true),
449 ];
450 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
451 $wpdb->insert($wpdb->prefix . 'fct_order_items', $itemData);
452 }
453
454 /**
455 * Store shipping method info as order metadata
456 */
457 private function migrateShippingMeta($item, array $meta, int $fluentOrderId): void
458 {
459 global $wpdb;
460
461 $shippingData = [
462 'method_title' => $item->order_item_name,
463 'method_id' => Arr::get($meta, 'method_id', ''),
464 'cost' => Arr::get($meta, 'cost', 0),
465 'taxes' => Arr::get($meta, 'taxes', ''),
466 ];
467 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
468 $wpdb->insert($wpdb->prefix . 'fct_order_meta', [
469 'order_id' => $fluentOrderId,
470 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
471 'meta_key' => 'shipping_method',
472 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
473 'meta_value' => json_encode($shippingData),
474 'created_at' => current_time('mysql', true),
475 'updated_at' => current_time('mysql', true),
476 ]);
477 }
478
479 /**
480 * Migrate order metadata
481 */
482 private function migrateOrderMeta(int $wooOrderId, int $fluentOrderId): void
483 {
484 global $wpdb;
485 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
486 $metaData = $wpdb->get_results($wpdb->prepare("
487 SELECT meta_key, meta_value
488 FROM {$wpdb->prefix}wc_orders_meta
489 WHERE order_id = %d
490 ", $wooOrderId));
491
492 // Important meta keys to migrate
493 $importantKeys = [
494 '_payment_method_title',
495 '_transaction_id',
496 '_customer_note',
497 '_order_key',
498 '_billing_phone',
499 '_shipping_phone',
500 '_order_version',
501 '_cart_hash',
502 '_utm_source',
503 '_utm_medium',
504 '_utm_campaign',
505 ];
506
507 foreach ($metaData as $meta) {
508 if (in_array($meta->meta_key, $importantKeys)) {
509 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
510 $wpdb->insert($wpdb->prefix . 'fct_order_meta', [
511 'order_id' => $fluentOrderId,
512 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
513 'meta_key' => $meta->meta_key,
514 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
515 'meta_value' => $meta->meta_value,
516 'created_at' => current_time('mysql', true),
517 'updated_at' => current_time('mysql', true),
518 ]);
519 }
520 }
521 }
522
523 /**
524 * Update order totals and validate
525 */
526 private function updateOrderTotals(int $fluentOrderId): void
527 {
528 global $wpdb;
529
530 // Recalculate totals from migrated items
531 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
532 $totals = $wpdb->get_row($wpdb->prepare("
533 SELECT
534 SUM(subtotal) as calculated_subtotal,
535 SUM(line_total) as calculated_total,
536 SUM(tax_amount) as calculated_tax,
537 SUM(discount_total) as calculated_discount
538 FROM {$wpdb->prefix}fct_order_items
539 WHERE order_id = %d
540 ", $fluentOrderId));
541
542 // Update order with calculated totals (for validation)
543 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
544 $wpdb->update(
545 $wpdb->prefix . 'fct_orders',
546 [
547 'item_count' => $wpdb->get_var($wpdb->prepare("
548 SELECT SUM(quantity) FROM {$wpdb->prefix}fct_order_items
549 WHERE order_id = %d AND post_id > 0
550 ", $fluentOrderId)),
551 'updated_at' => current_time('mysql', true),
552 ],
553 ['id' => $fluentOrderId]
554 );
555 }
556
557 /**
558 * Update customer purchase statistics after order migration
559 */
560 private function updateCustomerPurchaseStats(int $customerId): void
561 {
562 global $wpdb;
563
564 // Calculate customer order statistics grouped by currency
565 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
566 $orderStats = $wpdb->get_results($wpdb->prepare("
567 SELECT
568 currency,
569 COUNT(*) as order_count,
570 SUM(total_amount) as total_purchase_value,
571 AVG(total_amount) as average_order_value,
572 MIN(created_at) as first_purchase_date,
573 MAX(created_at) as last_purchase_date
574 FROM {$wpdb->prefix}fct_orders
575 WHERE customer_id = %d
576 AND status NOT IN ('failed', 'cancelled')
577 GROUP BY currency
578 ", $customerId));
579
580 if (!empty($orderStats)) {
581 // Build purchase_value JSON object by currency
582 $purchaseValueByCurrency = [];
583 $totalOrderCount = 0;
584 $allAmounts = [];
585 $firstDate = null;
586 $lastDate = null;
587
588 foreach ($orderStats as $currencyStats) {
589 $purchaseValueByCurrency[$currencyStats->currency] = (int) $currencyStats->total_purchase_value;
590 $totalOrderCount += (int) $currencyStats->order_count;
591 $allAmounts[] = (int) $currencyStats->average_order_value;
592
593 if (!$firstDate || $currencyStats->first_purchase_date < $firstDate) {
594 $firstDate = $currencyStats->first_purchase_date;
595 }
596 if (!$lastDate || $currencyStats->last_purchase_date > $lastDate) {
597 $lastDate = $currencyStats->last_purchase_date;
598 }
599 }
600
601 $averageOrderValue = !empty($allAmounts) ? intval(array_sum($allAmounts) / count($allAmounts)) : 0;
602 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
603 $wpdb->update(
604 $wpdb->prefix . 'fct_customers',
605 [
606 'purchase_count' => $totalOrderCount,
607 'purchase_value' => json_encode($purchaseValueByCurrency),
608 'aov' => $averageOrderValue,
609 'first_purchase_date' => $firstDate,
610 'last_purchase_date' => $lastDate,
611 'updated_at' => current_time('mysql', true),
612 ],
613 ['id' => $customerId]
614 );
615 }
616 }
617
618 /**
619 * Get or create coupon in FluentCart
620 */
621 private function getOrCreateCoupon(string $couponCode, int $discountAmount): int
622 {
623 global $wpdb;
624
625 // Check if coupon exists
626 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
627 $existingCoupon = $wpdb->get_var($wpdb->prepare("
628 SELECT id FROM {$wpdb->prefix}fct_coupons
629 WHERE code = %s
630 ", strtoupper($couponCode)));
631
632 if ($existingCoupon) {
633 return (int) $existingCoupon;
634 }
635
636 // Create new coupon with basic settings
637 $couponData = [
638 'title' => 'Migrated: ' . $couponCode,
639 'code' => strtoupper($couponCode),
640 'type' => 'fixed', // Default to fixed amount
641 'amount' => $discountAmount,
642 'status' => 'active',
643 'stackable' => 'yes',
644 'priority' => 10,
645 'use_count' => 0,
646 'notes' => 'Auto-created during WooCommerce migration',
647 'show_on_checkout' => 'no', // Don't show migrated coupons in checkout
648 'conditions' => json_encode([
649 'min_purchase_amount' => 0,
650 'max_discount_amount' => null,
651 'apply_to_whole_cart' => 'yes',
652 ]),
653 'created_at' => current_time('mysql', true),
654 'updated_at' => current_time('mysql', true),
655 ];
656 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
657 $wpdb->insert($wpdb->prefix . 'fct_coupons', $couponData);
658 return $wpdb->insert_id;
659 }
660
661 /**
662 * Convert WooCommerce order status to FluentCart status
663 */
664 private function convertOrderStatus(string $wooStatus): string
665 {
666 $statusMap = [
667 'wc-completed' => 'completed',
668 'wc-pending' => 'pending',
669 'wc-processing' => 'processing',
670 'wc-on-hold' => 'on-hold',
671 'wc-cancelled' => 'failed',
672 'wc-refunded' => 'refunded',
673 'wc-failed' => 'failed',
674 ];
675
676 return $statusMap[$wooStatus] ?? 'pending';
677 }
678
679 /**
680 * Convert WooCommerce order status to FluentCart payment status
681 */
682 private function convertPaymentStatus(string $wooStatus): string
683 {
684 $paymentStatusMap = [
685 'wc-completed' => 'paid',
686 'wc-processing' => 'paid',
687 'wc-pending' => 'pending',
688 'wc-on-hold' => 'pending',
689 'wc-cancelled' => 'failed',
690 'wc-refunded' => 'refunded',
691 'wc-failed' => 'failed',
692 ];
693
694 return $paymentStatusMap[$wooStatus] ?? 'pending';
695 }
696
697
698
699 /**
700 * Check if WooCommerce is active
701 */
702 private function isWooCommerceActive(): bool
703 {
704 return class_exists('WooCommerce');
705 }
706
707 /**
708 * Check if HPOS is active
709 */
710 private function isHPOSActive(): bool
711 {
712 return class_exists('Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore')
713 && get_option('woocommerce_custom_orders_table_enabled') === 'yes';
714 }
715
716 /**
717 * Check if dependencies are migrated
718 */
719 private function areDependenciesMigrated(): bool
720 {
721 global $wpdb;
722
723 // Check if customers are migrated
724 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
725 $customerCount = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}fct_customers");
726 if ($customerCount == 0) {
727 return false;
728 }
729
730 // Check if products/variations exist
731 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
732 $productCount = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}fct_product_variations");
733 if ($productCount == 0) {
734 return false;
735 }
736
737 return true;
738 }
739
740 /**
741 * Get cleanup instructions
742 */
743 public function getCleanupInstructions(): array
744 {
745 return [
746 'description' => 'Remove migrated FluentCart orders and related data',
747 'operations' => [
748 'Delete from wp_fct_orders WHERE invoice_no LIKE "WC-%"',
749 'Delete from wp_fct_order_items WHERE order_id IN (migrated orders)',
750 'Delete from wp_fct_order_addresses WHERE order_id IN (migrated orders)',
751 'Delete from wp_fct_applied_coupons WHERE order_id IN (migrated orders)',
752 'Delete from wp_fct_order_meta WHERE order_id IN (migrated orders)',
753 'Delete auto-created coupons with notes containing "Auto-created during WooCommerce migration"',
754 ],
755 'warning' => 'This will permanently remove all migrated order data from FluentCart'
756 ];
757 }
758
759 /**
760 * Check if the migration dependencies are met
761 */
762 public function checkDependencies(): bool
763 {
764 return $this->canMigrate();
765 }
766
767 /**
768 * Run the migration
769 */
770 public function migrate(array $options = []): array
771 {
772 if (!$this->canMigrate()) {
773 return [
774 'success' => 0,
775 'failed' => 0,
776 'skipped' => 0,
777 'errors' => $this->getErrors(),
778 'warnings' => []
779 ];
780 }
781
782 $this->initStats();
783 $totalCount = $this->getTotalCount();
784 $this->stats['total'] = $totalCount;
785
786 $offset = 0;
787 $batchSize = $this->batchSize;
788
789 while (true) {
790 $orders = $this->discoverItems($offset, $batchSize);
791
792 if (empty($orders)) {
793 break; // No more orders to process
794 }
795
796 foreach ($orders as $order) {
797 if ($this->migrateSingle($order)) {
798 $this->stats['success']++;
799 } else {
800 $this->stats['failed']++;
801 }
802 }
803
804 $offset += $batchSize;
805 }
806
807 $this->finalizeStats();
808
809 return [
810 'success' => $this->stats['success'],
811 'failed' => $this->stats['failed'],
812 'skipped' => $this->stats['skipped'],
813 'errors' => $this->errors,
814 'warnings' => []
815 ];
816 }
817
818 /**
819 * Update all customer purchase statistics (useful for existing migrations)
820 */
821 public function updateAllCustomerStats(): array
822 {
823 global $wpdb;
824
825 $updatedCount = 0;
826 $errorCount = 0;
827
828 // Get all customers who have migrated orders
829 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
830 $customers = $wpdb->get_results("
831 SELECT DISTINCT c.id
832 FROM {$wpdb->prefix}fct_customers c
833 INNER JOIN {$wpdb->prefix}fct_orders o ON c.id = o.customer_id
834 WHERE o.invoice_no LIKE 'WC-%'
835 ");
836
837 foreach ($customers as $customer) {
838 try {
839 $this->updateCustomerPurchaseStats($customer->id);
840 $updatedCount++;
841 } catch (\Exception $e) {
842 $errorCount++;
843 $this->addError("Failed to update customer {$customer->id}: " . $e->getMessage());
844 }
845 }
846
847 return [
848 'updated' => $updatedCount,
849 'errors' => $errorCount,
850 'message' => "Updated statistics for {$updatedCount} customers" . ($errorCount > 0 ? " with {$errorCount} errors" : "")
851 ];
852 }
853
854 /**
855 * Clean up migration data (for fresh migrations)
856 */
857 public function cleanup(): bool
858 {
859 global $wpdb;
860
861 try {
862 // Remove migrated orders and related data
863 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
864 $wpdb->query("DELETE FROM {$wpdb->prefix}fct_orders WHERE invoice_no LIKE 'WC-%'");
865 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
866 $wpdb->query("DELETE FROM {$wpdb->prefix}fct_order_items WHERE order_id NOT IN (SELECT id FROM {$wpdb->prefix}fct_orders)");
867 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
868 $wpdb->query("DELETE FROM {$wpdb->prefix}fct_order_addresses WHERE order_id NOT IN (SELECT id FROM {$wpdb->prefix}fct_orders)");
869 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
870 $wpdb->query("DELETE FROM {$wpdb->prefix}fct_applied_coupons WHERE order_id NOT IN (SELECT id FROM {$wpdb->prefix}fct_orders)");
871 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
872 $wpdb->query("DELETE FROM {$wpdb->prefix}fct_order_meta WHERE order_id NOT IN (SELECT id FROM {$wpdb->prefix}fct_orders)");
873 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
874 $wpdb->query("DELETE FROM {$wpdb->prefix}fct_coupons WHERE notes LIKE '%Auto-created during WooCommerce migration%'");
875
876 return true;
877 } catch (\Exception $e) {
878 $this->addError('Cleanup failed: ' . $e->getMessage());
879 return false;
880 }
881 }
882 }
883