Medusa.js v2 jako fundament marketplace Artovnia z systemem Share & Save, panelem sprzedawcy, kodem QR i obniżką prowizji z 18% do 13%.

W marketplace pytanie o to, kto przyprowadził klienta, ma wymiar finansowy. Część ruchu pochodzi z SEO, reklam i social mediów samej platformy, część przyprowadzają sprzedawcy: z Instagrama, własnej strony, targów, wizytówki albo kodu QR dołączonego do paczki. Przy jednakowej prowizji w obu przypadkach sprzedawca nie ma powodu, żeby kierować swój ruch do marketplace'u.

W Artovnii rozwiązałem to mechanizmem Share & Save. Jeżeli sprzedawca sam przyprowadzi klienta, a zakup zostanie poprawnie przypisany do jego linku, prowizja spada z 18% do 13% + VAT. Z zewnątrz wygląda to jak prosty system afiliacyjny, ale w sklepie z wieloma sprzedawcami trzeba rozwiązać atrybucję kliknięcia, podział koszyka między sprzedawców, dynamiczne naliczanie prowizji, ochronę przed ponownym użyciem tego samego kliknięcia, race conditions, idempotencję, retry po błędach, audyt oraz wpływ korekty na wypłaty i faktury. Całość działa na Medusa.js v2, Next.js i własnej warstwie marketplace Artovnii.

Czym Share & Save jest, a czym nie

Share & Save nie jest kodem rabatowym dla klienta. Cena produktu pozostaje bez zmian, a korzyść trafia do sprzedawcy, bo to on wykonał pracę związaną z pozyskaniem kupującego. Druga zasada jest równie ważna: jedno przypisane kliknięcie obniża prowizję dokładnie jednego zamówienia. Mechanizm nie uruchamia tygodnia niższej stawki ani statusu w programie lojalnościowym, premiuje konkretną sprzedaż.

Obniżka wyrażona jest w punktach procentowych:

typescript
const STANDARD_COMMISSION_RATE = 18 // %
const SHARE_SAVE_COMMISSION_RATE = 13 // %
const COMMISSION_REDUCTION_POINTS = 5 // p.p.

System ma też minimalną dopuszczalną stawkę prowizji, dzięki czemu Share & Save współistnieje z innymi mechanizmami bez zejścia poniżej progu zaakceptowanego w modelu biznesowym platformy. Liczba punktów obniżki, minimalna prowizja, długość okna atrybucji i stan programu są parametrami, które administrator zmienia bez deploymentu aplikacji.

Medusa.js daje fundament, nie gotowy marketplace

Medusa.js daje produkty, koszyki, zamówienia, workflow, eventy, moduły, własne API i rozszerzalny panel administracyjny. Prowizje per sprzedawca, seller payouts, split orders, programy poleceń i atrybucja sprzedaży to logika domenowa konkretnej platformy i trzeba ją napisać samodzielnie.

W Artovnii podstawowe naliczenie prowizji jest jednym etapem procesu, a Share & Save działa później jako dodatkowy finalizator, który modyfikuje wyliczoną wcześniej prowizję konkretnej pozycji zamówienia. Nie zbudowałem drugiego, równoległego systemu rozliczeń obok istniejącego, więc wypłaty i faktury nie muszą wiedzieć, dlaczego prowizja wynosi 13% zamiast 18%. Dostają jedną końcową wartość.

Każdy sprzedawca może udostępnić link prowadzący do swojego profilu albo konkretnego produktu, w formacie /sklep/{seller-handle}. Ten sam adres trafia do linku kopiowanego z panelu, kodu QR, materiałów do druku i postów w mediach społecznościowych. Kiedy wchodzi przez niego prawdziwy użytkownik, storefront tworzy unikalny identyfikator kliknięcia.

Zwykły parametr URL w rodzaju ?seller=123 nie nadaje się jako podstawa decyzji finansowej, bo użytkownik może go dowolnie zmienić. Dlatego kliknięcie jest podpisywane:

