import { BadRequestException, Injectable, NotFoundException, UnauthorizedException, } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { MeshkeeClient } from '../meshkee/meshkee.client'; import { PrismaService } from '../prisma/prisma.service'; import { CreateBusinessDto, ListBusinessesDto, RegisterShopDto, UpdateBusinessDto, } from './dto/business.dto'; function slugify(value: string) { return value .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') .slice(0, 80); } @Injectable() export class BusinessesService { constructor( private readonly prisma: PrismaService, private readonly meshkee: MeshkeeClient, ) {} async list(query: ListBusinessesDto) { const page = query.page ?? 1; const limit = Math.min(query.limit ?? 20, 100); const skip = (page - 1) * limit; const where: Prisma.BusinessWhereInput = {}; if (query.status && query.status !== 'all') { where.status = query.status; } else if (!query.status) { where.status = 'published'; } if (query.featured === true) { where.isFeatured = true; } if (query.q) { where.slug = { contains: query.q, mode: 'insensitive' }; } const [items, total] = await Promise.all([ this.prisma.business.findMany({ where, orderBy: [ { isFeatured: 'desc' }, { sortWeight: 'desc' }, { publishedAt: 'desc' }, { id: 'desc' }, ], skip, take: limit, }), this.prisma.business.count({ where }), ]); return { items, page, limit, total }; } async getBySlug(slug: string) { const business = await this.prisma.business.findUnique({ where: { slug } }); if (!business) { throw new NotFoundException(`Business "${slug}" not found`); } return business; } async getById(id: string) { const business = await this.prisma.business.findUnique({ where: { id: BigInt(id) }, }); if (!business) { throw new NotFoundException(`Business ${id} not found`); } return business; } /** * Self-serve shop signup: * 1) Meshkee business owned by the logged-in IranBay user (NOT the iranbay.com tenant) * 2) IranBay marketplace listing (pending_review) */ async registerShop(dto: RegisterShopDto, authorization?: string) { if (!authorization?.trim()) { throw new UnauthorizedException('Sign in required'); } const user = await this.meshkee.getMe(authorization); const name = dto.name.trim(); if (name.length < 2) { throw new BadRequestException('Business name is required'); } const slug = (dto.slug?.trim() || slugify(name)).slice(0, 100); if (!slug) { throw new BadRequestException('Could not build a slug from the name'); } let categoryIds = dto.meshkeeCategoryIds?.length ? this.parseMeshkeeCategoryIds(dto.meshkeeCategoryIds) : []; if (!categoryIds.length) { const categories = await this.meshkee.listBusinessCategories(); const other = categories.find((item) => String(item.slug) === 'other') ?? categories[0]; if (!other) { throw new BadRequestException('No Meshkee categories available'); } categoryIds = [Number(other.id)]; } const firstName = user.firstNameEn?.trim() || user.firstName?.trim() || 'Owner'; const lastName = user.lastNameEn?.trim() || user.lastName?.trim() || firstName; const created = await this.meshkee.createBusiness({ name, nameFa: name, slug, about: dto.about?.trim() || undefined, categoryIds, ownerFirstName: firstName, ownerLastName: lastName.length >= 2 ? lastName : firstName, ownerFirstNameEn: firstName, ownerLastNameEn: lastName.length >= 2 ? lastName : firstName, ownerCellNumber: user.cellNumber, ownerPassword: dto.ownerPassword, }); try { const listing = await this.prisma.business.create({ data: { meshkeeBusinessId: BigInt(created.id), slug: created.slug || slug, status: 'pending_review', isFeatured: false, sortWeight: 0, publishedAt: null, }, }); return { listing, meshkeeBusinessId: created.id, message: 'Shop created in Meshkee and submitted to IranBay for review.', }; } catch (error) { this.rethrowUnique(error); throw error; } } async create(dto: CreateBusinessDto, authorization?: string) { const status = dto.status ?? 'draft'; const now = new Date(); const meshkeePatch: { name?: string; slug?: string; categoryIds?: number[]; } = { slug: dto.slug }; if (dto.name?.trim()) meshkeePatch.name = dto.name.trim(); if (dto.meshkeeCategoryIds?.length) { meshkeePatch.categoryIds = this.parseMeshkeeCategoryIds( dto.meshkeeCategoryIds, ); } await this.meshkee.updateBusiness( dto.meshkeeBusinessId, meshkeePatch, authorization, ); try { return await this.prisma.business.create({ data: { meshkeeBusinessId: BigInt(dto.meshkeeBusinessId), slug: dto.slug, status, isFeatured: dto.isFeatured ?? false, sortWeight: dto.sortWeight ?? 0, publishedAt: status === 'published' ? now : null, }, }); } catch (error) { this.rethrowUnique(error); throw error; } } async update(id: string, dto: UpdateBusinessDto, authorization?: string) { const existing = await this.getById(id); const data: Prisma.BusinessUpdateInput = { slug: dto.slug, status: dto.status, isFeatured: dto.isFeatured, sortWeight: dto.sortWeight, }; if (dto.status === 'published' && existing.status !== 'published') { data.publishedAt = new Date(); } const meshkeePatch: { name?: string; slug?: string; categoryIds?: number[]; } = {}; if (dto.name !== undefined) meshkeePatch.name = dto.name; if (dto.slug !== undefined) meshkeePatch.slug = dto.slug; if (dto.meshkeeCategoryIds !== undefined) { meshkeePatch.categoryIds = this.parseMeshkeeCategoryIds( dto.meshkeeCategoryIds, ); } if (Object.keys(meshkeePatch).length) { await this.meshkee.updateBusiness( String(existing.meshkeeBusinessId), meshkeePatch, authorization, ); } try { return await this.prisma.business.update({ where: { id: BigInt(id) }, data, }); } catch (error) { this.rethrowUnique(error); throw error; } } async remove(id: string) { await this.getById(id); await this.prisma.business.delete({ where: { id: BigInt(id) } }); return { ok: true }; } private parseMeshkeeCategoryIds(ids: string[]): number[] { const parsed = [...new Set(ids)].map((id) => { const n = Number(id); if (!Number.isInteger(n) || n <= 0) { throw new BadRequestException(`Invalid Meshkee category id: ${id}`); } return n; }); if (!parsed.length) { throw new BadRequestException('meshkeeCategoryIds cannot be empty'); } return parsed; } private rethrowUnique(error: unknown): void { if ( error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002' ) { const target = Array.isArray(error.meta?.target) ? error.meta.target.join(', ') : 'field'; throw new BadRequestException(`Business ${target} already exists`); } } }