diff --git a/apps/api/src/contacts/merge-same-mobile.ts b/apps/api/src/contacts/merge-same-mobile.ts index 5de6701..bc82304 100644 --- a/apps/api/src/contacts/merge-same-mobile.ts +++ b/apps/api/src/contacts/merge-same-mobile.ts @@ -229,7 +229,7 @@ export async function mergeSameMobilePeople( }); const groups = new Map(); 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,37 +269,42 @@ 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; } - await prisma.$transaction( - async (tx) => { - await tx.person.update({ - where: { id: keeper.id }, - data: mergedFields(keeper, extras, canon), - }); - for (const extra of extras) { - await moveLinks(tx, tables, keeper.id, extra.id); - await tx.person.delete({ where: { id: extra.id } }); - merged += 1; - } - }, - { timeout: 60_000 }, - ); + try { + await prisma.$transaction( + async (tx) => { + await tx.person.update({ + where: { id: keeper.id }, + data: mergedFields(keeper, extras, canon), + }); + for (const extra of extras) { + await moveLinks(tx, tables, keeper.id, extra.id); + await tx.person.delete({ where: { id: extra.id } }); + merged += 1; + } + }, + { 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) { diff --git a/apps/api/src/contacts/same-mobile-merge.service.ts b/apps/api/src/contacts/same-mobile-merge.service.ts index f0bbc96..907bfeb 100644 --- a/apps/api/src/contacts/same-mobile-merge.service.ts +++ b/apps/api/src/contacts/same-mobile-merge.service.ts @@ -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) { - const result = await mergeSameMobilePeople(this.prisma, org.id, true); - merged += result.merged; - rewritten += result.rewritten; + 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