feat: Implement pagination and filtering for linked offers by integration

- Refactored `listLinkedOffersByIntegration` to `paginateLinkedOffersByIntegration` in `MarketplaceRepository`.
- Added pagination support with `page` and `per_page` filters.
- Introduced sorting options for offers.
- Created `listOfferChannelsByIntegration` method to retrieve distinct sales channels.
- Enhanced SQL queries to support dynamic filtering based on provided parameters.

feat: Add new fields for products and SKU generation

- Introduced new fields: `new_to_date`, `additional_message`, `additional_message_required`, and `additional_message_text` in the `products` table.
- Added `findAllSkus` method in `ProductRepository` to retrieve all SKUs.
- Created `ProductSkuGenerator` class to handle SKU generation based on a configurable format.
- Implemented `nextSku` method to generate the next available SKU.

feat: Enhance product settings management in the UI

- Added new settings page for product SKU format in `SettingsController`.
- Implemented form handling for saving SKU format settings.
- Updated the view to include SKU format configuration options.

feat: Implement cron job for refreshing ShopPro offer titles

- Created `ShopProOfferTitlesRefreshHandler` to handle the cron job for refreshing offer titles.
- Integrated with the `OfferImportService` to import offers from ShopPro.

docs: Update database schema documentation

- Added documentation for new fields in the `products` table and new cron job for offer title refresh.
- Documented the purpose and structure of the `app_settings` table.

migrations: Add necessary migrations for new features

- Created migration to add `products_sku_format` setting in `app_settings`.
- Added migration to introduce new fields in the `products` table.
- Created migration for the new cron job schedule for refreshing ShopPro offer titles.
This commit is contained in:
2026-03-01 22:05:21 +01:00
parent bcf078baac
commit d1576bc4ab
28 changed files with 1503 additions and 104 deletions

View File

@@ -6,6 +6,7 @@ namespace App\Modules\Cron;
final class CronJobType
{
public const PRODUCT_LINKS_HEALTH_CHECK = 'product_links_health_check';
public const SHOPPRO_OFFER_TITLES_REFRESH = 'shoppro_offer_titles_refresh';
public const PRIORITY_HIGH = 50;
public const PRIORITY_NORMAL = 100;
@@ -15,6 +16,7 @@ final class CronJobType
{
return match (trim($jobType)) {
self::PRODUCT_LINKS_HEALTH_CHECK => 110,
self::SHOPPRO_OFFER_TITLES_REFRESH => 170,
default => self::PRIORITY_NORMAL,
};
}
@@ -23,6 +25,7 @@ final class CronJobType
{
return match (trim($jobType)) {
self::PRODUCT_LINKS_HEALTH_CHECK => 3,
self::SHOPPRO_OFFER_TITLES_REFRESH => 3,
default => 3,
};
}

View File

@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace App\Modules\Cron;
use App\Modules\ProductLinks\OfferImportService;
use App\Modules\Settings\IntegrationRepository;
use Throwable;
final class ShopProOfferTitlesRefreshHandler
{
public function __construct(
private readonly IntegrationRepository $integrations,
private readonly OfferImportService $offerImportService
) {
}
/**
* @param array<string, mixed> $payload
* @param array<string, mixed> $job
* @return array<string, mixed>
*/
public function __invoke(array $payload = [], array $job = []): array
{
$forcedIntegrationId = max(0, (int) ($payload['integration_id'] ?? 0));
$activeIntegrations = array_values(array_filter(
$this->integrations->listByType('shoppro'),
static function (array $integration) use ($forcedIntegrationId): bool {
$id = (int) ($integration['id'] ?? 0);
if ($forcedIntegrationId > 0 && $id !== $forcedIntegrationId) {
return false;
}
return $id > 0
&& ($integration['is_active'] ?? false) === true
&& ($integration['has_api_key'] ?? false) === true;
}
));
if ($activeIntegrations === []) {
return [
'ok' => true,
'message' => 'Brak aktywnych integracji z kluczem API do odswiezenia tytulow ofert.',
'integrations' => 0,
'updated_offers' => 0,
'failed_offers' => 0,
'integration_failures' => 0,
'errors' => [],
];
}
$updatedOffers = 0;
$failedOffers = 0;
$integrationFailures = 0;
$errors = [];
foreach ($activeIntegrations as $integration) {
$integrationId = (int) ($integration['id'] ?? 0);
if ($integrationId <= 0) {
continue;
}
try {
$credentials = $this->integrations->findApiCredentials($integrationId);
} catch (Throwable $exception) {
$integrationFailures++;
if (count($errors) < 5) {
$errors[] = 'Integracja #' . $integrationId . ': ' . $exception->getMessage();
}
continue;
}
if ($credentials === null || trim((string) ($credentials['api_key'] ?? '')) === '') {
$integrationFailures++;
if (count($errors) < 5) {
$errors[] = 'Integracja #' . $integrationId . ': brak poprawnych danych API.';
}
continue;
}
$import = $this->offerImportService->importShopProOffers($credentials);
if (($import['ok'] ?? false) !== true) {
$integrationFailures++;
if (count($errors) < 5) {
$errors[] = 'Integracja #' . $integrationId . ': ' . trim((string) ($import['message'] ?? 'Blad importu ofert.'));
}
continue;
}
$updatedOffers += (int) ($import['imported'] ?? 0);
$failedOffers += (int) ($import['failed'] ?? 0);
}
return [
'ok' => $integrationFailures === 0,
'message' => $integrationFailures === 0
? 'Odswiezenie tytulow ofert zakonczone.'
: 'Odswiezenie tytulow zakonczone z bledami integracji.',
'integrations' => count($activeIntegrations),
'updated_offers' => $updatedOffers,
'failed_offers' => $failedOffers,
'integration_failures' => $integrationFailures,
'errors' => $errors,
];
}
}