feat(47-shipment-created-automation): immediate shipment automation trigger

Phase 47 complete:

- add event shipment.created triggered immediately after shipment creation

- add action update_shipment_status with real-change guard and chain-safe emit

- update automation UI/options, docs, and PAUL state artifacts
This commit is contained in:
2026-03-28 13:24:20 +01:00
parent d3f4bdaecd
commit ad9087d5e4
17 changed files with 784 additions and 310 deletions

View File

@@ -15,9 +15,9 @@ use Throwable;
final class AutomationController
{
private const ALLOWED_EVENTS = ['receipt.created', 'shipment.status_changed'];
private const ALLOWED_EVENTS = ['receipt.created', 'shipment.created', 'shipment.status_changed'];
private const ALLOWED_CONDITION_TYPES = ['integration', 'shipment_status'];
private const ALLOWED_ACTION_TYPES = ['send_email', 'issue_receipt'];
private const ALLOWED_ACTION_TYPES = ['send_email', 'issue_receipt', 'update_shipment_status'];
private const ALLOWED_RECIPIENTS = ['client', 'client_and_company', 'company'];
private const ALLOWED_RECEIPT_ISSUE_DATE_MODES = ['today', 'order_date', 'payment_date'];
private const ALLOWED_RECEIPT_DUPLICATE_POLICIES = ['skip_if_exists', 'allow_duplicates'];
@@ -416,6 +416,15 @@ final class AutomationController
];
}
if ($type === 'update_shipment_status') {
$statusKey = trim((string) ($action['shipment_status_key'] ?? ''));
if (!array_key_exists($statusKey, self::SHIPMENT_STATUS_OPTIONS)) {
return null;
}
return ['status_key' => $statusKey];
}
return null;
}

View File

@@ -8,6 +8,8 @@ use App\Modules\Email\EmailSendingService;
use App\Modules\Orders\OrdersRepository;
use App\Modules\Settings\CompanySettingsRepository;
use App\Modules\Settings\ReceiptConfigRepository;
use App\Modules\Shipments\DeliveryStatus;
use App\Modules\Shipments\ShipmentPackageRepository;
use Throwable;
final class AutomationService
@@ -32,7 +34,8 @@ final class AutomationService
private readonly OrdersRepository $orders,
private readonly CompanySettingsRepository $companySettings,
private readonly ReceiptRepository $receipts,
private readonly ReceiptConfigRepository $receiptConfigs
private readonly ReceiptConfigRepository $receiptConfigs,
private readonly ShipmentPackageRepository $shipmentPackages
) {
}
@@ -187,6 +190,11 @@ final class AutomationService
if ($type === 'issue_receipt') {
$this->handleIssueReceipt($config, $orderId, $ruleName, $context);
continue;
}
if ($type === 'update_shipment_status') {
$this->handleUpdateShipmentStatus($config, $orderId, $ruleName, $context);
}
}
}
@@ -355,6 +363,106 @@ final class AutomationService
}
}
/**
* @param array<string, mixed> $config
* @param array<string, mixed> $context
*/
private function handleUpdateShipmentStatus(array $config, int $orderId, string $ruleName, array $context): void
{
$statusKey = trim((string) ($config['status_key'] ?? ''));
$targetStatus = $this->resolveStatusFromActionKey($statusKey);
if ($targetStatus === null) {
return;
}
$targetPackage = $this->resolveTargetPackage($orderId, $context);
if ($targetPackage === null) {
return;
}
$packageId = (int) ($targetPackage['id'] ?? 0);
if ($packageId <= 0) {
return;
}
$previousStatus = trim((string) ($targetPackage['delivery_status'] ?? DeliveryStatus::UNKNOWN));
if ($previousStatus === '') {
$previousStatus = DeliveryStatus::UNKNOWN;
}
if ($previousStatus === $targetStatus) {
return;
}
$this->shipmentPackages->updateDeliveryStatus($packageId, $targetStatus, null);
$actorName = 'Automatyzacja: ' . $ruleName;
$this->orders->recordActivity(
$orderId,
'automation_shipment_status_updated',
$actorName . ' - zaktualizowano status przesylki',
[
'package_id' => $packageId,
'previous_status' => $previousStatus,
'delivery_status' => $targetStatus,
'status_key' => $statusKey,
],
'system',
$actorName
);
$this->emitEvent(
'shipment.status_changed',
$orderId,
$context,
[
'package_id' => $packageId,
'provider' => (string) ($targetPackage['provider'] ?? ''),
'delivery_status' => $targetStatus,
'delivery_status_raw' => '',
'previous_status' => $previousStatus,
'previous_status_raw' => '',
'automation_source' => 'update_shipment_status',
'automation_rule' => $ruleName,
]
);
}
private function resolveStatusFromActionKey(string $statusKey): ?string
{
if ($statusKey === '' || !isset(self::SHIPMENT_STATUS_OPTION_MAP[$statusKey])) {
return null;
}
$mappedStatuses = self::SHIPMENT_STATUS_OPTION_MAP[$statusKey];
if ($mappedStatuses === []) {
return null;
}
$candidate = trim((string) $mappedStatuses[0]);
if ($candidate === '' || !in_array($candidate, DeliveryStatus::ALL_STATUSES, true)) {
return null;
}
return $candidate;
}
/**
* @param array<string, mixed> $context
* @return array<string, mixed>|null
*/
private function resolveTargetPackage(int $orderId, array $context): ?array
{
$packageId = (int) ($context['package_id'] ?? 0);
if ($packageId > 0) {
$package = $this->shipmentPackages->findById($packageId);
if (is_array($package) && (int) ($package['order_id'] ?? 0) === $orderId) {
return $package;
}
}
return $this->shipmentPackages->findLatestByOrderId($orderId);
}
/**
* @param array<string, mixed> $order
* @param list<array<string, mixed>> $payments