Files
ms1inscription-v5/php/chronotrack_api/fx_chronotrack_sync.php

1246 lines
44 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* MSIN API ChronoTrack — Phase 2a : lecture MS1, diff, push manuel entries.
*/
// Lot POST ChronoTrack (créations). Plus gros = moins daller-retours HTTP (MSIN-4328).
define('MSIN_API_CHRONOTRACK_SYNC_BATCH_SIZE', 50);
// Taille dun chunk AJAX (barre davancement côté admin).
define('MSIN_API_CHRONOTRACK_SYNC_CHUNK_SIZE', 50);
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'] ?? '')));
}
function fxChronotrackApiSyncIsTeamRow(array $arrRow) {
return intval($arrRow['pec_equipe'] ?? 0) === 1 || intval($arrRow['par_equipe'] ?? 0) === 1;
}
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_race' => 'Épreuve non mappée',
'no_external' => 'External ID manquant',
);
$tabTypeOrder = array('team', 'duplicate_bib', 'no_bib', 'no_sex', '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);
$tabBlocked[] = array(
'code' => 'no_race',
'message' => 'Épreuve MS1 (epr_id ' . $intEprId . ') non mappée vers ChronoTrack',
) + $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;
$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;
}
$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_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;
$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() . "'";
$objDatabase->fxQuery($sql);
}
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,"
. " ec.pec_equipe, ec.no_equipe, ec.pec_nom_equipe,"
. " (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"
. " 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');
}
// 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';
return $arrPost;
}
$arrPostWrap = fxChronotrackApiOAuthApiPost(
'event/' . $intCtEventId . '/entry',
array('event_entry' => $tabEntries)
);
if ($arrPostWrap['state'] === 'ok') {
$arrPostWrap['body_format'] = 'event_entry';
return $arrPostWrap;
}
$arrPost['body_format'] = 'array';
$arrPost['fallback_error'] = $arrPostWrap['message'] ?? '';
return $arrPost;
}
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'] ?? '',
);
}
/**
* 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']);
$tabEligibleKeys = array();
foreach ($arrClass['transferable'] as $arrItem) {
$tabEligibleKeys[$arrItem['payload']['external_id']] = true;
}
$arrCtFetch = fxChronotrackApiSyncFetchCtEntriesIndexed($intCtEventId);
if ($arrCtFetch['state'] !== 'ok') {
return array(
'state' => 'error',
'message' => $arrCtFetch['message'],
);
}
$tabCtEntries = $arrCtFetch['entries'];
$intCtTotal = intval($arrCtFetch['total_entry_count'] ?? count($tabCtEntries));
$intCtWithoutExt = intval($arrCtFetch['without_external_id'] ?? 0);
$intAlreadyInCt = 0;
$intToCreate = 0;
foreach ($tabEligibleKeys as $strExt => $blnTrue) {
if (isset($tabCtEntries[$strExt])) {
$intAlreadyInCt++;
} else {
$intToCreate++;
}
}
$intCtOrphans = 0;
if (!empty($arrCtFetch['entities']) && is_array($arrCtFetch['entities'])) {
$tabOrphanRows = fxChronotrackApiSyncComputeCtOrphanRows($arrCtFetch['entities'], $tabEligibleKeys);
$intCtOrphans = count($tabOrphanRows);
} else {
foreach ($tabCtEntries as $strExt => $blnEntity) {
if (!isset($tabEligibleKeys[$strExt])) {
$intCtOrphans++;
}
}
}
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' => $intCtTotal,
'already_in_ct' => $intAlreadyInCt,
'will_update' => $intAlreadyInCt,
'to_create' => $intToCreate,
'will_create' => $intToCreate,
'ct_orphans' => $intCtOrphans,
'ct_without_external_id' => $intCtWithoutExt,
'to_push' => $intTransferable,
);
}
/**
* 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);
$intSkipLogged = 0;
foreach ($arrClass['blocked'] as $arrBlocked) {
if ($intSkipLogged >= 50) {
break;
}
fxChronotrackApiSyncLog(
$intEveId,
intval($arrBlocked['par_id'] ?? 0),
'push_skip',
'error',
($arrBlocked['code'] ?? 'blocked') . ' — external_id=' . ($arrBlocked['external_id'] ?? '')
. ' — ' . ($arrBlocked['message'] ?? '')
);
$intSkipLogged++;
}
if ($arrClass['blocked_count'] > $intSkipLogged) {
fxChronotrackApiSyncLog(
$intEveId,
0,
'push_skip',
'error',
'… +' . ($arrClass['blocked_count'] - $intSkipLogged) . ' exclus non détaillés'
);
}
$tabCtEntries = array();
$arrCtFetch = fxChronotrackApiSyncFetchCtEntriesIndexed($intCtEventId);
if ($arrCtFetch['state'] === 'ok') {
$tabCtEntries = $arrCtFetch['entries'];
}
$tabBatch = array();
$tabMeta = array();
foreach ($arrClass['transferable'] as $arrItem) {
$arrPayload = fxChronotrackApiSyncEnrichPayloadWithCtEntry(
$arrItem['payload'],
$tabCtEntries[$arrItem['payload']['external_id']] ?? null
);
$tabBatch[] = $arrPayload;
$tabMeta[] = array(
'par_id' => intval($arrItem['row']['par_id'] ?? 0),
'external_id' => $arrItem['payload']['external_id'],
);
}
if (count($tabBatch) === 0) {
fxChronotrackApiSyncPushJobClear($intEveId);
return array(
'state' => 'error',
'message' => 'Aucun participant transférable.'
. ($arrClass['blocked_count'] > 0 ? ' ' . $arrClass['blocked_count'] . ' exclus.' : ''),
'blocked_count' => $arrClass['blocked_count'],
'total' => 0,
);
}
$arrJob = array(
'ct_event_id' => $intCtEventId,
'batch' => $tabBatch,
'meta' => $tabMeta,
'blocked_count' => $arrClass['blocked_count'],
'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),
'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();
$intBatchSize = MSIN_API_CHRONOTRACK_SYNC_BATCH_SIZE;
$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;
}
}
for ($intBatchOff = 0; $intBatchOff < count($tabCreates); $intBatchOff += $intBatchSize) {
$tabSlice = array_slice($tabCreates, $intBatchOff, $intBatchSize);
$tabSliceMeta = array_slice($tabCreatesMeta, $intBatchOff, $intBatchSize);
$arrPost = fxChronotrackApiSyncPostEntries($intCtEventId, $tabSlice);
if ($arrPost['state'] === 'ok') {
$intOk += count($tabSlice);
$intPostOk += count($tabSlice);
continue;
}
$strErrMsg = $arrPost['message'] ?? 'Erreur POST entry';
foreach ($tabSlice as $intIdx => $arrEntry) {
$arrM = $tabSliceMeta[$intIdx];
$arrOne = fxChronotrackApiSyncPostEntries($intCtEventId, array($arrEntry));
if ($arrOne['state'] === 'ok') {
$intOk++;
$intPostOk++;
continue;
}
$intErr++;
$strOneErr = $arrOne['message'] ?? $strErrMsg;
if (count($tabErrors) < 8) {
$tabErrors[] = 'external_id=' . $arrM['external_id'] . ' — ' . $strOneErr;
}
fxChronotrackApiSyncLog($intEveId, $arrM['par_id'], 'push_entry', 'error',
'external_id=' . $arrM['external_id'] . ' — ' . $strOneErr);
}
}
if (count($tabUpdates) > 0) {
$tabUpdatesForPost = array();
foreach ($tabUpdates as $arrPayload) {
$tabUpdatesForPost[] = fxChronotrackApiSyncPayloadForPost($arrPayload);
}
for ($intBatchOff = 0; $intBatchOff < count($tabUpdatesForPost); $intBatchOff += $intBatchSize) {
$tabSlice = array_slice($tabUpdatesForPost, $intBatchOff, $intBatchSize);
$tabSliceMeta = array_slice($tabUpdatesMeta, $intBatchOff, $intBatchSize);
$tabSliceFull = array_slice($tabUpdates, $intBatchOff, $intBatchSize);
$arrPost = fxChronotrackApiSyncPostEntries($intCtEventId, $tabSlice);
if ($arrPost['state'] === 'ok') {
$intOk += count($tabSlice);
$intPostOk += count($tabSlice);
continue;
}
foreach ($tabSliceFull as $intIdx => $arrPayload) {
$arrM = $tabSliceMeta[$intIdx];
$arrOne = fxChronotrackApiSyncPushOneEntry($intCtEventId, $arrPayload);
if ($arrOne['state'] === 'ok') {
$intOk++;
if (($arrOne['method'] ?? '') === 'PUT') {
$intPutOk++;
} else {
$intPostOk++;
}
continue;
}
$intErr++;
$strOneErr = $arrOne['message'] ?? 'Erreur PUT/POST entry';
if (count($tabErrors) < 8) {
$tabErrors[] = 'external_id=' . $arrM['external_id'] . ' — ' . $strOneErr;
}
fxChronotrackApiSyncLog($intEveId, $arrM['par_id'], 'push_entry', 'error',
'external_id=' . $arrM['external_id'] . ' — ' . $strOneErr);
}
}
}
fxChronotrackApiSyncLog(
$intEveId,
0,
'push_chunk',
($intErr === 0) ? 'ok' : 'error',
'offset=' . $intOffset . ' n=' . $intChunkCount
. ' ok=' . $intOk . ' err=' . $intErr
. ' POST=' . $intPostOk . ' PUT=' . $intPutOk
);
$intNextOffset = $intOffset + $intChunkCount;
$blnDone = ($intNextOffset >= $intTotal);
if ($blnDone) {
fxChronotrackApiSyncPushJobClear($intEveId);
}
$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,
'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,
);
}