Files
orderPRO/.paul/phases/45-shoppro-status-push/45-01-PLAN.md
Jacek Pyziak 957fddaf84 feat(v1.7): orderPRO -> shopPRO status push sync
Implement bidirectional status sync for shopPRO integrations.
When direction is set to orderpro_to_shoppro, cron pushes manual
status changes to shopPRO via PUT API with reverse status mapping.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 12:54:57 +01:00

13 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous
phase plan type wave depends_on files_modified autonomous
45-shoppro-status-push 01 execute 1
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
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": <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.

## Required Skills (from SPECIAL-FLOWS.md)
Skill Priority When to Invoke Loaded?
sonar-scanner required Po APPLY, przed UNIFY

<acceptance_criteria>

AC-1: API Client obsluguje PUT change_status

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

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

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>

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)

<success_criteria>

  • Wszystkie taski ukonczone
  • Wszystkie weryfikacje przeszly
  • Brak nowych bledow skladni PHP
  • Logika push nie interferuje z istniejaca logika pull </success_criteria>
After completion, create `.paul/phases/45-shoppro-status-push/45-01-SUMMARY.md`