--- phase: 45-shoppro-status-push plan: 01 type: execute wave: 1 depends_on: [] files_modified: - src/Modules/Settings/ShopproApiClient.php - src/Modules/Settings/ShopproStatusSyncService.php - src/Modules/Settings/ShopproOrderSyncStateRepository.php - database/migrations/20260327_000071_add_last_status_pushed_at_to_sync_state.sql - DOCS/DB_SCHEMA.md - DOCS/ARCHITECTURE.md - DOCS/TECH_CHANGELOG.md autonomous: true --- ## Goal Zaimplementowac synchronizacje statusow zamowien w kierunku orderPRO -> shopPRO. Gdy uzytkownik zmieni status zamowienia w orderPRO, cron pushuje te zmiane do shopPRO API. ## Purpose Uzytkownik ustawil w konfiguracji integracji kierunek `orderpro_to_shoppro`, ale ten kierunek nie jest zaimplementowany — `ShopproStatusSyncService::sync()` jawnie go pomija (`$unsupportedCount++; continue`). Zamowienia zmienione w orderPRO nie aktualizuja sie w shopPRO. ## Output - Metoda PUT w `ShopproApiClient` do aktualizacji statusu zamowienia w shopPRO - Logika push w `ShopproStatusSyncService` dla kierunku `orderpro_to_shoppro` - Migracja: kolumna `last_status_pushed_at` w `integration_order_sync_state` - Aktualizacja dokumentacji ## Project Context @.paul/PROJECT.md @.paul/ROADMAP.md @.paul/STATE.md ## Source Files @src/Modules/Settings/ShopproApiClient.php @src/Modules/Settings/ShopproStatusSyncService.php @src/Modules/Settings/ShopproOrderSyncStateRepository.php @src/Modules/Settings/ShopproStatusMappingRepository.php @src/Modules/Settings/ShopproIntegrationsRepository.php @src/Modules/Cron/ShopproStatusSyncHandler.php ## shopPRO API Reference (z kodu shopPRO) Endpoint: `PUT /api.php?endpoint=orders&action=change_status&id={orderId}` Body JSON: `{"status_id": , "send_email": }` Auth: header `X-Api-Key` Response: `{"status":"ok","data":{"order_id":,"status_id":,"changed":}}` Statusy shopPRO: numeryczne ID z endpointu `GET /api.php?endpoint=dictionaries&action=statuses` Tabela `order_status_mappings`: `shoppro_status_code` przechowuje numeryczne ID statusu shopPRO jako string. ## Required Skills (from SPECIAL-FLOWS.md) | Skill | Priority | When to Invoke | Loaded? | |-------|----------|----------------|---------| | sonar-scanner | required | Po APPLY, przed UNIFY | ○ | ## AC-1: API Client obsluguje PUT change_status ```gherkin Given integracja shopPRO z poprawnym base_url i api_key When wywolana zostanie metoda ShopproApiClient::updateOrderStatus() Then wysylane jest zadanie PUT do /api.php?endpoint=orders&action=change_status&id={id} And body zawiera JSON z status_id (int) i send_email (bool) And zwracany jest wynik z polem ok, http_code, message, changed ``` ## AC-2: Push kierunek dziala w cron sync ```gherkin Given integracja shopPRO z direction = orderpro_to_shoppro And zamowienie w orderPRO ma zmieniony status (wpis w order_status_history z change_source = manual) And istnieje mapowanie orderpro_status_code -> shoppro_status_code w order_status_mappings When uruchomi sie cron shoppro_order_status_sync Then ShopproStatusSyncService pushuje nowy status do shopPRO API And aktualizuje last_status_pushed_at w integration_order_sync_state And nie pushuje zmian z change_source = import/sync (unikniecie petli) ``` ## AC-3: Brak mapowania nie blokuje synca ```gherkin Given zamowienie ma status orderpro bez mapowania na shoppro When cron probuje pushowac status Then pomija to zamowienie bez bledu And kontynuuje przetwarzanie pozostalych ``` Task 1: Migracja DB + metoda PUT w ShopproApiClient database/migrations/20260327_000071_add_last_status_pushed_at_to_sync_state.sql, src/Modules/Settings/ShopproApiClient.php, src/Modules/Settings/ShopproOrderSyncStateRepository.php 1. Utworzyc migracje idempotentna (IF NOT EXISTS pattern): ```sql ALTER TABLE integration_order_sync_state ADD COLUMN IF NOT EXISTS last_status_pushed_at DATETIME NULL DEFAULT NULL AFTER last_success_at; ``` 2. W `ShopproApiClient` dodac publiczna metode `updateOrderStatus()`: - Sygnatura: `updateOrderStatus(string $baseUrl, string $apiKey, int $timeoutSeconds, int $orderId, int $statusId, bool $sendEmail = false): array` - Zwraca: `array{ok:bool, http_code:int|null, message:string, changed:bool}` - URL: `$baseUrl/api.php?endpoint=orders&action=change_status&id=$orderId` - Metoda HTTP: PUT (CURLOPT_CUSTOMREQUEST => 'PUT') - Body: JSON `{"status_id": $statusId, "send_email": $sendEmail}` - Header: Content-Type: application/json + X-Api-Key - SSL: reuse logiki z `requestJson()` — wydzielic wspolna metode `buildCurlHandle()` LUB zduplikowac SSL opts (preferuj wydzielenie) - Parsowanie odpowiedzi: sprawdzic `data.changed` z odpowiedzi 3. W `ShopproOrderSyncStateRepository`: - Dodac metode `getLastStatusPushedAt(int $integrationId): ?string` Query: `SELECT last_status_pushed_at FROM integration_order_sync_state WHERE integration_id = :id` Defensywnie: sprawdzic czy kolumna istnieje (dodac do resolveColumns) - Dodac metode `updateLastStatusPushedAt(int $integrationId, string $datetime): void` Reuse istniejacego upsertState z nowym kluczem w columnMap Avoid: - Nie modyfikowac istniejacych metod requestJson() — dodac nowa prywatna metode do PUT - Nie uzywac file_get_contents — tylko curl php -l src/Modules/Settings/ShopproApiClient.php php -l src/Modules/Settings/ShopproOrderSyncStateRepository.php grep -c "updateOrderStatus" src/Modules/Settings/ShopproApiClient.php (powinno byc >= 1) grep -c "last_status_pushed_at" src/Modules/Settings/ShopproOrderSyncStateRepository.php (powinno byc >= 1) AC-1 satisfied: ShopproApiClient ma metode PUT do zmiany statusu w shopPRO Task 2: Implementacja push w ShopproStatusSyncService src/Modules/Settings/ShopproStatusSyncService.php 1. Dodac nowe zaleznosci w konstruktorze: - `ShopproApiClient $apiClient` - `ShopproOrderSyncStateRepository $syncState` - `ShopproStatusMappingRepository $statusMappings` - `PDO $pdo` (do query order_status_history + orders) 2. Zmodyfikowac `sync()` — zamiast `continue` dla `DIRECTION_ORDERPRO_TO_SHOPPRO`, wywolac nowa prywatna metode `syncPushDirection(int $integrationId, array $integration): array` 3. Implementacja `syncPushDirection()`: a) Pobrac API credentials z integracji (reuse wzorca z istniejacego kodu): - base_url, api_key (via $this->integrations->getApiKeyDecrypted), timeout b) Zbudowac reverse status map: - Pobrac mappingi z $this->statusMappings->listByIntegration($integrationId) - Odwrocic: `$reverseMap[$orderpro_code] = $shoppro_code` - Uwaga: jesli wiele shopPRO kodow mapuje na ten sam orderPRO kod, wziac pierwszy c) Pobrac `last_status_pushed_at` z $this->syncState d) Query do bazy — znalezc zamowienia z recznymi zmianami statusu po kursore: ```sql SELECT DISTINCT o.id AS order_id, o.source_order_id, o.external_status_id, MAX(h.changed_at) AS latest_change FROM order_status_history h JOIN orders o ON o.id = h.order_id WHERE o.integration_id = :integration_id AND h.change_source IN ('manual') AND h.changed_at > :last_pushed_at -- lub brak warunku jesli null GROUP BY o.id, o.source_order_id, o.external_status_id ORDER BY latest_change ASC LIMIT 50 ``` Jesli `last_status_pushed_at` jest null, uzyc ostatnich 24h jako fallback. e) Dla kazdego zamowienia: - Pobrac aktualny `external_status_id` (orderpro status code) - Sprawdzic reverse map: `$shopproStatusCode = $reverseMap[$orderproStatus] ?? null` - Jesli brak mapowania — pominac (log do wyniku) - Jesli jest — wywolac `$this->apiClient->updateOrderStatus($baseUrl, $apiKey, $timeout, (int)$sourceOrderId, (int)$shopproStatusCode)` - Zapisac wynik (success/fail count) - Zaktualizowac kursor `latest_change` f) Po zakonczeniu petli: `$this->syncState->updateLastStatusPushedAt($integrationId, $latestChangeAt)` g) Zwrocic wynik: ```php ['ok' => true, 'direction' => 'orderpro_to_shoppro', 'pushed' => $pushed, 'skipped' => $skipped, 'failed' => $failed] ``` 4. Zaktualizowac wynik `sync()`: - Zbierac wyniki z push integracji osobno - Dodac do glownego wyniku 5. Zaktualizowac konstruktor w `CronHandlerFactory` jesli potrzebne nowe zaleznosci dla ShopproStatusSyncService Avoid: - Nie pushowac zmian z change_source = 'import' lub 'sync' — to spowodowaloby petle - Nie modyfikowac logiki pull direction — zostawic ja nienaruszona - Nie pushowac statusow bez mapowania — po cichu pominac php -l src/Modules/Settings/ShopproStatusSyncService.php grep -c "syncPushDirection" src/Modules/Settings/ShopproStatusSyncService.php (powinno byc >= 2) grep -c "DIRECTION_ORDERPRO_TO_SHOPPRO" src/Modules/Settings/ShopproStatusSyncService.php (powinno byc >= 2) grep "unsupportedCount" src/Modules/Settings/ShopproStatusSyncService.php | head -3 (nie powinno byc juz w kontekscie orderpro_to_shoppro) AC-2 i AC-3 satisfied: Push direction dziala w cron, brak mapowania nie blokuje Task 3: Aktualizacja CronHandlerFactory + dokumentacja src/Modules/Cron/CronHandlerFactory.php, DOCS/DB_SCHEMA.md, DOCS/ARCHITECTURE.md, DOCS/TECH_CHANGELOG.md 1. W `CronHandlerFactory` — zaktualizowac tworzenie `ShopproStatusSyncService`: - Dodac brakujace zaleznosci do konstruktora (ShopproApiClient, ShopproOrderSyncStateRepository, ShopproStatusMappingRepository, PDO) - Reuse istniejacych instancji jesli juz sa tworzone w factory 2. W `DOCS/DB_SCHEMA.md`: - Dodac kolumne `last_status_pushed_at` do opisu `integration_order_sync_state` 3. W `DOCS/ARCHITECTURE.md`: - Dodac opis metody `ShopproApiClient::updateOrderStatus()` (PUT) - Dodac opis metody `ShopproStatusSyncService::syncPushDirection()` i logiki reverse mapping - Zaktualizowac opis crona `shoppro_order_status_sync` — obsluguje oba kierunki 4. W `DOCS/TECH_CHANGELOG.md`: - Dodac wpis chronologiczny: implementacja push direction orderpro_to_shoppro Avoid: - Nie modyfikowac istniejacych cron handlerow - Nie zmieniac interwalu crona php -l src/Modules/Cron/CronHandlerFactory.php grep "ShopproApiClient" src/Modules/Cron/CronHandlerFactory.php (powinno byc >= 1) Dokumentacja i factory zaktualizowane ## DO NOT CHANGE - src/Modules/Orders/OrdersController.php (logika zmiany statusu pozostaje bez zmian) - src/Modules/Orders/OrdersRepository.php (nie modyfikowac updateOrderStatus/recordStatusChange) - src/Modules/Settings/ShopproOrdersSyncService.php (import/pull logika nienaruszona) - src/Modules/Settings/ShopproOrderMapper.php (mapper importu) - resources/views/settings/shoppro.php (UI konfiguracji integracji — nie wymaga zmian) - Istniejace migracje - Istniejace cron schedule/interval ## SCOPE LIMITS - Tylko cron-based push (nie event-driven immediate push) - Brak nowego UI — istniejacy dropdown direction juz obsluguje oba kierunki - Nie implementowac synchronizacji platnosci w tym planie - send_email w API shopPRO ustawic na false (nie wysylac maili klientom przy sync) Before declaring plan complete: - [ ] `php -l` na wszystkich zmienionych plikach PHP (brak bledow skladni) - [ ] Migracja SQL jest poprawna skladniowo - [ ] `ShopproApiClient::updateOrderStatus()` wysyla PUT z poprawnym JSON body - [ ] `ShopproStatusSyncService::sync()` obsluguje oba kierunki - [ ] Reverse mapping poprawnie odwraca orderpro -> shoppro kody statusow - [ ] Zmiany z `change_source = 'import'` NIE sa pushowane (brak petli) - [ ] `CronHandlerFactory` poprawnie tworzy ShopproStatusSyncService z nowymi zaleznosciami - [ ] Dokumentacja zaktualizowana (DB_SCHEMA, ARCHITECTURE, TECH_CHANGELOG) - Wszystkie taski ukonczone - Wszystkie weryfikacje przeszly - Brak nowych bledow skladni PHP - Logika push nie interferuje z istniejaca logika pull After completion, create `.paul/phases/45-shoppro-status-push/45-01-SUMMARY.md`