Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
83.45% covered (success)
83.45%
116 / 139
0.00% covered (danger)
0.00%
0 / 1
CRAP
n/a
0 / 0
ticketbai_audit_retry_run
83.09% covered (success)
83.09%
113 / 136
0.00% covered (danger)
0.00%
0 / 1
51.94
1<?php
2
3declare(strict_types=1);
4
5require_once __DIR__.'/../legacy/model/spec/ticketbai.php';
6require_once __DIR__.'/../legacy/model/spec/audit.php';
7
8/**
9 * Retry pending/failed TicketBAI rows for one closed audit period.
10 *
11 * Period bounds come from {@see _audit_period_bounds} (same till-scoped window as
12 * AUDIT.report recalculation). Only TICKETBAI rows with `valid is null` are resent
13 * via {@see update_ticketbai_request}. After at least one send attempt, AUDIT.report
14 * is refreshed and published so clients sync the new fiscal counters.
15 *
16 * @param array<string, mixed> $query Query params (device, session, place, audit).
17 * @param \ConxHelper $conx Connection helper.
18 * @param \Psr\Log\LoggerInterface $logger Logger.
19 * @param \UppServices\SessionService $sessionService Session service.
20 * @return array{errorcode: int, ticketsok: list<array{0: string, 1: mixed}>, ticketsko: list<array{0: string, 1: mixed}>}
21 */
22function ticketbai_audit_retry_run(
23    array $query,
24    \ConxHelper $conx,
25    \Psr\Log\LoggerInterface $logger,
26    \UppServices\SessionService $sessionService,
27): array {
28    $retval = \ApplicationError::Success;
29    $ticketsok = [];
30    $ticketsko = [];
31    $userid = null;
32    $sessionObjid = null;
33    /** @var array<string, mixed> $auditRow */
34    $auditRow = [];
35    $pending = [];
36    $attempted = 0;
37
38    $device = isset($query['device']) ? (string) $query['device'] : '';
39    $sessionToken = isset($query['session']) ? (string) $query['session'] : '';
40    $placeId = isset($query['place']) ? (string) $query['place'] : '';
41    $auditId = isset($query['audit']) ? (string) $query['audit'] : '';
42
43    // validate mandatory query parameters
44    if (success($retval)) {
45        if ($device === '') {
46            $logger->error('No device provided on URL. Operation cancelled.');
47            $retval = \ApplicationError::Parameters;
48        }
49    }
50
51    if (success($retval)) {
52        if ($sessionToken === '') {
53            $logger->error('No session provided on URL. Operation cancelled.');
54            $retval = \ApplicationError::Parameters;
55        }
56    }
57
58    if (success($retval)) {
59        if ($placeId === '' || $placeId === '0') {
60            $logger->error("Mandatory parameter 'place' not provided. Operation cancelled.");
61            $retval = \ApplicationError::Parameters;
62        }
63    }
64
65    if (success($retval)) {
66        if ($auditId === '' || $auditId === '0') {
67            $logger->error("Mandatory parameter 'audit' not provided. Operation cancelled.");
68            $retval = \ApplicationError::Parameters;
69        }
70    }
71
72    // validate session
73    if (success($retval)) {
74        $retval = $sessionService->checkSession($conx, $sessionToken, $device, $userid, $sessionObjid);
75        if (!success($retval)) {
76            $logger->error('Session check failed for ticketbai/audit.');
77        }
78    }
79
80    // open tenant for the place
81    if (success($retval)) {
82        $conx->tenant = $placeId;
83        $conn = $conx->getTenant();
84        if ($conn === null) {
85            $logger->error('Tenant connection not available for audit TicketBAI retry.');
86            $retval = \ApplicationError::Database;
87        }
88    }
89
90    // load the closed audit row
91    if (success($retval)) {
92        $results = [];
93        $res = $conx->tenant->query('AUDIT', [ 'objid' => $auditId ], $results);
94        if ($res) {
95            if (count($results) > 0) {
96                $auditRow = $results[0];
97            }
98            else {
99                $logger->error("No audit entry found with objid '".$auditId."'");
100                $retval = \ApplicationError::NotFound;
101            }
102        }
103        else {
104            $retval = \ApplicationError::Database;
105        }
106    }
107
108    // resolve till-scoped period bounds (same source as AUDIT.report)
109    $iniDate = null;
110    $endDate = null;
111    if (success($retval)) {
112        $bounds = \_audit_period_bounds($conx->tenant, $auditRow);
113        if ($bounds === null) {
114            $logger->error("Audit [".$auditId."] has no till; cannot resolve TicketBAI retry period.");
115            $retval = \ApplicationError::Invalid;
116        }
117        else {
118            list($iniDate, $endDate) = $bounds;
119            \addlog(__FILE__, \LogLevel::Information, 'TicketBAI audit retry period ['.$iniDate.' - '.$endDate.']');
120        }
121    }
122
123    // select pending/failed TICKETBAI rows in the audit period
124    if (success($retval)) {
125        $tillObjid = \_audit_till_objid($auditRow);
126        $sql = "select TICKETCHANGE.series, TICKETCHANGE.invoice, TICKETBAI.* ";
127        $sql .= "from TICKETCHANGE ";
128        $sql .= "inner join TICKET on TICKETCHANGE.ticket = TICKET.objid ";
129        $sql .= "inner join TICKETBAI on TICKETBAI.tckchg = TICKETCHANGE.objid ";
130        $sql .= "and TICKETBAI.valid is null ";
131        $sql .= "where TICKET.till = ".$tillObjid." ";
132        $sql .= "and (";
133        $sql .= "(TICKET.status not in ('DE', 'CC') and TICKET.paidon is not NULL and ";
134        $sql .= "TICKET.paidon > '".$iniDate."' and TICKET.paidon <= '".$endDate."') or ";
135        $sql .= "(TICKET.status in ('DE', 'CC') and ";
136        $sql .= "TICKET.updated > '".$iniDate."' and TICKET.updated <= '".$endDate."')";
137        $sql .= ") ";
138        $sql .= "order by TICKETCHANGE.objid desc";
139
140        \addlog(__FILE__, \LogLevel::Debug, 'TicketBAI audit retry query: '.PHP_EOL.$sql.PHP_EOL);
141
142        $pending = [];
143        $res = $conx->tenant->runsql([ 'TICKET', 'TICKETCHANGE', 'TICKETBAI' ], $sql, $pending);
144        if (!$res) {
145            $retval = \ApplicationError::Database;
146        }
147    }
148
149    // resend each pending TicketBAI entry
150    if (success($retval)) {
151        $indexed = [];
152        $index = 1;
153        $total = count($pending);
154
155        foreach ($pending as &$ticketbai) {
156            \addlog(
157                __FILE__,
158                \LogLevel::Information,
159                "Retrying TicketBAI XML file: '".($ticketbai['xml'] ?? '')."' (".$index.' of '.$total.')'
160            );
161            $index++;
162            $attempted++;
163
164            $ticketinvoice = (string) ($ticketbai['series'] ?? '').(string) ($ticketbai['invoice'] ?? '');
165            if (!array_key_exists($ticketinvoice, $indexed)) {
166                $indexed[$ticketinvoice] = null;
167            }
168
169            if (!empty($ticketbai['xml'])) {
170                $errormsg = null;
171                $result = \update_ticketbai_request($conx, $sessionObjid, $ticketbai, $errormsg);
172                if (!success($result)) {
173                    $indexed[$ticketinvoice] = "Could not send ticketbai XML file [errorcode: '".$result."']";
174                    \addlog(__FILE__, \LogLevel::Warning, $indexed[$ticketinvoice]);
175                    continue;
176                }
177
178                if ($errormsg !== null) {
179                    $indexed[$ticketinvoice] = $errormsg;
180                    \addlog(__FILE__, \LogLevel::Warning, $indexed[$ticketinvoice]);
181                    continue;
182                }
183            }
184            else {
185                $indexed[$ticketinvoice] = 'No XML file found for ticketbai instance ['.($ticketbai['objid'] ?? '').']';
186                \addlog(__FILE__, \LogLevel::Warning, $indexed[$ticketinvoice]);
187                continue;
188            }
189        }
190        unset($ticketbai);
191
192        foreach ($indexed as $ivce => $error) {
193            if ($error === null) {
194                $ticketsok[] = [ $ivce, $error ];
195            }
196            else {
197                $ticketsko[] = [ $ivce, $error ];
198            }
199        }
200
201        \addlog(__FILE__, \LogLevel::Information, 'OK tickets: '.PHP_EOL.print_r($ticketsok, true).PHP_EOL);
202        \addlog(__FILE__, \LogLevel::Information, 'KO tickets: '.PHP_EOL.print_r($ticketsko, true).PHP_EOL);
203    }
204
205    // refresh AUDIT.report after send attempts so fiscal counters sync to clients
206    if (success($retval) && ($attempted > 0)) {
207        $delayed = new \SyncDelayed();
208        $updates = [];
209        $ok = $conx->transactional(function (\ConxHelper $conx) use ($sessionObjid, &$auditRow, $delayed, $placeId, &$retval, &$updates) {
210            $retval = \update_audit_info($conx, $sessionObjid, $auditRow, $delayed);
211            if (success($retval)) {
212                foreach ($delayed->touched() as $change) {
213                    $retval = \push_change($conx, $change, $updates);
214                    if (!success($retval)) {
215                        break;
216                    }
217                }
218            }
219            
220            if (success($retval)) {
221                $retval = \stage_changes($conx, $placeId, $updates);
222            }
223            
224            return success($retval);
225        });
226        
227        if (success($retval) && !$ok) {
228            $retval = \ApplicationError::Database;
229            $logger->error('AUDIT refresh commit failed at COMMIT time after TicketBAI retry.');
230        }
231        
232        if (success($retval)) {
233            \notify_changes($conx, $sessionObjid, $updates);
234        }
235    }
236
237    return [
238        'errorcode' => $retval,
239        'ticketsok' => $ticketsok,
240        'ticketsko' => $ticketsko,
241    ];
242}
243
244/**
245 * Action callable for TicketbaiService::audit (same signature as authorize/derived actions).
246 *
247 * @return callable(array, \ConxHelper, \Psr\Log\LoggerInterface, \UppServices\SessionService): array
248 */
249return \Closure::fromCallable('ticketbai_audit_retry_run');