feat(contacts): keep Tahesab honorifics with person gender

Male and female names can store the title when requested, and Tahesab import and export use the same gender.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-29 18:46:10 +03:30
co-authored by Cursor
parent 0d5e1034e5
commit b9bb2ff9fa
11 changed files with 350 additions and 14 deletions
+18 -1
View File
@@ -3,6 +3,7 @@ import {
ArrayMaxSize,
ArrayMinSize,
IsArray,
IsBoolean,
IsEmail,
IsIn,
IsInt,
@@ -15,7 +16,7 @@ import {
MinLength,
ValidateIf,
} from 'class-validator';
import { PERSON_IMPORT_SOURCES, PERSON_MOODS } from '@mento/shared';
import { PERSON_GENDERS, PERSON_IMPORT_SOURCES, PERSON_MOODS } from '@mento/shared';
export class BirthdaySmsDto {
@IsString()
@@ -282,6 +283,14 @@ export class CreatePersonDto {
@MaxLength(80)
lastName?: string;
@IsOptional()
@IsIn(PERSON_GENDERS)
gender?: (typeof PERSON_GENDERS)[number];
@IsOptional()
@IsBoolean()
showGenderInName?: boolean;
@IsOptional()
@ValidateIf((_, v) => v !== '' && v != null)
@IsString()
@@ -401,6 +410,14 @@ export class UpdatePersonDto {
@MaxLength(80)
lastName?: string;
@IsOptional()
@IsIn(PERSON_GENDERS)
gender?: (typeof PERSON_GENDERS)[number];
@IsOptional()
@IsBoolean()
showGenderInName?: boolean;
@IsOptional()
@ValidateIf((_, v) => v !== '' && v != null)
@IsString()
+46 -5
View File
@@ -18,7 +18,12 @@ import type {
PersonMood,
UpdatePersonRequest,
} from '@mento/shared';
import { FILTERED_IDS_MAX, PERSON_MOODS } from '@mento/shared';
import {
FILTERED_IDS_MAX,
PERSON_MOODS,
applyPersonGenderTitle,
splitPersonHonorific,
} from '@mento/shared';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { OutboundSmsLogService } from '../outbound-sms/outbound-sms-log.service';
@@ -73,6 +78,8 @@ type PersonRow = {
id: string;
firstName: string;
lastName: string;
gender: string;
showGenderInName: boolean;
previousFirstName: string | null;
previousLastName: string | null;
importSource: string;
@@ -417,10 +424,18 @@ export class PeopleService implements OnModuleInit {
}
const registeredAt = dto.registeredAt ? new Date(dto.registeredAt) : null;
const named = applyPersonGenderTitle({
firstName: dto.firstName ?? '',
lastName: dto.lastName ?? '',
gender: dto.gender,
showGenderInName: dto.showGenderInName,
});
const data: Prisma.PersonUncheckedCreateInput = {
organizationId: actor.organizationId,
firstName: (dto.firstName ?? '').trim(),
lastName: (dto.lastName ?? '').trim(),
firstName: named.firstName,
lastName: named.lastName,
gender: named.gender,
showGenderInName: named.showGenderInName,
previousFirstName: emptyToNull(dto.previousFirstName),
previousLastName: emptyToNull(dto.previousLastName),
importSource: normalizePersonImportSource(dto.importSource),
@@ -497,8 +512,32 @@ export class PeopleService implements OnModuleInit {
await this.ensureExists(actor, id);
const data: Prisma.PersonUncheckedUpdateInput = {};
if (dto.firstName !== undefined) data.firstName = dto.firstName.trim();
if (dto.lastName !== undefined) data.lastName = dto.lastName.trim();
const touchesName =
dto.firstName !== undefined ||
dto.lastName !== undefined ||
dto.gender !== undefined ||
dto.showGenderInName !== undefined;
if (touchesName) {
const existing = await this.prisma.person.findFirst({
where: { id, organizationId: actor.organizationId },
select: { firstName: true, lastName: true, gender: true, showGenderInName: true },
});
if (!existing) throw new NotFoundException('شخص پیدا نشد');
const bare = splitPersonHonorific(existing.firstName, existing.lastName);
const storedGender =
existing.gender === 'male' || existing.gender === 'female' ? existing.gender : 'none';
const named = applyPersonGenderTitle({
firstName: dto.firstName !== undefined ? dto.firstName : bare.firstName,
lastName: dto.lastName !== undefined ? dto.lastName : bare.lastName,
gender: dto.gender !== undefined ? dto.gender : storedGender,
showGenderInName:
dto.showGenderInName !== undefined ? dto.showGenderInName : existing.showGenderInName,
});
data.firstName = named.firstName;
data.lastName = named.lastName;
data.gender = named.gender;
data.showGenderInName = named.showGenderInName;
}
if ('previousFirstName' in dto) {
data.previousFirstName = emptyToNull(dto.previousFirstName);
}
@@ -809,6 +848,8 @@ export class PeopleService implements OnModuleInit {
previousImportSource: row.previousImportSource,
previousSourceRef: row.previousSourceRef,
fullName: `${row.firstName} ${row.lastName}`.trim(),
gender: row.gender === 'male' || row.gender === 'female' ? row.gender : 'none',
showGenderInName: row.showGenderInName,
email: row.email,
phone: row.phone,
mobile: row.mobile,
@@ -9,6 +9,7 @@ import {
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../prisma/prisma.service';
import { carryPreviousSource } from '../../contacts/person-source';
import { applyPersonGenderTitle } from '@mento/shared';
import { TahesabConnectionsService } from './connections.service';
import {
CreateInTahesabDto,
@@ -573,9 +574,9 @@ export class TahesabPersonLinkService {
}
const name = pickName(row) || `مشتری ته‌حساب ${dto.moshtariCode}`;
const tel = pickTel(row);
const parts = name.trim().split(/\s+/);
const firstName = parts[0] || name;
const lastName = parts.slice(1).join(' ') || '—';
const named = applyPersonGenderTitle({ firstName: name, lastName: '' });
const firstName = named.firstName;
const lastName = named.lastName;
const mobile = tel || null;
const sourceRef = String(dto.moshtariCode);
@@ -591,6 +592,8 @@ export class TahesabPersonLinkService {
data: {
firstName,
lastName,
gender: named.gender,
showGenderInName: named.showGenderInName,
...(nameChanged
? {
previousFirstName: prevFirst || null,
@@ -631,6 +634,8 @@ export class TahesabPersonLinkService {
data: {
firstName,
lastName,
gender: named.gender,
showGenderInName: named.showGenderInName,
...(nameChanged
? {
previousFirstName: byMobile.firstName || null,
@@ -650,6 +655,8 @@ export class TahesabPersonLinkService {
organizationId,
firstName,
lastName,
gender: named.gender,
showGenderInName: named.showGenderInName,
mobile,
phone: mobile,
notes: `واردشده از ته‌حساب (کد ${dto.moshtariCode})`,