317 lines
11 KiB
PHP
317 lines
11 KiB
PHP
<?php
|
||
/**
|
||
* MSIN-4574 — Vérification contenu MS1 ↔ ChronoTrack.
|
||
* Compare le payload d’écriture (BuildEntryPayload) à l’entry CT indexée par external_id.
|
||
* Affiche seulement les écarts (pas les hors-éligibles).
|
||
*/
|
||
|
||
/**
|
||
* Clés payload ignorées à la comparaison (métadonnées / doublons d’écriture).
|
||
*/
|
||
function fxChronotrackApiSyncVerifySkipPayloadKeys() {
|
||
return array(
|
||
'entry_id' => true,
|
||
'event_id' => true,
|
||
'reg_choice_id' => true,
|
||
// Alias courts déjà couverts par location_* / status
|
||
'street' => true,
|
||
'city' => true,
|
||
'postal_code' => true,
|
||
'status' => true,
|
||
'country_code' => true,
|
||
'state_code' => true,
|
||
// Marqueur interne omit province (jamais comparé / envoyé)
|
||
'_skip_region_log' => true,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Alias CT pour une clé payload (premier non vide = valeur lue).
|
||
* MSIN-4574 — meta entry CT : entry_bib / athlete_* / entry_external_id
|
||
* (écriture accepte souvent bib/first_name ; la relecture liste renvoie les clés meta).
|
||
*/
|
||
function fxChronotrackApiSyncVerifyCtAliasesForKey($strKey) {
|
||
$strKey = trim((string)$strKey);
|
||
$arrMap = array(
|
||
'bib' => array('bib', 'entry_bib', 'bib_number', 'bib_num', 'team_bib'),
|
||
'first_name' => array('first_name', 'athlete_first_name', 'fname', 'entry_first_name'),
|
||
'last_name' => array('last_name', 'athlete_last_name', 'lname', 'entry_last_name'),
|
||
'sex' => array('sex', 'athlete_sex', 'gender'),
|
||
'birthdate' => array('birthdate', 'athlete_birthdate', 'dob', 'date_of_birth'),
|
||
'location_street' => array('location_street', 'street'),
|
||
'location_city' => array('location_city', 'city'),
|
||
'location_postal_code' => array('location_postal_code', 'postal_code', 'zip'),
|
||
'location_region' => array('location_region', 'state_code', 'region', 'region_code'),
|
||
'location_country' => array('location_country', 'country_code'),
|
||
'country_name' => array('country_name'),
|
||
'entry_status' => array('entry_status', 'status'),
|
||
'race_id' => array('race_id'),
|
||
'entry_check_in' => array('entry_check_in', 'check_in', 'checked_in'),
|
||
'external_id' => array(
|
||
'external_id', 'entry_external_id', 'ext_id', 'external_entry_id',
|
||
),
|
||
);
|
||
if (isset($arrMap[$strKey])) {
|
||
return $arrMap[$strKey];
|
||
}
|
||
return array($strKey);
|
||
}
|
||
|
||
/**
|
||
* Normalise une valeur pour comparaison (champ → chaîne canonique).
|
||
*/
|
||
function fxChronotrackApiSyncVerifyNormalize($strField, $mixVal) {
|
||
if ($mixVal === null) {
|
||
$str = '';
|
||
} elseif (is_bool($mixVal)) {
|
||
$str = $mixVal ? '1' : '';
|
||
} else {
|
||
$str = trim((string)$mixVal);
|
||
}
|
||
|
||
$strField = strtolower(trim((string)$strField));
|
||
|
||
if ($strField === 'bib') {
|
||
return fxChronotrackApiSyncBibNorm($str);
|
||
}
|
||
if ($strField === 'entry_status' || $strField === 'status') {
|
||
return strtoupper($str);
|
||
}
|
||
if ($strField === 'sex') {
|
||
return strtoupper($str);
|
||
}
|
||
if ($strField === 'location_country' || $strField === 'country_code'
|
||
|| $strField === 'location_region' || $strField === 'state_code'
|
||
) {
|
||
if ($str === '--' || $str === '—' || $str === '-' || strtoupper($str) === 'N/A') {
|
||
return '';
|
||
}
|
||
return strtoupper($str);
|
||
}
|
||
if ($strField === 'race_id') {
|
||
return preg_replace('/[^0-9]/', '', $str);
|
||
}
|
||
if ($strField === 'entry_check_in') {
|
||
if ($str === '' || $str === '0' || strtolower($str) === 'false' || strtolower($str) === 'no') {
|
||
return '';
|
||
}
|
||
if ($str === '1' || strtolower($str) === 'true' || strtolower($str) === 'yes') {
|
||
return '1';
|
||
}
|
||
return $str;
|
||
}
|
||
if ($strField === 'birthdate') {
|
||
$strDigits = preg_replace('/[^0-9]/', '', $str);
|
||
if (strlen($strDigits) >= 8) {
|
||
return substr($strDigits, 0, 8);
|
||
}
|
||
return strtolower($str);
|
||
}
|
||
if ($strField === 'location_postal_code' || $strField === 'postal_code') {
|
||
return strtoupper(preg_replace('/\s+/', '', $str));
|
||
}
|
||
if (strpos($strField, 'entry_note_') === 0) {
|
||
return $str;
|
||
}
|
||
|
||
return $str;
|
||
}
|
||
|
||
/**
|
||
* Lit la valeur CT pour une clé payload.
|
||
*/
|
||
function fxChronotrackApiSyncVerifyReadCtValue(array $arrEntity, $strPayloadKey) {
|
||
$strPayloadKey = trim((string)$strPayloadKey);
|
||
if ($strPayloadKey === '') {
|
||
return '';
|
||
}
|
||
|
||
if (strpos($strPayloadKey, 'entry_note_') === 0) {
|
||
$arrFound = fxChronotrackApiSyncExtractEntryNoteValue($arrEntity, $strPayloadKey);
|
||
if (!empty($arrFound['present'])) {
|
||
return fxChronotrackApiSyncVerifyNormalize($strPayloadKey, $arrFound['value']);
|
||
}
|
||
return '';
|
||
}
|
||
|
||
$strRaw = fxChronotrackApiRaceField($arrEntity, fxChronotrackApiSyncVerifyCtAliasesForKey($strPayloadKey));
|
||
return fxChronotrackApiSyncVerifyNormalize($strPayloadKey, $strRaw);
|
||
}
|
||
|
||
/**
|
||
* Compare un payload MS1 à une entity CT → liste d’écarts de champs.
|
||
*
|
||
* @return array[] { field, ms1, ct }
|
||
*/
|
||
function fxChronotrackApiSyncVerifyDiffPayloadVsCt(array $arrPayload, array $arrCtEntity) {
|
||
$arrSkip = fxChronotrackApiSyncVerifySkipPayloadKeys();
|
||
$tabDiffs = array();
|
||
|
||
foreach ($arrPayload as $strKey => $mixMs1) {
|
||
$strKey = (string)$strKey;
|
||
if ($strKey === '' || isset($arrSkip[$strKey])) {
|
||
continue;
|
||
}
|
||
|
||
$strMs1 = fxChronotrackApiSyncVerifyNormalize($strKey, $mixMs1);
|
||
// entry_check_in null = non coché → '' côté CT aussi OK
|
||
if ($strKey === 'entry_check_in' && ($mixMs1 === null || $strMs1 === '')) {
|
||
$strMs1 = '';
|
||
}
|
||
|
||
$strCt = fxChronotrackApiSyncVerifyReadCtValue($arrCtEntity, $strKey);
|
||
if ($strMs1 === $strCt) {
|
||
continue;
|
||
}
|
||
|
||
$tabDiffs[] = array(
|
||
'field' => $strKey,
|
||
'ms1' => $strMs1,
|
||
'ct' => $strCt,
|
||
);
|
||
}
|
||
|
||
return $tabDiffs;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4574 — Vérifie le contenu envoyé : éligibles MS1 vs entries CT.
|
||
* Retourne uniquement les participants avec écarts (ou absents sur CT).
|
||
*/
|
||
function fxChronotrackApiSyncVerifyContent($intEveId) {
|
||
$intEveId = intval($intEveId);
|
||
$fltStart = microtime(true);
|
||
|
||
$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, true);
|
||
|
||
$arrCt = fxChronotrackApiSyncFetchCtEntriesIndexed($intCtEventId);
|
||
if ($arrCt['state'] !== 'ok') {
|
||
return array(
|
||
'state' => 'error',
|
||
'message' => $arrCt['message'] ?? 'Erreur lecture entries CT',
|
||
);
|
||
}
|
||
|
||
$tabCtByExt = $arrCt['entries'];
|
||
$tabMismatches = array();
|
||
$tabMismatchExtIds = array();
|
||
$intOk = 0;
|
||
$intAbsent = 0;
|
||
$intDiff = 0;
|
||
$intMaxRows = 400;
|
||
|
||
foreach ($arrClass['transferable'] as $arrItem) {
|
||
if (!is_array($arrItem) || !isset($arrItem['payload']) || !is_array($arrItem['payload'])) {
|
||
continue;
|
||
}
|
||
$arrPayload = $arrItem['payload'];
|
||
$arrRow = is_array($arrItem['row'] ?? null) ? $arrItem['row'] : array();
|
||
$strExt = trim((string)($arrPayload['external_id'] ?? ''));
|
||
if ($strExt === '') {
|
||
continue;
|
||
}
|
||
|
||
$strName = trim(
|
||
trim((string)($arrPayload['first_name'] ?? ''))
|
||
. ' '
|
||
. trim((string)($arrPayload['last_name'] ?? ''))
|
||
);
|
||
if ($strName === '') {
|
||
$strName = fxChronotrackApiSyncParticipantLabel($arrRow);
|
||
}
|
||
$strBibMs1 = isset($arrPayload['bib'])
|
||
? trim((string)$arrPayload['bib'])
|
||
: fxChronotrackApiSyncExtractBib($arrRow);
|
||
|
||
if (!isset($tabCtByExt[$strExt]) || !is_array($tabCtByExt[$strExt])) {
|
||
$intAbsent++;
|
||
$tabMismatchExtIds[] = $strExt;
|
||
if (count($tabMismatches) < $intMaxRows) {
|
||
$tabMismatches[] = array(
|
||
'par_id' => intval($arrRow['par_id'] ?? 0),
|
||
'external_id' => $strExt,
|
||
'name' => $strName,
|
||
'bib_ms1' => $strBibMs1,
|
||
'kind' => 'absent',
|
||
'fields' => array(
|
||
array(
|
||
'field' => '(entry)',
|
||
'ms1' => 'présent MS1',
|
||
'ct' => 'absent CT',
|
||
),
|
||
),
|
||
);
|
||
}
|
||
continue;
|
||
}
|
||
|
||
$tabFields = fxChronotrackApiSyncVerifyDiffPayloadVsCt($arrPayload, $tabCtByExt[$strExt]);
|
||
if (count($tabFields) === 0) {
|
||
$intOk++;
|
||
continue;
|
||
}
|
||
|
||
$intDiff++;
|
||
$tabMismatchExtIds[] = $strExt;
|
||
if (count($tabMismatches) < $intMaxRows) {
|
||
$tabMismatches[] = array(
|
||
'par_id' => intval($arrRow['par_id'] ?? 0),
|
||
'external_id' => $strExt,
|
||
'name' => $strName,
|
||
'bib_ms1' => $strBibMs1,
|
||
'kind' => 'diff',
|
||
'fields' => $tabFields,
|
||
);
|
||
}
|
||
}
|
||
|
||
$intMismatchPeople = $intAbsent + $intDiff;
|
||
$intElapsed = (int)round((microtime(true) - $fltStart) * 1000);
|
||
|
||
fxChronotrackApiSyncLog(
|
||
$intEveId,
|
||
0,
|
||
'verify_content',
|
||
$intMismatchPeople === 0 ? 'ok' : 'warn',
|
||
'contenu — éligibles=' . count($arrClass['transferable'])
|
||
. ' ok=' . $intOk
|
||
. ' écarts=' . $intDiff
|
||
. ' absents_ct=' . $intAbsent
|
||
. ' (' . $intElapsed . ' ms)'
|
||
);
|
||
|
||
$strMsg = $intMismatchPeople === 0
|
||
? ('Contenu aligné — ' . $intOk . ' éligibles OK sur ChronoTrack.')
|
||
: ($intMismatchPeople . ' participant(s) avec écart'
|
||
. ($intDiff ? (' · ' . $intDiff . ' champs') : '')
|
||
. ($intAbsent ? (' · ' . $intAbsent . ' absents CT') : '')
|
||
. '.');
|
||
|
||
return array(
|
||
'state' => 'ok',
|
||
'message' => $strMsg,
|
||
'eligible_count' => count($arrClass['transferable']),
|
||
'ok_count' => $intOk,
|
||
'diff_count' => $intDiff,
|
||
'absent_count' => $intAbsent,
|
||
'mismatch_count' => $intMismatchPeople,
|
||
'mismatches' => $tabMismatches,
|
||
'mismatch_external_ids' => $tabMismatchExtIds,
|
||
'truncated' => count($tabMismatches) < $intMismatchPeople,
|
||
'ct_indexed' => count($tabCtByExt),
|
||
'ct_total_entries' => intval($arrCt['total_entry_count'] ?? count($tabCtByExt)),
|
||
'elapsed_ms' => $intElapsed,
|
||
);
|
||
}
|