Files
CRM_Montzar/apps/api/src/public-api/public-api-v1.controller.ts
T

231 lines
5.6 KiB
TypeScript

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<string, number[]>();
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<Prisma.PersonWhereInput>(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<Prisma.DealWhereInput>(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(),
})),
};
}
}