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,480 @@
|
||||
import { randomBytes } from 'crypto';
|
||||
import type { TahesabHttpClient } 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 ? 'بس' : 'بد';
|
||||
}
|
||||
|
||||
/** نرمالسازی پاسخ 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',
|
||||
) {
|
||||
return this.http.call<T>({
|
||||
...this.creds,
|
||||
path,
|
||||
bodyKey,
|
||||
args,
|
||||
method,
|
||||
});
|
||||
}
|
||||
|
||||
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',
|
||||
);
|
||||
}
|
||||
|
||||
/** ثبت مشتری — پاسخ { 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) {
|
||||
return this.call(
|
||||
'DoListMoshtari-ByCode',
|
||||
'DoListMoshtari',
|
||||
[fromCode, toCode],
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
) {
|
||||
return this.call('DoListAsnad', 'DoListAsnad', [
|
||||
countLast,
|
||||
moshtariCode,
|
||||
fromShamsi,
|
||||
toShamsi,
|
||||
]);
|
||||
}
|
||||
|
||||
doListNameSekeh() {
|
||||
return this.call('DoListNameSekeh', 'DoListNameSekeh', []);
|
||||
}
|
||||
|
||||
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,
|
||||
];
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user