slugify((string) ($input['tenant_slug'] ?? $name)); $ownerName = trim((string) ($input['owner_name'] ?? '')); $ownerEmail = mb_strtolower(trim((string) ($input['owner_email'] ?? ''))); $ownerPassword = (string) ($input['owner_password'] ?? ''); $plan = (string) ($input['plan'] ?? 'starter'); $defaultPriceCents = max(50, (int) round(((float) ($input['default_price_eur'] ?? 1.20)) * 100)); if ($name === '' || $slug === '' || $ownerName === '' || $ownerEmail === '' || $ownerPassword === '') { throw new RuntimeException('Bitte alle Pflichtfelder ausfuellen.'); } if (!filter_var($ownerEmail, FILTER_VALIDATE_EMAIL)) { throw new RuntimeException('Die E-Mail-Adresse ist ungueltig.'); } if (mb_strlen($ownerPassword) < 12) { throw new RuntimeException('Das Passwort muss mindestens 12 Zeichen lang sein.'); } $allowedPlans = ['starter', 'team', 'business']; if (!in_array($plan, $allowedPlans, true)) { $plan = 'starter'; } $existingTenant = $this->pdo->prepare('SELECT id FROM tenants WHERE slug = :slug LIMIT 1'); $existingTenant->execute(['slug' => $slug]); if ($existingTenant->fetchColumn()) { throw new RuntimeException('Der gewuenschte Kurzname ist bereits vergeben.'); } $existingUser = $this->pdo->prepare('SELECT id FROM users WHERE email = :email LIMIT 1'); $existingUser->execute(['email' => $ownerEmail]); if ($existingUser->fetchColumn()) { throw new RuntimeException('Die E-Mail-Adresse ist bereits vorhanden. Fuer weitere Tenants bitte spaeter Einladungen nutzen.'); } $now = gmdate('Y-m-d H:i:s'); $this->pdo->beginTransaction(); try { $userInsert = $this->pdo->prepare( 'INSERT INTO users (full_name, email, password_hash, platform_role) VALUES (:full_name, :email, :password_hash, "user")' ); $userInsert->execute([ 'full_name' => $ownerName, 'email' => $ownerEmail, 'password_hash' => secure_password_hash($ownerPassword), ]); $userId = (int) $this->pdo->lastInsertId(); $tenantInsert = $this->pdo->prepare( 'INSERT INTO tenants (name, slug, plan, status, created_at) VALUES (:name, :slug, :plan, "active", :created_at)' ); $tenantInsert->execute([ 'name' => $name, 'slug' => $slug, 'plan' => $plan, 'created_at' => $now, ]); $tenantId = (int) $this->pdo->lastInsertId(); $membershipInsert = $this->pdo->prepare( 'INSERT INTO tenant_memberships (tenant_id, user_id, role, status) VALUES (:tenant_id, :user_id, "owner", "active")' ); $membershipInsert->execute([ 'tenant_id' => $tenantId, 'user_id' => $userId, ]); $settingsInsert = $this->pdo->prepare( 'INSERT INTO tenant_settings ( tenant_id, currency_code, default_product_price_cents, allow_self_service, allow_paper_lists, allow_rfid ) VALUES ( :tenant_id, "EUR", :default_product_price_cents, 1, 1, 0 )' ); $settingsInsert->execute([ 'tenant_id' => $tenantId, 'default_product_price_cents' => $defaultPriceCents, ]); $memberInsert = $this->pdo->prepare( 'INSERT INTO members (tenant_id, user_id, display_name, email, status) VALUES (:tenant_id, :user_id, :display_name, :email, "active")' ); $memberInsert->execute([ 'tenant_id' => $tenantId, 'user_id' => $userId, 'display_name' => $ownerName, 'email' => $ownerEmail, ]); $sourceInsert = $this->pdo->prepare( 'INSERT INTO capture_sources (tenant_id, code, name, channel, is_system) VALUES (:tenant_id, :code, :name, :channel, 1)' ); foreach ([ ['digital_self', 'Digitale Selbstbuchung', 'digital'], ['paper_sheet', 'Papierliste / Nacherfassung', 'paper'], ['admin_backoffice', 'Backoffice-Buchung', 'admin'], ['rfid_reader', 'RFID-Geraet', 'rfid'], ] as [$code, $sourceName, $channel]) { $sourceInsert->execute([ 'tenant_id' => $tenantId, 'code' => $code, 'name' => $sourceName, 'channel' => $channel, ]); } foreach ([ ['Kaffee', $defaultPriceCents], ['Espresso', $defaultPriceCents], ['Tee', max(80, $defaultPriceCents - 20)], ] as [$productName, $priceCents]) { $productInsert = $this->pdo->prepare( 'INSERT INTO products (tenant_id, name, sku, is_active) VALUES (:tenant_id, :name, :sku, 1)' ); $productInsert->execute([ 'tenant_id' => $tenantId, 'name' => $productName, 'sku' => strtolower(str_replace(' ', '-', $productName)), ]); $productId = (int) $this->pdo->lastInsertId(); $priceInsert = $this->pdo->prepare( 'INSERT INTO product_prices (tenant_id, product_id, price_cents, valid_from) VALUES (:tenant_id, :product_id, :price_cents, :valid_from)' ); $priceInsert->execute([ 'tenant_id' => $tenantId, 'product_id' => $productId, 'price_cents' => $priceCents, 'valid_from' => $now, ]); } $this->pdo->commit(); } catch (\Throwable $throwable) { $this->pdo->rollBack(); throw $throwable; } $this->audit->log($tenantId, $userId, 'tenant.registered', 'tenant', $tenantId, 'success', [ 'plan' => $plan, 'slug' => $slug, ]); return $slug; } private function slugify(string $value): string { $value = mb_strtolower($value); $value = preg_replace('/[^a-z0-9]+/u', '-', $value) ?? ''; $value = trim($value, '-'); return $value; } }