Kontekst: 7. projekt w cyklu 3 miesięcy
Klient prowadzi studio usługowe w Szwajcarii. System zarządzania zbudowany na platformie low-code (Base44) plus własne webhooks na Deno Deploy. Pracujemy razem od trzech miesięcy — relacja zaczęła się od jednej małej naprawy (zerwana integracja z Telegramem) i rozrosła się do siedmiu kolejnych projektów, każdy w fixed-price.
Wcześniejsze fazy: parowanie bota, zabezpieczenie wypłat (atomowość operacji), 3-poziomowe RBAC, drugi sklonowany system dla drugiej lokalizacji, osobny bot powiadomień, refaktor logiki wypłat z przeglądarki na backend. Ten case study to faza 7 — integracja maila compliance.
Powracający wzorzec: stany UI, które wyglądają jak odpowiedzi, ale nie są. Ten bug to klasyczny przykład.
Problem
Klientka zgłosiła precyzyjnie: niedawno dezaktywowana pracownica z 90-dniowym pozwoleniem nie wygenerowała
spodziewanego maila compliance do urzędu zewnętrznego. W panelu jej status wyglądał jak „nie odwiedziła" —
czyli warunek wysłania maila. W rzeczywistości pole było puste (null), a automatyzacja
odpalała się tylko na jawne "not_visited".
Honest framing: to była luka w mojej własnej implementacji sprzed dwóch tygodni.
Zaprojektowałem ścieżkę dla jawnego "not_visited" i nie pomyślałem, że pole może być puste —
a takie było dla każdego nowo utworzonego profilu, dopóki ktoś nie kliknął przycisku w panelu.
Naprawę zaabsorbowałem w cenie pierwotnego projektu jako warranty.
Diagnoza
Bug to góra lodowa. Pierwszy obiecany scope miał 5 punktów. Po wdrożeniu pierwszej zmiany (sekcji UI dla profili z pustym statusem) panel sam wyrenderował 11 historycznych rekordów z tym samym problemem. Klientka znała 4. System znalazł 14.
Po drodze wyszło dodatkowo:
- 3 wcześniej nieznane nowe profile utworzone w dniach przed pojawieniem się buga — ten sam pusty status, ten sam problem.
- Bug platformy: operator
is_emptynie matchowałnull, tylko pustych stringów. Pierwsza automatyzacja auto-default nie odpalała się w testach. - Bug klasyfikatora UI: dziedziczony fallback w panelu (
return 'not_visited') maskował puste pola jako „nie" — ten sam mechanizm, który ukrył oryginalnego buga. - Odkrycie warte złota: testy end-to-end przez agenta AI platformy (server-role) nie triggerują automatyzacji entity. Tylko operacje user-context. To wyjaśniło kilka wcześniejszych dziwnych wyników testów i zmieniło moją metodologię.
Rozwiązanie
1. Filtr funkcji mailowej
Przepisany filtr akceptuje null i pusty string jako „nie odwiedziła" — pod warunkiem,
że pozwolenie to 90-dniowe. Dwa stany prawdy: jawne i implicite.
2. Auto-default na nowych profilach
Automatyzacja inicjalizuje pole statusu na "not_visited" w momencie utworzenia profilu
z 90-dniowym pozwoleniem. Pole nigdy więcej nie jest puste „przypadkiem".
3. Nowa sekcja UI
Dedykowana sekcja w panelu dla profili z pustym statusem, z odrębnym kolorem. Klient widzi, co system nie wie, a nie tylko to, co system wie. Fix na poziomie wizualnym + naprawa klasyfikatora w kodzie panelu.
4. Migracja danych historycznych
14 zidentyfikowanych rekordów wyciągniętych do ręcznego przeglądu klienta — case-by-case, nie auto-resolve. Klient sam decyduje, co z każdym przypadkiem.
5. End-to-end test przez prawdziwą ścieżkę
Test wykonany przez interfejs jak realny użytkownik (utworzenie profilu testowego, dezaktywacja, weryfikacja maila compliance). Pass.
Efekt
Klient zapłacił cenę pierwotnego projektu. Otrzymał:
- 5 obiecanych punktów scope, wszystkie działają
- 3 wcześniej nieznane profile poprawnie zainicjalizowane jako efekt due diligence
- 11 historycznych rekordów wyciągniętych do przeglądu (nie auto-resolve — to jego decyzja)
- 2 bugi platformy znalezione i obejście / eskalacja do AI platformy
- Bug UI klasyfikatora poza explicit scope, ale konieczny żeby nowa sekcja w ogóle miała sens
Realny czas: ~2,5x szacunku dla pierwotnych 5 punktów. Część to warranty na moim wcześniejszym kodzie. Reszta to discovery — rzeczy, których nie było na liście, ale bez których lista by nic nie znaczyła.
Wnioski
Pięć lekcji, które zabieram z tego projektu do każdej kolejnej fixed-price:
- Stany UI, które wyglądają jak odpowiedzi, ale nie są, to fundamentalne ryzyko jakości danych. Naprawa wymaga zmian na trzech warstwach: model danych (inicjalizacja), klasyfikator UI (rozróżnienie pustego od „nie"), prezentacja wizualna (osobna sekcja). Sam jeden poziom nie wystarcza.
- End-to-end znaczy end-to-end — przez tę samą ścieżkę wykonania, którą używają realni użytkownicy. Pół dnia zmarnowane na testach przez kanał, który nie odpalał automatyzacji.
- Discovery rider w każdym fixed-price. Analiza zawsze niedoszacowuje scope, bo problemy z jakością danych wychodzą dopiero gdy system zaczyna ich szukać. Następne kontrakty będą miały zdanie: „jeśli analiza ujawni dodatkowe rekordy lub systemowe problemy poza listą, będą wycenione osobno".
- Warranty to warranty — nazywać po imieniu. Absorbcja rework w cenie pierwotnej to świadomy wybór, nie milczące rozszerzenie zakresu. Komunikować jasno.
- Korespondencja klienta to source of truth. Każda reguła biznesowa w tym projekcie sięgała do konkretnego cytatu z wiadomości klienta — czasem sprzed dwóch miesięcy. Nie zgadywać tam, gdzie można sprawdzić.
Trzy miesiące pracy z tym klientem to najcenniejszy aktyw tej współpracy. Ten projekt jest przykładem, gdzie wybrałem zaabsorbować dodatkowy koszt w zamian za czystą dostawę i kontynuację relacji. Zrobiłbym to samo jeszcze raz — ale następnym razem komunikuję trade-off jaśniej.