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:
@@ -136,7 +136,7 @@ export class CreateSalesInvoiceDto {
|
||||
@ApiProperty({ required: false, default: false })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
send_to_tax?: boolean
|
||||
send_to_tsp?: boolean
|
||||
|
||||
@ApiProperty({ required: true, default: CustomerType.UNKNOWN })
|
||||
@IsEnum(CustomerType)
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
IsString,
|
||||
Min,
|
||||
} from 'class-validator'
|
||||
import { FiscalResponseStatus } from 'generated/prisma/enums'
|
||||
import { TspProviderResponseStatus } from 'generated/prisma/enums'
|
||||
|
||||
export class SalesInvoicesFilterDto {
|
||||
@ApiPropertyOptional({ default: 1 })
|
||||
@@ -70,10 +70,10 @@ export class SalesInvoicesFilterDto {
|
||||
@IsString()
|
||||
customer_economic_code?: string
|
||||
|
||||
@ApiPropertyOptional({ enum: FiscalResponseStatus })
|
||||
@ApiPropertyOptional({ enum: TspProviderResponseStatus })
|
||||
@IsOptional()
|
||||
@IsEnum(FiscalResponseStatus)
|
||||
status?: FiscalResponseStatus
|
||||
@IsEnum(TspProviderResponseStatus)
|
||||
status?: TspProviderResponseStatus
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { PosInfo } from '@/common/decorators/posInfo.decorator'
|
||||
import type { IPosPayload } from '@/common/models/posPayload.model'
|
||||
import { Controller, Get, Param, Post } from '@nestjs/common'
|
||||
import { PosSalesInvoiceFiscalService } from './sales-invoice-fiscal.service'
|
||||
|
||||
@Controller('pos/sales_invoices/:invoiceId/fiscal')
|
||||
export class PosSalesInvoiceFiscalController {
|
||||
constructor(private readonly service: PosSalesInvoiceFiscalService) {}
|
||||
|
||||
@Post('send')
|
||||
async send(@PosInfo() posInfo: IPosPayload, @Param('invoiceId') invoiceId: string) {
|
||||
return this.service.send(invoiceId, posInfo)
|
||||
}
|
||||
|
||||
@Post('retry')
|
||||
async retry(@PosInfo() posInfo: IPosPayload, @Param('invoiceId') invoiceId: string) {
|
||||
return this.service.retry(invoiceId, posInfo)
|
||||
}
|
||||
|
||||
@Get('status')
|
||||
async getStatus(
|
||||
@PosInfo() posInfo: IPosPayload,
|
||||
@Param('invoiceId') invoiceId: string,
|
||||
) {
|
||||
return this.service.getStatus(invoiceId, posInfo)
|
||||
}
|
||||
|
||||
@Post('status/refresh')
|
||||
async refreshStatus(
|
||||
@PosInfo() posInfo: IPosPayload,
|
||||
@Param('invoiceId') invoiceId: string,
|
||||
) {
|
||||
return this.service.refreshStatus(invoiceId, posInfo)
|
||||
}
|
||||
|
||||
@Get('attempts')
|
||||
async getAttempts(
|
||||
@PosInfo() posInfo: IPosPayload,
|
||||
@Param('invoiceId') invoiceId: string,
|
||||
) {
|
||||
return this.service.getAttempts(invoiceId, posInfo)
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Module } from '@nestjs/common'
|
||||
import { SalesInvoiceFiscalSwitchService } from '../../../consumer/saleInvoices/fiscal/sales-invoice-fiscal-switch.service'
|
||||
import { NamaTaxSwitchAdapter } from '../../../consumer/saleInvoices/fiscal/switch/nama/nama-fiscal-switch.adapter'
|
||||
import { PosSalesInvoiceFiscalController } from './sales-invoice-fiscal.controller'
|
||||
import { PosSalesInvoiceFiscalService } from './sales-invoice-fiscal.service'
|
||||
|
||||
@Module({
|
||||
controllers: [PosSalesInvoiceFiscalController],
|
||||
providers: [PosSalesInvoiceFiscalService, SalesInvoiceFiscalSwitchService, NamaTaxSwitchAdapter],
|
||||
exports: [PosSalesInvoiceFiscalService],
|
||||
})
|
||||
export class PosSalesInvoiceFiscalModule {}
|
||||
@@ -1,411 +0,0 @@
|
||||
import { IPosPayload } from '@/common/models/posPayload.model'
|
||||
import { PrismaService } from '@/prisma/prisma.service'
|
||||
import { Injectable, NotFoundException } from '@nestjs/common'
|
||||
import {
|
||||
FiscalInvoiceCustomerType,
|
||||
FiscalRequestType,
|
||||
FiscalResponseStatus,
|
||||
} from 'generated/prisma/enums'
|
||||
import {
|
||||
TaxSendStatus,
|
||||
TaxSwitchGetResultDto,
|
||||
TaxSwitchSendItemResultDto,
|
||||
TaxSwitchSendPayloadDto,
|
||||
} from '../../../consumer/saleInvoices/fiscal/dto/tax-switch.dto'
|
||||
import { SalesInvoiceFiscalSwitchService } from '../../../consumer/saleInvoices/fiscal/sales-invoice-fiscal-switch.service'
|
||||
|
||||
@Injectable()
|
||||
export class PosSalesInvoiceFiscalService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly fiscalSwitchService: SalesInvoiceFiscalSwitchService,
|
||||
) {}
|
||||
|
||||
async send(invoiceId: string, posInfo: IPosPayload) {
|
||||
const payload = await this.buildPayload(invoiceId, posInfo)
|
||||
const itemResults = await this.safeSend(payload)
|
||||
const attempt = await this.persistAttempt(invoiceId, itemResults)
|
||||
|
||||
return {
|
||||
invoice_id: invoiceId,
|
||||
fiscal_id: attempt.fiscal_id,
|
||||
status: attempt.status,
|
||||
attempt_no: attempt.attempt_no,
|
||||
}
|
||||
}
|
||||
|
||||
async retry(invoiceId: string, posInfo: IPosPayload) {
|
||||
return this.send(invoiceId, posInfo)
|
||||
}
|
||||
|
||||
async getStatus(invoiceId: string, posInfo: IPosPayload) {
|
||||
const invoice = await this.getInvoiceForBusiness(invoiceId, posInfo.business_id)
|
||||
if (!invoice.fiscal) {
|
||||
return {
|
||||
invoice_id: invoiceId,
|
||||
status: FiscalResponseStatus.NOT_SEND,
|
||||
fiscal: null,
|
||||
}
|
||||
}
|
||||
|
||||
const latestAttempt = invoice.fiscal.attempts[0] || null
|
||||
return {
|
||||
invoice_id: invoiceId,
|
||||
status: latestAttempt?.status || FiscalResponseStatus.NOT_SEND,
|
||||
fiscal: {
|
||||
id: invoice.fiscal.id,
|
||||
retry_count: invoice.fiscal.retry_count,
|
||||
last_attempt_at: invoice.fiscal.last_attempt_at,
|
||||
},
|
||||
latest_attempt: latestAttempt,
|
||||
}
|
||||
}
|
||||
|
||||
async refreshStatus(invoiceId: string, posInfo: IPosPayload) {
|
||||
const invoice = await this.getInvoiceForBusiness(invoiceId, posInfo.business_id)
|
||||
const latestAttempt = invoice.fiscal?.attempts?.[0]
|
||||
|
||||
if (!invoice.fiscal || !latestAttempt?.tax_id) {
|
||||
return this.getStatus(invoiceId, posInfo)
|
||||
}
|
||||
|
||||
const providerCode = invoice.pos.provider?.code
|
||||
const switchResult = await this.fiscalSwitchService.get(
|
||||
providerCode,
|
||||
latestAttempt.tax_id,
|
||||
)
|
||||
const mappedStatus = this.mapSwitchGetStatus(switchResult.status)
|
||||
|
||||
const refreshedAttempt = await this.prisma.$transaction(async tx => {
|
||||
const nextNo = (latestAttempt.attempt_no || 0) + 1
|
||||
const createdAttempt = await tx.saleInvoiceFiscalAttempts.create({
|
||||
data: {
|
||||
fiscal_id: invoice.fiscal!.id,
|
||||
attempt_no: nextNo,
|
||||
status: mappedStatus,
|
||||
tax_id: switchResult.tax_id,
|
||||
response_payload: switchResult.response_payload
|
||||
? JSON.parse(JSON.stringify(switchResult.response_payload))
|
||||
: undefined,
|
||||
received_at: switchResult.received_at
|
||||
? new Date(switchResult.received_at)
|
||||
: null,
|
||||
type: FiscalRequestType.MAIN,
|
||||
},
|
||||
})
|
||||
|
||||
await tx.saleInvoiceFiscals.update({
|
||||
where: { id: invoice.fiscal!.id },
|
||||
data: {
|
||||
retry_count: nextNo,
|
||||
last_attempt_at: createdAttempt.created_at,
|
||||
},
|
||||
})
|
||||
|
||||
return createdAttempt
|
||||
})
|
||||
|
||||
return {
|
||||
invoice_id: invoiceId,
|
||||
status: refreshedAttempt.status,
|
||||
latest_attempt: refreshedAttempt,
|
||||
}
|
||||
}
|
||||
|
||||
async getAttempts(invoiceId: string, posInfo: IPosPayload) {
|
||||
const invoice = await this.getInvoiceForBusiness(invoiceId, posInfo.business_id)
|
||||
if (!invoice.fiscal) {
|
||||
return {
|
||||
invoice_id: invoiceId,
|
||||
fiscal_id: null,
|
||||
attempts: [],
|
||||
}
|
||||
}
|
||||
|
||||
const attempts = await this.prisma.saleInvoiceFiscalAttempts.findMany({
|
||||
where: {
|
||||
fiscal_id: invoice.fiscal.id,
|
||||
},
|
||||
orderBy: {
|
||||
attempt_no: 'desc',
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
invoice_id: invoiceId,
|
||||
fiscal_id: invoice.fiscal.id,
|
||||
attempts,
|
||||
}
|
||||
}
|
||||
|
||||
private async buildPayload(invoiceId: string, posInfo: IPosPayload) {
|
||||
const invoice = await this.prisma.salesInvoice.findUnique({
|
||||
where: {
|
||||
id: invoiceId,
|
||||
pos: {
|
||||
id: posInfo.pos_id,
|
||||
complex: {
|
||||
business_activity_id: posInfo.business_id,
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
invoice_date: true,
|
||||
total_amount: true,
|
||||
items: {
|
||||
select: {
|
||||
id: true,
|
||||
quantity: true,
|
||||
unit_price: true,
|
||||
total_amount: true,
|
||||
measure_unit_text: true,
|
||||
sku_code: true,
|
||||
sku_vat: true,
|
||||
measure_unit_code: true,
|
||||
good_id: true,
|
||||
service_id: true,
|
||||
payload: true,
|
||||
good_snapshot: true,
|
||||
},
|
||||
},
|
||||
customer: {
|
||||
select: {
|
||||
type: true,
|
||||
individual: true,
|
||||
legal: true,
|
||||
},
|
||||
},
|
||||
|
||||
pos: {
|
||||
select: {
|
||||
complex: {
|
||||
select: {
|
||||
business_activity: {
|
||||
select: {
|
||||
fiscal_id: true,
|
||||
economic_code: true,
|
||||
consumer: {
|
||||
select: {
|
||||
legal: {
|
||||
select: {
|
||||
partner: {
|
||||
select: {
|
||||
fiscal_switch_type: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
individual: {
|
||||
select: {
|
||||
partner: {
|
||||
select: {
|
||||
fiscal_switch_type: 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_code: invoice.code,
|
||||
invoice_date: invoice.invoice_date,
|
||||
total_amount: Number(invoice.total_amount),
|
||||
customer_type: invoice.customer
|
||||
? FiscalInvoiceCustomerType.Known
|
||||
: FiscalInvoiceCustomerType.Unknown,
|
||||
type: FiscalRequestType.MAIN,
|
||||
provider_code: partner.fiscal_switch_type,
|
||||
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),
|
||||
good_id: item.good_id,
|
||||
service_id: item.service_id,
|
||||
payload: item.payload,
|
||||
good_snapshot: item.good_snapshot,
|
||||
discount: String((item.payload as Record<string, any>)?.discount || '0'),
|
||||
sku: item.sku_code,
|
||||
sku_vat: String(item.sku_vat),
|
||||
measure_unit: item.measure_unit_code,
|
||||
// sku: item.
|
||||
})),
|
||||
} satisfies TaxSwitchSendPayloadDto
|
||||
}
|
||||
|
||||
private async safeSend(payload: TaxSwitchSendPayloadDto) {
|
||||
try {
|
||||
return await this.fiscalSwitchService.send(payload)
|
||||
} catch (error: any) {
|
||||
const now = new Date().toISOString()
|
||||
return payload.items.map(item => ({
|
||||
invoice_item_id: item.invoice_item_id,
|
||||
status: TaxSendStatus.FAILED,
|
||||
error_message: error?.message || 'Unexpected fiscal switch error.',
|
||||
sent_at: now,
|
||||
received_at: now,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
private async persistAttempt(
|
||||
invoiceId: string,
|
||||
itemResults: TaxSwitchSendItemResultDto[],
|
||||
) {
|
||||
return this.prisma.$transaction(async tx => {
|
||||
const fiscal = await tx.saleInvoiceFiscals.upsert({
|
||||
where: { invoice_id: invoiceId },
|
||||
update: {},
|
||||
create: {
|
||||
invoice_id: invoiceId,
|
||||
},
|
||||
})
|
||||
|
||||
const latest = await tx.saleInvoiceFiscalAttempts.findFirst({
|
||||
where: { fiscal_id: fiscal.id },
|
||||
orderBy: { attempt_no: 'desc' },
|
||||
select: { attempt_no: true },
|
||||
})
|
||||
const nextAttemptNo = (latest?.attempt_no || 0) + 1
|
||||
|
||||
const finalStatus = this.mapItemResultsStatus(itemResults)
|
||||
const firstTaxId = itemResults.find(item => item.tax_id)?.tax_id || null
|
||||
const sentAt = itemResults.map(item => item.sent_at).find(Boolean)
|
||||
const receivedAt = itemResults.map(item => item.received_at).find(Boolean)
|
||||
const errors = itemResults.map(item => item.error_message).filter(Boolean)
|
||||
const errorMessage = errors.length ? Array.from(new Set(errors)).join(' | ') : null
|
||||
|
||||
const attempt = await tx.saleInvoiceFiscalAttempts.create({
|
||||
data: {
|
||||
fiscal_id: fiscal.id,
|
||||
attempt_no: nextAttemptNo,
|
||||
status: finalStatus,
|
||||
tax_id: firstTaxId,
|
||||
type: FiscalRequestType.MAIN,
|
||||
request_payload: JSON.parse(
|
||||
JSON.stringify(itemResults.map(item => item.request_payload)),
|
||||
),
|
||||
response_payload: JSON.parse(
|
||||
JSON.stringify(itemResults.map(item => item.response_payload)),
|
||||
),
|
||||
error_message: errorMessage,
|
||||
sent_at: sentAt ? new Date(sentAt) : null,
|
||||
received_at: receivedAt ? new Date(receivedAt) : null,
|
||||
},
|
||||
})
|
||||
|
||||
await tx.saleInvoiceFiscals.update({
|
||||
where: { id: fiscal.id },
|
||||
data: {
|
||||
retry_count: nextAttemptNo,
|
||||
last_attempt_at: attempt.sent_at || attempt.created_at,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
fiscal_id: fiscal.id,
|
||||
status: attempt.status,
|
||||
attempt_no: attempt.attempt_no,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private mapItemResultsStatus(itemResults: TaxSwitchSendItemResultDto[]) {
|
||||
if (!itemResults.length) {
|
||||
return FiscalResponseStatus.NOT_SEND
|
||||
}
|
||||
const hasFailure = itemResults.some(item => item.status === TaxSendStatus.FAILED)
|
||||
if (hasFailure) {
|
||||
return FiscalResponseStatus.FAILURE
|
||||
}
|
||||
const hasSent = itemResults.some(item => item.status === TaxSendStatus.SENT)
|
||||
if (hasSent) {
|
||||
return FiscalResponseStatus.SUCCESS
|
||||
}
|
||||
return FiscalResponseStatus.NOT_SEND
|
||||
}
|
||||
|
||||
private mapSwitchGetStatus(status: TaxSwitchGetResultDto['status']) {
|
||||
switch (status) {
|
||||
case TaxSendStatus.SENT:
|
||||
return FiscalResponseStatus.SUCCESS
|
||||
case TaxSendStatus.FAILED:
|
||||
return FiscalResponseStatus.FAILURE
|
||||
default:
|
||||
return FiscalResponseStatus.NOT_SEND
|
||||
}
|
||||
}
|
||||
|
||||
private async getInvoiceForBusiness(invoiceId: string, businessId: string) {
|
||||
const invoice = await this.prisma.salesInvoice.findFirst({
|
||||
where: {
|
||||
id: invoiceId,
|
||||
pos: {
|
||||
complex: {
|
||||
business_activity_id: businessId,
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
pos: {
|
||||
select: {
|
||||
provider: {
|
||||
select: {
|
||||
code: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
fiscal: {
|
||||
select: {
|
||||
id: true,
|
||||
retry_count: true,
|
||||
last_attempt_at: true,
|
||||
attempts: {
|
||||
orderBy: {
|
||||
attempt_no: 'desc',
|
||||
},
|
||||
take: 1,
|
||||
select: {
|
||||
id: true,
|
||||
attempt_no: true,
|
||||
status: true,
|
||||
tax_id: true,
|
||||
sent_at: true,
|
||||
received_at: true,
|
||||
error_message: true,
|
||||
created_at: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!invoice) {
|
||||
throw new NotFoundException('فاکتور مورد نظر یافت نشد.')
|
||||
}
|
||||
|
||||
return invoice
|
||||
}
|
||||
}
|
||||
+1
-6
@@ -1,7 +1,6 @@
|
||||
import { ApiProperty, PartialType } from '@nestjs/swagger'
|
||||
import { IsEnum, IsNumber, IsObject, IsOptional, IsString } from 'class-validator'
|
||||
import { IsNumber, IsObject, IsOptional, IsString } from 'class-validator'
|
||||
import type { SaleInvoiceType } from 'common/interfaces/sale-invoice-payload'
|
||||
import { UnitType } from 'generated/prisma/enums'
|
||||
|
||||
export class CreateSalesInvoiceItemDto {
|
||||
@IsNumber()
|
||||
@@ -24,10 +23,6 @@ export class CreateSalesInvoiceItemDto {
|
||||
@ApiProperty({ required: true })
|
||||
invoice_id: string
|
||||
|
||||
@IsEnum(UnitType)
|
||||
@ApiProperty({ enum: Object.values(UnitType) })
|
||||
unit_type: UnitType
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ required: false })
|
||||
|
||||
@@ -20,15 +20,24 @@ export class SalesInvoicesController {
|
||||
return this.salesInvoicesService.findOne(posInfo, id)
|
||||
}
|
||||
|
||||
@Get(':id/inquiry')
|
||||
inquiry(@PosInfo() posInfo: IPosPayload, @Param('id') id: string) {
|
||||
return this.salesInvoicesService.inquiry(
|
||||
posInfo.consumer_account_id,
|
||||
posInfo.pos_id,
|
||||
id,
|
||||
)
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@Body() data: CreateSalesInvoiceDto, @PosInfo() posInfo: IPosPayload) {
|
||||
return this.salesInvoicesService.create(data, posInfo)
|
||||
}
|
||||
|
||||
// @Post(':id/send')
|
||||
// send(@Param('id') id: string, @PosInfo() posInfo: IPosPayload) {
|
||||
// return this.salesInvoicesService.send(id, posInfo)
|
||||
// }
|
||||
@Post(':id/send')
|
||||
send(@Param('id') id: string, @PosInfo() posInfo: IPosPayload) {
|
||||
return this.salesInvoicesService.send(id, posInfo)
|
||||
}
|
||||
|
||||
// @Post('send/bulk')
|
||||
// sendBulk(@Body() data: SendBulkSalesInvoicesDto, @PosInfo() posInfo: IPosPayload) {
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { Module } from '@nestjs/common'
|
||||
import { SalesInvoiceFiscalSwitchService } from '../../consumer/saleInvoices/fiscal/sales-invoice-fiscal-switch.service'
|
||||
import { SalesInvoiceFiscalService } from '../../consumer/saleInvoices/fiscal/sales-invoice-fiscal.service'
|
||||
import { NamaTaxSwitchAdapter } from '../../consumer/saleInvoices/fiscal/switch/nama/nama-fiscal-switch.adapter'
|
||||
import { PosSalesInvoiceFiscalModule } from './fiscal/sales-invoice-fiscal.module'
|
||||
import { HttpClientUtil } from '../../../common/utils/http-client.util'
|
||||
import { SalesInvoiceTspSwitchService } from '../../tspProviders/sales-invoice-tsp-switch.service'
|
||||
import { SalesInvoiceTspService } from '../../tspProviders/sales-invoice-tsp.service'
|
||||
import { NamaProviderSwitchAdapter } from '../../tspProviders/switch/nama/nama-provider.adapter'
|
||||
import { SalesInvoicesController } from './sales-invoices.controller'
|
||||
import { SalesInvoicesService } from './sales-invoices.service'
|
||||
|
||||
@Module({
|
||||
imports: [PosSalesInvoiceFiscalModule],
|
||||
controllers: [SalesInvoicesController],
|
||||
providers: [
|
||||
SalesInvoicesService,
|
||||
SalesInvoiceFiscalService,
|
||||
SalesInvoiceFiscalSwitchService,
|
||||
NamaTaxSwitchAdapter,
|
||||
HttpClientUtil,
|
||||
SalesInvoiceTspService,
|
||||
SalesInvoiceTspSwitchService,
|
||||
NamaProviderSwitchAdapter,
|
||||
],
|
||||
})
|
||||
export class PosSalesInvoicesModule {}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user