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

455 lines
14 KiB
TypeScript
Raw Normal View History

import { SharedSaleInvoiceCreateService } from '@/common/services/saleInvoices/sale-invoice-create.service'
import { PrismaService } from '@/prisma/prisma.service'
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'
import {
CustomerType,
TspProviderRequestType,
TspProviderResponseStatus,
} from 'generated/prisma/client'
import {
TspProviderActionResponseDto,
TspProviderCorrectionInvoicePayloadDto,
} from './dto/provider-switch.dto'
import { SalesInvoiceTspSwitchService } from './sales-invoice-tsp-switch.service'
import {
buildCorrectionPayload,
buildPayload,
buildRevokePayload,
getOriginalResendAttemptNumber,
onResult,
trySend,
} from './utils/sales-invoice-tsp.utils'
type ItemTspRow = {
tsp_provider: string | null
tax_id: string | null
}
@Injectable()
export class SalesInvoiceTspService {
constructor(
private readonly prisma: PrismaService,
private readonly tspSwitchService: SalesInvoiceTspSwitchService,
private sharedSaleInvoiceCreateService: SharedSaleInvoiceCreateService,
) {}
async originalSend(
posId: string,
invoice_id: string,
): Promise<TspProviderActionResponseDto> {
const payload = await buildPayload(this.prisma, invoice_id, posId)
2026-05-07 20:30:24 +03:30
const attemptNumber = await getOriginalResendAttemptNumber(this.prisma, invoice_id)
2026-05-07 20:30:24 +03:30
const attempt = await this.prisma.saleInvoiceTspAttempts.create({
data: {
2026-05-07 20:30:24 +03:30
attempt_no: attemptNumber,
invoice_id,
status: TspProviderResponseStatus.QUEUED,
raw_request_payload: JSON.parse(JSON.stringify(payload)),
message: 'در حال ارسال به سامانه مالیاتی...',
},
})
const result = await trySend(this.tspSwitchService, payload)
return await onResult(this.prisma, result, attempt.id)
}
async sendBulk(consumer_id: string, invoice_ids: string[]): Promise<void> {
// if (!invoice_ids.length) return
// const payloads: TspProviderOriginalSendPayloadDto[] = []
// for (const invoiceId of invoice_ids) {
// const posId = await this.getPosId(consumer_id, invoiceId)
// payloads.push(await this.buildPayload(invoiceId, posId))
// }
// const bulkResult = await this.tspSwitchService.sendBulk(payloads)
// const allItemResults = bulkResult.flatMap(result => result.items)
// await this.persistAttemptResults(allItemResults)
}
async get(
invoice_id: string,
pos_id: string,
consumer_id: string,
): Promise<TspProviderActionResponseDto> {
const [attempt, pos] = await this.prisma.$transaction(async tx => [
await tx.saleInvoiceTspAttempts.findFirst({
where: {
invoice_id,
invoice: {
pos: {
id: pos_id,
},
},
},
orderBy: {
attempt_no: 'desc',
},
}),
await tx.pos.findUnique({
where: {
id: pos_id,
complex: {
business_activity: {
consumer_id,
},
},
},
select: {
complex: {
select: {
business_activity: {
select: {
partner_token: true,
consumer: {
select: {
legal: {
select: {
partner: {
select: {
tsp_provider: true,
},
},
},
},
individual: {
select: {
partner: {
select: {
tsp_provider: true,
},
},
},
},
},
},
},
},
},
},
},
}),
])
if (!attempt) {
throw new NotFoundException('صورت‌حساب ارسالی فاکتور شما به سامانه یافت نشد.')
}
if (!pos) {
throw new NotFoundException('مشکلی در ساختار اطلاعات ورودی شما وجود دارد.')
}
const { business_activity } = pos.complex
const { partner } = (business_activity.consumer.individual ||
business_activity.consumer.legal)!
const result = await this.tspSwitchService.get(
partner.tsp_provider,
invoice_id,
business_activity.partner_token,
)
console.log('getResult', result)
if (!result) {
throw new NotFoundException('نتیجه ارسال فاکتور یافت نشد.')
}
const updatedAttempt = await this.prisma.saleInvoiceTspAttempts.update({
where: {
id: attempt.id,
},
data: {
provider_response_payload: JSON.parse(JSON.stringify(result)),
status: result.status,
received_at: result.received_at,
},
select: {
status: true,
invoice: true,
message: true,
},
})
return {
invoice: updatedAttempt.invoice,
status: updatedAttempt.status,
message: updatedAttempt.message,
}
}
async correctionSend(
consumerAccountId: string,
posId: string,
complexId: string,
businessId: string,
ref_invoice_id: string,
dataToUpdate: TspProviderCorrectionInvoicePayloadDto,
): Promise<TspProviderActionResponseDto> {
const [newInvoice, attempt, correctionPayload] = await this.prisma.$transaction(
async tx => {
const relatedInvoice = await tx.salesInvoice.findUnique({
where: {
id: ref_invoice_id,
},
include: {
customer: {
select: {
type: true,
},
},
tsp_attempts: {
orderBy: {
attempt_no: 'desc',
},
take: 1,
},
},
})
if (!relatedInvoice) {
throw new NotFoundException('فاکتور مورد نظر یافت نشد.')
}
2026-05-04 11:21:49 +03:30
if (relatedInvoice.type === TspProviderRequestType.REVOKE) {
throw new BadRequestException(
'فاکتور ارسالی قبلا ابطال شده است و امکان ویرایش آن وجود ندارد.',
)
}
if (!relatedInvoice.tax_id) {
throw new BadRequestException(
'فاکتور قبلی همچنان در حال بررسی است و امکان ویرایش آن وجود ندارد.',
)
}
2026-05-04 11:21:49 +03:30
const newInvoice = await this.sharedSaleInvoiceCreateService.create({
data: {
customer_type: relatedInvoice.customer?.type || CustomerType.UNKNOWN,
invoice_date: new Date(),
payments: dataToUpdate.payments,
items: dataToUpdate.items,
total_amount: dataToUpdate.total_amount,
customer_id: relatedInvoice.customer_id || undefined,
},
businessId,
complexId,
posId,
consumerAccountId,
main_invoice_id: relatedInvoice.main_id || relatedInvoice.id,
ref_invoice_id: relatedInvoice.id,
type: TspProviderRequestType.CORRECTION,
})
2026-05-04 11:21:49 +03:30
const correctionPayload = await buildCorrectionPayload(tx, newInvoice.id)
2026-05-04 11:21:49 +03:30
const attempt = await tx.saleInvoiceTspAttempts.create({
data: {
attempt_no: 1,
invoice_id: newInvoice.id,
message: 'در حال ارسال به سامانه مالیاتی...',
status: TspProviderResponseStatus.QUEUED,
sent_at: new Date().toISOString(),
raw_request_payload: JSON.parse(JSON.stringify(correctionPayload)),
},
})
return [newInvoice, attempt, correctionPayload]
},
)
const result = await trySend(this.tspSwitchService, correctionPayload)
console.log('sendResult')
console.log(result)
return onResult(this.prisma, result, attempt.id)
// const countByGoodId = (goodIds: string[]): Map<string, number> => {
// const counts = new Map<string, number>()
// for (const goodId of goodIds) {
// counts.set(goodId, (counts.get(goodId) ?? 0) + 1)
// }
// return counts
// }
// const updatedGoodIds = dataToUpdate.items.map(item => item.good_id!)
// const previousGoodIds = lastAttempt.invoice.items.map(item => item.good_id)
// const updatedCounts = countByGoodId(updatedGoodIds)
// const previousCounts = countByGoodId(previousGoodIds)
// let isBackFromSale = false
// if (updatedCounts.size !== previousCounts.size) {
// isBackFromSale = true
// } else {
// for (const [goodId, count] of updatedCounts) {
// if (previousCounts.get(goodId) !== count) {
// isBackFromSale = true
// break
// }
// }
// }
}
2026-05-04 11:21:49 +03:30
async revoke(
consumerAccountId: string,
posId: string,
complexId: string,
businessId: string,
ref_invoice_id: string,
): Promise<TspProviderActionResponseDto> {
const [newInvoice, attempt, revokePayload] = await this.prisma.$transaction(
async tx => {
const relatedInvoice = await tx.salesInvoice.findUnique({
where: {
id: ref_invoice_id,
2026-05-04 11:21:49 +03:30
},
include: {
customer: {
select: {
type: true,
},
},
tsp_attempts: {
orderBy: {
attempt_no: 'desc',
},
take: 1,
},
payments: {
include: {
terminal_info: true,
},
},
items: true,
},
})
2026-05-04 11:21:49 +03:30
if (!relatedInvoice) {
throw new NotFoundException('فاکتور مورد نظر یافت نشد.')
}
2026-05-04 11:21:49 +03:30
if (relatedInvoice.type === TspProviderRequestType.REVOKE) {
throw new BadRequestException(
'فاکتور ارسالی قبلا ابطال شده است و امکان ویرایش آن وجود ندارد.',
)
}
2026-05-04 11:21:49 +03:30
if (!relatedInvoice.tax_id) {
throw new BadRequestException(
'فاکتور قبلی همچنان در حال بررسی است و امکان ویرایش آن وجود ندارد.',
)
}
2026-05-04 11:21:49 +03:30
const payments = relatedInvoice.payments.reduce(
(acc, payment) => {
const amount = Number(payment.amount)
switch (payment.payment_method) {
case 'CASH':
acc.cash = (acc.cash || 0) + amount
break
case 'SET_OFF':
acc.set_off = (acc.set_off || 0) + amount
break
case 'CARD':
acc.card = (acc.card || 0) + amount
break
case 'BANK':
acc.bank = (acc.bank || 0) + amount
break
case 'CHEQUE':
acc.check = (acc.check || 0) + amount
break
case 'OTHER':
acc.other = (acc.other || 0) + amount
break
case 'TERMINAL':
acc.terminals = payment.terminal_info
? {
amount,
terminalId: payment.terminal_info.terminal_id,
stan: payment.terminal_info.stan,
rrn: payment.terminal_info.rrn,
customer_card_no:
payment.terminal_info.customer_card_no || undefined,
transaction_date_time: payment.terminal_info.transaction_date_time,
description: payment.terminal_info.description || undefined,
}
: acc.terminals
break
default:
break
}
return acc
},
{} as {
terminals?: {
amount?: number
terminalId: string
stan: string
rrn: string
customer_card_no?: string
transaction_date_time: Date
description?: string
}
cash?: number
set_off?: number
card?: number
bank?: number
check?: number
other?: number
},
)
const newInvoice = await this.sharedSaleInvoiceCreateService.create({
data: {
customer_type: relatedInvoice.customer?.type || CustomerType.UNKNOWN,
invoice_date: new Date(),
payments,
items: relatedInvoice.items.map(item => ({
unit_price: Number(item.unit_price),
quantity: Number(item.quantity),
total_amount: Number(item.total_amount),
discount_amount: Number(item.discount || 0),
invoice_id: '',
good_id: item.good_id,
service_id: item.service_id || undefined,
payload: item.payload as any,
notes: item.notes || '',
})),
total_amount: Number(relatedInvoice.total_amount),
customer_id: relatedInvoice.customer_id || undefined,
},
businessId,
complexId,
posId,
consumerAccountId,
main_invoice_id: relatedInvoice.main_id || relatedInvoice.id,
ref_invoice_id: relatedInvoice.id,
2026-05-04 11:21:49 +03:30
type: TspProviderRequestType.REVOKE,
})
2026-05-04 11:21:49 +03:30
const payload = await buildRevokePayload(this.prisma, newInvoice.id, posId)
const attempt = await tx.saleInvoiceTspAttempts.create({
data: {
attempt_no: 1,
invoice_id: newInvoice.id,
message: 'در حال ارسال به سامانه مالیاتی...',
status: TspProviderResponseStatus.QUEUED,
raw_request_payload: JSON.parse(JSON.stringify(payload)),
},
})
return [newInvoice, attempt, payload]
},
)
const result = await this.tspSwitchService.revoke(revokePayload)
return await onResult(this.prisma, result, attempt.id)
}
}