update
This commit is contained in:
@@ -13,6 +13,7 @@ Started: 2026-04-22
|
||||
| Phase | Name | Plans | Status |
|
||||
|-------|------|-------|--------|
|
||||
| 106 | Customer Return Alert | 1/1 | Complete |
|
||||
| 107 | Automation Email Send Once | 0/1 | Planning |
|
||||
|
||||
## Next Milestone
|
||||
|
||||
|
||||
@@ -1,50 +1,50 @@
|
||||
# Project State
|
||||
# Project State
|
||||
|
||||
## Project Reference
|
||||
|
||||
See: .paul/PROJECT.md (updated 2026-04-22)
|
||||
|
||||
**Core value:** Sprzedawca moze obslugiwac zamowienia ze wszystkich kanalow sprzedazy i nadawac przesylki bez przelaczania sie miedzy platformami.
|
||||
**Current focus:** v3.1 Operational Enhancements — Phase 106 complete, ready for next phase definition.
|
||||
**Current focus:** v3.1 Operational Enhancements - Phase 107 planning in progress.
|
||||
|
||||
## Current Position
|
||||
|
||||
Milestone: v3.1 Operational Enhancements
|
||||
Phase: 106 (Customer Return Alert) — Complete
|
||||
Plan: 106-01 complete (SUMMARY created)
|
||||
Phase: 107 of 107 (Automation Email Send Once) - Planning
|
||||
Plan: 107-01 created, awaiting approval
|
||||
Version: 3.1.0 (in progress)
|
||||
Status: Loop complete, ready for next PLAN or milestone decision
|
||||
Last activity: 2026-04-22 — UNIFY closed Phase 106 (SUMMARY + changelog + DOCS)
|
||||
Status: PLAN created, ready for APPLY
|
||||
Last activity: 2026-04-25 17:42:05 +02:00 - Created .paul/phases/107-automation-email-send-once/107-01-PLAN.md
|
||||
|
||||
Progress:
|
||||
- v3.1 Operational Enhancements: [##________] ~15% (1 phase complete, scope TBD)
|
||||
- Phase 106: [##########] 100%
|
||||
- Milestone: [##________] ~15%
|
||||
- Phase 107: [__________] 0%
|
||||
|
||||
## Loop Position
|
||||
|
||||
Current loop state:
|
||||
```
|
||||
PLAN --> APPLY --> UNIFY
|
||||
v v v [Loop complete — ready for next PLAN or milestone]
|
||||
[x] [ ] [ ] [Plan created, awaiting approval]
|
||||
```
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-04-22 — /paul:unify for plan 106-01
|
||||
Stopped at: Phase 106 Complete — SUMMARY + changelog created
|
||||
Next action: `/paul:plan` dla nastepnej fazy v3.1, lub `/paul:discuss-milestone` aby rozszerzyc zakres milestone
|
||||
Resume file: .paul/phases/106-customer-return-alert/106-01-SUMMARY.md
|
||||
Last session: 2026-04-25 17:42:05 +02:00
|
||||
Stopped at: Plan 107-01 created
|
||||
Next action: Review and approve plan, then run $paul-apply .paul/phases/107-automation-email-send-once/107-01-PLAN.md
|
||||
Resume file: .paul/phases/107-automation-email-send-once/107-01-PLAN.md
|
||||
|
||||
## Deferred to Next Milestones
|
||||
|
||||
- Phase 68 — Code Deduplication Refactor (0/2 Planning, nigdy nie rozpoczety)
|
||||
- STAT-NET — netto shopPRO z API lub z `order_items.tax_rate` (`.paul/TODO.md`)
|
||||
- Phase 68 - Code Deduplication Refactor (0/2 Planning, nigdy nie rozpoczety)
|
||||
- STAT-NET - netto shopPRO z API lub z `order_items.tax_rate` (`.paul/TODO.md`)
|
||||
- Mobile Orders List / Mobile Order Details / Mobile Settings (TBD z poprzedniego roadmapu)
|
||||
- sonar-scanner — skan dla phase 105 i phase 106 nie zostal uruchomiony w sesji UNIFY (skill gap odnotowany)
|
||||
- INDEX-106-01 — indeksy DB dla query `customer_returned_count`: `order_addresses(order_id, address_type)`, `shipment_packages(order_id, delivery_status)` (gdy dataset >50k wierszy)
|
||||
- sonar-scanner - skan dla phase 105 i phase 106 nie zostal uruchomiony w sesji UNIFY (skill gap odnotowany)
|
||||
- INDEX-106-01 - indeksy DB dla query `customer_returned_count`: `order_addresses(order_id, address_type)`, `shipment_packages(order_id, delivery_status)` (gdy dataset >50k wierszy)
|
||||
|
||||
## Skill Audit (Phase 106)
|
||||
|
||||
| Expected | Invoked | Notes |
|
||||
|----------|---------|-------|
|
||||
| sonar-scanner (required) | ○ | Nie uruchomiony — odlozony analogicznie do Phase 105 |
|
||||
| sonar-scanner (required) | o | Nie uruchomiony - odlozony analogicznie do Phase 105 |
|
||||
|
||||
@@ -85,6 +85,13 @@
|
||||
- sprawdza warunki,
|
||||
- wykonuje akcje (`send_email`, `issue_receipt`, `update_shipment_status`, `update_order_status`),
|
||||
- zapisuje wynik do `automation_execution_logs`.
|
||||
- Warunek `order_status` czyta status z kontekstu eventu:
|
||||
- `new_status` dla eventow zmianowych (`order.status_changed`),
|
||||
- `current_status` dla eventu czasu w statusie (`order.status_aged`).
|
||||
- Akcja `send_email` ma opcjonalna flage `send_once_per_order`:
|
||||
- konfiguracja trzymana w `automation_actions.action_config`,
|
||||
- deduplikacja oparta o `automation_email_once_deliveries` (`rule_id + action_id + order_id` UNIQUE),
|
||||
- wpis "wyslano raz" zapisywany tylko po udanej wysylce (`EmailSendingService::send(...)->success = true`).
|
||||
|
||||
## Shipment tracking — mapowanie statusow kuriera
|
||||
- `ShipmentTrackingHandler` (job `shipment_tracking_sync`) iteruje po aktywnych paczkach i pobiera status z API przewoznika (`Inpost/Apaczka/AllegroTrackingService`).
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
## Zakres i zrodlo prawdy
|
||||
- Schemat wynika z migracji SQL w `database/migrations`.
|
||||
- Dokument odzwierciedla stan repo na 2026-04-19 (migracje do `20260413_000100`).
|
||||
- Dokument odzwierciedla stan repo na 2026-04-25 (migracje do `20260425_000102`).
|
||||
|
||||
## Ostatnie istotne migracje
|
||||
- `20260425_000102_create_automation_email_once_deliveries_table.sql`
|
||||
- `20260422_000101_backfill_delivery_status_unknowns.sql`
|
||||
- `20260413_000100_ensure_orders_delivery_payment_columns.sql`
|
||||
- `20260412_000099_add_requires_photo_to_project_mappings.sql`
|
||||
- `20260412_000098_rename_external_status_id_to_status_code.sql`
|
||||
@@ -131,6 +133,15 @@
|
||||
### automation_rules, automation_conditions, automation_actions, automation_execution_logs
|
||||
- Definicje regul i historia ich wykonan.
|
||||
|
||||
### automation_email_once_deliveries
|
||||
- Rejestr jednorazowych wysylek e-mail dla akcji automatyzacji (`send_once_per_order`).
|
||||
- Klucz unikalny:
|
||||
- `(rule_id, action_id, order_id)` - gwarancja, ze ta sama akcja e-mail reguly nie zostanie oznaczona drugi raz dla tego samego zamowienia.
|
||||
- Relacje:
|
||||
- `rule_id -> automation_rules.id` (CASCADE),
|
||||
- `action_id -> automation_actions.id` (CASCADE),
|
||||
- `order_id -> orders.id` (CASCADE).
|
||||
|
||||
### shipment_packages
|
||||
- Rekordy przesylek i etykiet.
|
||||
- Wazne kolumny trackingowe (od `000060`):
|
||||
|
||||
@@ -1,63 +1,105 @@
|
||||
# TECH_CHANGELOG
|
||||
|
||||
> Chronologiczny log zmian technicznych — co i dlaczego.
|
||||
> 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.
|
||||
**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`.
|
||||
- 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.
|
||||
- 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).
|
||||
- `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`.
|
||||
- `.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.
|
||||
- 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).
|
||||
- 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.
|
||||
**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)**:
|
||||
**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 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`.
|
||||
**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.
|
||||
|
||||
@@ -83,3 +125,4 @@
|
||||
- `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`).
|
||||
|
||||
|
||||
196
.paul/phases/107-automation-email-send-once/107-01-PLAN.md
Normal file
196
.paul/phases/107-automation-email-send-once/107-01-PLAN.md
Normal file
@@ -0,0 +1,196 @@
|
||||
---
|
||||
phase: 107-automation-email-send-once
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- database/migrations/20260425_000102_create_automation_email_once_deliveries_table.sql
|
||||
- src/Modules/Automation/AutomationController.php
|
||||
- src/Modules/Automation/AutomationRepository.php
|
||||
- src/Modules/Automation/AutomationService.php
|
||||
- src/Modules/Automation/AutomationEmailOnceRepository.php
|
||||
- resources/views/automation/form.php
|
||||
- public/assets/js/modules/automation-form.js
|
||||
- src/Modules/Cron/CronHandlerFactory.php
|
||||
- tests/Unit/AutomationServiceTest.php
|
||||
- .paul/docs/DB_SCHEMA.md
|
||||
- .paul/docs/ARCHITECTURE.md
|
||||
- .paul/docs/TECH_CHANGELOG.md
|
||||
autonomous: true
|
||||
delegation: off
|
||||
---
|
||||
|
||||
<objective>
|
||||
## Goal
|
||||
Dodac w akcji automatyzacji `send_email` opcje "wyslij tylko raz na zamowienie", ktora gwarantuje, ze dla tej samej reguly i tego samego zamowienia ten sam mail nie zostanie wyslany ponownie przy kolejnych przebiegach crona.
|
||||
|
||||
## Purpose
|
||||
Event `order.status_aged` jest uruchamiany cyklicznie. Bez mechanizmu idempotencji klient moze dostawac ten sam mail przy kazdym przebiegu. Potrzebujemy bezpiecznego "once per order", ale tylko tam, gdzie operator swiadomie zaznaczy taka opcje.
|
||||
|
||||
## Output
|
||||
- Checkbox w konfiguracji akcji `Wyslij e-mail`: "Wyslij tylko raz dla tego zamowienia"
|
||||
- Trwale zapamietanie wysylki jednorazowej per `rule_id + action_id + order_id`
|
||||
- Logika wykonania, ktora pomija ponowna wysylke tylko dla akcji z wlaczonym checkboxem
|
||||
- Testy jednostkowe dla scenariusza jednorazowego i scenariusza domyslnego (wielokrotnego)
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
## Project Context
|
||||
@.paul/PROJECT.md
|
||||
@.paul/ROADMAP.md
|
||||
@.paul/STATE.md
|
||||
|
||||
## Prior Work
|
||||
@.paul/phases/60-order-status-aged-event/60-01-SUMMARY.md
|
||||
|
||||
## Source Files
|
||||
@src/Modules/Automation/AutomationController.php
|
||||
@src/Modules/Automation/AutomationRepository.php
|
||||
@src/Modules/Automation/AutomationService.php
|
||||
@src/Modules/Automation/OrderStatusAgedService.php
|
||||
@resources/views/automation/form.php
|
||||
@public/assets/js/modules/automation-form.js
|
||||
@src/Modules/Cron/CronHandlerFactory.php
|
||||
@database/migrations/20260318_000057_create_automation_tables.sql
|
||||
@database/migrations/20260328_000072_create_automation_execution_logs_table.sql
|
||||
</context>
|
||||
|
||||
<skills>
|
||||
## Required Skills (from SPECIAL-FLOWS.md)
|
||||
|
||||
| Skill | Priority | When to Invoke | Loaded? |
|
||||
|-------|----------|----------------|---------|
|
||||
| sonar-scanner (CLI) | required | Po APPLY, przed UNIFY | o |
|
||||
|
||||
## Skill Invocation Checklist
|
||||
- [ ] sonar-scanner uruchomiony po wdrozeniu zmian
|
||||
</skills>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## AC-1: Konfiguracja akcji e-mail ma opcje jednorazowej wysylki
|
||||
```gherkin
|
||||
Given operator tworzy lub edytuje zadanie automatyczne
|
||||
When wybierze akcje "Wyslij e-mail"
|
||||
Then widzi checkbox "Wyslij tylko raz dla tego zamowienia"
|
||||
And wartosc checkboxa zapisuje sie w action_config i wraca poprawnie po ponownej edycji reguly
|
||||
```
|
||||
|
||||
## AC-2: Jednorazowa wysylka dziala per zamowienie
|
||||
```gherkin
|
||||
Given regula z eventem "order.status_aged" i akcja send_email z wlaczona opcja "wyslij tylko raz"
|
||||
And zamowienie pozostaje w tym samym statusie przez kolejne uruchomienia crona
|
||||
When cron wyzwoli te sama regule wielokrotnie dla tego samego zamowienia
|
||||
Then e-mail zostanie wyslany tylko podczas pierwszego udanego wykonania tej akcji
|
||||
And kolejne wykonania pominaja tylko te akcje e-mail, nie przerywajac pozostalych akcji reguly
|
||||
```
|
||||
|
||||
## AC-3: Zachowanie domyslne pozostaje bez zmian
|
||||
```gherkin
|
||||
Given regula z akcja send_email bez zaznaczonej opcji "wyslij tylko raz"
|
||||
When event uruchomi regule wielokrotnie dla tego samego zamowienia
|
||||
Then e-mail moze byc wysylany wielokrotnie jak dotychczas
|
||||
```
|
||||
|
||||
## AC-4: Oznaczenie "wyslano raz" powstaje tylko po sukcesie wysylki
|
||||
```gherkin
|
||||
Given pierwsza proba wysylki zakonczyla sie bledem
|
||||
When kolejny przebieg crona ponowi wykonanie
|
||||
Then system ponownie sprobuje wyslac e-mail
|
||||
And blokada jednorazowosci nie jest zapisana po nieudanej probie
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Dodac model danych jednorazowej wysylki i repozytorium idempotencji</name>
|
||||
<files>database/migrations/20260425_000102_create_automation_email_once_deliveries_table.sql, src/Modules/Automation/AutomationEmailOnceRepository.php, src/Modules/Cron/CronHandlerFactory.php</files>
|
||||
<action>
|
||||
1. Dodac migracje tworzenia tabeli `automation_email_once_deliveries` z kolumnami:
|
||||
- `id` (PK),
|
||||
- `rule_id` (FK -> automation_rules.id),
|
||||
- `action_id` (FK -> automation_actions.id),
|
||||
- `order_id` (FK -> orders.id),
|
||||
- `created_at`.
|
||||
2. Dodac UNIQUE KEY na `(rule_id, action_id, order_id)` dla twardej deduplikacji.
|
||||
3. Utworzyc `AutomationEmailOnceRepository` z metodami:
|
||||
- `wasSent(int $ruleId, int $actionId, int $orderId): bool`
|
||||
- `markSent(int $ruleId, int $actionId, int $orderId): void`
|
||||
4. Podpiac repozytorium w `CronHandlerFactory` i konstruktorze `AutomationService`.
|
||||
</action>
|
||||
<verify>Uruchomienie migracji lokalnie konczy sie bez bledu, a tabela i indeks UNIQUE istnieja.</verify>
|
||||
<done>AC-2 i AC-4 maja trwaly mechanizm danych.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Rozszerzyc konfiguracje akcji send_email o flage "send_once_per_order"</name>
|
||||
<files>src/Modules/Automation/AutomationController.php, resources/views/automation/form.php, public/assets/js/modules/automation-form.js, src/Modules/Automation/AutomationRepository.php</files>
|
||||
<action>
|
||||
1. W `AutomationController::parseActionConfig()` dla `send_email` dodac bool `send_once_per_order` (0/1), domyslnie `0`.
|
||||
2. W widoku formularza (`resources/views/automation/form.php`) dodac checkbox dla akcji e-mail:
|
||||
`actions[idx][send_once_per_order]`.
|
||||
3. W generatorze dynamicznym JS (`automation-form.js`) dodac ten sam checkbox przy dodawaniu nowej akcji e-mail.
|
||||
4. Zachowac kompatybilnosc starych rekordow (brak pola = `false`).
|
||||
</action>
|
||||
<verify>Utworzenie i edycja reguly zapisuje/odczytuje checkbox poprawnie; brak regresji dla innych typow akcji.</verify>
|
||||
<done>AC-1 i AC-3 sa spelnione po stronie konfiguracji.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Wdrozyc wykonanie jednorazowe w AutomationService i testy</name>
|
||||
<files>src/Modules/Automation/AutomationService.php, tests/Unit/AutomationServiceTest.php, .paul/docs/DB_SCHEMA.md, .paul/docs/ARCHITECTURE.md, .paul/docs/TECH_CHANGELOG.md</files>
|
||||
<action>
|
||||
1. W `AutomationService::executeActions()` przekazac `ruleId` i `actionId` do obslugi send_email.
|
||||
2. W `handleSendEmail()`:
|
||||
- odczytac `send_once_per_order`,
|
||||
- gdy flaga aktywna: sprawdzic `wasSent(...)`; jesli true, pominac akcje e-mail,
|
||||
- po udanej wysylce (bez wyjatku): zapisac `markSent(...)`.
|
||||
3. Nie oznaczac jako wyslane przy bledzie wysylki (wyjatek -> brak `markSent`).
|
||||
4. Dodac testy:
|
||||
- `send_once_per_order=true` -> druga proba nie wywoluje `EmailSendingService::send`,
|
||||
- `send_once_per_order=false` -> kolejne proby wysylaja dalej.
|
||||
5. Zaktualizowac `.paul/docs/DB_SCHEMA.md`, `.paul/docs/ARCHITECTURE.md`, `.paul/docs/TECH_CHANGELOG.md`.
|
||||
</action>
|
||||
<verify>`php vendor/bin/phpunit tests/Unit/AutomationServiceTest.php` przechodzi; test reczny cron potwierdza pojedyncza wysylke dla zaznaczonej opcji.</verify>
|
||||
<done>AC-2, AC-3, AC-4 sa spelnione end-to-end.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## DO NOT CHANGE
|
||||
- Logika eventow i warunkow automatyzacji niezwiązanych z `send_email`
|
||||
- Mechanizmy wysylki e-mail poza potrzebnym miejscem integracji w `AutomationService`
|
||||
- Runtime konfiguracji DB hostow (`DB_HOST` / `DB_HOST_REMOTE`)
|
||||
|
||||
## SCOPE LIMITS
|
||||
- Brak nowych eventow automatyzacji
|
||||
- Brak zmian w harmonogramie crona `order_status_aged`
|
||||
- Brak zmian UI listy historii poza tym, co konieczne do konfiguracji checkboxa
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
Before declaring plan complete:
|
||||
- [ ] Migracja tworzy tabele i UNIQUE KEY dla deduplikacji
|
||||
- [ ] Checkbox "wyslij tylko raz" dziala przy create/edit reguly
|
||||
- [ ] Dla `send_once_per_order=1` e-mail idzie tylko raz na zamowienie
|
||||
- [ ] Dla `send_once_per_order=0` zachowanie pozostaje bez zmian
|
||||
- [ ] `php vendor/bin/phpunit tests/Unit/AutomationServiceTest.php` przechodzi
|
||||
- [ ] Dokumentacja `.paul/docs/*` zaktualizowana
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Operator moze wlaczyc jednorazowa wysylke per zamowienie bez zmian kodu
|
||||
- Cron nie wysyla duplikatow dla tej samej jednorazowej akcji e-mail
|
||||
- Brak regresji istniejacych automatyzacji e-mail
|
||||
- Plan gotowy do uruchomienia przez `$paul-apply`
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.paul/phases/107-automation-email-send-once/107-01-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user