get_id(); } elseif ( isset( $post_or_order->ID ) ) { $order_id = (int) $post_or_order->ID; } echo '
'; $this->render_panel( $order_id ); echo '
'; } /** * @param int $order_id Order ID. * @return void */ private function render_panel( $order_id ) { // Defense-in-depth: hide risk panel from non-admin roles. // WooCommerce does not restrict admin order editor URLs by role, // so any logged-in user could access this page via direct URL. if ( ! Capabilities::current_user_can_manage() ) { return; } $row = ( new OrderRiskRepository() )->get_current( $order_id ); if ( ! $row ) { echo '

' . esc_html__( 'Not evaluated yet.', 'marginbeacon' ) . '

'; return; } if ( isset( $_GET['aksmb_ack'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $ack = wp_unslash( $_GET['aksmb_ack'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Exact equality gate only. $ok = is_string( $ack ) && '1' === $ack; $msg = isset( $_GET['aksmb_msg'] ) ? sanitize_text_field( rawurldecode( wp_unslash( $_GET['aksmb_msg'] ) ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended printf( '

%2$s

', esc_attr( $ok ? 'success' : 'error' ), esc_html( $msg ? $msg : ( $ok ? __( 'Saved.', 'marginbeacon' ) : __( 'Failed.', 'marginbeacon' ) ) ) ); } $dual = DualContextPresenter::from_row( $row ); $badge = $row['risk_badge'] ? (string) $row['risk_badge'] : '—'; $summary = $dual['summary']; $order = function_exists( 'wc_get_order' ) ? wc_get_order( $order_id ) : null; $refunded = OrderRefundLifecycle::should_show_label( $row, $order ); if ( $refunded && ! OrderRefundLifecycle::row_is_marked_refunded( $row ) ) { echo '' . esc_html__( 'Refunded', 'marginbeacon' ) . ''; } echo ''; $this->render_recalculation_notice( $row ); $this->render_retry_form( $order_id, $row ); $this->render_acknowledgement_forms( $order_id, $row ); /** * Pro actions under order risk panel (e.g. View price history). * * @param array $row Current order risk snapshot. * @param int $order_id Order ID. */ do_action( 'aksmb_order_risk_pro_actions', $row, $order_id ); echo '

' . esc_html__( 'MarginBeacon calculates gross product margin estimates only. Results do not constitute financial or accounting advice. Acknowledgement never changes severity.', 'marginbeacon' ) . '

'; } /** * Secondary recalculation state while last completed result remains visible (PRD FR-055). * * @param array $row Snapshot. * @return void */ private function render_recalculation_notice( array $row ) { $state = isset( $row['recalculation_state'] ) ? (string) $row['recalculation_state'] : ''; if ( '' === $state ) { return; } $messages = array( 'queued' => __( 'Recalculation queued — last completed result shown until replacement finishes.', 'marginbeacon' ), 'running' => __( 'Recalculation running — last completed result shown until replacement finishes.', 'marginbeacon' ), 'failed' => __( 'Recalculation failed — last completed result shown.', 'marginbeacon' ), ); $msg = isset( $messages[ $state ] ) ? $messages[ $state ] : ''; if ( ! $msg ) { return; } $error = ! empty( $row['recalculation_error_code'] ) ? (string) $row['recalculation_error_code'] : ''; $class = 'failed' === $state ? 'notice-error' : 'notice-info'; echo '

'; echo esc_html( $msg ); if ( $error ) { echo ' ' . esc_html( $error ) . ''; } echo '

'; } /** * Failed retry + Free basic refresh. * * @param int $order_id Order ID. * @param array $row Current snapshot. * @return void */ private function render_retry_form( $order_id, array $row ) { /** * Whether to show Free basic refresh (Pro may replace with Recalculate risk). * * @param bool $show Default true. * @param array $row Snapshot. * @param int $order_id Order ID. */ if ( ! apply_filters( 'aksmb_order_risk_show_basic_refresh', true, $row, $order_id ) ) { return; } $order = function_exists( 'wc_get_order' ) ? wc_get_order( $order_id ) : null; if ( OrderRefundLifecycle::should_show_label( $row, $order ) ) { return; // Never refresh into refund-adjusted economics. } $presentation = isset( $row['presentation_state'] ) ? (string) $row['presentation_state'] : ''; $processing = isset( $row['processing_state'] ) ? (string) $row['processing_state'] : ''; $recalc = isset( $row['recalculation_state'] ) ? (string) $row['recalculation_state'] : ''; $is_failed = ( 'failed' === $presentation || 'failed' === $processing || 'failed' === $recalc ); $is_pending = ( 'pending' === $presentation || in_array( $processing, array( 'queued', 'running' ), true ) ) || in_array( $recalc, array( 'queued', 'running' ), true ); $signal = isset( $row['promotion_signal'] ) ? (string) $row['promotion_signal'] : 'none'; $can_refresh = false; if ( $is_failed ) { $can_refresh = true; } elseif ( ! $is_pending && 'completed' === $processing && 'none' !== $signal ) { $can_refresh = true; } elseif ( ! $is_pending && in_array( $presentation, array( 'financial_result', 'unsupported' ), true ) ) { $can_refresh = true; } if ( ! $can_refresh ) { return; } $label = $is_failed ? __( 'Retry evaluation', 'marginbeacon' ) : __( 'Refresh evaluation', 'marginbeacon' ); ?>

