Files
orderPRO/.paul/docs/TECH_CHANGELOG.md
2026-04-25 21:31:50 +02:00

129 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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_deliveries` z FK do `automation_rules`, `automation_actions`, `orders`;
- `UNIQUE (rule_id, action_id, order_id)` - twarda deduplikacja.
- `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 o `send_once_per_order` (0/1), domyslnie `0`.
- `resources/views/automation/form.php` i `public/assets/js/modules/automation-form.js`:
- dodany checkbox w akcji `Wyslij e-mail`: "Wyslij tylko raz dla tego zamowienia".
- `src/Modules/Automation/AutomationService.php`:
- akcja `send_email` uwzglednia `rule_id` i `action_id`;
- przy `send_once_per_order=1` pomija wysylke, gdy `wasSent(...) = true`;
- `markSent(...)` wykonywany tylko po udanej wysylce (`success=true`), wiec blad SMTP nie blokuje kolejnej proby.
- `src/Modules/Cron/CronHandlerFactory.php` i `routes/web.php`:
- podpiecie nowego repozytorium do konstruktora `AutomationService`.
- `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 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 `<tr>` 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 `<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 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`).