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

317 lines
11 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-4574 — Vérification contenu MS1 ↔ ChronoTrack.
* Compare le payload décriture (BuildEntryPayload) à lentry 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,
);
}