feat: soft-delete for teamer accounts

addresses #869dv9br3
This commit is contained in:
Björn Fromme
2026-08-11 12:26:13 +02:00
parent 388ecf9603
commit d654ce77fb
45 changed files with 1151 additions and 7 deletions
@@ -0,0 +1,98 @@
<?php
namespace App\Service\Teamer;
use App\Entity\Teamer;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
/**
* Soft-deletes a teamer together with their user account, and restores them again.
*
* A deletion excludes the person from every forward-looking process - the teamer list,
* autocompletes, forms, mailings and cron reminders - while every existing record stays
* untouched and visible: dispositions, applications, documents, feedback and the
* contracts and invoices rendered from them are business records that have to survive.
*
* Both Teamer and User carry their own flag, because neither side is guaranteed to
* exist and the queries that have to filter only ever have one of the two aliases at
* hand. This class is the only place that writes either flag, so the two cannot drift
* apart.
*
* Deletion is deliberately independent of the block expressed by User::$disabledAt: a
* disciplinary block has to survive a deletion and the restore that follows it.
*/
class AccountDeletionHandler
{
public const SOURCE_ADMIN = 'admin';
public const SOURCE_SELF = 'self';
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly LoggerInterface $logger,
) {
}
public function delete(Teamer|User $subject, string $source): void
{
[$teamer, $user] = $this->resolvePair($subject);
if (false === $this->isDeleted($teamer, $user)) {
$teamer?->setDeleted();
$user?->setDeleted();
$this->entityManager->flush();
$this->logger->info('Delete account', [...$this->logContext($teamer, $user), 'source' => $source]);
}
}
public function restore(Teamer|User $subject): void
{
[$teamer, $user] = $this->resolvePair($subject);
if (true === $this->isDeleted($teamer, $user)) {
$teamer?->setRestored();
$user?->setRestored();
$this->entityManager->flush();
$this->logger->info('Restore account', $this->logContext($teamer, $user));
}
}
/**
* @return array{0: ?Teamer, 1: ?User}
*/
private function resolvePair(Teamer|User $subject): array
{
if ($subject instanceof Teamer) {
return [$subject, $subject->getUser()];
}
return [$subject->getTeamer(), $subject];
}
/**
* Either side being flagged counts as deleted, so that a pair left inconsistent by
* an earlier failure is repaired rather than skipped.
*/
private function isDeleted(?Teamer $teamer, ?User $user): bool
{
return true === $teamer?->isDeleted() || true === $user?->isDeleted();
}
/**
* @return array<string, mixed>
*/
private function logContext(?Teamer $teamer, ?User $user): array
{
return [
'teamer' => $teamer?->getFullName(),
'teamer_id' => $teamer?->getId(),
'user' => $user?->getEmail(),
'user_id' => $user?->getId(),
];
}
}