Harte Teilnehmer-Obergrenze je Tarif statt automatischem Stufenwechsel

Statt automatisch hochzustufen, blockiert das Anlegen/Reaktivieren
weiterer aktiver Mitglieder, sobald das Limit des gebuchten Tarifs
erreicht ist (Neuanlage, Bearbeiten mit Statuswechsel, Aktivieren-
Button). mitarbeiterverwalten.php zeigt die aktuelle Auslastung an.
Neuer Regressionstest scripts/check-billing-capacity.php deckt alle
drei Enforcement-Stellen ab.
This commit is contained in:
2026-07-17 11:19:00 +02:00
parent 73d599f85b
commit d2330c81cd
4 changed files with 334 additions and 4 deletions
+241
View File
@@ -0,0 +1,241 @@
<?php
declare(strict_types=1);
require __DIR__ . '/dev-db.php';
$baseUrl = rtrim((string)(getenv('SMOKE_BASE_URL') ?: 'http://127.0.0.1:8080'), '/');
$pdo = dev_pdo();
$suffix = bin2hex(random_bytes(4));
$password = 'BillingCapTest123!';
/**
* @param array<string,string> $cookies
* @return array{status:int, body:string}
*/
function bc_request(string $url, array &$cookies, string $method = 'GET', ?string $postBody = null): array
{
$cookieHeader = '';
if ($cookies !== []) {
$pairs = [];
foreach ($cookies as $name => $value) {
$pairs[] = "{$name}={$value}";
}
$cookieHeader = 'Cookie: ' . implode('; ', $pairs) . "\r\n";
}
$headers = "User-Agent: KaffeelisteBillingCap/1.0\r\n" . $cookieHeader;
if ($postBody !== null) {
$headers .= "Content-Type: application/x-www-form-urlencoded\r\n";
}
$context = stream_context_create([
'http' => [
'method' => $method,
'header' => $headers,
'content' => $postBody,
'timeout' => 15,
'ignore_errors' => true,
'follow_location' => 0,
],
]);
$body = @file_get_contents($url, false, $context);
$responseHeaders = $http_response_header ?? [];
$status = 0;
foreach ($responseHeaders as $header) {
if (preg_match('~^HTTP/\S+\s+(\d{3})~', $header, $m) === 1) {
$status = (int)$m[1];
}
if (preg_match('/^Set-Cookie:\s*([^=;]+)=([^;]+)/i', $header, $m) === 1) {
$cookies[$m[1]] = $m[2];
}
}
return ['status' => $status, 'body' => (string)$body];
}
function bc_csrf(string $body): string
{
preg_match('/name="csrf_token" value="([^"]+)"/', $body, $matches);
return $matches[1] ?? '';
}
$tenantId = null;
$userId = null;
$extraParticipantIds = [];
try {
$stmt = $pdo->prepare('INSERT INTO tenants (slug, name, status) VALUES (?, ?, ?)');
$stmt->execute(["billingcap-{$suffix}", 'Billing Cap Tenant', 'active']);
$tenantId = (int)$pdo->lastInsertId();
$pdo->prepare('INSERT INTO tenant_settings (tenant_id) VALUES (?)')->execute([$tenantId]);
$email = "billingcap-owner-{$suffix}@test.local";
$pdo->prepare('INSERT INTO users (email, email_norm, display_name, password_hash, status) VALUES (?, ?, ?, ?, ?)')
->execute([$email, $email, 'Billing Cap Owner', password_hash($password, PASSWORD_DEFAULT), 'active']);
$userId = (int)$pdo->lastInsertId();
$pdo->prepare('INSERT INTO tenant_memberships (tenant_id, user_id, role, status, joined_at) VALUES (?, ?, ?, ?, NOW())')
->execute([$tenantId, $userId, 'owner', 'active']);
$pdo->prepare('INSERT INTO participants (tenant_id, user_id, display_name, email, email_norm, active) VALUES (?, ?, ?, ?, ?, 1)')
->execute([$tenantId, $userId, 'Billing Cap Owner', $email, $email]);
// Free-Plan-Limit ist 10. Owner zaehlt schon als 1 aktiver Teilnehmer,
// also 8 weitere anlegen, um bei 9 von 10 zu landen.
for ($i = 1; $i <= 8; $i++) {
$pEmail = "billingcap-seed{$i}-{$suffix}@test.local";
$pdo->prepare('INSERT INTO participants (tenant_id, display_name, email, email_norm, active) VALUES (?, ?, ?, ?, 1)')
->execute([$tenantId, "Seed {$i}", $pEmail, $pEmail]);
$extraParticipantIds[] = (int)$pdo->lastInsertId();
}
} catch (Throwable $e) {
fwrite(STDERR, "Billing capacity test setup failed: {$e->getMessage()}\n");
exit(1);
}
$failures = [];
$passes = 0;
function bc_assert(string $label, bool $condition, array &$failures, int &$passes): void
{
if ($condition) {
$passes++;
echo "PASS {$label}\n";
} else {
$failures[] = $label;
echo "FAIL {$label}\n";
}
}
$cookies = [];
$loginPage = bc_request("{$baseUrl}/login.php", $cookies);
$csrf = bc_csrf($loginPage['body']);
bc_request("{$baseUrl}/login.php", $cookies, 'POST', http_build_query([
'csrf_token' => $csrf,
'email' => $email,
'password' => $password,
'tenant_slug' => "billingcap-{$suffix}",
]));
// 9 aktive Teilnehmer vorhanden (Cap 10) -> ein aktives Anlegen muss klappen (10/10).
$page = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies);
$body10 = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([
'aktion' => 'anlegen',
'csrf_token' => bc_csrf($page['body']),
'name' => 'Zehntes Mitglied',
'email' => "billingcap-tenth-{$suffix}@test.local",
'paypalname' => '',
'aktiv' => '1',
]));
bc_assert('10. aktives Mitglied bei Cap 10 wird angelegt', str_contains($body10['body'], 'wurde angelegt'), $failures, $passes);
// Jetzt bei 10/10 -> ein weiteres aktives Anlegen muss abgelehnt werden.
$page = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies);
$body11 = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([
'aktion' => 'anlegen',
'csrf_token' => bc_csrf($page['body']),
'name' => 'Elftes Mitglied',
'email' => "billingcap-eleventh-{$suffix}@test.local",
'paypalname' => '',
'aktiv' => '1',
]));
bc_assert('11. aktives Mitglied bei Cap 10 wird abgelehnt', str_contains($body11['body'], 'maximal 10 aktive Mitglieder'), $failures, $passes);
bc_assert('11. Mitglied wurde nicht in der DB angelegt', (int)$pdo->query("SELECT COUNT(*) FROM participants WHERE tenant_id = {$tenantId} AND email = 'billingcap-eleventh-{$suffix}@test.local'")->fetchColumn() === 0, $failures, $passes);
// Inaktives Anlegen bei vollem Cap muss weiterhin moeglich sein.
$page = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies);
$bodyInactive = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([
'aktion' => 'anlegen',
'csrf_token' => bc_csrf($page['body']),
'name' => 'Inaktives Mitglied',
'email' => "billingcap-inactive-{$suffix}@test.local",
'paypalname' => '',
]));
bc_assert('Inaktives Mitglied bei vollem Cap wird angelegt', str_contains($bodyInactive['body'], 'wurde angelegt'), $failures, $passes);
$inactiveId = (int)$pdo->query("SELECT id FROM participants WHERE tenant_id = {$tenantId} AND email = 'billingcap-inactive-{$suffix}@test.local'")->fetchColumn();
bc_assert('Neu angelegtes Mitglied ist tatsaechlich inaktiv', (int)$pdo->query("SELECT active FROM participants WHERE id = {$inactiveId}")->fetchColumn() === 0, $failures, $passes);
// Aktivieren des inaktiven Mitglieds bei vollem Cap muss abgelehnt werden.
$page = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies);
$bodyActivateBlocked = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([
'aktion' => 'aktivieren',
'csrf_token' => bc_csrf($page['body']),
'mitgliedID' => (string)$inactiveId,
]));
bc_assert('Aktivieren bei vollem Cap wird abgelehnt', str_contains($bodyActivateBlocked['body'], 'maximal 10 aktive Mitglieder'), $failures, $passes);
bc_assert('Mitglied ist nach abgelehntem Aktivieren weiterhin inaktiv', (int)$pdo->query("SELECT active FROM participants WHERE id = {$inactiveId}")->fetchColumn() === 0, $failures, $passes);
// Ein aktives Mitglied deaktivieren (Platz schaffen), dann klappt Aktivieren.
$freeUpId = $extraParticipantIds[0];
$page = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies);
bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([
'aktion' => 'deaktivieren',
'csrf_token' => bc_csrf($page['body']),
'mitgliedID' => (string)$freeUpId,
]));
$page = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies);
$bodyActivateOk = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([
'aktion' => 'aktivieren',
'csrf_token' => bc_csrf($page['body']),
'mitgliedID' => (string)$inactiveId,
]));
bc_assert('Aktivieren nach Platzschaffen (Deaktivierung) klappt', str_contains($bodyActivateOk['body'], 'wurde aktiviert'), $failures, $passes);
// bearbeitenspeichern: ein weiteres inaktives Mitglied anlegen, dann per
// Bearbeiten-Formular aktivieren versuchen, wieder bei vollem Cap (10/10).
$page = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies);
bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([
'aktion' => 'anlegen',
'csrf_token' => bc_csrf($page['body']),
'name' => 'Editier-Testmitglied',
'email' => "billingcap-edit-{$suffix}@test.local",
'paypalname' => '',
]));
$editId = (int)$pdo->query("SELECT id FROM participants WHERE tenant_id = {$tenantId} AND email = 'billingcap-edit-{$suffix}@test.local'")->fetchColumn();
$page = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies);
$bodyEditBlocked = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([
'aktion' => 'bearbeitenspeichern',
'csrf_token' => bc_csrf($page['body']),
'mitgliedID' => (string)$editId,
'name' => 'Editier-Testmitglied',
'email' => "billingcap-edit-{$suffix}@test.local",
'paypalname' => '',
'aktiv' => '1',
]));
bc_assert('bearbeitenspeichern-Reaktivierung bei vollem Cap wird abgelehnt', str_contains($bodyEditBlocked['body'], 'maximal 10 aktive Mitglieder'), $failures, $passes);
// Editieren ohne Statuswechsel (bleibt inaktiv) muss weiterhin klappen.
$page = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies);
$bodyEditOk = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([
'aktion' => 'bearbeitenspeichern',
'csrf_token' => bc_csrf($page['body']),
'mitgliedID' => (string)$editId,
'name' => 'Editier-Testmitglied Neu',
'email' => "billingcap-edit-{$suffix}@test.local",
'paypalname' => '',
]));
bc_assert('bearbeitenspeichern ohne Aktivierung bei vollem Cap klappt', str_contains($bodyEditOk['body'], 'wurde gespeichert'), $failures, $passes);
// Aufraeumen
$pdo->prepare('DELETE FROM participants WHERE tenant_id = ?')->execute([$tenantId]);
$pdo->prepare('DELETE FROM tenant_memberships WHERE tenant_id = ?')->execute([$tenantId]);
$pdo->prepare('DELETE FROM tenant_billing WHERE tenant_id = ?')->execute([$tenantId]);
$pdo->prepare('DELETE FROM user_auth_tokens WHERE user_id = ?')->execute([$userId]);
$pdo->prepare('DELETE FROM users WHERE id = ?')->execute([$userId]);
$pdo->prepare('DELETE FROM tenant_settings WHERE tenant_id = ?')->execute([$tenantId]);
$pdo->prepare('DELETE FROM tenants WHERE id = ?')->execute([$tenantId]);
if ($failures !== []) {
echo "\nBilling capacity check failed with " . count($failures) . " failure(s):\n";
foreach ($failures as $failure) {
echo "- {$failure}\n";
}
exit(1);
}
echo "\nBilling capacity check passed with {$passes} assertions.\n";