Koszty wysyłki liczą się źle w koszyku – strefy, klasy, wagi – analiza
To nie jest błąd, który zgłasza się sam. Klient nie pisze maila z pytaniem, dlaczego wysyłka kosztuje tyle co produkt. On po prostu zamyka kartę. Dlatego źle policzona wysyłka potrafi cicho zjadać konwersję przez wiele miesięcy, a jedynym śladem jest rosnąca liczba porzuconych koszyków.
Dobra wiadomość jest taka, że WooCommerce liczy wysyłkę w sposób całkowicie deterministyczny. Jeśli znasz kolejność, w jakiej podejmuje decyzje, znalezienie przyczyny zajmuje kilkanaście minut zamiast całego popołudnia zgadywania.
Jak WooCommerce naprawdę liczy wysyłkę
Zanim zaczniesz cokolwiek klikać, warto mieć w głowie właściwy model. Każde przeliczenie koszyka przebiega tak samo:
- WooCommerce buduje paczkę (package) z zawartości koszyka i adresu docelowego klienta.
- Adres jest porównywany ze strefami wysyłki w kolejności ustawionej w panelu, od góry do dołu. Wygrywa pierwsza pasująca strefa, a nie najbardziej szczegółowa.
- Z tej jednej strefy pobierane są wszystkie aktywne metody wysyłki. Metody z pozostałych stref nie mają już żadnego znaczenia.
- Każda metoda wylicza swoją stawkę na podstawie własnych ustawień, klas wysyłki produktów w paczce, wag i wartości koszyka.
- Wynikowa lista stawek przechodzi przez filtr
woocommerce_package_rates, gdzie wtyczki mogą ją dowolnie zmienić, ukryć część opcji albo dopisać własne. - Do stawki doliczany jest podatek według klasy podatkowej wysyłki.
- Gotowy zestaw stawek ląduje w sesji klienta i tam zostaje zapamiętany do czasu zmiany zawartości koszyka lub adresu.
Prawie każdy przypadek błędnie naliczonej wysyłki daje się przypisać do jednego z tych siedmiu kroków. Sztuka polega na tym, żeby sprawdzać je po kolei, a nie zaczynać od punktu piątego, bo ktoś na forum napisał, że to pewnie wtyczka.
Krok 0. Zbuduj powtarzalny przypadek testowy
Bez tego wszystko dalej jest bez sensu. Zanim ruszysz, zapisz sobie cztery rzeczy:
- dokładny koszyk: konkretne produkty, warianty i ilości,
- pełny adres docelowy: kraj, województwo i kod pocztowy w takiej formie, w jakiej wpisuje go klient,
- kwotę oczekiwaną i skąd ona wynika,
- kwotę faktycznie pokazywaną przez sklep.
Sprawdź też, czy błąd występuje na koncie zalogowanym, na koncie wylogowanym, czy w obu przypadkach. Różnica między tymi dwoma scenariuszami od razu zawęża pole poszukiwań do adresu zapisanego w profilu klienta albo do reguł zależnych od roli użytkownika.
Testuj zawsze w oknie prywatnym. Sesja WooCommerce z poprzednich prób potrafi podsuwać stare stawki i skutecznie zamydlić obraz.
Krok 1. Ustal, która strefa się dopięła
To jest zdecydowanie najczęstsza przyczyna i zarazem ta, o której najłatwiej zapomnieć. Strefy nie działają na zasadzie dopasowania najbardziej szczegółowego. WooCommerce idzie listą od góry i bierze pierwszą, która pasuje.
Klasyczny układ, który psuje sklep:
1. Europa (zawiera Polskę)
2. Polska (nigdy nie zostanie użyta)
3. Reszta świata
Klient z Poznania trafia do strefy „Europa”, bo Polska jest jej częścią i ta strefa stoi wyżej. Metody z drugiej strefy nigdy się nie uruchomią. Poprawna kolejność idzie od najbardziej szczegółowej do najbardziej ogólnej: najpierw pojedyncze kody pocztowe, potem województwa, potem kraje, potem kontynenty.
W panelu zmienisz kolejność przeciągając strefy w WooCommerce → Ustawienia → Wysyłka. Jeśli wolisz sprawdzić to z konsoli:
wp wc shipping_zone list --user=1
wp wc shipping_zone_method list 3 --user=1
A jeśli chcesz zobaczyć czarno na białym, którą strefę system wybrał dla konkretnego koszyka, wrzuć tymczasowo taki fragment do pliku functions.php motywu potomnego:
add_action( 'woocommerce_after_cart_table', function () {
if ( ! current_user_can( 'manage_woocommerce' ) ) {
return;
}
foreach ( WC()->cart->get_shipping_packages() as $package ) {
$zone = wc_get_shipping_zone( $package );
echo '<pre>';
printf(
"Strefa: %s (ID %d)\nKraj: %s | Kod: %s\n\n",
esc_html( $zone->get_zone_name() ),
(int) $zone->get_id(),
esc_html( $package['destination']['country'] ),
esc_html( $package['destination']['postcode'] )
);
foreach ( $package['contents'] as $item ) {
$product = $item['data'];
printf(
"%s\n klasa: %s | waga: %s | szt.: %d\n",
esc_html( $product->get_name() ),
esc_html( $product->get_shipping_class() ?: 'BRAK' ),
esc_html( $product->get_weight() ?: '0' ),
(int) $item['quantity']
);
}
echo '</pre>';
}
} );
Ten jeden zrzut daje ci od razu odpowiedź na trzy pytania naraz: która strefa, jakie klasy i jakie wagi. Bardzo często diagnoza kończy się właśnie tutaj.
Uwaga na koszyk blokowy. Hook woocommerce_after_cart_table działa tylko na klasycznym koszyku opartym o shortcode. Jeśli sklep korzysta z bloków, na czas diagnozy podstaw sobie testową stronę z shortcode [woocommerce_cart] albo skorzystaj z loggera opisanego w kroku 9.
Krok 2. Sprawdź kody pocztowe w regułach strefy
Reguły oparte o kody pocztowe to drugie miejsce, w którym rzeczy cicho się psują, i akurat w Polsce mamy tu dodatkową pułapkę.
WooCommerce normalizuje kody pocztowe przed porównaniem. Funkcja wc_normalize_postcode() usuwa spacje i myślniki oraz zamienia znaki na wielkie litery. Kod 60-001 staje się więc wewnętrznie 60001. Jeśli w regule strefy wpisałeś zakres w formie 60-001...61-999, dopasowanie potrafi zadziałać zupełnie inaczej, niż zakładałeś.
Bezpieczna praktyka wygląda tak:
- zakresy zapisuj w formie ciągłej:
60001...61999, - pojedyncze kody także bez myślnika:
61001, - wzorce z gwiazdką traktuj ostrożnie, bo
6*obejmie wszystko od60000do69999, - po każdej zmianie przetestuj trzy kody: pierwszy z zakresu, ostatni z zakresu i jeden tuż poza nim.
Ten ostatni punkt jest ważniejszy niż się wydaje. Reguła, która działa dla środka zakresu, potrafi nie objąć jego krańców i wtedy pojedyncze miasta wypadają do zupełnie innej strefy.
Krok 3. Rozłóż formułę metody wysyłki na czynniki pierwsze
Skoro wiesz już, która strefa się dopina, wejdź w konkretną metodę. Przy „Zryczałtowanej stawce” (flat rate) koszt składa się z kilku niezależnych elementów i całkiem łatwo pomylić się przy ich składaniu.
W polu kosztu dostępne są trzy zmienne:
| Zmienna | Znaczenie |
|---|---|
[qty] | liczba sztuk w paczce |
[cost] | wartość produktów w paczce |
[fee percent="10" min_fee="5" max_fee="30"] | opłata procentowa z widełkami |
Przykład, który wygląda niewinnie, a potrafi zaskoczyć:
15 + ( 5 * [qty] )
Przy koszyku z dwunastoma sztukami taniej drobnicy klient zobaczy 75 zł. Formuła jest formalnie poprawna, tylko nikt nie przetestował jej na dużej ilości.
Sprawdź też, czy w polu nie ma literówki. WooCommerce ewaluuje wyrażenie matematyczne i przy błędnej składni potrafi zwrócić zero zamiast błędu, co daje darmową wysyłkę bez żadnego ostrzeżenia w panelu.
Krok 4. Klasy wysyłki, czyli najczęstsze źródło dziwnych kwot
Tutaj kryje się większość zgłoszeń, które wyglądają na „magiczne”. Kilka rzeczy warto rozumieć dokładnie.
Jak liczony jest koszt klasy
W ustawieniach zryczałtowanej stawki masz pole kosztu podstawowego, osobne pole dla każdej klasy wysyłki, pole „Koszt bez klasy wysyłki” oraz przełącznik typu obliczania. Mechanika jest następująca:
- koszt podstawowy jest doliczany zawsze, niezależnie od klas,
- w trybie per klasa system dodaje koszt każdej znalezionej klasy osobno,
- w trybie per zamówienie dodaje wyłącznie koszt najdroższej znalezionej klasy,
- jeśli pole kosztu danej klasy jest puste, ta klasa jest po prostu pomijana i nie wnosi nic do kwoty.
Ten ostatni punkt odpowiada za sporą część zgłoszeń typu „wysyłka za gabaryt się nie nalicza”. Klasa istnieje, produkt ma ją przypisaną, tylko pole kosztu w konkretnej metodzie zostało puste, bo ktoś dodał klasę później i nie wrócił do ustawień wysyłki.
Odwrotny scenariusz też jest częsty. Koszt podstawowy wynosi 15, koszt klasy „gabaryt” wynosi 74, tryb jest ustawiony na per klasa, a w koszyku są dwa gabaryty z różnych klas. Wynik to 15 plus 74 plus 74, czyli właśnie te nieszczęsne 163 zł, których nikt się nie spodziewał.
Zmienna qty w polu klasy znaczy co innego
W polu kosztu podstawowego [qty] oznacza wszystkie sztuki w paczce. W polu kosztu konkretnej klasy oznacza wyłącznie sztuki należące do tej klasy. To rozróżnienie potrafi zmienić wynik kilkukrotnie, więc po każdej zmianie formuły przetestuj mieszany koszyk.
Warianty mają własne pole klasy
Produkt wariantowy ma klasę wysyłki na poziomie rodzica, ale każda wariacja ma swoje własne pole, domyślnie ustawione na „Taka sama jak nadrzędna”. Jeśli ktoś kiedyś ustawił tam konkretną wartość, wariacja przestaje dziedziczyć i zmiana klasy u rodzica nie robi już nic.
To bardzo częsty efekt uboczny importu z pliku CSV, gdzie kolumna z klasą wysyłki trafia tylko do rekordów rodzica. Sprawdź to zawsze wtedy, gdy „ten sam produkt w innym rozmiarze liczy się inaczej”.
Listę istniejących klas i liczbę przypisanych produktów sprawdzisz szybko:
wp term list product_shipping_class --fields=term_id,name,slug,count
Krok 5. Zweryfikuj wagi i jednostki
Metody oparte o wagę są bezlitosne wobec danych. Dwie rzeczy sprawdź w pierwszej kolejności.
Jednostki. W WooCommerce → Ustawienia → Produkty masz jednostkę wagi i jednostkę wymiarów. Jeśli sklep jest ustawiony na kilogramy, a import wgrał wagi w gramach, produkt o masie 800 g staje się produktem o masie 800 kg. Kurierska wtyczka policzy to bez mrugnięcia okiem.
Puste wagi. Brak wagi to dla WooCommerce zero, a nie błąd. Przy progach wagowych taki produkt zawsze wpada do najtańszego progu, co przy realnie ciężkim towarze oznacza dopłacanie do każdej przesyłki z własnej kieszeni.
Szybki przegląd anomalii bezpośrednio w bazie:
-- Produkty o podejrzanie dużej wadze
SELECT post_id, meta_value AS waga
FROM wp_postmeta
WHERE meta_key = '_weight'
AND meta_value != ''
AND CAST(meta_value AS DECIMAL(10,3)) > 100
ORDER BY CAST(meta_value AS DECIMAL(10,3)) DESC;
-- Produkty publiczne bez wagi
SELECT p.ID, p.post_title
FROM wp_posts p
LEFT JOIN wp_postmeta m
ON m.post_id = p.ID AND m.meta_key = '_weight'
WHERE p.post_type IN ('product','product_variation')
AND p.post_status = 'publish'
AND (m.meta_value IS NULL OR m.meta_value = '');
Prefiks wp_ podmień na ten faktycznie używany w twojej instalacji. Zapytania są tylko odczytujące, ale i tak wykonuj je na kopii albo przez narzędzie z podglądem wyników, zanim zaczniesz cokolwiek poprawiać hurtowo.
Przy wtyczkach kurierskich dochodzi jeszcze waga gabarytowa, liczona z wymiarów paczki. Produkt lekki, ale duży, potrafi być rozliczany według objętości. Jeśli kwota rośnie tylko przy konkretnych produktach, sprawdź ich wymiary, zanim zaczniesz podejrzewać strefy.
Krok 6. Darmowa wysyłka i kupony
Darmowa wysyłka ma własny zestaw warunków, które łatwo źle zinterpretować.
W ustawieniach metody wybierasz wymaganie: ważny kupon, minimalna kwota zamówienia, minimalna kwota lub kupon, albo minimalna kwota i kupon. Pod spodem znajduje się osobny przełącznik decydujący o tym, czy minimalna kwota jest liczona przed rabatem z kuponu, czy po nim. Sklep, w którym próg wynosi 200 zł, a klient z rabatem 20 procent dostaje darmową wysyłkę przy koszyku za 170 zł, ma po prostu odznaczoną tę opcję.
Druga rzecz: darmowa wysyłka nie ukrywa automatycznie pozostałych metod z tej samej strefy. WooCommerce pokaże wszystkie dostępne opcje obok siebie. Jeśli u ciebie znikają, to znaczy, że w motywie albo wtyczce siedzi filtr woocommerce_package_rates, który je usuwa. Taki filtr bywa napisany pod jeden konkretny przypadek i przy okazji ukrywa też opcje, które powinny zostać widoczne.
Warto przy okazji sprawdzić, czy sam próg darmowej wysyłki jest komunikowany w koszyku. To jedna z tych rzeczy, które opisywałem szerzej przy błędach niszczących konwersję, bo niewidoczny próg działa dokładnie tak samo źle jak jego brak.
Krok 7. Sprawdź, czy to nie jest kwestia podatku
Zanim uznasz, że kwota jest zła, upewnij się, że porównujesz to samo. W WooCommerce → Ustawienia → Podatek ustawiasz klasę podatkową wysyłki. Domyślna wartość dziedziczy klasę z produktów w koszyku, więc przy koszyku mieszanym, gdzie część towaru ma 23 procent, a część 5 procent, wynik potrafi zaskoczyć.
Do tego dochodzi ustawienie wyświetlania cen w koszyku: z podatkiem albo bez. Różnica między 15,00 a 18,45 to nie jest błąd konfiguracji wysyłki, tylko zmiana sposobu prezentacji. Zaczynając diagnozę od podatku, oszczędzisz sobie czasem godziny grzebania w strefach.
Krok 8. Wyklucz cache
WooCommerce zapamiętuje wyliczone stawki w sesji klienta. Dopóki koszyk i adres się nie zmienią, przy kolejnych odsłonach nie liczy ich od nowa. To świetnie działa na produkcji i fatalnie podczas diagnozy, bo zmieniasz ustawienie w panelu, odświeżasz koszyk i widzisz starą kwotę.
Na czas pracy dodaj do pliku wp-config.php:
define( 'WOOCOMMERCE_SHIPPING_DEBUG', true );
Ta stała wyłącza cache stawek wysyłki. Po zakończeniu diagnozy koniecznie ją usuń, bo na produkcji zauważalnie obciąża sklep przy każdym przeliczeniu koszyka.
Osobna sprawa to cache stron. Sprawdź w WooCommerce → Ustawienia → Ogólne opcję Domyślna lokalizacja klienta. Jeśli jest ustawiona na geolokalizację bez obsługi cache, a przed sklepem stoi warstwa cache, to pierwszy odwiedzający zamraża swoją lokalizację dla wszystkich kolejnych. Klient z Berlina zobaczy wtedy stawki dla Polski i odwrotnie. Do tego scenariusza służy wariant Geolokalizacja z obsługą cache, a jeśli nie chcesz zgadywać w ogóle, ustaw Brak domyślnej lokalizacji i włącz opcję ukrywania kosztów wysyłki do momentu podania adresu.
Objaw rozpoznasz po tym, że koszyk pokazuje jedną kwotę, a zamówienie inną. To zwykle nie jest błąd wyliczeń, tylko różnica między adresem domyślnym a adresem faktycznie wpisanym przez klienta.
Krok 9. Dopiero teraz wtyczki i filtry
Jeżeli poprzednie osiem kroków niczego nie wyjaśniło, czas zobaczyć, co dzieje się ze stawkami po ich wyliczeniu. Ten logger zapisuje finalną listę stawek już po ingerencji wszystkich wtyczek:
add_filter( 'woocommerce_package_rates', function ( $rates, $package ) {
$zone = wc_get_shipping_zone( $package );
$lines = array();
foreach ( $rates as $id => $rate ) {
$lines[] = sprintf( '%s = %s', $id, $rate->get_cost() );
}
wc_get_logger()->debug(
sprintf(
'Strefa: %s | %s | stawki: %s',
$zone->get_zone_name(),
$package['destination']['postcode'],
$lines ? implode( ', ', $lines ) : 'BRAK'
),
array( 'source' => 'diagnostyka-wysylki' )
);
return $rates;
}, 9999, 2 );
Priorytet 9999 jest tu celowy. Chodzi o to, żeby zobaczyć stan po wszystkich innych filtrach, a nie przed nimi. Wyniki znajdziesz w WooCommerce → Status → Dzienniki, w pliku o źródle diagnostyka-wysylki.
Porównaj to, co widzisz w logu, z tym, co pokazuje koszyk. Jeżeli w logu kwota jest poprawna, a na froncie nie, problem leży w warstwie prezentacji: motyw, cache albo wtyczka nadpisująca szablony. Jeżeli już w logu kwota jest zła, wróć do kroków od trzeciego do piątego.
Warto też sprawdzić, czy w sklepie nie działają równolegle dwie wtyczki dodające metody wysyłki, na przykład osobna integracja kurierska i uniwersalna wtyczka table rate. Potrafią się nawzajem nadpisywać w sposób zależny od kolejności ładowania, co daje objawy pozornie losowe.
Koszyk blokowy zmienia część reguł
Jeśli sklep korzysta z nowych bloków koszyka i zamówienia, stawki są pobierane przez Store API, a nie renderowane w klasycznych szablonach PHP. Filtr woocommerce_package_rates nadal działa, ale wszystko, co było podpięte pod hooki szablonów, przestaje się wykonywać.
Podgląd surowej odpowiedzi zobaczysz w narzędziach deweloperskich przeglądarki, w zakładce sieci, przy żądaniu do /wp-json/wc/store/v1/cart. Sekcja shipping_rates zawiera dokładnie te stawki, które klient dostaje na stronie. To najszybszy sposób, żeby rozstrzygnąć, czy błąd jest po stronie logiki, czy po stronie wyświetlania.
Szybka mapa objawów
| Objaw | Sprawdź najpierw |
|---|---|
| Zawsze ta sama zła kwota, niezależnie od koszyka | Kolejność stref, koszt podstawowy metody |
| Kwota rośnie liniowo z liczbą sztuk | Zmienna qty w formule |
| Tylko niektóre produkty psują wynik | Klasy wysyłki, klasa na poziomie wariacji |
| Absurdalnie wysokie kwoty przy lekkim towarze | Jednostki wagi, waga gabarytowa |
| Wysyłka za darmo bez powodu | Puste pole kosztu klasy, błąd składni w formule |
| Inna kwota w koszyku niż w zamówieniu | Domyślna lokalizacja klienta, geolokalizacja |
| Kwota zmienia się po zalogowaniu | Adres w profilu, reguły zależne od roli |
| Zmiana w panelu nie robi nic | Cache stawek w sesji, cache stron |
| Kwota różni się między klientami losowo | Geolokalizacja plus cache stron |
| Znikają metody, które powinny być widoczne | Filtr woocommerce_package_rates |
Jak nie wracać do tego co miesiąc
Kilka nawyków, które realnie zmniejszają liczbę takich zgłoszeń.
Trzymaj listę przypadków testowych. Pięć do ośmiu koszyków pokrywających typowe scenariusze: jeden lekki produkt, wiele sztuk, gabaryt, koszyk mieszany, koszyk powyżej progu darmowej wysyłki, adres zagraniczny. Po każdej zmianie w konfiguracji wysyłki przechodzisz je po kolei. Zajmuje to kwadrans i wyłapuje prawie wszystko.
Testuj krańce zakresów, nie środek. Zdecydowana większość błędów siedzi na granicach: pierwszy kod pocztowy z zakresu, dokładnie próg darmowej wysyłki, maksymalna waga progu.
Nie zmieniaj konfiguracji wysyłki na produkcji. Strefy i metody to obszar, w którym błąd jest niewidoczny dla ciebie i bardzo widoczny dla klienta. Kopia na staging kosztuje kilka minut.
Dokumentuj intencję. W nazwie metody albo w notatce zapisz, skąd wzięła się dana kwota i z jakiego cennika. Za pół roku nikt, łącznie z tobą, nie będzie pamiętał, dlaczego akurat 74 zł.
Po każdej zmianie w produktach sprawdź wagi. Import, synchronizacja z hurtownią albo integracja z zewnętrznym systemem potrafią po cichu wyczyścić pole wagi. To ta sama rodzina problemów co rozjeżdżające się stany magazynowe i warto ją kontrolować tym samym mechanizmem.
Podsumowanie
Źle policzona wysyłka niemal nigdy nie jest przypadkiem. To zawsze konsekwencja jednej z kilku rzeczy: kolejności stref, reguły kodów pocztowych, składni formuły, klasy przypisanej na złym poziomie albo brakującej wagi. Kolejność sprawdzania ma znaczenie, bo każdy krok zawęża obszar poszukiwań dla następnego.
Zacznij od ustalenia strefy, potem zejdź do metody, potem do produktów, a wtyczki zostaw na koniec. W tej kolejności problem daje się zwykle zamknąć w kilkanaście minut.
Jeśli utknąłeś na którymś kroku albo sklep zachowuje się w sposób, którego nie potrafisz odtworzyć, napisz do mnie. Diagnozuję takie rzeczy osobiście i zwykle wystarczy dostęp do panelu oraz jeden konkretny przykład koszyka, który liczy się źle.
Nie chcesz robić tego samodzielnie?
Zajmę się tym za Ciebie. Napisz krótko, co się dzieje - odpowiem tego samego dnia roboczego.