query('SELECT id,name,internal_code,chip_id,birth_date FROM animals') ->fetchAll(PDO::FETCH_ASSOC); $byChip = []; $byCode = []; $byIdentity = []; $byName = []; foreach ($existing as $animal) { $chip = self::identifier($animal['chip_id'] ?? ''); if ($chip !== '') { $byChip[$chip][] = $animal; } $code = self::text($animal['internal_code'] ?? ''); if ($code !== '') { $byCode[$code][] = $animal; } $identity = self::identity($animal['name'] ?? '', $animal['birth_date'] ?? ''); if ($identity !== '') { $byIdentity[$identity][] = $animal; } $name = self::text($animal['name'] ?? ''); if ($name !== '') { $byName[$name][] = $animal; } } $items = []; $summary = ['create' => 0, 'match' => 0, 'review' => 0]; foreach ($source['animal'] ?? [] as $row) { $mapped = self::mapAnimal($row, $species, $colors, $sexes); $candidates = []; $reasons = []; $chip = self::identifier($mapped['chip_id']); if ($chip !== '' && !empty($byChip[$chip])) { foreach ($byChip[$chip] as $a) { $candidates[(int) $a['id']] = $a; } $reasons[] = 'même identification'; } // Les codes refuge peuvent être réutilisés après une remise à zéro d’ASM3. // Un code seul n’est donc jamais une preuve suffisante entre deux sauvegardes historiques. $code = self::text($mapped['internal_code']); $identity = self::identity($mapped['name'], $mapped['birth_date']); if ($identity !== '' && !empty($byIdentity[$identity])) { foreach ($byIdentity[$identity] as $a) { $candidates[(int) $a['id']] = $a; } $reasons[] = 'même nom et naissance'; } $strongMatch = count($candidates) > 0; if (!$strongMatch) { $name = self::text($mapped['name']); if ($name !== '' && !empty($byName[$name])) { $weakByName = $byName[$name]; $knownDifferentBirths = $mapped['birth_date'] !== '' && $mapped['birth_date'] !== null; foreach ($weakByName as $a) { $knownDifferentBirths = $knownDifferentBirths && !empty($a['birth_date']) && $a['birth_date'] !== $mapped['birth_date']; } if (!$knownDifferentBirths) { foreach ($weakByName as $a) { $candidates[(int) $a['id']] = $a; } $reasons[] = 'même nom uniquement'; } } } // Un code isolé et réutilisé ne doit ni fusionner, ni bloquer la création. // Asm3AnimalImporter suffixera le code technique si nécessaire. if (!$strongMatch && count($candidates) === 0) { $action = 'create'; } elseif ($strongMatch && count($candidates) === 1) { $action = 'match'; } else { $action = 'review'; } $summary[$action]++; $items[] = [ 'asm3_id' => (int) ($row['ID'] ?? 0), 'action' => $action, 'match_reasons' => array_values(array_unique($reasons)), 'globinours_candidates' => array_values( array_map( static fn($a) => ['id' => (int) $a['id'], 'name' => $a['name'], 'code' => $a['internal_code']], $candidates, ), ), 'mapped' => $mapped, ]; } return [ 'source' => basename($path), 'generated_at' => date(DATE_ATOM), 'read_only' => true, 'summary' => $summary, 'items' => $items, 'lookups' => ['species' => $species, 'colors' => $colors, 'sexes' => $sexes], ]; } private static function mapAnimal(array $r, array $species, array $colors, array $sexes): array { $date = static fn($v) => $v && preg_match('/^\d{4}-\d{2}-\d{2}/', (string) $v, $m) ? $m[0] : null; $yes = static fn($v) => (string) $v === '1'; $tri = static fn($v) => match ((string) $v) { '0' => 'no', '1' => 'yes', default => 'unknown', }; $chip = trim((string) ($r['IDENTICHIPNUMBER'] ?? '')); $tattoo = trim((string) ($r['TATTOONUMBER'] ?? '')); $identifier = $chip !== '' ? $chip : $tattoo; $deceased = $date($r['DECEASEDDATE'] ?? null) !== null || $yes($r['PUTTOSLEEP'] ?? 0); $availability = $yes($r['ISNOTAVAILABLEFORADOPTION'] ?? 0) ? 'not_available' : ($yes($r['ADOPTABLE'] ?? 0) ? 'available' : 'unknown'); return [ 'name' => trim((string) ($r['ANIMALNAME'] ?? '')), 'internal_code' => trim((string) ($r['SHELTERCODE'] ?? '' ?: $r['SHORTCODE'] ?? '')), 'species' => self::species($species[(string) ($r['SPECIESID'] ?? '')] ?? ''), 'sex' => match (mb_strtolower((string) ($sexes[(string) ($r['SEX'] ?? '')] ?? ''))) { 'mâle' => 'M', 'femelle' => 'F', default => 'U', }, 'birth_date' => $date($r['DATEOFBIRTH'] ?? null), 'birth_is_estimated' => $yes($r['ESTIMATEDDOB'] ?? 0) ? 1 : 0, 'breed' => trim((string) ($r['BREEDNAME'] ?? '')), 'color' => $colors[(string) ($r['BASECOLOURID'] ?? '')] ?? null, 'notes' => trim((string) ($r['ANIMALCOMMENTS'] ?? '')), 'intake_date' => $date($r['MOSTRECENTENTRYDATE'] ?? null ?: $r['DATEBROUGHTIN'] ?? null), 'status' => $deceased ? 'decede' : ($yes($r['ISQUARANTINE'] ?? 0) ? 'quarantaine' : 'refuge'), 'archived' => $yes($r['ARCHIVED'] ?? 0), 'deceased_date' => $date($r['DECEASEDDATE'] ?? null), 'identification_type' => $chip !== '' ? 'microchip' : ($tattoo !== '' ? 'tattoo' : 'unknown'), 'chip_id' => $identifier, 'identification_date' => $date($chip !== '' ? $r['IDENTICHIPDATE'] ?? null : $r['TATTOODATE'] ?? null), 'identification_registration_status' => match ((string) ($r['IDENTICHIPSTATUS'] ?? '')) { '1' => 'registered', '0' => 'pending', default => 'unknown', }, 'sterilization_status' => $yes($r['NEUTERED'] ?? 0) ? 'yes' : 'unknown', 'sterilization_date' => $date($r['NEUTEREDDATE'] ?? null), 'adoption_availability' => $availability, 'adoption_available_from' => $date($r['HOLDUNTILDATE'] ?? null), 'adoption_unavailability_reason' => trim((string) ($r['REASONNO'] ?? '')), 'compatibility_dogs' => $tri($r['ISGOODWITHDOGS'] ?? null), 'compatibility_cats' => $tri($r['ISGOODWITHCATS'] ?? null), 'compatibility_children' => $tri($r['ISGOODWITHCHILDREN'] ?? null), 'house_trained' => $tri($r['ISHOUSETRAINED'] ?? null), ]; } private static function lookup(array $rows, string $key, string $value): array { $out = []; foreach ($rows as $row) { $out[(string) $row[$key]] = (string) $row[$value]; } return $out; } private static function species(string $value): string { return match (mb_strtolower($value)) { 'chat' => 'chat', 'chien' => 'chien', default => mb_strtolower($value) ?: 'inconnu', }; } private static function identifier(mixed $value): string { return strtoupper((string) preg_replace('/[^a-zA-Z0-9]/', '', (string) $value)); } private static function text(mixed $value): string { return mb_strtolower(trim((string) $value)); } private static function identity(mixed $name, mixed $birth): string { $name = self::text($name); $birth = trim((string) $birth); return $name !== '' && $birth !== '' ? $name . '|' . $birth : ''; } }