feat: add Shoppro payment status synchronization service
- Implemented ShopproPaymentStatusSyncService to handle payment status synchronization between Shoppro and Orderpro. - Added methods for resolving watched status codes, finding candidate orders, and syncing individual order payments. - Introduced ShopproStatusMappingRepository for managing status mappings between Shoppro and Orderpro. - Created ShopproStatusSyncService to facilitate synchronization of order statuses from Shoppro to Orderpro.
This commit is contained in:
98
.vscode/ftp-kr.sync.cache.json
vendored
98
.vscode/ftp-kr.sync.cache.json
vendored
@@ -7,6 +7,12 @@
|
|||||||
"lmtime": 1772652932723,
|
"lmtime": 1772652932723,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
|
"_allegro_check.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 1954,
|
||||||
|
"lmtime": 1772803697369,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
"ARCHITECTURE.md": {
|
"ARCHITECTURE.md": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 659,
|
"size": 659,
|
||||||
@@ -733,6 +739,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"_db_check2.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 1656,
|
||||||
|
"lmtime": 1772803550728,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
"_db_check3.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 1919,
|
||||||
|
"lmtime": 1772803572007,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
"_db_check.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 2025,
|
||||||
|
"lmtime": 1772803459353,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
"DB_SCHEMA.md": {
|
"DB_SCHEMA.md": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 363,
|
"size": 363,
|
||||||
@@ -748,9 +772,9 @@
|
|||||||
},
|
},
|
||||||
"DB_SCHEMA.md": {
|
"DB_SCHEMA.md": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 9585,
|
"size": 9908,
|
||||||
"lmtime": 1772751841787,
|
"lmtime": 1772751841787,
|
||||||
"modified": false
|
"modified": true
|
||||||
},
|
},
|
||||||
"ORDERS_SCHEMA_APILO_DRAFT.md": {
|
"ORDERS_SCHEMA_APILO_DRAFT.md": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
@@ -766,8 +790,8 @@
|
|||||||
},
|
},
|
||||||
"TECH_CHANGELOG.md": {
|
"TECH_CHANGELOG.md": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 21813,
|
"size": 22477,
|
||||||
"lmtime": 1772751847189,
|
"lmtime": 1772803892625,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"todo.md": {
|
"todo.md": {
|
||||||
@@ -2057,8 +2081,8 @@
|
|||||||
"lang": {
|
"lang": {
|
||||||
"pl.php": {
|
"pl.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 49807,
|
"size": 49998,
|
||||||
"lmtime": 1772751834546,
|
"lmtime": 1772791865236,
|
||||||
"modified": false
|
"modified": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2168,14 +2192,14 @@
|
|||||||
},
|
},
|
||||||
"list.php": {
|
"list.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 2344,
|
"size": 1552,
|
||||||
"lmtime": 1772497374098,
|
"lmtime": 1772497374098,
|
||||||
"modified": true
|
"modified": true
|
||||||
},
|
},
|
||||||
"show.php": {
|
"show.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 17387,
|
"size": 26016,
|
||||||
"lmtime": 1772747158454,
|
"lmtime": 1772791685988,
|
||||||
"modified": false
|
"modified": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2214,8 +2238,8 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"allegro.php": {
|
"allegro.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 31284,
|
"size": 32395,
|
||||||
"lmtime": 1772754778803,
|
"lmtime": 1772791281085,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"apaczka.php": {
|
"apaczka.php": {
|
||||||
@@ -2290,8 +2314,8 @@
|
|||||||
"shipments": {
|
"shipments": {
|
||||||
"prepare.php": {
|
"prepare.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 26553,
|
"size": 28300,
|
||||||
"lmtime": 1772754107648,
|
"lmtime": 1772792497957,
|
||||||
"modified": false
|
"modified": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2300,8 +2324,8 @@
|
|||||||
"routes": {
|
"routes": {
|
||||||
"web.php": {
|
"web.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 10727,
|
"size": 10849,
|
||||||
"lmtime": 1772750372002,
|
"lmtime": 1772791657587,
|
||||||
"modified": false
|
"modified": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2349,8 +2373,8 @@
|
|||||||
"Core": {
|
"Core": {
|
||||||
"Application.php": {
|
"Application.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 10729,
|
"size": 11003,
|
||||||
"lmtime": 1772661737740,
|
"lmtime": 1772803779141,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"Database": {
|
"Database": {
|
||||||
@@ -2566,9 +2590,9 @@
|
|||||||
"Orders": {
|
"Orders": {
|
||||||
"OrderImportRepository.php": {
|
"OrderImportRepository.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 18405,
|
"size": 18666,
|
||||||
"lmtime": 1772655751334,
|
"lmtime": 1772655751334,
|
||||||
"modified": false
|
"modified": true
|
||||||
},
|
},
|
||||||
"OrderImportService.php": {
|
"OrderImportService.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
@@ -2578,15 +2602,15 @@
|
|||||||
},
|
},
|
||||||
"OrdersController.php": {
|
"OrdersController.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 26496,
|
"size": 27379,
|
||||||
"lmtime": 1772752037320,
|
"lmtime": 1772791653816,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"OrdersRepository.php": {
|
"OrdersRepository.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 30133,
|
"size": 30430,
|
||||||
"lmtime": 1772752022929,
|
"lmtime": 1772752022929,
|
||||||
"modified": false
|
"modified": true
|
||||||
},
|
},
|
||||||
"OrderStatusSyncService.php": {
|
"OrderStatusSyncService.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
@@ -2686,8 +2710,8 @@
|
|||||||
},
|
},
|
||||||
"AllegroIntegrationController.php": {
|
"AllegroIntegrationController.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 36725,
|
"size": 36988,
|
||||||
"lmtime": 1772751733083,
|
"lmtime": 1772794656848,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"AllegroIntegrationRepository.php": {
|
"AllegroIntegrationRepository.php": {
|
||||||
@@ -2704,14 +2728,14 @@
|
|||||||
},
|
},
|
||||||
"AllegroOrderImportService.php": {
|
"AllegroOrderImportService.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 32629,
|
"size": 32797,
|
||||||
"lmtime": 1772743953928,
|
"lmtime": 1772792227813,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"AllegroOrdersSyncService.php": {
|
"AllegroOrdersSyncService.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 11544,
|
"size": 11544,
|
||||||
"lmtime": 1772660728121,
|
"lmtime": 1772803813533,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"AllegroOrderSyncStateRepository.php": {
|
"AllegroOrderSyncStateRepository.php": {
|
||||||
@@ -2734,8 +2758,8 @@
|
|||||||
},
|
},
|
||||||
"AllegroStatusSyncService.php": {
|
"AllegroStatusSyncService.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 1537,
|
"size": 3266,
|
||||||
"lmtime": 1772661706942,
|
"lmtime": 1772803803020,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"ApaczkaIntegrationController.php": {
|
"ApaczkaIntegrationController.php": {
|
||||||
@@ -2834,14 +2858,14 @@
|
|||||||
"Shipments": {
|
"Shipments": {
|
||||||
"AllegroShipmentService.php": {
|
"AllegroShipmentService.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 17565,
|
"size": 18000,
|
||||||
"lmtime": 1772753969008,
|
"lmtime": 1772792396143,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"ShipmentController.php": {
|
"ShipmentController.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 10999,
|
"size": 12247,
|
||||||
"lmtime": 1772753068348,
|
"lmtime": 1772792492765,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"ShipmentPackageRepository.php": {
|
"ShipmentPackageRepository.php": {
|
||||||
@@ -4735,6 +4759,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"_test_status_sync.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 2434,
|
||||||
|
"lmtime": 1772803861129,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
"tmp_gs1_test.php": {
|
"tmp_gs1_test.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 3392,
|
"size": 3392,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
- `POST /settings/statuses/reorder`
|
- `POST /settings/statuses/reorder`
|
||||||
- `GET /settings/cron`
|
- `GET /settings/cron`
|
||||||
- `POST /settings/cron`
|
- `POST /settings/cron`
|
||||||
|
- `GET /settings/integrations`
|
||||||
- `GET /settings/integrations/allegro`
|
- `GET /settings/integrations/allegro`
|
||||||
- `POST /settings/integrations/allegro/save`
|
- `POST /settings/integrations/allegro/save`
|
||||||
- `POST /settings/integrations/allegro/oauth/start`
|
- `POST /settings/integrations/allegro/oauth/start`
|
||||||
@@ -45,6 +46,12 @@
|
|||||||
- `POST /settings/integrations/apaczka/save`
|
- `POST /settings/integrations/apaczka/save`
|
||||||
- `GET /settings/integrations/inpost`
|
- `GET /settings/integrations/inpost`
|
||||||
- `POST /settings/integrations/inpost/save`
|
- `POST /settings/integrations/inpost/save`
|
||||||
|
- `GET /settings/integrations/shoppro`
|
||||||
|
- `POST /settings/integrations/shoppro/save`
|
||||||
|
- `POST /settings/integrations/shoppro/test`
|
||||||
|
- `POST /settings/integrations/shoppro/statuses/save`
|
||||||
|
- `POST /settings/integrations/shoppro/statuses/sync`
|
||||||
|
- `POST /settings/integrations/shoppro/delivery/save`
|
||||||
- `GET /health`
|
- `GET /health`
|
||||||
- `GET /` (redirect)
|
- `GET /` (redirect)
|
||||||
|
|
||||||
@@ -69,6 +76,8 @@
|
|||||||
- `App\Modules\Settings\AllegroOrderImportService`
|
- `App\Modules\Settings\AllegroOrderImportService`
|
||||||
- `App\Modules\Settings\AllegroStatusMappingRepository`
|
- `App\Modules\Settings\AllegroStatusMappingRepository`
|
||||||
- `App\Modules\Settings\AllegroStatusDiscoveryService`
|
- `App\Modules\Settings\AllegroStatusDiscoveryService`
|
||||||
|
- `App\Modules\Settings\IntegrationsRepository`
|
||||||
|
- `App\Modules\Settings\IntegrationSecretCipher`
|
||||||
- `App\Modules\Orders\OrderImportRepository`
|
- `App\Modules\Orders\OrderImportRepository`
|
||||||
- `App\Modules\Settings\CronSettingsController`
|
- `App\Modules\Settings\CronSettingsController`
|
||||||
- `App\Modules\Cron\CronRepository`
|
- `App\Modules\Cron\CronRepository`
|
||||||
@@ -76,12 +85,24 @@
|
|||||||
- `App\Modules\Cron\AllegroTokenRefreshHandler`
|
- `App\Modules\Cron\AllegroTokenRefreshHandler`
|
||||||
- `App\Modules\Cron\AllegroOrdersImportHandler`
|
- `App\Modules\Cron\AllegroOrdersImportHandler`
|
||||||
- `App\Modules\Cron\AllegroStatusSyncHandler`
|
- `App\Modules\Cron\AllegroStatusSyncHandler`
|
||||||
|
- `App\Modules\Cron\ShopproOrdersImportHandler`
|
||||||
|
- `App\Modules\Cron\ShopproStatusSyncHandler`
|
||||||
|
- `App\Modules\Cron\ShopproPaymentStatusSyncHandler`
|
||||||
- `App\Modules\Users\UsersController`
|
- `App\Modules\Users\UsersController`
|
||||||
- `App\Modules\Users\UserRepository`
|
- `App\Modules\Users\UserRepository`
|
||||||
- `App\Modules\Settings\ApaczkaIntegrationController`
|
- `App\Modules\Settings\ApaczkaIntegrationController`
|
||||||
- `App\Modules\Settings\ApaczkaIntegrationRepository`
|
- `App\Modules\Settings\ApaczkaIntegrationRepository`
|
||||||
- `App\Modules\Settings\InpostIntegrationController`
|
- `App\Modules\Settings\InpostIntegrationController`
|
||||||
- `App\Modules\Settings\InpostIntegrationRepository`
|
- `App\Modules\Settings\InpostIntegrationRepository`
|
||||||
|
- `App\Modules\Settings\IntegrationsHubController`
|
||||||
|
- `App\Modules\Settings\ShopproIntegrationsController`
|
||||||
|
- `App\Modules\Settings\ShopproIntegrationsRepository`
|
||||||
|
- `App\Modules\Settings\ShopproStatusMappingRepository`
|
||||||
|
- `App\Modules\Settings\ShopproDeliveryMethodMappingRepository`
|
||||||
|
- `App\Modules\Settings\ShopproApiClient`
|
||||||
|
- `App\Modules\Settings\ShopproOrdersSyncService`
|
||||||
|
- `App\Modules\Settings\ShopproStatusSyncService`
|
||||||
|
- `App\Modules\Settings\ShopproPaymentStatusSyncService`
|
||||||
- `App\Modules\Settings\AllegroOrdersSyncService`
|
- `App\Modules\Settings\AllegroOrdersSyncService`
|
||||||
- `App\Modules\Settings\AllegroOrderSyncStateRepository`
|
- `App\Modules\Settings\AllegroOrderSyncStateRepository`
|
||||||
- `App\Modules\Settings\AllegroStatusSyncService`
|
- `App\Modules\Settings\AllegroStatusSyncService`
|
||||||
@@ -167,15 +188,14 @@
|
|||||||
- Sidebar (`resources/views/layouts/app.php`) ma nowy podlink:
|
- Sidebar (`resources/views/layouts/app.php`) ma nowy podlink:
|
||||||
- `Statusy` (`/settings/statuses`).
|
- `Statusy` (`/settings/statuses`).
|
||||||
- `Cron` (`/settings/cron`).
|
- `Cron` (`/settings/cron`).
|
||||||
- `Integracje Allegro` (`/settings/integrations/allegro`).
|
- `Integracje` (`/settings/integrations`) - wspolny hub konfiguracji providerow.
|
||||||
- `Integracja Apaczka` (`/settings/integrations/apaczka`).
|
|
||||||
- `Integracja InPost` (`/settings/integrations/inpost`).
|
|
||||||
|
|
||||||
## Przeplyw Ustawienia > Cron
|
## Przeplyw Ustawienia > Cron
|
||||||
- `GET /settings/cron`:
|
- `GET /settings/cron`:
|
||||||
- `CronSettingsController::index(Request): Response`
|
- `CronSettingsController::index(Request): Response`
|
||||||
- pobiera ustawienia `cron_run_on_web`, `cron_web_limit`,
|
- pobiera ustawienia `cron_run_on_web`, `cron_web_limit`,
|
||||||
- renderuje harmonogramy (`cron_schedules`) oraz kolejke/historie (`cron_jobs`).
|
- renderuje harmonogramy (`cron_schedules`) oraz kolejke/historie (`cron_jobs`),
|
||||||
|
- historia (`past_jobs`) ma stronicowanie po parametrze query `past_page` (25 rekordow na strone).
|
||||||
- `POST /settings/cron`:
|
- `POST /settings/cron`:
|
||||||
- `CronSettingsController::save(Request): Response`
|
- `CronSettingsController::save(Request): Response`
|
||||||
- waliduje CSRF,
|
- waliduje CSRF,
|
||||||
@@ -196,6 +216,9 @@
|
|||||||
- Dodatkowy handler:
|
- Dodatkowy handler:
|
||||||
- `allegro_orders_import` -> `AllegroOrdersImportHandler::handle(...)` (automatyczny import zamowien Allegro).
|
- `allegro_orders_import` -> `AllegroOrdersImportHandler::handle(...)` (automatyczny import zamowien Allegro).
|
||||||
- `allegro_status_sync` -> `AllegroStatusSyncHandler::handle(...)` (synchronizacja statusow wg kierunku z ustawien integracji Allegro).
|
- `allegro_status_sync` -> `AllegroStatusSyncHandler::handle(...)` (synchronizacja statusow wg kierunku z ustawien integracji Allegro).
|
||||||
|
- `shoppro_orders_import` -> `ShopproOrdersImportHandler::handle(...)` (automatyczny import zamowien z aktywnych integracji `shopPRO` z wlaczonym pobieraniem).
|
||||||
|
- `shoppro_order_status_sync` -> `ShopproStatusSyncHandler::handle(...)` (synchronizacja statusow shopPRO wg kierunku ustawionego per instancja).
|
||||||
|
- `shoppro_payment_status_sync` -> `ShopproPaymentStatusSyncHandler::handle(...)` (odswiezanie statusu platnosci zamowien shopPRO na podstawie flagi `paid`).
|
||||||
|
|
||||||
## Przeplyw Ustawienia > Integracje > Allegro
|
## Przeplyw Ustawienia > Integracje > Allegro
|
||||||
- `GET /settings/integrations/allegro`:
|
- `GET /settings/integrations/allegro`:
|
||||||
@@ -248,10 +271,11 @@
|
|||||||
- mapuje terminy wysylki z `delivery.time.dispatch` do `send_date_min` / `send_date_max`,
|
- mapuje terminy wysylki z `delivery.time.dispatch` do `send_date_min` / `send_date_max`,
|
||||||
- buduje diagnostyke importu miniatur (statystyki + przyczyny brakow),
|
- buduje diagnostyke importu miniatur (statystyki + przyczyny brakow),
|
||||||
- mapuje status Allegro na status orderPRO na podstawie `allegro_order_status_mappings`,
|
- mapuje status Allegro na status orderPRO na podstawie `allegro_order_status_mappings`,
|
||||||
- mapuje payload Allegro na neutralny model tabel zamowien,
|
- mapuje payload Allegro na neutralny model tabel zamowien (z `integration_id` aktywnej instancji Allegro),
|
||||||
- zapisuje aggregate przez `OrderImportRepository::upsertOrderAggregate(...)`.
|
- zapisuje aggregate przez `OrderImportRepository::upsertOrderAggregate(...)`.
|
||||||
- `AllegroOrdersSyncService`:
|
- `AllegroOrdersSyncService`:
|
||||||
- uruchamiany z crona (`allegro_orders_import`),
|
- uruchamiany z crona (`allegro_orders_import`),
|
||||||
|
- korzysta z dynamicznego `integration_id` aktywnego srodowiska Allegro (zamiast stalej),
|
||||||
- respektuje ustawienia integracji (`orders_fetch_enabled`, `orders_fetch_start_date`),
|
- respektuje ustawienia integracji (`orders_fetch_enabled`, `orders_fetch_start_date`),
|
||||||
- pobiera listy checkout forms (`GET /order/checkout-forms?sort=-updatedAt`) i importuje nowe/zmienione zamowienia,
|
- pobiera listy checkout forms (`GET /order/checkout-forms?sort=-updatedAt`) i importuje nowe/zmienione zamowienia,
|
||||||
- utrzymuje kursor sync i status ostatniego wykonania w `integration_order_sync_state`.
|
- utrzymuje kursor sync i status ostatniego wykonania w `integration_order_sync_state`.
|
||||||
@@ -286,7 +310,7 @@
|
|||||||
- `POST /settings/integrations/apaczka/save`:
|
- `POST /settings/integrations/apaczka/save`:
|
||||||
- `ApaczkaIntegrationController::save(Request): Response`
|
- `ApaczkaIntegrationController::save(Request): Response`
|
||||||
- waliduje CSRF i klucz API,
|
- waliduje CSRF i klucz API,
|
||||||
- zapisuje zaszyfrowany klucz API przez `ApaczkaIntegrationRepository::saveSettings(...)`.
|
- zapisuje zaszyfrowany klucz API przez `ApaczkaIntegrationRepository::saveSettings(...)` do tabeli bazowej `integrations` (`type=apaczka`).
|
||||||
|
|
||||||
## Przeplyw Ustawienia > Integracja InPost
|
## Przeplyw Ustawienia > Integracja InPost
|
||||||
- `GET /settings/integrations/inpost`:
|
- `GET /settings/integrations/inpost`:
|
||||||
@@ -296,7 +320,9 @@
|
|||||||
- `POST /settings/integrations/inpost/save`:
|
- `POST /settings/integrations/inpost/save`:
|
||||||
- `InpostIntegrationController::save(Request): Response`
|
- `InpostIntegrationController::save(Request): Response`
|
||||||
- waliduje CSRF,
|
- waliduje CSRF,
|
||||||
- zapisuje ustawienia (token API szyfrowany AES-256-CBC, parametry domyslne przesylek) przez `InpostIntegrationRepository::saveSettings(...)`.
|
- zapisuje ustawienia przez `InpostIntegrationRepository::saveSettings(...)`:
|
||||||
|
- token API w `integrations.api_key_encrypted` (`type=inpost`),
|
||||||
|
- parametry specyficzne przewoznika w `inpost_integration_settings`.
|
||||||
|
|
||||||
## Przeplyw Ustawienia > Baza danych
|
## Przeplyw Ustawienia > Baza danych
|
||||||
- `GET /settings/database`:
|
- `GET /settings/database`:
|
||||||
@@ -323,3 +349,66 @@
|
|||||||
- nowe klasy i metody (sygnatury + odpowiedzialnosc),
|
- nowe klasy i metody (sygnatury + odpowiedzialnosc),
|
||||||
- zmiany przeplywu request -> controller -> repository,
|
- zmiany przeplywu request -> controller -> repository,
|
||||||
- kontrakty wejscia/wyjscia istotnych metod.
|
- kontrakty wejscia/wyjscia istotnych metod.
|
||||||
|
## Przeplyw Ustawienia > Integracje (hub)
|
||||||
|
- `GET /settings/integrations`:
|
||||||
|
- `IntegrationsHubController::index(Request): Response`
|
||||||
|
- buduje liste instancji providerow (Allegro sandbox/production, Apaczka, InPost, shopPRO),
|
||||||
|
- pokazuje tabele podsumowania i przycisk `Ustawienia` w kazdym wierszu,
|
||||||
|
- przycisk `Ustawienia` prowadzi do dedykowanego ekranu providera (`/settings/integrations/allegro|apaczka|inpost|shoppro`),
|
||||||
|
- renderuje `resources/views/settings/integrations.php`.
|
||||||
|
|
||||||
|
## Przeplyw Ustawienia > Integracje > shopPRO
|
||||||
|
- `GET /settings/integrations/shoppro`:
|
||||||
|
- `ShopproIntegrationsController::index(Request): Response`
|
||||||
|
- pobiera liste instancji przez `ShopproIntegrationsRepository::listIntegrations()`,
|
||||||
|
- opcjonalnie laduje wskazana instancje (`?id=`) przez `findIntegration(...)`,
|
||||||
|
- renderuje `resources/views/settings/shoppro.php` z zakladkami: `Integracja`, `Statusy`, `Ustawienia`, `Formy dostawy`.
|
||||||
|
- `POST /settings/integrations/shoppro/save`:
|
||||||
|
- `ShopproIntegrationsController::save(Request): Response`
|
||||||
|
- waliduje CSRF, nazwe, URL (`http|https`), klucz API (wymagany przy nowej konfiguracji) oraz format daty `orders_fetch_start_date` (`Y-m-d`),
|
||||||
|
- zapisuje konfiguracje przez `ShopproIntegrationsRepository::saveIntegration(...)` do tabeli bazowej `integrations` (`type=shoppro`),
|
||||||
|
- zapisuje interwal joba `shoppro_orders_import` (minuty) do `cron_schedules.interval_seconds`,
|
||||||
|
- zapisuje kierunek synchronizacji statusow per instancja (`integrations.order_status_sync_direction`),
|
||||||
|
- zapisuje interwal joba `shoppro_order_status_sync` (minuty) do `cron_schedules.interval_seconds`,
|
||||||
|
- zapisuje interwal joba `shoppro_payment_status_sync` (minuty) do `cron_schedules.interval_seconds`,
|
||||||
|
- zapisuje liste statusow orderPRO (per instancja) dla kontroli platnosci (`integrations.payment_sync_status_codes_json`).
|
||||||
|
- `POST /settings/integrations/shoppro/test`:
|
||||||
|
- `ShopproIntegrationsController::test(Request): Response`
|
||||||
|
- waliduje CSRF i `integration_id`,
|
||||||
|
- wykonuje test API przez `ShopproIntegrationsRepository::testConnection(...)`,
|
||||||
|
- zapisuje wynik testu w `integrations.last_test_*` i `integration_test_logs`.
|
||||||
|
- `POST /settings/integrations/shoppro/statuses/sync`:
|
||||||
|
- `ShopproIntegrationsController::syncStatuses(Request): Response`
|
||||||
|
- pobiera slownik statusow z API (`dictionaries/statuses`) przez `ShopproIntegrationsRepository::fetchOrderStatuses(...)`,
|
||||||
|
- przekazuje odkryte statusy do widoku zakladki `Statusy` (flash/sesja).
|
||||||
|
- `POST /settings/integrations/shoppro/statuses/save`:
|
||||||
|
- `ShopproIntegrationsController::saveStatusMappings(Request): Response`
|
||||||
|
- waliduje CSRF, `integration_id` i kody statusow orderPRO,
|
||||||
|
- zapisuje mapowania per instancja shopPRO przez `ShopproStatusMappingRepository::replaceForIntegration(...)` do `order_status_mappings`.
|
||||||
|
- `POST /settings/integrations/shoppro/delivery/save`:
|
||||||
|
- `ShopproIntegrationsController::saveDeliveryMappings(Request): Response`
|
||||||
|
- waliduje CSRF i `integration_id`,
|
||||||
|
- zapisuje mapowania form dostawy przez `ShopproDeliveryMethodMappingRepository::saveMappings(...)` (per instancja).
|
||||||
|
- `ShopproOrdersSyncService`:
|
||||||
|
- uruchamiany z crona (`shoppro_orders_import`),
|
||||||
|
- pobiera liste zamowien i (opcjonalnie) szczegoly zamowienia z API shopPRO,
|
||||||
|
- mapuje kwoty z fallbackami (`summary`, `paid`, `transport_cost`) oraz ceny pozycji (`price_brutto`),
|
||||||
|
- uzupelnia `order_items.media_url` przez pobranie `products/get` po `product_id`, gdy zamowienie nie zawiera obrazu.
|
||||||
|
- mapuje punkty odbioru (`inpost_paczkomat` / `orlen_point`) do adresu `delivery` (`parcel_external_id`, `parcel_name`, ulica/kod/miasto),
|
||||||
|
- uzupelnia `delivery` o telefon/e-mail klienta i etykiete metody dostawy z kosztem (`transport_cost`).
|
||||||
|
- `ShopproStatusSyncService`:
|
||||||
|
- uruchamiany z crona (`shoppro_order_status_sync`),
|
||||||
|
- filtruje aktywne instancje `shopPRO` po kierunku synchronizacji statusow (`shoppro_to_orderpro`),
|
||||||
|
- dla wspieranego kierunku wykorzystuje `ShopproOrdersSyncService` do odswiezenia statusow/importu danych,
|
||||||
|
- dla kierunku `orderpro_to_shoppro` pomija instancje i zwraca wynik informacyjny (tryb przygotowany pod kolejny etap).
|
||||||
|
- `ShopproPaymentStatusSyncService`:
|
||||||
|
- uruchamiany z crona (`shoppro_payment_status_sync`),
|
||||||
|
- pobiera zamowienia shopPRO nieoznaczone jako oplacone (`orders.payment_status != 2`) i nie-finalne,
|
||||||
|
- dla kazdego zamowienia odpytuje API `orders/get|details` i odczytuje flage `paid`,
|
||||||
|
- aktualizuje `orders.payment_status`, `orders.total_paid` i `order_payments`,
|
||||||
|
- zapisuje log `payment` do `order_activity_log`,
|
||||||
|
- respektuje liste statusow z `integrations.payment_sync_status_codes_json` (gdy pusta: fallback na pomijanie statusow finalnych).
|
||||||
|
- Zakladka `Formy dostawy` (shopPRO):
|
||||||
|
- laduje formy dostawy wykryte w zamowieniach danej instancji (`orders.source=shoppro` + `orders.integration_id`),
|
||||||
|
- laduje uslugi dostawy z Allegro API (`delivery-services`) z fallbackiem na odswiezenie tokenu OAuth,
|
||||||
|
- zapisuje mapowanie: forma dostawy shopPRO -> usluga Allegro/InPost WZA.
|
||||||
|
|||||||
@@ -27,6 +27,24 @@
|
|||||||
- 2026-03-06: Dodano kolumne `carrier` do tabeli `allegro_delivery_method_mappings` (default 'allegro') - umozliwia mapowanie na roznych przewoznikow (Allegro, InPost).
|
- 2026-03-06: Dodano kolumne `carrier` do tabeli `allegro_delivery_method_mappings` (default 'allegro') - umozliwia mapowanie na roznych przewoznikow (Allegro, InPost).
|
||||||
- 2026-03-06: Wdrozono migracje `20260302_000019_add_internal_order_number_to_orders.sql` - kolumna `internal_order_number` VARCHAR(11) UNIQUE w tabeli `orders`, format `OPXXXXXXXXX` (np. `OP000000001`); backfill istniejacych rekordow; UI: lista i szczegoly zamowien wyswietlaja numer wewnetrzny jako glowny identyfikator.
|
- 2026-03-06: Wdrozono migracje `20260302_000019_add_internal_order_number_to_orders.sql` - kolumna `internal_order_number` VARCHAR(11) UNIQUE w tabeli `orders`, format `OPXXXXXXXXX` (np. `OP000000001`); backfill istniejacych rekordow; UI: lista i szczegoly zamowien wyswietlaja numer wewnetrzny jako glowny identyfikator.
|
||||||
- 2026-03-04: Poprawiono prezentacje daty zamowienia na liscie (`fallback ordered_at -> source_created_at -> source_updated_at -> fetched_at`) - bez zmian schematu.
|
- 2026-03-04: Poprawiono prezentacje daty zamowienia na liscie (`fallback ordered_at -> source_created_at -> source_updated_at -> fetched_at`) - bez zmian schematu.
|
||||||
|
- 2026-03-08: Rozpoczeto ujednolicanie integracji - migracja `20260308_000037_unify_integrations_base_links.sql`:
|
||||||
|
- dodano `integration_id` do tabel `allegro_integration_settings`, `apaczka_integration_settings`, `inpost_integration_settings`,
|
||||||
|
- dodano FK/UNIQUE 1:1 z tabela `integrations`,
|
||||||
|
- dodano/uzupelniono rekordy bazowe providerow (`allegro`, `apaczka`, `inpost`) w `integrations`.
|
||||||
|
- 2026-03-08: Dodano UI i endpointy konfiguracji `shopPRO` (wieloinstancyjnie) w oparciu o istniejaca tabele `integrations` (`type=shoppro`) - bez zmian schematu.
|
||||||
|
- 2026-03-08: Dodano mapowanie statusow dla `shopPRO` (zakladka `Statusy`) z zapisem do istniejacej tabeli `order_status_mappings` per `integration_id` - bez zmian schematu.
|
||||||
|
- 2026-03-08: Dodano migracje naprawcza `20260308_000038_ensure_order_status_mappings_table.sql` (uzupelnia brakujaca tabele `order_status_mappings` w srodowiskach z niepelna historia migracji).
|
||||||
|
- 2026-03-08: Dodano migracje naprawcza `20260308_000039_ensure_integrations_fetch_columns.sql` (uzupelnia brakujace kolumny `orders_fetch_enabled` i `orders_fetch_start_date` w `integrations`).
|
||||||
|
- 2026-03-08: Dodano migracje `20260308_000040_ensure_shoppro_orders_import_schedule.sql` (seed/naprawa harmonogramu `cron_schedules` dla joba `shoppro_orders_import`) - bez zmian schematu tabel.
|
||||||
|
- 2026-03-08: Dodano migracje `20260308_000041_ensure_shoppro_status_sync_schedule_and_direction.sql` (seed/naprawa harmonogramu `cron_schedules` dla joba `shoppro_order_status_sync` oraz uzupelnienie kolumny `integrations.order_status_sync_direction` w srodowiskach niezgodnych).
|
||||||
|
- 2026-03-08: Dodano migracje `20260308_000042_ensure_shoppro_payment_sync_schedule_and_columns.sql`:
|
||||||
|
- uzupelnienie kolumny `integrations.payment_sync_status_codes_json` (JSON) dla konfiguracji statusow objetych kontrola platnosci,
|
||||||
|
- seed/naprawa harmonogramu `cron_schedules` dla joba `shoppro_payment_status_sync` (domyslnie 600s, priorytet 105).
|
||||||
|
- 2026-03-08: Dodano migracje `20260308_000043_create_shoppro_delivery_method_mappings_table.sql`:
|
||||||
|
- nowa tabela `shoppro_delivery_method_mappings` (mapowanie form dostawy per `integration_id`),
|
||||||
|
- tabela przechowuje mapowanie formy shopPRO na usluge Allegro WZA/InPost (`allegro_delivery_method_id`, `allegro_credentials_id`, `allegro_carrier_id`, `allegro_service_name`, `carrier`).
|
||||||
|
- 2026-03-08: Poprawiono mapowanie importu zamowien shopPRO (kwoty i miniatury pozycji) - bez zmian schematu bazy.
|
||||||
|
- 2026-03-08: Poprawiono mapowanie danych wysylki shopPRO (paczkomat/punkt odbioru + kontakt klienta + koszt transportu) - bez zmian schematu bazy.
|
||||||
|
|
||||||
## Tabele
|
## Tabele
|
||||||
|
|
||||||
@@ -94,10 +112,27 @@
|
|||||||
- `last_error` (varchar 500),
|
- `last_error` (varchar 500),
|
||||||
- `created_at`, `updated_at`.
|
- `created_at`, `updated_at`.
|
||||||
|
|
||||||
|
### `order_status_mappings`
|
||||||
|
- Mapowanie statusow zamowien shopPRO na statusy orderPRO per instancja integracji.
|
||||||
|
- Kolumny:
|
||||||
|
- `id` (PK, int unsigned, AI),
|
||||||
|
- `integration_id` (int unsigned, FK -> `integrations.id`),
|
||||||
|
- `shoppro_status_code` (varchar 64),
|
||||||
|
- `shoppro_status_name` (varchar 128, nullable),
|
||||||
|
- `orderpro_status_code` (varchar 64),
|
||||||
|
- `created_at`, `updated_at`.
|
||||||
|
- Indeksy:
|
||||||
|
- `order_status_mappings_integration_shoppro_unique` (UNIQUE: `integration_id`, `shoppro_status_code`),
|
||||||
|
- `order_status_mappings_integration_idx` (`integration_id`),
|
||||||
|
- `order_status_mappings_orderpro_idx` (`orderpro_status_code`).
|
||||||
|
- Klucze obce:
|
||||||
|
- `order_status_mappings_integration_fk`: `integration_id` -> `integrations.id` (`ON DELETE CASCADE`, `ON UPDATE CASCADE`).
|
||||||
|
|
||||||
### `allegro_integration_settings`
|
### `allegro_integration_settings`
|
||||||
- Konfiguracja pojedynczej integracji Allegro (`id = 1`) zarzadzanej z `Ustawienia > Integracje > Allegro`.
|
- Konfiguracja OAuth i sync dla integracji Allegro per srodowisko (`sandbox|production`) zarzadzana z `Ustawienia > Integracje > Allegro`.
|
||||||
- Kolumny:
|
- Kolumny:
|
||||||
- `id` (PK, tinyint unsigned),
|
- `id` (PK, tinyint unsigned),
|
||||||
|
- `integration_id` (int unsigned, UNIQUE, FK -> `integrations.id`),
|
||||||
- `environment` (varchar 16, `sandbox|production`),
|
- `environment` (varchar 16, `sandbox|production`),
|
||||||
- `client_id` (varchar 128),
|
- `client_id` (varchar 128),
|
||||||
- `client_secret_encrypted` (text),
|
- `client_secret_encrypted` (text),
|
||||||
@@ -113,7 +148,8 @@
|
|||||||
- `created_at`, `updated_at`.
|
- `created_at`, `updated_at`.
|
||||||
- Indeksy:
|
- Indeksy:
|
||||||
- `allegro_integration_settings_environment_idx` (`environment`),
|
- `allegro_integration_settings_environment_idx` (`environment`),
|
||||||
- `allegro_integration_settings_token_expires_at_idx` (`token_expires_at`).
|
- `allegro_integration_settings_token_expires_at_idx` (`token_expires_at`),
|
||||||
|
- `allegro_integration_settings_integration_unique` (`integration_id`, UNIQUE).
|
||||||
|
|
||||||
### `allegro_order_status_mappings`
|
### `allegro_order_status_mappings`
|
||||||
- Mapowanie kodow statusow Allegro na kody statusow orderPRO.
|
- Mapowanie kodow statusow Allegro na kody statusow orderPRO.
|
||||||
@@ -142,17 +178,54 @@
|
|||||||
- `order_activity_log_order_created_idx` (`order_id`, `created_at`),
|
- `order_activity_log_order_created_idx` (`order_id`, `created_at`),
|
||||||
- `order_activity_log_event_type_idx` (`event_type`).
|
- `order_activity_log_event_type_idx` (`event_type`).
|
||||||
|
|
||||||
|
### `integrations`
|
||||||
|
- Bazowa tabela wszystkich instancji integracji (model docelowy pod wielu providerow i wiele kont per provider).
|
||||||
|
- Kolumny:
|
||||||
|
- `id` (PK, int unsigned, AI),
|
||||||
|
- `type` (varchar 32, np. `allegro`, `apaczka`, `inpost`, `shoppro`),
|
||||||
|
- `name` (varchar 128, unikalne w obrebie `type`),
|
||||||
|
- `base_url` (varchar 255),
|
||||||
|
- `api_key_encrypted` (text, nullable),
|
||||||
|
- `timeout_seconds` (smallint unsigned),
|
||||||
|
- `is_active` (tinyint(1)),
|
||||||
|
- `orders_fetch_enabled` (tinyint(1)),
|
||||||
|
- `orders_fetch_start_date` (date, nullable),
|
||||||
|
- `order_status_sync_direction` (varchar 32),
|
||||||
|
- `payment_sync_status_codes_json` (json, nullable; lista kodow statusow orderPRO, dla ktorych cron ma sprawdzac oplacenie zamowien),
|
||||||
|
- pola diagnostyki testu (`last_test_status`, `last_test_http_code`, `last_test_message`, `last_test_at`),
|
||||||
|
- `created_at`, `updated_at`.
|
||||||
|
|
||||||
|
### `shoppro_delivery_method_mappings`
|
||||||
|
- Mapowanie form dostawy shopPRO na uslugi dostawy Allegro WZA/InPost per instancja integracji.
|
||||||
|
- Kolumny:
|
||||||
|
- `id` (PK, int unsigned, AI),
|
||||||
|
- `integration_id` (int unsigned, FK -> `integrations.id`),
|
||||||
|
- `order_delivery_method` (varchar 200),
|
||||||
|
- `carrier` (varchar 50; np. `allegro`, `inpost`),
|
||||||
|
- `allegro_delivery_method_id` (varchar 128),
|
||||||
|
- `allegro_credentials_id` (varchar 128, nullable),
|
||||||
|
- `allegro_carrier_id` (varchar 128, nullable),
|
||||||
|
- `allegro_service_name` (varchar 255, nullable),
|
||||||
|
- `created_at`, `updated_at`.
|
||||||
|
- Indeksy:
|
||||||
|
- `shoppro_dm_mapping_unique` (UNIQUE: `integration_id`, `order_delivery_method`),
|
||||||
|
- `shoppro_dm_mapping_integration_idx` (`integration_id`).
|
||||||
|
- Klucze obce:
|
||||||
|
- `shoppro_dm_mapping_integration_fk`: `integration_id` -> `integrations.id` (`ON DELETE CASCADE`, `ON UPDATE CASCADE`).
|
||||||
|
|
||||||
### `apaczka_integration_settings`
|
### `apaczka_integration_settings`
|
||||||
- Konfiguracja pojedynczej integracji Apaczka (`id = 1`) zarzadzanej z `Ustawienia > Integracja Apaczka`.
|
- Tabela kompatybilnosci dla integracji Apaczka (`id = 1`); sekret API jest utrzymywany bazowo w `integrations.api_key_encrypted`.
|
||||||
- Kolumny:
|
- Kolumny:
|
||||||
- `id` (PK, tinyint unsigned),
|
- `id` (PK, tinyint unsigned),
|
||||||
|
- `integration_id` (int unsigned, UNIQUE, FK -> `integrations.id`),
|
||||||
- `api_key_encrypted` (text, nullable),
|
- `api_key_encrypted` (text, nullable),
|
||||||
- `created_at`, `updated_at`.
|
- `created_at`, `updated_at`.
|
||||||
|
|
||||||
### `inpost_integration_settings`
|
### `inpost_integration_settings`
|
||||||
- Konfiguracja pojedynczej integracji InPost ShipX (`id = 1`) zarzadzanej z `Ustawienia > Integracja InPost`.
|
- Tabela ustawien specyficznych InPost ShipX (`id = 1`); token API utrzymywany bazowo w `integrations.api_key_encrypted`.
|
||||||
- Kolumny:
|
- Kolumny:
|
||||||
- `id` (PK, tinyint unsigned),
|
- `id` (PK, tinyint unsigned),
|
||||||
|
- `integration_id` (int unsigned, UNIQUE, FK -> `integrations.id`),
|
||||||
- `api_token_encrypted` (text, nullable),
|
- `api_token_encrypted` (text, nullable),
|
||||||
- `organization_id` (varchar 50, nullable),
|
- `organization_id` (varchar 50, nullable),
|
||||||
- `environment` (enum: sandbox, production),
|
- `environment` (enum: sandbox, production),
|
||||||
|
|||||||
@@ -1,5 +1,148 @@
|
|||||||
# Tech Changelog
|
# Tech Changelog
|
||||||
|
|
||||||
|
## 2026-03-08
|
||||||
|
- Dodano zakladke `Formy dostawy` dla integracji `shopPRO` (analogicznie do Allegro):
|
||||||
|
- nowy endpoint `POST /settings/integrations/shoppro/delivery/save`,
|
||||||
|
- mapowanie per instancja: forma dostawy shopPRO -> usluga dostawy Allegro/InPost WZA,
|
||||||
|
- UI z wyborem przewoznika (`allegro`/`inpost`) i wyszukiwaniem uslug Allegro.
|
||||||
|
- Dodano `ShopproDeliveryMethodMappingRepository` i tabele mapowan per `integration_id`.
|
||||||
|
- Dodano migracje `20260308_000043_create_shoppro_delivery_method_mappings_table.sql`.
|
||||||
|
- Dodano synchronizacje platnosci shopPRO oparta o flage `paid`:
|
||||||
|
- nowy job cron `shoppro_payment_status_sync`,
|
||||||
|
- nowy handler `App\Modules\Cron\ShopproPaymentStatusSyncHandler`,
|
||||||
|
- nowy serwis `App\Modules\Settings\ShopproPaymentStatusSyncService`,
|
||||||
|
- runner podlaczony w `App\Core\Application::maybeRunCronOnWeb(...)` oraz `bin/cron.php`.
|
||||||
|
- Rozszerzono `Ustawienia > Integracje > shopPRO > Ustawienia`:
|
||||||
|
- dodano interwal sprawdzania platnosci (minuty) dla joba `shoppro_payment_status_sync`,
|
||||||
|
- dodano wybor statusow orderPRO, dla ktorych cron ma sprawdzac oplacenie zamowien,
|
||||||
|
- zapis konfiguracji listy statusow per instancja trafia do `integrations.payment_sync_status_codes_json`.
|
||||||
|
- Import zamowien shopPRO zapisuje status platnosci numerycznie (`orders.payment_status`) na podstawie flagi `paid`, co unifikuje filtry/statystyki platnosci z Allegro.
|
||||||
|
- Dodano migracje `20260308_000042_ensure_shoppro_payment_sync_schedule_and_columns.sql`:
|
||||||
|
- uzupelnienie kolumny `integrations.payment_sync_status_codes_json`,
|
||||||
|
- seed/naprawa harmonogramu `shoppro_payment_status_sync` (domyslnie 600s, priorytet 105).
|
||||||
|
- Fix danych wysylki dla zamowien shopPRO (np. `OP000000016`):
|
||||||
|
- `ShopproOrdersSyncService` mapuje `inpost_paczkomat`/`orlen_point` do adresu `delivery` (punkt, ulica, kod, miasto),
|
||||||
|
- zapisuje `parcel_external_id` i `parcel_name` dla punktu odbioru,
|
||||||
|
- `delivery` dziedziczy telefon i e-mail klienta, gdy API nie zwraca osobnych danych odbiorcy,
|
||||||
|
- etykieta metody dostawy (`external_carrier_id`) zawiera koszt transportu (`transport_cost`), np. `Paczkomaty InPost - przedpłata: 13.5 zł`.
|
||||||
|
- Fix importu shopPRO dla listy zamowien (`Kwoty` + miniatury):
|
||||||
|
- `ShopproOrdersSyncService` mapuje kwoty zamowienia z `summary` i `paid` (fallback), ceny pozycji z `price_brutto`,
|
||||||
|
- poprawiono laczenie payloadow `orders/list` i `orders/get|details` (zachowanie kluczowych pol z listy),
|
||||||
|
- dodano fallback miniatur pozycji przez API `products/get` po `product_id`,
|
||||||
|
- dodano dodatkowy fallback miniatur po `parent_product_id` (warianty), gdy obraz nie istnieje na produkcie potomnym.
|
||||||
|
- `OrdersRepository`:
|
||||||
|
- resolver miniatur pozycji uwzglednia kod kanalu zgodny ze zrodlem zamowienia (`o.source`) zamiast stalego `allegro`.
|
||||||
|
- Korekta layoutu sekcji `Ustawienia` dla integracji `shopPRO`:
|
||||||
|
- wyrownano pola w siatce (`integration-settings-group__grid`) przez `align-items: start`,
|
||||||
|
- wymuszono spojna wysokosc kontrolek (`.form-control`), w tym pola `date`,
|
||||||
|
- przebudowano CSS (`public/assets/css/app.css`) dla rownego przebiegu linii i pol w obu kolumnach.
|
||||||
|
- Fix UX przycisku `Nowa integracja` w `Ustawienia > Integracje > shopPRO`:
|
||||||
|
- przycisk otwiera teraz tryb wymuszonego tworzenia (`?new=1`),
|
||||||
|
- `ShopproIntegrationsController::index(...)` nie auto-wybiera wtedy pierwszej istniejacej integracji,
|
||||||
|
- formularz tworzenia otwiera sie zawsze jako pusty.
|
||||||
|
- Poprawiono prezentacje dostawy na szczegolach zamowienia (`orders/show`):
|
||||||
|
- `Platnosc i wysylka` sanitizuje nazwe przewoznika (usuwa tagi HTML typu `<b>...</b>`),
|
||||||
|
- `Dane wysylki` pokazuja `parcel_name` i `parcel_external_id` (np. punkt/paczkomat Allegro),
|
||||||
|
- gdy brak adresu `delivery`, sekcja `Dane wysylki` pokazuje fallback z metody dostawy (`external_carrier_id`).
|
||||||
|
- Poprawiono import shopPRO dla formy dostawy:
|
||||||
|
- `ShopproOrdersSyncService` sanitizuje `external_carrier_id` i `order_shipments.carrier_provider_id` (usuwanie HTML + dekodowanie encji),
|
||||||
|
- rozszerzono fallbacki mapowania przewoznika (`transport`, `transport_description`, `transport_id`).
|
||||||
|
- Fix mapowania formy dostawy z shopPRO:
|
||||||
|
- `ShopproOrdersSyncService` mapuje teraz `orders.external_carrier_id` z fallbackiem na pola `transport` i `transport_description`,
|
||||||
|
- `orders.external_carrier_account_id` mapowane z `transport_id`,
|
||||||
|
- `order_shipments.carrier_provider_id` rozszerzone o fallback `transport`/`transport_description`.
|
||||||
|
- Fix importu adresow shopPRO na realnym payloadzie zamowien (`client_name`, `client_surname`, `client_email`, `client_phone`):
|
||||||
|
- `ShopproOrdersSyncService::mapAddresses(...)` mapuje teraz pola klienta w formacie flat (bez zagniezdzenia),
|
||||||
|
- usunieto przypadek tworzenia pustego adresu `delivery` tylko na bazie fallbacku e-mail,
|
||||||
|
- po wymuszonym re-sync zamowienia #13 dane zamawiajacego zapisaly sie jako `Jacek Pyziak`, `pyziak84@gmail.com`, `530755774`.
|
||||||
|
- Poprawiono mapowanie danych adresowych w imporcie zamowien `shopPRO`:
|
||||||
|
- `ShopproOrdersSyncService::mapAddresses(...)` obsluguje rozszerzony zestaw aliasow pol klienta i dostawy (`buyer/customer/client`, `billing_address`, `shipping_address`, `delivery_address`, `receiver`, warianty `first_name/last_name`, `postcode` itd.),
|
||||||
|
- adres dostawy jest zapisywany takze wtedy, gdy brak pelnej nazwy, ale istnieja inne dane adresowe,
|
||||||
|
- rozszerzono mapowanie `orders.customer_login` i `orders.external_carrier_id` o dodatkowe fallbacki z payloadu shopPRO.
|
||||||
|
- Poprawiono UX zakladki `Ustawienia` integracji `shopPRO`:
|
||||||
|
- ustawienia sa pogrupowane w sekcje `Pobieranie zamowien` oraz `Synchronizacja statusow`,
|
||||||
|
- dodano naglowki sekcji i opisy kontekstu, aby pola nie zlewaly sie wizualnie,
|
||||||
|
- dodano dedykowane style SCSS (`integration-settings-group*`) i przebudowano `public/assets/css/app.css`.
|
||||||
|
- Dodano cron synchronizacji statusow `shopPRO`:
|
||||||
|
- nowy handler `App\Modules\Cron\ShopproStatusSyncHandler`,
|
||||||
|
- nowy serwis `App\Modules\Settings\ShopproStatusSyncService`,
|
||||||
|
- nowy job `shoppro_order_status_sync` podlaczony do runnera w `App\Core\Application::maybeRunCronOnWeb(...)` i `bin/cron.php`.
|
||||||
|
- Rozszerzono `Ustawienia > Integracje > shopPRO > Ustawienia`:
|
||||||
|
- dodano wybor kierunku synchronizacji statusow (`shopPRO -> orderPRO`, `orderPRO -> shopPRO`),
|
||||||
|
- dodano pole interwalu synchronizacji statusow (minuty),
|
||||||
|
- zapis aktualizuje `integrations.order_status_sync_direction` i harmonogram `cron_schedules` dla `shoppro_order_status_sync`.
|
||||||
|
- Dodano migracje `20260308_000041_ensure_shoppro_status_sync_schedule_and_direction.sql`:
|
||||||
|
- seed/naprawa harmonogramu `shoppro_order_status_sync` (domyslnie 900s, priorytet 100),
|
||||||
|
- uzupelnienie kolumny `integrations.order_status_sync_direction` jesli brak.
|
||||||
|
- Rozszerzono `ShopproOrdersSyncService` o opcje uruchomienia filtrowanego po `integration_id` i z pominięciem flagi `orders_fetch_enabled` (wykorzystane przez cron synchronizacji statusow).
|
||||||
|
- Dodano cron importu zamowien z `shopPRO`:
|
||||||
|
- nowy handler `App\Modules\Cron\ShopproOrdersImportHandler`,
|
||||||
|
- nowy serwis `App\Modules\Settings\ShopproOrdersSyncService`,
|
||||||
|
- nowy klient API `App\Modules\Settings\ShopproApiClient`,
|
||||||
|
- job `shoppro_orders_import` jest podlaczony do wykonania zarowno w `App\Core\Application::maybeRunCronOnWeb(...)`, jak i w `bin/cron.php`.
|
||||||
|
- Rozszerzono `Ustawienia > Integracje > shopPRO > Ustawienia`:
|
||||||
|
- dodano pole interwalu pobierania zamowien (minuty),
|
||||||
|
- zapis aktualizuje harmonogram `cron_schedules` dla joba `shoppro_orders_import`.
|
||||||
|
- Dodano migracje `20260308_000040_ensure_shoppro_orders_import_schedule.sql`:
|
||||||
|
- seed/naprawa harmonogramu `shoppro_orders_import` (domyslnie 300s, priorytet 90).
|
||||||
|
- Zadanie #13 z `DOCS/todo.md`: dodano stronicowanie historii w `Ustawienia > Cron`:
|
||||||
|
- `CronSettingsController` pobiera `past_page` z query i przekazuje metadane paginacji do widoku,
|
||||||
|
- `CronRepository` rozszerzono o `countPastJobs()` oraz `listPastJobs(limit, offset)`,
|
||||||
|
- widok `settings/cron.php` renderuje kontrolki paginacji dla sekcji `Historia jobow (przeszle)`.
|
||||||
|
- Rozpoczeto ujednolicanie modelu integracji na baze `integrations`:
|
||||||
|
- dodano klasy wspolne `App\Modules\Settings\IntegrationsRepository` oraz `App\Modules\Settings\IntegrationSecretCipher`,
|
||||||
|
- ograniczono duplikacje szyfrowania sekretow integracji (wspolny cipher dla repozytoriow integracji).
|
||||||
|
- Migracja `20260308_000037_unify_integrations_base_links.sql`:
|
||||||
|
- dodaje `integration_id` do `allegro_integration_settings`, `apaczka_integration_settings`, `inpost_integration_settings`,
|
||||||
|
- podpina relacje 1:1 FK do `integrations`,
|
||||||
|
- seeduje bazowe rekordy providerow (`allegro`, `apaczka`, `inpost`) i backfilluje powiazania.
|
||||||
|
- `ApaczkaIntegrationRepository`:
|
||||||
|
- klucz API zapisuje/odczytuje z `integrations.api_key_encrypted` (`type=apaczka`).
|
||||||
|
- `InpostIntegrationRepository`:
|
||||||
|
- token API zapisuje/odczytuje z `integrations.api_key_encrypted` (`type=inpost`),
|
||||||
|
- ustawienia specyficzne przewoznika pozostaja w `inpost_integration_settings`.
|
||||||
|
- `AllegroIntegrationRepository`:
|
||||||
|
- zapewnia powiazanie aktywnego srodowiska OAuth z rekordem bazowym `integrations`,
|
||||||
|
- dodana metoda `getActiveIntegrationId()` pod spojnosc domeny zamowien/sync.
|
||||||
|
- `AllegroOrderImportService` i `AllegroOrdersSyncService`:
|
||||||
|
- przestaly uzywac stalej/`null` dla `integration_id`,
|
||||||
|
- korzystaja z aktywnego `integration_id` Allegro, co eliminuje sztywne zalozenie `integration_id=1`.
|
||||||
|
- Dodano wspolny ekran `Ustawienia > Integracje`:
|
||||||
|
- nowa route `GET /settings/integrations`,
|
||||||
|
- nowa klasa `App\Modules\Settings\IntegrationsHubController`,
|
||||||
|
- nawigacja boczna prowadzi do jednego huba integracji.
|
||||||
|
- Hub integracji zawiera tabele podsumowania oraz akcje per instancja:
|
||||||
|
- przycisk `Ustawienia` w kazdym wierszu prowadzi do dedykowanego ekranu zaawansowanego providera.
|
||||||
|
- Fix UI: rozciaganie przyciskow w formularzach (`.form-actions`) przy ukladzie grid:
|
||||||
|
- ustawiono `align-items: flex-start` oraz `align-self: flex-start` dla `.form-actions .btn`,
|
||||||
|
- eliminuje pionowe rozciaganie przyciskow do wysokosci sasiednich pol formularza.
|
||||||
|
- Dodano wieloinstancyjna konfiguracje integracji `shopPRO`:
|
||||||
|
- nowe endpointy: `GET /settings/integrations/shoppro`, `POST /settings/integrations/shoppro/save`, `POST /settings/integrations/shoppro/test`,
|
||||||
|
- nowa klasa `App\Modules\Settings\ShopproIntegrationsController`,
|
||||||
|
- nowy widok `resources/views/settings/shoppro.php` (lista instancji, formularz dodawania/edycji, test polaczenia),
|
||||||
|
- hub integracji (`/settings/integrations`) zawiera wiersz `shopPRO` z przejsciem do ekranu ustawien,
|
||||||
|
- dodano pomocnicze style `.table-row-actions` dla kompaktowych akcji w tabelach.
|
||||||
|
- Ekran `shopPRO` rozbudowano o zakladki analogiczne do Allegro:
|
||||||
|
- `Integracja`, `Statusy`, `Ustawienia`, `Formy dostawy`,
|
||||||
|
- `Ustawienia` zawiera pola `Pobieraj zamowienia` i `Data startu pobierania`,
|
||||||
|
- zapis/odczyt tych pol jest realizowany przez tabele bazowa `integrations` (`orders_fetch_enabled`, `orders_fetch_start_date`).
|
||||||
|
- Wdrozone mapowanie statusow shopPRO (zakladka `Statusy`):
|
||||||
|
- nowe endpointy: `POST /settings/integrations/shoppro/statuses/sync` oraz `POST /settings/integrations/shoppro/statuses/save`,
|
||||||
|
- dodana klasa `App\Modules\Settings\ShopproStatusMappingRepository`,
|
||||||
|
- synchronizacja statusow pobiera slownik `dictionaries/statuses` z API shopPRO,
|
||||||
|
- zapis mapowan trafia do `order_status_mappings` per `integration_id` (wieloinstancyjnie).
|
||||||
|
- Poprawiono parser statusow shopPRO:
|
||||||
|
- obsluguje odpowiedzi zagniezdzone w `data`,
|
||||||
|
- obsluguje rowniez format mapy `kod => nazwa` oraz dodatkowe aliasy pol (`status_code`, `status_name`, `symbol`, `slug`).
|
||||||
|
- Dodano migracje naprawcza `20260308_000038_ensure_order_status_mappings_table.sql`:
|
||||||
|
- tworzy `order_status_mappings` jesli tabela nie istnieje (scenariusz niepelnej historii migracji na srodowisku).
|
||||||
|
- Dodano migracje naprawcza `20260308_000039_ensure_integrations_fetch_columns.sql`:
|
||||||
|
- uzupelnia w `integrations` brakujace kolumny `orders_fetch_enabled` i `orders_fetch_start_date`
|
||||||
|
dla srodowisk, gdzie tabela `integrations` zostala odtworzona pozniej niz pierwotne migracje shopPRO.
|
||||||
|
- Poprawiono UX ekranu `shopPRO`:
|
||||||
|
- przy istniejacych instancjach automatycznie wybierana jest pierwsza integracja (bez koniecznosci wracania do zakladki `Integracja`),
|
||||||
|
- dodano przełącznik instancji nad zakladkami (`Wybrana integracja`) dostepny globalnie dla `Statusy/Ustawienia/Formy dostawy`.
|
||||||
|
|
||||||
## 2026-03-06
|
## 2026-03-06
|
||||||
- Fix: synchronizacja statusow Allegro nie aktualizowala zamowien.
|
- Fix: synchronizacja statusow Allegro nie aktualizowala zamowien.
|
||||||
- Przyczyna: Allegro API nie zmienia `updatedAt` przy zmianie `fulfillment.status`.
|
- Przyczyna: Allegro API nie zmienia `updatedAt` przy zmianie `fulfillment.status`.
|
||||||
|
|||||||
32
DOCS/todo.md
32
DOCS/todo.md
@@ -1,15 +1,17 @@
|
|||||||
1. [x] Na liście zamówień powiększenie zdjęcia produktu na hover nie na onclick, wtedy to nie może być modal zamykany X
|
1. [x] Na liście zamówień powiększenie zdjęcia produktu na hover nie na onclick, wtedy to nie może być modal zamykany X
|
||||||
2. [x] Dodać rejestrację historii zamówień, i zmiana statusu rejestrowana w Historii zmian zamĂłwienia
|
2. [x] Doda<EFBFBD> rejestracj<EFBFBD> historii zam<EFBFBD>wie<EFBFBD>, i zmiana statusu rejestrowana w Historii zmian zamówienia
|
||||||
3. [x] Pobranie zamĂłwienia rejestrowane w histori zmian zamĂłwienia
|
3. [x] Pobranie zamówienia rejestrowane w histori zmian zamówienia
|
||||||
4. [x] Przy imporcie zamówień musi być pobierania forma wysyłki.
|
4. [x] Przy imporcie zamówień musi być pobierania forma wysyłki.
|
||||||
5. [x] W szczególach zamówienia dorobić opcję zmiany statusu.
|
5. [x] W szczególach zamówienia dorobić opcję zmiany statusu.
|
||||||
6. [x] W szczególach zamówienia 2 razy wyświetla się ID zamówienai z allegro, np: 008d3d60-1743-11f1-b15c-fdb4f87ccfc6
|
6. [x] W szczeg<EFBFBD>lach zam<EFBFBD>wienia 2 razy wy<EFBFBD>wietla si<EFBFBD> ID zam<EFBFBD>wienai z allegro, np: 008d3d60-1743-11f1-b15c-fdb4f87ccfc6
|
||||||
7. [x] Przy imporcie z allegro liczba przesyłek jest 0.
|
7. [x] Przy imporcie z allegro liczba przesy<EFBFBD>ek jest 0.
|
||||||
8. [x] Kolumna LP w szczególach zamówienia jest zbyt szeroka.
|
8. [x] Kolumna LP w szczeg<EFBFBD>lach zam<EFBFBD>wienia jest zbyt szeroka.
|
||||||
9. [x] Na lisćie zamówień pole po którym jest domyślnie sortowana czyli data zamówienia jest puste.
|
9. [x] Na lis<EFBFBD>ie zam<EFBFBD>wie<EFBFBD> pole po kt<EFBFBD>rym jest domy<EFBFBD>lnie sortowana czyli data zam<EFBFBD>wienia jest puste.
|
||||||
10. [x] Na liście zamówień ukryć kolumnę ostatnia zmiana.
|
10. [x] Na li<EFBFBD>cie zam<EFBFBD>wie<EFBFBD> ukry<EFBFBD> kolumn<EFBFBD> ostatnia zmiana.
|
||||||
11. [x] W ustawieniach dodać zakładkę Integracja Apaczka. Dodać tam pierwsze ustawienie, czyli klucz API.
|
11. [x] W ustawieniach doda<EFBFBD> zak<EFBFBD>adk<EFBFBD> Integracja Apaczka. Doda<EFBFBD> tam pierwsze ustawienie, czyli klucz API.
|
||||||
12. [] synchronizować ręczną zmianę statusu z allegro
|
12. [] synchronizowa<EFBFBD> r<EFBFBD>czn<EFBFBD> zmian<EFBFBD> statusu z allegro
|
||||||
13. [] W ustawieniach cron https://orderpro.projectpro.pl/settings/cron historia powinna mieć stronicowanie
|
13. [x] W ustawieniach cron https://orderpro.projectpro.pl/settings/cron historia powinna mie<EFBFBD> stronicowanie
|
||||||
14. [] border inputów, select, textarea, itd zrób troszkę ciemniejszy
|
14. [] border input<EFBFBD>w, select, textarea, itd zr<EFBFBD>b troszk<EFBFBD> ciemniejszy
|
||||||
|
15. [] W tym miejscu odwróć kolejność: najpierw źródło potem ID, <div class="orders-ref__meta"><span>f6079660-1af8-11f1-a7c9-231cf6ef29d1</span><span>allegro</span></div>
|
||||||
|
16. [] Na liście zamówień statusy powinno być pokolorowane zgodnie z ustawieniami.
|
||||||
|
17. [] Na liście zamówien jak jest źródło i id zamówienia to zamiast shopPRO musi pisać która integracja konkretnie. Oraz dodajemy napis ID: ...D
|
||||||
|
|||||||
48
bin/cron.php
48
bin/cron.php
@@ -7,6 +7,9 @@ use App\Modules\Cron\AllegroStatusSyncHandler;
|
|||||||
use App\Modules\Cron\AllegroTokenRefreshHandler;
|
use App\Modules\Cron\AllegroTokenRefreshHandler;
|
||||||
use App\Modules\Cron\CronRepository;
|
use App\Modules\Cron\CronRepository;
|
||||||
use App\Modules\Cron\CronRunner;
|
use App\Modules\Cron\CronRunner;
|
||||||
|
use App\Modules\Cron\ShopproOrdersImportHandler;
|
||||||
|
use App\Modules\Cron\ShopproPaymentStatusSyncHandler;
|
||||||
|
use App\Modules\Cron\ShopproStatusSyncHandler;
|
||||||
use App\Modules\Orders\OrderImportRepository;
|
use App\Modules\Orders\OrderImportRepository;
|
||||||
use App\Modules\Orders\OrdersRepository;
|
use App\Modules\Orders\OrdersRepository;
|
||||||
use App\Modules\Settings\AllegroApiClient;
|
use App\Modules\Settings\AllegroApiClient;
|
||||||
@@ -17,6 +20,12 @@ use App\Modules\Settings\AllegroOrderSyncStateRepository;
|
|||||||
use App\Modules\Settings\AllegroOAuthClient;
|
use App\Modules\Settings\AllegroOAuthClient;
|
||||||
use App\Modules\Settings\AllegroStatusSyncService;
|
use App\Modules\Settings\AllegroStatusSyncService;
|
||||||
use App\Modules\Settings\AllegroStatusMappingRepository;
|
use App\Modules\Settings\AllegroStatusMappingRepository;
|
||||||
|
use App\Modules\Settings\ShopproApiClient;
|
||||||
|
use App\Modules\Settings\ShopproIntegrationsRepository;
|
||||||
|
use App\Modules\Settings\ShopproOrdersSyncService;
|
||||||
|
use App\Modules\Settings\ShopproPaymentStatusSyncService;
|
||||||
|
use App\Modules\Settings\ShopproStatusSyncService;
|
||||||
|
use App\Modules\Settings\ShopproStatusMappingRepository;
|
||||||
|
|
||||||
/** @var Application $app */
|
/** @var Application $app */
|
||||||
$app = require dirname(__DIR__) . '/bootstrap/app.php';
|
$app = require dirname(__DIR__) . '/bootstrap/app.php';
|
||||||
@@ -51,6 +60,33 @@ $ordersSyncService = new AllegroOrdersSyncService(
|
|||||||
$apiClient,
|
$apiClient,
|
||||||
$orderImportService
|
$orderImportService
|
||||||
);
|
);
|
||||||
|
$shopproSyncService = new ShopproOrdersSyncService(
|
||||||
|
new ShopproIntegrationsRepository(
|
||||||
|
$app->db(),
|
||||||
|
(string) $app->config('app.integrations.secret', '')
|
||||||
|
),
|
||||||
|
new AllegroOrderSyncStateRepository($app->db()),
|
||||||
|
new ShopproApiClient(),
|
||||||
|
new OrderImportRepository($app->db()),
|
||||||
|
new ShopproStatusMappingRepository($app->db()),
|
||||||
|
new OrdersRepository($app->db())
|
||||||
|
);
|
||||||
|
$shopproStatusSyncService = new ShopproStatusSyncService(
|
||||||
|
new ShopproIntegrationsRepository(
|
||||||
|
$app->db(),
|
||||||
|
(string) $app->config('app.integrations.secret', '')
|
||||||
|
),
|
||||||
|
$shopproSyncService
|
||||||
|
);
|
||||||
|
$shopproPaymentSyncService = new ShopproPaymentStatusSyncService(
|
||||||
|
new ShopproIntegrationsRepository(
|
||||||
|
$app->db(),
|
||||||
|
(string) $app->config('app.integrations.secret', '')
|
||||||
|
),
|
||||||
|
new ShopproApiClient(),
|
||||||
|
new OrdersRepository($app->db()),
|
||||||
|
$app->db()
|
||||||
|
);
|
||||||
|
|
||||||
$runner = new CronRunner(
|
$runner = new CronRunner(
|
||||||
$cronRepository,
|
$cronRepository,
|
||||||
@@ -66,9 +102,19 @@ $runner = new CronRunner(
|
|||||||
'allegro_status_sync' => new AllegroStatusSyncHandler(
|
'allegro_status_sync' => new AllegroStatusSyncHandler(
|
||||||
new AllegroStatusSyncService(
|
new AllegroStatusSyncService(
|
||||||
$cronRepository,
|
$cronRepository,
|
||||||
$ordersSyncService
|
$orderImportService,
|
||||||
|
$app->db()
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
'shoppro_orders_import' => new ShopproOrdersImportHandler(
|
||||||
|
$shopproSyncService
|
||||||
|
),
|
||||||
|
'shoppro_order_status_sync' => new ShopproStatusSyncHandler(
|
||||||
|
$shopproStatusSyncService
|
||||||
|
),
|
||||||
|
'shoppro_payment_status_sync' => new ShopproPaymentStatusSyncHandler(
|
||||||
|
$shopproPaymentSyncService
|
||||||
|
),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS integrations (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
type VARCHAR(32) NOT NULL,
|
||||||
|
name VARCHAR(128) NOT NULL,
|
||||||
|
base_url VARCHAR(255) NOT NULL,
|
||||||
|
api_key_encrypted TEXT NULL,
|
||||||
|
timeout_seconds SMALLINT UNSIGNED NOT NULL DEFAULT 10,
|
||||||
|
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
last_test_status VARCHAR(16) NULL,
|
||||||
|
last_test_http_code SMALLINT UNSIGNED NULL,
|
||||||
|
last_test_message VARCHAR(255) NULL,
|
||||||
|
last_test_at DATETIME NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY integrations_type_name_unique (type, name),
|
||||||
|
KEY integrations_type_active_idx (type, is_active),
|
||||||
|
KEY integrations_last_test_at_idx (last_test_at)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS integration_test_logs (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
integration_id INT UNSIGNED NOT NULL,
|
||||||
|
status VARCHAR(16) NOT NULL,
|
||||||
|
http_code SMALLINT UNSIGNED NULL,
|
||||||
|
message VARCHAR(255) NOT NULL,
|
||||||
|
endpoint_url VARCHAR(255) NULL,
|
||||||
|
tested_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
KEY integration_test_logs_integration_idx (integration_id),
|
||||||
|
KEY integration_test_logs_tested_at_idx (tested_at),
|
||||||
|
CONSTRAINT integration_test_logs_integration_fk
|
||||||
|
FOREIGN KEY (integration_id) REFERENCES integrations(id)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `allegro_integration_settings`
|
||||||
|
ADD COLUMN IF NOT EXISTS `integration_id` INT UNSIGNED NULL AFTER `id`;
|
||||||
|
|
||||||
|
ALTER TABLE `apaczka_integration_settings`
|
||||||
|
ADD COLUMN IF NOT EXISTS `integration_id` INT UNSIGNED NULL AFTER `id`;
|
||||||
|
|
||||||
|
ALTER TABLE `inpost_integration_settings`
|
||||||
|
ADD COLUMN IF NOT EXISTS `integration_id` INT UNSIGNED NULL AFTER `id`;
|
||||||
|
|
||||||
|
INSERT INTO `integrations` (`type`, `name`, `base_url`, `timeout_seconds`, `is_active`, `created_at`, `updated_at`)
|
||||||
|
VALUES
|
||||||
|
('allegro', 'Allegro Sandbox', 'https://api.allegro.pl.allegrosandbox.pl', 20, 1, NOW(), NOW()),
|
||||||
|
('allegro', 'Allegro Production', 'https://api.allegro.pl', 20, 1, NOW(), NOW()),
|
||||||
|
('apaczka', 'Apaczka', 'https://www.apaczka.pl', 20, 1, NOW(), NOW()),
|
||||||
|
('inpost', 'InPost ShipX', 'https://api-shipx-pl.easypack24.net', 20, 1, NOW(), NOW())
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
`updated_at` = VALUES(`updated_at`);
|
||||||
|
|
||||||
|
UPDATE `allegro_integration_settings` `s`
|
||||||
|
INNER JOIN `integrations` `i`
|
||||||
|
ON `i`.`type` = 'allegro'
|
||||||
|
AND `i`.`name` = CASE
|
||||||
|
WHEN `s`.`environment` = 'production' THEN 'Allegro Production'
|
||||||
|
ELSE 'Allegro Sandbox'
|
||||||
|
END
|
||||||
|
SET `s`.`integration_id` = `i`.`id`
|
||||||
|
WHERE `s`.`integration_id` IS NULL OR `s`.`integration_id` = 0;
|
||||||
|
|
||||||
|
UPDATE `apaczka_integration_settings` `s`
|
||||||
|
INNER JOIN `integrations` `i`
|
||||||
|
ON `i`.`type` = 'apaczka'
|
||||||
|
AND `i`.`name` = 'Apaczka'
|
||||||
|
SET `s`.`integration_id` = `i`.`id`
|
||||||
|
WHERE `s`.`id` = 1
|
||||||
|
AND (`s`.`integration_id` IS NULL OR `s`.`integration_id` = 0);
|
||||||
|
|
||||||
|
UPDATE `inpost_integration_settings` `s`
|
||||||
|
INNER JOIN `integrations` `i`
|
||||||
|
ON `i`.`type` = 'inpost'
|
||||||
|
AND `i`.`name` = 'InPost ShipX'
|
||||||
|
SET `s`.`integration_id` = `i`.`id`
|
||||||
|
WHERE `s`.`id` = 1
|
||||||
|
AND (`s`.`integration_id` IS NULL OR `s`.`integration_id` = 0);
|
||||||
|
|
||||||
|
UPDATE `integrations` `i`
|
||||||
|
INNER JOIN `apaczka_integration_settings` `s`
|
||||||
|
ON `s`.`integration_id` = `i`.`id`
|
||||||
|
SET `i`.`api_key_encrypted` = `s`.`api_key_encrypted`,
|
||||||
|
`i`.`updated_at` = NOW()
|
||||||
|
WHERE `i`.`type` = 'apaczka'
|
||||||
|
AND (`i`.`api_key_encrypted` IS NULL OR `i`.`api_key_encrypted` = '')
|
||||||
|
AND `s`.`api_key_encrypted` IS NOT NULL
|
||||||
|
AND `s`.`api_key_encrypted` <> '';
|
||||||
|
|
||||||
|
UPDATE `integrations` `i`
|
||||||
|
INNER JOIN `inpost_integration_settings` `s`
|
||||||
|
ON `s`.`integration_id` = `i`.`id`
|
||||||
|
SET `i`.`api_key_encrypted` = `s`.`api_token_encrypted`,
|
||||||
|
`i`.`updated_at` = NOW()
|
||||||
|
WHERE `i`.`type` = 'inpost'
|
||||||
|
AND (`i`.`api_key_encrypted` IS NULL OR `i`.`api_key_encrypted` = '')
|
||||||
|
AND `s`.`api_token_encrypted` IS NOT NULL
|
||||||
|
AND `s`.`api_token_encrypted` <> '';
|
||||||
|
|
||||||
|
SET @sql := (
|
||||||
|
SELECT IF(COUNT(*) = 0,
|
||||||
|
'ALTER TABLE `allegro_integration_settings` ADD UNIQUE KEY `allegro_integration_settings_integration_unique` (`integration_id`)',
|
||||||
|
'SELECT 1')
|
||||||
|
FROM information_schema.statistics
|
||||||
|
WHERE table_schema = DATABASE()
|
||||||
|
AND table_name = 'allegro_integration_settings'
|
||||||
|
AND index_name = 'allegro_integration_settings_integration_unique'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql := (
|
||||||
|
SELECT IF(COUNT(*) = 0,
|
||||||
|
'ALTER TABLE `apaczka_integration_settings` ADD UNIQUE KEY `apaczka_integration_settings_integration_unique` (`integration_id`)',
|
||||||
|
'SELECT 1')
|
||||||
|
FROM information_schema.statistics
|
||||||
|
WHERE table_schema = DATABASE()
|
||||||
|
AND table_name = 'apaczka_integration_settings'
|
||||||
|
AND index_name = 'apaczka_integration_settings_integration_unique'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql := (
|
||||||
|
SELECT IF(COUNT(*) = 0,
|
||||||
|
'ALTER TABLE `inpost_integration_settings` ADD UNIQUE KEY `inpost_integration_settings_integration_unique` (`integration_id`)',
|
||||||
|
'SELECT 1')
|
||||||
|
FROM information_schema.statistics
|
||||||
|
WHERE table_schema = DATABASE()
|
||||||
|
AND table_name = 'inpost_integration_settings'
|
||||||
|
AND index_name = 'inpost_integration_settings_integration_unique'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql := (
|
||||||
|
SELECT IF(COUNT(*) = 0,
|
||||||
|
'ALTER TABLE `allegro_integration_settings` ADD CONSTRAINT `allegro_integration_settings_integration_fk` FOREIGN KEY (`integration_id`) REFERENCES `integrations`(`id`) ON DELETE CASCADE ON UPDATE CASCADE',
|
||||||
|
'SELECT 1')
|
||||||
|
FROM information_schema.table_constraints
|
||||||
|
WHERE constraint_schema = DATABASE()
|
||||||
|
AND table_name = 'allegro_integration_settings'
|
||||||
|
AND constraint_name = 'allegro_integration_settings_integration_fk'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql := (
|
||||||
|
SELECT IF(COUNT(*) = 0,
|
||||||
|
'ALTER TABLE `apaczka_integration_settings` ADD CONSTRAINT `apaczka_integration_settings_integration_fk` FOREIGN KEY (`integration_id`) REFERENCES `integrations`(`id`) ON DELETE CASCADE ON UPDATE CASCADE',
|
||||||
|
'SELECT 1')
|
||||||
|
FROM information_schema.table_constraints
|
||||||
|
WHERE constraint_schema = DATABASE()
|
||||||
|
AND table_name = 'apaczka_integration_settings'
|
||||||
|
AND constraint_name = 'apaczka_integration_settings_integration_fk'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql := (
|
||||||
|
SELECT IF(COUNT(*) = 0,
|
||||||
|
'ALTER TABLE `inpost_integration_settings` ADD CONSTRAINT `inpost_integration_settings_integration_fk` FOREIGN KEY (`integration_id`) REFERENCES `integrations`(`id`) ON DELETE CASCADE ON UPDATE CASCADE',
|
||||||
|
'SELECT 1')
|
||||||
|
FROM information_schema.table_constraints
|
||||||
|
WHERE constraint_schema = DATABASE()
|
||||||
|
AND table_name = 'inpost_integration_settings'
|
||||||
|
AND constraint_name = 'inpost_integration_settings_integration_fk'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS order_status_mappings (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
integration_id INT UNSIGNED NOT NULL,
|
||||||
|
shoppro_status_code VARCHAR(64) NOT NULL,
|
||||||
|
shoppro_status_name VARCHAR(128) NULL,
|
||||||
|
orderpro_status_code VARCHAR(64) NOT NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY order_status_mappings_integration_shoppro_unique (integration_id, shoppro_status_code),
|
||||||
|
KEY order_status_mappings_integration_idx (integration_id),
|
||||||
|
KEY order_status_mappings_orderpro_idx (orderpro_status_code),
|
||||||
|
CONSTRAINT order_status_mappings_integration_fk
|
||||||
|
FOREIGN KEY (integration_id) REFERENCES integrations(id)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
SET @sql := (
|
||||||
|
SELECT IF(COUNT(*) = 0,
|
||||||
|
'ALTER TABLE `integrations` ADD COLUMN `orders_fetch_enabled` TINYINT(1) NOT NULL DEFAULT 0 AFTER `is_active`',
|
||||||
|
'SELECT 1')
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = DATABASE()
|
||||||
|
AND table_name = 'integrations'
|
||||||
|
AND column_name = 'orders_fetch_enabled'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql := (
|
||||||
|
SELECT IF(COUNT(*) = 0,
|
||||||
|
'ALTER TABLE `integrations` ADD COLUMN `orders_fetch_start_date` DATE NULL AFTER `orders_fetch_enabled`',
|
||||||
|
'SELECT 1')
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = DATABASE()
|
||||||
|
AND table_name = 'integrations'
|
||||||
|
AND column_name = 'orders_fetch_start_date'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
INSERT INTO cron_schedules (
|
||||||
|
job_type, interval_seconds, priority, max_attempts, payload, enabled, last_run_at, next_run_at, created_at, updated_at
|
||||||
|
) VALUES (
|
||||||
|
'shoppro_orders_import',
|
||||||
|
300,
|
||||||
|
90,
|
||||||
|
3,
|
||||||
|
NULL,
|
||||||
|
1,
|
||||||
|
NULL,
|
||||||
|
NOW(),
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
interval_seconds = IFNULL(interval_seconds, VALUES(interval_seconds)),
|
||||||
|
priority = IFNULL(priority, VALUES(priority)),
|
||||||
|
max_attempts = IFNULL(max_attempts, VALUES(max_attempts)),
|
||||||
|
updated_at = VALUES(updated_at);
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
SET @sql := (
|
||||||
|
SELECT IF(COUNT(*) = 0,
|
||||||
|
'ALTER TABLE `integrations` ADD COLUMN `order_status_sync_direction` VARCHAR(32) NOT NULL DEFAULT ''shoppro_to_orderpro'' AFTER `orders_fetch_start_date`',
|
||||||
|
'SELECT 1')
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = DATABASE()
|
||||||
|
AND table_name = 'integrations'
|
||||||
|
AND column_name = 'order_status_sync_direction'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
INSERT INTO cron_schedules (
|
||||||
|
job_type, interval_seconds, priority, max_attempts, payload, enabled, last_run_at, next_run_at, created_at, updated_at
|
||||||
|
) VALUES (
|
||||||
|
'shoppro_order_status_sync',
|
||||||
|
900,
|
||||||
|
100,
|
||||||
|
3,
|
||||||
|
NULL,
|
||||||
|
1,
|
||||||
|
NULL,
|
||||||
|
NOW(),
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
interval_seconds = IFNULL(interval_seconds, VALUES(interval_seconds)),
|
||||||
|
priority = IFNULL(priority, VALUES(priority)),
|
||||||
|
max_attempts = IFNULL(max_attempts, VALUES(max_attempts)),
|
||||||
|
updated_at = VALUES(updated_at);
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
SET @sql := (
|
||||||
|
SELECT IF(COUNT(*) = 0,
|
||||||
|
'ALTER TABLE `integrations` ADD COLUMN `payment_sync_status_codes_json` JSON NULL AFTER `order_status_sync_direction`',
|
||||||
|
'SELECT 1')
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = DATABASE()
|
||||||
|
AND table_name = 'integrations'
|
||||||
|
AND column_name = 'payment_sync_status_codes_json'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
INSERT INTO cron_schedules (
|
||||||
|
job_type, interval_seconds, priority, max_attempts, payload, enabled, last_run_at, next_run_at, created_at, updated_at
|
||||||
|
) VALUES (
|
||||||
|
'shoppro_payment_status_sync',
|
||||||
|
600,
|
||||||
|
105,
|
||||||
|
3,
|
||||||
|
NULL,
|
||||||
|
1,
|
||||||
|
NULL,
|
||||||
|
NOW(),
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
interval_seconds = IFNULL(interval_seconds, VALUES(interval_seconds)),
|
||||||
|
priority = IFNULL(priority, VALUES(priority)),
|
||||||
|
max_attempts = IFNULL(max_attempts, VALUES(max_attempts)),
|
||||||
|
updated_at = VALUES(updated_at);
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS shoppro_delivery_method_mappings (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
integration_id INT UNSIGNED NOT NULL,
|
||||||
|
order_delivery_method VARCHAR(200) NOT NULL,
|
||||||
|
carrier VARCHAR(50) NOT NULL DEFAULT 'allegro',
|
||||||
|
allegro_delivery_method_id VARCHAR(128) NOT NULL,
|
||||||
|
allegro_credentials_id VARCHAR(128) NULL,
|
||||||
|
allegro_carrier_id VARCHAR(128) NULL,
|
||||||
|
allegro_service_name VARCHAR(255) NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY shoppro_dm_mapping_unique (integration_id, order_delivery_method),
|
||||||
|
KEY shoppro_dm_mapping_integration_idx (integration_id),
|
||||||
|
CONSTRAINT shoppro_dm_mapping_integration_fk
|
||||||
|
FOREIGN KEY (integration_id) REFERENCES integrations(id)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -28,6 +28,7 @@ return [
|
|||||||
'dashboard' => 'Dashboard',
|
'dashboard' => 'Dashboard',
|
||||||
'settings' => 'Ustawienia',
|
'settings' => 'Ustawienia',
|
||||||
'statuses' => 'Statusy',
|
'statuses' => 'Statusy',
|
||||||
|
'integrations' => 'Integracje',
|
||||||
'allegro' => 'Integracje Allegro',
|
'allegro' => 'Integracje Allegro',
|
||||||
'apaczka' => 'Integracja Apaczka',
|
'apaczka' => 'Integracja Apaczka',
|
||||||
'inpost' => 'Integracja InPost',
|
'inpost' => 'Integracja InPost',
|
||||||
@@ -469,6 +470,46 @@ return [
|
|||||||
'title' => 'Ustawienia',
|
'title' => 'Ustawienia',
|
||||||
'description' => 'Konfiguracja i narzedzia administracyjne systemu.',
|
'description' => 'Konfiguracja i narzedzia administracyjne systemu.',
|
||||||
'submenu_label' => 'Sekcje ustawien',
|
'submenu_label' => 'Sekcje ustawien',
|
||||||
|
'integrations_hub' => [
|
||||||
|
'title' => 'Integracje',
|
||||||
|
'description' => 'Wspolny panel konfiguracji wszystkich providerow.',
|
||||||
|
'list_title' => 'Skonfigurowane integracje',
|
||||||
|
'empty' => 'Brak dostepnych integracji.',
|
||||||
|
'fields' => [
|
||||||
|
'provider' => 'Provider',
|
||||||
|
'instance' => 'Instancja',
|
||||||
|
'authorization' => 'Status polaczenia',
|
||||||
|
'secret' => 'Sekret API',
|
||||||
|
'active' => 'Aktywna',
|
||||||
|
'last_test' => 'Ostatni test',
|
||||||
|
'actions' => 'Akcje',
|
||||||
|
],
|
||||||
|
'providers' => [
|
||||||
|
'allegro' => 'Allegro',
|
||||||
|
'allegro_sandbox' => 'Allegro Sandbox',
|
||||||
|
'allegro_production' => 'Allegro Production',
|
||||||
|
'apaczka' => 'Apaczka',
|
||||||
|
'inpost' => 'InPost',
|
||||||
|
'shoppro' => 'shopPRO',
|
||||||
|
'shoppro_instances' => ':count instancji',
|
||||||
|
],
|
||||||
|
'status' => [
|
||||||
|
'connected' => 'Polaczono',
|
||||||
|
'not_connected' => 'Brak polaczenia',
|
||||||
|
'configured' => 'Skonfigurowana',
|
||||||
|
'not_configured' => 'Brak konfiguracji',
|
||||||
|
'saved' => 'Zapisany',
|
||||||
|
'missing' => 'Brak',
|
||||||
|
],
|
||||||
|
'active' => [
|
||||||
|
'yes' => 'Tak',
|
||||||
|
'no' => 'Nie',
|
||||||
|
],
|
||||||
|
'actions' => [
|
||||||
|
'configure' => 'Konfiguruj',
|
||||||
|
'settings' => 'Ustawienia',
|
||||||
|
],
|
||||||
|
],
|
||||||
'database' => [
|
'database' => [
|
||||||
'title' => 'Baza danych',
|
'title' => 'Baza danych',
|
||||||
'state' => [
|
'state' => [
|
||||||
@@ -800,10 +841,80 @@ return [
|
|||||||
],
|
],
|
||||||
'integrations' => [
|
'integrations' => [
|
||||||
'title' => 'Integracje shopPRO',
|
'title' => 'Integracje shopPRO',
|
||||||
|
'description' => 'W tym miejscu konfigurujesz wiele niezaleznych instancji shopPRO.',
|
||||||
'list_title' => 'Integracje shopPRO',
|
'list_title' => 'Integracje shopPRO',
|
||||||
'create_title' => 'Dodaj integracje',
|
'create_title' => 'Dodaj integracje',
|
||||||
'edit_title' => 'Edytuj integracje',
|
'edit_title' => 'Edytuj integracje',
|
||||||
'empty' => 'Brak skonfigurowanych integracji.',
|
'empty' => 'Brak skonfigurowanych integracji.',
|
||||||
|
'tabs' => [
|
||||||
|
'label' => 'Zakladki integracji shopPRO',
|
||||||
|
'integration' => 'Integracja',
|
||||||
|
'statuses' => 'Statusy',
|
||||||
|
'settings' => 'Ustawienia',
|
||||||
|
'delivery' => 'Formy dostawy',
|
||||||
|
],
|
||||||
|
'selector' => [
|
||||||
|
'integration' => 'Wybrana integracja',
|
||||||
|
],
|
||||||
|
'statuses' => [
|
||||||
|
'title' => 'Statusy',
|
||||||
|
'description' => 'Mapowanie statusow zamowien pomiedzy shopPRO i orderPRO.',
|
||||||
|
'empty' => 'Brak statusow do mapowania. Uzyj przycisku pobrania statusow.',
|
||||||
|
'select_integration_first' => 'Najpierw wybierz lub zapisz integracje w zakladce Integracja.',
|
||||||
|
'actions' => [
|
||||||
|
'sync' => 'Pobierz statusy z shopPRO',
|
||||||
|
],
|
||||||
|
'flash' => [
|
||||||
|
'sync_ok' => 'Pobrano statusy shopPRO. Rozpoznane statusy: :count.',
|
||||||
|
'sync_failed' => 'Nie udalo sie pobrac statusow shopPRO.',
|
||||||
|
'saved' => 'Mapowanie statusow zostalo zapisane.',
|
||||||
|
'save_failed' => 'Nie udalo sie zapisac mapowania statusow.',
|
||||||
|
'invalid_payload' => 'Niepoprawne dane mapowania statusow.',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'settings' => [
|
||||||
|
'title' => 'Ustawienia synchronizacji',
|
||||||
|
'description' => 'Parametry automatycznego pobierania zamowien shopPRO.',
|
||||||
|
'select_integration_first' => 'Najpierw wybierz lub zapisz integracje w zakladce Integracja.',
|
||||||
|
'orders_group_title' => 'Pobieranie zamowien',
|
||||||
|
'orders_group_description' => 'Ustawienia automatycznego importu zamowien z shopPRO.',
|
||||||
|
'orders_import_interval_minutes' => 'Interwal pobierania zamowien (minuty)',
|
||||||
|
'orders_import_interval_hint' => 'Zakres: 1-1440 minut. Dotyczy harmonogramu joba shoppro_orders_import.',
|
||||||
|
'statuses_group_title' => 'Synchronizacja statusow',
|
||||||
|
'statuses_group_description' => 'Ustawienia harmonogramu i kierunku synchronizacji statusow.',
|
||||||
|
'status_sync_direction_hint' => 'Aktualnie aktywny jest kierunek shopPRO -> orderPRO. Ustawienie orderPRO -> shopPRO jest przygotowane pod kolejny etap.',
|
||||||
|
'status_sync_interval_minutes' => 'Interwal synchronizacji statusow (minuty)',
|
||||||
|
'status_sync_interval_hint' => 'Zakres: 1-1440 minut. Dotyczy harmonogramu joba shoppro_order_status_sync.',
|
||||||
|
'payment_group_title' => 'Synchronizacja platnosci',
|
||||||
|
'payment_group_description' => 'Sprawdza czy zamowienia shopPRO zostaly oplacone i aktualizuje status platnosci w orderPRO.',
|
||||||
|
'payment_sync_interval_minutes' => 'Interwal sprawdzania platnosci (minuty)',
|
||||||
|
'payment_sync_interval_hint' => 'Zakres: 1-1440 minut. Dotyczy harmonogramu joba shoppro_payment_status_sync.',
|
||||||
|
'payment_sync_status_codes' => 'Statusy do sprawdzania platnosci',
|
||||||
|
'payment_sync_status_codes_hint' => 'Jesli nic nie zaznaczysz, system pominie tylko statusy koncowe (np. wyslane/anulowane).',
|
||||||
|
],
|
||||||
|
'delivery' => [
|
||||||
|
'title' => 'Formy dostawy',
|
||||||
|
'description' => 'Mapowanie form dostawy shopPRO do uslug nadawczych Allegro WZA/InPost.',
|
||||||
|
'select_integration_first' => 'Najpierw wybierz lub zapisz integracje w zakladce Integracja.',
|
||||||
|
'empty_orders' => 'Brak form dostawy shopPRO wykrytych w zamowieniach tej integracji.',
|
||||||
|
'not_connected' => 'Brak aktywnego polaczenia Allegro. Podlacz konto Allegro, aby pobrac liste uslug dostawy.',
|
||||||
|
'no_inpost_services' => 'Brak uslug InPost (sprawdz polaczenie z Allegro).',
|
||||||
|
'fields' => [
|
||||||
|
'order_method' => 'Forma dostawy shopPRO',
|
||||||
|
'carrier' => 'Przewoznik',
|
||||||
|
'allegro_service' => 'Usluga dostawy Allegro',
|
||||||
|
'no_mapping' => 'brak mapowania',
|
||||||
|
'search_placeholder' => 'Szukaj uslugi Allegro...',
|
||||||
|
'select_carrier_first' => 'Najpierw wybierz przewoznika.',
|
||||||
|
],
|
||||||
|
'actions' => [
|
||||||
|
'save' => 'Zapisz mapowania dostawy',
|
||||||
|
],
|
||||||
|
'flash' => [
|
||||||
|
'saved' => 'Mapowania form dostawy zostaly zapisane.',
|
||||||
|
'save_failed' => 'Nie udalo sie zapisac mapowan form dostawy.',
|
||||||
|
],
|
||||||
|
],
|
||||||
'fields' => [
|
'fields' => [
|
||||||
'name' => 'Nazwa',
|
'name' => 'Nazwa',
|
||||||
'base_url' => 'Base URL',
|
'base_url' => 'Base URL',
|
||||||
@@ -947,6 +1058,9 @@ return [
|
|||||||
'save_failed' => 'Nie udalo sie zapisac ustawien crona.',
|
'save_failed' => 'Nie udalo sie zapisac ustawien crona.',
|
||||||
'load_failed' => 'Nie udalo sie pobrac danych crona.',
|
'load_failed' => 'Nie udalo sie pobrac danych crona.',
|
||||||
],
|
],
|
||||||
|
'pagination' => [
|
||||||
|
'summary' => 'Strona :page/:total_pages, rekordy: :total',
|
||||||
|
],
|
||||||
],
|
],
|
||||||
'gs1' => [
|
'gs1' => [
|
||||||
'title' => 'GS1 / EAN',
|
'title' => 'GS1 / EAN',
|
||||||
|
|||||||
@@ -445,6 +445,11 @@ details[open] > .sidebar__group-toggle .sidebar__toggle-arrow {
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions .btn {
|
||||||
|
align-self: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.statuses-form {
|
.statuses-form {
|
||||||
@@ -453,6 +458,10 @@ details[open] > .sidebar__group-toggle .sidebar__toggle-arrow {
|
|||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.statuses-form .form-actions {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
.statuses-color-input {
|
.statuses-color-input {
|
||||||
min-height: 32px;
|
min-height: 32px;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
@@ -673,6 +682,17 @@ details[open] > .sidebar__group-toggle .sidebar__toggle-arrow {
|
|||||||
color: #0f6b39;
|
color: #0f6b39;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table-row-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row-actions form {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.table-list {
|
.table-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
@@ -1997,6 +2017,111 @@ details[open] > .sidebar__group-toggle .sidebar__toggle-arrow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shoppro-tabs-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shoppro-tabs-toolbar__field {
|
||||||
|
margin: 0;
|
||||||
|
min-width: 260px;
|
||||||
|
max-width: 420px;
|
||||||
|
flex: 1 1 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shoppro-tabs-toolbar__field .form-control {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shoppro-tabs-toolbar__actions {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-settings-group {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
border: 1px solid var(--c-border);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #f8fbff;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-settings-group__head {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-left: 3px solid var(--c-primary, #2563eb);
|
||||||
|
background: #eef4ff;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-settings-group__title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
color: #1e3a8a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-settings-group__desc {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-settings-group__grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 10px 12px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-settings-group__full {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-settings-group__grid .form-field {
|
||||||
|
margin: 0;
|
||||||
|
align-self: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-settings-group__grid .form-control {
|
||||||
|
min-height: 34px;
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-settings-group__grid input[type='date'].form-control {
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-settings-checkboxes {
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-settings-checkboxes .field-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-settings-checkboxes__list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 6px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-settings-checkboxes__item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.app-shell {
|
.app-shell {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -2114,6 +2239,14 @@ details[open] > .sidebar__group-toggle .sidebar__toggle-arrow {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.integration-settings-group__grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-settings-checkboxes__list {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,14 +73,8 @@
|
|||||||
<a class="sidebar__sublink<?= $currentMenu === 'settings' && $currentSettings === 'cron' ? ' is-active' : '' ?>" href="/settings/cron">
|
<a class="sidebar__sublink<?= $currentMenu === 'settings' && $currentSettings === 'cron' ? ' is-active' : '' ?>" href="/settings/cron">
|
||||||
<?= $e($t('navigation.cron')) ?>
|
<?= $e($t('navigation.cron')) ?>
|
||||||
</a>
|
</a>
|
||||||
<a class="sidebar__sublink<?= $currentMenu === 'settings' && $currentSettings === 'allegro' ? ' is-active' : '' ?>" href="/settings/integrations/allegro">
|
<a class="sidebar__sublink<?= $currentMenu === 'settings' && in_array($currentSettings, ['integrations', 'allegro', 'apaczka', 'inpost', 'shoppro'], true) ? ' is-active' : '' ?>" href="/settings/integrations">
|
||||||
<?= $e($t('navigation.allegro')) ?>
|
<?= $e($t('navigation.integrations')) ?>
|
||||||
</a>
|
|
||||||
<a class="sidebar__sublink<?= $currentMenu === 'settings' && $currentSettings === 'apaczka' ? ' is-active' : '' ?>" href="/settings/integrations/apaczka">
|
|
||||||
<?= $e($t('navigation.apaczka')) ?>
|
|
||||||
</a>
|
|
||||||
<a class="sidebar__sublink<?= $currentMenu === 'settings' && $currentSettings === 'inpost' ? ' is-active' : '' ?>" href="/settings/integrations/inpost">
|
|
||||||
<?= $e($t('navigation.inpost')) ?>
|
|
||||||
</a>
|
</a>
|
||||||
<a class="sidebar__sublink<?= $currentMenu === 'settings' && $currentSettings === 'company' ? ' is-active' : '' ?>" href="/settings/company">
|
<a class="sidebar__sublink<?= $currentMenu === 'settings' && $currentSettings === 'company' ? ' is-active' : '' ?>" href="/settings/company">
|
||||||
<?= $e($t('navigation.company')) ?>
|
<?= $e($t('navigation.company')) ?>
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ $allStatusesList = is_array($allStatuses ?? null) ? $allStatuses : [];
|
|||||||
$currentStatusCodeValue = (string) ($currentStatusCode ?? '');
|
$currentStatusCodeValue = (string) ($currentStatusCode ?? '');
|
||||||
$flashSuccessMsg = (string) ($flashSuccess ?? '');
|
$flashSuccessMsg = (string) ($flashSuccess ?? '');
|
||||||
$flashErrorMsg = (string) ($flashError ?? '');
|
$flashErrorMsg = (string) ($flashError ?? '');
|
||||||
|
$carrierRaw = (string) ($orderRow['external_carrier_id'] ?? '');
|
||||||
|
$carrierDisplay = trim(html_entity_decode(strip_tags($carrierRaw), ENT_QUOTES | ENT_HTML5, 'UTF-8'));
|
||||||
|
|
||||||
$addressByType = [
|
$addressByType = [
|
||||||
'customer' => null,
|
'customer' => null,
|
||||||
@@ -193,7 +195,7 @@ foreach ($addressesList as $address) {
|
|||||||
</dd>
|
</dd>
|
||||||
<dt><?= $e($t('orders.details.fields.total_with_tax')) ?></dt><dd><?= $e((string) ($orderRow['total_with_tax'] ?? '-')) ?></dd>
|
<dt><?= $e($t('orders.details.fields.total_with_tax')) ?></dt><dd><?= $e((string) ($orderRow['total_with_tax'] ?? '-')) ?></dd>
|
||||||
<dt><?= $e($t('orders.details.fields.total_paid')) ?></dt><dd><?= $e((string) ($orderRow['total_paid'] ?? '-')) ?></dd>
|
<dt><?= $e($t('orders.details.fields.total_paid')) ?></dt><dd><?= $e((string) ($orderRow['total_paid'] ?? '-')) ?></dd>
|
||||||
<dt><?= $e($t('orders.details.fields.carrier')) ?></dt><dd><?= $e((string) ($orderRow['external_carrier_id'] ?? '-')) ?></dd>
|
<dt><?= $e($t('orders.details.fields.carrier')) ?></dt><dd><?= $e($carrierDisplay !== '' ? $carrierDisplay : '-') ?></dd>
|
||||||
<dt><?= $e($t('orders.details.fields.send_date')) ?></dt><dd><?= $e((string) ($orderRow['send_date_max'] ?? '-')) ?></dd>
|
<dt><?= $e($t('orders.details.fields.send_date')) ?></dt><dd><?= $e((string) ($orderRow['send_date_max'] ?? '-')) ?></dd>
|
||||||
<dt><?= $e($t('orders.details.fields.shipments_count')) ?></dt><dd><?= $e((string) count($shipmentsList)) ?></dd>
|
<dt><?= $e($t('orders.details.fields.shipments_count')) ?></dt><dd><?= $e((string) count($shipmentsList)) ?></dd>
|
||||||
</dl>
|
</dl>
|
||||||
@@ -207,12 +209,22 @@ foreach ($addressesList as $address) {
|
|||||||
<h3 class="section-title"><?= $e((string) $addrTitle) ?></h3>
|
<h3 class="section-title"><?= $e((string) $addrTitle) ?></h3>
|
||||||
<div class="order-address mt-12">
|
<div class="order-address mt-12">
|
||||||
<?php if ($addr === []): ?>
|
<?php if ($addr === []): ?>
|
||||||
|
<?php if ($addrType === 'delivery' && $carrierDisplay !== ''): ?>
|
||||||
|
<div><?= $e($carrierDisplay) ?></div>
|
||||||
|
<?php else: ?>
|
||||||
<div class="muted">-</div>
|
<div class="muted">-</div>
|
||||||
|
<?php endif; ?>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div><?= $e((string) ($addr['name'] ?? '')) ?></div>
|
<div><?= $e((string) ($addr['name'] ?? '')) ?></div>
|
||||||
<div><?= $e((string) (($addr['street_name'] ?? '') . ' ' . ($addr['street_number'] ?? ''))) ?></div>
|
<div><?= $e((string) (($addr['street_name'] ?? '') . ' ' . ($addr['street_number'] ?? ''))) ?></div>
|
||||||
<div><?= $e((string) (($addr['zip_code'] ?? '') . ' ' . ($addr['city'] ?? ''))) ?></div>
|
<div><?= $e((string) (($addr['zip_code'] ?? '') . ' ' . ($addr['city'] ?? ''))) ?></div>
|
||||||
<div><?= $e((string) ($addr['country'] ?? '')) ?></div>
|
<div><?= $e((string) ($addr['country'] ?? '')) ?></div>
|
||||||
|
<?php if ($addrType === 'delivery' && !empty($addr['parcel_name'])): ?>
|
||||||
|
<div><?= $e((string) $addr['parcel_name']) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($addrType === 'delivery' && !empty($addr['parcel_external_id'])): ?>
|
||||||
|
<div><?= $e((string) $addr['parcel_external_id']) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
<div><?= $e((string) ($addr['phone'] ?? '')) ?></div>
|
<div><?= $e((string) ($addr['phone'] ?? '')) ?></div>
|
||||||
<div><?= $e((string) ($addr['email'] ?? '')) ?></div>
|
<div><?= $e((string) ($addr['email'] ?? '')) ?></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
$schedulesList = is_array($schedules ?? null) ? $schedules : [];
|
$schedulesList = is_array($schedules ?? null) ? $schedules : [];
|
||||||
$futureJobsList = is_array($futureJobs ?? null) ? $futureJobs : [];
|
$futureJobsList = is_array($futureJobs ?? null) ? $futureJobs : [];
|
||||||
$pastJobsList = is_array($pastJobs ?? null) ? $pastJobs : [];
|
$pastJobsList = is_array($pastJobs ?? null) ? $pastJobs : [];
|
||||||
|
$pastPagination = is_array($pastJobsPagination ?? null) ? $pastJobsPagination : [];
|
||||||
|
$pastPage = max(1, (int) ($pastPagination['page'] ?? 1));
|
||||||
|
$pastTotalPages = max(1, (int) ($pastPagination['total_pages'] ?? 1));
|
||||||
|
$pastTotal = max(0, (int) ($pastPagination['total'] ?? 0));
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
@@ -144,4 +148,26 @@ $pastJobsList = is_array($pastJobs ?? null) ? $pastJobs : [];
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<?php if ($pastTotalPages > 1): ?>
|
||||||
|
<div class="table-list__footer">
|
||||||
|
<div class="pagination">
|
||||||
|
<a class="pagination__item<?= $pastPage <= 1 ? ' is-disabled' : '' ?>" href="/settings/cron?past_page=1">«</a>
|
||||||
|
<a class="pagination__item<?= $pastPage <= 1 ? ' is-disabled' : '' ?>" href="/settings/cron?past_page=<?= $e((string) max(1, $pastPage - 1)) ?>">‹</a>
|
||||||
|
|
||||||
|
<?php $startPage = max(1, $pastPage - 2); ?>
|
||||||
|
<?php $endPage = min($pastTotalPages, $pastPage + 2); ?>
|
||||||
|
<?php for ($page = $startPage; $page <= $endPage; $page++): ?>
|
||||||
|
<a class="pagination__item<?= $page === $pastPage ? ' is-active' : '' ?>" href="/settings/cron?past_page=<?= $e((string) $page) ?>">
|
||||||
|
<?= $e((string) $page) ?>
|
||||||
|
</a>
|
||||||
|
<?php endfor; ?>
|
||||||
|
|
||||||
|
<a class="pagination__item<?= $pastPage >= $pastTotalPages ? ' is-disabled' : '' ?>" href="/settings/cron?past_page=<?= $e((string) min($pastTotalPages, $pastPage + 1)) ?>">›</a>
|
||||||
|
<a class="pagination__item<?= $pastPage >= $pastTotalPages ? ' is-disabled' : '' ?>" href="/settings/cron?past_page=<?= $e((string) $pastTotalPages) ?>">»</a>
|
||||||
|
</div>
|
||||||
|
<div class="muted">
|
||||||
|
<?= $e($t('settings.cron.pagination.summary', ['page' => (string) $pastPage, 'total_pages' => (string) $pastTotalPages, 'total' => (string) $pastTotal])) ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
58
resources/views/settings/integrations.php
Normal file
58
resources/views/settings/integrations.php
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
$items = is_array($rows ?? null) ? $rows : [];
|
||||||
|
?>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2 class="section-title"><?= $e($t('settings.integrations_hub.title')) ?></h2>
|
||||||
|
<p class="muted mt-12"><?= $e($t('settings.integrations_hub.description')) ?></p>
|
||||||
|
|
||||||
|
<?php if (!empty($errorMessage)): ?>
|
||||||
|
<div class="alert alert--danger mt-12" role="alert"><?= $e((string) $errorMessage) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (!empty($successMessage)): ?>
|
||||||
|
<div class="alert alert--success mt-12" role="status"><?= $e((string) $successMessage) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card mt-16 integrations-overview">
|
||||||
|
<h3 class="section-title"><?= $e($t('settings.integrations_hub.list_title')) ?></h3>
|
||||||
|
<div class="table-wrap mt-12">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?= $e($t('settings.integrations_hub.fields.provider')) ?></th>
|
||||||
|
<th><?= $e($t('settings.integrations_hub.fields.instance')) ?></th>
|
||||||
|
<th><?= $e($t('settings.integrations_hub.fields.authorization')) ?></th>
|
||||||
|
<th><?= $e($t('settings.integrations_hub.fields.secret')) ?></th>
|
||||||
|
<th><?= $e($t('settings.integrations_hub.fields.active')) ?></th>
|
||||||
|
<th><?= $e($t('settings.integrations_hub.fields.last_test')) ?></th>
|
||||||
|
<th><?= $e($t('settings.integrations_hub.fields.actions')) ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if ($items === []): ?>
|
||||||
|
<tr>
|
||||||
|
<td class="muted" colspan="7"><?= $e($t('settings.integrations_hub.empty')) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($items as $item): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?= $e((string) ($item['provider'] ?? '')) ?></td>
|
||||||
|
<td><?= $e((string) ($item['instance'] ?? '')) ?></td>
|
||||||
|
<td><?= $e((string) ($item['authorization_status'] ?? '')) ?></td>
|
||||||
|
<td><?= $e((string) ($item['secret_status'] ?? '')) ?></td>
|
||||||
|
<td><?= $e(!empty($item['is_active']) ? $t('settings.integrations_hub.active.yes') : $t('settings.integrations_hub.active.no')) ?></td>
|
||||||
|
<td><?= $e((string) ($item['last_test_at'] ?? '')) ?></td>
|
||||||
|
<td>
|
||||||
|
<a class="btn btn--secondary btn--sm" href="<?= $e((string) ($item['configure_url'] ?? '/settings/integrations')) ?>">
|
||||||
|
<?= $e($t('settings.integrations_hub.actions.settings')) ?>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
677
resources/views/settings/shoppro.php
Normal file
677
resources/views/settings/shoppro.php
Normal file
@@ -0,0 +1,677 @@
|
|||||||
|
<?php
|
||||||
|
$list = is_array($rows ?? null) ? $rows : [];
|
||||||
|
$selected = is_array($selectedIntegration ?? null) ? $selectedIntegration : null;
|
||||||
|
$formValues = is_array($form ?? null) ? $form : [];
|
||||||
|
$statusRows = is_array($statusRows ?? null) ? $statusRows : [];
|
||||||
|
$orderproStatuses = is_array($orderproStatuses ?? null) ? $orderproStatuses : [];
|
||||||
|
$ordersImportIntervalMinutes = max(1, (int) ($ordersImportIntervalMinutes ?? 5));
|
||||||
|
$statusSyncIntervalMinutes = max(1, (int) ($statusSyncIntervalMinutes ?? 15));
|
||||||
|
$paymentSyncIntervalMinutes = max(1, (int) ($paymentSyncIntervalMinutes ?? 10));
|
||||||
|
$activeTab = (string) ($activeTab ?? 'integration');
|
||||||
|
$isEdit = ((int) ($formValues['integration_id'] ?? 0)) > 0;
|
||||||
|
$selectedIntegrationId = (int) ($formValues['integration_id'] ?? 0);
|
||||||
|
$selectedPaymentSyncCodes = is_array($formValues['payment_sync_status_codes'] ?? null) ? $formValues['payment_sync_status_codes'] : [];
|
||||||
|
$dmMappings = is_array($deliveryMappings ?? null) ? $deliveryMappings : [];
|
||||||
|
$dmOrderMethods = is_array($orderDeliveryMethods ?? null) ? $orderDeliveryMethods : [];
|
||||||
|
$dmAllegroServices = is_array($allegroDeliveryServices ?? null) ? $allegroDeliveryServices : [];
|
||||||
|
$dmInpostServices = is_array($inpostDeliveryServices ?? null) ? $inpostDeliveryServices : [];
|
||||||
|
$dmServicesError = (string) ($allegroDeliveryServicesError ?? '');
|
||||||
|
$dmMappingsByMethod = [];
|
||||||
|
foreach ($dmMappings as $dm) {
|
||||||
|
if (!is_array($dm)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$dmMappingsByMethod[trim((string) ($dm['order_delivery_method'] ?? ''))] = $dm;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2 class="section-title"><?= $e($t('settings.integrations.title')) ?></h2>
|
||||||
|
<p class="muted mt-12"><?= $e($t('settings.integrations.description')) ?></p>
|
||||||
|
|
||||||
|
<?php if (!empty($errorMessage)): ?>
|
||||||
|
<div class="alert alert--danger mt-12" role="alert"><?= $e((string) $errorMessage) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (!empty($successMessage)): ?>
|
||||||
|
<div class="alert alert--success mt-12" role="status"><?= $e((string) $successMessage) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card mt-16">
|
||||||
|
<?php if ($list !== []): ?>
|
||||||
|
<div class="shoppro-tabs-toolbar">
|
||||||
|
<label class="form-field shoppro-tabs-toolbar__field">
|
||||||
|
<span class="field-label"><?= $e($t('settings.integrations.selector.integration')) ?></span>
|
||||||
|
<select class="form-control" id="shoppro-integration-select">
|
||||||
|
<?php foreach ($list as $item): ?>
|
||||||
|
<?php $rowId = (int) ($item['id'] ?? 0); ?>
|
||||||
|
<?php if ($rowId <= 0) continue; ?>
|
||||||
|
<option value="<?= $e((string) $rowId) ?>"<?= $rowId === $selectedIntegrationId ? ' selected' : '' ?>>
|
||||||
|
<?= $e((string) ($item['name'] ?? ('#' . $rowId))) ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<div class="shoppro-tabs-toolbar__actions">
|
||||||
|
<a href="/settings/integrations/shoppro?new=1" class="btn btn--secondary btn--sm"><?= $e($t('settings.integrations.actions.new')) ?></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<nav class="content-tabs-nav" aria-label="<?= $e($t('settings.integrations.tabs.label')) ?>">
|
||||||
|
<button type="button" class="content-tab-btn<?= $activeTab === 'integration' ? ' is-active' : '' ?>" data-tab-target="shoppro-tab-integration">
|
||||||
|
<?= $e($t('settings.integrations.tabs.integration')) ?>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="content-tab-btn<?= $activeTab === 'statuses' ? ' is-active' : '' ?>" data-tab-target="shoppro-tab-statuses">
|
||||||
|
<?= $e($t('settings.integrations.tabs.statuses')) ?>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="content-tab-btn<?= $activeTab === 'settings' ? ' is-active' : '' ?>" data-tab-target="shoppro-tab-settings">
|
||||||
|
<?= $e($t('settings.integrations.tabs.settings')) ?>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="content-tab-btn<?= $activeTab === 'delivery' ? ' is-active' : '' ?>" data-tab-target="shoppro-tab-delivery">
|
||||||
|
<?= $e($t('settings.integrations.tabs.delivery')) ?>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="content-tab-panel<?= $activeTab === 'integration' ? ' is-active' : '' ?>" data-tab-panel="shoppro-tab-integration">
|
||||||
|
<section class="mt-16 integrations-overview">
|
||||||
|
<h3 class="section-title"><?= $e($t('settings.integrations.list_title')) ?></h3>
|
||||||
|
<div class="table-wrap mt-12">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th><?= $e($t('settings.integrations.fields.name')) ?></th>
|
||||||
|
<th><?= $e($t('settings.integrations.fields.base_url')) ?></th>
|
||||||
|
<th><?= $e($t('settings.integrations.fields.active')) ?></th>
|
||||||
|
<th><?= $e($t('settings.integrations.fields.last_test')) ?></th>
|
||||||
|
<th><?= $e($t('settings.integrations.fields.actions')) ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if ($list === []): ?>
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="muted"><?= $e($t('settings.integrations.empty')) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($list as $item): ?>
|
||||||
|
<?php
|
||||||
|
$status = (string) ($item['last_test_status'] ?? '');
|
||||||
|
$statusLabel = $status === 'ok'
|
||||||
|
? $t('settings.integrations.test_status.ok')
|
||||||
|
: ($status === 'error' ? $t('settings.integrations.test_status.error') : $t('settings.integrations.test_status.never'));
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td><?= $e((string) ($item['id'] ?? 0)) ?></td>
|
||||||
|
<td><?= $e((string) ($item['name'] ?? '')) ?></td>
|
||||||
|
<td><?= $e((string) ($item['base_url'] ?? '')) ?></td>
|
||||||
|
<td>
|
||||||
|
<?php if (!empty($item['is_active'])): ?>
|
||||||
|
<span class="status-pill is-active"><?= $e($t('settings.integrations.active.yes')) ?></span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="status-pill"><?= $e($t('settings.integrations.active.no')) ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div><?= $e($statusLabel) ?></div>
|
||||||
|
<?php if (!empty($item['last_test_at'])): ?>
|
||||||
|
<small class="muted">
|
||||||
|
<?= $e((string) $item['last_test_at']) ?>
|
||||||
|
<?php if (($item['last_test_http_code'] ?? null) !== null): ?> | HTTP <?= $e((string) ($item['last_test_http_code'])) ?><?php endif; ?>
|
||||||
|
</small>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="table-row-actions">
|
||||||
|
<a class="btn btn--secondary btn--sm" href="/settings/integrations/shoppro?id=<?= $e((string) ($item['id'] ?? 0)) ?>">
|
||||||
|
<?= $e($t('settings.integrations.actions.edit')) ?>
|
||||||
|
</a>
|
||||||
|
<form action="/settings/integrations/shoppro/test" method="post">
|
||||||
|
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||||
|
<input type="hidden" name="integration_id" value="<?= $e((string) ($item['id'] ?? 0)) ?>">
|
||||||
|
<input type="hidden" name="tab" value="integration">
|
||||||
|
<button type="submit" class="btn btn--primary btn--sm"><?= $e($t('settings.integrations.actions.test')) ?></button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mt-16">
|
||||||
|
<h3 class="section-title"><?= $e($isEdit ? $t('settings.integrations.edit_title') : $t('settings.integrations.create_title')) ?></h3>
|
||||||
|
<form class="statuses-form mt-16" action="/settings/integrations/shoppro/save" method="post" novalidate>
|
||||||
|
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||||
|
<input type="hidden" name="integration_id" value="<?= $e((string) ($formValues['integration_id'] ?? 0)) ?>">
|
||||||
|
<input type="hidden" name="tab" value="integration">
|
||||||
|
|
||||||
|
<label class="form-field">
|
||||||
|
<span class="field-label"><?= $e($t('settings.integrations.fields.name')) ?></span>
|
||||||
|
<input class="form-control" type="text" name="name" value="<?= $e((string) ($formValues['name'] ?? '')) ?>" required>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-field">
|
||||||
|
<span class="field-label"><?= $e($t('settings.integrations.fields.base_url')) ?></span>
|
||||||
|
<input class="form-control" type="url" name="base_url" value="<?= $e((string) ($formValues['base_url'] ?? '')) ?>" placeholder="https://shoppro.project-dc.pl/" required>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-field">
|
||||||
|
<span class="field-label"><?= $e($t('settings.integrations.fields.api_key')) ?></span>
|
||||||
|
<input class="form-control" type="password" name="api_key" value="" placeholder="<?= $e($isEdit ? $t('settings.integrations.api_key_placeholder_edit') : '') ?>" autocomplete="new-password">
|
||||||
|
<?php if ($isEdit): ?>
|
||||||
|
<span class="muted"><?= $e(($selected['has_api_key'] ?? false) ? $t('settings.integrations.api_key_saved') : $t('settings.integrations.api_key_missing')) ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-field">
|
||||||
|
<span class="field-label"><?= $e($t('settings.integrations.fields.timeout_seconds')) ?></span>
|
||||||
|
<input class="form-control" type="number" min="1" max="120" name="timeout_seconds" value="<?= $e((string) ($formValues['timeout_seconds'] ?? 10)) ?>">
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-field">
|
||||||
|
<span class="field-label">
|
||||||
|
<input type="checkbox" name="is_active" value="1"<?= ((int) ($formValues['is_active'] ?? 1)) === 1 ? ' checked' : '' ?>>
|
||||||
|
<?= $e($t('settings.integrations.fields.active_checkbox')) ?>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="form-actions mt-12">
|
||||||
|
<button type="submit" class="btn btn--primary"><?= $e($t('settings.integrations.actions.save')) ?></button>
|
||||||
|
<a href="/settings/integrations/shoppro?new=1" class="btn btn--secondary"><?= $e($t('settings.integrations.actions.new')) ?></a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-tab-panel<?= $activeTab === 'statuses' ? ' is-active' : '' ?>" data-tab-panel="shoppro-tab-statuses">
|
||||||
|
<section class="mt-16">
|
||||||
|
<h3 class="section-title"><?= $e($t('settings.integrations.statuses.title')) ?></h3>
|
||||||
|
<p class="muted mt-12"><?= $e($t('settings.integrations.statuses.description')) ?></p>
|
||||||
|
|
||||||
|
<?php if (!$isEdit && $list === []): ?>
|
||||||
|
<p class="muted mt-12"><?= $e($t('settings.integrations.statuses.select_integration_first')) ?></p>
|
||||||
|
<?php else: ?>
|
||||||
|
<form class="mt-12" action="/settings/integrations/shoppro/statuses/sync" method="post">
|
||||||
|
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||||
|
<input type="hidden" name="integration_id" value="<?= $e((string) ($formValues['integration_id'] ?? 0)) ?>">
|
||||||
|
<button type="submit" class="btn btn--secondary"><?= $e($t('settings.integrations.statuses.actions.sync')) ?></button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form class="mt-12" action="/settings/integrations/shoppro/statuses/save" method="post">
|
||||||
|
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||||
|
<input type="hidden" name="integration_id" value="<?= $e((string) ($formValues['integration_id'] ?? 0)) ?>">
|
||||||
|
<div class="table-wrap mt-12">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?= $e($t('settings.order_statuses.fields.shoppro_code')) ?></th>
|
||||||
|
<th><?= $e($t('settings.order_statuses.fields.shoppro_name')) ?></th>
|
||||||
|
<th><?= $e($t('settings.order_statuses.fields.orderpro_status')) ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if ($statusRows === []): ?>
|
||||||
|
<tr>
|
||||||
|
<td colspan="3" class="muted"><?= $e($t('settings.integrations.statuses.empty')) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($statusRows as $row): ?>
|
||||||
|
<?php
|
||||||
|
$shopCode = (string) ($row['shoppro_status_code'] ?? '');
|
||||||
|
$shopName = (string) ($row['shoppro_status_name'] ?? '');
|
||||||
|
$selectedOrderpro = strtolower(trim((string) ($row['orderpro_status_code'] ?? '')));
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code><?= $e($shopCode) ?></code>
|
||||||
|
<input type="hidden" name="shoppro_status_code[]" value="<?= $e($shopCode) ?>">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?= $e($shopName) ?>
|
||||||
|
<input type="hidden" name="shoppro_status_name[]" value="<?= $e($shopName) ?>">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select class="form-control" name="orderpro_status_code[]">
|
||||||
|
<option value=""><?= $e($t('settings.order_statuses.fields.no_mapping')) ?></option>
|
||||||
|
<?php foreach ($orderproStatuses as $status): ?>
|
||||||
|
<?php $statusCode = strtolower(trim((string) ($status['code'] ?? ''))); ?>
|
||||||
|
<?php if ($statusCode === '') continue; ?>
|
||||||
|
<option value="<?= $e($statusCode) ?>"<?= $selectedOrderpro === $statusCode ? ' selected' : '' ?>>
|
||||||
|
<?= $e((string) ($status['name'] ?? $statusCode)) ?> (<?= $e($statusCode) ?>)
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php if ($statusRows !== []): ?>
|
||||||
|
<div class="form-actions mt-12">
|
||||||
|
<button type="submit" class="btn btn--primary"><?= $e($t('settings.order_statuses.actions.save')) ?></button>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-tab-panel<?= $activeTab === 'settings' ? ' is-active' : '' ?>" data-tab-panel="shoppro-tab-settings">
|
||||||
|
<section class="mt-16">
|
||||||
|
<h3 class="section-title"><?= $e($t('settings.integrations.settings.title')) ?></h3>
|
||||||
|
<p class="muted mt-12"><?= $e($t('settings.integrations.settings.description')) ?></p>
|
||||||
|
|
||||||
|
<?php if (!$isEdit && $list === []): ?>
|
||||||
|
<p class="muted mt-12"><?= $e($t('settings.integrations.settings.select_integration_first')) ?></p>
|
||||||
|
<?php else: ?>
|
||||||
|
<form class="statuses-form mt-12" action="/settings/integrations/shoppro/save" method="post" novalidate>
|
||||||
|
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||||
|
<input type="hidden" name="integration_id" value="<?= $e((string) ($formValues['integration_id'] ?? 0)) ?>">
|
||||||
|
<input type="hidden" name="tab" value="settings">
|
||||||
|
<input type="hidden" name="name" value="<?= $e((string) ($formValues['name'] ?? '')) ?>">
|
||||||
|
<input type="hidden" name="base_url" value="<?= $e((string) ($formValues['base_url'] ?? '')) ?>">
|
||||||
|
<input type="hidden" name="timeout_seconds" value="<?= $e((string) ($formValues['timeout_seconds'] ?? 10)) ?>">
|
||||||
|
<input type="hidden" name="is_active" value="<?= $e(((int) ($formValues['is_active'] ?? 1)) === 1 ? '1' : '0') ?>">
|
||||||
|
|
||||||
|
<div class="integration-settings-group">
|
||||||
|
<div class="integration-settings-group__head">
|
||||||
|
<h4 class="integration-settings-group__title"><?= $e($t('settings.integrations.settings.orders_group_title')) ?></h4>
|
||||||
|
<p class="muted integration-settings-group__desc"><?= $e($t('settings.integrations.settings.orders_group_description')) ?></p>
|
||||||
|
</div>
|
||||||
|
<div class="integration-settings-group__grid">
|
||||||
|
<label class="form-field integration-settings-group__full">
|
||||||
|
<span class="field-label">
|
||||||
|
<input type="checkbox" name="orders_fetch_enabled" value="1"<?= ((int) ($formValues['orders_fetch_enabled'] ?? 0)) === 1 ? ' checked' : '' ?>>
|
||||||
|
<?= $e($t('settings.integrations.fields.orders_fetch_enabled_checkbox')) ?>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-field">
|
||||||
|
<span class="field-label"><?= $e($t('settings.integrations.fields.orders_fetch_start_date')) ?></span>
|
||||||
|
<input class="form-control" type="date" name="orders_fetch_start_date" value="<?= $e((string) ($formValues['orders_fetch_start_date'] ?? '')) ?>">
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-field">
|
||||||
|
<span class="field-label"><?= $e($t('settings.integrations.settings.orders_import_interval_minutes')) ?></span>
|
||||||
|
<input class="form-control" type="number" min="1" max="1440" name="orders_import_interval_minutes" value="<?= $e((string) $ordersImportIntervalMinutes) ?>">
|
||||||
|
<span class="muted"><?= $e($t('settings.integrations.settings.orders_import_interval_hint')) ?></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="integration-settings-group">
|
||||||
|
<div class="integration-settings-group__head">
|
||||||
|
<h4 class="integration-settings-group__title"><?= $e($t('settings.integrations.settings.statuses_group_title')) ?></h4>
|
||||||
|
<p class="muted integration-settings-group__desc"><?= $e($t('settings.integrations.settings.statuses_group_description')) ?></p>
|
||||||
|
</div>
|
||||||
|
<div class="integration-settings-group__grid">
|
||||||
|
<label class="form-field">
|
||||||
|
<span class="field-label"><?= $e($t('settings.integrations.fields.order_status_sync_direction')) ?></span>
|
||||||
|
<select class="form-control" name="order_status_sync_direction">
|
||||||
|
<?php $syncDirection = (string) ($formValues['order_status_sync_direction'] ?? 'shoppro_to_orderpro'); ?>
|
||||||
|
<option value="shoppro_to_orderpro"<?= $syncDirection === 'shoppro_to_orderpro' ? ' selected' : '' ?>>
|
||||||
|
<?= $e($t('settings.integrations.fields.order_status_sync_direction_shoppro_to_orderpro')) ?>
|
||||||
|
</option>
|
||||||
|
<option value="orderpro_to_shoppro"<?= $syncDirection === 'orderpro_to_shoppro' ? ' selected' : '' ?>>
|
||||||
|
<?= $e($t('settings.integrations.fields.order_status_sync_direction_orderpro_to_shoppro')) ?>
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<span class="muted"><?= $e($t('settings.integrations.settings.status_sync_direction_hint')) ?></span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-field">
|
||||||
|
<span class="field-label"><?= $e($t('settings.integrations.settings.status_sync_interval_minutes')) ?></span>
|
||||||
|
<input class="form-control" type="number" min="1" max="1440" name="status_sync_interval_minutes" value="<?= $e((string) $statusSyncIntervalMinutes) ?>">
|
||||||
|
<span class="muted"><?= $e($t('settings.integrations.settings.status_sync_interval_hint')) ?></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="integration-settings-group">
|
||||||
|
<div class="integration-settings-group__head">
|
||||||
|
<h4 class="integration-settings-group__title"><?= $e($t('settings.integrations.settings.payment_group_title')) ?></h4>
|
||||||
|
<p class="muted integration-settings-group__desc"><?= $e($t('settings.integrations.settings.payment_group_description')) ?></p>
|
||||||
|
</div>
|
||||||
|
<div class="integration-settings-group__grid">
|
||||||
|
<label class="form-field">
|
||||||
|
<span class="field-label"><?= $e($t('settings.integrations.settings.payment_sync_interval_minutes')) ?></span>
|
||||||
|
<input class="form-control" type="number" min="1" max="1440" name="payment_sync_interval_minutes" value="<?= $e((string) $paymentSyncIntervalMinutes) ?>">
|
||||||
|
<span class="muted"><?= $e($t('settings.integrations.settings.payment_sync_interval_hint')) ?></span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<fieldset class="form-field integration-settings-group__full integration-settings-checkboxes">
|
||||||
|
<legend class="field-label"><?= $e($t('settings.integrations.settings.payment_sync_status_codes')) ?></legend>
|
||||||
|
<span class="muted"><?= $e($t('settings.integrations.settings.payment_sync_status_codes_hint')) ?></span>
|
||||||
|
<div class="integration-settings-checkboxes__list mt-12">
|
||||||
|
<?php foreach ($orderproStatuses as $status): ?>
|
||||||
|
<?php $statusCode = strtolower(trim((string) ($status['code'] ?? ''))); ?>
|
||||||
|
<?php if ($statusCode === '') continue; ?>
|
||||||
|
<?php $isChecked = in_array($statusCode, $selectedPaymentSyncCodes, true); ?>
|
||||||
|
<label class="integration-settings-checkboxes__item">
|
||||||
|
<input type="checkbox" name="payment_sync_status_codes[]" value="<?= $e($statusCode) ?>"<?= $isChecked ? ' checked' : '' ?>>
|
||||||
|
<span><?= $e((string) ($status['name'] ?? $statusCode)) ?> (<?= $e($statusCode) ?>)</span>
|
||||||
|
</label>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn--primary"><?= $e($t('settings.integrations.actions.save')) ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-tab-panel<?= $activeTab === 'delivery' ? ' is-active' : '' ?>" data-tab-panel="shoppro-tab-delivery">
|
||||||
|
<section class="mt-16">
|
||||||
|
<h3 class="section-title"><?= $e($t('settings.integrations.delivery.title')) ?></h3>
|
||||||
|
<p class="muted mt-12"><?= $e($t('settings.integrations.delivery.description')) ?></p>
|
||||||
|
|
||||||
|
<?php if (!$isEdit): ?>
|
||||||
|
<p class="muted mt-12"><?= $e($t('settings.integrations.delivery.select_integration_first')) ?></p>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php if ($dmServicesError !== ''): ?>
|
||||||
|
<div class="alert alert--danger mt-12"><?= $e($dmServicesError) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($dmOrderMethods === []): ?>
|
||||||
|
<p class="muted mt-12"><?= $e($t('settings.integrations.delivery.empty_orders')) ?></p>
|
||||||
|
<?php else: ?>
|
||||||
|
<form action="/settings/integrations/shoppro/delivery/save" method="post">
|
||||||
|
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||||
|
<input type="hidden" name="integration_id" value="<?= $e((string) $selectedIntegrationId) ?>">
|
||||||
|
<div class="table-wrap table-wrap--visible mt-12">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?= $e($t('settings.integrations.delivery.fields.order_method')) ?></th>
|
||||||
|
<th><?= $e($t('settings.integrations.delivery.fields.carrier')) ?></th>
|
||||||
|
<th><?= $e($t('settings.integrations.delivery.fields.allegro_service')) ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($dmOrderMethods as $rowIdx => $orderMethod): ?>
|
||||||
|
<?php
|
||||||
|
$methodName = trim((string) $orderMethod);
|
||||||
|
$currentMapping = $dmMappingsByMethod[$methodName] ?? null;
|
||||||
|
$currentCarrier = $currentMapping !== null ? trim((string) ($currentMapping['carrier'] ?? 'allegro')) : '';
|
||||||
|
$currentAllegroId = $currentMapping !== null ? trim((string) ($currentMapping['allegro_delivery_method_id'] ?? '')) : '';
|
||||||
|
$currentServiceName = $currentMapping !== null ? trim((string) ($currentMapping['allegro_service_name'] ?? '')) : '';
|
||||||
|
?>
|
||||||
|
<tr data-dm-row="<?= $rowIdx ?>">
|
||||||
|
<td>
|
||||||
|
<strong><?= $e($methodName) ?></strong>
|
||||||
|
<input type="hidden" name="order_delivery_method[]" value="<?= $e($methodName) ?>">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select class="form-control dm-carrier-select" name="carrier[]" data-row="<?= $rowIdx ?>">
|
||||||
|
<option value="">-- <?= $e($t('settings.integrations.delivery.fields.no_mapping')) ?> --</option>
|
||||||
|
<option value="allegro"<?= $currentCarrier === 'allegro' && $currentAllegroId !== '' ? ' selected' : '' ?>>Allegro</option>
|
||||||
|
<option value="inpost"<?= $currentCarrier === 'inpost' ? ' selected' : '' ?>>InPost</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="dm-service-wrap" data-row="<?= $rowIdx ?>">
|
||||||
|
<input type="hidden" name="allegro_delivery_method_id[]" class="dm-hidden-method-id" value="<?= $e($currentAllegroId) ?>">
|
||||||
|
<input type="hidden" name="allegro_credentials_id[]" class="dm-hidden-credentials-id" value="<?= $e(trim((string) ($currentMapping['allegro_credentials_id'] ?? ''))) ?>">
|
||||||
|
<input type="hidden" name="allegro_carrier_id[]" class="dm-hidden-carrier-id" value="<?= $e(trim((string) ($currentMapping['allegro_carrier_id'] ?? ''))) ?>">
|
||||||
|
<input type="hidden" name="allegro_service_name[]" class="dm-hidden-service-name" value="<?= $e($currentServiceName) ?>">
|
||||||
|
|
||||||
|
<div class="dm-allegro-panel dm-searchable-select" data-current-id="<?= $e($currentCarrier === 'allegro' ? $currentAllegroId : '') ?>" data-current-name="<?= $e($currentCarrier === 'allegro' ? $currentServiceName : '') ?>" style="<?= $currentCarrier !== 'allegro' || $currentAllegroId === '' && $currentCarrier === '' ? 'display:none' : '' ?>">
|
||||||
|
<input type="text" class="form-control dm-search-input" placeholder="<?= $e($t('settings.integrations.delivery.fields.search_placeholder')) ?>" value="<?= $e($currentCarrier === 'allegro' ? $currentServiceName : '') ?>" autocomplete="off">
|
||||||
|
<div class="searchable-select__dropdown dm-dropdown">
|
||||||
|
<div class="searchable-select__option dm-option-clear" data-value="" data-label="" data-credentials-id="" data-carrier-id="">
|
||||||
|
<em class="muted">-- <?= $e($t('settings.integrations.delivery.fields.no_mapping')) ?> --</em>
|
||||||
|
</div>
|
||||||
|
<?php foreach ($dmAllegroServices as $svc): ?>
|
||||||
|
<?php
|
||||||
|
if (!is_array($svc)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$svcId = is_array($svc['id'] ?? null) ? $svc['id'] : [];
|
||||||
|
$svcMethodId = trim((string) ($svcId['deliveryMethodId'] ?? ''));
|
||||||
|
$svcCredentialsId = trim((string) ($svcId['credentialsId'] ?? ''));
|
||||||
|
$svcName = trim((string) ($svc['name'] ?? ''));
|
||||||
|
$svcCarrierId = trim((string) ($svc['carrierId'] ?? ''));
|
||||||
|
$svcOwner = trim((string) ($svc['owner'] ?? ''));
|
||||||
|
$svcLabel = $svcName . ' (' . $svcOwner . ')';
|
||||||
|
?>
|
||||||
|
<div class="searchable-select__option"
|
||||||
|
data-value="<?= $e($svcMethodId) ?>"
|
||||||
|
data-label="<?= $e($svcLabel) ?>"
|
||||||
|
data-credentials-id="<?= $e($svcCredentialsId) ?>"
|
||||||
|
data-carrier-id="<?= $e($svcCarrierId) ?>">
|
||||||
|
<?= $e($svcName) ?> <span class="muted">(<?= $e($svcOwner) ?>)</span>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dm-inpost-panel" style="<?= $currentCarrier !== 'inpost' ? 'display:none' : '' ?>">
|
||||||
|
<?php if ($dmInpostServices === []): ?>
|
||||||
|
<div class="muted"><?= $e($t('settings.integrations.delivery.no_inpost_services')) ?></div>
|
||||||
|
<?php else: ?>
|
||||||
|
<select class="form-control dm-inpost-select">
|
||||||
|
<option value="">-- <?= $e($t('settings.integrations.delivery.fields.no_mapping')) ?> --</option>
|
||||||
|
<?php foreach ($dmInpostServices as $inSvc): ?>
|
||||||
|
<?php
|
||||||
|
if (!is_array($inSvc)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$inSvcId = is_array($inSvc['id'] ?? null) ? $inSvc['id'] : [];
|
||||||
|
$inSvcMethodId = trim((string) ($inSvcId['deliveryMethodId'] ?? ''));
|
||||||
|
$inSvcCredentialsId = trim((string) ($inSvcId['credentialsId'] ?? ''));
|
||||||
|
$inSvcCarrierId = trim((string) ($inSvc['carrierId'] ?? ''));
|
||||||
|
$inSvcName = trim((string) ($inSvc['name'] ?? ''));
|
||||||
|
?>
|
||||||
|
<option
|
||||||
|
value="<?= $e($inSvcMethodId) ?>"
|
||||||
|
data-credentials-id="<?= $e($inSvcCredentialsId) ?>"
|
||||||
|
data-carrier-id="<?= $e($inSvcCarrierId) ?>"
|
||||||
|
<?= $currentCarrier === 'inpost' && $currentAllegroId === $inSvcMethodId ? 'selected' : '' ?>>
|
||||||
|
<?= $e($inSvcName) ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dm-empty-panel muted" style="<?= $currentCarrier !== '' ? 'display:none' : ($currentAllegroId !== '' ? 'display:none' : '') ?>">
|
||||||
|
<?= $e($t('settings.integrations.delivery.fields.select_carrier_first')) ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions mt-12">
|
||||||
|
<button type="submit" class="btn btn--primary"><?= $e($t('settings.integrations.delivery.actions.save')) ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var tabs = document.querySelectorAll('[data-tab-target]');
|
||||||
|
var panels = document.querySelectorAll('[data-tab-panel]');
|
||||||
|
if (tabs.length === 0 || panels.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tabNameMap = {
|
||||||
|
'shoppro-tab-integration': 'integration',
|
||||||
|
'shoppro-tab-statuses': 'statuses',
|
||||||
|
'shoppro-tab-settings': 'settings',
|
||||||
|
'shoppro-tab-delivery': 'delivery'
|
||||||
|
};
|
||||||
|
|
||||||
|
tabs.forEach(function (tab) {
|
||||||
|
tab.addEventListener('click', function () {
|
||||||
|
var target = tab.getAttribute('data-tab-target');
|
||||||
|
var tabName = tabNameMap[target] || 'integration';
|
||||||
|
var url = new URL(window.location.href);
|
||||||
|
var currentTab = url.searchParams.get('tab') || 'integration';
|
||||||
|
if (tabName === 'integration') {
|
||||||
|
url.searchParams.delete('tab');
|
||||||
|
} else {
|
||||||
|
url.searchParams.set('tab', tabName);
|
||||||
|
}
|
||||||
|
if (tabName === 'delivery' && currentTab !== 'delivery') {
|
||||||
|
window.location.href = url.toString();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.history.replaceState(null, '', url.toString());
|
||||||
|
|
||||||
|
tabs.forEach(function (node) { node.classList.remove('is-active'); });
|
||||||
|
panels.forEach(function (panel) { panel.classList.remove('is-active'); });
|
||||||
|
tab.classList.add('is-active');
|
||||||
|
var panel = document.querySelector('[data-tab-panel="' + target + '"]');
|
||||||
|
if (panel) {
|
||||||
|
panel.classList.add('is-active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
var select = document.getElementById('shoppro-integration-select');
|
||||||
|
if (!select) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
select.addEventListener('change', function () {
|
||||||
|
var url = new URL(window.location.href);
|
||||||
|
if (select.value) {
|
||||||
|
url.searchParams.set('id', select.value);
|
||||||
|
} else {
|
||||||
|
url.searchParams.delete('id');
|
||||||
|
}
|
||||||
|
window.location.href = url.toString();
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
document.querySelectorAll('.dm-carrier-select').forEach(function (carrierSelect) {
|
||||||
|
var rowIdx = carrierSelect.getAttribute('data-row');
|
||||||
|
var serviceWrap = document.querySelector('.dm-service-wrap[data-row="' + rowIdx + '"]');
|
||||||
|
if (!serviceWrap) return;
|
||||||
|
|
||||||
|
var allegroPanel = serviceWrap.querySelector('.dm-allegro-panel');
|
||||||
|
var inpostPanel = serviceWrap.querySelector('.dm-inpost-panel');
|
||||||
|
var emptyPanel = serviceWrap.querySelector('.dm-empty-panel');
|
||||||
|
var hiddenMethodId = serviceWrap.querySelector('.dm-hidden-method-id');
|
||||||
|
var hiddenCredentialsId = serviceWrap.querySelector('.dm-hidden-credentials-id');
|
||||||
|
var hiddenCarrierId = serviceWrap.querySelector('.dm-hidden-carrier-id');
|
||||||
|
var hiddenServiceName = serviceWrap.querySelector('.dm-hidden-service-name');
|
||||||
|
|
||||||
|
function showPanel(carrier) {
|
||||||
|
if (allegroPanel) allegroPanel.style.display = carrier === 'allegro' ? '' : 'none';
|
||||||
|
if (inpostPanel) inpostPanel.style.display = carrier === 'inpost' ? '' : 'none';
|
||||||
|
if (emptyPanel) emptyPanel.style.display = carrier === '' ? '' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
carrierSelect.addEventListener('change', function () {
|
||||||
|
var carrier = carrierSelect.value;
|
||||||
|
showPanel(carrier);
|
||||||
|
if (hiddenMethodId) hiddenMethodId.value = '';
|
||||||
|
if (hiddenCredentialsId) hiddenCredentialsId.value = '';
|
||||||
|
if (hiddenCarrierId) hiddenCarrierId.value = '';
|
||||||
|
if (hiddenServiceName) hiddenServiceName.value = '';
|
||||||
|
var allegroInput = allegroPanel ? allegroPanel.querySelector('.dm-search-input') : null;
|
||||||
|
if (allegroInput) allegroInput.value = '';
|
||||||
|
var inpostSelect = inpostPanel ? inpostPanel.querySelector('.dm-inpost-select') : null;
|
||||||
|
if (inpostSelect) inpostSelect.value = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
var inpostSelect = inpostPanel ? inpostPanel.querySelector('.dm-inpost-select') : null;
|
||||||
|
if (inpostSelect) {
|
||||||
|
inpostSelect.addEventListener('change', function () {
|
||||||
|
var opt = inpostSelect.options[inpostSelect.selectedIndex];
|
||||||
|
if (hiddenMethodId) hiddenMethodId.value = inpostSelect.value;
|
||||||
|
if (hiddenCredentialsId) hiddenCredentialsId.value = opt ? (opt.getAttribute('data-credentials-id') || '') : '';
|
||||||
|
if (hiddenCarrierId) hiddenCarrierId.value = opt ? (opt.getAttribute('data-carrier-id') || '') : '';
|
||||||
|
if (hiddenServiceName) hiddenServiceName.value = opt ? opt.textContent.trim() : '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.dm-searchable-select').forEach(function (wrapper) {
|
||||||
|
var searchInput = wrapper.querySelector('.dm-search-input');
|
||||||
|
var dropdown = wrapper.querySelector('.dm-dropdown');
|
||||||
|
var serviceWrap = wrapper.closest('.dm-service-wrap');
|
||||||
|
if (!searchInput || !dropdown || !serviceWrap) return;
|
||||||
|
|
||||||
|
var hiddenMethodId = serviceWrap.querySelector('.dm-hidden-method-id');
|
||||||
|
var hiddenCredentialsId = serviceWrap.querySelector('.dm-hidden-credentials-id');
|
||||||
|
var hiddenCarrierId = serviceWrap.querySelector('.dm-hidden-carrier-id');
|
||||||
|
var hiddenServiceName = serviceWrap.querySelector('.dm-hidden-service-name');
|
||||||
|
|
||||||
|
var options = dropdown.querySelectorAll('.searchable-select__option');
|
||||||
|
wrapper.style.position = 'relative';
|
||||||
|
|
||||||
|
function selectOption(opt) {
|
||||||
|
hiddenMethodId.value = opt.getAttribute('data-value') || '';
|
||||||
|
hiddenCredentialsId.value = opt.getAttribute('data-credentials-id') || '';
|
||||||
|
hiddenCarrierId.value = opt.getAttribute('data-carrier-id') || '';
|
||||||
|
hiddenServiceName.value = opt.getAttribute('data-label') || '';
|
||||||
|
searchInput.value = opt.getAttribute('data-label') || '';
|
||||||
|
dropdown.classList.remove('is-open');
|
||||||
|
options.forEach(function (o) { o.classList.remove('is-selected'); });
|
||||||
|
opt.classList.add('is-selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterOptions(query) {
|
||||||
|
var q = query.toLowerCase().trim();
|
||||||
|
options.forEach(function (opt) {
|
||||||
|
var label = (opt.getAttribute('data-label') || '').toLowerCase();
|
||||||
|
opt.style.display = (q === '' || label.indexOf(q) !== -1) ? '' : 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
searchInput.addEventListener('focus', function () {
|
||||||
|
filterOptions(searchInput.value);
|
||||||
|
dropdown.classList.add('is-open');
|
||||||
|
});
|
||||||
|
|
||||||
|
searchInput.addEventListener('input', function () {
|
||||||
|
filterOptions(searchInput.value);
|
||||||
|
dropdown.classList.add('is-open');
|
||||||
|
});
|
||||||
|
|
||||||
|
options.forEach(function (opt) {
|
||||||
|
opt.addEventListener('mousedown', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
selectOption(opt);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
searchInput.addEventListener('blur', function () {
|
||||||
|
setTimeout(function () { dropdown.classList.remove('is-open'); }, 150);
|
||||||
|
});
|
||||||
|
|
||||||
|
var currentId = wrapper.getAttribute('data-current-id') || '';
|
||||||
|
if (currentId !== '') {
|
||||||
|
options.forEach(function (opt) {
|
||||||
|
if (opt.getAttribute('data-value') === currentId) {
|
||||||
|
opt.classList.add('is-selected');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@@ -21,6 +21,12 @@ use App\Modules\Settings\ApaczkaIntegrationController;
|
|||||||
use App\Modules\Settings\ApaczkaIntegrationRepository;
|
use App\Modules\Settings\ApaczkaIntegrationRepository;
|
||||||
use App\Modules\Settings\InpostIntegrationController;
|
use App\Modules\Settings\InpostIntegrationController;
|
||||||
use App\Modules\Settings\InpostIntegrationRepository;
|
use App\Modules\Settings\InpostIntegrationRepository;
|
||||||
|
use App\Modules\Settings\IntegrationsHubController;
|
||||||
|
use App\Modules\Settings\IntegrationsRepository;
|
||||||
|
use App\Modules\Settings\ShopproIntegrationsController;
|
||||||
|
use App\Modules\Settings\ShopproDeliveryMethodMappingRepository;
|
||||||
|
use App\Modules\Settings\ShopproIntegrationsRepository;
|
||||||
|
use App\Modules\Settings\ShopproStatusMappingRepository;
|
||||||
use App\Modules\Settings\AllegroDeliveryMethodMappingRepository;
|
use App\Modules\Settings\AllegroDeliveryMethodMappingRepository;
|
||||||
use App\Modules\Settings\CompanySettingsController;
|
use App\Modules\Settings\CompanySettingsController;
|
||||||
use App\Modules\Settings\CompanySettingsRepository;
|
use App\Modules\Settings\CompanySettingsRepository;
|
||||||
@@ -97,6 +103,33 @@ return static function (Application $app): void {
|
|||||||
$auth,
|
$auth,
|
||||||
$inpostIntegrationRepository
|
$inpostIntegrationRepository
|
||||||
);
|
);
|
||||||
|
$shopproIntegrationsRepository = new ShopproIntegrationsRepository(
|
||||||
|
$app->db(),
|
||||||
|
(string) $app->config('app.integrations.secret', '')
|
||||||
|
);
|
||||||
|
$shopproIntegrationsController = new ShopproIntegrationsController(
|
||||||
|
$template,
|
||||||
|
$translator,
|
||||||
|
$auth,
|
||||||
|
$shopproIntegrationsRepository,
|
||||||
|
new ShopproStatusMappingRepository($app->db()),
|
||||||
|
$app->orderStatuses(),
|
||||||
|
$cronRepository,
|
||||||
|
new ShopproDeliveryMethodMappingRepository($app->db()),
|
||||||
|
$allegroIntegrationRepository,
|
||||||
|
$allegroOAuthClient,
|
||||||
|
new AllegroApiClient()
|
||||||
|
);
|
||||||
|
$integrationsHubController = new IntegrationsHubController(
|
||||||
|
$template,
|
||||||
|
$translator,
|
||||||
|
$auth,
|
||||||
|
new IntegrationsRepository($app->db()),
|
||||||
|
$allegroIntegrationRepository,
|
||||||
|
$apaczkaIntegrationRepository,
|
||||||
|
$inpostIntegrationRepository,
|
||||||
|
$shopproIntegrationsRepository
|
||||||
|
);
|
||||||
$cronSettingsController = new CronSettingsController(
|
$cronSettingsController = new CronSettingsController(
|
||||||
$template,
|
$template,
|
||||||
$translator,
|
$translator,
|
||||||
@@ -173,6 +206,7 @@ return static function (Application $app): void {
|
|||||||
$router->post('/settings/statuses/reorder', [$settingsController, 'reorderStatuses'], [$authMiddleware]);
|
$router->post('/settings/statuses/reorder', [$settingsController, 'reorderStatuses'], [$authMiddleware]);
|
||||||
$router->get('/settings/cron', [$cronSettingsController, 'index'], [$authMiddleware]);
|
$router->get('/settings/cron', [$cronSettingsController, 'index'], [$authMiddleware]);
|
||||||
$router->post('/settings/cron', [$cronSettingsController, 'save'], [$authMiddleware]);
|
$router->post('/settings/cron', [$cronSettingsController, 'save'], [$authMiddleware]);
|
||||||
|
$router->get('/settings/integrations', [$integrationsHubController, 'index'], [$authMiddleware]);
|
||||||
$router->get('/settings/integrations/allegro', [$allegroIntegrationController, 'index'], [$authMiddleware]);
|
$router->get('/settings/integrations/allegro', [$allegroIntegrationController, 'index'], [$authMiddleware]);
|
||||||
$router->post('/settings/integrations/allegro/save', [$allegroIntegrationController, 'save'], [$authMiddleware]);
|
$router->post('/settings/integrations/allegro/save', [$allegroIntegrationController, 'save'], [$authMiddleware]);
|
||||||
$router->post('/settings/integrations/allegro/settings/save', [$allegroIntegrationController, 'saveImportSettings'], [$authMiddleware]);
|
$router->post('/settings/integrations/allegro/settings/save', [$allegroIntegrationController, 'saveImportSettings'], [$authMiddleware]);
|
||||||
@@ -188,6 +222,12 @@ return static function (Application $app): void {
|
|||||||
$router->post('/settings/integrations/apaczka/save', [$apaczkaIntegrationController, 'save'], [$authMiddleware]);
|
$router->post('/settings/integrations/apaczka/save', [$apaczkaIntegrationController, 'save'], [$authMiddleware]);
|
||||||
$router->get('/settings/integrations/inpost', [$inpostIntegrationController, 'index'], [$authMiddleware]);
|
$router->get('/settings/integrations/inpost', [$inpostIntegrationController, 'index'], [$authMiddleware]);
|
||||||
$router->post('/settings/integrations/inpost/save', [$inpostIntegrationController, 'save'], [$authMiddleware]);
|
$router->post('/settings/integrations/inpost/save', [$inpostIntegrationController, 'save'], [$authMiddleware]);
|
||||||
|
$router->get('/settings/integrations/shoppro', [$shopproIntegrationsController, 'index'], [$authMiddleware]);
|
||||||
|
$router->post('/settings/integrations/shoppro/save', [$shopproIntegrationsController, 'save'], [$authMiddleware]);
|
||||||
|
$router->post('/settings/integrations/shoppro/test', [$shopproIntegrationsController, 'test'], [$authMiddleware]);
|
||||||
|
$router->post('/settings/integrations/shoppro/statuses/save', [$shopproIntegrationsController, 'saveStatusMappings'], [$authMiddleware]);
|
||||||
|
$router->post('/settings/integrations/shoppro/statuses/sync', [$shopproIntegrationsController, 'syncStatuses'], [$authMiddleware]);
|
||||||
|
$router->post('/settings/integrations/shoppro/delivery/save', [$shopproIntegrationsController, 'saveDeliveryMappings'], [$authMiddleware]);
|
||||||
$router->get('/settings/company', [$companySettingsController, 'index'], [$authMiddleware]);
|
$router->get('/settings/company', [$companySettingsController, 'index'], [$authMiddleware]);
|
||||||
$router->post('/settings/company/save', [$companySettingsController, 'save'], [$authMiddleware]);
|
$router->post('/settings/company/save', [$companySettingsController, 'save'], [$authMiddleware]);
|
||||||
$router->get('/orders/{id}/shipment/prepare', [$shipmentController, 'prepare'], [$authMiddleware]);
|
$router->get('/orders/{id}/shipment/prepare', [$shipmentController, 'prepare'], [$authMiddleware]);
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ use App\Modules\Cron\AllegroStatusSyncHandler;
|
|||||||
use App\Modules\Cron\AllegroTokenRefreshHandler;
|
use App\Modules\Cron\AllegroTokenRefreshHandler;
|
||||||
use App\Modules\Cron\CronRepository;
|
use App\Modules\Cron\CronRepository;
|
||||||
use App\Modules\Cron\CronRunner;
|
use App\Modules\Cron\CronRunner;
|
||||||
|
use App\Modules\Cron\ShopproOrdersImportHandler;
|
||||||
|
use App\Modules\Cron\ShopproPaymentStatusSyncHandler;
|
||||||
|
use App\Modules\Cron\ShopproStatusSyncHandler;
|
||||||
use App\Modules\Orders\OrderImportRepository;
|
use App\Modules\Orders\OrderImportRepository;
|
||||||
use App\Modules\Orders\OrdersRepository;
|
use App\Modules\Orders\OrdersRepository;
|
||||||
use App\Modules\Settings\AllegroApiClient;
|
use App\Modules\Settings\AllegroApiClient;
|
||||||
@@ -29,6 +32,12 @@ use App\Modules\Settings\AllegroOAuthClient;
|
|||||||
use App\Modules\Settings\AllegroStatusSyncService;
|
use App\Modules\Settings\AllegroStatusSyncService;
|
||||||
use App\Modules\Settings\AllegroStatusMappingRepository;
|
use App\Modules\Settings\AllegroStatusMappingRepository;
|
||||||
use App\Modules\Settings\OrderStatusRepository;
|
use App\Modules\Settings\OrderStatusRepository;
|
||||||
|
use App\Modules\Settings\ShopproApiClient;
|
||||||
|
use App\Modules\Settings\ShopproIntegrationsRepository;
|
||||||
|
use App\Modules\Settings\ShopproOrdersSyncService;
|
||||||
|
use App\Modules\Settings\ShopproPaymentStatusSyncService;
|
||||||
|
use App\Modules\Settings\ShopproStatusSyncService;
|
||||||
|
use App\Modules\Settings\ShopproStatusMappingRepository;
|
||||||
use App\Modules\Users\UserRepository;
|
use App\Modules\Users\UserRepository;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
use PDO;
|
use PDO;
|
||||||
@@ -282,6 +291,33 @@ final class Application
|
|||||||
$apiClient,
|
$apiClient,
|
||||||
$orderImportService
|
$orderImportService
|
||||||
);
|
);
|
||||||
|
$shopproSyncService = new ShopproOrdersSyncService(
|
||||||
|
new ShopproIntegrationsRepository(
|
||||||
|
$this->db,
|
||||||
|
(string) $this->config('app.integrations.secret', '')
|
||||||
|
),
|
||||||
|
new AllegroOrderSyncStateRepository($this->db),
|
||||||
|
new ShopproApiClient(),
|
||||||
|
new OrderImportRepository($this->db),
|
||||||
|
new ShopproStatusMappingRepository($this->db),
|
||||||
|
new OrdersRepository($this->db)
|
||||||
|
);
|
||||||
|
$shopproStatusSyncService = new ShopproStatusSyncService(
|
||||||
|
new ShopproIntegrationsRepository(
|
||||||
|
$this->db,
|
||||||
|
(string) $this->config('app.integrations.secret', '')
|
||||||
|
),
|
||||||
|
$shopproSyncService
|
||||||
|
);
|
||||||
|
$shopproPaymentSyncService = new ShopproPaymentStatusSyncService(
|
||||||
|
new ShopproIntegrationsRepository(
|
||||||
|
$this->db,
|
||||||
|
(string) $this->config('app.integrations.secret', '')
|
||||||
|
),
|
||||||
|
new ShopproApiClient(),
|
||||||
|
new OrdersRepository($this->db),
|
||||||
|
$this->db
|
||||||
|
);
|
||||||
|
|
||||||
$runner = new CronRunner(
|
$runner = new CronRunner(
|
||||||
$repository,
|
$repository,
|
||||||
@@ -301,6 +337,15 @@ final class Application
|
|||||||
$this->db
|
$this->db
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
'shoppro_orders_import' => new ShopproOrdersImportHandler(
|
||||||
|
$shopproSyncService
|
||||||
|
),
|
||||||
|
'shoppro_order_status_sync' => new ShopproStatusSyncHandler(
|
||||||
|
$shopproStatusSyncService
|
||||||
|
),
|
||||||
|
'shoppro_payment_status_sync' => new ShopproPaymentStatusSyncHandler(
|
||||||
|
$shopproPaymentSyncService
|
||||||
|
),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
$runner->run($webLimit);
|
$runner->run($webLimit);
|
||||||
|
|||||||
@@ -102,17 +102,19 @@ final class CronRepository
|
|||||||
/**
|
/**
|
||||||
* @return array<int, array<string, mixed>>
|
* @return array<int, array<string, mixed>>
|
||||||
*/
|
*/
|
||||||
public function listPastJobs(int $limit = 50): array
|
public function listPastJobs(int $limit = 50, int $offset = 0): array
|
||||||
{
|
{
|
||||||
$safeLimit = max(1, min(200, $limit));
|
$safeLimit = max(1, min(200, $limit));
|
||||||
|
$safeOffset = max(0, $offset);
|
||||||
$statement = $this->pdo->prepare(
|
$statement = $this->pdo->prepare(
|
||||||
'SELECT id, job_type, status, priority, attempts, max_attempts, scheduled_at, started_at, completed_at, last_error, created_at
|
'SELECT id, job_type, status, priority, attempts, max_attempts, scheduled_at, started_at, completed_at, last_error, created_at
|
||||||
FROM cron_jobs
|
FROM cron_jobs
|
||||||
WHERE status IN ("completed", "failed", "cancelled")
|
WHERE status IN ("completed", "failed", "cancelled")
|
||||||
ORDER BY completed_at DESC, id DESC
|
ORDER BY completed_at DESC, id DESC
|
||||||
LIMIT :limit'
|
LIMIT :limit OFFSET :offset'
|
||||||
);
|
);
|
||||||
$statement->bindValue(':limit', $safeLimit, PDO::PARAM_INT);
|
$statement->bindValue(':limit', $safeLimit, PDO::PARAM_INT);
|
||||||
|
$statement->bindValue(':offset', $safeOffset, PDO::PARAM_INT);
|
||||||
$statement->execute();
|
$statement->execute();
|
||||||
$rows = $statement->fetchAll(PDO::FETCH_ASSOC);
|
$rows = $statement->fetchAll(PDO::FETCH_ASSOC);
|
||||||
if (!is_array($rows)) {
|
if (!is_array($rows)) {
|
||||||
@@ -122,6 +124,18 @@ final class CronRepository
|
|||||||
return array_map(fn (array $row): array => $this->normalizeJobRow($row), $rows);
|
return array_map(fn (array $row): array => $this->normalizeJobRow($row), $rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function countPastJobs(): int
|
||||||
|
{
|
||||||
|
$statement = $this->pdo->query(
|
||||||
|
'SELECT COUNT(*)
|
||||||
|
FROM cron_jobs
|
||||||
|
WHERE status IN ("completed", "failed", "cancelled")'
|
||||||
|
);
|
||||||
|
$value = $statement !== false ? $statement->fetchColumn() : 0;
|
||||||
|
|
||||||
|
return max(0, (int) $value);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<int, array<string, mixed>>
|
* @return array<int, array<string, mixed>>
|
||||||
*/
|
*/
|
||||||
|
|||||||
26
src/Modules/Cron/ShopproOrdersImportHandler.php
Normal file
26
src/Modules/Cron/ShopproOrdersImportHandler.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Cron;
|
||||||
|
|
||||||
|
use App\Modules\Settings\ShopproOrdersSyncService;
|
||||||
|
|
||||||
|
final class ShopproOrdersImportHandler
|
||||||
|
{
|
||||||
|
public function __construct(private readonly ShopproOrdersSyncService $syncService)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $payload
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function handle(array $payload): array
|
||||||
|
{
|
||||||
|
return $this->syncService->sync([
|
||||||
|
'max_pages' => (int) ($payload['max_pages'] ?? 3),
|
||||||
|
'page_limit' => (int) ($payload['page_limit'] ?? 50),
|
||||||
|
'max_orders' => (int) ($payload['max_orders'] ?? 200),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/Modules/Cron/ShopproPaymentStatusSyncHandler.php
Normal file
24
src/Modules/Cron/ShopproPaymentStatusSyncHandler.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Cron;
|
||||||
|
|
||||||
|
use App\Modules\Settings\ShopproPaymentStatusSyncService;
|
||||||
|
|
||||||
|
final class ShopproPaymentStatusSyncHandler
|
||||||
|
{
|
||||||
|
public function __construct(private readonly ShopproPaymentStatusSyncService $syncService)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $payload
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function handle(array $payload): array
|
||||||
|
{
|
||||||
|
return $this->syncService->sync([
|
||||||
|
'per_integration_limit' => (int) ($payload['per_integration_limit'] ?? 100),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/Modules/Cron/ShopproStatusSyncHandler.php
Normal file
22
src/Modules/Cron/ShopproStatusSyncHandler.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Cron;
|
||||||
|
|
||||||
|
use App\Modules\Settings\ShopproStatusSyncService;
|
||||||
|
|
||||||
|
final class ShopproStatusSyncHandler
|
||||||
|
{
|
||||||
|
public function __construct(private readonly ShopproStatusSyncService $syncService)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $payload
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function handle(array $payload): array
|
||||||
|
{
|
||||||
|
return $this->syncService->sync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -538,6 +538,7 @@ final class OrdersController
|
|||||||
|
|
||||||
private function shippingHtml(string $deliveryMethod, int $shipments, int $documents): string
|
private function shippingHtml(string $deliveryMethod, int $shipments, int $documents): string
|
||||||
{
|
{
|
||||||
|
$deliveryMethod = trim(html_entity_decode(strip_tags($deliveryMethod), ENT_QUOTES | ENT_HTML5, 'UTF-8'));
|
||||||
$html = '<div class="orders-mini">';
|
$html = '<div class="orders-mini">';
|
||||||
if ($deliveryMethod !== '' && !preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $deliveryMethod)) {
|
if ($deliveryMethod !== '' && !preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $deliveryMethod)) {
|
||||||
$html .= '<div class="orders-mini__delivery">' . htmlspecialchars($deliveryMethod, ENT_QUOTES, 'UTF-8') . '</div>';
|
$html .= '<div class="orders-mini__delivery">' . htmlspecialchars($deliveryMethod, ENT_QUOTES, 'UTF-8') . '</div>';
|
||||||
|
|||||||
@@ -420,9 +420,10 @@ final class OrdersRepository
|
|||||||
$addresses = [];
|
$addresses = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$itemsMediaSql = $this->resolvedMediaUrlSql('oi');
|
$itemsMediaSql = $this->resolvedMediaUrlSql('oi', 'o.source');
|
||||||
$itemsStmt = $this->pdo->prepare('SELECT oi.*, ' . $itemsMediaSql . ' AS resolved_media_url
|
$itemsStmt = $this->pdo->prepare('SELECT oi.*, ' . $itemsMediaSql . ' AS resolved_media_url
|
||||||
FROM order_items oi
|
FROM order_items oi
|
||||||
|
INNER JOIN orders o ON o.id = oi.order_id
|
||||||
WHERE oi.order_id = :order_id
|
WHERE oi.order_id = :order_id
|
||||||
ORDER BY oi.sort_order ASC, oi.id ASC');
|
ORDER BY oi.sort_order ASC, oi.id ASC');
|
||||||
$itemsStmt->execute(['order_id' => $orderId]);
|
$itemsStmt->execute(['order_id' => $orderId]);
|
||||||
@@ -513,9 +514,10 @@ final class OrdersRepository
|
|||||||
|
|
||||||
$placeholders = implode(',', array_fill(0, count($cleanIds), '?'));
|
$placeholders = implode(',', array_fill(0, count($cleanIds), '?'));
|
||||||
try {
|
try {
|
||||||
$resolvedMediaSql = $this->resolvedMediaUrlSql('oi');
|
$resolvedMediaSql = $this->resolvedMediaUrlSql('oi', 'o.source');
|
||||||
$sql = 'SELECT oi.order_id, oi.original_name, oi.quantity, ' . $resolvedMediaSql . ' AS media_url, oi.sort_order, oi.id
|
$sql = 'SELECT oi.order_id, oi.original_name, oi.quantity, ' . $resolvedMediaSql . ' AS media_url, oi.sort_order, oi.id
|
||||||
FROM order_items oi
|
FROM order_items oi
|
||||||
|
INNER JOIN orders o ON o.id = oi.order_id
|
||||||
WHERE oi.order_id IN (' . $placeholders . ')
|
WHERE oi.order_id IN (' . $placeholders . ')
|
||||||
ORDER BY oi.order_id ASC, oi.sort_order ASC, oi.id ASC';
|
ORDER BY oi.order_id ASC, oi.sort_order ASC, oi.id ASC';
|
||||||
$stmt = $this->pdo->prepare($sql);
|
$stmt = $this->pdo->prepare($sql);
|
||||||
@@ -574,7 +576,7 @@ final class OrdersRepository
|
|||||||
. ')';
|
. ')';
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resolvedMediaUrlSql(string $itemAlias): string
|
private function resolvedMediaUrlSql(string $itemAlias, string $sourceAlias = '"allegro"'): string
|
||||||
{
|
{
|
||||||
if (!$this->canResolveMappedMedia()) {
|
if (!$this->canResolveMappedMedia()) {
|
||||||
return 'COALESCE(NULLIF(TRIM(' . $itemAlias . '.media_url), ""), "")';
|
return 'COALESCE(NULLIF(TRIM(' . $itemAlias . '.media_url), ""), "")';
|
||||||
@@ -587,7 +589,7 @@ final class OrdersRepository
|
|||||||
FROM product_channel_map pcm
|
FROM product_channel_map pcm
|
||||||
INNER JOIN sales_channels sc ON sc.id = pcm.channel_id
|
INNER JOIN sales_channels sc ON sc.id = pcm.channel_id
|
||||||
INNER JOIN product_images pi ON pi.product_id = pcm.product_id
|
INNER JOIN product_images pi ON pi.product_id = pcm.product_id
|
||||||
WHERE LOWER(sc.code) = "allegro"
|
WHERE LOWER(sc.code) = LOWER(' . $sourceAlias . ')
|
||||||
AND (
|
AND (
|
||||||
pcm.external_product_id = ' . $itemAlias . '.external_item_id
|
pcm.external_product_id = ' . $itemAlias . '.external_item_id
|
||||||
OR pcm.external_product_id = ' . $itemAlias . '.source_product_id
|
OR pcm.external_product_id = ' . $itemAlias . '.source_product_id
|
||||||
|
|||||||
@@ -111,34 +111,35 @@ final class AllegroIntegrationController
|
|||||||
|
|
||||||
public function save(Request $request): Response
|
public function save(Request $request): Response
|
||||||
{
|
{
|
||||||
$csrfError = $this->validateCsrf((string) $request->input('_token', ''));
|
$redirectTo = $this->resolveRedirectPath((string) $request->input('return_to', '/settings/integrations/allegro'));
|
||||||
if ($csrfError !== null) {
|
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||||
return $csrfError;
|
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
$environment = trim((string) $request->input('environment', 'sandbox'));
|
$environment = trim((string) $request->input('environment', 'sandbox'));
|
||||||
if (!in_array($environment, ['sandbox', 'production'], true)) {
|
if (!in_array($environment, ['sandbox', 'production'], true)) {
|
||||||
Flash::set('settings_error', $this->translator->get('settings.allegro.validation.environment_invalid'));
|
Flash::set('settings_error', $this->translator->get('settings.allegro.validation.environment_invalid'));
|
||||||
return Response::redirect('/settings/integrations/allegro');
|
return Response::redirect($redirectTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
$clientId = trim((string) $request->input('client_id', ''));
|
$clientId = trim((string) $request->input('client_id', ''));
|
||||||
if ($clientId !== '' && mb_strlen($clientId) > 128) {
|
if ($clientId !== '' && mb_strlen($clientId) > 128) {
|
||||||
Flash::set('settings_error', $this->translator->get('settings.allegro.validation.client_id_too_long'));
|
Flash::set('settings_error', $this->translator->get('settings.allegro.validation.client_id_too_long'));
|
||||||
return Response::redirect('/settings/integrations/allegro');
|
return Response::redirect($redirectTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
$redirectUriInput = trim((string) $request->input('redirect_uri', ''));
|
$redirectUriInput = trim((string) $request->input('redirect_uri', ''));
|
||||||
$redirectUri = $redirectUriInput !== '' ? $redirectUriInput : $this->defaultRedirectUri();
|
$redirectUri = $redirectUriInput !== '' ? $redirectUriInput : $this->defaultRedirectUri();
|
||||||
if (!$this->isValidHttpUrl($redirectUri)) {
|
if (!$this->isValidHttpUrl($redirectUri)) {
|
||||||
Flash::set('settings_error', $this->translator->get('settings.allegro.validation.redirect_uri_invalid'));
|
Flash::set('settings_error', $this->translator->get('settings.allegro.validation.redirect_uri_invalid'));
|
||||||
return Response::redirect('/settings/integrations/allegro');
|
return Response::redirect($redirectTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
$ordersFetchStartDate = trim((string) $request->input('orders_fetch_start_date', ''));
|
$ordersFetchStartDate = trim((string) $request->input('orders_fetch_start_date', ''));
|
||||||
if ($ordersFetchStartDate !== '' && !$this->isValidDate($ordersFetchStartDate)) {
|
if ($ordersFetchStartDate !== '' && !$this->isValidDate($ordersFetchStartDate)) {
|
||||||
Flash::set('settings_error', $this->translator->get('settings.allegro.validation.orders_fetch_start_date_invalid'));
|
Flash::set('settings_error', $this->translator->get('settings.allegro.validation.orders_fetch_start_date_invalid'));
|
||||||
return Response::redirect('/settings/integrations/allegro');
|
return Response::redirect($redirectTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -159,7 +160,7 @@ final class AllegroIntegrationController
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response::redirect('/settings/integrations/allegro');
|
return Response::redirect($redirectTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function saveImportSettings(Request $request): Response
|
public function saveImportSettings(Request $request): Response
|
||||||
@@ -649,6 +650,19 @@ final class AllegroIntegrationController
|
|||||||
return Response::redirect('/settings/integrations/allegro');
|
return Response::redirect('/settings/integrations/allegro');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function resolveRedirectPath(string $candidate): string
|
||||||
|
{
|
||||||
|
$value = trim($candidate);
|
||||||
|
if ($value === '') {
|
||||||
|
return '/settings/integrations/allegro';
|
||||||
|
}
|
||||||
|
if (!str_starts_with($value, '/settings/integrations')) {
|
||||||
|
return '/settings/integrations/allegro';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string, string>
|
* @return array<string, string>
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -10,11 +10,18 @@ use Throwable;
|
|||||||
final class AllegroIntegrationRepository
|
final class AllegroIntegrationRepository
|
||||||
{
|
{
|
||||||
private const DEFAULT_ENVIRONMENT = 'sandbox';
|
private const DEFAULT_ENVIRONMENT = 'sandbox';
|
||||||
|
private const INTEGRATION_TYPE = 'allegro';
|
||||||
|
|
||||||
|
private readonly IntegrationsRepository $integrations;
|
||||||
|
private readonly IntegrationSecretCipher $cipher;
|
||||||
|
private ?bool $hasIntegrationIdColumn = null;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly PDO $pdo,
|
private readonly PDO $pdo,
|
||||||
private readonly string $secret
|
private readonly string $secret
|
||||||
) {
|
) {
|
||||||
|
$this->integrations = new IntegrationsRepository($this->pdo);
|
||||||
|
$this->cipher = new IntegrationSecretCipher($this->secret);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,6 +37,7 @@ final class AllegroIntegrationRepository
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
'environment' => $this->normalizeEnvironment((string) ($row['environment'] ?? self::DEFAULT_ENVIRONMENT)),
|
'environment' => $this->normalizeEnvironment((string) ($row['environment'] ?? self::DEFAULT_ENVIRONMENT)),
|
||||||
|
'integration_id' => (int) ($row['integration_id'] ?? 0),
|
||||||
'client_id' => trim((string) ($row['client_id'] ?? '')),
|
'client_id' => trim((string) ($row['client_id'] ?? '')),
|
||||||
'has_client_secret' => trim((string) ($row['client_secret_encrypted'] ?? '')) !== '',
|
'has_client_secret' => trim((string) ($row['client_secret_encrypted'] ?? '')) !== '',
|
||||||
'redirect_uri' => trim((string) ($row['redirect_uri'] ?? '')),
|
'redirect_uri' => trim((string) ($row['redirect_uri'] ?? '')),
|
||||||
@@ -60,6 +68,21 @@ final class AllegroIntegrationRepository
|
|||||||
return $this->normalizeEnvironment(trim((string) ($row['setting_value'] ?? self::DEFAULT_ENVIRONMENT)));
|
return $this->normalizeEnvironment(trim((string) ($row['setting_value'] ?? self::DEFAULT_ENVIRONMENT)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getActiveIntegrationId(): int
|
||||||
|
{
|
||||||
|
$environment = $this->getActiveEnvironment();
|
||||||
|
$row = $this->fetchRowByEnv($environment);
|
||||||
|
$rowIntegrationId = (int) ($row['integration_id'] ?? 0);
|
||||||
|
if ($rowIntegrationId > 0) {
|
||||||
|
return $rowIntegrationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
$integrationId = $this->ensureBaseIntegration($environment);
|
||||||
|
$this->assignIntegrationIdIfPossible($environment, $integrationId);
|
||||||
|
|
||||||
|
return $integrationId;
|
||||||
|
}
|
||||||
|
|
||||||
public function setActiveEnvironment(string $environment): void
|
public function setActiveEnvironment(string $environment): void
|
||||||
{
|
{
|
||||||
$env = $this->normalizeEnvironment($environment);
|
$env = $this->normalizeEnvironment($environment);
|
||||||
@@ -86,7 +109,7 @@ final class AllegroIntegrationRepository
|
|||||||
$clientSecret = trim((string) ($payload['client_secret'] ?? ''));
|
$clientSecret = trim((string) ($payload['client_secret'] ?? ''));
|
||||||
$clientSecretEncrypted = trim((string) ($current['client_secret_encrypted'] ?? ''));
|
$clientSecretEncrypted = trim((string) ($current['client_secret_encrypted'] ?? ''));
|
||||||
if ($clientSecret !== '') {
|
if ($clientSecret !== '') {
|
||||||
$clientSecretEncrypted = (string) $this->encrypt($clientSecret);
|
$clientSecretEncrypted = (string) $this->cipher->encrypt($clientSecret);
|
||||||
}
|
}
|
||||||
|
|
||||||
$statement = $this->pdo->prepare(
|
$statement = $this->pdo->prepare(
|
||||||
@@ -123,7 +146,7 @@ final class AllegroIntegrationRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
$clientId = trim((string) ($row['client_id'] ?? ''));
|
$clientId = trim((string) ($row['client_id'] ?? ''));
|
||||||
$clientSecret = $this->decrypt((string) ($row['client_secret_encrypted'] ?? ''));
|
$clientSecret = (string) $this->cipher->decrypt((string) ($row['client_secret_encrypted'] ?? ''));
|
||||||
$redirectUri = trim((string) ($row['redirect_uri'] ?? ''));
|
$redirectUri = trim((string) ($row['redirect_uri'] ?? ''));
|
||||||
if ($clientId === '' || $clientSecret === '' || $redirectUri === '') {
|
if ($clientId === '' || $clientSecret === '' || $redirectUri === '') {
|
||||||
return null;
|
return null;
|
||||||
@@ -160,8 +183,8 @@ final class AllegroIntegrationRepository
|
|||||||
);
|
);
|
||||||
$statement->execute([
|
$statement->execute([
|
||||||
'environment' => $env,
|
'environment' => $env,
|
||||||
'access_token_encrypted' => $this->encrypt($accessToken),
|
'access_token_encrypted' => $this->cipher->encrypt($accessToken),
|
||||||
'refresh_token_encrypted' => $this->encrypt($refreshToken),
|
'refresh_token_encrypted' => $this->cipher->encrypt($refreshToken),
|
||||||
'token_type' => $this->nullableString($tokenType),
|
'token_type' => $this->nullableString($tokenType),
|
||||||
'token_scope' => $this->nullableString($scope),
|
'token_scope' => $this->nullableString($scope),
|
||||||
'token_expires_at' => $this->nullableString((string) $tokenExpiresAt),
|
'token_expires_at' => $this->nullableString((string) $tokenExpiresAt),
|
||||||
@@ -180,8 +203,8 @@ final class AllegroIntegrationRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
$clientId = trim((string) ($row['client_id'] ?? ''));
|
$clientId = trim((string) ($row['client_id'] ?? ''));
|
||||||
$clientSecret = $this->decrypt((string) ($row['client_secret_encrypted'] ?? ''));
|
$clientSecret = (string) $this->cipher->decrypt((string) ($row['client_secret_encrypted'] ?? ''));
|
||||||
$refreshToken = $this->decrypt((string) ($row['refresh_token_encrypted'] ?? ''));
|
$refreshToken = (string) $this->cipher->decrypt((string) ($row['refresh_token_encrypted'] ?? ''));
|
||||||
if ($clientId === '' || $clientSecret === '' || $refreshToken === '') {
|
if ($clientId === '' || $clientSecret === '' || $refreshToken === '') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -206,9 +229,9 @@ final class AllegroIntegrationRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
$clientId = trim((string) ($row['client_id'] ?? ''));
|
$clientId = trim((string) ($row['client_id'] ?? ''));
|
||||||
$clientSecret = $this->decrypt((string) ($row['client_secret_encrypted'] ?? ''));
|
$clientSecret = (string) $this->cipher->decrypt((string) ($row['client_secret_encrypted'] ?? ''));
|
||||||
$refreshToken = $this->decrypt((string) ($row['refresh_token_encrypted'] ?? ''));
|
$refreshToken = (string) $this->cipher->decrypt((string) ($row['refresh_token_encrypted'] ?? ''));
|
||||||
$accessToken = $this->decrypt((string) ($row['access_token_encrypted'] ?? ''));
|
$accessToken = (string) $this->cipher->decrypt((string) ($row['access_token_encrypted'] ?? ''));
|
||||||
if ($clientId === '' || $clientSecret === '' || $refreshToken === '') {
|
if ($clientId === '' || $clientSecret === '' || $refreshToken === '') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -226,6 +249,26 @@ final class AllegroIntegrationRepository
|
|||||||
private function ensureRow(string $environment): void
|
private function ensureRow(string $environment): void
|
||||||
{
|
{
|
||||||
$env = $this->normalizeEnvironment($environment);
|
$env = $this->normalizeEnvironment($environment);
|
||||||
|
$integrationId = $this->ensureBaseIntegration($env);
|
||||||
|
|
||||||
|
if ($this->hasIntegrationIdColumn()) {
|
||||||
|
$statement = $this->pdo->prepare(
|
||||||
|
'INSERT INTO allegro_integration_settings (
|
||||||
|
integration_id, environment, orders_fetch_enabled, created_at, updated_at
|
||||||
|
) VALUES (
|
||||||
|
:integration_id, :environment, 0, NOW(), NOW()
|
||||||
|
)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
updated_at = updated_at'
|
||||||
|
);
|
||||||
|
$statement->execute([
|
||||||
|
'integration_id' => $integrationId,
|
||||||
|
'environment' => $env,
|
||||||
|
]);
|
||||||
|
$this->assignIntegrationIdIfPossible($env, $integrationId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$statement = $this->pdo->prepare(
|
$statement = $this->pdo->prepare(
|
||||||
'INSERT INTO allegro_integration_settings (
|
'INSERT INTO allegro_integration_settings (
|
||||||
environment, orders_fetch_enabled, created_at, updated_at
|
environment, orders_fetch_enabled, created_at, updated_at
|
||||||
@@ -240,6 +283,79 @@ final class AllegroIntegrationRepository
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function ensureBaseIntegration(string $environment): int
|
||||||
|
{
|
||||||
|
$env = $this->normalizeEnvironment($environment);
|
||||||
|
return $this->integrations->ensureIntegration(
|
||||||
|
self::INTEGRATION_TYPE,
|
||||||
|
$this->integrationNameForEnvironment($env),
|
||||||
|
$this->integrationBaseUrlForEnvironment($env),
|
||||||
|
20,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function assignIntegrationIdIfPossible(string $environment, int $integrationId): void
|
||||||
|
{
|
||||||
|
if ($integrationId <= 0 || !$this->hasIntegrationIdColumn()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$statement = $this->pdo->prepare(
|
||||||
|
'UPDATE allegro_integration_settings
|
||||||
|
SET integration_id = :integration_id,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE environment = :environment
|
||||||
|
AND (integration_id IS NULL OR integration_id = 0)'
|
||||||
|
);
|
||||||
|
$statement->execute([
|
||||||
|
'integration_id' => $integrationId,
|
||||||
|
'environment' => $this->normalizeEnvironment($environment),
|
||||||
|
]);
|
||||||
|
} catch (Throwable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function hasIntegrationIdColumn(): bool
|
||||||
|
{
|
||||||
|
if ($this->hasIntegrationIdColumn !== null) {
|
||||||
|
return $this->hasIntegrationIdColumn;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$statement = $this->pdo->prepare(
|
||||||
|
"SELECT 1
|
||||||
|
FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'allegro_integration_settings'
|
||||||
|
AND COLUMN_NAME = 'integration_id'
|
||||||
|
LIMIT 1"
|
||||||
|
);
|
||||||
|
$statement->execute();
|
||||||
|
$value = $statement->fetchColumn();
|
||||||
|
} catch (Throwable) {
|
||||||
|
$this->hasIntegrationIdColumn = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->hasIntegrationIdColumn = $value !== false;
|
||||||
|
return $this->hasIntegrationIdColumn;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function integrationNameForEnvironment(string $environment): string
|
||||||
|
{
|
||||||
|
return $environment === 'production' ? 'Allegro Production' : 'Allegro Sandbox';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function integrationBaseUrlForEnvironment(string $environment): string
|
||||||
|
{
|
||||||
|
return $environment === 'production'
|
||||||
|
? 'https://api.allegro.pl'
|
||||||
|
: 'https://api.allegro.pl.allegrosandbox.pl';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string, mixed>|null
|
* @return array<string, mixed>|null
|
||||||
*/
|
*/
|
||||||
@@ -274,6 +390,7 @@ final class AllegroIntegrationRepository
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'environment' => $this->normalizeEnvironment($environment),
|
'environment' => $this->normalizeEnvironment($environment),
|
||||||
|
'integration_id' => 0,
|
||||||
'client_id' => '',
|
'client_id' => '',
|
||||||
'has_client_secret' => false,
|
'has_client_secret' => false,
|
||||||
'redirect_uri' => '',
|
'redirect_uri' => '',
|
||||||
@@ -313,60 +430,4 @@ final class AllegroIntegrationRepository
|
|||||||
$trimmed = trim($value);
|
$trimmed = trim($value);
|
||||||
return $trimmed === '' ? null : $trimmed;
|
return $trimmed === '' ? null : $trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function encrypt(string $plainText): ?string
|
|
||||||
{
|
|
||||||
$value = trim($plainText);
|
|
||||||
if ($value === '') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if ($this->secret === '') {
|
|
||||||
throw new RuntimeException('Brak INTEGRATIONS_SECRET do szyfrowania danych integracji.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$encryptionKey = hash('sha256', 'enc|' . $this->secret, true);
|
|
||||||
$hmacKey = hash('sha256', 'auth|' . $this->secret, true);
|
|
||||||
$iv = random_bytes(16);
|
|
||||||
$cipherRaw = openssl_encrypt($value, 'AES-256-CBC', $encryptionKey, OPENSSL_RAW_DATA, $iv);
|
|
||||||
if ($cipherRaw === false) {
|
|
||||||
throw new RuntimeException('Nie udalo sie zaszyfrowac danych integracji.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$mac = hash_hmac('sha256', $iv . $cipherRaw, $hmacKey, true);
|
|
||||||
return 'v1:' . base64_encode($iv . $mac . $cipherRaw);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function decrypt(string $encryptedValue): string
|
|
||||||
{
|
|
||||||
$payload = trim($encryptedValue);
|
|
||||||
if ($payload === '') {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
if ($this->secret === '') {
|
|
||||||
throw new RuntimeException('Brak INTEGRATIONS_SECRET do odszyfrowania danych integracji.');
|
|
||||||
}
|
|
||||||
if (!str_starts_with($payload, 'v1:')) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
$raw = base64_decode(substr($payload, 3), true);
|
|
||||||
if ($raw === false || strlen($raw) <= 48) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
$iv = substr($raw, 0, 16);
|
|
||||||
$mac = substr($raw, 16, 32);
|
|
||||||
$cipherRaw = substr($raw, 48);
|
|
||||||
|
|
||||||
$hmacKey = hash('sha256', 'auth|' . $this->secret, true);
|
|
||||||
$expectedMac = hash_hmac('sha256', $iv . $cipherRaw, $hmacKey, true);
|
|
||||||
if (!hash_equals($expectedMac, $mac)) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
$encryptionKey = hash('sha256', 'enc|' . $this->secret, true);
|
|
||||||
$plain = openssl_decrypt($cipherRaw, 'AES-256-CBC', $encryptionKey, OPENSSL_RAW_DATA, $iv);
|
|
||||||
|
|
||||||
return is_string($plain) ? $plain : '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ final class AllegroOrderImportService
|
|||||||
$fetchedAt = date('Y-m-d H:i:s');
|
$fetchedAt = date('Y-m-d H:i:s');
|
||||||
|
|
||||||
$order = [
|
$order = [
|
||||||
'integration_id' => null,
|
'integration_id' => $this->integrationRepository->getActiveIntegrationId(),
|
||||||
'source' => 'allegro',
|
'source' => 'allegro',
|
||||||
'source_order_id' => $checkoutFormId,
|
'source_order_id' => $checkoutFormId,
|
||||||
'external_order_id' => $checkoutFormId,
|
'external_order_id' => $checkoutFormId,
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ use Throwable;
|
|||||||
|
|
||||||
final class AllegroOrdersSyncService
|
final class AllegroOrdersSyncService
|
||||||
{
|
{
|
||||||
private const ALLEGRO_INTEGRATION_ID = 1;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly AllegroIntegrationRepository $integrationRepository,
|
private readonly AllegroIntegrationRepository $integrationRepository,
|
||||||
private readonly AllegroOrderSyncStateRepository $syncStateRepository,
|
private readonly AllegroOrderSyncStateRepository $syncStateRepository,
|
||||||
@@ -42,9 +40,14 @@ final class AllegroOrdersSyncService
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$integrationId = $this->integrationRepository->getActiveIntegrationId();
|
||||||
|
if ($integrationId <= 0) {
|
||||||
|
throw new RuntimeException('Brak aktywnej integracji bazowej Allegro.');
|
||||||
|
}
|
||||||
|
|
||||||
$now = new DateTimeImmutable('now');
|
$now = new DateTimeImmutable('now');
|
||||||
$state = $this->syncStateRepository->getState(self::ALLEGRO_INTEGRATION_ID);
|
$state = $this->syncStateRepository->getState($integrationId);
|
||||||
$this->syncStateRepository->markRunStarted(self::ALLEGRO_INTEGRATION_ID, $now);
|
$this->syncStateRepository->markRunStarted($integrationId, $now);
|
||||||
|
|
||||||
$maxPages = max(1, min(20, (int) ($options['max_pages'] ?? 5)));
|
$maxPages = max(1, min(20, (int) ($options['max_pages'] ?? 5)));
|
||||||
$pageLimit = max(1, min(100, (int) ($options['page_limit'] ?? 50)));
|
$pageLimit = max(1, min(100, (int) ($options['page_limit'] ?? 50)));
|
||||||
@@ -171,7 +174,7 @@ final class AllegroOrdersSyncService
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->syncStateRepository->markRunSuccess(
|
$this->syncStateRepository->markRunSuccess(
|
||||||
self::ALLEGRO_INTEGRATION_ID,
|
$integrationId,
|
||||||
new DateTimeImmutable('now'),
|
new DateTimeImmutable('now'),
|
||||||
$latestProcessedUpdatedAt,
|
$latestProcessedUpdatedAt,
|
||||||
$latestProcessedSourceOrderId
|
$latestProcessedSourceOrderId
|
||||||
@@ -181,7 +184,7 @@ final class AllegroOrdersSyncService
|
|||||||
return $result;
|
return $result;
|
||||||
} catch (Throwable $exception) {
|
} catch (Throwable $exception) {
|
||||||
$this->syncStateRepository->markRunFailed(
|
$this->syncStateRepository->markRunFailed(
|
||||||
self::ALLEGRO_INTEGRATION_ID,
|
$integrationId,
|
||||||
new DateTimeImmutable('now'),
|
new DateTimeImmutable('now'),
|
||||||
$exception->getMessage()
|
$exception->getMessage()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -42,15 +42,17 @@ final class ApaczkaIntegrationController
|
|||||||
|
|
||||||
public function save(Request $request): Response
|
public function save(Request $request): Response
|
||||||
{
|
{
|
||||||
|
$redirectTo = $this->resolveRedirectPath((string) $request->input('return_to', '/settings/integrations/apaczka'));
|
||||||
|
|
||||||
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||||
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
|
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
|
||||||
return Response::redirect('/settings/integrations/apaczka');
|
return Response::redirect($redirectTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
$apiKey = trim((string) $request->input('api_key', ''));
|
$apiKey = trim((string) $request->input('api_key', ''));
|
||||||
if ($apiKey === '') {
|
if ($apiKey === '') {
|
||||||
Flash::set('settings_error', $this->translator->get('settings.apaczka.validation.api_key_required'));
|
Flash::set('settings_error', $this->translator->get('settings.apaczka.validation.api_key_required'));
|
||||||
return Response::redirect('/settings/integrations/apaczka');
|
return Response::redirect($redirectTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -65,6 +67,19 @@ final class ApaczkaIntegrationController
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response::redirect('/settings/integrations/apaczka');
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveRedirectPath(string $candidate): string
|
||||||
|
{
|
||||||
|
$value = trim($candidate);
|
||||||
|
if ($value === '') {
|
||||||
|
return '/settings/integrations/apaczka';
|
||||||
|
}
|
||||||
|
if (!str_starts_with($value, '/settings/integrations')) {
|
||||||
|
return '/settings/integrations/apaczka';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,22 @@ declare(strict_types=1);
|
|||||||
namespace App\Modules\Settings;
|
namespace App\Modules\Settings;
|
||||||
|
|
||||||
use PDO;
|
use PDO;
|
||||||
use RuntimeException;
|
|
||||||
use Throwable;
|
|
||||||
|
|
||||||
final class ApaczkaIntegrationRepository
|
final class ApaczkaIntegrationRepository
|
||||||
{
|
{
|
||||||
|
private const INTEGRATION_TYPE = 'apaczka';
|
||||||
|
private const INTEGRATION_NAME = 'Apaczka';
|
||||||
|
private const INTEGRATION_BASE_URL = 'https://www.apaczka.pl';
|
||||||
|
|
||||||
|
private readonly IntegrationsRepository $integrations;
|
||||||
|
private readonly IntegrationSecretCipher $cipher;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly PDO $pdo,
|
private readonly PDO $pdo,
|
||||||
private readonly string $secret
|
private readonly string $secret
|
||||||
) {
|
) {
|
||||||
|
$this->integrations = new IntegrationsRepository($this->pdo);
|
||||||
|
$this->cipher = new IntegrationSecretCipher($this->secret);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -20,13 +27,11 @@ final class ApaczkaIntegrationRepository
|
|||||||
*/
|
*/
|
||||||
public function getSettings(): array
|
public function getSettings(): array
|
||||||
{
|
{
|
||||||
$row = $this->fetchRow();
|
$integrationId = $this->ensureBaseIntegration();
|
||||||
if ($row === null) {
|
$integration = $this->integrations->findById($integrationId);
|
||||||
return $this->defaultSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'has_api_key' => trim((string) ($row['api_key_encrypted'] ?? '')) !== '',
|
'has_api_key' => trim((string) ($integration['api_key_encrypted'] ?? '')) !== '',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,90 +40,25 @@ final class ApaczkaIntegrationRepository
|
|||||||
*/
|
*/
|
||||||
public function saveSettings(array $payload): void
|
public function saveSettings(array $payload): void
|
||||||
{
|
{
|
||||||
$this->ensureRow();
|
$integrationId = $this->ensureBaseIntegration();
|
||||||
$current = $this->fetchRow();
|
|
||||||
if ($current === null) {
|
|
||||||
throw new RuntimeException('Brak rekordu konfiguracji Apaczka.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$apiKey = trim((string) ($payload['api_key'] ?? ''));
|
$apiKey = trim((string) ($payload['api_key'] ?? ''));
|
||||||
$apiKeyEncrypted = trim((string) ($current['api_key_encrypted'] ?? ''));
|
if ($apiKey === '') {
|
||||||
if ($apiKey !== '') {
|
return;
|
||||||
$apiKeyEncrypted = (string) $this->encrypt($apiKey);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$statement = $this->pdo->prepare(
|
$encrypted = $this->cipher->encrypt($apiKey);
|
||||||
'UPDATE apaczka_integration_settings
|
$this->integrations->updateApiKeyEncrypted($integrationId, $encrypted);
|
||||||
SET api_key_encrypted = :api_key_encrypted,
|
}
|
||||||
updated_at = NOW()
|
|
||||||
WHERE id = 1'
|
private function ensureBaseIntegration(): int
|
||||||
|
{
|
||||||
|
return $this->integrations->ensureIntegration(
|
||||||
|
self::INTEGRATION_TYPE,
|
||||||
|
self::INTEGRATION_NAME,
|
||||||
|
self::INTEGRATION_BASE_URL,
|
||||||
|
20,
|
||||||
|
true
|
||||||
);
|
);
|
||||||
$statement->execute([
|
|
||||||
'api_key_encrypted' => $this->nullableString($apiKeyEncrypted),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function ensureRow(): void
|
|
||||||
{
|
|
||||||
$statement = $this->pdo->prepare(
|
|
||||||
'INSERT INTO apaczka_integration_settings (id, created_at, updated_at)
|
|
||||||
VALUES (1, NOW(), NOW())
|
|
||||||
ON DUPLICATE KEY UPDATE updated_at = VALUES(updated_at)'
|
|
||||||
);
|
|
||||||
$statement->execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, mixed>|null
|
|
||||||
*/
|
|
||||||
private function fetchRow(): ?array
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$statement = $this->pdo->prepare('SELECT * FROM apaczka_integration_settings WHERE id = 1 LIMIT 1');
|
|
||||||
$statement->execute();
|
|
||||||
$row = $statement->fetch(PDO::FETCH_ASSOC);
|
|
||||||
} catch (Throwable) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return is_array($row) ? $row : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
private function defaultSettings(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'has_api_key' => false,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function nullableString(string $value): ?string
|
|
||||||
{
|
|
||||||
$trimmed = trim($value);
|
|
||||||
return $trimmed === '' ? null : $trimmed;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function encrypt(string $plainText): ?string
|
|
||||||
{
|
|
||||||
$value = trim($plainText);
|
|
||||||
if ($value === '') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if ($this->secret === '') {
|
|
||||||
throw new RuntimeException('Brak INTEGRATIONS_SECRET do szyfrowania danych integracji.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$encryptionKey = hash('sha256', 'enc|' . $this->secret, true);
|
|
||||||
$hmacKey = hash('sha256', 'auth|' . $this->secret, true);
|
|
||||||
$iv = random_bytes(16);
|
|
||||||
$cipherRaw = openssl_encrypt($value, 'AES-256-CBC', $encryptionKey, OPENSSL_RAW_DATA, $iv);
|
|
||||||
if ($cipherRaw === false) {
|
|
||||||
throw new RuntimeException('Nie udalo sie zaszyfrowac danych integracji.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$mac = hash_hmac('sha256', $iv . $cipherRaw, $hmacKey, true);
|
|
||||||
return 'v1:' . base64_encode($iv . $mac . $cipherRaw);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ use Throwable;
|
|||||||
|
|
||||||
final class CronSettingsController
|
final class CronSettingsController
|
||||||
{
|
{
|
||||||
|
private const PAST_JOBS_PER_PAGE = 25;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly Template $template,
|
private readonly Template $template,
|
||||||
private readonly Translator $translator,
|
private readonly Translator $translator,
|
||||||
@@ -27,12 +29,20 @@ final class CronSettingsController
|
|||||||
|
|
||||||
public function index(Request $request): Response
|
public function index(Request $request): Response
|
||||||
{
|
{
|
||||||
|
$pastPage = max(1, (int) $request->input('past_page', 1));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$runOnWeb = $this->cronRepository->getBoolSetting('cron_run_on_web', $this->runOnWebDefault);
|
$runOnWeb = $this->cronRepository->getBoolSetting('cron_run_on_web', $this->runOnWebDefault);
|
||||||
$webLimit = $this->cronRepository->getIntSetting('cron_web_limit', $this->webLimitDefault, 1, 100);
|
$webLimit = $this->cronRepository->getIntSetting('cron_web_limit', $this->webLimitDefault, 1, 100);
|
||||||
$schedules = $this->cronRepository->listSchedules();
|
$schedules = $this->cronRepository->listSchedules();
|
||||||
$futureJobs = $this->cronRepository->listFutureJobs(60);
|
$futureJobs = $this->cronRepository->listFutureJobs(60);
|
||||||
$pastJobs = $this->cronRepository->listPastJobs(60);
|
$pastJobsTotal = $this->cronRepository->countPastJobs();
|
||||||
|
$pastTotalPages = max(1, (int) ceil($pastJobsTotal / self::PAST_JOBS_PER_PAGE));
|
||||||
|
if ($pastPage > $pastTotalPages) {
|
||||||
|
$pastPage = $pastTotalPages;
|
||||||
|
}
|
||||||
|
$pastOffset = ($pastPage - 1) * self::PAST_JOBS_PER_PAGE;
|
||||||
|
$pastJobs = $this->cronRepository->listPastJobs(self::PAST_JOBS_PER_PAGE, $pastOffset);
|
||||||
} catch (Throwable $exception) {
|
} catch (Throwable $exception) {
|
||||||
Flash::set('settings_error', $this->translator->get('settings.cron.flash.load_failed') . ' ' . $exception->getMessage());
|
Flash::set('settings_error', $this->translator->get('settings.cron.flash.load_failed') . ' ' . $exception->getMessage());
|
||||||
$runOnWeb = $this->runOnWebDefault;
|
$runOnWeb = $this->runOnWebDefault;
|
||||||
@@ -40,6 +50,9 @@ final class CronSettingsController
|
|||||||
$schedules = [];
|
$schedules = [];
|
||||||
$futureJobs = [];
|
$futureJobs = [];
|
||||||
$pastJobs = [];
|
$pastJobs = [];
|
||||||
|
$pastJobsTotal = 0;
|
||||||
|
$pastTotalPages = 1;
|
||||||
|
$pastPage = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
$html = $this->template->render('settings/cron', [
|
$html = $this->template->render('settings/cron', [
|
||||||
@@ -53,6 +66,12 @@ final class CronSettingsController
|
|||||||
'schedules' => $schedules,
|
'schedules' => $schedules,
|
||||||
'futureJobs' => $futureJobs,
|
'futureJobs' => $futureJobs,
|
||||||
'pastJobs' => $pastJobs,
|
'pastJobs' => $pastJobs,
|
||||||
|
'pastJobsPagination' => [
|
||||||
|
'page' => $pastPage,
|
||||||
|
'per_page' => self::PAST_JOBS_PER_PAGE,
|
||||||
|
'total' => $pastJobsTotal,
|
||||||
|
'total_pages' => $pastTotalPages,
|
||||||
|
],
|
||||||
'errorMessage' => (string) Flash::get('settings_error', ''),
|
'errorMessage' => (string) Flash::get('settings_error', ''),
|
||||||
'successMessage' => (string) Flash::get('settings_success', ''),
|
'successMessage' => (string) Flash::get('settings_success', ''),
|
||||||
], 'layouts/app');
|
], 'layouts/app');
|
||||||
|
|||||||
@@ -42,9 +42,11 @@ final class InpostIntegrationController
|
|||||||
|
|
||||||
public function save(Request $request): Response
|
public function save(Request $request): Response
|
||||||
{
|
{
|
||||||
|
$redirectTo = $this->resolveRedirectPath((string) $request->input('return_to', '/settings/integrations/inpost'));
|
||||||
|
|
||||||
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||||
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
|
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
|
||||||
return Response::redirect('/settings/integrations/inpost');
|
return Response::redirect($redirectTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -72,6 +74,19 @@ final class InpostIntegrationController
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response::redirect('/settings/integrations/inpost');
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveRedirectPath(string $candidate): string
|
||||||
|
{
|
||||||
|
$value = trim($candidate);
|
||||||
|
if ($value === '') {
|
||||||
|
return '/settings/integrations/inpost';
|
||||||
|
}
|
||||||
|
if (!str_starts_with($value, '/settings/integrations')) {
|
||||||
|
return '/settings/integrations/inpost';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,19 @@ use Throwable;
|
|||||||
|
|
||||||
final class InpostIntegrationRepository
|
final class InpostIntegrationRepository
|
||||||
{
|
{
|
||||||
|
private const INTEGRATION_TYPE = 'inpost';
|
||||||
|
private const INTEGRATION_NAME = 'InPost ShipX';
|
||||||
|
private const INTEGRATION_BASE_URL = 'https://api-shipx-pl.easypack24.net';
|
||||||
|
|
||||||
|
private readonly IntegrationsRepository $integrations;
|
||||||
|
private readonly IntegrationSecretCipher $cipher;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly PDO $pdo,
|
private readonly PDO $pdo,
|
||||||
private readonly string $secret
|
private readonly string $secret
|
||||||
) {
|
) {
|
||||||
|
$this->integrations = new IntegrationsRepository($this->pdo);
|
||||||
|
$this->cipher = new IntegrationSecretCipher($this->secret);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -22,16 +31,20 @@ final class InpostIntegrationRepository
|
|||||||
{
|
{
|
||||||
$row = $this->fetchRow();
|
$row = $this->fetchRow();
|
||||||
if ($row === null) {
|
if ($row === null) {
|
||||||
return $this->defaultSettings();
|
$row = $this->defaultSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$encryptedToken = $this->resolveEncryptedToken($row);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'has_api_token' => trim((string) ($row['api_token_encrypted'] ?? '')) !== '',
|
'has_api_token' => $encryptedToken !== null && $encryptedToken !== '',
|
||||||
'organization_id' => (string) ($row['organization_id'] ?? ''),
|
'organization_id' => (string) ($row['organization_id'] ?? ''),
|
||||||
'environment' => (string) ($row['environment'] ?? 'sandbox'),
|
'environment' => (string) ($row['environment'] ?? 'sandbox'),
|
||||||
'default_dispatch_method' => (string) ($row['default_dispatch_method'] ?? 'pop'),
|
'default_dispatch_method' => (string) ($row['default_dispatch_method'] ?? 'pop'),
|
||||||
'default_dispatch_point' => (string) ($row['default_dispatch_point'] ?? ''),
|
'default_dispatch_point' => (string) ($row['default_dispatch_point'] ?? ''),
|
||||||
'default_insurance' => $row['default_insurance'] !== null ? (float) $row['default_insurance'] : null,
|
'default_insurance' => isset($row['default_insurance']) && $row['default_insurance'] !== null
|
||||||
|
? (float) $row['default_insurance']
|
||||||
|
: null,
|
||||||
'default_locker_size' => (string) ($row['default_locker_size'] ?? 'small'),
|
'default_locker_size' => (string) ($row['default_locker_size'] ?? 'small'),
|
||||||
'default_courier_length' => (int) ($row['default_courier_length'] ?? 20),
|
'default_courier_length' => (int) ($row['default_courier_length'] ?? 20),
|
||||||
'default_courier_width' => (int) ($row['default_courier_width'] ?? 15),
|
'default_courier_width' => (int) ($row['default_courier_width'] ?? 15),
|
||||||
@@ -54,12 +67,17 @@ final class InpostIntegrationRepository
|
|||||||
throw new RuntimeException('Brak rekordu konfiguracji InPost.');
|
throw new RuntimeException('Brak rekordu konfiguracji InPost.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$integrationId = $this->ensureBaseIntegration();
|
||||||
|
$currentEncrypted = $this->resolveEncryptedToken($current);
|
||||||
|
|
||||||
$apiToken = trim((string) ($payload['api_token'] ?? ''));
|
$apiToken = trim((string) ($payload['api_token'] ?? ''));
|
||||||
$apiTokenEncrypted = trim((string) ($current['api_token_encrypted'] ?? ''));
|
$nextEncrypted = $currentEncrypted;
|
||||||
if ($apiToken !== '') {
|
if ($apiToken !== '') {
|
||||||
$apiTokenEncrypted = (string) $this->encrypt($apiToken);
|
$nextEncrypted = $this->cipher->encrypt($apiToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->integrations->updateApiKeyEncrypted($integrationId, $nextEncrypted);
|
||||||
|
|
||||||
$statement = $this->pdo->prepare(
|
$statement = $this->pdo->prepare(
|
||||||
'UPDATE inpost_integration_settings
|
'UPDATE inpost_integration_settings
|
||||||
SET api_token_encrypted = :api_token_encrypted,
|
SET api_token_encrypted = :api_token_encrypted,
|
||||||
@@ -80,7 +98,7 @@ final class InpostIntegrationRepository
|
|||||||
WHERE id = 1'
|
WHERE id = 1'
|
||||||
);
|
);
|
||||||
$statement->execute([
|
$statement->execute([
|
||||||
'api_token_encrypted' => $this->nullableString($apiTokenEncrypted),
|
'api_token_encrypted' => $this->nullableString((string) $nextEncrypted),
|
||||||
'organization_id' => $this->nullableString(trim((string) ($payload['organization_id'] ?? ''))),
|
'organization_id' => $this->nullableString(trim((string) ($payload['organization_id'] ?? ''))),
|
||||||
'environment' => in_array($payload['environment'] ?? '', ['sandbox', 'production'], true)
|
'environment' => in_array($payload['environment'] ?? '', ['sandbox', 'production'], true)
|
||||||
? $payload['environment']
|
? $payload['environment']
|
||||||
@@ -117,12 +135,12 @@ final class InpostIntegrationRepository
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$encrypted = trim((string) ($row['api_token_encrypted'] ?? ''));
|
$encrypted = $this->resolveEncryptedToken($row);
|
||||||
if ($encrypted === '') {
|
if ($encrypted === null || $encrypted === '') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->decrypt($encrypted);
|
return $this->cipher->decrypt($encrypted);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function ensureRow(): void
|
private function ensureRow(): void
|
||||||
@@ -174,60 +192,32 @@ final class InpostIntegrationRepository
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function resolveEncryptedToken(array $row): ?string
|
||||||
|
{
|
||||||
|
$integrationId = $this->ensureBaseIntegration();
|
||||||
|
$fromBase = $this->integrations->getApiKeyEncrypted($integrationId);
|
||||||
|
if ($fromBase !== null && $fromBase !== '') {
|
||||||
|
return $fromBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
$legacy = trim((string) ($row['api_token_encrypted'] ?? ''));
|
||||||
|
return $legacy === '' ? null : $legacy;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ensureBaseIntegration(): int
|
||||||
|
{
|
||||||
|
return $this->integrations->ensureIntegration(
|
||||||
|
self::INTEGRATION_TYPE,
|
||||||
|
self::INTEGRATION_NAME,
|
||||||
|
self::INTEGRATION_BASE_URL,
|
||||||
|
20,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private function nullableString(string $value): ?string
|
private function nullableString(string $value): ?string
|
||||||
{
|
{
|
||||||
$trimmed = trim($value);
|
$trimmed = trim($value);
|
||||||
return $trimmed === '' ? null : $trimmed;
|
return $trimmed === '' ? null : $trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function encrypt(string $plainText): ?string
|
|
||||||
{
|
|
||||||
$value = trim($plainText);
|
|
||||||
if ($value === '') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if ($this->secret === '') {
|
|
||||||
throw new RuntimeException('Brak INTEGRATIONS_SECRET do szyfrowania danych integracji.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$encryptionKey = hash('sha256', 'enc|' . $this->secret, true);
|
|
||||||
$hmacKey = hash('sha256', 'auth|' . $this->secret, true);
|
|
||||||
$iv = random_bytes(16);
|
|
||||||
$cipherRaw = openssl_encrypt($value, 'AES-256-CBC', $encryptionKey, OPENSSL_RAW_DATA, $iv);
|
|
||||||
if ($cipherRaw === false) {
|
|
||||||
throw new RuntimeException('Nie udalo sie zaszyfrowac danych integracji.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$mac = hash_hmac('sha256', $iv . $cipherRaw, $hmacKey, true);
|
|
||||||
return 'v1:' . base64_encode($iv . $mac . $cipherRaw);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function decrypt(string $encrypted): ?string
|
|
||||||
{
|
|
||||||
if ($this->secret === '') {
|
|
||||||
throw new RuntimeException('Brak INTEGRATIONS_SECRET do odszyfrowania danych integracji.');
|
|
||||||
}
|
|
||||||
if (!str_starts_with($encrypted, 'v1:')) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$raw = base64_decode(substr($encrypted, 3), true);
|
|
||||||
if ($raw === false || strlen($raw) < 48) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$encryptionKey = hash('sha256', 'enc|' . $this->secret, true);
|
|
||||||
$hmacKey = hash('sha256', 'auth|' . $this->secret, true);
|
|
||||||
$iv = substr($raw, 0, 16);
|
|
||||||
$mac = substr($raw, 16, 32);
|
|
||||||
$cipherRaw = substr($raw, 48);
|
|
||||||
|
|
||||||
$expectedMac = hash_hmac('sha256', $iv . $cipherRaw, $hmacKey, true);
|
|
||||||
if (!hash_equals($expectedMac, $mac)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$decrypted = openssl_decrypt($cipherRaw, 'AES-256-CBC', $encryptionKey, OPENSSL_RAW_DATA, $iv);
|
|
||||||
return $decrypted !== false ? $decrypted : null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
65
src/Modules/Settings/IntegrationSecretCipher.php
Normal file
65
src/Modules/Settings/IntegrationSecretCipher.php
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Settings;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
final class IntegrationSecretCipher
|
||||||
|
{
|
||||||
|
public function __construct(private readonly string $secret)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function encrypt(string $plainText): ?string
|
||||||
|
{
|
||||||
|
$value = trim($plainText);
|
||||||
|
if ($value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if ($this->secret === '') {
|
||||||
|
throw new RuntimeException('Brak INTEGRATIONS_SECRET do szyfrowania danych integracji.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$encryptionKey = hash('sha256', 'enc|' . $this->secret, true);
|
||||||
|
$hmacKey = hash('sha256', 'auth|' . $this->secret, true);
|
||||||
|
$iv = random_bytes(16);
|
||||||
|
$cipherRaw = openssl_encrypt($value, 'AES-256-CBC', $encryptionKey, OPENSSL_RAW_DATA, $iv);
|
||||||
|
if ($cipherRaw === false) {
|
||||||
|
throw new RuntimeException('Nie udalo sie zaszyfrowac danych integracji.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$mac = hash_hmac('sha256', $iv . $cipherRaw, $hmacKey, true);
|
||||||
|
return 'v1:' . base64_encode($iv . $mac . $cipherRaw);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function decrypt(string $encrypted): ?string
|
||||||
|
{
|
||||||
|
if ($this->secret === '') {
|
||||||
|
throw new RuntimeException('Brak INTEGRATIONS_SECRET do odszyfrowania danych integracji.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!str_starts_with($encrypted, 'v1:')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw = base64_decode(substr($encrypted, 3), true);
|
||||||
|
if ($raw === false || strlen($raw) < 48) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$encryptionKey = hash('sha256', 'enc|' . $this->secret, true);
|
||||||
|
$hmacKey = hash('sha256', 'auth|' . $this->secret, true);
|
||||||
|
$iv = substr($raw, 0, 16);
|
||||||
|
$mac = substr($raw, 16, 32);
|
||||||
|
$cipherRaw = substr($raw, 48);
|
||||||
|
|
||||||
|
$expectedMac = hash_hmac('sha256', $iv . $cipherRaw, $hmacKey, true);
|
||||||
|
if (!hash_equals($expectedMac, $mac)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$decrypted = openssl_decrypt($cipherRaw, 'AES-256-CBC', $encryptionKey, OPENSSL_RAW_DATA, $iv);
|
||||||
|
return $decrypted !== false ? $decrypted : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
169
src/Modules/Settings/IntegrationsHubController.php
Normal file
169
src/Modules/Settings/IntegrationsHubController.php
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Settings;
|
||||||
|
|
||||||
|
use App\Core\Http\Request;
|
||||||
|
use App\Core\Http\Response;
|
||||||
|
use App\Core\I18n\Translator;
|
||||||
|
use App\Core\Security\Csrf;
|
||||||
|
use App\Core\Support\Flash;
|
||||||
|
use App\Core\View\Template;
|
||||||
|
use App\Modules\Auth\AuthService;
|
||||||
|
|
||||||
|
final class IntegrationsHubController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly Template $template,
|
||||||
|
private readonly Translator $translator,
|
||||||
|
private readonly AuthService $auth,
|
||||||
|
private readonly IntegrationsRepository $integrations,
|
||||||
|
private readonly AllegroIntegrationRepository $allegro,
|
||||||
|
private readonly ApaczkaIntegrationRepository $apaczka,
|
||||||
|
private readonly InpostIntegrationRepository $inpost,
|
||||||
|
private readonly ShopproIntegrationsRepository $shoppro
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
$rows = [
|
||||||
|
$this->buildAllegroRow('sandbox'),
|
||||||
|
$this->buildAllegroRow('production'),
|
||||||
|
$this->buildApaczkaRow(),
|
||||||
|
$this->buildInpostRow(),
|
||||||
|
$this->buildShopproRow(),
|
||||||
|
];
|
||||||
|
|
||||||
|
$html = $this->template->render('settings/integrations', [
|
||||||
|
'title' => $this->translator->get('settings.integrations_hub.title'),
|
||||||
|
'activeMenu' => 'settings',
|
||||||
|
'activeSettings' => 'integrations',
|
||||||
|
'user' => $this->auth->user(),
|
||||||
|
'csrfToken' => Csrf::token(),
|
||||||
|
'rows' => $rows,
|
||||||
|
'errorMessage' => (string) Flash::get('settings_error', ''),
|
||||||
|
'successMessage' => (string) Flash::get('settings_success', ''),
|
||||||
|
], 'layouts/app');
|
||||||
|
|
||||||
|
return Response::html($html);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function buildAllegroRow(string $environment): array
|
||||||
|
{
|
||||||
|
$env = $environment === 'production' ? 'production' : 'sandbox';
|
||||||
|
$settings = $this->allegro->getSettings($env);
|
||||||
|
$integrationName = $env === 'production' ? 'Allegro Production' : 'Allegro Sandbox';
|
||||||
|
$meta = $this->integrations->findByTypeAndName('allegro', $integrationName) ?? [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'provider' => $this->translator->get('settings.integrations_hub.providers.allegro'),
|
||||||
|
'instance' => $this->translator->get('settings.integrations_hub.providers.' . ($env === 'production' ? 'allegro_production' : 'allegro_sandbox')),
|
||||||
|
'authorization_status' => !empty($settings['is_connected'])
|
||||||
|
? $this->translator->get('settings.integrations_hub.status.connected')
|
||||||
|
: $this->translator->get('settings.integrations_hub.status.not_connected'),
|
||||||
|
'secret_status' => !empty($settings['has_client_secret'])
|
||||||
|
? $this->translator->get('settings.integrations_hub.status.saved')
|
||||||
|
: $this->translator->get('settings.integrations_hub.status.missing'),
|
||||||
|
'is_active' => (int) ($meta['is_active'] ?? 0) === 1,
|
||||||
|
'last_test_at' => trim((string) ($meta['last_test_at'] ?? '')),
|
||||||
|
'configure_url' => '/settings/integrations/allegro?env=' . rawurlencode($env),
|
||||||
|
'configure_label' => $this->translator->get('settings.integrations_hub.actions.configure'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function buildApaczkaRow(): array
|
||||||
|
{
|
||||||
|
$settings = $this->apaczka->getSettings();
|
||||||
|
$meta = $this->integrations->findByTypeAndName('apaczka', 'Apaczka') ?? [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'provider' => $this->translator->get('settings.integrations_hub.providers.apaczka'),
|
||||||
|
'instance' => 'Apaczka',
|
||||||
|
'authorization_status' => !empty($settings['has_api_key'])
|
||||||
|
? $this->translator->get('settings.integrations_hub.status.configured')
|
||||||
|
: $this->translator->get('settings.integrations_hub.status.not_configured'),
|
||||||
|
'secret_status' => !empty($settings['has_api_key'])
|
||||||
|
? $this->translator->get('settings.integrations_hub.status.saved')
|
||||||
|
: $this->translator->get('settings.integrations_hub.status.missing'),
|
||||||
|
'is_active' => (int) ($meta['is_active'] ?? 0) === 1,
|
||||||
|
'last_test_at' => trim((string) ($meta['last_test_at'] ?? '')),
|
||||||
|
'configure_url' => '/settings/integrations/apaczka',
|
||||||
|
'configure_label' => $this->translator->get('settings.integrations_hub.actions.configure'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function buildInpostRow(): array
|
||||||
|
{
|
||||||
|
$settings = $this->inpost->getSettings();
|
||||||
|
$meta = $this->integrations->findByTypeAndName('inpost', 'InPost ShipX') ?? [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'provider' => $this->translator->get('settings.integrations_hub.providers.inpost'),
|
||||||
|
'instance' => 'InPost ShipX',
|
||||||
|
'authorization_status' => !empty($settings['has_api_token'])
|
||||||
|
? $this->translator->get('settings.integrations_hub.status.configured')
|
||||||
|
: $this->translator->get('settings.integrations_hub.status.not_configured'),
|
||||||
|
'secret_status' => !empty($settings['has_api_token'])
|
||||||
|
? $this->translator->get('settings.integrations_hub.status.saved')
|
||||||
|
: $this->translator->get('settings.integrations_hub.status.missing'),
|
||||||
|
'is_active' => (int) ($meta['is_active'] ?? 0) === 1,
|
||||||
|
'last_test_at' => trim((string) ($meta['last_test_at'] ?? '')),
|
||||||
|
'configure_url' => '/settings/integrations/inpost',
|
||||||
|
'configure_label' => $this->translator->get('settings.integrations_hub.actions.configure'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function buildShopproRow(): array
|
||||||
|
{
|
||||||
|
$rows = $this->shoppro->listIntegrations();
|
||||||
|
$instancesCount = count($rows);
|
||||||
|
$activeCount = 0;
|
||||||
|
$configuredCount = 0;
|
||||||
|
$lastTestAt = '';
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
if (!empty($row['is_active'])) {
|
||||||
|
$activeCount++;
|
||||||
|
}
|
||||||
|
if (!empty($row['has_api_key'])) {
|
||||||
|
$configuredCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$testedAt = trim((string) ($row['last_test_at'] ?? ''));
|
||||||
|
if ($testedAt !== '' && ($lastTestAt === '' || strcmp($testedAt, $lastTestAt) > 0)) {
|
||||||
|
$lastTestAt = $testedAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'provider' => $this->translator->get('settings.integrations_hub.providers.shoppro'),
|
||||||
|
'instance' => $this->translator->get('settings.integrations_hub.providers.shoppro_instances', [
|
||||||
|
'count' => $instancesCount,
|
||||||
|
]),
|
||||||
|
'authorization_status' => $configuredCount > 0
|
||||||
|
? $this->translator->get('settings.integrations_hub.status.configured')
|
||||||
|
: $this->translator->get('settings.integrations_hub.status.not_configured'),
|
||||||
|
'secret_status' => $configuredCount > 0
|
||||||
|
? $this->translator->get('settings.integrations_hub.status.saved')
|
||||||
|
: $this->translator->get('settings.integrations_hub.status.missing'),
|
||||||
|
'is_active' => $activeCount > 0,
|
||||||
|
'last_test_at' => $lastTestAt,
|
||||||
|
'configure_url' => '/settings/integrations/shoppro',
|
||||||
|
'configure_label' => $this->translator->get('settings.integrations_hub.actions.configure'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
155
src/Modules/Settings/IntegrationsRepository.php
Normal file
155
src/Modules/Settings/IntegrationsRepository.php
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Settings;
|
||||||
|
|
||||||
|
use PDO;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
final class IntegrationsRepository
|
||||||
|
{
|
||||||
|
public function __construct(private readonly PDO $pdo)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>|null
|
||||||
|
*/
|
||||||
|
public function findByTypeAndName(string $type, string $name): ?array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$statement = $this->pdo->prepare(
|
||||||
|
'SELECT * FROM integrations WHERE type = :type AND name = :name LIMIT 1'
|
||||||
|
);
|
||||||
|
$statement->execute([
|
||||||
|
'type' => trim($type),
|
||||||
|
'name' => trim($name),
|
||||||
|
]);
|
||||||
|
$row = $statement->fetch(PDO::FETCH_ASSOC);
|
||||||
|
} catch (Throwable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return is_array($row) ? $row : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>|null
|
||||||
|
*/
|
||||||
|
public function findFirstByType(string $type): ?array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$statement = $this->pdo->prepare(
|
||||||
|
'SELECT * FROM integrations WHERE type = :type ORDER BY id ASC LIMIT 1'
|
||||||
|
);
|
||||||
|
$statement->execute(['type' => trim($type)]);
|
||||||
|
$row = $statement->fetch(PDO::FETCH_ASSOC);
|
||||||
|
} catch (Throwable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return is_array($row) ? $row : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>|null
|
||||||
|
*/
|
||||||
|
public function findById(int $id): ?array
|
||||||
|
{
|
||||||
|
if ($id <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$statement = $this->pdo->prepare('SELECT * FROM integrations WHERE id = :id LIMIT 1');
|
||||||
|
$statement->execute(['id' => $id]);
|
||||||
|
$row = $statement->fetch(PDO::FETCH_ASSOC);
|
||||||
|
} catch (Throwable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return is_array($row) ? $row : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ensureIntegration(
|
||||||
|
string $type,
|
||||||
|
string $name,
|
||||||
|
string $baseUrl,
|
||||||
|
int $timeoutSeconds = 10,
|
||||||
|
bool $isActive = true
|
||||||
|
): int {
|
||||||
|
$typeValue = trim($type);
|
||||||
|
$nameValue = trim($name);
|
||||||
|
$baseUrlValue = trim($baseUrl);
|
||||||
|
|
||||||
|
$existing = $this->findByTypeAndName($typeValue, $nameValue);
|
||||||
|
if ($existing !== null) {
|
||||||
|
return (int) ($existing['id'] ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
$statement = $this->pdo->prepare(
|
||||||
|
'INSERT INTO integrations (
|
||||||
|
type, name, base_url, timeout_seconds, is_active, created_at, updated_at
|
||||||
|
) VALUES (
|
||||||
|
:type, :name, :base_url, :timeout_seconds, :is_active, NOW(), NOW()
|
||||||
|
)'
|
||||||
|
);
|
||||||
|
$statement->execute([
|
||||||
|
'type' => $typeValue,
|
||||||
|
'name' => $nameValue,
|
||||||
|
'base_url' => $baseUrlValue,
|
||||||
|
'timeout_seconds' => max(1, $timeoutSeconds),
|
||||||
|
'is_active' => $isActive ? 1 : 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (int) $this->pdo->lastInsertId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateApiKeyEncrypted(int $integrationId, ?string $encrypted): void
|
||||||
|
{
|
||||||
|
if ($integrationId <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$statement = $this->pdo->prepare(
|
||||||
|
'UPDATE integrations
|
||||||
|
SET api_key_encrypted = :api_key_encrypted,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = :id'
|
||||||
|
);
|
||||||
|
$statement->execute([
|
||||||
|
'id' => $integrationId,
|
||||||
|
'api_key_encrypted' => $this->nullableString((string) $encrypted),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getApiKeyEncrypted(int $integrationId): ?string
|
||||||
|
{
|
||||||
|
if ($integrationId <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$statement = $this->pdo->prepare(
|
||||||
|
'SELECT api_key_encrypted FROM integrations WHERE id = :id LIMIT 1'
|
||||||
|
);
|
||||||
|
$statement->execute(['id' => $integrationId]);
|
||||||
|
$value = $statement->fetchColumn();
|
||||||
|
} catch (Throwable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_string($value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$trimmed = trim($value);
|
||||||
|
return $trimmed === '' ? null : $trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function nullableString(string $value): ?string
|
||||||
|
{
|
||||||
|
$trimmed = trim($value);
|
||||||
|
return $trimmed === '' ? null : $trimmed;
|
||||||
|
}
|
||||||
|
}
|
||||||
263
src/Modules/Settings/ShopproApiClient.php
Normal file
263
src/Modules/Settings/ShopproApiClient.php
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Settings;
|
||||||
|
|
||||||
|
final class ShopproApiClient
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return array{ok:bool,http_code:int|null,message:string,items:array<int,array<string,mixed>>,total:int,page:int,per_page:int}
|
||||||
|
*/
|
||||||
|
public function fetchOrders(
|
||||||
|
string $baseUrl,
|
||||||
|
string $apiKey,
|
||||||
|
int $timeoutSeconds,
|
||||||
|
int $page = 1,
|
||||||
|
int $perPage = 100,
|
||||||
|
?string $fromDate = null
|
||||||
|
): array {
|
||||||
|
$query = [
|
||||||
|
'endpoint' => 'orders',
|
||||||
|
'action' => 'list',
|
||||||
|
'page' => max(1, $page),
|
||||||
|
'per_page' => max(1, min(100, $perPage)),
|
||||||
|
'sort' => 'updated_at',
|
||||||
|
'sort_dir' => 'ASC',
|
||||||
|
];
|
||||||
|
|
||||||
|
$dateFrom = trim((string) $fromDate);
|
||||||
|
if ($dateFrom !== '') {
|
||||||
|
$query['date_from'] = $dateFrom;
|
||||||
|
$query['updated_from'] = $dateFrom;
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = rtrim(trim($baseUrl), '/') . '/api.php?' . http_build_query($query);
|
||||||
|
$response = $this->requestJson($url, $apiKey, $timeoutSeconds);
|
||||||
|
if (($response['ok'] ?? false) !== true) {
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'http_code' => $response['http_code'] ?? null,
|
||||||
|
'message' => (string) ($response['message'] ?? 'Nie mozna pobrac listy zamowien z shopPRO.'),
|
||||||
|
'items' => [],
|
||||||
|
'total' => 0,
|
||||||
|
'page' => max(1, $page),
|
||||||
|
'per_page' => max(1, min(100, $perPage)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = is_array($response['data'] ?? null) ? $response['data'] : [];
|
||||||
|
$items = [];
|
||||||
|
if (isset($data['items']) && is_array($data['items'])) {
|
||||||
|
$items = $data['items'];
|
||||||
|
} elseif (isset($data['orders']) && is_array($data['orders'])) {
|
||||||
|
$items = $data['orders'];
|
||||||
|
} elseif ($data !== [] && array_keys($data) === range(0, count($data) - 1)) {
|
||||||
|
$items = $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'ok' => true,
|
||||||
|
'http_code' => $response['http_code'] ?? null,
|
||||||
|
'message' => '',
|
||||||
|
'items' => array_values(array_filter($items, static fn (mixed $row): bool => is_array($row))),
|
||||||
|
'total' => (int) ($data['total'] ?? count($items)),
|
||||||
|
'page' => (int) ($data['page'] ?? max(1, $page)),
|
||||||
|
'per_page' => (int) ($data['per_page'] ?? max(1, min(100, $perPage))),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{ok:bool,http_code:int|null,message:string,order:array<string,mixed>|null}
|
||||||
|
*/
|
||||||
|
public function fetchOrderById(string $baseUrl, string $apiKey, int $timeoutSeconds, string $orderId): array
|
||||||
|
{
|
||||||
|
$id = trim($orderId);
|
||||||
|
if ($id === '') {
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'http_code' => null,
|
||||||
|
'message' => 'Niepoprawne ID zamowienia.',
|
||||||
|
'order' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$base = rtrim(trim($baseUrl), '/');
|
||||||
|
$attemptUrls = [
|
||||||
|
$base . '/api.php?' . http_build_query(['endpoint' => 'orders', 'action' => 'get', 'id' => $id]),
|
||||||
|
$base . '/api.php?' . http_build_query(['endpoint' => 'orders', 'action' => 'details', 'id' => $id]),
|
||||||
|
];
|
||||||
|
|
||||||
|
$lastMessage = 'Nie mozna pobrac szczegolow zamowienia z shopPRO.';
|
||||||
|
$lastCode = null;
|
||||||
|
|
||||||
|
foreach ($attemptUrls as $url) {
|
||||||
|
$response = $this->requestJson($url, $apiKey, $timeoutSeconds);
|
||||||
|
if (($response['ok'] ?? false) !== true) {
|
||||||
|
$lastMessage = trim((string) ($response['message'] ?? $lastMessage));
|
||||||
|
$lastCode = $response['http_code'] ?? null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $response['data'] ?? null;
|
||||||
|
if (!is_array($data)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($data['order']) && is_array($data['order'])) {
|
||||||
|
return [
|
||||||
|
'ok' => true,
|
||||||
|
'http_code' => $response['http_code'] ?? null,
|
||||||
|
'message' => '',
|
||||||
|
'order' => $data['order'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'ok' => true,
|
||||||
|
'http_code' => $response['http_code'] ?? null,
|
||||||
|
'message' => '',
|
||||||
|
'order' => $data,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'http_code' => $lastCode,
|
||||||
|
'message' => $lastMessage,
|
||||||
|
'order' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{ok:bool,http_code:int|null,message:string,product:array<string,mixed>|null}
|
||||||
|
*/
|
||||||
|
public function fetchProductById(string $baseUrl, string $apiKey, int $timeoutSeconds, int $productId): array
|
||||||
|
{
|
||||||
|
if ($productId <= 0) {
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'http_code' => null,
|
||||||
|
'message' => 'Niepoprawne ID produktu.',
|
||||||
|
'product' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedBaseUrl = rtrim(trim($baseUrl), '/');
|
||||||
|
$query = http_build_query([
|
||||||
|
'endpoint' => 'products',
|
||||||
|
'action' => 'get',
|
||||||
|
'id' => $productId,
|
||||||
|
]);
|
||||||
|
$endpointUrl = $normalizedBaseUrl . '/api.php?' . $query;
|
||||||
|
|
||||||
|
$response = $this->requestJson($endpointUrl, $apiKey, $timeoutSeconds);
|
||||||
|
if (($response['ok'] ?? false) !== true) {
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'http_code' => $response['http_code'] ?? null,
|
||||||
|
'message' => (string) ($response['message'] ?? 'Nie mozna pobrac produktu z shopPRO.'),
|
||||||
|
'product' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = is_array($response['data'] ?? null) ? $response['data'] : null;
|
||||||
|
if ($data === null) {
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'http_code' => $response['http_code'] ?? null,
|
||||||
|
'message' => 'shopPRO zwrocil pusty payload produktu.',
|
||||||
|
'product' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'ok' => true,
|
||||||
|
'http_code' => $response['http_code'] ?? null,
|
||||||
|
'message' => '',
|
||||||
|
'product' => $data,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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
|
||||||
|
{
|
||||||
|
$curl = curl_init($url);
|
||||||
|
if ($curl === false) {
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'http_code' => null,
|
||||||
|
'message' => 'Nie udalo sie zainicjalizowac polaczenia HTTP.',
|
||||||
|
'data' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_setopt_array($curl, [
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_TIMEOUT => max(1, min(120, $timeoutSeconds)),
|
||||||
|
CURLOPT_CONNECTTIMEOUT => max(1, min(120, $timeoutSeconds)),
|
||||||
|
CURLOPT_HTTPHEADER => [
|
||||||
|
'Accept: application/json',
|
||||||
|
'X-Api-Key: ' . $apiKey,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$body = curl_exec($curl);
|
||||||
|
$httpCode = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||||
|
$curlError = trim(curl_error($curl));
|
||||||
|
|
||||||
|
if ($body === false) {
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'http_code' => $httpCode > 0 ? $httpCode : null,
|
||||||
|
'message' => $curlError !== '' ? $curlError : 'Nieznany blad transportu HTTP.',
|
||||||
|
'data' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = json_decode((string) $body, true);
|
||||||
|
if (!is_array($decoded)) {
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'http_code' => $httpCode > 0 ? $httpCode : null,
|
||||||
|
'message' => 'Odpowiedz API nie jest poprawnym JSON.',
|
||||||
|
'data' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiStatus = trim((string) ($decoded['status'] ?? ''));
|
||||||
|
$apiCode = trim((string) ($decoded['code'] ?? ''));
|
||||||
|
$apiMessage = trim((string) ($decoded['message'] ?? ''));
|
||||||
|
if ($apiStatus !== '' && mb_strtolower($apiStatus) !== 'ok') {
|
||||||
|
$message = trim('shopPRO zwrocil blad. ' . $apiCode . ' ' . $apiMessage);
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'http_code' => $httpCode > 0 ? $httpCode : null,
|
||||||
|
'message' => $message !== '' ? $message : 'Nieznany blad API shopPRO.',
|
||||||
|
'data' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($httpCode >= 400) {
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'http_code' => $httpCode,
|
||||||
|
'message' => $apiMessage !== '' ? $apiMessage : 'Blad HTTP podczas komunikacji z shopPRO.',
|
||||||
|
'data' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $decoded['data'] ?? $decoded;
|
||||||
|
if (!is_array($data)) {
|
||||||
|
$data = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'ok' => true,
|
||||||
|
'http_code' => $httpCode > 0 ? $httpCode : null,
|
||||||
|
'message' => '',
|
||||||
|
'data' => $data,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
104
src/Modules/Settings/ShopproDeliveryMethodMappingRepository.php
Normal file
104
src/Modules/Settings/ShopproDeliveryMethodMappingRepository.php
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Settings;
|
||||||
|
|
||||||
|
use PDO;
|
||||||
|
|
||||||
|
final class ShopproDeliveryMethodMappingRepository
|
||||||
|
{
|
||||||
|
public function __construct(private readonly PDO $pdo)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
public function listMappings(int $integrationId): array
|
||||||
|
{
|
||||||
|
if ($integrationId <= 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
'SELECT *
|
||||||
|
FROM shoppro_delivery_method_mappings
|
||||||
|
WHERE integration_id = :integration_id
|
||||||
|
ORDER BY order_delivery_method ASC'
|
||||||
|
);
|
||||||
|
$stmt->execute(['integration_id' => $integrationId]);
|
||||||
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
return is_array($rows) ? $rows : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, array<string, string>> $mappings
|
||||||
|
*/
|
||||||
|
public function saveMappings(int $integrationId, array $mappings): void
|
||||||
|
{
|
||||||
|
if ($integrationId <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$deleteStmt = $this->pdo->prepare(
|
||||||
|
'DELETE FROM shoppro_delivery_method_mappings WHERE integration_id = :integration_id'
|
||||||
|
);
|
||||||
|
$deleteStmt->execute(['integration_id' => $integrationId]);
|
||||||
|
|
||||||
|
if ($mappings === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$insertStmt = $this->pdo->prepare(
|
||||||
|
'INSERT INTO shoppro_delivery_method_mappings (
|
||||||
|
integration_id, order_delivery_method, carrier, allegro_delivery_method_id,
|
||||||
|
allegro_credentials_id, allegro_carrier_id, allegro_service_name
|
||||||
|
) VALUES (
|
||||||
|
:integration_id, :order_delivery_method, :carrier, :allegro_delivery_method_id,
|
||||||
|
:allegro_credentials_id, :allegro_carrier_id, :allegro_service_name
|
||||||
|
)'
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($mappings as $mapping) {
|
||||||
|
$orderMethod = trim((string) ($mapping['order_delivery_method'] ?? ''));
|
||||||
|
$allegroMethodId = trim((string) ($mapping['allegro_delivery_method_id'] ?? ''));
|
||||||
|
if ($orderMethod === '' || $allegroMethodId === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$carrier = trim((string) ($mapping['carrier'] ?? 'allegro'));
|
||||||
|
$insertStmt->execute([
|
||||||
|
'integration_id' => $integrationId,
|
||||||
|
'order_delivery_method' => $orderMethod,
|
||||||
|
'carrier' => $carrier !== '' ? $carrier : 'allegro',
|
||||||
|
'allegro_delivery_method_id' => $allegroMethodId,
|
||||||
|
'allegro_credentials_id' => trim((string) ($mapping['allegro_credentials_id'] ?? '')),
|
||||||
|
'allegro_carrier_id' => trim((string) ($mapping['allegro_carrier_id'] ?? '')),
|
||||||
|
'allegro_service_name' => trim((string) ($mapping['allegro_service_name'] ?? '')),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public function getDistinctOrderDeliveryMethods(int $integrationId): array
|
||||||
|
{
|
||||||
|
if ($integrationId <= 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
"SELECT DISTINCT external_carrier_id
|
||||||
|
FROM orders
|
||||||
|
WHERE external_carrier_id IS NOT NULL
|
||||||
|
AND external_carrier_id <> ''
|
||||||
|
AND source = 'shoppro'
|
||||||
|
AND integration_id = :integration_id
|
||||||
|
ORDER BY external_carrier_id ASC"
|
||||||
|
);
|
||||||
|
$stmt->execute(['integration_id' => $integrationId]);
|
||||||
|
$rows = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
return is_array($rows) ? $rows : [];
|
||||||
|
}
|
||||||
|
}
|
||||||
868
src/Modules/Settings/ShopproIntegrationsController.php
Normal file
868
src/Modules/Settings/ShopproIntegrationsController.php
Normal file
@@ -0,0 +1,868 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Settings;
|
||||||
|
|
||||||
|
use App\Core\Http\Request;
|
||||||
|
use App\Core\Http\Response;
|
||||||
|
use App\Core\I18n\Translator;
|
||||||
|
use App\Core\Security\Csrf;
|
||||||
|
use App\Core\Support\Flash;
|
||||||
|
use App\Core\View\Template;
|
||||||
|
use App\Modules\Auth\AuthService;
|
||||||
|
use App\Modules\Cron\CronRepository;
|
||||||
|
use DateInterval;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use RuntimeException;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
final class ShopproIntegrationsController
|
||||||
|
{
|
||||||
|
private const ORDERS_IMPORT_JOB_TYPE = 'shoppro_orders_import';
|
||||||
|
private const ORDERS_IMPORT_DEFAULT_INTERVAL_SECONDS = 300;
|
||||||
|
private const ORDERS_IMPORT_DEFAULT_PRIORITY = 90;
|
||||||
|
private const ORDERS_IMPORT_DEFAULT_MAX_ATTEMPTS = 3;
|
||||||
|
private const ORDER_STATUS_SYNC_JOB_TYPE = 'shoppro_order_status_sync';
|
||||||
|
private const ORDER_STATUS_SYNC_DEFAULT_INTERVAL_SECONDS = 900;
|
||||||
|
private const ORDER_STATUS_SYNC_DEFAULT_PRIORITY = 100;
|
||||||
|
private const ORDER_STATUS_SYNC_DEFAULT_MAX_ATTEMPTS = 3;
|
||||||
|
private const PAYMENT_SYNC_JOB_TYPE = 'shoppro_payment_status_sync';
|
||||||
|
private const PAYMENT_SYNC_DEFAULT_INTERVAL_SECONDS = 600;
|
||||||
|
private const PAYMENT_SYNC_DEFAULT_PRIORITY = 105;
|
||||||
|
private const PAYMENT_SYNC_DEFAULT_MAX_ATTEMPTS = 3;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly Template $template,
|
||||||
|
private readonly Translator $translator,
|
||||||
|
private readonly AuthService $auth,
|
||||||
|
private readonly ShopproIntegrationsRepository $repository,
|
||||||
|
private readonly ShopproStatusMappingRepository $statusMappings,
|
||||||
|
private readonly OrderStatusRepository $orderStatuses,
|
||||||
|
private readonly CronRepository $cronRepository,
|
||||||
|
private readonly ShopproDeliveryMethodMappingRepository $deliveryMappings,
|
||||||
|
private readonly AllegroIntegrationRepository $allegroIntegrationRepository,
|
||||||
|
private readonly AllegroOAuthClient $allegroOAuthClient,
|
||||||
|
private readonly AllegroApiClient $allegroApiClient
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
$integrations = $this->repository->listIntegrations();
|
||||||
|
$forceNewMode = trim((string) $request->input('new', '')) === '1';
|
||||||
|
$selectedId = max(0, (int) $request->input('id', 0));
|
||||||
|
$selectedIntegration = $selectedId > 0 ? $this->repository->findIntegration($selectedId) : null;
|
||||||
|
if (!$forceNewMode && $selectedIntegration === null && $integrations !== []) {
|
||||||
|
$firstId = (int) ($integrations[0]['id'] ?? 0);
|
||||||
|
if ($firstId > 0) {
|
||||||
|
$selectedIntegration = $this->repository->findIntegration($firstId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->ensureImportScheduleExists();
|
||||||
|
$this->ensureStatusSyncScheduleExists();
|
||||||
|
$this->ensurePaymentSyncScheduleExists();
|
||||||
|
$activeTab = $this->resolveTab((string) $request->input('tab', 'integration'));
|
||||||
|
$discoveredStatuses = $this->readDiscoveredStatuses();
|
||||||
|
$statusRows = $selectedIntegration !== null
|
||||||
|
? $this->buildStatusRows((int) ($selectedIntegration['id'] ?? 0), $discoveredStatuses)
|
||||||
|
: [];
|
||||||
|
$deliveryServicesData = $activeTab === 'delivery'
|
||||||
|
? $this->loadDeliveryServices()
|
||||||
|
: [[], ''];
|
||||||
|
$deliveryMappings = $selectedIntegration !== null
|
||||||
|
? $this->deliveryMappings->listMappings((int) ($selectedIntegration['id'] ?? 0))
|
||||||
|
: [];
|
||||||
|
$orderDeliveryMethods = $selectedIntegration !== null
|
||||||
|
? $this->deliveryMappings->getDistinctOrderDeliveryMethods((int) ($selectedIntegration['id'] ?? 0))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
$html = $this->template->render('settings/shoppro', [
|
||||||
|
'title' => $this->translator->get('settings.integrations.title'),
|
||||||
|
'activeMenu' => 'settings',
|
||||||
|
'activeSettings' => 'shoppro',
|
||||||
|
'user' => $this->auth->user(),
|
||||||
|
'csrfToken' => Csrf::token(),
|
||||||
|
'activeTab' => $activeTab,
|
||||||
|
'rows' => $integrations,
|
||||||
|
'selectedIntegration' => $selectedIntegration,
|
||||||
|
'form' => $this->buildFormValues($selectedIntegration),
|
||||||
|
'ordersImportIntervalMinutes' => $this->currentImportIntervalMinutes(),
|
||||||
|
'statusSyncIntervalMinutes' => $this->currentStatusSyncIntervalMinutes(),
|
||||||
|
'paymentSyncIntervalMinutes' => $this->currentPaymentSyncIntervalMinutes(),
|
||||||
|
'statusRows' => $statusRows,
|
||||||
|
'orderproStatuses' => $this->orderStatuses->listStatuses(),
|
||||||
|
'deliveryMappings' => $deliveryMappings,
|
||||||
|
'orderDeliveryMethods' => $orderDeliveryMethods,
|
||||||
|
'allegroDeliveryServices' => $deliveryServicesData[0],
|
||||||
|
'allegroDeliveryServicesError' => $deliveryServicesData[1],
|
||||||
|
'inpostDeliveryServices' => array_values(array_filter(
|
||||||
|
$deliveryServicesData[0],
|
||||||
|
static fn (array $svc): bool => stripos((string) ($svc['carrierId'] ?? ''), 'inpost') !== false
|
||||||
|
)),
|
||||||
|
'errorMessage' => (string) Flash::get('settings_error', ''),
|
||||||
|
'successMessage' => (string) Flash::get('settings_success', ''),
|
||||||
|
], 'layouts/app');
|
||||||
|
|
||||||
|
return Response::html($html);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save(Request $request): Response
|
||||||
|
{
|
||||||
|
$integrationId = max(0, (int) $request->input('integration_id', 0));
|
||||||
|
$tab = $this->resolveTab((string) $request->input('tab', 'integration'));
|
||||||
|
$redirectBase = '/settings/integrations/shoppro';
|
||||||
|
$redirectTo = $this->buildRedirectUrl($integrationId, $tab);
|
||||||
|
|
||||||
|
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||||
|
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
$existing = $integrationId > 0 ? $this->repository->findIntegration($integrationId) : null;
|
||||||
|
if ($integrationId > 0 && $existing === null) {
|
||||||
|
Flash::set('settings_error', $this->translator->get('settings.integrations.flash.not_found'));
|
||||||
|
return Response::redirect($this->buildRedirectUrl(0, $tab));
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = trim((string) $request->input('name', ''));
|
||||||
|
if (mb_strlen($name) < 2) {
|
||||||
|
Flash::set('settings_error', $this->translator->get('settings.integrations.validation.name_min'));
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
$baseUrl = rtrim(trim((string) $request->input('base_url', '')), '/');
|
||||||
|
if (!$this->isValidHttpUrl($baseUrl)) {
|
||||||
|
Flash::set('settings_error', $this->translator->get('settings.integrations.validation.base_url_invalid'));
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiKey = trim((string) $request->input('api_key', ''));
|
||||||
|
$hasExistingApiKey = (bool) ($existing['has_api_key'] ?? false);
|
||||||
|
if ($tab === 'integration' && $apiKey === '' && !$hasExistingApiKey) {
|
||||||
|
Flash::set('settings_error', $this->translator->get('settings.integrations.validation.api_key_required'));
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
$ordersFetchStartDate = trim((string) $request->input('orders_fetch_start_date', ''));
|
||||||
|
if ($ordersFetchStartDate !== '' && !$this->isValidYmdDate($ordersFetchStartDate)) {
|
||||||
|
Flash::set('settings_error', $this->translator->get('settings.integrations.validation.orders_fetch_start_date_invalid'));
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->isDuplicateName($integrationId, $name)) {
|
||||||
|
Flash::set('settings_error', $this->translator->get('settings.integrations.validation.name_taken'));
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$statusSyncDirectionInput = $request->input('order_status_sync_direction', null);
|
||||||
|
$statusSyncDirection = $statusSyncDirectionInput !== null
|
||||||
|
? trim((string) $statusSyncDirectionInput)
|
||||||
|
: (string) ($existing['order_status_sync_direction'] ?? 'shoppro_to_orderpro');
|
||||||
|
$paymentSyncStatusCodesInput = $request->input('payment_sync_status_codes', null);
|
||||||
|
if (is_array($paymentSyncStatusCodesInput)) {
|
||||||
|
$paymentSyncStatusCodes = $paymentSyncStatusCodesInput;
|
||||||
|
} elseif ($tab === 'settings') {
|
||||||
|
$paymentSyncStatusCodes = [];
|
||||||
|
} else {
|
||||||
|
$paymentSyncStatusCodes = $existing['payment_sync_status_codes'] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$savedId = $this->repository->saveIntegration([
|
||||||
|
'integration_id' => $integrationId,
|
||||||
|
'name' => $name,
|
||||||
|
'base_url' => $baseUrl,
|
||||||
|
'api_key' => $apiKey,
|
||||||
|
'timeout_seconds' => max(1, min(120, (int) $request->input('timeout_seconds', 10))),
|
||||||
|
'is_active' => $request->input('is_active', ''),
|
||||||
|
'orders_fetch_enabled' => $request->input('orders_fetch_enabled', ''),
|
||||||
|
'orders_fetch_start_date' => $ordersFetchStartDate,
|
||||||
|
'order_status_sync_direction' => $statusSyncDirection,
|
||||||
|
'payment_sync_status_codes' => $paymentSyncStatusCodes,
|
||||||
|
]);
|
||||||
|
$this->saveImportIntervalIfRequested($request);
|
||||||
|
$this->saveStatusSyncIntervalIfRequested($request);
|
||||||
|
$this->savePaymentSyncIntervalIfRequested($request);
|
||||||
|
|
||||||
|
$flashKey = $integrationId > 0
|
||||||
|
? 'settings.integrations.flash.updated'
|
||||||
|
: 'settings.integrations.flash.created';
|
||||||
|
Flash::set('settings_success', $this->translator->get($flashKey));
|
||||||
|
|
||||||
|
return Response::redirect($this->buildRedirectUrl($savedId, $tab));
|
||||||
|
} catch (Throwable) {
|
||||||
|
Flash::set('settings_error', $this->translator->get('settings.integrations.flash.failed'));
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test(Request $request): Response
|
||||||
|
{
|
||||||
|
$integrationId = max(0, (int) $request->input('integration_id', 0));
|
||||||
|
$tab = $this->resolveTab((string) $request->input('tab', 'integration'));
|
||||||
|
$redirectBase = '/settings/integrations/shoppro';
|
||||||
|
$redirectTo = $this->buildRedirectUrl($integrationId, $tab);
|
||||||
|
|
||||||
|
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||||
|
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($integrationId <= 0) {
|
||||||
|
Flash::set('settings_error', $this->translator->get('settings.integrations.flash.not_found'));
|
||||||
|
return Response::redirect($this->buildRedirectUrl(0, $tab));
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->repository->testConnection($integrationId);
|
||||||
|
$isOk = (string) ($result['status'] ?? 'error') === 'ok';
|
||||||
|
$message = trim((string) ($result['message'] ?? ''));
|
||||||
|
$httpCode = $result['http_code'] ?? null;
|
||||||
|
|
||||||
|
if ($isOk) {
|
||||||
|
Flash::set('settings_success', $this->translator->get('settings.integrations.flash.test_ok'));
|
||||||
|
} else {
|
||||||
|
$suffix = $message !== '' ? ' ' . $message : '';
|
||||||
|
$httpPart = $httpCode !== null ? ' (HTTP ' . (string) $httpCode . ')' : '';
|
||||||
|
Flash::set(
|
||||||
|
'settings_error',
|
||||||
|
$this->translator->get('settings.integrations.flash.test_failed') . $httpPart . $suffix
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveStatusMappings(Request $request): Response
|
||||||
|
{
|
||||||
|
$integrationId = max(0, (int) $request->input('integration_id', 0));
|
||||||
|
$redirectTo = $this->buildRedirectUrl($integrationId, 'statuses');
|
||||||
|
|
||||||
|
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||||
|
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($integrationId <= 0 || $this->repository->findIntegration($integrationId) === null) {
|
||||||
|
Flash::set('settings_error', $this->translator->get('settings.integrations.flash.not_found'));
|
||||||
|
return Response::redirect($this->buildRedirectUrl(0, 'statuses'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$shopCodes = $request->input('shoppro_status_code', []);
|
||||||
|
$shopNames = $request->input('shoppro_status_name', []);
|
||||||
|
$orderCodes = $request->input('orderpro_status_code', []);
|
||||||
|
if (!is_array($shopCodes) || !is_array($shopNames) || !is_array($orderCodes)) {
|
||||||
|
Flash::set('settings_error', $this->translator->get('settings.integrations.statuses.flash.invalid_payload'));
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowedOrderpro = $this->resolveAllowedOrderproStatusCodes();
|
||||||
|
$rowsCount = min(count($shopCodes), count($shopNames), count($orderCodes));
|
||||||
|
$mappings = [];
|
||||||
|
for ($index = 0; $index < $rowsCount; $index++) {
|
||||||
|
$shopCode = trim((string) ($shopCodes[$index] ?? ''));
|
||||||
|
$shopName = trim((string) ($shopNames[$index] ?? ''));
|
||||||
|
$orderCode = strtolower(trim((string) ($orderCodes[$index] ?? '')));
|
||||||
|
|
||||||
|
if ($shopCode === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($orderCode === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($allowedOrderpro[$orderCode])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mappings[] = [
|
||||||
|
'shoppro_status_code' => $shopCode,
|
||||||
|
'shoppro_status_name' => $shopName,
|
||||||
|
'orderpro_status_code' => $orderCode,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->statusMappings->replaceForIntegration($integrationId, $mappings);
|
||||||
|
Flash::set('settings_success', $this->translator->get('settings.integrations.statuses.flash.saved'));
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
Flash::set(
|
||||||
|
'settings_error',
|
||||||
|
$this->translator->get('settings.integrations.statuses.flash.save_failed') . ' ' . $exception->getMessage()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function syncStatuses(Request $request): Response
|
||||||
|
{
|
||||||
|
$integrationId = max(0, (int) $request->input('integration_id', 0));
|
||||||
|
$redirectTo = $this->buildRedirectUrl($integrationId, 'statuses');
|
||||||
|
|
||||||
|
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||||
|
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($integrationId <= 0 || $this->repository->findIntegration($integrationId) === null) {
|
||||||
|
Flash::set('settings_error', $this->translator->get('settings.integrations.flash.not_found'));
|
||||||
|
return Response::redirect($this->buildRedirectUrl(0, 'statuses'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->repository->fetchOrderStatuses($integrationId);
|
||||||
|
if (($result['ok'] ?? false) !== true) {
|
||||||
|
$message = trim((string) ($result['message'] ?? ''));
|
||||||
|
Flash::set(
|
||||||
|
'settings_error',
|
||||||
|
$this->translator->get('settings.integrations.statuses.flash.sync_failed') . ($message !== '' ? ' ' . $message : '')
|
||||||
|
);
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
$statuses = $result['statuses'] ?? [];
|
||||||
|
Flash::set('shoppro_discovered_statuses', is_array($statuses) ? $statuses : []);
|
||||||
|
Flash::set(
|
||||||
|
'settings_success',
|
||||||
|
$this->translator->get('settings.integrations.statuses.flash.sync_ok', [
|
||||||
|
'count' => (string) (is_array($statuses) ? count($statuses) : 0),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveDeliveryMappings(Request $request): Response
|
||||||
|
{
|
||||||
|
$integrationId = max(0, (int) $request->input('integration_id', 0));
|
||||||
|
$redirectTo = $this->buildRedirectUrl($integrationId, 'delivery');
|
||||||
|
|
||||||
|
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||||
|
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($integrationId <= 0 || $this->repository->findIntegration($integrationId) === null) {
|
||||||
|
Flash::set('settings_error', $this->translator->get('settings.integrations.flash.not_found'));
|
||||||
|
return Response::redirect($this->buildRedirectUrl(0, 'delivery'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$orderMethods = (array) $request->input('order_delivery_method', []);
|
||||||
|
$carriers = (array) $request->input('carrier', []);
|
||||||
|
$allegroMethodIds = (array) $request->input('allegro_delivery_method_id', []);
|
||||||
|
$credentialsIds = (array) $request->input('allegro_credentials_id', []);
|
||||||
|
$carrierIds = (array) $request->input('allegro_carrier_id', []);
|
||||||
|
$serviceNames = (array) $request->input('allegro_service_name', []);
|
||||||
|
|
||||||
|
$mappings = [];
|
||||||
|
foreach ($orderMethods as $index => $orderMethod) {
|
||||||
|
$orderMethodValue = trim((string) $orderMethod);
|
||||||
|
$carrier = trim((string) ($carriers[$index] ?? 'allegro'));
|
||||||
|
$allegroMethodId = trim((string) ($allegroMethodIds[$index] ?? ''));
|
||||||
|
if ($orderMethodValue === '' || $allegroMethodId === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mappings[] = [
|
||||||
|
'order_delivery_method' => $orderMethodValue,
|
||||||
|
'carrier' => $carrier,
|
||||||
|
'allegro_delivery_method_id' => $allegroMethodId,
|
||||||
|
'allegro_credentials_id' => trim((string) ($credentialsIds[$index] ?? '')),
|
||||||
|
'allegro_carrier_id' => trim((string) ($carrierIds[$index] ?? '')),
|
||||||
|
'allegro_service_name' => trim((string) ($serviceNames[$index] ?? '')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->deliveryMappings->saveMappings($integrationId, $mappings);
|
||||||
|
Flash::set('settings_success', $this->translator->get('settings.integrations.delivery.flash.saved'));
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
Flash::set(
|
||||||
|
'settings_error',
|
||||||
|
$this->translator->get('settings.integrations.delivery.flash.save_failed') . ' ' . $exception->getMessage()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed>|null $integration
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function buildFormValues(?array $integration): array
|
||||||
|
{
|
||||||
|
if ($integration === null) {
|
||||||
|
return [
|
||||||
|
'integration_id' => 0,
|
||||||
|
'name' => '',
|
||||||
|
'base_url' => '',
|
||||||
|
'timeout_seconds' => 10,
|
||||||
|
'is_active' => 1,
|
||||||
|
'orders_fetch_enabled' => 0,
|
||||||
|
'orders_fetch_start_date' => '',
|
||||||
|
'order_status_sync_direction' => 'shoppro_to_orderpro',
|
||||||
|
'payment_sync_status_codes' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'integration_id' => (int) ($integration['id'] ?? 0),
|
||||||
|
'name' => (string) ($integration['name'] ?? ''),
|
||||||
|
'base_url' => (string) ($integration['base_url'] ?? ''),
|
||||||
|
'timeout_seconds' => (int) ($integration['timeout_seconds'] ?? 10),
|
||||||
|
'is_active' => !empty($integration['is_active']) ? 1 : 0,
|
||||||
|
'orders_fetch_enabled' => !empty($integration['orders_fetch_enabled']) ? 1 : 0,
|
||||||
|
'orders_fetch_start_date' => (string) ($integration['orders_fetch_start_date'] ?? ''),
|
||||||
|
'order_status_sync_direction' => (string) ($integration['order_status_sync_direction'] ?? 'shoppro_to_orderpro'),
|
||||||
|
'payment_sync_status_codes' => is_array($integration['payment_sync_status_codes'] ?? null)
|
||||||
|
? $integration['payment_sync_status_codes']
|
||||||
|
: [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isDuplicateName(int $currentId, string $name): bool
|
||||||
|
{
|
||||||
|
$needle = mb_strtolower(trim($name));
|
||||||
|
if ($needle === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = $this->repository->listIntegrations();
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$rowId = (int) ($row['id'] ?? 0);
|
||||||
|
if ($rowId === $currentId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rowName = mb_strtolower(trim((string) ($row['name'] ?? '')));
|
||||||
|
if ($rowName === $needle) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isValidHttpUrl(string $value): bool
|
||||||
|
{
|
||||||
|
if (filter_var($value, FILTER_VALIDATE_URL) === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scheme = strtolower((string) parse_url($value, PHP_URL_SCHEME));
|
||||||
|
return in_array($scheme, ['http', 'https'], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isValidYmdDate(string $value): bool
|
||||||
|
{
|
||||||
|
$date = DateTimeImmutable::createFromFormat('Y-m-d', $value);
|
||||||
|
return $date !== false && $date->format('Y-m-d') === $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, array{shoppro_status_code:string,shoppro_status_name:string,orderpro_status_code:string}>
|
||||||
|
*/
|
||||||
|
private function buildStatusRows(int $integrationId, array $discoveredStatuses): array
|
||||||
|
{
|
||||||
|
$mappedRows = $this->statusMappings->listByIntegration($integrationId);
|
||||||
|
$result = [];
|
||||||
|
|
||||||
|
foreach ($mappedRows as $row) {
|
||||||
|
$code = trim((string) ($row['shoppro_status_code'] ?? ''));
|
||||||
|
if ($code === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = mb_strtolower($code);
|
||||||
|
$result[$key] = [
|
||||||
|
'shoppro_status_code' => $code,
|
||||||
|
'shoppro_status_name' => trim((string) ($row['shoppro_status_name'] ?? '')),
|
||||||
|
'orderpro_status_code' => strtolower(trim((string) ($row['orderpro_status_code'] ?? ''))),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($discoveredStatuses as $status) {
|
||||||
|
if (!is_array($status)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = trim((string) ($status['code'] ?? ''));
|
||||||
|
if ($code === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = mb_strtolower($code);
|
||||||
|
if (!isset($result[$key])) {
|
||||||
|
$result[$key] = [
|
||||||
|
'shoppro_status_code' => $code,
|
||||||
|
'shoppro_status_name' => trim((string) ($status['name'] ?? '')),
|
||||||
|
'orderpro_status_code' => '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uasort($result, static function (array $left, array $right): int {
|
||||||
|
return strcmp(
|
||||||
|
strtolower((string) ($left['shoppro_status_code'] ?? '')),
|
||||||
|
strtolower((string) ($right['shoppro_status_code'] ?? ''))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return array_values($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, true>
|
||||||
|
*/
|
||||||
|
private function resolveAllowedOrderproStatusCodes(): array
|
||||||
|
{
|
||||||
|
$allowed = [];
|
||||||
|
foreach ($this->orderStatuses->listStatuses() as $status) {
|
||||||
|
if (!is_array($status)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = strtolower(trim((string) ($status['code'] ?? '')));
|
||||||
|
if ($code === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowed[$code] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array{code:string,name:string}>
|
||||||
|
*/
|
||||||
|
private function readDiscoveredStatuses(): array
|
||||||
|
{
|
||||||
|
$raw = Flash::get('shoppro_discovered_statuses', []);
|
||||||
|
if (!is_array($raw)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach ($raw as $item) {
|
||||||
|
if (!is_array($item)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = trim((string) ($item['code'] ?? ''));
|
||||||
|
if ($code === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result[] = [
|
||||||
|
'code' => $code,
|
||||||
|
'name' => trim((string) ($item['name'] ?? $code)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveTab(string $candidate): string
|
||||||
|
{
|
||||||
|
$value = trim($candidate);
|
||||||
|
$allowed = ['integration', 'statuses', 'settings', 'delivery'];
|
||||||
|
if (!in_array($value, $allowed, true)) {
|
||||||
|
return 'integration';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildRedirectUrl(int $integrationId, string $tab): string
|
||||||
|
{
|
||||||
|
$url = '/settings/integrations/shoppro';
|
||||||
|
$query = [];
|
||||||
|
if ($integrationId > 0) {
|
||||||
|
$query['id'] = (string) $integrationId;
|
||||||
|
}
|
||||||
|
if ($tab !== 'integration') {
|
||||||
|
$query['tab'] = $tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($query === []) {
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $url . '?' . http_build_query($query);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function currentImportIntervalMinutes(): int
|
||||||
|
{
|
||||||
|
$schedule = $this->findImportSchedule();
|
||||||
|
$seconds = (int) ($schedule['interval_seconds'] ?? self::ORDERS_IMPORT_DEFAULT_INTERVAL_SECONDS);
|
||||||
|
return max(1, min(1440, (int) floor(max(60, $seconds) / 60)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function findImportSchedule(): array
|
||||||
|
{
|
||||||
|
foreach ($this->cronRepository->listSchedules() as $schedule) {
|
||||||
|
if ((string) ($schedule['job_type'] ?? '') !== self::ORDERS_IMPORT_JOB_TYPE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $schedule;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function findStatusSyncSchedule(): array
|
||||||
|
{
|
||||||
|
foreach ($this->cronRepository->listSchedules() as $schedule) {
|
||||||
|
if ((string) ($schedule['job_type'] ?? '') !== self::ORDER_STATUS_SYNC_JOB_TYPE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $schedule;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function findPaymentSyncSchedule(): array
|
||||||
|
{
|
||||||
|
foreach ($this->cronRepository->listSchedules() as $schedule) {
|
||||||
|
if ((string) ($schedule['job_type'] ?? '') !== self::PAYMENT_SYNC_JOB_TYPE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $schedule;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ensureImportScheduleExists(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if ($this->findImportSchedule() !== []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$this->cronRepository->upsertSchedule(
|
||||||
|
self::ORDERS_IMPORT_JOB_TYPE,
|
||||||
|
self::ORDERS_IMPORT_DEFAULT_INTERVAL_SECONDS,
|
||||||
|
self::ORDERS_IMPORT_DEFAULT_PRIORITY,
|
||||||
|
self::ORDERS_IMPORT_DEFAULT_MAX_ATTEMPTS,
|
||||||
|
null,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
} catch (Throwable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ensureStatusSyncScheduleExists(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if ($this->findStatusSyncSchedule() !== []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$this->cronRepository->upsertSchedule(
|
||||||
|
self::ORDER_STATUS_SYNC_JOB_TYPE,
|
||||||
|
self::ORDER_STATUS_SYNC_DEFAULT_INTERVAL_SECONDS,
|
||||||
|
self::ORDER_STATUS_SYNC_DEFAULT_PRIORITY,
|
||||||
|
self::ORDER_STATUS_SYNC_DEFAULT_MAX_ATTEMPTS,
|
||||||
|
null,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
} catch (Throwable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ensurePaymentSyncScheduleExists(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if ($this->findPaymentSyncSchedule() !== []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$this->cronRepository->upsertSchedule(
|
||||||
|
self::PAYMENT_SYNC_JOB_TYPE,
|
||||||
|
self::PAYMENT_SYNC_DEFAULT_INTERVAL_SECONDS,
|
||||||
|
self::PAYMENT_SYNC_DEFAULT_PRIORITY,
|
||||||
|
self::PAYMENT_SYNC_DEFAULT_MAX_ATTEMPTS,
|
||||||
|
null,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
} catch (Throwable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function saveImportIntervalIfRequested(Request $request): void
|
||||||
|
{
|
||||||
|
if ($request->input('orders_import_interval_minutes', null) === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->ensureImportScheduleExists();
|
||||||
|
$minutes = max(1, min(1440, (int) $request->input('orders_import_interval_minutes', 5)));
|
||||||
|
$schedule = $this->findImportSchedule();
|
||||||
|
$priority = max(1, min(255, (int) ($schedule['priority'] ?? self::ORDERS_IMPORT_DEFAULT_PRIORITY)));
|
||||||
|
$maxAttempts = max(1, min(20, (int) ($schedule['max_attempts'] ?? self::ORDERS_IMPORT_DEFAULT_MAX_ATTEMPTS)));
|
||||||
|
$payload = is_array($schedule['payload'] ?? null) ? $schedule['payload'] : null;
|
||||||
|
$enabled = array_key_exists('enabled', $schedule) ? !empty($schedule['enabled']) : true;
|
||||||
|
|
||||||
|
$this->cronRepository->upsertSchedule(
|
||||||
|
self::ORDERS_IMPORT_JOB_TYPE,
|
||||||
|
$minutes * 60,
|
||||||
|
$priority,
|
||||||
|
$maxAttempts,
|
||||||
|
$payload,
|
||||||
|
$enabled
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function currentStatusSyncIntervalMinutes(): int
|
||||||
|
{
|
||||||
|
$schedule = $this->findStatusSyncSchedule();
|
||||||
|
$seconds = (int) ($schedule['interval_seconds'] ?? self::ORDER_STATUS_SYNC_DEFAULT_INTERVAL_SECONDS);
|
||||||
|
return max(1, min(1440, (int) floor(max(60, $seconds) / 60)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function saveStatusSyncIntervalIfRequested(Request $request): void
|
||||||
|
{
|
||||||
|
if ($request->input('status_sync_interval_minutes', null) === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->ensureStatusSyncScheduleExists();
|
||||||
|
$minutes = max(1, min(1440, (int) $request->input('status_sync_interval_minutes', 15)));
|
||||||
|
$schedule = $this->findStatusSyncSchedule();
|
||||||
|
$priority = max(1, min(255, (int) ($schedule['priority'] ?? self::ORDER_STATUS_SYNC_DEFAULT_PRIORITY)));
|
||||||
|
$maxAttempts = max(1, min(20, (int) ($schedule['max_attempts'] ?? self::ORDER_STATUS_SYNC_DEFAULT_MAX_ATTEMPTS)));
|
||||||
|
$payload = is_array($schedule['payload'] ?? null) ? $schedule['payload'] : null;
|
||||||
|
$enabled = array_key_exists('enabled', $schedule) ? !empty($schedule['enabled']) : true;
|
||||||
|
|
||||||
|
$this->cronRepository->upsertSchedule(
|
||||||
|
self::ORDER_STATUS_SYNC_JOB_TYPE,
|
||||||
|
$minutes * 60,
|
||||||
|
$priority,
|
||||||
|
$maxAttempts,
|
||||||
|
$payload,
|
||||||
|
$enabled
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function currentPaymentSyncIntervalMinutes(): int
|
||||||
|
{
|
||||||
|
$schedule = $this->findPaymentSyncSchedule();
|
||||||
|
$seconds = (int) ($schedule['interval_seconds'] ?? self::PAYMENT_SYNC_DEFAULT_INTERVAL_SECONDS);
|
||||||
|
return max(1, min(1440, (int) floor(max(60, $seconds) / 60)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function savePaymentSyncIntervalIfRequested(Request $request): void
|
||||||
|
{
|
||||||
|
if ($request->input('payment_sync_interval_minutes', null) === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->ensurePaymentSyncScheduleExists();
|
||||||
|
$minutes = max(1, min(1440, (int) $request->input('payment_sync_interval_minutes', 10)));
|
||||||
|
$schedule = $this->findPaymentSyncSchedule();
|
||||||
|
$priority = max(1, min(255, (int) ($schedule['priority'] ?? self::PAYMENT_SYNC_DEFAULT_PRIORITY)));
|
||||||
|
$maxAttempts = max(1, min(20, (int) ($schedule['max_attempts'] ?? self::PAYMENT_SYNC_DEFAULT_MAX_ATTEMPTS)));
|
||||||
|
$payload = is_array($schedule['payload'] ?? null) ? $schedule['payload'] : null;
|
||||||
|
$enabled = array_key_exists('enabled', $schedule) ? !empty($schedule['enabled']) : true;
|
||||||
|
|
||||||
|
$this->cronRepository->upsertSchedule(
|
||||||
|
self::PAYMENT_SYNC_JOB_TYPE,
|
||||||
|
$minutes * 60,
|
||||||
|
$priority,
|
||||||
|
$maxAttempts,
|
||||||
|
$payload,
|
||||||
|
$enabled
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{0: array<int, array<string, mixed>>, 1: string}
|
||||||
|
*/
|
||||||
|
private function loadDeliveryServices(): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$oauth = $this->allegroIntegrationRepository->getTokenCredentials();
|
||||||
|
if (!is_array($oauth)) {
|
||||||
|
return [[], $this->translator->get('settings.integrations.delivery.not_connected')];
|
||||||
|
}
|
||||||
|
|
||||||
|
$env = (string) ($oauth['environment'] ?? 'sandbox');
|
||||||
|
$accessToken = trim((string) ($oauth['access_token'] ?? ''));
|
||||||
|
if ($accessToken === '') {
|
||||||
|
return [[], $this->translator->get('settings.integrations.delivery.not_connected')];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = $this->allegroApiClient->getDeliveryServices($env, $accessToken);
|
||||||
|
} catch (RuntimeException $exception) {
|
||||||
|
if (trim($exception->getMessage()) !== 'ALLEGRO_HTTP_401') {
|
||||||
|
throw $exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
$refreshedToken = $this->refreshAllegroAccessToken($oauth);
|
||||||
|
if ($refreshedToken === null) {
|
||||||
|
return [[], $this->translator->get('settings.integrations.delivery.not_connected')];
|
||||||
|
}
|
||||||
|
$response = $this->allegroApiClient->getDeliveryServices($env, $refreshedToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
$services = is_array($response['services'] ?? null) ? $response['services'] : [];
|
||||||
|
return [$services, ''];
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
return [[], $exception->getMessage()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $oauth
|
||||||
|
*/
|
||||||
|
private function refreshAllegroAccessToken(array $oauth): ?string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$token = $this->allegroOAuthClient->refreshAccessToken(
|
||||||
|
(string) ($oauth['environment'] ?? 'sandbox'),
|
||||||
|
(string) ($oauth['client_id'] ?? ''),
|
||||||
|
(string) ($oauth['client_secret'] ?? ''),
|
||||||
|
(string) ($oauth['refresh_token'] ?? '')
|
||||||
|
);
|
||||||
|
|
||||||
|
$expiresIn = max(0, (int) ($token['expires_in'] ?? 0));
|
||||||
|
$expiresAt = $expiresIn > 0
|
||||||
|
? (new DateTimeImmutable('now'))->add(new DateInterval('PT' . $expiresIn . 'S'))->format('Y-m-d H:i:s')
|
||||||
|
: null;
|
||||||
|
$refreshToken = trim((string) ($token['refresh_token'] ?? ''));
|
||||||
|
if ($refreshToken === '') {
|
||||||
|
$refreshToken = (string) ($oauth['refresh_token'] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->allegroIntegrationRepository->saveTokens(
|
||||||
|
(string) ($token['access_token'] ?? ''),
|
||||||
|
$refreshToken,
|
||||||
|
(string) ($token['token_type'] ?? ''),
|
||||||
|
(string) ($token['scope'] ?? ''),
|
||||||
|
$expiresAt
|
||||||
|
);
|
||||||
|
|
||||||
|
$accessToken = trim((string) ($token['access_token'] ?? ''));
|
||||||
|
return $accessToken !== '' ? $accessToken : null;
|
||||||
|
} catch (Throwable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
591
src/Modules/Settings/ShopproIntegrationsRepository.php
Normal file
591
src/Modules/Settings/ShopproIntegrationsRepository.php
Normal file
@@ -0,0 +1,591 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Settings;
|
||||||
|
|
||||||
|
use PDO;
|
||||||
|
use RuntimeException;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
final class ShopproIntegrationsRepository
|
||||||
|
{
|
||||||
|
private const TYPE = 'shoppro';
|
||||||
|
private const DIRECTION_SHOPPRO_TO_ORDERPRO = 'shoppro_to_orderpro';
|
||||||
|
private const DIRECTION_ORDERPRO_TO_SHOPPRO = 'orderpro_to_shoppro';
|
||||||
|
|
||||||
|
private readonly IntegrationSecretCipher $cipher;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly PDO $pdo,
|
||||||
|
private readonly string $secret
|
||||||
|
) {
|
||||||
|
$this->cipher = new IntegrationSecretCipher($this->secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
public function listIntegrations(): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$statement = $this->pdo->prepare(
|
||||||
|
'SELECT *
|
||||||
|
FROM integrations
|
||||||
|
WHERE type = :type
|
||||||
|
ORDER BY is_active DESC, name ASC, id ASC'
|
||||||
|
);
|
||||||
|
$statement->execute(['type' => self::TYPE]);
|
||||||
|
$rows = $statement->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
} catch (Throwable) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_array($rows)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
if (!is_array($row)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$encrypted = trim((string) ($row['api_key_encrypted'] ?? ''));
|
||||||
|
$result[] = [
|
||||||
|
'id' => (int) ($row['id'] ?? 0),
|
||||||
|
'name' => trim((string) ($row['name'] ?? '')),
|
||||||
|
'base_url' => trim((string) ($row['base_url'] ?? '')),
|
||||||
|
'timeout_seconds' => (int) ($row['timeout_seconds'] ?? 10),
|
||||||
|
'is_active' => (int) ($row['is_active'] ?? 0) === 1,
|
||||||
|
'orders_fetch_enabled' => (int) ($row['orders_fetch_enabled'] ?? 0) === 1,
|
||||||
|
'orders_fetch_start_date' => trim((string) ($row['orders_fetch_start_date'] ?? '')),
|
||||||
|
'order_status_sync_direction' => $this->normalizeStatusSyncDirection((string) ($row['order_status_sync_direction'] ?? '')),
|
||||||
|
'payment_sync_status_codes' => $this->decodeStatusCodesJson($row['payment_sync_status_codes_json'] ?? null),
|
||||||
|
'has_api_key' => $encrypted !== '',
|
||||||
|
'last_test_status' => trim((string) ($row['last_test_status'] ?? '')),
|
||||||
|
'last_test_http_code' => $row['last_test_http_code'] !== null ? (int) $row['last_test_http_code'] : null,
|
||||||
|
'last_test_message' => trim((string) ($row['last_test_message'] ?? '')),
|
||||||
|
'last_test_at' => trim((string) ($row['last_test_at'] ?? '')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>|null
|
||||||
|
*/
|
||||||
|
public function findIntegration(int $integrationId): ?array
|
||||||
|
{
|
||||||
|
if ($integrationId <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$statement = $this->pdo->prepare(
|
||||||
|
'SELECT *
|
||||||
|
FROM integrations
|
||||||
|
WHERE id = :id AND type = :type
|
||||||
|
LIMIT 1'
|
||||||
|
);
|
||||||
|
$statement->execute([
|
||||||
|
'id' => $integrationId,
|
||||||
|
'type' => self::TYPE,
|
||||||
|
]);
|
||||||
|
$row = $statement->fetch(PDO::FETCH_ASSOC);
|
||||||
|
} catch (Throwable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_array($row)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => (int) ($row['id'] ?? 0),
|
||||||
|
'name' => trim((string) ($row['name'] ?? '')),
|
||||||
|
'base_url' => trim((string) ($row['base_url'] ?? '')),
|
||||||
|
'timeout_seconds' => (int) ($row['timeout_seconds'] ?? 10),
|
||||||
|
'is_active' => (int) ($row['is_active'] ?? 0) === 1,
|
||||||
|
'orders_fetch_enabled' => (int) ($row['orders_fetch_enabled'] ?? 0) === 1,
|
||||||
|
'orders_fetch_start_date' => trim((string) ($row['orders_fetch_start_date'] ?? '')),
|
||||||
|
'order_status_sync_direction' => $this->normalizeStatusSyncDirection((string) ($row['order_status_sync_direction'] ?? '')),
|
||||||
|
'payment_sync_status_codes' => $this->decodeStatusCodesJson($row['payment_sync_status_codes_json'] ?? null),
|
||||||
|
'payment_sync_status_codes_json' => $row['payment_sync_status_codes_json'] ?? null,
|
||||||
|
'api_key_encrypted' => trim((string) ($row['api_key_encrypted'] ?? '')),
|
||||||
|
'has_api_key' => trim((string) ($row['api_key_encrypted'] ?? '')) !== '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getApiKeyDecrypted(int $integrationId): ?string
|
||||||
|
{
|
||||||
|
$integration = $this->findIntegration($integrationId);
|
||||||
|
if ($integration === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$encrypted = trim((string) ($integration['api_key_encrypted'] ?? ''));
|
||||||
|
if ($encrypted === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$decrypted = (string) $this->cipher->decrypt($encrypted);
|
||||||
|
$trimmed = trim($decrypted);
|
||||||
|
return $trimmed === '' ? null : $trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $payload
|
||||||
|
*/
|
||||||
|
public function saveIntegration(array $payload): int
|
||||||
|
{
|
||||||
|
$integrationId = (int) ($payload['integration_id'] ?? 0);
|
||||||
|
$name = trim((string) ($payload['name'] ?? ''));
|
||||||
|
$baseUrl = rtrim(trim((string) ($payload['base_url'] ?? '')), '/');
|
||||||
|
$timeoutSeconds = max(1, (int) ($payload['timeout_seconds'] ?? 10));
|
||||||
|
$isActive = !empty($payload['is_active']) ? 1 : 0;
|
||||||
|
$ordersFetchEnabled = !empty($payload['orders_fetch_enabled']) ? 1 : 0;
|
||||||
|
$ordersFetchStartDate = $this->normalizeDate((string) ($payload['orders_fetch_start_date'] ?? ''));
|
||||||
|
$statusSyncDirection = $this->normalizeStatusSyncDirection((string) ($payload['order_status_sync_direction'] ?? ''));
|
||||||
|
$paymentSyncStatusCodesJson = $this->encodeStatusCodesJson($payload['payment_sync_status_codes'] ?? []);
|
||||||
|
$apiKey = trim((string) ($payload['api_key'] ?? ''));
|
||||||
|
|
||||||
|
if ($integrationId > 0) {
|
||||||
|
$existing = $this->findIntegration($integrationId);
|
||||||
|
if ($existing === null) {
|
||||||
|
throw new RuntimeException('INTEGRATION_NOT_FOUND');
|
||||||
|
}
|
||||||
|
|
||||||
|
$encryptedApiKey = trim((string) ($existing['api_key_encrypted'] ?? ''));
|
||||||
|
if ($apiKey !== '') {
|
||||||
|
$encryptedApiKey = (string) $this->cipher->encrypt($apiKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
$statement = $this->pdo->prepare(
|
||||||
|
'UPDATE integrations
|
||||||
|
SET name = :name,
|
||||||
|
base_url = :base_url,
|
||||||
|
api_key_encrypted = :api_key_encrypted,
|
||||||
|
timeout_seconds = :timeout_seconds,
|
||||||
|
is_active = :is_active,
|
||||||
|
orders_fetch_enabled = :orders_fetch_enabled,
|
||||||
|
orders_fetch_start_date = :orders_fetch_start_date,
|
||||||
|
order_status_sync_direction = :order_status_sync_direction,
|
||||||
|
payment_sync_status_codes_json = :payment_sync_status_codes_json,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = :id AND type = :type'
|
||||||
|
);
|
||||||
|
$statement->execute([
|
||||||
|
'id' => $integrationId,
|
||||||
|
'type' => self::TYPE,
|
||||||
|
'name' => $name,
|
||||||
|
'base_url' => $baseUrl,
|
||||||
|
'api_key_encrypted' => $this->nullableString($encryptedApiKey),
|
||||||
|
'timeout_seconds' => $timeoutSeconds,
|
||||||
|
'is_active' => $isActive,
|
||||||
|
'orders_fetch_enabled' => $ordersFetchEnabled,
|
||||||
|
'orders_fetch_start_date' => $ordersFetchStartDate,
|
||||||
|
'order_status_sync_direction' => $statusSyncDirection,
|
||||||
|
'payment_sync_status_codes_json' => $paymentSyncStatusCodesJson,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $integrationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
$encryptedApiKey = $this->cipher->encrypt($apiKey);
|
||||||
|
$statement = $this->pdo->prepare(
|
||||||
|
'INSERT INTO integrations (
|
||||||
|
type, name, base_url, api_key_encrypted, timeout_seconds, is_active, orders_fetch_enabled, orders_fetch_start_date, order_status_sync_direction, payment_sync_status_codes_json, created_at, updated_at
|
||||||
|
) VALUES (
|
||||||
|
:type, :name, :base_url, :api_key_encrypted, :timeout_seconds, :is_active, :orders_fetch_enabled, :orders_fetch_start_date, :order_status_sync_direction, :payment_sync_status_codes_json, NOW(), NOW()
|
||||||
|
)'
|
||||||
|
);
|
||||||
|
$statement->execute([
|
||||||
|
'type' => self::TYPE,
|
||||||
|
'name' => $name,
|
||||||
|
'base_url' => $baseUrl,
|
||||||
|
'api_key_encrypted' => $this->nullableString((string) $encryptedApiKey),
|
||||||
|
'timeout_seconds' => $timeoutSeconds,
|
||||||
|
'is_active' => $isActive,
|
||||||
|
'orders_fetch_enabled' => $ordersFetchEnabled,
|
||||||
|
'orders_fetch_start_date' => $ordersFetchStartDate,
|
||||||
|
'order_status_sync_direction' => $statusSyncDirection,
|
||||||
|
'payment_sync_status_codes_json' => $paymentSyncStatusCodesJson,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (int) $this->pdo->lastInsertId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{status:string,http_code:int|null,message:string}
|
||||||
|
*/
|
||||||
|
public function testConnection(int $integrationId): array
|
||||||
|
{
|
||||||
|
$integration = $this->findIntegration($integrationId);
|
||||||
|
if ($integration === null) {
|
||||||
|
return [
|
||||||
|
'status' => 'error',
|
||||||
|
'http_code' => null,
|
||||||
|
'message' => 'Nie znaleziono integracji shopPRO.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiKeyEncrypted = trim((string) ($integration['api_key_encrypted'] ?? ''));
|
||||||
|
$apiKey = $apiKeyEncrypted !== '' ? (string) $this->cipher->decrypt($apiKeyEncrypted) : '';
|
||||||
|
if ($apiKey === '') {
|
||||||
|
return [
|
||||||
|
'status' => 'error',
|
||||||
|
'http_code' => null,
|
||||||
|
'message' => 'Brak zapisanego klucza API.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$baseUrl = rtrim((string) ($integration['base_url'] ?? ''), '/');
|
||||||
|
$timeout = max(1, min(120, (int) ($integration['timeout_seconds'] ?? 10)));
|
||||||
|
$url = $baseUrl . '/api.php?endpoint=dictionaries&action=statuses';
|
||||||
|
|
||||||
|
$curl = curl_init($url);
|
||||||
|
if ($curl === false) {
|
||||||
|
return [
|
||||||
|
'status' => 'error',
|
||||||
|
'http_code' => null,
|
||||||
|
'message' => 'Nie udalo sie zainicjalizowac polaczenia HTTP.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_setopt_array($curl, [
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_TIMEOUT => $timeout,
|
||||||
|
CURLOPT_CONNECTTIMEOUT => $timeout,
|
||||||
|
CURLOPT_HTTPHEADER => [
|
||||||
|
'Accept: application/json',
|
||||||
|
'X-Api-Key: ' . $apiKey,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$body = curl_exec($curl);
|
||||||
|
$httpCode = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||||
|
$curlError = curl_error($curl);
|
||||||
|
curl_close($curl);
|
||||||
|
|
||||||
|
if ($body === false) {
|
||||||
|
$message = trim($curlError) !== '' ? trim($curlError) : 'Nieznany blad transportu HTTP.';
|
||||||
|
$result = [
|
||||||
|
'status' => 'error',
|
||||||
|
'http_code' => $httpCode > 0 ? $httpCode : null,
|
||||||
|
'message' => $message,
|
||||||
|
];
|
||||||
|
$this->storeTestResult($integrationId, $url, $result);
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = json_decode((string) $body, true);
|
||||||
|
if (!is_array($decoded)) {
|
||||||
|
$result = [
|
||||||
|
'status' => 'error',
|
||||||
|
'http_code' => $httpCode > 0 ? $httpCode : null,
|
||||||
|
'message' => 'Odpowiedz nie jest poprawnym JSON.',
|
||||||
|
];
|
||||||
|
$this->storeTestResult($integrationId, $url, $result);
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiStatus = trim((string) ($decoded['status'] ?? ''));
|
||||||
|
$isOk = $httpCode >= 200 && $httpCode < 300 && $apiStatus === 'ok';
|
||||||
|
$message = $isOk
|
||||||
|
? 'Polaczenie z shopPRO dziala poprawnie.'
|
||||||
|
: trim((string) ($decoded['message'] ?? 'Blad odpowiedzi API shopPRO.'));
|
||||||
|
|
||||||
|
$result = [
|
||||||
|
'status' => $isOk ? 'ok' : 'error',
|
||||||
|
'http_code' => $httpCode > 0 ? $httpCode : null,
|
||||||
|
'message' => $message,
|
||||||
|
];
|
||||||
|
$this->storeTestResult($integrationId, $url, $result);
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{ok:bool,http_code:int|null,message:string,statuses:array<int,array{code:string,name:string}>}
|
||||||
|
*/
|
||||||
|
public function fetchOrderStatuses(int $integrationId): array
|
||||||
|
{
|
||||||
|
$integration = $this->findIntegration($integrationId);
|
||||||
|
if ($integration === null) {
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'http_code' => null,
|
||||||
|
'message' => 'Nie znaleziono integracji shopPRO.',
|
||||||
|
'statuses' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiKeyEncrypted = trim((string) ($integration['api_key_encrypted'] ?? ''));
|
||||||
|
$apiKey = $apiKeyEncrypted !== '' ? (string) $this->cipher->decrypt($apiKeyEncrypted) : '';
|
||||||
|
if ($apiKey === '') {
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'http_code' => null,
|
||||||
|
'message' => 'Brak zapisanego klucza API.',
|
||||||
|
'statuses' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$baseUrl = rtrim((string) ($integration['base_url'] ?? ''), '/');
|
||||||
|
$timeout = max(1, min(120, (int) ($integration['timeout_seconds'] ?? 10)));
|
||||||
|
$url = $baseUrl . '/api.php?endpoint=dictionaries&action=statuses';
|
||||||
|
|
||||||
|
$curl = curl_init($url);
|
||||||
|
if ($curl === false) {
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'http_code' => null,
|
||||||
|
'message' => 'Nie udalo sie zainicjalizowac polaczenia HTTP.',
|
||||||
|
'statuses' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_setopt_array($curl, [
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_TIMEOUT => $timeout,
|
||||||
|
CURLOPT_CONNECTTIMEOUT => $timeout,
|
||||||
|
CURLOPT_HTTPHEADER => [
|
||||||
|
'Accept: application/json',
|
||||||
|
'X-Api-Key: ' . $apiKey,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$body = curl_exec($curl);
|
||||||
|
$httpCode = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||||
|
$curlError = curl_error($curl);
|
||||||
|
curl_close($curl);
|
||||||
|
|
||||||
|
if ($body === false) {
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'http_code' => $httpCode > 0 ? $httpCode : null,
|
||||||
|
'message' => trim($curlError) !== '' ? trim($curlError) : 'Nieznany blad transportu HTTP.',
|
||||||
|
'statuses' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = json_decode((string) $body, true);
|
||||||
|
if (!is_array($decoded)) {
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'http_code' => $httpCode > 0 ? $httpCode : null,
|
||||||
|
'message' => 'Odpowiedz nie jest poprawnym JSON.',
|
||||||
|
'statuses' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = isset($decoded['data']) && is_array($decoded['data'])
|
||||||
|
? $decoded['data']
|
||||||
|
: $decoded;
|
||||||
|
|
||||||
|
$rawStatuses = [];
|
||||||
|
if (isset($payload['statuses']) && is_array($payload['statuses'])) {
|
||||||
|
$rawStatuses = $payload['statuses'];
|
||||||
|
} elseif (isset($payload['order_statuses']) && is_array($payload['order_statuses'])) {
|
||||||
|
$rawStatuses = $payload['order_statuses'];
|
||||||
|
} elseif ($payload !== [] && array_keys($payload) === range(0, count($payload) - 1)) {
|
||||||
|
$rawStatuses = $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
$statuses = $this->normalizeStatusesPayload($rawStatuses);
|
||||||
|
return [
|
||||||
|
'ok' => true,
|
||||||
|
'http_code' => $httpCode > 0 ? $httpCode : null,
|
||||||
|
'message' => '',
|
||||||
|
'statuses' => $statuses,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{status:string,http_code:int|null,message:string} $result
|
||||||
|
*/
|
||||||
|
private function storeTestResult(int $integrationId, string $endpointUrl, array $result): void
|
||||||
|
{
|
||||||
|
$status = trim((string) ($result['status'] ?? 'error'));
|
||||||
|
$httpCode = $result['http_code'] ?? null;
|
||||||
|
$message = mb_substr(trim((string) ($result['message'] ?? '')), 0, 255);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$statement = $this->pdo->prepare(
|
||||||
|
'UPDATE integrations
|
||||||
|
SET last_test_status = :last_test_status,
|
||||||
|
last_test_http_code = :last_test_http_code,
|
||||||
|
last_test_message = :last_test_message,
|
||||||
|
last_test_at = NOW(),
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = :id AND type = :type'
|
||||||
|
);
|
||||||
|
$statement->execute([
|
||||||
|
'id' => $integrationId,
|
||||||
|
'type' => self::TYPE,
|
||||||
|
'last_test_status' => $this->nullableString($status),
|
||||||
|
'last_test_http_code' => $httpCode,
|
||||||
|
'last_test_message' => $this->nullableString($message),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$log = $this->pdo->prepare(
|
||||||
|
'INSERT INTO integration_test_logs (
|
||||||
|
integration_id, status, http_code, message, endpoint_url, tested_at
|
||||||
|
) VALUES (
|
||||||
|
:integration_id, :status, :http_code, :message, :endpoint_url, NOW()
|
||||||
|
)'
|
||||||
|
);
|
||||||
|
$log->execute([
|
||||||
|
'integration_id' => $integrationId,
|
||||||
|
'status' => $status,
|
||||||
|
'http_code' => $httpCode,
|
||||||
|
'message' => $message,
|
||||||
|
'endpoint_url' => $endpointUrl,
|
||||||
|
]);
|
||||||
|
} catch (Throwable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function nullableString(string $value): ?string
|
||||||
|
{
|
||||||
|
$trimmed = trim($value);
|
||||||
|
return $trimmed === '' ? null : $trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeDate(string $value): ?string
|
||||||
|
{
|
||||||
|
$trimmed = trim($value);
|
||||||
|
return $trimmed === '' ? null : $trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeStatusSyncDirection(string $value): string
|
||||||
|
{
|
||||||
|
$normalized = trim(mb_strtolower($value));
|
||||||
|
if ($normalized === self::DIRECTION_ORDERPRO_TO_SHOPPRO) {
|
||||||
|
return self::DIRECTION_ORDERPRO_TO_SHOPPRO;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::DIRECTION_SHOPPRO_TO_ORDERPRO;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $value
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private function decodeStatusCodesJson(mixed $value): array
|
||||||
|
{
|
||||||
|
if (!is_string($value) || trim($value) === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = json_decode($value, true);
|
||||||
|
if (!is_array($decoded)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
$seen = [];
|
||||||
|
foreach ($decoded as $rawCode) {
|
||||||
|
$code = strtolower(trim((string) $rawCode));
|
||||||
|
if ($code === '' || isset($seen[$code])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$seen[$code] = true;
|
||||||
|
$result[] = $code;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function encodeStatusCodesJson(mixed $rawCodes): ?string
|
||||||
|
{
|
||||||
|
if (!is_array($rawCodes)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$codes = [];
|
||||||
|
$seen = [];
|
||||||
|
foreach ($rawCodes as $rawCode) {
|
||||||
|
$code = strtolower(trim((string) $rawCode));
|
||||||
|
if ($code === '' || isset($seen[$code])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$seen[$code] = true;
|
||||||
|
$codes[] = $code;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($codes === []) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return json_encode($codes, JSON_UNESCAPED_UNICODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, mixed> $rawStatuses
|
||||||
|
* @return array<int, array{code:string,name:string}>
|
||||||
|
*/
|
||||||
|
private function normalizeStatusesPayload(array $rawStatuses): array
|
||||||
|
{
|
||||||
|
$result = [];
|
||||||
|
$seen = [];
|
||||||
|
|
||||||
|
foreach ($rawStatuses as $key => $item) {
|
||||||
|
if (!is_array($item)) {
|
||||||
|
$codeFromKey = trim((string) $key);
|
||||||
|
$nameFromValue = trim((string) $item);
|
||||||
|
if ($codeFromKey !== '') {
|
||||||
|
$normalizedCode = mb_strtolower($codeFromKey);
|
||||||
|
if (!isset($seen[$normalizedCode])) {
|
||||||
|
$seen[$normalizedCode] = true;
|
||||||
|
$result[] = [
|
||||||
|
'code' => $codeFromKey,
|
||||||
|
'name' => $nameFromValue !== '' ? $nameFromValue : $codeFromKey,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_array($item)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = trim((string) (
|
||||||
|
$item['code']
|
||||||
|
?? $item['status_code']
|
||||||
|
?? $item['status']
|
||||||
|
?? $item['symbol']
|
||||||
|
?? $item['slug']
|
||||||
|
?? $item['id']
|
||||||
|
?? $key
|
||||||
|
));
|
||||||
|
$name = trim((string) (
|
||||||
|
$item['name']
|
||||||
|
?? $item['status_name']
|
||||||
|
?? $item['label']
|
||||||
|
?? $item['title']
|
||||||
|
?? $item['value']
|
||||||
|
?? $code
|
||||||
|
));
|
||||||
|
if ($code === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedCode = mb_strtolower($code);
|
||||||
|
if (isset($seen[$normalizedCode])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$seen[$normalizedCode] = true;
|
||||||
|
$result[] = [
|
||||||
|
'code' => $code,
|
||||||
|
'name' => $name !== '' ? $name : $code,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
1074
src/Modules/Settings/ShopproOrdersSyncService.php
Normal file
1074
src/Modules/Settings/ShopproOrdersSyncService.php
Normal file
File diff suppressed because it is too large
Load Diff
404
src/Modules/Settings/ShopproPaymentStatusSyncService.php
Normal file
404
src/Modules/Settings/ShopproPaymentStatusSyncService.php
Normal file
@@ -0,0 +1,404 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Settings;
|
||||||
|
|
||||||
|
use App\Modules\Orders\OrdersRepository;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use PDO;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
final class ShopproPaymentStatusSyncService
|
||||||
|
{
|
||||||
|
private const PAID_STATUS = 2;
|
||||||
|
private const UNPAID_STATUS = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<int, string>
|
||||||
|
*/
|
||||||
|
private const DEFAULT_FINAL_STATUS_CODES = [
|
||||||
|
'wyslane',
|
||||||
|
'zrealizowane',
|
||||||
|
'anulowane',
|
||||||
|
'cancelled',
|
||||||
|
'canceled',
|
||||||
|
'delivered',
|
||||||
|
'returned',
|
||||||
|
'shipped',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly ShopproIntegrationsRepository $integrations,
|
||||||
|
private readonly ShopproApiClient $apiClient,
|
||||||
|
private readonly OrdersRepository $orders,
|
||||||
|
private readonly PDO $pdo
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function sync(array $options = []): array
|
||||||
|
{
|
||||||
|
$perIntegrationLimit = max(1, min(500, (int) ($options['per_integration_limit'] ?? 100)));
|
||||||
|
$result = [
|
||||||
|
'ok' => true,
|
||||||
|
'checked_integrations' => 0,
|
||||||
|
'processed_orders' => 0,
|
||||||
|
'updated_orders' => 0,
|
||||||
|
'skipped_orders' => 0,
|
||||||
|
'failed_orders' => 0,
|
||||||
|
'errors' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($this->integrations->listIntegrations() as $integration) {
|
||||||
|
$integrationId = (int) ($integration['id'] ?? 0);
|
||||||
|
if ($integrationId <= 0 || empty($integration['is_active']) || empty($integration['has_api_key'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$baseUrl = trim((string) ($integration['base_url'] ?? ''));
|
||||||
|
$apiKey = $this->integrations->getApiKeyDecrypted($integrationId);
|
||||||
|
$timeout = max(1, min(120, (int) ($integration['timeout_seconds'] ?? 10)));
|
||||||
|
if ($baseUrl === '' || $apiKey === null || trim($apiKey) === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result['checked_integrations'] = (int) $result['checked_integrations'] + 1;
|
||||||
|
$watchedStatuses = $this->resolveWatchedStatusCodes($integration);
|
||||||
|
$orders = $this->findCandidateOrders($integrationId, $watchedStatuses, $perIntegrationLimit);
|
||||||
|
|
||||||
|
foreach ($orders as $order) {
|
||||||
|
$result['processed_orders'] = (int) $result['processed_orders'] + 1;
|
||||||
|
$sourceOrderId = trim((string) ($order['source_order_id'] ?? ''));
|
||||||
|
if ($sourceOrderId === '') {
|
||||||
|
$result['skipped_orders'] = (int) $result['skipped_orders'] + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$updated = $this->syncSingleOrderPayment(
|
||||||
|
$integrationId,
|
||||||
|
$baseUrl,
|
||||||
|
$apiKey,
|
||||||
|
$timeout,
|
||||||
|
$order
|
||||||
|
);
|
||||||
|
if ($updated) {
|
||||||
|
$result['updated_orders'] = (int) $result['updated_orders'] + 1;
|
||||||
|
} else {
|
||||||
|
$result['skipped_orders'] = (int) $result['skipped_orders'] + 1;
|
||||||
|
}
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
$result['failed_orders'] = (int) $result['failed_orders'] + 1;
|
||||||
|
$errors = is_array($result['errors']) ? $result['errors'] : [];
|
||||||
|
if (count($errors) < 20) {
|
||||||
|
$errors[] = [
|
||||||
|
'integration_id' => $integrationId,
|
||||||
|
'order_id' => (int) ($order['id'] ?? 0),
|
||||||
|
'source_order_id' => $sourceOrderId,
|
||||||
|
'error' => $exception->getMessage(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$result['errors'] = $errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $integration
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private function resolveWatchedStatusCodes(array $integration): array
|
||||||
|
{
|
||||||
|
$rawCodes = $integration['payment_sync_status_codes'] ?? [];
|
||||||
|
if (!is_array($rawCodes)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
$seen = [];
|
||||||
|
foreach ($rawCodes as $rawCode) {
|
||||||
|
$code = strtolower(trim((string) $rawCode));
|
||||||
|
if ($code === '' || isset($seen[$code])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$seen[$code] = true;
|
||||||
|
$result[] = $code;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, string> $watchedStatuses
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
private function findCandidateOrders(int $integrationId, array $watchedStatuses, int $limit): array
|
||||||
|
{
|
||||||
|
$where = [
|
||||||
|
'source = :source',
|
||||||
|
'integration_id = :integration_id',
|
||||||
|
'source_order_id IS NOT NULL',
|
||||||
|
'source_order_id <> ""',
|
||||||
|
'(payment_status IS NULL OR payment_status <> :paid_status)',
|
||||||
|
];
|
||||||
|
$params = [
|
||||||
|
'source' => 'shoppro',
|
||||||
|
'integration_id' => $integrationId,
|
||||||
|
'paid_status' => self::PAID_STATUS,
|
||||||
|
];
|
||||||
|
|
||||||
|
$statusPlaceholders = [];
|
||||||
|
$statusCodes = $watchedStatuses !== [] ? $watchedStatuses : self::DEFAULT_FINAL_STATUS_CODES;
|
||||||
|
foreach ($statusCodes as $index => $statusCode) {
|
||||||
|
$placeholder = ':status_' . $index;
|
||||||
|
$statusPlaceholders[] = $placeholder;
|
||||||
|
$params['status_' . $index] = strtolower($statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($watchedStatuses !== []) {
|
||||||
|
$where[] = 'LOWER(COALESCE(external_status_id, "")) IN (' . implode(', ', $statusPlaceholders) . ')';
|
||||||
|
} else {
|
||||||
|
$where[] = 'LOWER(COALESCE(external_status_id, "")) NOT IN (' . implode(', ', $statusPlaceholders) . ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = 'SELECT id, source_order_id, payment_status, total_paid, total_with_tax, currency, external_payment_type_id
|
||||||
|
FROM orders
|
||||||
|
WHERE ' . implode(' AND ', $where) . '
|
||||||
|
ORDER BY source_updated_at DESC, id DESC
|
||||||
|
LIMIT :limit';
|
||||||
|
$stmt = $this->pdo->prepare($sql);
|
||||||
|
foreach ($params as $key => $value) {
|
||||||
|
if (is_int($value)) {
|
||||||
|
$stmt->bindValue(':' . $key, $value, PDO::PARAM_INT);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$stmt->bindValue(':' . $key, $value);
|
||||||
|
}
|
||||||
|
$stmt->bindValue(':limit', max(1, min(1000, $limit)), PDO::PARAM_INT);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
return is_array($rows) ? $rows : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $order
|
||||||
|
*/
|
||||||
|
private function syncSingleOrderPayment(
|
||||||
|
int $integrationId,
|
||||||
|
string $baseUrl,
|
||||||
|
string $apiKey,
|
||||||
|
int $timeout,
|
||||||
|
array $order
|
||||||
|
): bool {
|
||||||
|
$sourceOrderId = trim((string) ($order['source_order_id'] ?? ''));
|
||||||
|
if ($sourceOrderId === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$details = $this->apiClient->fetchOrderById($baseUrl, $apiKey, $timeout, $sourceOrderId);
|
||||||
|
if (($details['ok'] ?? false) !== true || !is_array($details['order'] ?? null)) {
|
||||||
|
throw new \RuntimeException((string) ($details['message'] ?? 'Blad pobierania szczegolow zamowienia.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = (array) $details['order'];
|
||||||
|
$isPaid = $this->resolvePaidFlag($payload);
|
||||||
|
if ($isPaid === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$newPaymentStatus = $isPaid ? self::PAID_STATUS : self::UNPAID_STATUS;
|
||||||
|
$existingTotalWithTax = $order['total_with_tax'] !== null ? (float) $order['total_with_tax'] : null;
|
||||||
|
$newTotalPaid = $isPaid
|
||||||
|
? $this->resolvePaidAmount($payload, $existingTotalWithTax)
|
||||||
|
: 0.0;
|
||||||
|
$existingPaymentStatus = isset($order['payment_status']) ? (int) $order['payment_status'] : null;
|
||||||
|
$existingTotalPaid = $order['total_paid'] !== null ? (float) $order['total_paid'] : null;
|
||||||
|
$paymentMethod = $this->nullableString((string) ($payload['payment_method'] ?? $order['external_payment_type_id'] ?? ''));
|
||||||
|
$paymentDate = $this->normalizeDateTime((string) ($payload['payment_date'] ?? ''));
|
||||||
|
$sourceUpdatedAt = $this->normalizeDateTime((string) ($payload['updated_at'] ?? $payload['date_updated'] ?? ''));
|
||||||
|
|
||||||
|
if (
|
||||||
|
$existingPaymentStatus === $newPaymentStatus
|
||||||
|
&& $this->floatsEqual($existingTotalPaid, $newTotalPaid)
|
||||||
|
&& $paymentMethod === $this->nullableString((string) ($order['external_payment_type_id'] ?? ''))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$orderId = (int) ($order['id'] ?? 0);
|
||||||
|
if ($orderId <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->pdo->beginTransaction();
|
||||||
|
try {
|
||||||
|
$this->updateOrderPaymentColumns($orderId, $newPaymentStatus, $newTotalPaid, $paymentMethod, $sourceUpdatedAt);
|
||||||
|
$this->replaceOrderPaymentRow($orderId, $paymentMethod, $paymentDate, $newTotalPaid, (string) ($order['currency'] ?? 'PLN'), $isPaid);
|
||||||
|
$this->pdo->commit();
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
if ($this->pdo->inTransaction()) {
|
||||||
|
$this->pdo->rollBack();
|
||||||
|
}
|
||||||
|
throw $exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
$summary = $isPaid
|
||||||
|
? 'shopPRO: zamowienie oznaczone jako oplacone'
|
||||||
|
: 'shopPRO: zamowienie oznaczone jako nieoplacone';
|
||||||
|
$this->orders->recordActivity(
|
||||||
|
$orderId,
|
||||||
|
'payment',
|
||||||
|
$summary,
|
||||||
|
[
|
||||||
|
'integration_id' => $integrationId,
|
||||||
|
'source_order_id' => $sourceOrderId,
|
||||||
|
'old_payment_status' => $existingPaymentStatus,
|
||||||
|
'new_payment_status' => $newPaymentStatus,
|
||||||
|
'old_total_paid' => $existingTotalPaid,
|
||||||
|
'new_total_paid' => $newTotalPaid,
|
||||||
|
],
|
||||||
|
'sync',
|
||||||
|
'shopPRO'
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function updateOrderPaymentColumns(
|
||||||
|
int $orderId,
|
||||||
|
int $paymentStatus,
|
||||||
|
?float $totalPaid,
|
||||||
|
?string $paymentMethod,
|
||||||
|
?string $sourceUpdatedAt
|
||||||
|
): void {
|
||||||
|
$sql = 'UPDATE orders
|
||||||
|
SET payment_status = :payment_status,
|
||||||
|
total_paid = :total_paid,
|
||||||
|
external_payment_type_id = :external_payment_type_id,
|
||||||
|
fetched_at = NOW(),
|
||||||
|
updated_at = NOW()';
|
||||||
|
$params = [
|
||||||
|
'id' => $orderId,
|
||||||
|
'payment_status' => $paymentStatus,
|
||||||
|
'total_paid' => $totalPaid,
|
||||||
|
'external_payment_type_id' => $paymentMethod,
|
||||||
|
];
|
||||||
|
if ($sourceUpdatedAt !== null) {
|
||||||
|
$sql .= ', source_updated_at = :source_updated_at';
|
||||||
|
$params['source_updated_at'] = $sourceUpdatedAt;
|
||||||
|
}
|
||||||
|
$sql .= ' WHERE id = :id';
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function replaceOrderPaymentRow(
|
||||||
|
int $orderId,
|
||||||
|
?string $paymentMethod,
|
||||||
|
?string $paymentDate,
|
||||||
|
?float $amount,
|
||||||
|
string $currency,
|
||||||
|
bool $isPaid
|
||||||
|
): void {
|
||||||
|
$deleteStmt = $this->pdo->prepare('DELETE FROM order_payments WHERE order_id = :order_id');
|
||||||
|
$deleteStmt->execute(['order_id' => $orderId]);
|
||||||
|
|
||||||
|
$insertStmt = $this->pdo->prepare(
|
||||||
|
'INSERT INTO order_payments (
|
||||||
|
order_id, source_payment_id, external_payment_id, payment_type_id, payment_date, amount, currency, comment, payload_json
|
||||||
|
) VALUES (
|
||||||
|
:order_id, NULL, NULL, :payment_type_id, :payment_date, :amount, :currency, :comment, NULL
|
||||||
|
)'
|
||||||
|
);
|
||||||
|
$insertStmt->execute([
|
||||||
|
'order_id' => $orderId,
|
||||||
|
'payment_type_id' => $paymentMethod ?? 'unknown',
|
||||||
|
'payment_date' => $paymentDate,
|
||||||
|
'amount' => $amount,
|
||||||
|
'currency' => trim($currency) !== '' ? strtoupper($currency) : 'PLN',
|
||||||
|
'comment' => $isPaid ? 'paid' : 'unpaid',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolvePaidFlag(array $payload): ?bool
|
||||||
|
{
|
||||||
|
$raw = $payload['paid'] ?? $payload['is_paid'] ?? null;
|
||||||
|
if ($raw === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (is_bool($raw)) {
|
||||||
|
return $raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = strtolower(trim((string) $raw));
|
||||||
|
if (in_array($value, ['1', 'true', 'yes', 'paid'], true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (in_array($value, ['0', 'false', 'no', 'unpaid'], true)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolvePaidAmount(array $payload, ?float $fallbackGross): ?float
|
||||||
|
{
|
||||||
|
$value = $payload['total_paid'] ?? null;
|
||||||
|
if ($value !== null && is_numeric((string) $value)) {
|
||||||
|
return (float) $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$grossCandidates = [
|
||||||
|
$payload['total_gross'] ?? null,
|
||||||
|
$payload['total_with_tax'] ?? null,
|
||||||
|
$payload['summary']['total'] ?? null,
|
||||||
|
$payload['summary'] ?? null,
|
||||||
|
];
|
||||||
|
foreach ($grossCandidates as $candidate) {
|
||||||
|
if ($candidate !== null && is_numeric((string) $candidate)) {
|
||||||
|
return (float) $candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $fallbackGross;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeDateTime(string $value): ?string
|
||||||
|
{
|
||||||
|
$trimmed = trim($value);
|
||||||
|
if ($trimmed === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return (new DateTimeImmutable($trimmed))->format('Y-m-d H:i:s');
|
||||||
|
} catch (Throwable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function nullableString(string $value): ?string
|
||||||
|
{
|
||||||
|
$trimmed = trim($value);
|
||||||
|
return $trimmed === '' ? null : $trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function floatsEqual(?float $left, ?float $right): bool
|
||||||
|
{
|
||||||
|
if ($left === null && $right === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if ($left === null || $right === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return abs($left - $right) < 0.00001;
|
||||||
|
}
|
||||||
|
}
|
||||||
99
src/Modules/Settings/ShopproStatusMappingRepository.php
Normal file
99
src/Modules/Settings/ShopproStatusMappingRepository.php
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Settings;
|
||||||
|
|
||||||
|
use PDO;
|
||||||
|
|
||||||
|
final class ShopproStatusMappingRepository
|
||||||
|
{
|
||||||
|
public function __construct(private readonly PDO $pdo)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array{shoppro_status_code:string,shoppro_status_name:string,orderpro_status_code:string}>
|
||||||
|
*/
|
||||||
|
public function listByIntegration(int $integrationId): array
|
||||||
|
{
|
||||||
|
if ($integrationId <= 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$statement = $this->pdo->prepare(
|
||||||
|
'SELECT shoppro_status_code, shoppro_status_name, orderpro_status_code
|
||||||
|
FROM order_status_mappings
|
||||||
|
WHERE integration_id = :integration_id
|
||||||
|
ORDER BY shoppro_status_code ASC'
|
||||||
|
);
|
||||||
|
$statement->execute(['integration_id' => $integrationId]);
|
||||||
|
$rows = $statement->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!is_array($rows)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
if (!is_array($row)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$shopproCode = trim((string) ($row['shoppro_status_code'] ?? ''));
|
||||||
|
if ($shopproCode === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result[] = [
|
||||||
|
'shoppro_status_code' => $shopproCode,
|
||||||
|
'shoppro_status_name' => trim((string) ($row['shoppro_status_name'] ?? '')),
|
||||||
|
'orderpro_status_code' => trim((string) ($row['orderpro_status_code'] ?? '')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, array{shoppro_status_code:string,shoppro_status_name:string,orderpro_status_code:string}> $mappings
|
||||||
|
*/
|
||||||
|
public function replaceForIntegration(int $integrationId, array $mappings): void
|
||||||
|
{
|
||||||
|
if ($integrationId <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$deleteStatement = $this->pdo->prepare(
|
||||||
|
'DELETE FROM order_status_mappings WHERE integration_id = :integration_id'
|
||||||
|
);
|
||||||
|
$deleteStatement->execute(['integration_id' => $integrationId]);
|
||||||
|
|
||||||
|
if ($mappings === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$insertStatement = $this->pdo->prepare(
|
||||||
|
'INSERT INTO order_status_mappings (
|
||||||
|
integration_id, shoppro_status_code, shoppro_status_name, orderpro_status_code, created_at, updated_at
|
||||||
|
) VALUES (
|
||||||
|
:integration_id, :shoppro_status_code, :shoppro_status_name, :orderpro_status_code, NOW(), NOW()
|
||||||
|
)'
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($mappings as $mapping) {
|
||||||
|
$shopproCode = trim((string) ($mapping['shoppro_status_code'] ?? ''));
|
||||||
|
$orderproCode = trim((string) ($mapping['orderpro_status_code'] ?? ''));
|
||||||
|
if ($shopproCode === '' || $orderproCode === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$shopproName = trim((string) ($mapping['shoppro_status_name'] ?? ''));
|
||||||
|
$insertStatement->execute([
|
||||||
|
'integration_id' => $integrationId,
|
||||||
|
'shoppro_status_code' => $shopproCode,
|
||||||
|
'shoppro_status_name' => $shopproName !== '' ? $shopproName : null,
|
||||||
|
'orderpro_status_code' => $orderproCode,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
63
src/Modules/Settings/ShopproStatusSyncService.php
Normal file
63
src/Modules/Settings/ShopproStatusSyncService.php
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Settings;
|
||||||
|
|
||||||
|
final class ShopproStatusSyncService
|
||||||
|
{
|
||||||
|
private const DIRECTION_SHOPPRO_TO_ORDERPRO = 'shoppro_to_orderpro';
|
||||||
|
private const DIRECTION_ORDERPRO_TO_SHOPPRO = 'orderpro_to_shoppro';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly ShopproIntegrationsRepository $integrations,
|
||||||
|
private readonly ShopproOrdersSyncService $ordersSyncService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function sync(): array
|
||||||
|
{
|
||||||
|
$supportedIntegrationIds = [];
|
||||||
|
$unsupportedCount = 0;
|
||||||
|
|
||||||
|
foreach ($this->integrations->listIntegrations() as $integration) {
|
||||||
|
$integrationId = (int) ($integration['id'] ?? 0);
|
||||||
|
if ($integrationId <= 0 || empty($integration['is_active']) || empty($integration['has_api_key'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$direction = trim((string) ($integration['order_status_sync_direction'] ?? self::DIRECTION_SHOPPRO_TO_ORDERPRO));
|
||||||
|
if ($direction === self::DIRECTION_ORDERPRO_TO_SHOPPRO) {
|
||||||
|
$unsupportedCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$supportedIntegrationIds[] = $integrationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($supportedIntegrationIds === []) {
|
||||||
|
return [
|
||||||
|
'ok' => true,
|
||||||
|
'processed' => 0,
|
||||||
|
'checked_integrations' => 0,
|
||||||
|
'unsupported_integrations' => $unsupportedCount,
|
||||||
|
'message' => 'Brak aktywnych integracji shopPRO z kierunkiem shopPRO -> orderPRO.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->ordersSyncService->sync([
|
||||||
|
'max_pages' => 3,
|
||||||
|
'page_limit' => 50,
|
||||||
|
'max_orders' => 200,
|
||||||
|
'ignore_orders_fetch_enabled' => true,
|
||||||
|
'allowed_integration_ids' => $supportedIntegrationIds,
|
||||||
|
]);
|
||||||
|
$result['ok'] = true;
|
||||||
|
$result['direction'] = self::DIRECTION_SHOPPRO_TO_ORDERPRO;
|
||||||
|
$result['unsupported_integrations'] = $unsupportedCount;
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user