Перейти до вмісту
🛒 Дублі замовлень у WooCommerce: 3 скрипти для блокування на рівні кошика

🛒 Дублі замовлень у WooCommerce: 3 скрипти для блокування на рівні кошика

Клієнт натиснув «Оформити замовлення» двічі, і ось уже в адмінці два однакових замовлення з різницею в пару секунд. Гроші списано двічі, клієнт у люті, а ви розбираєте refund.

Дублі замовлень, не гіпотетична ситуація. З ними стикається приблизно кожен третій магазин на WooCommerce, особливо за нестабільного хостингу або повільних платіжних шлюзів. Але навіть на швидкому сервері користувач може додати товар через URL повторно або купити безплатний цифровий продукт десятки разів.

Ми зібрали три робочих скрипти, які закривають різні сценарії дублювання: від примусового продажу по одній одиниці до блокування повторних URL-додавань. Нижче, повний розбір із кодом.

Чому дублюються замовлення в WooCommerce

💡 Швидкий огляд:

  • Дізнаєтеся, чому замовлення дублюються на рівні кошика, URL і повторних покупок
  • Поставите скрипт для примусового продажу товару тільки по одній одиниці через мета-поля
  • Забороните повторну купівлю безплатних варіацій (цифрові товари, купони)
  • Приберете дублювання через URL з add-to-cart під час оновлення сторінки
  • Перевірите захист і дізнаєтеся, що ще може спричиняти дублі на рівні сервера

Дублі замовлень виникають за трьома основними сценаріями.

Перший, користувач кілька разів клацає «Оформити замовлення». Таке буває, коли сторінка гальмує, а людина не бачить реакції та тисне кнопку знову. На повільному хостингу, регулярна історія.

Другий сценарій, безплатні товари. Якщо у вас є безплатний цифровий продукт, ніщо не заважає одному користувачеві «купити» його десять разів поспіль. WooCommerce з коробки не відстежує, чи брав цей клієнт такий самий безплатний товар раніше.

Третій, пряме посилання з параметром ?add-to-cart=123. Оновив сторінку кошика, товар додався знову. Перейшов за посиланням із розсилки двічі, у кошику два екземпляри.

Усі три сценарії вирішуються на стороні сервера через хуки WooCommerce. Жодних плагінів, тільки код.

Спосіб 1: Примусовий продаж по одній одиниці через мета-поля

Перший скрипт змушує WooCommerce вважати конкретний товар «продаваним поштучно», навіть якщо в налаштуваннях товару цей прапорець не стоїть. Керування, через мета-поле.

Коли meta-поле maximum_allowed_quantity дорівнює 1, фільтр woocommerce_is_sold_individually повертає true, і WooCommerce сам блокує додавання другої одиниці в кошик. Працює і для простих товарів, і для варіацій.

Додайте код у functions.php дочірньої теми або через плагін Code Snippets:

1function sd_woocommerce_is_sold_individually( $return, $product ) {
2 switch ( $product->get_type() ) {
3 case 'variation':
4 if ( $product->get_meta( 'variation_maximum_allowed_quantity' ) == 1 ) {
5 return true;
6 }
7 break;
8 case 'simple':
9 if ( $product->get_meta( 'maximum_allowed_quantity' ) == 1 ) {
10 return true;
11 }
12 break;
13 }
14 return $return;
15}
16add_filter( 'woocommerce_is_sold_individually', 'sd_woocommerce_is_sold_individually', 10, 2 );

Що тут відбувається: хук woocommerce_is_sold_individually спрацьовує щоразу, коли WooCommerce перевіряє, чи можна додати товар у кошик більше одного разу. Функція зазирає в meta-поля товару і, якщо бачить maximum_allowed_quantity = 1, примусово вмикає режим «Sold Individually». Для варіацій поле називається variation_maximum_allowed_quantity.

Як застосувати. Зайдіть у редагування будь-якого товару або варіації, додайте custom field з іменем maximum_allowed_quantity (або variation_maximum_allowed_quantity для варіації) і значенням 1. Збережіть. Усе, тепер цей товар не можна покласти в кошик двічі.

Спосіб 2: Заборона повторної купівлі безплатних варіацій

Безплатні цифрові товари, головна прогалина в захисті від дублів. Користувач авторизований, ціна товару дорівнює нулю, WooCommerce пропускає замовлення без питань. Десять таких замовлень від одного клієнта, десять однакових записів в адмінці.

