4390 lines
163 KiB
PHP
4390 lines
163 KiB
PHP
<?php
|
||
/**
|
||
* MSIN API ChronoTrack — Phase 2a : lecture MS1, diff, push manuel entries.
|
||
*/
|
||
|
||
// MSIN-4574 — vitesse 2026 : 1 POST CT = jusqu’à 100 entries AVEC dossard.
|
||
// 2e passe bib uniquement pour conflits (pas 1300 PUT systématiques).
|
||
define('MSIN_API_CHRONOTRACK_SYNC_BATCH_SIZE', 100);
|
||
define('MSIN_API_CHRONOTRACK_SYNC_CHUNK_SIZE', 100);
|
||
define('MSIN_API_CHRONOTRACK_SYNC_BIB_CHUNK_SIZE', 40);
|
||
|
||
/**
|
||
* MSIN-4328 — persiste ct_entry_id sur resultats_participants.
|
||
* Important : figé par_maj (sinon ON UPDATE CURRENT_TIMESTAMP → faux différentiel).
|
||
*/
|
||
function fxChronotrackApiSyncSaveCtEntryId($intParId, $strCtEntryId) {
|
||
global $objDatabase;
|
||
|
||
$intParId = intval($intParId);
|
||
$strCtEntryId = preg_replace('/[^0-9]/', '', (string)$strCtEntryId);
|
||
if ($intParId <= 0 || $strCtEntryId === '') {
|
||
return false;
|
||
}
|
||
|
||
// par_maj = par_maj : empêche MySQL d’avancer le timestamp auto sur cette ligne
|
||
$sql = "UPDATE resultats_participants SET ct_entry_id = " . intval($strCtEntryId)
|
||
. ", par_maj = par_maj"
|
||
. " WHERE par_id = " . $intParId
|
||
. " AND (ct_entry_id IS NULL OR ct_entry_id <> " . intval($strCtEntryId) . ")";
|
||
return (bool)$objDatabase->fxQuery($sql);
|
||
}
|
||
|
||
/**
|
||
* MSIN-4328 — mappe la réponse POST/GET entry → par_id (via external_id).
|
||
*/
|
||
function fxChronotrackApiSyncApplyEntryIdsFromApiJson($mixJson, array $tabMeta) {
|
||
if (!is_array($mixJson) || count($tabMeta) === 0) {
|
||
return 0;
|
||
}
|
||
|
||
$tabByExt = array();
|
||
foreach ($tabMeta as $arrM) {
|
||
$strExt = trim((string)($arrM['external_id'] ?? ''));
|
||
$intParId = intval($arrM['par_id'] ?? 0);
|
||
if ($strExt !== '' && $intParId > 0) {
|
||
$tabByExt[$strExt] = $intParId;
|
||
}
|
||
}
|
||
if (count($tabByExt) === 0) {
|
||
return 0;
|
||
}
|
||
|
||
$arrEntities = fxChronotrackApiNormalizeEntityList($mixJson, 'entry');
|
||
$intSaved = 0;
|
||
foreach ($arrEntities as $arrEnt) {
|
||
if (!is_array($arrEnt)) {
|
||
continue;
|
||
}
|
||
$strStatus = strtoupper(trim((string)(
|
||
$arrEnt['status'] ?? $arrEnt['entry_status'] ?? ''
|
||
)));
|
||
if ($strStatus === 'FAILURE') {
|
||
continue;
|
||
}
|
||
$strEntryId = fxChronotrackApiEntityId($arrEnt);
|
||
$strExt = fxChronotrackApiEntityExternalId($arrEnt);
|
||
if ($strExt === '' && isset($arrEnt['entry_external_id'])) {
|
||
$strExt = trim((string)$arrEnt['entry_external_id']);
|
||
}
|
||
if ($strEntryId === '' || $strExt === '' || !isset($tabByExt[$strExt])) {
|
||
continue;
|
||
}
|
||
if (fxChronotrackApiSyncSaveCtEntryId($tabByExt[$strExt], $strEntryId)) {
|
||
$intSaved++;
|
||
}
|
||
}
|
||
return $intSaved;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4328 — URL admin ChronoTrack (fiche athlète).
|
||
*/
|
||
function fxChronotrackApiAdminEntryUrl($intCtEventId, $strCtEntryId) {
|
||
$intCtEventId = intval($intCtEventId);
|
||
$strCtEntryId = preg_replace('/[^0-9]/', '', (string)$strCtEntryId);
|
||
if ($intCtEventId <= 0 || $strCtEntryId === '') {
|
||
return '';
|
||
}
|
||
return 'https://admin.chronotrack.com/admin/entry/index/eventID/'
|
||
. $intCtEventId . '?entryID=' . $strCtEntryId;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4574 — table ISO-3166 alpha-2 → alpha-3 (alignée sql/MSIN-inscriptions-pays-iso3.sql).
|
||
* Enrichie au runtime avec pay_iso3 en BD si la colonne existe.
|
||
*/
|
||
function fxChronotrackApiSyncIso2ToIso3Map() {
|
||
static $tabMap = null;
|
||
if ($tabMap !== null) {
|
||
return $tabMap;
|
||
}
|
||
$tabMap = array(
|
||
'AD' => 'AND',
|
||
'AE' => 'ARE',
|
||
'AF' => 'AFG',
|
||
'AG' => 'ATG',
|
||
'AI' => 'AIA',
|
||
'AL' => 'ALB',
|
||
'AM' => 'ARM',
|
||
'AO' => 'AGO',
|
||
'AQ' => 'ATA',
|
||
'AR' => 'ARG',
|
||
'AS' => 'ASM',
|
||
'AT' => 'AUT',
|
||
'AU' => 'AUS',
|
||
'AW' => 'ABW',
|
||
'AX' => 'ALA',
|
||
'AZ' => 'AZE',
|
||
'BA' => 'BIH',
|
||
'BB' => 'BRB',
|
||
'BD' => 'BGD',
|
||
'BE' => 'BEL',
|
||
'BF' => 'BFA',
|
||
'BG' => 'BGR',
|
||
'BH' => 'BHR',
|
||
'BI' => 'BDI',
|
||
'BJ' => 'BEN',
|
||
'BL' => 'BLM',
|
||
'BM' => 'BMU',
|
||
'BN' => 'BRN',
|
||
'BO' => 'BOL',
|
||
'BQ' => 'BES',
|
||
'BR' => 'BRA',
|
||
'BS' => 'BHS',
|
||
'BT' => 'BTN',
|
||
'BV' => 'BVT',
|
||
'BW' => 'BWA',
|
||
'BY' => 'BLR',
|
||
'BZ' => 'BLZ',
|
||
'CA' => 'CAN',
|
||
'CC' => 'CCK',
|
||
'CD' => 'COD',
|
||
'CF' => 'CAF',
|
||
'CG' => 'COG',
|
||
'CH' => 'CHE',
|
||
'CI' => 'CIV',
|
||
'CK' => 'COK',
|
||
'CL' => 'CHL',
|
||
'CM' => 'CMR',
|
||
'CN' => 'CHN',
|
||
'CO' => 'COL',
|
||
'CR' => 'CRI',
|
||
'CU' => 'CUB',
|
||
'CV' => 'CPV',
|
||
'CW' => 'CUW',
|
||
'CX' => 'CXR',
|
||
'CY' => 'CYP',
|
||
'CZ' => 'CZE',
|
||
'DE' => 'DEU',
|
||
'DJ' => 'DJI',
|
||
'DK' => 'DNK',
|
||
'DM' => 'DMA',
|
||
'DO' => 'DOM',
|
||
'DZ' => 'DZA',
|
||
'EC' => 'ECU',
|
||
'EE' => 'EST',
|
||
'EG' => 'EGY',
|
||
'EH' => 'ESH',
|
||
'ER' => 'ERI',
|
||
'ES' => 'ESP',
|
||
'ET' => 'ETH',
|
||
'FI' => 'FIN',
|
||
'FJ' => 'FJI',
|
||
'FK' => 'FLK',
|
||
'FM' => 'FSM',
|
||
'FO' => 'FRO',
|
||
'FR' => 'FRA',
|
||
'GA' => 'GAB',
|
||
'GB' => 'GBR',
|
||
'GD' => 'GRD',
|
||
'GE' => 'GEO',
|
||
'GF' => 'GUF',
|
||
'GG' => 'GGY',
|
||
'GH' => 'GHA',
|
||
'GI' => 'GIB',
|
||
'GL' => 'GRL',
|
||
'GM' => 'GMB',
|
||
'GN' => 'GIN',
|
||
'GP' => 'GLP',
|
||
'GQ' => 'GNQ',
|
||
'GR' => 'GRC',
|
||
'GS' => 'SGS',
|
||
'GT' => 'GTM',
|
||
'GU' => 'GUM',
|
||
'GW' => 'GNB',
|
||
'GY' => 'GUY',
|
||
'HK' => 'HKG',
|
||
'HM' => 'HMD',
|
||
'HN' => 'HND',
|
||
'HR' => 'HRV',
|
||
'HT' => 'HTI',
|
||
'HU' => 'HUN',
|
||
'ID' => 'IDN',
|
||
'IE' => 'IRL',
|
||
'IL' => 'ISR',
|
||
'IM' => 'IMN',
|
||
'IN' => 'IND',
|
||
'IO' => 'IOT',
|
||
'IQ' => 'IRQ',
|
||
'IR' => 'IRN',
|
||
'IS' => 'ISL',
|
||
'IT' => 'ITA',
|
||
'JE' => 'JEY',
|
||
'JM' => 'JAM',
|
||
'JO' => 'JOR',
|
||
'JP' => 'JPN',
|
||
'KE' => 'KEN',
|
||
'KG' => 'KGZ',
|
||
'KH' => 'KHM',
|
||
'KI' => 'KIR',
|
||
'KM' => 'COM',
|
||
'KN' => 'KNA',
|
||
'KP' => 'PRK',
|
||
'KR' => 'KOR',
|
||
'KW' => 'KWT',
|
||
'KY' => 'CYM',
|
||
'KZ' => 'KAZ',
|
||
'LA' => 'LAO',
|
||
'LB' => 'LBN',
|
||
'LC' => 'LCA',
|
||
'LI' => 'LIE',
|
||
'LK' => 'LKA',
|
||
'LR' => 'LBR',
|
||
'LS' => 'LSO',
|
||
'LT' => 'LTU',
|
||
'LU' => 'LUX',
|
||
'LV' => 'LVA',
|
||
'LY' => 'LBY',
|
||
'MA' => 'MAR',
|
||
'MC' => 'MCO',
|
||
'MD' => 'MDA',
|
||
'ME' => 'MNE',
|
||
'MF' => 'MAF',
|
||
'MG' => 'MDG',
|
||
'MH' => 'MHL',
|
||
'MK' => 'MKD',
|
||
'ML' => 'MLI',
|
||
'MM' => 'MMR',
|
||
'MN' => 'MNG',
|
||
'MO' => 'MAC',
|
||
'MP' => 'MNP',
|
||
'MQ' => 'MTQ',
|
||
'MR' => 'MRT',
|
||
'MS' => 'MSR',
|
||
'MT' => 'MLT',
|
||
'MU' => 'MUS',
|
||
'MV' => 'MDV',
|
||
'MW' => 'MWI',
|
||
'MX' => 'MEX',
|
||
'MY' => 'MYS',
|
||
'MZ' => 'MOZ',
|
||
'NA' => 'NAM',
|
||
'NC' => 'NCL',
|
||
'NE' => 'NER',
|
||
'NF' => 'NFK',
|
||
'NG' => 'NGA',
|
||
'NI' => 'NIC',
|
||
'NL' => 'NLD',
|
||
'NO' => 'NOR',
|
||
'NP' => 'NPL',
|
||
'NR' => 'NRU',
|
||
'NU' => 'NIU',
|
||
'NZ' => 'NZL',
|
||
'OM' => 'OMN',
|
||
'PA' => 'PAN',
|
||
'PE' => 'PER',
|
||
'PF' => 'PYF',
|
||
'PG' => 'PNG',
|
||
'PH' => 'PHL',
|
||
'PK' => 'PAK',
|
||
'PL' => 'POL',
|
||
'PM' => 'SPM',
|
||
'PN' => 'PCN',
|
||
'PR' => 'PRI',
|
||
'PS' => 'PSE',
|
||
'PT' => 'PRT',
|
||
'PW' => 'PLW',
|
||
'PY' => 'PRY',
|
||
'QA' => 'QAT',
|
||
'RE' => 'REU',
|
||
'RO' => 'ROU',
|
||
'RS' => 'SRB',
|
||
'RU' => 'RUS',
|
||
'RW' => 'RWA',
|
||
'SA' => 'SAU',
|
||
'SB' => 'SLB',
|
||
'SC' => 'SYC',
|
||
'SD' => 'SDN',
|
||
'SE' => 'SWE',
|
||
'SG' => 'SGP',
|
||
'SH' => 'SHN',
|
||
'SI' => 'SVN',
|
||
'SJ' => 'SJM',
|
||
'SK' => 'SVK',
|
||
'SL' => 'SLE',
|
||
'SM' => 'SMR',
|
||
'SN' => 'SEN',
|
||
'SO' => 'SOM',
|
||
'SR' => 'SUR',
|
||
'SS' => 'SSD',
|
||
'ST' => 'STP',
|
||
'SV' => 'SLV',
|
||
'SX' => 'SXM',
|
||
'SY' => 'SYR',
|
||
'SZ' => 'SWZ',
|
||
'TC' => 'TCA',
|
||
'TD' => 'TCD',
|
||
'TF' => 'ATF',
|
||
'TG' => 'TGO',
|
||
'TH' => 'THA',
|
||
'TJ' => 'TJK',
|
||
'TK' => 'TKL',
|
||
'TL' => 'TLS',
|
||
'TM' => 'TKM',
|
||
'TN' => 'TUN',
|
||
'TO' => 'TON',
|
||
'TR' => 'TUR',
|
||
'TT' => 'TTO',
|
||
'TV' => 'TUV',
|
||
'TW' => 'TWN',
|
||
'TZ' => 'TZA',
|
||
'UA' => 'UKR',
|
||
'UG' => 'UGA',
|
||
'UM' => 'UMI',
|
||
'US' => 'USA',
|
||
'UY' => 'URY',
|
||
'UZ' => 'UZB',
|
||
'VA' => 'VAT',
|
||
'VC' => 'VCT',
|
||
'VE' => 'VEN',
|
||
'VG' => 'VGB',
|
||
'VI' => 'VIR',
|
||
'VN' => 'VNM',
|
||
'VU' => 'VUT',
|
||
'WF' => 'WLF',
|
||
'WS' => 'WSM',
|
||
'YE' => 'YEM',
|
||
'YT' => 'MYT',
|
||
'ZA' => 'ZAF',
|
||
'ZM' => 'ZMB',
|
||
'ZW' => 'ZWE',
|
||
'AN' => 'ANT',
|
||
'UK' => 'GBR',
|
||
'XK' => 'XKX',
|
||
);
|
||
// Enrichir / corriger depuis BD (source de vérité après script pay_iso3)
|
||
if (function_exists('fxChronotrackApiSyncPaysHasIso3Column') && fxChronotrackApiSyncPaysHasIso3Column()) {
|
||
global $objDatabase;
|
||
if (isset($objDatabase) && is_object($objDatabase)) {
|
||
$arrRows = $objDatabase->fxGetResults(
|
||
"SELECT pay_iso, pay_iso3 FROM inscriptions_pays"
|
||
. " WHERE pay_iso IS NOT NULL AND TRIM(pay_iso) <> ''"
|
||
. " AND pay_iso3 IS NOT NULL AND TRIM(pay_iso3) <> ''"
|
||
);
|
||
if (is_array($arrRows)) {
|
||
for ($i = 1; $i <= count($arrRows); $i++) {
|
||
$str2 = strtoupper(trim((string)($arrRows[$i]['pay_iso'] ?? '')));
|
||
$str3 = strtoupper(trim((string)($arrRows[$i]['pay_iso3'] ?? '')));
|
||
if (strlen($str2) === 2 && strlen($str3) === 3) {
|
||
$tabMap[$str2] = $str3;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return $tabMap;
|
||
}
|
||
|
||
function fxChronotrackApiSyncIso2ToIso3($strIso2) {
|
||
$strIso2 = strtoupper(trim((string)$strIso2));
|
||
if ($strIso2 === '') {
|
||
return '';
|
||
}
|
||
if (strlen($strIso2) === 3 && ctype_alpha($strIso2)) {
|
||
return $strIso2;
|
||
}
|
||
if (strlen($strIso2) !== 2) {
|
||
return '';
|
||
}
|
||
$tabMap = fxChronotrackApiSyncIso2ToIso3Map();
|
||
return isset($tabMap[$strIso2]) ? $tabMap[$strIso2] : '';
|
||
}
|
||
|
||
/**
|
||
* MSIN-4574 — ISO-2 pays (pay_iso BD).
|
||
* Preuve calibrage 2026-08-11 : CT Athlete Info / export stockent location_country + COUNTRY_CODE = « CA » (pas CAN).
|
||
*/
|
||
function fxChronotrackApiSyncResolveCountryIso2(array $arrRow) {
|
||
$strIso2 = strtoupper(trim((string)($arrRow['country_iso2'] ?? '')));
|
||
if (strlen($strIso2) === 2 && ctype_alpha($strIso2)) {
|
||
return $strIso2;
|
||
}
|
||
return '';
|
||
}
|
||
|
||
/**
|
||
* MSIN-4574 — ISO-3 (pay_iso3 / map) — utile logs / exports résultats CTLIVE.
|
||
* Pour l’écriture entry Athlete Info : préférer ResolveCountryIso2().
|
||
*/
|
||
function fxChronotrackApiSyncResolveCountryCode(array $arrRow) {
|
||
$strIso3 = strtoupper(trim((string)($arrRow['country_iso3'] ?? '')));
|
||
if (strlen($strIso3) === 3 && ctype_alpha($strIso3)) {
|
||
return $strIso3;
|
||
}
|
||
$strIso2 = fxChronotrackApiSyncResolveCountryIso2($arrRow);
|
||
if ($strIso2 === '') {
|
||
$strIso2 = strtoupper(trim((string)($arrRow['country_iso2'] ?? '')));
|
||
}
|
||
if (strlen($strIso2) === 3 && ctype_alpha($strIso2)) {
|
||
return $strIso2;
|
||
}
|
||
return fxChronotrackApiSyncIso2ToIso3($strIso2);
|
||
}
|
||
|
||
/**
|
||
* MSIN-4574 — libellé pays (country_name CT / export COUNTRY_NAME).
|
||
*/
|
||
function fxChronotrackApiSyncResolveCountryLabel(array $arrRow) {
|
||
$strName = trim((string)($arrRow['country_name'] ?? ''));
|
||
if ($strName !== '') {
|
||
return $strName;
|
||
}
|
||
$strIso2 = fxChronotrackApiSyncResolveCountryIso2($arrRow);
|
||
static $tabByIso2 = array(
|
||
'CA' => 'Canada',
|
||
'US' => 'United States',
|
||
'MX' => 'Mexico',
|
||
'FR' => 'France',
|
||
'GB' => 'United Kingdom',
|
||
'BE' => 'Belgium',
|
||
'CH' => 'Switzerland',
|
||
'DE' => 'Germany',
|
||
'ES' => 'Spain',
|
||
'IT' => 'Italy',
|
||
'AU' => 'Australia',
|
||
'NZ' => 'New Zealand',
|
||
'BR' => 'Brazil',
|
||
'JP' => 'Japan',
|
||
'CN' => 'China',
|
||
'NL' => 'Netherlands',
|
||
'IE' => 'Ireland',
|
||
'PT' => 'Portugal',
|
||
'PL' => 'Poland',
|
||
'AT' => 'Austria',
|
||
'SE' => 'Sweden',
|
||
'NO' => 'Norway',
|
||
'DK' => 'Denmark',
|
||
'FI' => 'Finland',
|
||
);
|
||
if ($strIso2 !== '' && isset($tabByIso2[$strIso2])) {
|
||
return $tabByIso2[$strIso2];
|
||
}
|
||
$strIso3 = fxChronotrackApiSyncResolveCountryCode($arrRow);
|
||
static $tabByIso3 = array(
|
||
'CAN' => 'Canada',
|
||
'USA' => 'United States',
|
||
'FRA' => 'France',
|
||
);
|
||
if ($strIso3 !== '' && isset($tabByIso3[$strIso3])) {
|
||
return $tabByIso3[$strIso3];
|
||
}
|
||
return $strIso2 !== '' ? $strIso2 : $strIso3;
|
||
}
|
||
|
||
function fxChronotrackApiSyncMapEntryStatus($strMs1Status, $blnCancelled = false) {
|
||
if ($blnCancelled) {
|
||
return 'WITHDRAWN';
|
||
}
|
||
$strMs1Status = strtoupper(trim((string)$strMs1Status));
|
||
switch ($strMs1Status) {
|
||
case 'DQ':
|
||
return 'DQ';
|
||
case 'DNS':
|
||
return 'DNS';
|
||
case 'NP':
|
||
return 'NP';
|
||
case 'ABANDON':
|
||
return 'DNF';
|
||
case 'UNRANKED':
|
||
return 'UNRANKED';
|
||
case 'DEFERRED':
|
||
return 'DEFERRED';
|
||
case 'CONF':
|
||
default:
|
||
return 'CONF';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* MSIN-4567 — MS1 par_sexe → ChronoTrack athlete_sex (enum API /meta).
|
||
* CT : M=Male, F=Female, NB=Non-Binary, NOT SPECIFIED=Unspecified.
|
||
*/
|
||
function fxChronotrackApiSyncNormalizeSex($strSexe) {
|
||
$strSexe = strtolower(trim((string)$strSexe));
|
||
if ($strSexe === 'f') {
|
||
return 'F';
|
||
}
|
||
if ($strSexe === 'h' || $strSexe === 'm') {
|
||
return 'M';
|
||
}
|
||
if ($strSexe === 'n') {
|
||
return 'NB';
|
||
}
|
||
if ($strSexe === '' || $strSexe === 'a') {
|
||
return 'NOT SPECIFIED';
|
||
}
|
||
return '';
|
||
}
|
||
|
||
function fxChronotrackApiSyncHasValidSex($strSexe) {
|
||
return fxChronotrackApiSyncNormalizeSex($strSexe) !== '';
|
||
}
|
||
|
||
/**
|
||
* MSIN-4328 / MSIN-4444 — dossard ChronoTrack = toujours p.no_bib.
|
||
* Jamais ec.no_equipe (n° d’équipe MS1) : en mode « 1 équipe = 1 dossard »
|
||
* les deux coïncident souvent au 1er assign, puis no_bib peut diverger (ex. 535 vs 507).
|
||
*/
|
||
function fxChronotrackApiSyncExtractBib(array $arrRow) {
|
||
$strBib = trim((string)($arrRow['no_bib'] ?? ''));
|
||
if ($strBib === '' || $strBib === '0') {
|
||
return '';
|
||
}
|
||
// Aligné affichage MS1 (fxShowBibNumber) : enlever préfixe epr_id-
|
||
$intEprId = intval($arrRow['epr_id'] ?? 0);
|
||
if ($intEprId > 0) {
|
||
$strBib = str_replace($intEprId . '-', '', $strBib);
|
||
}
|
||
$strBib = str_replace('-', '', $strBib);
|
||
$strBib = trim($strBib);
|
||
if ($strBib === '' || $strBib === '0') {
|
||
return '';
|
||
}
|
||
return $strBib;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4328 — conflit CT « tags / bib already assigned » (swap dossards).
|
||
*/
|
||
function fxChronotrackApiSyncIsBibConflictMessage($strMessage) {
|
||
$str = strtolower((string)$strMessage);
|
||
if ($str === '') {
|
||
return false;
|
||
}
|
||
if (strpos($str, 'already assigned') !== false) {
|
||
return true;
|
||
}
|
||
if (strpos($str, 'tags are already') !== false) {
|
||
return true;
|
||
}
|
||
if (strpos($str, 'bib') !== false && strpos($str, 'already') !== false) {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function fxChronotrackApiSyncPayloadWithoutBib(array $arrPayload) {
|
||
$arrOut = $arrPayload;
|
||
unset($arrOut['bib']);
|
||
return $arrOut;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4328 — libérer le dossard CT (swap) : bib vide.
|
||
*/
|
||
function fxChronotrackApiSyncPayloadClearBib(array $arrPayload) {
|
||
$arrOut = $arrPayload;
|
||
$arrOut['bib'] = '';
|
||
return $arrOut;
|
||
}
|
||
|
||
function fxChronotrackApiSyncParticipantLabel(array $arrRow) {
|
||
return trim(trim((string)($arrRow['par_prenom'] ?? '')) . ' ' . trim((string)($arrRow['par_nom'] ?? '')));
|
||
}
|
||
|
||
/**
|
||
* MSIN-4328 — libellé épreuve MS1 (type — nom), pour messages d’exclusion.
|
||
*/
|
||
function fxChronotrackApiSyncEpreuveLabel(array $arrRow) {
|
||
$strNom = trim((string)($arrRow['epr_nom'] ?? ''));
|
||
$strType = trim((string)($arrRow['epr_type'] ?? ''));
|
||
if ($strType !== '' && $strNom !== '') {
|
||
return $strType . ' — ' . $strNom;
|
||
}
|
||
if ($strNom !== '') {
|
||
return $strNom;
|
||
}
|
||
return $strType;
|
||
}
|
||
|
||
function fxChronotrackApiSyncIsTeamRow(array $arrRow) {
|
||
return intval($arrRow['pec_equipe'] ?? 0) === 1 || intval($arrRow['par_equipe'] ?? 0) === 1;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4444 — nom d’équipe MS1 (commande, sinon participant).
|
||
*/
|
||
function fxChronotrackApiSyncTeamName(array $arrRow) {
|
||
$strName = trim((string)($arrRow['pec_nom_equipe'] ?? ''));
|
||
if ($strName === '') {
|
||
$strName = trim((string)($arrRow['par_nom_equipe'] ?? ''));
|
||
}
|
||
return $strName;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4444 — clé d’équipe = pec_id (commande).
|
||
*/
|
||
function fxChronotrackApiSyncTeamGroupKey(array $arrRow) {
|
||
$intPecId = intval($arrRow['pec_id'] ?? 0);
|
||
if ($intPecId > 0) {
|
||
return 'pec:' . $intPecId;
|
||
}
|
||
return 'par:' . intval($arrRow['par_id'] ?? 0);
|
||
}
|
||
|
||
/**
|
||
* MSIN-4444 — 1 entrée CT par équipe : capitaine (rol_id=1), sinon 1er membre.
|
||
*/
|
||
function fxChronotrackApiSyncPickTeamRepresentative(array $tabMembers) {
|
||
$arrFirst = null;
|
||
foreach ($tabMembers as $arrRow) {
|
||
if (!is_array($arrRow)) {
|
||
continue;
|
||
}
|
||
if ($arrFirst === null) {
|
||
$arrFirst = $arrRow;
|
||
}
|
||
if (intval($arrRow['rol_id'] ?? 0) === 1) {
|
||
return $arrRow;
|
||
}
|
||
}
|
||
return $arrFirst;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4328 — éligibles dont par_maj > dernier push (ou tous si jamais poussé).
|
||
* Inclut aussi no_bib_remis_date : le check-in « dossard récupéré » met souvent
|
||
* par_maj = par_maj (n’incrémente pas) mais rafraîchit no_bib_remis_date.
|
||
* Inclut aussi par_date_bib (filet si un chemin dossard oublie par_maj).
|
||
*/
|
||
function fxChronotrackApiSyncFilterChangedSincePush(array $tabTransferable, $strLastPushAt) {
|
||
$strLastPushAt = trim((string)$strLastPushAt);
|
||
if ($strLastPushAt === '' || $strLastPushAt === '0000-00-00 00:00:00') {
|
||
return $tabTransferable;
|
||
}
|
||
$intLast = strtotime($strLastPushAt);
|
||
if ($intLast === false) {
|
||
return $tabTransferable;
|
||
}
|
||
|
||
$tabOut = array();
|
||
foreach ($tabTransferable as $arrItem) {
|
||
$strMaj = trim((string)($arrItem['row']['par_maj'] ?? ''));
|
||
if ($strMaj === '' || $strMaj === '0000-00-00 00:00:00') {
|
||
$tabOut[] = $arrItem;
|
||
continue;
|
||
}
|
||
$intMaj = strtotime($strMaj);
|
||
if ($intMaj === false || $intMaj > $intLast) {
|
||
$tabOut[] = $arrItem;
|
||
continue;
|
||
}
|
||
// MSIN-4328 — dossard récupéré / check-in CT
|
||
$strRemisDate = trim((string)($arrItem['row']['no_bib_remis_date'] ?? ''));
|
||
if ($strRemisDate !== '' && $strRemisDate !== '0000-00-00 00:00:00') {
|
||
$intRemis = strtotime($strRemisDate);
|
||
if ($intRemis !== false && $intRemis > $intLast) {
|
||
$tabOut[] = $arrItem;
|
||
continue;
|
||
}
|
||
}
|
||
// MSIN-4328 — assignation dossard (par_date_bib) sans par_maj
|
||
$strDateBib = trim((string)($arrItem['row']['par_date_bib'] ?? ''));
|
||
if ($strDateBib !== '' && $strDateBib !== '0000-00-00 00:00:00') {
|
||
$intDateBib = strtotime($strDateBib);
|
||
if ($intDateBib !== false && $intDateBib > $intLast) {
|
||
$tabOut[] = $arrItem;
|
||
}
|
||
}
|
||
}
|
||
return $tabOut;
|
||
}
|
||
|
||
function fxChronotrackApiSyncFormatRoleLabel($intRolId) {
|
||
switch (intval($intRolId)) {
|
||
case 1:
|
||
return 'Capitaine';
|
||
case 2:
|
||
return 'Membre';
|
||
default:
|
||
return $intRolId > 0 ? ('Rôle ' . intval($intRolId)) : '';
|
||
}
|
||
}
|
||
|
||
function fxChronotrackApiSyncFormatTeamBlockMessage(array $arrRow) {
|
||
$arrParts = array('Nom d\'équipe manquant — corriger dans MS1');
|
||
$strNoEquipe = trim((string)($arrRow['no_equipe'] ?? ''));
|
||
if ($strNoEquipe !== '' && $strNoEquipe !== '0') {
|
||
$arrParts[] = 'no équipe #' . $strNoEquipe;
|
||
}
|
||
return implode(' · ', $arrParts);
|
||
}
|
||
|
||
/**
|
||
* MSIN-4574 — code pays présent dans un payload CT (location_country prioritaire).
|
||
* Calibrage : CT stocke ISO-2 dans location_country (ex. CA).
|
||
*/
|
||
function fxChronotrackApiSyncCountryFromPayload(array $arrPayload) {
|
||
$str = trim((string)($arrPayload['location_country'] ?? ''));
|
||
if ($str !== '') {
|
||
return $str;
|
||
}
|
||
return trim((string)($arrPayload['country_code'] ?? ''));
|
||
}
|
||
|
||
/**
|
||
* MSIN-4574 — couverture pays sur les transférables (warning, pas blocage).
|
||
*
|
||
* @param array $tabTransferable items classify (row + payload optionnel)
|
||
* @return array{total:int,with_country:int,without_country:int,no_pay_id:int}
|
||
*/
|
||
function fxChronotrackApiSyncCountryCoverageFromTransferable(array $tabTransferable) {
|
||
$intTotal = count($tabTransferable);
|
||
$intWith = 0;
|
||
$intNoPay = 0;
|
||
foreach ($tabTransferable as $arrItem) {
|
||
if (!is_array($arrItem)) {
|
||
continue;
|
||
}
|
||
$arrRow = is_array($arrItem['row'] ?? null) ? $arrItem['row'] : array();
|
||
if (intval($arrRow['pay_id'] ?? 0) <= 0) {
|
||
$intNoPay++;
|
||
}
|
||
$strCode = '';
|
||
if (is_array($arrItem['payload'] ?? null)) {
|
||
$strCode = fxChronotrackApiSyncCountryFromPayload($arrItem['payload']);
|
||
}
|
||
if ($strCode === '') {
|
||
$strCode = fxChronotrackApiSyncResolveCountryCode($arrRow);
|
||
}
|
||
if ($strCode !== '') {
|
||
$intWith++;
|
||
}
|
||
}
|
||
return array(
|
||
'total' => $intTotal,
|
||
'with_country' => $intWith,
|
||
'without_country' => max(0, $intTotal - $intWith),
|
||
'no_pay_id' => $intNoPay,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* MSIN-4574 — fragment log « pays=CAN » / « pays=— ».
|
||
*/
|
||
function fxChronotrackApiSyncLogCountryFragment(array $arrPayload = array(), array $arrRow = array()) {
|
||
$str = fxChronotrackApiSyncCountryFromPayload($arrPayload);
|
||
if ($str === '' && count($arrRow) > 0) {
|
||
$str = fxChronotrackApiSyncResolveCountryCode($arrRow);
|
||
}
|
||
return 'pays=' . ($str !== '' ? $str : '—');
|
||
}
|
||
|
||
function fxChronotrackApiSyncParticipantSummary(array $arrRow) {
|
||
return array(
|
||
'par_id' => intval($arrRow['par_id'] ?? 0),
|
||
'external_id' => (string)intval($arrRow['par_id_original'] ?? 0),
|
||
'name' => fxChronotrackApiSyncParticipantLabel($arrRow),
|
||
'no_bib' => fxChronotrackApiSyncExtractBib($arrRow),
|
||
'par_sexe' => trim((string)($arrRow['par_sexe'] ?? '')),
|
||
'pec_id' => intval($arrRow['pec_id'] ?? 0),
|
||
'is_team' => fxChronotrackApiSyncIsTeamRow($arrRow),
|
||
'team_name' => fxChronotrackApiSyncTeamName($arrRow),
|
||
'no_equipe' => trim((string)($arrRow['no_equipe'] ?? '')),
|
||
'role_label' => fxChronotrackApiSyncFormatRoleLabel($arrRow['rol_id'] ?? 0),
|
||
);
|
||
}
|
||
|
||
function fxChronotrackApiSyncGroupBlockedByType(array $tabBlocked) {
|
||
$tabTypeLabels = array(
|
||
'team' => 'Équipes (nom manquant)',
|
||
'team_covered' => 'Membres équipe (couverts par EQ.)',
|
||
'duplicate_bib' => 'Dossard en double (individuel)',
|
||
'no_bib' => 'Dossard manquant',
|
||
'no_sex' => 'Sexe non reconnu',
|
||
'no_first_name' => 'Prénom manquant',
|
||
'no_last_name' => 'Nom manquant',
|
||
'no_race' => 'Épreuve non mappée',
|
||
'no_external' => 'External ID manquant',
|
||
);
|
||
$tabTypeOrder = array(
|
||
'team', 'team_covered', 'duplicate_bib', 'no_bib', 'no_sex',
|
||
'no_first_name', 'no_last_name', 'no_race', 'no_external',
|
||
);
|
||
$tabGroups = array();
|
||
foreach ($tabBlocked as $arrItem) {
|
||
$strCode = (string)($arrItem['code'] ?? 'other');
|
||
if (!isset($tabGroups[$strCode])) {
|
||
$tabGroups[$strCode] = array(
|
||
'code' => $strCode,
|
||
'label' => isset($tabTypeLabels[$strCode]) ? $tabTypeLabels[$strCode] : $strCode,
|
||
'items' => array(),
|
||
);
|
||
}
|
||
$tabGroups[$strCode]['items'][] = $arrItem;
|
||
}
|
||
|
||
$tabOrdered = array();
|
||
foreach ($tabTypeOrder as $strCode) {
|
||
if (isset($tabGroups[$strCode])) {
|
||
$tabOrdered[] = $tabGroups[$strCode];
|
||
unset($tabGroups[$strCode]);
|
||
}
|
||
}
|
||
foreach ($tabGroups as $arrGroup) {
|
||
$tabOrdered[] = $arrGroup;
|
||
}
|
||
return $tabOrdered;
|
||
}
|
||
|
||
/**
|
||
* Classe les participants MS1 : transférables vs exclus (dossard, sexe, doublons MS1, mapping).
|
||
*/
|
||
function fxChronotrackApiSyncClassifyParticipants(array $tabParticipants, array $arrRaceMap, $intCtEventId = 0, $blnBuildPayloads = true) {
|
||
$tabCandidates = array();
|
||
$tabBlocked = array();
|
||
$tabBibCounts = array();
|
||
|
||
foreach ($tabParticipants as $arrRow) {
|
||
$intExternalId = intval($arrRow['par_id_original'] ?? 0);
|
||
if ($intExternalId <= 0) {
|
||
$tabBlocked[] = array(
|
||
'code' => 'no_external',
|
||
'message' => 'par_id_original manquant',
|
||
'par_id' => intval($arrRow['par_id'] ?? 0),
|
||
'external_id' => '',
|
||
'name' => fxChronotrackApiSyncParticipantLabel($arrRow),
|
||
'no_bib' => fxChronotrackApiSyncExtractBib($arrRow),
|
||
'par_sexe' => trim((string)($arrRow['par_sexe'] ?? '')),
|
||
);
|
||
continue;
|
||
}
|
||
|
||
$intEprId = intval($arrRow['epr_id'] ?? 0);
|
||
$intCtRaceId = intval($arrRaceMap[$intEprId]['ct_race_id'] ?? 0);
|
||
if ($intCtRaceId <= 0) {
|
||
$arrSummary = fxChronotrackApiSyncParticipantSummary($arrRow);
|
||
$strEprLabel = fxChronotrackApiSyncEpreuveLabel($arrRow);
|
||
// MSIN-4328 — nom d’épreuve en premier (epr_id en secours)
|
||
if ($strEprLabel !== '') {
|
||
$strMsg = '« ' . $strEprLabel . ' » (epr_id ' . $intEprId . ') non mappée vers ChronoTrack';
|
||
} else {
|
||
$strMsg = 'Épreuve MS1 (epr_id ' . $intEprId . ') non mappée vers ChronoTrack';
|
||
}
|
||
$tabBlocked[] = array(
|
||
'code' => 'no_race',
|
||
'message' => $strMsg,
|
||
'epr_id' => $intEprId,
|
||
'epr_nom' => $strEprLabel,
|
||
) + $arrSummary;
|
||
continue;
|
||
}
|
||
|
||
$tabCandidates[] = $arrRow;
|
||
}
|
||
|
||
// MSIN-4444 — séparer solo / équipes ; 1 entry CT = capitaine (évite doublon dossard partagé)
|
||
$tabSoloCandidates = array();
|
||
$tabTeamGroups = array();
|
||
foreach ($tabCandidates as $arrRow) {
|
||
if (fxChronotrackApiSyncIsTeamRow($arrRow)) {
|
||
$strTeamKey = fxChronotrackApiSyncTeamGroupKey($arrRow);
|
||
if (!isset($tabTeamGroups[$strTeamKey])) {
|
||
$tabTeamGroups[$strTeamKey] = array();
|
||
}
|
||
$tabTeamGroups[$strTeamKey][] = $arrRow;
|
||
} else {
|
||
$tabSoloCandidates[] = $arrRow;
|
||
}
|
||
}
|
||
|
||
$tabBibCounts = array();
|
||
foreach ($tabSoloCandidates as $arrRow) {
|
||
$strBib = fxChronotrackApiSyncExtractBib($arrRow);
|
||
if ($strBib !== '') {
|
||
if (!isset($tabBibCounts[$strBib])) {
|
||
$tabBibCounts[$strBib] = 0;
|
||
}
|
||
$tabBibCounts[$strBib]++;
|
||
}
|
||
}
|
||
|
||
$tabTeamReps = array();
|
||
foreach ($tabTeamGroups as $strTeamKey => $tabMembers) {
|
||
$arrRep = fxChronotrackApiSyncPickTeamRepresentative($tabMembers);
|
||
if ($arrRep === null) {
|
||
continue;
|
||
}
|
||
$tabTeamReps[$strTeamKey] = array(
|
||
'rep' => $arrRep,
|
||
'members' => $tabMembers,
|
||
);
|
||
$strBib = fxChronotrackApiSyncExtractBib($arrRep);
|
||
if ($strBib !== '') {
|
||
if (!isset($tabBibCounts[$strBib])) {
|
||
$tabBibCounts[$strBib] = 0;
|
||
}
|
||
$tabBibCounts[$strBib]++;
|
||
}
|
||
}
|
||
|
||
$tabDuplicateBibs = array();
|
||
foreach ($tabBibCounts as $strBib => $intCount) {
|
||
if ($intCount > 1) {
|
||
$tabDuplicateBibs[$strBib] = true;
|
||
}
|
||
}
|
||
|
||
$tabTransferable = array();
|
||
$intBlockedTeam = 0;
|
||
$intBlockedTeamCovered = 0;
|
||
$intBlockedNoBib = 0;
|
||
$intBlockedNoSex = 0;
|
||
$intBlockedNoFirstName = 0;
|
||
$intBlockedNoLastName = 0;
|
||
$intBlockedDuplicateBib = 0;
|
||
|
||
foreach ($tabSoloCandidates as $arrRow) {
|
||
$arrSummary = fxChronotrackApiSyncParticipantSummary($arrRow);
|
||
$strBib = $arrSummary['no_bib'];
|
||
|
||
if ($strBib !== '' && isset($tabDuplicateBibs[$strBib])) {
|
||
$intBlockedDuplicateBib++;
|
||
$tabBlocked[] = array(
|
||
'code' => 'duplicate_bib',
|
||
'message' => 'Dossard #' . $strBib . ' en double — corriger dans MS1',
|
||
'bib' => $strBib,
|
||
) + $arrSummary;
|
||
continue;
|
||
}
|
||
|
||
if ($strBib === '') {
|
||
$intBlockedNoBib++;
|
||
$tabBlocked[] = array(
|
||
'code' => 'no_bib',
|
||
'message' => 'Dossard manquant — corriger dans MS1',
|
||
) + $arrSummary;
|
||
continue;
|
||
}
|
||
|
||
if (!fxChronotrackApiSyncHasValidSex($arrRow['par_sexe'] ?? '')) {
|
||
$intBlockedNoSex++;
|
||
$strSexeMs1 = trim((string)($arrRow['par_sexe'] ?? ''));
|
||
$tabBlocked[] = array(
|
||
'code' => 'no_sex',
|
||
'message' => 'Sexe « ' . $strSexeMs1 . ' » — mapping CT à définir',
|
||
) + $arrSummary;
|
||
continue;
|
||
}
|
||
|
||
// MSIN-4328 — CT refuse sans first_name / last_name
|
||
$strPrenom = trim((string)($arrRow['par_prenom'] ?? ''));
|
||
$strNom = trim((string)($arrRow['par_nom'] ?? ''));
|
||
if ($strPrenom === '') {
|
||
$intBlockedNoFirstName++;
|
||
$tabBlocked[] = array(
|
||
'code' => 'no_first_name',
|
||
'message' => 'Prénom manquant — corriger dans MS1',
|
||
) + $arrSummary;
|
||
continue;
|
||
}
|
||
if ($strNom === '') {
|
||
$intBlockedNoLastName++;
|
||
$tabBlocked[] = array(
|
||
'code' => 'no_last_name',
|
||
'message' => 'Nom manquant — corriger dans MS1',
|
||
) + $arrSummary;
|
||
continue;
|
||
}
|
||
|
||
$arrPayload = null;
|
||
if ($blnBuildPayloads) {
|
||
$arrPayload = fxChronotrackApiSyncBuildEntryPayload($arrRow, $arrRaceMap, $intCtEventId);
|
||
if ($arrPayload === null) {
|
||
continue;
|
||
}
|
||
}
|
||
|
||
$tabTransferable[] = array(
|
||
'row' => $arrRow,
|
||
'payload' => $arrPayload,
|
||
'summary' => $arrSummary,
|
||
);
|
||
}
|
||
|
||
// MSIN-4444 — équipes : prénom EQ. / nom = nom d'équipe (capitaine)
|
||
foreach ($tabTeamReps as $arrGroup) {
|
||
$arrRep = $arrGroup['rep'];
|
||
$tabMembers = $arrGroup['members'];
|
||
$intRepParId = intval($arrRep['par_id'] ?? 0);
|
||
|
||
foreach ($tabMembers as $arrMember) {
|
||
if (intval($arrMember['par_id'] ?? 0) === $intRepParId) {
|
||
continue;
|
||
}
|
||
$intBlockedTeamCovered++;
|
||
$tabBlocked[] = array(
|
||
'code' => 'team_covered',
|
||
'message' => 'Membre d\'équipe — couvert par EQ. / nom d\'équipe (capitaine)',
|
||
) + fxChronotrackApiSyncParticipantSummary($arrMember);
|
||
}
|
||
|
||
$arrSummary = fxChronotrackApiSyncParticipantSummary($arrRep);
|
||
$strTeamName = fxChronotrackApiSyncTeamName($arrRep);
|
||
if ($strTeamName === '') {
|
||
$intBlockedTeam++;
|
||
$tabBlocked[] = array(
|
||
'code' => 'team',
|
||
'message' => fxChronotrackApiSyncFormatTeamBlockMessage($arrRep),
|
||
) + $arrSummary;
|
||
continue;
|
||
}
|
||
$arrSummary['name'] = 'EQ. ' . $strTeamName;
|
||
$arrSummary['team_name'] = $strTeamName;
|
||
|
||
$strBib = $arrSummary['no_bib'];
|
||
if ($strBib !== '' && isset($tabDuplicateBibs[$strBib])) {
|
||
$intBlockedDuplicateBib++;
|
||
$tabBlocked[] = array(
|
||
'code' => 'duplicate_bib',
|
||
'message' => 'Dossard #' . $strBib . ' en double — corriger dans MS1',
|
||
'bib' => $strBib,
|
||
) + $arrSummary;
|
||
continue;
|
||
}
|
||
if ($strBib === '') {
|
||
$intBlockedNoBib++;
|
||
$tabBlocked[] = array(
|
||
'code' => 'no_bib',
|
||
'message' => 'Dossard manquant (équipe) — corriger dans MS1',
|
||
) + $arrSummary;
|
||
continue;
|
||
}
|
||
if (!fxChronotrackApiSyncHasValidSex($arrRep['par_sexe'] ?? '')) {
|
||
$intBlockedNoSex++;
|
||
$strSexeMs1 = trim((string)($arrRep['par_sexe'] ?? ''));
|
||
$tabBlocked[] = array(
|
||
'code' => 'no_sex',
|
||
'message' => 'Sexe capitaine « ' . $strSexeMs1 . ' » — mapping CT à définir',
|
||
) + $arrSummary;
|
||
continue;
|
||
}
|
||
|
||
$arrPayload = null;
|
||
if ($blnBuildPayloads) {
|
||
$arrPayload = fxChronotrackApiSyncBuildEntryPayload($arrRep, $arrRaceMap, $intCtEventId);
|
||
if ($arrPayload === null) {
|
||
continue;
|
||
}
|
||
}
|
||
|
||
$tabTransferable[] = array(
|
||
'row' => $arrRep,
|
||
'payload' => $arrPayload,
|
||
'summary' => $arrSummary,
|
||
);
|
||
}
|
||
|
||
$intBlockedNoRace = 0;
|
||
$intBlockedNoExternal = 0;
|
||
foreach ($tabBlocked as $arrItem) {
|
||
if (($arrItem['code'] ?? '') === 'no_race') {
|
||
$intBlockedNoRace++;
|
||
} elseif (($arrItem['code'] ?? '') === 'no_external') {
|
||
$intBlockedNoExternal++;
|
||
}
|
||
}
|
||
|
||
return array(
|
||
'transferable' => $tabTransferable,
|
||
'blocked' => $tabBlocked,
|
||
'blocked_by_type' => fxChronotrackApiSyncGroupBlockedByType($tabBlocked),
|
||
'blocked_count' => count($tabBlocked),
|
||
'skipped_no_race' => $intBlockedNoRace,
|
||
'skipped_no_external' => $intBlockedNoExternal,
|
||
'country_coverage' => fxChronotrackApiSyncCountryCoverageFromTransferable($tabTransferable),
|
||
'blocked_no_bib' => $intBlockedNoBib,
|
||
'blocked_no_sex' => $intBlockedNoSex,
|
||
'blocked_no_first_name' => $intBlockedNoFirstName,
|
||
'blocked_no_last_name' => $intBlockedNoLastName,
|
||
'blocked_duplicate_bib' => $intBlockedDuplicateBib,
|
||
'blocked_team' => $intBlockedTeam + $intBlockedTeamCovered,
|
||
'duplicate_bib_numbers' => array_keys($tabDuplicateBibs),
|
||
// Alias rétrocompat (compteurs)
|
||
'anomalies' => $tabBlocked,
|
||
'anomaly_no_bib' => $intBlockedNoBib,
|
||
'anomaly_no_sex' => $intBlockedNoSex,
|
||
'anomaly_duplicate_bib' => $intBlockedDuplicateBib,
|
||
);
|
||
}
|
||
|
||
function fxChronotrackApiSyncNormalizeBirthdate($strDob) {
|
||
$strDob = trim((string)$strDob);
|
||
if ($strDob === '' || $strDob === '0000-00-00') {
|
||
return '';
|
||
}
|
||
$intTs = strtotime($strDob);
|
||
if ($intTs === false) {
|
||
return '';
|
||
}
|
||
return date('Y-m-d', $intTs);
|
||
}
|
||
|
||
function fxChronotrackApiSyncLog($intEveId, $intParId, $strAction, $strStatus, $strMessage) {
|
||
global $objDatabase;
|
||
|
||
if (!isset($objDatabase) || !is_object($objDatabase)) {
|
||
error_log('MSIN-4328 sync_log: $objDatabase absent — action=' . $strAction);
|
||
return false;
|
||
}
|
||
|
||
$sql = "INSERT INTO api_chronotrack_sync_log SET eve_id = " . intval($intEveId)
|
||
. ", par_id = " . ($intParId > 0 ? intval($intParId) : 'NULL')
|
||
. ", action = '" . $objDatabase->fxEscape(substr(trim($strAction), 0, 32)) . "'"
|
||
. ", status = '" . $objDatabase->fxEscape($strStatus === 'ok' ? 'ok' : 'error') . "'"
|
||
. ", message = '" . $objDatabase->fxEscape(substr(trim($strMessage), 0, 65000)) . "'"
|
||
. ", created_at = '" . fxGetDateTime() . "'";
|
||
$blnOk = (bool)$objDatabase->fxQuery($sql);
|
||
if (!$blnOk) {
|
||
// Sans ça, push/cron « OK » mais Voir les logs = vide (INSERT silencieux).
|
||
error_log('MSIN-4328 sync_log INSERT failed eve=' . intval($intEveId)
|
||
. ' action=' . $strAction . ' sql_err=' . (isset($objDatabase->con) ? mysqli_error($objDatabase->con) : '?'));
|
||
}
|
||
return $blnOk;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4328 — derniers logs sync pour un événement (écran admin).
|
||
*/
|
||
function fxChronotrackApiSyncLogList($intEveId, $intLimit = 50) {
|
||
global $objDatabase;
|
||
|
||
$intEveId = intval($intEveId);
|
||
$intLimit = max(1, min(200, intval($intLimit)));
|
||
if ($intEveId <= 0) {
|
||
return array('state' => 'error', 'message' => 'eve_id manquant', 'logs' => array());
|
||
}
|
||
|
||
$sql = "SELECT log_id, eve_id, par_id, action, status, message, created_at"
|
||
. " FROM api_chronotrack_sync_log"
|
||
. " WHERE eve_id = " . $intEveId
|
||
. " ORDER BY log_id DESC"
|
||
. " LIMIT " . $intLimit;
|
||
$arrRows = $objDatabase->fxGetResults($sql);
|
||
$tabOut = array();
|
||
if (is_array($arrRows)) {
|
||
for ($i = 1; $i <= count($arrRows); $i++) {
|
||
$tabOut[] = $arrRows[$i];
|
||
}
|
||
}
|
||
return array(
|
||
'state' => 'ok',
|
||
'logs' => $tabOut,
|
||
'count' => count($tabOut),
|
||
);
|
||
}
|
||
|
||
function fxChronotrackApiSyncPaysHasIso3Column() {
|
||
static $blnHas = null;
|
||
if ($blnHas !== null) {
|
||
return $blnHas;
|
||
}
|
||
global $objDatabase;
|
||
$arrRow = $objDatabase->fxGetRow("SHOW COLUMNS FROM inscriptions_pays LIKE 'pay_iso3'");
|
||
$blnHas = ($arrRow !== null);
|
||
return $blnHas;
|
||
}
|
||
|
||
function fxChronotrackApiSyncLoadMs1Participants($intEveId) {
|
||
global $objDatabase;
|
||
|
||
$intEveId = intval($intEveId);
|
||
if ($intEveId <= 0) {
|
||
return array();
|
||
}
|
||
|
||
$strCountryCols = " (SELECT pay_iso FROM inscriptions_pays WHERE pay_id = p.pay_id) AS country_iso2,"
|
||
. " (SELECT pay_nom_en FROM inscriptions_pays WHERE pay_id = p.pay_id) AS country_name";
|
||
if (fxChronotrackApiSyncPaysHasIso3Column()) {
|
||
$strCountryCols = " (SELECT pay_iso3 FROM inscriptions_pays WHERE pay_id = p.pay_id) AS country_iso3,"
|
||
. " (SELECT pay_iso FROM inscriptions_pays WHERE pay_id = p.pay_id) AS country_iso2,"
|
||
. " (SELECT pay_nom_en FROM inscriptions_pays WHERE pay_id = p.pay_id) AS country_name";
|
||
}
|
||
|
||
$sql = "SELECT p.par_id, p.par_id_original, p.eve_id, p.epr_id, p.pec_id, p.rol_id, p.par_equipe, p.par_nom_equipe,"
|
||
. " p.par_prenom, p.par_nom, p.par_sexe, p.pay_id, p.pro_id,"
|
||
. " p.par_naissance, p.no_bib, p.no_bib_remis, p.no_bib_remis_date, p.par_date_bib, p.par_statut_course,"
|
||
. " p.par_ville, p.par_adresse, p.par_codepostal,"
|
||
. " p.is_cancelled, p.par_maj, p.ct_entry_id,"
|
||
. " ec.pec_equipe, ec.no_equipe, ec.pec_nom_equipe,"
|
||
. " ie.epr_nom_fr AS epr_nom, ie.epr_type_fr AS epr_type,"
|
||
. " (SELECT pro_iso FROM inscriptions_provinces WHERE pro_id = p.pro_id) AS state_iso,"
|
||
. $strCountryCols
|
||
. " FROM resultats_participants p"
|
||
. " JOIN resultats_epreuves_commandees ec ON p.pec_id = ec.pec_id_original"
|
||
. " LEFT JOIN inscriptions_epreuves ie ON ie.epr_id = p.epr_id"
|
||
. " WHERE ec.pec_actif = 1 AND ec.is_cancelled = 0 AND p.is_cancelled = 0"
|
||
. " AND p.eve_id = " . $intEveId
|
||
. " ORDER BY p.par_id_original, p.par_id";
|
||
|
||
$tabRows = $objDatabase->fxGetResults($sql);
|
||
if (!is_array($tabRows)) {
|
||
return array();
|
||
}
|
||
|
||
$tabOut = array();
|
||
for ($i = 1; $i <= count($tabRows); $i++) {
|
||
$tabOut[] = $tabRows[$i];
|
||
}
|
||
return $tabOut;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4461 — clé CT entry_note_{label} (support ChronoTrack).
|
||
*/
|
||
function fxChronotrackApiSyncEntryNoteKey($strLabel, $intQueId) {
|
||
$strLabel = trim((string)$strLabel);
|
||
$strLabel = preg_replace('/\s+/u', '_', $strLabel);
|
||
$strLabel = preg_replace('/["\\\\\x00-\x1F]+/u', '', $strLabel);
|
||
$strLabel = trim($strLabel, '_');
|
||
if ($strLabel === '') {
|
||
$strLabel = 'q' . intval($intQueId);
|
||
}
|
||
return 'entry_note_' . $strLabel;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4461 — index des réponses questions d’un événement (1 requête / eve_id).
|
||
*
|
||
* Périmètre actuel : TOUTES les questions actives de l’événement (pas seulement que_rapport).
|
||
* Si le payload devient trop lourd (beaucoup de questions / événements volumineux),
|
||
* restreindre ici (ex. que_rapport = 1, liste blanche, ou exclusion des validations techniques).
|
||
*
|
||
* @return array<int, array<int, array{par_id:int,que_id:int,label:string,value:string}>>
|
||
* indexé par pec_id
|
||
*/
|
||
function fxChronotrackApiSyncLoadMs1QuestionsIndex($intEveId) {
|
||
global $objDatabase;
|
||
|
||
static $tabCache = array();
|
||
$intEveId = intval($intEveId);
|
||
if ($intEveId <= 0) {
|
||
return array();
|
||
}
|
||
if (isset($tabCache[$intEveId])) {
|
||
return $tabCache[$intEveId];
|
||
}
|
||
|
||
$sql = "SELECT rq.pec_id, rq.par_id, rq.que_id,"
|
||
. " TRIM(COALESCE("
|
||
. " NULLIF(iq.que_rapport_label_fr, ''),"
|
||
. " NULLIF(rq.que_question_fr, ''),"
|
||
. " NULLIF(iq.que_question_fr, ''),"
|
||
. " '')) AS note_label,"
|
||
. " TRIM(COALESCE("
|
||
. " NULLIF(rq.que_choix_fr, ''),"
|
||
. " NULLIF(rq.pqu_note, ''),"
|
||
. " NULLIF(rq.que_choix_en, ''),"
|
||
. " '')) AS note_value"
|
||
. " FROM resultats_questions rq"
|
||
. " INNER JOIN inscriptions_questions iq ON iq.que_id = rq.que_id AND iq.eve_id = " . $intEveId
|
||
. " WHERE rq.que_actif = 1"
|
||
. " ORDER BY rq.pec_id, rq.que_id";
|
||
|
||
$tabRows = $objDatabase->fxGetResults($sql);
|
||
$tabIndex = array();
|
||
if (is_array($tabRows)) {
|
||
for ($i = 1; $i <= count($tabRows); $i++) {
|
||
$arrQ = $tabRows[$i];
|
||
$intPecId = intval($arrQ['pec_id'] ?? 0);
|
||
$strValue = trim((string)($arrQ['note_value'] ?? ''));
|
||
if ($intPecId <= 0 || $strValue === '') {
|
||
continue;
|
||
}
|
||
if (!isset($tabIndex[$intPecId])) {
|
||
$tabIndex[$intPecId] = array();
|
||
}
|
||
$tabIndex[$intPecId][] = array(
|
||
'par_id' => intval($arrQ['par_id'] ?? 0),
|
||
'que_id' => intval($arrQ['que_id'] ?? 0),
|
||
'label' => trim((string)($arrQ['note_label'] ?? '')),
|
||
'value' => $strValue,
|
||
);
|
||
}
|
||
}
|
||
|
||
$tabCache[$intEveId] = $tabIndex;
|
||
return $tabIndex;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4461 — map entry_note_* pour un participant (réponses épreuve + participant).
|
||
* resultats_questions.par_id = par_id_original (legacy) ; par_id 0 = question niveau épreuve.
|
||
*/
|
||
function fxChronotrackApiSyncEntryNotesForParticipant(array $arrRow) {
|
||
$intEveId = intval($arrRow['eve_id'] ?? 0);
|
||
$intPecId = intval($arrRow['pec_id'] ?? 0);
|
||
if ($intEveId <= 0 || $intPecId <= 0) {
|
||
return array();
|
||
}
|
||
|
||
$tabIndex = fxChronotrackApiSyncLoadMs1QuestionsIndex($intEveId);
|
||
if (!isset($tabIndex[$intPecId]) || !is_array($tabIndex[$intPecId])) {
|
||
return array();
|
||
}
|
||
|
||
$intParOrig = intval($arrRow['par_id_original'] ?? 0);
|
||
$intParId = intval($arrRow['par_id'] ?? 0);
|
||
$tabNotes = array();
|
||
$tabUsedKeys = array();
|
||
|
||
foreach ($tabIndex[$intPecId] as $arrQ) {
|
||
$intQPar = intval($arrQ['par_id'] ?? 0);
|
||
// Question d’épreuve (par_id vide) ou réponse de ce participant
|
||
if ($intQPar > 0 && $intQPar !== $intParOrig && $intQPar !== $intParId) {
|
||
continue;
|
||
}
|
||
$intQueId = intval($arrQ['que_id'] ?? 0);
|
||
$strKey = fxChronotrackApiSyncEntryNoteKey($arrQ['label'] ?? '', $intQueId);
|
||
if (isset($tabUsedKeys[$strKey])) {
|
||
$strKey = 'entry_note_q' . $intQueId;
|
||
}
|
||
$tabUsedKeys[$strKey] = true;
|
||
$tabNotes[$strKey] = (string)$arrQ['value'];
|
||
}
|
||
|
||
return $tabNotes;
|
||
}
|
||
|
||
function fxChronotrackApiSyncResolveRegChoiceIdForRace($intCtEventId, $intCtRaceId) {
|
||
static $tabCache = array();
|
||
$strKey = intval($intCtEventId) . ':' . intval($intCtRaceId);
|
||
if (array_key_exists($strKey, $tabCache)) {
|
||
return $tabCache[$strKey];
|
||
}
|
||
|
||
$tabCache[$strKey] = 0;
|
||
$arrRaces = fxChronotrackApiFetchRacesForEvent($intCtEventId);
|
||
if (($arrRaces['state'] ?? '') !== 'ok' || empty($arrRaces['races'])) {
|
||
return 0;
|
||
}
|
||
|
||
foreach ($arrRaces['races'] as $arrRace) {
|
||
if (intval($arrRace['ct_race_id'] ?? 0) !== intval($intCtRaceId)) {
|
||
continue;
|
||
}
|
||
$arrRaw = isset($arrRace['raw']) && is_array($arrRace['raw']) ? $arrRace['raw'] : array();
|
||
$arrChoices = fxChronotrackApiCollectRegChoiceEntities($arrRaw);
|
||
if (count($arrChoices) === 0 && isset($arrRaw['reg_choice_id'])) {
|
||
$tabCache[$strKey] = intval($arrRaw['reg_choice_id']);
|
||
return $tabCache[$strKey];
|
||
}
|
||
foreach ($arrChoices as $arrChoice) {
|
||
if (!is_array($arrChoice) || empty($arrChoice['reg_choice_id'])) {
|
||
continue;
|
||
}
|
||
$tabCache[$strKey] = intval($arrChoice['reg_choice_id']);
|
||
return $tabCache[$strKey];
|
||
}
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
function fxChronotrackApiSyncBuildEntryPayload(array $arrRow, array $arrRaceMap, $intCtEventId = 0) {
|
||
$intEprId = intval($arrRow['epr_id'] ?? 0);
|
||
$intCtRaceId = intval($arrRaceMap[$intEprId]['ct_race_id'] ?? 0);
|
||
if ($intCtRaceId <= 0) {
|
||
return null;
|
||
}
|
||
|
||
$intExternalId = intval($arrRow['par_id_original'] ?? 0);
|
||
if ($intExternalId <= 0) {
|
||
return null;
|
||
}
|
||
|
||
// MSIN-4574 — pay_iso (ISO-2) = source pour location_country CT
|
||
$strCountry = fxChronotrackApiSyncResolveCountryIso2($arrRow);
|
||
$strState = strtoupper(trim((string)($arrRow['state_iso'] ?? '')));
|
||
|
||
$strSex = fxChronotrackApiSyncNormalizeSex($arrRow['par_sexe'] ?? '');
|
||
if ($strSex === '') {
|
||
return null;
|
||
}
|
||
|
||
$strBib = fxChronotrackApiSyncExtractBib($arrRow);
|
||
if ($strBib === '') {
|
||
return null;
|
||
}
|
||
|
||
$arrEntry = array(
|
||
'external_id' => (string)$intExternalId,
|
||
'race_id' => $intCtRaceId,
|
||
'first_name' => trim((string)($arrRow['par_prenom'] ?? '')),
|
||
'last_name' => trim((string)($arrRow['par_nom'] ?? '')),
|
||
'sex' => $strSex,
|
||
);
|
||
|
||
// MSIN-4444 — inscription équipe : prénom = EQ., nom = nom d'équipe
|
||
if (fxChronotrackApiSyncIsTeamRow($arrRow)) {
|
||
$strTeamName = fxChronotrackApiSyncTeamName($arrRow);
|
||
if ($strTeamName === '') {
|
||
return null;
|
||
}
|
||
$arrEntry['first_name'] = 'EQ.';
|
||
$arrEntry['last_name'] = $strTeamName;
|
||
}
|
||
|
||
if ($intCtEventId > 0) {
|
||
$arrEntry['event_id'] = $intCtEventId;
|
||
}
|
||
|
||
$intRegChoiceId = fxChronotrackApiSyncResolveRegChoiceIdForRace($intCtEventId, $intCtRaceId);
|
||
if ($intRegChoiceId > 0) {
|
||
$arrEntry['reg_choice_id'] = $intRegChoiceId;
|
||
}
|
||
|
||
$strDob = fxChronotrackApiSyncNormalizeBirthdate($arrRow['par_naissance'] ?? '');
|
||
if ($strDob !== '') {
|
||
$arrEntry['birthdate'] = $strDob;
|
||
}
|
||
|
||
// MSIN-4574 — meta CT entry : location_* (pas street/city/country_code du format résultats).
|
||
// On envoie les clés meta + alias courts (CT accepte souvent les deux pour identité).
|
||
$strStreet = trim((string)($arrRow['par_adresse'] ?? ''));
|
||
if ($strStreet !== '') {
|
||
$arrEntry['location_street'] = $strStreet;
|
||
$arrEntry['street'] = $strStreet;
|
||
}
|
||
|
||
$strCity = trim((string)($arrRow['par_ville'] ?? ''));
|
||
if ($strCity !== '') {
|
||
$arrEntry['location_city'] = $strCity;
|
||
$arrEntry['city'] = $strCity;
|
||
}
|
||
|
||
$strPostal = trim((string)($arrRow['par_codepostal'] ?? ''));
|
||
if ($strPostal !== '') {
|
||
$arrEntry['location_postal_code'] = $strPostal;
|
||
$arrEntry['postal_code'] = $strPostal;
|
||
}
|
||
|
||
if ($strState !== '') {
|
||
$arrEntry['location_region'] = $strState;
|
||
$arrEntry['state_code'] = $strState;
|
||
}
|
||
|
||
// MSIN-4574 — pays / province (calibrage sonde lecture + export confirmed entries 2026-08-11) :
|
||
// location_country = ISO-2 (CA), location_region = pro_iso (QC),
|
||
// country_name = libellé, country_code = ISO-2 (comme export COUNTRY_CODE).
|
||
$strIso2 = fxChronotrackApiSyncResolveCountryIso2($arrRow);
|
||
$strCountryName = fxChronotrackApiSyncResolveCountryLabel($arrRow);
|
||
if ($strIso2 !== '') {
|
||
$arrEntry['location_country'] = $strIso2;
|
||
$arrEntry['country_code'] = $strIso2;
|
||
}
|
||
if ($strCountryName !== '') {
|
||
$arrEntry['country_name'] = $strCountryName;
|
||
}
|
||
|
||
$arrEntry['entry_status'] = fxChronotrackApiSyncMapEntryStatus(
|
||
$arrRow['par_statut_course'] ?? 'CONF',
|
||
intval($arrRow['is_cancelled'] ?? 0) === 1
|
||
);
|
||
$arrEntry['status'] = $arrEntry['entry_status'];
|
||
|
||
// MSIN-4328 — CT refuse « assign a bib » si l’entry n’est pas confirmed.
|
||
// Envoyer le dossard dès que CONF (création ET maj) — sinon un changement
|
||
// de dossard MS1 sur une entry déjà liée n’arrive jamais dans ChronoTrack.
|
||
if ($strBib !== '' && $arrEntry['entry_status'] === 'CONF') {
|
||
$arrEntry['bib'] = $strBib;
|
||
}
|
||
|
||
// MSIN-4328 — Dossard récupéré (MS1) → Check In Status CT (entry_check_in = "1" / null)
|
||
$arrEntry['entry_check_in'] = (intval($arrRow['no_bib_remis'] ?? 0) === 1) ? '1' : null;
|
||
|
||
// MSIN-4461 — questions MS1 → notes CT (entry_note_{label}).
|
||
// Toutes les réponses actives pour l’instant ; filtrer plus tard si trop lourd (voir LoadMs1QuestionsIndex).
|
||
foreach (fxChronotrackApiSyncEntryNotesForParticipant($arrRow) as $strNoteKey => $strNoteValue) {
|
||
$arrEntry[$strNoteKey] = $strNoteValue;
|
||
}
|
||
|
||
return $arrEntry;
|
||
}
|
||
|
||
function fxChronotrackApiSyncPayloadForPost(array $arrPayload) {
|
||
$arrOut = $arrPayload;
|
||
unset($arrOut['entry_id']);
|
||
return $arrOut;
|
||
}
|
||
|
||
function fxChronotrackApiSyncPayloadForPut(array $arrPayload) {
|
||
$arrOut = $arrPayload;
|
||
unset($arrOut['entry_id']);
|
||
return $arrOut;
|
||
}
|
||
|
||
function fxChronotrackApiSyncPutEntry($strEntryId, array $arrPayload) {
|
||
$strEntryId = trim((string)$strEntryId);
|
||
if ($strEntryId === '') {
|
||
return array('state' => 'error', 'message' => 'entry_id manquant pour PUT');
|
||
}
|
||
$arrPut = fxChronotrackApiOAuthApiPut('entry/' . $strEntryId, fxChronotrackApiSyncPayloadForPut($arrPayload));
|
||
if ($arrPut['state'] === 'ok') {
|
||
$arrPut['method'] = 'PUT';
|
||
}
|
||
return $arrPut;
|
||
}
|
||
|
||
function fxChronotrackApiSyncPushOneEntry($intCtEventId, array $arrPayload) {
|
||
$intCtEventId = intval($intCtEventId);
|
||
$strEntryId = trim((string)($arrPayload['entry_id'] ?? ''));
|
||
if ($strEntryId !== '') {
|
||
$arrPut = fxChronotrackApiSyncPutEntry($strEntryId, $arrPayload);
|
||
$arrPut['method'] = 'PUT';
|
||
return $arrPut;
|
||
}
|
||
$arrPost = fxChronotrackApiSyncPostEntries($intCtEventId, array(fxChronotrackApiSyncPayloadForPost($arrPayload)));
|
||
if ($arrPost['state'] === 'ok') {
|
||
$arrPost['method'] = 'POST';
|
||
}
|
||
return $arrPost;
|
||
}
|
||
|
||
function fxChronotrackApiSyncEnrichPayloadWithCtEntry(array $arrPayload, $arrCtEntity) {
|
||
if (!is_array($arrCtEntity)) {
|
||
return $arrPayload;
|
||
}
|
||
$strEntryId = fxChronotrackApiEntityId($arrCtEntity);
|
||
if ($strEntryId !== '') {
|
||
$arrPayload['entry_id'] = $strEntryId;
|
||
}
|
||
return $arrPayload;
|
||
}
|
||
|
||
function fxChronotrackApiSyncPostEntries($intCtEventId, array $tabEntries) {
|
||
$intCtEventId = intval($intCtEventId);
|
||
if ($intCtEventId <= 0 || count($tabEntries) === 0) {
|
||
return array('state' => 'error', 'message' => 'Aucune entry à envoyer');
|
||
}
|
||
|
||
// MSIN-4328 — mémoriser le format qui marche ; ne pas retenter le wrapper à chaque sous-lot
|
||
static $strBodyFormat = 'array';
|
||
static $blnWrapperTried = false;
|
||
|
||
if ($strBodyFormat === 'event_entry') {
|
||
$arrPost = fxChronotrackApiOAuthApiPost(
|
||
'event/' . $intCtEventId . '/entry',
|
||
array('event_entry' => $tabEntries)
|
||
);
|
||
if ($arrPost['state'] === 'ok') {
|
||
$arrPost['body_format'] = 'event_entry';
|
||
return $arrPost;
|
||
}
|
||
return $arrPost;
|
||
}
|
||
|
||
// Doc CT : POST body = tableau JSON d'entités entry (pas de wrapper event_entry).
|
||
$arrPost = fxChronotrackApiOAuthApiPost('event/' . $intCtEventId . '/entry', $tabEntries);
|
||
if ($arrPost['state'] === 'ok') {
|
||
$arrPost['body_format'] = 'array';
|
||
$strBodyFormat = 'array';
|
||
return $arrPost;
|
||
}
|
||
|
||
// Un seul essai wrapper pour toute la requête PHP (pas à chaque dichotomie)
|
||
if (!$blnWrapperTried) {
|
||
$blnWrapperTried = true;
|
||
$arrPostWrap = fxChronotrackApiOAuthApiPost(
|
||
'event/' . $intCtEventId . '/entry',
|
||
array('event_entry' => $tabEntries)
|
||
);
|
||
if ($arrPostWrap['state'] === 'ok') {
|
||
$strBodyFormat = 'event_entry';
|
||
$arrPostWrap['body_format'] = 'event_entry';
|
||
return $arrPostWrap;
|
||
}
|
||
$arrPost['fallback_error'] = $arrPostWrap['message'] ?? '';
|
||
}
|
||
|
||
$arrPost['body_format'] = 'array';
|
||
return $arrPost;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4328 — logs push_entry un-pour-un (défaut = off, résumé lots seulement).
|
||
* @param bool|null $blnSet null = lire, bool = fixer pour la requête courante
|
||
*/
|
||
function fxChronotrackApiSyncVerboseEntryLogs($blnSet = null) {
|
||
static $blnVerbose = false;
|
||
if ($blnSet !== null) {
|
||
$blnVerbose = (bool)$blnSet;
|
||
}
|
||
return $blnVerbose;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4328 — log push_entry (succès). Uniquement si logs détaillés cochés.
|
||
*/
|
||
function fxChronotrackApiSyncLogPushEntryOk($intEveId, array $arrM, array $arrPayload, $strPrefix = '') {
|
||
$intEveId = intval($intEveId);
|
||
if ($intEveId <= 0 || !fxChronotrackApiSyncVerboseEntryLogs()) {
|
||
return;
|
||
}
|
||
$strBibMs1 = trim((string)($arrM['bib_ms1'] ?? $arrM['bib'] ?? ''));
|
||
$strBibSent = isset($arrPayload['bib']) ? trim((string)$arrPayload['bib']) : '';
|
||
if ($strBibSent === '') {
|
||
$strBibSent = 'non';
|
||
}
|
||
$strNoEquipe = trim((string)($arrM['no_equipe'] ?? ''));
|
||
$strMsg = ($strPrefix !== '' ? ($strPrefix . ' ') : '')
|
||
. 'external_id=' . ($arrM['external_id'] ?? '')
|
||
. ' dossard=' . ($strBibMs1 !== '' ? $strBibMs1 : '—')
|
||
. ' envoi_bib=' . $strBibSent
|
||
. ' ' . fxChronotrackApiSyncLogCountryFragment($arrPayload)
|
||
. ($strNoEquipe !== '' && $strNoEquipe !== '0' ? (' no_equipe=' . $strNoEquipe) : '')
|
||
. ' nom=' . trim((string)($arrM['name'] ?? ''));
|
||
fxChronotrackApiSyncLog($intEveId, intval($arrM['par_id'] ?? 0), 'push_entry', 'ok', trim($strMsg));
|
||
}
|
||
|
||
/**
|
||
* MSIN-4328 — log push_entry détail (conflit bib, etc.) si mode verbose.
|
||
*/
|
||
function fxChronotrackApiSyncLogPushEntryDetail($intEveId, $intParId, $strStatus, $strMessage) {
|
||
$intEveId = intval($intEveId);
|
||
if ($intEveId <= 0 || !fxChronotrackApiSyncVerboseEntryLogs()) {
|
||
return;
|
||
}
|
||
fxChronotrackApiSyncLog($intEveId, intval($intParId), 'push_entry', $strStatus, $strMessage);
|
||
}
|
||
|
||
/**
|
||
* MSIN-4328 — 1 entry : tenter avec bib ; si conflit CT → clear + file pour 2e passe.
|
||
*
|
||
* @return array{ok:int,err:int,bib_retry:array,method:string}
|
||
*/
|
||
function fxChronotrackApiSyncPushOneEntryWithBibConflictHandling(
|
||
$intCtEventId,
|
||
array $arrPayload,
|
||
array $arrMeta,
|
||
$intEveId = 0
|
||
) {
|
||
$intCtEventId = intval($intCtEventId);
|
||
$intEveId = intval($intEveId);
|
||
$strEntryId = trim((string)($arrPayload['entry_id'] ?? ''));
|
||
$strBibWanted = isset($arrPayload['bib']) ? trim((string)$arrPayload['bib']) : '';
|
||
|
||
if ($strEntryId !== '') {
|
||
$arrFirst = fxChronotrackApiSyncPutEntry($strEntryId, $arrPayload);
|
||
$strMethod = 'PUT';
|
||
} else {
|
||
$arrFirst = fxChronotrackApiSyncPostEntries(
|
||
$intCtEventId,
|
||
array(fxChronotrackApiSyncPayloadForPost($arrPayload))
|
||
);
|
||
$strMethod = 'POST';
|
||
}
|
||
|
||
if (($arrFirst['state'] ?? '') === 'ok') {
|
||
if ($intEveId > 0) {
|
||
if ($strMethod === 'POST') {
|
||
fxChronotrackApiSyncApplyEntryIdsFromApiJson($arrFirst['json'] ?? null, array($arrMeta));
|
||
}
|
||
fxChronotrackApiSyncLogPushEntryOk($intEveId, $arrMeta, $arrPayload, $strMethod);
|
||
}
|
||
return array('ok' => 1, 'err' => 0, 'bib_retry' => array(), 'method' => $strMethod);
|
||
}
|
||
|
||
$strErr = (string)($arrFirst['message'] ?? 'Erreur entry');
|
||
// Conflit dossard + on voulait en envoyer un → libérer puis 2e passe
|
||
if ($strBibWanted !== '' && fxChronotrackApiSyncIsBibConflictMessage($strErr)) {
|
||
$arrCleared = fxChronotrackApiSyncPayloadClearBib($arrPayload);
|
||
if ($strEntryId !== '') {
|
||
$arrClearRes = fxChronotrackApiSyncPutEntry($strEntryId, $arrCleared);
|
||
// Si CT refuse bib vide : maj sans champ bib, file quand même pour 2e passe
|
||
if (($arrClearRes['state'] ?? '') !== 'ok') {
|
||
$arrClearRes = fxChronotrackApiSyncPutEntry(
|
||
$strEntryId,
|
||
fxChronotrackApiSyncPayloadWithoutBib($arrPayload)
|
||
);
|
||
}
|
||
} else {
|
||
// Création : upsert sans bib (laisse l’entry passer)
|
||
$arrClearRes = fxChronotrackApiSyncPostEntries(
|
||
$intCtEventId,
|
||
array(fxChronotrackApiSyncPayloadForPost(fxChronotrackApiSyncPayloadWithoutBib($arrPayload)))
|
||
);
|
||
}
|
||
|
||
if (($arrClearRes['state'] ?? '') === 'ok') {
|
||
if ($intEveId > 0) {
|
||
if ($strEntryId === '') {
|
||
fxChronotrackApiSyncApplyEntryIdsFromApiJson($arrClearRes['json'] ?? null, array($arrMeta));
|
||
}
|
||
fxChronotrackApiSyncLogPushEntryDetail(
|
||
$intEveId,
|
||
intval($arrMeta['par_id'] ?? 0),
|
||
'ok',
|
||
'conflit_bib → sans dossard (retry plus tard) external_id='
|
||
. ($arrMeta['external_id'] ?? '')
|
||
. ' dossard_voulu=' . $strBibWanted
|
||
. ' nom=' . trim((string)($arrMeta['name'] ?? ''))
|
||
);
|
||
}
|
||
// Reprendre entry_id si la création vient de réussir
|
||
if ($strEntryId === '' && is_array($arrClearRes['json'] ?? null)) {
|
||
$arrTmp = $arrPayload;
|
||
// ApplyEntryIds a pu écrire en BD ; pour le retry on préfère entry_id si dispo dans la réponse
|
||
$arrEntities = fxChronotrackApiNormalizeEntityList($arrClearRes['json'], 'entry');
|
||
if (isset($arrEntities[0]) && is_array($arrEntities[0])) {
|
||
$strNewId = fxChronotrackApiEntityId($arrEntities[0]);
|
||
if ($strNewId !== '') {
|
||
$arrTmp['entry_id'] = $strNewId;
|
||
}
|
||
}
|
||
$arrPayload = $arrTmp;
|
||
}
|
||
$arrRetryPayload = $arrPayload;
|
||
$arrRetryPayload['bib'] = $strBibWanted;
|
||
return array(
|
||
'ok' => 1,
|
||
'err' => 0,
|
||
'bib_retry' => array(array(
|
||
'payload' => $arrRetryPayload,
|
||
'meta' => $arrMeta,
|
||
)),
|
||
'method' => $strMethod,
|
||
);
|
||
}
|
||
|
||
// Clear a aussi échoué → vraie erreur
|
||
$strErr = 'conflit_bib + clear échoué — ' . ($arrClearRes['message'] ?? $strErr);
|
||
}
|
||
|
||
if ($intEveId > 0) {
|
||
fxChronotrackApiSyncLog(
|
||
$intEveId,
|
||
intval($arrMeta['par_id'] ?? 0),
|
||
'push_entry',
|
||
'error',
|
||
'external_id=' . ($arrMeta['external_id'] ?? '')
|
||
. ' ' . fxChronotrackApiSyncLogCountryFragment($arrPayload)
|
||
. ' — ' . $strErr
|
||
);
|
||
}
|
||
return array('ok' => 0, 'err' => 1, 'bib_retry' => array(), 'method' => $strMethod);
|
||
}
|
||
|
||
/**
|
||
* MSIN-4328 — payloads pour POST API (sans entry_id ; le PUT unitaire garde entry_id).
|
||
*
|
||
* @param array $tabEntries
|
||
* @return array
|
||
*/
|
||
function fxChronotrackApiSyncPayloadsForPostApi(array $tabEntries) {
|
||
$tabOut = array();
|
||
foreach ($tabEntries as $arrPayload) {
|
||
if (!is_array($arrPayload)) {
|
||
continue;
|
||
}
|
||
$tabOut[] = fxChronotrackApiSyncPayloadForPost($arrPayload);
|
||
}
|
||
return $tabOut;
|
||
}
|
||
|
||
/**
|
||
* POST entries avec sous-lots fixes si le lot échoue (évite explosion d’appels).
|
||
* MSIN-4328 — en conflit bib sur 1 entry : clear + file pour 2e passe.
|
||
* Les payloads peuvent encore contenir entry_id (conservé pour le clear PUT) ;
|
||
* il est retiré uniquement à l’appel POST API.
|
||
*
|
||
* @return array{ok:int,err:int,bib_retry:array}
|
||
*/
|
||
function fxChronotrackApiSyncPostEntriesAdaptive($intCtEventId, array $tabEntries, array &$tabErrors, $intEveId = 0, array $tabMeta = array()) {
|
||
$intCount = count($tabEntries);
|
||
if ($intCount === 0) {
|
||
return array('ok' => 0, 'err' => 0, 'bib_retry' => array());
|
||
}
|
||
|
||
// MSIN-4328 — 1 entry déjà liée CT : PUT (+ clear bib si swap), pas POST upsert.
|
||
if ($intCount === 1) {
|
||
$arrPayload0 = $tabEntries[0];
|
||
$strEntryId0 = is_array($arrPayload0) ? trim((string)($arrPayload0['entry_id'] ?? '')) : '';
|
||
if ($strEntryId0 !== '') {
|
||
return fxChronotrackApiSyncPushOneEntryWithBibConflictHandling(
|
||
$intCtEventId,
|
||
$arrPayload0,
|
||
$tabMeta[0] ?? array('par_id' => 0, 'external_id' => ''),
|
||
$intEveId
|
||
);
|
||
}
|
||
}
|
||
|
||
$arrPost = fxChronotrackApiSyncPostEntries(
|
||
$intCtEventId,
|
||
fxChronotrackApiSyncPayloadsForPostApi($tabEntries)
|
||
);
|
||
if ($arrPost['state'] === 'ok') {
|
||
if ($intEveId > 0 && count($tabMeta) > 0) {
|
||
fxChronotrackApiSyncApplyEntryIdsFromApiJson($arrPost['json'] ?? null, $tabMeta);
|
||
foreach ($tabMeta as $intI => $arrM) {
|
||
if (!is_array($arrM)) {
|
||
continue;
|
||
}
|
||
$arrPayload = $tabEntries[$intI] ?? array();
|
||
fxChronotrackApiSyncLogPushEntryOk($intEveId, $arrM, $arrPayload, 'POST');
|
||
}
|
||
}
|
||
return array('ok' => $intCount, 'err' => 0, 'bib_retry' => array());
|
||
}
|
||
|
||
if ($intCount === 1) {
|
||
$arrPayload = $tabEntries[0];
|
||
$arrM = $tabMeta[0] ?? array('par_id' => 0, 'external_id' => '');
|
||
$strOneErr = $arrPost['message'] ?? 'Erreur POST entry';
|
||
$strBibWanted = isset($arrPayload['bib']) ? trim((string)$arrPayload['bib']) : '';
|
||
|
||
if ($strBibWanted !== '' && fxChronotrackApiSyncIsBibConflictMessage($strOneErr)) {
|
||
// MSIN-4328 — conflit sans entry_id : tenter clear bib='' puis sans champ bib
|
||
$strEntryIdOne = trim((string)($arrPayload['entry_id'] ?? ''));
|
||
if ($strEntryIdOne !== '') {
|
||
$arrClearRes = fxChronotrackApiSyncPutEntry(
|
||
$strEntryIdOne,
|
||
fxChronotrackApiSyncPayloadClearBib($arrPayload)
|
||
);
|
||
if (($arrClearRes['state'] ?? '') !== 'ok') {
|
||
$arrClearRes = fxChronotrackApiSyncPutEntry(
|
||
$strEntryIdOne,
|
||
fxChronotrackApiSyncPayloadWithoutBib($arrPayload)
|
||
);
|
||
}
|
||
} else {
|
||
// bib='' d’abord (libérer le tag) ; fallback sans champ bib
|
||
$arrClearRes = fxChronotrackApiSyncPostEntries(
|
||
$intCtEventId,
|
||
array(fxChronotrackApiSyncPayloadForPost(
|
||
fxChronotrackApiSyncPayloadClearBib($arrPayload)
|
||
))
|
||
);
|
||
if (($arrClearRes['state'] ?? '') !== 'ok') {
|
||
$arrClearRes = fxChronotrackApiSyncPostEntries(
|
||
$intCtEventId,
|
||
array(fxChronotrackApiSyncPayloadForPost(
|
||
fxChronotrackApiSyncPayloadWithoutBib($arrPayload)
|
||
))
|
||
);
|
||
}
|
||
}
|
||
if (($arrClearRes['state'] ?? '') === 'ok') {
|
||
if ($intEveId > 0) {
|
||
if ($strEntryIdOne === '') {
|
||
fxChronotrackApiSyncApplyEntryIdsFromApiJson($arrClearRes['json'] ?? null, array($arrM));
|
||
}
|
||
fxChronotrackApiSyncLogPushEntryDetail(
|
||
$intEveId,
|
||
intval($arrM['par_id'] ?? 0),
|
||
'ok',
|
||
'conflit_bib → sans dossard (retry plus tard) external_id='
|
||
. ($arrM['external_id'] ?? '')
|
||
. ' dossard_voulu=' . $strBibWanted
|
||
. ' nom=' . trim((string)($arrM['name'] ?? ''))
|
||
);
|
||
}
|
||
$arrRetryPayload = $arrPayload;
|
||
$arrRetryPayload['bib'] = $strBibWanted;
|
||
if ($strEntryIdOne !== '') {
|
||
$arrRetryPayload['entry_id'] = $strEntryIdOne;
|
||
} else {
|
||
$arrEntities = fxChronotrackApiNormalizeEntityList($arrClearRes['json'] ?? null, 'entry');
|
||
if (isset($arrEntities[0]) && is_array($arrEntities[0])) {
|
||
$strNewId = fxChronotrackApiEntityId($arrEntities[0]);
|
||
if ($strNewId !== '') {
|
||
$arrRetryPayload['entry_id'] = $strNewId;
|
||
}
|
||
}
|
||
}
|
||
return array(
|
||
'ok' => 1,
|
||
'err' => 0,
|
||
'bib_retry' => array(array(
|
||
'payload' => $arrRetryPayload,
|
||
'meta' => $arrM,
|
||
)),
|
||
);
|
||
}
|
||
$strOneErr = 'conflit_bib + envoi sans dossard échoué — '
|
||
. ($arrClearRes['message'] ?? $strOneErr);
|
||
}
|
||
|
||
if (count($tabErrors) < 12) {
|
||
$tabErrors[] = 'external_id=' . ($arrM['external_id'] ?? '') . ' — ' . $strOneErr;
|
||
}
|
||
if ($intEveId > 0) {
|
||
fxChronotrackApiSyncLog(
|
||
$intEveId,
|
||
intval($arrM['par_id'] ?? 0),
|
||
'push_entry',
|
||
'error',
|
||
'external_id=' . ($arrM['external_id'] ?? '')
|
||
. ' ' . fxChronotrackApiSyncLogCountryFragment($arrPayload)
|
||
. ' — ' . $strOneErr
|
||
);
|
||
}
|
||
return array('ok' => 0, 'err' => 1, 'bib_retry' => array());
|
||
}
|
||
|
||
// Sous-lots : 100 → 25 → 5 → 1 (échecs / conflits seulement)
|
||
$intSubSize = ($intCount > 100) ? 100 : (($intCount > 25) ? 25 : (($intCount > 5) ? 5 : 1));
|
||
$intOk = 0;
|
||
$intErr = 0;
|
||
$tabBibRetry = array();
|
||
for ($intI = 0; $intI < $intCount; $intI += $intSubSize) {
|
||
$arrRes = fxChronotrackApiSyncPostEntriesAdaptive(
|
||
$intCtEventId,
|
||
array_slice($tabEntries, $intI, $intSubSize),
|
||
$tabErrors,
|
||
$intEveId,
|
||
array_slice($tabMeta, $intI, $intSubSize)
|
||
);
|
||
$intOk += $arrRes['ok'];
|
||
$intErr += $arrRes['err'];
|
||
if (!empty($arrRes['bib_retry']) && is_array($arrRes['bib_retry'])) {
|
||
foreach ($arrRes['bib_retry'] as $arrR) {
|
||
$tabBibRetry[] = $arrR;
|
||
}
|
||
}
|
||
}
|
||
return array(
|
||
'ok' => $intOk,
|
||
'err' => $intErr,
|
||
'bib_retry' => $tabBibRetry,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* MSIN-4328 / MSIN-4574 — 2e passe dossards (swaps) avec gestion de conflit.
|
||
* Plusieurs tours : clear → réassign, pour les échanges croisés (A↔B).
|
||
*
|
||
* @return array{ok:int,err:int,errors:array}
|
||
*/
|
||
function fxChronotrackApiSyncPushPendingBibs($intCtEventId, array $tabBibRetry, $intEveId = 0) {
|
||
$intOk = 0;
|
||
$intErr = 0;
|
||
$tabErrors = array();
|
||
$intEveId = intval($intEveId);
|
||
$intCtEventId = intval($intCtEventId);
|
||
$tabPending = array();
|
||
|
||
foreach ($tabBibRetry as $arrItem) {
|
||
if (!is_array($arrItem) || !is_array($arrItem['payload'] ?? null)) {
|
||
continue;
|
||
}
|
||
$strBib = isset($arrItem['payload']['bib']) ? trim((string)$arrItem['payload']['bib']) : '';
|
||
if ($strBib === '') {
|
||
continue;
|
||
}
|
||
$tabPending[] = $arrItem;
|
||
}
|
||
|
||
// MSIN-4574 — jusqu’à 5 tours (swap circulaire / dossard encore tenu)
|
||
$intMaxRounds = 5;
|
||
for ($intRound = 1; $intRound <= $intMaxRounds && count($tabPending) > 0; $intRound++) {
|
||
$tabNext = array();
|
||
$intRoundOk = 0;
|
||
$intRoundQueued = 0;
|
||
|
||
foreach ($tabPending as $arrItem) {
|
||
$arrPayload = $arrItem['payload'];
|
||
$arrMeta = is_array($arrItem['meta'] ?? null) ? $arrItem['meta'] : array();
|
||
$arrRes = fxChronotrackApiSyncPushOneEntryWithBibConflictHandling(
|
||
$intCtEventId,
|
||
$arrPayload,
|
||
$arrMeta,
|
||
$intEveId
|
||
);
|
||
|
||
if (intval($arrRes['ok'] ?? 0) > 0 && empty($arrRes['bib_retry'])) {
|
||
$intOk++;
|
||
$intRoundOk++;
|
||
continue;
|
||
}
|
||
|
||
if (!empty($arrRes['bib_retry']) && is_array($arrRes['bib_retry'])) {
|
||
foreach ($arrRes['bib_retry'] as $arrR) {
|
||
if (is_array($arrR)) {
|
||
$tabNext[] = $arrR;
|
||
$intRoundQueued++;
|
||
}
|
||
}
|
||
continue;
|
||
}
|
||
|
||
$intErr++;
|
||
$strBib = isset($arrPayload['bib']) ? trim((string)$arrPayload['bib']) : '';
|
||
$strMsg = 'RETRY_BIB round=' . $intRound
|
||
. ' external_id=' . ($arrMeta['external_id'] ?? '')
|
||
. ' dossard=' . $strBib
|
||
. ' — échec';
|
||
if (count($tabErrors) < 20) {
|
||
$tabErrors[] = $strMsg;
|
||
}
|
||
if ($intEveId > 0) {
|
||
fxChronotrackApiSyncLog(
|
||
$intEveId,
|
||
intval($arrMeta['par_id'] ?? 0),
|
||
'push_entry',
|
||
'error',
|
||
$strMsg
|
||
);
|
||
}
|
||
}
|
||
|
||
if ($intEveId > 0) {
|
||
fxChronotrackApiSyncLog(
|
||
$intEveId,
|
||
0,
|
||
'push_bib_retry',
|
||
'ok',
|
||
'round=' . $intRound
|
||
. ' ok=' . $intRoundOk
|
||
. ' queued=' . $intRoundQueued
|
||
. ' pending_next=' . count($tabNext)
|
||
);
|
||
}
|
||
|
||
// Aucun assign OK et aucun clear/queue → plus de progrès
|
||
if (count($tabNext) > 0 && $intRoundOk === 0 && $intRoundQueued === 0) {
|
||
foreach ($tabNext as $arrLeft) {
|
||
$intErr++;
|
||
$arrMetaL = is_array($arrLeft['meta'] ?? null) ? $arrLeft['meta'] : array();
|
||
$strBibL = isset($arrLeft['payload']['bib']) ? trim((string)$arrLeft['payload']['bib']) : '';
|
||
$strMsgL = 'RETRY_BIB bloqué (swap) external_id='
|
||
. ($arrMetaL['external_id'] ?? '')
|
||
. ' dossard=' . $strBibL;
|
||
if (count($tabErrors) < 20) {
|
||
$tabErrors[] = $strMsgL;
|
||
}
|
||
}
|
||
$tabPending = array();
|
||
break;
|
||
}
|
||
|
||
$tabPending = $tabNext;
|
||
}
|
||
|
||
// Reste après max tours
|
||
foreach ($tabPending as $arrLeft) {
|
||
$intErr++;
|
||
$arrMetaL = is_array($arrLeft['meta'] ?? null) ? $arrLeft['meta'] : array();
|
||
$strBibL = isset($arrLeft['payload']['bib']) ? trim((string)$arrLeft['payload']['bib']) : '';
|
||
$strMsgL = 'RETRY_BIB max tours external_id='
|
||
. ($arrMetaL['external_id'] ?? '')
|
||
. ' dossard=' . $strBibL;
|
||
if (count($tabErrors) < 20) {
|
||
$tabErrors[] = $strMsgL;
|
||
}
|
||
}
|
||
|
||
return array('ok' => $intOk, 'err' => $intErr, 'errors' => $tabErrors);
|
||
}
|
||
|
||
/**
|
||
* MSIN-4574 — sonde lecture seule pays/province CT (GET entry, aucun PUT/POST).
|
||
* Pour calibrer le format stocké quand l’UI Athlete Info est correcte (sans écraser).
|
||
*
|
||
* @param int $intEveId
|
||
* @param array $arrOptions external_id optionnel pour cibler un participant
|
||
*/
|
||
function fxChronotrackApiSyncProbePaysRead($intEveId, $arrOptions = array()) {
|
||
$intEveId = intval($intEveId);
|
||
$arrConfig = fxChronotrackApiConfigGet($intEveId);
|
||
if ($arrConfig === null) {
|
||
return array('state' => 'error', 'message' => 'Événement non lié à ChronoTrack');
|
||
}
|
||
if (!fxChronotrackApiSettingsConfigured()) {
|
||
return array('state' => 'error', 'message' => fxChronotrackApiSettingsErrorMessage());
|
||
}
|
||
|
||
$intCtEventId = intval($arrConfig['ct_event_id']);
|
||
$arrRaceMap = fxChronotrackApiRaceMapGetForEvent($intEveId);
|
||
$tabParticipants = fxChronotrackApiSyncLoadMs1Participants($intEveId);
|
||
$arrClass = fxChronotrackApiSyncClassifyParticipants(
|
||
$tabParticipants,
|
||
$arrRaceMap,
|
||
$intCtEventId,
|
||
false
|
||
);
|
||
|
||
if (count($arrClass['transferable']) === 0) {
|
||
return array('state' => 'error', 'message' => 'Aucun participant transférable pour la lecture.');
|
||
}
|
||
|
||
$arrFirst = null;
|
||
$strWantExt = trim((string)($arrOptions['external_id'] ?? ''));
|
||
$strWantBib = trim((string)($arrOptions['bib'] ?? ''));
|
||
$strWantName = trim((string)($arrOptions['name'] ?? ''));
|
||
|
||
if ($strWantExt !== '') {
|
||
foreach ($arrClass['transferable'] as $arrItem) {
|
||
$arrRowTry = $arrItem['row'] ?? array();
|
||
$strExtTry = trim((string)($arrRowTry['par_id_original'] ?? ''));
|
||
if ($strExtTry === $strWantExt) {
|
||
$arrFirst = $arrItem;
|
||
break;
|
||
}
|
||
}
|
||
if ($arrFirst === null) {
|
||
return array(
|
||
'state' => 'error',
|
||
'message' => 'external_id ' . $strWantExt . ' introuvable parmi les éligibles MS1.',
|
||
);
|
||
}
|
||
} elseif ($strWantBib !== '' && $strWantBib !== '0') {
|
||
foreach ($arrClass['transferable'] as $arrItem) {
|
||
$arrRowTry = $arrItem['row'] ?? array();
|
||
$strBibTry = fxChronotrackApiSyncExtractBib($arrRowTry);
|
||
if ($strBibTry !== '' && $strBibTry === $strWantBib) {
|
||
$arrFirst = $arrItem;
|
||
break;
|
||
}
|
||
}
|
||
if ($arrFirst === null) {
|
||
return array(
|
||
'state' => 'error',
|
||
'message' => 'Dossard ' . $strWantBib . ' introuvable parmi les éligibles MS1.',
|
||
);
|
||
}
|
||
} elseif ($strWantName !== '') {
|
||
$strWantNameLow = function_exists('mb_strtolower')
|
||
? mb_strtolower($strWantName)
|
||
: strtolower($strWantName);
|
||
foreach ($arrClass['transferable'] as $arrItem) {
|
||
$arrRowTry = $arrItem['row'] ?? array();
|
||
$strNom = trim((string)(($arrRowTry['par_prenom'] ?? '') . ' ' . ($arrRowTry['par_nom'] ?? '')));
|
||
$strNomLow = function_exists('mb_strtolower') ? mb_strtolower($strNom) : strtolower($strNom);
|
||
if ($strNom !== '' && (
|
||
(function_exists('mb_strpos') && mb_strpos($strNomLow, $strWantNameLow) !== false)
|
||
|| (!function_exists('mb_strpos') && strpos($strNomLow, $strWantNameLow) !== false)
|
||
)) {
|
||
$arrFirst = $arrItem;
|
||
break;
|
||
}
|
||
}
|
||
if ($arrFirst === null) {
|
||
return array(
|
||
'state' => 'error',
|
||
'message' => 'Aucun éligible dont le nom contient « ' . $strWantName . ' ».',
|
||
);
|
||
}
|
||
} else {
|
||
// Défaut : participant avec pays + déjà lié CT (sinon premier avec pays)
|
||
foreach ($arrClass['transferable'] as $arrItem) {
|
||
$arrRowTry = $arrItem['row'] ?? array();
|
||
if (fxChronotrackApiSyncResolveCountryCode($arrRowTry) === '') {
|
||
continue;
|
||
}
|
||
if (intval($arrRowTry['ct_entry_id'] ?? 0) > 0) {
|
||
$arrFirst = $arrItem;
|
||
break;
|
||
}
|
||
if ($arrFirst === null) {
|
||
$arrFirst = $arrItem;
|
||
}
|
||
}
|
||
if ($arrFirst === null) {
|
||
$arrFirst = $arrClass['transferable'][0];
|
||
}
|
||
}
|
||
|
||
$arrRow = $arrFirst['row'] ?? array();
|
||
$strExt = trim((string)($arrRow['par_id_original'] ?? ''));
|
||
if ($strExt === '' || $strExt === '0') {
|
||
$strExt = (string)intval($arrRow['par_id_original'] ?? 0);
|
||
}
|
||
$intCtEntryId = intval($arrRow['ct_entry_id'] ?? 0);
|
||
|
||
if ($intCtEntryId <= 0 && $strExt !== '' && $strExt !== '0') {
|
||
$arrCtFetch = fxChronotrackApiSyncFetchCtEntriesIndexed($intCtEventId);
|
||
if (($arrCtFetch['state'] ?? '') === 'ok') {
|
||
$arrEnt = $arrCtFetch['entries'][$strExt] ?? null;
|
||
if (is_array($arrEnt)) {
|
||
$intCtEntryId = intval(fxChronotrackApiEntityId($arrEnt));
|
||
}
|
||
}
|
||
}
|
||
|
||
if ($intCtEntryId <= 0) {
|
||
return array(
|
||
'state' => 'error',
|
||
'message' => 'Pas de ct_entry_id pour external_id=' . $strExt
|
||
. ' — impossible de lire sans écrire. Liez d’abord via un push, ou saisissez un entry_id connu.',
|
||
'external_id' => $strExt,
|
||
'par_id' => intval($arrRow['par_id'] ?? 0),
|
||
);
|
||
}
|
||
|
||
$arrGet = fxChronotrackApiOAuthApiGet('entry/' . $intCtEntryId);
|
||
if (($arrGet['state'] ?? '') !== 'ok') {
|
||
return array(
|
||
'state' => 'error',
|
||
'message' => $arrGet['message'] ?? 'Erreur GET entry',
|
||
'ct_entry_id' => $intCtEntryId,
|
||
'external_id' => $strExt,
|
||
'http_code' => intval($arrGet['http']['http_code'] ?? 0),
|
||
);
|
||
}
|
||
|
||
$arrRb = is_array($arrGet['json'] ?? null) ? $arrGet['json'] : array();
|
||
if (isset($arrRb['entry']) && is_array($arrRb['entry'])) {
|
||
$arrRb = $arrRb['entry'];
|
||
} elseif (isset($arrRb['event_entry'][0]) && is_array($arrRb['event_entry'][0])) {
|
||
$arrRb = $arrRb['event_entry'][0];
|
||
}
|
||
|
||
$arrLocation = array(
|
||
'location_country' => trim((string)($arrRb['location_country'] ?? '')),
|
||
'location_region' => trim((string)($arrRb['location_region'] ?? '')),
|
||
'location_city' => trim((string)($arrRb['location_city'] ?? '')),
|
||
'location_postal_code' => trim((string)($arrRb['location_postal_code'] ?? '')),
|
||
'location_street' => trim((string)($arrRb['location_street'] ?? '')),
|
||
'country_code' => trim((string)($arrRb['country_code'] ?? '')),
|
||
'country_name' => trim((string)($arrRb['country_name'] ?? '')),
|
||
'state_code' => trim((string)($arrRb['state_code'] ?? '')),
|
||
'state_name' => trim((string)($arrRb['state_name'] ?? '')),
|
||
'city' => trim((string)($arrRb['city'] ?? '')),
|
||
'postal_code' => trim((string)($arrRb['postal_code'] ?? '')),
|
||
'first_name' => trim((string)($arrRb['first_name'] ?? '')),
|
||
'last_name' => trim((string)($arrRb['last_name'] ?? '')),
|
||
'bib' => trim((string)($arrRb['bib'] ?? '')),
|
||
);
|
||
|
||
$strNameMs1 = trim((string)(($arrRow['par_prenom'] ?? '') . ' ' . ($arrRow['par_nom'] ?? '')));
|
||
$strBibMs1 = fxChronotrackApiSyncExtractBib($arrRow);
|
||
$strNameCt = trim((string)(
|
||
($arrLocation['first_name'] !== '' || $arrLocation['last_name'] !== '')
|
||
? ($arrLocation['first_name'] . ' ' . $arrLocation['last_name'])
|
||
: ($arrRb['entry_name'] ?? $arrRb['name'] ?? '')
|
||
));
|
||
|
||
return array(
|
||
'state' => 'ok',
|
||
'message' => 'Lecture seule OK — aucune écriture CT.',
|
||
'read_only' => true,
|
||
'ct_event_id' => $intCtEventId,
|
||
'ct_entry_id' => $intCtEntryId,
|
||
'external_id' => $strExt,
|
||
'par_id' => intval($arrRow['par_id'] ?? 0),
|
||
'name' => $strNameMs1,
|
||
'bib' => $strBibMs1,
|
||
'how' => 'GET entry/' . $intCtEntryId,
|
||
// Bloc identité lisible (humain)
|
||
'who' => array(
|
||
'name_ms1' => $strNameMs1,
|
||
'name_ct' => $strNameCt,
|
||
'bib_ms1' => $strBibMs1,
|
||
'bib_ct' => $arrLocation['bib'],
|
||
'city_ms1' => trim((string)($arrRow['par_ville'] ?? '')),
|
||
'street_ms1' => trim((string)($arrRow['par_adresse'] ?? '')),
|
||
'postal_ms1'=> trim((string)($arrRow['par_codepostal'] ?? '')),
|
||
),
|
||
'ms1' => array(
|
||
'pay_id' => intval($arrRow['pay_id'] ?? 0),
|
||
'pro_id' => intval($arrRow['pro_id'] ?? 0),
|
||
'country_iso2' => trim((string)($arrRow['country_iso2'] ?? '')),
|
||
'country_iso3' => trim((string)($arrRow['country_iso3'] ?? '')),
|
||
'country_code' => fxChronotrackApiSyncResolveCountryCode($arrRow),
|
||
'country_label' => fxChronotrackApiSyncResolveCountryLabel($arrRow),
|
||
'state_iso' => strtoupper(trim((string)($arrRow['state_iso'] ?? ''))),
|
||
),
|
||
'ct_location' => $arrLocation,
|
||
'http_code' => intval($arrGet['http']['http_code'] ?? 0),
|
||
'raw_keys' => array_keys($arrRb),
|
||
);
|
||
}
|
||
|
||
function fxChronotrackApiSyncProbePush($intEveId) {
|
||
$arrConfig = fxChronotrackApiConfigGet($intEveId);
|
||
if ($arrConfig === null) {
|
||
return array('state' => 'error', 'message' => 'Événement non lié à ChronoTrack');
|
||
}
|
||
|
||
$intCtEventId = intval($arrConfig['ct_event_id']);
|
||
$arrRaceMap = fxChronotrackApiRaceMapGetForEvent($intEveId);
|
||
$tabParticipants = fxChronotrackApiSyncLoadMs1Participants($intEveId);
|
||
$arrClass = fxChronotrackApiSyncClassifyParticipants($tabParticipants, $arrRaceMap, $intCtEventId);
|
||
|
||
if (count($arrClass['transferable']) === 0) {
|
||
return array('state' => 'error', 'message' => 'Aucun participant transférable pour la sonde (voir anomalies).');
|
||
}
|
||
|
||
// MSIN-4574 — préférer un participant avec pays MS1 pour valider location_country
|
||
$arrFirst = null;
|
||
foreach ($arrClass['transferable'] as $arrItem) {
|
||
$arrRowTry = $arrItem['row'] ?? array();
|
||
if (fxChronotrackApiSyncResolveCountryCode($arrRowTry) !== '') {
|
||
$arrFirst = $arrItem;
|
||
break;
|
||
}
|
||
}
|
||
if ($arrFirst === null) {
|
||
$arrFirst = $arrClass['transferable'][0];
|
||
}
|
||
$arrPayload = $arrFirst['payload'];
|
||
$arrRow = $arrFirst['row'];
|
||
$arrMeta = array(
|
||
'par_id' => intval($arrRow['par_id'] ?? 0),
|
||
'external_id' => (string)($arrPayload['external_id'] ?? ''),
|
||
'name' => trim((string)(($arrRow['par_prenom'] ?? '') . ' ' . ($arrRow['par_nom'] ?? ''))),
|
||
);
|
||
|
||
$arrCtFetch = fxChronotrackApiSyncFetchCtEntriesIndexed($intCtEventId);
|
||
if ($arrCtFetch['state'] === 'ok') {
|
||
$strExt = $arrPayload['external_id'];
|
||
$arrPayload = fxChronotrackApiSyncEnrichPayloadWithCtEntry(
|
||
$arrPayload,
|
||
$arrCtFetch['entries'][$strExt] ?? null
|
||
);
|
||
}
|
||
|
||
// MSIN-4574 — sonde pays : ne pas échouer sur conflit dossard (cause typique « Erreur » + CT inchangé)
|
||
$strEntryId = trim((string)($arrPayload['entry_id'] ?? ''));
|
||
if ($strEntryId !== '') {
|
||
$arrLocOnly = array(
|
||
'external_id' => (string)($arrPayload['external_id'] ?? ''),
|
||
'first_name' => (string)($arrPayload['first_name'] ?? ''),
|
||
'last_name' => (string)($arrPayload['last_name'] ?? ''),
|
||
'sex' => (string)($arrPayload['sex'] ?? ''),
|
||
'entry_status' => (string)($arrPayload['entry_status'] ?? 'CONF'),
|
||
'status' => (string)($arrPayload['status'] ?? 'CONF'),
|
||
);
|
||
if (!empty($arrPayload['race_id'])) {
|
||
$arrLocOnly['race_id'] = $arrPayload['race_id'];
|
||
}
|
||
if (!empty($arrPayload['event_id'])) {
|
||
$arrLocOnly['event_id'] = $arrPayload['event_id'];
|
||
}
|
||
if (!empty($arrPayload['location_street'])) {
|
||
$arrLocOnly['location_street'] = $arrPayload['location_street'];
|
||
}
|
||
if (!empty($arrPayload['location_city'])) {
|
||
$arrLocOnly['location_city'] = $arrPayload['location_city'];
|
||
}
|
||
if (!empty($arrPayload['location_postal_code'])) {
|
||
$arrLocOnly['location_postal_code'] = $arrPayload['location_postal_code'];
|
||
}
|
||
if (!empty($arrPayload['location_region'])) {
|
||
$arrLocOnly['location_region'] = $arrPayload['location_region'];
|
||
}
|
||
if (!empty($arrPayload['location_country'])) {
|
||
$arrLocOnly['location_country'] = $arrPayload['location_country'];
|
||
}
|
||
if (!empty($arrPayload['country_code'])) {
|
||
$arrLocOnly['country_code'] = $arrPayload['country_code'];
|
||
}
|
||
if (!empty($arrPayload['country_name'])) {
|
||
$arrLocOnly['country_name'] = $arrPayload['country_name'];
|
||
}
|
||
$arrPost = fxChronotrackApiSyncPutEntry($strEntryId, $arrLocOnly);
|
||
$arrPost['method'] = 'PUT_location';
|
||
// payload affiché = ce qui a été vraiment tenté pour le pays
|
||
$arrPayload = array_merge($arrPayload, $arrLocOnly);
|
||
$arrPayload['entry_id'] = $strEntryId;
|
||
} else {
|
||
$arrPost = fxChronotrackApiSyncPushOneEntryWithBibConflictHandling(
|
||
$intCtEventId,
|
||
$arrPayload,
|
||
$arrMeta,
|
||
$intEveId
|
||
);
|
||
// normaliser forme retour (ok/err → state)
|
||
if (!isset($arrPost['state'])) {
|
||
$arrPost['state'] = (intval($arrPost['ok'] ?? 0) > 0) ? 'ok' : 'error';
|
||
$arrPost['message'] = ($arrPost['state'] === 'ok')
|
||
? 'OK'
|
||
: (isset($arrPost['errors'][0]) ? $arrPost['errors'][0] : 'Échec sonde');
|
||
$arrPost['method'] = 'POST_bib_safe';
|
||
$arrPost['http'] = array('http_code' => 0, 'body' => '');
|
||
}
|
||
}
|
||
|
||
// Relecture pays CT (meta = location_country)
|
||
$arrCountryReadback = array(
|
||
'location_country' => '',
|
||
'location_region' => '',
|
||
'country_code' => '',
|
||
'country_name' => '',
|
||
'how' => '',
|
||
);
|
||
$intCtEntryId = intval($arrPayload['entry_id'] ?? $arrRow['ct_entry_id'] ?? 0);
|
||
if ($intCtEntryId <= 0 && ($arrPost['state'] ?? '') === 'ok') {
|
||
$arrCtFetch2 = fxChronotrackApiSyncFetchCtEntriesIndexed($intCtEventId);
|
||
if (($arrCtFetch2['state'] ?? '') === 'ok') {
|
||
$arrEnt = $arrCtFetch2['entries'][(string)$arrPayload['external_id']] ?? null;
|
||
if (is_array($arrEnt)) {
|
||
$intCtEntryId = intval(fxChronotrackApiEntityId($arrEnt));
|
||
}
|
||
}
|
||
}
|
||
if ($intCtEntryId > 0) {
|
||
$arrGet = fxChronotrackApiOAuthApiGet('entry/' . $intCtEntryId);
|
||
$arrCountryReadback['how'] = 'GET entry/' . $intCtEntryId;
|
||
if (($arrGet['state'] ?? '') === 'ok' && is_array($arrGet['json'] ?? null)) {
|
||
$arrRb = $arrGet['json'];
|
||
if (isset($arrRb['entry']) && is_array($arrRb['entry'])) {
|
||
$arrRb = $arrRb['entry'];
|
||
} elseif (isset($arrRb['event_entry'][0]) && is_array($arrRb['event_entry'][0])) {
|
||
$arrRb = $arrRb['event_entry'][0];
|
||
}
|
||
$arrCountryReadback['location_country'] = trim((string)(
|
||
$arrRb['location_country'] ?? ''
|
||
));
|
||
$arrCountryReadback['location_region'] = trim((string)(
|
||
$arrRb['location_region'] ?? $arrRb['state_code'] ?? ''
|
||
));
|
||
$arrCountryReadback['country_code'] = trim((string)($arrRb['country_code'] ?? ''));
|
||
$arrCountryReadback['country_name'] = trim((string)($arrRb['country_name'] ?? ''));
|
||
}
|
||
}
|
||
|
||
$intWithCountry = 0;
|
||
$intNoPayId = 0;
|
||
foreach ($arrClass['transferable'] as $arrItem) {
|
||
$arrR = $arrItem['row'] ?? array();
|
||
if (intval($arrR['pay_id'] ?? 0) <= 0) {
|
||
$intNoPayId++;
|
||
}
|
||
if (fxChronotrackApiSyncResolveCountryCode($arrR) !== '') {
|
||
$intWithCountry++;
|
||
}
|
||
}
|
||
|
||
$strSentLabel = trim((string)($arrPayload['location_country'] ?? ''));
|
||
$strRbCountry = trim((string)($arrCountryReadback['location_country'] ?? ''));
|
||
$blnPaysMatch = ($strSentLabel !== '' && strcasecmp($strSentLabel, $strRbCountry) === 0);
|
||
|
||
$strState = ($arrPost['state'] ?? '') === 'ok'
|
||
? ($blnPaysMatch ? 'ok' : 'partial')
|
||
: 'error';
|
||
$strMessage = trim((string)($arrPost['message'] ?? ''));
|
||
if ($strMessage === '') {
|
||
$strMessage = ($strState === 'ok') ? 'OK' : 'Erreur';
|
||
}
|
||
if ($strState === 'partial') {
|
||
$strMessage = 'Écriture OK mais pays CT ≠ envoyé (envoyé « ' . $strSentLabel
|
||
. ' », CT « ' . ($strRbCountry !== '' ? $strRbCountry : 'vide') . ' »)';
|
||
}
|
||
|
||
return array(
|
||
'state' => $strState,
|
||
'message' => $strMessage,
|
||
'ct_event_id' => $intCtEventId,
|
||
'par_id' => intval($arrRow['par_id'] ?? 0),
|
||
'payload' => $arrPayload,
|
||
'method' => $arrPost['method'] ?? '',
|
||
'body_format' => $arrPost['body_format'] ?? '',
|
||
'http_code' => intval($arrPost['http']['http_code'] ?? 0),
|
||
'response_body' => substr(trim((string)($arrPost['http']['body'] ?? '')), 0, 2000),
|
||
'fallback_error'=> $arrPost['fallback_error'] ?? '',
|
||
// MSIN-4574 — diagnostic pays
|
||
'ms1_pay_id' => intval($arrRow['pay_id'] ?? 0),
|
||
'ms1_country_iso2' => trim((string)($arrRow['country_iso2'] ?? '')),
|
||
'ms1_country_iso3' => trim((string)($arrRow['country_iso3'] ?? '')),
|
||
'ms1_country_resolved' => fxChronotrackApiSyncResolveCountryCode($arrRow),
|
||
'ms1_country_iso2_sent'=> fxChronotrackApiSyncResolveCountryIso2($arrRow),
|
||
'ms1_country_label' => fxChronotrackApiSyncResolveCountryLabel($arrRow),
|
||
'sent_location_country'=> $strSentLabel,
|
||
'sent_country_code' => trim((string)($arrPayload['country_code'] ?? '')),
|
||
'sent_location_region' => trim((string)($arrPayload['location_region'] ?? '')),
|
||
'ct_country_readback' => $arrCountryReadback,
|
||
'pays_match' => $blnPaysMatch,
|
||
'transferable_with_country' => $intWithCountry,
|
||
'transferable_total' => count($arrClass['transferable']),
|
||
'transferable_no_pay_id' => $intNoPayId,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* MSIN-4461 — extrait une valeur entry_note_* d’un objet entry CT (clé plate ou entry_notes).
|
||
*/
|
||
function fxChronotrackApiSyncExtractEntryNoteValue($arrEntry, $strFieldKey) {
|
||
if (!is_array($arrEntry) || $strFieldKey === '') {
|
||
return array('present' => false, 'value' => null);
|
||
}
|
||
if (array_key_exists($strFieldKey, $arrEntry)) {
|
||
return array('present' => true, 'value' => $arrEntry[$strFieldKey]);
|
||
}
|
||
// Support CT : préfixe entry_note_ ; la relecture peut regrouper sous entry_notes
|
||
$strLabel = (strpos($strFieldKey, 'entry_note_') === 0)
|
||
? substr($strFieldKey, strlen('entry_note_'))
|
||
: $strFieldKey;
|
||
if (isset($arrEntry['entry_notes']) && is_array($arrEntry['entry_notes'])) {
|
||
$arrNotes = $arrEntry['entry_notes'];
|
||
if (array_key_exists($strFieldKey, $arrNotes)) {
|
||
return array('present' => true, 'value' => $arrNotes[$strFieldKey]);
|
||
}
|
||
if (array_key_exists($strLabel, $arrNotes)) {
|
||
return array('present' => true, 'value' => $arrNotes[$strLabel]);
|
||
}
|
||
foreach ($arrNotes as $mixNote) {
|
||
if (!is_array($mixNote)) {
|
||
continue;
|
||
}
|
||
$strName = trim((string)($mixNote['name'] ?? $mixNote['label'] ?? $mixNote['key'] ?? ''));
|
||
if ($strName === $strFieldKey || $strName === $strLabel || $strName === 'entry_note_' . $strLabel) {
|
||
return array(
|
||
'present' => true,
|
||
'value' => $mixNote['value'] ?? $mixNote['note'] ?? $mixNote['text'] ?? null,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
return array('present' => false, 'value' => null);
|
||
}
|
||
|
||
/**
|
||
* MSIN-4461 — sonde entry_note_test (doc CT support : custom notes = préfixe entry_note_).
|
||
* Préfère PUT entry/{id} si ct_entry_id connu ; sinon POST event/.../entry.
|
||
*/
|
||
function fxChronotrackApiSyncProbeCustomField($intEveId) {
|
||
$arrConfig = fxChronotrackApiConfigGet($intEveId);
|
||
if ($arrConfig === null) {
|
||
return array('state' => 'error', 'message' => 'Événement non lié à ChronoTrack');
|
||
}
|
||
if (!fxChronotrackApiSettingsConfigured()) {
|
||
return array('state' => 'error', 'message' => fxChronotrackApiSettingsErrorMessage());
|
||
}
|
||
|
||
$intCtEventId = intval($arrConfig['ct_event_id']);
|
||
$arrRaceMap = fxChronotrackApiRaceMapGetForEvent($intEveId);
|
||
$tabParticipants = fxChronotrackApiSyncLoadMs1Participants($intEveId);
|
||
$arrClass = fxChronotrackApiSyncClassifyParticipants($tabParticipants, $arrRaceMap, $intCtEventId);
|
||
|
||
if (count($arrClass['transferable']) === 0) {
|
||
return array('state' => 'error', 'message' => 'Aucun participant transférable pour la sonde entry_note.');
|
||
}
|
||
|
||
// Préférer une entry déjà liée CT (PUT unitaire = chemin documenté par le support)
|
||
$arrPick = null;
|
||
foreach ($arrClass['transferable'] as $arrItem) {
|
||
if (intval($arrItem['row']['ct_entry_id'] ?? 0) > 0) {
|
||
$arrPick = $arrItem;
|
||
break;
|
||
}
|
||
}
|
||
if ($arrPick === null) {
|
||
$arrPick = $arrClass['transferable'][0];
|
||
}
|
||
|
||
$arrRow = $arrPick['row'];
|
||
$strExternalId = (string)($arrPick['payload']['external_id'] ?? '');
|
||
$intCtEntryId = intval($arrRow['ct_entry_id'] ?? 0);
|
||
$strFieldKey = 'entry_note_test';
|
||
$strValue = 'ms1-probe-' . date('YmdHis');
|
||
|
||
// Corps minimal : identifier + note custom (support CT 2026-07-22)
|
||
$arrProbePayload = array(
|
||
'external_id' => $strExternalId,
|
||
$strFieldKey => $strValue,
|
||
);
|
||
|
||
$strMethod = 'POST';
|
||
if ($intCtEntryId > 0) {
|
||
$arrProbePayload['entry_id'] = (string)$intCtEntryId;
|
||
$arrWrite = fxChronotrackApiSyncPutEntry((string)$intCtEntryId, $arrProbePayload);
|
||
$strMethod = 'PUT';
|
||
} else {
|
||
$arrWrite = fxChronotrackApiSyncPostEntries($intCtEventId, array($arrProbePayload));
|
||
$strMethod = 'POST';
|
||
}
|
||
|
||
$mixReadbackValue = null;
|
||
$blnFieldPresent = false;
|
||
$arrReadback = null;
|
||
$strReadbackHow = '';
|
||
|
||
if ($intCtEntryId > 0) {
|
||
$arrGet = fxChronotrackApiOAuthApiGet('entry/' . $intCtEntryId);
|
||
$strReadbackHow = 'GET entry/' . $intCtEntryId;
|
||
if (($arrGet['state'] ?? '') === 'ok' && is_array($arrGet['json'] ?? null)) {
|
||
$arrReadback = $arrGet['json'];
|
||
if (isset($arrReadback['entry']) && is_array($arrReadback['entry'])) {
|
||
$arrReadback = $arrReadback['entry'];
|
||
} elseif (isset($arrReadback['event_entry'][0]) && is_array($arrReadback['event_entry'][0])) {
|
||
$arrReadback = $arrReadback['event_entry'][0];
|
||
}
|
||
$arrFound = fxChronotrackApiSyncExtractEntryNoteValue($arrReadback, $strFieldKey);
|
||
if ($arrFound['present']) {
|
||
$blnFieldPresent = true;
|
||
$mixReadbackValue = $arrFound['value'];
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!$blnFieldPresent) {
|
||
// Repli : 1re page entries et recherche par external_id
|
||
$arrList = fxChronotrackApiOAuthApiGet(
|
||
'event/' . $intCtEventId . '/entry',
|
||
array('page' => 1, 'page_size' => 50)
|
||
);
|
||
if ($strReadbackHow === '') {
|
||
$strReadbackHow = 'GET event/.../entry page1';
|
||
} else {
|
||
$strReadbackHow .= ' + list page1';
|
||
}
|
||
$tabEnt = array();
|
||
if (($arrList['state'] ?? '') === 'ok' && is_array($arrList['json'] ?? null)) {
|
||
$tabEnt = fxChronotrackApiNormalizeEntityList($arrList['json'], 'entry');
|
||
if (count($tabEnt) === 0 && isset($arrList['json']['event_entry']) && is_array($arrList['json']['event_entry'])) {
|
||
$tabEnt = $arrList['json']['event_entry'];
|
||
}
|
||
}
|
||
foreach ($tabEnt as $arrEnt) {
|
||
if (!is_array($arrEnt)) {
|
||
continue;
|
||
}
|
||
$strExt = trim((string)(
|
||
$arrEnt['entry_external_id'] ?? $arrEnt['external_id'] ?? ''
|
||
));
|
||
if ($strExt !== $strExternalId) {
|
||
continue;
|
||
}
|
||
$arrReadback = $arrEnt;
|
||
$arrFound = fxChronotrackApiSyncExtractEntryNoteValue($arrEnt, $strFieldKey);
|
||
if ($arrFound['present']) {
|
||
$blnFieldPresent = true;
|
||
$mixReadbackValue = $arrFound['value'];
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
$strVerdict = $strMethod . ' ok mais ' . $strFieldKey . ' absent à la relecture.';
|
||
$strState = 'error';
|
||
if (($arrWrite['state'] ?? '') !== 'ok') {
|
||
$strVerdict = $strMethod . ' refusé — ' . (string)($arrWrite['message'] ?? 'erreur');
|
||
$strState = 'error';
|
||
} elseif ($blnFieldPresent && (string)$mixReadbackValue === $strValue) {
|
||
$strVerdict = 'OK — CT a accepté et conservé ' . $strFieldKey . ' via ' . $strMethod . '.';
|
||
$strState = 'ok';
|
||
} elseif ($blnFieldPresent) {
|
||
$strVerdict = $strMethod . ' ok, champ présent mais valeur différente: '
|
||
. json_encode($mixReadbackValue, JSON_UNESCAPED_UNICODE);
|
||
$strState = 'partial';
|
||
}
|
||
|
||
fxChronotrackApiSyncLog(
|
||
$intEveId,
|
||
intval($arrRow['par_id'] ?? 0),
|
||
'probe_entry_note',
|
||
($strState === 'ok') ? 'ok' : 'error',
|
||
$strVerdict
|
||
);
|
||
|
||
return array(
|
||
'state' => $strState,
|
||
'message' => $strVerdict,
|
||
'ct_event_id' => $intCtEventId,
|
||
'par_id' => intval($arrRow['par_id'] ?? 0),
|
||
'external_id' => $strExternalId,
|
||
'ct_entry_id' => $intCtEntryId,
|
||
'method' => $strMethod,
|
||
'field_key' => $strFieldKey,
|
||
'field_value_sent' => $strValue,
|
||
'field_present' => $blnFieldPresent,
|
||
'field_value_read' => $mixReadbackValue,
|
||
'payload' => $arrProbePayload,
|
||
'post_http_code' => intval($arrWrite['http']['http_code'] ?? 0),
|
||
'post_response' => substr(trim((string)($arrWrite['http']['body'] ?? '')), 0, 1500),
|
||
'readback_how' => $strReadbackHow,
|
||
'readback_sample' => is_array($arrReadback)
|
||
? array_intersect_key($arrReadback, array_flip(array(
|
||
'entry_id', 'entry_external_id', 'external_id', 'entry_notes', 'entry_note_test',
|
||
)))
|
||
: null,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Sonde perf — teste 10 / 50 / 100 / 200 entries (upsert, mêmes gens). MSIN-4328
|
||
* Évite un push complet pour calibrer la taille de lot.
|
||
*/
|
||
function fxChronotrackApiSyncProbeBatchPerf($intEveId) {
|
||
$arrConfig = fxChronotrackApiConfigGet($intEveId);
|
||
if ($arrConfig === null) {
|
||
return array('state' => 'error', 'message' => 'Événement non lié à ChronoTrack');
|
||
}
|
||
if (!fxChronotrackApiSettingsConfigured()) {
|
||
return array('state' => 'error', 'message' => fxChronotrackApiSettingsErrorMessage());
|
||
}
|
||
|
||
$intCtEventId = intval($arrConfig['ct_event_id']);
|
||
$arrRaceMap = fxChronotrackApiRaceMapGetForEvent($intEveId);
|
||
$tabParticipants = fxChronotrackApiSyncLoadMs1Participants($intEveId);
|
||
$arrClass = fxChronotrackApiSyncClassifyParticipants($tabParticipants, $arrRaceMap, $intCtEventId);
|
||
|
||
$intAvail = count($arrClass['transferable']);
|
||
if ($intAvail < 10) {
|
||
return array(
|
||
'state' => 'error',
|
||
'message' => 'Il faut au moins 10 éligibles pour la sonde lots (actuellement ' . $intAvail . ').',
|
||
);
|
||
}
|
||
|
||
$tabPayloads = array();
|
||
$intMax = min(200, $intAvail);
|
||
for ($i = 0; $i < $intMax; $i++) {
|
||
$tabPayloads[] = fxChronotrackApiSyncPayloadForPost($arrClass['transferable'][$i]['payload']);
|
||
}
|
||
|
||
$tabSizes = array(10, 50, 100, 200);
|
||
$tabResults = array();
|
||
$intBest = 10;
|
||
|
||
foreach ($tabSizes as $intSize) {
|
||
if ($intSize > count($tabPayloads)) {
|
||
break;
|
||
}
|
||
$tabSlice = array_slice($tabPayloads, 0, $intSize);
|
||
$floatStart = microtime(true);
|
||
$arrPost = fxChronotrackApiSyncPostEntries($intCtEventId, $tabSlice);
|
||
$floatMs = round((microtime(true) - $floatStart) * 1000);
|
||
$blnOk = (($arrPost['state'] ?? '') === 'ok');
|
||
$tabResults[] = array(
|
||
'size' => $intSize,
|
||
'ok' => $blnOk,
|
||
'ms' => $floatMs,
|
||
'http_code' => intval($arrPost['http']['http_code'] ?? 0),
|
||
'message' => $blnOk ? 'OK' : ($arrPost['message'] ?? 'échec'),
|
||
'ms_per' => $blnOk && $intSize > 0 ? round($floatMs / $intSize, 1) : null,
|
||
);
|
||
if ($blnOk) {
|
||
$intBest = $intSize;
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
|
||
$intEst2000 = ($intBest > 0)
|
||
? (int)ceil(2000 / $intBest) * max(1, intval($tabResults[count($tabResults) - 1]['ms'] ?? 1000))
|
||
: 0;
|
||
|
||
return array(
|
||
'state' => 'ok',
|
||
'message' => 'Plus grand lot OK : ' . $intBest
|
||
. ' — lot push actuel configuré : ' . MSIN_API_CHRONOTRACK_SYNC_BATCH_SIZE,
|
||
'recommended' => $intBest,
|
||
'configured' => MSIN_API_CHRONOTRACK_SYNC_BATCH_SIZE,
|
||
'results' => $tabResults,
|
||
'est_ms_2000' => $intEst2000,
|
||
'est_min_2000' => $intEst2000 > 0 ? round($intEst2000 / 60000, 1) : null,
|
||
'ct_event_id' => $intCtEventId,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Liste toutes les entries CT (pagination), dédupliquées par entry_id.
|
||
* Ne filtre pas sur external_id — décompte réel côté CT.
|
||
*/
|
||
function fxChronotrackApiSyncFetchAllCtEntryEntities($intCtEventId) {
|
||
$intCtEventId = intval($intCtEventId);
|
||
$tabAll = array();
|
||
$tabSeenEntryIds = array();
|
||
$intPage = 1;
|
||
$intPageSize = 200;
|
||
$intMaxPages = 500;
|
||
|
||
while ($intPage <= $intMaxPages) {
|
||
$arrApi = fxChronotrackApiOAuthApiGet('event/' . $intCtEventId . '/entry', array(
|
||
'page' => $intPage,
|
||
'page_size' => $intPageSize,
|
||
));
|
||
if ($arrApi['state'] !== 'ok') {
|
||
return array(
|
||
'state' => 'error',
|
||
'message' => $arrApi['message'] ?? 'Erreur lecture entries CT',
|
||
'entities' => $tabAll,
|
||
);
|
||
}
|
||
|
||
$arrEntities = fxChronotrackApiNormalizeEntityList($arrApi['json'], 'entry');
|
||
if (count($arrEntities) === 0) {
|
||
break;
|
||
}
|
||
|
||
foreach ($arrEntities as $arrEntity) {
|
||
if (!is_array($arrEntity)) {
|
||
continue;
|
||
}
|
||
$strEntryId = fxChronotrackApiEntityId($arrEntity);
|
||
if ($strEntryId === '' || isset($tabSeenEntryIds[$strEntryId])) {
|
||
continue;
|
||
}
|
||
$tabSeenEntryIds[$strEntryId] = true;
|
||
$tabAll[] = $arrEntity;
|
||
}
|
||
|
||
if (count($arrEntities) < $intPageSize) {
|
||
break;
|
||
}
|
||
$intPage++;
|
||
}
|
||
|
||
return array(
|
||
'state' => 'ok',
|
||
'message' => '',
|
||
'entities' => $tabAll,
|
||
'count' => count($tabAll),
|
||
);
|
||
}
|
||
|
||
function fxChronotrackApiSyncFetchCtEntriesIndexed($intCtEventId) {
|
||
$arrFetchAll = fxChronotrackApiSyncFetchAllCtEntryEntities($intCtEventId);
|
||
if ($arrFetchAll['state'] !== 'ok') {
|
||
return array(
|
||
'state' => 'error',
|
||
'message' => $arrFetchAll['message'] ?? 'Erreur lecture entries CT',
|
||
'entries' => array(),
|
||
);
|
||
}
|
||
|
||
$tabIndexed = array();
|
||
foreach ($arrFetchAll['entities'] as $arrEntity) {
|
||
if (!is_array($arrEntity)) {
|
||
continue;
|
||
}
|
||
$strExt = fxChronotrackApiEntityExternalId($arrEntity);
|
||
if ($strExt === '') {
|
||
continue;
|
||
}
|
||
$tabIndexed[$strExt] = $arrEntity;
|
||
}
|
||
|
||
return array(
|
||
'state' => 'ok',
|
||
'message' => '',
|
||
'entries' => $tabIndexed,
|
||
'entities' => $arrFetchAll['entities'],
|
||
'total_entry_count' => intval($arrFetchAll['count'] ?? count($arrFetchAll['entities'])),
|
||
'without_external_id'=> max(0, intval($arrFetchAll['count'] ?? 0) - count($tabIndexed)),
|
||
);
|
||
}
|
||
|
||
function fxChronotrackApiSyncFetchCtEntryRows($intCtEventId) {
|
||
$arrFetch = fxChronotrackApiSyncFetchAllCtEntryEntities($intCtEventId);
|
||
if ($arrFetch['state'] !== 'ok') {
|
||
return $arrFetch;
|
||
}
|
||
|
||
$tabRows = array();
|
||
foreach ($arrFetch['entities'] as $arrEntity) {
|
||
if (!is_array($arrEntity)) {
|
||
continue;
|
||
}
|
||
$strEntryId = fxChronotrackApiEntityId($arrEntity);
|
||
if ($strEntryId === '') {
|
||
continue;
|
||
}
|
||
$tabRows[] = array(
|
||
'entry_id' => $strEntryId,
|
||
'external_id' => fxChronotrackApiEntityExternalId($arrEntity),
|
||
'entity' => $arrEntity,
|
||
);
|
||
}
|
||
|
||
return array(
|
||
'state' => 'ok',
|
||
'message' => '',
|
||
'rows' => $tabRows,
|
||
'count' => count($tabRows),
|
||
);
|
||
}
|
||
|
||
function fxChronotrackApiSyncBuildPreview($intEveId) {
|
||
$arrConfig = fxChronotrackApiConfigGet($intEveId);
|
||
if ($arrConfig === null) {
|
||
return array('state' => 'error', 'message' => 'Événement non lié à ChronoTrack');
|
||
}
|
||
|
||
if (!fxChronotrackApiSettingsConfigured()) {
|
||
return array('state' => 'error', 'message' => fxChronotrackApiSettingsErrorMessage());
|
||
}
|
||
|
||
$intCtEventId = intval($arrConfig['ct_event_id']);
|
||
$arrRaceMap = fxChronotrackApiRaceMapGetForEvent($intEveId);
|
||
$tabParticipants = fxChronotrackApiSyncLoadMs1Participants($intEveId);
|
||
// MSIN-4328 — preview rapide : pas de build payloads / entry_notes (seulement compteurs + exclus)
|
||
$arrClass = fxChronotrackApiSyncClassifyParticipants($tabParticipants, $arrRaceMap, $intCtEventId, false);
|
||
|
||
$intMs1Total = count($tabParticipants);
|
||
$intTransferable = count($arrClass['transferable']);
|
||
|
||
// MSIN-4328 — preview MS1 seul (rapide). Plus de scan CT fiche-par-fiche :
|
||
// le push upsert crée/maj sans avoir besoin du inventaire Live ici.
|
||
$strLastPushAt = trim((string)($arrConfig['last_push_at'] ?? ''));
|
||
$tabPending = fxChronotrackApiSyncFilterChangedSincePush($arrClass['transferable'], $strLastPushAt);
|
||
$intPendingPush = count($tabPending);
|
||
|
||
return array(
|
||
'state' => 'ok',
|
||
'ct_event_id' => $intCtEventId,
|
||
'ms1_total' => $intMs1Total,
|
||
'transferable' => $intTransferable,
|
||
'blocked_count' => $arrClass['blocked_count'],
|
||
'blocked_by_type' => $arrClass['blocked_by_type'],
|
||
// Pas de liste plate « blocked » (doublon lourd) — l’UI utilise blocked_by_type
|
||
'eligible' => $intTransferable,
|
||
'skipped_no_race' => $arrClass['skipped_no_race'],
|
||
'skipped_no_external' => $arrClass['skipped_no_external'],
|
||
'blocked_no_bib' => $arrClass['blocked_no_bib'],
|
||
'blocked_no_sex' => $arrClass['blocked_no_sex'],
|
||
'blocked_duplicate_bib' => $arrClass['blocked_duplicate_bib'],
|
||
'blocked_team' => $arrClass['blocked_team'],
|
||
'anomaly_no_bib' => $arrClass['blocked_no_bib'],
|
||
'anomaly_no_sex' => $arrClass['blocked_no_sex'],
|
||
'anomaly_duplicate_bib' => $arrClass['blocked_duplicate_bib'],
|
||
'duplicate_bib_numbers' => $arrClass['duplicate_bib_numbers'],
|
||
'ct_total' => null,
|
||
'ct_total_loaded' => false,
|
||
'already_in_ct' => null,
|
||
'will_update' => null,
|
||
'to_create' => null,
|
||
'will_create' => null,
|
||
'ct_orphans' => null,
|
||
'ct_without_external_id' => null,
|
||
'to_push' => $intPendingPush,
|
||
'pending_push' => $intPendingPush,
|
||
'country_coverage' => $arrClass['country_coverage'] ?? array(),
|
||
'last_push_at' => ($strLastPushAt !== '' && $strLastPushAt !== '0000-00-00 00:00:00')
|
||
? $strLastPushAt
|
||
: null,
|
||
'last_push_ok_count' => intval($arrConfig['last_push_ok_count'] ?? 0),
|
||
'is_first_push' => ($strLastPushAt === '' || $strLastPushAt === '0000-00-00 00:00:00'),
|
||
);
|
||
}
|
||
|
||
/**
|
||
* MSIN-4328 — total entries CT Live (sans construire le diff MS1↔CT).
|
||
* Essaie d’abord un total dans la réponse API ; sinon compte par pagination légère.
|
||
*/
|
||
function fxChronotrackApiSyncExtractCtListTotal($mixJson) {
|
||
if (!is_array($mixJson)) {
|
||
return null;
|
||
}
|
||
$tabKeys = array(
|
||
'total', 'total_count', 'totalCount', 'count', 'entry_count', 'num_results',
|
||
'results_count', 'nb_results', 'total_entries',
|
||
);
|
||
foreach ($tabKeys as $strKey) {
|
||
if (isset($mixJson[$strKey]) && is_numeric($mixJson[$strKey])) {
|
||
$intVal = intval($mixJson[$strKey]);
|
||
if ($intVal >= 0) {
|
||
return $intVal;
|
||
}
|
||
}
|
||
}
|
||
foreach (array('pagination', 'page', 'meta', 'paging') as $strNest) {
|
||
if (!isset($mixJson[$strNest]) || !is_array($mixJson[$strNest])) {
|
||
continue;
|
||
}
|
||
$intNested = fxChronotrackApiSyncExtractCtListTotal($mixJson[$strNest]);
|
||
if ($intNested !== null) {
|
||
return $intNested;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4328 — total entries CT Live (+ backfill ct_entry_id si eve_id fourni).
|
||
*/
|
||
function fxChronotrackApiSyncFetchCtEntryCount($intCtEventId, $intEveId = 0) {
|
||
global $objDatabase;
|
||
|
||
$intCtEventId = intval($intCtEventId);
|
||
$intEveId = intval($intEveId);
|
||
if ($intCtEventId <= 0) {
|
||
return array('state' => 'error', 'message' => 'ct_event_id manquant', 'ct_total' => null);
|
||
}
|
||
|
||
$tabByExt = array();
|
||
if ($intEveId > 0) {
|
||
$sqlMap = "SELECT par_id, par_id_original FROM resultats_participants"
|
||
. " WHERE eve_id = " . $intEveId
|
||
. " AND is_cancelled = 0"
|
||
. " AND IFNULL(par_id_original, 0) > 0";
|
||
$arrMapRows = $objDatabase->fxGetResults($sqlMap);
|
||
if (is_array($arrMapRows)) {
|
||
for ($i = 1; $i <= count($arrMapRows); $i++) {
|
||
$strExt = (string)intval($arrMapRows[$i]['par_id_original']);
|
||
$tabByExt[$strExt] = intval($arrMapRows[$i]['par_id']);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Si pas de backfill et métadonnée total dispo → 1 appel.
|
||
if (count($tabByExt) === 0) {
|
||
$arrApi = fxChronotrackApiOAuthApiGet('event/' . $intCtEventId . '/entry', array(
|
||
'page' => 1,
|
||
'page_size' => 1,
|
||
));
|
||
if ($arrApi['state'] !== 'ok') {
|
||
return array(
|
||
'state' => 'error',
|
||
'message' => $arrApi['message'] ?? 'Erreur lecture entries CT',
|
||
'ct_total' => null,
|
||
);
|
||
}
|
||
$intFromMeta = fxChronotrackApiSyncExtractCtListTotal($arrApi['json'] ?? null);
|
||
if ($intFromMeta !== null) {
|
||
return array(
|
||
'state' => 'ok',
|
||
'message' => 'Total ChronoTrack Live (métadonnée API)',
|
||
'ct_total' => $intFromMeta,
|
||
'via' => 'meta',
|
||
'backfilled'=> 0,
|
||
);
|
||
}
|
||
}
|
||
|
||
$intTotal = 0;
|
||
$intBackfilled = 0;
|
||
$intPage = 1;
|
||
$intPageSize = 250;
|
||
$intMaxPages = 200;
|
||
while ($intPage <= $intMaxPages) {
|
||
$arrPage = fxChronotrackApiOAuthApiGet('event/' . $intCtEventId . '/entry', array(
|
||
'page' => $intPage,
|
||
'page_size' => $intPageSize,
|
||
));
|
||
if ($arrPage['state'] !== 'ok') {
|
||
return array(
|
||
'state' => 'error',
|
||
'message' => $arrPage['message'] ?? 'Erreur lecture entries CT',
|
||
'ct_total' => ($intTotal > 0) ? $intTotal : null,
|
||
'backfilled' => $intBackfilled,
|
||
);
|
||
}
|
||
$arrEntities = fxChronotrackApiNormalizeEntityList($arrPage['json'], 'entry');
|
||
$intN = count($arrEntities);
|
||
$intTotal += $intN;
|
||
|
||
if (count($tabByExt) > 0) {
|
||
$tabMetaPage = array();
|
||
foreach ($arrEntities as $arrEnt) {
|
||
if (!is_array($arrEnt)) {
|
||
continue;
|
||
}
|
||
$strExt = fxChronotrackApiEntityExternalId($arrEnt);
|
||
if ($strExt === '' || !isset($tabByExt[$strExt])) {
|
||
continue;
|
||
}
|
||
$tabMetaPage[] = array(
|
||
'par_id' => $tabByExt[$strExt],
|
||
'external_id' => $strExt,
|
||
);
|
||
}
|
||
if (count($tabMetaPage) > 0) {
|
||
$intBackfilled += fxChronotrackApiSyncApplyEntryIdsFromApiJson(
|
||
array('event_entry' => $arrEntities),
|
||
$tabMetaPage
|
||
);
|
||
}
|
||
}
|
||
|
||
if ($intN < $intPageSize) {
|
||
break;
|
||
}
|
||
$intPage++;
|
||
}
|
||
|
||
return array(
|
||
'state' => 'ok',
|
||
'message' => 'Total ChronoTrack Live (comptage paginé)'
|
||
. ($intBackfilled > 0 ? (' — ' . $intBackfilled . ' entry_id liés') : ''),
|
||
'ct_total' => $intTotal,
|
||
'via' => 'pages',
|
||
'backfilled' => $intBackfilled,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Stockage job push (fichier temp) — évite de saturer $_SESSION sur ~2000 entries. MSIN-4328
|
||
*/
|
||
function fxChronotrackApiSyncPushJobPath($intEveId) {
|
||
$strDir = rtrim(sys_get_temp_dir(), '/\\') . DIRECTORY_SEPARATOR . 'ms1_ct_push';
|
||
if (!is_dir($strDir)) {
|
||
@mkdir($strDir, 0700, true);
|
||
}
|
||
$strSid = substr(preg_replace('/[^a-zA-Z0-9_-]/', '', (string)session_id()), 0, 40);
|
||
if ($strSid === '') {
|
||
$strSid = 'nosession';
|
||
}
|
||
return $strDir . DIRECTORY_SEPARATOR . 'eve' . intval($intEveId) . '_' . $strSid . '.json';
|
||
}
|
||
|
||
function fxChronotrackApiSyncPushJobLoad($intEveId) {
|
||
$strPath = fxChronotrackApiSyncPushJobPath($intEveId);
|
||
if (!is_file($strPath)) {
|
||
return null;
|
||
}
|
||
$strRaw = @file_get_contents($strPath);
|
||
if ($strRaw === false || $strRaw === '') {
|
||
return null;
|
||
}
|
||
$arrJob = json_decode($strRaw, true);
|
||
return is_array($arrJob) ? $arrJob : null;
|
||
}
|
||
|
||
function fxChronotrackApiSyncPushJobSave($intEveId, array $arrJob) {
|
||
$strPath = fxChronotrackApiSyncPushJobPath($intEveId);
|
||
$strJson = json_encode($arrJob, JSON_UNESCAPED_UNICODE);
|
||
if (!is_string($strJson)) {
|
||
return false;
|
||
}
|
||
return (@file_put_contents($strPath, $strJson, LOCK_EX) !== false);
|
||
}
|
||
|
||
function fxChronotrackApiSyncPushJobClear($intEveId) {
|
||
$strPath = fxChronotrackApiSyncPushJobPath($intEveId);
|
||
if (is_file($strPath)) {
|
||
@unlink($strPath);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Prépare un job de push (liste éligibles + entry_id CT). MSIN-4328.
|
||
*
|
||
* @param array $arrOptions force_full=true → tous les éligibles (ignore last_push_at)
|
||
*/
|
||
function fxChronotrackApiSyncPushPrepare($intEveId, $arrOptions = array()) {
|
||
$intEveId = intval($intEveId);
|
||
$blnForceFull = !empty($arrOptions['force_full']);
|
||
$blnVerboseLogs = !empty($arrOptions['verbose_logs']);
|
||
fxChronotrackApiSyncVerboseEntryLogs($blnVerboseLogs);
|
||
$arrConfig = fxChronotrackApiConfigGet($intEveId);
|
||
if ($arrConfig === null) {
|
||
return array('state' => 'error', 'message' => 'Événement non lié à ChronoTrack');
|
||
}
|
||
if (!fxChronotrackApiSettingsConfigured()) {
|
||
return array('state' => 'error', 'message' => fxChronotrackApiSettingsErrorMessage());
|
||
}
|
||
|
||
$intCtEventId = intval($arrConfig['ct_event_id']);
|
||
$arrRaceMap = fxChronotrackApiRaceMapGetForEvent($intEveId);
|
||
$tabParticipants = fxChronotrackApiSyncLoadMs1Participants($intEveId);
|
||
$arrClass = fxChronotrackApiSyncClassifyParticipants($tabParticipants, $arrRaceMap, $intCtEventId);
|
||
|
||
// MSIN-4328 — ne plus journaliser push_skip ici (spam à chaque prepare/auto).
|
||
// Les exclus restent dans l’UI « Détails & exclus » (preview), jamais dans le lot poussé.
|
||
|
||
// MSIN-4328 — différentiel par défaut ; force_full = tout renvoyer
|
||
$strLastPushAt = trim((string)($arrConfig['last_push_at'] ?? ''));
|
||
if ($blnForceFull) {
|
||
$tabPending = $arrClass['transferable'];
|
||
} else {
|
||
$tabPending = fxChronotrackApiSyncFilterChangedSincePush($arrClass['transferable'], $strLastPushAt);
|
||
}
|
||
|
||
// Garder entry_id en meta/job pour retry dossard ; l’envoi lot utilise POST upsert (pas PUT unitaire).
|
||
$tabBatch = array();
|
||
$tabMeta = array();
|
||
foreach ($tabPending as $arrItem) {
|
||
$arrPayload = $arrItem['payload'];
|
||
$strCtEntryId = preg_replace('/[^0-9]/', '', (string)($arrItem['row']['ct_entry_id'] ?? ''));
|
||
if ($strCtEntryId !== '') {
|
||
$arrPayload['entry_id'] = $strCtEntryId;
|
||
}
|
||
$tabBatch[] = $arrPayload;
|
||
$strBibMs1 = fxChronotrackApiSyncExtractBib($arrItem['row'] ?? array());
|
||
$strBibSent = isset($arrPayload['bib']) ? trim((string)$arrPayload['bib']) : '';
|
||
$strNoEquipe = trim((string)($arrItem['row']['no_equipe'] ?? ''));
|
||
$tabMeta[] = array(
|
||
'par_id' => intval($arrItem['row']['par_id'] ?? 0),
|
||
'external_id' => $arrPayload['external_id'] ?? '',
|
||
'bib_ms1' => $strBibMs1,
|
||
'bib_sent' => $strBibSent,
|
||
'no_equipe' => $strNoEquipe,
|
||
'name' => trim(
|
||
trim((string)($arrPayload['first_name'] ?? ''))
|
||
. ' '
|
||
. trim((string)($arrPayload['last_name'] ?? ''))
|
||
),
|
||
);
|
||
}
|
||
|
||
if (count($tabBatch) === 0) {
|
||
fxChronotrackApiSyncPushJobClear($intEveId);
|
||
$strMsg = count($arrClass['transferable']) === 0
|
||
? ('Aucun participant transférable.'
|
||
. ($arrClass['blocked_count'] > 0 ? ' ' . $arrClass['blocked_count'] . ' exclus.' : ''))
|
||
: ('Aucun changement depuis le dernier push ('
|
||
. count($arrClass['transferable']) . ' éligibles à jour).');
|
||
fxChronotrackApiSyncLog($intEveId, 0, 'push_prepare', 'ok', 'idle — ' . $strMsg);
|
||
return array(
|
||
'state' => 'error',
|
||
'message' => $strMsg,
|
||
'blocked_count' => $arrClass['blocked_count'],
|
||
'transferable' => count($arrClass['transferable']),
|
||
'pending_push' => 0,
|
||
'total' => 0,
|
||
'force_full' => $blnForceFull ? 1 : 0,
|
||
);
|
||
}
|
||
|
||
$strMode = $blnForceFull ? 'FULL resync' : 'différentiel';
|
||
$arrCountryCov = $arrClass['country_coverage'] ?? array();
|
||
$strPaysLog = ' pays_ok=' . intval($arrCountryCov['with_country'] ?? 0)
|
||
. '/' . intval($arrCountryCov['total'] ?? count($arrClass['transferable']))
|
||
. ' sans_pays=' . intval($arrCountryCov['without_country'] ?? 0)
|
||
. ' sans_pay_id=' . intval($arrCountryCov['no_pay_id'] ?? 0);
|
||
fxChronotrackApiSyncLog(
|
||
$intEveId,
|
||
0,
|
||
'push_prepare',
|
||
'ok',
|
||
$strMode . ' — job prêt — pending=' . count($tabBatch)
|
||
. ' éligibles=' . count($arrClass['transferable'])
|
||
. ' exclus=' . intval($arrClass['blocked_count'])
|
||
. $strPaysLog
|
||
);
|
||
|
||
// MSIN-4574 — alerte log si beaucoup sans pays (ne bloque pas le push)
|
||
if (intval($arrCountryCov['without_country'] ?? 0) > 0) {
|
||
fxChronotrackApiSyncLog(
|
||
$intEveId,
|
||
0,
|
||
'push_prepare',
|
||
'warn',
|
||
'Sans pays MS1 (pay_id/iso) : ' . intval($arrCountryCov['without_country'])
|
||
. ' sur ' . intval($arrCountryCov['total'] ?? 0)
|
||
. ' — CT restera sans location_country pour ces lignes. Corriger pay_id ou mapping pays.'
|
||
);
|
||
}
|
||
|
||
$arrJob = array(
|
||
'ct_event_id' => $intCtEventId,
|
||
'batch' => $tabBatch,
|
||
'meta' => $tabMeta,
|
||
'blocked_count' => $arrClass['blocked_count'],
|
||
'country_coverage' => $arrCountryCov,
|
||
'force_full' => $blnForceFull ? 1 : 0,
|
||
'verbose_logs' => $blnVerboseLogs ? 1 : 0,
|
||
'ok_sum' => 0,
|
||
'err_sum' => 0,
|
||
'bib_retry' => array(),
|
||
'created_at' => time(),
|
||
);
|
||
if (!fxChronotrackApiSyncPushJobSave($intEveId, $arrJob)) {
|
||
return array(
|
||
'state' => 'error',
|
||
'message' => 'Impossible d’écrire le fichier temporaire du push (permissions /tmp).',
|
||
);
|
||
}
|
||
|
||
return array(
|
||
'state' => 'ok',
|
||
'message' => count($tabBatch) . ' prêts à envoyer'
|
||
. ($blnForceFull ? ' (resync complète)' : ''),
|
||
'total' => count($tabBatch),
|
||
'pending_push' => count($tabBatch),
|
||
'transferable' => count($arrClass['transferable']),
|
||
'blocked_count' => $arrClass['blocked_count'],
|
||
'country_coverage' => $arrCountryCov,
|
||
'chunk_size' => MSIN_API_CHRONOTRACK_SYNC_CHUNK_SIZE,
|
||
'ct_event_id' => $intCtEventId,
|
||
'force_full' => $blnForceFull ? 1 : 0,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Push un slice du job préparé. MSIN-4328.
|
||
* Même mécanique que « Envoyer » : POST par lots + offset/total (barre %).
|
||
* « Tout renvoyer » = force_full à la préparation seulement — pas un autre pipeline.
|
||
*/
|
||
function fxChronotrackApiSyncPushEvent($intEveId, $arrOptions = array()) {
|
||
$intEveId = intval($intEveId);
|
||
$intOffset = max(0, intval($arrOptions['offset'] ?? 0));
|
||
$intLimit = max(0, intval($arrOptions['limit'] ?? 0));
|
||
if ($intLimit <= 0) {
|
||
$intLimit = MSIN_API_CHRONOTRACK_SYNC_CHUNK_SIZE;
|
||
}
|
||
|
||
$arrJob = fxChronotrackApiSyncPushJobLoad($intEveId);
|
||
if (!is_array($arrJob) || empty($arrJob['batch'])) {
|
||
$arrPrep = fxChronotrackApiSyncPushPrepare($intEveId);
|
||
if ($arrPrep['state'] !== 'ok') {
|
||
return $arrPrep;
|
||
}
|
||
$arrJob = fxChronotrackApiSyncPushJobLoad($intEveId);
|
||
}
|
||
if (!is_array($arrJob) || empty($arrJob['batch'])) {
|
||
return array('state' => 'error', 'message' => 'Job de push introuvable — relancer');
|
||
}
|
||
|
||
$intCtEventId = intval($arrJob['ct_event_id']);
|
||
$tabBatchAll = $arrJob['batch'];
|
||
$tabMetaAll = $arrJob['meta'];
|
||
$intTotal = count($tabBatchAll);
|
||
$intSkipped = intval($arrJob['blocked_count'] ?? 0);
|
||
fxChronotrackApiSyncVerboseEntryLogs(!empty($arrJob['verbose_logs']));
|
||
|
||
$tabBatch = array_slice($tabBatchAll, $intOffset, $intLimit);
|
||
$tabMeta = array_slice($tabMetaAll, $intOffset, $intLimit);
|
||
$intChunkCount = count($tabBatch);
|
||
|
||
if ($intChunkCount === 0) {
|
||
fxChronotrackApiSyncPushJobClear($intEveId);
|
||
return array(
|
||
'state' => 'ok',
|
||
'message' => 'Rien à envoyer à cet offset',
|
||
'pushed_ok' => 0,
|
||
'pushed_put' => 0,
|
||
'pushed_post' => 0,
|
||
'pushed_error' => 0,
|
||
'total' => $intTotal,
|
||
'offset' => $intOffset,
|
||
'next_offset' => $intOffset,
|
||
'chunk_done' => 0,
|
||
'chunk_size' => $intLimit,
|
||
'done' => true,
|
||
'blocked_count' => $intSkipped,
|
||
);
|
||
}
|
||
|
||
$intOk = 0;
|
||
$intErr = 0;
|
||
$intPostOk = 0;
|
||
$intPutOk = 0;
|
||
$tabErrors = array();
|
||
$tabBibRetryChunk = array();
|
||
|
||
// MSIN-4328 — toujours POST en lot (upsert par external_id).
|
||
// Avant : PUT unitaire si ct_entry_id → ~100 appels/lot = 1–2 min figé à 0/N.
|
||
// entry_id reste sur le payload pour la 2e passe dossard ; retiré seulement à l’appel POST.
|
||
//
|
||
// MSIN-4574 — envoi AVEC dossard dans le POST bulk (rapide).
|
||
// Ancien modèle : strip systématique → 2e passe 1 PUT/personne = ~10 min pour 1300.
|
||
// Maintenant : conflit bib seulement → adaptive / file bib (minorité des cas).
|
||
$tabPostEntries = array();
|
||
$tabPostMeta = array();
|
||
foreach ($tabBatch as $intI => $arrPayload) {
|
||
if (!is_array($arrPayload)) {
|
||
continue;
|
||
}
|
||
$tabPostEntries[] = $arrPayload;
|
||
$tabPostMeta[] = $tabMeta[$intI] ?? array('par_id' => 0, 'external_id' => '');
|
||
}
|
||
|
||
if (count($tabPostEntries) > 0) {
|
||
$arrRes = fxChronotrackApiSyncPostEntriesAdaptive(
|
||
$intCtEventId,
|
||
$tabPostEntries,
|
||
$tabErrors,
|
||
$intEveId,
|
||
$tabPostMeta
|
||
);
|
||
$intOk += $arrRes['ok'];
|
||
$intPostOk += $arrRes['ok'];
|
||
$intErr += $arrRes['err'];
|
||
if (!empty($arrRes['bib_retry']) && is_array($arrRes['bib_retry'])) {
|
||
foreach ($arrRes['bib_retry'] as $arrR) {
|
||
$tabBibRetryChunk[] = $arrR;
|
||
}
|
||
}
|
||
}
|
||
|
||
$tabBibRetryJob = isset($arrJob['bib_retry']) && is_array($arrJob['bib_retry'])
|
||
? $arrJob['bib_retry']
|
||
: array();
|
||
foreach ($tabBibRetryChunk as $arrR) {
|
||
$tabBibRetryJob[] = $arrR;
|
||
}
|
||
$arrJob['bib_retry'] = $tabBibRetryJob;
|
||
|
||
fxChronotrackApiSyncLog(
|
||
$intEveId,
|
||
0,
|
||
'push_chunk',
|
||
($intErr === 0) ? 'ok' : 'error',
|
||
'offset=' . $intOffset . ' n=' . $intChunkCount
|
||
. ' ok=' . $intOk . ' err=' . $intErr
|
||
. ' PUT=' . $intPutOk . ' POST=' . $intPostOk
|
||
. ' bib_pending=' . count($tabBibRetryJob)
|
||
);
|
||
|
||
$intOkSum = intval($arrJob['ok_sum'] ?? 0) + $intOk;
|
||
$intErrSum = intval($arrJob['err_sum'] ?? 0) + $intErr;
|
||
$arrJob['ok_sum'] = $intOkSum;
|
||
$arrJob['err_sum'] = $intErrSum;
|
||
|
||
$intNextOffset = $intOffset + $intChunkCount;
|
||
$blnIdentityDone = ($intNextOffset >= $intTotal);
|
||
$blnLastPushSaved = false;
|
||
$intBibRetryOk = 0;
|
||
$intBibRetryErr = 0;
|
||
$intBibPending = count($tabBibRetryJob);
|
||
|
||
// MSIN-4574 — NE PAS enchaîner 1300 PUT dossards dans la même requête PHP que le dernier lot.
|
||
// C’était la cause typique du HTTP 503 HTML (timeout hébergeur) après 8–10 min.
|
||
if ($blnIdentityDone) {
|
||
$arrJob['phase'] = ($intBibPending > 0) ? 'bibs' : 'done';
|
||
$arrJob['identity_done_at'] = time();
|
||
if ($intBibPending === 0) {
|
||
if ($intErrSum === 0) {
|
||
$blnLastPushSaved = fxChronotrackApiConfigSetLastPush($intEveId, $intOkSum);
|
||
}
|
||
// résumé final (sans passe bib)
|
||
$intPaysSent = 0;
|
||
$intPaysMiss = 0;
|
||
foreach ($tabBatchAll as $arrP) {
|
||
if (!is_array($arrP)) {
|
||
continue;
|
||
}
|
||
if (fxChronotrackApiSyncCountryFromPayload($arrP) !== '') {
|
||
$intPaysSent++;
|
||
} else {
|
||
$intPaysMiss++;
|
||
}
|
||
}
|
||
$arrCovJob = is_array($arrJob['country_coverage'] ?? null) ? $arrJob['country_coverage'] : array();
|
||
fxChronotrackApiSyncLog(
|
||
$intEveId,
|
||
0,
|
||
'push_done',
|
||
($intErrSum === 0) ? 'ok' : 'error',
|
||
'fin identité — ok=' . $intOkSum . ' err=' . $intErrSum
|
||
. ' exclus=' . $intSkipped
|
||
. ' payload_avec_pays=' . $intPaysSent
|
||
. ' payload_sans_pays=' . $intPaysMiss
|
||
. (intval($arrCovJob['no_pay_id'] ?? 0) > 0
|
||
? (' ms1_sans_pay_id=' . intval($arrCovJob['no_pay_id']))
|
||
: '')
|
||
. (count($tabErrors) > 0
|
||
? (' — erreurs: ' . implode(' | ', array_slice($tabErrors, 0, 5)))
|
||
: '')
|
||
);
|
||
fxChronotrackApiSyncPushJobClear($intEveId);
|
||
} else {
|
||
fxChronotrackApiSyncLog(
|
||
$intEveId,
|
||
0,
|
||
'push_chunk',
|
||
'ok',
|
||
'identité terminée — passe dossards séparée pending=' . $intBibPending
|
||
);
|
||
fxChronotrackApiSyncPushJobSave($intEveId, $arrJob);
|
||
}
|
||
} else {
|
||
$arrJob['phase'] = 'identity';
|
||
fxChronotrackApiSyncPushJobSave($intEveId, $arrJob);
|
||
}
|
||
|
||
$strState = ($intErr === 0)
|
||
? 'ok'
|
||
: (($intOk > 0) ? 'partial' : 'error');
|
||
$strMessage = ($intErr === 0)
|
||
? ($intOk . ' envoyé(s) (offset ' . $intOffset . ').')
|
||
: ($intOk . ' OK, ' . $intErr . ' erreur(s) sur ce lot.');
|
||
if ($blnIdentityDone && $intBibPending > 0) {
|
||
$strMessage .= ' Identité OK — dossards en file (' . $intBibPending . ').';
|
||
}
|
||
|
||
return array(
|
||
'state' => $strState,
|
||
'message' => $strMessage,
|
||
'phase' => ($blnIdentityDone && $intBibPending > 0) ? 'bibs' : (($blnIdentityDone) ? 'done' : 'identity'),
|
||
'pushed_ok' => $intOk,
|
||
'pushed_put' => $intPutOk,
|
||
'pushed_post' => $intPostOk,
|
||
'pushed_error' => $intErr,
|
||
'job_ok_sum' => $intOkSum,
|
||
'job_err_sum' => $intErrSum,
|
||
'last_push_saved' => $blnLastPushSaved,
|
||
'skipped' => $intSkipped,
|
||
'blocked_count' => $intSkipped,
|
||
'errors' => array_slice($tabErrors, 0, 20),
|
||
'total' => $intTotal,
|
||
'offset' => $intOffset,
|
||
'next_offset' => $intNextOffset,
|
||
'chunk_done' => $intChunkCount,
|
||
'chunk_size' => $intLimit,
|
||
'done' => ($blnIdentityDone && $intBibPending === 0),
|
||
'identity_done' => $blnIdentityDone,
|
||
'bib_pending' => $intBibPending,
|
||
'bib_chunk_size' => MSIN_API_CHRONOTRACK_SYNC_BIB_CHUNK_SIZE,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* MSIN-4574 — passe dossards découpée (après identité), un sous-lot par requête AJAX.
|
||
*/
|
||
function fxChronotrackApiSyncPushBibsPhase($intEveId, $arrOptions = array()) {
|
||
$intEveId = intval($intEveId);
|
||
$intLimit = max(1, intval($arrOptions['limit'] ?? 0));
|
||
if ($intLimit <= 0) {
|
||
$intLimit = MSIN_API_CHRONOTRACK_SYNC_BIB_CHUNK_SIZE;
|
||
}
|
||
|
||
$arrJob = fxChronotrackApiSyncPushJobLoad($intEveId);
|
||
if (!is_array($arrJob)) {
|
||
return array(
|
||
'state' => 'error',
|
||
'message' => 'Aucun job dossards en cours (relancer Tout renvoyer).',
|
||
'done' => true,
|
||
'bib_pending' => 0,
|
||
);
|
||
}
|
||
|
||
$intCtEventId = intval($arrJob['ct_event_id'] ?? 0);
|
||
$tabBibRetryJob = isset($arrJob['bib_retry']) && is_array($arrJob['bib_retry'])
|
||
? $arrJob['bib_retry']
|
||
: array();
|
||
$intPendingBefore = count($tabBibRetryJob);
|
||
|
||
if ($intCtEventId <= 0) {
|
||
fxChronotrackApiSyncPushJobClear($intEveId);
|
||
return array('state' => 'error', 'message' => 'ct_event_id manquant dans le job', 'done' => true);
|
||
}
|
||
|
||
if ($intPendingBefore === 0) {
|
||
$intOkSum = intval($arrJob['ok_sum'] ?? 0);
|
||
$intErrSum = intval($arrJob['err_sum'] ?? 0);
|
||
if ($intErrSum === 0) {
|
||
fxChronotrackApiConfigSetLastPush($intEveId, $intOkSum);
|
||
}
|
||
fxChronotrackApiSyncLog($intEveId, 0, 'push_done', ($intErrSum === 0) ? 'ok' : 'error',
|
||
'fin — ok=' . $intOkSum . ' err=' . $intErrSum . ' (pas de dossards en file)');
|
||
fxChronotrackApiSyncPushJobClear($intEveId);
|
||
return array(
|
||
'state' => 'ok',
|
||
'message' => 'Aucun dossard en file — terminé.',
|
||
'done' => true,
|
||
'bib_pending' => 0,
|
||
'pushed_ok' => 0,
|
||
'pushed_error' => 0,
|
||
'phase' => 'done',
|
||
);
|
||
}
|
||
|
||
$tabSlice = array_slice($tabBibRetryJob, 0, $intLimit);
|
||
$tabRest = array_slice($tabBibRetryJob, $intLimit);
|
||
$tabErrors = array();
|
||
$intOk = 0;
|
||
$intErr = 0;
|
||
$tabRequeue = array();
|
||
|
||
foreach ($tabSlice as $arrItem) {
|
||
if (!is_array($arrItem) || !is_array($arrItem['payload'] ?? null)) {
|
||
continue;
|
||
}
|
||
$arrPayload = $arrItem['payload'];
|
||
$arrMeta = is_array($arrItem['meta'] ?? null) ? $arrItem['meta'] : array();
|
||
$strBib = isset($arrPayload['bib']) ? trim((string)$arrPayload['bib']) : '';
|
||
if ($strBib === '') {
|
||
continue;
|
||
}
|
||
$arrRes = fxChronotrackApiSyncPushOneEntryWithBibConflictHandling(
|
||
$intCtEventId,
|
||
$arrPayload,
|
||
$arrMeta,
|
||
$intEveId
|
||
);
|
||
if (intval($arrRes['ok'] ?? 0) > 0 && empty($arrRes['bib_retry'])) {
|
||
$intOk++;
|
||
continue;
|
||
}
|
||
if (!empty($arrRes['bib_retry']) && is_array($arrRes['bib_retry'])) {
|
||
foreach ($arrRes['bib_retry'] as $arrR) {
|
||
if (is_array($arrR)) {
|
||
$tabRequeue[] = $arrR;
|
||
}
|
||
}
|
||
continue;
|
||
}
|
||
$intErr++;
|
||
$strMsg = 'RETRY_BIB external_id=' . ($arrMeta['external_id'] ?? '')
|
||
. ' dossard=' . $strBib . ' — échec';
|
||
if (count($tabErrors) < 12) {
|
||
$tabErrors[] = $strMsg;
|
||
}
|
||
}
|
||
|
||
// Réessayer les conflits en fin de file (swaps) — max tours gérés par UI via job.bib_round
|
||
$intBibRound = intval($arrJob['bib_round'] ?? 0);
|
||
$tabNewPending = array_merge($tabRest, $tabRequeue);
|
||
if (count($tabRequeue) > 0 && count($tabRest) === 0) {
|
||
$intBibRound++;
|
||
$arrJob['bib_round'] = $intBibRound;
|
||
}
|
||
// Après trop de tours sans drain : abandonner le reste comme erreurs
|
||
if ($intBibRound >= 5 && count($tabRequeue) > 0 && count($tabRest) === 0) {
|
||
foreach ($tabRequeue as $arrLeft) {
|
||
$intErr++;
|
||
$arrMetaL = is_array($arrLeft['meta'] ?? null) ? $arrLeft['meta'] : array();
|
||
$strBibL = isset($arrLeft['payload']['bib']) ? trim((string)$arrLeft['payload']['bib']) : '';
|
||
$tabErrors[] = 'RETRY_BIB bloqué (swap max) external_id='
|
||
. ($arrMetaL['external_id'] ?? '') . ' dossard=' . $strBibL;
|
||
}
|
||
$tabNewPending = array();
|
||
}
|
||
|
||
$arrJob['bib_retry'] = $tabNewPending;
|
||
$arrJob['ok_sum'] = intval($arrJob['ok_sum'] ?? 0) + $intOk;
|
||
$arrJob['err_sum'] = intval($arrJob['err_sum'] ?? 0) + $intErr;
|
||
$intPendingAfter = count($tabNewPending);
|
||
$blnDone = ($intPendingAfter === 0);
|
||
|
||
fxChronotrackApiSyncLog(
|
||
$intEveId,
|
||
0,
|
||
'push_bib_retry',
|
||
($intErr === 0) ? 'ok' : 'error',
|
||
'slice n=' . count($tabSlice)
|
||
. ' ok=' . $intOk . ' err=' . $intErr
|
||
. ' requeue=' . count($tabRequeue)
|
||
. ' pending=' . $intPendingAfter
|
||
. ' round=' . $intBibRound
|
||
);
|
||
|
||
if ($blnDone) {
|
||
$intOkSum = intval($arrJob['ok_sum'] ?? 0);
|
||
$intErrSum = intval($arrJob['err_sum'] ?? 0);
|
||
if ($intErrSum === 0) {
|
||
fxChronotrackApiConfigSetLastPush($intEveId, $intOkSum);
|
||
}
|
||
fxChronotrackApiSyncLog(
|
||
$intEveId,
|
||
0,
|
||
'push_done',
|
||
($intErrSum === 0) ? 'ok' : 'error',
|
||
'fin — ok=' . $intOkSum . ' err=' . $intErrSum . ' (identité + dossards)'
|
||
);
|
||
fxChronotrackApiSyncPushJobClear($intEveId);
|
||
} else {
|
||
$arrJob['phase'] = 'bibs';
|
||
fxChronotrackApiSyncPushJobSave($intEveId, $arrJob);
|
||
}
|
||
|
||
return array(
|
||
'state' => ($intErr === 0) ? 'ok' : (($intOk > 0) ? 'partial' : 'error'),
|
||
'message' => 'Dossards : ' . $intOk . ' OK'
|
||
. ($intErr > 0 ? (', ' . $intErr . ' erreur(s)') : '')
|
||
. ' — reste ' . $intPendingAfter,
|
||
'phase' => $blnDone ? 'done' : 'bibs',
|
||
'pushed_ok' => $intOk,
|
||
'pushed_post' => $intOk,
|
||
'pushed_put' => 0,
|
||
'pushed_error' => $intErr,
|
||
'job_ok_sum' => intval($arrJob['ok_sum'] ?? 0),
|
||
'job_err_sum' => intval($arrJob['err_sum'] ?? 0),
|
||
'errors' => array_slice($tabErrors, 0, 20),
|
||
'done' => $blnDone,
|
||
'identity_done' => true,
|
||
'bib_pending' => $intPendingAfter,
|
||
'bib_chunk_size' => $intLimit,
|
||
'bib_before' => $intPendingBefore,
|
||
);
|
||
}
|
||
|
||
function fxChronotrackApiSyncOrphanReasonLabel($strReason) {
|
||
switch ($strReason) {
|
||
case 'sans_external_id':
|
||
return 'Sans external ID';
|
||
case 'doublon':
|
||
return 'Doublon CT';
|
||
case 'hors_eligible':
|
||
default:
|
||
return 'Hors éligibles MS1';
|
||
}
|
||
}
|
||
|
||
function fxChronotrackApiSyncComputeCtOrphanRows(array $tabEntities, array $tabEligibleKeys) {
|
||
$tabSeenLinkedExt = array();
|
||
$tabOrphans = array();
|
||
|
||
foreach ($tabEntities as $arrEntity) {
|
||
if (!is_array($arrEntity)) {
|
||
continue;
|
||
}
|
||
$strEntryId = fxChronotrackApiEntityId($arrEntity);
|
||
if ($strEntryId === '') {
|
||
continue;
|
||
}
|
||
|
||
$strExt = fxChronotrackApiEntityExternalId($arrEntity);
|
||
$strReason = '';
|
||
|
||
if ($strExt === '') {
|
||
$strReason = 'sans_external_id';
|
||
} elseif (!isset($tabEligibleKeys[$strExt])) {
|
||
$strReason = 'hors_eligible';
|
||
} elseif (isset($tabSeenLinkedExt[$strExt])) {
|
||
$strReason = 'doublon';
|
||
} else {
|
||
$tabSeenLinkedExt[$strExt] = true;
|
||
continue;
|
||
}
|
||
|
||
$tabOrphans[] = array(
|
||
'entry_id' => $strEntryId,
|
||
'external_id' => $strExt,
|
||
'name' => fxChronotrackApiEntityEntryLabel($arrEntity),
|
||
'bib' => fxChronotrackApiRaceField($arrEntity, array('bib', 'bib_number', 'entry_bib')),
|
||
'entry_status' => fxChronotrackApiRaceField($arrEntity, array('entry_status', 'status')),
|
||
'race_id' => fxChronotrackApiRaceField($arrEntity, array('race_id', 'event_race_id')),
|
||
'reason' => $strReason,
|
||
'reason_label' => fxChronotrackApiSyncOrphanReasonLabel($strReason),
|
||
);
|
||
}
|
||
|
||
usort($tabOrphans, function ($a, $b) {
|
||
$strA = strtolower(($a['name'] ?? '') . ($a['external_id'] ?? '') . ($a['entry_id'] ?? ''));
|
||
$strB = strtolower(($b['name'] ?? '') . ($b['external_id'] ?? '') . ($b['entry_id'] ?? ''));
|
||
return strcmp($strA, $strB);
|
||
});
|
||
|
||
return $tabOrphans;
|
||
}
|
||
|
||
/**
|
||
* Entries CT présentes sur l'événement mais absentes du jeu éligible MS1 (lié, doublon, sans ext).
|
||
* MSIN-4574 — version légère : pas de build payloads/notes (sinon gèle à ~1300).
|
||
*/
|
||
function fxChronotrackApiSyncListCtOrphans($intEveId) {
|
||
$intEveId = intval($intEveId);
|
||
$arrConfig = fxChronotrackApiConfigGet($intEveId);
|
||
if ($arrConfig === null) {
|
||
return array('state' => 'error', 'message' => 'Événement non lié à ChronoTrack');
|
||
}
|
||
if (!fxChronotrackApiSettingsConfigured()) {
|
||
return array('state' => 'error', 'message' => fxChronotrackApiSettingsErrorMessage());
|
||
}
|
||
|
||
$intCtEventId = intval($arrConfig['ct_event_id']);
|
||
$arrRaceMap = fxChronotrackApiRaceMapGetForEvent($intEveId);
|
||
$tabParticipants = fxChronotrackApiSyncLoadMs1Participants($intEveId);
|
||
// Pas de payloads / entry_notes — uniquement les clés external_id éligibles
|
||
$arrClass = fxChronotrackApiSyncClassifyParticipants(
|
||
$tabParticipants,
|
||
$arrRaceMap,
|
||
$intCtEventId,
|
||
false
|
||
);
|
||
|
||
$tabEligibleKeys = array();
|
||
foreach ($arrClass['transferable'] as $arrItem) {
|
||
if (!is_array($arrItem)) {
|
||
continue;
|
||
}
|
||
$strExt = trim((string)($arrItem['summary']['external_id'] ?? ''));
|
||
if ($strExt === '' || $strExt === '0') {
|
||
$strExt = (string)intval($arrItem['row']['par_id_original'] ?? 0);
|
||
}
|
||
if ($strExt !== '' && $strExt !== '0') {
|
||
$tabEligibleKeys[$strExt] = true;
|
||
}
|
||
}
|
||
|
||
$floatStart = microtime(true);
|
||
$arrFetch = fxChronotrackApiSyncFetchAllCtEntryEntities($intCtEventId);
|
||
$floatMs = (int)round((microtime(true) - $floatStart) * 1000);
|
||
if ($arrFetch['state'] !== 'ok') {
|
||
return array(
|
||
'state' => 'error',
|
||
'message' => $arrFetch['message'] ?? 'Erreur lecture entries CT',
|
||
'elapsed_ms' => $floatMs,
|
||
);
|
||
}
|
||
|
||
$tabOrphans = fxChronotrackApiSyncComputeCtOrphanRows($arrFetch['entities'], $tabEligibleKeys);
|
||
$intCtTotal = intval($arrFetch['count'] ?? count($arrFetch['entities']));
|
||
|
||
fxChronotrackApiSyncLog(
|
||
$intEveId,
|
||
0,
|
||
'reconcile',
|
||
'ok',
|
||
'list orphans=' . count($tabOrphans)
|
||
. ' ct_total=' . $intCtTotal
|
||
. ' eligible_keys=' . count($tabEligibleKeys)
|
||
. ' ms=' . $floatMs
|
||
);
|
||
|
||
return array(
|
||
'state' => 'ok',
|
||
'ct_event_id' => $intCtEventId,
|
||
'ct_total' => $intCtTotal,
|
||
'linked_count' => count($tabEligibleKeys),
|
||
'eligible_count' => count($arrClass['transferable']),
|
||
'count' => count($tabOrphans),
|
||
'orphans' => $tabOrphans,
|
||
'elapsed_ms' => $floatMs,
|
||
'message' => count($tabOrphans) . ' orphelin(s) sur ' . $intCtTotal . ' entries CT'
|
||
. ' (' . round($floatMs / 1000, 1) . ' s)',
|
||
);
|
||
}
|
||
|
||
/**
|
||
* MSIN-4574 — participants d’une commande (y compris annulés / inactifs) pour WITHDRAW.
|
||
*/
|
||
function fxChronotrackApiSyncLoadParticipantsByPec($intEveId, $intPecId) {
|
||
global $objDatabase;
|
||
|
||
$intEveId = intval($intEveId);
|
||
$intPecId = intval($intPecId);
|
||
if ($intEveId <= 0 || $intPecId <= 0 || !isset($objDatabase) || !is_object($objDatabase)) {
|
||
return array();
|
||
}
|
||
|
||
$strCountryCols = " (SELECT pay_iso FROM inscriptions_pays WHERE pay_id = p.pay_id) AS country_iso2,"
|
||
. " (SELECT pay_nom_en FROM inscriptions_pays WHERE pay_id = p.pay_id) AS country_name";
|
||
if (fxChronotrackApiSyncPaysHasIso3Column()) {
|
||
$strCountryCols = " (SELECT pay_iso3 FROM inscriptions_pays WHERE pay_id = p.pay_id) AS country_iso3,"
|
||
. " (SELECT pay_iso FROM inscriptions_pays WHERE pay_id = p.pay_id) AS country_iso2,"
|
||
. " (SELECT pay_nom_en FROM inscriptions_pays WHERE pay_id = p.pay_id) AS country_name";
|
||
}
|
||
|
||
$sql = "SELECT p.par_id, p.par_id_original, p.eve_id, p.epr_id, p.pec_id, p.rol_id, p.par_equipe, p.par_nom_equipe,"
|
||
. " p.par_prenom, p.par_nom, p.par_sexe,"
|
||
. " p.par_naissance, p.no_bib, p.no_bib_remis, p.no_bib_remis_date, p.par_date_bib, p.par_statut_course,"
|
||
. " p.par_ville, p.par_adresse, p.par_codepostal,"
|
||
. " p.is_cancelled, p.par_maj, p.ct_entry_id,"
|
||
. " ec.pec_equipe, ec.no_equipe, ec.pec_nom_equipe,"
|
||
. " ie.epr_nom_fr AS epr_nom, ie.epr_type_fr AS epr_type,"
|
||
. " (SELECT pro_iso FROM inscriptions_provinces WHERE pro_id = p.pro_id) AS state_iso,"
|
||
. $strCountryCols
|
||
. " FROM resultats_participants p"
|
||
. " JOIN resultats_epreuves_commandees ec ON p.pec_id = ec.pec_id_original"
|
||
. " LEFT JOIN inscriptions_epreuves ie ON ie.epr_id = p.epr_id"
|
||
. " WHERE p.eve_id = " . $intEveId
|
||
. " AND p.pec_id = " . $intPecId
|
||
. " ORDER BY p.rol_id, p.par_id";
|
||
|
||
$tabRows = $objDatabase->fxGetResults($sql);
|
||
if (!is_array($tabRows)) {
|
||
return array();
|
||
}
|
||
$tabOut = array();
|
||
for ($i = 1; $i <= count($tabRows); $i++) {
|
||
$tabOut[] = $tabRows[$i];
|
||
}
|
||
return $tabOut;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4574 — libère les dossards MS1 d’une commande (disponibles à réassigner).
|
||
*/
|
||
function fxChronotrackApiSyncClearMs1BibsForPec($intPecId) {
|
||
global $objDatabase;
|
||
|
||
$intPecId = intval($intPecId);
|
||
if ($intPecId <= 0 || !isset($objDatabase) || !is_object($objDatabase)) {
|
||
return false;
|
||
}
|
||
$strNow = function_exists('fxGetDateTime') ? fxGetDateTime() : date('Y-m-d H:i:s');
|
||
$sql = "UPDATE resultats_participants SET"
|
||
. " no_bib = NULL,"
|
||
. " no_bib_remis = 0,"
|
||
. " no_bib_remis_date = NULL,"
|
||
. " no_bib_remis_par = NULL,"
|
||
. " par_date_bib = NULL,"
|
||
. " par_maj = '" . $objDatabase->fxEscape($strNow) . "'"
|
||
. " WHERE pec_id = " . $intPecId;
|
||
return (bool)$objDatabase->fxQuery($sql);
|
||
}
|
||
|
||
/**
|
||
* MSIN-4574 — PUT/POST WITHDRAWN + bib vide sur une entry CT.
|
||
* Ne crée pas d’entry fantôme : exige entry_id (ct_entry_id) sauf si fourni explicitement.
|
||
*/
|
||
function fxChronotrackApiSyncWithdrawCtEntry($intEveId, array $arrRow, $intCtEventId = 0, array $arrRaceMap = null) {
|
||
$intEveId = intval($intEveId);
|
||
$strEntryId = preg_replace('/[^0-9]/', '', (string)($arrRow['ct_entry_id'] ?? $arrRow['entry_id'] ?? ''));
|
||
$strExternalId = trim((string)($arrRow['par_id_original'] ?? $arrRow['external_id'] ?? ''));
|
||
if ($strExternalId === '' || $strExternalId === '0') {
|
||
return array('state' => 'skip', 'message' => 'external_id manquant');
|
||
}
|
||
if ($strEntryId === '') {
|
||
return array('state' => 'skip', 'message' => 'ct_entry_id manquant — pas de WITHDRAW (éviter création)');
|
||
}
|
||
|
||
if ($intCtEventId <= 0) {
|
||
$arrConfig = fxChronotrackApiConfigGet($intEveId);
|
||
if ($arrConfig === null) {
|
||
return array('state' => 'skip', 'message' => 'Événement non lié à ChronoTrack');
|
||
}
|
||
$intCtEventId = intval($arrConfig['ct_event_id'] ?? 0);
|
||
}
|
||
if ($intCtEventId <= 0) {
|
||
return array('state' => 'skip', 'message' => 'ct_event_id manquant');
|
||
}
|
||
|
||
$arrPayload = array(
|
||
'external_id' => (string)intval($strExternalId),
|
||
'entry_status' => 'WITHDRAWN',
|
||
'status' => 'WITHDRAWN',
|
||
'bib' => '',
|
||
'entry_id' => $strEntryId,
|
||
);
|
||
|
||
$intCtRaceId = intval($arrRow['race_id'] ?? $arrRow['ct_race_id'] ?? 0);
|
||
if ($intCtRaceId <= 0) {
|
||
if ($arrRaceMap === null) {
|
||
$arrRaceMap = fxChronotrackApiRaceMapGetForEvent($intEveId);
|
||
}
|
||
$intEprId = intval($arrRow['epr_id'] ?? 0);
|
||
$intCtRaceId = intval($arrRaceMap[$intEprId]['ct_race_id'] ?? 0);
|
||
}
|
||
if ($intCtRaceId > 0) {
|
||
$arrPayload['race_id'] = $intCtRaceId;
|
||
}
|
||
$arrPayload['event_id'] = $intCtEventId;
|
||
|
||
$arrWrite = fxChronotrackApiSyncPutEntry($strEntryId, $arrPayload);
|
||
$intParId = intval($arrRow['par_id'] ?? 0);
|
||
if (($arrWrite['state'] ?? '') === 'ok') {
|
||
fxChronotrackApiSyncLog(
|
||
$intEveId,
|
||
$intParId,
|
||
'withdraw',
|
||
'ok',
|
||
'WITHDRAWN entry_id=' . $strEntryId . ' external_id=' . $arrPayload['external_id']
|
||
);
|
||
return array(
|
||
'state' => 'ok',
|
||
'entry_id' => $strEntryId,
|
||
'external_id' => $arrPayload['external_id'],
|
||
'message' => 'WITHDRAWN',
|
||
);
|
||
}
|
||
|
||
$strErr = (string)($arrWrite['message'] ?? 'erreur WITHDRAW');
|
||
fxChronotrackApiSyncLog($intEveId, $intParId, 'withdraw', 'error', $strErr);
|
||
return array('state' => 'error', 'message' => $strErr, 'entry_id' => $strEntryId);
|
||
}
|
||
|
||
/**
|
||
* MSIN-4574 — après annulation MS1 : free bib + WITHDRAWN CT (capitaine si équipe).
|
||
*/
|
||
function fxChronotrackApiSyncOnMs1Cancel($intEveId, $intPecId) {
|
||
$intEveId = intval($intEveId);
|
||
$intPecId = intval($intPecId);
|
||
$arrOut = array(
|
||
'state' => 'ok',
|
||
'bibs_cleared' => false,
|
||
'ct_attempted' => false,
|
||
'withdrawn' => 0,
|
||
'skipped' => 0,
|
||
'errors' => array(),
|
||
'message' => '',
|
||
);
|
||
|
||
$arrOut['bibs_cleared'] = fxChronotrackApiSyncClearMs1BibsForPec($intPecId);
|
||
|
||
if ($intEveId <= 0 || !function_exists('fxChronotrackApiConfigGet')) {
|
||
$arrOut['message'] = 'bibs MS1 seulement';
|
||
return $arrOut;
|
||
}
|
||
$arrConfig = fxChronotrackApiConfigGet($intEveId);
|
||
if ($arrConfig === null || !fxChronotrackApiSettingsConfigured()) {
|
||
$arrOut['message'] = 'bibs MS1 — pas de lien ChronoTrack';
|
||
return $arrOut;
|
||
}
|
||
|
||
$arrOut['ct_attempted'] = true;
|
||
$intCtEventId = intval($arrConfig['ct_event_id'] ?? 0);
|
||
$arrRaceMap = fxChronotrackApiRaceMapGetForEvent($intEveId);
|
||
$tabRows = fxChronotrackApiSyncLoadParticipantsByPec($intEveId, $intPecId);
|
||
if (count($tabRows) === 0) {
|
||
$arrOut['message'] = 'aucun participant pour pec';
|
||
return $arrOut;
|
||
}
|
||
|
||
$blnTeam = false;
|
||
foreach ($tabRows as $arrRow) {
|
||
if (fxChronotrackApiSyncIsTeamRow($arrRow)) {
|
||
$blnTeam = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
$tabTargets = array();
|
||
if ($blnTeam) {
|
||
$arrRep = fxChronotrackApiSyncPickTeamRepresentative($tabRows);
|
||
if (is_array($arrRep)) {
|
||
$tabTargets[] = $arrRep;
|
||
}
|
||
} else {
|
||
$tabTargets = $tabRows;
|
||
}
|
||
|
||
foreach ($tabTargets as $arrTarget) {
|
||
$arrW = fxChronotrackApiSyncWithdrawCtEntry($intEveId, $arrTarget, $intCtEventId, $arrRaceMap);
|
||
$strState = (string)($arrW['state'] ?? '');
|
||
if ($strState === 'ok') {
|
||
$arrOut['withdrawn']++;
|
||
} elseif ($strState === 'skip') {
|
||
$arrOut['skipped']++;
|
||
} else {
|
||
$arrOut['errors'][] = (string)($arrW['message'] ?? 'erreur');
|
||
}
|
||
}
|
||
|
||
if (count($arrOut['errors']) > 0) {
|
||
$arrOut['state'] = 'error';
|
||
$arrOut['message'] = 'WITHDRAW partiel — ' . implode(' ; ', $arrOut['errors']);
|
||
} else {
|
||
$arrOut['message'] = 'withdrawn=' . $arrOut['withdrawn']
|
||
. ' skipped=' . $arrOut['skipped']
|
||
. ' bibs=' . ($arrOut['bibs_cleared'] ? 'ok' : 'fail');
|
||
}
|
||
return $arrOut;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4574 — rétablissement : bumper par_maj pour re-push CONF (dossard à réassigner).
|
||
*/
|
||
function fxChronotrackApiSyncOnMs1Restore($intEveId, $intPecId) {
|
||
global $objDatabase;
|
||
|
||
$intEveId = intval($intEveId);
|
||
$intPecId = intval($intPecId);
|
||
if ($intPecId <= 0 || !isset($objDatabase) || !is_object($objDatabase)) {
|
||
return array('state' => 'skip', 'message' => 'pec manquant');
|
||
}
|
||
$strNow = function_exists('fxGetDateTime') ? fxGetDateTime() : date('Y-m-d H:i:s');
|
||
$sql = "UPDATE resultats_participants SET par_maj = '" . $objDatabase->fxEscape($strNow) . "'"
|
||
. " WHERE pec_id = " . $intPecId;
|
||
$blnOk = (bool)$objDatabase->fxQuery($sql);
|
||
if ($blnOk && $intEveId > 0) {
|
||
fxChronotrackApiSyncLog($intEveId, 0, 'restore_bump', 'ok', 'pec_id=' . $intPecId . ' par_maj bumpé');
|
||
}
|
||
return array('state' => $blnOk ? 'ok' : 'error', 'message' => $blnOk ? 'par_maj bumpé' : 'SQL fail');
|
||
}
|
||
|
||
/**
|
||
* MSIN-4574 — retire (WITHDRAWN + clear bib) les orphelins CT hors éligibles MS1.
|
||
*/
|
||
function fxChronotrackApiSyncWithdrawCtOrphans($intEveId) {
|
||
$intEveId = intval($intEveId);
|
||
$arrList = fxChronotrackApiSyncListCtOrphans($intEveId);
|
||
if (($arrList['state'] ?? '') !== 'ok') {
|
||
return $arrList;
|
||
}
|
||
|
||
$intCtEventId = intval($arrList['ct_event_id'] ?? 0);
|
||
$arrRaceMap = fxChronotrackApiRaceMapGetForEvent($intEveId);
|
||
$tabOrphans = is_array($arrList['orphans'] ?? null) ? $arrList['orphans'] : array();
|
||
$intOk = 0;
|
||
$intSkip = 0;
|
||
$tabErrors = array();
|
||
$tabDone = array();
|
||
|
||
foreach ($tabOrphans as $arrOrphan) {
|
||
// hors_eligible = annulés / transferts / ex-membres ; doublons aussi à retirer
|
||
$strReason = (string)($arrOrphan['reason'] ?? '');
|
||
if ($strReason !== 'hors_eligible' && $strReason !== 'doublon') {
|
||
$intSkip++;
|
||
continue;
|
||
}
|
||
$arrRow = array(
|
||
'entry_id' => $arrOrphan['entry_id'] ?? '',
|
||
'ct_entry_id' => $arrOrphan['entry_id'] ?? '',
|
||
'external_id' => $arrOrphan['external_id'] ?? '',
|
||
'par_id_original' => $arrOrphan['external_id'] ?? '',
|
||
'race_id' => intval($arrOrphan['race_id'] ?? 0),
|
||
'epr_id' => 0,
|
||
'par_id' => 0,
|
||
);
|
||
$arrW = fxChronotrackApiSyncWithdrawCtEntry($intEveId, $arrRow, $intCtEventId, $arrRaceMap);
|
||
$strState = (string)($arrW['state'] ?? '');
|
||
if ($strState === 'ok') {
|
||
$intOk++;
|
||
$tabDone[] = array(
|
||
'entry_id' => $arrOrphan['entry_id'] ?? '',
|
||
'external_id' => $arrOrphan['external_id'] ?? '',
|
||
'name' => $arrOrphan['name'] ?? '',
|
||
);
|
||
} elseif ($strState === 'skip') {
|
||
$intSkip++;
|
||
} else {
|
||
$tabErrors[] = ($arrOrphan['name'] ?? '') . ': ' . ($arrW['message'] ?? 'erreur');
|
||
}
|
||
}
|
||
|
||
fxChronotrackApiSyncLog(
|
||
$intEveId,
|
||
0,
|
||
'reconcile',
|
||
count($tabErrors) > 0 ? 'error' : 'ok',
|
||
'orphans WITHDRAWN ok=' . $intOk . ' skip=' . $intSkip . ' err=' . count($tabErrors)
|
||
);
|
||
|
||
return array(
|
||
'state' => count($tabErrors) > 0 ? 'error' : 'ok',
|
||
'message' => 'WITHDRAWN ' . $intOk . ' orphelin(s)'
|
||
. ($intSkip > 0 ? ', ' . $intSkip . ' ignoré(s)' : '')
|
||
. (count($tabErrors) > 0 ? ' — erreurs: ' . implode(' ; ', $tabErrors) : ''),
|
||
'withdrawn_count' => $intOk,
|
||
'skipped_count' => $intSkip,
|
||
'errors' => $tabErrors,
|
||
'withdrawn' => $tabDone,
|
||
'orphan_count' => count($tabOrphans),
|
||
);
|
||
}
|
||
|
||
/**
|
||
* MSIN-4328 — push différentiel complet pour un event (cron auto).
|
||
*/
|
||
function fxChronotrackApiSyncRunAutoPushForEvent($intEveId) {
|
||
$intEveId = intval($intEveId);
|
||
$arrPrep = fxChronotrackApiSyncPushPrepare($intEveId);
|
||
if (($arrPrep['state'] ?? '') !== 'ok') {
|
||
// Rien à pousser = succès « idle »
|
||
$strMsg = (string)($arrPrep['message'] ?? 'prepare failed');
|
||
if (stripos($strMsg, 'Aucun changement') !== false || intval($arrPrep['pending_push'] ?? -1) === 0) {
|
||
fxChronotrackApiSyncLog($intEveId, 0, 'auto_push', 'ok', 'idle — ' . $strMsg);
|
||
return array('state' => 'ok', 'message' => $strMsg, 'pushed_ok' => 0, 'idle' => true);
|
||
}
|
||
fxChronotrackApiSyncLog($intEveId, 0, 'auto_push', 'error', $strMsg);
|
||
return $arrPrep;
|
||
}
|
||
|
||
$intOffset = 0;
|
||
$intLimit = MSIN_API_CHRONOTRACK_SYNC_CHUNK_SIZE;
|
||
$intOkSum = 0;
|
||
$intErrSum = 0;
|
||
$intLoops = 0;
|
||
$intMaxLoops = 500;
|
||
|
||
while ($intLoops < $intMaxLoops) {
|
||
$intLoops++;
|
||
$arrChunk = fxChronotrackApiSyncPushEvent($intEveId, array(
|
||
'offset' => $intOffset,
|
||
'limit' => $intLimit,
|
||
));
|
||
$intOkSum += intval($arrChunk['pushed_ok'] ?? 0);
|
||
$intErrSum += intval($arrChunk['pushed_error'] ?? 0);
|
||
if (!empty($arrChunk['done'])) {
|
||
break;
|
||
}
|
||
// Identité finie → drain dossards en sous-lots
|
||
if (!empty($arrChunk['identity_done']) && intval($arrChunk['bib_pending'] ?? 0) > 0) {
|
||
$intBibLoops = 0;
|
||
while ($intBibLoops < $intMaxLoops) {
|
||
$intBibLoops++;
|
||
$arrBib = fxChronotrackApiSyncPushBibsPhase($intEveId, array(
|
||
'limit' => MSIN_API_CHRONOTRACK_SYNC_BIB_CHUNK_SIZE,
|
||
));
|
||
$intOkSum += intval($arrBib['pushed_ok'] ?? 0);
|
||
$intErrSum += intval($arrBib['pushed_error'] ?? 0);
|
||
if (!empty($arrBib['done'])) {
|
||
break;
|
||
}
|
||
if (($arrBib['state'] ?? '') === 'error' && intval($arrBib['pushed_ok'] ?? 0) === 0) {
|
||
break;
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
if (($arrChunk['state'] ?? '') === 'error' && intval($arrChunk['pushed_ok'] ?? 0) === 0) {
|
||
break;
|
||
}
|
||
$intOffset = intval($arrChunk['next_offset'] ?? ($intOffset + $intLimit));
|
||
}
|
||
|
||
$strState = ($intErrSum === 0) ? 'ok' : (($intOkSum > 0) ? 'partial' : 'error');
|
||
$strMsg = 'auto push ok=' . $intOkSum . ' err=' . $intErrSum
|
||
. ' total_job=' . intval($arrPrep['total'] ?? 0);
|
||
fxChronotrackApiSyncLog($intEveId, 0, 'auto_push', ($strState === 'ok') ? 'ok' : 'error', $strMsg);
|
||
|
||
return array(
|
||
'state' => $strState,
|
||
'message' => $strMsg,
|
||
'pushed_ok' => $intOkSum,
|
||
'pushed_error' => $intErrSum,
|
||
'idle' => false,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* MSIN-4328 — passage cron : events auto ON, fréquence / fin respectées.
|
||
* Chaque décision (run / skip / expire) écrit un log pour l’événement — sinon
|
||
* « Voir les logs » reste vide alors que le CrowdJob tourne vraiment.
|
||
*/
|
||
function fxChronotrackApiSyncRunAutoCron() {
|
||
if (!fxChronotrackApiSettingsConfigured()) {
|
||
return array(
|
||
'state' => 'error',
|
||
'message' => fxChronotrackApiSettingsErrorMessage(),
|
||
'ran' => 0,
|
||
'skipped' => 0,
|
||
'enabled_configs' => 0,
|
||
);
|
||
}
|
||
|
||
$tabConfigs = fxChronotrackApiConfigListAutoSyncEnabled();
|
||
$intRan = 0;
|
||
$intSkipped = 0;
|
||
$intExpired = 0;
|
||
$tabDetails = array();
|
||
$intNow = time();
|
||
$intEnabled = count($tabConfigs);
|
||
|
||
foreach ($tabConfigs as $arrCfg) {
|
||
$intEveId = intval($arrCfg['eve_id'] ?? 0);
|
||
if ($intEveId <= 0) {
|
||
continue;
|
||
}
|
||
|
||
$strUntil = trim((string)($arrCfg['auto_sync_until'] ?? ''));
|
||
if ($strUntil !== '' && $strUntil !== '0000-00-00 00:00:00') {
|
||
$intUntil = strtotime($strUntil);
|
||
if ($intUntil !== false && $intNow > $intUntil) {
|
||
fxChronotrackApiConfigDisableAutoSync($intEveId);
|
||
fxChronotrackApiSyncLog(
|
||
$intEveId,
|
||
0,
|
||
'auto_sync',
|
||
'ok',
|
||
'Échéance atteinte (' . $strUntil . ') — sync auto désactivée'
|
||
);
|
||
$intExpired++;
|
||
$tabDetails[] = array('eve_id' => $intEveId, 'action' => 'expired');
|
||
continue;
|
||
}
|
||
} else {
|
||
// Pas de fin = on ne tourne pas (UI exige une fin à l’enregistrement)
|
||
$intSkipped++;
|
||
fxChronotrackApiSyncLog(
|
||
$intEveId,
|
||
0,
|
||
'auto_cron',
|
||
'error',
|
||
'Skip: date/heure de fin manquante (réenregistrer la sync auto)'
|
||
);
|
||
$tabDetails[] = array('eve_id' => $intEveId, 'action' => 'skip_no_until');
|
||
continue;
|
||
}
|
||
|
||
$intInterval = intval($arrCfg['auto_sync_interval_min'] ?? 15);
|
||
if (!in_array($intInterval, fxChronotrackApiConfigAutoSyncIntervals(), true)) {
|
||
$intInterval = 15;
|
||
}
|
||
$strLast = trim((string)($arrCfg['last_auto_run_at'] ?? ''));
|
||
if ($strLast !== '' && $strLast !== '0000-00-00 00:00:00') {
|
||
$intLast = strtotime($strLast);
|
||
if ($intLast !== false && ($intNow - $intLast) < ($intInterval * 60)) {
|
||
$intSkipped++;
|
||
// Trace légère: 1 seule fois par fenêtre d’intervalle (évite 1440 lignes/jour).
|
||
// Déjà logué ailleurs au run — ici on ne spam pas.
|
||
$tabDetails[] = array('eve_id' => $intEveId, 'action' => 'skip_interval');
|
||
continue;
|
||
}
|
||
}
|
||
|
||
$arrRes = fxChronotrackApiSyncRunAutoPushForEvent($intEveId);
|
||
fxChronotrackApiConfigTouchAutoRun($intEveId);
|
||
$intRan++;
|
||
$tabDetails[] = array(
|
||
'eve_id' => $intEveId,
|
||
'action' => 'run',
|
||
'result' => $arrRes['state'] ?? 'error',
|
||
'message' => $arrRes['message'] ?? '',
|
||
'ok' => intval($arrRes['pushed_ok'] ?? 0),
|
||
);
|
||
}
|
||
|
||
$strMsg = 'cron auto — configs_on=' . $intEnabled
|
||
. ' ran=' . $intRan . ' skipped=' . $intSkipped . ' expired=' . $intExpired;
|
||
if ($intEnabled === 0) {
|
||
$strMsg .= ' (aucun event avec sync auto ON sur cette base)';
|
||
}
|
||
|
||
return array(
|
||
'state' => 'ok',
|
||
'message' => $strMsg,
|
||
'ran' => $intRan,
|
||
'skipped' => $intSkipped,
|
||
'expired' => $intExpired,
|
||
'enabled_configs' => $intEnabled,
|
||
'details' => $tabDetails,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* MSIN-4328 — état sync auto + logs pour un événement (diag UI, pas d’essai-erreur SSL).
|
||
*/
|
||
function fxChronotrackApiSyncAutoDiag($intEveId) {
|
||
global $objDatabase;
|
||
|
||
$intEveId = intval($intEveId);
|
||
$arrCfg = fxChronotrackApiConfigGet($intEveId);
|
||
if ($arrCfg === null) {
|
||
return array('state' => 'error', 'message' => 'Événement non lié à ChronoTrack');
|
||
}
|
||
|
||
$blnColsOk = true;
|
||
$strColNote = '';
|
||
$sqlCol = "SELECT COUNT(*) AS n FROM information_schema.COLUMNS"
|
||
. " WHERE TABLE_SCHEMA = DATABASE()"
|
||
. " AND TABLE_NAME = 'api_chronotrack_config'"
|
||
. " AND COLUMN_NAME = 'auto_sync_enabled'";
|
||
$arrCol = $objDatabase->fxGetRow($sqlCol);
|
||
if (intval($arrCol['n'] ?? 0) < 1) {
|
||
$blnColsOk = false;
|
||
$strColNote = 'Colonnes auto_sync absentes — exécuter sql/MSIN-4328-chronotrack-auto-sync-config.sql';
|
||
}
|
||
|
||
$sqlLog = "SELECT COUNT(*) AS n FROM api_chronotrack_sync_log WHERE eve_id = " . $intEveId;
|
||
$arrLogCnt = $objDatabase->fxGetRow($sqlLog);
|
||
$intLogCnt = intval($arrLogCnt['n'] ?? 0);
|
||
|
||
$arrLastLog = $objDatabase->fxGetRow(
|
||
"SELECT log_id, action, status, message, created_at FROM api_chronotrack_sync_log"
|
||
. " WHERE eve_id = " . $intEveId
|
||
. " ORDER BY log_id DESC LIMIT 1"
|
||
);
|
||
|
||
$blnOn = (intval($arrCfg['auto_sync_enabled'] ?? 0) === 1);
|
||
$intInterval = intval($arrCfg['auto_sync_interval_min'] ?? 15);
|
||
$strUntil = trim((string)($arrCfg['auto_sync_until'] ?? ''));
|
||
$strLastRun = trim((string)($arrCfg['last_auto_run_at'] ?? ''));
|
||
$strSyncStatus = trim((string)($arrCfg['sync_status'] ?? ''));
|
||
$intNow = time();
|
||
$strWould = 'inconnu';
|
||
if (!$blnColsOk) {
|
||
$strWould = 'bloqué: SQL auto_sync manquant';
|
||
} elseif ($strSyncStatus === MSIN_API_CHRONOTRACK_SYNC_FERME) {
|
||
$strWould = 'ne tourne pas: événement Fermé';
|
||
} elseif ($strSyncStatus !== MSIN_API_CHRONOTRACK_SYNC_ACTIF) {
|
||
// MSIN-4574 — Lié ≠ Actif pour le cron
|
||
$strWould = 'ne tourne pas: statut « '
|
||
. fxChronotrackApiConfigStatusLabel($strSyncStatus)
|
||
. ' » — passer en Actif (Cycle de vie)';
|
||
} elseif (!$blnOn) {
|
||
$strWould = 'ne tourne pas: sync auto OFF';
|
||
} elseif ($strUntil === '' || $strUntil === '0000-00-00 00:00:00') {
|
||
$strWould = 'ne tourne pas: date de fin manquante';
|
||
} elseif (strtotime($strUntil) !== false && $intNow > strtotime($strUntil)) {
|
||
$strWould = 'ne tourne pas: date de fin dépassée (' . $strUntil . ')';
|
||
} elseif ($strLastRun !== '' && $strLastRun !== '0000-00-00 00:00:00'
|
||
&& strtotime($strLastRun) !== false
|
||
&& ($intNow - strtotime($strLastRun)) < ($intInterval * 60)) {
|
||
$strWould = 'en attente d’intervalle (dernier passage ' . $strLastRun
|
||
. ', fréquence ' . $intInterval . ' min)';
|
||
} else {
|
||
$strWould = 'devrait pousser au prochain appel de auto_chronotrack_sync.php';
|
||
}
|
||
|
||
return array(
|
||
'state' => 'ok',
|
||
'message' => $strWould,
|
||
'diag' => array(
|
||
'cols_ok' => $blnColsOk,
|
||
'cols_note' => $strColNote,
|
||
'auto_enabled' => $blnOn,
|
||
'sync_status' => $strSyncStatus,
|
||
'interval_min' => $intInterval,
|
||
'until' => $strUntil !== '' ? $strUntil : null,
|
||
'last_auto_run_at' => $strLastRun !== '' ? $strLastRun : null,
|
||
'log_count' => $intLogCnt,
|
||
'last_log' => is_array($arrLastLog) ? $arrLastLog : null,
|
||
'host' => isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '',
|
||
'would' => $strWould,
|
||
),
|
||
);
|
||
}
|