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

1023 lines
32 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 — Payload MS1 → ChronoTrack (source de vérité écriture entry).
* Mapping pays / bib / sexe / notes / BuildEntryPayload.
* Ne contient pas le push HTTP ni la résolution des conflits bib côté CT.
*/
/**
* MSIN-4574 — table ISO-3166 alpha-2 → alpha-3 (alignée sql/MSIN-inscriptions-pays-iso3.sql).
* Enrichie au runtime avec pay_iso3 en BD si la colonne existe.
*/
function fxChronotrackApiSyncIso2ToIso3Map() {
static $tabMap = null;
if ($tabMap !== null) {
return $tabMap;
}
$tabMap = array(
'AD' => 'AND',
'AE' => 'ARE',
'AF' => 'AFG',
'AG' => 'ATG',
'AI' => 'AIA',
'AL' => 'ALB',
'AM' => 'ARM',
'AO' => 'AGO',
'AQ' => 'ATA',
'AR' => 'ARG',
'AS' => 'ASM',
'AT' => 'AUT',
'AU' => 'AUS',
'AW' => 'ABW',
'AX' => 'ALA',
'AZ' => 'AZE',
'BA' => 'BIH',
'BB' => 'BRB',
'BD' => 'BGD',
'BE' => 'BEL',
'BF' => 'BFA',
'BG' => 'BGR',
'BH' => 'BHR',
'BI' => 'BDI',
'BJ' => 'BEN',
'BL' => 'BLM',
'BM' => 'BMU',
'BN' => 'BRN',
'BO' => 'BOL',
'BQ' => 'BES',
'BR' => 'BRA',
'BS' => 'BHS',
'BT' => 'BTN',
'BV' => 'BVT',
'BW' => 'BWA',
'BY' => 'BLR',
'BZ' => 'BLZ',
'CA' => 'CAN',
'CC' => 'CCK',
'CD' => 'COD',
'CF' => 'CAF',
'CG' => 'COG',
'CH' => 'CHE',
'CI' => 'CIV',
'CK' => 'COK',
'CL' => 'CHL',
'CM' => 'CMR',
'CN' => 'CHN',
'CO' => 'COL',
'CR' => 'CRI',
'CU' => 'CUB',
'CV' => 'CPV',
'CW' => 'CUW',
'CX' => 'CXR',
'CY' => 'CYP',
'CZ' => 'CZE',
'DE' => 'DEU',
'DJ' => 'DJI',
'DK' => 'DNK',
'DM' => 'DMA',
'DO' => 'DOM',
'DZ' => 'DZA',
'EC' => 'ECU',
'EE' => 'EST',
'EG' => 'EGY',
'EH' => 'ESH',
'ER' => 'ERI',
'ES' => 'ESP',
'ET' => 'ETH',
'FI' => 'FIN',
'FJ' => 'FJI',
'FK' => 'FLK',
'FM' => 'FSM',
'FO' => 'FRO',
'FR' => 'FRA',
'GA' => 'GAB',
'GB' => 'GBR',
'GD' => 'GRD',
'GE' => 'GEO',
'GF' => 'GUF',
'GG' => 'GGY',
'GH' => 'GHA',
'GI' => 'GIB',
'GL' => 'GRL',
'GM' => 'GMB',
'GN' => 'GIN',
'GP' => 'GLP',
'GQ' => 'GNQ',
'GR' => 'GRC',
'GS' => 'SGS',
'GT' => 'GTM',
'GU' => 'GUM',
'GW' => 'GNB',
'GY' => 'GUY',
'HK' => 'HKG',
'HM' => 'HMD',
'HN' => 'HND',
'HR' => 'HRV',
'HT' => 'HTI',
'HU' => 'HUN',
'ID' => 'IDN',
'IE' => 'IRL',
'IL' => 'ISR',
'IM' => 'IMN',
'IN' => 'IND',
'IO' => 'IOT',
'IQ' => 'IRQ',
'IR' => 'IRN',
'IS' => 'ISL',
'IT' => 'ITA',
'JE' => 'JEY',
'JM' => 'JAM',
'JO' => 'JOR',
'JP' => 'JPN',
'KE' => 'KEN',
'KG' => 'KGZ',
'KH' => 'KHM',
'KI' => 'KIR',
'KM' => 'COM',
'KN' => 'KNA',
'KP' => 'PRK',
'KR' => 'KOR',
'KW' => 'KWT',
'KY' => 'CYM',
'KZ' => 'KAZ',
'LA' => 'LAO',
'LB' => 'LBN',
'LC' => 'LCA',
'LI' => 'LIE',
'LK' => 'LKA',
'LR' => 'LBR',
'LS' => 'LSO',
'LT' => 'LTU',
'LU' => 'LUX',
'LV' => 'LVA',
'LY' => 'LBY',
'MA' => 'MAR',
'MC' => 'MCO',
'MD' => 'MDA',
'ME' => 'MNE',
'MF' => 'MAF',
'MG' => 'MDG',
'MH' => 'MHL',
'MK' => 'MKD',
'ML' => 'MLI',
'MM' => 'MMR',
'MN' => 'MNG',
'MO' => 'MAC',
'MP' => 'MNP',
'MQ' => 'MTQ',
'MR' => 'MRT',
'MS' => 'MSR',
'MT' => 'MLT',
'MU' => 'MUS',
'MV' => 'MDV',
'MW' => 'MWI',
'MX' => 'MEX',
'MY' => 'MYS',
'MZ' => 'MOZ',
'NA' => 'NAM',
'NC' => 'NCL',
'NE' => 'NER',
'NF' => 'NFK',
'NG' => 'NGA',
'NI' => 'NIC',
'NL' => 'NLD',
'NO' => 'NOR',
'NP' => 'NPL',
'NR' => 'NRU',
'NU' => 'NIU',
'NZ' => 'NZL',
'OM' => 'OMN',
'PA' => 'PAN',
'PE' => 'PER',
'PF' => 'PYF',
'PG' => 'PNG',
'PH' => 'PHL',
'PK' => 'PAK',
'PL' => 'POL',
'PM' => 'SPM',
'PN' => 'PCN',
'PR' => 'PRI',
'PS' => 'PSE',
'PT' => 'PRT',
'PW' => 'PLW',
'PY' => 'PRY',
'QA' => 'QAT',
'RE' => 'REU',
'RO' => 'ROU',
'RS' => 'SRB',
'RU' => 'RUS',
'RW' => 'RWA',
'SA' => 'SAU',
'SB' => 'SLB',
'SC' => 'SYC',
'SD' => 'SDN',
'SE' => 'SWE',
'SG' => 'SGP',
'SH' => 'SHN',
'SI' => 'SVN',
'SJ' => 'SJM',
'SK' => 'SVK',
'SL' => 'SLE',
'SM' => 'SMR',
'SN' => 'SEN',
'SO' => 'SOM',
'SR' => 'SUR',
'SS' => 'SSD',
'ST' => 'STP',
'SV' => 'SLV',
'SX' => 'SXM',
'SY' => 'SYR',
'SZ' => 'SWZ',
'TC' => 'TCA',
'TD' => 'TCD',
'TF' => 'ATF',
'TG' => 'TGO',
'TH' => 'THA',
'TJ' => 'TJK',
'TK' => 'TKL',
'TL' => 'TLS',
'TM' => 'TKM',
'TN' => 'TUN',
'TO' => 'TON',
'TR' => 'TUR',
'TT' => 'TTO',
'TV' => 'TUV',
'TW' => 'TWN',
'TZ' => 'TZA',
'UA' => 'UKR',
'UG' => 'UGA',
'UM' => 'UMI',
'US' => 'USA',
'UY' => 'URY',
'UZ' => 'UZB',
'VA' => 'VAT',
'VC' => 'VCT',
'VE' => 'VEN',
'VG' => 'VGB',
'VI' => 'VIR',
'VN' => 'VNM',
'VU' => 'VUT',
'WF' => 'WLF',
'WS' => 'WSM',
'YE' => 'YEM',
'YT' => 'MYT',
'ZA' => 'ZAF',
'ZM' => 'ZMB',
'ZW' => 'ZWE',
'AN' => 'ANT',
'UK' => 'GBR',
'XK' => 'XKX',
);
// Enrichir / corriger depuis BD (source de vérité après script pay_iso3)
if (function_exists('fxChronotrackApiSyncPaysHasIso3Column') && fxChronotrackApiSyncPaysHasIso3Column()) {
global $objDatabase;
if (isset($objDatabase) && is_object($objDatabase)) {
$arrRows = $objDatabase->fxGetResults(
"SELECT pay_iso, pay_iso3 FROM inscriptions_pays"
. " WHERE pay_iso IS NOT NULL AND TRIM(pay_iso) <> ''"
. " AND pay_iso3 IS NOT NULL AND TRIM(pay_iso3) <> ''"
);
if (is_array($arrRows)) {
for ($i = 1; $i <= count($arrRows); $i++) {
$str2 = strtoupper(trim((string)($arrRows[$i]['pay_iso'] ?? '')));
$str3 = strtoupper(trim((string)($arrRows[$i]['pay_iso3'] ?? '')));
if (strlen($str2) === 2 && strlen($str3) === 3) {
$tabMap[$str2] = $str3;
}
}
}
}
}
return $tabMap;
}
function fxChronotrackApiSyncIso2ToIso3($strIso2) {
$strIso2 = strtoupper(trim((string)$strIso2));
if ($strIso2 === '') {
return '';
}
if (strlen($strIso2) === 3 && ctype_alpha($strIso2)) {
return $strIso2;
}
if (strlen($strIso2) !== 2) {
return '';
}
$tabMap = fxChronotrackApiSyncIso2ToIso3Map();
return isset($tabMap[$strIso2]) ? $tabMap[$strIso2] : '';
}
/**
* MSIN-4574 — codes province Canada (pro_iso typique).
*/
function fxChronotrackApiSyncIsCanadianProvinceCode($strCode) {
static $tab = array(
'AB' => 1, 'BC' => 1, 'MB' => 1, 'NB' => 1, 'NL' => 1, 'NS' => 1,
'NT' => 1, 'NU' => 1, 'ON' => 1, 'PE' => 1, 'QC' => 1, 'SK' => 1, 'YT' => 1,
);
$strCode = strtoupper(trim((string)$strCode));
return $strCode !== '' && isset($tab[$strCode]);
}
/**
* MSIN-4574 — si province CA alors que pays ≠ CA (ex. Kathy : QC + France),
* on nenvoie pas la province (CT refuse) — sans corriger le pays MS1.
*
* @return string raison (vide = OK à envoyer)
*/
function fxChronotrackApiSyncLocationRegionOmitReason($strState, $strCountryIso2) {
$strState = strtoupper(trim((string)$strState));
$strCountryIso2 = strtoupper(trim((string)$strCountryIso2));
if ($strState === '') {
return '';
}
// Pas de pays MS1 → on envoie quand même la province (meilleur effort)
if ($strCountryIso2 === '') {
return '';
}
if (fxChronotrackApiSyncIsCanadianProvinceCode($strState) && $strCountryIso2 !== 'CA') {
return 'Province ' . $strState . ' non envoyée — pays MS1=' . $strCountryIso2
. ' (incohérent : CT naccepte pas une province canadienne hors Canada.'
. ' Corriger le pays en MS1 si besoin).';
}
return '';
}
/**
* MSIN-4574 — ISO-2 pays (pay_iso BD).
* Preuve calibrage 2026-08-11 : CT Athlete Info / export stockent location_country + COUNTRY_CODE = « CA » (pas CAN).
*/
function fxChronotrackApiSyncResolveCountryIso2(array $arrRow) {
$strIso2 = strtoupper(trim((string)($arrRow['country_iso2'] ?? '')));
if (strlen($strIso2) === 2 && ctype_alpha($strIso2)) {
return $strIso2;
}
return '';
}
/**
* MSIN-4574 — ISO-3 (pay_iso3 / map) — utile logs / exports résultats CTLIVE.
* Pour lécriture entry Athlete Info : préférer ResolveCountryIso2().
*/
function fxChronotrackApiSyncResolveCountryCode(array $arrRow) {
$strIso3 = strtoupper(trim((string)($arrRow['country_iso3'] ?? '')));
if (strlen($strIso3) === 3 && ctype_alpha($strIso3)) {
return $strIso3;
}
$strIso2 = fxChronotrackApiSyncResolveCountryIso2($arrRow);
if ($strIso2 === '') {
$strIso2 = strtoupper(trim((string)($arrRow['country_iso2'] ?? '')));
}
if (strlen($strIso2) === 3 && ctype_alpha($strIso2)) {
return $strIso2;
}
return fxChronotrackApiSyncIso2ToIso3($strIso2);
}
/**
* MSIN-4574 — libellé pays (country_name CT / export COUNTRY_NAME).
*/
function fxChronotrackApiSyncResolveCountryLabel(array $arrRow) {
$strName = trim((string)($arrRow['country_name'] ?? ''));
if ($strName !== '') {
return $strName;
}
$strIso2 = fxChronotrackApiSyncResolveCountryIso2($arrRow);
static $tabByIso2 = array(
'CA' => 'Canada',
'US' => 'United States',
'MX' => 'Mexico',
'FR' => 'France',
'GB' => 'United Kingdom',
'BE' => 'Belgium',
'CH' => 'Switzerland',
'DE' => 'Germany',
'ES' => 'Spain',
'IT' => 'Italy',
'AU' => 'Australia',
'NZ' => 'New Zealand',
'BR' => 'Brazil',
'JP' => 'Japan',
'CN' => 'China',
'NL' => 'Netherlands',
'IE' => 'Ireland',
'PT' => 'Portugal',
'PL' => 'Poland',
'AT' => 'Austria',
'SE' => 'Sweden',
'NO' => 'Norway',
'DK' => 'Denmark',
'FI' => 'Finland',
);
if ($strIso2 !== '' && isset($tabByIso2[$strIso2])) {
return $tabByIso2[$strIso2];
}
$strIso3 = fxChronotrackApiSyncResolveCountryCode($arrRow);
static $tabByIso3 = array(
'CAN' => 'Canada',
'USA' => 'United States',
'FRA' => 'France',
);
if ($strIso3 !== '' && isset($tabByIso3[$strIso3])) {
return $tabByIso3[$strIso3];
}
return $strIso2 !== '' ? $strIso2 : $strIso3;
}
function fxChronotrackApiSyncMapEntryStatus($strMs1Status, $blnCancelled = false) {
if ($blnCancelled) {
return 'WITHDRAWN';
}
$strMs1Status = strtoupper(trim((string)$strMs1Status));
switch ($strMs1Status) {
case 'DQ':
return 'DQ';
case 'DNS':
return 'DNS';
case 'NP':
return 'NP';
case 'ABANDON':
return 'DNF';
case 'UNRANKED':
return 'UNRANKED';
case 'DEFERRED':
return 'DEFERRED';
case 'CONF':
default:
return 'CONF';
}
}
/**
* MSIN-4567 — MS1 par_sexe → ChronoTrack athlete_sex (enum API /meta).
* CT : M=Male, F=Female, NB=Non-Binary, NOT SPECIFIED=Unspecified.
*/
function fxChronotrackApiSyncNormalizeSex($strSexe) {
$strSexe = strtolower(trim((string)$strSexe));
if ($strSexe === 'f') {
return 'F';
}
if ($strSexe === 'h' || $strSexe === 'm') {
return 'M';
}
if ($strSexe === 'n') {
return 'NB';
}
if ($strSexe === '' || $strSexe === 'a') {
return 'NOT SPECIFIED';
}
return '';
}
function fxChronotrackApiSyncHasValidSex($strSexe) {
return fxChronotrackApiSyncNormalizeSex($strSexe) !== '';
}
/**
* MSIN-4328 / MSIN-4444 — dossard ChronoTrack = toujours p.no_bib.
* Jamais ec.no_equipe (n° déquipe MS1) : en mode « 1 équipe = 1 dossard »
* les deux coïncident souvent au 1er assign, puis no_bib peut diverger (ex. 535 vs 507).
*/
function fxChronotrackApiSyncExtractBib(array $arrRow) {
$strBib = trim((string)($arrRow['no_bib'] ?? ''));
if ($strBib === '' || $strBib === '0') {
return '';
}
// Aligné affichage MS1 (fxShowBibNumber) : enlever préfixe epr_id-
$intEprId = intval($arrRow['epr_id'] ?? 0);
if ($intEprId > 0) {
$strBib = str_replace($intEprId . '-', '', $strBib);
}
$strBib = str_replace('-', '', $strBib);
$strBib = trim($strBib);
if ($strBib === '' || $strBib === '0') {
return '';
}
return $strBib;
}
/**
* MSIN-4328 — conflit CT « tags / bib already assigned » (swap dossards).
*/
function fxChronotrackApiSyncIsBibConflictMessage($strMessage) {
$str = strtolower((string)$strMessage);
if ($str === '') {
return false;
}
if (strpos($str, 'already assigned') !== false) {
return true;
}
if (strpos($str, 'tags are already') !== false) {
return true;
}
if (strpos($str, 'bib') !== false && strpos($str, 'already') !== false) {
return true;
}
return false;
}
function fxChronotrackApiSyncPayloadWithoutBib(array $arrPayload) {
$arrOut = $arrPayload;
unset($arrOut['bib']);
return $arrOut;
}
/**
* MSIN-4328 — libérer le dossard CT (swap) : bib vide.
*/
function fxChronotrackApiSyncPayloadClearBib(array $arrPayload) {
$arrOut = $arrPayload;
$arrOut['bib'] = '';
return $arrOut;
}
/**
* MSIN-4574 — comparer dossards CT/MS1 (2342 == 02342).
*/
function fxChronotrackApiSyncBibNorm($strBib) {
$str = trim((string)$strBib);
if ($str === '') {
return '';
}
if (ctype_digit($str)) {
return (string)intval($str);
}
return strtolower($str);
}
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 dexclusion.
*/
function fxChronotrackApiSyncEpreuveLabel(array $arrRow) {
$strNom = trim((string)($arrRow['epr_nom'] ?? ''));
$strType = trim((string)($arrRow['epr_type'] ?? ''));
if ($strType !== '' && $strNom !== '') {
return $strType . ' — ' . $strNom;
}
if ($strNom !== '') {
return $strNom;
}
return $strType;
}
function fxChronotrackApiSyncIsTeamRow(array $arrRow) {
return intval($arrRow['pec_equipe'] ?? 0) === 1 || intval($arrRow['par_equipe'] ?? 0) === 1;
}
/**
* MSIN-4444 — nom déquipe MS1 (commande, sinon participant).
*/
function fxChronotrackApiSyncTeamName(array $arrRow) {
$strName = trim((string)($arrRow['pec_nom_equipe'] ?? ''));
if ($strName === '') {
$strName = trim((string)($arrRow['par_nom_equipe'] ?? ''));
}
return $strName;
}
/**
* MSIN-4444 — clé déquipe = pec_id (commande).
*/
function fxChronotrackApiSyncTeamGroupKey(array $arrRow) {
$intPecId = intval($arrRow['pec_id'] ?? 0);
if ($intPecId > 0) {
return 'pec:' . $intPecId;
}
return 'par:' . intval($arrRow['par_id'] ?? 0);
}
/**
* MSIN-4444 — 1 entrée CT par équipe : capitaine (rol_id=1), sinon 1er membre.
*/
function fxChronotrackApiSyncPickTeamRepresentative(array $tabMembers) {
$arrFirst = null;
foreach ($tabMembers as $arrRow) {
if (!is_array($arrRow)) {
continue;
}
if ($arrFirst === null) {
$arrFirst = $arrRow;
}
if (intval($arrRow['rol_id'] ?? 0) === 1) {
return $arrRow;
}
}
return $arrFirst;
}
function fxChronotrackApiSyncFormatRoleLabel($intRolId) {
switch (intval($intRolId)) {
case 1:
return 'Capitaine';
case 2:
return 'Membre';
default:
return $intRolId > 0 ? ('Rôle ' . intval($intRolId)) : '';
}
}
/**
* MSIN-4574 — code pays présent dans un payload CT (location_country prioritaire).
* Calibrage : CT stocke ISO-2 dans location_country (ex. CA).
*/
function fxChronotrackApiSyncCountryFromPayload(array $arrPayload) {
$str = trim((string)($arrPayload['location_country'] ?? ''));
if ($str !== '') {
return $str;
}
return trim((string)($arrPayload['country_code'] ?? ''));
}
/**
* MSIN-4574 — couverture pays sur les transférables (warning, pas blocage).
*
* @param array $tabTransferable items classify (row + payload optionnel)
* @return array{total:int,with_country:int,without_country:int,no_pay_id:int}
*/
function fxChronotrackApiSyncCountryCoverageFromTransferable(array $tabTransferable) {
$intTotal = count($tabTransferable);
$intWith = 0;
$intNoPay = 0;
foreach ($tabTransferable as $arrItem) {
if (!is_array($arrItem)) {
continue;
}
$arrRow = is_array($arrItem['row'] ?? null) ? $arrItem['row'] : array();
if (intval($arrRow['pay_id'] ?? 0) <= 0) {
$intNoPay++;
}
$strCode = '';
if (is_array($arrItem['payload'] ?? null)) {
$strCode = fxChronotrackApiSyncCountryFromPayload($arrItem['payload']);
}
if ($strCode === '') {
$strCode = fxChronotrackApiSyncResolveCountryCode($arrRow);
}
if ($strCode !== '') {
$intWith++;
}
}
return array(
'total' => $intTotal,
'with_country' => $intWith,
'without_country' => max(0, $intTotal - $intWith),
'no_pay_id' => $intNoPay,
);
}
/**
* MSIN-4574 — fragment log « pays=CAN » / « pays=— ».
*/
function fxChronotrackApiSyncLogCountryFragment(array $arrPayload = array(), array $arrRow = array()) {
$str = fxChronotrackApiSyncCountryFromPayload($arrPayload);
if ($str === '' && count($arrRow) > 0) {
$str = fxChronotrackApiSyncResolveCountryCode($arrRow);
}
return 'pays=' . ($str !== '' ? $str : '—');
}
function fxChronotrackApiSyncParticipantSummary(array $arrRow) {
return array(
'par_id' => intval($arrRow['par_id'] ?? 0),
'external_id' => (string)intval($arrRow['par_id_original'] ?? 0),
'name' => fxChronotrackApiSyncParticipantLabel($arrRow),
'no_bib' => fxChronotrackApiSyncExtractBib($arrRow),
'par_sexe' => trim((string)($arrRow['par_sexe'] ?? '')),
'pec_id' => intval($arrRow['pec_id'] ?? 0),
'is_team' => fxChronotrackApiSyncIsTeamRow($arrRow),
'team_name' => fxChronotrackApiSyncTeamName($arrRow),
'no_equipe' => trim((string)($arrRow['no_equipe'] ?? '')),
'role_label' => fxChronotrackApiSyncFormatRoleLabel($arrRow['rol_id'] ?? 0),
);
}
function 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 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;
}
/**
* MSIN-4461 — clé CT entry_note_{label} (support ChronoTrack).
*/
function fxChronotrackApiSyncEntryNoteKey($strLabel, $intQueId) {
$strLabel = trim((string)$strLabel);
$strLabel = preg_replace('/\s+/u', '_', $strLabel);
$strLabel = preg_replace('/["\\\\\x00-\x1F]+/u', '', $strLabel);
$strLabel = trim($strLabel, '_');
if ($strLabel === '') {
$strLabel = 'q' . intval($intQueId);
}
return 'entry_note_' . $strLabel;
}
/**
* MSIN-4461 — index des réponses questions dun événement (1 requête / eve_id).
*
* Périmètre actuel : TOUTES les questions actives de lévénement (pas seulement que_rapport).
* Si le payload devient trop lourd (beaucoup de questions / événements volumineux),
* restreindre ici (ex. que_rapport = 1, liste blanche, ou exclusion des validations techniques).
*
* @return array<int, array<int, array{par_id:int,que_id:int,label:string,value:string}>>
* indexé par pec_id
*/
function fxChronotrackApiSyncLoadMs1QuestionsIndex($intEveId) {
global $objDatabase;
static $tabCache = array();
$intEveId = intval($intEveId);
if ($intEveId <= 0) {
return array();
}
if (isset($tabCache[$intEveId])) {
return $tabCache[$intEveId];
}
$sql = "SELECT rq.pec_id, rq.par_id, rq.que_id,"
. " TRIM(COALESCE("
. " NULLIF(iq.que_rapport_label_fr, ''),"
. " NULLIF(rq.que_question_fr, ''),"
. " NULLIF(iq.que_question_fr, ''),"
. " '')) AS note_label,"
. " TRIM(COALESCE("
. " NULLIF(rq.que_choix_fr, ''),"
. " NULLIF(rq.pqu_note, ''),"
. " NULLIF(rq.que_choix_en, ''),"
. " '')) AS note_value"
. " FROM resultats_questions rq"
. " INNER JOIN inscriptions_questions iq ON iq.que_id = rq.que_id AND iq.eve_id = " . $intEveId
. " WHERE rq.que_actif = 1"
. " ORDER BY rq.pec_id, rq.que_id";
$tabRows = $objDatabase->fxGetResults($sql);
$tabIndex = array();
if (is_array($tabRows)) {
for ($i = 1; $i <= count($tabRows); $i++) {
$arrQ = $tabRows[$i];
$intPecId = intval($arrQ['pec_id'] ?? 0);
$strValue = trim((string)($arrQ['note_value'] ?? ''));
if ($intPecId <= 0 || $strValue === '') {
continue;
}
if (!isset($tabIndex[$intPecId])) {
$tabIndex[$intPecId] = array();
}
$tabIndex[$intPecId][] = array(
'par_id' => intval($arrQ['par_id'] ?? 0),
'que_id' => intval($arrQ['que_id'] ?? 0),
'label' => trim((string)($arrQ['note_label'] ?? '')),
'value' => $strValue,
);
}
}
$tabCache[$intEveId] = $tabIndex;
return $tabIndex;
}
/**
* MSIN-4461 — map entry_note_* pour un participant (réponses épreuve + participant).
* resultats_questions.par_id = par_id_original (legacy) ; par_id 0 = question niveau épreuve.
*/
function fxChronotrackApiSyncEntryNotesForParticipant(array $arrRow) {
$intEveId = intval($arrRow['eve_id'] ?? 0);
$intPecId = intval($arrRow['pec_id'] ?? 0);
if ($intEveId <= 0 || $intPecId <= 0) {
return array();
}
$tabIndex = fxChronotrackApiSyncLoadMs1QuestionsIndex($intEveId);
if (!isset($tabIndex[$intPecId]) || !is_array($tabIndex[$intPecId])) {
return array();
}
$intParOrig = intval($arrRow['par_id_original'] ?? 0);
$intParId = intval($arrRow['par_id'] ?? 0);
$tabNotes = array();
$tabUsedKeys = array();
foreach ($tabIndex[$intPecId] as $arrQ) {
$intQPar = intval($arrQ['par_id'] ?? 0);
// Question dépreuve (par_id vide) ou réponse de ce participant
if ($intQPar > 0 && $intQPar !== $intParOrig && $intQPar !== $intParId) {
continue;
}
$intQueId = intval($arrQ['que_id'] ?? 0);
$strKey = fxChronotrackApiSyncEntryNoteKey($arrQ['label'] ?? '', $intQueId);
if (isset($tabUsedKeys[$strKey])) {
$strKey = 'entry_note_q' . $intQueId;
}
$tabUsedKeys[$strKey] = true;
$tabNotes[$strKey] = (string)$arrQ['value'];
}
return $tabNotes;
}
function fxChronotrackApiSyncResolveRegChoiceIdForRace($intCtEventId, $intCtRaceId) {
static $tabCache = array();
$strKey = intval($intCtEventId) . ':' . intval($intCtRaceId);
if (array_key_exists($strKey, $tabCache)) {
return $tabCache[$strKey];
}
$tabCache[$strKey] = 0;
$arrRaces = fxChronotrackApiFetchRacesForEvent($intCtEventId);
if (($arrRaces['state'] ?? '') !== 'ok' || empty($arrRaces['races'])) {
return 0;
}
foreach ($arrRaces['races'] as $arrRace) {
if (intval($arrRace['ct_race_id'] ?? 0) !== intval($intCtRaceId)) {
continue;
}
$arrRaw = isset($arrRace['raw']) && is_array($arrRace['raw']) ? $arrRace['raw'] : array();
$arrChoices = fxChronotrackApiCollectRegChoiceEntities($arrRaw);
if (count($arrChoices) === 0 && isset($arrRaw['reg_choice_id'])) {
$tabCache[$strKey] = intval($arrRaw['reg_choice_id']);
return $tabCache[$strKey];
}
foreach ($arrChoices as $arrChoice) {
if (!is_array($arrChoice) || empty($arrChoice['reg_choice_id'])) {
continue;
}
$tabCache[$strKey] = intval($arrChoice['reg_choice_id']);
return $tabCache[$strKey];
}
}
return 0;
}
function fxChronotrackApiSyncBuildEntryPayload(array $arrRow, array $arrRaceMap, $intCtEventId = 0) {
$intEprId = intval($arrRow['epr_id'] ?? 0);
$intCtRaceId = intval($arrRaceMap[$intEprId]['ct_race_id'] ?? 0);
if ($intCtRaceId <= 0) {
return null;
}
$intExternalId = intval($arrRow['par_id_original'] ?? 0);
if ($intExternalId <= 0) {
return null;
}
// MSIN-4574 — pay_iso (ISO-2) = source pour location_country CT
$strCountry = fxChronotrackApiSyncResolveCountryIso2($arrRow);
$strState = strtoupper(trim((string)($arrRow['state_iso'] ?? '')));
$strSex = fxChronotrackApiSyncNormalizeSex($arrRow['par_sexe'] ?? '');
if ($strSex === '') {
return null;
}
$strBib = fxChronotrackApiSyncExtractBib($arrRow);
if ($strBib === '') {
return null;
}
$arrEntry = array(
'external_id' => (string)$intExternalId,
'race_id' => $intCtRaceId,
'first_name' => trim((string)($arrRow['par_prenom'] ?? '')),
'last_name' => trim((string)($arrRow['par_nom'] ?? '')),
'sex' => $strSex,
);
// MSIN-4444 — inscription équipe : prénom = EQ., nom = nom d'équipe
if (fxChronotrackApiSyncIsTeamRow($arrRow)) {
$strTeamName = fxChronotrackApiSyncTeamName($arrRow);
if ($strTeamName === '') {
return null;
}
$arrEntry['first_name'] = 'EQ.';
$arrEntry['last_name'] = $strTeamName;
}
if ($intCtEventId > 0) {
$arrEntry['event_id'] = $intCtEventId;
}
$intRegChoiceId = fxChronotrackApiSyncResolveRegChoiceIdForRace($intCtEventId, $intCtRaceId);
if ($intRegChoiceId > 0) {
$arrEntry['reg_choice_id'] = $intRegChoiceId;
}
$strDob = fxChronotrackApiSyncNormalizeBirthdate($arrRow['par_naissance'] ?? '');
if ($strDob !== '') {
$arrEntry['birthdate'] = $strDob;
}
// MSIN-4574 — meta CT entry : location_* (pas street/city/country_code du format résultats).
// On envoie les clés meta + alias courts (CT accepte souvent les deux pour identité).
$strStreet = trim((string)($arrRow['par_adresse'] ?? ''));
if ($strStreet !== '') {
$arrEntry['location_street'] = $strStreet;
$arrEntry['street'] = $strStreet;
}
$strCity = trim((string)($arrRow['par_ville'] ?? ''));
if ($strCity !== '') {
$arrEntry['location_city'] = $strCity;
$arrEntry['city'] = $strCity;
}
$strPostal = trim((string)($arrRow['par_codepostal'] ?? ''));
if ($strPostal !== '') {
$arrEntry['location_postal_code'] = $strPostal;
$arrEntry['postal_code'] = $strPostal;
}
if ($strState !== '') {
// MSIN-4574 — ne pas envoyer une province CA/US si le pays MS1 ne match pas
// (ex. QC + France → CT refuse ; pas de « réparation » auto du pays).
$strOmitRegion = fxChronotrackApiSyncLocationRegionOmitReason($strState, $strCountry);
if ($strOmitRegion === '') {
// Nenvoyer QUE location_region (pas state_code) :
// state_code en upsert bulk a pu finir dans location_country côté CT (QC dans le pays).
$arrEntry['location_region'] = $strState;
} else {
// Marqueur interne → log push_prepare, retiré avant HTTP / vérif
$arrEntry['_skip_region_log'] = $strOmitRegion;
}
}
// MSIN-4574 — pays / province (calibrage sonde lecture + export confirmed entries 2026-08-11) :
// location_country = ISO-2 (CA), location_region = pro_iso (QC), country_name = libellé.
// Pas de country_code en écriture entry (même risque de collision que state_code).
$strCountryName = fxChronotrackApiSyncResolveCountryLabel($arrRow);
if ($strCountry !== '') {
$arrEntry['location_country'] = $strCountry;
}
if ($strCountryName !== '') {
$arrEntry['country_name'] = $strCountryName;
}
$arrEntry['entry_status'] = fxChronotrackApiSyncMapEntryStatus(
$arrRow['par_statut_course'] ?? 'CONF',
intval($arrRow['is_cancelled'] ?? 0) === 1
);
$arrEntry['status'] = $arrEntry['entry_status'];
// MSIN-4328 / MSIN-4574 — CT refuse dassigner un dossard si lentry nest pas Confirmée.
// Preuve log : « not confirmed and cannot be assigned a bib » (ex. DQ).
// On nenvoie le bib que si statut CONF.
if ($strBib !== '' && $arrEntry['entry_status'] === 'CONF') {
$arrEntry['bib'] = $strBib;
}
// MSIN-4328 — Dossard récupéré (MS1) → Check In Status CT (entry_check_in = "1" / null)
$arrEntry['entry_check_in'] = (intval($arrRow['no_bib_remis'] ?? 0) === 1) ? '1' : null;
// MSIN-4461 — questions MS1 → notes CT (entry_note_{label}).
// Toutes les réponses actives pour linstant ; filtrer plus tard si trop lourd (voir LoadMs1QuestionsIndex).
foreach (fxChronotrackApiSyncEntryNotesForParticipant($arrRow) as $strNoteKey => $strNoteValue) {
$arrEntry[$strNoteKey] = $strNoteValue;
}
return $arrEntry;
}
function fxChronotrackApiSyncPayloadForPost(array $arrPayload) {
$arrOut = $arrPayload;
unset($arrOut['entry_id'], $arrOut['_skip_region_log']);
return $arrOut;
}
function fxChronotrackApiSyncPayloadForPut(array $arrPayload) {
$arrOut = $arrPayload;
unset($arrOut['entry_id'], $arrOut['_skip_region_log']);
return $arrOut;
}
function fxChronotrackApiSyncEnrichPayloadWithCtEntry(array $arrPayload, $arrCtEntity) {
if (!is_array($arrCtEntity)) {
return $arrPayload;
}
$strEntryId = fxChronotrackApiEntityId($arrCtEntity);
if ($strEntryId !== '') {
$arrPayload['entry_id'] = $strEntryId;
}
return $arrPayload;
}