Publier Globinours 1.0.0-rc.3

This commit is contained in:
Alexandre NOEL 2026-09-03 12:39:15 +02:00
commit 9a2b4068da
325 changed files with 38230 additions and 20 deletions

View file

@ -0,0 +1,199 @@
<?php
declare(strict_types=1);
final class Asm3MigrationPlanner
{
public static function plan(string $path, PDO $db): array
{
$source = Asm3Analyzer::rows($path, ['animal', 'species', 'basecolour', 'lksex']);
$species = self::lookup($source['species'] ?? [], 'ID', 'SPECIESNAME');
$colors = self::lookup($source['basecolour'] ?? [], 'ID', 'BASECOLOUR');
$sexes = self::lookup($source['lksex'] ?? [], 'ID', 'SEX');
$existing = $db
->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 dASM3.
// Un code seul nest 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 : '';
}
}