Globinours/app/Services/Asm3MigrationPlanner.php

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