Legacy-Abbau Schritt 1: Dual-Write in die kl_*-Tabellen entfernt
Die Migration in participants/ledger_entries ist abgeschlossen: kein
Teilnehmer traegt noch eine legacy_mitarbeiter_id, kein Journaleintrag eine
legacy_table. Damit war der jeweils zweite Zweig ("Default-Mandant schreibt
zusaetzlich nach kl_Einzahlungen/kl_Kaffeeverbrauch/kl_Mitarbeiter") in
sechs Dateien nicht mehr erreichbar - er musste aber bei jeder Aenderung
mitgepflegt werden, zuletzt bei der Bemerkung fuer Einzahlungen.
Entfernt:
- Dual-Write beim Buchen: einzahlung.php, stricheintragen.php, index.php,
jahresauswertung.php, app/imports.php, app/paypal-inbox.php
- Dual-Write in der Mitgliederverwaltung: ledger_create_participant,
ledger_update_participant, ledger_set_participant_active,
ledger_anonymize_participant
- die verwaisten Spiegelfunktionen ledger_mirror_legacy_payment,
ledger_mirror_legacy_consumption und ledger_void_entry_by_legacy_id
Nebenbei behoben: kaffeeliste.php hat die Teilnehmer-Detailseite nur fuer
Mitglieder mit legacy_mitarbeiter_id verlinkt - die hat seit der Migration
niemand mehr, die Seite war also fuer alle unerreichbar. Verlinkt und
adressiert wird jetzt ueber participant_id; "user_id" bleibt als Alias
erhalten. Der Mandanten-Isolationstest prueft jetzt ledger_void_entry, also
den Pfad, den letzteneintraege.php tatsaechlich nutzt.
Pruefskripte unveraendert gegenueber der Baseline vor dem Umbau
(http-smoke 27/6, role-matrix 55/0); die Isolationspruefung steigt von
9 auf 11 PASS bei gleichem vorbestehendem Fehler.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+10
-24
@@ -188,10 +188,8 @@ function imports_fetch_batch(PDO $pdo, int $tenantId, int $batchId): ?array
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits every 'matched' row of a previewed batch into the ledger: for
|
||||
* participants with a legacy_mitarbeiter_id (default tenant), a matching
|
||||
* kl_Einzahlungen row is dual-written and mirrored; every other tenant is
|
||||
* booked straight into the ledger. Already-committed batches are rejected.
|
||||
* Commits every 'matched' row of a previewed batch straight into the ledger.
|
||||
* Already-committed batches are rejected.
|
||||
*
|
||||
* @return array{imported: int}
|
||||
*/
|
||||
@@ -208,7 +206,7 @@ function imports_commit_batch(PDO $pdo, int $tenantId, int $batchId): array
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT r.id, r.participant_id, r.amount_cents, r.booked_at, p.legacy_mitarbeiter_id
|
||||
"SELECT r.id, r.participant_id, r.amount_cents, r.booked_at
|
||||
FROM payment_import_rows r
|
||||
JOIN participants p ON p.id = r.participant_id
|
||||
WHERE r.batch_id = ? AND r.status = 'matched'"
|
||||
@@ -220,30 +218,18 @@ function imports_commit_batch(PDO $pdo, int $tenantId, int $batchId): array
|
||||
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
$insertLegacy = $pdo->prepare(
|
||||
'INSERT INTO kl_Einzahlungen (MitarbeiterID, Betrag, Datum) VALUES (?, ?, ?)'
|
||||
);
|
||||
$updateRow = $pdo->prepare(
|
||||
"UPDATE payment_import_rows SET status = 'imported', ledger_entry_id = ? WHERE id = ?"
|
||||
);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$legacyMitarbeiterId = $row['legacy_mitarbeiter_id'] !== null ? (int)$row['legacy_mitarbeiter_id'] : null;
|
||||
$amountCents = (int)$row['amount_cents'];
|
||||
|
||||
if ($legacyMitarbeiterId !== null) {
|
||||
$insertLegacy->execute([$legacyMitarbeiterId, $amountCents / 100, $row['booked_at']]);
|
||||
$legacyPaymentId = (int)$pdo->lastInsertId();
|
||||
ledger_mirror_legacy_payment($pdo, $tenantId, $legacyPaymentId);
|
||||
|
||||
$entryStmt = $pdo->prepare(
|
||||
"SELECT id FROM ledger_entries WHERE tenant_id = ? AND legacy_table = 'kl_Einzahlungen' AND legacy_id = ?"
|
||||
);
|
||||
$entryStmt->execute([$tenantId, $legacyPaymentId]);
|
||||
$ledgerEntryId = (int)$entryStmt->fetchColumn();
|
||||
} else {
|
||||
$ledgerEntryId = ledger_record_payment($pdo, $tenantId, (int)$row['participant_id'], $amountCents, 'csv_import');
|
||||
}
|
||||
$ledgerEntryId = ledger_record_payment(
|
||||
$pdo,
|
||||
$tenantId,
|
||||
(int)$row['participant_id'],
|
||||
(int)$row['amount_cents'],
|
||||
'csv_import'
|
||||
);
|
||||
|
||||
$updateRow->execute([$ledgerEntryId, $row['id']]);
|
||||
$imported++;
|
||||
|
||||
+24
-243
@@ -305,60 +305,6 @@ function ledger_fetch_participant_summary_by_legacy_id(PDO $pdo, int $tenantId,
|
||||
return $summaries[0] ?? null;
|
||||
}
|
||||
|
||||
function ledger_mirror_legacy_consumption(PDO $pdo, int $tenantId, int $legacyConsumptionId): void
|
||||
{
|
||||
$stmt = $pdo->prepare(
|
||||
"INSERT INTO ledger_entries
|
||||
(tenant_id, participant_id, type, amount_cents, marks_count,
|
||||
unit_price_cents, booked_at, source, note, legacy_table, legacy_id)
|
||||
SELECT
|
||||
p.tenant_id,
|
||||
p.id,
|
||||
'consumption',
|
||||
-CAST(ROUND(v.Kosten * 100) AS SIGNED),
|
||||
v.AnzahlStriche,
|
||||
CAST(ROUND(v.KostenproStrich * 100) AS SIGNED),
|
||||
v.Datum,
|
||||
CASE
|
||||
WHEN v.Eintragsart = 2 THEN 'legacy_web'
|
||||
ELSE 'legacy_manual'
|
||||
END,
|
||||
CASE
|
||||
WHEN v.Eintragsart IS NULL THEN NULL
|
||||
ELSE CONCAT('Legacy Eintragsart: ', v.Eintragsart)
|
||||
END,
|
||||
'kl_Kaffeeverbrauch',
|
||||
v.VerbrauchID
|
||||
FROM kl_Kaffeeverbrauch v
|
||||
JOIN participants p
|
||||
ON p.tenant_id = ?
|
||||
AND p.legacy_mitarbeiter_id = v.MitarbeiterID
|
||||
WHERE v.VerbrauchID = ?
|
||||
ON DUPLICATE KEY UPDATE
|
||||
participant_id = VALUES(participant_id),
|
||||
type = VALUES(type),
|
||||
amount_cents = VALUES(amount_cents),
|
||||
marks_count = VALUES(marks_count),
|
||||
unit_price_cents = VALUES(unit_price_cents),
|
||||
booked_at = VALUES(booked_at),
|
||||
source = VALUES(source),
|
||||
note = VALUES(note)"
|
||||
);
|
||||
$stmt->execute([$tenantId, $legacyConsumptionId]);
|
||||
|
||||
$check = $pdo->prepare(
|
||||
"SELECT COUNT(*)
|
||||
FROM ledger_entries
|
||||
WHERE tenant_id = ?
|
||||
AND legacy_table = 'kl_Kaffeeverbrauch'
|
||||
AND legacy_id = ?"
|
||||
);
|
||||
$check->execute([$tenantId, $legacyConsumptionId]);
|
||||
if ((int)$check->fetchColumn() !== 1) {
|
||||
throw new RuntimeException('Der Legacy-Strich konnte nicht ins Ledger gespiegelt werden.');
|
||||
}
|
||||
}
|
||||
|
||||
function ledger_is_default_tenant(PDO $pdo, int $tenantId): bool
|
||||
{
|
||||
$tenant = ledger_fetch_default_tenant($pdo);
|
||||
@@ -412,10 +358,7 @@ function ledger_fetch_participants_for_admin(PDO $pdo, int $tenantId): array
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a tenant-scoped participant. For the default tenant, a matching
|
||||
* kl_Mitarbeiter row is dual-written too, so the still-legacy bulk entry
|
||||
* pages (stricheintragen.php, einzahlung.php) keep listing the member; other
|
||||
* tenants have no legacy shadow table and get a participants-only row.
|
||||
* Creates a tenant-scoped participant.
|
||||
*
|
||||
* @throws Throwable on constraint violations (e.g. duplicate email)
|
||||
*/
|
||||
@@ -427,35 +370,14 @@ function ledger_create_participant(PDO $pdo, int $tenantId, string $displayName,
|
||||
$paypalName = $paypalName !== null ? trim($paypalName) : null;
|
||||
$paypalName = $paypalName !== '' ? $paypalName : null;
|
||||
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
$legacyMitarbeiterId = null;
|
||||
if (ledger_is_default_tenant($pdo, $tenantId)) {
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO kl_Mitarbeiter (Name, Email, paypalname, aktiv, admin) VALUES (?, ?, ?, ?, 0)'
|
||||
);
|
||||
$stmt->execute([$displayName, $email, $paypalName, $active ? 1 : 0]);
|
||||
$legacyMitarbeiterId = (int)$pdo->lastInsertId();
|
||||
}
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO participants
|
||||
(tenant_id, display_name, email, email_norm, paypal_name, active)
|
||||
VALUES (?, ?, ?, ?, ?, ?)'
|
||||
);
|
||||
$stmt->execute([$tenantId, $displayName, $email, $emailNorm, $paypalName, $active ? 1 : 0]);
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO participants
|
||||
(tenant_id, display_name, email, email_norm, paypal_name, active, legacy_mitarbeiter_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)'
|
||||
);
|
||||
$stmt->execute([$tenantId, $displayName, $email, $emailNorm, $paypalName, $active ? 1 : 0, $legacyMitarbeiterId]);
|
||||
$participantId = (int)$pdo->lastInsertId();
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
return $participantId;
|
||||
} catch (Throwable $e) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
return (int)$pdo->lastInsertId();
|
||||
}
|
||||
|
||||
function ledger_update_participant(PDO $pdo, int $tenantId, int $participantId, string $displayName, string $email, ?string $paypalName, bool $active): bool
|
||||
@@ -466,168 +388,42 @@ function ledger_update_participant(PDO $pdo, int $tenantId, int $participantId,
|
||||
$paypalName = $paypalName !== null ? trim($paypalName) : null;
|
||||
$paypalName = $paypalName !== '' ? $paypalName : null;
|
||||
|
||||
$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;
|
||||
$stmt = $pdo->prepare(
|
||||
'UPDATE participants
|
||||
SET display_name = ?, email = ?, email_norm = ?, paypal_name = ?, active = ?
|
||||
WHERE id = ? AND tenant_id = ?'
|
||||
);
|
||||
$stmt->execute([$displayName, $email, $emailNorm, $paypalName, $active ? 1 : 0, $participantId, $tenantId]);
|
||||
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
if ($legacyMitarbeiterId !== null) {
|
||||
$stmt = $pdo->prepare(
|
||||
'UPDATE kl_Mitarbeiter SET Name = ?, Email = ?, paypalname = ?, aktiv = ? WHERE MitarbeiterID = ?'
|
||||
);
|
||||
$stmt->execute([$displayName, $email, $paypalName, $active ? 1 : 0, $legacyMitarbeiterId]);
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
'UPDATE participants
|
||||
SET display_name = ?, email = ?, email_norm = ?, paypal_name = ?, active = ?
|
||||
WHERE id = ? AND tenant_id = ?'
|
||||
);
|
||||
$stmt->execute([$displayName, $email, $emailNorm, $paypalName, $active ? 1 : 0, $participantId, $tenantId]);
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
return true;
|
||||
} catch (Throwable $e) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
return $stmt->rowCount() >= 0;
|
||||
}
|
||||
|
||||
function ledger_set_participant_active(PDO $pdo, int $tenantId, int $participantId, bool $active): 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;
|
||||
$stmt = $pdo->prepare('UPDATE participants SET active = ? WHERE id = ? AND tenant_id = ?');
|
||||
$stmt->execute([$active ? 1 : 0, $participantId, $tenantId]);
|
||||
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
if ($legacyMitarbeiterId !== null) {
|
||||
$pdo->prepare('UPDATE kl_Mitarbeiter SET aktiv = ? WHERE MitarbeiterID = ?')
|
||||
->execute([$active ? 1 : 0, $legacyMitarbeiterId]);
|
||||
}
|
||||
|
||||
$pdo->prepare('UPDATE participants SET active = ? WHERE id = ? AND tenant_id = ?')
|
||||
->execute([$active ? 1 : 0, $participantId, $tenantId]);
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
return true;
|
||||
} catch (Throwable $e) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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).
|
||||
* the tenant's bookkeeping.
|
||||
*/
|
||||
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(
|
||||
"INSERT INTO ledger_entries
|
||||
(tenant_id, participant_id, type, amount_cents, marks_count,
|
||||
unit_price_cents, booked_at, source, note, legacy_table, legacy_id)
|
||||
SELECT
|
||||
p.tenant_id,
|
||||
p.id,
|
||||
'payment',
|
||||
CAST(ROUND(e.Betrag * 100) AS SIGNED),
|
||||
NULL,
|
||||
NULL,
|
||||
e.Datum,
|
||||
'legacy_payment',
|
||||
e.Bemerkung,
|
||||
'kl_Einzahlungen',
|
||||
e.EinzahlungsID
|
||||
FROM kl_Einzahlungen e
|
||||
JOIN participants p
|
||||
ON p.tenant_id = ?
|
||||
AND p.legacy_mitarbeiter_id = e.MitarbeiterID
|
||||
WHERE e.EinzahlungsID = ?
|
||||
ON DUPLICATE KEY UPDATE
|
||||
participant_id = VALUES(participant_id),
|
||||
type = VALUES(type),
|
||||
amount_cents = VALUES(amount_cents),
|
||||
marks_count = VALUES(marks_count),
|
||||
unit_price_cents = VALUES(unit_price_cents),
|
||||
booked_at = VALUES(booked_at),
|
||||
source = VALUES(source),
|
||||
note = VALUES(note)"
|
||||
'UPDATE participants
|
||||
SET display_name = ?, email = NULL, email_norm = NULL, paypal_name = NULL, active = 0, user_id = NULL
|
||||
WHERE id = ? AND tenant_id = ?'
|
||||
);
|
||||
$stmt->execute([$tenantId, $legacyPaymentId]);
|
||||
$stmt->execute([$placeholderName, $participantId, $tenantId]);
|
||||
|
||||
$check = $pdo->prepare(
|
||||
"SELECT COUNT(*)
|
||||
FROM ledger_entries
|
||||
WHERE tenant_id = ?
|
||||
AND legacy_table = 'kl_Einzahlungen'
|
||||
AND legacy_id = ?"
|
||||
);
|
||||
$check->execute([$tenantId, $legacyPaymentId]);
|
||||
if ((int)$check->fetchColumn() !== 1) {
|
||||
throw new RuntimeException('Die Legacy-Einzahlung konnte nicht ins Ledger gespiegelt werden.');
|
||||
}
|
||||
return $stmt->rowCount() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -695,21 +491,6 @@ function ledger_void_entry(PDO $pdo, int $tenantId, int $entryId, ?string $expec
|
||||
return true;
|
||||
}
|
||||
|
||||
function ledger_void_entry_by_legacy_id(PDO $pdo, int $tenantId, string $legacyTable, int $legacyId): bool
|
||||
{
|
||||
$stmt = $pdo->prepare(
|
||||
"UPDATE ledger_entries
|
||||
SET voided_at = NOW()
|
||||
WHERE tenant_id = ?
|
||||
AND legacy_table = ?
|
||||
AND legacy_id = ?
|
||||
AND voided_at IS NULL"
|
||||
);
|
||||
$stmt->execute([$tenantId, $legacyTable, $legacyId]);
|
||||
|
||||
return $stmt->rowCount() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Storniert den juengsten, noch nicht stornierten Web-Selbsteintrag eines
|
||||
* Teilnehmers (Quelle 'self_entry' bei SaaS-nativen Mandanten bzw. 'legacy_web'
|
||||
|
||||
@@ -102,24 +102,6 @@ function paypal_inbox_extract_token(string $address): ?string
|
||||
*/
|
||||
function paypal_book_payment(PDO $pdo, int $tenantId, array $participant, int $netCents, ?int $actorUserId, ?string $note = null): int
|
||||
{
|
||||
$legacyMitarbeiterId = isset($participant['legacy_mitarbeiter_id']) && $participant['legacy_mitarbeiter_id'] !== null
|
||||
? (int) $participant['legacy_mitarbeiter_id']
|
||||
: 0;
|
||||
|
||||
if ($legacyMitarbeiterId > 0) {
|
||||
$stmt = $pdo->prepare('INSERT INTO kl_Einzahlungen (MitarbeiterID, Betrag, Bemerkung, Datum) VALUES (?, ?, ?, ?)');
|
||||
$stmt->execute([$legacyMitarbeiterId, $netCents / 100, $note, date('Y-m-d H:i:s')]);
|
||||
$legacyPaymentId = (int) $pdo->lastInsertId();
|
||||
ledger_mirror_legacy_payment($pdo, $tenantId, $legacyPaymentId);
|
||||
|
||||
$idStmt = $pdo->prepare(
|
||||
"SELECT id FROM ledger_entries WHERE tenant_id = ? AND legacy_table = 'kl_Einzahlungen' AND legacy_id = ? LIMIT 1"
|
||||
);
|
||||
$idStmt->execute([$tenantId, $legacyPaymentId]);
|
||||
|
||||
return (int) $idStmt->fetchColumn();
|
||||
}
|
||||
|
||||
return ledger_record_payment($pdo, $tenantId, (int) $participant['participant_id'], $netCents, 'paypal_import', $actorUserId, $note);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user