import prisma from '../../config/database'; import { AppError } from '../../shared/middleware/errorHandler'; import { AuditLogger } from '../../shared/utils/auditLogger'; import { Prisma } from '@prisma/client'; interface CreateDealData { name: string; contactId: string; structure: string; // B2B, B2C, B2G, PARTNERSHIP pipelineId: string; stage: string; estimatedValue: number; probability?: number; expectedCloseDate?: Date; ownerId: string; fiscalYear: number; } interface UpdateDealData extends Partial { stage?: string; actualValue?: number; actualCloseDate?: Date; wonReason?: string; lostReason?: string; status?: string; } class DealsService { async create(data: CreateDealData, userId: string) { // Generate deal number const dealNumber = await this.generateDealNumber(); const deal = await prisma.deal.create({ data: { dealNumber, name: data.name, contactId: data.contactId, structure: data.structure, pipelineId: data.pipelineId, stage: data.stage, estimatedValue: data.estimatedValue, probability: data.probability, expectedCloseDate: data.expectedCloseDate, ownerId: data.ownerId, fiscalYear: data.fiscalYear, currency: 'SAR', }, include: { contact: { select: { id: true, name: true, email: true, phone: true, }, }, owner: { select: { id: true, email: true, username: true, }, }, pipeline: true, }, }); await AuditLogger.log({ entityType: 'DEAL', entityId: deal.id, action: 'CREATE', userId, }); return deal; } async findAll(filters: any, page: number, pageSize: number) { const skip = (page - 1) * pageSize; const where: Prisma.DealWhereInput = {}; if (filters.search) { where.OR = [ { name: { contains: filters.search, mode: 'insensitive' } }, { dealNumber: { contains: filters.search } }, ]; } if (filters.structure) { where.structure = filters.structure; } if (filters.stage) { where.stage = filters.stage; } if (filters.status) { where.status = filters.status; } if (filters.ownerId) { where.ownerId = filters.ownerId; } if (filters.fiscalYear) { where.fiscalYear = parseInt(filters.fiscalYear); } const total = await prisma.deal.count({ where }); const deals = await prisma.deal.findMany({ where, skip, take: pageSize, include: { contact: { select: { id: true, name: true, email: true, }, }, owner: { select: { id: true, email: true, username: true, }, }, pipeline: true, }, orderBy: { createdAt: 'desc', }, }); return { deals, total, page, pageSize, }; } async findById(id: string) { const deal = await prisma.deal.findUnique({ where: { id }, include: { contact: { include: { categories: true, }, }, owner: { select: { id: true, email: true, username: true, employee: { select: { firstName: true, lastName: true, position: true, department: true, }, }, }, }, pipeline: true, quotes: { orderBy: { version: 'desc', }, }, costSheets: { orderBy: { version: 'desc', }, }, activities: { orderBy: { createdAt: 'desc', }, take: 20, }, notes: { orderBy: { createdAt: 'desc', }, }, attachments: { orderBy: { uploadedAt: 'desc', }, }, contracts: { orderBy: { createdAt: 'desc', }, }, invoices: { orderBy: { createdAt: 'desc', }, }, }, }); if (!deal) { throw new AppError(404, 'الصفقة غير موجودة - Deal not found'); } return deal; } async update(id: string, data: UpdateDealData, userId: string) { const existing = await prisma.deal.findUnique({ where: { id } }); if (!existing) { throw new AppError(404, 'الصفقة غير موجودة - Deal not found'); } const deal = await prisma.deal.update({ where: { id }, data: { name: data.name, contactId: data.contactId, stage: data.stage, estimatedValue: data.estimatedValue, actualValue: data.actualValue, probability: data.probability, expectedCloseDate: data.expectedCloseDate, actualCloseDate: data.actualCloseDate, wonReason: data.wonReason, lostReason: data.lostReason, status: data.status, }, include: { contact: true, owner: true, pipeline: true, }, }); await AuditLogger.log({ entityType: 'DEAL', entityId: deal.id, action: 'UPDATE', userId, changes: { before: existing, after: deal, }, }); return deal; } async updateStage(id: string, stage: string, userId: string) { const deal = await prisma.deal.update({ where: { id }, data: { stage }, include: { contact: true, owner: true, }, }); await AuditLogger.log({ entityType: 'DEAL', entityId: deal.id, action: 'STAGE_CHANGE', userId, changes: { stage }, }); // Create notification await prisma.notification.create({ data: { userId: deal.ownerId, type: 'DEAL_STAGE_CHANGED', title: 'تغيير مرحلة الصفقة - Deal stage changed', message: `تم تغيير مرحلة الصفقة "${deal.name}" إلى "${stage}"`, entityType: 'DEAL', entityId: deal.id, }, }); return deal; } async win(id: string, actualValue: number, wonReason: string, userId: string) { const deal = await prisma.deal.update({ where: { id }, data: { status: 'WON', stage: 'WON', actualValue, wonReason, actualCloseDate: new Date(), }, include: { contact: true, owner: true, }, }); await AuditLogger.log({ entityType: 'DEAL', entityId: deal.id, action: 'WIN', userId, changes: { status: 'WON', actualValue, wonReason, }, }); // Create notification await prisma.notification.create({ data: { userId: deal.ownerId, type: 'DEAL_WON', title: '🎉 صفقة رابحة - Deal Won!', message: `تم الفوز بالصفقة "${deal.name}" بقيمة ${actualValue} ريال`, entityType: 'DEAL', entityId: deal.id, }, }); return deal; } async lose(id: string, lostReason: string, userId: string) { const deal = await prisma.deal.update({ where: { id }, data: { status: 'LOST', stage: 'LOST', lostReason, actualCloseDate: new Date(), }, include: { contact: true, owner: true, }, }); await AuditLogger.log({ entityType: 'DEAL', entityId: deal.id, action: 'LOSE', userId, changes: { status: 'LOST', lostReason, }, }); return deal; } async getHistory(id: string) { return AuditLogger.getEntityHistory('DEAL', id); } private async generateDealNumber(): Promise { const year = new Date().getFullYear(); const prefix = `DEAL-${year}-`; const lastDeal = await prisma.deal.findFirst({ where: { dealNumber: { startsWith: prefix, }, }, orderBy: { createdAt: 'desc', }, select: { dealNumber: true, }, }); let nextNumber = 1; if (lastDeal) { const lastNumber = parseInt(lastDeal.dealNumber.split('-')[2]); nextNumber = lastNumber + 1; } return `${prefix}${nextNumber.toString().padStart(6, '0')}`; } } export const dealsService = new DealsService();