feat(128): erli orders import
Phase 128 complete: - add Erli /inbox order import with safe mark-read ACK - add cron/manual import controls and sync state tracking - map Erli orders into orderPRO aggregates with mapper tests and docs
This commit is contained in:
@@ -12,17 +12,25 @@ use App\Core\Security\Csrf;
|
||||
use App\Core\Support\Flash;
|
||||
use App\Core\View\Template;
|
||||
use App\Modules\Auth\AuthService;
|
||||
use App\Modules\Cron\CronRepository;
|
||||
use Throwable;
|
||||
|
||||
final class ErliIntegrationController
|
||||
{
|
||||
private const ORDERS_IMPORT_JOB_TYPE = 'erli_orders_import';
|
||||
private const ORDERS_IMPORT_DEFAULT_INTERVAL_SECONDS = 300;
|
||||
private const ORDERS_IMPORT_DEFAULT_PRIORITY = 40;
|
||||
private const ORDERS_IMPORT_DEFAULT_MAX_ATTEMPTS = 3;
|
||||
|
||||
public function __construct(
|
||||
private readonly Template $template,
|
||||
private readonly Translator $translator,
|
||||
private readonly AuthService $auth,
|
||||
private readonly ErliIntegrationRepository $repository,
|
||||
private readonly ErliApiClient $apiClient,
|
||||
private readonly IntegrationsRepository $integrations
|
||||
private readonly IntegrationsRepository $integrations,
|
||||
private readonly CronRepository $cronRepository,
|
||||
private readonly ErliOrdersSyncService $ordersSyncService
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -35,9 +43,11 @@ final class ErliIntegrationController
|
||||
'user' => $this->auth->user(),
|
||||
'csrfToken' => Csrf::token(),
|
||||
'settings' => $this->repository->getSettings(),
|
||||
'ordersImportIntervalMinutes' => $this->currentImportIntervalMinutes(),
|
||||
'errorMessage' => (string) Flash::get('settings_error', ''),
|
||||
'successMessage' => (string) Flash::get('settings_success', ''),
|
||||
'testMessage' => (string) Flash::get('erli_test', ''),
|
||||
'importMessage' => (string) Flash::get('erli_import', ''),
|
||||
], 'layouts/app');
|
||||
|
||||
return Response::html($html);
|
||||
@@ -56,7 +66,10 @@ final class ErliIntegrationController
|
||||
'account_label' => (string) $request->input('account_label', ''),
|
||||
'api_key' => (string) $request->input('api_key', ''),
|
||||
'is_active' => $request->input('is_active', ''),
|
||||
'orders_fetch_enabled' => $request->input('orders_fetch_enabled', ''),
|
||||
'orders_fetch_start_date' => $this->validateStartDate((string) $request->input('orders_fetch_start_date', '')),
|
||||
]);
|
||||
$this->upsertImportSchedule($request);
|
||||
Flash::set('settings_success', $this->translator->get('settings.erli.flash.saved'));
|
||||
} catch (Throwable $exception) {
|
||||
Flash::set(
|
||||
@@ -68,6 +81,27 @@ final class ErliIntegrationController
|
||||
return Response::redirect($redirectTo);
|
||||
}
|
||||
|
||||
public function importNow(Request $request): Response
|
||||
{
|
||||
$redirectTo = $this->resolveRedirect($request);
|
||||
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
|
||||
return Response::redirect($redirectTo);
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $this->ordersSyncService->sync([
|
||||
'ignore_orders_fetch_enabled' => true,
|
||||
'max_messages' => 100,
|
||||
]);
|
||||
Flash::set('erli_import', $this->formatImportResult($result));
|
||||
} catch (Throwable $exception) {
|
||||
Flash::set('settings_error', $this->translator->get('settings.erli.flash.import_failed') . ' ' . $exception->getMessage());
|
||||
}
|
||||
|
||||
return Response::redirect($redirectTo);
|
||||
}
|
||||
|
||||
public function test(Request $request): Response
|
||||
{
|
||||
$redirectTo = $this->resolveRedirect($request);
|
||||
@@ -110,4 +144,60 @@ final class ErliIntegrationController
|
||||
'/settings/integrations/erli'
|
||||
);
|
||||
}
|
||||
|
||||
private function validateStartDate(string $value): string
|
||||
{
|
||||
$trimmed = trim($value);
|
||||
if ($trimmed === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $trimmed) !== 1) {
|
||||
throw new IntegrationConfigException($this->translator->get('settings.erli.validation.orders_fetch_start_date_invalid'));
|
||||
}
|
||||
|
||||
return $trimmed;
|
||||
}
|
||||
|
||||
private function upsertImportSchedule(Request $request): void
|
||||
{
|
||||
$minutes = max(1, min(1440, (int) $request->input('orders_import_interval_minutes', 5)));
|
||||
$enabled = (string) $request->input('orders_fetch_enabled', '') === '1';
|
||||
$this->cronRepository->upsertSchedule(
|
||||
self::ORDERS_IMPORT_JOB_TYPE,
|
||||
$minutes * 60,
|
||||
self::ORDERS_IMPORT_DEFAULT_PRIORITY,
|
||||
self::ORDERS_IMPORT_DEFAULT_MAX_ATTEMPTS,
|
||||
null,
|
||||
$enabled
|
||||
);
|
||||
}
|
||||
|
||||
private function currentImportIntervalMinutes(): int
|
||||
{
|
||||
foreach ($this->cronRepository->listSchedules() as $schedule) {
|
||||
if ((string) ($schedule['job_type'] ?? '') !== self::ORDERS_IMPORT_JOB_TYPE) {
|
||||
continue;
|
||||
}
|
||||
$seconds = (int) ($schedule['interval_seconds'] ?? self::ORDERS_IMPORT_DEFAULT_INTERVAL_SECONDS);
|
||||
return max(1, min(1440, (int) floor(max(60, $seconds) / 60)));
|
||||
}
|
||||
|
||||
return 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $result
|
||||
*/
|
||||
private function formatImportResult(array $result): string
|
||||
{
|
||||
return strtr($this->translator->get('settings.erli.flash.import_success'), [
|
||||
':processed' => (string) (int) ($result['processed'] ?? 0),
|
||||
':created' => (string) (int) ($result['imported_created'] ?? 0),
|
||||
':updated' => (string) (int) ($result['imported_updated'] ?? 0),
|
||||
':failed' => (string) (int) ($result['failed'] ?? 0),
|
||||
':skipped' => (string) (int) ($result['skipped'] ?? 0),
|
||||
':ack' => !empty($result['acknowledged']) ? 'tak' : 'nie',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user