should_render_manual_field() ) { woocommerce_wp_text_input( array( 'id' => Constants::META_MANUAL_COST, 'label' => __( 'MarginBeacon cost', 'marginbeacon' ), 'desc_tip' => true, 'description' => __( 'Tax-exclusive unit cost for the MarginBeacon canonical cost source. Leave blank if missing.', 'marginbeacon' ), 'type' => 'text', 'class' => 'short', 'custom_attributes' => array( 'inputmode' => 'decimal', ), ) ); return; } if ( ! $this->should_render_woo_fallback_field() ) { return; } woocommerce_wp_text_input( array( 'id' => self::WOO_FALLBACK_FIELD, 'label' => __( 'COGS (WooCommerce)', 'marginbeacon' ), 'desc_tip' => true, 'description' => __( 'Tax-exclusive unit cost stored in WooCommerce COGS. Shown because this WooCommerce build does not provide its native Cost field. Leave blank if missing.', 'marginbeacon' ), 'type' => 'text', 'class' => 'short', 'value' => $this->current_woo_cogs_display_value( 0 ), 'custom_attributes' => array( 'inputmode' => 'decimal', ), ) ); } /** * Save simple product manual cost from POST. * * @param int $post_id Product ID. * @return void */ public function save_simple_field( $post_id ) { if ( ! Capabilities::current_user_can_manage() && ! current_user_can( 'edit_product', $post_id ) ) { return; } if ( ! $this->has_valid_product_save_nonce() ) { return; } if ( $this->should_render_manual_field() ) { // Woo submits the product form after saving variations through AJAX. Validate // its variation values again so that request cannot show a false success. $this->validate_submitted_variation_costs(); if ( ! array_key_exists( Constants::META_MANUAL_COST, $_POST ) ) { return; } $raw = isset( $_POST[ Constants::META_MANUAL_COST ] ) ? wp_unslash( $_POST[ Constants::META_MANUAL_COST ] ) : null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Exact-money validator must see raw digits/tags; sanitize_text_field would turn XSS into empty (valid) clear. if ( ! is_string( $raw ) ) { $this->mark_cost_error(); return; } $this->persist_cost( (int) $post_id, $raw ); return; } if ( ! $this->should_render_woo_fallback_field() ) { return; } $this->validate_submitted_woo_fallback_variation_costs(); if ( ! array_key_exists( self::WOO_FALLBACK_FIELD, $_POST ) ) { return; } $raw = isset( $_POST[ self::WOO_FALLBACK_FIELD ] ) ? wp_unslash( $_POST[ self::WOO_FALLBACK_FIELD ] ) : null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Exact-money validator must see raw digits/tags; sanitize_text_field would turn XSS into empty (valid) clear. if ( ! is_string( $raw ) ) { $this->mark_cost_error(); return; } $this->persist_woo_fallback_cost( (int) $post_id, $raw ); } /** * Render manual cost field on variation rows. * * As with the simple-product field, deliberately avoid `wc_input_price` so * invalid input reaches MarginBeacon's server-side validator unchanged. * * @param int $loop Loop index. * @param array $variation_data Data. * @param \WP_Post $variation Variation post. * @return void */ public function render_variation_field( $loop, $variation_data, $variation ) { if ( $this->should_render_manual_field() ) { $value = get_post_meta( $variation->ID, Constants::META_MANUAL_COST, true ); ?>
should_render_woo_fallback_field() ) { return; } $value = $this->current_woo_cogs_display_value( (int) $variation->ID ); ?>
has_valid_product_save_nonce() ) { return; } if ( $this->should_render_manual_field() ) { if ( ! array_key_exists( 'aksmb_manual_cost', $_POST ) ) { return; } $costs = wp_unslash( $_POST['aksmb_manual_cost'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Raw cost strings validated by is_valid_manual_cost_input before persistence. if ( ! is_array( $costs ) ) { $this->mark_cost_error(); return; } if ( ! array_key_exists( $loop, $costs ) ) { return; } $raw = $costs[ $loop ]; if ( ! is_string( $raw ) ) { $this->mark_cost_error(); return; } $this->persist_cost( (int) $variation_id, $raw ); return; } if ( ! $this->should_render_woo_fallback_field() ) { return; } if ( ! array_key_exists( self::WOO_FALLBACK_FIELD, $_POST ) ) { return; } $costs = wp_unslash( $_POST[ self::WOO_FALLBACK_FIELD ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Raw cost strings validated by is_valid_manual_cost_input before persistence. if ( ! is_array( $costs ) ) { $this->mark_cost_error(); return; } if ( ! array_key_exists( $loop, $costs ) ) { return; } $raw = $costs[ $loop ]; if ( ! is_string( $raw ) ) { $this->mark_cost_error(); return; } $this->persist_woo_fallback_cost( (int) $variation_id, $raw ); } /** * Validate manual cost input before persistence. * * Empty means "missing" and is valid. Non-empty input must be a plain, * non-negative decimal number that can be represented losslessly by the * DECIMAL(19,6) storage used by MarginBeacon calculations. Exponent notation, * signed values, ambiguous separators, overflow, and excess precision are * rejected before persistence. * * @param string $raw Raw input. * @return bool */ public static function is_valid_manual_cost_input( $raw ) { $raw = trim( (string) $raw ); if ( '' === $raw ) { return true; } if ( 1 !== preg_match( '/^(?:\d+|\d*\.\d+)$/', $raw ) ) { return false; } $parts = explode( '.', $raw, 2 ); $whole = '' === $parts[0] ? '0' : ltrim( $parts[0], '0' ); $frac = isset( $parts[1] ) ? $parts[1] : ''; if ( '' === $whole ) { $whole = '0'; } if ( strlen( $whole ) > self::MAX_INTEGER_DIGITS ) { return false; } if ( strlen( $frac ) > self::MAX_FRACTION_DIGITS ) { $extra = substr( $frac, self::MAX_FRACTION_DIGITS ); if ( '' !== trim( $extra, '0' ) ) { return false; } } return true; } /** * Mark invalid manual COGS on the post-save redirect and remove WordPress' * generic success message for that submission. * * @param string $location Redirect location. * @param int $post_id Post ID. * @return string */ public function filter_redirect_location( $location, $post_id = 0 ) { unset( $post_id ); if ( ! is_string( $location ) || '' === $location || ! $this->invalid_cost_detected ) { return $location; } $location = remove_query_arg( 'message', $location ); return add_query_arg( self::INVALID_COST_QUERY_ARG, $this->persistence_error_detected ? '2' : '1', $location ); } /** * Suppress WooCommerce's "Product updated" notice on the redirected request. * * This is a second line of defense in case another callback restores a * message query parameter after redirect_post_location has run. * * @param array $messages Post-updated messages. * @return array */ public function filter_post_updated_messages( $messages ) { if ( ! $this->is_product_edit_screen() || ! $this->is_invalid_cost_redirect() || ! isset( $messages['product'] ) || ! is_array( $messages['product'] ) ) { return $messages; } foreach ( $messages['product'] as $key => $message ) { unset( $message ); $messages['product'][ $key ] = ''; } return $messages; } /** * Render exactly one admin error after MarginBeacon rejects manual COGS. * * @return void */ public function render_invalid_cost_notice() { if ( ! current_user_can( 'edit_products' ) || ! $this->is_product_edit_screen() || ! $this->is_invalid_cost_redirect() ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only notice marker. $message = $this->error_message( isset( $_GET[ self::INVALID_COST_QUERY_ARG ] ) && '2' === wp_unslash( $_GET[ self::INVALID_COST_QUERY_ARG ] ) ); ?>

mark_cost_error(); return; } if ( '' === $raw ) { if ( '' !== $previous ) { if ( ! delete_post_meta( $post_id, Constants::META_MANUAL_COST ) ) { $this->mark_cost_error( true ); return; } if ( ! delete_post_meta( $post_id, Constants::META_MANUAL_COST_UPDATED ) && '' !== (string) get_post_meta( $post_id, Constants::META_MANUAL_COST_UPDATED, true ) ) { $this->mark_cost_error( true ); $changed = ! $this->restore_previous_cost( $post_id, $previous ); } else { $changed = true; } } } else { // MoneyAmount requires an integer part, even for sub-unit costs. if ( '.' === $raw[0] ) { $raw = '0' . $raw; } $formatted = wc_format_decimal( $raw ); if ( (string) $formatted !== $previous ) { if ( false === update_post_meta( $post_id, Constants::META_MANUAL_COST, $formatted ) ) { $this->mark_cost_error( true ); return; } $timestamp = gmdate( 'Y-m-d H:i:s' ); if ( false === update_post_meta( $post_id, Constants::META_MANUAL_COST_UPDATED, $timestamp ) && $timestamp !== (string) get_post_meta( $post_id, Constants::META_MANUAL_COST_UPDATED, true ) ) { $this->mark_cost_error( true ); $changed = ! $this->restore_previous_cost( $post_id, $previous ); } else { $changed = true; } } } if ( $changed ) { ( new StalenessObserver() )->on_manual_cost_change( (int) $post_id ); } } /** * Compensate a timestamp failure so a retry can save the same submitted cost. * * If compensation also fails, the caller still marks the changed cost stale. * * @param int $post_id Product ID. * @param string $previous Original cost. * @return bool Whether the original cost was restored. */ private function restore_previous_cost( $post_id, $previous ) { if ( '' === $previous ) { delete_post_meta( $post_id, Constants::META_MANUAL_COST ); } else { update_post_meta( $post_id, Constants::META_MANUAL_COST, $previous ); } return $previous === (string) get_post_meta( $post_id, Constants::META_MANUAL_COST, true ); } /** Validate variation values also present on the final product form request. */ private function validate_submitted_variation_costs() { // phpcs:disable WordPress.Security.NonceVerification.Missing -- Called only by the authorized Woo product save callback. if ( ! array_key_exists( 'aksmb_manual_cost', $_POST ) ) { return; } $costs = isset( $_POST['aksmb_manual_cost'] ) ? wp_unslash( $_POST['aksmb_manual_cost'] ) : null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Raw variation costs validated below. // phpcs:enable WordPress.Security.NonceVerification.Missing if ( ! is_array( $costs ) ) { $this->mark_cost_error(); return; } foreach ( $costs as $cost ) { if ( ! is_string( $cost ) || ! self::is_valid_manual_cost_input( $cost ) ) { $this->mark_cost_error(); return; } } } /** * Publish one request error through redirects or Woo's native AJAX response. * * @param bool $storage_error Whether persistence failed. */ private function mark_cost_error( $storage_error = false ) { $this->invalid_cost_detected = true; $this->persistence_error_detected = $this->persistence_error_detected || $storage_error; } /** * Render in Woo's native variation-save response, before wp_die(). * * Woo's existing admin handler replaces #woocommerce_errors on each save. * Keeping this request's error out of Woo's global option also prevents * deferred product sync from leaking an already-rendered AJAX notice. * * @return void */ public function render_ajax_cost_notice() { if ( ! $this->invalid_cost_detected ) { return; } ?>

error_message( $this->persistence_error_detected ) ); ?>

is_marginbeacon(); } /** * Show a WooCommerce-store fallback editor when canonical is Woo but the * installed WooCommerce build does not render its native Cost field. * * @return bool */ private function should_render_woo_fallback_field() { if ( ! ( new CanonicalCostSource() )->is_woocommerce() ) { return false; } $env = new CogsEnvironment(); if ( ! $env->native_cogs_feature_enabled() ) { return false; } return ! $env->native_product_edit_cogs_ui_available(); } /** * Current Woo COGS display string for an edit field (blank when Missing). * * @param int $product_id Product or variation ID; 0 resolves the current simple product. * @return string */ private function current_woo_cogs_display_value( $product_id ) { $product_id = (int) $product_id; if ( $product_id <= 0 ) { global $post, $thepostid, $product_object; if ( isset( $product_object ) && is_object( $product_object ) && is_callable( array( $product_object, 'get_id' ) ) ) { $product_id = (int) $product_object->get_id(); } elseif ( ! empty( $thepostid ) ) { $product_id = (int) $thepostid; } elseif ( isset( $post->ID ) ) { $product_id = (int) $post->ID; } } if ( $product_id <= 0 ) { return ''; } $product = function_exists( 'wc_get_product' ) ? wc_get_product( $product_id ) : null; $native = ( new ProductCostStore() )->read_native( $product ); if ( null === $native || ! $native->is_resolved() || ! $native->amount() ) { return ''; } return $native->amount()->to_decimal_string(); } /** * Persist WooCommerce COGS via the official store (never MarginBeacon manual meta). * * @param int $post_id Product ID. * @param string $raw Raw input. * @return void */ private function persist_woo_fallback_cost( $post_id, $raw ) { $raw = trim( (string) $raw ); if ( ! self::is_valid_manual_cost_input( $raw ) ) { $this->mark_cost_error(); return; } $store = new ProductCostStore(); // WooCommerce collapses explicit 0 to Missing — clear deliberately. if ( '' === $raw || (bool) preg_match( '/^0+(?:\.0+)?$/', $raw ) ) { $result = $store->clear_native( (int) $post_id ); } else { if ( '.' === $raw[0] ) { $raw = '0' . $raw; } $formatted = function_exists( 'wc_format_decimal' ) ? (string) wc_format_decimal( $raw ) : $raw; $result = $store->write_native( (int) $post_id, $formatted, false ); } if ( ! in_array( $result, array( 'migrated', 'same' ), true ) ) { $this->mark_cost_error( true ); } } /** * Validate Woo fallback variation values on the final product form request. * * @return void */ private function validate_submitted_woo_fallback_variation_costs() { if ( ! isset( $_POST[ self::WOO_FALLBACK_FIELD ] ) || ! is_array( $_POST[ self::WOO_FALLBACK_FIELD ] ) ) { return; } $costs = wp_unslash( $_POST[ self::WOO_FALLBACK_FIELD ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Raw variation costs validated below. foreach ( $costs as $raw ) { if ( ! is_string( $raw ) ) { $this->mark_cost_error(); return; } if ( ! self::is_valid_manual_cost_input( $raw ) ) { $this->mark_cost_error(); return; } } } /** * Verify the native WooCommerce nonce for product-form or variation-AJAX saves. * * @return bool */ private function has_valid_product_save_nonce() { if ( isset( $_POST['woocommerce_meta_nonce'] ) && is_string( $_POST['woocommerce_meta_nonce'] ) ) { $nonce = sanitize_text_field( wp_unslash( $_POST['woocommerce_meta_nonce'] ) ); return (bool) wp_verify_nonce( $nonce, 'woocommerce_save_data' ); } if ( isset( $_POST['security'] ) && is_string( $_POST['security'] ) ) { $nonce = sanitize_text_field( wp_unslash( $_POST['security'] ) ); return (bool) wp_verify_nonce( $nonce, 'save-variations' ); } return false; } /** * Whether the current request is rendering a WooCommerce product editor. * * @return bool */ private function is_product_edit_screen() { $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( ! $screen ) { return false; } return 'product' === (string) $screen->id || ( isset( $screen->post_type ) && 'product' === (string) $screen->post_type ); } /** * Whether the current admin request is the redirect after invalid COGS. * * @return bool */ private function is_invalid_cost_redirect() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only UI flag set by our own redirect filter. $raw = isset( $_GET[ self::INVALID_COST_QUERY_ARG ] ) ? wp_unslash( $_GET[ self::INVALID_COST_QUERY_ARG ] ) : ''; $flag = is_string( $raw ) ? $raw : ''; return '1' === $flag || '2' === $flag; } }