271 lines
12 KiB
PHP
271 lines
12 KiB
PHP
<?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 d’accueil 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 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]);
|
||
}
|
||
}
|
||
}
|
||
}
|