ver. 0.318: shopPRO export produktów + nowe API endpoints
- NEW: IntegrationsRepository::shopproExportProduct() — eksport produktu do zdalnej instancji shopPRO (pola główne, tłumaczenia, custom fields, zdjęcia) - NEW: sendImageToShopproApi() — wysyłka zdjęć przez API shopPRO (base64 POST) - REFACTOR: shopproImportProduct() — wydzielono shopproDb() i missingShopproSetting(); dodano security_information, producer_id, custom fields, alt zdjęcia - NEW: AttributeRepository::ensureAttributeForApi() i ensureAttributeValueForApi() — idempotent find-or-create dla słowników - NEW: API POST dictionaries/ensure_attribute — utwórz lub znajdź atrybut - NEW: API POST dictionaries/ensure_attribute_value — utwórz lub znajdź wartość - NEW: API POST products/upload_image — przyjmuje base64, zapisuje plik i DB - NEW: IntegrationsController::shoppro_product_export() — akcja admina - NEW: przycisk "Eksportuj do shopPRO" w liście produktów - NEW: pole API key w ustawieniach integracji shopPRO Tests: 765 tests, 2153 assertions — all green Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -655,6 +655,95 @@ class AttributeRepository
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find existing attribute by name/type or create a new one for API integration.
|
||||
*
|
||||
* @return array{id:int,created:bool}|null
|
||||
*/
|
||||
public function ensureAttributeForApi(string $name, int $type = 0, string $langId = 'pl'): ?array
|
||||
{
|
||||
$normalizedName = trim($name);
|
||||
$normalizedLangId = trim($langId) !== '' ? trim($langId) : 'pl';
|
||||
$normalizedType = $this->toTypeValue($type);
|
||||
if ($normalizedName === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$existingId = $this->findAttributeIdByNameAndType($normalizedName, $normalizedType);
|
||||
if ($existingId > 0) {
|
||||
return ['id' => $existingId, 'created' => false];
|
||||
}
|
||||
|
||||
$this->db->insert('pp_shop_attributes', [
|
||||
'status' => 1,
|
||||
'type' => $normalizedType,
|
||||
'o' => $this->nextOrder(),
|
||||
]);
|
||||
$attributeId = (int) $this->db->id();
|
||||
if ($attributeId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->db->insert('pp_shop_attributes_langs', [
|
||||
'attribute_id' => $attributeId,
|
||||
'lang_id' => $normalizedLangId,
|
||||
'name' => $normalizedName,
|
||||
]);
|
||||
|
||||
$this->clearTempAndCache();
|
||||
$this->clearFrontCache($attributeId, 'frontAttributeDetails');
|
||||
|
||||
return ['id' => $attributeId, 'created' => true];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find existing value by name within attribute or create a new one for API integration.
|
||||
*
|
||||
* @return array{id:int,created:bool}|null
|
||||
*/
|
||||
public function ensureAttributeValueForApi(int $attributeId, string $name, string $langId = 'pl'): ?array
|
||||
{
|
||||
$normalizedName = trim($name);
|
||||
$normalizedLangId = trim($langId) !== '' ? trim($langId) : 'pl';
|
||||
$attributeId = max(0, $attributeId);
|
||||
|
||||
if ($attributeId <= 0 || $normalizedName === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$attributeExists = (int) $this->db->count('pp_shop_attributes', ['id' => $attributeId]) > 0;
|
||||
if (!$attributeExists) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$existingId = $this->findAttributeValueIdByName($attributeId, $normalizedName);
|
||||
if ($existingId > 0) {
|
||||
return ['id' => $existingId, 'created' => false];
|
||||
}
|
||||
|
||||
$this->db->insert('pp_shop_attributes_values', [
|
||||
'attribute_id' => $attributeId,
|
||||
'impact_on_the_price' => null,
|
||||
'is_default' => 0,
|
||||
]);
|
||||
$valueId = (int) $this->db->id();
|
||||
if ($valueId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->db->insert('pp_shop_attributes_values_langs', [
|
||||
'value_id' => $valueId,
|
||||
'lang_id' => $normalizedLangId,
|
||||
'name' => $normalizedName,
|
||||
'value' => null,
|
||||
]);
|
||||
|
||||
$this->clearTempAndCache();
|
||||
$this->clearFrontCache($valueId, 'frontValueDetails');
|
||||
|
||||
return ['id' => $valueId, 'created' => true];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{sql: string, params: array<string, mixed>}
|
||||
*/
|
||||
@@ -972,6 +1061,52 @@ class AttributeRepository
|
||||
return $this->defaultLangId;
|
||||
}
|
||||
|
||||
private function findAttributeIdByNameAndType(string $name, int $type): int
|
||||
{
|
||||
$statement = $this->db->query(
|
||||
'SELECT sa.id
|
||||
FROM pp_shop_attributes sa
|
||||
INNER JOIN pp_shop_attributes_langs sal ON sal.attribute_id = sa.id
|
||||
WHERE sa.type = :type
|
||||
AND LOWER(TRIM(sal.name)) = LOWER(TRIM(:name))
|
||||
ORDER BY sa.id ASC
|
||||
LIMIT 1',
|
||||
[
|
||||
':type' => $type,
|
||||
':name' => $name,
|
||||
]
|
||||
);
|
||||
if (!$statement) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$id = $statement->fetchColumn();
|
||||
return $id === false ? 0 : (int) $id;
|
||||
}
|
||||
|
||||
private function findAttributeValueIdByName(int $attributeId, string $name): int
|
||||
{
|
||||
$statement = $this->db->query(
|
||||
'SELECT sav.id
|
||||
FROM pp_shop_attributes_values sav
|
||||
INNER JOIN pp_shop_attributes_values_langs savl ON savl.value_id = sav.id
|
||||
WHERE sav.attribute_id = :attribute_id
|
||||
AND LOWER(TRIM(savl.name)) = LOWER(TRIM(:name))
|
||||
ORDER BY sav.id ASC
|
||||
LIMIT 1',
|
||||
[
|
||||
':attribute_id' => $attributeId,
|
||||
':name' => $name,
|
||||
]
|
||||
);
|
||||
if (!$statement) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$id = $statement->fetchColumn();
|
||||
return $id === false ? 0 : (int) $id;
|
||||
}
|
||||
|
||||
// ── Frontend methods ──────────────────────────────────────────
|
||||
|
||||
public function frontAttributeDetails(int $attributeId, string $langId): array
|
||||
|
||||
Reference in New Issue
Block a user