Files
zibo-dashboard/public/userarea/class/MagoSync.php
T
2026-10-13 16:30:47 +02:00

653 lines
30 KiB
PHP

<?php
/**
* MagoSync - sincronizza dati Mago4 → tabelle MySQL locali (Mago in SOLA LETTURA).
*
* $sync = new MagoSync($pdo, MagoClient::fromEnv());
* $report = $sync->run(['limit' => 50]); // prova con pochi ordini
* $report = $sync->run(); // incrementale (TbModified)
* $report = $sync->run(['full' => true]); // tutto dal 2026-01-01
*
* Il logoff da Mago viene fatto sempre, anche in caso di errore.
*/
require_once __DIR__ . '/MagoClient.php';
class MagoSync
{
const CUSTOMER_TYPE = 3211264; // CustSuppType "Cliente" (verificato sui dati)
const DEFAULT_FROM = '2020-01-01'; // storico dal 2020 per confronto anni
const ENTITY_ORDERS = 'sale_ord';
const ENTITY_CUSTOMER = 'cust_supp';
const ENTITY_SUMMARY = 'sale_ord_summary';
const ENTITY_DETAILS = 'sale_ord_details';
/** @var PDO */
private $pdo;
/** @var MagoClient */
private $mago;
/** @var callable|null */
private $logger;
public function __construct(PDO $pdo, MagoClient $mago, ?callable $logger = null)
{
$this->pdo = $pdo;
$this->mago = $mago;
$this->logger = $logger;
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
/**
* Opzioni:
* limit int|null numero massimo di ordini (modalità prova)
* full bool ignora l'incrementale e rilegge tutto dal 'from'
* from string data minima ordini (default 2026-01-01)
* skip_customers bool non sincronizzare l'anagrafica clienti
*/
public function run(array $opts = [])
{
$limit = isset($opts['limit']) && (int)$opts['limit'] > 0 ? (int)$opts['limit'] : null;
$full = !empty($opts['full']);
$from = !empty($opts['from']) ? $opts['from'] : self::DEFAULT_FROM;
$report = ['ok' => true, 'limit' => $limit, 'full' => $full, 'from' => $from];
if (!$this->acquireLock()) {
return ['ok' => false, 'message' => 'Sincronizzazione già in corso, riprova tra poco.'];
}
try {
$this->mago->login();
if (empty($opts['skip_customers'])) {
$report['clienti'] = $this->syncCustomers($full);
}
$report['ordini'] = $this->syncSaleOrders($from, $limit, $full);
$report['righe'] = $this->syncSaleOrderDetails($from, $full, (bool)$limit);
$report['importi'] = $this->syncSaleOrderSummaries($from, $full, (bool)$limit);
} catch (Throwable $e) {
$report['ok'] = false;
$report['message'] = $e->getMessage();
$this->log('ERRORE: ' . $e->getMessage());
} finally {
$logoffOk = $this->mago->logoff();
$report['logoff'] = $logoffOk ? 'OK' : ('non riuscito: ' . $this->mago->getLastLogoffError());
$this->releaseLock();
}
return $report;
}
/* =========================================================
* ORDINI CLIENTE
* ========================================================= */
public function syncSaleOrders($from, $limit = null, $full = false)
{
$t0 = microtime(true);
$state = $this->getState(self::ENTITY_ORDERS);
$filters = [['key' => 'OrderDate', 'value' => $from . 'T00:00:00', 'operator' => '>=']];
// Incrementale: solo i record modificati dall'ultima sincronizzazione completa
$incremental = !$full && !$limit && !empty($state['last_tb_modified']);
if ($incremental) {
$filters[] = [
'key' => 'TbModified',
'value' => date('Y-m-d\TH:i:s', strtotime($state['last_tb_modified'])),
'operator' => '>=',
];
}
$this->log('Ordini: ' . ($incremental ? 'incrementale da ' . $state['last_tb_modified'] : 'completa dal ' . $from) . ($limit ? " (prova: max $limit)" : ''));
$sql = "INSERT INTO mago_sale_ord (
sale_ord_id, internal_ord_no, external_ord_no, order_date,
expected_delivery_date, confirmed_delivery_date, compulsory_delivery_date,
customer, invoicing_customer, salesperson, area_manager, area,
payment, currency, our_reference, your_reference,
transport, port, package, storage, job,
delivered, invoiced, cancelled, is_blocked, printed, archived, open_order,
priority, notes, tb_created, tb_modified, raw_json, synced_at
) VALUES (
:sale_ord_id, :internal_ord_no, :external_ord_no, :order_date,
:expected_delivery_date, :confirmed_delivery_date, :compulsory_delivery_date,
:customer, :invoicing_customer, :salesperson, :area_manager, :area,
:payment, :currency, :our_reference, :your_reference,
:transport, :port, :package, :storage, :job,
:delivered, :invoiced, :cancelled, :is_blocked, :printed, :archived, :open_order,
:priority, :notes, :tb_created, :tb_modified, :raw_json, NOW()
)
ON DUPLICATE KEY UPDATE
internal_ord_no = VALUES(internal_ord_no),
external_ord_no = VALUES(external_ord_no),
order_date = VALUES(order_date),
expected_delivery_date = VALUES(expected_delivery_date),
confirmed_delivery_date = VALUES(confirmed_delivery_date),
compulsory_delivery_date = VALUES(compulsory_delivery_date),
customer = VALUES(customer),
invoicing_customer = VALUES(invoicing_customer),
salesperson = VALUES(salesperson),
area_manager = VALUES(area_manager),
area = VALUES(area),
payment = VALUES(payment),
currency = VALUES(currency),
our_reference = VALUES(our_reference),
your_reference = VALUES(your_reference),
transport = VALUES(transport),
port = VALUES(port),
package = VALUES(package),
storage = VALUES(storage),
job = VALUES(job),
delivered = VALUES(delivered),
invoiced = VALUES(invoiced),
cancelled = VALUES(cancelled),
is_blocked = VALUES(is_blocked),
printed = VALUES(printed),
archived = VALUES(archived),
open_order = VALUES(open_order),
priority = VALUES(priority),
notes = VALUES(notes),
tb_created = VALUES(tb_created),
tb_modified = VALUES(tb_modified),
raw_json = VALUES(raw_json),
synced_at = NOW()";
$stmt = $this->pdo->prepare($sql);
$count = 0;
$maxModified = $state['last_tb_modified'] ?? null;
$this->pdo->beginTransaction();
try {
foreach ($this->mago->iterate('MA_SaleOrd', ['filters' => $filters]) as $r) {
$stmt->execute($this->mapSaleOrd($r));
$count++;
$mod = MagoClient::toMysqlDate($r['TbModified'] ?? null, true);
if ($mod && (!$maxModified || $mod > $maxModified)) {
$maxModified = $mod;
}
if ($count % 200 === 0) {
$this->pdo->commit();
$this->pdo->beginTransaction();
$this->log(" ordini salvati: $count");
}
if ($limit && $count >= $limit) break;
}
$this->pdo->commit();
} catch (Throwable $e) {
if ($this->pdo->inTransaction()) $this->pdo->rollBack();
$this->saveState(self::ENTITY_ORDERS, null, 'errore', $e->getMessage(), $count, $t0, false);
throw $e;
}
// In modalità prova NON avanzo il punto di ripresa (i record non arrivano in ordine di modifica)
$this->saveState(
self::ENTITY_ORDERS,
$limit ? null : $maxModified,
'ok',
($limit ? "Prova: $count ordini" : ($incremental ? "Incrementale: $count ordini" : "Completa: $count ordini")),
$count,
$t0,
!$limit
);
$this->log("Ordini sincronizzati: $count");
return ['records' => $count, 'incrementale' => $incremental, 'ms' => $this->ms($t0)];
}
private function mapSaleOrd(array $r)
{
$s = function ($k, $len = 255) use ($r) {
return mb_substr(trim((string)($r[$k] ?? '')), 0, $len);
};
return [
'sale_ord_id' => (int)($r['SaleOrdId'] ?? 0),
'internal_ord_no' => $s('InternalOrdNo', 30),
'external_ord_no' => $s('ExternalOrdNo', 60),
'order_date' => MagoClient::toMysqlDate($r['OrderDate'] ?? null),
'expected_delivery_date' => MagoClient::toMysqlDate($r['ExpectedDeliveryDate'] ?? null),
'confirmed_delivery_date' => MagoClient::toMysqlDate($r['ConfirmedDeliveryDate'] ?? null),
'compulsory_delivery_date' => MagoClient::toMysqlDate($r['CompulsoryDeliveryDate'] ?? null),
'customer' => $s('Customer', 20),
'invoicing_customer' => $s('InvoicingCustomer', 20),
'salesperson' => $s('Salesperson', 20),
'area_manager' => $s('AreaManager', 20),
'area' => $s('Area', 20),
'payment' => $s('Payment', 20),
'currency' => $s('Currency', 8),
'our_reference' => $s('OurReference', 150),
'your_reference' => $s('YourReference', 150),
'transport' => $s('Transport', 20),
'port' => $s('Port', 20),
'package' => $s('Package', 40),
'storage' => $s('StoragePhase1', 30),
'job' => $s('Job', 30),
'delivered' => MagoClient::toBool($r['Delivered'] ?? false),
'invoiced' => MagoClient::toBool($r['Invoiced'] ?? false),
'cancelled' => MagoClient::toBool($r['Cancelled'] ?? false),
'is_blocked' => MagoClient::toBool($r['IsBlocked'] ?? false),
'printed' => MagoClient::toBool($r['Printed'] ?? false),
'archived' => MagoClient::toBool($r['Archived'] ?? false),
'open_order' => MagoClient::toBool($r['OpenOrder'] ?? false),
'priority' => (int)($r['Priority'] ?? 0),
'notes' => trim((string)($r['Notes'] ?? '')) ?: null,
'tb_created' => MagoClient::toMysqlDate($r['TbCreated'] ?? null, true),
'tb_modified' => MagoClient::toMysqlDate($r['TbModified'] ?? null, true),
'raw_json' => json_encode($r, JSON_UNESCAPED_UNICODE),
];
}
/* =========================================================
* RIGHE ORDINE (MA_SaleOrdDetails → mago_sale_ord_detail)
* ========================================================= */
/**
* Le righe hanno OrderDate, quindi si filtrano come le testate.
* Vengono salvate solo le righe di ordini già presenti in locale.
* Nelle letture complete le righe degli ordini del periodo vengono
* prima cancellate, così spariscono anche le righe eliminate in Mago.
*/
public function syncSaleOrderDetails($from, $full = false, $testMode = false)
{
$t0 = microtime(true);
$state = $this->getState(self::ENTITY_DETAILS);
// ordini locali del periodo (per scartare righe di ordini non sincronizzati)
$st = $this->pdo->prepare("SELECT sale_ord_id FROM mago_sale_ord WHERE order_date >= ?");
$st->execute([$from]);
$localIds = array_flip(array_map('intval', $st->fetchAll(PDO::FETCH_COLUMN)));
if (!$localIds) {
return ['records' => 0, 'salvate' => 0, 'incrementale' => false, 'ms' => $this->ms($t0)];
}
$incremental = !$full && !$testMode && !empty($state['last_tb_modified']);
$filters = [['key' => 'OrderDate', 'value' => $from . 'T00:00:00', 'operator' => '>=']];
if ($incremental) {
$filters[] = [
'key' => 'TbModified',
'value' => date('Y-m-d\TH:i:s', strtotime($state['last_tb_modified'])),
'operator' => '>=',
];
}
if ($testMode) {
// prova: leggo solo le righe nel periodo degli ordini importati
$range = $this->pdo->query("SELECT MIN(order_date), MAX(order_date) FROM mago_sale_ord WHERE order_date >= " . $this->pdo->quote($from))->fetch(PDO::FETCH_NUM);
$filters = [
['key' => 'OrderDate', 'value' => $range[0] . 'T00:00:00', 'operator' => '>='],
['key' => 'OrderDate', 'value' => $range[1] . 'T00:00:00', 'operator' => '<='],
];
}
$this->log('Righe: ' . ($incremental ? 'incrementale da ' . $state['last_tb_modified'] : 'completa dal ' . $from));
$stmt = $this->pdo->prepare("
INSERT INTO mago_sale_ord_detail (
sale_ord_id, sub_id, line, position, line_type, item, description, uom,
qty, delivered_qty, invoiced_qty, unit_value, discount_amount, taxable_amount, total_amount,
expected_delivery_date, confirmed_delivery_date, delivered, invoiced, cancelled,
customer, order_date, internal_ord_no, notes, tb_modified, synced_at
) VALUES (
:sale_ord_id, :sub_id, :line, :position, :line_type, :item, :description, :uom,
:qty, :delivered_qty, :invoiced_qty, :unit_value, :discount_amount, :taxable_amount, :total_amount,
:expected, :confirmed, :delivered, :invoiced, :cancelled,
:customer, :order_date, :internal_ord_no, :notes, :tb_modified, NOW()
)
ON DUPLICATE KEY UPDATE
line = VALUES(line), position = VALUES(position), line_type = VALUES(line_type),
item = VALUES(item), description = VALUES(description), uom = VALUES(uom),
qty = VALUES(qty), delivered_qty = VALUES(delivered_qty), invoiced_qty = VALUES(invoiced_qty),
unit_value = VALUES(unit_value), discount_amount = VALUES(discount_amount),
taxable_amount = VALUES(taxable_amount), total_amount = VALUES(total_amount),
expected_delivery_date = VALUES(expected_delivery_date),
confirmed_delivery_date = VALUES(confirmed_delivery_date),
delivered = VALUES(delivered), invoiced = VALUES(invoiced), cancelled = VALUES(cancelled),
customer = VALUES(customer), order_date = VALUES(order_date),
internal_ord_no = VALUES(internal_ord_no), notes = VALUES(notes),
tb_modified = VALUES(tb_modified), synced_at = NOW()
");
$read = 0;
$saved = 0;
$maxModified = $state['last_tb_modified'] ?? null;
$this->pdo->beginTransaction();
try {
if (!$incremental) {
// lettura completa: riparto pulito per gli ordini del periodo
$this->pdo->prepare("
DELETE d FROM mago_sale_ord_detail d
JOIN mago_sale_ord o ON o.sale_ord_id = d.sale_ord_id
WHERE o.order_date >= ?
")->execute([$from]);
}
foreach ($this->mago->iterate('MA_SaleOrdDetails', ['filters' => $filters]) as $r) {
$read++;
$id = (int)($r['SaleOrdId'] ?? 0);
$mod = MagoClient::toMysqlDate($r['TbModified'] ?? null, true);
if ($mod && (!$maxModified || $mod > $maxModified)) $maxModified = $mod;
if (!isset($localIds[$id])) continue;
$s = function ($k, $len) use ($r) {
return mb_substr(trim((string)($r[$k] ?? '')), 0, $len);
};
$d = function ($k, $dec = 2) use ($r) {
return round(MagoClient::toDecimal($r[$k] ?? 0), $dec);
};
$stmt->execute([
'sale_ord_id' => $id,
'sub_id' => (int)($r['SubId'] ?? $r['Line'] ?? 0),
'line' => (int)($r['Line'] ?? 0),
'position' => (int)($r['Position'] ?? 0),
'line_type' => (int)($r['LineType'] ?? 0),
'item' => $s('Item', 30),
'description' => $s('Description', 255),
'uom' => $s('UoM', 10),
'qty' => $d('Qty', 3),
'delivered_qty' => $d('DeliveredQty', 3),
'invoiced_qty' => $d('InvoicedQty', 3),
'unit_value' => $d('UnitValue', 5),
'discount_amount' => $d('DiscountAmount'),
'taxable_amount' => $d('TaxableAmount'),
'total_amount' => $d('TotalAmount'),
'expected' => MagoClient::toMysqlDate($r['ExpectedDeliveryDate'] ?? null),
'confirmed' => MagoClient::toMysqlDate($r['ConfirmedDeliveryDate'] ?? null),
'delivered' => MagoClient::toBool($r['Delivered'] ?? false),
'invoiced' => MagoClient::toBool($r['Invoiced'] ?? false),
'cancelled' => MagoClient::toBool($r['Cancelled'] ?? false),
'customer' => $s('Customer', 20),
'order_date' => MagoClient::toMysqlDate($r['OrderDate'] ?? null),
'internal_ord_no' => $s('InternalOrdNo', 30),
'notes' => trim((string)($r['Notes'] ?? '')) ?: null,
'tb_modified' => $mod,
]);
$saved++;
if ($read % 200 === 0) {
$this->pdo->commit();
$this->pdo->beginTransaction();
$this->log(" righe lette: $read");
}
}
$this->refreshLineDates($from);
$this->pdo->commit();
} catch (Throwable $e) {
if ($this->pdo->inTransaction()) $this->pdo->rollBack();
$this->saveState(self::ENTITY_DETAILS, null, 'errore', $e->getMessage(), $read, $t0, false);
if (!$incremental) {
// lettura completa interrotta: al prossimo giro si riparte da capo
$this->pdo->prepare("UPDATE mago_sync_state SET last_tb_modified = NULL WHERE entity = ?")
->execute([self::ENTITY_DETAILS]);
}
throw $e;
}
$this->saveState(self::ENTITY_DETAILS, $testMode ? null : $maxModified, 'ok', "Lette $read righe, salvate $saved", $read, $t0, !$testMode);
$this->log("Righe: lette $read, salvate $saved");
return ['records' => $read, 'salvate' => $saved, 'incrementale' => $incremental, 'ms' => $this->ms($t0)];
}
/**
* Riporta sulla testata la prima data di consegna delle righe ancora da consegnare
* (se tutte consegnate: la prima data tra tutte le righe).
* La data confermata vale solo se successiva alla data ordine.
*/
private function refreshLineDates($from)
{
$this->pdo->prepare("
UPDATE mago_sale_ord o
LEFT JOIN (
SELECT sale_ord_id,
COALESCE(
MIN(CASE WHEN delivered = 0 AND cancelled = 0 THEN expected_delivery_date END),
MIN(expected_delivery_date)
) AS exp_date,
COALESCE(
MIN(CASE WHEN delivered = 0 AND cancelled = 0 AND confirmed_delivery_date > order_date THEN confirmed_delivery_date END),
MIN(CASE WHEN confirmed_delivery_date > order_date THEN confirmed_delivery_date END)
) AS conf_date,
COUNT(*) AS n
FROM mago_sale_ord_detail
WHERE item <> '' OR qty <> 0
GROUP BY sale_ord_id
) d ON d.sale_ord_id = o.sale_ord_id
SET o.line_expected_date = d.exp_date,
o.line_confirmed_date = d.conf_date,
o.lines_count = COALESCE(d.n, 0)
WHERE o.order_date >= ?
")->execute([$from]);
}
/* =========================================================
* IMPORTI (MA_SaleOrdSummary → colonne importo di mago_sale_ord)
* ========================================================= */
/**
* MA_SaleOrdSummary non ha la data ordine: filtro per TbModified, che è
* l'unico filtro data verificato. Parto dalla creazione del primo ordine
* locale del periodo (i totali nascono insieme all'ordine) e aggiorno
* solo gli ordini già presenti in locale.
*/
public function syncSaleOrderSummaries($from, $full = false, $testMode = false)
{
$t0 = microtime(true);
$state = $this->getState(self::ENTITY_SUMMARY);
$info = $this->pdo->query(
"
SELECT MIN(COALESCE(tb_created, order_date)) AS first_created,
SUM(summary_tb_modified IS NULL) AS missing,
COUNT(*) AS n
FROM mago_sale_ord
WHERE order_date >= " . $this->pdo->quote($from)
)->fetch(PDO::FETCH_ASSOC);
if (empty($info['n'])) {
return ['records' => 0, 'aggiornati' => 0, 'incrementale' => false, 'ms' => $this->ms($t0)];
}
// Incrementale solo se tutti gli ordini locali hanno già i totali
$incremental = !$full && !$testMode && !empty($state['last_tb_modified']) && (int)$info['missing'] === 0;
$since = $incremental
? $state['last_tb_modified']
: date('Y-m-d 00:00:00', strtotime($info['first_created'] . ' -1 day'));
$this->log('Importi: ' . ($incremental ? 'incrementale' : 'completa') . ' da ' . $since);
$filters = [['key' => 'TbModified', 'value' => date('Y-m-d\TH:i:s', strtotime($since)), 'operator' => '>=']];
$stmt = $this->pdo->prepare("
UPDATE mago_sale_ord SET
taxable_amount = :taxable,
tax_amount = :tax,
total_amount = :total,
goods_amount = :goods,
discounts = :discounts,
charges = :charges,
summary_tb_modified = :mod
WHERE sale_ord_id = :id
");
$read = 0;
$updated = 0;
$maxModified = $state['last_tb_modified'] ?? null;
$this->pdo->beginTransaction();
try {
foreach ($this->mago->iterate('MA_SaleOrdSummary', ['filters' => $filters]) as $r) {
$read++;
$mod = MagoClient::toMysqlDate($r['TbModified'] ?? null, true);
$d = function ($k) use ($r) {
return round(MagoClient::toDecimal($r[$k] ?? 0), 2);
};
$stmt->execute([
'taxable' => $d('TaxableAmount'),
'tax' => $d('TaxAmount'),
'total' => $d('TotalAmount'),
'goods' => $d('GoodsAmount'),
'discounts' => $d('Discounts'),
'charges' => $d('ShippingCharges') + $d('PackagingCharges') + $d('CollectionCharges')
+ $d('StampsCharges') + $d('AdditionalCharges') + $d('InsuranceCharges'),
'mod' => $mod ?: date('Y-m-d H:i:s'),
'id' => (int)($r['SaleOrdId'] ?? 0),
]);
$updated += $stmt->rowCount() > 0 ? 1 : 0;
if ($mod && (!$maxModified || $mod > $maxModified)) $maxModified = $mod;
if ($read % 200 === 0) {
$this->pdo->commit();
$this->pdo->beginTransaction();
$this->log(" totali letti: $read");
}
}
$this->pdo->commit();
} catch (Throwable $e) {
if ($this->pdo->inTransaction()) $this->pdo->rollBack();
$this->saveState(self::ENTITY_SUMMARY, null, 'errore', $e->getMessage(), $read, $t0, false);
throw $e;
}
$this->saveState(self::ENTITY_SUMMARY, $testMode ? null : $maxModified, 'ok', "Letti $read totali, $updated ordini aggiornati", $read, $t0, !$testMode);
$this->log("Importi: letti $read, ordini aggiornati $updated");
return ['records' => $read, 'aggiornati' => $updated, 'incrementale' => $incremental, 'ms' => $this->ms($t0)];
}
/* =========================================================
* ANAGRAFICA CLIENTI (per decodificare i codici cliente)
* ========================================================= */
public function syncCustomers($full = false)
{
$t0 = microtime(true);
$state = $this->getState(self::ENTITY_CUSTOMER);
$filters = [['key' => 'CustSuppType', 'value' => (string)self::CUSTOMER_TYPE, 'operator' => '=']];
$incremental = !$full && !empty($state['last_tb_modified']);
if ($incremental) {
$filters[] = [
'key' => 'TbModified',
'value' => date('Y-m-d\TH:i:s', strtotime($state['last_tb_modified'])),
'operator' => '>=',
];
}
$this->log('Clienti: ' . ($incremental ? 'incrementale' : 'completa'));
$stmt = $this->pdo->prepare("
INSERT INTO mago_cust_supp
(cust_supp_type, cust_supp, company_name, city, county, region, country, iso_country_code, disabled, tb_modified, synced_at)
VALUES
(:t, :c, :name, :city, :county, :region, :country, :iso, :disabled, :mod, NOW())
ON DUPLICATE KEY UPDATE
company_name = VALUES(company_name), city = VALUES(city), county = VALUES(county),
region = VALUES(region), country = VALUES(country), iso_country_code = VALUES(iso_country_code),
disabled = VALUES(disabled), tb_modified = VALUES(tb_modified), synced_at = NOW()
");
$count = 0;
$maxModified = $state['last_tb_modified'] ?? null;
$this->pdo->beginTransaction();
try {
foreach ($this->mago->iterate('MA_CustSupp', ['filters' => $filters]) as $r) {
$mod = MagoClient::toMysqlDate($r['TbModified'] ?? null, true);
$stmt->execute([
't' => (int)($r['CustSuppType'] ?? self::CUSTOMER_TYPE),
'c' => mb_substr(trim((string)($r['CustSupp'] ?? '')), 0, 20),
'name' => mb_substr(trim((string)($r['CompanyName'] ?? '')), 0, 200),
'city' => mb_substr(trim((string)($r['City'] ?? '')), 0, 100),
'county' => mb_substr(trim((string)($r['County'] ?? '')), 0, 10),
'region' => mb_substr(trim((string)($r['Region'] ?? '')), 0, 60),
'country' => mb_substr(trim((string)($r['Country'] ?? '')), 0, 60),
'iso' => mb_substr(trim((string)($r['ISOCountryCode'] ?? '')), 0, 4),
'disabled' => MagoClient::toBool($r['Disabled'] ?? false),
'mod' => $mod,
]);
$count++;
if ($mod && (!$maxModified || $mod > $maxModified)) $maxModified = $mod;
}
$this->pdo->commit();
} catch (Throwable $e) {
if ($this->pdo->inTransaction()) $this->pdo->rollBack();
$this->saveState(self::ENTITY_CUSTOMER, null, 'errore', $e->getMessage(), $count, $t0, false);
throw $e;
}
$this->saveState(self::ENTITY_CUSTOMER, $maxModified, 'ok', "$count clienti", $count, $t0, true);
$this->log("Clienti sincronizzati: $count");
return ['records' => $count, 'incrementale' => $incremental, 'ms' => $this->ms($t0)];
}
/* =========================================================
* STATO / LOCK / LOG
* ========================================================= */
private function getState($entity)
{
$st = $this->pdo->prepare("SELECT * FROM mago_sync_state WHERE entity = ?");
$st->execute([$entity]);
return $st->fetch(PDO::FETCH_ASSOC) ?: [];
}
private function saveState($entity, $lastModified, $status, $message, $records, $t0, $updateCursor)
{
$sql = "INSERT INTO mago_sync_state (entity, last_tb_modified, last_run_at, last_status, last_message, records_last_run, duration_ms)
VALUES (:e, :m, NOW(), :s, :msg, :r, :d)
ON DUPLICATE KEY UPDATE
" . ($updateCursor ? "last_tb_modified = COALESCE(VALUES(last_tb_modified), last_tb_modified)," : "") . "
last_run_at = NOW(),
last_status = VALUES(last_status),
last_message = VALUES(last_message),
records_last_run = VALUES(records_last_run),
duration_ms = VALUES(duration_ms)";
$this->pdo->prepare($sql)->execute([
'e' => $entity,
'm' => $updateCursor ? $lastModified : null,
's' => $status,
'msg' => mb_substr((string)$message, 0, 2000),
'r' => (int)$records,
'd' => (int)round((microtime(true) - $t0) * 1000),
]);
}
private function acquireLock()
{
return (int)$this->pdo->query("SELECT GET_LOCK('mago_sync', 0)")->fetchColumn() === 1;
}
private function releaseLock()
{
try {
$this->pdo->query("SELECT RELEASE_LOCK('mago_sync')");
} catch (Throwable $e) {
}
}
private function ms($t0)
{
return (int)round((microtime(true) - $t0) * 1000);
}
private function log($msg)
{
if ($this->logger) {
call_user_func($this->logger, '[' . date('H:i:s') . '] ' . $msg);
}
}
}