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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user