Initial Kaffeekasse SaaS restart
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use PDO;
|
||||
use RuntimeException;
|
||||
|
||||
final class TenantRegistrationService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PDO $pdo,
|
||||
private readonly AuditService $audit
|
||||
) {
|
||||
}
|
||||
|
||||
public function register(array $input): string
|
||||
{
|
||||
$name = trim((string) ($input['tenant_name'] ?? ''));
|
||||
$slug = $this->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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user