Files
psp_api/src/modules/tspProviders/utils/sales-invoice-tsp.utils.ts
T

675 lines
20 KiB
TypeScript
Raw Normal View History

import { CustomerType } from '@/generated/prisma/enums'
import { SaleInvoiceTspAttemptsUpdateInput } from '@/generated/prisma/models'
import { PrismaService } from '@/prisma/prisma.service'
import { BadRequestException, NotFoundException } from '@nestjs/common'
import {
Prisma,
TspProviderRequestType,
TspProviderResponseStatus,
} from 'generated/prisma/client'
import {
goldTypePayload,
TspProviderCorrectionSendPayloadDto,
2026-05-27 22:44:01 +03:30
TspProviderGetResponseDto,
TspProviderOriginalResponseDto,
TspProviderOriginalSendPayloadDto,
TspProviderRevokePayloadDto,
} from '../dto'
import { TspProviderActionResponseDto } from '../dto/provider-switch.dto'
export async function getOriginalResendAttemptNumber(
prisma: PrismaService,
invoice_id: string,
): Promise<number> {
let attemptNumber = 1
const invoice = await prisma.salesInvoice.findFirst({
where: {
id: invoice_id,
},
select: {
type: true,
last_attempt_no: true,
last_tsp_status: true,
},
orderBy: {
created_at: 'desc',
},
})
if (invoice) {
const { last_attempt_no, type, last_tsp_status } = invoice
attemptNumber = (last_attempt_no ?? 0) + 1
if (type !== TspProviderRequestType.ORIGINAL) {
throw new BadRequestException(
'فقط ‌صورت‌حساب‌های اصلی قابل ارسال مجدد به سامانه مالیاتی هستند.',
)
}
if (last_tsp_status === TspProviderResponseStatus.SUCCESS) {
throw new BadRequestException(
'صورت‌حساب تایید شده از طرف سازمان مالیاتی قابل ارسال مجدد نیست.',
)
}
if (last_tsp_status === TspProviderResponseStatus.QUEUED) {
throw new BadRequestException(
'در حال حاضر صورت‌حساب شما در صف ارسال به سازمان مالیاتی است.',
)
}
if (last_tsp_status === TspProviderResponseStatus.FISCAL_QUEUED) {
throw new BadRequestException(
'در حال حاضر صورت‌حساب شما در حال بررسی توسط سازمان مالیاتی است.',
)
}
} else throw new NotFoundException('صورت‌حساب مورد نظر یافت نشد.')
return attemptNumber
}
export async function buildRevokePayload(
prisma: PrismaService,
invoiceId: string,
posId: string,
): Promise<TspProviderRevokePayloadDto> {
const invoice = await prisma.salesInvoice.findUnique({
where: {
id: invoiceId,
pos_id: posId,
},
select: {
id: true,
code: true,
total_amount: true,
invoice_date: true,
invoice_number: true,
tax_id: true,
tsp_attempts: {
orderBy: {
created_at: 'asc',
},
take: 1,
select: {
id: true,
status: true,
},
},
pos: {
select: {
complex: {
select: {
business_activity: {
select: {
fiscal_id: true,
economic_code: true,
partner_token: true,
guild: {
select: {
invoice_template: true,
},
},
consumer: {
select: {
legal: {
select: {
partner: {
select: {
tsp_provider: true,
},
},
},
},
individual: {
select: {
partner: {
select: {
tsp_provider: true,
},
},
},
},
},
},
},
},
},
},
},
},
},
})
if (!invoice) {
throw new NotFoundException('صورت‌حساب مورد نظر یافت نشد.')
}
const { partner } = (invoice.pos.complex.business_activity.consumer.legal ||
invoice.pos.complex.business_activity.consumer.individual)!
return {
invoice_id: invoice.id,
invoice_number: invoice.invoice_number,
economic_code: invoice.pos.complex.business_activity.economic_code,
fiscal_id: invoice.pos.complex.business_activity.fiscal_id,
tsp_token: invoice.pos.complex.business_activity.partner_token,
tsp_provider: partner.tsp_provider!,
last_tax_id: invoice.tax_id!,
}
}
export async function buildCorrectionPayload(
tx: Prisma.TransactionClient,
invoice_id: string,
): Promise<TspProviderCorrectionSendPayloadDto> {
const invoice = await tx.salesInvoice.findUnique({
where: {
id: invoice_id,
},
select: {
id: true,
code: true,
total_amount: true,
invoice_date: true,
invoice_number: true,
settlement_type: true,
discount_amount: true,
tax_amount: true,
items: true,
pos: {
select: {
complex: {
select: {
business_activity: {
select: {
fiscal_id: true,
economic_code: true,
partner_token: true,
guild: {
select: {
invoice_template: true,
},
},
consumer: {
select: {
legal: {
select: {
partner: {
select: {
tsp_provider: true,
},
},
},
},
individual: {
select: {
partner: {
select: {
tsp_provider: true,
},
},
},
},
},
},
},
},
},
},
},
},
unknown_customer: true,
customer: {
select: {
type: true,
individual: true,
legal: true,
},
},
payments: {
include: {
terminal_info: true,
},
},
reference_invoice: {
select: {
tax_id: true,
id: true,
},
},
},
})
if (!invoice) {
throw Error()
}
const { partner } = (invoice.pos.complex.business_activity.consumer.legal ||
invoice.pos.complex.business_activity.consumer.individual)!
const unknown_customer = (invoice.unknown_customer || {}) as Record<string, string>
return {
items: invoice.items.map(item => ({
invoice_item_id: item.id,
quantity: Number(item.quantity),
unit_price: Number(item.unit_price),
total_amount: Number(item.total_amount),
measure_unit: item.measure_unit_code,
sku: item.sku_code,
sku_vat: String(item.sku_vat),
discount_amount: Number(item.discount_amount),
tax_amount: Number(item.tax_amount),
good_id: item.good_id,
service_id: item.service_id,
payload: item.payload,
good_snapshot: item.good_snapshot,
})),
payments: invoice.payments.map(payment => ({
amount: Number(payment.amount),
payment_method: payment.payment_method,
paid_at: payment.paid_at,
terminal_info: {
card_number: payment.terminal_info ? payment.terminal_info.stan : undefined,
tracking_code: payment.terminal_info ? payment.terminal_info.rrn : undefined,
},
})),
total_amount: Number(invoice.total_amount),
discount_amount: Number(invoice.discount_amount),
tax_amount: Number(invoice.tax_amount),
last_tax_id: invoice.reference_invoice!.tax_id!,
invoice_number: invoice.invoice_number,
id: invoice.id,
economic_code: invoice.pos.complex.business_activity.economic_code,
fiscal_id: invoice.pos.complex.business_activity.fiscal_id,
token: invoice.pos.complex.business_activity.partner_token,
tsp_provider: partner.tsp_provider!,
template: invoice.pos.complex.business_activity.guild.invoice_template,
invoice_date: invoice?.invoice_date
? new Date(invoice.invoice_date)
: invoice.invoice_date,
settlement_type: invoice.settlement_type,
customer:
invoice.customer && invoice.customer.type !== 'UNKNOWN'
? {
2026-05-30 19:05:56 +03:30
type: invoice.customer.type,
legal_info: invoice.customer.legal
? {
name: invoice.customer.legal.name ?? undefined,
registration_number:
invoice.customer.legal.registration_number ?? undefined,
postal_code: invoice.customer.legal.postal_code ?? undefined,
economic_code: invoice.customer.legal.economic_code ?? undefined,
}
: undefined,
individual_info: invoice.customer.individual
? {
first_name: invoice.customer.individual.first_name ?? undefined,
last_name: invoice.customer.individual.last_name ?? undefined,
national_id: invoice.customer.individual.national_id ?? undefined,
mobile_number: invoice.customer.individual.mobile_number ?? undefined,
postal_code: invoice.customer.individual.postal_code ?? undefined,
economic_code: invoice.customer.individual.economic_code ?? undefined,
}
: undefined,
2026-05-30 19:05:56 +03:30
}
: {
type: CustomerType.UNKNOWN,
2026-05-30 19:05:56 +03:30
unknown_info: {
name: unknown_customer?.name || '',
economic_code: unknown_customer?.economic_code || '',
},
},
}
}
export async function buildOriginalPayload(
prisma: PrismaService,
invoiceId: string,
posId: string,
): Promise<TspProviderOriginalSendPayloadDto> {
const invoice = await prisma.salesInvoice.findUnique({
where: {
id: invoiceId,
pos_id: posId,
},
select: {
id: true,
code: true,
total_amount: true,
invoice_date: true,
invoice_number: true,
settlement_type: true,
discount_amount: true,
tax_amount: true,
items: {
select: {
id: true,
quantity: true,
unit_price: true,
total_amount: true,
measure_unit_code: true,
measure_unit_text: true,
sku_code: true,
sku_vat: true,
good_id: true,
service_id: true,
discount_amount: true,
tax_amount: true,
payload: true,
good_snapshot: true,
},
},
pos: {
select: {
complex: {
select: {
business_activity: {
select: {
fiscal_id: true,
economic_code: true,
partner_token: true,
guild: {
select: {
invoice_template: true,
},
},
consumer: {
select: {
legal: {
select: {
partner: {
select: {
tsp_provider: true,
},
},
},
},
individual: {
select: {
partner: {
select: {
tsp_provider: true,
},
},
},
},
},
},
},
},
},
},
},
},
unknown_customer: true,
customer: {
select: {
type: true,
individual: true,
legal: true,
},
},
payments: {
select: {
amount: true,
payment_method: true,
paid_at: true,
terminal_info: {
select: {
stan: true,
rrn: true,
},
},
},
},
},
})
if (!invoice) {
throw new NotFoundException('صورت‌حساب مورد نظر یافت نشد.')
}
2026-05-30 19:05:56 +03:30
const {
pos,
id,
invoice_number,
invoice_date,
total_amount,
discount_amount,
tax_amount,
settlement_type,
} = invoice
const { business_activity: ba } = pos.complex
const unknown_customer = (invoice.unknown_customer || {}) as Record<string, string>
const { partner } = (ba.consumer.legal || ba.consumer.individual)!
return {
id,
invoice_number,
invoice_date,
settlement_type,
total_amount: Number(total_amount),
tax_amount: Number(tax_amount),
discount_amount: Number(discount_amount),
economic_code: ba.economic_code,
fiscal_id: ba.fiscal_id,
template: ba.guild.invoice_template,
token: ba.partner_token,
tsp_provider: partner.tsp_provider!,
payments: invoice.payments.map(payment => ({
amount: Number(payment.amount),
payment_method: payment.payment_method,
paid_at: payment.paid_at,
terminal_info: {
card_number: payment.terminal_info ? payment.terminal_info.stan : undefined,
tracking_code: payment.terminal_info ? payment.terminal_info.rrn : undefined,
},
})),
customer:
invoice.customer && invoice.customer.type !== CustomerType.UNKNOWN
? {
2026-05-30 19:05:56 +03:30
type: invoice.customer.type,
legal_info: invoice.customer.legal
? {
name: invoice.customer.legal.name ?? undefined,
registration_number:
invoice.customer.legal.registration_number ?? undefined,
postal_code: invoice.customer.legal.postal_code ?? undefined,
economic_code: invoice.customer.legal.economic_code ?? undefined,
}
: undefined,
individual_info: invoice.customer.individual
? {
first_name: invoice.customer.individual.first_name ?? undefined,
last_name: invoice.customer.individual.last_name ?? undefined,
national_id: invoice.customer.individual.national_id ?? undefined,
mobile_number: invoice.customer.individual.mobile_number ?? undefined,
postal_code: invoice.customer.individual.postal_code ?? undefined,
economic_code: invoice.customer.individual.economic_code ?? undefined,
}
: undefined,
2026-05-30 19:05:56 +03:30
}
: {
type: CustomerType.UNKNOWN,
2026-05-30 19:05:56 +03:30
unknown_info: {
name: unknown_customer?.name || '',
economic_code: unknown_customer?.economic_code || '',
},
},
items: invoice.items.map(item => ({
invoice_item_id: item.id,
quantity: Number(item.quantity),
unit_price: Number(item.unit_price),
total_amount: Number(item.total_amount),
tax_amount: Number(tax_amount),
discount_amount: Number(discount_amount),
measure_unit: item.measure_unit_code,
sku: item.sku_code,
sku_vat: String(item.sku_vat),
discount: String((item.payload as Record<string, any>)?.discount || '0'),
good_id: item.good_id,
service_id: item.service_id,
good_snapshot: item.good_snapshot,
2026-05-30 19:05:56 +03:30
gold_type_payload: isGoldTypePayload(item.payload) ? item.payload : undefined,
})),
}
}
export async function getRelatedInvoiceForCorrection(
tx: Prisma.TransactionClient,
ref_invoice_id: string,
) {
const relatedInvoice = await getRelatedInvoiceForModification(tx, ref_invoice_id)
return relatedInvoice
}
export async function getRelatedInvoiceForModification(
tx: Prisma.TransactionClient,
ref_invoice_id: string,
) {
const relatedInvoice = await tx.salesInvoice.findUnique({
where: {
id: ref_invoice_id,
},
include: {
customer: {
select: {
type: true,
},
},
payments: {
select: {},
},
tsp_attempts: {
orderBy: {
attempt_no: 'desc',
},
take: 1,
},
},
})
if (!relatedInvoice) {
throw new NotFoundException('صورت‌حساب مورد نظر یافت نشد.')
}
if (relatedInvoice.type === TspProviderRequestType.REVOKE) {
throw new BadRequestException(
'صورت‌حساب ارسالی قبلا ابطال شده است و امکان ویرایش آن وجود ندارد.',
)
}
if (
!relatedInvoice.tax_id ||
relatedInvoice.tsp_attempts?.[0].status !== TspProviderResponseStatus.SUCCESS
) {
throw new BadRequestException(
'صورت‌حساب قبلی همچنان در حال بررسی است و امکان اصلاح آن وجود ندارد.',
)
}
return relatedInvoice
}
export async function trySend(
tspSwitchService: {
send(
payload: TspProviderOriginalSendPayloadDto,
2026-05-27 22:44:01 +03:30
): Promise<TspProviderOriginalResponseDto>
},
payload: TspProviderOriginalSendPayloadDto,
2026-05-27 22:44:01 +03:30
): Promise<TspProviderOriginalResponseDto> {
return (await tspSwitchService.send(payload)) || null
}
export async function onResult(
prisma: PrismaService,
2026-05-27 22:44:01 +03:30
result: TspProviderOriginalResponseDto | TspProviderGetResponseDto,
invoice_id: string,
): Promise<TspProviderActionResponseDto> {
let attemptUpdatedData: SaleInvoiceTspAttemptsUpdateInput = {}
console.log('attempt', result)
2026-05-27 22:44:01 +03:30
const resultMessage = result.message
if (result) {
attemptUpdatedData = {
2026-05-27 22:44:01 +03:30
provider_request_payload: result['provider_request_payload']
? JSON.parse(JSON.stringify(result['provider_request_payload']))
: undefined,
status: result.status,
received_at: result.received_at || new Date().toISOString(),
message: resultMessage
? resultMessage
: result.hasError
? 'وجود مشکل در ارسال به سامانه مالیاتی'
: 'صورت‌حساب با موفقیت به سامانه مالیاتی ارسال شد.',
invoice: {
update: {
2026-05-27 22:44:01 +03:30
tax_id: result['tax_id'] ? result['tax_id'] : undefined,
},
},
2026-05-30 19:05:56 +03:30
error_message: result.error_message || undefined,
}
if (result.provider_response) {
attemptUpdatedData.provider_response = JSON.parse(
JSON.stringify(result.provider_response),
)
}
2026-05-30 19:05:56 +03:30
if (result.validation_errors) {
2026-05-27 22:44:01 +03:30
attemptUpdatedData.validation_errors = JSON.parse(
JSON.stringify(result.validation_errors),
)
2026-05-30 19:05:56 +03:30
}
if (result.fiscal_warnings) {
2026-05-27 22:44:01 +03:30
attemptUpdatedData.fiscal_warnings = JSON.parse(
JSON.stringify(result.fiscal_warnings),
)
}
} else {
attemptUpdatedData = {
status: TspProviderResponseStatus.SEND_FAILURE,
message:
'متاسفانه امکان ارسال صورت‌حساب به سیستم مالیاتی در حال حاضر وجود ندارد. لطفا بعدا تلاش کنید.',
received_at: new Date().toISOString(),
}
}
const invoice = await prisma.$transaction(async tx => {
const lastAttempt = await tx.saleInvoiceTspAttempts.findFirst({
where: { invoice_id },
orderBy: { attempt_no: 'desc' },
select: { id: true },
})
if (!lastAttempt) throw new NotFoundException('صورت‌حساب مورد نظر یافت نشد.')
await tx.saleInvoiceTspAttempts.update({
where: { id: lastAttempt.id },
data: attemptUpdatedData,
})
const updatedInvoice = await tx.salesInvoice.update({
where: { id: invoice_id },
data: {
last_tsp_status: attemptUpdatedData.status,
},
})
return updatedInvoice
})
return {
invoice,
status: attemptUpdatedData.status as TspProviderResponseStatus,
message: attemptUpdatedData.message as string,
}
}
function isGoldTypePayload(value: unknown): value is goldTypePayload {
2026-05-30 19:05:56 +03:30
return typeof value === 'object' && value !== null && !Array.isArray(value)
}