PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.5.4
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.5.4
1.6.6 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 All 49 releases
fluent-cart / app / Modules / WooCommerceMigrator / Services / CustomerMigrationService.php

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

376 lines 14.5 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\Framework\Support\Arr;
6
7 class CustomerMigrationService extends BaseMigrationService
8 {
9 const CUSTOMER_MAPPING_KEY = '__fluent_cart_wc_customer_map';
10
11 /**
12 * Check if the migration dependencies are met
13 *
14 * @return bool
15 */
16 public function checkDependencies(): bool
17 {
18 if (!$this->checkWooCommerceDependencies()) {
19 return false;
20 }
21
22 global $wpdb;
23
24 // Check if FluentCart customer tables exist
25 $fluentTables = [
26 $wpdb->prefix . 'fct_customers',
27 $wpdb->prefix . 'fct_customer_addresses',
28 $wpdb->prefix . 'fct_customer_meta'
29 ];
30
31 foreach ($fluentTables as $table) {
32 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Safe as it's just a table name
33 $result = $wpdb->get_var(
34 $wpdb->prepare("SHOW TABLES LIKE %s", $table)
35 );
36 if ($result !== $table) {
37 $this->logError("Required FluentCart table {$table} does not exist");
38 return false;
39 }
40 }
41
42 return true;
43 }
44
45 /**
46 * Run the customer migration
47 *
48 * @param array $options Migration options
49 * @return array Migration results with counts and status
50 */
51 public function migrate(array $options = []): array
52 {
53 $this->initStats();
54
55 if (!$this->checkDependencies()) {
56 return $this->getStats();
57 }
58
59 global $wpdb;
60
61 // Get all WooCommerce customers from the customer lookup table
62 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
63 $customers = $wpdb->get_results("
64 SELECT DISTINCT u.ID, u.user_login, u.user_email, u.user_nicename,
65 u.display_name, u.user_registered,
66 cl.customer_id as wc_customer_id
67 FROM {$wpdb->users} u
68 INNER JOIN {$wpdb->prefix}wc_customer_lookup cl ON u.ID = cl.user_id
69 WHERE cl.user_id > 0
70 ORDER BY u.user_registered ASC
71 ");
72
73 $this->stats['total'] = count($customers);
74
75 foreach ($customers as $customer) {
76 try {
77 $result = $this->migrateCustomer($customer, $options);
78 if (!$result) {
79 $this->logError("Failed to migrate customer {$customer->ID}", $customer);
80 }
81 } catch (\Exception $e) {
82 $this->logError("Failed to migrate customer {$customer->ID}: " . $e->getMessage(), $customer);
83 }
84 }
85
86 $this->finalizeStats();
87 return $this->getStats();
88 }
89
90 /**
91 * Migrate a single customer based on EDD migration pattern
92 *
93 * @param object $wooCustomer WordPress user object
94 * @param array $options Migration options
95 * @return int|false FluentCart customer ID or false on failure
96 */
97 private function migrateCustomer($wooCustomer, $options = [])
98 {
99 global $wpdb;
100
101 // Check if customer already exists (unless forcing)
102 if (empty($options['force'])) {
103 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
104 $existingCustomer = $wpdb->get_row(
105 $wpdb->prepare(
106 "SELECT * FROM {$wpdb->prefix}fct_customers WHERE email = %s OR user_id = %d",
107 $wooCustomer->user_email,
108 $wooCustomer->ID
109 ));
110
111 if ($existingCustomer) {
112 $this->logSkipped("Customer {$wooCustomer->ID} already exists");
113 return $existingCustomer->id;
114 }
115 }
116
117 // Get all user meta for the customer
118 $customerMeta = get_user_meta($wooCustomer->ID);
119
120 // Prepare essential customer data following FluentCart schema
121 $customerData = [
122 'user_id' => $wooCustomer->ID,
123 'email' => $wooCustomer->user_email,
124 'first_name' => $this->getMetaValue($customerMeta, 'first_name') ?: $this->getMetaValue($customerMeta, 'billing_first_name'),
125 'last_name' => $this->getMetaValue($customerMeta, 'last_name') ?: $this->getMetaValue($customerMeta, 'billing_last_name'),
126 'status' => 'active',
127 'country' => $this->getMetaValue($customerMeta, 'billing_country'),
128 'city' => $this->getMetaValue($customerMeta, 'billing_city'),
129 'state' => $this->getMetaValue($customerMeta, 'billing_state'),
130 'postcode' => $this->getMetaValue($customerMeta, 'billing_postcode'),
131 'created_at' => $wooCustomer->user_registered,
132 'updated_at' => current_time('mysql')
133 ];
134
135 // Clean empty values
136 $customerData = array_filter($customerData, function($value) {
137 return $value !== null && $value !== '';
138 });
139
140 // Insert customer
141 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
142 $insertResult = $wpdb->insert($wpdb->prefix . 'fct_customers', $customerData);
143
144 if (!$insertResult) {
145 $this->logError("Failed to insert customer: " . $wpdb->last_error, $wooCustomer);
146 return false;
147 }
148
149 $fluentCustomerId = $wpdb->insert_id;
150
151 // Migrate billing address
152 $this->migrateBillingAddress($fluentCustomerId, $customerMeta);
153
154 // Migrate shipping address (if different from billing)
155 $this->migrateShippingAddress($fluentCustomerId, $customerMeta);
156
157 // Update customer purchase statistics
158 $this->updateCustomerStats($fluentCustomerId, $wooCustomer->ID);
159
160 // Store mapping for reference
161 $this->getOrSetMapping(self::CUSTOMER_MAPPING_KEY, $wooCustomer->ID, $fluentCustomerId);
162
163 $this->logSuccess("Customer {$wooCustomer->ID} migrated to {$fluentCustomerId}");
164 return $fluentCustomerId;
165 }
166
167 /**
168 * Migrate billing address to FluentCart customer addresses table
169 *
170 * @param int $fluentCustomerId
171 * @param array $customerMeta
172 */
173 private function migrateBillingAddress($fluentCustomerId, $customerMeta)
174 {
175 // Combine first and last name for the name field
176 $firstName = $this->getMetaValue($customerMeta, 'billing_first_name');
177 $lastName = $this->getMetaValue($customerMeta, 'billing_last_name');
178 $name = trim($firstName . ' ' . $lastName);
179
180 $billingData = [
181 'customer_id' => $fluentCustomerId,
182 'is_primary' => 1,
183 'type' => 'billing',
184 'status' => 'active',
185 'label' => 'Billing',
186 'name' => $name,
187 'address_1' => $this->getMetaValue($customerMeta, 'billing_address_1'),
188 'address_2' => $this->getMetaValue($customerMeta, 'billing_address_2'),
189 'city' => $this->getMetaValue($customerMeta, 'billing_city'),
190 'state' => $this->getMetaValue($customerMeta, 'billing_state'),
191 'postcode' => $this->getMetaValue($customerMeta, 'billing_postcode'),
192 'country' => $this->getMetaValue($customerMeta, 'billing_country'),
193 'phone' => $this->getMetaValue($customerMeta, 'billing_phone'),
194 'email' => $this->getMetaValue($customerMeta, 'billing_email'),
195 'created_at' => current_time('mysql'),
196 'updated_at' => current_time('mysql')
197 ];
198
199 $this->insertAddressIfValid($billingData);
200 }
201
202 /**
203 * Migrate shipping address to FluentCart customer addresses table
204 *
205 * @param int $fluentCustomerId
206 * @param array $customerMeta
207 */
208 private function migrateShippingAddress($fluentCustomerId, $customerMeta)
209 {
210 // Combine first and last name for the name field
211 $firstName = $this->getMetaValue($customerMeta, 'shipping_first_name');
212 $lastName = $this->getMetaValue($customerMeta, 'shipping_last_name');
213 $name = trim($firstName . ' ' . $lastName);
214
215 $shippingData = [
216 'customer_id' => $fluentCustomerId,
217 'is_primary' => 1, // First shipping address is always primary
218 'type' => 'shipping',
219 'status' => 'active',
220 'label' => 'Shipping',
221 'name' => $name,
222 'address_1' => $this->getMetaValue($customerMeta, 'shipping_address_1'),
223 'address_2' => $this->getMetaValue($customerMeta, 'shipping_address_2'),
224 'city' => $this->getMetaValue($customerMeta, 'shipping_city'),
225 'state' => $this->getMetaValue($customerMeta, 'shipping_state'),
226 'postcode' => $this->getMetaValue($customerMeta, 'shipping_postcode'),
227 'country' => $this->getMetaValue($customerMeta, 'shipping_country'),
228 'phone' => $this->getMetaValue($customerMeta, 'shipping_phone'),
229 'email' => '', // WooCommerce doesn't typically store shipping email
230 'created_at' => current_time('mysql'),
231 'updated_at' => current_time('mysql')
232 ];
233
234 $this->insertAddressIfValid($shippingData);
235 }
236
237 /**
238 * Calculate and update customer purchase statistics
239 *
240 * @param int $fluentCustomerId
241 * @param int $wooUserId
242 */
243 private function updateCustomerStats($fluentCustomerId, $wooUserId)
244 {
245 $stats = $this->calculateCustomerStats($wooUserId);
246
247 if ($stats) {
248 global $wpdb;
249 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
250 $wpdb->update(
251 $wpdb->prefix . 'fct_customers',
252 [
253 'purchase_count' => $stats['order_count'],
254 'purchase_value' => json_encode([$stats['currency'] => $stats['total_spent_cents']]),
255 'first_purchase_date' => $stats['first_order_date'],
256 'last_purchase_date' => $stats['last_order_date'],
257 'aov' => $stats['aov_cents'],
258 'updated_at' => current_time('mysql')
259 ],
260 ['id' => $fluentCustomerId]
261 );
262 }
263 }
264
265 /**
266 * Calculate customer statistics from WooCommerce orders
267 *
268 * @param int $wooUserId
269 * @return array|null
270 */
271 private function calculateCustomerStats($wooUserId)
272 {
273 global $wpdb;
274
275 // Calculate stats from WooCommerce orders
276 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
277 $orderStats = $wpdb->get_row(
278 $wpdb->prepare("
279 SELECT
280 COUNT(*) as order_count,
281 SUM(CAST(pm_total.meta_value AS DECIMAL(10,2))) as total_spent,
282 MIN(p.post_date) as first_order_date,
283 MAX(p.post_date) as last_order_date,
284 pm_currency.meta_value as currency
285 FROM {$wpdb->posts} p
286 INNER JOIN {$wpdb->postmeta} pm_customer ON p.ID = pm_customer.post_id
287 AND pm_customer.meta_key = '_customer_user'
288 AND pm_customer.meta_value = %d
289 INNER JOIN {$wpdb->postmeta} pm_total ON p.ID = pm_total.post_id
290 AND pm_total.meta_key = '_order_total'
291 LEFT JOIN {$wpdb->postmeta} pm_currency ON p.ID = pm_currency.post_id
292 AND pm_currency.meta_key = '_order_currency'
293 WHERE p.post_type = 'shop_order'
294 AND p.post_status IN ('wc-completed', 'wc-processing', 'wc-on-hold')
295 ", $wooUserId));
296
297 if ($orderStats && $orderStats->order_count > 0) {
298 $totalSpent = (float) $orderStats->total_spent;
299 $currency = $orderStats->currency ?: get_woocommerce_currency();
300
301 return [
302 'order_count' => (int) $orderStats->order_count,
303 'total_spent_cents' => (int) ($totalSpent * 100), // Convert to cents
304 'first_order_date' => $orderStats->first_order_date,
305 'last_order_date' => $orderStats->last_order_date,
306 'aov_cents' => (int) (($totalSpent / $orderStats->order_count) * 100), // AOV in cents
307 'currency' => $currency
308 ];
309 }
310
311 return null;
312 }
313
314 /**
315 * Insert address data if valid, removing empty values but keeping required fields
316 *
317 * @param array $addressData
318 */
319 private function insertAddressIfValid($addressData)
320 {
321 global $wpdb;
322
323 // Remove null/empty values but keep required fields
324 $requiredFields = ['customer_id', 'is_primary', 'type', 'status', 'label', 'created_at', 'updated_at'];
325 $addressData = array_filter($addressData, function($value, $key) use ($requiredFields) {
326 return in_array($key, $requiredFields) || ($value !== null && $value !== '');
327 }, ARRAY_FILTER_USE_BOTH);
328
329 // Only insert if we have meaningful address data
330 if (!empty($addressData['address_1']) || !empty($addressData['city']) || !empty($addressData['country'])) {
331 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
332 $wpdb->insert($wpdb->prefix . 'fct_customer_addresses', $addressData);
333 }
334 }
335
336 /**
337 * Helper to get meta value from meta array
338 *
339 * @param array $meta
340 * @param string $key
341 * @return string|null
342 */
343 private function getMetaValue($meta, $key)
344 {
345 return isset($meta[$key][0]) ? $meta[$key][0] : null;
346 }
347
348 /**
349 * Clean up migration data (for fresh migrations)
350 *
351 * @return bool
352 */
353 public function cleanup(): bool
354 {
355 global $wpdb;
356
357 try {
358 // Delete all FluentCart customers and related data
359 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
360 $wpdb->query("DELETE FROM {$wpdb->prefix}fct_customers");
361 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
362 $wpdb->query("DELETE FROM {$wpdb->prefix}fct_customer_addresses");
363 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
364 $wpdb->query("DELETE FROM {$wpdb->prefix}fct_customer_meta");
365
366 // Clear mapping
367 $this->clearMapping(self::CUSTOMER_MAPPING_KEY);
368
369 return true;
370 } catch (\Exception $e) {
371 $this->logError("Failed to cleanup customer data: " . $e->getMessage());
372 return false;
373 }
374 }
375 }
376