has_old_data_to_migrate() ) { return; } ?>


get_var( $wpdb->prepare( "SELECT option_id FROM {$wpdb->options} WHERE option_name LIKE %s LIMIT 1", 'mepp\_%' ) ); if ( $has_options ) { return true; } // Check postmeta table $has_meta = $wpdb->get_var( $wpdb->prepare( "SELECT meta_id FROM {$wpdb->postmeta} WHERE meta_key LIKE %s LIMIT 1", '\_mepp\_%' ) ); if ( $has_meta ) { return true; } return false; } public function migration_script() { if ( ! isset( $_GET['page'] ) || 'apd-deposits' !== sanitize_text_field( wp_unslash( $_GET['page'] ) ) ) { return; } if ( get_option( 'apd_migration_from_mepp_done' ) === 'yes' ) { return; } if ( ! $this->has_old_data_to_migrate() ) { return; } ?> migrate_global_settings(); wp_send_json_success( array( 'message' => __( 'Global settings and categories migrated.', 'advanced-partial-payment-or-deposit-for-woocommerce' ), 'progress' => 10, 'next_step' => 'payment_plans', 'next_offset' => 0, ) ); break; case 'payment_plans': $this->migrate_payment_plans(); wp_send_json_success( array( 'message' => __( 'Payment plans migrated.', 'advanced-partial-payment-or-deposit-for-woocommerce' ), 'progress' => 20, 'next_step' => 'products', 'next_offset' => 0, ) ); break; case 'products': $args = array( 'post_type' => 'product', 'posts_per_page' => $batch_size, 'offset' => $offset, 'post_status' => 'any', 'fields' => 'ids', ); $products = get_posts( $args ); if ( ! empty( $products ) ) { foreach ( $products as $post_id ) { $this->migrate_product_meta( $post_id ); } wp_send_json_success( array( /* translators: %d: number of products migrated so far. */ 'message' => sprintf( __( 'Migrated %d products...', 'advanced-partial-payment-or-deposit-for-woocommerce' ), $offset + count( $products ) ), 'progress' => 40, 'next_step' => 'products', 'next_offset' => $offset + $batch_size, ) ); } else { wp_send_json_success( array( 'message' => __( 'Products migration complete.', 'advanced-partial-payment-or-deposit-for-woocommerce' ), 'progress' => 60, 'next_step' => 'orders', 'next_offset' => 0, ) ); } break; case 'orders': $args = array( 'post_type' => 'shop_order', 'posts_per_page' => $batch_size, 'offset' => $offset, 'post_status' => 'any', 'fields' => 'ids', ); // For HPOS compatibility if ( class_exists( 'Automattic\WooCommerce\Utilities\OrderUtil' ) && \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled() ) { $orders = wc_get_orders( array( 'limit' => $batch_size, 'offset' => $offset, 'return' => 'ids', ) ); } else { $orders = get_posts( $args ); } if ( ! empty( $orders ) ) { foreach ( $orders as $order_id ) { $this->migrate_order_meta( $order_id ); } wp_send_json_success( array( /* translators: %d: number of orders migrated so far. */ 'message' => sprintf( __( 'Migrated %d orders...', 'advanced-partial-payment-or-deposit-for-woocommerce' ), $offset + count( $orders ) ), 'progress' => 90, 'next_step' => 'orders', 'next_offset' => $offset + $batch_size, ) ); } else { update_option( 'apd_migration_from_mepp_done', 'yes' ); wp_send_json_success( array( 'message' => __( 'All data migrated successfully.', 'advanced-partial-payment-or-deposit-for-woocommerce' ), 'progress' => 100, 'next_step' => 'done', 'next_offset' => 0, ) ); } break; } } private function migrate_global_settings() { $apd_settings = get_option( 'apd_settings', array() ); // Map global settings $apd_settings['enable_deposit'] = get_option( 'mepp_storewide_deposit_enabled', 'no' ); $old_amount_type = get_option( 'mepp_storewide_deposit_amount_type', 'percent' ); $apd_settings['deposit_type'] = ( $old_amount_type === 'percent' ) ? 'percentage' : 'fixed'; $apd_settings['deposit_value'] = get_option( 'mepp_storewide_deposit_amount', '' ); $apd_settings['force_deposit'] = get_option( 'mepp_storewide_deposit_force_deposit', 'no' ); // Migrate Pro Gateway Rules if ( class_exists( 'WooCommerce' ) ) { $gateways = WC()->payment_gateways()->payment_gateways(); $gateway_modes = isset( $apd_settings['gateway_checkout_modes'] ) ? $apd_settings['gateway_checkout_modes'] : array(); $has_gateway_rules = false; foreach ( $gateways as $gateway_id => $gateway ) { $old_control = get_option( 'mepp_gateway_control_' . $gateway_id ); if ( $old_control === 'force_full' || $old_control === 'hide_ui' ) { $gateway_modes[ $gateway_id ] = array( 'mode' => 'full', 'value' => 0, 'plan_id' => '', ); $has_gateway_rules = true; } } if ( $has_gateway_rules ) { $apd_settings['gateway_checkout_modes'] = $gateway_modes; } } // Migrate Pro User Role / Guest Restrictions $restrict_guests = get_option( 'mepp_restrict_deposits_for_logged_in_users_only' ); if ( $restrict_guests === 'yes' ) { $apd_settings['conditional_allow_guests'] = 'no'; } $disabled_roles = get_option( 'mepp_disable_deposit_for_user_roles' ); if ( ! empty( $disabled_roles ) && is_array( $disabled_roles ) ) { // New plugin uses "Allowed Roles". We need to get all roles and exclude the disabled ones. global $wp_roles; if ( ! isset( $wp_roles ) ) { // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Standard core pattern: lazily populate the real $wp_roles global when nothing has instantiated it yet. $wp_roles = new WP_Roles(); } $all_roles = array_keys( $wp_roles->roles ); $allowed_roles = array_diff( $all_roles, $disabled_roles ); $apd_settings['conditional_user_roles'] = array_values( $allowed_roles ); } update_option( 'apd_settings', $apd_settings ); // Migrate categories $terms = get_terms( array( 'taxonomy' => 'product_cat', 'hide_empty' => false, ) ); if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) { foreach ( $terms as $term ) { $cat_id = $term->term_id; $enable_deposit = get_option( 'mepp_category_' . $cat_id . '_enable_deposit', '' ); if ( $enable_deposit && $enable_deposit !== 'inherit' ) { update_term_meta( $cat_id, '_apd_enable_deposit', $enable_deposit ); } $amount_type = get_option( 'mepp_category_' . $cat_id . '_amount_type', '' ); if ( $amount_type && $amount_type !== 'inherit' ) { $new_type = ( $amount_type === 'percent' ) ? 'percentage' : 'fixed'; update_term_meta( $cat_id, '_apd_deposit_type', $new_type ); } $amount = get_option( 'mepp_category_' . $cat_id . '_amount', '' ); if ( $amount !== '' ) { update_term_meta( $cat_id, '_apd_deposit_value', $amount ); } $force = get_option( 'mepp_category_' . $cat_id . '_force_deposit', '' ); if ( $force === 'yes' ) { update_term_meta( $cat_id, '_apd_force_deposit', 'yes' ); } } } } private function migrate_payment_plans() { $terms = self::get_legacy_plan_terms(); if ( empty( $terms ) ) { return; } $new_plans = get_option( 'apd_payment_plans', array() ); $new_plans = is_array( $new_plans ) ? $new_plans : array(); foreach ( $terms as $term ) { $plan_id = 'plan_' . $term->term_id; if ( isset( $new_plans[ $plan_id ] ) ) { continue; } $amount_type = get_term_meta( $term->term_id, 'amount_type', true ); $new_plans[ $plan_id ] = array( 'id' => $plan_id, 'name' => $term->name, 'description' => $term->description, 'price_type' => 'fixed' === $amount_type ? 'fixed' : 'percentage', 'status' => 'active', 'installments' => self::convert_legacy_plan( $term->term_id ), 'created' => current_time( 'mysql' ), 'updated' => current_time( 'mysql' ), ); } update_option( 'apd_payment_plans', $new_plans ); } /** * Payment plan terms left behind by the 3.x plugin. * * @return WP_Term[] */ private static function get_legacy_plan_terms() { if ( ! taxonomy_exists( 'mepp_payment_plan' ) ) { register_taxonomy( 'mepp_payment_plan', 'product', array( 'public' => false, 'rewrite' => false, 'query_var' => false, ) ); } $terms = get_terms( array( 'taxonomy' => 'mepp_payment_plan', 'hide_empty' => false, ) ); return is_wp_error( $terms ) || ! is_array( $terms ) ? array() : $terms; } /** * Schedule rows of a 3.x payment plan. * * The 3.x plan editor posted its jQuery Repeater value as JSON, which wraps the rows * in the repeater list name: {"payment-plan":[{...},{...}]}. Reading the wrapper as * the row list is what turned every imported plan into a deposit plus one empty row. * * @param int $term_id Legacy plan term ID. * @return array> */ private static function get_legacy_plan_rows( $term_id ) { $details = get_term_meta( $term_id, 'payment_details', true ); if ( is_string( $details ) ) { $decoded = json_decode( $details, true ); if ( ! is_array( $decoded ) ) { $decoded = json_decode( wp_unslash( $details ), true ); } $details = $decoded; } if ( ! is_array( $details ) ) { return array(); } if ( isset( $details['payment-plan'] ) ) { $details = $details['payment-plan']; } return is_array( $details ) ? array_values( array_filter( $details, 'is_array' ) ) : array(); } /** * Convert a 3.x payment plan into installment rules. * * The 3.x plugin counted every "after" offset from the previous payment, and a row with * a set date restarted that chain from the date. The new rules count "after" from the * purchase, so the offsets are added up here. A row that follows a set date can only be * expressed as a date of its own, so it becomes one. * * @param int $term_id Legacy plan term ID. * @return array> */ public static function convert_legacy_plan( $term_id ) { $installments = array( array( 'amount' => floatval( get_term_meta( $term_id, 'deposit_percentage', true ) ), 'due_type' => 'immediately', 'due_after_value' => 0, 'due_after_unit' => 'day', 'due_fixed_date' => '', ), ); $offset = array( 'day' => 0, 'week' => 0, 'month' => 0, ); $anchor = null; foreach ( self::get_legacy_plan_rows( $term_id ) as $row ) { $installment = array( 'amount' => floatval( $row['percentage'] ?? 0 ), 'due_type' => 'after_purchase', 'due_after_value' => 0, 'due_after_unit' => 'day', 'due_fixed_date' => '', ); // 3.x decided "set date" by the date being filled in, not by the checkbox. $date = isset( $row['date'] ) && is_scalar( $row['date'] ) ? trim( (string) $row['date'] ) : ''; $date = '' !== $date ? date_create_immutable( $date, new DateTimeZone( 'UTC' ) ) : false; if ( $date ) { $anchor = $date; } else { list( $value, $unit ) = self::get_legacy_row_offset( $row ); if ( $anchor ) { $anchor = $anchor->modify( "+{$value} {$unit}" ); } else { $offset[ $unit ] += $value; list( $installment['due_after_value'], $installment['due_after_unit'] ) = self::collapse_offset( $offset ); } } if ( $anchor ) { $installment['due_type'] = 'fixed_date'; $installment['due_fixed_date'] = $anchor->format( 'Y-m-d' ); } $installments[] = $installment; } return $installments; } /** * A 3.x row's "after" offset in a unit the new rules support. * * @param array $row Legacy schedule row. * @return array{0:int,1:string} Value and unit (day, week or month). */ private static function get_legacy_row_offset( $row ) { $value = max( 0, intval( $row['after'] ?? 0 ) ); $unit = isset( $row['after-term'] ) && is_scalar( $row['after-term'] ) ? strtolower( rtrim( trim( (string) $row['after-term'] ), 's' ) ) : 'day'; if ( 'year' === $unit ) { return array( $value * 12, 'month' ); } return array( $value, in_array( $unit, array( 'day', 'week', 'month' ), true ) ? $unit : 'day' ); } /** * Express an accumulated offset as one value and unit. * * @param array $offset Days, weeks and months since the purchase. * @return array{0:int,1:string} */ private static function collapse_offset( $offset ) { $used = array_filter( $offset ); if ( empty( $used ) ) { return array( 0, 'day' ); } if ( 1 === count( $used ) ) { $unit = (string) key( $used ); return array( (int) $used[ $unit ], $unit ); } // Mixed units only have an exact single-unit form when no months are involved. // With months the closest is a 30-day month. return array( (int) ( $offset['month'] * 30 + $offset['week'] * 7 + $offset['day'] ), 'day' ); } /** * The installments the 4.0.x import produced for a 3.x plan. * * Kept only so the repair can recognise a plan that is still exactly as that import * left it. Do not use it to import anything. * * @param int $term_id Legacy plan term ID. * @return array> */ private static function convert_legacy_plan_as_imported_by_4_0( $term_id ) { $payment_details = get_term_meta( $term_id, 'payment_details', true ); $payment_details = is_string( $payment_details ) ? json_decode( $payment_details, true ) : $payment_details; $installments = array( array( 'amount' => floatval( get_term_meta( $term_id, 'deposit_percentage', true ) ), 'due_type' => 'immediately', 'due_after_value' => 0, 'due_after_unit' => 'day', 'due_fixed_date' => '', ), ); if ( ! is_array( $payment_details ) ) { return $installments; } foreach ( $payment_details as $detail ) { $detail = is_array( $detail ) ? $detail : array(); $due_type = ( isset( $detail['date_checkbox'] ) && 'on' === $detail['date_checkbox'] ) ? 'fixed_date' : 'after_purchase'; $unit = 'day'; if ( isset( $detail['after-term'] ) && is_string( $detail['after-term'] ) ) { $old_unit = strtolower( rtrim( $detail['after-term'], 's' ) ); if ( in_array( $old_unit, array( 'day', 'week', 'month' ), true ) ) { $unit = $old_unit; } elseif ( 'year' === $old_unit ) { $unit = 'month'; $detail['after'] = intval( $detail['after'] ?? 1 ) * 12; } } $installments[] = array( 'amount' => floatval( $detail['percentage'] ?? 0 ), 'due_type' => $due_type, 'due_after_value' => intval( $detail['after'] ?? 0 ), 'due_after_unit' => $unit, 'due_fixed_date' => $detail['date'] ?? '', ); } return $installments; } /** * Whether two installment lists describe the same schedule. * * @param mixed $a Installments. * @param mixed $b Installments. * @return bool */ private static function same_installments( $a, $b ) { $normalize = static function ( $installments ) { if ( ! is_array( $installments ) ) { return array(); } return array_map( static function ( $inst ) { $inst = is_array( $inst ) ? $inst : array(); return array( round( floatval( $inst['amount'] ?? 0 ), 6 ), (string) ( $inst['due_type'] ?? '' ), intval( $inst['due_after_value'] ?? 0 ), (string) ( $inst['due_after_unit'] ?? '' ), is_scalar( $inst['due_fixed_date'] ?? '' ) ? (string) ( $inst['due_fixed_date'] ?? '' ) : '', ); }, array_values( $installments ) ); }; return $normalize( $a ) === $normalize( $b ); } /** * Rebuild imported plans that the 4.0.x import broke. * * Only a plan whose installments are still exactly what that import produced is * rebuilt. A plan the shop has edited since is left alone and reported instead. * * @return array{repaired:string[],skipped:string[]} */ public static function repair_imported_plans() { $result = array( 'repaired' => array(), 'skipped' => array(), ); $plans = get_option( 'apd_payment_plans', array() ); if ( ! is_array( $plans ) || empty( $plans ) ) { return $result; } $changed = false; foreach ( self::get_legacy_plan_terms() as $term ) { $plan_id = 'plan_' . $term->term_id; if ( empty( $plans[ $plan_id ]['installments'] ) ) { continue; } $current = $plans[ $plan_id ]['installments']; $correct = self::convert_legacy_plan( $term->term_id ); if ( self::same_installments( $current, $correct ) ) { continue; } $name = (string) ( $plans[ $plan_id ]['name'] ?? $term->name ); if ( ! self::same_installments( $current, self::convert_legacy_plan_as_imported_by_4_0( $term->term_id ) ) ) { $result['skipped'][] = $name; continue; } $plans[ $plan_id ]['installments'] = $correct; $plans[ $plan_id ]['updated'] = current_time( 'mysql' ); $result['repaired'][] = $name; $changed = true; } if ( $changed ) { update_option( 'apd_payment_plans', $plans ); } return $result; } /** * Run the imported-plan repair once, in an administrator's request. */ public function maybe_repair_imported_plans() { if ( false !== get_option( self::PLAN_REPAIR_OPTION, false ) || ! current_user_can( 'manage_woocommerce' ) ) { return; } $result = self::repair_imported_plans(); $result['notice'] = ( $result['repaired'] || $result['skipped'] ) ? 'pending' : 'none'; $result['time'] = current_time( 'mysql' ); update_option( self::PLAN_REPAIR_OPTION, $result, false ); } /** * Report the imported-plan repair once, on the Deposits screen. */ public function show_plan_repair_notice() { if ( ! isset( $_GET['page'] ) || 'apd-deposits' !== sanitize_text_field( wp_unslash( $_GET['page'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only screen check. return; } $result = get_option( self::PLAN_REPAIR_OPTION, false ); if ( ! is_array( $result ) || 'pending' !== ( $result['notice'] ?? '' ) ) { return; } $result['notice'] = 'shown'; update_option( self::PLAN_REPAIR_OPTION, $result, false ); $repaired = array_map( 'strval', (array) ( $result['repaired'] ?? array() ) ); $skipped = array_map( 'strval', (array) ( $result['skipped'] ?? array() ) ); ?>


get_meta( '_mepp_is_deposit' ); if ( $is_deposit === 'yes' ) { $order->update_meta_data( '_apd_is_deposit', 'yes' ); $deposit_amount = $order->get_meta( '_mepp_deposit_amount' ); if ( $deposit_amount !== '' ) { $order->update_meta_data( '_apd_deposit_amount', $deposit_amount ); } $due_amount = $order->get_meta( '_mepp_due_amount' ); if ( $due_amount !== '' ) { $order->update_meta_data( '_apd_due_balance', $due_amount ); } // In old plugin due date might be _mepp_payment_schedule or _mepp_due_date $due_date = $order->get_meta( '_mepp_due_date' ); if ( $due_date !== '' ) { $order->update_meta_data( '_apd_due_date', $due_date ); } $order->save_meta_data(); } } }