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;
}
}