import UrlUtils from "../app/UrlUtils"; import { reservedTenantChildRouteSlugs, reservedTenantRouteSlugs } from "../app/PublicRouteSegments"; import { db } from "../db.server"; import { deleteTenant } from "../db/tenants.db.server"; import { getTenantSubscription } from "../db/tenantSubscriptions.db.server"; import { deleteStripeCustomer } from "../stripe.server"; import { TFunction } from "i18next"; import { cancelTenantSubscription } from "./.server/subscriptionService"; export function normalizeTenantSlug(slug: string) { return UrlUtils.slugify(slug).toLowerCase(); } export const reservedTenantSlugs = new Set(reservedTenantRouteSlugs.map((slug) => normalizeTenantSlug(slug))); export const reservedTenantChildSlugs = new Set(reservedTenantChildRouteSlugs.map((slug) => normalizeTenantSlug(slug))); export function isReservedTenantSlug(slug: string) { return reservedTenantSlugs.has(normalizeTenantSlug(slug)); } export function isReservedTenantChildRouteSlug(slug: string) { return reservedTenantChildSlugs.has(normalizeTenantSlug(slug)); } // Mindestlaenge eines Tenant-Slugs. Ein-Zeichen-Slugs kollidieren mit reservierten // Kind-Routen (z.B. "/g") und sind als Workspace-URL ohnehin ungeeignet. export const MIN_TENANT_SLUG_LENGTH = 2; // Fallback-Basis, falls ein Firmenname keinen verwertbaren Slug ergibt (reine // Sonderzeichen -> slugify liefert ""). Verhindert leere/numerische Slugs ("", "1"). const FALLBACK_TENANT_SLUG_BASE = "workspace"; function tenantSlugBaseFromName(name: string) { const base = normalizeTenantSlug(name ?? ""); return base.length > 0 ? base : FALLBACK_TENANT_SLUG_BASE; } export type TenantSlugValidation = { ok: true; slug: string } | { ok: false; reason: "empty" | "too-short" | "reserved" }; // Synchrone Validierung eines vom Nutzer GEWAEHLTEN Slugs (Signup-Formular): liefert // einen typisierten Grund fuer Klartext-Fehlermeldungen, statt den Wunsch still // umzuschreiben. Die DB-Eindeutigkeitspruefung bleibt in getAvailableTenantSlug. export function validateDesiredTenantSlug(input: string): TenantSlugValidation { const slug = normalizeTenantSlug(input ?? ""); if (slug.length === 0) { return { ok: false, reason: "empty" }; } if (slug.length < MIN_TENANT_SLUG_LENGTH) { return { ok: false, reason: "too-short" }; } if (isReservedTenantSlug(slug)) { return { ok: false, reason: "reserved" }; } return { ok: true, slug }; } export async function deleteAndCancelTenant({ tenantId, userId, t }: { tenantId: string; userId: string; t?: TFunction }) { const tenantSubscription = await getTenantSubscription(tenantId); await cancelAllTenantPlans({ t, tenantId, userId }); if (tenantSubscription?.stripeCustomerId) { await deleteStripeCustomer(tenantSubscription?.stripeCustomerId); } return await deleteTenant(tenantId); } export async function cancelAllTenantPlans({ tenantId, userId, t }: { tenantId: string; userId: string; t?: TFunction }) { const tenantSubscription = await getTenantSubscription(tenantId); if (tenantSubscription?.products) { await Promise.all( tenantSubscription.products.map(async (tenantSubscriptionProduct) => { await cancelTenantSubscription(tenantSubscriptionProduct.id, { tenantId, userId, t, }); }) ); } } export async function getAvailableTenantSlug({ name, slug }: { name: string; slug?: string }) { const base = tenantSlugBaseFromName(name); slug = slug === undefined ? base : normalizeTenantSlug(slug); // Degenerierter Wunsch-Slug (leer nach Normalisierung) faellt auf die Namens-Basis zurueck. if (slug.length === 0) { slug = base; } let tries = 1; do { const existingSlug = await getExistingSlug(slug); if (existingSlug) { slug = base + tries.toString(); tries++; } else { break; } } while (true); return slug; } export async function getExistingSlug(slug: string) { if (isReservedTenantSlug(slug)) { return true; } const existing = await db.tenant.count({ where: { slug, }, }); return existing > 0; }