Скрипт нижче перевіряє через wc_customer_bought_product(), чи купував цей користувач такий самий товар раніше. Якщо так, додавання в кошик блокується зі зрозумілим повідомленням.

1function sd_disable_repeat_purchase_for_free_products( $passed, $product_id, $quantity, $variation_id = 0 ) {
2 $current_user = wp_get_current_user();
3 $product = $variation_id > 0 ? wc_get_product( $variation_id ) : wc_get_product( $product_id );
4 $target_id = $variation_id > 0 ? $variation_id : $product_id;
5
6 if ( floatval( $product->get_price() ) == 0 ) {
7 if ( wc_customer_bought_product( $current_user->user_email, $current_user->ID, $target_id ) ) {
8 wc_add_notice(
9 sprintf( __( 'You have already purchased "%s" and cannot purchase it again.', 'sd' ), $product->get_name() ),
10 'error'
11 );
12 return false;
13 }
14 }
15 return $passed;
16}
17add_filter( 'woocommerce_add_to_cart_validation', 'sd_disable_repeat_purchase_for_free_products', 10, 4 );

Після встановлення користувач бачить повідомлення про помилку під час спроби додати в кошик уже куплений безплатний товар. Саме замовлення не створюється.

Дві важливі умови. Перша, клієнт має бути авторизований. Для гостей wp_get_current_user() повертає порожній об'єкт, і перевірка не спрацює. Друга, ціна товару має бути рівно нуль (floatval виключає проблеми з форматуванням). Товари з ціною вище нуля скрипт не зачепить.

Спосіб 3: Захист від дублювання через URL з add-to-cart

WooCommerce дозволяє додати товар у кошик за прямим посиланням: /cart/?add-to-cart=123. Зручно для маркетингу, але якщо користувач оновить сторінку або перейде за посиланням повторно, товар додасться знову.

Цей скрипт перехоплює завантаження сторінки кошика або чекауту і, якщо товар із URL-параметра вже лежить у кошику, мовчки прибирає query-параметри та редіректить на чистий URL.

1function sd_prevent_duplicate_add_to_cart_redirect() {
2 if ( ( is_cart() || is_checkout() ) && isset( $_GET['add-to-cart'] ) ) {
3 $current_url = home_url( add_query_arg( null, null ) );
4 $product_id = intval( $_GET['add-to-cart'] );
5 $variation_id = isset( $_GET['variation_id'] ) ? intval( $_GET['variation_id'] ) : 0;
6
7 foreach ( WC()->cart->get_cart() as $cart_item ) {
8 if ( $cart_item['product_id'] == $product_id
9 && ( $variation_id == 0 || $cart_item['variation_id'] == $variation_id ) ) {
10 wp_redirect( remove_query_arg( array( 'add-to-cart', 'variation_id' ), $current_url ) );
11 exit;
12 }
13 }
14 }
15}
16add_action( 'template_redirect', 'sd_prevent_duplicate_add_to_cart_redirect' );

Хук template_redirect спрацьовує до того, як WordPress почне рендерити сторінку. Скрипт перевіряє, чи є в URL параметр add-to-cart, шукає цей товар у вмісті кошика (WC()->cart->get_cart()), і якщо знаходить, робить wp_redirect на ту саму сторінку без query-параметрів.

Повторне додавання через URL виключене. При цьому перше додавання (коли товару в кошику ще немає) працює як зазвичай.

Збираємо все разом

Усі три скрипти не конфліктують і працюють незалежно:

  • Перший обмежує кількість одиниць одного товару до 1, захист від множинного додавання.
  • Другий блокує повторну купівлю безплатних продуктів, захист від «наклікування» халяви.
  • Третій прибирає дублювання через URL, захист від випадкових повторних додавань.

Додавайте код через плагін Code Snippets на WP.org, а не прямо у functions.php теми. Плагін дозволяє вмикати та вимикати сніпети по одному, не ламаючи сайт у разі помилки, і не втрачає код під час зміни теми. Створіть три окремих сніпети, так простіше налагоджувати.

Що ще перевірити, якщо дублі зберігаються

Скрипти вище закривають дублі на рівні кошика та оформлення. Але є ще два джерела проблеми, які кодом не виправити.

