import * as http from 'http'; import * as https from 'https'; import { URL } from 'url'; import { BadGatewayException, Injectable, Logger, RequestTimeoutException, } from '@nestjs/common'; export type TahesabCallOptions = { baseUrl: string; token: string; dbName: string; /** مسیر OpenAPI مثل DoNewMoshtari یا CheckHealth-SSL */ path: string; /** کلید body؛ اگر خالی باشد فقط GET بدون body */ bodyKey?: string; args?: unknown[]; method?: 'GET' | 'POST'; allowInsecureTls?: boolean; timeoutMs?: number; /** * اگر ته‌حساب با فیلد ERROR و متن منطبق جواب داد، به‌جای ۵۰۲ * مقدار خالی ([] ) برگردان — مثل «رکوردی یافت نشد» در DoListAsnad. */ emptyResultOnError?: RegExp; }; function extractTahesabError(json: unknown, text: string): string { if (json && typeof json === 'object') { const o = json as Record; const err = o.ERROR ?? o.Error ?? o.error ?? o.message ?? o.Message; if (typeof err === 'string' && err.trim()) return err.trim().slice(0, 300); if (Array.isArray(err) && err.length) return String(err[0]).slice(0, 300); } const trimmed = text.trim(); return trimmed ? trimmed.slice(0, 300) : ''; } @Injectable() export class TahesabHttpClient { private readonly logger = new Logger(TahesabHttpClient.name); async call(opts: TahesabCallOptions): Promise { const base = opts.baseUrl.replace(/\/+$/, ''); const path = opts.path.replace(/^\/+/, ''); const url = new URL(`${base}/${path}`); const method = opts.method ?? (opts.bodyKey ? 'POST' : 'GET'); const timeoutMs = opts.timeoutMs ?? 30_000; const headers: Record = { Authorization: `Bearer ${opts.token}`, DBname: opts.dbName, DBName: opts.dbName, Accept: 'application/json', Host: url.host, }; let body: string | undefined; if (method === 'POST' && opts.bodyKey) { headers['Content-Type'] = 'application/json'; body = JSON.stringify({ [opts.bodyKey]: opts.args ?? [] }); headers['Content-Length'] = Buffer.byteLength(body).toString(); } const isHttps = url.protocol === 'https:'; const lib = isHttps ? https : http; const agent = isHttps && opts.allowInsecureTls ? new https.Agent({ rejectUnauthorized: false }) : undefined; return new Promise((resolve, reject) => { const req = lib.request( { protocol: url.protocol, hostname: url.hostname, port: url.port || (isHttps ? 443 : 80), path: `${url.pathname}${url.search}`, method, headers, timeout: timeoutMs, agent, }, (res) => { const chunks: Buffer[] = []; res.on('data', (c) => chunks.push(c)); res.on('end', () => { const text = Buffer.concat(chunks).toString('utf8'); let json: unknown = null; if (text) { try { json = JSON.parse(text); } catch { json = { raw: text }; } } const status = res.statusCode ?? 0; const detail = extractTahesabError(json, text); const isEmptyOk = Boolean(detail) && Boolean(opts.emptyResultOnError) && opts.emptyResultOnError!.test(detail); if (status < 200 || status >= 300) { if (isEmptyOk) { this.logger.debug( `Tahesab ${path} ${status} empty-on-error: ${detail.slice(0, 120)}`, ); resolve([] as T); return; } this.logger.warn( `Tahesab ${method} ${path} → ${status}: ${text.slice(0, 400)}`, ); reject( new BadGatewayException( detail ? `ته‌حساب (${status}): ${detail}` : `پاسخ نامعتبر ته‌حساب (${status}) برای ${path}`, ), ); return; } // بعضی متدها با ۲۰۰ و فیلد ERROR برمی‌گردند if (json && typeof json === 'object' && 'ERROR' in (json as object)) { if (isEmptyOk) { this.logger.debug( `Tahesab ${path} empty-on-error: ${detail.slice(0, 120)}`, ); resolve([] as T); return; } reject( new BadGatewayException( detail ? `ته‌حساب: ${detail}` : `خطای ته‌حساب برای ${path}`, ), ); return; } resolve(json as T); }); }, ); req.on('timeout', () => { req.destroy(); reject( new RequestTimeoutException( `مهلت اتصال به ته‌حساب برای ${path} به پایان رسید.`, ), ); }); req.on('error', (err) => { this.logger.error(`Tahesab call failed: ${path}`, 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}`, ), ); }); if (body) req.write(body); req.end(); }); } }