Files
CRM_Montzar/apps/api/src/integrations/tahesab/tahesab-methods.ts
T

631 lines
15 KiB
TypeScript

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<Record<string, unknown>> } | 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<string, unknown>;
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<Record<string, unknown>>;
};
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<string, unknown>;
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<T>(
path: string,
bodyKey?: string,
args?: unknown[],
method?: 'GET' | 'POST',
timeoutMs?: number,
extra?: Pick<TahesabCallOptions, 'emptyResultOnError'>,
) {
return this.http.call<T>({
...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<Health>({
...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,
];
},
};