Przejdź do treści
← Portfolio

Case study

Panel operacyjny i finansowy dla e-commerce

Case study wdrożenia. Jedna aplikacja zamiast ośmiu zakładek, arkusza i pamięci właściciela.

Kontekst

Sklep internetowy w UK sprzedający sprzęt kosmetyczny i fryzjerski w dwóch kanałach (własny sklep Shopify + eBay), zaopatrywany u dostawcy B2B w Polsce, wysyłający paczki przez Royal Mail Click & Drop, prowadzący obsługę klienta w Gmailu i przez wiadomości eBay.

Każde z tych narzędzi ma własny panel i własną wersję prawdy. Żadne nie wie o pozostałych:

  • Shopify wie, co klient zamówił — ale nie wie, ile ten towar kosztował ani czy jest w magazynie w UK.
  • eBay wie o swoim zamówieniu — ale ukrywa adres e-mail kupującego, więc jego zgłoszenie nie trafia do żadnej wspólnej skrzynki.
  • Royal Mail wie, że etykieta została opłacona — ale nie wie, którego zamówienia dotyczy.
  • Dostawca przysyła proformę mailem, fakturę udostępnia w panelu B2B, a fizyczne przyjęcie towaru odnotowuje ktoś inny w arkuszu.
  • Realny zysk nie istnieje nigdzie: żeby go policzyć, trzeba złożyć cenę zakupu, transport z Polski, wysyłkę krajową, prowizje kanału i zwroty.

Efekt: decyzje podejmowane „na czuja”, procesy trzymane w głowie jednej osoby, błędy wykrywane przypadkiem (albo przez klienta), a każde pytanie typu „ile realnie zarobiliśmy na tej kategorii” oznaczało pół dnia sklejania arkuszy.

Rozważaliśmy gotowe narzędzia analityczne dla e-commerce i zostały odrzucone świadomie — żadne nie obsługuje backorderów, kolejki pakowania, korespondencji z klientem i dokumentów zakupowych z Gmaila. To były narzędzia do oglądania sprzedaży, a nie do prowadzenia firmy.


Cel wdrożenia

Zbudować jedno źródło prawdy, które:

  1. łączy dane ze wszystkich kanałów w jeden model zamówienia, klienta i produktu,
  2. prowadzi ludzi przez proces — od zamówienia klienta, przez zakup u dostawcy, po spakowanie i wysyłkę — zamiast tylko raportować po fakcie,
  3. pokazuje realną marżę, a nie obrót,
  4. nie zrobi nic nieodwracalnego bez człowieka — bo system dotyka pieniędzy klientów, ich danych osobowych i wychodzącej korespondencji.

Architektura

Stack: Next.js 15 (App Router, TypeScript) · React 19 · PostgreSQL 16 · Prisma 6 · worker oparty o node-cron · Docker Compose · integracja z modelem Claude (Anthropic SDK) do wspomagania obsługi klienta.

Zasada nadrzędna: raw → core → prezentacja.

Każde źródło danych ląduje najpierw w bazie w postaci surowego payloadu (raw_*), dopiero potem jest normalizowane do modelu domenowego (orders, order_items, customers, shipments, purchase_orders, conversations, costs_*). Interfejs nigdy nie odpytuje API na żywo — czyta wyłącznie z bazy.

Trzy praktyczne konsekwencje tej decyzji:

  • Aplikacja działa, gdy integracja padnie. Wygasły token psuje świeżość danych, a nie cały panel.
  • Zmiana logiki nie wymaga ponownego pobrania danych. Poprawiony parser przepuszcza się przez zachowane payloady.
  • Widoki są szybkie i przewidywalne, bo to zwykłe zapytania SQL, a nie łańcuch wywołań do cudzych API z ich limitami.

Warstwa synchronizacji to worker z rejestrem zadań (11 niezależnych jobów: zamówienia Shopify, stany magazynowe, zgody marketingowe, spory płatnicze, zamówienia eBay, wiadomości eBay, Gmail, dokumenty Royal Mail, proformy i faktury dostawcy). Każdy job ma własny stan i własny bezpiecznik — awaria jednego nie zatrzymuje pozostałych i nigdy nie zgaduje kolejnego statusu. Osobna, szybka pętla odpytuje skrzynkę co 2 minuty z własnym lockiem, żeby długa synchronizacja zamówień nie zagłodziła obsługi klienta.


Co system robi — moduły

1. Zamówienia i realizacja

Wspólna lista zamówień z obu kanałów, karta zamówienia z pozycjami, historią, korespondencją, trackingiem i numerami seryjnymi. Filtry operacyjne odpowiadają na pytanie „czym trzeba się zająć dzisiaj”, a nie tylko „co się wydarzyło”.

2. Zakupy i backordery

Ilości w podziale: potrzebne / potwierdzone u dostawcy / przyjęte / pozostało kupić. Łańcuch dowodowy zakupu jest rozdzielony według źródła — proforma przychodzi mailem, faktura jest w panelu B2B dostawcy, a fizyczne przyjęcie towaru potwierdza arkusz magazynowy. System pilnuje, że żaden z tych dokumentów nie udaje pozostałych: faktura nie oznacza, że towar leży w magazynie.

