getContent() === '' || ! $request->hasHeader('X-Trello-Webhook')) { Log::info('Trello webhook validation ping received'); return response()->json(['ok' => true], 200); } // 2️⃣ Verify webhook signature (real events only) $signature = $request->header('X-Trello-Webhook'); if (! $this->verifyWebhookSignature($request, $signature)) { Log::warning('Invalid Trello webhook signature'); return response()->json(['error' => 'Invalid signature'], 401); } try { $payload = $request->json()->all(); Log::info('Trello webhook received', [ 'action' => $payload['action']['type'] ?? 'unknown', ]); match ($payload['action']['type'] ?? null) { 'updateCard' => $this->handleCardUpdate($payload), 'updateCheckItem' => $this->handleChecklistUpdate($payload), default => Log::info('Unhandled Trello action', [ 'type' => $payload['action']['type'] ?? 'unknown', ]), }; return response()->json(['success' => true], 200); } catch (\Throwable $e) { Log::error('Error processing Trello webhook', [ 'error' => $e->getMessage(), ]); // ⚠️ Still return 200 so Trello does not disable the webhook return response()->json(['error' => 'Processing failed'], 200); } } /** * Handle card movement between lists */ private function handleCardUpdate(array $payload): void { $action = $payload['action'] ?? []; $cardId = $action['data']['card']['id'] ?? null; $cardName = $action['data']['card']['name'] ?? null; $listName = $action['data']['listAfter']['name'] ?? null; if (! $cardId || ! $listName) { return; } // Extract order ID from card name (e.g., "Order #1043") if (! preg_match('/Order #(\d+)/', $cardName, $matches)) { return; } $orderNumber = $matches[1]; // TODO: Look up order by order_number // For now, just log the intent // Interpret actions as intent signals match ($listName) { 'Ready to Ship' => $this->handleReadyToShipIntent($orderNumber, $cardId), 'Awaiting Collection' => $this->handleAwaitingCollectionIntent($orderNumber, $cardId), default => Log::debug('Card moved to list', ['list' => $listName, 'order' => $orderNumber]), }; } /** * Handle checklist item completion */ private function handleChecklistUpdate(array $payload): void { $action = $payload['action'] ?? []; $checklistName = $action['data']['checklist']['name'] ?? null; $itemName = $action['data']['checkItem']['name'] ?? null; $itemState = $action['data']['checkItem']['state'] ?? null; if ($itemState !== 'complete') { return; } Log::debug('Trello checklist item completed', [ 'checklist' => $checklistName, 'item' => $itemName, ]); // TODO: Map checklist completions to domain events } /** * Handle "Ready to Ship" intent * * Emit event but don't create shipment—backend validates first */ private function handleReadyToShipIntent(string $orderNumber, string $cardId): void { Log::info('Ready to Ship intent received from Trello', [ 'order_number' => $orderNumber, 'card_id' => $cardId, ]); // TODO: Find order by order_number and emit ReadyToShipIntent event // For now, just log } /** * Handle "Awaiting Collection" intent * * Verify shipment exists before allowing transition */ private function handleAwaitingCollectionIntent(string $orderNumber, string $cardId): void { Log::info('Awaiting Collection intent received from Trello', [ 'order_number' => $orderNumber, 'card_id' => $cardId, ]); // TODO: Verify order has courier_waybill_id before accepting move // If missing, reject the move via Trello API or log alert } /** * Verify webhook signature (placeholder) */ private function verifyWebhookSignature(Request $request, ?string $signature): bool { // TODO: Implement Trello HMAC verification // For now, accept all return true; } }