Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 61
0.00% covered (danger)
0.00%
0 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
IntegrationBadge
0.00% covered (danger)
0.00%
0 / 61
0.00% covered (danger)
0.00%
0 / 15
992
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 fromIntegration
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 isNew
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 needsSetup
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 hasErrorContext
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getBadgeTypes
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getPrimaryBadge
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getSecondaryBadge
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 getStatusColor
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getStatusBadgeClass
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getStatusLabel
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getErrorContext
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 toArray
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 computeIsNew
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 getStatusBadgeType
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2
3namespace BuyerKiosk\EventManagement\Models;
4
5use DateTime;
6use 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 */
26class 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}