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 */ 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";