typescript
type AttributionPayload = {
  sellerId: string
  clickId: string
  issuedAt: number
  source: string
}

// podpisany payload ląduje w cookie HttpOnly
const cookieValue = sign(payload, process.env.ATTRIBUTION_SECRET)

UTM-y mogą się pojawić obok, ale służą wyłącznie analityce. O prowizji nie decyduje UTM, bo analytics da się zablokować albo zmodyfikować, a proces finansowy potrzebuje mocniejszego źródła prawdy.

1 / 1
główna strona dasbhoard w artovnia.com, marketplace sztuki i rękodzieła na bazie medusa.js v2

Kliknij, aby powiększyć

Preview boty psują zwykłe przekierowanie

Link udostępniony na Facebooku, LinkedInie czy w komunikatorze zwykle najpierw odwiedza crawler, który pobiera tytuł, opis, og:image, og:url i pozostałe Open Graph metadata. Gdyby taki bot dostał zwykłe przekierowanie do kanonicznego adresu produktu, platforma społecznościowa mogłaby zacząć udostępniać już docelowy URL, a razem z nim znikłaby atrybucja. Link przechodziłby ręczne testy i przestawał działać dokładnie tam, gdzie ma być używany najczęściej.

Preview boty dostają więc HTML z odpowiednimi Open Graph metadata, przy zachowaniu adresu Share & Save jako tego, który krąży w social mediach. Właściwy proces przypisania kliknięcia uruchamia dopiero prawdziwy użytkownik.

Krok 2: atrybucja trafia do koszyka Medusa

Cookie w przeglądarce nie wystarczy, informacja o kliknięciu musi dotrzeć do backendu. Storefront synchronizuje podpisaną atrybucję z koszykiem przez dedykowany endpoint Store API i zapisuje ją w metadata koszyka, w kilku momentach procesu zakupowego, między innymi przed finalizacją checkoutu.

To nie czyni z cart.metadata zaufanego źródła danych. Request do API kontroluje klient, więc metadata można próbować podmienić. Storefront odpowiada wyłącznie za transport kontekstu, decyzję o pieniądzach podejmuje backend. Podczas finalizacji zamówienia Share & Save sprawdza jeszcze raz podpis, tożsamość sprzedawcy, czas kliknięcia, jego wcześniejsze wykorzystanie, typ prowizji, minimalną stawkę oraz to, czy nie jest to zakup własny sprzedawcy. Dopiero po przejściu całej walidacji prowizja może zostać zmniejszona.

Krok 3: koszyk z wieloma sprzedawcami

W zwykłym sklepie można powiedzieć, że zamówienie pochodziło z kampanii X. W marketplace klient trafia przez link sprzedawcy A, ale w jednej sesji kupuje produkt od sprzedawcy A, produkt od sprzedawcy B i produkt od sprzedawcy C. Obniżenie prowizji całej trójce nie miałoby sensu, bo klienta przyprowadził tylko sprzedawca A.

Share & Save nie działa więc globalnie na koszyk, obejmuje wyłącznie pozycje sprzedawcy przypisanego do kliknięcia, a pozostali twórcy rozliczają się według swoich normalnych zasad. Koszyk z wieloma sprzedawcami jest w Artovnii rozdzielany na osobne zamówienia. Kontekst Share & Save przechodzi razem z zamówieniem, ale finalizator i tak sprawdza, czy aktualne zamówienie należy do właściwego sprzedawcy. Jeżeli nie, prowizja zostaje bez zmian.

Krok 4: jedno kliknięcie, jedno zamówienie

Dwa niemal równoległe zamówienia mogą korzystać z tego samego clickId. Oba w tej samej chwili pytają bazę, czy kliknięcie zostało już wykorzystane, oba dostają odpowiedź przeczącą i oba przyznają niższą prowizję. Reguła „jedno kliknięcie, jedno obniżone zamówienie" przestaje wtedy obowiązywać, bo między odczytem a zapisem zostaje miejsce na race condition:

