M3 SaaS-Basis mit Default-Tenant vorbereiten
- Tenant/User/Membership/Participant-Tabellen additiv migrieren - Default-Tenant-Backfill fuer Legacy-Mitarbeiter und Admins ergaenzen - M3-Kontrollskript und Plan-Dokumentation aktualisieren
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
CREATE TABLE IF NOT EXISTS tenants (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
slug VARCHAR(100) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
status VARCHAR(30) NOT NULL DEFAULT 'active',
|
||||
timezone VARCHAR(100) NOT NULL DEFAULT 'Europe/Berlin',
|
||||
locale VARCHAR(20) NOT NULL DEFAULT 'de-DE',
|
||||
currency_code CHAR(3) NOT NULL DEFAULT 'EUR',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uq_tenants_slug (slug),
|
||||
KEY idx_tenants_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tenant_settings (
|
||||
tenant_id INT PRIMARY KEY,
|
||||
mark_price_cents INT NOT NULL DEFAULT 20,
|
||||
self_entry_enabled TINYINT(1) NOT NULL DEFAULT 1,
|
||||
paypal_enabled TINYINT(1) NOT NULL DEFAULT 0,
|
||||
paypal_url_template VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
sheet_window_days INT NOT NULL DEFAULT 100,
|
||||
negative_warning_cents INT NOT NULL DEFAULT -1000,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_tenant_settings_tenant
|
||||
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
|
||||
ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
email_norm VARCHAR(255) NOT NULL,
|
||||
display_name VARCHAR(255) NOT NULL,
|
||||
password_hash VARCHAR(255) NULL,
|
||||
status VARCHAR(30) NOT NULL DEFAULT 'active',
|
||||
email_verified_at DATETIME NULL,
|
||||
mfa_enabled TINYINT(1) NOT NULL DEFAULT 0,
|
||||
disabled_at DATETIME NULL,
|
||||
last_login_at DATETIME NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uq_users_email_norm (email_norm),
|
||||
KEY idx_users_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tenant_memberships (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
tenant_id INT NOT NULL,
|
||||
user_id INT NOT NULL,
|
||||
role VARCHAR(30) NOT NULL DEFAULT 'member',
|
||||
status VARCHAR(30) NOT NULL DEFAULT 'active',
|
||||
invited_at DATETIME NULL,
|
||||
joined_at DATETIME NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uq_tenant_memberships_tenant_user (tenant_id, user_id),
|
||||
KEY idx_tenant_memberships_user (user_id),
|
||||
KEY idx_tenant_memberships_tenant_role_status (tenant_id, role, status),
|
||||
CONSTRAINT fk_tenant_memberships_tenant
|
||||
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT fk_tenant_memberships_user
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS participants (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
tenant_id INT NOT NULL,
|
||||
user_id INT NULL,
|
||||
display_name VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) NULL,
|
||||
email_norm VARCHAR(255) NULL,
|
||||
paypal_name VARCHAR(255) NULL,
|
||||
active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
legacy_mitarbeiter_id INT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uq_participants_tenant_legacy (tenant_id, legacy_mitarbeiter_id),
|
||||
UNIQUE KEY uq_participants_tenant_email_norm (tenant_id, email_norm),
|
||||
UNIQUE KEY uq_participants_tenant_user (tenant_id, user_id),
|
||||
KEY idx_participants_tenant_active_name (tenant_id, active, display_name),
|
||||
KEY idx_participants_tenant_paypal_name (tenant_id, paypal_name),
|
||||
KEY idx_participants_user (user_id),
|
||||
CONSTRAINT fk_participants_tenant
|
||||
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT fk_participants_user
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@@ -24,9 +24,9 @@ das neue Modell umgestellt.
|
||||
- Kein Billing, keine Tarife, keine E-Mail-Verifikation im ersten Schritt.
|
||||
- Kein produktiver Tenant-Wechsel in bestehenden Legacy-Seiten.
|
||||
|
||||
## Vorgeschlagene Migration
|
||||
## Umgesetzter erster Schritt
|
||||
|
||||
Naechste Migration:
|
||||
Umgesetzte Migration:
|
||||
|
||||
```text
|
||||
database/migrations/0002_saas_identity_tenants.sql
|
||||
@@ -40,6 +40,13 @@ Tabellen:
|
||||
- `tenant_memberships`
|
||||
- `participants`
|
||||
|
||||
Ergaenzende Skripte:
|
||||
|
||||
```text
|
||||
scripts/backfill-default-tenant.php
|
||||
scripts/check-m3-saas-basis.php
|
||||
```
|
||||
|
||||
Wichtige Regeln:
|
||||
|
||||
- `users` sind Login-Konten.
|
||||
@@ -49,6 +56,8 @@ Wichtige Regeln:
|
||||
- `participants.legacy_mitarbeiter_id` speichert die alte ID aus
|
||||
`kl_Mitarbeiter`.
|
||||
- Geldwerte in neuen Settings als Cent-Integer speichern.
|
||||
- Die Migration ist rein additiv; bestehende Legacy-Seiten fragen weiter die
|
||||
bisherigen `kl_*`-Tabellen ab.
|
||||
|
||||
## Default-Tenant
|
||||
|
||||
@@ -65,9 +74,9 @@ currency_code: EUR
|
||||
|
||||
Die Werte koennen spaeter in einer Tenant-Einstellungsseite angepasst werden.
|
||||
|
||||
## Backfill-Vorschlag
|
||||
## Backfill
|
||||
|
||||
Ein separates Skript sollte die bestehenden Mitarbeiter spiegeln:
|
||||
Ein separates Skript spiegelt die bestehenden Mitarbeiter:
|
||||
|
||||
```text
|
||||
scripts/backfill-default-tenant.php
|
||||
@@ -87,15 +96,24 @@ Vorgang:
|
||||
Der Backfill muss idempotent sein und darf keine bestehenden Legacy-Daten
|
||||
loeschen.
|
||||
|
||||
Ausgefuehrter Dev-Stand:
|
||||
|
||||
- Default-Tenant `default` wurde angelegt.
|
||||
- 9 Legacy-Mitarbeiter wurden als `participants` gespiegelt.
|
||||
- 2 aktive Admin-/Owner-Logins wurden als `users` und
|
||||
`tenant_memberships` angelegt.
|
||||
- Ein zweiter Backfill-Lauf blieb idempotent.
|
||||
|
||||
## Erste Akzeptanzkriterien
|
||||
|
||||
- Migrationen laufen mehrfach ohne Fehler.
|
||||
- Default-Tenant existiert genau einmal.
|
||||
- `tenant_settings` enthalten Preis pro Strich und bestehende PayPal-Optionen.
|
||||
- Jeder bestehende `kl_Mitarbeiter` hat genau einen `participant`.
|
||||
- `participants.legacy_mitarbeiter_id` ist gesetzt.
|
||||
- Admins werden als tenant-scoped Rolle abgebildet.
|
||||
- Golden-Master und HTTP-Smoke bleiben gruen.
|
||||
- Migrationen laufen mehrfach ohne Fehler: erfuellt.
|
||||
- Default-Tenant existiert genau einmal: erfuellt.
|
||||
- `tenant_settings` enthalten Preis pro Strich und bestehende PayPal-Optionen:
|
||||
erfuellt.
|
||||
- Jeder bestehende `kl_Mitarbeiter` hat genau einen `participant`: erfuellt.
|
||||
- `participants.legacy_mitarbeiter_id` ist gesetzt: erfuellt.
|
||||
- Admins werden als tenant-scoped Rolle abgebildet: erfuellt.
|
||||
- Golden-Master und HTTP-Smoke bleiben gruen: erfuellt.
|
||||
|
||||
## Risiken
|
||||
|
||||
@@ -109,10 +127,9 @@ loeschen.
|
||||
|
||||
## Empfohlene Reihenfolge
|
||||
|
||||
1. Migration `0002_saas_identity_tenants.sql` erstellen.
|
||||
2. `scripts/backfill-default-tenant.php` erstellen.
|
||||
3. Backfill gegen die Dev-Datenbank ausfuehren.
|
||||
4. Kontrollskript fuer Tenant/Participant/Role-Counts schreiben.
|
||||
5. Golden-Master und HTTP-Smoke ausfuehren.
|
||||
6. Danach Login-/Registrierungsrouten planen.
|
||||
|
||||
1. Migration `0002_saas_identity_tenants.sql` erstellen: erledigt.
|
||||
2. `scripts/backfill-default-tenant.php` erstellen: erledigt.
|
||||
3. Backfill gegen die Dev-Datenbank ausfuehren: erledigt.
|
||||
4. Kontrollskript fuer Tenant/Participant/Role-Counts schreiben: erledigt.
|
||||
5. Golden-Master und HTTP-Smoke ausfuehren: erledigt.
|
||||
6. Danach Login-/Registrierungsrouten planen: naechster Schritt.
|
||||
|
||||
@@ -411,10 +411,12 @@ Schritte:
|
||||
|
||||
- Tabellen fuer `tenants`, `users`, `tenant_memberships` und
|
||||
`tenant_settings` anlegen. Vorbereitung dokumentiert in
|
||||
`docs/m3-saas-basis-vorbereitung.md`.
|
||||
- Zusaetzlich `participants` als getrennte Kaffee-Teilnehmer-Tabelle anlegen.
|
||||
- Default-Tenant fuer den aktuellen Bestand anlegen.
|
||||
- Bestehende `kl_Mitarbeiter` idempotent in `participants` spiegeln.
|
||||
`docs/m3-saas-basis-vorbereitung.md`: erster Schritt erledigt.
|
||||
- Zusaetzlich `participants` als getrennte Kaffee-Teilnehmer-Tabelle anlegen:
|
||||
erledigt.
|
||||
- Default-Tenant fuer den aktuellen Bestand anlegen: erledigt.
|
||||
- Bestehende `kl_Mitarbeiter` idempotent in `participants` spiegeln:
|
||||
erledigt.
|
||||
- Registrierung: Tenant + Owner-User + Default-Settings erzeugen.
|
||||
- Login, Logout, Passwort-Reset und E-Mail-Verifikation bauen.
|
||||
- Tenant-Aufloesung definieren.
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require __DIR__ . '/dev-db.php';
|
||||
|
||||
function m3_env(string $name, string $default): string
|
||||
{
|
||||
$value = getenv($name);
|
||||
if ($value === false || trim($value) === '') {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return trim($value);
|
||||
}
|
||||
|
||||
function m3_email_norm(?string $email): ?string
|
||||
{
|
||||
$normalized = strtolower(trim((string)$email));
|
||||
|
||||
return $normalized !== '' ? $normalized : null;
|
||||
}
|
||||
|
||||
function m3_decimal_to_cents(mixed $value): int
|
||||
{
|
||||
$normalized = str_replace(',', '.', trim((string)$value));
|
||||
if ($normalized === '') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$negative = str_starts_with($normalized, '-');
|
||||
if ($negative) {
|
||||
$normalized = substr($normalized, 1);
|
||||
}
|
||||
|
||||
[$euros, $cents] = array_pad(explode('.', $normalized, 2), 2, '0');
|
||||
$euros = preg_replace('/[^0-9]/', '', $euros) ?? '0';
|
||||
$cents = preg_replace('/[^0-9]/', '', $cents) ?? '0';
|
||||
$cents = str_pad(substr($cents, 0, 2), 2, '0');
|
||||
|
||||
$result = ((int)$euros * 100) + (int)$cents;
|
||||
|
||||
return $negative ? -$result : $result;
|
||||
}
|
||||
|
||||
function m3_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}' 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,
|
||||
'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)'
|
||||
);
|
||||
$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']++;
|
||||
}
|
||||
|
||||
$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 "Users upserted: {$stats['users']}\n";
|
||||
echo "Memberships upserted: {$stats['memberships']}\n";
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require __DIR__ . '/dev-db.php';
|
||||
|
||||
function m3_check_assert(string $label, bool $condition, array &$failures, int &$passes): void
|
||||
{
|
||||
if ($condition) {
|
||||
$passes++;
|
||||
echo "PASS {$label}\n";
|
||||
return;
|
||||
}
|
||||
|
||||
$failures[] = $label;
|
||||
echo "FAIL {$label}\n";
|
||||
}
|
||||
|
||||
$pdo = dev_pdo();
|
||||
dev_apply_schema($pdo);
|
||||
|
||||
$tenantSlug = getenv('M3_DEFAULT_TENANT_SLUG');
|
||||
$tenantSlug = $tenantSlug !== false && trim($tenantSlug) !== '' ? trim($tenantSlug) : 'default';
|
||||
|
||||
$failures = [];
|
||||
$passes = 0;
|
||||
|
||||
$stmt = $pdo->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";
|
||||
Reference in New Issue
Block a user