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

550 lines
16 KiB
TypeScript
Raw Normal View History

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 {
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 existingAttempt = await prisma.saleInvoiceTspAttempts.findFirst({
where: {
invoice_id,
},
include: {
invoice: {
select: {
type: true,
},
},
},
orderBy: {
created_at: 'desc',
},
})
if (existingAttempt) {
attemptNumber = existingAttempt.attempt_no + 1
if (existingAttempt.invoice.type !== TspProviderRequestType.ORIGINAL) {
throw new BadRequestException(
'فقط فاکتورهای اصلی قابل ارسال مجدد به سامانه مالیاتی هستند.',
)
}
if (existingAttempt.status === TspProviderResponseStatus.SUCCESS) {
throw new BadRequestException(
'فاکتور تایید شده از طرف سازمان مالیاتی قابل ارسال مجدد نیست.',
)
}
if (
existingAttempt.status === TspProviderResponseStatus.QUEUED ||
existingAttempt.status === TspProviderResponseStatus.FISCAL_QUEUED
) {
throw new BadRequestException(
'در حال حاضر فاکتور شما در حال بررسی توسط سازمان مالیاتی است.',
)
}
}
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,
tax_id: 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,
},
},
reference_invoice: {
select: {
tax_id: true,
},
},
payments: {
include: {
terminal_info: 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: String((item.payload as Record<string, any>)?.discount || '0'),
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),
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: new Date(),
settlement_type: invoice.settlement_type,
customer:
invoice.customer && invoice.customer.type !== 'UNKNOWN'
? {
type: invoice.customer.type,
legal_info: invoice.customer.legal ?? undefined,
individual_info: invoice.customer.individual ?? undefined,
}
: {
type: 'UNKNOWN',
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,
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: {
select: {
first_name: true,
last_name: true,
national_id: true,
mobile_number: true,
},
},
legal: {
select: {
name: true,
economic_code: true,
},
},
},
},
payments: {
select: {
amount: true,
payment_method: true,
paid_at: true,
terminal_info: {
select: {
stan: true,
rrn: true,
},
},
},
},
},
})
if (!invoice) {
throw new NotFoundException('فاکتور مورد نظر یافت نشد.')
}
const { pos, id, invoice_number, invoice_date, total_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),
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 !== 'UNKNOWN'
? {
type: invoice.customer.type,
legal_info: invoice.customer.legal ?? undefined,
individual_info: invoice.customer.individual ?? undefined,
}
: {
type: 'UNKNOWN',
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),
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,
payload: item.payload,
good_snapshot: item.good_snapshot,
})),
}
}
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,
attempt_id: string,
): Promise<TspProviderActionResponseDto> {
let attemptUpdatedData: SaleInvoiceTspAttemptsUpdateInput = {}
2026-05-27 22:44:01 +03:30
console.log('attempt', result, attempt_id)
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,
provider_response: JSON.parse(JSON.stringify(result.provider_response)),
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,
},
},
}
if (result.hasError) {
2026-05-27 22:44:01 +03:30
attemptUpdatedData.error_message = result.error_message
attemptUpdatedData.validation_errors = JSON.parse(
JSON.stringify(result.validation_errors),
)
attemptUpdatedData.fiscal_warnings = JSON.parse(
JSON.stringify(result.fiscal_warnings),
)
}
} else {
attemptUpdatedData = {
status: TspProviderResponseStatus.SEND_FAILURE,
message:
'متاسفانه امکان ارسال فاکتور به سیستم مالیاتی در حال حاضر وجود ندارد. لطفا بعدا تلاش کنید.',
received_at: new Date().toISOString(),
}
}
const updatedAttempt = await prisma.saleInvoiceTspAttempts.update({
where: {
id: attempt_id,
},
data: attemptUpdatedData,
select: {
status: true,
invoice: true,
message: true,
},
})
return {
invoice: updatedAttempt.invoice,
status: updatedAttempt.status,
message: updatedAttempt.message,
}
}