Кешування сторінок кошика та чекауту. Плагіни кешування, як-от WP Rocket або LiteSpeed Cache, за замовчуванням виключають ці сторінки, але після оновлення налаштувань exclusion може скинутися. Перевірте: cart, checkout і my-account не повинні кешуватися ні на рівні плагіна, ні на рівні сервера, ні в CDN.

Повтори з боку платіжного шлюзу. Якщо шлюз (Stripe, PayPal) не отримує від сервера підтвердження вчасно, він надсилає сповіщення повторно, і WooCommerce створює друге замовлення. Причина зазвичай у повільному хостингу або firewall, що блокує callback-запити. Рішення: перевірте логи шлюзу на предмет повторних webhook-відправок і переконайтеся, що callback-URL доступний ззовні.

Як протестувати захист

Після встановлення скриптів обов'язково перевірте кожен сценарій на тестовому сайті.

Спосіб 1, створіть товар із meta-полем maximum_allowed_quantity = 1 і спробуйте додати дві одиниці в кошик. Має додатися тільки одна.

Спосіб 2, авторизуйтеся, купіть безплатний товар, потім спробуйте купити його знову. Маєте побачити повідомлення про помилку.

Спосіб 3, додайте товар у кошик через пряме посилання з ?add-to-cart=ID, потім оновіть сторінку. Кількість позицій у кошику не повинна змінитися.

Тестуйте в окремій інсталяції або на staging-сайті. На живому магазині, тільки після повної перевірки.

⁉️🤔 Часті питання

Чому дублюються замовлення, якщо кнопку «Оформити» натиснуто один раз?

Причина майже завжди в повільній відповіді сервера. Користувач натискає кнопку, запит іде, але сторінка не оновлюється кілька секунд. Людина думає, що не натиснула, і клацає знову. Обидва запити доходять до WooCommerce, і створюються два замовлення. Скрипти з цієї статті таку ситуацію не ловлять, тут допомагає кешування на стороні клієнта (disable button after click) або серверна перевірка унікальності замовлення за nonce-токеном.

Чи може платіжний шлюз створювати дублі сам по собі?

Так. Якщо шлюз не отримує від вашого сервера HTTP 200 у відповідь на IPN/webhook, він повторює відправку. За кожного повтору WooCommerce може створити нове замовлення. Проблема вирішується прискоренням хостингу та перевіркою, що callback-URL шлюзу не заблокований файрволом і не потребує HTTP-авторизації.

Чи блокує цей захист справжні повторні замовлення від клієнта?

Ні. Скрипти відпрацьовують на етапі додавання в кошик, а не на етапі створення замовлення. Якщо клієнт усвідомлено оформив одне замовлення, а через тиждень, друге, скрипти не втрутяться. Виняток, спосіб 2, який забороняє повторну купівлю безплатного товару тим самим користувачем.

Чи обов'язково використовувати Code Snippets, чи можна у functions.php?

Можна і в functions.php, але ми радимо Code Snippets. У разі одруку в коді functions.php ви отримаєте білий екран і доступ тільки через FTP. Плагін Code Snippets ловить фатальні помилки й автоматично вимикає проблемний сніпет, сайт залишається на ногах.

Чи допомагають плагіни для запобігання дублям?

Спеціалізовані плагіни, як-от WooCommerce Duplicate Order Blocker, працюють на іншому рівні, вони відстежують дублі за IP, email або відбитком браузера на етапі оформлення. Це закриває сценарій подвійного кліку по кнопці, який наші скрипти не покривають. При цьому плагіни не вирішують проблему дублів через add-to-cart URL і повторних покупок безплатного товару. Ідеальний варіант, скрипти з цієї статті плюс плагін-блокувальник на чекауті.

Підсумки: три рівні захисту від дублів

Три скрипти закривають три конкретних сценарії дублювання замовлень. Поставте всі одразу, кожен відповідає за свою ділянку, і разом вони дають щільний захист на рівні кошика.

Якщо після встановлення дублі все ще з'являються, дивіться в бік хостингу і платіжного шлюзу. Подвійний клік по кнопці «Оформити» кодом на рівні кошика не лікується, тут потрібен або плагін-блокувальник, або кастомний захист кнопки через JavaScript.

Перевірте захист на тестовому сайті та переносьте на бойовий. П'ятнадцять хвилин на встановлення трьох сніпетів заощаджують години розбору refund-ів.