$features[ $product_id % count( $features ) ], 'collection' => $collections[ $product_id % count( $collections ) ], ); } /** * Apply the visible demo facets to the main product query so filtering, * sorting and pagination all operate on the same result set. * * @param WP_Query $query Main archive query. */ function vyatka_shop_filter_product_archive( WP_Query $query ): void { static $collecting_products = false; if ( $collecting_products || is_admin() || ! $query->is_main_query() || ( ! $query->is_post_type_archive( 'product' ) && ! $query->is_tax( 'product_cat' ) ) ) { return; } $selected_features = isset( $_GET['feature'] ) ? array_values( array_diff( array_map( 'sanitize_key', (array) wp_unslash( $_GET['feature'] ) ), array( 'all', '' ) ) ) : array(); $selected_collection = isset( $_GET['collection'] ) ? sanitize_key( wp_unslash( $_GET['collection'] ) ) : ''; $price_active = isset( $_GET['filter_min_price'], $_GET['filter_max_price'] ); if ( ! $selected_features && ! $selected_collection && ! $price_active ) { return; } $minimum_price = $price_active ? max( 0, absint( wp_unslash( $_GET['filter_min_price'] ) ) ) : 0; $maximum_price = $price_active ? max( $minimum_price, absint( wp_unslash( $_GET['filter_max_price'] ) ) ) : PHP_INT_MAX; $collecting_products = true; $product_ids = get_posts( array( 'post_type' => 'product', 'post_status' => 'publish', 'fields' => 'ids', 'posts_per_page' => -1, 'no_found_rows' => true, 'suppress_filters' => true, ) ); $collecting_products = false; if ( $query->is_tax( 'product_cat' ) ) { $term = get_queried_object(); if ( $term instanceof WP_Term ) { $product_ids = array_values( array_filter( $product_ids, static fn( int $product_id ): bool => has_term( $term->term_id, 'product_cat', $product_id ) ) ); } } $matching_ids = array_values( array_filter( $product_ids, static function ( int $product_id ) use ( $selected_features, $selected_collection, $price_active, $minimum_price, $maximum_price ): bool { $facets = vyatka_shop_product_facets( $product_id ); $price = (float) get_post_meta( $product_id, '_price', true ); return ( ! $selected_features || in_array( $facets['feature'], $selected_features, true ) ) && ( ! $selected_collection || $facets['collection'] === $selected_collection ) && ( ! $price_active || ( $price >= $minimum_price && $price <= $maximum_price ) ); } ) ); $query->set( 'post__in', $matching_ids ?: array( 0 ) ); $query->set( 'paged', max( 1, absint( $query->get( 'paged' ) ) ) ); } add_action( 'pre_get_posts', 'vyatka_shop_filter_product_archive', 25 ); /** * Use the wording shown in the design for the default product order. * * @param array $options Ordering labels. * @return array */ function vyatka_shop_catalog_orderby( array $options ): array { if ( isset( $options['menu_order'] ) ) { $options['menu_order'] = __( 'По умолчанию', 'vyatka-shop' ); } return $options; } add_filter( 'woocommerce_catalog_orderby', 'vyatka_shop_catalog_orderby' ); /** * Adding a product is handled in place by the theme's AJAX control. Keeping * WooCommerce's redirect option disabled also prevents it from queueing a * second success message for the next page load. */ function vyatka_shop_disable_add_to_cart_redirect(): string { return 'no'; } add_filter( 'pre_option_woocommerce_cart_redirect_after_add', 'vyatka_shop_disable_add_to_cart_redirect' ); /** * Success feedback is rendered by the client-side toast. Remove legacy * success notices left in the WooCommerce session before any frontend * template can print them into the document flow. */ function vyatka_shop_clear_legacy_success_notices(): void { if ( is_admin() || wp_doing_ajax() || ! function_exists( 'wc_get_notices' ) || ! function_exists( 'wc_set_notices' ) ) { return; } $notices = wc_get_notices(); if ( ! isset( $notices['success'] ) ) { return; } unset( $notices['success'] ); wc_set_notices( $notices ); } add_action( 'wp', 'vyatka_shop_clear_legacy_success_notices', 20 ); /** * Calculate regular, current and sale-discount totals from one shared source. * * @param WC_Cart|null $cart Cart to summarize. Defaults to the active cart. * @return array{regular_total: float, current_total: float, discount_total: float} */ function vyatka_shop_cart_price_summary( ?WC_Cart $cart = null ): array { $cart = $cart ?? WC()->cart; $regular_total = 0.0; $current_total = 0.0; if ( ! $cart ) { return array( 'regular_total' => 0.0, 'current_total' => 0.0, 'discount_total' => 0.0, ); } foreach ( $cart->get_cart() as $cart_item ) { $product = $cart_item['data'] ?? null; $quantity = isset( $cart_item['quantity'] ) ? max( 0, (int) $cart_item['quantity'] ) : 0; if ( ! $product instanceof WC_Product || ! $product->exists() || 0 === $quantity ) { continue; } $current_price = max( 0.0, (float) $product->get_price() ); $regular_price = (float) $product->get_regular_price(); if ( $regular_price <= 0.0 ) { $regular_price = $current_price; } $regular_total += $regular_price * $quantity; $current_total += $current_price * $quantity; } return array( 'regular_total' => $regular_total, 'current_total' => $current_total, 'discount_total' => max( 0.0, $regular_total - $current_total ), ); } /** * Render the current cart state for an AJAX response. */ function vyatka_shop_render_cart_fragment(): string { ob_start(); if ( WC()->cart->is_empty() ) { wc_get_template( 'cart/cart-empty.php' ); } else { wc_get_template( 'cart/cart.php' ); } $html = (string) ob_get_clean(); return vyatka_shop_relative_frontend_urls( $html ); } /** * Mutate the WooCommerce cart without navigating away from the current page. */ function vyatka_shop_ajax_cart_mutation(): void { check_ajax_referer( 'vyatka-cart', 'nonce' ); if ( ! function_exists( 'WC' ) ) { wp_send_json_error( array( 'message' => __( 'Корзина недоступна.', 'vyatka-shop' ) ), 503 ); } if ( null === WC()->cart && function_exists( 'wc_load_cart' ) ) { wc_load_cart(); } if ( null === WC()->cart ) { wp_send_json_error( array( 'message' => __( 'Корзина недоступна.', 'vyatka-shop' ) ), 503 ); } $operation = isset( $_POST['operation'] ) ? sanitize_key( wp_unslash( $_POST['operation'] ) ) : ''; if ( 'add' === $operation ) { $product_id = isset( $_POST['product_id'] ) ? absint( wp_unslash( $_POST['product_id'] ) ) : 0; $quantity = isset( $_POST['quantity'] ) ? max( 1, absint( wp_unslash( $_POST['quantity'] ) ) ) : 1; $product = wc_get_product( $product_id ); if ( ! $product || ! $product->is_purchasable() || ! $product->is_in_stock() ) { wp_send_json_error( array( 'message' => __( 'Товар нельзя добавить в корзину.', 'vyatka-shop' ) ), 400 ); } $maximum = $product->get_max_purchase_quantity(); if ( 0 < $maximum ) { $quantity = min( $quantity, $maximum ); } $existing_key = ''; foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { if ( $product_id === (int) $cart_item['product_id'] && empty( $cart_item['variation_id'] ) ) { $existing_key = $cart_item_key; break; } } $changed = '' !== $existing_key ? WC()->cart->set_quantity( $existing_key, $quantity, false ) : WC()->cart->add_to_cart( $product_id, $quantity ); if ( false === $changed ) { wp_send_json_error( array( 'message' => __( 'Не удалось добавить товар в корзину.', 'vyatka-shop' ) ), 400 ); } } elseif ( 'update' === $operation ) { $cart_item_key = isset( $_POST['cart_item_key'] ) ? wc_clean( wp_unslash( $_POST['cart_item_key'] ) ) : ''; $quantity = isset( $_POST['quantity'] ) ? max( 1, absint( wp_unslash( $_POST['quantity'] ) ) ) : 1; $cart_item = WC()->cart->get_cart_item( $cart_item_key ); if ( ! $cart_item ) { wp_send_json_error( array( 'message' => __( 'Товар в корзине не найден.', 'vyatka-shop' ) ), 404 ); } $product = $cart_item['data']; $maximum = $product instanceof WC_Product ? $product->get_max_purchase_quantity() : -1; if ( 0 < $maximum ) { $quantity = min( $quantity, $maximum ); } if ( false === WC()->cart->set_quantity( $cart_item_key, $quantity, false ) ) { wp_send_json_error( array( 'message' => __( 'Не удалось изменить количество.', 'vyatka-shop' ) ), 400 ); } } elseif ( 'remove' === $operation ) { $cart_item_key = isset( $_POST['cart_item_key'] ) ? wc_clean( wp_unslash( $_POST['cart_item_key'] ) ) : ''; if ( '' === $cart_item_key || false === WC()->cart->remove_cart_item( $cart_item_key ) ) { wp_send_json_error( array( 'message' => __( 'Не удалось удалить товар.', 'vyatka-shop' ) ), 400 ); } } else { wp_send_json_error( array( 'message' => __( 'Неизвестная операция с корзиной.', 'vyatka-shop' ) ), 400 ); } WC()->cart->calculate_totals(); WC()->cart->set_session(); wp_send_json_success( array( 'cartCount' => WC()->cart->get_cart_contents_count(), 'html' => vyatka_shop_render_cart_fragment(), ) ); } add_action( 'wp_ajax_vyatka_cart_mutation', 'vyatka_shop_ajax_cart_mutation' ); add_action( 'wp_ajax_nopriv_vyatka_cart_mutation', 'vyatka_shop_ajax_cart_mutation' ); /** * Keep only the three customer fields present in the checkout design. * * @param array>> $fields Checkout fields. * @return array>> */ function vyatka_shop_checkout_fields( array $fields ): array { // WooCommerce prefixes billing labels with "Выставление счёта", which turns // a validation message into "Выставление счёта Имя — обязательное поле". // The design labels these fields plainly, and the shorter label is also // what the inline field errors match on. $labels = array( 'billing_first_name' => __( 'Имя', 'vyatka-shop' ), 'billing_phone' => __( 'Телефон', 'vyatka-shop' ), 'billing_email' => __( 'Email', 'vyatka-shop' ), ); $billing = array(); foreach ( $labels as $key => $label ) { if ( isset( $fields['billing'][ $key ] ) ) { $billing[ $key ] = $fields['billing'][ $key ]; $billing[ $key ]['required'] = true; $billing[ $key ]['label'] = $label; } } $fields['billing'] = $billing; $fields['shipping'] = array(); $fields['order'] = array(); return $fields; } add_filter( 'woocommerce_checkout_fields', 'vyatka_shop_checkout_fields' ); /** * Validate the delivery-only address field exposed by the checkout switch. */ function vyatka_shop_validate_delivery_fields(): void { $delivery_type = isset( $_POST['delivery_type'] ) ? sanitize_key( wp_unslash( $_POST['delivery_type'] ) ) : 'pickup'; $address = isset( $_POST['delivery_address'] ) ? sanitize_text_field( wp_unslash( $_POST['delivery_address'] ) ) : ''; if ( 'delivery' === $delivery_type && '' === $address ) { wc_add_notice( __( 'Укажите адрес доставки.', 'vyatka-shop' ), 'error' ); } } add_action( 'woocommerce_checkout_process', 'vyatka_shop_validate_delivery_fields' ); /** * Store custom delivery controls with the order. * * @param WC_Order $order Order being created. */ function vyatka_shop_save_delivery_fields( WC_Order $order ): void { $fields = array( 'delivery_type', 'pickup_address', 'delivery_address', 'delivery_unit' ); foreach ( $fields as $field ) { if ( isset( $_POST[ $field ] ) ) { $order->update_meta_data( '_' . $field, sanitize_text_field( wp_unslash( $_POST[ $field ] ) ) ); } } } add_action( 'woocommerce_checkout_create_order', 'vyatka_shop_save_delivery_fields' );