From 4acd4a2d40e46872fa346e92809ea7d56749e4fc Mon Sep 17 00:00:00 2001 From: Clemens Creutzburg Date: Wed, 22 Jul 2026 08:31:08 +0200 Subject: [PATCH] Legacy-Abbau Schritt 3+4: kl_*-Tabellen und legacy_*-Spalten entfernt Abschluss des Legacy-Abbaus. Die Migration in participants/ledger_entries war laut Dev-DB vollstaendig (keine legacy_mitarbeiter_id, kein legacy_table), die kl_*-Tabellen enthielten nur noch Golden-Master-Testfixtures. Da kein echter Altbestand mehr importiert werden muss (Prod entsteht aus der jetzigen Dev-DB), kommt die Altwelt komplett raus. Laufzeit-Code (Schritt 3a): - ledger.php: Option legacy_mitarbeiter_ids, die Spalten aus allen SELECTs und Rueckgaben, ledger_fetch_participant_summary_by_legacy_id sowie das Loeschen gespiegelter Legacy-Zeilen in ledger_void_entry/ledger_void_own_self_entry entfernt - imports.php, paypal-inbox.php: legacy_mitarbeiter_id aus Ergebnis und Typannotationen entfernt - ledger-preview.php: Spalte "Legacy" entfernt Skripte (Schritt 3b/3c): - die acht reinen Legacy-/Golden-Master-Skripte entfernt (backfill-*, seed-golden-master, golden-master-data, check-golden-master, check-m4-*, check-m3-saas-basis) - init-mysql-dev.php legt jetzt einen SaaS-Mandanten mit Owner-Login, Teilnehmer, Journalbuchungen und Hinweis an statt kl_*-Zeilen Schema (Schritt 4, Migration 0024): - participants.legacy_mitarbeiter_id + uq_participants_tenant_legacy - ledger_entries.legacy_table/legacy_id + uq_ledger_entries_tenant_legacy - DROP der Tabellen kl_Einzahlungen, kl_Kaffeeverbrauch, kl_hinweise, kl_config, kl_Mitarbeiter Verifikation unveraendert gruen: http-smoke 33/0, role-matrix 55/0, tenant-isolation 11/1 (Altfehler), m3-auth/tenant-resolution/billing gruen. check-m3-settings-flow 7/6 ist vorbestehend (schon bei 3e24910 rot, mit spaeteren Settings-Feldern nicht synchron) und unabhaengig von diesem Abbau. Co-Authored-By: Claude Opus 4.8 --- app/imports.php | 7 +- app/ledger.php | 122 ++-------- app/paypal-inbox.php | 2 +- database/migrations/0024_drop_legacy.sql | 22 ++ ledger-preview.php | 10 - scripts/backfill-default-tenant.php | 236 -------------------- scripts/backfill-ledger-entries.php | 212 ------------------ scripts/check-golden-master.php | 191 ---------------- scripts/check-m3-saas-basis.php | 111 ---------- scripts/check-m4-ledger-migration.php | 215 ------------------ scripts/check-m4-ledger-service.php | 212 ------------------ scripts/golden-master-data.php | 269 ----------------------- scripts/init-mysql-dev.php | 82 ++++--- scripts/seed-golden-master.php | 129 ----------- 14 files changed, 105 insertions(+), 1715 deletions(-) create mode 100644 database/migrations/0024_drop_legacy.sql delete mode 100644 scripts/backfill-default-tenant.php delete mode 100644 scripts/backfill-ledger-entries.php delete mode 100644 scripts/check-golden-master.php delete mode 100644 scripts/check-m3-saas-basis.php delete mode 100644 scripts/check-m4-ledger-migration.php delete mode 100644 scripts/check-m4-ledger-service.php delete mode 100644 scripts/golden-master-data.php delete mode 100644 scripts/seed-golden-master.php diff --git a/app/imports.php b/app/imports.php index 06e0c98..d3a4a67 100644 --- a/app/imports.php +++ b/app/imports.php @@ -23,9 +23,9 @@ function imports_normalize_amount(string $raw): ?int /** * Matches a CSV name against a participant's paypal_name or display_name - * (case-insensitive), the same rule the legacy CSV import used. + * (case-insensitive). * - * @return array{participant_id: int, legacy_mitarbeiter_id: ?int, display_name: string}|null + * @return array{participant_id: int, display_name: string}|null */ function imports_find_participant(PDO $pdo, int $tenantId, string $name): ?array { @@ -35,7 +35,7 @@ function imports_find_participant(PDO $pdo, int $tenantId, string $name): ?array } $stmt = $pdo->prepare( - 'SELECT id, legacy_mitarbeiter_id, display_name + 'SELECT id, display_name FROM participants WHERE tenant_id = ? AND (LOWER(paypal_name) = LOWER(?) OR LOWER(display_name) = LOWER(?)) @@ -50,7 +50,6 @@ function imports_find_participant(PDO $pdo, int $tenantId, string $name): ?array return [ 'participant_id' => (int)$row['id'], - 'legacy_mitarbeiter_id' => $row['legacy_mitarbeiter_id'] !== null ? (int)$row['legacy_mitarbeiter_id'] : null, 'display_name' => (string)$row['display_name'], ]; } diff --git a/app/ledger.php b/app/ledger.php index 5a89a3a..36a809a 100644 --- a/app/ledger.php +++ b/app/ledger.php @@ -166,10 +166,9 @@ function ledger_fetch_tenant_totals(PDO $pdo, int $tenantId, ?int $year = null): * - year: int * - active_only: bool|null * - participant_ids: list - * - legacy_mitarbeiter_ids: list * - email_norms: list * - * @return list + * @return list */ function ledger_fetch_participant_summaries(PDO $pdo, int $tenantId, array $options = []): array { @@ -193,17 +192,6 @@ function ledger_fetch_participant_summaries(PDO $pdo, int $tenantId, array $opti array_push($params, ...$participantIds); } - if (array_key_exists('legacy_mitarbeiter_ids', $options)) { - $legacyIds = is_array($options['legacy_mitarbeiter_ids']) - ? ledger_normalize_ids($options['legacy_mitarbeiter_ids']) - : []; - if ($legacyIds === []) { - return []; - } - $where[] = 'p.legacy_mitarbeiter_id IN (' . implode(',', array_fill(0, count($legacyIds), '?')) . ')'; - array_push($params, ...$legacyIds); - } - if (array_key_exists('email_norms', $options)) { $emailNorms = is_array($options['email_norms']) ? ledger_normalize_email_norms($options['email_norms']) @@ -230,7 +218,6 @@ function ledger_fetch_participant_summaries(PDO $pdo, int $tenantId, array $opti SELECT p.id AS participant_id, p.tenant_id, - p.legacy_mitarbeiter_id, p.display_name, p.email, p.email_norm, @@ -249,7 +236,7 @@ function ledger_fetch_participant_summaries(PDO $pdo, int $tenantId, array $opti AND le.participant_id = p.id AND le.voided_at IS NULL WHERE " . implode(' AND ', $where) . " - GROUP BY p.id, p.tenant_id, p.legacy_mitarbeiter_id, p.display_name, p.email, p.email_norm, p.paypal_name, p.active + GROUP BY p.id, p.tenant_id, p.display_name, p.email, p.email_norm, p.paypal_name, p.active ORDER BY p.display_name, p.id"; $stmt = $pdo->prepare($sql); @@ -260,7 +247,6 @@ function ledger_fetch_participant_summaries(PDO $pdo, int $tenantId, array $opti $summaries[] = [ 'participant_id' => (int)$row['participant_id'], 'tenant_id' => (int)$row['tenant_id'], - 'legacy_mitarbeiter_id' => $row['legacy_mitarbeiter_id'] !== null ? (int)$row['legacy_mitarbeiter_id'] : null, 'display_name' => (string)$row['display_name'], 'email' => $row['email'] !== null ? (string)$row['email'] : null, 'email_norm' => $row['email_norm'] !== null ? (string)$row['email_norm'] : null, @@ -280,7 +266,7 @@ function ledger_fetch_participant_summaries(PDO $pdo, int $tenantId, array $opti } /** - * @return array{participant_id: int, tenant_id: int, legacy_mitarbeiter_id: ?int, display_name: string, email: ?string, email_norm: ?string, paypal_name: ?string, active: bool, total_payments_cents: int, total_costs_cents: int, total_marks: int, balance_cents: int, year_payments_cents: int, year_costs_cents: int, year_marks: int}|null + * @return array{participant_id: int, tenant_id: int, display_name: string, email: ?string, email_norm: ?string, paypal_name: ?string, active: bool, total_payments_cents: int, total_costs_cents: int, total_marks: int, balance_cents: int, year_payments_cents: int, year_costs_cents: int, year_marks: int}|null */ function ledger_fetch_participant_summary(PDO $pdo, int $tenantId, int $participantId, ?int $year = null): ?array { @@ -292,19 +278,6 @@ function ledger_fetch_participant_summary(PDO $pdo, int $tenantId, int $particip return $summaries[0] ?? null; } -/** - * @return array{participant_id: int, tenant_id: int, legacy_mitarbeiter_id: ?int, display_name: string, email: ?string, email_norm: ?string, paypal_name: ?string, active: bool, total_payments_cents: int, total_costs_cents: int, total_marks: int, balance_cents: int, year_payments_cents: int, year_costs_cents: int, year_marks: int}|null - */ -function ledger_fetch_participant_summary_by_legacy_id(PDO $pdo, int $tenantId, int $legacyMitarbeiterId, ?int $year = null): ?array -{ - $summaries = ledger_fetch_participant_summaries($pdo, $tenantId, [ - 'legacy_mitarbeiter_ids' => [$legacyMitarbeiterId], - 'year' => $year ?? ledger_current_year(), - ]); - - return $summaries[0] ?? null; -} - function ledger_is_default_tenant(PDO $pdo, int $tenantId): bool { $tenant = ledger_fetch_default_tenant($pdo); @@ -313,7 +286,7 @@ function ledger_is_default_tenant(PDO $pdo, int $tenantId): bool } /** - * @return list + * @return list */ function ledger_fetch_participants_for_admin(PDO $pdo, int $tenantId): array { @@ -325,7 +298,6 @@ function ledger_fetch_participants_for_admin(PDO $pdo, int $tenantId): array p.email_norm, p.paypal_name, p.active, - p.legacy_mitarbeiter_id, p.user_id, tm.role, tm.status AS membership_status @@ -347,7 +319,6 @@ function ledger_fetch_participants_for_admin(PDO $pdo, int $tenantId): array 'email_norm' => $row['email_norm'] !== null ? (string)$row['email_norm'] : null, 'paypal_name' => $row['paypal_name'] !== null ? (string)$row['paypal_name'] : null, 'active' => (int)$row['active'] === 1, - 'legacy_mitarbeiter_id' => $row['legacy_mitarbeiter_id'] !== null ? (int)$row['legacy_mitarbeiter_id'] : null, 'user_id' => $row['user_id'] !== null ? (int)$row['user_id'] : null, 'role' => $row['role'] !== null ? (string)$row['role'] : null, 'membership_status' => $row['membership_status'] !== null ? (string)$row['membership_status'] : null, @@ -426,17 +397,12 @@ function ledger_anonymize_participant(PDO $pdo, int $tenantId, int $participantI return $stmt->rowCount() > 0; } -/** - * Marks the ledger entry mirrored from a legacy row as voided instead of - * deleting it, so corrections stay auditable. Idempotent: voiding an - * already-voided or missing entry is a no-op and returns false. - */ /** * Storniert einen Journaleintrag anhand seiner Ledger-ID - immer auf den * Mandanten eingegrenzt, damit ueber eine untergeschobene ID nicht in fremden - * Mandanten gebucht werden kann. Haengt am Eintrag noch eine Legacy-Zeile, - * wird sie mitgeloescht, damit der Mirror den Eintrag nicht wieder auferstehen - * laesst. + * Mandanten storniert werden kann. Markiert den Eintrag als storniert statt + * ihn zu loeschen, damit Korrekturen nachvollziehbar bleiben. Idempotent: ein + * bereits stornierter oder fehlender Eintrag ergibt false. * * @param string|null $expectedType Absichern, dass z. B. ueber das * Einzahlungs-Formular kein Strich storniert @@ -444,15 +410,8 @@ function ledger_anonymize_participant(PDO $pdo, int $tenantId, int $participantI */ function ledger_void_entry(PDO $pdo, int $tenantId, int $entryId, ?string $expectedType = null): bool { - // Nur bekannte Legacy-Tabellen, damit der Tabellenname nie ungeprueft in - // ein DELETE wandert. - $legacyKeys = [ - 'kl_Einzahlungen' => 'EinzahlungsID', - 'kl_Kaffeeverbrauch' => 'VerbrauchID', - ]; - $stmt = $pdo->prepare( - "SELECT id, type, legacy_table, legacy_id + "SELECT id, type FROM ledger_entries WHERE id = ? AND tenant_id = ? AND voided_at IS NULL LIMIT 1" @@ -467,51 +426,30 @@ function ledger_void_entry(PDO $pdo, int $tenantId, int $entryId, ?string $expec return false; } - try { - $pdo->beginTransaction(); + $stmt = $pdo->prepare('UPDATE ledger_entries SET voided_at = NOW() WHERE id = ? AND tenant_id = ? AND voided_at IS NULL'); + $stmt->execute([$entryId, $tenantId]); - $pdo->prepare('UPDATE ledger_entries SET voided_at = NOW() WHERE id = ? AND tenant_id = ? AND voided_at IS NULL') - ->execute([$entryId, $tenantId]); - - $legacyTable = $entry['legacy_table'] !== null ? (string)$entry['legacy_table'] : null; - if ($legacyTable !== null && $entry['legacy_id'] !== null && isset($legacyKeys[$legacyTable])) { - $pdo->prepare("DELETE FROM {$legacyTable} WHERE {$legacyKeys[$legacyTable]} = ?") - ->execute([(int)$entry['legacy_id']]); - } - - $pdo->commit(); - } catch (Throwable $e) { - if ($pdo->inTransaction()) { - $pdo->rollBack(); - } - - return false; - } - - return true; + return $stmt->rowCount() > 0; } /** * Storniert den juengsten, noch nicht stornierten Web-Selbsteintrag eines - * Teilnehmers (Quelle 'self_entry' bei SaaS-nativen Mandanten bzw. 'legacy_web' - * beim migrierten Default-Mandanten). Bewusst nur eigene Web-Eintraege - vom - * Kassenwart per Sammelerfassung gesetzte Striche ('manual_bulk'/'legacy_manual') - * bleiben unangetastet. Beim Default-Mandanten wird zusaetzlich die - * kl_Kaffeeverbrauch-Zeile geloescht, damit sie nicht erneut gespiegelt wird - * und auf den Legacy-Sammelseiten verschwindet. + * Teilnehmers (Quelle 'self_entry'). Bewusst nur eigene Web-Eintraege - vom + * Kassenwart per Sammelerfassung gesetzte Striche ('manual_bulk') bleiben + * unangetastet. * * @return array{ok: bool, marks?: int, error?: string} */ function ledger_void_own_self_entry(PDO $pdo, int $tenantId, int $participantId): array { $stmt = $pdo->prepare( - "SELECT id, marks_count, legacy_table, legacy_id + "SELECT id, marks_count FROM ledger_entries WHERE tenant_id = ? AND participant_id = ? AND type = 'consumption' AND voided_at IS NULL - AND source IN ('self_entry', 'legacy_web') + AND source = 'self_entry' ORDER BY booked_at DESC, id DESC LIMIT 1" ); @@ -525,11 +463,6 @@ function ledger_void_own_self_entry(PDO $pdo, int $tenantId, int $participantId) try { $pdo->beginTransaction(); - if ($entry['legacy_table'] === 'kl_Kaffeeverbrauch' && $entry['legacy_id'] !== null) { - $pdo->prepare('DELETE FROM kl_Kaffeeverbrauch WHERE VerbrauchID = ?') - ->execute([(int)$entry['legacy_id']]); - } - $pdo->prepare("UPDATE ledger_entries SET voided_at = NOW() WHERE id = ? AND voided_at IS NULL") ->execute([(int)$entry['id']]); @@ -546,9 +479,7 @@ function ledger_void_own_self_entry(PDO $pdo, int $tenantId, int $participantId) } /** - * Records a consumption entry straight into the ledger, without any legacy - * kl_Kaffeeverbrauch row. Used for tenants that have no legacy shadow table - * (every tenant except the migrated default tenant). + * Records a consumption entry straight into the ledger. */ function ledger_record_consumption(PDO $pdo, int $tenantId, int $participantId, int $marks, int $unitPriceCents, string $source, ?int $createdByUserId = null): int { @@ -568,10 +499,6 @@ function ledger_record_consumption(PDO $pdo, int $tenantId, int $participantId, return (int)$pdo->lastInsertId(); } -/** - * Records a payment entry straight into the ledger, without any legacy - * kl_Einzahlungen row. Used for tenants that have no legacy shadow table. - */ /** * Bucht eine Einzahlung. $amountCents darf negativ sein (Abzug, Auszahlung, * Korrektur) - fuer Tippfehler ist aber ledger_void_entry der richtige Weg, @@ -597,12 +524,11 @@ function ledger_record_payment(PDO $pdo, int $tenantId, int $participantId, int } /** - * Ledger-native equivalent of the legacy "100-Tage-Liste" front/back split: - * active participants whose summed marks within the trailing window are at - * or above the ten-mark threshold ($atLeastTen), or below it. Used for - * tenants without a legacy kl_Kaffeeverbrauch history to filter against. + * "100-Tage-Liste" Vorder-/Rueckseiten-Aufteilung: aktive Teilnehmer, deren + * summierte Striche im nachlaufenden Fenster die Zehner-Schwelle erreichen + * ($atLeastTen) oder darunter liegen. * - * @return list + * @return list */ function ledger_fetch_participants_by_window_marks(PDO $pdo, int $tenantId, int $windowDays, bool $atLeastTen): array { @@ -637,7 +563,7 @@ function ledger_fetch_participants_by_window_marks(PDO $pdo, int $tenantId, int * - include_voided: bool * - limit: int * - * @return list + * @return list */ /** * Verbrauch eines Teilnehmers pro Monat eines Jahres (nur nicht stornierte @@ -707,8 +633,6 @@ function ledger_fetch_recent_entries(PDO $pdo, int $tenantId, array $options = [ le.booked_at, le.source, le.note, - le.legacy_table, - le.legacy_id, le.voided_at FROM ledger_entries le JOIN participants p @@ -735,8 +659,6 @@ function ledger_fetch_recent_entries(PDO $pdo, int $tenantId, array $options = [ 'booked_at' => (string)$row['booked_at'], 'source' => (string)$row['source'], 'note' => $row['note'] !== null ? (string)$row['note'] : null, - 'legacy_table' => $row['legacy_table'] !== null ? (string)$row['legacy_table'] : null, - 'legacy_id' => $row['legacy_id'] !== null ? (int)$row['legacy_id'] : null, 'voided_at' => $row['voided_at'] !== null ? (string)$row['voided_at'] : null, ]; } diff --git a/app/paypal-inbox.php b/app/paypal-inbox.php index 6b20aff..3168c48 100644 --- a/app/paypal-inbox.php +++ b/app/paypal-inbox.php @@ -128,7 +128,7 @@ function paypal_booking_note(array $payment): string * Zahlernamen (paypal_name/display_name), sonst ueber die Mitteilung (Mitglied * schreibt dort teils seinen Namen). Liefert null, wenn nicht eindeutig. * - * @return array{participant_id:int, legacy_mitarbeiter_id:?int, display_name:string}|null + * @return array{participant_id:int, display_name:string}|null */ function paypal_match_participant(PDO $pdo, int $tenantId, string $payerName, ?string $note): ?array { diff --git a/database/migrations/0024_drop_legacy.sql b/database/migrations/0024_drop_legacy.sql new file mode 100644 index 0000000..f248666 --- /dev/null +++ b/database/migrations/0024_drop_legacy.sql @@ -0,0 +1,22 @@ +-- Legacy-Abbau Schritt 4: Altwelt vollstaendig entfernen. +-- +-- Voraussetzung: kein Laufzeit-Code liest oder schreibt mehr die kl_*-Tabellen +-- oder die legacy_*-Spalten (Schritte 1-3). Die Migration ist forward-only. + +-- participants: Verknuepfung zur alten Mitarbeitertabelle entfernen. +ALTER TABLE participants + DROP INDEX uq_participants_tenant_legacy, + DROP COLUMN legacy_mitarbeiter_id; + +-- ledger_entries: Herkunftszeiger auf die Legacy-Buchungstabellen entfernen. +ALTER TABLE ledger_entries + DROP INDEX uq_ledger_entries_tenant_legacy, + DROP COLUMN legacy_table, + DROP COLUMN legacy_id; + +-- Die alten Windows/IIS-Tabellen selbst. +DROP TABLE IF EXISTS kl_Einzahlungen; +DROP TABLE IF EXISTS kl_Kaffeeverbrauch; +DROP TABLE IF EXISTS kl_hinweise; +DROP TABLE IF EXISTS kl_config; +DROP TABLE IF EXISTS kl_Mitarbeiter; diff --git a/ledger-preview.php b/ledger-preview.php index 3179649..5c4e508 100644 --- a/ledger-preview.php +++ b/ledger-preview.php @@ -59,7 +59,6 @@ include 'nav.php'; Read-only Vorschau aus ledger_entries für (). - Die bestehenden App-Seiten schreiben weiterhin in die Legacy-Tabellen.

Tenant-Summen

@@ -117,7 +116,6 @@ include 'nav.php'; Betrag Striche Quelle - Legacy @@ -127,14 +125,6 @@ include 'nav.php'; - - - diff --git a/scripts/backfill-default-tenant.php b/scripts/backfill-default-tenant.php deleted file mode 100644 index 6626711..0000000 --- a/scripts/backfill-default-tenant.php +++ /dev/null @@ -1,236 +0,0 @@ -prepare('SELECT id FROM tenants WHERE slug = ?'); - $stmt->execute([$slug]); - $tenantId = $stmt->fetchColumn(); - - if ($tenantId === false) { - throw new RuntimeException("Tenant '{$slug}' was not created."); - } - - return (int)$tenantId; -} - -function m3_upsert_user(PDO $pdo, string $email, string $emailNorm, string $displayName): int -{ - $stmt = $pdo->prepare( - 'INSERT INTO users (email, email_norm, display_name, status) - VALUES (?, ?, ?, ?) - ON DUPLICATE KEY UPDATE - email = VALUES(email), - display_name = VALUES(display_name), - status = VALUES(status)' - ); - $stmt->execute([$email, $emailNorm, $displayName, 'active']); - - $stmt = $pdo->prepare('SELECT id FROM users WHERE email_norm = ?'); - $stmt->execute([$emailNorm]); - $userId = $stmt->fetchColumn(); - - if ($userId === false) { - throw new RuntimeException("User '{$emailNorm}' was not created."); - } - - return (int)$userId; -} - -$pdo = dev_pdo(); -$applied = dev_apply_migrations($pdo); - -$tenantSlug = m3_env('M3_DEFAULT_TENANT_SLUG', 'default'); -$tenantName = m3_env('M3_DEFAULT_TENANT_NAME', 'Kaffeeliste Bestand'); -$ownerEmailNorm = m3_email_norm(getenv('M3_DEFAULT_TENANT_OWNER_EMAIL') ?: null); - -$stats = [ - 'applied_migrations' => count($applied), - 'participants' => 0, - 'stale_participants_removed' => 0, - 'users' => 0, - 'memberships' => 0, -]; - -$pdo->beginTransaction(); - -try { - $stmt = $pdo->prepare( - 'INSERT INTO tenants (slug, name, status, timezone, locale, currency_code) - VALUES (?, ?, ?, ?, ?, ?) - ON DUPLICATE KEY UPDATE - name = VALUES(name), - status = VALUES(status), - timezone = VALUES(timezone), - locale = VALUES(locale), - currency_code = VALUES(currency_code)' - ); - $stmt->execute([$tenantSlug, $tenantName, 'active', 'Europe/Berlin', 'de-DE', 'EUR']); - $tenantId = m3_fetch_tenant_id($pdo, $tenantSlug); - - $config = $pdo->query( - 'SELECT KostenproStrich, paypaluse, paypallink, strichperweb - FROM kl_config - ORDER BY id - LIMIT 1' - )->fetch() ?: [ - 'KostenproStrich' => '0.20', - 'paypaluse' => 0, - 'paypallink' => '', - 'strichperweb' => 1, - ]; - - $stmt = $pdo->prepare( - 'INSERT INTO tenant_settings - (tenant_id, mark_price_cents, self_entry_enabled, paypal_enabled, paypal_url_template) - VALUES (?, ?, ?, ?, ?) - ON DUPLICATE KEY UPDATE - mark_price_cents = VALUES(mark_price_cents), - self_entry_enabled = VALUES(self_entry_enabled), - paypal_enabled = VALUES(paypal_enabled), - paypal_url_template = VALUES(paypal_url_template)' - ); - $stmt->execute([ - $tenantId, - m3_decimal_to_cents($config['KostenproStrich']), - (int)$config['strichperweb'] === 1 ? 1 : 0, - (int)$config['paypaluse'] === 1 ? 1 : 0, - (string)$config['paypallink'], - ]); - - $members = $pdo->query( - 'SELECT MitarbeiterID, Name, Email, paypalname, aktiv, admin - FROM kl_Mitarbeiter - ORDER BY MitarbeiterID' - )->fetchAll(); - - $upsertParticipant = $pdo->prepare( - 'INSERT INTO participants - (tenant_id, display_name, email, email_norm, paypal_name, active, legacy_mitarbeiter_id) - VALUES (?, ?, ?, ?, ?, ?, ?) - ON DUPLICATE KEY UPDATE - display_name = VALUES(display_name), - email = VALUES(email), - email_norm = VALUES(email_norm), - paypal_name = VALUES(paypal_name), - active = VALUES(active), - legacy_mitarbeiter_id = VALUES(legacy_mitarbeiter_id)' - ); - $upsertMembership = $pdo->prepare( - 'INSERT INTO tenant_memberships (tenant_id, user_id, role, status, joined_at) - VALUES (?, ?, ?, ?, NOW()) - ON DUPLICATE KEY UPDATE - role = VALUES(role), - status = VALUES(status), - joined_at = COALESCE(tenant_memberships.joined_at, VALUES(joined_at))' - ); - $linkParticipantUser = $pdo->prepare( - 'UPDATE participants - SET user_id = ? - WHERE tenant_id = ? - AND legacy_mitarbeiter_id = ?' - ); - - foreach ($members as $member) { - $email = trim((string)$member['Email']); - $emailNorm = m3_email_norm($email); - $displayName = trim((string)$member['Name']); - if ($displayName === '') { - $displayName = $emailNorm ?? ('Mitarbeiter ' . (int)$member['MitarbeiterID']); - } - - $upsertParticipant->execute([ - $tenantId, - $displayName, - $email !== '' ? $email : null, - $emailNorm, - $member['paypalname'] !== null && trim((string)$member['paypalname']) !== '' - ? trim((string)$member['paypalname']) - : null, - (int)$member['aktiv'] === 1 ? 1 : 0, - (int)$member['MitarbeiterID'], - ]); - $stats['participants']++; - - $isAdmin = (int)$member['admin'] === 1 && (int)$member['aktiv'] === 1; - $isOwner = $ownerEmailNorm !== null && $emailNorm === $ownerEmailNorm && (int)$member['aktiv'] === 1; - if ($emailNorm === null || (!$isAdmin && !$isOwner)) { - continue; - } - - $userId = m3_upsert_user($pdo, $email, $emailNorm, $displayName); - $role = $isOwner ? 'owner' : 'admin'; - $upsertMembership->execute([$tenantId, $userId, $role, 'active']); - $linkParticipantUser->execute([$userId, $tenantId, (int)$member['MitarbeiterID']]); - $stats['users']++; - $stats['memberships']++; - } - - $removeStaleParticipants = $pdo->prepare( - 'DELETE p - FROM participants p - LEFT JOIN kl_Mitarbeiter m - ON m.MitarbeiterID = p.legacy_mitarbeiter_id - WHERE p.tenant_id = ? - AND p.legacy_mitarbeiter_id IS NOT NULL - AND m.MitarbeiterID IS NULL' - ); - $removeStaleParticipants->execute([$tenantId]); - $stats['stale_participants_removed'] = $removeStaleParticipants->rowCount(); - - $pdo->commit(); -} catch (Throwable $e) { - $pdo->rollBack(); - fwrite(STDERR, "M3 default tenant backfill failed: {$e->getMessage()}\n"); - exit(1); -} - -echo "M3 default tenant backfill complete.\n"; -echo "Tenant: {$tenantSlug}\n"; -echo "Applied migrations: {$stats['applied_migrations']}\n"; -echo "Participants mirrored: {$stats['participants']}\n"; -echo "Stale participants removed: {$stats['stale_participants_removed']}\n"; -echo "Users upserted: {$stats['users']}\n"; -echo "Memberships upserted: {$stats['memberships']}\n"; diff --git a/scripts/backfill-ledger-entries.php b/scripts/backfill-ledger-entries.php deleted file mode 100644 index 4f164f8..0000000 --- a/scripts/backfill-ledger-entries.php +++ /dev/null @@ -1,212 +0,0 @@ -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"; diff --git a/scripts/check-golden-master.php b/scripts/check-golden-master.php deleted file mode 100644 index a7008bc..0000000 --- a/scripts/check-golden-master.php +++ /dev/null @@ -1,191 +0,0 @@ -prepare($sql); - $stmt->execute(array_merge([$year, $year], $emails)); - - $actual = []; - while ($row = $stmt->fetch()) { - $actual[$row['email']] = [ - 'total_payments' => gm_decimal_to_cents($row['total_payments']), - 'total_costs' => gm_decimal_to_cents($row['total_costs']), - 'total_marks' => (int)$row['total_marks'], - 'balance' => gm_decimal_to_cents($row['balance']), - 'year_payments' => gm_decimal_to_cents($row['year_payments']), - 'year_costs' => gm_decimal_to_cents($row['year_costs']), - 'year_marks' => (int)$row['year_marks'], - ]; - } - - return $actual; -} - -function gm_fetch_sheet_emails(PDO $pdo, array $emails, string $mode): array -{ - $placeholders = implode(',', array_fill(0, count($emails), '?')); - $stmt = $pdo->prepare( - "SELECT MAX(DATE(v.Datum)) - FROM kl_Kaffeeverbrauch v - JOIN kl_Mitarbeiter m ON m.MitarbeiterID = v.MitarbeiterID - WHERE m.Email IN ({$placeholders}) - AND DATE(v.Datum) < CURDATE()" - ); - $stmt->execute($emails); - $referenceDate = $stmt->fetchColumn() ?: date('Y-m-d'); - - $operator = $mode === 'front' ? '>=' : '<'; - $sql = " - SELECT m.Email AS email - FROM kl_Mitarbeiter m - LEFT JOIN kl_Kaffeeverbrauch v - ON m.MitarbeiterID = v.MitarbeiterID - AND v.Datum >= DATE_ADD(?, INTERVAL -100 DAY) - WHERE m.aktiv = 1 - AND m.Email IN ({$placeholders}) - GROUP BY m.MitarbeiterID, m.Email - HAVING COALESCE(SUM(v.AnzahlStriche), 0) {$operator} 10 - ORDER BY m.Email - "; - $stmt = $pdo->prepare($sql); - $stmt->execute(array_merge([$referenceDate], $emails)); - - return $stmt->fetchAll(PDO::FETCH_COLUMN); -} - -$actualSummaries = gm_fetch_member_summaries($pdo, $emails); -gm_assert_equal('member summary count', count($expected), count($actualSummaries), $failures, $passes); - -foreach ($expected as $email => $summary) { - gm_assert_equal("summary exists {$email}", true, isset($actualSummaries[$email]), $failures, $passes); - if (!isset($actualSummaries[$email])) { - continue; - } - - foreach ($summary as $key => $expectedValue) { - gm_assert_equal("{$email} {$key}", $expectedValue, $actualSummaries[$email][$key], $failures, $passes); - } -} - -$stmt = $pdo->prepare('SELECT Email, aktiv, admin, paypalname FROM kl_Mitarbeiter WHERE Email IN (' . implode(',', array_fill(0, count($emails), '?')) . ')'); -$stmt->execute($emails); -$memberRows = []; -while ($row = $stmt->fetch()) { - $memberRows[$row['Email']] = $row; -} -foreach ($members as $member) { - $row = $memberRows[$member['email']] ?? null; - gm_assert_equal("member flags exist {$member['email']}", true, $row !== null, $failures, $passes); - if ($row === null) { - continue; - } - gm_assert_equal("{$member['email']} active", $member['active'], (int)$row['aktiv'], $failures, $passes); - gm_assert_equal("{$member['email']} admin", $member['admin'], (int)$row['admin'], $failures, $passes); - gm_assert_equal("{$member['email']} paypalname", $member['paypalname'], $row['paypalname'], $failures, $passes); -} - -$front = gm_fetch_sheet_emails($pdo, $emails, 'front'); -$back = gm_fetch_sheet_emails($pdo, $emails, 'back'); -gm_assert_equal('front sheet emails', gm_expected_front_sheet_emails(), $front, $failures, $passes); -gm_assert_equal('back sheet emails', gm_expected_back_sheet_emails(), $back, $failures, $passes); - -$stmt = $pdo->prepare('SELECT MitarbeiterID FROM kl_Mitarbeiter WHERE Name = ? OR paypalname = ?'); -$stmt->execute(['GM PayPal Alias', 'GM PayPal Alias']); -$paypalId = $stmt->fetchColumn(); -gm_assert_equal('csv paypalname lookup finds participant', true, $paypalId !== false, $failures, $passes); - -$stmt = $pdo->prepare( - 'SELECT COUNT(*) - FROM kl_Einzahlungen - WHERE MitarbeiterID = ? - AND Betrag = ? - AND DATE(Datum) = DATE(?)' -); -$stmt->execute([$paypalId ?: 0, gm_cents_to_decimal(750), gm_dates()['recent']]); -gm_assert_equal('csv duplicate basis exists', 1, (int)$stmt->fetchColumn(), $failures, $passes); - -$stmt = $pdo->prepare('SELECT COUNT(*) FROM kl_Mitarbeiter WHERE Name = ? OR paypalname = ?'); -$stmt->execute(['GM Unbekannt', 'GM Unbekannt']); -gm_assert_equal('csv unknown lookup misses participant', 0, (int)$stmt->fetchColumn(), $failures, $passes); - -$activeNoticeCount = (int)$pdo - ->query("SELECT COUNT(*) FROM kl_hinweise WHERE nachricht LIKE '[GM]%' AND gueltig_bis >= NOW()") - ->fetchColumn(); -$expiredNoticeCount = (int)$pdo - ->query("SELECT COUNT(*) FROM kl_hinweise WHERE nachricht LIKE '[GM]%' AND gueltig_bis < NOW()") - ->fetchColumn(); -gm_assert_equal('active GM notice count', 1, $activeNoticeCount, $failures, $passes); -gm_assert_equal('expired GM notice count', 1, $expiredNoticeCount, $failures, $passes); - -if ($failures !== []) { - echo "\nGolden master check failed with " . count($failures) . " failure(s):\n"; - foreach ($failures as $failure) { - echo "- {$failure}\n"; - } - exit(1); -} - -echo "\nGolden master check passed with {$passes} assertions.\n"; - diff --git a/scripts/check-m3-saas-basis.php b/scripts/check-m3-saas-basis.php deleted file mode 100644 index 81704ad..0000000 --- a/scripts/check-m3-saas-basis.php +++ /dev/null @@ -1,111 +0,0 @@ -prepare('SELECT * FROM tenants WHERE slug = ?'); -$stmt->execute([$tenantSlug]); -$tenants = $stmt->fetchAll(); - -m3_check_assert('default tenant exists exactly once', count($tenants) === 1, $failures, $passes); -if (count($tenants) !== 1) { - echo "\nM3 SaaS basis check failed: tenant '{$tenantSlug}' is missing or duplicated.\n"; - exit(1); -} - -$tenantId = (int)$tenants[0]['id']; - -$legacyMemberCount = (int)$pdo->query('SELECT COUNT(*) FROM kl_Mitarbeiter')->fetchColumn(); - -$stmt = $pdo->prepare('SELECT COUNT(*) FROM tenant_settings WHERE tenant_id = ?'); -$stmt->execute([$tenantId]); -m3_check_assert('tenant settings exist exactly once', (int)$stmt->fetchColumn() === 1, $failures, $passes); - -$stmt = $pdo->prepare('SELECT COUNT(*) FROM participants WHERE tenant_id = ?'); -$stmt->execute([$tenantId]); -m3_check_assert('participant count matches legacy members', (int)$stmt->fetchColumn() === $legacyMemberCount, $failures, $passes); - -$stmt = $pdo->prepare( - 'SELECT COUNT(DISTINCT legacy_mitarbeiter_id) - FROM participants - WHERE tenant_id = ? - AND legacy_mitarbeiter_id IS NOT NULL' -); -$stmt->execute([$tenantId]); -m3_check_assert('legacy member ids are mapped one-to-one', (int)$stmt->fetchColumn() === $legacyMemberCount, $failures, $passes); - -$stmt = $pdo->prepare( - 'SELECT COUNT(*) - FROM kl_Mitarbeiter m - LEFT JOIN participants p - ON p.tenant_id = ? - AND p.legacy_mitarbeiter_id = m.MitarbeiterID - WHERE p.id IS NULL' -); -$stmt->execute([$tenantId]); -m3_check_assert('no legacy member is missing a participant', (int)$stmt->fetchColumn() === 0, $failures, $passes); - -$stmt = $pdo->prepare( - 'SELECT COUNT(*) - FROM kl_Mitarbeiter m - LEFT JOIN users u - ON u.email_norm = LOWER(TRIM(m.Email)) - LEFT JOIN tenant_memberships tm - ON tm.tenant_id = ? - AND tm.user_id = u.id - WHERE m.aktiv = 1 - AND m.admin = 1 - AND (tm.id IS NULL OR tm.role NOT IN (\'admin\', \'owner\'))' -); -$stmt->execute([$tenantId]); -m3_check_assert('active legacy admins have tenant memberships', (int)$stmt->fetchColumn() === 0, $failures, $passes); - -$stmt = $pdo->prepare( - 'SELECT COUNT(*) - FROM kl_Mitarbeiter m - JOIN users u - ON u.email_norm = LOWER(TRIM(m.Email)) - JOIN tenant_memberships tm - ON tm.tenant_id = ? - AND tm.user_id = u.id - JOIN participants p - ON p.tenant_id = tm.tenant_id - AND p.legacy_mitarbeiter_id = m.MitarbeiterID - WHERE m.aktiv = 1 - AND m.admin = 1 - AND p.user_id <> u.id' -); -$stmt->execute([$tenantId]); -m3_check_assert('admin participants link to their login users', (int)$stmt->fetchColumn() === 0, $failures, $passes); - -if ($failures !== []) { - echo "\nM3 SaaS basis check failed with " . count($failures) . " failure(s):\n"; - foreach ($failures as $failure) { - echo "- {$failure}\n"; - } - exit(1); -} - -echo "\nM3 SaaS basis check passed with {$passes} assertions.\n"; diff --git a/scripts/check-m4-ledger-migration.php b/scripts/check-m4-ledger-migration.php deleted file mode 100644 index b06d644..0000000 --- a/scripts/check-m4-ledger-migration.php +++ /dev/null @@ -1,215 +0,0 @@ -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') - AND voided_at IS NULL" -); -$stmt->execute([$tenantId]); -$ledgerLegacyCount = (int)$stmt->fetchColumn(); -m4_check_assert_equal( - 'active 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 -); - -// Ledger rows without a matching legacy row are only expected when they were -// voided through the storno flow in letzteneintraege.php. A missing legacy row -// on a non-voided ledger entry indicates real drift between the tables. -$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 le.voided_at IS NULL - AND e.EinzahlungsID IS NULL" -); -$stmt->execute([$tenantId]); -m4_check_assert_equal('no stale non-voided 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 le.voided_at IS NULL - AND v.VerbrauchID IS NULL" -); -$stmt->execute([$tenantId]); -m4_check_assert_equal('no stale non-voided 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"; diff --git a/scripts/check-m4-ledger-service.php b/scripts/check-m4-ledger-service.php deleted file mode 100644 index 449ec0d..0000000 --- a/scripts/check-m4-ledger-service.php +++ /dev/null @@ -1,212 +0,0 @@ -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{total_payments_cents: int, total_costs_cents: int, total_marks: int, balance_cents: int, year_payments_cents: int, year_costs_cents: int, year_marks: int} - */ -function m4_service_totals_from_summaries(array $summaries): array -{ - $totals = ledger_empty_totals(); - - foreach ($summaries as $summary) { - $totals['total_payments_cents'] += (int)$summary['total_payments_cents']; - $totals['total_costs_cents'] += (int)$summary['total_costs_cents']; - $totals['total_marks'] += (int)$summary['total_marks']; - $totals['balance_cents'] += (int)$summary['balance_cents']; - $totals['year_payments_cents'] += (int)$summary['year_payments_cents']; - $totals['year_costs_cents'] += (int)$summary['year_costs_cents']; - $totals['year_marks'] += (int)$summary['year_marks']; - } - - return $totals; -} - -$pdo = dev_pdo(); -dev_apply_schema($pdo); - -$tenantSlug = m4_service_env('M4_DEFAULT_TENANT_SLUG', m4_service_env('M3_DEFAULT_TENANT_SLUG', 'default')); -$tenantId = m4_service_fetch_tenant_id($pdo, $tenantSlug); -$expected = gm_expected_summaries(); -$emails = array_keys($expected); -$failures = []; -$passes = 0; - -$summaries = ledger_fetch_participant_summaries($pdo, $tenantId, [ - 'email_norms' => $emails, - 'year' => gm_current_year(), -]); - -$summariesByEmail = []; -foreach ($summaries as $summary) { - if ($summary['email_norm'] !== null) { - $summariesByEmail[$summary['email_norm']] = $summary; - } -} - -m4_service_assert_equal('ledger service summary count', count($expected), count($summariesByEmail), $failures, $passes); - -$keyMap = [ - 'total_payments' => 'total_payments_cents', - 'total_costs' => 'total_costs_cents', - 'total_marks' => 'total_marks', - 'balance' => 'balance_cents', - 'year_payments' => 'year_payments_cents', - 'year_costs' => 'year_costs_cents', - 'year_marks' => 'year_marks', -]; - -foreach ($expected as $email => $expectedSummary) { - m4_service_assert("ledger service summary exists {$email}", isset($summariesByEmail[$email]), $failures, $passes); - if (!isset($summariesByEmail[$email])) { - continue; - } - - m4_service_assert( - "ledger service {$email} keeps legacy member id", - $summariesByEmail[$email]['legacy_mitarbeiter_id'] !== null, - $failures, - $passes - ); - - foreach ($keyMap as $expectedKey => $actualKey) { - m4_service_assert_equal( - "ledger service {$email} {$expectedKey}", - $expectedSummary[$expectedKey], - $summariesByEmail[$email][$actualKey], - $failures, - $passes - ); - } -} - -$allSummaries = ledger_fetch_participant_summaries($pdo, $tenantId, [ - 'year' => gm_current_year(), -]); -$tenantTotals = ledger_fetch_tenant_totals($pdo, $tenantId, gm_current_year()); -foreach (m4_service_totals_from_summaries($allSummaries) as $key => $value) { - m4_service_assert_equal("ledger service tenant total {$key}", $value, $tenantTotals[$key], $failures, $passes); -} - -$stmt = $pdo->prepare('SELECT COUNT(*) FROM participants WHERE tenant_id = ? AND active = 1'); -$stmt->execute([$tenantId]); -$activeExpectedCount = (int)$stmt->fetchColumn(); -$activeSummaries = ledger_fetch_participant_summaries($pdo, $tenantId, ['active_only' => true]); -m4_service_assert_equal('ledger service active participant count', $activeExpectedCount, count($activeSummaries), $failures, $passes); - -$paypalSummary = $summariesByEmail['gm-paypal@test.local'] ?? null; -m4_service_assert('ledger service paypal summary is present', $paypalSummary !== null, $failures, $passes); -if ($paypalSummary !== null) { - $singleSummary = ledger_fetch_participant_summary($pdo, $tenantId, $paypalSummary['participant_id'], gm_current_year()); - m4_service_assert('ledger service single participant summary is present', $singleSummary !== null, $failures, $passes); - if ($singleSummary !== null) { - m4_service_assert_equal('ledger service single participant email', 'gm-paypal@test.local', $singleSummary['email_norm'], $failures, $passes); - m4_service_assert_equal('ledger service single participant balance', 650, $singleSummary['balance_cents'], $failures, $passes); - } - - $legacySummary = ledger_fetch_participant_summary_by_legacy_id( - $pdo, - $tenantId, - (int)$paypalSummary['legacy_mitarbeiter_id'], - gm_current_year() - ); - m4_service_assert('ledger service legacy participant summary is present', $legacySummary !== null, $failures, $passes); - if ($legacySummary !== null) { - m4_service_assert_equal('ledger service legacy participant id matches', $paypalSummary['participant_id'], $legacySummary['participant_id'], $failures, $passes); - m4_service_assert_equal('ledger service legacy participant balance', 650, $legacySummary['balance_cents'], $failures, $passes); - } - - $paypalEntries = ledger_fetch_recent_entries($pdo, $tenantId, [ - 'participant_id' => $paypalSummary['participant_id'], - 'limit' => 100, - ]); - m4_service_assert_equal('ledger service paypal entry count', 2, count($paypalEntries), $failures, $passes); - foreach ($paypalEntries as $entry) { - m4_service_assert_equal('ledger service paypal entry tenant', $tenantId, $entry['tenant_id'], $failures, $passes); - m4_service_assert_equal('ledger service paypal entry participant', $paypalSummary['participant_id'], $entry['participant_id'], $failures, $passes); - } -} - -$recentEntries = ledger_fetch_recent_entries($pdo, $tenantId, ['limit' => 5]); -m4_service_assert_equal('ledger service recent entry limit', 5, count($recentEntries), $failures, $passes); -$previousBookedAt = null; -foreach ($recentEntries as $entry) { - m4_service_assert_equal('ledger service recent entry tenant', $tenantId, $entry['tenant_id'], $failures, $passes); - m4_service_assert('ledger service recent entry has amount', $entry['amount_cents'] !== 0, $failures, $passes); - if ($previousBookedAt !== null) { - m4_service_assert('ledger service recent entries are sorted', $previousBookedAt >= $entry['booked_at'], $failures, $passes); - } - $previousBookedAt = $entry['booked_at']; -} - -$legacyConsumptionCount = (int)$pdo->query('SELECT COUNT(*) FROM kl_Kaffeeverbrauch')->fetchColumn(); -$consumptionEntries = ledger_fetch_recent_entries($pdo, $tenantId, [ - 'type' => 'consumption', - 'limit' => 100, -]); -m4_service_assert_equal('ledger service consumption entry count', $legacyConsumptionCount, count($consumptionEntries), $failures, $passes); - -m4_service_assert_equal('ledger service empty participant id filter', [], ledger_fetch_participant_summaries($pdo, $tenantId, [ - 'participant_ids' => [], -]), $failures, $passes); -m4_service_assert_equal('ledger service empty legacy id filter', [], ledger_fetch_participant_summaries($pdo, $tenantId, [ - 'legacy_mitarbeiter_ids' => [], -]), $failures, $passes); -m4_service_assert_equal('ledger service unknown legacy summary', null, ledger_fetch_participant_summary_by_legacy_id($pdo, $tenantId, 0), $failures, $passes); -m4_service_assert_equal('ledger service unknown tenant summaries', [], ledger_fetch_participant_summaries($pdo, 0), $failures, $passes); -m4_service_assert_equal('ledger service unknown tenant entries', [], ledger_fetch_recent_entries($pdo, 0), $failures, $passes); -m4_service_assert_equal('ledger service unknown tenant totals', ledger_empty_totals(), ledger_fetch_tenant_totals($pdo, 0), $failures, $passes); - -if ($failures !== []) { - echo "\nM4 ledger service check failed with " . count($failures) . " failure(s):\n"; - foreach ($failures as $failure) { - echo "- {$failure}\n"; - } - exit(1); -} - -echo "\nM4 ledger service check passed with {$passes} assertions.\n"; diff --git a/scripts/golden-master-data.php b/scripts/golden-master-data.php deleted file mode 100644 index 55b6fa3..0000000 --- a/scripts/golden-master-data.php +++ /dev/null @@ -1,269 +0,0 @@ - - */ -function gm_dates(): array -{ - $year = gm_current_year(); - - return [ - 'recent' => date('Y-m-d 10:00:00', strtotime('-1 day')), - 'current' => sprintf('%04d-01-15 09:00:00', $year), - 'previous' => sprintf('%04d-12-20 09:00:00', $year - 1), - 'notice_active' => date('Y-m-d 12:00:00', strtotime('+30 days')), - 'notice_expired' => date('Y-m-d 12:00:00', strtotime('-30 days')), - ]; -} - -/** - * @return array - */ -function gm_members(): array -{ - return [ - 'admin' => [ - 'name' => 'GM Admin', - 'email' => 'gm-admin@test.local', - 'active' => 1, - 'admin' => 1, - 'paypalname' => null, - ], - 'positive' => [ - 'name' => 'GM Positiv', - 'email' => 'gm-positive@test.local', - 'active' => 1, - 'admin' => 0, - 'paypalname' => null, - ], - 'negative' => [ - 'name' => 'GM Negativ', - 'email' => 'gm-negative@test.local', - 'active' => 1, - 'admin' => 0, - 'paypalname' => null, - ], - 'zero' => [ - 'name' => 'GM Nullsaldo', - 'email' => 'gm-zero@test.local', - 'active' => 1, - 'admin' => 0, - 'paypalname' => null, - ], - 'inactive' => [ - 'name' => 'GM Inaktiv', - 'email' => 'gm-inactive@test.local', - 'active' => 0, - 'admin' => 0, - 'paypalname' => null, - ], - 'paypal' => [ - 'name' => 'GM Paypal', - 'email' => 'gm-paypal@test.local', - 'active' => 1, - 'admin' => 0, - 'paypalname' => 'GM PayPal Alias', - ], - 'empty' => [ - 'name' => 'GM Ohne Buchungen', - 'email' => 'gm-empty@test.local', - 'active' => 1, - 'admin' => 0, - 'paypalname' => null, - ], - 'heavy' => [ - 'name' => 'GM Vieltrinker', - 'email' => 'gm-heavy@test.local', - 'active' => 1, - 'admin' => 0, - 'paypalname' => null, - ], - ]; -} - -/** - * @return array> - */ -function gm_payments(): array -{ - return [ - 'admin' => [ - ['amount_cents' => 300, 'date_key' => 'current'], - ], - 'positive' => [ - ['amount_cents' => 2000, 'date_key' => 'current'], - ['amount_cents' => 500, 'date_key' => 'previous'], - ], - 'negative' => [ - ['amount_cents' => 100, 'date_key' => 'current'], - ], - 'zero' => [ - ['amount_cents' => 200, 'date_key' => 'current'], - ], - 'inactive' => [ - ['amount_cents' => 1000, 'date_key' => 'current'], - ], - 'paypal' => [ - ['amount_cents' => 750, 'date_key' => 'recent'], - ], - 'heavy' => [ - ['amount_cents' => 500, 'date_key' => 'current'], - ], - ]; -} - -/** - * @return array> - */ -function gm_consumptions(): array -{ - return [ - 'admin' => [ - ['marks' => 1, 'unit_price_cents' => 20, 'date_key' => 'recent', 'entry_type' => 2], - ], - 'positive' => [ - ['marks' => 5, 'unit_price_cents' => 20, 'date_key' => 'recent', 'entry_type' => 2], - ['marks' => 2, 'unit_price_cents' => 20, 'date_key' => 'previous', 'entry_type' => null], - ], - 'negative' => [ - ['marks' => 10, 'unit_price_cents' => 20, 'date_key' => 'recent', 'entry_type' => null], - ['marks' => 5, 'unit_price_cents' => 20, 'date_key' => 'previous', 'entry_type' => null], - ], - 'zero' => [ - ['marks' => 10, 'unit_price_cents' => 20, 'date_key' => 'recent', 'entry_type' => null], - ], - 'inactive' => [ - ['marks' => 20, 'unit_price_cents' => 20, 'date_key' => 'recent', 'entry_type' => null], - ], - 'paypal' => [ - ['marks' => 5, 'unit_price_cents' => 20, 'date_key' => 'recent', 'entry_type' => 2], - ], - 'heavy' => [ - ['marks' => 12, 'unit_price_cents' => 20, 'date_key' => 'recent', 'entry_type' => null], - ], - ]; -} - -/** - * @return array - */ -function gm_expected_summaries(): array -{ - return [ - 'gm-admin@test.local' => [ - 'total_payments' => 300, - 'total_costs' => 20, - 'total_marks' => 1, - 'balance' => 280, - 'year_payments' => 300, - 'year_costs' => 20, - 'year_marks' => 1, - ], - 'gm-positive@test.local' => [ - 'total_payments' => 2500, - 'total_costs' => 140, - 'total_marks' => 7, - 'balance' => 2360, - 'year_payments' => 2000, - 'year_costs' => 100, - 'year_marks' => 5, - ], - 'gm-negative@test.local' => [ - 'total_payments' => 100, - 'total_costs' => 300, - 'total_marks' => 15, - 'balance' => -200, - 'year_payments' => 100, - 'year_costs' => 200, - 'year_marks' => 10, - ], - 'gm-zero@test.local' => [ - 'total_payments' => 200, - 'total_costs' => 200, - 'total_marks' => 10, - 'balance' => 0, - 'year_payments' => 200, - 'year_costs' => 200, - 'year_marks' => 10, - ], - 'gm-inactive@test.local' => [ - 'total_payments' => 1000, - 'total_costs' => 400, - 'total_marks' => 20, - 'balance' => 600, - 'year_payments' => 1000, - 'year_costs' => 400, - 'year_marks' => 20, - ], - 'gm-paypal@test.local' => [ - 'total_payments' => 750, - 'total_costs' => 100, - 'total_marks' => 5, - 'balance' => 650, - 'year_payments' => 750, - 'year_costs' => 100, - 'year_marks' => 5, - ], - 'gm-empty@test.local' => [ - 'total_payments' => 0, - 'total_costs' => 0, - 'total_marks' => 0, - 'balance' => 0, - 'year_payments' => 0, - 'year_costs' => 0, - 'year_marks' => 0, - ], - 'gm-heavy@test.local' => [ - 'total_payments' => 500, - 'total_costs' => 240, - 'total_marks' => 12, - 'balance' => 260, - 'year_payments' => 500, - 'year_costs' => 240, - 'year_marks' => 12, - ], - ]; -} - -/** - * @return list - */ -function gm_expected_front_sheet_emails(): array -{ - return [ - 'gm-heavy@test.local', - 'gm-negative@test.local', - 'gm-zero@test.local', - ]; -} - -/** - * @return list - */ -function gm_expected_back_sheet_emails(): array -{ - return [ - 'gm-admin@test.local', - 'gm-empty@test.local', - 'gm-paypal@test.local', - 'gm-positive@test.local', - ]; -} - -function gm_cents_to_decimal(int $cents): string -{ - return number_format($cents / 100, 2, '.', ''); -} - -function gm_decimal_to_cents(mixed $value): int -{ - return (int)round(((float)$value) * 100); -} - diff --git a/scripts/init-mysql-dev.php b/scripts/init-mysql-dev.php index dacf57f..96d2ebd 100644 --- a/scripts/init-mysql-dev.php +++ b/scripts/init-mysql-dev.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/** + * Richtet eine frische MySQL-Entwicklungsdatenbank ein: Schema/Migrationen + * anwenden und einen ersten SaaS-Mandanten samt Owner-Login, Teilnehmer, + * ein paar Journalbuchungen und einem Hinweis anlegen - alles idempotent. + * + * Der anzulegende Owner kommt aus DEV_AUTH_EMAIL (Anzeigename optional aus + * DEV_AUTH_NAME). Nach dem Lauf kann man sich damit ueber login.php anmelden. + */ + require __DIR__ . '/dev-db.php'; $email = getenv('DEV_AUTH_EMAIL'); @@ -15,35 +24,58 @@ dev_apply_schema($pdo); $email = strtolower(trim((string)$email)); $name = trim((string)(getenv('DEV_AUTH_NAME') ?: 'Test Admin')); +$password = trim((string)(getenv('DEV_AUTH_PASSWORD') ?: 'DevPasswort123!')); +$slug = trim((string)(getenv('DEV_TENANT_SLUG') ?: 'dev')); -$stmt = $pdo->prepare( - 'INSERT INTO kl_Mitarbeiter (Name, Email, aktiv, admin) - VALUES (?, ?, 1, 1) - ON DUPLICATE KEY UPDATE Name = VALUES(Name), aktiv = 1, admin = 1' -); -$stmt->execute([$name, $email]); - -$memberId = (int)$pdo->query('SELECT MitarbeiterID FROM kl_Mitarbeiter WHERE Email = ' . $pdo->quote($email))->fetchColumn(); - -$paymentCount = (int)$pdo->query('SELECT COUNT(*) FROM kl_Einzahlungen WHERE MitarbeiterID = ' . $memberId)->fetchColumn(); -if ($paymentCount === 0) { - $stmt = $pdo->prepare('INSERT INTO kl_Einzahlungen (MitarbeiterID, Betrag, Datum) VALUES (?, ?, NOW())'); - $stmt->execute([$memberId, 10.00]); +// Mandant. +$tenantId = (int)($pdo->query('SELECT id FROM tenants WHERE slug = ' . $pdo->quote($slug))->fetchColumn() ?: 0); +if ($tenantId === 0) { + $pdo->prepare('INSERT INTO tenants (slug, name, status) VALUES (?, ?, ?)') + ->execute([$slug, 'Dev-Mandant', 'active']); + $tenantId = (int)$pdo->lastInsertId(); +} +if ((int)$pdo->query("SELECT COUNT(*) FROM tenant_settings WHERE tenant_id = {$tenantId}")->fetchColumn() === 0) { + $pdo->prepare('INSERT INTO tenant_settings (tenant_id) VALUES (?)')->execute([$tenantId]); } -$consumptionCount = (int)$pdo->query('SELECT COUNT(*) FROM kl_Kaffeeverbrauch WHERE MitarbeiterID = ' . $memberId)->fetchColumn(); -if ($consumptionCount === 0) { - $stmt = $pdo->prepare( - 'INSERT INTO kl_Kaffeeverbrauch (MitarbeiterID, AnzahlStriche, Kosten, KostenproStrich, Datum, Eintragsart) - VALUES (?, ?, ?, ?, NOW(), ?)' - ); - $stmt->execute([$memberId, 3, 0.60, 0.20, 2]); +// Owner-Login. +$userId = (int)($pdo->query('SELECT id FROM users WHERE email_norm = ' . $pdo->quote($email))->fetchColumn() ?: 0); +if ($userId === 0) { + $pdo->prepare( + 'INSERT INTO users (email, email_norm, display_name, password_hash, status, email_verified_at) + VALUES (?, ?, ?, ?, ?, NOW())' + )->execute([$email, $email, $name, password_hash($password, PASSWORD_DEFAULT), 'active']); + $userId = (int)$pdo->lastInsertId(); +} +if ((int)$pdo->query("SELECT COUNT(*) FROM tenant_memberships WHERE tenant_id = {$tenantId} AND user_id = {$userId}")->fetchColumn() === 0) { + $pdo->prepare('INSERT INTO tenant_memberships (tenant_id, user_id, role, status, joined_at) VALUES (?, ?, ?, ?, NOW())') + ->execute([$tenantId, $userId, 'owner', 'active']); } -$noticeCount = (int)$pdo->query('SELECT COUNT(*) FROM kl_hinweise')->fetchColumn(); -if ($noticeCount === 0) { - $stmt = $pdo->prepare('INSERT INTO kl_hinweise (nachricht, gueltig_bis) VALUES (?, DATE_ADD(NOW(), INTERVAL 30 DAY))'); - $stmt->execute(['Dev-Hinweis: MySQL-Testumgebung aktiv.']); +// Teilnehmer des Owners. +$participantId = (int)($pdo->query("SELECT id FROM participants WHERE tenant_id = {$tenantId} AND user_id = {$userId}")->fetchColumn() ?: 0); +if ($participantId === 0) { + $pdo->prepare('INSERT INTO participants (tenant_id, user_id, display_name, email, email_norm, active) VALUES (?, ?, ?, ?, ?, 1)') + ->execute([$tenantId, $userId, $name, $email, $email]); + $participantId = (int)$pdo->lastInsertId(); } -echo "MySQL dev schema is ready for {$email}.\n"; +// Ein paar Buchungen, damit die Seiten nicht komplett leer sind. +if ((int)$pdo->query("SELECT COUNT(*) FROM ledger_entries WHERE tenant_id = {$tenantId} AND participant_id = {$participantId}")->fetchColumn() === 0) { + $pdo->prepare( + "INSERT INTO ledger_entries (tenant_id, participant_id, type, amount_cents, booked_at, source) VALUES (?, ?, 'payment', ?, NOW(), 'manual_bulk')" + )->execute([$tenantId, $participantId, 1000]); + $pdo->prepare( + "INSERT INTO ledger_entries (tenant_id, participant_id, type, amount_cents, marks_count, unit_price_cents, booked_at, source) VALUES (?, ?, 'consumption', ?, ?, 20, NOW(), 'manual_bulk')" + )->execute([$tenantId, $participantId, -60, 3]); +} + +// Hinweis. +if ((int)$pdo->query("SELECT COUNT(*) FROM notices WHERE tenant_id = {$tenantId} AND deleted_at IS NULL")->fetchColumn() === 0) { + $pdo->prepare( + 'INSERT INTO notices (tenant_id, message, valid_from, valid_until, created_by_user_id) + VALUES (?, ?, NOW(), DATE_ADD(NOW(), INTERVAL 30 DAY), ?)' + )->execute([$tenantId, 'Dev-Hinweis: MySQL-Testumgebung aktiv.', $userId]); +} + +echo "MySQL dev schema is ready. Login: {$email} / {$password} (Mandant '{$slug}').\n"; diff --git a/scripts/seed-golden-master.php b/scripts/seed-golden-master.php deleted file mode 100644 index bc5fec9..0000000 --- a/scripts/seed-golden-master.php +++ /dev/null @@ -1,129 +0,0 @@ - $member['email'], $members)); -$placeholders = implode(',', array_fill(0, count($emails), '?')); - -$pdo->beginTransaction(); - -try { - $step = 'select existing fixture ids'; - $stmt = $pdo->prepare("SELECT MitarbeiterID FROM kl_Mitarbeiter WHERE Email IN ({$placeholders})"); - $stmt->execute($emails); - $ids = array_map('intval', $stmt->fetchAll(PDO::FETCH_COLUMN)); - - if ($ids !== []) { - $idPlaceholders = implode(',', array_fill(0, count($ids), '?')); - $step = 'delete fixture payments'; - $stmt = $pdo->prepare("DELETE FROM kl_Einzahlungen WHERE MitarbeiterID IN ({$idPlaceholders})"); - $stmt->execute($ids); - $step = 'delete fixture consumption'; - $stmt = $pdo->prepare("DELETE FROM kl_Kaffeeverbrauch WHERE MitarbeiterID IN ({$idPlaceholders})"); - $stmt->execute($ids); - } - - $step = 'delete fixture members'; - $stmt = $pdo->prepare("DELETE FROM kl_Mitarbeiter WHERE Email IN ({$placeholders})"); - $stmt->execute($emails); - - $step = 'delete fixture notices'; - $pdo->exec("DELETE FROM kl_hinweise WHERE nachricht LIKE '[GM]%'"); - $step = 'delete fixture survey locks'; - $pdo->exec("DELETE FROM CoffeeSurveyVotedEmails WHERE EmailNorm LIKE 'gm-%@test.local'"); - - $insertMember = $pdo->prepare( - 'INSERT INTO kl_Mitarbeiter (Name, Email, paypalname, aktiv, admin) - VALUES (?, ?, ?, ?, ?)' - ); - $selectMemberId = $pdo->prepare('SELECT MitarbeiterID FROM kl_Mitarbeiter WHERE Email = ?'); - $memberIds = []; - - foreach ($members as $key => $member) { - $step = 'insert member ' . $key; - $insertMember->execute([ - $member['name'], - $member['email'], - $member['paypalname'], - $member['active'], - $member['admin'], - ]); - $selectMemberId->execute([$member['email']]); - $memberIds[$key] = (int)$selectMemberId->fetchColumn(); - } - - $insertPayment = $pdo->prepare( - 'INSERT INTO kl_Einzahlungen (MitarbeiterID, Betrag, Datum) - VALUES (?, ?, ?)' - ); - foreach ($payments as $memberKey => $entries) { - foreach ($entries as $entry) { - $step = 'insert payment ' . $memberKey; - $insertPayment->execute([ - $memberIds[$memberKey], - gm_cents_to_decimal($entry['amount_cents']), - $dates[$entry['date_key']], - ]); - } - } - - $insertConsumption = $pdo->prepare( - 'INSERT INTO kl_Kaffeeverbrauch - (MitarbeiterID, AnzahlStriche, Kosten, KostenproStrich, Datum, Eintragsart) - VALUES (?, ?, ?, ?, ?, ?)' - ); - foreach ($consumptions as $memberKey => $entries) { - foreach ($entries as $entry) { - $step = 'insert consumption ' . $memberKey; - $costCents = $entry['marks'] * $entry['unit_price_cents']; - $insertConsumption->execute([ - $memberIds[$memberKey], - $entry['marks'], - gm_cents_to_decimal($costCents), - gm_cents_to_decimal($entry['unit_price_cents']), - $dates[$entry['date_key']], - $entry['entry_type'], - ]); - } - } - - $step = 'insert notices'; - $insertNotice = $pdo->prepare('INSERT INTO kl_hinweise (nachricht, gueltig_bis) VALUES (?, ?)'); - $insertNotice->execute(['[GM] Aktiver Hinweis für Golden-Master-Test', $dates['notice_active']]); - $insertNotice->execute(['[GM] Abgelaufener Hinweis für Golden-Master-Test', $dates['notice_expired']]); - - $metadata = [ - 'golden_master_seed' => 'seeded', - 'golden_master_seed_year' => (string)gm_current_year(), - 'golden_master_seed_members' => (string)count($members), - 'golden_master_seed_source' => 'scripts/seed-golden-master.php', - ]; - $insertMeta = $pdo->prepare( - 'INSERT INTO dev_baseline_metadata (meta_key, meta_value) - VALUES (?, ?) - ON DUPLICATE KEY UPDATE meta_value = VALUES(meta_value)' - ); - foreach ($metadata as $key => $value) { - $step = 'insert metadata ' . $key; - $insertMeta->execute([$key, $value]); - } - - $pdo->commit(); -} catch (Throwable $e) { - $pdo->rollBack(); - fwrite(STDERR, "Golden master seed failed at {$step}: {$e->getMessage()}\n"); - exit(1); -} - -echo "Golden master fixtures seeded: " . count($members) . " members.\n";