feat: implement SalesInvoiceTspSwitchService and SalesInvoiceTspService for handling TSP provider interactions

- Add SalesInvoiceTspSwitchService to manage TSP provider selection and sending invoices.
- Introduce SalesInvoiceTspService for creating, sending, and retrieving sales invoices.
- Implement NamaProviderSwitchAdapter for communication with the NAMA TSP provider API.
- Define DTOs for request and response structures specific to the NAMA provider.
- Enhance error handling and logging for TSP provider interactions.
This commit is contained in:
2026-05-03 16:23:17 +03:30
parent ad470d2166
commit a486127ade
62 changed files with 4136 additions and 5015 deletions
@@ -1,16 +1,19 @@
import { IPosPayload } from '@/common/models/posPayload.model'
import { translateEnumValue } from '@/common/utils'
import { SalesInvoiceCreateInput } from '@/generated/prisma/models'
import { PrismaService } from '@/prisma/prisma.service'
import { BadRequestException, Injectable } from '@nestjs/common'
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'
import { ResponseMapper } from 'common/response/response-mapper'
import type { CustomerIndividual, CustomerLegal } from 'generated/prisma/client'
import { Prisma } from 'generated/prisma/client'
import {
ConsumerRole,
CustomerType,
FiscalResponseStatus,
PaymentMethodType,
TspProviderRequestType,
TspProviderResponseStatus,
} from 'generated/prisma/enums'
import { SalesInvoiceFiscalService } from '../../consumer/saleInvoices/fiscal/sales-invoice-fiscal.service'
import { SalesInvoiceTspService } from '../../tspProviders/sales-invoice-tsp.service'
import { CreateSalesInvoiceDto } from './dto/create-sales-invoice.dto'
import { SalesInvoicesFilterDto } from './dto/sales-invoices-filter.dto'
@@ -53,7 +56,7 @@ export class SalesInvoicesService {
constructor(
private prisma: PrismaService,
private salesInvoiceTaxService: SalesInvoiceFiscalService,
private salesInvoiceTaxService: SalesInvoiceTspService,
) {}
async findAll(posInfo: IPosPayload, query: SalesInvoicesFilterDto = {}) {
@@ -96,17 +99,13 @@ export class SalesInvoicesService {
},
},
},
fiscal: {
tsp_attempts: {
orderBy: {
created_at: 'desc',
},
take: 1,
select: {
attempts: {
orderBy: {
created_at: 'desc',
},
take: 1,
select: {
status: true,
},
},
status: true,
},
},
},
@@ -117,8 +116,8 @@ export class SalesInvoicesService {
const summaryItems = items.map(invoice => ({
...invoice,
status: translateEnumValue(
'FiscalResponseStatus',
invoice.fiscal?.attempts?.[0]?.status || FiscalResponseStatus.NOT_SEND,
'TspProviderResponseStatus',
invoice.tsp_attempts?.[0]?.status || TspProviderResponseStatus.NOT_SEND,
),
}))
@@ -126,10 +125,11 @@ export class SalesInvoicesService {
}
async findOne(posInfo: IPosPayload, id: string) {
const invoice = await this.prisma.salesInvoice.findFirstOrThrow({
const invoice = await this.prisma.salesInvoice.findUnique({
where: {
id,
pos: {
id: posInfo.pos_id,
complex: {
business_activity_id: posInfo.business_id,
},
@@ -234,45 +234,50 @@ export class SalesInvoicesService {
},
},
},
fiscal: {
tsp_attempts: {
orderBy: {
created_at: 'desc',
},
select: {
id: true,
retry_count: true,
last_attempt_at: true,
attempts: {
orderBy: {
created_at: 'desc',
},
select: {
id: true,
attempt_no: true,
status: true,
tax_id: true,
error_message: true,
sent_at: true,
received_at: true,
created_at: true,
},
},
attempt_no: true,
status: true,
tax_id: true,
error_message: true,
sent_at: true,
received_at: true,
created_at: true,
},
take: 1,
},
},
})
return ResponseMapper.single({
...invoice,
status: invoice.fiscal?.attempts?.[0]?.status || FiscalResponseStatus.NOT_SEND,
})
if (invoice) {
const { tsp_attempts, ...rest } = invoice || {}
return ResponseMapper.single({
...rest,
status: tsp_attempts?.[0]?.status || TspProviderResponseStatus.NOT_SEND,
})
} else throw new NotFoundException('فاکتور مورد نظر شما یافت نشد.')
}
// async send(invoiceId: string, posInfo: IPosPayload) {
// await this.salesInvoiceTaxService.send(invoiceId, posInfo.pos_id)
async send(invoiceId: string, posInfo: IPosPayload) {
const consumer_id = await this.checkAccessToInvoice(
posInfo.consumer_account_id,
posInfo.pos_id,
)
const invoice = await this.salesInvoiceTaxService.send(consumer_id, invoiceId)
// return ResponseMapper.single({
// invoice_id: invoiceId,
// sent_to_tax: true,
// })
// }
return ResponseMapper.single(invoice)
}
async inquiry(consumer_account_id: string, pos_id: string, invoiceId: string) {
const consumer_id = await this.checkAccessToInvoice(consumer_account_id, pos_id)
const invoice = await this.salesInvoiceTaxService.get(invoiceId, pos_id, consumer_id)
return ResponseMapper.single(invoice)
}
// async sendBulk(invoiceIds: string[], posInfo: IPosPayload) {
// if (!invoiceIds.length) {
@@ -325,13 +330,13 @@ export class SalesInvoicesService {
normalizedInvoiceDate,
)
if (data.send_to_tsp) {
await this.salesInvoiceTaxService.send(salesInvoice.id, pos_id)
}
return salesInvoice
})
if (data.send_to_tax) {
await this.salesInvoiceTaxService.send(salesInvoice.id, pos_id)
}
return ResponseMapper.create(salesInvoice)
} catch (error) {
if (
@@ -500,16 +505,12 @@ export class SalesInvoicesService {
}
if (query.status) {
if (query.status === FiscalResponseStatus.NOT_SEND) {
where.fiscal = null
if (query.status === TspProviderResponseStatus.NOT_SEND) {
where.tsp_attempts = undefined
} else {
where.fiscal = {
is: {
attempts: {
some: {
status: query.status,
},
},
where.tsp_attempts = {
some: {
status: query.status,
},
}
}
@@ -530,8 +531,9 @@ export class SalesInvoicesService {
cash: PaymentMethodType.CASH,
set_off: PaymentMethodType.SET_OFF,
card: PaymentMethodType.CARD,
payment_gateway: PaymentMethodType.PAYMENT_GATEWAY,
bank: PaymentMethodType.BANK,
check: PaymentMethodType.CHECK,
check: PaymentMethodType.CHEQUE,
other: PaymentMethodType.OTHER,
terminal: PaymentMethodType.TERMINAL,
}
@@ -705,6 +707,8 @@ export class SalesInvoicesService {
select: {
id: true,
name: true,
code: true,
VAT: true,
},
},
local_sku: true,
@@ -714,6 +718,7 @@ export class SalesInvoicesService {
select: {
id: true,
name: true,
code: true,
},
},
base_sale_price: true,
@@ -755,7 +760,7 @@ export class SalesInvoicesService {
} = params
const { customer_id, customer_type, customer, payments, ...invoiceData } = data
const salesInvoiceData: any = {
const salesInvoiceData: SalesInvoiceCreateInput = {
...invoiceData,
invoice_date: normalizedInvoiceDate,
invoice_number: invoiceNumber,
@@ -764,11 +769,14 @@ export class SalesInvoicesService {
items: {
createMany: {
data: data.items.map(item => ({
good_id: item.good_id,
good_id: item.good_id!,
quantity: item.quantity,
unit_price: item.unit_price,
total_amount: item.total_amount,
measure_unit_text: item.unit_type,
measure_unit_text: goodsById.get(item.good_id!)?.measure_unit.name || null,
measure_unit_code: goodsById.get(item.good_id!)?.measure_unit.code || null,
sku_code: goodsById.get(item.good_id!)?.sku.code || null,
sku_vat: goodsById.get(item.good_id!)?.sku.VAT || null,
payload: item.payload ? JSON.parse(JSON.stringify(item.payload)) : undefined,
good_snapshot: item.good_id
? JSON.parse(
@@ -777,7 +785,6 @@ export class SalesInvoicesService {
}),
)
: undefined,
unit_type: item.unit_type,
})),
},
},
@@ -794,6 +801,16 @@ export class SalesInvoicesService {
},
}
if (data.send_to_tsp) {
salesInvoiceData.tsp_attempts = {
create: {
attempt_no: 1,
status: TspProviderResponseStatus.QUEUED,
type: TspProviderRequestType.MAIN,
},
}
}
if (customerId) {
salesInvoiceData.customer = {
connect: {
@@ -819,10 +836,29 @@ export class SalesInvoicesService {
},
select: {
invoice_number: true,
pos: {
select: {
complex: {
select: {
business_activity: {
select: {
invoice_number_sequence: true,
},
},
},
},
},
},
},
})
return (latestInvoice?.invoice_number || 0) + 1
const latestSequence =
latestInvoice?.pos?.complex?.business_activity?.invoice_number_sequence.toNumber() ||
0
console.log(latestSequence)
return Math.max(latestInvoice?.invoice_number || 0, latestSequence) + 1
}
private async createPayments(
@@ -852,7 +888,7 @@ export class SalesInvoicesService {
terminal_id: terminalInfo.terminalId,
stan: terminalInfo.stan,
rrn: terminalInfo.rrn,
transaction_date_time: new Date(terminalInfo.transactionDateTime),
transaction_date_time: new Date(terminalInfo.transactionDateTime || ''),
customer_card_no: terminalInfo.customerCardNO || null,
description: terminalInfo.description || null,
},
@@ -867,6 +903,35 @@ export class SalesInvoicesService {
pos_id: string,
invoice_number: number,
) {
return `${business_id.substring(0, 2)}-${complex_id.substring(0, 2)}-${pos_id.substring(0, 2)}-${invoice_number.toString()}`
return `${business_id.substring(business_id.length - 2, business_id.length)}-${complex_id.substring(complex_id.length - 2, complex_id.length)}-${pos_id.substring(pos_id.length - 2, pos_id.length)}-${invoice_number.toString()}`
}
private async checkAccessToInvoice(
consumer_account_id: string,
pos_id: string,
): Promise<string> {
const consumer = await this.prisma.consumerAccount.findUnique({
where: {
id: consumer_account_id,
OR: [
{
pos: {
id: pos_id,
},
},
{
role: ConsumerRole.OWNER,
},
],
},
select: {
consumer_id: true,
},
})
if (!consumer) {
throw new BadRequestException('شما دسترسی لازم برای ارسال فاکتور را ندارید.')
}
return consumer.consumer_id
}
}