Files
orderPRO/src/Modules/Marketplace/MarketplaceController.php
Jacek Pyziak bfff05a61b fix: include languages.pl content when saving categories to shopPRO
shopPRO's products/update clears ALL language fields (name, description,
short_description, etc.) for any language not included in the payload.
Sending only {"categories":[...]} wiped them out.

saveProductCategoriesJson() now:
1. Looks up the local orderPRO product via findMappedProductId()
2. Loads per-integration translation (with global fallback)
3. Includes languages.pl in the payload alongside categories

Also adds ProductRepository to MarketplaceController constructor.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 19:27:39 +01:00

232 lines
9.7 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Modules\Marketplace;
use App\Core\Http\Request;
use App\Core\Http\Response;
use App\Core\I18n\Translator;
use App\Core\Security\Csrf;
use App\Core\Support\Flash;
use App\Core\View\Template;
use App\Modules\Auth\AuthService;
use App\Modules\Products\ProductRepository;
use App\Modules\Settings\IntegrationRepository;
use App\Modules\Settings\ShopProClient;
final class MarketplaceController
{
public function __construct(
private readonly Template $template,
private readonly Translator $translator,
private readonly AuthService $auth,
private readonly MarketplaceRepository $marketplace,
private readonly IntegrationRepository $integrationRepository,
private readonly ShopProClient $shopProClient,
private readonly ProductRepository $productRepository
) {
}
public function index(Request $request): Response
{
$integrations = $this->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();
$offers = $this->marketplace->listLinkedOffersByIntegration($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,
'errorMessage' => (string) Flash::get('marketplace_error', ''),
], 'layouts/app');
return Response::html($html);
}
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]);
}
}