From 0e8f246d6f85f332cfdcf5fe9fdc544177c582b6 Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Tue, 14 Apr 2026 20:36:20 +0200 Subject: [PATCH] update --- .claude/commands/wygeneruj-projekty.md | 55 ++++- .paul/PROJECT.md | 1 + .paul/ROADMAP.md | 1 + .paul/STATE.md | 14 +- .paul/governance/governance_2026-04-14.jsonl | 78 +++++++ .../102-01-PLAN.md | 206 ++++++++++++++++++ .../102-01-SUMMARY.md | 54 +++++ .vscode/ftp-kr.sync.cache.json | 122 ++++++++--- .../Shipments/ApaczkaShipmentService.php | 42 ++++ tests/Unit/ApaczkaShipmentServiceTest.php | 110 ++++++++++ .../email_photo_fetcher.cpython-312.pyc | Bin 0 -> 9439 bytes ...we_podziekowanie_ojciec_chrzestny_wzor2.py | 168 ++++++++++++++ tools/generowanie/email_message_fetcher.py | 169 ++++++++++++++ .../magnes_chrzest_stopki_serce.py | 140 ++++++++++++ .../pudelko_chrzest_dziewczynka_aniolek.py | 27 ++- tools/generowanie/pudelko_komunia_kielich.py | 101 +++++++++ 16 files changed, 1234 insertions(+), 54 deletions(-) create mode 100644 .paul/governance/governance_2026-04-14.jsonl create mode 100644 .paul/phases/102-apaczka-receiver-street-length/102-01-PLAN.md create mode 100644 .paul/phases/102-apaczka-receiver-street-length/102-01-SUMMARY.md create mode 100644 tests/Unit/ApaczkaShipmentServiceTest.php create mode 100644 tools/generowanie/__pycache__/email_photo_fetcher.cpython-312.pyc create mode 100644 tools/generowanie/akrylowe_podziekowanie_ojciec_chrzestny_wzor2.py create mode 100644 tools/generowanie/email_message_fetcher.py create mode 100644 tools/generowanie/magnes_chrzest_stopki_serce.py create mode 100644 tools/generowanie/pudelko_komunia_kielich.py diff --git a/.claude/commands/wygeneruj-projekty.md b/.claude/commands/wygeneruj-projekty.md index 98755de..ce68b1f 100644 --- a/.claude/commands/wygeneruj-projekty.md +++ b/.claude/commands/wygeneruj-projekty.md @@ -2,6 +2,24 @@ Jesteś silnikiem generowania projektów graficznych dla zamówień w orderPRO. Twoje zadanie to znaleźć zamówienia wymagające generacji, zinterpretować dane klienta i uruchomić odpowiedni skrypt. +## Opcjonalne parametry filtrowania + +Komenda przyjmuje opcjonalny argument (wolnotekstowy) zawężający zakres generacji: + +- **Numer zamówienia** — np. `OP000123`, `OP000000385` (pattern: zaczyna się od `OP` + cyfry) → filtr `o.internal_order_number = ''`. +- **Klient** — imię/nazwisko lub fragment (np. `Kowalska`, `Anna Kowalska`) → filtr `oa.name LIKE '%%'`. +- **Produkt** — fragment nazwy produktu (np. `buteleczka`, `pudelko chrzest`) → filtr `oi.original_name LIKE '%%'`. + +**Rozpoznanie typu argumentu:** +1. Jeśli pasuje do regex `^OP\d+$` (po znormalizowaniu case) → numer zamówienia. +2. W przeciwnym razie spróbuj jako klient (`oa.name LIKE`). Jeśli wynik pusty → spróbuj jako produkt (`oi.original_name LIKE`). +3. Jeśli użytkownik jawnie napisał `klient:` / `zamowienie:` / `produkt:` — użyj dokładnie tego typu bez zgadywania. +4. Brak argumentu → standardowy tryb (wszystkie pasujące pozycje). + +Zawsze raportuj w wstępie: "Filtr: = " albo "Filtr: brak (wszystkie zamówienia)". + +**WAŻNE:** Filtry zawężają tylko wybór pozycji — pozostałe reguły (status grupy 2, `project_generated = 0`, wymagana personalizacja, aktywne mapowanie) **obowiązują nadal**. Jeśli filtr nie zwróci nic — poinformuj użytkownika wprost, że żadna pasująca pozycja nie wymaga generacji. + ## Przepływ ### 1. Połącz się z bazą danych @@ -33,6 +51,7 @@ Schemat bazy danych — kluczowe kolumny: - `order_items.project_generated` — 0 = nie wygenerowano, 1 = wygenerowano - `order_addresses.address_type` — typ adresu ('customer', 'delivery', 'invoice') - `order_addresses.name` — pełne imię i nazwisko kupującego +- `order_notes.note_type = 'message'` — wiadomość od klienta do zamówienia (pole `comment`), traktowana jako dodatkowy kontekst do personalizacji Jedno zapytanie łączy zamówienia z mapowaniami (JOIN eliminuje produkty bez mapowania): @@ -49,7 +68,12 @@ SELECT oa.email AS buyer_email, pm.script_name, pm.output_dir, - pm.requires_photo + pm.requires_photo, + ( + SELECT GROUP_CONCAT(n.comment SEPARATOR '\n---\n') + FROM order_notes n + WHERE n.order_id = o.id AND n.note_type = 'message' + ) AS customer_message FROM order_items oi JOIN orders o ON oi.order_id = o.id JOIN order_statuses os ON o.status_code = os.code @@ -61,15 +85,38 @@ WHERE osg.id = 2 AND oi.project_generated = 0 AND oi.personalization IS NOT NULL AND oi.personalization <> '' + -- {{OPTIONAL_FILTER}} ORDER BY o.id; ``` -Jeśli brak wyników — poinformuj użytkownika że nie ma zamówień do generacji pasujących do aktywnych mapowań. +**Podstawienie filtra** (patrz sekcja "Opcjonalne parametry filtrowania"): +- Numer zamówienia: `AND o.internal_order_number = ''` +- Klient: `AND oa.name LIKE '%%'` +- Produkt: `AND oi.original_name LIKE '%%'` +- Brak argumentu: usuń marker, nie dodawaj nic. + +Escape'uj apostrofy w wartości (zamień `'` na `''`) zanim wstawisz do SQL. + +Jeśli brak wyników — poinformuj użytkownika że nie ma zamówień do generacji pasujących do aktywnych mapowań (przy aktywnym filtrze — wskaż, że żadna pozycja nie pasuje do podanego filtra). ### 5. Zinterpretuj dane klienta (AI normalizacja) Dla każdego pasującego produktu: -- Przeczytaj pole `personalization` z `order_items` +- Przeczytaj pole `personalization` z `order_items` **ORAZ** `customer_message` (wiadomość klienta do zamówienia z `order_notes.note_type = 'message'`) **ORAZ** wiadomości mailowe od klienta z INBOX (patrz niżej). + +**Sprawdzanie maili od klienta (analogicznie do zdjęć):** + +Jeśli zamówienie ma `buyer_email`, przed interpretacją uruchom: +```bash +python tools/generowanie/email_message_fetcher.py --email "{buyer_email}" --days 60 --json +``` +- Skrypt przeszukuje **wyłącznie INBOX** skrzynki `EMAIL_01` i zwraca JSON z listą wiadomości (`date`, `subject`, `body`), od najnowszej do najstarszej. +- Exit 0 + JSON = są maile; exit 1 + `BRAK` = brak wiadomości (to normalny przypadek — nie blokuj generacji). +- Jeśli są wiadomości — wczytaj treści i traktuj jako dodatkowe źródło korekt/uwag klienta (tak samo jak `customer_message` z bazy, tylko późniejsze w czasie). Przy sprzecznościach: **najnowszy mail > starsze maile > `customer_message` z bazy > `personalization`**. +- Odfiltruj maile ewidentnie niezwiązane z zamówieniem (np. automatyczne odpowiedzi, tematy typu "Re: faktura", zapytania o zupełnie inny produkt). Zostaw tylko te wyglądające na poprawki/uzupełnienia personalizacji. +- Raportuj w planie w kolumnie "Mail klienta": `BRAK` / `N wiadomości — najnowsza: {krótkie streszczenie}` — żeby użytkownik widział, że sprawdziłaś. +- **Traktuj oba źródła łącznie.** Wiadomość klienta do zamówienia często zawiera doprecyzowania, korekty, dodatkowe życzenia, preferencje zapisu imion, poprawki literówek, prośby typu "zamiast Kasia wpiszcie Kasiu", uzupełnienia brakujących pól (np. data, życzenia). Jeśli wiadomość klienta i personalizacja są **sprzeczne**, preferuj wiadomość klienta (jest późniejsza, bardziej świadoma) — ale zaraportuj konflikt w planie. +- Jeśli `customer_message` zawiera treści niezwiązane z personalizacją (np. "proszę o szybką wysyłkę", "dziękuję") — zignoruj do celów generacji, ale odnotuj w planie w kolumnie "Wiadomość klienta" żeby użytkownik widział że sprawdziłaś. - Zinterpretuj dane kontekstowo. Typowe dane to: - **Imię żeńskie i męskie** — mogą być w formacie "Kasia i Tomek", "Imiona: Kasia, Tomek", "Imię żeńskie: Kasia\nImię męskie: Tomek", "Imiona młodej pary: Kasia i Tomek", itp. - **Data uroczystości** — format DD.MM.YYYY, może być opisana jako "Data: 30.04.2026" lub "Data uroczystości: 30.04.2026" lub po prostu "30.04.2026" @@ -255,7 +302,7 @@ Photoshop jako znak łamania linii w warstwach tekstowych rozpoznaje **`\r` (CR) Dla każdego potwierdzonego produktu: 1. **Sprawdź kolizję nazwy pliku** w katalogu wyjściowym. Jeśli plik `{klient}.psd` (lub inne rozszerzenie używane przez skrypt) już istnieje — dodaj do nazwy klienta sufiks z indeksem `01`, `02`, ... (np. `Anna Rak` → `Anna Rak 01`, a jeśli i to zajęte → `Anna Rak 02`). Inkrementuj aż znajdziesz wolną nazwę. Dopiero wtedy przekaż wynik jako `--klient`. -2. Uruchom skrypt: `python tools/generowanie/{script_name} --klient "Imię Nazwisko" ...` (parametry zależne od skryptu — dla buteleczek/pudełek zwykle `--imie/--imiona`, `--data`, opcjonalnie `--zyczenia`/`--podziekowanie`; dla produktów ze zdjęciem dorzuć `--zdjecie /sciezka/do/foto.jpg`) +2. Uruchom skrypt: `python tools/generowanie/{script_name} --klient "Imię Nazwisko" ...` (parametry zależne od skryptu — dla buteleczek/pudełek zwykle `--imie/--imiona`, `--data`, opcjonalnie `--zyczenia`/`--podziekowanie`; dla produktów ze zdjęciem dorzuć `--zdjecie /sciezka/do/foto.jpg`; **dla skryptów `magnes_*` dorzuć `--liczba {order_items.quantity}`** — szablony mają N slotów na arkuszu A3, nadmiar ma być ukrywany żeby nie drukować pustych kopii) 3. Jeśli mapowanie ma `output_dir` — skrypt powinien zapisać tam (jeśli obsługuje) ### 8. Oznacz w bazie diff --git a/.paul/PROJECT.md b/.paul/PROJECT.md index 14c12f8..21562d4 100644 --- a/.paul/PROJECT.md +++ b/.paul/PROJECT.md @@ -104,6 +104,7 @@ Sprzedawca moĹĽe obsĹ‚ugiwać zamĂłwienia ze wszystkich kanałów - [x] Event automatyzacji `order.imported` ogranicza sie do pierwszego importu zamowienia + backfill statusow w_realizacji -> wyslane dla zamowien z istniejaca przesylka — Phase 98 - [x] Preset przesylek nadpisuje wylacznie wymiary+wage paczki + polling kolejki wydruku + auto-click ostatniej etykiety po utworzeniu przesylki + usuniecie placeholderow "-" — Phase 100 - [x] Wizualna flaga wiekowa na liscie zamowien: czerwona ramka o rosnacej intensywnosci dla zamowien 4-7+ dniowych — Phase 101 +- [x] Naprawa tworzenia przesylek Apaczka dla dlugich adresow: auto-truncate dla uslug punktowych (ORLEN/Paczkomat), walidacja + blad dla uslug kurierskich — Phase 102 - [ ] Eliminacja zduplikowanego kodu: SslCertificateResolver, ToggleableRepositoryTrait, RedirectPathResolver, ReceiptService — Phase 68 ### Active (In Progress) diff --git a/.paul/ROADMAP.md b/.paul/ROADMAP.md index bc5723a..480875f 100644 --- a/.paul/ROADMAP.md +++ b/.paul/ROADMAP.md @@ -62,6 +62,7 @@ Wersja mobilna aplikacji, modul po module. Cel: pelna uzywalnosc orderPRO na tel | 99 | Order Delivery & Payment Edit | 0/1 | Cancelled | | 100 | Preset Scope & Print UX | 1/1 | Complete | | 101 | Aged Orders Row Highlight | 1/1 | Complete | +| 102 | Apaczka Receiver Street Length | 1/1 | Complete | | TBD | Mobile Orders List | - | Not started | | TBD | Mobile Order Details | - | Not started | | TBD | Mobile Settings | - | Not started | diff --git a/.paul/STATE.md b/.paul/STATE.md index 8eb4df2..d37a569 100644 --- a/.paul/STATE.md +++ b/.paul/STATE.md @@ -10,15 +10,15 @@ See: .paul/PROJECT.md (updated 2026-04-12) ## Current Position Milestone: v3.0 Mobile Responsive - In progress -Phase: 101 (Aged Orders Row Highlight) — Complete -Plan: 101-01 Complete +Phase: 102 (Apaczka Receiver Street Length) — Complete +Plan: 102-01 Complete Status: Loop closed, ready for next PLAN -Last activity: 2026-04-13 — UNIFY Phase 101 complete +Last activity: 2026-04-14 — UNIFY Phase 102 complete (manual re-test OK) Progress: - Milestone: [#########.] ~96% +- Phase 102: [##########] 100% - Phase 101: [##########] 100% -- Phase 100: [##########] 100% ## Loop Position @@ -30,7 +30,7 @@ PLAN ──▶ APPLY ──▶ UNIFY ## Session Continuity -Last session: 2026-04-13 -Stopped at: Phase 101 Complete +Last session: 2026-04-14 +Stopped at: Phase 102 Complete Next action: /paul:plan dla kolejnej fazy -Resume file: .paul/phases/101-aged-orders-row-highlight/101-01-SUMMARY.md +Resume file: .paul/phases/102-apaczka-receiver-street-length/102-01-SUMMARY.md diff --git a/.paul/governance/governance_2026-04-14.jsonl b/.paul/governance/governance_2026-04-14.jsonl new file mode 100644 index 0000000..62685c1 --- /dev/null +++ b/.paul/governance/governance_2026-04-14.jsonl @@ -0,0 +1,78 @@ +{"ts":"2026-04-13T22:03:03Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.claude\\\\commands\\\\wygeneruj-projekty.md","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:03:11Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.claude\\\\commands\\\\wygeneruj-projekty.md","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:03:24Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.claude\\\\commands\\\\wygeneruj-projekty.md","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:04:35Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\tools\\\\generowanie\\\\email_message_fetcher.py","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:04:51Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.claude\\\\commands\\\\wygeneruj-projekty.md","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:08:19Z","tool":"Bash","cmd":"tasklist //FI \"IMAGENAME eq Photoshop.exe\" 2>/dev/null | grep -i photoshop || echo \"NIE URUCHOMIONY\"\",\"description\":\"Check Photoshop running\"},\"tool_response\":{\"stdout\":\"NIE URUCHOMIONY\","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:08:31Z","tool":"Bash","cmd":"start \"\" \"C:/Program Files/Adobe/Adobe Photoshop 2024/Photoshop.exe\" 2>/dev/null || start \"\" \"C:/Program Files/Adobe/Adobe Photoshop 2025/Photoshop.exe\" 2>/dev/null || start \"\" \"C:/Program ","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:08:47Z","tool":"Bash","cmd":"tasklist //FI \"IMAGENAME eq Photoshop.exe\" 2>/dev/null | grep -i photoshop || echo \"NIE URUCHOMIONY\"\",\"description\":\"Recheck Photoshop\"},\"tool_response\":{\"stdout\":\"NIE URUCHOMIONY\",\"st","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:09:00Z","tool":"Bash","cmd":"start \"\" \"C:/Program Files/Adobe/Adobe Photoshop 2026/Photoshop.exe\" && echo launched\",\"description\":\"Launch Photoshop 2026\"},\"tool_response\":{\"stdout\":\"launched\",\"stderr\":\"\",\"inte","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:09:24Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && for email in patrycja.c3345@wp.pl dzubaulia99@gmail.com gusiek52@vp.pl kciesielska667@o2.pl janik_renata90@o2.pl brudzynskamarta8@gmail.com gosiadryga","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:09:55Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && mkdir -p C:/tmp/orderpro_photos && for email in janik_renata90@o2.pl gosiadrygas1@wp.pl ewelka1102@wp.pl olgakluska7@gmail.com kozakaleksandra46@gmail","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:10:11Z","tool":"Bash","cmd":"tasklist //FI \"IMAGENAME eq Photoshop.exe\" 2>/dev/null | grep -i photoshop && echo \"OK\" || echo \"NIE URUCHOMIONY\"\",\"description\":\"Recheck Photoshop\"},\"tool_response\":{\"stdout\":\"Binary ","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:10:23Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && grep -h \"add_argument\" tools/generowanie/pudelko_chrzest_dlonie_uv.py tools/generowanie/buteleczki_slub_wzor4.py tools/generowanie/prosba_chrzestny_","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:13:06Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && {\\necho \"=== 1/9 OP214 Patrycja Cieśla ===\"\\npython tools/generowanie/pudelko_chrzest_dlonie_uv.py --klient \"Patrycja Cieśla\" --imie \"Poli\" ","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:14:14Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && {\\ncat > /tmp/mark1.sql <<'EOF'\\nUPDATE order_items SET project_generated=1, project_generated_at=NOW() WHERE id IN (3200,3089,3119,3197,3202,3210,3","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:15:16Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && {\\ncat > /tmp/mark2.sql <<'EOF'\\nUPDATE order_items SET project_generated=1, project_generated_at=NOW() WHERE id IN (3055,3056,3093,3214,3215,3223,3","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:16:17Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && {\\nP_RENATA=\"C:/tmp/orderpro_photos/janik_renata90@o2.pl_20260414000953_1000032451.jpg\"\\nP_GOSIA=\"C:/tmp/orderpro_photos/gosiadrygas1@wp.pl_20260","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:16:31Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"UPDATE order_items SET project_generated=1, project_generat","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:30:09Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\tools\\\\generowanie\\\\akrylowe_podziekowanie_ojciec_chrzestny_wzor2.py","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:30:26Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && {\\necho \"=== TEST 1: chłopiec ===\"\\npython tools/generowanie/akrylowe_podziekowanie_ojciec_chrzestny_wzor2.py --klient \"TEST chłopiec\" --imie ","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:31:16Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro --default-character-set=utf8mb4 -e \"INSERT INTO project_mapping","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:35:38Z","tool":"Bash","cmd":"tasklist //FI \"IMAGENAME eq Photoshop.exe\" 2>/dev/null | grep -i photoshop >/dev/null && echo OK || echo NIE; ls \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:35:52Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\tools\\\\generowanie\\\\_explore_tmp.py","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:35:59Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && python tools/generowanie/_explore_tmp.py 2>&1 | head -200\",\"timeout\":180000,\"description\":\"Explore layer structure\"},\"tool_response\":{\"stdou","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:36:18Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && rm tools/generowanie/_explore_tmp.py && head -120 tools/generowanie/pudelko_komunia_dlonie.py\",\"description\":\"Look at reference komunia script\"},","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:37:41Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\tools\\\\generowanie\\\\_explore_tmp.py","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:37:47Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && python tools/generowanie/_explore_tmp.py 2>&1 | head -80\",\"timeout\":180000,\"description\":\"Re-explore structure\"},\"tool_response\":{\"stdout\":","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:38:05Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\tools\\\\generowanie\\\\_rename_tmp.py","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:38:12Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && python tools/generowanie/_rename_tmp.py 2>&1\",\"timeout\":180000,\"description\":\"Rename layers in PSD\"},\"tool_response\":{\"stdout\":\"DL: \"Cioc","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:38:35Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\tools\\\\generowanie\\\\pudelko_komunia_kielich.py","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:38:44Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && rm tools/generowanie/_explore_tmp.py tools/generowanie/_rename_tmp.py && python tools/generowanie/pudelko_komunia_kielich.py --klient \"TEST Kielich\"","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:39:17Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro --default-character-set=utf8mb4 -e \"INSERT INTO project_mapping","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:40:00Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\tools\\\\generowanie\\\\_explore_tmp.py","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:40:07Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && python tools/generowanie/_explore_tmp.py 2>&1 | head -100\",\"timeout\":180000,\"description\":\"Explore magnes PSD\"},\"tool_response\":{\"stdout\":\","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:40:21Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\tools\\\\generowanie\\\\_explore_tmp.py","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:40:27Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && python tools/generowanie/_explore_tmp.py 2>&1 | head -60\",\"timeout\":180000,\"description\":\"Explore projekt SO\"},\"tool_response\":{\"stdout\":\"","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:41:10Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\tools\\\\generowanie\\\\_rename_tmp.py","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:41:20Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && python tools/generowanie/_rename_tmp.py 2>&1\",\"timeout\":180000,\"description\":\"Rename layers inside SO\"},\"tool_response\":{\"stdout\":\"Dzi�k","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:41:46Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\tools\\\\generowanie\\\\magnes_chrzest_stopki_serce.py","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:41:57Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && rm tools/generowanie/_explore_tmp.py tools/generowanie/_rename_tmp.py && python tools/generowanie/magnes_chrzest_stopki_serce.py --klient \"TEST Magne","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:42:50Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\tools\\\\generowanie\\\\magnes_chrzest_stopki_serce.py","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:42:57Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\tools\\\\generowanie\\\\magnes_chrzest_stopki_serce.py","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:43:11Z","tool":"Write","file":"C:\\\\Users\\\\jacek\\\\.claude\\\\projects\\\\C--visual-studio-code-projekty-orderPRO\\\\memory\\\\feedback_magnes_hide_excess.md","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:43:20Z","tool":"Edit","file":"C:\\\\Users\\\\jacek\\\\.claude\\\\projects\\\\C--visual-studio-code-projekty-orderPRO\\\\memory\\\\MEMORY.md","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:43:30Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.claude\\\\commands\\\\wygeneruj-projekty.md","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:43:43Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && python tools/generowanie/magnes_chrzest_stopki_serce.py --klient \"TEST Magnes 5\" --imie \"Rozalii\" --data \"19.04.2026\" --liczba 5 2>&1 | tail -15","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:43:56Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro --default-character-set=utf8mb4 -e \"INSERT INTO project_mapping","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:45:31Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\tools\\\\generowanie\\\\_explore_tmp.py","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:45:42Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro --default-character-set=utf8mb4 --table -e \"\\nSELECT osg.id, o","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:46:17Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro --table -e \"\\nSELECT os.code, os.label, osg.name as group_name","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:46:25Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro --default-character-set=utf8mb4 --vertical -e \"\\nSELECT oi.id,","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:46:45Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro --default-character-set=utf8mb4 -e \"\\nSELECT COUNT(*) FROM ord","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:47:00Z","tool":"Write","file":"C:\\\\Users\\\\jacek\\\\AppData\\\\Local\\\\Temp\\\\fix_mappings.sql","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:47:06Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro --default-character-set=utf8mb4 < /c/Users/jacek/AppData/Local/T","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:47:22Z","tool":"Write","file":"C:\\\\Users\\\\jacek\\\\.claude\\\\projects\\\\C--visual-studio-code-projekty-orderPRO\\\\memory\\\\feedback_mysql_utf8_insert.md","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:47:28Z","tool":"Edit","file":"C:\\\\Users\\\\jacek\\\\.claude\\\\projects\\\\C--visual-studio-code-projekty-orderPRO\\\\memory\\\\MEMORY.md","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:49:55Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && {\\nP_GOSIA=\"C:/tmp/orderpro_photos/gosiadrygas1@wp.pl_20260414000953_20260413_120237.jpg\"\\nZYCZ_DRYGAS_OJC=$'Choć jestem malutki, choć mówić n","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:50:08Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"UPDATE order_items SET project_generated=1, project_generat","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:51:48Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\tools\\\\generowanie\\\\pudelko_chrzest_dziewczynka_aniolek.py","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:52:02Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && rm -f tools/generowanie/_explore_tmp.py && python tools/generowanie/pudelko_chrzest_dziewczynka_aniolek.py --klient \"Karolina Kahl\" --imie \"Marceli","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-13T22:52:09Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"UPDATE order_items SET project_generated=1, project_generat","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-14T13:14:09Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.claude\\\\commands\\\\wygeneruj-projekty.md","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-14T13:14:20Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.claude\\\\commands\\\\wygeneruj-projekty.md","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-14T13:16:03Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && grep -E \"^DB_(HOST_REMOTE|DATABASE|USERNAME|PASSWORD|PORT)=\" .env\",\"description\":\"Read DB credentials from .env\"},\"tool_response\":{\"stdout\"","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-14T16:59:29Z","tool":"Bash","cmd":"mkdir -p \".paul/phases/102-apaczka-receiver-street-length\"\",\"description\":\"Create phase directory\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"\",\"interrupted\":false,\"isImage\":false,\"","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-14T17:00:26Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\phases\\\\102-apaczka-receiver-street-length\\\\102-01-PLAN.md","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-14T17:01:13Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-14T17:01:18Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-14T17:03:44Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\phases\\\\102-apaczka-receiver-street-length\\\\102-01-PLAN.md","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-14T17:04:42Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Shipments\\\\ApaczkaShipmentService.php","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-14T17:04:54Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Shipments\\\\ApaczkaShipmentService.php","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-14T17:05:36Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\tests\\\\Unit\\\\ApaczkaShipmentServiceTest.php","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-14T17:06:16Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\tests\\\\Unit\\\\ApaczkaShipmentServiceTest.php","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-14T17:06:52Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\phases\\\\102-apaczka-receiver-street-length\\\\102-01-SUMMARY.md","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-14T17:07:07Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-14T17:09:18Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-14T17:09:23Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-14T17:09:31Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/orderPRO"} diff --git a/.paul/phases/102-apaczka-receiver-street-length/102-01-PLAN.md b/.paul/phases/102-apaczka-receiver-street-length/102-01-PLAN.md new file mode 100644 index 0000000..ea018d3 --- /dev/null +++ b/.paul/phases/102-apaczka-receiver-street-length/102-01-PLAN.md @@ -0,0 +1,206 @@ +--- +phase: 102-apaczka-receiver-street-length +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - src/Modules/Shipments/ApaczkaShipmentService.php + - tests/Unit/Modules/Shipments/ApaczkaShipmentServiceTest.php +autonomous: true +delegation: off +--- + + +## Goal +Naprawic blad tworzenia przesylki Apaczka spowodowany przekroczeniem limitu 30 znakow w polu `receiver.street` (API Apaczka zwraca HTTP 400). Zachowanie zalezne od typu uslugi: +- **Uslugi punktowe (ORLEN Paczka, Paczkomat, punkty odbioru — `receiver_point_id` ustawione):** auto-truncate street do 30 znakow (street to etykieta punktu, nie realny adres dostarczenia). +- **Uslugi kurierskie (dostawa pod adres — brak `receiver_point_id`):** NIE obcinac automatycznie. Walidacja przed wywolaniem API: jesli street > 30, rzuc `ApaczkaApiException` z czytelnym komunikatem po polsku, zeby operator skorygowal adres recznie w formularzu. + +## Purpose +Zamowienie OP/ID=372 (Klaudia Florek-Mach, `Ul. Generała Pilota Józefa Kowalskiego 6/1` = 42 znaki, ORLEN Paczka) blokuje tworzenie przesylki. Automatyczne obcinanie jest bezpieczne dla punktow odbioru (street jest tylko labelem), ale NIE dla kuriera — moglibysmy uciac numer domu/mieszkania i paczka trafilaby w zle miejsce. + +## Output +- `ApaczkaShipmentService::buildReceiverAddress()` rozroznia tryby i stosuje wlasciwa strategie +- Dwa helpery: `truncateStreetForPoint()` i walidacja dla kuriera +- Unit testy pokrywajace oba scenariusze + edge cases + + + +## Project Context +@.paul/PROJECT.md +@.paul/ROADMAP.md +@.paul/STATE.md + +## Source Files +@src/Modules/Shipments/ApaczkaShipmentService.php +@src/Core/Exceptions/ApaczkaApiException.php + + + + +## AC-1: Usluga punktowa z dlugim street — auto-truncate +```gherkin +Given zamowienie z `receiver_point_id` = "POP-XYZ123" i street odbiorcy > 30 znakow (np. "Punkt odbioru POP-XYZ123 przy al. Jerozolimskich") +When ApaczkaShipmentService buduje receiver payload +Then receiver.line1 jest obcinane do 30 znakow (mb_substr + rtrim), request do API przechodzi +``` + +## AC-2: Usluga kurierska z dlugim street — walidacja + blad +```gherkin +Given zamowienie bez `receiver_point_id` i street > 30 znakow (np. "Ul. Generała Pilota Józefa Kowalskiego 6/1" = 42 znaki) +When ApaczkaShipmentService buduje receiver payload +Then rzucany jest ApaczkaApiException z komunikatem "Ulica odbiorcy przekracza 30 znakow (limit API Apaczka dla uslug kurierskich). Skroc adres recznie przed utworzeniem przesylki. Obecna wartosc: {N} znakow: {street}" +And request do API Apaczka NIE jest wykonywany +``` + +## AC-3: Krotki street — bez zmian (oba tryby) +```gherkin +Given street <= 30 znakow (np. "Polna 5") +When buduje receiver payload (usluga punktowa albo kurierska) +Then receiver.line1 = wartosc bez modyfikacji +``` + +## AC-4: Adres dokladnie 30 znakow — bez zmian +```gherkin +Given street o dlugosci dokladnie 30 znakow +When buduje receiver payload +Then receiver.line1 pozostaje 30 znakow, bez obcinania, bez wyjatku +``` + +## AC-5: Unit testy pokrywaja wszystkie AC +```gherkin +Given ApaczkaShipmentServiceTest +When uruchamiamy vendor/bin/phpunit --filter ApaczkaShipmentServiceTest +Then testy dla AC-1 (point truncate), AC-2 (courier throws), AC-3 (short passthrough), AC-4 (boundary 30) przechodza na zielono +``` + + + + + + + Task 1: Rozroznienie point vs courier w buildReceiverAddress + helpery + src/Modules/Shipments/ApaczkaShipmentService.php + + W `buildReceiverAddress()` po wyliczeniu `$street` a przed wstawieniem do `$receiver['line1']`: + + 1. Wyznacz tryb: `$isPointDelivery = $receiverPointId !== '';` (zmienna juz istnieje w metodzie). + 2. Zastosuj strategie zaleznie od trybu: + - Point: `$street = $this->truncateStreetForPoint($street);` + - Courier: `$this->assertStreetWithinCourierLimit($street);` (rzuca ApaczkaApiException jesli > 30) + 3. Dodaj dwie prywatne metody: + + ```php + /** + * Apaczka API: receiver.street limit 30 znakow. + * Dla uslug punktowych (ORLEN/Paczkomat) street to etykieta punktu — obcinamy bezpiecznie. + */ + private function truncateStreetForPoint(string $street): string + { + $street = trim($street); + if (mb_strlen($street, 'UTF-8') <= 30) { + return $street; + } + return rtrim(mb_substr($street, 0, 30, 'UTF-8')); + } + + /** + * Apaczka API: receiver.street limit 30 znakow. + * Dla uslug kurierskich NIE obcinamy — ryzyko utraty numeru domu/mieszkania. + * Operator musi skrocic adres recznie w formularzu. + */ + private function assertStreetWithinCourierLimit(string $street): void + { + $length = mb_strlen($street, 'UTF-8'); + if ($length <= 30) { + return; + } + throw new ApaczkaApiException(sprintf( + 'Ulica odbiorcy przekracza 30 znakow (limit API Apaczka dla uslug kurierskich). ' + . 'Skroc adres recznie przed utworzeniem przesylki. Obecna wartosc: %d znakow: "%s"', + $length, + $street + )); + } + ``` + + 4. Uzyj `mb_strlen`/`mb_substr` z jawnym `'UTF-8'` (polskie znaki). + 5. Upewnij sie, ze `ApaczkaApiException` jest zaimportowane w `use`. + + Avoid: + - `substr()`/`strlen()` bez `mb_` (psuje UTF-8) + - Automatyczne obcinanie w trybie kuriera (ryzyko utraty numeru) + - Modyfikacja innych pol odbiorcy + - Zmiana sygnatury `buildReceiverAddress` + + grep -nE "truncateStreetForPoint|assertStreetWithinCourierLimit|isPointDelivery" src/Modules/Shipments/ApaczkaShipmentService.php — widoczne wywolania + definicje + AC-1, AC-2, AC-3, AC-4 spelnione + + + + Task 2: Unit testy truncation point + walidacja courier + tests/Unit/Modules/Shipments/ApaczkaShipmentServiceTest.php + + Dodaj/rozszerz testy pokrywajace logike street length: + + **Helper access:** Jesli `truncateStreetForPoint` / `assertStreetWithinCourierLimit` sa private — uzyj `ReflectionMethod::setAccessible(true)`. + + **Test cases:** + 1. `testTruncateStreetForPointShortensLongValue` — input 45 znakow → output mb_strlen = 30 + 2. `testTruncateStreetForPointPreservesShortValue` — "Polna 5" → "Polna 5" bez zmian + 3. `testTruncateStreetForPointBoundary30` — 30 znakow → 30 znakow (bez zmian) + 4. `testTruncateStreetForPointUtf8Safe` — polskie znaki (35 znakow z ą/ę/ł) → mb_strlen = 30, bez uszkodzonych bajtow (sprawdz `mb_check_encoding($result, 'UTF-8')`) + 5. `testTruncateStreetForPointRtrimTrailingSpace` — gdy znak 30 to spacja, wynik bez koncowej spacji + 6. `testAssertStreetWithinCourierLimitAcceptsShort` — "Polna 5" → brak wyjatku + 7. `testAssertStreetWithinCourierLimitAcceptsBoundary30` — 30 znakow → brak wyjatku + 8. `testAssertStreetWithinCourierLimitRejectsLong` — "Ul. Generała Pilota Józefa Kowalskiego 6/1" (42 znaki) → `ApaczkaApiException` z komunikatem zawierajacym "przekracza 30 znakow" (PHPUnit `expectException` + `expectExceptionMessageMatches`) + + Uzyj `dg/bypass-finals` jesli klasa jest final. + + Avoid: integration testy (prawdziwe API), mockowanie calego flow order import. + + vendor/bin/phpunit --filter ApaczkaShipmentServiceTest + AC-5 spelnione + + + + + + +## DO NOT CHANGE +- Pozostale pola receiver (name, city, postal_code, country_code, phone, email) — nie ruszac +- `buildSenderAddress` i sender logic — poza zakresem +- `validateServiceRequirements`, `resolvePointAddress`, `applyPointIdentifiers` — nie ruszac +- Struktura payloadu API Apaczka (klucze: line1, line2, city, postal_code, ...) +- Inne providery (InPost ShipX, Allegro WZA) — poza zakresem +- UI/formularz przygotowania przesylki — brak zmian w widokach i kontrolerze + +## SCOPE LIMITS +- Tylko pole `receiver.street`/`line1` dla Apaczka +- Brak obslugi innych limitow dlugosci (name, city — jesli wystapia, osobne fazy) +- Brak automatycznego truncate w trybie kuriera (operator skraca recznie — celowa decyzja) +- Brak nowych komunikatow Flash/toast (blad przechodzi przez istniejacy kanal ApaczkaApiException -> ShipmentController) + + + + +Before declaring plan complete: +- [ ] vendor/bin/phpunit --filter ApaczkaShipmentServiceTest — zielone +- [ ] grep dla `truncateStreetForPoint` i `assertStreetWithinCourierLimit` zwraca definicje + wywolania w `buildReceiverAddress` +- [ ] Manualny re-test na zamowieniu 372 (ORLEN Paczka, 42-znakowy adres klienta) — tworzenie przesylki przechodzi (point truncate) +- [ ] Manualny re-test na zamowieniu z kurierem i adresem >30 znakow — widoczny bled walidacji ze zrozumialym komunikatem, brak wywolania API +- [ ] Wszystkie acceptance criteria spelnione + + + +- Usluga punktowa z dlugim street: przesylka tworzona pomyslnie (auto-truncate) +- Usluga kurierska z dlugim street: jasny komunikat dla operatora, brak ryzyka utraty numeru domu +- Krotkie adresy (<=30): obie sciezki bez zmian +- Unit testy pokrywaja 8 przypadkow +- Zero regresji w innych metodach serwisu + + + +After completion, create `.paul/phases/102-apaczka-receiver-street-length/102-01-SUMMARY.md` + diff --git a/.paul/phases/102-apaczka-receiver-street-length/102-01-SUMMARY.md b/.paul/phases/102-apaczka-receiver-street-length/102-01-SUMMARY.md new file mode 100644 index 0000000..0dfc3da --- /dev/null +++ b/.paul/phases/102-apaczka-receiver-street-length/102-01-SUMMARY.md @@ -0,0 +1,54 @@ +# Phase 102-01 — Apaczka Receiver Street Length + +**Status:** Complete +**Date:** 2026-04-14 + +## What Was Built + +Naprawiono blad HTTP 400 z API Apaczka: `receiver.street must be between 0 and 30` przy tworzeniu przesylki. + +### Strategia zalezna od typu uslugi + +- **Uslugi punktowe** (`receiver_point_id !== ''` — ORLEN Paczka, Paczkomat): auto-truncate street do 30 znakow przez `truncateStreetForPoint()` (mb_substr UTF-8 + rtrim). +- **Uslugi kurierskie** (brak `receiver_point_id`): `assertStreetWithinCourierLimit()` rzuca `ShipmentException` z komunikatem po polsku zawierajacym dlugosc i pelna wartosc — operator skraca recznie, zeby nie uciac numeru domu/mieszkania. + +## Files Modified + +- `src/Modules/Shipments/ApaczkaShipmentService.php` + - `buildReceiverAddress()` — dodano gałąź zaleznie od `$receiverPointId` + - Nowe metody: `truncateStreetForPoint(string): string`, `assertStreetWithinCourierLimit(string): void` +- `tests/Unit/ApaczkaShipmentServiceTest.php` (nowy plik, 8 test cases) + +## Decisions + +- **`ShipmentException` zamiast `ApaczkaApiException`** — klasa serwisu juz importuje `ShipmentException` i uzywa go spojnie. Dodawanie kolejnego typu bez powodu byloby niekonsystentne. Wyjatek propaguje przez `ShipmentController` do UI tak samo jak inne bledy przygotowania przesylki. +- **Brak auto-fallback na line2** — prosciej i bezpieczniej wymusic recznie skrocenie przez operatora niz zgadywac, jak podzielic adres (ryzyko utraty informacji dla kuriera). + +## Tests + +``` +PHPUnit 11.5.55 — OK (8 tests, 15 assertions) +``` + +Test cases: +1. `truncateStreetForPoint` — long value → ≤30 znakow, zaczyna sie od oryginalu +2. `truncateStreetForPoint` — short value (Polna 5) → bez zmian +3. `truncateStreetForPoint` — boundary 30 znakow → bez zmian +4. `truncateStreetForPoint` — UTF-8 (polskie znaki) → mb_check_encoding OK +5. `truncateStreetForPoint` — rtrim trailing space +6. `assertStreetWithinCourierLimit` — short accept +7. `assertStreetWithinCourierLimit` — boundary 30 accept +8. `assertStreetWithinCourierLimit` — 42-znakowy adres → ShipmentException z "przekracza 30 znakow" + +## AC Status + +- [x] AC-1: Usluga punktowa + dlugi street → truncate do ≤30 +- [x] AC-2: Usluga kurierska + dlugi street → ShipmentException +- [x] AC-3: Krotki street (<=30) → bez zmian +- [x] AC-4: Boundary 30 → bez zmian +- [x] AC-5: Unit testy zielone + +## Manual Re-test + +- Zamowienie 372 (Klaudia Florek-Mach, ORLEN Paczka, customer street 42 znaki): nalezy przetestowac w UI — oczekiwane: przesylka tworzona pomyslnie (sciezka point-truncate). +- Zamowienie kurierskie z dlugim street: oczekiwane — widoczny komunikat walidacji z zachetą do skrócenia adresu. diff --git a/.vscode/ftp-kr.sync.cache.json b/.vscode/ftp-kr.sync.cache.json index 671d25c..b6a2d3a 100644 --- a/.vscode/ftp-kr.sync.cache.json +++ b/.vscode/ftp-kr.sync.cache.json @@ -858,6 +858,12 @@ "size": 121, "lmtime": 1776018293548, "modified": false + }, + "20260413_000100_ensure_orders_delivery_payment_columns.sql": { + "type": "-", + "size": 1327, + "lmtime": 1776110985771, + "modified": false } }, "seeders": {}, @@ -2122,6 +2128,16 @@ "lmtime": 1772489488633, "modified": false }, + "pip-build-tracker-apg7g7wx": {}, + "pip-ephem-wheel-cache-1_43nyjz": {}, + "pip-install-3zoivscf": {}, + "pip-metadata-11qp_dbm": {}, + "pip-metadata-ntnnrio2": {}, + "pip-metadata-sfvx8ulh": {}, + "pip-unpack-0jcnlnr0": {}, + "pip-unpack-1ei5wymd": {}, + "pip-unpack-30yj5x6k": {}, + "pip-unpack-ayre5y33": {}, ".playwright-mcp": { "allegro-tracking-history.md": { "type": "-", @@ -2201,8 +2217,8 @@ "css": { "app.css": { "type": "-", - "size": 66847, - "lmtime": 1775950421440, + "size": 56754, + "lmtime": 1776112098657, "modified": false }, "app.css.map": { @@ -2214,7 +2230,7 @@ "login.css": { "type": "-", "size": 6586, - "lmtime": 1775817361366, + "lmtime": 1776112099165, "modified": false }, "login.css.map": { @@ -2328,8 +2344,8 @@ }, "app.scss": { "type": "-", - "size": 47675, - "lmtime": 1775947092385, + "size": 49917, + "lmtime": 1776112092617, "modified": false }, "components": { @@ -2456,9 +2472,9 @@ }, "table-list.php": { "type": "-", - "size": 22454, - "lmtime": 1771925480312, - "modified": true + "size": 22591, + "lmtime": 1776112045330, + "modified": false } }, "dashboard": { @@ -2544,8 +2560,8 @@ }, "show.php": { "type": "-", - "size": 54991, - "lmtime": 1775948492570, + "size": 62276, + "lmtime": 1776111575613, "modified": false } }, @@ -2714,8 +2730,8 @@ "shipments": { "prepare.php": { "type": "-", - "size": 51866, - "lmtime": 1774295564443, + "size": 53207, + "lmtime": 1776111603998, "modified": false } }, @@ -2740,8 +2756,8 @@ "routes": { "web.php": { "type": "-", - "size": 30318, - "lmtime": 1775947124874, + "size": 30521, + "lmtime": 1776111496634, "modified": false } }, @@ -3186,14 +3202,14 @@ }, "OrdersController.php": { "type": "-", - "size": 45567, - "lmtime": 1775948470375, + "size": 48524, + "lmtime": 1776112065501, "modified": false }, "OrdersRepository.php": { "type": "-", - "size": 41935, - "lmtime": 1775948577940, + "size": 45019, + "lmtime": 1776110603173, "modified": false }, "OrderStatusSyncService.php": { @@ -3212,8 +3228,8 @@ }, "PrintApiController.php": { "type": "-", - "size": 5405, - "lmtime": 1774473708117, + "size": 6109, + "lmtime": 1776111450009, "modified": false }, "PrintApiKeyRepository.php": { @@ -3224,8 +3240,8 @@ }, "PrintJobRepository.php": { "type": "-", - "size": 4696, - "lmtime": 1774474858944, + "size": 5537, + "lmtime": 1776111457539, "modified": false } }, @@ -3688,8 +3704,8 @@ }, "ShipmentController.php": { "type": "-", - "size": 22456, - "lmtime": 1775665182684, + "size": 22473, + "lmtime": 1776111534135, "modified": false }, "ShipmentPackageRepository.php": { @@ -5668,6 +5684,12 @@ "lmtime": 1776019295877, "modified": false }, + "akrylowe_podziekowanie_ojciec_chrzestny_wzor2.py": { + "type": "-", + "size": 6039, + "lmtime": 1776119409623, + "modified": false + }, "_assets": { "green_placeholder.png": { "type": "-", @@ -5718,6 +5740,12 @@ "lmtime": 1775981736922, "modified": false }, + "email_message_fetcher.py": { + "type": "-", + "size": 5421, + "lmtime": 1776117874899, + "modified": false + }, "email_photo_fetcher.py": { "type": "-", "size": 5977, @@ -5730,6 +5758,24 @@ "lmtime": 1776017342215, "modified": false }, + "_explore_tmp.py": { + "type": "-", + "size": 877, + "lmtime": 1776120330850, + "modified": false + }, + "magnes_babcia_kocham_babciu.py": { + "type": "-", + "size": 2208, + "lmtime": 1776034684478, + "modified": false + }, + "magnes_chrzest_stopki_serce.py": { + "type": "-", + "size": 4706, + "lmtime": 1776120177373, + "modified": false + }, "magnes_wieczor_panienski_kieliszki.py": { "type": "-", "size": 2803, @@ -5756,8 +5802,8 @@ }, "pudelko_chrzest_dziewczynka_aniolek.py": { "type": "-", - "size": 4334, - "lmtime": 1776015574853, + "size": 4491, + "lmtime": 1776120707770, "modified": false }, "pudelko_chrzest_golabek_hdf.py": { @@ -5814,6 +5860,12 @@ "lmtime": 1776034067875, "modified": false }, + "pudelko_komunia_kielich.py": { + "type": "-", + "size": 3487, + "lmtime": 1776119915146, + "modified": false + }, "pudelko_komunia_kwiaty.py": { "type": "-", "size": 3374, @@ -5832,6 +5884,12 @@ "size": 5380, "lmtime": 1776014788359, "modified": false + }, + "email_photo_fetcher.cpython-312.pyc": { + "type": "-", + "size": 9439, + "lmtime": 1776118155400, + "modified": false } }, "_rename_temp.py": { @@ -5840,6 +5898,12 @@ "lmtime": 1776017830161, "modified": false }, + "_rename_tmp.py": { + "type": "-", + "size": 1436, + "lmtime": 1776120070331, + "modified": false + }, "_tmp_photos": { "01_IMG_7330.jpeg": { "type": "-", @@ -5865,12 +5929,6 @@ "size": 5328, "lmtime": 1776032317220, "modified": false - }, - "magnes_babcia_kocham_babciu.py": { - "type": "-", - "size": 2208, - "lmtime": 1776034684478, - "modified": false } } }, diff --git a/src/Modules/Shipments/ApaczkaShipmentService.php b/src/Modules/Shipments/ApaczkaShipmentService.php index b7a7d56..66bdc97 100644 --- a/src/Modules/Shipments/ApaczkaShipmentService.php +++ b/src/Modules/Shipments/ApaczkaShipmentService.php @@ -485,6 +485,15 @@ final class ApaczkaShipmentService implements ShipmentProviderInterface } } + // Apaczka API: receiver.street limit 30 znakow. Dla uslug punktowych street to etykieta + // punktu — obcinamy bezpiecznie. Dla uslug kurierskich rzucamy wyjatek, zeby operator + // skrocil adres recznie (auto-truncate mogloby uciac numer domu/mieszkania). + if ($receiverPointId !== '') { + $street = $this->truncateStreetForPoint($street); + } else { + $this->assertStreetWithinCourierLimit($street); + } + if ($name === '' || $street === '' || $city === '' || $postalCode === '' || $countryCode === '') { throw new ShipmentException('Brak wymaganych danych adresowych odbiorcy.'); } @@ -519,6 +528,39 @@ final class ApaczkaShipmentService implements ShipmentProviderInterface return $receiver; } + /** + * Apaczka API: receiver.street limit 30 znakow. + * Dla uslug punktowych (ORLEN/Paczkomat) street to etykieta punktu — obcinamy bezpiecznie. + */ + private function truncateStreetForPoint(string $street): string + { + $street = trim($street); + if (mb_strlen($street, 'UTF-8') <= 30) { + return $street; + } + + return rtrim(mb_substr($street, 0, 30, 'UTF-8')); + } + + /** + * Apaczka API: receiver.street limit 30 znakow. + * Dla uslug kurierskich NIE obcinamy — ryzyko utraty numeru domu/mieszkania. + */ + private function assertStreetWithinCourierLimit(string $street): void + { + $length = mb_strlen($street, 'UTF-8'); + if ($length <= 30) { + return; + } + + throw new ShipmentException(sprintf( + 'Ulica odbiorcy przekracza 30 znakow (limit API Apaczka dla uslug kurierskich). ' + . 'Skroc adres recznie przed utworzeniem przesylki. Obecna wartosc: %d znakow: "%s"', + $length, + $street + )); + } + /** * @param array $address */ diff --git a/tests/Unit/ApaczkaShipmentServiceTest.php b/tests/Unit/ApaczkaShipmentServiceTest.php new file mode 100644 index 0000000..e1dad1b --- /dev/null +++ b/tests/Unit/ApaczkaShipmentServiceTest.php @@ -0,0 +1,110 @@ +service = new ApaczkaShipmentService( + $this->createMock(ApaczkaIntegrationRepository::class), + $this->createMock(ApaczkaApiClient::class), + $this->createMock(ShipmentPackageRepository::class), + $this->createMock(CompanySettingsRepository::class), + $this->createMock(OrdersRepository::class) + ); + } + + private function invokeTruncate(string $street): string + { + $method = new ReflectionMethod(ApaczkaShipmentService::class, 'truncateStreetForPoint'); + $method->setAccessible(true); + + return (string) $method->invoke($this->service, $street); + } + + private function invokeAssert(string $street): void + { + $method = new ReflectionMethod(ApaczkaShipmentService::class, 'assertStreetWithinCourierLimit'); + $method->setAccessible(true); + $method->invoke($this->service, $street); + } + + public function testTruncateStreetForPointShortensLongValue(): void + { + $input = 'Punkt odbioru POP-XYZ123 przy al. Jerozolimskich 55A/12'; + $result = $this->invokeTruncate($input); + + $this->assertLessThanOrEqual(30, mb_strlen($result, 'UTF-8')); + $this->assertStringStartsWith('Punkt odbioru', $result); + } + + public function testTruncateStreetForPointPreservesShortValue(): void + { + $this->assertSame('Polna 5', $this->invokeTruncate('Polna 5')); + } + + public function testTruncateStreetForPointBoundary30(): void + { + $input = str_repeat('A', 30); + + $result = $this->invokeTruncate($input); + + $this->assertSame(30, mb_strlen($result, 'UTF-8')); + $this->assertSame($input, $result); + } + + public function testTruncateStreetForPointUtf8Safe(): void + { + $input = 'Ulica Żółtych Słoneczników Małych 12345'; + $this->assertGreaterThan(30, mb_strlen($input, 'UTF-8')); + + $result = $this->invokeTruncate($input); + + $this->assertSame(30, mb_strlen($result, 'UTF-8')); + $this->assertTrue(mb_check_encoding($result, 'UTF-8')); + } + + public function testTruncateStreetForPointRtrimTrailingSpace(): void + { + $input = str_repeat('A', 29) . ' XYZ'; + + $result = $this->invokeTruncate($input); + + $this->assertSame(29, mb_strlen($result, 'UTF-8')); + $this->assertSame(str_repeat('A', 29), $result); + } + + public function testAssertStreetWithinCourierLimitAcceptsShort(): void + { + $this->invokeAssert('Polna 5'); + $this->addToAssertionCount(1); + } + + public function testAssertStreetWithinCourierLimitAcceptsBoundary30(): void + { + $this->invokeAssert(str_repeat('A', 30)); + $this->addToAssertionCount(1); + } + + public function testAssertStreetWithinCourierLimitRejectsLong(): void + { + $this->expectException(ShipmentException::class); + $this->expectExceptionMessageMatches('/przekracza 30 znakow/'); + + $this->invokeAssert('Ul. Generała Pilota Józefa Kowalskiego 6/1'); + } +} diff --git a/tools/generowanie/__pycache__/email_photo_fetcher.cpython-312.pyc b/tools/generowanie/__pycache__/email_photo_fetcher.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9eab78e7d37652ee6a36af1db4c35c06b706d43d GIT binary patch literal 9439 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 literal 0 HcmV?d00001 diff --git a/tools/generowanie/akrylowe_podziekowanie_ojciec_chrzestny_wzor2.py b/tools/generowanie/akrylowe_podziekowanie_ojciec_chrzestny_wzor2.py new file mode 100644 index 0000000..377dcb6 --- /dev/null +++ b/tools/generowanie/akrylowe_podziekowanie_ojciec_chrzestny_wzor2.py @@ -0,0 +1,168 @@ +""" +Generator PSD — Akrylowe podziekowanie na chrzest dla Ojca Chrzestnego (Wzór 2). + +Dwa warianty szablonu (chlopiec / dziewczynka) wybierane parametrem --plec. +Oba PSD maja identyczna strukture warstw w "Warstwa 1": +imie, data, naglowek, tytul, podziekowanie oraz Smart Object "zdjecie". + +Podmiana zdjecia — jak w Wzorze 1: cover do oryginalnych boundsow, +zielony placeholder gdy brak zdjecia. + +Wymaga: uruchomiony Adobe Photoshop, photoshop-python-api, Pillow. +""" + +import argparse +import os +import photoshop.api as ps + + +PROJEKT_DIR = os.path.join( + r"d:\pomysloweprezenty.pl\projekty\chrzest - podziękowania", + "Akrylowe podziękowanie na chrzest dla Ojca Chrzestnego - Wzór 2", +) +SZABLON_CHLOPIEC = os.path.join( + PROJEKT_DIR, "Akrylowe podziękowanie na chrzest dla Ojca Chrzestnego - Wzór 2 CMYK - chłopiec.psd" +) +SZABLON_DZIEWCZYNKA = os.path.join( + PROJEKT_DIR, "Akrylowe podziękowanie na chrzest dla Ojca Chrzestnego - Wzór 2 CMYK - dziewczynka.psd" +) +GOTOWE_DIR = os.path.join(PROJEKT_DIR, "_gotowe") + +ASSETS_DIR = os.path.join(os.path.dirname(__file__), "_assets") +GREEN_PLACEHOLDER_PATH = os.path.join(ASSETS_DIR, "green_placeholder.png") + + +def ensure_green_placeholder(): + if os.path.exists(GREEN_PLACEHOLDER_PATH): + return GREEN_PLACEHOLDER_PATH + os.makedirs(ASSETS_DIR, exist_ok=True) + from PIL import Image + Image.new("RGB", (1000, 1000), (0, 255, 0)).save(GREEN_PLACEHOLDER_PATH) + return GREEN_PLACEHOLDER_PATH + + +def set_text(layer, new_text): + b = [float(x) for x in layer.bounds] + cx, cy = (b[0] + b[2]) / 2, (b[1] + b[3]) / 2 + layer.textItem.contents = new_text + b2 = [float(x) for x in layer.bounds] + cx2, cy2 = (b2[0] + b2[2]) / 2, (b2[1] + b2[3]) / 2 + dx, dy = cx - cx2, cy - cy2 + if dx or dy: + layer.translate(dx, dy) + + +def set_layer_text(container, layer_name, new_text): + if new_text is None: + return + layer = container.artLayers[layer_name] + old = layer.textItem.contents + set_text(layer, new_text) + print(f' {layer_name}: "{old}" -> "{new_text}"') + + +def replace_smart_object_contents(app, file_path): + desc = ps.ActionDescriptor() + desc.putPath(app.charIDToTypeID("null"), file_path) + desc.putInteger(app.charIDToTypeID("PgNm"), 1) + app.executeAction(app.stringIDToTypeID("placedLayerReplaceContents"), desc) + + +def replace_photo_cover(app, so_layer, photo_path): + target = [float(x) for x in so_layer.bounds] + target_w = target[2] - target[0] + target_h = target[3] - target[1] + target_cx = (target[0] + target[2]) / 2 + target_cy = (target[1] + target[3]) / 2 + + app.activeDocument.activeLayer = so_layer + replace_smart_object_contents(app, photo_path) + + new_b = [float(x) for x in so_layer.bounds] + new_w = new_b[2] - new_b[0] + new_h = new_b[3] - new_b[1] + if new_w <= 0 or new_h <= 0: + return + + scale = max(target_w / new_w, target_h / new_h) * 100.0 + so_layer.resize(scale, scale, ps.AnchorPosition.MiddleCenter) + + cur = [float(x) for x in so_layer.bounds] + cur_cx = (cur[0] + cur[2]) / 2 + cur_cy = (cur[1] + cur[3]) / 2 + so_layer.translate(target_cx - cur_cx, target_cy - cur_cy) + + +def generate(klient, imie, data, plec, zdjecie=None, naglowek=None, tytul=None, podziekowanie=None): + if plec == "m": + szablon_path = SZABLON_CHLOPIEC + elif plec == "k": + szablon_path = SZABLON_DZIEWCZYNKA + else: + raise ValueError(f"--plec musi byc 'm' lub 'k', otrzymano: {plec!r}") + + os.makedirs(GOTOWE_DIR, exist_ok=True) + output_path = os.path.join(GOTOWE_DIR, f"{klient}.psd") + + app = ps.Application() + doc = app.open(szablon_path) + print(f"Otwarto szablon: {doc.name}") + + w1 = doc.layerSets["Warstwa 1"] + + print(" Podmiana tekstow:") + set_layer_text(w1, "imie", imie) + set_layer_text(w1, "data", data) + set_layer_text(w1, "naglowek", naglowek) + set_layer_text(w1, "tytul", tytul) + set_layer_text(w1, "podziekowanie", podziekowanie) + + so_layer = w1.artLayers["zdjecie"] + if zdjecie and os.path.isfile(zdjecie): + print(f" Podmiana zdjecia: {zdjecie}") + replace_photo_cover(app, so_layer, zdjecie) + else: + if zdjecie: + print(f" UWAGA: plik zdjecia nie istnieje ({zdjecie}) — wstawiam zielony placeholder") + else: + print(" Brak --zdjecie — wstawiam zielony placeholder") + replace_photo_cover(app, so_layer, ensure_green_placeholder()) + + psd_opts = ps.PhotoshopSaveOptions() + doc.saveAs(output_path, psd_opts, True) + print(f"Zapisano: {output_path}") + + doc.close(ps.SaveOptions.DoNotSaveChanges) + print("Gotowe!") + return output_path + + +def main(): + parser = argparse.ArgumentParser( + description="Generator PSD - Akrylowe podziekowanie chrzest, Ojciec Chrzestny Wzor 2 (chlopiec/dziewczynka)" + ) + parser.add_argument("--klient", required=True, help="Nazwa pliku wyjsciowego") + parser.add_argument("--imie", required=True, help="Imie dziecka (np. Wojtus, Blanka)") + parser.add_argument("--data", required=True, help="Data uroczystosci (np. 19.04.2026)") + parser.add_argument("--plec", required=True, choices=["m", "k"], + help="Plec dziecka: 'm' = chlopiec, 'k' = dziewczynka") + parser.add_argument("--zdjecie", default=None, help="Sciezka do zdjecia (opcjonalnie; brak = zielony placeholder)") + parser.add_argument("--naglowek", default=None, help="Naglowek (opcjonalnie, domyslnie 'OJCZE CHRZESTNY')") + parser.add_argument("--tytul", default=None, help="Tytul (opcjonalnie, domyslnie 'Dziękuję')") + parser.add_argument("--podziekowanie", default=None, help="Tresc podziekowania (opcjonalnie, domyslna z szablonu)") + + args = parser.parse_args() + generate( + klient=args.klient, + imie=args.imie, + data=args.data, + plec=args.plec, + zdjecie=args.zdjecie, + naglowek=args.naglowek, + tytul=args.tytul, + podziekowanie=args.podziekowanie, + ) + + +if __name__ == "__main__": + main() diff --git a/tools/generowanie/email_message_fetcher.py b/tools/generowanie/email_message_fetcher.py new file mode 100644 index 0000000..d1e7680 --- /dev/null +++ b/tools/generowanie/email_message_fetcher.py @@ -0,0 +1,169 @@ +""" +Pobieranie wiadomosci tekstowych od klientow z poczty IMAP. + +Bliznaczy modul do `email_photo_fetcher.py`, ale zamiast zalacznikow graficznych +zwraca tresc tekstowa wiadomosci (body) od podanego nadawcy. Uzywany przez silnik +`/wygeneruj-projekty` aby wykryc dodatkowe poprawki/uwagi ktore klient przysylal +mailem juz po zlozeniu zamowienia. + +Skrzynka: EMAIL_01, folder: INBOX (na stale — zgodnie z wymaganiem). + +Wymagane zmienne srodowiskowe (.env w katalogu projektu): + EMAIL_01_HOST + EMAIL_01_USERNAME + EMAIL_01_PASSWORD + EMAIL_01_IMAP_PORT - opcjonalnie, domyslnie 993 + +Uzycie programowe: + from email_message_fetcher import fetch_customer_messages + items = fetch_customer_messages("klient@example.com", days_back=60) + # items: list[dict] z kluczami: date, subject, body + +Uzycie z CLI: + python email_message_fetcher.py --email klient@example.com --days 60 +""" + +import argparse +import email +import imaplib +import json +import sys +from datetime import datetime, timedelta +from email.header import decode_header + +from email_photo_fetcher import _imap_config # reuse env loader + + +def _decode_header_value(raw): + if not raw: + return "" + parts = decode_header(raw) + out = [] + for chunk, enc in parts: + if isinstance(chunk, bytes): + try: + out.append(chunk.decode(enc or "utf-8", errors="replace")) + except LookupError: + out.append(chunk.decode("utf-8", errors="replace")) + else: + out.append(chunk) + return "".join(out).strip() + + +def _extract_text_body(msg): + """Zwraca text/plain z wiadomosci; gdy brak — fallback do text/html (bez tagow).""" + plain_parts = [] + html_parts = [] + + for part in msg.walk(): + if part.get_content_maintype() == "multipart": + continue + disposition = str(part.get("Content-Disposition") or "").lower() + if "attachment" in disposition: + continue + + ctype = part.get_content_type() + payload = part.get_payload(decode=True) + if not payload: + continue + charset = part.get_content_charset() or "utf-8" + try: + text = payload.decode(charset, errors="replace") + except LookupError: + text = payload.decode("utf-8", errors="replace") + + if ctype == "text/plain": + plain_parts.append(text) + elif ctype == "text/html": + html_parts.append(text) + + if plain_parts: + return "\n".join(p.strip() for p in plain_parts).strip() + + if html_parts: + import re + raw = "\n".join(html_parts) + raw = re.sub(r"(?is)<(script|style).*?>.*?", " ", raw) + raw = re.sub(r"(?s)<[^>]+>", " ", raw) + raw = re.sub(r"[ \t]+", " ", raw) + raw = re.sub(r"\s*\n\s*", "\n", raw) + return raw.strip() + + return "" + + +def fetch_customer_messages(customer_email, days_back=60): + """Szuka maili od `customer_email` w INBOX z ostatnich `days_back` dni. + Zwraca liste dict {date, subject, body}, od najnowszego do najstarszego.""" + cfg = _imap_config() + if not cfg["host"] or not cfg["user"] or not cfg["password"]: + raise RuntimeError( + "Brak konfiguracji konta w .env — wymagane: EMAIL_01_HOST, EMAIL_01_USERNAME, EMAIL_01_PASSWORD" + ) + + imap = imaplib.IMAP4_SSL(cfg["host"], cfg["port"]) + results = [] + + try: + imap.login(cfg["user"], cfg["password"]) + imap.select("INBOX") + + since = (datetime.now() - timedelta(days=days_back)).strftime("%d-%b-%Y") + criteria = f'(FROM "{customer_email}" SINCE "{since}")' + status, data = imap.search(None, criteria) + if status != "OK" or not data or not data[0]: + return results + + ids = data[0].split() + for msg_id in reversed(ids): + status, msg_data = imap.fetch(msg_id, "(RFC822)") + if status != "OK" or not msg_data or not msg_data[0]: + continue + msg = email.message_from_bytes(msg_data[0][1]) + + subject = _decode_header_value(msg.get("Subject")) + date_hdr = msg.get("Date") or "" + body = _extract_text_body(msg) + + if not body: + continue + + results.append({"date": date_hdr, "subject": subject, "body": body}) + + return results + finally: + try: + imap.logout() + except Exception: + pass + + +def main(): + parser = argparse.ArgumentParser(description="Pobierz wiadomosci klienta z INBOX") + parser.add_argument("--email", required=True, help="Adres email klienta (nadawca)") + parser.add_argument("--days", type=int, default=60, help="Ile dni wstecz szukac (domyslnie 60)") + parser.add_argument("--json", action="store_true", help="Wyjscie w formacie JSON") + args = parser.parse_args() + + try: + items = fetch_customer_messages(args.email, days_back=args.days) + except Exception as exc: + print(f"BLAD: {exc}") + sys.exit(2) + + if not items: + print("BRAK: nie znaleziono wiadomosci od tego klienta w INBOX") + sys.exit(1) + + if args.json: + print(json.dumps(items, ensure_ascii=False, indent=2)) + else: + for i, it in enumerate(items, 1): + print(f"=== [{i}] {it['date']} | {it['subject']} ===") + print(it["body"]) + print() + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/tools/generowanie/magnes_chrzest_stopki_serce.py b/tools/generowanie/magnes_chrzest_stopki_serce.py new file mode 100644 index 0000000..16f0616 --- /dev/null +++ b/tools/generowanie/magnes_chrzest_stopki_serce.py @@ -0,0 +1,140 @@ +""" +Generator PSD - Magnes podziekowanie na chrzest UV - Stopki serce. + +Makieta A3 z 15 kopiami magnesu linkowanymi do Smart Object "projekt". +Edycja warstw wewnatrz SO "projekt" propaguje zmiany na wszystkie kopie. + +Struktura wewnatrz Smart Object "projekt" -> "Warstwa 2": +- podziekowanie (text, domyslnie "Dziękuję za przybycie.") +- data (DD.MM.RRRR) +- tytul (2 linie: "Chrzest Świętego\r{imie}") + +Wymaga: uruchomiony Adobe Photoshop, photoshop-python-api. +""" + +import argparse +import os +import photoshop.api as ps + + +PROJEKT_DIR = os.path.join( + r"d:\pomysloweprezenty.pl\projekty\chrzest - magnesy", + "Magnes podziękowanie dla gości na chrzest UV - Stopki serce", +) +SZABLON_PATH = os.path.join(PROJEKT_DIR, "magnesy serduszka makieta A3 CMYK.psd") +GOTOWE_DIR = os.path.join(PROJEKT_DIR, "_gotowe") + +SMART_OBJECT_NAME = "projekt" +INNER_SET_NAME = "Warstwa 2" + + +def set_text(layer, new_text): + """Zmienia tekst warstwy zachowujac srodek bounding boxa.""" + b = [float(x) for x in layer.bounds] + cx, cy = (b[0] + b[2]) / 2, (b[1] + b[3]) / 2 + layer.textItem.contents = new_text + b2 = [float(x) for x in layer.bounds] + cx2, cy2 = (b2[0] + b2[2]) / 2, (b2[1] + b2[3]) / 2 + dx, dy = cx - cx2, cy - cy2 + if dx or dy: + layer.translate(dx, dy) + + +def set_layer_text(container, layer_name, new_text): + if new_text is None: + return + layer = container.artLayers[layer_name] + old = layer.textItem.contents + set_text(layer, new_text) + print(f' {layer_name}: "{old[:30]}..." -> "{new_text[:40]}..."') + + +def enter_smart_object(app, layer): + app.activeDocument.activeLayer = layer + desc = ps.ActionDescriptor() + ref = ps.ActionReference() + ref.putEnumerated( + app.stringIDToTypeID("layer"), + app.stringIDToTypeID("ordinal"), + app.stringIDToTypeID("targetEnum"), + ) + desc.putReference(app.stringIDToTypeID("null"), ref) + app.executeAction(app.stringIDToTypeID("placedLayerEditContents"), desc) + + +def hide_excess_copies(doc, liczba): + """Ukrywa nadmiarowe kopie magnesu (szablon ma 15 slotow).""" + slot_layers = [] + for al in doc.artLayers: + if al.name == SMART_OBJECT_NAME or al.name.startswith("main kopia"): + slot_layers.append(al) + total = len(slot_layers) + if liczba >= total: + return + for layer in slot_layers[liczba:]: + layer.visible = False + print(f" Ukryto {total - liczba}/{total} slotow (zamowienie: {liczba})") + + +def generate(klient, imie, data, liczba, podziekowanie=None): + os.makedirs(GOTOWE_DIR, exist_ok=True) + output_path = os.path.join(GOTOWE_DIR, f"{klient}.psd") + + app = ps.Application() + doc = app.open(SZABLON_PATH) + print(f"Otwarto szablon: {doc.name}") + + try: + projekt_layer = None + for al in doc.artLayers: + if al.name == SMART_OBJECT_NAME: + projekt_layer = al + break + if projekt_layer is None: + raise RuntimeError(f'Nie znaleziono warstwy Smart Object "{SMART_OBJECT_NAME}"') + + enter_smart_object(app, projekt_layer) + inner = app.activeDocument + print(f" Wewnatrz SO: {inner.name}") + + w2 = inner.layerSets[INNER_SET_NAME] + set_layer_text(w2, "tytul", f"Chrzest Świętego\r{imie}") + set_layer_text(w2, "data", data) + set_layer_text(w2, "podziekowanie", podziekowanie) + + inner.close(ps.SaveOptions.SaveChanges) + + hide_excess_copies(doc, liczba) + + psd_opts = ps.PhotoshopSaveOptions() + doc.saveAs(output_path, psd_opts, True) + print(f"Zapisano: {output_path}") + finally: + app.activeDocument.close(ps.SaveOptions.DoNotSaveChanges) + print("Gotowe!") + + return output_path + + +def main(): + parser = argparse.ArgumentParser(description="Generator PSD - Magnes chrzest, Stopki serce") + parser.add_argument("--klient", required=True, help="Nazwa pliku wyjsciowego") + parser.add_argument("--imie", required=True, help="Imie dziecka (dopelniacz, np. Rozalii, Szymona)") + parser.add_argument("--data", required=True, help='Data chrztu, format "DD.MM.RRRR" (np. 19.04.2026)') + parser.add_argument("--liczba", type=int, default=15, + help="Liczba zamowionych magnesow (1-15, nadmiar slotow ukrywany; domyslnie 15)") + parser.add_argument("--podziekowanie", default=None, + help='Tresc podziekowania (opcjonalnie, domyslnie "Dziękuję za przybycie.")') + + args = parser.parse_args() + generate( + klient=args.klient, + imie=args.imie, + data=args.data, + liczba=args.liczba, + podziekowanie=args.podziekowanie, + ) + + +if __name__ == "__main__": + main() diff --git a/tools/generowanie/pudelko_chrzest_dziewczynka_aniolek.py b/tools/generowanie/pudelko_chrzest_dziewczynka_aniolek.py index 0b48cbf..99d51cf 100644 --- a/tools/generowanie/pudelko_chrzest_dziewczynka_aniolek.py +++ b/tools/generowanie/pudelko_chrzest_dziewczynka_aniolek.py @@ -58,18 +58,23 @@ def generate(klient, imie, data, od_kogo, zyczenia=None, naglowek=None, tytul=No print(" [KP] data:") set_layer_text(doc.layerSets["KP"], "data", data) - # GL — życzenia (Layer 2 → dane) - print(" [GL] życzenia:") - gl_dane = doc.layerSets["GL"].layerSets["dane"] - set_layer_text(gl_dane, "od_kogo", od_kogo) - set_layer_text(gl_dane, "naglowek", naglowek) - set_layer_text(gl_dane, "zyczenia", zyczenia) + # GL/GP — opcjonalne (niektóre warianty szablonu mają tylko DL/DP) + try: + gl_dane = doc.layerSets["GL"].layerSets["dane"] + print(" [GL] życzenia:") + set_layer_text(gl_dane, "od_kogo", od_kogo) + set_layer_text(gl_dane, "naglowek", naglowek) + set_layer_text(gl_dane, "zyczenia", zyczenia) + except Exception: + print(" [GL] pomijam (brak w szablonie)") - # GP — imię + tytuł (Layer 1 kopia → dane); brak daty na ściance - print(" [GP] imię/tytuł:") - gp_dane = doc.layerSets["GP"].layerSets["dane"] - set_layer_text(gp_dane, "imie", imie) - set_layer_text(gp_dane, "tytul", tytul) + try: + gp_dane = doc.layerSets["GP"].layerSets["dane"] + print(" [GP] imię/tytuł:") + set_layer_text(gp_dane, "imie", imie) + set_layer_text(gp_dane, "tytul", tytul) + except Exception: + print(" [GP] pomijam (brak w szablonie)") # DL — życzenia (warstwy bezpośrednio w secie) print(" [DL] życzenia:") diff --git a/tools/generowanie/pudelko_komunia_kielich.py b/tools/generowanie/pudelko_komunia_kielich.py new file mode 100644 index 0000000..d4020a7 --- /dev/null +++ b/tools/generowanie/pudelko_komunia_kielich.py @@ -0,0 +1,101 @@ +""" +Generator PSD - Pudelko na pieniadze z zyczeniami na Komunie (Kielich). + +Makieta A3 z panelami: +- DL: od_kogo, naglowek, zyczenia +- DP: imie, data, tytul + +Format daty w szablonie: "DD.MM.RRRR" (standardowy). + +Wymaga: uruchomiony Adobe Photoshop, photoshop-python-api. +""" + +import argparse +import os +import photoshop.api as ps + + +PROJEKT_DIR = os.path.join( + r"d:\pomysloweprezenty.pl\projekty\komunia święta - pudełka na pieniądze", + "Pudełko na pieniądze z życzeniami na Komunie Świętą - Kielich", +) +SZABLON_PATH = os.path.join(PROJEKT_DIR, "pudełka na chrzest - makieta A3 CMYK.psd") +GOTOWE_DIR = os.path.join(PROJEKT_DIR, "_gotowe") + + +def set_text(layer, new_text): + """Zmienia tekst warstwy zachowujac srodek bounding boxa (centrowane warstwy).""" + b = [float(x) for x in layer.bounds] + cx, cy = (b[0] + b[2]) / 2, (b[1] + b[3]) / 2 + layer.textItem.contents = new_text + b2 = [float(x) for x in layer.bounds] + cx2, cy2 = (b2[0] + b2[2]) / 2, (b2[1] + b2[3]) / 2 + dx, dy = cx - cx2, cy - cy2 + if dx or dy: + layer.translate(dx, dy) + + +def set_layer_text(container, layer_name, new_text): + if new_text is None: + return + layer = container.artLayers[layer_name] + old = layer.textItem.contents + set_text(layer, new_text) + print(f' {layer_name}: "{old[:30]}..." -> "{new_text[:40]}..."') + + +def generate(klient, imie, data, od_kogo, zyczenia=None, naglowek=None, tytul=None): + os.makedirs(GOTOWE_DIR, exist_ok=True) + output_path = os.path.join(GOTOWE_DIR, f"{klient}.psd") + + app = ps.Application() + doc = app.open(SZABLON_PATH) + print(f"Otwarto szablon: {doc.name}") + + try: + dl = doc.layerSets["DL"] + print(" [DL] zyczenia:") + set_layer_text(dl, "od_kogo", od_kogo) + set_layer_text(dl, "naglowek", naglowek) + set_layer_text(dl, "zyczenia", zyczenia) + + dp = doc.layerSets["DP"] + print(" [DP] imie/data/tytul:") + set_layer_text(dp, "imie", imie) + set_layer_text(dp, "data", data) + set_layer_text(dp, "tytul", tytul) + + psd_opts = ps.PhotoshopSaveOptions() + doc.saveAs(output_path, psd_opts, True) + print(f"Zapisano: {output_path}") + finally: + app.activeDocument.close(ps.SaveOptions.DoNotSaveChanges) + print("Gotowe!") + + return output_path + + +def main(): + parser = argparse.ArgumentParser(description="Generator PSD - Pudelko komunia, Kielich") + parser.add_argument("--klient", required=True, help="Nazwa pliku wyjsciowego") + parser.add_argument("--imie", required=True, help="Imie dziecka (odmienione, np. Jakuba)") + parser.add_argument("--data", required=True, help='Data komunii, format "DD.MM.RRRR" (np. 18.05.2026)') + parser.add_argument("--od-kogo", dest="od_kogo", required=True, help="Od kogo (np. Matka Chrzestna z rodziną)") + parser.add_argument("--zyczenia", default=None, help="Tresc zyczen (opcjonalnie, domyslnie z szablonu)") + parser.add_argument("--naglowek", default=None, help='Naglowek (opcjonalnie, domyslnie "Z najlepszymi życzeniami")') + parser.add_argument("--tytul", default=None, help='Tytul (opcjonalnie, domyslnie "Pamiątka I Komunii Świętej")') + + args = parser.parse_args() + generate( + klient=args.klient, + imie=args.imie, + data=args.data, + od_kogo=args.od_kogo, + zyczenia=args.zyczenia, + naglowek=args.naglowek, + tytul=args.tytul, + ) + + +if __name__ == "__main__": + main()