Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 58
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
ComebackCashAbly
0.00% covered (danger)
0.00%
0 / 58
0.00% covered (danger)
0.00%
0 / 8
306
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
30
 setEventService
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 publish
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 broadcastSettingsUpdate
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 broadcastEventStarted
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 broadcastEventEnded
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 getActiveEventSettings
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 isEnabled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2namespace BuyerKiosk\ComebackCash\Services;
3
4/**
5 * ComebackCashAbly - Real-time event publishing for Comeback Cash
6 *
7 * Publishes events to Ably for real-time synchronization of
8 * Comeback Cash settings to connected POS systems.
9 *
10 * Events published:
11 * - comeback_cash.settings_updated: When event config changes
12 * - comeback_cash.event_started: When event goes active
13 * - comeback_cash.event_ended: When event ends or is cancelled
14 *
15 * SECURITY NOTE: All payloads must exclude PII (phone numbers, emails, etc.)
16 * Ably channels are shared and visible to all subscribers.
17 *
18 * @package BuyerKiosk\ComebackCash\Services
19 */
20class ComebackCashAbly
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 $typeNum;
31
32    /**
33     * @var EventService Event service for retrieving active events
34     */
35    private $eventService;
36
37    /**
38     * @var bool Whether Ably is enabled and available
39     */
40    private $enabled;
41
42    /**
43     * Constructor
44     *
45     * @param string $typeNum Store identifier (e.g., 'ou00', 'pa00')
46     * @param \Ably\AblyRest|null $ably Ably REST client (null to auto-initialize or disable)
47     * @param EventService|null $eventService Event service for retrieving settings
48     */
49    public function __construct(string $typeNum, $ably = null, ?EventService $eventService = null)
50    {
51        $this->typeNum = $typeNum;
52        $this->eventService = $eventService;
53        $this->enabled = false;
54
55        // If Ably client provided, use it
56        if ($ably !== null) {
57            $this->ably = $ably;
58            $this->enabled = true;
59            return;
60        }
61
62        // Auto-initialize from environment
63        if (!isset($_ENV['ABLY_KEY']) || empty($_ENV['ABLY_KEY'])) {
64            error_log("ComebackCashAbly: ABLY_KEY not configured, real-time events disabled");
65            return;
66        }
67
68        try {
69            $this->ably = new \Ably\AblyRest($_ENV['ABLY_KEY']);
70            $this->enabled = true;
71        } catch (\Exception $e) {
72            error_log("ComebackCashAbly: Failed to initialize Ably - " . $e->getMessage());
73            $this->enabled = false;
74        }
75    }
76
77    /**
78     * Set the EventService (used for dependency injection after construction)
79     *
80     * @param EventService $eventService
81     * @return void
82     */
83    public function setEventService(EventService $eventService): void
84    {
85        $this->eventService = $eventService;
86    }
87
88    /**
89     * Publish an event to the Ably channel
90     *
91     * @param string $action Event action name
92     * @param array $data Event data payload
93     * @return void
94     */
95    protected function publish(string $action, array $data): void
96    {
97        if (!$this->enabled || $this->ably === null) {
98            return;
99        }
100
101        try {
102            $payload = array_merge($data, [
103                'action' => $action,
104                'category' => $this->typeNum,
105                'timestamp' => time(),
106                'source' => 'workspace'
107            ]);
108
109            $channel = $this->ably->channel($this->typeNum);
110            $channel->publish($action, $payload);
111        } catch (\Exception $e) {
112            // Log but don't throw - Ably failures shouldn't break main flow
113            // POS systems can fall back to polling /settings API
114            error_log("ComebackCashAbly: Failed to publish event '{$action}' - " . $e->getMessage());
115        }
116    }
117
118    /**
119     * Broadcast settings update to all connected POS systems
120     *
121     * Called when event status changes or configuration is modified.
122     * POS systems use this to update their cached settings.
123     *
124     * Payload structure:
125     * - action: 'comeback_cash.settings_updated'
126     * - version: timestamp (for cache invalidation)
127     * - buy_side: active buy-side event settings or null
128     * - sales_side: active sales-side event settings or null
129     * - timestamp: event timestamp
130     * - source: 'workspace'
131     *
132     * @return void
133     */
134    public function broadcastSettingsUpdate(): void
135    {
136        $payload = [
137            'action' => 'comeback_cash.settings_updated',
138            'version' => time(),
139            'buy_side' => $this->getActiveEventSettings('buy'),
140            'sales_side' => $this->getActiveEventSettings('sales'),
141        ];
142
143        $this->publish('comeback_cash.settings_updated', $payload);
144    }
145
146    /**
147     * Broadcast event started notification
148     *
149     * Notifies POS systems that a new event has become active.
150     * They should refresh their settings.
151     *
152     * @param int $eventId Event ID that started
153     * @param string $side Event side ('buy' or 'sales')
154     * @return void
155     */
156    public function broadcastEventStarted(int $eventId, string $side): void
157    {
158        $payload = [
159            'action' => 'comeback_cash.event_started',
160            'event_id' => $eventId,
161            'side' => $side,
162            'version' => time(),
163            'buy_side' => $this->getActiveEventSettings('buy'),
164            'sales_side' => $this->getActiveEventSettings('sales'),
165        ];
166
167        $this->publish('comeback_cash.event_started', $payload);
168    }
169
170    /**
171     * Broadcast event ended notification
172     *
173     * Notifies POS systems that an event has ended or been cancelled.
174     * They should refresh their settings.
175     *
176     * @param int $eventId Event ID that ended
177     * @param string $side Event side ('buy' or 'sales')
178     * @return void
179     */
180    public function broadcastEventEnded(int $eventId, string $side): void
181    {
182        $payload = [
183            'action' => 'comeback_cash.event_ended',
184            'event_id' => $eventId,
185            'side' => $side,
186            'version' => time(),
187            'buy_side' => $this->getActiveEventSettings('buy'),
188            'sales_side' => $this->getActiveEventSettings('sales'),
189        ];
190
191        $this->publish('comeback_cash.event_ended', $payload);
192    }
193
194    /**
195     * Get active event settings for a side
196     *
197     * Returns the settings array for POS consumption, or null if no active event.
198     * The settings array is sanitized to exclude any PII.
199     *
200     * @param string $side Event side ('buy' or 'sales')
201     * @return array|null Settings array or null if no active event
202     */
203    private function getActiveEventSettings(string $side): ?array
204    {
205        if ($this->eventService === null) {
206            return null;
207        }
208
209        $event = $this->eventService->getActiveEvent($side);
210        return $event ? $event->toSettingsArray() : null;
211    }
212
213    /**
214     * Check if Ably broadcasting is enabled
215     *
216     * @return bool True if Ably is configured and available
217     */
218    public function isEnabled(): bool
219    {
220        return $this->enabled;
221    }
222}