# TECH_CHANGELOG > Chronologiczny log zmian technicznych — co i dlaczego. ## 2026-04-22 - Alert klienta z historia zwrotow (Phase 106) **Powod**: Operator wysylkowy nie widzial wczesniej, ze kupujacy juz raz nie odebral przesylki (`delivery_status='returned'`) zanim wyslal kolejna paczke — generowalo to kolejne koszty wysylki i magazynowania. **Zmiany**: - `src/Modules/Orders/OrdersRepository.php`: - nowa metoda prywatna `customerReturnedCountSubquerySql(orderAlias, addressAlias)` — generuje correlated subquery zliczajaca inne zamowienia klienta biezacego wiersza z paczka `returned`. - `buildListSql()` — dodana kolumna `customer_returned_count` (EXISTS-style COUNT DISTINCT) do SELECT listy zamowien. - `transformOrderRow()` — przekazuje `customer_returned_count` do wiersza. - `findDetails()` — JOIN `order_addresses` typu customer + subquery `customer_returned_count`; zwraca rowniez `buyer_email`, `buyer_phone`, `buyer_name` w `$order`. - `src/Modules/Shipments/ShipmentPackageRepository.php`: - nowa metoda `findReturnedByCustomer(array customer, int excludeOrderId, int limit=10)` — lista zwroconych paczek klienta (match OR: email lower+trim, phone tylko cyfry >=6, name lower+trim), sortowana po dacie malejaco. - `src/Modules/Orders/OrdersController.php`: - `toTableRow()` — dodano badge `zwroty: N` w kolumnie buyer + klasa `is-risk-return` na `` gdy `customer_returned_count >= 1` (kompozycja z klasa aged orders z Phase 101). - `show()` — oblicza `$customerRiskInfo` i przekazuje do widoku. - nowe metody prywatne: `buildCustomerRiskInfo(order, orderId)`, `composeCustomerRiskText(count, email, phone, name)` — budowa tresci alertu zaleznie od dostepnosci pol (phone+email / email / phone / name). - `resources/views/orders/show.php` — banner `customer-risk-banner` u samej gory karty szczegolow (pod naglowkiem, nad flash messages i status change), z `
` rozwijajacym liste zamowien ze zwrotem (order_id, data, tracking, provider). - `resources/scss/modules/_customer-risk-alert.scss` (nowy modul): - `.customer-risk-banner` + `__icon`, `__body`, `__text`, `__list`, `__table` — czerwony banner z pastelowym tlem i lewym paskiem 4px. - `.risk-return-badge` — maly inline badge przy buyer name. - `tr.is-risk-return` — lewy pasek wiersza w tabeli zamowien. - `resources/scss/app.scss` — `@use "modules/customer-risk-alert"`. - `public/assets/css/app.css` — rebuild przez `npm run build:css`. **Wymagania**: - MySQL 8.0+ (REGEXP_REPLACE w subquery matching phone). - Wynik licznika wyliczany on-the-fly (brak migracji DB, brak materializacji). Indeksy na `order_addresses(order_id, address_type)` i `shipment_packages(order_id, delivery_status)` sugerowane jesli lista zamowien przekroczy ~50k wierszy — zglosic w kolejnym planie. **Anti-fraud/false positive**: - Self-exclusion: `sp.order_id != o.id` — biezace zamowienie nie wlicza sie do licznika. - Minimum phone length 6 cyfr — eliminuje match na "", "+48", krotkich fragmentach. - OR matching po email/phone/name moze dac fałszywe pozytywy dla popularnych imion; swiadome odstepstwo (user wymagal szerokiego matchingu). ## 2026-04-22 - Mapowanie statusow dostawy: wykrywanie niezmapowanych + runtime overrides **Powod**: zamowienia (np. OP000000357, OP000000638) pokazywaly `delivery_status=unknown`, mimo ze apaczka API zwracala `RETURNED_TO_SHIPPER`. UI `/settings/delivery-status-mappings` pokazywalo wylacznie statusy obecne w defaultach kodu — nowe raw statusy kuriera nie mialy gdzie zostac przypisane bez zmiany kodu. **Faza A — quick fix (defaulty + backfill)**: - `src/Modules/Shipments/DeliveryStatus.php`: dodano 3 brakujace mapowania: - apaczka `RETURNED_TO_SHIPPER` -> `returned` - apaczka `PICKUP` -> `in_transit` - allegro_wza `collected_from_sender` -> `in_transit` - `database/migrations/20260422_000101_backfill_delivery_status_unknowns.sql`: backfill 11 paczek (3 + 7 + 1). Bez trigger `shipment.status_changed` (backfill starych rekordow, nie runtime event). **Faza B — rozwiazanie systemowe**: - `DeliveryStatusMappingRepository::listUnmappedRawStatuses(provider, knownKeys)` — zwraca raw statusy z `shipment_packages` ktore nie wystepuja w defaultach ani overrides DB; z licznikiem paczek i ostatnim wystapieniem. - `DeliveryStatusMappingController::index` — przekazuje `unmappedRawStatuses` do widoku; rowniez wlacza do listy overrides, ktore nie maja odpowiadajacego defaultu (user moze dodac completely custom raw statusy). - `resources/views/settings/delivery-status-mappings.php` — nowa sekcja „Niezmapowane statusy wykryte w systemie (N)" z form submit do `save-bulk`. Pomaranczowy akcent aby wyroznic od defaultow. - `ShipmentTrackingHandler` — dostal `?DeliveryStatusMappingRepository` w konstruktorze; po kazdym `service->getDeliveryStatus()` wywoluje `DeliveryStatus::normalizeWithOverrides(provider, raw, overrides)` jesli overrides istnieja. Dzieki temu override z UI dziala runtime bez zmian kodu. - `CronHandlerFactory` — przekazuje `DeliveryStatusMappingRepository` do `ShipmentTrackingHandler`. **Faza C — badge w menu**: - `Application` — dodano statyczny holder `self::$instance` + `Application::instance()`, aby layout mial dostep do kontenera. - `DeliveryStatusMappingRepository::countAllUnmappedForBadge()` — zlicza niezmapowane raw statusy dla wszystkich providerow UI (inpost, apaczka, allegro_wza); cache per-request. - `resources/views/layouts/app.php` — badge pomaranczowy przy linku „Mapowanie statusow dostawy" z liczba niezmapowanych statusow (jesli > 0). Try/catch — brak badge'a nie psuje layoutu. - `resources/scss/app.scss` — klasa `.sidebar__badge`. **Efekt**: user nie musi modyfikowac kodu przy kazdym nowym statusie kuriera. Badge sygnalizuje pojawienie sie nieznanych statusow; sekcja na stronie mapowan pozwala przypisac je do znormalizowanych kategorii. Cron po nastepnym tick'u automatycznie przeliczy istniejace paczki zgodnie z override. ## 2026-04-19 - Statystyki zamowien (menu + raport dzienny) - Dodano nowy modul `Statistics`: - `OrdersStatisticsController` (obsluga filtrow i render strony `/statistics/orders`). - `OrdersStatisticsRepository` (agregacje dzienne po kanalach i grupach statusow). - Dodano nowa pozycje menu: `Statystyki -> Zamowienia`. - Dodano widok raportowy z filtrem zakresu dat, multiselectem kanalow i multiselectem grup statusow. - Dodano tabele dzienna z metrykami `Ilosc`, `Netto`, `Brutto` per kanal oraz stopka `Razem`. - Dodano tlumaczenia `statistics.orders.*` i `navigation.statistics*`. - Brak zmian migracyjnych i brak zmian schematu bazy danych. ## 2026-04-19 - Fix: Statystyki nie pokazywaly zamowien (kolizja collation) - `OrdersStatisticsRepository::channelSql()` generowal wyrazenie `CONCAT("shoppro:", CAST(integration_id AS CHAR))`, ktore w MySQL dawalo wynik z collation `utf8mb4_bin`. W zestawieniu z parametrami bindowanymi (`utf8mb4_general_ci`) MySQL rzucal `SQLSTATE[HY000] 1271 Illegal mix of collations for operation 'in'`. - Blad byl polykany przez `try/catch (Throwable)` w `aggregateByDay()`, przez co widok dostawal pusta tablice i nie pokazywal zadnych zamowien. - Fix: dodano jawne `COLLATE utf8mb4_unicode_ci` na `CAST(integration_id AS CHAR)` oraz na calym wyrazeniu `CASE` zwracajacym `channel_key`, tak aby klucz kanalu mial spojne collation zgodne z `orders.source`. ## 2026-04-19 - Statystyki: fallback netto 23% VAT - `OrdersStatisticsRepository::netAmountSql()` dostal fallback: jesli `orders.total_without_tax` jest `NULL` lub `0`, a `orders.total_with_tax` ma wartosc, netto wyliczane jest jako `ROUND(total_with_tax / 1.23, 2)`. - Uzasadnienie: shopPRO nie wysyla netto ani na zamowieniu ani w pozycjach (`order_items.original_price_without_tax` jest puste), wiec bez fallbacku kolumna `Netto` w statystykach pokazywala 0. - Uwaga: fallback zaklada 23% VAT. Ostateczne rozwiazanie (prawidlowy netto z shopPRO / z `order_items.tax_rate`) opisane w `.paul/TODO.md` (tag `STAT-NET`).