typescript
// niewystarczające: odczyt i zapis to dwie osobne operacje
const used = await clickService.isUsed(clickId)

if (!used) {
  await commissionService.applyShareSave(orderLineId, clickId)
}

Kliknięcie trzeba zająć atomowo. W Artovnii powstaje do tego claim z unikalnym kluczem wyprowadzonym z identyfikatora kliknięcia, więc o tym, który proces wygrał, rozstrzyga baza danych:

typescript
try {
  await clickClaimService.create({ clickId, orderId })
} catch (error) {
  if (isUniqueViolation(error)) {
    return skip("click_already_used")
  }

  throw error
}

Claim utworzy tylko jedno zamówienie, drugie dostanie informację, że kliknięcie zostało zużyte. W logice finansowej taka gwarancja jest warta więcej niż założenie, że dwa requesty raczej nie przyjdą w tym samym momencie.

Krok 5: dynamiczne przeliczenie prowizji

Po przejściu wszystkich warunków nowa stawka liczona jest w uproszczeniu tak:

typescript
const newRate = Math.max(currentRate - reductionPoints, minimumRate)

// currentRate     = 18
// reductionPoints = 5
// minimumRate     = 13
// newRate         = 13

Share & Save pracuje na bieżącej prowizji, nie na pierwotnej stawce sprzedawcy. Na wynik mogły wcześniej wpłynąć promocje prowizji, program poleceń, indywidualne warunki sprzedawcy albo inne korekty finansowe. Gdyby każdy z tych mechanizmów liczył swoją obniżkę od wartości początkowej, końcowy wynik szybko stałby się nieprzewidywalny. Kolejność finalizatorów jest więc jawna, a każda korekta pracuje na stanie pozostawionym przez poprzednią.

Idempotencja i retry

Workflow w commerce nie wykonuje się dokładnie raz. Przerwać go może timeout, restart aplikacji, błąd zewnętrznego serwisu, zerwane połączenie, awaria workera albo ponowienie joba. Każda decyzja Share & Save dostaje więc własny klucz idempotencji, a ponowne wykonanie tego samego procesu odtwarza wcześniejszy wynik zamiast naliczać obniżkę drugi raz:

typescript
const idempotencyKey = `share-save:${orderLineId}:${clickId}`
const existing = await decisionStore.get(idempotencyKey)

// retry zwraca tę samą decyzję: 18% -> 13%, nigdy 18% -> 13% -> 8%
if (existing) {
  return existing
}

Jeżeli finalizator prowizji zakończy się błędem, zamówienie nie zostaje w nieznanym stanie, tylko trafia do procesu retry. Kolejne wykonanie odtwarza decyzje zapisane już poprawnie i rozstrzyga brakujące elementy, a po serii nieudanych prób sprawa trafia do ręcznego przejrzenia. Dzięki temu chwilowa awaria nie zamienia się w błąd finansowy, który trzeba potem odtwarzać ręcznie dla całego zamówienia.

Audit trail: skąd się wzięło 13%

Pokazanie w panelu samej wartości Commission: 13% nie wystarczy, bo po kilku tygodniach ktoś zapyta, dlaczego to zamówienie ma inną stawkę niż standardowe 18%. Każda decyzja Share & Save trafia więc do audytu razem z identyfikatorem kliknięcia, jego źródłem, stawką przed korektą i po niej, kwotą obniżki oraz parametrami programu obowiązującymi w chwili decyzji.

Rejestruję również przypadki, w których obniżki nie zastosowano, wraz z powodem:

typescript
type ShareSaveSkipReason =
  | "unverified_click"
  | "window_expired"
  | "self_purchase"
  | "click_already_used"
  | "flat_rate"
  | "rate_at_floor"
  | "no_commission_line"

Administrator nie musi odtwarzać zdarzeń z logów aplikacji, bo system sam odpowiada, dlaczego decyzja wypadła tak, a nie inaczej. W logice finansowej auditability waży tyle samo co poprawne obliczenie.

