import { Body, Controller, Get, HttpCode, HttpException, HttpStatus, Param, Post, Query, UseGuards, } from '@nestjs/common'; import { IsInt, IsOptional, IsString, Max, MaxLength, Min, MinLength } from 'class-validator'; import { Type } from 'class-transformer'; import { CurrentUser } from '../auth/current-user.decorator'; import type { Prisma } from '@prisma/client'; import { PrismaService } from '../prisma/prisma.service'; import { buildSearchWhere, ci } from '../common/search'; import { LeadsService } from '../leads/leads.service'; import { ApiKeyAuthGuard } from './api-key-auth.guard'; class CreateExternalLeadDto { @IsOptional() @IsString() @MaxLength(80) firstName?: string; @IsOptional() @IsString() @MaxLength(80) lastName?: string; @IsString() @MinLength(8) @MaxLength(20) mobile!: string; @IsOptional() @IsString() @MaxLength(200) storeName?: string; @IsOptional() @IsString() @MaxLength(200) source?: string; } const LEAD_RATE_WINDOW_MS = 60_000; const LEAD_RATE_MAX = 60; const leadHits = new Map(); class ListQueryDto { @IsOptional() @IsString() q?: string; @IsOptional() @Type(() => Number) @IsInt() @Min(1) page?: number = 1; @IsOptional() @Type(() => Number) @IsInt() @Min(1) @Max(100) limit?: number = 20; } @Controller('public/v1') @UseGuards(ApiKeyAuthGuard) export class PublicApiV1Controller { constructor( private readonly prisma: PrismaService, private readonly leads: LeadsService, ) {} @Post('leads') @HttpCode(200) createLead( @CurrentUser() user: { organizationId: string; apiKeyId?: string; apiKeyName?: string }, @Body() body: CreateExternalLeadDto, ) { const bucket = user.apiKeyId || user.organizationId; const now = Date.now(); const prev = (leadHits.get(bucket) ?? []).filter((t) => now - t < LEAD_RATE_WINDOW_MS); if (prev.length >= LEAD_RATE_MAX) { throw new HttpException('تعداد ارسال بیش از حد مجاز است. کمی بعد دوباره تلاش کنید.', HttpStatus.TOO_MANY_REQUESTS); } prev.push(now); leadHits.set(bucket, prev); return this.leads.createFromExternal({ organizationId: user.organizationId, siteName: user.apiKeyName?.trim() || 'سایت بیرونی', firstName: body.firstName, lastName: body.lastName, mobile: body.mobile, storeName: body.storeName, source: body.source, }); } @Get('people') async listPeople( @CurrentUser() user: { organizationId: string }, @Query() query: ListQueryDto, ) { const page = query.page ?? 1; const limit = query.limit ?? 20; const search = buildSearchWhere(query.q, (t) => [ { firstName: ci(t) }, { lastName: ci(t) }, { mobile: { contains: t } }, { phone: { contains: t } }, { email: ci(t) }, ]); const where: Prisma.PersonWhereInput = { organizationId: user.organizationId, ...(search ? { AND: [search] } : {}), }; const [total, items] = await Promise.all([ this.prisma.person.count({ where }), this.prisma.person.findMany({ where, select: { id: true, firstName: true, lastName: true, email: true, mobile: true, phone: true, createdAt: true, updatedAt: true, }, orderBy: { updatedAt: 'desc' }, skip: (page - 1) * limit, take: limit, }), ]); return { total, page, limit, items: items.map((p) => ({ ...p, fullName: `${p.firstName} ${p.lastName}`.trim(), createdAt: p.createdAt.toISOString(), updatedAt: p.updatedAt.toISOString(), })), }; } @Get('people/:id') async getPerson( @CurrentUser() user: { organizationId: string }, @Param('id') id: string, ) { const p = await this.prisma.person.findFirst({ where: { id, organizationId: user.organizationId }, select: { id: true, firstName: true, lastName: true, email: true, mobile: true, phone: true, notes: true, tags: true, createdAt: true, updatedAt: true, }, }); if (!p) return null; return { ...p, fullName: `${p.firstName} ${p.lastName}`.trim(), createdAt: p.createdAt.toISOString(), updatedAt: p.updatedAt.toISOString(), }; } @Get('deals') async listDeals( @CurrentUser() user: { organizationId: string }, @Query() query: ListQueryDto, ) { const page = query.page ?? 1; const limit = query.limit ?? 20; const search = buildSearchWhere(query.q, (t) => [{ title: ci(t) }]); const where: Prisma.DealWhereInput = { organizationId: user.organizationId, ...(search ? { AND: [search] } : {}), }; const [total, items] = await Promise.all([ this.prisma.deal.count({ where }), this.prisma.deal.findMany({ where, select: { id: true, title: true, status: true, amount: true, currency: true, source: true, stageId: true, pipelineId: true, createdAt: true, updatedAt: true, }, orderBy: { updatedAt: 'desc' }, skip: (page - 1) * limit, take: limit, }), ]); return { total, page, limit, items: items.map((d) => ({ ...d, amount: d.amount == null ? null : Number(d.amount), createdAt: d.createdAt.toISOString(), updatedAt: d.updatedAt.toISOString(), })), }; } }