Add Tahesab bulk customer import by code range.

Connecting alone does not copy customers; settings now list a code range and import them into CRM persons with link status.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-05 09:33:12 +03:30
co-authored by Cursor
parent a88bd83c9b
commit 15a0afae5c
5 changed files with 390 additions and 22 deletions
+39
View File
@@ -1,4 +1,5 @@
import {
IsArray,
IsBoolean,
IsIn,
IsInt,
@@ -121,6 +122,7 @@ export class ImportFromTahesabDto {
@IsString()
connectionId!: string;
@Type(() => Number)
@IsInt()
@Min(1)
moshtariCode!: number;
@@ -131,6 +133,29 @@ export class ImportFromTahesabDto {
personId?: string;
}
export class ImportBulkFromTahesabDto {
@IsString()
connectionId!: string;
@IsOptional()
@IsArray()
@Type(() => Number)
@IsInt({ each: true })
moshtariCodes?: number[];
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
fromCode?: number;
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
toCode?: number;
}
export class SearchTahesabDto {
@IsString()
connectionId!: string;
@@ -144,8 +169,22 @@ export class SearchTahesabDto {
name?: string;
@IsOptional()
@Type(() => Number)
@IsInt()
moshtariCode?: number;
/** لیست بازه‌ای با DoListMoshtari-ByCode */
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
fromCode?: number;
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
toCode?: number;
}
export class ListAsnadQueryDto {
@@ -9,6 +9,7 @@ import { PrismaService } from '../../prisma/prisma.service';
import { TahesabConnectionsService } from './connections.service';
import {
CreateInTahesabDto,
ImportBulkFromTahesabDto,
ImportFromTahesabDto,
ManualLinkDto,
MatchPersonDto,
@@ -317,7 +318,15 @@ export class TahesabPersonLinkService {
dto.connectionId,
);
let raw: unknown = [];
if (dto.moshtariCode) {
if (dto.fromCode != null && dto.toCode != null) {
if (dto.toCode < dto.fromCode) {
throw new BadRequestException('کد پایان باید بزرگ‌تر یا مساوی کد شروع باشد.');
}
if (dto.toCode - dto.fromCode > 2000) {
throw new BadRequestException('بازهٔ کد حداکثر ۲۰۰۰ عدد است (مثلاً ۱ تا ۲۰۰۰).');
}
raw = await api.doListMoshtariByCode(dto.fromCode, dto.toCode);
} else if (dto.moshtariCode) {
raw = await api.doListMoshtariByCode(dto.moshtariCode, dto.moshtariCode);
} else if (dto.phone) {
const m = normalizeIranMobile(dto.phone) ?? dto.phone;
@@ -325,16 +334,49 @@ export class TahesabPersonLinkService {
} else if (dto.name?.trim()) {
raw = await api.doListMoshtariByName(dto.name.trim());
} else {
throw new BadRequestException('یکی از phone، name یا moshtariCode لازم است.');
throw new BadRequestException(
'یکی از موارد لازم است: بازه کد (از–تا)، کد مشتری، موبایل یا نام.',
);
}
const results = asMoshtariList(raw).map((row) => ({
const mapped = asMoshtariList(raw).map((row) => ({
moshtariCode: pickCode(row),
name: pickName(row),
tel: pickTel(row),
raw: row,
}));
const codes = mapped
.map((r) => r.moshtariCode)
.filter((c): c is number => c != null);
const existing =
codes.length === 0
? []
: await this.prisma.personAccountingLink.findMany({
where: {
connectionId: dto.connectionId,
moshtariCode: { in: codes },
},
select: {
moshtariCode: true,
personId: true,
person: { select: { firstName: true, lastName: true } },
},
});
const byCode = new Map(existing.map((e) => [e.moshtariCode, e]));
const results = mapped.map((row) => {
const link = row.moshtariCode != null ? byCode.get(row.moshtariCode) : undefined;
return {
...row,
alreadyLinked: Boolean(link),
personId: link?.personId ?? null,
personName: link
? `${link.person.firstName} ${link.person.lastName}`.trim()
: null,
};
});
await this.prisma.accountingSyncJob.create({
data: {
organizationId,
@@ -435,4 +477,86 @@ export class TahesabPersonLinkService {
return { personId, link: this.linkDto(link) };
}
async importBulk(organizationId: string, dto: ImportBulkFromTahesabDto) {
let codes = (dto.moshtariCodes ?? []).filter((n) => Number.isFinite(n) && n >= 1);
if (dto.fromCode != null && dto.toCode != null) {
if (dto.toCode < dto.fromCode) {
throw new BadRequestException('کد پایان باید بزرگ‌تر یا مساوی کد شروع باشد.');
}
if (dto.toCode - dto.fromCode > 2000) {
throw new BadRequestException('بازهٔ کد حداکثر ۲۰۰۰ عدد است.');
}
const { api } = await this.connections.getApi(
organizationId,
dto.connectionId,
);
const raw = await api.doListMoshtariByCode(dto.fromCode, dto.toCode);
const listed = asMoshtariList(raw)
.map((row) => pickCode(row))
.filter((c): c is number => c != null);
codes = [...new Set([...codes, ...listed])];
}
if (!codes.length) {
throw new BadRequestException(
'کد مشتری یا بازهٔ کد برای وارد کردن مشخص نشده است.',
);
}
if (codes.length > 500) {
throw new BadRequestException('حداکثر ۵۰۰ مشتری در هر بار وارد کنید.');
}
const items: Array<{
moshtariCode: number;
ok: boolean;
personId?: string;
skipped?: boolean;
error?: string;
}> = [];
for (const moshtariCode of codes) {
try {
const existing = await this.prisma.personAccountingLink.findFirst({
where: { connectionId: dto.connectionId, moshtariCode },
});
if (existing) {
items.push({
moshtariCode,
ok: true,
skipped: true,
personId: existing.personId,
});
continue;
}
const r = await this.importFromTahesab(organizationId, {
connectionId: dto.connectionId,
moshtariCode,
});
items.push({ moshtariCode, ok: true, personId: r.personId });
} catch (e) {
const message =
e instanceof Error ? e.message : 'وارد کردن ناموفق بود';
items.push({ moshtariCode, ok: false, error: message });
}
}
const imported = items.filter((i) => i.ok && !i.skipped).length;
const skipped = items.filter((i) => i.skipped).length;
const failed = items.filter((i) => !i.ok).length;
await this.prisma.accountingSyncJob.create({
data: {
organizationId,
connectionId: dto.connectionId,
kind: 'import',
status: failed && !imported ? 'error' : 'success',
request: dto as unknown as Prisma.InputJsonValue,
result: { imported, skipped, failed, count: items.length } as Prisma.InputJsonValue,
},
});
return { imported, skipped, failed, items };
}
}
@@ -20,6 +20,7 @@ import { TahesabDocumentsService } from './documents.service';
import {
CreateInTahesabDto,
CreateTahesabConnectionDto,
ImportBulkFromTahesabDto,
ImportFromTahesabDto,
ManualLinkDto,
MatchPersonDto,
@@ -200,4 +201,13 @@ export class TahesabController {
) {
return this.links.importFromTahesab(user.organizationId, dto);
}
@Post('sync/import-bulk')
@Roles('owner', 'admin')
importBulk(
@CurrentUser() user: { organizationId: string },
@Body() dto: ImportBulkFromTahesabDto,
) {
return this.links.importBulk(user.organizationId, dto);
}
}