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,271 @@
<?php
declare(strict_types=1);
final class Asm3MovementImporter
{
public static function run(string $path, PDO $db, bool $apply = false, bool $createBackup = true): array
{
$hash = hash_file('sha256', $path);
if ($hash === false) {
throw new RuntimeException(t('asm3.hash_unavailable'));
}
$rows = Asm3Analyzer::rows($path, ['adoption'])['adoption'] ?? [];
$events = [];
$unsupported = 0;
foreach ($rows as $row) {
$source = (int) ($row['ID'] ?? 0);
$type = (int) ($row['MOVEMENTTYPE'] ?? 0);
$movementDate = self::date($row['MOVEMENTDATE'] ?? null);
$returnDate = self::date($row['RETURNDATE'] ?? null);
$reservationDate = self::date($row['RESERVATIONDATE'] ?? null);
$cancelDate = self::date($row['RESERVATIONCANCELLEDDATE'] ?? null);
if ($reservationDate) {
$events[] = self::event($row, $source, 'reservation', $reservationDate, 'Réservation ASM3');
}
if ($cancelDate) {
$events[] = self::event($row, $source, 'cancellation', $cancelDate, 'Annulation de réservation ASM3');
}
if ($movementDate && $type === 1) {
$events[] = self::event($row, $source, 'adoption', $movementDate, 'Adoption définitive ASM3');
} elseif ($movementDate && in_array($type, [2, 12], true)) {
$events[] = self::event(
$row,
$source,
'foster',
$movementDate,
$type === 12 || (string) ($row['ISPERMANENTFOSTER'] ?? 0) === '1'
? 'FA permanente ASM3'
: 'Famille daccueil ASM3',
);
} elseif ($movementDate && !in_array($type, [0, 9, 10], true)) {
$unsupported++;
}
if ($returnDate && in_array($type, [1, 2, 12], true)) {
$events[] = self::event(
$row,
$source,
'return',
$returnDate,
trim((string) ($row['REASONFORRETURN'] ?? '')) ?: 'Retour enregistré dans ASM3',
);
}
}
$links = self::links($db, $hash);
$summary = [
'events' => count($events),
'adoptions' => 0,
'fosters' => 0,
'returns' => 0,
'reservations' => 0,
'cancellations' => 0,
'unresolved_animals' => 0,
'unresolved_contacts' => 0,
'unsupported_movements' => $unsupported,
'would_import' => 0,
'created' => 0,
'already_imported' => 0,
];
foreach ($events as $event) {
$summary[
$event['type'] === 'adoption'
? 'adoptions'
: ($event['type'] === 'foster'
? 'fosters'
: ($event['type'] === 'return'
? 'returns'
: ($event['type'] === 'reservation'
? 'reservations'
: 'cancellations')))
]++;
if (!isset($links['animal'][$event['animal_source_id']])) {
$summary['unresolved_animals']++;
continue;
}
if ($event['contact_source_id'] > 0 && !isset($links['contact'][$event['contact_source_id']])) {
$summary['unresolved_contacts']++;
}
$summary['would_import']++;
}
if (!$apply) {
return ['mode' => 'dry-run', 'read_only' => true, 'summary' => $summary];
}
if ($createBackup) {
$backup = BackupService::create('pre-asm3-import');
$backupName = $backup['name'] ?? null;
} else {
$backupName = null;
}
$ownsTransaction = !$db->inTransaction();
if ($ownsTransaction) {
$db->beginTransaction();
}
try {
$db->prepare(
"INSERT INTO asm3_import_runs(source_name,source_sha256,status,summary_json,backup_name) VALUES(:name,:hash,'completed','{}',:backup)",
)->execute([':name' => basename($path) . ' · mouvements', ':hash' => $hash, ':backup' => $backupName]);
$runId = (int) $db->lastInsertId();
$affected = [];
foreach ($events as $event) {
$animalId = $links['animal'][$event['animal_source_id']] ?? 0;
if (!$animalId) {
continue;
}
$affected[$animalId] = true;
$entity = 'placement-' . $event['type'];
$exists = $db->prepare(
'SELECT target_id FROM asm3_import_links WHERE source_sha256=:hash AND entity_type=:entity AND source_id=:source',
);
$exists->execute([':hash' => $hash, ':entity' => $entity, ':source' => $event['source_id']]);
if ($exists->fetchColumn()) {
$summary['already_imported']++;
continue;
}
$contactId = $links['contact'][$event['contact_source_id']] ?? null;
$adoptionId = null;
if ($event['type'] === 'adoption') {
$contact = self::contact($db, $contactId);
$db->prepare(
'INSERT INTO adoptions(animal_id,adopter_name,adopter_phone,adopter_email,adopter_address,adopter_postal_code,adopter_city,adopter_country,adoption_date,notes,adopter_contact_id) VALUES(:animal,:name,:phone,:email,:address,:postal,:city,:country,:date,:notes,:contact)',
)->execute([
':animal' => $animalId,
':name' => $contact['name'] ?? 'Adoptant ASM3 non retrouvé',
':phone' => $contact['phone'] ?? null,
':email' => $contact['email'] ?? null,
':address' => $contact['address'] ?? null,
':postal' => $contact['postal_code'] ?? null,
':city' => $contact['city'] ?? null,
':country' => $contact['country'] ?? 'France',
':date' => $event['date'],
':notes' => $event['notes'] ?: null,
':contact' => $contactId,
]);
$adoptionId = (int) $db->lastInsertId();
}
$db->prepare(
'INSERT INTO animal_placements(animal_id,event_type,event_date,contact_id,reason,notes,adoption_id) VALUES(:animal,:type,:date,:contact,:reason,:notes,:adoption)',
)->execute([
':animal' => $animalId,
':type' => $event['type'],
':date' => $event['date'],
':contact' => $contactId,
':reason' => $event['reason'],
':notes' => $event['notes'] ?: null,
':adoption' => $adoptionId,
]);
$placementId = (int) $db->lastInsertId();
$db->prepare(
'INSERT INTO asm3_import_links(source_sha256,entity_type,source_id,target_id,import_run_id) VALUES(:hash,:entity,:source,:target,:run)',
)->execute([
':hash' => $hash,
':entity' => $entity,
':source' => $event['source_id'],
':target' => $placementId,
':run' => $runId,
]);
$summary['created']++;
}
self::syncAnimalStates($db, array_keys($affected));
$db->prepare('UPDATE asm3_import_runs SET summary_json=:summary WHERE id=:id')->execute([
':summary' => json_encode($summary, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR),
':id' => $runId,
]);
if ($ownsTransaction) {
$db->commit();
}
} catch (Throwable $e) {
if ($ownsTransaction && $db->inTransaction()) {
$db->rollBack();
}
throw $e;
}
return ['mode' => 'apply', 'read_only' => false, 'backup' => $backupName, 'summary' => $summary];
}
private static function event(array $row, int $source, string $type, string $date, string $reason): array
{
return [
'source_id' => $source,
'type' => $type,
'date' => $date,
'animal_source_id' => (int) ($row['ANIMALID'] ?? 0),
'contact_source_id' => (int) ($row['OWNERID'] ?? 0),
'reason' => $reason,
'notes' => trim((string) ($row['COMMENTS'] ?? '')),
];
}
private static function date(mixed $value): ?string
{
return $value && preg_match('/^\d{4}-\d{2}-\d{2}/', (string) $value, $m) ? $m[0] : null;
}
private static function links(PDO $db, string $hash): array
{
$out = ['animal' => [], 'contact' => []];
$s = $db->prepare(
"SELECT entity_type,source_id,target_id FROM asm3_import_links WHERE source_sha256=:hash AND entity_type IN ('animal','contact')",
);
$s->execute([':hash' => $hash]);
foreach ($s as $r) {
$out[$r['entity_type']][(int) $r['source_id']] = (int) $r['target_id'];
}
return $out;
}
private static function contact(PDO $db, ?int $id): ?array
{
if (!$id) {
return null;
}
$s = $db->prepare('SELECT * FROM directory_contacts WHERE id=:id');
$s->execute([':id' => $id]);
return $s->fetch(PDO::FETCH_ASSOC) ?: null;
}
private static function syncAnimalStates(PDO $db, array $animalIds): void
{
$latest = $db->prepare(
'SELECT ap.*,dc.name contact_name,dc.address,dc.postal_code,dc.city FROM animal_placements ap LEFT JOIN directory_contacts dc ON dc.id=ap.contact_id WHERE ap.animal_id=:animal ORDER BY date(ap.event_date) DESC,ap.id DESC LIMIT 1',
);
foreach ($animalIds as $animalId) {
$death = $db->prepare('SELECT 1 FROM animal_deaths WHERE animal_id=?');
$death->execute([$animalId]);
if ($death->fetchColumn()) {
continue;
}
$latest->execute([':animal' => $animalId]);
$event = $latest->fetch(PDO::FETCH_ASSOC);
if (!$event) {
continue;
}
$address = implode(
' ',
array_filter([$event['address'] ?? null, $event['postal_code'] ?? null, $event['city'] ?? null]),
);
if ($event['event_type'] === 'adoption') {
$db->prepare(
"UPDATE animals SET status='adopte',is_archived=1,archived_at=:archived,current_address=:address,refuge_room=NULL,care_box_key=NULL,quarantine_until=NULL,updated_at=datetime('now') WHERE id=:id",
)->execute([
':archived' => $event['event_date'] . ' 12:00:00',
':address' =>
$address !== ''
? $address
: 'Adopté par ' . ($event['contact_name'] ?: 'un adoptant non renseigné'),
':id' => $animalId,
]);
} elseif ($event['event_type'] === 'foster') {
$db->prepare(
"UPDATE animals SET status=CASE WHEN lower(COALESCE(reason,'')) LIKE '%permanente%' THEN 'fa_permanente' ELSE 'fa' END,is_archived=0,archived_at=NULL,current_address=:address,refuge_room=NULL,care_box_key=NULL,updated_at=datetime('now') WHERE id=:id",
)->execute([
':address' =>
$address !== '' ? $address : 'Chez ' . ($event['contact_name'] ?: 'une famille daccueil'),
':id' => $animalId,
]);
} elseif (in_array($event['event_type'], ['return', 'cancellation'], true)) {
$db->prepare(
"UPDATE animals SET status='refuge',is_archived=0,archived_at=NULL,current_address=NULL,updated_at=datetime('now') WHERE id=?",
)->execute([$animalId]);
} elseif ($event['event_type'] === 'reservation') {
$db->prepare(
"UPDATE animals SET status='reserve',is_archived=0,archived_at=NULL,updated_at=datetime('now') WHERE id=?",
)->execute([$animalId]);
}
}
}
}