rules ) {
$this->rules = Rules::runtime();
}
return $this->rules;
}
public function privacy(): void {
$post = get_post();
$content = $post ? $post->post_content : '';
if ( ! is_product() && ! is_shop() && ! is_product_taxonomy() && ! str_contains( $content, 'wp:woocommerce/' ) && ! preg_match( '/\[(?:products|product_page|product_category|sale_products|featured_products|recent_products|best_selling_products|top_rated_products)\b/', $content ) ) {
return;
}
foreach ( $this->rules() as $rule ) {
if ( ! empty( $rule['enabled'] ) && ( ! empty( $rule['show_single'] ) || ! empty( $rule['show_archive'] ) ) ) {
$this->private_response();
break;
}
}
}
private function private_response(): bool {
if ( ! $this->private_response && ! headers_sent() ) {
nocache_headers();
$this->private_response = true;
}
return $this->private_response;
}
public function assets(): void {
foreach ( $this->rules() as $rule ) {
if ( ! empty( $rule['enabled'] ) && ( ! empty( $rule['show_single'] ) || ! empty( $rule['show_archive'] ) ) ) {
wp_enqueue_style( 'digidiscount-store', plugins_url( 'assets/store.css', DIGIDISCOUNT_FILE ), array(), DIGIDISCOUNT_VERSION );
break;
}
}
}
private function context(): array {
if ( WC()->cart ) {
return Cart::context( WC()->cart );
}
$user = wp_get_current_user();
return apply_filters( 'digidiscount_cart_context', array( 'currency' => get_woocommerce_currency(), 'now' => time(), 'coupons' => array(), 'user_id' => $user->ID, 'roles' => $user->roles, 'gateway' => '' ), null );
}
private function line( $product, int $quantity ): array {
$id = $product->get_parent_id() ?: $product->get_id();
$categories = wc_get_product_term_ids( $id, 'product_cat' );
foreach ( $categories as $category ) {
$categories = array_merge( $categories, get_ancestors( $category, 'product_cat', 'taxonomy' ) );
}
$price = wc_prices_include_tax() ? wc_get_price_including_tax( $product, array( 'qty' => $quantity ) ) : (float) $product->get_price() * $quantity;
return array( 'key' => 'display', 'product_id' => $id, 'variation_id' => $product->is_type( 'variation' ) ? $product->get_id() : 0, 'qty' => $quantity, 'total' => (int) round( $price * 10 ** wc_get_price_decimals() ), 'categories' => array_unique( array_map( 'intval', $categories ) ), 'tags' => wc_get_product_term_ids( $id, 'product_tag' ), 'brands' => taxonomy_exists( 'product_brand' ) ? wc_get_product_term_ids( $id, 'product_brand' ) : array(), 'sale' => $product->is_on_sale(), 'gift' => false );
}
private function eligible( array $rule, array $line, array $context ): bool {
if ( empty( $rule['enabled'] ) || $rule['currency'] !== $context['currency'] || ! Engine::eligible( $line, $rule ) || ( $rule['start'] && $context['now'] < $rule['start'] ) || ( $rule['end'] && $context['now'] >= $rule['end'] ) || ( empty( $rule['allow_coupons'] ) && $context['coupons'] ) ) {
return false;
}
$relaxed = apply_filters( 'digidiscount_display_rule', $rule );
return (bool) apply_filters( 'digidiscount_rule_matches', true, $relaxed, array( 'display' => $line ), $context );
}
private function quotable( array $rule ): bool {
$quotable = ! $rule['min_total'] && ! $rule['max_discount'] && 'fixed' !== $rule['kind'] && in_array( $rule['type'], array( 'product', 'category', 'cart', 'threshold' ), true );
return (bool) apply_filters( 'digidiscount_display_quotable', $quotable, $rule );
}
private function quantity( array $rule ): int {
$quantity = max( 1, (int) ceil( $rule['min_qty'] ) );
return max( 1, (int) apply_filters( 'digidiscount_display_quantity', $quantity, $rule ) );
}
public function quote( $product, string $place, ?array $rules = null ): array {
if ( ! $product->is_type( array( 'simple', 'variation' ) ) || '' === $product->get_price() || ! wc_coupons_enabled() ) {
return array();
}
$rules = $rules ?? $this->rules();
$context = $this->context();
foreach ( $context['coupons'] as $code ) {
if ( ( new \WC_Coupon( $code ) )->get_individual_use() ) {
return array();
}
}
$candidates = array();
$fallback = array();
$index = array_column( $rules, null, 'id' );
foreach ( $rules as $rule ) {
if ( empty( $rule[ 'show_' . $place ] ) ) {
continue;
}
$scenarios = apply_filters( 'digidiscount_display_scenarios', array( $context ), $rule, $context );
$quantity = $this->quantity( $rule );
if ( $quantity > 10000 || ( $product->is_sold_individually() && $quantity > 1 ) ) {
continue;
}
$line = $this->line( $product, $quantity );
foreach ( $scenarios as $scenario ) {
if ( ! $this->eligible( $rule, $line, $scenario ) ) {
continue;
}
$conditions = array();
if ( $quantity > 1 ) {
/* translators: %d: Number of eligible product units. */
$conditions[] = sprintf( __( 'Price for %d items', 'digidiscount' ), $quantity );
}
if ( $rule['max_qty'] ) {
/* translators: %d: Maximum qualifying item quantity. */
$conditions[] = sprintf( __( 'Up to %d items', 'digidiscount' ), (int) $rule['max_qty'] );
}
$conditions = apply_filters( 'digidiscount_display_conditions', $conditions, $scenario, array( $rule ) );
if ( ! $this->quotable( $rule ) ) {
$fallback[] = array( 'kind' => 'conditions', 'name' => $rule['name'], 'conditions' => array_merge( $conditions, $this->requirements( $rule ) ), 'message' => $rule['message'], 'priority' => $rule['priority'] );
continue;
}
$result = Engine::calculate( array( 'display' => $line ), $rules, $scenario, get_option( 'digidiscount_mode', 'best' ) );
$discount = $result['discounts']['display'] ?? 0;
if ( ! $discount || ! $line['total'] ) {
continue;
}
$visible = true;
$messages = array();
$applied_rules = array();
$maximum = 0;
foreach ( $result['rules'] as $entry ) {
$applied = $index[ $entry['id'] ];
$visible = $visible && ! empty( $applied[ 'show_' . $place ] ) && $this->quotable( $applied );
$applied_rules[] = $applied;
if ( $applied['max_qty'] ) {
$maximum = $maximum ? min( $maximum, (int) $applied['max_qty'] ) : (int) $applied['max_qty'];
}
if ( $applied['message'] ) {
$messages[] = $applied['message'];
}
}
if ( ! $visible ) {
continue;
}
$conditions = array();
if ( $quantity > 1 ) {
/* translators: %d: Number of eligible product units. */
$conditions[] = sprintf( __( 'Price for %d items', 'digidiscount' ), $quantity );
}
if ( $maximum ) {
/* translators: %d: Maximum qualifying item quantity. */
$conditions[] = sprintf( __( 'Up to %d items', 'digidiscount' ), $maximum );
}
$conditions = apply_filters( 'digidiscount_display_conditions', $conditions, $scenario, $applied_rules );
$old = wc_get_price_to_display( $product, array( 'qty' => $quantity ) );
$new = max( 0, $old * ( 1 - $discount / $line['total'] ) );
$candidates[] = array( 'kind' => 'price', 'old' => $old, 'new' => $new, 'qty' => $quantity, 'percent' => round( $discount / $line['total'] * 100, 1 ), 'name' => implode( ' + ', array_column( $result['rules'], 'name' ) ), 'conditions' => $conditions, 'message' => implode( ' ', array_unique( $messages ) ), 'conditional' => (bool) $conditions );
}
}
if ( $candidates ) {
usort( $candidates, static fn( $a, $b ) => ( $a['new'] / $a['qty'] <=> $b['new'] / $b['qty'] ) ?: ( $a['qty'] <=> $b['qty'] ) );
return $candidates[0];
}
if ( $fallback ) {
usort( $fallback, static fn( $a, $b ) => $a['priority'] <=> $b['priority'] );
return $fallback[0];
}
return array();
}
private function requirements( array $rule ): array {
$conditions = array();
$money = static fn( $value ) => html_entity_decode( wp_strip_all_tags( wc_price( $value / 10 ** wc_get_price_decimals() ) ), ENT_QUOTES, 'UTF-8' );
if ( $rule['min_total'] ) {
/* translators: %s: Formatted minimum purchase amount, including currency. */
$conditions[] = sprintf( __( 'Minimum purchase: %s', 'digidiscount' ), $money( $rule['min_total'] ) );
}
if ( $rule['max_discount'] ) {
/* translators: %s: Formatted maximum discount, including currency. */
$conditions[] = sprintf( __( 'Maximum saving per order: %s', 'digidiscount' ), $money( $rule['max_discount'] ) );
}
$offer = array();
if ( 'percent' === $rule['kind'] ) {
/* translators: %s: Localized discount percentage, without the percent sign. */
$offer[] = sprintf( __( 'Save %s%% on eligible items.', 'digidiscount' ), wc_format_localized_decimal( $rule['amount'] ) );
} else {
/* translators: %s: Formatted campaign discount, including currency. */
$offer[] = sprintf( __( 'Campaign saving: %s. Final allocation is calculated in your cart.', 'digidiscount' ), $money( $rule['amount'] ) );
}
$conditions = array_merge( $conditions, apply_filters( 'digidiscount_display_offer', $offer, $rule ) );
return apply_filters( 'digidiscount_display_requirements', $conditions, $rule );
}
public function markup( array $quote ): string {
if ( ! $quote ) {
return '';
}
$html = '' . esc_html( $quote['name'] ) . '';
if ( 'price' === $quote['kind'] && $quote['percent'] > 0 ) {
$html .= '−' . esc_html( wc_format_localized_decimal( $quote['percent'] ) ) . '%';
}
$html .= '';
if ( 'price' === $quote['kind'] ) {
$old = $quote['old_html'] ?? wc_price( $quote['old'] );
$new = $quote['new_html'] ?? wc_price( $quote['new'] );
$old_label = __( 'Price before this campaign', 'digidiscount' ) . ': ' . html_entity_decode( wp_strip_all_tags( $old ), ENT_QUOTES, 'UTF-8' );
$new_label = __( 'Campaign price', 'digidiscount' ) . ': ' . html_entity_decode( wp_strip_all_tags( $new ), ENT_QUOTES, 'UTF-8' );
$html .= '' . esc_html__( 'Campaign price', 'digidiscount' ) . '' . wp_kses_post( $old ) . '' . wp_kses_post( $new ) . '';
}
foreach ( $quote['conditions'] as $condition ) {
$html .= '' . esc_html( $condition ) . '';
}
if ( $quote['message'] ) {
$html .= '' . esc_html( $quote['message'] ) . '';
}
$html .= '';
return $html;
}
public function render( string $html, $product, string $place ): string {
if ( $product->is_type( 'variable' ) ) {
$children = $product->get_visible_children();
$old = array(); $new = array(); $names = array(); $any = false; $conditional = false;
foreach ( array_slice( $children, 0, 100 ) as $id ) {
$variation = wc_get_product( $id );
if ( ! $variation || '' === $variation->get_price() || ! $variation->variation_is_visible() || ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && ! $variation->is_in_stock() ) ) {
continue;
}
$price = wc_get_price_to_display( $variation );
$quote = $this->quote( $variation, $place );
$old[] = $price;
$new[] = $quote && 'price' === $quote['kind'] && 1 === $quote['qty'] ? $quote['new'] : $price;
if ( $quote ) {
$any = true;
$names[] = $quote['name'];
$conditional = $conditional || 'price' !== $quote['kind'] || ! empty( $quote['conditional'] );
}
}
if ( ! $any || ! $old ) {
return $html;
}
$choice = __( 'Choose an option to see its campaign price and conditions.', 'digidiscount' );
if ( $conditional || count( $children ) > 100 || ( min( $old ) === min( $new ) && max( $old ) === max( $new ) ) ) {
return $html . $this->markup( array( 'kind' => 'conditions', 'name' => __( 'Campaign available', 'digidiscount' ), 'conditions' => array( $choice ), 'message' => '' ) );
}
$range = static fn( $values ) => min( $values ) === max( $values ) ? wc_price( min( $values ) ) : wc_format_price_range( min( $values ), max( $values ) );
return $this->markup( array( 'kind' => 'price', 'name' => count( array_unique( $names ) ) === 1 ? $names[0] : __( 'Campaign prices', 'digidiscount' ), 'old_html' => $range( $old ), 'new_html' => $range( $new ), 'percent' => 0, 'conditions' => array( $choice ), 'message' => '' ) ) . $product->get_price_suffix();
}
$quote = $this->quote( $product, $place );
if ( ! $quote ) {
return $html;
}
$suffix = 'price' === $quote['kind'] ? $product->get_price_suffix( (float) $product->get_price() * $quote['new'] / $quote['old'], $quote['qty'] ) : '';
return ( 'price' === $quote['kind'] ? '' : $html ) . $this->markup( $quote ) . $suffix;
}
public function price_html( $html, $product ): string {
if ( $this->rendering || is_admin() || wp_doing_cron() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) || is_cart() || is_checkout() || ! $html ) {
return $html;
}
$place = is_product() && ( $product->get_parent_id() ?: $product->get_id() ) === get_queried_object_id() ? 'single' : 'archive';
$this->rendering = true;
try {
$rendered = $this->render( $html, $product, $place );
if ( $rendered !== $html && ! $this->private_response() ) {
return $html;
}
return $rendered;
} finally {
$this->rendering = false;
}
}
public function variation( $data, $parent, $variation ): array {
if ( ( is_admin() && ! wp_doing_ajax() ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
return $data;
}
$this->rendering = true;
try {
$base = $variation->get_price_html();
if ( '' === $base ) {
$data['price_html'] = '';
return $data;
}
$html = $this->render( $base, $variation, 'single' );
if ( $html !== $base && $this->private_response() ) {
$data['price_html'] = '' . $html . '';
} elseif ( str_contains( $data['price_html'] ?? '', 'dd-campaign-price' ) ) {
$data['price_html'] = '' . $base . '';
}
} finally {
$this->rendering = false;
}
return $data;
}
}