3. Kolejka pakowania i wysyłki

Osobne ekrany dla właściciela (co blokuje wysyłkę: brak etykiety, brak QR, brak potwierdzenia kuriera) i dla osoby pakującej (bezpieczna kolejka „na dziś” + zaległości). Dokumenty Click & Drop są parsowane z maili potwierdzających płatność i wiązane z konkretnym zamówieniem po oficjalnym identyfikatorze z API przewoźnika — nie po heurystyce z treści maila.

4. Skrzynka obsługi klienta

Wątki z Gmaila i wiadomości kupujących eBay w jednym miejscu, obok pełnego kontekstu zamówienia. Statusy, odkładanie na później, tagi, notatki wewnętrzne, szablony z podstawianymi zmiennymi, metryki czasu reakcji.

To był moduł o najwyższym zwrocie: wiadomości eBay wcześniej w ogóle nie docierały do żadnej wspólnej skrzynki, bo platforma maskuje adres kupującego. Dodatkowo 95% ruchu w tym kanale to automaty platformy — filtr rozpoznający realne pytania kupującego był warunkiem, żeby kanał w ogóle nadawał się do obsługi.

5. Wspomaganie odpowiedzi modelem językowym

Model przygotowuje propozycję odpowiedzi na podstawie wątku, zamówienia, trackingu i firmowych szablonów. Model nie wysyła i nie zapisuje niczego. Propozycja wraca do edytora jako zwykły tekst i musi przejść dokładnie te same kontrole co treść napisana ręcznie. Reguły tagowania są natomiast celowo deterministyczne, nie modelowe — przy tej skali korespondencji model w ścieżce automatycznej dokładałby awaryjności tam, gdzie system musi po prostu działać.

6. Finanse: koszty i marża CM2

Koszty stałe i zmienne, realne ceny zakupu z faktur B2B, transport z Polski liczony po wadze, realne koszty wysyłki krajowej z rozliczeń przewoźnika, prowizje kanału. Wynik: marża po kosztach zmiennych per zamówienie, per klient i per kanał — czyli liczba, która mówi, czy sprzedaż ma sens, a nie ile pieniędzy przeszło przez konto.

7. Prognoza, retencja, Customer 360

Prognoza przepływów, wartość klienta w czasie, historia zakupowa, tożsamości klienta w wielu kanałach, ewidencja podstaw prawnych zgód marketingowych.

8. Jakość danych i rekoncyliacja

Osobny job porównuje każde zamówienie w bazie z jego aktualnym stanem w Shopify — kwotę brutto, VAT, liczbę pozycji — i raportuje rozjazdy. Ten moduł powstał po konkretnym incydencie: rozbieżność w edytowanym zamówieniu wykrył człowiek okiem, bo nic w systemie nie porównywało sum z bazy z sumami w źródle. Dziś porównuje je maszyna, przy każdym przebiegu.

9. Role i RODO

Logowanie, role, dziennik zdarzeń. Konto operacyjne widzi zamówienia, kolejkę pakowania i wysyłki, ale nie widzi finansów, banku, prognozy ani wartości klienta. Osobno zaimplementowana obsługa art. 15 i 17 RODO: eksport kompletu danych klienta jednym przyciskiem oraz redakcja zamiast kasowania — dane identyfikujące znikają, a kwoty, numery i daty zamówień zostają, bo dokumentacja podatkowa musi przetrwać żądanie usunięcia danych.


Zasady projektowe, które okazały się najważniejsze

Te decyzje są w tym wdrożeniu ciekawsze niż lista funkcji — to one odróżniają narzędzie, któremu można zaufać, od ładnego panelu.

Człowiek zatwierdza wszystko, co wychodzi na zewnątrz. Do klienta można wysłać wyłącznie wcześniej zapisany szkic; nie istnieje ścieżka „napisz i wyślij jednym kliknięciem”. Wysyłka jest domyślnie wyłączona przełącznikiem środowiskowym. Na eBay system nie odpowiada w ogóle i linkuje do panelu — bo tam nie ma szkiców, a wiadomość poszłaby natychmiast. Pilnują tego testy, które wywalają się, gdyby ktoś dołożył ścieżkę omijającą tę zasadę.

Automat może tylko zaostrzyć, nigdy poluzować. Reguła może podnieść priorytet zgłoszenia, nigdy go obniżyć. Synchronizacja zgód marketingowych nigdy nie cofa ręcznie ustawionego sprzeciwu. Ponowny import starej wiadomości nie odmyka sprawy, którą człowiek zamknął.

Idempotencja jako wymóg, nie ambicja. Wszystkie wskaźniki wątków są przeliczane z danych źródłowych, a nie inkrementowane. Powtórny import, uzupełnienie historii i ręczna akcja operatora dają dokładnie ten sam wynik. Dzięki temu „puść to jeszcze raz” jest bezpieczną odpowiedzią na większość problemów.

