Commit 35aeae08 authored by Karamel's avatar Karamel
Browse files

Fiscal mirror mode. Add some default configuration values.

parent 83fc0975
......@@ -46,6 +46,7 @@ $filesToCheck = [
'src/lib/API/CashregisterAPI.php',
'src/lib/API/FiscalAPI.php',
'src/lib/API/OrderAPI.php',
'src/lib/FiscalMirrorAPI/FiscalAPI.php',
'src/lib/Model/Tax.php',
'src/lib/Model/Category.php',
'src/lib/Model/PaymentMode.php',
......@@ -158,4 +159,4 @@ for ($i = 0; $i < count($md5sums); $i++) {
echo(sprintf("\"%s\": \"%s\"]", $split[0], $split[1]));
}
}
echo("}\n");
\ No newline at end of file
echo("}\n");
......@@ -68,3 +68,13 @@ log_level = "WARNING"
; Uncomment to add an other log directory. System log is still enabled.
;log_dir = "/var/log/pasteque"
debug = false
; Server mode
;------------
; 'regular' (default)
; Can perfom all the regular operations for daily usage.
; 'fiscal mirror'
; Can only copy and read fiscal tickets. They are designed to
; receive those tickets from a regular server to ensure non-alterability.
;server_mode = "regular"
CREATE TABLE fiscaltickets (type VARCHAR(255) NOT NULL, sequence VARCHAR(255) NOT NULL, number INT NOT NULL, date DATETIME NOT NULL, content LONGTEXT NOT NULL, signature VARCHAR(255) NOT NULL, PRIMARY KEY(type, sequence, number)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB;
CREATE TABLE fiscaltickets (type VARCHAR(255) NOT NULL, sequence VARCHAR(255) NOT NULL, number INT NOT NULL, date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, content TEXT NOT NULL, signature VARCHAR(255) NOT NULL, PRIMARY KEY(type, sequence, number));
......@@ -2,6 +2,8 @@
use Dflydev\FigCookies\FigResponseCookies;
use Dflydev\FigCookies\SetCookie;
use \Pasteque\Server\Model\FiscalTicket;
use \Pasteque\Server\System\DateUtils;
use \Pasteque\Server\System\Login;
use \Pasteque\Server\System\API\APICaller;
use \Pasteque\Server\System\API\APIResult;
......@@ -211,4 +213,28 @@ $app->GET('/fiscal/export', function ($request, $response, $args) {
}
return $response;
}
});
\ No newline at end of file
});
/** Fiscal export API */
$app->GET('/api/fiscal/export', function ($request, $response, $args) {
$ptApp = $this->get('settings')['ptApp'];
$apiResult = APICaller::run($ptApp, 'fiscal', 'getAll', [['type', 'sequence', 'number']]);
return $response->withAPIResult($apiResult);
});
/** Fiscal import API */
$app->POST('/api/fiscal/import', function($request, $response, $args) {
$ptApp = $this->get('settings')['ptApp'];
if (!$ptApp->isFiscalMirror()) {
return $response->withStatus(400, 'Only available for Fiscal Mirrors');
}
$data = $request->getParsedBody();
$tkts = [];
foreach ($data as $tkt) {
$tkt['date'] = DateUtils::readDate($tkt['date']);
$tkts[] = FiscalTicket::fromStruct($tkt, $ptApp->getDao());
}
$apiResult = APICaller::run($ptApp, 'fiscal', 'batchImport', [$tkts]);
return $response->withAPIResult($apiResult);
});
......@@ -30,6 +30,9 @@ use \Pasteque\Server\System\Thumbnailer;
class AppContext {
const MODE_REGULAR = 0;
const MODE_FISCALMIRROR = 1;
private $jwtSecret;
private $jwtTimeout;
private $identModule;
......@@ -37,9 +40,19 @@ class AppContext {
private $logger;
private $currentUser;
private $dbInfo;
private $thumbnailer;
private $isDebugMode;
private $serverMode;
private $dao;
public function __constsruct() {
$this->jwtSecret = '';
$this->jwtTimeout = 600;
$this->logger = new Logger('pasteque');
$this->isDebugMode = false;
$this->serverMode = static::MODE_REGULAR;
}
public static function loadFromConfig($config) {
$app = new AppContext();
// Configure logger
......@@ -90,9 +103,21 @@ class AppContext {
SysModuleFactory::extractDatabaseModuleConfig($config, $config['core_database']));
$app->setIdentModule($identModule);
$app->setDbModule($dbModule);
// JWT options
$app->setJwtSecret($config['jwt_secret']);
$app->setJwtTimeout($config['jwt_timeout']);
$app->setAllowedOrigin($config['allowed_origin']);
if (!empty($config['allowed_origin'])) {
$app->setAllowedOrigin($config['allowed_origin']);
}
// Misc options
if (!empty($config['server_mode'])) {
switch (trim(strtolower($config['server_mode']))) {
case 'fiscal mirror':
case 'fiscal_mirror':
$app->setServerMode(static::MODE_FISCALMIRROR);
break;
}
}
return $app;
}
......@@ -113,6 +138,10 @@ class AppContext {
public function getCurrentUser() { return $this->currentUser; }
public function getAllowedOrigin() { return $this->allowedOrigin; }
public function setAllowedOrigin($origins) { $this->allowedOrigin = $origins; }
public function isFiscalMirror() { return $this->serverMode == static::MODE_FISCALMIRROR; }
public function setServerMode($mode) {
$this->serverMode = $mode;
}
/** Set current user and it's dbInfo.
* @param $user User data returned by the IdentModule. */
......
<?php
// Pastèque API
//
// Copyright (C)
// 2012 Scil (http://scil.coop)
// 2017 Karamel, Association Pastèque (karamel@creativekara.fr, https://pasteque.org)
//
// This file is part of Pastèque.
//
// Pastèque is free software: you can redistribute it and/or modify
// 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.
//
// Pastèque is distributed in the hope that it will be useful,
// 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
// along with Pastèque. If not, see <http://www.gnu.org/licenses/>.
namespace Pasteque\Server\FiscalMirrorAPI;
use \Pasteque\Server\API\API;
use \Pasteque\Server\Model\FiscalTicket;
use \Pasteque\Server\Model\GenericModel;
use \Pasteque\Server\System\DAO\DAOCondition;
class FiscalAPI extends \Pasteque\Server\API\FiscalAPI implements API
{
private function writeOne($ticket) {
if ($ticket->getNumber() === 0) {
/// Special case for EOS, can replace at anytime
$this->dao->write($ticket);
$this->dao->commit();
return;
}
// Check if the ticket is already there
$tktSnap = $this->dao->readSnapshot(static::MODEL_NAME, $ticket->getId());
if ($tktSnap !== null) {
if ($tktSnap->getContent() == $ticket->getContent()
&& $tktSnap->getSignature() == $ticket->getSignature()) {
// Same ticket, ignore
return;
} else {
throw new \Exception('Trying to override an existing fiscal ticket.');
}
}
$this->dao->write($ticket);
$this->dao->commit();
}
public function import($ticket) {
$this->writeOne($ticket);
}
public function batchImport($tickets) {
$results = ['successes' => [], 'failures' => []];
foreach ($tickets as $ticket) {
try {
$this->writeOne($ticket);
$results['successes'][] = $ticket;
} catch (\Exception $e) {
$fail = new GenericModel();
$fail->set('ticket', $ticket);
$fail->set('reason', $e->getMessage());
$results['failures'][] = $fail;
}
}
$ret = new GenericModel();
$ret->set('successes', $results['successes']);
$ret->set('failures', $results['failures']);
return $ret;
}
}
......@@ -41,7 +41,9 @@ class APICaller {
if (!is_array($args)) { $args = array($args); }
// Get API class
$apiName = APICaller::formatAPIName($apiName);
$className = sprintf('\Pasteque\Server\API\%sAPI', $apiName);
$apiPkg = $app->isFiscalMirror() && strtolower($apiName) !== 'login' ?
'FiscalMirrorAPI' : 'API'; // TODO: ugly exception
$className = sprintf('\Pasteque\Server\%s\%sAPI', $apiPkg, $apiName);
if (class_exists($className, true)) {
// Class found, get requested method
$apiClass = new \ReflectionClass($className);
......
......@@ -159,7 +159,7 @@ abstract class DoctrineModel
if ($model === null && !$embedded) {
// Embedded classes don't have their own id, so it's ok
// not to find them: it's a new instance.
throw now \UnexpectedValueException('No data found with this Id');
throw new \UnexpectedValueException('No data found with this Id');
}
} else {
$model = new static();
......
......@@ -30,6 +30,12 @@ require_once(dirname(dirname(__FILE__)) . "/common_load.php");
class APICallerTest extends TestCase {
private static $app;
public static function setUpBeforeClass() {
static::$app = new AppContext();
}
public function setUp() {
}
......@@ -90,26 +96,26 @@ class APICallerTest extends TestCase {
}
public function testRunClassNotFound() {
$result = APICaller::run(null, 'someAPI', 'method', array());
$result = APICaller::run(static::$app, 'someAPI', 'method', array());
$this->assertEquals(APIResult::STATUS_CALL_REJECTED, $result->getStatus());
}
/** @depends testFormatAPIName
* @depends testRunClassNotFound */
public function testRunMethodNotFound() {
$result = APICaller::run(null, 'test', 'method', array());
$result = APICaller::run(static::$app, 'test', 'method', array());
$this->assertEquals(APIResult::STATUS_CALL_REJECTED, $result->getStatus());
}
/** @depends testRunMethodNotFound */
public function testRunWrongArgs() {
$result = APICaller::run(null, 'test', 'reqParams', array(1));
$result = APICaller::run(static::$app, 'test', 'reqParams', array(1));
$this->assertEquals(APIResult::STATUS_CALL_REJECTED, $result->getStatus());
}
/** @depends testFormatAPIName */
public function testRunNoArgsOK() {
$result = APICaller::run(null, 'test', 'noParam', array());
$result = APICaller::run(static::$app, 'test', 'noParam', array());
$this->assertEquals(APIResult::STATUS_CALL_OK, $result->getStatus(),
sprintf('Call rejected: %s', json_encode($result->getContent())));
$this->assertEquals(\Pasteque\Server\API\TestAPI::NO_PARAM_RESULT, $result->getContent());
......@@ -118,7 +124,7 @@ class APICallerTest extends TestCase {
/** @depends testFormatAPIName */
public function testRunArgsOK() {
$args = array(1, 2);
$result = APICaller::run(null, 'test', 'params', $args);
$result = APICaller::run(static::$app, 'test', 'params', $args);
$this->assertEquals(APIResult::STATUS_CALL_OK, $result->getStatus(),
sprintf('Call rejected: %s', json_encode($result->getContent())));
$this->assertEquals($args[0] + $args[1], $result->getContent());
......
<?php
// Pastèque Web back office
//
// Copyright (C) 2013 Scil (http://scil.coop)
//
// This file is part of Pastèque.
//
// Pastèque is free software: you can redistribute it and/or modify
// 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.
//
// Pastèque is distributed in the hope that it will be useful,
// 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
// along with Pastèque. If not, see <http://www.gnu.org/licenses/>.
// Some utility functions to test tickets.
use \Pasteque\Server\Model\FiscalTicket;
use \Pasteque\Server\System\DateUtils;
use \Pasteque\Server\System\DAO\DAOCondition;
function getBaseFiscalTicket($type, $sequence, $number, $date, $content, $signature) {
return ['type' => $type,
'sequence' => $sequence,
'number' => $number,
'date' => DateUtils::toTimestamp($date),
'content' => $content,
'signature' => $signature];
}
function assertFiscalTicketModelEqModel($m1, $m2, $ut) {
$ut->assertEquals($m1->getType(), $m2->getType());
$ut->assertEquals($m1->getSequence(), $m2->getSequence());
$ut->assertTrue(DateUtils::equals($m1->getDate(), $m2->getDate()));
$ut->assertEquals($m1->getContent(), $m2->getContent());
$ut->assertEquals($m1->getSignature(), $m2->getSignature());
}
function assertFiscalTicketModelEqStruct($model, $struct, $ut) {
$ut->assertEquals($model->getType(), $struct['type']);
$ut->assertEquals($model->getSequence(), $struct['sequence']);
$ut->assertTrue(DateUtils::equals($model->getDate(), $struct['date']));
$ut->assertEquals($model->getContent(), $struct['content']);
$ut->assertEquals($model->getSignature(), $struct['signature']);
}
function readFiscalTicketSnapshot($type, $sequence, $number, $dao) {
return $dao->readSnapshot(FiscalTicket::class,
['type' => $type, 'sequence' => $sequence, 'number' => $number]);
}
<?php
// Pasteque server testing
//
// Copyright (C)
// 2012 Scil (http://scil.coop)
// 2017 Karamel, Association Pastèque (karamel@creativekara.fr, https://pasteque.org)
//
// This file is part of Pasteque.
//
// Pasteque is free software: you can redistribute it and/or modify
// 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.
//
// Pasteque is distributed in the hope that it will be useful,
// 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
// along with Pasteque. If not, see <http://www.gnu.org/licenses/>.
namespace Pasteque\Server;
use \Pasteque\Server\Model\FiscalTicket;
use \Pasteque\Server\System\Login;
use \Pasteque\Server\System\DAO\DAOFactory;
use \PHPUnit\Framework\TestCase;
require_once(dirname(dirname(dirname(__FILE__))) . '/common_load.php');
require_once(dirname(dirname(dirname(__FILE__))) . '/common_fiscalticket.php');
class HttpFiscalTest extends TestCase
{
private $curl;
private $dao;
private static $token;
public static function setUpBeforeClass() {
static::$token = obtainToken();
}
public static function tearDownAfterClass() {
}
protected function setUp() {
$this->curl = curl_init();
curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($this->curl, CURLOPT_HTTPHEADER,
[Login::TOKEN_HEADER . ': ' . static::$token]);
global $dbInfo;
$this->dao = DAOFactory::getDAO($dbInfo, ['debug' => true]);
}
protected function tearDown() {
curl_close($this->curl);
$all = $this->dao->search(FiscalTicket::class);
foreach($all as $record) {
$this->dao->delete($record);
}
$this->dao->commit();
$this->dao->close();
}
public function testImportNew() {
// Create a few tickets
$tkt = new FiscalTicket();
$tkt->setType(FiscalTicket::TYPE_TICKET);
$tkt->setSequence('1');
$tkt->setNumber(1);
$tkt->setDate(new \DateTime('2018-01-01 12:00'));
$tkt->setContent('Nippon Ichi!');
$tkt->sign(null);
$tkt2 = new FiscalTicket();
$tkt2->setType(FiscalTicket::TYPE_TICKET);
$tkt2->setSequence('1');
$tkt2->setNumber(2);
$tkt2->setDate(new \DateTime('2018-01-01 15:02'));
$tkt2->setContent('Mewtwo');
$tkt2->sign($tkt);
$tkts = [$tkt->toStruct(), $tkt2->toStruct()];
unset($tkts[0]['id']);
unset($tkts[1]['id']);
// Import
curl_setopt($this->curl, CURLOPT_URL, apiUrl('api/fiscal/import'));
curl_setopt($this->curl, CURLOPT_HTTPHEADER,
[Login::TOKEN_HEADER . ': ' . static::$token, 'Content-type: application/json']);
curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($this->curl, CURLOPT_POSTFIELDS, json_encode($tkts));
$resp = curl_exec($this->curl);
$this->assertEquals(200, curl_getinfo($this->curl, CURLINFO_HTTP_CODE));
$result = json_decode($resp, true);
$this->assertEquals(2, count($result['successes']));
$this->assertEquals(0, count($result['failures']));
assertFiscalTicketModelEqStruct($tkt, $result['successes'][0], $this);
assertFiscalTicketModelEqStruct($tkt2, $result['successes'][1], $this);
$snap1 = readFiscalTicketSnapshot(FiscalTicket::TYPE_TICKET, '1', 1, $this->dao);
$snap2 = readFiscalTicketSnapshot(FiscalTicket::TYPE_TICKET, '1', 2, $this->dao);
assertFiscalTicketModelEqStruct($snap1, $tkts[0], $this);
assertFiscalTicketModelEqStruct($snap2, $tkts[1], $this);
$this->assertEquals(2, $this->dao->count(FiscalTicket::class));
}
/** @depends testImportNew
* Reimport existing ticket and add a few. */
public function testReimport() {
// Create a few tickets
$tkt = new FiscalTicket();
$tkt->setType(FiscalTicket::TYPE_TICKET);
$tkt->setSequence('1');
$tkt->setNumber(1);
$tkt->setDate(new \DateTime('2018-01-01 12:00'));
$tkt->setContent('Nippon Ichi!');
$tkt->sign(null);
$tkts = [$tkt->toStruct()];
unset($tkts[0]['id']);
// Import
curl_setopt($this->curl, CURLOPT_URL, apiUrl('api/fiscal/import'));
curl_setopt($this->curl, CURLOPT_HTTPHEADER,
[Login::TOKEN_HEADER . ': ' . static::$token, 'Content-type: application/json']);
curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($this->curl, CURLOPT_POSTFIELDS, json_encode($tkts));
$resp = curl_exec($this->curl);
$this->assertEquals(200, curl_getinfo($this->curl, CURLINFO_HTTP_CODE));
$result = json_decode($resp, true);
// Create a new ticket
$tkt2 = new FiscalTicket();
$tkt2->setType(FiscalTicket::TYPE_TICKET);
$tkt2->setSequence('1');
$tkt2->setNumber(2);
$tkt2->setDate(new \DateTime('2018-01-01 15:02'));
$tkt2->setContent('Mewtwo');
$tkt2->sign($tkt);
$tkts[] = $tkt2->toStruct();
unset($tkts[1]['id']);
// Import the whole
curl_setopt($this->curl, CURLOPT_URL, apiUrl('api/fiscal/import'));
curl_setopt($this->curl, CURLOPT_HTTPHEADER,
[Login::TOKEN_HEADER . ': ' . static::$token, 'Content-type: application/json']);
curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($this->curl, CURLOPT_POSTFIELDS, json_encode($tkts));
$resp = curl_exec($this->curl);
$this->assertEquals(200, curl_getinfo($this->curl, CURLINFO_HTTP_CODE));
$result = json_decode($resp, true);
$this->assertEquals(2, count($result['successes']));
$this->assertEquals(0, count($result['failures']));
assertFiscalTicketModelEqStruct($tkt, $result['successes'][0], $this);
assertFiscalTicketModelEqStruct($tkt2, $result['successes'][1], $this);
$snap1 = readFiscalTicketSnapshot(FiscalTicket::TYPE_TICKET, '1', 1, $this->dao);
$snap2 = readFiscalTicketSnapshot(FiscalTicket::TYPE_TICKET, '1', 2, $this->dao);
assertFiscalTicketModelEqStruct($snap1, $tkts[0], $this);
assertFiscalTicketModelEqStruct($snap2, $tkts[1], $this);
$this->assertEquals(2, $this->dao->count(FiscalTicket::class));
}
/** @depends testImportNew */
public function testImportRewrite() {
// Create a few tickets
$tkt = new FiscalTicket();
$tkt->setType(FiscalTicket::TYPE_TICKET);
$tkt->setSequence('1');
$tkt->setNumber(1);
$tkt->setDate(new \DateTime('2018-01-01 12:00'));
$tkt->setContent('Nippon Ichi!');
$tkt->sign(null);
$tkts = [$tkt->toStruct()];
unset($tkts[0]['id']);
// Import
curl_setopt($this->curl, CURLOPT_URL, apiUrl('api/fiscal/import'));
curl_setopt($this->curl, CURLOPT_HTTPHEADER,
[Login::TOKEN_HEADER . ': ' . static::$token, 'Content-type: application/json']);
curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($this->curl, CURLOPT_POSTFIELDS, json_encode($tkts));
$resp = curl_exec($this->curl);
$this->assertEquals(200, curl_getinfo($this->curl, CURLINFO_HTTP_CODE));
$result = json_decode($resp, true);
// Update that ticket and reimport it
$tkts[0]['content'] = 'Metamorphosis';
curl_setopt($this->curl, CURLOPT_URL, apiUrl('api/fiscal/import'));
curl_setopt($this->curl, CURLOPT_HTTPHEADER,
[Login::TOKEN_HEADER . ': ' . static::$token, 'Content-type: application/json']);
curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($this->curl, CURLOPT_POSTFIELDS, json_encode($tkts));
$resp = curl_exec($this->curl);
$this->assertEquals(200, curl_getinfo($this->curl, CURLINFO_HTTP_CODE));
$result = json_decode($resp, true);
$this->assertEquals(0, count($result['successes']));
$this->assertEquals(1, count($result['failures']));
$fail = $result['failures'][0]['ticket'];
$this->assertEquals($tkts[0]['type'], $fail['type']);
$this->assertEquals($tkts[0]['sequence'], $fail['sequence']);
$this->assertEquals($tkts[0]['number'], $fail['number']);
$this->assertEquals($tkts[0]['content'], $fail['content']);
$this->assertEquals($tkts[0]['signature'], $fail['signature']);
$this->assertEquals('Trying to override an existing fiscal ticket.',
$result['failures'][0]['reason']);
$this->assertEquals(1, $this->dao->count(FiscalTicket::class));
$snap = readFiscalTicketSnapshot(FiscalTicket::TYPE_TICKET, '1', 1, $this->dao);
$this->assertEquals($tkt->getContent(), $snap->getContent());
}
}
<?php
// Pasteque server testing
//
// Copyright (C)
// 2012 Scil (http://scil.coop)
// 2017 Karamel, Association Pastèque (karamel@creativekara.fr, https://pasteque.org)
//
// This file is part of Pasteque.
//
// Pasteque is free software: you can redistribute it and/or modify
// 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.
//
// Pasteque is distributed in the hope that it will be useful,
// 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
// along with Pasteque. If not, see <http://www.gnu.org/licenses/>.
namespace Pasteque\Server;
use \Pasteque\Server\Model\FiscalTicket;
use \Pasteque\Server\System\Login;
use \Pasteque\Server\System\DAO\DAOFactory;
use \PHPUnit\Framework\TestCase;
require_once(dirname(dirname(__FILE__)) . "/common_load.php");
class HttpFiscalTest extends TestCase
{
private $curl;
private $dao;
private static $token;
public static function setUpBeforeClass() {
static::$token = obtainToken();
}
public static function tearDownAfterClass() {
}