Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
78.97% covered (warning)
78.97%
154 / 195
62.50% covered (warning)
62.50%
5 / 8
CRAP
n/a
0 / 0
_duplicated_row_ticketlog
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
_log_action_ticketlog
93.24% covered (success)
93.24%
69 / 74
0.00% covered (danger)
0.00%
0 / 1
33.34
_log_hshchk_ticketlog
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
_ticketlog_strip_section_header
85.71% covered (success)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
5.07
_ticketlog_row_header
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
3.01
_ticketlog_output_header
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
_ticketlog_normalize_row
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
parse_line_ticketlog
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
8
1<?php
2
3/**
4 * @file
5 * Place action: ticket activity log.
6 */
7
8declare(strict_types=1);
9
10require_once __DIR__.'/../../Legacy/common/session.php';
11require_once __DIR__.'/../regstr/registry.php';
12
13/**
14 * @param array<string, mixed> $lastlog
15 * @param array<string, mixed> $currlog
16 */
17function _duplicated_row_ticketlog(array $lastlog, array $currlog): bool
18{
19    $skip = ['date', 'version', 'event', 'objid', 'hash', 'prev'];
20    foreach ($currlog as $field => $value) {
21        $previous = array_key_exists($field, $lastlog) ? $lastlog[$field] : null;
22        if (!in_array($field, $skip) && $previous != $value) {
23            return false;
24        }
25    }
26    return true;
27}
28
29/**
30 * @param array<string, mixed> $newentry
31 * @param array<string, mixed> $ticketlog
32 */
33function _log_action_ticketlog(array &$newentry, array &$ticketlog): ?string
34{
35    $action = null;
36    switch ($newentry['event'] ?? null) {
37        case 'e1': // Drawer
38            return 'DRAWER';
39        case 'e2': // TicketSII
40            return 'TICKETSII';
41        case 'e3': // TicketBAI
42            return 'TICKETBAI';
43        case 'e4': // VERIFACTU
44            return 'VERIFACTU';
45        case 'e5': // TicketPrint
46            return 'RECEIPT';
47        case 'l1': // UsrLogin
48            return 'USRLOGIN';
49        case 'l2': // UsrLogout
50            return 'USRLOGOUT';
51        case 'l3': // GstLogin
52            return 'GSTLOGIN';
53        case 'l4': // GstLogout
54            return 'GSTLOGOUT';
55    }
56
57    $_new_status = $newentry['status'] ?? null;
58    $_new_price = $newentry['price'] ?? null;
59    $_new_payment = $newentry['payment'] ?? null;
60    $_new_invoice = $newentry['invceno'] ?? null;
61
62    // Diffs overwrite in order: payment â†’ invoice â†’ price â†’ status.
63    // Status last so a pay (→PD) yields PAYMENT even if payment also changed.
64    if (count($ticketlog) == 0) {
65        switch ($_new_status) {
66            case 'DE':
67            case 'CC':
68                $action = 'CANCELLED';
69                break;
70            case 'PD':
71                $action = 'COMPLETED';
72                break;
73            default:
74                $action = 'CREATED';
75                break;
76        }
77    } else {
78        $lastlog = end($ticketlog);
79        $_old_status = $lastlog['status'] ?? null;
80        $_old_price = $lastlog['price'] ?? null;
81        $_old_payment = $lastlog['payment'] ?? null;
82        $_old_invoice = $lastlog['invceno'] ?? null;
83
84        if (($_old_payment != $_new_payment) && ($_new_payment !== '')) {
85            $action = $_new_payment;
86        }
87        if ($_old_invoice != $_new_invoice) {
88            $action = 'INVOICE';
89        }
90        if ($_old_price != $_new_price) {
91            $action = ($_new_payment != '') ? 'RETURN' : 'MODIFIED';
92        }
93        if ($_old_status != $_new_status) {
94            switch ($_new_status) {
95                case 'AC':
96                    switch ($_old_status) {
97                        case 'PR':
98                            $action = 'ACTIVE';
99                            break;
100                        case 'RD':
101                            $action = 'MODIFIED';
102                            break;
103                        default:
104                            $action = 'REOPEN';
105                            break;
106                    }
107                    break;
108                case 'RD':
109                    $action = 'READY';
110                    break;
111                case 'PP':
112                    $action = ($_new_payment == 'PAYACCOUNT') ? 'TOACC' : 'TOPAY';
113                    break;
114                case 'PD':
115                    $action = 'PAYMENT';
116                    break;
117                case 'CC':
118                    $action = ($_new_payment != '') ? 'REFUND' : 'CANCELLED';
119                    break;
120                case 'DE':
121                    $action = 'CANCELLED';
122                    break;
123            }
124        }
125        if ($_new_status == 'DE') {
126            $action = 'CANCELLED';
127        }
128    }
129
130    return $action;
131}
132
133/**
134 * @param array<string, mixed> $newentry
135 * @param array<string> $header
136 */
137function _log_hshchk_ticketlog(array &$newentry, array &$header, string $prvhash): bool
138{
139    $rowline = '';
140    foreach ($header as $field) {
141        if (in_array($field, ['hash', 'prev', 'action', 'hshchk'])) {
142            continue;
143        }
144        $rowline .= ($newentry[$field] ?? '').';';
145    }
146    return (md5($rowline) == ($newentry['hash'] ?? '')) && ($prvhash == ($newentry['prev'] ?? ''));
147}
148
149/**
150 * Strip computed/output-only columns from a section header read from regstr.
151 *
152 * @param array<string> $sectionHeader
153 * @return array<string>
154 */
155function _ticketlog_strip_section_header(array $sectionHeader): array
156{
157    $fields = [];
158    foreach ($sectionHeader as $field) {
159        if (!in_array($field, ['action', 'hshchk'], true)) {
160            $fields[] = $field;
161        }
162    }
163
164    while (count($fields) > 0 && end($fields) === '') {
165        array_pop($fields);
166    }
167
168    return $fields;
169}
170
171/**
172 * Build the column header for one regstr row from its embedded log version.
173 *
174 * Prefer the section header written in the CSV when its width matches the row:
175 * legacy rows are stored in column order for that header. When widths differ
176 * (mixed-version sections), fall back to version + field-count inference.
177 *
178 * @return array<string>
179 */
180function _ticketlog_row_header(string $version, array $fallbackHeader, int $fieldCount): array
181{
182    $sectionHeader = _ticketlog_strip_section_header($fallbackHeader);
183    if (count($sectionHeader) === $fieldCount) {
184        return $sectionHeader;
185    }
186
187    if (preg_match('/^v\d+$/', $version)) {
188        return array_merge(
189            ['date', 'version', 'event'],
190            _get_log_fields_for_line($fieldCount),
191            ['hash', 'prev']
192        );
193    }
194
195    return $fallbackHeader;
196}
197
198/**
199 * Canonical output header for ticketlog responses (current regstr schema).
200 *
201 * @return array<string>
202 */
203function _ticketlog_output_header(): array
204{
205    return array_merge(
206        ['date', 'version', 'event'],
207        _get_log_fields(),
208        ['hash', 'prev', 'action', 'hshchk']
209    );
210}
211
212/**
213 * Map one parsed row (any source schema) to the canonical v8 output shape.
214 *
215 * Missing columns in older versions are emitted as empty strings so clients
216 * always receive a single predictable header.
217 *
218 * @param array<string, mixed> $registry
219 * @return array<string, string>
220 */
221function _ticketlog_normalize_row(array $registry): array
222{
223    $normalized = [];
224    foreach (_ticketlog_output_header() as $field) {
225        $normalized[$field] = (string) ($registry[$field] ?? '');
226    }
227
228    return $normalized;
229}
230
231/**
232 * @param array<string> $header
233 * @param array<string> $fields
234 * @param array<int, array> $itmindex
235 */
236function parse_line_ticketlog(array &$header, array &$fields, array &$itmindex, string &$prvhash): array|false
237{
238    $lineHeader = _ticketlog_row_header((string) ($fields[1] ?? ''), $header, count($fields));
239    $result = [];
240    for ($i = 0; $i < count($lineHeader); $i++) {
241        if (!in_array($lineHeader[$i], ['action', 'hshchk'])) {
242            $result[$lineHeader[$i]] = $fields[$i] ?? '';
243        }
244    }
245    if (array_key_exists('objid', $result) && $result['objid']) {
246        if (!array_key_exists($result['objid'], $itmindex)) {
247            $itmindex[$result['objid']] = [];
248        }
249        $ticketlog = &$itmindex[$result['objid']];
250    } else {
251        $ticketlog = [];
252    }
253    $result['hshchk'] = _log_hshchk_ticketlog($result, $lineHeader, $prvhash);
254    $result['action'] = _log_action_ticketlog($result, $ticketlog);
255    $isDuplicate = (count($ticketlog) > 0) && _duplicated_row_ticketlog(end($ticketlog), $result);
256    if (!$isDuplicate) {
257        $ticketlog[] = $result;
258    }
259
260    return $result;
261}
262
263/**
264 * Action callable: get ticket activity log for place.
265 *
266 * @param string $body Unused.
267 * @param array<string, mixed> $query Query params: device, session, place, ini, end.
268 * @param \ConxHelper $conx Connection helper.
269 * @param \Psr\Log\LoggerInterface $logger Logger.
270 * @param \UppServices\SessionService $sessionService Session service for check.
271 * @return array{output: string, contentType: string}
272 */
273return function (string $body, array $query, \ConxHelper $conx, \Psr\Log\LoggerInterface $logger, \UppServices\SessionService $sessionService): array {
274    $retval = \ApplicationError::Success;
275    $place = null;
276    $header = [];
277    $reglog = [];
278
279    $device = $query['device'] ?? null;
280    if (!$device) {
281        $logger->error("No device provided on URL. Operation cancelled.");
282        $retval = \ApplicationError::Parameters;
283    }
284
285    if (\success($retval)) {
286        $session = $query['session'] ?? null;
287        if (!$session) {
288            $logger->error("No session provided on URL. Operation cancelled.");
289            $retval = \ApplicationError::Parameters;
290        } else {
291            $userid = null;
292            $retval = $sessionService->checkSession($conx, (string) $session, (string) $device, $userid);
293        }
294    }
295
296    if (\success($retval)) {
297        $place = $query['place'] ?? null;
298        if (!$place) {
299            $logger->error("Mandatory parameter 'place' not provided on URL. Operation cancelled.");
300            $retval = \ApplicationError::Parameters;
301        }
302    }
303
304    $dateini = $query['ini'] ?? null;
305    $dateend = $query['end'] ?? null;
306
307    if (\success($retval)) {
308        $dstfile = sprintf('%s/%05d_ticket_activity.csv', \AppConstants::RegLogPath(), $place);
309        if (!file_exists($dstfile)) {
310            $logger->warning("No registry found for place. file '".$dstfile."'.");
311            $retval = \ApplicationError::NotFound;
312        } else {
313            $rawlog = file_get_contents($dstfile);
314            if (!$rawlog) {
315                $logger->error("Could not read contents for file '".$dstfile."'.");
316                $retval = \ApplicationError::Generic;
317            } else {
318                $itmindex = [];
319                $logitems = [];
320                $inidate = strtotime((string) $dateini);
321                $enddate = strtotime((string) $dateend);
322                $prvhash = '00000000000000000000000000000000';
323                $lines = explode(PHP_EOL, $rawlog, -1);
324                $hdrfl = true;
325                for ($i = 0; $i < count($lines); $i++) {
326                    $fields = explode(';', $lines[$i], -1);
327                    if (empty($lines[$i])) {
328                        $hdrfl = true;
329                        continue;
330                    }
331                    if ($hdrfl) {
332                        $hdrfl = false;
333                        $header = array_merge($fields, ['action', 'hshchk']);
334                        continue;
335                    }
336                    $logtime = strtotime($fields[0]);
337                    if ($logtime >= $inidate && $logtime < $enddate) {
338                        $logitem = parse_line_ticketlog($header, $fields, $itmindex, $prvhash);
339                        if ($logitem !== false) {
340                            $logitems[] = $logitem;
341                        }
342                    }
343                    $prvhash = $fields[count($fields) - 2] ?? '';
344                }
345                $header = _ticketlog_output_header();
346                foreach ($logitems as &$registry) {
347                    $reglog[] = array_values(_ticketlog_normalize_row($registry));
348                }
349                unset($registry);
350            }
351        }
352    }
353
354    $result = [
355        "errorcode" => $retval,
356        "header" => \success($retval) ? $header : [],
357        "reglog" => \success($retval) ? $reglog : [],
358    ];
359    return ['output' => json_encode($result), 'contentType' => 'application/json; charset=utf-8'];
360};