import { BadRequestException, ForbiddenException, Injectable, NotFoundException, } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { AuthUser } from '../auth/auth.types'; import { PermissionsService } from '../auth/permissions.service'; import { PrismaService } from '../prisma/prisma.service'; import { TenantService } from '../tenant/tenant.service'; import { CreateWebsiteBrandGroupDto, ListWebsiteBrandGroupsDto, UpdateWebsiteBrandGroupDto, } from './dto/website-brand-groups.dto'; const groupInclude = { website_brand_group_items: { orderBy: [{ sort_order: 'asc' as const }, { id: 'asc' as const }], include: { brands: { include: { imageMedia: true }, }, }, }, } satisfies Prisma.website_brand_groupsInclude; type GroupWithItems = Prisma.website_brand_groupsGetPayload<{ include: typeof groupInclude; }>; @Injectable() export class WebsiteBrandGroupsService { constructor( private readonly prisma: PrismaService, private readonly permissions: PermissionsService, private readonly tenant: TenantService, ) {} async listPublic(host: string) { const business = await this.tenant.resolveBusinessByDomain(host); const groups = await this.prisma.website_brand_groups.findMany({ where: { business_id: business.id, is_active: true }, orderBy: [{ sort_order: 'asc' }, { created_at: 'asc' }], include: groupInclude, }); return { items: groups.map((group) => this.serializeGroup(group)), }; } async list( businessIdRaw: string, query: ListWebsiteBrandGroupsDto, actor: AuthUser, ) { const businessId = BigInt(businessIdRaw); await this.assertPermission(businessId, actor.id, 'website.read'); const page = query.page ?? 1; const pageSize = query.pageSize ?? 20; const skip = (page - 1) * pageSize; const where: Prisma.website_brand_groupsWhereInput = { business_id: businessId, ...(query.isActive !== undefined ? { is_active: query.isActive } : {}), }; const [items, total] = await Promise.all([ this.prisma.website_brand_groups.findMany({ where, orderBy: [{ sort_order: 'asc' }, { created_at: 'asc' }], skip, take: pageSize, include: groupInclude, }), this.prisma.website_brand_groups.count({ where }), ]); return { items: items.map((group) => this.serializeGroup(group)), total, page, pageSize, }; } async getOne( businessIdRaw: string, groupIdRaw: string, actor: AuthUser, ) { const businessId = BigInt(businessIdRaw); const groupId = BigInt(groupIdRaw); await this.assertPermission(businessId, actor.id, 'website.read'); const group = await this.findGroupOrThrow(businessId, groupId); return { group: this.serializeGroup(group) }; } async create( businessIdRaw: string, dto: CreateWebsiteBrandGroupDto, actor: AuthUser, ) { const businessId = BigInt(businessIdRaw); await this.assertPermission(businessId, actor.id, 'website.update'); const brandIds = this.parseUniqueIds(dto.brandIds ?? []); if (brandIds.length > 0) { await this.assertBrandsBelongToBusiness(businessId, brandIds); } const created = await this.prisma.$transaction(async (tx) => { const group = await tx.website_brand_groups.create({ data: { business_id: businessId, title: dto.title.trim(), sort_order: dto.sortOrder ?? 0, is_active: dto.isActive ?? true, }, }); await this.replaceItems(tx, group.id, brandIds); return tx.website_brand_groups.findUniqueOrThrow({ where: { id: group.id }, include: groupInclude, }); }); return { message: 'Website brand group created successfully', group: this.serializeGroup(created), }; } async update( businessIdRaw: string, groupIdRaw: string, dto: UpdateWebsiteBrandGroupDto, actor: AuthUser, ) { const businessId = BigInt(businessIdRaw); const groupId = BigInt(groupIdRaw); await this.assertPermission(businessId, actor.id, 'website.update'); await this.findGroupOrThrow(businessId, groupId); let brandIds: bigint[] | undefined; if (dto.brandIds !== undefined) { brandIds = this.parseUniqueIds(dto.brandIds); await this.assertBrandsBelongToBusiness(businessId, brandIds); } const updated = await this.prisma.$transaction(async (tx) => { await tx.website_brand_groups.update({ where: { id: groupId }, data: { ...(dto.title !== undefined ? { title: dto.title.trim() } : {}), ...(dto.sortOrder !== undefined ? { sort_order: dto.sortOrder } : {}), ...(dto.isActive !== undefined ? { is_active: dto.isActive } : {}), }, }); if (brandIds !== undefined) { await this.replaceItems(tx, groupId, brandIds); } return tx.website_brand_groups.findUniqueOrThrow({ where: { id: groupId }, include: groupInclude, }); }); return { message: 'Website brand group updated successfully', group: this.serializeGroup(updated), }; } async remove( businessIdRaw: string, groupIdRaw: string, actor: AuthUser, ) { const businessId = BigInt(businessIdRaw); const groupId = BigInt(groupIdRaw); await this.assertPermission(businessId, actor.id, 'website.update'); await this.findGroupOrThrow(businessId, groupId); await this.prisma.website_brand_groups.delete({ where: { id: groupId } }); return { message: 'Website brand group deleted successfully' }; } private async findGroupOrThrow(businessId: bigint, groupId: bigint) { const group = await this.prisma.website_brand_groups.findFirst({ where: { id: groupId, business_id: businessId }, include: groupInclude, }); if (!group) { throw new NotFoundException('Website brand group not found'); } return group; } private parseUniqueIds(ids: string[]) { const unique = [...new Set(ids.map((id) => id.trim()).filter(Boolean))]; return unique.map((id) => BigInt(id)); } private async assertBrandsBelongToBusiness( businessId: bigint, brandIds: bigint[], ) { const found = await this.prisma.brand.findMany({ where: { businessId, id: { in: brandIds } }, select: { id: true }, }); if (found.length !== brandIds.length) { throw new BadRequestException( 'One or more brands were not found for this business', ); } } private async replaceItems( tx: Prisma.TransactionClient, groupId: bigint, brandIds: bigint[], ) { await tx.website_brand_group_items.deleteMany({ where: { group_id: groupId } }); if (brandIds.length === 0) { return; } await tx.website_brand_group_items.createMany({ data: brandIds.map((brandId, index) => ({ group_id: groupId, brand_id: brandId, sort_order: index, })), }); } private serializeGroup(group: GroupWithItems) { const items = group.website_brand_group_items.map((entry) => { const brand = entry.brands; return { id: brand.id.toString(), nameEn: brand.nameEn, nameFa: brand.nameFa, slug: brand.slug, about: brand.about, imageMediaId: brand.imageMediaId?.toString() ?? null, imageUrl: brand.imageMedia?.publicUrl ?? null, sortOrder: entry.sort_order, }; }); return { id: group.id.toString(), title: group.title, sortOrder: group.sort_order, isActive: group.is_active, createdAt: group.created_at, updatedAt: group.updated_at, items, }; } private async assertPermission( businessId: bigint, userId: bigint, permission: string, ) { const allowed = await this.permissions.hasBusinessPermission( userId, businessId, permission, ); if (!allowed) { throw new ForbiddenException( `Missing permission: ${permission} for this business`, ); } } }