Add Tahesab jewelry accounting integration with multi-DB connections.
Encrypt tokens server-side, sync customers both ways, show balances/documents on the person Assets tab, and push sanads with idempotent Factor_Code logging. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
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;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class TahesabHttpClient {
|
||||
private readonly logger = new Logger(TahesabHttpClient.name);
|
||||
|
||||
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 method = opts.method ?? (opts.bodyKey ? 'POST' : 'GET');
|
||||
const timeoutMs = opts.timeoutMs ?? 30_000;
|
||||
|
||||
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;
|
||||
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<T>((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;
|
||||
if (status < 200 || status >= 300) {
|
||||
this.logger.warn(
|
||||
`Tahesab ${method} ${path} → ${status}: ${text.slice(0, 400)}`,
|
||||
);
|
||||
reject(
|
||||
new BadGatewayException(
|
||||
`پاسخ نامعتبر تهحساب (${status}) برای ${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);
|
||||
reject(
|
||||
new BadGatewayException(
|
||||
`اتصال به تهحساب ناموفق بود: ${err.message}`,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
if (body) req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user