diff --git a/src/tenants/tenants.service.ts b/src/tenants/tenants.service.ts index 2bd6030..96cb197 100644 --- a/src/tenants/tenants.service.ts +++ b/src/tenants/tenants.service.ts @@ -69,16 +69,23 @@ export class TenantsService { } async create(dto: CreateTenantDto, userId?: string) { - const aliases = this.uniqueNormalizedHosts(dto.domains); + const slug = dto.slug.toLowerCase().trim(); + const domain = this.normalizeDomain(dto.domain); + const aliases = this.uniqueNormalizedHosts(dto.domains).filter( + (host) => this.hostKey(host) !== this.hostKey(domain), + ); + + await this.assertSlugDomainAvailable({ slug, domain }); + try { const tenant = await this.prisma.$transaction(async (tx: any) => { const created = await tx.tenant.create({ data: { product: dto.product, - slug: dto.slug.toLowerCase(), + slug, nameFa: dto.nameFa, nameEn: dto.nameEn, - domain: this.normalizeDomain(dto.domain), + domain, domains: aliases as unknown as Prisma.InputJsonValue, avatarUrl: dto.avatarUrl, primaryColor: dto.primaryColor, @@ -140,8 +147,23 @@ export class TenantsService { throw new NotFoundException('نسخه یافت نشد'); } - const nextAliases = + const nextDomain = + dto.domain !== undefined ? this.normalizeDomain(dto.domain) : before.domain; + const nextAliasesRaw = dto.domains !== undefined ? this.uniqueNormalizedHosts(dto.domains) : undefined; + const nextAliases = + nextAliasesRaw !== undefined + ? nextAliasesRaw.filter((host) => this.hostKey(host) !== this.hostKey(nextDomain)) + : undefined; + + if (dto.slug !== undefined || dto.domain !== undefined) { + await this.assertSlugDomainAvailable({ + slug: dto.slug !== undefined ? dto.slug.toLowerCase().trim() : undefined, + domain: dto.domain !== undefined ? nextDomain : undefined, + excludeId: id, + }); + } + const aliasesChanged = nextAliases !== undefined && JSON.stringify(this.uniqueNormalizedHosts(this.readDomains(before.domains))) !== @@ -381,11 +403,52 @@ export class TenantsService { return `${Date.now()}`; } + private async assertSlugDomainAvailable(opts: { + slug?: string; + domain?: string; + excludeId?: string; + }) { + if (opts.slug) { + const bySlug = await this.prisma.tenant.findUnique({ + where: { slug: opts.slug }, + select: { id: true, nameFa: true, domain: true, slug: true }, + }); + if (bySlug && bySlug.id !== opts.excludeId) { + throw new ConflictException( + `slug «${opts.slug}» قبلاً برای نسخه «${bySlug.nameFa}» (${bySlug.domain}) ثبت شده است`, + ); + } + } + + if (opts.domain) { + const byDomain = await this.prisma.tenant.findUnique({ + where: { domain: opts.domain }, + select: { id: true, nameFa: true, domain: true, slug: true }, + }); + if (byDomain && byDomain.id !== opts.excludeId) { + throw new ConflictException( + `دامنه «${opts.domain}» قبلاً دامنهٔ اصلی نسخه «${byDomain.nameFa}» (slug: ${byDomain.slug}) است`, + ); + } + } + } + private handleUnique(error: unknown) { if ( error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002' ) { + const targets = Array.isArray(error.meta?.target) + ? (error.meta?.target as string[]) + : []; + if (targets.includes('slug')) { + throw new ConflictException('slug تکراری است — این slug قبلاً برای نسخه دیگری ثبت شده'); + } + if (targets.includes('domain')) { + throw new ConflictException( + 'دامنه تکراری است — این دامنه قبلاً دامنهٔ اصلی نسخه دیگری است', + ); + } throw new ConflictException('slug یا domain تکراری است'); } }