Files
kaffeekasse-saas/scripts/check-m4-ledger-migration.php
T
clemens e4b0c81b48 M4 Ledger-Migration fuer Legacy-Buchungen starten
- ledger_entries als tenant-sichere Buchungstabelle ergaenzen
- Legacy-Einzahlungen und Kaffeeverbrauch idempotent spiegeln
- M4-Paritaetscheck und Datenmigrationsdoku ergaenzen
2026-07-13 20:45:28 +02:00

210 lines
7.4 KiB
PHP

<?php
declare(strict_types=1);
require __DIR__ . '/dev-db.php';
require __DIR__ . '/golden-master-data.php';
function m4_check_env(string $name, string $default): string
{
$value = getenv($name);
if ($value === false || trim($value) === '') {
return $default;
}
return trim($value);
}
function m4_check_assert_equal(string $label, mixed $expected, mixed $actual, array &$failures, int &$passes): void
{
if ($expected === $actual) {
$passes++;
echo "PASS {$label}\n";
return;
}
$failures[] = "{$label}: expected " . var_export($expected, true) . ', got ' . var_export($actual, true);
echo "FAIL {$label}\n";
}
function m4_check_fetch_tenant_id(PDO $pdo, string $slug): int
{
$stmt = $pdo->prepare('SELECT id FROM tenants WHERE slug = ?');
$stmt->execute([$slug]);
$tenantId = $stmt->fetchColumn();
if ($tenantId === false) {
throw new RuntimeException("Tenant '{$slug}' not found.");
}
return (int)$tenantId;
}
/**
* @return array<string, array{total_payments: int, total_costs: int, total_marks: int, balance: int, year_payments: int, year_costs: int, year_marks: int}>
*/
function m4_check_fetch_ledger_summaries(PDO $pdo, int $tenantId, array $emails): array
{
$placeholders = implode(',', array_fill(0, count($emails), '?'));
$year = gm_current_year();
$sql = "
SELECT
p.email_norm AS email,
COALESCE(SUM(CASE WHEN le.type = 'payment' THEN le.amount_cents ELSE 0 END), 0) AS total_payments,
COALESCE(SUM(CASE WHEN le.type = 'consumption' THEN -le.amount_cents ELSE 0 END), 0) AS total_costs,
COALESCE(SUM(CASE WHEN le.type = 'consumption' THEN le.marks_count ELSE 0 END), 0) AS total_marks,
COALESCE(SUM(le.amount_cents), 0) AS balance,
COALESCE(SUM(CASE WHEN le.type = 'payment' AND YEAR(le.booked_at) = ? THEN le.amount_cents ELSE 0 END), 0) AS year_payments,
COALESCE(SUM(CASE WHEN le.type = 'consumption' AND YEAR(le.booked_at) = ? THEN -le.amount_cents ELSE 0 END), 0) AS year_costs,
COALESCE(SUM(CASE WHEN le.type = 'consumption' AND YEAR(le.booked_at) = ? THEN le.marks_count ELSE 0 END), 0) AS year_marks
FROM participants p
LEFT JOIN ledger_entries le
ON le.tenant_id = p.tenant_id
AND le.participant_id = p.id
AND le.voided_at IS NULL
WHERE p.tenant_id = ?
AND p.email_norm IN ({$placeholders})
GROUP BY p.id, p.email_norm
";
$stmt = $pdo->prepare($sql);
$stmt->execute(array_merge([$year, $year, $year, $tenantId], $emails));
$summaries = [];
while ($row = $stmt->fetch()) {
$summaries[(string)$row['email']] = [
'total_payments' => (int)$row['total_payments'],
'total_costs' => (int)$row['total_costs'],
'total_marks' => (int)$row['total_marks'],
'balance' => (int)$row['balance'],
'year_payments' => (int)$row['year_payments'],
'year_costs' => (int)$row['year_costs'],
'year_marks' => (int)$row['year_marks'],
];
}
return $summaries;
}
$pdo = dev_pdo();
$tenantSlug = m4_check_env('M4_DEFAULT_TENANT_SLUG', m4_check_env('M3_DEFAULT_TENANT_SLUG', 'default'));
$tenantId = m4_check_fetch_tenant_id($pdo, $tenantSlug);
$expected = gm_expected_summaries();
$emails = array_keys($expected);
$failures = [];
$passes = 0;
$legacyPaymentCount = (int)$pdo->query('SELECT COUNT(*) FROM kl_Einzahlungen')->fetchColumn();
$legacyConsumptionCount = (int)$pdo->query('SELECT COUNT(*) FROM kl_Kaffeeverbrauch')->fetchColumn();
$stmt = $pdo->prepare(
"SELECT COUNT(*)
FROM ledger_entries
WHERE tenant_id = ?
AND legacy_table IN ('kl_Einzahlungen', 'kl_Kaffeeverbrauch')"
);
$stmt->execute([$tenantId]);
$ledgerLegacyCount = (int)$stmt->fetchColumn();
m4_check_assert_equal(
'legacy ledger row count matches source rows',
$legacyPaymentCount + $legacyConsumptionCount,
$ledgerLegacyCount,
$failures,
$passes
);
$stmt = $pdo->prepare(
"SELECT
COUNT(*) AS total_rows,
COUNT(DISTINCT CONCAT(legacy_table, '#', legacy_id)) AS distinct_legacy_rows
FROM ledger_entries
WHERE tenant_id = ?
AND legacy_table IN ('kl_Einzahlungen', 'kl_Kaffeeverbrauch')"
);
$stmt->execute([$tenantId]);
$legacyDistinct = $stmt->fetch();
m4_check_assert_equal(
'legacy ledger ids are unique',
(int)$legacyDistinct['total_rows'],
(int)$legacyDistinct['distinct_legacy_rows'],
$failures,
$passes
);
$stmt = $pdo->prepare(
"SELECT COUNT(*)
FROM ledger_entries le
LEFT JOIN kl_Einzahlungen e ON e.EinzahlungsID = le.legacy_id
WHERE le.tenant_id = ?
AND le.legacy_table = 'kl_Einzahlungen'
AND e.EinzahlungsID IS NULL"
);
$stmt->execute([$tenantId]);
m4_check_assert_equal('no stale payment ledger rows', 0, (int)$stmt->fetchColumn(), $failures, $passes);
$stmt = $pdo->prepare(
"SELECT COUNT(*)
FROM ledger_entries le
LEFT JOIN kl_Kaffeeverbrauch v ON v.VerbrauchID = le.legacy_id
WHERE le.tenant_id = ?
AND le.legacy_table = 'kl_Kaffeeverbrauch'
AND v.VerbrauchID IS NULL"
);
$stmt->execute([$tenantId]);
m4_check_assert_equal('no stale consumption ledger rows', 0, (int)$stmt->fetchColumn(), $failures, $passes);
$stmt = $pdo->prepare("SELECT COUNT(*) FROM ledger_entries WHERE tenant_id = ? AND type = 'payment' AND amount_cents <= 0");
$stmt->execute([$tenantId]);
m4_check_assert_equal('payment amounts are positive', 0, (int)$stmt->fetchColumn(), $failures, $passes);
$stmt = $pdo->prepare("SELECT COUNT(*) FROM ledger_entries WHERE tenant_id = ? AND type = 'consumption' AND amount_cents >= 0");
$stmt->execute([$tenantId]);
m4_check_assert_equal('consumption amounts are negative', 0, (int)$stmt->fetchColumn(), $failures, $passes);
$stmt = $pdo->prepare(
"SELECT COUNT(*)
FROM ledger_entries
WHERE tenant_id = ?
AND type = 'consumption'
AND (marks_count IS NULL OR unit_price_cents IS NULL)"
);
$stmt->execute([$tenantId]);
m4_check_assert_equal('consumption rows keep marks and unit price', 0, (int)$stmt->fetchColumn(), $failures, $passes);
$webLegacyCount = (int)$pdo
->query('SELECT COUNT(*) FROM kl_Kaffeeverbrauch WHERE Eintragsart = 2')
->fetchColumn();
$stmt = $pdo->prepare(
"SELECT COUNT(*)
FROM ledger_entries
WHERE tenant_id = ?
AND legacy_table = 'kl_Kaffeeverbrauch'
AND source = 'legacy_web'"
);
$stmt->execute([$tenantId]);
m4_check_assert_equal('web consumption source is preserved', $webLegacyCount, (int)$stmt->fetchColumn(), $failures, $passes);
$actualSummaries = m4_check_fetch_ledger_summaries($pdo, $tenantId, $emails);
m4_check_assert_equal('ledger member summary count', count($expected), count($actualSummaries), $failures, $passes);
foreach ($expected as $email => $summary) {
m4_check_assert_equal("ledger summary exists {$email}", true, isset($actualSummaries[$email]), $failures, $passes);
if (!isset($actualSummaries[$email])) {
continue;
}
foreach ($summary as $key => $expectedValue) {
m4_check_assert_equal("ledger {$email} {$key}", $expectedValue, $actualSummaries[$email][$key], $failures, $passes);
}
}
if ($failures !== []) {
echo "\nM4 ledger migration check failed with " . count($failures) . " failure(s):\n";
foreach ($failures as $failure) {
echo "- {$failure}\n";
}
exit(1);
}
echo "\nM4 ledger migration check passed with {$passes} assertions.\n";