Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 61 |
|
0.00% |
0 / 15 |
CRAP | |
0.00% |
0 / 1 |
| IntegrationBadge | |
0.00% |
0 / 61 |
|
0.00% |
0 / 15 |
992 | |
0.00% |
0 / 1 |
| __construct | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
| fromIntegration | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
| isNew | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| needsSetup | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| hasErrorContext | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getBadgeTypes | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
| getPrimaryBadge | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
| getSecondaryBadge | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
12 | |||
| getStatusColor | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getStatusBadgeClass | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getStatusLabel | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
| getErrorContext | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
12 | |||
| toArray | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
2 | |||
| computeIsNew | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
| getStatusBadgeType | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
42 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace BuyerKiosk\EventManagement\Models; |
| 4 | |
| 5 | use DateTime; |
| 6 | use Exception; |
| 7 | |
| 8 | /** |
| 9 | * IntegrationBadge - Value object that computes badge state for integration cards |
| 10 | * |
| 11 | * Determines which badges to display for an integration on the event detail page: |
| 12 | * - "New" badge for integrations created within the last 24 hours |
| 13 | * - "Setup Required" badge for failed integrations (takes precedence over "New") |
| 14 | * - Status badges (Pending, Active, Completed) with appropriate colors |
| 15 | * |
| 16 | * Badge Priority Order: |
| 17 | * 1. setup-required (if failed) - ALWAYS takes precedence |
| 18 | * 2. new (if < 24 hours old AND not failed) |
| 19 | * 3. Status badge (pending, active, completed) |
| 20 | * |
| 21 | * PRD Reference: docs/specs/008-post-event-integration-confirmation/product-requirements.md |
| 22 | * Acceptance Criteria: Lines 184-194 (Feature 4: Integration Status Indicators) |
| 23 | * |
| 24 | * @package BuyerKiosk\EventManagement\Models |
| 25 | */ |
| 26 | class IntegrationBadge |
| 27 | { |
| 28 | // Badge type constants |
| 29 | public const BADGE_NEW = 'new'; |
| 30 | public const BADGE_SETUP_REQUIRED = 'setup-required'; |
| 31 | public const BADGE_PENDING = 'pending'; |
| 32 | public const BADGE_ACTIVE = 'active'; |
| 33 | public const BADGE_COMPLETED = 'completed'; |
| 34 | |
| 35 | // 24 hours in seconds |
| 36 | public const NEW_THRESHOLD_SECONDS = 86400; |
| 37 | |
| 38 | // Status to color mapping |
| 39 | private const STATUS_COLORS = [ |
| 40 | EventIntegration::STATUS_PENDING => 'yellow', |
| 41 | EventIntegration::STATUS_ACTIVE => 'green', |
| 42 | EventIntegration::STATUS_COMPLETED => 'gray', |
| 43 | EventIntegration::STATUS_FAILED => 'red', |
| 44 | ]; |
| 45 | |
| 46 | // Status to badge class mapping |
| 47 | private const STATUS_BADGE_CLASSES = [ |
| 48 | EventIntegration::STATUS_PENDING => 'badge-warning', |
| 49 | EventIntegration::STATUS_ACTIVE => 'badge-success', |
| 50 | EventIntegration::STATUS_COMPLETED => 'badge-default', |
| 51 | EventIntegration::STATUS_FAILED => 'badge-danger', |
| 52 | ]; |
| 53 | |
| 54 | /** |
| 55 | * @var string The integration status |
| 56 | */ |
| 57 | private string $status; |
| 58 | |
| 59 | /** |
| 60 | * @var string|null Created timestamp |
| 61 | */ |
| 62 | private ?string $createdAt; |
| 63 | |
| 64 | /** |
| 65 | * @var array|null Integration configuration (may contain error context) |
| 66 | */ |
| 67 | private ?array $config; |
| 68 | |
| 69 | /** |
| 70 | * @var bool Whether the integration is new (< 24 hours old) |
| 71 | */ |
| 72 | private bool $isNew; |
| 73 | |
| 74 | /** |
| 75 | * @var bool Whether the integration needs setup (failed status) |
| 76 | */ |
| 77 | private bool $needsSetup; |
| 78 | |
| 79 | /** |
| 80 | * Private constructor - use factory method |
| 81 | * |
| 82 | * @param string $status Integration status |
| 83 | * @param string|null $createdAt Created timestamp |
| 84 | * @param array|null $config Integration configuration |
| 85 | */ |
| 86 | private function __construct(string $status, ?string $createdAt, ?array $config) |
| 87 | { |
| 88 | $this->status = $status; |
| 89 | $this->createdAt = $createdAt; |
| 90 | $this->config = $config; |
| 91 | |
| 92 | // Compute derived state |
| 93 | $this->needsSetup = ($status === EventIntegration::STATUS_FAILED); |
| 94 | $this->isNew = $this->computeIsNew($createdAt); |
| 95 | } |
| 96 | |
| 97 | /** |
| 98 | * Factory method to create an IntegrationBadge from an EventIntegration |
| 99 | * |
| 100 | * @param EventIntegration $integration The integration to compute badges for |
| 101 | * @return self The computed badge state |
| 102 | */ |
| 103 | public static function fromIntegration(EventIntegration $integration): self |
| 104 | { |
| 105 | return new self( |
| 106 | $integration->status, |
| 107 | $integration->createdAt, |
| 108 | $integration->config |
| 109 | ); |
| 110 | } |
| 111 | |
| 112 | /** |
| 113 | * Check if the integration is new (created within last 24 hours) |
| 114 | * |
| 115 | * @return bool True if integration is less than 24 hours old |
| 116 | */ |
| 117 | public function isNew(): bool |
| 118 | { |
| 119 | return $this->isNew; |
| 120 | } |
| 121 | |
| 122 | /** |
| 123 | * Check if the integration needs setup (failed status) |
| 124 | * |
| 125 | * @return bool True if integration has failed and needs configuration |
| 126 | */ |
| 127 | public function needsSetup(): bool |
| 128 | { |
| 129 | return $this->needsSetup; |
| 130 | } |
| 131 | |
| 132 | /** |
| 133 | * Check if the integration has error context available |
| 134 | * |
| 135 | * @return bool True if there is an error message in the config |
| 136 | */ |
| 137 | public function hasErrorContext(): bool |
| 138 | { |
| 139 | return $this->getErrorContext() !== null; |
| 140 | } |
| 141 | |
| 142 | /** |
| 143 | * Get the list of badge types in priority order |
| 144 | * |
| 145 | * Priority order: |
| 146 | * 1. Status badge first (primary) |
| 147 | * 2. "new" badge second (secondary) - only if not failed |
| 148 | * |
| 149 | * For failed integrations, only 'setup-required' is returned. |
| 150 | * |
| 151 | * @return array Badge types in display order |
| 152 | */ |
| 153 | public function getBadgeTypes(): array |
| 154 | { |
| 155 | $badges = []; |
| 156 | |
| 157 | if ($this->needsSetup) { |
| 158 | // Failed integrations only show setup-required |
| 159 | $badges[] = self::BADGE_SETUP_REQUIRED; |
| 160 | } else { |
| 161 | // Add status badge first (primary) |
| 162 | $badges[] = $this->getStatusBadgeType(); |
| 163 | |
| 164 | // Add "new" badge second (secondary) if applicable |
| 165 | if ($this->isNew) { |
| 166 | $badges[] = self::BADGE_NEW; |
| 167 | } |
| 168 | } |
| 169 | |
| 170 | return $badges; |
| 171 | } |
| 172 | |
| 173 | /** |
| 174 | * Get the primary badge type |
| 175 | * |
| 176 | * For failed integrations: 'setup-required' |
| 177 | * For others: the status badge (pending, active, completed) |
| 178 | * |
| 179 | * @return string The primary badge type |
| 180 | */ |
| 181 | public function getPrimaryBadge(): string |
| 182 | { |
| 183 | if ($this->needsSetup) { |
| 184 | return self::BADGE_SETUP_REQUIRED; |
| 185 | } |
| 186 | |
| 187 | return $this->getStatusBadgeType(); |
| 188 | } |
| 189 | |
| 190 | /** |
| 191 | * Get the secondary badge type (if any) |
| 192 | * |
| 193 | * Only returns 'new' for non-failed integrations that are < 24 hours old. |
| 194 | * |
| 195 | * @return string|null The secondary badge type or null |
| 196 | */ |
| 197 | public function getSecondaryBadge(): ?string |
| 198 | { |
| 199 | // Failed integrations don't get a secondary badge |
| 200 | if ($this->needsSetup) { |
| 201 | return null; |
| 202 | } |
| 203 | |
| 204 | // Return "new" if applicable |
| 205 | return $this->isNew ? self::BADGE_NEW : null; |
| 206 | } |
| 207 | |
| 208 | /** |
| 209 | * Get the CSS color class for the status |
| 210 | * |
| 211 | * @return string Color name (yellow, green, gray, red) |
| 212 | */ |
| 213 | public function getStatusColor(): string |
| 214 | { |
| 215 | return self::STATUS_COLORS[$this->status] ?? 'gray'; |
| 216 | } |
| 217 | |
| 218 | /** |
| 219 | * Get the full CSS class for the status badge |
| 220 | * |
| 221 | * @return string CSS class (badge-warning, badge-success, badge-default, badge-danger) |
| 222 | */ |
| 223 | public function getStatusBadgeClass(): string |
| 224 | { |
| 225 | return self::STATUS_BADGE_CLASSES[$this->status] ?? 'badge-default'; |
| 226 | } |
| 227 | |
| 228 | /** |
| 229 | * Get the display label for the status |
| 230 | * |
| 231 | * Failed status shows "Setup Required", others show capitalized status. |
| 232 | * |
| 233 | * @return string Human-readable status label |
| 234 | */ |
| 235 | public function getStatusLabel(): string |
| 236 | { |
| 237 | if ($this->needsSetup) { |
| 238 | return 'Setup Required'; |
| 239 | } |
| 240 | |
| 241 | return ucfirst($this->status); |
| 242 | } |
| 243 | |
| 244 | /** |
| 245 | * Get the error context message for failed integrations |
| 246 | * |
| 247 | * @return string|null Error message or null if not available |
| 248 | */ |
| 249 | public function getErrorContext(): ?string |
| 250 | { |
| 251 | if (!$this->needsSetup || $this->config === null) { |
| 252 | return null; |
| 253 | } |
| 254 | |
| 255 | return $this->config['error'] ?? null; |
| 256 | } |
| 257 | |
| 258 | /** |
| 259 | * Convert the badge state to an array for serialization |
| 260 | * |
| 261 | * @return array Badge state as associative array |
| 262 | */ |
| 263 | public function toArray(): array |
| 264 | { |
| 265 | return [ |
| 266 | 'isNew' => $this->isNew, |
| 267 | 'needsSetup' => $this->needsSetup, |
| 268 | 'status' => $this->status, |
| 269 | 'primaryBadge' => $this->getPrimaryBadge(), |
| 270 | 'secondaryBadge' => $this->getSecondaryBadge(), |
| 271 | 'badgeTypes' => $this->getBadgeTypes(), |
| 272 | 'statusColor' => $this->getStatusColor(), |
| 273 | 'statusBadgeClass' => $this->getStatusBadgeClass(), |
| 274 | 'statusLabel' => $this->getStatusLabel(), |
| 275 | 'errorContext' => $this->getErrorContext(), |
| 276 | ]; |
| 277 | } |
| 278 | |
| 279 | /** |
| 280 | * Compute whether the integration is new based on createdAt timestamp |
| 281 | * |
| 282 | * Handles edge cases: |
| 283 | * - null/empty dates -> not new |
| 284 | * - Invalid dates -> not new |
| 285 | * - Future dates -> treated as new (within 24 hour window) |
| 286 | * |
| 287 | * @param string|null $createdAt Created timestamp |
| 288 | * @return bool True if integration is less than 24 hours old |
| 289 | */ |
| 290 | private function computeIsNew(?string $createdAt): bool |
| 291 | { |
| 292 | // Null or empty dates are treated as not new |
| 293 | if ($createdAt === null || $createdAt === '') { |
| 294 | return false; |
| 295 | } |
| 296 | |
| 297 | try { |
| 298 | $created = new DateTime($createdAt); |
| 299 | $now = new DateTime(); |
| 300 | |
| 301 | // Calculate the difference in seconds |
| 302 | $diffSeconds = $now->getTimestamp() - $created->getTimestamp(); |
| 303 | |
| 304 | // New if less than 24 hours (86400 seconds) |
| 305 | // Future dates result in negative diff, which is < 86400, so treated as new |
| 306 | return $diffSeconds < self::NEW_THRESHOLD_SECONDS; |
| 307 | } catch (Exception $e) { |
| 308 | // Invalid date format -> not new |
| 309 | return false; |
| 310 | } |
| 311 | } |
| 312 | |
| 313 | /** |
| 314 | * Get the status badge type constant |
| 315 | * |
| 316 | * Maps integration status to badge type constant. |
| 317 | * |
| 318 | * @return string Badge type for the current status |
| 319 | */ |
| 320 | private function getStatusBadgeType(): string |
| 321 | { |
| 322 | return match ($this->status) { |
| 323 | EventIntegration::STATUS_PENDING => self::BADGE_PENDING, |
| 324 | EventIntegration::STATUS_ACTIVE => self::BADGE_ACTIVE, |
| 325 | EventIntegration::STATUS_COMPLETED => self::BADGE_COMPLETED, |
| 326 | EventIntegration::STATUS_FAILED => self::BADGE_SETUP_REQUIRED, |
| 327 | default => $this->status, // Return as-is for unknown statuses |
| 328 | }; |
| 329 | } |
| 330 | } |