Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
84.48% |
49 / 58 |
|
37.50% |
3 / 8 |
CRAP | |
0.00% |
0 / 1 |
| ComebackCashAbly | |
84.48% |
49 / 58 |
|
37.50% |
3 / 8 |
18.08 | |
0.00% |
0 / 1 |
| __construct | |
66.67% |
10 / 15 |
|
0.00% |
0 / 1 |
5.93 | |||
| setEventService | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| publish | |
91.67% |
11 / 12 |
|
0.00% |
0 / 1 |
4.01 | |||
| broadcastSettingsUpdate | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
| broadcastEventStarted | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
| broadcastEventEnded | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
| getActiveEventSettings | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
3.14 | |||
| isEnabled | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | namespace 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 | */ |
| 20 | class 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 | } |