'environment', 2 => 'source', 3 => 'coverage', 4 => 'acknowledge', 5 => 'complete', ); /** * Register handlers (menu owned by AdminApp tabs). * * @return void */ public function register() { add_action( 'admin_init', array( $this, 'handle_post' ) ); add_action( 'admin_notices', array( $this, 'prompt_incomplete_wizard' ) ); } /** * Persist wizard step submissions. * * @return void */ public function handle_post() { if ( ! isset( $_POST['aksmb_wizard_nonce'] ) ) { return; } if ( ! Capabilities::current_user_can_manage() ) { return; } if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['aksmb_wizard_nonce'] ) ), 'aksmb_cogs_wizard' ) ) { return; } $step = isset( $_POST['wizard_step'] ) ? absint( $_POST['wizard_step'] ) : 1; $action = isset( $_POST['wizard_action'] ) ? sanitize_key( wp_unslash( $_POST['wizard_action'] ) ) : 'next'; $state = $this->get_state(); if ( 'back' === $action ) { $state['step'] = max( 1, $step - 1 ); $this->save_state( $state ); $this->redirect_step( (int) $state['step'] ); } if ( 2 === $step ) { $cost = get_option( Constants::OPTION_COST_SETTINGS, array() ); if ( ! is_array( $cost ) ) { $cost = array(); } $prev_cost = $cost; $source = isset( $_POST['canonical_cost_source'] ) ? sanitize_key( wp_unslash( $_POST['canonical_cost_source'] ) ) : ''; if ( ! ( new CanonicalCostSource() )->is_valid( $source ) ) { $env_detect = ( new CogsEnvironment() )->detect(); $source = ! empty( $env_detect['native_cogs_enabled'] ) ? CanonicalCostSource::WOOCOMMERCE : CanonicalCostSource::MARGINBEACON; } $cost['canonical_cost_source'] = $source; $cost['prefer_native_cogs'] = ( CanonicalCostSource::WOOCOMMERCE === $source ); $cost['fallback_enabled'] = ! empty( $_POST['fallback_enabled'] ); $percent = isset( $_POST['global_assumed_cost_percent'] ) ? sanitize_text_field( wp_unslash( $_POST['global_assumed_cost_percent'] ) ) : ''; if ( '' !== $percent ) { if ( ! preg_match( '/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/', trim( $percent ) ) ) { add_settings_error( 'aksmb_wizard', 'percent', __( 'Assumed cost percentage must be a number between 0 and 100.', 'marginbeacon' ), 'error' ); $state['step'] = 2; $this->save_state( $state ); return; } $percent_str = trim( (string) $percent ); $in_range = bccomp( $percent_str, '0', 6 ) >= 0 && bccomp( $percent_str, '100', 6 ) <= 0; if ( ! $in_range ) { add_settings_error( 'aksmb_wizard', 'percent', __( 'Assumed cost percentage must be between 0 and 100.', 'marginbeacon' ), 'error' ); $state['step'] = 2; $this->save_state( $state ); return; } $cost['global_assumed_cost_percent'] = $percent; } else { $cost['global_assumed_cost_percent'] = null; } $cost['custom_meta_key'] = isset( $_POST['custom_meta_key'] ) ? sanitize_text_field( wp_unslash( $_POST['custom_meta_key'] ) ) : ''; $cost_changed = $prev_cost !== $cost; $updated = update_option( Constants::OPTION_COST_SETTINGS, $cost, true ); ( new CanonicalCostSource() )->set( $source ); if ( $cost_changed && $updated ) { ( new \Kayvandra\MarginBeacon\Application\Freshness\FreshnessService() )->on_global_threshold_change(); } $state['source_configured'] = true; } if ( 4 === $step ) { if ( empty( $_POST['ack_incomplete'] ) || empty( $_POST['ack_tax_exclusive'] ) || empty( $_POST['ack_scope'] ) ) { add_settings_error( 'aksmb_wizard', 'ack', __( 'You must acknowledge all required statements before continuing. Cost completion can be incomplete, but limitation acknowledgement cannot be skipped.', 'marginbeacon' ), 'error' ); $state['step'] = 4; $this->save_state( $state ); return; } $tax_basis = isset( $_POST['cogs_tax_basis'] ) ? sanitize_key( wp_unslash( $_POST['cogs_tax_basis'] ) ) : 'exclusive'; $cost = get_option( Constants::OPTION_COST_SETTINGS, array() ); if ( ! is_array( $cost ) ) { $cost = array(); } $cost['cogs_tax_basis'] = ( 'inclusive' === $tax_basis ) ? 'inclusive' : 'exclusive'; if ( 'inclusive' === $cost['cogs_tax_basis'] ) { $cost['needs_cogs_correction'] = true; } else { $cost['needs_cogs_correction'] = false; } update_option( Constants::OPTION_COST_SETTINGS, $cost, true ); $settings = get_option( Constants::OPTION_SETTINGS, array() ); if ( ! is_array( $settings ) ) { $settings = array(); } $settings['disclaimer_accepted'] = true; update_option( Constants::OPTION_SETTINGS, $settings, true ); $state['acknowledged'] = true; $state['ack_incomplete'] = true; $state['ack_tax_exclusive'] = ( 'exclusive' === $cost['cogs_tax_basis'] ); $state['completed'] = true; $state['completed_at_gmt'] = gmdate( 'Y-m-d H:i:s' ); } if ( 'next' === $action ) { $state['step'] = min( 5, $step + 1 ); } $this->save_state( $state ); $this->redirect_step( (int) $state['step'] ); } /** * Soft prompt when wizard never completed. * * @return void */ public function prompt_incomplete_wizard() { if ( ! Capabilities::current_user_can_manage() ) { return; } $state = $this->get_state(); if ( ! empty( $state['completed'] ) ) { return; } $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( ! $screen || false === strpos( (string) $screen->id, 'marginbeacon' ) ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( isset( $_GET['tab'] ) && Tabs::COST_SETUP === sanitize_key( wp_unslash( $_GET['tab'] ) ) ) { return; } printf( '

%s %s

', esc_html__( 'Finish Cost Setup so MarginBeacon can resolve product costs. Incomplete coverage never produces Safe.', 'marginbeacon' ), esc_url( Tabs::url( Tabs::COST_SETUP ) ), esc_html__( 'Open COGS Wizard', 'marginbeacon' ) ); } /** * Human labels for each wizard step, keyed by step number. * * @return array */ public static function step_labels() { return array( 1 => __( 'Environment', 'marginbeacon' ), 2 => __( 'Cost source', 'marginbeacon' ), 3 => __( 'Coverage', 'marginbeacon' ), 4 => __( 'Acknowledge', 'marginbeacon' ), 5 => __( 'Next steps', 'marginbeacon' ), ); } /** * Render wizard. * * @return void */ public function render() { if ( ! Capabilities::current_user_can_manage() ) { wp_die( esc_html__( 'You do not have permission to access MarginBeacon.', 'marginbeacon' ) ); } // Captured before settings_errors() prints and clears them, so a field // can also show its error next to the input. $errors_by_code = array(); foreach ( (array) get_settings_errors( 'aksmb_wizard' ) as $wizard_error ) { if ( ! empty( $wizard_error['code'] ) ) { $errors_by_code[ (string) $wizard_error['code'] ] = (string) $wizard_error['message']; } } settings_errors( 'aksmb_wizard' ); $state = $this->get_state(); $step = isset( $_GET['step'] ) ? absint( $_GET['step'] ) : (int) $state['step']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( $step < 1 || $step > 5 ) { $step = (int) $state['step']; } $env = ( new CogsEnvironment() )->detect(); $coverage = ( new CostCoverageService() )->summarize( 25 ); $cost = get_option( Constants::OPTION_COST_SETTINGS, array() ); if ( ! is_array( $cost ) ) { $cost = array(); } $labels = self::step_labels(); $total_steps = count( self::STEPS ); ?>

    $slug ) : ?>
  1. >
render_step_environment( $env ); break; case 2: $this->render_step_source( $cost, $env, $errors_by_code ); break; case 3: $this->render_step_coverage( $coverage ); break; case 4: $this->render_step_acknowledge( $cost, $errors_by_code ); break; case 5: $this->render_step_complete( $coverage, $state ); break; } ?>

1 ) : ?>


·

value="" />

%

—

0 ) : ?>

%

1, 'completed' => false, ), $state ); } /** * Persist wizard state to options. * * @param array $state State. * @return void */ private function save_state( array $state ) { update_option( Constants::OPTION_WIZARD_STATE, $state, false ); } /** * Redirect back to wizard tab for the given step. * * @param int $step Step. * @return void */ private function redirect_step( $step ) { wp_safe_redirect( Tabs::url( Tabs::COST_SETUP, array( 'step' => (int) $step ) ) ); exit; } }