631 lines
15 KiB
TypeScript
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,
|
|
];
|
|
},
|
|
};
|