Globinours/app/Services/Asm3MovementImporter.php

271 lines
12 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 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]);
}
}
}
}