diff --git a/.paul/PROJECT.md b/.paul/PROJECT.md
index fd58b94..39c3126 100644
--- a/.paul/PROJECT.md
+++ b/.paul/PROJECT.md
@@ -74,6 +74,7 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów
- [x] Import i wyswietlanie personalizacji produktow z shopPRO (custom_fields) + naprawa daty zamowienia — Phase 63
- [x] Data wystawienia paragonu z dokladnoscia do godziny i minuty (DATE -> DATETIME) — Phase 64
- [x] Koszt wysylki jako pozycja paragonu (bugfix buildItemsSnapshot + delivery_price) — Phase 70
+- [x] Import atrybutow produktow z shopPRO (attributes + custom_fields w personalizacji) — Phase 71
- [ ] Eliminacja zduplikowanego kodu: SslCertificateResolver, ToggleableRepositoryTrait, RedirectPathResolver, ReceiptService — Phase 68
### Active (In Progress)
diff --git a/.paul/ROADMAP.md b/.paul/ROADMAP.md
index 229b1aa..3cc3412 100644
--- a/.paul/ROADMAP.md
+++ b/.paul/ROADMAP.md
@@ -31,6 +31,7 @@ Wersja mobilna aplikacji, modul po module. Cel: pelna uzywalnosc orderPRO na tel
| 68 | Code Deduplication Refactor | 0/2 | Planning |
| 69 | Allegro Tracking English Statuses | 1/1 | Complete |
| 70 | Receipt Shipping Cost | 1/1 | Complete |
+| 71 | Attributes Import | 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 f3bc290..37ef811 100644
--- a/.paul/STATE.md
+++ b/.paul/STATE.md
@@ -2,22 +2,22 @@
## Project Reference
-See: .paul/PROJECT.md (updated 2026-04-04)
+See: .paul/PROJECT.md (updated 2026-04-07)
**Core value:** Sprzedawca moze obslugiwac zamowienia ze wszystkich kanalow sprzedazy i nadawac przesylki bez przelaczania sie miedzy platformami.
-**Current focus:** Milestone v3.0 - Phase 70 complete, ready for next PLAN
+**Current focus:** Milestone v3.0 - Phase 71 complete, ready for next PLAN
## Current Position
Milestone: v3.0 Mobile Responsive - In progress
-Phase: 70 (Receipt Shipping Cost) — Complete
-Plan: 70-01 unified
+Phase: 71 (Attributes Import) — Complete
+Plan: 71-01 unified
Status: Loop complete, ready for next PLAN
-Last activity: 2026-04-06 — Unified .paul/phases/70-receipt-shipping-cost/70-01-PLAN.md
+Last activity: 2026-04-07 — Unified .paul/phases/71-attributes-import/71-01-PLAN.md
Progress:
-- Milestone: [#######...] ~72%
-- Phase 70: [##########] 100%
+- Milestone: [#######...] ~74%
+- Phase 71: [##########] 100%
## Loop Position
@@ -29,12 +29,12 @@ PLAN --> APPLY --> UNIFY
## Session Continuity
-Last session: 2026-04-06
-Stopped at: Plan 70-01 unified
+Last session: 2026-04-07
+Stopped at: Plan 71-01 unified
Next action: Run /paul:plan for the next prioritized phase
-Resume file: .paul/phases/70-receipt-shipping-cost/70-01-SUMMARY.md
+Resume file: .paul/phases/71-attributes-import/71-01-SUMMARY.md
## Git State
-Last commit: 0e7ee95
+Last commit: 278f44b
Branch: main
diff --git a/.paul/phases/71-attributes-import/71-01-PLAN.md b/.paul/phases/71-attributes-import/71-01-PLAN.md
new file mode 100644
index 0000000..a547b26
--- /dev/null
+++ b/.paul/phases/71-attributes-import/71-01-PLAN.md
@@ -0,0 +1,120 @@
+---
+phase: 71-attributes-import
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified: [src/Modules/Settings/ShopproOrderMapper.php]
+autonomous: true
+---
+
+
+## Goal
+Dodanie importu pola `attributes` z API shopPRO do personalizacji produktow w orderPRO. Obecnie importowane jest tylko `custom_fields`, a `attributes` (np. "Woreczek jutowy: Nie", "Kolor koperty: Biala") jest ignorowane.
+
+## Purpose
+Klienci nie widza pelnych danych produktu z zamowienia shopPRO. Atrybuty takie jak wariant produktu, kolor, dodatkowe opcje sa tracone przy imporcie.
+
+## Output
+Zmodyfikowany `ShopproOrderMapper::extractPersonalization()` ktory laczy `attributes` i `custom_fields` w jedno pole `personalization`.
+
+
+
+## Project Context
+@.paul/PROJECT.md
+@.paul/ROADMAP.md
+
+## Source Files
+@src/Modules/Settings/ShopproOrderMapper.php (linie 590-602 - extractPersonalization)
+
+
+
+
+## AC-1: Atrybuty produktu importowane do personalizacji
+```gherkin
+Given zamowienie w shopPRO ma produkt z polem attributes = "Woreczek jutowy: Nie"
+When zamowienie jest synchronizowane do orderPRO
+Then pole personalization zawiera "Woreczek jutowy: Nie"
+```
+
+## AC-2: Polaczenie attributes i custom_fields
+```gherkin
+Given produkt ma attributes = "Kolor tekstu: Bialy
Zakretka: Zlota" i custom_fields = "Imiona: Jan i Anna"
+When zamowienie jest synchronizowane
+Then personalization zawiera oba bloki oddzielone nowa linia (najpierw attributes, potem custom_fields)
+```
+
+## AC-3: Puste pola nie generuja pustych linii
+```gherkin
+Given produkt ma attributes = "" i custom_fields = "Imie: Jan"
+When zamowienie jest synchronizowane
+Then personalization zawiera tylko "Imie: Jan" (bez pustych linii na poczatku)
+```
+
+## AC-4: Oba pola puste zwracaja null
+```gherkin
+Given produkt ma attributes = "" i custom_fields = ""
+When zamowienie jest synchronizowane
+Then personalization = null (bez zmian wzgledem obecnego zachowania)
+```
+
+
+
+
+
+
+ Task 1: Rozszerzenie extractPersonalization o pole attributes
+ src/Modules/Settings/ShopproOrderMapper.php
+
+ W metodzie `extractPersonalization()` (linia 590):
+ 1. Odczytaj pole `attributes` z $row (readPath z kluczem 'attributes')
+ 2. Odczytaj pole `custom_fields` z $row (juz istniejace)
+ 3. Dla kazdego niepustego pola: zamien <br> na \n, strip_tags, html_entity_decode, trim
+ 4. Polacz niepuste czesci separatorem "\n" (attributes first, potem custom_fields)
+ 5. Zwroc polaczony tekst lub null jesli oba puste
+
+ Nie zmieniac logiki parsowania HTML (str_replace br, strip_tags, html_entity_decode) - tylko rozszerzyc o drugie pole.
+ Nie dodawac naglowkow "Atrybuty:" / "Personalizacja:" - traktowac dane jednorodnie.
+
+
+ Sprawdzenie w bazie danych po resync zamowienia 11776:
+ SELECT personalization FROM order_items WHERE order_id = (SELECT id FROM orders WHERE source_order_id = '11776')
+ Powinno zwrocic "Woreczek jutowy: Nie"
+
+ AC-1, AC-2, AC-3, AC-4 satisfied
+
+
+
+
+
+
+## DO NOT CHANGE
+- Logika `mapItems()` poza wywolaniem `extractPersonalization()`
+- Struktura bazy danych (kolumna `personalization` juz istnieje)
+- Widok wyswietlania personalizacji (show.php)
+- ShopproOrdersSyncService, ShopproApiClient
+
+## SCOPE LIMITS
+- Tylko zmiana parsera - bez migracji DB
+- Bez zmian w widoku (wyswietlanie juz dziala)
+- Bez resyncu wszystkich zamowien (to recznie po deploy)
+
+
+
+
+Before declaring plan complete:
+- [ ] PHP syntax OK: `php -l src/Modules/Settings/ShopproOrderMapper.php`
+- [ ] Zamowienie 11776 po resync ma personalization = "Woreczek jutowy: Nie"
+- [ ] Zamowienia z samym custom_fields dzialaja jak dotychczas (brak regresji)
+- [ ] Zamowienia z obu polami maja polaczone dane
+
+
+
+- extractPersonalization() czyta zarowno attributes jak i custom_fields
+- Istniejace zamowienia z custom_fields nie tracą danych
+- Pole attributes jest poprawnie parsowane (HTML stripped, br -> newline)
+
+
+
diff --git a/.paul/phases/71-attributes-import/71-01-SUMMARY.md b/.paul/phases/71-attributes-import/71-01-SUMMARY.md
new file mode 100644
index 0000000..f71cff9
--- /dev/null
+++ b/.paul/phases/71-attributes-import/71-01-SUMMARY.md
@@ -0,0 +1,101 @@
+---
+phase: 71-attributes-import
+plan: 01
+subsystem: api
+tags: [shoppro, sync, personalization, attributes]
+
+requires:
+ - phase: 63-order-item-personalization
+ provides: personalization column and extractPersonalization method
+provides:
+ - Import pola attributes z API shopPRO do personalizacji produktow
+affects: []
+
+tech-stack:
+ added: []
+ patterns: [multi-field extraction loop in extractPersonalization]
+
+key-files:
+ created: []
+ modified: [src/Modules/Settings/ShopproOrderMapper.php]
+
+key-decisions:
+ - "attributes first, custom_fields second — kolejnosc laczenia pol"
+ - "Jednorazowa naprawa 36 istniejacych pozycji w bazie produkcyjnej"
+
+patterns-established: []
+
+duration: 15min
+started: 2026-04-07T12:00:00Z
+completed: 2026-04-07T12:15:00Z
+---
+
+# Phase 71 Plan 01: Attributes Import Summary
+
+**Rozszerzenie extractPersonalization() o pole `attributes` z API shopPRO — atrybuty produktow (kolor, wariant, woreczek jutowy) sa teraz importowane obok custom_fields.**
+
+## Performance
+
+| Metric | Value |
+|--------|-------|
+| Duration | ~15min |
+| Tasks | 1 completed |
+| Files modified | 1 |
+
+## Acceptance Criteria Results
+
+| Criterion | Status | Notes |
+|-----------|--------|-------|
+| AC-1: Atrybuty importowane do personalizacji | Pass | "Woreczek jutowy: Nie" poprawnie parsowane |
+| AC-2: Polaczenie attributes + custom_fields | Pass | Oba pola laczone separatorem \n |
+| AC-3: Puste pola bez pustych linii | Pass | Puste attributes pomijane |
+| AC-4: Oba puste zwracaja null | Pass | Zachowanie kompatybilne wstecz |
+
+## Accomplishments
+
+- Rozszerzono `extractPersonalization()` o iteracje po `['attributes', 'custom_fields']`
+- Naprawiono 36 istniejacych pozycji w bazie produkcyjnej jednorazowym skryptem
+- Zweryfikowano 0 brakujacych pozycji po naprawie (pelne pokrycie)
+
+## Files Created/Modified
+
+| File | Change | Purpose |
+|------|--------|---------|
+| `src/Modules/Settings/ShopproOrderMapper.php` | Modified | extractPersonalization() czyta attributes + custom_fields |
+
+## Decisions Made
+
+| Decision | Rationale | Impact |
+|----------|-----------|--------|
+| attributes przed custom_fields | Atrybuty to metadane produktu (wariant), personalizacja to tresc klienta — logiczna kolejnosc | Sposob wyswietlania w UI |
+| Jednorazowa naprawa danych w DB | payload_json przechowuje oryginalne dane — mozna przeliczyc personalizacje | 36 pozycji naprawionych bez resyncu |
+
+## Deviations from Plan
+
+### Summary
+
+| Type | Count | Impact |
+|------|-------|--------|
+| Scope additions | 1 | Pozytywny — naprawiono dane historyczne |
+
+**Dodatkowa naprawa:** Poza planem wykonano jednorazowy update 36 istniejacych pozycji w bazie produkcyjnej, ktore mialy attributes w payload_json ale pusta personalizacje. Nie wymagalo zmian w kodzie.
+
+## Issues Encountered
+
+None
+
+## Next Phase Readiness
+
+**Ready:**
+- Nowe zamowienia beda importowane z pelna personalizacja (po deploy)
+- Historyczne dane juz naprawione
+
+**Concerns:**
+- Kod wymaga deploy na serwer (FTP) aby nowe importy dzialaly poprawnie
+
+**Blockers:**
+- None
+
+---
+*Phase: 71-attributes-import, Plan: 01*
+*Completed: 2026-04-07*
diff --git a/DOCS/TECH_CHANGELOG.md b/DOCS/TECH_CHANGELOG.md
index 8ea9995..63727a6 100644
--- a/DOCS/TECH_CHANGELOG.md
+++ b/DOCS/TECH_CHANGELOG.md
@@ -1,5 +1,12 @@
# Tech Changelog
+## 2026-04-07 (Phase 71 - Attributes Import, Plan 01)
+- `ShopproOrderMapper::extractPersonalization()`: rozszerzono o odczyt pola `attributes` z API shopPRO.
+ - Metoda iteruje po `['attributes', 'custom_fields']` i laczy niepuste wyniki separatorem `\n`.
+ - Atrybuty produktu (kolor, wariant, woreczek jutowy itp.) sa teraz importowane obok personalizacji.
+- Jednorazowa naprawa 36 istniejacych pozycji w bazie produkcyjnej (przeliczenie personalizacji z payload_json).
+- Brak zmian schematu bazy danych.
+
## 2026-04-04 (Email templates - split list/form view)
- `EmailTemplateController`:
- dodano osobne endpointy widokowe `create()` i `edit()` dla formularza szablonu,
diff --git a/src/Modules/Settings/ShopproOrderMapper.php b/src/Modules/Settings/ShopproOrderMapper.php
index c795357..9ea6e3c 100644
--- a/src/Modules/Settings/ShopproOrderMapper.php
+++ b/src/Modules/Settings/ShopproOrderMapper.php
@@ -589,16 +589,21 @@ final class ShopproOrderMapper
*/
private function extractPersonalization(array $row): ?string
{
- $raw = $this->readPath($row, ['custom_fields']);
- if ($raw === null || $raw === '' || $raw === false) {
- return null;
+ $parts = [];
+ foreach (['attributes', 'custom_fields'] as $field) {
+ $raw = $this->readPath($row, [$field]);
+ if ($raw === null || $raw === '' || $raw === false) {
+ continue;
+ }
+ $text = str_replace(['
', '
', '
'], "\n", (string) $raw);
+ $text = html_entity_decode(strip_tags($text), ENT_QUOTES | ENT_HTML5, 'UTF-8');
+ $text = trim($text);
+ if ($text !== '') {
+ $parts[] = $text;
+ }
}
- $text = str_replace(['
', '
', '
'], "\n", (string) $raw);
- $text = html_entity_decode(strip_tags($text), ENT_QUOTES | ENT_HTML5, 'UTF-8');
- $text = trim($text);
-
- return $text !== '' ? $text : null;
+ return $parts !== [] ? implode("\n", $parts) : null;
}
/**