Optymalizacja konwersji sprzedaży — integracja PHP z precise.sale

Ten artykuł kierowany jest głównie do programistów, którzy chcą podnieść zysk ze sprzedaży w sklepie napisanym w PHP. Pokazujemy tu jak na przykładzie prostego sklepu zintegrować się z serwisem precise.sale, napiszemy symulację użytkowników odwiedzających sklep i zaprezentujemy wyniki optymalizacji.

Repozytorium z kodem dostępne jest pod linkiem:

W tym artykule opiszemy kolejno wszystkie kroki prowadzące do otrzymania finalnego kodu tak aby łatwo można było napisać samemu odpowiednio zmodyfikowaną jego wersję. Zaczniemy od absolutnych podstaw — strony prezentującej produkt, dodamy stronę do potwierdzania zakupu, następnie podłączymy raportowanie wizyt oraz zakupów do serwisu precise.sale i na koniec napiszemy automat symulujący odwiedziny i zakupy w naszym sklepie.


1. Strona główna i logowanie wizyt do pliku

Chcemy aby strona główna wyświetlała tylko jeden produkt, ma on mieć podaną cenę, a każde jego wyświetlenie powinno podbijać licznik wyświetleń, który będzie później użyty w optymalizacji konwersji. Pliki których będziemy potrzebować to:

  • index.php zawierający podstawową logikę
  • template/index.html zawierający szablon html ze znacznikami wskazującymi gdzie umieścić dane
  • src/Helpers.php przechowujący metody do operowania na danych i wspomnianym szablonie

index.php spełnia następujące zadania

a) odczytanie ceny i liczby odwiedzin

b) wstrzyknięcie ich do szablonu

c) zapisanie liczby odwiedzin powiększonej o 1

<?php

require_once
'src/Helpers.php';

$state = Helpers::loadState();

echo Helpers::render('index',$state);

$state["visits"]++;

Helpers::saveState($state);

Zostały tu użyte trzy metody statyczne klasy `Helpers`, ale ich omówienie zostawimy na koniec ponieważ istotna jest dla nich zawartość pliku:

template/index.html

<html>
<head><title>Exemplary PHP shop</title></head>
<body>

<main>
<h1>Product</h1>
<p>Price <span>{{price}}</span></p>
<a href="/buy" onclick="(function (event) {alert('You bought product!'); event.preventDefault();})(event)">Buy</a>
</main>
<footer>
<p>Shop was visited {{visits}} times.</p>
</footer>

</body>
</html>

Widzimy, że jest to prosty HTML lecz w miejscu ceny i liczby wizyt ma podwójne nawiasy klamrowe. Jest to składnia powszechnie stosowana w różnych silnikach szablonów jak twig, handlebars czy vue. Za pomocą tych nawiasów definiujemy nazwy zmiennych, w które metoda Helpers::render podstawi wartości przekazane w tablicy podanej jako drugi argument.

Czas na przyjrzenie się jak wygląda plik src/Helpers.php

<?php

class
Helpers
{
const DB = __DIR__ . '/../db.json';

static function render($file, $data) {
$text = file_get_contents(__DIR__ . '/../template/' . $file . '.html');
foreach ($data as $key => $val) {
$text = preg_replace('/{{'.$key.'}}/',$val, $text );
}
return $text;
}

static function loadState() {
if(file_exists(self::DB)) {
$json = file_get_contents(self::DB);
} else {
$default = '{"price":3,"visits":0}';
file_put_contents(self::DB, $default);
$json = $default;
}
return json_decode($json,true);
}

static function saveState($state) {
file_put_contents(self::DB, json_encode($state));
}
}

Na początku mamy stałą z adresem pliku `db.json`, ze względu na to, że chciałem utworzyć najprostszy możliwy przykład, nie będziemy stosować bazy danych, a cały stan aplikacji będzie trzymany właśnie w tym pliku.

Pierwsza metoda z index.php czyli Helpers::loadState odpowiada właśnie za odczyt tego pliku, ponieważ początkowo on nie istnieje, zwraca ona wartość domyślną, to znaczy 0 wizyt i cena ustalona na 3. Jednocześnie pierwsze włączenie tworzy ten plik aby nie powodować błędów w przyszłości.

