11 KiB
TECH_CHANGELOG
Chronologiczny log zmian technicznych — co i dlaczego.
2026-04-25 - Fix: order_status condition for order.status_aged
Powod: reguly order.status_aged z warunkiem order_status nie wykonywaly akcji, bo warunek sprawdzal tylko context.new_status, a ten event przekazuje context.current_status.
Zmiany:
- src/Modules/Automation/AutomationService.php
- evaluateOrderStatusCondition: fallback z new_status na current_status.
- tests/Unit/AutomationServiceTest.php
- nowy test regresyjny: order.status_aged + current_status spelnia warunek order_status i wykonuje akcje.
Efekt:
- reguly order.status_aged z warunkiem statusu zamowienia dzialaja poprawnie,
- eventy zmianowe order.status_changed nadal korzystaja z new_status bez regresji.
2026-04-25 - Automatyzacja: jednorazowa wysylka e-mail per zamowienie (Phase 107)
Powod: event order.status_aged jest cykliczny, przez co ta sama regula mogla wysylac klientowi ten sam e-mail przy kazdym przebiegu crona. Potrzebna byla kontrola "wyslij tylko raz dla tego zamowienia".
Zmiany:
database/migrations/20260425_000102_create_automation_email_once_deliveries_table.sql:- nowa tabela
automation_email_once_deliveriesz FK doautomation_rules,automation_actions,orders; UNIQUE (rule_id, action_id, order_id)- twarda deduplikacja.
- nowa tabela
src/Modules/Automation/AutomationEmailOnceRepository.php(nowy):wasSent(ruleId, actionId, orderId)- sprawdzenie czy akcja e-mail byla juz wykonana jednorazowo;markSent(ruleId, actionId, orderId)- zapis idempotentny (INSERT ... ON DUPLICATE KEY UPDATE).
src/Modules/Automation/AutomationController.php:parseActionConfig(send_email)rozszerzone osend_once_per_order(0/1), domyslnie0.
resources/views/automation/form.phpipublic/assets/js/modules/automation-form.js:- dodany checkbox w akcji
Wyslij e-mail: "Wyslij tylko raz dla tego zamowienia".
- dodany checkbox w akcji
src/Modules/Automation/AutomationService.php:- akcja
send_emailuwzgledniarule_idiaction_id; - przy
send_once_per_order=1pomija wysylke, gdywasSent(...) = true; markSent(...)wykonywany tylko po udanej wysylce (success=true), wiec blad SMTP nie blokuje kolejnej proby.
- akcja
src/Modules/Cron/CronHandlerFactory.phpiroutes/web.php:- podpiecie nowego repozytorium do konstruktora
AutomationService.
- podpiecie nowego repozytorium do konstruktora
tests/Unit/AutomationServiceTest.php(nowy):- test scenariusza jednorazowego (drugi trigger nie wysyla),
- test scenariusza domyslnego (bez flagi wysyla wielokrotnie).
Efekt:
- Operator moze zaznaczyc jednorazowosc na poziomie konkretnej akcji e-mail.
- Dla jednego zamowienia i jednej akcji reguly mail nie duplikuje sie przy kolejnych uruchomieniach crona.
- Zachowanie domyslne pozostaje bez zmian dla istniejacych regul bez zaznaczonej opcji.
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 paczkareturned. buildListSql()— dodana kolumnacustomer_returned_count(EXISTS-style COUNT DISTINCT) do SELECT listy zamowien.transformOrderRow()— przekazujecustomer_returned_countdo wiersza.findDetails()— JOINorder_addressestypu customer + subquerycustomer_returned_count; zwraca rowniezbuyer_email,buyer_phone,buyer_namew$order.
- nowa metoda prywatna
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.
- nowa metoda
src/Modules/Orders/OrdersController.php:toTableRow()— dodano badgezwroty: Nw kolumnie buyer + klasais-risk-returnna<tr>gdycustomer_returned_count >= 1(kompozycja z klasa aged orders z Phase 101).show()— oblicza$customerRiskInfoi 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— bannercustomer-risk-banneru samej gory karty szczegolow (pod naglowkiem, nad flash messages i status change), z<details>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 przeznpm 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)ishipment_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
- apaczka
database/migrations/20260422_000101_backfill_delivery_status_unknowns.sql: backfill 11 paczek (3 + 7 + 1). Bez triggershipment.status_changed(backfill starych rekordow, nie runtime event).
Faza B — rozwiazanie systemowe:
DeliveryStatusMappingRepository::listUnmappedRawStatuses(provider, knownKeys)— zwraca raw statusy zshipment_packagesktore nie wystepuja w defaultach ani overrides DB; z licznikiem paczek i ostatnim wystapieniem.DeliveryStatusMappingController::index— przekazujeunmappedRawStatusesdo 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 dosave-bulk. Pomaranczowy akcent aby wyroznic od defaultow.ShipmentTrackingHandler— dostal?DeliveryStatusMappingRepositoryw konstruktorze; po kazdymservice->getDeliveryStatus()wywolujeDeliveryStatus::normalizeWithOverrides(provider, raw, overrides)jesli overrides istnieja. Dzieki temu override z UI dziala runtime bez zmian kodu.CronHandlerFactory— przekazujeDeliveryStatusMappingRepositorydoShipmentTrackingHandler.
Faza C — badge w menu:
Application— dodano statyczny holderself::$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,Bruttoper kanal oraz stopkaRazem. - Dodano tlumaczenia
statistics.orders.*inavigation.statistics*. - Brak zmian migracyjnych i brak zmian schematu bazy danych.
2026-04-19 - Fix: Statystyki nie pokazywaly zamowien (kolizja collation)
OrdersStatisticsRepository::channelSql()generowal wyrazenieCONCAT("shoppro:", CAST(integration_id AS CHAR)), ktore w MySQL dawalo wynik z collationutf8mb4_bin. W zestawieniu z parametrami bindowanymi (utf8mb4_general_ci) MySQL rzucalSQLSTATE[HY000] 1271 Illegal mix of collations for operation 'in'.- Blad byl polykany przez
try/catch (Throwable)waggregateByDay(), przez co widok dostawal pusta tablice i nie pokazywal zadnych zamowien. - Fix: dodano jawne
COLLATE utf8mb4_unicode_cinaCAST(integration_id AS CHAR)oraz na calym wyrazeniuCASEzwracajacymchannel_key, tak aby klucz kanalu mial spojne collation zgodne zorders.source.
2026-04-19 - Statystyki: fallback netto 23% VAT
OrdersStatisticsRepository::netAmountSql()dostal fallback: jesliorders.total_without_taxjestNULLlub0, aorders.total_with_taxma wartosc, netto wyliczane jest jakoROUND(total_with_tax / 1.23, 2).- Uzasadnienie: shopPRO nie wysyla netto ani na zamowieniu ani w pozycjach (
order_items.original_price_without_taxjest puste), wiec bez fallbacku kolumnaNettow statystykach pokazywala 0. - Uwaga: fallback zaklada 23% VAT. Ostateczne rozwiazanie (prawidlowy netto z shopPRO / z
order_items.tax_rate) opisane w.paul/TODO.md(tagSTAT-NET).