Każda liczba mówi, skąd pochodzi. Przy każdym skrócie i nagłówku kolumny jest podpowiedź z definicją, źródłem danych i zastrzeżeniem, czego liczba nie obejmuje. Okazało się, że większość pomyłek brała się nie z niezrozumienia nazwy, tylko z błędnego założenia, skąd wartość pochodzi — np. że prowizja kanału to kwota realna, a nie szacunek. Podpowiedź opisująca nieaktualny sposób liczenia jest gorsza niż jej brak, więc utrzymanie słownika jest częścią definicji ukończenia zadania.

Ostrzeżenie zamiast twardej blokady tam, gdzie życie jest bardziej złożone niż reguła. Gdy nadawca maila nie jest właścicielem zamówienia, system ostrzega i blokuje przycisk do świadomego potwierdzenia — zamiast zablokować na sztywno. Bo mąż pisze w sprawie zamówienia żony, a klient z drugiego adresu wciąż jest tym samym klientem. Ale bez tego ostrzeżenia szablon podstawiłby dane osobowe właściciela zamówienia pod adres obcej osoby. To jest gotowy wyciek danych i wyszedł dopiero na teście z użytkownikiem, nie w kodzie.

Jeden zapis, jeden właściciel. Dashboard stany magazynowe wyłącznie czyta; zapisy należą do osobnej automatyzacji. Dwa systemy piszące ten sam stan rozjeżdżają magazyn po cichu, a taki błąd wychodzi dopiero przy brakującej paczce.


Efekt dla firmy

Dla właściciela

  • realna marża zamiast obrotu — per zamówienie, per klient, per kanał,
  • jedno miejsce, w którym widać, co blokuje wysyłkę i co trzeba kupić,
  • rozjazdy między systemami zgłasza maszyna, a nie klient,
  • decyzje (np. co się opłaca sprzedawać w danym kanale) oparte na liczbie, nie na przeczuciu.

Dla obsługi klienta

  • jedna skrzynka zamiast dwóch platform i wyszukiwania w Gmailu,
  • pełen kontekst zamówienia obok wiadomości — bez przełączania się między panelami,
  • szablony i propozycje odpowiedzi skracające czas przygotowania odpowiedzi,
  • kanał eBay w ogóle stał się obsługiwalny,
  • mierzalny czas reakcji, liczony medianą, a nie średnią zniekształconą przez jeden zapomniany wątek.

Dla osoby pakującej

  • kolejka „co pakujemy dzisiaj”, w której nie da się wysłać paczki bez kompletu dokumentów,
  • dostęp ograniczony do tego, co potrzebne do pracy — bez wglądu w finanse firmy.

Dla zgodności z prawem

  • obsługa żądań dostępu i usunięcia danych jednym narzędziem, zamiast ręcznego przeszukiwania ośmiu tabel,
  • ewidencja podstawy prawnej zgód marketingowych,
  • dziennik zdarzeń dla wrażliwych operacji.

Skala techniczna

Ekrany aplikacji21
Modele danych51
Migracje bazy32
Niezależne joby synchronizacji11
Zintegrowane systemy zewnętrzneShopify Admin, eBay (REST + legacy Trading API), Gmail, Royal Mail Click & Drop, Google Sheets, panel B2B dostawcy, bank, Anthropic API
Kod aplikacji~29 000 linii TypeScript

Podejście do pracy

Projekt był prowadzony przyrostowo: kolejne etapy wdrażane niezależnie, każdy za własnym bezpiecznikiem, z możliwością wyłączenia bez ruszania reszty. Decyzje projektowe i pułapki konkretnych API są dokumentowane w repozytorium, przy kodzie — łącznie z analizami po incydentach. Dzięki temu wiedza o tym, dlaczego coś jest zrobione tak, a nie inaczej, nie mieszka w niczyjej głowie.

Świadome ograniczenia zakresu: system działa lokalnie, na maszynie właściciela, z bazą dostępną wyłącznie z tego komputera. Wystawienie go do internetu jest osobnym etapem z własnymi wymaganiami (pełny model użytkowników, HTTPS, dostęp za bramką, kopie zapasowe) i nie zostało zrobione „przy okazji”, bo w bazie są treści korespondencji z klientami.


Co to pokazuje

Że mała firma e-commerce może mieć system operacyjny skrojony dokładnie pod swój proces — z backorderami, kolejką pakowania i realną marżą — zamiast dopasowywać proces do tego, co akurat oferuje gotowa aplikacja. I że przy takim systemie najtrudniejsza część to nie napisanie ekranów, tylko uczciwe potraktowanie danych: wiedzieć, skąd pochodzi każda liczba, nie pozwolić automatowi podjąć decyzji, która należy do człowieka, i zbudować mechanizm, który sam zgłasza, że coś się rozjechało.

Potrzebujesz podobnego rozwiązania?

Napisz do mnie ↗