Kolejna metoda Helpers::render przyjmuje nazwę pliku z szablonem i dane jakie należy do niego wstawić. Odczytuje szablon i iterując po kluczach tablicy przekazanej jako drugi argument podmienia wystąpienia tego klucza otoczonego podwójnymi nawiasami klamrowymi na wartości tablicy.

Ostatnia metoda Helpers::saveState zapisuje stan aplikacji czyli cenę i ilość odwiedzin do pliku `db.json`.

Na tym etapie sklep może:

  • wyświetlać cenę jednego produktu
  • ma przycisk do jego kupowania
  • w stopce pokazuje liczbę wyświetleń
  • kolejne wyświetlenia są zliczane

W ten sposób napisaliśmy podstawę do dalszego rozwijania projektu czyli obsłużenia przycisku do zakupu i wyświetlenia strony potwierdzającej zakup.


2. Strona kupowania i przygotowanie do integracji

Teraz dodamy do projektu stronę potwierdzającą zakup oraz podłączymy klasę do logowania wizyt, zakupów i zmieniania cen. Metody tej klasy zostaną dopisane w trzecim kroku.

Przede wszystkim ponieważ mają być obsługiwany dwa adresy URL a nie jeden, w projekcie pojawia się plik:

  • src/Controller.php

Ma on dwie statyczne metody buy oraz index . Logika związana z tymi akcjami będzie przerzucona do niego. W pliku index.php znajdzie się jedynie routing. Więc tak wygląda teraz index.php

<?php

require_once
'src/Controller.php';

$path = $_SERVER["REQUEST_URI"];

if($path === "/buy") {
echo Controller::buy();
} else {
echo Controller::index();
}

a src/Controller.php przejął jego funkcje z poprzedniego kroku

<?php

require_once
__DIR__ . '/Api.php';
require_once __DIR__ . '/Helpers.php';

class Controller
{
static function index() {
$state = Helpers::loadState();
$state["visits"]++;
        $apiClient = new Api();
if($state["visits"] % 10 === 0) { 
$state["price"] = $apiClient->reprice($state["price"]);
}
$apiClient->logVisit();

Helpers::saveState($state);

return Helpers::render('index',$state);
}

static function buy() {
$state = Helpers::loadState();

(new Api())->logBuy($state);

return Helpers::render('confirm',$state);
}
}

Jedynymi różnicami jest pojawianie się klasy Api, która w dla co dziesiątych odwiedzin przestawia cenę na to co zwraca reprice i loguje każdą wizytę metodą logVisit . W metodzie Controller::buy logika jest bardzo podobna do strony głównej, ładujemy stan, logujemy tym razem zakup a nie wizytę i renderujemy widok.

Plik z widokiem potwierdzenia zakupu to super prosty szablon

template/confirm.html

<html>
<head><title>Exemplary PHP shop</title></head>
<body>

<main>
<h1>You bought product</h1>
<p>Congratulations</p>
<a href="/">Come back to shop</a>
</main>
<footer>
<p>Shop was visited {{visits}} times.</p>
</footer>

</body>
</html>

W template/index.html zmieniła się tylko jedna rzecz. Zamiast wyskakującego okienka po kliknięciu zakupu mamy teraz przekierowanie, innymi słowy wycięliśmy cały JavaScript, czyli zamiast

<a href="/buy" onclick="(function (event) {alert('You bought product!'); event.preventDefault();})(event)">Buy</a>

jest po prostu

<a href="/buy">Buy</a>

Została klasa Api z pliku src/Api.php, na tym etapie nie implementujemy jej metod, ponieważ zrobimy to w przyszłości

<?php

class
Api
{
public function reprice($currentPrice) {
return 0;
}

public function logVisit() {

}

public function logBuy($state) {

}
}

W tym kroku

  • rozdzieliliśmy kod na kontroler, metody pomocnicze (helper) i klasę API zawierającą metody do komunikacji z serwisem precise.sale
  • dodaliśmy drugi widok z podsumowaniem zakupu
  • przenieśliśmy logikę akcji do kontrolera, a w indeksie umieściliśmy routing

Teraz jesteśmy gotowi aby podłączyć się do serwisu precise.sale i dopisać ciało metod klasy Api .


3. Podłączenie do precises.sale, obsługa zmiennych środowiskowych

