From d3f4bdaecd955e8e3b7804740be53a2d91c6cd2d Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Sat, 28 Mar 2026 00:09:57 +0100 Subject: [PATCH] update --- .claude/memory/MEMORY.md | 4 + .claude/memory/project_psd_personalize.md | 20 + .claude/settings.local.json | 21 +- .../2026-03-02_users-only-reset/bin/cron.php | 115 -- .../resources/views/dashboard/index.php | 7 - .../resources/views/marketplace/index.php | 47 - .../resources/views/marketplace/offers.php | 406 ---- .../resources/views/orders/index.php | 28 - .../resources/views/products/create.php | 139 -- .../resources/views/products/edit.php | 626 ------ .../resources/views/products/index.php | 293 --- .../resources/views/products/links.php | 268 --- .../resources/views/products/show.php | 197 -- .../resources/views/settings/cron.php | 156 -- .../resources/views/settings/database.php | 100 - .../resources/views/settings/gs1.php | 58 - .../resources/views/settings/integrations.php | 215 -- .../views/settings/order-statuses.php | 107 - .../resources/views/settings/products.php | 40 - .../src/Modules/Cron/CronJobProcessor.php | 200 -- .../src/Modules/Cron/CronJobRepository.php | 517 ----- .../src/Modules/Cron/CronJobType.php | 38 - .../Cron/ProductLinksHealthCheckHandler.php | 140 -- .../Cron/ShopProOfferTitlesRefreshHandler.php | 106 - .../Cron/ShopProOrderStatusSyncHandler.php | 23 - .../Cron/ShopProOrdersImportHandler.php | 23 - .../src/Modules/GS1/GS1Service.php | 69 - .../src/Modules/GS1/MojeGS1Client.php | 211 -- .../Marketplace/MarketplaceController.php | 629 ------ .../Marketplace/MarketplaceRepository.php | 279 --- .../src/Modules/Orders/OrderImportService.php | 629 ------ .../Modules/Orders/OrderStatusSyncService.php | 496 ----- .../src/Modules/Orders/OrdersController.php | 963 --------- .../src/Modules/Orders/OrdersRepository.php | 656 ------- .../ProductLinks/ChannelOffersRepository.php | 270 --- .../ProductLinks/LinkMatcherService.php | 63 - .../ProductLinks/OfferImportService.php | 230 --- .../ProductLinks/ProductLinksController.php | 156 -- .../ProductLinks/ProductLinksRepository.php | 515 ----- .../ProductLinks/ProductLinksService.php | 453 ----- .../Modules/Products/ProductRepository.php | 751 ------- .../src/Modules/Products/ProductService.php | 479 ----- .../Modules/Products/ProductSkuGenerator.php | 103 - .../src/Modules/Products/ProductValidator.php | 102 - .../Modules/Products/ProductsController.php | 1164 ----------- .../Modules/Products/ShopProExportService.php | 1201 ------------ .../Settings/AppSettingsRepository.php | 70 - .../Settings/IntegrationRepository.php | 679 ------- .../Settings/OrderStatusMappingRepository.php | 125 -- .../Modules/Settings/SettingsController.php | 1725 ----------------- .../src/Modules/Settings/ShopProClient.php | 1138 ----------- .../tests/Unit/Cron/CronJobTypeTest.php | 22 - .../OrderStatusMappingRepositoryTest.php | 74 - .../tests/Unit/Settings/ShopProClientTest.php | 32 - 54 files changed, 44 insertions(+), 17134 deletions(-) create mode 100644 .claude/memory/project_psd_personalize.md delete mode 100644 archive/2026-03-02_users-only-reset/bin/cron.php delete mode 100644 archive/2026-03-02_users-only-reset/resources/views/dashboard/index.php delete mode 100644 archive/2026-03-02_users-only-reset/resources/views/marketplace/index.php delete mode 100644 archive/2026-03-02_users-only-reset/resources/views/marketplace/offers.php delete mode 100644 archive/2026-03-02_users-only-reset/resources/views/orders/index.php delete mode 100644 archive/2026-03-02_users-only-reset/resources/views/products/create.php delete mode 100644 archive/2026-03-02_users-only-reset/resources/views/products/edit.php delete mode 100644 archive/2026-03-02_users-only-reset/resources/views/products/index.php delete mode 100644 archive/2026-03-02_users-only-reset/resources/views/products/links.php delete mode 100644 archive/2026-03-02_users-only-reset/resources/views/products/show.php delete mode 100644 archive/2026-03-02_users-only-reset/resources/views/settings/cron.php delete mode 100644 archive/2026-03-02_users-only-reset/resources/views/settings/database.php delete mode 100644 archive/2026-03-02_users-only-reset/resources/views/settings/gs1.php delete mode 100644 archive/2026-03-02_users-only-reset/resources/views/settings/integrations.php delete mode 100644 archive/2026-03-02_users-only-reset/resources/views/settings/order-statuses.php delete mode 100644 archive/2026-03-02_users-only-reset/resources/views/settings/products.php delete mode 100644 archive/2026-03-02_users-only-reset/src/Modules/Cron/CronJobProcessor.php delete mode 100644 archive/2026-03-02_users-only-reset/src/Modules/Cron/CronJobRepository.php delete mode 100644 archive/2026-03-02_users-only-reset/src/Modules/Cron/CronJobType.php delete mode 100644 archive/2026-03-02_users-only-reset/src/Modules/Cron/ProductLinksHealthCheckHandler.php delete mode 100644 archive/2026-03-02_users-only-reset/src/Modules/Cron/ShopProOfferTitlesRefreshHandler.php delete mode 100644 archive/2026-03-02_users-only-reset/src/Modules/Cron/ShopProOrderStatusSyncHandler.php delete mode 100644 archive/2026-03-02_users-only-reset/src/Modules/Cron/ShopProOrdersImportHandler.php delete mode 100644 archive/2026-03-02_users-only-reset/src/Modules/GS1/GS1Service.php delete mode 100644 archive/2026-03-02_users-only-reset/src/Modules/GS1/MojeGS1Client.php delete mode 100644 archive/2026-03-02_users-only-reset/src/Modules/Marketplace/MarketplaceController.php delete mode 100644 archive/2026-03-02_users-only-reset/src/Modules/Marketplace/MarketplaceRepository.php delete mode 100644 archive/2026-03-02_users-only-reset/src/Modules/Orders/OrderImportService.php delete mode 100644 archive/2026-03-02_users-only-reset/src/Modules/Orders/OrderStatusSyncService.php delete mode 100644 archive/2026-03-02_users-only-reset/src/Modules/Orders/OrdersController.php delete mode 100644 archive/2026-03-02_users-only-reset/src/Modules/Orders/OrdersRepository.php delete mode 100644 archive/2026-03-02_users-only-reset/src/Modules/ProductLinks/ChannelOffersRepository.php delete mode 100644 archive/2026-03-02_users-only-reset/src/Modules/ProductLinks/LinkMatcherService.php delete mode 100644 archive/2026-03-02_users-only-reset/src/Modules/ProductLinks/OfferImportService.php delete mode 100644 archive/2026-03-02_users-only-reset/src/Modules/ProductLinks/ProductLinksController.php delete mode 100644 archive/2026-03-02_users-only-reset/src/Modules/ProductLinks/ProductLinksRepository.php delete mode 100644 archive/2026-03-02_users-only-reset/src/Modules/ProductLinks/ProductLinksService.php delete mode 100644 archive/2026-03-02_users-only-reset/src/Modules/Products/ProductRepository.php delete mode 100644 archive/2026-03-02_users-only-reset/src/Modules/Products/ProductService.php delete mode 100644 archive/2026-03-02_users-only-reset/src/Modules/Products/ProductSkuGenerator.php delete mode 100644 archive/2026-03-02_users-only-reset/src/Modules/Products/ProductValidator.php delete mode 100644 archive/2026-03-02_users-only-reset/src/Modules/Products/ProductsController.php delete mode 100644 archive/2026-03-02_users-only-reset/src/Modules/Products/ShopProExportService.php delete mode 100644 archive/2026-03-02_users-only-reset/src/Modules/Settings/AppSettingsRepository.php delete mode 100644 archive/2026-03-02_users-only-reset/src/Modules/Settings/IntegrationRepository.php delete mode 100644 archive/2026-03-02_users-only-reset/src/Modules/Settings/OrderStatusMappingRepository.php delete mode 100644 archive/2026-03-02_users-only-reset/src/Modules/Settings/SettingsController.php delete mode 100644 archive/2026-03-02_users-only-reset/src/Modules/Settings/ShopProClient.php delete mode 100644 archive/2026-03-02_users-only-reset/tests/Unit/Cron/CronJobTypeTest.php delete mode 100644 archive/2026-03-02_users-only-reset/tests/Unit/Settings/OrderStatusMappingRepositoryTest.php delete mode 100644 archive/2026-03-02_users-only-reset/tests/Unit/Settings/ShopProClientTest.php diff --git a/.claude/memory/MEMORY.md b/.claude/memory/MEMORY.md index 946a6c8..94e2b16 100644 --- a/.claude/memory/MEMORY.md +++ b/.claude/memory/MEMORY.md @@ -6,6 +6,10 @@ - [feedback_polish_language.md](feedback_polish_language.md) — Zawsze pisz po polsku w tym projekcie - [feedback_screenshots_dir.md](feedback_screenshots_dir.md) — Screenshoty zapisywać w .claude/screenshots/, nie w katalogu głównym +## Project + +- [project_psd_personalize.md](project_psd_personalize.md) — Automatyzacja personalizacji PSD: skrypt + Claude review + skill + ## Reference - [reference_server_deploy.md](reference_server_deploy.md) — FTP deploy na hostido, vendor/ bez dev deps, usuwanie ręczne diff --git a/.claude/memory/project_psd_personalize.md b/.claude/memory/project_psd_personalize.md new file mode 100644 index 0000000..75eadd0 --- /dev/null +++ b/.claude/memory/project_psd_personalize.md @@ -0,0 +1,20 @@ +--- +name: PSD Personalize - automatyzacja personalizowanych projektow +description: Workflow automatyzacji personalizacji produktow drukowanych z szablonow PSD — skrypt Python + Claude review +type: project +--- + +Stworzony workflow automatyzacji personalizacji produktow drukowanych: +- Skrypt `tools/psd_personalize.py` czyta PSD (psd-tools), podmienia tekst w warstwach VAR_*, renderuje podglad PNG +- Claude uczestniczy w procesie: lamie tekst typograficznie (sierotki!), ocenia podglad, sugeruje korekty +- Dane zamowienia przekazywane przez plik JSON (--order) — unika problemow z kodowaniem UTF-8 w bashu +- Rendering: pelny composite PSD jako tlo + nowy tekst rysowany Pillow na wierzchu +- Stary tekst zakrywany bialymi prostokatami (2-fazowe: najpierw wszystkie biale, potem caly tekst) +- Grupa "makieta" (linie ciecia) automatycznie ukrywana +- Tolerancja 3% na wrap_text (roznica kerningu Pillow vs Photoshop) + +**Why:** Reczna personalizacja w Photoshopie jest czasochlonna i podatna na bledy typograficzne. +**How to apply:** Przy nowych produktach do personalizacji — ten sam wzorzec: warstwy VAR_*, skrypt, JSON, iteracyjny review. + +Pierwszy produkt: Pudelko na pieniadze - Komunia Swieta (Dziewczynka) +Skill: `/pudelko-komunia-dziewczynka` diff --git a/.claude/settings.local.json b/.claude/settings.local.json index c069d82..9e0c52e 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -26,7 +26,26 @@ "Bash(PROJECT_DIR=\"C:/visual studio code/projekty/orderPRO\" node .claude/hooks/index-docs.mjs)", "mcp__plugin_context-mode_context-mode__ctx_index", "mcp__plugin_context-mode_context-mode__ctx_search", - "mcp__plugin_context-mode_context-mode__ctx_batch_execute" + "mcp__plugin_context-mode_context-mode__ctx_batch_execute", + "Bash(powershell.exe -Command \"\\(Get-Item 'C:\\\\Users\\\\jacek\\\\.claude\\\\projects\\\\C--visual-studio-code-projekty-orderPRO\\\\memory'\\).Attributes\")", + "Bash(python tools/psd_personalize.py generate --template \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/pudełka na chrzest - makieta A3 CMYK.psd\" --order \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/_order_lenki.json\" --output \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/_preview_Lenki.png\")", + "Bash(python tools/psd_personalize.py generate --template \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/pudełka na chrzest - makieta A3 CMYK.psd\" --order \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/_order_lenki.json\" --output \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/_preview_Lenki_v2.png\")", + "Bash(python tools/psd_personalize.py generate --template \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/pudełka na chrzest - makieta A3 CMYK.psd\" --order \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/_order_lenki.json\" --output \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/_preview_Lenki_v3.png\")", + "Bash(python tools/psd_personalize.py generate --template \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/pudełka na chrzest - makieta A3 CMYK.psd\" --order \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/_order_lenki.json\" --output \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/_preview_Lenki_v4.png\")", + "Bash(python tools/psd_personalize.py generate --template \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/pudełka na chrzest - makieta A3 CMYK.psd\" --order \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/_order_lenki.json\" --output \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/_preview_Lenki_v5.png\")", + "Bash(python tools/psd_personalize.py generate --template \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/pudełka na chrzest - makieta A3 CMYK.psd\" --order \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/_order_lenki.json\" --output \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/_preview_Lenki_v6.png\")", + "Bash(python tools/psd_personalize.py generate --template \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/pudełka na chrzest - makieta A3 CMYK.psd\" --order \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/_order_lenki.json\" --output \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/_preview_Lenki_v7.png\")", + "Bash(python tools/psd_personalize.py generate --template \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/pudełka na chrzest - makieta A3 CMYK.psd\" --order \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/_order_blanki.json\" --output \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/_preview_Blanki_auto.png\")", + "Bash(python tools/psd_personalize.py generate --template \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/pudełka na chrzest - makieta A3 CMYK.psd\" --order \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/_order_lenki.json\" --output \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/_preview_Lenki_v8.png\")", + "Bash(python tools/psd_personalize.py generate --template \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/pudełka na chrzest - makieta A3 CMYK.psd\" --order \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/_order_lenki.json\" --output \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/_preview_Lenki_v9.png\")", + "Bash(python tools/psd_personalize.py generate --template \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/pudełka na chrzest - makieta A3 CMYK.psd\" --order \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/_order_lenki.json\" --output \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/_preview_Lenki_v10.png\")", + "Bash(python tools/psd_personalize.py generate --template \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/pudełka na chrzest - makieta A3 CMYK.psd\" --order \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/_order_blanki.json\" --output \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/_preview_Blanki_final.png\")", + "Bash(python tools/psd_personalize.py generate --template \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/pudełka na chrzest - makieta A3 CMYK.psd\" --order \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/_order_blani.json\" --output \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/_preview_Blani.png\")", + "Bash(python tools/psd_personalize.py generate --template \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/pudełka na chrzest - makieta A3 CMYK.psd\" --order \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/_order_blani.json\" --output \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/_gotowe/Blani.tif\")", + "Bash(pip install:*)", + "Bash(python tools/psd_personalize.py export --template \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/pudełka na chrzest - makieta A3 CMYK.psd\" --order \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/_order_blani.json\" --output \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/_gotowe/Blani.psd\")", + "Bash(python tools/psd_personalize.py export-ps --template \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/pudełka na chrzest - makieta A3 CMYK.psd\" --order \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/_order_blani.json\" --output \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniądze/Pudełko na pieniądze z życzeniami na Komunie Świętą z nadrukiem UV - Dziewczynka/_gotowe/Blani_editable.psd\")", + "Bash(python:*)" ] }, "hooks": { diff --git a/archive/2026-03-02_users-only-reset/bin/cron.php b/archive/2026-03-02_users-only-reset/bin/cron.php deleted file mode 100644 index f195b8a..0000000 --- a/archive/2026-03-02_users-only-reset/bin/cron.php +++ /dev/null @@ -1,115 +0,0 @@ - $dbConfig */ -$dbConfig = require $basePath . '/config/database.php'; -/** @var array $appConfig */ -$appConfig = require $basePath . '/config/app.php'; - -$limit = 20; -foreach ($argv as $argument) { - if (!str_starts_with((string) $argument, '--limit=')) { - continue; - } - - $limitValue = (int) substr((string) $argument, strlen('--limit=')); - if ($limitValue > 0) { - $limit = min(200, $limitValue); - } -} - -try { - $pdo = ConnectionFactory::make($dbConfig); - - $cronJobs = new CronJobRepository($pdo); - $processor = new CronJobProcessor($cronJobs); - - $integrationRepository = new IntegrationRepository( - $pdo, - (string) (($appConfig['integrations']['secret'] ?? '') ?: '') - ); - $offersRepository = new ChannelOffersRepository($pdo); - $linksRepository = new ProductLinksRepository($pdo); - $shopProClient = new ShopProClient(); - $offerImportService = new OfferImportService($shopProClient, $offersRepository, $pdo); - $linksHealthCheckHandler = new ProductLinksHealthCheckHandler( - $integrationRepository, - $offerImportService, - $linksRepository, - $offersRepository - ); - $offerTitlesRefreshHandler = new ShopProOfferTitlesRefreshHandler( - $integrationRepository, - $offerImportService - ); - $ordersRepository = new OrdersRepository($pdo); - $orderImportService = new OrderImportService( - $integrationRepository, - $ordersRepository, - $shopProClient, - $pdo - ); - $orderStatusMappings = new OrderStatusMappingRepository($pdo); - $orderStatusSyncService = new OrderStatusSyncService( - $integrationRepository, - $ordersRepository, - $orderStatusMappings, - $shopProClient, - $pdo - ); - $ordersImportHandler = new ShopProOrdersImportHandler($orderImportService); - $orderStatusSyncHandler = new ShopProOrderStatusSyncHandler($orderStatusSyncService); - - $processor->registerHandler(CronJobType::PRODUCT_LINKS_HEALTH_CHECK, $linksHealthCheckHandler); - $processor->registerHandler(CronJobType::SHOPPRO_ORDERS_IMPORT, $ordersImportHandler); - $processor->registerHandler(CronJobType::SHOPPRO_ORDER_STATUS_SYNC, $orderStatusSyncHandler); - $processor->registerHandler(CronJobType::SHOPPRO_OFFER_TITLES_REFRESH, $offerTitlesRefreshHandler); - - $result = $processor->run($limit); - - echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL; -} catch (\Throwable $exception) { - fwrite(STDERR, '[error] ' . $exception->getMessage() . PHP_EOL); - exit(1); -} diff --git a/archive/2026-03-02_users-only-reset/resources/views/dashboard/index.php b/archive/2026-03-02_users-only-reset/resources/views/dashboard/index.php deleted file mode 100644 index 30f3914..0000000 --- a/archive/2026-03-02_users-only-reset/resources/views/dashboard/index.php +++ /dev/null @@ -1,7 +0,0 @@ -
-

-

- -

- -
diff --git a/archive/2026-03-02_users-only-reset/resources/views/marketplace/index.php b/archive/2026-03-02_users-only-reset/resources/views/marketplace/index.php deleted file mode 100644 index e2319a9..0000000 --- a/archive/2026-03-02_users-only-reset/resources/views/marketplace/index.php +++ /dev/null @@ -1,47 +0,0 @@ - - -
-

-

-
- -
-

- - - - - - -

- -
- - - - - - - - - - - - - - - - - - - - -
ID
- - - -
-
- -
- diff --git a/archive/2026-03-02_users-only-reset/resources/views/marketplace/offers.php b/archive/2026-03-02_users-only-reset/resources/views/marketplace/offers.php deleted file mode 100644 index 845500b..0000000 --- a/archive/2026-03-02_users-only-reset/resources/views/marketplace/offers.php +++ /dev/null @@ -1,406 +0,0 @@ - - - - - - - $value) { - if ($value === '' || $value === null) { - unset($merged[$key]); - } - } - $query = http_build_query($merged); - $base = '/marketplace/' . $integrationId; - return $query !== '' ? ($base . '?' . $query) : $base; -}; -?> - -
-

(string) ($integrationData['name'] ?? '')])) ?>

-

-
- -
- - - - - - -
- - -
- - - - - - -
- - -
-
- -

(string) $total])) ?>

- - -

- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - SKU - - - - EAN - - - - - - Kategorie
- - - - - 0): ?> - - - - - - - -
-
- - - -
- - - - - diff --git a/archive/2026-03-02_users-only-reset/resources/views/orders/index.php b/archive/2026-03-02_users-only-reset/resources/views/orders/index.php deleted file mode 100644 index 4623549..0000000 --- a/archive/2026-03-02_users-only-reset/resources/views/orders/index.php +++ /dev/null @@ -1,28 +0,0 @@ -
-
-
-
-

-

-
-
-
- - -
- -
- - - -
-
- -
-
- - - -
diff --git a/archive/2026-03-02_users-only-reset/resources/views/products/create.php b/archive/2026-03-02_users-only-reset/resources/views/products/create.php deleted file mode 100644 index a42010f..0000000 --- a/archive/2026-03-02_users-only-reset/resources/views/products/create.php +++ /dev/null @@ -1,139 +0,0 @@ -
-

-

-
- -
- - - - -
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - -
- - - - - - - -
- -
- - -
-
-
diff --git a/archive/2026-03-02_users-only-reset/resources/views/products/edit.php b/archive/2026-03-02_users-only-reset/resources/views/products/edit.php deleted file mode 100644 index f29d769..0000000 --- a/archive/2026-03-02_users-only-reset/resources/views/products/edit.php +++ /dev/null @@ -1,626 +0,0 @@ - - - - - -
-

(string) ($productId ?? 0)]))) ?>

-

- -

Tryb integracyjny: zapis aktualizuje bezposrednio produkt w shopPRO i synchronizuje dane lokalne.

- -
- -
- - - - - -
- - - - -
-
- SKU - - -
- - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - - - - -
- - - -
-
- - - - - - -
- - -
- - -
- -
-
-
- -
- -
- -
-
-
- -
-
- - - - -
-

- Puste pole = używana wartość globalna. -

- - - -
- -
-
-
- -
- -
- -
-
-
- -
-
- -
- - -
-

-

- -
- - -
-
- - <?= $e((string) ($image['alt'] ?? '')) ?> - -
NO IMAGE
- - -
-
-
- - -
-
- -
- - - -

-

-
- - -
- - -
-
-
- - - - - - - - - diff --git a/archive/2026-03-02_users-only-reset/resources/views/products/index.php b/archive/2026-03-02_users-only-reset/resources/views/products/index.php deleted file mode 100644 index a7921ba..0000000 --- a/archive/2026-03-02_users-only-reset/resources/views/products/index.php +++ /dev/null @@ -1,293 +0,0 @@ -
-
-
-

-

-
-
-
- - -
- -
- - - -
-
- -
-
- - - - - - - - - - - - diff --git a/archive/2026-03-02_users-only-reset/resources/views/products/links.php b/archive/2026-03-02_users-only-reset/resources/views/products/links.php deleted file mode 100644 index f088fd4..0000000 --- a/archive/2026-03-02_users-only-reset/resources/views/products/links.php +++ /dev/null @@ -1,268 +0,0 @@ - - - - - - - - - -
-

(string) ($productId ?? 0)])) ?>

-

-
- -
-
- - -
-
- -
- -
- -
-

- - - - - - -
- - -

- -

- -
- - - - - - - - - - - - - - - - - - $missingAlertFirstDetectedAt, - ]); - } - $lastChangeAt = trim((string) ($link['updated_at'] ?? '')); - if ($lastChangeAt === '') { - $lastChangeAt = trim((string) ($link['linked_at'] ?? '')); - } - ?> - - - - - - - - - - - - - - -
- - - - - - - - - - - -
-
- - -

- - - -

- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
SKUEAN
-
- - - - - - -
-
-
- -
- -
- - - -
- - diff --git a/archive/2026-03-02_users-only-reset/resources/views/products/show.php b/archive/2026-03-02_users-only-reset/resources/views/products/show.php deleted file mode 100644 index 0e9b1ea..0000000 --- a/archive/2026-03-02_users-only-reset/resources/views/products/show.php +++ /dev/null @@ -1,197 +0,0 @@ -
-

(string) ($productId ?? 0)])) ?>

-

-
- - - - - - - - -
- -
- - - -
-
- -
-
- - -
-
- - -
-
- -
- - - - -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ID
SKU
EAN - - - - -
- - -
- -
Producent
GPSR — informacje o bezpieczeństwie
Dodatkowe pola - -
()
- -
-
- -
-

- -

- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
IDSKUEAN
-
- -
- -
-

- -

- -
- -
-
- ID: ' . $e($t('products.images.main')) . '' : '' ?> - -
- -
-
- -
- -
- <?= $e((string) ($image['alt'] ?? '')) ?> -
- -
- -
- -
- -
- - - -
diff --git a/archive/2026-03-02_users-only-reset/resources/views/settings/cron.php b/archive/2026-03-02_users-only-reset/resources/views/settings/cron.php deleted file mode 100644 index 0e5f88e..0000000 --- a/archive/2026-03-02_users-only-reset/resources/views/settings/cron.php +++ /dev/null @@ -1,156 +0,0 @@ - - -
-

-

- -
- -
-

-

- - - - - - -
- - -
- - - - - - -
- -
-
-
- -
-

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
- -
-

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
ID
-
-
- -
-

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ID
-
-
diff --git a/archive/2026-03-02_users-only-reset/resources/views/settings/database.php b/archive/2026-03-02_users-only-reset/resources/views/settings/database.php deleted file mode 100644 index ff42635..0000000 --- a/archive/2026-03-02_users-only-reset/resources/views/settings/database.php +++ /dev/null @@ -1,100 +0,0 @@ - - -
-

-

- -
- -
-

- - - - - - -
- -
- - -
-
- - -
-
- - -
-
- - -
-
- - 0): ?> -
- -
- -
- - -
- -
- -
- -
- -
-

-
- - - - - - - - - - - - - - - - - - - -
-
-
- - -
-

-
-
- diff --git a/archive/2026-03-02_users-only-reset/resources/views/settings/gs1.php b/archive/2026-03-02_users-only-reset/resources/views/settings/gs1.php deleted file mode 100644 index d429388..0000000 --- a/archive/2026-03-02_users-only-reset/resources/views/settings/gs1.php +++ /dev/null @@ -1,58 +0,0 @@ -
-

-

- -
- -
-

-

- - - - - - -
- - -
- - - - - - - - - - - - -
- -
-
-
diff --git a/archive/2026-03-02_users-only-reset/resources/views/settings/integrations.php b/archive/2026-03-02_users-only-reset/resources/views/settings/integrations.php deleted file mode 100644 index b2f874a..0000000 --- a/archive/2026-03-02_users-only-reset/resources/views/settings/integrations.php +++ /dev/null @@ -1,215 +0,0 @@ - 0; -?> - -
-

-

- -
- -
-

- - - - - - -
- -
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ID
- - - - - - -
- - | HTTP - -
- -
- - - -
-
- - - -
-
-
-
- -
-

- -

- -
- - - -
- - - - - - - - - - - -
- - - - - -
- - - - - - -
-
- - -

-
- - - - - - - - - - - - - - - - - - - - -
-
- - -
diff --git a/archive/2026-03-02_users-only-reset/resources/views/settings/order-statuses.php b/archive/2026-03-02_users-only-reset/resources/views/settings/order-statuses.php deleted file mode 100644 index 3d9e1dc..0000000 --- a/archive/2026-03-02_users-only-reset/resources/views/settings/order-statuses.php +++ /dev/null @@ -1,107 +0,0 @@ - - -
-

-

- -
- -
-

-

- - - - - - -
- - -
- -
- - 0): ?> -
- - - -
- - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
-
- -
- -
-
- -
diff --git a/archive/2026-03-02_users-only-reset/resources/views/settings/products.php b/archive/2026-03-02_users-only-reset/resources/views/settings/products.php deleted file mode 100644 index 2421eb0..0000000 --- a/archive/2026-03-02_users-only-reset/resources/views/settings/products.php +++ /dev/null @@ -1,40 +0,0 @@ -
-

-

- - -
- -
-

-

- -
- - - - -

- -
- -
-
-
diff --git a/archive/2026-03-02_users-only-reset/src/Modules/Cron/CronJobProcessor.php b/archive/2026-03-02_users-only-reset/src/Modules/Cron/CronJobProcessor.php deleted file mode 100644 index 0ec2e70..0000000 --- a/archive/2026-03-02_users-only-reset/src/Modules/Cron/CronJobProcessor.php +++ /dev/null @@ -1,200 +0,0 @@ - */ - private array $handlers = []; - - public function __construct(private readonly CronJobRepository $jobs) - { - } - - public function registerHandler(string $jobType, callable $handler): void - { - $normalized = trim($jobType); - if ($normalized === '') { - return; - } - - $this->handlers[$normalized] = $handler; - } - - /** - * @return array{created:int,skipped:int} - */ - public function createScheduledJobs(): array - { - $created = 0; - $skipped = 0; - $schedules = $this->jobs->getDueSchedules(); - - foreach ($schedules as $schedule) { - $scheduleId = (int) ($schedule['id'] ?? 0); - $jobType = trim((string) ($schedule['job_type'] ?? '')); - $intervalSeconds = max(60, (int) ($schedule['interval_seconds'] ?? 0)); - - if ($scheduleId <= 0 || $jobType === '') { - continue; - } - - $hasPending = $this->jobs->hasPendingJob($jobType); - if ($hasPending) { - $skipped++; - } else { - $payload = is_array($schedule['payload'] ?? null) ? (array) $schedule['payload'] : null; - $this->jobs->enqueue( - $jobType, - $payload, - (int) ($schedule['priority'] ?? CronJobType::priorityFor($jobType)), - (int) ($schedule['max_attempts'] ?? CronJobType::maxAttemptsFor($jobType)) - ); - $created++; - } - - $this->jobs->touchSchedule($scheduleId, $intervalSeconds); - } - - return [ - 'created' => $created, - 'skipped' => $skipped, - ]; - } - - /** - * @return array{processed:int,completed:int,retried:int,failed:int} - */ - public function processQueue(int $limit = 20): array - { - $processed = 0; - $completed = 0; - $retried = 0; - $failed = 0; - - $jobs = $this->jobs->fetchNext($limit); - foreach ($jobs as $job) { - $processed++; - - $jobId = (int) ($job['id'] ?? 0); - $jobType = trim((string) ($job['job_type'] ?? '')); - if ($jobId <= 0 || $jobType === '') { - continue; - } - - $handler = $this->handlers[$jobType] ?? null; - if (!is_callable($handler)) { - $defaultBackoff = $this->defaultBackoffSeconds((int) ($job['attempts'] ?? 0)); - $isFinal = $this->jobs->markFailed( - $jobId, - 'Brak zarejestrowanego handlera dla typu joba: ' . $jobType, - $defaultBackoff - ); - if ($isFinal) { - $failed++; - } else { - $retried++; - } - continue; - } - - try { - $payload = is_array($job['payload'] ?? null) ? (array) $job['payload'] : []; - $result = $handler($payload, $job); - - $ok = true; - $message = ''; - $retryAfter = 0; - $resultPayload = []; - - if (is_bool($result)) { - $ok = $result; - } elseif (is_array($result)) { - $ok = ($result['ok'] ?? true) === true; - $message = trim((string) ($result['message'] ?? '')); - $retryAfter = max(0, (int) ($result['retry_after'] ?? 0)); - $resultPayload = $result; - } - - if ($ok) { - $this->jobs->markCompleted($jobId, $resultPayload === [] ? null : $resultPayload); - $completed++; - continue; - } - - if ($message === '') { - $message = 'Handler zakonczyl job niepowodzeniem.'; - } - $backoffSeconds = $retryAfter > 0 ? $retryAfter : $this->defaultBackoffSeconds((int) ($job['attempts'] ?? 0)); - $isFinal = $this->jobs->markFailed($jobId, $message, $backoffSeconds); - if ($isFinal) { - $failed++; - } else { - $retried++; - } - } catch (Throwable $exception) { - $backoffSeconds = $this->defaultBackoffSeconds((int) ($job['attempts'] ?? 0)); - $isFinal = $this->jobs->markFailed($jobId, $exception->getMessage(), $backoffSeconds); - if ($isFinal) { - $failed++; - } else { - $retried++; - } - } - } - - return [ - 'processed' => $processed, - 'completed' => $completed, - 'retried' => $retried, - 'failed' => $failed, - ]; - } - - /** - * @return array{ - * recovered:int, - * scheduled_created:int, - * scheduled_skipped:int, - * processed:int, - * completed:int, - * retried:int, - * failed:int, - * cleaned:int - * } - */ - public function run(int $limit = 20): array - { - if ($limit <= 0) { - throw new RuntimeException('Limit przetwarzania cron musi byc wiekszy od 0.'); - } - - $recovered = $this->jobs->recoverStuck(15); - $scheduled = $this->createScheduledJobs(); - $processed = $this->processQueue($limit); - $cleaned = $this->jobs->cleanup(30); - - return [ - 'recovered' => $recovered, - 'scheduled_created' => (int) ($scheduled['created'] ?? 0), - 'scheduled_skipped' => (int) ($scheduled['skipped'] ?? 0), - 'processed' => (int) ($processed['processed'] ?? 0), - 'completed' => (int) ($processed['completed'] ?? 0), - 'retried' => (int) ($processed['retried'] ?? 0), - 'failed' => (int) ($processed['failed'] ?? 0), - 'cleaned' => $cleaned, - ]; - } - - private function defaultBackoffSeconds(int $attemptsAlreadyDone): int - { - $currentAttempt = max(1, $attemptsAlreadyDone + 1); - $seconds = (int) (60 * (2 ** ($currentAttempt - 1))); - - return min(3600, max(60, $seconds)); - } -} diff --git a/archive/2026-03-02_users-only-reset/src/Modules/Cron/CronJobRepository.php b/archive/2026-03-02_users-only-reset/src/Modules/Cron/CronJobRepository.php deleted file mode 100644 index 054a28c..0000000 --- a/archive/2026-03-02_users-only-reset/src/Modules/Cron/CronJobRepository.php +++ /dev/null @@ -1,517 +0,0 @@ -pdo->prepare( - 'INSERT INTO cron_jobs ( - job_type, status, priority, payload, attempts, max_attempts, - scheduled_at, created_at, updated_at - ) VALUES ( - :job_type, :status, :priority, :payload, :attempts, :max_attempts, - :scheduled_at, :created_at, :updated_at - )' - ); - - $now = date('Y-m-d H:i:s'); - $scheduled = $scheduledAt !== null && trim($scheduledAt) !== '' - ? trim($scheduledAt) - : $now; - $resolvedPriority = $priority !== null && $priority >= 0 - ? min(255, $priority) - : CronJobType::priorityFor($jobType); - $resolvedMaxAttempts = $maxAttempts !== null && $maxAttempts > 0 - ? min(999, $maxAttempts) - : CronJobType::maxAttemptsFor($jobType); - - $statement->execute([ - 'job_type' => trim($jobType), - 'status' => 'pending', - 'priority' => $resolvedPriority, - 'payload' => $this->encodeJson($payload), - 'attempts' => 0, - 'max_attempts' => $resolvedMaxAttempts, - 'scheduled_at' => $scheduled, - 'created_at' => $now, - 'updated_at' => $now, - ]); - - return (int) $this->pdo->lastInsertId(); - } - - public function hasPendingJob(string $jobType, ?array $payload = null): bool - { - $sql = 'SELECT 1 - FROM cron_jobs - WHERE job_type = :job_type - AND status IN (\'pending\', \'processing\')'; - $params = [ - 'job_type' => trim($jobType), - ]; - - if ($payload !== null) { - $sql .= ' AND payload = :payload'; - $params['payload'] = $this->encodeJson($payload); - } - - $sql .= ' LIMIT 1'; - - $statement = $this->pdo->prepare($sql); - $statement->execute($params); - - return $statement->fetchColumn() !== false; - } - - /** - * @return array> - */ - public function fetchNext(int $limit = 1): array - { - $safeLimit = max(1, min(100, $limit)); - $now = date('Y-m-d H:i:s'); - - $this->pdo->beginTransaction(); - - try { - $select = $this->pdo->prepare( - 'SELECT id, job_type, status, priority, payload, result, attempts, max_attempts, - last_error, scheduled_at, started_at, completed_at, created_at, updated_at - FROM cron_jobs - WHERE status = :status - AND scheduled_at <= :scheduled_at - ORDER BY priority ASC, scheduled_at ASC, id ASC - LIMIT :limit - FOR UPDATE' - ); - $select->bindValue(':status', 'pending'); - $select->bindValue(':scheduled_at', $now); - $select->bindValue(':limit', $safeLimit, PDO::PARAM_INT); - $select->execute(); - - $rows = $select->fetchAll(); - if (!is_array($rows) || $rows === []) { - if ($this->pdo->inTransaction()) { - $this->pdo->commit(); - } - - return []; - } - - $ids = array_values(array_map( - static fn (array $row): int => (int) ($row['id'] ?? 0), - array_filter($rows, static fn (mixed $row): bool => is_array($row)) - )); - $ids = array_values(array_filter($ids, static fn (int $id): bool => $id > 0)); - - if ($ids === []) { - if ($this->pdo->inTransaction()) { - $this->pdo->commit(); - } - - return []; - } - - $placeholders = implode(', ', array_fill(0, count($ids), '?')); - $update = $this->pdo->prepare( - 'UPDATE cron_jobs SET - status = ?, - started_at = ?, - updated_at = ? - WHERE id IN (' . $placeholders . ')' - ); - $update->execute(array_merge(['processing', $now, $now], $ids)); - - if ($this->pdo->inTransaction()) { - $this->pdo->commit(); - } - - return array_map([$this, 'mapJobRow'], $rows); - } catch (Throwable $exception) { - if ($this->pdo->inTransaction()) { - $this->pdo->rollBack(); - } - - throw $exception; - } - } - - public function markCompleted(int $jobId, ?array $result = null): void - { - $statement = $this->pdo->prepare( - 'UPDATE cron_jobs SET - status = :status, - attempts = attempts + 1, - result = :result, - last_error = NULL, - completed_at = :completed_at, - updated_at = :updated_at - WHERE id = :id' - ); - - $now = date('Y-m-d H:i:s'); - $statement->execute([ - 'id' => $jobId, - 'status' => 'completed', - 'result' => $this->encodeJson($result), - 'completed_at' => $now, - 'updated_at' => $now, - ]); - } - - public function markFailed(int $jobId, string $errorMessage, int $backoffSeconds = 60): bool - { - $this->pdo->beginTransaction(); - - try { - $select = $this->pdo->prepare( - 'SELECT attempts, max_attempts - FROM cron_jobs - WHERE id = :id - LIMIT 1 - FOR UPDATE' - ); - $select->execute(['id' => $jobId]); - - $row = $select->fetch(); - if (!is_array($row)) { - if ($this->pdo->inTransaction()) { - $this->pdo->commit(); - } - - return true; - } - - $attempts = (int) ($row['attempts'] ?? 0) + 1; - $maxAttempts = max(1, (int) ($row['max_attempts'] ?? 1)); - $trimmedError = mb_substr(trim($errorMessage), 0, 500); - $now = date('Y-m-d H:i:s'); - - if ($attempts >= $maxAttempts) { - $update = $this->pdo->prepare( - 'UPDATE cron_jobs SET - status = :status, - attempts = :attempts, - last_error = :last_error, - completed_at = :completed_at, - updated_at = :updated_at - WHERE id = :id' - ); - $update->execute([ - 'id' => $jobId, - 'status' => 'failed', - 'attempts' => $attempts, - 'last_error' => $trimmedError, - 'completed_at' => $now, - 'updated_at' => $now, - ]); - - if ($this->pdo->inTransaction()) { - $this->pdo->commit(); - } - - return true; - } - - $scheduledAt = (new DateTimeImmutable($now)) - ->modify('+' . max(1, $backoffSeconds) . ' seconds') - ->format('Y-m-d H:i:s'); - - $update = $this->pdo->prepare( - 'UPDATE cron_jobs SET - status = :status, - attempts = :attempts, - last_error = :last_error, - scheduled_at = :scheduled_at, - started_at = NULL, - completed_at = NULL, - updated_at = :updated_at - WHERE id = :id' - ); - $update->execute([ - 'id' => $jobId, - 'status' => 'pending', - 'attempts' => $attempts, - 'last_error' => $trimmedError, - 'scheduled_at' => $scheduledAt, - 'updated_at' => $now, - ]); - - if ($this->pdo->inTransaction()) { - $this->pdo->commit(); - } - - return false; - } catch (Throwable $exception) { - if ($this->pdo->inTransaction()) { - $this->pdo->rollBack(); - } - - throw $exception; - } - } - - public function recoverStuck(int $olderThanMinutes = 15): int - { - $threshold = (new DateTimeImmutable()) - ->modify('-' . max(1, $olderThanMinutes) . ' minutes') - ->format('Y-m-d H:i:s'); - $now = date('Y-m-d H:i:s'); - - $statement = $this->pdo->prepare( - 'UPDATE cron_jobs SET - status = :status, - started_at = NULL, - scheduled_at = :scheduled_at, - updated_at = :updated_at - WHERE status = :processing_status - AND started_at IS NOT NULL - AND started_at < :threshold' - ); - $statement->execute([ - 'status' => 'pending', - 'processing_status' => 'processing', - 'scheduled_at' => $now, - 'updated_at' => $now, - 'threshold' => $threshold, - ]); - - return $statement->rowCount(); - } - - public function cleanup(int $olderThanDays = 30): int - { - $threshold = (new DateTimeImmutable()) - ->modify('-' . max(1, $olderThanDays) . ' days') - ->format('Y-m-d H:i:s'); - - $statement = $this->pdo->prepare( - 'DELETE FROM cron_jobs - WHERE status IN (\'completed\', \'failed\', \'cancelled\') - AND completed_at IS NOT NULL - AND completed_at < :threshold' - ); - $statement->execute(['threshold' => $threshold]); - - return $statement->rowCount(); - } - - /** - * @return array> - */ - public function getDueSchedules(): array - { - $statement = $this->pdo->prepare( - 'SELECT id, job_type, interval_seconds, priority, max_attempts, payload, - enabled, last_run_at, next_run_at, created_at, updated_at - FROM cron_schedules - WHERE enabled = 1 - AND (next_run_at IS NULL OR next_run_at <= :now) - ORDER BY priority ASC, next_run_at ASC, id ASC' - ); - $statement->execute(['now' => date('Y-m-d H:i:s')]); - - $rows = $statement->fetchAll(); - if (!is_array($rows)) { - return []; - } - - return array_map([$this, 'mapScheduleRow'], $rows); - } - - public function touchSchedule(int $scheduleId, int $intervalSeconds): void - { - $safeInterval = max(60, $intervalSeconds); - $now = date('Y-m-d H:i:s'); - $nextRunAt = (new DateTimeImmutable($now)) - ->modify('+' . $safeInterval . ' seconds') - ->format('Y-m-d H:i:s'); - - $statement = $this->pdo->prepare( - 'UPDATE cron_schedules SET - last_run_at = :last_run_at, - next_run_at = :next_run_at, - updated_at = :updated_at - WHERE id = :id' - ); - $statement->execute([ - 'id' => $scheduleId, - 'last_run_at' => $now, - 'next_run_at' => $nextRunAt, - 'updated_at' => $now, - ]); - } - - /** - * @return array> - */ - public function listPastJobs(int $limit = 100): array - { - $statement = $this->pdo->prepare( - 'SELECT id, job_type, status, priority, payload, result, attempts, max_attempts, - last_error, scheduled_at, started_at, completed_at, created_at, updated_at - FROM cron_jobs - WHERE scheduled_at <= :now - ORDER BY scheduled_at DESC, id DESC - LIMIT :limit' - ); - $statement->bindValue(':now', date('Y-m-d H:i:s')); - $statement->bindValue(':limit', max(1, min(500, $limit)), PDO::PARAM_INT); - $statement->execute(); - - $rows = $statement->fetchAll(); - if (!is_array($rows)) { - return []; - } - - return array_map([$this, 'mapJobRow'], $rows); - } - - /** - * @return array> - */ - public function listFutureJobs(int $limit = 100): array - { - $statement = $this->pdo->prepare( - 'SELECT id, job_type, status, priority, payload, result, attempts, max_attempts, - last_error, scheduled_at, started_at, completed_at, created_at, updated_at - FROM cron_jobs - WHERE scheduled_at > :now - ORDER BY scheduled_at ASC, priority ASC, id ASC - LIMIT :limit' - ); - $statement->bindValue(':now', date('Y-m-d H:i:s')); - $statement->bindValue(':limit', max(1, min(500, $limit)), PDO::PARAM_INT); - $statement->execute(); - - $rows = $statement->fetchAll(); - if (!is_array($rows)) { - return []; - } - - return array_map([$this, 'mapJobRow'], $rows); - } - - /** - * @return array> - */ - public function listSchedules(int $limit = 100): array - { - $statement = $this->pdo->prepare( - 'SELECT id, job_type, interval_seconds, priority, max_attempts, payload, - enabled, last_run_at, next_run_at, created_at, updated_at - FROM cron_schedules - ORDER BY priority ASC, job_type ASC - LIMIT :limit' - ); - $statement->bindValue(':limit', max(1, min(500, $limit)), PDO::PARAM_INT); - $statement->execute(); - - $rows = $statement->fetchAll(); - if (!is_array($rows)) { - return []; - } - - return array_map([$this, 'mapScheduleRow'], $rows); - } - - /** - * @param array|null $payload - */ - private function encodeJson(?array $payload): ?string - { - if ($payload === null) { - return null; - } - - $encoded = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - if ($encoded === false) { - return null; - } - - return $encoded; - } - - /** - * @param array $row - * @return array - */ - private function mapJobRow(array $row): array - { - return [ - 'id' => (int) ($row['id'] ?? 0), - 'job_type' => (string) ($row['job_type'] ?? ''), - 'status' => (string) ($row['status'] ?? ''), - 'priority' => (int) ($row['priority'] ?? 100), - 'payload' => $this->decodeJson($row['payload'] ?? null), - 'result' => $this->decodeJson($row['result'] ?? null), - 'attempts' => (int) ($row['attempts'] ?? 0), - 'max_attempts' => (int) ($row['max_attempts'] ?? 0), - 'last_error' => isset($row['last_error']) ? (string) $row['last_error'] : null, - 'scheduled_at' => isset($row['scheduled_at']) ? (string) $row['scheduled_at'] : null, - 'started_at' => isset($row['started_at']) ? (string) $row['started_at'] : null, - 'completed_at' => isset($row['completed_at']) ? (string) $row['completed_at'] : null, - 'created_at' => isset($row['created_at']) ? (string) $row['created_at'] : null, - 'updated_at' => isset($row['updated_at']) ? (string) $row['updated_at'] : null, - ]; - } - - /** - * @param array $row - * @return array - */ - private function mapScheduleRow(array $row): array - { - return [ - 'id' => (int) ($row['id'] ?? 0), - 'job_type' => (string) ($row['job_type'] ?? ''), - 'interval_seconds' => (int) ($row['interval_seconds'] ?? 0), - 'priority' => (int) ($row['priority'] ?? 100), - 'max_attempts' => (int) ($row['max_attempts'] ?? 3), - 'payload' => $this->decodeJson($row['payload'] ?? null), - 'enabled' => ((int) ($row['enabled'] ?? 0)) === 1, - 'last_run_at' => isset($row['last_run_at']) ? (string) $row['last_run_at'] : null, - 'next_run_at' => isset($row['next_run_at']) ? (string) $row['next_run_at'] : null, - 'created_at' => isset($row['created_at']) ? (string) $row['created_at'] : null, - 'updated_at' => isset($row['updated_at']) ? (string) $row['updated_at'] : null, - ]; - } - - /** - * @return array|null - */ - private function decodeJson(mixed $value): ?array - { - if ($value === null) { - return null; - } - - $raw = trim((string) $value); - if ($raw === '') { - return null; - } - - $decoded = json_decode($raw, true); - if (!is_array($decoded)) { - return null; - } - - return $decoded; - } -} diff --git a/archive/2026-03-02_users-only-reset/src/Modules/Cron/CronJobType.php b/archive/2026-03-02_users-only-reset/src/Modules/Cron/CronJobType.php deleted file mode 100644 index 151d70b..0000000 --- a/archive/2026-03-02_users-only-reset/src/Modules/Cron/CronJobType.php +++ /dev/null @@ -1,38 +0,0 @@ - 110, - self::SHOPPRO_ORDERS_IMPORT => 90, - self::SHOPPRO_ORDER_STATUS_SYNC => 95, - self::SHOPPRO_OFFER_TITLES_REFRESH => 170, - default => self::PRIORITY_NORMAL, - }; - } - - public static function maxAttemptsFor(string $jobType): int - { - return match (trim($jobType)) { - self::PRODUCT_LINKS_HEALTH_CHECK => 3, - self::SHOPPRO_ORDERS_IMPORT => 3, - self::SHOPPRO_ORDER_STATUS_SYNC => 3, - self::SHOPPRO_OFFER_TITLES_REFRESH => 3, - default => 3, - }; - } -} diff --git a/archive/2026-03-02_users-only-reset/src/Modules/Cron/ProductLinksHealthCheckHandler.php b/archive/2026-03-02_users-only-reset/src/Modules/Cron/ProductLinksHealthCheckHandler.php deleted file mode 100644 index f94177c..0000000 --- a/archive/2026-03-02_users-only-reset/src/Modules/Cron/ProductLinksHealthCheckHandler.php +++ /dev/null @@ -1,140 +0,0 @@ - $payload - * @param array $job - * @return array - */ - public function __invoke(array $payload = [], array $job = []): array - { - $forcedIntegrationId = max(0, (int) ($payload['integration_id'] ?? 0)); - $activeIntegrations = array_values(array_filter( - $this->integrations->listByType('shoppro'), - static function (array $integration) use ($forcedIntegrationId): bool { - $id = (int) ($integration['id'] ?? 0); - if ($forcedIntegrationId > 0 && $id !== $forcedIntegrationId) { - return false; - } - - return $id > 0 - && ($integration['is_active'] ?? false) === true - && ($integration['has_api_key'] ?? false) === true; - } - )); - - if ($activeIntegrations === []) { - return [ - 'ok' => true, - 'message' => 'Brak aktywnych integracji z kluczem API do weryfikacji powiazan.', - 'checked_links' => 0, - 'missing_links' => 0, - 'integrations' => 0, - 'integration_failures' => 0, - ]; - } - - $checkedLinks = 0; - $missingLinks = 0; - $resolvedAlerts = 0; - $integrationFailures = 0; - $errors = []; - $checkedAt = date('Y-m-d H:i:s'); - - foreach ($activeIntegrations as $integration) { - $integrationId = (int) ($integration['id'] ?? 0); - if ($integrationId <= 0) { - continue; - } - - try { - $credentials = $this->integrations->findApiCredentials($integrationId); - } catch (Throwable $exception) { - $integrationFailures++; - if (count($errors) < 5) { - $errors[] = 'Integracja #' . $integrationId . ': ' . $exception->getMessage(); - } - continue; - } - - if ($credentials === null || trim((string) ($credentials['api_key'] ?? '')) === '') { - $integrationFailures++; - if (count($errors) < 5) { - $errors[] = 'Integracja #' . $integrationId . ': brak poprawnych danych API.'; - } - continue; - } - - $import = $this->offerImportService->importShopProOffers($credentials); - if (($import['ok'] ?? false) !== true) { - $integrationFailures++; - if (count($errors) < 5) { - $errors[] = 'Integracja #' . $integrationId . ': ' . trim((string) ($import['message'] ?? 'Blad importu ofert.')); - } - continue; - } - - $links = $this->links->listActiveLinksForMissingCheck($integrationId); - foreach ($links as $link) { - $mapId = (int) ($link['id'] ?? 0); - $externalProductId = trim((string) ($link['external_product_id'] ?? '')); - $externalVariantId = $this->nullableText($link['external_variant_id'] ?? null); - if ($mapId <= 0 || $externalProductId === '') { - continue; - } - - $checkedLinks++; - $offer = $this->offers->findByExternalIdentity($integrationId, $externalProductId, $externalVariantId); - - if ($offer === null) { - $missingLinks++; - $this->links->upsertActiveAlert($mapId, self::ALERT_TYPE, self::ALERT_MESSAGE, $checkedAt); - continue; - } - - $this->links->resolveActiveAlert($mapId, self::ALERT_TYPE, $checkedAt); - $resolvedAlerts++; - } - } - - return [ - 'ok' => $integrationFailures === 0, - 'message' => $integrationFailures === 0 - ? 'Weryfikacja powiazan zakonczona.' - : 'Weryfikacja zakonczona z bledami integracji.', - 'checked_links' => $checkedLinks, - 'missing_links' => $missingLinks, - 'resolved_alerts' => $resolvedAlerts, - 'integrations' => count($activeIntegrations), - 'integration_failures' => $integrationFailures, - 'errors' => $errors, - ]; - } - - private function nullableText(mixed $value): ?string - { - $text = trim((string) $value); - return $text === '' ? null : $text; - } -} diff --git a/archive/2026-03-02_users-only-reset/src/Modules/Cron/ShopProOfferTitlesRefreshHandler.php b/archive/2026-03-02_users-only-reset/src/Modules/Cron/ShopProOfferTitlesRefreshHandler.php deleted file mode 100644 index 75cbeb5..0000000 --- a/archive/2026-03-02_users-only-reset/src/Modules/Cron/ShopProOfferTitlesRefreshHandler.php +++ /dev/null @@ -1,106 +0,0 @@ - $payload - * @param array $job - * @return array - */ - public function __invoke(array $payload = [], array $job = []): array - { - $forcedIntegrationId = max(0, (int) ($payload['integration_id'] ?? 0)); - $activeIntegrations = array_values(array_filter( - $this->integrations->listByType('shoppro'), - static function (array $integration) use ($forcedIntegrationId): bool { - $id = (int) ($integration['id'] ?? 0); - if ($forcedIntegrationId > 0 && $id !== $forcedIntegrationId) { - return false; - } - - return $id > 0 - && ($integration['is_active'] ?? false) === true - && ($integration['has_api_key'] ?? false) === true; - } - )); - - if ($activeIntegrations === []) { - return [ - 'ok' => true, - 'message' => 'Brak aktywnych integracji z kluczem API do odswiezenia tytulow ofert.', - 'integrations' => 0, - 'updated_offers' => 0, - 'failed_offers' => 0, - 'integration_failures' => 0, - 'errors' => [], - ]; - } - - $updatedOffers = 0; - $failedOffers = 0; - $integrationFailures = 0; - $errors = []; - - foreach ($activeIntegrations as $integration) { - $integrationId = (int) ($integration['id'] ?? 0); - if ($integrationId <= 0) { - continue; - } - - try { - $credentials = $this->integrations->findApiCredentials($integrationId); - } catch (Throwable $exception) { - $integrationFailures++; - if (count($errors) < 5) { - $errors[] = 'Integracja #' . $integrationId . ': ' . $exception->getMessage(); - } - continue; - } - - if ($credentials === null || trim((string) ($credentials['api_key'] ?? '')) === '') { - $integrationFailures++; - if (count($errors) < 5) { - $errors[] = 'Integracja #' . $integrationId . ': brak poprawnych danych API.'; - } - continue; - } - - $import = $this->offerImportService->importShopProOffers($credentials); - if (($import['ok'] ?? false) !== true) { - $integrationFailures++; - if (count($errors) < 5) { - $errors[] = 'Integracja #' . $integrationId . ': ' . trim((string) ($import['message'] ?? 'Blad importu ofert.')); - } - continue; - } - - $updatedOffers += (int) ($import['imported'] ?? 0); - $failedOffers += (int) ($import['failed'] ?? 0); - } - - return [ - 'ok' => $integrationFailures === 0, - 'message' => $integrationFailures === 0 - ? 'Odswiezenie tytulow ofert zakonczone.' - : 'Odswiezenie tytulow zakonczone z bledami integracji.', - 'integrations' => count($activeIntegrations), - 'updated_offers' => $updatedOffers, - 'failed_offers' => $failedOffers, - 'integration_failures' => $integrationFailures, - 'errors' => $errors, - ]; - } -} diff --git a/archive/2026-03-02_users-only-reset/src/Modules/Cron/ShopProOrderStatusSyncHandler.php b/archive/2026-03-02_users-only-reset/src/Modules/Cron/ShopProOrderStatusSyncHandler.php deleted file mode 100644 index 8a937d8..0000000 --- a/archive/2026-03-02_users-only-reset/src/Modules/Cron/ShopProOrderStatusSyncHandler.php +++ /dev/null @@ -1,23 +0,0 @@ - $payload - * @param array $job - * @return array - */ - public function __invoke(array $payload = [], array $job = []): array - { - return $this->syncService->sync($payload); - } -} diff --git a/archive/2026-03-02_users-only-reset/src/Modules/Cron/ShopProOrdersImportHandler.php b/archive/2026-03-02_users-only-reset/src/Modules/Cron/ShopProOrdersImportHandler.php deleted file mode 100644 index 3d7a76a..0000000 --- a/archive/2026-03-02_users-only-reset/src/Modules/Cron/ShopProOrdersImportHandler.php +++ /dev/null @@ -1,23 +0,0 @@ - $payload - * @param array $job - * @return array - */ - public function __invoke(array $payload = [], array $job = []): array - { - return $this->orderImportService->importOne($payload); - } -} diff --git a/archive/2026-03-02_users-only-reset/src/Modules/GS1/GS1Service.php b/archive/2026-03-02_users-only-reset/src/Modules/GS1/GS1Service.php deleted file mode 100644 index aeb8dd4..0000000 --- a/archive/2026-03-02_users-only-reset/src/Modules/GS1/GS1Service.php +++ /dev/null @@ -1,69 +0,0 @@ -products = $products; - $this->appSettings = $appSettings; - } - - /** - * @return array{ean: string} - * @throws \RuntimeException - */ - public function assignEanToProduct(int $productId): array - { - $product = $this->products->findById($productId, 'pl'); - if ($product === null) { - throw new \RuntimeException('Produkt nie istnieje.'); - } - - $existingEan = trim((string) ($product['ean'] ?? '')); - if ($existingEan !== '') { - throw new \RuntimeException('Produkt ma juz przypisany EAN: ' . $existingEan); - } - - $login = $this->appSettings->get('gs1_api_login', ''); - $password = $this->appSettings->get('gs1_api_password', ''); - $prefix = $this->appSettings->get('gs1_prefix', '590532390'); - $defaultBrand = $this->appSettings->get('gs1_default_brand', 'pomysloweprezenty.pl'); - $defaultGpcCode = $this->appSettings->getInt('gs1_default_gpc_code', 10008365); - - if ($login === '' || $password === '') { - throw new \RuntimeException('Brak danych dostepu do API GS1. Uzupelnij je w Ustawienia > GS1.'); - } - - $client = new MojeGS1Client($login, $password); - - $highest = $client->findHighestGtin($prefix); - $newEan = MojeGS1Client::generateNextEan($prefix, $highest); - - $productName = trim((string) ($product['name'] ?? '')); - $commonName = $productName !== '' ? mb_substr($productName, 0, 150) : 'Produkt ' . $productId; - - $client->upsertProduct($newEan, [ - 'brandName' => $defaultBrand, - 'commonName' => $commonName, - 'gpcCode' => $defaultGpcCode, - 'netContent' => 1, - 'netContentUnit' => 'szt', - 'status' => 'ACT', - 'targetMarket' => ['PL'], - 'descriptionLanguage' => 'PL', - ]); - - $this->products->updateEan($productId, $newEan); - - return ['ean' => $newEan]; - } -} diff --git a/archive/2026-03-02_users-only-reset/src/Modules/GS1/MojeGS1Client.php b/archive/2026-03-02_users-only-reset/src/Modules/GS1/MojeGS1Client.php deleted file mode 100644 index 925b533..0000000 --- a/archive/2026-03-02_users-only-reset/src/Modules/GS1/MojeGS1Client.php +++ /dev/null @@ -1,211 +0,0 @@ -login = $login; - $this->password = $password; - } - - /** - * @return array{data: array>, total: int} - */ - public function listProducts(int $offset = 1, int $limit = 100): array - { - $url = self::BASE_URL . '/products?page[offset]=' . max(1, $offset) . '&page[limit]=' . $limit . '&sort=name'; - $response = $this->request('GET', $url); - - $data = $response['data'] ?? []; - $total = (int) ($response['meta']['record-count'] ?? 0); - - return ['data' => is_array($data) ? $data : [], 'total' => $total]; - } - - /** - * @return array|null - */ - public function getProduct(string $gtin): ?array - { - $url = self::BASE_URL . '/products/' . urlencode($gtin); - - try { - $response = $this->request('GET', $url); - } catch (\RuntimeException $e) { - if (str_contains($e->getMessage(), '404')) { - return null; - } - throw $e; - } - - return is_array($response['data'] ?? null) ? $response['data'] : null; - } - - /** - * @param array $attributes - * @return array - */ - public function upsertProduct(string $gtin, array $attributes): array - { - $url = self::BASE_URL . '/products/' . urlencode($gtin); - $payload = [ - 'data' => [ - 'type' => 'products', - 'id' => $gtin, - 'attributes' => $attributes, - ], - ]; - - return $this->request('PUT', $url, $payload); - } - - /** - * Finds the highest GTIN registered under the given prefix by paginating through all products. - */ - public function findHighestGtin(string $prefix): ?string - { - $highest = null; - $page = 1; - $limit = 100; - - do { - $result = $this->listProducts($page, $limit); - $items = $result['data']; - - foreach ($items as $item) { - $gtin = (string) ($item['id'] ?? ''); - if ($gtin === '' || !str_starts_with($gtin, $prefix)) { - continue; - } - if ($highest === null || $gtin > $highest) { - $highest = $gtin; - } - } - - $page++; - } while (count($items) >= $limit); - - return $highest; - } - - /** - * Generates the next EAN-13 from a prefix, given the current highest GTIN. - */ - public static function generateNextEan(string $prefix, ?string $currentHighest): string - { - $prefixLen = strlen($prefix); - $itemDigits = 12 - $prefixLen; - - if ($currentHighest !== null && str_starts_with($currentHighest, $prefix)) { - $currentItem = (int) substr($currentHighest, $prefixLen, $itemDigits); - $nextItem = $currentItem + 1; - } else { - $nextItem = 0; - } - - $partial12 = $prefix . str_pad((string) $nextItem, $itemDigits, '0', STR_PAD_LEFT); - $checkDigit = self::calculateEan13CheckDigit($partial12); - - return $partial12 . $checkDigit; - } - - /** - * Calculates the EAN-13 check digit for the first 12 digits. - */ - public static function calculateEan13CheckDigit(string $partial12): int - { - if (strlen($partial12) !== 12 || !ctype_digit($partial12)) { - throw new \InvalidArgumentException('EAN-13 check digit requires exactly 12 digits, got: ' . $partial12); - } - - $sum = 0; - for ($i = 0; $i < 12; $i++) { - $digit = (int) $partial12[$i]; - $sum += ($i % 2 === 0) ? $digit : $digit * 3; - } - - $remainder = $sum % 10; - return $remainder === 0 ? 0 : 10 - $remainder; - } - - /** - * @param array|null $jsonBody - * @return array - */ - private function request(string $method, string $url, ?array $jsonBody = null): array - { - $curl = curl_init($url); - if ($curl === false) { - throw new \RuntimeException('Nie mozna zainicjalizowac cURL.'); - } - - $headers = [ - 'Accept: application/json', - ]; - - $requestBody = null; - if ($jsonBody !== null && $method !== 'GET') { - $encoded = json_encode($jsonBody, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - if ($encoded === false) { - throw new \RuntimeException('Nie mozna zakodowac payload JSON.'); - } - $requestBody = $encoded; - $headers[] = 'Content-Type: application/json'; - } - - curl_setopt_array($curl, [ - CURLOPT_RETURNTRANSFER => true, - CURLOPT_HTTPHEADER => $headers, - CURLOPT_USERPWD => $this->login . ':' . $this->password, - CURLOPT_HTTPAUTH => CURLAUTH_BASIC, - CURLOPT_TIMEOUT => self::TIMEOUT, - CURLOPT_CONNECTTIMEOUT => 10, - CURLOPT_SSL_VERIFYPEER => true, - CURLOPT_SSL_VERIFYHOST => 2, - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_MAXREDIRS => 5, - CURLOPT_CUSTOMREQUEST => $method, - ]); - - if ($requestBody !== null) { - curl_setopt($curl, CURLOPT_POSTFIELDS, $requestBody); - } - - $body = curl_exec($curl); - $httpCode = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE); - $error = curl_error($curl); - curl_close($curl); - - if ($error !== '') { - throw new \RuntimeException('GS1 API cURL error: ' . $error); - } - - $bodyStr = is_string($body) ? $body : ''; - - if ($httpCode < 200 || $httpCode >= 300) { - $debug = ' | ' . $method . ' ' . $url; - if ($requestBody !== null) { - $debug .= ' | REQ: ' . mb_substr($requestBody, 0, 600); - } - throw new \RuntimeException( - 'GS1 API HTTP ' . $httpCode . ': ' . mb_substr($bodyStr, 0, 400) . $debug - ); - } - - $decoded = json_decode($bodyStr, true); - if (!is_array($decoded)) { - throw new \RuntimeException('GS1 API: nieprawidlowa odpowiedz JSON.'); - } - - return $decoded; - } -} diff --git a/archive/2026-03-02_users-only-reset/src/Modules/Marketplace/MarketplaceController.php b/archive/2026-03-02_users-only-reset/src/Modules/Marketplace/MarketplaceController.php deleted file mode 100644 index 5194262..0000000 --- a/archive/2026-03-02_users-only-reset/src/Modules/Marketplace/MarketplaceController.php +++ /dev/null @@ -1,629 +0,0 @@ -marketplace->listActiveIntegrationsWithCounts(); - - $html = $this->template->render('marketplace/index', [ - 'title' => $this->translator->get('marketplace.title'), - 'activeMenu' => 'marketplace', - 'user' => $this->auth->user(), - 'csrfToken' => Csrf::token(), - 'selectedMarketplaceIntegrationId' => 0, - 'marketplaceIntegrations' => $integrations, - 'integrations' => $integrations, - 'errorMessage' => (string) Flash::get('marketplace_error', ''), - ], 'layouts/app'); - - return Response::html($html); - } - - public function offers(Request $request): Response - { - $integrationId = max(0, (int) $request->input('integration_id', 0)); - if ($integrationId <= 0) { - Flash::set('marketplace_error', $this->translator->get('marketplace.flash.integration_not_found')); - return Response::redirect('/marketplace'); - } - - $integration = $this->marketplace->findActiveIntegrationById($integrationId); - if ($integration === null) { - Flash::set('marketplace_error', $this->translator->get('marketplace.flash.integration_not_found')); - return Response::redirect('/marketplace'); - } - - $integrations = $this->marketplace->listActiveIntegrationsWithCounts(); - $filtersValues = [ - 'search' => trim((string) $request->input('search', '')), - 'channel' => trim((string) $request->input('channel', '')), - 'sort' => (string) $request->input('sort', 'updated_at'), - 'sort_dir' => (string) $request->input('sort_dir', 'DESC'), - 'page' => max(1, (int) $request->input('page', 1)), - 'per_page' => max(1, min(100, (int) $request->input('per_page', 20))), - ]; - $offersResult = $this->marketplace->paginateLinkedOffersByIntegration($integrationId, $filtersValues, 'pl'); - $offers = (array) ($offersResult['items'] ?? []); - $totalPages = max(1, (int) ceil(((int) ($offersResult['total'] ?? 0)) / (int) ($offersResult['per_page'] ?? 20))); - $channelOptions = $this->marketplace->listOfferChannelsByIntegration($integrationId); - - $html = $this->template->render('marketplace/offers', [ - 'title' => $this->translator->get('marketplace.offers_title', ['name' => (string) ($integration['name'] ?? '')]), - 'activeMenu' => 'marketplace', - 'user' => $this->auth->user(), - 'csrfToken' => Csrf::token(), - 'selectedMarketplaceIntegrationId' => $integrationId, - 'marketplaceIntegrations' => $integrations, - 'integration' => $integration, - 'offers' => $offers, - 'filters' => $filtersValues, - 'channelOptions' => $channelOptions, - 'pagination' => [ - 'page' => (int) ($offersResult['page'] ?? 1), - 'total_pages' => $totalPages, - 'total' => (int) ($offersResult['total'] ?? 0), - 'per_page' => (int) ($offersResult['per_page'] ?? 20), - ], - 'errorMessage' => (string) Flash::get('marketplace_error', ''), - 'successMessage' => (string) Flash::get('marketplace_success', ''), - ], 'layouts/app'); - - return Response::html($html); - } - - public function editProduct(Request $request): Response - { - $integrationId = max(0, (int) $request->input('integration_id', 0)); - $externalProductId = max(0, (int) $request->input('external_product_id', 0)); - if ($integrationId <= 0 || $externalProductId <= 0) { - Flash::set('marketplace_error', $this->translator->get('marketplace.flash.integration_not_found')); - return Response::redirect('/marketplace'); - } - - $integration = $this->marketplace->findActiveIntegrationById($integrationId); - if ($integration === null) { - Flash::set('marketplace_error', $this->translator->get('marketplace.flash.integration_not_found')); - return Response::redirect('/marketplace'); - } - - $credentials = $this->integrationRepository->findApiCredentials($integrationId); - if ($credentials === null) { - Flash::set('marketplace_error', $this->translator->get('marketplace.flash.integration_not_found')); - return Response::redirect('/marketplace/' . $integrationId); - } - - $localProductId = $this->integrationRepository->findMappedProductId( - 'shoppro', - (string) $externalProductId, - $integrationId - ); - if ($localProductId === null || $localProductId <= 0) { - Flash::set('marketplace_error', $this->translator->get('products.flash.not_found')); - return Response::redirect('/marketplace/' . $integrationId); - } - - $remoteResult = $this->shopProClient->fetchProductById( - (string) ($credentials['base_url'] ?? ''), - (string) ($credentials['api_key'] ?? ''), - (int) ($credentials['timeout_seconds'] ?? 10), - $externalProductId - ); - if (($remoteResult['ok'] ?? false) !== true) { - Flash::set('marketplace_error', (string) ($remoteResult['message'] ?? 'Nie mozna pobrac produktu z integracji.')); - return Response::redirect('/marketplace/' . $integrationId); - } - - $externalProduct = is_array($remoteResult['product'] ?? null) ? $remoteResult['product'] : null; - if ($externalProduct === null) { - Flash::set('marketplace_error', 'Brak danych produktu z integracji.'); - return Response::redirect('/marketplace/' . $integrationId); - } - - $form = $this->mapExternalProductToForm($externalProduct, $externalProductId); - $old = (array) Flash::get('products_form_old', []); - if ($old !== []) { - $form = array_merge($form, $old); - } - - $activeIntegrations = $this->integrationRepository->listByType('shoppro'); - $integrationTranslationsMap = []; - foreach ($this->productRepository->findIntegrationTranslations($localProductId) as $row) { - $integrationTranslationsMap[(int) ($row['integration_id'] ?? 0)] = $row; - } - - $lang = $this->resolveProductLanguage($externalProduct); - $integrationTranslationsMap[$integrationId] = [ - 'integration_id' => $integrationId, - 'name' => trim((string) ($lang['name'] ?? '')), - 'short_description' => trim((string) ($lang['short_description'] ?? '')), - 'description' => trim((string) ($lang['description'] ?? '')), - ]; - - $html = $this->template->render('products/edit', [ - 'title' => 'Edycja produktu z integracji #' . $externalProductId, - 'activeMenu' => 'marketplace', - 'user' => $this->auth->user(), - 'csrfToken' => Csrf::token(), - 'marketplaceIntegrations' => $this->marketplace->listActiveIntegrationsWithCounts(), - 'productId' => $localProductId, - 'form' => $form, - 'productImages' => [], - 'errors' => (array) Flash::get('products_form_errors', []), - 'activeIntegrations' => $activeIntegrations, - 'integrationTranslationsMap' => $integrationTranslationsMap, - 'productFormAction' => $this->editPath($integrationId, $externalProductId, true), - 'productBackUrl' => '/marketplace/' . $integrationId, - 'integrationEditMode' => true, - 'initialContentTab' => 'integration-' . $integrationId, - ], 'layouts/app'); - - return Response::html($html); - } - - public function updateProduct(Request $request): Response - { - $csrfToken = (string) $request->input('_token', ''); - if (!Csrf::validate($csrfToken)) { - Flash::set('marketplace_error', $this->translator->get('auth.errors.csrf_expired')); - return Response::redirect('/marketplace'); - } - - $integrationId = max(0, (int) $request->input('integration_id', 0)); - $externalProductId = max(0, (int) $request->input('external_product_id', 0)); - if ($integrationId <= 0 || $externalProductId <= 0) { - Flash::set('marketplace_error', $this->translator->get('marketplace.flash.integration_not_found')); - return Response::redirect('/marketplace'); - } - - $editPath = $this->editPath($integrationId, $externalProductId, false); - $integration = $this->marketplace->findActiveIntegrationById($integrationId); - if ($integration === null) { - Flash::set('marketplace_error', $this->translator->get('marketplace.flash.integration_not_found')); - return Response::redirect('/marketplace'); - } - - $credentials = $this->integrationRepository->findApiCredentials($integrationId); - if ($credentials === null) { - Flash::set('marketplace_error', $this->translator->get('marketplace.flash.integration_not_found')); - return Response::redirect('/marketplace/' . $integrationId); - } - - $localProductId = $this->integrationRepository->findMappedProductId( - 'shoppro', - (string) $externalProductId, - $integrationId - ); - if ($localProductId === null || $localProductId <= 0) { - Flash::set('marketplace_error', $this->translator->get('products.flash.not_found')); - return Response::redirect('/marketplace/' . $integrationId); - } - - $payload = $this->payloadFromRequest($request); - Flash::set('products_form_old', $payload); - - $validationErrors = $this->productValidator->validate($payload, true); - $sku = trim((string) ($payload['sku'] ?? '')); - if ($sku !== '' && $this->productRepository->existsSku($sku, $localProductId)) { - $validationErrors[] = 'Podane SKU produktu jest juz zajete.'; - } - if ($validationErrors !== []) { - Flash::set('products_form_errors', $validationErrors); - return Response::redirect($editPath); - } - - $allowedIntegrationIds = array_map( - static fn (array $i): int => (int) ($i['id'] ?? 0), - $this->integrationRepository->listByType('shoppro') - ); - $integrationContent = $request->input('integration_content', []); - $remotePayload = $this->buildRemoteUpdatePayload( - $payload, - is_array($integrationContent) ? $integrationContent : [], - $integrationId, - $localProductId - ); - $remoteUpdate = $this->shopProClient->updateProduct( - (string) ($credentials['base_url'] ?? ''), - (string) ($credentials['api_key'] ?? ''), - (int) ($credentials['timeout_seconds'] ?? 10), - $externalProductId, - $remotePayload - ); - if (($remoteUpdate['ok'] ?? false) !== true) { - Flash::set('products_form_errors', [(string) ($remoteUpdate['message'] ?? 'Nie mozna zapisac produktu w shopPRO.')]); - return Response::redirect($editPath); - } - - $updatedOfferName = trim((string) ($remotePayload['languages']['pl']['name'] ?? '')); - if ($updatedOfferName !== '') { - $this->marketplace->updateCachedOfferNameForExternalProduct( - $integrationId, - (string) $externalProductId, - $updatedOfferName - ); - } - - $result = $this->productService->update($localProductId, $payload, $this->auth->user()); - if (($result['ok'] ?? false) !== true) { - Flash::set('products_form_errors', (array) ($result['errors'] ?? ['Nie udalo sie zapisac lokalnych zmian produktu.'])); - return Response::redirect($editPath); - } - - if (is_array($integrationContent)) { - foreach ($integrationContent as $rawIntegrationId => $content) { - $contentIntegrationId = (int) $rawIntegrationId; - if ($contentIntegrationId <= 0 || !is_array($content) || !in_array($contentIntegrationId, $allowedIntegrationIds, true)) { - continue; - } - - $this->productRepository->upsertIntegrationTranslation( - $localProductId, - $contentIntegrationId, - isset($content['name']) ? trim((string) $content['name']) : null, - isset($content['short_description']) ? trim((string) $content['short_description']) : null, - isset($content['description']) ? trim((string) $content['description']) : null - ); - } - } - - Flash::set('products_form_old', []); - Flash::set('products_form_errors', []); - Flash::set('marketplace_success', $this->translator->get('marketplace.flash.product_updated')); - - return Response::redirect('/marketplace/' . $integrationId); - } - - public function categoriesJson(Request $request): Response - { - $integrationId = max(0, (int) $request->input('integration_id', 0)); - if ($integrationId <= 0) { - return Response::json(['ok' => false, 'message' => 'Brak integration_id.'], 400); - } - - $integration = $this->marketplace->findActiveIntegrationById($integrationId); - if ($integration === null) { - return Response::json(['ok' => false, 'message' => 'Integracja nie istnieje lub jest nieaktywna.'], 404); - } - - $creds = $this->integrationRepository->findApiCredentials($integrationId); - if ($creds === null) { - return Response::json(['ok' => false, 'message' => 'Brak danych uwierzytelniających.'], 404); - } - - $result = $this->shopProClient->fetchCategories( - (string) ($creds['base_url'] ?? ''), - (string) ($creds['api_key'] ?? ''), - (int) ($creds['timeout_seconds'] ?? 10) - ); - - if (!($result['ok'] ?? false)) { - return Response::json(['ok' => false, 'message' => $result['message']], 502); - } - - return Response::json(['ok' => true, 'categories' => $result['categories']]); - } - - public function productCategoriesJson(Request $request): Response - { - $integrationId = max(0, (int) $request->input('integration_id', 0)); - $externalProductId = max(0, (int) $request->input('external_product_id', 0)); - - if ($integrationId <= 0 || $externalProductId <= 0) { - return Response::json(['ok' => false, 'message' => 'Brak wymaganych parametrów.'], 400); - } - - $integration = $this->marketplace->findActiveIntegrationById($integrationId); - if ($integration === null) { - return Response::json(['ok' => false, 'message' => 'Integracja nie istnieje.'], 404); - } - - $creds = $this->integrationRepository->findApiCredentials($integrationId); - if ($creds === null) { - return Response::json(['ok' => false, 'message' => 'Brak danych uwierzytelniających.'], 404); - } - - $result = $this->shopProClient->fetchProductById( - (string) ($creds['base_url'] ?? ''), - (string) ($creds['api_key'] ?? ''), - (int) ($creds['timeout_seconds'] ?? 10), - $externalProductId - ); - - if (!($result['ok'] ?? false)) { - return Response::json(['ok' => false, 'message' => $result['message']], 502); - } - - $product = is_array($result['product'] ?? null) ? $result['product'] : []; - $categoryIds = isset($product['categories']) && is_array($product['categories']) - ? array_values(array_filter(array_map('intval', $product['categories']), static fn(int $id): bool => $id > 0)) - : []; - - return Response::json(['ok' => true, 'current_category_ids' => $categoryIds]); - } - - public function saveProductCategoriesJson(Request $request): Response - { - $integrationId = max(0, (int) $request->input('integration_id', 0)); - $externalProductId = max(0, (int) $request->input('external_product_id', 0)); - - if ($integrationId <= 0 || $externalProductId <= 0) { - return Response::json(['ok' => false, 'message' => 'Brak wymaganych parametrów.'], 400); - } - - $rawBody = (string) file_get_contents('php://input'); - $body = json_decode($rawBody, true); - if (!is_array($body)) { - return Response::json(['ok' => false, 'message' => 'Nieprawidłowe ciało żądania JSON.'], 400); - } - - $csrfToken = (string) ($body['_token'] ?? ''); - if (!Csrf::validate($csrfToken)) { - return Response::json(['ok' => false, 'message' => 'Nieprawidłowy token CSRF.'], 403); - } - - $integration = $this->marketplace->findActiveIntegrationById($integrationId); - if ($integration === null) { - return Response::json(['ok' => false, 'message' => 'Integracja nie istnieje lub jest nieaktywna.'], 404); - } - - $creds = $this->integrationRepository->findApiCredentials($integrationId); - if ($creds === null) { - return Response::json(['ok' => false, 'message' => 'Brak danych uwierzytelniających.'], 404); - } - - $categoryIds = isset($body['category_ids']) && is_array($body['category_ids']) - ? array_values(array_filter(array_map('intval', $body['category_ids']), static fn(int $id): bool => $id > 0)) - : []; - - $payload = ['categories' => $categoryIds]; - - // Include language content so shopPRO doesn't clear name/description when saving categories. - // Use per-integration translation if set, otherwise fall back to global product translation. - $localProductId = $this->integrationRepository->findMappedProductId( - 'shoppro', - (string) $externalProductId, - $integrationId - ); - if ($localProductId !== null && $localProductId > 0) { - $integrationTranslation = null; - foreach ($this->productRepository->findIntegrationTranslations($localProductId) as $row) { - if ((int) ($row['integration_id'] ?? 0) === $integrationId) { - $integrationTranslation = $row; - break; - } - } - $global = $this->productRepository->findById($localProductId, 'pl'); - if ($global !== null) { - $name = (string) ($integrationTranslation['name'] ?? $global['name'] ?? ''); - $payload['languages'] = [ - 'pl' => [ - 'name' => $name !== '' ? $name : ('orderPRO #' . $localProductId), - 'short_description' => $integrationTranslation['short_description'] ?? $global['short_description'] ?? null, - 'description' => $integrationTranslation['description'] ?? $global['description'] ?? null, - 'meta_title' => $global['meta_title'] ?? null, - 'meta_description' => $global['meta_description'] ?? null, - 'meta_keywords' => $global['meta_keywords'] ?? null, - 'seo_link' => $global['seo_link'] ?? null, - ], - ]; - } - } - - $result = $this->shopProClient->updateProduct( - (string) ($creds['base_url'] ?? ''), - (string) ($creds['api_key'] ?? ''), - (int) ($creds['timeout_seconds'] ?? 10), - $externalProductId, - $payload - ); - - if (!($result['ok'] ?? false)) { - return Response::json(['ok' => false, 'message' => $result['message']], 502); - } - - return Response::json(['ok' => true]); - } - - private function editPath(int $integrationId, int $externalProductId, bool $forRenderAction): string - { - $suffix = $forRenderAction ? 'update' : 'edit'; - return '/marketplace/' . $integrationId . '/product/' . $externalProductId . '/' . $suffix; - } - - /** - * @param array $externalProduct - * @return array - */ - private function mapExternalProductToForm(array $externalProduct, int $externalProductId): array - { - $lang = $this->resolveProductLanguage($externalProduct); - $name = trim((string) ($lang['name'] ?? '')); - if ($name === '') { - $name = 'shopPRO #' . $externalProductId; - } - - return [ - 'type' => (array_key_exists('variants', $externalProduct) && is_array($externalProduct['variants']) && $externalProduct['variants'] !== []) - ? 'variant_parent' - : 'simple', - 'name' => $name, - 'sku' => trim((string) ($externalProduct['sku'] ?? '')), - 'ean' => trim((string) ($externalProduct['ean'] ?? '')), - 'status' => ((int) ($externalProduct['status'] ?? 1)) === 1 ? '1' : '0', - 'promoted' => ((int) ($externalProduct['promoted'] ?? 0)) === 1 ? '1' : '0', - 'vat' => (string) ($externalProduct['vat'] ?? ''), - 'weight' => (string) ($externalProduct['weight'] ?? ''), - 'quantity' => (string) ($externalProduct['quantity'] ?? '0'), - 'price_input_mode' => 'brutto', - 'price_brutto' => (string) ($externalProduct['price_brutto'] ?? ''), - 'price_netto' => (string) ($externalProduct['price_netto'] ?? ''), - 'price_brutto_promo' => (string) ($externalProduct['price_brutto_promo'] ?? ''), - 'price_netto_promo' => (string) ($externalProduct['price_netto_promo'] ?? ''), - 'short_description' => trim((string) ($lang['short_description'] ?? '')), - 'description' => trim((string) ($lang['description'] ?? '')), - 'meta_title' => trim((string) ($lang['meta_title'] ?? '')), - 'meta_description' => trim((string) ($lang['meta_description'] ?? '')), - 'meta_keywords' => trim((string) ($lang['meta_keywords'] ?? '')), - 'seo_link' => trim((string) ($lang['seo_link'] ?? '')), - ]; - } - - /** - * @param array $externalProduct - * @return array - */ - private function resolveProductLanguage(array $externalProduct): array - { - $languages = $externalProduct['languages'] ?? null; - if (!is_array($languages)) { - return []; - } - - if (isset($languages['pl']) && is_array($languages['pl'])) { - return $languages['pl']; - } - - foreach ($languages as $language) { - if (is_array($language)) { - return $language; - } - } - - return []; - } - - /** - * @return array - */ - private function payloadFromRequest(Request $request): array - { - return [ - 'type' => (string) $request->input('type', 'simple'), - 'name' => (string) $request->input('name', ''), - 'sku' => (string) $request->input('sku', ''), - 'ean' => (string) $request->input('ean', ''), - 'status' => (string) $request->input('status', '1'), - 'promoted' => (string) $request->input('promoted', '0'), - 'vat' => (string) $request->input('vat', '23'), - 'weight' => (string) $request->input('weight', ''), - 'quantity' => (string) $request->input('quantity', '0'), - 'price_input_mode' => (string) $request->input('price_input_mode', 'brutto'), - 'price_brutto' => (string) $request->input('price_brutto', ''), - 'price_netto' => (string) $request->input('price_netto', ''), - 'price_brutto_promo' => (string) $request->input('price_brutto_promo', ''), - 'price_netto_promo' => (string) $request->input('price_netto_promo', ''), - 'short_description' => (string) $request->input('short_description', ''), - 'description' => (string) $request->input('description', ''), - 'meta_title' => (string) $request->input('meta_title', ''), - 'meta_description' => (string) $request->input('meta_description', ''), - 'meta_keywords' => (string) $request->input('meta_keywords', ''), - 'seo_link' => (string) $request->input('seo_link', ''), - ]; - } - - /** - * @param array $payload - * @param array $integrationContent - * @return array - */ - private function buildRemoteUpdatePayload( - array $payload, - array $integrationContent, - int $integrationId, - int $localProductId - ): array { - $integrationOverride = $integrationContent[$integrationId] ?? null; - $overrideName = is_array($integrationOverride) ? trim((string) ($integrationOverride['name'] ?? '')) : ''; - $overrideShort = is_array($integrationOverride) ? trim((string) ($integrationOverride['short_description'] ?? '')) : ''; - $overrideDesc = is_array($integrationOverride) ? trim((string) ($integrationOverride['description'] ?? '')) : ''; - - $name = trim((string) ($payload['name'] ?? '')); - if ($overrideName !== '') { - $name = $overrideName; - } - if ($name === '') { - $name = 'orderPRO #' . $localProductId; - } - - $shortDescription = trim((string) ($payload['short_description'] ?? '')); - if ($overrideShort !== '') { - $shortDescription = $overrideShort; - } - - $description = trim((string) ($payload['description'] ?? '')); - if ($overrideDesc !== '') { - $description = $overrideDesc; - } - - return [ - 'price_brutto' => round((float) ($payload['price_brutto'] ?? 0), 2), - 'price_brutto_promo' => $this->nullableFloat($payload['price_brutto_promo'] ?? null, 2), - 'price_netto' => $this->nullableFloat($payload['price_netto'] ?? null, 2), - 'price_netto_promo' => $this->nullableFloat($payload['price_netto_promo'] ?? null, 2), - 'vat' => $this->nullableFloat($payload['vat'] ?? null, 2), - 'quantity' => round((float) ($payload['quantity'] ?? 0), 3), - 'status' => ((int) ($payload['status'] ?? 0)) === 1 ? 1 : 0, - 'promoted' => ((int) ($payload['promoted'] ?? 0)) === 1 ? 1 : 0, - 'sku' => $this->nullableText($payload['sku'] ?? null), - 'ean' => $this->nullableText($payload['ean'] ?? null), - 'weight' => $this->nullableFloat($payload['weight'] ?? null, 3), - 'languages' => [ - 'pl' => [ - 'name' => $name, - 'short_description' => $this->nullableText($shortDescription), - 'description' => $this->nullableText($description), - 'meta_title' => $this->nullableText($payload['meta_title'] ?? null), - 'meta_description' => $this->nullableText($payload['meta_description'] ?? null), - 'meta_keywords' => $this->nullableText($payload['meta_keywords'] ?? null), - 'seo_link' => $this->nullableText($payload['seo_link'] ?? null), - ], - ], - ]; - } - - private function nullableText(mixed $value): ?string - { - $text = trim((string) $value); - return $text === '' ? null : $text; - } - - private function nullableFloat(mixed $value, int $precision = 2): ?float - { - $text = trim((string) $value); - if ($text === '' || !is_numeric($text)) { - return null; - } - - return round((float) $text, $precision); - } -} diff --git a/archive/2026-03-02_users-only-reset/src/Modules/Marketplace/MarketplaceRepository.php b/archive/2026-03-02_users-only-reset/src/Modules/Marketplace/MarketplaceRepository.php deleted file mode 100644 index dbedc54..0000000 --- a/archive/2026-03-02_users-only-reset/src/Modules/Marketplace/MarketplaceRepository.php +++ /dev/null @@ -1,279 +0,0 @@ -> - */ - public function listActiveIntegrationsWithCounts(): array - { - $statement = $this->pdo->query( - 'SELECT i.id, i.name, - ( - SELECT COUNT(1) - FROM product_channel_map pcm2 - WHERE pcm2.integration_id = i.id - AND pcm2.link_status = "active" - AND pcm2.external_product_id IS NOT NULL - AND pcm2.external_product_id <> "" - ) AS linked_offers_count - FROM integrations i - WHERE i.type = "shoppro" - AND i.is_active = 1 - ORDER BY i.name ASC, i.id ASC' - ); - - $rows = $statement->fetchAll(); - if (!is_array($rows)) { - return []; - } - - return array_map( - static fn (array $row): array => [ - 'id' => (int) ($row['id'] ?? 0), - 'name' => (string) ($row['name'] ?? ''), - 'linked_offers_count' => (int) ($row['linked_offers_count'] ?? 0), - ], - $rows - ); - } - - /** - * @return array|null - */ - public function findActiveIntegrationById(int $integrationId): ?array - { - $statement = $this->pdo->prepare( - 'SELECT id, name - FROM integrations - WHERE id = :id - AND type = :type - AND is_active = 1 - LIMIT 1' - ); - $statement->execute([ - 'id' => $integrationId, - 'type' => 'shoppro', - ]); - - $row = $statement->fetch(); - if (!is_array($row)) { - return null; - } - - return [ - 'id' => (int) ($row['id'] ?? 0), - 'name' => (string) ($row['name'] ?? ''), - ]; - } - - /** - * @param array $filters - * @return array{items:array>, total:int, page:int, per_page:int} - */ - public function paginateLinkedOffersByIntegration(int $integrationId, array $filters, string $lang = 'pl'): array - { - $page = max(1, (int) ($filters['page'] ?? 1)); - $perPage = max(1, min(100, (int) ($filters['per_page'] ?? 20))); - $offset = ($page - 1) * $perPage; - [$whereSql, $params] = $this->buildOfferFilters($integrationId, $filters); - $sort = $this->resolveOfferSort((string) ($filters['sort'] ?? 'updated_at')); - $sortDir = strtoupper((string) ($filters['sort_dir'] ?? 'DESC')) === 'ASC' ? 'ASC' : 'DESC'; - - $countStmt = $this->pdo->prepare( - 'SELECT COUNT(*) - FROM product_channel_map pcm - INNER JOIN products p ON p.id = pcm.product_id AND p.deleted_at IS NULL - LEFT JOIN product_translations pt ON pt.product_id = p.id AND pt.lang = :lang_count - LEFT JOIN sales_channels sc ON sc.id = pcm.channel_id - LEFT JOIN channel_offers co - ON co.integration_id = pcm.integration_id - AND co.external_product_id = pcm.external_product_id - AND ( - (co.external_variant_id IS NULL AND pcm.external_variant_id IS NULL) - OR co.external_variant_id = pcm.external_variant_id - ) - ' . $whereSql - ); - $countStmt->execute(array_merge(['lang_count' => $lang], $params)); - $total = (int) $countStmt->fetchColumn(); - - $statement = $this->pdo->prepare( - 'SELECT pcm.id, - pcm.product_id, - pcm.external_product_id, - pcm.external_variant_id, - pcm.updated_at, - p.sku AS product_sku, - p.ean AS product_ean, - COALESCE(pt.name, "") AS product_name, - sc.name AS channel_name, - COALESCE(co.name, "") AS offer_name, - co.external_offer_id - FROM product_channel_map pcm - INNER JOIN products p ON p.id = pcm.product_id AND p.deleted_at IS NULL - LEFT JOIN product_translations pt ON pt.product_id = p.id AND pt.lang = :lang - LEFT JOIN sales_channels sc ON sc.id = pcm.channel_id - LEFT JOIN channel_offers co - ON co.integration_id = pcm.integration_id - AND co.external_product_id = pcm.external_product_id - AND ( - (co.external_variant_id IS NULL AND pcm.external_variant_id IS NULL) - OR co.external_variant_id = pcm.external_variant_id - ) - ' . $whereSql . ' - ORDER BY ' . $sort . ' ' . $sortDir . ' - LIMIT :limit OFFSET :offset' - ); - foreach (array_merge(['lang' => $lang], $params) as $key => $value) { - $statement->bindValue(':' . $key, $value); - } - $statement->bindValue(':limit', $perPage, PDO::PARAM_INT); - $statement->bindValue(':offset', $offset, PDO::PARAM_INT); - $statement->execute(); - - $rows = $statement->fetchAll(); - if (!is_array($rows)) { - $rows = []; - } - - return [ - 'items' => array_map( - static fn (array $row): array => [ - 'id' => (int) ($row['id'] ?? 0), - 'product_id' => (int) ($row['product_id'] ?? 0), - 'product_name' => (string) ($row['product_name'] ?? ''), - 'product_sku' => (string) ($row['product_sku'] ?? ''), - 'product_ean' => (string) ($row['product_ean'] ?? ''), - 'channel_name' => (string) ($row['channel_name'] ?? ''), - 'offer_name' => (string) ($row['offer_name'] ?? ''), - 'external_product_id' => (string) ($row['external_product_id'] ?? ''), - 'external_variant_id' => isset($row['external_variant_id']) ? (string) $row['external_variant_id'] : '', - 'external_offer_id' => isset($row['external_offer_id']) ? (string) $row['external_offer_id'] : '', - 'updated_at' => (string) ($row['updated_at'] ?? ''), - ], - $rows - ), - 'total' => $total, - 'page' => $page, - 'per_page' => $perPage, - ]; - } - - /** - * @return array - */ - public function listOfferChannelsByIntegration(int $integrationId): array - { - $stmt = $this->pdo->prepare( - 'SELECT DISTINCT sc.name - FROM product_channel_map pcm - LEFT JOIN sales_channels sc ON sc.id = pcm.channel_id - WHERE pcm.integration_id = :integration_id - AND pcm.link_status = :link_status - AND pcm.external_product_id IS NOT NULL - AND pcm.external_product_id <> "" - AND sc.name IS NOT NULL - AND sc.name <> "" - ORDER BY sc.name ASC' - ); - $stmt->execute([ - 'integration_id' => $integrationId, - 'link_status' => 'active', - ]); - $rows = $stmt->fetchAll(PDO::FETCH_COLUMN); - if (!is_array($rows)) { - return []; - } - - return array_values(array_filter(array_map( - static fn (mixed $v): string => trim((string) $v), - $rows - ), static fn (string $v): bool => $v !== '')); - } - - /** - * @param array $filters - * @return array{0:string,1:array} - */ - private function buildOfferFilters(int $integrationId, array $filters): array - { - $where = [ - 'pcm.integration_id = :integration_id', - 'pcm.link_status = :link_status', - 'pcm.external_product_id IS NOT NULL', - 'pcm.external_product_id <> ""', - ]; - $params = [ - 'integration_id' => $integrationId, - 'link_status' => 'active', - ]; - - $search = trim((string) ($filters['search'] ?? '')); - if ($search !== '') { - $where[] = '(co.name LIKE :search - OR pcm.external_product_id LIKE :search - OR pcm.external_variant_id LIKE :search - OR co.external_offer_id LIKE :search - OR pt.name LIKE :search - OR p.sku LIKE :search - OR p.ean LIKE :search)'; - $params['search'] = '%' . $search . '%'; - } - - $channel = trim((string) ($filters['channel'] ?? '')); - if ($channel !== '') { - $where[] = 'sc.name = :channel'; - $params['channel'] = $channel; - } - - return ['WHERE ' . implode(' AND ', $where), $params]; - } - - private function resolveOfferSort(string $sort): string - { - return match ($sort) { - 'offer_name' => 'co.name', - 'external_product_id' => 'pcm.external_product_id', - 'external_variant_id' => 'pcm.external_variant_id', - 'external_offer_id' => 'co.external_offer_id', - 'channel_name' => 'sc.name', - 'product_name' => 'pt.name', - 'product_sku' => 'p.sku', - 'product_ean' => 'p.ean', - default => 'pcm.updated_at', - }; - } - - public function updateCachedOfferNameForExternalProduct(int $integrationId, string $externalProductId, string $offerName): int - { - $normalizedExternalProductId = trim($externalProductId); - if ($integrationId <= 0 || $normalizedExternalProductId === '') { - return 0; - } - - $statement = $this->pdo->prepare( - 'UPDATE channel_offers - SET name = :name, updated_at = :updated_at - WHERE integration_id = :integration_id - AND external_product_id = :external_product_id' - ); - $statement->execute([ - 'name' => trim($offerName), - 'updated_at' => date('Y-m-d H:i:s'), - 'integration_id' => $integrationId, - 'external_product_id' => $normalizedExternalProductId, - ]); - - return $statement->rowCount(); - } -} diff --git a/archive/2026-03-02_users-only-reset/src/Modules/Orders/OrderImportService.php b/archive/2026-03-02_users-only-reset/src/Modules/Orders/OrderImportService.php deleted file mode 100644 index c992545..0000000 --- a/archive/2026-03-02_users-only-reset/src/Modules/Orders/OrderImportService.php +++ /dev/null @@ -1,629 +0,0 @@ - $payload - * @return array - */ - public function importOne(array $payload = []): array - { - $forcedIntegrationId = max(0, (int) ($payload['integration_id'] ?? 0)); - $enabledIntegrations = array_values(array_filter( - $this->integrations->listByType('shoppro'), - static function (array $integration) use ($forcedIntegrationId): bool { - $id = (int) ($integration['id'] ?? 0); - if ($id <= 0) { - return false; - } - - if ($forcedIntegrationId > 0 && $id !== $forcedIntegrationId) { - return false; - } - - return ($integration['is_active'] ?? false) === true - && ($integration['has_api_key'] ?? false) === true - && ($integration['orders_fetch_enabled'] ?? false) === true; - } - )); - - if ($enabledIntegrations === []) { - return [ - 'ok' => true, - 'message' => 'Brak aktywnych integracji z wlaczonym pobieraniem zamowien.', - 'processed' => 0, - 'checked_integrations' => 0, - 'integration_failures' => 0, - 'errors' => [], - ]; - } - - $integrationFailures = 0; - $errors = []; - - foreach ($enabledIntegrations as $integration) { - $integrationId = (int) ($integration['id'] ?? 0); - if ($integrationId <= 0) { - continue; - } - - try { - $credentials = $this->integrations->findApiCredentials($integrationId); - } catch (Throwable $exception) { - $integrationFailures++; - $this->orders->touchSyncState($integrationId, $exception->getMessage()); - if (count($errors) < 5) { - $errors[] = 'Integracja #' . $integrationId . ': ' . $exception->getMessage(); - } - continue; - } - - if ($credentials === null || trim((string) ($credentials['api_key'] ?? '')) === '') { - $integrationFailures++; - $message = 'Brak poprawnych danych API.'; - $this->orders->touchSyncState($integrationId, $message); - if (count($errors) < 5) { - $errors[] = 'Integracja #' . $integrationId . ': ' . $message; - } - continue; - } - - $state = $this->orders->findSyncState($integrationId); - $fromDate = $this->resolveFromDate( - $this->normalizeDateOnly((string) ($integration['orders_fetch_start_date'] ?? '')), - $this->normalizeDateTime((string) ($state['last_synced_external_updated_at'] ?? '')) - ); - - $fetch = $this->shopProClient->fetchOrders( - (string) ($credentials['base_url'] ?? ''), - (string) ($credentials['api_key'] ?? ''), - (int) ($credentials['timeout_seconds'] ?? 10), - 1, - 100, - $fromDate - ); - - if (($fetch['ok'] ?? false) !== true) { - $integrationFailures++; - $message = trim((string) ($fetch['message'] ?? 'Blad pobierania zamowien.')); - $this->orders->touchSyncState($integrationId, $message); - if (count($errors) < 5) { - $errors[] = 'Integracja #' . $integrationId . ': ' . $message; - } - continue; - } - - $items = is_array($fetch['items'] ?? null) ? $fetch['items'] : []; - $candidates = $this->buildCandidates($items, $state); - if ($candidates === []) { - $this->orders->touchSyncState($integrationId, null); - continue; - } - - $candidate = $candidates[0]; - $sourcePayload = $candidate['payload']; - $detailsResult = $this->shopProClient->fetchOrderById( - (string) ($credentials['base_url'] ?? ''), - (string) ($credentials['api_key'] ?? ''), - (int) ($credentials['timeout_seconds'] ?? 10), - (string) ($candidate['external_order_id'] ?? '') - ); - if (($detailsResult['ok'] ?? false) === true && is_array($detailsResult['order'] ?? null)) { - $sourcePayload = (array) $detailsResult['order']; - } - - $mappedOrder = $this->mapOrder($sourcePayload); - $externalOrderId = (string) ($mappedOrder['external_order_id'] ?? ''); - $externalUpdatedAt = (string) ($mappedOrder['external_updated_at'] ?? ''); - if ($externalOrderId === '' || $externalUpdatedAt === '') { - $integrationFailures++; - $message = 'Nie mozna zidentyfikowac zamowienia (brak id albo daty aktualizacji).'; - $this->orders->touchSyncState($integrationId, $message); - if (count($errors) < 5) { - $errors[] = 'Integracja #' . $integrationId . ': ' . $message; - } - continue; - } - - try { - $this->pdo->beginTransaction(); - - $orderId = $this->orders->upsertOrder( - $integrationId, - $mappedOrder, - $sourcePayload - ); - $this->orders->replaceOrderItems($orderId, $this->extractOrderItems($sourcePayload)); - $this->orders->advanceSyncState($integrationId, $externalUpdatedAt, $externalOrderId); - - $this->pdo->commit(); - } catch (Throwable $exception) { - if ($this->pdo->inTransaction()) { - $this->pdo->rollBack(); - } - - $integrationFailures++; - $this->orders->touchSyncState($integrationId, $exception->getMessage()); - if (count($errors) < 5) { - $errors[] = 'Integracja #' . $integrationId . ': ' . $exception->getMessage(); - } - continue; - } - - return [ - 'ok' => true, - 'message' => 'Zaimportowano 1 zamowienie.', - 'processed' => 1, - 'integration_id' => $integrationId, - 'external_order_id' => $externalOrderId, - 'checked_integrations' => count($enabledIntegrations), - 'integration_failures' => $integrationFailures, - 'errors' => $errors, - ]; - } - - return [ - 'ok' => $integrationFailures === 0, - 'message' => $integrationFailures === 0 - ? 'Brak nowych zamowien do importu.' - : 'Import zamowien zakonczony z bledami integracji.', - 'processed' => 0, - 'checked_integrations' => count($enabledIntegrations), - 'integration_failures' => $integrationFailures, - 'errors' => $errors, - ]; - } - - /** - * @param array $items - * @param array|null $state - * @return array}> - */ - private function buildCandidates(array $items, ?array $state): array - { - $result = []; - - foreach ($items as $item) { - if (!is_array($item)) { - continue; - } - - $externalOrderId = $this->normalizeOrderId($this->readPath($item, [ - 'id', - 'order_id', - 'external_order_id', - ])); - if ($externalOrderId === '') { - continue; - } - - $externalUpdatedAt = $this->normalizeDateTime($this->readPath($item, [ - 'updated_at', - 'date_updated', - 'modified_at', - 'date_modified', - 'created_at', - 'date_created', - ])); - if ($externalUpdatedAt === null) { - continue; - } - - if (!$this->isAfterCursor($externalUpdatedAt, $externalOrderId, $state)) { - continue; - } - - $result[] = [ - 'external_order_id' => $externalOrderId, - 'external_updated_at' => $externalUpdatedAt, - 'payload' => $item, - ]; - } - - usort($result, function (array $a, array $b): int { - $cmp = strcmp((string) ($a['external_updated_at'] ?? ''), (string) ($b['external_updated_at'] ?? '')); - if ($cmp !== 0) { - return $cmp; - } - - return $this->compareOrderId((string) ($a['external_order_id'] ?? ''), (string) ($b['external_order_id'] ?? '')); - }); - - return $result; - } - - /** - * @param array|null $state - */ - private function isAfterCursor(string $externalUpdatedAt, string $externalOrderId, ?array $state): bool - { - if (!is_array($state)) { - return true; - } - - $cursorUpdatedAt = $this->normalizeDateTime($state['last_synced_external_updated_at'] ?? null); - $cursorOrderId = $this->normalizeOrderId($state['last_synced_external_order_id'] ?? null); - - if ($cursorUpdatedAt === null) { - return true; - } - - $dateCmp = strcmp($externalUpdatedAt, $cursorUpdatedAt); - if ($dateCmp > 0) { - return true; - } - - if ($dateCmp < 0) { - return false; - } - - if ($cursorOrderId === '') { - return true; - } - - return $this->compareOrderId($externalOrderId, $cursorOrderId) > 0; - } - - private function compareOrderId(string $left, string $right): int - { - if (ctype_digit($left) && ctype_digit($right)) { - return (int) $left <=> (int) $right; - } - - return strcmp($left, $right); - } - - /** - * @param array $payload - * @return array - */ - private function mapOrder(array $payload): array - { - $externalCreatedAt = $this->normalizeDateTime($this->readPath($payload, [ - 'created_at', - 'date_created', - 'date_add', - 'add_date', - 'order_date', - ])); - $externalUpdatedAt = $this->normalizeDateTime($this->readPath($payload, [ - 'updated_at', - 'date_updated', - 'date_upd', - 'update_date', - 'modified_at', - 'date_modified', - 'created_at', - 'date_created', - 'date_add', - ])); - $buyerName = $this->nullableString($this->readPath($payload, [ - 'buyer.name', - 'customer.name', - 'client.name', - 'user.name', - 'user.full_name', - 'user.fullname', - 'buyer.full_name', - 'customer.full_name', - 'buyer.fullname', - 'customer.fullname', - 'buyer.first_name', - 'buyer.firstname', - 'customer.firstname', - 'client.firstname', - 'user.firstname', - ])); - if ($buyerName === null) { - $buyerName = $this->buildFullName( - $this->nullableString($this->readPath($payload, [ - 'buyer.first_name', - 'customer.first_name', - 'client.first_name', - 'user.firstname', - 'user.first_name', - ])), - $this->nullableString($this->readPath($payload, [ - 'buyer.last_name', - 'buyer.lastname', - 'customer.last_name', - 'customer.lastname', - 'client.last_name', - 'client.lastname', - 'user.lastname', - 'user.last_name', - 'surname', - ])) - ); - } - - return [ - 'external_order_id' => $this->normalizeOrderId($this->readPath($payload, [ - 'id', - 'order_id', - 'external_order_id', - ])), - 'external_order_number' => $this->nullableString($this->readPath($payload, [ - 'order_number', - 'number', - 'full_number', - 'id', - ])), - 'status' => $this->nullableString($this->readPath($payload, [ - 'status', - 'order_status', - ])), - 'currency' => $this->nullableString($this->readPath($payload, [ - 'currency', - 'currency_code', - 'currency_symbol', - 'price_currency', - 'order_currency', - 'payment.currency', - 'summary.currency', - 'totals.currency', - ])), - 'total_gross' => $this->nullableFloat($this->readPath($payload, [ - 'total_gross', - 'total', - 'sum', - 'price_brutto', - 'total_brutto', - 'summary.total', - 'totals.total_gross', - 'totals.gross', - 'summary.total_gross', - ])), - 'total_net' => $this->nullableFloat($this->readPath($payload, [ - 'total_net', - 'price_netto', - 'total_netto', - 'totals.total_net', - 'totals.net', - 'summary.total_net', - ])), - 'buyer_email' => $this->nullableString($this->readPath($payload, [ - 'buyer.email', - 'customer.email', - 'client.email', - 'user.email', - 'user.mail', - 'buyer.mail', - 'customer.mail', - 'email', - ])), - 'buyer_name' => $buyerName, - 'buyer_phone' => $this->nullableString($this->readPath($payload, [ - 'buyer.phone', - 'customer.phone', - 'phone', - ])), - 'payment_method' => $this->nullableString($this->readPath($payload, [ - 'payment.method', - 'payment_method', - ])), - 'payment_status' => $this->nullableString($this->readPath($payload, [ - 'payment.status', - 'payment_status', - ])), - 'delivery_method' => $this->nullableString($this->readPath($payload, [ - 'delivery.method', - 'shipping.method', - 'delivery_method', - ])), - 'delivery_price' => $this->nullableFloat($this->readPath($payload, [ - 'delivery.price', - 'shipping.price', - 'delivery_price', - ])), - 'delivery_tracking_number' => $this->nullableString($this->readPath($payload, [ - 'delivery.tracking_number', - 'shipping.tracking_number', - 'tracking_number', - ])), - 'notes' => $this->nullableString($this->readPath($payload, [ - 'notes', - 'note', - 'comment', - ])), - 'external_created_at' => $externalCreatedAt, - 'external_updated_at' => $externalUpdatedAt ?? $externalCreatedAt, - 'fetched_at' => date('Y-m-d H:i:s'), - ]; - } - - /** - * @param array $payload - * @return array> - */ - private function extractOrderItems(array $payload): array - { - $items = $this->readPath($payload, ['items']); - if (!is_array($items)) { - $items = $this->readPath($payload, ['order_items']); - } - if (!is_array($items)) { - $items = $this->readPath($payload, ['products']); - } - if (!is_array($items)) { - return []; - } - - $result = []; - foreach ($items as $item) { - if (!is_array($item)) { - continue; - } - - $result[] = [ - 'external_item_id' => $this->normalizeOrderId($this->readPath($item, [ - 'id', - 'item_id', - 'external_item_id', - 'product_id', - ])), - 'name' => $this->nullableString($this->readPath($item, [ - 'name', - 'title', - 'product_name', - ])), - 'sku' => $this->nullableString($this->readPath($item, [ - 'sku', - 'product_sku', - ])), - 'ean' => $this->nullableString($this->readPath($item, [ - 'ean', - 'product_ean', - ])), - 'quantity' => $this->nullableFloat($this->readPath($item, [ - 'quantity', - 'qty', - 'count', - ])), - 'price_gross' => $this->nullableFloat($this->readPath($item, [ - 'price_gross', - 'price_brutto', - 'gross_price', - 'price', - ])), - 'price_net' => $this->nullableFloat($this->readPath($item, [ - 'price_net', - 'price_netto', - 'net_price', - ])), - 'vat' => $this->nullableFloat($this->readPath($item, [ - 'vat', - 'tax', - ])), - 'payload' => $item, - ]; - } - - return $result; - } - - private function resolveFromDate(?string $integrationStartDate, ?string $cursorDateTime): ?string - { - $cursorDate = null; - if ($cursorDateTime !== null) { - $cursorDate = substr($cursorDateTime, 0, 10); - } - - if ($integrationStartDate === null) { - return $cursorDate; - } - - if ($cursorDate === null) { - return $integrationStartDate; - } - - return strcmp($integrationStartDate, $cursorDate) > 0 - ? $integrationStartDate - : $cursorDate; - } - - private function readPath(array $data, array $paths): mixed - { - foreach ($paths as $path) { - $current = $data; - $segments = explode('.', $path); - $found = true; - - foreach ($segments as $segment) { - if (!is_array($current) || !array_key_exists($segment, $current)) { - $found = false; - break; - } - - $current = $current[$segment]; - } - - if ($found) { - return $current; - } - } - - return null; - } - - private function normalizeOrderId(mixed $value): string - { - $raw = trim((string) $value); - return $raw; - } - - private function normalizeDateOnly(mixed $value): ?string - { - $text = trim((string) $value); - if ($text === '') { - return null; - } - - if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $text) !== 1) { - return null; - } - - return $text; - } - - private function normalizeDateTime(mixed $value): ?string - { - $text = trim((string) $value); - if ($text === '') { - return null; - } - - $timestamp = strtotime($text); - if ($timestamp === false) { - return null; - } - - return date('Y-m-d H:i:s', $timestamp); - } - - private function nullableString(mixed $value): ?string - { - $text = trim((string) $value); - return $text === '' ? null : $text; - } - - private function nullableFloat(mixed $value): ?float - { - $text = trim((string) $value); - if ($text === '' || !is_numeric($text)) { - return null; - } - - return (float) $text; - } - - private function buildFullName(?string $firstName, ?string $lastName): ?string - { - $parts = array_filter([ - trim((string) $firstName), - trim((string) $lastName), - ], static fn (string $part): bool => $part !== ''); - - if ($parts === []) { - return null; - } - - return implode(' ', $parts); - } -} diff --git a/archive/2026-03-02_users-only-reset/src/Modules/Orders/OrderStatusSyncService.php b/archive/2026-03-02_users-only-reset/src/Modules/Orders/OrderStatusSyncService.php deleted file mode 100644 index c21ba01..0000000 --- a/archive/2026-03-02_users-only-reset/src/Modules/Orders/OrderStatusSyncService.php +++ /dev/null @@ -1,496 +0,0 @@ - $payload - * @return array - */ - public function sync(array $payload = []): array - { - $forcedIntegrationId = max(0, (int) ($payload['integration_id'] ?? 0)); - - $integrations = array_values(array_filter( - $this->integrations->listByType('shoppro'), - static function (array $integration) use ($forcedIntegrationId): bool { - $integrationId = (int) ($integration['id'] ?? 0); - if ($integrationId <= 0) { - return false; - } - - if ($forcedIntegrationId > 0 && $integrationId !== $forcedIntegrationId) { - return false; - } - - return ($integration['is_active'] ?? false) === true - && ($integration['has_api_key'] ?? false) === true; - } - )); - - if ($integrations === []) { - return [ - 'ok' => true, - 'message' => 'Brak aktywnych integracji do synchronizacji statusow.', - 'checked_integrations' => 0, - 'processed_orders' => 0, - 'failed_integrations' => 0, - 'errors' => [], - ]; - } - - $processedOrders = 0; - $failedIntegrations = 0; - $errors = []; - - foreach ($integrations as $integration) { - $integrationId = (int) ($integration['id'] ?? 0); - if ($integrationId <= 0) { - continue; - } - - try { - $credentials = $this->integrations->findApiCredentials($integrationId); - if ($credentials === null || trim((string) ($credentials['api_key'] ?? '')) === '') { - throw new \RuntimeException('Brak poprawnych danych API.'); - } - - $direction = $this->normalizeDirection((string) ($integration['order_status_sync_direction'] ?? '')); - $result = $direction === self::DIRECTION_ORDERPRO_TO_SHOPPRO - ? $this->syncOrderProToShopPro($integrationId, $credentials) - : $this->syncShopProToOrderPro($integrationId, $credentials, $integration); - - $processedOrders += (int) ($result['processed_orders'] ?? 0); - } catch (Throwable $exception) { - $failedIntegrations++; - $this->touchState($integrationId, $this->normalizeDirection((string) ($integration['order_status_sync_direction'] ?? '')), $exception->getMessage()); - - if (count($errors) < 10) { - $errors[] = 'Integracja #' . $integrationId . ': ' . $exception->getMessage(); - } - } - } - - return [ - 'ok' => $failedIntegrations === 0, - 'message' => $failedIntegrations === 0 - ? 'Synchronizacja statusow zamowien zakonczona.' - : 'Synchronizacja statusow zakonczona z bledami.', - 'checked_integrations' => count($integrations), - 'processed_orders' => $processedOrders, - 'failed_integrations' => $failedIntegrations, - 'errors' => $errors, - ]; - } - - /** - * @param array $credentials - * @param array $integration - * @return array{processed_orders:int} - */ - private function syncShopProToOrderPro(int $integrationId, array $credentials, array $integration): array - { - $direction = self::DIRECTION_SHOPPRO_TO_ORDERPRO; - $state = $this->findState($integrationId, $direction); - $cursorAt = $this->normalizeDateTime($state['last_synced_at'] ?? null); - $cursorRef = trim((string) ($state['last_synced_order_ref'] ?? '')); - - $fromDate = $this->resolveFromDate( - $this->normalizeDateOnly((string) ($integration['orders_fetch_start_date'] ?? '')), - $cursorAt - ); - - $response = $this->shopProClient->fetchOrders( - (string) ($credentials['base_url'] ?? ''), - (string) ($credentials['api_key'] ?? ''), - (int) ($credentials['timeout_seconds'] ?? 10), - 1, - 100, - $fromDate - ); - if (($response['ok'] ?? false) !== true) { - $message = trim((string) ($response['message'] ?? 'Blad pobierania statusow z shopPRO.')); - $this->touchState($integrationId, $direction, $message); - throw new \RuntimeException($message); - } - - $items = is_array($response['items'] ?? null) ? $response['items'] : []; - $candidates = $this->buildShopProCandidates($items, $cursorAt, $cursorRef); - - if ($candidates === []) { - $this->touchState($integrationId, $direction, null); - return ['processed_orders' => 0]; - } - - $processed = 0; - foreach ($candidates as $candidate) { - $externalOrderId = (string) ($candidate['external_order_id'] ?? ''); - $externalUpdatedAt = (string) ($candidate['external_updated_at'] ?? ''); - $status = trim((string) ($candidate['status'] ?? '')); - - if ($externalOrderId === '' || $externalUpdatedAt === '') { - continue; - } - - $local = $this->orders->findByIntegrationExternalOrderId($integrationId, $externalOrderId); - if ($local !== null && $status !== '') { - $localStatus = trim((string) ($local['status'] ?? '')); - if (mb_strtolower($localStatus) !== mb_strtolower($status)) { - $this->orders->updateStatus((int) ($local['id'] ?? 0), $status, $externalUpdatedAt); - $processed++; - } - } - - $this->advanceState($integrationId, $direction, $externalUpdatedAt, $externalOrderId); - } - - return ['processed_orders' => $processed]; - } - - /** - * @param array $credentials - * @return array{processed_orders:int} - */ - private function syncOrderProToShopPro(int $integrationId, array $credentials): array - { - $direction = self::DIRECTION_ORDERPRO_TO_SHOPPRO; - $state = $this->findState($integrationId, $direction); - $cursorAt = $this->normalizeDateTime($state['last_synced_at'] ?? null); - $cursorOrderId = max(0, (int) ($state['last_synced_order_ref'] ?? 0)); - - $rows = $this->orders->listForStatusPush($integrationId, $cursorAt, $cursorOrderId, 100); - if ($rows === []) { - $this->touchState($integrationId, $direction, null); - return ['processed_orders' => 0]; - } - - $mapping = $this->mappings->listOrderProToShopProMap($integrationId); - $processed = 0; - - foreach ($rows as $row) { - $orderId = (int) ($row['id'] ?? 0); - $externalOrderId = trim((string) ($row['external_order_id'] ?? '')); - $orderProStatus = $this->normalizeCode((string) ($row['status'] ?? '')); - $updatedAt = (string) ($row['updated_at'] ?? ''); - - if ($orderId <= 0 || $updatedAt === '') { - continue; - } - - if ($externalOrderId === '' || $orderProStatus === '' || !isset($mapping[$orderProStatus])) { - $this->advanceState($integrationId, $direction, $updatedAt, (string) $orderId); - continue; - } - - $shopStatusCode = trim((string) $mapping[$orderProStatus]); - if ($shopStatusCode === '') { - $this->advanceState($integrationId, $direction, $updatedAt, (string) $orderId); - continue; - } - - $response = $this->shopProClient->updateOrderStatus( - (string) ($credentials['base_url'] ?? ''), - (string) ($credentials['api_key'] ?? ''), - (int) ($credentials['timeout_seconds'] ?? 10), - $externalOrderId, - $shopStatusCode - ); - - if (($response['ok'] ?? false) !== true) { - $message = trim((string) ($response['message'] ?? 'Blad aktualizacji statusu zamowienia w shopPRO.')); - $this->touchState($integrationId, $direction, $message); - throw new \RuntimeException($message); - } - - $this->advanceState($integrationId, $direction, $updatedAt, (string) $orderId); - $processed++; - } - - return ['processed_orders' => $processed]; - } - - /** - * @param array $items - * @return array - */ - private function buildShopProCandidates(array $items, ?string $cursorAt, string $cursorRef): array - { - $result = []; - - foreach ($items as $item) { - if (!is_array($item)) { - continue; - } - - $externalOrderId = $this->normalizeOrderId($this->readPath($item, ['id', 'order_id', 'external_order_id'])); - $externalUpdatedAt = $this->normalizeDateTime($this->readPath($item, [ - 'updated_at', - 'date_updated', - 'modified_at', - 'date_modified', - 'created_at', - 'date_created', - ])); - $status = trim((string) $this->readPath($item, ['status', 'order_status'])); - - if ($externalOrderId === '' || $externalUpdatedAt === null || $status === '') { - continue; - } - - if (!$this->isAfterCursor($externalUpdatedAt, $externalOrderId, $cursorAt, $cursorRef)) { - continue; - } - - $result[] = [ - 'external_order_id' => $externalOrderId, - 'external_updated_at' => $externalUpdatedAt, - 'status' => $status, - ]; - } - - usort($result, function (array $a, array $b): int { - $dateCmp = strcmp((string) ($a['external_updated_at'] ?? ''), (string) ($b['external_updated_at'] ?? '')); - if ($dateCmp !== 0) { - return $dateCmp; - } - - return $this->compareOrderRef( - (string) ($a['external_order_id'] ?? ''), - (string) ($b['external_order_id'] ?? '') - ); - }); - - return $result; - } - - private function isAfterCursor(string $itemAt, string $itemRef, ?string $cursorAt, string $cursorRef): bool - { - if ($cursorAt === null) { - return true; - } - - $dateCmp = strcmp($itemAt, $cursorAt); - if ($dateCmp > 0) { - return true; - } - - if ($dateCmp < 0) { - return false; - } - - if ($cursorRef === '') { - return true; - } - - return $this->compareOrderRef($itemRef, $cursorRef) > 0; - } - - private function compareOrderRef(string $left, string $right): int - { - $leftRaw = trim($left); - $rightRaw = trim($right); - - if (ctype_digit($leftRaw) && ctype_digit($rightRaw)) { - return (int) $leftRaw <=> (int) $rightRaw; - } - - return strcmp($leftRaw, $rightRaw); - } - - private function resolveFromDate(?string $integrationStartDate, ?string $cursorDateTime): ?string - { - $cursorDate = null; - if ($cursorDateTime !== null) { - $cursorDate = substr($cursorDateTime, 0, 10); - } - - if ($integrationStartDate === null) { - return $cursorDate; - } - - if ($cursorDate === null) { - return $integrationStartDate; - } - - return strcmp($integrationStartDate, $cursorDate) > 0 - ? $integrationStartDate - : $cursorDate; - } - - private function normalizeDirection(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; - } - - /** - * @return array|null - */ - private function findState(int $integrationId, string $direction): ?array - { - $stmt = $this->pdo->prepare( - 'SELECT integration_id, direction, last_synced_at, last_synced_order_ref, last_run_at, last_error - FROM integration_order_status_sync_state - WHERE integration_id = :integration_id - AND direction = :direction - LIMIT 1' - ); - $stmt->execute([ - 'integration_id' => $integrationId, - 'direction' => $direction, - ]); - $row = $stmt->fetch(); - - return is_array($row) ? $row : null; - } - - private function touchState(int $integrationId, string $direction, ?string $error): void - { - $now = date('Y-m-d H:i:s'); - - $stmt = $this->pdo->prepare( - 'INSERT INTO integration_order_status_sync_state ( - integration_id, direction, last_synced_at, last_synced_order_ref, - last_run_at, last_error, created_at, updated_at - ) VALUES ( - :integration_id, :direction, NULL, NULL, - :last_run_at, :last_error, :created_at, :updated_at - ) - ON DUPLICATE KEY UPDATE - last_run_at = VALUES(last_run_at), - last_error = VALUES(last_error), - updated_at = VALUES(updated_at)' - ); - $stmt->execute([ - 'integration_id' => $integrationId, - 'direction' => $direction, - 'last_run_at' => $now, - 'last_error' => $this->nullableString($error), - 'created_at' => $now, - 'updated_at' => $now, - ]); - } - - private function advanceState(int $integrationId, string $direction, string $cursorAt, string $cursorRef): void - { - $now = date('Y-m-d H:i:s'); - - $stmt = $this->pdo->prepare( - 'INSERT INTO integration_order_status_sync_state ( - integration_id, direction, last_synced_at, last_synced_order_ref, - last_run_at, last_error, created_at, updated_at - ) VALUES ( - :integration_id, :direction, :last_synced_at, :last_synced_order_ref, - :last_run_at, NULL, :created_at, :updated_at - ) - ON DUPLICATE KEY UPDATE - last_synced_at = VALUES(last_synced_at), - last_synced_order_ref = VALUES(last_synced_order_ref), - last_run_at = VALUES(last_run_at), - last_error = NULL, - updated_at = VALUES(updated_at)' - ); - $stmt->execute([ - 'integration_id' => $integrationId, - 'direction' => $direction, - 'last_synced_at' => $cursorAt, - 'last_synced_order_ref' => $cursorRef, - 'last_run_at' => $now, - 'created_at' => $now, - 'updated_at' => $now, - ]); - } - - private function readPath(array $data, array $paths): mixed - { - foreach ($paths as $path) { - $current = $data; - $segments = explode('.', (string) $path); - $found = true; - - foreach ($segments as $segment) { - if (!is_array($current) || !array_key_exists($segment, $current)) { - $found = false; - break; - } - - $current = $current[$segment]; - } - - if ($found) { - return $current; - } - } - - return null; - } - - private function normalizeOrderId(mixed $value): string - { - return trim((string) $value); - } - - private function normalizeDateOnly(mixed $value): ?string - { - $text = trim((string) $value); - if ($text === '') { - return null; - } - - if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $text) !== 1) { - return null; - } - - return $text; - } - - private function normalizeDateTime(mixed $value): ?string - { - $text = trim((string) $value); - if ($text === '') { - return null; - } - - $timestamp = strtotime($text); - if ($timestamp === false) { - return null; - } - - return date('Y-m-d H:i:s', $timestamp); - } - - private function normalizeCode(string $value): string - { - return trim(mb_strtolower($value)); - } - - private function nullableString(mixed $value): ?string - { - $text = trim((string) $value); - return $text === '' ? null : $text; - } -} diff --git a/archive/2026-03-02_users-only-reset/src/Modules/Orders/OrdersController.php b/archive/2026-03-02_users-only-reset/src/Modules/Orders/OrdersController.php deleted file mode 100644 index c37f737..0000000 --- a/archive/2026-03-02_users-only-reset/src/Modules/Orders/OrdersController.php +++ /dev/null @@ -1,963 +0,0 @@ - trim((string) $request->input('search', '')), - 'integration_id' => max(0, (int) $request->input('integration_id', 0)), - 'status' => trim((string) $request->input('status', '')), - 'date_from' => trim((string) $request->input('date_from', '')), - 'date_to' => trim((string) $request->input('date_to', '')), - 'sort' => (string) $request->input('sort', 'external_updated_at'), - 'sort_dir' => (string) $request->input('sort_dir', 'DESC'), - 'page' => max(1, (int) $request->input('page', 1)), - 'per_page' => max(1, min(100, (int) $request->input('per_page', 20))), - ]; - - $result = $this->orders->paginate($filters); - $totalPages = max(1, (int) ceil(((int) $result['total']) / max(1, (int) $result['per_page']))); - $result['items'] = $this->enrichRowsWithBuyerDetailsFromApi((array) ($result['items'] ?? [])); - $statusNameMapByIntegration = $this->buildLiveStatusNameMap((array) ($result['items'] ?? [])); - - $integrationOptions = $this->integrations->listByType('shoppro'); - $statusOptions = $this->buildStatusOptions((array) ($result['items'] ?? []), $statusNameMapByIntegration); - - $html = $this->template->render('orders/index', [ - 'title' => $this->translator->get('orders.title'), - 'activeMenu' => 'orders', - 'user' => $this->auth->user(), - 'csrfToken' => Csrf::token(), - 'marketplaceIntegrations' => $this->marketplaceIntegrations(), - 'tableList' => [ - 'list_key' => 'orders', - 'base_path' => '/orders', - 'query' => $filters, - 'filters' => [ - [ - 'key' => 'search', - 'label' => $this->translator->get('orders.filters.search'), - 'type' => 'text', - 'value' => $filters['search'], - ], - [ - 'key' => 'integration_id', - 'label' => $this->translator->get('orders.filters.integration'), - 'type' => 'select', - 'value' => (string) $filters['integration_id'], - 'options' => $this->integrationFilterOptions($integrationOptions), - ], - [ - 'key' => 'status', - 'label' => $this->translator->get('orders.filters.status'), - 'type' => 'select', - 'value' => $filters['status'], - 'options' => $statusOptions, - ], - [ - 'key' => 'date_from', - 'label' => $this->translator->get('orders.filters.date_from'), - 'type' => 'date', - 'value' => $filters['date_from'], - ], - [ - 'key' => 'date_to', - 'label' => $this->translator->get('orders.filters.date_to'), - 'type' => 'date', - 'value' => $filters['date_to'], - ], - ], - 'columns' => [ - ['key' => 'id', 'label' => 'ID', 'sortable' => true, 'sort_key' => 'id'], - ['key' => 'internal_order_number', 'label' => $this->translator->get('orders.fields.internal_order_number'), 'sortable' => true, 'sort_key' => 'internal_order_number'], - ['key' => 'external_order_number', 'label' => $this->translator->get('orders.fields.external_order_number'), 'sortable' => true, 'sort_key' => 'external_order_number'], - ['key' => 'status_badge', 'label' => $this->translator->get('orders.fields.status'), 'sortable' => true, 'sort_key' => 'status', 'raw' => true], - ['key' => 'buyer_display', 'label' => $this->translator->get('orders.fields.buyer'), 'raw' => true], - ['key' => 'total_gross', 'label' => $this->translator->get('orders.fields.total_gross'), 'sortable' => true, 'sort_key' => 'total_gross'], - ['key' => 'currency', 'label' => $this->translator->get('orders.fields.currency'), 'sortable' => true, 'sort_key' => 'currency'], - ['key' => 'external_created_at', 'label' => $this->translator->get('orders.fields.external_created_at'), 'sortable' => true, 'sort_key' => 'external_created_at'], - ['key' => 'external_updated_at', 'label' => $this->translator->get('orders.fields.external_updated_at'), 'sortable' => true, 'sort_key' => 'external_updated_at'], - ['key' => 'fetched_at', 'label' => $this->translator->get('orders.fields.fetched_at'), 'sortable' => true, 'sort_key' => 'fetched_at'], - ], - 'rows' => $this->tableRows((array) ($result['items'] ?? []), $statusNameMapByIntegration), - 'pagination' => [ - 'page' => (int) ($result['page'] ?? 1), - 'total_pages' => $totalPages, - 'total' => (int) ($result['total'] ?? 0), - 'per_page' => (int) ($result['per_page'] ?? 20), - ], - 'per_page_options' => [10, 20, 50, 100], - 'empty_message' => $this->translator->get('orders.empty'), - 'show_actions' => false, - ], - 'errorMessage' => (string) Flash::get('orders_error', ''), - 'successMessage' => (string) Flash::get('orders_success', ''), - ], 'layouts/app'); - - return Response::html($html); - } - - /** - * @param array> $rows - * @param array> $statusNameMapByIntegration - * @return array - */ - private function buildStatusOptions(array $rows, array $statusNameMapByIntegration): array - { - $options = ['' => $this->translator->get('orders.filters.any')]; - - foreach ($rows as $row) { - $status = trim((string) ($row['status'] ?? '')); - if ($status === '' || isset($options[$status])) { - continue; - } - - $options[$status] = $this->resolveStatusLabel( - max(0, (int) ($row['integration_id'] ?? 0)), - $status, - trim((string) ($row['status_text'] ?? '')), - $statusNameMapByIntegration - ); - } - - return $options; - } - - /** - * @param array> $integrations - * @return array - */ - private function integrationFilterOptions(array $integrations): array - { - $options = ['0' => $this->translator->get('orders.filters.any')]; - - foreach ($integrations as $integration) { - $id = max(0, (int) ($integration['id'] ?? 0)); - if ($id <= 0) { - continue; - } - - $options[(string) $id] = (string) ($integration['name'] ?? ('#' . $id)); - } - - return $options; - } - - /** - * @param array> $items - * @param array> $statusNameMapByIntegration - * @return array> - */ - private function tableRows(array $items, array $statusNameMapByIntegration): array - { - return array_map(function (array $row) use ($statusNameMapByIntegration): array { - $integrationId = max(0, (int) ($row['integration_id'] ?? 0)); - $statusCode = trim((string) ($row['status'] ?? '')); - $statusText = trim((string) ($row['status_text'] ?? '')); - $payload = $this->decodePayload((string) ($row['payload_json'] ?? '')); - - $buyerName = trim((string) ($row['buyer_name'] ?? '')); - if ($buyerName === '') { - $buyerName = $this->payloadString($payload, [ - 'buyer.name', - 'customer.name', - 'client.name', - 'user.name', - 'user.full_name', - ]); - if ($buyerName === '') { - $buyerName = trim($this->payloadString($payload, [ - 'buyer.first_name', - 'customer.first_name', - 'client.first_name', - 'user.firstname', - 'user.first_name', - ]) . ' ' . $this->payloadString($payload, [ - 'buyer.last_name', - 'customer.last_name', - 'client.last_name', - 'user.lastname', - 'user.last_name', - ])); - } - if ($buyerName === '') { - $buyerName = $this->payloadStringFromBranches( - $payload, - ['buyer', 'customer', 'client', 'user', 'billing', 'invoice', 'recipient', 'address'], - ['name', 'full_name', 'fullname', 'client_name', 'company_name'] - ); - if ($buyerName === '') { - $buyerName = $this->heuristicBuyerName($payload); - } - } - } - $buyerLastName = $this->payloadString($payload, [ - 'buyer.last_name', - 'customer.last_name', - 'client.last_name', - 'user.lastname', - 'user.last_name', - 'billing.last_name', - 'invoice.last_name', - ]); - if ($buyerLastName === '') { - $buyerLastName = $this->payloadStringFromBranches( - $payload, - ['buyer', 'customer', 'client', 'user', 'billing', 'invoice', 'recipient', 'address'], - ['last_name', 'lastname', 'surname', 'nazwisko'] - ); - } - $buyerName = $this->mergeBuyerNameAndLastName($buyerName, $buyerLastName); - - $buyerEmail = trim((string) ($row['buyer_email'] ?? '')); - if ($buyerEmail === '') { - $buyerEmail = $this->payloadString($payload, [ - 'buyer.email', - 'customer.email', - 'client.email', - 'user.email', - 'user.mail', - 'email', - ]); - if ($buyerEmail === '') { - $buyerEmail = $this->payloadStringFromBranches( - $payload, - ['buyer', 'customer', 'client', 'user', 'billing', 'invoice', 'recipient', 'address'], - ['email', 'mail', 'e_mail'] - ); - if ($buyerEmail === '') { - $buyerEmail = $this->heuristicEmail($payload); - } - } - } - - $currency = trim((string) ($row['currency'] ?? '')); - if ($currency === '') { - $currency = $this->payloadString($payload, [ - 'currency', - 'currency_code', - 'price_currency', - 'order_currency', - 'payment.currency', - 'summary.currency', - ]); - if ($currency === '') { - $currency = $this->payloadStringFromBranches( - $payload, - ['summary', 'totals', 'payment', 'prices'], - ['currency', 'currency_code', 'price_currency'] - ); - if ($currency === '') { - $currency = $this->heuristicCurrency($payload); - } - } - } - - $totalGross = $row['total_gross']; - if ($totalGross === null) { - $totalGross = $this->payloadFloat($payload, [ - 'total_gross', - 'total', - 'sum', - 'price_brutto', - 'total_brutto', - 'summary.total', - 'summary.total_gross', - ]); - if ($totalGross === null) { - $totalGross = $this->payloadFloatFromBranches( - $payload, - ['summary', 'totals', 'payment', 'prices'], - ['total_gross', 'total', 'sum', 'price_brutto', 'gross', 'amount'] - ); - if ($totalGross === null) { - $totalGross = $this->heuristicTotalGross($payload); - } - } - } - - $externalCreatedAt = trim((string) ($row['external_created_at'] ?? '')); - if ($externalCreatedAt === '') { - $externalCreatedAt = $this->normalizeDateText($this->payloadString($payload, [ - 'created_at', - 'date_created', - 'date_add', - 'add_date', - 'order_date', - ])); - if ($externalCreatedAt === '') { - $externalCreatedAt = $this->normalizeDateText($this->payloadStringFromBranches( - $payload, - ['dates', 'order', 'summary'], - ['created_at', 'date_created', 'date_add', 'add_date', 'order_date'] - )); - if ($externalCreatedAt === '') { - $externalCreatedAt = $this->heuristicCreatedAt($payload); - } - } - } - $buyer = $buyerName; - if ($buyerEmail !== '') { - $buyer = $buyer === '' ? $buyerEmail : ($buyer . ' (' . $buyerEmail . ')'); - } - - return [ - 'id' => (int) ($row['id'] ?? 0), - 'internal_order_number' => (string) ($row['internal_order_number'] ?? ''), - 'integration_name' => (string) ($row['integration_name'] ?? ''), - 'external_order_id' => (string) ($row['external_order_id'] ?? ''), - 'external_order_number' => (string) ($row['external_order_number'] ?? ''), - 'status' => $this->resolveStatusLabel($integrationId, $statusCode, $statusText, $statusNameMapByIntegration), - 'status_badge' => $this->statusBadgeHtml( - $this->resolveStatusLabel($integrationId, $statusCode, $statusText, $statusNameMapByIntegration) - ), - 'buyer' => $buyer, - 'buyer_display' => $this->buyerHtml($buyerName, $buyerEmail), - 'total_gross' => $totalGross === null - ? '' - : number_format((float) $totalGross, 2, '.', ''), - 'currency' => $currency, - 'external_created_at' => $externalCreatedAt, - 'external_updated_at' => (string) ($row['external_updated_at'] ?? ''), - 'fetched_at' => (string) ($row['fetched_at'] ?? ''), - ]; - }, $items); - } - - /** - * @param array> $items - * @return array> - */ - private function buildLiveStatusNameMap(array $items): array - { - $integrationIds = []; - foreach ($items as $row) { - $integrationId = max(0, (int) ($row['integration_id'] ?? 0)); - if ($integrationId > 0) { - $integrationIds[$integrationId] = $integrationId; - } - } - - if ($integrationIds === []) { - return []; - } - - $result = []; - foreach (array_values($integrationIds) as $integrationId) { - try { - $credentials = $this->integrations->findApiCredentials($integrationId); - if ($credentials === null || trim((string) ($credentials['api_key'] ?? '')) === '') { - continue; - } - - $statusesResult = $this->shopProClient->fetchOrderStatuses( - (string) ($credentials['base_url'] ?? ''), - (string) ($credentials['api_key'] ?? ''), - (int) ($credentials['timeout_seconds'] ?? 10) - ); - if (($statusesResult['ok'] ?? false) !== true) { - continue; - } - - $map = []; - $statuses = is_array($statusesResult['statuses'] ?? null) ? $statusesResult['statuses'] : []; - foreach ($statuses as $status) { - if (!is_array($status)) { - continue; - } - - $code = trim((string) ($status['code'] ?? '')); - if ($code === '') { - continue; - } - - $name = trim((string) ($status['name'] ?? $code)); - if ($name === '') { - $name = $code; - } - - $map[$this->normalizeStatusCode($code)] = $name; - } - - if ($map !== []) { - $result[$integrationId] = $map; - } - } catch (Throwable) { - continue; - } - } - - return $result; - } - - /** - * @param array> $statusNameMapByIntegration - */ - private function resolveStatusLabel( - int $integrationId, - string $statusCode, - string $statusText, - array $statusNameMapByIntegration - ): string { - $rawCode = trim($statusCode); - $rawText = trim($statusText); - - if ($rawText !== '' && !$this->isNumericStatusCode($rawText)) { - return $rawText; - } - - if ($integrationId > 0 && $rawCode !== '' && isset($statusNameMapByIntegration[$integrationId])) { - $normalizedCode = $this->normalizeStatusCode($rawCode); - $label = trim((string) ($statusNameMapByIntegration[$integrationId][$normalizedCode] ?? '')); - if ($label !== '') { - return $label; - } - } - - if ($rawText !== '') { - return $rawText; - } - - return $rawCode; - } - - private function normalizeStatusCode(string $value): string - { - return trim(mb_strtolower($value)); - } - - private function isNumericStatusCode(string $value): bool - { - return preg_match('/^\d+$/', trim($value)) === 1; - } - - /** - * @return array - */ - private function decodePayload(string $payloadJson): array - { - $raw = trim($payloadJson); - if ($raw === '') { - return []; - } - - $decoded = json_decode($raw, true); - if (is_array($decoded)) { - return $decoded; - } - - if (is_string($decoded)) { - $second = json_decode($decoded, true); - if (is_array($second)) { - return $second; - } - } - - return []; - } - - /** - * @param array $payload - * @param array $paths - */ - private function payloadString(array $payload, array $paths): string - { - foreach ($paths as $path) { - $value = $this->payloadPath($payload, $path); - $text = trim((string) $value); - if ($text !== '') { - return $text; - } - } - - return ''; - } - - /** - * @param array $payload - * @param array $branches - * @param array $keys - */ - private function payloadStringFromBranches(array $payload, array $branches, array $keys): string - { - foreach ($branches as $branch) { - $node = $this->payloadPath($payload, $branch); - if (!is_array($node)) { - continue; - } - - $found = $this->findFirstByKeysRecursive($node, $keys); - if ($found !== null) { - $text = trim((string) $found); - if ($text !== '') { - return $text; - } - } - } - - $found = $this->findFirstByKeysRecursive($payload, $keys); - if ($found !== null) { - $text = trim((string) $found); - if ($text !== '') { - return $text; - } - } - - return ''; - } - - /** - * @param array $payload - * @param array $paths - */ - private function payloadFloat(array $payload, array $paths): ?float - { - foreach ($paths as $path) { - $value = $this->payloadPath($payload, $path); - $text = trim((string) $value); - if ($text === '' || !is_numeric($text)) { - continue; - } - - return (float) $text; - } - - return null; - } - - /** - * @param array $payload - * @param array $branches - * @param array $keys - */ - private function payloadFloatFromBranches(array $payload, array $branches, array $keys): ?float - { - $value = $this->payloadStringFromBranches($payload, $branches, $keys); - if ($value === '' || !is_numeric($value)) { - return null; - } - - return (float) $value; - } - - /** - * @param array $payload - */ - private function payloadPath(array $payload, string $path): mixed - { - $current = $payload; - foreach (explode('.', $path) as $segment) { - if (!is_array($current) || !array_key_exists($segment, $current)) { - return null; - } - $current = $current[$segment]; - } - - return $current; - } - - /** - * @param array $payload - * @param array $keys - */ - private function findFirstByKeysRecursive(array $payload, array $keys): mixed - { - $normalizedKeys = array_map( - static fn (string $key): string => trim(mb_strtolower($key)), - $keys - ); - - foreach ($payload as $key => $value) { - $normalizedKey = trim(mb_strtolower((string) $key)); - if (in_array($normalizedKey, $normalizedKeys, true) && !is_array($value)) { - return $value; - } - } - - foreach ($payload as $value) { - if (!is_array($value)) { - continue; - } - - $found = $this->findFirstByKeysRecursive($value, $keys); - if ($found !== null) { - return $found; - } - } - - return null; - } - - private function normalizeDateText(string $value): string - { - $text = trim($value); - if ($text === '') { - return ''; - } - - if (ctype_digit($text)) { - $timestamp = (int) $text; - if ($timestamp > 0) { - return date('Y-m-d H:i:s', $timestamp); - } - } - - $timestamp = strtotime($text); - if ($timestamp === false) { - return $text; - } - - return date('Y-m-d H:i:s', $timestamp); - } - - /** - * @param array $payload - */ - private function heuristicEmail(array $payload): string - { - foreach ($this->flattenPayload($payload) as $entry) { - $value = trim((string) ($entry['value'] ?? '')); - if ($value !== '' && filter_var($value, FILTER_VALIDATE_EMAIL) !== false) { - return $value; - } - } - - return ''; - } - - /** - * @param array $payload - */ - private function heuristicCurrency(array $payload): string - { - foreach ($this->flattenPayload($payload) as $entry) { - $key = trim(mb_strtolower((string) ($entry['key'] ?? ''))); - $value = trim((string) ($entry['value'] ?? '')); - if ($value === '') { - continue; - } - - if (str_contains($key, 'currency') || str_contains($key, 'walut')) { - return mb_strtoupper($value); - } - } - - return ''; - } - - /** - * @param array $payload - */ - private function heuristicTotalGross(array $payload): ?float - { - foreach ($this->flattenPayload($payload) as $entry) { - $key = trim(mb_strtolower((string) ($entry['key'] ?? ''))); - $value = trim((string) ($entry['value'] ?? '')); - if ($value === '' || !is_numeric($value)) { - continue; - } - - if ( - str_contains($key, 'total') || - str_contains($key, 'sum') || - str_contains($key, 'gross') || - str_contains($key, 'brutto') || - str_contains($key, 'amount') - ) { - return (float) $value; - } - } - - return null; - } - - /** - * @param array $payload - */ - private function heuristicCreatedAt(array $payload): string - { - foreach ($this->flattenPayload($payload) as $entry) { - $key = trim(mb_strtolower((string) ($entry['key'] ?? ''))); - if ( - !str_contains($key, 'created') && - !str_contains($key, 'date_add') && - !str_contains($key, 'order_date') && - !str_contains($key, 'add_date') - ) { - continue; - } - - $normalized = $this->normalizeDateText((string) ($entry['value'] ?? '')); - if ($normalized !== '') { - return $normalized; - } - } - - return ''; - } - - /** - * @param array $payload - */ - private function heuristicBuyerName(array $payload): string - { - foreach ($this->flattenPayload($payload) as $entry) { - $key = trim(mb_strtolower((string) ($entry['key'] ?? ''))); - $value = trim((string) ($entry['value'] ?? '')); - if ($value === '' || mb_strlen($value) < 3 || str_contains($value, '@')) { - continue; - } - - if ( - str_contains($key, 'name') || - str_contains($key, 'fullname') || - str_contains($key, 'full_name') || - str_contains($key, 'imie') || - str_contains($key, 'nazw') - ) { - return $value; - } - } - - return ''; - } - - /** - * @param array $payload - * @return array - */ - private function flattenPayload(array $payload, string $prefix = ''): array - { - $result = []; - - foreach ($payload as $key => $value) { - $currentKey = $prefix === '' ? (string) $key : ($prefix . '.' . (string) $key); - if (is_array($value)) { - $result = array_merge($result, $this->flattenPayload($value, $currentKey)); - continue; - } - - $result[] = [ - 'key' => $currentKey, - 'value' => (string) $value, - ]; - } - - return $result; - } - - private function statusBadgeHtml(string $statusLabel): string - { - $label = trim($statusLabel); - if ($label === '') { - return '-'; - } - - $class = 'is-default'; - $normalized = mb_strtolower($label); - if (str_contains($normalized, 'anul') || str_contains($normalized, 'cancel') || str_contains($normalized, 'zwrot')) { - $class = 'is-danger'; - } elseif (str_contains($normalized, 'wys') || str_contains($normalized, 'ship') || str_contains($normalized, 'dostar')) { - $class = 'is-success'; - } elseif (str_contains($normalized, 'now') || str_contains($normalized, 'new')) { - $class = 'is-info'; - } elseif (str_contains($normalized, 'realiz') || str_contains($normalized, 'progress')) { - $class = 'is-warn'; - } - - return '' . htmlspecialchars($label, ENT_QUOTES, 'UTF-8') . ''; - } - - private function buyerHtml(string $buyerName, string $buyerEmail): string - { - $name = trim($buyerName); - $email = trim($buyerEmail); - if ($name === '' && $email === '') { - return '-'; - } - - $html = '
'; - if ($name !== '') { - $html .= '
' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . '
'; - } - if ($email !== '') { - $html .= ''; - } - $html .= '
'; - - return $html; - } - - /** - * @param array> $items - * @return array> - */ - private function enrichRowsWithBuyerDetailsFromApi(array $items): array - { - if ($items === []) { - return $items; - } - - $credentialsCache = []; - $maxLookups = 10; - $lookups = 0; - - foreach ($items as $index => $row) { - if (!is_array($row)) { - continue; - } - - if ($lookups >= $maxLookups) { - break; - } - - $integrationId = max(0, (int) ($row['integration_id'] ?? 0)); - $externalOrderId = trim((string) ($row['external_order_id'] ?? '')); - $buyerName = trim((string) ($row['buyer_name'] ?? '')); - if ($integrationId <= 0 || $externalOrderId === '' || $buyerName === '' && trim((string) ($row['buyer_email'] ?? '')) !== '') { - continue; - } - - if (!$this->looksLikeMissingLastName($buyerName)) { - continue; - } - - if (!array_key_exists($integrationId, $credentialsCache)) { - try { - $credentialsCache[$integrationId] = $this->integrations->findApiCredentials($integrationId); - } catch (Throwable) { - $credentialsCache[$integrationId] = null; - } - } - $credentials = $credentialsCache[$integrationId]; - if (!is_array($credentials) || trim((string) ($credentials['api_key'] ?? '')) === '') { - continue; - } - - try { - $details = $this->shopProClient->fetchOrderById( - (string) ($credentials['base_url'] ?? ''), - (string) ($credentials['api_key'] ?? ''), - (int) ($credentials['timeout_seconds'] ?? 10), - $externalOrderId - ); - $lookups++; - } catch (Throwable) { - continue; - } - - if (($details['ok'] ?? false) !== true || !is_array($details['order'] ?? null)) { - continue; - } - - $orderPayload = $details['order']; - $firstName = trim((string) ($this->payloadPath($orderPayload, 'buyer.first_name') - ?? $this->payloadPath($orderPayload, 'buyer.firstname') - ?? $this->payloadPath($orderPayload, 'customer.first_name') - ?? $this->payloadPath($orderPayload, 'customer.firstname') - ?? $this->payloadPath($orderPayload, 'client.first_name') - ?? $this->payloadPath($orderPayload, 'client.firstname') - ?? $this->payloadPath($orderPayload, 'user.first_name') - ?? $this->payloadPath($orderPayload, 'user.firstname') - ?? '')); - $lastName = trim((string) ($this->payloadPath($orderPayload, 'buyer.last_name') - ?? $this->payloadPath($orderPayload, 'buyer.lastname') - ?? $this->payloadPath($orderPayload, 'customer.last_name') - ?? $this->payloadPath($orderPayload, 'customer.lastname') - ?? $this->payloadPath($orderPayload, 'client.last_name') - ?? $this->payloadPath($orderPayload, 'client.lastname') - ?? $this->payloadPath($orderPayload, 'user.last_name') - ?? $this->payloadPath($orderPayload, 'user.lastname') - ?? $this->payloadPath($orderPayload, 'surname') - ?? '')); - - $composed = $this->mergeBuyerNameAndLastName( - $firstName !== '' ? $firstName : $buyerName, - $lastName - ); - if (trim($composed) !== '') { - $row['buyer_name'] = $composed; - } - - $email = trim((string) ($this->payloadPath($orderPayload, 'buyer.email') - ?? $this->payloadPath($orderPayload, 'customer.email') - ?? $this->payloadPath($orderPayload, 'client.email') - ?? $this->payloadPath($orderPayload, 'user.email') - ?? $this->payloadPath($orderPayload, 'user.mail') - ?? '')); - if ($email !== '') { - $row['buyer_email'] = $email; - } - - $items[$index] = $row; - } - - return $items; - } - - private function looksLikeMissingLastName(string $buyerName): bool - { - $name = trim($buyerName); - if ($name === '') { - return true; - } - - $parts = preg_split('/\s+/u', $name) ?: []; - return count(array_filter($parts, static fn (string $part): bool => $part !== '')) < 2; - } - - private function mergeBuyerNameAndLastName(string $buyerName, string $buyerLastName): string - { - $name = trim($buyerName); - $lastName = trim($buyerLastName); - - if ($name === '') { - return $lastName; - } - if ($lastName === '') { - return $name; - } - - if (str_contains(mb_strtolower($name), mb_strtolower($lastName))) { - return $name; - } - - return trim($name . ' ' . $lastName); - } - - /** - * @return array> - */ - private function marketplaceIntegrations(): array - { - return array_values(array_filter( - $this->integrations->listByType('shoppro'), - static fn (array $row): bool => (bool) ($row['is_active'] ?? false) - )); - } -} diff --git a/archive/2026-03-02_users-only-reset/src/Modules/Orders/OrdersRepository.php b/archive/2026-03-02_users-only-reset/src/Modules/Orders/OrdersRepository.php deleted file mode 100644 index f3ba284..0000000 --- a/archive/2026-03-02_users-only-reset/src/Modules/Orders/OrdersRepository.php +++ /dev/null @@ -1,656 +0,0 @@ - $filters - * @return array{items:array>, total:int, page:int, per_page:int} - */ - public function paginate(array $filters): array - { - $page = max(1, (int) ($filters['page'] ?? 1)); - $perPage = max(1, min(100, (int) ($filters['per_page'] ?? 20))); - $offset = ($page - 1) * $perPage; - - [$whereSql, $params] = $this->buildFilters($filters); - $sort = $this->resolveSort((string) ($filters['sort'] ?? 'external_updated_at')); - $sortDir = strtoupper((string) ($filters['sort_dir'] ?? 'DESC')) === 'ASC' ? 'ASC' : 'DESC'; - - $countStmt = $this->pdo->prepare( - 'SELECT COUNT(*) - FROM orders o - INNER JOIN integrations i ON i.id = o.integration_id - ' . $whereSql - ); - $countStmt->execute($params); - $total = (int) $countStmt->fetchColumn(); - - $listStmt = $this->pdo->prepare( - 'SELECT o.id, o.internal_order_number, o.integration_id, o.external_order_id, o.external_order_number, - o.status, o.currency, o.total_gross, o.buyer_email, o.buyer_name, - o.external_created_at, o.external_updated_at, o.fetched_at, o.payload_json, - ( - SELECT m.orderpro_status_code - FROM order_status_mappings m - WHERE m.integration_id = o.integration_id - AND LOWER(TRIM(m.shoppro_status_code)) = LOWER(TRIM(COALESCE(o.status, ""))) - LIMIT 1 - ) AS orderpro_status_code, - ( - SELECT m.shoppro_status_name - FROM order_status_mappings m - WHERE m.integration_id = o.integration_id - AND LOWER(TRIM(m.shoppro_status_code)) = LOWER(TRIM(COALESCE(o.status, ""))) - LIMIT 1 - ) AS shoppro_status_name, - ( - SELECT m.shoppro_status_name - FROM order_status_mappings m - WHERE m.integration_id = o.integration_id - AND LOWER(TRIM(m.orderpro_status_code)) = LOWER(TRIM(COALESCE(o.status, ""))) - LIMIT 1 - ) AS shoppro_status_name_by_orderpro, - i.name AS integration_name - FROM orders o - INNER JOIN integrations i ON i.id = o.integration_id - ' . $whereSql . ' - ORDER BY ' . $sort . ' ' . $sortDir . ' - LIMIT :limit OFFSET :offset' - ); - - foreach ($params as $key => $value) { - $listStmt->bindValue(':' . $key, $value); - } - $listStmt->bindValue(':limit', $perPage, PDO::PARAM_INT); - $listStmt->bindValue(':offset', $offset, PDO::PARAM_INT); - $listStmt->execute(); - - $rows = $listStmt->fetchAll(); - if (!is_array($rows)) { - $rows = []; - } - - return [ - 'items' => array_map([$this, 'mapListRow'], $rows), - 'total' => $total, - 'page' => $page, - 'per_page' => $perPage, - ]; - } - - /** - * @param array $order - * @param array|null $payload - */ - public function upsertOrder(int $integrationId, array $order, ?array $payload = null): int - { - $stmt = $this->pdo->prepare( - 'INSERT INTO orders ( - integration_id, external_order_id, external_order_number, status, currency, - total_gross, total_net, - buyer_email, buyer_name, buyer_phone, - payment_method, payment_status, - delivery_method, delivery_price, delivery_tracking_number, - notes, external_created_at, external_updated_at, - payload_json, fetched_at, created_at, updated_at - ) VALUES ( - :integration_id, :external_order_id, :external_order_number, :status, :currency, - :total_gross, :total_net, - :buyer_email, :buyer_name, :buyer_phone, - :payment_method, :payment_status, - :delivery_method, :delivery_price, :delivery_tracking_number, - :notes, :external_created_at, :external_updated_at, - :payload_json, :fetched_at, :created_at, :updated_at - ) - ON DUPLICATE KEY UPDATE - id = LAST_INSERT_ID(id), - external_order_number = VALUES(external_order_number), - status = VALUES(status), - currency = VALUES(currency), - total_gross = VALUES(total_gross), - total_net = VALUES(total_net), - buyer_email = VALUES(buyer_email), - buyer_name = VALUES(buyer_name), - buyer_phone = VALUES(buyer_phone), - payment_method = VALUES(payment_method), - payment_status = VALUES(payment_status), - delivery_method = VALUES(delivery_method), - delivery_price = VALUES(delivery_price), - delivery_tracking_number = VALUES(delivery_tracking_number), - notes = VALUES(notes), - external_created_at = VALUES(external_created_at), - external_updated_at = VALUES(external_updated_at), - payload_json = VALUES(payload_json), - fetched_at = VALUES(fetched_at), - updated_at = VALUES(updated_at)' - ); - - $now = date('Y-m-d H:i:s'); - $stmt->execute([ - 'integration_id' => $integrationId, - 'external_order_id' => (string) ($order['external_order_id'] ?? ''), - 'external_order_number' => $this->nullableString($order['external_order_number'] ?? null), - 'status' => $this->nullableString($order['status'] ?? null), - 'currency' => $this->nullableString($order['currency'] ?? null), - 'total_gross' => $this->nullableNumeric($order['total_gross'] ?? null, 2), - 'total_net' => $this->nullableNumeric($order['total_net'] ?? null, 2), - 'buyer_email' => $this->nullableString($order['buyer_email'] ?? null), - 'buyer_name' => $this->nullableString($order['buyer_name'] ?? null), - 'buyer_phone' => $this->nullableString($order['buyer_phone'] ?? null), - 'payment_method' => $this->nullableString($order['payment_method'] ?? null), - 'payment_status' => $this->nullableString($order['payment_status'] ?? null), - 'delivery_method' => $this->nullableString($order['delivery_method'] ?? null), - 'delivery_price' => $this->nullableNumeric($order['delivery_price'] ?? null, 2), - 'delivery_tracking_number' => $this->nullableString($order['delivery_tracking_number'] ?? null), - 'notes' => $this->nullableString($order['notes'] ?? null), - 'external_created_at' => $this->nullableString($order['external_created_at'] ?? null), - 'external_updated_at' => $this->nullableString($order['external_updated_at'] ?? null), - 'payload_json' => $this->encodeJson($payload), - 'fetched_at' => $this->nullableString($order['fetched_at'] ?? null) ?? $now, - 'created_at' => $now, - 'updated_at' => $now, - ]); - - $orderId = (int) $this->pdo->lastInsertId(); - if ($orderId > 0) { - $this->ensureInternalOrderNumber($orderId); - } - - return $orderId; - } - - /** - * @param array> $items - */ - public function replaceOrderItems(int $orderId, array $items): void - { - $deleteStmt = $this->pdo->prepare('DELETE FROM order_items WHERE order_id = :order_id'); - $deleteStmt->execute(['order_id' => $orderId]); - - if ($items === []) { - return; - } - - $insertStmt = $this->pdo->prepare( - 'INSERT INTO order_items ( - order_id, external_item_id, name, sku, ean, - quantity, price_gross, price_net, vat, - payload_json, created_at, updated_at - ) VALUES ( - :order_id, :external_item_id, :name, :sku, :ean, - :quantity, :price_gross, :price_net, :vat, - :payload_json, :created_at, :updated_at - )' - ); - - $now = date('Y-m-d H:i:s'); - foreach ($items as $item) { - $name = trim((string) ($item['name'] ?? '')); - if ($name === '') { - $name = 'Pozycja'; - } - - $insertStmt->execute([ - 'order_id' => $orderId, - 'external_item_id' => $this->nullableString($item['external_item_id'] ?? null), - 'name' => $name, - 'sku' => $this->nullableString($item['sku'] ?? null), - 'ean' => $this->nullableString($item['ean'] ?? null), - 'quantity' => $this->nullableNumeric($item['quantity'] ?? null, 3) ?? 0, - 'price_gross' => $this->nullableNumeric($item['price_gross'] ?? null, 2), - 'price_net' => $this->nullableNumeric($item['price_net'] ?? null, 2), - 'vat' => $this->nullableNumeric($item['vat'] ?? null, 2), - 'payload_json' => $this->encodeJson(is_array($item['payload'] ?? null) ? $item['payload'] : null), - 'created_at' => $now, - 'updated_at' => $now, - ]); - } - } - - /** - * @return array|null - */ - public function findSyncState(int $integrationId): ?array - { - $stmt = $this->pdo->prepare( - 'SELECT integration_id, last_synced_external_updated_at, last_synced_external_order_id, last_run_at, last_error - FROM integration_order_sync_state - WHERE integration_id = :integration_id - LIMIT 1' - ); - $stmt->execute(['integration_id' => $integrationId]); - $row = $stmt->fetch(); - - if (!is_array($row)) { - return null; - } - - return [ - 'integration_id' => (int) ($row['integration_id'] ?? 0), - 'last_synced_external_updated_at' => $this->nullableString($row['last_synced_external_updated_at'] ?? null), - 'last_synced_external_order_id' => $this->nullableString($row['last_synced_external_order_id'] ?? null), - 'last_run_at' => $this->nullableString($row['last_run_at'] ?? null), - 'last_error' => $this->nullableString($row['last_error'] ?? null), - ]; - } - - public function touchSyncState(int $integrationId, ?string $error = null): void - { - $now = date('Y-m-d H:i:s'); - - $stmt = $this->pdo->prepare( - 'INSERT INTO integration_order_sync_state ( - integration_id, last_synced_external_updated_at, last_synced_external_order_id, - last_run_at, last_error, created_at, updated_at - ) VALUES ( - :integration_id, NULL, NULL, - :last_run_at, :last_error, :created_at, :updated_at - ) - ON DUPLICATE KEY UPDATE - last_run_at = VALUES(last_run_at), - last_error = VALUES(last_error), - updated_at = VALUES(updated_at)' - ); - $stmt->execute([ - 'integration_id' => $integrationId, - 'last_run_at' => $now, - 'last_error' => $this->nullableString($error), - 'created_at' => $now, - 'updated_at' => $now, - ]); - } - - public function advanceSyncState(int $integrationId, string $externalUpdatedAt, string $externalOrderId): void - { - $now = date('Y-m-d H:i:s'); - - $stmt = $this->pdo->prepare( - 'INSERT INTO integration_order_sync_state ( - integration_id, last_synced_external_updated_at, last_synced_external_order_id, - last_run_at, last_error, created_at, updated_at - ) VALUES ( - :integration_id, :last_synced_external_updated_at, :last_synced_external_order_id, - :last_run_at, NULL, :created_at, :updated_at - ) - ON DUPLICATE KEY UPDATE - last_synced_external_updated_at = VALUES(last_synced_external_updated_at), - last_synced_external_order_id = VALUES(last_synced_external_order_id), - last_run_at = VALUES(last_run_at), - last_error = NULL, - updated_at = VALUES(updated_at)' - ); - $stmt->execute([ - 'integration_id' => $integrationId, - 'last_synced_external_updated_at' => $externalUpdatedAt, - 'last_synced_external_order_id' => $externalOrderId, - 'last_run_at' => $now, - 'created_at' => $now, - 'updated_at' => $now, - ]); - } - - /** - * @return array|null - */ - public function findByIntegrationExternalOrderId(int $integrationId, string $externalOrderId): ?array - { - $externalId = trim($externalOrderId); - if ($integrationId <= 0 || $externalId === '') { - return null; - } - - $stmt = $this->pdo->prepare( - 'SELECT id, integration_id, external_order_id, status, external_updated_at, updated_at - FROM orders - WHERE integration_id = :integration_id - AND external_order_id = :external_order_id - LIMIT 1' - ); - $stmt->execute([ - 'integration_id' => $integrationId, - 'external_order_id' => $externalId, - ]); - $row = $stmt->fetch(); - if (!is_array($row)) { - return null; - } - - return [ - 'id' => (int) ($row['id'] ?? 0), - 'integration_id' => (int) ($row['integration_id'] ?? 0), - 'external_order_id' => (string) ($row['external_order_id'] ?? ''), - 'status' => (string) ($row['status'] ?? ''), - 'external_updated_at' => $this->nullableString($row['external_updated_at'] ?? null), - 'updated_at' => $this->nullableString($row['updated_at'] ?? null), - ]; - } - - public function updateStatus(int $orderId, string $status, ?string $externalUpdatedAt = null): void - { - if ($orderId <= 0) { - return; - } - - $params = [ - 'id' => $orderId, - 'status' => $this->nullableString($status), - 'updated_at' => date('Y-m-d H:i:s'), - ]; - $sql = 'UPDATE orders SET status = :status, updated_at = :updated_at'; - if ($externalUpdatedAt !== null && trim($externalUpdatedAt) !== '') { - $sql .= ', external_updated_at = :external_updated_at'; - $params['external_updated_at'] = trim($externalUpdatedAt); - } - - $sql .= ' WHERE id = :id'; - $stmt = $this->pdo->prepare($sql); - $stmt->execute($params); - } - - /** - * @return array> - */ - public function listForStatusPush(int $integrationId, ?string $cursorUpdatedAt, ?int $cursorOrderId, int $limit = 100): array - { - if ($integrationId <= 0) { - return []; - } - - $safeLimit = max(1, min(500, $limit)); - $params = [ - 'integration_id' => $integrationId, - ]; - $sql = 'SELECT id, integration_id, external_order_id, status, updated_at - FROM orders - WHERE integration_id = :integration_id - AND external_order_id IS NOT NULL - AND external_order_id <> ""'; - - $normalizedCursorUpdatedAt = $this->nullableString($cursorUpdatedAt); - if ($normalizedCursorUpdatedAt !== null) { - $params['cursor_updated_at'] = $normalizedCursorUpdatedAt; - $normalizedCursorOrderId = max(0, (int) ($cursorOrderId ?? 0)); - $params['cursor_order_id'] = $normalizedCursorOrderId; - $sql .= ' AND ( - updated_at > :cursor_updated_at - OR (updated_at = :cursor_updated_at AND id > :cursor_order_id) - )'; - } - - $sql .= ' ORDER BY updated_at ASC, id ASC - LIMIT :limit'; - - $stmt = $this->pdo->prepare($sql); - foreach ($params as $key => $value) { - if ($key === 'cursor_order_id' || $key === 'integration_id') { - $stmt->bindValue(':' . $key, (int) $value, PDO::PARAM_INT); - continue; - } - - $stmt->bindValue(':' . $key, (string) $value); - } - $stmt->bindValue(':limit', $safeLimit, PDO::PARAM_INT); - $stmt->execute(); - $rows = $stmt->fetchAll(); - if (!is_array($rows)) { - return []; - } - - return array_map( - static fn (array $row): array => [ - 'id' => (int) ($row['id'] ?? 0), - 'integration_id' => (int) ($row['integration_id'] ?? 0), - 'external_order_id' => (string) ($row['external_order_id'] ?? ''), - 'status' => (string) ($row['status'] ?? ''), - 'updated_at' => (string) ($row['updated_at'] ?? ''), - ], - $rows - ); - } - - /** - * @param array $filters - * @return array{0:string,1:array} - */ - private function buildFilters(array $filters): array - { - $where = []; - $params = []; - - $integrationId = max(0, (int) ($filters['integration_id'] ?? 0)); - if ($integrationId > 0) { - $where[] = 'o.integration_id = :integration_id'; - $params['integration_id'] = $integrationId; - } - - $status = trim((string) ($filters['status'] ?? '')); - if ($status !== '') { - $where[] = 'o.status = :status'; - $params['status'] = $status; - } - - $search = trim((string) ($filters['search'] ?? '')); - if ($search !== '') { - $where[] = '(o.internal_order_number LIKE :search OR o.external_order_id LIKE :search OR o.external_order_number LIKE :search OR o.buyer_email LIKE :search OR o.buyer_name LIKE :search)'; - $params['search'] = '%' . $search . '%'; - } - - $dateFrom = trim((string) ($filters['date_from'] ?? '')); - if ($dateFrom !== '') { - $where[] = 'DATE(o.external_updated_at) >= :date_from'; - $params['date_from'] = $dateFrom; - } - - $dateTo = trim((string) ($filters['date_to'] ?? '')); - if ($dateTo !== '') { - $where[] = 'DATE(o.external_updated_at) <= :date_to'; - $params['date_to'] = $dateTo; - } - - $whereSql = $where === [] ? '' : ('WHERE ' . implode(' AND ', $where)); - - return [$whereSql, $params]; - } - - private function resolveSort(string $sort): string - { - return match ($sort) { - 'id' => 'o.id', - 'internal_order_number' => 'o.internal_order_number', - 'integration_name' => 'i.name', - 'external_order_id' => 'o.external_order_id', - 'external_order_number' => 'o.external_order_number', - 'status' => 'o.status', - 'buyer_name' => 'o.buyer_name', - 'total_gross' => 'o.total_gross', - 'currency' => 'o.currency', - 'external_created_at' => 'o.external_created_at', - 'fetched_at' => 'o.fetched_at', - default => 'o.external_updated_at', - }; - } - - /** - * @param array $row - * @return array - */ - private function mapListRow(array $row): array - { - return [ - 'id' => (int) ($row['id'] ?? 0), - 'internal_order_number' => (string) ($row['internal_order_number'] ?? ''), - 'integration_id' => (int) ($row['integration_id'] ?? 0), - 'integration_name' => (string) ($row['integration_name'] ?? ''), - 'external_order_id' => (string) ($row['external_order_id'] ?? ''), - 'external_order_number' => (string) ($row['external_order_number'] ?? ''), - 'status' => (string) ($row['status'] ?? ''), - 'status_text' => $this->resolveStatusText($row), - 'currency' => (string) ($row['currency'] ?? ''), - 'total_gross' => $row['total_gross'] === null ? null : (float) $row['total_gross'], - 'buyer_email' => (string) ($row['buyer_email'] ?? ''), - 'buyer_name' => (string) ($row['buyer_name'] ?? ''), - 'external_created_at' => (string) ($row['external_created_at'] ?? ''), - 'external_updated_at' => (string) ($row['external_updated_at'] ?? ''), - 'fetched_at' => (string) ($row['fetched_at'] ?? ''), - 'payload_json' => isset($row['payload_json']) ? (string) $row['payload_json'] : '', - ]; - } - - private function nullableString(mixed $value): ?string - { - $text = trim((string) $value); - return $text === '' ? null : $text; - } - - /** - * @param array $row - */ - private function resolveStatusText(array $row): string - { - $shopName = trim((string) ($row['shoppro_status_name'] ?? '')); - if ($shopName !== '') { - return $shopName; - } - - $shopNameByOrderPro = trim((string) ($row['shoppro_status_name_by_orderpro'] ?? '')); - if ($shopNameByOrderPro !== '') { - return $shopNameByOrderPro; - } - - $payloadStatusName = $this->extractStatusNameFromPayload( - (string) ($row['payload_json'] ?? ''), - trim((string) ($row['status'] ?? '')) - ); - if ($payloadStatusName !== '') { - return $payloadStatusName; - } - - $orderProCode = trim((string) ($row['orderpro_status_code'] ?? '')); - if ($orderProCode !== '') { - return $orderProCode; - } - - return trim((string) ($row['status'] ?? '')); - } - - private function extractStatusNameFromPayload(string $payloadJson, string $statusCode = ''): string - { - $raw = trim($payloadJson); - if ($raw === '') { - return ''; - } - - $payload = json_decode($raw, true); - if (!is_array($payload)) { - return ''; - } - - $candidates = [ - $this->payloadPath($payload, 'status_name'), - $this->payloadPath($payload, 'order_status_name'), - $this->payloadPath($payload, 'status.name'), - $this->payloadPath($payload, 'order_status.name'), - $this->payloadPath($payload, 'status.label'), - $this->payloadPath($payload, 'order_status.label'), - $this->payloadPath($payload, 'status_title'), - $this->payloadPath($payload, 'order_status_title'), - $this->payloadPath($payload, 'status_text'), - $this->payloadPath($payload, 'order_status_text'), - $this->payloadPath($payload, 'status.status_name'), - $this->payloadPath($payload, 'order_status.status_name'), - ]; - - foreach ($candidates as $candidate) { - $text = trim((string) $candidate); - if ($text !== '') { - return $text; - } - } - - $normalizedStatusCode = trim($statusCode); - if ($normalizedStatusCode !== '') { - $lookupCandidates = [ - $this->payloadPath($payload, 'statuses.' . $normalizedStatusCode), - $this->payloadPath($payload, 'order_statuses.' . $normalizedStatusCode), - $this->payloadPath($payload, 'statuses_map.' . $normalizedStatusCode), - $this->payloadPath($payload, 'status_map.' . $normalizedStatusCode), - ]; - - foreach ($lookupCandidates as $candidate) { - $text = trim((string) $candidate); - if ($text !== '') { - return $text; - } - } - } - - return ''; - } - - private function payloadPath(array $payload, string $path): mixed - { - $current = $payload; - foreach (explode('.', $path) as $segment) { - if (!is_array($current) || !array_key_exists($segment, $current)) { - return null; - } - $current = $current[$segment]; - } - - return $current; - } - - private function ensureInternalOrderNumber(int $orderId): void - { - if ($orderId <= 0) { - return; - } - - $stmt = $this->pdo->prepare( - 'UPDATE orders - SET internal_order_number = :internal_order_number - WHERE id = :id - AND (internal_order_number IS NULL OR internal_order_number = "")' - ); - $stmt->execute([ - 'id' => $orderId, - 'internal_order_number' => sprintf('OP%09d', $orderId), - ]); - } - - private function nullableNumeric(mixed $value, int $precision = 2): ?float - { - $text = trim((string) $value); - if ($text === '' || !is_numeric($text)) { - return null; - } - - return round((float) $text, $precision); - } - - /** - * @param array|null $payload - */ - private function encodeJson(?array $payload): ?string - { - if ($payload === null) { - return null; - } - - $encoded = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - if ($encoded === false) { - return null; - } - - return $encoded; - } -} diff --git a/archive/2026-03-02_users-only-reset/src/Modules/ProductLinks/ChannelOffersRepository.php b/archive/2026-03-02_users-only-reset/src/Modules/ProductLinks/ChannelOffersRepository.php deleted file mode 100644 index 3516e4b..0000000 --- a/archive/2026-03-02_users-only-reset/src/Modules/ProductLinks/ChannelOffersRepository.php +++ /dev/null @@ -1,270 +0,0 @@ -> - */ - public function listByIntegration(int $integrationId, int $limit = 100): array - { - $statement = $this->pdo->prepare( - 'SELECT co.id, co.integration_id, co.channel_id, - co.external_product_id, co.external_variant_id, co.external_offer_id, - co.name, co.sku, co.ean, co.price_brutto, co.quantity, co.currency, - co.offer_status, co.source_updated_at, co.last_seen_at, co.payload_json, - co.created_at, co.updated_at, - sc.code AS channel_code, sc.name AS channel_name - FROM channel_offers co - INNER JOIN sales_channels sc ON sc.id = co.channel_id - WHERE co.integration_id = :integration_id - ORDER BY co.last_seen_at DESC, co.id DESC - LIMIT :limit' - ); - $statement->bindValue(':integration_id', $integrationId, PDO::PARAM_INT); - $statement->bindValue(':limit', max(1, $limit), PDO::PARAM_INT); - $statement->execute(); - - $rows = $statement->fetchAll(); - if (!is_array($rows)) { - return []; - } - - return array_map([$this, 'mapOfferRow'], $rows); - } - - /** - * @return array> - */ - public function search( - int $integrationId, - ?int $channelId, - string $query, - int $limit = 50 - ): array { - $sql = 'SELECT co.id, co.integration_id, co.channel_id, - co.external_product_id, co.external_variant_id, co.external_offer_id, - co.name, co.sku, co.ean, co.price_brutto, co.quantity, co.currency, - co.offer_status, co.source_updated_at, co.last_seen_at, co.payload_json, - co.created_at, co.updated_at, - sc.code AS channel_code, sc.name AS channel_name - FROM channel_offers co - INNER JOIN sales_channels sc ON sc.id = co.channel_id - WHERE co.integration_id = :integration_id'; - $params = ['integration_id' => $integrationId]; - - if ($channelId !== null && $channelId > 0) { - $sql .= ' AND co.channel_id = :channel_id'; - $params['channel_id'] = $channelId; - } - - $needle = trim($query); - if ($needle !== '') { - $sql .= ' AND ( - co.name LIKE :query_name - OR co.sku LIKE :query_sku - OR co.ean LIKE :query_ean - OR co.external_product_id LIKE :query_external_product_id - )'; - $like = '%' . $needle . '%'; - $params['query_name'] = $like; - $params['query_sku'] = $like; - $params['query_ean'] = $like; - $params['query_external_product_id'] = $like; - } - - $sql .= ' ORDER BY co.last_seen_at DESC, co.id DESC LIMIT :limit'; - - $statement = $this->pdo->prepare($sql); - foreach ($params as $key => $value) { - $statement->bindValue(':' . $key, $value); - } - $statement->bindValue(':limit', max(1, $limit), PDO::PARAM_INT); - $statement->execute(); - - $rows = $statement->fetchAll(); - if (!is_array($rows)) { - return []; - } - - return array_map([$this, 'mapOfferRow'], $rows); - } - - public function upsertOffer( - int $integrationId, - int $channelId, - string $externalProductId, - ?string $externalVariantId, - ?string $externalOfferId, - string $name, - ?string $sku, - ?string $ean, - ?float $priceBrutto, - ?float $quantity, - ?string $currency, - string $offerStatus, - ?string $sourceUpdatedAt, - string $lastSeenAt, - ?string $payloadJson - ): void { - $statement = $this->pdo->prepare( - 'INSERT INTO channel_offers ( - integration_id, channel_id, external_product_id, external_variant_id, external_offer_id, - name, sku, ean, price_brutto, quantity, currency, offer_status, - source_updated_at, last_seen_at, payload_json, created_at, updated_at - ) VALUES ( - :integration_id, :channel_id, :external_product_id, :external_variant_id, :external_offer_id, - :name, :sku, :ean, :price_brutto, :quantity, :currency, :offer_status, - :source_updated_at, :last_seen_at, :payload_json, :created_at, :updated_at - ) ON DUPLICATE KEY UPDATE - channel_id = VALUES(channel_id), - external_offer_id = VALUES(external_offer_id), - name = VALUES(name), - sku = VALUES(sku), - ean = VALUES(ean), - price_brutto = VALUES(price_brutto), - quantity = VALUES(quantity), - currency = VALUES(currency), - offer_status = VALUES(offer_status), - source_updated_at = VALUES(source_updated_at), - last_seen_at = VALUES(last_seen_at), - payload_json = VALUES(payload_json), - updated_at = VALUES(updated_at)' - ); - - $now = date('Y-m-d H:i:s'); - $statement->execute([ - 'integration_id' => $integrationId, - 'channel_id' => $channelId, - 'external_product_id' => trim($externalProductId), - 'external_variant_id' => $this->nullableText($externalVariantId), - 'external_offer_id' => $this->nullableText($externalOfferId), - 'name' => trim($name), - 'sku' => $this->nullableText($sku), - 'ean' => $this->nullableText($ean), - 'price_brutto' => $priceBrutto, - 'quantity' => $quantity, - 'currency' => $this->nullableText($currency), - 'offer_status' => trim($offerStatus), - 'source_updated_at' => $this->nullableText($sourceUpdatedAt), - 'last_seen_at' => $lastSeenAt, - 'payload_json' => $this->nullableJson($payloadJson), - 'created_at' => $now, - 'updated_at' => $now, - ]); - } - - public function removeStaleByIntegration(int $integrationId, string $lastSeenThreshold): int - { - $statement = $this->pdo->prepare( - 'DELETE FROM channel_offers - WHERE integration_id = :integration_id - AND last_seen_at < :last_seen_threshold' - ); - $statement->execute([ - 'integration_id' => $integrationId, - 'last_seen_threshold' => trim($lastSeenThreshold), - ]); - - return $statement->rowCount(); - } - - /** - * @return array|null - */ - public function findByExternalIdentity( - int $integrationId, - string $externalProductId, - ?string $externalVariantId - ): ?array { - $statement = $this->pdo->prepare( - 'SELECT co.id, co.integration_id, co.channel_id, - co.external_product_id, co.external_variant_id, co.external_offer_id, - co.name, co.sku, co.ean, co.price_brutto, co.quantity, co.currency, - co.offer_status, co.source_updated_at, co.last_seen_at, co.payload_json, - co.created_at, co.updated_at, - sc.code AS channel_code, sc.name AS channel_name - FROM channel_offers co - INNER JOIN sales_channels sc ON sc.id = co.channel_id - WHERE co.integration_id = :integration_id - AND co.external_product_id = :external_product_id - AND ( - (:external_variant_id_value IS NULL AND co.external_variant_id IS NULL) - OR co.external_variant_id = :external_variant_id_match - ) - ORDER BY co.id DESC - LIMIT 1' - ); - $statement->execute([ - 'integration_id' => $integrationId, - 'external_product_id' => trim($externalProductId), - 'external_variant_id_value' => $this->nullableText($externalVariantId), - 'external_variant_id_match' => $this->nullableText($externalVariantId), - ]); - - $row = $statement->fetch(); - if (!is_array($row)) { - return null; - } - - return $this->mapOfferRow($row); - } - - private function nullableText(?string $value): ?string - { - if ($value === null) { - return null; - } - - $trimmed = trim($value); - return $trimmed === '' ? null : $trimmed; - } - - private function nullableJson(?string $payload): ?string - { - if ($payload === null) { - return null; - } - - $trimmed = trim($payload); - return $trimmed === '' ? null : $trimmed; - } - - /** - * @param array $row - * @return array - */ - private function mapOfferRow(array $row): array - { - return [ - 'id' => (int) ($row['id'] ?? 0), - 'integration_id' => (int) ($row['integration_id'] ?? 0), - 'channel_id' => (int) ($row['channel_id'] ?? 0), - 'external_product_id' => (string) ($row['external_product_id'] ?? ''), - 'external_variant_id' => isset($row['external_variant_id']) ? (string) $row['external_variant_id'] : null, - 'external_offer_id' => isset($row['external_offer_id']) ? (string) $row['external_offer_id'] : null, - 'name' => (string) ($row['name'] ?? ''), - 'sku' => isset($row['sku']) ? (string) $row['sku'] : null, - 'ean' => isset($row['ean']) ? (string) $row['ean'] : null, - 'price_brutto' => $row['price_brutto'] === null ? null : (float) $row['price_brutto'], - 'quantity' => $row['quantity'] === null ? null : (float) $row['quantity'], - 'currency' => isset($row['currency']) ? (string) $row['currency'] : null, - 'offer_status' => (string) ($row['offer_status'] ?? 'active'), - 'source_updated_at' => isset($row['source_updated_at']) ? (string) $row['source_updated_at'] : null, - 'last_seen_at' => (string) ($row['last_seen_at'] ?? ''), - 'payload_json' => isset($row['payload_json']) ? (string) $row['payload_json'] : null, - 'channel_code' => isset($row['channel_code']) ? (string) $row['channel_code'] : '', - 'channel_name' => isset($row['channel_name']) ? (string) $row['channel_name'] : '', - 'created_at' => (string) ($row['created_at'] ?? ''), - 'updated_at' => (string) ($row['updated_at'] ?? ''), - ]; - } -} diff --git a/archive/2026-03-02_users-only-reset/src/Modules/ProductLinks/LinkMatcherService.php b/archive/2026-03-02_users-only-reset/src/Modules/ProductLinks/LinkMatcherService.php deleted file mode 100644 index 7b689f0..0000000 --- a/archive/2026-03-02_users-only-reset/src/Modules/ProductLinks/LinkMatcherService.php +++ /dev/null @@ -1,63 +0,0 @@ - $product - * @param array $offer - * @return array{match_hint:string,confidence:int,link_type:string} - */ - public function match(array $product, array $offer): array - { - $productEan = trim((string) ($product['ean'] ?? '')); - $productSku = trim((string) ($product['sku'] ?? '')); - $offerEan = trim((string) ($offer['ean'] ?? '')); - $offerSku = trim((string) ($offer['sku'] ?? '')); - - if ($productEan !== '' && $offerEan !== '' && $productEan === $offerEan) { - return [ - 'match_hint' => 'EAN exact', - 'confidence' => 98, - 'link_type' => 'auto_ean', - ]; - } - - if ($productSku !== '' && $offerSku !== '' && mb_strtolower($productSku) === mb_strtolower($offerSku)) { - return [ - 'match_hint' => 'SKU exact', - 'confidence' => 90, - 'link_type' => 'auto_sku', - ]; - } - - $normalizedProductSku = $this->normalizeSku($productSku); - $normalizedOfferSku = $this->normalizeSku($offerSku); - if ($normalizedProductSku !== '' && $normalizedProductSku === $normalizedOfferSku) { - return [ - 'match_hint' => 'SKU normalized', - 'confidence' => 75, - 'link_type' => 'auto_sku', - ]; - } - - return [ - 'match_hint' => '', - 'confidence' => 0, - 'link_type' => 'manual', - ]; - } - - private function normalizeSku(string $value): string - { - if ($value === '') { - return ''; - } - - $lower = mb_strtolower($value); - return preg_replace('/[\s\-_]+/u', '', $lower) ?? ''; - } -} - diff --git a/archive/2026-03-02_users-only-reset/src/Modules/ProductLinks/OfferImportService.php b/archive/2026-03-02_users-only-reset/src/Modules/ProductLinks/OfferImportService.php deleted file mode 100644 index 7b096b4..0000000 --- a/archive/2026-03-02_users-only-reset/src/Modules/ProductLinks/OfferImportService.php +++ /dev/null @@ -1,230 +0,0 @@ - $credentials - * @return array{ - * ok:bool, - * imported:int, - * failed:int, - * pages:int, - * message:string - * } - */ - public function importShopProOffers(array $credentials, int $perPage = 100, int $maxPages = 200): array - { - $integrationId = (int) ($credentials['id'] ?? 0); - $baseUrl = trim((string) ($credentials['base_url'] ?? '')); - $apiKey = trim((string) ($credentials['api_key'] ?? '')); - $timeout = (int) ($credentials['timeout_seconds'] ?? 10); - - if ($integrationId <= 0) { - return [ - 'ok' => false, - 'imported' => 0, - 'failed' => 0, - 'pages' => 0, - 'message' => 'Niepoprawne ID integracji.', - ]; - } - - if ($baseUrl === '' || $apiKey === '') { - return [ - 'ok' => false, - 'imported' => 0, - 'failed' => 0, - 'pages' => 0, - 'message' => 'Brak base_url lub api_key dla integracji.', - ]; - } - - $channelId = $this->findChannelIdByCode('shoppro'); - if ($channelId === null) { - return [ - 'ok' => false, - 'imported' => 0, - 'failed' => 0, - 'pages' => 0, - 'message' => 'Brak kanalu sprzedazy shoppro. Uruchom seeding kanalow.', - ]; - } - - $imported = 0; - $failed = 0; - $pages = 0; - $errors = []; - $syncStartedAt = date('Y-m-d H:i:s'); - $page = 1; - $safePerPage = max(1, min(100, $perPage)); - $safeMaxPages = max(1, min(500, $maxPages)); - - while ($page <= $safeMaxPages) { - $listResult = $this->shopProClient->fetchProducts($baseUrl, $apiKey, $timeout, $page, $safePerPage); - if (($listResult['ok'] ?? false) !== true) { - return [ - 'ok' => false, - 'imported' => $imported, - 'failed' => $failed, - 'pages' => $pages, - 'message' => 'Blad pobierania listy produktow (strona ' . $page . '): ' . (string) ($listResult['message'] ?? ''), - ]; - } - - $items = is_array($listResult['items'] ?? null) ? $listResult['items'] : []; - if ($items === []) { - break; - } - - $pages++; - foreach ($items as $item) { - $mapped = $this->mapExternalItem($item, $syncStartedAt); - if ($mapped === null) { - $failed++; - if (count($errors) < 3) { - $errors[] = 'Pominieto rekord bez poprawnego ID produktu z API.'; - } - continue; - } - - try { - $this->offers->upsertOffer( - $integrationId, - $channelId, - (string) $mapped['external_product_id'], - $mapped['external_variant_id'] === null ? null : (string) $mapped['external_variant_id'], - $mapped['external_offer_id'] === null ? null : (string) $mapped['external_offer_id'], - (string) $mapped['name'], - $mapped['sku'] === null ? null : (string) $mapped['sku'], - $mapped['ean'] === null ? null : (string) $mapped['ean'], - $mapped['price_brutto'] === null ? null : (float) $mapped['price_brutto'], - $mapped['quantity'] === null ? null : (float) $mapped['quantity'], - $mapped['currency'] === null ? null : (string) $mapped['currency'], - (string) $mapped['offer_status'], - $mapped['source_updated_at'] === null ? null : (string) $mapped['source_updated_at'], - (string) $mapped['last_seen_at'], - (string) $mapped['payload_json'] - ); - $imported++; - } catch (Throwable $exception) { - $failed++; - if (count($errors) < 3) { - $errors[] = $exception->getMessage(); - } - } - } - - if (count($items) < $safePerPage) { - break; - } - - $page++; - } - - $message = ''; - if ($errors !== []) { - $message = implode(' | ', $errors); - } - - try { - $this->offers->removeStaleByIntegration($integrationId, $syncStartedAt); - } catch (Throwable $exception) { - return [ - 'ok' => false, - 'imported' => $imported, - 'failed' => $failed, - 'pages' => $pages, - 'message' => 'Nie mozna wyczyscic nieaktualnych ofert: ' . $exception->getMessage(), - ]; - } - - return [ - 'ok' => true, - 'imported' => $imported, - 'failed' => $failed, - 'pages' => $pages, - 'message' => $message, - ]; - } - - private function findChannelIdByCode(string $code): ?int - { - $statement = $this->pdo->prepare('SELECT id FROM sales_channels WHERE code = :code LIMIT 1'); - $statement->execute(['code' => trim($code)]); - $value = $statement->fetchColumn(); - - if ($value === false) { - return null; - } - - return (int) $value; - } - - /** - * @param array $item - * @return array|null - */ - private function mapExternalItem(array $item, string $lastSeenAt): ?array - { - $externalProductId = $this->nullableText($item['id'] ?? null); - if ($externalProductId === null) { - return null; - } - - $name = $this->nullableText($item['name'] ?? null); - if ($name === null) { - $name = 'shopPRO #' . $externalProductId; - } - - $payloadJson = json_encode($item, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - if ($payloadJson === false) { - $payloadJson = '{}'; - } - - return [ - 'external_product_id' => $externalProductId, - 'external_variant_id' => $this->nullableText($item['variant_id'] ?? $item['external_variant_id'] ?? null), - 'external_offer_id' => $this->nullableText($item['offer_id'] ?? $item['external_offer_id'] ?? null), - 'name' => $name, - 'sku' => $this->nullableText($item['sku'] ?? null), - 'ean' => $this->nullableText($item['ean'] ?? null), - 'price_brutto' => $this->nullableFloat($item['price_brutto'] ?? $item['price'] ?? null), - 'quantity' => $this->nullableFloat($item['quantity'] ?? null, 3), - 'currency' => $this->nullableText($item['currency'] ?? null), - 'offer_status' => ((int) ($item['status'] ?? 1)) === 1 ? 'active' : 'inactive', - 'source_updated_at' => $this->nullableText($item['updated_at'] ?? $item['modified_at'] ?? null), - 'last_seen_at' => $lastSeenAt, - 'payload_json' => $payloadJson, - ]; - } - - private function nullableText(mixed $value): ?string - { - $text = trim((string) $value); - return $text === '' ? null : $text; - } - - private function nullableFloat(mixed $value, int $precision = 2): ?float - { - $text = trim((string) $value); - if ($text === '' || !is_numeric($text)) { - return null; - } - - return round((float) $text, $precision); - } -} diff --git a/archive/2026-03-02_users-only-reset/src/Modules/ProductLinks/ProductLinksController.php b/archive/2026-03-02_users-only-reset/src/Modules/ProductLinks/ProductLinksController.php deleted file mode 100644 index 9fd977e..0000000 --- a/archive/2026-03-02_users-only-reset/src/Modules/ProductLinks/ProductLinksController.php +++ /dev/null @@ -1,156 +0,0 @@ -resolvePositiveInt($request, ['id', 'product_id']); - if (!$this->validateCsrf($request, $productId)) { - return Response::redirect($this->linksPagePath($productId)); - } - - $integrationId = $this->resolvePositiveInt($request, ['integration_id']); - $externalProductId = (string) $request->input('external_product_id', ''); - $externalVariantId = $this->nullableText((string) $request->input('external_variant_id', '')); - $userId = $this->resolveUserId(); - - $result = $this->service->createManualLink( - $productId, - $integrationId, - $externalProductId, - $externalVariantId, - $userId - ); - - if (($result['ok'] ?? false) !== true) { - Flash::set('product_links_error', (string) ($result['message'] ?? $this->translator->get('products.links.flash.link_failed'))); - return Response::redirect($this->linksPagePath($productId)); - } - - Flash::set('product_links_success', $this->translator->get('products.links.flash.linked')); - return Response::redirect($this->linksPagePath($productId)); - } - - public function relink(Request $request): Response - { - $productId = $this->resolvePositiveInt($request, ['id', 'product_id']); - if (!$this->validateCsrf($request, $productId)) { - return Response::redirect($this->linksPagePath($productId)); - } - - $mapId = $this->resolvePositiveInt($request, ['mapId', 'map_id']); - $integrationId = $this->resolvePositiveInt($request, ['integration_id']); - $externalProductId = (string) $request->input('external_product_id', ''); - $externalVariantId = $this->nullableText((string) $request->input('external_variant_id', '')); - $userId = $this->resolveUserId(); - - $result = $this->service->relink( - $productId, - $mapId, - $integrationId, - $externalProductId, - $externalVariantId, - $userId - ); - - if (($result['ok'] ?? false) !== true) { - Flash::set('product_links_error', (string) ($result['message'] ?? $this->translator->get('products.links.flash.relink_failed'))); - return Response::redirect($this->linksPagePath($productId)); - } - - Flash::set('product_links_success', $this->translator->get('products.links.flash.relinked')); - return Response::redirect($this->linksPagePath($productId)); - } - - public function unlink(Request $request): Response - { - $productId = $this->resolvePositiveInt($request, ['id', 'product_id']); - if (!$this->validateCsrf($request, $productId)) { - return Response::redirect($this->linksPagePath($productId)); - } - - $mapId = $this->resolvePositiveInt($request, ['mapId', 'map_id']); - $userId = $this->resolveUserId(); - $result = $this->service->unlink($productId, $mapId, $userId); - - if (($result['ok'] ?? false) !== true) { - Flash::set('product_links_error', (string) ($result['message'] ?? $this->translator->get('products.links.flash.unlink_failed'))); - return Response::redirect($this->linksPagePath($productId)); - } - - Flash::set('product_links_success', $this->translator->get('products.links.flash.unlinked')); - return Response::redirect($this->linksPagePath($productId)); - } - - private function validateCsrf(Request $request, int $productId): bool - { - $csrfToken = (string) $request->input('_token', ''); - if (Csrf::validate($csrfToken)) { - return true; - } - - Flash::set('product_links_error', $this->translator->get('auth.errors.csrf_expired')); - if ($productId > 0) { - Flash::set('products_error', $this->translator->get('auth.errors.csrf_expired')); - } - return false; - } - - private function resolveUserId(): ?int - { - $user = $this->auth->user(); - if (!is_array($user)) { - return null; - } - - $id = (int) ($user['id'] ?? 0); - return $id > 0 ? $id : null; - } - - private function nullableText(string $value): ?string - { - $trimmed = trim($value); - return $trimmed === '' ? null : $trimmed; - } - - /** - * @param array $keys - */ - private function resolvePositiveInt(Request $request, array $keys): int - { - foreach ($keys as $key) { - $value = max(0, (int) $request->input($key, 0)); - if ($value > 0) { - return $value; - } - } - - return 0; - } - - private function linksPagePath(int $productId): string - { - if ($productId > 0) { - return '/products/' . $productId . '/links'; - } - - return '/products/links'; - } -} diff --git a/archive/2026-03-02_users-only-reset/src/Modules/ProductLinks/ProductLinksRepository.php b/archive/2026-03-02_users-only-reset/src/Modules/ProductLinks/ProductLinksRepository.php deleted file mode 100644 index 8414083..0000000 --- a/archive/2026-03-02_users-only-reset/src/Modules/ProductLinks/ProductLinksRepository.php +++ /dev/null @@ -1,515 +0,0 @@ -> - */ - public function listByProductId(int $productId): array - { - $statement = $this->pdo->prepare( - 'SELECT pcm.id, pcm.product_id, pcm.channel_id, pcm.integration_id, - pcm.external_product_id, pcm.external_variant_id, pcm.sync_state, - pcm.link_type, pcm.link_status, pcm.confidence, - pcm.linked_at, pcm.linked_by_user_id, pcm.unlinked_at, pcm.unlinked_by_user_id, - pcm.sync_meta_json, pcm.last_sync_at, pcm.created_at, pcm.updated_at, - sc.code AS channel_code, sc.name AS channel_name, - i.name AS integration_name, - pla.message AS missing_alert_message, - pla.first_detected_at AS missing_alert_first_detected_at, - pla.last_detected_at AS missing_alert_last_detected_at - FROM product_channel_map pcm - INNER JOIN sales_channels sc ON sc.id = pcm.channel_id - LEFT JOIN integrations i ON i.id = pcm.integration_id - LEFT JOIN product_link_alerts pla - ON pla.product_channel_map_id = pcm.id - AND pla.alert_type = \'missing_remote_link\' - AND pla.status = \'active\' - WHERE pcm.product_id = :product_id - ORDER BY pcm.id DESC' - ); - $statement->execute(['product_id' => $productId]); - $rows = $statement->fetchAll(); - - if (!is_array($rows)) { - return []; - } - - return array_map([$this, 'mapLinkRow'], $rows); - } - - public function findById(int $id): ?array - { - $statement = $this->pdo->prepare( - 'SELECT pcm.id, pcm.product_id, pcm.channel_id, pcm.integration_id, - pcm.external_product_id, pcm.external_variant_id, pcm.sync_state, - pcm.link_type, pcm.link_status, pcm.confidence, - pcm.linked_at, pcm.linked_by_user_id, pcm.unlinked_at, pcm.unlinked_by_user_id, - pcm.sync_meta_json, pcm.last_sync_at, pcm.created_at, pcm.updated_at, - sc.code AS channel_code, sc.name AS channel_name, - i.name AS integration_name, - pla.message AS missing_alert_message, - pla.first_detected_at AS missing_alert_first_detected_at, - pla.last_detected_at AS missing_alert_last_detected_at - FROM product_channel_map pcm - INNER JOIN sales_channels sc ON sc.id = pcm.channel_id - LEFT JOIN integrations i ON i.id = pcm.integration_id - LEFT JOIN product_link_alerts pla - ON pla.product_channel_map_id = pcm.id - AND pla.alert_type = \'missing_remote_link\' - AND pla.status = \'active\' - WHERE pcm.id = :id - LIMIT 1' - ); - $statement->execute(['id' => $id]); - - $row = $statement->fetch(); - if (!is_array($row)) { - return null; - } - - return $this->mapLinkRow($row); - } - - public function findByProductAndIdentity( - int $productId, - int $integrationId, - string $externalProductId, - ?string $externalVariantId - ): ?array { - $statement = $this->pdo->prepare( - 'SELECT pcm.id, pcm.product_id, pcm.channel_id, pcm.integration_id, - pcm.external_product_id, pcm.external_variant_id, pcm.sync_state, - pcm.link_type, pcm.link_status, pcm.confidence, - pcm.linked_at, pcm.linked_by_user_id, pcm.unlinked_at, pcm.unlinked_by_user_id, - pcm.sync_meta_json, pcm.last_sync_at, pcm.created_at, pcm.updated_at, - sc.code AS channel_code, sc.name AS channel_name, - i.name AS integration_name - FROM product_channel_map pcm - INNER JOIN sales_channels sc ON sc.id = pcm.channel_id - LEFT JOIN integrations i ON i.id = pcm.integration_id - WHERE pcm.product_id = :product_id - AND pcm.integration_id = :integration_id - AND pcm.external_product_id = :external_product_id - AND ( - (:external_variant_id_value IS NULL AND pcm.external_variant_id IS NULL) - OR pcm.external_variant_id = :external_variant_id_match - ) - ORDER BY pcm.id DESC - LIMIT 1' - ); - $statement->execute([ - 'product_id' => $productId, - 'integration_id' => $integrationId, - 'external_product_id' => trim($externalProductId), - 'external_variant_id_value' => $this->normalizeNullableText($externalVariantId), - 'external_variant_id_match' => $this->normalizeNullableText($externalVariantId), - ]); - - $row = $statement->fetch(); - if (!is_array($row)) { - return null; - } - - return $this->mapLinkRow($row); - } - - public function insertLink( - int $productId, - int $channelId, - ?int $integrationId, - string $externalProductId, - ?string $externalVariantId, - string $syncState, - string $linkType, - string $linkStatus, - ?int $confidence, - ?int $linkedByUserId, - ?string $syncMetaJson - ): int { - $now = date('Y-m-d H:i:s'); - $statement = $this->pdo->prepare( - 'INSERT INTO product_channel_map ( - product_id, channel_id, integration_id, external_product_id, external_variant_id, - sync_state, link_type, link_status, confidence, - linked_at, linked_by_user_id, sync_meta_json, last_sync_at, created_at, updated_at - ) VALUES ( - :product_id, :channel_id, :integration_id, :external_product_id, :external_variant_id, - :sync_state, :link_type, :link_status, :confidence, - :linked_at, :linked_by_user_id, :sync_meta_json, :last_sync_at, :created_at, :updated_at - )' - ); - $statement->execute([ - 'product_id' => $productId, - 'channel_id' => $channelId, - 'integration_id' => $integrationId, - 'external_product_id' => trim($externalProductId), - 'external_variant_id' => $this->normalizeNullableText($externalVariantId), - 'sync_state' => trim($syncState), - 'link_type' => trim($linkType), - 'link_status' => trim($linkStatus), - 'confidence' => $confidence, - 'linked_at' => $now, - 'linked_by_user_id' => $linkedByUserId, - 'sync_meta_json' => $this->normalizeJson($syncMetaJson), - 'last_sync_at' => $now, - 'created_at' => $now, - 'updated_at' => $now, - ]); - - return (int) $this->pdo->lastInsertId(); - } - - public function updateLink( - int $mapId, - ?int $integrationId, - string $externalProductId, - ?string $externalVariantId, - string $syncState, - string $linkType, - string $linkStatus, - ?int $confidence, - ?int $linkedByUserId, - ?string $syncMetaJson - ): void { - $statement = $this->pdo->prepare( - 'UPDATE product_channel_map SET - integration_id = :integration_id, - external_product_id = :external_product_id, - external_variant_id = :external_variant_id, - sync_state = :sync_state, - link_type = :link_type, - link_status = :link_status, - confidence = :confidence, - linked_at = :linked_at, - linked_by_user_id = :linked_by_user_id, - unlinked_at = NULL, - unlinked_by_user_id = NULL, - sync_meta_json = :sync_meta_json, - last_sync_at = :last_sync_at, - updated_at = :updated_at - WHERE id = :id' - ); - - $now = date('Y-m-d H:i:s'); - $statement->execute([ - 'id' => $mapId, - 'integration_id' => $integrationId, - 'external_product_id' => trim($externalProductId), - 'external_variant_id' => $this->normalizeNullableText($externalVariantId), - 'sync_state' => trim($syncState), - 'link_type' => trim($linkType), - 'link_status' => trim($linkStatus), - 'confidence' => $confidence, - 'linked_at' => $now, - 'linked_by_user_id' => $linkedByUserId, - 'sync_meta_json' => $this->normalizeJson($syncMetaJson), - 'last_sync_at' => $now, - 'updated_at' => $now, - ]); - } - - public function markAsUnlinked( - int $mapId, - ?int $userId, - string $linkStatus = 'inactive', - string $syncState = 'unlinked' - ): void - { - $statement = $this->pdo->prepare( - 'UPDATE product_channel_map SET - sync_state = :sync_state, - link_status = :link_status, - unlinked_at = :unlinked_at, - unlinked_by_user_id = :unlinked_by_user_id, - updated_at = :updated_at - WHERE id = :id' - ); - - $now = date('Y-m-d H:i:s'); - $statement->execute([ - 'id' => $mapId, - 'sync_state' => trim($syncState), - 'link_status' => trim($linkStatus), - 'unlinked_at' => $now, - 'unlinked_by_user_id' => $userId, - 'updated_at' => $now, - ]); - } - - public function deleteById(int $id): bool - { - $statement = $this->pdo->prepare('DELETE FROM product_channel_map WHERE id = :id LIMIT 1'); - $statement->execute(['id' => $id]); - - return $statement->rowCount() > 0; - } - - /** - * @return array> - */ - public function findActiveByExternalIdentity( - int $integrationId, - string $externalProductId, - ?string $externalVariantId - ): array { - $statement = $this->pdo->prepare( - 'SELECT id, product_id, channel_id, integration_id, external_product_id, external_variant_id, - sync_state, link_type, link_status, confidence, linked_at, linked_by_user_id, - unlinked_at, unlinked_by_user_id, sync_meta_json, last_sync_at, created_at, updated_at - FROM product_channel_map - WHERE integration_id = :integration_id - AND external_product_id = :external_product_id - AND ( - (:external_variant_id_value IS NULL AND external_variant_id IS NULL) - OR external_variant_id = :external_variant_id_match - ) - AND link_status = :link_status - ORDER BY id DESC' - ); - $statement->execute([ - 'integration_id' => $integrationId, - 'external_product_id' => trim($externalProductId), - 'external_variant_id_value' => $this->normalizeNullableText($externalVariantId), - 'external_variant_id_match' => $this->normalizeNullableText($externalVariantId), - 'link_status' => 'active', - ]); - - $rows = $statement->fetchAll(); - if (!is_array($rows)) { - return []; - } - - return array_map([$this, 'mapLinkRow'], $rows); - } - - public function logEvent( - int $productChannelMapId, - string $eventType, - ?array $before, - ?array $after, - ?int $createdByUserId - ): int { - $statement = $this->pdo->prepare( - 'INSERT INTO product_link_events ( - product_channel_map_id, event_type, before_json, after_json, created_by_user_id, created_at - ) VALUES ( - :product_channel_map_id, :event_type, :before_json, :after_json, :created_by_user_id, :created_at - )' - ); - - $beforeJson = $before === null ? null : json_encode($before, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - $afterJson = $after === null ? null : json_encode($after, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - - $statement->execute([ - 'product_channel_map_id' => $productChannelMapId, - 'event_type' => trim($eventType), - 'before_json' => $beforeJson === false ? null : $beforeJson, - 'after_json' => $afterJson === false ? null : $afterJson, - 'created_by_user_id' => $createdByUserId, - 'created_at' => date('Y-m-d H:i:s'), - ]); - - return (int) $this->pdo->lastInsertId(); - } - - /** - * @return array> - */ - public function listEventsByMapId(int $productChannelMapId, int $limit = 50): array - { - $statement = $this->pdo->prepare( - 'SELECT id, product_channel_map_id, event_type, before_json, after_json, created_by_user_id, created_at - FROM product_link_events - WHERE product_channel_map_id = :product_channel_map_id - ORDER BY id DESC - LIMIT :limit' - ); - $statement->bindValue(':product_channel_map_id', $productChannelMapId, PDO::PARAM_INT); - $statement->bindValue(':limit', max(1, $limit), PDO::PARAM_INT); - $statement->execute(); - - $rows = $statement->fetchAll(); - if (!is_array($rows)) { - return []; - } - - return array_map([$this, 'mapEventRow'], $rows); - } - - /** - * @return array> - */ - public function listActiveLinksForMissingCheck(?int $integrationId = null): array - { - $sql = 'SELECT id, product_id, channel_id, integration_id, external_product_id, external_variant_id, - sync_state, link_type, link_status, confidence, linked_at, linked_by_user_id, - unlinked_at, unlinked_by_user_id, sync_meta_json, last_sync_at, created_at, updated_at - FROM product_channel_map - WHERE link_status = :link_status - AND integration_id IS NOT NULL - AND external_product_id IS NOT NULL - AND external_product_id <> \'\' - AND unlinked_at IS NULL'; - $params = [ - 'link_status' => 'active', - ]; - - if ($integrationId !== null && $integrationId > 0) { - $sql .= ' AND integration_id = :integration_id'; - $params['integration_id'] = $integrationId; - } - - $sql .= ' ORDER BY id ASC'; - - $statement = $this->pdo->prepare($sql); - $statement->execute($params); - $rows = $statement->fetchAll(); - if (!is_array($rows)) { - return []; - } - - return array_map([$this, 'mapLinkRow'], $rows); - } - - public function upsertActiveAlert( - int $productChannelMapId, - string $alertType, - string $message, - string $detectedAt - ): void { - $statement = $this->pdo->prepare( - 'INSERT INTO product_link_alerts ( - product_channel_map_id, alert_type, status, message, - first_detected_at, last_detected_at, resolved_at, created_at, updated_at - ) VALUES ( - :product_channel_map_id, :alert_type, :status, :message, - :first_detected_at, :last_detected_at, NULL, :created_at, :updated_at - ) ON DUPLICATE KEY UPDATE - status = VALUES(status), - message = VALUES(message), - last_detected_at = VALUES(last_detected_at), - resolved_at = NULL, - updated_at = VALUES(updated_at)' - ); - $statement->execute([ - 'product_channel_map_id' => $productChannelMapId, - 'alert_type' => trim($alertType), - 'status' => 'active', - 'message' => mb_substr(trim($message), 0, 255), - 'first_detected_at' => $detectedAt, - 'last_detected_at' => $detectedAt, - 'created_at' => $detectedAt, - 'updated_at' => $detectedAt, - ]); - } - - public function resolveActiveAlert( - int $productChannelMapId, - string $alertType, - string $resolvedAt - ): void { - $statement = $this->pdo->prepare( - 'UPDATE product_link_alerts SET - status = :status, - resolved_at = :resolved_at, - updated_at = :updated_at - WHERE product_channel_map_id = :product_channel_map_id - AND alert_type = :alert_type - AND status = :current_status' - ); - $statement->execute([ - 'product_channel_map_id' => $productChannelMapId, - 'alert_type' => trim($alertType), - 'status' => 'resolved', - 'current_status' => 'active', - 'resolved_at' => $resolvedAt, - 'updated_at' => $resolvedAt, - ]); - } - - private function normalizeNullableText(?string $value): ?string - { - if ($value === null) { - return null; - } - - $trimmed = trim($value); - return $trimmed === '' ? null : $trimmed; - } - - private function normalizeJson(?string $json): ?string - { - if ($json === null) { - return null; - } - - $trimmed = trim($json); - if ($trimmed === '') { - return null; - } - - return $trimmed; - } - - /** - * @param array $row - * @return array - */ - private function mapLinkRow(array $row): array - { - return [ - 'id' => (int) ($row['id'] ?? 0), - 'product_id' => (int) ($row['product_id'] ?? 0), - 'channel_id' => (int) ($row['channel_id'] ?? 0), - 'integration_id' => $row['integration_id'] === null ? null : (int) $row['integration_id'], - 'external_product_id' => (string) ($row['external_product_id'] ?? ''), - 'external_variant_id' => isset($row['external_variant_id']) ? (string) $row['external_variant_id'] : null, - 'sync_state' => (string) ($row['sync_state'] ?? ''), - 'link_type' => (string) ($row['link_type'] ?? 'manual'), - 'link_status' => (string) ($row['link_status'] ?? 'active'), - 'confidence' => $row['confidence'] === null ? null : (int) $row['confidence'], - 'linked_at' => isset($row['linked_at']) ? (string) $row['linked_at'] : null, - 'linked_by_user_id' => $row['linked_by_user_id'] === null ? null : (int) $row['linked_by_user_id'], - 'unlinked_at' => isset($row['unlinked_at']) ? (string) $row['unlinked_at'] : null, - 'unlinked_by_user_id' => $row['unlinked_by_user_id'] === null ? null : (int) $row['unlinked_by_user_id'], - 'sync_meta_json' => isset($row['sync_meta_json']) ? (string) $row['sync_meta_json'] : null, - 'last_sync_at' => isset($row['last_sync_at']) ? (string) $row['last_sync_at'] : null, - 'channel_code' => isset($row['channel_code']) ? (string) $row['channel_code'] : '', - 'channel_name' => isset($row['channel_name']) ? (string) $row['channel_name'] : '', - 'integration_name' => isset($row['integration_name']) ? (string) $row['integration_name'] : '', - 'has_missing_alert' => isset($row['missing_alert_first_detected_at']) && (string) $row['missing_alert_first_detected_at'] !== '', - 'missing_alert_message' => isset($row['missing_alert_message']) ? (string) $row['missing_alert_message'] : null, - 'missing_alert_first_detected_at' => isset($row['missing_alert_first_detected_at']) ? (string) $row['missing_alert_first_detected_at'] : null, - 'missing_alert_last_detected_at' => isset($row['missing_alert_last_detected_at']) ? (string) $row['missing_alert_last_detected_at'] : null, - 'created_at' => (string) ($row['created_at'] ?? ''), - 'updated_at' => (string) ($row['updated_at'] ?? ''), - ]; - } - - /** - * @param array $row - * @return array - */ - private function mapEventRow(array $row): array - { - return [ - 'id' => (int) ($row['id'] ?? 0), - 'product_channel_map_id' => (int) ($row['product_channel_map_id'] ?? 0), - 'event_type' => (string) ($row['event_type'] ?? ''), - 'before_json' => isset($row['before_json']) ? (string) $row['before_json'] : null, - 'after_json' => isset($row['after_json']) ? (string) $row['after_json'] : null, - 'created_by_user_id' => $row['created_by_user_id'] === null ? null : (int) $row['created_by_user_id'], - 'created_at' => (string) ($row['created_at'] ?? ''), - ]; - } -} diff --git a/archive/2026-03-02_users-only-reset/src/Modules/ProductLinks/ProductLinksService.php b/archive/2026-03-02_users-only-reset/src/Modules/ProductLinks/ProductLinksService.php deleted file mode 100644 index b6bb5f1..0000000 --- a/archive/2026-03-02_users-only-reset/src/Modules/ProductLinks/ProductLinksService.php +++ /dev/null @@ -1,453 +0,0 @@ - $product - * @return array{ - * links:array>, - * link_events_by_map:array>>, - * integrations:array>, - * selected_integration_id:int, - * search_query:string, - * offers:array> - * } - */ - public function buildProductLinksViewData( - int $productId, - array $product, - int $selectedIntegrationId, - string $searchQuery - ): array { - $links = array_values(array_filter( - $this->links->listByProductId($productId), - static function (array $link): bool { - $integrationId = (int) ($link['integration_id'] ?? 0); - return $integrationId > 0; - } - )); - $linkEventsByMap = []; - foreach ($links as $link) { - $mapId = (int) ($link['id'] ?? 0); - if ($mapId <= 0) { - continue; - } - - $linkEventsByMap[$mapId] = $this->links->listEventsByMapId($mapId, 10); - } - $integrations = array_values(array_filter( - $this->integrations->listByType('shoppro'), - static fn (array $row): bool => ($row['is_active'] ?? false) === true - && ($row['has_api_key'] ?? false) === true - )); - - $effectiveIntegrationId = $selectedIntegrationId; - if ($effectiveIntegrationId <= 0 && $integrations !== []) { - $effectiveIntegrationId = (int) ($integrations[0]['id'] ?? 0); - } - - $query = trim($searchQuery); - $offers = []; - if ($effectiveIntegrationId > 0) { - if ($query !== '') { - $offers = $this->offers->search($effectiveIntegrationId, null, $query, 30); - } else { - $offers = $this->offerSuggestions($effectiveIntegrationId, $product); - } - } - - return [ - 'links' => $links, - 'link_events_by_map' => $linkEventsByMap, - 'integrations' => $integrations, - 'selected_integration_id' => $effectiveIntegrationId, - 'search_query' => $query, - 'offers' => $this->decorateOffersWithMatchHint($offers, $product), - ]; - } - - /** - * @return array{ok:bool,conflict:bool,message:string} - */ - public function createManualLink( - int $productId, - int $integrationId, - string $externalProductId, - ?string $externalVariantId, - ?int $userId - ): array { - return $this->upsertManualLink( - $productId, - null, - $integrationId, - $externalProductId, - $externalVariantId, - $userId - ); - } - - /** - * @return array{ok:bool,conflict:bool,message:string} - */ - public function relink( - int $productId, - int $mapId, - int $integrationId, - string $externalProductId, - ?string $externalVariantId, - ?int $userId - ): array { - $existingMap = $this->links->findById($mapId); - if ($existingMap === null || (int) ($existingMap['product_id'] ?? 0) !== $productId) { - return [ - 'ok' => false, - 'conflict' => false, - 'message' => 'Nie znaleziono wskazanego powiazania produktu.', - ]; - } - - return $this->upsertManualLink( - $productId, - $mapId, - $integrationId, - $externalProductId, - $externalVariantId, - $userId - ); - } - - /** - * @return array{ok:bool,message:string} - */ - public function unlink(int $productId, int $mapId, ?int $userId): array - { - $existingMap = $this->links->findById($mapId); - if ($existingMap === null || (int) ($existingMap['product_id'] ?? 0) !== $productId) { - return [ - 'ok' => false, - 'message' => 'Nie znaleziono wskazanego powiazania produktu.', - ]; - } - - try { - $this->pdo->beginTransaction(); - $deleted = $this->links->deleteById($mapId); - if (!$deleted) { - throw new \RuntimeException('Nie udalo sie usunac wskazanego powiazania.'); - } - if ($this->pdo->inTransaction()) { - $this->pdo->commit(); - } - } catch (Throwable $exception) { - if ($this->pdo->inTransaction()) { - $this->pdo->rollBack(); - } - - return [ - 'ok' => false, - 'message' => $exception->getMessage(), - ]; - } - - return [ - 'ok' => true, - 'message' => '', - ]; - } - - /** - * @return array{ok:bool,conflict:bool,message:string} - */ - private function upsertManualLink( - int $productId, - ?int $mapId, - int $integrationId, - string $externalProductId, - ?string $externalVariantId, - ?int $userId - ): array { - $normalizedExternalProductId = trim($externalProductId); - $normalizedExternalVariantId = $this->nullableText($externalVariantId); - - if ($integrationId <= 0) { - return [ - 'ok' => false, - 'conflict' => false, - 'message' => 'Wybierz poprawna integracje.', - ]; - } - if ($normalizedExternalProductId === '') { - return [ - 'ok' => false, - 'conflict' => false, - 'message' => 'Podaj poprawne external_product_id.', - ]; - } - - $offer = $this->offers->findByExternalIdentity( - $integrationId, - $normalizedExternalProductId, - $normalizedExternalVariantId - ); - if ($offer === null) { - return [ - 'ok' => false, - 'conflict' => false, - 'message' => 'Nie znaleziono oferty w lokalnym cache. Najpierw wykonaj import ofert.', - ]; - } - - $channelId = (int) ($offer['channel_id'] ?? 0); - if ($channelId <= 0) { - return [ - 'ok' => false, - 'conflict' => false, - 'message' => 'Oferta ma niepoprawny channel_id.', - ]; - } - - $syncMetaJson = json_encode([ - 'offer_name' => (string) ($offer['name'] ?? ''), - 'external_offer_id' => $offer['external_offer_id'] ?? null, - 'offer_status' => (string) ($offer['offer_status'] ?? ''), - 'source' => 'channel_offers', - ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - if ($syncMetaJson === false) { - $syncMetaJson = null; - } - - $activeMatches = $this->links->findActiveByExternalIdentity( - $integrationId, - $normalizedExternalProductId, - $normalizedExternalVariantId - ); - $conflictingActive = null; - foreach ($activeMatches as $match) { - if ($mapId !== null && (int) ($match['id'] ?? 0) === $mapId) { - continue; - } - if ((int) ($match['product_id'] ?? 0) !== $productId) { - $conflictingActive = $match; - break; - } - } - - $targetMap = $mapId === null - ? $this->links->findByProductAndIdentity( - $productId, - $integrationId, - $normalizedExternalProductId, - $normalizedExternalVariantId - ) - : $this->links->findById($mapId); - - try { - $this->pdo->beginTransaction(); - - if ($conflictingActive !== null) { - $targetMapId = $targetMap === null - ? $this->links->insertLink( - $productId, - $channelId, - $integrationId, - $normalizedExternalProductId, - $normalizedExternalVariantId, - 'conflict', - 'manual', - 'conflict', - null, - $userId, - $syncMetaJson - ) - : (int) ($targetMap['id'] ?? 0); - - if ($targetMap !== null) { - $this->links->updateLink( - $targetMapId, - $integrationId, - $normalizedExternalProductId, - $normalizedExternalVariantId, - 'conflict', - 'manual', - 'conflict', - null, - $userId, - $syncMetaJson - ); - } - - $afterConflict = $this->links->findById($targetMapId); - $this->links->logEvent( - $targetMapId, - 'conflict_detected', - $targetMap, - $afterConflict, - $userId - ); - - if ($this->pdo->inTransaction()) { - $this->pdo->commit(); - } - - return [ - 'ok' => false, - 'conflict' => true, - 'message' => 'Wykryto konflikt: oferta jest aktywnie powiazana z innym produktem.', - ]; - } - - if ($targetMap === null) { - $targetMapId = $this->links->insertLink( - $productId, - $channelId, - $integrationId, - $normalizedExternalProductId, - $normalizedExternalVariantId, - 'linked', - 'manual', - 'active', - null, - $userId, - $syncMetaJson - ); - - $after = $this->links->findById($targetMapId); - $this->links->logEvent($targetMapId, 'linked', null, $after, $userId); - } else { - $targetMapId = (int) ($targetMap['id'] ?? 0); - $this->links->updateLink( - $targetMapId, - $integrationId, - $normalizedExternalProductId, - $normalizedExternalVariantId, - 'linked', - 'manual', - 'active', - null, - $userId, - $syncMetaJson - ); - - $after = $this->links->findById($targetMapId); - $eventType = $mapId === null ? 'linked' : 'relinked'; - $this->links->logEvent($targetMapId, $eventType, $targetMap, $after, $userId); - } - - if ($this->pdo->inTransaction()) { - $this->pdo->commit(); - } - } catch (Throwable $exception) { - if ($this->pdo->inTransaction()) { - $this->pdo->rollBack(); - } - - return [ - 'ok' => false, - 'conflict' => false, - 'message' => $exception->getMessage(), - ]; - } - - return [ - 'ok' => true, - 'conflict' => false, - 'message' => '', - ]; - } - - /** - * @param array $product - * @return array> - */ - private function offerSuggestions(int $integrationId, array $product): array - { - $sku = trim((string) ($product['sku'] ?? '')); - $ean = trim((string) ($product['ean'] ?? '')); - $combined = []; - - if ($ean !== '') { - foreach ($this->offers->search($integrationId, null, $ean, 15) as $row) { - $combined[$this->offerKey($row)] = $row; - } - } - if ($sku !== '') { - foreach ($this->offers->search($integrationId, null, $sku, 15) as $row) { - $combined[$this->offerKey($row)] = $row; - } - - $normalizedSku = preg_replace('/[\s\-_]+/u', '', $sku) ?? ''; - if ($normalizedSku !== '' && $normalizedSku !== $sku) { - foreach ($this->offers->search($integrationId, null, $normalizedSku, 15) as $row) { - $combined[$this->offerKey($row)] = $row; - } - } - } - - $offers = $this->decorateOffersWithMatchHint(array_values($combined), $product); - usort($offers, static function (array $a, array $b): int { - $left = (int) ($a['match_confidence'] ?? 0); - $right = (int) ($b['match_confidence'] ?? 0); - if ($left === $right) { - return strcmp((string) ($b['last_seen_at'] ?? ''), (string) ($a['last_seen_at'] ?? '')); - } - - return $right <=> $left; - }); - - return $offers; - } - - /** - * @param array> $offers - * @param array $product - * @return array> - */ - private function decorateOffersWithMatchHint(array $offers, array $product): array - { - return array_map(function (array $offer) use ($product): array { - $match = $this->matcher->match($product, $offer); - $offer['match_hint'] = (string) ($match['match_hint'] ?? ''); - $offer['match_confidence'] = (int) ($match['confidence'] ?? 0); - $offer['suggested_link_type'] = (string) ($match['link_type'] ?? 'manual'); - return $offer; - }, $offers); - } - - /** - * @param array $offer - */ - private function offerKey(array $offer): string - { - return (string) ($offer['integration_id'] ?? 0) - . '|' - . (string) ($offer['external_product_id'] ?? '') - . '|' - . (string) ($offer['external_variant_id'] ?? ''); - } - - private function nullableText(?string $value): ?string - { - if ($value === null) { - return null; - } - - $trimmed = trim($value); - return $trimmed === '' ? null : $trimmed; - } -} diff --git a/archive/2026-03-02_users-only-reset/src/Modules/Products/ProductRepository.php b/archive/2026-03-02_users-only-reset/src/Modules/Products/ProductRepository.php deleted file mode 100644 index 66e9a1c..0000000 --- a/archive/2026-03-02_users-only-reset/src/Modules/Products/ProductRepository.php +++ /dev/null @@ -1,751 +0,0 @@ - $filters - * @return array{items:array>, total:int, page:int, per_page:int} - */ - public function paginate(array $filters, string $lang = 'pl'): array - { - $page = max(1, (int) ($filters['page'] ?? 1)); - $perPage = max(1, min(100, (int) ($filters['per_page'] ?? 20))); - $offset = ($page - 1) * $perPage; - - [$whereSql, $params] = $this->buildFilters($filters); - $sort = $this->resolveSort((string) ($filters['sort'] ?? 'id')); - $sortDir = strtoupper((string) ($filters['sort_dir'] ?? 'DESC')) === 'ASC' ? 'ASC' : 'DESC'; - - $countStmt = $this->pdo->prepare( - 'SELECT COUNT(*) - FROM products p - LEFT JOIN product_translations pt ON pt.product_id = p.id AND pt.lang = :lang_count - ' . $whereSql - ); - $countStmt->execute(array_merge(['lang_count' => $lang], $params)); - $total = (int) $countStmt->fetchColumn(); - - $listStmt = $this->pdo->prepare( - 'SELECT p.id, p.type, p.sku, p.ean, p.status, p.promoted, p.price_brutto, p.quantity, p.updated_at, p.created_at, - COALESCE(pt.name, "") AS name, - ( - SELECT pi.storage_path - FROM product_images pi - WHERE pi.product_id = p.id - ORDER BY pi.is_main DESC, pi.sort_order ASC, pi.id ASC - LIMIT 1 - ) AS main_image_path - FROM products p - LEFT JOIN product_translations pt ON pt.product_id = p.id AND pt.lang = :lang_list - ' . $whereSql . ' - ORDER BY ' . $sort . ' ' . $sortDir . ' - LIMIT :limit OFFSET :offset' - ); - - foreach (array_merge(['lang_list' => $lang], $params) as $key => $value) { - $listStmt->bindValue(':' . $key, $value); - } - $listStmt->bindValue(':limit', $perPage, PDO::PARAM_INT); - $listStmt->bindValue(':offset', $offset, PDO::PARAM_INT); - $listStmt->execute(); - - $rows = $listStmt->fetchAll(); - if (!is_array($rows)) { - $rows = []; - } - - return [ - 'items' => array_map([$this, 'mapListRow'], $rows), - 'total' => $total, - 'page' => $page, - 'per_page' => $perPage, - ]; - } - - /** - * @return array|null - */ - public function findById(int $id, string $lang = 'pl'): ?array - { - $stmt = $this->pdo->prepare( - 'SELECT p.*, pt.name, pt.short_description, pt.description, pt.meta_title, - pt.meta_description, pt.meta_keywords, pt.seo_link, pt.security_information, - p.producer_name - FROM products p - LEFT JOIN product_translations pt ON pt.product_id = p.id AND pt.lang = :lang - WHERE p.id = :id - LIMIT 1' - ); - $stmt->execute([ - 'id' => $id, - 'lang' => $lang, - ]); - - $row = $stmt->fetch(); - if (!is_array($row)) { - return null; - } - - return $this->mapDetailsRow($row); - } - - public function existsSku(string $sku, ?int $excludeProductId = null): bool - { - $normalized = trim($sku); - if ($normalized === '') { - return false; - } - - $sql = 'SELECT 1 FROM products WHERE sku = :sku'; - $params = ['sku' => $normalized]; - - if ($excludeProductId !== null) { - $sql .= ' AND id <> :exclude_id'; - $params['exclude_id'] = $excludeProductId; - } - - $sql .= ' LIMIT 1'; - - $stmt = $this->pdo->prepare($sql); - $stmt->execute($params); - - return $stmt->fetchColumn() !== false; - } - - /** - * @return array - */ - public function findAllSkus(): array - { - $stmt = $this->pdo->query( - 'SELECT sku - FROM products - WHERE sku IS NOT NULL - AND sku <> "" - AND deleted_at IS NULL' - ); - $rows = $stmt === false ? [] : $stmt->fetchAll(PDO::FETCH_COLUMN); - if (!is_array($rows)) { - return []; - } - - return array_values(array_filter(array_map( - static fn (mixed $value): string => trim((string) $value), - $rows - ), static fn (string $sku): bool => $sku !== '')); - } - - public function findIdBySku(string $sku): ?int - { - $normalized = trim($sku); - if ($normalized === '') { - return null; - } - - $stmt = $this->pdo->prepare('SELECT id FROM products WHERE sku = :sku LIMIT 1'); - $stmt->execute(['sku' => $normalized]); - $value = $stmt->fetchColumn(); - - return $value === false ? null : (int) $value; - } - - public function findIdByEan(string $ean): ?int - { - $normalized = trim($ean); - if ($normalized === '') { - return null; - } - - $stmt = $this->pdo->prepare('SELECT id FROM products WHERE ean = :ean LIMIT 1'); - $stmt->execute(['ean' => $normalized]); - $value = $stmt->fetchColumn(); - - return $value === false ? null : (int) $value; - } - - /** - * @param array $payload - * @param array $translation - */ - public function create(array $payload, array $translation): int - { - $stmt = $this->pdo->prepare( - 'INSERT INTO products ( - uuid, type, sku, ean, status, promoted, new_to_date, - additional_message, additional_message_required, additional_message_text, - vat, weight, - price_brutto, price_brutto_promo, price_netto, price_netto_promo, - quantity, producer_id, producer_name, product_unit_id, custom_fields_json, created_at, updated_at - ) VALUES ( - :uuid, :type, :sku, :ean, :status, :promoted, :new_to_date, - :additional_message, :additional_message_required, :additional_message_text, - :vat, :weight, - :price_brutto, :price_brutto_promo, :price_netto, :price_netto_promo, - :quantity, :producer_id, :producer_name, :product_unit_id, :custom_fields_json, :created_at, :updated_at - )' - ); - $stmt->execute($payload); - - $productId = (int) $this->pdo->lastInsertId(); - - $translationStmt = $this->pdo->prepare( - 'INSERT INTO product_translations ( - product_id, lang, name, short_description, description, - meta_title, meta_description, meta_keywords, seo_link, security_information, created_at, updated_at - ) VALUES ( - :product_id, :lang, :name, :short_description, :description, - :meta_title, :meta_description, :meta_keywords, :seo_link, :security_information, :created_at, :updated_at - )' - ); - - $translationStmt->execute(array_merge(['product_id' => $productId], $translation)); - - return $productId; - } - - /** - * @param array $payload - * @param array $translation - */ - public function update(int $id, array $payload, array $translation): void - { - $stmt = $this->pdo->prepare( - 'UPDATE products SET - type = :type, - sku = :sku, - ean = :ean, - status = :status, - promoted = :promoted, - new_to_date = :new_to_date, - additional_message = :additional_message, - additional_message_required = :additional_message_required, - additional_message_text = :additional_message_text, - vat = :vat, - weight = :weight, - price_brutto = :price_brutto, - price_brutto_promo = :price_brutto_promo, - price_netto = :price_netto, - price_netto_promo = :price_netto_promo, - quantity = :quantity, - producer_id = :producer_id, - producer_name = :producer_name, - product_unit_id = :product_unit_id, - custom_fields_json = :custom_fields_json, - updated_at = :updated_at - WHERE id = :id' - ); - $stmt->execute(array_merge($payload, ['id' => $id])); - - $translationUpsert = $this->pdo->prepare( - 'INSERT INTO product_translations ( - product_id, lang, name, short_description, description, - meta_title, meta_description, meta_keywords, seo_link, security_information, created_at, updated_at - ) VALUES ( - :product_id, :lang, :name, :short_description, :description, - :meta_title, :meta_description, :meta_keywords, :seo_link, :security_information, :created_at, :updated_at - ) ON DUPLICATE KEY UPDATE - name = VALUES(name), - short_description = VALUES(short_description), - description = VALUES(description), - meta_title = VALUES(meta_title), - meta_description = VALUES(meta_description), - meta_keywords = VALUES(meta_keywords), - seo_link = VALUES(seo_link), - security_information = VALUES(security_information), - updated_at = VALUES(updated_at)' - ); - $translationUpsert->execute(array_merge(['product_id' => $id], $translation)); - } - - public function deleteById(int $id): bool - { - $stmt = $this->pdo->prepare('DELETE FROM products WHERE id = :id LIMIT 1'); - $stmt->execute(['id' => $id]); - - return $stmt->rowCount() > 0; - } - - /** - * @return array> - */ - public function findImagesByProductId(int $productId): array - { - $stmt = $this->pdo->prepare( - 'SELECT id, product_id, storage_path, alt, sort_order, is_main, created_at, updated_at - FROM product_images - WHERE product_id = :product_id - ORDER BY is_main DESC, sort_order ASC, id ASC' - ); - $stmt->execute(['product_id' => $productId]); - $rows = $stmt->fetchAll(); - - if (!is_array($rows)) { - return []; - } - - return array_map(static function (array $row): array { - return [ - 'id' => (int) ($row['id'] ?? 0), - 'product_id' => (int) ($row['product_id'] ?? 0), - 'storage_path' => (string) ($row['storage_path'] ?? ''), - 'alt' => isset($row['alt']) ? (string) $row['alt'] : null, - 'sort_order' => (int) ($row['sort_order'] ?? 0), - 'is_main' => (int) ($row['is_main'] ?? 0), - 'created_at' => (string) ($row['created_at'] ?? ''), - 'updated_at' => (string) ($row['updated_at'] ?? ''), - ]; - }, $rows); - } - - public function existsVariantSku(string $sku, ?int $excludeProductId = null): bool - { - $normalized = trim($sku); - if ($normalized === '') { - return false; - } - - $sql = 'SELECT 1 - FROM product_variants pv - INNER JOIN products p ON p.id = pv.product_id - WHERE pv.sku = :sku - AND p.deleted_at IS NULL'; - $params = ['sku' => $normalized]; - - if ($excludeProductId !== null && $excludeProductId > 0) { - $sql .= ' AND pv.product_id <> :exclude_product_id'; - $params['exclude_product_id'] = $excludeProductId; - } - - $sql .= ' LIMIT 1'; - $stmt = $this->pdo->prepare($sql); - $stmt->execute($params); - - return $stmt->fetchColumn() !== false; - } - - /** - * @return array> - */ - public function findVariantsByProductId(int $productId, string $lang = 'pl'): array - { - $variantStmt = $this->pdo->prepare( - 'SELECT id, product_id, permutation_hash, sku, ean, status, - price_brutto, price_brutto_promo, price_netto, price_netto_promo, weight, stock_0_buy, - created_at, updated_at - FROM product_variants - WHERE product_id = :product_id - ORDER BY id ASC' - ); - $variantStmt->execute(['product_id' => $productId]); - $variantRows = $variantStmt->fetchAll(); - if (!is_array($variantRows) || $variantRows === []) { - return []; - } - - $variants = []; - foreach ($variantRows as $row) { - if (!is_array($row)) { - continue; - } - - $id = (int) ($row['id'] ?? 0); - if ($id <= 0) { - continue; - } - - $variants[$id] = [ - 'id' => $id, - 'product_id' => (int) ($row['product_id'] ?? 0), - 'permutation_hash' => (string) ($row['permutation_hash'] ?? ''), - 'sku' => isset($row['sku']) ? (string) $row['sku'] : null, - 'ean' => isset($row['ean']) ? (string) $row['ean'] : null, - 'status' => (int) ($row['status'] ?? 0), - 'price_brutto' => $row['price_brutto'] === null ? null : (float) $row['price_brutto'], - 'price_brutto_promo' => $row['price_brutto_promo'] === null ? null : (float) $row['price_brutto_promo'], - 'price_netto' => $row['price_netto'] === null ? null : (float) $row['price_netto'], - 'price_netto_promo' => $row['price_netto_promo'] === null ? null : (float) $row['price_netto_promo'], - 'weight' => $row['weight'] === null ? null : (float) $row['weight'], - 'stock_0_buy' => (int) ($row['stock_0_buy'] ?? 0), - 'created_at' => (string) ($row['created_at'] ?? ''), - 'updated_at' => (string) ($row['updated_at'] ?? ''), - 'attributes' => [], - ]; - } - - if ($variants === []) { - return []; - } - - $attributeStmt = $this->pdo->prepare( - 'SELECT pva.variant_id, pva.attribute_id, pva.value_id, - COALESCE(a.type, 0) AS attribute_type, - COALESCE(at.name, CONCAT("Atrybut #", pva.attribute_id)) AS attribute_name, - COALESCE(avt.name, CONCAT("Wartosc #", pva.value_id)) AS value_name - FROM product_variant_attributes pva - LEFT JOIN attributes a ON a.id = pva.attribute_id - LEFT JOIN attribute_translations at ON at.attribute_id = pva.attribute_id AND at.lang = :lang_attr - LEFT JOIN attribute_value_translations avt ON avt.value_id = pva.value_id AND avt.lang = :lang_value - WHERE pva.variant_id IN (' . implode(',', array_map('intval', array_keys($variants))) . ') - ORDER BY pva.variant_id ASC, pva.attribute_id ASC' - ); - $attributeStmt->execute([ - 'lang_attr' => $lang, - 'lang_value' => $lang, - ]); - $attributeRows = $attributeStmt->fetchAll(); - if (is_array($attributeRows)) { - foreach ($attributeRows as $row) { - if (!is_array($row)) { - continue; - } - - $variantId = (int) ($row['variant_id'] ?? 0); - if ($variantId <= 0 || !isset($variants[$variantId])) { - continue; - } - - $variants[$variantId]['attributes'][] = [ - 'attribute_id' => (int) ($row['attribute_id'] ?? 0), - 'value_id' => (int) ($row['value_id'] ?? 0), - 'attribute_type' => (int) ($row['attribute_type'] ?? 0), - 'attribute_name' => (string) ($row['attribute_name'] ?? ''), - 'value_name' => (string) ($row['value_name'] ?? ''), - ]; - } - } - - return array_values($variants); - } - - /** - * @return array|null - */ - public function findLatestImportWarning(int $productId): ?array - { - $stmt = $this->pdo->prepare( - 'SELECT change_type, after_json, created_at - FROM product_change_log - WHERE product_id = :product_id - AND change_type IN ("product_import_warning", "product_import_warning_clear") - ORDER BY id DESC - LIMIT 1' - ); - $stmt->execute(['product_id' => $productId]); - $row = $stmt->fetch(); - if (!is_array($row)) { - return null; - } - - if ((string) ($row['change_type'] ?? '') === 'product_import_warning_clear') { - return null; - } - - $payloadRaw = $row['after_json'] ?? null; - $payload = null; - if (is_string($payloadRaw) && trim($payloadRaw) !== '') { - $decoded = json_decode($payloadRaw, true); - if (is_array($decoded)) { - $payload = $decoded; - } - } elseif (is_array($payloadRaw)) { - $payload = $payloadRaw; - } - - if (!is_array($payload)) { - return null; - } - - return [ - 'created_at' => (string) ($row['created_at'] ?? ''), - 'title' => (string) ($payload['title'] ?? ''), - 'messages' => array_values(array_filter( - (array) ($payload['messages'] ?? []), - static fn (mixed $item): bool => is_string($item) && trim($item) !== '' - )), - ]; - } - - public function createImage(int $productId, string $storagePath, ?string $alt, int $sortOrder, int $isMain): int - { - $stmt = $this->pdo->prepare( - 'INSERT INTO product_images (product_id, storage_path, alt, sort_order, is_main, created_at, updated_at) - VALUES (:product_id, :storage_path, :alt, :sort_order, :is_main, :created_at, :updated_at)' - ); - - $now = date('Y-m-d H:i:s'); - $stmt->execute([ - 'product_id' => $productId, - 'storage_path' => trim($storagePath), - 'alt' => $alt, - 'sort_order' => $sortOrder, - 'is_main' => $isMain === 1 ? 1 : 0, - 'created_at' => $now, - 'updated_at' => $now, - ]); - - return (int) $this->pdo->lastInsertId(); - } - - public function deleteImageById(int $productId, int $imageId): ?string - { - $find = $this->pdo->prepare( - 'SELECT storage_path - FROM product_images - WHERE id = :id AND product_id = :product_id - LIMIT 1' - ); - $find->execute(['id' => $imageId, 'product_id' => $productId]); - $path = $find->fetchColumn(); - if (!is_string($path) || trim($path) === '') { - return null; - } - - $delete = $this->pdo->prepare( - 'DELETE FROM product_images - WHERE id = :id AND product_id = :product_id - LIMIT 1' - ); - $delete->execute(['id' => $imageId, 'product_id' => $productId]); - - if ($delete->rowCount() <= 0) { - return null; - } - - return trim($path); - } - - public function setMainImage(int $productId, int $imageId): void - { - $reset = $this->pdo->prepare('UPDATE product_images SET is_main = 0 WHERE product_id = :product_id'); - $reset->execute(['product_id' => $productId]); - - $setMain = $this->pdo->prepare( - 'UPDATE product_images - SET is_main = 1 - WHERE product_id = :product_id AND id = :image_id - LIMIT 1' - ); - $setMain->execute([ - 'product_id' => $productId, - 'image_id' => $imageId, - ]); - } - - /** - * @param array|null $before - * @param array|null $after - */ - public function logChange(int $productId, ?int $userId, string $changeType, ?array $before, ?array $after): void - { - $stmt = $this->pdo->prepare( - 'INSERT INTO product_change_log (product_id, user_id, change_type, before_json, after_json, created_at) - VALUES (:product_id, :user_id, :change_type, :before_json, :after_json, :created_at)' - ); - - $beforeJson = $before === null ? null : json_encode($before, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - $afterJson = $after === null ? null : json_encode($after, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - - $stmt->execute([ - 'product_id' => $productId, - 'user_id' => $userId, - 'change_type' => $changeType, - 'before_json' => $beforeJson === false ? null : $beforeJson, - 'after_json' => $afterJson === false ? null : $afterJson, - 'created_at' => date('Y-m-d H:i:s'), - ]); - } - - /** - * @param array $filters - * @return array{0:string,1:array} - */ - private function buildFilters(array $filters): array - { - $where = ['p.deleted_at IS NULL']; - $params = []; - - $search = trim((string) ($filters['search'] ?? '')); - if ($search !== '') { - $where[] = '(pt.name LIKE :search OR p.sku LIKE :search OR p.ean LIKE :search)'; - $params['search'] = '%' . $search . '%'; - } - - $status = (string) ($filters['status'] ?? ''); - if ($status !== '' && in_array($status, ['0', '1'], true)) { - $where[] = 'p.status = :status'; - $params['status'] = (int) $status; - } - - $type = (string) ($filters['type'] ?? ''); - if ($type !== '' && in_array($type, ['simple', 'variant_parent'], true)) { - $where[] = 'p.type = :type'; - $params['type'] = $type; - } - - $whereSql = 'WHERE ' . implode(' AND ', $where); - - return [$whereSql, $params]; - } - - private function resolveSort(string $sort): string - { - return match ($sort) { - 'name' => 'pt.name', - 'sku' => 'p.sku', - 'ean' => 'p.ean', - 'price_brutto' => 'p.price_brutto', - 'quantity' => 'p.quantity', - 'status' => 'p.status', - 'updated_at' => 'p.updated_at', - 'created_at' => 'p.created_at', - default => 'p.id', - }; - } - - /** - * @param array $row - * @return array - */ - private function mapListRow(array $row): array - { - return [ - 'id' => (int) ($row['id'] ?? 0), - 'name' => (string) ($row['name'] ?? ''), - 'main_image_path' => (string) ($row['main_image_path'] ?? ''), - 'type' => (string) ($row['type'] ?? 'simple'), - 'sku' => (string) ($row['sku'] ?? ''), - 'ean' => (string) ($row['ean'] ?? ''), - 'status' => (int) ($row['status'] ?? 0), - 'promoted' => (int) ($row['promoted'] ?? 0), - 'price_brutto' => (float) ($row['price_brutto'] ?? 0), - 'quantity' => (float) ($row['quantity'] ?? 0), - 'updated_at' => (string) ($row['updated_at'] ?? ''), - 'created_at' => (string) ($row['created_at'] ?? ''), - ]; - } - - /** - * @param array $row - * @return array - */ - private function mapDetailsRow(array $row): array - { - return [ - 'id' => (int) ($row['id'] ?? 0), - 'uuid' => (string) ($row['uuid'] ?? ''), - 'type' => (string) ($row['type'] ?? 'simple'), - 'sku' => (string) ($row['sku'] ?? ''), - 'ean' => (string) ($row['ean'] ?? ''), - 'status' => (int) ($row['status'] ?? 1), - 'promoted' => (int) ($row['promoted'] ?? 0), - 'new_to_date' => isset($row['new_to_date']) && $row['new_to_date'] !== null ? (string) $row['new_to_date'] : null, - 'additional_message' => (int) ($row['additional_message'] ?? 0), - 'additional_message_required' => (int) ($row['additional_message_required'] ?? 0), - 'additional_message_text' => isset($row['additional_message_text']) ? (string) $row['additional_message_text'] : null, - 'vat' => $row['vat'] === null ? null : (float) $row['vat'], - 'weight' => $row['weight'] === null ? null : (float) $row['weight'], - 'price_brutto' => $row['price_brutto'] === null ? null : (float) $row['price_brutto'], - 'price_brutto_promo' => $row['price_brutto_promo'] === null ? null : (float) $row['price_brutto_promo'], - 'price_netto' => $row['price_netto'] === null ? null : (float) $row['price_netto'], - 'price_netto_promo' => $row['price_netto_promo'] === null ? null : (float) $row['price_netto_promo'], - 'quantity' => (float) ($row['quantity'] ?? 0), - 'producer_id' => $row['producer_id'] === null ? null : (int) $row['producer_id'], - 'producer_name' => isset($row['producer_name']) && $row['producer_name'] !== null ? (string) $row['producer_name'] : null, - 'product_unit_id' => $row['product_unit_id'] === null ? null : (int) $row['product_unit_id'], - 'name' => (string) ($row['name'] ?? ''), - 'short_description' => (string) ($row['short_description'] ?? ''), - 'description' => (string) ($row['description'] ?? ''), - 'meta_title' => (string) ($row['meta_title'] ?? ''), - 'meta_description' => (string) ($row['meta_description'] ?? ''), - 'meta_keywords' => (string) ($row['meta_keywords'] ?? ''), - 'seo_link' => (string) ($row['seo_link'] ?? ''), - 'security_information' => isset($row['security_information']) ? (string) $row['security_information'] : null, - 'custom_fields_json' => isset($row['custom_fields_json']) ? (string) $row['custom_fields_json'] : null, - 'created_at' => (string) ($row['created_at'] ?? ''), - 'updated_at' => (string) ($row['updated_at'] ?? ''), - ]; - } - - public function updateEan(int $id, string $ean): void - { - $stmt = $this->pdo->prepare( - 'UPDATE products SET ean = :ean, updated_at = :updated_at WHERE id = :id' - ); - $stmt->execute([ - 'ean' => $ean, - 'updated_at' => date('Y-m-d H:i:s'), - 'id' => $id, - ]); - } - - /** - * @return array> - */ - public function findIntegrationTranslations(int $productId): array - { - $stmt = $this->pdo->prepare( - 'SELECT pit.id, pit.product_id, pit.integration_id, - pit.name, pit.short_description, pit.description, - i.name AS integration_name - FROM product_integration_translations pit - LEFT JOIN integrations i ON i.id = pit.integration_id - WHERE pit.product_id = :product_id - ORDER BY i.name ASC' - ); - $stmt->execute(['product_id' => $productId]); - $rows = $stmt->fetchAll(); - - if (!is_array($rows)) { - return []; - } - - return array_map(static fn (array $row): array => [ - 'id' => (int) ($row['id'] ?? 0), - 'product_id' => (int) ($row['product_id'] ?? 0), - 'integration_id' => (int) ($row['integration_id'] ?? 0), - 'integration_name' => (string) ($row['integration_name'] ?? ''), - 'name' => isset($row['name']) ? (string) $row['name'] : null, - 'short_description' => isset($row['short_description']) ? (string) $row['short_description'] : null, - 'description' => isset($row['description']) ? (string) $row['description'] : null, - ], $rows); - } - - public function upsertIntegrationTranslation( - int $productId, - int $integrationId, - ?string $name, - ?string $shortDescription, - ?string $description - ): void { - $now = date('Y-m-d H:i:s'); - $stmt = $this->pdo->prepare( - 'INSERT INTO product_integration_translations - (product_id, integration_id, name, short_description, description, created_at, updated_at) - VALUES - (:product_id, :integration_id, :name, :short_description, :description, :created_at, :updated_at) - ON DUPLICATE KEY UPDATE - name = VALUES(name), - short_description = VALUES(short_description), - description = VALUES(description), - updated_at = VALUES(updated_at)' - ); - $stmt->execute([ - 'product_id' => $productId, - 'integration_id' => $integrationId, - 'name' => $name !== '' ? $name : null, - 'short_description' => $shortDescription !== '' ? $shortDescription : null, - 'description' => $description !== '' ? $description : null, - 'created_at' => $now, - 'updated_at' => $now, - ]); - } -} diff --git a/archive/2026-03-02_users-only-reset/src/Modules/Products/ProductService.php b/archive/2026-03-02_users-only-reset/src/Modules/Products/ProductService.php deleted file mode 100644 index ea01196..0000000 --- a/archive/2026-03-02_users-only-reset/src/Modules/Products/ProductService.php +++ /dev/null @@ -1,479 +0,0 @@ - $input - * @param array|null $actor - * @return array{ok:bool, errors:array, id?:int} - */ - public function create(array $input, ?array $actor): array - { - $errors = $this->validator->validate($input, false); - if ($errors !== []) { - return ['ok' => false, 'errors' => $errors]; - } - - $sku = trim((string) ($input['sku'] ?? '')); - if ($sku !== '' && $this->products->existsSku($sku)) { - return ['ok' => false, 'errors' => ['Podane SKU produktu jest juz zajete.']]; - } - - $normalized = $this->normalizeForSave($input); - $updatePayload = $this->toUpdatePayload($normalized['product']); - $actorId = isset($actor['id']) ? (int) $actor['id'] : null; - - try { - $this->pdo->beginTransaction(); - $productId = $this->products->create($normalized['product'], $normalized['translation']); - $this->products->logChange($productId, $actorId, 'product_created', null, $normalized['audit']); - $this->pdo->commit(); - - return ['ok' => true, 'errors' => [], 'id' => $productId]; - } catch (Throwable $exception) { - if ($this->pdo->inTransaction()) { - $this->pdo->rollBack(); - } - - return ['ok' => false, 'errors' => ['Nie udalo sie zapisac produktu: ' . $exception->getMessage()]]; - } - } - - /** - * @param array $input - * @param array|null $actor - * @return array{ok:bool, errors:array} - */ - public function update(int $id, array $input, ?array $actor): array - { - $existing = $this->products->findById($id, 'pl'); - if ($existing === null) { - return ['ok' => false, 'errors' => ['Produkt nie istnieje.']]; - } - - // These fields are not edited in orderPRO UI, so keep current values when absent in request. - $input = $this->mergeMissingShopProSettingsFromExisting($input, $existing); - - $errors = $this->validator->validate($input, true); - if ($errors !== []) { - return ['ok' => false, 'errors' => $errors]; - } - - $sku = trim((string) ($input['sku'] ?? '')); - if ($sku !== '' && $this->products->existsSku($sku, $id)) { - return ['ok' => false, 'errors' => ['Podane SKU produktu jest juz zajete.']]; - } - - $normalized = $this->normalizeForSave($input); - $updatePayload = $this->toUpdatePayload($normalized['product']); - $actorId = isset($actor['id']) ? (int) $actor['id'] : null; - - try { - $this->pdo->beginTransaction(); - $this->products->update($id, $updatePayload, $normalized['translation']); - - $criticalBefore = $this->extractCriticalFields($existing); - $criticalAfter = $this->extractCriticalFields($normalized['audit']); - if ($criticalBefore !== $criticalAfter) { - $this->products->logChange($id, $actorId, 'product_updated', $criticalBefore, $criticalAfter); - } - - $this->pdo->commit(); - return ['ok' => true, 'errors' => []]; - } catch (Throwable $exception) { - if ($this->pdo->inTransaction()) { - $this->pdo->rollBack(); - } - - return ['ok' => false, 'errors' => ['Nie udalo sie zaktualizowac produktu: ' . $exception->getMessage()]]; - } - } - - /** - * @param array|null $actor - * @return array{ok:bool, errors:array} - */ - public function delete(int $id, ?array $actor): array - { - $existing = $this->products->findById($id, 'pl'); - if ($existing === null) { - return ['ok' => false, 'errors' => ['Produkt nie istnieje.']]; - } - - $imagePaths = $this->findProductImageStoragePaths($id); - - try { - $this->pdo->beginTransaction(); - $deleted = $this->products->deleteById($id); - if (!$deleted) { - throw new \RuntimeException('Nie udalo sie usunac produktu.'); - } - $this->pdo->commit(); - $this->deleteProductImageFiles($imagePaths); - - return ['ok' => true, 'errors' => []]; - } catch (Throwable $exception) { - if ($this->pdo->inTransaction()) { - $this->pdo->rollBack(); - } - - return ['ok' => false, 'errors' => ['Nie udalo sie usunac produktu: ' . $exception->getMessage()]]; - } - } - - /** - * @param array $input - * @return array{product:array, translation:array, audit:array} - */ - private function normalizeForSave(array $input): array - { - $vatRaw = trim((string) ($input['vat'] ?? '')); - $vat = $vatRaw === '' ? null : round((float) $vatRaw, 2); - - $pricePair = $this->resolvePricePair( - trim((string) ($input['price_brutto'] ?? '')), - trim((string) ($input['price_netto'] ?? '')), - $vat, - (string) ($input['price_input_mode'] ?? 'brutto') - ); - - $promoPair = $this->resolvePricePair( - trim((string) ($input['price_brutto_promo'] ?? '')), - trim((string) ($input['price_netto_promo'] ?? '')), - $vat, - (string) ($input['price_input_mode'] ?? 'brutto'), - true - ); - - $now = date('Y-m-d H:i:s'); - - $product = [ - 'uuid' => $this->uuidV4(), - 'type' => (string) ($input['type'] ?? 'simple'), - 'sku' => $this->nullableString($input['sku'] ?? null), - 'ean' => $this->nullableString($input['ean'] ?? null), - 'status' => (int) ($input['status'] ?? 1), - 'promoted' => (int) ($input['promoted'] ?? 0), - 'new_to_date' => $this->nullableString($input['new_to_date'] ?? null), - 'additional_message' => ((int) ($input['additional_message'] ?? 0)) === 1 ? 1 : 0, - 'additional_message_required' => ((int) ($input['additional_message_required'] ?? 0)) === 1 ? 1 : 0, - 'additional_message_text' => $this->nullableString($input['additional_message_text'] ?? null), - 'vat' => $vat, - 'weight' => $this->nullableFloat($input['weight'] ?? null, 3), - 'price_brutto' => $pricePair['brutto'] ?? 0.00, - 'price_brutto_promo' => $promoPair['brutto'], - 'price_netto' => $pricePair['netto'], - 'price_netto_promo' => $promoPair['netto'], - 'quantity' => round((float) ($input['quantity'] ?? 0), 3), - 'producer_id' => $this->nullableInt($input['producer_id'] ?? null), - 'producer_name' => null, - 'product_unit_id' => $this->nullableInt($input['product_unit_id'] ?? null), - 'custom_fields_json' => null, - 'created_at' => $now, - 'updated_at' => $now, - ]; - - $translation = [ - 'lang' => 'pl', - 'name' => trim((string) ($input['name'] ?? '')), - 'short_description' => $this->nullableString($input['short_description'] ?? null), - 'description' => $this->nullableString($input['description'] ?? null), - 'meta_title' => $this->nullableString($input['meta_title'] ?? null), - 'meta_description' => $this->nullableString($input['meta_description'] ?? null), - 'meta_keywords' => $this->nullableString($input['meta_keywords'] ?? null), - 'seo_link' => $this->nullableString($input['seo_link'] ?? null), - 'security_information' => null, - 'created_at' => $now, - 'updated_at' => $now, - ]; - - $audit = [ - 'type' => $product['type'], - 'sku' => $product['sku'], - 'ean' => $product['ean'], - 'status' => $product['status'], - 'promoted' => $product['promoted'], - 'vat' => $product['vat'], - 'price_brutto' => $product['price_brutto'], - 'price_netto' => $product['price_netto'], - 'price_brutto_promo' => $product['price_brutto_promo'], - 'price_netto_promo' => $product['price_netto_promo'], - 'quantity' => $product['quantity'], - 'name' => $translation['name'], - ]; - - return [ - 'product' => $product, - 'translation' => $translation, - 'audit' => $audit, - ]; - } - - /** - * @return array{brutto:float|null, netto:float|null} - */ - private function resolvePricePair( - string $bruttoRaw, - string $nettoRaw, - ?float $vat, - string $mode, - bool $allowEmpty = false - ): array { - if ($allowEmpty && $bruttoRaw === '' && $nettoRaw === '') { - return ['brutto' => null, 'netto' => null]; - } - - $multiplier = 1 + (($vat ?? 0.0) / 100); - - if ($mode === 'netto') { - if ($nettoRaw === '' && $bruttoRaw !== '') { - $brutto = round((float) $bruttoRaw, 2); - $netto = $multiplier > 0 ? round($brutto / $multiplier, 2) : $brutto; - return ['brutto' => $brutto, 'netto' => $netto]; - } - - $netto = $nettoRaw === '' ? 0.0 : round((float) $nettoRaw, 2); - $brutto = round($netto * $multiplier, 2); - return ['brutto' => $brutto, 'netto' => $netto]; - } - - if ($bruttoRaw === '' && $nettoRaw !== '') { - $netto = round((float) $nettoRaw, 2); - $brutto = round($netto * $multiplier, 2); - return ['brutto' => $brutto, 'netto' => $netto]; - } - - $brutto = $bruttoRaw === '' ? 0.0 : round((float) $bruttoRaw, 2); - $netto = $multiplier > 0 ? round($brutto / $multiplier, 2) : $brutto; - - return ['brutto' => $brutto, 'netto' => $netto]; - } - - /** - * @param array $row - * @return array - */ - private function extractCriticalFields(array $row): array - { - return [ - 'sku' => $row['sku'] ?? null, - 'ean' => $row['ean'] ?? null, - 'status' => $row['status'] ?? null, - 'promoted' => $row['promoted'] ?? null, - 'price_brutto' => $row['price_brutto'] ?? null, - 'price_netto' => $row['price_netto'] ?? null, - 'price_brutto_promo' => $row['price_brutto_promo'] ?? null, - 'price_netto_promo' => $row['price_netto_promo'] ?? null, - 'quantity' => $row['quantity'] ?? null, - 'name' => $row['name'] ?? null, - ]; - } - - private function nullableString(mixed $value): ?string - { - $text = trim((string) $value); - return $text === '' ? null : $text; - } - - private function nullableInt(mixed $value): ?int - { - $text = trim((string) $value); - if ($text === '' || !is_numeric($text)) { - return null; - } - - return (int) $text; - } - - private function nullableFloat(mixed $value, int $precision = 2): ?float - { - $text = trim((string) $value); - if ($text === '' || !is_numeric($text)) { - return null; - } - - return round((float) $text, $precision); - } - - private function uuidV4(): string - { - $data = random_bytes(16); - $data[6] = chr((ord($data[6]) & 0x0f) | 0x40); - $data[8] = chr((ord($data[8]) & 0x3f) | 0x80); - - return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); - } - - /** - * @param array $product - * @return array - */ - private function toUpdatePayload(array $product): array - { - return [ - 'type' => $product['type'] ?? 'simple', - 'sku' => $product['sku'] ?? null, - 'ean' => $product['ean'] ?? null, - 'status' => $product['status'] ?? 1, - 'promoted' => $product['promoted'] ?? 0, - 'new_to_date' => $product['new_to_date'] ?? null, - 'additional_message' => ((int) ($product['additional_message'] ?? 0)) === 1 ? 1 : 0, - 'additional_message_required' => ((int) ($product['additional_message_required'] ?? 0)) === 1 ? 1 : 0, - 'additional_message_text' => $product['additional_message_text'] ?? null, - 'vat' => $product['vat'] ?? null, - 'weight' => $product['weight'] ?? null, - 'price_brutto' => $product['price_brutto'] ?? 0, - 'price_brutto_promo' => $product['price_brutto_promo'] ?? null, - 'price_netto' => $product['price_netto'] ?? null, - 'price_netto_promo' => $product['price_netto_promo'] ?? null, - 'quantity' => $product['quantity'] ?? 0, - 'producer_id' => $product['producer_id'] ?? null, - 'producer_name' => $product['producer_name'] ?? null, - 'product_unit_id' => $product['product_unit_id'] ?? null, - 'custom_fields_json' => $product['custom_fields_json'] ?? null, - 'updated_at' => date('Y-m-d H:i:s'), - ]; - } - - /** - * @param array $input - * @param array $existing - * @return array - */ - private function mergeMissingShopProSettingsFromExisting(array $input, array $existing): array - { - $keys = [ - 'new_to_date', - 'additional_message', - 'additional_message_required', - 'additional_message_text', - ]; - - foreach ($keys as $key) { - if (!array_key_exists($key, $input)) { - $input[$key] = $existing[$key] ?? null; - } - } - - return $input; - } - - /** - * @return array - */ - private function findProductImageStoragePaths(int $productId): array - { - $stmt = $this->pdo->prepare('SELECT storage_path FROM product_images WHERE product_id = :product_id'); - $stmt->execute(['product_id' => $productId]); - $rows = $stmt->fetchAll(); - if (!is_array($rows)) { - return []; - } - - $paths = []; - foreach ($rows as $row) { - if (!is_array($row)) { - continue; - } - - $path = trim((string) ($row['storage_path'] ?? '')); - if ($path !== '') { - $paths[] = $path; - } - } - - return array_values(array_unique($paths)); - } - - /** - * @param array $storagePaths - */ - private function deleteProductImageFiles(array $storagePaths): void - { - foreach ($storagePaths as $storagePath) { - if ($this->storagePathHasOtherReferences($storagePath)) { - continue; - } - - $resolvedFilePath = $this->resolveLocalImageFilePath($storagePath); - if ($resolvedFilePath === null || !is_file($resolvedFilePath)) { - continue; - } - - @unlink($resolvedFilePath); - } - } - - private function storagePathHasOtherReferences(string $storagePath): bool - { - $stmt = $this->pdo->prepare( - 'SELECT 1 FROM product_images WHERE storage_path = :storage_path LIMIT 1' - ); - $stmt->execute(['storage_path' => $storagePath]); - - return $stmt->fetchColumn() !== false; - } - - private function resolveLocalImageFilePath(string $storagePath): ?string - { - $path = trim($storagePath); - if ($path === '') { - return null; - } - - if (preg_match('#^https?://#i', $path) === 1 || str_starts_with($path, '//')) { - return null; - } - - $projectRoot = dirname(__DIR__, 3); - $projectRootReal = realpath($projectRoot); - if ($projectRootReal === false) { - return null; - } - - $trimmed = ltrim(str_replace(['\\', '/'], DIRECTORY_SEPARATOR, $path), DIRECTORY_SEPARATOR); - $candidates = []; - - if ($this->isAbsolutePath($path)) { - $candidates[] = str_replace(['\\', '/'], DIRECTORY_SEPARATOR, $path); - } - - $candidates[] = $projectRoot . DIRECTORY_SEPARATOR . 'public' . DIRECTORY_SEPARATOR . $trimmed; - $candidates[] = $projectRoot . DIRECTORY_SEPARATOR . $trimmed; - - foreach ($candidates as $candidate) { - $real = realpath($candidate); - if ($real === false || !is_file($real)) { - continue; - } - - if ($real === $projectRootReal || str_starts_with($real, $projectRootReal . DIRECTORY_SEPARATOR)) { - return $real; - } - } - - return null; - } - - private function isAbsolutePath(string $path): bool - { - if ($path === '') { - return false; - } - - return preg_match('/^[A-Za-z]:[\\\\\\/]/', $path) === 1 || str_starts_with($path, DIRECTORY_SEPARATOR); - } -} diff --git a/archive/2026-03-02_users-only-reset/src/Modules/Products/ProductSkuGenerator.php b/archive/2026-03-02_users-only-reset/src/Modules/Products/ProductSkuGenerator.php deleted file mode 100644 index 3687f09..0000000 --- a/archive/2026-03-02_users-only-reset/src/Modules/Products/ProductSkuGenerator.php +++ /dev/null @@ -1,103 +0,0 @@ -normalizeFormat($this->appSettings->get('products_sku_format', 'PP000000')); - } - - public function nextSku(): string - { - $format = $this->format(); - [$prefix, $width, $suffix] = $this->parseFormat($format); - - $maxNumber = 0; - foreach ($this->products->findAllSkus() as $sku) { - $number = $this->extractNumber($sku, $prefix, $width, $suffix); - if ($number !== null && $number > $maxNumber) { - $maxNumber = $number; - } - } - - $candidateNumber = max(1, $maxNumber + 1); - while (true) { - $candidate = $prefix . str_pad((string) $candidateNumber, $width, '0', STR_PAD_LEFT) . $suffix; - if (!$this->products->existsSku($candidate)) { - return $candidate; - } - - $candidateNumber++; - if ($candidateNumber > 999999999) { - throw new RuntimeException('Nie udalo sie wygenerowac kolejnego SKU.'); - } - } - } - - private function normalizeFormat(?string $raw): string - { - $value = trim((string) $raw); - return $value === '' ? 'PP000000' : $value; - } - - /** - * @return array{0:string,1:int,2:string} - */ - private function parseFormat(string $format): array - { - if (mb_strlen($format) > 128) { - throw new RuntimeException('Format SKU jest za dlugi (maksymalnie 128 znakow).'); - } - - if (preg_match('/0+/', $format, $matches, PREG_OFFSET_CAPTURE) !== 1) { - throw new RuntimeException('Format SKU musi zawierac czesc liczbowa (zera), np. PP000000.'); - } - - $token = (string) $matches[0][0]; - $offset = (int) $matches[0][1]; - $width = strlen($token); - - $prefix = substr($format, 0, $offset); - $suffix = substr($format, $offset + $width); - - return [$prefix, $width, $suffix === false ? '' : $suffix]; - } - - private function extractNumber(string $sku, string $prefix, int $width, string $suffix): ?int - { - $prefixLen = strlen($prefix); - $suffixLen = strlen($suffix); - $skuLen = strlen($sku); - - if ($skuLen !== ($prefixLen + $width + $suffixLen)) { - return null; - } - - if ($prefix !== '' && !str_starts_with($sku, $prefix)) { - return null; - } - - if ($suffix !== '' && !str_ends_with($sku, $suffix)) { - return null; - } - - $numberPart = substr($sku, $prefixLen, $width); - if ($numberPart === false || preg_match('/^\d+$/', $numberPart) !== 1) { - return null; - } - - return (int) $numberPart; - } -} diff --git a/archive/2026-03-02_users-only-reset/src/Modules/Products/ProductValidator.php b/archive/2026-03-02_users-only-reset/src/Modules/Products/ProductValidator.php deleted file mode 100644 index 173e95b..0000000 --- a/archive/2026-03-02_users-only-reset/src/Modules/Products/ProductValidator.php +++ /dev/null @@ -1,102 +0,0 @@ - $input - * @param bool $isUpdate - * @return array - */ - public function validate(array $input, bool $isUpdate = false): array - { - $errors = []; - - $name = trim((string) ($input['name'] ?? '')); - if (mb_strlen($name) < 2) { - $errors[] = 'Nazwa produktu musi miec co najmniej 2 znaki.'; - } - - $type = (string) ($input['type'] ?? 'simple'); - if (!in_array($type, ['simple', 'variant_parent'], true)) { - $errors[] = 'Niepoprawny typ produktu.'; - } - - $sku = trim((string) ($input['sku'] ?? '')); - if ($sku !== '' && mb_strlen($sku) > 128) { - $errors[] = 'SKU produktu moze miec maksymalnie 128 znakow.'; - } - - $ean = trim((string) ($input['ean'] ?? '')); - if ($ean !== '' && !preg_match('/^[0-9]{8,14}$/', $ean)) { - $errors[] = 'EAN musi zawierac od 8 do 14 cyfr.'; - } - - $status = (string) ($input['status'] ?? '1'); - if (!in_array($status, ['0', '1'], true)) { - $errors[] = 'Status produktu jest niepoprawny.'; - } - - $promoted = (string) ($input['promoted'] ?? '0'); - if (!in_array($promoted, ['0', '1'], true)) { - $errors[] = 'Flaga promocji jest niepoprawna.'; - } - - $priceInputMode = (string) ($input['price_input_mode'] ?? 'brutto'); - if (!in_array($priceInputMode, ['brutto', 'netto'], true)) { - $errors[] = 'Tryb wprowadzania ceny jest niepoprawny.'; - } - - $vat = trim((string) ($input['vat'] ?? '')); - if ($vat !== '' && !is_numeric($vat)) { - $errors[] = 'Stawka VAT musi byc liczba.'; - } - - if ($vat !== '' && is_numeric($vat)) { - $vatValue = (float) $vat; - if ($vatValue < 0 || $vatValue > 100) { - $errors[] = 'Stawka VAT musi byc w zakresie 0-100.'; - } - } - - $quantity = trim((string) ($input['quantity'] ?? '0')); - if ($quantity === '' || !is_numeric($quantity) || (float) $quantity < 0) { - $errors[] = 'Stan magazynowy musi byc liczba >= 0.'; - } - - $weight = trim((string) ($input['weight'] ?? '')); - if ($weight !== '' && (!is_numeric($weight) || (float) $weight < 0)) { - $errors[] = 'Waga musi byc liczba >= 0.'; - } - - $priceBrutto = trim((string) ($input['price_brutto'] ?? '')); - $priceNetto = trim((string) ($input['price_netto'] ?? '')); - - if ($priceInputMode === 'brutto') { - if ($priceBrutto === '' || !is_numeric($priceBrutto) || (float) $priceBrutto < 0) { - $errors[] = 'Cena brutto jest wymagana i musi byc liczba >= 0.'; - } - } - - if ($priceInputMode === 'netto') { - if ($priceNetto === '' || !is_numeric($priceNetto) || (float) $priceNetto < 0) { - $errors[] = 'Cena netto jest wymagana i musi byc liczba >= 0.'; - } - } - - $priceBruttoPromo = trim((string) ($input['price_brutto_promo'] ?? '')); - $priceNettoPromo = trim((string) ($input['price_netto_promo'] ?? '')); - - if ($priceBruttoPromo !== '' && (!is_numeric($priceBruttoPromo) || (float) $priceBruttoPromo < 0)) { - $errors[] = 'Cena promocyjna brutto musi byc liczba >= 0.'; - } - - if ($priceNettoPromo !== '' && (!is_numeric($priceNettoPromo) || (float) $priceNettoPromo < 0)) { - $errors[] = 'Cena promocyjna netto musi byc liczba >= 0.'; - } - - return $errors; - } -} diff --git a/archive/2026-03-02_users-only-reset/src/Modules/Products/ProductsController.php b/archive/2026-03-02_users-only-reset/src/Modules/Products/ProductsController.php deleted file mode 100644 index c1882a0..0000000 --- a/archive/2026-03-02_users-only-reset/src/Modules/Products/ProductsController.php +++ /dev/null @@ -1,1164 +0,0 @@ - trim((string) $request->input('search', '')), - 'status' => (string) $request->input('status', ''), - 'type' => (string) $request->input('type', ''), - 'sort' => (string) $request->input('sort', 'created_at'), - 'sort_dir' => (string) $request->input('sort_dir', 'DESC'), - 'page' => max(1, (int) $request->input('page', 1)), - 'per_page' => max(1, min(100, (int) $request->input('per_page', 20))), - ]; - - $result = $this->products->paginate($filtersValues, 'pl'); - $totalPages = max(1, (int) ceil(((int) $result['total']) / (int) $result['per_page'])); - $rows = $this->tableRows((array) ($result['items'] ?? [])); - $shopProIntegrations = array_values(array_filter( - $this->integrations->listByType('shoppro'), - static fn (array $row): bool => ($row['has_api_key'] ?? false) === true - )); - - $html = $this->template->render('products/index', [ - 'title' => $this->translator->get('products.title'), - 'activeMenu' => 'products', - 'user' => $this->auth->user(), - 'csrfToken' => Csrf::token(), - 'marketplaceIntegrations' => $this->marketplaceIntegrations(), - 'shopProIntegrations' => $shopProIntegrations, - 'tableList' => [ - 'list_key' => 'products', - 'base_path' => '/products', - 'query' => $filtersValues, - 'create_url' => '/products/create', - 'create_label' => $this->translator->get('products.actions.add'), - 'header_actions' => [ - [ - 'type' => 'button', - 'label' => $this->translator->get('products.actions.import_shoppro'), - 'class' => 'btn btn--secondary', - 'attrs' => [ - 'data-open-modal' => 'product-import-modal', - ], - ], - [ - 'type' => 'button', - 'label' => $this->translator->get('products.actions.export_shoppro'), - 'class' => 'btn btn--secondary', - 'attrs' => [ - 'data-open-modal' => 'product-export-modal', - ], - ], - ], - 'filters' => [ - [ - 'key' => 'search', - 'label' => $this->translator->get('products.filters.search'), - 'type' => 'text', - 'value' => $filtersValues['search'], - ], - [ - 'key' => 'status', - 'label' => $this->translator->get('products.filters.status'), - 'type' => 'select', - 'value' => $filtersValues['status'], - 'options' => [ - '' => $this->translator->get('products.filters.any'), - '1' => $this->translator->get('products.status.active'), - '0' => $this->translator->get('products.status.inactive'), - ], - ], - [ - 'key' => 'type', - 'label' => $this->translator->get('products.filters.type'), - 'type' => 'select', - 'value' => $filtersValues['type'], - 'options' => [ - '' => $this->translator->get('products.filters.any'), - 'simple' => $this->translator->get('products.type.simple'), - 'variant_parent' => $this->translator->get('products.type.variant_parent'), - ], - ], - ], - 'columns' => [ - ['key' => 'id', 'label' => 'ID', 'sortable' => true, 'sort_key' => 'id'], - ['key' => 'name', 'label' => $this->translator->get('products.fields.name'), 'raw' => true, 'sortable' => true, 'sort_key' => 'name'], - ['key' => 'sku', 'label' => 'SKU', 'sortable' => true, 'sort_key' => 'sku'], - ['key' => 'ean', 'label' => 'EAN', 'sortable' => true, 'sort_key' => 'ean'], - ['key' => 'type_label', 'label' => $this->translator->get('products.fields.type')], - ['key' => 'price_brutto', 'label' => $this->translator->get('products.fields.price_brutto'), 'sortable' => true, 'sort_key' => 'price_brutto'], - ['key' => 'quantity', 'label' => $this->translator->get('products.fields.quantity'), 'sortable' => true, 'sort_key' => 'quantity'], - ['key' => 'status_label', 'label' => $this->translator->get('products.fields.status'), 'raw' => true, 'sortable' => true, 'sort_key' => 'status'], - ['key' => 'updated_at', 'label' => $this->translator->get('products.fields.updated_at'), 'sortable' => true, 'sort_key' => 'updated_at'], - ['key' => 'created_at', 'label' => $this->translator->get('products.fields.created_at'), 'sortable' => true, 'sort_key' => 'created_at'], - ], - 'selectable' => true, - 'select_name' => 'export_product_ids[]', - 'select_value_key' => 'id', - 'select_column_label' => $this->translator->get('products.export.select_column_label'), - 'rows' => $rows, - 'pagination' => [ - 'page' => (int) ($result['page'] ?? 1), - 'total_pages' => $totalPages, - 'total' => (int) ($result['total'] ?? 0), - 'per_page' => (int) ($result['per_page'] ?? 20), - ], - 'per_page_options' => [10, 20, 50, 100], - 'empty_message' => $this->translator->get('products.empty'), - 'show_actions' => true, - 'actions_label' => $this->translator->get('products.fields.actions'), - ], - 'errorMessage' => (string) Flash::get('products_error', ''), - 'successMessage' => (string) Flash::get('products_success', ''), - ], 'layouts/app'); - - return Response::html($html); - } - - public function create(Request $request): Response - { - $html = $this->template->render('products/create', [ - 'title' => $this->translator->get('products.create.title'), - 'activeMenu' => 'products', - 'user' => $this->auth->user(), - 'csrfToken' => Csrf::token(), - 'marketplaceIntegrations' => $this->marketplaceIntegrations(), - 'form' => $this->formDataFromFlash(), - 'errors' => (array) Flash::get('products_form_errors', []), - ], 'layouts/app'); - - return Response::html($html); - } - - public function store(Request $request): Response - { - $csrfToken = (string) $request->input('_token', ''); - if (!Csrf::validate($csrfToken)) { - Flash::set('products_error', $this->translator->get('auth.errors.csrf_expired')); - return Response::redirect('/products'); - } - - $payload = $this->payloadFromRequest($request); - Flash::set('products_form_old', $payload); - - $result = $this->service->create($payload, $this->auth->user()); - if (($result['ok'] ?? false) !== true) { - Flash::set('products_form_errors', (array) ($result['errors'] ?? [])); - return Response::redirect('/products/create'); - } - - Flash::set('products_form_old', []); - Flash::set('products_success', $this->translator->get('products.flash.created')); - - return Response::redirect('/products'); - } - - public function edit(Request $request): Response - { - $id = (int) $request->input('id', 0); - if ($id <= 0) { - Flash::set('products_error', $this->translator->get('products.flash.not_found')); - return Response::redirect('/products'); - } - - $product = $this->products->findById($id, 'pl'); - if ($product === null) { - Flash::set('products_error', $this->translator->get('products.flash.not_found')); - return Response::redirect('/products'); - } - - $form = $this->mergeOldWithProduct($product); - $productImages = $this->withPublicImageUrls($this->products->findImagesByProductId($id)); - - $activeIntegrations = $this->integrations->listByType('shoppro'); - $integrationTranslations = $this->products->findIntegrationTranslations($id); - - // Index integration translations by integration_id for easy lookup in view - $integrationTranslationsMap = []; - foreach ($integrationTranslations as $it) { - $integrationTranslationsMap[(int) $it['integration_id']] = $it; - } - - $html = $this->template->render('products/edit', [ - 'title' => $this->translator->get('products.edit.title', ['id' => (string) $id]), - 'activeMenu' => 'products', - 'user' => $this->auth->user(), - 'csrfToken' => Csrf::token(), - 'marketplaceIntegrations' => $this->marketplaceIntegrations(), - 'productId' => $id, - 'form' => $form, - 'productImages' => $productImages, - 'errors' => (array) Flash::get('products_form_errors', []), - 'activeIntegrations' => $activeIntegrations, - 'integrationTranslationsMap' => $integrationTranslationsMap, - ], 'layouts/app'); - - return Response::html($html); - } - - public function show(Request $request): Response - { - $id = (int) $request->input('id', 0); - if ($id <= 0) { - Flash::set('products_error', $this->translator->get('products.flash.not_found')); - return Response::redirect('/products'); - } - - $product = $this->products->findById($id, 'pl'); - if ($product === null) { - Flash::set('products_error', $this->translator->get('products.flash.not_found')); - return Response::redirect('/products'); - } - - $productImages = $this->withPublicImageUrls($this->products->findImagesByProductId($id)); - $productVariants = $this->products->findVariantsByProductId($id, 'pl'); - $importWarning = $this->products->findLatestImportWarning($id); - - $html = $this->template->render('products/show', [ - 'title' => $this->translator->get('products.show.title', ['id' => (string) $id]), - 'activeMenu' => 'products', - 'user' => $this->auth->user(), - 'csrfToken' => Csrf::token(), - 'marketplaceIntegrations' => $this->marketplaceIntegrations(), - 'productId' => $id, - 'product' => $product, - 'productImages' => $productImages, - 'productVariants' => $productVariants, - 'productImportWarning' => $importWarning, - 'errorMessage' => (string) Flash::get('products_error', ''), - 'successMessage' => (string) Flash::get('products_success', ''), - ], 'layouts/app'); - - return Response::html($html); - } - - public function links(Request $request): Response - { - $id = (int) $request->input('id', 0); - if ($id <= 0) { - Flash::set('products_error', $this->translator->get('products.flash.not_found')); - return Response::redirect('/products'); - } - - $product = $this->products->findById($id, 'pl'); - if ($product === null) { - Flash::set('products_error', $this->translator->get('products.flash.not_found')); - return Response::redirect('/products'); - } - - $linksIntegrationId = max(0, (int) $request->input('links_integration_id', 0)); - $linksQuery = trim((string) $request->input('links_query', '')); - $linksData = $this->productLinks->buildProductLinksViewData($id, $product, $linksIntegrationId, $linksQuery); - - $html = $this->template->render('products/links', [ - 'title' => $this->translator->get('products.links.page_title', ['id' => (string) $id]), - 'activeMenu' => 'products', - 'user' => $this->auth->user(), - 'csrfToken' => Csrf::token(), - 'marketplaceIntegrations' => $this->marketplaceIntegrations(), - 'productId' => $id, - 'product' => $product, - 'productLinks' => (array) ($linksData['links'] ?? []), - 'productLinkEventsByMap' => (array) ($linksData['link_events_by_map'] ?? []), - 'linkIntegrations' => (array) ($linksData['integrations'] ?? []), - 'selectedLinksIntegrationId' => (int) ($linksData['selected_integration_id'] ?? 0), - 'linksQuery' => (string) ($linksData['search_query'] ?? ''), - 'linkOffers' => (array) ($linksData['offers'] ?? []), - 'linksErrorMessage' => (string) Flash::get('product_links_error', ''), - 'linksSuccessMessage' => (string) Flash::get('product_links_success', ''), - ], 'layouts/app'); - - return Response::html($html); - } - - public function linkSuggestions(Request $request): Response - { - $id = (int) $request->input('id', 0); - if ($id <= 0) { - return Response::json([ - 'ok' => false, - 'message' => $this->translator->get('products.flash.not_found'), - ], 404); - } - - $product = $this->products->findById($id, 'pl'); - if ($product === null) { - return Response::json([ - 'ok' => false, - 'message' => $this->translator->get('products.flash.not_found'), - ], 404); - } - - $linksIntegrationId = max(0, (int) $request->input('links_integration_id', 0)); - $linksQuery = trim((string) $request->input('links_query', '')); - $linksData = $this->productLinks->buildProductLinksViewData($id, $product, $linksIntegrationId, $linksQuery); - - return Response::json([ - 'ok' => true, - 'product_id' => $id, - 'integration_id' => (int) ($linksData['selected_integration_id'] ?? 0), - 'offers' => array_values(array_filter( - (array) ($linksData['offers'] ?? []), - static fn (array $offer): bool => (int) ($offer['match_confidence'] ?? 0) > 0 - )), - ]); - } - - public function nextSku(Request $request): Response - { - $csrfToken = (string) $request->input('_token', ''); - if (!Csrf::validate($csrfToken)) { - return Response::json([ - 'ok' => false, - 'message' => $this->translator->get('auth.errors.csrf_expired'), - ], 419); - } - - try { - return Response::json([ - 'ok' => true, - 'sku' => $this->skuGenerator->nextSku(), - ]); - } catch (\Throwable $exception) { - return Response::json([ - 'ok' => false, - 'message' => $this->translator->get('products.sku_generator.failed') . ' ' . $exception->getMessage(), - ], 422); - } - } - - public function exportShopPro(Request $request): Response - { - $csrfToken = (string) $request->input('_token', ''); - if (!Csrf::validate($csrfToken)) { - Flash::set('products_error', $this->translator->get('auth.errors.csrf_expired')); - return Response::redirect('/products'); - } - - $integrationId = max(0, (int) $request->input('integration_id', 0)); - $exportMode = (string) $request->input('export_mode', 'simple'); - $selectedIds = $this->normalizeIntArray($request->input('export_product_ids', [])); - - if ($integrationId <= 0) { - Flash::set('products_error', $this->translator->get('products.export.flash.integration_required')); - return Response::redirect('/products'); - } - - if (!in_array($exportMode, ['simple', 'variant'], true)) { - Flash::set('products_error', $this->translator->get('products.export.flash.mode_invalid')); - return Response::redirect('/products'); - } - - if ($selectedIds === []) { - Flash::set('products_error', $this->translator->get('products.export.flash.no_products_selected')); - return Response::redirect('/products'); - } - - try { - $credentials = $this->integrations->findApiCredentials($integrationId); - } catch (\Throwable $exception) { - Flash::set('products_error', $this->translator->get('products.export.flash.failed') . ' ' . $exception->getMessage()); - return Response::redirect('/products'); - } - - if ($credentials === null) { - Flash::set('products_error', $this->translator->get('products.export.flash.integration_not_found')); - return Response::redirect('/products'); - } - - $apiKey = (string) ($credentials['api_key'] ?? ''); - if ($apiKey === '') { - Flash::set('products_error', $this->translator->get('products.export.flash.api_key_missing')); - return Response::redirect('/products'); - } - - $result = $this->shopProExport->exportProducts($selectedIds, $credentials, $exportMode, $this->auth->user()); - - $summary = $this->translator->get('products.export.flash.done', [ - 'exported' => (string) ($result['exported'] ?? 0), - 'failed' => (string) ($result['failed'] ?? 0), - 'mode' => $exportMode === 'variant' - ? $this->translator->get('products.export.mode_variant') - : $this->translator->get('products.export.mode_simple'), - ]); - - $errors = is_array($result['errors'] ?? null) ? $result['errors'] : []; - if ($errors !== []) { - Flash::set('products_error', $summary . ' ' . implode(' | ', $errors)); - } else { - Flash::set('products_success', $summary); - } - - return Response::redirect('/products'); - } - - public function assignGs1Ean(Request $request): Response - { - $csrfToken = (string) $request->input('_token', ''); - if (!Csrf::validate($csrfToken)) { - Flash::set('products_error', $this->translator->get('auth.errors.csrf_expired')); - return Response::redirect('/products'); - } - - $id = (int) $request->input('id', 0); - if ($id <= 0) { - Flash::set('products_error', $this->translator->get('products.flash.not_found')); - return Response::redirect('/products'); - } - - try { - $result = $this->gs1Service->assignEanToProduct($id); - Flash::set('products_success', $this->translator->get('products.gs1.ean_assigned', [ - 'ean' => $result['ean'], - ])); - } catch (\Throwable $e) { - Flash::set('products_error', $this->translator->get('products.gs1.error') . ' ' . $e->getMessage()); - } - - return Response::redirect('/products/' . $id); - } - public function update(Request $request): Response - { - $csrfToken = (string) $request->input('_token', ''); - if (!Csrf::validate($csrfToken)) { - Flash::set('products_error', $this->translator->get('auth.errors.csrf_expired')); - return Response::redirect('/products'); - } - - $id = (int) $request->input('id', 0); - if ($id <= 0) { - Flash::set('products_error', $this->translator->get('products.flash.not_found')); - return Response::redirect('/products'); - } - - $payload = $this->payloadFromRequest($request); - Flash::set('products_form_old', $payload); - - $result = $this->service->update($id, $payload, $this->auth->user()); - if (($result['ok'] ?? false) !== true) { - Flash::set('products_form_errors', (array) ($result['errors'] ?? [])); - return Response::redirect('/products/edit?id=' . $id); - } - - $imageResult = $this->applyImageChanges($id, $request); - if (($imageResult['ok'] ?? false) !== true) { - $errors = (array) ($imageResult['errors'] ?? []); - if ($errors !== []) { - Flash::set('products_error', implode(' ', $errors)); - } - } - - // Save per-integration content overrides - $allowedIntegrationIds = array_map( - static fn (array $i): int => (int) ($i['id'] ?? 0), - $this->integrations->listByType('shoppro') - ); - $integrationContent = $request->input('integration_content', []); - if (is_array($integrationContent)) { - foreach ($integrationContent as $rawIntegrationId => $content) { - $integrationId = (int) $rawIntegrationId; - if ($integrationId <= 0 || !is_array($content) || !in_array($integrationId, $allowedIntegrationIds, true)) { - continue; - } - $this->products->upsertIntegrationTranslation( - $id, - $integrationId, - isset($content['name']) ? trim((string) $content['name']) : null, - isset($content['short_description']) ? trim((string) $content['short_description']) : null, - isset($content['description']) ? trim((string) $content['description']) : null - ); - } - } - - Flash::set('products_form_old', []); - Flash::set('products_success', $this->translator->get('products.flash.updated')); - - return Response::redirect('/products'); - } - - public function uploadImage(Request $request): Response - { - $csrfToken = (string) $request->input('_token', ''); - if (!Csrf::validate($csrfToken)) { - return Response::json([ - 'ok' => false, - 'message' => $this->translator->get('auth.errors.csrf_expired'), - ], 419); - } - - $productId = (int) $request->input('id', 0); - if ($productId <= 0 || $this->products->findById($productId, 'pl') === null) { - return Response::json(['ok' => false, 'message' => $this->translator->get('products.flash.not_found')], 404); - } - - $uploadedImages = $this->normalizeUploadedFiles($request->file('new_images')); - if ($uploadedImages === []) { - return Response::json(['ok' => false, 'message' => 'Nie wybrano pliku do uploadu.'], 422); - } - - $currentImages = $this->products->findImagesByProductId($productId); - $nextSortOrder = $currentImages === [] - ? 0 - : ((int) max(array_column($currentImages, 'sort_order')) + 1); - $setFirstAsMain = $currentImages === []; - $created = []; - $errors = []; - - foreach ($uploadedImages as $imageFile) { - if ((int) ($imageFile['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) { - continue; - } - - $saved = $this->saveUploadedImageFile($productId, $imageFile); - if (($saved['ok'] ?? false) !== true) { - $errors[] = (string) ($saved['error'] ?? 'Nie udalo sie zapisac pliku.'); - continue; - } - - $imageId = $this->products->createImage( - $productId, - (string) ($saved['storage_path'] ?? ''), - null, - $nextSortOrder, - $setFirstAsMain ? 1 : 0 - ); - $nextSortOrder++; - $setFirstAsMain = false; - - $inserted = $this->products->findImagesByProductId($productId); - foreach ($inserted as $row) { - if ((int) ($row['id'] ?? 0) === $imageId) { - $created[] = $this->mapImageForApi($row); - break; - } - } - } - - if ($created === []) { - return Response::json([ - 'ok' => false, - 'message' => $errors !== [] ? implode(' ', $errors) : 'Nie udalo sie dodac zdjec.', - ], 422); - } - - return Response::json([ - 'ok' => true, - 'images' => $created, - 'message' => $errors === [] ? '' : implode(' ', $errors), - ]); - } - - public function setMainImage(Request $request): Response - { - $csrfToken = (string) $request->input('_token', ''); - if (!Csrf::validate($csrfToken)) { - return Response::json([ - 'ok' => false, - 'message' => $this->translator->get('auth.errors.csrf_expired'), - ], 419); - } - - $productId = (int) $request->input('id', 0); - $imageId = (int) $request->input('image_id', 0); - if ($productId <= 0 || $imageId <= 0) { - return Response::json(['ok' => false, 'message' => 'Niepoprawne dane zdjecia.'], 422); - } - - $images = $this->products->findImagesByProductId($productId); - $exists = false; - foreach ($images as $image) { - if ((int) ($image['id'] ?? 0) === $imageId) { - $exists = true; - break; - } - } - - if (!$exists) { - return Response::json(['ok' => false, 'message' => 'Nie znaleziono wskazanego zdjecia.'], 404); - } - - $this->products->setMainImage($productId, $imageId); - - return Response::json([ - 'ok' => true, - 'image_id' => $imageId, - ]); - } - - public function deleteImage(Request $request): Response - { - $csrfToken = (string) $request->input('_token', ''); - if (!Csrf::validate($csrfToken)) { - return Response::json([ - 'ok' => false, - 'message' => $this->translator->get('auth.errors.csrf_expired'), - ], 419); - } - - $productId = (int) $request->input('id', 0); - $imageId = (int) $request->input('image_id', 0); - if ($productId <= 0 || $imageId <= 0) { - return Response::json(['ok' => false, 'message' => 'Niepoprawne dane zdjecia.'], 422); - } - - $deletedPath = $this->products->deleteImageById($productId, $imageId); - if ($deletedPath === null) { - return Response::json(['ok' => false, 'message' => 'Nie znaleziono wskazanego zdjecia.'], 404); - } - - $this->deleteLocalImageFile($deletedPath); - - $remaining = $this->products->findImagesByProductId($productId); - $hasMain = false; - foreach ($remaining as $row) { - if ((int) ($row['is_main'] ?? 0) === 1) { - $hasMain = true; - break; - } - } - - $newMainId = 0; - if (!$hasMain && $remaining !== []) { - $newMainId = (int) ($remaining[0]['id'] ?? 0); - if ($newMainId > 0) { - $this->products->setMainImage($productId, $newMainId); - } - } - - return Response::json([ - 'ok' => true, - 'deleted_id' => $imageId, - 'main_image_id' => $newMainId, - ]); - } - - public function destroy(Request $request): Response - { - $csrfToken = (string) $request->input('_token', ''); - if (!Csrf::validate($csrfToken)) { - Flash::set('products_error', $this->translator->get('auth.errors.csrf_expired')); - return Response::redirect('/products'); - } - - $id = (int) $request->input('id', 0); - if ($id <= 0) { - Flash::set('products_error', $this->translator->get('products.flash.not_found')); - return Response::redirect('/products'); - } - - $result = $this->service->delete($id, $this->auth->user()); - if (($result['ok'] ?? false) !== true) { - $errors = (array) ($result['errors'] ?? []); - Flash::set('products_error', $errors !== [] ? (string) $errors[0] : $this->translator->get('products.flash.delete_failed')); - return Response::redirect('/products'); - } - - Flash::set('products_success', $this->translator->get('products.flash.deleted')); - - return Response::redirect('/products'); - } - - /** - * @return array - */ - private function payloadFromRequest(Request $request): array - { - return [ - 'type' => (string) $request->input('type', 'simple'), - 'name' => (string) $request->input('name', ''), - 'sku' => (string) $request->input('sku', ''), - 'ean' => (string) $request->input('ean', ''), - 'status' => (string) $request->input('status', '1'), - 'promoted' => (string) $request->input('promoted', '0'), - 'vat' => (string) $request->input('vat', '23'), - 'weight' => (string) $request->input('weight', ''), - 'quantity' => (string) $request->input('quantity', '0'), - 'price_input_mode' => (string) $request->input('price_input_mode', 'brutto'), - 'price_brutto' => (string) $request->input('price_brutto', ''), - 'price_netto' => (string) $request->input('price_netto', ''), - 'price_brutto_promo' => (string) $request->input('price_brutto_promo', ''), - 'price_netto_promo' => (string) $request->input('price_netto_promo', ''), - 'short_description' => (string) $request->input('short_description', ''), - 'description' => (string) $request->input('description', ''), - 'meta_title' => (string) $request->input('meta_title', ''), - 'meta_description' => (string) $request->input('meta_description', ''), - 'meta_keywords' => (string) $request->input('meta_keywords', ''), - 'seo_link' => (string) $request->input('seo_link', ''), - ]; - } - - /** - * @return array - */ - private function formDataFromFlash(): array - { - $old = (array) Flash::get('products_form_old', []); - - return array_merge([ - 'type' => 'simple', - 'name' => '', - 'sku' => '', - 'ean' => '', - 'status' => '1', - 'promoted' => '0', - 'vat' => '23', - 'weight' => '', - 'quantity' => '0', - 'price_input_mode' => 'brutto', - 'price_brutto' => '', - 'price_netto' => '', - 'price_brutto_promo' => '', - 'price_netto_promo' => '', - 'short_description' => '', - 'description' => '', - 'meta_title' => '', - 'meta_description' => '', - 'meta_keywords' => '', - 'seo_link' => '', - ], $old); - } - - /** - * @param array $product - * @return array - */ - private function mergeOldWithProduct(array $product): array - { - $old = (array) Flash::get('products_form_old', []); - - $base = [ - 'type' => (string) ($product['type'] ?? 'simple'), - 'name' => (string) ($product['name'] ?? ''), - 'sku' => (string) ($product['sku'] ?? ''), - 'ean' => (string) ($product['ean'] ?? ''), - 'status' => (string) ($product['status'] ?? '1'), - 'promoted' => (string) ($product['promoted'] ?? '0'), - 'vat' => (string) ($product['vat'] ?? ''), - 'weight' => (string) ($product['weight'] ?? ''), - 'quantity' => (string) ($product['quantity'] ?? '0'), - 'price_input_mode' => 'brutto', - 'price_brutto' => (string) ($product['price_brutto'] ?? ''), - 'price_netto' => (string) ($product['price_netto'] ?? ''), - 'price_brutto_promo' => (string) ($product['price_brutto_promo'] ?? ''), - 'price_netto_promo' => (string) ($product['price_netto_promo'] ?? ''), - 'short_description' => (string) ($product['short_description'] ?? ''), - 'description' => (string) ($product['description'] ?? ''), - 'meta_title' => (string) ($product['meta_title'] ?? ''), - 'meta_description' => (string) ($product['meta_description'] ?? ''), - 'meta_keywords' => (string) ($product['meta_keywords'] ?? ''), - 'seo_link' => (string) ($product['seo_link'] ?? ''), - ]; - - return array_merge($base, $old); - } - - /** - * @param array> $items - * @return array> - */ - private function tableRows(array $items): array - { - return array_map(function (array $row): array { - $id = (int) ($row['id'] ?? 0); - $isActive = (int) ($row['status'] ?? 0) === 1; - $type = (string) ($row['type'] ?? 'simple'); - - return [ - 'id' => $id, - 'name' => $this->renderProductNameCell((string) ($row['name'] ?? ''), (string) ($row['main_image_path'] ?? '')), - 'sku' => (string) ($row['sku'] ?? ''), - 'ean' => (string) ($row['ean'] ?? ''), - 'type_label' => $type === 'variant_parent' - ? $this->translator->get('products.type.variant_parent') - : $this->translator->get('products.type.simple'), - 'price_brutto' => number_format((float) ($row['price_brutto'] ?? 0), 2, '.', ''), - 'quantity' => number_format((float) ($row['quantity'] ?? 0), 3, '.', ''), - 'status_label' => sprintf( - '%s', - $isActive ? ' is-active' : '', - htmlspecialchars( - $isActive - ? $this->translator->get('products.status.active') - : $this->translator->get('products.status.inactive'), - ENT_QUOTES, - 'UTF-8' - ) - ), - 'updated_at' => (string) ($row['updated_at'] ?? ''), - 'created_at' => (string) ($row['created_at'] ?? ''), - '_actions' => [ - [ - 'label' => $this->translator->get('products.actions.preview'), - 'url' => '/products/' . $id, - 'class' => 'btn btn--secondary', - ], - [ - 'label' => $this->translator->get('products.actions.links'), - 'url' => '/products/' . $id . '/links', - 'class' => 'btn btn--secondary', - ], - [ - 'label' => $this->translator->get('products.actions.edit'), - 'url' => '/products/edit?id=' . $id, - 'class' => 'btn btn--secondary', - ], - [ - 'label' => $this->translator->get('products.actions.delete'), - 'url' => '/products/delete', - 'class' => 'btn btn--danger', - 'method' => 'post', - 'confirm' => $this->translator->get('products.confirm.delete', ['id' => (string) $id]), - 'params' => [ - 'id' => (string) $id, - ], - ], - ], - ]; - }, $items); - } - - private function renderProductNameCell(string $name, string $mainImagePath): string - { - $safeName = htmlspecialchars($name, ENT_QUOTES, 'UTF-8'); - $imageUrl = $this->publicImageUrl($mainImagePath); - $safeImageUrl = htmlspecialchars($imageUrl, ENT_QUOTES, 'UTF-8'); - - if ($safeImageUrl === '') { - return '
' . $safeName . '
'; - } - - return '
' . $safeName . '
'; - } - - /** - * @return array{ok:bool,errors:array} - */ - private function applyImageChanges(int $productId, Request $request): array - { - $errors = []; - $removedPaths = []; - $newMainChoice = trim((string) $request->input('main_image_choice', '')); - $newlyInsertedIds = []; - - $removeImageIds = $this->normalizeIntArray($request->input('remove_image_ids', [])); - foreach ($removeImageIds as $imageId) { - $storagePath = $this->products->deleteImageById($productId, $imageId); - if ($storagePath !== null) { - $removedPaths[] = $storagePath; - } - } - - $uploadedImages = $this->normalizeUploadedFiles($request->file('new_images')); - if ($uploadedImages !== []) { - $currentImages = $this->products->findImagesByProductId($productId); - $nextSortOrder = $currentImages === [] - ? 0 - : ((int) max(array_column($currentImages, 'sort_order')) + 1); - - foreach ($uploadedImages as $uploadIndex => $imageFile) { - if ((int) ($imageFile['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) { - continue; - } - - $saved = $this->saveUploadedImageFile($productId, $imageFile); - if (($saved['ok'] ?? false) !== true) { - $message = (string) ($saved['error'] ?? 'Nie udalo sie zapisac pliku.'); - $errors[] = $message; - continue; - } - - $imageId = $this->products->createImage( - $productId, - (string) ($saved['storage_path'] ?? ''), - null, - $nextSortOrder, - 0 - ); - $newlyInsertedIds[$uploadIndex] = $imageId; - $nextSortOrder++; - } - } - - $allImages = $this->products->findImagesByProductId($productId); - if ($allImages !== []) { - $selectedMainId = $this->resolveMainImageId($newMainChoice, $allImages, $newlyInsertedIds); - if ($selectedMainId === null) { - $currentMain = array_values(array_filter( - $allImages, - static fn (array $row): bool => ((int) ($row['is_main'] ?? 0)) === 1 - )); - $selectedMainId = $currentMain !== [] ? (int) ($currentMain[0]['id'] ?? 0) : null; - } - - if ($selectedMainId === null || $selectedMainId <= 0) { - $selectedMainId = (int) ($allImages[0]['id'] ?? 0); - } - - if ($selectedMainId > 0) { - $this->products->setMainImage($productId, $selectedMainId); - } - } - - foreach ($removedPaths as $path) { - $this->deleteLocalImageFile($path); - } - - return [ - 'ok' => $errors === [], - 'errors' => $errors, - ]; - } - - /** - * @param array> $images - * @return array> - */ - private function withPublicImageUrls(array $images): array - { - return array_map(function (array $image): array { - $storagePath = trim((string) ($image['storage_path'] ?? '')); - $image['public_url'] = $this->publicImageUrl($storagePath); - return $image; - }, $images); - } - - private function publicImageUrl(string $storagePath): string - { - $path = trim($storagePath); - if ($path === '') { - return ''; - } - - if (preg_match('#^https?://#i', $path) === 1 || str_starts_with($path, '//')) { - return $path; - } - - return '/' . ltrim(str_replace('\\', '/', $path), '/'); - } - - /** - * @param array|null $input - * @return array - */ - private function normalizeIntArray(mixed $input): array - { - if (!is_array($input)) { - return []; - } - - $values = []; - foreach ($input as $value) { - if (is_scalar($value) && is_numeric((string) $value)) { - $parsed = (int) $value; - if ($parsed > 0) { - $values[] = $parsed; - } - } - } - - return array_values(array_unique($values)); - } - - /** - * @param mixed $rawFiles - * @return array> - */ - private function normalizeUploadedFiles(mixed $rawFiles): array - { - if (!is_array($rawFiles) || !isset($rawFiles['name'])) { - return []; - } - - $normalized = []; - if (is_array($rawFiles['name'])) { - foreach ($rawFiles['name'] as $index => $name) { - $normalized[] = [ - 'name' => (string) $name, - 'type' => (string) ($rawFiles['type'][$index] ?? ''), - 'tmp_name' => (string) ($rawFiles['tmp_name'][$index] ?? ''), - 'error' => (int) ($rawFiles['error'][$index] ?? UPLOAD_ERR_NO_FILE), - 'size' => (int) ($rawFiles['size'][$index] ?? 0), - ]; - } - - return $normalized; - } - - return [[ - 'name' => (string) ($rawFiles['name'] ?? ''), - 'type' => (string) ($rawFiles['type'] ?? ''), - 'tmp_name' => (string) ($rawFiles['tmp_name'] ?? ''), - 'error' => (int) ($rawFiles['error'] ?? UPLOAD_ERR_NO_FILE), - 'size' => (int) ($rawFiles['size'] ?? 0), - ]]; - } - - /** - * @param array $file - * @return array{ok:bool,storage_path?:string,error?:string} - */ - private function saveUploadedImageFile(int $productId, array $file): array - { - $error = (int) ($file['error'] ?? UPLOAD_ERR_NO_FILE); - if ($error === UPLOAD_ERR_NO_FILE) { - return ['ok' => false, 'error' => 'Nie wybrano pliku do uploadu.']; - } - if ($error !== UPLOAD_ERR_OK) { - return ['ok' => false, 'error' => 'Upload obrazu zakonczyl sie bledem.']; - } - - $originalName = trim((string) ($file['name'] ?? '')); - $tmpFile = (string) ($file['tmp_name'] ?? ''); - if ($tmpFile === '' || !is_uploaded_file($tmpFile)) { - return ['ok' => false, 'error' => 'Niepoprawny plik tymczasowy obrazu.']; - } - - $extension = strtolower((string) pathinfo($originalName, PATHINFO_EXTENSION)); - if (!in_array($extension, ['jpg', 'jpeg', 'png', 'webp', 'gif'], true)) { - return ['ok' => false, 'error' => 'Dozwolone formaty obrazow: JPG, PNG, WEBP, GIF.']; - } - - $imageInfo = @getimagesize($tmpFile); - if (!is_array($imageInfo)) { - return ['ok' => false, 'error' => 'Plik nie jest poprawnym obrazem.']; - } - - $projectRoot = dirname(__DIR__, 3); - $relativeDir = 'uploads/products/' . $productId; - $targetDir = $projectRoot . '/public/' . $relativeDir; - if (!is_dir($targetDir) && !mkdir($targetDir, 0775, true) && !is_dir($targetDir)) { - return ['ok' => false, 'error' => 'Nie mozna utworzyc katalogu na obrazy.']; - } - - $safeBaseName = preg_replace('/[^a-zA-Z0-9_-]+/', '-', (string) pathinfo($originalName, PATHINFO_FILENAME)); - $safeBaseName = trim((string) $safeBaseName, '-'); - if ($safeBaseName === '') { - $safeBaseName = 'image'; - } - - $fileName = sprintf('%s-%s.%s', $safeBaseName, bin2hex(random_bytes(6)), $extension); - $targetPath = $targetDir . '/' . $fileName; - - if (!move_uploaded_file($tmpFile, $targetPath)) { - return ['ok' => false, 'error' => 'Nie mozna zapisac obrazu na serwerze.']; - } - - return [ - 'ok' => true, - 'storage_path' => $relativeDir . '/' . $fileName, - ]; - } - - /** - * @param array> $images - * @param array $newlyInsertedIds - */ - private function resolveMainImageId(string $choice, array $images, array $newlyInsertedIds): ?int - { - $imageIds = array_map(static fn (array $row): int => (int) ($row['id'] ?? 0), $images); - if ($choice === '') { - return null; - } - - if (preg_match('/^existing:(\d+)$/', $choice, $match) === 1) { - $id = (int) $match[1]; - return in_array($id, $imageIds, true) ? $id : null; - } - - if (preg_match('/^new:(\d+)$/', $choice, $match) === 1) { - $newIndex = (int) $match[1]; - return $newlyInsertedIds[$newIndex] ?? null; - } - - return null; - } - - private function deleteLocalImageFile(string $storagePath): void - { - $path = trim($storagePath); - if ($path === '' || preg_match('#^https?://#i', $path) === 1 || str_starts_with($path, '//')) { - return; - } - - $projectRoot = dirname(__DIR__, 3); - $filePath = $projectRoot . '/public/' . ltrim(str_replace('\\', '/', $path), '/'); - $realFilePath = realpath($filePath); - $realPublicPath = realpath($projectRoot . '/public'); - if ($realFilePath === false || $realPublicPath === false) { - return; - } - - if (!str_starts_with($realFilePath, $realPublicPath . DIRECTORY_SEPARATOR)) { - return; - } - - if (is_file($realFilePath)) { - @unlink($realFilePath); - } - } - - /** - * @param array $image - * @return array - */ - private function mapImageForApi(array $image): array - { - $storagePath = (string) ($image['storage_path'] ?? ''); - - return [ - 'id' => (int) ($image['id'] ?? 0), - 'storage_path' => $storagePath, - 'alt' => (string) ($image['alt'] ?? ''), - 'sort_order' => (int) ($image['sort_order'] ?? 0), - 'is_main' => (int) ($image['is_main'] ?? 0), - 'public_url' => $this->publicImageUrl($storagePath), - ]; - } - - /** - * @return array> - */ - private function marketplaceIntegrations(): array - { - return array_values(array_filter( - $this->integrations->listByType('shoppro'), - static fn (array $row): bool => (bool) ($row['is_active'] ?? false) - )); - } -} - - - - - diff --git a/archive/2026-03-02_users-only-reset/src/Modules/Products/ShopProExportService.php b/archive/2026-03-02_users-only-reset/src/Modules/Products/ShopProExportService.php deleted file mode 100644 index b3486db..0000000 --- a/archive/2026-03-02_users-only-reset/src/Modules/Products/ShopProExportService.php +++ /dev/null @@ -1,1201 +0,0 @@ - $productIds - * @param array $credentials - * @param array|null $actor - * @return array{ok:bool,exported:int,failed:int,errors:array} - */ - public function exportProducts(array $productIds, array $credentials, string $mode, ?array $actor): array - { - $integrationId = max(0, (int) ($credentials['id'] ?? 0)); - $baseUrl = (string) ($credentials['base_url'] ?? ''); - $apiKey = (string) ($credentials['api_key'] ?? ''); - $timeout = (int) ($credentials['timeout_seconds'] ?? 10); - - $uniqueIds = array_values(array_unique(array_filter($productIds, static fn (int $id): bool => $id > 0))); - $exported = 0; - $failed = 0; - $errors = []; - - $this->integrations->ensureSalesChannelsSeeded(); - - foreach ($uniqueIds as $productId) { - $this->logExportStep($productId, $actor, 'start', [ - 'mode' => $mode, - 'integration_id' => $integrationId, - ]); - - $result = $mode === 'variant' - ? $this->exportVariantProduct($productId, $integrationId, $baseUrl, $apiKey, $timeout, $actor) - : $this->exportSimpleProduct($productId, $integrationId, $baseUrl, $apiKey, $timeout, $actor); - - if (($result['ok'] ?? false) === true) { - $exported++; - $this->logExportStep($productId, $actor, 'done', [ - 'mode' => $mode, - 'integration_id' => $integrationId, - ]); - continue; - } - - $failed++; - $this->logExportStep($productId, $actor, 'failed', [ - 'mode' => $mode, - 'integration_id' => $integrationId, - 'message' => (string) ($result['message'] ?? 'Blad eksportu.'), - ]); - if (count($errors) < 5) { - $errors[] = 'ID ' . $productId . ': ' . (string) ($result['message'] ?? 'Blad eksportu.'); - } - } - - return [ - 'ok' => $failed === 0, - 'exported' => $exported, - 'failed' => $failed, - 'errors' => $errors, - ]; - } - - /** - * @return array{ok:bool,message:string,external_product_id?:int} - */ - private function exportSimpleProduct( - int $productId, - int $integrationId, - string $baseUrl, - string $apiKey, - int $timeout, - ?array $actor - ): array { - $product = $this->products->findById($productId, 'pl'); - if ($product === null) { - $this->logExportStep($productId, $actor, 'load_product_failed', [ - 'message' => 'Produkt nie istnieje.', - ]); - return ['ok' => false, 'message' => 'Produkt nie istnieje.']; - } - - $integrationTranslation = $this->loadIntegrationTranslation($productId, $integrationId); - $payload = $this->mapProductPayload($product, $baseUrl, $apiKey, $timeout, $integrationTranslation); - $customFieldsPayload = $this->mapCustomFieldsPayload($product['custom_fields_json'] ?? null); - if (($customFieldsPayload['include'] ?? false) === true) { - $payload['custom_fields'] = $customFieldsPayload['fields'] ?? []; - } - - $this->logExportStep($productId, $actor, 'custom_fields_prepared', [ - 'include' => (bool) ($customFieldsPayload['include'] ?? false), - 'count' => is_array($customFieldsPayload['fields'] ?? null) ? count((array) $customFieldsPayload['fields']) : 0, - 'warning' => (string) ($customFieldsPayload['warning'] ?? ''), - ]); - - $gpsrValue = trim((string) (($payload['languages']['pl']['security_information'] ?? null) ?? '')); - $this->logExportStep($productId, $actor, 'producer_gpsr_prepared', [ - 'producer_name' => trim((string) ($product['producer_name'] ?? '')), - 'resolved_producer_id' => isset($payload['producer_id']) && is_numeric((string) $payload['producer_id']) - ? (int) $payload['producer_id'] - : null, - 'gpsr_present' => $gpsrValue !== '', - ]); - $externalProductId = $this->findMappedExternalProductId($productId, $integrationId); - - if ($externalProductId > 0) { - $update = $this->shopProClient->updateProduct($baseUrl, $apiKey, $timeout, $externalProductId, $payload); - if (($update['ok'] ?? false) !== true) { - if ((int) ($update['http_code'] ?? 0) === 404) { - $externalProductId = 0; - } else { - $this->logExportStep($productId, $actor, 'update_failed', [ - 'external_product_id' => $externalProductId, - 'http_code' => (int) ($update['http_code'] ?? 0), - 'message' => (string) ($update['message'] ?? ''), - ]); - return ['ok' => false, 'message' => (string) ($update['message'] ?? 'Nie mozna zaktualizowac produktu w shopPRO.')]; - } - } - } - - if ($externalProductId <= 0) { - $create = $this->shopProClient->createProduct($baseUrl, $apiKey, $timeout, $payload); - if (($create['ok'] ?? false) !== true) { - $this->logExportStep($productId, $actor, 'create_failed', [ - 'http_code' => (int) ($create['http_code'] ?? 0), - 'message' => (string) ($create['message'] ?? ''), - ]); - return ['ok' => false, 'message' => (string) ($create['message'] ?? 'Nie mozna utworzyc produktu w shopPRO.')]; - } - $externalProductId = (int) ($create['external_id'] ?? 0); - } - - if ($externalProductId <= 0) { - $this->logExportStep($productId, $actor, 'external_id_missing', []); - return ['ok' => false, 'message' => 'Brak ID produktu shopPRO po eksporcie.']; - } - - $imageSync = $this->syncProductImages($productId, $externalProductId, $baseUrl, $apiKey, $timeout, $actor); - if (($imageSync['ok'] ?? false) !== true) { - return [ - 'ok' => false, - 'message' => (string) ($imageSync['message'] ?? 'Nie udalo sie zsynchronizowac zdjec produktu.'), - ]; - } - - $this->integrations->upsertProductChannelMap( - $productId, - 'shoppro', - 'listed', - (string) $externalProductId, - '', - $integrationId > 0 ? $integrationId : null - ); - $this->integrations->upsertProductChannelMap($productId, 'allegro', 'unknown', '', ''); - $this->integrations->upsertProductChannelMap($productId, 'erli', 'not_available', '', ''); - - $this->products->logChange( - $productId, - isset($actor['id']) ? (int) $actor['id'] : null, - 'product_export_shoppro', - null, - [ - 'mode' => 'simple', - 'integration_id' => $integrationId, - 'external_product_id' => $externalProductId, - 'images_uploaded' => (int) ($imageSync['uploaded'] ?? 0), - 'images_skipped' => (int) ($imageSync['skipped'] ?? 0), - 'custom_fields_included' => (bool) ($customFieldsPayload['include'] ?? false), - 'custom_fields_count' => is_array($customFieldsPayload['fields'] ?? null) ? count((array) $customFieldsPayload['fields']) : 0, - ] - ); - - return [ - 'ok' => true, - 'message' => '', - 'external_product_id' => $externalProductId, - ]; - } - - /** - * @return array{ok:bool,message:string} - */ - private function exportVariantProduct( - int $productId, - int $integrationId, - string $baseUrl, - string $apiKey, - int $timeout, - ?array $actor - ): array { - $product = $this->products->findById($productId, 'pl'); - if ($product === null) { - $this->logExportStep($productId, $actor, 'variant_load_product_failed', [ - 'message' => 'Produkt nie istnieje.', - ]); - return ['ok' => false, 'message' => 'Produkt nie istnieje.']; - } - - $variants = $this->products->findVariantsByProductId($productId, 'pl'); - if ($variants === []) { - $this->logExportStep($productId, $actor, 'variant_no_variants', []); - return ['ok' => false, 'message' => 'Produkt nie ma wariantow do eksportu.']; - } - - $parentResult = $this->exportSimpleProduct($productId, $integrationId, $baseUrl, $apiKey, $timeout, null); - if (($parentResult['ok'] ?? false) !== true) { - $this->logExportStep($productId, $actor, 'variant_parent_export_failed', [ - 'message' => (string) ($parentResult['message'] ?? ''), - ]); - return ['ok' => false, 'message' => (string) ($parentResult['message'] ?? 'Nie mozna wyeksportowac produktu nadrzednego.')]; - } - - $externalProductId = (int) ($parentResult['external_product_id'] ?? 0); - if ($externalProductId <= 0) { - $this->logExportStep($productId, $actor, 'variant_parent_external_id_missing', []); - return ['ok' => false, 'message' => 'Brak ID produktu nadrzednego w shopPRO.']; - } - - $remoteVariantsResult = $this->shopProClient->fetchProductVariants($baseUrl, $apiKey, $timeout, $externalProductId); - if (($remoteVariantsResult['ok'] ?? false) !== true) { - $this->logExportStep($productId, $actor, 'variant_fetch_remote_failed', [ - 'external_product_id' => $externalProductId, - 'http_code' => (int) ($remoteVariantsResult['http_code'] ?? 0), - 'message' => (string) ($remoteVariantsResult['message'] ?? ''), - ]); - return ['ok' => false, 'message' => (string) ($remoteVariantsResult['message'] ?? 'Nie mozna pobrac wariantow z shopPRO.')]; - } - - $remoteVariants = is_array($remoteVariantsResult['variants'] ?? null) ? $remoteVariantsResult['variants'] : []; - $remoteByHash = $this->mapRemoteVariantsByHash($remoteVariants); - $variantErrors = []; - $exportedVariantIds = []; - $attributeMapCache = []; - $valueMapCache = []; - - foreach ($variants as $variant) { - $attributesResult = $this->resolveVariantAttributesPayload( - $variant, - $baseUrl, - $apiKey, - $timeout, - $attributeMapCache, - $valueMapCache - ); - if (($attributesResult['ok'] ?? false) !== true) { - $variantErrors[] = (string) ($attributesResult['message'] ?? 'Nie mozna dopasowac atrybutow wariantu.'); - continue; - } - - $attributesPayload = is_array($attributesResult['payload'] ?? null) ? $attributesResult['payload'] : []; - if ($attributesPayload === []) { - $variantErrors[] = 'Wariant bez poprawnych atrybutow (ID: ' . (int) ($variant['id'] ?? 0) . ').'; - continue; - } - - $permutationHash = trim((string) ($variant['permutation_hash'] ?? '')); - if ($permutationHash === '') { - $permutationHash = $this->buildPermutationHash($attributesPayload); - } else { - $permutationHash = $this->buildPermutationHash($attributesPayload); - } - - $variantPayload = $this->mapVariantPayload($variant, $attributesPayload); - $remoteVariantId = isset($remoteByHash[$permutationHash]) ? (int) $remoteByHash[$permutationHash] : 0; - - if ($remoteVariantId > 0) { - $update = $this->shopProClient->updateProductVariant($baseUrl, $apiKey, $timeout, $remoteVariantId, $variantPayload); - if (($update['ok'] ?? false) !== true) { - $variantErrors[] = 'Nie mozna zaktualizowac wariantu ' . $permutationHash . ': ' . (string) ($update['message'] ?? ''); - continue; - } - } else { - $create = $this->shopProClient->createProductVariant($baseUrl, $apiKey, $timeout, $externalProductId, $variantPayload); - if (($create['ok'] ?? false) !== true) { - $variantErrors[] = 'Nie mozna utworzyc wariantu ' . $permutationHash . ': ' . (string) ($create['message'] ?? ''); - continue; - } - $remoteVariantId = (int) ($create['external_variant_id'] ?? 0); - } - - if ($remoteVariantId <= 0) { - $variantErrors[] = 'Brak ID wariantu po eksporcie (' . $permutationHash . ').'; - continue; - } - - $exportedVariantIds[] = $remoteVariantId; - $this->integrations->upsertProductChannelMap( - $productId, - 'shoppro', - 'listed', - (string) $externalProductId, - (string) $remoteVariantId, - $integrationId > 0 ? $integrationId : null - ); - } - - if ($variantErrors !== []) { - $this->logExportStep($productId, $actor, 'variant_sync_failed', [ - 'external_product_id' => $externalProductId, - 'errors' => array_slice($variantErrors, 0, 3), - ]); - return [ - 'ok' => false, - 'message' => implode(' ', array_slice($variantErrors, 0, 3)), - ]; - } - - $this->products->logChange( - $productId, - isset($actor['id']) ? (int) $actor['id'] : null, - 'product_export_shoppro', - null, - [ - 'mode' => 'variant', - 'integration_id' => $integrationId, - 'external_product_id' => $externalProductId, - 'external_variant_ids' => $exportedVariantIds, - ] - ); - - return ['ok' => true, 'message' => '']; - } - - /** - * @param array $product - * @param array|null $integrationTranslation Per-integration overrides (null fields = use global) - * @return array - */ - private function mapProductPayload(array $product, string $baseUrl, string $apiKey, int $timeout, ?array $integrationTranslation = null): array - { - $name = trim((string) ($integrationTranslation['name'] ?? $product['name'] ?? '')); - if ($name === '') { - $name = 'orderPRO #' . (int) ($product['id'] ?? 0); - } - - $producerId = $this->resolveRemoteProducerIdByName($product, $baseUrl, $apiKey, $timeout); - $gpsr = $this->nullableText($product['security_information'] ?? null); - - return [ - 'price_brutto' => round((float) ($product['price_brutto'] ?? 0), 2), - 'price_brutto_promo' => $this->nullableFloat($product['price_brutto_promo'] ?? null, 2), - 'price_netto' => $this->nullableFloat($product['price_netto'] ?? null, 2), - 'price_netto_promo' => $this->nullableFloat($product['price_netto_promo'] ?? null, 2), - 'vat' => $this->nullableFloat($product['vat'] ?? null, 2), - 'quantity' => round((float) ($product['quantity'] ?? 0), 3), - 'status' => ((int) ($product['status'] ?? 0)) === 1 ? 1 : 0, - 'promoted' => ((int) ($product['promoted'] ?? 0)) === 1 ? 1 : 0, - 'new_to_date' => $this->nullableText($product['new_to_date'] ?? null), - 'additional_message' => ((int) ($product['additional_message'] ?? 0)) === 1 ? 1 : 0, - 'additional_message_required' => ((int) ($product['additional_message_required'] ?? 0)) === 1 ? 1 : 0, - 'additional_message_text' => $this->nullableText($product['additional_message_text'] ?? null), - 'sku' => $this->nullableText($product['sku'] ?? null), - 'ean' => $this->nullableText($product['ean'] ?? null), - 'weight' => $this->nullableFloat($product['weight'] ?? null, 3), - 'producer_id' => $producerId, - 'product_unit_id' => $this->nullableInt($product['product_unit_id'] ?? null), - 'languages' => [ - 'pl' => [ - 'name' => $name, - 'short_description' => $this->nullableText($integrationTranslation['short_description'] ?? $product['short_description'] ?? null), - 'description' => $this->nullableText($integrationTranslation['description'] ?? $product['description'] ?? null), - 'meta_title' => $this->nullableText($product['meta_title'] ?? null), - 'meta_description' => $this->nullableText($product['meta_description'] ?? null), - 'meta_keywords' => $this->nullableText($product['meta_keywords'] ?? null), - 'seo_link' => $this->nullableText($product['seo_link'] ?? null), - 'security_information' => $gpsr, - ], - ], - ]; - } - - /** - * Returns the per-integration translation row for a product+integration pair, or null if none exists. - */ - private function loadIntegrationTranslation(int $productId, int $integrationId): ?array - { - if ($integrationId <= 0) { - return null; - } - - foreach ($this->products->findIntegrationTranslations($productId) as $row) { - if ((int) ($row['integration_id'] ?? 0) === $integrationId) { - return $row; - } - } - - return null; - } - - /** - * Dopasowuje producenta po nazwie i zwraca ID producenta po stronie shopPRO. - */ - private function resolveRemoteProducerIdByName(array $product, string $baseUrl, string $apiKey, int $timeout): ?int - { - $producerName = trim((string) ($product['producer_name'] ?? '')); - if ($producerName === '') { - return null; - } - - $ensureResult = $this->shopProClient->ensureProducer($baseUrl, $apiKey, $timeout, $producerName); - if (($ensureResult['ok'] ?? false) !== true) { - return null; - } - - $producerId = (int) ($ensureResult['producer_id'] ?? 0); - - return $producerId > 0 ? $producerId : null; - } - - /** - * @param array $variant - * @param array $attributesPayload - * @return array - */ - private function mapVariantPayload(array $variant, array $attributesPayload): array - { - return [ - 'attributes' => $attributesPayload, - 'sku' => $this->nullableText($variant['sku'] ?? null), - 'ean' => $this->nullableText($variant['ean'] ?? null), - 'price_brutto' => $this->nullableFloat($variant['price_brutto'] ?? null, 2), - 'price_brutto_promo' => $this->nullableFloat($variant['price_brutto_promo'] ?? null, 2), - 'price_netto' => $this->nullableFloat($variant['price_netto'] ?? null, 2), - 'price_netto_promo' => $this->nullableFloat($variant['price_netto_promo'] ?? null, 2), - 'quantity' => round((float) ($variant['quantity'] ?? 0), 3), - 'stock_0_buy' => ((int) ($variant['stock_0_buy'] ?? 0)) === 1 ? 1 : 0, - 'weight' => $this->nullableFloat($variant['weight'] ?? null, 3), - 'status' => ((int) ($variant['status'] ?? 0)) === 1 ? 1 : 0, - ]; - } - - /** - * @param array $variant - * @return array - */ - private function mapVariantAttributesPayload(array $variant): array - { - $attributes = is_array($variant['attributes'] ?? null) ? $variant['attributes'] : []; - $payload = []; - - foreach ($attributes as $attribute) { - if (!is_array($attribute)) { - continue; - } - - $attributeId = max(0, (int) ($attribute['attribute_id'] ?? 0)); - $valueId = max(0, (int) ($attribute['value_id'] ?? 0)); - if ($attributeId <= 0 || $valueId <= 0) { - continue; - } - - $payload[(string) $attributeId] = $valueId; - } - - ksort($payload, SORT_NATURAL); - - return $payload; - } - - /** - * @param array $variant - * @param array $attributeMapCache - * @param array $valueMapCache - * @return array{ok:bool,message:string,payload:array} - */ - private function resolveVariantAttributesPayload( - array $variant, - string $baseUrl, - string $apiKey, - int $timeout, - array &$attributeMapCache, - array &$valueMapCache - ): array { - $attributes = is_array($variant['attributes'] ?? null) ? $variant['attributes'] : []; - if ($attributes === []) { - return [ - 'ok' => false, - 'message' => 'Wariant nie ma atrybutow do mapowania.', - 'payload' => [], - ]; - } - - $payload = []; - foreach ($attributes as $attribute) { - if (!is_array($attribute)) { - continue; - } - - $localAttributeId = max(0, (int) ($attribute['attribute_id'] ?? 0)); - $localValueId = max(0, (int) ($attribute['value_id'] ?? 0)); - $attributeType = max(0, (int) ($attribute['attribute_type'] ?? 0)); - $attributeName = trim((string) ($attribute['attribute_name'] ?? '')); - $valueName = trim((string) ($attribute['value_name'] ?? '')); - - if ($attributeName === '' || $valueName === '') { - return [ - 'ok' => false, - 'message' => 'Brak nazwy atrybutu lub wartosci w wariancie (ID: ' . (int) ($variant['id'] ?? 0) . ').', - 'payload' => [], - ]; - } - - $attributeCacheKey = $localAttributeId > 0 - ? 'a:' . $localAttributeId - : 'an:' . $this->normalizeLookupKey($attributeName) . '|t:' . $attributeType; - if (!isset($attributeMapCache[$attributeCacheKey])) { - $ensureAttribute = $this->shopProClient->ensureAttribute( - $baseUrl, - $apiKey, - $timeout, - $attributeName, - $attributeType, - 'pl' - ); - if (($ensureAttribute['ok'] ?? false) !== true) { - return [ - 'ok' => false, - 'message' => 'Nie mozna dopasowac/utworzyc atrybutu "' . $attributeName . '": ' . (string) ($ensureAttribute['message'] ?? ''), - 'payload' => [], - ]; - } - - $attributeMapCache[$attributeCacheKey] = (int) ($ensureAttribute['attribute_id'] ?? 0); - } - - $remoteAttributeId = (int) ($attributeMapCache[$attributeCacheKey] ?? 0); - if ($remoteAttributeId <= 0) { - return [ - 'ok' => false, - 'message' => 'Brak poprawnego ID docelowego atrybutu dla "' . $attributeName . '".', - 'payload' => [], - ]; - } - - $valueCacheKey = $localValueId > 0 - ? 'v:' . $localValueId . '|ra:' . $remoteAttributeId - : 'vn:' . $this->normalizeLookupKey($valueName) . '|ra:' . $remoteAttributeId; - if (!isset($valueMapCache[$valueCacheKey])) { - $ensureValue = $this->shopProClient->ensureAttributeValue( - $baseUrl, - $apiKey, - $timeout, - $remoteAttributeId, - $valueName, - 'pl' - ); - if (($ensureValue['ok'] ?? false) !== true) { - return [ - 'ok' => false, - 'message' => 'Nie mozna dopasowac/utworzyc wartosci "' . $valueName . '" (atrybut: ' . $attributeName . '): ' . (string) ($ensureValue['message'] ?? ''), - 'payload' => [], - ]; - } - - $valueMapCache[$valueCacheKey] = (int) ($ensureValue['value_id'] ?? 0); - } - - $remoteValueId = (int) ($valueMapCache[$valueCacheKey] ?? 0); - if ($remoteValueId <= 0) { - return [ - 'ok' => false, - 'message' => 'Brak poprawnego ID docelowej wartosci dla "' . $valueName . '".', - 'payload' => [], - ]; - } - - $payload[(string) $remoteAttributeId] = $remoteValueId; - } - - if ($payload === []) { - return [ - 'ok' => false, - 'message' => 'Nie znaleziono poprawnych atrybutow wariantu.', - 'payload' => [], - ]; - } - - ksort($payload, SORT_NATURAL); - - return [ - 'ok' => true, - 'message' => '', - 'payload' => $payload, - ]; - } - - /** - * @param array> $remoteVariants - * @return array - */ - private function mapRemoteVariantsByHash(array $remoteVariants): array - { - $mapped = []; - foreach ($remoteVariants as $variant) { - if (!is_array($variant)) { - continue; - } - - $hash = trim((string) ($variant['permutation_hash'] ?? '')); - $id = (int) ($variant['id'] ?? 0); - if ($hash === '' || $id <= 0) { - continue; - } - - $mapped[$hash] = $id; - } - - return $mapped; - } - - /** - * @param array $attributesPayload - */ - private function buildPermutationHash(array $attributesPayload): string - { - if ($attributesPayload === []) { - return ''; - } - - $parts = []; - foreach ($attributesPayload as $attributeId => $valueId) { - $parts[] = ((int) $attributeId) . '-' . ((int) $valueId); - } - - return implode('|', $parts); - } - - private function normalizeLookupKey(string $text): string - { - $normalized = mb_strtolower(trim($text)); - $normalized = preg_replace('/\s+/u', ' ', $normalized); - return is_string($normalized) ? $normalized : ''; - } - - private function findMappedExternalProductId(int $productId, int $integrationId): int - { - if ($integrationId <= 0) { - return 0; - } - - $statement = $this->pdo->prepare( - 'SELECT pcm.external_product_id - FROM product_channel_map pcm - INNER JOIN sales_channels sc ON sc.id = pcm.channel_id - WHERE sc.code = :channel_code - AND pcm.product_id = :product_id - AND pcm.integration_id = :integration_id - AND pcm.external_product_id IS NOT NULL - AND pcm.external_product_id <> "" - AND pcm.external_variant_id IS NULL - ORDER BY pcm.id DESC - LIMIT 1' - ); - $statement->execute([ - 'channel_code' => 'shoppro', - 'product_id' => $productId, - 'integration_id' => $integrationId, - ]); - $value = $statement->fetchColumn(); - - if ($value === false) { - return 0; - } - - return (int) $value; - } - - /** - * @return array{ok:bool,message:string,uploaded:int,skipped:int} - */ - private function syncProductImages( - int $productId, - int $externalProductId, - string $baseUrl, - string $apiKey, - int $timeout, - ?array $actor - ): array { - $images = $this->products->findImagesByProductId($productId); - if ($images === []) { - $this->logExportStep($productId, $actor, 'images_skip_no_local', []); - - return [ - 'ok' => true, - 'message' => '', - 'uploaded' => 0, - 'skipped' => 0, - ]; - } - - $remoteImagesByName = []; - $remoteProduct = $this->shopProClient->fetchProductById($baseUrl, $apiKey, $timeout, $externalProductId); - if (($remoteProduct['ok'] ?? false) === true) { - $remoteData = is_array($remoteProduct['product'] ?? null) ? $remoteProduct['product'] : []; - $remoteImages = is_array($remoteData['images'] ?? null) ? $remoteData['images'] : []; - foreach ($remoteImages as $remoteImage) { - if (!is_array($remoteImage)) { - continue; - } - - $remoteName = $this->extractImageFileName((string) ($remoteImage['src'] ?? '')); - if ($remoteName === '') { - continue; - } - - $remoteImagesByName[$remoteName] = true; - } - } else { - $this->logExportStep($productId, $actor, 'images_remote_fetch_failed', [ - 'external_product_id' => $externalProductId, - 'http_code' => (int) ($remoteProduct['http_code'] ?? 0), - 'message' => (string) ($remoteProduct['message'] ?? ''), - ]); - } - - $uploaded = 0; - $skipped = 0; - $errors = []; - $position = 1; - - foreach ($images as $image) { - if (!is_array($image)) { - continue; - } - - $imageId = (int) ($image['id'] ?? 0); - $storagePath = trim((string) ($image['storage_path'] ?? '')); - if ($storagePath === '') { - $errors[] = 'Puste storage_path dla zdjecia #' . $imageId . '.'; - continue; - } - - $binaryResult = $this->readImageBinaryForExport($storagePath, $timeout); - if (($binaryResult['ok'] ?? false) !== true) { - $message = (string) ($binaryResult['message'] ?? 'Nie mozna odczytac obrazu.'); - $errors[] = 'Zdjecie #' . $imageId . ': ' . $message; - $this->logExportStep($productId, $actor, 'image_read_failed', [ - 'image_id' => $imageId, - 'storage_path' => $storagePath, - 'message' => $message, - ]); - continue; - } - - $binary = (string) ($binaryResult['content'] ?? ''); - if ($binary === '') { - $errors[] = 'Zdjecie #' . $imageId . ': pusty odczyt pliku.'; - continue; - } - - $extension = $this->normalizeImageExtension((string) ($binaryResult['extension'] ?? '')); - $fileName = $this->determineUploadFileName($productId, $imageId, $extension !== '' ? $extension : 'jpg'); - $fileNameLookup = strtolower($fileName); - if (isset($remoteImagesByName[$fileNameLookup])) { - $skipped++; - $position++; - $this->logExportStep($productId, $actor, 'image_skip_already_exists', [ - 'image_id' => $imageId, - 'file_name' => $fileName, - ]); - continue; - } - - $contentBase64 = base64_encode($binary); - if ($contentBase64 === false || $contentBase64 === '') { - $errors[] = 'Zdjecie #' . $imageId . ': nie mozna zakodowac base64.'; - continue; - } - - $alt = isset($image['alt']) ? trim((string) $image['alt']) : ''; - $upload = $this->shopProClient->uploadProductImage( - $baseUrl, - $apiKey, - $timeout, - $externalProductId, - $fileName, - $contentBase64, - $alt !== '' ? $alt : null, - $position - ); - - if (($upload['ok'] ?? false) !== true) { - $message = (string) ($upload['message'] ?? 'Nie mozna przeslac zdjecia do shopPRO.'); - $errors[] = 'Zdjecie #' . $imageId . ': ' . $message; - $this->logExportStep($productId, $actor, 'image_upload_failed', [ - 'image_id' => $imageId, - 'file_name' => $fileName, - 'http_code' => (int) ($upload['http_code'] ?? 0), - 'message' => $message, - ]); - continue; - } - - $uploaded++; - $position++; - - $remoteSrc = (string) ($upload['src'] ?? ''); - $remoteName = $this->extractImageFileName($remoteSrc); - if ($remoteName !== '') { - $remoteImagesByName[$remoteName] = true; - } - - $this->logExportStep($productId, $actor, 'image_uploaded', [ - 'image_id' => $imageId, - 'file_name' => $fileName, - 'remote_src' => $remoteSrc, - ]); - } - - if ($errors !== []) { - $this->logExportStep($productId, $actor, 'images_failed', [ - 'external_product_id' => $externalProductId, - 'uploaded' => $uploaded, - 'skipped' => $skipped, - 'errors' => array_slice($errors, 0, 3), - ]); - - return [ - 'ok' => false, - 'message' => 'Nie udalo sie wyeksportowac wszystkich zdjec: ' . implode(' ', array_slice($errors, 0, 3)), - 'uploaded' => $uploaded, - 'skipped' => $skipped, - ]; - } - - $this->logExportStep($productId, $actor, 'images_done', [ - 'external_product_id' => $externalProductId, - 'uploaded' => $uploaded, - 'skipped' => $skipped, - 'total_local' => count($images), - ]); - - return [ - 'ok' => true, - 'message' => '', - 'uploaded' => $uploaded, - 'skipped' => $skipped, - ]; - } - - /** - * @return array{ok:bool,message:string,content?:string,extension?:string} - */ - private function readImageBinaryForExport(string $storagePath, int $timeout): array - { - $path = trim($storagePath); - if ($path === '') { - return ['ok' => false, 'message' => 'Pusta sciezka obrazu.']; - } - - if (preg_match('#^https?://#i', $path) === 1 || str_starts_with($path, '//')) { - return $this->readRemoteImageBinary($path, $timeout); - } - - return $this->readLocalImageBinary($path); - } - - /** - * @return array{ok:bool,message:string,content?:string,extension?:string} - */ - private function readLocalImageBinary(string $storagePath): array - { - $projectRoot = dirname(__DIR__, 3); - $normalizedPath = ltrim(str_replace('\\', '/', $storagePath), '/'); - if ($normalizedPath === '') { - return ['ok' => false, 'message' => 'Pusta sciezka lokalnego obrazu.']; - } - - $candidate = $projectRoot . '/public/' . $normalizedPath; - $realCandidate = realpath($candidate); - if ($realCandidate === false || !is_file($realCandidate)) { - return ['ok' => false, 'message' => 'Plik obrazu nie istnieje na dysku.']; - } - - $realPublic = realpath($projectRoot . '/public'); - if ($realPublic === false || !str_starts_with($realCandidate, $realPublic . DIRECTORY_SEPARATOR)) { - return ['ok' => false, 'message' => 'Sciezka obrazu jest poza katalogiem public/.']; - } - - $binary = @file_get_contents($realCandidate); - if (!is_string($binary) || $binary === '') { - return ['ok' => false, 'message' => 'Nie mozna odczytac lokalnego pliku obrazu.']; - } - - $extension = $this->normalizeImageExtension((string) pathinfo($realCandidate, PATHINFO_EXTENSION)); - if ($extension === '') { - $extension = $this->detectImageExtensionFromBinary($binary); - } - - return [ - 'ok' => true, - 'message' => '', - 'content' => $binary, - 'extension' => $extension, - ]; - } - - /** - * @return array{ok:bool,message:string,content?:string,extension?:string} - */ - private function readRemoteImageBinary(string $url, int $timeout): array - { - $normalizedUrl = str_starts_with($url, '//') ? ('https:' . $url) : $url; - $binary = ''; - $httpCode = null; - $normalizedTimeout = max(3, min(60, $timeout)); - - if (function_exists('curl_init')) { - $curl = curl_init($normalizedUrl); - if ($curl === false) { - return ['ok' => false, 'message' => 'Nie mozna zainicjalizowac cURL dla obrazu.']; - } - - curl_setopt_array($curl, [ - CURLOPT_RETURNTRANSFER => true, - CURLOPT_TIMEOUT => $normalizedTimeout, - CURLOPT_CONNECTTIMEOUT => max(2, min(10, $normalizedTimeout)), - CURLOPT_SSL_VERIFYPEER => true, - CURLOPT_SSL_VERIFYHOST => 2, - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_MAXREDIRS => 5, - ]); - - $responseBody = curl_exec($curl); - $httpCode = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE); - $error = curl_error($curl); - curl_close($curl); - - if ($error !== '') { - return ['ok' => false, 'message' => 'Blad pobierania obrazu (cURL): ' . $error]; - } - - $binary = is_string($responseBody) ? $responseBody : ''; - } else { - $context = stream_context_create([ - 'http' => [ - 'method' => 'GET', - 'timeout' => $normalizedTimeout, - 'ignore_errors' => true, - 'follow_location' => 1, - 'max_redirects' => 5, - ], - ]); - - $responseBody = @file_get_contents($normalizedUrl, false, $context); - $binary = is_string($responseBody) ? $responseBody : ''; - $headers = is_array($http_response_header ?? null) ? $http_response_header : []; - foreach ($headers as $header) { - if (preg_match('/^HTTP\/\d\.\d\s+(\d{3})\b/i', $header, $matches) === 1) { - $httpCode = (int) $matches[1]; - break; - } - } - } - - if ($httpCode !== null && $httpCode >= 400) { - return ['ok' => false, 'message' => 'Zdalny serwer zwrocil HTTP ' . $httpCode . ' dla obrazu.']; - } - - if ($binary === '') { - return ['ok' => false, 'message' => 'Pusta odpowiedz przy pobieraniu zdalnego obrazu.']; - } - - $path = (string) (parse_url($normalizedUrl, PHP_URL_PATH) ?? ''); - $extension = $this->normalizeImageExtension((string) pathinfo($path, PATHINFO_EXTENSION)); - if ($extension === '') { - $extension = $this->detectImageExtensionFromBinary($binary); - } - - return [ - 'ok' => true, - 'message' => '', - 'content' => $binary, - 'extension' => $extension, - ]; - } - - private function normalizeImageExtension(string $extension): string - { - $normalized = strtolower(trim($extension)); - if ($normalized === 'jpeg') { - return 'jpg'; - } - - return in_array($normalized, ['jpg', 'png', 'webp', 'gif'], true) ? $normalized : ''; - } - - private function detectImageExtensionFromBinary(string $binary): string - { - $info = @getimagesizefromstring($binary); - if (!is_array($info)) { - return ''; - } - - $mime = strtolower((string) ($info['mime'] ?? '')); - - return match ($mime) { - 'image/jpeg' => 'jpg', - 'image/png' => 'png', - 'image/webp' => 'webp', - 'image/gif' => 'gif', - default => '', - }; - } - - private function determineUploadFileName(int $productId, int $imageId, string $extension): string - { - $safeExtension = $this->normalizeImageExtension($extension); - if ($safeExtension === '') { - $safeExtension = 'jpg'; - } - - $suffix = $imageId > 0 ? (string) $imageId : (string) time(); - - return 'orderpro-' . $productId . '-' . $suffix . '.' . $safeExtension; - } - - private function extractImageFileName(string $src): string - { - $value = trim($src); - if ($value === '') { - return ''; - } - - $path = (string) (parse_url($value, PHP_URL_PATH) ?? $value); - $name = strtolower(trim((string) basename($path))); - - return $name === '' ? '' : $name; - } - - /** - * @param array|null $actor - * @param array $context - */ - private function logExportStep(int $productId, ?array $actor, string $stage, array $context): void - { - try { - $this->products->logChange( - $productId, - isset($actor['id']) ? (int) $actor['id'] : null, - 'product_export_shoppro_debug', - null, - [ - 'stage' => $stage, - 'context' => $context, - ] - ); - } catch (\Throwable) { - // logging should never break export flow - } - } - - /** - * @return array{include:bool,fields:array>,warning:string} - */ - private function mapCustomFieldsPayload(mixed $customFieldsRaw): array - { - if ($customFieldsRaw === null) { - return [ - 'include' => false, - 'fields' => [], - 'warning' => '', - ]; - } - - $decoded = null; - if (is_string($customFieldsRaw)) { - $raw = trim($customFieldsRaw); - if ($raw === '') { - return [ - 'include' => false, - 'fields' => [], - 'warning' => '', - ]; - } - - $jsonDecoded = json_decode($raw, true); - if (!is_array($jsonDecoded)) { - return [ - 'include' => false, - 'fields' => [], - 'warning' => 'Niepoprawny custom_fields_json (JSON decode failed).', - ]; - } - $decoded = $jsonDecoded; - } elseif (is_array($customFieldsRaw)) { - $decoded = $customFieldsRaw; - } else { - return [ - 'include' => false, - 'fields' => [], - 'warning' => 'Nieobslugiwany format custom_fields_json.', - ]; - } - - $items = $decoded; - if (array_key_exists('name', $items) || array_key_exists('type', $items)) { - $items = [$items]; - } - - if (!is_array($items)) { - return [ - 'include' => false, - 'fields' => [], - 'warning' => 'Niepoprawna struktura custom_fields_json.', - ]; - } - - $payload = []; - foreach ($items as $item) { - if (!is_array($item)) { - continue; - } - - $name = trim((string) ($item['name'] ?? '')); - if ($name === '') { - continue; - } - - $payload[] = [ - 'name' => $name, - 'type' => $this->normalizeCustomFieldType((string) ($item['type'] ?? 'text')), - 'is_required' => $this->normalizeCustomFieldRequired($item['is_required'] ?? ($item['required'] ?? 0)), - ]; - } - - return [ - 'include' => true, - 'fields' => $payload, - 'warning' => '', - ]; - } - - private function normalizeCustomFieldType(string $type): string - { - $normalized = mb_strtolower(trim($type)); - - return in_array($normalized, ['image', 'img', 'photo', 'obrazek', 'zdjecie', 'picture'], true) - ? 'image' - : 'text'; - } - - private function normalizeCustomFieldRequired(mixed $required): int - { - if (is_bool($required)) { - return $required ? 1 : 0; - } - - if (is_int($required) || is_float($required)) { - return ((int) $required) > 0 ? 1 : 0; - } - - $text = mb_strtolower(trim((string) $required)); - if ($text === '') { - return 0; - } - - return in_array($text, ['1', 'true', 'yes', 'on', 'required'], true) ? 1 : 0; - } - - private function nullableText(mixed $value): ?string - { - $text = trim((string) $value); - return $text === '' ? null : $text; - } - - private function nullableInt(mixed $value): ?int - { - $text = trim((string) $value); - if ($text === '' || !is_numeric($text)) { - return null; - } - - return (int) $text; - } - - private function nullableFloat(mixed $value, int $precision = 2): ?float - { - $text = trim((string) $value); - if ($text === '' || !is_numeric($text)) { - return null; - } - - return round((float) $text, $precision); - } -} diff --git a/archive/2026-03-02_users-only-reset/src/Modules/Settings/AppSettingsRepository.php b/archive/2026-03-02_users-only-reset/src/Modules/Settings/AppSettingsRepository.php deleted file mode 100644 index 43a71e6..0000000 --- a/archive/2026-03-02_users-only-reset/src/Modules/Settings/AppSettingsRepository.php +++ /dev/null @@ -1,70 +0,0 @@ -pdo->prepare( - 'SELECT setting_value - FROM app_settings - WHERE setting_key = :setting_key - LIMIT 1' - ); - $statement->execute(['setting_key' => trim($key)]); - - $value = $statement->fetchColumn(); - if ($value === false || $value === null) { - return $default; - } - - $text = trim((string) $value); - return $text === '' ? $default : $text; - } - - public function getBool(string $key, bool $default = false): bool - { - $value = $this->get($key); - if ($value === null) { - return $default; - } - - return in_array(strtolower(trim($value)), ['1', 'true', 'yes', 'on'], true); - } - - public function getInt(string $key, int $default = 0): int - { - $value = $this->get($key); - if ($value === null || !is_numeric($value)) { - return $default; - } - - return (int) $value; - } - - public function set(string $key, string $value): void - { - $statement = $this->pdo->prepare( - 'INSERT INTO app_settings (setting_key, setting_value, created_at, updated_at) - VALUES (:setting_key, :setting_value, :created_at, :updated_at) - ON DUPLICATE KEY UPDATE - setting_value = VALUES(setting_value), - updated_at = VALUES(updated_at)' - ); - $now = date('Y-m-d H:i:s'); - $statement->execute([ - 'setting_key' => trim($key), - 'setting_value' => trim($value), - 'created_at' => $now, - 'updated_at' => $now, - ]); - } -} diff --git a/archive/2026-03-02_users-only-reset/src/Modules/Settings/IntegrationRepository.php b/archive/2026-03-02_users-only-reset/src/Modules/Settings/IntegrationRepository.php deleted file mode 100644 index d4ced03..0000000 --- a/archive/2026-03-02_users-only-reset/src/Modules/Settings/IntegrationRepository.php +++ /dev/null @@ -1,679 +0,0 @@ -> - */ - public function listByType(string $type): array - { - $statement = $this->pdo->prepare( - 'SELECT id, type, name, base_url, timeout_seconds, is_active' - . $this->ordersFetchSelectFragment() - . $this->orderStatusSyncDirectionSelectFragment() . ', - last_test_status, last_test_http_code, last_test_message, last_test_at, - created_at, updated_at, - CASE WHEN api_key_encrypted IS NULL OR api_key_encrypted = "" THEN 0 ELSE 1 END AS has_api_key - FROM integrations - WHERE type = :type - ORDER BY id DESC' - ); - $statement->execute(['type' => $type]); - - $rows = $statement->fetchAll(); - if (!is_array($rows)) { - return []; - } - - return array_map([$this, 'mapRow'], $rows); - } - - /** - * @return array|null - */ - public function findById(int $id): ?array - { - $statement = $this->pdo->prepare( - 'SELECT id, type, name, base_url, timeout_seconds, is_active' - . $this->ordersFetchSelectFragment() - . $this->orderStatusSyncDirectionSelectFragment() . ', - last_test_status, last_test_http_code, last_test_message, last_test_at, - created_at, updated_at, - CASE WHEN api_key_encrypted IS NULL OR api_key_encrypted = "" THEN 0 ELSE 1 END AS has_api_key - FROM integrations - WHERE id = :id - LIMIT 1' - ); - $statement->execute(['id' => $id]); - - $row = $statement->fetch(); - if (!is_array($row)) { - return null; - } - - return $this->mapRow($row); - } - - /** - * @return array|null - */ - public function findApiCredentials(int $id): ?array - { - $statement = $this->pdo->prepare( - 'SELECT id, name, base_url, timeout_seconds, api_key_encrypted' - . $this->ordersFetchSelectFragment() - . $this->orderStatusSyncDirectionSelectFragment() . ' - FROM integrations - WHERE id = :id - LIMIT 1' - ); - $statement->execute(['id' => $id]); - - $row = $statement->fetch(); - if (!is_array($row)) { - return null; - } - - return [ - 'id' => (int) ($row['id'] ?? 0), - 'name' => (string) ($row['name'] ?? ''), - 'base_url' => (string) ($row['base_url'] ?? ''), - 'timeout_seconds' => (int) ($row['timeout_seconds'] ?? 10), - 'api_key' => $this->decryptApiKey((string) ($row['api_key_encrypted'] ?? '')), - 'orders_fetch_enabled' => (int) ($row['orders_fetch_enabled'] ?? 0) === 1, - 'orders_fetch_start_date' => $row['orders_fetch_start_date'] === null ? null : (string) $row['orders_fetch_start_date'], - 'order_status_sync_direction' => $this->normalizeOrderStatusSyncDirection((string) ($row['order_status_sync_direction'] ?? self::ORDER_STATUS_SYNC_DIRECTION_DEFAULT)), - ]; - } - - /** - * @return array|null - */ - public function findActiveApiCredentialsByType(string $type): ?array - { - $statement = $this->pdo->prepare( - 'SELECT id, name, base_url, timeout_seconds, api_key_encrypted' - . $this->ordersFetchSelectFragment() - . $this->orderStatusSyncDirectionSelectFragment() . ' - FROM integrations - WHERE type = :type AND is_active = 1 - ORDER BY id DESC - LIMIT 1' - ); - $statement->execute(['type' => $type]); - - $row = $statement->fetch(); - if (!is_array($row)) { - return null; - } - - return [ - 'id' => (int) ($row['id'] ?? 0), - 'name' => (string) ($row['name'] ?? ''), - 'base_url' => (string) ($row['base_url'] ?? ''), - 'timeout_seconds' => (int) ($row['timeout_seconds'] ?? 10), - 'api_key' => $this->decryptApiKey((string) ($row['api_key_encrypted'] ?? '')), - 'orders_fetch_enabled' => (int) ($row['orders_fetch_enabled'] ?? 0) === 1, - 'orders_fetch_start_date' => $row['orders_fetch_start_date'] === null ? null : (string) $row['orders_fetch_start_date'], - 'order_status_sync_direction' => $this->normalizeOrderStatusSyncDirection((string) ($row['order_status_sync_direction'] ?? self::ORDER_STATUS_SYNC_DIRECTION_DEFAULT)), - ]; - } - - public function create( - string $type, - string $name, - string $baseUrl, - int $timeoutSeconds, - bool $isActive, - string $apiKey, - bool $ordersFetchEnabled = false, - ?string $ordersFetchStartDate = null, - string $orderStatusSyncDirection = self::ORDER_STATUS_SYNC_DIRECTION_DEFAULT - ): int { - $normalizedSyncDirection = $this->normalizeOrderStatusSyncDirection($orderStatusSyncDirection); - if ($this->hasOrdersFetchColumns() && $this->hasOrderStatusSyncDirectionColumn()) { - $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, - 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, - :created_at, :updated_at - )' - ); - $statement->execute([ - 'type' => $type, - 'name' => $name, - 'base_url' => $baseUrl, - 'api_key_encrypted' => $this->encryptApiKey($apiKey), - 'timeout_seconds' => $timeoutSeconds, - 'is_active' => $isActive ? 1 : 0, - 'orders_fetch_enabled' => $ordersFetchEnabled ? 1 : 0, - 'orders_fetch_start_date' => $ordersFetchStartDate, - 'order_status_sync_direction' => $normalizedSyncDirection, - 'created_at' => date('Y-m-d H:i:s'), - 'updated_at' => date('Y-m-d H:i:s'), - ]); - } elseif ($this->hasOrdersFetchColumns()) { - $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, - created_at, updated_at - ) VALUES ( - :type, :name, :base_url, :api_key_encrypted, :timeout_seconds, :is_active, - :orders_fetch_enabled, :orders_fetch_start_date, - :created_at, :updated_at - )' - ); - $statement->execute([ - 'type' => $type, - 'name' => $name, - 'base_url' => $baseUrl, - 'api_key_encrypted' => $this->encryptApiKey($apiKey), - 'timeout_seconds' => $timeoutSeconds, - 'is_active' => $isActive ? 1 : 0, - 'orders_fetch_enabled' => $ordersFetchEnabled ? 1 : 0, - 'orders_fetch_start_date' => $ordersFetchStartDate, - 'created_at' => date('Y-m-d H:i:s'), - 'updated_at' => date('Y-m-d H:i:s'), - ]); - } else { - $statement = $this->pdo->prepare( - 'INSERT INTO integrations ( - type, name, base_url, api_key_encrypted, timeout_seconds, is_active, created_at, updated_at - ) VALUES ( - :type, :name, :base_url, :api_key_encrypted, :timeout_seconds, :is_active, :created_at, :updated_at - )' - ); - $statement->execute([ - 'type' => $type, - 'name' => $name, - 'base_url' => $baseUrl, - 'api_key_encrypted' => $this->encryptApiKey($apiKey), - 'timeout_seconds' => $timeoutSeconds, - 'is_active' => $isActive ? 1 : 0, - 'created_at' => date('Y-m-d H:i:s'), - 'updated_at' => date('Y-m-d H:i:s'), - ]); - } - - return (int) $this->pdo->lastInsertId(); - } - - public function update( - int $id, - string $name, - string $baseUrl, - int $timeoutSeconds, - bool $isActive, - ?string $apiKey, - bool $ordersFetchEnabled = false, - ?string $ordersFetchStartDate = null, - string $orderStatusSyncDirection = self::ORDER_STATUS_SYNC_DIRECTION_DEFAULT - ): void { - $normalizedSyncDirection = $this->normalizeOrderStatusSyncDirection($orderStatusSyncDirection); - $params = [ - 'id' => $id, - 'name' => $name, - 'base_url' => $baseUrl, - 'timeout_seconds' => $timeoutSeconds, - 'is_active' => $isActive ? 1 : 0, - 'orders_fetch_enabled' => $ordersFetchEnabled ? 1 : 0, - 'orders_fetch_start_date' => $ordersFetchStartDate, - 'order_status_sync_direction' => $normalizedSyncDirection, - 'updated_at' => date('Y-m-d H:i:s'), - ]; - - $sql = 'UPDATE integrations SET - name = :name, - base_url = :base_url, - timeout_seconds = :timeout_seconds, - is_active = :is_active, - updated_at = :updated_at'; - if ($this->hasOrdersFetchColumns() && $this->hasOrderStatusSyncDirectionColumn()) { - $sql = 'UPDATE integrations SET - name = :name, - base_url = :base_url, - 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, - updated_at = :updated_at'; - } elseif ($this->hasOrdersFetchColumns()) { - $sql = 'UPDATE integrations SET - name = :name, - base_url = :base_url, - timeout_seconds = :timeout_seconds, - is_active = :is_active, - orders_fetch_enabled = :orders_fetch_enabled, - orders_fetch_start_date = :orders_fetch_start_date, - updated_at = :updated_at'; - unset($params['order_status_sync_direction']); - } else { - unset($params['orders_fetch_enabled'], $params['orders_fetch_start_date'], $params['order_status_sync_direction']); - } - - if ($apiKey !== null && trim($apiKey) !== '') { - $sql .= ', api_key_encrypted = :api_key_encrypted'; - $params['api_key_encrypted'] = $this->encryptApiKey($apiKey); - } - - $sql .= ' WHERE id = :id'; - - $statement = $this->pdo->prepare($sql); - $statement->execute($params); - } - - public function setTestResult( - int $id, - string $status, - ?int $httpCode, - string $message, - string $testedAt - ): void { - $statement = $this->pdo->prepare( - 'UPDATE integrations SET - last_test_status = :status, - last_test_http_code = :http_code, - last_test_message = :message, - last_test_at = :tested_at, - updated_at = :updated_at - WHERE id = :id' - ); - $statement->execute([ - 'id' => $id, - 'status' => $status, - 'http_code' => $httpCode, - 'message' => mb_substr($message, 0, 255), - 'tested_at' => $testedAt, - 'updated_at' => date('Y-m-d H:i:s'), - ]); - } - - public function logTest( - int $integrationId, - string $status, - ?int $httpCode, - string $message, - string $endpointUrl, - string $testedAt - ): void { - $statement = $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, :tested_at - )' - ); - $statement->execute([ - 'integration_id' => $integrationId, - 'status' => $status, - 'http_code' => $httpCode, - 'message' => mb_substr($message, 0, 255), - 'endpoint_url' => mb_substr($endpointUrl, 0, 255), - 'tested_at' => $testedAt, - ]); - } - - /** - * @return array> - */ - public function recentTests(int $integrationId, int $limit = 5): array - { - $statement = $this->pdo->prepare( - 'SELECT id, integration_id, status, http_code, message, endpoint_url, tested_at - FROM integration_test_logs - WHERE integration_id = :integration_id - ORDER BY tested_at DESC, id DESC - LIMIT :limit' - ); - $statement->bindValue(':integration_id', $integrationId, PDO::PARAM_INT); - $statement->bindValue(':limit', max(1, $limit), PDO::PARAM_INT); - $statement->execute(); - - $rows = $statement->fetchAll(); - if (!is_array($rows)) { - return []; - } - - return array_map( - static fn (array $row): array => [ - 'id' => (int) ($row['id'] ?? 0), - 'integration_id' => (int) ($row['integration_id'] ?? 0), - 'status' => (string) ($row['status'] ?? ''), - 'http_code' => $row['http_code'] === null ? null : (int) $row['http_code'], - 'message' => (string) ($row['message'] ?? ''), - 'endpoint_url' => (string) ($row['endpoint_url'] ?? ''), - 'tested_at' => (string) ($row['tested_at'] ?? ''), - ], - $rows - ); - } - - public function nameExists(string $type, string $name, ?int $excludeId = null): bool - { - $sql = 'SELECT 1 FROM integrations WHERE type = :type AND name = :name'; - $params = [ - 'type' => $type, - 'name' => $name, - ]; - - if ($excludeId !== null) { - $sql .= ' AND id <> :exclude_id'; - $params['exclude_id'] = $excludeId; - } - - $sql .= ' LIMIT 1'; - - $statement = $this->pdo->prepare($sql); - $statement->execute($params); - - return $statement->fetchColumn() !== false; - } - - public function ensureSalesChannelsSeeded(): void - { - $rows = [ - ['code' => 'shoppro', 'name' => 'shopPRO', 'type' => 'shop_instance'], - ['code' => 'allegro', 'name' => 'Allegro', 'type' => 'marketplace'], - ['code' => 'erli', 'name' => 'Erli', 'type' => 'marketplace'], - ]; - - $statement = $this->pdo->prepare( - 'INSERT INTO sales_channels (code, name, type, status, created_at, updated_at) - VALUES (:code, :name, :type, 1, :created_at, :updated_at) - ON DUPLICATE KEY UPDATE - name = VALUES(name), - type = VALUES(type), - updated_at = VALUES(updated_at)' - ); - - $now = date('Y-m-d H:i:s'); - foreach ($rows as $row) { - $statement->execute([ - 'code' => $row['code'], - 'name' => $row['name'], - 'type' => $row['type'], - 'created_at' => $now, - 'updated_at' => $now, - ]); - } - } - - public function findMappedProductId(string $channelCode, string $externalProductId, ?int $integrationId = null): ?int - { - $sql = 'SELECT pcm.product_id - FROM product_channel_map pcm - INNER JOIN sales_channels sc ON sc.id = pcm.channel_id - WHERE sc.code = :channel_code - AND pcm.external_product_id = :external_product_id'; - $params = [ - 'channel_code' => $channelCode, - 'external_product_id' => $externalProductId, - ]; - if ($integrationId !== null && $integrationId > 0) { - $sql .= ' AND pcm.integration_id = :integration_id'; - $params['integration_id'] = $integrationId; - } - $sql .= ' LIMIT 1'; - - $statement = $this->pdo->prepare($sql); - $statement->execute($params); - - $value = $statement->fetchColumn(); - if ($value === false) { - return null; - } - - return (int) $value; - } - - public function upsertProductChannelMap( - int $productId, - string $channelCode, - string $syncState, - string $externalProductId = '', - string $externalVariantId = '', - ?int $integrationId = null - ): void { - $channelId = $this->findChannelIdByCode($channelCode); - if ($channelId === null) { - throw new RuntimeException('Brak kanalu sprzedazy: ' . $channelCode); - } - - $externalProductId = trim($externalProductId); - $externalVariantId = trim($externalVariantId); - $normalizedIntegrationId = $integrationId !== null && $integrationId > 0 - ? $integrationId - : null; - $linkType = 'manual'; - $linkStatus = $externalProductId !== '' ? 'active' : 'unverified'; - $linkedAt = $externalProductId !== '' ? date('Y-m-d H:i:s') : null; - - $statement = $this->pdo->prepare( - 'INSERT INTO product_channel_map ( - product_id, channel_id, integration_id, external_product_id, external_variant_id, - sync_state, link_type, link_status, linked_at, last_sync_at, created_at, updated_at - ) VALUES ( - :product_id, :channel_id, :integration_id, :external_product_id, :external_variant_id, - :sync_state, :link_type, :link_status, :linked_at, :last_sync_at, :created_at, :updated_at - ) ON DUPLICATE KEY UPDATE - integration_id = VALUES(integration_id), - sync_state = VALUES(sync_state), - last_sync_at = VALUES(last_sync_at), - external_product_id = VALUES(external_product_id), - external_variant_id = VALUES(external_variant_id), - link_type = VALUES(link_type), - link_status = VALUES(link_status), - linked_at = VALUES(linked_at), - updated_at = VALUES(updated_at)' - ); - - $now = date('Y-m-d H:i:s'); - $statement->execute([ - 'product_id' => $productId, - 'channel_id' => $channelId, - 'integration_id' => $normalizedIntegrationId, - 'external_product_id' => $externalProductId, - 'external_variant_id' => $externalVariantId !== '' ? $externalVariantId : null, - 'sync_state' => $syncState, - 'link_type' => $linkType, - 'link_status' => $linkStatus, - 'linked_at' => $linkedAt, - 'last_sync_at' => $now, - 'created_at' => $now, - 'updated_at' => $now, - ]); - } - - /** - * @param array $row - * @return array - */ - private function mapRow(array $row): array - { - return [ - 'id' => (int) ($row['id'] ?? 0), - 'type' => (string) ($row['type'] ?? ''), - 'name' => (string) ($row['name'] ?? ''), - 'base_url' => (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' => $row['orders_fetch_start_date'] === null ? null : (string) $row['orders_fetch_start_date'], - 'order_status_sync_direction' => $this->normalizeOrderStatusSyncDirection((string) ($row['order_status_sync_direction'] ?? self::ORDER_STATUS_SYNC_DIRECTION_DEFAULT)), - 'last_test_status' => (string) ($row['last_test_status'] ?? ''), - 'last_test_http_code' => $row['last_test_http_code'] === null ? null : (int) $row['last_test_http_code'], - 'last_test_message' => (string) ($row['last_test_message'] ?? ''), - 'last_test_at' => (string) ($row['last_test_at'] ?? ''), - 'has_api_key' => (int) ($row['has_api_key'] ?? 0) === 1, - 'created_at' => (string) ($row['created_at'] ?? ''), - 'updated_at' => (string) ($row['updated_at'] ?? ''), - ]; - } - - private function encryptApiKey(string $apiKey): string - { - $plain = trim($apiKey); - if ($plain === '') { - return ''; - } - - $secret = trim($this->secret); - if ($secret === '') { - throw new RuntimeException('Brak INTEGRATIONS_SECRET w konfiguracji .env.'); - } - - $iv = random_bytes(16); - $cipher = openssl_encrypt( - $plain, - 'AES-256-CBC', - hash('sha256', $secret, true), - OPENSSL_RAW_DATA, - $iv - ); - if ($cipher === false) { - throw new RuntimeException('Nie mozna zaszyfrowac klucza API.'); - } - - return base64_encode($iv) . ':' . base64_encode($cipher); - } - - private function decryptApiKey(string $payload): string - { - $serialized = trim($payload); - if ($serialized === '') { - return ''; - } - - $secret = trim($this->secret); - if ($secret === '') { - throw new RuntimeException('Brak INTEGRATIONS_SECRET w konfiguracji .env.'); - } - - $parts = explode(':', $serialized, 2); - if (count($parts) !== 2) { - throw new RuntimeException('Niepoprawny format zapisanego klucza API.'); - } - - $iv = base64_decode($parts[0], true); - $cipher = base64_decode($parts[1], true); - if ($iv === false || $cipher === false || strlen($iv) !== 16) { - throw new RuntimeException('Nie mozna odczytac zapisanego klucza API.'); - } - - $plain = openssl_decrypt( - $cipher, - 'AES-256-CBC', - hash('sha256', $secret, true), - OPENSSL_RAW_DATA, - $iv - ); - if ($plain === false) { - throw new RuntimeException('Nie mozna odszyfrowac zapisanego klucza API.'); - } - - return $plain; - } - - private function findChannelIdByCode(string $code): ?int - { - $statement = $this->pdo->prepare('SELECT id FROM sales_channels WHERE code = :code LIMIT 1'); - $statement->execute(['code' => $code]); - $value = $statement->fetchColumn(); - - if ($value === false) { - return null; - } - - return (int) $value; - } - - private function ordersFetchSelectFragment(): string - { - if ($this->hasOrdersFetchColumns()) { - return ', orders_fetch_enabled, orders_fetch_start_date'; - } - - return ', 0 AS orders_fetch_enabled, NULL AS orders_fetch_start_date'; - } - - private function orderStatusSyncDirectionSelectFragment(): string - { - if ($this->hasOrderStatusSyncDirectionColumn()) { - return ', order_status_sync_direction'; - } - - return ", '" . self::ORDER_STATUS_SYNC_DIRECTION_DEFAULT . "' AS order_status_sync_direction"; - } - - private function hasOrdersFetchColumns(): bool - { - if ($this->ordersFetchColumnsAvailable !== null) { - return $this->ordersFetchColumnsAvailable; - } - - try { - $enabledStmt = $this->pdo->query("SHOW COLUMNS FROM integrations LIKE 'orders_fetch_enabled'"); - $startDateStmt = $this->pdo->query("SHOW COLUMNS FROM integrations LIKE 'orders_fetch_start_date'"); - - $this->ordersFetchColumnsAvailable = - $enabledStmt !== false - && $enabledStmt->fetch() !== false - && $startDateStmt !== false - && $startDateStmt->fetch() !== false; - } catch (\Throwable) { - $this->ordersFetchColumnsAvailable = false; - } - - return $this->ordersFetchColumnsAvailable; - } - - private function hasOrderStatusSyncDirectionColumn(): bool - { - if ($this->orderStatusSyncDirectionColumnAvailable !== null) { - return $this->orderStatusSyncDirectionColumnAvailable; - } - - try { - $stmt = $this->pdo->query("SHOW COLUMNS FROM integrations LIKE 'order_status_sync_direction'"); - $this->orderStatusSyncDirectionColumnAvailable = - $stmt !== false - && $stmt->fetch() !== false; - } catch (\Throwable) { - $this->orderStatusSyncDirectionColumnAvailable = false; - } - - return $this->orderStatusSyncDirectionColumnAvailable; - } - - private function normalizeOrderStatusSyncDirection(string $value): string - { - $normalized = trim(mb_strtolower($value)); - if ($normalized === 'orderpro_to_shoppro') { - return 'orderpro_to_shoppro'; - } - - return self::ORDER_STATUS_SYNC_DIRECTION_DEFAULT; - } -} diff --git a/archive/2026-03-02_users-only-reset/src/Modules/Settings/OrderStatusMappingRepository.php b/archive/2026-03-02_users-only-reset/src/Modules/Settings/OrderStatusMappingRepository.php deleted file mode 100644 index 6153763..0000000 --- a/archive/2026-03-02_users-only-reset/src/Modules/Settings/OrderStatusMappingRepository.php +++ /dev/null @@ -1,125 +0,0 @@ - - */ - public function listByIntegration(int $integrationId): array - { - $stmt = $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' - ); - $stmt->execute(['integration_id' => $integrationId]); - - $rows = $stmt->fetchAll(); - if (!is_array($rows)) { - return []; - } - - $result = []; - foreach ($rows as $row) { - if (!is_array($row)) { - continue; - } - - $code = trim((string) ($row['shoppro_status_code'] ?? '')); - if ($code === '') { - continue; - } - - $result[$code] = [ - 'orderpro_status_code' => trim((string) ($row['orderpro_status_code'] ?? '')), - 'shoppro_status_name' => isset($row['shoppro_status_name']) ? trim((string) $row['shoppro_status_name']) : null, - ]; - } - - return $result; - } - - /** - * @param array $mappings - */ - public function replaceForIntegration(int $integrationId, array $mappings): void - { - $deleteStmt = $this->pdo->prepare('DELETE FROM order_status_mappings WHERE integration_id = :integration_id'); - $deleteStmt->execute(['integration_id' => $integrationId]); - - if ($mappings === []) { - return; - } - - $insertStmt = $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, :created_at, :updated_at - )' - ); - - $now = date('Y-m-d H:i:s'); - foreach ($mappings as $mapping) { - $shopCode = trim((string) ($mapping['shoppro_status_code'] ?? '')); - $orderCode = trim((string) ($mapping['orderpro_status_code'] ?? '')); - if ($shopCode === '' || $orderCode === '') { - continue; - } - - $shopNameRaw = isset($mapping['shoppro_status_name']) ? trim((string) $mapping['shoppro_status_name']) : ''; - $shopName = $shopNameRaw === '' ? null : $shopNameRaw; - - $insertStmt->execute([ - 'integration_id' => $integrationId, - 'shoppro_status_code' => $shopCode, - 'shoppro_status_name' => $shopName, - 'orderpro_status_code' => $orderCode, - 'created_at' => $now, - 'updated_at' => $now, - ]); - } - } - - /** - * @return array - */ - public function listOrderProToShopProMap(int $integrationId): array - { - $rows = $this->listByIntegration($integrationId); - if ($rows === []) { - return []; - } - - $result = []; - foreach ($rows as $shopCode => $mapping) { - $orderProCode = trim((string) ($mapping['orderpro_status_code'] ?? '')); - $normalizedOrderProCode = $this->normalizeCode($orderProCode); - $normalizedShopCode = $this->normalizeCode((string) $shopCode); - if ($normalizedOrderProCode === '' || $normalizedShopCode === '') { - continue; - } - - if (!isset($result[$normalizedOrderProCode])) { - $result[$normalizedOrderProCode] = $normalizedShopCode; - } - } - - return $result; - } - - private function normalizeCode(string $value): string - { - return trim(mb_strtolower($value)); - } -} diff --git a/archive/2026-03-02_users-only-reset/src/Modules/Settings/SettingsController.php b/archive/2026-03-02_users-only-reset/src/Modules/Settings/SettingsController.php deleted file mode 100644 index d8f89e1..0000000 --- a/archive/2026-03-02_users-only-reset/src/Modules/Settings/SettingsController.php +++ /dev/null @@ -1,1725 +0,0 @@ -migrator->status(); - - $html = $this->template->render('settings/database', [ - 'title' => $this->translator->get('settings.database.title'), - 'activeMenu' => 'settings', - 'activeSettings' => 'database', - 'user' => $this->auth->user(), - 'csrfToken' => Csrf::token(), - 'marketplaceIntegrations' => $this->marketplaceIntegrations(), - 'status' => $status, - 'errorMessage' => (string) Flash::get('settings_error', ''), - 'successMessage' => (string) Flash::get('settings_success', ''), - 'runLogs' => (array) Flash::get('settings_migrate_logs', []), - ], 'layouts/app'); - - return Response::html($html); - } - - public function migrate(Request $request): Response - { - $csrfToken = (string) $request->input('_token', ''); - if (!Csrf::validate($csrfToken)) { - Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired')); - return Response::redirect('/settings/database'); - } - - try { - $result = $this->migrator->runPending(); - $executed = (int) ($result['executed'] ?? 0); - $skipped = (int) ($result['skipped'] ?? 0); - $logs = (array) ($result['logs'] ?? []); - - Flash::set( - 'settings_success', - $this->translator->get('settings.database.flash.updated', [ - 'executed' => (string) $executed, - 'skipped' => (string) $skipped, - ]) - ); - Flash::set('settings_migrate_logs', $logs); - } catch (Throwable $exception) { - Flash::set( - 'settings_error', - $this->translator->get('settings.database.flash.failed') . ' ' . $exception->getMessage() - ); - Flash::set('settings_migrate_logs', ['[error] ' . $exception->getMessage()]); - } - - return Response::redirect('/settings/database'); - } - - public function cron(Request $request): Response - { - $runOnWebEnabled = false; - $webLimit = 5; - $errorMessage = (string) Flash::get('settings_error', ''); - $successMessage = (string) Flash::get('settings_success', ''); - - try { - $runOnWebEnabled = $this->appSettings->getBool('cron_run_on_web', false); - $webLimit = max(1, min(100, $this->appSettings->getInt('cron_web_limit', 5))); - $schedules = $this->cronJobs->listSchedules(100); - $futureJobs = $this->cronJobs->listFutureJobs(150); - $pastJobs = $this->cronJobs->listPastJobs(150); - } catch (Throwable $exception) { - $schedules = []; - $futureJobs = []; - $pastJobs = []; - if ($errorMessage === '') { - $errorMessage = $this->translator->get('settings.cron.flash.load_failed') . ' ' . $exception->getMessage(); - } - } - - $html = $this->template->render('settings/cron', [ - 'title' => $this->translator->get('settings.cron.title'), - 'activeMenu' => 'cron', - 'activeSettings' => 'cron', - 'user' => $this->auth->user(), - 'csrfToken' => Csrf::token(), - 'marketplaceIntegrations' => $this->marketplaceIntegrations(), - 'runOnWebEnabled' => $runOnWebEnabled, - 'webCronLimit' => $webLimit, - 'schedules' => $schedules, - 'futureJobs' => $futureJobs, - 'pastJobs' => $pastJobs, - 'errorMessage' => $errorMessage, - 'successMessage' => $successMessage, - ], 'layouts/app'); - - return Response::html($html); - } - - public function saveCronSettings(Request $request): Response - { - $csrfToken = (string) $request->input('_token', ''); - if (!Csrf::validate($csrfToken)) { - Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired')); - return Response::redirect('/settings/cron'); - } - - $runOnWeb = (string) $request->input('cron_run_on_web', '0') === '1'; - $webLimit = max(1, min(100, (int) $request->input('cron_web_limit', 5))); - - try { - $this->appSettings->set('cron_run_on_web', $runOnWeb ? '1' : '0'); - $this->appSettings->set('cron_web_limit', (string) $webLimit); - Flash::set('settings_success', $this->translator->get('settings.cron.flash.saved')); - } catch (Throwable $exception) { - Flash::set('settings_error', $this->translator->get('settings.cron.flash.save_failed') . ' ' . $exception->getMessage()); - } - - return Response::redirect('/settings/cron'); - } - - public function gs1(Request $request): Response - { - $errorMessage = (string) Flash::get('settings_error', ''); - $successMessage = (string) Flash::get('settings_success', ''); - - $html = $this->template->render('settings/gs1', [ - 'title' => $this->translator->get('settings.gs1.title'), - 'activeMenu' => 'settings', - 'activeSettings' => 'gs1', - 'user' => $this->auth->user(), - 'csrfToken' => Csrf::token(), - 'marketplaceIntegrations' => $this->marketplaceIntegrations(), - 'gs1ApiLogin' => $this->appSettings->get('gs1_api_login', ''), - 'gs1ApiPassword' => $this->appSettings->get('gs1_api_password', ''), - 'gs1Prefix' => $this->appSettings->get('gs1_prefix', '590532390'), - 'gs1DefaultBrand' => $this->appSettings->get('gs1_default_brand', 'marianek.pl'), - 'gs1DefaultGpcCode' => $this->appSettings->get('gs1_default_gpc_code', '10008365'), - 'errorMessage' => $errorMessage, - 'successMessage' => $successMessage, - ], 'layouts/app'); - - return Response::html($html); - } - - public function gs1Save(Request $request): Response - { - $csrfToken = (string) $request->input('_token', ''); - if (!Csrf::validate($csrfToken)) { - Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired')); - return Response::redirect('/settings/gs1'); - } - - try { - $this->appSettings->set('gs1_api_login', trim((string) $request->input('gs1_api_login', ''))); - $this->appSettings->set('gs1_api_password', trim((string) $request->input('gs1_api_password', ''))); - $this->appSettings->set('gs1_prefix', trim((string) $request->input('gs1_prefix', '590532390'))); - $this->appSettings->set('gs1_default_brand', trim((string) $request->input('gs1_default_brand', 'marianek.pl'))); - $this->appSettings->set('gs1_default_gpc_code', trim((string) $request->input('gs1_default_gpc_code', '10008365'))); - Flash::set('settings_success', $this->translator->get('settings.gs1.flash.saved')); - } catch (Throwable $exception) { - Flash::set('settings_error', $this->translator->get('settings.gs1.flash.save_failed') . ' ' . $exception->getMessage()); - } - - return Response::redirect('/settings/gs1'); - } - - public function products(Request $request): Response - { - $errorMessage = (string) Flash::get('settings_error', ''); - $successMessage = (string) Flash::get('settings_success', ''); - - $html = $this->template->render('settings/products', [ - 'title' => $this->translator->get('settings.products.title'), - 'activeMenu' => 'settings', - 'activeSettings' => 'products', - 'user' => $this->auth->user(), - 'csrfToken' => Csrf::token(), - 'marketplaceIntegrations' => $this->marketplaceIntegrations(), - 'productsSkuFormat' => $this->appSettings->get('products_sku_format', 'PP000000'), - 'errorMessage' => $errorMessage, - 'successMessage' => $successMessage, - ], 'layouts/app'); - - return Response::html($html); - } - - public function productsSave(Request $request): Response - { - $csrfToken = (string) $request->input('_token', ''); - if (!Csrf::validate($csrfToken)) { - Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired')); - return Response::redirect('/settings/products'); - } - - $skuFormat = trim((string) $request->input('products_sku_format', 'PP000000')); - if ($skuFormat === '') { - $skuFormat = 'PP000000'; - } - - if (mb_strlen($skuFormat) > 128) { - Flash::set('settings_error', $this->translator->get('settings.products.flash.invalid_too_long')); - return Response::redirect('/settings/products'); - } - - if (preg_match('/0+/', $skuFormat) !== 1) { - Flash::set('settings_error', $this->translator->get('settings.products.flash.invalid_no_counter')); - return Response::redirect('/settings/products'); - } - - try { - $this->appSettings->set('products_sku_format', $skuFormat); - Flash::set('settings_success', $this->translator->get('settings.products.flash.saved')); - } catch (Throwable $exception) { - Flash::set( - 'settings_error', - $this->translator->get('settings.products.flash.save_failed') . ' ' . $exception->getMessage() - ); - } - - return Response::redirect('/settings/products'); - } - - public function orderStatuses(Request $request): Response - { - $errorMessage = (string) Flash::get('settings_error', ''); - $successMessage = (string) Flash::get('settings_success', ''); - - $integrations = array_values(array_filter( - $this->integrations->listByType('shoppro'), - static fn (array $row): bool => ($row['has_api_key'] ?? false) === true - )); - - $selectedIntegrationId = max(0, (int) $request->input('integration_id', 0)); - if ($selectedIntegrationId <= 0 && $integrations !== []) { - $selectedIntegrationId = (int) ($integrations[0]['id'] ?? 0); - } - - $shopProStatuses = []; - $savedMappings = []; - if ($selectedIntegrationId > 0) { - $savedMappings = $this->orderStatusMappings->listByIntegration($selectedIntegrationId); - try { - $credentials = $this->integrations->findApiCredentials($selectedIntegrationId); - if ($credentials === null || trim((string) ($credentials['api_key'] ?? '')) === '') { - if ($errorMessage === '') { - $errorMessage = $this->translator->get('settings.order_statuses.flash.credentials_missing'); - } - } else { - $statusesResult = $this->shopProClient->fetchOrderStatuses( - (string) ($credentials['base_url'] ?? ''), - (string) ($credentials['api_key'] ?? ''), - (int) ($credentials['timeout_seconds'] ?? 10) - ); - if (($statusesResult['ok'] ?? false) === true) { - $shopProStatuses = (array) ($statusesResult['statuses'] ?? []); - } elseif ($errorMessage === '') { - $errorMessage = $this->translator->get('settings.order_statuses.flash.load_failed') - . ' ' - . trim((string) ($statusesResult['message'] ?? '')); - } - } - } catch (Throwable $exception) { - if ($errorMessage === '') { - $errorMessage = $this->translator->get('settings.order_statuses.flash.load_failed') . ' ' . $exception->getMessage(); - } - } - } - - $fallbackStatuses = [ - ['code' => 'new', 'name' => 'Nowe'], - ['code' => 'confirmed', 'name' => 'Potwierdzone'], - ['code' => 'in_progress', 'name' => 'W realizacji'], - ['code' => 'ready_to_ship', 'name' => 'Gotowe do wysylki'], - ['code' => 'shipped', 'name' => 'Wyslane'], - ['code' => 'completed', 'name' => 'Zakonczone'], - ['code' => 'cancelled', 'name' => 'Anulowane'], - ['code' => 'returned', 'name' => 'Zwrocone'], - ]; - if ($shopProStatuses === []) { - $shopProStatuses = $fallbackStatuses; - } - - $shopProStatusesMap = []; - foreach ($shopProStatuses as $status) { - $code = $this->normalizeStatusCode((string) ($status['code'] ?? '')); - if ($code === '') { - continue; - } - - $name = trim((string) ($status['name'] ?? '')); - $shopProStatusesMap[$code] = [ - 'code' => $code, - 'name' => $name !== '' ? $name : $code, - 'mapped_orderpro_status' => (string) (($savedMappings[$code]['orderpro_status_code'] ?? '')), - ]; - } - - foreach ($savedMappings as $shopCode => $mapping) { - $normalizedCode = $this->normalizeStatusCode((string) $shopCode); - if ($normalizedCode === '' || isset($shopProStatusesMap[$normalizedCode])) { - continue; - } - - $savedName = trim((string) ($mapping['shoppro_status_name'] ?? '')); - $shopProStatusesMap[$normalizedCode] = [ - 'code' => $normalizedCode, - 'name' => $savedName !== '' ? $savedName : $normalizedCode, - 'mapped_orderpro_status' => (string) ($mapping['orderpro_status_code'] ?? ''), - ]; - } - - $html = $this->template->render('settings/order-statuses', [ - 'title' => $this->translator->get('settings.order_statuses.title'), - 'activeMenu' => 'settings', - 'activeSettings' => 'order_statuses', - 'user' => $this->auth->user(), - 'csrfToken' => Csrf::token(), - 'marketplaceIntegrations' => $this->marketplaceIntegrations(), - 'integrations' => $integrations, - 'selectedIntegrationId' => $selectedIntegrationId, - 'shopProStatuses' => array_values($shopProStatusesMap), - 'orderProStatusOptions' => $this->orderProStatuses(array_values($shopProStatusesMap)), - 'errorMessage' => $errorMessage, - 'successMessage' => $successMessage, - ], 'layouts/app'); - - return Response::html($html); - } - - public function saveOrderStatuses(Request $request): Response - { - $csrfToken = (string) $request->input('_token', ''); - if (!Csrf::validate($csrfToken)) { - Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired')); - return Response::redirect('/settings/order-statuses'); - } - - $integrationId = max(0, (int) $request->input('integration_id', 0)); - if ($integrationId <= 0) { - Flash::set('settings_error', $this->translator->get('settings.order_statuses.flash.integration_required')); - return Response::redirect('/settings/order-statuses'); - } - - $integration = $this->integrations->findById($integrationId); - if ($integration === null) { - Flash::set('settings_error', $this->translator->get('settings.integrations.flash.not_found')); - return Response::redirect('/settings/order-statuses'); - } - - $rawMappings = $request->input('mappings', []); - $rawShopNames = $request->input('shoppro_names', []); - $mappings = []; - if (is_array($rawMappings)) { - foreach ($rawMappings as $shopCodeRaw => $orderProCodeRaw) { - $shopCode = $this->normalizeStatusCode((string) $shopCodeRaw); - $orderProCode = $this->normalizeStatusCode((string) $orderProCodeRaw); - if ($shopCode === '' || $orderProCode === '') { - continue; - } - - $shopName = ''; - if (is_array($rawShopNames) && array_key_exists((string) $shopCodeRaw, $rawShopNames)) { - $shopName = trim((string) $rawShopNames[(string) $shopCodeRaw]); - } - if ($shopName === '' && is_array($rawShopNames) && array_key_exists($shopCode, $rawShopNames)) { - $shopName = trim((string) $rawShopNames[$shopCode]); - } - - $mappings[] = [ - 'shoppro_status_code' => $shopCode, - 'shoppro_status_name' => $shopName !== '' ? $shopName : null, - 'orderpro_status_code' => $orderProCode, - ]; - } - } - - try { - $this->orderStatusMappings->replaceForIntegration($integrationId, $mappings); - Flash::set('settings_success', $this->translator->get('settings.order_statuses.flash.saved')); - } catch (Throwable $exception) { - Flash::set('settings_error', $this->translator->get('settings.order_statuses.flash.save_failed') . ' ' . $exception->getMessage()); - } - - return Response::redirect('/settings/order-statuses?integration_id=' . $integrationId); - } - - public function integrations(Request $request): Response - { - $integrationId = max(0, (int) $request->input('id', 0)); - $selected = $integrationId > 0 ? $this->integrations->findById($integrationId) : null; - - if ($integrationId > 0 && $selected === null) { - Flash::set('settings_error', $this->translator->get('settings.integrations.flash.not_found')); - return Response::redirect('/settings/integrations/shoppro'); - } - - $old = (array) Flash::get('settings_integrations_old', []); - $baseForm = [ - 'integration_id' => $selected['id'] ?? 0, - 'name' => $selected['name'] ?? '', - 'base_url' => $selected['base_url'] ?? 'https://shoppro.project-dc.pl/', - 'timeout_seconds' => (string) ($selected['timeout_seconds'] ?? 10), - 'is_active' => (string) (($selected['is_active'] ?? true) ? '1' : '0'), - 'orders_fetch_enabled' => (string) (($selected['orders_fetch_enabled'] ?? false) ? '1' : '0'), - 'orders_fetch_start_date' => (string) ($selected['orders_fetch_start_date'] ?? ''), - 'order_status_sync_direction' => (string) ($selected['order_status_sync_direction'] ?? 'shoppro_to_orderpro'), - 'api_key' => '', - ]; - $form = array_merge($baseForm, $old); - $list = $this->integrations->listByType('shoppro'); - $recentTests = $selected !== null ? $this->integrations->recentTests((int) $selected['id']) : []; - - $html = $this->template->render('settings/integrations', [ - 'title' => $this->translator->get('settings.integrations.title'), - 'activeMenu' => 'settings', - 'activeSettings' => 'integrations', - 'user' => $this->auth->user(), - 'csrfToken' => Csrf::token(), - 'marketplaceIntegrations' => $this->marketplaceIntegrations(), - 'integrations' => $list, - 'selectedIntegration' => $selected, - 'recentTests' => $recentTests, - 'form' => $form, - 'errorMessage' => (string) Flash::get('settings_error', ''), - 'successMessage' => (string) Flash::get('settings_success', ''), - ], 'layouts/app'); - - return Response::html($html); - } - - public function saveIntegration(Request $request): Response - { - $csrfToken = (string) $request->input('_token', ''); - if (!Csrf::validate($csrfToken)) { - Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired')); - return Response::redirect('/settings/integrations/shoppro'); - } - - $integrationId = max(0, (int) $request->input('integration_id', 0)); - $name = trim((string) $request->input('name', '')); - $baseUrl = $this->normalizeBaseUrl((string) $request->input('base_url', '')); - $apiKey = trim((string) $request->input('api_key', '')); - $timeoutSeconds = max(3, min(60, (int) $request->input('timeout_seconds', 10))); - $isActive = (string) $request->input('is_active', '0') === '1'; - $ordersFetchEnabled = (string) $request->input('orders_fetch_enabled', '0') === '1'; - $ordersFetchStartDateInput = trim((string) $request->input('orders_fetch_start_date', '')); - $ordersFetchStartDate = $this->normalizeDateInput($ordersFetchStartDateInput); - $orderStatusSyncDirection = $this->normalizeOrderStatusSyncDirection( - (string) $request->input('order_status_sync_direction', 'shoppro_to_orderpro') - ); - - Flash::set('settings_integrations_old', [ - 'integration_id' => (string) $integrationId, - 'name' => $name, - 'base_url' => $baseUrl, - 'timeout_seconds' => (string) $timeoutSeconds, - 'is_active' => $isActive ? '1' : '0', - 'orders_fetch_enabled' => $ordersFetchEnabled ? '1' : '0', - 'orders_fetch_start_date' => $ordersFetchStartDateInput, - 'order_status_sync_direction' => $orderStatusSyncDirection, - ]); - - if (mb_strlen($name) < 2) { - Flash::set('settings_error', $this->translator->get('settings.integrations.validation.name_min')); - return Response::redirect($this->integrationRedirectPath($integrationId)); - } - - if (!$this->isValidHttpUrl($baseUrl)) { - Flash::set('settings_error', $this->translator->get('settings.integrations.validation.base_url_invalid')); - return Response::redirect($this->integrationRedirectPath($integrationId)); - } - - if ($ordersFetchStartDateInput !== '' && $ordersFetchStartDate === null) { - Flash::set('settings_error', $this->translator->get('settings.integrations.validation.orders_fetch_start_date_invalid')); - return Response::redirect($this->integrationRedirectPath($integrationId)); - } - - if ($integrationId === 0 && $apiKey === '') { - Flash::set('settings_error', $this->translator->get('settings.integrations.validation.api_key_required')); - return Response::redirect('/settings/integrations/shoppro'); - } - - if ($this->integrations->nameExists('shoppro', $name, $integrationId > 0 ? $integrationId : null)) { - Flash::set('settings_error', $this->translator->get('settings.integrations.validation.name_taken')); - return Response::redirect($this->integrationRedirectPath($integrationId)); - } - - try { - if ($integrationId > 0) { - if ($this->integrations->findById($integrationId) === null) { - Flash::set('settings_error', $this->translator->get('settings.integrations.flash.not_found')); - return Response::redirect('/settings/integrations/shoppro'); - } - - $this->integrations->update( - $integrationId, - $name, - $baseUrl, - $timeoutSeconds, - $isActive, - $apiKey === '' ? null : $apiKey, - $ordersFetchEnabled, - $ordersFetchStartDate, - $orderStatusSyncDirection - ); - $savedId = $integrationId; - Flash::set('settings_success', $this->translator->get('settings.integrations.flash.updated')); - } else { - $savedId = $this->integrations->create( - 'shoppro', - $name, - $baseUrl, - $timeoutSeconds, - $isActive, - $apiKey, - $ordersFetchEnabled, - $ordersFetchStartDate, - $orderStatusSyncDirection - ); - Flash::set('settings_success', $this->translator->get('settings.integrations.flash.created')); - } - } catch (Throwable $exception) { - Flash::set('settings_error', $this->translator->get('settings.integrations.flash.failed') . ' ' . $exception->getMessage()); - return Response::redirect($this->integrationRedirectPath($integrationId)); - } - - Flash::set('settings_integrations_old', []); - - return Response::redirect('/settings/integrations/shoppro?id=' . $savedId); - } - - public function testIntegration(Request $request): Response - { - $csrfToken = (string) $request->input('_token', ''); - if (!Csrf::validate($csrfToken)) { - Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired')); - return Response::redirect('/settings/integrations/shoppro'); - } - - $integrationId = max(0, (int) $request->input('integration_id', 0)); - if ($integrationId <= 0) { - Flash::set('settings_error', $this->translator->get('settings.integrations.flash.not_found')); - return Response::redirect('/settings/integrations/shoppro'); - } - - try { - $credentials = $this->integrations->findApiCredentials($integrationId); - } catch (Throwable $exception) { - Flash::set('settings_error', $this->translator->get('settings.integrations.flash.failed') . ' ' . $exception->getMessage()); - return Response::redirect('/settings/integrations/shoppro?id=' . $integrationId); - } - - if ($credentials === null) { - Flash::set('settings_error', $this->translator->get('settings.integrations.flash.not_found')); - return Response::redirect('/settings/integrations/shoppro'); - } - - $apiKey = (string) ($credentials['api_key'] ?? ''); - if ($apiKey === '') { - Flash::set('settings_error', $this->translator->get('settings.integrations.validation.api_key_required')); - return Response::redirect('/settings/integrations/shoppro?id=' . $integrationId); - } - - $result = $this->shopProClient->testConnection( - (string) ($credentials['base_url'] ?? ''), - $apiKey, - (int) ($credentials['timeout_seconds'] ?? 10) - ); - - $this->integrations->setTestResult( - $integrationId, - (string) ($result['status'] ?? 'error'), - isset($result['http_code']) ? (int) $result['http_code'] : null, - (string) ($result['message'] ?? ''), - (string) ($result['tested_at'] ?? date('Y-m-d H:i:s')) - ); - - $this->integrations->logTest( - $integrationId, - (string) ($result['status'] ?? 'error'), - isset($result['http_code']) ? (int) $result['http_code'] : null, - (string) ($result['message'] ?? ''), - (string) ($result['endpoint_url'] ?? ''), - (string) ($result['tested_at'] ?? date('Y-m-d H:i:s')) - ); - - if (($result['ok'] ?? false) === true) { - Flash::set('settings_success', $this->translator->get('settings.integrations.flash.test_ok')); - } else { - Flash::set('settings_error', $this->translator->get('settings.integrations.flash.test_failed') . ' ' . (string) ($result['message'] ?? '')); - } - - return Response::redirect('/settings/integrations/shoppro?id=' . $integrationId); - } - - public function importOneProduct(Request $request): Response - { - $csrfToken = (string) $request->input('_token', ''); - if (!Csrf::validate($csrfToken)) { - Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired')); - return Response::redirect('/settings/integrations/shoppro'); - } - - $integrationId = max(0, (int) $request->input('integration_id', 0)); - $requestedProductId = max(0, (int) $request->input('external_product_id', 0)); - - try { - $credentials = $integrationId > 0 - ? $this->integrations->findApiCredentials($integrationId) - : $this->integrations->findActiveApiCredentialsByType('shoppro'); - } catch (Throwable $exception) { - Flash::set('settings_error', $this->translator->get('settings.integrations.flash.import_failed') . ' ' . $exception->getMessage()); - return Response::redirect('/settings/integrations/shoppro' . ($integrationId > 0 ? ('?id=' . $integrationId) : '')); - } - - if ($credentials === null) { - Flash::set('settings_error', $this->translator->get('settings.integrations.flash.not_found')); - return Response::redirect('/settings/integrations/shoppro'); - } - - $resolvedIntegrationId = (int) ($credentials['id'] ?? 0); - $apiKey = (string) ($credentials['api_key'] ?? ''); - if ($apiKey === '') { - Flash::set('settings_error', $this->translator->get('settings.integrations.validation.api_key_required')); - return Response::redirect('/settings/integrations/shoppro?id=' . $resolvedIntegrationId); - } - - $baseUrl = (string) ($credentials['base_url'] ?? ''); - $timeout = (int) ($credentials['timeout_seconds'] ?? 10); - - $externalProductId = $requestedProductId; - if ($externalProductId <= 0) { - $listResult = $this->shopProClient->fetchProducts($baseUrl, $apiKey, $timeout, 1, 1); - if (($listResult['ok'] ?? false) !== true) { - Flash::set('settings_error', $this->translator->get('settings.integrations.flash.import_failed') . ' ' . (string) ($listResult['message'] ?? '')); - return Response::redirect('/settings/integrations/shoppro?id=' . $resolvedIntegrationId); - } - - $items = (array) ($listResult['items'] ?? []); - if ($items === []) { - Flash::set('settings_error', $this->translator->get('settings.integrations.flash.import_no_products')); - return Response::redirect('/settings/integrations/shoppro?id=' . $resolvedIntegrationId); - } - - $externalProductId = (int) ($items[0]['id'] ?? 0); - if ($externalProductId <= 0) { - Flash::set('settings_error', $this->translator->get('settings.integrations.flash.import_failed') . ' Niepoprawne ID produktu w odpowiedzi API.'); - return Response::redirect('/settings/integrations/shoppro?id=' . $resolvedIntegrationId); - } - } - - $importResult = $this->importExternalProductById($credentials, $externalProductId); - if (($importResult['ok'] ?? false) !== true) { - Flash::set('settings_error', $this->translator->get('settings.integrations.flash.import_failed') . ' ' . (string) ($importResult['message'] ?? '')); - return Response::redirect('/settings/integrations/shoppro?id=' . $resolvedIntegrationId); - } - - $message = $this->translator->get('settings.integrations.flash.import_ok', [ - 'external_id' => (string) $externalProductId, - 'local_id' => (string) ($importResult['local_id'] ?? 0), - ]); - Flash::set('settings_success', $message); - - return Response::redirect('/settings/integrations/shoppro?id=' . $resolvedIntegrationId); - } - - public function importProducts(Request $request): Response - { - $csrfToken = (string) $request->input('_token', ''); - if (!Csrf::validate($csrfToken)) { - Flash::set('products_error', $this->translator->get('auth.errors.csrf_expired')); - return Response::redirect('/products'); - } - - $integrationId = max(0, (int) $request->input('integration_id', 0)); - $mode = (string) $request->input('import_mode', 'single'); - $externalProductId = max(0, (int) $request->input('external_product_id', 0)); - $importVariants = (string) $request->input('import_variants', '0') === '1'; - - if ($integrationId <= 0) { - Flash::set('products_error', $this->translator->get('products.import.flash.integration_required')); - return Response::redirect('/products'); - } - - try { - $credentials = $this->integrations->findApiCredentials($integrationId); - } catch (Throwable $exception) { - Flash::set('products_error', $this->translator->get('products.import.flash.failed') . ' ' . $exception->getMessage()); - return Response::redirect('/products'); - } - - if ($credentials === null) { - Flash::set('products_error', $this->translator->get('products.import.flash.integration_not_found')); - return Response::redirect('/products'); - } - - $apiKey = (string) ($credentials['api_key'] ?? ''); - if ($apiKey === '') { - Flash::set('products_error', $this->translator->get('products.import.flash.api_key_missing')); - return Response::redirect('/products'); - } - - if ($mode === 'single') { - if ($externalProductId <= 0) { - Flash::set('products_error', $this->translator->get('products.import.flash.single_id_required')); - return Response::redirect('/products'); - } - - $singleResult = $this->importExternalProductById($credentials, $externalProductId, $importVariants); - if (($singleResult['ok'] ?? false) !== true) { - Flash::set('products_error', $this->translator->get('products.import.flash.failed') . ' ' . (string) ($singleResult['message'] ?? '')); - return Response::redirect('/products'); - } - - Flash::set('products_success', $this->translator->get('products.import.flash.single_ok', [ - 'external_id' => (string) $externalProductId, - 'local_id' => (string) ($singleResult['local_id'] ?? 0), - ])); - - return Response::redirect('/products'); - } - - if ($mode !== 'all') { - Flash::set('products_error', $this->translator->get('products.import.flash.mode_invalid')); - return Response::redirect('/products'); - } - - $baseUrl = (string) ($credentials['base_url'] ?? ''); - $timeout = (int) ($credentials['timeout_seconds'] ?? 10); - - $page = 1; - $perPage = 50; - $maxPages = 200; - $imported = 0; - $failed = 0; - $errorMessages = []; - - while ($page <= $maxPages) { - $listResult = $this->shopProClient->fetchProducts($baseUrl, $apiKey, $timeout, $page, $perPage); - if (($listResult['ok'] ?? false) !== true) { - Flash::set('products_error', $this->translator->get('products.import.flash.failed') . ' ' . (string) ($listResult['message'] ?? '')); - return Response::redirect('/products'); - } - - $items = (array) ($listResult['items'] ?? []); - if ($items === []) { - break; - } - - foreach ($items as $item) { - $id = (int) ($item['id'] ?? 0); - if ($id <= 0) { - $failed++; - if (count($errorMessages) < 3) { - $errorMessages[] = 'Niepoprawne ID produktu w odpowiedzi listy.'; - } - continue; - } - - $singleResult = $this->importExternalProductById($credentials, $id, $importVariants); - if (($singleResult['ok'] ?? false) === true) { - $imported++; - } else { - $failed++; - if (count($errorMessages) < 3) { - $errorMessages[] = 'ID ' . $id . ': ' . (string) ($singleResult['message'] ?? 'blad importu'); - } - } - } - - if (count($items) < $perPage) { - break; - } - - $page++; - } - - if ($imported === 0 && $failed === 0) { - Flash::set('products_error', $this->translator->get('products.import.flash.no_products')); - return Response::redirect('/products'); - } - - $summary = $this->translator->get('products.import.flash.all_done', [ - 'imported' => (string) $imported, - 'failed' => (string) $failed, - ]); - - if ($failed > 0 && $errorMessages !== []) { - Flash::set('products_error', $summary . ' ' . implode(' | ', $errorMessages)); - if ($imported > 0) { - Flash::set('products_success', $summary); - } - } else { - Flash::set('products_success', $summary); - } - - return Response::redirect('/products'); - } - - public function importOffersCache(Request $request): Response - { - $csrfToken = (string) $request->input('_token', ''); - if (!Csrf::validate($csrfToken)) { - Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired')); - return Response::redirect('/settings/integrations/shoppro'); - } - - $integrationId = max(0, (int) $request->input('integration_id', 0)); - if ($integrationId <= 0) { - Flash::set('settings_error', $this->translator->get('settings.integrations.flash.not_found')); - return Response::redirect('/settings/integrations/shoppro'); - } - - try { - $credentials = $this->integrations->findApiCredentials($integrationId); - } catch (Throwable $exception) { - Flash::set('settings_error', $this->translator->get('settings.integrations.flash.import_offers_failed') . ' ' . $exception->getMessage()); - return Response::redirect('/settings/integrations/shoppro?id=' . $integrationId); - } - - if ($credentials === null) { - Flash::set('settings_error', $this->translator->get('settings.integrations.flash.not_found')); - return Response::redirect('/settings/integrations/shoppro'); - } - - try { - $this->integrations->ensureSalesChannelsSeeded(); - $result = $this->offerImportService->importShopProOffers($credentials); - } catch (Throwable $exception) { - Flash::set('settings_error', $this->translator->get('settings.integrations.flash.import_offers_failed') . ' ' . $exception->getMessage()); - return Response::redirect('/settings/integrations/shoppro?id=' . $integrationId); - } - - if (($result['ok'] ?? false) !== true) { - Flash::set('settings_error', $this->translator->get('settings.integrations.flash.import_offers_failed') . ' ' . (string) ($result['message'] ?? '')); - return Response::redirect('/settings/integrations/shoppro?id=' . $integrationId); - } - - $summary = $this->translator->get('settings.integrations.flash.import_offers_ok', [ - 'imported' => (string) ($result['imported'] ?? 0), - 'failed' => (string) ($result['failed'] ?? 0), - 'pages' => (string) ($result['pages'] ?? 0), - ]); - $details = trim((string) ($result['message'] ?? '')); - - if ($details !== '') { - Flash::set('settings_success', $summary . ' ' . $details); - } else { - Flash::set('settings_success', $summary); - } - - return Response::redirect('/settings/integrations/shoppro?id=' . $integrationId); - } - - private function normalizeBaseUrl(string $value): string - { - $url = trim($value); - if ($url === '') { - return ''; - } - - return rtrim($url, '/') . '/'; - } - - private function isValidHttpUrl(string $url): bool - { - if (!filter_var($url, FILTER_VALIDATE_URL)) { - return false; - } - - $scheme = strtolower((string) parse_url($url, PHP_URL_SCHEME)); - return in_array($scheme, ['http', 'https'], true); - } - - private function integrationRedirectPath(int $integrationId): string - { - return $integrationId > 0 - ? ('/settings/integrations/shoppro?id=' . $integrationId) - : '/settings/integrations/shoppro'; - } - - /** - * @param array $credentials - * @return array{ok:bool,local_id:int,message:string} - */ - private function importExternalProductById(array $credentials, int $externalProductId, bool $importVariants = false): array - { - if ($externalProductId <= 0) { - return [ - 'ok' => false, - 'local_id' => 0, - 'message' => 'Niepoprawne ID produktu do importu.', - ]; - } - - $productResult = $this->shopProClient->fetchProductById( - (string) ($credentials['base_url'] ?? ''), - (string) ($credentials['api_key'] ?? ''), - (int) ($credentials['timeout_seconds'] ?? 10), - $externalProductId - ); - if (($productResult['ok'] ?? false) !== true) { - return [ - 'ok' => false, - 'local_id' => 0, - 'message' => (string) ($productResult['message'] ?? 'Nie mozna pobrac produktu z shopPRO.'), - ]; - } - - $externalProduct = is_array($productResult['product'] ?? null) ? $productResult['product'] : null; - if ($externalProduct === null) { - return [ - 'ok' => false, - 'local_id' => 0, - 'message' => 'Brak danych produktu.', - ]; - } - - // DEBUG: log raw API response for GPSR + custom_fields - if ($this->logger !== null) { - $lang = $this->resolveProductLanguage($externalProduct); - $this->logger->info('import_debug_api_response', [ - 'external_product_id' => $externalProductId, - 'has_languages_key' => array_key_exists('languages', $externalProduct), - 'languages_keys' => is_array($externalProduct['languages'] ?? null) ? array_keys($externalProduct['languages']) : null, - 'security_information_key_exists' => array_key_exists('security_information', $lang), - 'security_information_value' => array_key_exists('security_information', $lang) ? $lang['security_information'] : '(klucz nieobecny w tablicy)', - 'has_custom_fields_key' => array_key_exists('custom_fields', $externalProduct), - 'custom_fields_count' => is_array($externalProduct['custom_fields'] ?? null) ? count($externalProduct['custom_fields']) : null, - ]); - } - - $sku = trim((string) ($externalProduct['sku'] ?? '')); - $ean = trim((string) ($externalProduct['ean'] ?? '')); - $integrationId = max(0, (int) ($credentials['id'] ?? 0)); - - $mappedProductId = $this->integrations->findMappedProductId( - 'shoppro', - (string) $externalProductId, - $integrationId > 0 ? $integrationId : null - ); - $existingProductId = $mappedProductId; - if ($existingProductId === null && $sku !== '') { - $existingProductId = $this->products->findIdBySku($sku); - } - if ($existingProductId === null && $ean !== '') { - $existingProductId = $this->products->findIdByEan($ean); - } - - $normalized = $this->normalizeExternalProductForLocalSave($externalProduct, $externalProductId); - - // DEBUG: log what will be saved - if ($this->logger !== null) { - $this->logger->info('import_debug_normalized', [ - 'external_product_id' => $externalProductId, - 'security_information' => $normalized['translation']['security_information'] ?? '(brak klucza)', - 'custom_fields_json' => $normalized['product_create']['custom_fields_json'] ?? '(brak klucza)', - ]); - } - - $savedProductId = 0; - - try { - $this->pdo->beginTransaction(); - if ($existingProductId !== null && $existingProductId > 0) { - $savedProductId = $existingProductId; - $this->products->update($savedProductId, $normalized['product_update'], $normalized['translation']); - } else { - $savedProductId = $this->products->create($normalized['product_create'], $normalized['translation']); - } - - $this->integrations->ensureSalesChannelsSeeded(); - $this->integrations->upsertProductChannelMap( - $savedProductId, - 'shoppro', - 'listed', - (string) $externalProductId, - '', - $integrationId > 0 ? $integrationId : null - ); - $this->integrations->upsertProductChannelMap($savedProductId, 'allegro', 'unknown', '', ''); - $this->integrations->upsertProductChannelMap($savedProductId, 'erli', 'not_available', '', ''); - $this->syncImportedProductImages( - $savedProductId, - $externalProduct, - (string) ($credentials['base_url'] ?? '') - ); - if ($importVariants) { - $variantWarnings = $this->syncImportedProductVariants($savedProductId, $externalProduct); - $this->storeProductImportVariantWarnings($savedProductId, $variantWarnings); - } - // Save per-integration content override - if ($integrationId > 0) { - $this->products->upsertIntegrationTranslation( - $savedProductId, - $integrationId, - $normalized['translation']['name'] ?? null, - $normalized['translation']['short_description'] ?? null, - $normalized['translation']['description'] ?? null - ); - } - $this->pdo->commit(); - } catch (Throwable $exception) { - if ($this->pdo->inTransaction()) { - $this->pdo->rollBack(); - } - - return [ - 'ok' => false, - 'local_id' => 0, - 'message' => $exception->getMessage(), - ]; - } - - return [ - 'ok' => true, - 'local_id' => $savedProductId, - 'message' => '', - ]; - } - - /** - * @param array $externalProduct - * @return array{product_create:array,product_update:array,translation:array} - */ - private function normalizeExternalProductForLocalSave(array $externalProduct, int $externalProductId): array - { - $now = date('Y-m-d H:i:s'); - $lang = $this->resolveProductLanguage($externalProduct); - - $name = trim((string) ($lang['name'] ?? '')); - if ($name === '') { - $name = 'shopPRO #' . $externalProductId; - } - - $shortDescription = trim((string) ($lang['short_description'] ?? '')); - $description = trim((string) ($lang['description'] ?? '')); - $metaTitle = trim((string) ($lang['meta_title'] ?? '')); - $metaDescription = trim((string) ($lang['meta_description'] ?? '')); - $metaKeywords = trim((string) ($lang['meta_keywords'] ?? '')); - $seoLink = trim((string) ($lang['seo_link'] ?? '')); - - $sku = trim((string) ($externalProduct['sku'] ?? '')); - $ean = trim((string) ($externalProduct['ean'] ?? '')); - $vat = $this->nullableFloat($externalProduct['vat'] ?? null, 2); - - $baseType = (array_key_exists('variants', $externalProduct) && is_array($externalProduct['variants']) && $externalProduct['variants'] !== []) - ? 'variant_parent' - : 'simple'; - - $customFields = is_array($externalProduct['custom_fields'] ?? null) && $externalProduct['custom_fields'] !== [] - ? json_encode($externalProduct['custom_fields'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) - : null; - - $common = [ - 'type' => $baseType, - 'sku' => $sku !== '' ? $sku : null, - 'ean' => $ean !== '' ? $ean : null, - 'status' => ((int) ($externalProduct['status'] ?? 1)) === 1 ? 1 : 0, - 'promoted' => ((int) ($externalProduct['promoted'] ?? 0)) === 1 ? 1 : 0, - 'new_to_date' => $this->normalizeExternalDate($externalProduct['new_to_date'] ?? null), - 'additional_message' => ((int) ($externalProduct['additional_message'] ?? 0)) === 1 ? 1 : 0, - 'additional_message_required' => ((int) ($externalProduct['additional_message_required'] ?? 0)) === 1 ? 1 : 0, - 'additional_message_text' => $this->nullableText($externalProduct['additional_message_text'] ?? null), - 'vat' => $vat, - 'weight' => $this->nullableFloat($externalProduct['weight'] ?? null, 3), - 'price_brutto' => round((float) ($externalProduct['price_brutto'] ?? 0), 2), - 'price_brutto_promo' => $this->nullableFloat($externalProduct['price_brutto_promo'] ?? null, 2), - 'price_netto' => $this->nullableFloat($externalProduct['price_netto'] ?? null, 2), - 'price_netto_promo' => $this->nullableFloat($externalProduct['price_netto_promo'] ?? null, 2), - 'quantity' => round((float) ($externalProduct['quantity'] ?? 0), 3), - 'producer_id' => $this->nullableInt($externalProduct['producer_id'] ?? null), - 'producer_name' => $this->nullableText($externalProduct['producer_name'] ?? null), - 'product_unit_id' => $this->nullableInt($externalProduct['product_unit_id'] ?? null), - 'custom_fields_json' => $customFields === false ? null : $customFields, - ]; - - $productCreate = $common + [ - 'uuid' => $this->uuidV4(), - 'created_at' => $now, - 'updated_at' => $now, - ]; - $productUpdate = $common + [ - 'updated_at' => $now, - ]; - - $securityInformation = trim((string) ($lang['security_information'] ?? '')); - - $translation = [ - 'lang' => 'pl', - 'name' => $name, - 'short_description' => $shortDescription !== '' ? $shortDescription : null, - 'description' => $description !== '' ? $description : null, - 'meta_title' => $metaTitle !== '' ? $metaTitle : null, - 'meta_description' => $metaDescription !== '' ? $metaDescription : null, - 'meta_keywords' => $metaKeywords !== '' ? $metaKeywords : null, - 'seo_link' => $seoLink !== '' ? $seoLink : null, - 'security_information' => $securityInformation !== '' ? $securityInformation : null, - 'created_at' => $now, - 'updated_at' => $now, - ]; - - return [ - 'product_create' => $productCreate, - 'product_update' => $productUpdate, - 'translation' => $translation, - ]; - } - - /** - * @param array $externalProduct - * @return array - */ - private function resolveProductLanguage(array $externalProduct): array - { - $languages = $externalProduct['languages'] ?? null; - if (!is_array($languages)) { - return []; - } - - if (isset($languages['pl']) && is_array($languages['pl'])) { - return $languages['pl']; - } - - foreach ($languages as $language) { - if (is_array($language)) { - return $language; - } - } - - return []; - } - - private function nullableInt(mixed $value): ?int - { - $text = trim((string) $value); - if ($text === '' || !is_numeric($text)) { - return null; - } - - return (int) $text; - } - - private function nullableFloat(mixed $value, int $precision = 2): ?float - { - $text = trim((string) $value); - if ($text === '' || !is_numeric($text)) { - return null; - } - - return round((float) $text, $precision); - } - - private function uuidV4(): string - { - $data = random_bytes(16); - $data[6] = chr((ord($data[6]) & 0x0f) | 0x40); - $data[8] = chr((ord($data[8]) & 0x3f) | 0x80); - - return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); - } - - /** - * @param array $externalProduct - */ - private function syncImportedProductImages(int $productId, array $externalProduct, string $baseUrl): void - { - $deleteStmt = $this->pdo->prepare('DELETE FROM product_images WHERE product_id = :product_id'); - $deleteStmt->execute(['product_id' => $productId]); - - $rows = $this->extractExternalImageRows($externalProduct, $baseUrl); - if ($rows === []) { - return; - } - - $insertStmt = $this->pdo->prepare( - 'INSERT INTO product_images (product_id, storage_path, alt, sort_order, is_main, created_at, updated_at) - VALUES (:product_id, :storage_path, :alt, :sort_order, :is_main, :created_at, :updated_at)' - ); - - $now = date('Y-m-d H:i:s'); - foreach ($rows as $index => $row) { - $insertStmt->execute([ - 'product_id' => $productId, - 'storage_path' => (string) ($row['storage_path'] ?? ''), - 'alt' => $this->nullableText($row['alt'] ?? null), - 'sort_order' => (int) ($row['sort_order'] ?? $index), - 'is_main' => ((int) ($row['is_main'] ?? 0)) === 1 ? 1 : 0, - 'created_at' => $now, - 'updated_at' => $now, - ]); - } - } - - /** - * @param array $externalProduct - */ - private function syncImportedProductVariants(int $productId, array $externalProduct): array - { - $this->deleteExistingVariantsForProduct($productId); - - $variants = $this->extractExternalVariants($externalProduct); - if ($variants === []) { - return []; - } - $warnings = []; - $seenSkus = []; - $seenPermutationHashes = []; - - $insertVariantStmt = $this->pdo->prepare( - 'INSERT INTO product_variants ( - product_id, permutation_hash, sku, ean, status, stock_0_buy, - price_brutto, price_brutto_promo, price_netto, price_netto_promo, weight, - created_at, updated_at - ) VALUES ( - :product_id, :permutation_hash, :sku, :ean, :status, :stock_0_buy, - :price_brutto, :price_brutto_promo, :price_netto, :price_netto_promo, :weight, - :created_at, :updated_at - )' - ); - $insertVariantAttrStmt = $this->pdo->prepare( - 'INSERT INTO product_variant_attributes (variant_id, attribute_id, value_id, created_at) - VALUES (:variant_id, :attribute_id, :value_id, :created_at)' - ); - $now = date('Y-m-d H:i:s'); - - foreach ($variants as $index => $variant) { - $permutationHash = trim((string) ($variant['permutation_hash'] ?? '')); - if ($permutationHash === '') { - $permutationHash = 'external-' . trim((string) ($variant['id'] ?? '')) . '-' . (string) $index; - } - if (isset($seenPermutationHashes[$permutationHash])) { - $warnings[] = 'Pominieto wariant ' . $this->buildExternalVariantLabel($variant, $index) . ' (duplikat permutation_hash: ' . $permutationHash . ').'; - continue; - } - $seenPermutationHashes[$permutationHash] = true; - - $variantSku = $this->nullableText($variant['sku'] ?? null); - if ($variantSku !== null) { - $normalizedSkuKey = mb_strtolower($variantSku); - if (isset($seenSkus[$normalizedSkuKey])) { - $warnings[] = 'Pominieto wariant ' . $this->buildExternalVariantLabel($variant, $index) . ' (duplikat SKU w imporcie: ' . $variantSku . ').'; - continue; - } - if ($this->products->existsVariantSku($variantSku, $productId)) { - $warnings[] = 'Pominieto wariant ' . $this->buildExternalVariantLabel($variant, $index) . ' (SKU zajete przez inny produkt: ' . $variantSku . ').'; - continue; - } - $seenSkus[$normalizedSkuKey] = true; - } - - try { - $insertVariantStmt->execute([ - 'product_id' => $productId, - 'permutation_hash' => $permutationHash, - 'sku' => $variantSku, - 'ean' => $this->nullableText($variant['ean'] ?? null), - 'status' => 1, - 'stock_0_buy' => ((int) ($variant['stock_0_buy'] ?? 0)) === 1 ? 1 : 0, - 'price_brutto' => $this->nullableFloat($variant['price_brutto'] ?? null, 2), - 'price_brutto_promo' => $this->nullableFloat($variant['price_brutto_promo'] ?? null, 2), - 'price_netto' => $this->nullableFloat($variant['price_netto'] ?? null, 2), - 'price_netto_promo' => $this->nullableFloat($variant['price_netto_promo'] ?? null, 2), - 'weight' => $this->nullableFloat($variant['weight'] ?? null, 3), - 'created_at' => $now, - 'updated_at' => $now, - ]); - } catch (Throwable $exception) { - $warnings[] = 'Pominieto wariant ' . $this->buildExternalVariantLabel($variant, $index) . ' (' . $exception->getMessage() . ').'; - continue; - } - $variantId = (int) $this->pdo->lastInsertId(); - if ($variantId <= 0) { - $warnings[] = 'Pominieto wariant ' . $this->buildExternalVariantLabel($variant, $index) . ' (nie udalo sie zapisac rekordu wariantu).'; - continue; - } - - $variantAttributes = $this->extractExternalVariantAttributes($variant); - foreach ($variantAttributes as $variantAttribute) { - $attributeId = max(0, (int) ($variantAttribute['attribute_id'] ?? 0)); - $valueId = max(0, (int) ($variantAttribute['value_id'] ?? 0)); - if ($attributeId <= 0 || $valueId <= 0) { - continue; - } - - $this->upsertAttributeFromExternal($attributeId, $variantAttribute); - $this->upsertAttributeValueFromExternal($attributeId, $valueId, $variantAttribute); - $insertVariantAttrStmt->execute([ - 'variant_id' => $variantId, - 'attribute_id' => $attributeId, - 'value_id' => $valueId, - 'created_at' => $now, - ]); - } - } - - return $warnings; - } - - /** - * @param array $externalProduct - * @return array - */ - private function extractExternalImageRows(array $externalProduct, string $baseUrl): array - { - $rawImages = []; - if (isset($externalProduct['images']) && is_array($externalProduct['images'])) { - $rawImages = $externalProduct['images']; - } elseif (isset($externalProduct['main_image'])) { - $rawImages = [['src' => (string) $externalProduct['main_image'], 'is_main' => 1]]; - } - - $rows = []; - $seenPaths = []; - $hasMain = false; - - foreach ($rawImages as $index => $rawImage) { - if (is_array($rawImage)) { - $src = trim((string) ($rawImage['src'] ?? $rawImage['url'] ?? $rawImage['storage_path'] ?? '')); - $alt = $this->nullableText($rawImage['alt'] ?? $rawImage['title'] ?? null); - $sortOrder = $this->nullableInt($rawImage['sort_order'] ?? $rawImage['order'] ?? $rawImage['position'] ?? null); - $isMain = ((int) ($rawImage['is_main'] ?? $rawImage['main'] ?? $rawImage['default'] ?? 0)) === 1; - } else { - $src = trim((string) $rawImage); - $alt = null; - $sortOrder = null; - $isMain = false; - } - - if ($src === '') { - continue; - } - - $storagePath = $this->normalizeExternalImagePath($src, $baseUrl); - if ($storagePath === '' || isset($seenPaths[$storagePath])) { - continue; - } - - $seenPaths[$storagePath] = true; - $rows[] = [ - 'storage_path' => $storagePath, - 'alt' => $alt, - 'sort_order' => $sortOrder ?? (int) $index, - 'is_main' => $isMain ? 1 : 0, - ]; - - if ($isMain) { - $hasMain = true; - } - } - - if ($rows !== [] && $hasMain === false) { - $rows[0]['is_main'] = 1; - } - - return $rows; - } - - private function normalizeExternalImagePath(string $path, string $baseUrl): string - { - $normalized = trim($path); - if ($normalized === '') { - return ''; - } - - if (preg_match('#^https?://#i', $normalized) === 1) { - return $normalized; - } - - if (str_starts_with($normalized, '//')) { - $scheme = parse_url($baseUrl, PHP_URL_SCHEME); - $scheme = is_string($scheme) && $scheme !== '' ? strtolower($scheme) : 'https'; - - return $scheme . ':' . $normalized; - } - - $base = rtrim(trim($baseUrl), '/'); - if ($base === '') { - return $normalized; - } - - if (str_starts_with($normalized, '/')) { - return $base . $normalized; - } - - return $base . '/' . ltrim($normalized, '/'); - } - - private function deleteExistingVariantsForProduct(int $productId): void - { - $deleteAttributesStmt = $this->pdo->prepare( - 'DELETE pva - FROM product_variant_attributes pva - INNER JOIN product_variants pv ON pv.id = pva.variant_id - WHERE pv.product_id = :product_id' - ); - $deleteAttributesStmt->execute(['product_id' => $productId]); - - $deleteVariantsStmt = $this->pdo->prepare('DELETE FROM product_variants WHERE product_id = :product_id'); - $deleteVariantsStmt->execute(['product_id' => $productId]); - } - - /** - * @param array $externalProduct - * @return array> - */ - private function extractExternalVariants(array $externalProduct): array - { - if (!isset($externalProduct['variants']) || !is_array($externalProduct['variants'])) { - return []; - } - - return array_values(array_filter( - $externalProduct['variants'], - static fn (mixed $item): bool => is_array($item) - )); - } - - /** - * @param array $variant - * @return array> - */ - private function extractExternalVariantAttributes(array $variant): array - { - if (!isset($variant['attributes']) || !is_array($variant['attributes'])) { - return []; - } - - return array_values(array_filter( - $variant['attributes'], - static fn (mixed $item): bool => is_array($item) - )); - } - - /** - * @param array $variantAttribute - */ - private function upsertAttributeFromExternal(int $attributeId, array $variantAttribute): void - { - $stmt = $this->pdo->prepare( - 'INSERT INTO attributes (id, type, status, created_at, updated_at) - VALUES (:id, :type, :status, :created_at, :updated_at) - ON DUPLICATE KEY UPDATE - type = VALUES(type), - status = VALUES(status), - updated_at = VALUES(updated_at)' - ); - $now = date('Y-m-d H:i:s'); - $stmt->execute([ - 'id' => $attributeId, - 'type' => max(1, (int) ($variantAttribute['attribute_type'] ?? 1)), - 'status' => 1, - 'created_at' => $now, - 'updated_at' => $now, - ]); - - $attributeNames = is_array($variantAttribute['attribute_names'] ?? null) ? $variantAttribute['attribute_names'] : []; - foreach ($this->resolveLocalizedNames($attributeNames) as $lang => $name) { - $translationStmt = $this->pdo->prepare( - 'INSERT INTO attribute_translations (attribute_id, lang, name, created_at, updated_at) - VALUES (:attribute_id, :lang, :name, :created_at, :updated_at) - ON DUPLICATE KEY UPDATE - name = VALUES(name), - updated_at = VALUES(updated_at)' - ); - $translationStmt->execute([ - 'attribute_id' => $attributeId, - 'lang' => $lang, - 'name' => $name, - 'created_at' => $now, - 'updated_at' => $now, - ]); - } - } - - /** - * @param array $variantAttribute - */ - private function upsertAttributeValueFromExternal(int $attributeId, int $valueId, array $variantAttribute): void - { - $stmt = $this->pdo->prepare( - 'INSERT INTO attribute_values ( - id, attribute_id, status, is_default, impact_on_price, sort_order, created_at, updated_at - ) VALUES ( - :id, :attribute_id, :status, :is_default, :impact_on_price, :sort_order, :created_at, :updated_at - ) ON DUPLICATE KEY UPDATE - attribute_id = VALUES(attribute_id), - status = VALUES(status), - is_default = VALUES(is_default), - impact_on_price = VALUES(impact_on_price), - sort_order = VALUES(sort_order), - updated_at = VALUES(updated_at)' - ); - $now = date('Y-m-d H:i:s'); - $stmt->execute([ - 'id' => $valueId, - 'attribute_id' => $attributeId, - 'status' => 1, - 'is_default' => 0, - 'impact_on_price' => null, - 'sort_order' => 0, - 'created_at' => $now, - 'updated_at' => $now, - ]); - - $valueNames = is_array($variantAttribute['value_names'] ?? null) ? $variantAttribute['value_names'] : []; - foreach ($this->resolveLocalizedNames($valueNames) as $lang => $name) { - $translationStmt = $this->pdo->prepare( - 'INSERT INTO attribute_value_translations (value_id, lang, name, created_at, updated_at) - VALUES (:value_id, :lang, :name, :created_at, :updated_at) - ON DUPLICATE KEY UPDATE - name = VALUES(name), - updated_at = VALUES(updated_at)' - ); - $translationStmt->execute([ - 'value_id' => $valueId, - 'lang' => $lang, - 'name' => $name, - 'created_at' => $now, - 'updated_at' => $now, - ]); - } - } - - /** - * @param array $namesByLang - * @return array - */ - private function resolveLocalizedNames(array $namesByLang): array - { - $result = []; - foreach ($namesByLang as $lang => $value) { - $language = trim((string) $lang); - $name = trim((string) $value); - if ($language === '' || $name === '') { - continue; - } - - $result[$language] = $name; - } - - if (isset($result['pl'])) { - return $result; - } - - foreach ($result as $name) { - $result['pl'] = $name; - break; - } - - return $result; - } - - /** - * @param array $warnings - */ - private function storeProductImportVariantWarnings(int $productId, array $warnings): void - { - if ($warnings === []) { - $this->products->logChange( - $productId, - null, - 'product_import_warning_clear', - null, - ['source' => 'shoppro_variants_import', 'messages' => []] - ); - return; - } - - $this->products->logChange( - $productId, - null, - 'product_import_warning', - null, - [ - 'source' => 'shoppro_variants_import', - 'title' => 'Czesc wariantow nie zostala zaimportowana.', - 'messages' => array_values($warnings), - ] - ); - } - - /** - * @param array $variant - */ - private function buildExternalVariantLabel(array $variant, int $fallbackIndex): string - { - $externalId = trim((string) ($variant['id'] ?? '')); - if ($externalId !== '') { - return '#ID ' . $externalId; - } - - $sku = trim((string) ($variant['sku'] ?? '')); - if ($sku !== '') { - return '#SKU ' . $sku; - } - - return '#pozycja ' . ($fallbackIndex + 1); - } - - private function nullableText(mixed $value): ?string - { - $text = trim((string) $value); - return $text === '' ? null : $text; - } - - private function normalizeExternalDate(mixed $value): ?string - { - $text = trim((string) $value); - if ($text === '') { - return null; - } - - $datePart = substr($text, 0, 10); - if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $datePart) !== 1) { - return null; - } - - return $datePart; - } - - private function normalizeDateInput(string $value): ?string - { - $text = trim($value); - if ($text === '') { - return null; - } - - if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $text) !== 1) { - return null; - } - - return $text; - } - - /** - * @return array - */ - private function orderProStatuses(array $shopProStatuses = []): array - { - $options = []; - - foreach ($shopProStatuses as $status) { - if (!is_array($status)) { - continue; - } - - $code = $this->normalizeStatusCode((string) ($status['code'] ?? '')); - if ($code === '' || isset($options[$code])) { - continue; - } - - $name = trim((string) ($status['name'] ?? $code)); - $options[$code] = $name !== '' ? $name : $code; - } - - return $options; - } - - private function normalizeStatusCode(string $value): string - { - $normalized = trim(mb_strtolower($value)); - if ($normalized === '') { - return ''; - } - - return preg_replace('/[^a-z0-9_\-\.]+/u', '_', $normalized) ?? ''; - } - - private function normalizeOrderStatusSyncDirection(string $value): string - { - $normalized = trim(mb_strtolower($value)); - if ($normalized === 'orderpro_to_shoppro') { - return 'orderpro_to_shoppro'; - } - - return 'shoppro_to_orderpro'; - } - - /** - * @return array> - */ - private function marketplaceIntegrations(): array - { - return array_values(array_filter( - $this->integrations->listByType('shoppro'), - static fn (array $row): bool => (bool) ($row['is_active'] ?? false) - )); - } -} diff --git a/archive/2026-03-02_users-only-reset/src/Modules/Settings/ShopProClient.php b/archive/2026-03-02_users-only-reset/src/Modules/Settings/ShopProClient.php deleted file mode 100644 index 8b5d427..0000000 --- a/archive/2026-03-02_users-only-reset/src/Modules/Settings/ShopProClient.php +++ /dev/null @@ -1,1138 +0,0 @@ ->,total:int,page:int,per_page:int} - */ - public function fetchProducts( - string $baseUrl, - string $apiKey, - int $timeoutSeconds, - int $page = 1, - int $perPage = 1 - ): array { - $normalizedBaseUrl = rtrim(trim($baseUrl), '/'); - $query = http_build_query([ - 'endpoint' => 'products', - 'action' => 'list', - 'page' => max(1, $page), - 'per_page' => max(1, min(100, $perPage)), - 'sort' => 'id', - 'sort_dir' => 'DESC', - 'status' => '1', - ]); - $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 listy produktow 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 = isset($data['items']) && is_array($data['items']) ? $data['items'] : []; - - return [ - 'ok' => true, - 'http_code' => $response['http_code'] ?? null, - 'message' => '', - 'items' => $items, - 'total' => (int) ($data['total'] ?? 0), - '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,items:array>,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 { - $normalizedBaseUrl = rtrim(trim($baseUrl), '/'); - $queryParams = [ - 'endpoint' => 'orders', - 'action' => 'list', - 'page' => max(1, $page), - 'per_page' => max(1, min(100, $perPage)), - 'sort' => 'updated_at', - 'sort_dir' => 'ASC', - ]; - $normalizedFromDate = trim((string) $fromDate); - if ($normalizedFromDate !== '') { - $queryParams['date_from'] = $normalizedFromDate; - $queryParams['updated_from'] = $normalizedFromDate; - } - - $endpointUrl = $normalizedBaseUrl . '/api.php?' . http_build_query($queryParams); - $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 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 $item): bool => is_array($item))), - '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|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, - ]; - } - - $normalizedBaseUrl = rtrim(trim($baseUrl), '/'); - $attempts = [ - $normalizedBaseUrl . '/api.php?' . http_build_query([ - 'endpoint' => 'orders', - 'action' => 'get', - 'id' => $id, - ]), - $normalizedBaseUrl . '/api.php?' . http_build_query([ - 'endpoint' => 'orders', - 'action' => 'details', - 'id' => $id, - ]), - ]; - - $lastMessage = 'Nie mozna pobrac szczegolow zamowienia z shopPRO.'; - $lastHttpCode = null; - - foreach ($attempts as $endpointUrl) { - $response = $this->requestJson($endpointUrl, $apiKey, $timeoutSeconds); - if (($response['ok'] ?? false) !== true) { - $lastMessage = trim((string) ($response['message'] ?? $lastMessage)); - $lastHttpCode = isset($response['http_code']) ? (int) $response['http_code'] : null; - continue; - } - - $data = $response['data'] ?? null; - if (is_array($data)) { - 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' => $lastHttpCode, - 'message' => $lastMessage, - 'order' => null, - ]; - } - - /** - * @return array{ok:bool,http_code:int|null,message:string,product:array|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 do importu.', - '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, - ]; - } - - /** - * @param array $payload - * @return array{ok:bool,http_code:int|null,message:string,external_id:int} - */ - public function createProduct(string $baseUrl, string $apiKey, int $timeoutSeconds, array $payload): array - { - $normalizedBaseUrl = rtrim(trim($baseUrl), '/'); - $endpointUrl = $normalizedBaseUrl . '/api.php?endpoint=products&action=create'; - $response = $this->requestJson($endpointUrl, $apiKey, $timeoutSeconds, 'POST', $payload); - if (($response['ok'] ?? false) !== true) { - return [ - 'ok' => false, - 'http_code' => $response['http_code'] ?? null, - 'message' => (string) ($response['message'] ?? 'Nie mozna utworzyc produktu w shopPRO.'), - 'external_id' => 0, - ]; - } - - $data = is_array($response['data'] ?? null) ? $response['data'] : []; - $externalId = (int) ($data['id'] ?? 0); - if ($externalId <= 0) { - return [ - 'ok' => false, - 'http_code' => $response['http_code'] ?? null, - 'message' => 'shopPRO nie zwrocil ID nowo utworzonego produktu.', - 'external_id' => 0, - ]; - } - - return [ - 'ok' => true, - 'http_code' => $response['http_code'] ?? null, - 'message' => '', - 'external_id' => $externalId, - ]; - } - - /** - * @param array $payload - * @return array{ok:bool,http_code:int|null,message:string} - */ - public function updateProduct( - string $baseUrl, - string $apiKey, - int $timeoutSeconds, - int $externalProductId, - array $payload - ): array { - if ($externalProductId <= 0) { - return [ - 'ok' => false, - 'http_code' => null, - 'message' => 'Niepoprawne ID produktu do aktualizacji.', - ]; - } - - $normalizedBaseUrl = rtrim(trim($baseUrl), '/'); - $query = http_build_query([ - 'endpoint' => 'products', - 'action' => 'update', - 'id' => $externalProductId, - ]); - $endpointUrl = $normalizedBaseUrl . '/api.php?' . $query; - $response = $this->requestJson($endpointUrl, $apiKey, $timeoutSeconds, 'PUT', $payload); - - return [ - 'ok' => ($response['ok'] ?? false) === true, - 'http_code' => $response['http_code'] ?? null, - 'message' => (string) ($response['message'] ?? ''), - ]; - } - - /** - * @return array{ok:bool,http_code:int|null,message:string,src:string} - */ - public function uploadProductImage( - string $baseUrl, - string $apiKey, - int $timeoutSeconds, - int $externalProductId, - string $fileName, - string $contentBase64, - ?string $alt = null, - ?int $position = null - ): array { - if ($externalProductId <= 0) { - return [ - 'ok' => false, - 'http_code' => null, - 'message' => 'Niepoprawne ID produktu do uploadu zdjecia.', - 'src' => '', - ]; - } - - $safeFileName = trim($fileName); - if ($safeFileName === '' || trim($contentBase64) === '') { - return [ - 'ok' => false, - 'http_code' => null, - 'message' => 'Brak nazwy pliku lub zawartosci obrazu do uploadu.', - 'src' => '', - ]; - } - - $normalizedBaseUrl = rtrim(trim($baseUrl), '/'); - $endpointUrl = $normalizedBaseUrl . '/api.php?endpoint=products&action=upload_image'; - $payload = [ - 'id' => $externalProductId, - 'file_name' => $safeFileName, - 'content_base64' => $contentBase64, - ]; - if ($alt !== null) { - $payload['alt'] = $alt; - } - if ($position !== null && $position >= 0) { - $payload['o'] = $position; - } - - $response = $this->requestJson($endpointUrl, $apiKey, $timeoutSeconds, 'POST', $payload); - if (($response['ok'] ?? false) !== true) { - return [ - 'ok' => false, - 'http_code' => $response['http_code'] ?? null, - 'message' => (string) ($response['message'] ?? 'Nie mozna przeslac zdjecia produktu do shopPRO.'), - 'src' => '', - ]; - } - - $data = is_array($response['data'] ?? null) ? $response['data'] : []; - - return [ - 'ok' => true, - 'http_code' => $response['http_code'] ?? null, - 'message' => '', - 'src' => (string) ($data['src'] ?? ''), - ]; - } - - /** - * @return array{ok:bool,http_code:int|null,message:string,variants:array>} - */ - public function fetchProductVariants( - string $baseUrl, - string $apiKey, - int $timeoutSeconds, - int $externalProductId - ): array { - if ($externalProductId <= 0) { - return [ - 'ok' => false, - 'http_code' => null, - 'message' => 'Niepoprawne ID produktu do pobrania wariantow.', - 'variants' => [], - ]; - } - - $normalizedBaseUrl = rtrim(trim($baseUrl), '/'); - $query = http_build_query([ - 'endpoint' => 'products', - 'action' => 'variants', - 'id' => $externalProductId, - ]); - $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 wariantow produktu z shopPRO.'), - 'variants' => [], - ]; - } - - $data = is_array($response['data'] ?? null) ? $response['data'] : []; - $variants = isset($data['variants']) && is_array($data['variants']) ? $data['variants'] : []; - - return [ - 'ok' => true, - 'http_code' => $response['http_code'] ?? null, - 'message' => '', - 'variants' => $variants, - ]; - } - - /** - * @param array $payload - * @return array{ok:bool,http_code:int|null,message:string,external_variant_id:int} - */ - public function createProductVariant( - string $baseUrl, - string $apiKey, - int $timeoutSeconds, - int $externalProductId, - array $payload - ): array { - if ($externalProductId <= 0) { - return [ - 'ok' => false, - 'http_code' => null, - 'message' => 'Niepoprawne ID produktu do tworzenia wariantu.', - 'external_variant_id' => 0, - ]; - } - - $normalizedBaseUrl = rtrim(trim($baseUrl), '/'); - $query = http_build_query([ - 'endpoint' => 'products', - 'action' => 'create_variant', - 'id' => $externalProductId, - ]); - $endpointUrl = $normalizedBaseUrl . '/api.php?' . $query; - $response = $this->requestJson($endpointUrl, $apiKey, $timeoutSeconds, 'POST', $payload); - if (($response['ok'] ?? false) !== true) { - return [ - 'ok' => false, - 'http_code' => $response['http_code'] ?? null, - 'message' => (string) ($response['message'] ?? 'Nie mozna utworzyc wariantu w shopPRO.'), - 'external_variant_id' => 0, - ]; - } - - $data = is_array($response['data'] ?? null) ? $response['data'] : []; - $externalVariantId = (int) ($data['id'] ?? 0); - if ($externalVariantId <= 0) { - return [ - 'ok' => false, - 'http_code' => $response['http_code'] ?? null, - 'message' => 'shopPRO nie zwrocil ID nowo utworzonego wariantu.', - 'external_variant_id' => 0, - ]; - } - - return [ - 'ok' => true, - 'http_code' => $response['http_code'] ?? null, - 'message' => '', - 'external_variant_id' => $externalVariantId, - ]; - } - - /** - * @param array $payload - * @return array{ok:bool,http_code:int|null,message:string} - */ - public function updateProductVariant( - string $baseUrl, - string $apiKey, - int $timeoutSeconds, - int $externalVariantId, - array $payload - ): array { - if ($externalVariantId <= 0) { - return [ - 'ok' => false, - 'http_code' => null, - 'message' => 'Niepoprawne ID wariantu do aktualizacji.', - ]; - } - - $normalizedBaseUrl = rtrim(trim($baseUrl), '/'); - $query = http_build_query([ - 'endpoint' => 'products', - 'action' => 'update_variant', - 'id' => $externalVariantId, - ]); - $endpointUrl = $normalizedBaseUrl . '/api.php?' . $query; - $response = $this->requestJson($endpointUrl, $apiKey, $timeoutSeconds, 'PUT', $payload); - - return [ - 'ok' => ($response['ok'] ?? false) === true, - 'http_code' => $response['http_code'] ?? null, - 'message' => (string) ($response['message'] ?? ''), - ]; - } - - /** - * @return array{ok:bool,http_code:int|null,message:string,attribute_id:int,created:bool} - */ - public function ensureAttribute( - string $baseUrl, - string $apiKey, - int $timeoutSeconds, - string $name, - int $type = 0, - string $lang = 'pl' - ): array { - $normalizedBaseUrl = rtrim(trim($baseUrl), '/'); - $endpointUrl = $normalizedBaseUrl . '/api.php?endpoint=dictionaries&action=ensure_attribute'; - $payload = [ - 'name' => trim($name), - 'type' => $type, - 'lang' => trim($lang) !== '' ? trim($lang) : 'pl', - ]; - - $response = $this->requestJson($endpointUrl, $apiKey, $timeoutSeconds, 'POST', $payload); - if (($response['ok'] ?? false) !== true) { - return [ - 'ok' => false, - 'http_code' => $response['http_code'] ?? null, - 'message' => (string) ($response['message'] ?? 'Nie mozna dopasowac/utworzyc atrybutu w shopPRO.'), - 'attribute_id' => 0, - 'created' => false, - ]; - } - - $data = is_array($response['data'] ?? null) ? $response['data'] : []; - $attributeId = (int) ($data['id'] ?? 0); - if ($attributeId <= 0) { - return [ - 'ok' => false, - 'http_code' => $response['http_code'] ?? null, - 'message' => 'shopPRO nie zwrocil poprawnego ID atrybutu.', - 'attribute_id' => 0, - 'created' => false, - ]; - } - - return [ - 'ok' => true, - 'http_code' => $response['http_code'] ?? null, - 'message' => '', - 'attribute_id' => $attributeId, - 'created' => !empty($data['created']), - ]; - } - - /** - * @return array{ok:bool,http_code:int|null,message:string,value_id:int,created:bool} - */ - public function ensureAttributeValue( - string $baseUrl, - string $apiKey, - int $timeoutSeconds, - int $attributeId, - string $name, - string $lang = 'pl' - ): array { - $normalizedBaseUrl = rtrim(trim($baseUrl), '/'); - $endpointUrl = $normalizedBaseUrl . '/api.php?endpoint=dictionaries&action=ensure_attribute_value'; - $payload = [ - 'attribute_id' => max(0, $attributeId), - 'name' => trim($name), - 'lang' => trim($lang) !== '' ? trim($lang) : 'pl', - ]; - - $response = $this->requestJson($endpointUrl, $apiKey, $timeoutSeconds, 'POST', $payload); - if (($response['ok'] ?? false) !== true) { - return [ - 'ok' => false, - 'http_code' => $response['http_code'] ?? null, - 'message' => (string) ($response['message'] ?? 'Nie mozna dopasowac/utworzyc wartosci atrybutu w shopPRO.'), - 'value_id' => 0, - 'created' => false, - ]; - } - - $data = is_array($response['data'] ?? null) ? $response['data'] : []; - $valueId = (int) ($data['id'] ?? 0); - if ($valueId <= 0) { - return [ - 'ok' => false, - 'http_code' => $response['http_code'] ?? null, - 'message' => 'shopPRO nie zwrocil poprawnego ID wartosci atrybutu.', - 'value_id' => 0, - 'created' => false, - ]; - } - - return [ - 'ok' => true, - 'http_code' => $response['http_code'] ?? null, - 'message' => '', - 'value_id' => $valueId, - 'created' => !empty($data['created']), - ]; - } - - /** - * @return array{ok:bool,status:string,http_code:int|null,message:string,endpoint_url:string,tested_at:string} - */ - public function testConnection(string $baseUrl, string $apiKey, int $timeoutSeconds): array - { - $normalizedBaseUrl = rtrim(trim($baseUrl), '/'); - $endpointUrl = $normalizedBaseUrl . '/api.php?endpoint=dictionaries&action=statuses'; - $testedAt = date('Y-m-d H:i:s'); - $response = $this->requestJson($endpointUrl, $apiKey, $timeoutSeconds); - if (($response['ok'] ?? false) !== true) { - return [ - 'ok' => false, - 'status' => 'error', - 'http_code' => $response['http_code'] ?? null, - 'message' => (string) ($response['message'] ?? 'Nieznany blad polaczenia.'), - 'endpoint_url' => $endpointUrl, - 'tested_at' => $testedAt, - ]; - } - - $payload = is_array($response['payload'] ?? null) ? $response['payload'] : []; - $apiStatus = (string) ($payload['status'] ?? ''); - $httpCode = isset($response['http_code']) ? (int) $response['http_code'] : null; - if ($httpCode === 200 && $apiStatus === 'ok') { - return [ - 'ok' => true, - 'status' => 'ok', - 'http_code' => $httpCode, - 'message' => 'Polaczenie poprawne. Endpoint dictionaries/statuses zwrocil status=ok.', - 'endpoint_url' => $endpointUrl, - 'tested_at' => $testedAt, - ]; - } - - $errorCode = (string) ($payload['code'] ?? ''); - $errorMessage = (string) ($payload['message'] ?? 'Brak szczegolow bledu.'); - - return [ - 'ok' => false, - 'status' => 'error', - 'http_code' => $httpCode, - 'message' => trim('shopPRO zwrocil blad. ' . $errorCode . ' ' . $errorMessage), - 'endpoint_url' => $endpointUrl, - 'tested_at' => $testedAt, - ]; - } - - /** - * @return array{ok:bool,http_code:int|null,message:string,statuses:array} - */ - public function fetchOrderStatuses(string $baseUrl, string $apiKey, int $timeoutSeconds): array - { - $normalizedBaseUrl = rtrim(trim($baseUrl), '/'); - $endpointUrl = $normalizedBaseUrl . '/api.php?endpoint=dictionaries&action=statuses'; - $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 statusow zamowien z shopPRO.'), - 'statuses' => [], - ]; - } - - $data = is_array($response['data'] ?? null) ? $response['data'] : []; - $rawStatuses = []; - if (isset($data['statuses']) && is_array($data['statuses'])) { - $rawStatuses = $data['statuses']; - } elseif (isset($data['order_statuses']) && is_array($data['order_statuses'])) { - $rawStatuses = $data['order_statuses']; - } elseif ($data !== [] && array_keys($data) === range(0, count($data) - 1)) { - $rawStatuses = $data; - } - - $statuses = $this->normalizeStatusesPayload($rawStatuses); - - return [ - 'ok' => true, - 'http_code' => $response['http_code'] ?? null, - 'message' => '', - 'statuses' => $statuses, - ]; - } - - /** - * @return array{ok:bool,http_code:int|null,message:string,endpoint_url:string,method:string} - */ - public function updateOrderStatus( - string $baseUrl, - string $apiKey, - int $timeoutSeconds, - string $externalOrderId, - string $shopProStatusCode - ): array { - $orderId = trim($externalOrderId); - $statusCode = trim($shopProStatusCode); - if ($orderId === '' || $statusCode === '') { - return [ - 'ok' => false, - 'http_code' => null, - 'message' => 'Brak ID zamowienia lub kodu statusu do synchronizacji.', - 'endpoint_url' => '', - 'method' => '', - ]; - } - - $normalizedBaseUrl = rtrim(trim($baseUrl), '/'); - $attempts = [ - [ - 'url' => $normalizedBaseUrl . '/api.php?endpoint=orders&action=update_status', - 'method' => 'POST', - 'payload' => ['id' => $orderId, 'status' => $statusCode], - ], - [ - 'url' => $normalizedBaseUrl . '/api.php?endpoint=orders&action=update_status', - 'method' => 'PUT', - 'payload' => ['id' => $orderId, 'status' => $statusCode], - ], - [ - 'url' => $normalizedBaseUrl . '/api.php?endpoint=orders&action=set_status', - 'method' => 'POST', - 'payload' => ['id' => $orderId, 'status' => $statusCode], - ], - [ - 'url' => $normalizedBaseUrl . '/api.php?endpoint=orders&action=update', - 'method' => 'PUT', - 'payload' => ['id' => $orderId, 'status' => $statusCode], - ], - ]; - - $lastFailure = [ - 'ok' => false, - 'http_code' => null, - 'message' => 'Nie udalo sie zaktualizowac statusu zamowienia po stronie shopPRO.', - 'endpoint_url' => '', - 'method' => '', - ]; - - foreach ($attempts as $attempt) { - $response = $this->requestJson( - (string) ($attempt['url'] ?? ''), - $apiKey, - $timeoutSeconds, - (string) ($attempt['method'] ?? 'POST'), - is_array($attempt['payload'] ?? null) ? $attempt['payload'] : null - ); - - if (($response['ok'] ?? false) === true) { - return [ - 'ok' => true, - 'http_code' => $response['http_code'] ?? null, - 'message' => '', - 'endpoint_url' => (string) ($attempt['url'] ?? ''), - 'method' => (string) ($attempt['method'] ?? ''), - ]; - } - - $lastFailure = [ - 'ok' => false, - 'http_code' => $response['http_code'] ?? null, - 'message' => trim((string) ($response['message'] ?? 'Nieznany blad synchronizacji statusu.')), - 'endpoint_url' => (string) ($attempt['url'] ?? ''), - 'method' => (string) ($attempt['method'] ?? ''), - ]; - } - - return $lastFailure; - } - - /** - * @return array{ok:bool,http_code:int|null,message:string,producer_id:int,created:bool} - */ - public function ensureProducer( - string $baseUrl, - string $apiKey, - int $timeoutSeconds, - string $name - ): array { - $normalizedBaseUrl = rtrim(trim($baseUrl), '/'); - $endpointUrl = $normalizedBaseUrl . '/api.php?endpoint=dictionaries&action=ensure_producer'; - $payload = ['name' => trim($name)]; - - $response = $this->requestJson($endpointUrl, $apiKey, $timeoutSeconds, 'POST', $payload); - if (($response['ok'] ?? false) !== true) { - return [ - 'ok' => false, - 'http_code' => $response['http_code'] ?? null, - 'message' => (string) ($response['message'] ?? 'Nie mozna dopasowac/utworzyc producenta w shopPRO.'), - 'producer_id' => 0, - 'created' => false, - ]; - } - - $data = is_array($response['data'] ?? null) ? $response['data'] : []; - $producerId = (int) ($data['id'] ?? 0); - if ($producerId <= 0) { - return [ - 'ok' => false, - 'http_code' => $response['http_code'] ?? null, - 'message' => 'shopPRO nie zwrocil poprawnego ID producenta.', - 'producer_id' => 0, - 'created' => false, - ]; - } - - return [ - 'ok' => true, - 'http_code' => $response['http_code'] ?? null, - 'message' => '', - 'producer_id' => $producerId, - 'created' => !empty($data['created']), - ]; - } - - /** - * @return array{ok:bool,http_code:int|null,message:string,categories:array>} - */ - public function fetchCategories( - string $baseUrl, - string $apiKey, - int $timeoutSeconds - ): array { - $normalizedBaseUrl = rtrim(trim($baseUrl), '/'); - $endpointUrl = $normalizedBaseUrl . '/api.php?endpoint=categories&action=list'; - - $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 kategorii z shopPRO.'), - 'categories' => [], - ]; - } - - $data = is_array($response['data'] ?? null) ? $response['data'] : []; - $categories = isset($data['categories']) && is_array($data['categories']) - ? $data['categories'] - : []; - - return [ - 'ok' => true, - 'http_code' => $response['http_code'] ?? null, - 'message' => '', - 'categories' => $categories, - ]; - } - - /** - * @param array|null $jsonBody - * @return array{ok:bool,http_code:int|null,message:string,payload?:array,data?:mixed} - */ - private function requestJson( - string $endpointUrl, - string $apiKey, - int $timeoutSeconds, - string $method = 'GET', - ?array $jsonBody = null - ): array { - $timeout = max(3, min(60, $timeoutSeconds)); - $normalizedMethod = strtoupper(trim($method)); - if ($normalizedMethod === '') { - $normalizedMethod = 'GET'; - } - - if (function_exists('curl_init')) { - [$httpCode, $body, $transportError, $contentType] = $this->requestByCurl( - $endpointUrl, - $apiKey, - $timeout, - $normalizedMethod, - $jsonBody - ); - } else { - [$httpCode, $body, $transportError, $contentType] = $this->requestByStream( - $endpointUrl, - $apiKey, - $timeout, - $normalizedMethod, - $jsonBody - ); - } - - if ($transportError !== '') { - return [ - 'ok' => false, - 'http_code' => $httpCode, - 'message' => 'Blad transportu: ' . $transportError, - ]; - } - - $cleanBody = $this->stripUtf8Bom($body); - $bodyLength = strlen($cleanBody); - if ($bodyLength === 0) { - return [ - 'ok' => false, - 'http_code' => $httpCode, - 'message' => trim( - 'Pusta odpowiedz z shopPRO. HTTP=' . (string) ($httpCode ?? 0) - . ' content_type="' . $contentType . '" body_length=0' - ), - ]; - } - - $payload = json_decode($cleanBody, true); - if (!is_array($payload)) { - $prefix = trim(mb_substr($cleanBody, 0, 180)); - $looksLikeHtml = str_starts_with($prefix, '<'); - $details = $looksLikeHtml - ? 'Odpowiedz wyglada na HTML (np. redirect, blad serwera lub blokada).' - : 'Odpowiedz nie jest poprawnym JSON.'; - - return [ - 'ok' => false, - 'http_code' => $httpCode, - 'message' => trim( - $details - . ' HTTP=' . (string) ($httpCode ?? 0) - . ' content_type="' . $contentType . '"' - . ' body_length=' . (string) $bodyLength - . ' fragment="' . $prefix . '"' - ), - ]; - } - - $status = (string) ($payload['status'] ?? ''); - if ($status !== 'ok') { - $errorCode = (string) ($payload['code'] ?? ''); - $errorMessage = (string) ($payload['message'] ?? 'Brak szczegolow bledu.'); - - return [ - 'ok' => false, - 'http_code' => $httpCode, - 'message' => trim('shopPRO zwrocil blad. ' . $errorCode . ' ' . $errorMessage), - 'payload' => $payload, - ]; - } - - return [ - 'ok' => true, - 'http_code' => $httpCode, - 'message' => '', - 'payload' => $payload, - 'data' => $payload['data'] ?? null, - ]; - } - - /** - * @param array|null $jsonBody - * @return array{0:int|null,1:string,2:string,3:string} - */ - private function requestByCurl( - string $url, - string $apiKey, - int $timeoutSeconds, - string $method, - ?array $jsonBody - ): array { - $curl = curl_init($url); - if ($curl === false) { - return [null, '', 'Nie mozna zainicjalizowac cURL.', '']; - } - - $requestBody = null; - if ($jsonBody !== null && $method !== 'GET') { - $encoded = json_encode($jsonBody, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - if ($encoded === false) { - return [null, '', 'Nie mozna zakodowac payload JSON.', '']; - } - $requestBody = $encoded; - } - - $headers = [ - 'X-Api-Key: ' . $apiKey, - 'Accept: application/json', - ]; - if ($requestBody !== null) { - $headers[] = 'Content-Type: application/json'; - } - - curl_setopt_array($curl, [ - CURLOPT_RETURNTRANSFER => true, - CURLOPT_HTTPHEADER => $headers, - CURLOPT_TIMEOUT => $timeoutSeconds, - CURLOPT_CONNECTTIMEOUT => max(2, min(10, $timeoutSeconds)), - CURLOPT_SSL_VERIFYPEER => true, - CURLOPT_SSL_VERIFYHOST => 2, - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_MAXREDIRS => 5, - CURLOPT_CUSTOMREQUEST => $method, - ]); - if ($requestBody !== null) { - curl_setopt($curl, CURLOPT_POSTFIELDS, $requestBody); - } - - $body = curl_exec($curl); - $httpCode = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE); - $contentType = (string) curl_getinfo($curl, CURLINFO_CONTENT_TYPE); - $error = curl_error($curl); - - return [ - $httpCode > 0 ? $httpCode : null, - is_string($body) ? $body : '', - $error, - $contentType, - ]; - } - - /** - * @param array|null $jsonBody - * @return array{0:int|null,1:string,2:string,3:string} - */ - private function requestByStream( - string $url, - string $apiKey, - int $timeoutSeconds, - string $method, - ?array $jsonBody - ): array { - $requestBody = null; - $headers = "X-Api-Key: {$apiKey}\r\nAccept: application/json\r\n"; - if ($jsonBody !== null && $method !== 'GET') { - $encoded = json_encode($jsonBody, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - if ($encoded === false) { - return [null, '', 'Nie mozna zakodowac payload JSON.', '']; - } - $requestBody = $encoded; - $headers .= "Content-Type: application/json\r\n"; - } - - $context = stream_context_create([ - 'http' => [ - 'method' => $method, - 'header' => $headers, - 'content' => $requestBody ?? '', - 'timeout' => $timeoutSeconds, - 'ignore_errors' => true, - 'follow_location' => 1, - 'max_redirects' => 5, - ], - ]); - - $body = @file_get_contents($url, false, $context); - $headers = is_array($http_response_header ?? null) ? $http_response_header : []; - $httpCode = $this->extractHttpStatusCode($headers); - $contentType = $this->extractContentType($headers); - $error = $body === false ? 'Nie mozna pobrac odpowiedzi HTTP.' : ''; - - return [ - $httpCode, - $body === false ? '' : $body, - $error, - $contentType, - ]; - } - - /** - * @param array $headers - */ - private function extractHttpStatusCode(array $headers): ?int - { - foreach ($headers as $header) { - if (preg_match('/^HTTP\/\d\.\d\s+(\d{3})\b/i', $header, $matches) === 1) { - return (int) $matches[1]; - } - } - - return null; - } - - /** - * @param array $headers - */ - private function extractContentType(array $headers): string - { - foreach ($headers as $header) { - if (stripos($header, 'Content-Type:') === 0) { - return trim(substr($header, strlen('Content-Type:'))); - } - } - - return ''; - } - - private function stripUtf8Bom(string $body): string - { - if (str_starts_with($body, "\xEF\xBB\xBF")) { - return substr($body, 3); - } - - return $body; - } - - /** - * @param array $rawStatuses - * @return array - */ - private function normalizeStatusesPayload(array $rawStatuses): array - { - $normalized = []; - - foreach ($rawStatuses as $key => $item) { - if (is_array($item)) { - $code = trim((string) ($item['code'] ?? $item['id'] ?? $item['status'] ?? $key)); - $name = trim((string) ($item['name'] ?? $item['label'] ?? $item['title'] ?? $code)); - } else { - $code = trim((string) (is_string($key) ? $key : $item)); - $name = trim((string) $item); - if ($name === '') { - $name = $code; - } - } - - if ($code === '') { - continue; - } - - $normalized[$code] = [ - 'code' => $code, - 'name' => $name !== '' ? $name : $code, - ]; - } - - return array_values($normalized); - } -} diff --git a/archive/2026-03-02_users-only-reset/tests/Unit/Cron/CronJobTypeTest.php b/archive/2026-03-02_users-only-reset/tests/Unit/Cron/CronJobTypeTest.php deleted file mode 100644 index cdf6392..0000000 --- a/archive/2026-03-02_users-only-reset/tests/Unit/Cron/CronJobTypeTest.php +++ /dev/null @@ -1,22 +0,0 @@ -pdo = new PDO('sqlite::memory:'); - $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - - $this->pdo->exec( - 'CREATE TABLE order_status_mappings ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - integration_id INTEGER 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, - updated_at DATETIME NOT NULL - )' - ); - - $this->repository = new OrderStatusMappingRepository($this->pdo); - } - - public function testReplaceAndReadMappingsForIntegration(): void - { - $this->repository->replaceForIntegration(10, [ - [ - 'shoppro_status_code' => 'new', - 'shoppro_status_name' => 'Nowe', - 'orderpro_status_code' => 'new', - ], - [ - 'shoppro_status_code' => 'paid', - 'shoppro_status_name' => 'Oplacone', - 'orderpro_status_code' => 'completed', - ], - ]); - - $rows = $this->repository->listByIntegration(10); - - self::assertArrayHasKey('new', $rows); - self::assertSame('Nowe', $rows['new']['shoppro_status_name']); - self::assertSame('new', $rows['new']['orderpro_status_code']); - self::assertSame('completed', $rows['paid']['orderpro_status_code']); - } - - public function testListOrderProToShopProMapNormalizesCodes(): void - { - $this->repository->replaceForIntegration(11, [ - [ - 'shoppro_status_code' => 'Paid', - 'shoppro_status_name' => 'Oplacone', - 'orderpro_status_code' => 'Completed', - ], - ]); - - $map = $this->repository->listOrderProToShopProMap(11); - - self::assertSame('paid', $map['completed']); - } -} diff --git a/archive/2026-03-02_users-only-reset/tests/Unit/Settings/ShopProClientTest.php b/archive/2026-03-02_users-only-reset/tests/Unit/Settings/ShopProClientTest.php deleted file mode 100644 index f8e5f5e..0000000 --- a/archive/2026-03-02_users-only-reset/tests/Unit/Settings/ShopProClientTest.php +++ /dev/null @@ -1,32 +0,0 @@ -setAccessible(true); - - $normalized = $method->invoke($client, [ - ['id' => 8, 'name' => 'Wyslane'], - ['code' => 'completed', 'label' => 'Zakonczone'], - 'cancelled' => 'Anulowane', - ]); - - self::assertSame([ - ['code' => '8', 'name' => 'Wyslane'], - ['code' => 'completed', 'name' => 'Zakonczone'], - ['code' => 'cancelled', 'name' => 'Anulowane'], - ], $normalized); - } -}