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:
+9
-23
@@ -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
@@ -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'
|
||||
|
||||
@@ -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
@@ -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++;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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>";
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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++;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user