Serwis precise sale identyfikuje klienta i sprawdza jego uprawnienia używając klucza. Wpisanie go bezpośrednio w kodzie jest na tyle złą praktyką, że nawet chęć maksymalnego uproszczenia nie usprawiedliwia mieszania kodu i klucza API w jednym pliku. Aby tego uniknąć wykorzystamy prostą bibliotekę: vlucas/phpdotenv. Instalujemy ją komendą:

composer req vlucas/phpdotenv "^2.5"

Na początku pliku index.php dołączamy linie

require_once 'vendor/autoload.php';

$dotenv = new Dotenv\Dotenv(__DIR__);
$dotenv->load();

Potrzebujemy teraz dwóch plików: .env w którym znajdą się nasze zmienne oraz .env.dist który zostanie dodany do repozytorium i będzie pełnił funkcję spisu treści dla zmiennych które powinien zawierać .env oraz ich wartości domyślnych. Tak powinien wyglądać .env.dist

###> precise sale ###
PRECISE_SALE_URL=https://api.precise.sale
PRECISE_SALE_API_KEY=xxx
###< precise sale ###

.env jest taki sam z tym, że wartość klucza API powinna być ustawiona zgodnie z tym jaki klucz zostanie wygenerowany w serwisie precise.sale. W celu utworzenia takiego klucza zakładamy konto na stronie

https://precises.sale

Przechodzimy do zakłdadki “platforms”, klikamy przycisk “integrate” w karcie “custom”. Na liście na dole powinna pojawić się nowa platforma. Po kliknięciu przy niej przycisku “pick” będziemy mogli oglądać pozostałe widoki z perspektywy tej platformy. Zamazany napis w trzeciej kolumnie to API key którego będziemy potrzebować i który wkleimy jako wartość zmiennej PRECISE_SALE_API_KEY w pliku .env

Tworzenie platformy w panelu precise.sale

Drugim krokiem będzie utworzenie produktu w zakładce “products”

Tworzenie produktu w panelu precise.sale

Chcemy mu zdefiniować id, ważne aby to była wartość 1, ponieważ taką wartość będziemy wpisywać w kodzie klasy Api , cenę sprzedaży ustawiamy na 3, a cenę bazową czyli tą od której jest liczona marża na 2.

Po kliknięciu na produkt powinniśmy zobaczyć przycisk “Start optimizing price” po prawej stronie. Klikamy go.

Produkt powinien zostać zatwierdzony do optymalizacji cen zielonym przyciskiem po prawej.

Zmieniamy frequency (częstotliwość przeliczania ustawiona w milisekundach) na 6000, dla naszych testowych zastosowań nie chcemy czekać na przeliczanie ceny całego dnia, a 6 sekund jest minimalną dopuszczalną wartością. Ustawienie mniejszej nic nie zmieni. Zatwierdzamy klikając “Update algorithm parameters”.

Jeśli plik .env jest już gotowy i wkleiliśmy do niego poprawny API_KEY możemy przejść do jego odczytania w klasie Api. Dzięki paczce phpdotenv dostęp do tych zmiennych jest bardzo łatwy. Zapiszemy zarówno API_KEY jak i URL do własności klasy Api dodając jej następujący konstruktor.

private $URL;
private $KEY;

public function __construct()
{
$this->URL=getenv('PRECISE_SALE_URL');
$this->KEY=getenv('PRECISE_SALE_API_KEY');
}

Oczywiście nie możemy zapomnieć o dodaniu linii

.env
vendor

do pliku .gitignore jeśli korzystamy z gita w naszym projekcie.

W tym kroku nie zmieniliśmy zbyt wiele w kodzie, ale ustawiliśmy zmienne które będą nam niezbędne w kolejnym kroku przy implementacji metod do logowania wizyty, logowania zakupu i zmieniania ceny.


4. Implementacja metod do logowania wizyt oraz zakupów i zmieniania cen

Do integracji będziemy potrzebowali dokumentacji API, jest ona dostępna pod adresem:

Spośród opisanych tam końcówek będziemy potrzebowali wysyłania wizyt za pomocą POST na adres /visit. Zakupy logujemy już za pomocą PUT /order/:id ponieważ zawierają one identyfikatory używane przez sklep, więc można traktować to raczej jako synchronizację niż tworzenie nowego zasobu.

