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> { if (!raw) return []; if (Array.isArray(raw)) return raw as Array>; if (typeof raw === 'object') { const obj = raw as Record; 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 >; } } return Object.values(obj).filter( (v) => v && typeof v === 'object' && !Array.isArray(v), ) as Array>; } 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; 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(), }; } }