import { randomBytes } from 'crypto'; import { tahesabHealthBaseCandidates, type TahesabHttpClient, type TahesabCallOptions, } from './tahesab-http.client'; export type ConnCreds = { baseUrl: string; token: string; dbName: string; allowInsecureTls?: boolean; }; /** حداقل طول Factor_Code طبق مستندات ته‌حساب */ export const FACTOR_CODE_MIN_LEN = 20; export function buildFactorCode(prefix = 'crm'): string { const stamp = Date.now().toString(36); const rand = randomBytes(12).toString('hex'); const code = `${prefix}${stamp}${rand}`; return code.length >= FACTOR_CODE_MIN_LEN ? code : code.padEnd(FACTOR_CODE_MIN_LEN, '0'); } export function assertFactorCode(code: string): void { if (!code || code.trim().length < FACTOR_CODE_MIN_LEN) { throw new Error( `Factor_Code باید حداقل ${FACTOR_CODE_MIN_LEN} کاراکتر باشد.`, ); } } /** نرمال‌سازی موبایل ایران برای جستجو در ته‌حساب */ export function normalizeIranMobile(input: string | null | undefined): string | null { if (!input) return null; let d = input.replace(/[^\d]/g, ''); if (d.startsWith('98') && d.length >= 12) d = d.slice(2); if (d.startsWith('0') && d.length >= 11) d = d.slice(1); if (d.length < 10) return null; return d; } export type MandehValue = { amount: string | number; unit: string; status: 'بس' | 'بد'; }; export type MandehDetailItem = { Name: string; values: MandehValue[]; }; export type NormalizedAssets = { goldBalance: number | null; rialBalance: number | null; currencyBalance: number | null; totalGoldEquiv: number | null; mandehDetail: MandehDetailItem[]; raw: unknown; }; function num(v: unknown): number | null { if (v === null || v === undefined || v === '') return null; const n = Number(v); return Number.isFinite(n) ? n : null; } function detailStatus(amount: number): 'بس' | 'بد' { return amount < 0 ? 'بس' : 'بد'; } function rawStr(v: unknown): string | null { if (v === null || v === undefined) return null; const s = String(v); return s === 'null' || s === 'undefined' ? null : s; } /** فیلدهای پاسخ مانده بدون هیچ تبدیل/محاسبه، به ترتیب خود ته‌حساب */ export function extractMandehRaw(raw: unknown): { fields: Array<{ key: string; value: string | null }>; details: Array<{ Name: string; Value: string | null; Value1: string | null; Value2: string | null; Value3: string | null; Value4: string | null; }>; } { const row = (raw as { MandeHesab?: Array> } | null) ?.MandeHesab?.[0]; if (!row || typeof row !== 'object') return { fields: [], details: [] }; const fields = Object.entries(row) .filter(([k, v]) => k !== 'details' && (v === null || typeof v !== 'object')) .map(([key, v]) => ({ key, value: rawStr(v) })); const details = (Array.isArray(row.details) ? row.details : []).map((item) => { const it = (item ?? {}) as Record; return { Name: String(it.Name ?? ''), Value: rawStr(it.Value), Value1: rawStr(it.Value1), Value2: rawStr(it.Value2), Value3: rawStr(it.Value3), Value4: rawStr(it.Value4), }; }); return { fields, details }; } /** نرمال‌سازی پاسخ getmandehesabbycode مثل همکار */ export function normalizeMandehResponse(raw: unknown): NormalizedAssets { const root = raw as { MandeHesab?: Array>; }; const row = root?.MandeHesab?.[0]; if (!row) { return { goldBalance: null, rialBalance: null, currencyBalance: null, totalGoldEquiv: null, mandehDetail: [], raw, }; } const details = Array.isArray(row.details) ? row.details : []; const mandehDetail: MandehDetailItem[] = details.map((item) => { const it = item as Record; const values: MandehValue[] = []; if (it.Value1 !== null && it.Value1 !== undefined) { const a = Number(it.Value1); values.push({ amount: it.Value1 as string | number, unit: 'گرم', status: detailStatus(a), }); } if (it.Value2 !== null && it.Value2 !== undefined) { const a = Number(it.Value2); values.push({ amount: it.Value2 as string | number, unit: 'عدد', status: detailStatus(a), }); } if (it.Value3 !== null && it.Value3 !== undefined) { const a = Number(it.Value3); values.push({ amount: it.Value3 as string | number, unit: 'ارز', status: detailStatus(a), }); } if (it.Value4 !== null && it.Value4 !== undefined) { const a = Number(it.Value4); values.push({ amount: it.Value4 as string | number, unit: 'ریال', status: detailStatus(a), }); } return { Name: String(it.Name ?? ''), values }; }); return { goldBalance: num(row.MandeyeVazni), rialBalance: num(row.MandeyeMali), currencyBalance: num(row.MandeyeKolBePool), totalGoldEquiv: num(row.MandeyeKolBeTALA), mandehDetail, raw, }; } export class TahesabApi { constructor( private readonly http: TahesabHttpClient, private readonly creds: ConnCreds, ) {} private call( path: string, bodyKey?: string, args?: unknown[], method?: 'GET' | 'POST', timeoutMs?: number, extra?: Pick, ) { return this.http.call({ ...this.creds, path, bodyKey, args, method, timeoutMs, ...extra, }); } checkHealthSsl() { return this.call<{ Status?: string; Api_Status?: string }>( 'CheckHealth-SSL', undefined, undefined, 'GET', ); } checkHealthNoSsl() { return this.call<{ Status?: string; Api_Status?: string }>( 'CheckHealth-No-SSL', undefined, undefined, 'GET', ); } /** * تست سلامت مطابق Postman ته‌حساب (ریشه + CheckHealth). * روی تایم‌اوت، پورت/پروتکل جایگزین (8081/9550) و در صورت نیاز TLS بدون verify را هم امتحان می‌کند. */ async checkHealth(): Promise<{ Status?: string; Api_Status?: string; usedBaseUrl?: string; usedPath?: string; usedInsecureTls?: boolean; }> { const timeoutMs = 8_000; type Health = { Status?: string; Api_Status?: string }; const bases = tahesabHealthBaseCandidates(this.creds.baseUrl); const insecureFlags = this.creds.allowInsecureTls ? [true] : [false, true]; let lastErr: unknown; for (const baseUrl of bases) { const isHttps = /^https:/i.test(baseUrl); const pathAttempts: 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', }, ]; for (const allowInsecureTls of insecureFlags) { if (!isHttps && allowInsecureTls) continue; for (const attempt of pathAttempts) { try { const health = await this.http.call({ ...this.creds, baseUrl, allowInsecureTls, path: attempt.path, bodyKey: attempt.bodyKey, args: attempt.bodyKey ? [] : undefined, method: attempt.method, timeoutMs, }); return { ...health, usedBaseUrl: baseUrl, usedPath: attempt.path || '(root)', usedInsecureTls: allowInsecureTls, }; } catch (err) { lastErr = err; } } } } throw lastErr instanceof Error ? lastErr : new Error('تست اتصال ته‌حساب ناموفق بود.'); } /** ثبت مشتری — پاسخ { OK: moshtariCode } */ doNewMoshtari(args: unknown[]) { return this.call<{ OK?: number }>('DoNewMoshtari', 'DoNewMoshtari', args); } doEditMoshtari(args: unknown[]) { return this.call('DoEditMoshtari', 'DoEditMoshtari', args); } doListMoshtariByCode( fromCode: number, toCode: number, opts?: { timeoutMs?: number }, ) { // OpenAPI/Postman: کلید body برای ByCode دقیقاً «Dolistmoshtari» است (حساس به حروف) return this.call( 'DoListMoshtari-ByCode', 'Dolistmoshtari', [fromCode, toCode], 'POST', opts?.timeoutMs, ); } doListMoshtariByPhone(phone: string | number) { return this.call('DoListMoshtari-ByPhone', 'DoListMoshtari', [phone]); } doListMoshtariByName(name: string) { return this.call('DoListMoshtari-ByName', 'DoListMoshtari', [name]); } /** body key حساس به حروف کوچک مثل همکار */ getMandeHesabByCode(moshtariCode: number | string) { return this.call( 'GetMandeHesabByCode', 'getmandehesabbycode', [String(moshtariCode)], ); } doListAsnad( countLast: number, moshtariCode: number, fromShamsi: string, toShamsi: string, ) { // همکار: POST به baseUrl بدون path، با کلید body = DoListAsnad و ۴ آرگومان const args = [countLast, moshtariCode, fromShamsi, toShamsi]; const empty = { emptyResultOnError: /رکوردی یافت نشد/i }; const timeoutMs = 55_000; const rootCall = () => this.http.call({ ...this.creds, path: '', bodyKey: 'DoListAsnad', args, method: 'POST' as const, timeoutMs, ...empty, }); const pathCall = () => this.http.call({ ...this.creds, path: 'DoListAsnad', bodyKey: 'DoListAsnad', args, method: 'POST' as const, timeoutMs, ...empty, }); return rootCall().catch(async (err) => { try { return await pathCall(); } catch { throw err; } }); } doListNameSekeh() { return this.call('DoListNameSekeh', 'DoListNameSekeh', []); } /** لیست نام کارهای ساخته دارای سند */ doListNameKarsakhte() { return this.call('DoListNameKarsakhte', 'DoListNameKarsakhte', []); } doListNameArz() { return this.call('DoListNameARZ', 'DoListNameARZ', []); } doListHesabBanki() { return this.call('DoListHesabBanki', 'DoListHesabBanki', []); } doNewSanadBuySaleGold(args: unknown[]) { return this.call('DoNewSanadBuySaleGOLD', 'DoNewSanadBuySaleGOLD', args); } doNewSanadBuySaleSekeh(args: unknown[]) { return this.call('DoNewSanadBuySaleSEKEH', 'DoNewSanadBuySaleSEKEH', args); } doNewSanadTalabBedehi(args: unknown[]) { return this.call('DoNewSanadTalabBedehi', 'DoNewSanadTalabBedehi', args); } doNewSanadVkhGold(args: unknown[]) { return this.call('DoNewSanadVKHGOLD', 'DoNewSanadVKHGOLD', args); } doNewSanadVkhBank(args: unknown[]) { return this.call('DoNewSanadVKHBank', 'DoNewSanadVKHBank', args); } } /** سازنده‌های آرایه سند — ترتیب پارامترها قفل‌شده با OpenAPI/همکار */ export const SanadBuilders = { buySaleGold(p: { moshtariCode: number; year: number; month: number; day: number; vazn: number; ayar: number; buyOrSale: 0 | 1; mazaneh: number; mazanehIsGeram: 0 | 1; isAbshode: 0 | 1; mablaghKol: number; sharh: string; factorCode: string; havalehBeMcode?: number; jensFelez?: number; zamanTasvie?: string; }): unknown[] { assertFactorCode(p.factorCode); return [ 1, p.moshtariCode, -1, 1, p.year, p.month, p.day, p.vazn, p.ayar, 123456, 'Reygiri', p.buyOrSale, p.mazaneh, p.mazanehIsGeram, p.isAbshode, p.mablaghKol, p.sharh, p.factorCode, p.havalehBeMcode ?? -1, -1, p.jensFelez ?? 0, p.zamanTasvie ?? '', ]; }, buySaleSekeh(p: { moshtariCode: number; year: number; month: number; day: number; vazn: number; ayar: number; count: number; nameSekeh: string; buyOrSale: 0 | 1; mazaneh: number; mablaghKol: number; sharh: string; factorCode: string; jensFelez?: number; arzName?: string; }): unknown[] { assertFactorCode(p.factorCode); return [ 1, p.moshtariCode, -1, 1, p.year, p.month, p.day, p.vazn, p.ayar, p.count, p.nameSekeh, p.buyOrSale, p.mazaneh, p.mablaghKol, p.sharh, p.factorCode, -1, -1, p.jensFelez ?? 0, p.arzName ?? 'IRR', ]; }, /** قرارداد همکار HavalehTahesabService */ talabBedehi(p: { moshtariCode: number; year: number; month: number; day: number; isBedehiOrTalab: 0 | 1; mablagh: number; sharh: string; baabat: string; vazn: number; factorCode: string; havalehBeMcode?: number; jensFelez?: number; arzName?: string; }): unknown[] { assertFactorCode(p.factorCode); return [ 1, p.moshtariCode, -1, 0, p.year, p.month, p.day, p.isBedehiOrTalab, p.mablagh, p.sharh, p.baabat, p.vazn, p.factorCode, p.havalehBeMcode ?? -1, 0, p.jensFelez ?? 0, p.arzName ?? 'ریال', ]; }, vkhGold(p: { moshtariCode: number; year: number; month: number; day: number; vazn: number; ayar: number; angNumber: string | number; nameAz: string; isVoroodOrKhorooj: 0 | 1; isAbshode: 0 | 1; sharh: string; factorCode: string; isSharti?: boolean; }): unknown[] { assertFactorCode(p.factorCode); return [ 1, p.moshtariCode, -1, 1, p.year, p.month, p.day, p.vazn, p.ayar, p.angNumber, p.nameAz, p.isVoroodOrKhorooj, p.isAbshode, p.sharh, p.factorCode, -1, -1, 0, p.isSharti ?? false, ]; }, vkhBank(p: { moshtariCode: number; year: number; month: number; day: number; isDaryaftOrPardakht: 0 | 1; mablagh: number; sharh: string; nameBank: string; codeRahgiri: string; factorCode: string; }): unknown[] { assertFactorCode(p.factorCode); return [ 1, p.moshtariCode, -1, 1, p.year, p.month, p.day, p.isDaryaftOrPardakht, p.mablagh, p.sharh, p.nameBank, p.codeRahgiri, p.factorCode, -1, 0, ]; }, newMoshtari(p: { name: string; groupName?: string; tel?: string; address?: string; codeMeli?: string; tarikhTavalod?: string; moaref?: string; codeMoaref?: string; moshtariCode?: number; jensFelez?: number; }): unknown[] { return [ p.name, p.groupName ?? 'CRM', p.tel ?? '', p.address ?? '', p.codeMeli ?? '', p.tarikhTavalod ?? '', p.moaref ?? '', p.codeMoaref ?? '-1', p.moshtariCode ?? -1, p.jensFelez ?? 0, ]; }, };