Metoda reprice będzie zawierała dwa żądania. Pierwsze sprawdza czy są jakieś produkty, których cena powinna być zmieniona. GET /product?to_reprice=true, drugie jest wysyłane w celu potwierdzenia zmiany ceny i jest to PUT /product/:id z nową ceną.

Do wysyłania żądań HTTP wykorzystamy bardzo popularną paczkę Guzzle.

composer req guzzlehttp/guzzle "^6.3"

Jeśli to Twoje pierwsze spotkanie z tą paczką zalecam przynajmniej pobieżne zapoznanie się z jej dokumentacją przed dalszym czytaniem.

Czas zaprezentować zawartość pliku src/Api.php czyli implementację metod

  • repirce
  • logVisit
  • logBuy
<?php

const
BASH_RED = "\e[0;31m";
const BASH_END = "\033[0m";

Na początku definiujemy specjalne znaczniki do kolorowania błędów w konsoli. Ich wstawienie do ciągu znakowego i wydrukowanie w systemie Linux pozwoli na oglądanie czerwonych dobrze widocznych logów.

class Api
{
private $URL;
private $KEY;
private $client;

public function __construct()
{
$this->URL=getenv('PRECISE_SALE_URL');
$this->KEY=getenv('PRECISE_SALE_API_KEY');
$this->client = new GuzzleHttp\Client();
}

Następnie do $URL i $KEY dołącza zmienna $client zawierająca instancję klienta Guzzle. Pozwoli on nam wysyłać żądania http łatwiej niż curl.

Teraz pokażemy całkiem prostą metodę logVisit która łączy w sobie elementy takie jak przygotowanie i wysłanie żądania oraz obsługa błędu.

/**
*
@throws \GuzzleHttp\Exception\GuzzleException
*/
public function logVisit() {

$data = [
"url" => (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]",
"ip" => $_SERVER["REMOTE_ADDR"],
"userAgent" => $_SERVER["HTTP_USER_AGENT"] ?? null,
"productId" => 1
];

$res = $this->client->request('POST', $this->URL.'/visit', [
'json' => $data,
'headers' => [
'Authorization' => 'Bearer '.$this->KEY
]
]);

if($res->getStatusCode() !== 201) {
file_put_contents("php://stdout", BASH_RED."Invalid visit logging. Check URL and API KEY in .env file.".BASH_END."\n");
}
}

W zmiennej $data zapisywane są podstawowe informacje o wyświetlanym produkcie, jak jego id , czy adres url , ale też o adresie ip i urządzeniu z jakiego oglądana jest strona. Te dane są opcjonalne, ale warto je dodać, bo pozwalają na przeprowadzanie bardziej złożonych analiz.

Następnie tworzone jest żądanie wysyłane metodą POST na adres /visit zawierające te dane, oraz nagłówek Authorization z kluczem dodanym w poprzednim kroku.

Serwer powinien odpowiedzieć kodem 201, jeśli tak się nie stanie, to w konsoli w której jest postawiony serwer wyświetli się informacja o błędzie.

Identyczną konstrukcję możemy zobaczyć w metodzie logujące zakupy

/**
*
@param $state
*
@throws \GuzzleHttp\Exception\GuzzleException
*
@throws \Exception
*/
public function logBuy($state) {
$id = random_int(1, 1e10);

$data = [
"id" => $id,
"grand_total" => $state["price"],
"products" => [[
"id" => 1,
"price" => $state["price"],
"quantity" => 1
]],
];

$res = $this->client->request('PUT', $this->URL.'/order/'.$id, [
'json' => $data,
'headers' => [
'Authorization' => 'Bearer '.$this->KEY
]
]);

if($res->getStatusCode() !== 200) {
file_put_contents("php://stdout", BASH_RED."Invalid buy logging. Check URL and API KEY in .env file.".BASH_END."\n");
}
}

Różnica polega na tym, że tu losujemy id ponieważ to na sklepie ciąży obowiązek jego wytworzenia, co jest uzasadnione tym, że sklep sam zwykle zapisuje sobie zamówienia i te identyfikatory i tak już w nim istnieją.

Poza tą różnicą podobnie tworzymy zmienną$data tym razem wypełnioną szczegółami zamówienia a nie wizyty. Analogicznie wysyłamy żądanie zmieniając mu jedynie metodę na PUT i adres na /order/:id

Teraz za błąd uznamy każdy kod odpowiedzi różny od 200.

Metoda do zmiany ceny jest bardziej złożona dlatego rozbijemy ją na części:

/**
*
@param $currentPrice double
*
@return double
*
@throws \GuzzleHttp\Exception\GuzzleException
*/
public function reprice($currentPrice) {
$res = $this->client->request('GET', $this->URL.'/product?to_reprice=true', [
'headers' => [
'Authorization' => 'Bearer '.$this->KEY,
'Content-Type' => 'application/json'
]
]);

$propositions = json_decode((string) $res->getBody());

Przede wszystkim metoda ta wysyła początkowo żądanie GET na adres /product?to_reprice=true. $res->getBody() to bufor, więc rzutujemy go na ciąg znakowy i dekodujemy strukturę danych zapisaną w formacie JSON.

Jest to tablica. Trafia ona do zmiennej $propositions . W naszym przypadku będzie ona pusta lub jednoelementowa, bo mamy tylko jeden produkt. Może się jednak okazać, że algorytm nie widzi sensu zmieniania jego ceny.

if(count($propositions) < 1) {
file_put_contents("php://stdout", "Reprice finished without changes."."\n");
return $currentPrice;
}

Dokładnie taki scenariusz rozważamy w powyższej instrukcji warunkowej. Jeśli nie ma potrzeby zmieniania ceny, to metoda reprice zwraca obecną cenę i kończy swoje działanie. W przeciwnym wypadku trzeba zwrócić nową cenę, ale wcześniej poinformować serwer precise.sale, że ta cena została zmieniona.

else {

$price = $propositions[0]->proposed_price;
$id = 1;

$data = [
"id" => $id,
"price" => $price
];

$res = $this->client->request('PUT', $this->URL.'/product/1', [
'json' => $data,
'headers' => [
'Authorization' => 'Bearer '.$this->KEY,
]
]);

if($res->getStatusCode() !== 200) {
file_put_contents("php://stdout", BASH_RED."Invalid confirmation that price is changed.".BASH_END."\n");
return $currentPrice;
} else {
return $price;
}
}

Wyciągamy cenę z własności proposed_price pierwszego produktu z tablicy $propositions , ustawiamy id na 1 i aktualizujemy produkt wysyłając odpowiednie żądanie PUT. Jeśli coś pójdzie źle zwracamy poprzednią cenę, jeśli serwer zapisał zmiany, to nową.

Tak doszliśmy do końca integracji. Teraz możemy używać naszego sklepu i obserwować jak w panelu pojawiają się kolejne zakupy oraz wizyty. Niestety losowe klikanie nie odpowiada raczej zachowaniom klientów, którzy mając przeznaczyć realne pieniądze na zakupy będą uzależniać swoją decyzję o kliknięciu przycisku zakupu między innymi od tego jaka cena im się wyświetli.

Symulację takiego zachowania napiszemy w ostatnim kroku tego tutorialu.


5. Symulacja wizyt i zakupów

Postawimy sklep lokalnie komendą

php -S localhost:8020

Ruch wygenerujemy uruchamiając w drugiej konsoli skrypt

simulate_visitors.php

Będzie to super prosty skrypt, ale będzie potrzebował dwóch funkcji do swojego działania: losującej wartość true / false z zadanym prawdopodobieństwem i obliczającej prawdopodobieństwo zakupu przy danej cenie. Kluczowe jest to, że tego prawdopodobieństwa nie zna ani algorytm precise.sale ani właściciel sklepu. To że przyjmujemy jakiś model jest wynikiem chęci symulowania rynku, zadaniem precise.sale jest natomiast możliwie najszybsze, najtańsze i najdokładniejsze zbadanie tego modelu.

Oto pierwsza funkcja w pliku simulate_visitors.php

<?php

/**
* Function that give true with probability from first argument
*
*
@param float $probability
*
@param int $length
*
@return bool
*/
function trueWithProbability($probability=0.1, $length=10000)
{
$test = mt_rand(1, $length);
return $test<=$probability*$length;
}

mt_rand zwraca wartość z podanego przedziału, jeśli zmieści się ona w tym przedziale przeskalowanym przez oczekiwane prawdopodobieństwo to zwraca true, w przeciwnym wypadku false. Kolejna funkcja to konwersja

/**
* Selling in different price level
*
* // in 0 -> 0.5 |---
* // between 0 and 20 linear | ---
* // in 20 -> 0 | ---
* // greater than 20 -> 0 | ---------
*
*
@param $p
*
@return double
*/
function s($p)
{
return max(0.0, 0.5 - 1/40 * $p);
}

Zakładamy model liniowy, to znaczy, dla ceny 0, co drugi odwiedzający zamówi produkt, dla ceny 20 i wyższej nikt, a między tymi cenami prawdopodobieństwo zakupu będzie spadać liniowo.

Następnie ustawiamy adres sklepu na zgodny z tym, który ustawiliśmy w pierwszej konsoli

const SHOP_URL = "http://localhost:8020";

$visits = 0;
$buys = 0;

Pętlą symulujemy wczytania strony głównej. Odczytujemy z niej cenę, jeśli jest ona odpowiednia by odwiedzający stał się zamawiającym klika on w przycisk zakupu.

for($i=0; $i<20; $i++) {

$page = file_get_contents(SHOP_URL);

preg_match("/<span>(\d+\.?\d*)<\/span>/", $page, $matches);

$price = (float)$matches[1];

$visits++;
echo "Visits $visits\t Buys: $buys\t Price: ".$price."\n";

if (trueWithProbability(s($price))) {
file_get_contents(SHOP_URL . "/buy");
$buys++;
}

}

Tym razem zamiast Guzzle, wykorzystaliśmy file_get_contents, które pozwala korzystać z zasobów sieciowych wysyłając pod spodem żądanie typu GET zamiast ścieżki do pliku podamy adres URL.


Prezentacja wyników optymalizacji konwersji

Po wykonaniu kilku wykonaniach takiego programu powinniśmy w widoku produktu zobaczyć mniej więcej takie wykresy:

Wykresy konwersji i zysku w zależności od ceny w widoku produktu.

Oczywiście mogą one być trochę inne ze względu na losową charakterystykę symulacji, ale istotne jest to, że na pierwszym widzimy odwzorowanie konwersji w funkcji ceny, która liniowo spada.

Na drugim zysk, który zgodnie z wyprowadzonymi wcześniej wzorami powinien być maksymalny dla ceny równej 11. U rzeczywiście prezentowana parabola ma maksimum bardzo blisko tej wartości.

Wielkość kropek odpowiada ilości pomiarów (wizyt) dla danego punktu cenowego.

Po wykonaniu kilka razy tego skryptu możemy oczekiwać, że wartość ceny będzie zbiegała w okolice liczby 11. Przykładowe wyniki można zobaczyć Summarytutaj.

https://pastebin.com/raw/pJpwFNht

A ich podsumowanie prezentuje tabela:

Ceny wskazywane przez precise.sale w kolejnych iteracjach.

Łatwo policzyć, że w rozważanym tutaj scenariuszu obserwujemy prawie pięciokrotny wzrost zysku po wykonaniu 260 sprzedaży. Cena podniosła się z 3 do 10.63. Co prawda wcześniej 42.5% klientów dokonywało zakupu, a teraz konwersja wynosi tylko 22.5%, ale za to zostawiają znacznie więcej pieniędzy. Zysk dla ceny wyliczonej w ostatnim kroku jest tylko o 1.7‰ niższy od maksymalnego jaki można było osiągnąć, gdyby cena wyniosła równo 11.

Wykresy konwersji i zysku w zależności od ceny dla danych z tabeli.

W tabeli widać też bardzo niski zysk dla ostatniej wskazanej ceny. Należy podkreślić, że jest to wynik statystycznie występujących odchyleń od średniej sprzedaży i jeśli prowadzili byśmy sprzedaż po cenie 10.63 dostatecznie długo, średni zysk na wizytę wyniósł by 4.76.

Tak samo dla ceny 3.15 zaobserwowaliśmy konwersję wyższą niż wynikało by to z przyjętego modelu. Takie sytuacje są normalne i zadaniem algorytmu jej uwzględnianie niepewności pomiarowych.

Podsumowanie

Pokazaliśmy w jaki sposób napisać integrację platformy sklepowej napisanej w języku PHP z serwisem precise.sale, jak utworzyć w nim platformę sklepową, wygenerować dla niej klucz, utworzyć produkt i symulując klientów odwiedzających sklep zobaczyć jak algorytm wybiera dla niego coraz lepsze ceny.