diff --git a/apps/api/src/contacts/merge-same-mobile.ts b/apps/api/src/contacts/merge-same-mobile.ts index bc82304..7c9c041 100644 --- a/apps/api/src/contacts/merge-same-mobile.ts +++ b/apps/api/src/contacts/merge-same-mobile.ts @@ -26,6 +26,9 @@ const personSelect = { companyId: true, customFields: true, importSource: true, + sourceRef: true, + previousImportSource: true, + previousSourceRef: true, loyaltyPoints: true, discLabel: true, discReliable: true, @@ -62,6 +65,8 @@ function mergedFields(keeper: PersonRow, extras: PersonRow[], canon: string): Pr let showGenderInName = keeper.showGenderInName; let previousFirstName = keeper.previousFirstName; let previousLastName = keeper.previousLastName; + let previousImportSource = keeper.previousImportSource; + let previousSourceRef = keeper.previousSourceRef; let discLabel = keeper.discLabel; let discReliable = keeper.discReliable; let discAssessedAt = keeper.discAssessedAt; @@ -93,6 +98,14 @@ function mergedFields(keeper: PersonRow, extras: PersonRow[], canon: string): Pr if (blank(previousLastName) && extra.previousLastName) { previousLastName = extra.previousLastName; } + if ( + blank(previousImportSource) && + extra.importSource && + extra.importSource !== keeper.importSource + ) { + previousImportSource = extra.importSource; + previousSourceRef = extra.sourceRef; + } if ( keeper.importSource === 'tahesab' && blank(previousFirstName) && @@ -134,6 +147,8 @@ function mergedFields(keeper: PersonRow, extras: PersonRow[], canon: string): Pr showGenderInName, previousFirstName, previousLastName, + previousImportSource, + previousSourceRef, discLabel, discReliable, discAssessedAt, diff --git a/apps/api/src/contacts/same-mobile-merge.service.ts b/apps/api/src/contacts/same-mobile-merge.service.ts index 907bfeb..bed1c1e 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-v2'; +const FIX_KEY = 'person-mobile-canonical-v3'; @Injectable() export class SameMobileMergeService implements OnApplicationBootstrap { diff --git a/apps/api/src/integrations/tahesab/person-link.service.ts b/apps/api/src/integrations/tahesab/person-link.service.ts index 7cdda80..21c74df 100644 --- a/apps/api/src/integrations/tahesab/person-link.service.ts +++ b/apps/api/src/integrations/tahesab/person-link.service.ts @@ -601,6 +601,7 @@ export class TahesabPersonLinkService { const sourceRef = String(dto.moshtariCode); let personId = dto.personId; + let preserveExistingLink = false; if (personId) { const existing = await this.getPerson(organizationId, personId); const prevFirst = existing.firstName; @@ -629,21 +630,28 @@ export class TahesabPersonLinkService { }); } else { // اگر موبایل تکراری است → همان شخص؛ نام قبلی حفظ و نام ته‌حساب جایگزین. - // شخصی که روی همین اتصال به کد دیگری لینک است کنار گذاشته می‌شود؛ - // وگرنه upsert لینک، کد قبلی را جابه‌جا و آن مشتری را دوباره «واردنشده» می‌کند. + // اگر همان شخص قبلاً به کد دیگری از این اتصال لینک شده، لینک جابه‌جا نمی‌شود + // تا کد قبلی دوباره «واردنشده» نشود و مشتری تکراری ساخته نشود. const mobileKeys = phoneIdentityKeys(mobile); const matches = mobileKeys.length ? await this.prisma.person.findMany({ where: { organizationId, OR: [{ mobile: { in: mobileKeys } }, { phone: { in: mobileKeys } }], - accountingLinks: { none: { connectionId: dto.connectionId } }, + }, + include: { + accountingLinks: { + where: { connectionId: dto.connectionId }, + select: { moshtariCode: true }, + }, }, orderBy: { createdAt: 'asc' }, }) : []; const byMobile = matches.find((person) => person.importSource === 'tahesab') ?? matches[0] ?? null; if (byMobile) { + const linkedCode = byMobile.accountingLinks[0]?.moshtariCode; + preserveExistingLink = linkedCode != null && linkedCode !== dto.moshtariCode; const nameChanged = `${byMobile.firstName} ${byMobile.lastName}`.trim() !== `${firstName} ${lastName}`.trim(); @@ -662,7 +670,7 @@ export class TahesabPersonLinkService { : {}), ...carryPreviousSource(byMobile, 'tahesab'), importSource: 'tahesab', - sourceRef, + sourceRef: preserveExistingLink ? String(linkedCode) : sourceRef, ...(!byMobile.birthDate && pickBirthDate(row) ? { birthDate: pickBirthDate(row) } : {}), ...canonicalMobileUpdate(mobile, byMobile), }, @@ -688,6 +696,32 @@ export class TahesabPersonLinkService { } } + if (preserveExistingLink) { + await this.applyTahesabGroup(organizationId, dto.connectionId, personId, row); + const existing = await this.prisma.personAccountingLink.findFirst({ + where: { personId, connectionId: dto.connectionId }, + include: { + connection: { select: { label: true, dbName: true, isDefault: true } }, + }, + }); + if (!existing) { + throw new NotFoundException('لینک ته‌حساب این مشتری یافت نشد.'); + } + if (!options?.skipJob) { + await this.prisma.accountingSyncJob.create({ + data: { + organizationId, + connectionId: dto.connectionId, + kind: 'import', + status: 'success', + request: dto as unknown as Prisma.InputJsonValue, + result: { personId, linkId: existing.id, keptLink: true } as Prisma.InputJsonValue, + }, + }); + } + return { personId, link: this.linkDto(existing) }; + } + const link = await this.prisma.personAccountingLink.upsert({ where: { personId_connectionId: {