diff --git a/CHANGELOG.md b/CHANGELOG.md index eb4dc51..0e6a81b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,3 +50,4 @@ Alle Einträge verwenden ISO-8601-Timestamps in UTC. Mehrere Änderungen desselb - Administratoren erhalten bei fehlgeschlagenen Instanzaktionen einen bereinigten technischen Diagnosehinweis inklusive cURL-Fehlerdetails bei Verbindungsproblemen. - Löschaktionen verwenden jetzt den korrekten Docker-API-Endpunkt mit `force=true`. - Als gelöscht markierte Instanzen werden nicht mehr im Dashboard angezeigt; die Datenbankhistorie bleibt erhalten. +- Tarife zeigen Brutto-Monats- und Jahrespreise; die Jahrespreise werden mit Rabatt gespeichert. Tarif S umfasst jetzt 5 GB Speicher. diff --git a/dashboard.php b/dashboard.php index 936d241..9211972 100644 --- a/dashboard.php +++ b/dashboard.php @@ -34,12 +34,13 @@ $message = null; $error = null; $plans = [ - 's' => ['label' => 'S', 'memory' => 512, 'cpu' => 1.00, 'storage' => 1024], - 'm' => ['label' => 'M', 'memory' => 2048, 'cpu' => 2.00, 'storage' => 10240], - 'l' => ['label' => 'L', 'memory' => 4096, 'cpu' => 4.00, 'storage' => 25600], + 's' => ['label' => 'S', 'memory' => 512, 'cpu' => 1.00, 'storage' => 5120, 'monthly_cents' => 299, 'yearly_cents' => 2999], + 'm' => ['label' => 'M', 'memory' => 2048, 'cpu' => 2.00, 'storage' => 10240, 'monthly_cents' => 399, 'yearly_cents' => 3999], + 'l' => ['label' => 'L', 'memory' => 4096, 'cpu' => 4.00, 'storage' => 25600, 'monthly_cents' => 499, 'yearly_cents' => 4999], ]; function dashboard_escape(string $value): string { return htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); } +function dashboard_money(int $cents): string { return $cents > 0 ? number_format($cents / 100, 2, ',', '.') . ' €' : '–'; } function dashboard_status(string $status): string { return match ($status) { 'pending_payment' => 'Zahlung ausstehend', @@ -67,11 +68,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($action === 'request_instance') { $name = trim((string)($_POST['name'] ?? '')); $planKey = strtolower(trim((string)($_POST['plan'] ?? ''))); + $billingPeriod = (string)($_POST['billing_period'] ?? ''); $paymentMethod = (string)($_POST['payment_method'] ?? 'bank_transfer'); if ($name === '' || !preg_match('/^[A-Za-z0-9][A-Za-z0-9 _.-]{1,119}$/', $name)) { throw new RuntimeException('Bitte einen gültigen Instanznamen eingeben.'); } - if (!isset($plans[$planKey]) || !in_array($paymentMethod, ['paypal', 'bank_transfer'], true)) { + if (!isset($plans[$planKey]) || !in_array($billingPeriod, ['monthly', 'yearly'], true) || !in_array($paymentMethod, ['paypal', 'bank_transfer'], true)) { throw new RuntimeException('Tarif oder Zahlungsart ist ungültig.'); } $slug = strtolower(preg_replace('/[^a-z0-9]+/i', '-', $name) ?? 'foundry'); @@ -79,8 +81,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $port = (int)$db->query('SELECT COALESCE(MAX(port), 29999) + 1 FROM instances')->fetchColumn(); if ($port > 60000) throw new RuntimeException('Es sind derzeit keine Ports verfügbar.'); $p = $plans[$planKey]; - $statement = $db->prepare('INSERT INTO instances (owner_user_id, name, plan, payment_status, payment_method, container_name, port, volume_size_mb, memory_limit_mb, cpu_limit, status) VALUES (:owner, :name, :plan, "pending", :method, :container, :port, :storage, :memory, :cpu, "pending_payment")'); - $statement->execute(['owner' => $userId, 'name' => $name, 'plan' => $planKey, 'method' => $paymentMethod, 'container' => $containerName, 'port' => $port, 'storage' => $p['storage'], 'memory' => $p['memory'], 'cpu' => $p['cpu']]); + $statement = $db->prepare('INSERT INTO instances (owner_user_id, name, plan, billing_period, price_gross_cents, payment_status, payment_method, container_name, port, volume_size_mb, memory_limit_mb, cpu_limit, status) VALUES (:owner, :name, :plan, :billing_period, :price_gross_cents, "pending", :method, :container, :port, :storage, :memory, :cpu, "pending_payment")'); + $statement->execute(['owner' => $userId, 'name' => $name, 'plan' => $planKey, 'billing_period' => $billingPeriod, 'price_gross_cents' => $p[$billingPeriod . '_cents'], 'method' => $paymentMethod, 'container' => $containerName, 'port' => $port, 'storage' => $p['storage'], 'memory' => $p['memory'], 'cpu' => $p['cpu']]); $message = 'Die Instanzanfrage wurde erfasst und wartet auf die Zahlungsfreigabe.'; } elseif ($action === 'approve_instance' && $isAdmin) { $instanceId = (int)($_POST['instance_id'] ?? 0); @@ -301,7 +303,7 @@ function dashboard_bytes(int $bytes): string {

Neue Buchung

Instanz anfragen

S · M · L
-
+

Provisionierung

Instanzen

← Realms
-
Noch keine Instanzen vorhanden.
NameBenutzerTarifZahlungStatusAdresseAktion

:
+
Noch keine Instanzen vorhanden.
NameBenutzerTarifPreisZahlungStatusAdresseAktion
()
:
diff --git a/database/migrations/003_instance_billing.sql b/database/migrations/003_instance_billing.sql new file mode 100644 index 0000000..f229f6b --- /dev/null +++ b/database/migrations/003_instance_billing.sql @@ -0,0 +1,4 @@ +-- Add billing period and gross price snapshot to existing instances. +ALTER TABLE instances + ADD COLUMN IF NOT EXISTS billing_period ENUM('monthly', 'yearly') NOT NULL DEFAULT 'monthly' AFTER plan, + ADD COLUMN IF NOT EXISTS price_gross_cents INT UNSIGNED NOT NULL DEFAULT 0 AFTER billing_period; diff --git a/database/schema.sql b/database/schema.sql index 221b9d7..0c567db 100644 --- a/database/schema.sql +++ b/database/schema.sql @@ -72,6 +72,8 @@ CREATE TABLE IF NOT EXISTS instances ( owner_user_id BIGINT UNSIGNED NOT NULL, name VARCHAR(120) NOT NULL, plan ENUM('s', 'm', 'l') NOT NULL DEFAULT 's', + billing_period ENUM('monthly', 'yearly') NOT NULL DEFAULT 'monthly', + price_gross_cents INT UNSIGNED NOT NULL DEFAULT 0, payment_status ENUM('pending', 'paid', 'rejected') NOT NULL DEFAULT 'pending', payment_method ENUM('paypal', 'bank_transfer', 'manual') NULL, requested_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,