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:
2026-07-21 20:57:49 +02:00
co-authored by Claude Opus 4.8
parent dd6e1e77b0
commit eec7a2fef2
11 changed files with 98 additions and 482 deletions
+9 -23
View File
@@ -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 = ?"
$ledgerEntryId = ledger_record_payment(
$pdo,
$tenantId,
(int)$row['participant_id'],
(int)$row['amount_cents'],
'csv_import'
);
$entryStmt->execute([$tenantId, $legacyPaymentId]);
$ledgerEntryId = (int)$entryStmt->fetchColumn();
} else {
$ledgerEntryId = ledger_record_payment($pdo, $tenantId, (int)$row['participant_id'], $amountCents, 'csv_import');
}
$updateRow->execute([$ledgerEntryId, $row['id']]);
$imported++;
+12 -231
View File
@@ -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, legacy_mitarbeiter_id)
VALUES (?, ?, ?, ?, ?, ?, ?)'
(tenant_id, display_name, email, email_norm, paypal_name, active)
VALUES (?, ?, ?, ?, ?, ?)'
);
$stmt->execute([$tenantId, $displayName, $email, $emailNorm, $paypalName, $active ? 1 : 0, $legacyMitarbeiterId]);
$participantId = (int)$pdo->lastInsertId();
$stmt->execute([$tenantId, $displayName, $email, $emailNorm, $paypalName, $active ? 1 : 0]);
$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,24 +388,6 @@ 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;
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 = ?
@@ -491,143 +395,35 @@ function ledger_update_participant(PDO $pdo, int $tenantId, int $participantId,
);
$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;
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();
$stmt = $pdo->prepare('UPDATE participants SET active = ? WHERE id = ? AND tenant_id = ?');
$stmt->execute([$active ? 1 : 0, $participantId, $tenantId]);
return true;
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
return false;
}
}
/**
* 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(
$stmt = $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)"
);
$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'
-18
View File
@@ -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);
}
+7 -48
View File
@@ -43,34 +43,14 @@ if (!$hasAccess) {
}
/**
* Liefert die Mitarbeiter/Teilnehmerliste fuer die Vorder-/Rueckseiten- oder
* Alle-Auswahl. Siehe stricheintragen.php fuer dieselbe Logik: Default-
* Mandant nutzt die Legacy-Historie, andere Mandanten die Ledger-native
* Fensterpruefung mit dem konfigurierten Listenfenster.
* Liefert die Teilnehmerliste fuer die Vorder-/Rueckseiten- oder Alle-Auswahl.
* Siehe stricheintragen.php fuer dieselbe Logik.
*
* @return list<array>
*/
function einzahlung_fetch_participants(PDO $pdo, int $tenantId, string $action): array
{
$isDefault = ledger_is_default_tenant($pdo, $tenantId);
if ($action === 'vorderseite' || $action === 'rueckseite') {
if ($isDefault) {
$vergleich = $action === 'vorderseite' ? '>= 10' : '< 10';
$join = $action === 'vorderseite' ? 'JOIN' : 'LEFT JOIN';
$stmt = $pdo->query(
"SELECT M.MitarbeiterID
FROM kl_Mitarbeiter M
{$join} kl_Kaffeeverbrauch V ON M.MitarbeiterID = V.MitarbeiterID AND V.Datum >= DATE_SUB(NOW(), INTERVAL 100 DAY)
WHERE M.aktiv = 1
GROUP BY M.MitarbeiterID
HAVING COALESCE(SUM(V.AnzahlStriche), 0) {$vergleich}"
);
$legacyIds = array_map('intval', $stmt->fetchAll(PDO::FETCH_COLUMN));
return ledger_fetch_participant_summaries($pdo, $tenantId, ['legacy_mitarbeiter_ids' => $legacyIds]);
}
$settings = saas_fetch_tenant_settings($pdo, $tenantId);
$windowDays = $settings['sheet_window_days'] ?? 100;
@@ -94,24 +74,13 @@ const EINZAHLUNG_MAX_BETRAG = 1000.00;
// Verarbeitung des Formulars, wenn es gesendet wurde
if ($_SERVER["REQUEST_METHOD"] == "POST" ) {
$hatGespeichert = true;
$datum = date("Y-m-d H:i:s"); // Das aktuelle Datum verwenden
// Legacy-Verknuepfung je Teilnehmer vorab laden, damit pro Zeile
// entschieden werden kann, ob (Default-Mandant) zusaetzlich in
// kl_Einzahlungen geschrieben oder (jeder andere Mandant) direkt ins
// Ledger gebucht wird.
$legacyMap = [];
$stmt = $pdo->prepare('SELECT id, legacy_mitarbeiter_id FROM participants WHERE tenant_id = ?');
// Teilnehmer des Mandanten: dient zugleich der Zugehoerigkeitspruefung und
// liefert die Namen fuer verstaendliche Fehlermeldungen.
$namen = [];
$stmt = $pdo->prepare('SELECT id, display_name FROM participants WHERE tenant_id = ?');
$stmt->execute([$tenantId]);
foreach ($stmt->fetchAll() as $row) {
$legacyMap[(int)$row['id']] = $row['legacy_mitarbeiter_id'] !== null ? (int)$row['legacy_mitarbeiter_id'] : null;
}
// Namen fuer verstaendliche Fehlermeldungen.
$namen = [];
$stmtNamen = $pdo->prepare('SELECT id, display_name FROM participants WHERE tenant_id = ?');
$stmtNamen->execute([$tenantId]);
foreach ($stmtNamen->fetchAll() as $row) {
$namen[(int)$row['id']] = (string)$row['display_name'];
}
@@ -121,7 +90,7 @@ if ($_SERVER["REQUEST_METHOD"] == "POST" ) {
$zuBuchen = [];
foreach ($_POST["anzahlBetrag"] ?? [] as $participantId => $anzahlBetrag) {
$participantId = (int)$participantId;
if ($participantId <= 0 || !array_key_exists($participantId, $legacyMap)) {
if ($participantId <= 0 || !array_key_exists($participantId, $namen)) {
continue;
}
@@ -180,17 +149,8 @@ if ($_SERVER["REQUEST_METHOD"] == "POST" ) {
} else {
try {
$pdo->beginTransaction();
$insertLegacy = $pdo->prepare(
"INSERT INTO kl_Einzahlungen (MitarbeiterID, Betrag, Bemerkung, Datum) VALUES (?, ?, ?, ?)"
);
foreach ($zuBuchen as $participantId => $zeile) {
$legacyMitarbeiterId = $legacyMap[$participantId];
if ($legacyMitarbeiterId !== null) {
$insertLegacy->execute([$legacyMitarbeiterId, $zeile['betrag'], $zeile['bemerkung'], $datum]);
$legacyPaymentId = (int)$pdo->lastInsertId();
ledger_mirror_legacy_payment($pdo, $tenantId, $legacyPaymentId);
} else {
ledger_record_payment(
$pdo,
$tenantId,
@@ -200,7 +160,6 @@ if ($_SERVER["REQUEST_METHOD"] == "POST" ) {
$saasUser['user_id'] ?? null,
$zeile['bemerkung']
);
}
$eingetragen++;
}
-32
View File
@@ -129,38 +129,7 @@ function dashboard_record_self_entry(PDO $pdo, array $participant, array $settin
return ['ok' => false, 'error' => 'Der Preis pro Strich ist nicht gültig konfiguriert.'];
}
$legacyMitarbeiterId = $participant['legacy_mitarbeiter_id'] !== null
? (int)$participant['legacy_mitarbeiter_id']
: 0;
try {
if ($legacyMitarbeiterId > 0) {
// Migrierter Default-Mandant: weiterhin in die Legacy-Tabelle
// schreiben, damit die noch-legacy Sammelseiten (stricheintragen.php)
// den Eintrag ebenfalls sehen; der Ledger-Eintrag entsteht per
// Spiegelung.
$bookedAt = date('Y-m-d H:i:s');
$cost = (($marks * $unitPriceCents) / 100);
$unitPrice = ($unitPriceCents / 100);
$pdo->beginTransaction();
$stmt = $pdo->prepare(
'INSERT INTO kl_Kaffeeverbrauch
(MitarbeiterID, AnzahlStriche, Kosten, KostenproStrich, Datum, Eintragsart)
VALUES (?, ?, ?, ?, ?, 2)'
);
$stmt->execute([$legacyMitarbeiterId, $marks, $cost, $unitPrice, $bookedAt]);
$legacyConsumptionId = (int)$pdo->lastInsertId();
if ($legacyConsumptionId <= 0) {
throw new RuntimeException('Legacy-Strich konnte nicht angelegt werden.');
}
ledger_mirror_legacy_consumption($pdo, (int)$participant['tenant_id'], $legacyConsumptionId);
$pdo->commit();
} else {
// SaaS-native Mandanten (alle ausser dem migrierten Default-
// Mandanten) haben keine Legacy-Schattentabelle - direkt ins
// Ledger buchen.
ledger_record_consumption(
$pdo,
(int)$participant['tenant_id'],
@@ -170,7 +139,6 @@ function dashboard_record_self_entry(PDO $pdo, array $participant, array $settin
'self_entry',
$actorUserId
);
}
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
-19
View File
@@ -101,22 +101,10 @@ if ($hasAccess && $_SERVER["REQUEST_METHOD"] === "POST") {
];
}, $zuteilung);
} else {
// Legacy-Verknuepfung je Teilnehmer vorab laden (gleiches Zweig-Muster
// wie bei Sammelerfassung und CSV-Import: Default-Mandant bucht
// zusaetzlich in kl_Einzahlungen, alle anderen Mandanten rein Ledger-nativ).
$legacyMap = [];
$stmt = $pdo->prepare('SELECT id, legacy_mitarbeiter_id FROM participants WHERE tenant_id = ?');
$stmt->execute([$tenantId]);
foreach ($stmt->fetchAll() as $row) {
$legacyMap[(int)$row['id']] = $row['legacy_mitarbeiter_id'] !== null ? (int)$row['legacy_mitarbeiter_id'] : null;
}
$ergebnisse = [];
$zuBenachrichtigen = [];
try {
$pdo->beginTransaction();
$insertLegacy = $pdo->prepare('INSERT INTO kl_Einzahlungen (MitarbeiterID, Betrag, Datum) VALUES (?, ?, ?)');
$jetzt = date('Y-m-d H:i:s');
foreach ($zuteilung as $z) {
$person = $z['person'];
@@ -125,14 +113,7 @@ if ($hasAccess && $_SERVER["REQUEST_METHOD"] === "POST") {
continue;
}
$legacyMitarbeiterId = $legacyMap[$person['participant_id']] ?? null;
if ($legacyMitarbeiterId !== null) {
$insertLegacy->execute([$legacyMitarbeiterId, $bonusCents / 100, $jetzt]);
$legacyPaymentId = (int)$pdo->lastInsertId();
ledger_mirror_legacy_payment($pdo, $tenantId, $legacyPaymentId);
} else {
ledger_record_payment($pdo, $tenantId, (int)$person['participant_id'], $bonusCents, 'year_end_bonus', $createdByUserId);
}
$email = trim((string)($person['email'] ?? ''));
if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
+2 -6
View File
@@ -72,7 +72,7 @@ if ($hasAccess) {
</tr>
<?php
foreach ($participants as $participant) {
$legacyId = $participant['legacy_mitarbeiter_id'];
$participantId = (int)$participant['participant_id'];
$name = saas_html($participant['display_name']);
$email = saas_html((string)($participant['email'] ?? ''));
$balance = kaffeeliste_money((int)$participant['balance_cents']);
@@ -81,11 +81,7 @@ if ($hasAccess) {
$totalPayments = kaffeeliste_money((int)$participant['total_payments_cents']);
echo "<tr>";
if ($legacyId !== null) {
echo "<td><a href=\"teilnehmerauswertung.php?user_id=" . (int)$legacyId . "\">{$name}</a></td>";
} else {
echo "<td>{$name}</td>";
}
echo "<td><a href=\"teilnehmerauswertung.php?participant_id={$participantId}\">{$name}</a></td>";
echo "<td>{$email}</td>";
echo "<td>{$balance}</td>";
echo "<td>{$totalCosts}</td>";
+13 -8
View File
@@ -102,15 +102,20 @@ try {
m8_assert('Zugangsvergabe auf fremden Teilnehmer schlaegt fehl', $grantResult['ok'] === false, $failures, $passes);
// 7. Storno eines fremden Eintrags ueber den falschen Mandanten aendert nichts.
// ledger_void_entry_by_legacy_id() matcht ueber (tenant_id, legacy_table,
// legacy_id); dafuer braucht es einen Eintrag mit einer synthetischen
// Legacy-Referenz, unabhaengig von echten kl_*-Zeilen.
$pdo->prepare(
"UPDATE ledger_entries SET legacy_table = 'isolation_test', legacy_id = ? WHERE tenant_id = ? AND participant_id = ?"
)->execute([$participantA, $tenantA, $participantA]);
$voidedWrongTenant = ledger_void_entry_by_legacy_id($pdo, $tenantB, 'isolation_test', $participantA);
// Geprueft wird der echte Storno-Pfad ledger_void_entry(), den auch
// letzteneintraege.php nutzt.
$entryIdA = (int)$pdo->query(
"SELECT id FROM ledger_entries WHERE tenant_id = {$tenantA} AND participant_id = {$participantA} AND voided_at IS NULL LIMIT 1"
)->fetchColumn();
m8_assert('Testeintrag fuer Storno vorhanden', $entryIdA > 0, $failures, $passes);
$voidedWrongTenant = ledger_void_entry($pdo, $tenantB, $entryIdA);
m8_assert('Storno ueber falschen Mandanten aendert nichts', $voidedWrongTenant === false, $failures, $passes);
$voidedRightTenant = ledger_void_entry_by_legacy_id($pdo, $tenantA, 'isolation_test', $participantA);
$stillActive = (string)$pdo->query("SELECT COUNT(*) FROM ledger_entries WHERE id = {$entryIdA} AND voided_at IS NULL")->fetchColumn();
m8_assert('fremder Eintrag ist nach abgelehntem Storno unveraendert', $stillActive === '1', $failures, $passes);
$voidedRightTenant = ledger_void_entry($pdo, $tenantA, $entryIdA);
m8_assert('Storno ueber den richtigen Mandanten funktioniert', $voidedRightTenant === true, $failures, $passes);
// 8. Audit-Log ist mandantenscoped.
+1 -1
View File
@@ -51,7 +51,7 @@ $checks = [
'path' => 'kaffeeliste.php',
'contains' => [
'Aktive Mitarbeiter',
'teilnehmerauswertung.php?user_id=',
'teilnehmerauswertung.php?participant_id=',
'GM Negativ',
'gm-negative@test.local',
'-2,00 €',
+11 -58
View File
@@ -44,48 +44,15 @@ if (!$hasAccess) {
}
/**
* Liefert die Mitarbeiter/Teilnehmerliste fuer die Vorder-/Rueckseiten- oder
* Alle-Auswahl. Fuer den Default-Mandanten bleibt die Fensterlogik exakt wie
* im Legacy-Bestand (Anker ist das juengste Datum in kl_Kaffeeverbrauch);
* andere Mandanten haben keine Legacy-Historie und nutzen die
* Ledger-native Fensterpruefung mit dem konfigurierten Listenfenster.
* Liefert die Teilnehmerliste fuer die Vorder-/Rueckseiten- oder
* Alle-Auswahl. Vorderseite sind die Vieltrinker (>= 10 Striche im
* Listenfenster), Rueckseite der Rest.
*
* @return list<array>
*/
function stricheintragen_fetch_participants(PDO $pdo, int $tenantId, string $action): array
{
$isDefault = ledger_is_default_tenant($pdo, $tenantId);
if ($isDefault && $action === 'vorderseite') {
$stmt = $pdo->query(
"SELECT M.MitarbeiterID
FROM kl_Mitarbeiter M
JOIN kl_Kaffeeverbrauch V ON M.MitarbeiterID = V.MitarbeiterID
WHERE V.Datum >= DATE_SUB((SELECT MAX(Datum) FROM kl_Kaffeeverbrauch WHERE Datum < CURDATE()), INTERVAL 100 DAY)
AND M.aktiv = 1
GROUP BY M.MitarbeiterID
HAVING SUM(V.AnzahlStriche) >= 10"
);
$legacyIds = array_map('intval', $stmt->fetchAll(PDO::FETCH_COLUMN));
return ledger_fetch_participant_summaries($pdo, $tenantId, ['legacy_mitarbeiter_ids' => $legacyIds]);
}
if ($isDefault && $action === 'rueckseite') {
$stmt = $pdo->query(
"SELECT M.MitarbeiterID
FROM kl_Mitarbeiter M
LEFT JOIN kl_Kaffeeverbrauch V ON M.MitarbeiterID = V.MitarbeiterID AND V.Datum >= DATE_SUB((SELECT MAX(Datum) FROM kl_Kaffeeverbrauch), INTERVAL 100 DAY)
WHERE M.aktiv = 1
GROUP BY M.MitarbeiterID
HAVING COALESCE(SUM(V.AnzahlStriche), 0) < 10"
);
$legacyIds = array_map('intval', $stmt->fetchAll(PDO::FETCH_COLUMN));
return ledger_fetch_participant_summaries($pdo, $tenantId, ['legacy_mitarbeiter_ids' => $legacyIds]);
}
if (!$isDefault && ($action === 'vorderseite' || $action === 'rueckseite')) {
if ($action === 'vorderseite' || $action === 'rueckseite') {
$settings = saas_fetch_tenant_settings($pdo, $tenantId);
$windowDays = $settings['sheet_window_days'] ?? 100;
@@ -104,41 +71,27 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") {
$hatGespeichert = true;
$kostenproStrich = floatval($_POST["kostenproStrich"] ?? 0);
$unitPriceCents = (int)round($kostenproStrich * 100);
$datum = date("Y-m-d H:i:s");
// Legacy-Verknuepfung je Teilnehmer vorab laden, damit pro Zeile
// entschieden werden kann, ob (Default-Mandant) zusaetzlich in
// kl_Kaffeeverbrauch geschrieben oder (jeder andere Mandant) direkt
// ins Ledger gebucht wird.
$legacyMap = [];
$stmt = $pdo->prepare('SELECT id, legacy_mitarbeiter_id FROM participants WHERE tenant_id = ?');
// Teilnehmer des Mandanten - dient der Zugehoerigkeitspruefung, damit ueber
// das Formular keine fremden IDs bebucht werden koennen.
$eigeneTeilnehmer = [];
$stmt = $pdo->prepare('SELECT id FROM participants WHERE tenant_id = ?');
$stmt->execute([$tenantId]);
foreach ($stmt->fetchAll() as $row) {
$legacyMap[(int)$row['id']] = $row['legacy_mitarbeiter_id'] !== null ? (int)$row['legacy_mitarbeiter_id'] : null;
foreach ($stmt->fetchAll(PDO::FETCH_COLUMN) as $id) {
$eigeneTeilnehmer[(int)$id] = true;
}
try {
$pdo->beginTransaction();
$insertLegacy = $pdo->prepare(
"INSERT INTO kl_Kaffeeverbrauch (MitarbeiterID, AnzahlStriche, Kosten, KostenproStrich, Datum) VALUES (?, ?, ?, ?, ?)"
);
foreach ($_POST["anzahlStriche"] ?? [] as $participantId => $anzahlStriche) {
$participantId = (int)$participantId;
$anzahlStriche = (int)$anzahlStriche;
if ($participantId <= 0 || $anzahlStriche === 0 || !array_key_exists($participantId, $legacyMap)) {
if ($participantId <= 0 || $anzahlStriche === 0 || !isset($eigeneTeilnehmer[$participantId])) {
continue;
}
$legacyMitarbeiterId = $legacyMap[$participantId];
if ($legacyMitarbeiterId !== null) {
$kosten = $anzahlStriche * $kostenproStrich;
$insertLegacy->execute([$legacyMitarbeiterId, $anzahlStriche, $kosten, $kostenproStrich, $datum]);
$legacyConsumptionId = (int)$pdo->lastInsertId();
ledger_mirror_legacy_consumption($pdo, $tenantId, $legacyConsumptionId);
} else {
ledger_record_consumption($pdo, $tenantId, $participantId, $anzahlStriche, $unitPriceCents, 'manual_bulk');
}
$eingetragen++;
}
+10 -5
View File
@@ -3,14 +3,19 @@
include "functions.php";
require_once __DIR__ . "/app/ledger.php";
$legacyUserId = filter_input(INPUT_GET, 'user_id', FILTER_VALIDATE_INT);
if ($legacyUserId === null) {
// Teilnehmer-ID aus dem Journal. "user_id" bleibt als Alias erhalten, damit
// aeltere Links und Lesezeichen weiter funktionieren.
$participantId = filter_input(INPUT_GET, 'participant_id', FILTER_VALIDATE_INT);
if ($participantId === null || $participantId === false) {
$participantId = filter_input(INPUT_GET, 'user_id', FILTER_VALIDATE_INT);
}
if ($participantId === null) {
http_response_code(400);
exit('Fehlender Parameter');
}
if ($legacyUserId === false || $legacyUserId <= 0) {
if ($participantId === false || $participantId <= 0) {
http_response_code(400);
exit('Ungültige Benutzer-ID');
exit('Ungültige Teilnehmer-ID');
}
$pdo = app_db_pdo();
@@ -32,7 +37,7 @@ if (!$hasAccess && $saasUser === null && checkKaffeelisteAdmin($conn, $mailadres
}
$participant = $hasAccess
? ledger_fetch_participant_summary_by_legacy_id($pdo, $tenantId, $legacyUserId)
? ledger_fetch_participant_summary($pdo, $tenantId, $participantId)
: null;
$settings = $hasAccess ? saas_fetch_tenant_settings($pdo, $tenantId) : null;
$paymentEntries = $participant !== null