feat: Implement Slack, Trello, and Courier (ShipLogic) integration

- Add 14 domain events for order lifecycle (OrderCreated, OrderPacked, ShipmentCreated, ParcelDelivered, etc.)
- Create SlackNotifierService with channels for orders, design, production, shipping, ops-alerts
- Create TrelloService to create cards, move cards between lists, attach files, check items
- Create CourierService to integrate with ShipLogic API for shipment creation and document retrieval
- Create PackingController to explicitly capture packing dimensions and weight
- Create ShippingController with multi-layer guards: packing validation, payment/approval verification, idempotency
- Create TrelloWebhookController to handle incoming Trello webhooks as intent signals
- Create CourierWebhookController to handle Shiplogic status updates
- Create event listeners for Slack notifications and Trello card updates
- Create EventServiceProvider to register all events and listeners
- Add database migration for packing, courier, and Trello data columns
- Create config files for slack, trello, and courier integration
- Update .env with integration secrets placeholders
- Add routes for /orders/{id}/pack, /orders/{id}/ship, /api/webhooks/trello, /api/webhooks/courier

Key architectural decisions:
- Packing is explicit ops action (not automatic from status)
- Shipment creation only after: Trello intent + packing confirmed + payment/approval rules met
- Courier API failures keep order in Ready to Ship state (safe retry)
- Trello and Slack are mirrors of backend state, not decision makers
- All side effects flow through event listeners, maintaining separation of concerns
This commit is contained in:
twotalesanimation
2026-01-02 13:47:31 +02:00
parent 4730f5770c
commit 12aadfd917
35 changed files with 1815 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class SlackNotifierService
{
protected array $webhooks = [];
public function __construct()
{
$this->webhooks = [
'orders' => config('slack.webhooks.orders'),
'design' => config('slack.webhooks.design'),
'production' => config('slack.webhooks.production'),
'shipping' => config('slack.webhooks.shipping'),
'ops_alerts' => config('slack.webhooks.ops_alerts'),
];
}
/**
* Send notification to #orders channel
*/
public function orders(string $message, array $blocks = []): void
{
$this->send($this->webhooks['orders'], $message, $blocks);
}
/**
* Send notification to #design channel
*/
public function design(string $message, array $blocks = []): void
{
$this->send($this->webhooks['design'], $message, $blocks);
}
/**
* Send notification to #production channel
*/
public function production(string $message, array $blocks = []): void
{
$this->send($this->webhooks['production'], $message, $blocks);
}
/**
* Send notification to #shipping channel
*/
public function shipping(string $message, array $blocks = []): void
{
$this->send($this->webhooks['shipping'], $message, $blocks);
}
/**
* Send notification to #ops-alerts channel
*/
public function opsAlerts(string $message, array $blocks = []): void
{
$this->send($this->webhooks['ops_alerts'], $message, $blocks);
}
/**
* Send payload to Slack webhook
*/
protected function send(string $webhook, string $message, array $blocks = []): void
{
if (! $webhook) {
Log::warning('Slack webhook not configured for channel', ['message' => $message]);
return;
}
try {
$payload = ['text' => $message];
if (! empty($blocks)) {
$payload['blocks'] = $blocks;
}
Http::post($webhook, $payload);
} catch (\Exception $e) {
Log::error('Failed to send Slack notification', [
'webhook' => substr($webhook, 0, 20).'...',
'message' => $message,
'error' => $e->getMessage(),
]);
}
}
}