1930 lines
69 KiB
PHP
1930 lines
69 KiB
PHP
<?php
|
||
/**
|
||
* MSIN API ChronoTrack — Phase 2a : lecture MS1, diff, push manuel entries.
|
||
*/
|
||
|
||
// Lot POST ChronoTrack — taille prudente jusqu’à validation par « Sonde lots » (MSIN-4328).
|
||
define('MSIN_API_CHRONOTRACK_SYNC_BATCH_SIZE', 100);
|
||
define('MSIN_API_CHRONOTRACK_SYNC_CHUNK_SIZE', 100);
|
||
|
||
/**
|
||
* 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;
|
||
}
|
||
|
||
function fxChronotrackApiSyncIso2ToIso3($strIso2) {
|
||
static $tabMap = null;
|
||
if ($tabMap === null) {
|
||
$tabMap = array(
|
||
'CA' => 'CAN', 'US' => 'USA', 'MX' => 'MEX', 'FR' => 'FRA', 'GB' => 'GBR', 'UK' => 'GBR',
|
||
'DE' => 'DEU', 'ES' => 'ESP', 'IT' => 'ITA', 'BE' => 'BEL', 'CH' => 'CHE', 'AU' => 'AUS',
|
||
'JP' => 'JPN', 'CN' => 'CHN', 'BR' => 'BRA', 'NL' => 'NLD', 'SE' => 'SWE', 'NO' => 'NOR',
|
||
'DK' => 'DNK', 'FI' => 'FIN', 'IE' => 'IRL', 'PT' => 'PRT', 'PL' => 'POL', 'AT' => 'AUT',
|
||
'NZ' => 'NZL', 'IN' => 'IND', 'KR' => 'KOR', 'AD' => 'AND', 'AE' => 'ARE', 'AF' => 'AFG',
|
||
);
|
||
}
|
||
$strIso2 = strtoupper(trim((string)$strIso2));
|
||
if ($strIso2 === '') {
|
||
return '';
|
||
}
|
||
if (strlen($strIso2) === 3) {
|
||
return $strIso2;
|
||
}
|
||
return isset($tabMap[$strIso2]) ? $tabMap[$strIso2] : '';
|
||
}
|
||
|
||
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';
|
||
}
|
||
}
|
||
|
||
function fxChronotrackApiSyncNormalizeSex($strSexe) {
|
||
$strSexe = strtolower(trim((string)$strSexe));
|
||
if ($strSexe === 'f') {
|
||
return 'F';
|
||
}
|
||
if ($strSexe === 'h' || $strSexe === 'm') {
|
||
return 'M';
|
||
}
|
||
if ($strSexe === '' || $strSexe === 'n') {
|
||
return 'NOT SPECIFIED';
|
||
}
|
||
// TODO ChronoTrack : « a » (autre / non-binaire selon config épreuve) — confirmer option CT.
|
||
return '';
|
||
}
|
||
|
||
function fxChronotrackApiSyncHasValidSex($strSexe) {
|
||
return fxChronotrackApiSyncNormalizeSex($strSexe) !== '';
|
||
}
|
||
|
||
function fxChronotrackApiSyncExtractBib(array $arrRow) {
|
||
$strBib = trim((string)($arrRow['no_bib'] ?? ''));
|
||
if ($strBib === '' || $strBib === '0') {
|
||
return '';
|
||
}
|
||
return $strBib;
|
||
}
|
||
|
||
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-4328 — éligibles dont par_maj > dernier push (ou tous si jamais poussé).
|
||
*/
|
||
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;
|
||
}
|
||
}
|
||
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('Inscription équipe — transfert individuel seulement (traitement équipe à venir)');
|
||
$strTeamName = trim((string)($arrRow['pec_nom_equipe'] ?? $arrRow['par_nom_equipe'] ?? ''));
|
||
if ($strTeamName !== '') {
|
||
$arrParts[] = '« ' . $strTeamName . ' »';
|
||
}
|
||
$strNoEquipe = trim((string)($arrRow['no_equipe'] ?? ''));
|
||
if ($strNoEquipe !== '' && $strNoEquipe !== '0') {
|
||
$arrParts[] = 'no équipe #' . $strNoEquipe;
|
||
}
|
||
return implode(' · ', $arrParts);
|
||
}
|
||
|
||
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' => trim((string)($arrRow['pec_nom_equipe'] ?? $arrRow['par_nom_equipe'] ?? '')),
|
||
'no_equipe' => trim((string)($arrRow['no_equipe'] ?? '')),
|
||
'role_label' => fxChronotrackApiSyncFormatRoleLabel($arrRow['rol_id'] ?? 0),
|
||
);
|
||
}
|
||
|
||
function fxChronotrackApiSyncGroupBlockedByType(array $tabBlocked) {
|
||
$tabTypeLabels = array(
|
||
'team' => 'Inscriptions équipe',
|
||
'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', '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) {
|
||
$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;
|
||
$strBib = fxChronotrackApiSyncExtractBib($arrRow);
|
||
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;
|
||
$intBlockedNoBib = 0;
|
||
$intBlockedNoSex = 0;
|
||
$intBlockedNoFirstName = 0;
|
||
$intBlockedNoLastName = 0;
|
||
$intBlockedDuplicateBib = 0;
|
||
|
||
foreach ($tabCandidates as $arrRow) {
|
||
$arrSummary = fxChronotrackApiSyncParticipantSummary($arrRow);
|
||
|
||
if (fxChronotrackApiSyncIsTeamRow($arrRow)) {
|
||
$intBlockedTeam++;
|
||
$tabBlocked[] = array(
|
||
'code' => 'team',
|
||
'message' => fxChronotrackApiSyncFormatTeamBlockMessage($arrRow),
|
||
) + $arrSummary;
|
||
continue;
|
||
}
|
||
|
||
$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 = fxChronotrackApiSyncBuildEntryPayload($arrRow, $arrRaceMap, $intCtEventId);
|
||
if ($arrPayload === null) {
|
||
continue;
|
||
}
|
||
|
||
$tabTransferable[] = array(
|
||
'row' => $arrRow,
|
||
'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,
|
||
'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,
|
||
'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";
|
||
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";
|
||
}
|
||
|
||
$sql = "SELECT p.par_id, p.par_id_original, 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.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;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
$strCountry = strtoupper(trim((string)($arrRow['country_iso3'] ?? '')));
|
||
if ($strCountry === '') {
|
||
$strCountry = fxChronotrackApiSyncIso2ToIso3($arrRow['country_iso2'] ?? '');
|
||
}
|
||
$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,
|
||
'bib' => $strBib,
|
||
);
|
||
|
||
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;
|
||
}
|
||
|
||
$strStreet = trim((string)($arrRow['par_adresse'] ?? ''));
|
||
if ($strStreet !== '') {
|
||
$arrEntry['street'] = $strStreet;
|
||
}
|
||
|
||
$strCity = trim((string)($arrRow['par_ville'] ?? ''));
|
||
if ($strCity !== '') {
|
||
$arrEntry['city'] = $strCity;
|
||
}
|
||
|
||
$strPostal = trim((string)($arrRow['par_codepostal'] ?? ''));
|
||
if ($strPostal !== '') {
|
||
$arrEntry['postal_code'] = $strPostal;
|
||
}
|
||
|
||
if ($strState !== '') {
|
||
$arrEntry['state_code'] = $strState;
|
||
}
|
||
|
||
if ($strCountry !== '') {
|
||
$arrEntry['country_code'] = $strCountry;
|
||
}
|
||
|
||
$arrEntry['entry_status'] = fxChronotrackApiSyncMapEntryStatus(
|
||
$arrRow['par_statut_course'] ?? 'CONF',
|
||
intval($arrRow['is_cancelled'] ?? 0) === 1
|
||
);
|
||
$arrEntry['status'] = $arrEntry['entry_status'];
|
||
|
||
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');
|
||
}
|
||
|
||
static $strBodyFormat = 'array';
|
||
|
||
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;
|
||
}
|
||
|
||
// Essai wrapper une seule fois (mémoire du format si succès).
|
||
$arrPostWrap = fxChronotrackApiOAuthApiPost(
|
||
'event/' . $intCtEventId . '/entry',
|
||
array('event_entry' => $tabEntries)
|
||
);
|
||
if ($arrPostWrap['state'] === 'ok') {
|
||
$strBodyFormat = 'event_entry';
|
||
$arrPostWrap['body_format'] = 'event_entry';
|
||
return $arrPostWrap;
|
||
}
|
||
|
||
$arrPost['body_format'] = 'array';
|
||
$arrPost['fallback_error'] = $arrPostWrap['message'] ?? '';
|
||
return $arrPost;
|
||
}
|
||
|
||
/**
|
||
* POST entries avec découpage dichotomique si le lot échoue (évite 250 appels unitaires).
|
||
* MSIN-4328
|
||
*/
|
||
function fxChronotrackApiSyncPostEntriesAdaptive($intCtEventId, array $tabEntries, array &$tabErrors, $intEveId = 0, array $tabMeta = array()) {
|
||
$intCount = count($tabEntries);
|
||
if ($intCount === 0) {
|
||
return array('ok' => 0, 'err' => 0);
|
||
}
|
||
|
||
$arrPost = fxChronotrackApiSyncPostEntries($intCtEventId, $tabEntries);
|
||
if ($arrPost['state'] === 'ok') {
|
||
// MSIN-4328 — récupérer entry_id CT pour lien admin
|
||
if ($intEveId > 0 && count($tabMeta) > 0) {
|
||
fxChronotrackApiSyncApplyEntryIdsFromApiJson($arrPost['json'] ?? null, $tabMeta);
|
||
}
|
||
return array('ok' => $intCount, 'err' => 0);
|
||
}
|
||
|
||
if ($intCount === 1) {
|
||
$arrM = $tabMeta[0] ?? array('par_id' => 0, 'external_id' => '');
|
||
$strOneErr = $arrPost['message'] ?? 'Erreur POST entry';
|
||
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'] ?? '') . ' — ' . $strOneErr);
|
||
}
|
||
return array('ok' => 0, 'err' => 1);
|
||
}
|
||
|
||
$intMid = (int)floor($intCount / 2);
|
||
$arrLeft = fxChronotrackApiSyncPostEntriesAdaptive(
|
||
$intCtEventId,
|
||
array_slice($tabEntries, 0, $intMid),
|
||
$tabErrors,
|
||
$intEveId,
|
||
array_slice($tabMeta, 0, $intMid)
|
||
);
|
||
$arrRight = fxChronotrackApiSyncPostEntriesAdaptive(
|
||
$intCtEventId,
|
||
array_slice($tabEntries, $intMid),
|
||
$tabErrors,
|
||
$intEveId,
|
||
array_slice($tabMeta, $intMid)
|
||
);
|
||
return array(
|
||
'ok' => $arrLeft['ok'] + $arrRight['ok'],
|
||
'err' => $arrLeft['err'] + $arrRight['err'],
|
||
);
|
||
}
|
||
|
||
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).');
|
||
}
|
||
|
||
$arrFirst = $arrClass['transferable'][0];
|
||
$arrPayload = $arrFirst['payload'];
|
||
$arrRow = $arrFirst['row'];
|
||
|
||
$arrCtFetch = fxChronotrackApiSyncFetchCtEntriesIndexed($intCtEventId);
|
||
if ($arrCtFetch['state'] === 'ok') {
|
||
$strExt = $arrPayload['external_id'];
|
||
$arrPayload = fxChronotrackApiSyncEnrichPayloadWithCtEntry(
|
||
$arrPayload,
|
||
$arrCtFetch['entries'][$strExt] ?? null
|
||
);
|
||
}
|
||
|
||
$arrPost = fxChronotrackApiSyncPushOneEntry($intCtEventId, $arrPayload);
|
||
|
||
return array(
|
||
'state' => $arrPost['state'],
|
||
'message' => $arrPost['message'] ?? '',
|
||
'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'] ?? '',
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 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 = 100;
|
||
$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);
|
||
$arrClass = fxChronotrackApiSyncClassifyParticipants($tabParticipants, $arrRaceMap, $intCtEventId);
|
||
|
||
$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'],
|
||
'blocked' => $arrClass['blocked'],
|
||
'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'],
|
||
'anomalies' => $arrClass['blocked'],
|
||
'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,
|
||
'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.
|
||
*/
|
||
function fxChronotrackApiSyncPushPrepare($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);
|
||
$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 — ne pousser que les éligibles modifiés depuis last_push_at
|
||
$strLastPushAt = trim((string)($arrConfig['last_push_at'] ?? ''));
|
||
$tabPending = fxChronotrackApiSyncFilterChangedSincePush($arrClass['transferable'], $strLastPushAt);
|
||
|
||
// Plus de scan CT à la préparation : POST upsert (crée ou maj) sans entry_id.
|
||
$tabBatch = array();
|
||
$tabMeta = array();
|
||
foreach ($tabPending as $arrItem) {
|
||
$tabBatch[] = fxChronotrackApiSyncPayloadForPost($arrItem['payload']);
|
||
$tabMeta[] = array(
|
||
'par_id' => intval($arrItem['row']['par_id'] ?? 0),
|
||
'external_id' => $arrItem['payload']['external_id'],
|
||
);
|
||
}
|
||
|
||
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,
|
||
);
|
||
}
|
||
|
||
fxChronotrackApiSyncLog(
|
||
$intEveId,
|
||
0,
|
||
'push_prepare',
|
||
'ok',
|
||
'job prêt — pending=' . count($tabBatch)
|
||
. ' éligibles=' . count($arrClass['transferable'])
|
||
. ' exclus=' . intval($arrClass['blocked_count'])
|
||
);
|
||
|
||
$arrJob = array(
|
||
'ct_event_id' => $intCtEventId,
|
||
'batch' => $tabBatch,
|
||
'meta' => $tabMeta,
|
||
'blocked_count' => $arrClass['blocked_count'],
|
||
'ok_sum' => 0,
|
||
'err_sum' => 0,
|
||
'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',
|
||
'total' => count($tabBatch),
|
||
'pending_push' => count($tabBatch),
|
||
'transferable' => count($arrClass['transferable']),
|
||
'blocked_count' => $arrClass['blocked_count'],
|
||
'chunk_size' => MSIN_API_CHRONOTRACK_SYNC_CHUNK_SIZE,
|
||
'ct_event_id' => $intCtEventId,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Push un slice du job préparé. MSIN-4328.
|
||
*/
|
||
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);
|
||
|
||
$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,
|
||
'done' => true,
|
||
'blocked_count' => $intSkipped,
|
||
);
|
||
}
|
||
|
||
$intOk = 0;
|
||
$intErr = 0;
|
||
$intPutOk = 0;
|
||
$intPostOk = 0;
|
||
$tabErrors = array();
|
||
|
||
$tabCreates = array();
|
||
$tabCreatesMeta = array();
|
||
$tabUpdates = array();
|
||
$tabUpdatesMeta = array();
|
||
|
||
foreach ($tabBatch as $intIdx => $arrPayload) {
|
||
$arrMeta = $tabMeta[$intIdx];
|
||
if (trim((string)($arrPayload['entry_id'] ?? '')) !== '') {
|
||
$tabUpdates[] = $arrPayload;
|
||
$tabUpdatesMeta[] = $arrMeta;
|
||
} else {
|
||
$tabCreates[] = fxChronotrackApiSyncPayloadForPost($arrPayload);
|
||
$tabCreatesMeta[] = $arrMeta;
|
||
}
|
||
}
|
||
|
||
// Créations + maj via POST lot (upsert) — découpage dichotomique si échec.
|
||
if (count($tabCreates) > 0) {
|
||
$arrRes = fxChronotrackApiSyncPostEntriesAdaptive(
|
||
$intCtEventId,
|
||
$tabCreates,
|
||
$tabErrors,
|
||
$intEveId,
|
||
$tabCreatesMeta
|
||
);
|
||
$intOk += $arrRes['ok'];
|
||
$intPostOk += $arrRes['ok'];
|
||
$intErr += $arrRes['err'];
|
||
}
|
||
|
||
if (count($tabUpdates) > 0) {
|
||
$tabUpdatesForPost = array();
|
||
foreach ($tabUpdates as $arrPayload) {
|
||
$tabUpdatesForPost[] = fxChronotrackApiSyncPayloadForPost($arrPayload);
|
||
}
|
||
$arrRes = fxChronotrackApiSyncPostEntriesAdaptive(
|
||
$intCtEventId,
|
||
$tabUpdatesForPost,
|
||
$tabErrors,
|
||
$intEveId,
|
||
$tabUpdatesMeta
|
||
);
|
||
$intOk += $arrRes['ok'];
|
||
$intPostOk += $arrRes['ok'];
|
||
$intErr += $arrRes['err'];
|
||
}
|
||
|
||
// Si des MAJ restent en erreur (POST n’a pas pris le statut), tenter PUT unitaire
|
||
// uniquement pour ce petit sous-ensemble déjà en erreur — trop coûteux de re-PUT tout.
|
||
// Les erreurs sont déjà dans $tabErrors / logs.
|
||
|
||
fxChronotrackApiSyncLog(
|
||
$intEveId,
|
||
0,
|
||
'push_chunk',
|
||
($intErr === 0) ? 'ok' : 'error',
|
||
'offset=' . $intOffset . ' n=' . $intChunkCount
|
||
. ' ok=' . $intOk . ' err=' . $intErr
|
||
. ' POST=' . $intPostOk . ' PUT=' . $intPutOk
|
||
);
|
||
|
||
$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;
|
||
$blnDone = ($intNextOffset >= $intTotal);
|
||
$blnLastPushSaved = false;
|
||
if ($blnDone) {
|
||
// MSIN-4328 — ne figer last_push_at que si tout le job a réussi
|
||
if ($intErrSum === 0) {
|
||
$blnLastPushSaved = fxChronotrackApiConfigSetLastPush($intEveId, $intOkSum);
|
||
}
|
||
fxChronotrackApiSyncPushJobClear($intEveId);
|
||
} else {
|
||
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.');
|
||
|
||
return array(
|
||
'state' => $strState,
|
||
'message' => $strMessage,
|
||
'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' => $blnDone,
|
||
);
|
||
}
|
||
|
||
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).
|
||
*/
|
||
function fxChronotrackApiSyncListCtOrphans($intEveId) {
|
||
$arrPreview = fxChronotrackApiSyncBuildPreview($intEveId);
|
||
if ($arrPreview['state'] !== 'ok') {
|
||
return $arrPreview;
|
||
}
|
||
|
||
$intCtEventId = intval($arrPreview['ct_event_id']);
|
||
$arrRaceMap = fxChronotrackApiRaceMapGetForEvent($intEveId);
|
||
$tabParticipants = fxChronotrackApiSyncLoadMs1Participants($intEveId);
|
||
$arrClass = fxChronotrackApiSyncClassifyParticipants($tabParticipants, $arrRaceMap, $intCtEventId);
|
||
|
||
$tabEligibleKeys = array();
|
||
foreach ($arrClass['transferable'] as $arrItem) {
|
||
$tabEligibleKeys[$arrItem['payload']['external_id']] = true;
|
||
}
|
||
|
||
$arrFetch = fxChronotrackApiSyncFetchAllCtEntryEntities($intCtEventId);
|
||
if ($arrFetch['state'] !== 'ok') {
|
||
return array(
|
||
'state' => 'error',
|
||
'message' => $arrFetch['message'] ?? 'Erreur lecture entries CT',
|
||
);
|
||
}
|
||
|
||
$tabOrphans = fxChronotrackApiSyncComputeCtOrphanRows($arrFetch['entities'], $tabEligibleKeys);
|
||
|
||
return array(
|
||
'state' => 'ok',
|
||
'ct_event_id' => $intCtEventId,
|
||
'ct_total' => intval($arrPreview['ct_total'] ?? 0),
|
||
'linked_count' => intval($arrPreview['already_in_ct'] ?? 0),
|
||
'count' => count($tabOrphans),
|
||
'orphans' => $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;
|
||
}
|
||
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'] ?? ''));
|
||
$intNow = time();
|
||
$strWould = 'inconnu';
|
||
if (!$blnColsOk) {
|
||
$strWould = 'bloqué: SQL auto_sync manquant';
|
||
} 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,
|
||
'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,
|
||
),
|
||
);
|
||
}
|