get_items( 'line_item' ) as $item ) { $product_id = (int) $item->get_product_id(); $variation_id = (int) $item->get_variation_id(); $key = $variation_id ? 'v' . $variation_id : 'p' . $product_id; if ( isset( $seen[ $key ] ) ) { continue; } $seen[ $key ] = true; $active = $svc->active_for_catalog( $product_id, $variation_id ); if ( ! $active && ! $offer_new ) { continue; } $entries[] = array( 'name' => $item->get_name(), 'active' => $active, 'scope' => $variation_id ? AcknowledgementService::SCOPE_VARIATION : AcknowledgementService::SCOPE_PRODUCT, 'entity' => $variation_id ? $variation_id : $product_id, ); } foreach ( $order->get_coupon_codes() as $code ) { $coupon = new \WC_Coupon( (string) $code ); if ( ! $coupon->get_id() ) { continue; } $coupon_id = (int) $coupon->get_id(); $key = 'c' . $coupon_id; if ( isset( $seen[ $key ] ) ) { continue; } $seen[ $key ] = true; $active = $svc->active_for_coupon( $coupon_id ); if ( ! $active && ! $offer_new ) { continue; } $entries[] = array( /* translators: %s: coupon code */ 'name' => sprintf( __( 'Coupon: %s', 'marginbeacon' ), $coupon->get_code() ), 'active' => $active, 'scope' => AcknowledgementService::SCOPE_COUPON, 'entity' => $coupon_id, ); } if ( empty( $entries ) ) { return; } echo '
'; echo '

' . esc_html__( 'Acknowledged loss leader', 'marginbeacon' ) . '

'; echo '

' . esc_html__( 'Mark intentional below-cost items. Severity stays Critical/High/Warning; only dashboard noise and work-list grouping change.', 'marginbeacon' ) . '

'; foreach ( $entries as $entry ) { echo '
'; echo '

' . esc_html( $entry['name'] ) . '

'; if ( $entry['active'] ) { echo '
'; echo '' . esc_html__( 'Acknowledged', 'marginbeacon' ) . ''; echo '
'; if ( ! empty( $entry['active']['reason'] ) ) { echo '

' . esc_html__( 'Reason', 'marginbeacon' ) . ': ' . esc_html( (string) $entry['active']['reason'] ) . '

'; } echo '
'; wp_nonce_field( 'aksmb_ack', 'aksmb_ack_nonce' ); echo ''; echo ''; echo ''; echo ''; echo '
'; } else { echo '
'; wp_nonce_field( 'aksmb_ack', 'aksmb_ack_nonce' ); echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo '
'; } echo '
'; } echo '
'; } /** * @param array $row Risk row. * @return string */ private function signal_label( array $row ) { $signal = isset( $row['promotion_signal'] ) ? (string) $row['promotion_signal'] : 'none'; $conf = isset( $row['promotion_signal_confidence'] ) ? (string) $row['promotion_signal_confidence'] : 'none'; $signal_labels = array( 'coupon' => __( 'Coupon', 'marginbeacon' ), 'sale_price' => __( 'Sale price', 'marginbeacon' ), 'both' => __( 'Coupon + sale', 'marginbeacon' ), 'none' => __( 'None', 'marginbeacon' ), ); $conf_labels = array( 'verified' => __( 'Verified', 'marginbeacon' ), 'suspected' => __( 'Suspected', 'marginbeacon' ), 'none' => __( 'None', 'marginbeacon' ), ); $left = isset( $signal_labels[ $signal ] ) ? $signal_labels[ $signal ] : $signal; $right = isset( $conf_labels[ $conf ] ) ? $conf_labels[ $conf ] : $conf; return $left . ' / ' . $right; } /** * @param array $row Risk row. * @return string */ private function origin_label( array $row ) { $origin = ! empty( $row['evaluation_origin'] ) ? (string) $row['evaluation_origin'] : 'realtime'; if ( 'recent_preview' === $origin ) { return __( 'Recent orders preview', 'marginbeacon' ); } if ( 'realtime' === $origin ) { return __( 'Realtime', 'marginbeacon' ); } return ucfirst( str_replace( '_', ' ', $origin ) ); } }