Fix Tahesab health check to match Postman root-endpoint flow.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-03 16:45:04 +03:30
co-authored by Cursor
parent c322a24424
commit 17628722d3
3 changed files with 109 additions and 33 deletions
@@ -251,12 +251,7 @@ export class TahesabConnectionsService {
async test(organizationId: string, id: string) {
const { row, api } = await this.getApi(organizationId, id);
try {
let result: unknown;
try {
result = await api.checkHealthSsl();
} catch {
result = await api.checkHealthNoSsl();
}
const result = await api.checkHealth();
const updated = await this.prisma.accountingConnection.update({
where: { id: row.id },
data: {
@@ -268,15 +263,20 @@ export class TahesabConnectionsService {
return { ok: true, connection: this.toDto(updated), health: result };
} catch (err) {
const message = (err as Error).message ?? 'خطای ناشناخته';
const hint =
/مهلت اتصال|timeout/i.test(message) && !row.allowInsecureTls && /^https:/i.test(row.baseUrl)
? ' اگر گواهی ته‌حساب خودامضاست، «پذیرش گواهی SSL خودامضا» را فعال کنید.'
: '';
const full = `${message}${hint}`.slice(0, 500);
const updated = await this.prisma.accountingConnection.update({
where: { id: row.id },
data: {
lastHealthAt: new Date(),
lastHealthOk: false,
lastError: message.slice(0, 500),
lastError: full,
},
});
return { ok: false, connection: this.toDto(updated), error: message };
return { ok: false, connection: this.toDto(updated), error: full };
}
}
}
@@ -44,17 +44,18 @@ export class TahesabHttpClient {
async call<T = unknown>(opts: TahesabCallOptions): Promise<T> {
const base = opts.baseUrl.replace(/\/+$/, '');
const path = opts.path.replace(/^\/+/, '');
const url = new URL(`${base}/${path}`);
const path = (opts.path ?? '').replace(/^\/+/, '');
// Postman ته‌حساب برای health به ریشه می‌زند (بدون path)
const url = path ? new URL(`${base}/${path}`) : new URL(base);
const method = opts.method ?? (opts.bodyKey ? 'POST' : 'GET');
const timeoutMs = opts.timeoutMs ?? 30_000;
const label = path || '(root)';
const headers: Record<string, string> = {
Authorization: `Bearer ${opts.token}`,
DBname: opts.dbName,
DBName: opts.dbName,
Accept: 'application/json',
Host: url.host,
};
let body: string | undefined;
@@ -72,12 +73,20 @@ export class TahesabHttpClient {
: undefined;
return new Promise<T>((resolve, reject) => {
let settled = false;
const finish = (fn: () => void) => {
if (settled) return;
settled = true;
clearTimeout(hardTimer);
fn();
};
const req = lib.request(
{
protocol: url.protocol,
hostname: url.hostname,
port: url.port || (isHttps ? 443 : 80),
path: `${url.pathname}${url.search}`,
path: `${url.pathname}${url.search}` || '/',
method,
headers,
timeout: timeoutMs,
@@ -106,19 +115,21 @@ export class TahesabHttpClient {
if (status < 200 || status >= 300) {
if (isEmptyOk) {
this.logger.debug(
`Tahesab ${path} ${status} empty-on-error: ${detail.slice(0, 120)}`,
`Tahesab ${label} ${status} empty-on-error: ${detail.slice(0, 120)}`,
);
resolve([] as T);
finish(() => resolve([] as T));
return;
}
this.logger.warn(
`Tahesab ${method} ${path} → ${status}: ${text.slice(0, 400)}`,
`Tahesab ${method} ${label} → ${status}: ${text.slice(0, 400)}`,
);
reject(
new BadGatewayException(
detail
? `ته‌حساب (${status}): ${detail}`
: `پاسخ نامعتبر ته‌حساب (${status}) برای ${path}`,
finish(() =>
reject(
new BadGatewayException(
detail
? `ته‌حساب (${status}): ${detail}`
: `پاسخ نامعتبر ته‌حساب (${status}) برای ${label}`,
),
),
);
return;
@@ -127,42 +138,61 @@ export class TahesabHttpClient {
if (json && typeof json === 'object' && 'ERROR' in (json as object)) {
if (isEmptyOk) {
this.logger.debug(
`Tahesab ${path} empty-on-error: ${detail.slice(0, 120)}`,
`Tahesab ${label} empty-on-error: ${detail.slice(0, 120)}`,
);
resolve([] as T);
finish(() => resolve([] as T));
return;
}
reject(
new BadGatewayException(
detail ? `ته‌حساب: ${detail}` : `خطای ته‌حساب برای ${path}`,
finish(() =>
reject(
new BadGatewayException(
detail ? `ته‌حساب: ${detail}` : `خطای ته‌حساب برای ${label}`,
),
),
);
return;
}
resolve(json as T);
finish(() => resolve(json as T));
});
},
);
// مهلت از لحظهٔ شروع (شامل اتصال TCP/TLS) — timeout سوکت Node فقط بعد از connect است
const hardTimer = setTimeout(() => {
req.destroy();
finish(() =>
reject(
new RequestTimeoutException(
`مهلت اتصال به ته‌حساب برای ${label} به پایان رسید.`,
),
),
);
}, timeoutMs);
req.on('timeout', () => {
req.destroy();
reject(
new RequestTimeoutException(
`مهلت اتصال به ته‌حساب برای ${path} به پایان رسید.`,
finish(() =>
reject(
new RequestTimeoutException(
`مهلت اتصال به ته‌حساب برای ${label} به پایان رسید.`,
),
),
);
});
req.on('error', (err) => {
this.logger.error(`Tahesab call failed: ${path}`, err);
if (settled) return;
this.logger.error(`Tahesab call failed: ${label}`, err);
const raw = err.message || String(err);
const tlsHint =
/self-signed|certificate|UNABLE_TO_VERIFY|CERT_/i.test(raw)
? ' — در تنظیمات اتصال، گزینه «پذیرش گواهی SSL خودامضا» را فعال کنید.'
: '';
reject(
new BadGatewayException(
`اتصال به ته‌حساب ناموفق بود: ${raw}${tlsHint}`,
finish(() =>
reject(
new BadGatewayException(
`اتصال به ته‌حساب ناموفق بود: ${raw}${tlsHint}`,
),
),
);
});
@@ -177,6 +177,52 @@ export class TahesabApi {
);
}
/**
* تست سلامت مطابق Postman ته‌حساب:
* ابتدا ریشهٔ baseUrl، بعد /CheckHealth، در نهایت مسیر SSL یا No-SSL بر اساس پروتکل.
*/
async checkHealth() {
const isHttps = /^https:/i.test(this.creds.baseUrl.trim());
const timeoutMs = 12_000;
type Health = { Status?: string; Api_Status?: string };
const attempts: Array<{
path: string;
bodyKey?: string;
method: 'GET' | 'POST';
}> = [
{ path: '', method: 'GET' },
{ path: '', bodyKey: 'CheckHealth', method: 'POST' },
{ path: 'CheckHealth', method: 'GET' },
{
path: isHttps ? 'CheckHealth-SSL' : 'CheckHealth-No-SSL',
method: 'GET',
},
];
let lastErr: unknown;
for (const attempt of attempts) {
try {
return await this.call<Health>(
attempt.path,
attempt.bodyKey,
attempt.bodyKey ? [] : undefined,
attempt.method,
timeoutMs,
);
} catch (err) {
lastErr = err;
const msg = err instanceof Error ? err.message : String(err);
// خطای گواهی با تلاش‌های بعدی روی همان https حل نمی‌شود
if (/certificate|UNABLE_TO_VERIFY|self-signed|CERT_/i.test(msg)) {
throw err;
}
}
}
throw lastErr instanceof Error
? lastErr
: new Error('تست اتصال ته‌حساب ناموفق بود.');
}
/** ثبت مشتری — پاسخ { OK: moshtariCode } */
doNewMoshtari(args: unknown[]) {
return this.call<{ OK?: number }>('DoNewMoshtari', 'DoNewMoshtari', args);