Decisions per product and price state, for this request. */ private $decisions = array(); public function __construct( ReferenceService $references ) { $this->references = $references; } public function register(): void { add_filter( 'strikewise_settings', array( $this, 'settings' ) ); if ( ! self::is_enabled() ) { return; } add_action( 'strikewise_reference_refreshed', array( $this, 'forget' ) ); add_filter( 'woocommerce_product_get_regular_price', array( $this, 'filter_regular_price' ), 20, 2 ); add_filter( 'woocommerce_product_variation_get_regular_price', array( $this, 'filter_regular_price' ), 20, 2 ); add_filter( 'woocommerce_product_is_on_sale', array( $this, 'filter_is_on_sale' ), 20, 2 ); add_filter( 'woocommerce_variation_prices_regular_price', array( $this, 'filter_variation_prices_regular' ), 20, 3 ); add_filter( 'woocommerce_get_variation_prices_hash', array( $this, 'filter_prices_hash' ), 20, 3 ); } public static function is_enabled(): bool { return 'yes' === get_option( self::OPTION_ENABLED, 'yes' ); } /** * Reductions without a known earlier price are not shown, unless the shop chose to keep them. */ public static function hides_unknown(): bool { return 'keep' !== get_option( self::OPTION_NO_HISTORY, 'hide' ); } /** * What CJEU mode does with this product right now. * * @return array{action:string,regular:string} regular = the regular price to show, in the basis prices are entered in. */ public function decide( \WC_Product $product ): array { $sale = $product->get_sale_price( 'edit' ); $regular = $product->get_regular_price( 'edit' ); $key = $product->get_id() . '|' . $regular . '|' . $sale; $none = array( 'action' => self::ACTION_NONE, 'regular' => (string) $regular, ); // Re-checked on every call, so switching the option off takes effect immediately. if ( ! self::is_enabled() ) { return $none; } if ( isset( $this->decisions[ $key ] ) ) { return $this->decisions[ $key ]; } $decision = $none; // 'edit' context: the stored sale, without our own is_on_sale filter. // A UVP comparison is not a reduction of the own price, and perishable goods are exempt. if ( '' !== $sale && $product->is_on_sale( 'edit' ) && ! $product->is_type( array( 'variable', 'grouped' ) ) && ! Perishable::applies( $product ) && ! (bool) apply_filters( 'strikewise_is_price_comparison', false, $product ) ) { $reference = $this->references->get( $product ); $current = PriceResolver::to_gross( $product, (float) $sale ); if ( $reference->has_price() && null !== $reference->t0 ) { $decision = (float) $reference->price - $current > 0.005 ? array( 'action' => self::ACTION_REPLACE, 'regular' => self::format( PriceResolver::from_gross( $product, (float) $reference->price ) ), ) : array( 'action' => self::ACTION_NO_REDUCTION, 'regular' => (string) $sale, ); } elseif ( Reference::STATUS_NO_HISTORY === $reference->status && ( self::hides_unknown() || ReferenceService::launched_reduced( $product, $reference ) ) ) { // Never offered at a higher price, or no earlier price known: no reduction to announce. $decision = array( 'action' => self::ACTION_NO_REDUCTION, 'regular' => (string) $sale, ); } } $this->decisions[ $key ] = $decision; return $decision; } /** * Drops cached decisions of a product whose reference price changed in this request. * * @param int $product_id */ public function forget( $product_id ): void { $prefix = (int) $product_id . '|'; foreach ( array_keys( $this->decisions ) as $key ) { if ( 0 === strpos( $key, $prefix ) ) { unset( $this->decisions[ $key ] ); } } } /** * @param string|float $value * @param mixed $product * @return string|float */ public function filter_regular_price( $value, $product ) { if ( ! $product instanceof \WC_Product || '' === $value ) { return $value; } $decision = $this->decide( $product ); return self::ACTION_NONE === $decision['action'] ? $value : $decision['regular']; } /** * @param bool $on_sale * @param mixed $product */ public function filter_is_on_sale( $on_sale, $product ): bool { if ( ! $on_sale || ! $product instanceof \WC_Product || $product->is_type( 'variable' ) ) { return (bool) $on_sale; } return self::ACTION_NO_REDUCTION !== $this->decide( $product )['action']; } /** * Price ranges and sale detection of variable products use these cached arrays. * * @param string|float $price * @param mixed $variation * @param mixed $product * @return string|float */ public function filter_variation_prices_regular( $price, $variation, $product ) { return $variation instanceof \WC_Product ? $this->filter_regular_price( $price, $variation ) : $price; } /** * Makes WooCommerce cache the variation prices separately for CJEU mode. * * @param array $hash * @param mixed $product * @param bool $for_display * @return array */ public function filter_prices_hash( $hash, $product, $for_display ) { $hash[] = 'strikewise-cjeu-1|' . ( self::hides_unknown() ? 'hide' : 'keep' ); return $hash; } private static function format( float $amount ): string { return (string) round( $amount, wc_get_price_decimals() ); } /** * Settings rows, added to WooCommerce -> Settings -> Products -> Strikewise. * * @param array> $settings * @return array> */ public function settings( $settings ) { $settings[] = array( 'title' => __( 'CJEU mode', 'strikewise' ), 'type' => 'title', 'desc' => __( 'Judgment C-330/23 of the CJEU: an announced reduction must refer to the lowest price of the 30 days before it.', 'strikewise' ), 'id' => 'strikewise_cjeu', ); $settings[] = array( 'title' => __( 'Enable', 'strikewise' ), 'desc' => __( 'Calculate strike-through prices from the lowest price of the 30 days before the reduction (CJEU, C-330/23). A reduction that is not below that price is not shown as one.', 'strikewise' ), 'id' => self::OPTION_ENABLED, 'type' => 'checkbox', 'default' => 'yes', ); $settings[] = array( 'title' => __( 'Without price history', 'strikewise' ), 'desc' => __( 'For reduced products without a known earlier price, e.g. directly after installing Strikewise. A strike-through price without the earlier price is not allowed; you can enter a known earlier price in the product.', 'strikewise' ), 'id' => self::OPTION_NO_HISTORY, 'type' => 'select', 'default' => 'hide', 'options' => array( 'hide' => __( 'Do not show a reduction (recommended)', 'strikewise' ), 'keep' => __( 'Keep the regular price as strike-through price', 'strikewise' ), ), ); $settings[] = array( 'type' => 'sectionend', 'id' => 'strikewise_cjeu', ); return $settings; } }