import { BadRequestException, ConflictException, Injectable } from '@nestjs/common'; import type { ImportContactsResult, PersonMood } from '@mento/shared'; import { PERSON_MOOD_LABELS, PERSON_MOODS } from '@mento/shared'; import { PrismaService } from '../prisma/prisma.service'; import type { Actor } from '../common/record-scope'; import { ownerWhere, resolveOwnerId } from '../common/record-scope'; import { collectAddresses, } from '../common/custom-fields'; import { formatJalaliDate, parseExcelMapping, parseJalaliDateTime, previewExcel, readMappedRows, writeXlsx, type ExcelPreviewResult, } from '../common/excel'; import { chunkImportMeta, resolveExcelImportChunk, } from '../common/excel-import-session'; import { CompaniesService } from './companies.service'; import { PeopleService } from './people.service'; import { normalizePersonImportSource } from './person-source'; type ImportChunkOpts = { sessionId?: string; offset?: string | number; limit?: string | number; }; function parseMood(raw?: string): PersonMood | undefined { const value = raw?.trim(); if (!value) return undefined; if (PERSON_MOODS.includes(value as PersonMood)) return value as PersonMood; return PERSON_MOODS.find((mood) => PERSON_MOOD_LABELS[mood] === value); } function splitDidarName(firstName: string, lastName: string, title?: string) { const first = firstName.trim(); const last = lastName.trim(); if (first && last) return { firstName: first.slice(0, 80), lastName: last.slice(0, 80) }; const full = (first || last || title || '').trim(); const parts = full.split(/\s+/).filter(Boolean); if (parts.length >= 2) { return { firstName: parts[0]!.slice(0, 80), lastName: parts.slice(1).join(' ').slice(0, 80), }; } if (full) return { firstName: full.slice(0, 80), lastName: full.slice(0, 80) }; return null; } const PERSON_ALIASES: Record = { firstname: 'firstName', نام: 'firstName', ناممشتری: 'firstName', lastname: 'lastName', نامخانوادگی: 'lastName', نامخانوادگیمشتری: 'lastName', عنوانمشتری: 'title', email: 'email', ایمیل: 'email', ایمیلمشتری: 'email', phone: 'phone', تلفن: 'phone', تلفنثابت: 'phone', تلفنثابتمشتری: 'phone', mobile: 'mobile', موبایل: 'mobile', تلفنهمراهمشتری: 'mobile', position: 'position', سمت: 'position', سمت1: 'position', company: 'companyName', companyname: 'companyName', نامشرکت: 'companyName', شرکت: 'companyName', notes: 'notes', یادداشت: 'notes', توضیحاتشخص: 'notes', mood: 'mood', وضعیت: 'mood', وضعیتمشتری: 'mood', گروهبندیشخص: 'mood', گروهمشتری: 'group', آدرس: 'address', آدرس1: 'address', آدرساصلی: 'address', address: 'address', address2: 'address2', آدرس۲: 'address2', آدرس2: 'address2', آدرس_1: 'address2', آدرسدوم: 'address2', آدرسمنزل: 'homeAddress', آدرمنزل: 'homeAddress', آدرسخانه: 'homeAddress', homeaddress: 'homeAddress', کدپستی: 'postalCode', کدپستی1: 'postalCode', استانفیلدپیشفرض: 'province', استان: 'province', شهرستان: 'city', شهر1: 'city', کدمشتری: 'customerCode', کددیدارمشتری: 'didarId', کدملیمشتری: 'nationalId', تاریخثبتمشتری: 'registeredAt', تاریختولدمشتری: 'birthDate', تاریختولد1: 'birthDate', تاریختولد: 'birthDate', منبعاطلاعات: 'source', نوعمشتری: 'customerType', type: '_type', _type: '_type', آیدیاینستاگرام: 'instagram', اینستاگرام: 'instagram', پیجاینستاگرام: 'instagram', instagram: 'instagram', معرف: 'referrer', سایتهایمشتری: 'website', تاریخثبت: 'registeredAt', registeredat: 'registeredAt', شمارهفکس: 'fax', تلفن2: 'phone2', تلفنهایدیگرمشتری: 'otherPhones', مسئولمشتری: 'ownerName', مسئول: 'ownerName', مسئولفروش: 'ownerName', کارشناسفروش: 'ownerName', کارشناس: 'ownerName', owner: 'ownerName', ownername: 'ownerName', تلفنثابتشرکت: 'companyPhone', کددیدارشرکت: 'companyDidarId', مجوزمشاهدهمشتری: 'visibility', ایمیلهایدیگرمشتری: 'otherEmails', کد1: 'customerCode', گروه1: 'group', تماسهمکار: 'colleagueContact', شرکتقبلی: 'previousCompany', ستاره: 'star', }; const PERSON_FIELDS = [ { key: 'firstName', label: 'نام' }, { key: 'lastName', label: 'نام خانوادگی' }, { key: 'title', label: 'نام کامل / عنوان' }, { key: 'didarId', label: 'کد مبدأ (کد دیدار)' }, { key: 'customerCode', label: 'کد مشتری' }, { key: 'mobile', label: 'موبایل' }, { key: 'phone', label: 'تلفن' }, { key: 'email', label: 'ایمیل' }, { key: 'position', label: 'سمت' }, { key: 'companyName', label: 'شرکت' }, { key: 'ownerName', label: 'کارشناس فروش' }, { key: 'address', label: 'آدرس' }, { key: 'address2', label: 'آدرس ۲' }, { key: 'homeAddress', label: 'آدرس منزل' }, { key: 'instagram', label: 'اینستاگرام' }, { key: 'registeredAt', label: 'تاریخ ثبت' }, { key: 'notes', label: 'یادداشت' }, { key: 'mood', label: 'وضعیت مشتری' }, ]; const COMPANY_ALIASES: Record = { type: '_type', _type: '_type', name: 'name', نام: 'name', نامشرکت: 'name', کددیدارشرکت: 'didarId', didarid: 'didarId', کدشرکت: 'code', code: 'code', استانفیلدپیشفرض: 'province', استان: 'province', province: 'province', شهرستان: 'city', شهر: 'city', city: 'city', nationalid: 'nationalId', شناسهملی: 'nationalId', شمارهاقتصادی: 'economicCode', کداقتصادی: 'economicCode', شمارهثبت: 'registrationNumber', phone: 'phone', تلفن: 'phone', تلفنشرکت: 'phone', تلفنثابتشرکت: 'phone', تلفنهایدیگرشرکت: 'otherPhones', شمارهفکس: 'fax', فکس: 'fax', fax: 'fax', email: 'email', ایمیل: 'email', ایمیلشرکت: 'email', ایمیلهایدیگرشرکت: 'otherEmails', website: 'website', وبسایت: 'website', سایتهایشرکت: 'website', کدپستی: 'postalCode', postalcode: 'postalCode', address: 'address', آدرس: 'address', notes: 'notes', یادداشت: 'notes', توضیحاتشرکت: 'notes', مسئولشرکت: 'ownerName', مسئول: 'ownerName', owner: 'ownerName', مجوزمشاهدهشرکت: 'visibility', تاریخثبتشرکت: 'registeredAt', تاریخثبت: 'registeredAt', گروهبندیشرکت: 'groupName', گروه: 'groupName', }; const COMPANY_FIELDS = [ { key: 'name', label: 'نام شرکت', required: true }, { key: 'didarId', label: 'کد دیدار شرکت' }, { key: 'code', label: 'کد شرکت' }, { key: 'province', label: 'استان' }, { key: 'city', label: 'شهرستان' }, { key: 'phone', label: 'تلفن شرکت' }, { key: 'email', label: 'ایمیل شرکت' }, { key: 'economicCode', label: 'شماره اقتصادی' }, { key: 'registrationNumber', label: 'شماره ثبت' }, { key: 'nationalId', label: 'شناسه ملی' }, { key: 'fax', label: 'شماره فکس' }, { key: 'postalCode', label: 'کد پستی' }, { key: 'notes', label: 'توضیحات شرکت' }, { key: 'ownerName', label: 'مسئول شرکت' }, { key: 'visibility', label: 'مجوز مشاهده شرکت' }, { key: 'registeredAt', label: 'تاریخ ثبت شرکت' }, { key: 'groupName', label: 'گروه بندی شرکت' }, { key: 'otherPhones', label: 'تلفن های دیگر شرکت' }, { key: 'address', label: 'آدرس' }, { key: 'website', label: 'سایت های شرکت' }, { key: 'otherEmails', label: 'ایمیل های دیگر شرکت' }, ]; const COMPANY_COLUMNS = [ { header: '_type', key: '_type', width: 12 }, { header: 'کد دیدار شرکت', key: 'didarId', width: 14 }, { header: 'کد شرکت', key: 'code', width: 12 }, { header: 'نام شرکت', key: 'name', width: 28 }, { header: 'استان (فیلد پیش فرض)', key: 'province', width: 16 }, { header: 'شهرستان', key: 'city', width: 14 }, { header: 'تلفن شرکت', key: 'phone', width: 16 }, { header: 'ایمیل شرکت', key: 'email', width: 24 }, { header: 'شماره اقتصادی', key: 'economicCode', width: 16 }, { header: 'شماره ثبت', key: 'registrationNumber', width: 14 }, { header: 'شناسه ملی', key: 'nationalId', width: 16 }, { header: 'شماره فکس', key: 'fax', width: 14 }, { header: 'کد پستی', key: 'postalCode', width: 12 }, { header: 'توضیحات شرکت', key: 'notes', width: 32 }, { header: 'مسئول شرکت', key: 'ownerName', width: 16 }, { header: 'مجوز مشاهده شرکت', key: 'visibility', width: 18 }, { header: 'تاریخ ثبت شرکت', key: 'registeredAt', width: 16 }, { header: 'گروه بندی شرکت', key: 'groupName', width: 16 }, { header: 'تلفن های دیگر شرکت', key: 'otherPhones', width: 18 }, { header: 'آدرس', key: 'address', width: 28 }, { header: 'سایت های شرکت', key: 'website', width: 20 }, { header: 'ایمیل های دیگر شرکت', key: 'otherEmails', width: 20 }, ]; function parseLooseDate(raw?: string): Date | null { const jalali = parseJalaliDateTime(raw); if (jalali) return jalali; const v = raw?.trim(); if (!v) return null; const d = new Date(v); return Number.isNaN(d.getTime()) ? null : d; } const CONTACT_COLUMNS = [ { header: '_type', key: '_type', width: 12 }, { header: 'کد دیدار مشتری', key: 'didarId', width: 16 }, { header: 'کد مشتری', key: 'customerCode', width: 14 }, { header: 'عنوان مشتری', key: 'title', width: 16 }, { header: 'نام مشتری', key: 'firstName', width: 16 }, { header: 'نام خانوادگی مشتری', key: 'lastName', width: 22 }, { header: 'ایمیل مشتری', key: 'email', width: 24 }, { header: 'شماره فکس', key: 'fax', width: 14 }, { header: 'سمت', key: 'position', width: 16 }, { header: 'کد پستی', key: 'postalCode', width: 12 }, { header: 'توضیحات شخص', key: 'notes', width: 32 }, { header: 'تلفن همراه مشتری', key: 'mobile', width: 16 }, { header: 'استان (فیلد پیش فرض)', key: 'province', width: 16 }, { header: 'شهرستان', key: 'city', width: 14 }, { header: 'تلفن ثابت مشتری', key: 'phone', width: 16 }, { header: 'تلفن ثابت شرکت', key: 'companyPhone', width: 16 }, { header: 'نام شرکت', key: 'companyName', width: 24 }, { header: 'کد دیدار شرکت', key: 'companyDidarId', width: 14 }, { header: 'مسئول مشتری', key: 'ownerName', width: 16 }, { header: 'مجوز مشاهده مشتری', key: 'visibility', width: 18 }, { header: 'کد ملی مشتری', key: 'nationalId', width: 16 }, { header: 'تاریخ ثبت مشتری', key: 'registeredAt', width: 16 }, { header: 'تاریخ تولد مشتری', key: 'birthDate', width: 16 }, { header: 'گروه بندی شخص', key: 'mood', width: 16 }, { header: 'تلفن های دیگر مشتری', key: 'otherPhones', width: 18 }, { header: 'آدرس', key: 'address', width: 28 }, { header: 'سایت های مشتری', key: 'website', width: 20 }, { header: 'ایمیل های دیگر مشتری', key: 'otherEmails', width: 20 }, { header: 'سمت_1', key: 'position2', width: 14 }, { header: 'شهر_1', key: 'city2', width: 12 }, { header: 'کد_1', key: 'code2', width: 12 }, { header: 'گروه_1', key: 'group2', width: 12 }, { header: 'آدرس_1', key: 'address2', width: 20 }, { header: 'تلفن2', key: 'phone2', width: 14 }, { header: 'کدپستی', key: 'postalCode2', width: 12 }, { header: 'معرف', key: 'referrer', width: 14 }, { header: 'آدرس منزل', key: 'homeAddress', width: 20 }, { header: 'تاریخ تولد_1', key: 'birthDate2', width: 14 }, { header: 'آی دی اینستاگرام', key: 'instagram', width: 16 }, { header: 'گروه مشتری', key: 'group', width: 16 }, { header: 'تماس همکار', key: 'colleagueContact', width: 16 }, { header: 'شرکت قبلی', key: 'previousCompany', width: 16 }, { header: 'نوع مشتری', key: 'customerType', width: 14 }, { header: 'ستاره', key: 'star', width: 10 }, { header: 'منبع اطلاعات', key: 'source', width: 16 }, ]; const CUSTOM_KEYS = [ 'didarId', 'customerCode', 'title', 'fax', 'postalCode', 'province', 'city', 'address', 'address2', 'nationalId', 'birthDate', 'group', 'otherPhones', 'website', 'otherEmails', 'instagram', 'referrer', 'source', 'customerType', 'homeAddress', 'colleagueContact', 'previousCompany', 'star', 'phone2', 'companyDidarId', 'companyPhone', 'visibility', ] as const; @Injectable() export class ContactsExcelService { constructor( private readonly prisma: PrismaService, private readonly people: PeopleService, private readonly companies: CompaniesService, ) {} async exportPeople(actor: Actor, q?: string): Promise { const { items } = await this.people.list(actor, q, 1, 5000); return writeXlsx( 'Contact', CONTACT_COLUMNS, items.map((p) => { const extra = (p.customFields ?? {}) as Record; return { _type: 'Contact', didarId: extra.didarId ?? p.id, customerCode: extra.customerCode ?? '', title: extra.title ?? '', firstName: p.firstName, lastName: p.lastName, email: p.email ?? '', fax: extra.fax ?? '', position: p.position ?? extra.position ?? '', postalCode: extra.postalCode ?? '', notes: p.notes ?? '', mobile: p.mobile ?? '', province: extra.province ?? '', city: extra.city ?? '', phone: p.phone ?? '', companyPhone: extra.companyPhone ?? '', companyName: p.company?.name ?? '', companyDidarId: extra.companyDidarId ?? '', ownerName: p.owner?.fullName ?? '', visibility: 'همه افراد شرکت', nationalId: extra.nationalId ?? '', registeredAt: formatJalaliDate(p.createdAt), birthDate: extra.birthDate ?? '', mood: extra.group || PERSON_MOOD_LABELS[p.mood || 'newcomer'], otherPhones: extra.otherPhones ?? '', address: p.addresses[0] ?? extra.address ?? '', website: p.website ?? extra.website ?? '', otherEmails: extra.otherEmails ?? '', position2: p.position ?? extra.position ?? '', city2: extra.city ?? '', code2: extra.customerCode ?? '', group2: extra.group ?? '', address2: p.addresses[1] ?? extra.address ?? '', phone2: extra.phone2 ?? p.phone ?? '', postalCode2: extra.postalCode ?? '', referrer: extra.referrer ?? p.referrer ?? '', homeAddress: p.addresses[2] ?? extra.homeAddress ?? '', birthDate2: extra.birthDate ?? '', instagram: p.instagram ?? extra.instagram ?? '', group: extra.group ?? '', colleagueContact: extra.colleagueContact ?? '', previousCompany: extra.previousCompany ?? '', customerType: extra.customerType ?? '', star: extra.star ?? '', source: extra.source ?? '', }; }), ); } async exportCompanies(actor: Actor, q?: string): Promise { const { items } = await this.companies.list(actor, q, 1, 5000); return writeXlsx( 'Company', COMPANY_COLUMNS, items.map((c) => { const extra = (c.customFields ?? {}) as Record; return { _type: 'Company', didarId: c.importSource === 'didar' && c.sourceRef ? c.sourceRef : '', code: c.code ?? '', name: c.name, province: c.province ?? '', city: c.city ?? '', phone: c.phone ?? '', email: c.email ?? '', economicCode: c.economicCode ?? '', registrationNumber: c.registrationNumber ?? '', nationalId: c.nationalId ?? '', fax: c.fax ?? '', postalCode: c.postalCode ?? '', notes: c.notes ?? '', ownerName: c.owner?.fullName ?? '', visibility: extra.visibility || 'همه افراد شرکت', registeredAt: formatJalaliDate( c.registeredAt ? new Date(c.registeredAt) : new Date(c.createdAt), ), groupName: c.groupName ?? '', otherPhones: c.otherPhones ?? '', address: c.address ?? '', website: c.website ?? '', otherEmails: c.otherEmails ?? '', }; }), ); } previewPeople(file: Express.Multer.File): Promise { return previewExcel(file, PERSON_ALIASES, PERSON_FIELDS); } previewCompanies(file: Express.Multer.File): Promise { return previewExcel(file, COMPANY_ALIASES, COMPANY_FIELDS); } async importPeople( actor: Actor, file: Express.Multer.File | undefined, mappingRaw?: string, chunk?: ImportChunkOpts, ): Promise { const ctx = await resolveExcelImportChunk({ organizationId: actor.organizationId, file, sessionId: chunk?.sessionId, offset: chunk?.offset, limit: chunk?.limit, loadRows: () => readMappedRows(file!, PERSON_ALIASES, parseExcelMapping(mappingRaw)), }); const result: ImportContactsResult = { created: 0, skipped: 0, errors: [] }; const ownerCache = new Map(); for (let i = 0; i < ctx.slice.length; i++) { const row = ctx.slice[i]!; const rowNum = ctx.offset + i + 2; if ((row._type || '').trim() && row._type.trim().toLowerCase() !== 'contact') { continue; } const names = splitDidarName(row.firstName ?? '', row.lastName ?? '', row.title) ?? { firstName: (row.firstName ?? '').trim().slice(0, 80), lastName: (row.lastName ?? '').trim().slice(0, 80), }; if ( !names.firstName && !names.lastName && !row.mobile?.trim() && !row.email?.trim() && !row.phone?.trim() ) { result.errors.push({ row: rowNum, message: 'حداقل نام، موبایل، تلفن یا ایمیل لازم است' }); continue; } let companyId: string | null = null; const companyName = row.companyName?.trim(); if (companyName) { companyId = await this.findOrCreateCompanyId( actor, companyName, row.companyDidarId, row.companyPhone, ); } const ownerName = row.ownerName?.trim() ?? ''; let ownerId: string | undefined; let ownerMissing = false; if (ownerName) { const key = ownerName.toLowerCase(); if (!ownerCache.has(key)) { ownerCache.set(key, await this.findUserId(actor.organizationId, ownerName)); } const matched = ownerCache.get(key); if (matched) ownerId = matched; else ownerMissing = true; } const customFields: Record = {}; for (const key of CUSTOM_KEYS) { if ( key === 'address' || key === 'address2' || key === 'homeAddress' || key === 'instagram' || key === 'website' ) { continue; } const value = row[key]?.trim(); if (value) customFields[key] = value; } const addresses = collectAddresses( row.address, row.address2, row.homeAddress, ); const registeredAt = parseJalaliDateTime(row.registeredAt) ?? (() => { const raw = row.registeredAt?.trim(); if (!raw) return null; const d = new Date(raw); return Number.isNaN(d.getTime()) ? null : d; })(); try { const didarId = customFields.didarId?.trim() || ''; const customerCode = customFields.customerCode?.trim() || ''; const sourceCol = customFields.source?.trim() || ''; const importSource = didarId ? 'didar' : sourceCol ? normalizePersonImportSource(sourceCol) : 'excel'; const sourceRef = didarId || customerCode || null; await this.people.create(actor, { firstName: names.firstName, lastName: names.lastName, email: row.email || undefined, phone: row.phone || row.phone2 || undefined, mobile: row.mobile || undefined, position: row.position || undefined, website: row.website || undefined, instagram: row.instagram || undefined, addresses, notes: row.notes || undefined, mood: parseMood(row.mood) ?? parseMood(row.group), companyId, ownerId: resolveOwnerId(actor, ownerId ?? null), customFields, importSource, sourceRef, registeredAt: registeredAt?.toISOString() ?? null, }); result.created += 1; if (ownerMissing) { result.errors.push({ row: rowNum, message: `کارشناس فروش «${ownerName}» در تیم پیدا نشد؛ به کاربر ایمپورتر اختصاص داده شد`, }); } } catch (e) { if (e instanceof ConflictException) { result.skipped += 1; } else { result.errors.push({ row: rowNum, message: e instanceof Error ? e.message : 'خطای ناشناخته', }); } } } return { ...result, ...chunkImportMeta(ctx) }; } async importCompanies( actor: Actor, file: Express.Multer.File | undefined, mappingRaw?: string, chunk?: ImportChunkOpts, ): Promise { const ctx = await resolveExcelImportChunk({ organizationId: actor.organizationId, file, sessionId: chunk?.sessionId, offset: chunk?.offset, limit: chunk?.limit, loadRows: () => readMappedRows(file!, COMPANY_ALIASES, parseExcelMapping(mappingRaw)), }); const result: ImportContactsResult = { created: 0, skipped: 0, errors: [] }; const ownerCache = new Map(); for (let i = 0; i < ctx.slice.length; i++) { const row = ctx.slice[i]!; const rowNum = ctx.offset + i + 2; if ((row._type || '').trim() && row._type.trim().toLowerCase() !== 'company') { continue; } const name = row.name?.trim() ?? ''; if (!name) { result.errors.push({ row: rowNum, message: 'نام شرکت الزامی است' }); continue; } const ownerName = row.ownerName?.trim() ?? ''; let ownerId: string | undefined; let ownerMissing = false; if (ownerName) { const key = ownerName.toLowerCase(); if (!ownerCache.has(key)) { ownerCache.set(key, await this.findUserId(actor.organizationId, ownerName)); } const matched = ownerCache.get(key); if (matched) ownerId = matched; else ownerMissing = true; } const didarId = row.didarId?.trim() || ''; const customFields: Record = {}; const visibility = row.visibility?.trim(); if (visibility) customFields.visibility = visibility; const registeredAt = parseLooseDate(row.registeredAt); const payload = { name, code: row.code?.trim() || null, importSource: didarId ? 'didar' : 'excel', sourceRef: didarId || null, nationalId: row.nationalId || undefined, economicCode: row.economicCode?.trim() || null, registrationNumber: row.registrationNumber?.trim() || null, phone: row.phone || undefined, otherPhones: row.otherPhones?.trim() || null, fax: row.fax?.trim() || null, email: row.email || undefined, otherEmails: row.otherEmails?.trim() || null, website: row.website || undefined, province: row.province?.trim() || null, city: row.city?.trim() || null, postalCode: row.postalCode?.trim() || null, address: row.address || undefined, groupName: row.groupName?.trim() || null, notes: row.notes || undefined, registeredAt: registeredAt?.toISOString() ?? null, customFields, }; try { const existing = await this.findImportedCompany(actor, didarId, name); if (existing) { const merged: Record = {}; for (const [k, v] of Object.entries(payload)) { if (k === 'customFields') continue; if (v != null && v !== '') merged[k] = v; } merged.customFields = { ...((existing.customFields ?? {}) as Record), ...customFields, }; if (ownerId) merged.ownerId = ownerId; await this.companies.update(actor, existing.id, merged); result.skipped += 1; } else { await this.companies.create(actor, { ...payload, ownerId: resolveOwnerId(actor, ownerId ?? null), }); result.created += 1; } if (ownerMissing) { result.errors.push({ row: rowNum, message: `مسئول شرکت «${ownerName}» در تیم پیدا نشد؛ به کاربر ایمپورتر اختصاص داده شد`, }); } } catch (e) { if (e instanceof ConflictException) { result.skipped += 1; } else { result.errors.push({ row: rowNum, message: e instanceof Error ? e.message : 'خطای ناشناخته', }); } } } return { ...result, ...chunkImportMeta(ctx) }; } /** شرکت موجود برای ورود مجدد: اول با کد دیدار، بعد با نام (اگر کد مبدأ نداشته باشد). */ private async findImportedCompany(actor: Actor, didarId: string, name: string) { if (didarId) { const byRef = await this.prisma.company.findFirst({ where: { organizationId: actor.organizationId, sourceRef: didarId, ...ownerWhere(actor), }, select: { id: true, customFields: true }, }); if (byRef) return byRef; } return this.prisma.company.findFirst({ where: { organizationId: actor.organizationId, name: { equals: name, mode: 'insensitive' }, ...(didarId ? { sourceRef: null } : {}), ...ownerWhere(actor), }, select: { id: true, customFields: true }, }); } private async findOrCreateCompanyId( actor: Actor, name: string, didarId?: string, phone?: string, ): Promise { const ref = didarId?.trim() || ''; if (ref) { const byRef = await this.prisma.company.findFirst({ where: { organizationId: actor.organizationId, sourceRef: ref, ...ownerWhere(actor), }, select: { id: true }, }); if (byRef) return byRef.id; } const existing = await this.prisma.company.findFirst({ where: { organizationId: actor.organizationId, name: { equals: name, mode: 'insensitive' }, ...ownerWhere(actor), }, select: { id: true, sourceRef: true }, }); if (existing) { if (ref && !existing.sourceRef) { await this.prisma.company.update({ where: { id: existing.id }, data: { sourceRef: ref, importSource: 'didar' }, }); } return existing.id; } try { const created = await this.companies.create(actor, { name, ...(ref ? { sourceRef: ref, importSource: 'didar' } : {}), ...(phone?.trim() ? { phone: phone.trim() } : {}), }); return created.id; } catch (e) { if (!(e instanceof ConflictException) || !phone) throw e; const created = await this.companies.create(actor, { name, ...(ref ? { sourceRef: ref, importSource: 'didar' } : {}), }); return created.id; } } private async findUserId(organizationId: string, name?: string) { const n = name?.trim(); if (!n) return null; const exact = await this.prisma.user.findFirst({ where: { fullName: { equals: n, mode: 'insensitive' }, memberships: { some: { organizationId } }, }, select: { id: true }, }); if (exact) return exact.id; const partial = await this.prisma.user.findFirst({ where: { fullName: { contains: n, mode: 'insensitive' }, memberships: { some: { organizationId } }, }, select: { id: true }, }); return partial?.id ?? null; } }