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 d’accueil'), ':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]); } } } }