Paginate Tahesab customer list by code windows past the 5000 cap.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-04 11:09:40 +03:30
co-authored by Cursor
parent 60c16053a4
commit 236f73de93
3 changed files with 233 additions and 45 deletions
+35 -2
View File
@@ -7,6 +7,7 @@ import {
IsOptional,
IsString,
IsUrl,
Max,
MaxLength,
Min,
MinLength,
@@ -155,10 +156,24 @@ export class ImportBulkFromTahesabDto {
@Min(1)
toCode?: number;
/** اگر true باشد همهٔ مشتریان با اسکن بازه‌ای خوانده می‌شوند */
/** اگر true باشد یک صفحه از مشتریان (بازهٔ کد) خوانده می‌شود */
@IsOptional()
@IsBoolean()
listAll?: boolean;
/** صفحهٔ بازهٔ کد هنگام listAll (پیش‌فرض ۱) */
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
page?: number;
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(100)
@Max(2000)
pageSize?: number;
}
export class SearchTahesabDto {
@@ -191,10 +206,28 @@ export class SearchTahesabDto {
@Min(1)
toCode?: number;
/** بازه خالی در UI → همهٔ مشتریان */
/** بازه خالی در UI → مرور صفحه‌ای همهٔ مشتریان */
@IsOptional()
@IsBoolean()
listAll?: boolean;
/**
* صفحهٔ بازهٔ کد برای listAll (۱ = کد ۱…۲۰۰۰، ۲ = ۲۰۰۱…۴۰۰۰، …).
* سقف ۵۰۰۰ دیگر اعمال نمی‌شود.
*/
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
page?: number;
/** اندازهٔ پنجرهٔ کد در هر صفحه (پیش‌فرض ۲۰۰۰، حداکثر ۲۰۰۰) */
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(100)
@Max(2000)
pageSize?: number;
}
export class SearchTahesabProductsDto {
@@ -17,10 +17,9 @@ import {
} from './dto';
import { normalizeIranMobile, SanadBuilders, type TahesabApi } from './tahesab-methods';
const LIST_ALL_CHUNK = 2000;
/** سقف «همه» در یک درخواست ByCode — تکه‌تکه باعث ۵۰۲ پروکسی می‌شود */
const LIST_ALL_MAX_CODE = 5_000;
const LIST_ALL_TIMEOUT_MS = 55_000;
/** اندازهٔ هر صفحهٔ «همه» (بازهٔ کد) — امن برای پروکسی/nginx */
const LIST_PAGE_SIZE = 2000;
const LIST_TIMEOUT_MS = 55_000;
function asMoshtariList(raw: unknown): Array<Record<string, unknown>> {
if (!raw) return [];
@@ -318,25 +317,32 @@ export class TahesabPersonLinkService {
}
/**
* لیست همهٔ مشتریان در یک درخواست (بازه ۱…LIST_ALL_MAX_CODE).
* تکه‌تکه کردن پشت‌سرهم معمولاً از مهلت nginx بیشتر می‌شود و ۵۰۲ می‌دهد.
* یک صفحه از مشتریان بر اساس بازهٔ کد:
* صفحه ۱ → ۱…pageSize، صفحه ۲ → pageSize+1…۲×pageSize، …
* سقف ۵۰۰۰ حذف شده؛ با پیجینیت می‌توان همه را مرور کرد.
*/
private async listAllMoshtari(api: TahesabApi) {
try {
return asMoshtariList(
await api.doListMoshtariByCode(1, LIST_ALL_MAX_CODE, {
timeoutMs: LIST_ALL_TIMEOUT_MS,
}),
);
} catch (err) {
// اگر بازهٔ بزرگ سمت ته‌حساب رد شد، همان سقف دستی ۲۰۰۰ را یک‌بار امتحان کن
if (LIST_ALL_MAX_CODE <= LIST_ALL_CHUNK) throw err;
return asMoshtariList(
await api.doListMoshtariByCode(1, LIST_ALL_CHUNK, {
timeoutMs: LIST_ALL_TIMEOUT_MS,
}),
);
}
private async listMoshtariPage(
api: TahesabApi,
page: number,
pageSize?: number,
) {
const size = Math.min(
LIST_PAGE_SIZE,
Math.max(100, Math.floor(pageSize ?? LIST_PAGE_SIZE) || LIST_PAGE_SIZE),
);
const p = Math.max(1, Math.floor(page) || 1);
const fromCode = (p - 1) * size + 1;
const toCode = fromCode + size - 1;
const raw = await api.doListMoshtariByCode(fromCode, toCode, {
timeoutMs: LIST_TIMEOUT_MS,
});
return {
rows: asMoshtariList(raw),
fromCode,
toCode,
page: p,
pageSize: size,
};
}
async search(organizationId: string, dto: SearchTahesabDto) {
@@ -345,8 +351,28 @@ export class TahesabPersonLinkService {
dto.connectionId,
);
let raw: unknown = [];
let listMeta:
| {
page: number;
pageSize: number;
fromCode: number;
toCode: number;
}
| undefined;
if (dto.listAll) {
raw = await this.listAllMoshtari(api);
const listed = await this.listMoshtariPage(
api,
dto.page ?? 1,
dto.pageSize,
);
raw = listed.rows;
listMeta = {
page: listed.page,
pageSize: listed.pageSize,
fromCode: listed.fromCode,
toCode: listed.toCode,
};
} else if (dto.fromCode != null && dto.toCode != null) {
if (dto.toCode < dto.fromCode) {
throw new BadRequestException('کد پایان باید بزرگ‌تر یا مساوی کد شروع باشد.');
@@ -354,7 +380,9 @@ export class TahesabPersonLinkService {
if (dto.toCode - dto.fromCode > 2000) {
throw new BadRequestException('بازهٔ کد حداکثر ۲۰۰۰ عدد است (مثلاً ۱ تا ۲۰۰۰). برای همه، بازه را خالی بگذارید.');
}
raw = await api.doListMoshtariByCode(dto.fromCode, dto.toCode);
raw = await api.doListMoshtariByCode(dto.fromCode, dto.toCode, {
timeoutMs: LIST_TIMEOUT_MS,
});
} else if (dto.moshtariCode) {
raw = await api.doListMoshtariByCode(dto.moshtariCode, dto.moshtariCode);
} else if (dto.phone) {
@@ -414,11 +442,23 @@ export class TahesabPersonLinkService {
kind: 'search',
status: 'success',
request: dto as unknown as Prisma.InputJsonValue,
result: { count: results.length } as Prisma.InputJsonValue,
result: {
count: results.length,
...(listMeta ?? {}),
} as Prisma.InputJsonValue,
},
});
return { results };
return {
results,
...(listMeta
? {
...listMeta,
hasMore: results.length > 0,
note: `صفحه ${listMeta.page}: کد ${listMeta.fromCode} تا ${listMeta.toCode}. برای ادامه صفحه بعد را بزنید.`,
}
: {}),
};
}
async importFromTahesab(
@@ -516,10 +556,15 @@ export class TahesabPersonLinkService {
organizationId,
dto.connectionId,
);
const listed = (await this.listAllMoshtari(api))
const listed = await this.listMoshtariPage(
api,
dto.page ?? 1,
dto.pageSize,
);
const listedCodes = listed.rows
.map((row) => pickCode(row))
.filter((c): c is number => c != null);
codes = [...new Set([...codes, ...listed])];
codes = [...new Set([...codes, ...listedCodes])];
} else if (dto.fromCode != null && dto.toCode != null) {
if (dto.toCode < dto.fromCode) {
throw new BadRequestException('کد پایان باید بزرگ‌تر یا مساوی کد شروع باشد.');
@@ -545,10 +590,10 @@ export class TahesabPersonLinkService {
'کد مشتری یا بازهٔ کد برای وارد کردن مشخص نشده است.',
);
}
const maxImport = dto.listAll ? 2000 : 500;
const maxImport = dto.listAll || (dto.fromCode != null && dto.toCode != null) ? 2000 : 500;
if (codes.length > maxImport) {
throw new BadRequestException(
`حداکثر ${maxImport} مشتری در هر بار وارد کنید (الان ${codes.length}). بازه را کوچک‌تر کنید.`,
`حداکثر ${maxImport} مشتری در هر بار وارد کنید (الان ${codes.length}). صفحه یا بازه را کوچک‌تر کنید.`,
);
}