Files
CRM_Montzar/apps/api/src/integrations/tahesab/documents.service.ts
T

408 lines
12 KiB
TypeScript

import { Prisma } from '@prisma/client';
import {
BadRequestException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
import { TahesabConnectionsService } from './connections.service';
import { PushSanadDto } from './dto';
import {
buildFactorCode,
SanadBuilders,
} from './tahesab-methods';
function normalizeShamsiYmd(input: string): string {
const m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(input.trim());
if (!m) {
throw new BadRequestException(
'تاریخ از/تا باید شمسی با قالب YYYY-MM-DD باشد.',
);
}
return `${m[1]}-${m[2]!.padStart(2, '0')}-${m[3]!.padStart(2, '0')}`;
}
function asAsnadList(raw: unknown): Array<Record<string, unknown>> {
if (!raw) return [];
if (Array.isArray(raw)) return raw as Array<Record<string, unknown>>;
if (typeof raw === 'object') {
const obj = raw as Record<string, unknown>;
for (const key of ['DoListAsnad', 'Asnad', 'asnad', 'Items', 'items', 'Data']) {
const nested = obj[key];
if (Array.isArray(nested)) {
return nested.filter((v) => v && typeof v === 'object') as Array<
Record<string, unknown>
>;
}
}
return Object.values(obj).filter(
(v) => v && typeof v === 'object' && !Array.isArray(v),
) as Array<Record<string, unknown>>;
}
return [];
}
@Injectable()
export class TahesabDocumentsService {
constructor(
private readonly prisma: PrismaService,
private readonly connections: TahesabConnectionsService,
) {}
private async requireLink(
organizationId: string,
personId: string,
connectionId: string,
) {
const person = await this.prisma.person.findFirst({
where: { id: personId, organizationId },
});
if (!person) throw new NotFoundException('شخص یافت نشد.');
const link = await this.prisma.personAccountingLink.findFirst({
where: { personId, connectionId },
});
if (!link) {
throw new BadRequestException(
'شخص به این اتصال ته‌حساب لینک نشده است.',
);
}
return link;
}
async listAsnad(
organizationId: string,
personId: string,
from: string,
to: string,
connectionId?: string,
) {
const fromNorm = normalizeShamsiYmd(from);
const toNorm = normalizeShamsiYmd(to);
const resolvedId = await this.connections.resolveConnectionId(
organizationId,
connectionId,
);
const link = await this.requireLink(organizationId, personId, resolvedId);
const { api } = await this.connections.getApi(organizationId, resolvedId);
// Count_Last مثل تست‌های همکار (۲۰۰) — مقدار ۱- گاهی سمت ته‌حساب خطا/۵۰۲ می‌دهد
const raw = await api.doListAsnad(
200,
link.moshtariCode,
fromNorm,
toNorm,
);
const items = asAsnadList(raw);
return {
connectionId: resolvedId,
moshtariCode: link.moshtariCode,
from: fromNorm,
to: toNorm,
items,
count: items.length,
};
}
async listLookups(organizationId: string, connectionId?: string) {
const resolvedId = await this.connections.resolveConnectionId(
organizationId,
connectionId,
);
const { api } = await this.connections.getApi(organizationId, resolvedId);
const [sekeh, arz, banks] = await Promise.all([
api.doListNameSekeh().catch(() => []),
api.doListNameArz().catch(() => []),
api.doListHesabBanki().catch(() => []),
]);
return { connectionId: resolvedId, sekeh, arz, banks };
}
async pushSanad(
organizationId: string,
personId: string,
dto: PushSanadDto,
) {
const link = await this.requireLink(
organizationId,
personId,
dto.connectionId,
);
const factorCode = dto.factorCode?.trim() || buildFactorCode('crm');
const existing = await this.prisma.accountingDocumentLog.findUnique({
where: {
connectionId_factorCode: {
connectionId: dto.connectionId,
factorCode,
},
},
});
if (existing?.status === 'success') {
return {
idempotent: true,
log: this.logDto(existing),
response: existing.responsePayload,
};
}
const { api } = await this.connections.getApi(
organizationId,
dto.connectionId,
);
const code = link.moshtariCode;
const sharh = dto.sharh ?? '';
let docTypeApi = '';
let args: unknown[] = [];
switch (dto.docType) {
case 'buy_sale_gold': {
if (dto.vazn == null || dto.ayar == null || dto.mazaneh == null || dto.mablaghKol == null || dto.buyOrSale == null) {
throw new BadRequestException(
'برای خرید/فروش آبشده: vazn، ayar، mazaneh، mablaghKol، buyOrSale الزامی است.',
);
}
docTypeApi = 'DoNewSanadBuySaleGOLD';
args = SanadBuilders.buySaleGold({
moshtariCode: code,
year: dto.year,
month: dto.month,
day: dto.day,
vazn: dto.vazn,
ayar: dto.ayar,
buyOrSale: dto.buyOrSale,
mazaneh: dto.mazaneh,
mazanehIsGeram: dto.mazanehIsGeram ?? 0,
isAbshode: dto.isAbshode ?? 1,
mablaghKol: dto.mablaghKol,
sharh,
factorCode,
havalehBeMcode: dto.havalehBeMcode,
jensFelez: dto.jensFelez,
});
break;
}
case 'buy_sale_sekeh': {
if (
dto.vazn == null ||
dto.ayar == null ||
dto.count == null ||
!dto.nameSekeh ||
dto.buyOrSale == null ||
dto.mazaneh == null ||
dto.mablaghKol == null
) {
throw new BadRequestException(
'برای خرید/فروش سکه فیلدهای وزن، عیار، تعداد، نام سکه، خرید/فروش، مظنه و مبلغ کل الزامی است.',
);
}
docTypeApi = 'DoNewSanadBuySaleSEKEH';
args = SanadBuilders.buySaleSekeh({
moshtariCode: code,
year: dto.year,
month: dto.month,
day: dto.day,
vazn: dto.vazn,
ayar: dto.ayar,
count: dto.count,
nameSekeh: dto.nameSekeh,
buyOrSale: dto.buyOrSale,
mazaneh: dto.mazaneh,
mablaghKol: dto.mablaghKol,
sharh,
factorCode,
jensFelez: dto.jensFelez,
arzName: dto.arzName,
});
break;
}
case 'talab_bedehi': {
if (dto.isBedehiOrTalab == null || dto.mablagh == null) {
throw new BadRequestException(
'برای طلب/بدهی: isBedehiOrTalab و mablagh الزامی است.',
);
}
docTypeApi = 'DoNewSanadTalabBedehi';
args = SanadBuilders.talabBedehi({
moshtariCode: code,
year: dto.year,
month: dto.month,
day: dto.day,
isBedehiOrTalab: dto.isBedehiOrTalab,
mablagh: dto.mablagh,
sharh,
baabat: dto.baabat ?? 'CRM',
vazn: dto.vazn ?? 0,
factorCode,
havalehBeMcode: dto.havalehBeMcode,
jensFelez: dto.jensFelez,
arzName: dto.arzName,
});
break;
}
case 'vkh_gold': {
if (
dto.vazn == null ||
dto.ayar == null ||
dto.isVoroodOrKhorooj == null
) {
throw new BadRequestException(
'برای واریز/خروج طلا: vazn، ayar، isVoroodOrKhorooj الزامی است.',
);
}
docTypeApi = 'DoNewSanadVKHGOLD';
args = SanadBuilders.vkhGold({
moshtariCode: code,
year: dto.year,
month: dto.month,
day: dto.day,
vazn: dto.vazn,
ayar: dto.ayar,
angNumber: dto.angNumber ?? '0',
nameAz: dto.nameAz ?? '',
isVoroodOrKhorooj: dto.isVoroodOrKhorooj,
isAbshode: dto.isAbshode ?? 1,
sharh,
factorCode,
});
break;
}
case 'vkh_bank': {
if (
dto.isDaryaftOrPardakht == null ||
dto.mablagh == null ||
!dto.nameBank
) {
throw new BadRequestException(
'برای واریز/خروج بانکی: isDaryaftOrPardakht، mablagh و nameBank الزامی است.',
);
}
docTypeApi = 'DoNewSanadVKHBank';
args = SanadBuilders.vkhBank({
moshtariCode: code,
year: dto.year,
month: dto.month,
day: dto.day,
isDaryaftOrPardakht: dto.isDaryaftOrPardakht,
mablagh: dto.mablagh,
sharh,
nameBank: dto.nameBank,
codeRahgiri: dto.codeRahgiri ?? '',
factorCode,
});
break;
}
default:
throw new BadRequestException('نوع سند نامعتبر است.');
}
const requestPayload = { [docTypeApi]: args } as Prisma.InputJsonValue;
const log = existing
? await this.prisma.accountingDocumentLog.update({
where: { id: existing.id },
data: {
docType: docTypeApi,
requestPayload,
status: 'pending',
errorMessage: null,
personId,
},
})
: await this.prisma.accountingDocumentLog.create({
data: {
connectionId: dto.connectionId,
personId,
docType: docTypeApi,
factorCode,
requestPayload,
status: 'pending',
},
});
try {
let response: unknown;
switch (dto.docType) {
case 'buy_sale_gold':
response = await api.doNewSanadBuySaleGold(args);
break;
case 'buy_sale_sekeh':
response = await api.doNewSanadBuySaleSekeh(args);
break;
case 'talab_bedehi':
response = await api.doNewSanadTalabBedehi(args);
break;
case 'vkh_gold':
response = await api.doNewSanadVkhGold(args);
break;
case 'vkh_bank':
response = await api.doNewSanadVkhBank(args);
break;
}
const resObj = (response ?? {}) as Record<string, unknown>;
const okVal = resObj.OK ?? resObj.Factor_Code;
if (okVal === undefined || okVal === null || okVal === '') {
throw new BadRequestException(
'ته‌حساب پاسخ موفق معتبر برنگرداند: ' + JSON.stringify(response),
);
}
const updated = await this.prisma.accountingDocumentLog.update({
where: { id: log.id },
data: {
status: 'success',
responsePayload: response as Prisma.InputJsonValue,
tahesabOk: String(okVal),
tahesabFactorId:
typeof resObj.ID === 'number' ? resObj.ID : undefined,
errorMessage: null,
},
});
return {
idempotent: false,
log: this.logDto(updated),
response,
};
} catch (err) {
const message = (err as Error).message ?? 'خطای ناشناخته';
await this.prisma.accountingDocumentLog.update({
where: { id: log.id },
data: {
status: 'failed',
errorMessage: message.slice(0, 1000),
},
});
throw err;
}
}
private logDto(log: {
id: string;
connectionId: string;
personId: string | null;
docType: string;
factorCode: string;
status: string;
tahesabOk: string | null;
tahesabFactorId: number | null;
errorMessage: string | null;
createdAt: Date;
updatedAt: Date;
}) {
return {
id: log.id,
connectionId: log.connectionId,
personId: log.personId,
docType: log.docType,
factorCode: log.factorCode,
status: log.status,
tahesabOk: log.tahesabOk,
tahesabFactorId: log.tahesabFactorId,
errorMessage: log.errorMessage,
createdAt: log.createdAt.toISOString(),
updatedAt: log.updatedAt.toISOString(),
};
}
}