---
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
---

<objective>
## 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
</objective>

<context>
## 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": <int>, "send_email": <bool>}`
Auth: header `X-Api-Key`
Response: `{"status":"ok","data":{"order_id":<int>,"status_id":<int>,"changed":<bool>}}`

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.
</context>

<skills>
## Required Skills (from SPECIAL-FLOWS.md)

| Skill | Priority | When to Invoke | Loaded? |
|-------|----------|----------------|---------|
| sonar-scanner | required | Po APPLY, przed UNIFY | ○ |

</skills>

<acceptance_criteria>

## 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
```

</acceptance_criteria>

<tasks>

<task type="auto">
  <name>Task 1: Migracja DB + metoda PUT w ShopproApiClient</name>
  <files>
    database/migrations/20260327_000071_add_last_status_pushed_at_to_sync_state.sql,
    src/Modules/Settings/ShopproApiClient.php,
    src/Modules/Settings/ShopproOrderSyncStateRepository.php
  </files>
  <action>
    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
  </action>
  <verify>
    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)
  </verify>
  <done>AC-1 satisfied: ShopproApiClient ma metode PUT do zmiany statusu w shopPRO</done>
</task>

<task type="auto">
  <name>Task 2: Implementacja push w ShopproStatusSyncService</name>
  <files>
    src/Modules/Settings/ShopproStatusSyncService.php
  </files>
  <action>
    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
  </action>
  <verify>
    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)
  </verify>
  <done>AC-2 i AC-3 satisfied: Push direction dziala w cron, brak mapowania nie blokuje</done>
</task>

<task type="auto">
  <name>Task 3: Aktualizacja CronHandlerFactory + dokumentacja</name>
  <files>
    src/Modules/Cron/CronHandlerFactory.php,
    DOCS/DB_SCHEMA.md,
    DOCS/ARCHITECTURE.md,
    DOCS/TECH_CHANGELOG.md
  </files>
  <action>
    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
  </action>
  <verify>
    php -l src/Modules/Cron/CronHandlerFactory.php
    grep "ShopproApiClient" src/Modules/Cron/CronHandlerFactory.php (powinno byc >= 1)
  </verify>
  <done>Dokumentacja i factory zaktualizowane</done>
</task>

</tasks>

<boundaries>

## 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)

</boundaries>

<verification>
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)
</verification>

<success_criteria>
- Wszystkie taski ukonczone
- Wszystkie weryfikacje przeszly
- Brak nowych bledow skladni PHP
- Logika push nie interferuje z istniejaca logika pull
</success_criteria>

<output>
After completion, create `.paul/phases/45-shoppro-status-push/45-01-SUMMARY.md`
</output>
