Separate modules for post-sales kanban, person points/tiers, and deal-won/ticket-resolved rules without overlapping the sales pipeline. Co-authored-by: Cursor <cursoragent@cursor.com>
224 lines
6.5 KiB
TypeScript
224 lines
6.5 KiB
TypeScript
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<AutomationRuleDto[]> {
|
|
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<AutomationRuleDto> {
|
|
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<AutomationRuleDto> {
|
|
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<void> {
|
|
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<void> {
|
|
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;
|
|
}
|