register_old_post_types(); } /** * Register old post types temporarily for migration. * * @since 2.0.0 * @return void */ private function register_old_post_types(): void { // Register old post types temporarily for migration foreach (array_keys($this->post_types) as $old_post_type) { if (!post_type_exists($old_post_type)) { register_post_type($old_post_type, [ 'public' => false, 'show_ui' => false, 'capability_type' => 'post', 'supports' => ['title', 'editor', 'custom-fields'], 'can_export' => true ]); } } } /** * Post type mappings. * Maps old post types to new post types. * * @since 2.0.0 * @var array */ private $post_types = [ 'easy-invoice' => 'easy_invoice', 'easy-invoice-quotes' => 'easy_invoice_quote', 'easy-invoice-payment' => 'easy_invoice_payment' ]; /** * Check if migration is needed. * * @since 2.0.0 * @return bool */ public function is_needed(): bool { global $wpdb; // Check if any old post types exist in the database foreach (array_keys($this->post_types) as $old_post_type) { $count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = %s AND post_status NOT IN ('auto-draft', 'trash', 'inherit')", $old_post_type ) ); if ($count > 0) { $this->log(sprintf('Found %d posts of type %s that need migration', $count, $old_post_type)); return true; } } return false; } /** * Run the migration. * * @since 2.0.0 * @return array */ public function migrate(): array { $this->log('Starting post type migration (preserving old data)'); try { $migrated_count = 0; $errors = []; foreach ($this->post_types as $old_post_type => $new_post_type) { $result = $this->migrate_post_type($old_post_type, $new_post_type); if ($result['success']) { $migrated_count += $result['count']; $this->log(sprintf('Successfully migrated %d posts from %s to %s (old data preserved)', $result['count'], $old_post_type, $new_post_type)); } else { $errors[] = $result['message']; $this->log(sprintf('Failed to migrate posts from %s: %s', $old_post_type, $result['message']), 'error'); } } if (empty($errors)) { return $this->success_result(sprintf('Successfully migrated %d posts (old data preserved)', $migrated_count)); } else { return $this->error_result('Post type migration completed with errors: ' . implode(', ', $errors)); } } catch (\Exception $e) { $this->log('Post type migration failed: ' . $e->getMessage(), 'error'); return $this->error_result('Post type migration failed: ' . $e->getMessage()); } } /** * Migrate a specific post type. * * @since 2.0.0 * @param string $old_post_type Old post type name. * @param string $new_post_type New post type name. * @return array */ private function migrate_post_type(string $old_post_type, string $new_post_type): array { global $wpdb; // Direct database query to get posts since post type might not be registered $posts = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->posts} WHERE post_type = %s", $old_post_type ) ); if (empty($posts)) { return [ 'success' => true, 'count' => 0, 'message' => sprintf('No posts found for post type %s', $old_post_type), ]; } $migrated_count = 0; $errors = []; $old_to_new_mapping = []; // First pass: Create new posts and store mappings foreach ($posts as $post) { $result = $this->migrate_single_post($post, $new_post_type); if ($result['success']) { $migrated_count++; $old_to_new_mapping[$post->ID] = $result['new_post_id']; $this->log(sprintf('Successfully migrated post %d to new post %d (type: %s)', $post->ID, $result['new_post_id'], $new_post_type)); } else { $errors[] = sprintf('Post %d: %s', $post->ID, $result['message']); } } // Second pass: Update relationships using the mappings if (!empty($old_to_new_mapping)) { foreach ($old_to_new_mapping as $old_id => $new_id) { $this->update_relationships($old_id, $new_id, $new_post_type, $old_to_new_mapping); } } // Store migration summary for this post type $summary = [ 'old_post_type' => $old_post_type, 'new_post_type' => $new_post_type, 'migrated_count' => $migrated_count, 'old_to_new_mapping' => $old_to_new_mapping, 'completed_at' => current_time('mysql') ]; update_option('easy_invoice_migration_summary_' . $old_post_type, $summary); if (empty($errors)) { return [ 'success' => true, 'count' => $migrated_count, 'message' => sprintf('Successfully migrated %d posts (old data preserved)', $migrated_count), ]; } else { return [ 'success' => false, 'count' => $migrated_count, 'message' => 'Migration completed with errors: ' . implode(', ', $errors), ]; } } /** * Migrate a single post. * * @since 2.0.0 * @param object $post Post object from database query. * @param string $new_post_type New post type name. * @return array */ private function migrate_single_post($post, string $new_post_type): array { try { global $wpdb; // Create new post with auto-generated ID $new_post_data = [ 'post_title' => $post->post_title, 'post_content' => $post->post_content, 'post_excerpt' => $post->post_excerpt, 'post_status' => $post->post_status === 'publish' ? 'publish' : 'draft', // Ensure valid status 'post_type' => $new_post_type, 'post_author' => $post->post_author, 'post_date' => $post->post_date, 'post_date_gmt' => $post->post_date_gmt, 'post_modified' => $post->post_modified, 'post_modified_gmt' => $post->post_modified_gmt, 'comment_status' => $post->comment_status, 'ping_status' => $post->ping_status, 'post_password' => $post->post_password, 'post_name' => $post->post_name, 'to_ping' => $post->to_ping, 'pinged' => $post->pinged, 'post_content_filtered' => $post->post_content_filtered, 'post_parent' => $post->post_parent, 'menu_order' => $post->menu_order, 'post_mime_type' => $post->post_mime_type, 'guid' => '', // Let WordPress generate new GUID ]; // Insert new post $new_post_id = wp_insert_post($new_post_data, true); if (is_wp_error($new_post_id)) { return [ 'success' => false, 'message' => $new_post_id->get_error_message(), ]; } // Verify the post was actually created $new_post = get_post($new_post_id); if (!$new_post || $new_post->post_type !== $new_post_type) { return [ 'success' => false, 'message' => sprintf('Failed to create new post of type %s', $new_post_type), ]; } // Store mapping for reference (old ID -> new ID) update_option('easy_invoice_migration_post_mapping_' . $post->ID, $new_post_id); // Store original post ID in meta for reference update_post_meta($new_post_id, '_easy_invoice_original_post_id', $post->ID); update_post_meta($new_post_id, '_easy_invoice_original_post_type', $post->post_type); // Generate new invoice/quote numbers using the new plugin's numbering system $this->generate_new_numbers($new_post_id, $new_post_type); // Store mapping for reference (old ID -> new ID) update_option('easy_invoice_migration_post_mapping_' . $post->ID, $new_post_id); // Store original post ID in meta for reference update_post_meta($new_post_id, '_easy_invoice_original_post_id', $post->ID); update_post_meta($new_post_id, '_easy_invoice_original_post_type', $post->post_type); $this->log(sprintf('Successfully migrated post %d to new post %d (type: %s)', $post->ID, $new_post_id, $new_post_type)); return [ 'success' => true, 'message' => 'Post migrated successfully', 'new_post_id' => $new_post_id, ]; } catch (\Exception $e) { $this->log(sprintf('Failed to migrate post %d: %s', $post->ID, $e->getMessage()), 'error'); return [ 'success' => false, 'message' => $e->getMessage(), ]; } } /** * Update relationships between posts. * * @since 2.0.0 * @param int $old_id Old post ID * @param int $new_id New post ID * @param string $post_type Post type * @param array $mapping Mapping of old IDs to new IDs * @return void */ private function update_relationships(int $old_id, int $new_id, string $post_type, array $mapping): void { global $wpdb; // Update client relationships if ($post_type === 'easy_invoice' || $post_type === 'easy_invoice_quote') { // Try different possible meta keys for client ID $possible_client_meta_keys = [ '_client_id', 'client_id', '_invoice_client_id', '_quote_client_id', 'easy_invoice_client_id', 'easy_invoice_quote_client_id', '_easy_invoice_client_id', '_easy_invoice_quote_client_id' ]; $client_id = null; // First try to get client ID from meta foreach ($possible_client_meta_keys as $meta_key) { $client_id = get_post_meta($old_id, $meta_key, true); if (!empty($client_id)) { break; } } // If no client ID in meta, try to get it from post meta table directly if (empty($client_id)) { foreach ($possible_client_meta_keys as $meta_key) { $client_id = $wpdb->get_var($wpdb->prepare( "SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = %s", $old_id, $meta_key )); if (!empty($client_id)) { break; } } } if (!empty($client_id)) { // Get the WordPress user ID for this client $client_user_id = get_option('easy_invoice_client_user_mapping_' . $client_id, 0); // If no mapping found, try to find the client by email if ($client_user_id <= 0) { // Try to get client email from old post meta $possible_email_keys = [ '_client_email', 'client_email', 'easy_invoice_client_email', 'easy_invoice_quote_client_email', '_easy_invoice_client_email', '_easy_invoice_quote_client_email' ]; $client_email = null; foreach ($possible_email_keys as $meta_key) { $client_email = get_post_meta($old_id, $meta_key, true); if (!empty($client_email)) { break; } } if (!empty($client_email)) { $user = get_user_by('email', $client_email); if ($user) { $client_user_id = $user->ID; } } } if ($client_user_id > 0) { $meta_prefix = $post_type === 'easy_invoice' ? '_easy_invoice' : '_easy_invoice_quote'; // Delete any existing client meta $client_meta_keys = [ $meta_prefix . '_client_id', $meta_prefix . '_client_name', $meta_prefix . '_client_email', $meta_prefix . '_client_company', $meta_prefix . '_client_address', $meta_prefix . '_client_phone', $meta_prefix . '_client_vat' ]; foreach ($client_meta_keys as $meta_key) { delete_post_meta($new_id, $meta_key); } // Set new client ID update_post_meta($new_id, $meta_prefix . '_client_id', $client_user_id); // Get user data $user = get_user_by('id', $client_user_id); if ($user) { // Get business name from user meta, fallback to display name $business_name = get_user_meta($client_user_id, 'easy_invoice_business_client_name', true); $client_name = !empty($business_name) ? $business_name : $user->display_name; update_post_meta($new_id, $meta_prefix . '_client_name', $client_name); update_post_meta($new_id, $meta_prefix . '_client_email', $user->user_email); // Copy other client details from user meta $client_fields = [ 'company' => ['easy_invoice_company', 'company', '_company'], 'address' => ['easy_invoice_address', 'address', '_address'], 'phone' => ['easy_invoice_phone', 'phone', '_phone'], 'vat' => ['easy_invoice_vat', 'vat', '_vat'] ]; foreach ($client_fields as $field => $meta_keys) { foreach ($meta_keys as $meta_key) { $value = get_user_meta($client_user_id, $meta_key, true); if (!empty($value)) { update_post_meta($new_id, $meta_prefix . '_client_' . $field, $value); break; } } } $this->log(sprintf('Updated client relationship for %s %d: Client ID %d -> User ID %d', $post_type, $new_id, $client_id, $client_user_id)); } } else { $this->log(sprintf('Could not find WordPress user for client ID %d on %s %d', $client_id, $post_type, $new_id), 'warning'); } } else { $this->log(sprintf('No client ID found for %s %d', $post_type, $new_id), 'warning'); } } // Update payment relationships if ($post_type === 'easy_invoice_payment') { // Try different possible meta keys for invoice ID $possible_invoice_meta_keys = ['_invoice_id', 'invoice_id', '_payment_invoice_id']; $invoice_id = null; foreach ($possible_invoice_meta_keys as $meta_key) { $invoice_id = get_post_meta($old_id, $meta_key, true); if (!empty($invoice_id)) { break; } } if (!empty($invoice_id)) { // Get the new invoice ID from the mapping $new_invoice_id = get_option('easy_invoice_migration_post_mapping_' . $invoice_id, 0); if ($new_invoice_id > 0) { update_post_meta($new_id, '_easy_invoice_payment_invoice_id', $new_invoice_id); // Copy invoice details to payment $invoice_fields = [ 'number' => '_easy_invoice_number', 'client_id' => '_easy_invoice_client_id', 'client_name' => '_easy_invoice_client_name', 'client_email' => '_easy_invoice_client_email', 'currency_code' => '_easy_invoice_currency_code', 'currency_symbol' => '_easy_invoice_currency_symbol' ]; foreach ($invoice_fields as $field => $meta_key) { $value = get_post_meta($new_invoice_id, $meta_key, true); if (!empty($value)) { update_post_meta($new_id, '_easy_invoice_payment_' . $field, $value); } } } } } } /** * Generate new invoice/quote numbers using the new plugin's numbering system. * * @since 2.0.0 * @param int $post_id Post ID. * @param string $post_type Post type. * @return void */ private function generate_new_numbers(int $post_id, string $post_type): void { try { if ($post_type === 'easy_invoice') { // Generate new invoice number if (class_exists('\\EasyInvoice\\Services\\InvoiceNumberService')) { $invoice_service = new \EasyInvoice\Services\InvoiceNumberService(); $new_invoice_number = $invoice_service->generateUniqueNumber(); // Update the invoice number meta update_post_meta($post_id, '_easy_invoice_number', $new_invoice_number); $this->log(sprintf('Generated new invoice number: %s for post %d', $new_invoice_number, $post_id)); } } elseif ($post_type === 'easy_invoice_quote') { // Generate new quote number if (class_exists('\\EasyInvoice\\Services\\QuoteNumberService')) { $quote_service = new \EasyInvoice\Services\QuoteNumberService(); $new_quote_number = $quote_service->generateUniqueNumber(); // Update the quote number meta update_post_meta($post_id, '_easy_invoice_quote_number', $new_quote_number); $this->log(sprintf('Generated new quote number: %s for post %d', $new_quote_number, $post_id)); } } } catch (\Exception $e) { $this->log(sprintf('Error generating new number for post %d: %s', $post_id, $e->getMessage()), 'error'); } } }