fix(contacts): merge the same mobile stored on phone or mobile

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-05 11:27:05 +03:30
co-authored by Cursor
parent 57a0b379f6
commit c976c0bcd6
2 changed files with 60 additions and 40 deletions
+20 -14
View File
@@ -229,7 +229,7 @@ export async function mergeSameMobilePeople(
});
const groups = new Map<string, PersonRow[]>();
for (const person of people) {
const key = canonicalIranMobile(person.mobile);
const key = canonicalIranMobile(person.mobile) ?? canonicalIranMobile(person.phone);
if (!key) continue;
const list = groups.get(key) ?? [];
list.push(person);
@@ -248,15 +248,16 @@ export async function mergeSameMobilePeople(
if (!apply) {
const needsRewrite = people.filter((person) => {
const canon = canonicalIranMobile(person.mobile);
return Boolean(canon && person.mobile !== canon);
const canon = canonicalIranMobile(person.mobile) ?? canonicalIranMobile(person.phone);
return Boolean(canon && (person.mobile !== canon || canonicalIranMobile(person.phone)));
}).length;
return { merged: 0, rewritten: 0, needsRewrite, groups: preview };
return { merged: 0, rewritten: 0, needsRewrite, failed: 0, groups: preview };
}
const tables = await personIdTables(prisma);
let merged = 0;
let rewritten = 0;
let failed = 0;
for (const [canon, list] of groups) {
const ordered = [...list].sort((a, b) => {
@@ -268,20 +269,18 @@ export async function mergeSameMobilePeople(
const [keeper, ...extras] = ordered;
if (!keeper) continue;
if (!extras.length) {
const phone = storedPhoneNumber(keeper.phone);
if (keeper.mobile !== canon || (phone && keeper.phone !== phone)) {
await prisma.person.update({
where: { id: keeper.id },
data: {
mobile: canon,
...(phone && keeper.phone !== phone ? { phone } : {}),
},
});
const phoneCanon = canonicalIranMobile(keeper.phone);
const data: Prisma.PersonUpdateInput = {};
if (keeper.mobile !== canon) data.mobile = canon;
if (phoneCanon && keeper.phone !== phoneCanon) data.phone = phoneCanon;
if (Object.keys(data).length) {
await prisma.person.update({ where: { id: keeper.id }, data });
rewritten += 1;
}
continue;
}
try {
await prisma.$transaction(
async (tx) => {
await tx.person.update({
@@ -296,9 +295,16 @@ export async function mergeSameMobilePeople(
},
{ timeout: 60_000 },
);
} catch (error) {
failed += 1;
console.error(
`merge mobile ${canon} failed`,
error instanceof Error ? error.message : error,
);
}
}
return { merged, rewritten, needsRewrite: 0, groups: preview };
return { merged, rewritten, needsRewrite: 0, failed, groups: preview };
}
function loadEnv(file: string) {
@@ -2,7 +2,7 @@ import { Injectable, Logger, type OnApplicationBootstrap } from '@nestjs/common'
import { PrismaService } from '../prisma/prisma.service';
import { mergeSameMobilePeople } from './merge-same-mobile';
const FIX_KEY = 'person-mobile-canonical-v1';
const FIX_KEY = 'person-mobile-canonical-v2';
@Injectable()
export class SameMobileMergeService implements OnApplicationBootstrap {
@@ -21,17 +21,20 @@ export class SameMobileMergeService implements OnApplicationBootstrap {
SELECT 1 AS ok
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'Person'
AND column_name = 'gender'
AND lower(table_name) = 'person'
AND lower(column_name) = 'gender'
`;
if (!ready.length) return;
if (!ready.length) {
this.logger.warn('یکسان‌سازی موبایل رد شد: ستون gender هنوز نیست');
return;
}
const table = await this.prisma.$queryRaw<{ ok: number }[]>`
SELECT 1 AS ok
FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = 'DataFix'
await this.prisma.$executeRaw`
CREATE TABLE IF NOT EXISTS "DataFix" (
"key" TEXT PRIMARY KEY,
"doneAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
)
`;
if (!table.length) return;
const done = await this.prisma.$queryRaw<{ key: string }[]>`
SELECT "key" FROM "DataFix" WHERE "key" = ${FIX_KEY}
@@ -48,11 +51,22 @@ export class SameMobileMergeService implements OnApplicationBootstrap {
const orgs = await this.prisma.organization.findMany({ select: { id: true } });
let merged = 0;
let rewritten = 0;
let failed = 0;
for (const org of orgs) {
try {
const result = await mergeSameMobilePeople(this.prisma, org.id, true);
merged += result.merged;
rewritten += result.rewritten;
failed += result.failed;
} catch (error) {
failed += 1;
this.logger.error(
`یکسان‌سازی موبایل برای محیط ${org.id} انجام نشد`,
error instanceof Error ? error.message : String(error),
);
}
}
if (failed) return;
await this.prisma.$executeRaw`
INSERT INTO "DataFix" ("key") VALUES (${FIX_KEY})
ON CONFLICT ("key") DO NOTHING