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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -64,6 +64,7 @@ final class CronHandlerFactory
|
||||
$apiClient = new AllegroApiClient();
|
||||
$statusMappingRepository = new AllegroStatusMappingRepository($this->db);
|
||||
$ordersRepository = new OrdersRepository($this->db);
|
||||
$allegroSyncStateRepository = new AllegroOrderSyncStateRepository($this->db);
|
||||
|
||||
$orderImportService = new AllegroOrderImportService(
|
||||
$integrationRepository,
|
||||
@@ -76,7 +77,7 @@ final class CronHandlerFactory
|
||||
|
||||
$ordersSyncService = new AllegroOrdersSyncService(
|
||||
$integrationRepository,
|
||||
new AllegroOrderSyncStateRepository($this->db),
|
||||
$allegroSyncStateRepository,
|
||||
$tokenManager,
|
||||
$apiClient,
|
||||
$orderImportService
|
||||
@@ -128,6 +129,11 @@ final class CronHandlerFactory
|
||||
new AllegroStatusSyncService(
|
||||
$cronRepository,
|
||||
$orderImportService,
|
||||
$apiClient,
|
||||
$tokenManager,
|
||||
$statusMappingRepository,
|
||||
$allegroSyncStateRepository,
|
||||
$integrationRepository,
|
||||
$this->db
|
||||
)
|
||||
),
|
||||
@@ -194,7 +200,8 @@ final class CronHandlerFactory
|
||||
$ordersRepository,
|
||||
$companySettingsRepository,
|
||||
new ReceiptRepository($this->db),
|
||||
new ReceiptConfigRepository($this->db)
|
||||
new ReceiptConfigRepository($this->db),
|
||||
new ShipmentPackageRepository($this->db)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Shipments;
|
||||
|
||||
use App\Modules\Automation\AutomationService;
|
||||
use App\Core\Http\Request;
|
||||
use App\Core\Http\Response;
|
||||
use App\Core\I18n\Translator;
|
||||
@@ -26,6 +27,7 @@ final class ShipmentController
|
||||
private readonly CompanySettingsRepository $companySettings,
|
||||
private readonly ShipmentProviderRegistry $providerRegistry,
|
||||
private readonly ShipmentPackageRepository $packageRepository,
|
||||
private readonly AutomationService $automationService,
|
||||
private readonly string $storagePath,
|
||||
private readonly ?CarrierDeliveryMethodMappingRepository $deliveryMappings = null,
|
||||
private readonly ?\App\Modules\Printing\PrintJobRepository $printJobRepo = null
|
||||
@@ -213,6 +215,8 @@ final class ShipmentController
|
||||
'user',
|
||||
$actorName
|
||||
);
|
||||
|
||||
$this->triggerShipmentCreatedAutomation($orderId, $packageId, $providerCode);
|
||||
Flash::set('order.success', 'Przesylka utworzona. Sprawdz status w zakladce Przesylki.');
|
||||
return Response::redirect('/orders/' . $orderId);
|
||||
} catch (Throwable $exception) {
|
||||
@@ -382,10 +386,35 @@ final class ShipmentController
|
||||
$actorName
|
||||
);
|
||||
|
||||
$this->triggerShipmentCreatedAutomation($orderId, $packageId, 'manual');
|
||||
Flash::set('order.success', 'Numer przesylki dodany.');
|
||||
return Response::redirect('/orders/' . $orderId);
|
||||
}
|
||||
|
||||
private function triggerShipmentCreatedAutomation(int $orderId, int $packageId, string $providerCode): void
|
||||
{
|
||||
if ($orderId <= 0 || $packageId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$package = $this->packageRepository->findById($packageId);
|
||||
$context = [
|
||||
'package_id' => $packageId,
|
||||
'provider' => $providerCode,
|
||||
'package_status' => is_array($package) ? (string) ($package['status'] ?? '') : '',
|
||||
'tracking_number' => is_array($package) ? (string) ($package['tracking_number'] ?? '') : '',
|
||||
'delivery_status' => is_array($package)
|
||||
? (string) ($package['delivery_status'] ?? DeliveryStatus::UNKNOWN)
|
||||
: DeliveryStatus::UNKNOWN,
|
||||
];
|
||||
|
||||
try {
|
||||
$this->automationService->trigger('shipment.created', $orderId, $context);
|
||||
} catch (Throwable) {
|
||||
// Blad automatyzacji nie powinien blokowac tworzenia przesylki
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $deliveryAddr
|
||||
* @param array<string, mixed>|null $customerAddr
|
||||
|
||||
@@ -118,6 +118,20 @@ final class ShipmentPackageRepository
|
||||
return $statement->fetchAll(PDO::FETCH_ASSOC) ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function findLatestByOrderId(int $orderId): ?array
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'SELECT * FROM shipment_packages WHERE order_id = :order_id ORDER BY created_at DESC, id DESC LIMIT 1'
|
||||
);
|
||||
$statement->execute(['order_id' => $orderId]);
|
||||
$row = $statement->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
return is_array($row) ? $row : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user