M8: Loesch-/Anonymisierungsprozess fuer Teilnehmer und Mandanten

Zwei getrennte Flows:

- Teilnehmer anonymisieren statt loeschen (ledger_anonymize_participant):
  Name/E-Mail/PayPal-Name werden durch einen Platzhalter ersetzt, das
  Mitglied deaktiviert und vom Login-Konto getrennt. Buchungshistorie
  bleibt fuer die Kassenfuehrung erhalten, konsistent mit dem
  Storno-statt-Delete-Prinzip. Default-Mandant spiegelt die
  Anonymisierung in kl_Mitarbeiter (Email dort NOT NULL UNIQUE, bekommt
  Platzhalter statt NULL). Inhaber kann nicht anonymisiert werden.
- Mandant vollstaendig loeschen (mandant-loeschen.php, nur Inhaber):
  erfordert exakte Eingabe des Kundenkuerzels, loescht die tenants-Zeile;
  alle tenant-scoped Tabellen kaskadieren per Fremdschluessel. users
  bleiben bestehen (koennen zu mehreren Mandanten gehoeren). Der
  migrierte Default-Mandant ist ausgenommen, da seine kl_Mitarbeiter-
  Historie sonst verwaisen wuerde.

Live getestet: Mitglied mit Buchungshistorie anonymisiert (Historie
blieb erhalten), Mandantenloeschung mit falscher/richtiger Bestaetigung
geprueft, vollstaendiger Cascade-Delete ueber alle tenant-scoped Tabellen
verifiziert, globale users-Zeile bleibt korrekt erhalten.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 20:17:57 +02:00
co-authored by Claude Sonnet 5
parent f3045dbab9
commit f60c0bb85e
7 changed files with 218 additions and 3 deletions
+46
View File
@@ -496,6 +496,52 @@ function ledger_set_participant_active(PDO $pdo, int $tenantId, int $participant
}
}
/**
* Anonymizes a participant (GDPR-style erasure) instead of deleting them:
* name and email are replaced with a non-identifying placeholder and the
* participant is deactivated, but their ledger history stays intact for
* the tenant's bookkeeping. For the default tenant, the linked
* kl_Mitarbeiter row is anonymized the same way (its email column is
* NOT NULL UNIQUE, so it gets a placeholder instead of NULL).
*/
function ledger_anonymize_participant(PDO $pdo, int $tenantId, int $participantId): bool
{
$stmt = $pdo->prepare('SELECT legacy_mitarbeiter_id FROM participants WHERE id = ? AND tenant_id = ?');
$stmt->execute([$participantId, $tenantId]);
$row = $stmt->fetch();
if ($row === false) {
return false;
}
$legacyMitarbeiterId = $row['legacy_mitarbeiter_id'] !== null ? (int)$row['legacy_mitarbeiter_id'] : null;
$placeholderName = 'Gelöschter Teilnehmer #' . $participantId;
$placeholderEmail = "deleted-participant-{$participantId}@invalid.local";
try {
$pdo->beginTransaction();
if ($legacyMitarbeiterId !== null) {
$pdo->prepare('UPDATE kl_Mitarbeiter SET Name = ?, Email = ?, paypalname = NULL, aktiv = 0 WHERE MitarbeiterID = ?')
->execute([$placeholderName, $placeholderEmail, $legacyMitarbeiterId]);
}
$pdo->prepare(
'UPDATE participants
SET display_name = ?, email = NULL, email_norm = NULL, paypal_name = NULL, active = 0, user_id = NULL
WHERE id = ? AND tenant_id = ?'
)->execute([$placeholderName, $participantId, $tenantId]);
$pdo->commit();
return true;
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
return false;
}
}
function ledger_mirror_legacy_payment(PDO $pdo, int $tenantId, int $legacyPaymentId): void
{
$stmt = $pdo->prepare(