CashsessionAPI.php 17.8 KB
Newer Older
Karamel's avatar
Karamel committed
1
<?php
2
//    Pastèque API
Karamel's avatar
Karamel committed
3
//
Philippe Pary's avatar
Philippe Pary committed
4
5
6
//    Copyright (C) 
//			2012 Scil (http://scil.coop)
//			2017 Karamel, Association Pastèque (karamel@creativekara.fr, https://pasteque.org)
Karamel's avatar
Karamel committed
7
//
8
//    This file is part of Pastèque.
Karamel's avatar
Karamel committed
9
//
10
//    Pastèque is free software: you can redistribute it and/or modify
Karamel's avatar
Karamel committed
11
12
13
14
//    it under the terms of the GNU General Public License as published by
//    the Free Software Foundation, either version 3 of the License, or
//    (at your option) any later version.
//
15
//    Pastèque is distributed in the hope that it will be useful,
Karamel's avatar
Karamel committed
16
17
18
19
20
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//    GNU General Public License for more details.
//
//    You should have received a copy of the GNU General Public License
21
//    along with Pastèque.  If not, see <http://www.gnu.org/licenses/>.
Karamel's avatar
Karamel committed
22
23
24
25
26

namespace Pasteque\Server\API;

use \Pasteque\Server\Model\CashRegister;
use \Pasteque\Server\Model\CashSession;
27
use \Pasteque\Server\Model\FiscalTicket;
28
29
use \Pasteque\Server\Model\GenericModel;
use \Pasteque\Server\Model\Ticket;
Karamel's avatar
Karamel committed
30
31
32
33
34
35
36
37
use \Pasteque\Server\System\DAO\DAOCondition;

/** CRUD API for Role. */
class CashsessionAPI extends APIHelper implements API
{
    const MODEL_NAME = 'Pasteque\Server\Model\CashSession';

