feat: Add CronJob functionality and integrate with existing services
- Implemented CronJobProcessor for managing scheduled jobs and processing job queues. - Created CronJobRepository for database interactions related to cron jobs. - Defined CronJobType for job types, statuses, and backoff calculations. - Added ApiloLogger for logging actions related to API interactions. - Enhanced UpdateController to check for updates and display update logs. - Updated FormAction to include a preview action for forms. - Modified ApiRouter to handle new dependencies for OrderAdminService and ProductsApiController. - Extended DictionariesApiController to manage attributes and producers. - Enhanced ProductsApiController with variant management and image upload functionality. - Updated ShopBasketController and ShopProductController to sort attributes and handle custom fields. - Added configuration for cron jobs in config.php. - Initialized apilo-sync-queue.json for managing sync tasks.
This commit is contained in:
@@ -46,7 +46,7 @@ class ApiRouter
|
||||
}
|
||||
|
||||
$controller->$action();
|
||||
} catch (\Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
self::sendError('INTERNAL_ERROR', 'Internal server error', 500);
|
||||
}
|
||||
}
|
||||
@@ -87,18 +87,22 @@ class ApiRouter
|
||||
$settingsRepo = new \Domain\Settings\SettingsRepository($db);
|
||||
$productRepo = new \Domain\Product\ProductRepository($db);
|
||||
$transportRepo = new \Domain\Transport\TransportRepository($db);
|
||||
$service = new \Domain\Order\OrderAdminService($orderRepo, $productRepo, $settingsRepo, $transportRepo);
|
||||
$cronJobRepo = new \Domain\CronJob\CronJobRepository($db);
|
||||
$service = new \Domain\Order\OrderAdminService($orderRepo, $productRepo, $settingsRepo, $transportRepo, $cronJobRepo);
|
||||
return new Controllers\OrdersApiController($service, $orderRepo);
|
||||
},
|
||||
'products' => function () use ($db) {
|
||||
$productRepo = new \Domain\Product\ProductRepository($db);
|
||||
return new Controllers\ProductsApiController($productRepo);
|
||||
$attrRepo = new \Domain\Attribute\AttributeRepository($db);
|
||||
return new Controllers\ProductsApiController($productRepo, $attrRepo);
|
||||
},
|
||||
'dictionaries' => function () use ($db) {
|
||||
$statusRepo = new \Domain\ShopStatus\ShopStatusRepository($db);
|
||||
$transportRepo = new \Domain\Transport\TransportRepository($db);
|
||||
$paymentRepo = new \Domain\PaymentMethod\PaymentMethodRepository($db);
|
||||
return new Controllers\DictionariesApiController($statusRepo, $transportRepo, $paymentRepo);
|
||||
$attrRepo = new \Domain\Attribute\AttributeRepository($db);
|
||||
$producerRepo = new \Domain\Producer\ProducerRepository($db);
|
||||
return new Controllers\DictionariesApiController($statusRepo, $transportRepo, $paymentRepo, $attrRepo, $producerRepo);
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
namespace api\Controllers;
|
||||
|
||||
use api\ApiRouter;
|
||||
use Domain\Attribute\AttributeRepository;
|
||||
use Domain\Producer\ProducerRepository;
|
||||
use Domain\ShopStatus\ShopStatusRepository;
|
||||
use Domain\Transport\TransportRepository;
|
||||
use Domain\PaymentMethod\PaymentMethodRepository;
|
||||
@@ -11,15 +13,21 @@ class DictionariesApiController
|
||||
private $statusRepo;
|
||||
private $transportRepo;
|
||||
private $paymentRepo;
|
||||
private $attrRepo;
|
||||
private $producerRepo;
|
||||
|
||||
public function __construct(
|
||||
ShopStatusRepository $statusRepo,
|
||||
TransportRepository $transportRepo,
|
||||
PaymentMethodRepository $paymentRepo
|
||||
PaymentMethodRepository $paymentRepo,
|
||||
AttributeRepository $attrRepo,
|
||||
ProducerRepository $producerRepo
|
||||
) {
|
||||
$this->statusRepo = $statusRepo;
|
||||
$this->transportRepo = $transportRepo;
|
||||
$this->paymentRepo = $paymentRepo;
|
||||
$this->attrRepo = $attrRepo;
|
||||
$this->producerRepo = $producerRepo;
|
||||
}
|
||||
|
||||
public function statuses(): void
|
||||
@@ -79,4 +87,122 @@ class DictionariesApiController
|
||||
|
||||
ApiRouter::sendSuccess($result);
|
||||
}
|
||||
|
||||
public function attributes(): void
|
||||
{
|
||||
if (!ApiRouter::requireMethod('GET')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$attributes = $this->attrRepo->listForApi();
|
||||
|
||||
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']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function ensure_producer(): 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;
|
||||
}
|
||||
|
||||
$result = $this->producerRepo->ensureProducerForApi($name);
|
||||
if ((int) ($result['id'] ?? 0) <= 0) {
|
||||
ApiRouter::sendError('INTERNAL_ERROR', 'Failed to ensure producer', 500);
|
||||
return;
|
||||
}
|
||||
|
||||
ApiRouter::sendSuccess([
|
||||
'id' => (int) ($result['id'] ?? 0),
|
||||
'created' => !empty($result['created']),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,18 @@
|
||||
namespace api\Controllers;
|
||||
|
||||
use api\ApiRouter;
|
||||
use Domain\Attribute\AttributeRepository;
|
||||
use Domain\Product\ProductRepository;
|
||||
|
||||
class ProductsApiController
|
||||
{
|
||||
private $productRepo;
|
||||
private $attrRepo;
|
||||
|
||||
public function __construct(ProductRepository $productRepo)
|
||||
public function __construct(ProductRepository $productRepo, AttributeRepository $attrRepo)
|
||||
{
|
||||
$this->productRepo = $productRepo;
|
||||
$this->attrRepo = $attrRepo;
|
||||
}
|
||||
|
||||
public function list(): void
|
||||
@@ -25,6 +28,20 @@ class ProductsApiController
|
||||
'promoted' => isset($_GET['promoted']) ? $_GET['promoted'] : '',
|
||||
];
|
||||
|
||||
// Attribute filters: attribute_{id}={value_id}
|
||||
$attrFilters = [];
|
||||
foreach ($_GET as $key => $value) {
|
||||
if (strpos($key, 'attribute_') === 0) {
|
||||
$attrId = (int)substr($key, 10);
|
||||
if ($attrId > 0 && (int)$value > 0) {
|
||||
$attrFilters[$attrId] = (int)$value;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!empty($attrFilters)) {
|
||||
$filters['attributes'] = $attrFilters;
|
||||
}
|
||||
|
||||
$sort = isset($_GET['sort']) ? $_GET['sort'] : 'id';
|
||||
$sortDir = isset($_GET['sort_dir']) ? $_GET['sort_dir'] : 'DESC';
|
||||
$page = max(1, (int)(isset($_GET['page']) ? $_GET['page'] : 1));
|
||||
@@ -90,6 +107,11 @@ class ProductsApiController
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_numeric($body['price_brutto']) || (float)$body['price_brutto'] < 0) {
|
||||
ApiRouter::sendError('BAD_REQUEST', 'price_brutto must be a non-negative number', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$formData = $this->mapApiToFormData($body);
|
||||
|
||||
$productId = $this->productRepo->saveProduct($formData);
|
||||
@@ -139,6 +161,231 @@ class ProductsApiController
|
||||
ApiRouter::sendSuccess($updated);
|
||||
}
|
||||
|
||||
public function variants(): void
|
||||
{
|
||||
if (!ApiRouter::requireMethod('GET')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$id = (int)(isset($_GET['id']) ? $_GET['id'] : 0);
|
||||
if ($id <= 0) {
|
||||
ApiRouter::sendError('BAD_REQUEST', 'Missing or invalid id parameter', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$product = $this->productRepo->find($id);
|
||||
if ($product === null) {
|
||||
ApiRouter::sendError('NOT_FOUND', 'Product not found', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!empty($product['parent_id'])) {
|
||||
ApiRouter::sendError('BAD_REQUEST', 'Cannot get variants of a variant product', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$variants = $this->productRepo->findVariantsForApi($id);
|
||||
|
||||
// Available attributes for this product
|
||||
$allAttributes = $this->attrRepo->listForApi();
|
||||
$usedAttrIds = [];
|
||||
foreach ($variants as $variant) {
|
||||
foreach ($variant['attributes'] as $a) {
|
||||
$usedAttrIds[(int)$a['attribute_id']] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$availableAttributes = [];
|
||||
foreach ($allAttributes as $attr) {
|
||||
if (isset($usedAttrIds[$attr['id']])) {
|
||||
$availableAttributes[] = $attr;
|
||||
}
|
||||
}
|
||||
|
||||
ApiRouter::sendSuccess([
|
||||
'product_id' => $id,
|
||||
'available_attributes' => $availableAttributes,
|
||||
'variants' => $variants,
|
||||
]);
|
||||
}
|
||||
|
||||
public function create_variant(): void
|
||||
{
|
||||
if (!ApiRouter::requireMethod('POST')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$parentId = (int)(isset($_GET['id']) ? $_GET['id'] : 0);
|
||||
if ($parentId <= 0) {
|
||||
ApiRouter::sendError('BAD_REQUEST', 'Missing or invalid id parameter', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$body = ApiRouter::getJsonBody();
|
||||
if ($body === null) {
|
||||
ApiRouter::sendError('BAD_REQUEST', 'Missing or invalid JSON body', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
if (empty($body['attributes']) || !is_array($body['attributes'])) {
|
||||
ApiRouter::sendError('BAD_REQUEST', 'Missing or empty attributes', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $this->productRepo->createVariantForApi($parentId, $body);
|
||||
if ($result === null) {
|
||||
ApiRouter::sendError('BAD_REQUEST', 'Cannot create variant: parent not found, is archived, is itself a variant, or combination already exists', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$variant = $this->productRepo->findVariantForApi($result['id']);
|
||||
|
||||
http_response_code(201);
|
||||
echo json_encode([
|
||||
'status' => 'ok',
|
||||
'data' => $variant !== null ? $variant : $result,
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
public function update_variant(): void
|
||||
{
|
||||
if (!ApiRouter::requireMethod('PUT')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$variantId = (int)(isset($_GET['id']) ? $_GET['id'] : 0);
|
||||
if ($variantId <= 0) {
|
||||
ApiRouter::sendError('BAD_REQUEST', 'Missing or invalid id parameter', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$body = ApiRouter::getJsonBody();
|
||||
if ($body === null) {
|
||||
ApiRouter::sendError('BAD_REQUEST', 'Missing or invalid JSON body', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$success = $this->productRepo->updateVariantForApi($variantId, $body);
|
||||
if (!$success) {
|
||||
ApiRouter::sendError('NOT_FOUND', 'Variant not found', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
$variant = $this->productRepo->findVariantForApi($variantId);
|
||||
ApiRouter::sendSuccess($variant);
|
||||
}
|
||||
|
||||
public function delete_variant(): void
|
||||
{
|
||||
if (!ApiRouter::requireMethod('DELETE')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$variantId = (int)(isset($_GET['id']) ? $_GET['id'] : 0);
|
||||
if ($variantId <= 0) {
|
||||
ApiRouter::sendError('BAD_REQUEST', 'Missing or invalid id parameter', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$success = $this->productRepo->deleteVariantForApi($variantId);
|
||||
if (!$success) {
|
||||
ApiRouter::sendError('NOT_FOUND', 'Variant not found', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
// api.php działa z rootu projektu (nie z admin/), więc ścieżka bez ../
|
||||
$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().
|
||||
*
|
||||
@@ -182,6 +429,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',
|
||||
@@ -246,6 +498,21 @@ class ProductsApiController
|
||||
$d['products_related'] = $body['products_related'];
|
||||
}
|
||||
|
||||
// Custom fields (Dodatkowe pola)
|
||||
if (isset($body['custom_fields']) && is_array($body['custom_fields'])) {
|
||||
$d['custom_field_name'] = [];
|
||||
$d['custom_field_type'] = [];
|
||||
$d['custom_field_required'] = [];
|
||||
foreach ($body['custom_fields'] as $cf) {
|
||||
if (!is_array($cf) || empty($cf['name'])) {
|
||||
continue;
|
||||
}
|
||||
$d['custom_field_name'][] = (string)$cf['name'];
|
||||
$d['custom_field_type'][] = !empty($cf['type']) ? (string)$cf['type'] : 'text';
|
||||
$d['custom_field_required'][] = !empty($cf['is_required']) ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
return $d;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user