O projekcie
Sklep odzieżowy, który sprzedaje poza jeden kraj, więc odwiedzający ogląda ceny w swojej walucie — z kursem pobieranym automatycznie i przeliczeniem pokazywanym na bieżąco. Cała trudność tego projektu siedzi nie w pobieraniu kursów, tylko w tym, co wolno z nimi zrobić.
Zacznę od rozróżnienia, na którym stoi cały projekt: KURS TO NIE CENA. Jeżeli przeliczenie dzieje się przy każdym wyrenderowaniu strony, to ta sama sukienka kosztuje 49,90 euro na liście, 50,10 w koszyku i 50,20 przy płatności — bo między tymi trzema ekranami minęło kilkanaście minut i kurs drgnął. Klient, który to zauważy, składa reklamację i MA RACJĘ. Dlatego kwota w obcej walucie jest wprost opisana jako orientacyjna do momentu złożenia zamówienia, a w tym momencie KURS ZOSTAJE ZAMROŻONY i idzie z zamówieniem dalej.
Druga rzecz, którą zwykle się przemilcza: KTÓRY to ma być kurs. Kurs średni z banku centralnego jest ładny i darmowy, ale nikt nigdy po nim nie zapłaci — operator płatności przewalutuje po swoim, a wydawca karty po jeszcze innym. Pokazywanie kursu średniego jako ceny do zapłaty to obietnica, której sklep nie kontroluje. Tutaj kurs średni jest tym, czym jest — punktem odniesienia z podaną datą i godziną — a tam, gdzie sklep faktycznie rozlicza się w obcej walucie, liczy się kursem operatora ze spreadem opisanym wprost, nie schowanym w zaokrągleniu.
Zaokrąglanie jest miejscem, w którym najczęściej widać, że ktoś tego nie robił. Jen nie ma części ułamkowej, a dinar kuwejcki ma TRZY miejsca po przecinku; korona islandzka chodzi na liczbach całkowitych. Odruchowe zaokrąglenie do dwóch miejsc jest więc błędne dla sporej części świata i w jednym kierunku produkuje kwoty, których nie da się zapłacić, a w drugim — groszowe rozjazdy narastające przez cały koszyk. Liczba miejsc i sposób zaokrąglenia są cechą WALUTY zapisaną w danych, a nie stałą w kodzie, a arytmetyka idzie na liczbach stałoprzecinkowych, bo cena w typie zmiennoprzecinkowym to pomyłka czekająca na okazję.
Przeliczanie ma też skutek, który boli akurat w modzie: ROZBIJA PUNKTY CENOWE. Cena 199 złotych zamienia się w 46,31 euro — i cała praca nad tym, jak cena wygląda, idzie do kosza. Dlatego dojrzała odpowiedź nie brzmi „przeliczaj wszystko”, tylko: dla rynków, na których sklep naprawdę sprzedaje, obowiązuje WŁASNY CENNIK z cenami ustawionymi ręcznie, a przelicznik służy do orientacji na pozostałych. To rozróżnienie jest w danych, więc da się je zmieniać rynek po rynku, bez dotykania kodu.
Pobieranie kursów wygląda na prostą rzecz do czasu pierwszej soboty. Banki centralne publikują RAZ na dzień roboczy, o określonej godzinie, i nie publikują w weekendy ani w święta — a kalendarz świąt jest inny w każdym kraju. Zadanie, które po prostu pyta o „kurs na dziś”, w sobotę nie dostaje nic i albo się wywraca, albo po cichu bierze wartość sprzed tygodnia. Tutaj magazyn trzyma OSTATNI ZNANY KURS razem z jego wiekiem, a wiek ma twardy sufit: po jego przekroczeniu przelicznik nie pokazuje liczby, tylko znika i zostawia cenę w walucie sklepu.
To jest zresztą ogólniejsza zasada tego projektu: NA CENIE NIE WOLNO OTWIERAĆ SIĘ NA WYPADEK AWARII. Kiedy źródła kursów milczą albo różnią się od siebie bardziej, niż wynosi próg, system nie zgaduje i nie wybiera „lepszej” wartości. Brak przeliczenia jest niewygodny; przeliczenie nieprawdziwe jest kosztowne — i to w sposób, który wraca zwrotem, reklamacją albo wpisem w rejestrze skarg.
Osobno warto powiedzieć, co tutaj znaczy „NA ŻYWO”, bo to określenie bywa sprzedawane nieuczciwie. Nie znaczy zapytania do zewnętrznego serwisu przy każdym wejściu na stronę — to jest wolne, kosztowne i w szczycie ruchu kończy się odcięciem przez limit zapytań, czyli brakiem cen dokładnie wtedy, gdy są najbardziej potrzebne. Kursy walut zmieniają się w skali minut, a nie milisekund, więc „na żywo” znaczy tu: kurs aktualny, wzięty z krótkiego bufora po stronie serwera i ZAWSZE POKAZANY RAZEM Z MOMENTEM POBRANIA. Data przy kursie jest częścią informacji, nie ozdobą.
Waluta nie jest tym samym co kraj ani co język, choć interfejsy notorycznie je zlepiają. Ktoś w Niemczech może chcieć zapłacić w złotych, a Polak mieszkający w Irlandii oglądać sklep po polsku i płacić w euro. Rozpoznanie po adresie IP jest zgadywaniem, więc służy tu wyłącznie do ustawienia wartości domyślnej — wybór waluty należy do człowieka, jest zapamiętywany i nie zmienia się sam pod nim przy kolejnej wizycie.
Na końcu jest rzecz, która zaskakuje przy pierwszym zamknięciu miesiąca: JEDNO ZAMÓWIENIE NIESIE TRZY RÓŻNE KURSY. Kurs POKAZANY klientowi przy składaniu zamówienia, kurs ROZLICZENIOWY operatora płatności, po którym pieniądze faktycznie wpłynęły, i kurs KSIĘGOWY, po którym przychód trafia do ksiąg — w Polsce średni kurs NBP z ostatniego dnia roboczego poprzedzającego transakcję. To są trzy różne liczby i sklep, który trzyma jedną, nie umie się rozliczyć. Stąd też odpowiedź na zwroty: oddaje się DOKŁADNIE TĘ KWOTĘ, która została pobrana, w walucie, w której została pobrana — a nie równowartość przeliczoną po kursie z dnia zwrotu, bo ta potrafi różnić się o kilka procent i zawsze na czyjąś niekorzyść.
Problem
Sklep sprzedający za granicę albo pokazuje ceny tylko w swojej walucie i traci klientów na etapie „ile to właściwie jest”, albo przelicza je przy każdym renderze — i wtedy ta sama rzecz kosztuje inaczej na liście, w koszyku i przy płatności. Do tego kurs średni banku centralnego, którym zwykle się przelicza, nie jest kursem, po którym ktokolwiek zapłaci.
Rozwiązanie
Kurs jest pobierany automatycznie, trzymany z własnym wiekiem i twardym sufitem nieświeżości, a przy każdej kwocie widać moment jego pobrania. Przeliczenie jest jawnie orientacyjne do złożenia zamówienia, w którym kurs zostaje zamrożony i niesiony dalej — aż do zwrotu, rozliczenia z operatorem i zapisu księgowego, bo to trzy różne kursy.
Funkcje
- kurs zamrażany przy złożeniu zamówienia i niesiony z nim aż do zwrotu
- każda przeliczona kwota pokazana razem z datą i godziną kursu
- liczba miejsc po przecinku i zaokrąglenie jako cecha waluty, nie stała w kodzie
- arytmetyka stałoprzecinkowa — cena nigdy nie przechodzi przez typ zmiennoprzecinkowy
- własne cenniki dla rynków, na których sklep faktycznie sprzedaje
- ostatni znany kurs z twardym sufitem wieku; po przekroczeniu przelicznik znika
- rozbieżność między źródłami ponad próg wstrzymuje przeliczanie zamiast zgadywać
- waluta jako wybór człowieka; adres IP ustawia najwyżej wartość domyślną
- trzy kursy przy zamówieniu: pokazany, rozliczeniowy i księgowy
Moduły
kursy-zrodla
warstwa pobieraniaŚciąga kursy z banków centralnych i ze źródła zapasowego, i porównuje je ze sobą.
- harmonogram dopasowany do godzin publikacji, osobno dla każdego banku centralnego
- kalendarz dni wolnych per kraj — brak publikacji w sobotę to nie jest awaria
- dwa źródła zestawiane ze sobą; rozjazd ponad próg zatrzymuje aktualizację
- odpowiedź źródła zapisywana w całości, żeby dało się odtworzyć, skąd wziął się kurs
- ponowienia z narastającą przerwą; cisza źródła nie kasuje ostatniego znanego kursu
kursy-magazyn
magazyn kursówTrzyma kursy z ich wiekiem i rozstrzyga, kiedy kurs przestaje nadawać się do pokazania.
- wpis dopisywany, nigdy nadpisywany — historia kursu jest potrzebna przy reklamacji
- ostatni znany kurs zawsze dostępny, ale zawsze z podanym wiekiem
- twardy sufit nieświeżości; po jego przekroczeniu kurs przestaje być wydawany
- liczba miejsc po przecinku i reguła zaokrąglenia zapisane przy walucie
- kursy trzymane jako liczby stałoprzecinkowe, bez przejścia przez typ zmiennoprzecinkowy
cennik-rynkow
warstwa cenRozdziela rynki z własnym cennikiem od tych, na których cena jest tylko przeliczana.
- rynek z własnym cennikiem ma ceny ustawione ręcznie, z zachowanymi punktami cenowymi
- rynek bez cennika dostaje przeliczenie opisane jako orientacyjne
- przypisanie rynku do trybu stoi w danych, więc zmiana nie wymaga wydania kodu
- cennik rynku niesie własną walutę, własny VAT i własne progi dostawy
- brak ceny dla rynku nie ukrywa produktu — pokazuje go w walucie sklepu
przelicznik
warstwa prezentacjiPokazuje kwotę w wybranej walucie tak, żeby nikt nie wziął jej za cenę wiążącą.
- przy kwocie zawsze moment pobrania kursu — to część informacji, nie ozdoba
- krótki bufor po stronie serwera zamiast zapytania do źródła przy każdym wejściu
- wybór waluty należy do człowieka i jest zapamiętywany między wizytami
- adres IP ustawia najwyżej wartość domyślną i nigdy nie nadpisuje wyboru
- brak świeżego kursu ukrywa przeliczenie zamiast pokazywać liczbę z wczoraj
zamowienie-kurs
zamrożenie kursuW momencie złożenia zamówienia zamienia kurs orientacyjny w kurs wiążący.
- kurs, jego źródło i moment pobrania zapisane przy zamówieniu, nie doliczane później
- kwota do zapłaty wyliczona raz i niezmienna — ponowne wejście w koszyk jej nie rusza
- widełki bezpieczeństwa: skok kursu ponad próg wymaga potwierdzenia przez klienta
- zamówienie niesie obie kwoty — w walucie sklepu i w walucie zapłaty
- dokument sprzedaży powstaje z zamrożonych wartości, nie z przeliczenia na nowo
podatek-rynki
warstwa podatkowaPilnuje, żeby przeliczona kwota była kwotą właściwą dla kraju odbiorcy.
- stawka VAT liczona według kraju dostawy, nie kraju sklepu
- próg sprzedaży zagranicznej pilnowany w danych, z ostrzeżeniem przed przekroczeniem
- cena pokazana klientowi zawiera podatek jego kraju, zanim zostanie przeliczona
- wysyłka poza obszar celny opisana wprost: cło i podatek po stronie odbiorcy
- zmiana stawki nie przelicza wstecz dokumentów już wystawionych
zwroty-rozliczenia
warstwa rozliczeńDomyka pieniądze: zwrot, wpływ od operatora i zapis księgowy — trzy różne kursy.
- zwrot oddaje DOKŁADNIE pobraną kwotę w walucie pobrania, bez przeliczania na nowo
- kurs rozliczeniowy operatora zestawiany z kursem pokazanym; różnica jest raportowana
- kurs księgowy brany z ostatniego dnia roboczego przed transakcją, osobno od pozostałych
- różnice kursowe pokazane jako osobna pozycja, a nie rozmyte w marży
- zwrot częściowy liczony z pozycji zamówienia, żeby suma zwrotów nie przekroczyła wpłaty
Architektura
- Podstawa
- WooCommerce na WordPressie z warstwą walutową jako osobnym modułem; sklep działa dalej, gdy warstwa kursów milczy
- Źródła kursów
- banki centralne według ich godzin publikacji plus źródło zapasowe; dwa źródła zestawiane, rozjazd ponad próg wstrzymuje aktualizację
- Kalendarz
- dni wolne per kraj — brak publikacji w weekend i święto jest stanem normalnym, nie awarią
- Świeżość
- ostatni znany kurs z podanym wiekiem i twardym sufitem; po przekroczeniu przelicznik znika zamiast zgadywać
- Arytmetyka
- liczby stałoprzecinkowe od źródła po dokument; cena nie przechodzi przez typ zmiennoprzecinkowy ani razu
- Zaokrąglanie
- liczba miejsc i reguła jako cecha waluty w danych — jen bez części ułamkowej, dinar kuwejcki z trzema miejscami
- Ceny rynkowe
- rynki z własnym cennikiem mają ceny ustawiane ręcznie; przelicznik obsługuje wyłącznie rynki bez cennika
- Status kwoty
- orientacyjna do złożenia zamówienia, wiążąca po zamrożeniu kursu; różnica opisana w interfejsie, nie w regulaminie
- Zamrożenie
- kurs, źródło i moment zapisane przy zamówieniu; skok kursu ponad widełki wymaga potwierdzenia klienta
- Podatek
- stawka według kraju dostawy, progi sprzedaży zagranicznej pilnowane w danych, cło poza obszarem celnym opisane wprost
- Zwroty
- oddawana jest dokładnie pobrana kwota w walucie pobrania — nigdy równowartość po kursie z dnia zwrotu
- Trzy kursy
- pokazany klientowi, rozliczeniowy operatora i księgowy z ostatniego dnia roboczego; różnice kursowe jako osobna pozycja
- Wydajność
- krótki bufor w Redisie zamiast zapytania do źródła na wejście; limit zapytań nie może wyłączyć cen w szczycie ruchu
Technologie
- PHP
- WordPress
- WooCommerce
- MySQL
- Redis
- REST API
Autor
Mariusz PerzyńskiClient Relations & Operations