Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
50.15% covered (warning)
50.15%
168 / 335
11.11% covered (danger)
11.11%
1 / 9
CRAP
n/a
0 / 0
get_ticket_list_action
100.00% covered (success)
100.00%
33 / 33
100.00% covered (success)
100.00%
1 / 1
3
normalize_ticket_taxdetail_amounts
94.44% covered (success)
94.44%
34 / 36
0.00% covered (danger)
0.00%
0 / 1
15.04
ticketlst_set_tid
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
ticketlst_till_needs_label_resolution
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
5.27
ticketlst_is_objid
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
42
ticketlst_labels_from_report
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
4.01
ticketlst_report_label
66.67% covered (warning)
66.67%
6 / 9
0.00% covered (danger)
0.00%
0 / 1
8.81
ticketlst_paidin_device_names
25.00% covered (danger)
25.00%
4 / 16
0.00% covered (danger)
0.00%
0 / 1
21.19
ticketlst_enrich_report_fields
82.14% covered (success)
82.14%
23 / 28
0.00% covered (danger)
0.00%
0 / 1
15.12
1<?php
2
3/**
4 * @file
5 * Place action: ticket list for date range.
6 */
7
8declare(strict_types=1);
9
10require_once __DIR__.'/../../Legacy/common/session.php';
11
12/**
13 * Load tickets from place DB. SESSION (central) is not joined; caller must merge _device from central.
14 *
15 * @param \ddbb $conx_tenant
16 */
17function get_ticket_list_action($conx_tenant, $place, $ini, $end, &$tickets): int
18{
19    $retval = \ApplicationError::Success;
20    if (\success($retval)) {
21        $sql = "select TICKET.*, ";
22        $sql .= "LASTCHANGE.objid as _change, ";
23        $sql .= "TICKETDATA.report as _tdreport, ";
24        $sql .= "TICKETDATA.regstatus as _tdregstatus, ";
25        $sql .= "IVBUSINESS.created as _ivcreated, ";
26        $sql .= "TICKET.audit as _intill ";
27        $sql .= "from TICKET ";
28        $sql .= "left join (";
29        $sql .= "  select ticket, MAX(objid) as maxid from TICKETDATA group by ticket";
30        $sql .= ") LASTDATA on LASTDATA.ticket = TICKET.objid ";
31        $sql .= "left join TICKETDATA on TICKETDATA.objid = LASTDATA.maxid ";
32        $sql .= "left join (";
33        $sql .= "  select TICKETCHANGE.ticket, MAX(TICKETCHANGE.objid) as objid from TICKETCHANGE group by TICKETCHANGE.ticket";
34        $sql .= ") LASTCHANGE on LASTCHANGE.ticket = TICKET.objid ";
35        $sql .= "left join (";
36        $sql .= "  select TICKETINVOICE.ticket as ticket, TICKETINVOICE.created as created";
37        $sql .= "  from TICKETINVOICE ";
38        $sql .= "  inner join (";
39        $sql .= "    select ticket, MAX(objid) as maxid from TICKETINVOICE group by ticket ";
40        $sql .= "  ) LASTINVOICE on TICKETINVOICE.objid = LASTINVOICE.maxid ";
41        $sql .= "  where TICKETINVOICE.status = 'AC'";
42        $sql .= ") IVBUSINESS on IVBUSINESS.ticket = TICKET.objid ";
43        $sql .= "where TICKET.status not in ('PA') ";
44        $sql .= "and ((TICKET.created between '".$ini."' and '".$end."') ";
45        $sql .= "or (TICKET.updated between '".$ini."' and '".$end."')) ";
46        $tables = ['TICKET', 'TICKETDATA', 'TICKETINVOICE', 'TICKETCHANGE'];
47        $res = $conx_tenant->runsql($tables, $sql, $tickets);
48        if ($res === false) {
49            \addlog(__FILE__, \LogLevel::Error, "Ticket list query failed for place ".$place);
50            $retval = \ApplicationError::Database;
51        }
52    }
53    return $retval;
54}
55
56/**
57 * Normalize taxdetail amounts so the sum matches ticket taxes.
58 *
59 * @param array<int, mixed> $taxdetail
60 * @return array<int, array<string, float>>
61 */
62function normalize_ticket_taxdetail_amounts(array $taxdetail, $ticketTaxes): array
63{
64    $rows = [];
65    $target = is_numeric($ticketTaxes) ? (int) round(((float) $ticketTaxes) * 100) : null;
66    $current = 0;
67
68    foreach ($taxdetail as $row) {
69        if (!is_array($row)) {
70            continue;
71        }
72        $amount = isset($row['amount']) ? (float) $row['amount'] : 0.0;
73        if ($amount == 0.0) {
74            continue;
75        }
76        $rate = isset($row['rate']) ? (float) $row['rate'] : 0.0;
77        $base = isset($row['base']) ? (float) $row['base'] : 0.0;
78        $amountCents = (int) round(round($amount, 2) * 100);
79        $baseRounded = round($base, 2);
80        $rows[] = [
81            'rate' => round($rate, 2),
82            'base' => $baseRounded,
83            'amount' => $amountCents / 100,
84            '_amount_cents' => $amountCents,
85        ];
86        $current += $amountCents;
87    }
88
89    if ($target !== null && count($rows) > 0) {
90        $delta = $target - $current;
91        if ($delta !== 0) {
92            $index = 0;
93            $smallest = null;
94            foreach ($rows as $idx => $row) {
95                $amountAbs = abs((int) $row['_amount_cents']);
96                if ($smallest === null || $amountAbs < $smallest) {
97                    $smallest = $amountAbs;
98                    $index = $idx;
99                }
100            }
101            $rows[$index]['_amount_cents'] += $delta;
102            $rows[$index]['amount'] = round($rows[$index]['_amount_cents'] / 100, 2);
103        }
104    }
105
106    foreach ($rows as &$row) {
107        unset($row['_amount_cents']);
108    }
109    unset($row);
110
111    return $rows;
112}
113
114/**
115 * Stores till objid in `tid` from TICKET.till (SQL FK) or report.till.objid.
116 */
117function ticketlst_set_tid(array &$ticket, mixed $objid): void
118{
119    if ($objid === null || $objid === '') {
120        return;
121    }
122
123    $ticket['tid'] = (string) $objid;
124}
125
126/**
127 * True when `till` still needs a display label resolved from `tid` (FK or empty, not report text).
128 */
129function ticketlst_till_needs_label_resolution(array $ticket): bool
130{
131    if (empty($ticket['tid'])) {
132        return false;
133    }
134
135    $tid = (string) $ticket['tid'];
136    if (!array_key_exists('till', $ticket)) {
137        return true;
138    }
139
140    $till = $ticket['till'];
141    if ($till === null || $till === '') {
142        return true;
143    }
144
145    return (string) $till === $tid;
146}
147
148/**
149 * True when paidin still holds a SESSION objid (not yet replaced by device display name).
150 */
151function ticketlst_is_objid(mixed $value): bool
152{
153    if ($value === null || $value === '') {
154        return false;
155    }
156
157    if (is_int($value)) {
158        return true;
159    }
160
161    if (!is_string($value)) {
162        return false;
163    }
164
165    $trimmed = trim($value);
166
167    return $trimmed !== '' && ctype_digit($trimmed);
168}
169
170/**
171 * Reads till/paidin display labels from TICKETDATA.report (string or legacy object shape).
172 *
173 * @return array{till: string|null, paidin: string|null}
174 */
175function ticketlst_labels_from_report(?array $report): array
176{
177    $labels = [
178        'till' => null,
179        'paidin' => null,
180    ];
181
182    if (!is_array($report)) {
183        return $labels;
184    }
185
186    if (array_key_exists('till', $report)) {
187        $labels['till'] = ticketlst_report_label($report['till']);
188    }
189
190    if (array_key_exists('paidin', $report)) {
191        $labels['paidin'] = ticketlst_report_label($report['paidin']);
192    }
193
194    return $labels;
195}
196
197/**
198 * Normalizes a report till/paidin entry to a display label.
199 * Returns null when absent; empty string when assigned but unnamed.
200 */
201function ticketlst_report_label(mixed $value): ?string
202{
203    if ($value === null) {
204        return null;
205    }
206
207    if (is_string($value)) {
208        return trim($value);
209    }
210
211    if (!is_array($value)) {
212        return null;
213    }
214
215    if (!array_key_exists('objid', $value) && !array_key_exists('name', $value)) {
216        return null;
217    }
218
219    return isset($value['name']) ? trim((string) $value['name']) : '';
220}
221
222/**
223 * Resolves payment session objid â†’ POS device display name.
224 *
225 * @param array<int|string> $sessionObjids
226 * @return array<string, string|null>
227 */
228function ticketlst_paidin_device_names(\ConxHelper $conx, array $sessionObjids): array
229{
230    $map = [];
231    $pending = array_values(array_unique(array_filter($sessionObjids)));
232    if (count($pending) === 0 || $conx->global === null) {
233        return $map;
234    }
235
236    $_objids = implode(',', array_map('intval', $pending));
237    $rows = [];
238    $sql = 'select SESSION.objid as session, POSDEVICE.name as name ';
239    $sql .= 'from SESSION left join POSDEVICE on SESSION.posdevice = POSDEVICE.objid ';
240    $sql .= 'where SESSION.objid in ('.$_objids.')';
241    $res = $conx->global->runsql(['SESSION', 'POSDEVICE'], $sql, $rows);
242    if ($res) {
243        foreach ($rows as $row) {
244            $session = (string) $row['session'];
245            $name = trim((string) ($row['name'] ?? ''));
246            $map[$session] = $name !== '' ? $name : null;
247        }
248    }
249
250    return $map;
251}
252
253/**
254 * Resolves till display labels from `tid` and paidin session FKs to device names.
255 *
256 * Pipeline: SQL sets `tid` (+ FK in `till` until labeled); report pass may set `till` text;
257 * this pass fills `till` from TILL.name when still unset, and resolves `paidin` labels.
258 *
259 * @param array<int, array<string, mixed>> $tickets
260 */
261function ticketlst_enrich_report_fields(\ConxHelper $conx, array &$tickets): void
262{
263    if (count($tickets) === 0) {
264        return;
265    }
266
267    $tillIds = [];
268    $paidinIds = [];
269    foreach ($tickets as $ticket) {
270        if (ticketlst_till_needs_label_resolution($ticket)) {
271            $tillIds[(string) $ticket['tid']] = true;
272        }
273        if (!empty($ticket['paidin']) && ticketlst_is_objid($ticket['paidin'])) {
274            $paidinIds[(string) $ticket['paidin']] = true;
275        }
276    }
277
278    $nameByTill = [];
279    if ($conx->tenant !== null && count($tillIds) > 0) {
280        $objids = implode(',', array_map('intval', array_keys($tillIds)));
281        $rows = [];
282        $res = $conx->tenant->runsql(['TILL'], 'select objid, name from TILL where objid in ('.$objids.')', $rows);
283        if ($res) {
284            foreach ($rows as $row) {
285                $name = trim((string) ($row['name'] ?? ''));
286                $nameByTill[(string) $row['objid']] = $name;
287            }
288        }
289    }
290
291    $nameBySession = ticketlst_paidin_device_names($conx, array_keys($paidinIds));
292
293    foreach ($tickets as &$ticket) {
294        if (ticketlst_till_needs_label_resolution($ticket)) {
295            $tid = (string) $ticket['tid'];
296            $ticket['till'] = $nameByTill[$tid] ?? '';
297        }
298
299        if (!empty($ticket['paidin']) && ticketlst_is_objid($ticket['paidin'])) {
300            $key = (string) $ticket['paidin'];
301            $resolved = $nameBySession[$key] ?? null;
302            $ticket['paidin'] = $resolved ?? '';
303        }
304    }
305    unset($ticket);
306}
307
308/**
309 * Action callable: get ticket list for place and date range.
310 *
311 * @param string $body Unused.
312 * @param array<string, mixed> $query Query params: device, session, place, dateini, dateend, timestamp.
313 * @param \ConxHelper $conx Connection helper.
314 * @param \Psr\Log\LoggerInterface $logger Logger.
315 * @param \UppServices\SessionService $sessionService Session service for check.
316 * @return array{output: string, contentType: string}
317 */
318return function (string $body, array $query, \ConxHelper $conx, \Psr\Log\LoggerInterface $logger, \UppServices\SessionService $sessionService): array {
319    $retval = \ApplicationError::Success;
320    $place = null;
321    $now = null;
322
323    $device = $query['device'] ?? null;
324    if ($device === null || $device === '') {
325        $logger->error("No device provided on URL. Operation cancelled.");
326        $retval = \ApplicationError::Parameters;
327    }
328
329    if (\success($retval)) {
330        $session = $query['session'] ?? null;
331        if ($session === null || $session === '') {
332            $logger->error("No session provided on URL. Operation cancelled.");
333            $retval = \ApplicationError::Parameters;
334        } else {
335            $unused = null;
336            $sessionObjid = null;
337            $retval = $sessionService->checkSession($conx, (string) $session, (string) $device, $unused, $sessionObjid);
338        }
339    }
340
341    if (\success($retval)) {
342        $place = $query['place'] ?? null;
343        if ($place === null || $place === '') {
344            $logger->error("Mandatory argument 'place' not provided in url parameters.");
345            $retval = \ApplicationError::Parameters;
346        }
347    }
348
349    if (\success($retval)) {
350        $conx->tenant = $place;
351    }
352
353    if (\success($retval) && $conx->tenant === null) {
354        $logger->error("Connection to place database failed.");
355        $retval = \ApplicationError::Database;
356    }
357
358    if (\success($retval)) {
359        $now = $conx->tenant->now(-60);
360    }
361
362    $ini = null;
363    $end = null;
364    $timestamp = null;
365    if (\success($retval)) {
366        $ini = $query['dateini'] ?? null;
367        if ($ini === null) {
368            $logger->error("Mandatory argument 'dateini' not provided in url parameters.");
369            $retval = \ApplicationError::Parameters;
370        } else {
371            $ini = date("Y-m-d H:i:s", (int) $ini);
372        }
373    }
374    if (\success($retval)) {
375        $end = $query['dateend'] ?? null;
376        if ($end === null) {
377            $logger->error("Mandatory argument 'dateend' not provided in url parameters.");
378            $retval = \ApplicationError::Parameters;
379        } else {
380            $end = date("Y-m-d H:i:s", (int) $end);
381        }
382    }
383    if (\success($retval)) {
384        $timestamp = $query['timestamp'] ?? null;
385        if ($timestamp === null) {
386            $logger->error("Mandatory argument 'timestamp' not provided in url parameters.");
387            $retval = \ApplicationError::Parameters;
388        }
389    }
390
391    $tickets = [];
392    if (\success($retval)) {
393        $tsDatetime = is_numeric($timestamp) ? date("Y-m-d H:i:s", (int) $timestamp) : (string) $timestamp;
394        $startRange = ($tsDatetime > $ini) ? $tsDatetime : $ini;
395        $retval = get_ticket_list_action($conx->tenant, $place, $startRange, $end, $tickets);
396    }
397
398    if (\success($retval) && $conx->global !== null && count($tickets) > 0) {
399        $sessionObjids = array_unique(array_filter(array_column($tickets, 'session')));
400        if (count($sessionObjids) > 0) {
401            $deviceMap = [];
402            $results = [];
403            $res = $conx->global->query('SESSION', ['objid' => $sessionObjids], $results);
404            if ($res) {
405                foreach ($results as $row) {
406                    $deviceMap[(string) $row['objid']] = $row['deviceid'] ?? null;
407                }
408            }
409            foreach ($tickets as &$ticket) {
410                $sessionId = (string) ($ticket['session'] ?? '');
411                $ticket['_device'] = $deviceMap[$sessionId] ?? null;
412            }
413            unset($ticket);
414        }
415    }
416
417    if (\success($retval)) {
418        foreach ($tickets as &$ticket) {
419            // Stage 1: capture till FK before report/enrich turn `till` into a display label.
420            ticketlst_set_tid($ticket, $ticket['till'] ?? null);
421
422            $emptyRegstatus = [
423                'system' => null,
424                'status' => null,
425                'id' => null,
426                'sent' => null,
427                'valid' => null,
428                'error' => null,
429            ];
430
431            $report = null;
432            if (!empty($ticket['_tdreport']) && is_string($ticket['_tdreport'])) {
433                $report = json_decode($ticket['_tdreport'], true);
434            }
435
436            $ticket['client'] = null;
437            $ticket['clientname'] = null;
438            $ticket['clienttaxid'] = null;
439            $ticket['discount'] = 0.0;
440            $ticket['hasdiscount'] = false;
441            $ticket['payments'] = json_encode([]);
442            $ticket['taxdetail'] = json_encode([]);
443            $ticket['paycash'] = 0.0;
444            $ticket['paycard'] = 0.0;
445            $ticket['payline'] = 0.0;
446            $ticket['payaccount'] = 0.0;
447            $ticket['source'] = '';
448            $ticket['addlines_amount'] = 0.0;
449            $ticket['addlines_income'] = 0.0;
450            $ticket['dellines_amount'] = 0.0;
451            $ticket['dellines_income'] = 0.0;
452            $ticket['items_completed_amount'] = 0.0;
453            $ticket['items_completed_income'] = 0.0;
454            $ticket['items_cancelled_amount'] = 0.0;
455            $ticket['items_cancelled_income'] = 0.0;
456            $ticket['items_onthehouse_amount'] = 0.0;
457            $ticket['items_onthehouse_income'] = 0.0;
458
459            $hasValidReport = is_array($report);
460            if ($hasValidReport) {
461                $reportSource = strtoupper(trim((string)($report['source'] ?? '')));
462                if ($reportSource !== '') {
463                    $ticket['source'] = $reportSource;
464                }
465
466                $clientInfo = (isset($report['client']) && is_array($report['client'])) ? $report['client'] : null;
467                if (is_array($clientInfo)) {
468                    if (array_key_exists('objid', $clientInfo)) {
469                        $ticket['client'] = $clientInfo['objid'];
470                    }
471                    if (array_key_exists('name', $clientInfo)) {
472                        $ticket['clientname'] = $clientInfo['name'] ?? null;
473                    }
474                    if (array_key_exists('taxid', $clientInfo)) {
475                        $ticket['clienttaxid'] = $clientInfo['taxid'] ?? null;
476                    }
477                }
478
479                if (array_key_exists('discount', $report)) {
480                    $ticket['discount'] = (float) ($report['discount'] ?? 0);
481                }
482
483                $ticket['hasdiscount'] = ((float) $ticket['discount'] > 0.0);
484
485                $payments = (isset($report['payments']) && is_array($report['payments'])) ? $report['payments'] : null;
486                if ($payments !== null) {
487                    $ticket['payments'] = json_encode($payments);
488                    $ticket['paycash'] = isset($payments['PAYCASH']) ? (float) $payments['PAYCASH'] : 0.0;
489                    $ticket['paycard'] = isset($payments['PAYCARD']) ? (float) $payments['PAYCARD'] : 0.0;
490                    $ticket['payline'] = isset($payments['PAYLINE']) ? (float) $payments['PAYLINE'] : 0.0;
491                    $ticket['payaccount'] = isset($payments['PAYACCOUNT']) ? (float) $payments['PAYACCOUNT'] : 0.0;
492                }
493
494                $taxdetail = array_key_exists('taxdetail', $report) ? $report['taxdetail'] : null;
495                if ($taxdetail !== null) {
496                    if (is_array($taxdetail)) {
497                        $taxdetail = normalize_ticket_taxdetail_amounts($taxdetail, $ticket['taxes'] ?? null);
498                    }
499                    $ticket['taxdetail'] = json_encode($taxdetail);
500                }
501
502                if (array_key_exists('till', $report) && is_array($report['till']) && array_key_exists('objid', $report['till'])) {
503                    ticketlst_set_tid($ticket, $report['till']['objid']);
504                }
505
506                // Stage 2: report JSON may supply till/paidin display labels.
507                $reportLabels = ticketlst_labels_from_report($report);
508                if ($reportLabels['till'] !== null) {
509                    $ticket['till'] = $reportLabels['till'];
510                }
511                if ($reportLabels['paidin'] !== null) {
512                    $ticket['paidin'] = $reportLabels['paidin'];
513                }
514
515                $activity = (isset($report['activity']) && is_array($report['activity'])) ? $report['activity'] : null;
516                if ($activity !== null) {
517                    $ticket['addlines_amount'] = (float) ($activity['addlines']['amount'] ?? 0);
518                    $ticket['addlines_income'] = (float) ($activity['addlines']['income'] ?? 0);
519                    $ticket['dellines_amount'] = (float) ($activity['dellines']['amount'] ?? 0);
520                    $ticket['dellines_income'] = (float) ($activity['dellines']['income'] ?? 0);
521                }
522
523                $items = (isset($report['items']) && is_array($report['items'])) ? $report['items'] : null;
524                if ($items !== null) {
525                    $ticket['items_completed_amount'] = (float) ($items['completed']['amount'] ?? 0);
526                    $ticket['items_completed_income'] = (float) ($items['completed']['income'] ?? 0);
527                    $ticket['items_cancelled_amount'] = (float) ($items['cancelled']['amount'] ?? 0);
528                    $ticket['items_cancelled_income'] = (float) ($items['cancelled']['income'] ?? 0);
529                    $ticket['items_onthehouse_amount'] = (float) ($items['onthehouse']['amount'] ?? 0);
530                    $ticket['items_onthehouse_income'] = (float) ($items['onthehouse']['income'] ?? 0);
531                }
532            }
533
534            if (!empty($ticket['_ivcreated'])) {
535                $ticket['ivcreated'] = $ticket['_ivcreated'];
536            }
537
538            $regstatus = null;
539            if (!empty($ticket['_tdregstatus']) && is_string($ticket['_tdregstatus'])) {
540                $regstatus = json_decode($ticket['_tdregstatus'], true);
541            }
542
543            if (!is_array($regstatus)) {
544                $regstatus = $emptyRegstatus;
545            }
546
547            $fiscalValid = (is_array($regstatus) && !empty($regstatus['valid']));
548            $ticket['sentto'] = $fiscalValid ? '1' : '0';
549            if (!empty($ticket['_device'])) {
550                $ticket['device'] = $ticket['_device'];
551            }
552
553            if (!empty($ticket['_intill'])) {
554                $ticket['intill'] = $ticket['_intill'];
555            }
556
557            $ticket['regstatus'] = json_encode($regstatus);
558
559            $ticket['fiscalsystem'] = is_array($regstatus) ? ($regstatus['system'] ?? null) : null;
560            $ticket['fiscalstatus'] = is_array($regstatus) ? ($regstatus['status'] ?? null) : null;
561            $ticket['tbaiid'] = is_array($regstatus) ? ($regstatus['id'] ?? null) : null;
562            $ticket['tbaisent'] = is_array($regstatus) ? ($regstatus['sent'] ?? null) : null;
563            $ticket['tbaivalid'] = is_array($regstatus) ? ($regstatus['valid'] ?? null) : null;
564            $ticket['tbaierror'] = is_array($regstatus) ? ($regstatus['error'] ?? null) : null;
565        } unset($ticket);
566    }
567
568    if (\success($retval) && count($tickets) > 0) {
569        // Stage 3: fill till from TILL.name when report did not label; resolve paidin device names.
570        ticketlst_enrich_report_fields($conx, $tickets);
571    }
572
573    $result = [
574        "errorcode" => $retval,
575        "tickets" => \success($retval) ? $tickets : null,
576        "timestamp" => \success($retval) ? $now : null,
577    ];
578
579    return ['output' => json_encode($result), 'contentType' => 'application/json; charset=utf-8'];
580};