PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.3.1
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.3.1
2.4.0 2.4.1 2.3.8 2.3.7 2.3.6 2.3.5 2.3.4 2.3.3 2.3.2 2.3.1 2.2.0 2.1.21 2.1.20 2.1.19 2.1.18 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.16 2.1.2 All 57 releases
easy-invoice / includes / Migration / Src / PostTypeMigration.php

PostTypeMigration.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.3.1, at includes/Migration/Src/PostTypeMigration.php

514 lines 19.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Post Type Migration for Easy Invoice
4 *
5 * @package EasyInvoice
6 * @subpackage Migration
7 * @since 2.0.0
8 */
9
10 namespace EasyInvoice\Migration\Src;
11
12 /**
13 * Post type migration class.
14 *
15 * Handles migration of custom post types from old structure to new structure.
16 *
17 * @since 2.0.0
18 */
19 class PostTypeMigration extends AbstractMigration {
20
21 /**
22 * Migration description.
23 *
24 * @since 2.0.0
25 * @var string
26 */
27 protected $description = 'Migrate custom post types to new structure (preserves old data)';
28
29 /**
30 * Constructor.
31 *
32 * @since 2.0.0
33 */
34 public function __construct() {
35 $this->register_old_post_types();
36 }
37
38 /**
39 * Register old post types temporarily for migration.
40 *
41 * @since 2.0.0
42 * @return void
43 */
44 private function register_old_post_types(): void {
45 // Register old post types temporarily for migration
46 foreach (array_keys($this->post_types) as $old_post_type) {
47 if (!post_type_exists($old_post_type)) {
48 register_post_type($old_post_type, [
49 'public' => false,
50 'show_ui' => false,
51 'capability_type' => 'post',
52 'supports' => ['title', 'editor', 'custom-fields'],
53 'can_export' => true
54 ]);
55 }
56 }
57 }
58
59 /**
60 * Post type mappings.
61 * Maps old post types to new post types.
62 *
63 * @since 2.0.0
64 * @var array
65 */
66 private $post_types = [
67 'easy-invoice' => 'easy_invoice',
68 'easy-invoice-quotes' => 'easy_invoice_quote',
69 'easy-invoice-payment' => 'easy_invoice_payment'
70 ];
71
72 /**
73 * Check if migration is needed.
74 *
75 * @since 2.0.0
76 * @return bool
77 */
78 public function is_needed(): bool {
79 global $wpdb;
80
81 // Check if any old post types exist in the database
82 foreach (array_keys($this->post_types) as $old_post_type) {
83 $count = $wpdb->get_var(
84 $wpdb->prepare(
85 "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = %s AND post_status NOT IN ('auto-draft', 'trash', 'inherit')",
86 $old_post_type
87 )
88 );
89
90 if ($count > 0) {
91 $this->log(sprintf('Found %d posts of type %s that need migration', $count, $old_post_type));
92 return true;
93 }
94 }
95
96 return false;
97 }
98
99 /**
100 * Run the migration.
101 *
102 * @since 2.0.0
103 * @return array
104 */
105 public function migrate(): array {
106 $this->log('Starting post type migration (preserving old data)');
107
108 try {
109 $migrated_count = 0;
110 $errors = [];
111
112 foreach ($this->post_types as $old_post_type => $new_post_type) {
113
114 $result = $this->migrate_post_type($old_post_type, $new_post_type);
115
116 if ($result['success']) {
117 $migrated_count += $result['count'];
118 $this->log(sprintf('Successfully migrated %d posts from %s to %s (old data preserved)', $result['count'], $old_post_type, $new_post_type));
119 } else {
120 $errors[] = $result['message'];
121 $this->log(sprintf('Failed to migrate posts from %s: %s', $old_post_type, $result['message']), 'error');
122 }
123 }
124
125 if (empty($errors)) {
126 return $this->success_result(sprintf('Successfully migrated %d posts (old data preserved)', $migrated_count));
127 } else {
128 return $this->error_result('Post type migration completed with errors: ' . implode(', ', $errors));
129 }
130
131 } catch (\Exception $e) {
132 $this->log('Post type migration failed: ' . $e->getMessage(), 'error');
133 return $this->error_result('Post type migration failed: ' . $e->getMessage());
134 }
135 }
136
137 /**
138 * Migrate a specific post type.
139 *
140 * @since 2.0.0
141 * @param string $old_post_type Old post type name.
142 * @param string $new_post_type New post type name.
143 * @return array
144 */
145 private function migrate_post_type(string $old_post_type, string $new_post_type): array {
146 global $wpdb;
147
148 // Direct database query to get posts since post type might not be registered
149 $posts = $wpdb->get_results(
150 $wpdb->prepare(
151 "SELECT * FROM {$wpdb->posts} WHERE post_type = %s",
152 $old_post_type
153 )
154 );
155
156 if (empty($posts)) {
157 return [
158 'success' => true,
159 'count' => 0,
160 'message' => sprintf('No posts found for post type %s', $old_post_type),
161 ];
162 }
163
164 $migrated_count = 0;
165 $errors = [];
166 $old_to_new_mapping = [];
167
168 // First pass: Create new posts and store mappings
169 foreach ($posts as $post) {
170 $result = $this->migrate_single_post($post, $new_post_type);
171
172 if ($result['success']) {
173 $migrated_count++;
174 $old_to_new_mapping[$post->ID] = $result['new_post_id'];
175 $this->log(sprintf('Successfully migrated post %d to new post %d (type: %s)', $post->ID, $result['new_post_id'], $new_post_type));
176 } else {
177 $errors[] = sprintf('Post %d: %s', $post->ID, $result['message']);
178 }
179 }
180
181 // Second pass: Update relationships using the mappings
182 if (!empty($old_to_new_mapping)) {
183 foreach ($old_to_new_mapping as $old_id => $new_id) {
184 $this->update_relationships($old_id, $new_id, $new_post_type, $old_to_new_mapping);
185 }
186 }
187
188 // Store migration summary for this post type
189 $summary = [
190 'old_post_type' => $old_post_type,
191 'new_post_type' => $new_post_type,
192 'migrated_count' => $migrated_count,
193 'old_to_new_mapping' => $old_to_new_mapping,
194 'completed_at' => current_time('mysql')
195 ];
196 update_option('easy_invoice_migration_summary_' . $old_post_type, $summary);
197
198 if (empty($errors)) {
199 return [
200 'success' => true,
201 'count' => $migrated_count,
202 'message' => sprintf('Successfully migrated %d posts (old data preserved)', $migrated_count),
203 ];
204 } else {
205 return [
206 'success' => false,
207 'count' => $migrated_count,
208 'message' => 'Migration completed with errors: ' . implode(', ', $errors),
209 ];
210 }
211 }
212
213 /**
214 * Migrate a single post.
215 *
216 * @since 2.0.0
217 * @param object $post Post object from database query.
218 * @param string $new_post_type New post type name.
219 * @return array
220 */
221 private function migrate_single_post($post, string $new_post_type): array {
222 try {
223 global $wpdb;
224
225 // Create new post with auto-generated ID
226 $new_post_data = [
227 'post_title' => $post->post_title,
228 'post_content' => $post->post_content,
229 'post_excerpt' => $post->post_excerpt,
230 'post_status' => $post->post_status === 'publish' ? 'publish' : 'draft', // Ensure valid status
231 'post_type' => $new_post_type,
232 'post_author' => $post->post_author,
233 'post_date' => $post->post_date,
234 'post_date_gmt' => $post->post_date_gmt,
235 'post_modified' => $post->post_modified,
236 'post_modified_gmt' => $post->post_modified_gmt,
237 'comment_status' => $post->comment_status,
238 'ping_status' => $post->ping_status,
239 'post_password' => $post->post_password,
240 'post_name' => $post->post_name,
241 'to_ping' => $post->to_ping,
242 'pinged' => $post->pinged,
243 'post_content_filtered' => $post->post_content_filtered,
244 'post_parent' => $post->post_parent,
245 'menu_order' => $post->menu_order,
246 'post_mime_type' => $post->post_mime_type,
247 'guid' => '', // Let WordPress generate new GUID
248 ];
249
250 // Insert new post
251 $new_post_id = wp_insert_post($new_post_data, true);
252
253 if (is_wp_error($new_post_id)) {
254 return [
255 'success' => false,
256 'message' => $new_post_id->get_error_message(),
257 ];
258 }
259
260 // Verify the post was actually created
261 $new_post = get_post($new_post_id);
262 if (!$new_post || $new_post->post_type !== $new_post_type) {
263 return [
264 'success' => false,
265 'message' => sprintf('Failed to create new post of type %s', $new_post_type),
266 ];
267 }
268
269 // Store mapping for reference (old ID -> new ID)
270 update_option('easy_invoice_migration_post_mapping_' . $post->ID, $new_post_id);
271
272 // Store original post ID in meta for reference
273 update_post_meta($new_post_id, '_easy_invoice_original_post_id', $post->ID);
274 update_post_meta($new_post_id, '_easy_invoice_original_post_type', $post->post_type);
275
276 // Generate new invoice/quote numbers using the new plugin's numbering system
277 $this->generate_new_numbers($new_post_id, $new_post_type);
278
279 // Store mapping for reference (old ID -> new ID)
280 update_option('easy_invoice_migration_post_mapping_' . $post->ID, $new_post_id);
281
282 // Store original post ID in meta for reference
283 update_post_meta($new_post_id, '_easy_invoice_original_post_id', $post->ID);
284 update_post_meta($new_post_id, '_easy_invoice_original_post_type', $post->post_type);
285
286 $this->log(sprintf('Successfully migrated post %d to new post %d (type: %s)', $post->ID, $new_post_id, $new_post_type));
287
288 return [
289 'success' => true,
290 'message' => 'Post migrated successfully',
291 'new_post_id' => $new_post_id,
292 ];
293
294 } catch (\Exception $e) {
295 $this->log(sprintf('Failed to migrate post %d: %s', $post->ID, $e->getMessage()), 'error');
296 return [
297 'success' => false,
298 'message' => $e->getMessage(),
299 ];
300 }
301 }
302
303 /**
304 * Update relationships between posts.
305 *
306 * @since 2.0.0
307 * @param int $old_id Old post ID
308 * @param int $new_id New post ID
309 * @param string $post_type Post type
310 * @param array $mapping Mapping of old IDs to new IDs
311 * @return void
312 */
313 private function update_relationships(int $old_id, int $new_id, string $post_type, array $mapping): void {
314 global $wpdb;
315
316 // Update client relationships
317 if ($post_type === 'easy_invoice' || $post_type === 'easy_invoice_quote') {
318 // Try different possible meta keys for client ID
319 $possible_client_meta_keys = [
320 '_client_id', 'client_id', '_invoice_client_id', '_quote_client_id',
321 'easy_invoice_client_id', 'easy_invoice_quote_client_id',
322 '_easy_invoice_client_id', '_easy_invoice_quote_client_id'
323 ];
324 $client_id = null;
325
326 // First try to get client ID from meta
327 foreach ($possible_client_meta_keys as $meta_key) {
328 $client_id = get_post_meta($old_id, $meta_key, true);
329 if (!empty($client_id)) {
330 break;
331 }
332 }
333
334 // If no client ID in meta, try to get it from post meta table directly
335 if (empty($client_id)) {
336 foreach ($possible_client_meta_keys as $meta_key) {
337 $client_id = $wpdb->get_var($wpdb->prepare(
338 "SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = %s",
339 $old_id,
340 $meta_key
341 ));
342 if (!empty($client_id)) {
343 break;
344 }
345 }
346 }
347
348 if (!empty($client_id)) {
349 // Get the WordPress user ID for this client
350 $client_user_id = get_option('easy_invoice_client_user_mapping_' . $client_id, 0);
351
352 // If no mapping found, try to find the client by email
353 if ($client_user_id <= 0) {
354 // Try to get client email from old post meta
355 $possible_email_keys = [
356 '_client_email', 'client_email',
357 'easy_invoice_client_email', 'easy_invoice_quote_client_email',
358 '_easy_invoice_client_email', '_easy_invoice_quote_client_email'
359 ];
360
361 $client_email = null;
362 foreach ($possible_email_keys as $meta_key) {
363 $client_email = get_post_meta($old_id, $meta_key, true);
364 if (!empty($client_email)) {
365 break;
366 }
367 }
368
369 if (!empty($client_email)) {
370 $user = get_user_by('email', $client_email);
371 if ($user) {
372 $client_user_id = $user->ID;
373 }
374 }
375 }
376
377 if ($client_user_id > 0) {
378 $meta_prefix = $post_type === 'easy_invoice' ? '_easy_invoice' : '_easy_invoice_quote';
379
380 // Delete any existing client meta
381 $client_meta_keys = [
382 $meta_prefix . '_client_id',
383 $meta_prefix . '_client_name',
384 $meta_prefix . '_client_email',
385 $meta_prefix . '_client_company',
386 $meta_prefix . '_client_address',
387 $meta_prefix . '_client_phone',
388 $meta_prefix . '_client_vat'
389 ];
390 foreach ($client_meta_keys as $meta_key) {
391 delete_post_meta($new_id, $meta_key);
392 }
393
394 // Set new client ID
395 update_post_meta($new_id, $meta_prefix . '_client_id', $client_user_id);
396
397 // Get user data
398 $user = get_user_by('id', $client_user_id);
399 if ($user) {
400 // Get business name from user meta, fallback to display name
401 $business_name = get_user_meta($client_user_id, 'easy_invoice_business_client_name', true);
402 $client_name = !empty($business_name) ? $business_name : $user->display_name;
403
404 update_post_meta($new_id, $meta_prefix . '_client_name', $client_name);
405 update_post_meta($new_id, $meta_prefix . '_client_email', $user->user_email);
406
407 // Copy other client details from user meta
408 $client_fields = [
409 'company' => ['easy_invoice_company', 'company', '_company'],
410 'address' => ['easy_invoice_address', 'address', '_address'],
411 'phone' => ['easy_invoice_phone', 'phone', '_phone'],
412 'vat' => ['easy_invoice_vat', 'vat', '_vat']
413 ];
414
415 foreach ($client_fields as $field => $meta_keys) {
416 foreach ($meta_keys as $meta_key) {
417 $value = get_user_meta($client_user_id, $meta_key, true);
418 if (!empty($value)) {
419 update_post_meta($new_id, $meta_prefix . '_client_' . $field, $value);
420 break;
421 }
422 }
423 }
424
425 $this->log(sprintf('Updated client relationship for %s %d: Client ID %d -> User ID %d', $post_type, $new_id, $client_id, $client_user_id));
426 }
427 } else {
428 $this->log(sprintf('Could not find WordPress user for client ID %d on %s %d', $client_id, $post_type, $new_id), 'warning');
429 }
430 } else {
431 $this->log(sprintf('No client ID found for %s %d', $post_type, $new_id), 'warning');
432 }
433 }
434
435 // Update payment relationships
436 if ($post_type === 'easy_invoice_payment') {
437 // Try different possible meta keys for invoice ID
438 $possible_invoice_meta_keys = ['_invoice_id', 'invoice_id', '_payment_invoice_id'];
439 $invoice_id = null;
440
441 foreach ($possible_invoice_meta_keys as $meta_key) {
442 $invoice_id = get_post_meta($old_id, $meta_key, true);
443 if (!empty($invoice_id)) {
444 break;
445 }
446 }
447
448 if (!empty($invoice_id)) {
449 // Get the new invoice ID from the mapping
450 $new_invoice_id = get_option('easy_invoice_migration_post_mapping_' . $invoice_id, 0);
451
452 if ($new_invoice_id > 0) {
453 update_post_meta($new_id, '_easy_invoice_payment_invoice_id', $new_invoice_id);
454
455 // Copy invoice details to payment
456 $invoice_fields = [
457 'number' => '_easy_invoice_number',
458 'client_id' => '_easy_invoice_client_id',
459 'client_name' => '_easy_invoice_client_name',
460 'client_email' => '_easy_invoice_client_email',
461 'currency_code' => '_easy_invoice_currency_code',
462 'currency_symbol' => '_easy_invoice_currency_symbol'
463 ];
464
465 foreach ($invoice_fields as $field => $meta_key) {
466 $value = get_post_meta($new_invoice_id, $meta_key, true);
467 if (!empty($value)) {
468 update_post_meta($new_id, '_easy_invoice_payment_' . $field, $value);
469 }
470 }
471 }
472 }
473 }
474 }
475
476 /**
477 * Generate new invoice/quote numbers using the new plugin's numbering system.
478 *
479 * @since 2.0.0
480 * @param int $post_id Post ID.
481 * @param string $post_type Post type.
482 * @return void
483 */
484 private function generate_new_numbers(int $post_id, string $post_type): void {
485 try {
486 if ($post_type === 'easy_invoice') {
487 // Generate new invoice number
488 if (class_exists('\\EasyInvoice\\Services\\InvoiceNumberService')) {
489 $invoice_service = new \EasyInvoice\Services\InvoiceNumberService();
490 $new_invoice_number = $invoice_service->generateUniqueNumber();
491
492 // Update the invoice number meta
493 update_post_meta($post_id, '_easy_invoice_number', $new_invoice_number);
494
495 $this->log(sprintf('Generated new invoice number: %s for post %d', $new_invoice_number, $post_id));
496 }
497 } elseif ($post_type === 'easy_invoice_quote') {
498 // Generate new quote number
499 if (class_exists('\\EasyInvoice\\Services\\QuoteNumberService')) {
500 $quote_service = new \EasyInvoice\Services\QuoteNumberService();
501 $new_quote_number = $quote_service->generateUniqueNumber();
502
503 // Update the quote number meta
504 update_post_meta($post_id, '_easy_invoice_quote_number', $new_quote_number);
505
506 $this->log(sprintf('Generated new quote number: %s for post %d', $new_quote_number, $post_id));
507 }
508 }
509 } catch (\Exception $e) {
510 $this->log(sprintf('Error generating new number for post %d: %s', $post_id, $e->getMessage()), 'error');
511 }
512 }
513 }
514