199 lines
8.9 KiB
PHP
199 lines
8.9 KiB
PHP
<?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 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 : '';
|
||
}
|
||
}
|