408 lines
12 KiB
TypeScript
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(),
|
|
};
|
|
}
|
|
}
|