    /** Get an existing cash session or get the last one.
38
     * @param $id Session Id: [<cash register id>, <sequence>].
39
     * If sequence is omitted, will get the last one.
Karamel's avatar
Karamel committed
40
41
42
     * @return The requested session, created if there aren't any yet. */
    public function get($id) {
        if (!is_array($id)) { $id = array('cashRegister' => $id, 'sequence' =>null); }
43
44
45
46
47
48
        // Look for the cash register
        $cashRegister = $this->dao->read(CashRegister::class, $id['cashRegister']);
        if ($cashRegister == null) {
            return null;
        }
        // Look for session
Karamel's avatar
Karamel committed
49
50
51
        if ($id['sequence'] === null) {
            // Look for the latest session for the requested cash register
            $sessions = $this->dao->search(static::MODEL_NAME,
52
                    [new DAOCondition('cashRegister', '=', $cashRegister)],
53
                    1, 0, '-sequence');
Karamel's avatar
Karamel committed
54
55
56
57
58
59
60
61
62
63
64
65
            if (count($sessions) > 0) {
                return $sessions[0];
            } else {
                // No session for this cash register yet, create it.
                $session = new CashSession();
                $session->setCashRegister($cashRegister);
                $session->setSequence(1);
                $this->dao->write($session);
                $this->dao->commit();
                return $session;
            }
        } else {
66
67
68
69
70
71
72
73
            $search = $this->dao->search(static::MODEL_NAME,
                    [new DAOCondition('cashRegister', '=', $cashRegister),
                     new DAOCondition('sequence', '=', $id['sequence'])],
                    1);
            if (count($search) > 0) {
                return $search[0];
            }
            return null;
Karamel's avatar
Karamel committed
74
75
76
        }
    }

77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
    /** Write a FiscalTicket about the failed zticket, so that there is a trace
     * and the ticket can be purged client-side. It does commit.
     * @param $session The faulty session.
     * @param $reason The technical fault, added in the FiscalTicket. */
    private function registerFailure($session, $reason) {
        $sequence = FiscalTicket::getFailureZTicketSequence($session);
        $number = 1; /* Number is dissociated from session number, because the
                      * same number can be rejected multiple times. */
        // Look for the last failure in sequence to get the number.
        $previousFTicket = null;
        $prevSearch = $this->dao->search(FiscalTicket::class,
                [new DAOCondition('type', '=', FiscalTicket::TYPE_ZTICKET),
                 new DAOCondition('sequence', '=', $sequence)],
                1, null, '-number');
        if (count($prevSearch) > 0) {
            $previousFTicket = $prevSearch[0];
            if ($previousFTicket->getNumber() === 0) {
                $previousFTicket = null; // Ignore EOS
            } else {
                $number = $previousFTicket->getNumber() + 1;
            }
        }
        // Create the failure fiscal ticket.
        $fiscalTicket = new FiscalTicket();
        $fiscalTicket->setType(FiscalTicket::TYPE_ZTICKET);
        $fiscalTicket->setSequence($sequence);
        $fiscalTicket->setNumber($number);
        $fiscalTicket->setDate(new \DateTime());
        $stone = $session->toStone();
        $stone['failure'] = $reason;
        $fiscalTicket->setContent(json_encode($stone));
        // Sign
        $fiscalTicket->sign($previousFTicket);
        // Write
        $this->dao->write($fiscalTicket);
        $this->updateEOSTicket($fiscalTicket);
        $this->dao->commit();
    }

Karamel's avatar
Karamel committed
116
117
118
119
120
    /** Write session data. Can be called only once all the tickets are stored.
     * To write all data at once (open, tickets and close), a call to write must
     * be done first with the opened session. Then register all the tickets and
     * finally call write with the full cash session data.
     * When closing a session, the next one is automatically created.
Karamel's avatar
Karamel committed
121
     * @return The Id(s)
Karamel's avatar
Karamel committed
122
123
124
125
126
127
     * @throw \BadMethodCallException When trying to close a session without
     * writing it as opened before, or when trying to update an already closed
     * session. */
    public function write($data) {
        $this->supportOrDie($data);
        $data = (is_array($data)) ? $data : array($data);
128
        $previousFTicket = null;
Karamel's avatar
Karamel committed
129
        foreach ($data as $d) {
130
131
132
            // Defensive checks
            // Reserved number 0
            if ($d->getSequence() === 0) {
133
134
135
                $rejectReason = 'Session number 0 is reserved.';
                $this->registerFailure($d, $rejectReason);
                throw new \BadMethodCallException($rejectReason);
136
137
138
            }
            $currentData = $this->dao->readSnapshot(static::MODEL_NAME, $d->getId());
            // Overwriting a closed session
Karamel's avatar
Karamel committed
139
            if ($currentData !== null && $currentData->isClosed()) {
140
141
142
                $rejectReason = 'Closed sessions are read-only.';
                $this->registerFailure($d, $rejectReason);
                throw new \BadMethodCallException($rejectReason);
Karamel's avatar
Karamel committed
143
144
145
146
            }
            if ($currentData === null && $d->isClosed()) {
                // Trying to register everything at once, this will prevent
                // registering the tickets. Throw an error.
147
148
149
                $rejectReason = 'Trying to close a session that was not registered as open.';
                $this->registerFailure($d, $rejectReason);
                throw new \BadMethodCallException($rejectReason);
Karamel's avatar
Karamel committed
150
151
            }
            if ($d->isClosed()) {
152
153
154
155
156
                // Create associated fiscal ticket
                $fiscalTicket = new FiscalTicket();
                $fiscalTicket->setType(FiscalTicket::TYPE_ZTICKET);
                $fiscalTicket->setSequence(FiscalTicket::getZTicketSequence($d));
                $fiscalTicket->setNumber($d->getSequence());
157
                $fiscalTicket->setDate($d->getCloseDate());
158
                $fiscalTicket->setContent(json_encode($d->toStone()));
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
                // Check continuity and integrity
                if ($d->getSequence() == 1 && $previousFTicket !== null) {
                    // No.1 with a previous ticket
                    // TODO: warn messing up things
                    $previousFTicket = null;
                } elseif ($previousFTicket !== null) {
                    // Check number continuity from previous to this one
                    if ($previousFTicket->getNumber() != $d->getSequence() - 1) {
                        // TODO: warn ticket discontinuity
                    }
                } else { // $previousTicket is null and it's not No.1
                    // First registered ticket, get the previous one
                    $previousFTicket = $this->dao->read(FiscalTicket::class,
                            ['type' => FiscalTicket::TYPE_ZTICKET,
                            'sequence' => $fiscalTicket->getSequence(),
                            'number' => $fiscalTicket->getNumber() - 1]);
                    if ($previousFTicket === null) {
                        // TODO: warn discontinuity
                    }
                }
                // Sign
                $fiscalTicket->sign($previousFTicket);
                $this->dao->write($fiscalTicket);
                $previousFTicket = $fiscalTicket;
                // Create next empty session
Karamel's avatar
Karamel committed
184
185
                $next = new CashSession();
                $next->setCashRegister($d->getCashRegister());
186
                $next->setSequence($d->getSequence() + 1);
187
                $next->setContinuous(true); // until proven different by clients
188
                $next->initSums($d);
Karamel's avatar
Karamel committed
189
190
191
192
                $this->dao->write($next);
            }
            $this->dao->write($d);
        }
193
        $this->updateEOSTicket($previousFTicket);
Karamel's avatar
Karamel committed
194
        $this->dao->commit();
Karamel's avatar
Karamel committed
195
196
197
198
199
200
        $ids = array();
        foreach ($data as $d) {
            $ids[] = $d->getId();
        }
        if (count($data) == 1) {
            $ids = $ids[0];
Karamel's avatar
Karamel committed
201
        }
Karamel's avatar
Karamel committed
202
        return $ids;
Karamel's avatar
Karamel committed
203
204
    }

205
206
    /** Update the signature of the end-of-sequence FiscalTicket with
     * the last Z ticket inserted. Does not commit. */
207
    protected function updateEOSTicket($lastFTicket) {
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
        if ($lastFTicket === null) {
            // TODO: warning
            return;
        }
        $eosTicket = $this->dao->read('\Pasteque\Server\Model\FiscalTicket',
                ['type' => FiscalTicket::TYPE_ZTICKET,
                'sequence' => $lastFTicket->getSequence(),
                'number' => 0]);
        if ($eosTicket === null) {
            $eosTicket = new FiscalTicket();
            $eosTicket->setType(FiscalTicket::TYPE_ZTICKET);
            $eosTicket->setSequence($lastFTicket->getSequence());
            $eosTicket->setNumber(0);
            $eosTicket->setContent('EOS');
        }
223
        $eosTicket->setDate($lastFTicket->getDate());
224
225
226
227
        $eosTicket->sign($lastFTicket);
        $this->dao->write($eosTicket);
    }

228
229
230
231
232
233
234
235
236
237
238
239
240
241
    /** Get the summary of a session from registered tickets.
     * This is not the Z Ticket, but it looks like it. */
    public function summary($cashSession) {
        // This one is used by Desktop to show the summary before closing
        // the cash session.
        $ret = new GenericModel();
        // Initialize result
        $ret->set('cashRegister', $cashSession->getCashRegister()->getId());
        $ret->set('sequence', $cashSession->getSequence());
        $ticketCount = 0;
        $custCount = null;
        $paymentCount = 0;
        $cs = 0.0;
        $payments = [];
242
        $custBalances = [];
243
244
        $taxes = [];
        $catSales = [];
Karamel's avatar
Karamel committed
245
        $catTaxes = [];
246
247
248
249
250
251
252
        // Load tickets and add them to the summary
        $tickets = $this->dao->search(Ticket::class,
                [new DAOCondition('cashRegister', '=', $cashSession->getCashRegister()),
                 new DAOCondition('sequence', '=', $cashSession->getSequence())]);
        $summaryPmts = [];
        $summaryTaxes = [];
        $summaryCats = []; // Meow?
Karamel's avatar
Karamel committed
253
        $summaryCatTaxes = [];
254
        $summaryCusts = [];
255
256
        foreach ($tickets as $tkt) {
            $ticketCount++;
257
            $cs += $tkt->getFinalPrice();
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
            if (!empty($tkt->getCustCount())) {
                if ($custCount === null) {
                    $custCount = $tkt->getCustCount();
                } else {
                    $custCount += $tkt->getCustCount();
                }
            }
            $tktPayments = $tkt->getPayments();
            // Payments sums
            foreach ($tktPayments as $pmt) {
                $paymentCount++;
                // Because it is for Desktop, it follows the desktop strucure
                // of ZTicket.Payment instead of the one from TicketPayment
                $pmtRef = $pmt->getPaymentMode()->getReference() . $pmt->getCurrency()->getId();
                if (!isset($summaryPmts[$pmtRef])) {
                    $summaryPmts[$pmtRef] = ['type' => $pmt->getPaymentMode()->getReference(),
                            'amount' => 0.0,
                            'currency' => $pmt->getCurrency()->getId(),
                            'currencyAmount' => 0.0];
                }
                $summaryPmts[$pmtRef]['amount'] += $pmt->getAmount();
                $summaryPmts[$pmtRef]['currencyAmount'] += $pmt->getCurrencyAmount();
            }
281
282
283
284
285
286
287
288
289
            // Cust balance sums
            if ($tkt->getCustomer() != null && abs($tkt->getCustBalance() > 0.005)) {
                $custId = $tkt->getCustomer()->getId();
                if (!isset($summaryCusts[$custId]) && $tkt->getCustBalance()) {
                    $summaryCusts[$custId] = ['customer' => $custId,
                            'balance' => 0.0];
                }
                $summaryCusts[$custId]['balance'] += $tkt->getCustBalance();
            }
290
291
292
293
            // Tax sums
            $tktTax = $tkt->getTaxes();
            foreach ($tktTax as $tax) {
                $taxId = $tax->getTax()->getId();
294
295
296
297
298
                if (!isset($summaryTaxes[$taxId])) {
                    $summaryTaxes[$taxId] = ['tax' => $taxId, // desktop
                            'base' => 0.0,
                            'amount' => 0.0];
                }
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
                $summaryTaxes[$taxId]['base'] += $tax->getBase();
                $summaryTaxes[$taxId]['amount'] += $tax->getAmount();
            }
            // Line sums
            $tktLines = $tkt->getLines();
            foreach ($tktLines as $line) {
                if ($line->getProduct() !== null
                        && ($line->getProduct()->isPrepay())) {
                    // Remove from CS if it is a prepayment refill
                    if ($line->getFinalTaxedPrice() !== null) {
                        $cs -= $line->getFinalTaxedPrice();
                    } else {
                        $cs -= $line->getFinalPrice(); // it should be the same
                    }
                    // And don't include it in category sales and taxes
                    // It is already set in customer balances
                    continue;
                }
317
                // Category
318
319
                // This one may give rounding issues. The accounting service
                // will deal with it.
320
321
322
323
324
                $catId = ($line->getProduct() != null)
                        ? $line->getProduct()->getCategory()->getId()
                        : 0;
                if (!isset($summaryCats[$catId])) {
                    $summaryCats[$catId] = ['category' => $catId, // desktop
325
                            'amount' => 0.0];
326
                }
327
328
329
330
331
332
                $finalPrice = $line->getFinalPrice();
                if ($finalPrice === null) {
                    // This is where rounding issues come.
                    $finalPrice = $line->getFinalTaxedPrice()
                            / (1.0 + $line->getTax()->getRate());
                    $finalPrice = round($finalPrice, 2);
Karamel's avatar
Karamel committed
333
                }
334
                $summaryCats[$catId]['amount'] += $finalPrice;
Karamel's avatar
Karamel committed
335
                // Category tax
336
337
338
339
                if ($line->getTax() === null) {
                    continue;
                }
                $taxId = $line->getTax()->getId();
Karamel's avatar
Karamel committed
340
341
342
343
344
345
346
347
348
349
350
351
352
353
                if (!isset($summaryCatTaxes[$catId])) {
                    $summaryCatTaxes[$catId] = [];
                }
                if (!isset($summaryCatTaxes[$catId][$taxId])) {
                    $ref = ""; $label = ""; // for no category
                    if ($catId != 0) {
                        $category = $line->getProduct()->getCategory();
                        $ref = $category->getReference();
                        $label = $category->getLabel();
                    }
                    $summaryCatTaxes[$catId][$taxId] = ['tax' => $taxId,
                            'reference' => $ref, 'label' => $label,
                            'base' => 0.0, 'amount' => 0.0];
                }
354
355
356
                $summaryCatTaxes[$catId][$taxId]['base'] += $finalPrice;
                $taxAmount = round($finalPrice * $line->getTax()->getRate(), 2);
                $summaryCatTaxes[$catId][$taxId]['amount'] += $taxAmount;
357
358
359
360
361
362
363
364
365
            }
        } // foreach $tickets end
        foreach ($summaryPmts as $pmtRef => $sum) {
            $pmtSum = new GenericModel();
            foreach ($sum as $key => $value) {
                $pmtSum->set($key, $value);
            }
            $payments[] = $pmtSum;
        }
366
367
368
369
370
371
372
        foreach ($summaryCusts as $custId => $sum) {
            $custSum = new GenericModel();
            foreach ($sum as $key => $value) {
                $custSum->set($key, $value);
            }
            $custBalances[] = $custSum;
        }
373
374
375
376
377
378
379
380
381
382
383
384
385
386
        foreach ($summaryTaxes as $taxId => $sum) {
            $taxSum = new GenericModel();
            foreach ($sum as $key => $value) {
                $taxSum->set($key, $value);
            }
            $taxes[] = $taxSum;
        }
        foreach ($summaryCats as $catId => $sum) {
            $catSum = new GenericModel();
            foreach ($sum as $key => $value) {
                $catSum->set($key, $value);
            }
            $catSales[] = $catSum;
        }
Karamel's avatar
Karamel committed
387
388
389
390
391
392
393
394
395
        foreach ($summaryCatTaxes as $catId => $tax) {
            foreach ($tax as $taxId => $sum) {
                $catTaxSum = new GenericModel();
                foreach ($sum as $key => $value) {
                    $catTaxSum->set($key, $value);
                }
                $catTaxes[] = $catTaxSum;
            }
        }
396
397
398
399
400
        $ret->set('ticketCount', $ticketCount);
        $ret->set('custCount', $custCount);
        $ret->set('paymentCount', $paymentCount);
        $ret->set('cs', $cs);
        $ret->set('payments', $payments);
401
        $ret->set('custBalances', $custBalances);
402
403
        $ret->set('taxes', $taxes);
        $ret->set('catSales', $catSales);
Karamel's avatar
Karamel committed
404
        $ret->set('catTaxes', $catTaxes);
405
406
407
        return $ret;
    }

Karamel's avatar
Karamel committed
408
409
410
    /** Delete is disabled. */
    public function delete($id) {}
}