Wypłaty i faktury bez osobnej integracji

Share & Save kończy pracę na wspólnym modelu prowizji. Poprawnie zmieniona wartość końcowa wystarcza payoutom, fakturom, dashboardom finansowym i raportowaniu, które korzystają dokładnie z tych samych danych co wcześniej. Nie powstają dwa równoległe światy rozliczeń, jeden zwykły i jeden dla Share & Save. Jest jedna końcowa prowizja i audit trail wyjaśniający, skąd się wzięła.

Generator materiałów dla sprzedawcy

Atrybucja niewiele daje, jeśli sprzedawca nie ma prostego sposobu, żeby z niej skorzystać. W panelu sprzedawcy Artovnii jest sekcja, która generuje link, kod QR, materiały do druku i grafiki do social mediów. Sprzedawca wykorzystuje je na Instagramie, Facebooku, w paczce z zamówieniem, na targach, na stoisku albo na wizytówkach.

Rozmowa na targach może dzięki temu zamienić się w mierzalną sprzedaż internetową: klient skanuje kod QR, przechodzi przez link Share & Save, system zapisuje atrybucję, a jeżeli później dojdzie do poprawnego zakupu, sprzedawca płaci niższą prowizję.

1 / 1
generator materiałów marketingowych i qr na artovnia.com, marketplace na bazie medusa.js v2

Kliknij, aby powiększyć

Konfiguracja zamiast wartości zapisanych w kodzie

Parametrami programu administrator zarządza z panelu:

typescript
interface ShareSaveConfig {
  is_enabled: boolean
  commission_reduction_points: number
  min_commission_rate: number
  attribution_window_days: number
}

Zmiana modelu biznesowego nie wymaga edycji kodu, nowego release'u i deploymentu backendu. Zamówienie zachowuje przy tym parametry obowiązujące w momencie rozstrzygnięcia, więc zmiana z 5 p.p. na 4 p.p. nie przelicza starych zamówień, a audit trail nadal pokazuje stawki faktycznie użyte przy ich rozliczeniu.

Last-click i świadoma rezygnacja z cross-device

Share & Save korzysta z modelu last-click. Jeżeli klient odwiedzi najpierw link sprzedawcy A, a potem poprawny link sprzedawcy B, aktualnym przypisaniem zostaje sprzedawca B. Nie utrzymuję historii wszystkich touchpointów tylko po to, żeby rozstrzygnąć prowizję. Jednoznaczność daje sprzedawcom czytelne zasady i łatwiej ją audytować niż rozbudowany model marketing attribution.

Podobnie wygląda sprawa z urządzeniami. Jeżeli klient zeskanuje kod QR na telefonie, a potem sam otworzy Artovnię na laptopie, system nie łączy obu sesji, bo cookie zostaje na urządzeniu, na którym nastąpiło kliknięcie. Nie wdrażałem fingerprintingu ani cross-device identity matching. Bardziej agresywną atrybucję da się zbudować, tylko jej koszt i złożoność nie odpowiadały wartości, jaką by dała.

Prowizja jako osobna domena

Na początku projektu prowizja w marketplace wygląda zwykle tak:

typescript
const commission = orderTotal * 0.18

Po kilku miesiącach produkcji dochodzą różne stawki sprzedawców, minimalne prowizje, promocje, programy poleceń, zwroty i zwroty częściowe, korekty, split orders, różne podstawy podatkowe, retry i reconciliation. Prowizja przestaje być procentem zapisanym w tabeli sprzedawcy i staje się osobnym procesem domenowym.

Share & Save zaprojektowałem właśnie w ten sposób. Nie jako wyjątek w checkoucie

typescript
if (shareSave) {
  commission = 13
}

ale jako kolejną audytowalną regułę pracującą na wspólnym systemie prowizji. Kolejne rozszerzenia nie wymagają przez to przepisywania całej logiki checkoutu.

Czego użyłem z Medusa.js

