fix(contacts): treat Iranian mobile formats as one person

Tahesab import still replaces the name and keeps the previous name.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-05 10:51:36 +03:30
co-authored by Cursor
parent 6abbf8e8fd
commit 7f6d9a84d6
17 changed files with 586 additions and 79 deletions
+22 -22
View File
@@ -38,7 +38,12 @@ import {
collectAddresses,
toJsonAddresses,
} from '../common/custom-fields';
import { emptyToNull, normalizePhone } from '../contacts/normalize';
import {
canonicalIranMobile,
emptyToNull,
normalizePhone,
phoneIdentityKeys,
} from '../contacts/normalize';
import { LocalFileStorage } from '../storage/local-file.storage';
import { StorageQuotaService } from '../billing/storage-quota.service';
import { LeadStagesService } from './lead-stages.service';
@@ -55,13 +60,20 @@ const LOGO_EXT: Record<string, string> = {
};
function mobileKey(raw: string | null | undefined): string | null {
const canon = canonicalIranMobile(raw);
if (canon) return canon;
const n = normalizePhone(raw);
if (!n) return null;
const d = n.replace(/\D/g, '');
if (!d) return null;
if (d.startsWith('98') && d.length >= 12) return `0${d.slice(2)}`;
if (d.length === 10 && d.startsWith('9')) return `0${d}`;
return d.startsWith('0') ? d : d;
return d || null;
}
function personWithMobile(organizationId: string, mobile: string): Prisma.PersonWhereInput {
const keys = phoneIdentityKeys(mobile);
return {
organizationId,
OR: [{ mobile: { in: keys } }, { phone: { in: keys } }],
};
}
function leadAccessWhere(actor: Actor): Prisma.LeadWhereInput {
@@ -323,10 +335,7 @@ export class LeadsService {
};
}
const person = await this.prisma.person.findFirst({
where: {
organizationId: actor.organizationId,
OR: [{ mobile }, { phone: mobile }],
},
where: personWithMobile(actor.organizationId, mobile),
select: { id: true, firstName: true, lastName: true },
});
if (person) {
@@ -966,10 +975,7 @@ export class LeadsService {
}
let person = await this.prisma.person.findFirst({
where: {
organizationId: actor.organizationId,
OR: [{ mobile: lead.mobile }, { phone: lead.mobile }],
},
where: personWithMobile(actor.organizationId, lead.mobile),
});
let createdPerson = false;
@@ -996,7 +1002,7 @@ export class LeadsService {
lastName: named.lastName,
gender: named.gender,
showGenderInName: named.showGenderInName,
mobile: lead.mobile,
mobile: canonicalIranMobile(lead.mobile) ?? lead.mobile,
phone: lead.phone,
instagram: lead.instagram,
birthDate: lead.birthDate,
@@ -1222,10 +1228,7 @@ export class LeadsService {
}
const person = await this.prisma.person.findFirst({
where: {
organizationId: input.organizationId,
OR: [{ mobile }, { phone: mobile }],
},
where: personWithMobile(input.organizationId, mobile),
select: { id: true },
});
if (person) {
@@ -1304,10 +1307,7 @@ export class LeadsService {
}
const existingPerson = await this.prisma.person.findFirst({
where: {
organizationId,
OR: [{ mobile }, { phone: mobile }],
},
where: personWithMobile(organizationId, mobile),
select: { id: true },
});
if (existingPerson) {