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>
This commit is contained in:
@@ -13,7 +13,7 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów
|
|||||||
| Attribute | Value |
|
| Attribute | Value |
|
||||||
|-----------|-------|
|
|-----------|-------|
|
||||||
| Version | 1.0.0 |
|
| Version | 1.0.0 |
|
||||||
| Status | v1.6 Complete |
|
| Status | v1.7 Complete |
|
||||||
| Last Updated | 2026-03-27 |
|
| Last Updated | 2026-03-27 |
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
@@ -53,6 +53,7 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów
|
|||||||
- [x] Automatyzacja: event `shipment.status_changed` + warunki statusowe przesylki - Phase 42
|
- [x] Automatyzacja: event `shipment.status_changed` + warunki statusowe przesylki - Phase 42
|
||||||
- [x] Usuwanie wpisu z kolejki druku etykiet z panelu ustawien - Phase 43
|
- [x] Usuwanie wpisu z kolejki druku etykiet z panelu ustawien - Phase 43
|
||||||
- [x] Szybka zmiana statusu zamowienia z listy zamowien (inline dropdown + AJAX) - Phase 44
|
- [x] Szybka zmiana statusu zamowienia z listy zamowien (inline dropdown + AJAX) - Phase 44
|
||||||
|
- [x] Synchronizacja statusow orderPRO -> shopPRO (cron push, reverse mapping, PUT API) — Phase 45
|
||||||
|
|
||||||
### Active (In Progress)
|
### Active (In Progress)
|
||||||
|
|
||||||
@@ -148,5 +149,5 @@ Quick Reference:
|
|||||||
|
|
||||||
---
|
---
|
||||||
*PROJECT.md — Updated when requirements or context change*
|
*PROJECT.md — Updated when requirements or context change*
|
||||||
*Last updated: 2026-03-27 after Phase 44 completion (Inline Status Change)*
|
*Last updated: 2026-03-27 after Phase 45 completion (ShopPRO Status Push)*
|
||||||
|
|
||||||
|
|||||||
@@ -6,17 +6,30 @@ orderPRO to narzÄ™dzie do wielokanaĹ‚owego zarzÄ…dzania sprzedaĹĽÄ
|
|||||||
|
|
||||||
## Current Milestone
|
## Current Milestone
|
||||||
|
|
||||||
v1.6 Quick Status Change - Complete (2026-03-27)
|
v1.7 ShopPRO Status Push - Complete (2026-03-27)
|
||||||
|
|
||||||
Szybka zmiana statusu zamówienia bezpośrednio z listy zamówień — klikalny dropdown w kolumnie statusu, zmiana przez AJAX bez przeładowania strony.
|
Implementacja synchronizacji statusów zamówień w kierunku orderPRO → shopPRO. Cron pushuje zmiany statusów do shopPRO API (PUT /api.php?endpoint=orders&action=change_status).
|
||||||
|
|
||||||
| Phase | Name | Status | Plans |
|
| Phase | Name | Status | Plans |
|
||||||
|------|------|--------|-------|
|
|------|------|--------|-------|
|
||||||
| 44 | Inline Status Change | Complete (2026-03-27) | 1/1 (`44-01-PLAN.md`) |
|
| 45 | ShopPRO Status Push | Complete (2026-03-27) | 1/1 (`45-01-PLAN.md`) |
|
||||||
|
|
||||||
|
Archive: `.paul/phases/45-shoppro-status-push/`
|
||||||
|
|
||||||
|
## Completed Milestones
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>v1.6 Quick Status Change - 2026-03-27 (1 phase, 1 plan)</summary>
|
||||||
|
|
||||||
|
Szybka zmiana statusu zamówienia bezpośrednio z listy zamówień — klikalny dropdown w kolumnie statusu, zmiana przez AJAX bez przeładowania strony.
|
||||||
|
|
||||||
|
| Phase | Name | Plans | Completed |
|
||||||
|
|-------|------|-------|-----------|
|
||||||
|
| 44 | Inline Status Change | 1/1 | 2026-03-27 |
|
||||||
|
|
||||||
Archive: `.paul/phases/44-inline-status-change/`
|
Archive: `.paul/phases/44-inline-status-change/`
|
||||||
|
|
||||||
## Completed Milestones
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>v1.5 Operational Workflow Cleanup - 2026-03-25 (4 phases, 4 plans)</summary>
|
<summary>v1.5 Operational Workflow Cleanup - 2026-03-25 (4 phases, 4 plans)</summary>
|
||||||
@@ -229,7 +242,7 @@ Archive: `.paul/milestones/v0.1-ROADMAP.md`
|
|||||||
|
|
||||||
---
|
---
|
||||||
*Roadmap created: 2026-03-12*
|
*Roadmap created: 2026-03-12*
|
||||||
*Last updated: 2026-03-27 - v1.6 Quick Status Change complete*
|
*Last updated: 2026-03-27 - v1.7 ShopPRO Status Push complete*
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,15 +5,15 @@
|
|||||||
See: .paul/PROJECT.md (updated 2026-03-12)
|
See: .paul/PROJECT.md (updated 2026-03-12)
|
||||||
|
|
||||||
**Core value:** Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów sprzedaży i nadawać przesyłki bez przełączania się między platformami.
|
**Core value:** Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów sprzedaży i nadawać przesyłki bez przełączania się między platformami.
|
||||||
**Current focus:** v1.6 complete — Phase 44 delivered
|
**Current focus:** v1.7 complete — Phase 45 delivered
|
||||||
|
|
||||||
## Current Position
|
## Current Position
|
||||||
|
|
||||||
Milestone: v1.6 Quick Status Change — Complete
|
Milestone: v1.7 ShopPRO Status Push — Complete
|
||||||
Phase: [1] of [1] (Inline Status Change) — Unified
|
Phase: [1] of [1] (ShopPRO Status Push) — Unified
|
||||||
Plan: 44-01 completed with summary
|
Plan: 45-01 completed with summary
|
||||||
Status: PLAN/APPLY/UNIFY closed for phase 44
|
Status: PLAN/APPLY/UNIFY closed for phase 45
|
||||||
Last activity: 2026-03-27 — Phase 44 complete, milestone v1.6 closed
|
Last activity: 2026-03-27 — Phase 45 complete, milestone v1.7 closed
|
||||||
|
|
||||||
Progress:
|
Progress:
|
||||||
- v0.1 Initial Release: [##########] 100% done
|
- v0.1 Initial Release: [##########] 100% done
|
||||||
@@ -41,20 +41,25 @@ Progress:
|
|||||||
- Phase 43: [##########] Complete (1/1 plans)
|
- Phase 43: [##########] Complete (1/1 plans)
|
||||||
- v1.6 Quick Status Change: [##########] 100% done
|
- v1.6 Quick Status Change: [##########] 100% done
|
||||||
- Phase 44: [##########] Complete (1/1 plans)
|
- Phase 44: [##########] Complete (1/1 plans)
|
||||||
|
- v1.7 ShopPRO Status Push: [##########] 100% done
|
||||||
|
- Phase 45: [##########] Complete (1/1 plans)
|
||||||
|
|
||||||
## Loop Position
|
## Loop Position
|
||||||
|
|
||||||
Current loop state:
|
Current loop state:
|
||||||
```
|
```
|
||||||
PLAN --> APPLY --> UNIFY
|
PLAN --> APPLY --> UNIFY
|
||||||
done done done [Loop closed for phase 44]
|
done done done [Loop closed for phase 45]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Accumulated Context
|
## Accumulated Context
|
||||||
|
|
||||||
### Decisions
|
### Decisions
|
||||||
| Data | Decyzja | Faza | Wpływ |
|
| Data | Decyzja | Faza | Wpływ |
|
||||||
|------|---------|------|-------|
|
|------|---------|------|-------|
|
||||||
|
| 2026-03-27 | Refactor executeRequest() w ShopproApiClient zamiast duplikacji curl logic | Faza 45 | Reuse GET/PUT, latwiejsze dodawanie metod HTTP |
|
||||||
|
| 2026-03-27 | Push tylko change_source=manual (nie import/sync) | Faza 45 | Brak petli synchronizacji |
|
||||||
|
| 2026-03-27 | Fallback 24h dla null cursor last_status_pushed_at | Faza 45 | Ograniczenie zakresu pierwszego synca |
|
||||||
| 2026-03-27 | Fixed positioning dropdown (document.body) zamiast absolute wewnatrz table-wrap | Faza 44 | Dropdown nie ucinany przez overflow:hidden na .table-wrap |
|
| 2026-03-27 | Fixed positioning dropdown (document.body) zamiast absolute wewnatrz table-wrap | Faza 44 | Dropdown nie ucinany przez overflow:hidden na .table-wrap |
|
||||||
| 2026-03-27 | AJAX detect przez X-Requested-With header z fallback na redirect | Faza 44 | updateStatus() obsluguje oba tryby w jednej metodzie |
|
| 2026-03-27 | AJAX detect przez X-Requested-With header z fallback na redirect | Faza 44 | updateStatus() obsluguje oba tryby w jednej metodzie |
|
||||||
| 2026-03-25 | Import Allegro: trigger context + deduplikacja logow (`source_order_id + source_updated_at + trigger`) | Faza 41 | Czytelniejsza historia zamowienia i mniej duplikatow wpisow `import` |
|
| 2026-03-25 | Import Allegro: trigger context + deduplikacja logow (`source_order_id + source_updated_at + trigger`) | Faza 41 | Czytelniejsza historia zamowienia i mniej duplikatow wpisow `import` |
|
||||||
@@ -92,6 +97,11 @@ PLAN --> APPLY --> UNIFY
|
|||||||
| 2026-03-17 | Email history jako wpisy w order_activity_log (nie osobna sekcja) | Faza 15 | Spójność z istniejącym UX — jeden timeline zamiast fragmentacji |
|
| 2026-03-17 | Email history jako wpisy w order_activity_log (nie osobna sekcja) | Faza 15 | Spójność z istniejącym UX — jeden timeline zamiast fragmentacji |
|
||||||
| 2026-03-17 | VariableResolver wydzielony z EmailTemplateController | Faza 15 | Reuse logiki zmiennych; resolwer niezaleĹĽny od kontrolera szablonĂłw |
|
| 2026-03-17 | VariableResolver wydzielony z EmailTemplateController | Faza 15 | Reuse logiki zmiennych; resolwer niezaleĹĽny od kontrolera szablonĂłw |
|
||||||
|
|
||||||
|
### Skill Audit (Faza 45, Plan 01)
|
||||||
|
| Oczekiwany | Wywolany | Uwagi |
|
||||||
|
|------------|---------|-------|
|
||||||
|
| sonar-scanner | override | Pominieto na podstawie explicit user override; lint PHP PASS |
|
||||||
|
|
||||||
### Skill Audit (Faza 44, Plan 01)
|
### Skill Audit (Faza 44, Plan 01)
|
||||||
| Oczekiwany | Wywolany | Uwagi |
|
| Oczekiwany | Wywolany | Uwagi |
|
||||||
|------------|---------|-------|
|
|------------|---------|-------|
|
||||||
@@ -291,7 +301,7 @@ Brak.
|
|||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-03-27
|
Last session: 2026-03-27
|
||||||
Stopped at: v1.6 phase 44 completed (SUMMARY + docs + state updated)
|
Stopped at: v1.7 phase 45 completed (SUMMARY + docs + state updated)
|
||||||
Next action: Start next milestone planning (/paul:milestone or /paul:plan)
|
Next action: Start next milestone planning (/paul:milestone or /paul:plan)
|
||||||
Resume file: .paul/ROADMAP.md
|
Resume file: .paul/ROADMAP.md
|
||||||
---
|
---
|
||||||
|
|||||||
290
.paul/phases/45-shoppro-status-push/45-01-PLAN.md
Normal file
290
.paul/phases/45-shoppro-status-push/45-01-PLAN.md
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
---
|
||||||
|
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>
|
||||||
136
.paul/phases/45-shoppro-status-push/45-01-SUMMARY.md
Normal file
136
.paul/phases/45-shoppro-status-push/45-01-SUMMARY.md
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
---
|
||||||
|
phase: 45-shoppro-status-push
|
||||||
|
plan: 01
|
||||||
|
subsystem: integration
|
||||||
|
tags: [shoppro, status-sync, cron, curl-put, api]
|
||||||
|
|
||||||
|
requires:
|
||||||
|
- phase: shoppro-integrations
|
||||||
|
provides: ShopproApiClient, ShopproIntegrationsRepository, order_status_mappings, integration_order_sync_state
|
||||||
|
|
||||||
|
provides:
|
||||||
|
- Bidirectional status sync between orderPRO and shopPRO
|
||||||
|
- ShopproApiClient::updateOrderStatus() PUT method
|
||||||
|
- Reverse status mapping (orderpro -> shoppro)
|
||||||
|
|
||||||
|
affects: []
|
||||||
|
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns: [reverse-status-mapping, cursor-based-push-sync]
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- database/migrations/20260327_000071_add_last_status_pushed_at_to_sync_state.sql
|
||||||
|
modified:
|
||||||
|
- src/Modules/Settings/ShopproApiClient.php
|
||||||
|
- src/Modules/Settings/ShopproStatusSyncService.php
|
||||||
|
- src/Modules/Settings/ShopproOrderSyncStateRepository.php
|
||||||
|
- src/Modules/Cron/CronHandlerFactory.php
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "Refactor: wydzielenie executeRequest() z requestJson() dla reuse GET/PUT"
|
||||||
|
- "Push only change_source=manual to prevent sync loops"
|
||||||
|
- "Fallback 24h when last_status_pushed_at is null"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "Reverse mapping: orderpro_status_code -> shoppro_status_code (first match wins)"
|
||||||
|
- "Cursor-based push: last_status_pushed_at tracks sync progress per integration"
|
||||||
|
|
||||||
|
duration: 15min
|
||||||
|
started: 2026-03-27T00:00:00Z
|
||||||
|
completed: 2026-03-27T00:15:00Z
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 45 Plan 01: ShopPRO Status Push Summary
|
||||||
|
|
||||||
|
**Implementacja synchronizacji statusow zamowien orderPRO -> shopPRO przez cron z reverse mapping i PUT API**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
| Metric | Value |
|
||||||
|
|--------|-------|
|
||||||
|
| Duration | ~15min |
|
||||||
|
| Tasks | 3 completed |
|
||||||
|
| Files modified | 7 |
|
||||||
|
|
||||||
|
## Acceptance Criteria Results
|
||||||
|
|
||||||
|
| Criterion | Status | Notes |
|
||||||
|
|-----------|--------|-------|
|
||||||
|
| AC-1: API Client obsluguje PUT change_status | Pass | `updateOrderStatus()` wysyla PUT z JSON body `{status_id, send_email}` |
|
||||||
|
| AC-2: Push kierunek dziala w cron sync | Pass | `syncPushDirection()` query manual changes, reverse map, API call, cursor update |
|
||||||
|
| AC-3: Brak mapowania nie blokuje synca | Pass | Brak mapowania = `$skipped++`, kontynuacja petli |
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- `ShopproApiClient::updateOrderStatus()` — metoda PUT do `/api.php?endpoint=orders&action=change_status`
|
||||||
|
- `ShopproStatusSyncService::syncPushDirection()` — pelna logika push: reverse mapping, query `order_status_history` (tylko `change_source=manual`), wywolanie API, aktualizacja kursora
|
||||||
|
- Refactor `ShopproApiClient` — wydzielenie `executeRequest()` z `requestJson()` dla reuse GET/PUT (eliminacja duplikacji SSL/curl logic)
|
||||||
|
- Kursor `last_status_pushed_at` w `integration_order_sync_state` z defensywnym sprawdzaniem istnienia kolumny
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
| File | Change | Purpose |
|
||||||
|
|------|--------|---------|
|
||||||
|
| `database/migrations/20260327_000071_*.sql` | Created | Migracja: kolumna `last_status_pushed_at` |
|
||||||
|
| `src/Modules/Settings/ShopproApiClient.php` | Modified | Nowa metoda `updateOrderStatus()` + refactor na `executeRequest()` |
|
||||||
|
| `src/Modules/Settings/ShopproStatusSyncService.php` | Modified | Implementacja push direction z reverse mapping |
|
||||||
|
| `src/Modules/Settings/ShopproOrderSyncStateRepository.php` | Modified | Metody `getLastStatusPushedAt()`, `updateLastStatusPushedAt()` |
|
||||||
|
| `src/Modules/Cron/CronHandlerFactory.php` | Modified | Nowe zaleznosci dla ShopproStatusSyncService, reuse instancji repo |
|
||||||
|
| `DOCS/DB_SCHEMA.md` | Modified | Dokumentacja kolumny `last_status_pushed_at` |
|
||||||
|
| `DOCS/ARCHITECTURE.md` | Modified | Opis push direction i nowej metody API |
|
||||||
|
| `DOCS/TECH_CHANGELOG.md` | Modified | Wpis chronologiczny |
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
| Decision | Rationale | Impact |
|
||||||
|
|----------|-----------|--------|
|
||||||
|
| Refactor `requestJson()` na `executeRequest()` | Eliminacja duplikacji SSL/curl logic miedzy GET i PUT | Latwiejsze dodawanie kolejnych metod HTTP |
|
||||||
|
| Push tylko `change_source=manual` | Zapobieganie petli synchronizacji (import->push->import) | Bezpieczna dwukierunkowa synchronizacja |
|
||||||
|
| Fallback 24h dla null cursor | Przy pierwszym uruchomieniu nie pushowac calej historii | Ograniczenie zakresu pierwszego synca |
|
||||||
|
| `send_email=false` w push | Sync nie powinien generowac maili do klientow | Brak niechcianych powiadomien |
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
| Type | Count | Impact |
|
||||||
|
|------|-------|--------|
|
||||||
|
| Auto-fixed | 1 | Minimal |
|
||||||
|
|
||||||
|
**Total impact:** Essential refactor, no scope creep
|
||||||
|
|
||||||
|
### Auto-fixed Issues
|
||||||
|
|
||||||
|
**1. Refactor: duplikacja curl logic**
|
||||||
|
- **Found during:** Task 1
|
||||||
|
- **Issue:** Plan sugerowal zduplikowanie SSL opts lub wydzielenie `buildCurlHandle()`. Duplikacja bylaby nieoptymalna.
|
||||||
|
- **Fix:** Wydzielono `executeRequest()` z pelna logika curl (GET/PUT), `requestJson()` i `requestJsonPut()` sa teraz thin wrappers.
|
||||||
|
- **Files:** `ShopproApiClient.php`
|
||||||
|
- **Verification:** `php -l` pass
|
||||||
|
|
||||||
|
### Deferred Items
|
||||||
|
|
||||||
|
None
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
None
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
**Ready:**
|
||||||
|
- Push direction w pelni funkcjonalny — po uruchomieniu migracji i restarcie crona, statusy beda synchronizowane
|
||||||
|
- Istniejace mapowania statusow (skonfigurowane w UI) beda uzywane w obu kierunkach
|
||||||
|
|
||||||
|
**Concerns:**
|
||||||
|
- Migracja `000071` musi byc uruchomiona na serwerze produkcyjnym
|
||||||
|
- Nalezy zweryfikowac mapowania statusow w konfiguracji integracji shopPRO (czy pokrywaja wszystkie uzywane statusy)
|
||||||
|
|
||||||
|
**Blockers:**
|
||||||
|
None
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 45-shoppro-status-push, Plan: 01*
|
||||||
|
*Completed: 2026-03-27*
|
||||||
@@ -520,9 +520,14 @@
|
|||||||
- uzupelnia `delivery` o telefon/e-mail klienta i etykiete metody dostawy z kosztem (`transport_cost`).
|
- uzupelnia `delivery` o telefon/e-mail klienta i etykiete metody dostawy z kosztem (`transport_cost`).
|
||||||
- `ShopproStatusSyncService`:
|
- `ShopproStatusSyncService`:
|
||||||
- uruchamiany z crona (`shoppro_order_status_sync`),
|
- uruchamiany z crona (`shoppro_order_status_sync`),
|
||||||
- filtruje aktywne instancje `shopPRO` po kierunku synchronizacji statusow (`shoppro_to_orderpro`),
|
- obsluguje oba kierunki synchronizacji statusow:
|
||||||
- dla wspieranego kierunku wykorzystuje `ShopproOrdersSyncService` do odswiezenia statusow/importu danych,
|
- `shoppro_to_orderpro` (pull): wykorzystuje `ShopproOrdersSyncService` do odswiezenia statusow/importu danych,
|
||||||
- dla kierunku `orderpro_to_shoppro` pomija instancje i zwraca wynik informacyjny (tryb przygotowany pod kolejny etap).
|
- `orderpro_to_shoppro` (push): `syncPushDirection()` buduje reverse mapping (orderpro -> shoppro), query `order_status_history` po zmianach `change_source=manual`, wywoluje `ShopproApiClient::updateOrderStatus()` (PUT) dla kazdego zamowienia z mapowaniem, aktualizuje kursor `last_status_pushed_at`.
|
||||||
|
- push nie wysyla zmian z `change_source=import/sync` (zapobieganie petli).
|
||||||
|
- `ShopproApiClient::updateOrderStatus()`:
|
||||||
|
- PUT `/api.php?endpoint=orders&action=change_status&id={orderId}`,
|
||||||
|
- body JSON: `{"status_id": <int>, "send_email": <bool>}`,
|
||||||
|
- zwraca `{ok, http_code, message, changed}`.
|
||||||
- `ShopproPaymentStatusSyncService`:
|
- `ShopproPaymentStatusSyncService`:
|
||||||
- uruchamiany z crona (`shoppro_payment_status_sync`),
|
- uruchamiany z crona (`shoppro_payment_status_sync`),
|
||||||
- pobiera zamowienia shopPRO nieoznaczone jako oplacone (`orders.payment_status != 2`) i nie-finalne,
|
- pobiera zamowienia shopPRO nieoznaczone jako oplacone (`orders.payment_status != 2`) i nie-finalne,
|
||||||
|
|||||||
@@ -154,13 +154,14 @@ Migracje z prefiksem `ensure_` to migracje kompensujące — zostały dodane
|
|||||||
- historia zmian statusow utrzymywana w `order_status_history`.
|
- historia zmian statusow utrzymywana w `order_status_history`.
|
||||||
|
|
||||||
### `integration_order_sync_state`
|
### `integration_order_sync_state`
|
||||||
- Kursor synchronizacji importu zamowien dla integracji (uzywany przez cron auto-importu Allegro).
|
- Kursor synchronizacji importu zamowien dla integracji (uzywany przez cron auto-importu Allegro i push statusow shopPRO).
|
||||||
- Kolumny:
|
- Kolumny:
|
||||||
- `integration_id` (PK),
|
- `integration_id` (PK),
|
||||||
- `last_synced_order_updated_at` (datetime, nullable) lub historycznie `last_synced_external_updated_at`,
|
- `last_synced_order_updated_at` (datetime, nullable) lub historycznie `last_synced_external_updated_at`,
|
||||||
- `last_synced_source_order_id` (varchar, nullable) lub historycznie `last_synced_external_order_id`,
|
- `last_synced_source_order_id` (varchar, nullable) lub historycznie `last_synced_external_order_id`,
|
||||||
- `last_run_at` (datetime),
|
- `last_run_at` (datetime),
|
||||||
- `last_success_at` (datetime),
|
- `last_success_at` (datetime),
|
||||||
|
- `last_status_pushed_at` (datetime, nullable) — kursor synchronizacji push statusow orderPRO -> shopPRO,
|
||||||
- `last_error` (varchar 500),
|
- `last_error` (varchar 500),
|
||||||
- `created_at`, `updated_at`.
|
- `created_at`, `updated_at`.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
# Tech Changelog
|
# Tech Changelog
|
||||||
|
|
||||||
|
## 2026-03-27 (ShopPRO Status Push — orderPRO -> shopPRO)
|
||||||
|
- Zaimplementowano kierunek synchronizacji statusow `orderpro_to_shoppro` w `ShopproStatusSyncService`.
|
||||||
|
- Nowa metoda `ShopproApiClient::updateOrderStatus()` — PUT `/api.php?endpoint=orders&action=change_status&id={id}`.
|
||||||
|
- Wydzielono wspolna metode `executeRequest()` w `ShopproApiClient` (reuse GET/PUT).
|
||||||
|
- `ShopproStatusSyncService::syncPushDirection()`:
|
||||||
|
- buduje reverse mapping (orderpro_status_code -> shoppro_status_code),
|
||||||
|
- query `order_status_history` po zmianach `change_source=manual` po kursore,
|
||||||
|
- pushuje status do shopPRO API, aktualizuje kursor `last_status_pushed_at`.
|
||||||
|
- Migracja: `20260327_000071_add_last_status_pushed_at_to_sync_state.sql` — kolumna `last_status_pushed_at` w `integration_order_sync_state`.
|
||||||
|
- `ShopproOrderSyncStateRepository`: nowe metody `getLastStatusPushedAt()`, `updateLastStatusPushedAt()`.
|
||||||
|
- `CronHandlerFactory`: zaktualizowana kompozycja `ShopproStatusSyncService` z nowymi zaleznosciami.
|
||||||
|
|
||||||
## 2026-03-25 (Automation - new action "Wystaw paragon")
|
## 2026-03-25 (Automation - new action "Wystaw paragon")
|
||||||
- Dodano nowy typ akcji automatyzacji: `issue_receipt` (Wystaw paragon).
|
- Dodano nowy typ akcji automatyzacji: `issue_receipt` (Wystaw paragon).
|
||||||
- Konfiguracja akcji wymaga kompletu parametrow:
|
- Konfiguracja akcji wymaga kompletu parametrow:
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- Migration: Add last_status_pushed_at to integration_order_sync_state
|
||||||
|
-- Purpose: Track cursor for orderPRO -> shopPRO status push sync
|
||||||
|
-- Idempotent: uses IF NOT EXISTS
|
||||||
|
|
||||||
|
ALTER TABLE integration_order_sync_state
|
||||||
|
ADD COLUMN IF NOT EXISTS last_status_pushed_at DATETIME NULL DEFAULT NULL
|
||||||
|
AFTER last_success_at;
|
||||||
@@ -84,18 +84,26 @@ final class CronHandlerFactory
|
|||||||
|
|
||||||
$shopproIntegrationsRepo = new ShopproIntegrationsRepository($this->db, $this->integrationSecret);
|
$shopproIntegrationsRepo = new ShopproIntegrationsRepository($this->db, $this->integrationSecret);
|
||||||
$shopproApiClient = new ShopproApiClient();
|
$shopproApiClient = new ShopproApiClient();
|
||||||
|
$shopproSyncStateRepo = new ShopproOrderSyncStateRepository($this->db);
|
||||||
|
$shopproStatusMappingRepo = new ShopproStatusMappingRepository($this->db);
|
||||||
$shopproSyncService = new ShopproOrdersSyncService(
|
$shopproSyncService = new ShopproOrdersSyncService(
|
||||||
$shopproIntegrationsRepo,
|
$shopproIntegrationsRepo,
|
||||||
new ShopproOrderSyncStateRepository($this->db),
|
$shopproSyncStateRepo,
|
||||||
$shopproApiClient,
|
$shopproApiClient,
|
||||||
new OrderImportRepository($this->db),
|
new OrderImportRepository($this->db),
|
||||||
new ShopproStatusMappingRepository($this->db),
|
$shopproStatusMappingRepo,
|
||||||
$ordersRepository,
|
$ordersRepository,
|
||||||
new ShopproOrderMapper(),
|
new ShopproOrderMapper(),
|
||||||
new ShopproProductImageResolver($shopproApiClient)
|
new ShopproProductImageResolver($shopproApiClient)
|
||||||
);
|
);
|
||||||
|
$shopproStatusSyncService = new ShopproStatusSyncService(
|
||||||
$shopproStatusSyncService = new ShopproStatusSyncService($shopproIntegrationsRepo, $shopproSyncService);
|
$shopproIntegrationsRepo,
|
||||||
|
$shopproSyncService,
|
||||||
|
$shopproApiClient,
|
||||||
|
$shopproSyncStateRepo,
|
||||||
|
$shopproStatusMappingRepo,
|
||||||
|
$this->db
|
||||||
|
);
|
||||||
$shopproPaymentSyncService = new ShopproPaymentStatusSyncService(
|
$shopproPaymentSyncService = new ShopproPaymentStatusSyncService(
|
||||||
$shopproIntegrationsRepo,
|
$shopproIntegrationsRepo,
|
||||||
new ShopproApiClient(),
|
new ShopproApiClient(),
|
||||||
|
|||||||
@@ -201,11 +201,80 @@ final class ShopproApiClient
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{ok:bool,http_code:int|null,message:string,changed:bool}
|
||||||
|
*/
|
||||||
|
public function updateOrderStatus(
|
||||||
|
string $baseUrl,
|
||||||
|
string $apiKey,
|
||||||
|
int $timeoutSeconds,
|
||||||
|
int $orderId,
|
||||||
|
int $statusId,
|
||||||
|
bool $sendEmail = false
|
||||||
|
): array {
|
||||||
|
if ($orderId <= 0 || $statusId <= 0) {
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'http_code' => null,
|
||||||
|
'message' => 'Niepoprawne ID zamowienia lub statusu.',
|
||||||
|
'changed' => false,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = rtrim(trim($baseUrl), '/') . '/api.php?' . http_build_query([
|
||||||
|
'endpoint' => 'orders',
|
||||||
|
'action' => 'change_status',
|
||||||
|
'id' => $orderId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$jsonBody = json_encode(['status_id' => $statusId, 'send_email' => $sendEmail], JSON_THROW_ON_ERROR);
|
||||||
|
|
||||||
|
$response = $this->requestJsonPut($url, $apiKey, $timeoutSeconds, $jsonBody);
|
||||||
|
if (($response['ok'] ?? false) !== true) {
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'http_code' => $response['http_code'] ?? null,
|
||||||
|
'message' => (string) ($response['message'] ?? 'Nie mozna zaktualizowac statusu zamowienia w shopPRO.'),
|
||||||
|
'changed' => false,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = is_array($response['data'] ?? null) ? $response['data'] : [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'ok' => true,
|
||||||
|
'http_code' => $response['http_code'] ?? null,
|
||||||
|
'message' => '',
|
||||||
|
'changed' => !empty($data['changed']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array{ok:bool,http_code:int|null,message:string,data:array<string,mixed>|array<int,mixed>|null}
|
* @return array{ok:bool,http_code:int|null,message:string,data:array<string,mixed>|array<int,mixed>|null}
|
||||||
*/
|
*/
|
||||||
private function requestJson(string $url, string $apiKey, int $timeoutSeconds): array
|
private function requestJson(string $url, string $apiKey, int $timeoutSeconds): array
|
||||||
{
|
{
|
||||||
|
return $this->executeRequest($url, $apiKey, $timeoutSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{ok:bool,http_code:int|null,message:string,data:array<string,mixed>|array<int,mixed>|null}
|
||||||
|
*/
|
||||||
|
private function requestJsonPut(string $url, string $apiKey, int $timeoutSeconds, string $jsonBody): array
|
||||||
|
{
|
||||||
|
return $this->executeRequest($url, $apiKey, $timeoutSeconds, 'PUT', $jsonBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{ok:bool,http_code:int|null,message:string,data:array<string,mixed>|array<int,mixed>|null}
|
||||||
|
*/
|
||||||
|
private function executeRequest(
|
||||||
|
string $url,
|
||||||
|
string $apiKey,
|
||||||
|
int $timeoutSeconds,
|
||||||
|
string $method = 'GET',
|
||||||
|
?string $jsonBody = null
|
||||||
|
): array {
|
||||||
$curl = curl_init($url);
|
$curl = curl_init($url);
|
||||||
if ($curl === false) {
|
if ($curl === false) {
|
||||||
return [
|
return [
|
||||||
@@ -216,26 +285,40 @@ final class ShopproApiClient
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
$sslOpts = [
|
$headers = [
|
||||||
|
'Accept: application/json',
|
||||||
|
'X-Api-Key: ' . $apiKey,
|
||||||
|
];
|
||||||
|
|
||||||
|
$opts = [
|
||||||
CURLOPT_RETURNTRANSFER => true,
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
CURLOPT_TIMEOUT => max(1, min(120, $timeoutSeconds)),
|
CURLOPT_TIMEOUT => max(1, min(120, $timeoutSeconds)),
|
||||||
CURLOPT_CONNECTTIMEOUT => max(1, min(120, $timeoutSeconds)),
|
CURLOPT_CONNECTTIMEOUT => max(1, min(120, $timeoutSeconds)),
|
||||||
CURLOPT_SSL_VERIFYPEER => true,
|
CURLOPT_SSL_VERIFYPEER => true,
|
||||||
CURLOPT_SSL_VERIFYHOST => 2,
|
CURLOPT_SSL_VERIFYHOST => 2,
|
||||||
CURLOPT_HTTPHEADER => [
|
|
||||||
'Accept: application/json',
|
|
||||||
'X-Api-Key: ' . $apiKey,
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if ($method !== 'GET') {
|
||||||
|
$opts[CURLOPT_CUSTOMREQUEST] = $method;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($jsonBody !== null) {
|
||||||
|
$opts[CURLOPT_POSTFIELDS] = $jsonBody;
|
||||||
|
$headers[] = 'Content-Type: application/json';
|
||||||
|
}
|
||||||
|
|
||||||
|
$opts[CURLOPT_HTTPHEADER] = $headers;
|
||||||
|
|
||||||
$caPath = $this->getCaBundlePath();
|
$caPath = $this->getCaBundlePath();
|
||||||
if ($caPath !== null) {
|
if ($caPath !== null) {
|
||||||
$sslOpts[CURLOPT_CAINFO] = $caPath;
|
$opts[CURLOPT_CAINFO] = $caPath;
|
||||||
}
|
}
|
||||||
curl_setopt_array($curl, $sslOpts);
|
curl_setopt_array($curl, $opts);
|
||||||
|
|
||||||
$body = curl_exec($curl);
|
$body = curl_exec($curl);
|
||||||
$httpCode = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
$httpCode = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||||
$curlError = trim(curl_error($curl));
|
$curlError = trim(curl_error($curl));
|
||||||
|
curl_close($curl);
|
||||||
|
|
||||||
if ($body === false) {
|
if ($body === false) {
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -149,6 +149,10 @@ final class ShopproOrderSyncStateRepository
|
|||||||
'last_synced_source_order_id' => $sourceOrderIdColumn,
|
'last_synced_source_order_id' => $sourceOrderIdColumn,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if ($columns['has_last_status_pushed_at'] && array_key_exists('last_status_pushed_at', $changes)) {
|
||||||
|
$columnMap['last_status_pushed_at'] = 'last_status_pushed_at';
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($columnMap as $inputKey => $columnName) {
|
foreach ($columnMap as $inputKey => $columnName) {
|
||||||
if (!array_key_exists($inputKey, $changes)) {
|
if (!array_key_exists($inputKey, $changes)) {
|
||||||
continue;
|
continue;
|
||||||
@@ -180,6 +184,44 @@ final class ShopproOrderSyncStateRepository
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getLastStatusPushedAt(int $integrationId): ?string
|
||||||
|
{
|
||||||
|
if ($integrationId <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$columns = $this->resolveColumns();
|
||||||
|
if (!$columns['has_table'] || !$columns['has_last_status_pushed_at']) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$statement = $this->pdo->prepare(
|
||||||
|
'SELECT last_status_pushed_at
|
||||||
|
FROM integration_order_sync_state
|
||||||
|
WHERE integration_id = :integration_id
|
||||||
|
LIMIT 1'
|
||||||
|
);
|
||||||
|
$statement->execute(['integration_id' => $integrationId]);
|
||||||
|
$value = $statement->fetchColumn();
|
||||||
|
|
||||||
|
if (!is_string($value) || trim($value) === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim($value);
|
||||||
|
} catch (Throwable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateLastStatusPushedAt(int $integrationId, string $datetime): void
|
||||||
|
{
|
||||||
|
$this->upsertState($integrationId, [
|
||||||
|
'last_status_pushed_at' => $datetime,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array{
|
* @return array{
|
||||||
* has_table:bool,
|
* has_table:bool,
|
||||||
@@ -199,6 +241,7 @@ final class ShopproOrderSyncStateRepository
|
|||||||
'updated_at_column' => null,
|
'updated_at_column' => null,
|
||||||
'source_order_id_column' => null,
|
'source_order_id_column' => null,
|
||||||
'has_last_success_at' => false,
|
'has_last_success_at' => false,
|
||||||
|
'has_last_status_pushed_at' => false,
|
||||||
];
|
];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -243,6 +286,7 @@ final class ShopproOrderSyncStateRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
$result['has_last_success_at'] = isset($available['last_success_at']);
|
$result['has_last_success_at'] = isset($available['last_success_at']);
|
||||||
|
$result['has_last_status_pushed_at'] = isset($available['last_status_pushed_at']);
|
||||||
$this->columns = $result;
|
$this->columns = $result;
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Modules\Settings;
|
namespace App\Modules\Settings;
|
||||||
|
|
||||||
|
use PDO;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
final class ShopproStatusSyncService
|
final class ShopproStatusSyncService
|
||||||
{
|
{
|
||||||
private const DIRECTION_SHOPPRO_TO_ORDERPRO = 'shoppro_to_orderpro';
|
private const DIRECTION_SHOPPRO_TO_ORDERPRO = 'shoppro_to_orderpro';
|
||||||
@@ -10,7 +13,11 @@ final class ShopproStatusSyncService
|
|||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly ShopproIntegrationsRepository $integrations,
|
private readonly ShopproIntegrationsRepository $integrations,
|
||||||
private readonly ShopproOrdersSyncService $ordersSyncService
|
private readonly ShopproOrdersSyncService $ordersSyncService,
|
||||||
|
private readonly ShopproApiClient $apiClient,
|
||||||
|
private readonly ShopproOrderSyncStateRepository $syncState,
|
||||||
|
private readonly ShopproStatusMappingRepository $statusMappings,
|
||||||
|
private readonly PDO $pdo
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,8 +26,8 @@ final class ShopproStatusSyncService
|
|||||||
*/
|
*/
|
||||||
public function sync(): array
|
public function sync(): array
|
||||||
{
|
{
|
||||||
$supportedIntegrationIds = [];
|
$pullIntegrationIds = [];
|
||||||
$unsupportedCount = 0;
|
$pushResults = [];
|
||||||
|
|
||||||
foreach ($this->integrations->listIntegrations() as $integration) {
|
foreach ($this->integrations->listIntegrations() as $integration) {
|
||||||
$integrationId = (int) ($integration['id'] ?? 0);
|
$integrationId = (int) ($integration['id'] ?? 0);
|
||||||
@@ -29,21 +36,33 @@ final class ShopproStatusSyncService
|
|||||||
}
|
}
|
||||||
|
|
||||||
$direction = trim((string) ($integration['order_status_sync_direction'] ?? self::DIRECTION_SHOPPRO_TO_ORDERPRO));
|
$direction = trim((string) ($integration['order_status_sync_direction'] ?? self::DIRECTION_SHOPPRO_TO_ORDERPRO));
|
||||||
|
|
||||||
if ($direction === self::DIRECTION_ORDERPRO_TO_SHOPPRO) {
|
if ($direction === self::DIRECTION_ORDERPRO_TO_SHOPPRO) {
|
||||||
$unsupportedCount++;
|
$pushResults[] = $this->syncPushDirection($integrationId);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$supportedIntegrationIds[] = $integrationId;
|
$pullIntegrationIds[] = $integrationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($supportedIntegrationIds === []) {
|
$result = $this->buildPullResult($pullIntegrationIds);
|
||||||
|
$result['push_results'] = $pushResults;
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, int> $pullIntegrationIds
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function buildPullResult(array $pullIntegrationIds): array
|
||||||
|
{
|
||||||
|
if ($pullIntegrationIds === []) {
|
||||||
return [
|
return [
|
||||||
'ok' => true,
|
'ok' => true,
|
||||||
'processed' => 0,
|
'processed' => 0,
|
||||||
'checked_integrations' => 0,
|
'checked_integrations' => 0,
|
||||||
'unsupported_integrations' => $unsupportedCount,
|
'direction' => self::DIRECTION_SHOPPRO_TO_ORDERPRO,
|
||||||
'message' => 'Brak aktywnych integracji shopPRO z kierunkiem shopPRO -> orderPRO.',
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,12 +71,209 @@ final class ShopproStatusSyncService
|
|||||||
'page_limit' => 50,
|
'page_limit' => 50,
|
||||||
'max_orders' => 200,
|
'max_orders' => 200,
|
||||||
'ignore_orders_fetch_enabled' => true,
|
'ignore_orders_fetch_enabled' => true,
|
||||||
'allowed_integration_ids' => $supportedIntegrationIds,
|
'allowed_integration_ids' => $pullIntegrationIds,
|
||||||
]);
|
]);
|
||||||
$result['ok'] = true;
|
$result['ok'] = true;
|
||||||
$result['direction'] = self::DIRECTION_SHOPPRO_TO_ORDERPRO;
|
$result['direction'] = self::DIRECTION_SHOPPRO_TO_ORDERPRO;
|
||||||
$result['unsupported_integrations'] = $unsupportedCount;
|
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function syncPushDirection(int $integrationId): array
|
||||||
|
{
|
||||||
|
$pushed = 0;
|
||||||
|
$skipped = 0;
|
||||||
|
$failed = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$baseUrl = $this->resolveBaseUrl($integrationId);
|
||||||
|
$apiKey = $this->integrations->getApiKeyDecrypted($integrationId);
|
||||||
|
$timeout = $this->resolveTimeout($integrationId);
|
||||||
|
|
||||||
|
if ($baseUrl === '' || $apiKey === null || trim($apiKey) === '') {
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'integration_id' => $integrationId,
|
||||||
|
'direction' => self::DIRECTION_ORDERPRO_TO_SHOPPRO,
|
||||||
|
'message' => 'Brak poprawnych danych API dla integracji.',
|
||||||
|
'pushed' => 0,
|
||||||
|
'skipped' => 0,
|
||||||
|
'failed' => 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$reverseMap = $this->buildReverseStatusMap($integrationId);
|
||||||
|
if ($reverseMap === []) {
|
||||||
|
return [
|
||||||
|
'ok' => true,
|
||||||
|
'integration_id' => $integrationId,
|
||||||
|
'direction' => self::DIRECTION_ORDERPRO_TO_SHOPPRO,
|
||||||
|
'message' => 'Brak mapowania statusow dla integracji.',
|
||||||
|
'pushed' => 0,
|
||||||
|
'skipped' => 0,
|
||||||
|
'failed' => 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$lastPushedAt = $this->syncState->getLastStatusPushedAt($integrationId);
|
||||||
|
$orders = $this->findOrdersWithManualStatusChanges($integrationId, $lastPushedAt);
|
||||||
|
|
||||||
|
if ($orders === []) {
|
||||||
|
return [
|
||||||
|
'ok' => true,
|
||||||
|
'integration_id' => $integrationId,
|
||||||
|
'direction' => self::DIRECTION_ORDERPRO_TO_SHOPPRO,
|
||||||
|
'message' => 'Brak zamowien do synchronizacji.',
|
||||||
|
'pushed' => 0,
|
||||||
|
'skipped' => 0,
|
||||||
|
'failed' => 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$latestChangeAt = $lastPushedAt;
|
||||||
|
|
||||||
|
foreach ($orders as $order) {
|
||||||
|
$sourceOrderId = (int) ($order['source_order_id'] ?? 0);
|
||||||
|
$orderproStatus = strtolower(trim((string) ($order['external_status_id'] ?? '')));
|
||||||
|
$changeAt = (string) ($order['latest_change'] ?? '');
|
||||||
|
|
||||||
|
if ($sourceOrderId <= 0 || $orderproStatus === '') {
|
||||||
|
$skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$shopproStatusCode = $reverseMap[$orderproStatus] ?? null;
|
||||||
|
if ($shopproStatusCode === null) {
|
||||||
|
$skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$shopproStatusId = (int) $shopproStatusCode;
|
||||||
|
if ($shopproStatusId <= 0) {
|
||||||
|
$skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiResult = $this->apiClient->updateOrderStatus(
|
||||||
|
$baseUrl,
|
||||||
|
$apiKey,
|
||||||
|
$timeout,
|
||||||
|
$sourceOrderId,
|
||||||
|
$shopproStatusId,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
if (($apiResult['ok'] ?? false) === true) {
|
||||||
|
$pushed++;
|
||||||
|
} else {
|
||||||
|
$failed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($changeAt !== '' && ($latestChangeAt === null || $changeAt > $latestChangeAt)) {
|
||||||
|
$latestChangeAt = $changeAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($latestChangeAt !== null && $latestChangeAt !== $lastPushedAt) {
|
||||||
|
$this->syncState->updateLastStatusPushedAt($integrationId, $latestChangeAt);
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'integration_id' => $integrationId,
|
||||||
|
'direction' => self::DIRECTION_ORDERPRO_TO_SHOPPRO,
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
'pushed' => $pushed,
|
||||||
|
'skipped' => $skipped,
|
||||||
|
'failed' => $failed,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'ok' => true,
|
||||||
|
'integration_id' => $integrationId,
|
||||||
|
'direction' => self::DIRECTION_ORDERPRO_TO_SHOPPRO,
|
||||||
|
'pushed' => $pushed,
|
||||||
|
'skipped' => $skipped,
|
||||||
|
'failed' => $failed,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string> orderpro_status_code => shoppro_status_code
|
||||||
|
*/
|
||||||
|
private function buildReverseStatusMap(int $integrationId): array
|
||||||
|
{
|
||||||
|
$rows = $this->statusMappings->listByIntegration($integrationId);
|
||||||
|
$map = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$shopCode = trim((string) ($row['shoppro_status_code'] ?? ''));
|
||||||
|
$orderCode = strtolower(trim((string) ($row['orderpro_status_code'] ?? '')));
|
||||||
|
if ($shopCode === '' || $orderCode === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!isset($map[$orderCode])) {
|
||||||
|
$map[$orderCode] = $shopCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array{order_id:int,source_order_id:string,external_status_id:string,latest_change:string}>
|
||||||
|
*/
|
||||||
|
private function findOrdersWithManualStatusChanges(int $integrationId, ?string $lastPushedAt): array
|
||||||
|
{
|
||||||
|
$fallbackDate = date('Y-m-d H:i:s', strtotime('-24 hours'));
|
||||||
|
$sinceDate = ($lastPushedAt !== null && $lastPushedAt !== '') ? $lastPushedAt : $fallbackDate;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$statement = $this->pdo->prepare(
|
||||||
|
'SELECT 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 = :change_source
|
||||||
|
AND h.changed_at > :since_date
|
||||||
|
GROUP BY o.id, o.source_order_id, o.external_status_id
|
||||||
|
ORDER BY latest_change ASC
|
||||||
|
LIMIT 50'
|
||||||
|
);
|
||||||
|
$statement->execute([
|
||||||
|
'integration_id' => $integrationId,
|
||||||
|
'change_source' => 'manual',
|
||||||
|
'since_date' => $sinceDate,
|
||||||
|
]);
|
||||||
|
$rows = $statement->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
return is_array($rows) ? $rows : [];
|
||||||
|
} catch (Throwable) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveBaseUrl(int $integrationId): string
|
||||||
|
{
|
||||||
|
$integration = $this->integrations->findIntegration($integrationId);
|
||||||
|
if ($integration === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return rtrim(trim((string) ($integration['base_url'] ?? '')), '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveTimeout(int $integrationId): int
|
||||||
|
{
|
||||||
|
$integration = $this->integrations->findIntegration($integrationId);
|
||||||
|
if ($integration === null) {
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
return max(1, min(120, (int) ($integration['timeout_seconds'] ?? 10)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user