Rabat na markę to nie jedna liczba więcej na koncie klienta. To reguła, którą muszą tak samo stosować wszystkie miejsca liczące cenę, łącznie z pamięcią podręczną. Klient zamawia na podstawie ceny z listingu, a płaci cenę z koszyka, więc każda rozbieżność między nimi jest błędem w cenie, nie kosmetyką.
| Konfiguracja | Magento Open Source 2.4.8, wszyscy klienci firmowi w jednej grupie, rabat przypisany do konta, wyjątki dla wybranych marek osobno dla katalogu bieżącego i przedsprzedaży |
|---|---|
| Zasada | wyjątek dla marki → stawka konta → brak rabatu |
| Miejsca, które muszą znać profil | cena produktu na listingu i karcie, blok wyceny netto, koszyk i kasa, klucz pamięci stron |
| Wynik | na sprawdzanych stronach katalogu każde konto widzi swoje ceny; koszyk zgodny z kartą przy stawce konta; rozbieżność netto/brutto w podpowiedziach wyszukiwarki poprawiona; koszyk z wyjątkiem dla marki nie był osobno testowany |
Kiedy wystarczą grupy klientów i reguły cenowe Magento
Magento Open Source różnicuje ceny grupami klientów: ceny progowe i grupowe ustawiane na produkcie oraz reguły cenowe katalogu. Według dokumentacji Adobe o regułach cenowych katalogu reguła działa dla wybranych witryn i grup klientów, a jej warunki mogą opierać się na atrybutach produktu, o ile atrybut jest dopuszczony do warunków reguł promocyjnych. Marka jako atrybut nadaje się więc do warunku.
Przy kilku klientach to wystarcza: grupa na klienta i reguła na każdą parę „grupa × marka”. Przy kilkudziesięciu kontach z indywidualną stawką i wyjątkami dla części marek liczba grup i reguł rośnie razem z liczbą klientów, a każda zmiana warunków handlowych to edycja reguł. Adobe Commerce ma do tego katalogi współdzielone w module B2B, ale w Magento Open Source ich nie ma. Wtedy warto ocenić rozszerzenie standardowej logiki cen: gotowy moduł albo rozwiązanie przygotowane dla sklepu. W opisanym sklepie powstało to drugie: stawka konta, tabela wyjątków i jeden mechanizm, który z nich liczy cenę.
Zasada: wyjątek dla marki, potem stawka konta
Najprostsza reguła, która nie zmienia cen nikomu, kogo zmiana nie dotyczy, ma trzy kroki: jeśli klient ma wyjątek dla marki produktu, obowiązuje wyjątek; jeśli nie ma, obowiązuje stawka z konta; jeśli konto nie ma stawki, rabatu nie ma.
- w tabeli wyjątków zapisuje się tylko odstępstwa (klient × marka), a nie pełną macierz stawek
- katalog bieżący i przedsprzedaż mogą mieć osobne stawki; puste pole dla jednego obiegu znaczy „w tym obiegu stawka z konta”
- brak wyjątku dla marki oznacza stawkę z konta, a nie ukrycie marki albo cenę bez rabatu; tę decyzję trzeba podjąć jawnie, bo wszystkie trzy warianty da się obronić
Skutek tej zasady: klient bez żadnego wyjątku liczy się dokładnie tak jak przed wdrożeniem. Zmianę można więc uruchomić bez przestawiania cen istniejącym klientom i dopisywać wyjątki pojedynczo.
Jeśli sklep ma kampanie przedsprzedażowe ze stawkami przypisanymi do klienta, kolejność wydłuża się o jeden krok: stawka imienna z kampanii, potem wyjątek dla marki kampanii, potem stawka konta. Kampania, w której jest więcej niż jedna marka, nie ma jednoznacznej marki, więc w opisanym sklepie obowiązuje w niej stawka konta.
Od jakiej ceny liczy się rabat i co z innymi cenami Magento
Kolejność „wyjątek, potem stawka konta” mówi, którą stawkę wybrać, ale nie rozstrzyga, od jakiej ceny ją liczyć. Standardowe Magento ma kilka konkurujących mechanizmów: według dokumentacji Adobe o cenach specjalnych cena końcowa to najniższa z odpowiednich cen: bazowej, grupowej lub progowej, specjalnej i wyniku reguły cenowej katalogu. Własny rabat musi więc jawnie ustalić swoją relację do tych mechanizmów.
W opisanym sklepie zasady są takie:
- rabat liczy się od ceny katalogowej produktu, przeliczonej na netto, a nie od ceny już obniżonej innym mechanizmem
- w koszyku wynik trafia jako cena własna pozycji, więc to on decyduje o kwocie do zapłaty
- wyprzedaż to osobna cena, jedna dla wszystkich klientów i niezależna od rabatu; rabat konta się z nią nie łączy
- w tabeli wyjątków 0% oznacza „bez rabatu na tę markę”, a puste pole powrót do stawki konta; panel musi te dwie decyzje odróżniać
Niezależność wyprzedaży od rabatu ma skutek handlowy: klient z rabatem np. 45% przy wyprzedaży 30% zapłaciłby więcej niż swoją zwykłą cenę. Rabat wyprzedażowy musi więc przewyższać rabaty klientów, żeby miał sens. Jeśli sklep korzysta też z cen specjalnych, progowych albo reguł katalogowych, trzeba spisać, która cena wygrywa w każdej kombinacji, i sprawdzić to w koszyku. W opisanym wdrożeniu testy obejmowały rabat konta, wyjątki dla marek i cenę wyprzedażową.
Cztery miejsca, które muszą uwzględniać profil cenowy
W sklepie z własną logiką rabatów cena nie powstaje w jednym miejscu. Trzy z poniższych miejsc liczą albo pokazują cenę, a czwarte rozdziela warianty stron w pamięci podręcznej. Każde musi stosować tę samą zasadę:
| Miejsce | Co ustala | Gdy pominie wyjątek |
|---|---|---|
| cena końcowa produktu | cena na listingu i karcie produktu | klient wybiera towar po złej cenie |
| blok wyceny | wyświetlana cena netto | dwie różne kwoty na jednej stronie |
| koszyk i kasa | cena, którą klient płaci | w koszyku inna kwota niż na karcie |
| kontekst HTTP | klucz pamięci stron | klient dostaje stronę z cenami innego klienta |
Dlatego wszystkie cztery miejsca powinny pytać o stawkę jeden wspólny mechanizm, a nie ustalać jej każde po swojemu. Rozbieżność zwykle pojawia się nie przy wdrożeniu, tylko przy kolejnej zmianie, gdy ktoś poprawi regułę w trzech miejscach z czterech. Najważniejsze z nich jest koszyk, bo tam klient płaci, więc rabat sprawdza się w koszyku, a nie tylko w modelu ceny.
Pułapka: marka na produkcie nadrzędnym, a nie na wariancie
W katalogu z rozmiarami i kolorami produkt to zwykle produkt konfigurowalny z wariantami. W opisanym sklepie marka była ustawiona na około 94% produktów nadrzędnych i praktycznie na żadnym wariancie. Logika, która czyta markę z wariantu, nie znajdzie jej i po cichu zastosuje stawkę konta zamiast wyjątku.
To samo dotyczy koszyka: pozycja w koszyku to produkt nadrzędny, a wariant jest jej pozycją podrzędną. Rabat trzeba więc liczyć na pozycji nadrzędnej i z jej marki, a pozycje podrzędne celowo pomijać, żeby nie policzyć rabatu dwa razy.
Pułapka: netto i brutto w różnych miejscach
W opisanym sklepie katalog trzyma ceny brutto, a klient firmowy widzi ceny netto. Rabat liczy się od netto. Wynik się zgadza, ale łatwo porównać kwoty w różnych jednostkach i uznać poprawną cenę za błąd albo odwrotnie.
Przykład umowny: produkt kosztuje 123,00 zł brutto, czyli 100,00 zł netto przy VAT 23%. Rabat 40% daje 60,00 zł netto, czyli 73,80 zł brutto. Karta pokazuje 60,00 zł netto, a podpowiedzi wyszukiwarki 73,80 zł. To ta sama cena, ale klient widzi dwie różne kwoty. W opisanym sklepie dokładnie tak rozjechały się podpowiedzi wyszukiwarki i karta produktu. Podpowiedzi poprawiliśmy tak, żeby pokazywały netto, jak karta.
Przy testach zapisuj więc przy każdej kwocie, czy to netto, czy brutto, i porównuj kwoty w tej samej jednostce. Skąd w Magento bierze się VAT doliczany do ceny brutto i dlaczego jeden klient może widzieć brutto jako netto, opisuje kolejny wpis z serii: Magento dolicza VAT do ceny brutto.
Pułapka: klucz pamięci bez wyjątków
Jeśli do kontekstu pamięci stron trafia tylko stawka z konta, dwóch klientów z tą samą stawką ogólną, ale innymi wyjątkami dla marek, dostaje ten sam klucz i dzieli te same strony z cenami. Do kontekstu musi trafiać cały profil: stawka konta i lista wyjątków, zapisane w stałej kolejności. Bez stałej kolejności ten sam profil daje różne klucze i pamięć mnoży kopie stron.
Dlaczego do kontekstu nie wkłada się identyfikatora konta, wyjaśnia dokumentacja Adobe o kontekście pamięci stron: zmienna kontekstu per użytkownik oznacza osobną kopię strony dla każdego użytkownika. Profil pozwala kontom o identycznych warunkach dzielić wpisy. Ten sam warunek dotyczy pamięci bloków z ceną i widżetów produktów, które w kluczu rozróżniają tylko grupę klienta. Mechanizm i testy opisuje poprzedni wpis z tej serii: Magento 2.4.8: problem z full page cache po zalogowaniu.
Pułapka: stara kopia profilu po zapisie wyjątku
Mechanizm liczący stawkę zwykle buduje profil klienta raz i trzyma go w pamięci, żeby nie czytać tabeli wyjątków przy każdym produkcie na listingu. Po zapisie nowego wyjątku taki profil jest nieaktualny i do czasu odświeżenia pokazuje starą stawkę.
W opisanym wdrożeniu zapamiętany profil ma w kluczu numer wersji tabeli wyjątków. Każdy zapis podnosi wersję, więc kolejne odczyty budują profil od nowa. Ten błąd wyszedł w teście przed wdrożeniem: zapis wyjątku i odczyt ceny w tym samym żądaniu pokazywały starą stawkę.
Numer wersji odświeża profil po stronie serwera. Nie odświeża sam z siebie danych prywatnych w przeglądarce klienta, np. paska z rabatem. Według dokumentacji Adobe o treściach prywatnych Magento trzyma takie sekcje w pamięci przeglądarki i odświeża je według reguł unieważniania. Opisany test nie potwierdza więc ani nowej ceny w kolejnych żądaniach trwającej sesji, ani odświeżenia paska w otwartej przeglądarce. To osobne testy, opisane w sekcji o testach odbioru.
Jak pokazać klientowi, jaki ma rabat
Gdy rabat zależy od marki, jedna liczba „Twój rabat: 40%” przestaje być prawdziwa. W opisanym sklepie wszystkie miejsca, które opisują rabat, korzystają z jednego wspólnego podsumowania:
- klient bez wyjątków widzi jedną liczbę, tak jak wcześniej
- klient z wyjątkami widzi zakres od najniższej do najwyższej stawki, np. „40–45%”, także wtedy, gdy wyjątek jest niższy od stawki konta
- na koncie klienta jest dodatkowo lista marek z wyjątkiem i ich stawek
- ten sam opis trafia do pasku konta, na stronę konta, do maila o zamówieniu i do eksportu klientów w panelu
Pasek z rabatem jest częścią prywatnych danych klienta, ładowanych osobnym zapytaniem, więc nie trafia do pamięci stron i nie musi być uwzględniany w jej kluczu.
Testy odbioru rabatów marek
W opisanym sklepie ceny na stronach katalogu sprawdziliśmy dwoma kontami z różnymi stawkami, w obu kolejnościach, od pustej pamięci. Koszyk sprawdziliśmy przy samej stawce konta i przy cenie wyprzedażowej:
| Test | Wynik |
|---|---|
| strona główna, kategoria marki (str. 1 i 2), podkategoria, szybkie zamawianie, dwa wyszukiwania, karta produktu | każde konto widzi swoje ceny |
| podpowiedzi wyszukiwarki | bez przecieku cen; rozbieżność netto/brutto znaleziona i poprawiona |
| koszyk z 2 szt. przy stawce konta: netto, VAT i suma | zgodny z kartą co do grosza |
| cena wyprzedażowa na karcie i w koszyku | zgodna; rabat konta się z nią nie łączy |
| zapis wyjątku i odczyt ceny w tym samym żądaniu | nowa stawka po dodaniu wersji do klucza profilu |
Tych testów w opisanym przeglądzie nie było: koszyk i kasa z wyjątkiem dla marki, zmiana ilości w koszyku, dwa konta z tą samą stawką ogólną i różnymi wyjątkami (najlepszy test pułapki klucza pamięci), nowa cena w kolejnych żądaniach trwającej sesji po zapisie wyjątku i odświeżenie paska rabatu w otwartej przeglądarce. Przy podobnym wdrożeniu to pierwsze testy do wykonania.
Test w modelu albo w konsoli sprawdza samo obliczenie. Nie pokazuje tego, co klient zobaczy w sesji, po wyrenderowaniu strony i z pamięcią podręczną, dlatego testy odbioru robi się przez żądania HTTP z prawdziwymi sesjami klientów.
Kiedy zrobisz to regułami, a kiedy rozszerzeniem logiki cen
Kilku klientów firmowych, stawki zmieniane rzadko, wyjątki dla nielicznych marek. Grupa na klienta i reguły cenowe katalogu z warunkiem na markę dadzą ten sam efekt bez własnego kodu. Zostaje kontrola, czy reguły są przeliczone i czy marka jest atrybutem dopuszczonym do warunków.
Dziesiątki kont z indywidualną stawką, wyjątki per marka, osobne stawki dla przedsprzedaży i katalog dostępny po zalogowaniu. Gotowy moduł albo rozwiązanie przygotowane dla sklepu musi objąć wszystkie cztery miejsca z tabeli, klucze pamięci i każde miejsce, które opisuje rabat klientowi.
Gdy zlecasz nam taki przegląd albo wdrożenie, dostajesz opis obowiązującej zasady liczenia ceny, listę miejsc, które ją stosują, porównanie cen na kontach testowych na listingu, karcie, w wyszukiwarce, w koszyku i w kasie oraz raport z testów odbioru w układzie z sekcji 10. Więcej o pracy przy indywidualnych rabatach i cennikach B2B i przy sklepach na Magento.