prepare('SELECT id FROM tenants WHERE slug = ?'); $stmt->execute([$slug]); $tenantId = $stmt->fetchColumn(); if ($tenantId === false) { throw new RuntimeException("Tenant '{$slug}' was not found. Run scripts/backfill-default-tenant.php first."); } return (int)$tenantId; } function m4_count_missing_payment_participants(PDO $pdo, int $tenantId): int { $stmt = $pdo->prepare( 'SELECT COUNT(*) FROM kl_Einzahlungen e LEFT JOIN participants p ON p.tenant_id = ? AND p.legacy_mitarbeiter_id = e.MitarbeiterID WHERE p.id IS NULL' ); $stmt->execute([$tenantId]); return (int)$stmt->fetchColumn(); } function m4_count_missing_consumption_participants(PDO $pdo, int $tenantId): int { $stmt = $pdo->prepare( 'SELECT COUNT(*) FROM kl_Kaffeeverbrauch v LEFT JOIN participants p ON p.tenant_id = ? AND p.legacy_mitarbeiter_id = v.MitarbeiterID WHERE p.id IS NULL' ); $stmt->execute([$tenantId]); return (int)$stmt->fetchColumn(); } $pdo = dev_pdo(); $applied = dev_apply_migrations($pdo); $tenantSlug = m4_env('M4_DEFAULT_TENANT_SLUG', m4_env('M3_DEFAULT_TENANT_SLUG', 'default')); $stats = [ 'applied_migrations' => count($applied), 'stale_payments_removed' => 0, 'stale_consumptions_removed' => 0, 'payments_mirrored' => 0, 'consumptions_mirrored' => 0, ]; try { $tenantId = m4_fetch_tenant_id($pdo, $tenantSlug); $missingPayments = m4_count_missing_payment_participants($pdo, $tenantId); $missingConsumptions = m4_count_missing_consumption_participants($pdo, $tenantId); if ($missingPayments > 0 || $missingConsumptions > 0) { throw new RuntimeException( "Missing participant mappings for {$missingPayments} payment(s) and {$missingConsumptions} consumption(s)." ); } $pdo->beginTransaction(); $removeStalePayments = $pdo->prepare( "DELETE le 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" ); $removeStalePayments->execute([$tenantId]); $stats['stale_payments_removed'] = $removeStalePayments->rowCount(); $removeStaleConsumptions = $pdo->prepare( "DELETE le 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" ); $removeStaleConsumptions->execute([$tenantId]); $stats['stale_consumptions_removed'] = $removeStaleConsumptions->rowCount(); $insertPayments = $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', NULL, 'kl_Einzahlungen', e.EinzahlungsID FROM kl_Einzahlungen e JOIN participants p ON p.tenant_id = ? AND p.legacy_mitarbeiter_id = e.MitarbeiterID 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)" ); $insertPayments->execute([$tenantId]); $insertConsumptions = $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 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)" ); $insertConsumptions->execute([$tenantId]); $stmt = $pdo->prepare( "SELECT COUNT(*) FROM ledger_entries WHERE tenant_id = ? AND legacy_table = 'kl_Einzahlungen'" ); $stmt->execute([$tenantId]); $stats['payments_mirrored'] = (int)$stmt->fetchColumn(); $stmt = $pdo->prepare( "SELECT COUNT(*) FROM ledger_entries WHERE tenant_id = ? AND legacy_table = 'kl_Kaffeeverbrauch'" ); $stmt->execute([$tenantId]); $stats['consumptions_mirrored'] = (int)$stmt->fetchColumn(); $pdo->commit(); } catch (Throwable $e) { if ($pdo->inTransaction()) { $pdo->rollBack(); } fwrite(STDERR, "M4 ledger backfill failed: {$e->getMessage()}\n"); exit(1); } echo "M4 ledger backfill complete.\n"; echo "Tenant: {$tenantSlug}\n"; echo "Applied migrations: {$stats['applied_migrations']}\n"; echo "Stale payments removed: {$stats['stale_payments_removed']}\n"; echo "Stale consumptions removed: {$stats['stale_consumptions_removed']}\n"; echo "Payments mirrored: {$stats['payments_mirrored']}\n"; echo "Consumptions mirrored: {$stats['consumptions_mirrored']}\n";