Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 81
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
ChatBillingService
0.00% covered (danger)
0.00%
0 / 81
0.00% covered (danger)
0.00%
0 / 6
110
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 createWithDefaults
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 trackUsage
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
6
 getUsageReport
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 getUsageSummary
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
12
 getGlobalUsageSummary
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace BuyerKiosk\Chat\Services;
4
5use PDO;
6use DateTime;
7
8/**
9 * ChatBillingService - SMS Usage Tracking for Billing
10 *
11 * Tracks SMS message usage for billing and analytics per PRD Feature 9 (lines 197-205):
12 * - Every outbound message logged with category (transactional/interactive)
13 * - Transactional messages marked as included (free) - NOT billable
14 * - Interactive messages marked as billable
15 * - Usage aggregated by store and billing period
16 * - Usage report available to admin
17 *
18 * Billing Rules:
19 * - Transactional messages: NOT billable (free) - e.g., "Your buy is ready"
20 * - Interactive messages: BILLABLE - customer replies, staff freetext responses
21 * - Inbound messages: Always 'interactive' category (billable)
22 * - Outbound with templateId: Check template category
23 * - Outbound freetext: Always 'interactive' (billable)
24 *
25 * @package BuyerKiosk\Chat\Services
26 */
27class ChatBillingService
28{
29    /**
30     * Default cost per SMS segment (USD)
31     */
32    private const DEFAULT_COST_PER_SEGMENT = 0.0075;
33
34    /**
35     * @var PDO Central database connection (kiosk_buykiosk)
36     */
37    private PDO $centralDb;
38
39    /**
40     * @var float Cost per segment (can be overridden)
41     */
42    private float $costPerSegment;
43
44    /**
45     * Create a new ChatBillingService
46     *
47     * @param PDO $centralDb Central database connection (kiosk_buykiosk)
48     * @param float|null $costPerSegment Optional override for cost per segment
49     */
50    public function __construct(PDO $centralDb, ?float $costPerSegment = null)
51    {
52        $this->centralDb = $centralDb;
53        $this->costPerSegment = $costPerSegment ?? self::DEFAULT_COST_PER_SEGMENT;
54    }
55
56    /**
57     * Create service with default dependencies using global functions
58     *
59     * Factory method for production use where we use the actual BaseModel functions.
60     *
61     * @return self
62     */
63    public static function createWithDefaults(): self
64    {
65        global $db_name;
66
67        $centralDb = dbConnectByName($db_name);
68
69        return new self($centralDb);
70    }
71
72    /**
73     * Track SMS usage for billing
74     *
75     * Records a message in the chat_sms_usage table for billing and analytics.
76     * Automatically determines billable status based on category:
77     * - 'transactional' = NOT billable (free tier)
78     * - 'interactive' = billable
79     *
80     * @param string $typeNum Store identifier (e.g., 'ou00')
81     * @param int|null $threadId Reference to chat_threads.id in store DB (null for webhooks)
82     * @param int|null $messageId Reference to chat_messages.id in store DB (null for webhooks)
83     * @param string $direction Message direction: 'inbound' or 'outbound'
84     * @param string $category Message category: 'transactional' or 'interactive'
85     * @param string $provider SMS provider: 'vonage' or 'twilio'
86     * @param string|null $providerMessageId External message ID from provider
87     * @param int $segmentCount Number of SMS segments
88     * @param DateTime $sentAt When the message was sent
89     * @return int The inserted usage record ID
90     */
91    public function trackUsage(
92        string $typeNum,
93        ?int $threadId,
94        ?int $messageId,
95        string $direction,
96        string $category,
97        string $provider,
98        ?string $providerMessageId,
99        int $segmentCount,
100        DateTime $sentAt
101    ): int {
102        // Determine billable status based on category
103        // Transactional = free tier (not billable)
104        // Interactive = billable
105        $billable = ($category === 'interactive') ? 1 : 0;
106
107        // Calculate cost
108        $totalCost = $segmentCount * $this->costPerSegment;
109
110        // Calculate billing period from sent_at
111        $billingPeriod = $sentAt->format('Y-m');
112
113        // Insert usage record
114        $sql = "INSERT INTO chat_sms_usage (
115                    typeNum,
116                    thread_id,
117                    message_id,
118                    direction,
119                    category,
120                    provider,
121                    provider_message_id,
122                    segment_count,
123                    cost_per_segment,
124                    total_cost,
125                    billable,
126                    sent_at,
127                    billing_period
128                ) VALUES (
129                    :typeNum,
130                    :thread_id,
131                    :message_id,
132                    :direction,
133                    :category,
134                    :provider,
135                    :provider_message_id,
136                    :segment_count,
137                    :cost_per_segment,
138                    :total_cost,
139                    :billable,
140                    :sent_at,
141                    :billing_period
142                )";
143
144        $stmt = $this->centralDb->prepare($sql);
145        $stmt->execute([
146            ':typeNum' => $typeNum,
147            ':thread_id' => $threadId,
148            ':message_id' => $messageId,
149            ':direction' => $direction,
150            ':category' => $category,
151            ':provider' => $provider,
152            ':provider_message_id' => $providerMessageId,
153            ':segment_count' => $segmentCount,
154            ':cost_per_segment' => $this->costPerSegment,
155            ':total_cost' => $totalCost,
156            ':billable' => $billable,
157            ':sent_at' => $sentAt->format('Y-m-d H:i:s'),
158            ':billing_period' => $billingPeriod,
159        ]);
160
161        return (int) $this->centralDb->lastInsertId();
162    }
163
164    /**
165     * Get detailed usage report for a store and billing period
166     *
167     * Returns all messages for the specified store and billing period,
168     * ordered by sent_at descending (most recent first).
169     *
170     * @param string $typeNum Store identifier (e.g., 'ou00')
171     * @param string $billingPeriod Billing period in 'YYYY-MM' format
172     * @return array Array of usage records
173     */
174    public function getUsageReport(string $typeNum, string $billingPeriod): array
175    {
176        $sql = "SELECT
177                    id,
178                    typeNum,
179                    thread_id,
180                    message_id,
181                    direction,
182                    category,
183                    provider,
184                    provider_message_id,
185                    segment_count,
186                    cost_per_segment,
187                    total_cost,
188                    billable,
189                    sent_at,
190                    billing_period,
191                    created_at
192                FROM chat_sms_usage
193                WHERE typeNum = :typeNum
194                  AND billing_period = :billing_period
195                ORDER BY sent_at DESC";
196
197        $stmt = $this->centralDb->prepare($sql);
198        $stmt->execute([
199            ':typeNum' => $typeNum,
200            ':billing_period' => $billingPeriod,
201        ]);
202
203        return $stmt->fetchAll(PDO::FETCH_ASSOC);
204    }
205
206    /**
207     * Get usage summary for a store and billing period
208     *
209     * Returns aggregated statistics including:
210     * - total: Total message count
211     * - billable: Billable message count (interactive)
212     * - free: Free message count (transactional)
213     * - totalSegments: Total SMS segments
214     * - billableSegments: Billable segments only
215     * - totalCost: Total cost for all messages
216     * - billableCost: Cost for billable messages only
217     *
218     * @param string $typeNum Store identifier (e.g., 'ou00')
219     * @param string $billingPeriod Billing period in 'YYYY-MM' format
220     * @return array Summary statistics
221     */
222    public function getUsageSummary(string $typeNum, string $billingPeriod): array
223    {
224        $sql = "SELECT
225                    COUNT(*) as total_messages,
226                    SUM(CASE WHEN billable = 1 THEN 1 ELSE 0 END) as billable_messages,
227                    SUM(CASE WHEN billable = 0 THEN 1 ELSE 0 END) as free_messages,
228                    SUM(segment_count) as total_segments,
229                    SUM(CASE WHEN billable = 1 THEN segment_count ELSE 0 END) as billable_segments,
230                    SUM(total_cost) as total_cost,
231                    SUM(CASE WHEN billable = 1 THEN total_cost ELSE 0 END) as billable_cost
232                FROM chat_sms_usage
233                WHERE typeNum = :typeNum
234                  AND billing_period = :billing_period";
235
236        $stmt = $this->centralDb->prepare($sql);
237        $stmt->execute([
238            ':typeNum' => $typeNum,
239            ':billing_period' => $billingPeriod,
240        ]);
241
242        $result = $stmt->fetch(PDO::FETCH_ASSOC);
243
244        // Handle empty period (no data)
245        if (!$result || $result['total_messages'] === null) {
246            return [
247                'total' => 0,
248                'billable' => 0,
249                'free' => 0,
250                'totalSegments' => 0,
251                'billableSegments' => 0,
252                'totalCost' => 0.0,
253                'billableCost' => 0.0,
254            ];
255        }
256
257        return [
258            'total' => (int) $result['total_messages'],
259            'billable' => (int) $result['billable_messages'],
260            'free' => (int) $result['free_messages'],
261            'totalSegments' => (int) $result['total_segments'],
262            'billableSegments' => (int) $result['billable_segments'],
263            'totalCost' => (float) $result['total_cost'],
264            'billableCost' => (float) $result['billable_cost'],
265        ];
266    }
267
268    /**
269     * Get usage summary across all stores for a billing period
270     *
271     * Returns aggregated statistics for all stores, useful for admin reports.
272     *
273     * @param string $billingPeriod Billing period in 'YYYY-MM' format
274     * @return array Array of summaries keyed by typeNum
275     */
276    public function getGlobalUsageSummary(string $billingPeriod): array
277    {
278        $sql = "SELECT
279                    typeNum,
280                    COUNT(*) as total_messages,
281                    SUM(CASE WHEN billable = 1 THEN 1 ELSE 0 END) as billable_messages,
282                    SUM(CASE WHEN billable = 0 THEN 1 ELSE 0 END) as free_messages,
283                    SUM(segment_count) as total_segments,
284                    SUM(CASE WHEN billable = 1 THEN segment_count ELSE 0 END) as billable_segments,
285                    SUM(total_cost) as total_cost,
286                    SUM(CASE WHEN billable = 1 THEN total_cost ELSE 0 END) as billable_cost
287                FROM chat_sms_usage
288                WHERE billing_period = :billing_period
289                GROUP BY typeNum
290                ORDER BY total_messages DESC";
291
292        $stmt = $this->centralDb->prepare($sql);
293        $stmt->execute([
294            ':billing_period' => $billingPeriod,
295        ]);
296
297        $results = $stmt->fetchAll(PDO::FETCH_ASSOC);
298        $summaries = [];
299
300        foreach ($results as $row) {
301            $summaries[$row['typeNum']] = [
302                'total' => (int) $row['total_messages'],
303                'billable' => (int) $row['billable_messages'],
304                'free' => (int) $row['free_messages'],
305                'totalSegments' => (int) $row['total_segments'],
306                'billableSegments' => (int) $row['billable_segments'],
307                'totalCost' => (float) $row['total_cost'],
308                'billableCost' => (float) $row['billable_cost'],
309            ];
310        }
311
312        return $summaries;
313    }
314}