Fix Tahesab health check to match Postman root-endpoint flow.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -251,12 +251,7 @@ export class TahesabConnectionsService {
|
|||||||
async test(organizationId: string, id: string) {
|
async test(organizationId: string, id: string) {
|
||||||
const { row, api } = await this.getApi(organizationId, id);
|
const { row, api } = await this.getApi(organizationId, id);
|
||||||
try {
|
try {
|
||||||
let result: unknown;
|
const result = await api.checkHealth();
|
||||||
try {
|
|
||||||
result = await api.checkHealthSsl();
|
|
||||||
} catch {
|
|
||||||
result = await api.checkHealthNoSsl();
|
|
||||||
}
|
|
||||||
const updated = await this.prisma.accountingConnection.update({
|
const updated = await this.prisma.accountingConnection.update({
|
||||||
where: { id: row.id },
|
where: { id: row.id },
|
||||||
data: {
|
data: {
|
||||||
@@ -268,15 +263,20 @@ export class TahesabConnectionsService {
|
|||||||
return { ok: true, connection: this.toDto(updated), health: result };
|
return { ok: true, connection: this.toDto(updated), health: result };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = (err as Error).message ?? 'خطای ناشناخته';
|
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({
|
const updated = await this.prisma.accountingConnection.update({
|
||||||
where: { id: row.id },
|
where: { id: row.id },
|
||||||
data: {
|
data: {
|
||||||
lastHealthAt: new Date(),
|
lastHealthAt: new Date(),
|
||||||
lastHealthOk: false,
|
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> {
|
async call<T = unknown>(opts: TahesabCallOptions): Promise<T> {
|
||||||
const base = opts.baseUrl.replace(/\/+$/, '');
|
const base = opts.baseUrl.replace(/\/+$/, '');
|
||||||
const path = opts.path.replace(/^\/+/, '');
|
const path = (opts.path ?? '').replace(/^\/+/, '');
|
||||||
const url = new URL(`${base}/${path}`);
|
// Postman تهحساب برای health به ریشه میزند (بدون path)
|
||||||
|
const url = path ? new URL(`${base}/${path}`) : new URL(base);
|
||||||
const method = opts.method ?? (opts.bodyKey ? 'POST' : 'GET');
|
const method = opts.method ?? (opts.bodyKey ? 'POST' : 'GET');
|
||||||
const timeoutMs = opts.timeoutMs ?? 30_000;
|
const timeoutMs = opts.timeoutMs ?? 30_000;
|
||||||
|
const label = path || '(root)';
|
||||||
|
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
Authorization: `Bearer ${opts.token}`,
|
Authorization: `Bearer ${opts.token}`,
|
||||||
DBname: opts.dbName,
|
DBname: opts.dbName,
|
||||||
DBName: opts.dbName,
|
DBName: opts.dbName,
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
Host: url.host,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let body: string | undefined;
|
let body: string | undefined;
|
||||||
@@ -72,12 +73,20 @@ export class TahesabHttpClient {
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return new Promise<T>((resolve, reject) => {
|
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(
|
const req = lib.request(
|
||||||
{
|
{
|
||||||
protocol: url.protocol,
|
protocol: url.protocol,
|
||||||
hostname: url.hostname,
|
hostname: url.hostname,
|
||||||
port: url.port || (isHttps ? 443 : 80),
|
port: url.port || (isHttps ? 443 : 80),
|
||||||
path: `${url.pathname}${url.search}`,
|
path: `${url.pathname}${url.search}` || '/',
|
||||||
method,
|
method,
|
||||||
headers,
|
headers,
|
||||||
timeout: timeoutMs,
|
timeout: timeoutMs,
|
||||||
@@ -106,19 +115,21 @@ export class TahesabHttpClient {
|
|||||||
if (status < 200 || status >= 300) {
|
if (status < 200 || status >= 300) {
|
||||||
if (isEmptyOk) {
|
if (isEmptyOk) {
|
||||||
this.logger.debug(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
`Tahesab ${method} ${path} → ${status}: ${text.slice(0, 400)}`,
|
`Tahesab ${method} ${label} → ${status}: ${text.slice(0, 400)}`,
|
||||||
);
|
);
|
||||||
reject(
|
finish(() =>
|
||||||
new BadGatewayException(
|
reject(
|
||||||
detail
|
new BadGatewayException(
|
||||||
? `تهحساب (${status}): ${detail}`
|
detail
|
||||||
: `پاسخ نامعتبر تهحساب (${status}) برای ${path}`,
|
? `تهحساب (${status}): ${detail}`
|
||||||
|
: `پاسخ نامعتبر تهحساب (${status}) برای ${label}`,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@@ -127,42 +138,61 @@ export class TahesabHttpClient {
|
|||||||
if (json && typeof json === 'object' && 'ERROR' in (json as object)) {
|
if (json && typeof json === 'object' && 'ERROR' in (json as object)) {
|
||||||
if (isEmptyOk) {
|
if (isEmptyOk) {
|
||||||
this.logger.debug(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
reject(
|
finish(() =>
|
||||||
new BadGatewayException(
|
reject(
|
||||||
detail ? `تهحساب: ${detail}` : `خطای تهحساب برای ${path}`,
|
new BadGatewayException(
|
||||||
|
detail ? `تهحساب: ${detail}` : `خطای تهحساب برای ${label}`,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return;
|
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.on('timeout', () => {
|
||||||
req.destroy();
|
req.destroy();
|
||||||
reject(
|
finish(() =>
|
||||||
new RequestTimeoutException(
|
reject(
|
||||||
`مهلت اتصال به تهحساب برای ${path} به پایان رسید.`,
|
new RequestTimeoutException(
|
||||||
|
`مهلت اتصال به تهحساب برای ${label} به پایان رسید.`,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
req.on('error', (err) => {
|
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 raw = err.message || String(err);
|
||||||
const tlsHint =
|
const tlsHint =
|
||||||
/self-signed|certificate|UNABLE_TO_VERIFY|CERT_/i.test(raw)
|
/self-signed|certificate|UNABLE_TO_VERIFY|CERT_/i.test(raw)
|
||||||
? ' — در تنظیمات اتصال، گزینه «پذیرش گواهی SSL خودامضا» را فعال کنید.'
|
? ' — در تنظیمات اتصال، گزینه «پذیرش گواهی SSL خودامضا» را فعال کنید.'
|
||||||
: '';
|
: '';
|
||||||
reject(
|
finish(() =>
|
||||||
new BadGatewayException(
|
reject(
|
||||||
`اتصال به تهحساب ناموفق بود: ${raw}${tlsHint}`,
|
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 } */
|
/** ثبت مشتری — پاسخ { OK: moshtariCode } */
|
||||||
doNewMoshtari(args: unknown[]) {
|
doNewMoshtari(args: unknown[]) {
|
||||||
return this.call<{ OK?: number }>('DoNewMoshtari', 'DoNewMoshtari', args);
|
return this.call<{ OK?: number }>('DoNewMoshtari', 'DoNewMoshtari', args);
|
||||||
|
|||||||
Reference in New Issue
Block a user