stats sales with mago
This commit is contained in:
@@ -0,0 +1,402 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* MagoClient - client REST in SOLA LETTURA per Mago4 (Zucchetti) - ERPServiceProvider
|
||||
*
|
||||
* Uso tipico:
|
||||
* $mago = MagoClient::fromEnv();
|
||||
* foreach ($mago->iterate('MA_SaleOrd', ['filters' => [...]]) as $rec) { ... }
|
||||
* $mago->logoff(); // SEMPRE: le sessioni occupano licenza
|
||||
*
|
||||
* Variabili .env richieste:
|
||||
* MAGO_BASE_URL, MAGO_USER, MAGO_PASSWORD, MAGO_COMPANY
|
||||
* Opzionale:
|
||||
* MAGO_TIMEOUT (secondi, default 60)
|
||||
*
|
||||
* Compatibile PHP 7.4+
|
||||
*/
|
||||
|
||||
class MagoApiException extends RuntimeException {}
|
||||
|
||||
class MagoClient
|
||||
{
|
||||
/** @var string */
|
||||
private $baseUrl;
|
||||
/** @var string */
|
||||
private $username;
|
||||
/** @var string */
|
||||
private $password;
|
||||
/** @var string */
|
||||
private $company;
|
||||
/** @var int */
|
||||
private $timeout;
|
||||
/** @var string|null */
|
||||
private $token = null;
|
||||
/** @var string|null */
|
||||
private $lastLogoffError = null;
|
||||
/** @var string|null percorso di logoff che ha risposto correttamente */
|
||||
private $logoffPath = null;
|
||||
|
||||
public function __construct($baseUrl, $username, $password, $company, $timeout = 60)
|
||||
{
|
||||
$this->baseUrl = rtrim((string)$baseUrl, '/') . '/';
|
||||
$this->username = (string)$username;
|
||||
$this->password = (string)$password;
|
||||
$this->company = (string)$company;
|
||||
$this->timeout = max(5, (int)$timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea il client leggendo le credenziali da .env (dotenv) / variabili d'ambiente.
|
||||
*/
|
||||
public static function fromEnv()
|
||||
{
|
||||
$get = function ($k, $default = null) {
|
||||
if (isset($_ENV[$k]) && $_ENV[$k] !== '') return $_ENV[$k];
|
||||
if (isset($_SERVER[$k]) && $_SERVER[$k] !== '') return $_SERVER[$k];
|
||||
$v = getenv($k);
|
||||
return ($v !== false && $v !== '') ? $v : $default;
|
||||
};
|
||||
|
||||
$cfg = [
|
||||
'MAGO_BASE_URL' => $get('MAGO_BASE_URL'),
|
||||
'MAGO_USER' => $get('MAGO_USER'),
|
||||
'MAGO_PASSWORD' => $get('MAGO_PASSWORD'),
|
||||
'MAGO_COMPANY' => $get('MAGO_COMPANY'),
|
||||
];
|
||||
|
||||
$missing = array_keys(array_filter($cfg, function ($v) {
|
||||
return $v === null;
|
||||
}));
|
||||
if ($missing) {
|
||||
throw new MagoApiException('Variabili .env mancanti: ' . implode(', ', $missing));
|
||||
}
|
||||
|
||||
return new self(
|
||||
$cfg['MAGO_BASE_URL'],
|
||||
$cfg['MAGO_USER'],
|
||||
$cfg['MAGO_PASSWORD'],
|
||||
$cfg['MAGO_COMPANY'],
|
||||
(int)$get('MAGO_TIMEOUT', 60)
|
||||
);
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
// Rete di sicurezza: se lo script dimentica il logoff, libero comunque la licenza
|
||||
$this->logoff();
|
||||
}
|
||||
|
||||
public function isLoggedIn()
|
||||
{
|
||||
return $this->token !== null;
|
||||
}
|
||||
|
||||
public function getLastLogoffError()
|
||||
{
|
||||
return $this->lastLogoffError;
|
||||
}
|
||||
|
||||
public function getLogoffPath()
|
||||
{
|
||||
return $this->logoffPath;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* SESSIONE
|
||||
* ========================================================= */
|
||||
|
||||
public function login()
|
||||
{
|
||||
$r = $this->request('api/account-manager/login', [
|
||||
'username' => $this->username,
|
||||
'password' => $this->password,
|
||||
'company' => $this->company,
|
||||
], false);
|
||||
|
||||
$token = $r['data']['token'] ?? null;
|
||||
if (!is_string($token) || $token === '') {
|
||||
throw new MagoApiException('Login Mago fallito (HTTP ' . $r['status'] . '): ' . $this->extractMessage($r));
|
||||
}
|
||||
|
||||
$this->token = $token;
|
||||
return $this->token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Chiude la sessione e libera lo slot di licenza.
|
||||
* Non lancia eccezioni: ritorna true/false (dettaglio in getLastLogoffError()).
|
||||
*/
|
||||
public function logoff()
|
||||
{
|
||||
if ($this->token === null) return true;
|
||||
|
||||
$this->lastLogoffError = null;
|
||||
|
||||
// Il percorso "logoff" ha risposto 404: provo le varianti note e memorizzo quella buona
|
||||
$candidates = $this->logoffPath
|
||||
? [$this->logoffPath]
|
||||
: ['api/account-manager/logoff', 'api/account-manager/logout', 'api/account-manager/logOff'];
|
||||
|
||||
$errors = [];
|
||||
try {
|
||||
foreach ($candidates as $path) {
|
||||
$r = $this->request($path, ['token' => $this->token], true);
|
||||
if ($r['status'] >= 400) {
|
||||
$errors[] = $path . ' → HTTP ' . $r['status'];
|
||||
continue;
|
||||
}
|
||||
$this->logoffPath = $path;
|
||||
return true;
|
||||
}
|
||||
$this->lastLogoffError = implode(' | ', $errors);
|
||||
return false;
|
||||
} catch (Throwable $e) {
|
||||
$this->lastLogoffError = $e->getMessage();
|
||||
return false;
|
||||
} finally {
|
||||
$this->token = null;
|
||||
}
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* LETTURA DATI
|
||||
* ========================================================= */
|
||||
|
||||
/**
|
||||
* Legge UNA pagina di una tabella.
|
||||
*
|
||||
* $opts:
|
||||
* 'fields' => ['Campo1', 'Campo2'] (opzionale)
|
||||
* 'filters' => [['key'=>'OrderDate','value'=>'2026-01-01T00:00:00','operator'=>'>=']]
|
||||
* value può essere anche DateTimeInterface
|
||||
* 'recordsPerPage' => 200 (massimo accettato da Mago)
|
||||
* 'pageNumber' => 1
|
||||
*
|
||||
* Ritorna: ['total'=>int, 'count'=>int, 'page'=>int, 'records'=>array]
|
||||
*/
|
||||
public function getDataTable($tableName, array $opts = [])
|
||||
{
|
||||
$body = [
|
||||
'tableName' => (string)$tableName,
|
||||
'recordsPerPage' => max(1, min(200, (int)($opts['recordsPerPage'] ?? 200))), // Mago restituisce max 200 per pagina
|
||||
'pageNumber' => max(1, (int)($opts['pageNumber'] ?? 1)),
|
||||
];
|
||||
|
||||
if (!empty($opts['fields'])) {
|
||||
$body['fieldsName'] = array_values($opts['fields']);
|
||||
}
|
||||
if (!empty($opts['filters'])) {
|
||||
$body['filters'] = $this->normalizeFilters($opts['filters']);
|
||||
}
|
||||
|
||||
$data = $this->callWithRelogin('api/magoservices/getDataTable', $body);
|
||||
|
||||
if (($data['response'] ?? '') !== 'OK') {
|
||||
throw new MagoApiException("getDataTable($tableName) risposta non OK: " . $this->extractMessage(['data' => $data, 'raw' => json_encode($data)]));
|
||||
}
|
||||
|
||||
$d = $data['responseData'] ?? [];
|
||||
|
||||
return [
|
||||
'total' => (int)($d['totalRecordsNumber'] ?? 0),
|
||||
'count' => (int)($d['currentPageRecordsNumber'] ?? 0),
|
||||
'page' => (int)($d['currentPageNumber'] ?? $body['pageNumber']),
|
||||
'records' => is_array($d['records'] ?? null) ? $d['records'] : [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Itera TUTTI i record di una tabella paginando in automatico (generator, poca memoria).
|
||||
*
|
||||
* foreach ($mago->iterate('MA_SaleOrd', ['filters'=>[...], 'recordsPerPage'=>500]) as $rec) {...}
|
||||
*/
|
||||
public function iterate($tableName, array $opts = [], $maxPages = 100000)
|
||||
{
|
||||
$perPage = max(1, min(200, (int)($opts['recordsPerPage'] ?? 200)));
|
||||
$page = 1;
|
||||
$read = 0;
|
||||
|
||||
do {
|
||||
$r = $this->getDataTable($tableName, array_merge($opts, [
|
||||
'recordsPerPage' => $perPage,
|
||||
'pageNumber' => $page,
|
||||
]));
|
||||
|
||||
if (empty($r['records'])) break;
|
||||
|
||||
foreach ($r['records'] as $rec) {
|
||||
$read++;
|
||||
yield $rec;
|
||||
}
|
||||
|
||||
$page++;
|
||||
} while ($read < $r['total'] && $page <= $maxPages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Conta i record (chiede 1 solo record e legge totalRecordsNumber).
|
||||
*/
|
||||
public function count($tableName, array $filters = [])
|
||||
{
|
||||
$r = $this->getDataTable($tableName, [
|
||||
'filters' => $filters,
|
||||
'recordsPerPage' => 1,
|
||||
'pageNumber' => 1,
|
||||
]);
|
||||
return $r['total'];
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* HELPER DI CONVERSIONE (per l'import in MySQL)
|
||||
* ========================================================= */
|
||||
|
||||
/**
|
||||
* Converte una data Mago in formato MySQL. Le date "vuote" di Mago (1799-12-31) diventano null.
|
||||
*/
|
||||
public static function toMysqlDate($value, $withTime = false)
|
||||
{
|
||||
if ($value === null) return null;
|
||||
$value = trim((string)$value);
|
||||
if ($value === '' || strpos($value, '1799-12-31') === 0) return null;
|
||||
|
||||
$ts = strtotime($value);
|
||||
if ($ts === false) return null;
|
||||
|
||||
return date($withTime ? 'Y-m-d H:i:s' : 'Y-m-d', $ts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converte i booleani di Mago (true/false, 1/0, "1"/"0", "true"/"false", "Y"/"N") in 0/1.
|
||||
*/
|
||||
public static function toBool($value)
|
||||
{
|
||||
if (is_bool($value)) return $value ? 1 : 0;
|
||||
$v = strtolower(trim((string)$value));
|
||||
return in_array($v, ['1', 'true', 'y', 'yes', 's', 'si'], true) ? 1 : 0;
|
||||
}
|
||||
|
||||
public static function toDecimal($value)
|
||||
{
|
||||
if ($value === null || $value === '') return 0.0;
|
||||
return (float)str_replace(',', '.', (string)$value);
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* INTERNI
|
||||
* ========================================================= */
|
||||
|
||||
private function normalizeFilters(array $filters)
|
||||
{
|
||||
$out = [];
|
||||
foreach ($filters as $f) {
|
||||
if (!isset($f['key'])) continue;
|
||||
|
||||
$value = $f['value'] ?? '';
|
||||
if ($value instanceof DateTimeInterface) {
|
||||
$value = $value->format('Y-m-d\TH:i:s');
|
||||
}
|
||||
// Verificato: una data SENZA orario dà risultati errati → aggiungo sempre T00:00:00
|
||||
if (is_string($value) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) {
|
||||
$value .= 'T00:00:00';
|
||||
}
|
||||
|
||||
$out[] = [
|
||||
'key' => (string)$f['key'],
|
||||
'value' => is_bool($value) ? ($value ? 'true' : 'false') : (string)$value,
|
||||
'operator' => (string)($f['operator'] ?? '='),
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
private function callWithRelogin($path, $body)
|
||||
{
|
||||
if ($this->token === null) {
|
||||
$this->login();
|
||||
}
|
||||
|
||||
$r = $this->request($path, $body, true);
|
||||
|
||||
// Token scaduto → rifaccio login UNA volta e ritento
|
||||
if ($this->isAuthError($r)) {
|
||||
$this->token = null;
|
||||
$this->login();
|
||||
$r = $this->request($path, $body, true);
|
||||
}
|
||||
|
||||
if ($r['status'] >= 400 || $r['data'] === null) {
|
||||
throw new MagoApiException('Errore Mago (HTTP ' . $r['status'] . ') su ' . $path . ': ' . $this->extractMessage($r));
|
||||
}
|
||||
|
||||
return $r['data'];
|
||||
}
|
||||
|
||||
private function isAuthError(array $r)
|
||||
{
|
||||
$raw = (string)$r['raw'];
|
||||
return stripos($raw, 'LoginInfo Not Found') !== false
|
||||
|| stripos($raw, 'Missing token') !== false
|
||||
|| $r['status'] === 401;
|
||||
}
|
||||
|
||||
private function request($path, $body, $auth)
|
||||
{
|
||||
if (!function_exists('curl_init')) {
|
||||
throw new MagoApiException('Estensione PHP cURL non disponibile');
|
||||
}
|
||||
|
||||
$headers = [
|
||||
'Content-Type: application/json',
|
||||
'Accept: application/json',
|
||||
];
|
||||
|
||||
if ($auth) {
|
||||
if ($this->token === null) {
|
||||
throw new MagoApiException('Chiamata autenticata senza token (login non eseguito)');
|
||||
}
|
||||
$headers[] = 'Authorization: ' . $this->token; // token nudo, senza "Bearer"
|
||||
}
|
||||
|
||||
$ch = curl_init($this->baseUrl . ltrim($path, '/'));
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_CONNECTTIMEOUT => 10,
|
||||
CURLOPT_TIMEOUT => $this->timeout,
|
||||
]);
|
||||
|
||||
$raw = curl_exec($ch);
|
||||
|
||||
if ($raw === false) {
|
||||
$err = curl_error($ch);
|
||||
curl_close($ch);
|
||||
throw new MagoApiException('Connessione a Mago fallita (VPN attiva?): ' . $err);
|
||||
}
|
||||
|
||||
$status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$data = json_decode($raw, true);
|
||||
|
||||
return [
|
||||
'status' => $status,
|
||||
'data' => is_array($data) ? $data : null,
|
||||
'raw' => $raw,
|
||||
];
|
||||
}
|
||||
|
||||
private function extractMessage(array $r)
|
||||
{
|
||||
$d = $r['data'] ?? null;
|
||||
if (is_array($d)) {
|
||||
foreach (['message', 'Message', 'error', 'errorMessage', 'responseMessage', 'response'] as $k) {
|
||||
if (!empty($d[$k]) && is_string($d[$k])) return $d[$k];
|
||||
}
|
||||
}
|
||||
$raw = trim((string)($r['raw'] ?? ''));
|
||||
return $raw !== '' ? mb_substr($raw, 0, 300) : 'risposta vuota';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user