fix: keep birthdays and show Shamsi dates

Imported customers keep their birthday, and assistant text plus remaining calendars use the Jalali date.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-13 18:04:53 +03:30
co-authored by Cursor
parent 5f4cc47c41
commit cf326d1ed9
34 changed files with 637 additions and 105 deletions
+74 -1
View File
@@ -1,7 +1,9 @@
import {
ConflictException,
Injectable,
Logger,
NotFoundException,
type OnModuleInit,
} from '@nestjs/common';
import type {
CreatePersonRequest,
@@ -32,6 +34,7 @@ import { AutomationsService } from '../automations/automations.service';
import { MessageAutomationsService } from '../message-automations/message-automations.service';
import { WebhooksService } from '../public-api/webhooks.service';
import { GamificationService } from '../gamification/gamification.service';
import { parseBirthDate } from '../common/excel';
import { normalizeTags } from '../common/tags';
import { buildSearchWhere, ci } from '../common/search';
import {
@@ -63,6 +66,7 @@ type PersonRow = {
position: string | null;
website: string | null;
instagram: string | null;
birthDate: Date | null;
referrer: string | null;
addresses: unknown;
notes: string | null;
@@ -83,7 +87,8 @@ type PersonRow = {
};
@Injectable()
export class PeopleService {
export class PeopleService implements OnModuleInit {
private readonly logger = new Logger(PeopleService.name);
constructor(
private readonly prisma: PrismaService,
private readonly companies: CompaniesService,
@@ -168,6 +173,71 @@ export class PeopleService {
.sort((a, b) => b.count - a.count);
}
onModuleInit() {
void this.backfillStoredBirthDates();
}
/** تاریخ تولدی که قبلاً در فیلد اضافی دیدار/اکسل یا پروفایل مونتیفای مانده، روی شخص می‌نشیند. */
async backfillStoredBirthDates() {
try {
const fromFields = await this.prisma.$queryRaw<Array<{ id: string; birth: string }>>`
SELECT id,
COALESCE(
NULLIF("customFields"->>'birthDate', ''),
NULLIF("customFields"->>'birthDate2', '')
) AS birth
FROM "Person"
WHERE "birthDate" IS NULL
AND (
COALESCE("customFields"->>'birthDate', '') <> ''
OR COALESCE("customFields"->>'birthDate2', '') <> ''
)
LIMIT 20000
`;
const fromMontify = await this.prisma.$queryRaw<Array<{ id: string; birth: string }>>`
SELECT p.id, m.data->>'birthday' AS birth
FROM "MontifyLink" m
JOIN "Person" p ON p.id = m."entityId"
WHERE m."entityType" = 'customer'
AND p."birthDate" IS NULL
AND COALESCE(m.data->>'birthday', '') <> ''
LIMIT 20000
`;
const seen = new Set<string>();
let filled = 0;
for (const row of [...fromFields, ...fromMontify]) {
if (!row.birth || seen.has(row.id)) continue;
seen.add(row.id);
const birthDate = parseBirthDate(row.birth);
if (!birthDate) continue;
const updated = await this.prisma.person.updateMany({
where: { id: row.id, birthDate: null },
data: { birthDate },
});
filled += updated.count;
}
if (filled) this.logger.log(`تاریخ تولد ${filled} مشتری از داده‌های ذخیره‌شده پر شد`);
} catch (err) {
this.logger.warn(
`پر کردن تاریخ تولد ذخیره‌شده ناموفق بود: ${err instanceof Error ? err.message : err}`,
);
}
}
async fillBirthDateIfEmpty(
organizationId: string,
input: { mobile?: string | null; email?: string | null; birthDate: Date },
) {
const or: Prisma.PersonWhereInput[] = [];
if (input.mobile) or.push({ mobile: input.mobile });
if (input.email) or.push({ email: input.email });
if (!or.length) return;
await this.prisma.person.updateMany({
where: { organizationId, birthDate: null, OR: or },
data: { birthDate: input.birthDate },
});
}
async get(actor: Actor, id: string): Promise<PersonDto> {
const row = await this.prisma.person.findFirst({
where: { id, organizationId: actor.organizationId, ...ownerWhere(actor) },
@@ -198,6 +268,7 @@ export class PeopleService {
position: emptyToNull(dto.position),
website: emptyToNull(dto.website),
instagram: emptyToNull(dto.instagram),
birthDate: parseBirthDate(dto.birthDate),
referrer: emptyToNull(dto.referrer),
addresses: toJsonAddresses(dto.addresses),
notes: emptyToNull(dto.notes),
@@ -279,6 +350,7 @@ export class PeopleService {
if ('position' in dto) data.position = emptyToNull(dto.position);
if ('website' in dto) data.website = emptyToNull(dto.website);
if ('instagram' in dto) data.instagram = emptyToNull(dto.instagram);
if ('birthDate' in dto) data.birthDate = parseBirthDate(dto.birthDate);
if ('referrer' in dto) data.referrer = emptyToNull(dto.referrer);
if ('addresses' in dto) data.addresses = toJsonAddresses(dto.addresses);
if ('notes' in dto) data.notes = emptyToNull(dto.notes);
@@ -396,6 +468,7 @@ export class PeopleService {
position: row.position,
website: row.website,
instagram: row.instagram,
birthDate: row.birthDate?.toISOString() ?? null,
referrer: row.referrer,
addresses: (() => {
const fromCol = asAddresses(row.addresses);