Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
11.36% covered (danger)
11.36%
5 / 44
25.00% covered (danger)
25.00%
2 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
ChatAblyPublisher
11.36% covered (danger)
11.36%
5 / 44
25.00% covered (danger)
25.00%
2 / 8
130.69
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 publish
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 isEnabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 publishNewMessage
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 publishNewThread
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 publishDeliveryUpdate
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 publishThreadClosed
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 publishFreetextUnlocked
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace BuyerKiosk\Chat\Events;
4
5/**
6 * ChatAblyPublisher - Real-time event publishing for Two-Way SMS Chat
7 *
8 * Publishes events to Ably for real-time synchronization across clients
9 * viewing the Chat interface in the Workbook dashboard.
10 *
11 * Events published:
12 * - workbook:chat:message - New message received/sent
13 * - workbook:chat:thread:new - New conversation thread created
14 * - workbook:chat:thread:closed - Thread was closed
15 * - workbook:chat:delivered - Message delivery status update
16 * - workbook:chat:freetext:unlocked - Freetext messaging unlocked by customer reply
17 *
18 * @package BuyerKiosk\Chat\Events
19 */
20class ChatAblyPublisher
21{
22    /**
23     * @var \Ably\AblyRest|null Ably REST client instance
24     */
25    private $ably;
26
27    /**
28     * @var string Store identifier (used as channel name)
29     */
30    private string $typeNum;
31
32    /**
33     * @var bool Whether Ably is enabled and available
34     */
35    private bool $enabled;
36
37    /**
38     * Constructor
39     *
40     * @param string $typeNum Store identifier (e.g., 'ou00', 'pa00')
41     */
42    public function __construct(string $typeNum)
43    {
44        $this->typeNum = $typeNum;
45        $this->enabled = false;
46        $this->ably = null;
47
48        // Check if Ably key is configured
49        if (!isset($_ENV['ABLY_KEY']) || empty($_ENV['ABLY_KEY'])) {
50            error_log("ChatAblyPublisher: ABLY_KEY not configured, real-time events disabled");
51            return;
52        }
53
54        try {
55            $this->ably = new \Ably\AblyRest($_ENV['ABLY_KEY']);
56            $this->enabled = true;
57        } catch (\Exception $e) {
58            error_log("ChatAblyPublisher: Failed to initialize Ably - " . $e->getMessage());
59            $this->enabled = false;
60        }
61    }
62
63    /**
64     * Publish an event to the Ably channel
65     *
66     * @param string $action Event action name
67     * @param array $data Event data payload
68     * @return void
69     */
70    public function publish(string $action, array $data): void
71    {
72        if (!$this->enabled) {
73            return;
74        }
75
76        try {
77            $payload = array_merge($data, [
78                'action' => $action,
79                'category' => $this->typeNum,
80                'timestamp' => date('c'), // ISO 8601 format
81                'source' => 'chat'
82            ]);
83
84            $channel = $this->ably->channel($this->typeNum);
85            $channel->publish($action, $payload);
86        } catch (\Exception $e) {
87            error_log("ChatAblyPublisher: Failed to publish event '{$action}' - " . $e->getMessage());
88        }
89    }
90
91    /**
92     * Check if Ably publishing is enabled
93     *
94     * @return bool
95     */
96    public function isEnabled(): bool
97    {
98        return $this->enabled;
99    }
100
101    // =========================================================================
102    // Chat Message Events
103    // =========================================================================
104
105    /**
106     * Publish new message event
107     *
108     * Called when staff sends message OR customer replies.
109     *
110     * @param int $threadId Thread ID
111     * @param array $messageData Message data containing:
112     *   - messageId: int - Message ID
113     *   - direction: string - 'inbound' or 'outbound'
114     *   - body: string - Message content
115     *   - timestamp: string - ISO 8601 timestamp
116     *   - customerId: int - Customer ID
117     *   - customerName: string - Customer name
118     *   - employeeId: int|null - Employee ID (null for inbound)
119     * @return void
120     */
121    public function publishNewMessage(int $threadId, array $messageData): void
122    {
123        $this->publish('workbook:chat:message', array_merge(
124            ['threadId' => $threadId],
125            $messageData
126        ));
127    }
128
129    /**
130     * Publish new thread event
131     *
132     * Called when a new conversation thread is created.
133     *
134     * @param int $threadId Thread ID
135     * @param array $threadData Thread data containing:
136     *   - customerId: int - Customer ID
137     *   - customerName: string - Customer name
138     *   - customerPhone: string - Customer phone number
139     *   - buyId: int|null - Associated buy ID (if any)
140     *   - status: string - Thread status
141     * @return void
142     */
143    public function publishNewThread(int $threadId, array $threadData): void
144    {
145        $this->publish('workbook:chat:thread:new', array_merge(
146            ['threadId' => $threadId],
147            $threadData
148        ));
149    }
150
151    /**
152     * Publish delivery status update
153     *
154     * Called when SMS delivery status is confirmed by the provider.
155     *
156     * @param int $threadId Thread ID
157     * @param int $messageId Message ID
158     * @param string $status Delivery status: 'delivered', 'failed', 'pending'
159     * @return void
160     */
161    public function publishDeliveryUpdate(int $threadId, int $messageId, string $status): void
162    {
163        $this->publish('workbook:chat:delivered', [
164            'threadId' => $threadId,
165            'messageId' => $messageId,
166            'status' => $status,
167        ]);
168    }
169
170    /**
171     * Publish thread closed event
172     *
173     * Called when a staff member closes a conversation thread.
174     *
175     * @param int $threadId Thread ID
176     * @param int $employeeId Employee ID who closed the thread
177     * @return void
178     */
179    public function publishThreadClosed(int $threadId, int $employeeId): void
180    {
181        $this->publish('workbook:chat:thread:closed', [
182            'threadId' => $threadId,
183            'employeeId' => $employeeId,
184        ]);
185    }
186
187    /**
188     * Publish freetext unlocked event
189     *
190     * Called when a customer replies to a thread, unlocking freetext messaging
191     * for staff members. This enables the compose input for arbitrary messages.
192     *
193     * @param int $threadId Thread ID
194     * @return void
195     */
196    public function publishFreetextUnlocked(int $threadId): void
197    {
198        $this->publish('workbook:chat:freetext:unlocked', [
199            'threadId' => $threadId,
200        ]);
201    }
202}