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>
232 lines
9.7 KiB
PHP
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]);
|
|
}
|
|
}
|
|
|