import { Injectable, NotFoundException } from '@nestjs/common'; import type { AutomationAction, AutomationRuleConfig, AutomationRuleDto, AutomationTrigger, CreateAutomationRuleRequest, UpdateAutomationRuleRequest, } from '@mento/shared'; import { Prisma } from '@prisma/client'; import { PrismaService } from '../prisma/prisma.service'; import { NotificationsService } from '../notifications/notifications.service'; import { LoyaltyService } from '../loyalty/loyalty.service'; export type AutomationContext = { organizationId: string; entityType: 'deal' | 'ticket'; entityId: string; title: string; personId?: string | null; companyId?: string | null; dealId?: string | null; ownerId?: string | null; assigneeId?: string | null; }; @Injectable() export class AutomationsService { constructor( private readonly prisma: PrismaService, private readonly notifications: NotificationsService, private readonly loyalty: LoyaltyService, ) {} async list(organizationId: string): Promise { const rows = await this.prisma.automationRule.findMany({ where: { organizationId }, orderBy: [{ isActive: 'desc' }, { updatedAt: 'desc' }], }); return rows.map((r) => this.toDto(r)); } async create( organizationId: string, dto: CreateAutomationRuleRequest, ): Promise { const row = await this.prisma.automationRule.create({ data: { organizationId, name: dto.name.trim(), trigger: dto.trigger, action: dto.action, isActive: dto.isActive ?? true, config: (dto.config ?? {}) as Prisma.InputJsonValue, }, }); return this.toDto(row); } async update( organizationId: string, id: string, dto: UpdateAutomationRuleRequest, ): Promise { const existing = await this.prisma.automationRule.findFirst({ where: { id, organizationId }, }); if (!existing) throw new NotFoundException('قانون پیدا نشد'); const row = await this.prisma.automationRule.update({ where: { id }, data: { ...(dto.name !== undefined ? { name: dto.name.trim() } : {}), ...(dto.trigger !== undefined ? { trigger: dto.trigger } : {}), ...(dto.action !== undefined ? { action: dto.action } : {}), ...(dto.isActive !== undefined ? { isActive: dto.isActive } : {}), ...(dto.config !== undefined ? { config: dto.config as Prisma.InputJsonValue } : {}), }, }); return this.toDto(row); } async remove(organizationId: string, id: string): Promise { const existing = await this.prisma.automationRule.findFirst({ where: { id, organizationId }, }); if (!existing) throw new NotFoundException('قانون پیدا نشد'); await this.prisma.automationRule.delete({ where: { id } }); } async runTrigger( trigger: AutomationTrigger, ctx: AutomationContext, ): Promise { const rules = await this.prisma.automationRule.findMany({ where: { organizationId: ctx.organizationId, isActive: true, trigger, }, }); for (const rule of rules) { try { await this.execute(rule.action as AutomationAction, asConfig(rule.config), ctx); } catch { // قوانین نباید شکست عملیات اصلی را ایجاد کنند } } } private async execute( action: AutomationAction, config: AutomationRuleConfig, ctx: AutomationContext, ) { const title = (config.title?.trim() || defaultTitle(action, ctx.title)).slice(0, 200); if (action === 'create_activity') { const assigneeId = config.assigneeId?.trim() || ctx.assigneeId || ctx.ownerId || null; await this.prisma.activity.create({ data: { organizationId: ctx.organizationId, type: (config.activityType as never) || 'task', title, description: config.body?.trim() || null, personId: ctx.personId ?? null, companyId: ctx.companyId ?? null, dealId: ctx.dealId ?? (ctx.entityType === 'deal' ? ctx.entityId : null), assigneeId, }, }); if (assigneeId) { await this.notifications.create({ organizationId: ctx.organizationId, userId: assigneeId, type: 'automation', title: 'فعالیت خودکار', body: title, link: ctx.entityType === 'deal' ? `/deals/${ctx.entityId}` : `/tickets/${ctx.entityId}`, entityType: ctx.entityType, entityId: ctx.entityId, }); } return; } if (action === 'create_notification') { const userId = config.notifyUserId?.trim() || ctx.assigneeId || ctx.ownerId || null; if (!userId) return; await this.notifications.create({ organizationId: ctx.organizationId, userId, type: 'automation', title, body: config.body?.trim() || ctx.title, link: ctx.entityType === 'deal' ? `/deals/${ctx.entityId}` : `/tickets/${ctx.entityId}`, entityType: ctx.entityType, entityId: ctx.entityId, }); return; } if (action === 'add_loyalty_points') { const points = Math.max(1, Math.trunc(config.points ?? 100)); if (!ctx.personId) return; await this.loyalty.addPointsByOrg( ctx.organizationId, ctx.personId, points, ); } } private toDto(row: { id: string; name: string; isActive: boolean; trigger: string; action: string; config: unknown; createdAt: Date; updatedAt: Date; }): AutomationRuleDto { return { id: row.id, name: row.name, isActive: row.isActive, trigger: row.trigger as AutomationTrigger, action: row.action as AutomationAction, config: asConfig(row.config), createdAt: row.createdAt.toISOString(), updatedAt: row.updatedAt.toISOString(), }; } } function asConfig(raw: unknown): AutomationRuleConfig { if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {}; return raw as AutomationRuleConfig; } function defaultTitle(action: AutomationAction, entityTitle: string) { if (action === 'create_activity') return `پیگیری خودکار: ${entityTitle}`; if (action === 'create_notification') return `اتوماسیون: ${entityTitle}`; return entityTitle; }