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 { CreateWebsiteSliderDto, ListWebsiteSlidersDto, UpdateWebsiteSliderDto, WebsiteSliderSlideInputDto, } from './dto/website-sliders.dto'; const sliderInclude = { website_slider_slides: { orderBy: [{ sort_order: 'asc' as const }, { id: 'asc' as const }], include: { media: true, }, }, } satisfies Prisma.website_slidersInclude; type SliderWithSlides = Prisma.website_slidersGetPayload<{ include: typeof sliderInclude; }>; @Injectable() export class WebsiteSlidersService { 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 sliders = await this.prisma.website_sliders.findMany({ where: { business_id: business.id, is_active: true }, orderBy: [{ sort_order: 'asc' }, { created_at: 'asc' }], include: sliderInclude, }); return { items: sliders.map((slider) => this.serializeSlider(slider, true)), }; } async list( businessIdRaw: string, query: ListWebsiteSlidersDto, 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_slidersWhereInput = { business_id: businessId, ...(query.isActive !== undefined ? { is_active: query.isActive } : {}), }; const [items, total] = await Promise.all([ this.prisma.website_sliders.findMany({ where, orderBy: [{ sort_order: 'asc' }, { created_at: 'asc' }], skip, take: pageSize, include: sliderInclude, }), this.prisma.website_sliders.count({ where }), ]); return { items: items.map((slider) => this.serializeSlider(slider, false)), total, page, pageSize, }; } async getOne( businessIdRaw: string, sliderIdRaw: string, actor: AuthUser, ) { const businessId = BigInt(businessIdRaw); const sliderId = BigInt(sliderIdRaw); await this.assertPermission(businessId, actor.id, 'website.read'); const slider = await this.findSliderOrThrow(businessId, sliderId); return { slider: this.serializeSlider(slider, false) }; } async create( businessIdRaw: string, dto: CreateWebsiteSliderDto, actor: AuthUser, ) { const businessId = BigInt(businessIdRaw); await this.assertPermission(businessId, actor.id, 'website.update'); const slides = dto.slides ?? []; await this.assertSlideMedia(businessId, slides); const created = await this.prisma.$transaction(async (tx) => { const slider = await tx.website_sliders.create({ data: { business_id: businessId, title: dto.title.trim(), sort_order: dto.sortOrder ?? 0, is_active: dto.isActive ?? true, }, }); await this.replaceSlides(tx, slider.id, slides); return tx.website_sliders.findUniqueOrThrow({ where: { id: slider.id }, include: sliderInclude, }); }); return { message: 'Website slider created successfully', slider: this.serializeSlider(created, false), }; } async update( businessIdRaw: string, sliderIdRaw: string, dto: UpdateWebsiteSliderDto, actor: AuthUser, ) { const businessId = BigInt(businessIdRaw); const sliderId = BigInt(sliderIdRaw); await this.assertPermission(businessId, actor.id, 'website.update'); await this.findSliderOrThrow(businessId, sliderId); let slides: WebsiteSliderSlideInputDto[] | undefined; if (dto.slides !== undefined) { slides = dto.slides; await this.assertSlideMedia(businessId, slides); } const updated = await this.prisma.$transaction(async (tx) => { await tx.website_sliders.update({ where: { id: sliderId }, data: { ...(dto.title !== undefined ? { title: dto.title.trim() } : {}), ...(dto.sortOrder !== undefined ? { sort_order: dto.sortOrder } : {}), ...(dto.isActive !== undefined ? { is_active: dto.isActive } : {}), }, }); if (slides !== undefined) { await this.replaceSlides(tx, sliderId, slides); } return tx.website_sliders.findUniqueOrThrow({ where: { id: sliderId }, include: sliderInclude, }); }); return { message: 'Website slider updated successfully', slider: this.serializeSlider(updated, false), }; } async remove( businessIdRaw: string, sliderIdRaw: string, actor: AuthUser, ) { const businessId = BigInt(businessIdRaw); const sliderId = BigInt(sliderIdRaw); await this.assertPermission(businessId, actor.id, 'website.update'); await this.findSliderOrThrow(businessId, sliderId); await this.prisma.website_sliders.delete({ where: { id: sliderId } }); return { message: 'Website slider deleted successfully' }; } private async findSliderOrThrow(businessId: bigint, sliderId: bigint) { const slider = await this.prisma.website_sliders.findFirst({ where: { id: sliderId, business_id: businessId }, include: sliderInclude, }); if (!slider) { throw new NotFoundException('Website slider not found'); } return slider; } private async assertSlideMedia( businessId: bigint, slides: WebsiteSliderSlideInputDto[], ) { if (slides.length === 0) { return; } const mediaIds = slides.map((slide) => BigInt(slide.imageMediaId)); const uniqueMediaIds = [...new Set(mediaIds.map((id) => id.toString()))].map( (id) => BigInt(id), ); const found = await this.prisma.media.findMany({ where: { businessId, id: { in: uniqueMediaIds }, mimeType: { startsWith: 'image/' }, }, select: { id: true }, }); if (found.length !== uniqueMediaIds.length) { throw new BadRequestException( 'One or more slide images were not found for this business', ); } } private async replaceSlides( tx: Prisma.TransactionClient, sliderId: bigint, slides: WebsiteSliderSlideInputDto[], ) { await tx.website_slider_slides.deleteMany({ where: { slider_id: sliderId } }); if (slides.length === 0) { return; } await tx.website_slider_slides.createMany({ data: slides.map((slide, index) => ({ slider_id: sliderId, image_media_id: BigInt(slide.imageMediaId), title: slide.title?.trim() || null, link_url: slide.linkUrl?.trim() || null, sort_order: index, is_active: slide.isActive ?? true, })), }); } private serializeSlider(slider: SliderWithSlides, publicView: boolean) { const slides = slider.website_slider_slides .filter((slide) => !publicView || slide.is_active) .map((slide) => ({ id: slide.id.toString(), imageMediaId: slide.image_media_id.toString(), imageUrl: slide.media.publicUrl, title: slide.title, linkUrl: slide.link_url, sortOrder: slide.sort_order, isActive: slide.is_active, createdAt: slide.created_at, updatedAt: slide.updated_at, })); return { id: slider.id.toString(), title: slider.title, sortOrder: slider.sort_order, isActive: slider.is_active, createdAt: slider.created_at, updatedAt: slider.updated_at, slides, }; } 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`, ); } } }