feat(16-automated-tasks): moduł zadań automatycznych — CRUD + watcher/executor

Reguły automatyzacji oparte na zdarzeniach (receipt.created) z warunkami
(integracja/kanał sprzedaży, AND logic) i akcjami (wyślij e-mail z 3 trybami
odbiorcy: klient / firma / klient+firma). Trigger w ReceiptController po
utworzeniu paragonu — błąd automatyzacji nie blokuje sukcesu paragonu.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-18 00:39:47 +01:00
parent a6512cbfa4
commit b9f639e037
24 changed files with 4997 additions and 32 deletions

View File

@@ -0,0 +1,277 @@
<?php
declare(strict_types=1);
namespace App\Modules\Automation;
use PDO;
use Throwable;
final class AutomationRepository
{
public function __construct(
private readonly PDO $pdo
) {
}
/**
* @return list<array<string, mixed>>
*/
public function findAll(): array
{
$sql = '
SELECT r.id, r.name, r.event_type, r.is_active, r.created_at, r.updated_at,
(SELECT COUNT(*) FROM automation_conditions WHERE rule_id = r.id) AS conditions_count,
(SELECT COUNT(*) FROM automation_actions WHERE rule_id = r.id) AS actions_count
FROM automation_rules r
ORDER BY r.created_at DESC
';
$statement = $this->pdo->prepare($sql);
$statement->execute();
$rows = $statement->fetchAll(PDO::FETCH_ASSOC);
return is_array($rows) ? $rows : [];
}
/**
* @return array<string, mixed>|null
*/
public function findById(int $id): ?array
{
$statement = $this->pdo->prepare('SELECT * FROM automation_rules WHERE id = :id LIMIT 1');
$statement->execute(['id' => $id]);
$rule = $statement->fetch(PDO::FETCH_ASSOC);
if (!is_array($rule)) {
return null;
}
$rule['conditions'] = $this->loadConditions($id);
$rule['actions'] = $this->loadActions($id);
return $rule;
}
/**
* @param array<string, mixed> $data
* @param list<array<string, mixed>> $conditions
* @param list<array<string, mixed>> $actions
*/
public function create(array $data, array $conditions, array $actions): int
{
$this->pdo->beginTransaction();
try {
$statement = $this->pdo->prepare(
'INSERT INTO automation_rules (name, event_type, is_active)
VALUES (:name, :event_type, :is_active)'
);
$statement->execute([
'name' => $data['name'],
'event_type' => $data['event_type'],
'is_active' => (int) ($data['is_active'] ?? 1),
]);
$ruleId = (int) $this->pdo->lastInsertId();
$this->insertConditions($ruleId, $conditions);
$this->insertActions($ruleId, $actions);
$this->pdo->commit();
return $ruleId;
} catch (Throwable $e) {
$this->pdo->rollBack();
throw $e;
}
}
/**
* @param array<string, mixed> $data
* @param list<array<string, mixed>> $conditions
* @param list<array<string, mixed>> $actions
*/
public function update(int $id, array $data, array $conditions, array $actions): bool
{
$this->pdo->beginTransaction();
try {
$statement = $this->pdo->prepare(
'UPDATE automation_rules SET name = :name, event_type = :event_type, is_active = :is_active WHERE id = :id'
);
$statement->execute([
'id' => $id,
'name' => $data['name'],
'event_type' => $data['event_type'],
'is_active' => (int) ($data['is_active'] ?? 1),
]);
$this->pdo->prepare('DELETE FROM automation_conditions WHERE rule_id = :id')->execute(['id' => $id]);
$this->pdo->prepare('DELETE FROM automation_actions WHERE rule_id = :id')->execute(['id' => $id]);
$this->insertConditions($id, $conditions);
$this->insertActions($id, $actions);
$this->pdo->commit();
return true;
} catch (Throwable $e) {
$this->pdo->rollBack();
throw $e;
}
}
public function delete(int $id): void
{
$statement = $this->pdo->prepare('DELETE FROM automation_rules WHERE id = :id');
$statement->execute(['id' => $id]);
}
public function toggleActive(int $id): void
{
$statement = $this->pdo->prepare(
'UPDATE automation_rules SET is_active = NOT is_active WHERE id = :id'
);
$statement->execute(['id' => $id]);
}
/**
* @return list<array<string, mixed>>
*/
public function findActiveByEvent(string $eventType): array
{
$statement = $this->pdo->prepare(
'SELECT id, name, event_type FROM automation_rules WHERE event_type = :event_type AND is_active = 1'
);
$statement->execute(['event_type' => $eventType]);
$rules = $statement->fetchAll(PDO::FETCH_ASSOC);
if (!is_array($rules)) {
return [];
}
foreach ($rules as &$rule) {
$ruleId = (int) $rule['id'];
$rule['conditions'] = $this->loadConditions($ruleId);
$rule['actions'] = $this->loadActions($ruleId);
}
unset($rule);
return $rules;
}
/**
* @return list<array{id: int, type: string, name: string}>
*/
public function listOrderIntegrations(): array
{
$statement = $this->pdo->prepare(
"SELECT id, type, name FROM integrations WHERE type IN ('allegro', 'shoppro') AND is_active = 1 ORDER BY type, name"
);
$statement->execute();
$rows = $statement->fetchAll(PDO::FETCH_ASSOC);
return is_array($rows) ? $rows : [];
}
/**
* @return list<array{id: int, name: string}>
*/
public function listEmailTemplates(): array
{
$statement = $this->pdo->prepare(
'SELECT id, name FROM email_templates WHERE is_active = 1 ORDER BY name'
);
$statement->execute();
$rows = $statement->fetchAll(PDO::FETCH_ASSOC);
return is_array($rows) ? $rows : [];
}
/**
* @return list<array<string, mixed>>
*/
private function loadConditions(int $ruleId): array
{
$statement = $this->pdo->prepare(
'SELECT id, condition_type, condition_value, sort_order FROM automation_conditions WHERE rule_id = :rule_id ORDER BY sort_order'
);
$statement->execute(['rule_id' => $ruleId]);
$rows = $statement->fetchAll(PDO::FETCH_ASSOC);
if (!is_array($rows)) {
return [];
}
foreach ($rows as &$row) {
$decoded = json_decode((string) ($row['condition_value'] ?? '{}'), true);
$row['condition_value'] = is_array($decoded) ? $decoded : [];
}
unset($row);
return $rows;
}
/**
* @return list<array<string, mixed>>
*/
private function loadActions(int $ruleId): array
{
$statement = $this->pdo->prepare(
'SELECT id, action_type, action_config, sort_order FROM automation_actions WHERE rule_id = :rule_id ORDER BY sort_order'
);
$statement->execute(['rule_id' => $ruleId]);
$rows = $statement->fetchAll(PDO::FETCH_ASSOC);
if (!is_array($rows)) {
return [];
}
foreach ($rows as &$row) {
$decoded = json_decode((string) ($row['action_config'] ?? '{}'), true);
$row['action_config'] = is_array($decoded) ? $decoded : [];
}
unset($row);
return $rows;
}
/**
* @param list<array<string, mixed>> $conditions
*/
private function insertConditions(int $ruleId, array $conditions): void
{
$statement = $this->pdo->prepare(
'INSERT INTO automation_conditions (rule_id, condition_type, condition_value, sort_order)
VALUES (:rule_id, :condition_type, :condition_value, :sort_order)'
);
foreach ($conditions as $index => $condition) {
$statement->execute([
'rule_id' => $ruleId,
'condition_type' => $condition['type'],
'condition_value' => json_encode($condition['value'], JSON_UNESCAPED_UNICODE),
'sort_order' => $index,
]);
}
}
/**
* @param list<array<string, mixed>> $actions
*/
private function insertActions(int $ruleId, array $actions): void
{
$statement = $this->pdo->prepare(
'INSERT INTO automation_actions (rule_id, action_type, action_config, sort_order)
VALUES (:rule_id, :action_type, :action_config, :sort_order)'
);
foreach ($actions as $index => $action) {
$statement->execute([
'rule_id' => $ruleId,
'action_type' => $action['type'],
'action_config' => json_encode($action['config'], JSON_UNESCAPED_UNICODE),
'sort_order' => $index,
]);
}
}
}