Files
kaffeekasse-saas/scripts/backfill-default-tenant.php
T
clemens 9f2e1bdfb1 M3 Tenant-Aufloesung per App-Session ergaenzen
- Mandantenauswahl fuer Benutzer mit mehreren Tenants bauen
- feste Tenant-Domains ohne Wildcard-Abhaengigkeit vorbereiten
- Login, Backfill, Smoke-Checks und M3-Dokumentation aktualisieren
2026-07-13 20:04:20 +02:00

237 lines
7.6 KiB
PHP

<?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,
'stale_participants_removed' => 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),
legacy_mitarbeiter_id = VALUES(legacy_mitarbeiter_id)'
);
$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']++;
}
$removeStaleParticipants = $pdo->prepare(
'DELETE p
FROM participants p
LEFT JOIN kl_Mitarbeiter m
ON m.MitarbeiterID = p.legacy_mitarbeiter_id
WHERE p.tenant_id = ?
AND p.legacy_mitarbeiter_id IS NOT NULL
AND m.MitarbeiterID IS NULL'
);
$removeStaleParticipants->execute([$tenantId]);
$stats['stale_participants_removed'] = $removeStaleParticipants->rowCount();
$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 "Stale participants removed: {$stats['stale_participants_removed']}\n";
echo "Users upserted: {$stats['users']}\n";
echo "Memberships upserted: {$stats['memberships']}\n";