ID, '_aumnexcart_migration_notes', true ); if ( '' === trim( (string) $notes ) ) { return; } add_meta_box( 'aumnexcart_migration_notes', __( 'AumNexCart Migration Notes', 'aumnexcart' ), array( $this, 'render_notes_metabox' ), 'product', 'side', 'low' ); } /** * Renders the stored migration notes as a bullet list. * * @param WP_Post $post */ public function render_notes_metabox( $post ) { $notes = (string) get_post_meta( $post->ID, '_aumnexcart_migration_notes', true ); echo '

' . esc_html__( 'Recorded when this product was migrated from AumNexCart.', 'aumnexcart' ) . '

'; echo ''; } /** * Detects WooCommerce. Returns true only when the main class is * present, since the plugin file existing on disk is not enough. * * @return bool */ public static function is_woocommerce_active() { return class_exists( 'WooCommerce' ); } /** * Counts products eligible for migration. Used for the preview * step in the UI. * * @return int */ public static function count_products() { $q = new WP_Query( array( 'post_type' => AumNexCart_Post_Type::POST_TYPE, 'post_status' => array( 'publish', 'draft' ), 'posts_per_page' => 1, 'fields' => 'ids', 'no_found_rows' => false, ) ); return (int) $q->found_posts; } /** * Initiates a migration session. Stores a token in a transient * and redirects back with it so the JS runner can begin. */ public function handle_start() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Insufficient permissions.', 'aumnexcart' ) ); } check_admin_referer( self::NONCE ); if ( ! self::is_woocommerce_active() ) { self::redirect_migration( array( 'mig_error' => 'no_woo' ) ); } $token = wp_generate_password( 12, false ); $q = new WP_Query( array( 'post_type' => AumNexCart_Post_Type::POST_TYPE, 'post_status' => array( 'publish', 'draft' ), 'posts_per_page' => -1, 'fields' => 'ids', 'orderby' => 'ID', 'order' => 'ASC', ) ); set_transient( self::TRANSIENT . $token, array( 'ids' => $q->posts, 'total' => count( $q->posts ), 'processed' => 0, 'success' => 0, 'failed' => array(), 'results' => array(), ), HOUR_IN_SECONDS ); self::redirect_migration( array( 'mig_token' => $token ) ); } /** * Processes a batch of products. Returns progress JSON. */ public function ajax_batch() { check_ajax_referer( self::NONCE, 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Permission denied.', 'aumnexcart' ) ) ); } $token = isset( $_POST['token'] ) ? sanitize_text_field( wp_unslash( $_POST['token'] ) ) : ''; $state = get_transient( self::TRANSIENT . $token ); if ( ! is_array( $state ) ) { wp_send_json_error( array( 'message' => __( 'Migration session expired.', 'aumnexcart' ) ) ); } $processed = (int) $state['processed']; $end = min( $processed + self::BATCH_SIZE, $state['total'] ); for ( $i = $processed; $i < $end; $i++ ) { $source_id = $state['ids'][ $i ]; $title = get_the_title( $source_id ); $result = $this->migrate_one( $source_id ); if ( is_wp_error( $result ) ) { $state['failed'][] = array( 'id' => $source_id, 'title' => $title, 'message' => $result->get_error_message(), ); $state['results'][] = array( 'id' => $source_id, 'title' => $title, 'ok' => false, 'message' => $result->get_error_message(), ); } else { $state['success']++; $state['results'][] = array( 'id' => $source_id, 'title' => $title, 'ok' => true, 'new_id' => $result['new_id'], 'edit_url' => admin_url( 'post.php?post=' . $result['new_id'] . '&action=edit' ), 'featured' => $result['featured'], 'gallery' => $result['gallery'], 'remote_skipped' => $result['remote_skipped'], 'price' => $result['price'], 'warnings' => $result['warnings'], ); } } $state['processed'] = $end; $done = $end >= $state['total']; set_transient( self::TRANSIENT . $token, $state, HOUR_IN_SECONDS ); $response = array( 'processed' => $state['processed'], 'total' => $state['total'], 'success' => $state['success'], 'failed' => count( $state['failed'] ), 'done' => $done, ); if ( $done ) { $response['failures'] = $state['failed']; $response['results'] = $state['results']; delete_transient( self::TRANSIENT . $token ); } wp_send_json_success( $response ); } /** * Migrates a single product. Returns a structured result array on * success (surfaced in the migration report) or a WP_Error. * * @param int $source_id * @return array|WP_Error { * @type int $new_id New WooCommerce product ID. * @type bool $featured Whether a featured image was set. * @type int $gallery Number of gallery images transferred. * @type int $remote_skipped Number of remote media URLs skipped. * @type string $price Numeric price written to Woo, '' if none. * @type array $warnings Human-readable per-product warnings. * } */ private function migrate_one( $source_id ) { $source = get_post( $source_id ); if ( ! $source ) { return new WP_Error( 'not_found', __( 'Source product not found.', 'aumnexcart' ) ); } $new_id = wp_insert_post( array( 'post_type' => 'product', 'post_title' => $source->post_title, 'post_content' => $source->post_content, 'post_excerpt' => $source->post_excerpt, 'post_status' => $source->post_status, 'post_author' => $source->post_author, 'post_name' => $source->post_name . '-wc', ), true ); if ( is_wp_error( $new_id ) ) { return $new_id; } /* WooCommerce required meta */ update_post_meta( $new_id, '_product_type', 'simple' ); update_post_meta( $new_id, '_manage_stock', 'no' ); update_post_meta( $new_id, '_visibility', 'visible' ); update_post_meta( $new_id, '_stock_status', 'instock' ); /* Per-product result accumulator surfaced in the migration report. */ $result = array( 'new_id' => (int) $new_id, 'featured' => false, 'gallery' => 0, 'remote_skipped' => 0, 'price' => '', 'warnings' => array(), ); /* Price */ $price = AumNexCart_Meta::get_price( $source_id ); if ( 'range' === $price['type'] ) { if ( '' !== $price['min'] ) { update_post_meta( $new_id, '_price', $price['min'] ); update_post_meta( $new_id, '_regular_price', $price['min'] ); $result['price'] = (string) $price['min']; } if ( '' !== $price['max'] && $price['max'] !== $price['min'] ) { /* translators: %s: max price value */ $result['warnings'][] = sprintf( __( 'Price range maximum (%s) not transferred — a Woo simple product carries a single price.', 'aumnexcart' ), $price['max'] ); } } elseif ( 'text' === $price['type'] && '' !== $price['text'] ) { /* translators: %s: original price text */ $result['warnings'][] = sprintf( __( 'Text price (“%s”) not transferred — the Woo product has no numeric price and will not be purchasable until you set one.', 'aumnexcart' ), $price['text'] ); } /* Categories */ $terms = wp_get_object_terms( $source_id, AumNexCart_Post_Type::TAXONOMY, array( 'fields' => 'names' ) ); if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) { $wc_term_ids = array(); foreach ( $terms as $name ) { $existing = term_exists( $name, 'product_cat' ); if ( ! $existing ) { $created = wp_insert_term( $name, 'product_cat' ); if ( ! is_wp_error( $created ) ) { $wc_term_ids[] = (int) $created['term_id']; } } else { $wc_term_ids[] = (int) ( is_array( $existing ) ? $existing['term_id'] : $existing ); } } if ( ! empty( $wc_term_ids ) ) { wp_set_object_terms( $new_id, $wc_term_ids, 'product_cat' ); } } /* Featured image */ $thumb_id = get_post_thumbnail_id( $source_id ); if ( $thumb_id ) { set_post_thumbnail( $new_id, $thumb_id ); $result['featured'] = true; } /* Media gallery — only attachments transfer cleanly. Exclude the * featured image by ID (it may sit at any position, or appear more * than once) and de-duplicate so nothing repeats in the gallery. */ $media = AumNexCart_Meta::get_media( $source_id ); $attachments = array(); $remote_urls = array(); foreach ( $media as $item ) { if ( 'upload' === $item['source'] && $item['attachment_id'] ) { $att_id = (int) $item['attachment_id']; if ( $thumb_id && $att_id === (int) $thumb_id ) { continue; // already used as the featured image } if ( ! in_array( $att_id, $attachments, true ) ) { $attachments[] = $att_id; } } elseif ( ! in_array( $item['url'], $remote_urls, true ) ) { $remote_urls[] = $item['url']; } } if ( ! empty( $attachments ) ) { update_post_meta( $new_id, '_product_image_gallery', implode( ',', $attachments ) ); } $result['gallery'] = count( $attachments ); $result['remote_skipped'] = count( $remote_urls ); if ( ! empty( $remote_urls ) ) { $result['warnings'][] = sprintf( /* translators: %d: number of remote media URLs */ _n( '%d remote media URL not migrated (upload it manually):', '%d remote media URLs not migrated (upload them manually):', count( $remote_urls ), 'aumnexcart' ), count( $remote_urls ) ) . ' ' . implode( ', ', $remote_urls ); } /* Attributes — converted to WooCommerce custom attribute format */ $attrs = AumNexCart_Meta::get_attributes( $source_id ); if ( ! empty( $attrs ) ) { $wc_attrs = array(); $position = 0; foreach ( $attrs as $a ) { $slug = sanitize_title( $a['key'] ); $wc_attrs[ $slug ] = array( 'name' => $a['key'], 'value' => $a['value'], 'position' => $position++, 'is_visible' => 1, 'is_variation' => 0, 'is_taxonomy' => 0, ); } update_post_meta( $new_id, '_product_attributes', $wc_attrs ); } /* Contact actions are NOT applicable to Woo — record as note */ $contact = AumNexCart_Meta::get_contact( $source_id ); if ( ! empty( $contact ) ) { $labels = array(); foreach ( $contact as $c ) { $labels[] = $c['type'] . ': ' . $c['value']; } $result['warnings'][] = __( 'Contact actions not migrated (WooCommerce has no equivalent):', 'aumnexcart' ) . ' ' . implode( '; ', $labels ); } if ( ! empty( $result['warnings'] ) ) { update_post_meta( $new_id, '_aumnexcart_migration_notes', implode( "\n", $result['warnings'] ) ); } return $result; } /** * Renders the Migration tab content. */ public static function render_tab() { $err = isset( $_GET['mig_error'] ) ? sanitize_text_field( wp_unslash( $_GET['mig_error'] ) ) : ''; if ( 'no_woo' === $err ) { echo '

' . esc_html__( 'WooCommerce is not active.', 'aumnexcart' ) . '

'; } $token = isset( $_GET['mig_token'] ) ? sanitize_text_field( wp_unslash( $_GET['mig_token'] ) ) : ''; echo '

' . esc_html__( 'WooCommerce Migration', 'aumnexcart' ) . '

'; echo '

' . esc_html__( 'Copies every NexCart product into a new WooCommerce product. The original products are kept intact, so you can roll back by deactivating WooCommerce. Run this only when you are ready to enable online purchasing.', 'aumnexcart' ) . '

'; if ( ! self::is_woocommerce_active() ) { echo '

' . esc_html__( 'WooCommerce is not installed or activated on this site.', 'aumnexcart' ) . '

'; echo '

' . esc_html__( 'Install and activate WooCommerce first, then return to this page.', 'aumnexcart' ) . '

'; return; } $count = self::count_products(); echo '

' . sprintf( /* translators: %d: number of products to migrate */ esc_html( _n( '%d product will be migrated.', '%d products will be migrated.', $count, 'aumnexcart' ) ), esc_html( $count ) ) . '

'; echo '

' . esc_html__( 'Note:', 'aumnexcart' ) . ' '; echo esc_html__( 'Price ranges only transfer the minimum value; text-mode prices are stored in a notes meta field. Remote media URLs are not auto-uploaded; their list is preserved as a note for manual handling. Contact actions are recorded as notes since WooCommerce does not use them.', 'aumnexcart' ); echo '

'; if ( $token ) { ?>

0 / 0