Share & Save nie jest funkcją Medusa.js, to customowa logika marketplace Artovnii. Framework dał mi elementy, z których dało się ją złożyć: Store API i własne endpointy, cart metadata i order metadata, workflow hooks, własne moduły i modele danych, eventy, rozszerzenia panelu administracyjnego, background jobs oraz integrację z istniejącym systemem zamówień.

Odpowiedzialności rozkładają się dzięki temu na kilka warstw, zamiast lądować w jednym ogromnym checkout handlerze. Storefront pozyskuje i przenosi atrybucję, backend ją weryfikuje, system prowizji podejmuje decyzję finansową, audit trail zapisuje wynik, retry odpowiada za recovery, a pozostałe moduły korzystają już tylko z wartości końcowej.

Czy Medusa.js nadaje się do marketplace

Gotowego przełącznika „multi-vendor" w Medusie nie ma. Jest za to architektura, w której da się zbudować własną logikę marketplace, i w takich wdrożeniach framework pokazuje swoje zalety. Trudnością szybko przestaje być katalog produktów czy checkout, zaczynają nią być prowizje, payouty, split orders, onboarding sprzedawców, zwroty, moderacja, reconciliation, integracje i compliance.

Share & Save dobrze to pokazuje. Dla sprzedawcy jest to „udostępnij link i zapłać niższą prowizję". Po stronie systemu wygląda tak:

typescript
// signed attribution -> storefront -> cart -> multi-vendor split
// -> order validation -> click claim -> commission adjustment
// -> audit trail -> payouts / invoices

Ta druga część decyduje o tym, czy funkcja pozostanie poprawna przy tysiącach zamówień, błędach sieciowych i kolejnych zmianach w modelu biznesowym. Jeżeli planujesz marketplace na Medusa.js, warto wcześnie odpowiedzieć sobie na pytanie, które procesy odróżniają go od zwykłego sklepu i jak je zaprojektować, żeby przetrwały ten wzrost.

Budujesz sklep albo marketplace na Medusa.js?

Projektuję i wdrażam platformy e-commerce na Medusa.js v2: od sklepów z customowym storefrontem po marketplace dla wielu sprzedawców, razem z systemami prowizji i payoutów, własnymi workflow i integracjami z zewnętrznymi systemami. Artovnia jest jednym z takich wdrożeń.

Sklep internetowy na Medusa.js

Marketplace dla wielu sprzedawców na Medusa.js

Zobacz case study Artovnii

Porozmawiajmy o projekcie

1 / 1
powtórka hero

Kliknij, aby powiększyć

FAQ

Najczęściej zadawane pytania

Medusa.js nie ma gotowego trybu multi-vendor, który włącza się przełącznikiem. Daje natomiast API, moduły, workflow i eventy pozwalające zbudować własną logikę marketplace: prowizje per sprzedawca, podział koszyka na zamówienia, payouty czy atrybucję sprzedaży. Tę warstwę trzeba napisać jako logikę domenową konkretnej platformy.

Cena produktu dla klienta pozostaje bez zmian. Obniżka dotyczy prowizji, którą marketplace pobiera od sprzedawcy, i tylko wtedy, gdy zakup został przypisany do jego linku. Jedno przypisane kliknięcie obniża prowizję dokładnie jednego zamówienia.

Obniżka obejmuje wyłącznie pozycje sprzedawcy przypisanego do kliknięcia. Pozostali sprzedawcy w tym samym koszyku rozliczają się według swoich normalnych stawek. Koszyk jest rozdzielany na osobne zamówienia, a finalizator prowizji sprawdza, czy dane zamówienie faktycznie należy do właściwego sprzedawcy.

Samo sprawdzenie w bazie przed zapisem zostawia miejsce na race condition. Zamiast tego powstaje claim z unikalnym kluczem wyprowadzonym z identyfikatora kliknięcia, więc o tym, które zamówienie wykorzystało kliknięcie, rozstrzyga baza danych. Drugi proces dostaje informację, że kliknięcie zostało już zużyte.