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:
@@ -94,4 +94,81 @@ class DictionariesApiController
|
||||
|
||||
ApiRouter::sendSuccess($attributes);
|
||||
}
|
||||
|
||||
public function ensure_attribute(): void
|
||||
{
|
||||
if (!ApiRouter::requireMethod('POST')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$body = ApiRouter::getJsonBody();
|
||||
if (!is_array($body)) {
|
||||
ApiRouter::sendError('BAD_REQUEST', 'Missing or invalid JSON body', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$name = trim((string) ($body['name'] ?? ''));
|
||||
if ($name === '') {
|
||||
ApiRouter::sendError('BAD_REQUEST', 'Missing name', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$type = (int) ($body['type'] ?? 0);
|
||||
$lang = trim((string) ($body['lang'] ?? 'pl'));
|
||||
if ($lang === '') {
|
||||
$lang = 'pl';
|
||||
}
|
||||
|
||||
$result = $this->attrRepo->ensureAttributeForApi($name, $type, $lang);
|
||||
if (!is_array($result) || (int) ($result['id'] ?? 0) <= 0) {
|
||||
ApiRouter::sendError('INTERNAL_ERROR', 'Failed to ensure attribute', 500);
|
||||
return;
|
||||
}
|
||||
|
||||
ApiRouter::sendSuccess([
|
||||
'id' => (int) ($result['id'] ?? 0),
|
||||
'created' => !empty($result['created']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function ensure_attribute_value(): void
|
||||
{
|
||||
if (!ApiRouter::requireMethod('POST')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$body = ApiRouter::getJsonBody();
|
||||
if (!is_array($body)) {
|
||||
ApiRouter::sendError('BAD_REQUEST', 'Missing or invalid JSON body', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$attributeId = (int) ($body['attribute_id'] ?? 0);
|
||||
if ($attributeId <= 0) {
|
||||
ApiRouter::sendError('BAD_REQUEST', 'Missing or invalid attribute_id', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$name = trim((string) ($body['name'] ?? ''));
|
||||
if ($name === '') {
|
||||
ApiRouter::sendError('BAD_REQUEST', 'Missing name', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$lang = trim((string) ($body['lang'] ?? 'pl'));
|
||||
if ($lang === '') {
|
||||
$lang = 'pl';
|
||||
}
|
||||
|
||||
$result = $this->attrRepo->ensureAttributeValueForApi($attributeId, $name, $lang);
|
||||
if (!is_array($result) || (int) ($result['id'] ?? 0) <= 0) {
|
||||
ApiRouter::sendError('INTERNAL_ERROR', 'Failed to ensure attribute value', 500);
|
||||
return;
|
||||
}
|
||||
|
||||
ApiRouter::sendSuccess([
|
||||
'id' => (int) ($result['id'] ?? 0),
|
||||
'created' => !empty($result['created']),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,6 +296,95 @@ class ProductsApiController
|
||||
ApiRouter::sendSuccess(['id' => $variantId, 'deleted' => true]);
|
||||
}
|
||||
|
||||
public function upload_image(): void
|
||||
{
|
||||
if (!ApiRouter::requireMethod('POST')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$body = ApiRouter::getJsonBody();
|
||||
if ($body === null) {
|
||||
ApiRouter::sendError('BAD_REQUEST', 'Missing or invalid JSON body', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$productId = (int)($body['id'] ?? 0);
|
||||
if ($productId <= 0) {
|
||||
ApiRouter::sendError('BAD_REQUEST', 'Missing or invalid product id', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$product = $this->productRepo->find($productId);
|
||||
if ($product === null) {
|
||||
ApiRouter::sendError('NOT_FOUND', 'Product not found', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
$fileName = trim((string)($body['file_name'] ?? ''));
|
||||
$base64 = (string)($body['content_base64'] ?? '');
|
||||
if ($fileName === '' || $base64 === '') {
|
||||
ApiRouter::sendError('BAD_REQUEST', 'Missing file_name or content_base64', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$binary = base64_decode($base64, true);
|
||||
if ($binary === false) {
|
||||
ApiRouter::sendError('BAD_REQUEST', 'Invalid content_base64 payload', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$safeName = preg_replace('/[^a-zA-Z0-9._-]/', '_', basename($fileName));
|
||||
if ($safeName === '' || $safeName === null) {
|
||||
$safeName = 'image_' . md5((string)microtime(true)) . '.jpg';
|
||||
}
|
||||
|
||||
$baseDir = '../upload/product_images/product_' . $productId;
|
||||
if (!is_dir($baseDir) && !mkdir($baseDir, 0775, true) && !is_dir($baseDir)) {
|
||||
ApiRouter::sendError('INTERNAL_ERROR', 'Failed to create target directory', 500);
|
||||
return;
|
||||
}
|
||||
|
||||
$targetPath = $baseDir . '/' . $safeName;
|
||||
if (is_file($targetPath)) {
|
||||
$name = pathinfo($safeName, PATHINFO_FILENAME);
|
||||
$ext = pathinfo($safeName, PATHINFO_EXTENSION);
|
||||
$targetPath = $baseDir . '/' . $name . '_' . substr(md5($safeName . microtime(true)), 0, 8) . ($ext !== '' ? '.' . $ext : '');
|
||||
}
|
||||
|
||||
if (file_put_contents($targetPath, $binary) === false) {
|
||||
ApiRouter::sendError('INTERNAL_ERROR', 'Failed to save image file', 500);
|
||||
return;
|
||||
}
|
||||
|
||||
$src = '/upload/product_images/product_' . $productId . '/' . basename($targetPath);
|
||||
$alt = (string)($body['alt'] ?? '');
|
||||
$position = isset($body['o']) ? (int)$body['o'] : null;
|
||||
|
||||
$db = $GLOBALS['mdb'] ?? null;
|
||||
if (!$db) {
|
||||
ApiRouter::sendError('INTERNAL_ERROR', 'Database not available', 500);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($position === null) {
|
||||
$max = $db->max('pp_shop_products_images', 'o', ['product_id' => $productId]);
|
||||
$position = (int)$max + 1;
|
||||
}
|
||||
|
||||
$db->insert('pp_shop_products_images', [
|
||||
'product_id' => $productId,
|
||||
'src' => $src,
|
||||
'alt' => $alt,
|
||||
'o' => $position,
|
||||
]);
|
||||
|
||||
ApiRouter::sendSuccess([
|
||||
'src' => $src,
|
||||
'alt' => $alt,
|
||||
'o' => $position,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapuje dane z JSON API na format oczekiwany przez saveProduct().
|
||||
*
|
||||
@@ -339,6 +428,11 @@ class ProductsApiController
|
||||
}
|
||||
}
|
||||
|
||||
// saveProduct() traktuje float 0.00 jako "puste", ale cena 0 musi pozostać jawnie ustawiona.
|
||||
if (isset($d['price_brutto']) && is_numeric($d['price_brutto']) && (float)$d['price_brutto'] === 0.0) {
|
||||
$d['price_brutto'] = '0';
|
||||
}
|
||||
|
||||
// String fields — direct mapping
|
||||
$stringFields = [
|
||||
'sku', 'ean', 'custom_label_0', 'custom_label_1', 'custom_label_2',
|
||||
|
||||
Reference in New Issue
Block a user